Test device constructor

This commit is contained in:
Alexander Nozik 2023-11-07 08:46:56 +03:00
parent 825f1a4d04
commit 53fc240c75
8 changed files with 123 additions and 84 deletions

View File

@ -33,7 +33,7 @@ public abstract class DeviceConstructor(
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
val name = nameOverride ?: property.name.asName() val name = nameOverride ?: property.name.asName()
val device = registerDevice(name, factory, meta, metaLocation ?: name) val device = install(name, factory, meta, metaLocation ?: name)
ReadOnlyProperty { _: DeviceConstructor, _ -> ReadOnlyProperty { _: DeviceConstructor, _ ->
device device
} }
@ -45,7 +45,7 @@ public abstract class DeviceConstructor(
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
val name = nameOverride ?: property.name.asName() val name = nameOverride ?: property.name.asName()
registerDevice(name, device) install(name, device)
ReadOnlyProperty { _: DeviceConstructor, _ -> ReadOnlyProperty { _: DeviceConstructor, _ ->
device device
} }
@ -58,7 +58,7 @@ public abstract class DeviceConstructor(
public fun <T : Any> property( public fun <T : Any> property(
state: DeviceState<T>, state: DeviceState<T>,
nameOverride: String? = null, nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> =
PropertyDelegateProvider { _: DeviceConstructor, property -> PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name val name = nameOverride ?: property.name
@ -78,7 +78,7 @@ public abstract class DeviceConstructor(
readInterval: Duration, readInterval: Duration,
initialState: T, initialState: T,
nameOverride: String? = null, nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property( ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader), DeviceState.external(this, metaConverter, readInterval, initialState, reader),
nameOverride, descriptorBuilder nameOverride, descriptorBuilder
@ -91,8 +91,8 @@ public abstract class DeviceConstructor(
public fun <T : Any> mutableProperty( public fun <T : Any> mutableProperty(
state: MutableDeviceState<T>, state: MutableDeviceState<T>,
nameOverride: String? = null, nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = ): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> =
PropertyDelegateProvider { _: DeviceConstructor, property -> PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name val name = nameOverride ?: property.name
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
@ -117,8 +117,8 @@ public abstract class DeviceConstructor(
readInterval: Duration, readInterval: Duration,
initialState: T, initialState: T,
nameOverride: String? = null, nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = mutableProperty( ): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = mutableProperty(
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
nameOverride, nameOverride,
descriptorBuilder descriptorBuilder

View File

@ -3,6 +3,8 @@ package space.kscience.controls.constructor
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.* import space.kscience.controls.api.*
import space.kscience.controls.api.DeviceLifecycleState.* import space.kscience.controls.api.DeviceLifecycleState.*
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
@ -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( private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
SupervisorJob(context.coroutineContext[Job]) +
CoroutineName("Device $this") + override val messageFlow: Flow<DeviceMessage>
CoroutineExceptionHandler { _, throwable -> get() = sharedMessageFlow
launch {
sharedMessageFlow.emit( override val coroutineContext: CoroutineContext = context.newCoroutineContext(
DeviceErrorMessage( SupervisorJob(context.coroutineContext[Job]) +
errorMessage = throwable.message, CoroutineName("Device $this") +
errorType = throwable::class.simpleName, CoroutineExceptionHandler { _, throwable ->
errorStackTrace = throwable.stackTraceToString() context.launch {
) sharedMessageFlow.emit(
DeviceErrorMessage(
errorMessage = throwable.message,
errorType = throwable::class.simpleName,
errorStackTrace = throwable.stackTraceToString()
) )
} )
} }
) }
} )
private val _devices = hashMapOf<NameToken, Device>() private val _devices = hashMapOf<NameToken, Device>()
@ -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 * Register and initialize (synchronize child's lifecycle state with group state) a new device in this group
*/ */
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
public fun <D : Device> registerDevice(token: NameToken, device: D): D { public fun <D : Device> install(token: NameToken, device: D): D {
require(_devices[token] == null) { "A child device with name $token already exists" } require(_devices[token] == null) { "A child device with name $token already exists" }
//start or stop the child if needed //start the child device if needed
when (lifecycleState) { if(lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() }
STARTING, STARTED -> launch { device.start() }
STOPPED -> device.stop()
ERROR -> {}
}
_devices[token] = device _devices[token] = device
return device return device
} }
@ -89,6 +92,14 @@ public open class DeviceGroup(
val name = descriptor.name.parseAsName() val name = descriptor.name.parseAsName()
require(properties[name] == null) { "Can't add property with name $name. It already exists." } require(properties[name] == null) { "Can't add property with name $name. It already exists." }
properties[name] = Property(state, descriptor) properties[name] = Property(state, descriptor)
state.metaFlow.onEach {
sharedMessageFlow.emit(
PropertyChangedMessage(
descriptor.name,
it
)
)
}.launchIn(this)
} }
private val actions: MutableMap<Name, Action> = hashMapOf() private val actions: MutableMap<Name, Action> = hashMapOf()
@ -115,10 +126,6 @@ public open class DeviceGroup(
property.valueAsMeta = value property.valueAsMeta = value
} }
private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
override val messageFlow: Flow<DeviceMessage>
get() = sharedMessageFlow
override suspend fun execute(actionName: String, argument: Meta?): Meta? { override suspend fun execute(actionName: String, argument: Meta?): Meta? {
val action = actions[actionName] ?: error("Action with name $actionName not found") val action = actions[actionName] ?: error("Action with name $actionName not found")
@ -185,7 +192,7 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup {
1 -> { 1 -> {
val token = name.first() val token = name.first()
when (val d = devices[token]) { when (val d = devices[token]) {
null -> registerDevice( null -> install(
token, token,
DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY) DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY)
) )
@ -201,15 +208,18 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup {
/** /**
* Register a device at given [name] path * Register a device at given [name] path
*/ */
public fun <D : Device> DeviceGroup.registerDevice(name: Name, device: D): D { public fun <D : Device> DeviceGroup.install(name: Name, device: D): D {
return when (name.length) { return when (name.length) {
0 -> error("Can't use empty name for a child device") 0 -> error("Can't use empty name for a child device")
1 -> registerDevice(name.first(), device) 1 -> install(name.first(), device)
else -> getOrCreateGroup(name.cutLast()).registerDevice(name.tokens.last(), device) else -> getOrCreateGroup(name.cutLast()).install(name.tokens.last(), device)
} }
} }
public fun <D : Device> DeviceGroup.registerDevice(name: String, device: D): D = registerDevice(name.parseAsName(), device) public fun <D : Device> DeviceGroup.install(name: String, device: D): D =
install(name.parseAsName(), device)
public fun <D : Device> 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. * Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error.
@ -218,23 +228,23 @@ public fun <D : Device> DeviceGroup.registerDevice(name: String, device: D): D =
* @param deviceMeta meta override for this specific device * @param deviceMeta meta override for this specific device
* @param metaLocation location of the template meta in parent group meta * @param metaLocation location of the template meta in parent group meta
*/ */
public fun <D : Device> DeviceGroup.registerDevice( public fun <D : Device> DeviceGroup.install(
name: Name, name: Name,
factory: Factory<D>, factory: Factory<D>,
deviceMeta: Meta? = null, deviceMeta: Meta? = null,
metaLocation: Name = name, metaLocation: Name = name,
): D { ): D {
val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[metaLocation])) val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[metaLocation]))
registerDevice(name, newDevice) install(name, newDevice)
return newDevice return newDevice
} }
public fun <D : Device> DeviceGroup.registerDevice( public fun <D : Device> DeviceGroup.install(
name: String, name: String,
factory: Factory<D>, factory: Factory<D>,
metaLocation: Name = name.parseAsName(), metaLocation: Name = name.parseAsName(),
metaBuilder: (MutableMeta.() -> Unit)? = null, 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]. * Create or edit a group with a given [name].

View File

@ -75,7 +75,7 @@ private open class BoundDeviceState<T>(
/** /**
* Bind a read-only [DeviceState] to a [Device] property * Bind a read-only [DeviceState] to a [Device] property
*/ */
public suspend fun <T> Device.bindStateToProperty( public suspend fun <T> Device.propertyAsState(
propertyName: String, propertyName: String,
metaConverter: MetaConverter<T>, metaConverter: MetaConverter<T>,
): DeviceState<T> { ): DeviceState<T> {
@ -83,9 +83,9 @@ public suspend fun <T> Device.bindStateToProperty(
return BoundDeviceState(metaConverter, this, propertyName, initialValue) return BoundDeviceState(metaConverter, this, propertyName, initialValue)
} }
public suspend fun <D : Device, T> D.bindStateToProperty( public suspend fun <D : Device, T> D.propertyAsState(
propertySpec: DevicePropertySpec<D, T>, propertySpec: DevicePropertySpec<D, T>,
): DeviceState<T> = bindStateToProperty(propertySpec.name, propertySpec.converter) ): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
public fun <T, R> DeviceState<T>.map( public fun <T, R> DeviceState<T>.map(
converter: MetaConverter<R>, mapper: (T) -> R, converter: MetaConverter<R>, mapper: (T) -> R,
@ -113,17 +113,28 @@ private class MutableBoundDeviceState<T>(
} }
} }
public suspend fun <T> Device.bindMutableStateToProperty( public fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
initialValue: T,
): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
public suspend fun <T> Device.mutablePropertyAsState(
propertyName: String, propertyName: String,
metaConverter: MetaConverter<T>, metaConverter: MetaConverter<T>,
): MutableDeviceState<T> { ): MutableDeviceState<T> {
val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") 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 : Device, T> D.bindMutableStateToProperty( public suspend fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>, propertySpec: MutableDevicePropertySpec<D, T>,
): MutableDeviceState<T> = bindMutableStateToProperty(propertySpec.name, propertySpec.converter) ): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
public fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
initialValue: T,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)
private open class ExternalState<T>( private open class ExternalState<T>(

View File

@ -96,4 +96,4 @@ public class VirtualDrive(
} }
} }
public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = bindMutableStateToProperty(Drive.force) public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = mutablePropertyAsState(Drive.force)

View File

@ -79,4 +79,4 @@ public fun DeviceGroup.pid(
name: String, name: String,
drive: Drive, drive: Drive,
pidParameters: PidParameters, pidParameters: PidParameters,
): PidRegulator = registerDevice(name, PidRegulator(drive, pidParameters)) ): PidRegulator = install(name, PidRegulator(drive, pidParameters))

View File

@ -72,23 +72,21 @@ public abstract class DeviceBase<D : Device>(
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.DROP_OLDEST
) )
override val coroutineContext: CoroutineContext by lazy { override val coroutineContext: CoroutineContext = context.newCoroutineContext(
context.newCoroutineContext( SupervisorJob(context.coroutineContext[Job]) +
SupervisorJob(context.coroutineContext[Job]) + CoroutineName("Device $this") +
CoroutineName("Device $this") + CoroutineExceptionHandler { _, throwable ->
CoroutineExceptionHandler { _, throwable -> launch {
launch { sharedMessageFlow.emit(
sharedMessageFlow.emit( DeviceErrorMessage(
DeviceErrorMessage( errorMessage = throwable.message,
errorMessage = throwable.message, errorType = throwable::class.simpleName,
errorType = throwable::class.simpleName, errorStackTrace = throwable.stackTraceToString()
errorStackTrace = throwable.stackTraceToString()
)
) )
} )
} }
) }
} )
/** /**

View File

@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.mpp")
application application
@ -20,4 +22,6 @@ kscience {
application { application {
mainClass.set("space.kscience.controls.demo.constructor.MainKt") mainClass.set("space.kscience.controls.demo.constructor.MainKt")
} }
kotlin.explicitApi = ExplicitApiMode.Disabled

View File

@ -1,18 +1,18 @@
package space.kscience.controls.demo.constructor package space.kscience.controls.demo.constructor
import space.kscience.controls.api.get
import space.kscience.controls.constructor.* import space.kscience.controls.constructor.*
import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.ClockManager
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.clock import space.kscience.controls.manager.clock
import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.doRecurring
import space.kscience.controls.spec.name import space.kscience.controls.spec.name
import space.kscience.controls.spec.write
import space.kscience.controls.vision.plot import space.kscience.controls.vision.plot
import space.kscience.controls.vision.plotDeviceProperty import space.kscience.controls.vision.plotDeviceProperty
import space.kscience.controls.vision.plotNumberState import space.kscience.controls.vision.plotNumberState
import space.kscience.controls.vision.showDashboard import space.kscience.controls.vision.showDashboard
import space.kscience.dataforge.context.Context 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.plotly.models.ScatterMode
import space.kscience.visionforge.plotly.PlotlyPlugin import space.kscience.visionforge.plotly.PlotlyPlugin
import kotlin.math.PI import kotlin.math.PI
@ -21,6 +21,27 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit 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() { public fun main() {
val context = Context { val context = Context {
plugin(DeviceManager) plugin(DeviceManager)
@ -37,25 +58,20 @@ public fun main() {
timeStep = 0.005.seconds timeStep = 0.005.seconds
) )
val device = context.registerDeviceGroup { val device = context.install("device", LinearDrive(context, state, 0.005, pidParameters)).apply {
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 clock = context.clock val clock = context.clock
val clockStart = clock.now() val clockStart = clock.now()
doRecurring(10.milliseconds) { doRecurring(10.milliseconds) {
val timeFromStart = clock.now() - clockStart val timeFromStart = clock.now() - clockStart
val t = timeFromStart.toDouble(DurationUnit.SECONDS) val t = timeFromStart.toDouble(DurationUnit.SECONDS)
val freq = 0.1 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)) sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
pid.write(Regulator.target, target)
} }
} }
val maxAge = 10.seconds val maxAge = 10.seconds
context.showDashboard { context.showDashboard {
@ -63,21 +79,21 @@ public fun main() {
plotNumberState(context, state, maxAge = maxAge) { plotNumberState(context, state, maxAge = maxAge) {
name = "real position" name = "real position"
} }
plotDeviceProperty(device["pid"], Regulator.position.name, maxAge = maxAge) { plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge) {
name = "read position" name = "read position"
} }
plotDeviceProperty(device["pid"], Regulator.target.name, maxAge = maxAge) { plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge) {
name = "target" name = "target"
} }
} }
plot { plot {
plotDeviceProperty(device["start"], LimitSwitch.locked.name, maxAge = maxAge) { plotDeviceProperty(device.start, LimitSwitch.locked.name, maxAge = maxAge) {
name = "start measured" name = "start measured"
mode = ScatterMode.markers mode = ScatterMode.markers
} }
plotDeviceProperty(device["end"], LimitSwitch.locked.name, maxAge = maxAge) { plotDeviceProperty(device.end, LimitSwitch.locked.name, maxAge = maxAge) {
name = "end measured" name = "end measured"
mode = ScatterMode.markers mode = ScatterMode.markers
} }