diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a258d7..2c7ea2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,14 @@ ### Added - Value averaging plot extension - PLC4X bindings +- Shortcuts to access all Controls devices in a magix network. +- `DeviceClient` properly evaluates lifecycle and logs ### Changed -- Constructor properties return `DeviceStat` in order to be able to subscribe to them +- Constructor properties return `DeviceState` in order to be able to subscribe to them - Refactored ports. Now we have `AsynchronousPort` as well as `SynchronousPort` +- `DeviceClient` now initializes property and action descriptors eagerly. +- `DeviceHub` now works with `Name` instead of `NameToken`. Tree-like structure is made using `Path`. Device messages no longer have access to sub-devices. ### Deprecated diff --git a/build.gradle.kts b/build.gradle.kts index 38d5c6a..0f00407 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { allprojects { group = "space.kscience" - version = "0.4.0-dev-1" + version = "0.4.0-dev-2" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt index 6785b4b..ef886dc 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -10,9 +10,14 @@ import space.kscience.controls.api.DeviceLifecycleState.* import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install import space.kscience.dataforge.context.* -import space.kscience.dataforge.meta.* +import space.kscience.dataforge.meta.Laminate +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.meta.MutableMeta import space.kscience.dataforge.misc.DFExperimental -import space.kscience.dataforge.names.* +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.get +import space.kscience.dataforge.names.parseAsName import kotlin.collections.set import kotlin.coroutines.CoroutineContext @@ -60,15 +65,15 @@ public open class DeviceGroup( ) - private val _devices = hashMapOf() + private val _devices = hashMapOf() - override val devices: Map = _devices + override val devices: Map = _devices /** * Register and initialize (synchronize child's lifecycle state with group state) a new device in this group */ @OptIn(DFExperimental::class) - public open fun install(token: NameToken, device: D): D { + public open fun install(token: Name, device: D): D { require(_devices[token] == null) { "A child device with name $token already exists" } //start the child device if needed if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() } @@ -175,35 +180,16 @@ public fun Context.registerDeviceGroup( block: DeviceGroup.() -> Unit, ): DeviceGroup = request(DeviceManager).registerDeviceGroup(name, meta, block) -private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { - return when (name.length) { - 0 -> this - 1 -> { - val token = name.first() - when (val d = devices[token]) { - null -> install( - token, - DeviceGroup(context, meta[token] ?: Meta.EMPTY) - ) - - else -> (d as? DeviceGroup) ?: error("Device $name is not a DeviceGroup") - } - } - - else -> getOrCreateGroup(name.first().asName()).getOrCreateGroup(name.cutFirst()) - } -} - -/** - * Register a device at given [name] path - */ -public fun DeviceGroup.install(name: Name, device: D): D { - return when (name.length) { - 0 -> error("Can't use empty name for a child device") - 1 -> install(name.first(), device) - else -> getOrCreateGroup(name.cutLast()).install(name.tokens.last(), device) - } -} +///** +// * Register a device at given [path] path +// */ +//public fun DeviceGroup.install(path: Path, device: D): D { +// return when (path.length) { +// 0 -> error("Can't use empty path for a child device") +// 1 -> install(path.first().name, device) +// else -> getOrCreateGroup(path.cutLast()).install(path.tokens.last(), device) +// } +//} public fun DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device) @@ -238,7 +224,7 @@ public fun DeviceGroup.install( * Create or edit a group with a given [name]. */ public fun DeviceGroup.registerDeviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup = - getOrCreateGroup(name).apply(block) + install(name, DeviceGroup(context, meta).apply(block)) public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup = registerDeviceGroup(name.parseAsName(), block) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt index 7ce2054..afb2cbd 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt @@ -8,10 +8,10 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Instant import space.kscience.controls.constructor.DeviceGroup -import space.kscience.controls.constructor.install import space.kscience.controls.manager.clock import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.write +import space.kscience.dataforge.names.parseAsName import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit @@ -94,4 +94,4 @@ public fun DeviceGroup.pid( name: String, drive: Drive, pidParameters: PidParameters, -): PidRegulator = install(name, PidRegulator(drive, pidParameters)) \ No newline at end of file +): PidRegulator = install(name.parseAsName(), PidRegulator(drive, pidParameters)) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt index f6ca4ec..077585b 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt @@ -1,40 +1,24 @@ package space.kscience.controls.api import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.names.* +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.provider.Path import space.kscience.dataforge.provider.Provider +import space.kscience.dataforge.provider.asPath +import space.kscience.dataforge.provider.plus /** * A hub that could locate multiple devices and redirect actions to them */ public interface DeviceHub : Provider { - public val devices: Map + public val devices: Map override val defaultTarget: String get() = Device.DEVICE_TARGET override val defaultChainTarget: String get() = Device.DEVICE_TARGET - /** - * List all devices, including sub-devices - */ - public fun buildDeviceTree(): Map = buildMap { - fun putAll(prefix: Name, hub: DeviceHub) { - hub.devices.forEach { - put(prefix + it.key, it.value) - } - } - - devices.forEach { - val name = it.key.asName() - put(name, it.value) - (it.value as? DeviceHub)?.let { hub -> - putAll(name, hub) - } - } - } - override fun content(target: String): Map = if (target == Device.DEVICE_TARGET) { - buildDeviceTree() + devices } else { emptyMap() } @@ -42,38 +26,31 @@ public interface DeviceHub : Provider { public companion object } +/** + * List all devices, including sub-devices + */ +public fun DeviceHub.provideAllDevices(): Map = buildMap { + fun putAll(prefix: Path, hub: DeviceHub) { + hub.devices.forEach { + put(prefix + it.key.asPath(), it.value) + } + } -public operator fun DeviceHub.get(nameToken: NameToken): Device = - devices[nameToken] ?: error("Device with name $nameToken not found in $this") - -public fun DeviceHub.getOrNull(name: Name): Device? = when { - name.isEmpty() -> this as? Device - name.length == 1 -> devices[name.firstOrNull()!!] - else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst()) + devices.forEach { + val name: Name = it.key + put(name.asPath(), it.value) + (it.value as? DeviceHub)?.let { hub -> + putAll(name.asPath(), hub) + } + } } -public operator fun DeviceHub.get(name: Name): Device = - getOrNull(name) ?: error("Device with name $name not found in $this") - -public fun DeviceHub.getOrNull(nameString: String): Device? = getOrNull(Name.parse(nameString)) - -public operator fun DeviceHub.get(nameString: String): Device = - getOrNull(nameString) ?: error("Device with name $nameString not found in $this") - public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta = - this[deviceName].readProperty(propertyName) + (devices[deviceName] ?: error("Device with name $deviceName not found in $this")).readProperty(propertyName) public suspend fun DeviceHub.writeProperty(deviceName: Name, propertyName: String, value: Meta) { - this[deviceName].writeProperty(propertyName, value) + (devices[deviceName] ?: error("Device with name $deviceName not found in $this")).writeProperty(propertyName, value) } public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: Meta?): Meta? = - this[deviceName].execute(command, argument) - - -//suspend fun DeviceHub.respond(request: Envelope): EnvelopeBuilder { -// val target = request.meta[DeviceMessage.TARGET_KEY].string ?: defaultTarget -// val device = this[target.toName()] -// -// return device.respond(device, target, request) -//} \ No newline at end of file + (devices[deviceName] ?: error("Device with name $deviceName not found in $this")).execute(command, argument) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt index 93b0696..689cb90 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt @@ -3,13 +3,13 @@ package space.kscience.controls.manager import kotlinx.coroutines.launch import space.kscience.controls.api.Device import space.kscience.controls.api.DeviceHub -import space.kscience.controls.api.getOrNull import space.kscience.controls.api.id import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MutableMeta import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.NameToken +import space.kscience.dataforge.names.get +import space.kscience.dataforge.names.parseAsName import kotlin.collections.set import kotlin.properties.ReadOnlyProperty @@ -22,11 +22,11 @@ public class DeviceManager : AbstractPlugin(), DeviceHub { /** * Actual list of connected devices */ - private val top = HashMap() - override val devices: Map get() = top + private val _devices = HashMap() + override val devices: Map get() = _devices - public fun registerDevice(name: NameToken, device: Device) { - top[name] = device + public fun registerDevice(name: Name, device: Device) { + _devices[name] = device } override fun content(target: String): Map = super.content(target) @@ -39,7 +39,7 @@ public class DeviceManager : AbstractPlugin(), DeviceHub { } public fun DeviceManager.install(name: String, device: D): D { - registerDevice(NameToken(name), device) + registerDevice(name.parseAsName(), device) device.launch { device.start() } @@ -69,7 +69,7 @@ public inline fun DeviceManager.installing( val meta = Meta(builder) return ReadOnlyProperty { _, property -> val name = property.name - val current = getOrNull(name) + val current = devices[name] if (current == null) { install(name, factory, meta) } else if (current.meta != meta) { diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt index fc6fb5d..a15bcef 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt @@ -74,11 +74,11 @@ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): List, - override val actionDescriptors: Collection, + propertyDescriptors: Collection, + actionDescriptors: Collection, incomingFlow: Flow, private val send: suspend (DeviceMessage) -> Unit, ) : CachingDevice { + override var actionDescriptors: Collection = actionDescriptors + internal set + + override var propertyDescriptors: Collection = propertyDescriptors + internal set + override val coroutineContext: CoroutineContext = context.coroutineContext + Job(context.coroutineContext[Job]) private val mutex = Mutex() @@ -44,19 +52,17 @@ public class DeviceClient internal constructor( private val flowInternal = incomingFlow.filter { it.sourceDevice == deviceName - }.shareIn(this, started = SharingStarted.Eagerly).also { - it.onEach { message -> - when (message) { - is PropertyChangedMessage -> mutex.withLock { - propertyCache[message.property] = message.value - } - - else -> { - //ignore - } + }.onEach { message -> + when (message) { + is PropertyChangedMessage -> mutex.withLock { + propertyCache[message.property] = message.value } - }.launchIn(this) - } + + else -> { + //ignore + } + } + }.shareIn(this, started = SharingStarted.Eagerly) override val messageFlow: Flow get() = flowInternal @@ -65,7 +71,7 @@ public class DeviceClient internal constructor( send( PropertyGetMessage(propertyName, targetDevice = deviceName) ) - return flowInternal.filterIsInstance().first { + return messageFlow.filterIsInstance().first { it.property == propertyName }.value } @@ -89,30 +95,33 @@ public class DeviceClient internal constructor( send( ActionExecuteMessage(actionName, argument, id, targetDevice = deviceName) ) - return flowInternal.filterIsInstance().first { + return messageFlow.filterIsInstance().first { it.action == actionName && it.requestId == id }.result } + private val lifecycleStateFlow = messageFlow.filterIsInstance() + .map { it.state }.stateIn(this, started = SharingStarted.Eagerly, DeviceLifecycleState.STARTED) + @DFExperimental - override val lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STARTED + override val lifecycleState: DeviceLifecycleState get() = lifecycleStateFlow.value } /** * Connect to a remote device via this endpoint. * * @param context a [Context] to run device in - * @param sourceEndpointName the name of this endpoint - * @param targetEndpointName the name of endpoint in Magix to connect to + * @param thisEndpoint the name of this endpoint + * @param deviceEndpoint the name of endpoint in Magix to connect to * @param deviceName the name of device within endpoint */ public suspend fun MagixEndpoint.remoteDevice( context: Context, - sourceEndpointName: String, - targetEndpointName: String, + thisEndpoint: String, + deviceEndpoint: String, deviceName: Name, -): DeviceClient = coroutineScope{ - val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(targetEndpointName)).map { it.second } +): DeviceClient = coroutineScope { + val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)).map { it.second } val deferredDescriptorMessage = CompletableDeferred() @@ -123,8 +132,8 @@ public suspend fun MagixEndpoint.remoteDevice( send( format = DeviceManager.magixFormat, payload = GetDescriptionMessage(targetDevice = deviceName), - source = sourceEndpointName, - target = targetEndpointName, + source = thisEndpoint, + target = deviceEndpoint, id = stringUID() ) @@ -141,14 +150,37 @@ public suspend fun MagixEndpoint.remoteDevice( send( format = DeviceManager.magixFormat, payload = it, - source = sourceEndpointName, - target = targetEndpointName, + source = thisEndpoint, + target = deviceEndpoint, id = stringUID() ) } } -//public fun MagixEndpoint.remoteDeviceHub() +private class MapBasedDeviceHub(val deviceMap: Map, val prefix: Name) : DeviceHub { + override val devices: Map + get() = deviceMap.filterKeys { name: Name -> name == prefix || (name.startsWith(prefix) && name.length == prefix.length + 1) } + +} + +public fun MagixEndpoint.remoteDeviceHub( + context: Context, + thisEndpoint: String, + deviceEndpoint: String, +): DeviceHub { + val devices = mutableMapOf() + val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)).map { it.second } + subscription.filterIsInstance().onEach { + + }.launchIn(context) + + + return object : DeviceHub { + override val devices: Map + get() = TODO("Not yet implemented") + + } +} /** * Subscribe on specific property of a device without creating a device diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt index d0bdda4..8f3e742 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt @@ -5,12 +5,12 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import space.kscience.controls.api.get import space.kscience.controls.api.getOrReadProperty import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.context.error import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.names.get import space.kscience.magix.api.* public const val TANGO_MAGIX_FORMAT: String = "tango" @@ -88,7 +88,7 @@ public fun DeviceManager.launchTangoMagix( return context.launch { endpoint.subscribe(tangoMagixFormat).onEach { (request, payload) -> try { - val device = get(payload.device) + val device = devices[payload.device] ?: error("Device ${payload.device} not found") when (payload.action) { TangoAction.read -> { val value = device.getOrReadProperty(payload.name) @@ -99,6 +99,7 @@ public fun DeviceManager.launchTangoMagix( ) } } + TangoAction.write -> { payload.value?.let { value -> device.writeProperty(payload.name, value) @@ -112,6 +113,7 @@ public fun DeviceManager.launchTangoMagix( ) } } + TangoAction.exec -> { val result = device.execute(payload.name, payload.argin) respond(request, payload) { requestPayload -> @@ -121,6 +123,7 @@ public fun DeviceManager.launchTangoMagix( ) } } + TangoAction.pipe -> TODO("Pipe not implemented") } } catch (ex: Exception) { diff --git a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt index ff82486..db80848 100644 --- a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt +++ b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt @@ -59,7 +59,7 @@ internal class RemoteDeviceConnect { val virtualMagixEndpoint = object : MagixEndpoint { - private val additionalMessages = MutableSharedFlow(10) + private val additionalMessages = MutableSharedFlow(1) override fun subscribe( filter: MagixMessageFilter, diff --git a/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt index e0bd47a..8557f9d 100644 --- a/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt +++ b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt @@ -7,7 +7,7 @@ import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceSpec import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.names.NameToken +import space.kscience.dataforge.names.Name /** * A variant of [DeviceBySpec] that includes Modbus RTU/TCP/UDP client @@ -35,12 +35,12 @@ public open class ModbusDeviceBySpec( public class ModbusHub( public val context: Context, public val masterBuilder: () -> AbstractModbusMaster, - public val specs: Map>>, + public val specs: Map>>, ) : DeviceHub, AutoCloseable { public val master: AbstractModbusMaster by lazy(masterBuilder) - override val devices: Map by lazy { + override val devices: Map by lazy { specs.mapValues { (_, pair) -> ModbusDeviceBySpec( context, diff --git a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt index 04bb46d..4f37322 100644 --- a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt +++ b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt @@ -29,19 +29,18 @@ import kotlinx.serialization.json.put import space.kscience.controls.api.DeviceMessage import space.kscience.controls.api.PropertyGetMessage import space.kscience.controls.api.PropertySetMessage -import space.kscience.controls.api.getOrNull import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.respondHubMessage import space.kscience.dataforge.meta.toMeta import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName +import space.kscience.dataforge.names.get import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.MagixFlowPlugin import space.kscience.magix.api.MagixMessage import space.kscience.magix.server.magixModule - private fun Application.deviceServerModule(manager: DeviceManager) { install(StatusPages) { exception { call, cause -> @@ -100,10 +99,9 @@ public fun Application.deviceManagerModule( h1 { +"Device server dashboard" } - deviceNames.forEach { deviceName -> - val device = - manager.getOrNull(deviceName) - ?: error("The device with name $deviceName not found in $manager") + deviceNames.forEach { deviceName: String -> + val device = manager.devices[deviceName] + ?: error("The device with name $deviceName not found in $manager") div { id = deviceName h2 { +deviceName } diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt index 6c271d7..106b077 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -19,7 +19,8 @@ import space.kscience.controls.ports.withStringDelimiter import space.kscience.controls.spec.* import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.* -import space.kscience.dataforge.names.NameToken +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.parseAsName import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.time.Duration @@ -47,7 +48,7 @@ class PiMotionMasterDevice( var axes: Map = emptyMap() private set - override val devices: Map = axes.mapKeys { (key, _) -> NameToken(key) } + override val devices: Map = axes.mapKeys { (key, _) -> key.parseAsName() } private suspend fun failIfError(message: (Int) -> String = { "Failed with error code $it" }) { val errorCode = getErrorCode()