implement constructor
This commit is contained in:
parent
1fcdbdc9f4
commit
1414cf5a2f
@ -7,6 +7,7 @@
|
||||
- Low-code constructor
|
||||
|
||||
### Changed
|
||||
- Property caching moved from core `Device` to the `CachingDevice`
|
||||
|
||||
### Deprecated
|
||||
|
||||
|
@ -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)
|
@ -1,6 +1,12 @@
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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
|
||||
|
||||
@ -12,14 +18,106 @@ public interface DeviceState<T> {
|
||||
public val value: T
|
||||
|
||||
public val valueFlow: Flow<T>
|
||||
|
||||
public val metaFlow: Flow<Meta>
|
||||
}
|
||||
|
||||
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>{
|
||||
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)
|
||||
|
||||
|
||||
|
@ -1,33 +0,0 @@
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
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.component1
|
||||
import kotlin.collections.component2
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
@ -4,12 +4,9 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.DeviceBySpec
|
||||
import space.kscience.controls.spec.DevicePropertySpec
|
||||
import space.kscience.controls.spec.DeviceSpec
|
||||
import space.kscience.controls.spec.doubleProperty
|
||||
import space.kscience.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
|
||||
@ -33,7 +30,7 @@ public interface Drive : Device {
|
||||
public val position: Double
|
||||
|
||||
public companion object : DeviceSpec<Drive>() {
|
||||
public val force: DevicePropertySpec<Drive, Double> by Drive.property(
|
||||
public val force: MutableDevicePropertySpec<Drive, Double> by Drive.mutableProperty(
|
||||
MetaConverter.double,
|
||||
Drive::force
|
||||
)
|
||||
@ -48,16 +45,15 @@ public interface Drive : Device {
|
||||
public class VirtualDrive(
|
||||
context: Context,
|
||||
private val mass: Double,
|
||||
position: Double,
|
||||
public val positionState: MutableDeviceState<Double>,
|
||||
) : Drive, DeviceBySpec<Drive>(Drive, context) {
|
||||
|
||||
private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds
|
||||
private val clock = Clock.System
|
||||
private val clock = context.clock
|
||||
|
||||
override var force: Double = 0.0
|
||||
|
||||
override var position: Double = position
|
||||
private set
|
||||
override val position: Double get() = positionState.value
|
||||
|
||||
public var velocity: Double = 0.0
|
||||
private set
|
||||
@ -76,10 +72,10 @@ public class VirtualDrive(
|
||||
lastTime = realTime
|
||||
|
||||
// compute new value based on velocity and acceleration from the previous step
|
||||
position += velocity * dtSeconds + force/mass * dtSeconds.pow(2) / 2
|
||||
positionState.value += velocity * dtSeconds + force / mass * dtSeconds.pow(2) / 2
|
||||
|
||||
// compute new velocity based on acceleration on the previous step
|
||||
velocity += force/mass * dtSeconds
|
||||
velocity += force / mass * dtSeconds
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,3 +85,10 @@ public class VirtualDrive(
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
@ -6,6 +6,7 @@ import space.kscience.controls.spec.DevicePropertySpec
|
||||
import space.kscience.controls.spec.DeviceSpec
|
||||
import space.kscience.controls.spec.booleanProperty
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
|
||||
|
||||
/**
|
||||
@ -25,7 +26,10 @@ public interface LimitSwitch : Device {
|
||||
*/
|
||||
public class VirtualLimitSwitch(
|
||||
context: Context,
|
||||
private val lockedFunction: () -> Boolean,
|
||||
public val lockedState: DeviceState<Boolean>,
|
||||
) : 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))
|
@ -6,25 +6,33 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import space.kscience.controls.manager.clock
|
||||
import space.kscience.controls.spec.DeviceBySpec
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
/**
|
||||
* Pid regulator parameters
|
||||
*/
|
||||
public data class PidParameters(
|
||||
public val kp: Double,
|
||||
public val ki: Double,
|
||||
public val kd: Double,
|
||||
public val timeStep: Duration = 1.milliseconds,
|
||||
)
|
||||
|
||||
/**
|
||||
* A drive with PID regulator
|
||||
*/
|
||||
public class PidRegulator(
|
||||
public val drive: Drive,
|
||||
public val kp: Double,
|
||||
public val ki: Double,
|
||||
public val kd: Double,
|
||||
private val dt: Duration = 1.milliseconds,
|
||||
private val clock: Clock = Clock.System,
|
||||
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()
|
||||
@ -41,7 +49,7 @@ public class PidRegulator(
|
||||
drive.start()
|
||||
updateJob = launch {
|
||||
while (isActive) {
|
||||
delay(dt)
|
||||
delay(pidParameters.timeStep)
|
||||
mutex.withLock {
|
||||
val realTime = clock.now()
|
||||
val delta = target - position
|
||||
@ -53,7 +61,7 @@ public class PidRegulator(
|
||||
lastTime = realTime
|
||||
lastPosition = drive.position
|
||||
|
||||
drive.force = kp * delta + ki * integral + kd * derivative
|
||||
drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,5 +72,10 @@ public class PidRegulator(
|
||||
}
|
||||
|
||||
override val position: Double get() = drive.position
|
||||
|
||||
}
|
||||
|
||||
public fun DeviceGroup.pid(
|
||||
name: String,
|
||||
drive: Drive,
|
||||
pidParameters: PidParameters,
|
||||
): PidRegulator = device(name, PidRegulator(drive, pidParameters))
|
@ -3,6 +3,7 @@ package space.kscience.controls.constructor
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.DevicePropertySpec
|
||||
import space.kscience.controls.spec.DeviceSpec
|
||||
import space.kscience.controls.spec.MutableDevicePropertySpec
|
||||
import space.kscience.controls.spec.doubleProperty
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
|
||||
@ -22,7 +23,7 @@ public interface Regulator : Device {
|
||||
public val position: Double
|
||||
|
||||
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 }
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
@ -3,10 +3,7 @@ package space.kscience.controls.api
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
||||
import space.kscience.dataforge.context.ContextAware
|
||||
@ -74,18 +71,6 @@ public interface Device : ContextAware, CoroutineScope {
|
||||
*/
|
||||
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].
|
||||
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
|
||||
@ -126,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.
|
||||
*/
|
||||
public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
|
||||
public suspend fun Device.requestProperty(propertyName: String): Meta = if (this is CachingDevice) {
|
||||
getProperty(propertyName) ?: readProperty(propertyName)
|
||||
} else {
|
||||
readProperty(propertyName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a snapshot of the device logical state
|
||||
*
|
||||
*/
|
||||
public fun Device.getAllProperties(): Meta = Meta {
|
||||
public fun CachingDevice.getAllProperties(): Meta = Meta {
|
||||
for (descriptor in propertyDescriptors) {
|
||||
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
|
||||
}
|
||||
@ -148,5 +154,11 @@ public fun Device.getAllProperties(): Meta = Meta {
|
||||
public fun Device.onPropertyChange(
|
||||
scope: CoroutineScope = this,
|
||||
callback: suspend PropertyChangedMessage.() -> Unit,
|
||||
): Job =
|
||||
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(scope)
|
||||
): Job = 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 }
|
||||
|
@ -71,7 +71,7 @@ public data class PropertyChangedMessage(
|
||||
@SerialName("property.set")
|
||||
public data class PropertySetMessage(
|
||||
public val property: String,
|
||||
public val value: Meta?,
|
||||
public val value: Meta,
|
||||
override val sourceDevice: Name? = null,
|
||||
override val targetDevice: Name,
|
||||
override val comment: String? = null,
|
||||
|
@ -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
|
@ -17,21 +17,17 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
||||
is PropertyGetMessage -> {
|
||||
PropertyChangedMessage(
|
||||
property = request.property,
|
||||
value = getOrReadProperty(request.property),
|
||||
value = requestProperty(request.property),
|
||||
sourceDevice = deviceTarget,
|
||||
targetDevice = request.sourceDevice
|
||||
)
|
||||
}
|
||||
|
||||
is PropertySetMessage -> {
|
||||
if (request.value == null) {
|
||||
invalidate(request.property)
|
||||
} else {
|
||||
writeProperty(request.property, request.value)
|
||||
}
|
||||
PropertyChangedMessage(
|
||||
property = request.property,
|
||||
value = getOrReadProperty(request.property),
|
||||
value = requestProperty(request.property),
|
||||
sourceDevice = deviceTarget,
|
||||
targetDevice = request.sourceDevice
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ import kotlin.coroutines.CoroutineContext
|
||||
* Write a meta [item] to [device]
|
||||
*/
|
||||
@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"))
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta
|
||||
public abstract class DeviceBase<D : Device>(
|
||||
final override val context: Context,
|
||||
final override val meta: Meta = Meta.EMPTY,
|
||||
) : Device {
|
||||
) : CachingDevice {
|
||||
|
||||
/**
|
||||
* Collection of property specifications
|
||||
@ -166,7 +166,7 @@ public abstract class DeviceBase<D : Device>(
|
||||
propertyChanged(propertyName, value)
|
||||
}
|
||||
|
||||
is WritableDevicePropertySpec -> {
|
||||
is MutableDevicePropertySpec -> {
|
||||
//if there is a writeable property with a given name, invalidate logical and write physical
|
||||
invalidate(propertyName)
|
||||
property.writeMeta(self, value)
|
||||
@ -189,8 +189,8 @@ public abstract class DeviceBase<D : Device>(
|
||||
}
|
||||
|
||||
@DFExperimental
|
||||
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
||||
protected set(value) {
|
||||
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
||||
private set(value) {
|
||||
if (field != value) {
|
||||
launch {
|
||||
sharedMessageFlow.emit(
|
||||
|
@ -4,10 +4,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import space.kscience.controls.api.ActionDescriptor
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.api.PropertyChangedMessage
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.api.*
|
||||
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 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
|
||||
*/
|
||||
@ -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? =
|
||||
readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||
|
||||
|
||||
public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||
getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||
public suspend fun <T, D : Device> D.request(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||
propertySpec.converter.metaToObject(requestProperty(propertySpec.name))
|
||||
|
||||
/**
|
||||
* 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))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
@ -151,7 +147,7 @@ public fun <D : Device, T> D.useProperty(
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
|
@ -72,9 +72,9 @@ public abstract class DeviceSpec<D : Device> {
|
||||
converter: MetaConverter<T>,
|
||||
readWriteProperty: KMutableProperty1<D, T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, MutableDevicePropertySpec<D, T>>> =
|
||||
PropertyDelegateProvider { _, property ->
|
||||
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
||||
val deviceProperty = object : MutableDevicePropertySpec<D, T> {
|
||||
|
||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
|
||||
//TODO add the type from converter
|
||||
@ -123,10 +123,10 @@ public abstract class DeviceSpec<D : Device> {
|
||||
name: String? = null,
|
||||
read: suspend D.() -> T?,
|
||||
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<*> ->
|
||||
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)
|
||||
.apply(descriptorBuilder)
|
||||
override val converter: MetaConverter<T> = converter
|
||||
@ -138,7 +138,7 @@ public abstract class DeviceSpec<D : Device> {
|
||||
}
|
||||
}
|
||||
_properties[propertyName] = deviceProperty
|
||||
ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>> { _, _ ->
|
||||
ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>> { _, _ ->
|
||||
deviceProperty
|
||||
}
|
||||
}
|
||||
@ -218,9 +218,9 @@ public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, MutableDevicePropertySpec<D, T>>> =
|
||||
PropertyDelegateProvider { _, property ->
|
||||
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
||||
val deviceProperty = object : MutableDevicePropertySpec<D, T> {
|
||||
val propertyName = name ?: property.name
|
||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
|
||||
//TODO add type from converter
|
||||
|
@ -97,7 +97,7 @@ public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Boolean?,
|
||||
write: suspend D.(Boolean) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Boolean>>> =
|
||||
mutableProperty(
|
||||
MetaConverter.boolean,
|
||||
{
|
||||
@ -117,7 +117,7 @@ public fun <D : Device> DeviceSpec<D>.numberProperty(
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Number,
|
||||
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)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
||||
@ -125,7 +125,7 @@ public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Double,
|
||||
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)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.stringProperty(
|
||||
@ -133,7 +133,7 @@ public fun <D : Device> DeviceSpec<D>.stringProperty(
|
||||
name: String? = null,
|
||||
read: suspend D.() -> String,
|
||||
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)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.metaProperty(
|
||||
@ -141,5 +141,5 @@ public fun <D : Device> DeviceSpec<D>.metaProperty(
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Meta,
|
||||
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)
|
@ -26,7 +26,7 @@ public class DeviceClient(
|
||||
private val deviceName: Name,
|
||||
incomingFlow: Flow<DeviceMessage>,
|
||||
private val send: suspend (DeviceMessage) -> Unit,
|
||||
) : Device {
|
||||
) : CachingDevice {
|
||||
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext)
|
||||
|
@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
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.dataforge.context.error
|
||||
import space.kscience.dataforge.context.logger
|
||||
@ -91,7 +91,7 @@ public fun DeviceManager.launchTangoMagix(
|
||||
val device = get(payload.device)
|
||||
when (payload.action) {
|
||||
TangoAction.read -> {
|
||||
val value = device.getOrReadProperty(payload.name)
|
||||
val value = device.requestProperty(payload.name)
|
||||
respond(request, payload) { requestPayload ->
|
||||
requestPayload.copy(
|
||||
value = value,
|
||||
@ -104,7 +104,7 @@ public fun DeviceManager.launchTangoMagix(
|
||||
device.writeProperty(payload.name, value)
|
||||
}
|
||||
//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 ->
|
||||
requestPayload.copy(
|
||||
value = value,
|
||||
|
@ -6,10 +6,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.DevicePropertySpec
|
||||
import space.kscience.controls.spec.WritableDevicePropertySpec
|
||||
import space.kscience.controls.spec.set
|
||||
import space.kscience.controls.spec.useProperty
|
||||
import space.kscience.controls.spec.*
|
||||
|
||||
|
||||
public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||
@ -29,10 +26,10 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||
|
||||
public fun bind(
|
||||
key: ModbusRegistryKey.Coil,
|
||||
propertySpec: WritableDevicePropertySpec<D, Boolean>,
|
||||
propertySpec: MutableDevicePropertySpec<D, Boolean>,
|
||||
): ObservableDigitalOut = bind(key) { coil ->
|
||||
coil.addObserver { _, _ ->
|
||||
device[propertySpec] = coil.isSet
|
||||
device.writeAsync(propertySpec, coil.isSet)
|
||||
}
|
||||
device.useProperty(propertySpec) { value ->
|
||||
coil.set(value)
|
||||
@ -89,10 +86,10 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||
|
||||
public fun bind(
|
||||
key: ModbusRegistryKey.HoldingRegister,
|
||||
propertySpec: WritableDevicePropertySpec<D, Short>,
|
||||
propertySpec: MutableDevicePropertySpec<D, Short>,
|
||||
): ObservableRegister = bind(key) { register ->
|
||||
register.addObserver { _, _ ->
|
||||
device[propertySpec] = register.toShort()
|
||||
device.writeAsync(propertySpec, register.toShort())
|
||||
}
|
||||
device.useProperty(propertySpec) { value ->
|
||||
register.setValue(value)
|
||||
@ -121,7 +118,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||
/**
|
||||
* 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
|
||||
|
||||
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) {
|
||||
ObservableRegister()
|
||||
}
|
||||
@ -157,7 +154,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||
}
|
||||
|
||||
registers.onChange { packet ->
|
||||
device[propertySpec] = key.format.readObject(packet)
|
||||
device.write(propertySpec, key.format.readObject(packet))
|
||||
}
|
||||
|
||||
device.useProperty(propertySpec) { value ->
|
||||
|
@ -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.types.builtin.DateTime
|
||||
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.api.DeviceHub
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.api.onPropertyChange
|
||||
import space.kscience.controls.api.*
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.MetaSerializer
|
||||
@ -31,7 +28,7 @@ import space.kscience.dataforge.names.Name
|
||||
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)
|
||||
|
||||
@ -106,10 +103,12 @@ public class DeviceNameSpace(
|
||||
setTypeDefinition(Identifiers.BaseDataVariableType)
|
||||
}.build()
|
||||
|
||||
|
||||
// Update initial value, but only if it is cached
|
||||
if(device is CachingDevice) {
|
||||
device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let {
|
||||
node.value = it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to node value changes
|
||||
|
@ -7,14 +7,23 @@ description = """
|
||||
Dashboard and visualization extensions for devices
|
||||
""".trimIndent()
|
||||
|
||||
kscience{
|
||||
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{
|
||||
readme {
|
||||
maturity = space.kscience.gradle.Maturity.PROTOTYPE
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -4,8 +4,8 @@ package space.kscience.controls.demo.car
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import space.kscience.controls.manager.clock
|
||||
import space.kscience.controls.spec.DeviceBySpec
|
||||
import space.kscience.controls.spec.doRecurring
|
||||
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 {
|
||||
private val clock = context.clock
|
||||
|
||||
private val timeScale = 1e-3
|
||||
|
||||
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 fun update(newTime: Instant = Clock.System.now()) {
|
||||
private fun update(newTime: Instant = clock.now()) {
|
||||
//initialize time if it is not initialized
|
||||
if (timeState == null) {
|
||||
timeState = newTime
|
||||
@ -102,7 +104,7 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(I
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override suspend fun onStart() {
|
||||
//initializing the clock
|
||||
timeState = Clock.System.now()
|
||||
timeState = clock.now()
|
||||
//starting regular updates
|
||||
doRecurring(100.milliseconds) {
|
||||
update()
|
||||
|
20
demo/constructor/build.gradle.kts
Normal file
20
demo/constructor/build.gradle.kts
Normal 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")
|
||||
}
|
6
demo/constructor/src/jsMain/kotlin/constructorJs.kt
Normal file
6
demo/constructor/src/jsMain/kotlin/constructorJs.kt
Normal file
@ -0,0 +1,6 @@
|
||||
import space.kscience.visionforge.plotly.PlotlyPlugin
|
||||
import space.kscience.visionforge.runVisionClient
|
||||
|
||||
public fun main(): Unit = runVisionClient {
|
||||
plugin(PlotlyPlugin)
|
||||
}
|
103
demo/constructor/src/jvmMain/kotlin/Main.kt
Normal file
103
demo/constructor/src/jvmMain/kotlin/Main.kt
Normal 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()
|
||||
}
|
11
demo/constructor/src/jvmMain/resources/logback.xml
Normal file
11
demo/constructor/src/jvmMain/resources/logback.xml
Normal 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>
|
@ -94,7 +94,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
|
||||
val channel by logicalProperty(MetaConverter.int)
|
||||
|
||||
val value by doubleProperty(read = {
|
||||
readChannelData(get(channel) ?: DEFAULT_CHANNEL)
|
||||
readChannelData(request(channel) ?: DEFAULT_CHANNEL)
|
||||
})
|
||||
|
||||
val error by logicalProperty(MetaConverter.string)
|
||||
|
@ -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>() {
|
||||
override fun getBean(): Any = this
|
||||
override fun getName(): String = spec.name
|
||||
@ -51,7 +51,7 @@ fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>):
|
||||
|
||||
onChange { newValue ->
|
||||
if (newValue != null) {
|
||||
set(spec, newValue)
|
||||
writeAsync(spec, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,5 +69,6 @@ include(
|
||||
":demo:car",
|
||||
":demo:motors",
|
||||
":demo:echo",
|
||||
":demo:mks-pdr900"
|
||||
":demo:mks-pdr900",
|
||||
":demo:constructor"
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user