Compare commits
2 Commits
4f028ccee8
...
1414cf5a2f
Author | SHA1 | Date | |
---|---|---|---|
1414cf5a2f | |||
1fcdbdc9f4 |
@ -3,8 +3,11 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Device lifecycle message
|
||||||
|
- Low-code constructor
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Property caching moved from core `Device` to the `CachingDevice`
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
description = """
|
description = """
|
||||||
A low-code constructor foe composite devices simulation
|
A low-code constructor for composite devices simulation
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
kscience{
|
kscience{
|
||||||
|
@ -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)
|
@ -0,0 +1,123 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.api.PropertyChangedMessage
|
||||||
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
|
import space.kscience.controls.spec.MutableDevicePropertySpec
|
||||||
|
import space.kscience.controls.spec.name
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An observable state of a device
|
||||||
|
*/
|
||||||
|
public interface DeviceState<T> {
|
||||||
|
public val converter: MetaConverter<T>
|
||||||
|
public val value: T
|
||||||
|
|
||||||
|
public val valueFlow: Flow<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::objectToMeta)
|
||||||
|
|
||||||
|
public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.objectToMeta(value)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mutable state of a device
|
||||||
|
*/
|
||||||
|
public interface MutableDeviceState<T> : DeviceState<T> {
|
||||||
|
override var value: T
|
||||||
|
}
|
||||||
|
|
||||||
|
public var <T : Any> MutableDeviceState<T>.valueAsMeta: Meta
|
||||||
|
get() = converter.objectToMeta(value)
|
||||||
|
set(arg) {
|
||||||
|
value = converter.metaToObject(arg) ?: error("Conversion for meta $arg to property type with $converter failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [MutableDeviceState] that does not correspond to a physical state
|
||||||
|
*/
|
||||||
|
public class VirtualDeviceState<T>(
|
||||||
|
override val converter: MetaConverter<T>,
|
||||||
|
initialValue: T,
|
||||||
|
) : MutableDeviceState<T> {
|
||||||
|
private val flow = MutableStateFlow(initialValue)
|
||||||
|
override val valueFlow: Flow<T> get() = flow
|
||||||
|
|
||||||
|
override var value: T by flow::value
|
||||||
|
}
|
||||||
|
|
||||||
|
private open class BoundDeviceState<T>(
|
||||||
|
override val converter: MetaConverter<T>,
|
||||||
|
val device: Device,
|
||||||
|
val propertyName: String,
|
||||||
|
private val initialValue: T,
|
||||||
|
) : DeviceState<T> {
|
||||||
|
|
||||||
|
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
|
||||||
|
it.property == propertyName
|
||||||
|
}.mapNotNull {
|
||||||
|
converter.metaToObject(it.value)
|
||||||
|
}.stateIn(device.context, SharingStarted.Eagerly, initialValue)
|
||||||
|
|
||||||
|
override val value: T get() = valueFlow.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a read-only [DeviceState] to a [Device] property
|
||||||
|
*/
|
||||||
|
public suspend fun <T> Device.bindStateToProperty(
|
||||||
|
propertyName: String,
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
): DeviceState<T> {
|
||||||
|
val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed")
|
||||||
|
return BoundDeviceState(metaConverter, this, propertyName, initialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public suspend fun <D : Device, T> D.bindStateToProperty(
|
||||||
|
propertySpec: DevicePropertySpec<D, T>,
|
||||||
|
): DeviceState<T> = bindStateToProperty(propertySpec.name, propertySpec.converter)
|
||||||
|
|
||||||
|
public fun <T, R> DeviceState<T>.map(
|
||||||
|
converter: MetaConverter<R>, mapper: (T) -> R,
|
||||||
|
): DeviceState<R> = object : DeviceState<R> {
|
||||||
|
override val converter: MetaConverter<R> = converter
|
||||||
|
override val value: R
|
||||||
|
get() = mapper(this@map.value)
|
||||||
|
|
||||||
|
override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MutableBoundDeviceState<T>(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
device: Device,
|
||||||
|
propertyName: String,
|
||||||
|
initialValue: T,
|
||||||
|
) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> {
|
||||||
|
|
||||||
|
override var value: T
|
||||||
|
get() = valueFlow.value
|
||||||
|
set(newValue) {
|
||||||
|
device.launch {
|
||||||
|
device.writeProperty(propertyName, converter.objectToMeta(newValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public suspend fun <T> Device.bindMutableStateToProperty(
|
||||||
|
propertyName: String,
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
): MutableDeviceState<T> {
|
||||||
|
val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed")
|
||||||
|
return MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public suspend fun <D : Device, T> D.bindMutableStateToProperty(
|
||||||
|
propertySpec: MutableDevicePropertySpec<D, T>,
|
||||||
|
): MutableDeviceState<T> = bindMutableStateToProperty(propertySpec.name, propertySpec.converter)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.manager.clock
|
||||||
|
import space.kscience.controls.spec.*
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.meta.double
|
||||||
|
import space.kscience.dataforge.meta.get
|
||||||
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A classic drive regulated by force with encoder
|
||||||
|
*/
|
||||||
|
public interface Drive : Device {
|
||||||
|
/**
|
||||||
|
* Get or set drive force or momentum
|
||||||
|
*/
|
||||||
|
public var force: Double
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current position value
|
||||||
|
*/
|
||||||
|
public val position: Double
|
||||||
|
|
||||||
|
public companion object : DeviceSpec<Drive>() {
|
||||||
|
public val force: MutableDevicePropertySpec<Drive, Double> by Drive.mutableProperty(
|
||||||
|
MetaConverter.double,
|
||||||
|
Drive::force
|
||||||
|
)
|
||||||
|
|
||||||
|
public val position: DevicePropertySpec<Drive, Double> by doubleProperty { position }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A virtual drive
|
||||||
|
*/
|
||||||
|
public class VirtualDrive(
|
||||||
|
context: Context,
|
||||||
|
private val mass: Double,
|
||||||
|
public val positionState: MutableDeviceState<Double>,
|
||||||
|
) : Drive, DeviceBySpec<Drive>(Drive, context) {
|
||||||
|
|
||||||
|
private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds
|
||||||
|
private val clock = context.clock
|
||||||
|
|
||||||
|
override var force: Double = 0.0
|
||||||
|
|
||||||
|
override val position: Double get() = positionState.value
|
||||||
|
|
||||||
|
public var velocity: Double = 0.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var updateJob: Job? = null
|
||||||
|
|
||||||
|
override suspend fun onStart() {
|
||||||
|
updateJob = launch {
|
||||||
|
var lastTime = clock.now()
|
||||||
|
while (isActive) {
|
||||||
|
delay(dt)
|
||||||
|
val realTime = clock.now()
|
||||||
|
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
||||||
|
|
||||||
|
//set last time and value to new values
|
||||||
|
lastTime = realTime
|
||||||
|
|
||||||
|
// compute new value based on velocity and acceleration from the previous step
|
||||||
|
positionState.value += velocity * dtSeconds + force / mass * dtSeconds.pow(2) / 2
|
||||||
|
|
||||||
|
// compute new velocity based on acceleration on the previous step
|
||||||
|
velocity += force / mass * dtSeconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
updateJob?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = bindMutableStateToProperty(Drive.force)
|
||||||
|
|
||||||
|
public fun DeviceGroup.virtualDrive(
|
||||||
|
name: String,
|
||||||
|
mass: Double,
|
||||||
|
positionState: MutableDeviceState<Double>,
|
||||||
|
): VirtualDrive = device(name, VirtualDrive(context, mass, positionState))
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.controls.devices.misc
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.spec.DeviceBySpec
|
import space.kscience.controls.spec.DeviceBySpec
|
||||||
@ -6,6 +6,7 @@ import space.kscience.controls.spec.DevicePropertySpec
|
|||||||
import space.kscience.controls.spec.DeviceSpec
|
import space.kscience.controls.spec.DeviceSpec
|
||||||
import space.kscience.controls.spec.booleanProperty
|
import space.kscience.controls.spec.booleanProperty
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.names.parseAsName
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,7 +26,10 @@ public interface LimitSwitch : Device {
|
|||||||
*/
|
*/
|
||||||
public class VirtualLimitSwitch(
|
public class VirtualLimitSwitch(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val lockedFunction: () -> Boolean,
|
public val lockedState: DeviceState<Boolean>,
|
||||||
) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch {
|
) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch {
|
||||||
override val locked: Boolean get() = lockedFunction()
|
override val locked: Boolean get() = lockedState.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun DeviceGroup.virtualLimitSwitch(name: String, lockedState: DeviceState<Boolean>): VirtualLimitSwitch =
|
||||||
|
device(name.parseAsName(), VirtualLimitSwitch(context, lockedState))
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.controls.devices.misc
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -6,30 +6,37 @@ import kotlinx.coroutines.isActive
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import space.kscience.controls.api.DeviceLifecycleState
|
import space.kscience.controls.manager.clock
|
||||||
import space.kscience.controls.spec.DeviceBySpec
|
import space.kscience.controls.spec.DeviceBySpec
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PID controller on top of a [Regulator]
|
* Pid regulator parameters
|
||||||
*/
|
*/
|
||||||
public class PidRegulator(
|
public data class PidParameters(
|
||||||
public val regulator: Regulator,
|
|
||||||
public val kp: Double,
|
public val kp: Double,
|
||||||
public val ki: Double,
|
public val ki: Double,
|
||||||
public val kd: Double,
|
public val kd: Double,
|
||||||
private val dt: Duration = 0.5.milliseconds,
|
public val timeStep: Duration = 1.milliseconds,
|
||||||
private val clock: Clock = Clock.System,
|
)
|
||||||
) : DeviceBySpec<Regulator>(Regulator, regulator.context), Regulator {
|
|
||||||
|
|
||||||
override var target: Double = regulator.target
|
/**
|
||||||
|
* A drive with PID regulator
|
||||||
|
*/
|
||||||
|
public class PidRegulator(
|
||||||
|
public val drive: Drive,
|
||||||
|
public val pidParameters: PidParameters,
|
||||||
|
) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator {
|
||||||
|
|
||||||
|
private val clock = drive.context.clock
|
||||||
|
|
||||||
|
override var target: Double = drive.position
|
||||||
|
|
||||||
private var lastTime: Instant = clock.now()
|
private var lastTime: Instant = clock.now()
|
||||||
private var lastRegulatorTarget: Double = target
|
private var lastPosition: Double = target
|
||||||
|
|
||||||
private var integral: Double = 0.0
|
private var integral: Double = 0.0
|
||||||
|
|
||||||
@ -39,25 +46,22 @@ public class PidRegulator(
|
|||||||
|
|
||||||
|
|
||||||
override suspend fun onStart() {
|
override suspend fun onStart() {
|
||||||
if(regulator.lifecycleState == DeviceLifecycleState.STOPPED){
|
drive.start()
|
||||||
regulator.start()
|
|
||||||
}
|
|
||||||
regulator.start()
|
|
||||||
updateJob = launch {
|
updateJob = launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
delay(dt)
|
delay(pidParameters.timeStep)
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val realTime = clock.now()
|
val realTime = clock.now()
|
||||||
val delta = target - position
|
val delta = target - position
|
||||||
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
||||||
integral += delta * dtSeconds
|
integral += delta * dtSeconds
|
||||||
val derivative = (regulator.target - lastRegulatorTarget) / dtSeconds
|
val derivative = (drive.position - lastPosition) / dtSeconds
|
||||||
|
|
||||||
//set last time and value to new values
|
//set last time and value to new values
|
||||||
lastTime = realTime
|
lastTime = realTime
|
||||||
lastRegulatorTarget = regulator.target
|
lastPosition = drive.position
|
||||||
|
|
||||||
regulator.target = regulator.position + kp * delta + ki * integral + kd * derivative
|
drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,129 +71,11 @@ public class PidRegulator(
|
|||||||
updateJob?.cancel()
|
updateJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val position: Double get() = regulator.position
|
override val position: Double get() = drive.position
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun DeviceGroup.pid(
|
||||||
//
|
name: String,
|
||||||
//interface PidRegulator : Device {
|
drive: Drive,
|
||||||
// /**
|
pidParameters: PidParameters,
|
||||||
// * Proportional coefficient
|
): PidRegulator = device(name, PidRegulator(drive, pidParameters))
|
||||||
// */
|
|
||||||
// val kp: Double
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Integral coefficient
|
|
||||||
// */
|
|
||||||
// val ki: Double
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Differential coefficient
|
|
||||||
// */
|
|
||||||
// val kd: Double
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * The target value for PID
|
|
||||||
// */
|
|
||||||
// var target: Double
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Read current value
|
|
||||||
// */
|
|
||||||
// suspend fun read(): Double
|
|
||||||
//
|
|
||||||
// companion object : DeviceSpec<PidRegulator>() {
|
|
||||||
// val target by property(MetaConverter.double, PidRegulator::target)
|
|
||||||
// val value by doubleProperty { read() }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
///**
|
|
||||||
// *
|
|
||||||
// */
|
|
||||||
//class VirtualPid(
|
|
||||||
// context: Context,
|
|
||||||
// override val kp: Double,
|
|
||||||
// override val ki: Double,
|
|
||||||
// override val kd: Double,
|
|
||||||
// val mass: Double,
|
|
||||||
// override var target: Double = 0.0,
|
|
||||||
// private val dt: Duration = 0.5.milliseconds,
|
|
||||||
// private val clock: Clock = Clock.System,
|
|
||||||
//) : DeviceBySpec<PidRegulator>(PidRegulator, context), PidRegulator {
|
|
||||||
//
|
|
||||||
// private val mutex = Mutex()
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// private var lastTime: Instant = clock.now()
|
|
||||||
// private var lastValue: Double = target
|
|
||||||
//
|
|
||||||
// private var value: Double = target
|
|
||||||
// private var velocity: Double = 0.0
|
|
||||||
// private var acceleration: Double = 0.0
|
|
||||||
// private var integral: Double = 0.0
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// private var updateJob: Job? = null
|
|
||||||
//
|
|
||||||
// override suspend fun onStart() {
|
|
||||||
// updateJob = launch {
|
|
||||||
// while (isActive) {
|
|
||||||
// delay(dt)
|
|
||||||
// mutex.withLock {
|
|
||||||
// val realTime = clock.now()
|
|
||||||
// val delta = target - value
|
|
||||||
// val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
|
||||||
// integral += delta * dtSeconds
|
|
||||||
// val derivative = (value - lastValue) / dtSeconds
|
|
||||||
//
|
|
||||||
// //set last time and value to new values
|
|
||||||
// lastTime = realTime
|
|
||||||
// lastValue = value
|
|
||||||
//
|
|
||||||
// // compute new value based on velocity and acceleration from the previous step
|
|
||||||
// value += velocity * dtSeconds + acceleration * dtSeconds.pow(2) / 2
|
|
||||||
//
|
|
||||||
// // compute new velocity based on acceleration on the previous step
|
|
||||||
// velocity += acceleration * dtSeconds
|
|
||||||
//
|
|
||||||
// //compute force for the next step based on current values
|
|
||||||
// acceleration = (kp * delta + ki * integral + kd * derivative) / mass
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// check(value.isFinite() && velocity.isFinite()) {
|
|
||||||
// "Value $value is not finite"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onStop() {
|
|
||||||
// updateJob?.cancel()
|
|
||||||
// super<PidRegulator>.stop()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override suspend fun read(): Double = value
|
|
||||||
//
|
|
||||||
// suspend fun readVelocity(): Double = velocity
|
|
||||||
//
|
|
||||||
// suspend fun readAcceleration(): Double = acceleration
|
|
||||||
//
|
|
||||||
// suspend fun write(newTarget: Double) = mutex.withLock {
|
|
||||||
// require(newTarget.isFinite()) { "Value $newTarget is not valid" }
|
|
||||||
// target = newTarget
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// companion object : Factory<Device> {
|
|
||||||
// override fun build(context: Context, meta: Meta) = VirtualPid(
|
|
||||||
// context,
|
|
||||||
// meta["kp"].double ?: error("Kp is not defined"),
|
|
||||||
// meta["ki"].double ?: error("Ki is not defined"),
|
|
||||||
// meta["kd"].double ?: error("Kd is not defined"),
|
|
||||||
// meta["m"].double ?: error("Mass is not defined"),
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
//}
|
|
@ -1,17 +1,15 @@
|
|||||||
package center.sciprog.controls.devices.misc
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.spec.DeviceBySpec
|
|
||||||
import space.kscience.controls.spec.DevicePropertySpec
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
import space.kscience.controls.spec.DeviceSpec
|
import space.kscience.controls.spec.DeviceSpec
|
||||||
|
import space.kscience.controls.spec.MutableDevicePropertySpec
|
||||||
import space.kscience.controls.spec.doubleProperty
|
import space.kscience.controls.spec.doubleProperty
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single axis drive
|
* A regulator with target value and current position
|
||||||
*/
|
*/
|
||||||
public interface Regulator : Device {
|
public interface Regulator : Device {
|
||||||
/**
|
/**
|
||||||
@ -25,27 +23,8 @@ public interface Regulator : Device {
|
|||||||
public val position: Double
|
public val position: Double
|
||||||
|
|
||||||
public companion object : DeviceSpec<Regulator>() {
|
public companion object : DeviceSpec<Regulator>() {
|
||||||
public val target: DevicePropertySpec<Regulator, Double> by property(MetaConverter.double, Regulator::target)
|
public val target: MutableDevicePropertySpec<Regulator, Double> by mutableProperty(MetaConverter.double, Regulator::target)
|
||||||
|
|
||||||
public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { position }
|
public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { position }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Virtual [Regulator] with speed limit
|
|
||||||
*/
|
|
||||||
public class VirtualRegulator(
|
|
||||||
context: Context,
|
|
||||||
value: Double,
|
|
||||||
private val speed: Double,
|
|
||||||
) : DeviceBySpec<Regulator>(Regulator, context), Regulator {
|
|
||||||
|
|
||||||
private var moveJob: Job? = null
|
|
||||||
|
|
||||||
override var position: Double = value
|
|
||||||
private set
|
|
||||||
|
|
||||||
override var target: Double = value
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,41 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state describing a [Double] value in the [range]
|
||||||
|
*/
|
||||||
|
public class DoubleRangeState(
|
||||||
|
initialValue: Double,
|
||||||
|
public val range: ClosedFloatingPointRange<Double>,
|
||||||
|
) : MutableDeviceState<Double> {
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(initialValue in range) { "Initial value should be in range" }
|
||||||
|
}
|
||||||
|
|
||||||
|
override val converter: MetaConverter<Double> = MetaConverter.double
|
||||||
|
|
||||||
|
private val _valueFlow = MutableStateFlow(initialValue)
|
||||||
|
|
||||||
|
override var value: Double
|
||||||
|
get() = _valueFlow.value
|
||||||
|
set(newValue) {
|
||||||
|
_valueFlow.value = newValue.coerceIn(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val valueFlow: StateFlow<Double> get() = _valueFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state showing that the range is on its lower boundary
|
||||||
|
*/
|
||||||
|
public val atStartState: DeviceState<Boolean> = map(MetaConverter.boolean) { it == range.start }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state showing that the range is on its higher boundary
|
||||||
|
*/
|
||||||
|
public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it == range.endInclusive }
|
||||||
|
}
|
@ -3,10 +3,8 @@ package space.kscience.controls.api
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import space.kscience.dataforge.context.ContextAware
|
||||||
import space.kscience.dataforge.context.info
|
import space.kscience.dataforge.context.info
|
||||||
@ -19,6 +17,7 @@ import space.kscience.dataforge.names.Name
|
|||||||
/**
|
/**
|
||||||
* A lifecycle state of a device
|
* A lifecycle state of a device
|
||||||
*/
|
*/
|
||||||
|
@Serializable
|
||||||
public enum class DeviceLifecycleState {
|
public enum class DeviceLifecycleState {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,7 +33,12 @@ public enum class DeviceLifecycleState {
|
|||||||
/**
|
/**
|
||||||
* The Device is closed
|
* The Device is closed
|
||||||
*/
|
*/
|
||||||
STOPPED
|
STOPPED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The device encountered irrecoverable error
|
||||||
|
*/
|
||||||
|
ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,18 +71,6 @@ public interface Device : ContextAware, CoroutineScope {
|
|||||||
*/
|
*/
|
||||||
public suspend fun readProperty(propertyName: String): Meta
|
public suspend fun readProperty(propertyName: String): Meta
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the logical state of property or return null if it is invalid
|
|
||||||
*/
|
|
||||||
public fun getProperty(propertyName: String): Meta?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate property (set logical state to invalid)
|
|
||||||
*
|
|
||||||
* This message is suspended to provide lock-free local property changes (they require coroutine context).
|
|
||||||
*/
|
|
||||||
public suspend fun invalidate(propertyName: String)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set property [value] for a property with name [propertyName].
|
* Set property [value] for a property with name [propertyName].
|
||||||
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
|
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
|
||||||
@ -98,7 +90,8 @@ public interface Device : ContextAware, CoroutineScope {
|
|||||||
public suspend fun execute(actionName: String, argument: Meta? = null): Meta?
|
public suspend fun execute(actionName: String, argument: Meta? = null): Meta?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the device. This function suspends until the device is finished initialization
|
* Initialize the device. This function suspends until the device is finished initialization.
|
||||||
|
* Does nothing if the device is started or is starting
|
||||||
*/
|
*/
|
||||||
public suspend fun start(): Unit = Unit
|
public suspend fun start(): Unit = Unit
|
||||||
|
|
||||||
@ -118,17 +111,38 @@ public interface Device : ContextAware, CoroutineScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device that caches properties values
|
||||||
|
*/
|
||||||
|
public interface CachingDevice : Device {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately (without waiting) get the cached (logical) state of property or return null if it is invalid
|
||||||
|
*/
|
||||||
|
public fun getProperty(propertyName: String): Meta?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate property (set logical state to invalid).
|
||||||
|
*
|
||||||
|
* This message is suspended to provide lock-free local property changes (they require coroutine context).
|
||||||
|
*/
|
||||||
|
public suspend fun invalidate(propertyName: String)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the logical state of property or suspend to read the physical value.
|
* Get the logical state of property or suspend to read the physical value.
|
||||||
*/
|
*/
|
||||||
public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
|
public suspend fun Device.requestProperty(propertyName: String): Meta = if (this is CachingDevice) {
|
||||||
getProperty(propertyName) ?: readProperty(propertyName)
|
getProperty(propertyName) ?: readProperty(propertyName)
|
||||||
|
} else {
|
||||||
|
readProperty(propertyName)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a snapshot of the device logical state
|
* Get a snapshot of the device logical state
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public fun Device.getAllProperties(): Meta = Meta {
|
public fun CachingDevice.getAllProperties(): Meta = Meta {
|
||||||
for (descriptor in propertyDescriptors) {
|
for (descriptor in propertyDescriptors) {
|
||||||
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
|
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
|
||||||
}
|
}
|
||||||
@ -140,5 +154,11 @@ public fun Device.getAllProperties(): Meta = Meta {
|
|||||||
public fun Device.onPropertyChange(
|
public fun Device.onPropertyChange(
|
||||||
scope: CoroutineScope = this,
|
scope: CoroutineScope = this,
|
||||||
callback: suspend PropertyChangedMessage.() -> Unit,
|
callback: suspend PropertyChangedMessage.() -> Unit,
|
||||||
): Job =
|
): Job = messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(scope)
|
||||||
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(scope)
|
|
||||||
|
/**
|
||||||
|
* A [Flow] of property change messages for specific property.
|
||||||
|
*/
|
||||||
|
public fun Device.propertyMessageFlow(propertyName: String): Flow<PropertyChangedMessage> = messageFlow
|
||||||
|
.filterIsInstance<PropertyChangedMessage>()
|
||||||
|
.filter { it.property == propertyName }
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@file:OptIn(ExperimentalSerializationApi::class)
|
@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class)
|
||||||
|
|
||||||
package space.kscience.controls.api
|
package space.kscience.controls.api
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ public data class PropertyChangedMessage(
|
|||||||
@SerialName("property.set")
|
@SerialName("property.set")
|
||||||
public data class PropertySetMessage(
|
public data class PropertySetMessage(
|
||||||
public val property: String,
|
public val property: String,
|
||||||
public val value: Meta?,
|
public val value: Meta,
|
||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name,
|
override val targetDevice: Name,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@ -228,6 +228,21 @@ public data class DeviceErrorMessage(
|
|||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device [Device.lifecycleState] is changed
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
@SerialName("lifecycle")
|
||||||
|
public data class DeviceLifeCycleMessage(
|
||||||
|
val state: DeviceLifecycleState,
|
||||||
|
override val sourceDevice: Name = Name.EMPTY,
|
||||||
|
override val targetDevice: Name? = null,
|
||||||
|
override val comment: String? = null,
|
||||||
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
|
) : DeviceMessage() {
|
||||||
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta()
|
public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta()
|
||||||
|
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
package space.kscience.controls.manager
|
||||||
|
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import space.kscience.dataforge.context.AbstractPlugin
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.PluginFactory
|
||||||
|
import space.kscience.dataforge.context.PluginTag
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
|
||||||
|
public class ClockManager : AbstractPlugin() {
|
||||||
|
override val tag: PluginTag get() = DeviceManager.tag
|
||||||
|
|
||||||
|
public val clock: Clock by lazy {
|
||||||
|
//TODO add clock customization
|
||||||
|
Clock.System
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object : PluginFactory<ClockManager> {
|
||||||
|
override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP)
|
||||||
|
|
||||||
|
override fun build(context: Context, meta: Meta): ClockManager = ClockManager()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public val Context.clock: Clock get() = plugins[ClockManager]?.clock ?: Clock.System
|
@ -17,21 +17,17 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
|||||||
is PropertyGetMessage -> {
|
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
|
||||||
)
|
)
|
||||||
@ -64,6 +60,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
|||||||
is DeviceErrorMessage,
|
is DeviceErrorMessage,
|
||||||
is EmptyDeviceMessage,
|
is EmptyDeviceMessage,
|
||||||
is DeviceLogMessage,
|
is DeviceLogMessage,
|
||||||
|
is DeviceLifeCycleMessage,
|
||||||
-> null
|
-> null
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
|
@ -20,7 +20,7 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
* Write a meta [item] to [device]
|
* Write a meta [item] to [device]
|
||||||
*/
|
*/
|
||||||
@OptIn(InternalDeviceAPI::class)
|
@OptIn(InternalDeviceAPI::class)
|
||||||
private suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
|
private suspend fun <D : Device, T> MutableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
|
||||||
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
|
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta
|
|||||||
public abstract class DeviceBase<D : Device>(
|
public abstract class DeviceBase<D : Device>(
|
||||||
final override val context: Context,
|
final override val context: Context,
|
||||||
final override val meta: Meta = Meta.EMPTY,
|
final override val meta: Meta = Meta.EMPTY,
|
||||||
) : Device {
|
) : CachingDevice {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection of property specifications
|
* Collection of property specifications
|
||||||
@ -166,7 +166,7 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
propertyChanged(propertyName, value)
|
propertyChanged(propertyName, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
is WritableDevicePropertySpec -> {
|
is MutableDevicePropertySpec -> {
|
||||||
//if there is a writeable property with a given name, invalidate logical and write physical
|
//if there is a writeable property with a given name, invalidate logical and write physical
|
||||||
invalidate(propertyName)
|
invalidate(propertyName)
|
||||||
property.writeMeta(self, value)
|
property.writeMeta(self, value)
|
||||||
@ -189,8 +189,17 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@DFExperimental
|
@DFExperimental
|
||||||
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
||||||
protected set
|
private set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
launch {
|
||||||
|
sharedMessageFlow.emit(
|
||||||
|
DeviceLifeCycleMessage(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
protected open suspend fun onStart() {
|
protected open suspend fun onStart() {
|
||||||
|
|
||||||
@ -198,7 +207,7 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
|
|
||||||
@OptIn(DFExperimental::class)
|
@OptIn(DFExperimental::class)
|
||||||
final override suspend fun start() {
|
final override suspend fun start() {
|
||||||
if(lifecycleState == DeviceLifecycleState.STOPPED) {
|
if (lifecycleState == DeviceLifecycleState.STOPPED) {
|
||||||
super.start()
|
super.start()
|
||||||
lifecycleState = DeviceLifecycleState.STARTING
|
lifecycleState = DeviceLifecycleState.STARTING
|
||||||
onStart()
|
onStart()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
package space.kscience.controls.spec
|
|
||||||
|
|
||||||
import space.kscience.controls.api.Device
|
|
||||||
import space.kscience.controls.api.DeviceHub
|
|
||||||
import space.kscience.controls.manager.DeviceManager
|
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.names.NameToken
|
|
||||||
import kotlin.collections.Map
|
|
||||||
import kotlin.collections.component1
|
|
||||||
import kotlin.collections.component2
|
|
||||||
import kotlin.collections.mapValues
|
|
||||||
import kotlin.collections.mutableMapOf
|
|
||||||
import kotlin.collections.set
|
|
||||||
|
|
||||||
|
|
||||||
public class DeviceTree(
|
|
||||||
public val deviceManager: DeviceManager,
|
|
||||||
public val meta: Meta,
|
|
||||||
builder: Builder,
|
|
||||||
) : DeviceHub {
|
|
||||||
public class Builder(public val manager: DeviceManager) {
|
|
||||||
internal val childrenFactories = mutableMapOf<NameToken, Factory<Device>>()
|
|
||||||
|
|
||||||
public fun <D : Device> device(name: String, factory: Factory<Device>) {
|
|
||||||
childrenFactories[NameToken.parse(name)] = factory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val devices: Map<NameToken, Device> = builder.childrenFactories.mapValues { (token, factory) ->
|
|
||||||
val devicesMeta = meta["devices"]
|
|
||||||
factory.build(deviceManager.context, devicesMeta?.get(token) ?: Meta.EMPTY)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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 ->
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
29
controls-vision/build.gradle.kts
Normal file
29
controls-vision/build.gradle.kts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
plugins {
|
||||||
|
id("space.kscience.gradle.mpp")
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
description = """
|
||||||
|
Dashboard and visualization extensions for devices
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val visionforgeVersion = "0.3.0-dev-10"
|
||||||
|
|
||||||
|
kscience {
|
||||||
|
jvm()
|
||||||
|
js()
|
||||||
|
dependencies {
|
||||||
|
api(projects.controlsCore)
|
||||||
|
api(projects.controlsConstructor)
|
||||||
|
api("space.kscience:visionforge-plotly:$visionforgeVersion")
|
||||||
|
api("space.kscience:visionforge-markdown:$visionforgeVersion")
|
||||||
|
}
|
||||||
|
|
||||||
|
jvmMain{
|
||||||
|
api("space.kscience:visionforge-server:$visionforgeVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readme {
|
||||||
|
maturity = space.kscience.gradle.Maturity.PROTOTYPE
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package space.kscience.controls.vision
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.api.propertyMessageFlow
|
||||||
|
import space.kscience.controls.constructor.DeviceState
|
||||||
|
import space.kscience.controls.manager.clock
|
||||||
|
import space.kscience.dataforge.meta.ListValue
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.Null
|
||||||
|
import space.kscience.dataforge.meta.Value
|
||||||
|
import space.kscience.plotly.Plot
|
||||||
|
import space.kscience.plotly.models.Scatter
|
||||||
|
import space.kscience.plotly.models.TraceValues
|
||||||
|
import space.kscience.plotly.scatter
|
||||||
|
|
||||||
|
private var TraceValues.values: List<Value>
|
||||||
|
get() = value?.list ?: emptyList()
|
||||||
|
set(newValues) {
|
||||||
|
value = ListValue(newValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a trace that shows a [Device] property change over time. Show only latest [pointsNumber] .
|
||||||
|
* @return a [Job] that handles the listener
|
||||||
|
*/
|
||||||
|
public fun Plot.plotDeviceProperty(
|
||||||
|
device: Device,
|
||||||
|
propertyName: String,
|
||||||
|
extractValue: Meta.() -> Value = { value ?: Null },
|
||||||
|
pointsNumber: Int = 400,
|
||||||
|
coroutineScope: CoroutineScope = device,
|
||||||
|
configuration: Scatter.() -> Unit = {},
|
||||||
|
): Job = scatter(configuration).run {
|
||||||
|
val clock = device.context.clock
|
||||||
|
device.propertyMessageFlow(propertyName).onEach { message ->
|
||||||
|
x.strings = (x.strings + (message.time ?: clock.now()).toString()).takeLast(pointsNumber)
|
||||||
|
y.values = (y.values + message.value.extractValue()).takeLast(pointsNumber)
|
||||||
|
}.launchIn(coroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun Plot.plotDeviceState(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
state: DeviceState<out Number>,
|
||||||
|
pointsNumber: Int = 400,
|
||||||
|
configuration: Scatter.() -> Unit = {},
|
||||||
|
): Job = scatter(configuration).run {
|
||||||
|
state.valueFlow.onEach {
|
||||||
|
x.strings = (x.strings + Clock.System.now().toString()).takeLast(pointsNumber)
|
||||||
|
y.numbers = (y.numbers + it).takeLast(pointsNumber)
|
||||||
|
}.launchIn(scope)
|
||||||
|
}
|
@ -4,8 +4,8 @@ package space.kscience.controls.demo.car
|
|||||||
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.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()
|
||||||
|
20
demo/constructor/build.gradle.kts
Normal file
20
demo/constructor/build.gradle.kts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
plugins {
|
||||||
|
id("space.kscience.gradle.mpp")
|
||||||
|
application
|
||||||
|
}
|
||||||
|
|
||||||
|
kscience {
|
||||||
|
fullStack("js/constructor.js")
|
||||||
|
useKtor()
|
||||||
|
dependencies {
|
||||||
|
api(projects.controlsVision)
|
||||||
|
}
|
||||||
|
jvmMain {
|
||||||
|
implementation("io.ktor:ktor-server-cio")
|
||||||
|
implementation(spclibs.logback.classic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("space.kscience.controls.demo.constructor.MainKt")
|
||||||
|
}
|
6
demo/constructor/src/jsMain/kotlin/constructorJs.kt
Normal file
6
demo/constructor/src/jsMain/kotlin/constructorJs.kt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import space.kscience.visionforge.plotly.PlotlyPlugin
|
||||||
|
import space.kscience.visionforge.runVisionClient
|
||||||
|
|
||||||
|
public fun main(): Unit = runVisionClient {
|
||||||
|
plugin(PlotlyPlugin)
|
||||||
|
}
|
103
demo/constructor/src/jvmMain/kotlin/Main.kt
Normal file
103
demo/constructor/src/jvmMain/kotlin/Main.kt
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package space.kscience.controls.demo.constructor
|
||||||
|
|
||||||
|
import io.ktor.server.cio.CIO
|
||||||
|
import io.ktor.server.engine.embeddedServer
|
||||||
|
import io.ktor.server.http.content.staticResources
|
||||||
|
import io.ktor.server.routing.routing
|
||||||
|
import space.kscience.controls.api.get
|
||||||
|
import space.kscience.controls.constructor.*
|
||||||
|
import space.kscience.controls.manager.ClockManager
|
||||||
|
import space.kscience.controls.manager.DeviceManager
|
||||||
|
import space.kscience.controls.manager.clock
|
||||||
|
import space.kscience.controls.spec.doRecurring
|
||||||
|
import space.kscience.controls.spec.name
|
||||||
|
import space.kscience.controls.spec.write
|
||||||
|
import space.kscience.controls.vision.plotDeviceProperty
|
||||||
|
import space.kscience.controls.vision.plotDeviceState
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.request
|
||||||
|
import space.kscience.visionforge.VisionManager
|
||||||
|
import space.kscience.visionforge.html.VisionPage
|
||||||
|
import space.kscience.visionforge.plotly.PlotlyPlugin
|
||||||
|
import space.kscience.visionforge.plotly.plotly
|
||||||
|
import space.kscience.visionforge.server.close
|
||||||
|
import space.kscience.visionforge.server.openInBrowser
|
||||||
|
import space.kscience.visionforge.server.visionPage
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
|
@Suppress("ExtractKtorModule")
|
||||||
|
public fun main() {
|
||||||
|
val context = Context {
|
||||||
|
plugin(DeviceManager)
|
||||||
|
plugin(PlotlyPlugin)
|
||||||
|
plugin(ClockManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceManager = context.request(DeviceManager)
|
||||||
|
val visionManager = context.request(VisionManager)
|
||||||
|
|
||||||
|
val state = DoubleRangeState(0.0, -100.0..100.0)
|
||||||
|
|
||||||
|
val pidParameters = PidParameters(
|
||||||
|
kp = 2.5,
|
||||||
|
ki = 0.0,
|
||||||
|
kd = -0.1,
|
||||||
|
timeStep = 0.005.seconds
|
||||||
|
)
|
||||||
|
|
||||||
|
val device = deviceManager.deviceGroup {
|
||||||
|
val drive = virtualDrive("drive", 0.005, state)
|
||||||
|
val pid = pid("pid", drive, pidParameters)
|
||||||
|
virtualLimitSwitch("start", state.atStartState)
|
||||||
|
virtualLimitSwitch("end", state.atEndState)
|
||||||
|
|
||||||
|
val clock = context.clock
|
||||||
|
val clockStart = clock.now()
|
||||||
|
|
||||||
|
doRecurring(10.milliseconds) {
|
||||||
|
val timeFromStart = clock.now() - clockStart
|
||||||
|
val t = timeFromStart.toDouble(DurationUnit.SECONDS)
|
||||||
|
val freq = 0.1
|
||||||
|
val target = 5 * sin(2.0 * PI * freq * t) +
|
||||||
|
sin(2 * PI * 21 * freq * t + 0.1 * (timeFromStart / pidParameters.timeStep))
|
||||||
|
pid.write(Regulator.target, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val server = embeddedServer(CIO, port = 7777) {
|
||||||
|
routing {
|
||||||
|
staticResources("", null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
visionPage(
|
||||||
|
visionManager,
|
||||||
|
VisionPage.scriptHeader("js/constructor.js")
|
||||||
|
) {
|
||||||
|
vision {
|
||||||
|
plotly {
|
||||||
|
plotDeviceState(this@embeddedServer, state){
|
||||||
|
name = "value"
|
||||||
|
}
|
||||||
|
plotDeviceProperty(device["pid"], Regulator.target.name){
|
||||||
|
name = "target"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}.start(false)
|
||||||
|
|
||||||
|
server.openInBrowser()
|
||||||
|
|
||||||
|
|
||||||
|
println("Enter 'exit' to close server")
|
||||||
|
while (readlnOrNull() != "exit") {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
server.close()
|
||||||
|
}
|
11
demo/constructor/src/jvmMain/resources/logback.xml
Normal file
11
demo/constructor/src/jvmMain/resources/logback.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<configuration>
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
@ -94,7 +94,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
|
|||||||
val channel by logicalProperty(MetaConverter.int)
|
val 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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ include(
|
|||||||
":controls-storage",
|
":controls-storage",
|
||||||
":controls-storage:controls-xodus",
|
":controls-storage:controls-xodus",
|
||||||
":controls-constructor",
|
":controls-constructor",
|
||||||
|
":controls-vision",
|
||||||
":magix",
|
":magix",
|
||||||
":magix:magix-api",
|
":magix:magix-api",
|
||||||
":magix:magix-server",
|
":magix:magix-server",
|
||||||
@ -68,5 +69,6 @@ include(
|
|||||||
":demo:car",
|
":demo:car",
|
||||||
":demo:motors",
|
":demo:motors",
|
||||||
":demo:echo",
|
":demo:echo",
|
||||||
":demo:mks-pdr900"
|
":demo:mks-pdr900",
|
||||||
|
":demo:constructor"
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user