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

View File

@ -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<MetaItem<*>?>
public fun flow(): Flow<MetaItem<*>?>
}
/**
* 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<*>)
}

View File

@ -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<MetaItem<*>?> = state
}
fun DeviceBase.readOnlyProperty(
public fun DeviceBase.readOnlyProperty(
name: String,
default: MetaItem<*>?,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
@ -87,9 +85,9 @@ private class ReadOnlyDevicePropertyDelegate<D : DeviceBase>(
val default: MetaItem<*>?,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
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
return owner.registerProperty(name) {
@ -102,37 +100,37 @@ private class ReadOnlyDevicePropertyDelegate<D : DeviceBase>(
owner::propertyChanged,
getter
)
} as IsolatedReadOnlyDeviceProperty
}
}
}
fun <D : DeviceBase> D.reading(
public fun <D : DeviceBase> D.reading(
default: MetaItem<*>? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (MetaItem<*>?) -> MetaItem<*>
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
): ReadOnlyProperty<D, ReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this,
default,
descriptorBuilder,
getter
)
fun <D : DeviceBase> D.readingValue(
public fun <D : DeviceBase> D.readingValue(
default: Value? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Any
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
getter: suspend () -> Any?
): ReadOnlyProperty<D, ReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this,
default?.let { MetaItem.ValueItem(it) },
descriptorBuilder,
getter = { MetaItem.ValueItem(Value.of(getter())) }
)
fun <D : DeviceBase> D.readingNumber(
public fun <D : DeviceBase> D.readingNumber(
default: Number? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Number
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
): ReadOnlyProperty<D, ReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this,
default?.let { MetaItem.ValueItem(it.asValue()) },
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,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend MetaBuilder.() -> Unit
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
): ReadOnlyProperty<D, ReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this,
default?.let { MetaItem.NodeItem(it) },
descriptorBuilder,
@ -156,7 +168,7 @@ fun <D : DeviceBase> 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<D : DeviceBase>(
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (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 {
val name = property.name
return owner.registerProperty(name) {
return owner.registerMutableProperty(name) {
@OptIn(ExperimentalCoroutinesApi::class)
IsolatedDeviceProperty(
name,
@ -239,12 +251,12 @@ private class DevicePropertyDelegate<D : DeviceBase>(
}
}
fun <D : DeviceBase> D.writing(
public fun <D : DeviceBase> D.writing(
default: MetaItem<*>? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (MetaItem<*>?) -> MetaItem<*>,
setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>?
): ReadOnlyProperty<D, IsolatedDeviceProperty> = DevicePropertyDelegate(
): ReadOnlyProperty<D, DeviceProperty> = DevicePropertyDelegate(
this,
default,
descriptorBuilder,
@ -252,31 +264,31 @@ fun <D : DeviceBase> D.writing(
setter
)
fun <D : DeviceBase> D.writingVirtual(
public fun <D : DeviceBase> D.writingVirtual(
default: MetaItem<*>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): ReadOnlyProperty<D, IsolatedDeviceProperty> = writing(
): ReadOnlyProperty<D, DeviceProperty> = writing(
default,
descriptorBuilder,
getter = { it ?: default },
setter = { _, newItem -> newItem }
)
fun <D : DeviceBase> D.writingVirtual(
public fun <D : DeviceBase> D.writingVirtual(
default: Value,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): ReadOnlyProperty<D, IsolatedDeviceProperty> = writing(
): ReadOnlyProperty<D, DeviceProperty> = writing(
MetaItem.ValueItem(default),
descriptorBuilder,
getter = { it ?: MetaItem.ValueItem(default) },
setter = { _, newItem -> newItem }
)
fun <D : DeviceBase> D.writingDouble(
public fun <D : DeviceBase> D.writingDouble(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Double) -> Double,
setter: suspend (oldValue: Double?, newValue: Double) -> Double?
): ReadOnlyProperty<D, IsolatedDeviceProperty> {
): ReadOnlyProperty<D, DeviceProperty> {
val innerGetter: suspend (MetaItem<*>?) -> MetaItem<*> = {
MetaItem.ValueItem(getter(it.double ?: Double.NaN).asValue())
}
@ -293,3 +305,25 @@ fun <D : DeviceBase> D.writingDouble(
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.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 {

View File

@ -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<Name, Device>()
override val devices: Map<Name, Device> get() = top
private val top = HashMap<NameToken, Device>()
override val devices: Map<NameToken, Device> get() = top
val controller by lazy {
HubController(this, context)
}
fun registerDevice(name: Name, device: Device) {
fun registerDevice(name: NameToken, device: Device) {
top[name] = device
}

View File

@ -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<Name, DeviceListener> = hub.devices.mapValues { (name, device) ->
private val listeners: Map<NameToken, DeviceListener> = 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 {

View File

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

View File

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

View File

@ -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<Iterable<Pair<Double, Double>>>) {
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 {

View File

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

View File

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