From 7413bda5a1831ffad4d96bc85dea1c50216f31de Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 2 Sep 2020 11:59:16 +0300 Subject: [PATCH] Properties refactoring --- .../hep/dataforge/control/base/DeviceBase.kt | 4 + .../dataforge/control/base/DeviceProperty.kt | 34 +++--- .../control/base/IsolatedDeviceProperty.kt | 102 ++++++++++++------ .../control/controllers/DeviceController.kt | 3 +- .../control/controllers/DeviceManager.kt | 7 +- .../control/controllers/HubController.kt | 9 +- .../control/server/deviceWebServer.kt | 2 +- .../hep/dataforge/control/demo/DemoDevice.kt | 2 +- .../control/demo/demoDeviceServer.kt | 4 +- motors/build.gradle.kts | 1 - .../pimotionmaster/PiMotionMasterDevice.kt | 94 ++++++++++++++-- 11 files changed, 188 insertions(+), 74 deletions(-) diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt index 71d4d3a..1647bc9 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt @@ -45,6 +45,10 @@ abstract class DeviceBase : Device { 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 { return actions.getOrPut(name, builder) } diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceProperty.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceProperty.kt index d100b9c..426dee6 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceProperty.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceProperty.kt @@ -9,52 +9,52 @@ import kotlin.time.Duration /** * Read-only device property */ -interface ReadOnlyDeviceProperty { +public interface ReadOnlyDeviceProperty { /** * Property name, should be unique in device */ - val name: String + public val name: String /** * 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] */ - 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 */ - val value: MetaItem<*>? + public val value: MetaItem<*>? /** * 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. * Produces null when the state is invalidated */ - fun flow(): Flow?> + public fun flow(): Flow?> } /** * 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) { read(true) delay(duration) @@ -64,11 +64,11 @@ fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch { /** * A writeable device property with non-suspended write */ -interface DeviceProperty : ReadOnlyDeviceProperty { +public interface DeviceProperty : ReadOnlyDeviceProperty { override var value: MetaItem<*>? /** * 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<*>) } \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt index 9632810..44cbd15 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt @@ -1,10 +1,8 @@ package hep.dataforge.control.base import hep.dataforge.control.api.PropertyDescriptor -import hep.dataforge.meta.Meta -import hep.dataforge.meta.MetaBuilder -import hep.dataforge.meta.MetaItem -import hep.dataforge.meta.double +import hep.dataforge.meta.* +import hep.dataforge.values.Null import hep.dataforge.values.Value import hep.dataforge.values.asValue 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 */ @OptIn(ExperimentalCoroutinesApi::class) -open class IsolatedReadOnlyDeviceProperty( +public open class IsolatedReadOnlyDeviceProperty( override val name: String, default: MetaItem<*>?, override val descriptor: PropertyDescriptor, @@ -42,7 +40,7 @@ open class IsolatedReadOnlyDeviceProperty( state.value = null } - protected fun update(item: MetaItem<*>) { + override fun updateLogical(item: MetaItem<*>) { state.value = item callback(name, item) } @@ -56,7 +54,7 @@ open class IsolatedReadOnlyDeviceProperty( //TODO add error catching getter(currentValue) } - update(res) + updateLogical(res) res } else { currentValue @@ -66,7 +64,7 @@ open class IsolatedReadOnlyDeviceProperty( override fun flow(): StateFlow?> = state } -fun DeviceBase.readOnlyProperty( +public fun DeviceBase.readOnlyProperty( name: String, default: MetaItem<*>?, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, @@ -87,9 +85,9 @@ private class ReadOnlyDevicePropertyDelegate( val default: MetaItem<*>?, val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, private val getter: suspend (MetaItem<*>?) -> MetaItem<*> -) : ReadOnlyProperty { +) : ReadOnlyProperty { - override fun getValue(thisRef: D, property: KProperty<*>): IsolatedReadOnlyDeviceProperty { + override fun getValue(thisRef: D, property: KProperty<*>): ReadOnlyDeviceProperty { val name = property.name return owner.registerProperty(name) { @@ -102,37 +100,37 @@ private class ReadOnlyDevicePropertyDelegate( owner::propertyChanged, getter ) - } as IsolatedReadOnlyDeviceProperty + } } } -fun D.reading( +public fun D.reading( default: MetaItem<*>? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, getter: suspend (MetaItem<*>?) -> MetaItem<*> -): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( +): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( this, default, descriptorBuilder, getter ) -fun D.readingValue( +public fun D.readingValue( default: Value? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend () -> Any -): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( + getter: suspend () -> Any? +): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( this, default?.let { MetaItem.ValueItem(it) }, descriptorBuilder, getter = { MetaItem.ValueItem(Value.of(getter())) } ) -fun D.readingNumber( +public fun D.readingNumber( default: Number? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, getter: suspend () -> Number -): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( +): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( this, default?.let { MetaItem.ValueItem(it.asValue()) }, descriptorBuilder, @@ -142,11 +140,25 @@ fun D.readingNumber( } ) -fun D.readingMeta( +public fun D.readingString( + default: Number? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + getter: suspend () -> String +): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( + this, + default?.let { MetaItem.ValueItem(it.asValue()) }, + descriptorBuilder, + getter = { + val number = getter() + MetaItem.ValueItem(number.asValue()) + } +) + +public fun D.readingMeta( default: Meta? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, getter: suspend MetaBuilder.() -> Unit -): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( +): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( this, default?.let { MetaItem.NodeItem(it) }, descriptorBuilder, @@ -156,7 +168,7 @@ fun D.readingMeta( ) @OptIn(ExperimentalCoroutinesApi::class) -class IsolatedDeviceProperty( +public class IsolatedDeviceProperty( name: String, default: MetaItem<*>?, descriptor: PropertyDescriptor, @@ -189,20 +201,20 @@ class IsolatedDeviceProperty( withContext(scope.coroutineContext) { //TODO add error catching setter(oldValue, item)?.let { - update(it) + updateLogical(it) } } } } } -fun DeviceBase.mutableProperty( +public fun DeviceBase.mutableProperty( name: String, default: MetaItem<*>?, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, getter: suspend (MetaItem<*>?) -> MetaItem<*>, setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? -): ReadOnlyDeviceProperty = registerProperty(name) { +): DeviceProperty = registerMutableProperty(name) { IsolatedDeviceProperty( name, default, @@ -220,11 +232,11 @@ private class DevicePropertyDelegate( val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, private val getter: suspend (MetaItem<*>?) -> MetaItem<*>, private val setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? -) : ReadOnlyProperty { +) : ReadOnlyProperty { override fun getValue(thisRef: D, property: KProperty<*>): IsolatedDeviceProperty { val name = property.name - return owner.registerProperty(name) { + return owner.registerMutableProperty(name) { @OptIn(ExperimentalCoroutinesApi::class) IsolatedDeviceProperty( name, @@ -239,12 +251,12 @@ private class DevicePropertyDelegate( } } -fun D.writing( +public fun D.writing( default: MetaItem<*>? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, getter: suspend (MetaItem<*>?) -> MetaItem<*>, setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? -): ReadOnlyProperty = DevicePropertyDelegate( +): ReadOnlyProperty = DevicePropertyDelegate( this, default, descriptorBuilder, @@ -252,31 +264,31 @@ fun D.writing( setter ) -fun D.writingVirtual( +public fun D.writingVirtual( default: MetaItem<*>, descriptorBuilder: PropertyDescriptor.() -> Unit = {} -): ReadOnlyProperty = writing( +): ReadOnlyProperty = writing( default, descriptorBuilder, getter = { it ?: default }, setter = { _, newItem -> newItem } ) -fun D.writingVirtual( +public fun D.writingVirtual( default: Value, descriptorBuilder: PropertyDescriptor.() -> Unit = {} -): ReadOnlyProperty = writing( +): ReadOnlyProperty = writing( MetaItem.ValueItem(default), descriptorBuilder, getter = { it ?: MetaItem.ValueItem(default) }, setter = { _, newItem -> newItem } ) -fun D.writingDouble( +public fun D.writingDouble( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, getter: suspend (Double) -> Double, setter: suspend (oldValue: Double?, newValue: Double) -> Double? -): ReadOnlyProperty { +): ReadOnlyProperty { val innerGetter: suspend (MetaItem<*>?) -> MetaItem<*> = { MetaItem.ValueItem(getter(it.double ?: Double.NaN).asValue()) } @@ -292,4 +304,26 @@ fun D.writingDouble( innerGetter, innerSetter ) +} + +public fun D.writingBoolean( + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + getter: suspend (Boolean?) -> Boolean, + setter: suspend (oldValue: Boolean?, newValue: Boolean) -> Boolean? +): ReadOnlyProperty { + 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 + ) } \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt index 61ce52a..6e4465f 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt @@ -5,6 +5,7 @@ import hep.dataforge.control.api.DeviceHub import hep.dataforge.control.api.DeviceListener import hep.dataforge.control.api.get import hep.dataforge.control.controllers.DeviceMessage.Companion.PROPERTY_CHANGED_ACTION +import hep.dataforge.io.Consumer import hep.dataforge.io.Envelope import hep.dataforge.io.Responder import hep.dataforge.io.SimpleEnvelope @@ -168,7 +169,7 @@ class DeviceController( suspend fun DeviceHub.respondMessage(request: DeviceMessage): DeviceMessage { return try { 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) } catch (ex: Exception) { DeviceMessage.fail { diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt index d0186d9..3a650d5 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt @@ -8,6 +8,7 @@ import hep.dataforge.control.api.Device import hep.dataforge.control.api.DeviceHub import hep.dataforge.meta.Meta import hep.dataforge.names.Name +import hep.dataforge.names.NameToken import kotlin.reflect.KClass class DeviceManager : AbstractPlugin(), DeviceHub { @@ -16,14 +17,14 @@ class DeviceManager : AbstractPlugin(), DeviceHub { /** * Actual list of connected devices */ - private val top = HashMap() - override val devices: Map get() = top + private val top = HashMap() + override val devices: Map get() = top val controller by lazy { HubController(this, context) } - fun registerDevice(name: Name, device: Device) { + fun registerDevice(name: NameToken, device: Device) { top[name] = device } diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/HubController.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/HubController.kt index 4e67adf..183ea5b 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/HubController.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/HubController.kt @@ -1,9 +1,9 @@ package hep.dataforge.control.controllers -import hep.dataforge.control.api.Consumer import hep.dataforge.control.api.DeviceHub import hep.dataforge.control.api.DeviceListener import hep.dataforge.control.api.get +import hep.dataforge.io.Consumer import hep.dataforge.io.Envelope import hep.dataforge.io.Responder import hep.dataforge.meta.MetaItem @@ -11,6 +11,7 @@ import hep.dataforge.meta.get import hep.dataforge.meta.string import hep.dataforge.meta.wrap import hep.dataforge.names.Name +import hep.dataforge.names.NameToken import hep.dataforge.names.toName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel @@ -38,7 +39,7 @@ class HubController( } } - private val listeners: Map = hub.devices.mapValues { (name, device) -> + private val listeners: Map = hub.devices.mapValues { (name, device) -> object : DeviceListener { override fun propertyChanged(propertyName: String, value: MetaItem<*>?) { if (value == null) return @@ -62,7 +63,7 @@ class HubController( suspend fun respondMessage(message: DeviceMessage): DeviceMessage = try { 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) } catch (ex: Exception) { DeviceMessage.fail { @@ -72,7 +73,7 @@ class HubController( override suspend fun respond(request: Envelope): Envelope = try { 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) { DeviceController.respondMessage(device, targetName.toString(), DeviceMessage.wrap(request.meta)).wrap() } else { diff --git a/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/deviceWebServer.kt b/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/deviceWebServer.kt index bfe2bd6..1ddb1f2 100644 --- a/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/deviceWebServer.kt +++ b/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/deviceWebServer.kt @@ -115,7 +115,7 @@ public fun Application.deviceModule( +"Device server dashboard" } deviceNames.forEach { deviceName -> - val device = manager[deviceName] + val device = manager[deviceName] ?: error("The device with name $deviceName not found in $manager") div { id = deviceName h2 { +deviceName } diff --git a/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt b/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt index 544c065..7ff95d3 100644 --- a/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt +++ b/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt @@ -25,7 +25,7 @@ class DemoDevice(parentScope: CoroutineScope) : DeviceBase() { 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() val sinScale by writingVirtual(1.0.asValue()) diff --git a/demo/src/main/kotlin/hep/dataforge/control/demo/demoDeviceServer.kt b/demo/src/main/kotlin/hep/dataforge/control/demo/demoDeviceServer.kt index 0f5ecc0..58ac070 100644 --- a/demo/src/main/kotlin/hep/dataforge/control/demo/demoDeviceServer.kt +++ b/demo/src/main/kotlin/hep/dataforge/control/demo/demoDeviceServer.kt @@ -5,7 +5,7 @@ import hep.dataforge.control.controllers.devices import hep.dataforge.control.server.startDeviceServer import hep.dataforge.control.server.whenStarted import hep.dataforge.meta.double -import hep.dataforge.names.asName +import hep.dataforge.names.NameToken import io.ktor.server.engine.ApplicationEngine import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -50,7 +50,7 @@ suspend fun Trace.updateXYFrom(flow: Flow>>) { 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) server.whenStarted { plotlyModule("plots").apply { diff --git a/motors/build.gradle.kts b/motors/build.gradle.kts index 97317c4..cb0dbd8 100644 --- a/motors/build.gradle.kts +++ b/motors/build.gradle.kts @@ -5,7 +5,6 @@ plugins { //TODO to be moved to a separate project - dependencies { implementation(project(":dataforge-device-core")) } diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt index 6ae69da..56998ea 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -1,18 +1,25 @@ package ru.mipt.npm.devices.pimotionmaster -import hep.dataforge.control.base.DeviceBase -import hep.dataforge.control.base.DeviceProperty -import hep.dataforge.control.base.writingVirtual +import hep.dataforge.control.base.* import hep.dataforge.control.ports.Port import hep.dataforge.control.ports.PortProxy +import hep.dataforge.control.ports.send import hep.dataforge.control.ports.withDelimiter import hep.dataforge.meta.MetaItem import hep.dataforge.values.Null import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job 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( 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 suspend fun readPhrase(command: String) { - connector.receiving().withDelimiter("\n").first { it.startsWith(command) } + private val mutex = Mutex() + + 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 { -// connector.r -// } + /** + * Send a synchronous request and receive a list of lines as a response + */ + private suspend fun request(command: String, vararg arguments: String): List = 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 = 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( + 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 + + } } \ No newline at end of file