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, 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<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
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 <T : Any> property(
state: DeviceState<T>,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> =
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<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
nameOverride, descriptorBuilder
@ -91,8 +91,8 @@ public abstract class DeviceConstructor(
public fun <T : Any> mutableProperty(
state: MutableDeviceState<T>,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> =
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> =
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<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = mutableProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = mutableProperty(
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
nameOverride,
descriptorBuilder

View File

@ -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<DeviceMessage>()
override val messageFlow: Flow<DeviceMessage>
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<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
*/
@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" }
//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<Name, Action> = hashMapOf()
@ -115,10 +126,6 @@ public open class DeviceGroup(
property.valueAsMeta = value
}
private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
override val messageFlow: Flow<DeviceMessage>
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 <D : Device> DeviceGroup.registerDevice(name: Name, device: D): D {
public fun <D : Device> 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 <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.
@ -218,23 +228,23 @@ public fun <D : Device> 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 <D : Device> DeviceGroup.registerDevice(
public fun <D : Device> DeviceGroup.install(
name: Name,
factory: Factory<D>,
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 <D : Device> DeviceGroup.registerDevice(
public fun <D : Device> DeviceGroup.install(
name: String,
factory: Factory<D>,
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].

View File

@ -75,7 +75,7 @@ private open class BoundDeviceState<T>(
/**
* Bind a read-only [DeviceState] to a [Device] property
*/
public suspend fun <T> Device.bindStateToProperty(
public suspend fun <T> Device.propertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
): DeviceState<T> {
@ -83,9 +83,9 @@ public suspend fun <T> Device.bindStateToProperty(
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>,
): DeviceState<T> = bindStateToProperty(propertySpec.name, propertySpec.converter)
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
public fun <T, R> DeviceState<T>.map(
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,
metaConverter: MetaConverter<T>,
): MutableDeviceState<T> {
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>,
): 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>(

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,
drive: Drive,
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
)
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()
)
}
)
}
)
}
}
)
/**

View File

@ -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")
}
}
kotlin.explicitApi = ExplicitApiMode.Disabled

View File

@ -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
}