- get() = _actions.values.map { it.descriptor }
-
- private fun registerProperty(name: String, property: P) {
- if (_properties.contains(name)) error("Property with name $name already registered")
- _properties[name] = property
- }
-
- internal fun registerAction(name: String, action: DeviceAction) {
- if (_actions.contains(name)) error("Action with name $name already registered")
- _actions[name] = action
- }
-
- override suspend fun readProperty(propertyName: String): Meta =
- (_properties[propertyName] ?: error("Property with name $propertyName not defined")).read()
-
- override fun getProperty(propertyName: String): Meta? =
- (_properties[propertyName] ?: error("Property with name $propertyName not defined")).value
-
- override suspend fun invalidate(propertyName: String) {
- (_properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate()
- }
-
- override suspend fun writeProperty(propertyName: String, value: Meta) {
- (_properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write(
- value
- )
- }
-
- override suspend fun execute(action: String, argument: Meta?): Meta? =
- (_actions[action] ?: error("Request with name $action not defined")).invoke(argument)
-
- /**
- * Create a bound read-only property with given [getter]
- */
- public fun createReadOnlyProperty(
- name: String,
- default: Meta?,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend (Meta?) -> Meta,
- ): ReadOnlyDeviceProperty {
- val property = BasicReadOnlyDeviceProperty(
- this,
- name,
- default,
- PropertyDescriptor(name).apply(descriptorBuilder),
- getter
- )
- registerProperty(name, property)
- return property
- }
-
-
- /**
- * Create a bound mutable property with given [getter] and [setter]
- */
- internal fun createMutableProperty(
- name: String,
- default: Meta?,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend (Meta?) -> Meta,
- setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
- ): DeviceProperty {
- val property = BasicDeviceProperty(
- this,
- name,
- default,
- PropertyDescriptor(name).apply(descriptorBuilder),
- getter,
- setter
- )
- registerProperty(name, property)
- return property
- }
-
- /**
- * A stand-alone action
- */
- private inner class BasicDeviceAction(
- override val name: String,
- override val descriptor: ActionDescriptor,
- private val block: suspend (Meta?) -> Meta?,
- ) : DeviceAction {
- override suspend fun invoke(arg: Meta?): Meta? =
- withContext(coroutineContext) {
- block(arg)
- }
- }
-
- /**
- * Create a new bound action
- */
- internal fun createAction(
- name: String,
- descriptorBuilder: ActionDescriptor.() -> Unit = {},
- block: suspend (Meta?) -> Meta?,
- ): DeviceAction {
- val action = BasicDeviceAction(name, ActionDescriptor(name).apply(descriptorBuilder), block)
- registerAction(name, action)
- return action
- }
-
- public companion object {
-
- }
-}
-
-
-
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceProperty.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceProperty.kt
deleted file mode 100644
index 5f67acf..0000000
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceProperty.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package ru.mipt.npm.controls.base
-
-import kotlinx.coroutines.*
-import kotlinx.coroutines.flow.Flow
-import ru.mipt.npm.controls.api.PropertyDescriptor
-import space.kscience.dataforge.meta.Meta
-import kotlin.time.Duration
-
-/**
- * Read-only device property
- */
-public interface ReadOnlyDeviceProperty {
- /**
- * Property name, should be unique in device
- */
- public val name: String
-
- /**
- * Property descriptor
- */
- public val descriptor: PropertyDescriptor
-
- public val scope: CoroutineScope
-
- /**
- * Erase logical value and force re-read from device on next [read]
- */
- public suspend fun invalidate()
-
- /**
- * Directly update property logical value and notify listener without writing it to device
- */
- public fun updateLogical(item: Meta)
-
- /**
- * Get cached value and return null if value is invalid or not initialized
- */
- public val value: Meta?
-
- /**
- * Read value either from cache if cache is valid or directly from physical device.
- * If [force], reread from physical state even if the logical state is set.
- */
- public suspend fun read(force: Boolean = false): Meta
-
- /**
- * The [Flow] representing future logical states of the property.
- * Produces null when the state is invalidated
- */
- public fun flow(): Flow
-}
-
-
-/**
- * Launch recurring force re-read job on a property scope with given [duration] between reads.
- */
-public fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch {
- while (isActive) {
- read(true)
- delay(duration)
- }
-}
-
-/**
- * A writeable device property with non-suspended write
- */
-public interface DeviceProperty : ReadOnlyDeviceProperty {
- override var value: Meta?
-
- /**
- * Write value to physical device. Invalidates logical value, but does not update it automatically
- */
- public suspend fun write(item: Meta)
-}
\ No newline at end of file
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/TypedDeviceProperty.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/TypedDeviceProperty.kt
deleted file mode 100644
index b783fe2..0000000
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/TypedDeviceProperty.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package ru.mipt.npm.controls.base
-
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import space.kscience.dataforge.meta.Meta
-import space.kscience.dataforge.meta.transformations.MetaConverter
-
-/**
- * A type-safe wrapper on top of read-only property
- */
-public open class TypedReadOnlyDeviceProperty(
- private val property: ReadOnlyDeviceProperty,
- protected val converter: MetaConverter,
-) : ReadOnlyDeviceProperty by property {
-
- public fun updateLogical(obj: T) {
- property.updateLogical(converter.objectToMeta(obj))
- }
-
- public open val typedValue: T? get() = value?.let { converter.metaToObject(it) }
-
- public suspend fun readTyped(force: Boolean = false): T {
- val meta = read(force)
- return converter.metaToObject(meta)
- ?: error("Meta $meta could not be converted by $converter")
- }
-
- public fun flowTyped(): Flow = flow().map { it?.let { converter.metaToObject(it) } }
-}
-
-/**
- * A type-safe wrapper for a read-write device property
- */
-public class TypedDeviceProperty(
- private val property: DeviceProperty,
- converter: MetaConverter,
-) : TypedReadOnlyDeviceProperty(property, converter), DeviceProperty {
-
- override var value: Meta?
- get() = property.value
- set(arg) {
- property.value = arg
- }
-
- public override var typedValue: T?
- get() = value?.let { converter.metaToObject(it) }
- set(arg) {
- property.value = arg?.let { converter.objectToMeta(arg) }
- }
-
- override suspend fun write(item: Meta) {
- property.write(item)
- }
-
- public suspend fun write(obj: T) {
- property.write(converter.objectToMeta(obj))
- }
-}
\ No newline at end of file
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/actionDelegates.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/actionDelegates.kt
deleted file mode 100644
index 452e5a1..0000000
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/actionDelegates.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package ru.mipt.npm.controls.base
-
-import ru.mipt.npm.controls.api.ActionDescriptor
-import space.kscience.dataforge.meta.Meta
-import space.kscience.dataforge.meta.MutableMeta
-import space.kscience.dataforge.values.Value
-import kotlin.properties.PropertyDelegateProvider
-import kotlin.properties.ReadOnlyProperty
-import kotlin.reflect.KProperty
-
-
-private fun D.provideAction(): ReadOnlyProperty =
- ReadOnlyProperty { _: D, property: KProperty<*> ->
- val name = property.name
- return@ReadOnlyProperty actions[name]!!
- }
-
-public typealias ActionDelegate = ReadOnlyProperty
-
-private class ActionProvider(
- val owner: D,
- val descriptorBuilder: ActionDescriptor.() -> Unit = {},
- val block: suspend (Meta?) -> Meta?,
-) : PropertyDelegateProvider {
- override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ActionDelegate {
- val name = property.name
- owner.createAction(name, descriptorBuilder, block)
- return owner.provideAction()
- }
-}
-
-public fun DeviceBase.requesting(
- descriptorBuilder: ActionDescriptor.() -> Unit = {},
- action: suspend (Meta?) -> Meta?,
-): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder, action)
-
-public fun D.requestingValue(
- descriptorBuilder: ActionDescriptor.() -> Unit = {},
- action: suspend (Meta?) -> Any?,
-): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) {
- val res = action(it)
- Meta(Value.of(res))
-}
-
-public fun D.requestingMeta(
- descriptorBuilder: ActionDescriptor.() -> Unit = {},
- action: suspend MutableMeta.(Meta?) -> Unit,
-): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) {
- Meta { action(it) }
-}
-
-public fun DeviceBase.acting(
- descriptorBuilder: ActionDescriptor.() -> Unit = {},
- action: suspend (Meta?) -> Unit,
-): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) {
- action(it)
- null
-}
\ No newline at end of file
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/devicePropertyDelegates.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/devicePropertyDelegates.kt
deleted file mode 100644
index 0f47204..0000000
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/devicePropertyDelegates.kt
+++ /dev/null
@@ -1,283 +0,0 @@
-package ru.mipt.npm.controls.base
-
-import ru.mipt.npm.controls.api.PropertyDescriptor
-import space.kscience.dataforge.meta.Meta
-import space.kscience.dataforge.meta.MutableMeta
-import space.kscience.dataforge.meta.boolean
-import space.kscience.dataforge.meta.double
-import space.kscience.dataforge.meta.transformations.MetaConverter
-import space.kscience.dataforge.values.Null
-import space.kscience.dataforge.values.Value
-import space.kscience.dataforge.values.asValue
-import kotlin.properties.PropertyDelegateProvider
-import kotlin.properties.ReadOnlyProperty
-import kotlin.reflect.KProperty
-
-private fun D.provideProperty(name: String): ReadOnlyProperty =
- ReadOnlyProperty { _: D, _: KProperty<*> ->
- return@ReadOnlyProperty properties.getValue(name)
- }
-
-private fun D.provideProperty(
- name: String,
- converter: MetaConverter,
-): ReadOnlyProperty> =
- ReadOnlyProperty { _: D, _: KProperty<*> ->
- return@ReadOnlyProperty TypedReadOnlyDeviceProperty(properties.getValue(name), converter)
- }
-
-
-public typealias ReadOnlyPropertyDelegate = ReadOnlyProperty
-public typealias TypedReadOnlyPropertyDelegate = ReadOnlyProperty>
-
-private class ReadOnlyDevicePropertyProvider(
- val owner: D,
- val default: Meta?,
- val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- private val getter: suspend (Meta?) -> Meta,
-) : PropertyDelegateProvider {
-
- override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ReadOnlyPropertyDelegate {
- val name = property.name
- owner.createReadOnlyProperty(name, default, descriptorBuilder, getter)
- return owner.provideProperty(name)
- }
-}
-
-private class TypedReadOnlyDevicePropertyProvider(
- val owner: D,
- val default: Meta?,
- val converter: MetaConverter,
- val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- private val getter: suspend (Meta?) -> Meta,
-) : PropertyDelegateProvider> {
-
- override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedReadOnlyPropertyDelegate {
- val name = property.name
- owner.createReadOnlyProperty(name, default, descriptorBuilder, getter)
- return owner.provideProperty(name, converter)
- }
-}
-
-public fun DeviceBase.reading(
- default: Meta? = null,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend (Meta?) -> Meta,
-): PropertyDelegateProvider = ReadOnlyDevicePropertyProvider(
- this,
- default,
- descriptorBuilder,
- getter
-)
-
-public fun DeviceBase.readingValue(
- default: Value? = null,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend () -> Any?,
-): PropertyDelegateProvider = ReadOnlyDevicePropertyProvider(
- this,
- default?.let { Meta(it) },
- descriptorBuilder,
- getter = { Meta(Value.of(getter())) }
-)
-
-public fun DeviceBase.readingNumber(
- default: Number? = null,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend () -> Number,
-): PropertyDelegateProvider> = TypedReadOnlyDevicePropertyProvider(
- this,
- default?.let { Meta(it.asValue()) },
- MetaConverter.number,
- descriptorBuilder,
- getter = {
- val number = getter()
- Meta(number.asValue())
- }
-)
-
-public fun DeviceBase.readingDouble(
- default: Number? = null,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend () -> Double,
-): PropertyDelegateProvider> = TypedReadOnlyDevicePropertyProvider(
- this,
- default?.let { Meta(it.asValue()) },
- MetaConverter.double,
- descriptorBuilder,
- getter = {
- val number = getter()
- Meta(number.asValue())
- }
-)
-
-public fun DeviceBase.readingString(
- default: String? = null,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend () -> String,
-): PropertyDelegateProvider> = TypedReadOnlyDevicePropertyProvider(
- this,
- default?.let { Meta(it.asValue()) },
- MetaConverter.string,
- descriptorBuilder,
- getter = {
- val number = getter()
- Meta(number.asValue())
- }
-)
-
-public fun DeviceBase.readingBoolean(
- default: Boolean? = null,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend () -> Boolean,
-): PropertyDelegateProvider> = TypedReadOnlyDevicePropertyProvider(
- this,
- default?.let { Meta(it.asValue()) },
- MetaConverter.boolean,
- descriptorBuilder,
- getter = {
- val boolean = getter()
- Meta(boolean.asValue())
- }
-)
-
-public fun DeviceBase.readingMeta(
- default: Meta? = null,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend MutableMeta.() -> Unit,
-): PropertyDelegateProvider> = TypedReadOnlyDevicePropertyProvider(
- this,
- default,
- MetaConverter.meta,
- descriptorBuilder,
- getter = {
- Meta { getter() }
- }
-)
-
-private fun DeviceBase.provideMutableProperty(name: String): ReadOnlyProperty =
- ReadOnlyProperty { _: DeviceBase, _: KProperty<*> ->
- return@ReadOnlyProperty properties[name] as DeviceProperty
- }
-
-private fun DeviceBase.provideMutableProperty(
- name: String,
- converter: MetaConverter,
-): ReadOnlyProperty> =
- ReadOnlyProperty { _: DeviceBase, _: KProperty<*> ->
- return@ReadOnlyProperty TypedDeviceProperty(properties[name] as DeviceProperty, converter)
- }
-
-public typealias PropertyDelegate = ReadOnlyProperty
-public typealias TypedPropertyDelegate = ReadOnlyProperty>
-
-private class DevicePropertyProvider(
- val owner: D,
- val default: Meta?,
- val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- private val getter: suspend (Meta?) -> Meta,
- private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
-) : PropertyDelegateProvider {
-
- override operator fun provideDelegate(thisRef: D, property: KProperty<*>): PropertyDelegate {
- val name = property.name
- owner.createMutableProperty(name, default, descriptorBuilder, getter, setter)
- return owner.provideMutableProperty(name)
- }
-}
-
-private class TypedDevicePropertyProvider(
- val owner: D,
- val default: Meta?,
- val converter: MetaConverter,
- val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- private val getter: suspend (Meta?) -> Meta,
- private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
-) : PropertyDelegateProvider> {
-
- override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedPropertyDelegate {
- val name = property.name
- owner.createMutableProperty(name, default, descriptorBuilder, getter, setter)
- return owner.provideMutableProperty(name, converter)
- }
-}
-
-public fun DeviceBase.writing(
- default: Meta? = null,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend (Meta?) -> Meta,
- setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
-): PropertyDelegateProvider = DevicePropertyProvider(
- this,
- default,
- descriptorBuilder,
- getter,
- setter
-)
-
-public fun DeviceBase.writingVirtual(
- default: Meta,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
-): PropertyDelegateProvider = writing(
- default,
- descriptorBuilder,
- getter = { it ?: default },
- setter = { _, newItem -> newItem }
-)
-
-public fun DeviceBase.writingVirtual(
- default: Value,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
-): PropertyDelegateProvider = writing(
- Meta(default),
- descriptorBuilder,
- getter = { it ?: Meta(default) },
- setter = { _, newItem -> newItem }
-)
-
-public fun D.writingDouble(
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend (Double) -> Double,
- setter: suspend (oldValue: Double?, newValue: Double) -> Double?,
-): PropertyDelegateProvider> {
- val innerGetter: suspend (Meta?) -> Meta = {
- Meta(getter(it.double ?: Double.NaN).asValue())
- }
-
- val innerSetter: suspend (oldValue: Meta?, newValue: Meta) -> Meta? = { oldValue, newValue ->
- setter(oldValue.double, newValue.double ?: Double.NaN)?.asMeta()
- }
-
- return TypedDevicePropertyProvider(
- this,
- Meta(Double.NaN.asValue()),
- MetaConverter.double,
- descriptorBuilder,
- innerGetter,
- innerSetter
- )
-}
-
-public fun D.writingBoolean(
- descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- getter: suspend (Boolean?) -> Boolean,
- setter: suspend (oldValue: Boolean?, newValue: Boolean) -> Boolean?,
-): PropertyDelegateProvider> {
- val innerGetter: suspend (Meta?) -> Meta = {
- Meta(getter(it.boolean).asValue())
- }
-
- val innerSetter: suspend (oldValue: Meta?, newValue: Meta) -> Meta? = { oldValue, newValue ->
- setter(oldValue.boolean, newValue.boolean ?: error("Can't convert $newValue to boolean"))?.asValue()
- ?.let { Meta(it) }
- }
-
- return TypedDevicePropertyProvider(
- this,
- Meta(Null),
- MetaConverter.boolean,
- descriptorBuilder,
- innerGetter,
- innerSetter
- )
-}
\ No newline at end of file
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/SynchronousPortHandler.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/SynchronousPortHandler.kt
deleted file mode 100644
index 508ce6d..0000000
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/SynchronousPortHandler.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package ru.mipt.npm.controls.ports
-
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-
-/**
- * A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended.
- * The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment.
- *
- */
-public class SynchronousPortHandler(public val port: Port) {
- private val mutex = Mutex()
-
- /**
- * Send a single message and wait for the flow of respond messages.
- */
- public suspend fun respond(data: ByteArray, transform: suspend Flow.() -> R): R {
- return mutex.withLock {
- port.send(data)
- transform(port.receiving())
- }
- }
-}
-
-/**
- * Send request and read incoming data blocks until the delimiter is encountered
- */
-public suspend fun SynchronousPortHandler.respondWithDelimiter(data: ByteArray, delimiter: ByteArray): ByteArray {
- return respond(data) {
- withDelimiter(delimiter).first()
- }
-}
\ No newline at end of file
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt
deleted file mode 100644
index 23ceffb..0000000
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-package ru.mipt.npm.controls.properties
-
-import ru.mipt.npm.controls.api.ActionDescriptor
-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
-
-
-/**
- * This API is internal and should not be used in user code
- */
-@RequiresOptIn
-public annotation class InternalDeviceAPI
-
-public interface DevicePropertySpec {
- /**
- * Property name, should be unique in device
- */
- public val name: String
-
- /**
- * Property descriptor
- */
- public val descriptor: PropertyDescriptor
-
- /**
- * Meta item converter for resulting type
- */
- public val converter: MetaConverter
-
- /**
- * Read physical value from the given [device]
- */
- @InternalDeviceAPI
- public suspend fun read(device: D): T
-}
-
-@OptIn(InternalDeviceAPI::class)
-public suspend fun DevicePropertySpec.readMeta(device: D): Meta =
- converter.objectToMeta(read(device))
-
-
-public interface WritableDevicePropertySpec : DevicePropertySpec {
- /**
- * Write physical value to a device
- */
- @InternalDeviceAPI
- public suspend fun write(device: D, value: T)
-}
-
-@OptIn(InternalDeviceAPI::class)
-public suspend fun WritableDevicePropertySpec.writeMeta(device: D, item: Meta) {
- write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
-}
-
-public interface DeviceActionSpec {
- /**
- * Action name, should be unique in device
- */
- public val name: String
-
- /**
- * Action descriptor
- */
- public val descriptor: ActionDescriptor
-
- public val inputConverter: MetaConverter
-
- public val outputConverter: MetaConverter
-
- /**
- * Execute action on a device
- */
- public suspend fun execute(device: D, input: I?): O?
-}
-
-public suspend fun DeviceActionSpec.executeMeta(
- device: D,
- item: Meta?
-): Meta? {
- val arg = item?.let { inputConverter.metaToObject(item) }
- val res = execute(device, arg)
- return res?.let { outputConverter.objectToMeta(res) }
-}
\ No newline at end of file
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt
similarity index 82%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt
index 6298657..c122260 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt
@@ -1,4 +1,4 @@
-package ru.mipt.npm.controls.api
+package space.kscience.controls.api
import io.ktor.utils.io.core.Closeable
import kotlinx.coroutines.CoroutineScope
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import ru.mipt.npm.controls.api.Device.Companion.DEVICE_TARGET
+import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.Type
@@ -21,6 +21,12 @@ import space.kscience.dataforge.names.Name
*/
@Type(DEVICE_TARGET)
public interface Device : Closeable, ContextAware, CoroutineScope {
+
+ /**
+ * Initial configuration meta for the device
+ */
+ public val meta: Meta get() = Meta.EMPTY
+
/**
* List of supported property descriptors
*/
@@ -51,13 +57,13 @@ 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.
+ * In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
*/
public suspend fun writeProperty(propertyName: String, value: Meta)
/**
* A subscription-based [Flow] of [DeviceMessage] provided by device. The flow is guaranteed to be readable
- * multiple times
+ * multiple times.
*/
public val messageFlow: Flow
@@ -67,6 +73,14 @@ public interface Device : Closeable, ContextAware, CoroutineScope {
*/
public suspend fun execute(action: String, argument: Meta? = null): Meta?
+ /**
+ * Initialize the device. This function suspends until the device is finished initialization
+ */
+ public suspend fun open(): Unit = Unit
+
+ /**
+ * Close and terminate the device. This function does not wait for device to be closed.
+ */
override fun close() {
cancel("The device is closed")
}
@@ -85,9 +99,9 @@ public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
/**
* Get a snapshot of logical state of the device
*
- * TODO currently this
+ * TODO currently this
*/
-public fun Device.getProperties(): Meta = Meta {
+public fun Device.getAllProperties(): Meta = Meta {
for (descriptor in propertyDescriptors) {
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
}
@@ -97,4 +111,4 @@ public fun Device.getProperties(): Meta = Meta {
* Subscribe on property changes for the whole device
*/
public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job =
- messageFlow.filterIsInstance().onEach(callback).launchIn(this)
\ No newline at end of file
+ messageFlow.filterIsInstance().onEach(callback).launchIn(this)
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt
similarity index 98%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt
index aba8517..9565950 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt
@@ -1,4 +1,4 @@
-package ru.mipt.npm.controls.api
+package space.kscience.controls.api
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.*
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt
similarity index 96%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt
index f919f1e..80a7f9d 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt
@@ -1,4 +1,4 @@
-package ru.mipt.npm.controls.api
+package space.kscience.controls.api
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
@@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
-import space.kscience.dataforge.io.SimpleEnvelope
+import space.kscience.dataforge.io.Envelope
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.toJson
import space.kscience.dataforge.meta.toMeta
@@ -113,6 +113,8 @@ public data class GetDescriptionMessage(
@SerialName("description")
public data class DescriptionMessage(
val description: Meta,
+ val properties: Collection,
+ val actions: Collection,
override val sourceDevice: Name,
override val targetDevice: Name? = null,
override val comment: String? = null,
@@ -219,4 +221,4 @@ public data class DeviceErrorMessage(
public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta()
-public fun DeviceMessage.toEnvelope(): SimpleEnvelope = SimpleEnvelope(toMeta(), null)
+public fun DeviceMessage.toEnvelope(): Envelope = Envelope(toMeta(), null)
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Socket.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt
similarity index 85%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Socket.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt
index eda8942..02598ba 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Socket.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt
@@ -1,14 +1,13 @@
-package ru.mipt.npm.controls.api
+package space.kscience.controls.api
import io.ktor.utils.io.core.Closeable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/**
- * A generic bi-directional sender/receiver object
+ * A generic bidirectional sender/receiver object
*/
public interface Socket : Closeable {
/**
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/descriptors.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt
similarity index 95%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/descriptors.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt
index 1e70962..8e1705b 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/descriptors.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt
@@ -1,4 +1,4 @@
-package ru.mipt.npm.controls.api
+package space.kscience.controls.api
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceManager.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt
similarity index 84%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceManager.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt
index 03880ca..a3a5bfd 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceManager.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt
@@ -1,7 +1,8 @@
-package ru.mipt.npm.controls.controllers
+package space.kscience.controls.manager
-import ru.mipt.npm.controls.api.Device
-import ru.mipt.npm.controls.api.DeviceHub
+import kotlinx.coroutines.launch
+import space.kscience.controls.api.Device
+import space.kscience.controls.api.DeviceHub
import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
@@ -30,7 +31,7 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass = DeviceManager::class
- override fun invoke(meta: Meta, context: Context): DeviceManager = DeviceManager()
+ override fun build(context: Context, meta: Meta): DeviceManager = DeviceManager()
}
}
@@ -38,6 +39,9 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
public fun DeviceManager.install(name: String, factory: Factory, meta: Meta = Meta.EMPTY): D {
val device = factory(meta, context)
registerDevice(NameToken(name), device)
+ device.launch {
+ device.open()
+ }
return device
}
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/deviceMessages.kt
similarity index 80%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/manager/deviceMessages.kt
index 5158961..ea5d34c 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/deviceMessages.kt
@@ -1,15 +1,11 @@
-package ru.mipt.npm.controls.controllers
+package space.kscience.controls.manager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.encodeToJsonElement
-import ru.mipt.npm.controls.api.*
-import space.kscience.dataforge.meta.Meta
-import space.kscience.dataforge.meta.toMeta
+import space.kscience.controls.api.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.plus
@@ -48,21 +44,10 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
}
is GetDescriptionMessage -> {
- val descriptionMeta = Meta {
- "properties" put {
- propertyDescriptors.map { descriptor ->
- descriptor.name put Json.encodeToJsonElement(descriptor).toMeta()
- }
- }
- "actions" put {
- actionDescriptors.map { descriptor ->
- descriptor.name put Json.encodeToJsonElement(descriptor).toMeta()
- }
- }
- }
-
DescriptionMessage(
- description = descriptionMeta,
+ description = meta,
+ properties = propertyDescriptors,
+ actions = actionDescriptors,
sourceDevice = deviceTarget,
targetDevice = request.sourceDevice
)
@@ -95,6 +80,8 @@ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMe
* Collect all messages from given [DeviceHub], applying proper relative names
*/
public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow {
+
+ //TODO could we avoid using downstream scope?
val outbox = MutableSharedFlow()
if (this is Device) {
messageFlow.onEach {
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/Port.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt
similarity index 82%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/Port.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt
index 4cf672d..374c404 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/Port.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt
@@ -1,16 +1,24 @@
-package ru.mipt.npm.controls.ports
+package space.kscience.controls.ports
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
-import ru.mipt.npm.controls.api.Socket
+import space.kscience.controls.api.Socket
import space.kscience.dataforge.context.*
+import space.kscience.dataforge.misc.Type
import kotlin.coroutines.CoroutineContext
public interface Port : ContextAware, Socket
-public typealias PortFactory = Factory
+@Type(PortFactory.TYPE)
+public interface PortFactory: Factory{
+ public val type: String
+
+ public companion object{
+ public const val TYPE: String = "controls.port"
+ }
+}
public abstract class AbstractPort(
override val context: Context,
@@ -64,12 +72,10 @@ public abstract class AbstractPort(
/**
* Raw flow of incoming data chunks. The chunks are not guaranteed to be complete phrases.
- * In order to form phrases some condition should used on top of it.
+ * In order to form phrases some condition should be used on top of it.
* For example [delimitedIncoming] generates phrases with fixed delimiter.
*/
- override fun receiving(): Flow {
- return incoming.receiveAsFlow()
- }
+ override fun receiving(): Flow = incoming.receiveAsFlow()
override fun close() {
outgoing.close()
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/PortProxy.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/PortProxy.kt
similarity index 95%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/PortProxy.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/ports/PortProxy.kt
index 686992d..4e51f6f 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/PortProxy.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/PortProxy.kt
@@ -1,8 +1,7 @@
-package ru.mipt.npm.controls.ports
+package space.kscience.controls.ports
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Ports.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Ports.kt
new file mode 100644
index 0000000..8c652cc
--- /dev/null
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Ports.kt
@@ -0,0 +1,34 @@
+package space.kscience.controls.ports
+
+import space.kscience.dataforge.context.*
+import space.kscience.dataforge.meta.Meta
+import space.kscience.dataforge.meta.string
+import kotlin.reflect.KClass
+
+public class Ports : AbstractPlugin() {
+
+ override val tag: PluginTag get() = Companion.tag
+
+ private val portFactories by lazy {
+ context.gather(PortFactory.TYPE)
+ }
+
+ private val portCache = mutableMapOf()
+
+ public fun buildPort(meta: Meta): Port = portCache.getOrPut(meta) {
+ val type by meta.string { error("Port type is not defined") }
+ val factory = portFactories.values.firstOrNull { it.type == type }
+ ?: error("Port factory for type $type not found")
+ factory.build(context, meta)
+ }
+
+ public companion object : PluginFactory {
+
+ override val tag: PluginTag = PluginTag("controls.ports", group = PluginTag.DATAFORGE_GROUP)
+
+ override val type: KClass = Ports::class
+
+ override fun build(context: Context, meta: Meta): Ports = Ports()
+
+ }
+}
\ No newline at end of file
diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt
new file mode 100644
index 0000000..0ed4764
--- /dev/null
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt
@@ -0,0 +1,42 @@
+package space.kscience.controls.ports
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/**
+ * A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended.
+ * The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment.
+ */
+public class SynchronousPort(public val port: Port, private val mutex: Mutex) : Port by port {
+ /**
+ * Send a single message and wait for the flow of respond messages.
+ */
+ public suspend fun respond(data: ByteArray, transform: suspend Flow.() -> R): R = mutex.withLock {
+ port.send(data)
+ transform(port.receiving())
+ }
+}
+
+/**
+ * Provide a synchronous wrapper for a port
+ */
+public fun Port.synchronous(mutex: Mutex = Mutex()): SynchronousPort = SynchronousPort(this, mutex)
+
+/**
+ * Send request and read incoming data blocks until the delimiter is encountered
+ */
+public suspend fun SynchronousPort.respondWithDelimiter(
+ data: ByteArray,
+ delimiter: ByteArray,
+): ByteArray = respond(data) {
+ withDelimiter(delimiter).first()
+}
+
+public suspend fun SynchronousPort.respondStringWithDelimiter(
+ data: String,
+ delimiter: String,
+): String = respond(data.encodeToByteArray()) {
+ withStringDelimiter(delimiter).first()
+}
\ No newline at end of file
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/phrases.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt
similarity index 68%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/phrases.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt
index 62d075a..21afa8d 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/phrases.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt
@@ -1,4 +1,4 @@
-package ru.mipt.npm.controls.ports
+package space.kscience.controls.ports
import io.ktor.utils.io.core.BytePacketBuilder
import io.ktor.utils.io.core.readBytes
@@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.transform
/**
* Transform byte fragments into complete phrases using given delimiter. Not thread safe.
*/
-public fun Flow.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow {
+public fun Flow.withDelimiter(delimiter: ByteArray): Flow {
require(delimiter.isNotEmpty()) { "Delimiter must not be empty" }
- val output = BytePacketBuilder(expectedMessageSize)
+ val output = BytePacketBuilder()
var matcherPosition = 0
return transform { chunk ->
@@ -40,12 +40,11 @@ public fun Flow.withDelimiter(delimiter: ByteArray, expectedMessageSi
/**
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
*/
-public fun Flow.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow {
- return withDelimiter(delimiter.encodeToByteArray(), expectedMessageSize).map { it.decodeToString() }
+public fun Flow.withStringDelimiter(delimiter: String): Flow {
+ return withDelimiter(delimiter.encodeToByteArray()).map { it.decodeToString() }
}
/**
* A flow of delimited phrases
*/
-public suspend fun Port.delimitedIncoming(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow =
- receiving().withDelimiter(delimiter, expectedMessageSize)
+public fun Port.delimitedIncoming(delimiter: ByteArray): Flow = receiving().withDelimiter(delimiter)
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt
similarity index 65%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt
index c569cc3..d1fd9b7 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt
@@ -1,4 +1,4 @@
-package ru.mipt.npm.controls.properties
+package space.kscience.controls.spec
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
@@ -7,30 +7,28 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
-import ru.mipt.npm.controls.api.*
+import space.kscience.controls.api.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta
import kotlin.coroutines.CoroutineContext
-/**
- * A device generated from specification
- * @param D recursive self-type for properties and actions
- */
+
@OptIn(InternalDeviceAPI::class)
-public open class DeviceBySpec>(
- public val spec: DeviceSpec,
- context: Context = Global,
- meta: Meta = Meta.EMPTY
+public abstract class DeviceBase>(
+ override val context: Context = Global,
+ override val meta: Meta = Meta.EMPTY,
) : Device {
- override var context: Context = context
- internal set
- public var meta: Meta = meta
- internal set
+ /**
+ * Collection of property specifications
+ */
+ public abstract val properties: Map>
- public val properties: Map> get() = spec.properties
- public val actions: Map> get() = spec.actions
+ /**
+ * Collection of action specifications
+ */
+ public abstract val actions: Map>
override val propertyDescriptors: Collection
get() = properties.values.map { it.descriptor }
@@ -42,6 +40,9 @@ public open class DeviceBySpec>(
context.coroutineContext + SupervisorJob(context.coroutineContext[Job])
}
+ /**
+ * Logical state store
+ */
private val logicalState: HashMap = HashMap()
private val sharedMessageFlow: MutableSharedFlow = MutableSharedFlow()
@@ -68,6 +69,13 @@ public open class DeviceBySpec>(
}
}
+ /**
+ * Update logical state using given [spec] and its convertor
+ */
+ protected suspend fun updateLogical(spec: DevicePropertySpec, value: T) {
+ updateLogical(spec.name, spec.converter.objectToMeta(value))
+ }
+
/**
* Force read physical value and push an update if it is changed. It does not matter if logical state is present.
* The logical state is updated after read
@@ -98,17 +106,21 @@ public open class DeviceBySpec>(
}
override suspend fun execute(action: String, argument: Meta?): Meta? =
- actions[action]?.executeMeta(self, argument)
+ actions[action]?.executeWithMeta(self, argument)
/**
- * 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.
*/
- public suspend fun DevicePropertySpec.read(): T {
- val res = read(self)
+ public suspend fun DevicePropertySpec.readOrNull(): T? {
+ val res = read(self) ?: return null
updateLogical(name, converter.objectToMeta(res))
return res
}
+ public suspend fun DevicePropertySpec.read(): T =
+ readOrNull() ?: error("Failed to read property $name state")
+
public fun DevicePropertySpec.get(): T? = getProperty(name)?.let(converter::metaToObject)
/**
@@ -123,19 +135,37 @@ public open class DeviceBySpec>(
}
}
- override fun close() {
- with(spec) { self.onShutdown() }
+ /**
+ * Reset logical state of a property
+ */
+ public suspend fun DevicePropertySpec.invalidate() {
+ invalidate(name)
+ }
+
+ public suspend operator fun DeviceActionSpec.invoke(input: I? = null): O? = execute(self, input)
+
+}
+
+/**
+ * A device generated from specification
+ * @param D recursive self-type for properties and actions
+ */
+public open class DeviceBySpec>(
+ public val spec: DeviceSpec,
+ context: Context = Global,
+ meta: Meta = Meta.EMPTY,
+) : DeviceBase(context, meta) {
+ override val properties: Map> get() = spec.properties
+ override val actions: Map> get() = spec.actions
+
+ override suspend fun open(): Unit = with(spec) {
+ super.open()
+ self.onOpen()
+ }
+
+ override fun close(): Unit = with(spec) {
+ self.onClose()
super.close()
}
}
-public suspend fun , T : Any> D.read(
- propertySpec: DevicePropertySpec
-): T = propertySpec.read()
-
-public fun , T> D.write(
- propertySpec: WritableDevicePropertySpec,
- value: T
-): Job = launch {
- propertySpec.write(value)
-}
diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceMetaPropertySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceMetaPropertySpec.kt
new file mode 100644
index 0000000..809d940
--- /dev/null
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceMetaPropertySpec.kt
@@ -0,0 +1,15 @@
+package space.kscience.controls.spec
+
+import space.kscience.controls.api.Device
+import space.kscience.controls.api.PropertyDescriptor
+import space.kscience.dataforge.meta.Meta
+import space.kscience.dataforge.meta.transformations.MetaConverter
+
+internal object DeviceMetaPropertySpec: DevicePropertySpec {
+ override val descriptor: PropertyDescriptor = PropertyDescriptor("@meta")
+
+ override val converter: MetaConverter = MetaConverter.meta
+
+ @InternalDeviceAPI
+ override suspend fun read(device: Device): Meta = device.meta
+}
\ No newline at end of file
diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt
new file mode 100644
index 0000000..b545910
--- /dev/null
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt
@@ -0,0 +1,129 @@
+package space.kscience.controls.spec
+
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+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
+
+
+/**
+ * This API is internal and should not be used in user code
+ */
+@RequiresOptIn("This API should not be called outside of Device internals")
+public annotation class InternalDeviceAPI
+
+public interface DevicePropertySpec {
+ /**
+ * Property descriptor
+ */
+ public val descriptor: PropertyDescriptor
+
+ /**
+ * Meta item converter for resulting type
+ */
+ public val converter: MetaConverter
+
+ /**
+ * Read physical value from the given [device]
+ */
+ @InternalDeviceAPI
+ public suspend fun read(device: D): T?
+}
+
+/**
+ * Property name, should be unique in device
+ */
+public val DevicePropertySpec<*, *>.name: String get() = descriptor.name
+
+@OptIn(InternalDeviceAPI::class)
+public suspend fun DevicePropertySpec.readMeta(device: D): Meta? =
+ read(device)?.let(converter::objectToMeta)
+
+
+public interface WritableDevicePropertySpec : DevicePropertySpec {
+ /**
+ * Write physical value to a device
+ */
+ @InternalDeviceAPI
+ public suspend fun write(device: D, value: T)
+}
+
+@OptIn(InternalDeviceAPI::class)
+public suspend fun WritableDevicePropertySpec.writeMeta(device: D, item: Meta) {
+ write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
+}
+
+public interface DeviceActionSpec {
+ /**
+ * Action descriptor
+ */
+ public val descriptor: ActionDescriptor
+
+ public val inputConverter: MetaConverter
+
+ public val outputConverter: MetaConverter
+
+ /**
+ * Execute action on a device
+ */
+ public suspend fun execute(device: D, input: I?): O?
+}
+
+/**
+ * Action name, should be unique in device
+ */
+public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name
+
+public suspend fun DeviceActionSpec.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 : Any> D.read(
+ propertySpec: DevicePropertySpec,
+): T = propertySpec.read()
+
+public suspend fun D.read(
+ propertySpec: DevicePropertySpec,
+): T = propertySpec.converter.metaToObject(readProperty(propertySpec.name))
+ ?: error("Property meta converter returned null")
+
+public fun D.write(
+ propertySpec: WritableDevicePropertySpec,
+ value: T,
+): Job = launch {
+ writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
+}
+
+public fun , T> D.write(
+ propertySpec: WritableDevicePropertySpec,
+ value: T,
+): Job = launch {
+ propertySpec.write(value)
+}
+
+/**
+ * A type safe property change listener
+ */
+public fun Device.onPropertyChange(
+ spec: DevicePropertySpec,
+ callback: suspend PropertyChangedMessage.(T?) -> Unit,
+): Job = messageFlow
+ .filterIsInstance()
+ .filter { it.property == spec.name }
+ .onEach { change ->
+ change.callback(spec.converter.metaToObject(change.value))
+ }.launchIn(this)
\ No newline at end of file
diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt
similarity index 52%
rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt
rename to controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt
index 934220f..83364ee 100644
--- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt
@@ -1,10 +1,9 @@
-package ru.mipt.npm.controls.properties
+package space.kscience.controls.spec
import kotlinx.coroutines.withContext
-import ru.mipt.npm.controls.api.ActionDescriptor
-import ru.mipt.npm.controls.api.PropertyDescriptor
-import space.kscience.dataforge.context.Context
-import space.kscience.dataforge.context.Factory
+import space.kscience.controls.api.ActionDescriptor
+import space.kscience.controls.api.Device
+import space.kscience.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.PropertyDelegateProvider
@@ -14,28 +13,37 @@ import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
@OptIn(InternalDeviceAPI::class)
-public abstract class DeviceSpec>(
- private val buildDevice: () -> D
-) : Factory {
- private val _properties = HashMap>()
+public abstract class DeviceSpec {
+ //initializing meta property for everyone
+ private val _properties = hashMapOf>(
+ DeviceMetaPropertySpec.name to DeviceMetaPropertySpec
+ )
public val properties: Map> get() = _properties
private val _actions = HashMap>()
public val actions: Map> get() = _actions
- public fun > registerProperty(deviceProperty: P): P {
+
+ public open suspend fun D.onOpen() {
+ }
+
+ public open fun D.onClose() {
+ }
+
+
+ public fun > registerProperty(deviceProperty: P): P {
_properties[deviceProperty.name] = deviceProperty
return deviceProperty
}
- public fun registerProperty(
+ public fun registerProperty(
converter: MetaConverter,
readOnlyProperty: KProperty1,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {}
+ descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): DevicePropertySpec {
val deviceProperty = object : DevicePropertySpec {
- override val name: String = readOnlyProperty.name
- override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
+ override val descriptor: PropertyDescriptor =
+ PropertyDescriptor(readOnlyProperty.name).apply(descriptorBuilder)
override val converter: MetaConverter = converter
override suspend fun read(device: D): T =
withContext(device.coroutineContext) { readOnlyProperty.get(device) }
@@ -43,16 +51,39 @@ public abstract class DeviceSpec>(
return registerProperty(deviceProperty)
}
- public fun property(
+ public fun property(
+ converter: MetaConverter,
+ readOnlyProperty: KProperty1,
+ descriptorBuilder: PropertyDescriptor.() -> Unit = {},
+ ): PropertyDelegateProvider, ReadOnlyProperty>> =
+ PropertyDelegateProvider { _, property ->
+ val deviceProperty = object : DevicePropertySpec {
+ override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
+ //TODO add type from converter
+ writable = true
+ }.apply(descriptorBuilder)
+
+ override val converter: MetaConverter = converter
+
+ override suspend fun read(device: D): T = withContext(device.coroutineContext) {
+ readOnlyProperty.get(device)
+ }
+ }
+ registerProperty(deviceProperty)
+ ReadOnlyProperty { _, _ ->
+ deviceProperty
+ }
+ }
+
+ public fun mutableProperty(
converter: MetaConverter,
readWriteProperty: KMutableProperty1,
- descriptorBuilder: PropertyDescriptor.() -> Unit = {}
+ descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider, ReadOnlyProperty>> =
PropertyDelegateProvider { _, property ->
val deviceProperty = object : WritableDevicePropertySpec {
- override val name: String = property.name
- override val descriptor: PropertyDescriptor = PropertyDescriptor(name).apply {
+ override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
//TODO add type from converter
writable = true
}.apply(descriptorBuilder)
@@ -73,20 +104,19 @@ public abstract class DeviceSpec>(
}
}
- public fun property(
+ public fun property(
converter: MetaConverter,
- name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- read: suspend D.() -> T
+ name: String? = null,
+ read: suspend D.() -> T?,
): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> =
PropertyDelegateProvider { _: DeviceSpec, property ->
val propertyName = name ?: property.name
val deviceProperty = object : DevicePropertySpec {
- override val name: String = propertyName
- override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
+ override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
override val converter: MetaConverter = converter
- override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() }
+ override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
}
registerProperty(deviceProperty)
ReadOnlyProperty, DevicePropertySpec> { _, _ ->
@@ -94,21 +124,20 @@ public abstract class DeviceSpec>(
}
}
- public fun property(
+ public fun mutableProperty(
converter: MetaConverter,
- name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
- read: suspend D.() -> T,
- write: suspend D.(T) -> Unit
+ name: String? = null,
+ read: suspend D.() -> T?,
+ write: suspend D.(T) -> Unit,
): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> =
PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> ->
val propertyName = name ?: property.name
val deviceProperty = object : WritableDevicePropertySpec {
- override val name: String = propertyName
- override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
+ override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
override val converter: MetaConverter = converter
- override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() }
+ override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
device.write(value)
@@ -121,22 +150,21 @@ public abstract class DeviceSpec>(
}
- public fun registerAction(deviceAction: DeviceActionSpec): DeviceActionSpec {
+ public fun registerAction(deviceAction: DeviceActionSpec): DeviceActionSpec {
_actions[deviceAction.name] = deviceAction
return deviceAction
}
- public fun action(
+ public fun action(
inputConverter: MetaConverter,
outputConverter: MetaConverter,
- name: String? = null,
descriptorBuilder: ActionDescriptor.() -> Unit = {},
- execute: suspend D.(I?) -> O?
+ name: String? = null,
+ execute: suspend D.(I?) -> O?,
): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> =
PropertyDelegateProvider { _: DeviceSpec, property ->
val actionName = name ?: property.name
val deviceAction = object : DeviceActionSpec {
- override val name: String = actionName
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder)
override val inputConverter: MetaConverter = inputConverter
@@ -153,19 +181,68 @@ public abstract class DeviceSpec>(
}
/**
- * The function is executed right after device initialization is finished
+ * An action that takes [Meta] and returns [Meta]. No conversions are done
*/
- public open fun D.onStartup() {}
+ public fun metaAction(
+ descriptorBuilder: ActionDescriptor.() -> Unit = {},
+ name: String? = null,
+ execute: suspend D.(Meta?) -> Meta?,
+ ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> =
+ action(
+ MetaConverter.Companion.meta,
+ MetaConverter.Companion.meta,
+ descriptorBuilder,
+ name
+ ) {
+ execute(it)
+ }
/**
- * The function is executed before device is shut down
+ * An action that takes no parameters and returns no values
*/
- public open fun D.onShutdown() {}
-
-
- override fun invoke(meta: Meta, context: Context): D = buildDevice().apply {
- this.context = context
- this.meta = meta
- onStartup()
- }
+ public fun unitAction(
+ descriptorBuilder: ActionDescriptor.() -> Unit = {},
+ name: String? = null,
+ execute: suspend D.() -> Unit,
+ ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec