diff --git a/dataforge-device-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt b/dataforge-device-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt index 1db6841..d14f6c4 100644 --- a/dataforge-device-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt +++ b/dataforge-device-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt @@ -35,10 +35,10 @@ import kotlin.coroutines.CoroutineContext /** * Communicate with server in [Magix format](https://github.com/waltz-controls/rfc/tree/master/1) */ -class MagixClient( - val manager: DeviceManager, - val postUrl: Url, - val inbox: Flow +public class MagixClient( + private val manager: DeviceManager, + private val postUrl: Url, + private val inbox: Flow ): CoroutineScope { override val coroutineContext: CoroutineContext = manager.context.coroutineContext + Job(manager.context.coroutineContext[Job]) diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt index 990b42e..8c1f607 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt @@ -1,5 +1,6 @@ package hep.dataforge.control.api +import hep.dataforge.context.ContextAware import hep.dataforge.control.api.Device.Companion.DEVICE_TARGET import hep.dataforge.io.Envelope import hep.dataforge.io.EnvelopeBuilder @@ -14,7 +15,7 @@ import kotlinx.io.Closeable * General interface describing a managed Device */ @Type(DEVICE_TARGET) -public interface Device : Closeable { +public interface Device : Closeable, ContextAware { /** * List of supported property descriptors */ @@ -27,7 +28,7 @@ public interface Device : Closeable { public val actionDescriptors: Collection /** - * The scope encompassing all operations on a device. When canceled, cancels all running processes + * The supervisor scope encompassing all operations on a device. When canceled, cancels all running processes */ public val scope: CoroutineScope diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt index 7ffc574..626ce11 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt @@ -1,5 +1,6 @@ package hep.dataforge.control.base +import hep.dataforge.context.Context import hep.dataforge.control.api.ActionDescriptor import hep.dataforge.control.api.Device import hep.dataforge.control.api.DeviceListener @@ -14,7 +15,8 @@ import kotlinx.coroutines.sync.withLock /** * Baseline implementation of [Device] interface */ -public abstract class DeviceBase : Device { +public abstract class DeviceBase(override val context: Context) : Device { + private val _properties = HashMap() public val properties: Map get() = _properties private val _actions = HashMap() diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/actionDelegates.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/actionDelegates.kt index 7b34c42..877739e 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/actionDelegates.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/actionDelegates.kt @@ -31,29 +31,29 @@ private class ActionProvider( public fun DeviceBase.requesting( descriptorBuilder: ActionDescriptor.() -> Unit = {}, - block: suspend (MetaItem<*>?) -> MetaItem<*>?, -): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder, block) + action: suspend (MetaItem<*>?) -> MetaItem<*>?, +): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder, action) public fun D.requestingValue( descriptorBuilder: ActionDescriptor.() -> Unit = {}, - block: suspend (MetaItem<*>?) -> Any?, + action: suspend (MetaItem<*>?) -> Any?, ): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) { - val res = block(it) + val res = action(it) MetaItem.ValueItem(Value.of(res)) } public fun D.requestingMeta( descriptorBuilder: ActionDescriptor.() -> Unit = {}, - block: suspend MetaBuilder.(MetaItem<*>?) -> Unit, + action: suspend MetaBuilder.(MetaItem<*>?) -> Unit, ): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) { - val res = MetaBuilder().apply { block(it) } + val res = MetaBuilder().apply { action(it) } MetaItem.NodeItem(res) } public fun DeviceBase.acting( descriptorBuilder: ActionDescriptor.() -> Unit = {}, - block: suspend (MetaItem<*>?) -> Unit, + action: suspend (MetaItem<*>?) -> Unit, ): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) { - block(it) + action(it) null } \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/devicePropertyDelegates.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/devicePropertyDelegates.kt index 3cf044e..2ad215a 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/devicePropertyDelegates.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/devicePropertyDelegates.kt @@ -68,7 +68,7 @@ public fun DeviceBase.readingNumber( ) public fun DeviceBase.readingString( - default: Number? = null, + default: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, getter: suspend () -> String, ): PropertyDelegateProvider = ReadOnlyDevicePropertyProvider( @@ -81,6 +81,20 @@ public fun DeviceBase.readingString( } ) +public fun DeviceBase.readingBoolean( + default: Boolean? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + getter: suspend () -> Boolean, +): PropertyDelegateProvider = ReadOnlyDevicePropertyProvider( + this, + default?.let { MetaItem.ValueItem(it.asValue()) }, + descriptorBuilder, + getter = { + val boolean = getter() + MetaItem.ValueItem(boolean.asValue()) + } +) + public fun DeviceBase.readingMeta( default: Meta? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/misc.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/misc.kt index e627c18..ad9fc87 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/misc.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/misc.kt @@ -3,4 +3,4 @@ package hep.dataforge.control.base import hep.dataforge.meta.MetaItem import hep.dataforge.values.asValue -fun Double.asMetaItem(): MetaItem.ValueItem = MetaItem.ValueItem(asValue()) \ No newline at end of file +public fun Double.asMetaItem(): MetaItem.ValueItem = MetaItem.ValueItem(asValue()) \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt index 6607c5c..eabe36b 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt @@ -9,22 +9,21 @@ import hep.dataforge.io.Consumer import hep.dataforge.io.Envelope import hep.dataforge.io.Responder import hep.dataforge.io.SimpleEnvelope -import hep.dataforge.meta.MetaItem -import hep.dataforge.meta.get -import hep.dataforge.meta.string -import hep.dataforge.meta.wrap +import hep.dataforge.meta.* import hep.dataforge.names.Name import hep.dataforge.names.toName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.launch import kotlinx.io.Binary -class DeviceController( - val device: Device, - val deviceTarget: String, - val scope: CoroutineScope = device.scope +@OptIn(DFExperimental::class) +public class DeviceController( + public val device: Device, + public val deviceTarget: String, + public val scope: CoroutineScope = device.scope ) : Responder, Consumer, DeviceListener { init { @@ -33,7 +32,7 @@ class DeviceController( private val outputChannel = Channel(Channel.CONFLATED) - suspend fun respondMessage(message: DeviceMessage): DeviceMessage = respondMessage(device, deviceTarget, message) + public suspend fun respondMessage(message: DeviceMessage): DeviceMessage = respondMessage(device, deviceTarget, message) override suspend fun respond(request: Envelope): Envelope = respond(device, deviceTarget, request) @@ -54,8 +53,9 @@ class DeviceController( } } - fun output() = outputChannel.consumeAsFlow() + public fun recieving(): Flow = outputChannel.consumeAsFlow() + @DFExperimental override fun consume(message: Envelope) { // Fire the respond procedure and forget about the result scope.launch { @@ -63,12 +63,12 @@ class DeviceController( } } - companion object { - const val GET_PROPERTY_ACTION = "read" - const val SET_PROPERTY_ACTION = "write" - const val EXECUTE_ACTION = "execute" - const val PROPERTY_LIST_ACTION = "propertyList" - const val ACTION_LIST_ACTION = "actionList" + public companion object { + public const val GET_PROPERTY_ACTION: String = "read" + public const val SET_PROPERTY_ACTION: String = "write" + public const val EXECUTE_ACTION: String = "execute" + public const val PROPERTY_LIST_ACTION: String = "propertyList" + public const val ACTION_LIST_ACTION: String = "actionList" internal suspend fun respond(device: Device, deviceTarget: String, request: Envelope): Envelope { val target = request.meta["target"].string @@ -166,7 +166,7 @@ class DeviceController( } -suspend fun DeviceHub.respondMessage(request: DeviceMessage): DeviceMessage { +public suspend fun DeviceHub.respondMessage(request: DeviceMessage): DeviceMessage { return try { val targetName = request.target?.toName() ?: Name.EMPTY val device = this[targetName] ?: error("The device with name $targetName not found in $this") diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt index 3a650d5..f316382 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt @@ -11,7 +11,7 @@ import hep.dataforge.names.Name import hep.dataforge.names.NameToken import kotlin.reflect.KClass -class DeviceManager : AbstractPlugin(), DeviceHub { +public class DeviceManager : AbstractPlugin(), DeviceHub { override val tag: PluginTag get() = Companion.tag /** @@ -20,17 +20,17 @@ class DeviceManager : AbstractPlugin(), DeviceHub { private val top = HashMap() override val devices: Map get() = top - val controller by lazy { + public val controller: HubController by lazy { HubController(this, context) } - fun registerDevice(name: NameToken, device: Device) { + public fun registerDevice(name: NameToken, device: Device) { top[name] = device } override fun provideTop(target: String): Map = super.provideTop(target) - companion object : PluginFactory { + public companion object : PluginFactory { override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP) override val type: KClass = DeviceManager::class @@ -39,5 +39,5 @@ class DeviceManager : AbstractPlugin(), DeviceHub { } -val Context.devices: DeviceManager get() = plugins.fetch(DeviceManager) +public val Context.devices: DeviceManager get() = plugins.fetch(DeviceManager) diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceMessage.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceMessage.kt index 4877daf..3533110 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceMessage.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceMessage.kt @@ -3,19 +3,20 @@ package hep.dataforge.control.controllers import hep.dataforge.control.controllers.DeviceController.Companion.GET_PROPERTY_ACTION import hep.dataforge.io.SimpleEnvelope import hep.dataforge.meta.* +import hep.dataforge.names.Name import hep.dataforge.names.asName import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -class DeviceMessage : Scheme() { - var source by string(key = SOURCE_KEY) - var target by string(key = TARGET_KEY) - var type by string(default = GET_PROPERTY_ACTION, key = MESSAGE_TYPE_KEY) - var comment by string() - var status by string(RESPONSE_OK_STATUS) - var data: List +public class DeviceMessage : Scheme() { + public var source: String? by string(key = SOURCE_KEY) + public var target: String? by string(key = TARGET_KEY) + public var type: String by string(default = GET_PROPERTY_ACTION, key = MESSAGE_TYPE_KEY) + public var comment: String? by string() + public var status: String by string(RESPONSE_OK_STATUS) + public var data: List get() = config.getIndexed(MESSAGE_DATA_KEY).values.map { MessageData.wrap(it.node!!) } set(value) { config[MESSAGE_DATA_KEY] = value.map { it.config } @@ -24,27 +25,27 @@ class DeviceMessage : Scheme() { /** * Append a payload to this message according to the given scheme */ - fun append(spec: Specification, block: T.() -> Unit): T = + public fun append(spec: Specification, block: T.() -> Unit): T = spec.invoke(block).also { config.append(MESSAGE_DATA_KEY, it) } - companion object : SchemeSpec(::DeviceMessage), KSerializer { - val SOURCE_KEY = "source".asName() - val TARGET_KEY = "target".asName() - val MESSAGE_TYPE_KEY = "type".asName() - val MESSAGE_DATA_KEY = "data".asName() + public companion object : SchemeSpec(::DeviceMessage), KSerializer { + public val SOURCE_KEY: Name = "source".asName() + public val TARGET_KEY: Name = "target".asName() + public val MESSAGE_TYPE_KEY: Name = "type".asName() + public val MESSAGE_DATA_KEY: Name = "data".asName() - const val RESPONSE_OK_STATUS = "response.OK" - const val RESPONSE_FAIL_STATUS = "response.FAIL" - const val PROPERTY_CHANGED_ACTION = "event.propertyChange" + public const val RESPONSE_OK_STATUS: String = "response.OK" + public const val RESPONSE_FAIL_STATUS: String = "response.FAIL" + public const val PROPERTY_CHANGED_ACTION: String = "event.propertyChange" - inline fun ok( + public inline fun ok( request: DeviceMessage? = null, block: DeviceMessage.() -> Unit = {} ): DeviceMessage = DeviceMessage { target = request?.source }.apply(block) - inline fun fail( + public inline fun fail( request: DeviceMessage? = null, block: DeviceMessage.() -> Unit = {} ): DeviceMessage = DeviceMessage { @@ -65,16 +66,16 @@ class DeviceMessage : Scheme() { } } -class MessageData : Scheme() { - var name by string { error("Property name could not be empty") } - var value by item(key = DATA_VALUE_KEY) +public class MessageData : Scheme() { + public var name: String by string { error("Property name could not be empty") } + public var value: MetaItem<*>? by item(key = DATA_VALUE_KEY) - companion object : SchemeSpec(::MessageData) { - val DATA_VALUE_KEY = "value".asName() + public companion object : SchemeSpec(::MessageData) { + public val DATA_VALUE_KEY: Name = "value".asName() } } @DFBuilder -fun DeviceMessage.data(block: MessageData.() -> Unit): MessageData = append(MessageData, block) +public fun DeviceMessage.data(block: MessageData.() -> Unit): MessageData = append(MessageData, block) -fun DeviceMessage.wrap() = SimpleEnvelope(this.config, null) +public fun DeviceMessage.wrap(): SimpleEnvelope = SimpleEnvelope(this.config, null) diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/HubController.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/HubController.kt index 183ea5b..e31d7ed 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/HubController.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/HubController.kt @@ -6,31 +6,31 @@ import hep.dataforge.control.api.get import hep.dataforge.io.Consumer import hep.dataforge.io.Envelope import hep.dataforge.io.Responder -import hep.dataforge.meta.MetaItem -import hep.dataforge.meta.get -import hep.dataforge.meta.string -import hep.dataforge.meta.wrap +import hep.dataforge.meta.* import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.toName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -class HubController( - val hub: DeviceHub, - val scope: CoroutineScope + +@OptIn(DFExperimental::class) +public class HubController( + public val hub: DeviceHub, + public val scope: CoroutineScope ) : Consumer, Responder { private val messageOutbox = Channel(Channel.CONFLATED) private val envelopeOutbox = Channel(Channel.CONFLATED) - fun messageOutput() = messageOutbox.consumeAsFlow() + public fun messageOutput(): Flow = messageOutbox.consumeAsFlow() - fun envelopeOutput() = envelopeOutbox.consumeAsFlow() + public fun envelopeOutput(): Flow = envelopeOutbox.consumeAsFlow() private val packJob = scope.launch { while (isActive) { @@ -61,7 +61,7 @@ class HubController( } } - suspend fun respondMessage(message: DeviceMessage): DeviceMessage = try { + public suspend fun respondMessage(message: DeviceMessage): DeviceMessage = try { val targetName = message.target?.toName() ?: Name.EMPTY val device = hub[targetName] ?: error("The device with name $targetName not found in $hub") DeviceController.respondMessage(device, targetName.toString(), message) diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/PropertyFlow.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/PropertyFlow.kt index ed45c53..e07f8a4 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/PropertyFlow.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/PropertyFlow.kt @@ -9,9 +9,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.launch - -@ExperimentalCoroutinesApi -suspend fun Device.flowValues(): Flow>> = callbackFlow { +@OptIn(ExperimentalCoroutinesApi::class) +public suspend fun Device.flowValues(): Flow>> = callbackFlow { val listener = object : DeviceListener { override fun propertyChanged(propertyName: String, value: MetaItem<*>?) { if (value != null) { diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt index 7f1b41c..1472f27 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt @@ -2,28 +2,32 @@ package hep.dataforge.control.controllers import hep.dataforge.control.base.DeviceProperty import hep.dataforge.control.base.ReadOnlyDeviceProperty -import hep.dataforge.meta.MetaItem +import hep.dataforge.control.base.asMetaItem +import hep.dataforge.meta.* import hep.dataforge.meta.transformations.MetaConverter import hep.dataforge.values.Null +import hep.dataforge.values.double import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration -operator fun ReadOnlyDeviceProperty.getValue(thisRef: Any?, property: KProperty<*>): MetaItem<*> = +public operator fun ReadOnlyDeviceProperty.getValue(thisRef: Any?, property: KProperty<*>): MetaItem<*> = value ?: MetaItem.ValueItem(Null) -operator fun DeviceProperty.setValue(thisRef: Any?, property: KProperty<*>, value: MetaItem<*>) { +public operator fun DeviceProperty.setValue(thisRef: Any?, property: KProperty<*>, value: MetaItem<*>) { this.value = value } -fun ReadOnlyDeviceProperty.convert(metaConverter: MetaConverter): ReadOnlyProperty { +public fun ReadOnlyDeviceProperty.convert(metaConverter: MetaConverter): ReadOnlyProperty { return ReadOnlyProperty { thisRef, property -> getValue(thisRef, property).let { metaConverter.itemToObject(it) } } } -fun DeviceProperty.convert(metaConverter: MetaConverter): ReadWriteProperty { +public fun DeviceProperty.convert(metaConverter: MetaConverter): ReadWriteProperty { return object : ReadWriteProperty { override fun getValue(thisRef: Any?, property: KProperty<*>): T { return this@convert.getValue(thisRef, property).let { metaConverter.itemToObject(it) } @@ -35,14 +39,30 @@ fun DeviceProperty.convert(metaConverter: MetaConverter): ReadWrite } } -fun ReadOnlyDeviceProperty.double() = convert(MetaConverter.double) -fun DeviceProperty.double() = convert(MetaConverter.double) +public fun ReadOnlyDeviceProperty.double(): ReadOnlyProperty = convert(MetaConverter.double) +public fun DeviceProperty.double(): ReadWriteProperty = convert(MetaConverter.double) -fun ReadOnlyDeviceProperty.int() = convert(MetaConverter.int) -fun DeviceProperty.int() = convert(MetaConverter.int) +public fun ReadOnlyDeviceProperty.int(): ReadOnlyProperty = convert(MetaConverter.int) +public fun DeviceProperty.int(): ReadWriteProperty = convert(MetaConverter.int) -fun ReadOnlyDeviceProperty.string() = convert(MetaConverter.string) -fun DeviceProperty.string() = convert(MetaConverter.string) +public fun ReadOnlyDeviceProperty.string(): ReadOnlyProperty = convert(MetaConverter.string) +public fun DeviceProperty.string(): ReadWriteProperty = convert(MetaConverter.string) -fun ReadOnlyDeviceProperty.duration(): ReadOnlyProperty = TODO() -fun DeviceProperty.duration(): ReadWriteProperty = TODO() \ No newline at end of file +//TODO to be moved to DF +private object DurationConverter : MetaConverter { + override fun itemToObject(item: MetaItem<*>): Duration = when (item) { + is MetaItem.NodeItem -> { + val unit: DurationUnit = item.node["unit"].enum() ?: DurationUnit.SECONDS + val value = item.node[Meta.VALUE_KEY].double ?: error("No value present for Duration") + value.toDuration(unit) + } + is MetaItem.ValueItem -> item.value.double.toDuration(DurationUnit.SECONDS) + } + + override fun objectToMetaItem(obj: Duration): MetaItem<*> = obj.toDouble(DurationUnit.SECONDS).asMetaItem() +} + +public val MetaConverter.Companion.duration: MetaConverter get() = DurationConverter + +public fun ReadOnlyDeviceProperty.duration(): ReadOnlyProperty = convert(DurationConverter) +public fun DeviceProperty.duration(): ReadWriteProperty = convert(DurationConverter) \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/Port.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/Port.kt index 635961e..5d23142 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/Port.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/Port.kt @@ -1,26 +1,26 @@ package hep.dataforge.control.ports +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.context.Factory import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.io.Closeable -import mu.KLogger -import mu.KotlinLogging import kotlin.coroutines.CoroutineContext -interface Port: Closeable { - suspend fun send(data: ByteArray) - suspend fun receiving(): Flow - fun isOpen(): Boolean +public interface Port: Closeable, ContextAware { + public suspend fun send(data: ByteArray) + public suspend fun receiving(): Flow + public fun isOpen(): Boolean } +public typealias PortFactory = Factory -abstract class AbstractPort(parentContext: CoroutineContext) : Port { +public abstract class AbstractPort(override val context: Context, parentContext: CoroutineContext = context.coroutineContext) : Port { - protected val scope = CoroutineScope(SupervisorJob(parentContext[Job])) - - protected val logger: KLogger by lazy { KotlinLogging.logger(toString()) } + protected val scope: CoroutineScope = CoroutineScope(parentContext + SupervisorJob(parentContext[Job])) private val outgoing = Channel(100) private val incoming = Channel(Channel.CONFLATED) @@ -87,4 +87,4 @@ abstract class AbstractPort(parentContext: CoroutineContext) : Port { /** * Send UTF-8 encoded string */ -suspend fun Port.send(string: String) = send(string.encodeToByteArray()) \ No newline at end of file +public suspend fun Port.send(string: String): Unit = send(string.encodeToByteArray()) \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/PortProxy.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/PortProxy.kt index 2849d74..91b474f 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/PortProxy.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/PortProxy.kt @@ -1,36 +1,70 @@ package hep.dataforge.control.ports +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.context.Global +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.isActive import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -class PortProxy(val factory: suspend () -> Port) : Port { +/** + * A port that could be closed multiple times and opens automatically on request + */ +public class PortProxy(override val context: Context = Global, public val factory: suspend () -> Port) : Port, ContextAware { private var actualPort: Port? = null - private val mutex = Mutex() + private val mutex: Mutex = Mutex() - suspend fun port(): Port{ + private suspend fun port(): Port { return mutex.withLock { - if(actualPort?.isOpen() == true){ + if (actualPort?.isOpen() == true) { actualPort!! } else { - factory().also{ + factory().also { actualPort = it } } } } + /** + * Ensure that the port is open. If it is already open, does nothing. Otherwise, open a new port. + */ + public suspend fun open() { + port()//ignore result + } + override suspend fun send(data: ByteArray) { port().send(data) } - override suspend fun receiving(): Flow = port().receiving() + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun receiving(): Flow = channelFlow { + while (isActive) { + try { + //recreate port and Flow on cancel + port().receiving().collect { + send(it) + } + } catch (t: Throwable) { + logger.warn(t){"Port read failed. Reconnecting."} + //cancel +// if (t is CancellationException) { +// cancel(t) +// } + } + } + }// port().receiving() // open by default override fun isOpen(): Boolean = true override fun close() { actualPort?.close() + actualPort = null } } \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/SynchronousPortHandler.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/SynchronousPortHandler.kt index 1cbcb78..a599378 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/SynchronousPortHandler.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/SynchronousPortHandler.kt @@ -10,13 +10,13 @@ import kotlinx.coroutines.sync.withLock * The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment. * */ -class SynchronousPortHandler(val port: Port) { +public class SynchronousPortHandler(public val port: Port) { private val mutex = Mutex() /** * Send a single message and wait for the flow of respond messages. */ - suspend fun respond(data: ByteArray, transform: suspend Flow.() -> R): R { + public suspend fun respond(data: ByteArray, transform: suspend Flow.() -> R): R { return mutex.withLock { port.send(data) transform(port.receiving()) @@ -27,7 +27,7 @@ class SynchronousPortHandler(val port: Port) { /** * Send request and read incoming data blocks until the delimiter is encountered */ -suspend fun SynchronousPortHandler.respondWithDelimiter(data: ByteArray, delimiter: ByteArray): ByteArray { +public suspend fun SynchronousPortHandler.respondWithDelimiter(data: ByteArray, delimiter: ByteArray): ByteArray { return respond(data) { withDelimiter(delimiter).first() } diff --git a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt b/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt index 0ec61a1..90ab9da 100644 --- a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt +++ b/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt @@ -1,9 +1,15 @@ package hep.dataforge.control.ports +import hep.dataforge.context.Context +import hep.dataforge.meta.Meta +import hep.dataforge.meta.get +import hep.dataforge.meta.int +import hep.dataforge.meta.string import io.ktor.network.selector.ActorSelectorManager import io.ktor.network.sockets.aSocket import io.ktor.network.sockets.openReadChannel import io.ktor.network.sockets.openWriteChannel +import io.ktor.util.KtorExperimentalAPI import io.ktor.utils.io.consumeEachBufferRange import io.ktor.utils.io.writeAvailable import kotlinx.coroutines.Dispatchers @@ -12,16 +18,17 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.net.InetSocketAddress import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.coroutineContext -class KtorTcpPort internal constructor( - parentContext: CoroutineContext, - val host: String, - val port: Int -) : AbstractPort(parentContext), AutoCloseable { +public class KtorTcpPort internal constructor( + context: Context, + public val host: String, + public val port: Int, + parentContext: CoroutineContext = context.coroutineContext, +) : AbstractPort(context, parentContext), AutoCloseable { - override fun toString() = "port[tcp:$host:$port]" + override fun toString(): String = "port[tcp:$host:$port]" + @OptIn(KtorExperimentalAPI::class) private val futureSocket = scope.async { aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(InetSocketAddress(host, port)) } @@ -50,9 +57,20 @@ class KtorTcpPort internal constructor( super.close() } - companion object { - suspend fun open(host: String, port: Int): KtorTcpPort { - return KtorTcpPort(coroutineContext, host, port) + public companion object: PortFactory { + public fun open( + context: Context, + host: String, + port: Int, + coroutineContext: CoroutineContext = context.coroutineContext, + ): KtorTcpPort { + return KtorTcpPort(context, host, port, coroutineContext) + } + + override fun invoke(meta: Meta, context: Context): Port { + val host = meta["host"].string ?: "localhost" + val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta") + return open(context, host, port) } } } \ No newline at end of file diff --git a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/TcpPort.kt b/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/TcpPort.kt index 2049c3c..352b977 100644 --- a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/TcpPort.kt +++ b/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/TcpPort.kt @@ -1,11 +1,15 @@ package hep.dataforge.control.ports +import hep.dataforge.context.Context +import hep.dataforge.meta.Meta +import hep.dataforge.meta.get +import hep.dataforge.meta.int +import hep.dataforge.meta.string import kotlinx.coroutines.* import java.net.InetSocketAddress import java.nio.ByteBuffer import java.nio.channels.SocketChannel import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.coroutineContext internal fun ByteBuffer.readArray(limit: Int = limit()): ByteArray { rewind() @@ -15,13 +19,14 @@ internal fun ByteBuffer.readArray(limit: Int = limit()): ByteArray { return response } -class TcpPort private constructor( - parentContext: CoroutineContext, - val host: String, - val port: Int -) : AbstractPort(parentContext), AutoCloseable { +public class TcpPort private constructor( + context: Context, + public val host: String, + public val port: Int, + parentContext: CoroutineContext = context.coroutineContext, +) : AbstractPort(context, parentContext), AutoCloseable { - override fun toString(): String = "port[tcp:$host:$port]" + override fun toString(): String = "port[tcp:$host:$port]" private val futureChannel: Deferred = this.scope.async(Dispatchers.IO) { SocketChannel.open(InetSocketAddress(host, port)).apply { @@ -32,7 +37,7 @@ class TcpPort private constructor( /** * A handler to await port connection */ - val startJob: Job get() = futureChannel + public val startJob: Job get() = futureChannel private val listenerJob = this.scope.launch { val channel = futureChannel.await() @@ -61,9 +66,20 @@ class TcpPort private constructor( super.close() } - companion object{ - suspend fun open(host: String, port: Int): TcpPort{ - return TcpPort(coroutineContext, host, port) + public companion object : PortFactory { + public fun open( + context: Context, + host: String, + port: Int, + coroutineContext: CoroutineContext = context.coroutineContext, + ): TcpPort { + return TcpPort(context, host, port, coroutineContext) + } + + override fun invoke(meta: Meta, context: Context): Port { + val host = meta["host"].string ?: "localhost" + val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta") + return open(context, host, port) } } } \ No newline at end of file diff --git a/dataforge-device-core/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt b/dataforge-device-core/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt index 0f25b53..ed1621d 100644 --- a/dataforge-device-core/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt +++ b/dataforge-device-core/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt @@ -1,5 +1,6 @@ package hep.dataforge.control.ports +import hep.dataforge.context.Global import io.ktor.network.selector.ActorSelectorManager import io.ktor.network.sockets.aSocket import io.ktor.network.sockets.openReadChannel @@ -53,9 +54,9 @@ class TcpPortTest { @Test fun testWithEchoServer() { try { - runBlocking{ + runBlocking { val server = launchEchoServer(22188) - val port = TcpPort.open("localhost", 22188) + val port = TcpPort.open(Global, "localhost", 22188) val logJob = launch { port.receiving().collect { @@ -78,9 +79,9 @@ class TcpPortTest { @Test fun testKtorWithEchoServer() { try { - runBlocking{ + runBlocking { val server = launchEchoServer(22188) - val port = KtorTcpPort.open("localhost", 22188) + val port = KtorTcpPort.open(Global,"localhost", 22188) val logJob = launch { port.receiving().collect { diff --git a/dataforge-device-serial/src/main/kotlin/hep/dataforge/control/serial/SerialPort.kt b/dataforge-device-serial/src/main/kotlin/hep/dataforge/control/serial/SerialPort.kt index 989aa1a..18b5e1f 100644 --- a/dataforge-device-serial/src/main/kotlin/hep/dataforge/control/serial/SerialPort.kt +++ b/dataforge-device-serial/src/main/kotlin/hep/dataforge/control/serial/SerialPort.kt @@ -1,17 +1,25 @@ package hep.dataforge.control.serial +import hep.dataforge.context.Context import hep.dataforge.control.ports.AbstractPort +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.meta.Meta +import hep.dataforge.meta.int +import hep.dataforge.meta.string import jssc.SerialPort.* import jssc.SerialPortEventListener import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.coroutineContext import jssc.SerialPort as JSSCPort /** * COM/USB port */ -public class SerialPort private constructor(parentContext: CoroutineContext, private val jssc: JSSCPort) : - AbstractPort(parentContext) { +public class SerialPort private constructor( + context: Context, + private val jssc: JSSCPort, + parentContext: CoroutineContext = context.coroutineContext, +) : AbstractPort(context, parentContext) { override fun toString(): String = "port[${jssc.portName}]" @@ -48,23 +56,34 @@ public class SerialPort private constructor(parentContext: CoroutineContext, pri super.close() } - public companion object { + public companion object : PortFactory { /** * Construct ComPort with given parameters */ - public suspend fun open( + public fun open( + context: Context, portName: String, baudRate: Int = BAUDRATE_9600, dataBits: Int = DATABITS_8, stopBits: Int = STOPBITS_1, parity: Int = PARITY_NONE, + coroutineContext: CoroutineContext = context.coroutineContext, ): SerialPort { val jssc = JSSCPort(portName).apply { openPort() setParams(baudRate, dataBits, stopBits, parity) } - return SerialPort(coroutineContext, jssc) + return SerialPort(context, jssc, coroutineContext) + } + + override fun invoke(meta: Meta, context: Context): Port { + val name by meta.string { error("Serial port name not defined") } + val baudRate by meta.int(BAUDRATE_9600) + val dataBits by meta.int(DATABITS_8) + val stopBits by meta.int(STOPBITS_1) + val parity by meta.int(PARITY_NONE) + return open(context, name, baudRate, dataBits, stopBits, parity) } } } \ No newline at end of file diff --git a/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt b/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt index 220d0a9..abe1d8a 100644 --- a/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt +++ b/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt @@ -17,12 +17,12 @@ import kotlin.time.ExperimentalTime import kotlin.time.seconds @OptIn(ExperimentalTime::class) -class DemoDevice(parentScope: CoroutineScope) : DeviceBase() { +class DemoDevice(context: Context) : DeviceBase(context) { private val executor = Executors.newSingleThreadExecutor() override val scope: CoroutineScope = CoroutineScope( - parentScope.coroutineContext + executor.asCoroutineDispatcher() + Job(parentScope.coroutineContext[Job]) + context.coroutineContext + executor.asCoroutineDispatcher() + Job(context.coroutineContext[Job]) ) val timeScale: DeviceProperty by writingVirtual(5000.0.asValue()) diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt index af5d831..a9418e1 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -1,6 +1,8 @@ package ru.mipt.npm.devices.pimotionmaster +import hep.dataforge.context.Context import hep.dataforge.control.api.DeviceHub +import hep.dataforge.control.api.PropertyDescriptor import hep.dataforge.control.base.* import hep.dataforge.control.controllers.duration import hep.dataforge.control.ports.Port @@ -12,6 +14,7 @@ import hep.dataforge.names.NameToken import hep.dataforge.values.Null import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.toList @@ -22,13 +25,13 @@ import kotlin.time.Duration public class PiMotionMasterDevice( - parentScope: CoroutineScope, + context: Context, axes: List, private val portFactory: suspend (MetaItem<*>?) -> Port, -) : DeviceBase(), DeviceHub { +) : DeviceBase(context), DeviceHub { override val scope: CoroutineScope = CoroutineScope( - parentScope.coroutineContext + Job(parentScope.coroutineContext[Job]) + context.coroutineContext + SupervisorJob(context.coroutineContext[Job]) ) public val port: DeviceProperty by writingVirtual(Null) { @@ -92,39 +95,90 @@ public class PiMotionMasterDevice( request("VER?").first() } - public inner class Axis(public val axisId: String) : DeviceBase() { + public val stop: Action by acting( + descriptorBuilder = { + info = "Stop all axis" + }, + action = { send("STP") } + ) + + public inner class Axis(public val axisId: String) : DeviceBase(context) { override val scope: CoroutineScope get() = this@PiMotionMasterDevice.scope - public val enabled: DeviceProperty by writingBoolean( - getter = { - val eax = requestAndParse("EAX?", axisId)[axisId]?.toIntOrNull() - ?: error("Malformed EAX response. Should include integer value for $axisId") - eax != 0 - }, - setter = { _, newValue -> - val value = if (newValue) { - "1" - } else { - "0" - } - send("EAX", axisId, value) - newValue + + private suspend fun readAxisBoolean(command: String): Boolean = + requestAndParse(command, axisId)[axisId]?.toIntOrNull() + ?: error("Malformed $command response. Should include integer value for $axisId") != 0 + + private suspend fun writeAxisBoolean(command: String, value: Boolean): Boolean { + val boolean = if (value) { + "1" + } else { + "0" } - ) + send(command, axisId, boolean) + return value + } + + private fun axisBooleanProperty(command: String, descriptorBuilder: PropertyDescriptor.() -> Unit = {}) = + writingBoolean( + getter = { readAxisBoolean("$command?") }, + setter = { _, newValue -> writeAxisBoolean(command, newValue) }, + descriptorBuilder = descriptorBuilder + ) + + private fun axisNumberProperty(command: String, descriptorBuilder: PropertyDescriptor.() -> Unit = {}) = + writingDouble( + getter = { + requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull() + ?: error("Malformed $command response. Should include float value for $axisId") + }, + setter = { _, newValue -> + send(command, axisId, newValue.toString()) + newValue + }, + descriptorBuilder = descriptorBuilder + ) + + public val enabled: DeviceProperty by axisBooleanProperty("EAX") { + info = "Motor enable state." + } public val halt: Action by acting { send("HLT", axisId) } - public val targetPosition: DeviceProperty by writingDouble( - getter = { - requestAndParse("MOV?", axisId)[axisId]?.toDoubleOrNull() - ?: error("Malformed MOV response. Should include float value for $axisId") + public val targetPosition: DeviceProperty by axisNumberProperty("MOV") { + info = """ + Sets a new absolute target position for the specified axis. + Servo mode must be switched on for the commanded axis prior to using this command (closed-loop operation). + """.trimIndent() + } + + public val onTarget: ReadOnlyDeviceProperty by readingBoolean( + descriptorBuilder = { + info = "Queries the on-target state of the specified axis." }, - setter = { _, newValue -> - send("MOV", axisId, newValue.toString()) - newValue + getter = { + readAxisBoolean("ONT?") } ) + + public val position: DeviceProperty by axisNumberProperty("POS") { + info = "The current axis position." + } + + public val openLoopTarget: DeviceProperty by axisNumberProperty("OMA") { + info = "Position for open-loop operation." + } + + public val closedLoop: DeviceProperty by axisBooleanProperty("SVO") { + info = "Servo closed loop mode" + } + + public val velocity: DeviceProperty by axisNumberProperty("VEL") { + info = "Velocity value for closed-loop operation" + } + } override val devices: Map = axes.associate { NameToken(it) to Axis(it) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 40fd8a0..62a0213 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ pluginManagement { - val kotlinVersion = "1.4.0" - val toolsVersion = "0.6.0-dev-4" + val kotlinVersion = "1.4.10" + val toolsVersion = "0.6.0" repositories { mavenLocal()