diff --git a/dataforge-control-core/build.gradle.kts b/dataforge-control-core/build.gradle.kts index f8c6538..4145956 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-meta:$dataforgeVersion") + api("hep.dataforge:dataforge-context:$dataforgeVersion") } } } 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 ead69d7..72a7245 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,13 +1,40 @@ package hep.dataforge.control.api import hep.dataforge.meta.MetaItem +import kotlinx.coroutines.CoroutineScope interface Device { - val descriptors: Collection - var controller: DeviceController? + /** + * List of supported property descriptors + */ + val propertyDescriptors: Collection + /** + * List of supported requests descriptors + */ + val requestDescriptors: Collection + + /** + * The scope encompassing all operations on a device. When canceled, cancels all running processes + */ + val scope: CoroutineScope + + var controller: 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<*> - suspend fun setProperty(propertyName: String, propertyValue: MetaItem<*>) - suspend fun request(command: String, argument: MetaItem<*>?): MetaItem<*>? + /** + * 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. + */ + suspend fun setProperty(propertyName: String, value: MetaItem<*>) + + /** + * 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<*>? } \ No newline at end of file diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceController.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceController.kt deleted file mode 100644 index 88546f0..0000000 --- a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceController.kt +++ /dev/null @@ -1,4 +0,0 @@ -package hep.dataforge.control.api - -interface DeviceController { -} \ 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 new file mode 100644 index 0000000..52e1a10 --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceHub.kt @@ -0,0 +1,23 @@ +package hep.dataforge.control.api + +import hep.dataforge.meta.MetaItem + +/** + * A hub that could locate multiple devices and redirect actions to them + */ +interface DeviceHub { + fun getDevice(deviceName: String): Device? +} + +suspend fun DeviceHub.getProperty(deviceName: String, propertyName: String): MetaItem<*> = + (getDevice(deviceName) ?: error("Device with name $deviceName not found in the hub")) + .getProperty(propertyName) + +suspend fun DeviceHub.setProperty(deviceName: String, propertyName: String, value: MetaItem<*>) { + (getDevice(deviceName) ?: error("Device with name $deviceName not found in the hub")) + .setProperty(propertyName, value) +} + +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 diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/PropertyChangeListener.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/PropertyChangeListener.kt new file mode 100644 index 0000000..bc00083 --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/PropertyChangeListener.kt @@ -0,0 +1,7 @@ +package hep.dataforge.control.api + +import hep.dataforge.meta.MetaItem + +interface PropertyChangeListener { + fun propertyChanged(propertyName: String, value: MetaItem<*>) +} \ No newline at end of file diff --git a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/PropertyDescriptor.kt b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/PropertyDescriptor.kt index 437b60f..e8c149c 100644 --- a/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/PropertyDescriptor.kt +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/PropertyDescriptor.kt @@ -2,14 +2,13 @@ package hep.dataforge.control.api import hep.dataforge.meta.scheme.Scheme import hep.dataforge.meta.scheme.SchemeSpec -import hep.dataforge.meta.scheme.string /** * A descriptor for property */ -class PropertyDescriptor: Scheme() { - var name by string{ error("Property name is mandatory")} +class PropertyDescriptor : Scheme() { + //var name by string { error("Property name is mandatory") } //var descriptor by spec(ItemDescriptor) - companion object: SchemeSpec(::PropertyDescriptor) + companion object : SchemeSpec(::PropertyDescriptor) } \ No newline at end of file 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/RequestDescriptor.kt new file mode 100644 index 0000000..69a4f26 --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/api/RequestDescriptor.kt @@ -0,0 +1,14 @@ +package hep.dataforge.control.api + +import hep.dataforge.meta.scheme.Scheme +import hep.dataforge.meta.scheme.SchemeSpec + +/** + * A descriptor for property + */ +class RequestDescriptor : Scheme() { + //var name by string { error("Property name is mandatory") } + //var descriptor by spec(ItemDescriptor) + + companion object : SchemeSpec(::RequestDescriptor) +} \ 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 new file mode 100644 index 0000000..e15e466 --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt @@ -0,0 +1,91 @@ +package hep.dataforge.control.base + +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.MetaItem +import kotlin.jvm.JvmStatic +import kotlin.reflect.KProperty + +abstract class DeviceBase : Device, PropertyChangeListener { + private val properties = HashMap() + private val requests = HashMap() + + override var controller: PropertyChangeListener? = null + + override fun propertyChanged(propertyName: String, value: MetaItem<*>) { + controller?.propertyChanged(propertyName, value) + } + + override val propertyDescriptors: Collection + get() = properties.values.map { it.descriptor } + + override val requestDescriptors: Collection + get() = requests.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 + } + + protected fun initRequest( + name: String, + descriptor: RequestDescriptor = RequestDescriptor.empty(), + block: suspend (MetaItem<*>?) -> MetaItem<*>? + ): Request { + val request = SimpleRequest(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 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) + + companion object { + @JvmStatic + protected fun D.initProperty( + name: String, + builder: PropertyBuilder.() -> P + ): P { + val property = PropertyBuilder(name, this).run(builder) + initProperty(property) + return property + } + } +} + +class PropertyDelegateProvider>( + val owner: D, + val builder: PropertyBuilder.() -> P +) { + operator fun provideDelegate(thisRef: D, property: KProperty<*>): P { + val name = property.name + return owner.initProperty(PropertyBuilder(name, owner).run(builder)) + } +} + +fun D.property( + builder: PropertyBuilder.() -> GenericReadOnlyProperty +): PropertyDelegateProvider> { + return PropertyDelegateProvider(this, builder) +} + +fun D.mutableProperty( + builder: PropertyBuilder.() -> GenericProperty +): PropertyDelegateProvider> { + return PropertyDelegateProvider(this, builder) +} + + 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 new file mode 100644 index 0000000..6e87655 --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/GenericProperty.kt @@ -0,0 +1,84 @@ +package hep.dataforge.control.base + +import hep.dataforge.control.api.PropertyDescriptor +import hep.dataforge.meta.MetaItem +import hep.dataforge.meta.transformations.MetaCaster +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +open class GenericReadOnlyProperty( + override val name: String, + override val descriptor: PropertyDescriptor, + override val owner: D, + internal val converter: MetaCaster, + internal val getter: suspend D.() -> T +) : ReadOnlyProperty, kotlin.properties.ReadOnlyProperty { + + protected val mutex = Mutex() + protected var value: T? = null + + suspend fun updateValue(value: T) { + mutex.withLock { this.value = value } + owner.propertyChanged(name, converter.objectToMetaItem(value)) + } + + suspend fun readValue(): T = + 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<*>) { + updateValue(converter.itemToObject(item)) + } + + override suspend fun read(): MetaItem<*> = converter.objectToMetaItem(readValue()) + + override fun peek(): MetaItem<*>? = value?.let { converter.objectToMetaItem(it) } + + override fun getValue(thisRef: Any?, property: KProperty<*>): T? = peekValue() +} + +class GenericProperty( + name: String, + descriptor: PropertyDescriptor, + owner: D, + converter: MetaCaster, + getter: suspend D.() -> T, + private val setter: suspend D.(oldValue: T?, newValue: T) -> Unit +) : Property, ReadWriteProperty, GenericReadOnlyProperty(name, descriptor, owner, converter, getter) { + + suspend fun writeValue(newValue: T) { + val oldValue = value + withContext(owner.scope.coroutineContext) { + //all device operations should be run on device context + invalidate() + owner.setter(oldValue, newValue) + } + } + + override suspend fun invalidate() { + mutex.withLock { value = null } + } + + override suspend fun write(item: MetaItem<*>) { + writeValue(converter.itemToObject(item)) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { + owner.scope.launch { + if (value == null) { + invalidate() + } else { + writeValue(value) + } + } + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000..93324f8 --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Property.kt @@ -0,0 +1,54 @@ +package hep.dataforge.control.base + +import hep.dataforge.control.api.Device +import hep.dataforge.control.api.PropertyDescriptor +import hep.dataforge.meta.MetaItem + +/** + * Read-only device property + */ +interface ReadOnlyProperty { + /** + * Property name, should be unique in device + */ + val name: String + + val owner: Device + + /** + * Property descriptor + */ + val descriptor: PropertyDescriptor + + /** + * Get cached value and return null if value is invalid + */ + fun peek(): MetaItem<*>? + + /** + * Read value either from cache if cache is valid or directly from physical device + */ + suspend fun read(): MetaItem<*> +} + +/** + * A single writeable property handler + */ +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 + */ + suspend fun write(item: MetaItem<*>) +} + 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 new file mode 100644 index 0000000..b4f9191 --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/PropertyBuilder.kt @@ -0,0 +1,46 @@ +package hep.dataforge.control.base + +import hep.dataforge.control.api.PropertyDescriptor +import hep.dataforge.meta.transformations.MetaCaster +import hep.dataforge.values.Value + +class PropertyBuilder(val name: String, val owner: D) { + var descriptor: PropertyDescriptor = PropertyDescriptor.empty() + + inline fun descriptor(block: PropertyDescriptor.() -> Unit) { + descriptor.apply(block) + } + + fun get(converter: MetaCaster, getter: (suspend D.() -> T)): GenericReadOnlyProperty = + GenericReadOnlyProperty(name, descriptor, owner, converter, getter) + + fun getDouble(getter: (suspend D.() -> Double)): GenericReadOnlyProperty = + GenericReadOnlyProperty(name, descriptor, owner, MetaCaster.double) { getter() } + + fun getString(getter: suspend D.() -> String): GenericReadOnlyProperty = + GenericReadOnlyProperty(name, descriptor, owner, MetaCaster.string) { getter() } + + fun getBoolean(getter: suspend D.() -> Boolean): GenericReadOnlyProperty = + GenericReadOnlyProperty(name, descriptor, owner, MetaCaster.boolean) { getter() } + + fun getValue(getter: suspend D.() -> Any?): GenericReadOnlyProperty = + GenericReadOnlyProperty(name, descriptor, owner, MetaCaster.value) { Value.of(getter()) } + + /** + * Convert this read-only property to read-write property + */ + 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 { + 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) + } +} \ 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 new file mode 100644 index 0000000..3f58126 --- /dev/null +++ b/dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/Request.kt @@ -0,0 +1,18 @@ +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/jvmMain/kotlin/hep/dataforge/control/demo/VirtualDevice.kt b/dataforge-control-core/src/jvmMain/kotlin/hep/dataforge/control/demo/VirtualDevice.kt new file mode 100644 index 0000000..2019581 --- /dev/null +++ b/dataforge-control-core/src/jvmMain/kotlin/hep/dataforge/control/demo/VirtualDevice.kt @@ -0,0 +1,35 @@ +package hep.dataforge.control.demo + +import hep.dataforge.control.base.DeviceBase +import hep.dataforge.control.base.mutableProperty +import hep.dataforge.control.base.property +import hep.dataforge.meta.Meta +import kotlinx.coroutines.CoroutineScope +import java.time.Instant +import kotlin.math.cos +import kotlin.math.sin + +class VirtualDevice(val meta: Meta, override val scope: CoroutineScope) : DeviceBase() { + + var scale by mutableProperty { + getDouble { + 200.0 + } set { _, _ -> + + } + } + + val sin by property { + getDouble { + val time = Instant.now() + sin(time.toEpochMilli().toDouble() / (scale ?: 1000.0)) + } + } + + val cos by property { + getDouble { + val time = Instant.now() + cos(time.toEpochMilli().toDouble() / (scale ?: 1000.0)) + } + } +} \ No newline at end of file