From 1414cf5a2f074a941999a2f5989bc0551a413f32 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 30 Oct 2023 21:35:46 +0300 Subject: [PATCH] implement constructor --- CHANGELOG.md | 1 + .../controls/constructor/DeviceGroup.kt | 258 ++++++++++++++++++ .../controls/constructor/DeviceState.kt | 108 +++++++- .../controls/constructor/DeviceTree.kt | 33 --- .../kscience/controls/constructor/Drive.kt | 27 +- .../controls/constructor/LimitSwitch.kt | 10 +- .../controls/constructor/PidRegulator.kt | 31 ++- .../controls/constructor/Regulator.kt | 3 +- .../controls/constructor/customState.kt | 41 +++ .../space/kscience/controls/api/Device.kt | 52 ++-- .../kscience/controls/api/DeviceMessage.kt | 2 +- .../kscience/controls/manager/ClockManager.kt | 25 ++ .../controls/manager/respondMessage.kt | 10 +- .../kscience/controls/spec/DeviceBase.kt | 10 +- .../controls/spec/DevicePropertySpec.kt | 18 +- .../kscience/controls/spec/DeviceSpec.kt | 14 +- .../controls/spec/propertySpecDelegates.kt | 10 +- .../kscience/controls/client/DeviceClient.kt | 2 +- .../kscience/controls/client/tangoMagix.kt | 6 +- .../controls/modbus/DeviceProcessImage.kt | 19 +- .../controls/opcua/server/DeviceNameSpace.kt | 15 +- controls-vision/build.gradle.kts | 13 +- .../controls/vision/plotExtensions.kt | 56 ++++ .../kscience/controls/demo/car/VirtualCar.kt | 8 +- demo/constructor/build.gradle.kts | 20 ++ .../src/jsMain/kotlin/constructorJs.kt | 6 + demo/constructor/src/jvmMain/kotlin/Main.kt | 103 +++++++ .../src/jvmMain/resources/logback.xml | 11 + .../sciprog/devices/mks/MksPdr900Device.kt | 2 +- .../pimotionmaster/fxDeviceProperties.kt | 4 +- settings.gradle.kts | 3 +- 31 files changed, 770 insertions(+), 151 deletions(-) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt create mode 100644 controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt create mode 100644 demo/constructor/build.gradle.kts create mode 100644 demo/constructor/src/jsMain/kotlin/constructorJs.kt create mode 100644 demo/constructor/src/jvmMain/kotlin/Main.kt create mode 100644 demo/constructor/src/jvmMain/resources/logback.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f3b5a9..4083a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Low-code constructor ### Changed +- Property caching moved from core `Device` to the `CachingDevice` ### Deprecated diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt new file mode 100644 index 0000000..f7bf494 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -0,0 +1,258 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import space.kscience.controls.api.* +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.meta.Laminate +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MutableMeta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.misc.DFExperimental +import space.kscience.dataforge.names.* +import kotlin.collections.set +import kotlin.coroutines.CoroutineContext + + +/** + * A mutable group of devices and properties to be used for lightweight design and simulations. + */ +public class DeviceGroup( + public val deviceManager: DeviceManager, + override val meta: Meta, +) : DeviceHub, CachingDevice { + + internal class Property( + val state: DeviceState, + val descriptor: PropertyDescriptor, + ) + + internal class Action( + val invoke: suspend (Meta?) -> Meta?, + val descriptor: ActionDescriptor, + ) + + + override val context: Context get() = deviceManager.context + + override val coroutineContext: CoroutineContext by lazy { + context.newCoroutineContext( + SupervisorJob(context.coroutineContext[Job]) + + CoroutineName("Device $this") + + CoroutineExceptionHandler { _, throwable -> + launch { + sharedMessageFlow.emit( + DeviceErrorMessage( + errorMessage = throwable.message, + errorType = throwable::class.simpleName, + errorStackTrace = throwable.stackTraceToString() + ) + ) + } + } + ) + } + + private val _devices = hashMapOf() + + override val devices: Map = _devices + + public fun device(token: NameToken, device: D): D { + check(_devices[token] == null) { "A child device with name $token already exists" } + _devices[token] = device + return device + } + + private val properties: MutableMap = hashMapOf() + + public fun property(descriptor: PropertyDescriptor, state: DeviceState) { + val name = descriptor.name.parseAsName() + require(properties[name] == null) { "Can't add property with name $name. It already exists." } + properties[name] = Property(state, descriptor) + } + + private val actions: MutableMap = hashMapOf() + + override val propertyDescriptors: Collection + get() = properties.values.map { it.descriptor } + + override val actionDescriptors: Collection + get() = actions.values.map { it.descriptor } + + override suspend fun readProperty(propertyName: String): Meta = + properties[propertyName.parseAsName()]?.state?.valueAsMeta + ?: error("Property with name $propertyName not found") + + override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.state?.valueAsMeta + + override suspend fun invalidate(propertyName: String) { + //does nothing for this implementation + } + + override suspend fun writeProperty(propertyName: String, value: Meta) { + val property = (properties[propertyName.parseAsName()]?.state as? MutableDeviceState) + ?: error("Property with name $propertyName not found") + property.valueAsMeta = value + } + + private val sharedMessageFlow = MutableSharedFlow() + + override val messageFlow: Flow + get() = sharedMessageFlow + + override suspend fun execute(actionName: String, argument: Meta?): Meta? { + val action = actions[actionName] ?: error("Action with name $actionName not found") + return action.invoke(argument) + } + + @DFExperimental + override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED + private set(value) { + if (field != value) { + launch { + sharedMessageFlow.emit( + DeviceLifeCycleMessage(value) + ) + } + } + field = value + } + + + @OptIn(DFExperimental::class) + override suspend fun start() { + lifecycleState = DeviceLifecycleState.STARTING + super.start() + devices.values.forEach { + it.start() + } + lifecycleState = DeviceLifecycleState.STARTED + } + + @OptIn(DFExperimental::class) + override fun stop() { + devices.values.forEach { + it.stop() + } + super.stop() + lifecycleState = DeviceLifecycleState.STOPPED + } + + public companion object { + + } +} + +public fun DeviceManager.deviceGroup( + name: String = "@group", + meta: Meta = Meta.EMPTY, + block: DeviceGroup.() -> Unit, +): DeviceGroup { + val group = DeviceGroup(this, meta).apply(block) + install(name, group) + return group +} + +private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { + return when (name.length) { + 0 -> this + 1 -> { + val token = name.first() + when (val d = devices[token]) { + null -> device( + token, + DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY) + ) + + else -> (d as? DeviceGroup) ?: error("Device $name is not a DeviceGroup") + } + } + + else -> getOrCreateGroup(name.first().asName()).getOrCreateGroup(name.cutFirst()) + } +} + +/** + * Register a device at given [name] path + */ +public fun DeviceGroup.device(name: Name, device: D): D { + return when (name.length) { + 0 -> error("Can't use empty name for a child device") + 1 -> device(name.first(), device) + else -> getOrCreateGroup(name.cutLast()).device(name.tokens.last(), device) + } +} + +public fun DeviceGroup.device(name: String, device: D): D = device(name.parseAsName(), device) + +/** + * Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error. + */ +public fun DeviceGroup.device(name: Name, factory: Factory, deviceMeta: Meta? = null): Device { + val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[name])) + device(name, newDevice) + return newDevice +} + +public fun DeviceGroup.device( + name: String, + factory: Factory, + metaBuilder: (MutableMeta.() -> Unit)? = null, +): Device = device(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }) + +/** + * Create or edit a group with a given [name]. + */ +public fun DeviceGroup.deviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup = + getOrCreateGroup(name).apply(block) + +public fun DeviceGroup.deviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup = + deviceGroup(name.parseAsName(), block) + +public fun DeviceGroup.property( + name: String, + state: DeviceState, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): DeviceState { + property( + PropertyDescriptor(name).apply(descriptorBuilder), + state + ) + return state +} + +public fun DeviceGroup.mutableProperty( + name: String, + state: MutableDeviceState, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): MutableDeviceState { + property( + PropertyDescriptor(name).apply(descriptorBuilder), + state + ) + return state +} + +public fun DeviceGroup.virtualProperty( + name: String, + initialValue: T, + converter: MetaConverter, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): MutableDeviceState { + val state = VirtualDeviceState(converter, initialValue) + return mutableProperty(name, state, descriptorBuilder) +} + +/** + * Create a virtual [MutableDeviceState], but do not register it to a device + */ +@Suppress("UnusedReceiverParameter") +public fun DeviceGroup.standAloneProperty( + initialValue: T, + converter: MetaConverter, +): MutableDeviceState = VirtualDeviceState(converter, initialValue) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index 2e6461f..783752a 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -1,6 +1,12 @@ package space.kscience.controls.constructor -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import space.kscience.controls.api.Device +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.MutableDevicePropertySpec +import space.kscience.controls.spec.name import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.transformations.MetaConverter @@ -12,14 +18,106 @@ public interface DeviceState { public val value: T public val valueFlow: Flow - - public val metaFlow: Flow } +public val DeviceState.metaFlow: Flow get() = valueFlow.map(converter::objectToMeta) + +public val DeviceState.valueAsMeta: Meta get() = converter.objectToMeta(value) + /** * A mutable state of a device */ -public interface MutableDeviceState : DeviceState{ +public interface MutableDeviceState : DeviceState { override var value: T -} \ No newline at end of file +} + +public var MutableDeviceState.valueAsMeta: Meta + get() = converter.objectToMeta(value) + set(arg) { + value = converter.metaToObject(arg) ?: error("Conversion for meta $arg to property type with $converter failed") + } + +/** + * A [MutableDeviceState] that does not correspond to a physical state + */ +public class VirtualDeviceState( + override val converter: MetaConverter, + initialValue: T, +) : MutableDeviceState { + private val flow = MutableStateFlow(initialValue) + override val valueFlow: Flow get() = flow + + override var value: T by flow::value +} + +private open class BoundDeviceState( + override val converter: MetaConverter, + val device: Device, + val propertyName: String, + private val initialValue: T, +) : DeviceState { + + override val valueFlow: StateFlow = device.messageFlow.filterIsInstance().filter { + it.property == propertyName + }.mapNotNull { + converter.metaToObject(it.value) + }.stateIn(device.context, SharingStarted.Eagerly, initialValue) + + override val value: T get() = valueFlow.value +} + +/** + * Bind a read-only [DeviceState] to a [Device] property + */ +public suspend fun Device.bindStateToProperty( + propertyName: String, + metaConverter: MetaConverter, +): DeviceState { + val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") + return BoundDeviceState(metaConverter, this, propertyName, initialValue) +} + +public suspend fun D.bindStateToProperty( + propertySpec: DevicePropertySpec, +): DeviceState = bindStateToProperty(propertySpec.name, propertySpec.converter) + +public fun DeviceState.map( + converter: MetaConverter, mapper: (T) -> R, +): DeviceState = object : DeviceState { + override val converter: MetaConverter = converter + override val value: R + get() = mapper(this@map.value) + + override val valueFlow: Flow = this@map.valueFlow.map(mapper) +} + +private class MutableBoundDeviceState( + converter: MetaConverter, + device: Device, + propertyName: String, + initialValue: T, +) : BoundDeviceState(converter, device, propertyName, initialValue), MutableDeviceState { + + override var value: T + get() = valueFlow.value + set(newValue) { + device.launch { + device.writeProperty(propertyName, converter.objectToMeta(newValue)) + } + } +} + +public suspend fun Device.bindMutableStateToProperty( + propertyName: String, + metaConverter: MetaConverter, +): MutableDeviceState { + val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") + return MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) +} + +public suspend fun D.bindMutableStateToProperty( + propertySpec: MutableDevicePropertySpec, +): MutableDeviceState = bindMutableStateToProperty(propertySpec.name, propertySpec.converter) + + diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt deleted file mode 100644 index 8d971bb..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt +++ /dev/null @@ -1,33 +0,0 @@ -package space.kscience.controls.constructor - -import space.kscience.controls.api.Device -import space.kscience.controls.api.DeviceHub -import space.kscience.controls.manager.DeviceManager -import space.kscience.dataforge.context.Factory -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.names.NameToken -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set - - -public class DeviceTree( - public val deviceManager: DeviceManager, - public val meta: Meta, - builder: Builder, -) : DeviceHub { - public class Builder(public val manager: DeviceManager) { - internal val childrenFactories = mutableMapOf>() - - public fun device(name: String, factory: Factory) { - childrenFactories[NameToken.parse(name)] = factory - } - } - - override val devices: Map = builder.childrenFactories.mapValues { (token, factory) -> - val devicesMeta = meta["devices"] - factory.build(deviceManager.context, devicesMeta?.get(token) ?: Meta.EMPTY) - } - -} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt index 6ac5a07..8111ff6 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt @@ -4,12 +4,9 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.datetime.Clock import space.kscience.controls.api.Device -import space.kscience.controls.spec.DeviceBySpec -import space.kscience.controls.spec.DevicePropertySpec -import space.kscience.controls.spec.DeviceSpec -import space.kscience.controls.spec.doubleProperty +import space.kscience.controls.manager.clock +import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.get @@ -33,7 +30,7 @@ public interface Drive : Device { public val position: Double public companion object : DeviceSpec() { - public val force: DevicePropertySpec by Drive.property( + public val force: MutableDevicePropertySpec by Drive.mutableProperty( MetaConverter.double, Drive::force ) @@ -48,16 +45,15 @@ public interface Drive : Device { public class VirtualDrive( context: Context, private val mass: Double, - position: Double, + public val positionState: MutableDeviceState, ) : Drive, DeviceBySpec(Drive, context) { private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds - private val clock = Clock.System + private val clock = context.clock override var force: Double = 0.0 - override var position: Double = position - private set + override val position: Double get() = positionState.value public var velocity: Double = 0.0 private set @@ -76,10 +72,10 @@ public class VirtualDrive( lastTime = realTime // compute new value based on velocity and acceleration from the previous step - position += velocity * dtSeconds + force/mass * dtSeconds.pow(2) / 2 + positionState.value += velocity * dtSeconds + force / mass * dtSeconds.pow(2) / 2 // compute new velocity based on acceleration on the previous step - velocity += force/mass * dtSeconds + velocity += force / mass * dtSeconds } } } @@ -89,3 +85,10 @@ public class VirtualDrive( } } +public suspend fun Drive.stateOfForce(): MutableDeviceState = bindMutableStateToProperty(Drive.force) + +public fun DeviceGroup.virtualDrive( + name: String, + mass: Double, + positionState: MutableDeviceState, +): VirtualDrive = device(name, VirtualDrive(context, mass, positionState)) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt index dd0dfed..f692e00 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt @@ -6,6 +6,7 @@ import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.booleanProperty import space.kscience.dataforge.context.Context +import space.kscience.dataforge.names.parseAsName /** @@ -25,7 +26,10 @@ public interface LimitSwitch : Device { */ public class VirtualLimitSwitch( context: Context, - private val lockedFunction: () -> Boolean, + public val lockedState: DeviceState, ) : DeviceBySpec(LimitSwitch, context), LimitSwitch { - override val locked: Boolean get() = lockedFunction() -} \ No newline at end of file + override val locked: Boolean get() = lockedState.value +} + +public fun DeviceGroup.virtualLimitSwitch(name: String, lockedState: DeviceState): VirtualLimitSwitch = + device(name.parseAsName(), VirtualLimitSwitch(context, lockedState)) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt index 4a4d0f3..e8e2ec3 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt @@ -6,25 +6,33 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import space.kscience.controls.manager.clock import space.kscience.controls.spec.DeviceBySpec import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit +/** + * Pid regulator parameters + */ +public data class PidParameters( + public val kp: Double, + public val ki: Double, + public val kd: Double, + public val timeStep: Duration = 1.milliseconds, +) + /** * A drive with PID regulator */ public class PidRegulator( public val drive: Drive, - public val kp: Double, - public val ki: Double, - public val kd: Double, - private val dt: Duration = 1.milliseconds, - private val clock: Clock = Clock.System, + public val pidParameters: PidParameters, ) : DeviceBySpec(Regulator, drive.context), Regulator { + private val clock = drive.context.clock + override var target: Double = drive.position private var lastTime: Instant = clock.now() @@ -41,7 +49,7 @@ public class PidRegulator( drive.start() updateJob = launch { while (isActive) { - delay(dt) + delay(pidParameters.timeStep) mutex.withLock { val realTime = clock.now() val delta = target - position @@ -53,7 +61,7 @@ public class PidRegulator( lastTime = realTime lastPosition = drive.position - drive.force = kp * delta + ki * integral + kd * derivative + drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative } } } @@ -64,5 +72,10 @@ public class PidRegulator( } override val position: Double get() = drive.position +} -} \ No newline at end of file +public fun DeviceGroup.pid( + name: String, + drive: Drive, + pidParameters: PidParameters, +): PidRegulator = device(name, PidRegulator(drive, pidParameters)) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt index fa43b57..71cb3b0 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt @@ -3,6 +3,7 @@ package space.kscience.controls.constructor import space.kscience.controls.api.Device import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DeviceSpec +import space.kscience.controls.spec.MutableDevicePropertySpec import space.kscience.controls.spec.doubleProperty import space.kscience.dataforge.meta.transformations.MetaConverter @@ -22,7 +23,7 @@ public interface Regulator : Device { public val position: Double public companion object : DeviceSpec() { - public val target: DevicePropertySpec by property(MetaConverter.double, Regulator::target) + public val target: MutableDevicePropertySpec by mutableProperty(MetaConverter.double, Regulator::target) public val position: DevicePropertySpec by doubleProperty { position } } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt new file mode 100644 index 0000000..e6a29a5 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt @@ -0,0 +1,41 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import space.kscience.dataforge.meta.transformations.MetaConverter + + +/** + * A state describing a [Double] value in the [range] + */ +public class DoubleRangeState( + initialValue: Double, + public val range: ClosedFloatingPointRange, +) : MutableDeviceState { + + init { + require(initialValue in range) { "Initial value should be in range" } + } + + override val converter: MetaConverter = MetaConverter.double + + private val _valueFlow = MutableStateFlow(initialValue) + + override var value: Double + get() = _valueFlow.value + set(newValue) { + _valueFlow.value = newValue.coerceIn(range) + } + + override val valueFlow: StateFlow get() = _valueFlow + + /** + * A state showing that the range is on its lower boundary + */ + public val atStartState: DeviceState = map(MetaConverter.boolean) { it == range.start } + + /** + * A state showing that the range is on its higher boundary + */ + public val atEndState: DeviceState = map(MetaConverter.boolean) { it == range.endInclusive } +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt index 872a5e6..f967c89 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt @@ -3,10 +3,7 @@ package space.kscience.controls.api import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import kotlinx.serialization.Serializable import space.kscience.controls.api.Device.Companion.DEVICE_TARGET import space.kscience.dataforge.context.ContextAware @@ -74,18 +71,6 @@ public interface Device : ContextAware, CoroutineScope { */ public suspend fun readProperty(propertyName: String): Meta - /** - * Get the logical state of property or return null if it is invalid - */ - public fun getProperty(propertyName: String): Meta? - - /** - * Invalidate property (set logical state to invalid) - * - * This message is suspended to provide lock-free local property changes (they require coroutine context). - */ - public suspend fun invalidate(propertyName: String) - /** * Set property [value] for a property with name [propertyName]. * In rare cases could suspend if the [Device] supports command queue, and it is full at the moment. @@ -126,17 +111,38 @@ public interface Device : ContextAware, CoroutineScope { } } +/** + * Device that caches properties values + */ +public interface CachingDevice : Device { + + /** + * Immediately (without waiting) get the cached (logical) state of property or return null if it is invalid + */ + public fun getProperty(propertyName: String): Meta? + + /** + * Invalidate property (set logical state to invalid). + * + * This message is suspended to provide lock-free local property changes (they require coroutine context). + */ + public suspend fun invalidate(propertyName: String) +} + /** * Get the logical state of property or suspend to read the physical value. */ -public suspend fun Device.getOrReadProperty(propertyName: String): Meta = +public suspend fun Device.requestProperty(propertyName: String): Meta = if (this is CachingDevice) { getProperty(propertyName) ?: readProperty(propertyName) +} else { + readProperty(propertyName) +} /** * Get a snapshot of the device logical state * */ -public fun Device.getAllProperties(): Meta = Meta { +public fun CachingDevice.getAllProperties(): Meta = Meta { for (descriptor in propertyDescriptors) { setMeta(Name.parse(descriptor.name), getProperty(descriptor.name)) } @@ -148,5 +154,11 @@ public fun Device.getAllProperties(): Meta = Meta { public fun Device.onPropertyChange( scope: CoroutineScope = this, callback: suspend PropertyChangedMessage.() -> Unit, -): Job = - messageFlow.filterIsInstance().onEach(callback).launchIn(scope) +): Job = messageFlow.filterIsInstance().onEach(callback).launchIn(scope) + +/** + * A [Flow] of property change messages for specific property. + */ +public fun Device.propertyMessageFlow(propertyName: String): Flow = messageFlow + .filterIsInstance() + .filter { it.property == propertyName } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt index 32df1bb..b3436e9 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt @@ -71,7 +71,7 @@ public data class PropertyChangedMessage( @SerialName("property.set") public data class PropertySetMessage( public val property: String, - public val value: Meta?, + public val value: Meta, override val sourceDevice: Name? = null, override val targetDevice: Name, override val comment: String? = null, diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt new file mode 100644 index 0000000..ca69b91 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt @@ -0,0 +1,25 @@ +package space.kscience.controls.manager + +import kotlinx.datetime.Clock +import space.kscience.dataforge.context.AbstractPlugin +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.PluginFactory +import space.kscience.dataforge.context.PluginTag +import space.kscience.dataforge.meta.Meta + +public class ClockManager : AbstractPlugin() { + override val tag: PluginTag get() = DeviceManager.tag + + public val clock: Clock by lazy { + //TODO add clock customization + Clock.System + } + + public companion object : PluginFactory { + override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP) + + override fun build(context: Context, meta: Meta): ClockManager = ClockManager() + } +} + +public val Context.clock: Clock get() = plugins[ClockManager]?.clock ?: Clock.System \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt index d3abc03..4d319af 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt @@ -17,21 +17,17 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess is PropertyGetMessage -> { PropertyChangedMessage( property = request.property, - value = getOrReadProperty(request.property), + value = requestProperty(request.property), sourceDevice = deviceTarget, targetDevice = request.sourceDevice ) } is PropertySetMessage -> { - if (request.value == null) { - invalidate(request.property) - } else { - writeProperty(request.property, request.value) - } + writeProperty(request.property, request.value) PropertyChangedMessage( property = request.property, - value = getOrReadProperty(request.property), + value = requestProperty(request.property), sourceDevice = deviceTarget, targetDevice = request.sourceDevice ) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt index da730f8..43574cf 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt @@ -20,7 +20,7 @@ import kotlin.coroutines.CoroutineContext * Write a meta [item] to [device] */ @OptIn(InternalDeviceAPI::class) -private suspend fun WritableDevicePropertySpec.writeMeta(device: D, item: Meta) { +private suspend fun MutableDevicePropertySpec.writeMeta(device: D, item: Meta) { write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter")) } @@ -48,7 +48,7 @@ private suspend fun DeviceActionSpec.executeWithMeta public abstract class DeviceBase( final override val context: Context, final override val meta: Meta = Meta.EMPTY, -) : Device { +) : CachingDevice { /** * Collection of property specifications @@ -166,7 +166,7 @@ public abstract class DeviceBase( propertyChanged(propertyName, value) } - is WritableDevicePropertySpec -> { + is MutableDevicePropertySpec -> { //if there is a writeable property with a given name, invalidate logical and write physical invalidate(propertyName) property.writeMeta(self, value) @@ -189,8 +189,8 @@ public abstract class DeviceBase( } @DFExperimental - override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED - protected set(value) { + final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED + private set(value) { if (field != value) { launch { sharedMessageFlow.emit( diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt index c42a23e..cb511ea 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt @@ -4,10 +4,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import space.kscience.controls.api.ActionDescriptor -import space.kscience.controls.api.Device -import space.kscience.controls.api.PropertyChangedMessage -import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.api.* import space.kscience.dataforge.meta.transformations.MetaConverter @@ -44,7 +41,7 @@ public interface DevicePropertySpec { public val DevicePropertySpec<*, *>.name: String get() = descriptor.name -public interface WritableDevicePropertySpec : DevicePropertySpec { +public interface MutableDevicePropertySpec : DevicePropertySpec { /** * Write physical value to a device */ @@ -84,21 +81,20 @@ public suspend fun D.read(propertySpec: DevicePropertySpec public suspend fun > D.readOrNull(propertySpec: DevicePropertySpec): T? = readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject) - -public operator fun D.get(propertySpec: DevicePropertySpec): T? = - getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject) +public suspend fun D.request(propertySpec: DevicePropertySpec): T? = + propertySpec.converter.metaToObject(requestProperty(propertySpec.name)) /** * Write typed property state and invalidate logical state */ -public suspend fun D.write(propertySpec: WritableDevicePropertySpec, value: T) { +public suspend fun D.write(propertySpec: MutableDevicePropertySpec, value: T) { writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value)) } /** * Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope */ -public operator fun D.set(propertySpec: WritableDevicePropertySpec, value: T): Job = launch { +public fun D.writeAsync(propertySpec: MutableDevicePropertySpec, value: T): Job = launch { write(propertySpec, value) } @@ -151,7 +147,7 @@ public fun D.useProperty( /** * Reset the logical state of a property */ -public suspend fun D.invalidate(propertySpec: DevicePropertySpec) { +public suspend fun D.invalidate(propertySpec: DevicePropertySpec) { invalidate(propertySpec.name) } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt index 14966ca..55122d9 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt @@ -72,9 +72,9 @@ public abstract class DeviceSpec { converter: MetaConverter, readWriteProperty: KMutableProperty1, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - ): PropertyDelegateProvider, ReadOnlyProperty>> = + ): PropertyDelegateProvider, ReadOnlyProperty>> = PropertyDelegateProvider { _, property -> - val deviceProperty = object : WritableDevicePropertySpec { + val deviceProperty = object : MutableDevicePropertySpec { override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { //TODO add the type from converter @@ -123,10 +123,10 @@ public abstract class DeviceSpec { name: String? = null, read: suspend D.() -> T?, write: suspend D.(T) -> Unit, - ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = + ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val propertyName = name ?: property.name - val deviceProperty = object : WritableDevicePropertySpec { + val deviceProperty = object : MutableDevicePropertySpec { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, mutable = true) .apply(descriptorBuilder) override val converter: MetaConverter = converter @@ -138,7 +138,7 @@ public abstract class DeviceSpec { } } _properties[propertyName] = deviceProperty - ReadOnlyProperty, WritableDevicePropertySpec> { _, _ -> + ReadOnlyProperty, MutableDevicePropertySpec> { _, _ -> deviceProperty } } @@ -218,9 +218,9 @@ public fun > DeviceSpec.logicalProperty( converter: MetaConverter, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, -): PropertyDelegateProvider, ReadOnlyProperty>> = +): PropertyDelegateProvider, ReadOnlyProperty>> = PropertyDelegateProvider { _, property -> - val deviceProperty = object : WritableDevicePropertySpec { + val deviceProperty = object : MutableDevicePropertySpec { val propertyName = name ?: property.name override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { //TODO add type from converter diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt index ff89662..70ef94a 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt @@ -97,7 +97,7 @@ public fun DeviceSpec.booleanProperty( name: String? = null, read: suspend D.() -> Boolean?, write: suspend D.(Boolean) -> Unit -): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty( MetaConverter.boolean, { @@ -117,7 +117,7 @@ public fun DeviceSpec.numberProperty( name: String? = null, read: suspend D.() -> Number, write: suspend D.(Number) -> Unit -): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) public fun DeviceSpec.doubleProperty( @@ -125,7 +125,7 @@ public fun DeviceSpec.doubleProperty( name: String? = null, read: suspend D.() -> Double, write: suspend D.(Double) -> Unit -): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) public fun DeviceSpec.stringProperty( @@ -133,7 +133,7 @@ public fun DeviceSpec.stringProperty( name: String? = null, read: suspend D.() -> String, write: suspend D.(String) -> Unit -): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) public fun DeviceSpec.metaProperty( @@ -141,5 +141,5 @@ public fun DeviceSpec.metaProperty( name: String? = null, read: suspend D.() -> Meta, write: suspend D.(Meta) -> Unit -): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write) \ No newline at end of file diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt index 2fcecff..474f86a 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt @@ -26,7 +26,7 @@ public class DeviceClient( private val deviceName: Name, incomingFlow: Flow, private val send: suspend (DeviceMessage) -> Unit, -) : Device { +) : CachingDevice { @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext) diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt index d0bdda4..9c30f8c 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import space.kscience.controls.api.get -import space.kscience.controls.api.getOrReadProperty +import space.kscience.controls.api.requestProperty import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.context.error import space.kscience.dataforge.context.logger @@ -91,7 +91,7 @@ public fun DeviceManager.launchTangoMagix( val device = get(payload.device) when (payload.action) { TangoAction.read -> { - val value = device.getOrReadProperty(payload.name) + val value = device.requestProperty(payload.name) respond(request, payload) { requestPayload -> requestPayload.copy( value = value, @@ -104,7 +104,7 @@ public fun DeviceManager.launchTangoMagix( device.writeProperty(payload.name, value) } //wait for value to be written and return final state - val value = device.getOrReadProperty(payload.name) + val value = device.requestProperty(payload.name) respond(request, payload) { requestPayload -> requestPayload.copy( value = value, diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt index b4fae90..8f4a2b4 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt @@ -6,10 +6,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import space.kscience.controls.api.Device -import space.kscience.controls.spec.DevicePropertySpec -import space.kscience.controls.spec.WritableDevicePropertySpec -import space.kscience.controls.spec.set -import space.kscience.controls.spec.useProperty +import space.kscience.controls.spec.* public class DeviceProcessImageBuilder internal constructor( @@ -29,10 +26,10 @@ public class DeviceProcessImageBuilder internal constructor( public fun bind( key: ModbusRegistryKey.Coil, - propertySpec: WritableDevicePropertySpec, + propertySpec: MutableDevicePropertySpec, ): ObservableDigitalOut = bind(key) { coil -> coil.addObserver { _, _ -> - device[propertySpec] = coil.isSet + device.writeAsync(propertySpec, coil.isSet) } device.useProperty(propertySpec) { value -> coil.set(value) @@ -89,10 +86,10 @@ public class DeviceProcessImageBuilder internal constructor( public fun bind( key: ModbusRegistryKey.HoldingRegister, - propertySpec: WritableDevicePropertySpec, + propertySpec: MutableDevicePropertySpec, ): ObservableRegister = bind(key) { register -> register.addObserver { _, _ -> - device[propertySpec] = register.toShort() + device.writeAsync(propertySpec, register.toShort()) } device.useProperty(propertySpec) { value -> register.setValue(value) @@ -121,7 +118,7 @@ public class DeviceProcessImageBuilder internal constructor( /** * Trigger [block] if one of register changes. */ - private fun List.onChange(block: (ByteReadPacket) -> Unit) { + private fun List.onChange(block: suspend (ByteReadPacket) -> Unit) { var ready = false forEach { register -> @@ -147,7 +144,7 @@ public class DeviceProcessImageBuilder internal constructor( } } - public fun bind(key: ModbusRegistryKey.HoldingRange, propertySpec: WritableDevicePropertySpec) { + public fun bind(key: ModbusRegistryKey.HoldingRange, propertySpec: MutableDevicePropertySpec) { val registers = List(key.count) { ObservableRegister() } @@ -157,7 +154,7 @@ public class DeviceProcessImageBuilder internal constructor( } registers.onChange { packet -> - device[propertySpec] = key.format.readObject(packet) + device.write(propertySpec, key.format.readObject(packet)) } device.useProperty(propertySpec) { value -> diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt index a05a81d..05fc3c6 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt @@ -19,10 +19,7 @@ import org.eclipse.milo.opcua.stack.core.AttributeId import org.eclipse.milo.opcua.stack.core.Identifiers import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText -import space.kscience.controls.api.Device -import space.kscience.controls.api.DeviceHub -import space.kscience.controls.api.PropertyDescriptor -import space.kscience.controls.api.onPropertyChange +import space.kscience.controls.api.* import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaSerializer @@ -31,7 +28,7 @@ import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.plus -public operator fun Device.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name) +public operator fun CachingDevice.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name) public suspend fun Device.read(propertyDescriptor: PropertyDescriptor): Meta = readProperty(propertyDescriptor.name) @@ -106,9 +103,11 @@ public class DeviceNameSpace( setTypeDefinition(Identifiers.BaseDataVariableType) }.build() - - device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let { - node.value = it + // Update initial value, but only if it is cached + if(device is CachingDevice) { + device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let { + node.value = it + } } /** diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts index 0b3e9f1..e1401d9 100644 --- a/controls-vision/build.gradle.kts +++ b/controls-vision/build.gradle.kts @@ -7,14 +7,23 @@ description = """ Dashboard and visualization extensions for devices """.trimIndent() -kscience{ +val visionforgeVersion = "0.3.0-dev-10" + +kscience { jvm() js() dependencies { api(projects.controlsCore) + api(projects.controlsConstructor) + api("space.kscience:visionforge-plotly:$visionforgeVersion") + api("space.kscience:visionforge-markdown:$visionforgeVersion") + } + + jvmMain{ + api("space.kscience:visionforge-server:$visionforgeVersion") } } -readme{ +readme { maturity = space.kscience.gradle.Maturity.PROTOTYPE } diff --git a/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt new file mode 100644 index 0000000..392930c --- /dev/null +++ b/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt @@ -0,0 +1,56 @@ +package space.kscience.controls.vision + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.datetime.Clock +import space.kscience.controls.api.Device +import space.kscience.controls.api.propertyMessageFlow +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.manager.clock +import space.kscience.dataforge.meta.ListValue +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.Null +import space.kscience.dataforge.meta.Value +import space.kscience.plotly.Plot +import space.kscience.plotly.models.Scatter +import space.kscience.plotly.models.TraceValues +import space.kscience.plotly.scatter + +private var TraceValues.values: List + get() = value?.list ?: emptyList() + set(newValues) { + value = ListValue(newValues) + } + +/** + * Add a trace that shows a [Device] property change over time. Show only latest [pointsNumber] . + * @return a [Job] that handles the listener + */ +public fun Plot.plotDeviceProperty( + device: Device, + propertyName: String, + extractValue: Meta.() -> Value = { value ?: Null }, + pointsNumber: Int = 400, + coroutineScope: CoroutineScope = device, + configuration: Scatter.() -> Unit = {}, +): Job = scatter(configuration).run { + val clock = device.context.clock + device.propertyMessageFlow(propertyName).onEach { message -> + x.strings = (x.strings + (message.time ?: clock.now()).toString()).takeLast(pointsNumber) + y.values = (y.values + message.value.extractValue()).takeLast(pointsNumber) + }.launchIn(coroutineScope) +} + +public fun Plot.plotDeviceState( + scope: CoroutineScope, + state: DeviceState, + pointsNumber: Int = 400, + configuration: Scatter.() -> Unit = {}, +): Job = scatter(configuration).run { + state.valueFlow.onEach { + x.strings = (x.strings + Clock.System.now().toString()).takeLast(pointsNumber) + y.numbers = (y.numbers + it).takeLast(pointsNumber) + }.launchIn(scope) +} \ No newline at end of file diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt index 2ba0fdb..8ccb8a4 100644 --- a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt @@ -4,8 +4,8 @@ package space.kscience.controls.demo.car import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import space.kscience.controls.manager.clock import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.read @@ -41,6 +41,8 @@ data class Vector2D(var x: Double = 0.0, var y: Double = 0.0) : MetaRepr { } open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec(IVirtualCar, context, meta), IVirtualCar { + private val clock = context.clock + private val timeScale = 1e-3 private val mass by meta.double(1000.0) // mass in kilograms @@ -57,7 +59,7 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec(I private var timeState: Instant? = null - private fun update(newTime: Instant = Clock.System.now()) { + private fun update(newTime: Instant = clock.now()) { //initialize time if it is not initialized if (timeState == null) { timeState = newTime @@ -102,7 +104,7 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec(I @OptIn(ExperimentalTime::class) override suspend fun onStart() { //initializing the clock - timeState = Clock.System.now() + timeState = clock.now() //starting regular updates doRecurring(100.milliseconds) { update() diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts new file mode 100644 index 0000000..031c3a0 --- /dev/null +++ b/demo/constructor/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("space.kscience.gradle.mpp") + application +} + +kscience { + fullStack("js/constructor.js") + useKtor() + dependencies { + api(projects.controlsVision) + } + jvmMain { + implementation("io.ktor:ktor-server-cio") + implementation(spclibs.logback.classic) + } +} + +application { + mainClass.set("space.kscience.controls.demo.constructor.MainKt") +} \ No newline at end of file diff --git a/demo/constructor/src/jsMain/kotlin/constructorJs.kt b/demo/constructor/src/jsMain/kotlin/constructorJs.kt new file mode 100644 index 0000000..27bbc28 --- /dev/null +++ b/demo/constructor/src/jsMain/kotlin/constructorJs.kt @@ -0,0 +1,6 @@ +import space.kscience.visionforge.plotly.PlotlyPlugin +import space.kscience.visionforge.runVisionClient + +public fun main(): Unit = runVisionClient { + plugin(PlotlyPlugin) +} \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/Main.kt b/demo/constructor/src/jvmMain/kotlin/Main.kt new file mode 100644 index 0000000..fdba90c --- /dev/null +++ b/demo/constructor/src/jvmMain/kotlin/Main.kt @@ -0,0 +1,103 @@ +package space.kscience.controls.demo.constructor + +import io.ktor.server.cio.CIO +import io.ktor.server.engine.embeddedServer +import io.ktor.server.http.content.staticResources +import io.ktor.server.routing.routing +import space.kscience.controls.api.get +import space.kscience.controls.constructor.* +import space.kscience.controls.manager.ClockManager +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.clock +import space.kscience.controls.spec.doRecurring +import space.kscience.controls.spec.name +import space.kscience.controls.spec.write +import space.kscience.controls.vision.plotDeviceProperty +import space.kscience.controls.vision.plotDeviceState +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request +import space.kscience.visionforge.VisionManager +import space.kscience.visionforge.html.VisionPage +import space.kscience.visionforge.plotly.PlotlyPlugin +import space.kscience.visionforge.plotly.plotly +import space.kscience.visionforge.server.close +import space.kscience.visionforge.server.openInBrowser +import space.kscience.visionforge.server.visionPage +import kotlin.math.PI +import kotlin.math.sin +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit + +@Suppress("ExtractKtorModule") +public fun main() { + val context = Context { + plugin(DeviceManager) + plugin(PlotlyPlugin) + plugin(ClockManager) + } + + val deviceManager = context.request(DeviceManager) + val visionManager = context.request(VisionManager) + + val state = DoubleRangeState(0.0, -100.0..100.0) + + val pidParameters = PidParameters( + kp = 2.5, + ki = 0.0, + kd = -0.1, + timeStep = 0.005.seconds + ) + + val device = deviceManager.deviceGroup { + val drive = virtualDrive("drive", 0.005, state) + val pid = pid("pid", drive, pidParameters) + virtualLimitSwitch("start", state.atStartState) + virtualLimitSwitch("end", state.atEndState) + + val clock = context.clock + val clockStart = clock.now() + + doRecurring(10.milliseconds) { + val timeFromStart = clock.now() - clockStart + val t = timeFromStart.toDouble(DurationUnit.SECONDS) + val freq = 0.1 + val target = 5 * sin(2.0 * PI * freq * t) + + sin(2 * PI * 21 * freq * t + 0.1 * (timeFromStart / pidParameters.timeStep)) + pid.write(Regulator.target, target) + } + } + + val server = embeddedServer(CIO, port = 7777) { + routing { + staticResources("", null, null) + } + + visionPage( + visionManager, + VisionPage.scriptHeader("js/constructor.js") + ) { + vision { + plotly { + plotDeviceState(this@embeddedServer, state){ + name = "value" + } + plotDeviceProperty(device["pid"], Regulator.target.name){ + name = "target" + } + } + } + } + + }.start(false) + + server.openInBrowser() + + + println("Enter 'exit' to close server") + while (readlnOrNull() != "exit") { + // + } + + server.close() +} \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/resources/logback.xml b/demo/constructor/src/jvmMain/resources/logback.xml new file mode 100644 index 0000000..3865b14 --- /dev/null +++ b/demo/constructor/src/jvmMain/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/MksPdr900Device.kt b/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/MksPdr900Device.kt index 7771709..df54bfa 100644 --- a/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/MksPdr900Device.kt +++ b/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/MksPdr900Device.kt @@ -94,7 +94,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec D.fxProperty( } } -fun D.fxProperty(spec: WritableDevicePropertySpec): Property = +fun D.fxProperty(spec: MutableDevicePropertySpec): Property = object : ObjectPropertyBase() { override fun getBean(): Any = this override fun getName(): String = spec.name @@ -51,7 +51,7 @@ fun D.fxProperty(spec: WritableDevicePropertySpec): onChange { newValue -> if (newValue != null) { - set(spec, newValue) + writeAsync(spec, newValue) } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index ee40d35..453519f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -69,5 +69,6 @@ include( ":demo:car", ":demo:motors", ":demo:echo", - ":demo:mks-pdr900" + ":demo:mks-pdr900", + ":demo:constructor" )