Compare commits
2 Commits
673a7c89a6
...
05757aefdc
Author | SHA1 | Date | |
---|---|---|---|
05757aefdc | |||
55bcb08668 |
@ -1 +0,0 @@
|
|||||||
./space/* "Project Admin"
|
|
@ -7,10 +7,7 @@ plugins {
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "space.kscience"
|
group = "space.kscience"
|
||||||
version = "0.4.0-dev-3"
|
version = "0.4.0-dev-4"
|
||||||
repositories{
|
|
||||||
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ksciencePublish {
|
ksciencePublish {
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
package space.kscience.controls.constructor
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
import space.kscience.controls.spec.DevicePropertySpec
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
@ -25,29 +22,24 @@ public abstract class DeviceConstructor(
|
|||||||
context: Context,
|
context: Context,
|
||||||
meta: Meta = Meta.EMPTY,
|
meta: Meta = Meta.EMPTY,
|
||||||
) : DeviceGroup(context, meta), StateContainer {
|
) : DeviceGroup(context, meta), StateContainer {
|
||||||
private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf()
|
private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf()
|
||||||
override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors
|
override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors
|
||||||
|
|
||||||
override fun registerState(stateDescriptor: StateDescriptor) {
|
override fun registerState(stateDescriptor: StateDescriptor) {
|
||||||
_stateDescriptors.add(stateDescriptor)
|
_stateDescriptors.add(stateDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
override fun unregisterState(stateDescriptor: StateDescriptor) {
|
||||||
super.registerProperty(descriptor, state)
|
_stateDescriptors.remove(stateDescriptor)
|
||||||
registerState(PropertyStateDescriptor(this, descriptor.name, state))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun <T> registerProperty(
|
||||||
* Bind an action to a [DeviceState]. [onChange] block is performed on each state change
|
converter: MetaConverter<T>,
|
||||||
*
|
descriptor: PropertyDescriptor,
|
||||||
* Optionally provide [writes] - a set of states that this change affects.
|
state: DeviceState<T>,
|
||||||
*/
|
) {
|
||||||
public fun <T> DeviceState<T>.onChange(
|
super.registerProperty(converter, descriptor, state)
|
||||||
vararg writes: DeviceState<*>,
|
registerState(StatePropertyDescriptor(this, descriptor.name, state))
|
||||||
reads: Collection<DeviceState<*>>,
|
|
||||||
onChange: suspend (T) -> Unit,
|
|
||||||
): Job = valueFlow.onEach(onChange).launchIn(this@DeviceConstructor).also {
|
|
||||||
registerState(ConnectionStateDescriptor(setOf(this, *reads.toTypedArray()), setOf(*writes)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +76,7 @@ public fun <D : Device> DeviceConstructor.device(
|
|||||||
* Register a property and provide a direct reader for it
|
* Register a property and provide a direct reader for it
|
||||||
*/
|
*/
|
||||||
public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
state: S,
|
state: S,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
nameOverride: String? = null,
|
nameOverride: String? = null,
|
||||||
@ -91,7 +84,7 @@ public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
|||||||
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
||||||
val name = nameOverride ?: property.name
|
val name = nameOverride ?: property.name
|
||||||
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
||||||
registerProperty(descriptor, state)
|
registerProperty(converter, descriptor, state)
|
||||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@ -108,7 +101,8 @@ public fun <T : Any> DeviceConstructor.property(
|
|||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
nameOverride: String? = null,
|
nameOverride: String? = null,
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
|
||||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
|
metaConverter,
|
||||||
|
DeviceState.external(this, readInterval, initialState, reader),
|
||||||
descriptorBuilder,
|
descriptorBuilder,
|
||||||
nameOverride,
|
nameOverride,
|
||||||
)
|
)
|
||||||
@ -125,7 +119,8 @@ public fun <T : Any> DeviceConstructor.mutableProperty(
|
|||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
nameOverride: String? = null,
|
nameOverride: String? = null,
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
|
metaConverter,
|
||||||
|
DeviceState.external(this, readInterval, initialState, reader, writer),
|
||||||
descriptorBuilder,
|
descriptorBuilder,
|
||||||
nameOverride,
|
nameOverride,
|
||||||
)
|
)
|
||||||
@ -140,7 +135,8 @@ public fun <T> DeviceConstructor.virtualProperty(
|
|||||||
nameOverride: String? = null,
|
nameOverride: String? = null,
|
||||||
callback: (T) -> Unit = {},
|
callback: (T) -> Unit = {},
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||||
DeviceState.internal(metaConverter, initialState, callback),
|
metaConverter,
|
||||||
|
MutableDeviceState(initialState, callback),
|
||||||
descriptorBuilder,
|
descriptorBuilder,
|
||||||
nameOverride,
|
nameOverride,
|
||||||
)
|
)
|
||||||
@ -153,7 +149,7 @@ public fun <T, D : Device> DeviceConstructor.deviceProperty(
|
|||||||
property: DevicePropertySpec<D, T>,
|
property: DevicePropertySpec<D, T>,
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> =
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> =
|
||||||
property(device.propertyAsState(property, initialValue))
|
property(property.converter, device.propertyAsState(property, initialValue))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind existing property provided by specification to this device
|
* Bind existing property provided by specification to this device
|
||||||
@ -163,4 +159,4 @@ public fun <T, D : Device> DeviceConstructor.deviceProperty(
|
|||||||
property: MutableDevicePropertySpec<D, T>,
|
property: MutableDevicePropertySpec<D, T>,
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> =
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> =
|
||||||
property(device.mutablePropertyAsState(property, initialValue))
|
property(property.converter, device.mutablePropertyAsState(property, initialValue))
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
package space.kscience.controls.constructor
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import space.kscience.controls.api.*
|
import space.kscience.controls.api.*
|
||||||
import space.kscience.controls.api.DeviceLifecycleState.*
|
import space.kscience.controls.api.DeviceLifecycleState.*
|
||||||
import space.kscience.controls.manager.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
import space.kscience.controls.manager.install
|
import space.kscience.controls.manager.install
|
||||||
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
import space.kscience.dataforge.meta.Laminate
|
import space.kscience.dataforge.meta.Laminate
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
@ -30,12 +28,21 @@ public open class DeviceGroup(
|
|||||||
override val meta: Meta,
|
override val meta: Meta,
|
||||||
) : DeviceHub, CachingDevice {
|
) : DeviceHub, CachingDevice {
|
||||||
|
|
||||||
internal class Property(
|
private class Property<T>(
|
||||||
val state: DeviceState<*>,
|
val state: DeviceState<T>,
|
||||||
|
val converter: MetaConverter<T>,
|
||||||
val descriptor: PropertyDescriptor,
|
val descriptor: PropertyDescriptor,
|
||||||
)
|
) {
|
||||||
|
val valueAsMeta get() = converter.convert(state.value)
|
||||||
|
|
||||||
internal class Action(
|
fun setMeta(meta: Meta) {
|
||||||
|
check(state is MutableDeviceState) { "Can't write to read-only property" }
|
||||||
|
|
||||||
|
state.value = converter.read(meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Action(
|
||||||
val invoke: suspend (Meta?) -> Meta?,
|
val invoke: suspend (Meta?) -> Meta?,
|
||||||
val descriptor: ActionDescriptor,
|
val descriptor: ActionDescriptor,
|
||||||
)
|
)
|
||||||
@ -81,16 +88,20 @@ public open class DeviceGroup(
|
|||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
private val properties: MutableMap<Name, Property> = hashMapOf()
|
private val properties: MutableMap<Name, Property<*>> = hashMapOf()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new property based on [DeviceState]. Properties could be modified dynamically
|
* Register a new property based on [DeviceState]. Properties could be modified dynamically
|
||||||
*/
|
*/
|
||||||
public open fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
public open fun <T> registerProperty(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
descriptor: PropertyDescriptor,
|
||||||
|
state: DeviceState<T>,
|
||||||
|
) {
|
||||||
val name = descriptor.name.parseAsName()
|
val name = descriptor.name.parseAsName()
|
||||||
require(properties[name] == null) { "Can't add property with name $name. It already exists." }
|
require(properties[name] == null) { "Can't add property with name $name. It already exists." }
|
||||||
properties[name] = Property(state, descriptor)
|
properties[name] = Property(state, converter, descriptor)
|
||||||
state.metaFlow.onEach {
|
state.valueFlow.map(converter::convert).onEach {
|
||||||
sharedMessageFlow.emit(
|
sharedMessageFlow.emit(
|
||||||
PropertyChangedMessage(
|
PropertyChangedMessage(
|
||||||
descriptor.name,
|
descriptor.name,
|
||||||
@ -109,19 +120,18 @@ public open class DeviceGroup(
|
|||||||
get() = actions.values.map { it.descriptor }
|
get() = actions.values.map { it.descriptor }
|
||||||
|
|
||||||
override suspend fun readProperty(propertyName: String): Meta =
|
override suspend fun readProperty(propertyName: String): Meta =
|
||||||
properties[propertyName.parseAsName()]?.state?.valueAsMeta
|
properties[propertyName.parseAsName()]?.valueAsMeta
|
||||||
?: error("Property with name $propertyName not found")
|
?: error("Property with name $propertyName not found")
|
||||||
|
|
||||||
override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.state?.valueAsMeta
|
override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.valueAsMeta
|
||||||
|
|
||||||
override suspend fun invalidate(propertyName: String) {
|
override suspend fun invalidate(propertyName: String) {
|
||||||
//does nothing for this implementation
|
//does nothing for this implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeProperty(propertyName: String, value: Meta) {
|
override suspend fun writeProperty(propertyName: String, value: Meta) {
|
||||||
val property = (properties[propertyName.parseAsName()]?.state as? MutableDeviceState)
|
val property = properties[propertyName.parseAsName()] ?: error("Property with name $propertyName not found")
|
||||||
?: error("Property with name $propertyName not found")
|
property.setMeta(value)
|
||||||
property.valueAsMeta = value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -164,6 +174,10 @@ public open class DeviceGroup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun <T> DeviceGroup.registerProperty(propertySpec: DevicePropertySpec<*, T>, state: DeviceState<T>) {
|
||||||
|
registerProperty(propertySpec.converter, propertySpec.descriptor, state)
|
||||||
|
}
|
||||||
|
|
||||||
public fun DeviceManager.registerDeviceGroup(
|
public fun DeviceManager.registerDeviceGroup(
|
||||||
name: String = "@group",
|
name: String = "@group",
|
||||||
meta: Meta = Meta.EMPTY,
|
meta: Meta = Meta.EMPTY,
|
||||||
@ -234,10 +248,12 @@ public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -
|
|||||||
*/
|
*/
|
||||||
public fun <T : Any> DeviceGroup.registerProperty(
|
public fun <T : Any> DeviceGroup.registerProperty(
|
||||||
name: String,
|
name: String,
|
||||||
|
converter: MetaConverter<T>,
|
||||||
state: DeviceState<T>,
|
state: DeviceState<T>,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
registerProperty(
|
registerProperty(
|
||||||
|
converter,
|
||||||
PropertyDescriptor(name).apply(descriptorBuilder),
|
PropertyDescriptor(name).apply(descriptorBuilder),
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
@ -248,10 +264,12 @@ public fun <T : Any> DeviceGroup.registerProperty(
|
|||||||
*/
|
*/
|
||||||
public fun <T : Any> DeviceGroup.registerMutableProperty(
|
public fun <T : Any> DeviceGroup.registerMutableProperty(
|
||||||
name: String,
|
name: String,
|
||||||
|
converter: MetaConverter<T>,
|
||||||
state: MutableDeviceState<T>,
|
state: MutableDeviceState<T>,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
registerProperty(
|
registerProperty(
|
||||||
|
converter,
|
||||||
PropertyDescriptor(name).apply(descriptorBuilder),
|
PropertyDescriptor(name).apply(descriptorBuilder),
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
@ -269,7 +287,7 @@ public fun <T : Any> DeviceGroup.registerVirtualProperty(
|
|||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
callback: (T) -> Unit = {},
|
callback: (T) -> Unit = {},
|
||||||
): MutableDeviceState<T> {
|
): MutableDeviceState<T> {
|
||||||
val state = DeviceState.internal<T>(converter, initialValue, callback)
|
val state = MutableDeviceState<T>(initialValue, callback)
|
||||||
registerMutableProperty(name, state, descriptorBuilder)
|
registerMutableProperty(name, converter, state, descriptorBuilder)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,32 @@
|
|||||||
package space.kscience.controls.constructor
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.newCoroutineContext
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
public abstract class DeviceModel(override val context: Context) : StateContainer {
|
public abstract class DeviceModel(
|
||||||
|
final override val context: Context,
|
||||||
|
vararg dependencies: DeviceState<*>,
|
||||||
|
) : StateContainer, CoroutineScope {
|
||||||
|
|
||||||
private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf()
|
override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob())
|
||||||
|
|
||||||
override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors
|
|
||||||
|
private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf<StateDescriptor>().apply {
|
||||||
|
dependencies.forEach {
|
||||||
|
add(StateNodeDescriptor(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors
|
||||||
|
|
||||||
override fun registerState(stateDescriptor: StateDescriptor) {
|
override fun registerState(stateDescriptor: StateDescriptor) {
|
||||||
_stateDescriptors.add(stateDescriptor)
|
_stateDescriptors.add(stateDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun unregisterState(stateDescriptor: StateDescriptor) {
|
||||||
|
_stateDescriptors.remove(stateDescriptor)
|
||||||
|
}
|
||||||
}
|
}
|
@ -6,15 +6,12 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable state of a device
|
* An observable state of a device
|
||||||
*/
|
*/
|
||||||
public interface DeviceState<T> {
|
public interface DeviceState<T> {
|
||||||
public val converter: MetaConverter<T>
|
|
||||||
public val value: T
|
public val value: T
|
||||||
|
|
||||||
public val valueFlow: Flow<T>
|
public val valueFlow: Flow<T>
|
||||||
@ -24,9 +21,6 @@ public interface DeviceState<T> {
|
|||||||
public companion object
|
public companion object
|
||||||
}
|
}
|
||||||
|
|
||||||
public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::convert)
|
|
||||||
|
|
||||||
public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.convert(value)
|
|
||||||
|
|
||||||
public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
|
public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
|
||||||
|
|
||||||
@ -47,12 +41,6 @@ public operator fun <T> MutableDeviceState<T>.setValue(thisRef: Any?, property:
|
|||||||
this.value = value
|
this.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
public var <T> MutableDeviceState<T>.valueAsMeta: Meta
|
|
||||||
get() = converter.convert(value)
|
|
||||||
set(arg) {
|
|
||||||
value = converter.read(arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Device state with a value that depends on other device states
|
* Device state with a value that depends on other device states
|
||||||
*/
|
*/
|
||||||
@ -65,17 +53,15 @@ public interface DeviceStateWithDependencies<T> : DeviceState<T> {
|
|||||||
*/
|
*/
|
||||||
public fun <T, R> DeviceState.Companion.map(
|
public fun <T, R> DeviceState.Companion.map(
|
||||||
state: DeviceState<T>,
|
state: DeviceState<T>,
|
||||||
converter: MetaConverter<R>, mapper: (T) -> R,
|
mapper: (T) -> R,
|
||||||
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
||||||
override val dependencies = listOf(state)
|
override val dependencies = listOf(state)
|
||||||
|
|
||||||
override val converter: MetaConverter<R> = converter
|
|
||||||
|
|
||||||
override val value: R get() = mapper(state.value)
|
override val value: R get() = mapper(state.value)
|
||||||
|
|
||||||
override val valueFlow: Flow<R> = state.valueFlow.map(mapper)
|
override val valueFlow: Flow<R> = state.valueFlow.map(mapper)
|
||||||
|
|
||||||
override fun toString(): String = "DeviceState.map(arg=${state}, converter=$converter)"
|
override fun toString(): String = "DeviceState.map(arg=${state})"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,13 +70,10 @@ public fun <T, R> DeviceState.Companion.map(
|
|||||||
public fun <T1, T2, R> DeviceState.Companion.combine(
|
public fun <T1, T2, R> DeviceState.Companion.combine(
|
||||||
state1: DeviceState<T1>,
|
state1: DeviceState<T1>,
|
||||||
state2: DeviceState<T2>,
|
state2: DeviceState<T2>,
|
||||||
converter: MetaConverter<R>,
|
|
||||||
mapper: (T1, T2) -> R,
|
mapper: (T1, T2) -> R,
|
||||||
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
||||||
override val dependencies = listOf(state1, state2)
|
override val dependencies = listOf(state1, state2)
|
||||||
|
|
||||||
override val converter: MetaConverter<R> = converter
|
|
||||||
|
|
||||||
override val value: R get() = mapper(state1.value, state2.value)
|
override val value: R get() = mapper(state1.value, state2.value)
|
||||||
|
|
||||||
override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)
|
override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package space.kscience.controls.constructor
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.runningFold
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.manager.ClockManager
|
import space.kscience.controls.manager.ClockManager
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import space.kscience.dataforge.context.ContextAware
|
||||||
import space.kscience.dataforge.context.request
|
import space.kscience.dataforge.context.request
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,7 +19,7 @@ public sealed interface StateDescriptor
|
|||||||
/**
|
/**
|
||||||
* A binding that exposes device property as read-only state
|
* A binding that exposes device property as read-only state
|
||||||
*/
|
*/
|
||||||
public class PropertyStateDescriptor<T>(
|
public class StatePropertyDescriptor<T>(
|
||||||
public val device: Device,
|
public val device: Device,
|
||||||
public val propertyName: String,
|
public val propertyName: String,
|
||||||
public val state: DeviceState<T>,
|
public val state: DeviceState<T>,
|
||||||
@ -24,51 +28,172 @@ public class PropertyStateDescriptor<T>(
|
|||||||
/**
|
/**
|
||||||
* A binding for independent state like a timer
|
* A binding for independent state like a timer
|
||||||
*/
|
*/
|
||||||
public class StateBinding<T>(
|
public class StateNodeDescriptor<T>(
|
||||||
public val state: DeviceState<T>,
|
public val state: DeviceState<T>,
|
||||||
) : StateDescriptor
|
) : StateDescriptor
|
||||||
|
|
||||||
public class ConnectionStateDescriptor(
|
public class StateConnectionDescriptor(
|
||||||
public val reads: Collection<DeviceState<*>>,
|
public val reads: Collection<DeviceState<*>>,
|
||||||
public val writes: Collection<DeviceState<*>>,
|
public val writes: Collection<DeviceState<*>>,
|
||||||
) : StateDescriptor
|
) : StateDescriptor
|
||||||
|
|
||||||
|
|
||||||
public interface StateContainer : ContextAware {
|
public interface StateContainer : ContextAware, CoroutineScope {
|
||||||
public val stateDescriptors: List<StateDescriptor>
|
public val stateDescriptors: Set<StateDescriptor>
|
||||||
public fun registerState(stateDescriptor: StateDescriptor)
|
public fun registerState(stateDescriptor: StateDescriptor)
|
||||||
|
public fun unregisterState(stateDescriptor: StateDescriptor)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind an action to a [DeviceState]. [onChange] block is performed on each state change
|
||||||
|
*
|
||||||
|
* Optionally provide [writes] - a set of states that this change affects.
|
||||||
|
*/
|
||||||
|
public fun <T> DeviceState<T>.onNext(
|
||||||
|
vararg writes: DeviceState<*>,
|
||||||
|
alsoReads: Collection<DeviceState<*>> = emptySet(),
|
||||||
|
onChange: suspend (T) -> Unit,
|
||||||
|
): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also {
|
||||||
|
registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <T> DeviceState<T>.onChange(
|
||||||
|
vararg writes: DeviceState<*>,
|
||||||
|
alsoReads: Collection<DeviceState<*>> = emptySet(),
|
||||||
|
onChange: suspend (prev: T, next: T) -> Unit,
|
||||||
|
): Job = valueFlow.runningFold(Pair(value, value)) { pair, next ->
|
||||||
|
Pair(pair.second, next)
|
||||||
|
}.onEach { pair ->
|
||||||
|
if (pair.first != pair.second) {
|
||||||
|
onChange(pair.first, pair.second)
|
||||||
|
}
|
||||||
|
}.launchIn(this@StateContainer).also {
|
||||||
|
registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a [state] in this container. The state is not registered as a device property if [this] is a [DeviceConstructor]
|
* Register a [state] in this container. The state is not registered as a device property if [this] is a [DeviceConstructor]
|
||||||
*/
|
*/
|
||||||
public fun <T, D : DeviceState<T>> StateContainer.state(state: D): D {
|
public fun <T, D : DeviceState<T>> StateContainer.state(state: D): D {
|
||||||
registerState(StateBinding(state))
|
registerState(StateNodeDescriptor(state))
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a register a [MutableDeviceState] with a given [converter]
|
* Create a register a [MutableDeviceState] with a given [converter]
|
||||||
*/
|
*/
|
||||||
public fun <T> StateContainer.state(converter: MetaConverter<T>, initialValue: T): MutableDeviceState<T> = state(
|
public fun <T> StateContainer.mutableState(initialValue: T): MutableDeviceState<T> = state(
|
||||||
DeviceState.internal(converter, initialValue)
|
MutableDeviceState(initialValue)
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
public fun <T : DeviceModel> StateContainer.model(model: T): T {
|
||||||
* Create a register a mutable [Double] state
|
model.stateDescriptors.forEach {
|
||||||
*/
|
registerState(it)
|
||||||
public fun StateContainer.doubleState(initialValue: Double): MutableDeviceState<Double> = state(
|
}
|
||||||
MetaConverter.double, initialValue
|
return model
|
||||||
)
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a register a mutable [String] state
|
|
||||||
*/
|
|
||||||
public fun StateContainer.stringState(initialValue: String): MutableDeviceState<String> = state(
|
|
||||||
MetaConverter.string, initialValue
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and register a timer state.
|
* Create and register a timer state.
|
||||||
*/
|
*/
|
||||||
public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(context.request(ClockManager), tick))
|
public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(context.request(ClockManager), tick))
|
||||||
|
|
||||||
|
|
||||||
|
public fun <T, R> StateContainer.mapState(
|
||||||
|
state: DeviceState<T>,
|
||||||
|
transformation: (T) -> R,
|
||||||
|
): DeviceStateWithDependencies<R> = state(DeviceState.map(state, transformation))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new state by combining two existing ones
|
||||||
|
*/
|
||||||
|
public fun <T1, T2, R> StateContainer.combineState(
|
||||||
|
first: DeviceState<T1>,
|
||||||
|
second: DeviceState<T2>,
|
||||||
|
transformation: (T1, T2) -> R,
|
||||||
|
): DeviceState<R> = state(DeviceState.combine(first, second, transformation))
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically
|
||||||
|
* transferred onto [targetState], but not vise versa.
|
||||||
|
*
|
||||||
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
|
*/
|
||||||
|
public fun <T> StateContainer.bindTo(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job {
|
||||||
|
val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState))
|
||||||
|
registerState(descriptor)
|
||||||
|
return sourceState.valueFlow.onEach {
|
||||||
|
targetState.value = it
|
||||||
|
}.launchIn(this).apply {
|
||||||
|
invokeOnCompletion {
|
||||||
|
unregisterState(descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically
|
||||||
|
* transferred onto [targetState] via [transformation], but not vise versa.
|
||||||
|
*
|
||||||
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
|
*/
|
||||||
|
public fun <T, R> StateContainer.transformTo(
|
||||||
|
sourceState: DeviceState<T>,
|
||||||
|
targetState: MutableDeviceState<R>,
|
||||||
|
transformation: suspend (T) -> R,
|
||||||
|
): Job {
|
||||||
|
val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState))
|
||||||
|
registerState(descriptor)
|
||||||
|
return sourceState.valueFlow.onEach {
|
||||||
|
targetState.value = transformation(it)
|
||||||
|
}.launchIn(this).apply {
|
||||||
|
invokeOnCompletion {
|
||||||
|
unregisterState(descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register [StateDescriptor] that combines values from [sourceState1] and [sourceState2] using [transformation].
|
||||||
|
*
|
||||||
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
|
*/
|
||||||
|
public fun <T1, T2, R> StateContainer.combineTo(
|
||||||
|
sourceState1: DeviceState<T1>,
|
||||||
|
sourceState2: DeviceState<T2>,
|
||||||
|
targetState: MutableDeviceState<R>,
|
||||||
|
transformation: suspend (T1, T2) -> R,
|
||||||
|
): Job {
|
||||||
|
val descriptor = StateConnectionDescriptor(setOf(sourceState1, sourceState2), setOf(targetState))
|
||||||
|
registerState(descriptor)
|
||||||
|
return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
|
||||||
|
targetState.value = it
|
||||||
|
}.launchIn(this).apply {
|
||||||
|
invokeOnCompletion {
|
||||||
|
unregisterState(descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register [StateDescriptor] that combines values from [sourceStates] using [transformation].
|
||||||
|
*
|
||||||
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
|
*/
|
||||||
|
public inline fun <reified T, R> StateContainer.combineTo(
|
||||||
|
sourceStates: Collection<DeviceState<T>>,
|
||||||
|
targetState: MutableDeviceState<R>,
|
||||||
|
noinline transformation: suspend (Array<T>) -> R,
|
||||||
|
): Job {
|
||||||
|
val descriptor = StateConnectionDescriptor(sourceStates, setOf(targetState))
|
||||||
|
registerState(descriptor)
|
||||||
|
return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach {
|
||||||
|
targetState.value = it
|
||||||
|
}.launchIn(this).apply {
|
||||||
|
invokeOnCompletion {
|
||||||
|
unregisterState(descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,8 +7,6 @@ import kotlinx.coroutines.isActive
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import space.kscience.controls.manager.ClockManager
|
import space.kscience.controls.manager.ClockManager
|
||||||
import space.kscience.controls.spec.instant
|
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,7 +21,6 @@ public class TimerState(
|
|||||||
public val clockManager: ClockManager,
|
public val clockManager: ClockManager,
|
||||||
public val tick: Duration,
|
public val tick: Duration,
|
||||||
) : DeviceState<Instant> {
|
) : DeviceState<Instant> {
|
||||||
override val converter: MetaConverter<Instant> get() = MetaConverter.instant
|
|
||||||
|
|
||||||
private val clock = MutableStateFlow(clockManager.clock.now())
|
private val clock = MutableStateFlow(clockManager.clock.now())
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import space.kscience.dataforge.meta.MetaConverter
|
|||||||
* A copy-free [DeviceState] bound to a device property
|
* A copy-free [DeviceState] bound to a device property
|
||||||
*/
|
*/
|
||||||
private open class BoundDeviceState<T>(
|
private open class BoundDeviceState<T>(
|
||||||
override val converter: MetaConverter<T>,
|
val converter: MetaConverter<T>,
|
||||||
val device: Device,
|
val device: Device,
|
||||||
val propertyName: String,
|
val propertyName: String,
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
|
@ -2,7 +2,6 @@ package space.kscience.controls.constructor
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,7 +16,6 @@ public class DoubleInRangeState(
|
|||||||
require(initialValue in range) { "Initial value should be in range" }
|
require(initialValue in range) { "Initial value should be in range" }
|
||||||
}
|
}
|
||||||
|
|
||||||
override val converter: MetaConverter<Double> = MetaConverter.double
|
|
||||||
|
|
||||||
private val _valueFlow = MutableStateFlow(initialValue)
|
private val _valueFlow = MutableStateFlow(initialValue)
|
||||||
|
|
||||||
@ -32,18 +30,18 @@ public class DoubleInRangeState(
|
|||||||
/**
|
/**
|
||||||
* A state showing that the range is on its lower boundary
|
* A state showing that the range is on its lower boundary
|
||||||
*/
|
*/
|
||||||
public val atStart: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) {
|
public val atStart: DeviceState<Boolean> = DeviceState.map(this) {
|
||||||
it <= range.start
|
it <= range.start
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A state showing that the range is on its higher boundary
|
* A state showing that the range is on its higher boundary
|
||||||
*/
|
*/
|
||||||
public val atEnd: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) {
|
public val atEnd: DeviceState<Boolean> = DeviceState.map(this) {
|
||||||
it >= range.endInclusive
|
it >= range.endInclusive
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)"
|
override fun toString(): String = "DoubleRangeState(range=$range)"
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -55,5 +53,5 @@ public fun StateContainer.doubleInRangeState(
|
|||||||
initialValue: Double,
|
initialValue: Double,
|
||||||
range: ClosedFloatingPointRange<Double>,
|
range: ClosedFloatingPointRange<Double>,
|
||||||
): DoubleInRangeState = DoubleInRangeState(initialValue, range).also {
|
): DoubleInRangeState = DoubleInRangeState(initialValue, range).also {
|
||||||
registerState(StateBinding(it))
|
registerState(StateNodeDescriptor(it))
|
||||||
}
|
}
|
@ -4,13 +4,11 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
|
||||||
private open class ExternalState<T>(
|
private open class ExternalState<T>(
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
override val converter: MetaConverter<T>,
|
|
||||||
val readInterval: Duration,
|
val readInterval: Duration,
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
val reader: suspend () -> T,
|
val reader: suspend () -> T,
|
||||||
@ -26,7 +24,7 @@ private open class ExternalState<T>(
|
|||||||
override val value: T get() = flow.value
|
override val value: T get() = flow.value
|
||||||
override val valueFlow: Flow<T> get() = flow
|
override val valueFlow: Flow<T> get() = flow
|
||||||
|
|
||||||
override fun toString(): String = "ExternalState(converter=$converter)"
|
override fun toString(): String = "ExternalState()"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,20 +32,18 @@ private open class ExternalState<T>(
|
|||||||
*/
|
*/
|
||||||
public fun <T> DeviceState.Companion.external(
|
public fun <T> DeviceState.Companion.external(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
converter: MetaConverter<T>,
|
|
||||||
readInterval: Duration,
|
readInterval: Duration,
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
reader: suspend () -> T,
|
reader: suspend () -> T,
|
||||||
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader)
|
): DeviceState<T> = ExternalState(scope, readInterval, initialValue, reader)
|
||||||
|
|
||||||
private class MutableExternalState<T>(
|
private class MutableExternalState<T>(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
converter: MetaConverter<T>,
|
|
||||||
readInterval: Duration,
|
readInterval: Duration,
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
reader: suspend () -> T,
|
reader: suspend () -> T,
|
||||||
val writer: suspend (T) -> Unit,
|
val writer: suspend (T) -> Unit,
|
||||||
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
|
) : ExternalState<T>(scope, readInterval, initialValue, reader), MutableDeviceState<T> {
|
||||||
override var value: T
|
override var value: T
|
||||||
get() = super.value
|
get() = super.value
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -62,9 +58,8 @@ private class MutableExternalState<T>(
|
|||||||
*/
|
*/
|
||||||
public fun <T> DeviceState.Companion.external(
|
public fun <T> DeviceState.Companion.external(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
converter: MetaConverter<T>,
|
|
||||||
readInterval: Duration,
|
readInterval: Duration,
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
reader: suspend () -> T,
|
reader: suspend () -> T,
|
||||||
writer: suspend (T) -> Unit,
|
writer: suspend (T) -> Unit,
|
||||||
): MutableDeviceState<T> = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer)
|
): MutableDeviceState<T> = MutableExternalState(scope, readInterval, initialValue, reader, writer)
|
@ -2,22 +2,19 @@ package space.kscience.controls.constructor
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
|
|
||||||
|
|
||||||
private class StateFlowAsState<T>(
|
private class StateFlowAsState<T>(
|
||||||
override val converter: MetaConverter<T>,
|
|
||||||
val flow: MutableStateFlow<T>,
|
val flow: MutableStateFlow<T>,
|
||||||
) : MutableDeviceState<T> {
|
) : MutableDeviceState<T> {
|
||||||
override var value: T by flow::value
|
override var value: T by flow::value
|
||||||
override val valueFlow: Flow<T> get() = flow
|
override val valueFlow: Flow<T> get() = flow
|
||||||
|
|
||||||
override fun toString(): String = "FlowAsState(converter=$converter)"
|
override fun toString(): String = "FlowAsState()"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a read-only [DeviceState] that wraps [MutableStateFlow].
|
* Create a read-only [DeviceState] that wraps [MutableStateFlow].
|
||||||
* No data copy is performed.
|
* No data copy is performed.
|
||||||
*/
|
*/
|
||||||
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> =
|
public fun <T> MutableStateFlow<T>.asDeviceState(): MutableDeviceState<T> = StateFlowAsState(this)
|
||||||
StateFlowAsState(converter, this)
|
|
@ -2,7 +2,6 @@ package space.kscience.controls.constructor
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [MutableDeviceState] that does not correspond to a physical state
|
* A [MutableDeviceState] that does not correspond to a physical state
|
||||||
@ -10,7 +9,6 @@ import space.kscience.dataforge.meta.MetaConverter
|
|||||||
* @param callback a synchronous callback that could be used without a scope
|
* @param callback a synchronous callback that could be used without a scope
|
||||||
*/
|
*/
|
||||||
private class VirtualDeviceState<T>(
|
private class VirtualDeviceState<T>(
|
||||||
override val converter: MetaConverter<T>,
|
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
private val callback: (T) -> Unit = {},
|
private val callback: (T) -> Unit = {},
|
||||||
) : MutableDeviceState<T> {
|
) : MutableDeviceState<T> {
|
||||||
@ -24,7 +22,7 @@ private class VirtualDeviceState<T>(
|
|||||||
callback(value)
|
callback(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "VirtualDeviceState(converter=$converter)"
|
override fun toString(): String = "VirtualDeviceState()"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -33,8 +31,7 @@ private class VirtualDeviceState<T>(
|
|||||||
*
|
*
|
||||||
* @param callback a synchronous callback that could be used without a scope
|
* @param callback a synchronous callback that could be used without a scope
|
||||||
*/
|
*/
|
||||||
public fun <T> DeviceState.Companion.internal(
|
public fun <T> MutableDeviceState(
|
||||||
converter: MetaConverter<T>,
|
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
callback: (T) -> Unit = {},
|
callback: (T) -> Unit = {},
|
||||||
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)
|
): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback)
|
||||||
|
@ -22,6 +22,7 @@ public interface DeviceHub : Provider {
|
|||||||
} else {
|
} else {
|
||||||
emptyMap()
|
emptyMap()
|
||||||
}
|
}
|
||||||
|
//TODO send message on device change
|
||||||
|
|
||||||
public companion object
|
public companion object
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package space.kscience.controls.spec
|
package space.kscience.controls.misc
|
||||||
|
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import space.kscience.dataforge.meta.*
|
import space.kscience.dataforge.meta.*
|
||||||
@ -12,9 +12,9 @@ public fun Double.asMeta(): Meta = Meta(asValue())
|
|||||||
* Generate a nullable [MetaConverter] from non-nullable one
|
* Generate a nullable [MetaConverter] from non-nullable one
|
||||||
*/
|
*/
|
||||||
public fun <T : Any> MetaConverter<T>.nullable(): MetaConverter<T?> = object : MetaConverter<T?> {
|
public fun <T : Any> MetaConverter<T>.nullable(): MetaConverter<T?> = object : MetaConverter<T?> {
|
||||||
override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) }?: Meta(Null)
|
override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) } ?: Meta(Null)
|
||||||
|
|
||||||
override fun readOrNull(source: Meta): T? = if(source.value == Null) null else this@nullable.readOrNull(source)
|
override fun readOrNull(source: Meta): T? = if (source.value == Null) null else this@nullable.readOrNull(source)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,3 +39,15 @@ private object InstantConverter : MetaConverter<Instant> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter
|
public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter
|
||||||
|
|
||||||
|
private object DoubleRangeConverter : MetaConverter<ClosedFloatingPointRange<Double>> {
|
||||||
|
override fun readOrNull(source: Meta): ClosedFloatingPointRange<Double>? = source.value?.doubleArray?.let { (start, end)->
|
||||||
|
start..end
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun convert(
|
||||||
|
obj: ClosedFloatingPointRange<Double>,
|
||||||
|
): Meta = Meta(doubleArrayOf(obj.start, obj.endInclusive).asValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
public val MetaConverter.Companion.doubleRange: MetaConverter<ClosedFloatingPointRange<Double>> get() = DoubleRangeConverter
|
@ -1,9 +1,8 @@
|
|||||||
package space.kscience.controls.api
|
package space.kscience.controls.api
|
||||||
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import space.kscience.controls.spec.asMeta
|
import space.kscience.controls.misc.asMeta
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
@ -16,8 +16,6 @@ import space.kscience.dataforge.context.Context
|
|||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
import space.kscience.dataforge.misc.DFExperimental
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.length
|
|
||||||
import space.kscience.dataforge.names.startsWith
|
|
||||||
import space.kscience.magix.api.MagixEndpoint
|
import space.kscience.magix.api.MagixEndpoint
|
||||||
import space.kscience.magix.api.send
|
import space.kscience.magix.api.send
|
||||||
import space.kscience.magix.api.subscribe
|
import space.kscience.magix.api.subscribe
|
||||||
@ -121,12 +119,18 @@ public suspend fun MagixEndpoint.remoteDevice(
|
|||||||
deviceEndpoint: String,
|
deviceEndpoint: String,
|
||||||
deviceName: Name,
|
deviceName: Name,
|
||||||
): DeviceClient = coroutineScope {
|
): DeviceClient = coroutineScope {
|
||||||
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)).map { it.second }
|
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint))
|
||||||
|
.map { it.second }
|
||||||
|
.filter {
|
||||||
|
it.sourceDevice == null || it.sourceDevice == deviceName
|
||||||
|
}
|
||||||
|
|
||||||
val deferredDescriptorMessage = CompletableDeferred<DescriptionMessage>()
|
val deferredDescriptorMessage = CompletableDeferred<DescriptionMessage>()
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
deferredDescriptorMessage.complete(subscription.filterIsInstance<DescriptionMessage>().first())
|
deferredDescriptorMessage.complete(
|
||||||
|
subscription.filterIsInstance<DescriptionMessage>().first()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
send(
|
send(
|
||||||
@ -157,12 +161,6 @@ public suspend fun MagixEndpoint.remoteDevice(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MapBasedDeviceHub(val deviceMap: Map<Name, Device>, val prefix: Name) : DeviceHub {
|
|
||||||
override val devices: Map<Name, Device>
|
|
||||||
get() = deviceMap.filterKeys { name: Name -> name == prefix || (name.startsWith(prefix) && name.length == prefix.length + 1) }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a dynamic [DeviceHub] from incoming messages
|
* Create a dynamic [DeviceHub] from incoming messages
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package space.kscience.controls.client
|
package space.kscience.controls.client
|
||||||
|
|
||||||
import com.benasher44.uuid.uuid4
|
import com.benasher44.uuid.uuid4
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@ -40,7 +41,7 @@ internal fun generateId(request: MagixMessage): String = if (request.id != null)
|
|||||||
*/
|
*/
|
||||||
public fun DeviceManager.launchMagixService(
|
public fun DeviceManager.launchMagixService(
|
||||||
endpoint: MagixEndpoint,
|
endpoint: MagixEndpoint,
|
||||||
endpointID: String = controlsMagixFormat.defaultFormat,
|
endpointID: String,
|
||||||
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||||
): Job = context.launch(coroutineContext) {
|
): Job = context.launch(coroutineContext) {
|
||||||
endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID, null)).onEach { (request, payload) ->
|
endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID, null)).onEach { (request, payload) ->
|
||||||
@ -56,7 +57,7 @@ public fun DeviceManager.launchMagixService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.catch { error ->
|
}.catch { error ->
|
||||||
logger.error(error) { "Error while responding to message: ${error.message}" }
|
if (error !is CancellationException) logger.error(error) { "Error while responding to message: ${error.message}" }
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
|
|
||||||
hubMessageFlow().onEach { payload ->
|
hubMessageFlow().onEach { payload ->
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
package space.kscience.controls.vision
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import space.kscience.dataforge.meta.boolean
|
|
||||||
import space.kscience.visionforge.AbstractVision
|
|
||||||
import space.kscience.visionforge.Vision
|
|
||||||
import space.kscience.visionforge.html.VisionOfHtml
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [Vision] that shows an indicator
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
@SerialName("controls.indicator")
|
|
||||||
public class BooleanIndicatorVision : AbstractVision(), VisionOfHtml {
|
|
||||||
public val isOn: Boolean by properties.boolean(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
///**
|
|
||||||
// * A [Vision] that allows both showing the value and changing it
|
|
||||||
// */
|
|
||||||
//public interface RegulatorVision: IndicatorVision{
|
|
||||||
//
|
|
||||||
//}
|
|
@ -13,6 +13,7 @@ public expect class ControlVisionPlugin: VisionPlugin{
|
|||||||
|
|
||||||
internal val controlsVisionSerializersModule = SerializersModule {
|
internal val controlsVisionSerializersModule = SerializersModule {
|
||||||
polymorphic(Vision::class) {
|
polymorphic(Vision::class) {
|
||||||
subclass(BooleanIndicatorVision.serializer())
|
subclass(IndicatorVision.serializer())
|
||||||
|
subclass(SliderVision.serializer())
|
||||||
}
|
}
|
||||||
}
|
}
|
35
controls-vision/src/commonMain/kotlin/controlsVisions.kt
Normal file
35
controls-vision/src/commonMain/kotlin/controlsVisions.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package space.kscience.controls.vision
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import space.kscience.controls.misc.doubleRange
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
import space.kscience.dataforge.meta.convertable
|
||||||
|
import space.kscience.dataforge.meta.double
|
||||||
|
import space.kscience.dataforge.meta.string
|
||||||
|
import space.kscience.visionforge.AbstractControlVision
|
||||||
|
import space.kscience.visionforge.AbstractVision
|
||||||
|
import space.kscience.visionforge.Vision
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Vision] that shows a colored indicator
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
@SerialName("controls.indicator")
|
||||||
|
public class IndicatorVision : AbstractVision() {
|
||||||
|
public val color: String? by properties.string()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("controls.slider")
|
||||||
|
public class SliderVision : AbstractControlVision() {
|
||||||
|
public var position: Double? by properties.double()
|
||||||
|
public var range: ClosedFloatingPointRange<Double>? by properties.convertable(MetaConverter.doubleRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
///**
|
||||||
|
// * A [Vision] that allows both showing the value and changing it
|
||||||
|
// */
|
||||||
|
//public interface RegulatorVision: IndicatorVision{
|
||||||
|
//
|
||||||
|
//}
|
@ -145,7 +145,7 @@ private fun <T> Trace.updateFromState(
|
|||||||
public fun <T> Plot.plotDeviceState(
|
public fun <T> Plot.plotDeviceState(
|
||||||
context: Context,
|
context: Context,
|
||||||
state: DeviceState<T>,
|
state: DeviceState<T>,
|
||||||
extractValue: T.() -> Value = { state.converter.convert(this).value ?: Null },
|
extractValue: (T) -> Value = { Value.of(it) },
|
||||||
maxAge: Duration = defaultMaxAge,
|
maxAge: Duration = defaultMaxAge,
|
||||||
maxPoints: Int = defaultMaxPoints,
|
maxPoints: Int = defaultMaxPoints,
|
||||||
minPoints: Int = defaultMinPoints,
|
minPoints: Int = defaultMinPoints,
|
||||||
|
@ -10,7 +10,34 @@ import space.kscience.dataforge.names.asName
|
|||||||
import space.kscience.visionforge.VisionPlugin
|
import space.kscience.visionforge.VisionPlugin
|
||||||
import space.kscience.visionforge.html.ElementVisionRenderer
|
import space.kscience.visionforge.html.ElementVisionRenderer
|
||||||
|
|
||||||
private val indicatorRenderer = ElementVisionRenderer<BooleanIndicatorVision> { name, vision: BooleanIndicatorVision, meta ->
|
|
||||||
|
private val indicatorRenderer = ElementVisionRenderer<IndicatorVision> { name, vision: IndicatorVision, meta ->
|
||||||
|
// val ledSize = vision.properties["size"].int ?: 15
|
||||||
|
// val color = vision.color ?: "LightGray"
|
||||||
|
// div("controls-indicator") {
|
||||||
|
// style = """
|
||||||
|
//
|
||||||
|
// @keyframes blink {
|
||||||
|
// 0% { box-shadow: 0 0 10px; }
|
||||||
|
// 50% { box-shadow: 0 0 30px; }
|
||||||
|
// 100% { box-shadow: 0 0 10px; }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// display: inline-block;
|
||||||
|
// margin: ${ledSize}px;
|
||||||
|
// width: ${ledSize}px;
|
||||||
|
// height: ${ledSize}px;
|
||||||
|
// border-radius: 50%;
|
||||||
|
//
|
||||||
|
// background: $color;
|
||||||
|
// border: 1px solid darken($color,5%);
|
||||||
|
// color: $color;
|
||||||
|
// animation: blink 3s infinite;
|
||||||
|
// """.trimIndent()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sliderRenderer = ElementVisionRenderer<SliderVision> { name, vision: SliderVision, meta ->
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,7 +49,8 @@ public actual class ControlVisionPlugin : VisionPlugin() {
|
|||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = when (target) {
|
override fun content(target: String): Map<Name, Any> = when (target) {
|
||||||
ElementVisionRenderer.TYPE -> mapOf(
|
ElementVisionRenderer.TYPE -> mapOf(
|
||||||
"indicator".asName() to indicatorRenderer
|
"indicator".asName() to indicatorRenderer,
|
||||||
|
"slider".asName() to sliderRenderer
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> super.content(target)
|
else -> super.content(target)
|
||||||
|
@ -64,7 +64,7 @@ class VirtualCarController : Controller(), ContextAware {
|
|||||||
//mongoStorageJob = deviceManager.storeMessages(DefaultAsynchronousMongoClientFactory)
|
//mongoStorageJob = deviceManager.storeMessages(DefaultAsynchronousMongoClientFactory)
|
||||||
//Launch device client and connect it to the server
|
//Launch device client and connect it to the server
|
||||||
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
|
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
|
||||||
deviceManager.launchMagixService(deviceEndpoint)
|
deviceManager.launchMagixService(deviceEndpoint, "car")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import org.jetbrains.compose.ExperimentalComposeLibrary
|
||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
|
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
|
||||||
|
|
||||||
@ -11,12 +12,15 @@ kscience {
|
|||||||
withJava()
|
withJava()
|
||||||
}
|
}
|
||||||
useKtor()
|
useKtor()
|
||||||
|
useSerialization()
|
||||||
useContextReceivers()
|
useContextReceivers()
|
||||||
dependencies {
|
commonMain {
|
||||||
api(projects.controlsVision)
|
implementation(projects.controlsVision)
|
||||||
|
implementation(projects.controlsConstructor)
|
||||||
|
// implementation("io.github.koalaplot:koalaplot-core:0.6.0")
|
||||||
}
|
}
|
||||||
jvmMain {
|
jvmMain {
|
||||||
implementation("io.ktor:ktor-server-cio")
|
// implementation("io.ktor:ktor-server-cio")
|
||||||
implementation(spclibs.logback.classic)
|
implementation(spclibs.logback.classic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -26,6 +30,8 @@ kotlin {
|
|||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
|
@OptIn(ExperimentalComposeLibrary::class)
|
||||||
|
implementation(compose.desktop.components.splitPane)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
185
demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt
Normal file
185
demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package space.kscience.controls.demo.constructor
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import space.kscience.controls.constructor.*
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class XY(val x: Double, val y: Double) {
|
||||||
|
companion object {
|
||||||
|
val ZERO = XY(0.0, 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun XY.plus(other: XY): XY = XY(x + other.x, y + other.y)
|
||||||
|
operator fun XY.times(c: Double): XY = XY(x * c, y * c)
|
||||||
|
operator fun XY.div(c: Double): XY = XY(x / c, y / c)
|
||||||
|
//
|
||||||
|
//class XYPosition(context: Context, x0: Double, y0: Double) : DeviceModel(context) {
|
||||||
|
// val x: MutableDeviceState<Double> = mutableState(x0)
|
||||||
|
// val y: MutableDeviceState<Double> = mutableState(y0)
|
||||||
|
//
|
||||||
|
// val xy = combineState(x, y) { x, y -> XY(x, y) }
|
||||||
|
//}
|
||||||
|
|
||||||
|
class Spring(
|
||||||
|
context: Context,
|
||||||
|
val k: Double,
|
||||||
|
val l0: Double,
|
||||||
|
val begin: DeviceState<XY>,
|
||||||
|
val end: DeviceState<XY>,
|
||||||
|
) : DeviceConstructor(context) {
|
||||||
|
|
||||||
|
val length = combineState(begin, end) { begin, end ->
|
||||||
|
sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).pow(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
val tension: DeviceState<Double> = mapState(length) { l ->
|
||||||
|
val delta = l - l0
|
||||||
|
k * delta
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* direction from start to end
|
||||||
|
*/
|
||||||
|
val direction = combineState(begin, end) { begin, end ->
|
||||||
|
val dx = end.x - begin.x
|
||||||
|
val dy = end.y - begin.y
|
||||||
|
val l = sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).pow(2))
|
||||||
|
XY(dx / l, dy / l)
|
||||||
|
}
|
||||||
|
|
||||||
|
val beginForce = combineState(direction, tension) { direction: XY, tension: Double ->
|
||||||
|
direction * (tension)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val endForce = combineState(direction, tension) { direction: XY, tension: Double ->
|
||||||
|
direction * (-tension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MaterialPoint(
|
||||||
|
context: Context,
|
||||||
|
val mass: Double,
|
||||||
|
val force: DeviceState<XY>,
|
||||||
|
val position: MutableDeviceState<XY>,
|
||||||
|
val velocity: MutableDeviceState<XY> = MutableDeviceState(XY.ZERO),
|
||||||
|
) : DeviceModel(context, force) {
|
||||||
|
|
||||||
|
private val timer: TimerState = timer(2.milliseconds)
|
||||||
|
|
||||||
|
private val movement = timer.onChange(
|
||||||
|
position, velocity,
|
||||||
|
alsoReads = setOf(force, velocity, position)
|
||||||
|
) { prev, next ->
|
||||||
|
val dt = (next - prev).toDouble(DurationUnit.SECONDS)
|
||||||
|
val a = force.value / mass
|
||||||
|
position.value += a * (dt * dt / 2) + velocity.value * dt
|
||||||
|
velocity.value += a * dt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BodyOnSprings(
|
||||||
|
context: Context,
|
||||||
|
mass: Double,
|
||||||
|
k: Double,
|
||||||
|
startPosition: XY,
|
||||||
|
l0: Double = 1.0,
|
||||||
|
val xLeft: Double = 0.0,
|
||||||
|
val xRight: Double = 2.0,
|
||||||
|
val yBottom: Double = 0.0,
|
||||||
|
val yTop: Double = 2.0,
|
||||||
|
) : DeviceConstructor(context) {
|
||||||
|
|
||||||
|
val width = xRight - xLeft
|
||||||
|
val height = yTop - yBottom
|
||||||
|
|
||||||
|
val position = mutableState(startPosition)
|
||||||
|
|
||||||
|
private val leftAnchor = mutableState(XY(xLeft, yTop + yBottom / 2))
|
||||||
|
|
||||||
|
val leftSpring by device(
|
||||||
|
Spring(context, k, l0, leftAnchor, position)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val rightAnchor = mutableState(XY(xRight, yTop + yBottom / 2))
|
||||||
|
|
||||||
|
val rightSpring by device(
|
||||||
|
Spring(context, k, l0, rightAnchor, position)
|
||||||
|
)
|
||||||
|
|
||||||
|
val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, rignt ->
|
||||||
|
left + rignt
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val body = model(
|
||||||
|
MaterialPoint(
|
||||||
|
context = context,
|
||||||
|
mass = mass,
|
||||||
|
force = force,
|
||||||
|
position = position
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> DeviceState<T>.collect(
|
||||||
|
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
): State<T> = valueFlow.collectAsState(value, coroutineContext)
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
val initialState = XY(1.1, 1.1)
|
||||||
|
|
||||||
|
Window(title = "Ball on springs", onCloseRequest = ::exitApplication) {
|
||||||
|
MaterialTheme {
|
||||||
|
val context = remember {
|
||||||
|
Context("simulation")
|
||||||
|
}
|
||||||
|
|
||||||
|
val model = remember {
|
||||||
|
BodyOnSprings(context, 100.0, 1000.0, initialState)
|
||||||
|
}
|
||||||
|
|
||||||
|
val position: XY by model.body.position.collect()
|
||||||
|
Box(Modifier.size(400.dp)) {
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
fun XY.toOffset() = Offset(
|
||||||
|
(x / model.width * size.width).toFloat(),
|
||||||
|
(y / model.height * size.height).toFloat()
|
||||||
|
)
|
||||||
|
|
||||||
|
drawCircle(
|
||||||
|
Color.Red, 10f, center = position.toOffset()
|
||||||
|
)
|
||||||
|
drawLine(Color.Blue, model.leftSpring.begin.value.toOffset(), model.leftSpring.end.value.toOffset())
|
||||||
|
drawLine(
|
||||||
|
Color.Blue,
|
||||||
|
model.rightSpring.begin.value.toOffset(),
|
||||||
|
model.rightSpring.end.value.toOffset()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,8 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import space.kscience.controls.api.DeviceHub
|
import space.kscience.controls.api.DeviceHub
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
|
import space.kscience.controls.misc.asMeta
|
||||||
|
import space.kscience.controls.misc.duration
|
||||||
import space.kscience.controls.ports.AsynchronousPort
|
import space.kscience.controls.ports.AsynchronousPort
|
||||||
import space.kscience.controls.ports.KtorTcpPort
|
import space.kscience.controls.ports.KtorTcpPort
|
||||||
import space.kscience.controls.ports.send
|
import space.kscience.controls.ports.send
|
||||||
|
@ -79,6 +79,7 @@ visionforge-jupiter = { module = "space.kscience:visionforge-jupyter", version.r
|
|||||||
visionforge-plotly = { module = "space.kscience:visionforge-plotly", version.ref = "visionforge" }
|
visionforge-plotly = { module = "space.kscience:visionforge-plotly", version.ref = "visionforge" }
|
||||||
visionforge-markdown = { module = "space.kscience:visionforge-markdown", version.ref = "visionforge" }
|
visionforge-markdown = { module = "space.kscience:visionforge-markdown", version.ref = "visionforge" }
|
||||||
visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" }
|
visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" }
|
||||||
|
visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" }
|
||||||
|
|
||||||
# Buildscript
|
# Buildscript
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user