diff --git a/dataforge-control-core/build.gradle.kts b/dataforge-control-core/build.gradle.kts index 4145956..a701c36 100644 --- a/dataforge-control-core/build.gradle.kts +++ b/dataforge-control-core/build.gradle.kts @@ -10,7 +10,7 @@ kotlin { sourceSets { commonMain{ dependencies { - api("hep.dataforge:dataforge-context:$dataforgeVersion") + api("hep.dataforge:dataforge-io:$dataforgeVersion") } } } diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/RequestDescriptor.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/ActionDescriptor.kt similarity index 70% rename from dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/RequestDescriptor.kt rename to dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/ActionDescriptor.kt index 69a4f26..c44ae90 100644 --- a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/RequestDescriptor.kt +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/ActionDescriptor.kt @@ -6,9 +6,9 @@ import hep.dataforge.meta.scheme.SchemeSpec /** * A descriptor for property */ -class RequestDescriptor : Scheme() { +class ActionDescriptor : Scheme() { //var name by string { error("Property name is mandatory") } //var descriptor by spec(ItemDescriptor) - companion object : SchemeSpec(::RequestDescriptor) + companion object : SchemeSpec(::ActionDescriptor) } \ No newline at end of file diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt index 72a7245..8ea39ff 100644 --- a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt @@ -1,5 +1,6 @@ package hep.dataforge.control.api +import hep.dataforge.meta.Meta import hep.dataforge.meta.MetaItem import kotlinx.coroutines.CoroutineScope @@ -12,20 +13,25 @@ interface Device { /** * List of supported requests descriptors */ - val requestDescriptors: Collection + val actionDescriptors: Collection /** * The scope encompassing all operations on a device. When canceled, cancels all running processes */ val scope: CoroutineScope - var controller: PropertyChangeListener? + var listener: PropertyChangeListener? /** * Get the value of the property or throw error if property in not defined. Suspend if property value is not available */ suspend fun getProperty(propertyName: String): MetaItem<*> + /** + * Invalidate property and force recalculate + */ + suspend fun invalidateProperty(propertyName: String) + /** * 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. @@ -36,5 +42,10 @@ interface Device { * Send a request and suspend caller while request is being processed. * Could return null if request does not return meaningful answer. */ - suspend fun request(name: String, argument: MetaItem<*>? = null): MetaItem<*>? + suspend fun action(name: String, argument: Meta? = null): Meta? + + companion object { + const val GET_PROPERTY_ACTION = "@getProperty" + const val SET_PROPERTY_ACTION = "@setProperty" + } } \ No newline at end of file diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceHub.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceHub.kt index 52e1a10..7235489 100644 --- a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceHub.kt +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceHub.kt @@ -20,4 +20,4 @@ suspend fun DeviceHub.setProperty(deviceName: String, propertyName: String, valu suspend fun DeviceHub.request(deviceName: String, command: String, argument: MetaItem<*>?): MetaItem<*>? = (getDevice(deviceName) ?: error("Device with name $deviceName not found in the hub")) - .request(command, argument) \ No newline at end of file + .action(command, argument) \ No newline at end of file diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Action.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Action.kt new file mode 100644 index 0000000..755df94 --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Action.kt @@ -0,0 +1,18 @@ +package hep.dataforge.control.base + +import hep.dataforge.control.api.ActionDescriptor +import hep.dataforge.meta.Meta + +interface Action { + val name: String + val descriptor: ActionDescriptor + suspend operator fun invoke(arg: Meta?): Meta? +} + +class SimpleAction( + override val name: String, + override val descriptor: ActionDescriptor, + val block: suspend (Meta?)->Meta? +): Action{ + override suspend fun invoke(arg: Meta?): Meta? = block(arg) +} \ No newline at end of file diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt index e15e466..884a0da 100644 --- a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt @@ -1,59 +1,66 @@ package hep.dataforge.control.base +import hep.dataforge.control.api.ActionDescriptor import hep.dataforge.control.api.Device import hep.dataforge.control.api.PropertyChangeListener import hep.dataforge.control.api.PropertyDescriptor -import hep.dataforge.control.api.RequestDescriptor +import hep.dataforge.meta.Meta import hep.dataforge.meta.MetaItem import kotlin.jvm.JvmStatic import kotlin.reflect.KProperty abstract class DeviceBase : Device, PropertyChangeListener { private val properties = HashMap() - private val requests = HashMap() + private val actions = HashMap() - override var controller: PropertyChangeListener? = null + override var listener: PropertyChangeListener? = null override fun propertyChanged(propertyName: String, value: MetaItem<*>) { - controller?.propertyChanged(propertyName, value) + listener?.propertyChanged(propertyName, value) } override val propertyDescriptors: Collection get() = properties.values.map { it.descriptor } - override val requestDescriptors: Collection - get() = requests.values.map { it.descriptor } + override val actionDescriptors: Collection + get() = actions.values.map { it.descriptor } fun

initProperty(prop: P): P { properties[prop.name] = prop return prop } - fun initRequest(request: Request): Request { - requests[request.name] = request - return request + fun initRequest(Action: Action): Action { + actions[Action.name] = Action + return Action } protected fun initRequest( name: String, - descriptor: RequestDescriptor = RequestDescriptor.empty(), + descriptor: ActionDescriptor = ActionDescriptor.empty(), block: suspend (MetaItem<*>?) -> MetaItem<*>? - ): Request { - val request = SimpleRequest(name, descriptor, block) + ): Action { + val request = SimpleAction(name, descriptor, block) return initRequest(request) } override suspend fun getProperty(propertyName: String): MetaItem<*> = (properties[propertyName] ?: error("Property with name $propertyName not defined")).read() + override suspend fun invalidateProperty(propertyName: String) { + (properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate() + } + override suspend fun setProperty(propertyName: String, value: MetaItem<*>) { (properties[propertyName] as? Property ?: error("Property with name $propertyName not defined")).write(value) } - override suspend fun request(name: String, argument: MetaItem<*>?): MetaItem<*>? = - (requests[name] ?: error("Request with name $name not defined")).invoke(argument) + override suspend fun action(name: String, argument: Meta?): Meta? = + (actions[name] ?: error("Request with name $name not defined")).invoke(argument) + companion object { + @JvmStatic protected fun D.initProperty( name: String, @@ -82,6 +89,7 @@ fun D.property( return PropertyDelegateProvider(this, builder) } +//TODO try to use 'property' with new inference fun D.mutableProperty( builder: PropertyBuilder.() -> GenericProperty ): PropertyDelegateProvider> { diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/GenericProperty.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/GenericProperty.kt index 6e87655..831a9e1 100644 --- a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/GenericProperty.kt +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/GenericProperty.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.withContext import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty -open class GenericReadOnlyProperty( +open class GenericReadOnlyProperty( override val name: String, override val descriptor: PropertyDescriptor, override val owner: D, @@ -26,26 +26,32 @@ open class GenericReadOnlyProperty( owner.propertyChanged(name, converter.objectToMetaItem(value)) } - suspend fun readValue(): T = - value ?: withContext(owner.scope.coroutineContext) { + override suspend fun invalidate() { + mutex.withLock { value = null } + } + + suspend fun readValue(force: Boolean = false): T { + if (force) invalidate() + return value ?: withContext(owner.scope.coroutineContext) { //all device operations should be run on device context owner.getter().also { updateValue(it) } } + } fun peekValue(): T? = value - suspend fun update(item: MetaItem<*>) { + override suspend fun update(item: MetaItem<*>) { updateValue(converter.itemToObject(item)) } - override suspend fun read(): MetaItem<*> = converter.objectToMetaItem(readValue()) + override suspend fun read(force: Boolean): MetaItem<*> = converter.objectToMetaItem(readValue(force)) override fun peek(): MetaItem<*>? = value?.let { converter.objectToMetaItem(it) } override fun getValue(thisRef: Any?, property: KProperty<*>): T? = peekValue() } -class GenericProperty( +class GenericProperty( name: String, descriptor: PropertyDescriptor, owner: D, @@ -63,10 +69,6 @@ class GenericProperty( } } - override suspend fun invalidate() { - mutex.withLock { value = null } - } - override suspend fun write(item: MetaItem<*>) { writeValue(converter.itemToObject(item)) } diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Property.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Property.kt index 93324f8..98b6f0f 100644 --- a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Property.kt +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Property.kt @@ -20,6 +20,16 @@ interface ReadOnlyProperty { */ val descriptor: PropertyDescriptor + /** + * Erase logical value and force re-read from device on next [read] + */ + suspend fun invalidate() + + /** + * 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 */ @@ -28,7 +38,7 @@ interface ReadOnlyProperty { /** * Read value either from cache if cache is valid or directly from physical device */ - suspend fun read(): MetaItem<*> + suspend fun read(force: Boolean = false): MetaItem<*> } /** @@ -36,16 +46,6 @@ interface ReadOnlyProperty { */ interface Property : ReadOnlyProperty { - /** - * Update property logical value and notify listener without writing it to device - */ - suspend fun update(item: MetaItem<*>) - - /** - * Erase logical value and force re-read from device on next [read] - */ - suspend fun invalidate() - /** * Write value to physical device. Invalidates logical value, but does not update it automatically */ diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/PropertyBuilder.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/PropertyBuilder.kt index b4f9191..359f6b1 100644 --- a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/PropertyBuilder.kt +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/PropertyBuilder.kt @@ -29,18 +29,28 @@ class PropertyBuilder(val name: String, val owner: D) { /** * Convert this read-only property to read-write property */ - infix fun GenericReadOnlyProperty.set(setter: (suspend D.(oldValue: T?, newValue: T) -> Unit)): GenericProperty { + infix fun GenericReadOnlyProperty.set(setter: (suspend D.(oldValue: T?, newValue: T) -> Unit)): GenericProperty { return GenericProperty(name, descriptor, owner, converter, getter, setter) } /** * Create read-write property with synchronized setter which updates value after set */ - fun GenericReadOnlyProperty.set(synchronousSetter: (suspend D.(oldValue: T?, newValue: T) -> T)): GenericProperty { + fun GenericReadOnlyProperty.set(synchronousSetter: (suspend D.(oldValue: T?, newValue: T) -> T)): GenericProperty { val setter: suspend D.(oldValue: T?, newValue: T) -> Unit = { oldValue, newValue -> val result = synchronousSetter(oldValue, newValue) updateValue(result) } return GenericProperty(name, descriptor, owner, converter, getter, setter) } + + /** + * Define a setter that does nothing for virtual property + */ + fun GenericReadOnlyProperty.virtualSet(): GenericProperty { + val setter: suspend D.(oldValue: T?, newValue: T) -> Unit = { oldValue, newValue -> + updateValue(newValue) + } + return GenericProperty(name, descriptor, owner, converter, getter, setter) + } } \ No newline at end of file diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Request.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Request.kt deleted file mode 100644 index 3f58126..0000000 --- a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Request.kt +++ /dev/null @@ -1,18 +0,0 @@ -package hep.dataforge.control.base - -import hep.dataforge.control.api.RequestDescriptor -import hep.dataforge.meta.MetaItem - -interface Request { - val name: String - val descriptor: RequestDescriptor - suspend operator fun invoke(arg: MetaItem<*>?): MetaItem<*>? -} - -class SimpleRequest( - override val name: String, - override val descriptor: RequestDescriptor, - val block: suspend (MetaItem<*>?)->MetaItem<*>? -): Request{ - override suspend fun invoke(arg: MetaItem<*>?): MetaItem<*>? = block(arg) -} \ No newline at end of file diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/controlers/DeviceMessage.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/controlers/DeviceMessage.kt new file mode 100644 index 0000000..379a64d --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/controlers/DeviceMessage.kt @@ -0,0 +1,55 @@ +package hep.dataforge.control.controlers + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.scheme.* + +class PropertyValue : Scheme() { + var name by string { error("Name property not defined") } + var value by item() + + companion object : SchemeSpec(::PropertyValue) +} + +open class DeviceMessage : Scheme() { + var id by item() + var source by string()//TODO consider replacing by item + var target by string() + var action by string() + var status by string() + + companion object : SchemeSpec(::DeviceMessage) { + const val MESSAGE_ACTION_KEY = "action" + const val MESSAGE_PROPERTY_NAME_KEY = "propertyName" + const val MESSAGE_VALUE_KEY = "value" + const val RESPONSE_OK_STATUS = "response.OK" + const val EVENT_STATUS = "event.propertyChange" + + fun ok(request: DeviceMessage? = null, block: DeviceMessage.() -> Unit): Meta { + return DeviceMessage { + id = request?.id + status = RESPONSE_OK_STATUS + }.apply(block).toMeta() + } + } +} + +class DevicePropertyMessage : DeviceMessage() { + //TODO add multiple properties in the same message + var property by spec(PropertyValue) + + fun property(builder: PropertyValue.() -> Unit) { + this.property = PropertyValue.invoke(builder) + } + + companion object : SchemeSpec(::DevicePropertyMessage) { + fun ok(request: DeviceMessage? = null, block: DevicePropertyMessage.() -> Unit): Meta { + return DevicePropertyMessage { + id = request?.id + property { + name + } + status = RESPONSE_OK_STATUS + }.apply(block).toMeta() + } + } +} \ No newline at end of file diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/controlers/FlowController.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/controlers/FlowController.kt new file mode 100644 index 0000000..b25bd93 --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/controlers/FlowController.kt @@ -0,0 +1,60 @@ +package hep.dataforge.control.controlers + +import hep.dataforge.control.api.Device +import hep.dataforge.control.api.PropertyChangeListener +import hep.dataforge.control.controlers.DeviceMessage.Companion.EVENT_STATUS +import hep.dataforge.io.Envelope +import hep.dataforge.io.SimpleEnvelope +import hep.dataforge.meta.MetaItem +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.launch +import kotlinx.io.Closeable +import kotlinx.io.EmptyBinary + +class FlowController(val device: D, val target: String, val scope: CoroutineScope) : PropertyChangeListener, + Closeable { + + init { + if (device.listener != null) error("Can't attach controller to $device, the controller is already attached") + device.listener = this + } + + private val outputChannel = Channel(CONFLATED) + private val inputChannel = Channel(CONFLATED) + + val input: SendChannel get() = inputChannel + val output = outputChannel.consumeAsFlow() + + init { + scope.launch { + while (!inputChannel.isClosedForSend) { + val request = inputChannel.receive() + val response = device.respond(target, request) + outputChannel.send(response) + } + } + } + + override fun propertyChanged(propertyName: String, value: MetaItem<*>) { + scope.launch { + val changeMeta = DevicePropertyMessage.ok { + this.source = target + status = EVENT_STATUS + property { + name = propertyName + this.value = value + } + } + outputChannel.send(SimpleEnvelope(changeMeta, EmptyBinary)) + } + } + + override fun close() { + outputChannel.cancel() + } + +} \ No newline at end of file diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/controlers/deviceResponse.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/controlers/deviceResponse.kt new file mode 100644 index 0000000..91148ed --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/controlers/deviceResponse.kt @@ -0,0 +1,61 @@ +package hep.dataforge.control.controlers + +import hep.dataforge.control.api.Device +import hep.dataforge.io.Envelope +import hep.dataforge.io.SimpleEnvelope +import hep.dataforge.meta.Meta +import hep.dataforge.meta.set +import kotlinx.io.EmptyBinary + +suspend fun Device.respond(target: String, request: Envelope, action: String): Envelope { + val metaResult = when (action) { + Device.GET_PROPERTY_ACTION -> { + val message = DevicePropertyMessage.wrap(request.meta) + val property = message.property ?: error("Property item not defined") + val propertyName: String = property.name + val result = getProperty(propertyName) + + DevicePropertyMessage.ok { + this.source = target + this.target = message.source + property { + name = propertyName + value = result + } + } + } + Device.SET_PROPERTY_ACTION -> { + val message = DevicePropertyMessage.wrap(request.meta) + val property = message.property ?: error("Property item not defined") + val propertyName: String = property.name + val propertyValue = property.value + if (propertyValue == null) { + invalidateProperty(propertyName) + } else { + setProperty(propertyName, propertyValue) + } + DevicePropertyMessage.ok { + this.source = target + this.target = message.source + property { + name = propertyName + } + } + } + else -> { + val data: Meta? = request.meta[DeviceMessage.MESSAGE_VALUE_KEY].node + val result = action(action, data) + DeviceMessage.ok { + this.source = target + config[DeviceMessage.MESSAGE_ACTION_KEY] = action + config[DeviceMessage.MESSAGE_VALUE_KEY] = result + } + } + } + return SimpleEnvelope(metaResult, EmptyBinary) +} + +suspend fun Device.respond(target: String, request: Envelope): Envelope { + val action: String = request.meta[DeviceMessage.MESSAGE_ACTION_KEY].string ?: error("Action not defined") + return respond(target, request, action) +} \ No newline at end of file diff --git a/dataforge-control-core/src/jvmMain/kotlin/hep/dataforge/control/demo/VirtualDevice.kt b/dataforge-control-core/src/jvmMain/kotlin/hep/dataforge/control/demo/VirtualDevice.kt index 2019581..b0b7dc3 100644 --- a/dataforge-control-core/src/jvmMain/kotlin/hep/dataforge/control/demo/VirtualDevice.kt +++ b/dataforge-control-core/src/jvmMain/kotlin/hep/dataforge/control/demo/VirtualDevice.kt @@ -14,9 +14,7 @@ class VirtualDevice(val meta: Meta, override val scope: CoroutineScope) : Device var scale by mutableProperty { getDouble { 200.0 - } set { _, _ -> - - } + }.virtualSet() } val sin by property {