diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt index 54ebc4e..01d9087 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt @@ -33,7 +33,7 @@ public abstract class DeviceConstructor( ): PropertyDelegateProvider> = PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> val name = nameOverride ?: property.name.asName() - val device = registerDevice(name, factory, meta, metaLocation ?: name) + val device = install(name, factory, meta, metaLocation ?: name) ReadOnlyProperty { _: DeviceConstructor, _ -> device } @@ -45,7 +45,7 @@ public abstract class DeviceConstructor( ): PropertyDelegateProvider> = PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> val name = nameOverride ?: property.name.asName() - registerDevice(name, device) + install(name, device) ReadOnlyProperty { _: DeviceConstructor, _ -> device } @@ -58,7 +58,7 @@ public abstract class DeviceConstructor( public fun property( state: DeviceState, nameOverride: String? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ): PropertyDelegateProvider> = PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name @@ -78,7 +78,7 @@ public abstract class DeviceConstructor( readInterval: Duration, initialState: T, nameOverride: String? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ): PropertyDelegateProvider> = property( DeviceState.external(this, metaConverter, readInterval, initialState, reader), nameOverride, descriptorBuilder @@ -91,8 +91,8 @@ public abstract class DeviceConstructor( public fun mutableProperty( state: MutableDeviceState, nameOverride: String? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit, - ): PropertyDelegateProvider> = + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + ): PropertyDelegateProvider> = PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) @@ -117,8 +117,8 @@ public abstract class DeviceConstructor( readInterval: Duration, initialState: T, nameOverride: String? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit, - ): PropertyDelegateProvider> = mutableProperty( + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + ): PropertyDelegateProvider> = mutableProperty( DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), nameOverride, descriptorBuilder 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 index 936dd04..68f8301 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -3,6 +3,8 @@ package space.kscience.controls.constructor import kotlinx.coroutines.* import kotlinx.coroutines.flow.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.DeviceLifecycleState.* import space.kscience.controls.manager.DeviceManager @@ -40,25 +42,30 @@ public open class DeviceGroup( ) - override val context: Context get() = deviceManager.context + override final 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 sharedMessageFlow = MutableSharedFlow() + + override val messageFlow: Flow + get() = sharedMessageFlow + + override val coroutineContext: CoroutineContext = context.newCoroutineContext( + SupervisorJob(context.coroutineContext[Job]) + + CoroutineName("Device $this") + + CoroutineExceptionHandler { _, throwable -> + context.launch { + sharedMessageFlow.emit( + DeviceErrorMessage( + errorMessage = throwable.message, + errorType = throwable::class.simpleName, + errorStackTrace = throwable.stackTraceToString() ) - } + ) } - ) - } + } + ) + private val _devices = hashMapOf() @@ -68,14 +75,10 @@ public open class DeviceGroup( * Register and initialize (synchronize child's lifecycle state with group state) a new device in this group */ @OptIn(DFExperimental::class) - public fun registerDevice(token: NameToken, device: D): D { + public fun install(token: NameToken, device: D): D { require(_devices[token] == null) { "A child device with name $token already exists" } - //start or stop the child if needed - when (lifecycleState) { - STARTING, STARTED -> launch { device.start() } - STOPPED -> device.stop() - ERROR -> {} - } + //start the child device if needed + if(lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() } _devices[token] = device return device } @@ -89,6 +92,14 @@ public open class DeviceGroup( val name = descriptor.name.parseAsName() require(properties[name] == null) { "Can't add property with name $name. It already exists." } properties[name] = Property(state, descriptor) + state.metaFlow.onEach { + sharedMessageFlow.emit( + PropertyChangedMessage( + descriptor.name, + it + ) + ) + }.launchIn(this) } private val actions: MutableMap = hashMapOf() @@ -115,10 +126,6 @@ public open class DeviceGroup( 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") @@ -185,7 +192,7 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { 1 -> { val token = name.first() when (val d = devices[token]) { - null -> registerDevice( + null -> install( token, DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY) ) @@ -201,15 +208,18 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { /** * Register a device at given [name] path */ -public fun DeviceGroup.registerDevice(name: Name, device: D): D { +public fun DeviceGroup.install(name: Name, device: D): D { return when (name.length) { 0 -> error("Can't use empty name for a child device") - 1 -> registerDevice(name.first(), device) - else -> getOrCreateGroup(name.cutLast()).registerDevice(name.tokens.last(), device) + 1 -> install(name.first(), device) + else -> getOrCreateGroup(name.cutLast()).install(name.tokens.last(), device) } } -public fun DeviceGroup.registerDevice(name: String, device: D): D = registerDevice(name.parseAsName(), device) +public fun DeviceGroup.install(name: String, device: D): D = + install(name.parseAsName(), device) + +public fun Context.install(name: String, device: D): D = request(DeviceManager).install(name, device) /** * Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error. @@ -218,23 +228,23 @@ public fun DeviceGroup.registerDevice(name: String, device: D): D = * @param deviceMeta meta override for this specific device * @param metaLocation location of the template meta in parent group meta */ -public fun DeviceGroup.registerDevice( +public fun DeviceGroup.install( name: Name, factory: Factory, deviceMeta: Meta? = null, metaLocation: Name = name, ): D { val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[metaLocation])) - registerDevice(name, newDevice) + install(name, newDevice) return newDevice } -public fun DeviceGroup.registerDevice( +public fun DeviceGroup.install( name: String, factory: Factory, metaLocation: Name = name.parseAsName(), metaBuilder: (MutableMeta.() -> Unit)? = null, -): D = registerDevice(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }, metaLocation) +): D = install(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }, metaLocation) /** * Create or edit a group with a given [name]. 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 6e841f8..ed7027a 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 @@ -75,7 +75,7 @@ private open class BoundDeviceState( /** * Bind a read-only [DeviceState] to a [Device] property */ -public suspend fun Device.bindStateToProperty( +public suspend fun Device.propertyAsState( propertyName: String, metaConverter: MetaConverter, ): DeviceState { @@ -83,9 +83,9 @@ public suspend fun Device.bindStateToProperty( return BoundDeviceState(metaConverter, this, propertyName, initialValue) } -public suspend fun D.bindStateToProperty( +public suspend fun D.propertyAsState( propertySpec: DevicePropertySpec, -): DeviceState = bindStateToProperty(propertySpec.name, propertySpec.converter) +): DeviceState = propertyAsState(propertySpec.name, propertySpec.converter) public fun DeviceState.map( converter: MetaConverter, mapper: (T) -> R, @@ -113,17 +113,28 @@ private class MutableBoundDeviceState( } } -public suspend fun Device.bindMutableStateToProperty( +public fun Device.mutablePropertyAsState( + propertyName: String, + metaConverter: MetaConverter, + initialValue: T, +): MutableDeviceState = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) + +public suspend fun Device.mutablePropertyAsState( propertyName: String, metaConverter: MetaConverter, ): MutableDeviceState { val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") - return MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) + return mutablePropertyAsState(propertyName, metaConverter, initialValue) } -public suspend fun D.bindMutableStateToProperty( +public suspend fun D.mutablePropertyAsState( propertySpec: MutableDevicePropertySpec, -): MutableDeviceState = bindMutableStateToProperty(propertySpec.name, propertySpec.converter) +): MutableDeviceState = mutablePropertyAsState(propertySpec.name, propertySpec.converter) + +public fun D.mutablePropertyAsState( + propertySpec: MutableDevicePropertySpec, + initialValue: T, +): MutableDeviceState = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue) private open class ExternalState( 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 d8802eb..9366971 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 @@ -96,4 +96,4 @@ public class VirtualDrive( } } -public suspend fun Drive.stateOfForce(): MutableDeviceState = bindMutableStateToProperty(Drive.force) +public suspend fun Drive.stateOfForce(): MutableDeviceState = mutablePropertyAsState(Drive.force) 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 e6f9df1..9fc6e18 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 @@ -79,4 +79,4 @@ public fun DeviceGroup.pid( name: String, drive: Drive, pidParameters: PidParameters, -): PidRegulator = registerDevice(name, PidRegulator(drive, pidParameters)) \ No newline at end of file +): PidRegulator = install(name, PidRegulator(drive, pidParameters)) \ No newline at end of file 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 43574cf..846815c 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 @@ -72,23 +72,21 @@ public abstract class DeviceBase( onBufferOverflow = BufferOverflow.DROP_OLDEST ) - 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() - ) + override val coroutineContext: CoroutineContext = 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() ) - } + ) } - ) - } + } + ) /** diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index dec09b7..06cb285 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + plugins { id("space.kscience.gradle.mpp") application @@ -20,4 +22,6 @@ kscience { application { mainClass.set("space.kscience.controls.demo.constructor.MainKt") -} \ No newline at end of file +} + +kotlin.explicitApi = ExplicitApiMode.Disabled \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 59881e5..385ccfc 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -1,18 +1,18 @@ package space.kscience.controls.demo.constructor -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.plot import space.kscience.controls.vision.plotDeviceProperty import space.kscience.controls.vision.plotNumberState import space.kscience.controls.vision.showDashboard import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request +import space.kscience.dataforge.meta.Meta import space.kscience.plotly.models.ScatterMode import space.kscience.visionforge.plotly.PlotlyPlugin import kotlin.math.PI @@ -21,6 +21,27 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit + +class LinearDrive( + context: Context, + state: DoubleRangeState, + mass: Double, + pidParameters: PidParameters, + meta: Meta = Meta.EMPTY, +) : DeviceConstructor(context.request(DeviceManager), meta) { + + val drive by device(VirtualDrive.factory(mass, state)) + val pid by device(PidRegulator(drive, pidParameters)) + + val start by device(LimitSwitch.factory(state.atStartState)) + val end by device(LimitSwitch.factory(state.atEndState)) + + + val position by property(state) + var target by mutableProperty(pid.mutablePropertyAsState(Regulator.target, 0.0)) +} + + public fun main() { val context = Context { plugin(DeviceManager) @@ -37,25 +58,20 @@ public fun main() { timeStep = 0.005.seconds ) - val device = context.registerDeviceGroup { - val drive = VirtualDrive(context, 0.005, state) - val pid = pid("pid", drive, pidParameters) - registerDevice("start", LimitSwitch.factory(state.atStartState)) - registerDevice("end", LimitSwitch.factory(state.atEndState)) - + val device = context.install("device", LinearDrive(context, state, 0.005, pidParameters)).apply { 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) + + + target = 5 * sin(2.0 * PI * freq * t) + sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep)) - pid.write(Regulator.target, target) } } + val maxAge = 10.seconds context.showDashboard { @@ -63,21 +79,21 @@ public fun main() { plotNumberState(context, state, maxAge = maxAge) { name = "real position" } - plotDeviceProperty(device["pid"], Regulator.position.name, maxAge = maxAge) { + plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge) { name = "read position" } - plotDeviceProperty(device["pid"], Regulator.target.name, maxAge = maxAge) { + plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge) { name = "target" } } plot { - plotDeviceProperty(device["start"], LimitSwitch.locked.name, maxAge = maxAge) { + plotDeviceProperty(device.start, LimitSwitch.locked.name, maxAge = maxAge) { name = "start measured" mode = ScatterMode.markers } - plotDeviceProperty(device["end"], LimitSwitch.locked.name, maxAge = maxAge) { + plotDeviceProperty(device.end, LimitSwitch.locked.name, maxAge = maxAge) { name = "end measured" mode = ScatterMode.markers }