Clean up property access syntax
This commit is contained in:
parent
691b2ae67a
commit
b66e23cca6
@ -39,7 +39,7 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
|||||||
public val actionDescriptors: Collection<ActionDescriptor>
|
public val actionDescriptors: Collection<ActionDescriptor>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read physical state of property and update/push notifications if needed.
|
* Read the physical state of property and update/push notifications if needed.
|
||||||
*/
|
*/
|
||||||
public suspend fun readProperty(propertyName: String): Meta
|
public suspend fun readProperty(propertyName: String): Meta
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
|||||||
* Send an action request and suspend caller while request is being processed.
|
* Send an action request and suspend caller while request is being processed.
|
||||||
* Could return null if request does not return a meaningful answer.
|
* Could return null if request does not return a meaningful answer.
|
||||||
*/
|
*/
|
||||||
public suspend fun execute(action: String, argument: Meta? = null): Meta?
|
public suspend fun execute(actionName: String, argument: Meta? = null): Meta?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the device. This function suspends until the device is finished initialization
|
* Initialize the device. This function suspends until the device is finished initialization
|
||||||
|
@ -13,10 +13,29 @@ import space.kscience.dataforge.meta.Meta
|
|||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(InternalDeviceAPI::class)
|
||||||
|
private 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"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(InternalDeviceAPI::class)
|
||||||
|
private suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
|
||||||
|
read(device)?.let(converter::objectToMeta)
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta(
|
||||||
|
device: D,
|
||||||
|
item: Meta?,
|
||||||
|
): Meta? {
|
||||||
|
val arg = item?.let { inputConverter.metaToObject(item) }
|
||||||
|
val res = execute(device, arg)
|
||||||
|
return res?.let { outputConverter.objectToMeta(res) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base abstractions for [Device], introducing specifications for properties
|
* A base abstractions for [Device], introducing specifications for properties
|
||||||
*/
|
*/
|
||||||
@OptIn(InternalDeviceAPI::class)
|
|
||||||
public abstract class DeviceBase<D : Device>(
|
public abstract class DeviceBase<D : Device>(
|
||||||
override val context: Context = Global,
|
override val context: Context = Global,
|
||||||
override val meta: Meta = Meta.EMPTY,
|
override val meta: Meta = Meta.EMPTY,
|
||||||
@ -83,10 +102,17 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
* 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]?.readMeta(self)
|
val spec = properties[propertyName] ?: error("Property with name $propertyName not found")
|
||||||
?: error("A property with name $propertyName is not registered in $this")
|
val meta = spec.readMeta(self) ?: error("Failed to read property $propertyName")
|
||||||
updateLogical(propertyName, newValue)
|
updateLogical(propertyName, meta)
|
||||||
return newValue
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
public suspend fun readPropertyOrNull(propertyName: String): Meta? {
|
||||||
|
val spec = properties[propertyName] ?: return null
|
||||||
|
val meta = spec.readMeta(self) ?: return null
|
||||||
|
updateLogical(propertyName, meta)
|
||||||
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getProperty(propertyName: String): Meta? = logicalState[propertyName]
|
override fun getProperty(propertyName: String): Meta? = logicalState[propertyName]
|
||||||
@ -98,40 +124,27 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeProperty(propertyName: String, value: Meta): Unit {
|
override suspend fun writeProperty(propertyName: String, value: Meta): Unit {
|
||||||
|
when (val property = properties[propertyName]) {
|
||||||
|
null -> {
|
||||||
//If there is a physical property with a given name, invalidate logical property and write physical one
|
//If there is a physical property with a given name, invalidate logical property and write physical one
|
||||||
(properties[propertyName] as? WritableDevicePropertySpec<D, out Any?>)?.let {
|
|
||||||
invalidate(propertyName)
|
|
||||||
it.writeMeta(self, value)
|
|
||||||
} ?: run {
|
|
||||||
updateLogical(propertyName, value)
|
updateLogical(propertyName, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is WritableDevicePropertySpec -> {
|
||||||
|
invalidate(propertyName)
|
||||||
|
property.writeMeta(self, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute(action: String, argument: Meta?): Meta? =
|
else -> {
|
||||||
actions[action]?.executeWithMeta(self, argument)
|
error("Property $property is not writeable")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A device generated from specification
|
|
||||||
* @param D recursive self-type for properties and actions
|
|
||||||
*/
|
|
||||||
public open class DeviceBySpec<D : Device>(
|
|
||||||
public val spec: DeviceSpec<in D>,
|
|
||||||
context: Context = Global,
|
|
||||||
meta: Meta = Meta.EMPTY,
|
|
||||||
) : DeviceBase<D>(context, meta) {
|
|
||||||
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
|
|
||||||
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
|
|
||||||
|
|
||||||
override suspend fun open(): Unit = with(spec) {
|
|
||||||
super.open()
|
|
||||||
self.onOpen()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close(): Unit = with(spec) {
|
|
||||||
self.onClose()
|
|
||||||
super.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun execute(actionName: String, argument: Meta?): Meta? {
|
||||||
|
val spec = actions[actionName] ?: error("Action with name $actionName not found")
|
||||||
|
return spec.executeWithMeta(self, argument)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
package space.kscience.controls.spec
|
||||||
|
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.Global
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A device generated from specification
|
||||||
|
* @param D recursive self-type for properties and actions
|
||||||
|
*/
|
||||||
|
public open class DeviceBySpec<D : Device>(
|
||||||
|
public val spec: DeviceSpec<in D>,
|
||||||
|
context: Context = Global,
|
||||||
|
meta: Meta = Meta.EMPTY,
|
||||||
|
) : DeviceBase<D>(context, meta) {
|
||||||
|
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
|
||||||
|
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
|
||||||
|
|
||||||
|
override suspend fun open(): Unit = with(spec) {
|
||||||
|
super.open()
|
||||||
|
self.onOpen()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close(): Unit = with(spec) {
|
||||||
|
self.onClose()
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,6 @@ import space.kscience.controls.api.ActionDescriptor
|
|||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.api.PropertyChangedMessage
|
import space.kscience.controls.api.PropertyChangedMessage
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
|
|
||||||
|
|
||||||
@ -36,17 +35,14 @@ public interface DevicePropertySpec<in D : Device, T> {
|
|||||||
*/
|
*/
|
||||||
@InternalDeviceAPI
|
@InternalDeviceAPI
|
||||||
public suspend fun read(device: D): T?
|
public suspend fun read(device: D): T?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Property name, should be unique in device
|
* Property name should be unique in a device
|
||||||
*/
|
*/
|
||||||
public val DevicePropertySpec<*, *>.name: String get() = descriptor.name
|
public val DevicePropertySpec<*, *>.name: String get() = descriptor.name
|
||||||
|
|
||||||
@OptIn(InternalDeviceAPI::class)
|
|
||||||
public suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
|
|
||||||
read(device)?.let(converter::objectToMeta)
|
|
||||||
|
|
||||||
|
|
||||||
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
|
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
|
||||||
/**
|
/**
|
||||||
@ -54,11 +50,7 @@ public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySp
|
|||||||
*/
|
*/
|
||||||
@InternalDeviceAPI
|
@InternalDeviceAPI
|
||||||
public suspend fun write(device: D, value: T)
|
public suspend fun write(device: D, value: T)
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(InternalDeviceAPI::class)
|
|
||||||
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, O> {
|
public interface DeviceActionSpec<in D : Device, I, O> {
|
||||||
@ -82,29 +74,16 @@ public interface DeviceActionSpec<in D : Device, I, O> {
|
|||||||
*/
|
*/
|
||||||
public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name
|
public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name
|
||||||
|
|
||||||
public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta(
|
public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>): T =
|
||||||
device: D,
|
propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Can't convert result from meta")
|
||||||
item: Meta?,
|
|
||||||
): Meta? {
|
|
||||||
val arg = item?.let { inputConverter.metaToObject(item) }
|
|
||||||
val res = execute(device, arg)
|
|
||||||
return res?.let { outputConverter.objectToMeta(res) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read typed value and update/push event if needed.
|
* Read typed value and update/push event if needed.
|
||||||
* Return null if property read is not successful or property is undefined.
|
* Return null if property read is not successful or property is undefined.
|
||||||
*/
|
*/
|
||||||
@OptIn(InternalDeviceAPI::class)
|
public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||||
public suspend fun <T, D : Device> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? {
|
readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||||
val res = propertySpec.read(this) ?: return null
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(this as? DeviceBase<D>)?.updateLogical(propertySpec, res)
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>): T =
|
|
||||||
readOrNull(propertySpec) ?: error("Failed to read property ${propertySpec.name} state")
|
|
||||||
|
|
||||||
public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? =
|
public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||||
getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||||
@ -112,34 +91,17 @@ public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>
|
|||||||
/**
|
/**
|
||||||
* Write typed property state and invalidate logical state
|
* Write typed property state and invalidate logical state
|
||||||
*/
|
*/
|
||||||
@OptIn(InternalDeviceAPI::class)
|
|
||||||
public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, value: T) {
|
public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, value: T) {
|
||||||
invalidate(propertySpec.name)
|
writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
|
||||||
propertySpec.write(this, value)
|
|
||||||
//perform asynchronous read and update after write
|
|
||||||
launch {
|
|
||||||
read(propertySpec)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope
|
* Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope
|
||||||
*/
|
*/
|
||||||
public operator fun <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Unit {
|
public operator fun <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Job = launch {
|
||||||
launch {
|
|
||||||
write(propertySpec, value)
|
write(propertySpec, value)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the logical state of a property
|
|
||||||
*/
|
|
||||||
public suspend fun <D : Device> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
|
|
||||||
invalidate(propertySpec.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun <I, O, D : Device> D.execute(actionSpec: DeviceActionSpec<D, I, O>, input: I? = null): O? =
|
|
||||||
actionSpec.execute(this, input)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A type safe property change listener
|
* A type safe property change listener
|
||||||
@ -153,3 +115,13 @@ public fun <D : Device, T> Device.onPropertyChange(
|
|||||||
.onEach { change ->
|
.onEach { change ->
|
||||||
change.callback(spec.converter.metaToObject(change.value))
|
change.callback(spec.converter.metaToObject(change.value))
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the logical state of a property
|
||||||
|
*/
|
||||||
|
public suspend fun <D : Device> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
|
||||||
|
invalidate(propertySpec.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
public suspend fun <I, O, D : Device> D.execute(actionSpec: DeviceActionSpec<D, I, O>, input: I? = null): O? =
|
||||||
|
actionSpec.execute(this, input)
|
@ -9,7 +9,7 @@ import kotlin.time.Duration
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a recurring asynchronous read action and return a flow of results.
|
* Perform a recurring asynchronous read action and return a flow of results.
|
||||||
* The flow is lazy so action is not performed unless flow is consumed.
|
* The flow is lazy, so action is not performed unless flow is consumed.
|
||||||
* The flow uses called context. In order to call it on device context, use `flowOn(coroutineContext)`.
|
* The flow uses called context. In order to call it on device context, use `flowOn(coroutineContext)`.
|
||||||
*
|
*
|
||||||
* The flow is canceled when the device scope is canceled
|
* The flow is canceled when the device scope is canceled
|
||||||
|
@ -28,8 +28,6 @@ class OpcUaClientTest {
|
|||||||
return DemoOpcUaDevice(config)
|
return DemoOpcUaDevice(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <R> use(block: (DemoOpcUaDevice) -> R): R = build().use(block)
|
|
||||||
|
|
||||||
val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble)
|
val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble)
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -40,7 +38,7 @@ class OpcUaClientTest {
|
|||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun testReadDouble() = runTest {
|
fun testReadDouble() = runTest {
|
||||||
DemoOpcUaDevice.use{
|
DemoOpcUaDevice.build().use{
|
||||||
println(it.read(DemoOpcUaDevice.randomDouble))
|
println(it.read(DemoOpcUaDevice.randomDouble))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user