diff --git a/CHANGELOG.md b/CHANGELOG.md index b7673a4..4f3b5a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased ### Added +- Device lifecycle message +- Low-code constructor ### Changed diff --git a/controls-constructor/build.gradle.kts b/controls-constructor/build.gradle.kts index 5815444..e62dd8e 100644 --- a/controls-constructor/build.gradle.kts +++ b/controls-constructor/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } description = """ - A low-code constructor foe composite devices simulation + A low-code constructor for composite devices simulation """.trimIndent() kscience{ 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 new file mode 100644 index 0000000..2e6461f --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -0,0 +1,25 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.Flow +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.transformations.MetaConverter + +/** + * An observable state of a device + */ +public interface DeviceState { + public val converter: MetaConverter + public val value: T + + public val valueFlow: Flow + + public val metaFlow: Flow +} + + +/** + * A mutable state of a device + */ +public interface MutableDeviceState : DeviceState{ + override var value: T +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceTree.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt similarity index 88% rename from controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceTree.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt index f100af6..8d971bb 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceTree.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.spec +package space.kscience.controls.constructor import space.kscience.controls.api.Device import space.kscience.controls.api.DeviceHub @@ -7,11 +7,8 @@ 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.Map import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.mapValues -import kotlin.collections.mutableMapOf import kotlin.collections.set 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 new file mode 100644 index 0000000..6ac5a07 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt @@ -0,0 +1,91 @@ +package space.kscience.controls.constructor + +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.dataforge.context.Context +import space.kscience.dataforge.meta.double +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.transformations.MetaConverter +import kotlin.math.pow +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit + +/** + * A classic drive regulated by force with encoder + */ +public interface Drive : Device { + /** + * Get or set drive force or momentum + */ + public var force: Double + + /** + * Current position value + */ + public val position: Double + + public companion object : DeviceSpec() { + public val force: DevicePropertySpec by Drive.property( + MetaConverter.double, + Drive::force + ) + + public val position: DevicePropertySpec by doubleProperty { position } + } +} + +/** + * A virtual drive + */ +public class VirtualDrive( + context: Context, + private val mass: Double, + position: Double, +) : Drive, DeviceBySpec(Drive, context) { + + private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds + private val clock = Clock.System + + override var force: Double = 0.0 + + override var position: Double = position + private set + + public var velocity: Double = 0.0 + private set + + private var updateJob: Job? = null + + override suspend fun onStart() { + updateJob = launch { + var lastTime = clock.now() + while (isActive) { + delay(dt) + val realTime = clock.now() + val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) + + //set last time and value to new values + lastTime = realTime + + // compute new value based on velocity and acceleration from the previous step + position += velocity * dtSeconds + force/mass * dtSeconds.pow(2) / 2 + + // compute new velocity based on acceleration on the previous step + velocity += force/mass * dtSeconds + } + } + } + + override fun onStop() { + updateJob?.cancel() + } +} + 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 6e82064..dd0dfed 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 @@ -1,4 +1,4 @@ -package center.sciprog.controls.devices.misc +package space.kscience.controls.constructor import space.kscience.controls.api.Device import space.kscience.controls.spec.DeviceBySpec 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 cd38c93..4a4d0f3 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 @@ -1,4 +1,4 @@ -package center.sciprog.controls.devices.misc +package space.kscience.controls.constructor import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -8,28 +8,27 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Clock import kotlinx.datetime.Instant -import space.kscience.controls.api.DeviceLifecycleState import space.kscience.controls.spec.DeviceBySpec import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit /** - * PID controller on top of a [Regulator] + * A drive with PID regulator */ public class PidRegulator( - public val regulator: Regulator, + public val drive: Drive, public val kp: Double, public val ki: Double, public val kd: Double, - private val dt: Duration = 0.5.milliseconds, + private val dt: Duration = 1.milliseconds, private val clock: Clock = Clock.System, -) : DeviceBySpec(Regulator, regulator.context), Regulator { +) : DeviceBySpec(Regulator, drive.context), Regulator { - override var target: Double = regulator.target + override var target: Double = drive.position private var lastTime: Instant = clock.now() - private var lastRegulatorTarget: Double = target + private var lastPosition: Double = target private var integral: Double = 0.0 @@ -39,10 +38,7 @@ public class PidRegulator( override suspend fun onStart() { - if(regulator.lifecycleState == DeviceLifecycleState.STOPPED){ - regulator.start() - } - regulator.start() + drive.start() updateJob = launch { while (isActive) { delay(dt) @@ -51,13 +47,13 @@ public class PidRegulator( val delta = target - position val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) integral += delta * dtSeconds - val derivative = (regulator.target - lastRegulatorTarget) / dtSeconds + val derivative = (drive.position - lastPosition) / dtSeconds //set last time and value to new values lastTime = realTime - lastRegulatorTarget = regulator.target + lastPosition = drive.position - regulator.target = regulator.position + kp * delta + ki * integral + kd * derivative + drive.force = kp * delta + ki * integral + kd * derivative } } } @@ -67,129 +63,6 @@ public class PidRegulator( updateJob?.cancel() } - override val position: Double get() = regulator.position + override val position: Double get() = drive.position -} - - -// -//interface PidRegulator : Device { -// /** -// * Proportional coefficient -// */ -// val kp: Double -// -// /** -// * Integral coefficient -// */ -// val ki: Double -// -// /** -// * Differential coefficient -// */ -// val kd: Double -// -// /** -// * The target value for PID -// */ -// var target: Double -// -// /** -// * Read current value -// */ -// suspend fun read(): Double -// -// companion object : DeviceSpec() { -// val target by property(MetaConverter.double, PidRegulator::target) -// val value by doubleProperty { read() } -// } -//} -// -///** -// * -// */ -//class VirtualPid( -// context: Context, -// override val kp: Double, -// override val ki: Double, -// override val kd: Double, -// val mass: Double, -// override var target: Double = 0.0, -// private val dt: Duration = 0.5.milliseconds, -// private val clock: Clock = Clock.System, -//) : DeviceBySpec(PidRegulator, context), PidRegulator { -// -// private val mutex = Mutex() -// -// -// private var lastTime: Instant = clock.now() -// private var lastValue: Double = target -// -// private var value: Double = target -// private var velocity: Double = 0.0 -// private var acceleration: Double = 0.0 -// private var integral: Double = 0.0 -// -// -// private var updateJob: Job? = null -// -// override suspend fun onStart() { -// updateJob = launch { -// while (isActive) { -// delay(dt) -// mutex.withLock { -// val realTime = clock.now() -// val delta = target - value -// val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) -// integral += delta * dtSeconds -// val derivative = (value - lastValue) / dtSeconds -// -// //set last time and value to new values -// lastTime = realTime -// lastValue = value -// -// // compute new value based on velocity and acceleration from the previous step -// value += velocity * dtSeconds + acceleration * dtSeconds.pow(2) / 2 -// -// // compute new velocity based on acceleration on the previous step -// velocity += acceleration * dtSeconds -// -// //compute force for the next step based on current values -// acceleration = (kp * delta + ki * integral + kd * derivative) / mass -// -// -// check(value.isFinite() && velocity.isFinite()) { -// "Value $value is not finite" -// } -// } -// } -// } -// } -// -// override fun onStop() { -// updateJob?.cancel() -// super.stop() -// } -// -// override suspend fun read(): Double = value -// -// suspend fun readVelocity(): Double = velocity -// -// suspend fun readAcceleration(): Double = acceleration -// -// suspend fun write(newTarget: Double) = mutex.withLock { -// require(newTarget.isFinite()) { "Value $newTarget is not valid" } -// target = newTarget -// } -// -// companion object : Factory { -// override fun build(context: Context, meta: Meta) = VirtualPid( -// context, -// meta["kp"].double ?: error("Kp is not defined"), -// meta["ki"].double ?: error("Ki is not defined"), -// meta["kd"].double ?: error("Kd is not defined"), -// meta["m"].double ?: error("Mass is not defined"), -// ) -// -// } -//} \ No newline at end of file +} \ 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 2db263d..fa43b57 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 @@ -1,17 +1,14 @@ -package center.sciprog.controls.devices.misc +package space.kscience.controls.constructor -import kotlinx.coroutines.Job 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.dataforge.context.Context import space.kscience.dataforge.meta.transformations.MetaConverter /** - * A single axis drive + * A regulator with target value and current position */ public interface Regulator : Device { /** @@ -29,23 +26,4 @@ public interface Regulator : Device { public val position: DevicePropertySpec by doubleProperty { position } } -} - -/** - * Virtual [Regulator] with speed limit - */ -public class VirtualRegulator( - context: Context, - value: Double, - private val speed: Double, -) : DeviceBySpec(Regulator, context), Regulator { - - private var moveJob: Job? = null - - override var position: Double = value - private set - - override var target: Double = value - - } \ 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 a0268c2..872a5e6 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 @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.serialization.Serializable import space.kscience.controls.api.Device.Companion.DEVICE_TARGET import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.info @@ -19,6 +20,7 @@ import space.kscience.dataforge.names.Name /** * A lifecycle state of a device */ +@Serializable public enum class DeviceLifecycleState { /** @@ -34,7 +36,12 @@ public enum class DeviceLifecycleState { /** * The Device is closed */ - STOPPED + STOPPED, + + /** + * The device encountered irrecoverable error + */ + ERROR } /** @@ -98,7 +105,8 @@ public interface Device : ContextAware, CoroutineScope { public suspend fun execute(actionName: String, argument: Meta? = null): Meta? /** - * Initialize the device. This function suspends until the device is finished initialization + * Initialize the device. This function suspends until the device is finished initialization. + * Does nothing if the device is started or is starting */ public suspend fun start(): Unit = Unit 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 c59e4c3..32df1bb 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 @@ -1,4 +1,4 @@ -@file:OptIn(ExperimentalSerializationApi::class) +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class) package space.kscience.controls.api @@ -228,6 +228,21 @@ public data class DeviceErrorMessage( override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) } +/** + * Device [Device.lifecycleState] is changed + */ +@Serializable +@SerialName("lifecycle") +public data class DeviceLifeCycleMessage( + val state: DeviceLifecycleState, + override val sourceDevice: Name = Name.EMPTY, + override val targetDevice: Name? = null, + override val comment: String? = null, + @EncodeDefault override val time: Instant? = Clock.System.now(), +) : DeviceMessage() { + override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) +} + public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta() 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 13d072c..d3abc03 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 @@ -64,6 +64,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess is DeviceErrorMessage, is EmptyDeviceMessage, is DeviceLogMessage, + is DeviceLifeCycleMessage, -> null } } catch (ex: Exception) { @@ -87,7 +88,7 @@ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMe * Collect all messages from given [DeviceHub], applying proper relative names. */ public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow { - + //TODO could we avoid using downstream scope? val outbox = MutableSharedFlow() if (this is Device) { 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 3b6bce0..da730f8 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 @@ -190,7 +190,16 @@ public abstract class DeviceBase( @DFExperimental override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED - protected set + protected set(value) { + if (field != value) { + launch { + sharedMessageFlow.emit( + DeviceLifeCycleMessage(value) + ) + } + } + field = value + } protected open suspend fun onStart() { @@ -198,7 +207,7 @@ public abstract class DeviceBase( @OptIn(DFExperimental::class) final override suspend fun start() { - if(lifecycleState == DeviceLifecycleState.STOPPED) { + if (lifecycleState == DeviceLifecycleState.STOPPED) { super.start() lifecycleState = DeviceLifecycleState.STARTING onStart() diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts new file mode 100644 index 0000000..0b3e9f1 --- /dev/null +++ b/controls-vision/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("space.kscience.gradle.mpp") + `maven-publish` +} + +description = """ + Dashboard and visualization extensions for devices +""".trimIndent() + +kscience{ + jvm() + js() + dependencies { + api(projects.controlsCore) + } +} + +readme{ + maturity = space.kscience.gradle.Maturity.PROTOTYPE +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0f53909..ee40d35 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,6 +51,7 @@ include( ":controls-storage", ":controls-storage:controls-xodus", ":controls-constructor", + ":controls-vision", ":magix", ":magix:magix-api", ":magix:magix-server",