implement constructor

This commit is contained in:
Alexander Nozik 2023-10-30 21:35:46 +03:00
parent 1fcdbdc9f4
commit 1414cf5a2f
31 changed files with 770 additions and 151 deletions

View File

@ -7,6 +7,7 @@
- Low-code constructor - Low-code constructor
### Changed ### Changed
- Property caching moved from core `Device` to the `CachingDevice`
### Deprecated ### Deprecated

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

@ -1,6 +1,12 @@
package space.kscience.controls.constructor 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.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
@ -12,14 +18,106 @@ public interface DeviceState<T> {
public val value: T public val value: T
public val valueFlow: Flow<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 * A mutable state of a device
*/ */
public interface MutableDeviceState<T> : DeviceState<T>{ public interface MutableDeviceState<T> : DeviceState<T> {
override var value: 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

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

View File

@ -4,12 +4,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.manager.clock
import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.*
import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.doubleProperty
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
@ -33,7 +30,7 @@ public interface Drive : Device {
public val position: Double public val position: Double
public companion object : DeviceSpec<Drive>() { 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, MetaConverter.double,
Drive::force Drive::force
) )
@ -48,16 +45,15 @@ public interface Drive : Device {
public class VirtualDrive( public class VirtualDrive(
context: Context, context: Context,
private val mass: Double, private val mass: Double,
position: Double, public val positionState: MutableDeviceState<Double>,
) : Drive, DeviceBySpec<Drive>(Drive, context) { ) : Drive, DeviceBySpec<Drive>(Drive, context) {
private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds 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 force: Double = 0.0
override var position: Double = position override val position: Double get() = positionState.value
private set
public var velocity: Double = 0.0 public var velocity: Double = 0.0
private set private set
@ -76,10 +72,10 @@ public class VirtualDrive(
lastTime = realTime lastTime = realTime
// compute new value based on velocity and acceleration from the previous step // 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 // 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))

View File

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

@ -6,25 +6,33 @@ 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.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 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 * A drive with PID regulator
*/ */
public class PidRegulator( public class PidRegulator(
public val drive: Drive, public val drive: Drive,
public val kp: Double, public val pidParameters: PidParameters,
public val ki: Double,
public val kd: Double,
private val dt: Duration = 1.milliseconds,
private val clock: Clock = Clock.System,
) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator { ) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator {
private val clock = drive.context.clock
override var target: Double = drive.position override var target: Double = drive.position
private var lastTime: Instant = clock.now() private var lastTime: Instant = clock.now()
@ -41,7 +49,7 @@ public class PidRegulator(
drive.start() drive.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
@ -53,7 +61,7 @@ public class PidRegulator(
lastTime = realTime lastTime = realTime
lastPosition = drive.position 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 override val position: Double get() = drive.position
} }
public fun DeviceGroup.pid(
name: String,
drive: Drive,
pidParameters: PidParameters,
): PidRegulator = device(name, PidRegulator(drive, pidParameters))

View File

@ -3,6 +3,7 @@ package space.kscience.controls.constructor
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
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.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
@ -22,7 +23,7 @@ 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 }
} }

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,7 @@ 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.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
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
@ -74,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.
@ -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. * 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))
} }
@ -148,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

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

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) { writeProperty(request.property, request.value)
invalidate(request.property)
} else {
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
) )

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,8 @@ public abstract class DeviceBase<D : Device>(
} }
@DFExperimental @DFExperimental
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
protected set(value) { private set(value) {
if (field != value) { if (field != value) {
launch { launch {
sharedMessageFlow.emit( sharedMessageFlow.emit(

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

@ -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,9 +103,11 @@ public class DeviceNameSpace(
setTypeDefinition(Identifiers.BaseDataVariableType) setTypeDefinition(Identifiers.BaseDataVariableType)
}.build() }.build()
// Update initial value, but only if it is cached
device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let { if(device is CachingDevice) {
node.value = it device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let {
node.value = it
}
} }
/** /**

View File

@ -7,14 +7,23 @@ description = """
Dashboard and visualization extensions for devices Dashboard and visualization extensions for devices
""".trimIndent() """.trimIndent()
kscience{ val visionforgeVersion = "0.3.0-dev-10"
kscience {
jvm() jvm()
js() js()
dependencies { dependencies {
api(projects.controlsCore) 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 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

@ -69,5 +69,6 @@ include(
":demo:car", ":demo:car",
":demo:motors", ":demo:motors",
":demo:echo", ":demo:echo",
":demo:mks-pdr900" ":demo:mks-pdr900",
":demo:constructor"
) )