diff --git a/CHANGELOG.md b/CHANGELOG.md index b4de4b7..93b9611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Changed - Property caching moved from core `Device` to the `CachingDevice` - `DeviceSpec` properties no explicitly pass property name to getters and setters. +- `DeviceHub.respondHubMessage` now returns a list of messages to allow querying multiple devices. Device server also returns an array. ### Deprecated 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 8bd7452..72b0dc3 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 @@ -14,22 +14,27 @@ public interface DeviceHub : Provider { override val defaultChainTarget: String get() = Device.DEVICE_TARGET - override fun content(target: String): Map = if (target == Device.DEVICE_TARGET) { - 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) - } + /** + * 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() } else { emptyMap() } @@ -37,6 +42,7 @@ public interface DeviceHub : Provider { public companion object } + public operator fun DeviceHub.get(nameToken: NameToken): Device = devices[nameToken] ?: error("Device with name $nameToken not found in $this") diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt index b3436e9..4cd91b0 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt @@ -103,7 +103,7 @@ public data class PropertyGetMessage( @SerialName("description.get") public data class GetDescriptionMessage( override val sourceDevice: Name? = null, - override val targetDevice: Name, + override val targetDevice: Name? = null, override val comment: String? = null, @EncodeDefault override val time: Instant? = Clock.System.now(), ) : DeviceMessage() { 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 4d319af..f8a641f 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 @@ -68,15 +68,22 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess } /** - * Process incoming [DeviceMessage], using hub naming to evaluate target. + * Process incoming [DeviceMessage], using hub naming to find target. + * If the `targetDevice` is `null`, then message is sent to each device in this hub */ -public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMessage? { +public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): List { return try { - val targetName = request.targetDevice ?: return null - val device = getOrNull(targetName) ?: error("The device with name $targetName not found in $this") - device.respondMessage(targetName, request) + val targetName = request.targetDevice + if(targetName == null) { + buildDeviceTree().mapNotNull { + it.value.respondMessage(it.key, request) + } + } else { + val device = getOrNull(targetName) ?: error("The device with name $targetName not found in $this") + listOfNotNull(device.respondMessage(targetName, request)) + } } catch (ex: Exception) { - DeviceMessage.error(ex, sourceDevice = Name.EMPTY, targetDevice = request.sourceDevice) + listOf(DeviceMessage.error(ex, sourceDevice = Name.EMPTY, targetDevice = request.sourceDevice)) } } diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt index ed69bff..5e7f4fb 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt @@ -39,10 +39,10 @@ public fun DeviceManager.launchMagixService( ): Job = context.launch { endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID)).onEach { (request, payload) -> val responsePayload = respondHubMessage(payload) - if (responsePayload != null) { + responsePayload.forEach { endpoint.send( format = controlsMagixFormat, - payload = responsePayload, + payload = it, source = endpointID, target = request.sourceEndpoint, id = generateId(request), 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 ba63583..04bb46d 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 @@ -157,8 +157,8 @@ public fun Application.deviceManagerModule( val body = call.receiveText() val request: DeviceMessage = MagixEndpoint.magixJson.decodeFromString(DeviceMessage.serializer(), body) val response = manager.respondHubMessage(request) - if (response != null) { - call.respondMessage(response) + if (response.isNotEmpty()) { + call.respondMessages(response) } else { call.respondText("No response") } @@ -177,9 +177,9 @@ public fun Application.deviceManagerModule( property = property, ) - val response = manager.respondHubMessage(request) - if (response != null) { - call.respondMessage(response) + val responses = manager.respondHubMessage(request) + if (responses.isNotEmpty()) { + call.respondMessages(responses) } else { call.respond(HttpStatusCode.InternalServerError) } @@ -197,9 +197,9 @@ public fun Application.deviceManagerModule( value = json.toMeta() ) - val response = manager.respondHubMessage(request) - if (response != null) { - call.respondMessage(response) + val responses = manager.respondHubMessage(request) + if (responses.isNotEmpty()) { + call.respondMessages(responses) } else { call.respond(HttpStatusCode.InternalServerError) } diff --git a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/responses.kt b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/responses.kt index 93d11c4..ffe489f 100644 --- a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/responses.kt +++ b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/responses.kt @@ -5,6 +5,7 @@ import io.ktor.server.application.ApplicationCall import io.ktor.server.response.respondText import kotlinx.serialization.json.JsonObjectBuilder import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.serializer import space.kscience.controls.api.DeviceMessage import space.kscience.magix.api.MagixEndpoint @@ -25,7 +26,7 @@ internal suspend fun ApplicationCall.respondJson(builder: JsonObjectBuilder.() - respondText(json.toString(), contentType = ContentType.Application.Json) } -internal suspend fun ApplicationCall.respondMessage(message: DeviceMessage): Unit = respondText( - MagixEndpoint.magixJson.encodeToString(DeviceMessage.serializer(), message), +internal suspend fun ApplicationCall.respondMessages(messages: List): Unit = respondText( + MagixEndpoint.magixJson.encodeToString(serializer>(), messages), contentType = ContentType.Application.Json ) \ No newline at end of file diff --git a/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts index 33b654e..6888d35 100644 --- a/demo/all-things/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("no.tornado:tornadofx:1.7.20") - implementation("space.kscience:plotlykt-server:0.6.0") + implementation("space.kscience:plotlykt-server:0.6.1") // implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") implementation(spclibs.logback.classic) } diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt index 067792c..657d275 100644 --- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt @@ -47,7 +47,12 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(Compa description = "Real to virtual time scale" } - val sinScale by mutableProperty(MetaConverter.double, IDemoDevice::sinScaleState) + val sinScale by mutableProperty(MetaConverter.double, IDemoDevice::sinScaleState){ + description = "The scale of sin plot" + metaDescriptor { + valueType(ValueType.NUMBER) + } + } val cosScale by mutableProperty(MetaConverter.double, IDemoDevice::cosScaleState) val sin by doubleProperty { sinValue() } @@ -74,6 +79,10 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(Compa write(cosScale, 1.0) } + val setSinScale by action(MetaConverter.double, MetaConverter.unit){ value: Double -> + write(sinScale, value) + } + override suspend fun IDemoDevice.onOpen() { launch { read(sinScale)