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].
* 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

View File

@ -59,8 +59,8 @@ public operator fun DeviceHub.get(nameString: String): Device =
public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta =
this[deviceName].readProperty(propertyName)
public suspend fun DeviceHub.writeItem(deviceName: Name, propertyName: String, value: Meta) {
this[deviceName].writeItem(propertyName, value)
public suspend fun DeviceHub.writeProperty(deviceName: Name, propertyName: String, value: Meta) {
this[deviceName].writeProperty(propertyName, value)
}
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()
}
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(
value
)

View File

@ -25,7 +25,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
if (request.value == null) {
invalidate(request.property)
} else {
writeItem(request.property, request.value)
writeProperty(request.property, request.value)
}
PropertyChangedMessage(
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.Global
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.coroutines.CoroutineContext
import kotlin.properties.Delegates.observable
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* A device generated from specification
@ -58,7 +54,7 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
private val stateLock = Mutex()
private suspend fun updateLogical(propertyName: String, value: Meta?) {
protected suspend fun updateLogical(propertyName: String, value: Meta?) {
if (value != logicalState[propertyName]) {
stateLock.withLock {
logicalState[propertyName] = value
@ -74,7 +70,7 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
* The logical state is updated after read
*/
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")
updateLogical(propertyName, 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
(properties[propertyName] as? WritableDevicePropertySpec<D, out Any>)?.let {
it.writeItem(self, value)
(properties[propertyName] as? WritableDevicePropertySpec<D, out Any?>)?.let {
it.writeMeta(self, value)
invalidate(propertyName)
} ?: run {
updateLogical(propertyName, value)
@ -99,39 +95,23 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
}
override suspend fun execute(action: String, argument: Meta?): Meta? =
actions[action]?.executeItem(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)))
}
}
}
actions[action]?.executeMeta(self, argument)
/**
* 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)
updateLogical(name, converter.objectToMeta(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
*/
public suspend fun <T : Any> WritableDevicePropertySpec<D, T>.write(value: T) {
public suspend fun <T> WritableDevicePropertySpec<D, T>.write(value: T) {
write(self, value)
invalidate(name)
}
@ -146,7 +126,7 @@ public suspend fun <D : DeviceBySpec<D>, T : Any> D.read(
propertySpec: DevicePropertySpec<D, T>
): T = propertySpec.read()
public fun <D : DeviceBySpec<D>, T : Any> D.write(
public fun <D : DeviceBySpec<D>, T> D.write(
propertySpec: WritableDevicePropertySpec<D, T>,
value: T
): Job = launch {

View File

@ -5,8 +5,6 @@ import ru.mipt.npm.controls.api.Device
import ru.mipt.npm.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
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
public annotation class InternalDeviceAPI
//TODO relax T restriction after DF 0.4.4
public interface DevicePropertySpec<in D : Device, T : Any> {
public interface DevicePropertySpec<in D : Device, T> {
/**
* Property name, should be unique in device
*/
@ -40,11 +37,11 @@ public interface DevicePropertySpec<in D : Device, T : Any> {
}
@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))
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
*/
@ -53,11 +50,11 @@ public interface WritableDevicePropertySpec<in D : Device, T : Any> : DeviceProp
}
@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"))
}
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
*/
@ -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 <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,
item: Meta?
): Meta? {
val arg = inputConverter.nullableMetaToObject(item)
val arg = item?.let { inputConverter.metaToObject(item) }
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 descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T =
withContext(device.coroutineContext) { readWriteProperty.get(device) }
override suspend fun read(device: D): T = withContext(device.coroutineContext) {
readWriteProperty.get(device)
}
override suspend fun write(device: D, value: T) = withContext(device.coroutineContext) {
readWriteProperty.set(device, value)
@ -145,12 +146,12 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
/**
* The function is executed right after device initialization is finished
*/
public open fun D.onStartup(){}
public open fun D.onStartup() {}
/**
* The function is executed before device is shut down
*/
public open fun D.onShutdown(){}
public open fun D.onShutdown() {}
override fun invoke(meta: Meta, context: Context): D = buildDevice().apply {

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 -> {
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
val value = device.getOrReadItem(request.payload.name)

View File

@ -8,6 +8,7 @@ val miloVersion: String = "0.6.3"
dependencies {
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:bsd-parser:$miloVersion")
implementation("org.eclipse.milo:dictionary-reader:$miloVersion")

View File

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

View File

@ -1,6 +1,9 @@
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.stack.core.types.builtin.NodeId
import ru.mipt.npm.controls.properties.DeviceBySpec
import ru.mipt.npm.controls.properties.DeviceSpec
import space.kscience.dataforge.context.Context
@ -8,12 +11,15 @@ import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
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>,
context: Context = Global,
meta: Meta = Meta.EMPTY
): MiloDevice, DeviceBySpec<D>(spec, context, meta) {
) : MiloDevice, DeviceBySpec<D>(spec, context, meta) {
override val client: OpcUaClient by lazy {
val endpointUrl = meta["endpointUrl"].string ?: error("Endpoint url is not defined")
@ -27,3 +33,37 @@ public open class MiloDeviceBySpec<D: MiloDeviceBySpec<D>>(
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.controllers.DeviceManager
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.rsocket.rSocketWithTcp
import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets
@ -97,10 +100,12 @@ class DemoControllerView : View(title = " Demo controller remote") {
button("Submit") {
useMaxWidth = true
action {
controller.device?.apply {
timeScale = timeScaleSlider.value
sinScale = xScaleSlider.value
cosScale = yScaleSlider.value
controller.device?.run {
launch {
timeScale.write(timeScaleSlider.value)
sinScale.write(xScaleSlider.value)
cosScale.write(yScaleSlider.value)
}
}
}
}

View File

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