Properties refactoring

This commit is contained in:
Alexander Nozik 2020-09-02 11:59:16 +03:00
parent 0e6363048e
commit 7413bda5a1
11 changed files with 188 additions and 74 deletions

View File

@ -45,6 +45,10 @@ abstract class DeviceBase : Device {
return properties.getOrPut(name, builder) return properties.getOrPut(name, builder)
} }
internal fun registerMutableProperty(name: String, builder: () -> DeviceProperty): DeviceProperty {
return properties.getOrPut(name, builder) as DeviceProperty
}
internal fun registerAction(name: String, builder: () -> Action): Action { internal fun registerAction(name: String, builder: () -> Action): Action {
return actions.getOrPut(name, builder) return actions.getOrPut(name, builder)
} }

View File

@ -9,52 +9,52 @@ import kotlin.time.Duration
/** /**
* Read-only device property * Read-only device property
*/ */
interface ReadOnlyDeviceProperty { public interface ReadOnlyDeviceProperty {
/** /**
* Property name, should be unique in device * Property name, should be unique in device
*/ */
val name: String public val name: String
/** /**
* Property descriptor * Property descriptor
*/ */
val descriptor: PropertyDescriptor public val descriptor: PropertyDescriptor
val scope: CoroutineScope public val scope: CoroutineScope
/** /**
* Erase logical value and force re-read from device on next [read] * Erase logical value and force re-read from device on next [read]
*/ */
suspend fun invalidate() public suspend fun invalidate()
/**
* Directly update property logical value and notify listener without writing it to device
*/
public fun updateLogical(item: MetaItem<*>)
// /**
// * Update property logical value and notify listener without writing it to device
// */
// suspend fun update(item: MetaItem<*>)
//
/** /**
* Get cached value and return null if value is invalid or not initialized * Get cached value and return null if value is invalid or not initialized
*/ */
val value: MetaItem<*>? public val value: MetaItem<*>?
/** /**
* Read value either from cache if cache is valid or directly from physical device. * Read value either from cache if cache is valid or directly from physical device.
* If [force], reread * If [force], reread from physical state even if the logical state is set.
*/ */
suspend fun read(force: Boolean = false): MetaItem<*> public suspend fun read(force: Boolean = false): MetaItem<*>
/** /**
* The [Flow] representing future logical states of the property. * The [Flow] representing future logical states of the property.
* Produces null when the state is invalidated * Produces null when the state is invalidated
*/ */
fun flow(): Flow<MetaItem<*>?> public fun flow(): Flow<MetaItem<*>?>
} }
/** /**
* Launch recurring force re-read job on a property scope with given [duration] between reads. * Launch recurring force re-read job on a property scope with given [duration] between reads.
*/ */
fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch { public fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch {
while (isActive) { while (isActive) {
read(true) read(true)
delay(duration) delay(duration)
@ -64,11 +64,11 @@ fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch {
/** /**
* A writeable device property with non-suspended write * A writeable device property with non-suspended write
*/ */
interface DeviceProperty : ReadOnlyDeviceProperty { public interface DeviceProperty : ReadOnlyDeviceProperty {
override var value: MetaItem<*>? override var value: MetaItem<*>?
/** /**
* Write value to physical device. Invalidates logical value, but does not update it automatically * Write value to physical device. Invalidates logical value, but does not update it automatically
*/ */
suspend fun write(item: MetaItem<*>) public suspend fun write(item: MetaItem<*>)
} }

View File

@ -1,10 +1,8 @@
package hep.dataforge.control.base package hep.dataforge.control.base
import hep.dataforge.control.api.PropertyDescriptor import hep.dataforge.control.api.PropertyDescriptor
import hep.dataforge.meta.Meta import hep.dataforge.meta.*
import hep.dataforge.meta.MetaBuilder import hep.dataforge.values.Null
import hep.dataforge.meta.MetaItem
import hep.dataforge.meta.double
import hep.dataforge.values.Value import hep.dataforge.values.Value
import hep.dataforge.values.asValue import hep.dataforge.values.asValue
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -26,7 +24,7 @@ private fun DeviceBase.propertyChanged(name: String, item: MetaItem<*>?){
* A stand-alone [ReadOnlyDeviceProperty] implementation not directly attached to a device * A stand-alone [ReadOnlyDeviceProperty] implementation not directly attached to a device
*/ */
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
open class IsolatedReadOnlyDeviceProperty( public open class IsolatedReadOnlyDeviceProperty(
override val name: String, override val name: String,
default: MetaItem<*>?, default: MetaItem<*>?,
override val descriptor: PropertyDescriptor, override val descriptor: PropertyDescriptor,
@ -42,7 +40,7 @@ open class IsolatedReadOnlyDeviceProperty(
state.value = null state.value = null
} }
protected fun update(item: MetaItem<*>) { override fun updateLogical(item: MetaItem<*>) {
state.value = item state.value = item
callback(name, item) callback(name, item)
} }
@ -56,7 +54,7 @@ open class IsolatedReadOnlyDeviceProperty(
//TODO add error catching //TODO add error catching
getter(currentValue) getter(currentValue)
} }
update(res) updateLogical(res)
res res
} else { } else {
currentValue currentValue
@ -66,7 +64,7 @@ open class IsolatedReadOnlyDeviceProperty(
override fun flow(): StateFlow<MetaItem<*>?> = state override fun flow(): StateFlow<MetaItem<*>?> = state
} }
fun DeviceBase.readOnlyProperty( public fun DeviceBase.readOnlyProperty(
name: String, name: String,
default: MetaItem<*>?, default: MetaItem<*>?,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
@ -87,9 +85,9 @@ private class ReadOnlyDevicePropertyDelegate<D : DeviceBase>(
val default: MetaItem<*>?, val default: MetaItem<*>?,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (MetaItem<*>?) -> MetaItem<*> private val getter: suspend (MetaItem<*>?) -> MetaItem<*>
) : ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> { ) : ReadOnlyProperty<D, ReadOnlyDeviceProperty> {
override fun getValue(thisRef: D, property: KProperty<*>): IsolatedReadOnlyDeviceProperty { override fun getValue(thisRef: D, property: KProperty<*>): ReadOnlyDeviceProperty {
val name = property.name val name = property.name
return owner.registerProperty(name) { return owner.registerProperty(name) {
@ -102,37 +100,37 @@ private class ReadOnlyDevicePropertyDelegate<D : DeviceBase>(
owner::propertyChanged, owner::propertyChanged,
getter getter
) )
} as IsolatedReadOnlyDeviceProperty }
} }
} }
fun <D : DeviceBase> D.reading( public fun <D : DeviceBase> D.reading(
default: MetaItem<*>? = null, default: MetaItem<*>? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (MetaItem<*>?) -> MetaItem<*> getter: suspend (MetaItem<*>?) -> MetaItem<*>
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate( ): ReadOnlyProperty<D, ReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this, this,
default, default,
descriptorBuilder, descriptorBuilder,
getter getter
) )
fun <D : DeviceBase> D.readingValue( public fun <D : DeviceBase> D.readingValue(
default: Value? = null, default: Value? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Any getter: suspend () -> Any?
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate( ): ReadOnlyProperty<D, ReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this, this,
default?.let { MetaItem.ValueItem(it) }, default?.let { MetaItem.ValueItem(it) },
descriptorBuilder, descriptorBuilder,
getter = { MetaItem.ValueItem(Value.of(getter())) } getter = { MetaItem.ValueItem(Value.of(getter())) }
) )
fun <D : DeviceBase> D.readingNumber( public fun <D : DeviceBase> D.readingNumber(
default: Number? = null, default: Number? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Number getter: suspend () -> Number
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate( ): ReadOnlyProperty<D, ReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this, this,
default?.let { MetaItem.ValueItem(it.asValue()) }, default?.let { MetaItem.ValueItem(it.asValue()) },
descriptorBuilder, descriptorBuilder,
@ -142,11 +140,25 @@ fun <D : DeviceBase> D.readingNumber(
} }
) )
fun <D : DeviceBase> D.readingMeta( public fun <D : DeviceBase> D.readingString(
default: Number? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> String
): ReadOnlyProperty<D, ReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this,
default?.let { MetaItem.ValueItem(it.asValue()) },
descriptorBuilder,
getter = {
val number = getter()
MetaItem.ValueItem(number.asValue())
}
)
public fun <D : DeviceBase> D.readingMeta(
default: Meta? = null, default: Meta? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend MetaBuilder.() -> Unit getter: suspend MetaBuilder.() -> Unit
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate( ): ReadOnlyProperty<D, ReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this, this,
default?.let { MetaItem.NodeItem(it) }, default?.let { MetaItem.NodeItem(it) },
descriptorBuilder, descriptorBuilder,
@ -156,7 +168,7 @@ fun <D : DeviceBase> D.readingMeta(
) )
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class IsolatedDeviceProperty( public class IsolatedDeviceProperty(
name: String, name: String,
default: MetaItem<*>?, default: MetaItem<*>?,
descriptor: PropertyDescriptor, descriptor: PropertyDescriptor,
@ -189,20 +201,20 @@ class IsolatedDeviceProperty(
withContext(scope.coroutineContext) { withContext(scope.coroutineContext) {
//TODO add error catching //TODO add error catching
setter(oldValue, item)?.let { setter(oldValue, item)?.let {
update(it) updateLogical(it)
} }
} }
} }
} }
} }
fun DeviceBase.mutableProperty( public fun DeviceBase.mutableProperty(
name: String, name: String,
default: MetaItem<*>?, default: MetaItem<*>?,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (MetaItem<*>?) -> MetaItem<*>, getter: suspend (MetaItem<*>?) -> MetaItem<*>,
setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>?
): ReadOnlyDeviceProperty = registerProperty(name) { ): DeviceProperty = registerMutableProperty(name) {
IsolatedDeviceProperty( IsolatedDeviceProperty(
name, name,
default, default,
@ -220,11 +232,11 @@ private class DevicePropertyDelegate<D : DeviceBase>(
val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (MetaItem<*>?) -> MetaItem<*>, private val getter: suspend (MetaItem<*>?) -> MetaItem<*>,
private val setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? private val setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>?
) : ReadOnlyProperty<D, IsolatedDeviceProperty> { ) : ReadOnlyProperty<D, DeviceProperty> {
override fun getValue(thisRef: D, property: KProperty<*>): IsolatedDeviceProperty { override fun getValue(thisRef: D, property: KProperty<*>): IsolatedDeviceProperty {
val name = property.name val name = property.name
return owner.registerProperty(name) { return owner.registerMutableProperty(name) {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
IsolatedDeviceProperty( IsolatedDeviceProperty(
name, name,
@ -239,12 +251,12 @@ private class DevicePropertyDelegate<D : DeviceBase>(
} }
} }
fun <D : DeviceBase> D.writing( public fun <D : DeviceBase> D.writing(
default: MetaItem<*>? = null, default: MetaItem<*>? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (MetaItem<*>?) -> MetaItem<*>, getter: suspend (MetaItem<*>?) -> MetaItem<*>,
setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>?
): ReadOnlyProperty<D, IsolatedDeviceProperty> = DevicePropertyDelegate( ): ReadOnlyProperty<D, DeviceProperty> = DevicePropertyDelegate(
this, this,
default, default,
descriptorBuilder, descriptorBuilder,
@ -252,31 +264,31 @@ fun <D : DeviceBase> D.writing(
setter setter
) )
fun <D : DeviceBase> D.writingVirtual( public fun <D : DeviceBase> D.writingVirtual(
default: MetaItem<*>, default: MetaItem<*>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {} descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): ReadOnlyProperty<D, IsolatedDeviceProperty> = writing( ): ReadOnlyProperty<D, DeviceProperty> = writing(
default, default,
descriptorBuilder, descriptorBuilder,
getter = { it ?: default }, getter = { it ?: default },
setter = { _, newItem -> newItem } setter = { _, newItem -> newItem }
) )
fun <D : DeviceBase> D.writingVirtual( public fun <D : DeviceBase> D.writingVirtual(
default: Value, default: Value,
descriptorBuilder: PropertyDescriptor.() -> Unit = {} descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): ReadOnlyProperty<D, IsolatedDeviceProperty> = writing( ): ReadOnlyProperty<D, DeviceProperty> = writing(
MetaItem.ValueItem(default), MetaItem.ValueItem(default),
descriptorBuilder, descriptorBuilder,
getter = { it ?: MetaItem.ValueItem(default) }, getter = { it ?: MetaItem.ValueItem(default) },
setter = { _, newItem -> newItem } setter = { _, newItem -> newItem }
) )
fun <D : DeviceBase> D.writingDouble( public fun <D : DeviceBase> D.writingDouble(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Double) -> Double, getter: suspend (Double) -> Double,
setter: suspend (oldValue: Double?, newValue: Double) -> Double? setter: suspend (oldValue: Double?, newValue: Double) -> Double?
): ReadOnlyProperty<D, IsolatedDeviceProperty> { ): ReadOnlyProperty<D, DeviceProperty> {
val innerGetter: suspend (MetaItem<*>?) -> MetaItem<*> = { val innerGetter: suspend (MetaItem<*>?) -> MetaItem<*> = {
MetaItem.ValueItem(getter(it.double ?: Double.NaN).asValue()) MetaItem.ValueItem(getter(it.double ?: Double.NaN).asValue())
} }
@ -292,4 +304,26 @@ fun <D : DeviceBase> D.writingDouble(
innerGetter, innerGetter,
innerSetter innerSetter
) )
}
public fun <D : DeviceBase> D.writingBoolean(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Boolean?) -> Boolean,
setter: suspend (oldValue: Boolean?, newValue: Boolean) -> Boolean?
): ReadOnlyProperty<D, DeviceProperty> {
val innerGetter: suspend (MetaItem<*>?) -> MetaItem<*> = {
MetaItem.ValueItem(getter(it.boolean).asValue())
}
val innerSetter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? = { oldValue, newValue ->
setter(oldValue.boolean, newValue.boolean?: error("Can't convert $newValue to boolean"))?.asValue()?.asMetaItem()
}
return DevicePropertyDelegate(
this,
MetaItem.ValueItem(Null),
descriptorBuilder,
innerGetter,
innerSetter
)
} }

View File

@ -5,6 +5,7 @@ import hep.dataforge.control.api.DeviceHub
import hep.dataforge.control.api.DeviceListener import hep.dataforge.control.api.DeviceListener
import hep.dataforge.control.api.get import hep.dataforge.control.api.get
import hep.dataforge.control.controllers.DeviceMessage.Companion.PROPERTY_CHANGED_ACTION import hep.dataforge.control.controllers.DeviceMessage.Companion.PROPERTY_CHANGED_ACTION
import hep.dataforge.io.Consumer
import hep.dataforge.io.Envelope import hep.dataforge.io.Envelope
import hep.dataforge.io.Responder import hep.dataforge.io.Responder
import hep.dataforge.io.SimpleEnvelope import hep.dataforge.io.SimpleEnvelope
@ -168,7 +169,7 @@ class DeviceController(
suspend fun DeviceHub.respondMessage(request: DeviceMessage): DeviceMessage { suspend fun DeviceHub.respondMessage(request: DeviceMessage): DeviceMessage {
return try { return try {
val targetName = request.target?.toName() ?: Name.EMPTY val targetName = request.target?.toName() ?: Name.EMPTY
val device = this[targetName] val device = this[targetName] ?: error("The device with name $targetName not found in $this")
DeviceController.respondMessage(device, targetName.toString(), request) DeviceController.respondMessage(device, targetName.toString(), request)
} catch (ex: Exception) { } catch (ex: Exception) {
DeviceMessage.fail { DeviceMessage.fail {

View File

@ -8,6 +8,7 @@ import hep.dataforge.control.api.Device
import hep.dataforge.control.api.DeviceHub import hep.dataforge.control.api.DeviceHub
import hep.dataforge.meta.Meta import hep.dataforge.meta.Meta
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.names.NameToken
import kotlin.reflect.KClass import kotlin.reflect.KClass
class DeviceManager : AbstractPlugin(), DeviceHub { class DeviceManager : AbstractPlugin(), DeviceHub {
@ -16,14 +17,14 @@ class DeviceManager : AbstractPlugin(), DeviceHub {
/** /**
* Actual list of connected devices * Actual list of connected devices
*/ */
private val top = HashMap<Name, Device>() private val top = HashMap<NameToken, Device>()
override val devices: Map<Name, Device> get() = top override val devices: Map<NameToken, Device> get() = top
val controller by lazy { val controller by lazy {
HubController(this, context) HubController(this, context)
} }
fun registerDevice(name: Name, device: Device) { fun registerDevice(name: NameToken, device: Device) {
top[name] = device top[name] = device
} }

View File

@ -1,9 +1,9 @@
package hep.dataforge.control.controllers package hep.dataforge.control.controllers
import hep.dataforge.control.api.Consumer
import hep.dataforge.control.api.DeviceHub import hep.dataforge.control.api.DeviceHub
import hep.dataforge.control.api.DeviceListener import hep.dataforge.control.api.DeviceListener
import hep.dataforge.control.api.get import hep.dataforge.control.api.get
import hep.dataforge.io.Consumer
import hep.dataforge.io.Envelope import hep.dataforge.io.Envelope
import hep.dataforge.io.Responder import hep.dataforge.io.Responder
import hep.dataforge.meta.MetaItem import hep.dataforge.meta.MetaItem
@ -11,6 +11,7 @@ import hep.dataforge.meta.get
import hep.dataforge.meta.string import hep.dataforge.meta.string
import hep.dataforge.meta.wrap import hep.dataforge.meta.wrap
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.names.NameToken
import hep.dataforge.names.toName import hep.dataforge.names.toName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@ -38,7 +39,7 @@ class HubController(
} }
} }
private val listeners: Map<Name, DeviceListener> = hub.devices.mapValues { (name, device) -> private val listeners: Map<NameToken, DeviceListener> = hub.devices.mapValues { (name, device) ->
object : DeviceListener { object : DeviceListener {
override fun propertyChanged(propertyName: String, value: MetaItem<*>?) { override fun propertyChanged(propertyName: String, value: MetaItem<*>?) {
if (value == null) return if (value == null) return
@ -62,7 +63,7 @@ class HubController(
suspend fun respondMessage(message: DeviceMessage): DeviceMessage = try { suspend fun respondMessage(message: DeviceMessage): DeviceMessage = try {
val targetName = message.target?.toName() ?: Name.EMPTY val targetName = message.target?.toName() ?: Name.EMPTY
val device = hub[targetName] val device = hub[targetName] ?: error("The device with name $targetName not found in $hub")
DeviceController.respondMessage(device, targetName.toString(), message) DeviceController.respondMessage(device, targetName.toString(), message)
} catch (ex: Exception) { } catch (ex: Exception) {
DeviceMessage.fail { DeviceMessage.fail {
@ -72,7 +73,7 @@ class HubController(
override suspend fun respond(request: Envelope): Envelope = try { override suspend fun respond(request: Envelope): Envelope = try {
val targetName = request.meta[DeviceMessage.TARGET_KEY].string?.toName() ?: Name.EMPTY val targetName = request.meta[DeviceMessage.TARGET_KEY].string?.toName() ?: Name.EMPTY
val device = hub[targetName] val device = hub[targetName] ?: error("The device with name $targetName not found in $hub")
if (request.data == null) { if (request.data == null) {
DeviceController.respondMessage(device, targetName.toString(), DeviceMessage.wrap(request.meta)).wrap() DeviceController.respondMessage(device, targetName.toString(), DeviceMessage.wrap(request.meta)).wrap()
} else { } else {

View File

@ -115,7 +115,7 @@ public fun Application.deviceModule(
+"Device server dashboard" +"Device server dashboard"
} }
deviceNames.forEach { deviceName -> deviceNames.forEach { deviceName ->
val device = manager[deviceName] val device = manager[deviceName] ?: error("The device with name $deviceName not found in $manager")
div { div {
id = deviceName id = deviceName
h2 { +deviceName } h2 { +deviceName }

View File

@ -25,7 +25,7 @@ class DemoDevice(parentScope: CoroutineScope) : DeviceBase() {
parentScope.coroutineContext + executor.asCoroutineDispatcher() + Job(parentScope.coroutineContext[Job]) parentScope.coroutineContext + executor.asCoroutineDispatcher() + Job(parentScope.coroutineContext[Job])
) )
val timeScale: IsolatedDeviceProperty by writingVirtual(5000.0.asValue()) val timeScale: DeviceProperty by writingVirtual(5000.0.asValue())
var timeScaleValue by timeScale.double() var timeScaleValue by timeScale.double()
val sinScale by writingVirtual(1.0.asValue()) val sinScale by writingVirtual(1.0.asValue())

View File

@ -5,7 +5,7 @@ import hep.dataforge.control.controllers.devices
import hep.dataforge.control.server.startDeviceServer import hep.dataforge.control.server.startDeviceServer
import hep.dataforge.control.server.whenStarted import hep.dataforge.control.server.whenStarted
import hep.dataforge.meta.double import hep.dataforge.meta.double
import hep.dataforge.names.asName import hep.dataforge.names.NameToken
import io.ktor.server.engine.ApplicationEngine import io.ktor.server.engine.ApplicationEngine
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -50,7 +50,7 @@ suspend fun Trace.updateXYFrom(flow: Flow<Iterable<Pair<Double, Double>>>) {
fun startDemoDeviceServer(context: Context, device: DemoDevice): ApplicationEngine { fun startDemoDeviceServer(context: Context, device: DemoDevice): ApplicationEngine {
context.devices.registerDevice("demo".asName(), device) context.devices.registerDevice(NameToken("demo"), device)
val server = context.startDeviceServer(context.devices) val server = context.startDeviceServer(context.devices)
server.whenStarted { server.whenStarted {
plotlyModule("plots").apply { plotlyModule("plots").apply {

View File

@ -5,7 +5,6 @@ plugins {
//TODO to be moved to a separate project //TODO to be moved to a separate project
dependencies { dependencies {
implementation(project(":dataforge-device-core")) implementation(project(":dataforge-device-core"))
} }

View File

@ -1,18 +1,25 @@
package ru.mipt.npm.devices.pimotionmaster package ru.mipt.npm.devices.pimotionmaster
import hep.dataforge.control.base.DeviceBase import hep.dataforge.control.base.*
import hep.dataforge.control.base.DeviceProperty
import hep.dataforge.control.base.writingVirtual
import hep.dataforge.control.ports.Port import hep.dataforge.control.ports.Port
import hep.dataforge.control.ports.PortProxy import hep.dataforge.control.ports.PortProxy
import hep.dataforge.control.ports.send
import hep.dataforge.control.ports.withDelimiter import hep.dataforge.control.ports.withDelimiter
import hep.dataforge.meta.MetaItem import hep.dataforge.meta.MetaItem
import hep.dataforge.values.Null import hep.dataforge.values.Null
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
public class PiMotionMasterDevice(
parentScope: CoroutineScope,
private val portFactory: suspend (MetaItem<*>?) -> Port,
) : DeviceBase() {
class PiMotionMasterDevice(parentScope: CoroutineScope, val portFactory: suspend (MetaItem<*>?) -> Port) : DeviceBase() {
override val scope: CoroutineScope = CoroutineScope( override val scope: CoroutineScope = CoroutineScope(
parentScope.coroutineContext + Job(parentScope.coroutineContext[Job]) parentScope.coroutineContext + Job(parentScope.coroutineContext[Job])
) )
@ -23,14 +30,81 @@ class PiMotionMasterDevice(parentScope: CoroutineScope, val portFactory: suspend
private val connector = PortProxy { portFactory(port.value) } private val connector = PortProxy { portFactory(port.value) }
private suspend fun readPhrase(command: String) { private val mutex = Mutex()
connector.receiving().withDelimiter("\n").first { it.startsWith(command) }
private suspend fun sendCommand(command: String, vararg arguments: String) {
val joinedArguments = if (arguments.isEmpty()) {
""
} else {
arguments.joinToString(prefix = " ", separator = " ", postfix = "")
}
val stringToSend = "$command$joinedArguments\n"
connector.send(stringToSend)
} }
// /**
// val firmwareVersion by reading { * Send a synchronous request and receive a list of lines as a response
// connector.r */
// } private suspend fun request(command: String, vararg arguments: String): List<String> = mutex.withLock {
sendCommand(command, *arguments)
val phrases = connector.receiving().withDelimiter("\n")
return@withLock phrases.takeWhile { it.endsWith(" \n") }.toList() + phrases.first()
}
private suspend fun requestAndParse(command: String, vararg arguments: String): Map<String, String> = buildMap {
request(command, *arguments).forEach { line ->
val (key, value) = line.split("=")
put(key, value)
}
}
/**
* Send a synchronous command
*/
private suspend fun send(command: String, vararg arguments: String) {
mutex.withLock {
sendCommand(command, *arguments)
}
}
public val initialize: Action by action {
send("INI")
}
public val firmwareVersion: ReadOnlyDeviceProperty by readingString {
request("VER?").first()
}
public inner class Axis(public val axisId: String) : DeviceBase() {
override val scope: CoroutineScope get() = this@PiMotionMasterDevice.scope
public val enabled: DeviceProperty by writingBoolean<Axis>(
getter = {
val result = requestAndParse("EAX?", axisId)[axisId]?.toIntOrNull()
?: error("Malformed response. Should include integer value for $axisId")
result != 0
},
setter = { oldValue, newValue ->
val value = if(newValue){
"1"
} else {
"0"
}
send("EAX", axisId, value)
oldValue
}
)
public val halt: Action by action {
send("HLT", axisId)
}
}
init {
//list everything here to ensure it is initialized
initialize
firmwareVersion
}
} }