From 197675fc1556755025333310a3c6e5ae44b3abe7 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 15 Apr 2023 20:00:47 +0300 Subject: [PATCH 01/14] Add MQTT prototype (not tested) --- controls-core/build.gradle.kts | 2 +- .../controls-xodus/build.gradle.kts | 2 +- demo/all-things/build.gradle.kts | 6 +- demo/car/build.gradle.kts | 5 +- .../controls/demo/car/VirtualCarController.kt | 2 +- demo/echo/build.gradle.kts | 4 +- demo/magix-demo/build.gradle.kts | 2 +- .../pimotionmaster/PiMotionMasterApp.kt | 4 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- magix/magix-mqtt/build.gradle.kts | 18 +++++ .../ksceince/magix/mqtt/MqttMagixEndpoint.kt | 69 +++++++++++++++++++ .../magix-storage-xodus/build.gradle.kts | 2 +- settings.gradle.kts | 4 +- 14 files changed, 110 insertions(+), 14 deletions(-) create mode 100644 magix/magix-mqtt/build.gradle.kts create mode 100644 magix/magix-mqtt/src/main/kotlin/space/ksceince/magix/mqtt/MqttMagixEndpoint.kt diff --git a/controls-core/build.gradle.kts b/controls-core/build.gradle.kts index 8585f17..abceb3d 100644 --- a/controls-core/build.gradle.kts +++ b/controls-core/build.gradle.kts @@ -15,6 +15,6 @@ kscience { } dependencies { api("space.kscience:dataforge-io:$dataforgeVersion") - api(npmlibs.kotlinx.datetime) + api(spclibs.kotlinx.datetime) } } diff --git a/controls-storage/controls-xodus/build.gradle.kts b/controls-storage/controls-xodus/build.gradle.kts index b745b0b..635f8e3 100644 --- a/controls-storage/controls-xodus/build.gradle.kts +++ b/controls-storage/controls-xodus/build.gradle.kts @@ -11,7 +11,7 @@ dependencies { // implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion") // implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion") - testImplementation(npmlibs.kotlinx.coroutines.test) + testImplementation(spclibs.kotlinx.coroutines.test) } readme{ diff --git a/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts index 1f86f1c..b135253 100644 --- a/demo/all-things/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -29,9 +29,13 @@ dependencies { implementation("ch.qos.logback:logback-classic:1.2.11") } +kotlin{ + jvmToolchain(11) +} + + tasks.withType().configureEach { kotlinOptions { - jvmTarget = "11" freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") } } diff --git a/demo/car/build.gradle.kts b/demo/car/build.gradle.kts index 27e5776..3c943dd 100644 --- a/demo/car/build.gradle.kts +++ b/demo/car/build.gradle.kts @@ -35,9 +35,12 @@ dependencies { // implementation("org.litote.kmongo:kmongo-coroutine-serialization:4.4.0") } +kotlin{ + jvmToolchain(11) +} + tasks.withType().configureEach { kotlinOptions { - jvmTarget = "11" freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") } } diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt index 1f98ef7..bde7e6b 100644 --- a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt @@ -38,7 +38,7 @@ class VirtualCarController : Controller(), ContextAware { plugin(DeviceManager) } - private val deviceManager = context.fetch(DeviceManager, Meta { + private val deviceManager = context.request(DeviceManager, Meta { "xodusConfig" put { "entityStorePath" put deviceEntityStorePath.toString() } diff --git a/demo/echo/build.gradle.kts b/demo/echo/build.gradle.kts index 47ac2af..5563ba3 100644 --- a/demo/echo/build.gradle.kts +++ b/demo/echo/build.gradle.kts @@ -19,10 +19,12 @@ dependencies { implementation("ch.qos.logback:logback-classic:1.2.11") } +kotlin{ + jvmToolchain(11) +} tasks.withType().configureEach { kotlinOptions { - jvmTarget = "11" freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") } } diff --git a/demo/magix-demo/build.gradle.kts b/demo/magix-demo/build.gradle.kts index 411b9c4..babf240 100644 --- a/demo/magix-demo/build.gradle.kts +++ b/demo/magix-demo/build.gradle.kts @@ -8,7 +8,7 @@ dependencies{ implementation(projects.magix.magixServer) implementation(projects.magix.magixZmq) implementation(projects.magix.magixRsocket) - implementation("ch.qos.logback:logback-classic:1.2.3") + implementation(spclibs.logback.classic) } kotlin{ diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt index 45fe757..4ead06d 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt @@ -17,7 +17,7 @@ import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.po import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.installing import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.fetch +import space.kscience.dataforge.context.request import tornadofx.* class PiMotionMasterApp : App(PiMotionMasterView::class) @@ -29,7 +29,7 @@ class PiMotionMasterController : Controller() { } //initialize deviceManager plugin - val deviceManager: DeviceManager = context.fetch(DeviceManager) + val deviceManager: DeviceManager = context.request(DeviceManager) // install device val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice) diff --git a/gradle.properties b/gradle.properties index d422be3..3ba1c64 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,4 +7,4 @@ org.gradle.parallel=true publishing.github=false publishing.sonatype=false -toolsVersion=0.14.3-kotlin-1.8.10 \ No newline at end of file +toolsVersion=0.14.6-kotlin-1.8.20 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 070cb70..59bc51a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/magix/magix-mqtt/build.gradle.kts b/magix/magix-mqtt/build.gradle.kts new file mode 100644 index 0000000..e7037e6 --- /dev/null +++ b/magix/magix-mqtt/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("space.kscience.gradle.jvm") + `maven-publish` +} + +description = """ + MQTT client magix endpoint +""".trimIndent() + +dependencies { + api(projects.magix.magixApi) + implementation("com.hivemq:hivemq-mqtt-client:1.3.1") + implementation(spclibs.kotlinx.coroutines.jdk8) +} + +readme{ + maturity = space.kscience.gradle.Maturity.PROTOTYPE +} diff --git a/magix/magix-mqtt/src/main/kotlin/space/ksceince/magix/mqtt/MqttMagixEndpoint.kt b/magix/magix-mqtt/src/main/kotlin/space/ksceince/magix/mqtt/MqttMagixEndpoint.kt new file mode 100644 index 0000000..c9ac7f7 --- /dev/null +++ b/magix/magix-mqtt/src/main/kotlin/space/ksceince/magix/mqtt/MqttMagixEndpoint.kt @@ -0,0 +1,69 @@ +package space.ksceince.magix.mqtt + +import com.hivemq.client.mqtt.MqttClient +import com.hivemq.client.mqtt.datatypes.MqttQos +import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.future.await +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import java.util.* + + +public class MqttMagixEndpoint( + serverHost: String, + clientId: String = UUID.randomUUID().toString(), + public val topic: String = DEFAULT_MAGIX_TOPIC_NAME, + public val qos: MqttQos = MqttQos.AT_LEAST_ONCE, +) : MagixEndpoint, AutoCloseable { + + private val client: Mqtt5AsyncClient by lazy { + MqttClient.builder() + .identifier(clientId) + .serverHost(serverHost) + .useMqttVersion5() + .buildAsync() + } + + private val connection by lazy { + client.connect() + } + + override fun subscribe(filter: MagixMessageFilter): Flow = callbackFlow { + connection.await() + client.subscribeWith() + .topicFilter(topic) + .qos(qos) + .callback { published -> + val message = MagixEndpoint.magixJson.decodeFromString( + MagixMessage.serializer(), + published.payloadAsBytes.decodeToString() + ) + trySendBlocking(message) + } + .send() + + awaitClose { + client.disconnect() + } + } + + override suspend fun broadcast(message: MagixMessage) { + connection.await() + client.publishWith().topic(topic).qos(qos).payload( + MagixEndpoint.magixJson.encodeToString(MagixMessage.serializer(), message).encodeToByteArray() + ).send() + } + + override fun close() { + client.disconnect() + } + + public companion object { + public const val DEFAULT_MAGIX_TOPIC_NAME: String = "magix" + } +} \ No newline at end of file diff --git a/magix/magix-storage/magix-storage-xodus/build.gradle.kts b/magix/magix-storage/magix-storage-xodus/build.gradle.kts index 63a1b44..8869867 100644 --- a/magix/magix-storage/magix-storage-xodus/build.gradle.kts +++ b/magix/magix-storage/magix-storage-xodus/build.gradle.kts @@ -13,7 +13,7 @@ dependencies { api(projects.magix.magixApi) implementation("org.jetbrains.xodus:xodus-entity-store:$xodusVersion") - testImplementation(npmlibs.kotlinx.coroutines.test) + testImplementation(spclibs.kotlinx.coroutines.test) } readme{ diff --git a/settings.gradle.kts b/settings.gradle.kts index a8f250b..c66ec81 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,6 @@ rootProject.name = "controls-kt" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") -enableFeaturePreview("VERSION_CATALOGS") pluginManagement { @@ -34,7 +33,7 @@ dependencyResolutionManagement { } versionCatalogs { - create("npmlibs") { + create("spclibs") { from("space.kscience:version-catalog:$toolsVersion") } } @@ -56,6 +55,7 @@ include( ":magix:magix-java-client", ":magix:magix-zmq", ":magix:magix-rabbit", + ":magix:magix-mqtt", // ":magix:magix-storage", ":magix:magix-storage:magix-storage-xodus", From e7cfb1d2ba7dfb8dbf6b58aab632a78fe2c8e819 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 16 Apr 2023 12:29:24 +0300 Subject: [PATCH 02/14] Add opcua test --- controls-opcua/build.gradle.kts | 8 +- .../controls/opcua/client/MiloDevice.kt | 55 +++++++++-- .../controls/opcua/client/MiloDeviceBySpec.kt | 91 +++++++++---------- .../controls/opcua/client/OpcUaClientTest.kt | 47 ++++++++++ 4 files changed, 145 insertions(+), 56 deletions(-) create mode 100644 controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt diff --git a/controls-opcua/build.gradle.kts b/controls-opcua/build.gradle.kts index 8fe7564..1be4f75 100644 --- a/controls-opcua/build.gradle.kts +++ b/controls-opcua/build.gradle.kts @@ -4,14 +4,16 @@ plugins { val ktorVersion: String by rootProject.extra -val miloVersion: String = "0.6.7" +val miloVersion: String = "0.6.9" dependencies { - api(project(":controls-core")) - api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${space.kscience.gradle.KScienceVersions.coroutinesVersion}") + api(projects.controlsCore) + api(spclibs.kotlinx.coroutines.jdk8) api("org.eclipse.milo:sdk-client:$miloVersion") api("org.eclipse.milo:bsd-parser:$miloVersion") api("org.eclipse.milo:sdk-server:$miloVersion") + + testImplementation(spclibs.kotlinx.coroutines.test) } diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt index 7172e22..8132835 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt @@ -1,6 +1,8 @@ package space.kscience.controls.opcua.client import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import org.eclipse.milo.opcua.sdk.client.OpcUaClient import org.eclipse.milo.opcua.stack.core.types.builtin.* @@ -9,6 +11,8 @@ import space.kscience.controls.api.Device import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaSerializer import space.kscience.dataforge.meta.transformations.MetaConverter +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty /** @@ -19,11 +23,6 @@ public interface MiloDevice : Device { * The OPC-UA client initialized on first use */ public val client: OpcUaClient - - override fun close() { - client.disconnect() - super.close() - } } /** @@ -80,4 +79,48 @@ public suspend inline fun MiloDevice.writeOpc( ): StatusCode { val meta = converter.objectToMeta(value) return client.writeValue(nodeId, DataValue(Variant(meta))).await() -} \ No newline at end of file +} + + +/** + * A device-bound OPC-UA property. Does not trigger device properties change. + */ +public inline fun MiloDevice.opc( + nodeId: NodeId, + converter: MetaConverter, + magAge: Double = 500.0 +): ReadWriteProperty = object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking { + readOpc(nodeId, converter, magAge) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + launch { + writeOpc(nodeId, converter, value) + } + } +} + +/** + * Register a mutable OPC-UA based [Double] property in a device spec + */ +public fun MiloDevice.opcDouble( + nodeId: NodeId, + magAge: Double = 1.0 +): ReadWriteProperty = opc(nodeId, MetaConverter.double, magAge) + +/** + * Register a mutable OPC-UA based [Int] property in a device spec + */ +public fun MiloDevice.opcInt( + nodeId: NodeId, + magAge: Double = 1.0 +): ReadWriteProperty = opc(nodeId, MetaConverter.int, magAge) + +/** + * Register a mutable OPC-UA based [String] property in a device spec + */ +public fun MiloDevice.opcString( + nodeId: NodeId, + magAge: Double = 1.0 +): ReadWriteProperty = opc(nodeId, MetaConverter.string, magAge) \ No newline at end of file diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt index cae0f5e..2a130d7 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt @@ -1,69 +1,66 @@ package space.kscience.controls.opcua.client -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.eclipse.milo.opcua.sdk.client.OpcUaClient -import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider +import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider +import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceSpec import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Global -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.Scheme +import space.kscience.dataforge.meta.SchemeSpec +import space.kscience.dataforge.meta.specOrNull import space.kscience.dataforge.meta.string -import space.kscience.dataforge.meta.transformations.MetaConverter -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty + +public sealed class MiloIdentity: Scheme() + +public class MiloUsername : MiloIdentity() { + + public var username: String by string{ error("Username not defined") } + public var password: String by string{ error("Password not defined") } + + public companion object : SchemeSpec(::MiloUsername) +} + +//public class MiloKeyPair : MiloIdentity() { +// +// public companion object : SchemeSpec(::MiloUsername) +//} + +public class MiloConfiguration : Scheme() { + + public var endpointUrl: String by string { error("Endpoint url is not defined") } + + public var username: MiloUsername? by specOrNull(MiloUsername) + + public companion object : SchemeSpec(::MiloConfiguration) +} + +/** + * A variant of [DeviceBySpec] that includes OPC-UA client + */ public open class MiloDeviceBySpec>( spec: DeviceSpec, + config: MiloConfiguration, context: Context = Global, - meta: Meta = Meta.EMPTY -) : MiloDevice, DeviceBySpec(spec, context, meta) { +) : MiloDevice, DeviceBySpec(spec, context, config.meta) { override val client: OpcUaClient by lazy { - val endpointUrl = meta["endpointUrl"].string ?: error("Endpoint url is not defined") - context.createMiloClient(endpointUrl).apply { + context.createMiloClient( + config.endpointUrl, + securityPolicy = SecurityPolicy.None, + identityProvider = config.username?.let { + UsernameProvider(it.username,it.password) + } ?: AnonymousProvider() + ).apply { connect().get() } } override fun close() { - super.close() + client.disconnect() super.close() } } - -/** - * A device-bound OPC-UA property. Does not trigger device properties change. - */ -public inline fun MiloDeviceBySpec<*>.opc( - nodeId: NodeId, - converter: MetaConverter, - magAge: Double = 500.0 -): ReadWriteProperty = object : ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking { - readOpc(nodeId, converter, magAge) - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - launch { - writeOpc(nodeId, converter, value) - } - } -} - -public inline fun MiloDeviceBySpec<*>.opcDouble( - nodeId: NodeId, - magAge: Double = 1.0 -): ReadWriteProperty = opc(nodeId, MetaConverter.double, magAge) - -public inline fun MiloDeviceBySpec<*>.opcInt( - nodeId: NodeId, - magAge: Double = 1.0 -): ReadWriteProperty = opc(nodeId, MetaConverter.int, magAge) - -public inline fun MiloDeviceBySpec<*>.opcString( - nodeId: NodeId, - magAge: Double = 1.0 -): ReadWriteProperty = opc(nodeId, MetaConverter.string, magAge) \ No newline at end of file diff --git a/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt new file mode 100644 index 0000000..88d7e8d --- /dev/null +++ b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt @@ -0,0 +1,47 @@ +package space.kscience.controls.opcua.client + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.junit.jupiter.api.Test +import space.kscience.controls.opcua.client.OpcUaClientTest.DemoMiloDevice.Companion.randomDouble +import space.kscience.controls.spec.DeviceSpec +import space.kscience.controls.spec.doubleProperty +import space.kscience.dataforge.meta.transformations.MetaConverter + +class OpcUaClientTest { + class DemoMiloDevice(config: MiloConfiguration) : MiloDeviceBySpec(DemoMiloDevice, config) { + + //val randomDouble by opcDouble(NodeId(2, "Dynamic/RandomDouble")) + + suspend fun readRandomDouble() = readOpc(NodeId(2, "Dynamic/RandomDouble"), MetaConverter.double) + + + companion object : DeviceSpec() { + fun build(): DemoMiloDevice { + val config = MiloConfiguration { + endpointUrl = "opc.tcp://milo.digitalpetri.com:62541/milo" +// username = MiloUsername{ +// username = "user1" +// password = "password" +// } + } + return DemoMiloDevice(config) + } + + inline fun use(block: DemoMiloDevice.() -> R): R = build().use(block) + + val randomDouble by doubleProperty(read = DemoMiloDevice::readRandomDouble) + + } + + } + + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testReadDouble() = runTest { + println(DemoMiloDevice.use { randomDouble.read() }) + } + +} \ No newline at end of file From 8cd191bb0d033b1fd388a737fa9a4216e9c93568 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 3 May 2023 11:05:54 +0300 Subject: [PATCH 03/14] Documentation update --- .github/workflows/build.yml | 24 +++ .github/workflows/gradle.yml | 17 -- .github/workflows/pages.yml | 31 +++ .github/workflows/publish.yml | 50 +++++ .space.kts | 3 + README.md | 178 +++++++++++++++--- build.gradle.kts | 2 + controls-core/build.gradle.kts | 25 +++ .../space/kscience/controls/api/Device.kt | 9 +- .../controls/manager/DeviceManager.kt | 20 +- .../{deviceMessages.kt => respondMessage.kt} | 8 +- .../space/kscience/controls/ports/Port.kt | 11 +- .../space/kscience/controls/ports/Ports.kt | 6 + .../kscience/controls/spec/DeviceBase.kt | 3 + .../controls/spec/DevicePropertySpec.kt | 4 +- .../kscience/controls/spec/DeviceSpec.kt | 2 + controls-ktor-tcp/README.md | 4 + controls-magix-client/README.md | 32 ++++ controls-modbus/README.md | 4 + controls-modbus/build.gradle.kts | 13 ++ .../kscience/controls/modbus/ModbusDevice.kt | 146 ++++++++++++++ .../controls/modbus/ModbusDeviceBySpec.kt | 45 +++++ .../controls/modbus/ModbusRegistryMap.kt | 44 +++++ controls-opcua/README.md | 4 + .../controls/opcua/client/MiloDeviceBySpec.kt | 9 +- .../controls/opcua/client/OpcUaClientTest.kt | 7 +- controls-serial/README.md | 32 ++++ controls-server/README.md | 32 ++++ controls-storage/README.md | 36 +++- controls-storage/controls-xodus/README.md | 32 ++++ .../xodus/XodusDeviceMessageStorage.kt | 4 +- .../controls/storage/workDirectory.kt | 3 - demo/README.md | 4 + demo/all-things/README.md | 4 + demo/all-things/build.gradle.kts | 4 +- demo/car/README.md | 4 + demo/echo/README.md | 4 + demo/magix-demo/README.md | 4 + demo/mks-pdr900/README.md | 4 + demo/motors/README.md | 4 + docs/templates/ARTIFACT-TEMPLATE.md | 30 +++ docs/templates/README-TEMPLATE.md | 48 +++++ gradle/wrapper/gradle-wrapper.properties | 2 +- magix/README.md | 4 + magix/magix-api/README.md | 32 ++++ magix/magix-java-client/README.md | 32 ++++ magix/magix-mqtt/README.md | 32 ++++ magix/magix-rabbit/README.md | 32 ++++ magix/magix-rsocket/README.md | 32 ++++ magix/magix-server/README.md | 32 ++++ magix/magix-storage/README.md | 4 + .../magix-storage-xodus/README.md | 32 ++++ magix/magix-zmq/README.md | 32 ++++ settings.gradle.kts | 1 + 54 files changed, 1136 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/gradle.yml create mode 100644 .github/workflows/pages.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .space.kts rename controls-core/src/commonMain/kotlin/space/kscience/controls/manager/{deviceMessages.kt => respondMessage.kt} (94%) create mode 100644 controls-ktor-tcp/README.md create mode 100644 controls-magix-client/README.md create mode 100644 controls-modbus/README.md create mode 100644 controls-modbus/build.gradle.kts create mode 100644 controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt create mode 100644 controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt create mode 100644 controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt create mode 100644 controls-opcua/README.md create mode 100644 controls-serial/README.md create mode 100644 controls-server/README.md create mode 100644 controls-storage/controls-xodus/README.md create mode 100644 demo/README.md create mode 100644 demo/all-things/README.md create mode 100644 demo/car/README.md create mode 100644 demo/echo/README.md create mode 100644 demo/magix-demo/README.md create mode 100644 demo/mks-pdr900/README.md create mode 100644 demo/motors/README.md create mode 100644 docs/templates/ARTIFACT-TEMPLATE.md create mode 100644 docs/templates/README-TEMPLATE.md create mode 100644 magix/README.md create mode 100644 magix/magix-api/README.md create mode 100644 magix/magix-java-client/README.md create mode 100644 magix/magix-mqtt/README.md create mode 100644 magix/magix-rabbit/README.md create mode 100644 magix/magix-rsocket/README.md create mode 100644 magix/magix-server/README.md create mode 100644 magix/magix-storage/README.md create mode 100644 magix/magix-storage/magix-storage-xodus/README.md create mode 100644 magix/magix-zmq/README.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6ad294e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +name: Gradle build + +on: + push: + branches: [ dev, master ] + pull_request: + +jobs: + build: + runs-on: windows-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3.5.1 + with: + java-version: '11' + distribution: 'liberica' + cache: 'gradle' + - name: Gradle Wrapper Validation + uses: gradle/wrapper-validation-action@v1.0.4 + - name: Gradle Build + uses: gradle/gradle-build-action@v2.4.2 + with: + arguments: test jvmTest diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml deleted file mode 100644 index adc74ad..0000000 --- a/.github/workflows/gradle.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Gradle build - -on: [push] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 11 - - name: Build with Gradle - run: ./gradlew build diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..f8b0b86 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,31 @@ +name: Dokka publication + +on: + workflow_dispatch: + release: + types: [ created ] + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@v3.0.0 + - uses: actions/setup-java@v3.0.0 + with: + java-version: 11 + distribution: liberica + - name: Cache konan + uses: actions/cache@v3.0.1 + with: + path: ~/.konan + key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }} + restore-keys: | + ${{ runner.os }}-gradle- + - uses: gradle/gradle-build-action@v2.4.2 + with: + arguments: dokkaHtmlMultiModule --no-parallel + - uses: JamesIves/github-pages-deploy-action@v4.3.0 + with: + branch: gh-pages + folder: build/dokka/htmlMultiModule diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..31d539c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,50 @@ +name: Gradle publish + +on: + workflow_dispatch: + release: + types: [ created ] + +jobs: + publish: + environment: + name: publish + strategy: + matrix: + os: [ macOS-latest, windows-latest ] + runs-on: ${{matrix.os}} + steps: + - uses: actions/checkout@v3.0.0 + - uses: actions/setup-java@v3.10.0 + with: + java-version: 11 + distribution: liberica + - name: Cache konan + uses: actions/cache@v3.0.1 + with: + path: ~/.konan + key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Publish Windows Artifacts + if: matrix.os == 'windows-latest' + uses: gradle/gradle-build-action@v2.4.2 + with: + arguments: | + publishAllPublicationsToSpaceRepository + -Ppublishing.targets=all + -Ppublishing.space.user=${{ secrets.SPACE_APP_ID }} + -Ppublishing.space.token=${{ secrets.SPACE_APP_SECRET }} + - name: Publish Mac Artifacts + if: matrix.os == 'macOS-latest' + uses: gradle/gradle-build-action@v2.4.2 + with: + arguments: | + publishMacosX64PublicationToSpaceRepository + publishMacosArm64PublicationToSpaceRepository + publishIosX64PublicationToSpaceRepository + publishIosArm64PublicationToSpaceRepository + publishIosSimulatorArm64PublicationToSpaceRepository + -Ppublishing.targets=all + -Ppublishing.space.user=${{ secrets.SPACE_APP_ID }} + -Ppublishing.space.token=${{ secrets.SPACE_APP_SECRET }} diff --git a/.space.kts b/.space.kts new file mode 100644 index 0000000..e4d7522 --- /dev/null +++ b/.space.kts @@ -0,0 +1,3 @@ +job("Build and run tests") { + gradlew("amazoncorretto:17-alpine", "build") +} \ No newline at end of file diff --git a/README.md b/README.md index c2037e9..e52bdac 100644 --- a/README.md +++ b/README.md @@ -3,58 +3,182 @@ # Controls.kt Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing. -This repository contains a prototype of API and simple implementation +This repository contains a prototype of API and simple implementation of a slow control system, including a demo. -Controls.kt uses some concepts and modules of DataForge, -such as `Meta` (immutable tree-like structure) and `Meta` (which -includes a scalar value, or a tree of values, easily convertable to/from JSON -if needed). +Controls.kt uses some concepts and modules of DataForge, +such as `Meta` (tree-like value structure). To learn more about DataForge, please consult the following URLs: - * [Kotlin multiplatform implementation of DataForge](https://github.com/mipt-npm/dataforge-core) - * [DataForge documentation](http://npm.mipt.ru/dataforge/) - * [Original implementation of DataForge](https://bitbucket.org/Altavir/dataforge/src/default/) +* [Kotlin multiplatform implementation of DataForge](https://github.com/mipt-npm/dataforge-core) +* [DataForge documentation](http://npm.mipt.ru/dataforge/) +* [Original implementation of DataForge](https://bitbucket.org/Altavir/dataforge/src/default/) DataForge-control is a [Kotlin-multiplatform](https://kotlinlang.org/docs/reference/multiplatform.html) -application. Asynchronous operations are implemented with +application. Asynchronous operations are implemented with [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) library. ## Materials and publications * Video - [A general overview seminar](https://youtu.be/LO-qjWgXMWc) * Video - [A seminar about the system mechanics](https://youtu.be/wES0RV5GpoQ) -* Article - [A Novel Solution for Controlling Hardware Components of Accelerators and Beamlines](https://www.preprints.org/manuscript/202108.0336/v1) +* Article - [A Novel Solution for Controlling Hardware Components of Accelerators and Beamlines](https://www.preprints.org/manuscript/202108.0336/v1) ### Features Among other things, you can: -- Describe devices and their properties. +- Describe devices and their properties. - Collect data from devices and execute arbitrary actions supported by a device. - Property values can be cached in the system and requested from devices as needed, asynchronously. - Connect devices to event bus via bidirectional message flows. -### `dataforge-control-core` module packages +Example view of a demo: -- `api` - defines API for device management. The main class here is -[`Device`](controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt). -Generally, a Device has Properties that can be read and written. Also, some Actions -can optionally be applied on a device (may or may not affect properties). +![](docs/pictures/demo-view.png) -- `base` - contains baseline `Device` implementation -[`DeviceBase`](controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt) -and property implementation, including property asynchronous flows. +## Modules + + +### [controls-core](controls-core) +> +> +> **Maturity**: EXPERIMENTAL +> +> **Features:** +> - [device](controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt) : Device API with subscription (asynchronous and pseudo-synchronous properties) +> - [deviceMessage](controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt) : Specification for messages used to communicate between Controls-kt devices. +> - [deviceHub](controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt) : Grouping of devices into local tree-like hubs. + + +### [controls-ktor-tcp](controls-ktor-tcp) +> +> +> **Maturity**: EXPERIMENTAL + +### [controls-magix-client](controls-magix-client) +> +> +> **Maturity**: EXPERIMENTAL + +### [controls-modbus](controls-modbus) +> +> +> **Maturity**: EXPERIMENTAL + +### [controls-opcua](controls-opcua) +> +> +> **Maturity**: EXPERIMENTAL + +### [controls-serial](controls-serial) +> +> +> **Maturity**: EXPERIMENTAL + +### [controls-server](controls-server) +> +> +> **Maturity**: EXPERIMENTAL + +### [controls-storage](controls-storage) +> +> +> **Maturity**: PROTOTYPE + +### [demo](demo) +> +> +> **Maturity**: EXPERIMENTAL + +### [magix](magix) +> +> +> **Maturity**: EXPERIMENTAL + +### [controls-storage/controls-xodus](controls-storage/controls-xodus) +> +> +> **Maturity**: PROTOTYPE + +### [demo/all-things](demo/all-things) +> +> +> **Maturity**: EXPERIMENTAL + +### [demo/car](demo/car) +> +> +> **Maturity**: EXPERIMENTAL + +### [demo/echo](demo/echo) +> +> +> **Maturity**: EXPERIMENTAL + +### [demo/magix-demo](demo/magix-demo) +> +> +> **Maturity**: EXPERIMENTAL + +### [demo/mks-pdr900](demo/mks-pdr900) +> +> +> **Maturity**: EXPERIMENTAL + +### [demo/motors](demo/motors) +> +> +> **Maturity**: EXPERIMENTAL + +### [magix/magix-api](magix/magix-api) +> +> +> **Maturity**: EXPERIMENTAL + +### [magix/magix-java-client](magix/magix-java-client) +> +> +> **Maturity**: EXPERIMENTAL + +### [magix/magix-mqtt](magix/magix-mqtt) +> +> +> **Maturity**: PROTOTYPE + +### [magix/magix-rabbit](magix/magix-rabbit) +> +> +> **Maturity**: PROTOTYPE + +### [magix/magix-rsocket](magix/magix-rsocket) +> +> +> **Maturity**: EXPERIMENTAL + +### [magix/magix-server](magix/magix-server) +> +> +> **Maturity**: EXPERIMENTAL + +### [magix/magix-storage](magix/magix-storage) +> +> +> **Maturity**: EXPERIMENTAL + +### [magix/magix-zmq](magix/magix-zmq) +> +> +> **Maturity**: EXPERIMENTAL + +### [magix/magix-storage/magix-storage-xodus](magix/magix-storage/magix-storage-xodus) +> +> +> **Maturity**: PROTOTYPE -- `controllers` - implements Message Controller that can be attached to the event bus, Message -and Property flows. ### `demo` module The demo includes a simple mock device with a few properties changing as `sin` and `cos` of -the current time. The device is configurable via a simple TornadoFX-based control panel. -You can run a demo by executing `application/run` Gradle task. +the current time. The device is configurable via a simple TornadoFX-based control panel. +You can run a demo by executing `application/run` Gradle task. The graphs are displayed using [plotly.kt](https://github.com/mipt-npm/plotly.kt) library. - -Example view of a demo: - -![](docs/pictures/demo-view.png) diff --git a/build.gradle.kts b/build.gradle.kts index c9162f9..1e3ae71 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,6 +35,8 @@ ksciencePublish { space("https://maven.pkg.jetbrains.space/spc/p/controls/maven") } +readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md") + apiValidation { validationDisabled = true } \ No newline at end of file diff --git a/controls-core/build.gradle.kts b/controls-core/build.gradle.kts index abceb3d..b66fc12 100644 --- a/controls-core/build.gradle.kts +++ b/controls-core/build.gradle.kts @@ -18,3 +18,28 @@ kscience { api(spclibs.kotlinx.datetime) } } + + +readme{ + feature("device", ref = "src/commonMain/kotlin/space/kscience/controls/api/Device.kt"){ + """ + Device API with subscription (asynchronous and pseudo-synchronous properties) + """.trimIndent() + } +} + +readme{ + feature("deviceMessage", ref = "src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt"){ + """ + Specification for messages used to communicate between Controls-kt devices. + """.trimIndent() + } +} + +readme{ + feature("deviceHub", ref = "src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt"){ + """ + Grouping of devices into local tree-like hubs. + """.trimIndent() + } +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt index c122260..a9dab80 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt @@ -1,6 +1,5 @@ package space.kscience.controls.api -import io.ktor.utils.io.core.Closeable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -17,10 +16,11 @@ import space.kscience.dataforge.names.Name /** * General interface describing a managed Device. - * Device is a supervisor scope encompassing all operations on a device. When canceled, cancels all running processes. + * [Device] is a supervisor scope encompassing all operations on a device. + * When canceled, cancels all running processes. */ @Type(DEVICE_TARGET) -public interface Device : Closeable, ContextAware, CoroutineScope { +public interface Device : AutoCloseable, ContextAware, CoroutineScope { /** * Initial configuration meta for the device @@ -97,9 +97,8 @@ public suspend fun Device.getOrReadProperty(propertyName: String): Meta = getProperty(propertyName) ?: readProperty(propertyName) /** - * Get a snapshot of logical state of the device + * Get a snapshot of the device logical state * - * TODO currently this */ public fun Device.getAllProperties(): Meta = Meta { for (descriptor in propertyDescriptors) { 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 a3a5bfd..c1a943a 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,6 +3,7 @@ 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.dataforge.context.* import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MutableMeta @@ -12,6 +13,9 @@ import kotlin.collections.set import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KClass +/** + * DataForge Context plugin that allows to manage devices locally + */ public class DeviceManager : AbstractPlugin(), DeviceHub { override val tag: PluginTag get() = Companion.tag @@ -36,6 +40,9 @@ public class DeviceManager : AbstractPlugin(), DeviceHub { } +/** + * Register and start a device built by [factory] with current [Context] and [meta]. + */ public fun DeviceManager.install(name: String, factory: Factory, meta: Meta = Meta.EMPTY): D { val device = factory(meta, context) registerDevice(NameToken(name), device) @@ -45,6 +52,9 @@ public fun DeviceManager.install(name: String, factory: Factory, return device } +/** + * A delegate that initializes device on the first use + */ public inline fun DeviceManager.installing( factory: Factory, builder: MutableMeta.() -> Unit = {}, @@ -52,7 +62,15 @@ public inline fun DeviceManager.installing( val meta = Meta(builder) return ReadOnlyProperty { _, property -> val name = property.name - install(name, factory, meta) + val current = getOrNull(name) + if (current == null) { + install(name, factory, meta) + } else if (current.meta != meta) { + error("Meta mismatch. Current device meta: ${current.meta}, but factory meta is $meta") + } else { + @Suppress("UNCHECKED_CAST") + current as D + } } } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/deviceMessages.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt similarity index 94% rename from controls-core/src/commonMain/kotlin/space/kscience/controls/manager/deviceMessages.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt index ea5d34c..4db7894 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/deviceMessages.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt @@ -9,6 +9,9 @@ import space.kscience.controls.api.* import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.plus +/** + * Process a message targeted at this [Device], assuming its name is [deviceTarget]. + */ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMessage): DeviceMessage? = try { when (request) { is PropertyGetMessage -> { @@ -66,6 +69,9 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess DeviceMessage.error(ex, sourceDevice = deviceTarget, targetDevice = request.sourceDevice) } +/** + * Process incoming [DeviceMessage], using hub naming to evaluate target. + */ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMessage? { return try { val targetName = request.targetDevice ?: return null @@ -77,7 +83,7 @@ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMe } /** - * Collect all messages from given [DeviceHub], applying proper relative names + * Collect all messages from given [DeviceHub], applying proper relative names. */ public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow { diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt index 374c404..4fad1db 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt @@ -9,8 +9,14 @@ import space.kscience.dataforge.context.* import space.kscience.dataforge.misc.Type import kotlin.coroutines.CoroutineContext +/** + * Raw [ByteArray] port + */ public interface Port : ContextAware, Socket +/** + * A specialized factory for [Port] + */ @Type(PortFactory.TYPE) public interface PortFactory: Factory{ public val type: String @@ -20,6 +26,9 @@ public interface PortFactory: Factory{ } } +/** + * Common abstraction for [Port] based on [Channel] + */ public abstract class AbstractPort( override val context: Context, coroutineContext: CoroutineContext = context.coroutineContext, @@ -72,7 +81,7 @@ 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 be 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 = incoming.receiveAsFlow() 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 index 8c652cc..6d5c6d9 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Ports.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Ports.kt @@ -5,6 +5,9 @@ import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.string import kotlin.reflect.KClass +/** + * A DataForge plugin for managing ports + */ public class Ports : AbstractPlugin() { override val tag: PluginTag get() = Companion.tag @@ -15,6 +18,9 @@ public class Ports : AbstractPlugin() { private val portCache = mutableMapOf() + /** + * Create a new [Port] according to specification + */ 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 } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt index d1fd9b7..4546975 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt @@ -14,6 +14,9 @@ import space.kscience.dataforge.meta.Meta import kotlin.coroutines.CoroutineContext +/** + * A base abstractions for [Device], introducing specifications for properties + */ @OptIn(InternalDeviceAPI::class) public abstract class DeviceBase>( override val context: Context = Global, 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 index b545910..4fff906 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt @@ -27,7 +27,7 @@ public interface DevicePropertySpec { public val descriptor: PropertyDescriptor /** - * Meta item converter for resulting type + * Meta item converter for the resulting type */ public val converter: MetaConverter @@ -78,7 +78,7 @@ public interface DeviceActionSpec { } /** - * Action name, should be unique in device + * Action name. Should be unique in the device */ public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt index 83364ee..966ee32 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt @@ -12,6 +12,8 @@ import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 + + @OptIn(InternalDeviceAPI::class) public abstract class DeviceSpec { //initializing meta property for everyone diff --git a/controls-ktor-tcp/README.md b/controls-ktor-tcp/README.md new file mode 100644 index 0000000..94deb3b --- /dev/null +++ b/controls-ktor-tcp/README.md @@ -0,0 +1,4 @@ +# Module controls-ktor-tcp + + + diff --git a/controls-magix-client/README.md b/controls-magix-client/README.md new file mode 100644 index 0000000..2c1895e --- /dev/null +++ b/controls-magix-client/README.md @@ -0,0 +1,32 @@ +# Module controls-magix-client + + + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:controls-magix-client:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:controls-magix-client:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:controls-magix-client:0.1.1-SNAPSHOT") +} +``` diff --git a/controls-modbus/README.md b/controls-modbus/README.md new file mode 100644 index 0000000..f6d2335 --- /dev/null +++ b/controls-modbus/README.md @@ -0,0 +1,4 @@ +# Module controls-modbus + +A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols + diff --git a/controls-modbus/build.gradle.kts b/controls-modbus/build.gradle.kts new file mode 100644 index 0000000..8734460 --- /dev/null +++ b/controls-modbus/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("space.kscience.gradle.jvm") +} + +description = """ + A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols +""".trimIndent() + + +dependencies { + api(projects.controlsCore) + api("com.ghgande:j2mod:3.1.1") +} diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt new file mode 100644 index 0000000..04bb6a0 --- /dev/null +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt @@ -0,0 +1,146 @@ +package space.kscience.controls.modbus + +import com.ghgande.j2mod.modbus.facade.AbstractModbusMaster +import com.ghgande.j2mod.modbus.procimg.InputRegister +import com.ghgande.j2mod.modbus.procimg.Register +import com.ghgande.j2mod.modbus.procimg.SimpleRegister +import com.ghgande.j2mod.modbus.util.BitVector +import space.kscience.controls.api.Device +import java.nio.ByteBuffer +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + + +/** + * A Modbus device backed by j2mod client + */ +public interface ModbusDevice : Device { + + /** + * Client id for this specific device + */ + public val clientId: Int + + /** + * The OPC-UA client initialized on first use + */ + public val master: AbstractModbusMaster +} + +/** + * Read multiple sequential modbus coils (bit-values) + */ +public fun ModbusDevice.readCoils(ref: Int, count: Int): BitVector = + master.readCoils(clientId, ref, count) + +public fun ModbusDevice.readCoil(ref: Int): Boolean = + master.readCoils(clientId, ref, 1).getBit(0) + +public fun ModbusDevice.writeCoils(ref: Int, values: BooleanArray) { + val bitVector = BitVector(values.size) + values.forEachIndexed { index, value -> + bitVector.setBit(index, value) + } + master.writeMultipleCoils(clientId, ref, bitVector) +} + +public fun ModbusDevice.writeCoil(ref: Int, value: Boolean) { + master.writeCoil(clientId, ref, value) +} + +public fun ModbusDevice.readInputDiscretes(ref: Int, count: Int): BitVector = + master.readInputDiscretes(clientId, ref, count) + +public fun ModbusDevice.readInputRegisters(ref: Int, count: Int): List = + master.readInputRegisters(clientId, ref, count).toList() + +private fun Array.toBuffer(): ByteBuffer { + val buffer: ByteBuffer = ByteBuffer.allocate(size * 2) + forEachIndexed { index, value -> + buffer.position(index * 2) + buffer.put(value.toBytes()) + } + buffer.flip() + return buffer +} + +public fun ModbusDevice.readInputRegistersToBuffer(ref: Int, count: Int): ByteBuffer = + master.readInputRegisters(clientId, ref, count).toBuffer() + +public fun ModbusDevice.readDoubleInput(ref: Int): Double = + readInputRegistersToBuffer(ref, Double.SIZE_BYTES).getDouble() + +public fun ModbusDevice.readShortInput(ref: Int): Short = + readInputRegisters(ref, 1).first().toShort() + +public fun ModbusDevice.readHoldingRegisters(ref: Int, count: Int): List = + master.readMultipleRegisters(clientId, ref, count).toList() + +public fun ModbusDevice.readHoldingRegistersToBuffer(ref: Int, count: Int): ByteBuffer = + master.readMultipleRegisters(clientId, ref, count).toBuffer() + +public fun ModbusDevice.readDoubleRegister(ref: Int): Double = + readHoldingRegistersToBuffer(ref, Double.SIZE_BYTES).getDouble() + +public fun ModbusDevice.readShortRegister(ref: Int): Short = + readHoldingRegisters(ref, 1).first().toShort() + +public fun ModbusDevice.writeHoldingRegisters(ref: Int, values: ShortArray): Int = + master.writeMultipleRegisters( + clientId, + ref, + Array(values.size) { SimpleRegister().apply { setValue(values[it]) } } + ) + +public fun ModbusDevice.writeHoldingRegister(ref: Int, value: Short): Int = + master.writeSingleRegister( + clientId, + ref, + SimpleRegister().apply { setValue(value) } + ) + +public fun ModbusDevice.writeHoldingRegisters(ref: Int, buffer: ByteBuffer): Int { + val array = ShortArray(buffer.limit().floorDiv(2)) { buffer.getShort(it * 2) } + + return writeHoldingRegisters(ref, array) +} + +public fun ModbusDevice.writeShortRegister(ref: Int, value: Short) { + master.writeSingleRegister(ref, SimpleRegister().apply { setValue(value) }) +} + +public fun ModbusDevice.modBusRegister( + ref: Int, +): ReadWriteProperty = object : ReadWriteProperty { + override fun getValue(thisRef: ModbusDevice, property: KProperty<*>): Short = readShortRegister(ref) + + override fun setValue(thisRef: ModbusDevice, property: KProperty<*>, value: Short) { + writeHoldingRegister(ref, value) + } +} + +public fun ModbusDevice.modBusDoubleRegister( + ref: Int, +): ReadWriteProperty = object : ReadWriteProperty { + override fun getValue(thisRef: ModbusDevice, property: KProperty<*>): Double = readDoubleRegister(ref) + + override fun setValue(thisRef: ModbusDevice, property: KProperty<*>, value: Double) { + val buffer = ByteBuffer.allocate(Double.SIZE_BYTES).apply { putDouble(value) } + writeHoldingRegisters(ref, buffer) + } +} + + +// +//public inline fun ModbusDevice.opcDouble( +//): ReadWriteProperty = ma +// +//public inline fun ModbusDeviceBySpec<*>.opcInt( +// nodeId: NodeId, +// magAge: Double = 1.0, +//): ReadWriteProperty = opc(nodeId, MetaConverter.int, magAge) +// +//public inline fun ModbusDeviceBySpec<*>.opcString( +// nodeId: NodeId, +// magAge: Double = 1.0, +//): ReadWriteProperty = opc(nodeId, MetaConverter.string, magAge) diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt new file mode 100644 index 0000000..cc6044a --- /dev/null +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt @@ -0,0 +1,45 @@ +package space.kscience.controls.modbus + +import com.ghgande.j2mod.modbus.facade.AbstractModbusMaster +import space.kscience.controls.api.DeviceHub +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 + +/** + * A variant of [DeviceBySpec] that includes Modbus RTU/TCP/UDP client + */ +public open class ModbusDeviceBySpec( + context: Context, + spec: DeviceSpec, + override val clientId: Int, + override val master: AbstractModbusMaster, + meta: Meta = Meta.EMPTY, +) : ModbusDevice, DeviceBySpec(spec, context, meta) + + +public class ModbusHub( + public val context: Context, + public val masterBuilder: () -> AbstractModbusMaster, + public val specs: Map>>, +) : DeviceHub, AutoCloseable { + + public val master: AbstractModbusMaster by lazy(masterBuilder) + + override val devices: Map by lazy { + specs.mapValues { (_, pair) -> + ModbusDeviceBySpec( + context, + pair.second, + pair.first, + master + ) + } + } + + override fun close() { + master.disconnect() + } +} diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt new file mode 100644 index 0000000..e365a88 --- /dev/null +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt @@ -0,0 +1,44 @@ +package space.kscience.controls.modbus + + +public sealed class ModbusRegistryKey { + /** + * Read-only boolean value + */ + public class Coil(public val address: Int) : ModbusRegistryKey() { + init { + require(address in 1..9999) { "Coil address must be in 1..9999 range" } + } + } + + /** + * Read-write boolean value + */ + public class DiscreteInput(public val address: Int) : ModbusRegistryKey() { + init { + require(address in 10001..19999) { "DiscreteInput address must be in 10001..19999 range" } + } + } + + public class InputRegister(public val address: Int) : ModbusRegistryKey() { + init { + require(address in 20001..29999) { "InputRegister address must be in 20001..29999 range" } + } + } + + public class HoldingRegister(public val address: Int) : ModbusRegistryKey() { + init { + require(address in 30001..39999) { "HoldingRegister address must be in 30001..39999 range" } + } + } +} + +public abstract class ModbusRegistryMap { + protected fun coil(address: Int): ModbusRegistryKey.Coil = ModbusRegistryKey.Coil(address) + + protected fun discrete(address: Int): ModbusRegistryKey.DiscreteInput = ModbusRegistryKey.DiscreteInput(address) + + protected fun input(address: Int): ModbusRegistryKey.InputRegister = ModbusRegistryKey.InputRegister(address) + + protected fun register(address: Int): ModbusRegistryKey.HoldingRegister = ModbusRegistryKey.HoldingRegister(address) +} diff --git a/controls-opcua/README.md b/controls-opcua/README.md new file mode 100644 index 0000000..3accf5b --- /dev/null +++ b/controls-opcua/README.md @@ -0,0 +1,4 @@ +# Module controls-opcua + + + diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt index 2a130d7..8fef3c1 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt @@ -8,10 +8,7 @@ import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceSpec import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Global -import space.kscience.dataforge.meta.Scheme -import space.kscience.dataforge.meta.SchemeSpec -import space.kscience.dataforge.meta.specOrNull -import space.kscience.dataforge.meta.string +import space.kscience.dataforge.meta.* public sealed class MiloIdentity: Scheme() @@ -35,6 +32,8 @@ public class MiloConfiguration : Scheme() { public var username: MiloUsername? by specOrNull(MiloUsername) + public var securityPolicy: SecurityPolicy by enum(SecurityPolicy.None) + public companion object : SchemeSpec(::MiloConfiguration) } @@ -50,7 +49,7 @@ public open class MiloDeviceBySpec>( override val client: OpcUaClient by lazy { context.createMiloClient( config.endpointUrl, - securityPolicy = SecurityPolicy.None, + securityPolicy = config.securityPolicy, identityProvider = config.username?.let { UsernameProvider(it.username,it.password) } ?: AnonymousProvider() diff --git a/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt index 88d7e8d..72cee67 100644 --- a/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt +++ b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.junit.jupiter.api.Test -import space.kscience.controls.opcua.client.OpcUaClientTest.DemoMiloDevice.Companion.randomDouble import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.doubleProperty import space.kscience.dataforge.meta.transformations.MetaConverter @@ -21,10 +20,6 @@ class OpcUaClientTest { fun build(): DemoMiloDevice { val config = MiloConfiguration { endpointUrl = "opc.tcp://milo.digitalpetri.com:62541/milo" -// username = MiloUsername{ -// username = "user1" -// password = "password" -// } } return DemoMiloDevice(config) } @@ -41,7 +36,7 @@ class OpcUaClientTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun testReadDouble() = runTest { - println(DemoMiloDevice.use { randomDouble.read() }) + println(DemoMiloDevice.use { DemoMiloDevice.randomDouble.read() }) } } \ No newline at end of file diff --git a/controls-serial/README.md b/controls-serial/README.md new file mode 100644 index 0000000..eb214ba --- /dev/null +++ b/controls-serial/README.md @@ -0,0 +1,32 @@ +# Module controls-serial + + + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:controls-serial:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:controls-serial:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:controls-serial:0.1.1-SNAPSHOT") +} +``` diff --git a/controls-server/README.md b/controls-server/README.md new file mode 100644 index 0000000..561a815 --- /dev/null +++ b/controls-server/README.md @@ -0,0 +1,32 @@ +# Module controls-server + +A magix event loop server with web server for visualization. + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:controls-server:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:controls-server:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:controls-server:0.1.1-SNAPSHOT") +} +``` diff --git a/controls-storage/README.md b/controls-storage/README.md index 14289a1..650dd53 100644 --- a/controls-storage/README.md +++ b/controls-storage/README.md @@ -1,12 +1,32 @@ -# Description +# Module controls-storage -This module provides API to store [DeviceMessages](/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt) -from certain [DeviceManager](/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceManager.kt) -or [MagixMessages](magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixMessage.kt) -from certain [magix server](/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/server.kt). -# Usage -All usage examples can be found in [VirtualCarController](/demo/car/src/main/kotlin/ru/mipt/npm/controls/demo/car/VirtualCarController.kt). +## Usage -For more details, you can see comments in source code of this module. +## Artifact: + +The Maven coordinates of this project are `space.kscience:controls-storage:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:controls-storage:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:controls-storage:0.1.1-SNAPSHOT") +} +``` diff --git a/controls-storage/controls-xodus/README.md b/controls-storage/controls-xodus/README.md new file mode 100644 index 0000000..a25650e --- /dev/null +++ b/controls-storage/controls-xodus/README.md @@ -0,0 +1,32 @@ +# Module controls-xodus + + + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:controls-xodus:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:controls-xodus:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:controls-xodus:0.1.1-SNAPSHOT") +} +``` diff --git a/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt b/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt index 72cdd1f..bccaee9 100644 --- a/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt +++ b/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt @@ -16,7 +16,7 @@ import space.kscience.controls.storage.DeviceMessageStorage import space.kscience.controls.storage.workDirectory import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory -import space.kscience.dataforge.context.fetch +import space.kscience.dataforge.context.request import space.kscience.dataforge.io.IOPlugin import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get @@ -112,7 +112,7 @@ public class XodusDeviceMessageStorage( public val XODUS_STORE_PROPERTY: Name = Name.of("xodus", "storagePath") override fun build(context: Context, meta: Meta): XodusDeviceMessageStorage { - val io = context.fetch(IOPlugin) + val io = context.request(IOPlugin) val storePath = io.workDirectory.resolve( meta[XODUS_STORE_PROPERTY]?.string ?: context.properties[XODUS_STORE_PROPERTY]?.string ?: "storage" diff --git a/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/workDirectory.kt b/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/workDirectory.kt index 7e4086f..0847055 100644 --- a/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/workDirectory.kt +++ b/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/workDirectory.kt @@ -8,9 +8,6 @@ import space.kscience.dataforge.meta.string import java.nio.file.Path import kotlin.io.path.Path -//TODO remove on DF 0.6 - -internal val IOPlugin.Companion.WORK_DIRECTORY_KEY: String get() = ".dataforge" public val IOPlugin.workDirectory: Path get() { diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..e2a13c4 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,4 @@ +# Module demo + + + diff --git a/demo/all-things/README.md b/demo/all-things/README.md new file mode 100644 index 0000000..5d9c12f --- /dev/null +++ b/demo/all-things/README.md @@ -0,0 +1,4 @@ +# Module all-things + + + diff --git a/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts index b135253..1ea3c05 100644 --- a/demo/all-things/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -24,9 +24,9 @@ dependencies { implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("no.tornado:tornadofx:1.7.20") - implementation("space.kscience:plotlykt-server:0.5.3-dev-1") + implementation("space.kscience:plotlykt-server:0.5.3") // implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") - implementation("ch.qos.logback:logback-classic:1.2.11") + implementation(spclibs.logback.classic) } kotlin{ diff --git a/demo/car/README.md b/demo/car/README.md new file mode 100644 index 0000000..8c7ee00 --- /dev/null +++ b/demo/car/README.md @@ -0,0 +1,4 @@ +# Module car + + + diff --git a/demo/echo/README.md b/demo/echo/README.md new file mode 100644 index 0000000..4f3696a --- /dev/null +++ b/demo/echo/README.md @@ -0,0 +1,4 @@ +# Module echo + + + diff --git a/demo/magix-demo/README.md b/demo/magix-demo/README.md new file mode 100644 index 0000000..f27a6d3 --- /dev/null +++ b/demo/magix-demo/README.md @@ -0,0 +1,4 @@ +# Module magix-demo + + + diff --git a/demo/mks-pdr900/README.md b/demo/mks-pdr900/README.md new file mode 100644 index 0000000..e0e366c --- /dev/null +++ b/demo/mks-pdr900/README.md @@ -0,0 +1,4 @@ +# Module mks-pdr900 + + + diff --git a/demo/motors/README.md b/demo/motors/README.md new file mode 100644 index 0000000..2563da8 --- /dev/null +++ b/demo/motors/README.md @@ -0,0 +1,4 @@ +# Module motors + + + diff --git a/docs/templates/ARTIFACT-TEMPLATE.md b/docs/templates/ARTIFACT-TEMPLATE.md new file mode 100644 index 0000000..a3e47e6 --- /dev/null +++ b/docs/templates/ARTIFACT-TEMPLATE.md @@ -0,0 +1,30 @@ +## Artifact: + +The Maven coordinates of this project are `${group}:${name}:${version}`. + +**Gradle:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() + // development and snapshot versions + maven { url 'https://maven.pkg.jetbrains.space/spc/p/sci/dev' } +} + +dependencies { + implementation '${group}:${name}:${version}' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() + // development and snapshot versions + maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") +} + +dependencies { + implementation("${group}:${name}:${version}") +} +``` \ No newline at end of file diff --git a/docs/templates/README-TEMPLATE.md b/docs/templates/README-TEMPLATE.md new file mode 100644 index 0000000..57f30c1 --- /dev/null +++ b/docs/templates/README-TEMPLATE.md @@ -0,0 +1,48 @@ +[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) + +# Controls.kt + +Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing. +This repository contains a prototype of API and simple implementation +of a slow control system, including a demo. + +Controls.kt uses some concepts and modules of DataForge, +such as `Meta` (tree-like value structure). + +To learn more about DataForge, please consult the following URLs: +* [Kotlin multiplatform implementation of DataForge](https://github.com/mipt-npm/dataforge-core) +* [DataForge documentation](http://npm.mipt.ru/dataforge/) +* [Original implementation of DataForge](https://bitbucket.org/Altavir/dataforge/src/default/) + +DataForge-control is a [Kotlin-multiplatform](https://kotlinlang.org/docs/reference/multiplatform.html) +application. Asynchronous operations are implemented with +[kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) library. + +## Materials and publications + +* Video - [A general overview seminar](https://youtu.be/LO-qjWgXMWc) +* Video - [A seminar about the system mechanics](https://youtu.be/wES0RV5GpoQ) +* Article - [A Novel Solution for Controlling Hardware Components of Accelerators and Beamlines](https://www.preprints.org/manuscript/202108.0336/v1) + +### Features +Among other things, you can: +- Describe devices and their properties. +- Collect data from devices and execute arbitrary actions supported by a device. +- Property values can be cached in the system and requested from devices as needed, asynchronously. +- Connect devices to event bus via bidirectional message flows. + +Example view of a demo: + +![](docs/pictures/demo-view.png) + +## Modules + +${modules} + +### `demo` module + +The demo includes a simple mock device with a few properties changing as `sin` and `cos` of +the current time. The device is configurable via a simple TornadoFX-based control panel. +You can run a demo by executing `application/run` Gradle task. + +The graphs are displayed using [plotly.kt](https://github.com/mipt-npm/plotly.kt) library. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 59bc51a..fae0804 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/magix/README.md b/magix/README.md new file mode 100644 index 0000000..de1d7a3 --- /dev/null +++ b/magix/README.md @@ -0,0 +1,4 @@ +# Module magix + + + diff --git a/magix/magix-api/README.md b/magix/magix-api/README.md new file mode 100644 index 0000000..0226fc6 --- /dev/null +++ b/magix/magix-api/README.md @@ -0,0 +1,32 @@ +# Module magix-api + + + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:magix-api:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:magix-api:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:magix-api:0.1.1-SNAPSHOT") +} +``` diff --git a/magix/magix-java-client/README.md b/magix/magix-java-client/README.md new file mode 100644 index 0000000..375d6df --- /dev/null +++ b/magix/magix-java-client/README.md @@ -0,0 +1,32 @@ +# Module magix-java-client + + + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:magix-java-client:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:magix-java-client:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:magix-java-client:0.1.1-SNAPSHOT") +} +``` diff --git a/magix/magix-mqtt/README.md b/magix/magix-mqtt/README.md new file mode 100644 index 0000000..d8b9da6 --- /dev/null +++ b/magix/magix-mqtt/README.md @@ -0,0 +1,32 @@ +# Module magix-mqtt + +MQTT client magix endpoint + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:magix-mqtt:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:magix-mqtt:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:magix-mqtt:0.1.1-SNAPSHOT") +} +``` diff --git a/magix/magix-rabbit/README.md b/magix/magix-rabbit/README.md new file mode 100644 index 0000000..7c60864 --- /dev/null +++ b/magix/magix-rabbit/README.md @@ -0,0 +1,32 @@ +# Module magix-rabbit + +RabbitMQ client magix endpoint + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:magix-rabbit:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:magix-rabbit:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:magix-rabbit:0.1.1-SNAPSHOT") +} +``` diff --git a/magix/magix-rsocket/README.md b/magix/magix-rsocket/README.md new file mode 100644 index 0000000..0d5c9e6 --- /dev/null +++ b/magix/magix-rsocket/README.md @@ -0,0 +1,32 @@ +# Module magix-rsocket + +Magix endpoint (client) based on RSocket + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:magix-rsocket:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:magix-rsocket:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:magix-rsocket:0.1.1-SNAPSHOT") +} +``` diff --git a/magix/magix-server/README.md b/magix/magix-server/README.md new file mode 100644 index 0000000..f5a83e3 --- /dev/null +++ b/magix/magix-server/README.md @@ -0,0 +1,32 @@ +# Module magix-server + +A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes. + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:magix-server:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:magix-server:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:magix-server:0.1.1-SNAPSHOT") +} +``` diff --git a/magix/magix-storage/README.md b/magix/magix-storage/README.md new file mode 100644 index 0000000..7ebafae --- /dev/null +++ b/magix/magix-storage/README.md @@ -0,0 +1,4 @@ +# Module magix-storage + + + diff --git a/magix/magix-storage/magix-storage-xodus/README.md b/magix/magix-storage/magix-storage-xodus/README.md new file mode 100644 index 0000000..57107c5 --- /dev/null +++ b/magix/magix-storage/magix-storage-xodus/README.md @@ -0,0 +1,32 @@ +# Module magix-storage-xodus + + + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:magix-storage-xodus:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:magix-storage-xodus:0.1.1-SNAPSHOT") +} +``` diff --git a/magix/magix-zmq/README.md b/magix/magix-zmq/README.md new file mode 100644 index 0000000..cf150fe --- /dev/null +++ b/magix/magix-zmq/README.md @@ -0,0 +1,32 @@ +# Module magix-zmq + +ZMQ client endpoint for Magix + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:magix-zmq:0.1.1-SNAPSHOT`. + +**Gradle Groovy:** +```groovy +repositories { + maven { url 'https://repo.kotlin.link' } + mavenCentral() +} + +dependencies { + implementation 'space.kscience:magix-zmq:0.1.1-SNAPSHOT' +} +``` +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:magix-zmq:0.1.1-SNAPSHOT") +} +``` diff --git a/settings.gradle.kts b/settings.gradle.kts index c66ec81..a255044 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,6 +45,7 @@ include( ":controls-serial", ":controls-server", ":controls-opcua", + ":controls-modbus", // ":controls-mongo", ":controls-storage", ":controls-storage:controls-xodus", From 96305b1be6c66723d7d9df20562b024a202b151b Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 3 May 2023 08:16:46 +0000 Subject: [PATCH 04/14] Update space.kts JVM version --- .space.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.space.kts b/.space.kts index e4d7522..b978aa3 100644 --- a/.space.kts +++ b/.space.kts @@ -1,3 +1,3 @@ job("Build and run tests") { - gradlew("amazoncorretto:17-alpine", "build") + gradlew("amazoncorretto:11-alpine", "build") } \ No newline at end of file From fd1194205c13b08fc0061e3cd36cc54523bf4ca5 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 3 May 2023 08:50:52 +0000 Subject: [PATCH 05/14] change docker image for CI --- .space.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.space.kts b/.space.kts index b978aa3..64e2650 100644 --- a/.space.kts +++ b/.space.kts @@ -1,3 +1,3 @@ job("Build and run tests") { - gradlew("amazoncorretto:11-alpine", "build") + gradlew("gradle:8.1.1-jdk11", "build") } \ No newline at end of file From 86273101b68c1d5c4d4e0abd522577161daf7273 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 3 May 2023 08:51:19 +0000 Subject: [PATCH 06/14] Change command for CI --- .space.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.space.kts b/.space.kts index 64e2650..fe28b1f 100644 --- a/.space.kts +++ b/.space.kts @@ -1,3 +1,3 @@ job("Build and run tests") { - gradlew("gradle:8.1.1-jdk11", "build") + gradle("gradle:8.1.1-jdk11", "build") } \ No newline at end of file From 691b2ae67a23907aa0a9eda7948c39cba5c3c0f8 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 7 May 2023 11:13:42 +0300 Subject: [PATCH 07/14] Clean up property access syntax --- .space.kts | 46 ++++++++++++- build.gradle.kts | 6 +- controls-core/build.gradle.kts | 1 + .../kscience/controls/spec/DeviceBase.kt | 45 ++----------- .../controls/spec/DevicePropertySpec.kt | 66 +++++++++++++------ .../controls/spec/getDeviceProperty.kt | 10 --- .../controls/modbus/ModbusDeviceBySpec.kt | 9 +-- controls-opcua/build.gradle.kts | 1 - .../client/{MiloDevice.kt => OpcUaDevice.kt} | 16 ++--- ...loDeviceBySpec.kt => OpcUaDeviceBySpec.kt} | 7 +- .../controls/opcua/client/miloClient.kt | 4 +- .../controls/opcua/client/OpcUaClientTest.kt | 20 ++++-- .../xodus/XodusDeviceMessageStorage.kt | 2 +- .../controls/storage/workDirectory.kt | 29 -------- .../controls/demo/DemoControllerView.kt | 7 +- .../kscience/controls/demo/DemoDevice.kt | 18 ++--- .../controls/demo/car/MagixVirtualCar.kt | 3 +- .../kscience/controls/demo/car/VirtualCar.kt | 7 +- .../controls/demo/car/VirtualCarController.kt | 4 +- .../sciprog/devices/mks/MksPdr900Device.kt | 6 +- .../pimotionmaster/PiMotionMasterApp.kt | 7 +- .../pimotionmaster/PiMotionMasterDevice.kt | 22 +++---- .../pimotionmaster/fxDeviceProperties.kt | 2 +- 23 files changed, 170 insertions(+), 168 deletions(-) delete mode 100644 controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/getDeviceProperty.kt rename controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/{MiloDevice.kt => OpcUaDevice.kt} (90%) rename controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/{MiloDeviceBySpec.kt => OpcUaDeviceBySpec.kt} (91%) delete mode 100644 controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/workDirectory.kt diff --git a/.space.kts b/.space.kts index fe28b1f..c5dd962 100644 --- a/.space.kts +++ b/.space.kts @@ -1,3 +1,45 @@ -job("Build and run tests") { - gradle("gradle:8.1.1-jdk11", "build") +import kotlin.io.path.readText + +job("Build") { + gradlew("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3", "build") +} + +job("Publish") { + startOn { + gitPush { enabled = false } + } + container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") { + env["SPACE_USER"] = "{{ project:space_user }}" + env["SPACE_TOKEN"] = "{{ project:space_token }}" + kotlinScript { api -> + + val spaceUser = System.getenv("SPACE_USER") + val spaceToken = System.getenv("SPACE_TOKEN") + + // write the version to the build directory + api.gradlew("version") + + //read the version from build file + val version = java.nio.file.Path.of("build/project-version.txt").readText() + + val revisionSuffix = if (version.endsWith("SNAPSHOT")) { + "-" + api.gitRevision().take(7) + } else { + "" + } + + api.space().projects.automation.deployments.start( + project = api.projectIdentifier(), + targetIdentifier = TargetIdentifier.Key("maps-kt"), + version = version+revisionSuffix, + // automatically update deployment status based on the status of a job + syncWithAutomationJob = true + ) + api.gradlew( + "publishAllPublicationsToSpaceRepository", + "-Ppublishing.space.user=\"$spaceUser\"", + "-Ppublishing.space.token=\"$spaceToken\"", + ) + } + } } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 1e3ae71..7327460 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,8 +35,4 @@ ksciencePublish { space("https://maven.pkg.jetbrains.space/spc/p/controls/maven") } -readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md") - -apiValidation { - validationDisabled = true -} \ No newline at end of file +readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md") \ No newline at end of file diff --git a/controls-core/build.gradle.kts b/controls-core/build.gradle.kts index b66fc12..41acc60 100644 --- a/controls-core/build.gradle.kts +++ b/controls-core/build.gradle.kts @@ -13,6 +13,7 @@ kscience { useSerialization{ json() } + useContextReceivers() dependencies { api("space.kscience:dataforge-io:$dataforgeVersion") api(spclibs.kotlinx.datetime) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt index 4546975..e74ca2d 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import space.kscience.controls.api.* @@ -18,7 +17,7 @@ import kotlin.coroutines.CoroutineContext * A base abstractions for [Device], introducing specifications for properties */ @OptIn(InternalDeviceAPI::class) -public abstract class DeviceBase>( +public abstract class DeviceBase( override val context: Context = Global, override val meta: Meta = Meta.EMPTY, ) : Device { @@ -75,7 +74,7 @@ public abstract class DeviceBase>( /** * Update logical state using given [spec] and its convertor */ - protected suspend fun updateLogical(spec: DevicePropertySpec, value: T) { + public suspend fun updateLogical(spec: DevicePropertySpec, value: T) { updateLogical(spec.name, spec.converter.objectToMeta(value)) } @@ -99,7 +98,7 @@ public abstract class DeviceBase>( } override suspend fun writeProperty(propertyName: String, value: Meta): Unit { - //If there is a physical property with given name, invalidate logical property and write physical one + //If there is a physical property with a given name, invalidate logical property and write physical one (properties[propertyName] as? WritableDevicePropertySpec)?.let { invalidate(propertyName) it.writeMeta(self, value) @@ -111,49 +110,13 @@ public abstract class DeviceBase>( override suspend fun execute(action: String, argument: Meta?): Meta? = actions[action]?.executeWithMeta(self, argument) - /** - * 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.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) - - /** - * Write typed property state and invalidate logical state - */ - public suspend fun WritableDevicePropertySpec.write(value: T) { - invalidate(name) - write(self, value) - //perform asynchronous read and update after write - launch { - read() - } - } - - /** - * 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 open class DeviceBySpec( public val spec: DeviceSpec, context: Context = Global, meta: Meta = Meta.EMPTY, 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 index 4fff906..53b3953 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt @@ -91,30 +91,56 @@ public suspend fun DeviceActionSpec.executeWithMeta( 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)) +/** + * Read typed value and update/push event if needed. + * Return null if property read is not successful or property is undefined. + */ +@OptIn(InternalDeviceAPI::class) +public suspend fun D.readOrNull(propertySpec: DevicePropertySpec): T? { + val res = propertySpec.read(this) ?: return null + @Suppress("UNCHECKED_CAST") + (this as? DeviceBase)?.updateLogical(propertySpec, res) + return res } -public fun , T> D.write( - propertySpec: WritableDevicePropertySpec, - value: T, -): Job = launch { - propertySpec.write(value) +public suspend fun D.read(propertySpec: DevicePropertySpec): T = + readOrNull(propertySpec) ?: error("Failed to read property ${propertySpec.name} state") + +public operator fun D.get(propertySpec: DevicePropertySpec): T? = + getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject) + +/** + * Write typed property state and invalidate logical state + */ +@OptIn(InternalDeviceAPI::class) +public suspend fun D.write(propertySpec: WritableDevicePropertySpec, value: T) { + invalidate(propertySpec.name) + propertySpec.write(this, value) + //perform asynchronous read and update after write + launch { + read(propertySpec) + } } +/** + * Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope + */ +public operator fun D.set(propertySpec: WritableDevicePropertySpec, value: T): Unit { + launch { + write(propertySpec, value) + } +} + +/** + * Reset the logical state of a property + */ +public suspend fun D.invalidate(propertySpec: DevicePropertySpec) { + invalidate(propertySpec.name) +} + +public suspend fun D.execute(actionSpec: DeviceActionSpec, input: I? = null): O? = + actionSpec.execute(this, input) + /** * A type safe property change listener */ diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/getDeviceProperty.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/getDeviceProperty.kt deleted file mode 100644 index a7063f5..0000000 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/getDeviceProperty.kt +++ /dev/null @@ -1,10 +0,0 @@ -package space.kscience.controls.spec - -import kotlinx.coroutines.runBlocking - -/** - * Blocking property get call - */ -public operator fun , T : Any> D.get( - propertySpec: DevicePropertySpec -): T? = runBlocking { read(propertySpec) } \ No newline at end of file diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt index cc6044a..e1acd4b 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt @@ -1,6 +1,7 @@ package space.kscience.controls.modbus import com.ghgande.j2mod.modbus.facade.AbstractModbusMaster +import space.kscience.controls.api.Device import space.kscience.controls.api.DeviceHub import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceSpec @@ -11,19 +12,19 @@ import space.kscience.dataforge.names.NameToken /** * A variant of [DeviceBySpec] that includes Modbus RTU/TCP/UDP client */ -public open class ModbusDeviceBySpec( +public open class ModbusDeviceBySpec( context: Context, - spec: DeviceSpec, + spec: DeviceSpec, override val clientId: Int, override val master: AbstractModbusMaster, meta: Meta = Meta.EMPTY, -) : ModbusDevice, DeviceBySpec(spec, context, meta) +) : ModbusDevice, DeviceBySpec(spec, context, meta) 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) diff --git a/controls-opcua/build.gradle.kts b/controls-opcua/build.gradle.kts index 1be4f75..02510e2 100644 --- a/controls-opcua/build.gradle.kts +++ b/controls-opcua/build.gradle.kts @@ -12,7 +12,6 @@ dependencies { api("org.eclipse.milo:sdk-client:$miloVersion") api("org.eclipse.milo:bsd-parser:$miloVersion") - api("org.eclipse.milo:sdk-server:$miloVersion") testImplementation(spclibs.kotlinx.coroutines.test) diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDevice.kt similarity index 90% rename from controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt rename to controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDevice.kt index 8132835..dd83e58 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDevice.kt @@ -18,7 +18,7 @@ import kotlin.reflect.KProperty /** * An OPC-UA device backed by Eclipse Milo client */ -public interface MiloDevice : Device { +public interface OpcUaDevice : Device { /** * The OPC-UA client initialized on first use */ @@ -29,7 +29,7 @@ public interface MiloDevice : Device { * Read OPC-UA value with timestamp * @param T the type of property to read. The value is coerced to it. */ -public suspend inline fun MiloDevice.readOpcWithTime( +public suspend inline fun OpcUaDevice.readOpcWithTime( nodeId: NodeId, converter: MetaConverter, magAge: Double = 500.0 @@ -50,7 +50,7 @@ public suspend inline fun MiloDevice.readOpcWithTime( /** * Read and coerce value from OPC-UA */ -public suspend inline fun MiloDevice.readOpc( +public suspend inline fun OpcUaDevice.readOpc( nodeId: NodeId, converter: MetaConverter, magAge: Double = 500.0 @@ -72,7 +72,7 @@ public suspend inline fun MiloDevice.readOpc( return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}") } -public suspend inline fun MiloDevice.writeOpc( +public suspend inline fun OpcUaDevice.writeOpc( nodeId: NodeId, converter: MetaConverter, value: T @@ -85,7 +85,7 @@ public suspend inline fun MiloDevice.writeOpc( /** * A device-bound OPC-UA property. Does not trigger device properties change. */ -public inline fun MiloDevice.opc( +public inline fun OpcUaDevice.opc( nodeId: NodeId, converter: MetaConverter, magAge: Double = 500.0 @@ -104,7 +104,7 @@ public inline fun MiloDevice.opc( /** * Register a mutable OPC-UA based [Double] property in a device spec */ -public fun MiloDevice.opcDouble( +public fun OpcUaDevice.opcDouble( nodeId: NodeId, magAge: Double = 1.0 ): ReadWriteProperty = opc(nodeId, MetaConverter.double, magAge) @@ -112,7 +112,7 @@ public fun MiloDevice.opcDouble( /** * Register a mutable OPC-UA based [Int] property in a device spec */ -public fun MiloDevice.opcInt( +public fun OpcUaDevice.opcInt( nodeId: NodeId, magAge: Double = 1.0 ): ReadWriteProperty = opc(nodeId, MetaConverter.int, magAge) @@ -120,7 +120,7 @@ public fun MiloDevice.opcInt( /** * Register a mutable OPC-UA based [String] property in a device spec */ -public fun MiloDevice.opcString( +public fun OpcUaDevice.opcString( nodeId: NodeId, magAge: Double = 1.0 ): ReadWriteProperty = opc(nodeId, MetaConverter.string, magAge) \ No newline at end of file diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt similarity index 91% rename from controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt rename to controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt index 8fef3c1..821b693 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt @@ -4,6 +4,7 @@ import org.eclipse.milo.opcua.sdk.client.OpcUaClient import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy +import space.kscience.controls.api.Device import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceSpec import space.kscience.dataforge.context.Context @@ -40,14 +41,14 @@ public class MiloConfiguration : Scheme() { /** * A variant of [DeviceBySpec] that includes OPC-UA client */ -public open class MiloDeviceBySpec>( +public open class OpcUaDeviceBySpec( spec: DeviceSpec, config: MiloConfiguration, context: Context = Global, -) : MiloDevice, DeviceBySpec(spec, context, config.meta) { +) : OpcUaDevice, DeviceBySpec(spec, context, config.meta) { override val client: OpcUaClient by lazy { - context.createMiloClient( + context.createOpcUaClient( config.endpointUrl, securityPolicy = config.securityPolicy, identityProvider = config.username?.let { diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/miloClient.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/miloClient.kt index 8415b3a..face6cc 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/miloClient.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/miloClient.kt @@ -18,10 +18,10 @@ import java.nio.file.Path import java.nio.file.Paths import java.util.* -public fun T?.toOptional(): Optional = if(this == null) Optional.empty() else Optional.of(this) +internal fun T?.toOptional(): Optional = if(this == null) Optional.empty() else Optional.of(this) -internal fun Context.createMiloClient( +internal fun Context.createOpcUaClient( endpointUrl: String, //"opc.tcp://localhost:12686/milo" securityPolicy: SecurityPolicy = SecurityPolicy.Basic256Sha256, identityProvider: IdentityProvider = AnonymousProvider(), diff --git a/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt index 72cee67..d16cb1e 100644 --- a/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt +++ b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt @@ -6,27 +6,31 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.junit.jupiter.api.Test import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.doubleProperty +import space.kscience.controls.spec.read import space.kscience.dataforge.meta.transformations.MetaConverter class OpcUaClientTest { - class DemoMiloDevice(config: MiloConfiguration) : MiloDeviceBySpec(DemoMiloDevice, config) { + class DemoOpcUaDevice(config: MiloConfiguration) : OpcUaDeviceBySpec(DemoOpcUaDevice, config) { //val randomDouble by opcDouble(NodeId(2, "Dynamic/RandomDouble")) suspend fun readRandomDouble() = readOpc(NodeId(2, "Dynamic/RandomDouble"), MetaConverter.double) - companion object : DeviceSpec() { - fun build(): DemoMiloDevice { + companion object : DeviceSpec() { + /** + * Build a device. This is not a part of the specification + */ + fun build(): DemoOpcUaDevice { val config = MiloConfiguration { endpointUrl = "opc.tcp://milo.digitalpetri.com:62541/milo" } - return DemoMiloDevice(config) + return DemoOpcUaDevice(config) } - inline fun use(block: DemoMiloDevice.() -> R): R = build().use(block) + inline fun use(block: (DemoOpcUaDevice) -> R): R = build().use(block) - val randomDouble by doubleProperty(read = DemoMiloDevice::readRandomDouble) + val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble) } @@ -36,7 +40,9 @@ class OpcUaClientTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun testReadDouble() = runTest { - println(DemoMiloDevice.use { DemoMiloDevice.randomDouble.read() }) + DemoOpcUaDevice.use{ + println(it.read(DemoOpcUaDevice.randomDouble)) + } } } \ No newline at end of file diff --git a/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt b/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt index bccaee9..e5d2e4a 100644 --- a/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt +++ b/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt @@ -13,11 +13,11 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import space.kscience.controls.api.DeviceMessage import space.kscience.controls.storage.DeviceMessageStorage -import space.kscience.controls.storage.workDirectory import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory import space.kscience.dataforge.context.request import space.kscience.dataforge.io.IOPlugin +import space.kscience.dataforge.io.workDirectory import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.string diff --git a/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/workDirectory.kt b/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/workDirectory.kt deleted file mode 100644 index 0847055..0000000 --- a/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/workDirectory.kt +++ /dev/null @@ -1,29 +0,0 @@ -package space.kscience.controls.storage - -import space.kscience.dataforge.context.ContextBuilder -import space.kscience.dataforge.io.IOPlugin -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.set -import space.kscience.dataforge.meta.string -import java.nio.file.Path -import kotlin.io.path.Path - - -public val IOPlugin.workDirectory: Path - get() { - val workDirectoryPath = meta[IOPlugin.WORK_DIRECTORY_KEY].string - ?: context.properties[IOPlugin.WORK_DIRECTORY_KEY].string - ?: ".dataforge" - - return Path(workDirectoryPath) - } - -public fun ContextBuilder.workDirectory(path: String) { - properties { - set(IOPlugin.WORK_DIRECTORY_KEY, path) - } -} - -public fun ContextBuilder.workDirectory(path: Path){ - workDirectory(path.toAbsolutePath().toString()) -} diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt index 802fc4d..538570d 100644 --- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt @@ -17,6 +17,7 @@ import space.kscience.controls.manager.install import space.kscience.controls.opcua.server.OpcUaServer import space.kscience.controls.opcua.server.endpoint import space.kscience.controls.opcua.server.serveDevices +import space.kscience.controls.spec.write import space.kscience.dataforge.context.* import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.rsocket.rSocketWithTcp @@ -125,9 +126,9 @@ class DemoControllerView : View(title = " Demo controller remote") { action { controller.device?.run { launch { - timeScale.write(timeScaleSlider.value) - sinScale.write(xScaleSlider.value) - cosScale.write(yScaleSlider.value) + write(timeScale, timeScaleSlider.value) + write(sinScale, xScaleSlider.value) + write(cosScale, yScaleSlider.value) } } } 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 10713f3..1a8ad97 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 @@ -62,21 +62,21 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(DemoDe override suspend fun DemoDevice.onOpen() { launch { - sinScale.read() - cosScale.read() - timeScale.read() + read(sinScale) + read(cosScale) + read(timeScale) } doRecurring(50.milliseconds) { - sin.read() - cos.read() - coordinates.read() + read(sin) + read(cos) + read(coordinates) } } val resetScale by action(MetaConverter.meta, MetaConverter.meta) { - timeScale.write(5000.0) - sinScale.write(1.0) - cosScale.write(1.0) + write(timeScale, 5000.0) + write(sinScale, 1.0) + write(cosScale, 1.0) null } diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt index 5cf3386..addc3a9 100644 --- a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt @@ -3,6 +3,7 @@ package space.kscience.controls.demo.car import kotlinx.coroutines.launch import space.kscience.controls.api.PropertyChangedMessage import space.kscience.controls.client.controlsMagixFormat +import space.kscience.controls.spec.write import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta @@ -21,7 +22,7 @@ class MagixVirtualCar(context: Context, meta: Meta) : VirtualCar(context, meta) (payload as? PropertyChangedMessage)?.let { message -> if (message.sourceDevice == Name.parse("virtual-car")) { when (message.property) { - "acceleration" -> IVirtualCar.acceleration.write(Vector2D.metaToObject(message.value)) + "acceleration" -> write(IVirtualCar.acceleration, Vector2D.metaToObject(message.value)) } } } diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt index db0638c..1f1dc69 100644 --- a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt @@ -8,6 +8,7 @@ import kotlinx.datetime.Clock import kotlinx.datetime.Instant import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.doRecurring +import space.kscience.controls.spec.read import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta @@ -78,9 +79,9 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec(I //TODO apply friction. One can introduce rotation of the cabin and different friction coefficients along the axis launch { //update logical states - IVirtualCar.location.read() - IVirtualCar.speed.read() - IVirtualCar.acceleration.read() + read(IVirtualCar.location) + read(IVirtualCar.speed) + read(IVirtualCar.acceleration) } } diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt index bde7e6b..f68c585 100644 --- a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt @@ -12,6 +12,7 @@ import space.kscience.controls.client.connectToMagix import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install +import space.kscience.controls.spec.write import space.kscience.controls.storage.storeMessages import space.kscience.controls.xodus.XodusDeviceMessageStorage import space.kscience.dataforge.context.* @@ -113,7 +114,8 @@ class VirtualCarControllerView : View(title = " Virtual car controller remote") action { controller.virtualCar?.run { launch { - acceleration.write( + write( + acceleration, Vector2D( accelerationXProperty.get(), accelerationYProperty.get() diff --git a/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/MksPdr900Device.kt b/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/MksPdr900Device.kt index 304102e..949ced9 100644 --- a/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/MksPdr900Device.kt +++ b/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/MksPdr900Device.kt @@ -45,7 +45,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec() { @@ -336,15 +336,15 @@ class PiMotionMasterDevice( val move by metaAction { val target = it.double ?: it?.get("target").double ?: error("Unacceptable target value $it") - closedLoop.write(true) + write(closedLoop, true) //optionally set velocity it?.get("velocity").double?.let { v -> - velocity.write(v) + write(velocity, v) } - targetPosition.write(target) + write(targetPosition, target) //read `onTarget` and `position` properties in a cycle until movement is complete - while (!onTarget.read()) { - position.read() + while (!read(onTarget)) { + read(position) delay(200) } null diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt index 8e399a4..e8b4e68 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt @@ -59,7 +59,7 @@ fun D.fxProperty(spec: WritableDevicePropertySpec): onChange { newValue -> if (newValue != null) { - write(spec, newValue) + set(spec, newValue) } } } From b66e23cca6d5ce97e91d896b19d6657871cdb78f Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 7 May 2023 16:39:58 +0300 Subject: [PATCH 08/14] Clean up property access syntax --- .../space/kscience/controls/api/Device.kt | 4 +- .../kscience/controls/spec/DeviceBase.kt | 85 +++++++++++-------- .../kscience/controls/spec/DeviceBySpec.kt | 29 +++++++ .../controls/spec/DevicePropertySpec.kt | 68 +++++---------- .../controls/spec/deviceExtensions.kt | 2 +- .../controls/opcua/client/OpcUaClientTest.kt | 4 +- 6 files changed, 102 insertions(+), 90 deletions(-) create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt index a9dab80..5056123 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt @@ -39,7 +39,7 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope { public val actionDescriptors: Collection /** - * Read physical state of property and update/push notifications if needed. + * Read the physical state of property and update/push notifications if needed. */ public suspend fun readProperty(propertyName: String): Meta @@ -71,7 +71,7 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope { * Send an action request and suspend caller while request is being processed. * Could return null if request does not return a meaningful answer. */ - public suspend fun execute(action: String, argument: Meta? = null): Meta? + public suspend fun execute(actionName: String, argument: Meta? = null): Meta? /** * Initialize the device. This function suspends until the device is finished initialization diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt index e74ca2d..bafe688 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt @@ -13,10 +13,29 @@ import space.kscience.dataforge.meta.Meta import kotlin.coroutines.CoroutineContext +@OptIn(InternalDeviceAPI::class) +private suspend fun WritableDevicePropertySpec.writeMeta(device: D, item: Meta) { + write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter")) +} + +@OptIn(InternalDeviceAPI::class) +private suspend fun DevicePropertySpec.readMeta(device: D): Meta? = + read(device)?.let(converter::objectToMeta) + + +private 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) } +} + + /** * A base abstractions for [Device], introducing specifications for properties */ -@OptIn(InternalDeviceAPI::class) public abstract class DeviceBase( override val context: Context = Global, override val meta: Meta = Meta.EMPTY, @@ -83,10 +102,17 @@ public abstract class DeviceBase( * The logical state is updated after read */ override suspend fun readProperty(propertyName: String): Meta { - val newValue = properties[propertyName]?.readMeta(self) - ?: error("A property with name $propertyName is not registered in $this") - updateLogical(propertyName, newValue) - return newValue + val spec = properties[propertyName] ?: error("Property with name $propertyName not found") + val meta = spec.readMeta(self) ?: error("Failed to read property $propertyName") + updateLogical(propertyName, meta) + return meta + } + + public suspend fun readPropertyOrNull(propertyName: String): Meta? { + val spec = properties[propertyName] ?: return null + val meta = spec.readMeta(self) ?: return null + updateLogical(propertyName, meta) + return meta } override fun getProperty(propertyName: String): Meta? = logicalState[propertyName] @@ -98,40 +124,27 @@ public abstract class DeviceBase( } override suspend fun writeProperty(propertyName: String, value: Meta): Unit { - //If there is a physical property with a given name, invalidate logical property and write physical one - (properties[propertyName] as? WritableDevicePropertySpec)?.let { - invalidate(propertyName) - it.writeMeta(self, value) - } ?: run { - updateLogical(propertyName, value) + when (val property = properties[propertyName]) { + null -> { + //If there is a physical property with a given name, invalidate logical property and write physical one + updateLogical(propertyName, value) + } + + is WritableDevicePropertySpec -> { + invalidate(propertyName) + property.writeMeta(self, value) + } + + else -> { + error("Property $property is not writeable") + } } } - override suspend fun execute(action: String, argument: Meta?): Meta? = - actions[action]?.executeWithMeta(self, argument) + override suspend fun execute(actionName: String, argument: Meta?): Meta? { + val spec = actions[actionName] ?: error("Action with name $actionName not found") + return spec.executeWithMeta(self, argument) + } } -/** - * 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() - } -} - diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt new file mode 100644 index 0000000..e8a071c --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt @@ -0,0 +1,29 @@ +package space.kscience.controls.spec + +import space.kscience.controls.api.Device +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Global +import space.kscience.dataforge.meta.Meta + +/** + * 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() + } +} \ 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 index 53b3953..ede312a 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt @@ -10,7 +10,6 @@ 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 @@ -36,17 +35,14 @@ public interface DevicePropertySpec { */ @InternalDeviceAPI public suspend fun read(device: D): T? + } /** - * Property name, should be unique in device + * Property name should be unique in a 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 { /** @@ -54,11 +50,7 @@ public interface WritableDevicePropertySpec : DevicePropertySp */ @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 { @@ -82,29 +74,16 @@ public interface DeviceActionSpec { */ 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 D.read(propertySpec: DevicePropertySpec): T = + propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Can't convert result from meta") /** * Read typed value and update/push event if needed. * Return null if property read is not successful or property is undefined. */ -@OptIn(InternalDeviceAPI::class) -public suspend fun D.readOrNull(propertySpec: DevicePropertySpec): T? { - val res = propertySpec.read(this) ?: return null - @Suppress("UNCHECKED_CAST") - (this as? DeviceBase)?.updateLogical(propertySpec, res) - return res -} +public suspend fun > D.readOrNull(propertySpec: DevicePropertySpec): T? = + readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject) -public suspend fun D.read(propertySpec: DevicePropertySpec): T = - readOrNull(propertySpec) ?: error("Failed to read property ${propertySpec.name} state") public operator fun D.get(propertySpec: DevicePropertySpec): T? = getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject) @@ -112,34 +91,17 @@ public operator fun D.get(propertySpec: DevicePropertySpec /** * Write typed property state and invalidate logical state */ -@OptIn(InternalDeviceAPI::class) public suspend fun D.write(propertySpec: WritableDevicePropertySpec, value: T) { - invalidate(propertySpec.name) - propertySpec.write(this, value) - //perform asynchronous read and update after write - launch { - read(propertySpec) - } + writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value)) } /** * Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope */ -public operator fun D.set(propertySpec: WritableDevicePropertySpec, value: T): Unit { - launch { - write(propertySpec, value) - } +public operator fun D.set(propertySpec: WritableDevicePropertySpec, value: T): Job = launch { + write(propertySpec, value) } -/** - * Reset the logical state of a property - */ -public suspend fun D.invalidate(propertySpec: DevicePropertySpec) { - invalidate(propertySpec.name) -} - -public suspend fun D.execute(actionSpec: DeviceActionSpec, input: I? = null): O? = - actionSpec.execute(this, input) /** * A type safe property change listener @@ -152,4 +114,14 @@ public fun Device.onPropertyChange( .filter { it.property == spec.name } .onEach { change -> change.callback(spec.converter.metaToObject(change.value)) - }.launchIn(this) \ No newline at end of file + }.launchIn(this) + +/** + * Reset the logical state of a property + */ +public suspend fun D.invalidate(propertySpec: DevicePropertySpec) { + invalidate(propertySpec.name) +} + +public suspend fun D.execute(actionSpec: DeviceActionSpec, input: I? = null): O? = + actionSpec.execute(this, input) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt index ed48d39..55fc216 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt @@ -9,7 +9,7 @@ import kotlin.time.Duration /** * Perform a recurring asynchronous read action and return a flow of results. - * The flow is lazy so action is not performed unless flow is consumed. + * The flow is lazy, so action is not performed unless flow is consumed. * The flow uses called context. In order to call it on device context, use `flowOn(coroutineContext)`. * * The flow is canceled when the device scope is canceled diff --git a/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt index d16cb1e..e9b4c69 100644 --- a/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt +++ b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt @@ -28,8 +28,6 @@ class OpcUaClientTest { return DemoOpcUaDevice(config) } - inline fun use(block: (DemoOpcUaDevice) -> R): R = build().use(block) - val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble) } @@ -40,7 +38,7 @@ class OpcUaClientTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun testReadDouble() = runTest { - DemoOpcUaDevice.use{ + DemoOpcUaDevice.build().use{ println(it.read(DemoOpcUaDevice.randomDouble)) } } From 4c33c16c94a2d59b5377b3253fd86411b7414122 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 7 May 2023 18:03:46 +0300 Subject: [PATCH 09/14] Fix demo --- .../kscience/controls/spec/DeviceBase.kt | 3 + .../controls/spec/DevicePropertySpec.kt | 12 +++- .../controls/demo/DemoControllerView.kt | 4 +- .../kscience/controls/demo/DemoDevice.kt | 2 +- .../controls/demo/demoDeviceServer.kt | 58 ++++++++----------- .../space/kscience/magix/api/MagixEndpoint.kt | 2 +- .../magix/rsocket/RSocketMagixEndpoint.kt | 2 +- 7 files changed, 41 insertions(+), 42 deletions(-) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt index bafe688..0f5436b 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt @@ -108,6 +108,9 @@ public abstract class DeviceBase( return meta } + /** + * Read property if it exists and read correctly. Return null otherwise. + */ public suspend fun readPropertyOrNull(propertyName: String): Meta? { val spec = properties[propertyName] ?: return null val meta = spec.readMeta(self) ?: return null 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 index ede312a..4feace7 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt @@ -1,5 +1,6 @@ package space.kscience.controls.spec +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance @@ -19,6 +20,9 @@ import space.kscience.dataforge.meta.transformations.MetaConverter @RequiresOptIn("This API should not be called outside of Device internals") public annotation class InternalDeviceAPI +/** + * Specification for a device read-only property + */ public interface DevicePropertySpec { /** * Property descriptor @@ -75,7 +79,7 @@ public interface DeviceActionSpec { public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name public suspend fun D.read(propertySpec: DevicePropertySpec): T = - propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Can't convert result from meta") + propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Property read result is not valid") /** * Read typed value and update/push event if needed. @@ -102,9 +106,8 @@ public operator fun D.set(propertySpec: WritableDevicePropertySp write(propertySpec, value) } - /** - * A type safe property change listener + * A type safe property change listener. Uses the device [CoroutineScope]. */ public fun Device.onPropertyChange( spec: DevicePropertySpec, @@ -123,5 +126,8 @@ public suspend fun D.invalidate(propertySpec: DevicePropertySpec D.execute(actionSpec: DeviceActionSpec, input: I? = null): O? = actionSpec.execute(this, input) \ No newline at end of file diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt index 538570d..b464611 100644 --- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt @@ -56,12 +56,12 @@ class DemoController : Controller(), ContextAware { RSocketMagixFlowPlugin(), //TCP rsocket support ZmqMagixFlowPlugin() //ZMQ support ) - //Launch device client and connect it to the server + //Launch a device client and connect it to the server val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost") deviceManager.connectToMagix(deviceEndpoint) //connect visualization to a magix endpoint val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") - visualizer = visualEndpoint.startDemoDeviceServer() + visualizer = startDemoDeviceServer(visualEndpoint) //serve devices as OPC-UA namespace opcUaServer.startup() 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 1a8ad97..b576b9a 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 @@ -41,7 +41,7 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(DemoDe val cos by doubleProperty { val time = Instant.now() - kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState + kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * cosScaleState } val coordinates by metaProperty( diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/demoDeviceServer.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/demoDeviceServer.kt index 5b6c7cc..9ef69cf 100644 --- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/demoDeviceServer.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/demoDeviceServer.kt @@ -1,27 +1,24 @@ package space.kscience.controls.demo -import io.ktor.server.application.install -import io.ktor.server.cio.CIO import io.ktor.server.engine.ApplicationEngine -import io.ktor.server.engine.embeddedServer -import io.ktor.server.plugins.cors.routing.CORS -import io.ktor.server.websocket.WebSockets -import io.rsocket.kotlin.ktor.server.RSocketSupport +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.html.div import kotlinx.html.link import space.kscience.controls.api.PropertyChangedMessage import space.kscience.controls.client.controlsMagixFormat -import space.kscience.dataforge.meta.Meta +import space.kscience.controls.spec.name import space.kscience.dataforge.meta.double +import space.kscience.dataforge.meta.get import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.subscribe +import space.kscience.plotly.Plotly import space.kscience.plotly.layout import space.kscience.plotly.models.Trace import space.kscience.plotly.plot import space.kscience.plotly.server.PlotlyUpdateMode -import space.kscience.plotly.server.plotlyModule +import space.kscience.plotly.server.serve import space.kscience.plotly.trace import java.util.concurrent.ConcurrentLinkedQueue @@ -55,36 +52,26 @@ suspend fun Trace.updateXYFrom(flow: Flow>>) { } -@Suppress("ExtractKtorModule") -suspend fun MagixEndpoint.startDemoDeviceServer(): ApplicationEngine = embeddedServer(CIO, 9091) { - install(WebSockets) - install(RSocketSupport) +fun CoroutineScope.startDemoDeviceServer(magixEndpoint: MagixEndpoint): ApplicationEngine { + //share subscription to a parse message only once + val subscription = magixEndpoint.subscribe(controlsMagixFormat).shareIn(this, SharingStarted.Lazily) - install(CORS) { - anyHost() - } + val sinFlow = subscription.mapNotNull { (_, payload) -> + (payload as? PropertyChangedMessage)?.takeIf { it.property == DemoDevice.sin.name } + }.map { it.value } - val sinFlow = MutableSharedFlow()// = device.sin.flow() - val cosFlow = MutableSharedFlow()// = device.cos.flow() + val cosFlow = subscription.mapNotNull { (_, payload) -> + (payload as? PropertyChangedMessage)?.takeIf { it.property == DemoDevice.cos.name } + }.map { it.value } - launch { - subscribe(controlsMagixFormat).collect { (_, payload) -> - (payload as? PropertyChangedMessage)?.let { message -> - when (message.property) { - "sin" -> sinFlow.emit(message.value) - "cos" -> cosFlow.emit(message.value) - } - } - } - } + val sinCosFlow = subscription.mapNotNull { (_, payload) -> + (payload as? PropertyChangedMessage)?.takeIf { it.property == DemoDevice.coordinates.name } + }.map { it.value } - plotlyModule{ + return Plotly.serve(port = 9091, scope = this) { updateMode = PlotlyUpdateMode.PUSH - updateInterval = 50 + updateInterval = 100 page { container -> - val sinCosFlow = sinFlow.zip(cosFlow) { sin, cos -> - sin.double!! to cos.double!! - } link { rel = "stylesheet" href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" @@ -134,7 +121,9 @@ suspend fun MagixEndpoint.startDemoDeviceServer(): ApplicationEngine = embeddedS trace { name = "non-synchronized" launch { - val flow: Flow>> = sinCosFlow.windowed(30) + val flow: Flow>> = sinCosFlow.mapNotNull { + it["x"].double!! to it["y"].double!! + }.windowed(30) updateXYFrom(flow) } } @@ -144,5 +133,6 @@ suspend fun MagixEndpoint.startDemoDeviceServer(): ApplicationEngine = embeddedS } } -}.apply { start() } + +} diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixEndpoint.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixEndpoint.kt index 9baafea..0a5abd3 100644 --- a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixEndpoint.kt +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixEndpoint.kt @@ -9,7 +9,7 @@ import kotlinx.serialization.json.Json public interface MagixEndpoint { /** - * Subscribe to a [Flow] of messages + * Subscribe to a [Flow] of messages. Subscription is not guaranteed to be shared. */ public fun subscribe( filter: MagixMessageFilter = MagixMessageFilter.ALL, diff --git a/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketMagixEndpoint.kt b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketMagixEndpoint.kt index 43c505c..f9690fc 100644 --- a/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketMagixEndpoint.kt +++ b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketMagixEndpoint.kt @@ -75,7 +75,7 @@ public suspend fun MagixEndpoint.Companion.rSocketWithWebSockets( val rSocket = client.rSocket(host, port, path) - //Ensure client is closed after rSocket if finished + //Ensure the client is closed after rSocket if finished rSocket.coroutineContext[Job]?.invokeOnCompletion { client.close() } From 53e506893f517bcadbef3edf06fc54adef04aa32 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 7 May 2023 21:04:08 +0300 Subject: [PATCH 10/14] Add RPC for devices --- .../kscience/controls/api/DeviceMessage.kt | 6 + .../controls/manager/respondMessage.kt | 1 + controls-magix-client/build.gradle.kts | 15 ++- .../kscience/controls/client/DeviceClient.kt | 107 ++++++++++++++++++ controls-server/build.gradle.kts | 4 +- .../controls/server/deviceWebServer.kt | 4 - 6 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt 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 80a7f9d..0b4131e 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 @@ -125,12 +125,15 @@ public data class DescriptionMessage( /** * A request to execute an action. [targetDevice] is mandatory + * + * @param requestId action request id that should be returned in a response */ @Serializable @SerialName("action.execute") public data class ActionExecuteMessage( public val action: String, public val argument: Meta?, + public val requestId: String, override val sourceDevice: Name? = null, override val targetDevice: Name, override val comment: String? = null, @@ -141,12 +144,15 @@ public data class ActionExecuteMessage( /** * Asynchronous action result. [sourceDevice] is mandatory + * + * @param requestId request id passed in the request */ @Serializable @SerialName("action.result") public data class ActionResultMessage( public val action: String, public val result: Meta?, + public val requestId: String, override val sourceDevice: Name, override val targetDevice: Name? = null, override val comment: String? = null, 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 4db7894..13d072c 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 @@ -41,6 +41,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess ActionResultMessage( action = request.action, result = execute(request.action, request.argument), + requestId = request.requestId, sourceDevice = deviceTarget, targetDevice = request.sourceDevice ) diff --git a/controls-magix-client/build.gradle.kts b/controls-magix-client/build.gradle.kts index a94e770..0f108f4 100644 --- a/controls-magix-client/build.gradle.kts +++ b/controls-magix-client/build.gradle.kts @@ -3,14 +3,23 @@ plugins { `maven-publish` } -kscience{ +description = """ + Magix service for binding controls devices (both as RPC client and server +""".trimIndent() + +kscience { jvm() js() useSerialization { json() } dependencies { - implementation(project(":magix:magix-rsocket")) - implementation(project(":controls-core")) + implementation(projects.magix.magixApi) + implementation(projects.controlsCore) + implementation("com.benasher44:uuid:0.7.0") } +} + +readme { + } \ No newline at end of file diff --git a/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt new file mode 100644 index 0000000..16fd4b0 --- /dev/null +++ b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt @@ -0,0 +1,107 @@ +package space.kscience.controls.client + +import com.benasher44.uuid.uuid4 +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.newCoroutineContext +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import space.kscience.controls.api.* +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.names.Name +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.broadcast +import space.kscience.magix.api.subscribe +import kotlin.coroutines.CoroutineContext + +private fun stringUID() = uuid4().leastSignificantBits.toString(16) + +/** + * An implementation of device via RPC + */ +public class DeviceClient( + override val context: Context, + private val deviceName: Name, + incomingFlow: Flow, + private val send: suspend (DeviceMessage) -> Unit, +) : Device { + + override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext) + + private val mutex = Mutex() + + private val propertyCache = HashMap() + + override var propertyDescriptors: Collection = emptyList() + private set + + override var actionDescriptors: Collection = emptyList() + private set + + 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 + } + + is DescriptionMessage -> mutex.withLock { + propertyDescriptors = message.properties + actionDescriptors = message.actions + } + + else -> { + //ignore + } + } + }.launchIn(this) + } + + override val messageFlow: Flow get() = flowInternal + + + override suspend fun readProperty(propertyName: String): Meta { + send( + PropertyGetMessage(propertyName, targetDevice = deviceName) + ) + return flowInternal.filterIsInstance().first { + it.property == propertyName + }.value + } + + override fun getProperty(propertyName: String): Meta? = propertyCache[propertyName] + + override suspend fun invalidate(propertyName: String) { + mutex.withLock { + propertyCache.remove(propertyName) + } + } + + override suspend fun writeProperty(propertyName: String, value: Meta) { + send( + PropertySetMessage(propertyName, value, targetDevice = deviceName) + ) + } + + override suspend fun execute(actionName: String, argument: Meta?): Meta? { + val id = stringUID() + send( + ActionExecuteMessage(actionName, argument, id, targetDevice = deviceName) + ) + return flowInternal.filterIsInstance().first { + it.action == actionName && it.requestId == id + }.result + } +} + +/** + * Connect to a remote device via this client. + */ +public fun MagixEndpoint.remoteDevice(context: Context, magixTarget: String, deviceName: Name): DeviceClient { + val subscription = subscribe(controlsMagixFormat, originFilter = listOf(magixTarget)).map { it.second } + return DeviceClient(context, deviceName, subscription) { + broadcast(controlsMagixFormat, it, magixTarget, id = stringUID()) + } +} \ No newline at end of file diff --git a/controls-server/build.gradle.kts b/controls-server/build.gradle.kts index 0553b72..d661459 100644 --- a/controls-server/build.gradle.kts +++ b/controls-server/build.gradle.kts @@ -11,8 +11,8 @@ val dataforgeVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra dependencies { - implementation(project(":controls-core")) - implementation(project(":controls-ktor-tcp")) + implementation(projects.controlsCore) + implementation(projects.controlsKtorTcp) implementation(projects.magix.magixServer) implementation("io.ktor:ktor-server-cio:$ktorVersion") implementation("io.ktor:ktor-server-websockets:$ktorVersion") diff --git a/controls-server/src/main/kotlin/space/kscience/controls/server/deviceWebServer.kt b/controls-server/src/main/kotlin/space/kscience/controls/server/deviceWebServer.kt index 65ffb07..ba63583 100644 --- a/controls-server/src/main/kotlin/space/kscience/controls/server/deviceWebServer.kt +++ b/controls-server/src/main/kotlin/space/kscience/controls/server/deviceWebServer.kt @@ -43,10 +43,6 @@ import space.kscience.magix.server.magixModule private fun Application.deviceServerModule(manager: DeviceManager) { - install(WebSockets) -// install(CORS) { -// anyHost() -// } install(StatusPages) { exception { call, cause -> call.respond(HttpStatusCode.BadRequest, cause.message ?: "") From 7103786ec9384dbde75596a4e632b0b876a2757a Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 8 May 2023 15:39:34 +0300 Subject: [PATCH 11/14] Stress test demo --- .../controls/spec/deviceExtensions.kt | 18 ++- controls-magix-client/build.gradle.kts | 6 +- .../kscience/controls/client/DeviceClient.kt | 1 + .../kscience/controls/client/controlsMagix.kt | 6 +- .../kscience/controls/client/tangoMagix.kt | 4 +- demo/many-devices/README.md | 4 + demo/many-devices/build.gradle.kts | 39 ++++++ .../kscience/controls/demo/MassDevice.kt | 118 ++++++++++++++++++ .../space/kscience/magix/api/MagixEndpoint.kt | 4 +- .../kscience/magix/api/MagixMessageFilter.kt | 12 +- .../magix/rsocket/RSocketMagixEndpoint.kt | 14 +-- .../rsocket/RSocketStreamMagixEndpoint.kt | 2 +- .../space/kscience/magix/rsocket/withTcp.kt | 2 +- .../magix/server/RSocketMagixFlowPlugin.kt | 2 +- settings.gradle.kts | 1 + 15 files changed, 201 insertions(+), 32 deletions(-) create mode 100644 demo/many-devices/README.md create mode 100644 demo/many-devices/build.gradle.kts create mode 100644 demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt index 55fc216..af42f60 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt @@ -1,10 +1,12 @@ package space.kscience.controls.spec import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import space.kscience.controls.api.Device import kotlin.time.Duration /** @@ -14,19 +16,23 @@ import kotlin.time.Duration * * The flow is canceled when the device scope is canceled */ -public fun , R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow = flow { +public fun D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow = flow { while (isActive) { - kotlinx.coroutines.delay(interval) - emit(reader()) + delay(interval) + launch { + emit(reader()) + } } } /** * Do a recurring (with a fixed delay) task on a device. */ -public fun > D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch { +public fun D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch { while (isActive) { - kotlinx.coroutines.delay(interval) - task() + delay(interval) + launch { + task() + } } } \ No newline at end of file diff --git a/controls-magix-client/build.gradle.kts b/controls-magix-client/build.gradle.kts index 0f108f4..cbdefcc 100644 --- a/controls-magix-client/build.gradle.kts +++ b/controls-magix-client/build.gradle.kts @@ -14,9 +14,9 @@ kscience { json() } dependencies { - implementation(projects.magix.magixApi) - implementation(projects.controlsCore) - implementation("com.benasher44:uuid:0.7.0") + api(projects.magix.magixApi) + api(projects.controlsCore) + api("com.benasher44:uuid:0.7.0") } } diff --git a/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt index 16fd4b0..53a7704 100644 --- a/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt +++ b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt @@ -26,6 +26,7 @@ public class DeviceClient( private val send: suspend (DeviceMessage) -> Unit, ) : Device { + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext) private val mutex = Mutex() diff --git a/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt index be77a4a..f64c8e0 100644 --- a/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt +++ b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt @@ -32,7 +32,7 @@ public fun DeviceManager.connectToMagix( endpoint: MagixEndpoint, endpointID: String = controlsMagixFormat.defaultFormat, ): Job = context.launch { - endpoint.subscribe(controlsMagixFormat).onEach { (request, payload) -> + endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID)).onEach { (request, payload) -> val responsePayload = respondHubMessage(payload) if (responsePayload != null) { endpoint.broadcast( @@ -44,7 +44,7 @@ public fun DeviceManager.connectToMagix( ) } }.catch { error -> - logger.error(error) { "Error while responding to message" } + logger.error(error) { "Error while responding to message: ${error.message}" } }.launchIn(this) hubMessageFlow(this).onEach { payload -> @@ -55,7 +55,7 @@ public fun DeviceManager.connectToMagix( id = "df[${payload.hashCode()}]" ) }.catch { error -> - logger.error(error) { "Error while sending a message" } + logger.error(error) { "Error while sending a message: ${error.message}" } }.launchIn(this) } diff --git a/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt index ddec8d6..48ae3c5 100644 --- a/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt +++ b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt @@ -66,7 +66,9 @@ internal val tangoMagixFormat = MagixFormat( setOf("tango") ) - +/** + * Controls-kt device binding for Tango-flavored magix loop + */ public fun DeviceManager.launchTangoMagix( endpoint: MagixEndpoint, endpointID: String = TANGO_MAGIX_FORMAT, diff --git a/demo/many-devices/README.md b/demo/many-devices/README.md new file mode 100644 index 0000000..5d9c12f --- /dev/null +++ b/demo/many-devices/README.md @@ -0,0 +1,4 @@ +# Module all-things + + + diff --git a/demo/many-devices/build.gradle.kts b/demo/many-devices/build.gradle.kts new file mode 100644 index 0000000..d360ffa --- /dev/null +++ b/demo/many-devices/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + kotlin("jvm") + application +} + + +repositories { + mavenCentral() + maven("https://repo.kotlin.link") +} + +val ktorVersion: String by rootProject.extra +val rsocketVersion: String by rootProject.extra + +dependencies { + implementation(projects.magix.magixServer) + implementation(projects.controlsMagixClient) + implementation(projects.magix.magixRsocket) + + implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation("space.kscience:plotlykt-server:0.5.3") + implementation(spclibs.logback.classic) +} + +kotlin{ + jvmToolchain(11) +} + + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") + } +} + + +application { + mainClass.set("space.kscience.controls.demo.DemoControllerViewKt") +} \ No newline at end of file diff --git a/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt b/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt new file mode 100644 index 0000000..a62b928 --- /dev/null +++ b/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt @@ -0,0 +1,118 @@ +package space.kscience.controls.demo + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import space.kscience.controls.client.connectToMagix +import space.kscience.controls.client.controlsMagixFormat +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.controls.spec.* +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.context.request +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.int +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.subscribe +import space.kscience.magix.rsocket.rSocketWithTcp +import space.kscience.magix.rsocket.rSocketWithWebSockets +import space.kscience.magix.server.RSocketMagixFlowPlugin +import space.kscience.magix.server.startMagixServer +import space.kscience.plotly.Plotly +import space.kscience.plotly.layout +import space.kscience.plotly.plot +import space.kscience.plotly.scatter +import space.kscience.plotly.server.PlotlyUpdateMode +import space.kscience.plotly.server.serve +import space.kscience.plotly.server.show +import java.util.concurrent.ConcurrentHashMap +import kotlin.random.Random +import kotlin.time.Duration.Companion.milliseconds + + +class MassDevice(context: Context, meta: Meta) : DeviceBySpec(MassDevice, context, meta) { + private val rng = Random(meta["seed"].int ?: 0) + + private val randomValue get() = rng.nextDouble() + + companion object : DeviceSpec(), Factory { + + override fun build(context: Context, meta: Meta): MassDevice = MassDevice(context, meta) + + val value by doubleProperty { randomValue } + + override suspend fun MassDevice.onOpen() { + doRecurring(200.milliseconds) { + read(value) + } + } + } +} + +fun main() { + val context = Context("Mass") + + context.startMagixServer( + RSocketMagixFlowPlugin() + ) + + val numDevices = 1000 + + repeat(numDevices) { + val deviceContext = Context("Device${it}") { + plugin(DeviceManager) + } + + val deviceManager = deviceContext.request(DeviceManager) + + deviceManager.install("device$it", MassDevice) + + deviceContext.launch(Dispatchers.Default) { + val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + deviceManager.connectToMagix(deviceEndpoint, "device$it") + } + } + + val application = Plotly.serve(port = 9091, scope = context) { + updateMode = PlotlyUpdateMode.PUSH + updateInterval = 1000 + page { container -> + plot(renderer = container) { + layout { + title = "Latest event" + } + scatter { + launch(Dispatchers.Default) { + val monitorEndpoint = MagixEndpoint.rSocketWithTcp("localhost") + + val latest = ConcurrentHashMap() + + monitorEndpoint.subscribe(controlsMagixFormat).onEach { (magixMessage, payload) -> + latest[magixMessage.origin] = payload.time ?: Clock.System.now() + }.launchIn(this) + + while (isActive) { + delay(1000) + val now = Clock.System.now() + x.strings = latest.keys + y.numbers = latest.values.map { now.minus(it).inWholeMilliseconds / 1000.0 } + } + } + } + } + } + } + + application.show() + + while (readlnOrNull().isNullOrBlank()) { + + } +} diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixEndpoint.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixEndpoint.kt index 0a5abd3..35cb8e5 100644 --- a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixEndpoint.kt +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixEndpoint.kt @@ -19,9 +19,7 @@ public interface MagixEndpoint { /** * Send an event */ - public suspend fun broadcast( - message: MagixMessage, - ) + public suspend fun broadcast(message: MagixMessage) /** * Close the endpoint and the associated connection if it exists diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixMessageFilter.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixMessageFilter.kt index f497b97..4ce1995 100644 --- a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixMessageFilter.kt +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixMessageFilter.kt @@ -10,6 +10,12 @@ public data class MagixMessageFilter( val origin: Collection? = null, val target: Collection? = null, ) { + + public fun accepts(message: MagixMessage): Boolean = + format?.contains(message.format) ?: true + && origin?.contains(message.origin) ?: true + && target?.contains(message.target) ?: true + public companion object { public val ALL: MagixMessageFilter = MagixMessageFilter() } @@ -22,9 +28,5 @@ public fun Flow.filter(filter: MagixMessageFilter): Flow - filter.format?.contains(message.format) ?: true - && filter.origin?.contains(message.origin) ?: true - && filter.target?.contains(message.target) ?: true - } + return filter(filter::accepts) } \ No newline at end of file diff --git a/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketMagixEndpoint.kt b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketMagixEndpoint.kt index f9690fc..19cc8f8 100644 --- a/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketMagixEndpoint.kt +++ b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketMagixEndpoint.kt @@ -18,22 +18,20 @@ import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.MagixMessage import space.kscience.magix.api.MagixMessageFilter import space.kscience.magix.api.filter -import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext -public class RSocketMagixEndpoint( - private val rSocket: RSocket, - private val coroutineContext: CoroutineContext, -) : MagixEndpoint, Closeable { +public class RSocketMagixEndpoint(private val rSocket: RSocket) : MagixEndpoint, Closeable { override fun subscribe( filter: MagixMessageFilter, ): Flow { - val payload = buildPayload { data(MagixEndpoint.magixJson.encodeToString(MagixMessageFilter.serializer(), filter)) } + val payload = buildPayload { + data(MagixEndpoint.magixJson.encodeToString(MagixMessageFilter.serializer(), filter)) + } val flow = rSocket.requestStream(payload) return flow.map { MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), it.data.readText()) - }.filter(filter).flowOn(coroutineContext[CoroutineDispatcher] ?: Dispatchers.Unconfined) + }.filter(filter).flowOn(rSocket.coroutineContext[CoroutineDispatcher] ?: Dispatchers.Unconfined) } override suspend fun broadcast(message: MagixMessage): Unit = withContext(coroutineContext) { @@ -80,5 +78,5 @@ public suspend fun MagixEndpoint.Companion.rSocketWithWebSockets( client.close() } - return RSocketMagixEndpoint(rSocket, coroutineContext) + return RSocketMagixEndpoint(rSocket) } \ No newline at end of file diff --git a/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketStreamMagixEndpoint.kt b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketStreamMagixEndpoint.kt index 1875602..b6fd428 100644 --- a/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketStreamMagixEndpoint.kt +++ b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketStreamMagixEndpoint.kt @@ -26,7 +26,7 @@ import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext /** - * RSocket endpoint based on established channel. This way it works a bit faster than [RSocketMagixEndpoint] + * RSocket endpoint based on an established channel. This way it works a bit faster than [RSocketMagixEndpoint] * for sending and receiving, but less flexible in terms of filters. One general [streamFilter] could be set * in constructor and applied on the loop side. Filters in [subscribe] are applied on the endpoint side on top * of received data. diff --git a/magix/magix-rsocket/src/jvmMain/kotlin/space/kscience/magix/rsocket/withTcp.kt b/magix/magix-rsocket/src/jvmMain/kotlin/space/kscience/magix/rsocket/withTcp.kt index 23fe0be..211ce68 100644 --- a/magix/magix-rsocket/src/jvmMain/kotlin/space/kscience/magix/rsocket/withTcp.kt +++ b/magix/magix-rsocket/src/jvmMain/kotlin/space/kscience/magix/rsocket/withTcp.kt @@ -23,7 +23,7 @@ public suspend fun MagixEndpoint.Companion.rSocketWithTcp( ) val rSocket = buildConnector(rSocketConfig).connect(transport) - return RSocketMagixEndpoint(rSocket, coroutineContext) + return RSocketMagixEndpoint(rSocket) } diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt index a2f875d..7d5a34e 100644 --- a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt +++ b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt @@ -49,7 +49,7 @@ public class RSocketMagixFlowPlugin(public val port: Int = DEFAULT_MAGIX_RAW_POR val message = MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), request.data.readText()) magixFlow.emit(message) } - // bi-directional connection + // bidirectional connection, not covered by a standard requestChannel { request: Payload, input: Flow -> input.onEach { magixFlow.emit(MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), it.data.readText())) diff --git a/settings.gradle.kts b/settings.gradle.kts index a255044..21b2467 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -62,6 +62,7 @@ include( ":magix:magix-storage:magix-storage-xodus", ":controls-magix-client", ":demo:all-things", + ":demo:many-devices", ":demo:magix-demo", ":demo:car", ":demo:motors", From aabf2b85a495509dfaaa3f45ba4a8846dd61506d Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 8 May 2023 21:25:25 +0300 Subject: [PATCH 12/14] Documentation and DeviceSpec fix --- README.md | 9 ++ .../controls/spec/DevicePropertySpec.kt | 1 - .../kscience/controls/spec/DeviceSpec.kt | 33 ++-- .../controls/spec/propertySpecDelegates.kt | 21 +-- controls-magix-client/README.md | 2 +- .../controls/demo/DemoControllerView.kt | 1 + .../kscience/controls/demo/DemoDevice.kt | 63 ++++---- demo/many-devices/README.md | 2 +- .../kscience/controls/demo/MassDevice.kt | 6 +- docs/Device and DeviceSpec.md | 147 ++++++++++++++++++ docs/templates/README-TEMPLATE.md | 4 + 11 files changed, 230 insertions(+), 59 deletions(-) create mode 100644 docs/Device and DeviceSpec.md diff --git a/README.md b/README.md index e52bdac..098569b 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ Example view of a demo: ![](docs/pictures/demo-view.png) +## Documentation + +* [Creating a device](docs/Device%20and%20DeviceSpec.md) + ## Modules @@ -119,6 +123,11 @@ Example view of a demo: > > **Maturity**: EXPERIMENTAL +### [demo/many-devices](demo/many-devices) +> +> +> **Maturity**: EXPERIMENTAL + ### [demo/mks-pdr900](demo/mks-pdr900) > > 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 index 4feace7..078a43d 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt @@ -39,7 +39,6 @@ public interface DevicePropertySpec { */ @InternalDeviceAPI public suspend fun read(device: D): T? - } /** diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt index 966ee32..eb5c978 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt @@ -13,7 +13,6 @@ import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 - @OptIn(InternalDeviceAPI::class) public abstract class DeviceSpec { //initializing meta property for everyone @@ -38,20 +37,24 @@ public abstract class DeviceSpec { return deviceProperty } - public fun registerProperty( - converter: MetaConverter, - readOnlyProperty: KProperty1, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - ): DevicePropertySpec { - val deviceProperty = object : DevicePropertySpec { - 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) } - } - return registerProperty(deviceProperty) - } +// public fun registerProperty( +// converter: MetaConverter, +// readOnlyProperty: KProperty1, +// descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +// ): DevicePropertySpec { +// val deviceProperty = object : DevicePropertySpec { +// +// 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) +// } +// } +// return registerProperty(deviceProperty) +// } public fun property( converter: MetaConverter, diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt index e2afcab..ff89662 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt @@ -1,5 +1,6 @@ package space.kscience.controls.spec +import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.metaDescriptor import space.kscience.dataforge.meta.Meta @@ -10,7 +11,7 @@ import kotlin.properties.ReadOnlyProperty //read only delegates -public fun > DeviceSpec.booleanProperty( +public fun DeviceSpec.booleanProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.() -> Boolean? @@ -35,7 +36,7 @@ private inline fun numberDescriptor( descriptorBuilder() } -public fun > DeviceSpec.numberProperty( +public fun DeviceSpec.numberProperty( name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, read: suspend D.() -> Number? @@ -46,7 +47,7 @@ public fun > DeviceSpec.numberProperty( read ) -public fun > DeviceSpec.doubleProperty( +public fun DeviceSpec.doubleProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.() -> Double? @@ -57,7 +58,7 @@ public fun > DeviceSpec.doubleProperty( read ) -public fun > DeviceSpec.stringProperty( +public fun DeviceSpec.stringProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.() -> String? @@ -73,7 +74,7 @@ public fun > DeviceSpec.stringProperty( read ) -public fun > DeviceSpec.metaProperty( +public fun DeviceSpec.metaProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.() -> Meta? @@ -91,7 +92,7 @@ public fun > DeviceSpec.metaProperty( //read-write delegates -public fun > DeviceSpec.booleanProperty( +public fun DeviceSpec.booleanProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.() -> Boolean?, @@ -111,7 +112,7 @@ public fun > DeviceSpec.booleanProperty( ) -public fun > DeviceSpec.numberProperty( +public fun DeviceSpec.numberProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.() -> Number, @@ -119,7 +120,7 @@ public fun > DeviceSpec.numberProperty( ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) -public fun > DeviceSpec.doubleProperty( +public fun DeviceSpec.doubleProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.() -> Double, @@ -127,7 +128,7 @@ public fun > DeviceSpec.doubleProperty( ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) -public fun > DeviceSpec.stringProperty( +public fun DeviceSpec.stringProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.() -> String, @@ -135,7 +136,7 @@ public fun > DeviceSpec.stringProperty( ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) -public fun > DeviceSpec.metaProperty( +public fun DeviceSpec.metaProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.() -> Meta, diff --git a/controls-magix-client/README.md b/controls-magix-client/README.md index 2c1895e..9e3187b 100644 --- a/controls-magix-client/README.md +++ b/controls-magix-client/README.md @@ -1,6 +1,6 @@ # Module controls-magix-client - +Magix service for binding controls devices (both as RPC client and server ## Usage diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt index b464611..9eb8244 100644 --- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt @@ -147,6 +147,7 @@ class DemoControllerView : View(title = " Demo controller remote") { } } + class DemoControllerApp : App(DemoControllerView::class) { private val controller: DemoController by inject() 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 b576b9a..37b7b4b 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 @@ -1,6 +1,7 @@ package space.kscience.controls.demo import kotlinx.coroutines.launch +import space.kscience.controls.api.Device import space.kscience.controls.api.metaDescriptor import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context @@ -10,39 +11,47 @@ import space.kscience.dataforge.meta.ValueType import space.kscience.dataforge.meta.descriptors.value import space.kscience.dataforge.meta.transformations.MetaConverter import java.time.Instant +import kotlin.math.cos +import kotlin.math.sin import kotlin.time.Duration.Companion.milliseconds -class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(DemoDevice, context, meta) { - private var timeScaleState = 5000.0 - private var sinScaleState = 1.0 - private var cosScaleState = 1.0 +interface IDemoDevice: Device { + var timeScaleState: Double + var sinScaleState: Double + var cosScaleState: Double + fun time(): Instant = Instant.now() + fun sinValue(): Double + fun cosValue(): Double +} - companion object : DeviceSpec(), Factory { +class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(Companion, context, meta), IDemoDevice { + override var timeScaleState = 5000.0 + override var sinScaleState = 1.0 + override var cosScaleState = 1.0 + + override fun sinValue(): Double = sin(time().toEpochMilli().toDouble() / timeScaleState) * sinScaleState + + override fun cosValue(): Double = cos(time().toEpochMilli().toDouble() / timeScaleState) * cosScaleState + + companion object : DeviceSpec(), Factory { override fun build(context: Context, meta: Meta): DemoDevice = DemoDevice(context, meta) // register virtual properties based on actual object state - val timeScale by mutableProperty(MetaConverter.double, DemoDevice::timeScaleState) { + val timeScale by mutableProperty(MetaConverter.double, IDemoDevice::timeScaleState) { metaDescriptor { type(ValueType.NUMBER) } info = "Real to virtual time scale" } - val sinScale by mutableProperty(MetaConverter.double, DemoDevice::sinScaleState) - val cosScale by mutableProperty(MetaConverter.double, DemoDevice::cosScaleState) + val sinScale by mutableProperty(MetaConverter.double, IDemoDevice::sinScaleState) + val cosScale by mutableProperty(MetaConverter.double, IDemoDevice::cosScaleState) - val sin by doubleProperty { - val time = Instant.now() - kotlin.math.sin(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState - } - - val cos by doubleProperty { - val time = Instant.now() - kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * cosScaleState - } + val sin by doubleProperty(read = IDemoDevice::sinValue) + val cos by doubleProperty(read = IDemoDevice::cosValue) val coordinates by metaProperty( descriptorBuilder = { @@ -52,15 +61,21 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(DemoDe } ) { Meta { - val time = Instant.now() - "time" put time.toEpochMilli() + "time" put time().toEpochMilli() "x" put read(sin) "y" put read(cos) } } - override suspend fun DemoDevice.onOpen() { + val resetScale by action(MetaConverter.meta, MetaConverter.meta) { + write(timeScale, 5000.0) + write(sinScale, 1.0) + write(cosScale, 1.0) + null + } + + override suspend fun IDemoDevice.onOpen() { launch { read(sinScale) read(cosScale) @@ -72,13 +87,5 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(DemoDe read(coordinates) } } - - val resetScale by action(MetaConverter.meta, MetaConverter.meta) { - write(timeScale, 5000.0) - write(sinScale, 1.0) - write(cosScale, 1.0) - null - } - } } \ No newline at end of file diff --git a/demo/many-devices/README.md b/demo/many-devices/README.md index 5d9c12f..cd860b5 100644 --- a/demo/many-devices/README.md +++ b/demo/many-devices/README.md @@ -1,4 +1,4 @@ -# Module all-things +# Module many-devices diff --git a/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt b/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt index a62b928..36e8253 100644 --- a/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt +++ b/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt @@ -49,7 +49,7 @@ class MassDevice(context: Context, meta: Meta) : DeviceBySpec(MassDe val value by doubleProperty { randomValue } override suspend fun MassDevice.onOpen() { - doRecurring(200.milliseconds) { + doRecurring(2.milliseconds) { read(value) } } @@ -63,7 +63,7 @@ fun main() { RSocketMagixFlowPlugin() ) - val numDevices = 1000 + val numDevices = 100 repeat(numDevices) { val deviceContext = Context("Device${it}") { @@ -99,7 +99,7 @@ fun main() { }.launchIn(this) while (isActive) { - delay(1000) + delay(200) val now = Clock.System.now() x.strings = latest.keys y.numbers = latest.values.map { now.minus(it).inWholeMilliseconds / 1000.0 } diff --git a/docs/Device and DeviceSpec.md b/docs/Device and DeviceSpec.md new file mode 100644 index 0000000..37dbeda --- /dev/null +++ b/docs/Device and DeviceSpec.md @@ -0,0 +1,147 @@ +# Device and DeviceSpec - what is the difference? + +One of the problems with creating device servers is that one needs device properties to be accessible both in static and dynamic mode. For example, consider a property: + +```kotlin +var property: Double = 1.0 + +``` + +We can change the state of the property, but neither propagate this change to the device, nor observe changes made to the property value by the device. The propagation to the device state could be added via custom getters and setters: + +```kotlin +var property: Double + get() = device.read(...) + set(value){ + device.write(..., value) + } +``` + +But this approach does not solve the observability problem. Neither it exposes the property to be automatically collected from the outside of the device + +The next stop is to use Kotlin delegates: + +```kotlin +var property by property( + read = { device.read(...)}, + write = {value-> device.write(..., value)} +) +``` + +Delegate solves almost all problems: it allows reading and writing the hardware, also it allows registering observation handles to listen to property changes externally (one needs to use [delegate providers](https://kotlinlang.org/docs/delegated-properties.html#providing-a-delegate) to register properties eagerly on instance creation. The only problem left is that properties registered this way are created on object instance creation and not accessible without creating the device instance. + +In order to solve this problem `Controls-kt` allows to separate device properties specification from the device itself. + +Check [DemoDevice](../demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt) for an example of a device with a specification. + +```kotlin +interface IDemoDevice: Device { + var timeScaleState: Double + var sinScaleState: Double + var cosScaleState: Double + + fun time(): Instant = Instant.now() + fun sinValue(): Double + fun cosValue(): Double +} + +class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(Companion, context, meta), IDemoDevice { + override var timeScaleState = 5000.0 + override var sinScaleState = 1.0 + override var cosScaleState = 1.0 + + override fun sinValue(): Double = sin(time().toEpochMilli().toDouble() / timeScaleState) * sinScaleState + + override fun cosValue(): Double = cos(time().toEpochMilli().toDouble() / timeScaleState) * cosScaleState + + companion object : DeviceSpec(), Factory { + + override fun build(context: Context, meta: Meta): DemoDevice = DemoDevice(context, meta) + + // register virtual properties based on actual object state + val timeScale by mutableProperty(MetaConverter.double, IDemoDevice::timeScaleState) { + metaDescriptor { + type(ValueType.NUMBER) + } + info = "Real to virtual time scale" + } + + val sinScale by mutableProperty(MetaConverter.double, IDemoDevice::sinScaleState) + val cosScale by mutableProperty(MetaConverter.double, IDemoDevice::cosScaleState) + + val sin by doubleProperty(read = IDemoDevice::sinValue) + val cos by doubleProperty(read = IDemoDevice::cosValue) + + val coordinates by metaProperty( + descriptorBuilder = { + metaDescriptor { + value("time", ValueType.NUMBER) + } + } + ) { + Meta { + "time" put time().toEpochMilli() + "x" put read(sin) + "y" put read(cos) + } + } + + + val resetScale by action(MetaConverter.meta, MetaConverter.meta) { + write(timeScale, 5000.0) + write(sinScale, 1.0) + write(cosScale, 1.0) + null + } + + override suspend fun IDemoDevice.onOpen() { + launch { + read(sinScale) + read(cosScale) + read(timeScale) + } + doRecurring(50.milliseconds) { + read(sin) + read(cos) + read(coordinates) + } + } + } +} +``` + +## Device body + +Device inherits the class `DeviceBySpec` and takes the specification as an argument. The device itself contains hardware logic, but not communication logic. For example, it does not define properties exposed to the external observers. In the given example, it stores states for virtual properties (states) and contains logic to request current values for two properties. + +States for logical properties could also be stored via device mechanics without explicit state variables. + +## Device specification + +Specification is an object (singleton) that defines property scheme for external communication. Specification could define the following components: + +* Properties specifications via `property` delegate or specialized delegate variants. +* Action specification via `action` delegate or specialized delegates. +* Initialization logic (override `onOpen`). +* Finalization logic (override `onClose`). + +Properties can reference properties and method of the device. They also could contain device-independent logic or manipulate properties (like `coordinates` property in the example does). It is not recommended to implement direct device integration from the spec (yet it is possible). + +## Device specification abstraction + +In the example, the specification is a companion for `DemoDevice` and could be used as a factory for the device. Yet it works with the abstraction `IDemoDevice`. It is done to demonstrate that the device logic could be separated from the hardware logic. For example, one could swap a real device or a virtual device anytime without changing integrations anywhere. There could be also layers of abstractions for a device. + +## Access to properties + +In order to access property values, one needs to use both the device instance and property descriptor from the spec like follows: +```kotlin +val device = DemoDevice.build() + +val res = device.read(DemoDevice.sin) + +``` + +## Other ways to create a device + +It is not obligatory to use `DeviceBySpec` to define a `Device`. One could directly implement the `Device` interface or use intermediate abstraction `DeviceBase`, which uses properties schema but allows to define it manually. + diff --git a/docs/templates/README-TEMPLATE.md b/docs/templates/README-TEMPLATE.md index 57f30c1..11e0905 100644 --- a/docs/templates/README-TEMPLATE.md +++ b/docs/templates/README-TEMPLATE.md @@ -35,6 +35,10 @@ Example view of a demo: ![](docs/pictures/demo-view.png) +## Documentation + +* [Creating a device](docs/Device%20and%20DeviceSpec.md) + ## Modules ${modules} From 1d7403ef3034366cf6d61fd445fb296f2e2afa3f Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 9 May 2023 22:50:35 +0300 Subject: [PATCH 13/14] Update to stable DataForge --- build.gradle.kts | 2 +- .../controls/manager/DeviceManager.kt | 2 - .../space/kscience/controls/ports/Ports.kt | 3 -- .../kscience/controls/api/MessageTest.kt | 18 +++++++++ .../kscience/controls/ports/TcpPortPlugin.kt | 3 -- .../controls/ports/KtorTcpPortPlugin.kt | 3 -- .../controls/serial/SerialPortPlugin.kt | 3 -- demo/many-devices/build.gradle.kts | 3 +- .../kscience/controls/demo/MassDevice.kt | 40 ++++++++++--------- docs/Device and DeviceSpec.md | 2 +- .../rsocket/RSocketStreamMagixEndpoint.kt | 7 ++-- .../space/kscience/magix/rsocket/withTcp.kt | 4 +- .../linuxX64Main/kotlin/rsocket/withTcp.kt | 3 +- .../magix/server/RSocketMagixFlowPlugin.kt | 2 +- .../kscince/magix/zmq/ZmqMagixEndpoint.kt | 6 +-- .../kscince/magix/zmq/ZmqMagixFlowPlugin.kt | 6 +-- 16 files changed, 58 insertions(+), 49 deletions(-) create mode 100644 controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 7327460..172bdbc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { id("space.kscience.gradle.project") } -val dataforgeVersion: String by extra("0.6.1-dev-4") +val dataforgeVersion: String by extra("0.6.1") val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion) val rsocketVersion by extra("0.15.4") val xodusVersion by extra("2.0.1") 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 c1a943a..871f236 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 @@ -11,7 +11,6 @@ import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.NameToken import kotlin.collections.set import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KClass /** * DataForge Context plugin that allows to manage devices locally @@ -33,7 +32,6 @@ public class DeviceManager : AbstractPlugin(), DeviceHub { public companion object : PluginFactory { override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP) - override val type: KClass = DeviceManager::class override fun build(context: Context, meta: Meta): DeviceManager = DeviceManager() } 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 index 6d5c6d9..3d01e62 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Ports.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Ports.kt @@ -3,7 +3,6 @@ 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 /** * A DataForge plugin for managing ports @@ -32,8 +31,6 @@ public class Ports : AbstractPlugin() { 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() } diff --git a/controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt b/controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt new file mode 100644 index 0000000..719738a --- /dev/null +++ b/controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt @@ -0,0 +1,18 @@ +package space.kscience.controls.api + +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import space.kscience.controls.spec.asMeta +import kotlin.test.Test +import kotlin.test.assertEquals + +class MessageTest { + @Test + fun messageSerialization() { + val changedMessage = PropertyChangedMessage("test", 22.0.asMeta()) + val json = Json.encodeToString(changedMessage) + val reconstructed: PropertyChangedMessage = Json.decodeFromString(json) + assertEquals(changedMessage.time, reconstructed.time) + } +} \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/TcpPortPlugin.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/TcpPortPlugin.kt index 23174af..592d0c3 100644 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/TcpPortPlugin.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/TcpPortPlugin.kt @@ -6,7 +6,6 @@ import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.PluginTag import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.Name -import kotlin.reflect.KClass public class TcpPortPlugin : AbstractPlugin() { @@ -21,8 +20,6 @@ public class TcpPortPlugin : AbstractPlugin() { override val tag: PluginTag = PluginTag("controls.ports.tcp", group = PluginTag.DATAFORGE_GROUP) - override val type: KClass = TcpPortPlugin::class - override fun build(context: Context, meta: Meta): TcpPortPlugin = TcpPortPlugin() } diff --git a/controls-ktor-tcp/src/main/kotlin/space/kscience/controls/ports/KtorTcpPortPlugin.kt b/controls-ktor-tcp/src/main/kotlin/space/kscience/controls/ports/KtorTcpPortPlugin.kt index 1256455..079f457 100644 --- a/controls-ktor-tcp/src/main/kotlin/space/kscience/controls/ports/KtorTcpPortPlugin.kt +++ b/controls-ktor-tcp/src/main/kotlin/space/kscience/controls/ports/KtorTcpPortPlugin.kt @@ -6,7 +6,6 @@ import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.PluginTag import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.Name -import kotlin.reflect.KClass public class KtorTcpPortPlugin : AbstractPlugin() { @@ -21,8 +20,6 @@ public class KtorTcpPortPlugin : AbstractPlugin() { override val tag: PluginTag = PluginTag("controls.ports.serial", group = PluginTag.DATAFORGE_GROUP) - override val type: KClass = KtorTcpPortPlugin::class - override fun build(context: Context, meta: Meta): KtorTcpPortPlugin = KtorTcpPortPlugin() } diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt b/controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt index 871d266..6b573ae 100644 --- a/controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt +++ b/controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt @@ -7,7 +7,6 @@ import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.PluginTag import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.Name -import kotlin.reflect.KClass public class SerialPortPlugin : AbstractPlugin() { @@ -22,8 +21,6 @@ public class SerialPortPlugin : AbstractPlugin() { override val tag: PluginTag = PluginTag("controls.ports.serial", group = PluginTag.DATAFORGE_GROUP) - override val type: KClass = SerialPortPlugin::class - override fun build(context: Context, meta: Meta): SerialPortPlugin = SerialPortPlugin() } diff --git a/demo/many-devices/build.gradle.kts b/demo/many-devices/build.gradle.kts index d360ffa..8c765d6 100644 --- a/demo/many-devices/build.gradle.kts +++ b/demo/many-devices/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(projects.magix.magixServer) implementation(projects.controlsMagixClient) implementation(projects.magix.magixRsocket) + implementation(projects.magix.magixZmq) implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("space.kscience:plotlykt-server:0.5.3") @@ -35,5 +36,5 @@ tasks.withType().configureEach application { - mainClass.set("space.kscience.controls.demo.DemoControllerViewKt") + mainClass.set("space.kscience.controls.demo.MassDeviceKt") } \ No newline at end of file diff --git a/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt b/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt index 36e8253..6370d8d 100644 --- a/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt +++ b/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt @@ -21,14 +21,13 @@ import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.int import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.subscribe -import space.kscience.magix.rsocket.rSocketWithTcp -import space.kscience.magix.rsocket.rSocketWithWebSockets +import space.kscience.magix.rsocket.rSocketStreamWithTcp import space.kscience.magix.server.RSocketMagixFlowPlugin import space.kscience.magix.server.startMagixServer import space.kscience.plotly.Plotly +import space.kscience.plotly.bar import space.kscience.plotly.layout import space.kscience.plotly.plot -import space.kscience.plotly.scatter import space.kscience.plotly.server.PlotlyUpdateMode import space.kscience.plotly.server.serve import space.kscience.plotly.server.show @@ -49,7 +48,7 @@ class MassDevice(context: Context, meta: Meta) : DeviceBySpec(MassDe val value by doubleProperty { randomValue } override suspend fun MassDevice.onOpen() { - doRecurring(2.milliseconds) { + doRecurring(100.milliseconds) { read(value) } } @@ -60,23 +59,25 @@ fun main() { val context = Context("Mass") context.startMagixServer( - RSocketMagixFlowPlugin() + RSocketMagixFlowPlugin(), +// ZmqMagixFlowPlugin() ) val numDevices = 100 - repeat(numDevices) { - val deviceContext = Context("Device${it}") { - plugin(DeviceManager) - } + context.launch(Dispatchers.IO) { + repeat(numDevices) { + val deviceContext = Context("Device${it}") { + plugin(DeviceManager) + } - val deviceManager = deviceContext.request(DeviceManager) + val deviceManager = deviceContext.request(DeviceManager) - deviceManager.install("device$it", MassDevice) + deviceManager.install("device$it", MassDevice) - deviceContext.launch(Dispatchers.Default) { - val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") - deviceManager.connectToMagix(deviceEndpoint, "device$it") + val endpointId = "device$it" + val deviceEndpoint = MagixEndpoint.rSocketStreamWithTcp("localhost") + deviceManager.connectToMagix(deviceEndpoint, endpointId) } } @@ -88,9 +89,9 @@ fun main() { layout { title = "Latest event" } - scatter { - launch(Dispatchers.Default) { - val monitorEndpoint = MagixEndpoint.rSocketWithTcp("localhost") + bar { + launch(Dispatchers.Default){ + val monitorEndpoint = MagixEndpoint.rSocketStreamWithTcp("localhost") val latest = ConcurrentHashMap() @@ -101,8 +102,9 @@ fun main() { while (isActive) { delay(200) val now = Clock.System.now() - x.strings = latest.keys - y.numbers = latest.values.map { now.minus(it).inWholeMilliseconds / 1000.0 } + val sorted = latest.mapKeys { it.key.substring(6).toInt() }.toSortedMap() + x.numbers = sorted.keys + y.numbers = sorted.values.map { now.minus(it).inWholeMilliseconds / 1000.0 } } } } diff --git a/docs/Device and DeviceSpec.md b/docs/Device and DeviceSpec.md index 37dbeda..65f6026 100644 --- a/docs/Device and DeviceSpec.md +++ b/docs/Device and DeviceSpec.md @@ -143,5 +143,5 @@ val res = device.read(DemoDevice.sin) ## Other ways to create a device -It is not obligatory to use `DeviceBySpec` to define a `Device`. One could directly implement the `Device` interface or use intermediate abstraction `DeviceBase`, which uses properties schema but allows to define it manually. +It is not obligatory to use `DeviceBySpec` to define a `Device`. One could directly implement the `Device` interface or use intermediate abstraction `DeviceBase`, which uses properties' schema but allows to define it manually. diff --git a/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketStreamMagixEndpoint.kt b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketStreamMagixEndpoint.kt index b6fd428..e4eae79 100644 --- a/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketStreamMagixEndpoint.kt +++ b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketStreamMagixEndpoint.kt @@ -26,7 +26,7 @@ import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext /** - * RSocket endpoint based on an established channel. This way it works a bit faster than [RSocketMagixEndpoint] + * RSocket endpoint based on an established channel. This way it works a lot faster than [RSocketMagixEndpoint] * for sending and receiving, but less flexible in terms of filters. One general [streamFilter] could be set * in constructor and applied on the loop side. Filters in [subscribe] are applied on the endpoint side on top * of received data. @@ -78,6 +78,7 @@ public suspend fun MagixEndpoint.Companion.rSocketStreamWithWebSockets( host: String, port: Int = DEFAULT_MAGIX_HTTP_PORT, path: String = "/rsocket", + filter: MagixMessageFilter = MagixMessageFilter.ALL, rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {}, ): RSocketStreamMagixEndpoint { val client = HttpClient { @@ -89,10 +90,10 @@ public suspend fun MagixEndpoint.Companion.rSocketStreamWithWebSockets( val rSocket = client.rSocket(host, port, path) - //Ensure client is closed after rSocket if finished + //Ensure the client is closed after rSocket if finished rSocket.coroutineContext[Job]?.invokeOnCompletion { client.close() } - return RSocketStreamMagixEndpoint(rSocket, coroutineContext) + return RSocketStreamMagixEndpoint(rSocket, coroutineContext, filter) } \ No newline at end of file diff --git a/magix/magix-rsocket/src/jvmMain/kotlin/space/kscience/magix/rsocket/withTcp.kt b/magix/magix-rsocket/src/jvmMain/kotlin/space/kscience/magix/rsocket/withTcp.kt index 211ce68..9dc0abd 100644 --- a/magix/magix-rsocket/src/jvmMain/kotlin/space/kscience/magix/rsocket/withTcp.kt +++ b/magix/magix-rsocket/src/jvmMain/kotlin/space/kscience/magix/rsocket/withTcp.kt @@ -4,6 +4,7 @@ import io.ktor.network.sockets.SocketOptions import io.rsocket.kotlin.core.RSocketConnectorBuilder import io.rsocket.kotlin.transport.ktor.tcp.TcpClientTransport import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixMessageFilter import kotlin.coroutines.coroutineContext @@ -30,6 +31,7 @@ public suspend fun MagixEndpoint.Companion.rSocketWithTcp( public suspend fun MagixEndpoint.Companion.rSocketStreamWithTcp( host: String, port: Int = DEFAULT_MAGIX_RAW_PORT, + filter: MagixMessageFilter = MagixMessageFilter.ALL, tcpConfig: SocketOptions.TCPClientSocketOptions.() -> Unit = {}, rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {}, ): RSocketStreamMagixEndpoint { @@ -40,5 +42,5 @@ public suspend fun MagixEndpoint.Companion.rSocketStreamWithTcp( ) val rSocket = buildConnector(rSocketConfig).connect(transport) - return RSocketStreamMagixEndpoint(rSocket, coroutineContext) + return RSocketStreamMagixEndpoint(rSocket, coroutineContext, filter) } \ No newline at end of file diff --git a/magix/magix-rsocket/src/linuxX64Main/kotlin/rsocket/withTcp.kt b/magix/magix-rsocket/src/linuxX64Main/kotlin/rsocket/withTcp.kt index 117bcdd..efa8217 100644 --- a/magix/magix-rsocket/src/linuxX64Main/kotlin/rsocket/withTcp.kt +++ b/magix/magix-rsocket/src/linuxX64Main/kotlin/rsocket/withTcp.kt @@ -6,7 +6,6 @@ import io.rsocket.kotlin.transport.ktor.tcp.TcpClientTransport import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.rsocket.RSocketMagixEndpoint import space.kscience.magix.rsocket.buildConnector -import kotlin.coroutines.coroutineContext /** @@ -25,5 +24,5 @@ public suspend fun MagixEndpoint.Companion.rSocketWithTcp( ) val rSocket = buildConnector(rSocketConfig).connect(transport) - return RSocketMagixEndpoint(rSocket, coroutineContext) + return RSocketMagixEndpoint(rSocket) } diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt index 7d5a34e..3116513 100644 --- a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt +++ b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt @@ -49,7 +49,7 @@ public class RSocketMagixFlowPlugin(public val port: Int = DEFAULT_MAGIX_RAW_POR val message = MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), request.data.readText()) magixFlow.emit(message) } - // bidirectional connection, not covered by a standard + // bidirectional connection, used for streaming connection requestChannel { request: Payload, input: Flow -> input.onEach { magixFlow.emit(MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), it.data.readText())) diff --git a/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt b/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt index d8c8343..e878ce9 100644 --- a/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt +++ b/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt @@ -13,7 +13,6 @@ import space.kscience.magix.api.MagixMessage import space.kscience.magix.api.MagixMessageFilter import space.kscience.magix.api.filter import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.coroutineContext public class ZmqMagixEndpoint( private val host: String, @@ -71,7 +70,8 @@ public class ZmqMagixEndpoint( } } -public suspend fun MagixEndpoint.Companion.zmq( +public fun MagixEndpoint.Companion.zmq( + scope: CoroutineScope, host: String, protocol: String = "tcp", pubPort: Int = DEFAULT_MAGIX_ZMQ_PUB_PORT, @@ -81,5 +81,5 @@ public suspend fun MagixEndpoint.Companion.zmq( protocol, pubPort, pullPort, - coroutineContext = coroutineContext + coroutineContext = scope.coroutineContext ) \ No newline at end of file diff --git a/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt b/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt index 1f075ac..e66d326 100644 --- a/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt +++ b/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt @@ -24,13 +24,13 @@ public class ZmqMagixFlowPlugin( val logger = LoggerFactory.getLogger("magix-server-zmq") ZContext().use { context -> - //launch publishing job + //launch the publishing job val pubSocket = context.createSocket(SocketType.PUB) pubSocket.bind("$localHost:$zmqPubSocketPort") magixFlow.onEach { message -> val string = MagixEndpoint.magixJson.encodeToString(message) pubSocket.send(string) - logger.debug("Published: $string") + logger.trace("Published: $string") }.launchIn(this) //launch pulling job @@ -41,7 +41,7 @@ public class ZmqMagixFlowPlugin( while (isActive) { val string: String? = pullSocket.recvStr() if (string != null) { - logger.debug("Received: $string") + logger.trace("Received: $string") val message = MagixEndpoint.magixJson.decodeFromString(string) magixFlow.emit(message) } From 5e91ec9a9710ea4032bc42523e557f967b5a45cc Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 12 May 2023 13:35:34 +0300 Subject: [PATCH 14/14] update version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 172bdbc..93a689c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ val xodusVersion by extra("2.0.1") allprojects { group = "space.kscience" - version = "0.1.1-SNAPSHOT" + version = "0.2.0-dev-1" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") }