Compare commits

...

2 Commits

Author SHA1 Message Date
1414cf5a2f implement constructor 2023-10-30 21:35:46 +03:00
1fcdbdc9f4 Update constructor 2023-10-28 14:18:00 +03:00
32 changed files with 939 additions and 300 deletions

View File

@ -3,8 +3,11 @@
## Unreleased ## Unreleased
### Added ### Added
- Device lifecycle message
- Low-code constructor
### Changed ### Changed
- Property caching moved from core `Device` to the `CachingDevice`
### Deprecated ### Deprecated

View File

@ -4,7 +4,7 @@ plugins {
} }
description = """ description = """
A low-code constructor foe composite devices simulation A low-code constructor for composite devices simulation
""".trimIndent() """.trimIndent()
kscience{ kscience{

View File

@ -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<out Any>,
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<NameToken, Device>()
override val devices: Map<NameToken, Device> = _devices
public fun <D : Device> 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<Name, Property> = hashMapOf()
public fun property(descriptor: PropertyDescriptor, state: DeviceState<out Any>) {
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<Name, Action> = hashMapOf()
override val propertyDescriptors: Collection<PropertyDescriptor>
get() = properties.values.map { it.descriptor }
override val actionDescriptors: Collection<ActionDescriptor>
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<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")
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 <D : Device> 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 <D: Device> 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<Device>, 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<Device>,
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 <T : Any> DeviceGroup.property(
name: String,
state: DeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): DeviceState<T> {
property(
PropertyDescriptor(name).apply(descriptorBuilder),
state
)
return state
}
public fun <T : Any> DeviceGroup.mutableProperty(
name: String,
state: MutableDeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): MutableDeviceState<T> {
property(
PropertyDescriptor(name).apply(descriptorBuilder),
state
)
return state
}
public fun <T : Any> DeviceGroup.virtualProperty(
name: String,
initialValue: T,
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): MutableDeviceState<T> {
val state = VirtualDeviceState<T>(converter, initialValue)
return mutableProperty(name, state, descriptorBuilder)
}
/**
* Create a virtual [MutableDeviceState], but do not register it to a device
*/
@Suppress("UnusedReceiverParameter")
public fun <T : Any> DeviceGroup.standAloneProperty(
initialValue: T,
converter: MetaConverter<T>,
): MutableDeviceState<T> = VirtualDeviceState<T>(converter, initialValue)

View File

@ -0,0 +1,123 @@
package space.kscience.controls.constructor
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
/**
* An observable state of a device
*/
public interface DeviceState<T> {
public val converter: MetaConverter<T>
public val value: T
public val valueFlow: Flow<T>
}
public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::objectToMeta)
public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.objectToMeta(value)
/**
* A mutable state of a device
*/
public interface MutableDeviceState<T> : DeviceState<T> {
override var value: T
}
public var <T : Any> MutableDeviceState<T>.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<T>(
override val converter: MetaConverter<T>,
initialValue: T,
) : MutableDeviceState<T> {
private val flow = MutableStateFlow(initialValue)
override val valueFlow: Flow<T> get() = flow
override var value: T by flow::value
}
private open class BoundDeviceState<T>(
override val converter: MetaConverter<T>,
val device: Device,
val propertyName: String,
private val initialValue: T,
) : DeviceState<T> {
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().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 <T> Device.bindStateToProperty(
propertyName: String,
metaConverter: MetaConverter<T>,
): DeviceState<T> {
val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed")
return BoundDeviceState(metaConverter, this, propertyName, initialValue)
}
public suspend fun <D : Device, T> D.bindStateToProperty(
propertySpec: DevicePropertySpec<D, T>,
): DeviceState<T> = bindStateToProperty(propertySpec.name, propertySpec.converter)
public fun <T, R> DeviceState<T>.map(
converter: MetaConverter<R>, mapper: (T) -> R,
): DeviceState<R> = object : DeviceState<R> {
override val converter: MetaConverter<R> = converter
override val value: R
get() = mapper(this@map.value)
override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper)
}
private class MutableBoundDeviceState<T>(
converter: MetaConverter<T>,
device: Device,
propertyName: String,
initialValue: T,
) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> {
override var value: T
get() = valueFlow.value
set(newValue) {
device.launch {
device.writeProperty(propertyName, converter.objectToMeta(newValue))
}
}
}
public suspend fun <T> Device.bindMutableStateToProperty(
propertyName: String,
metaConverter: MetaConverter<T>,
): MutableDeviceState<T> {
val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed")
return MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
}
public suspend fun <D : Device, T> D.bindMutableStateToProperty(
propertySpec: MutableDevicePropertySpec<D, T>,
): MutableDeviceState<T> = bindMutableStateToProperty(propertySpec.name, propertySpec.converter)

View File

@ -0,0 +1,94 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device
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
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<Drive>() {
public val force: MutableDevicePropertySpec<Drive, Double> by Drive.mutableProperty(
MetaConverter.double,
Drive::force
)
public val position: DevicePropertySpec<Drive, Double> by doubleProperty { position }
}
}
/**
* A virtual drive
*/
public class VirtualDrive(
context: Context,
private val mass: Double,
public val positionState: MutableDeviceState<Double>,
) : Drive, DeviceBySpec<Drive>(Drive, context) {
private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds
private val clock = context.clock
override var force: Double = 0.0
override val position: Double get() = positionState.value
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
positionState.value += 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()
}
}
public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = bindMutableStateToProperty(Drive.force)
public fun DeviceGroup.virtualDrive(
name: String,
mass: Double,
positionState: MutableDeviceState<Double>,
): VirtualDrive = device(name, VirtualDrive(context, mass, positionState))

View File

@ -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.api.Device
import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceBySpec
@ -6,6 +6,7 @@ import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.booleanProperty import space.kscience.controls.spec.booleanProperty
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.names.parseAsName
/** /**
@ -25,7 +26,10 @@ public interface LimitSwitch : Device {
*/ */
public class VirtualLimitSwitch( public class VirtualLimitSwitch(
context: Context, context: Context,
private val lockedFunction: () -> Boolean, public val lockedState: DeviceState<Boolean>,
) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch { ) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch {
override val locked: Boolean get() = lockedFunction() override val locked: Boolean get() = lockedState.value
} }
public fun DeviceGroup.virtualLimitSwitch(name: String, lockedState: DeviceState<Boolean>): VirtualLimitSwitch =
device(name.parseAsName(), VirtualLimitSwitch(context, lockedState))

View File

@ -1,4 +1,4 @@
package center.sciprog.controls.devices.misc package space.kscience.controls.constructor
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -6,30 +6,37 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import space.kscience.controls.api.DeviceLifecycleState import space.kscience.controls.manager.clock
import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceBySpec
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
/** /**
* PID controller on top of a [Regulator] * Pid regulator parameters
*/ */
public class PidRegulator( public data class PidParameters(
public val regulator: Regulator,
public val kp: Double, public val kp: Double,
public val ki: Double, public val ki: Double,
public val kd: Double, public val kd: Double,
private val dt: Duration = 0.5.milliseconds, public val timeStep: Duration = 1.milliseconds,
private val clock: Clock = Clock.System, )
) : DeviceBySpec<Regulator>(Regulator, regulator.context), Regulator {
override var target: Double = regulator.target /**
* A drive with PID regulator
*/
public class PidRegulator(
public val drive: Drive,
public val pidParameters: PidParameters,
) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator {
private val clock = drive.context.clock
override var target: Double = drive.position
private var lastTime: Instant = clock.now() private var lastTime: Instant = clock.now()
private var lastRegulatorTarget: Double = target private var lastPosition: Double = target
private var integral: Double = 0.0 private var integral: Double = 0.0
@ -39,25 +46,22 @@ public class PidRegulator(
override suspend fun onStart() { override suspend fun onStart() {
if(regulator.lifecycleState == DeviceLifecycleState.STOPPED){ drive.start()
regulator.start()
}
regulator.start()
updateJob = launch { updateJob = launch {
while (isActive) { while (isActive) {
delay(dt) delay(pidParameters.timeStep)
mutex.withLock { mutex.withLock {
val realTime = clock.now() val realTime = clock.now()
val delta = target - position val delta = target - position
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
integral += delta * dtSeconds integral += delta * dtSeconds
val derivative = (regulator.target - lastRegulatorTarget) / dtSeconds val derivative = (drive.position - lastPosition) / dtSeconds
//set last time and value to new values //set last time and value to new values
lastTime = realTime lastTime = realTime
lastRegulatorTarget = regulator.target lastPosition = drive.position
regulator.target = regulator.position + kp * delta + ki * integral + kd * derivative drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative
} }
} }
} }
@ -67,129 +71,11 @@ public class PidRegulator(
updateJob?.cancel() updateJob?.cancel()
} }
override val position: Double get() = regulator.position override val position: Double get() = drive.position
} }
public fun DeviceGroup.pid(
// name: String,
//interface PidRegulator : Device { drive: Drive,
// /** pidParameters: PidParameters,
// * Proportional coefficient ): PidRegulator = device(name, PidRegulator(drive, pidParameters))
// */
// 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<PidRegulator>() {
// 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>(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<PidRegulator>.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<Device> {
// 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"),
// )
//
// }
//}

View File

@ -1,17 +1,15 @@
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.api.Device
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.MutableDevicePropertySpec
import space.kscience.controls.spec.doubleProperty import space.kscience.controls.spec.doubleProperty
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
/** /**
* A single axis drive * A regulator with target value and current position
*/ */
public interface Regulator : Device { public interface Regulator : Device {
/** /**
@ -25,27 +23,8 @@ public interface Regulator : Device {
public val position: Double public val position: Double
public companion object : DeviceSpec<Regulator>() { public companion object : DeviceSpec<Regulator>() {
public val target: DevicePropertySpec<Regulator, Double> by property(MetaConverter.double, Regulator::target) public val target: MutableDevicePropertySpec<Regulator, Double> by mutableProperty(MetaConverter.double, Regulator::target)
public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { position } public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { position }
} }
} }
/**
* Virtual [Regulator] with speed limit
*/
public class VirtualRegulator(
context: Context,
value: Double,
private val speed: Double,
) : DeviceBySpec<Regulator>(Regulator, context), Regulator {
private var moveJob: Job? = null
override var position: Double = value
private set
override var target: Double = value
}

View File

@ -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<Double>,
) : MutableDeviceState<Double> {
init {
require(initialValue in range) { "Initial value should be in range" }
}
override val converter: MetaConverter<Double> = 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<Double> get() = _valueFlow
/**
* A state showing that the range is on its lower boundary
*/
public val atStartState: DeviceState<Boolean> = map(MetaConverter.boolean) { it == range.start }
/**
* A state showing that the range is on its higher boundary
*/
public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it == range.endInclusive }
}

View File

@ -3,10 +3,8 @@ package space.kscience.controls.api
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.serialization.Serializable
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.info import space.kscience.dataforge.context.info
@ -19,6 +17,7 @@ import space.kscience.dataforge.names.Name
/** /**
* A lifecycle state of a device * A lifecycle state of a device
*/ */
@Serializable
public enum class DeviceLifecycleState { public enum class DeviceLifecycleState {
/** /**
@ -34,7 +33,12 @@ public enum class DeviceLifecycleState {
/** /**
* The Device is closed * The Device is closed
*/ */
STOPPED STOPPED,
/**
* The device encountered irrecoverable error
*/
ERROR
} }
/** /**
@ -67,18 +71,6 @@ public interface Device : ContextAware, CoroutineScope {
*/ */
public suspend fun readProperty(propertyName: String): Meta 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]. * 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. * In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
@ -98,7 +90,8 @@ public interface Device : ContextAware, CoroutineScope {
public suspend fun execute(actionName: String, argument: Meta? = null): Meta? 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 public suspend fun start(): Unit = Unit
@ -118,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. * 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) getProperty(propertyName) ?: readProperty(propertyName)
} else {
readProperty(propertyName)
}
/** /**
* Get a snapshot of the device logical state * Get a snapshot of the device logical state
* *
*/ */
public fun Device.getAllProperties(): Meta = Meta { public fun CachingDevice.getAllProperties(): Meta = Meta {
for (descriptor in propertyDescriptors) { for (descriptor in propertyDescriptors) {
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name)) setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
} }
@ -140,5 +154,11 @@ public fun Device.getAllProperties(): Meta = Meta {
public fun Device.onPropertyChange( public fun Device.onPropertyChange(
scope: CoroutineScope = this, scope: CoroutineScope = this,
callback: suspend PropertyChangedMessage.() -> Unit, callback: suspend PropertyChangedMessage.() -> Unit,
): Job = ): Job = messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(scope)
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(scope)
/**
* A [Flow] of property change messages for specific property.
*/
public fun Device.propertyMessageFlow(propertyName: String): Flow<PropertyChangedMessage> = messageFlow
.filterIsInstance<PropertyChangedMessage>()
.filter { it.property == propertyName }

View File

@ -1,4 +1,4 @@
@file:OptIn(ExperimentalSerializationApi::class) @file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class)
package space.kscience.controls.api package space.kscience.controls.api
@ -71,7 +71,7 @@ public data class PropertyChangedMessage(
@SerialName("property.set") @SerialName("property.set")
public data class PropertySetMessage( public data class PropertySetMessage(
public val property: String, public val property: String,
public val value: Meta?, public val value: Meta,
override val sourceDevice: Name? = null, override val sourceDevice: Name? = null,
override val targetDevice: Name, override val targetDevice: Name,
override val comment: String? = null, override val comment: String? = null,
@ -228,6 +228,21 @@ public data class DeviceErrorMessage(
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) 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() public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta()

View File

@ -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<ClockManager> {
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

View File

@ -17,21 +17,17 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
is PropertyGetMessage -> { is PropertyGetMessage -> {
PropertyChangedMessage( PropertyChangedMessage(
property = request.property, property = request.property,
value = getOrReadProperty(request.property), value = requestProperty(request.property),
sourceDevice = deviceTarget, sourceDevice = deviceTarget,
targetDevice = request.sourceDevice targetDevice = request.sourceDevice
) )
} }
is PropertySetMessage -> { is PropertySetMessage -> {
if (request.value == null) {
invalidate(request.property)
} else {
writeProperty(request.property, request.value) writeProperty(request.property, request.value)
}
PropertyChangedMessage( PropertyChangedMessage(
property = request.property, property = request.property,
value = getOrReadProperty(request.property), value = requestProperty(request.property),
sourceDevice = deviceTarget, sourceDevice = deviceTarget,
targetDevice = request.sourceDevice targetDevice = request.sourceDevice
) )
@ -64,6 +60,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
is DeviceErrorMessage, is DeviceErrorMessage,
is EmptyDeviceMessage, is EmptyDeviceMessage,
is DeviceLogMessage, is DeviceLogMessage,
is DeviceLifeCycleMessage,
-> null -> null
} }
} catch (ex: Exception) { } catch (ex: Exception) {

View File

@ -20,7 +20,7 @@ import kotlin.coroutines.CoroutineContext
* Write a meta [item] to [device] * Write a meta [item] to [device]
*/ */
@OptIn(InternalDeviceAPI::class) @OptIn(InternalDeviceAPI::class)
private suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) { private suspend fun <D : Device, T> MutableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter")) write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
} }
@ -48,7 +48,7 @@ private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta
public abstract class DeviceBase<D : Device>( public abstract class DeviceBase<D : Device>(
final override val context: Context, final override val context: Context,
final override val meta: Meta = Meta.EMPTY, final override val meta: Meta = Meta.EMPTY,
) : Device { ) : CachingDevice {
/** /**
* Collection of property specifications * Collection of property specifications
@ -166,7 +166,7 @@ public abstract class DeviceBase<D : Device>(
propertyChanged(propertyName, value) propertyChanged(propertyName, value)
} }
is WritableDevicePropertySpec -> { is MutableDevicePropertySpec -> {
//if there is a writeable property with a given name, invalidate logical and write physical //if there is a writeable property with a given name, invalidate logical and write physical
invalidate(propertyName) invalidate(propertyName)
property.writeMeta(self, value) property.writeMeta(self, value)
@ -189,8 +189,17 @@ public abstract class DeviceBase<D : Device>(
} }
@DFExperimental @DFExperimental
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
protected set private set(value) {
if (field != value) {
launch {
sharedMessageFlow.emit(
DeviceLifeCycleMessage(value)
)
}
}
field = value
}
protected open suspend fun onStart() { protected open suspend fun onStart() {
@ -198,7 +207,7 @@ public abstract class DeviceBase<D : Device>(
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
final override suspend fun start() { final override suspend fun start() {
if(lifecycleState == DeviceLifecycleState.STOPPED) { if (lifecycleState == DeviceLifecycleState.STOPPED) {
super.start() super.start()
lifecycleState = DeviceLifecycleState.STARTING lifecycleState = DeviceLifecycleState.STARTING
onStart() onStart()

View File

@ -4,10 +4,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.controls.api.ActionDescriptor import space.kscience.controls.api.*
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
@ -44,7 +41,7 @@ public interface DevicePropertySpec<in D, T> {
public val DevicePropertySpec<*, *>.name: String get() = descriptor.name public val DevicePropertySpec<*, *>.name: String get() = descriptor.name
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> { public interface MutableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
/** /**
* Write physical value to a device * Write physical value to a device
*/ */
@ -84,21 +81,20 @@ public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>
public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? = public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? =
readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject) readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject)
public suspend fun <T, D : Device> D.request(propertySpec: DevicePropertySpec<D, T>): T? =
public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? = propertySpec.converter.metaToObject(requestProperty(propertySpec.name))
getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject)
/** /**
* Write typed property state and invalidate logical state * Write typed property state and invalidate logical state
*/ */
public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, value: T) { public suspend fun <T, D : Device> D.write(propertySpec: MutableDevicePropertySpec<D, T>, value: T) {
writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value)) writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
} }
/** /**
* Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope * Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope
*/ */
public operator fun <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Job = launch { public fun <T, D : Device> D.writeAsync(propertySpec: MutableDevicePropertySpec<D, T>, value: T): Job = launch {
write(propertySpec, value) write(propertySpec, value)
} }
@ -151,7 +147,7 @@ public fun <D : Device, T> D.useProperty(
/** /**
* Reset the logical state of a property * Reset the logical state of a property
*/ */
public suspend fun <D : Device> D.invalidate(propertySpec: DevicePropertySpec<D, *>) { public suspend fun <D : CachingDevice> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
invalidate(propertySpec.name) invalidate(propertySpec.name)
} }

View File

@ -72,9 +72,9 @@ public abstract class DeviceSpec<D : Device> {
converter: MetaConverter<T>, converter: MetaConverter<T>,
readWriteProperty: KMutableProperty1<D, T>, readWriteProperty: KMutableProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, MutableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property -> PropertyDelegateProvider { _, property ->
val deviceProperty = object : WritableDevicePropertySpec<D, T> { val deviceProperty = object : MutableDevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
//TODO add the type from converter //TODO add the type from converter
@ -123,10 +123,10 @@ public abstract class DeviceSpec<D : Device> {
name: String? = null, name: String? = null,
read: suspend D.() -> T?, read: suspend D.() -> T?,
write: suspend D.(T) -> Unit, write: suspend D.(T) -> Unit,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> -> PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
val propertyName = name ?: property.name val propertyName = name ?: property.name
val deviceProperty = object : WritableDevicePropertySpec<D, T> { val deviceProperty = object : MutableDevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, mutable = true) override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, mutable = true)
.apply(descriptorBuilder) .apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter override val converter: MetaConverter<T> = converter
@ -138,7 +138,7 @@ public abstract class DeviceSpec<D : Device> {
} }
} }
_properties[propertyName] = deviceProperty _properties[propertyName] = deviceProperty
ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>> { _, _ -> ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>> { _, _ ->
deviceProperty deviceProperty
} }
} }
@ -218,9 +218,9 @@ public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty(
converter: MetaConverter<T>, converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, MutableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property -> PropertyDelegateProvider { _, property ->
val deviceProperty = object : WritableDevicePropertySpec<D, T> { val deviceProperty = object : MutableDevicePropertySpec<D, T> {
val propertyName = name ?: property.name val propertyName = name ?: property.name
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
//TODO add type from converter //TODO add type from converter

View File

@ -1,36 +0,0 @@
package space.kscience.controls.spec
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.Map
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.mapValues
import kotlin.collections.mutableMapOf
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<NameToken, Factory<Device>>()
public fun <D : Device> device(name: String, factory: Factory<Device>) {
childrenFactories[NameToken.parse(name)] = factory
}
}
override val devices: Map<NameToken, Device> = builder.childrenFactories.mapValues { (token, factory) ->
val devicesMeta = meta["devices"]
factory.build(deviceManager.context, devicesMeta?.get(token) ?: Meta.EMPTY)
}
}

View File

@ -97,7 +97,7 @@ public fun <D : Device> DeviceSpec<D>.booleanProperty(
name: String? = null, name: String? = null,
read: suspend D.() -> Boolean?, read: suspend D.() -> Boolean?,
write: suspend D.(Boolean) -> Unit write: suspend D.(Boolean) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Boolean>>> =
mutableProperty( mutableProperty(
MetaConverter.boolean, MetaConverter.boolean,
{ {
@ -117,7 +117,7 @@ public fun <D : Device> DeviceSpec<D>.numberProperty(
name: String? = null, name: String? = null,
read: suspend D.() -> Number, read: suspend D.() -> Number,
write: suspend D.(Number) -> Unit write: suspend D.(Number) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Number>>> =
mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : Device> DeviceSpec<D>.doubleProperty( public fun <D : Device> DeviceSpec<D>.doubleProperty(
@ -125,7 +125,7 @@ public fun <D : Device> DeviceSpec<D>.doubleProperty(
name: String? = null, name: String? = null,
read: suspend D.() -> Double, read: suspend D.() -> Double,
write: suspend D.(Double) -> Unit write: suspend D.(Double) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Double>>> =
mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : Device> DeviceSpec<D>.stringProperty( public fun <D : Device> DeviceSpec<D>.stringProperty(
@ -133,7 +133,7 @@ public fun <D : Device> DeviceSpec<D>.stringProperty(
name: String? = null, name: String? = null,
read: suspend D.() -> String, read: suspend D.() -> String,
write: suspend D.(String) -> Unit write: suspend D.(String) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, String>>> =
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
public fun <D : Device> DeviceSpec<D>.metaProperty( public fun <D : Device> DeviceSpec<D>.metaProperty(
@ -141,5 +141,5 @@ public fun <D : Device> DeviceSpec<D>.metaProperty(
name: String? = null, name: String? = null,
read: suspend D.() -> Meta, read: suspend D.() -> Meta,
write: suspend D.(Meta) -> Unit write: suspend D.(Meta) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Meta>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Meta>>> =
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write) mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)

View File

@ -26,7 +26,7 @@ public class DeviceClient(
private val deviceName: Name, private val deviceName: Name,
incomingFlow: Flow<DeviceMessage>, incomingFlow: Flow<DeviceMessage>,
private val send: suspend (DeviceMessage) -> Unit, private val send: suspend (DeviceMessage) -> Unit,
) : Device { ) : CachingDevice {
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext) override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext)

View File

@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import space.kscience.controls.api.get 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.controls.manager.DeviceManager
import space.kscience.dataforge.context.error import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger import space.kscience.dataforge.context.logger
@ -91,7 +91,7 @@ public fun DeviceManager.launchTangoMagix(
val device = get(payload.device) val device = get(payload.device)
when (payload.action) { when (payload.action) {
TangoAction.read -> { TangoAction.read -> {
val value = device.getOrReadProperty(payload.name) val value = device.requestProperty(payload.name)
respond(request, payload) { requestPayload -> respond(request, payload) { requestPayload ->
requestPayload.copy( requestPayload.copy(
value = value, value = value,
@ -104,7 +104,7 @@ public fun DeviceManager.launchTangoMagix(
device.writeProperty(payload.name, value) device.writeProperty(payload.name, value)
} }
//wait for value to be written and return final state //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 -> respond(request, payload) { requestPayload ->
requestPayload.copy( requestPayload.copy(
value = value, value = value,

View File

@ -6,10 +6,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.*
import space.kscience.controls.spec.WritableDevicePropertySpec
import space.kscience.controls.spec.set
import space.kscience.controls.spec.useProperty
public class DeviceProcessImageBuilder<D : Device> internal constructor( public class DeviceProcessImageBuilder<D : Device> internal constructor(
@ -29,10 +26,10 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
public fun bind( public fun bind(
key: ModbusRegistryKey.Coil, key: ModbusRegistryKey.Coil,
propertySpec: WritableDevicePropertySpec<D, Boolean>, propertySpec: MutableDevicePropertySpec<D, Boolean>,
): ObservableDigitalOut = bind(key) { coil -> ): ObservableDigitalOut = bind(key) { coil ->
coil.addObserver { _, _ -> coil.addObserver { _, _ ->
device[propertySpec] = coil.isSet device.writeAsync(propertySpec, coil.isSet)
} }
device.useProperty(propertySpec) { value -> device.useProperty(propertySpec) { value ->
coil.set(value) coil.set(value)
@ -89,10 +86,10 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
public fun bind( public fun bind(
key: ModbusRegistryKey.HoldingRegister, key: ModbusRegistryKey.HoldingRegister,
propertySpec: WritableDevicePropertySpec<D, Short>, propertySpec: MutableDevicePropertySpec<D, Short>,
): ObservableRegister = bind(key) { register -> ): ObservableRegister = bind(key) { register ->
register.addObserver { _, _ -> register.addObserver { _, _ ->
device[propertySpec] = register.toShort() device.writeAsync(propertySpec, register.toShort())
} }
device.useProperty(propertySpec) { value -> device.useProperty(propertySpec) { value ->
register.setValue(value) register.setValue(value)
@ -121,7 +118,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
/** /**
* Trigger [block] if one of register changes. * Trigger [block] if one of register changes.
*/ */
private fun List<ObservableRegister>.onChange(block: (ByteReadPacket) -> Unit) { private fun List<ObservableRegister>.onChange(block: suspend (ByteReadPacket) -> Unit) {
var ready = false var ready = false
forEach { register -> forEach { register ->
@ -147,7 +144,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
} }
} }
public fun <T> bind(key: ModbusRegistryKey.HoldingRange<T>, propertySpec: WritableDevicePropertySpec<D, T>) { public fun <T> bind(key: ModbusRegistryKey.HoldingRange<T>, propertySpec: MutableDevicePropertySpec<D, T>) {
val registers = List(key.count) { val registers = List(key.count) {
ObservableRegister() ObservableRegister()
} }
@ -157,7 +154,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
} }
registers.onChange { packet -> registers.onChange { packet ->
device[propertySpec] = key.format.readObject(packet) device.write(propertySpec, key.format.readObject(packet))
} }
device.useProperty(propertySpec) { value -> device.useProperty(propertySpec) { value ->

View File

@ -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.Identifiers
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
import space.kscience.controls.api.Device import space.kscience.controls.api.*
import space.kscience.controls.api.DeviceHub
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.onPropertyChange
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer import space.kscience.dataforge.meta.MetaSerializer
@ -31,7 +28,7 @@ import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.plus 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) public suspend fun Device.read(propertyDescriptor: PropertyDescriptor): Meta = readProperty(propertyDescriptor.name)
@ -106,10 +103,12 @@ public class DeviceNameSpace(
setTypeDefinition(Identifiers.BaseDataVariableType) setTypeDefinition(Identifiers.BaseDataVariableType)
}.build() }.build()
// Update initial value, but only if it is cached
if(device is CachingDevice) {
device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let { device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let {
node.value = it node.value = it
} }
}
/** /**
* Subscribe to node value changes * Subscribe to node value changes

View File

@ -0,0 +1,29 @@
plugins {
id("space.kscience.gradle.mpp")
`maven-publish`
}
description = """
Dashboard and visualization extensions for devices
""".trimIndent()
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 {
maturity = space.kscience.gradle.Maturity.PROTOTYPE
}

View File

@ -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<Value>
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<out Number>,
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)
}

View File

@ -4,8 +4,8 @@ package space.kscience.controls.demo.car
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import space.kscience.controls.manager.clock
import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.doRecurring
import space.kscience.controls.spec.read 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<VirtualCar>(IVirtualCar, context, meta), IVirtualCar { open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(IVirtualCar, context, meta), IVirtualCar {
private val clock = context.clock
private val timeScale = 1e-3 private val timeScale = 1e-3
private val mass by meta.double(1000.0) // mass in kilograms private val mass by meta.double(1000.0) // mass in kilograms
@ -57,7 +59,7 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(I
private var timeState: Instant? = null 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 //initialize time if it is not initialized
if (timeState == null) { if (timeState == null) {
timeState = newTime timeState = newTime
@ -102,7 +104,7 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(I
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
override suspend fun onStart() { override suspend fun onStart() {
//initializing the clock //initializing the clock
timeState = Clock.System.now() timeState = clock.now()
//starting regular updates //starting regular updates
doRecurring(100.milliseconds) { doRecurring(100.milliseconds) {
update() update()

View File

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

View File

@ -0,0 +1,6 @@
import space.kscience.visionforge.plotly.PlotlyPlugin
import space.kscience.visionforge.runVisionClient
public fun main(): Unit = runVisionClient {
plugin(PlotlyPlugin)
}

View File

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

View File

@ -0,0 +1,11 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@ -94,7 +94,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
val channel by logicalProperty(MetaConverter.int) val channel by logicalProperty(MetaConverter.int)
val value by doubleProperty(read = { val value by doubleProperty(read = {
readChannelData(get(channel) ?: DEFAULT_CHANNEL) readChannelData(request(channel) ?: DEFAULT_CHANNEL)
}) })
val error by logicalProperty(MetaConverter.string) val error by logicalProperty(MetaConverter.string)

View File

@ -32,7 +32,7 @@ fun <D : Device, T : Any> D.fxProperty(
} }
} }
fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>): Property<T> = fun <D : Device, T : Any> D.fxProperty(spec: MutableDevicePropertySpec<D, T>): Property<T> =
object : ObjectPropertyBase<T>() { object : ObjectPropertyBase<T>() {
override fun getBean(): Any = this override fun getBean(): Any = this
override fun getName(): String = spec.name override fun getName(): String = spec.name
@ -51,7 +51,7 @@ fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>):
onChange { newValue -> onChange { newValue ->
if (newValue != null) { if (newValue != null) {
set(spec, newValue) writeAsync(spec, newValue)
} }
} }
} }

View File

@ -51,6 +51,7 @@ include(
":controls-storage", ":controls-storage",
":controls-storage:controls-xodus", ":controls-storage:controls-xodus",
":controls-constructor", ":controls-constructor",
":controls-vision",
":magix", ":magix",
":magix:magix-api", ":magix:magix-api",
":magix:magix-server", ":magix:magix-server",
@ -68,5 +69,6 @@ include(
":demo:car", ":demo:car",
":demo:motors", ":demo:motors",
":demo:echo", ":demo:echo",
":demo:mks-pdr900" ":demo:mks-pdr900",
":demo:constructor"
) )