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>
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@ -71,7 +71,7 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
||||
* Send an action request and suspend caller while request is being processed.
|
||||
* 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
|
||||
|
@ -13,10 +13,29 @@ import space.kscience.dataforge.meta.Meta
|
||||
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
|
||||
*/
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
public abstract class DeviceBase<D : Device>(
|
||||
override val context: Context = Global,
|
||||
override val meta: Meta = Meta.EMPTY,
|
||||
@ -83,10 +102,17 @@ public abstract class DeviceBase<D : Device>(
|
||||
* The logical state is updated after read
|
||||
*/
|
||||
override suspend fun readProperty(propertyName: String): Meta {
|
||||
val newValue = properties[propertyName]?.readMeta(self)
|
||||
?: error("A property with name $propertyName is not registered in $this")
|
||||
updateLogical(propertyName, newValue)
|
||||
return newValue
|
||||
val spec = properties[propertyName] ?: error("Property with name $propertyName not found")
|
||||
val meta = spec.readMeta(self) ?: error("Failed to read property $propertyName")
|
||||
updateLogical(propertyName, meta)
|
||||
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]
|
||||
@ -98,40 +124,27 @@ public abstract class DeviceBase<D : Device>(
|
||||
}
|
||||
|
||||
override suspend fun writeProperty(propertyName: String, value: Meta): Unit {
|
||||
//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)
|
||||
when (val property = properties[propertyName]) {
|
||||
null -> {
|
||||
//If there is a physical property with a given name, invalidate logical property and write physical one
|
||||
updateLogical(propertyName, value)
|
||||
}
|
||||
|
||||
is WritableDevicePropertySpec -> {
|
||||
invalidate(propertyName)
|
||||
property.writeMeta(self, value)
|
||||
}
|
||||
|
||||
else -> {
|
||||
error("Property $property is not writeable")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun execute(action: String, argument: Meta?): Meta? =
|
||||
actions[action]?.executeWithMeta(self, argument)
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.PropertyChangedMessage
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
|
||||
|
||||
@ -36,17 +35,14 @@ public interface DevicePropertySpec<in D : Device, T> {
|
||||
*/
|
||||
@InternalDeviceAPI
|
||||
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
|
||||
|
||||
@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> {
|
||||
/**
|
||||
@ -54,11 +50,7 @@ public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySp
|
||||
*/
|
||||
@InternalDeviceAPI
|
||||
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> {
|
||||
@ -82,29 +74,16 @@ public interface DeviceActionSpec<in D : Device, I, O> {
|
||||
*/
|
||||
public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name
|
||||
|
||||
public 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) }
|
||||
}
|
||||
public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>): T =
|
||||
propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Can't convert result from meta")
|
||||
|
||||
/**
|
||||
* Read typed value and update/push event if needed.
|
||||
* Return null if property read is not successful or property is undefined.
|
||||
*/
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
public suspend fun <T, D : Device> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? {
|
||||
val res = propertySpec.read(this) ?: return null
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(this as? DeviceBase<D>)?.updateLogical(propertySpec, res)
|
||||
return res
|
||||
}
|
||||
public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||
readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||
|
||||
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? =
|
||||
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
|
||||
*/
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, value: T) {
|
||||
invalidate(propertySpec.name)
|
||||
propertySpec.write(this, value)
|
||||
//perform asynchronous read and update after write
|
||||
launch {
|
||||
read(propertySpec)
|
||||
}
|
||||
writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
launch {
|
||||
write(propertySpec, value)
|
||||
}
|
||||
public operator fun <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Job = launch {
|
||||
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
|
||||
@ -153,3 +115,13 @@ public fun <D : Device, T> Device.onPropertyChange(
|
||||
.onEach { change ->
|
||||
change.callback(spec.converter.metaToObject(change.value))
|
||||
}.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.
|
||||
* 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 is canceled when the device scope is canceled
|
||||
|
@ -28,8 +28,6 @@ class OpcUaClientTest {
|
||||
return DemoOpcUaDevice(config)
|
||||
}
|
||||
|
||||
inline fun <R> use(block: (DemoOpcUaDevice) -> R): R = build().use(block)
|
||||
|
||||
val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble)
|
||||
|
||||
}
|
||||
@ -40,7 +38,7 @@ class OpcUaClientTest {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun testReadDouble() = runTest {
|
||||
DemoOpcUaDevice.use{
|
||||
DemoOpcUaDevice.build().use{
|
||||
println(it.read(DemoOpcUaDevice.randomDouble))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user