Cleanup DeviceWithSpec API. Now all public things are in Spec

This commit is contained in:
Alexander Nozik 2021-08-04 14:14:05 +03:00
parent d503f0499e
commit 3aa00ec491
14 changed files with 127 additions and 113 deletions

View File

@ -47,7 +47,7 @@ public interface Device : Closeable, ContextAware, CoroutineScope {
* 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.
*/ */
public suspend fun writeItem(propertyName: String, value: Meta) public suspend fun writeProperty(propertyName: String, value: Meta)
/** /**
* A subscription-based [Flow] of [DeviceMessage] provided by device. The flow is guaranteed to be readable * A subscription-based [Flow] of [DeviceMessage] provided by device. The flow is guaranteed to be readable

View File

@ -59,8 +59,8 @@ public operator fun DeviceHub.get(nameString: String): Device =
public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta = public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta =
this[deviceName].readProperty(propertyName) this[deviceName].readProperty(propertyName)
public suspend fun DeviceHub.writeItem(deviceName: Name, propertyName: String, value: Meta) { public suspend fun DeviceHub.writeProperty(deviceName: Name, propertyName: String, value: Meta) {
this[deviceName].writeItem(propertyName, value) this[deviceName].writeProperty(propertyName, value)
} }
public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: Meta?): Meta? = public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: Meta?): Meta? =

View File

@ -164,7 +164,7 @@ public abstract class DeviceBase(final override val context: Context) : Device {
(_properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate() (_properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate()
} }
override suspend fun writeItem(propertyName: String, value: Meta) { override suspend fun writeProperty(propertyName: String, value: Meta) {
(_properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write( (_properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write(
value value
) )

View File

@ -25,7 +25,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
if (request.value == null) { if (request.value == null) {
invalidate(request.property) invalidate(request.property)
} else { } else {
writeItem(request.property, request.value) writeProperty(request.property, request.value)
} }
PropertyChangedMessage( PropertyChangedMessage(
property = request.property, property = request.property,

View File

@ -11,11 +11,7 @@ import ru.mipt.npm.controls.api.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.properties.Delegates.observable
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/** /**
* A device generated from specification * A device generated from specification
@ -58,7 +54,7 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
private val stateLock = Mutex() private val stateLock = Mutex()
private suspend fun updateLogical(propertyName: String, value: Meta?) { protected suspend fun updateLogical(propertyName: String, value: Meta?) {
if (value != logicalState[propertyName]) { if (value != logicalState[propertyName]) {
stateLock.withLock { stateLock.withLock {
logicalState[propertyName] = value logicalState[propertyName] = value
@ -74,7 +70,7 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
* The logical state is updated after read * The logical state is updated after read
*/ */
override suspend fun readProperty(propertyName: String): Meta { override suspend fun readProperty(propertyName: String): Meta {
val newValue = properties[propertyName]?.readItem(self) val newValue = properties[propertyName]?.readMeta(self)
?: error("A property with name $propertyName is not registered in $this") ?: error("A property with name $propertyName is not registered in $this")
updateLogical(propertyName, newValue) updateLogical(propertyName, newValue)
return newValue return newValue
@ -88,10 +84,10 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
} }
} }
override suspend fun writeItem(propertyName: String, value: Meta): Unit { override suspend fun writeProperty(propertyName: String, value: Meta): Unit {
//If there is a physical property with given name, invalidate logical property and write physical one //If there is a physical property with given name, invalidate logical property and write physical one
(properties[propertyName] as? WritableDevicePropertySpec<D, out Any>)?.let { (properties[propertyName] as? WritableDevicePropertySpec<D, out Any?>)?.let {
it.writeItem(self, value) it.writeMeta(self, value)
invalidate(propertyName) invalidate(propertyName)
} ?: run { } ?: run {
updateLogical(propertyName, value) updateLogical(propertyName, value)
@ -99,39 +95,23 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
} }
override suspend fun execute(action: String, argument: Meta?): Meta? = override suspend fun execute(action: String, argument: Meta?): Meta? =
actions[action]?.executeItem(self, argument) actions[action]?.executeMeta(self, argument)
/**
* A delegate that represents the logical-only state of the device
*/
public fun <T : Any> state(
converter: MetaConverter<T>,
initialValue: T,
): ReadWriteProperty<D, T> = observable(initialValue) { property: KProperty<*>, oldValue: T, newValue: T ->
if (oldValue != newValue) {
launch {
invalidate(property.name)
sharedMessageFlow.emit(PropertyChangedMessage(property.name, converter.objectToMeta(newValue)))
}
}
}
/** /**
* Read typed value and update/push event if needed * Read typed value and update/push event if needed
*/ */
public suspend fun <T : Any> DevicePropertySpec<D, T>.read(): T { public suspend fun <T> DevicePropertySpec<D, T>.read(): T {
val res = read(self) val res = read(self)
updateLogical(name, converter.objectToMeta(res)) updateLogical(name, converter.objectToMeta(res))
return res return res
} }
public fun <T : Any> DevicePropertySpec<D, T>.get(): T? = getProperty(name)?.let(converter::metaToObject) public fun <T> DevicePropertySpec<D, T>.get(): T? = getProperty(name)?.let(converter::metaToObject)
/** /**
* Write typed property state and invalidate logical state * Write typed property state and invalidate logical state
*/ */
public suspend fun <T : Any> WritableDevicePropertySpec<D, T>.write(value: T) { public suspend fun <T> WritableDevicePropertySpec<D, T>.write(value: T) {
write(self, value) write(self, value)
invalidate(name) invalidate(name)
} }
@ -146,7 +126,7 @@ public suspend fun <D : DeviceBySpec<D>, T : Any> D.read(
propertySpec: DevicePropertySpec<D, T> propertySpec: DevicePropertySpec<D, T>
): T = propertySpec.read() ): T = propertySpec.read()
public fun <D : DeviceBySpec<D>, T : Any> D.write( public fun <D : DeviceBySpec<D>, T> D.write(
propertySpec: WritableDevicePropertySpec<D, T>, propertySpec: WritableDevicePropertySpec<D, T>,
value: T value: T
): Job = launch { ): Job = launch {

View File

@ -5,8 +5,6 @@ import ru.mipt.npm.controls.api.Device
import ru.mipt.npm.controls.api.PropertyDescriptor import ru.mipt.npm.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.meta.transformations.nullableMetaToObject
import space.kscience.dataforge.meta.transformations.nullableObjectToMeta
/** /**
@ -15,8 +13,7 @@ import space.kscience.dataforge.meta.transformations.nullableObjectToMeta
@RequiresOptIn @RequiresOptIn
public annotation class InternalDeviceAPI public annotation class InternalDeviceAPI
//TODO relax T restriction after DF 0.4.4 public interface DevicePropertySpec<in D : Device, T> {
public interface DevicePropertySpec<in D : Device, T : Any> {
/** /**
* Property name, should be unique in device * Property name, should be unique in device
*/ */
@ -40,11 +37,11 @@ public interface DevicePropertySpec<in D : Device, T : Any> {
} }
@OptIn(InternalDeviceAPI::class) @OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T : Any> DevicePropertySpec<D, T>.readItem(device: D): Meta = public suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta =
converter.objectToMeta(read(device)) converter.objectToMeta(read(device))
public interface WritableDevicePropertySpec<in D : Device, T : Any> : DevicePropertySpec<D, T> { public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
/** /**
* Write physical value to a device * Write physical value to a device
*/ */
@ -53,11 +50,11 @@ public interface WritableDevicePropertySpec<in D : Device, T : Any> : DeviceProp
} }
@OptIn(InternalDeviceAPI::class) @OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T : Any> WritableDevicePropertySpec<D, T>.writeItem(device: D, item: Meta) { public suspend fun <D : Device, T> WritableDevicePropertySpec<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"))
} }
public interface DeviceActionSpec<in D : Device, I : Any, O : Any> { public interface DeviceActionSpec<in D : Device, I, O> {
/** /**
* Action name, should be unique in device * Action name, should be unique in device
*/ */
@ -78,11 +75,11 @@ public interface DeviceActionSpec<in D : Device, I : Any, O : Any> {
public suspend fun execute(device: D, input: I?): O? public suspend fun execute(device: D, input: I?): O?
} }
public suspend fun <D : Device, I : Any, O : Any> DeviceActionSpec<D, I, O>.executeItem( public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeMeta(
device: D, device: D,
item: Meta? item: Meta?
): Meta? { ): Meta? {
val arg = inputConverter.nullableMetaToObject(item) val arg = item?.let { inputConverter.metaToObject(item) }
val res = execute(device, arg) val res = execute(device, arg)
return outputConverter.nullableObjectToMeta(res) return res?.let { outputConverter.objectToMeta(res) }
} }

View File

@ -52,8 +52,9 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
override val name: String = readWriteProperty.name override val name: String = readWriteProperty.name
override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder) override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T = override suspend fun read(device: D): T = withContext(device.coroutineContext) {
withContext(device.coroutineContext) { readWriteProperty.get(device) } readWriteProperty.get(device)
}
override suspend fun write(device: D, value: T) = withContext(device.coroutineContext) { override suspend fun write(device: D, value: T) = withContext(device.coroutineContext) {
readWriteProperty.set(device, value) readWriteProperty.set(device, value)

View File

@ -1,12 +0,0 @@
package ru.mipt.npm.controls.properties
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.ReadWriteProperty
public fun <D : DeviceBySpec<D>> D.state(
initialValue: Double,
): ReadWriteProperty<D, Double> = state(MetaConverter.double, initialValue)
public fun <D : DeviceBySpec<D>> D.state(
initialValue: Number,
): ReadWriteProperty<D, Number> = state(MetaConverter.number, initialValue)

View File

@ -94,7 +94,7 @@ public fun DeviceManager.launchTangoMagix(
} }
TangoAction.write -> { TangoAction.write -> {
request.payload.value?.let { value -> request.payload.value?.let { value ->
device.writeItem(request.payload.name, value) device.writeProperty(request.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.getOrReadItem(request.payload.name) val value = device.getOrReadItem(request.payload.name)

View File

@ -8,6 +8,7 @@ val miloVersion: String = "0.6.3"
dependencies { dependencies {
api(project(":controls-core")) api(project(":controls-core"))
api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${ru.mipt.npm.gradle.KScienceVersions.coroutinesVersion}")
implementation("org.eclipse.milo:sdk-client:$miloVersion") implementation("org.eclipse.milo:sdk-client:$miloVersion")
implementation("org.eclipse.milo:bsd-parser:$miloVersion") implementation("org.eclipse.milo:bsd-parser:$miloVersion")
implementation("org.eclipse.milo:dictionary-reader:$miloVersion") implementation("org.eclipse.milo:dictionary-reader:$miloVersion")

View File

@ -1,16 +1,12 @@
package ru.mipt.npm.controls.opcua package ru.mipt.npm.controls.opcua
import kotlinx.coroutines.future.await
import org.eclipse.milo.opcua.sdk.client.OpcUaClient import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue import org.eclipse.milo.opcua.stack.core.types.builtin.*
import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn
import ru.mipt.npm.controls.api.Device import ru.mipt.npm.controls.api.Device
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/** /**
@ -28,13 +24,31 @@ public interface MiloDevice : Device {
} }
} }
public inline fun <reified T> MiloDevice.opc( public suspend inline fun <reified T> MiloDevice.readOpcWithTime(
nodeId: NodeId, nodeId: NodeId,
converter: MetaConverter<T>, converter: MetaConverter<T>,
magAge: Double = 500.0 magAge: Double = 500.0
): ReadWriteProperty<Any?, T> = object : ReadWriteProperty<Any?, T> { ): Pair<T, DateTime> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T { val data = client.readValue(magAge, TimestampsToReturn.Server, nodeId).await()
val data = client.readValue(magAge, TimestampsToReturn.Server, nodeId).get() val time = data.serverTime ?: error("No server time provided")
val meta: Meta = when (val content = data.value.value) {
is T -> return content to time
content is Meta -> content as Meta
content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta
else -> error("Incompatible OPC property value $content")
}
val res = converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
return res to time
}
public suspend inline fun <reified T> MiloDevice.readOpc(
nodeId: NodeId,
converter: MetaConverter<T>,
magAge: Double = 500.0
): T {
val data = client.readValue(magAge, TimestampsToReturn.Neither, nodeId).await()
val meta: Meta = when (val content = data.value.value) { val meta: Meta = when (val content = data.value.value) {
is T -> return content is T -> return content
content is Meta -> content as Meta content is Meta -> content as Meta
@ -45,23 +59,11 @@ public inline fun <reified T> MiloDevice.opc(
return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}") return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
} }
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { public suspend inline fun <reified T> MiloDevice.writeOpc(
nodeId: NodeId,
converter: MetaConverter<T>,
value: T
): StatusCode {
val meta = converter.objectToMeta(value) val meta = converter.objectToMeta(value)
client.writeValue(nodeId, DataValue(Variant(meta))) return client.writeValue(nodeId, DataValue(Variant(meta))).await()
} }
}
public inline fun <reified T> MiloDevice.opcDouble(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, Double> = opc(nodeId, MetaConverter.double, magAge)
public inline fun <reified T> MiloDevice.opcInt(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, Int> = opc(nodeId, MetaConverter.int, magAge)
public inline fun <reified T> MiloDevice.opcString(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, String> = opc(nodeId, MetaConverter.string, magAge)

View File

@ -1,6 +1,9 @@
package ru.mipt.npm.controls.opcua package ru.mipt.npm.controls.opcua
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.eclipse.milo.opcua.sdk.client.OpcUaClient import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId
import ru.mipt.npm.controls.properties.DeviceBySpec import ru.mipt.npm.controls.properties.DeviceBySpec
import ru.mipt.npm.controls.properties.DeviceSpec import ru.mipt.npm.controls.properties.DeviceSpec
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
@ -8,6 +11,9 @@ import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string import space.kscience.dataforge.meta.string
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
public open class MiloDeviceBySpec<D : MiloDeviceBySpec<D>>( public open class MiloDeviceBySpec<D : MiloDeviceBySpec<D>>(
spec: DeviceSpec<D>, spec: DeviceSpec<D>,
@ -27,3 +33,37 @@ public open class MiloDeviceBySpec<D: MiloDeviceBySpec<D>>(
super<DeviceBySpec>.close() super<DeviceBySpec>.close()
} }
} }
/**
* A device-bound OPC-UA property. Does not trigger device properties change.
*/
public inline fun <reified T> MiloDeviceBySpec<*>.opc(
nodeId: NodeId,
converter: MetaConverter<T>,
magAge: Double = 500.0
): ReadWriteProperty<Any?, T> = object : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking {
readOpc(nodeId, converter, magAge)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
launch {
writeOpc(nodeId, converter, value)
}
}
}
public inline fun <reified T> MiloDeviceBySpec<*>.opcDouble(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, Double> = opc(nodeId, MetaConverter.double, magAge)
public inline fun <reified T> MiloDeviceBySpec<*>.opcInt(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, Int> = opc(nodeId, MetaConverter.int, magAge)
public inline fun <reified T> MiloDeviceBySpec<*>.opcString(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, String> = opc(nodeId, MetaConverter.string, magAge)

View File

@ -10,6 +10,9 @@ import ru.mipt.npm.controls.api.DeviceMessage
import ru.mipt.npm.controls.client.connectToMagix import ru.mipt.npm.controls.client.connectToMagix
import ru.mipt.npm.controls.controllers.DeviceManager import ru.mipt.npm.controls.controllers.DeviceManager
import ru.mipt.npm.controls.controllers.install import ru.mipt.npm.controls.controllers.install
import ru.mipt.npm.controls.demo.DemoDevice.Companion.cosScale
import ru.mipt.npm.controls.demo.DemoDevice.Companion.sinScale
import ru.mipt.npm.controls.demo.DemoDevice.Companion.timeScale
import ru.mipt.npm.magix.api.MagixEndpoint import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.rsocket.rSocketWithTcp import ru.mipt.npm.magix.rsocket.rSocketWithTcp
import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets
@ -97,10 +100,12 @@ class DemoControllerView : View(title = " Demo controller remote") {
button("Submit") { button("Submit") {
useMaxWidth = true useMaxWidth = true
action { action {
controller.device?.apply { controller.device?.run {
timeScale = timeScaleSlider.value launch {
sinScale = xScaleSlider.value timeScale.write(timeScaleSlider.value)
cosScale = yScaleSlider.value sinScale.write(xScaleSlider.value)
cosScale.write(yScaleSlider.value)
}
} }
} }
} }

View File

@ -9,24 +9,24 @@ import kotlin.time.ExperimentalTime
class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) { class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
var timeScale by state(5000.0) private var timeScaleState = 5000.0
var sinScale by state(1.0) private var sinScaleState = 1.0
var cosScale by state(1.0) private var cosScaleState = 1.0
companion object : DeviceSpec<DemoDevice>(::DemoDevice) { companion object : DeviceSpec<DemoDevice>(::DemoDevice) {
// register virtual properties based on actual object state // register virtual properties based on actual object state
val timeScaleProperty = registerProperty(MetaConverter.double, DemoDevice::timeScale) val timeScale = registerProperty(MetaConverter.double, DemoDevice::timeScaleState)
val sinScaleProperty = registerProperty(MetaConverter.double, DemoDevice::sinScale) val sinScale = registerProperty(MetaConverter.double, DemoDevice::sinScaleState)
val cosScaleProperty = registerProperty(MetaConverter.double, DemoDevice::cosScale) val cosScale = registerProperty(MetaConverter.double, DemoDevice::cosScaleState)
val sin by doubleProperty { val sin by doubleProperty {
val time = Instant.now() val time = Instant.now()
kotlin.math.sin(time.toEpochMilli().toDouble() / timeScale) * sinScale kotlin.math.sin(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState
} }
val cos by doubleProperty { val cos by doubleProperty {
val time = Instant.now() val time = Instant.now()
kotlin.math.cos(time.toEpochMilli().toDouble() / timeScale) * sinScale kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState
} }
val coordinates by metaProperty { val coordinates by metaProperty {
@ -39,9 +39,9 @@ class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
} }
val resetScale by action(MetaConverter.meta, MetaConverter.meta) { val resetScale by action(MetaConverter.meta, MetaConverter.meta) {
timeScale = 5000.0 timeScale.write(5000.0)
sinScale = 1.0 sinScale.write(1.0)
cosScale = 1.0 cosScale.write(1.0)
null null
} }