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
### Changed
- Property caching moved from core `Device` to the `CachingDevice`
### 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
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,10 +18,12 @@ 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
@ -23,3 +31,93 @@ public interface 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)

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.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,7 +72,7 @@ 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
@ -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.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))

View File

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

View File

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

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

View File

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

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 -> {
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
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -7,11 +7,20 @@ description = """
Dashboard and visualization extensions for devices
""".trimIndent()
val visionforgeVersion = "0.3.0-dev-10"
kscience {
jvm()
js()
dependencies {
api(projects.controlsCore)
api(projects.controlsConstructor)
api("space.kscience:visionforge-plotly:$visionforgeVersion")
api("space.kscience:visionforge-markdown:$visionforgeVersion")
}
jvmMain{
api("space.kscience:visionforge-server:$visionforgeVersion")
}
}

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.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()

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 value by doubleProperty(read = {
readChannelData(get(channel) ?: DEFAULT_CHANNEL)
readChannelData(request(channel) ?: DEFAULT_CHANNEL)
})
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>() {
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)
}
}
}

View File

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