From 4c93b5c9b3feda0321fe95d54266821b6df7b0c7 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 23 Aug 2023 16:37:35 +0300 Subject: [PATCH 01/71] Update readme --- README.md | 8 +++++--- build.gradle.kts | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5b40c3..5518d1f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ [![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) -# Controls.kt +[![DOI](https://zenodo.org/badge/240888288.svg)](https://zenodo.org/badge/latestdoi/240888288) -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. +# 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, +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: diff --git a/build.gradle.kts b/build.gradle.kts index df7c664..8dfafdc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,7 @@ ksciencePublish { "https://maven.pkg.jetbrains.space/spc/p/sci/maven" } ) + sonatype("https://oss.sonatype.org") } readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md") \ No newline at end of file From 0f610a5e198e9f3f263f38915236454aec6709c8 Mon Sep 17 00:00:00 2001 From: darksnake Date: Thu, 24 Aug 2023 16:25:17 +0300 Subject: [PATCH 02/71] Fix mass demo plot --- .../space/kscience/controls/demo/MassDevice.kt | 12 +++++------- magix/magix-zmq/build.gradle.kts | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) 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 5c89c26..040d288 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 @@ -23,10 +23,7 @@ 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.bar -import space.kscience.plotly.layout -import space.kscience.plotly.plot +import space.kscience.plotly.* import space.kscience.plotly.server.PlotlyUpdateMode import space.kscience.plotly.server.serve import space.kscience.plotly.server.show @@ -88,9 +85,10 @@ suspend fun main() { updateMode = PlotlyUpdateMode.PUSH updateInterval = 1000 page { container -> - plot(renderer = container) { + plot(renderer = container, config = PlotlyConfig { saveAsSvg() }) { layout { - title = "Latest event" +// title = "Latest event" + xaxis.title = "Device number" yaxis.title = "Maximum latency in ms" } @@ -119,7 +117,7 @@ suspend fun main() { latest.clear() max.clear() x.numbers = sorted.keys - y.numbers = sorted.values.map { it.inWholeMilliseconds / 1000.0 + 0.0001 } + y.numbers = sorted.values.map { it.inWholeMicroseconds / 1000.0 + 0.0001 } } } } diff --git a/magix/magix-zmq/build.gradle.kts b/magix/magix-zmq/build.gradle.kts index cf4ee9b..20cc246 100644 --- a/magix/magix-zmq/build.gradle.kts +++ b/magix/magix-zmq/build.gradle.kts @@ -12,7 +12,7 @@ description = """ dependencies { api(projects.magix.magixApi) api("org.slf4j:slf4j-api:2.0.6") - api("org.zeromq:jeromq:0.5.2") + api("org.zeromq:jeromq:0.5.3") } readme { From cc36ef805bdb91aee6f2a9c42acccb41c27f271a Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 4 Sep 2023 14:56:18 +0300 Subject: [PATCH 03/71] update version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8dfafdc..60e263d 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.2.0" + version = "0.2.1-dev-1" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } From 036bef1adb4daa3053f0e5a4d1f3e64d445e60b8 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 16 Sep 2023 15:54:36 +0300 Subject: [PATCH 04/71] fix dataforge version --- build.gradle.kts | 2 +- controls-pi/build.gradle.kts | 8 +++++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 60e263d..b18f0a2 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.2-dev-3") +val dataforgeVersion: String by extra("0.6.2") 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-pi/build.gradle.kts b/controls-pi/build.gradle.kts index a763396..cf7bfbe 100644 --- a/controls-pi/build.gradle.kts +++ b/controls-pi/build.gradle.kts @@ -7,10 +7,12 @@ description = """ Utils to work with controls-kt on Raspberry pi """.trimIndent() +val pi4jVerstion = "2.3.0" + dependencies{ api(project(":controls-core")) api("com.pi4j:pi4j-ktx:2.4.0") // Kotlin DSL - api("com.pi4j:pi4j-core:2.3.0") - api("com.pi4j:pi4j-plugin-raspberrypi:2.3.0") - api("com.pi4j:pi4j-plugin-pigpio:2.3.0") + api("com.pi4j:pi4j-core:$pi4jVerstion") + api("com.pi4j:pi4j-plugin-raspberrypi:$pi4jVerstion") + api("com.pi4j:pi4j-plugin-pigpio:$pi4jVerstion") } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fae0804..db9a6b8 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.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From bc5037b256c5df6250f18c03e2ae7d2e7cd3f594 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 16 Sep 2023 16:09:47 +0300 Subject: [PATCH 05/71] fix dataforge version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index b18f0a2..619948b 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.2.1-dev-1" + version = "0.2.1" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } From 8b6a6abd9245e07d53402f724ff6f9f6f7c47f09 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 18 Sep 2023 09:00:04 +0300 Subject: [PATCH 06/71] Update to PiPlugin logic --- build.gradle.kts | 2 +- .../space/kscience/controls/pi/PiPlugin.kt | 24 +++++++++++++++++++ .../kscience/controls/pi/PiSerialPort.kt | 23 +++++++++++------- docs/templates/README-TEMPLATE.md | 2 ++ 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 619948b..d188449 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.2.1" + version = "0.2.2-dev-1" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt b/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt index 547a142..20d4c80 100644 --- a/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt +++ b/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt @@ -1,22 +1,46 @@ package space.kscience.controls.pi +import com.pi4j.Pi4J +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.ports.PortFactory import space.kscience.controls.ports.Ports import space.kscience.dataforge.context.AbstractPlugin import space.kscience.dataforge.context.Context 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 space.kscience.dataforge.names.parseAsName +import com.pi4j.context.Context as PiContext public class PiPlugin : AbstractPlugin() { public val ports: Ports by require(Ports) + public val devices: DeviceManager by require(DeviceManager) override val tag: PluginTag get() = Companion.tag + public val piContext: PiContext by lazy { createPiContext(context, meta) } + + override fun content(target: String): Map = when (target) { + PortFactory.TYPE -> mapOf( + PiSerialPort.type.parseAsName() to PiSerialPort, + ) + + else -> super.content(target) + } + + override fun detach() { + piContext.shutdown() + super.detach() + } + public companion object : PluginFactory { override val tag: PluginTag = PluginTag("controls.ports.pi", group = PluginTag.DATAFORGE_GROUP) override fun build(context: Context, meta: Meta): PiPlugin = PiPlugin() + public fun createPiContext(context: Context, meta: Meta): PiContext = Pi4J.newAutoContext() + } } \ No newline at end of file diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiSerialPort.kt b/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiSerialPort.kt index 4924b8d..6b9662d 100644 --- a/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiSerialPort.kt +++ b/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiSerialPort.kt @@ -1,6 +1,5 @@ package space.kscience.controls.pi -import com.pi4j.Pi4J import com.pi4j.io.serial.Baud import com.pi4j.io.serial.Serial import com.pi4j.io.serial.SerialConfigBuilder @@ -13,20 +12,25 @@ import space.kscience.controls.ports.toArray import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.error import space.kscience.dataforge.context.logger +import space.kscience.dataforge.context.request import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.enum import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.string import java.nio.ByteBuffer import kotlin.coroutines.CoroutineContext +import com.pi4j.context.Context as PiContext public class PiSerialPort( context: Context, coroutineContext: CoroutineContext = context.coroutineContext, - public val serialBuilder: () -> Serial, + public val serialBuilder: PiContext.() -> Serial, ) : AbstractPort(context, coroutineContext) { - private val serial: Serial by lazy { serialBuilder() } + private val serial: Serial by lazy { + val pi = context.request(PiPlugin) + pi.piContext.serialBuilder() + } private val listenerJob = this.scope.launch(Dispatchers.IO) { @@ -57,15 +61,18 @@ public class PiSerialPort( public companion object : PortFactory { override val type: String get() = "pi" - public fun open(context: Context, device: String, block: SerialConfigBuilder.() -> Unit): PiSerialPort = - PiSerialPort(context) { - Pi4J.newAutoContext().serial(device, block) - } + public fun open( + context: Context, + device: String, + block: SerialConfigBuilder.() -> Unit, + ): PiSerialPort = PiSerialPort(context) { + serial(device, block) + } override fun build(context: Context, meta: Meta): Port = PiSerialPort(context) { val device: String = meta["device"].string ?: error("Device name not defined") val baudRate: Baud = meta["baudRate"].enum() ?: Baud._9600 - Pi4J.newAutoContext().serial(device) { + serial(device) { baud8N1(baudRate) } } diff --git a/docs/templates/README-TEMPLATE.md b/docs/templates/README-TEMPLATE.md index 11e0905..ef2cb74 100644 --- a/docs/templates/README-TEMPLATE.md +++ b/docs/templates/README-TEMPLATE.md @@ -1,5 +1,7 @@ [![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) +[![](https://maven.sciprog.center/api/badge/latest/kscience/space/kscience/controls-core-jvm?color=40c14a&name=repo.kotlin.link&prefix=v)](https://maven.sciprog.center/) + # 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. From aef94767c5b6689868fb897318a484a9f7c8c539 Mon Sep 17 00:00:00 2001 From: darksnake Date: Mon, 18 Sep 2023 13:38:45 +0300 Subject: [PATCH 07/71] Fix all-things demo --- build.gradle.kts | 1 + demo/all-things/build.gradle.kts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index d188449..aa9f9fb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,6 +7,7 @@ plugins { } val dataforgeVersion: String by extra("0.6.2") +val visionforgeVersion by extra("0.3.0-dev-10") 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/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts index 05d7395..bb6d5ec 100644 --- a/demo/all-things/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -12,6 +12,7 @@ repositories { val ktorVersion: String by rootProject.extra val rsocketVersion: String by rootProject.extra +val visionforgeVersion: String by rootProject.extra dependencies { implementation(projects.controlsCore) @@ -24,7 +25,8 @@ dependencies { implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("no.tornado:tornadofx:1.7.20") - implementation("space.kscience:plotlykt-server:0.5.3") + implementation("space.kscience:plotlykt-server:0.6.0") + implementation("space.kscience:visionforge-plotly:$visionforgeVersion") // implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") implementation(spclibs.logback.classic) } From a51510606f2c5681a34d9137535e302f539a0b21 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 24 Sep 2023 13:02:52 +0300 Subject: [PATCH 08/71] add customizeable scopes to property listeners --- .../commonMain/kotlin/space/kscience/controls/api/Device.kt | 4 ++-- .../space/kscience/controls/spec/DevicePropertySpec.kt | 6 ++++-- .../kotlin/space/kscience/controls/modbus/ModbusDevice.kt | 4 ---- 3 files changed, 6 insertions(+), 8 deletions(-) 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 4fc6365..8a19e68 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 @@ -124,5 +124,5 @@ public fun Device.getAllProperties(): Meta = Meta { /** * Subscribe on property changes for the whole device */ -public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job = - messageFlow.filterIsInstance().onEach(callback).launchIn(this) +public fun Device.onPropertyChange(scope: CoroutineScope = this, callback: suspend PropertyChangedMessage.() -> Unit): Job = + messageFlow.filterIsInstance().onEach(callback).launchIn(scope) 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 cf8c741..7f3aa06 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 @@ -115,6 +115,7 @@ public fun D.propertyFlow(spec: DevicePropertySpec): Flow< */ public fun D.onPropertyChange( spec: DevicePropertySpec, + scope: CoroutineScope = this, callback: suspend PropertyChangedMessage.(T) -> Unit, ): Job = messageFlow .filterIsInstance() @@ -124,15 +125,16 @@ public fun D.onPropertyChange( if (newValue != null) { change.callback(newValue) } - }.launchIn(this) + }.launchIn(scope) /** * Call [callback] on initial property value and each value change */ public fun D.useProperty( spec: DevicePropertySpec, + scope: CoroutineScope = this, callback: suspend (T) -> Unit, -): Job = launch { +): Job = scope.launch { callback(read(spec)) messageFlow .filterIsInstance() 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 index ea4330c..3522f15 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt @@ -183,10 +183,6 @@ public fun ModbusDevice.writeHoldingRegisters(address: Int, buffer: ByteBuffer): return writeHoldingRegisters(address, array) } -public fun ModbusDevice.writeShortRegister(address: Int, value: Short) { - master.writeSingleRegister(address, SimpleInputRegister(value.toInt())) -} - public fun ModbusDevice.modbusRegister( address: Int, ): ReadWriteProperty = object : ReadWriteProperty { From a337daee9319ab2de541dbe9caa0f9ba0e64be76 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 24 Sep 2023 13:21:01 +0300 Subject: [PATCH 09/71] Add read-after-write for DeviceBase property writers --- .../kscience/controls/spec/DeviceBase.kt | 21 ++++++++++++------- .../kscience/controls/spec/DeviceSpec.kt | 2 +- .../sciprog/devices/mks/MksPdr900Device.kt | 14 ++++++------- .../pimotionmaster/PiMotionMasterDevice.kt | 4 ++-- 4 files changed, 23 insertions(+), 18 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 56f5aa9..dc43637 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 @@ -87,7 +87,7 @@ public abstract class DeviceBase( /** * Update logical property state and notify listeners */ - protected suspend fun updateLogical(propertyName: String, value: Meta?) { + protected suspend fun propertyChanged(propertyName: String, value: Meta?) { if (value != logicalState[propertyName]) { stateLock.withLock { logicalState[propertyName] = value @@ -99,10 +99,10 @@ public abstract class DeviceBase( } /** - * Update logical state using given [spec] and its convertor + * Notify the device that a property with [spec] value is changed */ - public suspend fun updateLogical(spec: DevicePropertySpec, value: T) { - updateLogical(spec.name, spec.converter.objectToMeta(value)) + protected suspend fun propertyChanged(spec: DevicePropertySpec, value: T) { + propertyChanged(spec.name, spec.converter.objectToMeta(value)) } /** @@ -112,7 +112,7 @@ public abstract class DeviceBase( override suspend fun readProperty(propertyName: String): Meta { 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) + propertyChanged(propertyName, meta) return meta } @@ -122,7 +122,7 @@ public abstract class DeviceBase( public suspend fun readPropertyOrNull(propertyName: String): Meta? { val spec = properties[propertyName] ?: return null val meta = spec.readMeta(self) ?: return null - updateLogical(propertyName, meta) + propertyChanged(propertyName, meta) return meta } @@ -137,13 +137,18 @@ public abstract class DeviceBase( override suspend fun writeProperty(propertyName: String, value: Meta): Unit { 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) + //If there are no physical properties with given name, write a logical one. + propertyChanged(propertyName, value) } is WritableDevicePropertySpec -> { + //if there is a writeable property with a given name, invalidate logical and write physical invalidate(propertyName) property.writeMeta(self, value) + // perform read after writing if the writer did not set the value + if (logicalState[propertyName] == null) { + readPropertyOrNull(propertyName) + } } else -> { 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 d05bf30..17c3ecb 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 @@ -22,7 +22,7 @@ public val MetaConverter.Companion.unit: MetaConverter get() = UnitMetaCon @OptIn(InternalDeviceAPI::class) public abstract class DeviceSpec { - //initializing meta property for everyone + //initializing the metadata property for everyone private val _properties = hashMapOf>( DeviceMetaPropertySpec.name to DeviceMetaPropertySpec ) 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 949ced9..7771709 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 @@ -49,16 +49,16 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec Date: Sun, 24 Sep 2023 13:29:15 +0300 Subject: [PATCH 10/71] Add read-after-write for DeviceBase property writers --- CHANGELOG.md | 30 ++++++++++++------- .../kscience/controls/spec/DeviceBase.kt | 3 +- demo/all-things/build.gradle.kts | 2 +- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f9d13..93d9645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ ## Unreleased +### Added + +### Changed + +### Deprecated + +### Removed + +### Fixed + +### Security + +## 0.2.2-dev-1 - 2023-09-24 + +### Changed +- updating logical state in `DeviceBase` is now protected and called `propertyChanged()` +- `DeviceBase` tries to read property after write if the writer does not set the value. + +## 0.2.1 - 2023-09-24 + ### Added - Core interfaces for building a device server - Magix service for binding controls devices (both as RPC client and server) @@ -20,13 +40,3 @@ - A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes. - Magix history database API - ZMQ client endpoint for Magix - -### Changed - -### Deprecated - -### Removed - -### Fixed - -### Security 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 dc43637..82a4d4f 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 @@ -147,7 +147,8 @@ public abstract class DeviceBase( property.writeMeta(self, value) // perform read after writing if the writer did not set the value if (logicalState[propertyName] == null) { - readPropertyOrNull(propertyName) + val meta = property.readMeta(self) + propertyChanged(propertyName, meta) } } diff --git a/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts index 05d7395..33b654e 100644 --- a/demo/all-things/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("no.tornado:tornadofx:1.7.20") - implementation("space.kscience:plotlykt-server:0.5.3") + implementation("space.kscience:plotlykt-server:0.6.0") // implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") implementation(spclibs.logback.classic) } From efe9a2e842f0c26193f43bac0ac3f4e3d7779231 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 2 Oct 2023 21:24:01 +0300 Subject: [PATCH 11/71] Fixex in modbus and write-protection for same meta --- CHANGELOG.md | 2 + build.gradle.kts | 2 +- .../kscience/controls/spec/DeviceBase.kt | 17 ++++- .../controls/modbus/DeviceProcessImage.kt | 69 +++++++++++++------ .../kscience/controls/modbus/ModbusDevice.kt | 2 +- 5 files changed, 65 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93d9645..b7673a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ ### Removed ### Fixed +- Property writing does not trigger change if logical state already is the same as value to be set. +- Modbus-slave triggers only once for multi-register write. ### Security diff --git a/build.gradle.kts b/build.gradle.kts index d188449..c851997 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.2.2-dev-1" + version = "0.2.2-dev-2" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } 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 82a4d4f..2a6773e 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 @@ -7,18 +7,24 @@ 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.context.debug import space.kscience.dataforge.context.error import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.misc.DFExperimental import kotlin.coroutines.CoroutineContext - +/** + * Write a meta [item] to [device] + */ @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")) } +/** + * Read Meta item from the [device] + */ @OptIn(InternalDeviceAPI::class) private suspend fun DevicePropertySpec.readMeta(device: D): Meta? = read(device)?.let(converter::objectToMeta) @@ -135,9 +141,14 @@ public abstract class DeviceBase( } override suspend fun writeProperty(propertyName: String, value: Meta): Unit { + //bypass property setting if it already has that value + if (logicalState[propertyName] == value) { + logger.debug { "Skipping setting $propertyName to $value because value is already set" } + return + } when (val property = properties[propertyName]) { null -> { - //If there are no physical properties with given name, write a logical one. + //If there are no registered physical properties with given name, write a logical one. propertyChanged(propertyName, value) } @@ -145,7 +156,7 @@ public abstract class DeviceBase( //if there is a writeable property with a given name, invalidate logical and write physical invalidate(propertyName) property.writeMeta(self, value) - // perform read after writing if the writer did not set the value + // perform read after writing if the writer did not set the value and the value is still in invalid state if (logicalState[propertyName] == null) { val meta = property.readMeta(self) propertyChanged(propertyName, meta) diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt index 2f56a8a..32adfb3 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt @@ -1,9 +1,9 @@ package space.kscience.controls.modbus import com.ghgande.j2mod.modbus.procimg.* -import io.ktor.utils.io.core.buildPacket -import io.ktor.utils.io.core.readByteBuffer -import io.ktor.utils.io.core.writeShort +import io.ktor.utils.io.core.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import space.kscience.controls.api.Device import space.kscience.controls.spec.DevicePropertySpec @@ -118,22 +118,48 @@ public class DeviceProcessImageBuilder internal constructor( } } + /** + * Trigger [block] if one of register changes. + */ + private fun List.onChange(block: (ByteReadPacket) -> Unit) { + var ready = false + + forEach { register -> + register.addObserver { _, _ -> + ready = true + } + } + + device.launch { + val builder = BytePacketBuilder() + while (isActive) { + delay(1) + if (ready) { + val packet = builder.apply { + forEach { value -> + writeShort(value.toShort()) + } + }.build() + block(packet) + ready = false + } + } + } + } + public fun bind(key: ModbusRegistryKey.HoldingRange, propertySpec: WritableDevicePropertySpec) { val registers = List(key.count) { ObservableRegister() } + registers.forEachIndexed { index, register -> - register.addObserver { _, _ -> - val packet = buildPacket { - registers.forEach { value -> - writeShort(value.toShort()) - } - } - device[propertySpec] = key.format.readObject(packet) - } image.addRegister(key.address + index, register) } + registers.onChange { packet -> + device[propertySpec] = key.format.readObject(packet) + } + device.useProperty(propertySpec) { value -> val packet = buildPacket { key.format.writeObject(this, value) @@ -182,20 +208,17 @@ public class DeviceProcessImageBuilder internal constructor( val registers = List(key.count) { ObservableRegister() } + registers.forEachIndexed { index, register -> - register.addObserver { _, _ -> - val packet = buildPacket { - registers.forEach { value -> - writeShort(value.toShort()) - } - } - device.launch { - device.action(key.format.readObject(packet)) - } - } image.addRegister(key.address + index, register) } + registers.onChange { packet -> + device.launch { + device.action(key.format.readObject(packet)) + } + } + return registers } @@ -205,11 +228,13 @@ public class DeviceProcessImageBuilder internal constructor( * Bind the device to Modbus slave (server) image. */ public fun D.bindProcessImage( + unitId: Int = 0, openOnBind: Boolean = true, binding: DeviceProcessImageBuilder.() -> Unit, ): ProcessImage { - val image = SimpleProcessImage() + val image = SimpleProcessImage(unitId) DeviceProcessImageBuilder(this, image).apply(binding) + image.setLocked(true) if (openOnBind) { launch { open() 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 index 3522f15..2f9e923 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt @@ -61,7 +61,7 @@ public interface ModbusDevice : Device { } public operator fun ModbusRegistryKey.HoldingRange.getValue(thisRef: Any?, property: KProperty<*>): T { - val packet = readInputRegistersToPacket(address, count) + val packet = readHoldingRegistersToPacket(address, count) return format.readObject(packet) } From 2cc0a5bcbca0cd43f77e7bd93242ad805bdd72e5 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 2 Oct 2023 22:12:11 +0300 Subject: [PATCH 12/71] Fixex in modbus and write-protection for same meta --- .../space/kscience/controls/spec/DeviceBase.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 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 2a6773e..5e07880 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 @@ -1,14 +1,16 @@ package space.kscience.controls.spec -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +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.context.debug -import space.kscience.dataforge.context.error import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.misc.DFExperimental @@ -65,13 +67,7 @@ public abstract class DeviceBase( get() = actions.values.map { it.descriptor } override val coroutineContext: CoroutineContext by lazy { - context.newCoroutineContext( - SupervisorJob(context.coroutineContext[Job]) + - CoroutineName("Device $this") + - CoroutineExceptionHandler { _, throwable -> - logger.error(throwable) { "Exception in device $this job" } - } - ) + context.newCoroutineContext(SupervisorJob(context.coroutineContext[Job]) + CoroutineName("Device $this")) } From 01606af30799d719472b7cc91125a73afccc890c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 5 Oct 2023 07:43:49 +0300 Subject: [PATCH 13/71] clientId -> unitId --- .../kscience/controls/modbus/ModbusDevice.kt | 34 +++++++++---------- .../controls/modbus/ModbusDeviceBySpec.kt | 2 +- .../controls/modbus/ModbusRegistryMap.kt | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) 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 index 2f9e923..7297616 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt @@ -21,9 +21,9 @@ import kotlin.reflect.KProperty public interface ModbusDevice : Device { /** - * Client id for this specific device + * Unit id for this specific device */ - public val clientId: Int + public val unitId: Int /** * The modubus master connector @@ -82,35 +82,35 @@ public interface ModbusDevice : Device { * Read multiple sequential modbus coils (bit-values) */ public fun ModbusDevice.readCoils(address: Int, count: Int): BitVector = - master.readCoils(clientId, address, count) + master.readCoils(unitId, address, count) public fun ModbusDevice.readCoil(address: Int): Boolean = - master.readCoils(clientId, address, 1).getBit(0) + master.readCoils(unitId, address, 1).getBit(0) public fun ModbusDevice.writeCoils(address: Int, values: BooleanArray) { val bitVector = BitVector(values.size) values.forEachIndexed { index, value -> bitVector.setBit(index, value) } - master.writeMultipleCoils(clientId, address, bitVector) + master.writeMultipleCoils(unitId, address, bitVector) } public fun ModbusDevice.writeCoil(address: Int, value: Boolean) { - master.writeCoil(clientId, address, value) + master.writeCoil(unitId, address, value) } public fun ModbusDevice.writeCoil(key: ModbusRegistryKey.Coil, value: Boolean) { - master.writeCoil(clientId, key.address, value) + master.writeCoil(unitId, key.address, value) } public fun ModbusDevice.readInputDiscretes(address: Int, count: Int): BitVector = - master.readInputDiscretes(clientId, address, count) + master.readInputDiscretes(unitId, address, count) public fun ModbusDevice.readInputDiscrete(address: Int): Boolean = - master.readInputDiscretes(clientId, address, 1).getBit(0) + master.readInputDiscretes(unitId, address, 1).getBit(0) public fun ModbusDevice.readInputRegisters(address: Int, count: Int): List = - master.readInputRegisters(clientId, address, count).toList() + master.readInputRegisters(unitId, address, count).toList() private fun Array.toBuffer(): ByteBuffer { val buffer: ByteBuffer = ByteBuffer.allocate(size * 2) @@ -129,10 +129,10 @@ private fun Array.toPacket(): ByteReadPacket = buildPacket { } public fun ModbusDevice.readInputRegistersToBuffer(address: Int, count: Int): ByteBuffer = - master.readInputRegisters(clientId, address, count).toBuffer() + master.readInputRegisters(unitId, address, count).toBuffer() public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): ByteReadPacket = - master.readInputRegisters(clientId, address, count).toPacket() + master.readInputRegisters(unitId, address, count).toPacket() public fun ModbusDevice.readDoubleInput(address: Int): Double = readInputRegistersToBuffer(address, Double.SIZE_BYTES).getDouble() @@ -141,7 +141,7 @@ public fun ModbusDevice.readInputRegister(address: Int): Short = readInputRegisters(address, 1).first().toShort() public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List = - master.readMultipleRegisters(clientId, address, count).toList() + master.readMultipleRegisters(unitId, address, count).toList() /** * Read a number of registers to a [ByteBuffer] @@ -149,10 +149,10 @@ public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List(values.size) { SimpleInputRegister(values[it].toInt()) } ) public fun ModbusDevice.writeHoldingRegister(address: Int, value: Short): Int = master.writeSingleRegister( - clientId, + unitId, address, SimpleInputRegister(value.toInt()) ) 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 916187f..48f48a4 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 @@ -15,7 +15,7 @@ import space.kscience.dataforge.names.NameToken public open class ModbusDeviceBySpec( context: Context, spec: DeviceSpec, - override val clientId: Int, + override val unitId: Int, override val master: AbstractModbusMaster, private val disposeMasterOnClose: Boolean = true, meta: Meta = Meta.EMPTY, 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 index a6fc894..6e3c1da 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt @@ -152,7 +152,7 @@ public abstract class ModbusRegistryMap { val rangeString = if (key.count == 1) { key.address.toString() } else { - "${key.address} - ${key.address + key.count}" + "${key.address} - ${key.address + key.count - 1}" } to.appendLine("${typeString}\t$rangeString\t$description") } From f1b63c3951c5da04a4e4ee7c836702dbc1086c02 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 7 Oct 2023 18:34:44 +0300 Subject: [PATCH 14/71] Add buffer to device messages --- .../kotlin/space/kscience/controls/spec/DeviceBase.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 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 5e07880..38aa20a 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 @@ -3,6 +3,7 @@ package space.kscience.controls.spec import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.newCoroutineContext @@ -13,6 +14,8 @@ import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.debug import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.int import space.kscience.dataforge.misc.DFExperimental import kotlin.coroutines.CoroutineContext @@ -47,7 +50,7 @@ private suspend fun DeviceActionSpec.executeWithMeta */ public abstract class DeviceBase( final override val context: Context, - override val meta: Meta = Meta.EMPTY, + final override val meta: Meta = Meta.EMPTY, ) : Device { /** @@ -76,7 +79,10 @@ public abstract class DeviceBase( */ private val logicalState: HashMap = HashMap() - private val sharedMessageFlow: MutableSharedFlow = MutableSharedFlow() + private val sharedMessageFlow: MutableSharedFlow = MutableSharedFlow( + replay = meta["message.buffer"].int ?: 1000, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) public override val messageFlow: SharedFlow get() = sharedMessageFlow From 290010fc8c2b71f789f8aa990080b8502b7325d8 Mon Sep 17 00:00:00 2001 From: darksnake Date: Thu, 19 Oct 2023 16:38:50 +0300 Subject: [PATCH 15/71] Add writeable flag to mutable properties --- .../kotlin/space/kscience/controls/spec/DeviceSpec.kt | 5 +++-- .../kotlin/space/kscience/controls/demo/demoDeviceServer.kt | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) 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 17c3ecb..0376635 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,7 +12,7 @@ import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 -public object UnitMetaConverter: MetaConverter{ +public object UnitMetaConverter : MetaConverter { override fun metaToObject(meta: Meta): Unit = Unit override fun objectToMeta(obj: Unit): Meta = Meta.EMPTY @@ -127,7 +127,8 @@ public abstract class DeviceSpec { PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val propertyName = name ?: property.name val deviceProperty = object : WritableDevicePropertySpec { - override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) + override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, writable = true) + .apply(descriptorBuilder) override val converter: MetaConverter = converter override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() } 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 fda8ead..5ff1287 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 @@ -57,15 +57,15 @@ fun CoroutineScope.startDemoDeviceServer(magixEndpoint: MagixEndpoint): Applicat //share subscription to a parse message only once val subscription = magixEndpoint.subscribe(DeviceManager.magixFormat).shareIn(this, SharingStarted.Lazily) - val sinFlow = subscription.mapNotNull { (_, payload) -> + val sinFlow = subscription.mapNotNull { (_, payload) -> (payload as? PropertyChangedMessage)?.takeIf { it.property == DemoDevice.sin.name } }.map { it.value } - val cosFlow = subscription.mapNotNull { (_, payload) -> + val cosFlow = subscription.mapNotNull { (_, payload) -> (payload as? PropertyChangedMessage)?.takeIf { it.property == DemoDevice.cos.name } }.map { it.value } - val sinCosFlow = subscription.mapNotNull { (_, payload) -> + val sinCosFlow = subscription.mapNotNull { (_, payload) -> (payload as? PropertyChangedMessage)?.takeIf { it.property == DemoDevice.coordinates.name } }.map { it.value } From 7f71d0c9e998eab4c27873071c9668a31cd74e50 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 20 Oct 2023 10:14:14 +0300 Subject: [PATCH 16/71] modbus registry to json rendering --- build.gradle.kts | 14 +- .../space/kscience/controls/ports/phrases.kt | 5 + controls-modbus/build.gradle.kts | 2 +- .../controls/modbus/ModbusRegistryMap.kt | 129 +++++++++++++----- gradle.properties | 2 +- 5 files changed, 107 insertions(+), 45 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f88ec4b..347a66e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,3 @@ -import space.kscience.gradle.isInDevelopment import space.kscience.gradle.useApache2Licence import space.kscience.gradle.useSPCTeam @@ -14,25 +13,18 @@ val xodusVersion by extra("2.0.1") allprojects { group = "space.kscience" - version = "0.2.2-dev-2" + version = "0.2.2-dev-3" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } } ksciencePublish { - pom("https://github.com/SciProgCentre/controls.kt") { + pom("https://github.com/SciProgCentre/controls-kt") { useApache2Licence() useSPCTeam() } - github("controls.kt", "SciProgCentre") - space( - if (isInDevelopment) { - "https://maven.pkg.jetbrains.space/spc/p/sci/dev" - } else { - "https://maven.pkg.jetbrains.space/spc/p/sci/maven" - } - ) + repository("spc","https://maven.sciprog.center/kscience") sonatype("https://oss.sonatype.org") } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt index 1214d01..02a0058 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt @@ -5,6 +5,7 @@ import io.ktor.utils.io.core.readBytes import io.ktor.utils.io.core.reset import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.transform /** @@ -16,6 +17,10 @@ public fun Flow.withDelimiter(delimiter: ByteArray): Flow val output = BytePacketBuilder() var matcherPosition = 0 + onCompletion { + output.close() + } + return transform { chunk -> chunk.forEach { byte -> output.writeByte(byte) diff --git a/controls-modbus/build.gradle.kts b/controls-modbus/build.gradle.kts index aee64d5..1ec39d6 100644 --- a/controls-modbus/build.gradle.kts +++ b/controls-modbus/build.gradle.kts @@ -12,7 +12,7 @@ description = """ dependencies { api(projects.controlsCore) - api("com.ghgande:j2mod:3.1.1") + api("com.ghgande:j2mod:3.2.0") } readme{ 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 index 6e3c1da..1d81d1b 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt @@ -1,8 +1,15 @@ package space.kscience.controls.modbus +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import space.kscience.dataforge.io.IOFormat +/** + * Modbus registry key + */ public sealed class ModbusRegistryKey { public abstract val address: Int public open val count: Int = 1 @@ -25,6 +32,9 @@ public sealed class ModbusRegistryKey { override fun toString(): String = "InputRegister(address=$address)" } + /** + * A range of read-only register encoding a single value + */ public class InputRange( address: Int, override val count: Int, @@ -36,10 +46,16 @@ public sealed class ModbusRegistryKey { } + /** + * A single read-write register + */ public open class HoldingRegister(override val address: Int) : ModbusRegistryKey() { override fun toString(): String = "HoldingRegister(address=$address)" } + /** + * A range of read-write registers encoding a single value + */ public class HoldingRange( address: Int, override val count: Int, @@ -52,6 +68,9 @@ public sealed class ModbusRegistryKey { } } +/** + * A base class for modbus registers + */ public abstract class ModbusRegistryMap { private val _entries: MutableMap = mutableMapOf() @@ -63,36 +82,56 @@ public abstract class ModbusRegistryMap { return key } + /** + * Register a [ModbusRegistryKey.Coil] key and return it + */ protected fun coil(address: Int, description: String = ""): ModbusRegistryKey.Coil = register(ModbusRegistryKey.Coil(address), description) + /** + * Register a [ModbusRegistryKey.DiscreteInput] key and return it + */ protected fun discrete(address: Int, description: String = ""): ModbusRegistryKey.DiscreteInput = register(ModbusRegistryKey.DiscreteInput(address), description) + /** + * Register a [ModbusRegistryKey.InputRegister] key and return it + */ protected fun input(address: Int, description: String = ""): ModbusRegistryKey.InputRegister = register(ModbusRegistryKey.InputRegister(address), description) + /** + * Register a [ModbusRegistryKey.InputRange] key and return it + */ protected fun input( address: Int, count: Int, reader: IOFormat, description: String = "", - ): ModbusRegistryKey.InputRange = - register(ModbusRegistryKey.InputRange(address, count, reader), description) + ): ModbusRegistryKey.InputRange = register(ModbusRegistryKey.InputRange(address, count, reader), description) + /** + * Register a [ModbusRegistryKey.HoldingRegister] key and return it + */ protected fun register(address: Int, description: String = ""): ModbusRegistryKey.HoldingRegister = register(ModbusRegistryKey.HoldingRegister(address), description) + /** + * Register a [ModbusRegistryKey.HoldingRange] key and return it + */ protected fun register( address: Int, count: Int, format: IOFormat, description: String = "", - ): ModbusRegistryKey.HoldingRange = - register(ModbusRegistryKey.HoldingRange(address, count, format), description) + ): ModbusRegistryKey.HoldingRange = register(ModbusRegistryKey.HoldingRange(address, count, format), description) public companion object { + + /** + * Validate the register map. Throw an error if the map is invalid + */ public fun validate(map: ModbusRegistryMap) { var lastCoil: ModbusRegistryKey.Coil? = null var lastDiscreteInput: ModbusRegistryKey.DiscreteInput? = null @@ -127,36 +166,62 @@ public abstract class ModbusRegistryMap { } } - private val ModbusRegistryKey.sectionNumber - get() = when (this) { - is ModbusRegistryKey.Coil -> 1 - is ModbusRegistryKey.DiscreteInput -> 2 - is ModbusRegistryKey.HoldingRegister -> 4 - is ModbusRegistryKey.InputRegister -> 3 - } + } +} - public fun print(map: ModbusRegistryMap, to: Appendable = System.out) { - validate(map) - map.entries.entries - .sortedWith( - Comparator.comparingInt?> { it.key.sectionNumber } - .thenComparingInt { it.key.address } - ) - .forEach { (key, description) -> - val typeString = when (key) { - is ModbusRegistryKey.Coil -> "Coil" - is ModbusRegistryKey.DiscreteInput -> "Discrete" - is ModbusRegistryKey.HoldingRegister -> "Register" - is ModbusRegistryKey.InputRegister -> "Input" - } - val rangeString = if (key.count == 1) { - key.address.toString() - } else { - "${key.address} - ${key.address + key.count - 1}" - } - to.appendLine("${typeString}\t$rangeString\t$description") - } +private val ModbusRegistryKey.sectionNumber + get() = when (this) { + is ModbusRegistryKey.Coil -> 1 + is ModbusRegistryKey.DiscreteInput -> 2 + is ModbusRegistryKey.HoldingRegister -> 4 + is ModbusRegistryKey.InputRegister -> 3 + } + +public fun ModbusRegistryMap.print(to: Appendable = System.out) { + ModbusRegistryMap.validate(this) + entries.entries + .sortedWith( + Comparator.comparingInt?> { it.key.sectionNumber } + .thenComparingInt { it.key.address } + ) + .forEach { (key, description) -> + val typeString = when (key) { + is ModbusRegistryKey.Coil -> "Coil" + is ModbusRegistryKey.DiscreteInput -> "Discrete" + is ModbusRegistryKey.HoldingRegister -> "Register" + is ModbusRegistryKey.InputRegister -> "Input" + } + val rangeString = if (key.count == 1) { + key.address.toString() + } else { + "${key.address} - ${key.address + key.count - 1}" + } + to.appendLine("${typeString}\t$rangeString\t$description") } +} + +public fun ModbusRegistryMap.toJson(): JsonArray = buildJsonArray { + ModbusRegistryMap.validate(this@toJson) + entries.forEach { (key, description) -> + + val entry = buildJsonObject { + put( + "type", + when (key) { + is ModbusRegistryKey.Coil -> "Coil" + is ModbusRegistryKey.DiscreteInput -> "Discrete" + is ModbusRegistryKey.HoldingRegister -> "Register" + is ModbusRegistryKey.InputRegister -> "Input" + } + ) + put("address", key.address) + if (key.count > 1) { + put("count", key.count) + } + put("description", description) + } + + add(entry) } } diff --git a/gradle.properties b/gradle.properties index 5b956f1..c7e0d88 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,4 +10,4 @@ publishing.sonatype=false org.gradle.configureondemand=true org.gradle.jvmargs=-Xmx4096m -toolsVersion=0.14.10-kotlin-1.9.0 \ No newline at end of file +toolsVersion=0.15.0-kotlin-1.9.20-RC \ No newline at end of file From 1619fdadf23e033b049d7ad3b724f8acffa012bb Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 25 Oct 2023 22:31:36 +0300 Subject: [PATCH 17/71] Refactoring. Developing composer --- build.gradle.kts | 2 +- controls-constructor/build.gradle.kts | 20 +++ .../kscience/controls/constructor/Drive.kt | 51 ++++++ .../controls/constructor/LimitSwitch.kt | 31 ++++ .../controls/constructor/PidRegulator.kt | 146 ++++++++++++++++++ .../space/kscience/controls/api/Device.kt | 18 ++- .../kscience/controls/api/DeviceMessage.kt | 8 +- .../kscience/controls/api/descriptors.kt | 4 +- .../controls/manager/DeviceManager.kt | 2 +- .../kscience/controls/spec/DeviceBase.kt | 58 +++++-- .../kscience/controls/spec/DeviceBySpec.kt | 7 +- .../controls/spec/DevicePropertySpec.kt | 4 +- .../kscience/controls/spec/DeviceSpec.kt | 8 +- .../kscience/controls/spec/DeviceTree.kt | 36 +++++ .../controls/modbus/DeviceProcessImage.kt | 2 +- .../controls/modbus/ModbusDeviceBySpec.kt | 6 +- .../opcua/client/OpcUaDeviceBySpec.kt | 3 +- .../controls/opcua/server/DeviceNameSpace.kt | 4 +- .../controls/opcua/client/OpcUaClientTest.kt | 7 +- .../controls/demo/DemoControllerView.kt | 2 +- .../kscience/controls/demo/DemoDevice.kt | 2 +- .../controls/demo/car/MagixVirtualCar.kt | 9 +- .../kscience/controls/demo/car/VirtualCar.kt | 3 +- .../controls/demo/car/VirtualCarController.kt | 4 +- .../pimotionmaster/PiMotionMasterDevice.kt | 24 +-- gradle.properties | 2 +- settings.gradle.kts | 1 + 27 files changed, 388 insertions(+), 76 deletions(-) create mode 100644 controls-constructor/build.gradle.kts create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceTree.kt diff --git a/build.gradle.kts b/build.gradle.kts index 347a66e..4ca88d6 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.2.2-dev-3" + version = "0.3.0-dev-1" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-constructor/build.gradle.kts b/controls-constructor/build.gradle.kts new file mode 100644 index 0000000..5815444 --- /dev/null +++ b/controls-constructor/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("space.kscience.gradle.mpp") + `maven-publish` +} + +description = """ + A low-code constructor foe composite devices simulation +""".trimIndent() + +kscience{ + jvm() + js() + dependencies { + api(projects.controlsCore) + } +} + +readme{ + maturity = space.kscience.gradle.Maturity.PROTOTYPE +} diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt new file mode 100644 index 0000000..ede628a --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt @@ -0,0 +1,51 @@ +package center.sciprog.controls.devices.misc + +import kotlinx.coroutines.Job +import space.kscience.controls.api.Device +import space.kscience.controls.spec.DeviceBySpec +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.DeviceSpec +import space.kscience.controls.spec.doubleProperty +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.transformations.MetaConverter + + +/** + * A single axis drive + */ +public interface Drive : Device { + /** + * Get or set target value + */ + public var target: Double + + /** + * Current position value + */ + public val position: Double + + public companion object : DeviceSpec() { + public val target: DevicePropertySpec by property(MetaConverter.double, Drive::target) + + public val position: DevicePropertySpec by doubleProperty { position } + } +} + +/** + * Virtual [Drive] with speed limit + */ +public class VirtualDrive( + context: Context, + value: Double, + private val speed: Double, +) : DeviceBySpec(Drive, context), Drive { + + private var moveJob: Job? = null + + override var position: Double = value + private set + + override var target: Double = value + + +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt new file mode 100644 index 0000000..6e82064 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt @@ -0,0 +1,31 @@ +package center.sciprog.controls.devices.misc + +import space.kscience.controls.api.Device +import space.kscience.controls.spec.DeviceBySpec +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.DeviceSpec +import space.kscience.controls.spec.booleanProperty +import space.kscience.dataforge.context.Context + + +/** + * A limit switch device + */ +public interface LimitSwitch : Device { + + public val locked: Boolean + + public companion object : DeviceSpec() { + public val locked: DevicePropertySpec by booleanProperty { locked } + } +} + +/** + * Virtual [LimitSwitch] + */ +public class VirtualLimitSwitch( + context: Context, + private val lockedFunction: () -> Boolean, +) : DeviceBySpec(LimitSwitch, context), LimitSwitch { + override val locked: Boolean get() = lockedFunction() +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt new file mode 100644 index 0000000..8dbe6ba --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt @@ -0,0 +1,146 @@ +package center.sciprog.controls.devices.misc + +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import space.kscience.controls.api.Device +import space.kscience.controls.spec.DeviceBySpec +import space.kscience.controls.spec.DeviceSpec +import space.kscience.controls.spec.doubleProperty +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.double +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.transformations.MetaConverter +import kotlin.math.pow +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit + + +interface PidRegulator : Device { + /** + * Proportional coefficient + */ + val kp: Double + + /** + * Integral coefficient + */ + val ki: Double + + /** + * Differential coefficient + */ + val kd: Double + + /** + * The target value for PID + */ + var target: Double + + /** + * Read current value + */ + suspend fun read(): Double + + companion object : DeviceSpec() { + val target by property(MetaConverter.double, PidRegulator::target) + val value by doubleProperty { read() } + } +} + +/** + * + */ +class VirtualPid( + context: Context, + override val kp: Double, + override val ki: Double, + override val kd: Double, + val mass: Double, + override var target: Double = 0.0, + private val dt: Duration = 0.5.milliseconds, + private val clock: Clock = Clock.System, +) : DeviceBySpec(PidRegulator, context), PidRegulator { + + private val mutex = Mutex() + + + private var lastTime: Instant = clock.now() + private var lastValue: Double = target + + private var value: Double = target + private var velocity: Double = 0.0 + private var acceleration: Double = 0.0 + private var integral: Double = 0.0 + + + private var updateJob: Job? = null + + override suspend fun onStart() { + updateJob = launch { + while (isActive) { + delay(dt) + mutex.withLock { + val realTime = clock.now() + val delta = target - value + val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) + integral += delta * dtSeconds + val derivative = (value - lastValue) / dtSeconds + + //set last time and value to new values + lastTime = realTime + lastValue = value + + // compute new value based on velocity and acceleration from the previous step + value += velocity * dtSeconds + acceleration * dtSeconds.pow(2) / 2 + + // compute new velocity based on acceleration on the previous step + velocity += acceleration * dtSeconds + + //compute force for the next step based on current values + acceleration = (kp * delta + ki * integral + kd * derivative) / mass + + + check(value.isFinite() && velocity.isFinite()) { + "Value $value is not finite" + } + } + } + } + } + + override fun onStop() { + updateJob?.cancel() + super.stop() + } + + override suspend fun read(): Double = value + + suspend fun readVelocity(): Double = velocity + + suspend fun readAcceleration(): Double = acceleration + + suspend fun write(newTarget: Double) = mutex.withLock { + require(newTarget.isFinite()) { "Value $newTarget is not valid" } + target = newTarget + } + + companion object : Factory { + override fun build(context: Context, meta: Meta) = VirtualPid( + context, + meta["kp"].double ?: error("Kp is not defined"), + meta["ki"].double ?: error("Ki is not defined"), + meta["kd"].double ?: error("Kd is not defined"), + meta["m"].double ?: error("Mass is not defined"), + ) + + } +} \ 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 8a19e68..dad1d90 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 @@ -20,8 +20,19 @@ import space.kscience.dataforge.names.Name * A lifecycle state of a device */ public enum class DeviceLifecycleState{ + /** + * Device is initializing + */ INIT, + + /** + * The Device is initialized and running + */ OPEN, + + /** + * The Device is closed + */ CLOSED } @@ -31,13 +42,14 @@ public enum class DeviceLifecycleState{ * When canceled, cancels all running processes. */ @Type(DEVICE_TARGET) -public interface Device : AutoCloseable, ContextAware, CoroutineScope { +public interface Device : ContextAware, CoroutineScope { /** * Initial configuration meta for the device */ public val meta: Meta get() = Meta.EMPTY + /** * List of supported property descriptors */ @@ -87,12 +99,12 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope { /** * Initialize the device. This function suspends until the device is finished initialization */ - public suspend fun open(): Unit = Unit + public suspend fun start(): Unit = Unit /** * Close and terminate the device. This function does not wait for the device to be closed. */ - override fun close() { + public fun stop() { logger.info { "Device $this is closed" } cancel("The device is closed") } 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 45d7f0b..c59e4c3 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 @@ -25,7 +25,7 @@ public sealed class DeviceMessage { public abstract val time: Instant? /** - * Update the source device name for composition. If the original name is null, resulting name is also null. + * Update the source device name for composition. If the original name is null, the resulting name is also null. */ public abstract fun changeSource(block: (Name) -> Name): DeviceMessage @@ -203,12 +203,12 @@ public data class EmptyDeviceMessage( public data class DeviceLogMessage( val message: String, val data: Meta? = null, - override val sourceDevice: Name? = null, + override val sourceDevice: Name = Name.EMPTY, override val targetDevice: Name? = null, override val comment: String? = null, @EncodeDefault override val time: Instant? = Clock.System.now(), ) : DeviceMessage() { - override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block)) + override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) } /** @@ -220,7 +220,7 @@ public data class DeviceErrorMessage( public val errorMessage: String?, public val errorType: String? = null, public val errorStackTrace: String? = null, - override val sourceDevice: Name, + override val sourceDevice: Name = Name.EMPTY, override val targetDevice: Name? = null, override val comment: String? = null, @EncodeDefault override val time: Instant? = Clock.System.now(), diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt index 8e1705b..6f7d27c 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt @@ -12,10 +12,10 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder @Serializable public class PropertyDescriptor( public val name: String, - public var info: String? = null, + public var description: String? = null, public var metaDescriptor: MetaDescriptor = MetaDescriptor(), public var readable: Boolean = true, - public var writable: Boolean = false + public var mutable: Boolean = false ) public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){ 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 cc043c7..be59b4c 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 @@ -40,7 +40,7 @@ public class DeviceManager : AbstractPlugin(), DeviceHub { public fun DeviceManager.install(name: String, device: D): D { registerDevice(NameToken(name), device) device.launch { - device.open() + device.start() } return device } 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 38aa20a..b1544e0 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 @@ -1,12 +1,9 @@ package space.kscience.controls.spec -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.newCoroutineContext import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import space.kscience.controls.api.* @@ -69,8 +66,28 @@ public abstract class DeviceBase( override val actionDescriptors: Collection get() = actions.values.map { it.descriptor } + + private val sharedMessageFlow: MutableSharedFlow = MutableSharedFlow( + replay = meta["message.buffer"].int ?: 1000, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + override val coroutineContext: CoroutineContext by lazy { - context.newCoroutineContext(SupervisorJob(context.coroutineContext[Job]) + CoroutineName("Device $this")) + context.newCoroutineContext( + SupervisorJob(context.coroutineContext[Job]) + + CoroutineName("Device $this") + + CoroutineExceptionHandler { _, throwable -> + launch { + sharedMessageFlow.emit( + DeviceErrorMessage( + errorMessage = throwable.message, + errorType = throwable::class.simpleName, + errorStackTrace = throwable.stackTraceToString() + ) + ) + } + } + ) } @@ -79,11 +96,6 @@ public abstract class DeviceBase( */ private val logicalState: HashMap = HashMap() - private val sharedMessageFlow: MutableSharedFlow = MutableSharedFlow( - replay = meta["message.buffer"].int ?: 1000, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - public override val messageFlow: SharedFlow get() = sharedMessageFlow @Suppress("UNCHECKED_CAST") @@ -180,18 +192,30 @@ public abstract class DeviceBase( override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.INIT protected set - @OptIn(DFExperimental::class) - override suspend fun open() { - super.open() - lifecycleState = DeviceLifecycleState.OPEN + protected open suspend fun onStart() { + } @OptIn(DFExperimental::class) - override fun close() { - lifecycleState = DeviceLifecycleState.CLOSED - super.close() + final override suspend fun start() { + super.start() + lifecycleState = DeviceLifecycleState.INIT + onStart() + lifecycleState = DeviceLifecycleState.OPEN } + protected open fun onStop() { + + } + + @OptIn(DFExperimental::class) + final override fun stop() { + onStop() + lifecycleState = DeviceLifecycleState.CLOSED + super.stop() + } + + abstract override fun toString(): String } 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 index 9309224..31f4c09 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt @@ -16,15 +16,14 @@ public open class DeviceBySpec( override val properties: Map> get() = spec.properties override val actions: Map> get() = spec.actions - override suspend fun open(): Unit = with(spec) { - super.open() + override suspend fun onStart(): Unit = with(spec) { self.onOpen() } - override fun close(): Unit = with(spec) { + override fun onStop(): Unit = with(spec){ self.onClose() - super.close() } + override fun toString(): String = "Device(spec=$spec)" } \ 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 7f3aa06..c42a23e 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 @@ -20,7 +20,7 @@ public annotation class InternalDeviceAPI /** * Specification for a device read-only property */ -public interface DevicePropertySpec { +public interface DevicePropertySpec { /** * Property descriptor */ @@ -53,7 +53,7 @@ public interface WritableDevicePropertySpec : DevicePropertySp } -public interface DeviceActionSpec { +public interface DeviceActionSpec { /** * Action descriptor */ 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 0376635..14966ca 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 @@ -53,7 +53,7 @@ public abstract class DeviceSpec { val deviceProperty = object : DevicePropertySpec { override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { //TODO add type from converter - writable = true + mutable = true }.apply(descriptorBuilder) override val converter: MetaConverter = converter @@ -78,7 +78,7 @@ public abstract class DeviceSpec { override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { //TODO add the type from converter - writable = true + mutable = true }.apply(descriptorBuilder) override val converter: MetaConverter = converter @@ -127,7 +127,7 @@ public abstract class DeviceSpec { PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val propertyName = name ?: property.name val deviceProperty = object : WritableDevicePropertySpec { - override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, writable = true) + override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, mutable = true) .apply(descriptorBuilder) override val converter: MetaConverter = converter @@ -224,7 +224,7 @@ public fun > DeviceSpec.logicalProperty( val propertyName = name ?: property.name override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { //TODO add type from converter - writable = true + mutable = true }.apply(descriptorBuilder) override val converter: MetaConverter = converter diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceTree.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceTree.kt new file mode 100644 index 0000000..f100af6 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceTree.kt @@ -0,0 +1,36 @@ +package space.kscience.controls.spec + +import space.kscience.controls.api.Device +import space.kscience.controls.api.DeviceHub +import space.kscience.controls.manager.DeviceManager +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.names.NameToken +import kotlin.collections.Map +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.mapValues +import kotlin.collections.mutableMapOf +import kotlin.collections.set + + +public class DeviceTree( + public val deviceManager: DeviceManager, + public val meta: Meta, + builder: Builder, +) : DeviceHub { + public class Builder(public val manager: DeviceManager) { + internal val childrenFactories = mutableMapOf>() + + public fun device(name: String, factory: Factory) { + childrenFactories[NameToken.parse(name)] = factory + } + } + + override val devices: Map = builder.childrenFactories.mapValues { (token, factory) -> + val devicesMeta = meta["devices"] + factory.build(deviceManager.context, devicesMeta?.get(token) ?: Meta.EMPTY) + } + +} \ No newline at end of file diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt index 32adfb3..b4fae90 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt @@ -237,7 +237,7 @@ public fun D.bindProcessImage( image.setLocked(true) if (openOnBind) { launch { - open() + start() } } return image 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 48f48a4..995e0df 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 @@ -20,16 +20,14 @@ public open class ModbusDeviceBySpec( private val disposeMasterOnClose: Boolean = true, meta: Meta = Meta.EMPTY, ) : ModbusDevice, DeviceBySpec(spec, context, meta){ - override suspend fun open() { + override suspend fun onStart() { master.connect() - super.open() } - override fun close() { + override fun onStop() { if(disposeMasterOnClose){ master.disconnect() } - super.close() } } diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt index eb9b688..188760e 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt @@ -63,8 +63,7 @@ public open class OpcUaDeviceBySpec( } } - override fun close() { + override fun onStop() { client.disconnect() - super.close() } } diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt index 010c2c0..a05a81d 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt @@ -73,11 +73,11 @@ public class DeviceNameSpace( //for now, use DF paths as ids nodeId = newNodeId("${deviceName.tokens.joinToString("/")}/$propertyName") when { - descriptor.readable && descriptor.writable -> { + descriptor.readable && descriptor.mutable -> { setAccessLevel(AccessLevel.READ_WRITE) setUserAccessLevel(AccessLevel.READ_WRITE) } - descriptor.writable -> { + descriptor.mutable -> { setAccessLevel(AccessLevel.WRITE_ONLY) setUserAccessLevel(AccessLevel.WRITE_ONLY) } 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 eedafe7..8537449 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 @@ -40,9 +40,10 @@ class OpcUaClientTest { @Test @Ignore fun testReadDouble() = runTest { - DemoOpcUaDevice.build().use{ - println(it.read(DemoOpcUaDevice.randomDouble)) - } + val device = DemoOpcUaDevice.build() + device.start() + println(device.read(DemoOpcUaDevice.randomDouble)) + device.stop() } } \ 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 94815fa..8edd90e 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 @@ -78,7 +78,7 @@ class DemoController : Controller(), ContextAware { logger.info { "Visualization server stopped" } magixServer?.stop(1000, 5000) logger.info { "Magix server stopped" } - device?.close() + device?.stop() logger.info { "Device server stopped" } context.close() } 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 dd65b78..4f902c0 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 @@ -44,7 +44,7 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(Compa metaDescriptor { type(ValueType.NUMBER) } - info = "Real to virtual time scale" + description = "Real to virtual time scale" } val sinScale by mutableProperty(MetaConverter.double, IDemoDevice::sinScaleState) 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 03c781b..2c5d65a 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 @@ -14,7 +14,6 @@ import space.kscience.dataforge.names.Name import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.subscribe import space.kscience.magix.rsocket.rSocketWithWebSockets -import kotlin.time.ExperimentalTime class MagixVirtualCar(context: Context, meta: Meta) : VirtualCar(context, meta) { @@ -31,17 +30,13 @@ class MagixVirtualCar(context: Context, meta: Meta) : VirtualCar(context, meta) } - @OptIn(ExperimentalTime::class) - override suspend fun open() { - super.open() + override suspend fun onStart() { val magixEndpoint = MagixEndpoint.rSocketWithWebSockets( meta["magixServerHost"].string ?: "localhost", ) - launch { - magixEndpoint.launchMagixVirtualCarUpdate() - } + magixEndpoint.launchMagixVirtualCarUpdate() } companion object : Factory { 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 1f1dc69..2ba0fdb 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 @@ -100,8 +100,7 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec(I } @OptIn(ExperimentalTime::class) - override suspend fun open() { - super.open() + override suspend fun onStart() { //initializing the clock timeState = Clock.System.now() //starting regular updates 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 7a170ac..3a63003 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 @@ -71,9 +71,9 @@ class VirtualCarController : Controller(), ContextAware { logger.info { "Shutting down..." } magixServer?.stop(1000, 5000) logger.info { "Magix server stopped" } - magixVirtualCar?.close() + magixVirtualCar?.stop() logger.info { "Magix virtual car server stopped" } - virtualCar?.close() + virtualCar?.stop() logger.info { "Virtual car server stopped" } context.close() } diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt index 54573e4..18e5838 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -138,7 +138,7 @@ class PiMotionMasterDevice( override fun build(context: Context, meta: Meta): PiMotionMasterDevice = PiMotionMasterDevice(context) val connected by booleanProperty(descriptorBuilder = { - info = "True if the connection address is defined and the device is initialized" + description = "True if the connection address is defined and the device is initialized" }) { port != null } @@ -201,7 +201,7 @@ class PiMotionMasterDevice( val timeout by mutableProperty(MetaConverter.duration, PiMotionMasterDevice::timeoutValue) { - info = "Timeout" + description = "Timeout" } } @@ -267,7 +267,7 @@ class PiMotionMasterDevice( ) val enabled by axisBooleanProperty("EAX") { - info = "Motor enable state." + description = "Motor enable state." } val halt by unitAction { @@ -275,20 +275,20 @@ class PiMotionMasterDevice( } val targetPosition by axisNumberProperty("MOV") { - info = """ + description = """ Sets a new absolute target position for the specified axis. Servo mode must be switched on for the commanded axis prior to using this command (closed-loop operation). """.trimIndent() } val onTarget by booleanProperty({ - info = "Queries the on-target state of the specified axis." + description = "Queries the on-target state of the specified axis." }) { readAxisBoolean("ONT?") } val reference by booleanProperty({ - info = "Get Referencing Result" + description = "Get Referencing Result" }) { readAxisBoolean("FRF?") } @@ -298,36 +298,36 @@ class PiMotionMasterDevice( } val minPosition by doubleProperty({ - info = "Minimal position value for the axis" + description = "Minimal position value for the axis" }) { mm.requestAndParse("TMN?", axisId)[axisId]?.toDoubleOrNull() ?: error("Malformed `TMN?` response. Should include float value for $axisId") } val maxPosition by doubleProperty({ - info = "Maximal position value for the axis" + description = "Maximal position value for the axis" }) { mm.requestAndParse("TMX?", axisId)[axisId]?.toDoubleOrNull() ?: error("Malformed `TMX?` response. Should include float value for $axisId") } val position by doubleProperty({ - info = "The current axis position." + description = "The current axis position." }) { mm.requestAndParse("POS?", axisId)[axisId]?.toDoubleOrNull() ?: error("Malformed `POS?` response. Should include float value for $axisId") } val openLoopTarget by axisNumberProperty("OMA") { - info = "Position for open-loop operation." + description = "Position for open-loop operation." } val closedLoop by axisBooleanProperty("SVO") { - info = "Servo closed loop mode" + description = "Servo closed loop mode" } val velocity by axisNumberProperty("VEL") { - info = "Velocity value for closed-loop operation" + description = "Velocity value for closed-loop operation" } val move by action(MetaConverter.meta, MetaConverter.unit) { diff --git a/gradle.properties b/gradle.properties index c7e0d88..62d03f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,4 +10,4 @@ publishing.sonatype=false org.gradle.configureondemand=true org.gradle.jvmargs=-Xmx4096m -toolsVersion=0.15.0-kotlin-1.9.20-RC \ No newline at end of file +toolsVersion=0.15.0-kotlin-1.9.20-RC2 \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 20ac44e..0f53909 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -50,6 +50,7 @@ include( // ":controls-mongo", ":controls-storage", ":controls-storage:controls-xodus", + ":controls-constructor", ":magix", ":magix:magix-api", ":magix:magix-server", From 4f028ccee803207ce1db009062ea11a9a6e4bb3e Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 27 Oct 2023 10:57:46 +0300 Subject: [PATCH 18/71] Lifecycle change --- .../controls/constructor/PidRegulator.kt | 237 +++++++++++------- .../constructor/{Drive.kt => Regulator.kt} | 14 +- .../space/kscience/controls/api/Device.kt | 16 +- .../kscience/controls/spec/DeviceBase.kt | 16 +- .../kscience/controls/client/DeviceClient.kt | 2 +- 5 files changed, 171 insertions(+), 114 deletions(-) rename controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/{Drive.kt => Regulator.kt} (65%) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt index 8dbe6ba..cd38c93 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt @@ -8,110 +8,56 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Clock import kotlinx.datetime.Instant -import space.kscience.controls.api.Device +import space.kscience.controls.api.DeviceLifecycleState import space.kscience.controls.spec.DeviceBySpec -import space.kscience.controls.spec.DeviceSpec -import space.kscience.controls.spec.doubleProperty -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.Factory -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.double -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.transformations.MetaConverter -import kotlin.math.pow import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit - -interface PidRegulator : Device { - /** - * Proportional coefficient - */ - val kp: Double - - /** - * Integral coefficient - */ - val ki: Double - - /** - * Differential coefficient - */ - val kd: Double - - /** - * The target value for PID - */ - var target: Double - - /** - * Read current value - */ - suspend fun read(): Double - - companion object : DeviceSpec() { - val target by property(MetaConverter.double, PidRegulator::target) - val value by doubleProperty { read() } - } -} - /** - * + * PID controller on top of a [Regulator] */ -class VirtualPid( - context: Context, - override val kp: Double, - override val ki: Double, - override val kd: Double, - val mass: Double, - override var target: Double = 0.0, +public class PidRegulator( + public val regulator: Regulator, + public val kp: Double, + public val ki: Double, + public val kd: Double, private val dt: Duration = 0.5.milliseconds, private val clock: Clock = Clock.System, -) : DeviceBySpec(PidRegulator, context), PidRegulator { - - private val mutex = Mutex() +) : DeviceBySpec(Regulator, regulator.context), Regulator { + override var target: Double = regulator.target private var lastTime: Instant = clock.now() - private var lastValue: Double = target + private var lastRegulatorTarget: Double = target - private var value: Double = target - private var velocity: Double = 0.0 - private var acceleration: Double = 0.0 private var integral: Double = 0.0 private var updateJob: Job? = null + private val mutex = Mutex() + override suspend fun onStart() { + if(regulator.lifecycleState == DeviceLifecycleState.STOPPED){ + regulator.start() + } + regulator.start() updateJob = launch { while (isActive) { delay(dt) mutex.withLock { val realTime = clock.now() - val delta = target - value + val delta = target - position val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) integral += delta * dtSeconds - val derivative = (value - lastValue) / dtSeconds + val derivative = (regulator.target - lastRegulatorTarget) / dtSeconds //set last time and value to new values lastTime = realTime - lastValue = value + lastRegulatorTarget = regulator.target - // compute new value based on velocity and acceleration from the previous step - value += velocity * dtSeconds + acceleration * dtSeconds.pow(2) / 2 - - // compute new velocity based on acceleration on the previous step - velocity += acceleration * dtSeconds - - //compute force for the next step based on current values - acceleration = (kp * delta + ki * integral + kd * derivative) / mass - - - check(value.isFinite() && velocity.isFinite()) { - "Value $value is not finite" - } + regulator.target = regulator.position + kp * delta + ki * integral + kd * derivative } } } @@ -119,28 +65,131 @@ class VirtualPid( override fun onStop() { updateJob?.cancel() - super.stop() } - override suspend fun read(): Double = value + override val position: Double get() = regulator.position - suspend fun readVelocity(): Double = velocity +} - suspend fun readAcceleration(): Double = acceleration - suspend fun write(newTarget: Double) = mutex.withLock { - require(newTarget.isFinite()) { "Value $newTarget is not valid" } - target = newTarget - } - - companion object : Factory { - override fun build(context: Context, meta: Meta) = VirtualPid( - context, - meta["kp"].double ?: error("Kp is not defined"), - meta["ki"].double ?: error("Ki is not defined"), - meta["kd"].double ?: error("Kd is not defined"), - meta["m"].double ?: error("Mass is not defined"), - ) - - } -} \ No newline at end of file +// +//interface PidRegulator : Device { +// /** +// * Proportional coefficient +// */ +// val kp: Double +// +// /** +// * Integral coefficient +// */ +// val ki: Double +// +// /** +// * Differential coefficient +// */ +// val kd: Double +// +// /** +// * The target value for PID +// */ +// var target: Double +// +// /** +// * Read current value +// */ +// suspend fun read(): Double +// +// companion object : DeviceSpec() { +// val target by property(MetaConverter.double, PidRegulator::target) +// val value by doubleProperty { read() } +// } +//} +// +///** +// * +// */ +//class VirtualPid( +// context: Context, +// override val kp: Double, +// override val ki: Double, +// override val kd: Double, +// val mass: Double, +// override var target: Double = 0.0, +// private val dt: Duration = 0.5.milliseconds, +// private val clock: Clock = Clock.System, +//) : DeviceBySpec(PidRegulator, context), PidRegulator { +// +// private val mutex = Mutex() +// +// +// private var lastTime: Instant = clock.now() +// private var lastValue: Double = target +// +// private var value: Double = target +// private var velocity: Double = 0.0 +// private var acceleration: Double = 0.0 +// private var integral: Double = 0.0 +// +// +// private var updateJob: Job? = null +// +// override suspend fun onStart() { +// updateJob = launch { +// while (isActive) { +// delay(dt) +// mutex.withLock { +// val realTime = clock.now() +// val delta = target - value +// val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) +// integral += delta * dtSeconds +// val derivative = (value - lastValue) / dtSeconds +// +// //set last time and value to new values +// lastTime = realTime +// lastValue = value +// +// // compute new value based on velocity and acceleration from the previous step +// value += velocity * dtSeconds + acceleration * dtSeconds.pow(2) / 2 +// +// // compute new velocity based on acceleration on the previous step +// velocity += acceleration * dtSeconds +// +// //compute force for the next step based on current values +// acceleration = (kp * delta + ki * integral + kd * derivative) / mass +// +// +// check(value.isFinite() && velocity.isFinite()) { +// "Value $value is not finite" +// } +// } +// } +// } +// } +// +// override fun onStop() { +// updateJob?.cancel() +// super.stop() +// } +// +// override suspend fun read(): Double = value +// +// suspend fun readVelocity(): Double = velocity +// +// suspend fun readAcceleration(): Double = acceleration +// +// suspend fun write(newTarget: Double) = mutex.withLock { +// require(newTarget.isFinite()) { "Value $newTarget is not valid" } +// target = newTarget +// } +// +// companion object : Factory { +// override fun build(context: Context, meta: Meta) = VirtualPid( +// context, +// meta["kp"].double ?: error("Kp is not defined"), +// meta["ki"].double ?: error("Ki is not defined"), +// meta["kd"].double ?: error("Kd is not defined"), +// meta["m"].double ?: error("Mass is not defined"), +// ) +// +// } +//} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt similarity index 65% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt index ede628a..2db263d 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt @@ -13,7 +13,7 @@ import space.kscience.dataforge.meta.transformations.MetaConverter /** * A single axis drive */ -public interface Drive : Device { +public interface Regulator : Device { /** * Get or set target value */ @@ -24,21 +24,21 @@ public interface Drive : Device { */ public val position: Double - public companion object : DeviceSpec() { - public val target: DevicePropertySpec by property(MetaConverter.double, Drive::target) + public companion object : DeviceSpec() { + public val target: DevicePropertySpec by property(MetaConverter.double, Regulator::target) - public val position: DevicePropertySpec by doubleProperty { position } + public val position: DevicePropertySpec by doubleProperty { position } } } /** - * Virtual [Drive] with speed limit + * Virtual [Regulator] with speed limit */ -public class VirtualDrive( +public class VirtualRegulator( context: Context, value: Double, private val speed: Double, -) : DeviceBySpec(Drive, context), Drive { +) : DeviceBySpec(Regulator, context), Regulator { private var moveJob: Job? = null 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 dad1d90..a0268c2 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 @@ -19,21 +19,22 @@ import space.kscience.dataforge.names.Name /** * A lifecycle state of a device */ -public enum class DeviceLifecycleState{ +public enum class DeviceLifecycleState { + /** * Device is initializing - */ - INIT, + */ + STARTING, /** * The Device is initialized and running */ - OPEN, + STARTED, /** * The Device is closed */ - CLOSED + STOPPED } /** @@ -136,5 +137,8 @@ public fun Device.getAllProperties(): Meta = Meta { /** * Subscribe on property changes for the whole device */ -public fun Device.onPropertyChange(scope: CoroutineScope = this, callback: suspend PropertyChangedMessage.() -> Unit): Job = +public fun Device.onPropertyChange( + scope: CoroutineScope = this, + callback: suspend PropertyChangedMessage.() -> Unit, +): Job = messageFlow.filterIsInstance().onEach(callback).launchIn(scope) 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 b1544e0..3b6bce0 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 @@ -189,7 +189,7 @@ public abstract class DeviceBase( } @DFExperimental - override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.INIT + override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED protected set protected open suspend fun onStart() { @@ -198,10 +198,14 @@ public abstract class DeviceBase( @OptIn(DFExperimental::class) final override suspend fun start() { - super.start() - lifecycleState = DeviceLifecycleState.INIT - onStart() - lifecycleState = DeviceLifecycleState.OPEN + if(lifecycleState == DeviceLifecycleState.STOPPED) { + super.start() + lifecycleState = DeviceLifecycleState.STARTING + onStart() + lifecycleState = DeviceLifecycleState.STARTED + } else { + logger.debug { "Device $this is already started" } + } } protected open fun onStop() { @@ -211,7 +215,7 @@ public abstract class DeviceBase( @OptIn(DFExperimental::class) final override fun stop() { onStop() - lifecycleState = DeviceLifecycleState.CLOSED + lifecycleState = DeviceLifecycleState.STOPPED super.stop() } diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt index 64e8a9e..2fcecff 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt @@ -99,7 +99,7 @@ public class DeviceClient( } @DFExperimental - override val lifecycleState: DeviceLifecycleState = DeviceLifecycleState.OPEN + override val lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STARTED } /** From 1fcdbdc9f4f174c8075b149f6b85c0e094c635de Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 28 Oct 2023 14:18:00 +0300 Subject: [PATCH 19/71] Update constructor --- CHANGELOG.md | 2 + controls-constructor/build.gradle.kts | 2 +- .../controls/constructor/DeviceState.kt | 25 +++ .../controls/constructor}/DeviceTree.kt | 5 +- .../kscience/controls/constructor/Drive.kt | 91 +++++++++++ .../controls/constructor/LimitSwitch.kt | 2 +- .../controls/constructor/PidRegulator.kt | 153 ++---------------- .../controls/constructor/Regulator.kt | 26 +-- .../space/kscience/controls/api/Device.kt | 12 +- .../kscience/controls/api/DeviceMessage.kt | 17 +- .../controls/manager/respondMessage.kt | 3 +- .../kscience/controls/spec/DeviceBase.kt | 13 +- controls-vision/build.gradle.kts | 20 +++ settings.gradle.kts | 1 + 14 files changed, 196 insertions(+), 176 deletions(-) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt rename {controls-core/src/commonMain/kotlin/space/kscience/controls/spec => controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor}/DeviceTree.kt (88%) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt create mode 100644 controls-vision/build.gradle.kts diff --git a/CHANGELOG.md b/CHANGELOG.md index b7673a4..4f3b5a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased ### Added +- Device lifecycle message +- Low-code constructor ### Changed diff --git a/controls-constructor/build.gradle.kts b/controls-constructor/build.gradle.kts index 5815444..e62dd8e 100644 --- a/controls-constructor/build.gradle.kts +++ b/controls-constructor/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } description = """ - A low-code constructor foe composite devices simulation + A low-code constructor for composite devices simulation """.trimIndent() kscience{ diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt new file mode 100644 index 0000000..2e6461f --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -0,0 +1,25 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.Flow +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.transformations.MetaConverter + +/** + * An observable state of a device + */ +public interface DeviceState { + public val converter: MetaConverter + public val value: T + + public val valueFlow: Flow + + public val metaFlow: Flow +} + + +/** + * A mutable state of a device + */ +public interface MutableDeviceState : DeviceState{ + override var value: T +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceTree.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt similarity index 88% rename from controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceTree.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt index f100af6..8d971bb 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceTree.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.spec +package space.kscience.controls.constructor import space.kscience.controls.api.Device import space.kscience.controls.api.DeviceHub @@ -7,11 +7,8 @@ import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get import space.kscience.dataforge.names.NameToken -import kotlin.collections.Map import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.mapValues -import kotlin.collections.mutableMapOf import kotlin.collections.set diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt new file mode 100644 index 0000000..6ac5a07 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt @@ -0,0 +1,91 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import space.kscience.controls.api.Device +import space.kscience.controls.spec.DeviceBySpec +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.DeviceSpec +import space.kscience.controls.spec.doubleProperty +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.double +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.transformations.MetaConverter +import kotlin.math.pow +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit + +/** + * A classic drive regulated by force with encoder + */ +public interface Drive : Device { + /** + * Get or set drive force or momentum + */ + public var force: Double + + /** + * Current position value + */ + public val position: Double + + public companion object : DeviceSpec() { + public val force: DevicePropertySpec by Drive.property( + MetaConverter.double, + Drive::force + ) + + public val position: DevicePropertySpec by doubleProperty { position } + } +} + +/** + * A virtual drive + */ +public class VirtualDrive( + context: Context, + private val mass: Double, + position: Double, +) : Drive, DeviceBySpec(Drive, context) { + + private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds + private val clock = Clock.System + + override var force: Double = 0.0 + + override var position: Double = position + private set + + public var velocity: Double = 0.0 + private set + + private var updateJob: Job? = null + + override suspend fun onStart() { + updateJob = launch { + var lastTime = clock.now() + while (isActive) { + delay(dt) + val realTime = clock.now() + val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) + + //set last time and value to new values + lastTime = realTime + + // compute new value based on velocity and acceleration from the previous step + position += velocity * dtSeconds + force/mass * dtSeconds.pow(2) / 2 + + // compute new velocity based on acceleration on the previous step + velocity += force/mass * dtSeconds + } + } + } + + override fun onStop() { + updateJob?.cancel() + } +} + diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt index 6e82064..dd0dfed 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt @@ -1,4 +1,4 @@ -package center.sciprog.controls.devices.misc +package space.kscience.controls.constructor import space.kscience.controls.api.Device import space.kscience.controls.spec.DeviceBySpec diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt index cd38c93..4a4d0f3 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt @@ -1,4 +1,4 @@ -package center.sciprog.controls.devices.misc +package space.kscience.controls.constructor import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -8,28 +8,27 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Clock import kotlinx.datetime.Instant -import space.kscience.controls.api.DeviceLifecycleState import space.kscience.controls.spec.DeviceBySpec import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit /** - * PID controller on top of a [Regulator] + * A drive with PID regulator */ public class PidRegulator( - public val regulator: Regulator, + public val drive: Drive, public val kp: Double, public val ki: Double, public val kd: Double, - private val dt: Duration = 0.5.milliseconds, + private val dt: Duration = 1.milliseconds, private val clock: Clock = Clock.System, -) : DeviceBySpec(Regulator, regulator.context), Regulator { +) : DeviceBySpec(Regulator, drive.context), Regulator { - override var target: Double = regulator.target + override var target: Double = drive.position private var lastTime: Instant = clock.now() - private var lastRegulatorTarget: Double = target + private var lastPosition: Double = target private var integral: Double = 0.0 @@ -39,10 +38,7 @@ public class PidRegulator( override suspend fun onStart() { - if(regulator.lifecycleState == DeviceLifecycleState.STOPPED){ - regulator.start() - } - regulator.start() + drive.start() updateJob = launch { while (isActive) { delay(dt) @@ -51,13 +47,13 @@ public class PidRegulator( val delta = target - position val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) integral += delta * dtSeconds - val derivative = (regulator.target - lastRegulatorTarget) / dtSeconds + val derivative = (drive.position - lastPosition) / dtSeconds //set last time and value to new values lastTime = realTime - lastRegulatorTarget = regulator.target + lastPosition = drive.position - regulator.target = regulator.position + kp * delta + ki * integral + kd * derivative + drive.force = kp * delta + ki * integral + kd * derivative } } } @@ -67,129 +63,6 @@ public class PidRegulator( updateJob?.cancel() } - override val position: Double get() = regulator.position + override val position: Double get() = drive.position -} - - -// -//interface PidRegulator : Device { -// /** -// * Proportional coefficient -// */ -// val kp: Double -// -// /** -// * Integral coefficient -// */ -// val ki: Double -// -// /** -// * Differential coefficient -// */ -// val kd: Double -// -// /** -// * The target value for PID -// */ -// var target: Double -// -// /** -// * Read current value -// */ -// suspend fun read(): Double -// -// companion object : DeviceSpec() { -// val target by property(MetaConverter.double, PidRegulator::target) -// val value by doubleProperty { read() } -// } -//} -// -///** -// * -// */ -//class VirtualPid( -// context: Context, -// override val kp: Double, -// override val ki: Double, -// override val kd: Double, -// val mass: Double, -// override var target: Double = 0.0, -// private val dt: Duration = 0.5.milliseconds, -// private val clock: Clock = Clock.System, -//) : DeviceBySpec(PidRegulator, context), PidRegulator { -// -// private val mutex = Mutex() -// -// -// private var lastTime: Instant = clock.now() -// private var lastValue: Double = target -// -// private var value: Double = target -// private var velocity: Double = 0.0 -// private var acceleration: Double = 0.0 -// private var integral: Double = 0.0 -// -// -// private var updateJob: Job? = null -// -// override suspend fun onStart() { -// updateJob = launch { -// while (isActive) { -// delay(dt) -// mutex.withLock { -// val realTime = clock.now() -// val delta = target - value -// val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) -// integral += delta * dtSeconds -// val derivative = (value - lastValue) / dtSeconds -// -// //set last time and value to new values -// lastTime = realTime -// lastValue = value -// -// // compute new value based on velocity and acceleration from the previous step -// value += velocity * dtSeconds + acceleration * dtSeconds.pow(2) / 2 -// -// // compute new velocity based on acceleration on the previous step -// velocity += acceleration * dtSeconds -// -// //compute force for the next step based on current values -// acceleration = (kp * delta + ki * integral + kd * derivative) / mass -// -// -// check(value.isFinite() && velocity.isFinite()) { -// "Value $value is not finite" -// } -// } -// } -// } -// } -// -// override fun onStop() { -// updateJob?.cancel() -// super.stop() -// } -// -// override suspend fun read(): Double = value -// -// suspend fun readVelocity(): Double = velocity -// -// suspend fun readAcceleration(): Double = acceleration -// -// suspend fun write(newTarget: Double) = mutex.withLock { -// require(newTarget.isFinite()) { "Value $newTarget is not valid" } -// target = newTarget -// } -// -// companion object : Factory { -// override fun build(context: Context, meta: Meta) = VirtualPid( -// context, -// meta["kp"].double ?: error("Kp is not defined"), -// meta["ki"].double ?: error("Ki is not defined"), -// meta["kd"].double ?: error("Kd is not defined"), -// meta["m"].double ?: error("Mass is not defined"), -// ) -// -// } -//} \ No newline at end of file +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt index 2db263d..fa43b57 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt @@ -1,17 +1,14 @@ -package center.sciprog.controls.devices.misc +package space.kscience.controls.constructor -import kotlinx.coroutines.Job import space.kscience.controls.api.Device -import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.doubleProperty -import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.transformations.MetaConverter /** - * A single axis drive + * A regulator with target value and current position */ public interface Regulator : Device { /** @@ -29,23 +26,4 @@ public interface Regulator : Device { public val position: DevicePropertySpec by doubleProperty { position } } -} - -/** - * Virtual [Regulator] with speed limit - */ -public class VirtualRegulator( - context: Context, - value: Double, - private val speed: Double, -) : DeviceBySpec(Regulator, context), Regulator { - - private var moveJob: Job? = null - - override var position: Double = value - private set - - override var target: Double = value - - } \ 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 a0268c2..872a5e6 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 @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.serialization.Serializable import space.kscience.controls.api.Device.Companion.DEVICE_TARGET import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.info @@ -19,6 +20,7 @@ import space.kscience.dataforge.names.Name /** * A lifecycle state of a device */ +@Serializable public enum class DeviceLifecycleState { /** @@ -34,7 +36,12 @@ public enum class DeviceLifecycleState { /** * The Device is closed */ - STOPPED + STOPPED, + + /** + * The device encountered irrecoverable error + */ + ERROR } /** @@ -98,7 +105,8 @@ public interface Device : ContextAware, CoroutineScope { public suspend fun execute(actionName: String, argument: Meta? = null): Meta? /** - * Initialize the device. This function suspends until the device is finished initialization + * Initialize the device. This function suspends until the device is finished initialization. + * Does nothing if the device is started or is starting */ public suspend fun start(): Unit = Unit 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 c59e4c3..32df1bb 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 @@ -1,4 +1,4 @@ -@file:OptIn(ExperimentalSerializationApi::class) +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class) package space.kscience.controls.api @@ -228,6 +228,21 @@ public data class DeviceErrorMessage( override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) } +/** + * Device [Device.lifecycleState] is changed + */ +@Serializable +@SerialName("lifecycle") +public data class DeviceLifeCycleMessage( + val state: DeviceLifecycleState, + override val sourceDevice: Name = Name.EMPTY, + override val targetDevice: Name? = null, + override val comment: String? = null, + @EncodeDefault override val time: Instant? = Clock.System.now(), +) : DeviceMessage() { + override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) +} + public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta() 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 13d072c..d3abc03 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 @@ -64,6 +64,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess is DeviceErrorMessage, is EmptyDeviceMessage, is DeviceLogMessage, + is DeviceLifeCycleMessage, -> null } } catch (ex: Exception) { @@ -87,7 +88,7 @@ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMe * Collect all messages from given [DeviceHub], applying proper relative names. */ public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow { - + //TODO could we avoid using downstream scope? val outbox = MutableSharedFlow() if (this is Device) { 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 3b6bce0..da730f8 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 @@ -190,7 +190,16 @@ public abstract class DeviceBase( @DFExperimental override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED - protected set + protected set(value) { + if (field != value) { + launch { + sharedMessageFlow.emit( + DeviceLifeCycleMessage(value) + ) + } + } + field = value + } protected open suspend fun onStart() { @@ -198,7 +207,7 @@ public abstract class DeviceBase( @OptIn(DFExperimental::class) final override suspend fun start() { - if(lifecycleState == DeviceLifecycleState.STOPPED) { + if (lifecycleState == DeviceLifecycleState.STOPPED) { super.start() lifecycleState = DeviceLifecycleState.STARTING onStart() diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts new file mode 100644 index 0000000..0b3e9f1 --- /dev/null +++ b/controls-vision/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("space.kscience.gradle.mpp") + `maven-publish` +} + +description = """ + Dashboard and visualization extensions for devices +""".trimIndent() + +kscience{ + jvm() + js() + dependencies { + api(projects.controlsCore) + } +} + +readme{ + maturity = space.kscience.gradle.Maturity.PROTOTYPE +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0f53909..ee40d35 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,6 +51,7 @@ include( ":controls-storage", ":controls-storage:controls-xodus", ":controls-constructor", + ":controls-vision", ":magix", ":magix:magix-api", ":magix:magix-server", From 1414cf5a2f074a941999a2f5989bc0551a413f32 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 30 Oct 2023 21:35:46 +0300 Subject: [PATCH 20/71] implement constructor --- CHANGELOG.md | 1 + .../controls/constructor/DeviceGroup.kt | 258 ++++++++++++++++++ .../controls/constructor/DeviceState.kt | 108 +++++++- .../controls/constructor/DeviceTree.kt | 33 --- .../kscience/controls/constructor/Drive.kt | 27 +- .../controls/constructor/LimitSwitch.kt | 10 +- .../controls/constructor/PidRegulator.kt | 31 ++- .../controls/constructor/Regulator.kt | 3 +- .../controls/constructor/customState.kt | 41 +++ .../space/kscience/controls/api/Device.kt | 52 ++-- .../kscience/controls/api/DeviceMessage.kt | 2 +- .../kscience/controls/manager/ClockManager.kt | 25 ++ .../controls/manager/respondMessage.kt | 10 +- .../kscience/controls/spec/DeviceBase.kt | 10 +- .../controls/spec/DevicePropertySpec.kt | 18 +- .../kscience/controls/spec/DeviceSpec.kt | 14 +- .../controls/spec/propertySpecDelegates.kt | 10 +- .../kscience/controls/client/DeviceClient.kt | 2 +- .../kscience/controls/client/tangoMagix.kt | 6 +- .../controls/modbus/DeviceProcessImage.kt | 19 +- .../controls/opcua/server/DeviceNameSpace.kt | 15 +- controls-vision/build.gradle.kts | 13 +- .../controls/vision/plotExtensions.kt | 56 ++++ .../kscience/controls/demo/car/VirtualCar.kt | 8 +- demo/constructor/build.gradle.kts | 20 ++ .../src/jsMain/kotlin/constructorJs.kt | 6 + demo/constructor/src/jvmMain/kotlin/Main.kt | 103 +++++++ .../src/jvmMain/resources/logback.xml | 11 + .../sciprog/devices/mks/MksPdr900Device.kt | 2 +- .../pimotionmaster/fxDeviceProperties.kt | 4 +- settings.gradle.kts | 3 +- 31 files changed, 770 insertions(+), 151 deletions(-) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt create mode 100644 controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt create mode 100644 demo/constructor/build.gradle.kts create mode 100644 demo/constructor/src/jsMain/kotlin/constructorJs.kt create mode 100644 demo/constructor/src/jvmMain/kotlin/Main.kt create mode 100644 demo/constructor/src/jvmMain/resources/logback.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f3b5a9..4083a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Low-code constructor ### Changed +- Property caching moved from core `Device` to the `CachingDevice` ### Deprecated diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt new file mode 100644 index 0000000..f7bf494 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -0,0 +1,258 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import space.kscience.controls.api.* +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.meta.Laminate +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MutableMeta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.misc.DFExperimental +import space.kscience.dataforge.names.* +import kotlin.collections.set +import kotlin.coroutines.CoroutineContext + + +/** + * A mutable group of devices and properties to be used for lightweight design and simulations. + */ +public class DeviceGroup( + public val deviceManager: DeviceManager, + override val meta: Meta, +) : DeviceHub, CachingDevice { + + internal class Property( + val state: DeviceState, + val descriptor: PropertyDescriptor, + ) + + internal class Action( + val invoke: suspend (Meta?) -> Meta?, + val descriptor: ActionDescriptor, + ) + + + override val context: Context get() = deviceManager.context + + override val coroutineContext: CoroutineContext by lazy { + context.newCoroutineContext( + SupervisorJob(context.coroutineContext[Job]) + + CoroutineName("Device $this") + + CoroutineExceptionHandler { _, throwable -> + launch { + sharedMessageFlow.emit( + DeviceErrorMessage( + errorMessage = throwable.message, + errorType = throwable::class.simpleName, + errorStackTrace = throwable.stackTraceToString() + ) + ) + } + } + ) + } + + private val _devices = hashMapOf() + + override val devices: Map = _devices + + public fun device(token: NameToken, device: D): D { + check(_devices[token] == null) { "A child device with name $token already exists" } + _devices[token] = device + return device + } + + private val properties: MutableMap = hashMapOf() + + public fun property(descriptor: PropertyDescriptor, state: DeviceState) { + val name = descriptor.name.parseAsName() + require(properties[name] == null) { "Can't add property with name $name. It already exists." } + properties[name] = Property(state, descriptor) + } + + private val actions: MutableMap = hashMapOf() + + override val propertyDescriptors: Collection + get() = properties.values.map { it.descriptor } + + override val actionDescriptors: Collection + get() = actions.values.map { it.descriptor } + + override suspend fun readProperty(propertyName: String): Meta = + properties[propertyName.parseAsName()]?.state?.valueAsMeta + ?: error("Property with name $propertyName not found") + + override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.state?.valueAsMeta + + override suspend fun invalidate(propertyName: String) { + //does nothing for this implementation + } + + override suspend fun writeProperty(propertyName: String, value: Meta) { + val property = (properties[propertyName.parseAsName()]?.state as? MutableDeviceState) + ?: error("Property with name $propertyName not found") + property.valueAsMeta = value + } + + private val sharedMessageFlow = MutableSharedFlow() + + override val messageFlow: Flow + get() = sharedMessageFlow + + override suspend fun execute(actionName: String, argument: Meta?): Meta? { + val action = actions[actionName] ?: error("Action with name $actionName not found") + return action.invoke(argument) + } + + @DFExperimental + override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED + private set(value) { + if (field != value) { + launch { + sharedMessageFlow.emit( + DeviceLifeCycleMessage(value) + ) + } + } + field = value + } + + + @OptIn(DFExperimental::class) + override suspend fun start() { + lifecycleState = DeviceLifecycleState.STARTING + super.start() + devices.values.forEach { + it.start() + } + lifecycleState = DeviceLifecycleState.STARTED + } + + @OptIn(DFExperimental::class) + override fun stop() { + devices.values.forEach { + it.stop() + } + super.stop() + lifecycleState = DeviceLifecycleState.STOPPED + } + + public companion object { + + } +} + +public fun DeviceManager.deviceGroup( + name: String = "@group", + meta: Meta = Meta.EMPTY, + block: DeviceGroup.() -> Unit, +): DeviceGroup { + val group = DeviceGroup(this, meta).apply(block) + install(name, group) + return group +} + +private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { + return when (name.length) { + 0 -> this + 1 -> { + val token = name.first() + when (val d = devices[token]) { + null -> device( + token, + DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY) + ) + + else -> (d as? DeviceGroup) ?: error("Device $name is not a DeviceGroup") + } + } + + else -> getOrCreateGroup(name.first().asName()).getOrCreateGroup(name.cutFirst()) + } +} + +/** + * Register a device at given [name] path + */ +public fun DeviceGroup.device(name: Name, device: D): D { + return when (name.length) { + 0 -> error("Can't use empty name for a child device") + 1 -> device(name.first(), device) + else -> getOrCreateGroup(name.cutLast()).device(name.tokens.last(), device) + } +} + +public fun DeviceGroup.device(name: String, device: D): D = device(name.parseAsName(), device) + +/** + * Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error. + */ +public fun DeviceGroup.device(name: Name, factory: Factory, deviceMeta: Meta? = null): Device { + val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[name])) + device(name, newDevice) + return newDevice +} + +public fun DeviceGroup.device( + name: String, + factory: Factory, + metaBuilder: (MutableMeta.() -> Unit)? = null, +): Device = device(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }) + +/** + * Create or edit a group with a given [name]. + */ +public fun DeviceGroup.deviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup = + getOrCreateGroup(name).apply(block) + +public fun DeviceGroup.deviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup = + deviceGroup(name.parseAsName(), block) + +public fun DeviceGroup.property( + name: String, + state: DeviceState, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): DeviceState { + property( + PropertyDescriptor(name).apply(descriptorBuilder), + state + ) + return state +} + +public fun DeviceGroup.mutableProperty( + name: String, + state: MutableDeviceState, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): MutableDeviceState { + property( + PropertyDescriptor(name).apply(descriptorBuilder), + state + ) + return state +} + +public fun DeviceGroup.virtualProperty( + name: String, + initialValue: T, + converter: MetaConverter, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): MutableDeviceState { + val state = VirtualDeviceState(converter, initialValue) + return mutableProperty(name, state, descriptorBuilder) +} + +/** + * Create a virtual [MutableDeviceState], but do not register it to a device + */ +@Suppress("UnusedReceiverParameter") +public fun DeviceGroup.standAloneProperty( + initialValue: T, + converter: MetaConverter, +): MutableDeviceState = VirtualDeviceState(converter, initialValue) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index 2e6461f..783752a 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -1,6 +1,12 @@ package space.kscience.controls.constructor -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import space.kscience.controls.api.Device +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.MutableDevicePropertySpec +import space.kscience.controls.spec.name import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.transformations.MetaConverter @@ -12,14 +18,106 @@ public interface DeviceState { public val value: T public val valueFlow: Flow - - public val metaFlow: Flow } +public val DeviceState.metaFlow: Flow get() = valueFlow.map(converter::objectToMeta) + +public val DeviceState.valueAsMeta: Meta get() = converter.objectToMeta(value) + /** * A mutable state of a device */ -public interface MutableDeviceState : DeviceState{ +public interface MutableDeviceState : DeviceState { override var value: T -} \ No newline at end of file +} + +public var MutableDeviceState.valueAsMeta: Meta + get() = converter.objectToMeta(value) + set(arg) { + value = converter.metaToObject(arg) ?: error("Conversion for meta $arg to property type with $converter failed") + } + +/** + * A [MutableDeviceState] that does not correspond to a physical state + */ +public class VirtualDeviceState( + override val converter: MetaConverter, + initialValue: T, +) : MutableDeviceState { + private val flow = MutableStateFlow(initialValue) + override val valueFlow: Flow get() = flow + + override var value: T by flow::value +} + +private open class BoundDeviceState( + override val converter: MetaConverter, + val device: Device, + val propertyName: String, + private val initialValue: T, +) : DeviceState { + + override val valueFlow: StateFlow = device.messageFlow.filterIsInstance().filter { + it.property == propertyName + }.mapNotNull { + converter.metaToObject(it.value) + }.stateIn(device.context, SharingStarted.Eagerly, initialValue) + + override val value: T get() = valueFlow.value +} + +/** + * Bind a read-only [DeviceState] to a [Device] property + */ +public suspend fun Device.bindStateToProperty( + propertyName: String, + metaConverter: MetaConverter, +): DeviceState { + val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") + return BoundDeviceState(metaConverter, this, propertyName, initialValue) +} + +public suspend fun D.bindStateToProperty( + propertySpec: DevicePropertySpec, +): DeviceState = bindStateToProperty(propertySpec.name, propertySpec.converter) + +public fun DeviceState.map( + converter: MetaConverter, mapper: (T) -> R, +): DeviceState = object : DeviceState { + override val converter: MetaConverter = converter + override val value: R + get() = mapper(this@map.value) + + override val valueFlow: Flow = this@map.valueFlow.map(mapper) +} + +private class MutableBoundDeviceState( + converter: MetaConverter, + device: Device, + propertyName: String, + initialValue: T, +) : BoundDeviceState(converter, device, propertyName, initialValue), MutableDeviceState { + + override var value: T + get() = valueFlow.value + set(newValue) { + device.launch { + device.writeProperty(propertyName, converter.objectToMeta(newValue)) + } + } +} + +public suspend fun Device.bindMutableStateToProperty( + propertyName: String, + metaConverter: MetaConverter, +): MutableDeviceState { + val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") + return MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) +} + +public suspend fun D.bindMutableStateToProperty( + propertySpec: MutableDevicePropertySpec, +): MutableDeviceState = bindMutableStateToProperty(propertySpec.name, propertySpec.converter) + + diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt deleted file mode 100644 index 8d971bb..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceTree.kt +++ /dev/null @@ -1,33 +0,0 @@ -package space.kscience.controls.constructor - -import space.kscience.controls.api.Device -import space.kscience.controls.api.DeviceHub -import space.kscience.controls.manager.DeviceManager -import space.kscience.dataforge.context.Factory -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.names.NameToken -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set - - -public class DeviceTree( - public val deviceManager: DeviceManager, - public val meta: Meta, - builder: Builder, -) : DeviceHub { - public class Builder(public val manager: DeviceManager) { - internal val childrenFactories = mutableMapOf>() - - public fun device(name: String, factory: Factory) { - childrenFactories[NameToken.parse(name)] = factory - } - } - - override val devices: Map = builder.childrenFactories.mapValues { (token, factory) -> - val devicesMeta = meta["devices"] - factory.build(deviceManager.context, devicesMeta?.get(token) ?: Meta.EMPTY) - } - -} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt index 6ac5a07..8111ff6 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt @@ -4,12 +4,9 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.datetime.Clock import space.kscience.controls.api.Device -import space.kscience.controls.spec.DeviceBySpec -import space.kscience.controls.spec.DevicePropertySpec -import space.kscience.controls.spec.DeviceSpec -import space.kscience.controls.spec.doubleProperty +import space.kscience.controls.manager.clock +import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.get @@ -33,7 +30,7 @@ public interface Drive : Device { public val position: Double public companion object : DeviceSpec() { - public val force: DevicePropertySpec by Drive.property( + public val force: MutableDevicePropertySpec by Drive.mutableProperty( MetaConverter.double, Drive::force ) @@ -48,16 +45,15 @@ public interface Drive : Device { public class VirtualDrive( context: Context, private val mass: Double, - position: Double, + public val positionState: MutableDeviceState, ) : Drive, DeviceBySpec(Drive, context) { private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds - private val clock = Clock.System + private val clock = context.clock override var force: Double = 0.0 - override var position: Double = position - private set + override val position: Double get() = positionState.value public var velocity: Double = 0.0 private set @@ -76,10 +72,10 @@ public class VirtualDrive( lastTime = realTime // compute new value based on velocity and acceleration from the previous step - position += velocity * dtSeconds + force/mass * dtSeconds.pow(2) / 2 + positionState.value += velocity * dtSeconds + force / mass * dtSeconds.pow(2) / 2 // compute new velocity based on acceleration on the previous step - velocity += force/mass * dtSeconds + velocity += force / mass * dtSeconds } } } @@ -89,3 +85,10 @@ public class VirtualDrive( } } +public suspend fun Drive.stateOfForce(): MutableDeviceState = bindMutableStateToProperty(Drive.force) + +public fun DeviceGroup.virtualDrive( + name: String, + mass: Double, + positionState: MutableDeviceState, +): VirtualDrive = device(name, VirtualDrive(context, mass, positionState)) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt index dd0dfed..f692e00 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt @@ -6,6 +6,7 @@ import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.booleanProperty import space.kscience.dataforge.context.Context +import space.kscience.dataforge.names.parseAsName /** @@ -25,7 +26,10 @@ public interface LimitSwitch : Device { */ public class VirtualLimitSwitch( context: Context, - private val lockedFunction: () -> Boolean, + public val lockedState: DeviceState, ) : DeviceBySpec(LimitSwitch, context), LimitSwitch { - override val locked: Boolean get() = lockedFunction() -} \ No newline at end of file + override val locked: Boolean get() = lockedState.value +} + +public fun DeviceGroup.virtualLimitSwitch(name: String, lockedState: DeviceState): VirtualLimitSwitch = + device(name.parseAsName(), VirtualLimitSwitch(context, lockedState)) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt index 4a4d0f3..e8e2ec3 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt @@ -6,25 +6,33 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import space.kscience.controls.manager.clock import space.kscience.controls.spec.DeviceBySpec import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit +/** + * Pid regulator parameters + */ +public data class PidParameters( + public val kp: Double, + public val ki: Double, + public val kd: Double, + public val timeStep: Duration = 1.milliseconds, +) + /** * A drive with PID regulator */ public class PidRegulator( public val drive: Drive, - public val kp: Double, - public val ki: Double, - public val kd: Double, - private val dt: Duration = 1.milliseconds, - private val clock: Clock = Clock.System, + public val pidParameters: PidParameters, ) : DeviceBySpec(Regulator, drive.context), Regulator { + private val clock = drive.context.clock + override var target: Double = drive.position private var lastTime: Instant = clock.now() @@ -41,7 +49,7 @@ public class PidRegulator( drive.start() updateJob = launch { while (isActive) { - delay(dt) + delay(pidParameters.timeStep) mutex.withLock { val realTime = clock.now() val delta = target - position @@ -53,7 +61,7 @@ public class PidRegulator( lastTime = realTime lastPosition = drive.position - drive.force = kp * delta + ki * integral + kd * derivative + drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative } } } @@ -64,5 +72,10 @@ public class PidRegulator( } override val position: Double get() = drive.position +} -} \ No newline at end of file +public fun DeviceGroup.pid( + name: String, + drive: Drive, + pidParameters: PidParameters, +): PidRegulator = device(name, PidRegulator(drive, pidParameters)) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt index fa43b57..71cb3b0 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt @@ -3,6 +3,7 @@ package space.kscience.controls.constructor import space.kscience.controls.api.Device import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DeviceSpec +import space.kscience.controls.spec.MutableDevicePropertySpec import space.kscience.controls.spec.doubleProperty import space.kscience.dataforge.meta.transformations.MetaConverter @@ -22,7 +23,7 @@ public interface Regulator : Device { public val position: Double public companion object : DeviceSpec() { - public val target: DevicePropertySpec by property(MetaConverter.double, Regulator::target) + public val target: MutableDevicePropertySpec by mutableProperty(MetaConverter.double, Regulator::target) public val position: DevicePropertySpec by doubleProperty { position } } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt new file mode 100644 index 0000000..e6a29a5 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt @@ -0,0 +1,41 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import space.kscience.dataforge.meta.transformations.MetaConverter + + +/** + * A state describing a [Double] value in the [range] + */ +public class DoubleRangeState( + initialValue: Double, + public val range: ClosedFloatingPointRange, +) : MutableDeviceState { + + init { + require(initialValue in range) { "Initial value should be in range" } + } + + override val converter: MetaConverter = MetaConverter.double + + private val _valueFlow = MutableStateFlow(initialValue) + + override var value: Double + get() = _valueFlow.value + set(newValue) { + _valueFlow.value = newValue.coerceIn(range) + } + + override val valueFlow: StateFlow get() = _valueFlow + + /** + * A state showing that the range is on its lower boundary + */ + public val atStartState: DeviceState = map(MetaConverter.boolean) { it == range.start } + + /** + * A state showing that the range is on its higher boundary + */ + public val atEndState: DeviceState = map(MetaConverter.boolean) { it == range.endInclusive } +} \ 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 872a5e6..f967c89 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 @@ -3,10 +3,7 @@ package space.kscience.controls.api import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import kotlinx.serialization.Serializable import space.kscience.controls.api.Device.Companion.DEVICE_TARGET import space.kscience.dataforge.context.ContextAware @@ -74,18 +71,6 @@ public interface Device : ContextAware, CoroutineScope { */ public suspend fun readProperty(propertyName: String): Meta - /** - * Get the logical state of property or return null if it is invalid - */ - public fun getProperty(propertyName: String): Meta? - - /** - * Invalidate property (set logical state to invalid) - * - * This message is suspended to provide lock-free local property changes (they require coroutine context). - */ - public suspend fun invalidate(propertyName: String) - /** * Set property [value] for a property with name [propertyName]. * In rare cases could suspend if the [Device] supports command queue, and it is full at the moment. @@ -126,17 +111,38 @@ public interface Device : ContextAware, CoroutineScope { } } +/** + * Device that caches properties values + */ +public interface CachingDevice : Device { + + /** + * Immediately (without waiting) get the cached (logical) state of property or return null if it is invalid + */ + public fun getProperty(propertyName: String): Meta? + + /** + * Invalidate property (set logical state to invalid). + * + * This message is suspended to provide lock-free local property changes (they require coroutine context). + */ + public suspend fun invalidate(propertyName: String) +} + /** * Get the logical state of property or suspend to read the physical value. */ -public suspend fun Device.getOrReadProperty(propertyName: String): Meta = +public suspend fun Device.requestProperty(propertyName: String): Meta = if (this is CachingDevice) { getProperty(propertyName) ?: readProperty(propertyName) +} else { + readProperty(propertyName) +} /** * Get a snapshot of the device logical state * */ -public fun Device.getAllProperties(): Meta = Meta { +public fun CachingDevice.getAllProperties(): Meta = Meta { for (descriptor in propertyDescriptors) { setMeta(Name.parse(descriptor.name), getProperty(descriptor.name)) } @@ -148,5 +154,11 @@ public fun Device.getAllProperties(): Meta = Meta { public fun Device.onPropertyChange( scope: CoroutineScope = this, callback: suspend PropertyChangedMessage.() -> Unit, -): Job = - messageFlow.filterIsInstance().onEach(callback).launchIn(scope) +): Job = messageFlow.filterIsInstance().onEach(callback).launchIn(scope) + +/** + * A [Flow] of property change messages for specific property. + */ +public fun Device.propertyMessageFlow(propertyName: String): Flow = messageFlow + .filterIsInstance() + .filter { it.property == propertyName } 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 32df1bb..b3436e9 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 @@ -71,7 +71,7 @@ public data class PropertyChangedMessage( @SerialName("property.set") public data class PropertySetMessage( public val property: String, - public val value: Meta?, + public val value: Meta, override val sourceDevice: Name? = null, override val targetDevice: Name, override val comment: String? = null, diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt new file mode 100644 index 0000000..ca69b91 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt @@ -0,0 +1,25 @@ +package space.kscience.controls.manager + +import kotlinx.datetime.Clock +import space.kscience.dataforge.context.AbstractPlugin +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.PluginFactory +import space.kscience.dataforge.context.PluginTag +import space.kscience.dataforge.meta.Meta + +public class ClockManager : AbstractPlugin() { + override val tag: PluginTag get() = DeviceManager.tag + + public val clock: Clock by lazy { + //TODO add clock customization + Clock.System + } + + public companion object : PluginFactory { + override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP) + + override fun build(context: Context, meta: Meta): ClockManager = ClockManager() + } +} + +public val Context.clock: Clock get() = plugins[ClockManager]?.clock ?: Clock.System \ No newline at end of file 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 d3abc03..4d319af 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 @@ -17,21 +17,17 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess is PropertyGetMessage -> { PropertyChangedMessage( property = request.property, - value = getOrReadProperty(request.property), + value = requestProperty(request.property), sourceDevice = deviceTarget, targetDevice = request.sourceDevice ) } is PropertySetMessage -> { - if (request.value == null) { - invalidate(request.property) - } else { - writeProperty(request.property, request.value) - } + writeProperty(request.property, request.value) PropertyChangedMessage( property = request.property, - value = getOrReadProperty(request.property), + value = requestProperty(request.property), sourceDevice = deviceTarget, targetDevice = request.sourceDevice ) 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 da730f8..43574cf 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 @@ -20,7 +20,7 @@ import kotlin.coroutines.CoroutineContext * Write a meta [item] to [device] */ @OptIn(InternalDeviceAPI::class) -private suspend fun WritableDevicePropertySpec.writeMeta(device: D, item: Meta) { +private suspend fun MutableDevicePropertySpec.writeMeta(device: D, item: Meta) { write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter")) } @@ -48,7 +48,7 @@ private suspend fun DeviceActionSpec.executeWithMeta public abstract class DeviceBase( final override val context: Context, final override val meta: Meta = Meta.EMPTY, -) : Device { +) : CachingDevice { /** * Collection of property specifications @@ -166,7 +166,7 @@ public abstract class DeviceBase( propertyChanged(propertyName, value) } - is WritableDevicePropertySpec -> { + is MutableDevicePropertySpec -> { //if there is a writeable property with a given name, invalidate logical and write physical invalidate(propertyName) property.writeMeta(self, value) @@ -189,8 +189,8 @@ public abstract class DeviceBase( } @DFExperimental - override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED - protected set(value) { + final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED + private set(value) { if (field != value) { launch { sharedMessageFlow.emit( 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 c42a23e..cb511ea 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 @@ -4,10 +4,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import space.kscience.controls.api.ActionDescriptor -import space.kscience.controls.api.Device -import space.kscience.controls.api.PropertyChangedMessage -import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.api.* import space.kscience.dataforge.meta.transformations.MetaConverter @@ -44,7 +41,7 @@ public interface DevicePropertySpec { public val DevicePropertySpec<*, *>.name: String get() = descriptor.name -public interface WritableDevicePropertySpec : DevicePropertySpec { +public interface MutableDevicePropertySpec : DevicePropertySpec { /** * Write physical value to a device */ @@ -84,21 +81,20 @@ public suspend fun D.read(propertySpec: DevicePropertySpec public suspend fun > D.readOrNull(propertySpec: DevicePropertySpec): T? = readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject) - -public operator fun D.get(propertySpec: DevicePropertySpec): T? = - getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject) +public suspend fun D.request(propertySpec: DevicePropertySpec): T? = + propertySpec.converter.metaToObject(requestProperty(propertySpec.name)) /** * Write typed property state and invalidate logical state */ -public suspend fun D.write(propertySpec: WritableDevicePropertySpec, value: T) { +public suspend fun D.write(propertySpec: MutableDevicePropertySpec, value: T) { 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): Job = launch { +public fun D.writeAsync(propertySpec: MutableDevicePropertySpec, value: T): Job = launch { write(propertySpec, value) } @@ -151,7 +147,7 @@ public fun D.useProperty( /** * Reset the logical state of a property */ -public suspend fun D.invalidate(propertySpec: DevicePropertySpec) { +public suspend fun D.invalidate(propertySpec: DevicePropertySpec) { invalidate(propertySpec.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 14966ca..55122d9 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 @@ -72,9 +72,9 @@ public abstract class DeviceSpec { converter: MetaConverter, readWriteProperty: KMutableProperty1, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - ): PropertyDelegateProvider, ReadOnlyProperty>> = + ): PropertyDelegateProvider, ReadOnlyProperty>> = PropertyDelegateProvider { _, property -> - val deviceProperty = object : WritableDevicePropertySpec { + val deviceProperty = object : MutableDevicePropertySpec { override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { //TODO add the type from converter @@ -123,10 +123,10 @@ public abstract class DeviceSpec { name: String? = null, read: suspend D.() -> T?, write: suspend D.(T) -> Unit, - ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = + ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val propertyName = name ?: property.name - val deviceProperty = object : WritableDevicePropertySpec { + val deviceProperty = object : MutableDevicePropertySpec { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, mutable = true) .apply(descriptorBuilder) override val converter: MetaConverter = converter @@ -138,7 +138,7 @@ public abstract class DeviceSpec { } } _properties[propertyName] = deviceProperty - ReadOnlyProperty, WritableDevicePropertySpec> { _, _ -> + ReadOnlyProperty, MutableDevicePropertySpec> { _, _ -> deviceProperty } } @@ -218,9 +218,9 @@ public fun > DeviceSpec.logicalProperty( converter: MetaConverter, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, -): PropertyDelegateProvider, ReadOnlyProperty>> = +): PropertyDelegateProvider, ReadOnlyProperty>> = PropertyDelegateProvider { _, property -> - val deviceProperty = object : WritableDevicePropertySpec { + val deviceProperty = object : MutableDevicePropertySpec { val propertyName = name ?: property.name override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { //TODO add type from converter 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 ff89662..70ef94a 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 @@ -97,7 +97,7 @@ public fun DeviceSpec.booleanProperty( name: String? = null, read: suspend D.() -> Boolean?, write: suspend D.(Boolean) -> Unit -): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty( MetaConverter.boolean, { @@ -117,7 +117,7 @@ public fun DeviceSpec.numberProperty( name: String? = null, read: suspend D.() -> Number, write: suspend D.(Number) -> Unit -): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) public fun DeviceSpec.doubleProperty( @@ -125,7 +125,7 @@ public fun DeviceSpec.doubleProperty( name: String? = null, read: suspend D.() -> Double, write: suspend D.(Double) -> Unit -): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) public fun DeviceSpec.stringProperty( @@ -133,7 +133,7 @@ public fun DeviceSpec.stringProperty( name: String? = null, read: suspend D.() -> String, write: suspend D.(String) -> Unit -): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) public fun DeviceSpec.metaProperty( @@ -141,5 +141,5 @@ public fun DeviceSpec.metaProperty( name: String? = null, read: suspend D.() -> Meta, write: suspend D.(Meta) -> Unit -): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write) \ No newline at end of file diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt index 2fcecff..474f86a 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt @@ -26,7 +26,7 @@ public class DeviceClient( private val deviceName: Name, incomingFlow: Flow, private val send: suspend (DeviceMessage) -> Unit, -) : Device { +) : CachingDevice { @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext) diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt index d0bdda4..9c30f8c 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import space.kscience.controls.api.get -import space.kscience.controls.api.getOrReadProperty +import space.kscience.controls.api.requestProperty import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.context.error import space.kscience.dataforge.context.logger @@ -91,7 +91,7 @@ public fun DeviceManager.launchTangoMagix( val device = get(payload.device) when (payload.action) { TangoAction.read -> { - val value = device.getOrReadProperty(payload.name) + val value = device.requestProperty(payload.name) respond(request, payload) { requestPayload -> requestPayload.copy( value = value, @@ -104,7 +104,7 @@ public fun DeviceManager.launchTangoMagix( device.writeProperty(payload.name, value) } //wait for value to be written and return final state - val value = device.getOrReadProperty(payload.name) + val value = device.requestProperty(payload.name) respond(request, payload) { requestPayload -> requestPayload.copy( value = value, diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt index b4fae90..8f4a2b4 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt @@ -6,10 +6,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import space.kscience.controls.api.Device -import space.kscience.controls.spec.DevicePropertySpec -import space.kscience.controls.spec.WritableDevicePropertySpec -import space.kscience.controls.spec.set -import space.kscience.controls.spec.useProperty +import space.kscience.controls.spec.* public class DeviceProcessImageBuilder internal constructor( @@ -29,10 +26,10 @@ public class DeviceProcessImageBuilder internal constructor( public fun bind( key: ModbusRegistryKey.Coil, - propertySpec: WritableDevicePropertySpec, + propertySpec: MutableDevicePropertySpec, ): ObservableDigitalOut = bind(key) { coil -> coil.addObserver { _, _ -> - device[propertySpec] = coil.isSet + device.writeAsync(propertySpec, coil.isSet) } device.useProperty(propertySpec) { value -> coil.set(value) @@ -89,10 +86,10 @@ public class DeviceProcessImageBuilder internal constructor( public fun bind( key: ModbusRegistryKey.HoldingRegister, - propertySpec: WritableDevicePropertySpec, + propertySpec: MutableDevicePropertySpec, ): ObservableRegister = bind(key) { register -> register.addObserver { _, _ -> - device[propertySpec] = register.toShort() + device.writeAsync(propertySpec, register.toShort()) } device.useProperty(propertySpec) { value -> register.setValue(value) @@ -121,7 +118,7 @@ public class DeviceProcessImageBuilder internal constructor( /** * Trigger [block] if one of register changes. */ - private fun List.onChange(block: (ByteReadPacket) -> Unit) { + private fun List.onChange(block: suspend (ByteReadPacket) -> Unit) { var ready = false forEach { register -> @@ -147,7 +144,7 @@ public class DeviceProcessImageBuilder internal constructor( } } - public fun bind(key: ModbusRegistryKey.HoldingRange, propertySpec: WritableDevicePropertySpec) { + public fun bind(key: ModbusRegistryKey.HoldingRange, propertySpec: MutableDevicePropertySpec) { val registers = List(key.count) { ObservableRegister() } @@ -157,7 +154,7 @@ public class DeviceProcessImageBuilder internal constructor( } registers.onChange { packet -> - device[propertySpec] = key.format.readObject(packet) + device.write(propertySpec, key.format.readObject(packet)) } device.useProperty(propertySpec) { value -> diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt index a05a81d..05fc3c6 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt @@ -19,10 +19,7 @@ import org.eclipse.milo.opcua.stack.core.AttributeId import org.eclipse.milo.opcua.stack.core.Identifiers import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText -import space.kscience.controls.api.Device -import space.kscience.controls.api.DeviceHub -import space.kscience.controls.api.PropertyDescriptor -import space.kscience.controls.api.onPropertyChange +import space.kscience.controls.api.* import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaSerializer @@ -31,7 +28,7 @@ import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.plus -public operator fun Device.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name) +public operator fun CachingDevice.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name) public suspend fun Device.read(propertyDescriptor: PropertyDescriptor): Meta = readProperty(propertyDescriptor.name) @@ -106,9 +103,11 @@ public class DeviceNameSpace( setTypeDefinition(Identifiers.BaseDataVariableType) }.build() - - device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let { - node.value = it + // Update initial value, but only if it is cached + if(device is CachingDevice) { + device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let { + node.value = it + } } /** diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts index 0b3e9f1..e1401d9 100644 --- a/controls-vision/build.gradle.kts +++ b/controls-vision/build.gradle.kts @@ -7,14 +7,23 @@ description = """ Dashboard and visualization extensions for devices """.trimIndent() -kscience{ +val visionforgeVersion = "0.3.0-dev-10" + +kscience { jvm() js() dependencies { api(projects.controlsCore) + api(projects.controlsConstructor) + api("space.kscience:visionforge-plotly:$visionforgeVersion") + api("space.kscience:visionforge-markdown:$visionforgeVersion") + } + + jvmMain{ + api("space.kscience:visionforge-server:$visionforgeVersion") } } -readme{ +readme { maturity = space.kscience.gradle.Maturity.PROTOTYPE } diff --git a/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt new file mode 100644 index 0000000..392930c --- /dev/null +++ b/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt @@ -0,0 +1,56 @@ +package space.kscience.controls.vision + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.datetime.Clock +import space.kscience.controls.api.Device +import space.kscience.controls.api.propertyMessageFlow +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.manager.clock +import space.kscience.dataforge.meta.ListValue +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.Null +import space.kscience.dataforge.meta.Value +import space.kscience.plotly.Plot +import space.kscience.plotly.models.Scatter +import space.kscience.plotly.models.TraceValues +import space.kscience.plotly.scatter + +private var TraceValues.values: List + get() = value?.list ?: emptyList() + set(newValues) { + value = ListValue(newValues) + } + +/** + * Add a trace that shows a [Device] property change over time. Show only latest [pointsNumber] . + * @return a [Job] that handles the listener + */ +public fun Plot.plotDeviceProperty( + device: Device, + propertyName: String, + extractValue: Meta.() -> Value = { value ?: Null }, + pointsNumber: Int = 400, + coroutineScope: CoroutineScope = device, + configuration: Scatter.() -> Unit = {}, +): Job = scatter(configuration).run { + val clock = device.context.clock + device.propertyMessageFlow(propertyName).onEach { message -> + x.strings = (x.strings + (message.time ?: clock.now()).toString()).takeLast(pointsNumber) + y.values = (y.values + message.value.extractValue()).takeLast(pointsNumber) + }.launchIn(coroutineScope) +} + +public fun Plot.plotDeviceState( + scope: CoroutineScope, + state: DeviceState, + pointsNumber: Int = 400, + configuration: Scatter.() -> Unit = {}, +): Job = scatter(configuration).run { + state.valueFlow.onEach { + x.strings = (x.strings + Clock.System.now().toString()).takeLast(pointsNumber) + y.numbers = (y.numbers + it).takeLast(pointsNumber) + }.launchIn(scope) +} \ No newline at end of file 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 2ba0fdb..8ccb8a4 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 @@ -4,8 +4,8 @@ package space.kscience.controls.demo.car import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import space.kscience.controls.manager.clock import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.read @@ -41,6 +41,8 @@ data class Vector2D(var x: Double = 0.0, var y: Double = 0.0) : MetaRepr { } open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec(IVirtualCar, context, meta), IVirtualCar { + private val clock = context.clock + private val timeScale = 1e-3 private val mass by meta.double(1000.0) // mass in kilograms @@ -57,7 +59,7 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec(I private var timeState: Instant? = null - private fun update(newTime: Instant = Clock.System.now()) { + private fun update(newTime: Instant = clock.now()) { //initialize time if it is not initialized if (timeState == null) { timeState = newTime @@ -102,7 +104,7 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec(I @OptIn(ExperimentalTime::class) override suspend fun onStart() { //initializing the clock - timeState = Clock.System.now() + timeState = clock.now() //starting regular updates doRecurring(100.milliseconds) { update() diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts new file mode 100644 index 0000000..031c3a0 --- /dev/null +++ b/demo/constructor/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("space.kscience.gradle.mpp") + application +} + +kscience { + fullStack("js/constructor.js") + useKtor() + dependencies { + api(projects.controlsVision) + } + jvmMain { + implementation("io.ktor:ktor-server-cio") + implementation(spclibs.logback.classic) + } +} + +application { + mainClass.set("space.kscience.controls.demo.constructor.MainKt") +} \ No newline at end of file diff --git a/demo/constructor/src/jsMain/kotlin/constructorJs.kt b/demo/constructor/src/jsMain/kotlin/constructorJs.kt new file mode 100644 index 0000000..27bbc28 --- /dev/null +++ b/demo/constructor/src/jsMain/kotlin/constructorJs.kt @@ -0,0 +1,6 @@ +import space.kscience.visionforge.plotly.PlotlyPlugin +import space.kscience.visionforge.runVisionClient + +public fun main(): Unit = runVisionClient { + plugin(PlotlyPlugin) +} \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/Main.kt b/demo/constructor/src/jvmMain/kotlin/Main.kt new file mode 100644 index 0000000..fdba90c --- /dev/null +++ b/demo/constructor/src/jvmMain/kotlin/Main.kt @@ -0,0 +1,103 @@ +package space.kscience.controls.demo.constructor + +import io.ktor.server.cio.CIO +import io.ktor.server.engine.embeddedServer +import io.ktor.server.http.content.staticResources +import io.ktor.server.routing.routing +import space.kscience.controls.api.get +import space.kscience.controls.constructor.* +import space.kscience.controls.manager.ClockManager +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.clock +import space.kscience.controls.spec.doRecurring +import space.kscience.controls.spec.name +import space.kscience.controls.spec.write +import space.kscience.controls.vision.plotDeviceProperty +import space.kscience.controls.vision.plotDeviceState +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request +import space.kscience.visionforge.VisionManager +import space.kscience.visionforge.html.VisionPage +import space.kscience.visionforge.plotly.PlotlyPlugin +import space.kscience.visionforge.plotly.plotly +import space.kscience.visionforge.server.close +import space.kscience.visionforge.server.openInBrowser +import space.kscience.visionforge.server.visionPage +import kotlin.math.PI +import kotlin.math.sin +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit + +@Suppress("ExtractKtorModule") +public fun main() { + val context = Context { + plugin(DeviceManager) + plugin(PlotlyPlugin) + plugin(ClockManager) + } + + val deviceManager = context.request(DeviceManager) + val visionManager = context.request(VisionManager) + + val state = DoubleRangeState(0.0, -100.0..100.0) + + val pidParameters = PidParameters( + kp = 2.5, + ki = 0.0, + kd = -0.1, + timeStep = 0.005.seconds + ) + + val device = deviceManager.deviceGroup { + val drive = virtualDrive("drive", 0.005, state) + val pid = pid("pid", drive, pidParameters) + virtualLimitSwitch("start", state.atStartState) + virtualLimitSwitch("end", state.atEndState) + + val clock = context.clock + val clockStart = clock.now() + + doRecurring(10.milliseconds) { + val timeFromStart = clock.now() - clockStart + val t = timeFromStart.toDouble(DurationUnit.SECONDS) + val freq = 0.1 + val target = 5 * sin(2.0 * PI * freq * t) + + sin(2 * PI * 21 * freq * t + 0.1 * (timeFromStart / pidParameters.timeStep)) + pid.write(Regulator.target, target) + } + } + + val server = embeddedServer(CIO, port = 7777) { + routing { + staticResources("", null, null) + } + + visionPage( + visionManager, + VisionPage.scriptHeader("js/constructor.js") + ) { + vision { + plotly { + plotDeviceState(this@embeddedServer, state){ + name = "value" + } + plotDeviceProperty(device["pid"], Regulator.target.name){ + name = "target" + } + } + } + } + + }.start(false) + + server.openInBrowser() + + + println("Enter 'exit' to close server") + while (readlnOrNull() != "exit") { + // + } + + server.close() +} \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/resources/logback.xml b/demo/constructor/src/jvmMain/resources/logback.xml new file mode 100644 index 0000000..3865b14 --- /dev/null +++ b/demo/constructor/src/jvmMain/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file 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 7771709..df54bfa 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 @@ -94,7 +94,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec D.fxProperty( } } -fun D.fxProperty(spec: WritableDevicePropertySpec): Property = +fun D.fxProperty(spec: MutableDevicePropertySpec): Property = object : ObjectPropertyBase() { override fun getBean(): Any = this override fun getName(): String = spec.name @@ -51,7 +51,7 @@ fun D.fxProperty(spec: WritableDevicePropertySpec): onChange { newValue -> if (newValue != null) { - set(spec, newValue) + writeAsync(spec, newValue) } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index ee40d35..453519f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -69,5 +69,6 @@ include( ":demo:car", ":demo:motors", ":demo:echo", - ":demo:mks-pdr900" + ":demo:mks-pdr900", + ":demo:constructor" ) From 984e7f12ef4ad6e0a4fe8e4a4cfb77ae8bcacadc Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 30 Oct 2023 21:47:41 +0300 Subject: [PATCH 21/71] Add JVM application for constructor demo --- .../space/kscience/controls/vision/plotExtensions.kt | 9 +++++---- demo/constructor/build.gradle.kts | 2 +- demo/constructor/src/jvmMain/kotlin/{Main.kt => main.kt} | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) rename demo/constructor/src/jvmMain/kotlin/{Main.kt => main.kt} (97%) diff --git a/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt index 392930c..958a9ff 100644 --- a/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt @@ -4,11 +4,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.datetime.Clock import space.kscience.controls.api.Device import space.kscience.controls.api.propertyMessageFlow import space.kscience.controls.constructor.DeviceState import space.kscience.controls.manager.clock +import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.ListValue import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Null @@ -44,13 +44,14 @@ public fun Plot.plotDeviceProperty( } public fun Plot.plotDeviceState( - scope: CoroutineScope, + context: Context, state: DeviceState, pointsNumber: Int = 400, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { + val clock = context.clock state.valueFlow.onEach { - x.strings = (x.strings + Clock.System.now().toString()).takeLast(pointsNumber) + x.strings = (x.strings + clock.now().toString()).takeLast(pointsNumber) y.numbers = (y.numbers + it).takeLast(pointsNumber) - }.launchIn(scope) + }.launchIn(context) } \ No newline at end of file diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index 031c3a0..897b9af 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } kscience { - fullStack("js/constructor.js") + fullStack("js/constructor.js", jvmConfig = {withJava()}) useKtor() dependencies { api(projects.controlsVision) diff --git a/demo/constructor/src/jvmMain/kotlin/Main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt similarity index 97% rename from demo/constructor/src/jvmMain/kotlin/Main.kt rename to demo/constructor/src/jvmMain/kotlin/main.kt index fdba90c..76d4b51 100644 --- a/demo/constructor/src/jvmMain/kotlin/Main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -79,7 +79,7 @@ public fun main() { ) { vision { plotly { - plotDeviceState(this@embeddedServer, state){ + plotDeviceState(context, state){ name = "value" } plotDeviceProperty(device["pid"], Regulator.target.name){ From 811477a6367dae6aa39755fd0be9df3c48c86164 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 30 Oct 2023 22:51:17 +0300 Subject: [PATCH 22/71] add limit readers --- .../kscience/controls/constructor/Drive.kt | 1 + .../controls/constructor/LimitSwitch.kt | 9 +++++ .../controls/constructor/PidRegulator.kt | 1 + .../controls/constructor/customState.kt | 4 +-- .../controls/vision/plotExtensions.kt | 25 ++++++++++---- demo/constructor/src/jvmMain/kotlin/main.kt | 34 ++++++++++++++++--- 6 files changed, 61 insertions(+), 13 deletions(-) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt index 8111ff6..e7c3d39 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt @@ -73,6 +73,7 @@ public class VirtualDrive( // compute new value based on velocity and acceleration from the previous step positionState.value += velocity * dtSeconds + force / mass * dtSeconds.pow(2) / 2 + propertyChanged(Drive.position, positionState.value) // compute new velocity based on acceleration on the previous step velocity += force / mass * dtSeconds diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt index f692e00..3db6512 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt @@ -1,5 +1,7 @@ package space.kscience.controls.constructor +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import space.kscience.controls.api.Device import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DevicePropertySpec @@ -28,6 +30,13 @@ public class VirtualLimitSwitch( context: Context, public val lockedState: DeviceState, ) : DeviceBySpec(LimitSwitch, context), LimitSwitch { + + init { + lockedState.valueFlow.onEach { + propertyChanged(LimitSwitch.locked, it) + }.launchIn(this) + } + override val locked: Boolean get() = lockedState.value } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt index e8e2ec3..9268d02 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt @@ -62,6 +62,7 @@ public class PidRegulator( lastPosition = drive.position drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative + propertyChanged(Regulator.position, drive.position) } } } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt index e6a29a5..e745875 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt @@ -32,10 +32,10 @@ public class DoubleRangeState( /** * A state showing that the range is on its lower boundary */ - public val atStartState: DeviceState = map(MetaConverter.boolean) { it == range.start } + public val atStartState: DeviceState = map(MetaConverter.boolean) { it <= range.start } /** * A state showing that the range is on its higher boundary */ - public val atEndState: DeviceState = map(MetaConverter.boolean) { it == range.endInclusive } + public val atEndState: DeviceState = map(MetaConverter.boolean) { it >= range.endInclusive } } \ No newline at end of file diff --git a/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt index 958a9ff..14899d8 100644 --- a/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt @@ -9,11 +9,10 @@ import space.kscience.controls.api.propertyMessageFlow import space.kscience.controls.constructor.DeviceState import space.kscience.controls.manager.clock import space.kscience.dataforge.context.Context -import space.kscience.dataforge.meta.ListValue -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.Null -import space.kscience.dataforge.meta.Value +import space.kscience.dataforge.meta.* import space.kscience.plotly.Plot +import space.kscience.plotly.bar +import space.kscience.plotly.models.Bar import space.kscience.plotly.models.Scatter import space.kscience.plotly.models.TraceValues import space.kscience.plotly.scatter @@ -33,7 +32,7 @@ public fun Plot.plotDeviceProperty( propertyName: String, extractValue: Meta.() -> Value = { value ?: Null }, pointsNumber: Int = 400, - coroutineScope: CoroutineScope = device, + coroutineScope: CoroutineScope = device.context, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { val clock = device.context.clock @@ -43,7 +42,8 @@ public fun Plot.plotDeviceProperty( }.launchIn(coroutineScope) } -public fun Plot.plotDeviceState( + +public fun Plot.plotNumberState( context: Context, state: DeviceState, pointsNumber: Int = 400, @@ -54,4 +54,17 @@ public fun Plot.plotDeviceState( x.strings = (x.strings + clock.now().toString()).takeLast(pointsNumber) y.numbers = (y.numbers + it).takeLast(pointsNumber) }.launchIn(context) +} + +public fun Plot.plotBooleanState( + context: Context, + state: DeviceState, + pointsNumber: Int = 400, + configuration: Bar.() -> Unit = {}, +): Job = bar(configuration).run { + val clock = context.clock + state.valueFlow.onEach { + x.strings = (x.strings + clock.now().toString()).takeLast(pointsNumber) + y.values = (y.values + it.asValue()).takeLast(pointsNumber) + }.launchIn(context) } \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 76d4b51..1df0f33 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -13,9 +13,10 @@ import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.name import space.kscience.controls.spec.write import space.kscience.controls.vision.plotDeviceProperty -import space.kscience.controls.vision.plotDeviceState +import space.kscience.controls.vision.plotNumberState import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.request +import space.kscience.plotly.models.ScatterMode import space.kscience.visionforge.VisionManager import space.kscience.visionforge.html.VisionPage import space.kscience.visionforge.plotly.PlotlyPlugin @@ -40,7 +41,7 @@ public fun main() { val deviceManager = context.request(DeviceManager) val visionManager = context.request(VisionManager) - val state = DoubleRangeState(0.0, -100.0..100.0) + val state = DoubleRangeState(0.0, -5.0..5.0) val pidParameters = PidParameters( kp = 2.5, @@ -79,14 +80,37 @@ public fun main() { ) { vision { plotly { - plotDeviceState(context, state){ - name = "value" + plotNumberState(context, state) { + name = "real position" } - plotDeviceProperty(device["pid"], Regulator.target.name){ + plotDeviceProperty(device["pid"], Regulator.position.name) { + name = "read position" + } + + plotDeviceProperty(device["pid"], Regulator.target.name) { name = "target" } } } + + vision { + plotly { +// plotBooleanState(context, state.atStartState) { +// name = "start" +// } +// plotBooleanState(context, state.atEndState) { +// name = "end" +// } + plotDeviceProperty(device["start"], LimitSwitch.locked.name) { + name = "start measured" + mode = ScatterMode.markers + } + plotDeviceProperty(device["end"], LimitSwitch.locked.name) { + name = "end measured" + mode = ScatterMode.markers + } + } + } } }.start(false) From 2698cee80b1de7f0bb2bbf8c48782c8061695b8c Mon Sep 17 00:00:00 2001 From: darksnake Date: Thu, 2 Nov 2023 15:36:10 +0300 Subject: [PATCH 23/71] Remove automatic reads from virtual drive and pid --- .../kotlin/space/kscience/controls/constructor/Drive.kt | 3 +-- .../kotlin/space/kscience/controls/constructor/PidRegulator.kt | 1 - demo/constructor/src/jvmMain/kotlin/main.kt | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt index e7c3d39..f87d0e3 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt @@ -48,7 +48,7 @@ public class VirtualDrive( public val positionState: MutableDeviceState, ) : Drive, DeviceBySpec(Drive, context) { - private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds + private val dt = meta["time.step"].double?.milliseconds ?: 1.milliseconds private val clock = context.clock override var force: Double = 0.0 @@ -73,7 +73,6 @@ public class VirtualDrive( // compute new value based on velocity and acceleration from the previous step positionState.value += velocity * dtSeconds + force / mass * dtSeconds.pow(2) / 2 - propertyChanged(Drive.position, positionState.value) // compute new velocity based on acceleration on the previous step velocity += force / mass * dtSeconds diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt index 9268d02..e8e2ec3 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt @@ -62,7 +62,6 @@ public class PidRegulator( lastPosition = drive.position drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative - propertyChanged(Regulator.position, drive.position) } } } diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 1df0f33..2996da2 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -11,6 +11,7 @@ import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.clock import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.name +import space.kscience.controls.spec.read import space.kscience.controls.spec.write import space.kscience.controls.vision.plotDeviceProperty import space.kscience.controls.vision.plotNumberState @@ -66,6 +67,7 @@ public fun main() { val target = 5 * sin(2.0 * PI * freq * t) + sin(2 * PI * 21 * freq * t + 0.1 * (timeFromStart / pidParameters.timeStep)) pid.write(Regulator.target, target) + pid.read(Regulator.position) } } From 0e963a7b136b487b1192469aea1bd4e4ab3b4f93 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 5 Nov 2023 09:47:58 +0300 Subject: [PATCH 24/71] Simplify UI management in constructor --- .../controls/constructor/DeviceGroup.kt | 7 ++ controls-vision/build.gradle.kts | 3 + .../kscience/controls/vision/dashboard.kt | 61 ++++++++++++++ demo/constructor/build.gradle.kts | 1 + demo/constructor/src/jvmMain/kotlin/main.kt | 80 ++++++------------- 5 files changed, 95 insertions(+), 57 deletions(-) create mode 100644 controls-vision/src/jvmMain/kotlin/space/kscience/controls/vision/dashboard.kt diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt index f7bf494..b39d7b6 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -8,6 +8,7 @@ import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.context.request import space.kscience.dataforge.meta.Laminate import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MutableMeta @@ -158,6 +159,12 @@ public fun DeviceManager.deviceGroup( return group } +public fun Context.deviceGroup( + name: String = "@group", + meta: Meta = Meta.EMPTY, + block: DeviceGroup.() -> Unit, +): DeviceGroup = request(DeviceManager).deviceGroup(name, meta, block) + private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { return when (name.length) { 0 -> this diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts index e1401d9..335bde2 100644 --- a/controls-vision/build.gradle.kts +++ b/controls-vision/build.gradle.kts @@ -12,6 +12,8 @@ val visionforgeVersion = "0.3.0-dev-10" kscience { jvm() js() + useKtor() + useContextReceivers() dependencies { api(projects.controlsCore) api(projects.controlsConstructor) @@ -21,6 +23,7 @@ kscience { jvmMain{ api("space.kscience:visionforge-server:$visionforgeVersion") + api("io.ktor:ktor-server-cio") } } diff --git a/controls-vision/src/jvmMain/kotlin/space/kscience/controls/vision/dashboard.kt b/controls-vision/src/jvmMain/kotlin/space/kscience/controls/vision/dashboard.kt new file mode 100644 index 0000000..2597e48 --- /dev/null +++ b/controls-vision/src/jvmMain/kotlin/space/kscience/controls/vision/dashboard.kt @@ -0,0 +1,61 @@ +package space.kscience.controls.vision + +import io.ktor.server.cio.CIO +import io.ktor.server.engine.ApplicationEngine +import io.ktor.server.engine.embeddedServer +import io.ktor.server.http.content.staticResources +import io.ktor.server.routing.Routing +import io.ktor.server.routing.routing +import kotlinx.html.TagConsumer +import space.kscience.dataforge.context.Context +import space.kscience.plotly.Plot +import space.kscience.plotly.PlotlyConfig +import space.kscience.visionforge.html.HtmlVisionFragment +import space.kscience.visionforge.html.VisionPage +import space.kscience.visionforge.html.VisionTagConsumer +import space.kscience.visionforge.plotly.plotly +import space.kscience.visionforge.server.VisionRoute +import space.kscience.visionforge.server.close +import space.kscience.visionforge.server.openInBrowser +import space.kscience.visionforge.server.visionPage +import space.kscience.visionforge.visionManager + +public fun Context.showDashboard( + port: Int = 7777, + routes: Routing.() -> Unit = {}, + configurationBuilder: VisionRoute.() -> Unit = {}, + visionFragment: HtmlVisionFragment, +): ApplicationEngine = embeddedServer(CIO, port = port) { + routing { + staticResources("", null, null) + routes() + } + + visionPage( + visionManager, + VisionPage.scriptHeader("js/constructor.js"), + configurationBuilder = configurationBuilder, + visionFragment = visionFragment + ) +}.also { + it.start(false) + it.openInBrowser() + + + println("Enter 'exit' to close server") + while (readlnOrNull() != "exit") { + // + } + + it.close() +} + +context(VisionTagConsumer<*>) +public fun TagConsumer<*>.plot( + config: PlotlyConfig = PlotlyConfig(), + block: Plot.() -> Unit, +) { + vision { + plotly(config, block) + } +} diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index 897b9af..daa4381 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -6,6 +6,7 @@ plugins { kscience { fullStack("js/constructor.js", jvmConfig = {withJava()}) useKtor() + useContextReceivers() dependencies { api(projects.controlsVision) } diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 1df0f33..8fc1dac 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -1,9 +1,5 @@ package space.kscience.controls.demo.constructor -import io.ktor.server.cio.CIO -import io.ktor.server.engine.embeddedServer -import io.ktor.server.http.content.staticResources -import io.ktor.server.routing.routing import space.kscience.controls.api.get import space.kscience.controls.constructor.* import space.kscience.controls.manager.ClockManager @@ -12,18 +8,13 @@ import space.kscience.controls.manager.clock import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.name import space.kscience.controls.spec.write +import space.kscience.controls.vision.plot import space.kscience.controls.vision.plotDeviceProperty import space.kscience.controls.vision.plotNumberState +import space.kscience.controls.vision.showDashboard import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.request import space.kscience.plotly.models.ScatterMode -import space.kscience.visionforge.VisionManager -import space.kscience.visionforge.html.VisionPage import space.kscience.visionforge.plotly.PlotlyPlugin -import space.kscience.visionforge.plotly.plotly -import space.kscience.visionforge.server.close -import space.kscience.visionforge.server.openInBrowser -import space.kscience.visionforge.server.visionPage import kotlin.math.PI import kotlin.math.sin import kotlin.time.Duration.Companion.milliseconds @@ -38,9 +29,6 @@ public fun main() { plugin(ClockManager) } - val deviceManager = context.request(DeviceManager) - val visionManager = context.request(VisionManager) - val state = DoubleRangeState(0.0, -5.0..5.0) val pidParameters = PidParameters( @@ -50,7 +38,7 @@ public fun main() { timeStep = 0.005.seconds ) - val device = deviceManager.deviceGroup { + val device = context.deviceGroup { val drive = virtualDrive("drive", 0.005, state) val pid = pid("pid", drive, pidParameters) virtualLimitSwitch("start", state.atStartState) @@ -69,59 +57,37 @@ public fun main() { } } - val server = embeddedServer(CIO, port = 7777) { - routing { - staticResources("", null, null) - } - visionPage( - visionManager, - VisionPage.scriptHeader("js/constructor.js") - ) { - vision { - plotly { - plotNumberState(context, state) { - name = "real position" - } - plotDeviceProperty(device["pid"], Regulator.position.name) { - name = "read position" - } - - plotDeviceProperty(device["pid"], Regulator.target.name) { - name = "target" - } - } + context.showDashboard { + plot { + plotNumberState(context, state) { + name = "real position" + } + plotDeviceProperty(device["pid"], Regulator.position.name) { + name = "read position" } - vision { - plotly { + plotDeviceProperty(device["pid"], Regulator.target.name) { + name = "target" + } + } + + plot { // plotBooleanState(context, state.atStartState) { // name = "start" // } // plotBooleanState(context, state.atEndState) { // name = "end" // } - plotDeviceProperty(device["start"], LimitSwitch.locked.name) { - name = "start measured" - mode = ScatterMode.markers - } - plotDeviceProperty(device["end"], LimitSwitch.locked.name) { - name = "end measured" - mode = ScatterMode.markers - } - } + plotDeviceProperty(device["start"], LimitSwitch.locked.name) { + name = "start measured" + mode = ScatterMode.markers + } + plotDeviceProperty(device["end"], LimitSwitch.locked.name) { + name = "end measured" + mode = ScatterMode.markers } } - }.start(false) - - server.openInBrowser() - - - println("Enter 'exit' to close server") - while (readlnOrNull() != "exit") { - // } - - server.close() } \ No newline at end of file From 78b18ebda63d0bba12183d96bed3a575c75062b0 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 5 Nov 2023 10:18:26 +0300 Subject: [PATCH 25/71] Move server to controls-vision --- controls-vision/build.gradle.kts | 4 ++-- .../{space/kscience/controls/vision => }/plotExtensions.kt | 0 .../src/jsMain/kotlin/client.kt | 5 +++++ .../{space/kscience/controls/vision => }/dashboard.kt | 2 +- demo/constructor/build.gradle.kts | 4 +++- demo/constructor/src/jvmMain/kotlin/main.kt | 7 ------- 6 files changed, 11 insertions(+), 11 deletions(-) rename controls-vision/src/commonMain/kotlin/{space/kscience/controls/vision => }/plotExtensions.kt (100%) rename demo/constructor/src/jsMain/kotlin/constructorJs.kt => controls-vision/src/jsMain/kotlin/client.kt (53%) rename controls-vision/src/jvmMain/kotlin/{space/kscience/controls/vision => }/dashboard.kt (96%) diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts index 335bde2..3659986 100644 --- a/controls-vision/build.gradle.kts +++ b/controls-vision/build.gradle.kts @@ -10,8 +10,7 @@ description = """ val visionforgeVersion = "0.3.0-dev-10" kscience { - jvm() - js() + fullStack("js/controls-vision.js", development = true) useKtor() useContextReceivers() dependencies { @@ -19,6 +18,7 @@ kscience { api(projects.controlsConstructor) api("space.kscience:visionforge-plotly:$visionforgeVersion") api("space.kscience:visionforge-markdown:$visionforgeVersion") + api("space.kscience:visionforge-tables:$visionforgeVersion") } jvmMain{ diff --git a/controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt similarity index 100% rename from controls-vision/src/commonMain/kotlin/space/kscience/controls/vision/plotExtensions.kt rename to controls-vision/src/commonMain/kotlin/plotExtensions.kt diff --git a/demo/constructor/src/jsMain/kotlin/constructorJs.kt b/controls-vision/src/jsMain/kotlin/client.kt similarity index 53% rename from demo/constructor/src/jsMain/kotlin/constructorJs.kt rename to controls-vision/src/jsMain/kotlin/client.kt index 27bbc28..a835f9a 100644 --- a/demo/constructor/src/jsMain/kotlin/constructorJs.kt +++ b/controls-vision/src/jsMain/kotlin/client.kt @@ -1,6 +1,11 @@ +package space.kscience.controls.vision + +import space.kscience.visionforge.markup.MarkupPlugin import space.kscience.visionforge.plotly.PlotlyPlugin import space.kscience.visionforge.runVisionClient public fun main(): Unit = runVisionClient { plugin(PlotlyPlugin) + plugin(MarkupPlugin) +// plugin(TableVisionJsPlugin) } \ No newline at end of file diff --git a/controls-vision/src/jvmMain/kotlin/space/kscience/controls/vision/dashboard.kt b/controls-vision/src/jvmMain/kotlin/dashboard.kt similarity index 96% rename from controls-vision/src/jvmMain/kotlin/space/kscience/controls/vision/dashboard.kt rename to controls-vision/src/jvmMain/kotlin/dashboard.kt index 2597e48..29c4740 100644 --- a/controls-vision/src/jvmMain/kotlin/space/kscience/controls/vision/dashboard.kt +++ b/controls-vision/src/jvmMain/kotlin/dashboard.kt @@ -33,7 +33,7 @@ public fun Context.showDashboard( visionPage( visionManager, - VisionPage.scriptHeader("js/constructor.js"), + VisionPage.scriptHeader("js/controls-vision.js"), configurationBuilder = configurationBuilder, visionFragment = visionFragment ) diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index daa4381..dec09b7 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -4,7 +4,9 @@ plugins { } kscience { - fullStack("js/constructor.js", jvmConfig = {withJava()}) + jvm{ + withJava() + } useKtor() useContextReceivers() dependencies { diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 8fc1dac..7a88b7f 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -21,7 +21,6 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit -@Suppress("ExtractKtorModule") public fun main() { val context = Context { plugin(DeviceManager) @@ -73,12 +72,6 @@ public fun main() { } plot { -// plotBooleanState(context, state.atStartState) { -// name = "start" -// } -// plotBooleanState(context, state.atEndState) { -// name = "end" -// } plotDeviceProperty(device["start"], LimitSwitch.locked.name) { name = "start measured" mode = ScatterMode.markers From 0443fdc3c04a8ae02fea6c103f8b243460224219 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 6 Nov 2023 11:39:56 +0300 Subject: [PATCH 26/71] Add fixed age plots for properties and states. --- .../kscience/controls/misc/ValueWithTime.kt | 69 ++++++++++ .../space/kscience/controls/misc/timeIO.kt | 40 ++++++ .../space/kscience/controls/misc/timeMeta.kt | 18 --- .../controls/opcua/client/MetaBsdParser.kt | 2 +- .../src/commonMain/kotlin/plotExtensions.kt | 123 +++++++++++++++--- demo/constructor/src/jvmMain/kotlin/main.kt | 13 +- 6 files changed, 222 insertions(+), 43 deletions(-) create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeIO.kt delete mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeMeta.kt diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt new file mode 100644 index 0000000..ce651e4 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt @@ -0,0 +1,69 @@ +package space.kscience.controls.misc + +import io.ktor.utils.io.core.Input +import io.ktor.utils.io.core.Output +import kotlinx.datetime.Instant +import space.kscience.dataforge.io.IOFormat +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.transformations.MetaConverter +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * A value coupled to a time it was obtained at + */ +public data class ValueWithTime(val value: T, val time: Instant) { + public companion object { + /** + * Create a [ValueWithTime] format for given value value [IOFormat] + */ + public fun ioFormat( + valueFormat: IOFormat, + ): IOFormat> = ValueWithTimeIOFormat(valueFormat) + + /** + * Create a [MetaConverter] with time for given value [MetaConverter] + */ + public fun metaConverter( + valueConverter: MetaConverter, + ): MetaConverter> = ValueWithTimeMetaConverter(valueConverter) + + + public const val META_TIME_KEY: String = "time" + public const val META_VALUE_KEY: String = "value" + } +} + +private class ValueWithTimeIOFormat(val valueFormat: IOFormat) : IOFormat> { + override val type: KType get() = typeOf>() + + override fun readObject(input: Input): ValueWithTime { + val timestamp = InstantIOFormat.readObject(input) + val value = valueFormat.readObject(input) + return ValueWithTime(value, timestamp) + } + + override fun writeObject(output: Output, obj: ValueWithTime) { + InstantIOFormat.writeObject(output, obj.time) + valueFormat.writeObject(output, obj.value) + } + +} + +private class ValueWithTimeMetaConverter( + val valueConverter: MetaConverter, +) : MetaConverter> { + override fun metaToObject( + meta: Meta, + ): ValueWithTime? = valueConverter.metaToObject(meta[ValueWithTime.META_VALUE_KEY] ?: Meta.EMPTY)?.let { + ValueWithTime(it, meta[ValueWithTime.META_TIME_KEY]?.instant ?: Instant.DISTANT_PAST) + } + + override fun objectToMeta(obj: ValueWithTime): Meta = Meta { + ValueWithTime.META_TIME_KEY put obj.time.toMeta() + ValueWithTime.META_VALUE_KEY put valueConverter.objectToMeta(obj.value) + } +} + +public fun MetaConverter.withTime(): MetaConverter> = ValueWithTimeMetaConverter(this) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeIO.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeIO.kt new file mode 100644 index 0000000..aef7401 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeIO.kt @@ -0,0 +1,40 @@ +package space.kscience.controls.misc + +import io.ktor.utils.io.core.* +import kotlinx.datetime.Instant +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.io.IOFormat +import space.kscience.dataforge.io.IOFormatFactory +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.string +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.asName +import kotlin.reflect.KType +import kotlin.reflect.typeOf + + +/** + * An [IOFormat] for [Instant] + */ +public object InstantIOFormat : IOFormat, IOFormatFactory { + override fun build(context: Context, meta: Meta): IOFormat = this + + override val name: Name = "instant".asName() + + override val type: KType get() = typeOf() + + override fun writeObject(output: Output, obj: Instant) { + output.writeLong(obj.epochSeconds) + output.writeInt(obj.nanosecondsOfSecond) + } + + override fun readObject(input: Input): Instant { + val seconds = input.readLong() + val nanoseconds = input.readInt() + return Instant.fromEpochSeconds(seconds, nanoseconds) + } +} + +public fun Instant.toMeta(): Meta = Meta(toString()) + +public val Meta.instant: Instant? get() = value?.string?.let { Instant.parse(it) } \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeMeta.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeMeta.kt deleted file mode 100644 index 11683d9..0000000 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeMeta.kt +++ /dev/null @@ -1,18 +0,0 @@ -package space.kscience.controls.misc - -import kotlinx.datetime.Instant -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.long - -// TODO move to core - -public fun Instant.toMeta(): Meta = Meta { - "seconds" put epochSeconds - "nanos" put nanosecondsOfSecond -} - -public fun Meta.instant(): Instant = value?.long?.let { Instant.fromEpochMilliseconds(it) } ?: Instant.fromEpochSeconds( - get("seconds")?.long ?: 0L, - get("nanos")?.long ?: 0L, -) \ No newline at end of file diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt index 574343e..0442e31 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt @@ -147,7 +147,7 @@ internal class MetaStructureCodec( "Float" -> member.value?.numberOrNull?.toFloat() "Double" -> member.value?.numberOrNull?.toDouble() "String" -> member.string - "DateTime" -> DateTime(member.instant().toJavaInstant()) + "DateTime" -> DateTime(member.instant.toJavaInstant()) "Guid" -> member.string?.let { UUID.fromString(it) } "ByteString" -> member.value?.list?.let { list -> ByteString(list.map { it.number.toByte() }.toByteArray()) diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index 14899d8..ce85483 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -4,18 +4,27 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import space.kscience.controls.api.Device import space.kscience.controls.api.propertyMessageFlow import space.kscience.controls.constructor.DeviceState import space.kscience.controls.manager.clock +import space.kscience.controls.misc.ValueWithTime import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.* import space.kscience.plotly.Plot import space.kscience.plotly.bar import space.kscience.plotly.models.Bar import space.kscience.plotly.models.Scatter +import space.kscience.plotly.models.Trace import space.kscience.plotly.models.TraceValues import space.kscience.plotly.scatter +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours private var TraceValues.values: List get() = value?.list ?: emptyList() @@ -23,48 +32,126 @@ private var TraceValues.values: List value = ListValue(newValues) } + +private var TraceValues.times: List + get() = value?.list?.map { Instant.parse(it.string) } ?: emptyList() + set(newValues) { + value = ListValue(newValues.map { it.toString().asValue() }) + } + + +private class TimeData(private var points: MutableList> = mutableListOf()) { + private val mutex = Mutex() + + suspend fun append(time: Instant, value: Value) = mutex.withLock { + points.add(ValueWithTime(value, time)) + } + + suspend fun trim(maxAge: Duration, maxPoints: Int = 800, minPoints: Int = 400) { + require(maxPoints > 2) + require(minPoints > 0) + require(maxPoints > minPoints) + val now = Clock.System.now() + // filter old points + points.removeAll { now - it.time > maxAge } + + if (points.size > maxPoints) { + val durationBetweenPoints = maxAge / minPoints + val markedForRemoval = buildList> { + var lastTime: Instant? = null + points.forEach { point -> + if (lastTime?.let { point.time - it < durationBetweenPoints } == true) { + add(point) + } else { + lastTime = point.time + } + } + } + points.removeAll(markedForRemoval) + } + } + + suspend fun fillPlot(x: TraceValues, y: TraceValues) = mutex.withLock { + x.strings = points.map { it.time.toString() } + y.values = points.map { it.value } + } +} + /** - * Add a trace that shows a [Device] property change over time. Show only latest [pointsNumber] . + * Add a trace that shows a [Device] property change over time. Show only latest [maxPoints] . * @return a [Job] that handles the listener */ public fun Plot.plotDeviceProperty( device: Device, propertyName: String, extractValue: Meta.() -> Value = { value ?: Null }, - pointsNumber: Int = 400, + maxAge: Duration = 1.hours, + maxPoints: Int = 800, + minPoints: Int = 400, coroutineScope: CoroutineScope = device.context, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { val clock = device.context.clock - device.propertyMessageFlow(propertyName).onEach { message -> - x.strings = (x.strings + (message.time ?: clock.now()).toString()).takeLast(pointsNumber) - y.values = (y.values + message.value.extractValue()).takeLast(pointsNumber) + val data = TimeData() + device.propertyMessageFlow(propertyName).transform { + data.append(it.time ?: clock.now(), it.value.extractValue()) + data.trim(maxAge, maxPoints, minPoints) + emit(data) + }.onEach { + it.fillPlot(x, y) }.launchIn(coroutineScope) } +private fun Trace.updateFromState( + context: Context, + state: DeviceState, + extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: space.kscience.dataforge.meta.Null }, + maxAge: Duration = 1.hours, + maxPoints: Int = 800, + minPoints: Int = 400, +): Job{ + val clock = context.clock + val data = TimeData() + return state.valueFlow.transform { + data.append(clock.now(), it.extractValue()) + data.trim(maxAge, maxPoints, minPoints) + }.onEach { + it.fillPlot(x, y) + }.launchIn(context) +} + +public fun Plot.plotDeviceState( + context: Context, + state: DeviceState, + extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: Null }, + maxAge: Duration = 1.hours, + maxPoints: Int = 800, + minPoints: Int = 400, + configuration: Scatter.() -> Unit = {}, +): Job = scatter(configuration).run { + updateFromState(context, state, extractValue, maxAge, maxPoints, minPoints) +} + public fun Plot.plotNumberState( context: Context, state: DeviceState, - pointsNumber: Int = 400, + maxAge: Duration = 1.hours, + maxPoints: Int = 800, + minPoints: Int = 400, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { - val clock = context.clock - state.valueFlow.onEach { - x.strings = (x.strings + clock.now().toString()).takeLast(pointsNumber) - y.numbers = (y.numbers + it).takeLast(pointsNumber) - }.launchIn(context) + updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints) } + public fun Plot.plotBooleanState( context: Context, state: DeviceState, - pointsNumber: Int = 400, + maxAge: Duration = 1.hours, + maxPoints: Int = 800, + minPoints: Int = 400, configuration: Bar.() -> Unit = {}, -): Job = bar(configuration).run { - val clock = context.clock - state.valueFlow.onEach { - x.strings = (x.strings + clock.now().toString()).takeLast(pointsNumber) - y.values = (y.values + it.asValue()).takeLast(pointsNumber) - }.launchIn(context) +): Job = bar(configuration).run { + updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints) } \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 7a88b7f..079499d 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -51,32 +51,33 @@ public fun main() { val t = timeFromStart.toDouble(DurationUnit.SECONDS) val freq = 0.1 val target = 5 * sin(2.0 * PI * freq * t) + - sin(2 * PI * 21 * freq * t + 0.1 * (timeFromStart / pidParameters.timeStep)) + sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep)) pid.write(Regulator.target, target) } } + val maxAge = 10.seconds context.showDashboard { plot { - plotNumberState(context, state) { + plotNumberState(context, state, maxAge = maxAge) { name = "real position" } - plotDeviceProperty(device["pid"], Regulator.position.name) { + plotDeviceProperty(device["pid"], Regulator.position.name, maxAge = maxAge) { name = "read position" } - plotDeviceProperty(device["pid"], Regulator.target.name) { + plotDeviceProperty(device["pid"], Regulator.target.name, maxAge = maxAge) { name = "target" } } plot { - plotDeviceProperty(device["start"], LimitSwitch.locked.name) { + plotDeviceProperty(device["start"], LimitSwitch.locked.name, maxAge = maxAge) { name = "start measured" mode = ScatterMode.markers } - plotDeviceProperty(device["end"], LimitSwitch.locked.name) { + plotDeviceProperty(device["end"], LimitSwitch.locked.name, maxAge = maxAge) { name = "end measured" mode = ScatterMode.markers } From 825f1a4d049a0d6b4198a500137a58e41c95dc2c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 6 Nov 2023 16:46:16 +0300 Subject: [PATCH 27/71] Add DeviceConstructor --- .../controls/constructor/DeviceConstructor.kt | 126 ++++++++++++++++++ .../controls/constructor/DeviceGroup.kt | 124 ++++++++++------- .../controls/constructor/DeviceState.kt | 62 ++++++++- .../kscience/controls/constructor/Drive.kt | 16 ++- .../controls/constructor/LimitSwitch.kt | 10 +- .../controls/constructor/PidRegulator.kt | 2 +- .../controls/constructor/customState.kt | 8 +- .../controls/opcua/client/MetaBsdParser.kt | 2 +- controls-vision/build.gradle.kts | 4 +- demo/constructor/src/jvmMain/kotlin/main.kt | 8 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 11 files changed, 297 insertions(+), 67 deletions(-) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt new file mode 100644 index 0000000..54ebc4e --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt @@ -0,0 +1,126 @@ +package space.kscience.controls.constructor + +import space.kscience.controls.api.Device +import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.manager.DeviceManager +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.asName +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadOnlyProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty +import kotlin.time.Duration + +/** + * A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices + */ +public abstract class DeviceConstructor( + deviceManager: DeviceManager, + meta: Meta, +) : DeviceGroup(deviceManager, meta) { + + /** + * Register a device, provided by a given [factory] and + */ + public fun device( + factory: Factory, + meta: Meta? = null, + nameOverride: Name? = null, + metaLocation: Name? = null, + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> + val name = nameOverride ?: property.name.asName() + val device = registerDevice(name, factory, meta, metaLocation ?: name) + ReadOnlyProperty { _: DeviceConstructor, _ -> + device + } + } + + public fun device( + device: D, + nameOverride: Name? = null, + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> + val name = nameOverride ?: property.name.asName() + registerDevice(name, device) + ReadOnlyProperty { _: DeviceConstructor, _ -> + device + } + } + + + /** + * Register a property and provide a direct reader for it + */ + public fun property( + state: DeviceState, + nameOverride: String? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit, + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _: DeviceConstructor, property -> + val name = nameOverride ?: property.name + val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) + registerProperty(descriptor, state) + ReadOnlyProperty { _: DeviceConstructor, _ -> + state.value + } + } + + /** + * Register external state as a property + */ + public fun property( + metaConverter: MetaConverter, + reader: suspend () -> T, + readInterval: Duration, + initialState: T, + nameOverride: String? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit, + ): PropertyDelegateProvider> = property( + DeviceState.external(this, metaConverter, readInterval, initialState, reader), + nameOverride, descriptorBuilder + ) + + + /** + * Register a mutable property and provide a direct reader for it + */ + public fun mutableProperty( + state: MutableDeviceState, + nameOverride: String? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit, + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _: DeviceConstructor, property -> + val name = nameOverride ?: property.name + val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) + registerProperty(descriptor, state) + object : ReadWriteProperty { + override fun getValue(thisRef: DeviceConstructor, property: KProperty<*>): T = state.value + + override fun setValue(thisRef: DeviceConstructor, property: KProperty<*>, value: T) { + state.value = value + } + + } + } + + /** + * Register external state as a property + */ + public fun mutableProperty( + metaConverter: MetaConverter, + reader: suspend () -> T, + writer: suspend (T) -> Unit, + readInterval: Duration, + initialState: T, + nameOverride: String? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit, + ): PropertyDelegateProvider> = mutableProperty( + DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), + nameOverride, + descriptorBuilder + ) +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt index b39d7b6..936dd04 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import space.kscience.controls.api.* +import space.kscience.controls.api.DeviceLifecycleState.* import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install import space.kscience.dataforge.context.Context @@ -23,7 +24,7 @@ import kotlin.coroutines.CoroutineContext /** * A mutable group of devices and properties to be used for lightweight design and simulations. */ -public class DeviceGroup( +public open class DeviceGroup( public val deviceManager: DeviceManager, override val meta: Meta, ) : DeviceHub, CachingDevice { @@ -63,15 +64,28 @@ public class DeviceGroup( override val devices: Map = _devices - public fun device(token: NameToken, device: D): D { - check(_devices[token] == null) { "A child device with name $token already exists" } + /** + * Register and initialize (synchronize child's lifecycle state with group state) a new device in this group + */ + @OptIn(DFExperimental::class) + public fun registerDevice(token: NameToken, device: D): D { + require(_devices[token] == null) { "A child device with name $token already exists" } + //start or stop the child if needed + when (lifecycleState) { + STARTING, STARTED -> launch { device.start() } + STOPPED -> device.stop() + ERROR -> {} + } _devices[token] = device return device } private val properties: MutableMap = hashMapOf() - public fun property(descriptor: PropertyDescriptor, state: DeviceState) { + /** + * Register a new property based on [DeviceState]. Properties could be modified dynamically + */ + public fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState) { val name = descriptor.name.parseAsName() require(properties[name] == null) { "Can't add property with name $name. It already exists." } properties[name] = Property(state, descriptor) @@ -112,8 +126,8 @@ public class DeviceGroup( } @DFExperimental - override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED - private set(value) { + override var lifecycleState: DeviceLifecycleState = STOPPED + protected set(value) { if (field != value) { launch { sharedMessageFlow.emit( @@ -127,12 +141,12 @@ public class DeviceGroup( @OptIn(DFExperimental::class) override suspend fun start() { - lifecycleState = DeviceLifecycleState.STARTING + lifecycleState = STARTING super.start() devices.values.forEach { it.start() } - lifecycleState = DeviceLifecycleState.STARTED + lifecycleState = STARTED } @OptIn(DFExperimental::class) @@ -141,7 +155,7 @@ public class DeviceGroup( it.stop() } super.stop() - lifecycleState = DeviceLifecycleState.STOPPED + lifecycleState = STOPPED } public companion object { @@ -149,7 +163,7 @@ public class DeviceGroup( } } -public fun DeviceManager.deviceGroup( +public fun DeviceManager.registerDeviceGroup( name: String = "@group", meta: Meta = Meta.EMPTY, block: DeviceGroup.() -> Unit, @@ -159,11 +173,11 @@ public fun DeviceManager.deviceGroup( return group } -public fun Context.deviceGroup( +public fun Context.registerDeviceGroup( name: String = "@group", meta: Meta = Meta.EMPTY, block: DeviceGroup.() -> Unit, -): DeviceGroup = request(DeviceManager).deviceGroup(name, meta, block) +): DeviceGroup = request(DeviceManager).registerDeviceGroup(name, meta, block) private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { return when (name.length) { @@ -171,7 +185,7 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { 1 -> { val token = name.first() when (val d = devices[token]) { - null -> device( + null -> registerDevice( token, DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY) ) @@ -187,79 +201,99 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { /** * Register a device at given [name] path */ -public fun DeviceGroup.device(name: Name, device: D): D { +public fun DeviceGroup.registerDevice(name: Name, device: D): D { return when (name.length) { 0 -> error("Can't use empty name for a child device") - 1 -> device(name.first(), device) - else -> getOrCreateGroup(name.cutLast()).device(name.tokens.last(), device) + 1 -> registerDevice(name.first(), device) + else -> getOrCreateGroup(name.cutLast()).registerDevice(name.tokens.last(), device) } } -public fun DeviceGroup.device(name: String, device: D): D = device(name.parseAsName(), device) +public fun DeviceGroup.registerDevice(name: String, device: D): D = registerDevice(name.parseAsName(), device) /** * Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error. + * @param name the name of the device in the group + * @param factory a factory used to create a device + * @param deviceMeta meta override for this specific device + * @param metaLocation location of the template meta in parent group meta */ -public fun DeviceGroup.device(name: Name, factory: Factory, deviceMeta: Meta? = null): Device { - val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[name])) - device(name, newDevice) +public fun DeviceGroup.registerDevice( + name: Name, + factory: Factory, + deviceMeta: Meta? = null, + metaLocation: Name = name, +): D { + val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[metaLocation])) + registerDevice(name, newDevice) return newDevice } -public fun DeviceGroup.device( +public fun DeviceGroup.registerDevice( name: String, - factory: Factory, + factory: Factory, + metaLocation: Name = name.parseAsName(), metaBuilder: (MutableMeta.() -> Unit)? = null, -): Device = device(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }) +): D = registerDevice(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }, metaLocation) /** * Create or edit a group with a given [name]. */ -public fun DeviceGroup.deviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup = +public fun DeviceGroup.registerDeviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup = getOrCreateGroup(name).apply(block) -public fun DeviceGroup.deviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup = - deviceGroup(name.parseAsName(), block) +public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup = + registerDeviceGroup(name.parseAsName(), block) -public fun DeviceGroup.property( +/** + * Register read-only property based on [state] + */ +public fun DeviceGroup.registerProperty( name: String, state: DeviceState, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): DeviceState { - property( +) { + registerProperty( PropertyDescriptor(name).apply(descriptorBuilder), state ) - return state } -public fun DeviceGroup.mutableProperty( +/** + * Register a mutable property based on mutable [state] + */ +public fun DeviceGroup.registerMutableProperty( name: String, state: MutableDeviceState, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): MutableDeviceState { - property( +) { + registerProperty( PropertyDescriptor(name).apply(descriptorBuilder), state ) - return state } -public fun DeviceGroup.virtualProperty( - name: String, - initialValue: T, - converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): MutableDeviceState { - val state = VirtualDeviceState(converter, initialValue) - return mutableProperty(name, state, descriptorBuilder) -} /** * Create a virtual [MutableDeviceState], but do not register it to a device */ @Suppress("UnusedReceiverParameter") -public fun DeviceGroup.standAloneProperty( +public fun DeviceGroup.state( + converter: MetaConverter, + initialValue: T, +): MutableDeviceState = VirtualDeviceState(converter, initialValue) + +/** + * Create a new virtual mutable state and a property based on it. + * @return the mutable state used in property + */ +public fun DeviceGroup.registerVirtualProperty( + name: String, initialValue: T, converter: MetaConverter, -): MutableDeviceState = VirtualDeviceState(converter, initialValue) \ No newline at end of file + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): MutableDeviceState { + val state = state(converter, initialValue) + registerMutableProperty(name, state, descriptorBuilder) + return state +} diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index 783752a..6e841f8 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -1,5 +1,7 @@ package space.kscience.controls.constructor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import space.kscience.controls.api.Device @@ -9,6 +11,7 @@ import space.kscience.controls.spec.MutableDevicePropertySpec import space.kscience.controls.spec.name import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.transformations.MetaConverter +import kotlin.time.Duration /** * An observable state of a device @@ -18,6 +21,8 @@ public interface DeviceState { public val value: T public val valueFlow: Flow + + public companion object } public val DeviceState.metaFlow: Flow get() = valueFlow.map(converter::objectToMeta) @@ -55,7 +60,7 @@ private open class BoundDeviceState( override val converter: MetaConverter, val device: Device, val propertyName: String, - private val initialValue: T, + initialValue: T, ) : DeviceState { override val valueFlow: StateFlow = device.messageFlow.filterIsInstance().filter { @@ -121,3 +126,58 @@ public suspend fun D.bindMutableStateToProperty( ): MutableDeviceState = bindMutableStateToProperty(propertySpec.name, propertySpec.converter) +private open class ExternalState( + val scope: CoroutineScope, + override val converter: MetaConverter, + val readInterval: Duration, + initialValue: T, + val reader: suspend () -> T, +) : DeviceState { + + protected val flow: StateFlow = flow { + while (true) { + delay(readInterval) + emit(reader()) + } + }.stateIn(scope, SharingStarted.Eagerly, initialValue) + + override val value: T get() = flow.value + override val valueFlow: Flow get() = flow +} + +/** + * Create a [DeviceState] which is constructed by periodically reading external value + */ +public fun DeviceState.Companion.external( + scope: CoroutineScope, + converter: MetaConverter, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, +): DeviceState = ExternalState(scope, converter, readInterval, initialValue, reader) + +private class MutableExternalState( + scope: CoroutineScope, + converter: MetaConverter, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, + val writer: suspend (T) -> Unit, +) : ExternalState(scope, converter, readInterval, initialValue, reader), MutableDeviceState { + override var value: T + get() = super.value + set(value) { + scope.launch { + writer(value) + } + } +} + +public fun DeviceState.Companion.external( + scope: CoroutineScope, + converter: MetaConverter, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, + writer: suspend (T) -> Unit, +): MutableDeviceState = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt index e7c3d39..d8802eb 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt @@ -8,6 +8,7 @@ import space.kscience.controls.api.Device import space.kscience.controls.manager.clock import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.transformations.MetaConverter @@ -84,12 +85,15 @@ public class VirtualDrive( override fun onStop() { updateJob?.cancel() } + + public companion object { + public fun factory( + mass: Double, + positionState: MutableDeviceState, + ): Factory = Factory { context, _ -> + VirtualDrive(context, mass, positionState) + } + } } public suspend fun Drive.stateOfForce(): MutableDeviceState = bindMutableStateToProperty(Drive.force) - -public fun DeviceGroup.virtualDrive( - name: String, - mass: Double, - positionState: MutableDeviceState, -): VirtualDrive = device(name, VirtualDrive(context, mass, positionState)) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt index 3db6512..977930d 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt @@ -8,7 +8,7 @@ import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.booleanProperty import space.kscience.dataforge.context.Context -import space.kscience.dataforge.names.parseAsName +import space.kscience.dataforge.context.Factory /** @@ -20,6 +20,9 @@ public interface LimitSwitch : Device { public companion object : DeviceSpec() { public val locked: DevicePropertySpec by booleanProperty { locked } + public fun factory(lockedState: DeviceState): Factory = Factory { context, _ -> + VirtualLimitSwitch(context, lockedState) + } } } @@ -38,7 +41,4 @@ public class VirtualLimitSwitch( } override val locked: Boolean get() = lockedState.value -} - -public fun DeviceGroup.virtualLimitSwitch(name: String, lockedState: DeviceState): VirtualLimitSwitch = - device(name.parseAsName(), VirtualLimitSwitch(context, lockedState)) \ No newline at end of file +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt index 9268d02..e6f9df1 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt @@ -79,4 +79,4 @@ public fun DeviceGroup.pid( name: String, drive: Drive, pidParameters: PidParameters, -): PidRegulator = device(name, PidRegulator(drive, pidParameters)) \ No newline at end of file +): PidRegulator = registerDevice(name, PidRegulator(drive, pidParameters)) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt index e745875..6e95ccd 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt @@ -38,4 +38,10 @@ public class DoubleRangeState( * A state showing that the range is on its higher boundary */ public val atEndState: DeviceState = map(MetaConverter.boolean) { it >= range.endInclusive } -} \ No newline at end of file +} + +@Suppress("UnusedReceiverParameter") +public fun DeviceGroup.rangeState( + initialValue: Double, + range: ClosedFloatingPointRange, +): DoubleRangeState = DoubleRangeState(initialValue, range) \ No newline at end of file diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt index 0442e31..b760184 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt @@ -147,7 +147,7 @@ internal class MetaStructureCodec( "Float" -> member.value?.numberOrNull?.toFloat() "Double" -> member.value?.numberOrNull?.toDouble() "String" -> member.string - "DateTime" -> DateTime(member.instant.toJavaInstant()) + "DateTime" -> member.instant?.toJavaInstant()?.let { DateTime(it) } "Guid" -> member.string?.let { UUID.fromString(it) } "ByteString" -> member.value?.list?.let { list -> ByteString(list.map { it.number.toByte() }.toByteArray()) diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts index 3659986..a69329a 100644 --- a/controls-vision/build.gradle.kts +++ b/controls-vision/build.gradle.kts @@ -10,7 +10,7 @@ description = """ val visionforgeVersion = "0.3.0-dev-10" kscience { - fullStack("js/controls-vision.js", development = true) + fullStack("js/controls-vision.js") useKtor() useContextReceivers() dependencies { @@ -29,4 +29,4 @@ kscience { readme { maturity = space.kscience.gradle.Maturity.PROTOTYPE -} +} \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 079499d..59881e5 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -37,11 +37,11 @@ public fun main() { timeStep = 0.005.seconds ) - val device = context.deviceGroup { - val drive = virtualDrive("drive", 0.005, state) + val device = context.registerDeviceGroup { + val drive = VirtualDrive(context, 0.005, state) val pid = pid("pid", drive, pidParameters) - virtualLimitSwitch("start", state.atStartState) - virtualLimitSwitch("end", state.atEndState) + registerDevice("start", LimitSwitch.factory(state.atStartState)) + registerDevice("end", LimitSwitch.factory(state.atEndState)) val clock = context.clock val clockStart = clock.now() diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index db9a6b8..e411586 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.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 53fc240c759daa9791463af03a7422d907c4e19b Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 7 Nov 2023 08:46:56 +0300 Subject: [PATCH 28/71] Test device constructor --- .../controls/constructor/DeviceConstructor.kt | 16 ++-- .../controls/constructor/DeviceGroup.kt | 82 +++++++++++-------- .../controls/constructor/DeviceState.kt | 25 ++++-- .../kscience/controls/constructor/Drive.kt | 2 +- .../controls/constructor/PidRegulator.kt | 2 +- .../kscience/controls/spec/DeviceBase.kt | 28 +++---- demo/constructor/build.gradle.kts | 6 +- demo/constructor/src/jvmMain/kotlin/main.kt | 46 +++++++---- 8 files changed, 123 insertions(+), 84 deletions(-) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt index 54ebc4e..01d9087 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt @@ -33,7 +33,7 @@ public abstract class DeviceConstructor( ): PropertyDelegateProvider> = PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> val name = nameOverride ?: property.name.asName() - val device = registerDevice(name, factory, meta, metaLocation ?: name) + val device = install(name, factory, meta, metaLocation ?: name) ReadOnlyProperty { _: DeviceConstructor, _ -> device } @@ -45,7 +45,7 @@ public abstract class DeviceConstructor( ): PropertyDelegateProvider> = PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> val name = nameOverride ?: property.name.asName() - registerDevice(name, device) + install(name, device) ReadOnlyProperty { _: DeviceConstructor, _ -> device } @@ -58,7 +58,7 @@ public abstract class DeviceConstructor( public fun property( state: DeviceState, nameOverride: String? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ): PropertyDelegateProvider> = PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name @@ -78,7 +78,7 @@ public abstract class DeviceConstructor( readInterval: Duration, initialState: T, nameOverride: String? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ): PropertyDelegateProvider> = property( DeviceState.external(this, metaConverter, readInterval, initialState, reader), nameOverride, descriptorBuilder @@ -91,8 +91,8 @@ public abstract class DeviceConstructor( public fun mutableProperty( state: MutableDeviceState, nameOverride: String? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit, - ): PropertyDelegateProvider> = + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + ): PropertyDelegateProvider> = PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) @@ -117,8 +117,8 @@ public abstract class DeviceConstructor( readInterval: Duration, initialState: T, nameOverride: String? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit, - ): PropertyDelegateProvider> = mutableProperty( + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + ): PropertyDelegateProvider> = mutableProperty( DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), nameOverride, descriptorBuilder diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt index 936dd04..68f8301 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -3,6 +3,8 @@ package space.kscience.controls.constructor import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import space.kscience.controls.api.* import space.kscience.controls.api.DeviceLifecycleState.* import space.kscience.controls.manager.DeviceManager @@ -40,25 +42,30 @@ public open class DeviceGroup( ) - override val context: Context get() = deviceManager.context + override final val context: Context get() = deviceManager.context - override val coroutineContext: CoroutineContext by lazy { - context.newCoroutineContext( - SupervisorJob(context.coroutineContext[Job]) + - CoroutineName("Device $this") + - CoroutineExceptionHandler { _, throwable -> - launch { - sharedMessageFlow.emit( - DeviceErrorMessage( - errorMessage = throwable.message, - errorType = throwable::class.simpleName, - errorStackTrace = throwable.stackTraceToString() - ) + + private val sharedMessageFlow = MutableSharedFlow() + + override val messageFlow: Flow + get() = sharedMessageFlow + + override val coroutineContext: CoroutineContext = context.newCoroutineContext( + SupervisorJob(context.coroutineContext[Job]) + + CoroutineName("Device $this") + + CoroutineExceptionHandler { _, throwable -> + context.launch { + sharedMessageFlow.emit( + DeviceErrorMessage( + errorMessage = throwable.message, + errorType = throwable::class.simpleName, + errorStackTrace = throwable.stackTraceToString() ) - } + ) } - ) - } + } + ) + private val _devices = hashMapOf() @@ -68,14 +75,10 @@ public open class DeviceGroup( * Register and initialize (synchronize child's lifecycle state with group state) a new device in this group */ @OptIn(DFExperimental::class) - public fun registerDevice(token: NameToken, device: D): D { + public fun install(token: NameToken, device: D): D { require(_devices[token] == null) { "A child device with name $token already exists" } - //start or stop the child if needed - when (lifecycleState) { - STARTING, STARTED -> launch { device.start() } - STOPPED -> device.stop() - ERROR -> {} - } + //start the child device if needed + if(lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() } _devices[token] = device return device } @@ -89,6 +92,14 @@ public open class DeviceGroup( val name = descriptor.name.parseAsName() require(properties[name] == null) { "Can't add property with name $name. It already exists." } properties[name] = Property(state, descriptor) + state.metaFlow.onEach { + sharedMessageFlow.emit( + PropertyChangedMessage( + descriptor.name, + it + ) + ) + }.launchIn(this) } private val actions: MutableMap = hashMapOf() @@ -115,10 +126,6 @@ public open class DeviceGroup( property.valueAsMeta = value } - private val sharedMessageFlow = MutableSharedFlow() - - override val messageFlow: Flow - get() = sharedMessageFlow override suspend fun execute(actionName: String, argument: Meta?): Meta? { val action = actions[actionName] ?: error("Action with name $actionName not found") @@ -185,7 +192,7 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { 1 -> { val token = name.first() when (val d = devices[token]) { - null -> registerDevice( + null -> install( token, DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY) ) @@ -201,15 +208,18 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { /** * Register a device at given [name] path */ -public fun DeviceGroup.registerDevice(name: Name, device: D): D { +public fun DeviceGroup.install(name: Name, device: D): D { return when (name.length) { 0 -> error("Can't use empty name for a child device") - 1 -> registerDevice(name.first(), device) - else -> getOrCreateGroup(name.cutLast()).registerDevice(name.tokens.last(), device) + 1 -> install(name.first(), device) + else -> getOrCreateGroup(name.cutLast()).install(name.tokens.last(), device) } } -public fun DeviceGroup.registerDevice(name: String, device: D): D = registerDevice(name.parseAsName(), device) +public fun DeviceGroup.install(name: String, device: D): D = + install(name.parseAsName(), device) + +public fun Context.install(name: String, device: D): D = request(DeviceManager).install(name, device) /** * Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error. @@ -218,23 +228,23 @@ public fun DeviceGroup.registerDevice(name: String, device: D): D = * @param deviceMeta meta override for this specific device * @param metaLocation location of the template meta in parent group meta */ -public fun DeviceGroup.registerDevice( +public fun DeviceGroup.install( name: Name, factory: Factory, deviceMeta: Meta? = null, metaLocation: Name = name, ): D { val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[metaLocation])) - registerDevice(name, newDevice) + install(name, newDevice) return newDevice } -public fun DeviceGroup.registerDevice( +public fun DeviceGroup.install( name: String, factory: Factory, metaLocation: Name = name.parseAsName(), metaBuilder: (MutableMeta.() -> Unit)? = null, -): D = registerDevice(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }, metaLocation) +): D = install(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }, metaLocation) /** * Create or edit a group with a given [name]. diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index 6e841f8..ed7027a 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -75,7 +75,7 @@ private open class BoundDeviceState( /** * Bind a read-only [DeviceState] to a [Device] property */ -public suspend fun Device.bindStateToProperty( +public suspend fun Device.propertyAsState( propertyName: String, metaConverter: MetaConverter, ): DeviceState { @@ -83,9 +83,9 @@ public suspend fun Device.bindStateToProperty( return BoundDeviceState(metaConverter, this, propertyName, initialValue) } -public suspend fun D.bindStateToProperty( +public suspend fun D.propertyAsState( propertySpec: DevicePropertySpec, -): DeviceState = bindStateToProperty(propertySpec.name, propertySpec.converter) +): DeviceState = propertyAsState(propertySpec.name, propertySpec.converter) public fun DeviceState.map( converter: MetaConverter, mapper: (T) -> R, @@ -113,17 +113,28 @@ private class MutableBoundDeviceState( } } -public suspend fun Device.bindMutableStateToProperty( +public fun Device.mutablePropertyAsState( + propertyName: String, + metaConverter: MetaConverter, + initialValue: T, +): MutableDeviceState = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) + +public suspend fun Device.mutablePropertyAsState( propertyName: String, metaConverter: MetaConverter, ): MutableDeviceState { val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") - return MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) + return mutablePropertyAsState(propertyName, metaConverter, initialValue) } -public suspend fun D.bindMutableStateToProperty( +public suspend fun D.mutablePropertyAsState( propertySpec: MutableDevicePropertySpec, -): MutableDeviceState = bindMutableStateToProperty(propertySpec.name, propertySpec.converter) +): MutableDeviceState = mutablePropertyAsState(propertySpec.name, propertySpec.converter) + +public fun D.mutablePropertyAsState( + propertySpec: MutableDevicePropertySpec, + initialValue: T, +): MutableDeviceState = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue) private open class ExternalState( diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt index d8802eb..9366971 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt @@ -96,4 +96,4 @@ public class VirtualDrive( } } -public suspend fun Drive.stateOfForce(): MutableDeviceState = bindMutableStateToProperty(Drive.force) +public suspend fun Drive.stateOfForce(): MutableDeviceState = mutablePropertyAsState(Drive.force) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt index e6f9df1..9fc6e18 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt @@ -79,4 +79,4 @@ public fun DeviceGroup.pid( name: String, drive: Drive, pidParameters: PidParameters, -): PidRegulator = registerDevice(name, PidRegulator(drive, pidParameters)) \ No newline at end of file +): PidRegulator = install(name, PidRegulator(drive, pidParameters)) \ No newline at end of file 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 43574cf..846815c 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 @@ -72,23 +72,21 @@ public abstract class DeviceBase( onBufferOverflow = BufferOverflow.DROP_OLDEST ) - override val coroutineContext: CoroutineContext by lazy { - context.newCoroutineContext( - SupervisorJob(context.coroutineContext[Job]) + - CoroutineName("Device $this") + - CoroutineExceptionHandler { _, throwable -> - launch { - sharedMessageFlow.emit( - DeviceErrorMessage( - errorMessage = throwable.message, - errorType = throwable::class.simpleName, - errorStackTrace = throwable.stackTraceToString() - ) + override val coroutineContext: CoroutineContext = context.newCoroutineContext( + SupervisorJob(context.coroutineContext[Job]) + + CoroutineName("Device $this") + + CoroutineExceptionHandler { _, throwable -> + launch { + sharedMessageFlow.emit( + DeviceErrorMessage( + errorMessage = throwable.message, + errorType = throwable::class.simpleName, + errorStackTrace = throwable.stackTraceToString() ) - } + ) } - ) - } + } + ) /** diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index dec09b7..06cb285 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + plugins { id("space.kscience.gradle.mpp") application @@ -20,4 +22,6 @@ kscience { application { mainClass.set("space.kscience.controls.demo.constructor.MainKt") -} \ No newline at end of file +} + +kotlin.explicitApi = ExplicitApiMode.Disabled \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 59881e5..385ccfc 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -1,18 +1,18 @@ package space.kscience.controls.demo.constructor -import space.kscience.controls.api.get import space.kscience.controls.constructor.* import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.clock import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.name -import space.kscience.controls.spec.write import space.kscience.controls.vision.plot import space.kscience.controls.vision.plotDeviceProperty import space.kscience.controls.vision.plotNumberState import space.kscience.controls.vision.showDashboard import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request +import space.kscience.dataforge.meta.Meta import space.kscience.plotly.models.ScatterMode import space.kscience.visionforge.plotly.PlotlyPlugin import kotlin.math.PI @@ -21,6 +21,27 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit + +class LinearDrive( + context: Context, + state: DoubleRangeState, + mass: Double, + pidParameters: PidParameters, + meta: Meta = Meta.EMPTY, +) : DeviceConstructor(context.request(DeviceManager), meta) { + + val drive by device(VirtualDrive.factory(mass, state)) + val pid by device(PidRegulator(drive, pidParameters)) + + val start by device(LimitSwitch.factory(state.atStartState)) + val end by device(LimitSwitch.factory(state.atEndState)) + + + val position by property(state) + var target by mutableProperty(pid.mutablePropertyAsState(Regulator.target, 0.0)) +} + + public fun main() { val context = Context { plugin(DeviceManager) @@ -37,25 +58,20 @@ public fun main() { timeStep = 0.005.seconds ) - val device = context.registerDeviceGroup { - val drive = VirtualDrive(context, 0.005, state) - val pid = pid("pid", drive, pidParameters) - registerDevice("start", LimitSwitch.factory(state.atStartState)) - registerDevice("end", LimitSwitch.factory(state.atEndState)) - + val device = context.install("device", LinearDrive(context, state, 0.005, pidParameters)).apply { val clock = context.clock val clockStart = clock.now() - doRecurring(10.milliseconds) { val timeFromStart = clock.now() - clockStart val t = timeFromStart.toDouble(DurationUnit.SECONDS) val freq = 0.1 - val target = 5 * sin(2.0 * PI * freq * t) + + + target = 5 * sin(2.0 * PI * freq * t) + sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep)) - pid.write(Regulator.target, target) } } + val maxAge = 10.seconds context.showDashboard { @@ -63,21 +79,21 @@ public fun main() { plotNumberState(context, state, maxAge = maxAge) { name = "real position" } - plotDeviceProperty(device["pid"], Regulator.position.name, maxAge = maxAge) { + plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge) { name = "read position" } - plotDeviceProperty(device["pid"], Regulator.target.name, maxAge = maxAge) { + plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge) { name = "target" } } plot { - plotDeviceProperty(device["start"], LimitSwitch.locked.name, maxAge = maxAge) { + plotDeviceProperty(device.start, LimitSwitch.locked.name, maxAge = maxAge) { name = "start measured" mode = ScatterMode.markers } - plotDeviceProperty(device["end"], LimitSwitch.locked.name, maxAge = maxAge) { + plotDeviceProperty(device.end, LimitSwitch.locked.name, maxAge = maxAge) { name = "end measured" mode = ScatterMode.markers } From 0f687c3c51ab48375c279cf7e7c431dc08a8214c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 8 Nov 2023 11:52:57 +0300 Subject: [PATCH 29/71] Update jupyter integration --- build.gradle.kts | 2 +- controls-jupyter/build.gradle.kts | 19 ++ .../src/jsMain/kotlin/commonJupyter.kt | 14 ++ .../src/jvmMain/kotlin/ControlsJupyter.kt | 71 ++++++++ controls-server/build.gradle.kts | 26 +-- .../controls/server/deviceWebServer.kt | 0 .../kscience/controls/server/responses.kt | 0 controls-vision/build.gradle.kts | 2 +- demo/constructor/src/jvmMain/kotlin/main.kt | 2 +- demo/notebooks/constructor.ipynb | 162 ++++++++++++++++++ gradle.properties | 5 +- settings.gradle.kts | 1 + 12 files changed, 286 insertions(+), 18 deletions(-) create mode 100644 controls-jupyter/build.gradle.kts create mode 100644 controls-jupyter/src/jsMain/kotlin/commonJupyter.kt create mode 100644 controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt rename controls-server/src/{main => jvmMain}/kotlin/space/kscience/controls/server/deviceWebServer.kt (100%) rename controls-server/src/{main => jvmMain}/kotlin/space/kscience/controls/server/responses.kt (100%) create mode 100644 demo/notebooks/constructor.ipynb diff --git a/build.gradle.kts b/build.gradle.kts index 4ca88d6..a135960 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } val dataforgeVersion: String by extra("0.6.2") -val visionforgeVersion by extra("0.3.0-dev-10") +val visionforgeVersion by extra("0.3.0-dev-14") 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-jupyter/build.gradle.kts b/controls-jupyter/build.gradle.kts new file mode 100644 index 0000000..3191720 --- /dev/null +++ b/controls-jupyter/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("space.kscience.gradle.mpp") +} + +val visionforgeVersion: String by rootProject.extra + +kscience { + fullStack("js/controls-jupyter.js") + useKtor() + useContextReceivers() + jupyterLibrary() + dependencies { + implementation(projects.controlsVision) + implementation("space.kscience:visionforge-jupyter:$visionforgeVersion") + } + jvmMain { + implementation(spclibs.logback.classic) + } +} \ No newline at end of file diff --git a/controls-jupyter/src/jsMain/kotlin/commonJupyter.kt b/controls-jupyter/src/jsMain/kotlin/commonJupyter.kt new file mode 100644 index 0000000..41711d7 --- /dev/null +++ b/controls-jupyter/src/jsMain/kotlin/commonJupyter.kt @@ -0,0 +1,14 @@ +import space.kscience.visionforge.jupyter.VFNotebookClient +import space.kscience.visionforge.markup.MarkupPlugin +import space.kscience.visionforge.plotly.PlotlyPlugin +import space.kscience.visionforge.runVisionClient + +public fun main(): Unit = runVisionClient { +// plugin(DeviceManager) +// plugin(ClockManager) + plugin(PlotlyPlugin) + plugin(MarkupPlugin) +// plugin(TableVisionJsPlugin) + plugin(VFNotebookClient) +} + diff --git a/controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt b/controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt new file mode 100644 index 0000000..2e05e5d --- /dev/null +++ b/controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt @@ -0,0 +1,71 @@ +import org.jetbrains.kotlinx.jupyter.api.declare +import org.jetbrains.kotlinx.jupyter.api.libraries.resources +import space.kscience.controls.manager.ClockManager +import space.kscience.controls.manager.DeviceManager +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.misc.DFExperimental +import space.kscience.plotly.Plot +import space.kscience.tables.Table +import space.kscience.visionforge.jupyter.VisionForge +import space.kscience.visionforge.jupyter.VisionForgeIntegration +import space.kscience.visionforge.markup.MarkupPlugin +import space.kscience.visionforge.plotly.PlotlyPlugin +import space.kscience.visionforge.plotly.asVision +import space.kscience.visionforge.tables.toVision +import space.kscience.visionforge.visionManager + + +@OptIn(DFExperimental::class) +public class ControlsJupyter : VisionForgeIntegration(CONTEXT.visionManager) { + + override fun Builder.afterLoaded(vf: VisionForge) { + + resources { + js("controls-jupyter") { + classPath("js/controls-jupyter.js") + } + } + + onLoaded { + declare("context" to CONTEXT) + } + + import( + "kotlin.time.*", + "kotlin.time.Duration.Companion.milliseconds", + "kotlin.time.Duration.Companion.seconds", + "space.kscience.tables.*", + "space.kscience.dataforge.meta.*", + "space.kscience.dataforge.context.*", + "space.kscience.plotly.*", + "space.kscience.plotly.models.*", + "space.kscience.visionforge.plotly.*", + "space.kscience.controls.manager.*", + "space.kscience.controls.constructor.*", + "space.kscience.controls.vision.*", + "space.kscience.controls.spec.*" + ) + + render> { table -> + vf.produceHtml { + vision { table.toVision() } + } + } + + render { plot -> + vf.produceHtml { + vision { plot.asVision() } + } + } + } + + public companion object { + private val CONTEXT: Context = Context("controls-jupyter") { + plugin(DeviceManager) + plugin(ClockManager) + plugin(PlotlyPlugin) +// plugin(TableVisionPlugin) + plugin(MarkupPlugin) + } + } +} diff --git a/controls-server/build.gradle.kts b/controls-server/build.gradle.kts index 43a9d61..f6a293f 100644 --- a/controls-server/build.gradle.kts +++ b/controls-server/build.gradle.kts @@ -1,7 +1,7 @@ import space.kscience.gradle.Maturity plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` } @@ -12,16 +12,20 @@ description = """ val dataforgeVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra -dependencies { - implementation(projects.controlsCore) - implementation(projects.controlsPortsKtor) - implementation(projects.magix.magixServer) - implementation("io.ktor:ktor-server-cio:$ktorVersion") - implementation("io.ktor:ktor-server-websockets:$ktorVersion") - implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - implementation("io.ktor:ktor-server-html-builder:$ktorVersion") - implementation("io.ktor:ktor-server-status-pages:$ktorVersion") + +kscience { + jvm() + dependencies { + implementation(projects.controlsCore) + implementation(projects.controlsPortsKtor) + implementation(projects.magix.magixServer) + implementation("io.ktor:ktor-server-cio:$ktorVersion") + implementation("io.ktor:ktor-server-websockets:$ktorVersion") + implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + implementation("io.ktor:ktor-server-html-builder:$ktorVersion") + implementation("io.ktor:ktor-server-status-pages:$ktorVersion") + } } readme{ diff --git a/controls-server/src/main/kotlin/space/kscience/controls/server/deviceWebServer.kt b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt similarity index 100% rename from controls-server/src/main/kotlin/space/kscience/controls/server/deviceWebServer.kt rename to controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt diff --git a/controls-server/src/main/kotlin/space/kscience/controls/server/responses.kt b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/responses.kt similarity index 100% rename from controls-server/src/main/kotlin/space/kscience/controls/server/responses.kt rename to controls-server/src/jvmMain/kotlin/space/kscience/controls/server/responses.kt diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts index a69329a..89080a0 100644 --- a/controls-vision/build.gradle.kts +++ b/controls-vision/build.gradle.kts @@ -7,7 +7,7 @@ description = """ Dashboard and visualization extensions for devices """.trimIndent() -val visionforgeVersion = "0.3.0-dev-10" +val visionforgeVersion: String by rootProject.extra kscience { fullStack("js/controls-vision.js") diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 385ccfc..4ddf8e6 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -42,7 +42,7 @@ class LinearDrive( } -public fun main() { +fun main() { val context = Context { plugin(DeviceManager) plugin(PlotlyPlugin) diff --git a/demo/notebooks/constructor.ipynb b/demo/notebooks/constructor.ipynb new file mode 100644 index 0000000..ec09359 --- /dev/null +++ b/demo/notebooks/constructor.ipynb @@ -0,0 +1,162 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "USE(ControlsJupyter())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "class LinearDrive(\n", + " context: Context,\n", + " state: DoubleRangeState,\n", + " mass: Double,\n", + " pidParameters: PidParameters,\n", + " meta: Meta = Meta.EMPTY,\n", + ") : DeviceConstructor(context.request(DeviceManager), meta) {\n", + "\n", + " val drive by device(VirtualDrive.factory(mass, state))\n", + " val pid by device(PidRegulator(drive, pidParameters))\n", + "\n", + " val start by device(LimitSwitch.factory(state.atStartState))\n", + " val end by device(LimitSwitch.factory(state.atEndState))\n", + "\n", + "\n", + " val position by property(state)\n", + " var target by mutableProperty(pid.mutablePropertyAsState(Regulator.target, 0.0))\n", + "}\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import kotlin.time.Duration.Companion.milliseconds\n", + "import kotlin.time.Duration.Companion.seconds\n", + "\n", + "val state = DoubleRangeState(0.0, -5.0..5.0)\n", + "\n", + "val pidParameters = PidParameters(\n", + " kp = 2.5,\n", + " ki = 0.0,\n", + " kd = -0.1,\n", + " timeStep = 0.005.seconds\n", + ")\n", + "\n", + "val device = context.install(\"device\", LinearDrive(context, state, 0.005, pidParameters)).apply {\n", + " val clock = context.clock\n", + " val clockStart = clock.now()\n", + " doRecurring(10.milliseconds) {\n", + " val timeFromStart = clock.now() - clockStart\n", + " val t = timeFromStart.toDouble(DurationUnit.SECONDS)\n", + " val freq = 0.1\n", + "\n", + " target = 5 * sin(2.0 * PI * freq * t) +\n", + " sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))\n", + " }\n", + "}" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "val maxAge = 10.seconds\n", + "\n", + "\n", + "VisionForge.fragment {\n", + " vision {\n", + " plotly {\n", + " plotNumberState(context, state, maxAge = maxAge) {\n", + " name = \"real position\"\n", + " }\n", + " plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge) {\n", + " name = \"read position\"\n", + " }\n", + "\n", + " plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge) {\n", + " name = \"target\"\n", + " }\n", + " }\n", + " }\n", + "\n", + " vision {\n", + " plotly {\n", + " plotDeviceProperty(device.start, LimitSwitch.locked.name, maxAge = maxAge) {\n", + " name = \"start measured\"\n", + " mode = ScatterMode.markers\n", + " }\n", + " plotDeviceProperty(device.end, LimitSwitch.locked.name, maxAge = maxAge) {\n", + " name = \"end measured\"\n", + " mode = ScatterMode.markers\n", + " }\n", + " }\n", + " }\n", + "}" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "device.stop()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [], + "metadata": { + "collapsed": false + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "name": "kotlin", + "version": "1.9.0", + "mimetype": "text/x-kotlin", + "file_extension": ".kt", + "pygments_lexer": "kotlin", + "codemirror_mode": "text/x-kotlin", + "nbconvert_exporter": "" + }, + "ktnbPluginMetadata": { + "projectDependencies": [ + "controls-kt.controls-jupyter.jvmMain" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gradle.properties b/gradle.properties index 62d03f6..ea080f4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,10 +4,7 @@ kotlin.native.ignoreDisabledTargets=true org.gradle.parallel=true -publishing.github=false -publishing.sonatype=false - org.gradle.configureondemand=true org.gradle.jvmargs=-Xmx4096m -toolsVersion=0.15.0-kotlin-1.9.20-RC2 \ No newline at end of file +toolsVersion=0.15.0-kotlin-1.9.20 \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 453519f..7de4241 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -52,6 +52,7 @@ include( ":controls-storage:controls-xodus", ":controls-constructor", ":controls-vision", + ":controls-jupyter", ":magix", ":magix:magix-api", ":magix:magix-server", From 4e17c9051c54a009683f51d04033c5ae2aae9815 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 8 Nov 2023 15:31:12 +0300 Subject: [PATCH 30/71] Update jupyter integration --- demo/notebooks/constructor.ipynb | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/demo/notebooks/constructor.ipynb b/demo/notebooks/constructor.ipynb index ec09359..c670638 100644 --- a/demo/notebooks/constructor.ipynb +++ b/demo/notebooks/constructor.ipynb @@ -56,7 +56,19 @@ " timeStep = 0.005.seconds\n", ")\n", "\n", - "val device = context.install(\"device\", LinearDrive(context, state, 0.005, pidParameters)).apply {\n", + "val device = context.install(\"device\", LinearDrive(context, state, 0.005, pidParameters))" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "\n", + "val job = device.run {\n", " val clock = context.clock\n", " val clockStart = clock.now()\n", " doRecurring(10.milliseconds) {\n", @@ -120,7 +132,9 @@ "execution_count": null, "outputs": [], "source": [ - "device.stop()" + "import kotlinx.coroutines.cancel\n", + "\n", + "job.cancel()" ], "metadata": { "collapsed": false From fe98a836f89648dd3a1b8c114f3ea8c2626d8466 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 8 Nov 2023 21:01:42 +0300 Subject: [PATCH 31/71] Update jupyter integration --- build.gradle.kts | 2 +- controls-jupyter/build.gradle.kts | 3 ++- .../src/jvmMain/kotlin/ControlsJupyter.kt | 16 ++++++++-------- controls-vision/build.gradle.kts | 3 ++- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a135960..2b3f6b3 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.3.0-dev-1" + version = "0.3.0-dev-2" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-jupyter/build.gradle.kts b/controls-jupyter/build.gradle.kts index 3191720..c8486bd 100644 --- a/controls-jupyter/build.gradle.kts +++ b/controls-jupyter/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("space.kscience.gradle.mpp") + `maven-publish` } val visionforgeVersion: String by rootProject.extra @@ -8,7 +9,7 @@ kscience { fullStack("js/controls-jupyter.js") useKtor() useContextReceivers() - jupyterLibrary() + jupyterLibrary("space.kscience.controls.jupyter.ControlsJupyter") dependencies { implementation(projects.controlsVision) implementation("space.kscience:visionforge-jupyter:$visionforgeVersion") diff --git a/controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt b/controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt index 2e05e5d..ec4bcee 100644 --- a/controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt +++ b/controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt @@ -1,3 +1,5 @@ +package space.kscience.controls.jupyter + import org.jetbrains.kotlinx.jupyter.api.declare import org.jetbrains.kotlinx.jupyter.api.libraries.resources import space.kscience.controls.manager.ClockManager @@ -5,13 +7,11 @@ import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.context.Context import space.kscience.dataforge.misc.DFExperimental import space.kscience.plotly.Plot -import space.kscience.tables.Table import space.kscience.visionforge.jupyter.VisionForge import space.kscience.visionforge.jupyter.VisionForgeIntegration import space.kscience.visionforge.markup.MarkupPlugin import space.kscience.visionforge.plotly.PlotlyPlugin import space.kscience.visionforge.plotly.asVision -import space.kscience.visionforge.tables.toVision import space.kscience.visionforge.visionManager @@ -34,7 +34,7 @@ public class ControlsJupyter : VisionForgeIntegration(CONTEXT.visionManager) { "kotlin.time.*", "kotlin.time.Duration.Companion.milliseconds", "kotlin.time.Duration.Companion.seconds", - "space.kscience.tables.*", +// "space.kscience.tables.*", "space.kscience.dataforge.meta.*", "space.kscience.dataforge.context.*", "space.kscience.plotly.*", @@ -46,11 +46,11 @@ public class ControlsJupyter : VisionForgeIntegration(CONTEXT.visionManager) { "space.kscience.controls.spec.*" ) - render> { table -> - vf.produceHtml { - vision { table.toVision() } - } - } +// render> { table -> +// vf.produceHtml { +// vision { table.toVision() } +// } +// } render { plot -> vf.produceHtml { diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts index 89080a0..9395b92 100644 --- a/controls-vision/build.gradle.kts +++ b/controls-vision/build.gradle.kts @@ -18,7 +18,8 @@ kscience { api(projects.controlsConstructor) api("space.kscience:visionforge-plotly:$visionforgeVersion") api("space.kscience:visionforge-markdown:$visionforgeVersion") - api("space.kscience:visionforge-tables:$visionforgeVersion") +// api("space.kscience:tables-kt:0.2.1") +// api("space.kscience:visionforge-tables:$visionforgeVersion") } jvmMain{ From 74301afb42fe3046ee0de2ae8d05cdfe25c3b187 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 8 Nov 2023 22:28:26 +0300 Subject: [PATCH 32/71] Return notifications about pid and drive updates. Introduce debounce --- .../kscience/controls/constructor/Drive.kt | 1 + .../controls/constructor/PidRegulator.kt | 1 + .../src/commonMain/kotlin/plotExtensions.kt | 24 +++-- demo/notebooks/constructor.ipynb | 91 +++++++++++-------- 4 files changed, 74 insertions(+), 43 deletions(-) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt index 0cf4ce0..c2391c8 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt @@ -74,6 +74,7 @@ public class VirtualDrive( // compute new value based on velocity and acceleration from the previous step positionState.value += velocity * dtSeconds + force / mass * dtSeconds.pow(2) / 2 + propertyChanged(Drive.position, positionState.value) // compute new velocity based on acceleration on the previous step velocity += force / mass * dtSeconds diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt index 860af60..9fc6e18 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt @@ -62,6 +62,7 @@ public class PidRegulator( lastPosition = drive.position drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative + propertyChanged(Regulator.position, drive.position) } } } diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index ce85483..40f746b 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -1,7 +1,11 @@ +@file:OptIn(FlowPreview::class) + package space.kscience.controls.vision import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.transform @@ -25,6 +29,7 @@ import space.kscience.plotly.models.TraceValues import space.kscience.plotly.scatter import kotlin.time.Duration import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds private var TraceValues.values: List get() = value?.list ?: emptyList() @@ -88,12 +93,13 @@ public fun Plot.plotDeviceProperty( maxAge: Duration = 1.hours, maxPoints: Int = 800, minPoints: Int = 400, + debounceDuration: Duration = 10.milliseconds, coroutineScope: CoroutineScope = device.context, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { val clock = device.context.clock val data = TimeData() - device.propertyMessageFlow(propertyName).transform { + device.propertyMessageFlow(propertyName).debounce(debounceDuration).transform { data.append(it.time ?: clock.now(), it.value.extractValue()) data.trim(maxAge, maxPoints, minPoints) emit(data) @@ -109,10 +115,11 @@ private fun Trace.updateFromState( maxAge: Duration = 1.hours, maxPoints: Int = 800, minPoints: Int = 400, -): Job{ + debounceDuration: Duration = 10.milliseconds, +): Job { val clock = context.clock val data = TimeData() - return state.valueFlow.transform { + return state.valueFlow.debounce(debounceDuration).transform { data.append(clock.now(), it.extractValue()) data.trim(maxAge, maxPoints, minPoints) }.onEach { @@ -127,9 +134,10 @@ public fun Plot.plotDeviceState( maxAge: Duration = 1.hours, maxPoints: Int = 800, minPoints: Int = 400, + debounceDuration: Duration = 10.milliseconds, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { - updateFromState(context, state, extractValue, maxAge, maxPoints, minPoints) + updateFromState(context, state, extractValue, maxAge, maxPoints, minPoints, debounceDuration) } @@ -139,9 +147,10 @@ public fun Plot.plotNumberState( maxAge: Duration = 1.hours, maxPoints: Int = 800, minPoints: Int = 400, + debounceDuration: Duration = 10.milliseconds, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { - updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints) + updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, debounceDuration) } @@ -151,7 +160,8 @@ public fun Plot.plotBooleanState( maxAge: Duration = 1.hours, maxPoints: Int = 800, minPoints: Int = 400, + debounceDuration: Duration = 10.milliseconds, configuration: Bar.() -> Unit = {}, -): Job = bar(configuration).run { - updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints) +): Job = bar(configuration).run { + updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, debounceDuration) } \ No newline at end of file diff --git a/demo/notebooks/constructor.ipynb b/demo/notebooks/constructor.ipynb index c670638..3b4d73e 100644 --- a/demo/notebooks/constructor.ipynb +++ b/demo/notebooks/constructor.ipynb @@ -3,17 +3,28 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ - "USE(ControlsJupyter())" + "//import space.kscience.controls.jupyter.ControlsJupyter\n", + "\n", + "//USE(ControlsJupyter())\n", + "USE{\n", + " repositories{\n", + " maven(\"https://repo.kotlin.link\")\n", + " }\n", + " dependencies{\n", + " implementation(\"space.kscience:controls-jupyter-jvm:0.3.0-dev-2\")\n", + " }\n", + "}" ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ "class LinearDrive(\n", @@ -34,14 +45,14 @@ " val position by property(state)\n", " var target by mutableProperty(pid.mutablePropertyAsState(Regulator.target, 0.0))\n", "}\n" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ "import kotlin.time.Duration.Companion.milliseconds\n", @@ -57,14 +68,14 @@ ")\n", "\n", "val device = context.install(\"device\", LinearDrive(context, state, 0.005, pidParameters))" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ "\n", @@ -80,14 +91,14 @@ " sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))\n", " }\n", "}" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ "val maxAge = 10.seconds\n", @@ -96,16 +107,18 @@ "VisionForge.fragment {\n", " vision {\n", " plotly {\n", + " \n", + " plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge) {\n", + " name = \"target\"\n", + " }\n", + " \n", " plotNumberState(context, state, maxAge = maxAge) {\n", " name = \"real position\"\n", " }\n", + " \n", " plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge) {\n", " name = \"read position\"\n", " }\n", - "\n", - " plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge) {\n", - " name = \"target\"\n", - " }\n", " }\n", " }\n", "\n", @@ -122,23 +135,29 @@ " }\n", " }\n", "}" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ "import kotlinx.coroutines.cancel\n", "\n", "job.cancel()" - ], + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": { "collapsed": false - } + }, + "outputs": [], + "source": [] }, { "cell_type": "code", @@ -156,21 +175,21 @@ "language": "kotlin", "name": "kotlin" }, - "language_info": { - "name": "kotlin", - "version": "1.9.0", - "mimetype": "text/x-kotlin", - "file_extension": ".kt", - "pygments_lexer": "kotlin", - "codemirror_mode": "text/x-kotlin", - "nbconvert_exporter": "" - }, "ktnbPluginMetadata": { "projectDependencies": [ "controls-kt.controls-jupyter.jvmMain" ] + }, + "language_info": { + "codemirror_mode": "text/x-kotlin", + "file_extension": ".kt", + "mimetype": "text/x-kotlin", + "name": "kotlin", + "nbconvert_exporter": "", + "pygments_lexer": "kotlin", + "version": "1.8.20" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } From fb8ee59f148e1312ae4bbf06718d1465b391755b Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 8 Nov 2023 22:33:49 +0300 Subject: [PATCH 33/71] replace debounce by sample --- .../src/commonMain/kotlin/plotExtensions.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index 40f746b..3640ee9 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -5,9 +5,9 @@ package space.kscience.controls.vision import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.transform import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -93,13 +93,13 @@ public fun Plot.plotDeviceProperty( maxAge: Duration = 1.hours, maxPoints: Int = 800, minPoints: Int = 400, - debounceDuration: Duration = 10.milliseconds, + sampling: Duration = 10.milliseconds, coroutineScope: CoroutineScope = device.context, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { val clock = device.context.clock val data = TimeData() - device.propertyMessageFlow(propertyName).debounce(debounceDuration).transform { + device.propertyMessageFlow(propertyName).sample(sampling).transform { data.append(it.time ?: clock.now(), it.value.extractValue()) data.trim(maxAge, maxPoints, minPoints) emit(data) @@ -115,11 +115,11 @@ private fun Trace.updateFromState( maxAge: Duration = 1.hours, maxPoints: Int = 800, minPoints: Int = 400, - debounceDuration: Duration = 10.milliseconds, + sampling: Duration = 10.milliseconds, ): Job { val clock = context.clock val data = TimeData() - return state.valueFlow.debounce(debounceDuration).transform { + return state.valueFlow.sample(sampling).transform { data.append(clock.now(), it.extractValue()) data.trim(maxAge, maxPoints, minPoints) }.onEach { @@ -134,10 +134,10 @@ public fun Plot.plotDeviceState( maxAge: Duration = 1.hours, maxPoints: Int = 800, minPoints: Int = 400, - debounceDuration: Duration = 10.milliseconds, + sampling: Duration = 10.milliseconds, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { - updateFromState(context, state, extractValue, maxAge, maxPoints, minPoints, debounceDuration) + updateFromState(context, state, extractValue, maxAge, maxPoints, minPoints, sampling) } @@ -147,10 +147,10 @@ public fun Plot.plotNumberState( maxAge: Duration = 1.hours, maxPoints: Int = 800, minPoints: Int = 400, - debounceDuration: Duration = 10.milliseconds, + sampling: Duration = 10.milliseconds, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { - updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, debounceDuration) + updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, sampling) } @@ -160,8 +160,8 @@ public fun Plot.plotBooleanState( maxAge: Duration = 1.hours, maxPoints: Int = 800, minPoints: Int = 400, - debounceDuration: Duration = 10.milliseconds, + sampling: Duration = 10.milliseconds, configuration: Bar.() -> Unit = {}, ): Job = bar(configuration).run { - updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, debounceDuration) + updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, sampling) } \ No newline at end of file From afee2f0a02bdd97bf82c8e48d2050825c6608f1a Mon Sep 17 00:00:00 2001 From: darksnake Date: Fri, 17 Nov 2023 12:22:06 +0300 Subject: [PATCH 34/71] minor update to constructor --- demo/constructor/src/jvmMain/kotlin/main.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 4ddf8e6..16a6283 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -1,11 +1,15 @@ package space.kscience.controls.demo.constructor +import kotlinx.coroutines.delay +import space.kscience.controls.api.get import space.kscience.controls.constructor.* import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.clock import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.name +import space.kscience.controls.spec.read +import space.kscience.controls.spec.write import space.kscience.controls.vision.plot import space.kscience.controls.vision.plotDeviceProperty import space.kscience.controls.vision.plotNumberState @@ -100,4 +104,15 @@ fun main() { } } +} + +suspend fun DeviceGroup.findEnd(): Double{ + val regulator = get("pid") as Regulator + val limitEnd = get("end") as LimitSwitch + + while(!limitEnd.read(LimitSwitch.locked)){ + delay(10.milliseconds) + regulator.write(Regulator.target, regulator.read(Regulator.position) + 0.1) + } + return regulator.read(Regulator.position) } \ No newline at end of file From b539c2046a8237acab772d65080485a2a26c8077 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 18 Nov 2023 14:49:23 +0300 Subject: [PATCH 35/71] `DeviceSpec` properties no explicitly pass property name to getters and setters --- CHANGELOG.md | 1 + .../controls/constructor/Regulator.kt | 5 +- .../kscience/controls/api/descriptors.kt | 2 +- .../kscience/controls/spec/DeviceSpec.kt | 141 +++++++----------- .../controls/spec/deviceExtensions.kt | 2 +- .../controls/spec/propertySpecDelegates.kt | 32 ++-- .../controls/spec/propertyReflection.kt | 8 + .../controls/opcua/client/OpcUaClientTest.kt | 4 +- .../commonMain/kotlin/ControlVisionPlugin.kt | 19 +++ .../src/commonMain/kotlin/IndicatorVision.kt | 20 +++ .../jsMain/kotlin/ControlsVisionPlugin.js.kt | 33 ++++ .../kotlin/ControlsVisionPlugin.jvm.kt | 21 +++ .../kscience/controls/demo/DemoDevice.kt | 6 +- .../kscience/controls/demo/car/IVirtualCar.kt | 2 + .../sciprog/devices/mks/MksPdr900Device.kt | 2 +- .../pimotionmaster/PiMotionMasterDevice.kt | 12 +- 16 files changed, 185 insertions(+), 125 deletions(-) create mode 100644 controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/propertyReflection.kt create mode 100644 controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt create mode 100644 controls-vision/src/commonMain/kotlin/IndicatorVision.kt create mode 100644 controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt create mode 100644 controls-vision/src/jvmMain/kotlin/ControlsVisionPlugin.jvm.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 4083a94..abf5789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Changed - Property caching moved from core `Device` to the `CachingDevice` +- `DeviceSpec` properties no explicitly pass property name to getters and setters. ### Deprecated diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt index 71cb3b0..7495d33 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt @@ -1,10 +1,7 @@ package space.kscience.controls.constructor import space.kscience.controls.api.Device -import space.kscience.controls.spec.DevicePropertySpec -import space.kscience.controls.spec.DeviceSpec -import space.kscience.controls.spec.MutableDevicePropertySpec -import space.kscience.controls.spec.doubleProperty +import space.kscience.controls.spec.* import space.kscience.dataforge.meta.transformations.MetaConverter diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt index 6f7d27c..2adb89a 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt @@ -27,6 +27,6 @@ public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Un */ @Serializable public class ActionDescriptor(public val name: String) { - public var info: String? = null + public var description: String? = null } 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 55122d9..b5813b7 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 @@ -44,64 +44,11 @@ public abstract class DeviceSpec { return deviceProperty } - public fun property( + public inline fun property( converter: MetaConverter, - readOnlyProperty: KProperty1, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - ): PropertyDelegateProvider, ReadOnlyProperty>> = - PropertyDelegateProvider { _, property -> - val deviceProperty = object : DevicePropertySpec { - override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { - //TODO add type from converter - mutable = true - }.apply(descriptorBuilder) - - override val converter: MetaConverter = converter - - override suspend fun read(device: D): T = withContext(device.coroutineContext) { - readOnlyProperty.get(device) - } - } - registerProperty(deviceProperty) - ReadOnlyProperty { _, _ -> - deviceProperty - } - } - - public fun mutableProperty( - converter: MetaConverter, - readWriteProperty: KMutableProperty1, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - ): PropertyDelegateProvider, ReadOnlyProperty>> = - PropertyDelegateProvider { _, property -> - val deviceProperty = object : MutableDevicePropertySpec { - - override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { - //TODO add the type from converter - mutable = true - }.apply(descriptorBuilder) - - override val converter: MetaConverter = converter - - override suspend fun read(device: D): T = withContext(device.coroutineContext) { - readWriteProperty.get(device) - } - - override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) { - readWriteProperty.set(device, value) - } - } - registerProperty(deviceProperty) - ReadOnlyProperty { _, _ -> - deviceProperty - } - } - - public fun property( - converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> T?, + crossinline read: suspend D.(propertyName: String) -> T?, ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property -> val propertyName = name ?: property.name @@ -109,7 +56,7 @@ public abstract class DeviceSpec { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) override val converter: MetaConverter = converter - override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() } + override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read(propertyName) } } registerProperty(deviceProperty) ReadOnlyProperty, DevicePropertySpec> { _, _ -> @@ -117,27 +64,30 @@ public abstract class DeviceSpec { } } - public fun mutableProperty( + public inline fun mutableProperty( converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> T?, - write: suspend D.(T) -> Unit, + crossinline read: suspend D.(propertyName: String) -> T?, + crossinline write: suspend D.(propertyName: String, value: T) -> Unit, ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val propertyName = name ?: property.name val deviceProperty = object : MutableDevicePropertySpec { - override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, mutable = true) - .apply(descriptorBuilder) + override val descriptor: PropertyDescriptor = PropertyDescriptor( + propertyName, + mutable = true + ).apply(descriptorBuilder) override val converter: MetaConverter = converter - override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() } + override suspend fun read(device: D): T? = + withContext(device.coroutineContext) { device.read(propertyName) } override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) { - device.write(value) + device.write(propertyName, value) } } - _properties[propertyName] = deviceProperty + registerProperty(deviceProperty) ReadOnlyProperty, MutableDevicePropertySpec> { _, _ -> deviceProperty } @@ -209,33 +159,42 @@ public abstract class DeviceSpec { } } +public inline fun DeviceSpec.property( + converter: MetaConverter, + readOnlyProperty: KProperty1, + crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( + converter, + descriptorBuilder, + name = readOnlyProperty.name, + read = { readOnlyProperty.get(this) } +) + +public inline fun DeviceSpec.mutableProperty( + converter: MetaConverter, + readWriteProperty: KMutableProperty1, + crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty( + converter, + descriptorBuilder, + readWriteProperty.name, + read = { _ -> readWriteProperty.get(this) }, + write = { _, value: T -> readWriteProperty.set(this, value) } + ) /** - * Register a mutable logical property for a device + * Register a mutable logical property (without a corresponding physical state) for a device */ -@OptIn(InternalDeviceAPI::class) -public fun > DeviceSpec.logicalProperty( +public inline fun > DeviceSpec.logicalProperty( converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, -): PropertyDelegateProvider, ReadOnlyProperty>> = - PropertyDelegateProvider { _, property -> - val deviceProperty = object : MutableDevicePropertySpec { - val propertyName = name ?: property.name - override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { - //TODO add type from converter - mutable = true - }.apply(descriptorBuilder) - - override val converter: MetaConverter = converter - - override suspend fun read(device: D): T? = device.getProperty(propertyName)?.let(converter::metaToObject) - - override suspend fun write(device: D, value: T): Unit = - device.writeProperty(propertyName, converter.objectToMeta(value)) - } - registerProperty(deviceProperty) - ReadOnlyProperty { _, _ -> - deviceProperty - } - } \ No newline at end of file +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty( + converter, + descriptorBuilder, + name, + read = { propertyName -> getProperty(propertyName)?.let(converter::metaToObject) }, + write = { propertyName, value -> writeProperty(propertyName, converter.objectToMeta(value)) } + ) \ 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 af42f60..fefb65e 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 @@ -12,7 +12,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 uses called context. In order to call it on device context, use `flowOn(coroutineContext)`. + * The flow uses caller context. To call it on device context, use `flowOn(coroutineContext)`. * * The flow is canceled when the device scope is canceled */ 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 70ef94a..8314838 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 @@ -14,7 +14,7 @@ import kotlin.properties.ReadOnlyProperty public fun DeviceSpec.booleanProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> Boolean? + read: suspend D.(propertyName: String) -> Boolean? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( MetaConverter.boolean, { @@ -37,9 +37,9 @@ private inline fun numberDescriptor( } public fun DeviceSpec.numberProperty( - name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - read: suspend D.() -> Number? + name: String? = null, + read: suspend D.(propertyName: String) -> Number? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( MetaConverter.number, numberDescriptor(descriptorBuilder), @@ -50,7 +50,7 @@ public fun DeviceSpec.numberProperty( public fun DeviceSpec.doubleProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> Double? + read: suspend D.(propertyName: String) -> Double? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( MetaConverter.double, numberDescriptor(descriptorBuilder), @@ -61,7 +61,7 @@ public fun DeviceSpec.doubleProperty( public fun DeviceSpec.stringProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> String? + read: suspend D.(propertyName: String) -> String? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( MetaConverter.string, { @@ -77,7 +77,7 @@ public fun DeviceSpec.stringProperty( public fun DeviceSpec.metaProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> Meta? + read: suspend D.(propertyName: String) -> Meta? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( MetaConverter.meta, { @@ -95,8 +95,8 @@ public fun DeviceSpec.metaProperty( public fun DeviceSpec.booleanProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> Boolean?, - write: suspend D.(Boolean) -> Unit + read: suspend D.(propertyName: String) -> Boolean?, + write: suspend D.(propertyName: String, value: Boolean) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty( MetaConverter.boolean, @@ -115,31 +115,31 @@ public fun DeviceSpec.booleanProperty( public fun DeviceSpec.numberProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> Number, - write: suspend D.(Number) -> Unit + read: suspend D.(propertyName: String) -> Number, + write: suspend D.(propertyName: String, value: Number) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) public fun DeviceSpec.doubleProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> Double, - write: suspend D.(Double) -> Unit + read: suspend D.(propertyName: String) -> Double, + write: suspend D.(propertyName: String, value: Double) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) public fun DeviceSpec.stringProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> String, - write: suspend D.(String) -> Unit + read: suspend D.(propertyName: String) -> String, + write: suspend D.(propertyName: String, value: String) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) public fun DeviceSpec.metaProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> Meta, - write: suspend D.(Meta) -> Unit + read: suspend D.(propertyName: String) -> Meta, + write: suspend D.(propertyName: String, value: Meta) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write) \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/propertyReflection.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/propertyReflection.kt new file mode 100644 index 0000000..1d4cb56 --- /dev/null +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/propertyReflection.kt @@ -0,0 +1,8 @@ +package space.kscience.controls.spec + +import space.kscience.controls.api.PropertyDescriptor +import kotlin.reflect.KProperty + +internal fun PropertyDescriptor.fromSpec(property: KProperty<*>){ + property.annotations +} \ 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 index 8537449..33aa8fb 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 @@ -29,7 +29,7 @@ class OpcUaClientTest { return DemoOpcUaDevice(config) } - val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble) + val randomDouble by doubleProperty { readRandomDouble() } } @@ -42,7 +42,7 @@ class OpcUaClientTest { fun testReadDouble() = runTest { val device = DemoOpcUaDevice.build() device.start() - println(device.read(DemoOpcUaDevice.randomDouble)) + println(device.read(DemoOpcUaDevice.randomDouble)) device.stop() } diff --git a/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt b/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt new file mode 100644 index 0000000..d587250 --- /dev/null +++ b/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt @@ -0,0 +1,19 @@ +package space.kscience.controls.vision + +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass +import space.kscience.dataforge.context.PluginFactory +import space.kscience.visionforge.Vision +import space.kscience.visionforge.VisionPlugin +import space.kscience.visionforge.plotly.VisionOfPlotly + +public expect class ControlVisionPlugin: VisionPlugin{ + public companion object: PluginFactory +} + +internal val controlsVisionSerializersModule = SerializersModule { + polymorphic(Vision::class) { + subclass(VisionOfPlotly.serializer()) + } +} \ No newline at end of file diff --git a/controls-vision/src/commonMain/kotlin/IndicatorVision.kt b/controls-vision/src/commonMain/kotlin/IndicatorVision.kt new file mode 100644 index 0000000..9ec2319 --- /dev/null +++ b/controls-vision/src/commonMain/kotlin/IndicatorVision.kt @@ -0,0 +1,20 @@ +package space.kscience.controls.vision + +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.node +import space.kscience.visionforge.AbstractVision +import space.kscience.visionforge.Vision + +/** + * A [Vision] that shows an indicator + */ +public class IndicatorVision: AbstractVision() { + public val value: Meta? by properties.node() +} + +///** +// * A [Vision] that allows both showing the value and changing it +// */ +//public interface RegulatorVision: IndicatorVision{ +// +//} \ No newline at end of file diff --git a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt new file mode 100644 index 0000000..0d928ac --- /dev/null +++ b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt @@ -0,0 +1,33 @@ +package space.kscience.controls.vision + +import kotlinx.serialization.modules.SerializersModule +import org.w3c.dom.Element +import space.kscience.dataforge.context.Context +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 space.kscience.visionforge.ElementVisionRenderer +import space.kscience.visionforge.Vision +import space.kscience.visionforge.VisionPlugin + +public actual class ControlVisionPlugin : VisionPlugin(), ElementVisionRenderer { + override val tag: PluginTag get() = Companion.tag + + override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule + + override fun rateVision(vision: Vision): Int { + TODO("Not yet implemented") + } + + override fun render(element: Element, name: Name, vision: Vision, meta: Meta) { + TODO("Not yet implemented") + } + + public actual companion object : PluginFactory { + override val tag: PluginTag = PluginTag("controls.vision") + + override fun build(context: Context, meta: Meta): ControlVisionPlugin = ControlVisionPlugin() + + } +} \ No newline at end of file diff --git a/controls-vision/src/jvmMain/kotlin/ControlsVisionPlugin.jvm.kt b/controls-vision/src/jvmMain/kotlin/ControlsVisionPlugin.jvm.kt new file mode 100644 index 0000000..55074ae --- /dev/null +++ b/controls-vision/src/jvmMain/kotlin/ControlsVisionPlugin.jvm.kt @@ -0,0 +1,21 @@ +package space.kscience.controls.vision + +import kotlinx.serialization.modules.SerializersModule +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.PluginFactory +import space.kscience.dataforge.context.PluginTag +import space.kscience.dataforge.meta.Meta +import space.kscience.visionforge.VisionPlugin + +public actual class ControlVisionPlugin : VisionPlugin() { + override val tag: PluginTag get() = Companion.tag + + override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule + + public actual companion object : PluginFactory { + override val tag: PluginTag = PluginTag("controls.vision") + + override fun build(context: Context, meta: Meta): ControlVisionPlugin = ControlVisionPlugin() + + } +} \ No newline at end of file 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 4f902c0..eaa1bff 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 @@ -16,7 +16,7 @@ import kotlin.math.sin import kotlin.time.Duration.Companion.milliseconds -interface IDemoDevice: Device { +interface IDemoDevice : Device { var timeScaleState: Double var sinScaleState: Double var cosScaleState: Double @@ -50,8 +50,8 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(Compa 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 sin by doubleProperty { sinValue() } + val cos by doubleProperty { cosValue() } val coordinates by metaProperty( descriptorBuilder = { diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt index 3bb8c79..c61041b 100644 --- a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt @@ -2,6 +2,8 @@ package space.kscience.controls.demo.car import space.kscience.controls.api.Device import space.kscience.controls.spec.DeviceSpec +import space.kscience.controls.spec.mutableProperty +import space.kscience.controls.spec.property interface IVirtualCar : Device { var speedState: Vector2D 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 df54bfa..cb8030c 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 @@ -89,7 +89,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec writePowerOn(value) }) val channel by logicalProperty(MetaConverter.int) diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt index 18e5838..dde2750 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -157,13 +157,13 @@ class PiMotionMasterDevice( } val stop by unitAction({ - info = "Stop all axis" + description = "Stop all axis" }) { send("STP") } val connect by action(MetaConverter.meta, MetaConverter.unit, descriptorBuilder = { - info = "Connect to specific port and initialize axis" + description = "Connect to specific port and initialize axis" }) { portSpec -> //Clear current actions if present if (port != null) { @@ -189,7 +189,7 @@ class PiMotionMasterDevice( } val disconnect by unitAction({ - info = "Disconnect the program from the device if it is connected" + description = "Disconnect the program from the device if it is connected" }) { port?.let { execute(stop) @@ -245,8 +245,8 @@ class PiMotionMasterDevice( read = { readAxisBoolean("$command?") }, - write = { - writeAxisBoolean(command, it) + write = { _, value -> + writeAxisBoolean(command, value) }, descriptorBuilder = descriptorBuilder ) @@ -259,7 +259,7 @@ class PiMotionMasterDevice( mm.requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull() ?: error("Malformed $command response. Should include float value for $axisId") }, - write = { newValue -> + write = { _, newValue -> mm.send(command, axisId, newValue.toString()) mm.failIfError() }, From 0c647cff306ab732e49378a1b16e152b270a15a5 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 18 Nov 2023 15:39:56 +0300 Subject: [PATCH 36/71] `DeviceSpec` properties no explicitly pass property name to getters and setters --- .../kscience/controls/spec/DeviceSpec.kt | 115 ++++++------------ .../controls/spec/propertySpecDelegates.kt | 49 ++++++++ 2 files changed, 87 insertions(+), 77 deletions(-) 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 b5813b7..19e5ab2 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 @@ -8,9 +8,7 @@ import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.transformations.MetaConverter import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KProperty -import kotlin.reflect.KProperty1 public object UnitMetaConverter : MetaConverter { override fun metaToObject(meta: Meta): Unit = Unit @@ -44,11 +42,11 @@ public abstract class DeviceSpec { return deviceProperty } - public inline fun property( + public fun property( converter: MetaConverter, - crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - crossinline read: suspend D.(propertyName: String) -> T?, + read: suspend D.(propertyName: String) -> T?, ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property -> val propertyName = name ?: property.name @@ -56,7 +54,8 @@ public abstract class DeviceSpec { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) override val converter: MetaConverter = converter - override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read(propertyName) } + override suspend fun read(device: D): T? = + withContext(device.coroutineContext) { device.read(propertyName) } } registerProperty(deviceProperty) ReadOnlyProperty, DevicePropertySpec> { _, _ -> @@ -64,12 +63,12 @@ public abstract class DeviceSpec { } } - public inline fun mutableProperty( + public fun mutableProperty( converter: MetaConverter, - crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - crossinline read: suspend D.(propertyName: String) -> T?, - crossinline write: suspend D.(propertyName: String, value: T) -> Unit, + read: suspend D.(propertyName: String) -> T?, + write: suspend D.(propertyName: String, value: T) -> Unit, ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val propertyName = name ?: property.name @@ -106,7 +105,7 @@ public abstract class DeviceSpec { name: String? = null, execute: suspend D.(I) -> O, ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = - PropertyDelegateProvider { _: DeviceSpec, property -> + PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val actionName = name ?: property.name val deviceAction = object : DeviceActionSpec { override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder) @@ -124,77 +123,39 @@ public abstract class DeviceSpec { } } - /** - * An action that takes [Meta] and returns [Meta]. No conversions are done - */ - public fun metaAction( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, - name: String? = null, - execute: suspend D.(Meta) -> Meta, - ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = - action( - MetaConverter.Companion.meta, - MetaConverter.Companion.meta, - descriptorBuilder, - name - ) { - execute(it) - } - - /** - * An action that takes no parameters and returns no values - */ - public fun unitAction( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, - name: String? = null, - execute: suspend D.() -> Unit, - ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = - action( - MetaConverter.Companion.unit, - MetaConverter.Companion.unit, - descriptorBuilder, - name - ) { - execute() - } } -public inline fun DeviceSpec.property( - converter: MetaConverter, - readOnlyProperty: KProperty1, - crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( - converter, - descriptorBuilder, - name = readOnlyProperty.name, - read = { readOnlyProperty.get(this) } -) - -public inline fun DeviceSpec.mutableProperty( - converter: MetaConverter, - readWriteProperty: KMutableProperty1, - crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = - mutableProperty( - converter, +/** + * An action that takes no parameters and returns no values + */ +public fun DeviceSpec.unitAction( + descriptorBuilder: ActionDescriptor.() -> Unit = {}, + name: String? = null, + execute: suspend D.() -> Unit, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + MetaConverter.Companion.unit, + MetaConverter.Companion.unit, descriptorBuilder, - readWriteProperty.name, - read = { _ -> readWriteProperty.get(this) }, - write = { _, value: T -> readWriteProperty.set(this, value) } - ) + name + ) { + execute() + } /** - * Register a mutable logical property (without a corresponding physical state) for a device + * An action that takes [Meta] and returns [Meta]. No conversions are done */ -public inline fun > DeviceSpec.logicalProperty( - converter: MetaConverter, - crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +public fun DeviceSpec.metaAction( + descriptorBuilder: ActionDescriptor.() -> Unit = {}, name: String? = null, -): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = - mutableProperty( - converter, + execute: suspend D.(Meta) -> Meta, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + MetaConverter.Companion.meta, + MetaConverter.Companion.meta, descriptorBuilder, - name, - read = { propertyName -> getProperty(propertyName)?.let(converter::metaToObject) }, - write = { propertyName, value -> writeProperty(propertyName, converter.objectToMeta(value)) } - ) \ No newline at end of file + name + ) { + execute(it) + } + 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 8314838..6f599a8 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 @@ -8,6 +8,55 @@ import space.kscience.dataforge.meta.ValueType import space.kscience.dataforge.meta.transformations.MetaConverter import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.KProperty1 + +/** + * A read-only device property that delegates reading to a device [KProperty1] + */ +public fun DeviceSpec.property( + converter: MetaConverter, + readOnlyProperty: KProperty1, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( + converter, + descriptorBuilder, + name = readOnlyProperty.name, + read = { readOnlyProperty.get(this) } +) + +/** + * Mutable property that delegates reading and writing to a device [KMutableProperty1] + */ +public fun DeviceSpec.mutableProperty( + converter: MetaConverter, + readWriteProperty: KMutableProperty1, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty( + converter, + descriptorBuilder, + readWriteProperty.name, + read = { _ -> readWriteProperty.get(this) }, + write = { _, value: T -> readWriteProperty.set(this, value) } + ) + +/** + * Register a mutable logical property (without a corresponding physical state) for a device + */ +public fun > DeviceSpec.logicalProperty( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + name: String? = null, +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty( + converter, + descriptorBuilder, + name, + read = { propertyName -> getProperty(propertyName)?.let(converter::metaToObject) }, + write = { propertyName, value -> writeProperty(propertyName, converter.objectToMeta(value)) } + ) + //read only delegates From 07cc41c645692f1004ba293498f9768482150cd4 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 18 Nov 2023 19:02:56 +0300 Subject: [PATCH 37/71] Automatic description generation for spec properties (JVM only) --- CHANGELOG.md | 1 + .../space/kscience/controls/spec/DeviceSpec.kt | 17 ++++++++++++++--- .../space/kscience/controls/spec/fromSpec.kt | 12 ++++++++++++ .../kscience/controls/spec/fromSpec.js.kt | 9 +++++++++ .../kscience/controls/spec/fromSpec.jvm.kt | 18 ++++++++++++++++++ .../controls/spec/propertyReflection.kt | 8 -------- .../kscience/controls/spec/fromSpec.native.kt | 9 +++++++++ 7 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt create mode 100644 controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt create mode 100644 controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt delete mode 100644 controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/propertyReflection.kt create mode 100644 controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index abf5789..b4de4b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Device lifecycle message - Low-code constructor +- Automatic description generation for spec properties (JVM only) ### Changed - Property caching moved from core `Device` to the `CachingDevice` 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 19e5ab2..cd0d7cc 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 @@ -51,7 +51,12 @@ public abstract class DeviceSpec { PropertyDelegateProvider { _: DeviceSpec, property -> val propertyName = name ?: property.name val deviceProperty = object : DevicePropertySpec { - override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) + + override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { + fromSpec(property) + descriptorBuilder() + } + override val converter: MetaConverter = converter override suspend fun read(device: D): T? = @@ -76,7 +81,10 @@ public abstract class DeviceSpec { override val descriptor: PropertyDescriptor = PropertyDescriptor( propertyName, mutable = true - ).apply(descriptorBuilder) + ).apply { + fromSpec(property) + descriptorBuilder() + } override val converter: MetaConverter = converter override suspend fun read(device: D): T? = @@ -108,7 +116,10 @@ public abstract class DeviceSpec { PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val actionName = name ?: property.name val deviceAction = object : DeviceActionSpec { - override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder) + override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply { + fromSpec(property) + descriptorBuilder() + } override val inputConverter: MetaConverter = inputConverter override val outputConverter: MetaConverter = outputConverter diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt new file mode 100644 index 0000000..000458e --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt @@ -0,0 +1,12 @@ +package space.kscience.controls.spec + +import space.kscience.controls.api.ActionDescriptor +import space.kscience.controls.api.PropertyDescriptor +import kotlin.reflect.KProperty + +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD) +public annotation class Description(val content: String) + +internal expect fun PropertyDescriptor.fromSpec(property: KProperty<*>) + +internal expect fun ActionDescriptor.fromSpec(property: KProperty<*>) \ No newline at end of file diff --git a/controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt b/controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt new file mode 100644 index 0000000..cd77248 --- /dev/null +++ b/controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt @@ -0,0 +1,9 @@ +package space.kscience.controls.spec + +import space.kscience.controls.api.ActionDescriptor +import space.kscience.controls.api.PropertyDescriptor +import kotlin.reflect.KProperty + +internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>){} + +internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){} \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt new file mode 100644 index 0000000..7ae572f --- /dev/null +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt @@ -0,0 +1,18 @@ +package space.kscience.controls.spec + +import space.kscience.controls.api.ActionDescriptor +import space.kscience.controls.api.PropertyDescriptor +import kotlin.reflect.KProperty +import kotlin.reflect.full.findAnnotation + +internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) { + property.findAnnotation()?.let { + description = it.content + } +} + +internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){ + property.findAnnotation()?.let { + description = it.content + } +} \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/propertyReflection.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/propertyReflection.kt deleted file mode 100644 index 1d4cb56..0000000 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/propertyReflection.kt +++ /dev/null @@ -1,8 +0,0 @@ -package space.kscience.controls.spec - -import space.kscience.controls.api.PropertyDescriptor -import kotlin.reflect.KProperty - -internal fun PropertyDescriptor.fromSpec(property: KProperty<*>){ - property.annotations -} \ No newline at end of file diff --git a/controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt b/controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt new file mode 100644 index 0000000..1d1ccc4 --- /dev/null +++ b/controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt @@ -0,0 +1,9 @@ +package space.kscience.controls.spec + +import space.kscience.controls.api.ActionDescriptor +import space.kscience.controls.api.PropertyDescriptor +import kotlin.reflect.KProperty + +internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {} + +internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){} \ No newline at end of file From 81d6b672cfcd8bf223ea73c5866ca0cb722685bf Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 22 Nov 2023 21:55:13 +0300 Subject: [PATCH 38/71] Add compose controls to pid simulator --- .../controls/constructor/PidRegulator.kt | 22 ++- demo/constructor/build.gradle.kts | 34 ++++- demo/constructor/src/jvmMain/kotlin/main.kt | 139 ++++++++++++++---- 3 files changed, 154 insertions(+), 41 deletions(-) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt index 9fc6e18..d782e05 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt @@ -16,12 +16,22 @@ import kotlin.time.DurationUnit /** * Pid regulator parameters */ -public data class PidParameters( - public val kp: Double, - public val ki: Double, - public val kd: Double, - public val timeStep: Duration = 1.milliseconds, -) +public interface PidParameters { + public val kp: Double + public val ki: Double + public val kd: Double + public val timeStep: Duration +} + +private data class PidParametersImpl( + override val kp: Double, + override val ki: Double, + override val kd: Double, + override val timeStep: Duration, +) : PidParameters + +public fun PidParameters(kp: Double, ki: Double, kd: Double, timeStep: Duration = 1.milliseconds): PidParameters = + PidParametersImpl(kp, ki, kd, timeStep) /** * A drive with PID regulator diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index 06cb285..9026247 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -1,12 +1,13 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode plugins { id("space.kscience.gradle.mpp") - application + id("org.jetbrains.compose") version "1.5.10" } kscience { - jvm{ + jvm { withJava() } useKtor() @@ -20,8 +21,31 @@ kscience { } } -application { - mainClass.set("space.kscience.controls.demo.constructor.MainKt") +kotlin { + sourceSets { + jvmMain { + dependencies { + implementation(compose.desktop.currentOs) + } + } + } } -kotlin.explicitApi = ExplicitApiMode.Disabled \ No newline at end of file +//application { +// mainClass.set("space.kscience.controls.demo.constructor.MainKt") +//} + +kotlin.explicitApi = ExplicitApiMode.Disabled + + +compose.desktop { + application { + mainClass = "space.kscience.controls.demo.constructor.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Exe) + packageName = "PidConstructor" + packageVersion = "1.0.0" + } + } +} \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 16a6283..636bd82 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -1,15 +1,26 @@ package space.kscience.controls.demo.constructor -import kotlinx.coroutines.delay -import space.kscience.controls.api.get +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.* +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import kotlinx.coroutines.launch import space.kscience.controls.constructor.* import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.clock import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.name -import space.kscience.controls.spec.read -import space.kscience.controls.spec.write import space.kscience.controls.vision.plot import space.kscience.controls.vision.plotDeviceProperty import space.kscience.controls.vision.plotNumberState @@ -21,6 +32,7 @@ import space.kscience.plotly.models.ScatterMode import space.kscience.visionforge.plotly.PlotlyPlugin import kotlin.math.PI import kotlin.math.sin +import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit @@ -46,23 +58,15 @@ class LinearDrive( } -fun main() { - val context = Context { - plugin(DeviceManager) - plugin(PlotlyPlugin) - plugin(ClockManager) - } - - val state = DoubleRangeState(0.0, -5.0..5.0) - - val pidParameters = PidParameters( - kp = 2.5, - ki = 0.0, - kd = -0.1, - timeStep = 0.005.seconds - ) - - val device = context.install("device", LinearDrive(context, state, 0.005, pidParameters)).apply { +private fun Context.launchPidDevice( + state: DoubleRangeState, + pidParameters: PidParameters, + mass: Double, +) = launch { + val device = install( + "device", + LinearDrive(this@launchPidDevice, state, mass, pidParameters) + ).apply { val clock = context.clock val clockStart = clock.now() doRecurring(10.milliseconds) { @@ -78,7 +82,7 @@ fun main() { val maxAge = 10.seconds - context.showDashboard { + showDashboard { plot { plotNumberState(context, state, maxAge = maxAge) { name = "real position" @@ -106,13 +110,88 @@ fun main() { } } -suspend fun DeviceGroup.findEnd(): Double{ - val regulator = get("pid") as Regulator - val limitEnd = get("end") as LimitSwitch - - while(!limitEnd.read(LimitSwitch.locked)){ - delay(10.milliseconds) - regulator.write(Regulator.target, regulator.read(Regulator.position) + 0.1) +fun main() = application { + val context = Context { + plugin(DeviceManager) + plugin(PlotlyPlugin) + plugin(ClockManager) + } + + class MutablePidParameters( + kp: Double, + ki: Double, + kd: Double, + timeStep: Duration, + ) : PidParameters { + override var kp by mutableStateOf(kp) + override var ki by mutableStateOf(ki) + override var kd by mutableStateOf(kd) + override var timeStep by mutableStateOf(timeStep) + } + + val pidParameters = remember { + MutablePidParameters( + kp = 2.5, + ki = 0.0, + kd = -0.1, + timeStep = 0.005.seconds + ) + } + + context.launchPidDevice( + DoubleRangeState(0.0, -6.0..6.0), + pidParameters, + mass = 0.05 + ) + + Window(onCloseRequest = ::exitApplication) { + MaterialTheme { + Column { + Row { + Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) + TextField(pidParameters.kp.toString(),{pidParameters.kp = it.toDouble()}, enabled = false) + Slider( + pidParameters.kp.toFloat(), + { pidParameters.kp = it.toDouble()}, + valueRange = 0f..10f, + steps = 100 + ) + } + Row { + Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) + TextField(pidParameters.ki.toString(),{pidParameters.ki = it.toDouble()}, enabled = false) + + Slider( + pidParameters.ki.toFloat(), + { pidParameters.ki = it.toDouble()}, + valueRange = -5f..5f, + steps = 100 + ) + } + Row { + Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) + TextField(pidParameters.kd.toString(),{pidParameters.kd = it.toDouble()}, enabled = false) + + Slider( + pidParameters.kd.toFloat(), + { pidParameters.kd = it.toDouble()}, + valueRange = -5f..5f, + steps = 100 + ) + } + Row { + Button({ + pidParameters.run { + kp = 2.5 + ki = 0.0 + kd = -0.1 + timeStep = 0.005.seconds + } + }){ + Text("Reset") + } + } + } + } } - return regulator.read(Regulator.position) } \ No newline at end of file From 827eb6e4c1f036f04c29c941e41ace5356199421 Mon Sep 17 00:00:00 2001 From: darksnake Date: Thu, 23 Nov 2023 16:52:07 +0300 Subject: [PATCH 39/71] minor update to constructor --- demo/constructor/src/jvmMain/kotlin/main.kt | 62 ++++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 636bd82..3054399 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -123,10 +123,10 @@ fun main() = application { kd: Double, timeStep: Duration, ) : PidParameters { - override var kp by mutableStateOf(kp) - override var ki by mutableStateOf(ki) - override var kd by mutableStateOf(kd) - override var timeStep by mutableStateOf(timeStep) + override var kp by mutableStateOf(kp) + override var ki by mutableStateOf(ki) + override var kd by mutableStateOf(kd) + override var timeStep by mutableStateOf(timeStep) } val pidParameters = remember { @@ -144,38 +144,70 @@ fun main() = application { mass = 0.05 ) - Window(onCloseRequest = ::exitApplication) { + Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) { MaterialTheme { Column { Row { Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) - TextField(pidParameters.kp.toString(),{pidParameters.kp = it.toDouble()}, enabled = false) + TextField( + String.format("%.2f",pidParameters.kp), + { pidParameters.kp = it.toDouble() }, + Modifier.width(100.dp), + enabled = false + ) Slider( pidParameters.kp.toFloat(), - { pidParameters.kp = it.toDouble()}, - valueRange = 0f..10f, + { pidParameters.kp = it.toDouble() }, + valueRange = 0f..20f, steps = 100 ) } Row { Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) - TextField(pidParameters.ki.toString(),{pidParameters.ki = it.toDouble()}, enabled = false) + TextField( + String.format("%.2f",pidParameters.ki), + { pidParameters.ki = it.toDouble() }, + Modifier.width(100.dp), + enabled = false + ) Slider( pidParameters.ki.toFloat(), - { pidParameters.ki = it.toDouble()}, - valueRange = -5f..5f, + { pidParameters.ki = it.toDouble() }, + valueRange = -10f..10f, steps = 100 ) } Row { Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) - TextField(pidParameters.kd.toString(),{pidParameters.kd = it.toDouble()}, enabled = false) + TextField( + String.format("%.2f",pidParameters.kd), + { pidParameters.kd = it.toDouble() }, + Modifier.width(100.dp), + enabled = false + ) Slider( pidParameters.kd.toFloat(), - { pidParameters.kd = it.toDouble()}, - valueRange = -5f..5f, + { pidParameters.kd = it.toDouble() }, + valueRange = -10f..10f, + steps = 100 + ) + } + + Row { + Text("dt:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) + TextField( + pidParameters.timeStep.toString(DurationUnit.MILLISECONDS), + { pidParameters.timeStep = it.toDouble().milliseconds }, + Modifier.width(100.dp), + enabled = false + ) + + Slider( + pidParameters.timeStep.toDouble(DurationUnit.MILLISECONDS).toFloat(), + { pidParameters.timeStep = it.toDouble().milliseconds }, + valueRange = 0f..100f, steps = 100 ) } @@ -187,7 +219,7 @@ fun main() = application { kd = -0.1 timeStep = 0.005.seconds } - }){ + }) { Text("Reset") } } From cf129b6242ac53395a972dd5e9a859d64e186c13 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 12 Dec 2023 09:59:52 +0300 Subject: [PATCH 40/71] Migrate to DF 0.7 --- build.gradle.kts | 6 ++--- .../space/kscience/controls/api/Device.kt | 8 +++---- .../space/kscience/controls/api/Socket.kt | 3 +-- .../kscience/controls/misc/ValueWithTime.kt | 22 +++++++++++-------- .../space/kscience/controls/misc/timeIO.kt | 16 ++++++++------ .../space/kscience/controls/ports/Port.kt | 5 +++-- .../space/kscience/controls/ports/phrases.kt | 6 ++--- .../kscience/controls/spec/DeviceSpec.kt | 7 +++++- .../space/kscience/controls/spec/misc.kt | 6 ++++- .../controls/spec/propertySpecDelegates.kt | 10 ++++----- demo/constructor/build.gradle.kts | 2 +- gradle.properties | 2 +- magix/README.md | 4 ++++ 13 files changed, 57 insertions(+), 40 deletions(-) create mode 100644 magix/README.md diff --git a/build.gradle.kts b/build.gradle.kts index 2b3f6b3..fd2f3d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,15 +5,15 @@ plugins { id("space.kscience.gradle.project") } -val dataforgeVersion: String by extra("0.6.2") -val visionforgeVersion by extra("0.3.0-dev-14") +val dataforgeVersion: String by extra("0.7.1") +val visionforgeVersion by extra("0.3.0-RC") val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion) val rsocketVersion by extra("0.15.4") val xodusVersion by extra("2.0.1") allprojects { group = "space.kscience" - version = "0.3.0-dev-2" + version = "0.3.0-dev-3" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } 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 f967c89..4b78f24 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 @@ -11,8 +11,8 @@ import space.kscience.dataforge.context.info import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.misc.DFExperimental -import space.kscience.dataforge.misc.Type -import space.kscience.dataforge.names.Name +import space.kscience.dataforge.misc.DfType +import space.kscience.dataforge.names.parseAsName /** * A lifecycle state of a device @@ -46,7 +46,7 @@ public enum class DeviceLifecycleState { * [Device] is a supervisor scope encompassing all operations on a device. * When canceled, cancels all running processes. */ -@Type(DEVICE_TARGET) +@DfType(DEVICE_TARGET) public interface Device : ContextAware, CoroutineScope { /** @@ -144,7 +144,7 @@ public suspend fun Device.requestProperty(propertyName: String): Meta = if (this */ public fun CachingDevice.getAllProperties(): Meta = Meta { for (descriptor in propertyDescriptors) { - setMeta(Name.parse(descriptor.name), getProperty(descriptor.name)) + set(descriptor.name.parseAsName(), getProperty(descriptor.name)) } } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt index 02598ba..700db2c 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.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.flow.Flow @@ -9,7 +8,7 @@ import kotlinx.coroutines.launch /** * A generic bidirectional sender/receiver object */ -public interface Socket : Closeable { +public interface Socket : AutoCloseable { /** * Send an object to the socket */ diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt index ce651e4..0bf07d3 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt @@ -1,8 +1,8 @@ package space.kscience.controls.misc -import io.ktor.utils.io.core.Input -import io.ktor.utils.io.core.Output import kotlinx.datetime.Instant +import kotlinx.io.Sink +import kotlinx.io.Source import space.kscience.dataforge.io.IOFormat import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get @@ -38,15 +38,16 @@ public data class ValueWithTime(val value: T, val time: Instant) { private class ValueWithTimeIOFormat(val valueFormat: IOFormat) : IOFormat> { override val type: KType get() = typeOf>() - override fun readObject(input: Input): ValueWithTime { - val timestamp = InstantIOFormat.readObject(input) - val value = valueFormat.readObject(input) + + override fun readFrom(source: Source): ValueWithTime { + val timestamp = InstantIOFormat.readFrom(source) + val value = valueFormat.readFrom(source) return ValueWithTime(value, timestamp) } - override fun writeObject(output: Output, obj: ValueWithTime) { - InstantIOFormat.writeObject(output, obj.time) - valueFormat.writeObject(output, obj.value) + override fun writeTo(sink: Sink, obj: ValueWithTime) { + InstantIOFormat.writeTo(sink, obj.time) + valueFormat.writeTo(sink, obj.value) } } @@ -54,7 +55,10 @@ private class ValueWithTimeIOFormat(val valueFormat: IOFormat) : IOFormat< private class ValueWithTimeMetaConverter( val valueConverter: MetaConverter, ) : MetaConverter> { - override fun metaToObject( + + override val type: KType = typeOf>() + + override fun metaToObjectOrNull( meta: Meta, ): ValueWithTime? = valueConverter.metaToObject(meta[ValueWithTime.META_VALUE_KEY] ?: Meta.EMPTY)?.let { ValueWithTime(it, meta[ValueWithTime.META_TIME_KEY]?.instant ?: Instant.DISTANT_PAST) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeIO.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeIO.kt index aef7401..8686c67 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeIO.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/timeIO.kt @@ -1,7 +1,9 @@ package space.kscience.controls.misc -import io.ktor.utils.io.core.* + import kotlinx.datetime.Instant +import kotlinx.io.Sink +import kotlinx.io.Source import space.kscience.dataforge.context.Context import space.kscience.dataforge.io.IOFormat import space.kscience.dataforge.io.IOFormatFactory @@ -23,14 +25,14 @@ public object InstantIOFormat : IOFormat, IOFormatFactory { override val type: KType get() = typeOf() - override fun writeObject(output: Output, obj: Instant) { - output.writeLong(obj.epochSeconds) - output.writeInt(obj.nanosecondsOfSecond) + override fun writeTo(sink: Sink, obj: Instant) { + sink.writeLong(obj.epochSeconds) + sink.writeInt(obj.nanosecondsOfSecond) } - override fun readObject(input: Input): Instant { - val seconds = input.readLong() - val nanoseconds = input.readInt() + override fun readFrom(source: Source): Instant { + val seconds = source.readLong() + val nanoseconds = source.readInt() return Instant.fromEpochSeconds(seconds, nanoseconds) } } 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 1f07251..83044ec 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 @@ -6,7 +6,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow import space.kscience.controls.api.Socket import space.kscience.dataforge.context.* -import space.kscience.dataforge.misc.Type +import space.kscience.dataforge.misc.DfType + import kotlin.coroutines.CoroutineContext /** @@ -17,7 +18,7 @@ public interface Port : ContextAware, Socket /** * A specialized factory for [Port] */ -@Type(PortFactory.TYPE) +@DfType(PortFactory.TYPE) public interface PortFactory : Factory { public val type: String diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt index 02a0058..0f2b9bd 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt @@ -1,12 +1,10 @@ package space.kscience.controls.ports -import io.ktor.utils.io.core.BytePacketBuilder -import io.ktor.utils.io.core.readBytes -import io.ktor.utils.io.core.reset import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.transform +import kotlinx.io.Buffer /** * Transform byte fragments into complete phrases using given delimiter. Not thread safe. @@ -14,7 +12,7 @@ import kotlinx.coroutines.flow.transform public fun Flow.withDelimiter(delimiter: ByteArray): Flow { require(delimiter.isNotEmpty()) { "Delimiter must not be empty" } - val output = BytePacketBuilder() + val output = Buffer() var matcherPosition = 0 onCompletion { 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 cd0d7cc..4091921 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 @@ -9,9 +9,14 @@ import space.kscience.dataforge.meta.transformations.MetaConverter import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty +import kotlin.reflect.KType +import kotlin.reflect.typeOf public object UnitMetaConverter : MetaConverter { - override fun metaToObject(meta: Meta): Unit = Unit + + override val type: KType = typeOf() + + override fun metaToObjectOrNull(meta: Meta): Unit = Unit override fun objectToMeta(obj: Unit): Meta = Meta.EMPTY } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt index e264212..ee14237 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt @@ -2,6 +2,8 @@ package space.kscience.controls.spec import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.transformations.MetaConverter +import kotlin.reflect.KType +import kotlin.reflect.typeOf import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -10,7 +12,9 @@ public fun Double.asMeta(): Meta = Meta(asValue()) //TODO to be moved to DF public object DurationConverter : MetaConverter { - override fun metaToObject(meta: Meta): Duration = meta.value?.double?.toDuration(DurationUnit.SECONDS) + override val type: KType = typeOf() + + override fun metaToObjectOrNull(meta: Meta): Duration = meta.value?.double?.toDuration(DurationUnit.SECONDS) ?: run { val unit: DurationUnit = meta["unit"].enum() ?: DurationUnit.SECONDS val value = meta[Meta.VALUE_KEY].double ?: error("No value present for Duration") 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 6f599a8..231891c 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 @@ -68,7 +68,7 @@ public fun DeviceSpec.booleanProperty( MetaConverter.boolean, { metaDescriptor { - type(ValueType.BOOLEAN) + valueType(ValueType.BOOLEAN) } descriptorBuilder() }, @@ -80,7 +80,7 @@ private inline fun numberDescriptor( crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {} ): PropertyDescriptor.() -> Unit = { metaDescriptor { - type(ValueType.NUMBER) + valueType(ValueType.NUMBER) } descriptorBuilder() } @@ -115,7 +115,7 @@ public fun DeviceSpec.stringProperty( MetaConverter.string, { metaDescriptor { - type(ValueType.STRING) + valueType(ValueType.STRING) } descriptorBuilder() }, @@ -131,7 +131,7 @@ public fun DeviceSpec.metaProperty( MetaConverter.meta, { metaDescriptor { - type(ValueType.STRING) + valueType(ValueType.STRING) } descriptorBuilder() }, @@ -151,7 +151,7 @@ public fun DeviceSpec.booleanProperty( MetaConverter.boolean, { metaDescriptor { - type(ValueType.BOOLEAN) + valueType(ValueType.BOOLEAN) } descriptorBuilder() }, diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index 9026247..470533f 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode plugins { id("space.kscience.gradle.mpp") - id("org.jetbrains.compose") version "1.5.10" + id("org.jetbrains.compose") version "1.5.11" } kscience { diff --git a/gradle.properties b/gradle.properties index ea080f4..717cec4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,4 +7,4 @@ org.gradle.parallel=true org.gradle.configureondemand=true org.gradle.jvmargs=-Xmx4096m -toolsVersion=0.15.0-kotlin-1.9.20 \ No newline at end of file +toolsVersion=0.15.2-kotlin-1.9.21 \ No newline at end of file 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 + + + From fb03fcc982b92be102a8047137aa41ea1d11c12b Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 13 Dec 2023 12:29:06 +0300 Subject: [PATCH 41/71] Finish migration to kotlinx-io --- .../controls/constructor/DeviceState.kt | 2 +- controls-core/build.gradle.kts | 6 +++- .../space/kscience/controls/ports/Port.kt | 3 +- .../kscience/controls/ports/ioExtensions.kt | 5 +++ .../space/kscience/controls/ports/phrases.kt | 6 ++-- .../kscience/controls/ports/ChannelPort.kt | 18 +++++----- .../kscience/controls/ports/PortIOTest.kt | 35 +++++++++++++++---- .../controls/modbus/DeviceProcessImage.kt | 30 ++++++++-------- .../kscience/controls/modbus/ModbusDevice.kt | 30 +++++++++------- .../controls/opcua/client/MetaBsdParser.kt | 2 +- .../controls/opcua/client/OpcUaDevice.kt | 2 +- .../jsMain/kotlin/ControlsVisionPlugin.js.kt | 3 +- .../kscience/controls/demo/DemoDevice.kt | 2 +- .../kscience/controls/demo/car/VirtualCar.kt | 10 ++++-- .../mks/NullableStringMetaConverter.kt | 10 ------ magix/magix-api/build.gradle.kts | 4 +++ 16 files changed, 104 insertions(+), 64 deletions(-) create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/ports/ioExtensions.kt delete mode 100644 demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/NullableStringMetaConverter.kt diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index ed7027a..a9cff51 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -40,7 +40,7 @@ public interface MutableDeviceState : DeviceState { public var MutableDeviceState.valueAsMeta: Meta get() = converter.objectToMeta(value) set(arg) { - value = converter.metaToObject(arg) ?: error("Conversion for meta $arg to property type with $converter failed") + value = converter.metaToObject(arg) } /** diff --git a/controls-core/build.gradle.kts b/controls-core/build.gradle.kts index bbe32eb..859b067 100644 --- a/controls-core/build.gradle.kts +++ b/controls-core/build.gradle.kts @@ -20,10 +20,14 @@ kscience { json() } useContextReceivers() - dependencies { + commonMain { api("space.kscience:dataforge-io:$dataforgeVersion") api(spclibs.kotlinx.datetime) } + + jvmTest{ + implementation(spclibs.logback.classic) + } } 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 83044ec..b17de92 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 @@ -38,7 +38,7 @@ public abstract class AbstractPort( protected val scope: CoroutineScope = CoroutineScope(coroutineContext + SupervisorJob(coroutineContext[Job])) private val outgoing = Channel(100) - private val incoming = Channel(Channel.CONFLATED) + private val incoming = Channel(100) init { scope.coroutineContext[Job]?.invokeOnCompletion { @@ -88,7 +88,6 @@ public abstract class AbstractPort( override fun close() { outgoing.close() incoming.close() - sendJob.cancel() scope.cancel() } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/ioExtensions.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/ioExtensions.kt new file mode 100644 index 0000000..dbe7256 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/ioExtensions.kt @@ -0,0 +1,5 @@ +package space.kscience.controls.ports + +import space.kscience.dataforge.io.Binary + +public fun Binary.readShort(position: Int): Short = read(position) { readShort() } \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt index 0f2b9bd..732e5d4 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.transform import kotlinx.io.Buffer +import kotlinx.io.readByteArray /** * Transform byte fragments into complete phrases using given delimiter. Not thread safe. @@ -27,9 +28,8 @@ public fun Flow.withDelimiter(delimiter: ByteArray): Flow matcherPosition++ if (matcherPosition == delimiter.size) { //full match achieved, sending result - val bytes = output.build() - emit(bytes.readBytes()) - output.reset() + emit(output.readByteArray()) + output.clear() matcherPosition = 0 } } else if (matcherPosition > 0) { diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt index d7983f2..1e187b6 100644 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt @@ -8,6 +8,7 @@ import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.* import java.net.InetSocketAddress import java.nio.ByteBuffer +import java.nio.channels.AsynchronousCloseException import java.nio.channels.ByteChannel import java.nio.channels.DatagramChannel import java.nio.channels.SocketChannel @@ -30,7 +31,7 @@ public class ChannelPort( channelBuilder: suspend () -> ByteChannel, ) : AbstractPort(context, coroutineContext), AutoCloseable { - private val futureChannel: Deferred = this.scope.async(Dispatchers.IO) { + private val futureChannel: Deferred = scope.async(Dispatchers.IO) { channelBuilder() } @@ -39,10 +40,10 @@ public class ChannelPort( */ public val startJob: Job get() = futureChannel - private val listenerJob = this.scope.launch(Dispatchers.IO) { + private val listenerJob = scope.launch(Dispatchers.IO) { val channel = futureChannel.await() val buffer = ByteBuffer.allocate(1024) - while (isActive) { + while (isActive && channel.isOpen) { try { val num = channel.read(buffer) if (num > 0) { @@ -50,8 +51,12 @@ public class ChannelPort( } if (num < 0) cancel("The input channel is exhausted") } catch (ex: Exception) { - logger.error(ex) { "Channel read error" } - delay(1000) + if(ex is AsynchronousCloseException){ + logger.info { "Channel closed" } + } else { + logger.error(ex) { "Channel read error, retrying in 1 second" } + delay(1000) + } } } } @@ -62,11 +67,8 @@ public class ChannelPort( @OptIn(ExperimentalCoroutinesApi::class) override fun close() { - listenerJob.cancel() if (futureChannel.isCompleted) { futureChannel.getCompleted().close() - } else { - futureChannel.cancel() } super.close() } diff --git a/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt b/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt index bdf6891..3f92500 100644 --- a/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt +++ b/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt @@ -1,25 +1,46 @@ package space.kscience.controls.ports -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.flow.* import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test +import space.kscience.dataforge.context.Global import kotlin.test.assertEquals -internal class PortIOTest{ +internal class PortIOTest { @Test - fun testDelimiteredByteArrayFlow(){ - val flow = flowOf("bb?b","ddd?",":defgb?:ddf","34fb?:--").map { it.encodeToByteArray() } + fun testDelimiteredByteArrayFlow() { + val flow = flowOf("bb?b", "ddd?", ":defgb?:ddf", "34fb?:--").map { it.encodeToByteArray() } val chunked = flow.withDelimiter("?:".encodeToByteArray()) runBlocking { val result = chunked.toList() assertEquals(3, result.size) - assertEquals("bb?bddd?:",result[0].decodeToString()) + assertEquals("bb?bddd?:", result[0].decodeToString()) assertEquals("defgb?:", result[1].decodeToString()) assertEquals("ddf34fb?:", result[2].decodeToString()) } } + + @Test + fun testUdpCommunication() = runTest { + val receiver = UdpPort.open(Global, "localhost", 8811, localPort = 8812) + val sender = UdpPort.open(Global, "localhost", 8812, localPort = 8811) + + repeat(10) { + sender.send("Line number $it\n") + } + + val res = receiver + .receiving() + .onEach { println("ARRAY: ${it.decodeToString()}") } + .withStringDelimiter("\n") + .onEach { println("LINE: $it") } + .take(10).toList() + + assertEquals("Line number 3", res[3].trim()) + receiver.close() + sender.close() + } } \ No newline at end of file diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt index 8f4a2b4..3b3f91b 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt @@ -1,12 +1,14 @@ package space.kscience.controls.modbus import com.ghgande.j2mod.modbus.procimg.* -import io.ktor.utils.io.core.* import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.io.Buffer import space.kscience.controls.api.Device +import space.kscience.controls.ports.readShort import space.kscience.controls.spec.* +import space.kscience.dataforge.io.Binary public class DeviceProcessImageBuilder internal constructor( @@ -106,11 +108,11 @@ public class DeviceProcessImageBuilder internal constructor( } device.useProperty(propertySpec) { value -> - val packet = buildPacket { - key.format.writeObject(this, value) - }.readByteBuffer() + val binary = Binary { + key.format.writeTo(this, value) + } registers.forEachIndexed { index, register -> - register.setValue(packet.getShort(index * 2)) + register.setValue(binary.readShort(index * 2)) } } } @@ -118,7 +120,7 @@ public class DeviceProcessImageBuilder internal constructor( /** * Trigger [block] if one of register changes. */ - private fun List.onChange(block: suspend (ByteReadPacket) -> Unit) { + private fun List.onChange(block: suspend (Buffer) -> Unit) { var ready = false forEach { register -> @@ -128,7 +130,7 @@ public class DeviceProcessImageBuilder internal constructor( } device.launch { - val builder = BytePacketBuilder() + val builder = Buffer() while (isActive) { delay(1) if (ready) { @@ -136,7 +138,7 @@ public class DeviceProcessImageBuilder internal constructor( forEach { value -> writeShort(value.toShort()) } - }.build() + } block(packet) ready = false } @@ -154,15 +156,15 @@ public class DeviceProcessImageBuilder internal constructor( } registers.onChange { packet -> - device.write(propertySpec, key.format.readObject(packet)) + device.write(propertySpec, key.format.readFrom(packet)) } device.useProperty(propertySpec) { value -> - val packet = buildPacket { - key.format.writeObject(this, value) - }.readByteBuffer() + val binary = Binary { + key.format.writeTo(this, value) + } registers.forEachIndexed { index, observableRegister -> - observableRegister.setValue(packet.getShort(index * 2)) + observableRegister.setValue(binary.readShort(index * 2)) } } } @@ -212,7 +214,7 @@ public class DeviceProcessImageBuilder internal constructor( registers.onChange { packet -> device.launch { - device.action(key.format.readObject(packet)) + device.action(key.format.readFrom(packet)) } } 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 index 7297616..2585302 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt @@ -5,11 +5,10 @@ import com.ghgande.j2mod.modbus.procimg.InputRegister import com.ghgande.j2mod.modbus.procimg.Register import com.ghgande.j2mod.modbus.procimg.SimpleInputRegister import com.ghgande.j2mod.modbus.util.BitVector -import io.ktor.utils.io.core.ByteReadPacket -import io.ktor.utils.io.core.buildPacket -import io.ktor.utils.io.core.readByteBuffer -import io.ktor.utils.io.core.writeShort +import kotlinx.io.Buffer import space.kscience.controls.api.Device +import space.kscience.dataforge.io.Buffer +import space.kscience.dataforge.io.ByteArray import java.nio.ByteBuffer import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -45,7 +44,7 @@ public interface ModbusDevice : Device { public operator fun ModbusRegistryKey.InputRange.getValue(thisRef: Any?, property: KProperty<*>): T { val packet = readInputRegistersToPacket(address, count) - return format.readObject(packet) + return format.readFrom(packet) } @@ -62,7 +61,7 @@ public interface ModbusDevice : Device { public operator fun ModbusRegistryKey.HoldingRange.getValue(thisRef: Any?, property: KProperty<*>): T { val packet = readHoldingRegistersToPacket(address, count) - return format.readObject(packet) + return format.readFrom(packet) } public operator fun ModbusRegistryKey.HoldingRange.setValue( @@ -70,9 +69,9 @@ public interface ModbusDevice : Device { property: KProperty<*>, value: T, ) { - val buffer = buildPacket { - format.writeObject(this, value) - }.readByteBuffer() + val buffer = ByteArray { + format.writeTo(this, value) + } writeHoldingRegisters(address, buffer) } @@ -122,7 +121,7 @@ private fun Array.toBuffer(): ByteBuffer { return buffer } -private fun Array.toPacket(): ByteReadPacket = buildPacket { +private fun Array.toPacket(): Buffer = Buffer { forEach { value -> writeShort(value.toShort()) } @@ -131,7 +130,7 @@ private fun Array.toPacket(): ByteReadPacket = buildPacket { public fun ModbusDevice.readInputRegistersToBuffer(address: Int, count: Int): ByteBuffer = master.readInputRegisters(unitId, address, count).toBuffer() -public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): ByteReadPacket = +public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): Buffer = master.readInputRegisters(unitId, address, count).toPacket() public fun ModbusDevice.readDoubleInput(address: Int): Double = @@ -151,7 +150,7 @@ public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List = object : ReadWriteProperty { diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt index b760184..27330bb 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt @@ -107,7 +107,7 @@ internal class MetaStructureCodec( override fun createStructure(name: String, members: LinkedHashMap): Meta = Meta { members.forEach { (property: String, value: Meta?) -> - setMeta(Name.parse(property), value) + set(Name.parse(property), value) } } diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDevice.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDevice.kt index dd83e58..51e6032 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDevice.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDevice.kt @@ -43,7 +43,7 @@ public suspend inline fun OpcUaDevice.readOpcWithTime( else -> error("Incompatible OPC property value $content") } - val res: T = converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}") + val res: T = converter.metaToObject(meta) return res to time } diff --git a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt index 0d928ac..b34cc44 100644 --- a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt +++ b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt @@ -9,6 +9,7 @@ import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.Name import space.kscience.visionforge.ElementVisionRenderer import space.kscience.visionforge.Vision +import space.kscience.visionforge.VisionClient import space.kscience.visionforge.VisionPlugin public actual class ControlVisionPlugin : VisionPlugin(), ElementVisionRenderer { @@ -20,7 +21,7 @@ public actual class ControlVisionPlugin : VisionPlugin(), ElementVisionRenderer TODO("Not yet implemented") } - override fun render(element: Element, name: Name, vision: Vision, meta: Meta) { + override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) { TODO("Not yet implemented") } 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 eaa1bff..067792c 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 @@ -42,7 +42,7 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(Compa // register virtual properties based on actual object state val timeScale by mutableProperty(MetaConverter.double, IDemoDevice::timeScaleState) { metaDescriptor { - type(ValueType.NUMBER) + valueType(ValueType.NUMBER) } description = "Real to virtual time scale" } 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 8ccb8a4..05bb2f0 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 @@ -17,6 +17,8 @@ import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.transformations.MetaConverter import kotlin.math.pow +import kotlin.reflect.KType +import kotlin.reflect.typeOf import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime @@ -28,7 +30,10 @@ data class Vector2D(var x: Double = 0.0, var y: Double = 0.0) : MetaRepr { operator fun div(arg: Double): Vector2D = Vector2D(x / arg, y / arg) companion object CoordinatesMetaConverter : MetaConverter { - override fun metaToObject(meta: Meta): Vector2D = Vector2D( + + override val type: KType = typeOf() + + override fun metaToObjectOrNull(meta: Meta): Vector2D = Vector2D( meta["x"].double ?: 0.0, meta["y"].double ?: 0.0 ) @@ -40,7 +45,8 @@ data class Vector2D(var x: Double = 0.0, var y: Double = 0.0) : MetaRepr { } } -open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec(IVirtualCar, context, meta), IVirtualCar { +open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec(IVirtualCar, context, meta), + IVirtualCar { private val clock = context.clock private val timeScale = 1e-3 diff --git a/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/NullableStringMetaConverter.kt b/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/NullableStringMetaConverter.kt deleted file mode 100644 index 40c20ed..0000000 --- a/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/NullableStringMetaConverter.kt +++ /dev/null @@ -1,10 +0,0 @@ -package center.sciprog.devices.mks - -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.string -import space.kscience.dataforge.meta.transformations.MetaConverter - -object NullableStringMetaConverter : MetaConverter { - override fun metaToObject(meta: Meta): String? = meta.string - override fun objectToMeta(obj: String?): Meta = Meta {} -} \ No newline at end of file diff --git a/magix/magix-api/build.gradle.kts b/magix/magix-api/build.gradle.kts index 159989d..68151c3 100644 --- a/magix/magix-api/build.gradle.kts +++ b/magix/magix-api/build.gradle.kts @@ -17,6 +17,10 @@ kscience { useSerialization{ json() } + + commonMain{ + implementation(spclibs.atomicfu) + } } readme{ From 606c2cf5b14401648eef6d36a44711d1ddbec8bc Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 13 Dec 2023 14:50:56 +0300 Subject: [PATCH 42/71] Finish migration to kotlinx-io --- .../jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt index 1e187b6..ec50d47 100644 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt @@ -52,7 +52,7 @@ public class ChannelPort( if (num < 0) cancel("The input channel is exhausted") } catch (ex: Exception) { if(ex is AsynchronousCloseException){ - logger.info { "Channel closed" } + logger.info { "Channel $channel closed" } } else { logger.error(ex) { "Channel read error, retrying in 1 second" } delay(1000) From a12cf440e8a7bd686f2ae69d6f526f3863c61d09 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 13 Dec 2023 20:20:03 +0300 Subject: [PATCH 43/71] Finish migration to kotlinx-io --- build.gradle.kts | 2 +- .../space/kscience/controls/ports/ChannelPort.kt | 6 +++--- .../space/kscience/controls/ports/PortIOTest.kt | 16 ++++++++++------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index fd2f3d8..d6ab9cf 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.3.0-dev-3" + version = "0.3.0-dev-4" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt index ec50d47..f450607 100644 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt @@ -51,7 +51,7 @@ public class ChannelPort( } if (num < 0) cancel("The input channel is exhausted") } catch (ex: Exception) { - if(ex is AsynchronousCloseException){ + if (ex is AsynchronousCloseException) { logger.info { "Channel $channel closed" } } else { logger.error(ex) { "Channel read error, retrying in 1 second" } @@ -108,7 +108,7 @@ public object UdpPort : PortFactory { /** * Connect a datagram channel to a remote host/port. If [localPort] is provided, it is used to bind local port for receiving messages. */ - public fun open( + public fun openChannel( context: Context, remoteHost: String, remotePort: Int, @@ -130,6 +130,6 @@ public object UdpPort : PortFactory { val remotePort by meta.number { error("Remote port is not specified") } val localHost: String? by meta.string() val localPort: Int? by meta.int() - return open(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost") + return openChannel(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost") } } \ No newline at end of file diff --git a/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt b/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt index 3f92500..daa6173 100644 --- a/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt +++ b/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt @@ -1,6 +1,10 @@ package space.kscience.controls.ports -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test @@ -25,19 +29,19 @@ internal class PortIOTest { @Test fun testUdpCommunication() = runTest { - val receiver = UdpPort.open(Global, "localhost", 8811, localPort = 8812) - val sender = UdpPort.open(Global, "localhost", 8812, localPort = 8811) + val receiver = UdpPort.openChannel(Global, "localhost", 8811, localPort = 8812) + val sender = UdpPort.openChannel(Global, "localhost", 8812, localPort = 8811) + delay(30) repeat(10) { sender.send("Line number $it\n") } val res = receiver .receiving() - .onEach { println("ARRAY: ${it.decodeToString()}") } .withStringDelimiter("\n") - .onEach { println("LINE: $it") } - .take(10).toList() + .take(10) + .toList() assertEquals("Line number 3", res[3].trim()) receiver.close() From 701ea8cf57928f58c2c8bc9276b7fe80b5b73474 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 15 Dec 2023 16:55:56 +0300 Subject: [PATCH 44/71] Minor fixes to port implementations --- .../kscience/controls/ports/UdpSocketPort.kt | 55 +++++++++++++++++++ .../kscience/controls/ports/KtorTcpPort.kt | 7 ++- .../kscience/controls/ports/KtorUdpPort.kt | 30 +++++----- controls-serial/build.gradle.kts | 2 +- .../controls/serial/JSerialCommPort.kt | 2 +- 5 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/UdpSocketPort.kt diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/UdpSocketPort.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/UdpSocketPort.kt new file mode 100644 index 0000000..27bb0d8 --- /dev/null +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/UdpSocketPort.kt @@ -0,0 +1,55 @@ +package space.kscience.controls.ports + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import space.kscience.dataforge.context.Context +import java.net.DatagramPacket +import java.net.DatagramSocket +import kotlin.coroutines.CoroutineContext + +/** + * A port based on [DatagramSocket] for cases, where [ChannelPort] does not work for some reason + */ +public class UdpSocketPort( + override val context: Context, + private val socket: DatagramSocket, + coroutineContext: CoroutineContext = context.coroutineContext, +) : AbstractPort(context, coroutineContext) { + + private val listenerJob = context.launch(Dispatchers.IO) { + while (isActive) { + val buf = ByteArray(socket.receiveBufferSize) + + val packet = DatagramPacket( + buf, + buf.size, + ) + socket.receive(packet) + + val bytes = packet.data.copyOfRange( + packet.offset, + packet.offset + packet.length + ) + receive(bytes) + } + } + + override fun close() { + listenerJob.cancel() + } + + override fun isOpen(): Boolean = listenerJob.isActive + + + override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) { + val packet = DatagramPacket( + data, + data.size, + socket.remoteSocketAddress + ) + socket.send(packet) + } + +} \ No newline at end of file diff --git a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt b/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt index 7f906d3..a7a698e 100644 --- a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt +++ b/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt @@ -1,6 +1,7 @@ package space.kscience.controls.ports import io.ktor.network.selector.ActorSelectorManager +import io.ktor.network.sockets.SocketOptions import io.ktor.network.sockets.aSocket import io.ktor.network.sockets.openReadChannel import io.ktor.network.sockets.openWriteChannel @@ -23,12 +24,13 @@ public class KtorTcpPort internal constructor( public val host: String, public val port: Int, coroutineContext: CoroutineContext = context.coroutineContext, + socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {} ) : AbstractPort(context, coroutineContext), Closeable { override fun toString(): String = "port[tcp:$host:$port]" private val futureSocket = scope.async { - aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(host, port) + aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(host, port, socketOptions) } private val writeChannel = scope.async { @@ -64,8 +66,9 @@ public class KtorTcpPort internal constructor( host: String, port: Int, coroutineContext: CoroutineContext = context.coroutineContext, + socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {} ): KtorTcpPort { - return KtorTcpPort(context, host, port, coroutineContext) + return KtorTcpPort(context, host, port, coroutineContext, socketOptions) } override fun build(context: Context, meta: Meta): Port { diff --git a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt b/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt index 8b8446c..1f30914 100644 --- a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt +++ b/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt @@ -1,17 +1,12 @@ package space.kscience.controls.ports import io.ktor.network.selector.ActorSelectorManager -import io.ktor.network.sockets.InetSocketAddress -import io.ktor.network.sockets.aSocket -import io.ktor.network.sockets.openReadChannel -import io.ktor.network.sockets.openWriteChannel +import io.ktor.network.sockets.* +import io.ktor.utils.io.ByteWriteChannel import io.ktor.utils.io.consumeEachBufferRange import io.ktor.utils.io.core.Closeable import io.ktor.utils.io.writeAvailable -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.int @@ -26,6 +21,7 @@ public class KtorUdpPort internal constructor( public val localPort: Int? = null, public val localHost: String = "localhost", coroutineContext: CoroutineContext = context.coroutineContext, + socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {} ) : AbstractPort(context, coroutineContext), Closeable { override fun toString(): String = "port[udp:$remoteHost:$remotePort]" @@ -33,11 +29,12 @@ public class KtorUdpPort internal constructor( private val futureSocket = scope.async { aSocket(ActorSelectorManager(Dispatchers.IO)).udp().connect( remoteAddress = InetSocketAddress(remoteHost, remotePort), - localAddress = localPort?.let { InetSocketAddress(localHost, localPort) } + localAddress = localPort?.let { InetSocketAddress(localHost, localPort) }, + configure = socketOptions ) } - private val writeChannel = scope.async { + private val writeChannel: Deferred = scope.async { futureSocket.await().openWriteChannel(true) } @@ -72,9 +69,16 @@ public class KtorUdpPort internal constructor( localPort: Int? = null, localHost: String = "localhost", coroutineContext: CoroutineContext = context.coroutineContext, - ): KtorUdpPort { - return KtorUdpPort(context, remoteHost, remotePort, localPort, localHost, coroutineContext) - } + socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {} + ): KtorUdpPort = KtorUdpPort( + context = context, + remoteHost = remoteHost, + remotePort = remotePort, + localPort = localPort, + localHost = localHost, + coroutineContext = coroutineContext, + socketOptions = socketOptions + ) override fun build(context: Context, meta: Meta): Port { val remoteHost by meta.string { error("Remote host is not specified") } diff --git a/controls-serial/build.gradle.kts b/controls-serial/build.gradle.kts index a9afc41..aa950e1 100644 --- a/controls-serial/build.gradle.kts +++ b/controls-serial/build.gradle.kts @@ -9,7 +9,7 @@ description = "Implementation of direct serial port communication with JSerialCo dependencies{ api(project(":controls-core")) - implementation("com.fazecast:jSerialComm:2.10.3") + implementation("com.fazecast:jSerialComm:2.10.4") } readme{ diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/JSerialCommPort.kt b/controls-serial/src/main/kotlin/space/kscience/controls/serial/JSerialCommPort.kt index 3e0601c..8a2caab 100644 --- a/controls-serial/src/main/kotlin/space/kscience/controls/serial/JSerialCommPort.kt +++ b/controls-serial/src/main/kotlin/space/kscience/controls/serial/JSerialCommPort.kt @@ -28,7 +28,7 @@ public class JSerialCommPort( override fun getListeningEvents(): Int = SerialPort.LISTENING_EVENT_DATA_AVAILABLE override fun serialEvent(event: SerialPortEvent) { - if (event.eventType == SerialPort.LISTENING_EVENT_DATA_AVAILABLE) { + if (event.eventType == SerialPort.LISTENING_EVENT_DATA_AVAILABLE && event.receivedData != null) { scope.launch { receive(event.receivedData) } } } From bec075328b3075ea4ac5c27282d0972e52580e23 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 22 Dec 2023 09:28:39 +0300 Subject: [PATCH 45/71] Make constructor device use context instead of device manager --- build.gradle.kts | 2 +- .../controls/constructor/DeviceConstructor.kt | 6 +- .../controls/constructor/DeviceGroup.kt | 11 ++-- .../space/kscience/controls/ports/phrases.kt | 2 + .../controls/spec/DevicePropertySpec.kt | 2 +- .../src/commonMain/kotlin/plotExtensions.kt | 66 ++++++++++++------- demo/constructor/src/jvmMain/kotlin/main.kt | 3 +- 7 files changed, 55 insertions(+), 37 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index d6ab9cf..6c169fe 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.3.0-dev-4" + version = "0.3.0-dev-5" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt index 01d9087..1eb3b91 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt @@ -2,7 +2,7 @@ package space.kscience.controls.constructor import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor -import space.kscience.controls.manager.DeviceManager +import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.transformations.MetaConverter @@ -18,9 +18,9 @@ import kotlin.time.Duration * A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices */ public abstract class DeviceConstructor( - deviceManager: DeviceManager, + context: Context, meta: Meta, -) : DeviceGroup(deviceManager, meta) { +) : DeviceGroup(context, meta) { /** * Register a device, provided by a given [factory] and diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt index 68f8301..cea9d71 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -27,7 +27,7 @@ import kotlin.coroutines.CoroutineContext * A mutable group of devices and properties to be used for lightweight design and simulations. */ public open class DeviceGroup( - public val deviceManager: DeviceManager, + final override val context: Context, override val meta: Meta, ) : DeviceHub, CachingDevice { @@ -42,9 +42,6 @@ public open class DeviceGroup( ) - override final val context: Context get() = deviceManager.context - - private val sharedMessageFlow = MutableSharedFlow() override val messageFlow: Flow @@ -175,7 +172,7 @@ public fun DeviceManager.registerDeviceGroup( meta: Meta = Meta.EMPTY, block: DeviceGroup.() -> Unit, ): DeviceGroup { - val group = DeviceGroup(this, meta).apply(block) + val group = DeviceGroup(context, meta).apply(block) install(name, group) return group } @@ -194,7 +191,7 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { when (val d = devices[token]) { null -> install( token, - DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY) + DeviceGroup(context, meta[token] ?: Meta.EMPTY) ) else -> (d as? DeviceGroup) ?: error("Device $name is not a DeviceGroup") @@ -234,7 +231,7 @@ public fun DeviceGroup.install( deviceMeta: Meta? = null, metaLocation: Name = name, ): D { - val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[metaLocation])) + val newDevice = factory.build(context, Laminate(deviceMeta, meta[metaLocation])) install(name, newDevice) return newDevice } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt index 732e5d4..b86591f 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt @@ -9,6 +9,8 @@ import kotlinx.io.readByteArray /** * Transform byte fragments into complete phrases using given delimiter. Not thread safe. + * + * TODO add type wrapper for phrases */ public fun Flow.withDelimiter(delimiter: ByteArray): Flow { require(delimiter.isNotEmpty()) { "Delimiter must not be 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 cb511ea..8e0d6de 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 @@ -81,7 +81,7 @@ public suspend fun D.read(propertySpec: DevicePropertySpec public suspend fun > D.readOrNull(propertySpec: DevicePropertySpec): T? = readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject) -public suspend fun D.request(propertySpec: DevicePropertySpec): T? = +public suspend fun D.request(propertySpec: DevicePropertySpec): T = propertySpec.converter.metaToObject(requestProperty(propertySpec.name)) /** diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index 3640ee9..8c7b02f 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -18,6 +18,8 @@ import space.kscience.controls.api.propertyMessageFlow import space.kscience.controls.constructor.DeviceState import space.kscience.controls.manager.clock import space.kscience.controls.misc.ValueWithTime +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.name import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.* import space.kscience.plotly.Plot @@ -28,8 +30,8 @@ import space.kscience.plotly.models.Trace import space.kscience.plotly.models.TraceValues import space.kscience.plotly.scatter import kotlin.time.Duration -import kotlin.time.Duration.Companion.hours -import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds private var TraceValues.values: List get() = value?.list ?: emptyList() @@ -82,6 +84,11 @@ private class TimeData(private var points: MutableList> = m } } +private val defaultMaxAge get() = 10.minutes +private val defaultMaxPoints get() = 800 +private val defaultMinPoints get() = 400 +private val defaultSampling get() = 1.seconds + /** * Add a trace that shows a [Device] property change over time. Show only latest [maxPoints] . * @return a [Job] that handles the listener @@ -90,10 +97,10 @@ public fun Plot.plotDeviceProperty( device: Device, propertyName: String, extractValue: Meta.() -> Value = { value ?: Null }, - maxAge: Duration = 1.hours, - maxPoints: Int = 800, - minPoints: Int = 400, - sampling: Duration = 10.milliseconds, + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + sampling: Duration = defaultSampling, coroutineScope: CoroutineScope = device.context, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { @@ -108,14 +115,27 @@ public fun Plot.plotDeviceProperty( }.launchIn(coroutineScope) } +public fun Plot.plotDeviceProperty( + device: Device, + property: DevicePropertySpec<*, Number>, + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + sampling: Duration = defaultSampling, + coroutineScope: CoroutineScope = device.context, + configuration: Scatter.() -> Unit = {}, +): Job = plotDeviceProperty( + device, property.name, { value ?: Null }, maxAge, maxPoints, minPoints, sampling, coroutineScope, configuration +) + private fun Trace.updateFromState( context: Context, state: DeviceState, - extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: space.kscience.dataforge.meta.Null }, - maxAge: Duration = 1.hours, - maxPoints: Int = 800, - minPoints: Int = 400, - sampling: Duration = 10.milliseconds, + extractValue: T.() -> Value, + maxAge: Duration, + maxPoints: Int, + minPoints: Int, + sampling: Duration, ): Job { val clock = context.clock val data = TimeData() @@ -131,10 +151,10 @@ public fun Plot.plotDeviceState( context: Context, state: DeviceState, extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: Null }, - maxAge: Duration = 1.hours, - maxPoints: Int = 800, - minPoints: Int = 400, - sampling: Duration = 10.milliseconds, + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + sampling: Duration = defaultSampling, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { updateFromState(context, state, extractValue, maxAge, maxPoints, minPoints, sampling) @@ -144,10 +164,10 @@ public fun Plot.plotDeviceState( public fun Plot.plotNumberState( context: Context, state: DeviceState, - maxAge: Duration = 1.hours, - maxPoints: Int = 800, - minPoints: Int = 400, - sampling: Duration = 10.milliseconds, + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + sampling: Duration = defaultSampling, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, sampling) @@ -157,10 +177,10 @@ public fun Plot.plotNumberState( public fun Plot.plotBooleanState( context: Context, state: DeviceState, - maxAge: Duration = 1.hours, - maxPoints: Int = 800, - minPoints: Int = 400, - sampling: Duration = 10.milliseconds, + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + sampling: Duration = defaultSampling, configuration: Bar.() -> Unit = {}, ): Job = bar(configuration).run { updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, sampling) diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 3054399..5cbef67 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -26,7 +26,6 @@ import space.kscience.controls.vision.plotDeviceProperty import space.kscience.controls.vision.plotNumberState import space.kscience.controls.vision.showDashboard import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.request import space.kscience.dataforge.meta.Meta import space.kscience.plotly.models.ScatterMode import space.kscience.visionforge.plotly.PlotlyPlugin @@ -44,7 +43,7 @@ class LinearDrive( mass: Double, pidParameters: PidParameters, meta: Meta = Meta.EMPTY, -) : DeviceConstructor(context.request(DeviceManager), meta) { +) : DeviceConstructor(context, meta) { val drive by device(VirtualDrive.factory(mass, state)) val pid by device(PidRegulator(drive, pidParameters)) From 34f9108ef7901afa7c7ef0eabdba8fd2ae429f5e Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 25 Dec 2023 19:09:40 +0300 Subject: [PATCH 46/71] New builders for devices --- build.gradle.kts | 4 +-- .../controls/constructor/DeviceConstructor.kt | 28 +++++++++++---- .../controls/constructor/DeviceGroup.kt | 20 +++++------ .../controls/constructor/DeviceState.kt | 36 +++++++++++++++++-- .../space/kscience/controls/api/Device.kt | 7 ++++ .../controls/manager/DeviceManager.kt | 3 ++ 6 files changed, 76 insertions(+), 22 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6c169fe..aebfc4f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,14 +6,14 @@ plugins { } val dataforgeVersion: String by extra("0.7.1") -val visionforgeVersion by extra("0.3.0-RC") +val visionforgeVersion by extra("0.3.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") allprojects { group = "space.kscience" - version = "0.3.0-dev-5" + version = "0.3.0-dev-6" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt index 1eb3b91..5dbbe13 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt @@ -57,8 +57,8 @@ public abstract class DeviceConstructor( */ public fun property( state: DeviceState, - nameOverride: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, ): PropertyDelegateProvider> = PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name @@ -77,11 +77,12 @@ public abstract class DeviceConstructor( reader: suspend () -> T, readInterval: Duration, initialState: T, - nameOverride: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, ): PropertyDelegateProvider> = property( DeviceState.external(this, metaConverter, readInterval, initialState, reader), - nameOverride, descriptorBuilder + descriptorBuilder, + nameOverride, ) @@ -90,8 +91,8 @@ public abstract class DeviceConstructor( */ public fun mutableProperty( state: MutableDeviceState, - nameOverride: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, ): PropertyDelegateProvider> = PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name @@ -116,11 +117,26 @@ public abstract class DeviceConstructor( writer: suspend (T) -> Unit, readInterval: Duration, initialState: T, - nameOverride: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, ): PropertyDelegateProvider> = mutableProperty( DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), + descriptorBuilder, + nameOverride, + ) + + /** + * Create and register a virtual property with optional [callback] + */ + public fun state( + metaConverter: MetaConverter, + initialState: T, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, + callback: (T) -> Unit = {}, + ): PropertyDelegateProvider> = mutableProperty( + DeviceState.virtual(metaConverter, initialState, callback), + descriptorBuilder, nameOverride, - descriptorBuilder ) } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt index cea9d71..26b5896 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -47,9 +47,10 @@ public open class DeviceGroup( override val messageFlow: Flow get() = sharedMessageFlow + @OptIn(ExperimentalCoroutinesApi::class) override val coroutineContext: CoroutineContext = context.newCoroutineContext( SupervisorJob(context.coroutineContext[Job]) + - CoroutineName("Device $this") + + CoroutineName("Device $id") + CoroutineExceptionHandler { _, throwable -> context.launch { sharedMessageFlow.emit( @@ -75,7 +76,7 @@ public open class DeviceGroup( public fun install(token: NameToken, device: D): D { require(_devices[token] == null) { "A child device with name $token already exists" } //start the child device if needed - if(lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() } + if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() } _devices[token] = device return device } @@ -216,6 +217,9 @@ public fun DeviceGroup.install(name: Name, device: D): D { public fun DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device) +public fun DeviceGroup.install(device: D): D = + install(device.id, device) + public fun Context.install(name: String, device: D): D = request(DeviceManager).install(name, device) /** @@ -281,15 +285,6 @@ public fun DeviceGroup.registerMutableProperty( } -/** - * Create a virtual [MutableDeviceState], but do not register it to a device - */ -@Suppress("UnusedReceiverParameter") -public fun DeviceGroup.state( - converter: MetaConverter, - initialValue: T, -): MutableDeviceState = VirtualDeviceState(converter, initialValue) - /** * Create a new virtual mutable state and a property based on it. * @return the mutable state used in property @@ -299,8 +294,9 @@ public fun DeviceGroup.registerVirtualProperty( initialValue: T, converter: MetaConverter, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + callback: (T) -> Unit = {}, ): MutableDeviceState { - val state = state(converter, initialValue) + val state = DeviceState.virtual(converter, initialValue, callback) registerMutableProperty(name, state, descriptorBuilder) return state } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index a9cff51..672b0a6 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -45,17 +45,49 @@ public var MutableDeviceState.valueAsMeta: Meta /** * A [MutableDeviceState] that does not correspond to a physical state + * + * @param callback a synchronous callback that could be used without a scope */ -public class VirtualDeviceState( +private class VirtualDeviceState( override val converter: MetaConverter, initialValue: T, + private val callback: (T) -> Unit = {}, ) : MutableDeviceState { private val flow = MutableStateFlow(initialValue) override val valueFlow: Flow get() = flow - override var value: T by flow::value + override var value: T + get() = flow.value + set(value) { + flow.value = value + callback(value) + } } + +/** + * A [MutableDeviceState] that does not correspond to a physical state + * + * @param callback a synchronous callback that could be used without a scope + */ +public fun DeviceState.Companion.virtual( + converter: MetaConverter, + initialValue: T, + callback: (T) -> Unit = {}, +): MutableDeviceState = VirtualDeviceState(converter, initialValue, callback) + +private class StateFlowAsState( + override val converter: MetaConverter, + val flow: MutableStateFlow, +) : MutableDeviceState { + override var value: T by flow::value + override val valueFlow: Flow get() = flow +} + +public fun MutableStateFlow.asDeviceState(converter: MetaConverter): DeviceState = + StateFlowAsState(converter, this) + + private open class BoundDeviceState( override val converter: MetaConverter, val device: Device, 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 4b78f24..89040c0 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 @@ -10,6 +10,8 @@ import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.info import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.string import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DfType import space.kscience.dataforge.names.parseAsName @@ -111,6 +113,11 @@ public interface Device : ContextAware, CoroutineScope { } } +/** + * Inner id of a device. Not necessary corresponds to the name in the parent container + */ +public val Device.id: String get() = meta["id"].string?: "device[${hashCode().toString(16)}]" + /** * Device that caches properties values */ 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 be59b4c..c8c7ffc 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 @@ -4,6 +4,7 @@ import kotlinx.coroutines.launch import space.kscience.controls.api.Device import space.kscience.controls.api.DeviceHub import space.kscience.controls.api.getOrNull +import space.kscience.controls.api.id import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MutableMeta @@ -45,6 +46,8 @@ public fun DeviceManager.install(name: String, device: D): D { return device } +public fun DeviceManager.install(device: D): D = install(device.id, device) + /** * Register and start a device built by [factory] with current [Context] and [meta]. From aa52b4b92791054debc07d900808c80a756ca534 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 28 Dec 2023 21:09:23 +0300 Subject: [PATCH 47/71] hub returns list of messages. --- CHANGELOG.md | 1 + .../space/kscience/controls/api/DeviceHub.kt | 34 +++++++++++-------- .../kscience/controls/api/DeviceMessage.kt | 2 +- .../controls/manager/respondMessage.kt | 19 +++++++---- .../kscience/controls/client/controlsMagix.kt | 4 +-- .../controls/server/deviceWebServer.kt | 16 ++++----- .../kscience/controls/server/responses.kt | 5 +-- demo/all-things/build.gradle.kts | 2 +- .../kscience/controls/demo/DemoDevice.kt | 11 +++++- 9 files changed, 59 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4de4b7..93b9611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Changed - Property caching moved from core `Device` to the `CachingDevice` - `DeviceSpec` properties no explicitly pass property name to getters and setters. +- `DeviceHub.respondHubMessage` now returns a list of messages to allow querying multiple devices. Device server also returns an array. ### Deprecated diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt index 8bd7452..72b0dc3 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt @@ -14,22 +14,27 @@ public interface DeviceHub : Provider { override val defaultChainTarget: String get() = Device.DEVICE_TARGET - override fun content(target: String): Map = if (target == Device.DEVICE_TARGET) { - buildMap { - fun putAll(prefix: Name, hub: DeviceHub) { - hub.devices.forEach { - put(prefix + it.key, it.value) - } - } - - devices.forEach { - val name = it.key.asName() - put(name, it.value) - (it.value as? DeviceHub)?.let { hub -> - putAll(name, hub) - } + /** + * List all devices, including sub-devices + */ + public fun buildDeviceTree(): Map = buildMap { + fun putAll(prefix: Name, hub: DeviceHub) { + hub.devices.forEach { + put(prefix + it.key, it.value) } } + + devices.forEach { + val name = it.key.asName() + put(name, it.value) + (it.value as? DeviceHub)?.let { hub -> + putAll(name, hub) + } + } + } + + override fun content(target: String): Map = if (target == Device.DEVICE_TARGET) { + buildDeviceTree() } else { emptyMap() } @@ -37,6 +42,7 @@ public interface DeviceHub : Provider { public companion object } + public operator fun DeviceHub.get(nameToken: NameToken): Device = devices[nameToken] ?: error("Device with name $nameToken not found in $this") diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt index b3436e9..4cd91b0 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt @@ -103,7 +103,7 @@ public data class PropertyGetMessage( @SerialName("description.get") public data class GetDescriptionMessage( override val sourceDevice: Name? = null, - override val targetDevice: Name, + override val targetDevice: Name? = null, override val comment: String? = null, @EncodeDefault override val time: Instant? = Clock.System.now(), ) : DeviceMessage() { diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt index 4d319af..f8a641f 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt @@ -68,15 +68,22 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess } /** - * Process incoming [DeviceMessage], using hub naming to evaluate target. + * Process incoming [DeviceMessage], using hub naming to find target. + * If the `targetDevice` is `null`, then message is sent to each device in this hub */ -public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMessage? { +public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): List { return try { - val targetName = request.targetDevice ?: return null - val device = getOrNull(targetName) ?: error("The device with name $targetName not found in $this") - device.respondMessage(targetName, request) + val targetName = request.targetDevice + if(targetName == null) { + buildDeviceTree().mapNotNull { + it.value.respondMessage(it.key, request) + } + } else { + val device = getOrNull(targetName) ?: error("The device with name $targetName not found in $this") + listOfNotNull(device.respondMessage(targetName, request)) + } } catch (ex: Exception) { - DeviceMessage.error(ex, sourceDevice = Name.EMPTY, targetDevice = request.sourceDevice) + listOf(DeviceMessage.error(ex, sourceDevice = Name.EMPTY, targetDevice = request.sourceDevice)) } } diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt index ed69bff..5e7f4fb 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt @@ -39,10 +39,10 @@ public fun DeviceManager.launchMagixService( ): Job = context.launch { endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID)).onEach { (request, payload) -> val responsePayload = respondHubMessage(payload) - if (responsePayload != null) { + responsePayload.forEach { endpoint.send( format = controlsMagixFormat, - payload = responsePayload, + payload = it, source = endpointID, target = request.sourceEndpoint, id = generateId(request), diff --git a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt index ba63583..04bb46d 100644 --- a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt +++ b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt @@ -157,8 +157,8 @@ public fun Application.deviceManagerModule( val body = call.receiveText() val request: DeviceMessage = MagixEndpoint.magixJson.decodeFromString(DeviceMessage.serializer(), body) val response = manager.respondHubMessage(request) - if (response != null) { - call.respondMessage(response) + if (response.isNotEmpty()) { + call.respondMessages(response) } else { call.respondText("No response") } @@ -177,9 +177,9 @@ public fun Application.deviceManagerModule( property = property, ) - val response = manager.respondHubMessage(request) - if (response != null) { - call.respondMessage(response) + val responses = manager.respondHubMessage(request) + if (responses.isNotEmpty()) { + call.respondMessages(responses) } else { call.respond(HttpStatusCode.InternalServerError) } @@ -197,9 +197,9 @@ public fun Application.deviceManagerModule( value = json.toMeta() ) - val response = manager.respondHubMessage(request) - if (response != null) { - call.respondMessage(response) + val responses = manager.respondHubMessage(request) + if (responses.isNotEmpty()) { + call.respondMessages(responses) } else { call.respond(HttpStatusCode.InternalServerError) } diff --git a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/responses.kt b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/responses.kt index 93d11c4..ffe489f 100644 --- a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/responses.kt +++ b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/responses.kt @@ -5,6 +5,7 @@ import io.ktor.server.application.ApplicationCall import io.ktor.server.response.respondText import kotlinx.serialization.json.JsonObjectBuilder import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.serializer import space.kscience.controls.api.DeviceMessage import space.kscience.magix.api.MagixEndpoint @@ -25,7 +26,7 @@ internal suspend fun ApplicationCall.respondJson(builder: JsonObjectBuilder.() - respondText(json.toString(), contentType = ContentType.Application.Json) } -internal suspend fun ApplicationCall.respondMessage(message: DeviceMessage): Unit = respondText( - MagixEndpoint.magixJson.encodeToString(DeviceMessage.serializer(), message), +internal suspend fun ApplicationCall.respondMessages(messages: List): Unit = respondText( + MagixEndpoint.magixJson.encodeToString(serializer>(), messages), contentType = ContentType.Application.Json ) \ No newline at end of file diff --git a/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts index 33b654e..6888d35 100644 --- a/demo/all-things/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("no.tornado:tornadofx:1.7.20") - implementation("space.kscience:plotlykt-server:0.6.0") + implementation("space.kscience:plotlykt-server:0.6.1") // implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") implementation(spclibs.logback.classic) } diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt index 067792c..657d275 100644 --- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt @@ -47,7 +47,12 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(Compa description = "Real to virtual time scale" } - val sinScale by mutableProperty(MetaConverter.double, IDemoDevice::sinScaleState) + val sinScale by mutableProperty(MetaConverter.double, IDemoDevice::sinScaleState){ + description = "The scale of sin plot" + metaDescriptor { + valueType(ValueType.NUMBER) + } + } val cosScale by mutableProperty(MetaConverter.double, IDemoDevice::cosScaleState) val sin by doubleProperty { sinValue() } @@ -74,6 +79,10 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(Compa write(cosScale, 1.0) } + val setSinScale by action(MetaConverter.double, MetaConverter.unit){ value: Double -> + write(sinScale, value) + } + override suspend fun IDemoDevice.onOpen() { launch { read(sinScale) From 7579ddfad4ed2605551793d070c01c0345a6ab33 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 28 Dec 2023 22:40:58 +0300 Subject: [PATCH 48/71] Quick fix for OPC us server --- .../controls/opcua/client/MetaBsdParser.kt | 18 +++-- .../controls/opcua/server/DeviceNameSpace.kt | 79 ++++++++++--------- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt index 27330bb..48aa6dc 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt @@ -56,6 +56,7 @@ internal class MetaEnumCodec : OpcUaBinaryDataTypeCodec { internal fun opcToMeta(value: Any?): Meta = when (value) { null -> Meta(Null) + is Variant -> opcToMeta(value.value) is Meta -> value is Value -> Meta(value) is Number -> when (value) { @@ -79,12 +80,17 @@ internal fun opcToMeta(value: Any?): Meta = when (value) { "text" put value.text?.asValue() } is DataValue -> Meta { - "value" put opcToMeta(value.value) // need SerializationContext to do that properly - value.statusCode?.value?.let { "status" put Meta(it.asValue()) } - value.sourceTime?.javaInstant?.let { "sourceTime" put it.toKotlinInstant().toMeta() } - value.sourcePicoseconds?.let { "sourcePicoseconds" put Meta(it.asValue()) } - value.serverTime?.javaInstant?.let { "serverTime" put it.toKotlinInstant().toMeta() } - value.serverPicoseconds?.let { "serverPicoseconds" put Meta(it.asValue()) } + val variant= opcToMeta(value.value) + update(variant)// need SerializationContext to do that properly + //TODO remove after DF 0.7.2 + this.value = variant.value + "@opc" put { + value.statusCode?.value?.let { "status" put Meta(it.asValue()) } + value.sourceTime?.javaInstant?.let { "sourceTime" put it.toKotlinInstant().toMeta() } + value.sourcePicoseconds?.let { "sourcePicoseconds" put Meta(it.asValue()) } + value.serverTime?.javaInstant?.let { "serverTime" put it.toKotlinInstant().toMeta() } + value.serverPicoseconds?.let { "serverPicoseconds" put Meta(it.asValue()) } + } } is ByteString -> Meta(value.bytesOrEmpty().asValue()) is XmlElement -> Meta(value.fragment?.asValue() ?: Null) diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt index 05fc3c6..057eb2e 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt @@ -2,7 +2,6 @@ package space.kscience.controls.opcua.server import kotlinx.coroutines.launch import kotlinx.datetime.toJavaInstant -import kotlinx.serialization.json.Json import org.eclipse.milo.opcua.sdk.core.AccessLevel import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.server.Lifecycle @@ -21,14 +20,15 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText import space.kscience.controls.api.* import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.opcua.client.opcToMeta import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.MetaSerializer import space.kscience.dataforge.meta.ValueType import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.plus -public operator fun CachingDevice.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name) +public operator fun CachingDevice.get(propertyDescriptor: PropertyDescriptor): Meta? = + getProperty(propertyDescriptor.name) public suspend fun Device.read(propertyDescriptor: PropertyDescriptor): Meta = readProperty(propertyDescriptor.name) @@ -38,29 +38,11 @@ https://github.com/eclipse/milo/blob/master/milo-examples/server-examples/src/ma public class DeviceNameSpace( server: OpcUaServer, - public val deviceManager: DeviceManager + public val deviceManager: DeviceManager, ) : ManagedNamespaceWithLifecycle(server, NAMESPACE_URI) { private val subscription = SubscriptionModel(server, this) - init { - lifecycleManager.addLifecycle(subscription) - - lifecycleManager.addStartupTask { - nodeContext.registerHub(deviceManager, Name.EMPTY) - } - - lifecycleManager.addLifecycle(object : Lifecycle { - override fun startup() { - server.addressSpaceManager.register(this@DeviceNameSpace) - } - - override fun shutdown() { - server.addressSpaceManager.unregister(this@DeviceNameSpace) - } - }) - } - private fun UaFolderNode.registerDeviceNodes(deviceName: Name, device: Device) { val nodes = device.propertyDescriptors.associate { descriptor -> val propertyName = descriptor.name @@ -74,14 +56,17 @@ public class DeviceNameSpace( setAccessLevel(AccessLevel.READ_WRITE) setUserAccessLevel(AccessLevel.READ_WRITE) } + descriptor.mutable -> { setAccessLevel(AccessLevel.WRITE_ONLY) setUserAccessLevel(AccessLevel.WRITE_ONLY) } + descriptor.readable -> { setAccessLevel(AccessLevel.READ_ONLY) setUserAccessLevel(AccessLevel.READ_ONLY) } + else -> { setAccessLevel(AccessLevel.NONE) setUserAccessLevel(AccessLevel.NONE) @@ -104,26 +89,23 @@ public class DeviceNameSpace( }.build() // Update initial value, but only if it is cached - if(device is CachingDevice) { + if (device is CachingDevice) { device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let { node.value = it } } - /** - * Subscribe to node value changes - */ - node.addAttributeObserver { _: UaNode, attributeId: AttributeId, value: Any -> - if (attributeId == AttributeId.Value) { - val meta: Meta = when (value) { - is Meta -> value - is Boolean -> Meta(value) - is Number -> Meta(value) - is String -> Json.decodeFromString(MetaSerializer, value) - else -> return@addAttributeObserver //TODO("other types not implemented") - } - deviceManager.context.launch { - device.writeProperty(propertyName, meta) + if (descriptor.mutable) { + + /** + * Subscribe to node value changes + */ + node.addAttributeObserver { _: UaNode, attributeId: AttributeId, value: Any? -> + if (attributeId == AttributeId.Value) { + val meta: Meta = opcToMeta(value) + deviceManager.context.launch { + device.writeProperty(propertyName, meta) + } } } } @@ -137,7 +119,10 @@ public class DeviceNameSpace( device.onPropertyChange { nodes[property]?.let { node -> val sourceTime = time?.let { DateTime(it.toJavaInstant()) } - node.value = value.toOpc(sourceTime = sourceTime) + val newValue = value.toOpc(sourceTime = sourceTime) + if (node.value.value != newValue.value) { + node.value = newValue + } } } //recursively add sub-devices @@ -168,6 +153,24 @@ public class DeviceNameSpace( } } + init { + lifecycleManager.addLifecycle(subscription) + + lifecycleManager.addStartupTask { + nodeContext.registerHub(deviceManager, Name.EMPTY) + } + + lifecycleManager.addLifecycle(object : Lifecycle { + override fun startup() { + server.addressSpaceManager.register(this@DeviceNameSpace) + } + + override fun shutdown() { + server.addressSpaceManager.unregister(this@DeviceNameSpace) + } + }) + } + override fun onDataItemsCreated(dataItems: List?) { subscription.onDataItemsCreated(dataItems) } From fa2414ef473feaa33402e13c68b6d89f949757c8 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 2 Feb 2024 16:04:41 +0300 Subject: [PATCH 49/71] Add demo for device message listening --- .../kscience/controls/api/DeviceMessage.kt | 2 +- .../controls/demo/DemoControllerView.kt | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) 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 4cd91b0..406cfca 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 @@ -1,4 +1,4 @@ -@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class) +@file:OptIn(ExperimentalSerializationApi::class) package space.kscience.controls.api 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 8edd90e..2d2086b 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 @@ -5,10 +5,17 @@ import javafx.scene.Parent import javafx.scene.control.Slider import javafx.scene.layout.Priority import javafx.stage.Stage +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json import org.eclipse.milo.opcua.sdk.server.OpcUaServer import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText +import space.kscience.controls.api.DeviceMessage +import space.kscience.controls.api.GetDescriptionMessage +import space.kscience.controls.api.PropertyChangedMessage import space.kscience.controls.client.launchMagixService +import space.kscience.controls.client.magixFormat import space.kscience.controls.demo.DemoDevice.Companion.cosScale import space.kscience.controls.demo.DemoDevice.Companion.sinScale import space.kscience.controls.demo.DemoDevice.Companion.timeScale @@ -20,6 +27,8 @@ 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.api.send +import space.kscience.magix.api.subscribe import space.kscience.magix.rsocket.rSocketWithTcp import space.kscience.magix.rsocket.rSocketWithWebSockets import space.kscience.magix.server.RSocketMagixFlowPlugin @@ -49,6 +58,7 @@ class DemoController : Controller(), ContextAware { private val deviceManager = context.request(DeviceManager) + fun init() { context.launch { device = deviceManager.install("demo", DemoDevice) @@ -67,6 +77,17 @@ class DemoController : Controller(), ContextAware { //serve devices as OPC-UA namespace opcUaServer.startup() opcUaServer.serveDevices(deviceManager) + + + val listenerEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (_, deviceMessage)-> + // print all messages that are not property change message + if(deviceMessage !is PropertyChangedMessage){ + println(">> ${Json.encodeToString(DeviceMessage.serializer(), deviceMessage)}") + } + }.launchIn(this) + listenerEndpoint.send(DeviceManager.magixFormat, GetDescriptionMessage(), "listener", "controls-kt") + } } From b1121d61cb9e8832ed3e2cb25a23424182c620ee Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 5 Feb 2024 14:08:15 +0300 Subject: [PATCH 50/71] Allow controls magix endpoint to receive broadcast. --- .../kscience/controls/client/controlsMagix.kt | 4 ++- .../space/kscience/magix/api/MagixFormat.kt | 2 +- .../kscience/magix/api/MagixMessageFilter.kt | 2 +- .../magix/storage/xodus/XodusMagixStorage.kt | 28 ++++++++++--------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt index 5e7f4fb..2257c74 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt @@ -32,12 +32,14 @@ internal fun generateId(request: MagixMessage): String = if (request.id != null) /** * Communicate with server in [Magix format](https://github.com/waltz-controls/rfc/tree/master/1) + * + * Accepts messages with target that equals [endpointID] or null (broadcast messages) */ public fun DeviceManager.launchMagixService( endpoint: MagixEndpoint, endpointID: String = controlsMagixFormat.defaultFormat, ): Job = context.launch { - endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID)).onEach { (request, payload) -> + endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID, null)).onEach { (request, payload) -> val responsePayload = respondHubMessage(payload) responsePayload.forEach { endpoint.send( diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFormat.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFormat.kt index 115fcff..42d55b7 100644 --- a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFormat.kt +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFormat.kt @@ -26,7 +26,7 @@ public data class MagixFormat( public fun MagixEndpoint.subscribe( format: MagixFormat, originFilter: Collection? = null, - targetFilter: Collection? = null, + targetFilter: Collection? = null, ): Flow> = subscribe( MagixMessageFilter(format = format.formats, source = originFilter, target = targetFilter) ).map { 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 4bd4746..72c7f7f 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 @@ -11,7 +11,7 @@ import kotlinx.serialization.Serializable public data class MagixMessageFilter( val format: Collection? = null, val source: Collection? = null, - val target: Collection? = null, + val target: Collection? = null, ) { public fun accepts(message: MagixMessage): Boolean = diff --git a/magix/magix-storage/magix-storage-xodus/src/main/kotlin/space/kscience/magix/storage/xodus/XodusMagixStorage.kt b/magix/magix-storage/magix-storage-xodus/src/main/kotlin/space/kscience/magix/storage/xodus/XodusMagixStorage.kt index 4a3efa3..8d0d852 100644 --- a/magix/magix-storage/magix-storage-xodus/src/main/kotlin/space/kscience/magix/storage/xodus/XodusMagixStorage.kt +++ b/magix/magix-storage/magix-storage-xodus/src/main/kotlin/space/kscience/magix/storage/xodus/XodusMagixStorage.kt @@ -39,9 +39,8 @@ public class XodusMagixHistory(private val store: PersistentEntityStore) : Write setBlobString(MagixMessage::payload.name, magixJson.encodeToString(message.payload)) - message.targetEndpoint?.let { - setProperty(MagixMessage::targetEndpoint.name, it) - } + setProperty(MagixMessage::targetEndpoint.name, (message.targetEndpoint ?: "")) + message.id?.let { setProperty(MagixMessage::id.name, it) } @@ -68,14 +67,14 @@ public class XodusMagixHistory(private val store: PersistentEntityStore) : Write ): Unit = store.executeInReadonlyTransaction { transaction -> val all = transaction.getAll(XodusMagixStorage.MAGIC_MESSAGE_ENTITY_TYPE) - fun StoreTransaction.findAllIn( + fun findAllIn( entityType: String, field: String, - values: Collection?, + values: Collection?, ): EntityIterable? { var union: EntityIterable? = null values?.forEach { - val filter = transaction.find(entityType, field, it) + val filter = transaction.find(entityType, field, it ?: "") union = union?.union(filter) ?: filter } return union @@ -84,21 +83,24 @@ public class XodusMagixHistory(private val store: PersistentEntityStore) : Write // filter by magix filter val filteredByMagix: EntityIterable = magixFilter?.let { mf -> var res = all - transaction.findAllIn(XodusMagixStorage.MAGIC_MESSAGE_ENTITY_TYPE, MagixMessage::format.name, mf.format) - ?.let { - res = res.intersect(it) - } - transaction.findAllIn( + findAllIn( + XodusMagixStorage.MAGIC_MESSAGE_ENTITY_TYPE, + MagixMessage::format.name, + mf.format + )?.let { + res = res.intersect(it) + } + findAllIn( XodusMagixStorage.MAGIC_MESSAGE_ENTITY_TYPE, MagixMessage::sourceEndpoint.name, mf.source )?.let { res = res.intersect(it) } - transaction.findAllIn( + findAllIn( XodusMagixStorage.MAGIC_MESSAGE_ENTITY_TYPE, MagixMessage::targetEndpoint.name, - mf.target + mf.target?.filterNotNull() )?.let { res = res.intersect(it) } From 8bd9bcc6a6a310fd765c4ad592f947597c538b8c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 15 Feb 2024 21:04:59 +0300 Subject: [PATCH 51/71] Fix bizzare NPE in context generation for DeviceClient. Add test for remote client --- controls-magix/build.gradle.kts | 1 + .../kscience/controls/client/DeviceClient.kt | 6 +- .../controls/client/RemoteDeviceConnect.kt | 87 +++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt diff --git a/controls-magix/build.gradle.kts b/controls-magix/build.gradle.kts index 0296b8c..3a819bf 100644 --- a/controls-magix/build.gradle.kts +++ b/controls-magix/build.gradle.kts @@ -12,6 +12,7 @@ description = """ kscience { jvm() js() + useCoroutines("1.8.0") useSerialization { json() } diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt index 474f86a..924fad9 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt @@ -1,8 +1,8 @@ package space.kscience.controls.client import com.benasher44.uuid.uuid4 +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* -import kotlinx.coroutines.newCoroutineContext import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import space.kscience.controls.api.* @@ -28,8 +28,8 @@ public class DeviceClient( private val send: suspend (DeviceMessage) -> Unit, ) : CachingDevice { - @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) - override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext) + + override val coroutineContext: CoroutineContext = context.coroutineContext + Job(context.coroutineContext[Job]) private val mutex = Mutex() diff --git a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt new file mode 100644 index 0000000..f0f65fa --- /dev/null +++ b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt @@ -0,0 +1,87 @@ +package space.kscience.controls.client + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import space.kscience.controls.api.Device +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.controls.manager.respondMessage +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.dataforge.names.Name +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.time.Duration.Companion.milliseconds + + +public suspend fun Device.readUnsafe(propertySpec: DevicePropertySpec<*, T>): T = + propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Property read result is not valid") + +internal class RemoteDeviceConnect { + + class TestDevice(context: Context, meta: Meta) : DeviceBySpec(TestDevice, 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): TestDevice = TestDevice(context, meta) + + val value by doubleProperty { randomValue } + + override suspend fun TestDevice.onOpen() { + doRecurring((meta["delay"].int ?: 10).milliseconds) { + read(value) + } + } + } + } + + @Test + fun wrapper() = runTest { + val context = Context { + plugin(DeviceManager) + } + + val device = context.request(DeviceManager).install("test", TestDevice) + + val virtualMagixEndpoint = object : MagixEndpoint { + + + override fun subscribe(filter: MagixMessageFilter): Flow = device.messageFlow.map { + MagixMessage( + format = DeviceManager.magixFormat.defaultFormat, + payload = MagixEndpoint.magixJson.encodeToJsonElement(DeviceManager.magixFormat.serializer, it), + sourceEndpoint = "test", + ) + } + + override suspend fun broadcast(message: MagixMessage) { + device.respondMessage( + Name.EMPTY, + Json.decodeFromJsonElement(DeviceManager.magixFormat.serializer, message.payload) + ) + } + + override fun close() { + // + } + } + + val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "test", Name.EMPTY) + + assertContains(0.0..1.0, remoteDevice.readUnsafe(TestDevice.value)) + } +} \ No newline at end of file From 231f1bc858a8823c76a3f7109e4c4df90a77870c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 19 Feb 2024 14:27:36 +0300 Subject: [PATCH 52/71] Add special unsafe calls for DeviceClient to mirror safe device --- .../controls/client/clientPropertyAccess.kt | 79 +++++++++++++++++++ .../controls/client/RemoteDeviceConnect.kt | 6 +- 2 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt new file mode 100644 index 0000000..22b0413 --- /dev/null +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt @@ -0,0 +1,79 @@ +package space.kscience.controls.client + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.api.requestProperty +import space.kscience.controls.spec.DeviceActionSpec +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.MutableDevicePropertySpec +import space.kscience.controls.spec.name +import space.kscience.dataforge.meta.Meta + + +/** + * An accessor that allows DeviceClient to connect to any property without type checks + */ +public suspend fun DeviceClient.read(propertySpec: DevicePropertySpec<*, T>): T = + propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Property read result is not valid") + + +public suspend fun DeviceClient.request(propertySpec: DevicePropertySpec<*, T>): T = + propertySpec.converter.metaToObject(requestProperty(propertySpec.name)) + +public suspend fun DeviceClient.write(propertySpec: MutableDevicePropertySpec<*, T>, value: T) { + writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value)) +} + +public fun DeviceClient.writeAsync(propertySpec: MutableDevicePropertySpec<*, T>, value: T): Job = launch { + write(propertySpec, value) +} + +public fun DeviceClient.propertyFlow(spec: DevicePropertySpec<*, T>): Flow = messageFlow + .filterIsInstance() + .filter { it.property == spec.name } + .mapNotNull { spec.converter.metaToObject(it.value) } + +public fun DeviceClient.onPropertyChange( + spec: DevicePropertySpec<*, T>, + scope: CoroutineScope = this, + callback: suspend PropertyChangedMessage.(T) -> Unit, +): Job = messageFlow + .filterIsInstance() + .filter { it.property == spec.name } + .onEach { change -> + val newValue = spec.converter.metaToObject(change.value) + if (newValue != null) { + change.callback(newValue) + } + }.launchIn(scope) + +public fun DeviceClient.useProperty( + spec: DevicePropertySpec<*, T>, + scope: CoroutineScope = this, + callback: suspend (T) -> Unit, +): Job = scope.launch { + callback(read(spec)) + messageFlow + .filterIsInstance() + .filter { it.property == spec.name } + .collect { change -> + val newValue = spec.converter.metaToObject(change.value) + if (newValue != null) { + callback(newValue) + } + } +} + +public suspend fun DeviceClient.execute(actionSpec: DeviceActionSpec<*, I, O>, input: I): O { + val inputMeta = actionSpec.inputConverter.objectToMeta(input) + val res = execute(actionSpec.name, inputMeta) + return actionSpec.outputConverter.metaToObject(res ?: Meta.EMPTY) +} + +public suspend fun DeviceClient.execute(actionSpec: DeviceActionSpec<*, Unit, O>): O { + val res = execute(actionSpec.name, Meta.EMPTY) + return actionSpec.outputConverter.metaToObject(res ?: Meta.EMPTY) +} \ No newline at end of file diff --git a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt index f0f65fa..d55dce1 100644 --- a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt +++ b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json -import space.kscience.controls.api.Device import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install import space.kscience.controls.manager.respondMessage @@ -25,9 +24,6 @@ import kotlin.test.assertContains import kotlin.time.Duration.Companion.milliseconds -public suspend fun Device.readUnsafe(propertySpec: DevicePropertySpec<*, T>): T = - propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Property read result is not valid") - internal class RemoteDeviceConnect { class TestDevice(context: Context, meta: Meta) : DeviceBySpec(TestDevice, context, meta) { @@ -82,6 +78,6 @@ internal class RemoteDeviceConnect { val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "test", Name.EMPTY) - assertContains(0.0..1.0, remoteDevice.readUnsafe(TestDevice.value)) + assertContains(0.0..1.0, remoteDevice.read(TestDevice.value)) } } \ No newline at end of file From 57e9df140b72cfb156ff9d252fe1a9f61ac540e2 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 19 Feb 2024 15:10:51 +0300 Subject: [PATCH 53/71] Add utilities to work with remote devices --- .../kscience/controls/client/DeviceClient.kt | 73 ++++++++++++++++++- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt index 924fad9..31dbd89 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt @@ -7,6 +7,8 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import space.kscience.controls.api.* import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.name import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.misc.DFExperimental @@ -106,12 +108,75 @@ public class DeviceClient( * Connect to a remote device via this endpoint. * * @param context a [Context] to run device in - * @param endpointName the name of endpoint in Magix to connect to + * @param sourceEndpointName the name of this endpoint + * @param targetEndpointName the name of endpoint in Magix to connect to * @param deviceName the name of device within endpoint */ -public fun MagixEndpoint.remoteDevice(context: Context, endpointName: String, deviceName: Name): DeviceClient { - val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(endpointName)).map { it.second } +public fun MagixEndpoint.remoteDevice( + context: Context, + sourceEndpointName: String, + targetEndpointName: String, + deviceName: Name, +): DeviceClient { + val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(targetEndpointName)).map { it.second } return DeviceClient(context, deviceName, subscription) { - send(DeviceManager.magixFormat, it, endpointName, id = stringUID()) + send( + format = DeviceManager.magixFormat, + payload = it, + source = sourceEndpointName, + target = targetEndpointName, + id = stringUID() + ) } +} + +/** + * Subscribe on specific property of a device without creating a device + */ +public fun MagixEndpoint.controlsPropertyFlow( + endpointName: String, + deviceName: Name, + propertySpec: DevicePropertySpec<*, T>, +): Flow { + val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(endpointName)).map { it.second } + + return subscription.filterIsInstance() + .filter { message -> + message.sourceDevice == deviceName && message.property == propertySpec.name + }.map { + propertySpec.converter.metaToObject(it.value) + } +} + +public suspend fun MagixEndpoint.sendControlsPropertyChange( + sourceEndpointName: String, + targetEndpointName: String, + deviceName: Name, + propertySpec: DevicePropertySpec<*, T>, + value: T, +) { + val message = PropertySetMessage( + property = propertySpec.name, + value = propertySpec.converter.objectToMeta(value), + targetDevice = deviceName + ) + send(DeviceManager.magixFormat, message, source = sourceEndpointName, target = targetEndpointName) +} + +/** + * Subscribe on property change messages together with property values + */ +public fun MagixEndpoint.controlsPropertyMessageFlow( + endpointName: String, + deviceName: Name, + propertySpec: DevicePropertySpec<*, T>, +): Flow> { + val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(endpointName)).map { it.second } + + return subscription.filterIsInstance() + .filter { message -> + message.sourceDevice == deviceName && message.property == propertySpec.name + }.map { + it to propertySpec.converter.metaToObject(it.value) + } } \ No newline at end of file From 9edf3b13efaf238a60bdc54de31a3d2acd63ac49 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 27 Feb 2024 10:31:35 +0300 Subject: [PATCH 54/71] Remove unnecessary scope in hub message flow --- CHANGELOG.md | 1 + .../controls/manager/respondMessage.kt | 37 +++++++------------ .../controls/storage/storageCommon.kt | 2 +- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b9611..8183d79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### Fixed - Property writing does not trigger change if logical state already is the same as value to be set. - Modbus-slave triggers only once for multi-register write. +- Removed unnecessary scope in hub messageFlow ### Security 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 f8a641f..3a0ac7b 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 @@ -1,10 +1,9 @@ package space.kscience.controls.manager -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import space.kscience.controls.api.* import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.plus @@ -74,7 +73,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): List { return try { val targetName = request.targetDevice - if(targetName == null) { + if (targetName == null) { buildDeviceTree().mapNotNull { it.value.respondMessage(it.key, request) } @@ -90,27 +89,19 @@ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): List { +public fun DeviceHub.hubMessageFlow(): Flow { - //TODO could we avoid using downstream scope? - val outbox = MutableSharedFlow() - if (this is Device) { - messageFlow.onEach { - outbox.emit(it) - }.launchIn(scope) - } - //TODO maybe better create map of all devices to limit copying - devices.forEach { (token, childDevice) -> - val flow = if (childDevice is DeviceHub) { - childDevice.hubMessageFlow(scope) + val deviceMessageFlow = if (this is Device) messageFlow else emptyFlow() + + val childrenFlows = devices.map { (token, childDevice) -> + if (childDevice is DeviceHub) { + childDevice.hubMessageFlow() } else { childDevice.messageFlow + }.map { deviceMessage -> + deviceMessage.changeSource { token + it } } - flow.onEach { deviceMessage -> - outbox.emit( - deviceMessage.changeSource { token + it } - ) - }.launchIn(scope) } - return outbox + + return merge(deviceMessageFlow, *childrenFlows.toTypedArray()) } \ No newline at end of file diff --git a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt index 2a453cf..f0bf1f2 100644 --- a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt +++ b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt @@ -12,7 +12,7 @@ import space.kscience.dataforge.context.Factory import space.kscience.dataforge.context.debug import space.kscience.dataforge.context.logger -//TODO replace by plugin? + public fun DeviceManager.storage( factory: Factory, ): DeviceMessageStorage = factory.build(context, meta) From cfd9eb053ca5b6f22237e4a592bf485010e0d80d Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 4 Mar 2024 11:11:56 +0300 Subject: [PATCH 55/71] Make DeviceMessage time mandatory --- .../kscience/controls/api/DeviceMessage.kt | 26 +++++++++---------- .../kscience/controls/client/controlsMagix.kt | 2 +- .../controls/client/RemoteDeviceConnect.kt | 4 +-- .../controls/opcua/server/DeviceNameSpace.kt | 2 +- .../xodus/XodusDeviceMessageStorage.kt | 5 +--- .../controls/storage/storageCommon.kt | 2 +- .../src/commonMain/kotlin/plotExtensions.kt | 3 +-- .../kscience/controls/demo/MassDevice.kt | 4 +-- 8 files changed, 22 insertions(+), 26 deletions(-) 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 406cfca..dda89de 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 @@ -22,7 +22,7 @@ public sealed class DeviceMessage { public abstract val sourceDevice: Name? public abstract val targetDevice: Name? public abstract val comment: String? - public abstract val time: Instant? + public abstract val time: Instant /** * Update the source device name for composition. If the original name is null, the resulting name is also null. @@ -59,7 +59,7 @@ public data class PropertyChangedMessage( override val sourceDevice: Name = Name.EMPTY, override val targetDevice: Name? = null, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) } @@ -75,7 +75,7 @@ public data class PropertySetMessage( override val sourceDevice: Name? = null, override val targetDevice: Name, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block)) } @@ -91,7 +91,7 @@ public data class PropertyGetMessage( override val sourceDevice: Name? = null, override val targetDevice: Name, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block)) } @@ -105,7 +105,7 @@ public data class GetDescriptionMessage( override val sourceDevice: Name? = null, override val targetDevice: Name? = null, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block)) } @@ -122,7 +122,7 @@ public data class DescriptionMessage( override val sourceDevice: Name, override val targetDevice: Name? = null, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) } @@ -141,7 +141,7 @@ public data class ActionExecuteMessage( override val sourceDevice: Name? = null, override val targetDevice: Name, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block)) } @@ -160,7 +160,7 @@ public data class ActionResultMessage( override val sourceDevice: Name, override val targetDevice: Name? = null, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) } @@ -175,7 +175,7 @@ public data class BinaryNotificationMessage( override val sourceDevice: Name, override val targetDevice: Name? = null, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) } @@ -190,7 +190,7 @@ public data class EmptyDeviceMessage( override val sourceDevice: Name? = null, override val targetDevice: Name? = null, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block)) } @@ -206,7 +206,7 @@ public data class DeviceLogMessage( override val sourceDevice: Name = Name.EMPTY, override val targetDevice: Name? = null, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) } @@ -223,7 +223,7 @@ public data class DeviceErrorMessage( override val sourceDevice: Name = Name.EMPTY, override val targetDevice: Name? = null, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) } @@ -238,7 +238,7 @@ public data class DeviceLifeCycleMessage( override val sourceDevice: Name = Name.EMPTY, override val targetDevice: Name? = null, override val comment: String? = null, - @EncodeDefault override val time: Instant? = Clock.System.now(), + @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) } diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt index 2257c74..8aa4383 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt @@ -55,7 +55,7 @@ public fun DeviceManager.launchMagixService( logger.error(error) { "Error while responding to message: ${error.message}" } }.launchIn(this) - hubMessageFlow(this).onEach { payload -> + hubMessageFlow().onEach { payload -> endpoint.send( format = controlsMagixFormat, payload = payload, diff --git a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt index d55dce1..1478899 100644 --- a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt +++ b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt @@ -60,7 +60,7 @@ internal class RemoteDeviceConnect { MagixMessage( format = DeviceManager.magixFormat.defaultFormat, payload = MagixEndpoint.magixJson.encodeToJsonElement(DeviceManager.magixFormat.serializer, it), - sourceEndpoint = "test", + sourceEndpoint = "source", ) } @@ -76,7 +76,7 @@ internal class RemoteDeviceConnect { } } - val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "test", Name.EMPTY) + val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "source", "target", Name.EMPTY) assertContains(0.0..1.0, remoteDevice.read(TestDevice.value)) } diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt index 057eb2e..e108b21 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt @@ -118,7 +118,7 @@ public class DeviceNameSpace( //Subscribe on properties updates device.onPropertyChange { nodes[property]?.let { node -> - val sourceTime = time?.let { DateTime(it.toJavaInstant()) } + val sourceTime = DateTime(time.toJavaInstant()) val newValue = value.toOpc(sourceTime = sourceTime) if (node.value.value != newValue.value) { node.value = newValue 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 e5d2e4a..e1b5a8d 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 @@ -19,7 +19,6 @@ 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 import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.names.Name @@ -39,9 +38,7 @@ internal fun StoreTransaction.writeMessage(message: DeviceMessage): Unit { message.targetDevice?.let { entity.setProperty(DeviceMessage::targetDevice.name, it.toString()) } - message.time?.let { - entity.setProperty(DeviceMessage::targetDevice.name, it.toString()) - } + entity.setProperty(DeviceMessage::targetDevice.name, message.time.toString()) entity.setBlobString("json", Json.encodeToString(json)) } diff --git a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt index f0bf1f2..04b633a 100644 --- a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt +++ b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt @@ -31,7 +31,7 @@ public fun DeviceManager.storeMessages( val storage = factory.build(context, meta) logger.debug { "Message storage with meta = $meta created" } - return hubMessageFlow(context).filter(filterCondition).onEach { message -> + return hubMessageFlow().filter(filterCondition).onEach { message -> storage.write(message) }.onCompletion { storage.close() diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index 8c7b02f..fde716b 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -104,10 +104,9 @@ public fun Plot.plotDeviceProperty( coroutineScope: CoroutineScope = device.context, configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { - val clock = device.context.clock val data = TimeData() device.propertyMessageFlow(propertyName).sample(sampling).transform { - data.append(it.time ?: clock.now(), it.value.extractValue()) + data.append(it.time, it.value.extractValue()) data.trim(maxAge, maxPoints, minPoints) emit(data) }.onEach { 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 040d288..22d7e1c 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 @@ -103,8 +103,8 @@ suspend fun main() { monitorEndpoint.subscribe(DeviceManager.magixFormat).onEach { (magixMessage, payload) -> mutex.withLock { - val delay = Clock.System.now() - payload.time!! - latest[magixMessage.sourceEndpoint] = Clock.System.now() - payload.time!! + val delay = Clock.System.now() - payload.time + latest[magixMessage.sourceEndpoint] = Clock.System.now() - payload.time max[magixMessage.sourceEndpoint] = maxOf(delay, max[magixMessage.sourceEndpoint] ?: ZERO) } From 28ec2bc8b8135072ddb154abbaf17325f667cc41 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 4 Mar 2024 11:12:16 +0300 Subject: [PATCH 56/71] Add PropertyHistory API --- .../kscience/controls/misc/PropertyHistory.kt | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt new file mode 100644 index 0000000..5b58321 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt @@ -0,0 +1,66 @@ +package space.kscience.controls.misc + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import space.kscience.controls.api.Device +import space.kscience.controls.api.DeviceMessage +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.name +import space.kscience.dataforge.meta.transformations.MetaConverter + +/** + * An interface for device property history. + */ +public interface PropertyHistory { + /** + * Flow property values filtered by a time range. The implementation could flow it as a chunk or provide paging. + * So the resulting flow is allowed to suspend. + * + * If [until] is in the future, the resulting flow is potentially unlimited. + * Theoretically, it could be also unlimited if the event source keeps producing new event with timestamp in a given range. + */ + public fun flowHistory( + from: Instant = Instant.DISTANT_PAST, + until: Instant = Clock.System.now(), + ): Flow> +} + +/** + * An in-memory property values history collector + */ +public class CollectedPropertyHistory( + public val scope: CoroutineScope, + eventFlow: Flow, + public val propertyName: String, + public val converter: MetaConverter, + maxSize: Int = 1000, +) : PropertyHistory { + + private val store: SharedFlow> = eventFlow + .filterIsInstance() + .filter { it.property == propertyName } + .map { ValueWithTime(converter.metaToObject(it.value), it.time) } + .shareIn(scope, started = SharingStarted.Eagerly, replay = maxSize) + + override fun flowHistory(from: Instant, until: Instant): Flow> = + store.filter { it.time in from..until } +} + +/** + * Collect and store in memory device property changes for a given property + */ +public fun Device.collectPropertyHistory( + scope: CoroutineScope = this, + propertyName: String, + converter: MetaConverter, + maxSize: Int = 1000, +): PropertyHistory = CollectedPropertyHistory(scope, messageFlow, propertyName, converter, maxSize) + +public fun D.collectPropertyHistory( + scope: CoroutineScope = this, + spec: DevicePropertySpec, + maxSize: Int = 1000, +): PropertyHistory = collectPropertyHistory(scope, spec.name, spec.converter, maxSize) \ No newline at end of file From dbacdbc7cff379b573d538c279aa422cec5f235c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 4 Mar 2024 12:47:40 +0300 Subject: [PATCH 57/71] Replace event controls-storage with async api --- .../xodus/XodusDeviceMessageStorage.kt | 33 +++++++------------ .../controls/storage/DeviceMessageStorage.kt | 30 +++++++++++++++-- .../controls/storage/propertyHistory.kt | 20 +++++++++++ .../controls/storage/storageCommon.kt | 22 ------------- 4 files changed, 58 insertions(+), 47 deletions(-) create mode 100644 controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/propertyHistory.kt 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 e1b5a8d..35c9b89 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 @@ -4,9 +4,11 @@ import jetbrains.exodus.entitystore.Entity import jetbrains.exodus.entitystore.PersistentEntityStore import jetbrains.exodus.entitystore.PersistentEntityStores import jetbrains.exodus.entitystore.StoreTransaction +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import kotlinx.datetime.Instant -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.serialDescriptor import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject @@ -65,12 +67,12 @@ public class XodusDeviceMessageStorage( } } - override suspend fun readAll(): List = entityStore.computeInReadonlyTransaction { transaction -> + override fun readAll(): Flow = entityStore.computeInReadonlyTransaction { transaction -> transaction.sort( DEVICE_MESSAGE_ENTITY_TYPE, DeviceMessage::time.name, true - ).map { + ).asFlow().map { Json.decodeFromString( DeviceMessage.serializer(), it.getBlobString("json") ?: error("No json content found") @@ -78,17 +80,17 @@ public class XodusDeviceMessageStorage( } } - override suspend fun read( + override fun read( eventType: String, range: ClosedRange?, sourceDevice: Name?, targetDevice: Name?, - ): List = entityStore.computeInReadonlyTransaction { transaction -> + ): Flow = entityStore.computeInReadonlyTransaction { transaction -> transaction.find( DEVICE_MESSAGE_ENTITY_TYPE, "type", eventType - ).asSequence().filter { + ).asFlow().filter { it.timeInRange(range) && it.propertyMatchesName(DeviceMessage::sourceDevice.name, sourceDevice) && it.propertyMatchesName(DeviceMessage::targetDevice.name, targetDevice) @@ -97,7 +99,7 @@ public class XodusDeviceMessageStorage( DeviceMessage.serializer(), it.getBlobString("json") ?: error("No json content found") ) - }.sortedBy { it.time }.toList() + } } override fun close() { @@ -120,17 +122,4 @@ public class XodusDeviceMessageStorage( return XodusDeviceMessageStorage(entityStore) } } -} - -/** - * Query all messages of given type - */ -@OptIn(ExperimentalSerializationApi::class) -public suspend inline fun XodusDeviceMessageStorage.query( - range: ClosedRange? = null, - sourceDevice: Name? = null, - targetDevice: Name? = null, -): List = read(serialDescriptor().serialName, range, sourceDevice, targetDevice).map { - //Check that all types are correct - it as T -} +} \ No newline at end of file diff --git a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/DeviceMessageStorage.kt b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/DeviceMessageStorage.kt index 87f4b74..b9cc84f 100644 --- a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/DeviceMessageStorage.kt +++ b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/DeviceMessageStorage.kt @@ -1,6 +1,10 @@ package space.kscience.controls.storage +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.datetime.Instant +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.serialDescriptor import space.kscience.controls.api.DeviceMessage import space.kscience.dataforge.names.Name @@ -10,14 +14,34 @@ import space.kscience.dataforge.names.Name public interface DeviceMessageStorage { public suspend fun write(event: DeviceMessage) - public suspend fun readAll(): List + /** + * Return all messages in a storage as a flow + */ + public fun readAll(): Flow - public suspend fun read( + /** + * Flow messages with given [eventType] and filters by [range], [sourceDevice] and [targetDevice]. + * Null in filters means that there is not filtering for this field. + */ + public fun read( eventType: String, range: ClosedRange? = null, sourceDevice: Name? = null, targetDevice: Name? = null, - ): List + ): Flow public fun close() +} + +/** + * Query all messages of given type + */ +@OptIn(ExperimentalSerializationApi::class) +public inline fun DeviceMessageStorage.read( + range: ClosedRange? = null, + sourceDevice: Name? = null, + targetDevice: Name? = null, +): Flow = read(serialDescriptor().serialName, range, sourceDevice, targetDevice).map { + //Check that all types are correct + it as T } \ No newline at end of file diff --git a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/propertyHistory.kt b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/propertyHistory.kt new file mode 100644 index 0000000..0ca30cc --- /dev/null +++ b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/propertyHistory.kt @@ -0,0 +1,20 @@ +package space.kscience.controls.storage + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.datetime.Instant +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.misc.PropertyHistory +import space.kscience.controls.misc.ValueWithTime +import space.kscience.dataforge.meta.transformations.MetaConverter + +public fun DeviceMessageStorage.propertyHistory( + propertyName: String, + converter: MetaConverter, +): PropertyHistory = object : PropertyHistory { + override fun flowHistory(from: Instant, until: Instant): Flow> = + read(from..until) + .filter { it.property == propertyName } + .map { ValueWithTime(converter.metaToObject(it.value), it.time) } +} \ No newline at end of file diff --git a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt index 04b633a..ed96efc 100644 --- a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt +++ b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt @@ -39,26 +39,4 @@ public fun DeviceManager.storeMessages( }.launchIn(context) } -///** -// * @return the list of deviceMessages that describes changes of specified property of specified device sorted by time -// * @param sourceDeviceName a name of device, history of which property we want to get -// * @param propertyName a name of property, history of which we want to get -// * @param factory a factory that produce mongo clients -// */ -//public suspend fun getPropertyHistory( -// sourceDeviceName: String, -// propertyName: String, -// factory: Factory, -// meta: Meta = Meta.EMPTY, -//): List { -// return factory(meta).use { -// it.getPropertyHistory(sourceDeviceName, propertyName) -// } -//} -// -// -//public enum class StorageKind { -// DEVICE_HUB, -// MAGIX_SERVER -//} From 2a700a5a2ab452c4fc8fa92a0d91f527ae633c18 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 4 Mar 2024 15:24:27 +0300 Subject: [PATCH 58/71] Migrate to DataForge 0.8.0 --- build.gradle.kts | 4 ++-- .../controls/constructor/DeviceConstructor.kt | 2 +- .../controls/constructor/DeviceGroup.kt | 6 +----- .../controls/constructor/DeviceState.kt | 18 ++++++++--------- .../kscience/controls/constructor/Drive.kt | 2 +- .../controls/constructor/Regulator.kt | 2 +- .../controls/constructor/customState.kt | 2 +- .../space/kscience/controls/api/Device.kt | 2 +- .../controls/manager/respondMessage.kt | 4 ++-- .../kscience/controls/misc/PropertyHistory.kt | 4 ++-- .../kscience/controls/misc/ValueWithTime.kt | 20 ++++++++----------- .../kscience/controls/spec/DeviceBase.kt | 10 +++++----- .../controls/spec/DeviceMetaPropertySpec.kt | 4 ++-- .../controls/spec/DevicePropertySpec.kt | 18 ++++++++--------- .../kscience/controls/spec/DeviceSpec.kt | 10 +++------- .../space/kscience/controls/spec/misc.kt | 13 ++++-------- .../controls/spec/propertySpecDelegates.kt | 6 +++--- .../src/jsMain/kotlin/commonJupyter.kt | 2 +- .../kscience/controls/client/DeviceClient.kt | 6 +++--- .../controls/client/clientPropertyAccess.kt | 20 +++++++++---------- .../kscience/controls/client/tangoMagix.kt | 6 +++--- .../controls/opcua/client/OpcUaDevice.kt | 8 ++++---- .../opcua/client/OpcUaDeviceBySpec.kt | 2 +- .../controls/opcua/client/OpcUaClientTest.kt | 2 +- .../xodus/XodusDeviceMessageStorage.kt | 10 ++++------ .../src/test/kotlin/PropertyHistoryTest.kt | 5 +++-- .../controls/storage/propertyHistory.kt | 4 ++-- .../src/commonMain/kotlin/plotExtensions.kt | 2 +- .../jsMain/kotlin/ControlsVisionPlugin.js.kt | 5 ++--- controls-vision/src/jsMain/kotlin/client.kt | 2 +- demo/all-things/build.gradle.kts | 2 +- .../kscience/controls/demo/DemoDevice.kt | 2 +- .../controls/demo/car/MagixVirtualCar.kt | 2 +- .../kscience/controls/demo/car/VirtualCar.kt | 20 ++++++------------- .../sciprog/devices/mks/MksPdr900Device.kt | 4 ++-- .../pimotionmaster/PiMotionMasterDevice.kt | 6 +----- 36 files changed, 103 insertions(+), 134 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index aebfc4f..b0b93d5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,8 +5,8 @@ plugins { id("space.kscience.gradle.project") } -val dataforgeVersion: String by extra("0.7.1") -val visionforgeVersion by extra("0.3.1") +val dataforgeVersion: String by extra("0.8.0") +val visionforgeVersion by extra("0.4.0") 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-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt index 5dbbe13..5c11f27 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt @@ -5,7 +5,7 @@ import space.kscience.controls.api.PropertyDescriptor import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName import kotlin.properties.PropertyDelegateProvider diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt index 26b5896..6cdb784 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -12,11 +12,7 @@ import space.kscience.controls.manager.install import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory import space.kscience.dataforge.context.request -import space.kscience.dataforge.meta.Laminate -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.MutableMeta -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.meta.* import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.names.* import kotlin.collections.set diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index 672b0a6..1828edd 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -10,7 +10,7 @@ import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.MutableDevicePropertySpec import space.kscience.controls.spec.name import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.meta.MetaConverter import kotlin.time.Duration /** @@ -25,9 +25,9 @@ public interface DeviceState { public companion object } -public val DeviceState.metaFlow: Flow get() = valueFlow.map(converter::objectToMeta) +public val DeviceState.metaFlow: Flow get() = valueFlow.map(converter::convert) -public val DeviceState.valueAsMeta: Meta get() = converter.objectToMeta(value) +public val DeviceState.valueAsMeta: Meta get() = converter.convert(value) /** @@ -38,9 +38,9 @@ public interface MutableDeviceState : DeviceState { } public var MutableDeviceState.valueAsMeta: Meta - get() = converter.objectToMeta(value) + get() = converter.convert(value) set(arg) { - value = converter.metaToObject(arg) + value = converter.read(arg) } /** @@ -98,7 +98,7 @@ private open class BoundDeviceState( override val valueFlow: StateFlow = device.messageFlow.filterIsInstance().filter { it.property == propertyName }.mapNotNull { - converter.metaToObject(it.value) + converter.read(it.value) }.stateIn(device.context, SharingStarted.Eagerly, initialValue) override val value: T get() = valueFlow.value @@ -111,7 +111,7 @@ public suspend fun Device.propertyAsState( propertyName: String, metaConverter: MetaConverter, ): DeviceState { - val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") + val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed") return BoundDeviceState(metaConverter, this, propertyName, initialValue) } @@ -140,7 +140,7 @@ private class MutableBoundDeviceState( get() = valueFlow.value set(newValue) { device.launch { - device.writeProperty(propertyName, converter.objectToMeta(newValue)) + device.writeProperty(propertyName, converter.convert(newValue)) } } } @@ -155,7 +155,7 @@ public suspend fun Device.mutablePropertyAsState( propertyName: String, metaConverter: MetaConverter, ): MutableDeviceState { - val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") + val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed") return mutablePropertyAsState(propertyName, metaConverter, initialValue) } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt index c2391c8..8301622 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt @@ -9,9 +9,9 @@ import space.kscience.controls.manager.clock import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.transformations.MetaConverter import kotlin.math.pow import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt index 7495d33..cbe6967 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt @@ -2,7 +2,7 @@ package space.kscience.controls.constructor import space.kscience.controls.api.Device import space.kscience.controls.spec.* -import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.meta.MetaConverter /** diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt index 6e95ccd..100f755 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt @@ -2,7 +2,7 @@ package space.kscience.controls.constructor import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.meta.MetaConverter /** 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 89040c0..3226422 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 @@ -139,7 +139,7 @@ public interface CachingDevice : Device { /** * Get the logical state of property or suspend to read the physical value. */ -public suspend fun Device.requestProperty(propertyName: String): Meta = if (this is CachingDevice) { +public suspend fun Device.getOrReadProperty(propertyName: String): Meta = if (this is CachingDevice) { getProperty(propertyName) ?: readProperty(propertyName) } else { readProperty(propertyName) 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 3a0ac7b..fc6fb5d 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 @@ -16,7 +16,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess is PropertyGetMessage -> { PropertyChangedMessage( property = request.property, - value = requestProperty(request.property), + value = getOrReadProperty(request.property), sourceDevice = deviceTarget, targetDevice = request.sourceDevice ) @@ -26,7 +26,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess writeProperty(request.property, request.value) PropertyChangedMessage( property = request.property, - value = requestProperty(request.property), + value = getOrReadProperty(request.property), sourceDevice = deviceTarget, targetDevice = request.sourceDevice ) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt index 5b58321..96bd8d5 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt @@ -9,7 +9,7 @@ import space.kscience.controls.api.DeviceMessage import space.kscience.controls.api.PropertyChangedMessage import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.name -import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.meta.MetaConverter /** * An interface for device property history. @@ -42,7 +42,7 @@ public class CollectedPropertyHistory( private val store: SharedFlow> = eventFlow .filterIsInstance() .filter { it.property == propertyName } - .map { ValueWithTime(converter.metaToObject(it.value), it.time) } + .map { ValueWithTime(converter.read(it.value), it.time) } .shareIn(scope, started = SharingStarted.Eagerly, replay = maxSize) override fun flowHistory(from: Instant, until: Instant): Flow> = diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt index 0bf07d3..f25c4f4 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/ValueWithTime.kt @@ -5,10 +5,8 @@ import kotlinx.io.Sink import kotlinx.io.Source import space.kscience.dataforge.io.IOFormat import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.transformations.MetaConverter -import kotlin.reflect.KType -import kotlin.reflect.typeOf /** * A value coupled to a time it was obtained at @@ -36,8 +34,6 @@ public data class ValueWithTime(val value: T, val time: Instant) { } private class ValueWithTimeIOFormat(val valueFormat: IOFormat) : IOFormat> { - override val type: KType get() = typeOf>() - override fun readFrom(source: Source): ValueWithTime { val timestamp = InstantIOFormat.readFrom(source) @@ -56,18 +52,18 @@ private class ValueWithTimeMetaConverter( val valueConverter: MetaConverter, ) : MetaConverter> { - override val type: KType = typeOf>() - override fun metaToObjectOrNull( - meta: Meta, - ): ValueWithTime? = valueConverter.metaToObject(meta[ValueWithTime.META_VALUE_KEY] ?: Meta.EMPTY)?.let { - ValueWithTime(it, meta[ValueWithTime.META_TIME_KEY]?.instant ?: Instant.DISTANT_PAST) + override fun readOrNull( + source: Meta, + ): ValueWithTime? = valueConverter.read(source[ValueWithTime.META_VALUE_KEY] ?: Meta.EMPTY)?.let { + ValueWithTime(it, source[ValueWithTime.META_TIME_KEY]?.instant ?: Instant.DISTANT_PAST) } - override fun objectToMeta(obj: ValueWithTime): Meta = Meta { + override fun convert(obj: ValueWithTime): Meta = Meta { ValueWithTime.META_TIME_KEY put obj.time.toMeta() - ValueWithTime.META_VALUE_KEY put valueConverter.objectToMeta(obj.value) + ValueWithTime.META_VALUE_KEY put valueConverter.convert(obj.value) } } + public fun MetaConverter.withTime(): MetaConverter> = ValueWithTimeMetaConverter(this) \ No newline at end of file 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 846815c..dcb48b4 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 @@ -21,7 +21,7 @@ import kotlin.coroutines.CoroutineContext */ @OptIn(InternalDeviceAPI::class) private suspend fun MutableDevicePropertySpec.writeMeta(device: D, item: Meta) { - write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter")) + write(device, converter.readOrNull(item) ?: error("Meta $item could not be read with $converter")) } /** @@ -29,16 +29,16 @@ private suspend fun MutableDevicePropertySpec.writeMeta(de */ @OptIn(InternalDeviceAPI::class) private suspend fun DevicePropertySpec.readMeta(device: D): Meta? = - read(device)?.let(converter::objectToMeta) + read(device)?.let(converter::convert) private suspend fun DeviceActionSpec.executeWithMeta( device: D, item: Meta, ): Meta? { - val arg: I = inputConverter.metaToObject(item) ?: error("Failed to convert $item with $inputConverter") + val arg: I = inputConverter.readOrNull(item) ?: error("Failed to convert $item with $inputConverter") val res = execute(device, arg) - return res?.let { outputConverter.objectToMeta(res) } + return res?.let { outputConverter.convert(res) } } @@ -120,7 +120,7 @@ public abstract class DeviceBase( * Notify the device that a property with [spec] value is changed */ protected suspend fun propertyChanged(spec: DevicePropertySpec, value: T) { - propertyChanged(spec.name, spec.converter.objectToMeta(value)) + propertyChanged(spec.name, spec.converter.convert(value)) } /** diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceMetaPropertySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceMetaPropertySpec.kt index 809d940..c5fe8b5 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceMetaPropertySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceMetaPropertySpec.kt @@ -3,9 +3,9 @@ package space.kscience.controls.spec import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.meta.MetaConverter -internal object DeviceMetaPropertySpec: DevicePropertySpec { +internal object DeviceMetaPropertySpec : DevicePropertySpec { override val descriptor: PropertyDescriptor = PropertyDescriptor("@meta") override val converter: MetaConverter = MetaConverter.meta 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 8e0d6de..edf9f93 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 @@ -5,7 +5,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import space.kscience.controls.api.* -import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.meta.MetaConverter /** @@ -72,23 +72,23 @@ 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("Property read result is not valid") + propertySpec.converter.readOrNull(readProperty(propertySpec.name)) ?: error("Property read result is not valid") /** * Read typed value and update/push event if needed. * Return null if property read is not successful or property is undefined. */ public suspend fun > D.readOrNull(propertySpec: DevicePropertySpec): T? = - readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject) + readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::readOrNull) -public suspend fun D.request(propertySpec: DevicePropertySpec): T = - propertySpec.converter.metaToObject(requestProperty(propertySpec.name)) +public suspend fun D.getOrRead(propertySpec: DevicePropertySpec): T = + propertySpec.converter.read(getOrReadProperty(propertySpec.name)) /** * Write typed property state and invalidate logical state */ public suspend fun D.write(propertySpec: MutableDevicePropertySpec, value: T) { - writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value)) + writeProperty(propertySpec.name, propertySpec.converter.convert(value)) } /** @@ -104,7 +104,7 @@ public fun D.writeAsync(propertySpec: MutableDevicePropertySpec< public fun D.propertyFlow(spec: DevicePropertySpec): Flow = messageFlow .filterIsInstance() .filter { it.property == spec.name } - .mapNotNull { spec.converter.metaToObject(it.value) } + .mapNotNull { spec.converter.read(it.value) } /** * A type safe property change listener. Uses the device [CoroutineScope]. @@ -117,7 +117,7 @@ public fun D.onPropertyChange( .filterIsInstance() .filter { it.property == spec.name } .onEach { change -> - val newValue = spec.converter.metaToObject(change.value) + val newValue = spec.converter.read(change.value) if (newValue != null) { change.callback(newValue) } @@ -136,7 +136,7 @@ public fun D.useProperty( .filterIsInstance() .filter { it.property == spec.name } .collect { change -> - val newValue = spec.converter.metaToObject(change.value) + val newValue = spec.converter.readOrNull(change.value) if (newValue != null) { callback(newValue) } 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 4091921..2f90cb8 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 @@ -5,20 +5,16 @@ import space.kscience.controls.api.ActionDescriptor import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.meta.MetaConverter import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty -import kotlin.reflect.KType -import kotlin.reflect.typeOf public object UnitMetaConverter : MetaConverter { - override val type: KType = typeOf() + override fun readOrNull(source: Meta): Unit = Unit - override fun metaToObjectOrNull(meta: Meta): Unit = Unit - - override fun objectToMeta(obj: Unit): Meta = Meta.EMPTY + override fun convert(obj: Unit): Meta = Meta.EMPTY } public val MetaConverter.Companion.unit: MetaConverter get() = UnitMetaConverter diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt index ee14237..1fc4649 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt @@ -1,9 +1,6 @@ package space.kscience.controls.spec import space.kscience.dataforge.meta.* -import space.kscience.dataforge.meta.transformations.MetaConverter -import kotlin.reflect.KType -import kotlin.reflect.typeOf import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -12,16 +9,14 @@ public fun Double.asMeta(): Meta = Meta(asValue()) //TODO to be moved to DF public object DurationConverter : MetaConverter { - override val type: KType = typeOf() - - override fun metaToObjectOrNull(meta: Meta): Duration = meta.value?.double?.toDuration(DurationUnit.SECONDS) + override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS) ?: run { - val unit: DurationUnit = meta["unit"].enum() ?: DurationUnit.SECONDS - val value = meta[Meta.VALUE_KEY].double ?: error("No value present for Duration") + val unit: DurationUnit = source["unit"].enum() ?: DurationUnit.SECONDS + val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration") return@run value.toDuration(unit) } - override fun objectToMeta(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta() + override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta() } public val MetaConverter.Companion.duration: MetaConverter get() = DurationConverter \ No newline at end of file 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 231891c..8bd22b6 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 @@ -4,8 +4,8 @@ import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.metaDescriptor import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.ValueType -import space.kscience.dataforge.meta.transformations.MetaConverter import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KMutableProperty1 @@ -53,8 +53,8 @@ public fun > DeviceSpec.logicalProperty( converter, descriptorBuilder, name, - read = { propertyName -> getProperty(propertyName)?.let(converter::metaToObject) }, - write = { propertyName, value -> writeProperty(propertyName, converter.objectToMeta(value)) } + read = { propertyName -> getProperty(propertyName)?.let(converter::readOrNull) }, + write = { propertyName, value -> writeProperty(propertyName, converter.convert(value)) } ) diff --git a/controls-jupyter/src/jsMain/kotlin/commonJupyter.kt b/controls-jupyter/src/jsMain/kotlin/commonJupyter.kt index 41711d7..388e1ab 100644 --- a/controls-jupyter/src/jsMain/kotlin/commonJupyter.kt +++ b/controls-jupyter/src/jsMain/kotlin/commonJupyter.kt @@ -1,7 +1,7 @@ +import space.kscience.visionforge.html.runVisionClient import space.kscience.visionforge.jupyter.VFNotebookClient import space.kscience.visionforge.markup.MarkupPlugin import space.kscience.visionforge.plotly.PlotlyPlugin -import space.kscience.visionforge.runVisionClient public fun main(): Unit = runVisionClient { // plugin(DeviceManager) diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt index 31dbd89..51836b9 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt @@ -144,7 +144,7 @@ public fun MagixEndpoint.controlsPropertyFlow( .filter { message -> message.sourceDevice == deviceName && message.property == propertySpec.name }.map { - propertySpec.converter.metaToObject(it.value) + propertySpec.converter.read(it.value) } } @@ -157,7 +157,7 @@ public suspend fun MagixEndpoint.sendControlsPropertyChange( ) { val message = PropertySetMessage( property = propertySpec.name, - value = propertySpec.converter.objectToMeta(value), + value = propertySpec.converter.convert(value), targetDevice = deviceName ) send(DeviceManager.magixFormat, message, source = sourceEndpointName, target = targetEndpointName) @@ -177,6 +177,6 @@ public fun MagixEndpoint.controlsPropertyMessageFlow( .filter { message -> message.sourceDevice == deviceName && message.property == propertySpec.name }.map { - it to propertySpec.converter.metaToObject(it.value) + it to propertySpec.converter.read(it.value) } } \ No newline at end of file diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt index 22b0413..20c59e8 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import space.kscience.controls.api.PropertyChangedMessage -import space.kscience.controls.api.requestProperty +import space.kscience.controls.api.getOrReadProperty import space.kscience.controls.spec.DeviceActionSpec import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.MutableDevicePropertySpec @@ -17,14 +17,14 @@ import space.kscience.dataforge.meta.Meta * An accessor that allows DeviceClient to connect to any property without type checks */ public suspend fun DeviceClient.read(propertySpec: DevicePropertySpec<*, T>): T = - propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Property read result is not valid") + propertySpec.converter.readOrNull(readProperty(propertySpec.name)) ?: error("Property read result is not valid") public suspend fun DeviceClient.request(propertySpec: DevicePropertySpec<*, T>): T = - propertySpec.converter.metaToObject(requestProperty(propertySpec.name)) + propertySpec.converter.read(getOrReadProperty(propertySpec.name)) public suspend fun DeviceClient.write(propertySpec: MutableDevicePropertySpec<*, T>, value: T) { - writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value)) + writeProperty(propertySpec.name, propertySpec.converter.convert(value)) } public fun DeviceClient.writeAsync(propertySpec: MutableDevicePropertySpec<*, T>, value: T): Job = launch { @@ -34,7 +34,7 @@ public fun DeviceClient.writeAsync(propertySpec: MutableDevicePropertySpec<* public fun DeviceClient.propertyFlow(spec: DevicePropertySpec<*, T>): Flow = messageFlow .filterIsInstance() .filter { it.property == spec.name } - .mapNotNull { spec.converter.metaToObject(it.value) } + .mapNotNull { spec.converter.readOrNull(it.value) } public fun DeviceClient.onPropertyChange( spec: DevicePropertySpec<*, T>, @@ -44,7 +44,7 @@ public fun DeviceClient.onPropertyChange( .filterIsInstance() .filter { it.property == spec.name } .onEach { change -> - val newValue = spec.converter.metaToObject(change.value) + val newValue = spec.converter.readOrNull(change.value) if (newValue != null) { change.callback(newValue) } @@ -60,7 +60,7 @@ public fun DeviceClient.useProperty( .filterIsInstance() .filter { it.property == spec.name } .collect { change -> - val newValue = spec.converter.metaToObject(change.value) + val newValue = spec.converter.readOrNull(change.value) if (newValue != null) { callback(newValue) } @@ -68,12 +68,12 @@ public fun DeviceClient.useProperty( } public suspend fun DeviceClient.execute(actionSpec: DeviceActionSpec<*, I, O>, input: I): O { - val inputMeta = actionSpec.inputConverter.objectToMeta(input) + val inputMeta = actionSpec.inputConverter.convert(input) val res = execute(actionSpec.name, inputMeta) - return actionSpec.outputConverter.metaToObject(res ?: Meta.EMPTY) + return actionSpec.outputConverter.read(res ?: Meta.EMPTY) } public suspend fun DeviceClient.execute(actionSpec: DeviceActionSpec<*, Unit, O>): O { val res = execute(actionSpec.name, Meta.EMPTY) - return actionSpec.outputConverter.metaToObject(res ?: Meta.EMPTY) + return actionSpec.outputConverter.read(res ?: Meta.EMPTY) } \ No newline at end of file diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt index 9c30f8c..d0bdda4 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import space.kscience.controls.api.get -import space.kscience.controls.api.requestProperty +import space.kscience.controls.api.getOrReadProperty import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.context.error import space.kscience.dataforge.context.logger @@ -91,7 +91,7 @@ public fun DeviceManager.launchTangoMagix( val device = get(payload.device) when (payload.action) { TangoAction.read -> { - val value = device.requestProperty(payload.name) + val value = device.getOrReadProperty(payload.name) respond(request, payload) { requestPayload -> requestPayload.copy( value = value, @@ -104,7 +104,7 @@ public fun DeviceManager.launchTangoMagix( device.writeProperty(payload.name, value) } //wait for value to be written and return final state - val value = device.requestProperty(payload.name) + val value = device.getOrReadProperty(payload.name) respond(request, payload) { requestPayload -> requestPayload.copy( value = value, diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDevice.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDevice.kt index 51e6032..08a84b0 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDevice.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDevice.kt @@ -9,8 +9,8 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.* import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn import space.kscience.controls.api.Device import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.MetaSerializer -import space.kscience.dataforge.meta.transformations.MetaConverter import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -43,7 +43,7 @@ public suspend inline fun OpcUaDevice.readOpcWithTime( else -> error("Incompatible OPC property value $content") } - val res: T = converter.metaToObject(meta) + val res: T = converter.read(meta) return res to time } @@ -69,7 +69,7 @@ public suspend inline fun OpcUaDevice.readOpc( else -> error("Incompatible OPC property value $content") } - return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}") + return converter.readOrNull(meta) ?: error("Meta $meta could not be converted to ${T::class}") } public suspend inline fun OpcUaDevice.writeOpc( @@ -77,7 +77,7 @@ public suspend inline fun OpcUaDevice.writeOpc( converter: MetaConverter, value: T ): StatusCode { - val meta = converter.objectToMeta(value) + val meta = converter.convert(value) return client.writeValue(nodeId, DataValue(Variant(meta))).await() } diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt index 188760e..7debc0e 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt @@ -31,7 +31,7 @@ public class MiloConfiguration : Scheme() { public var endpointUrl: String by string { error("Endpoint url is not defined") } - public var username: MiloUsername? by specOrNull(MiloUsername) + public var username: MiloUsername? by schemeOrNull(MiloUsername) public var securityPolicy: SecurityPolicy by enum(SecurityPolicy.None) 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 33aa8fb..b4d63ad 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 @@ -7,7 +7,7 @@ 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 +import space.kscience.dataforge.meta.MetaConverter import kotlin.test.Ignore class OpcUaClientTest { 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 35c9b89..559d2a0 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 @@ -6,8 +6,6 @@ import jetbrains.exodus.entitystore.PersistentEntityStores import jetbrains.exodus.entitystore.StoreTransaction import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map import kotlinx.datetime.Instant import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -72,13 +70,13 @@ public class XodusDeviceMessageStorage( DEVICE_MESSAGE_ENTITY_TYPE, DeviceMessage::time.name, true - ).asFlow().map { + ).map { Json.decodeFromString( DeviceMessage.serializer(), it.getBlobString("json") ?: error("No json content found") ) } - } + }.asFlow() override fun read( eventType: String, @@ -90,7 +88,7 @@ public class XodusDeviceMessageStorage( DEVICE_MESSAGE_ENTITY_TYPE, "type", eventType - ).asFlow().filter { + ).filter { it.timeInRange(range) && it.propertyMatchesName(DeviceMessage::sourceDevice.name, sourceDevice) && it.propertyMatchesName(DeviceMessage::targetDevice.name, targetDevice) @@ -100,7 +98,7 @@ public class XodusDeviceMessageStorage( it.getBlobString("json") ?: error("No json content found") ) } - } + }.asFlow() override fun close() { entityStore.close() diff --git a/controls-storage/controls-xodus/src/test/kotlin/PropertyHistoryTest.kt b/controls-storage/controls-xodus/src/test/kotlin/PropertyHistoryTest.kt index 1724079..e7017b1 100644 --- a/controls-storage/controls-xodus/src/test/kotlin/PropertyHistoryTest.kt +++ b/controls-storage/controls-xodus/src/test/kotlin/PropertyHistoryTest.kt @@ -1,5 +1,6 @@ import jetbrains.exodus.entitystore.PersistentEntityStores import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant import org.junit.jupiter.api.AfterAll @@ -7,8 +8,8 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.storage.read import space.kscience.controls.xodus.XodusDeviceMessageStorage -import space.kscience.controls.xodus.query import space.kscience.controls.xodus.writeMessage import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.Name @@ -67,7 +68,7 @@ internal class PropertyHistoryTest { XodusDeviceMessageStorage(entityStore).use { storage -> assertEquals( propertyChangedMessages[0], - storage.query( + storage.read( sourceDevice = "virtual-car".asName() ).first { it.property == "speed" } ) diff --git a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/propertyHistory.kt b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/propertyHistory.kt index 0ca30cc..b52ed2b 100644 --- a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/propertyHistory.kt +++ b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/propertyHistory.kt @@ -7,7 +7,7 @@ import kotlinx.datetime.Instant import space.kscience.controls.api.PropertyChangedMessage import space.kscience.controls.misc.PropertyHistory import space.kscience.controls.misc.ValueWithTime -import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.meta.MetaConverter public fun DeviceMessageStorage.propertyHistory( propertyName: String, @@ -16,5 +16,5 @@ public fun DeviceMessageStorage.propertyHistory( override fun flowHistory(from: Instant, until: Instant): Flow> = read(from..until) .filter { it.property == propertyName } - .map { ValueWithTime(converter.metaToObject(it.value), it.time) } + .map { ValueWithTime(converter.read(it.value), it.time) } } \ No newline at end of file diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index fde716b..6e92165 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -149,7 +149,7 @@ private fun Trace.updateFromState( public fun Plot.plotDeviceState( context: Context, state: DeviceState, - extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: Null }, + extractValue: T.() -> Value = { state.converter.convert(this).value ?: Null }, maxAge: Duration = defaultMaxAge, maxPoints: Int = defaultMaxPoints, minPoints: Int = defaultMinPoints, diff --git a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt index b34cc44..017989b 100644 --- a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt +++ b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt @@ -7,10 +7,9 @@ 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 space.kscience.visionforge.ElementVisionRenderer import space.kscience.visionforge.Vision -import space.kscience.visionforge.VisionClient import space.kscience.visionforge.VisionPlugin +import space.kscience.visionforge.html.ElementVisionRenderer public actual class ControlVisionPlugin : VisionPlugin(), ElementVisionRenderer { override val tag: PluginTag get() = Companion.tag @@ -21,7 +20,7 @@ public actual class ControlVisionPlugin : VisionPlugin(), ElementVisionRenderer TODO("Not yet implemented") } - override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) { + override fun render(element: Element, name: Name, vision: Vision, meta: Meta) { TODO("Not yet implemented") } diff --git a/controls-vision/src/jsMain/kotlin/client.kt b/controls-vision/src/jsMain/kotlin/client.kt index a835f9a..78da2e8 100644 --- a/controls-vision/src/jsMain/kotlin/client.kt +++ b/controls-vision/src/jsMain/kotlin/client.kt @@ -1,8 +1,8 @@ package space.kscience.controls.vision +import space.kscience.visionforge.html.runVisionClient import space.kscience.visionforge.markup.MarkupPlugin import space.kscience.visionforge.plotly.PlotlyPlugin -import space.kscience.visionforge.runVisionClient public fun main(): Unit = runVisionClient { plugin(PlotlyPlugin) diff --git a/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts index 6888d35..1496f67 100644 --- a/demo/all-things/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("no.tornado:tornadofx:1.7.20") - implementation("space.kscience:plotlykt-server:0.6.1") + implementation("space.kscience:plotlykt-server:0.7.1") // implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") implementation(spclibs.logback.classic) } diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt index 657d275..5cf1b28 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 @@ -7,9 +7,9 @@ import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaConverter 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 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 2c5d65a..27ea243 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 @@ -22,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" -> write(IVirtualCar.acceleration, Vector2D.metaToObject(message.value)) + "acceleration" -> write(IVirtualCar.acceleration, Vector2D.read(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 05bb2f0..86564a5 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 @@ -11,34 +11,26 @@ 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 -import space.kscience.dataforge.meta.MetaRepr -import space.kscience.dataforge.meta.double -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.meta.* import kotlin.math.pow -import kotlin.reflect.KType -import kotlin.reflect.typeOf import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime data class Vector2D(var x: Double = 0.0, var y: Double = 0.0) : MetaRepr { - override fun toMeta(): Meta = objectToMeta(this) + override fun toMeta(): Meta = convert(this) operator fun div(arg: Double): Vector2D = Vector2D(x / arg, y / arg) companion object CoordinatesMetaConverter : MetaConverter { - override val type: KType = typeOf() - - override fun metaToObjectOrNull(meta: Meta): Vector2D = Vector2D( - meta["x"].double ?: 0.0, - meta["y"].double ?: 0.0 + override fun readOrNull(source: Meta): Vector2D = Vector2D( + source["x"].double ?: 0.0, + source["y"].double ?: 0.0 ) - override fun objectToMeta(obj: Vector2D): Meta = Meta { + override fun convert(obj: Vector2D): Meta = Meta { "x" put obj.x "y" put obj.y } 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 cb8030c..1d48553 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 @@ -10,9 +10,9 @@ 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.MetaConverter import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.int -import space.kscience.dataforge.meta.transformations.MetaConverter //TODO this device is not tested @@ -94,7 +94,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec Date: Mon, 4 Mar 2024 15:58:53 +0300 Subject: [PATCH 59/71] Update readme and API --- CHANGELOG.md | 30 ++++++++++++++----- README.md | 26 +++++++++++++--- build.gradle.kts | 2 +- controls-core/README.md | 6 ++-- controls-jupyter/api/controls-jupyter.api | 8 +++++ controls-magix/README.md | 6 ++-- controls-modbus/README.md | 6 ++-- controls-opcua/README.md | 6 ++-- controls-pi/README.md | 6 ++-- controls-pi/api/controls-pi.api | 11 +++++-- controls-ports-ktor/README.md | 6 ++-- controls-serial/README.md | 6 ++-- controls-server/README.md | 6 ++-- controls-storage/README.md | 6 ++-- controls-storage/controls-xodus/README.md | 6 ++-- demo/all-things/api/all-things.api | 7 +++-- demo/car/api/car.api | 15 +++++----- demo/constructor/api/constructor.api | 27 +++++++++++++++++ demo/mks-pdr900/api/mks-pdr900.api | 14 ++------- demo/motors/api/motors.api | 14 ++++----- magix/magix-api/README.md | 6 ++-- magix/magix-java-endpoint/README.md | 6 ++-- magix/magix-mqtt/README.md | 6 ++-- magix/magix-rabbit/README.md | 6 ++-- magix/magix-rsocket/README.md | 6 ++-- magix/magix-server/README.md | 6 ++-- magix/magix-storage/README.md | 6 ++-- .../magix-storage-xodus/README.md | 6 ++-- magix/magix-zmq/README.md | 6 ++-- 29 files changed, 147 insertions(+), 121 deletions(-) create mode 100644 controls-jupyter/api/controls-jupyter.api create mode 100644 demo/constructor/api/constructor.api diff --git a/CHANGELOG.md b/CHANGELOG.md index 8183d79..30a9fee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,35 +3,49 @@ ## Unreleased ### Added -- Device lifecycle message -- Low-code constructor -- Automatic description generation for spec properties (JVM only) ### Changed -- Property caching moved from core `Device` to the `CachingDevice` -- `DeviceSpec` properties no explicitly pass property name to getters and setters. -- `DeviceHub.respondHubMessage` now returns a list of messages to allow querying multiple devices. Device server also returns an array. ### Deprecated ### Removed ### Fixed + +### Security + +## 0.3.0 - 2024-03-04 + +### Added + +- Device lifecycle message +- Low-code constructor +- Automatic description generation for spec properties (JVM only) + +### Changed + +- Property caching moved from core `Device` to the `CachingDevice` +- `DeviceSpec` properties no explicitly pass property name to getters and setters. +- `DeviceHub.respondHubMessage` now returns a list of messages to allow querying multiple devices. Device server also returns an array. +- DataForge 0.8.0 + +### Fixed + - Property writing does not trigger change if logical state already is the same as value to be set. - Modbus-slave triggers only once for multi-register write. - Removed unnecessary scope in hub messageFlow -### Security - ## 0.2.2-dev-1 - 2023-09-24 ### Changed + - updating logical state in `DeviceBase` is now protected and called `propertyChanged()` - `DeviceBase` tries to read property after write if the writer does not set the value. ## 0.2.1 - 2023-09-24 ### Added + - Core interfaces for building a device server - Magix service for binding controls devices (both as RPC client and server) - A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols diff --git a/README.md b/README.md index 5518d1f..d6a602b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ [![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) -[![DOI](https://zenodo.org/badge/240888288.svg)](https://zenodo.org/badge/latestdoi/240888288) +[![](https://maven.sciprog.center/api/badge/latest/kscience/space/kscience/controls-core-jvm?color=40c14a&name=repo.kotlin.link&prefix=v)](https://maven.sciprog.center/) -# Controls-kt +# 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. +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, +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: @@ -44,6 +44,11 @@ Example view of a demo: ## Modules +### [controls-constructor](controls-constructor) +> A low-code constructor for composite devices simulation +> +> **Maturity**: PROTOTYPE + ### [controls-core](controls-core) > Core interfaces for building a device server > @@ -58,6 +63,10 @@ Example view of a demo: > - [ports](controls-core/src/commonMain/kotlin/space/kscience/controls/ports) : Working with asynchronous data sending and receiving raw byte arrays +### [controls-jupyter](controls-jupyter) +> +> **Maturity**: EXPERIMENTAL + ### [controls-magix](controls-magix) > Magix service for binding controls devices (both as RPC client and server) > @@ -115,6 +124,11 @@ Automatically checks consistency. > > **Maturity**: PROTOTYPE +### [controls-vision](controls-vision) +> Dashboard and visualization extensions for devices +> +> **Maturity**: PROTOTYPE + ### [demo](demo) > > **Maturity**: EXPERIMENTAL @@ -136,6 +150,10 @@ Automatically checks consistency. > > **Maturity**: EXPERIMENTAL +### [demo/constructor](demo/constructor) +> +> **Maturity**: EXPERIMENTAL + ### [demo/echo](demo/echo) > > **Maturity**: EXPERIMENTAL diff --git a/build.gradle.kts b/build.gradle.kts index b0b93d5..99946f1 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.3.0-dev-6" + version = "0.3.0" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-core/README.md b/controls-core/README.md index b75961d..b1edbdc 100644 --- a/controls-core/README.md +++ b/controls-core/README.md @@ -16,18 +16,16 @@ Core interfaces for building a device server ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-core:0.2.0`. +The Maven coordinates of this project are `space.kscience:controls-core:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:controls-core:0.2.0") + implementation("space.kscience:controls-core:0.3.0") } ``` diff --git a/controls-jupyter/api/controls-jupyter.api b/controls-jupyter/api/controls-jupyter.api new file mode 100644 index 0000000..726b523 --- /dev/null +++ b/controls-jupyter/api/controls-jupyter.api @@ -0,0 +1,8 @@ +public final class space/kscience/controls/jupyter/ControlsJupyter : space/kscience/visionforge/jupyter/VisionForgeIntegration { + public static final field Companion Lspace/kscience/controls/jupyter/ControlsJupyter$Companion; + public fun ()V +} + +public final class space/kscience/controls/jupyter/ControlsJupyter$Companion { +} + diff --git a/controls-magix/README.md b/controls-magix/README.md index 5473f02..4048990 100644 --- a/controls-magix/README.md +++ b/controls-magix/README.md @@ -12,18 +12,16 @@ Magix service for binding controls devices (both as RPC client and server) ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-magix:0.2.0`. +The Maven coordinates of this project are `space.kscience:controls-magix:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:controls-magix:0.2.0") + implementation("space.kscience:controls-magix:0.3.0") } ``` diff --git a/controls-modbus/README.md b/controls-modbus/README.md index 78d515a..18c37ee 100644 --- a/controls-modbus/README.md +++ b/controls-modbus/README.md @@ -14,18 +14,16 @@ Automatically checks consistency. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-modbus:0.2.0`. +The Maven coordinates of this project are `space.kscience:controls-modbus:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:controls-modbus:0.2.0") + implementation("space.kscience:controls-modbus:0.3.0") } ``` diff --git a/controls-opcua/README.md b/controls-opcua/README.md index 8cdd373..3befbb9 100644 --- a/controls-opcua/README.md +++ b/controls-opcua/README.md @@ -12,18 +12,16 @@ A client and server connectors for OPC-UA via Eclipse Milo ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-opcua:0.2.0`. +The Maven coordinates of this project are `space.kscience:controls-opcua:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:controls-opcua:0.2.0") + implementation("space.kscience:controls-opcua:0.3.0") } ``` diff --git a/controls-pi/README.md b/controls-pi/README.md index cd9ee0a..6235995 100644 --- a/controls-pi/README.md +++ b/controls-pi/README.md @@ -6,18 +6,16 @@ Utils to work with controls-kt on Raspberry pi ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-pi:0.2.0`. +The Maven coordinates of this project are `space.kscience:controls-pi:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:controls-pi:0.2.0") + implementation("space.kscience:controls-pi:0.3.0") } ``` diff --git a/controls-pi/api/controls-pi.api b/controls-pi/api/controls-pi.api index 2fdaf2d..34bfd53 100644 --- a/controls-pi/api/controls-pi.api +++ b/controls-pi/api/controls-pi.api @@ -1,6 +1,10 @@ public final class space/kscience/controls/pi/PiPlugin : space/kscience/dataforge/context/AbstractPlugin { public static final field Companion Lspace/kscience/controls/pi/PiPlugin$Companion; public fun ()V + public fun content (Ljava/lang/String;)Ljava/util/Map; + public fun detach ()V + public final fun getDevices ()Lspace/kscience/controls/manager/DeviceManager; + public final fun getPiContext ()Lcom/pi4j/context/Context; public final fun getPorts ()Lspace/kscience/controls/ports/Ports; public fun getTag ()Lspace/kscience/dataforge/context/PluginTag; } @@ -8,15 +12,16 @@ public final class space/kscience/controls/pi/PiPlugin : space/kscience/dataforg public final class space/kscience/controls/pi/PiPlugin$Companion : space/kscience/dataforge/context/PluginFactory { public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object; public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/pi/PiPlugin; + public final fun createPiContext (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lcom/pi4j/context/Context; public fun getTag ()Lspace/kscience/dataforge/context/PluginTag; } public final class space/kscience/controls/pi/PiSerialPort : space/kscience/controls/ports/AbstractPort { public static final field Companion Lspace/kscience/controls/pi/PiSerialPort$Companion; - public fun (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close ()V - public final fun getSerialBuilder ()Lkotlin/jvm/functions/Function0; + public final fun getSerialBuilder ()Lkotlin/jvm/functions/Function1; } public final class space/kscience/controls/pi/PiSerialPort$Companion : space/kscience/controls/ports/PortFactory { diff --git a/controls-ports-ktor/README.md b/controls-ports-ktor/README.md index 7f935f9..b703521 100644 --- a/controls-ports-ktor/README.md +++ b/controls-ports-ktor/README.md @@ -6,18 +6,16 @@ Implementation of byte ports on top os ktor-io asynchronous API ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.2.0`. +The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:controls-ports-ktor:0.2.0") + implementation("space.kscience:controls-ports-ktor:0.3.0") } ``` diff --git a/controls-serial/README.md b/controls-serial/README.md index 209055d..861b6d3 100644 --- a/controls-serial/README.md +++ b/controls-serial/README.md @@ -6,18 +6,16 @@ Implementation of direct serial port communication with JSerialComm ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-serial:0.2.0`. +The Maven coordinates of this project are `space.kscience:controls-serial:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:controls-serial:0.2.0") + implementation("space.kscience:controls-serial:0.3.0") } ``` diff --git a/controls-server/README.md b/controls-server/README.md index 83408e8..86a5bf5 100644 --- a/controls-server/README.md +++ b/controls-server/README.md @@ -6,18 +6,16 @@ A combined Magix event loop server with web server for visualization. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-server:0.2.0`. +The Maven coordinates of this project are `space.kscience:controls-server:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:controls-server:0.2.0") + implementation("space.kscience:controls-server:0.3.0") } ``` diff --git a/controls-storage/README.md b/controls-storage/README.md index 51cdbcb..3242de2 100644 --- a/controls-storage/README.md +++ b/controls-storage/README.md @@ -6,18 +6,16 @@ An API for stand-alone Controls-kt device or a hub. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-storage:0.2.0`. +The Maven coordinates of this project are `space.kscience:controls-storage:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:controls-storage:0.2.0") + implementation("space.kscience:controls-storage:0.3.0") } ``` diff --git a/controls-storage/controls-xodus/README.md b/controls-storage/controls-xodus/README.md index 790b356..2a6c247 100644 --- a/controls-storage/controls-xodus/README.md +++ b/controls-storage/controls-xodus/README.md @@ -6,18 +6,16 @@ An implementation of controls-storage on top of JetBrains Xodus. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-xodus:0.2.0`. +The Maven coordinates of this project are `space.kscience:controls-xodus:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:controls-xodus:0.2.0") + implementation("space.kscience:controls-xodus:0.3.0") } ``` diff --git a/demo/all-things/api/all-things.api b/demo/all-things/api/all-things.api index 4d283e5..1a1b140 100644 --- a/demo/all-things/api/all-things.api +++ b/demo/all-things/api/all-things.api @@ -47,11 +47,12 @@ public final class space/kscience/controls/demo/DemoDevice$Companion : space/ksc public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/demo/DemoDevice; public final fun getCoordinates ()Lspace/kscience/controls/spec/DevicePropertySpec; public final fun getCos ()Lspace/kscience/controls/spec/DevicePropertySpec; - public final fun getCosScale ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; + public final fun getCosScale ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; public final fun getResetScale ()Lspace/kscience/controls/spec/DeviceActionSpec; + public final fun getSetSinScale ()Lspace/kscience/controls/spec/DeviceActionSpec; public final fun getSin ()Lspace/kscience/controls/spec/DevicePropertySpec; - public final fun getSinScale ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; - public final fun getTimeScale ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; + public final fun getSinScale ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; + public final fun getTimeScale ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; public synthetic fun onOpen (Lspace/kscience/controls/api/Device;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun onOpen (Lspace/kscience/controls/demo/IDemoDevice;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/demo/car/api/car.api b/demo/car/api/car.api index 5b1940f..f5bda95 100644 --- a/demo/car/api/car.api +++ b/demo/car/api/car.api @@ -9,7 +9,7 @@ public abstract interface class space/kscience/controls/demo/car/IVirtualCar : s } public final class space/kscience/controls/demo/car/IVirtualCar$Companion : space/kscience/controls/spec/DeviceSpec { - public final fun getAcceleration ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; + public final fun getAcceleration ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; public final fun getLocation ()Lspace/kscience/controls/spec/DevicePropertySpec; public final fun getSpeed ()Lspace/kscience/controls/spec/DevicePropertySpec; } @@ -17,7 +17,6 @@ public final class space/kscience/controls/demo/car/IVirtualCar$Companion : spac public final class space/kscience/controls/demo/car/MagixVirtualCar : space/kscience/controls/demo/car/VirtualCar { public static final field Companion Lspace/kscience/controls/demo/car/MagixVirtualCar$Companion; public fun (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)V - public fun open (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class space/kscience/controls/demo/car/MagixVirtualCar$Companion : space/kscience/dataforge/context/Factory { @@ -45,11 +44,11 @@ public final class space/kscience/controls/demo/car/Vector2D : space/kscience/da public fun toString ()Ljava/lang/String; } -public final class space/kscience/controls/demo/car/Vector2D$CoordinatesMetaConverter : space/kscience/dataforge/meta/transformations/MetaConverter { - public synthetic fun metaToObject (Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object; - public fun metaToObject (Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/demo/car/Vector2D; - public synthetic fun objectToMeta (Ljava/lang/Object;)Lspace/kscience/dataforge/meta/Meta; - public fun objectToMeta (Lspace/kscience/controls/demo/car/Vector2D;)Lspace/kscience/dataforge/meta/Meta; +public final class space/kscience/controls/demo/car/Vector2D$CoordinatesMetaConverter : space/kscience/dataforge/meta/MetaConverter { + public synthetic fun convert (Ljava/lang/Object;)Lspace/kscience/dataforge/meta/Meta; + public fun convert (Lspace/kscience/controls/demo/car/Vector2D;)Lspace/kscience/dataforge/meta/Meta; + public synthetic fun readOrNull (Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object; + public fun readOrNull (Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/demo/car/Vector2D; } public class space/kscience/controls/demo/car/VirtualCar : space/kscience/controls/spec/DeviceBySpec, space/kscience/controls/demo/car/IVirtualCar { @@ -59,7 +58,7 @@ public class space/kscience/controls/demo/car/VirtualCar : space/kscience/contro public fun getAccelerationState ()Lspace/kscience/controls/demo/car/Vector2D; public fun getLocationState ()Lspace/kscience/controls/demo/car/Vector2D; public fun getSpeedState ()Lspace/kscience/controls/demo/car/Vector2D; - public fun open (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected fun onStart (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun setAccelerationState (Lspace/kscience/controls/demo/car/Vector2D;)V public fun setLocationState (Lspace/kscience/controls/demo/car/Vector2D;)V public fun setSpeedState (Lspace/kscience/controls/demo/car/Vector2D;)V diff --git a/demo/constructor/api/constructor.api b/demo/constructor/api/constructor.api new file mode 100644 index 0000000..a6e5b7e --- /dev/null +++ b/demo/constructor/api/constructor.api @@ -0,0 +1,27 @@ +public final class space/kscience/controls/demo/constructor/ComposableSingletons$MainKt { + public static final field INSTANCE Lspace/kscience/controls/demo/constructor/ComposableSingletons$MainKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$constructor ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$constructor ()Lkotlin/jvm/functions/Function3; +} + +public final class space/kscience/controls/demo/constructor/LinearDrive : space/kscience/controls/constructor/DeviceConstructor { + public static final field $stable I + public fun (Lspace/kscience/dataforge/context/Context;Lspace/kscience/controls/constructor/DoubleRangeState;DLspace/kscience/controls/constructor/PidParameters;Lspace/kscience/dataforge/meta/Meta;)V + public synthetic fun (Lspace/kscience/dataforge/context/Context;Lspace/kscience/controls/constructor/DoubleRangeState;DLspace/kscience/controls/constructor/PidParameters;Lspace/kscience/dataforge/meta/Meta;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getDrive ()Lspace/kscience/controls/constructor/Drive; + public final fun getEnd ()Lspace/kscience/controls/constructor/LimitSwitch; + public final fun getPid ()Lspace/kscience/controls/constructor/PidRegulator; + public final fun getPosition ()D + public final fun getStart ()Lspace/kscience/controls/constructor/LimitSwitch; + public final fun getTarget ()D + public final fun setTarget (D)V +} + +public final class space/kscience/controls/demo/constructor/MainKt { + public static final fun main ()V + public static synthetic fun main ([Ljava/lang/String;)V +} + diff --git a/demo/mks-pdr900/api/mks-pdr900.api b/demo/mks-pdr900/api/mks-pdr900.api index a9c9ecb..35e5f3c 100644 --- a/demo/mks-pdr900/api/mks-pdr900.api +++ b/demo/mks-pdr900/api/mks-pdr900.api @@ -10,19 +10,11 @@ public final class center/sciprog/devices/mks/MksPdr900Device : space/kscience/c public final class center/sciprog/devices/mks/MksPdr900Device$Companion : space/kscience/controls/spec/DeviceSpec, space/kscience/dataforge/context/Factory { public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lcenter/sciprog/devices/mks/MksPdr900Device; public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object; - public final fun getChannel ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; - public final fun getError ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; - public final fun getPowerOn ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; + public final fun getChannel ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; + public final fun getError ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; + public final fun getPowerOn ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; public final fun getValue ()Lspace/kscience/controls/spec/DevicePropertySpec; public fun onClose (Lcenter/sciprog/devices/mks/MksPdr900Device;)V public synthetic fun onClose (Lspace/kscience/controls/api/Device;)V } -public final class center/sciprog/devices/mks/NullableStringMetaConverter : space/kscience/dataforge/meta/transformations/MetaConverter { - public static final field INSTANCE Lcenter/sciprog/devices/mks/NullableStringMetaConverter; - public synthetic fun metaToObject (Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object; - public fun metaToObject (Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/String; - public synthetic fun objectToMeta (Ljava/lang/Object;)Lspace/kscience/dataforge/meta/Meta; - public fun objectToMeta (Ljava/lang/String;)Lspace/kscience/dataforge/meta/Meta; -} - diff --git a/demo/motors/api/motors.api b/demo/motors/api/motors.api index 506d7ee..05f430a 100644 --- a/demo/motors/api/motors.api +++ b/demo/motors/api/motors.api @@ -1,6 +1,6 @@ public final class ru/mipt/npm/devices/pimotionmaster/FxDevicePropertiesKt { public static final fun fxProperty (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/DevicePropertySpec;)Ljavafx/beans/property/ReadOnlyProperty; - public static final fun fxProperty (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/WritableDevicePropertySpec;)Ljavafx/beans/property/Property; + public static final fun fxProperty (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/MutableDevicePropertySpec;)Ljavafx/beans/property/Property; } public final class ru/mipt/npm/devices/pimotionmaster/PiDebugServerKt { @@ -50,19 +50,19 @@ public final class ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice$Axis } public final class ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice$Axis$Companion : space/kscience/controls/spec/DeviceSpec { - public final fun getClosedLoop ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; - public final fun getEnabled ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; + public final fun getClosedLoop ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; + public final fun getEnabled ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; public final fun getHalt ()Lspace/kscience/controls/spec/DeviceActionSpec; public final fun getMaxPosition ()Lspace/kscience/controls/spec/DevicePropertySpec; public final fun getMinPosition ()Lspace/kscience/controls/spec/DevicePropertySpec; public final fun getMove ()Lspace/kscience/controls/spec/DeviceActionSpec; public final fun getMoveToReference ()Lspace/kscience/controls/spec/DeviceActionSpec; public final fun getOnTarget ()Lspace/kscience/controls/spec/DevicePropertySpec; - public final fun getOpenLoopTarget ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; + public final fun getOpenLoopTarget ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; public final fun getPosition ()Lspace/kscience/controls/spec/DevicePropertySpec; public final fun getReference ()Lspace/kscience/controls/spec/DevicePropertySpec; - public final fun getTargetPosition ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; - public final fun getVelocity ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; + public final fun getTargetPosition ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; + public final fun getVelocity ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; } public final class ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice$Companion : space/kscience/controls/spec/DeviceSpec, space/kscience/dataforge/context/Factory { @@ -75,7 +75,7 @@ public final class ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice$Compa public final fun getIdentity ()Lspace/kscience/controls/spec/DevicePropertySpec; public final fun getInitialize ()Lspace/kscience/controls/spec/DeviceActionSpec; public final fun getStop ()Lspace/kscience/controls/spec/DeviceActionSpec; - public final fun getTimeout ()Lspace/kscience/controls/spec/WritableDevicePropertySpec; + public final fun getTimeout ()Lspace/kscience/controls/spec/MutableDevicePropertySpec; } public final class ru/mipt/npm/devices/pimotionmaster/PiMotionMasterView : tornadofx/View { diff --git a/magix/magix-api/README.md b/magix/magix-api/README.md index ee00c53..d624d84 100644 --- a/magix/magix-api/README.md +++ b/magix/magix-api/README.md @@ -6,18 +6,16 @@ A kotlin API for magix standard and some zero-dependency magix services ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-api:0.2.0`. +The Maven coordinates of this project are `space.kscience:magix-api:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:magix-api:0.2.0") + implementation("space.kscience:magix-api:0.3.0") } ``` diff --git a/magix/magix-java-endpoint/README.md b/magix/magix-java-endpoint/README.md index abcaa6f..40f4c8e 100644 --- a/magix/magix-java-endpoint/README.md +++ b/magix/magix-java-endpoint/README.md @@ -6,18 +6,16 @@ Java API to work with magix endpoints without Kotlin ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.2.0`. +The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:magix-java-endpoint:0.2.0") + implementation("space.kscience:magix-java-endpoint:0.3.0") } ``` diff --git a/magix/magix-mqtt/README.md b/magix/magix-mqtt/README.md index 6c34fdc..35165a0 100644 --- a/magix/magix-mqtt/README.md +++ b/magix/magix-mqtt/README.md @@ -6,18 +6,16 @@ MQTT client magix endpoint ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-mqtt:0.2.0`. +The Maven coordinates of this project are `space.kscience:magix-mqtt:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:magix-mqtt:0.2.0") + implementation("space.kscience:magix-mqtt:0.3.0") } ``` diff --git a/magix/magix-rabbit/README.md b/magix/magix-rabbit/README.md index 7fc42ad..609303d 100644 --- a/magix/magix-rabbit/README.md +++ b/magix/magix-rabbit/README.md @@ -6,18 +6,16 @@ RabbitMQ client magix endpoint ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-rabbit:0.2.0`. +The Maven coordinates of this project are `space.kscience:magix-rabbit:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:magix-rabbit:0.2.0") + implementation("space.kscience:magix-rabbit:0.3.0") } ``` diff --git a/magix/magix-rsocket/README.md b/magix/magix-rsocket/README.md index 799717d..0a1b34b 100644 --- a/magix/magix-rsocket/README.md +++ b/magix/magix-rsocket/README.md @@ -6,18 +6,16 @@ Magix endpoint (client) based on RSocket ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-rsocket:0.2.0`. +The Maven coordinates of this project are `space.kscience:magix-rsocket:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:magix-rsocket:0.2.0") + implementation("space.kscience:magix-rsocket:0.3.0") } ``` diff --git a/magix/magix-server/README.md b/magix/magix-server/README.md index 27d97e0..4175dcd 100644 --- a/magix/magix-server/README.md +++ b/magix/magix-server/README.md @@ -6,18 +6,16 @@ A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket route ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-server:0.2.0`. +The Maven coordinates of this project are `space.kscience:magix-server:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:magix-server:0.2.0") + implementation("space.kscience:magix-server:0.3.0") } ``` diff --git a/magix/magix-storage/README.md b/magix/magix-storage/README.md index fa367b4..db1d340 100644 --- a/magix/magix-storage/README.md +++ b/magix/magix-storage/README.md @@ -6,18 +6,16 @@ Magix history database API ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-storage:0.2.0`. +The Maven coordinates of this project are `space.kscience:magix-storage:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:magix-storage:0.2.0") + implementation("space.kscience:magix-storage:0.3.0") } ``` diff --git a/magix/magix-storage/magix-storage-xodus/README.md b/magix/magix-storage/magix-storage-xodus/README.md index b3d871b..2d9e202 100644 --- a/magix/magix-storage/magix-storage-xodus/README.md +++ b/magix/magix-storage/magix-storage-xodus/README.md @@ -6,18 +6,16 @@ ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.2.0`. +The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:magix-storage-xodus:0.2.0") + implementation("space.kscience:magix-storage-xodus:0.3.0") } ``` diff --git a/magix/magix-zmq/README.md b/magix/magix-zmq/README.md index 1338cce..b016e6b 100644 --- a/magix/magix-zmq/README.md +++ b/magix/magix-zmq/README.md @@ -6,18 +6,16 @@ ZMQ client endpoint for Magix ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-zmq:0.2.0`. +The Maven coordinates of this project are `space.kscience:magix-zmq:0.3.0`. **Gradle Kotlin DSL:** ```kotlin repositories { maven("https://repo.kotlin.link") - //uncomment to access development builds - //maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") mavenCentral() } dependencies { - implementation("space.kscience:magix-zmq:0.2.0") + implementation("space.kscience:magix-zmq:0.3.0") } ``` From 2946f23a4bffd2bfd6dda6df1f618df46c4fc3d6 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 4 Mar 2024 16:02:50 +0300 Subject: [PATCH 60/71] Update readme and API --- controls-constructor/README.md | 21 +++++++++++++++++++++ controls-jupyter/README.md | 21 +++++++++++++++++++++ controls-vision/README.md | 21 +++++++++++++++++++++ demo/constructor/README.md | 4 ++++ 4 files changed, 67 insertions(+) create mode 100644 controls-constructor/README.md create mode 100644 controls-jupyter/README.md create mode 100644 controls-vision/README.md create mode 100644 demo/constructor/README.md diff --git a/controls-constructor/README.md b/controls-constructor/README.md new file mode 100644 index 0000000..9a8b27c --- /dev/null +++ b/controls-constructor/README.md @@ -0,0 +1,21 @@ +# Module controls-constructor + +A low-code constructor for composite devices simulation + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:controls-constructor:0.3.0`. + +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:controls-constructor:0.3.0") +} +``` diff --git a/controls-jupyter/README.md b/controls-jupyter/README.md new file mode 100644 index 0000000..15d8e2d --- /dev/null +++ b/controls-jupyter/README.md @@ -0,0 +1,21 @@ +# Module controls-jupyter + + + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:controls-jupyter:0.3.0`. + +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:controls-jupyter:0.3.0") +} +``` diff --git a/controls-vision/README.md b/controls-vision/README.md new file mode 100644 index 0000000..41483ce --- /dev/null +++ b/controls-vision/README.md @@ -0,0 +1,21 @@ +# Module controls-vision + +Dashboard and visualization extensions for devices + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:controls-vision:0.3.0`. + +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:controls-vision:0.3.0") +} +``` diff --git a/demo/constructor/README.md b/demo/constructor/README.md new file mode 100644 index 0000000..3bd1e74 --- /dev/null +++ b/demo/constructor/README.md @@ -0,0 +1,4 @@ +# Module constructor + + + From 4835376c0de47caa95cd6d0f362c140f1f6af7e8 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 6 Mar 2024 18:55:11 +0300 Subject: [PATCH 61/71] Add proper deviceName to in-memory property history --- .../space/kscience/controls/misc/PropertyHistory.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt index 96bd8d5..092d9bb 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/PropertyHistory.kt @@ -10,6 +10,7 @@ import space.kscience.controls.api.PropertyChangedMessage import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.name import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.names.Name /** * An interface for device property history. @@ -34,6 +35,7 @@ public interface PropertyHistory { public class CollectedPropertyHistory( public val scope: CoroutineScope, eventFlow: Flow, + public val deviceName: Name, public val propertyName: String, public val converter: MetaConverter, maxSize: Int = 1000, @@ -41,7 +43,7 @@ public class CollectedPropertyHistory( private val store: SharedFlow> = eventFlow .filterIsInstance() - .filter { it.property == propertyName } + .filter { it.sourceDevice == deviceName && it.property == propertyName } .map { ValueWithTime(converter.read(it.value), it.time) } .shareIn(scope, started = SharingStarted.Eagerly, replay = maxSize) @@ -54,13 +56,15 @@ public class CollectedPropertyHistory( */ public fun Device.collectPropertyHistory( scope: CoroutineScope = this, + deviceName: Name, propertyName: String, converter: MetaConverter, maxSize: Int = 1000, -): PropertyHistory = CollectedPropertyHistory(scope, messageFlow, propertyName, converter, maxSize) +): PropertyHistory = CollectedPropertyHistory(scope, messageFlow, deviceName, propertyName, converter, maxSize) public fun D.collectPropertyHistory( scope: CoroutineScope = this, + deviceName: Name, spec: DevicePropertySpec, maxSize: Int = 1000, -): PropertyHistory = collectPropertyHistory(scope, spec.name, spec.converter, maxSize) \ No newline at end of file +): PropertyHistory = collectPropertyHistory(scope, deviceName, spec.name, spec.converter, maxSize) \ No newline at end of file From 29af4dfb2c68c90000c282f3c346b1c6e1f2597e Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 12 Mar 2024 22:26:43 +0300 Subject: [PATCH 62/71] Add heartbeat and watchdog --- magix/magix-utils/build.gradle.kts | 26 ++++++ .../kscience/magix/services/MagixRegistry.kt | 6 +- .../magix/services/WatcherEndpointWrapper.kt | 82 +++++++++++++++++++ .../kscience/magix/services/converters.kt | 0 .../kscience/magix/services/magixPortal.kt | 0 settings.gradle.kts | 2 + 6 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 magix/magix-utils/build.gradle.kts rename magix/{magix-api => magix-utils}/src/commonMain/kotlin/space/kscience/magix/services/MagixRegistry.kt (97%) create mode 100644 magix/magix-utils/src/commonMain/kotlin/space/kscience/magix/services/WatcherEndpointWrapper.kt rename magix/{magix-api => magix-utils}/src/commonMain/kotlin/space/kscience/magix/services/converters.kt (100%) rename magix/{magix-api => magix-utils}/src/commonMain/kotlin/space/kscience/magix/services/magixPortal.kt (100%) diff --git a/magix/magix-utils/build.gradle.kts b/magix/magix-utils/build.gradle.kts new file mode 100644 index 0000000..1f0ca20 --- /dev/null +++ b/magix/magix-utils/build.gradle.kts @@ -0,0 +1,26 @@ +import space.kscience.gradle.Maturity + +plugins { + id("space.kscience.gradle.mpp") + `maven-publish` +} + +description = """ + Common utilities and services for Magix endpoints. +""".trimIndent() + +val dataforgeVersion: String by rootProject.extra + +kscience { + jvm() + js() + native() + commonMain { + api(projects.magix.magixApi) + api("space.kscience:dataforge-meta:$dataforgeVersion") + } +} + +readme { + maturity = Maturity.EXPERIMENTAL +} \ No newline at end of file diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/services/MagixRegistry.kt b/magix/magix-utils/src/commonMain/kotlin/space/kscience/magix/services/MagixRegistry.kt similarity index 97% rename from magix/magix-api/src/commonMain/kotlin/space/kscience/magix/services/MagixRegistry.kt rename to magix/magix-utils/src/commonMain/kotlin/space/kscience/magix/services/MagixRegistry.kt index 3272131..c1a9466 100644 --- a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/services/MagixRegistry.kt +++ b/magix/magix-utils/src/commonMain/kotlin/space/kscience/magix/services/MagixRegistry.kt @@ -127,11 +127,11 @@ public fun CoroutineScope.launchMagixRegistry( * * If [registryEndpoint] field is provided, send request only to given endpoint. * - * @param endpointName the name of endpoint requesting a property + * @param sourceEndpoint the name of endpoint requesting a property */ public suspend fun MagixEndpoint.getProperty( propertyName: String, - endpointName: String, + sourceEndpoint: String, user: JsonElement? = null, registryEndpoint: String? = null, ): Flow> = subscribe( @@ -146,7 +146,7 @@ public suspend fun MagixEndpoint.getProperty( send( MagixRegistryMessage.format, MagixRegistryRequestMessage(propertyName), - source = endpointName, + source = sourceEndpoint, target = registryEndpoint, user = user ) diff --git a/magix/magix-utils/src/commonMain/kotlin/space/kscience/magix/services/WatcherEndpointWrapper.kt b/magix/magix-utils/src/commonMain/kotlin/space/kscience/magix/services/WatcherEndpointWrapper.kt new file mode 100644 index 0000000..8560c79 --- /dev/null +++ b/magix/magix-utils/src/commonMain/kotlin/space/kscience/magix/services/WatcherEndpointWrapper.kt @@ -0,0 +1,82 @@ +package space.kscience.magix.services + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.onEach +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.string +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import space.kscience.magix.api.send +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +public class WatcherEndpointWrapper( + private val scope: CoroutineScope, + private val endpointName: String, + private val endpoint: MagixEndpoint, + private val meta: Meta, +) : MagixEndpoint { + + private val watchDogJob: Job = scope.launch { + val filter = MagixMessageFilter( + format = listOf(MAGIX_WATCHDOG_FORMAT), + target = listOf(null, endpointName) + ) + endpoint.subscribe(filter).filter { + it.payload.jsonPrimitive.content == MAGIX_PING + }.onEach { request -> + endpoint.send( + MagixMessage( + MAGIX_WATCHDOG_FORMAT, + JsonPrimitive(MAGIX_PONG), + sourceEndpoint = endpointName, + targetEndpoint = request.sourceEndpoint, + parentId = request.id + ) + ) + }.collect() + } + + private val heartBeatDelay: Duration = meta["heartbeat.period"].string?.let { Duration.parse(it) } ?: 10.seconds + //TODO add update from registry + + private val heartBeatJob = scope.launch { + while (isActive){ + delay(heartBeatDelay) + endpoint.send( + MagixMessage( + MAGIX_HEARTBEAT_FORMAT, + JsonNull, //TODO consider adding timestamp + endpointName + ) + ) + } + } + + override fun subscribe(filter: MagixMessageFilter): Flow = endpoint.subscribe(filter) + + override suspend fun broadcast(message: MagixMessage) { + endpoint.broadcast(message) + } + + override fun close() { + endpoint.close() + watchDogJob.cancel() + heartBeatJob.cancel() + } + + public companion object { + public const val MAGIX_WATCHDOG_FORMAT: String = "magix.watchdog" + public const val MAGIX_PING: String = "ping" + public const val MAGIX_PONG: String = "pong" + public const val MAGIX_HEARTBEAT_FORMAT: String = "magix.heartbeat" + } +} \ No newline at end of file diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/services/converters.kt b/magix/magix-utils/src/commonMain/kotlin/space/kscience/magix/services/converters.kt similarity index 100% rename from magix/magix-api/src/commonMain/kotlin/space/kscience/magix/services/converters.kt rename to magix/magix-utils/src/commonMain/kotlin/space/kscience/magix/services/converters.kt diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/services/magixPortal.kt b/magix/magix-utils/src/commonMain/kotlin/space/kscience/magix/services/magixPortal.kt similarity index 100% rename from magix/magix-api/src/commonMain/kotlin/space/kscience/magix/services/magixPortal.kt rename to magix/magix-utils/src/commonMain/kotlin/space/kscience/magix/services/magixPortal.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 7de4241..01bb379 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -47,6 +47,7 @@ include( ":controls-server", ":controls-opcua", ":controls-modbus", + ":controls-plc4x", // ":controls-mongo", ":controls-storage", ":controls-storage:controls-xodus", @@ -55,6 +56,7 @@ include( ":controls-jupyter", ":magix", ":magix:magix-api", + ":magix:magix-utils", ":magix:magix-server", ":magix:magix-rsocket", ":magix:magix-java-endpoint", From f28e9dc226ea1856cae1575e1b7ae636777ad3a8 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 18 Mar 2024 09:30:41 +0300 Subject: [PATCH 63/71] Update constructor api --- build.gradle.kts | 4 +- .../controls/constructor/DeviceConstructor.kt | 44 +++--------- .../controls/constructor/DeviceState.kt | 16 +++++ .../controls/opcua/server/DeviceNameSpace.kt | 2 +- .../src/commonMain/kotlin/plotExtensions.kt | 69 ++++++++++++++++--- .../jsMain/kotlin/ControlsVisionPlugin.js.kt | 14 +--- demo/constructor/api/constructor.api | 2 +- demo/constructor/build.gradle.kts | 2 +- demo/constructor/src/jvmMain/kotlin/main.kt | 5 +- gradle.properties | 2 +- magix/magix-utils/build.gradle.kts | 1 + 11 files changed, 98 insertions(+), 63 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 99946f1..eeb4bfe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,14 +6,14 @@ plugins { } val dataforgeVersion: String by extra("0.8.0") -val visionforgeVersion by extra("0.4.0") +val visionforgeVersion by extra("0.4.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") allprojects { group = "space.kscience" - version = "0.3.0" + version = "0.3.1-dev-1" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt index 5c11f27..820f6ef 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt @@ -10,7 +10,6 @@ import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty -import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty import kotlin.time.Duration @@ -55,17 +54,17 @@ public abstract class DeviceConstructor( /** * Register a property and provide a direct reader for it */ - public fun property( - state: DeviceState, + public fun > property( + state: S, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, - ): PropertyDelegateProvider> = + ): PropertyDelegateProvider> = PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) registerProperty(descriptor, state) ReadOnlyProperty { _: DeviceConstructor, _ -> - state.value + state } } @@ -79,37 +78,14 @@ public abstract class DeviceConstructor( initialState: T, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, - ): PropertyDelegateProvider> = property( + ): PropertyDelegateProvider>> = property( DeviceState.external(this, metaConverter, readInterval, initialState, reader), descriptorBuilder, nameOverride, ) - /** - * Register a mutable property and provide a direct reader for it - */ - public fun mutableProperty( - state: MutableDeviceState, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - nameOverride: String? = null, - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _: DeviceConstructor, property -> - val name = nameOverride ?: property.name - val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) - registerProperty(descriptor, state) - object : ReadWriteProperty { - override fun getValue(thisRef: DeviceConstructor, property: KProperty<*>): T = state.value - - override fun setValue(thisRef: DeviceConstructor, property: KProperty<*>, value: T) { - state.value = value - } - - } - } - - /** - * Register external state as a property + * Register a mutable external state as a property */ public fun mutableProperty( metaConverter: MetaConverter, @@ -119,22 +95,22 @@ public abstract class DeviceConstructor( initialState: T, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, - ): PropertyDelegateProvider> = mutableProperty( + ): PropertyDelegateProvider>> = property( DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), descriptorBuilder, nameOverride, ) /** - * Create and register a virtual property with optional [callback] + * Create and register a virtual mutable property with optional [callback] */ - public fun state( + public fun virtualProperty( metaConverter: MetaConverter, initialState: T, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, callback: (T) -> Unit = {}, - ): PropertyDelegateProvider> = mutableProperty( + ): PropertyDelegateProvider>> = property( DeviceState.virtual(metaConverter, initialState, callback), descriptorBuilder, nameOverride, diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index 1828edd..1219210 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -1,6 +1,7 @@ package space.kscience.controls.constructor import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -11,6 +12,7 @@ import space.kscience.controls.spec.MutableDevicePropertySpec import space.kscience.controls.spec.name import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaConverter +import kotlin.reflect.KProperty import kotlin.time.Duration /** @@ -29,6 +31,13 @@ public val DeviceState.metaFlow: Flow get() = valueFlow.map(convert public val DeviceState.valueAsMeta: Meta get() = converter.convert(value) +public operator fun DeviceState.getValue(thisRef: Any?, property: KProperty<*>): T = value + +/** + * Collect values in a given [scope] + */ +public fun DeviceState.collectValuesIn(scope: CoroutineScope, block: suspend (T)->Unit): Job = + valueFlow.onEach(block).launchIn(scope) /** * A mutable state of a device @@ -37,6 +46,10 @@ public interface MutableDeviceState : DeviceState { override var value: T } +public operator fun MutableDeviceState.setValue(thisRef: Any?, property: KProperty<*>, value: T) { + this.value = value +} + public var MutableDeviceState.valueAsMeta: Meta get() = converter.convert(value) set(arg) { @@ -216,6 +229,9 @@ private class MutableExternalState( } } +/** + * Create a [DeviceState] that regularly reads and caches an external value + */ public fun DeviceState.Companion.external( scope: CoroutineScope, converter: MetaConverter, diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt index e108b21..aa73890 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt @@ -75,7 +75,7 @@ public class DeviceNameSpace( browseName = newQualifiedName(propertyName) displayName = LocalizedText.english(propertyName) - dataType = if (descriptor.metaDescriptor.children.isNotEmpty()) { + dataType = if (descriptor.metaDescriptor.nodes.isNotEmpty()) { Identifiers.String } else when (descriptor.metaDescriptor.valueTypes?.first()) { null, ValueType.STRING, ValueType.NULL -> Identifiers.String diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index 6e92165..4cfa49b 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -2,13 +2,8 @@ package space.kscience.controls.vision -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.sample -import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Clock @@ -22,6 +17,7 @@ import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.name import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.* +import space.kscience.dataforge.misc.DFExperimental import space.kscience.plotly.Plot import space.kscience.plotly.bar import space.kscience.plotly.models.Bar @@ -183,4 +179,61 @@ public fun Plot.plotBooleanState( configuration: Bar.() -> Unit = {}, ): Job = bar(configuration).run { updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, sampling) -} \ No newline at end of file +} + +private fun Flow.chunkedByPeriod(duration: Duration): Flow> { + val collector: ArrayDeque = ArrayDeque() + return channelFlow { + coroutineScope { + launch { + while (isActive) { + delay(duration) + send(ArrayList(collector)) + collector.clear() + } + } + this@chunkedByPeriod.collect { + collector.add(it) + } + } + } +} + +private fun List.averageTime(): Instant { + val min = min() + val max = max() + val duration = max - min + return min + duration / 2 +} + +/** + * Average property value by [averagingInterval]. Return [missingValue] on each sample interval if no events arrived. + */ +@DFExperimental +public fun Plot.plotAveragedDeviceProperty( + device: Device, + propertyName: String, + missingValue: Double = 0.0, + extractValue: Meta.() -> Double = { value?.double ?: missingValue }, + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + averagingInterval: Duration = defaultSampling, + coroutineScope: CoroutineScope = device.context, + configuration: Scatter.() -> Unit = {}, +): Job = scatter(configuration).run { + val data = TimeData() + device.propertyMessageFlow(propertyName).chunkedByPeriod(averagingInterval).transform { eventList -> + if(eventList.isEmpty()){ + data.append(Clock.System.now(), missingValue.asValue()) + } else { + val time = eventList.map { it.time }.averageTime() + val value = eventList.map { extractValue(it.value) }.average() + data.append(time, value.asValue()) + } + data.trim(maxAge, maxPoints, minPoints) + emit(data) + }.onEach { + it.fillPlot(x, y) + }.launchIn(coroutineScope) +} diff --git a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt index 017989b..55074ae 100644 --- a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt +++ b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt @@ -1,29 +1,17 @@ package space.kscience.controls.vision import kotlinx.serialization.modules.SerializersModule -import org.w3c.dom.Element import space.kscience.dataforge.context.Context 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 space.kscience.visionforge.Vision import space.kscience.visionforge.VisionPlugin -import space.kscience.visionforge.html.ElementVisionRenderer -public actual class ControlVisionPlugin : VisionPlugin(), ElementVisionRenderer { +public actual class ControlVisionPlugin : VisionPlugin() { override val tag: PluginTag get() = Companion.tag override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule - override fun rateVision(vision: Vision): Int { - TODO("Not yet implemented") - } - - override fun render(element: Element, name: Name, vision: Vision, meta: Meta) { - TODO("Not yet implemented") - } - public actual companion object : PluginFactory { override val tag: PluginTag = PluginTag("controls.vision") diff --git a/demo/constructor/api/constructor.api b/demo/constructor/api/constructor.api index a6e5b7e..7206553 100644 --- a/demo/constructor/api/constructor.api +++ b/demo/constructor/api/constructor.api @@ -14,7 +14,7 @@ public final class space/kscience/controls/demo/constructor/LinearDrive : space/ public final fun getDrive ()Lspace/kscience/controls/constructor/Drive; public final fun getEnd ()Lspace/kscience/controls/constructor/LimitSwitch; public final fun getPid ()Lspace/kscience/controls/constructor/PidRegulator; - public final fun getPosition ()D + public final fun getPositionState ()Lspace/kscience/controls/constructor/DoubleRangeState; public final fun getStart ()Lspace/kscience/controls/constructor/LimitSwitch; public final fun getTarget ()D public final fun setTarget (D)V diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index 470533f..6909639 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode plugins { id("space.kscience.gradle.mpp") - id("org.jetbrains.compose") version "1.5.11" + alias(spclibs.plugins.compose) } kscience { diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 5cbef67..26ca10a 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -52,8 +52,9 @@ class LinearDrive( val end by device(LimitSwitch.factory(state.atEndState)) - val position by property(state) - var target by mutableProperty(pid.mutablePropertyAsState(Regulator.target, 0.0)) + val positionState: DoubleRangeState by property(state) + private val targetState: MutableDeviceState by property(pid.mutablePropertyAsState(Regulator.target, 0.0)) + var target by targetState } diff --git a/gradle.properties b/gradle.properties index 717cec4..4f937f0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,4 +7,4 @@ org.gradle.parallel=true org.gradle.configureondemand=true org.gradle.jvmargs=-Xmx4096m -toolsVersion=0.15.2-kotlin-1.9.21 \ No newline at end of file +toolsVersion=0.15.2-kotlin-1.9.22 \ No newline at end of file diff --git a/magix/magix-utils/build.gradle.kts b/magix/magix-utils/build.gradle.kts index 1f0ca20..b22b5f0 100644 --- a/magix/magix-utils/build.gradle.kts +++ b/magix/magix-utils/build.gradle.kts @@ -15,6 +15,7 @@ kscience { jvm() js() native() + useSerialization() commonMain { api(projects.magix.magixApi) api("space.kscience:dataforge-meta:$dataforgeVersion") From 53cc4dc0df86908d09091a17104310a5b25d209b Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 18 Mar 2024 09:34:14 +0300 Subject: [PATCH 64/71] PLC4x bindings --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a9fee..760e00a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,11 @@ ## Unreleased ### Added +- Value averaging plot extension +- PLC4X bindings ### Changed +- Constructor properties return `DeviceStat` in order to be able to subscribe to them ### Deprecated From 70ab60f98c353c5952c2a3e41e12e6052a8a8be3 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 18 Mar 2024 17:15:39 +0300 Subject: [PATCH 65/71] fix plot extensions --- .../controls/constructor/DeviceConstructor.kt | 4 ++-- .../controls/constructor/DeviceGroup.kt | 4 ++-- .../controls/constructor/DeviceState.kt | 2 +- .../src/commonMain/kotlin/plotExtensions.kt | 20 +++++++++---------- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt index 820f6ef..63b5303 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt @@ -54,7 +54,7 @@ public abstract class DeviceConstructor( /** * Register a property and provide a direct reader for it */ - public fun > property( + public fun > property( state: S, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, @@ -104,7 +104,7 @@ public abstract class DeviceConstructor( /** * Create and register a virtual mutable property with optional [callback] */ - public fun virtualProperty( + public fun virtualProperty( metaConverter: MetaConverter, initialState: T, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt index 6cdb784..ca05c27 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -28,7 +28,7 @@ public open class DeviceGroup( ) : DeviceHub, CachingDevice { internal class Property( - val state: DeviceState, + val state: DeviceState<*>, val descriptor: PropertyDescriptor, ) @@ -82,7 +82,7 @@ public open class DeviceGroup( /** * Register a new property based on [DeviceState]. Properties could be modified dynamically */ - public fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState) { + public fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { val name = descriptor.name.parseAsName() require(properties[name] == null) { "Can't add property with name $name. It already exists." } properties[name] = Property(state, descriptor) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index 1219210..faa7b2f 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -50,7 +50,7 @@ public operator fun MutableDeviceState.setValue(thisRef: Any?, property: this.value = value } -public var MutableDeviceState.valueAsMeta: Meta +public var MutableDeviceState.valueAsMeta: Meta get() = converter.convert(value) set(arg) { value = converter.read(arg) diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index 4cfa49b..804fe9d 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -184,18 +184,16 @@ public fun Plot.plotBooleanState( private fun Flow.chunkedByPeriod(duration: Duration): Flow> { val collector: ArrayDeque = ArrayDeque() return channelFlow { - coroutineScope { - launch { - while (isActive) { - delay(duration) - send(ArrayList(collector)) - collector.clear() - } - } - this@chunkedByPeriod.collect { - collector.add(it) + launch { + while (isActive) { + delay(duration) + send(ArrayList(collector)) + collector.clear() } } + this@chunkedByPeriod.collect { + collector.add(it) + } } } @@ -224,7 +222,7 @@ public fun Plot.plotAveragedDeviceProperty( ): Job = scatter(configuration).run { val data = TimeData() device.propertyMessageFlow(propertyName).chunkedByPeriod(averagingInterval).transform { eventList -> - if(eventList.isEmpty()){ + if (eventList.isEmpty()) { data.append(Clock.System.now(), missingValue.asValue()) } else { val time = eventList.map { it.time }.averageTime() From 78dade4b4969f64c4316e36fc02e84e9969614af Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 18 Mar 2024 17:18:31 +0300 Subject: [PATCH 66/71] PLC4x bindings --- controls-plc4x/build.gradle.kts | 24 ++++ .../kscience/controls/plc4x/Plc4XDevice.kt | 28 ++++ .../kscience/controls/plc4x/Plc4xProperty.kt | 31 +++++ .../kscience/controls/plc4x/plc4xConnector.kt | 123 ++++++++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 controls-plc4x/build.gradle.kts create mode 100644 controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt create mode 100644 controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt create mode 100644 controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/plc4xConnector.kt diff --git a/controls-plc4x/build.gradle.kts b/controls-plc4x/build.gradle.kts new file mode 100644 index 0000000..0836476 --- /dev/null +++ b/controls-plc4x/build.gradle.kts @@ -0,0 +1,24 @@ +import space.kscience.gradle.Maturity + +plugins { + id("space.kscience.gradle.mpp") + `maven-publish` +} + +val plc4xVersion = "0.12.0" + +description = """ + A plugin for Controls-kt device server on top of plc4x library +""".trimIndent() + +kscience{ + jvm() + jvmMain{ + api(projects.controlsCore) + api("org.apache.plc4x:plc4j-spi:$plc4xVersion") + } +} + +readme{ + maturity = Maturity.EXPERIMENTAL +} \ No newline at end of file diff --git a/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt new file mode 100644 index 0000000..996d194 --- /dev/null +++ b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt @@ -0,0 +1,28 @@ +package space.kscience.controls.plc4x + +import kotlinx.coroutines.future.await +import org.apache.plc4x.java.api.PlcConnection +import org.apache.plc4x.java.api.messages.PlcWriteRequest +import space.kscience.controls.api.Device +import space.kscience.dataforge.meta.Meta + +public interface Plc4XDevice: Device { + public val connection: PlcConnection + + public suspend fun read(plc4xProperty: Plc4xProperty): Meta = with(plc4xProperty){ + val request = connection.readRequestBuilder().request().build() + val response = request.execute().await() + response.readProperty() + } + + public suspend fun write(plc4xProperty: Plc4xProperty, value: Meta): Unit = with(plc4xProperty){ + val request: PlcWriteRequest = connection.writeRequestBuilder().writeProperty(value).build() + request.execute().await() + } + + public suspend fun subscribe(propertyName: String, plc4xProperty: Plc4xProperty): Unit = with(plc4xProperty){ + + } + +} + diff --git a/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt new file mode 100644 index 0000000..5b16dec --- /dev/null +++ b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt @@ -0,0 +1,31 @@ +package space.kscience.controls.plc4x + +import org.apache.plc4x.java.api.messages.PlcReadRequest +import org.apache.plc4x.java.api.messages.PlcReadResponse +import org.apache.plc4x.java.api.messages.PlcWriteRequest +import org.apache.plc4x.java.api.types.PlcValueType +import space.kscience.dataforge.meta.Meta + +public interface Plc4xProperty { + + public fun PlcReadRequest.Builder.request(): PlcReadRequest.Builder + + public fun PlcReadResponse.readProperty(): Meta + + public fun PlcWriteRequest.Builder.writeProperty(meta: Meta): PlcWriteRequest.Builder +} + +public class DefaultPlc4xProperty( + private val address: String, + private val plcValueType: PlcValueType, + private val name: String = "@default", +) : Plc4xProperty { + + override fun PlcReadRequest.Builder.request(): PlcReadRequest.Builder = + addTagAddress(name, address) + override fun PlcReadResponse.readProperty(): Meta = + asPlcValue.toMeta() + + override fun PlcWriteRequest.Builder.writeProperty(meta: Meta): PlcWriteRequest.Builder = + addTagAddress(name, address, meta.toPlcValue(plcValueType)) +} \ No newline at end of file diff --git a/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/plc4xConnector.kt b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/plc4xConnector.kt new file mode 100644 index 0000000..5d0b2de --- /dev/null +++ b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/plc4xConnector.kt @@ -0,0 +1,123 @@ +package space.kscience.controls.plc4x + +import org.apache.plc4x.java.api.types.PlcValueType +import org.apache.plc4x.java.api.value.PlcValue +import org.apache.plc4x.java.spi.values.* +import space.kscience.dataforge.meta.* +import space.kscience.dataforge.names.asName +import java.math.BigInteger + +internal fun PlcValue.toMeta(): Meta = Meta { + when (plcValueType) { + null, PlcValueType.NULL -> value = Null + PlcValueType.BOOL -> value = this@toMeta.boolean.asValue() + PlcValueType.BYTE -> this@toMeta.byte.asValue() + PlcValueType.WORD -> this@toMeta.short.asValue() + PlcValueType.DWORD -> this@toMeta.int.asValue() + PlcValueType.LWORD -> this@toMeta.long.asValue() + PlcValueType.USINT -> this@toMeta.short.asValue() + PlcValueType.UINT -> this@toMeta.int.asValue() + PlcValueType.UDINT -> this@toMeta.long.asValue() + PlcValueType.ULINT -> this@toMeta.bigInteger.asValue() + PlcValueType.SINT -> this@toMeta.byte.asValue() + PlcValueType.INT -> this@toMeta.short.asValue() + PlcValueType.DINT -> this@toMeta.int.asValue() + PlcValueType.LINT -> this@toMeta.long.asValue() + PlcValueType.REAL -> this@toMeta.float.asValue() + PlcValueType.LREAL -> this@toMeta.double.asValue() + PlcValueType.CHAR -> this@toMeta.int.asValue() + PlcValueType.WCHAR -> this@toMeta.short.asValue() + PlcValueType.STRING -> this@toMeta.string.asValue() + PlcValueType.WSTRING -> this@toMeta.string.asValue() + PlcValueType.TIME -> this@toMeta.duration.toString().asValue() + PlcValueType.LTIME -> this@toMeta.duration.toString().asValue() + PlcValueType.DATE -> this@toMeta.date.toString().asValue() + PlcValueType.LDATE -> this@toMeta.date.toString().asValue() + PlcValueType.TIME_OF_DAY -> this@toMeta.time.toString().asValue() + PlcValueType.LTIME_OF_DAY -> this@toMeta.time.toString().asValue() + PlcValueType.DATE_AND_TIME -> this@toMeta.dateTime.toString().asValue() + PlcValueType.DATE_AND_LTIME -> this@toMeta.dateTime.toString().asValue() + PlcValueType.LDATE_AND_TIME -> this@toMeta.dateTime.toString().asValue() + PlcValueType.Struct -> this@toMeta.struct.forEach { (name, item) -> + set(name, item.toMeta()) + } + + PlcValueType.List -> { + val listOfMeta = this@toMeta.list.map { it.toMeta() } + if (listOfMeta.all { it.items.isEmpty() }) { + value = listOfMeta.map { it.value ?: Null }.asValue() + } else { + setIndexed("@list".asName(), list.map { it.toMeta() }) + } + } + + PlcValueType.RAW_BYTE_ARRAY -> this@toMeta.raw.asValue() + } +} + +private fun Value.toPlcValue(): PlcValue = when (type) { + ValueType.NUMBER -> when (val number = number) { + is Short -> PlcINT(number.toShort()) + is Int -> PlcDINT(number.toInt()) + is Long -> PlcLINT(number.toLong()) + is Float -> PlcREAL(number.toFloat()) + else -> PlcLREAL(number.toDouble()) + } + + ValueType.STRING -> PlcSTRING(string) + ValueType.BOOLEAN -> PlcBOOL(boolean) + ValueType.NULL -> PlcNull() + ValueType.LIST -> TODO() +} + +internal fun Meta.toPlcValue(hint: PlcValueType): PlcValue = when (hint) { + PlcValueType.Struct -> PlcStruct( + items.entries.associate { (token, item) -> + token.toString() to item.toPlcValue(PlcValueType.Struct) + } + ) + + PlcValueType.NULL -> PlcNull() + PlcValueType.BOOL -> PlcBOOL(boolean) + PlcValueType.BYTE -> PlcBYTE(int) + PlcValueType.WORD -> PlcWORD(int) + PlcValueType.DWORD -> PlcDWORD(int) + PlcValueType.LWORD -> PlcLWORD(long) + PlcValueType.USINT -> PlcLWORD(short) + PlcValueType.UINT -> PlcUINT(int) + PlcValueType.UDINT -> PlcDINT(long) + PlcValueType.ULINT -> (number as? BigInteger)?.let { PlcULINT(it) } ?: PlcULINT(long) + PlcValueType.SINT -> PlcSINT(int) + PlcValueType.INT -> PlcINT(int) + PlcValueType.DINT -> PlcDINT(int) + PlcValueType.LINT -> PlcLINT(long) + PlcValueType.REAL -> PlcREAL(float) + PlcValueType.LREAL -> PlcLREAL(double) + PlcValueType.CHAR -> PlcCHAR(int) + PlcValueType.WCHAR -> PlcWCHAR(short) + PlcValueType.STRING -> PlcSTRING(string) + PlcValueType.WSTRING -> PlcWSTRING(string) + PlcValueType.TIME -> PlcTIME(string?.let { java.time.Duration.parse(it) }) + PlcValueType.LTIME -> PlcLTIME(string?.let { java.time.Duration.parse(it) }) + PlcValueType.DATE -> PlcDATE(string?.let { java.time.LocalDate.parse(it) }) + PlcValueType.LDATE -> PlcLDATE(string?.let { java.time.LocalDate.parse(it) }) + PlcValueType.TIME_OF_DAY -> PlcTIME_OF_DAY(string?.let { java.time.LocalTime.parse(it) }) + PlcValueType.LTIME_OF_DAY -> PlcLTIME_OF_DAY(string?.let { java.time.LocalTime.parse(it) }) + PlcValueType.DATE_AND_TIME -> PlcDATE_AND_TIME(string?.let { java.time.LocalDateTime.parse(it) }) + PlcValueType.DATE_AND_LTIME -> PlcDATE_AND_LTIME(string?.let { java.time.LocalDateTime.parse(it) }) + PlcValueType.LDATE_AND_TIME -> PlcLDATE_AND_TIME(string?.let { java.time.LocalDateTime.parse(it) }) + PlcValueType.List -> PlcList().apply { + value?.list?.forEach { add(it.toPlcValue()) } + getIndexed("@list").forEach { (_, meta) -> + if (meta.items.isEmpty()) { + meta.value?.let { add(it.toPlcValue()) } + } else { + add(meta.toPlcValue(PlcValueType.Struct)) + } + } + } + + PlcValueType.RAW_BYTE_ARRAY -> PlcRawByteArray( + value?.list?.map { it.number.toByte() }?.toByteArray() ?: error("The meta content is not byte array") + ) +} \ No newline at end of file From d91296c47d73358b3d5e0ebcb018240902168cf7 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 25 Mar 2024 15:48:23 +0300 Subject: [PATCH 67/71] Refactor load test --- .../space/kscience/controls/ports/Port.kt | 2 +- .../space/kscience/controls/ports/phrases.kt | 25 ++++ .../kscience/controls/client/controlsMagix.kt | 5 +- demo/many-devices/build.gradle.kts | 2 +- .../kscience/controls/demo/MassDevice.kt | 109 ++++++++++-------- gradle/wrapper/gradle-wrapper.properties | 2 +- 6 files changed, 91 insertions(+), 54 deletions(-) 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 b17de92..9366604 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 @@ -80,7 +80,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. + * To form phrases, some condition should be used on top of it. * For example [stringsDelimitedIncoming] generates phrases with fixed delimiter. */ override fun receiving(): Flow = incoming.receiveAsFlow() diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt index b86591f..03d21f5 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt @@ -42,6 +42,31 @@ public fun Flow.withDelimiter(delimiter: ByteArray): Flow } } +private fun Flow.withFixedMessageSize(messageSize: Int): Flow { + require(messageSize > 0) { "Message size should be positive" } + + val output = Buffer() + + onCompletion { + output.close() + } + + return transform { chunk -> + val remaining: Int = (messageSize - output.size).toInt() + if (chunk.size >= remaining) { + output.write(chunk, endIndex = remaining) + emit(output.readByteArray()) + output.clear() + //write the remaining chunk fragment + if(chunk.size> remaining) { + output.write(chunk, startIndex = remaining) + } + } else { + output.write(chunk) + } + } +} + /** * Transform byte fragments into utf-8 phrases using utf-8 delimiter */ diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt index 8aa4383..915d0a0 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt @@ -12,6 +12,8 @@ import space.kscience.controls.manager.respondHubMessage import space.kscience.dataforge.context.error import space.kscience.dataforge.context.logger import space.kscience.magix.api.* +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext internal val controlsMagixFormat: MagixFormat = MagixFormat( @@ -38,7 +40,8 @@ internal fun generateId(request: MagixMessage): String = if (request.id != null) public fun DeviceManager.launchMagixService( endpoint: MagixEndpoint, endpointID: String = controlsMagixFormat.defaultFormat, -): Job = context.launch { + coroutineContext: CoroutineContext = EmptyCoroutineContext, +): Job = context.launch(coroutineContext) { endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID, null)).onEach { (request, payload) -> val responsePayload = respondHubMessage(payload) responsePayload.forEach { diff --git a/demo/many-devices/build.gradle.kts b/demo/many-devices/build.gradle.kts index 7248e42..f46b71f 100644 --- a/demo/many-devices/build.gradle.kts +++ b/demo/many-devices/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { implementation(projects.magix.magixZmq) implementation("io.ktor:ktor-client-cio:$ktorVersion") - implementation("space.kscience:plotlykt-server:0.6.0") + implementation("space.kscience:plotlykt-server:0.7.1") implementation(spclibs.logback.classic) } 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 22d7e1c..5eafabf 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 @@ -1,8 +1,11 @@ package space.kscience.controls.demo -import kotlinx.coroutines.* +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.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Clock @@ -19,15 +22,19 @@ 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.rSocketStreamWithWebSockets import space.kscience.magix.server.RSocketMagixFlowPlugin import space.kscience.magix.server.startMagixServer -import space.kscience.plotly.* +import space.kscience.plotly.Plotly +import space.kscience.plotly.PlotlyConfig +import space.kscience.plotly.layout +import space.kscience.plotly.models.Bar +import space.kscience.plotly.plot import space.kscience.plotly.server.PlotlyUpdateMode import space.kscience.plotly.server.serve import space.kscience.plotly.server.show import space.kscince.magix.zmq.ZmqMagixFlowPlugin +import space.kscince.magix.zmq.zmq import kotlin.random.Random import kotlin.time.Duration import kotlin.time.Duration.Companion.ZERO @@ -46,14 +53,13 @@ class MassDevice(context: Context, meta: Meta) : DeviceBySpec(MassDe val value by doubleProperty { randomValue } override suspend fun MassDevice.onOpen() { - doRecurring((meta["delay"].int ?: 10).milliseconds) { + doRecurring((meta["delay"].int ?: 5).milliseconds) { read(value) } } } } -@OptIn(DelicateCoroutinesApi::class) suspend fun main() { val context = Context("Mass") @@ -62,28 +68,60 @@ suspend fun main() { ZmqMagixFlowPlugin() ) - val numDevices = 100 + val numDevices = 50 + repeat(numDevices) { - context.launch(newFixedThreadPoolContext(2, "Device${it}")) { - delay(1) - val deviceContext = Context("Device${it}") { - plugin(DeviceManager) + delay(1) + val deviceContext = Context("Device${it}") { + plugin(DeviceManager) + } + + val deviceManager = deviceContext.request(DeviceManager) + + deviceManager.install("device$it", MassDevice) + + val endpointId = "device$it" + val deviceEndpoint = MagixEndpoint.rSocketStreamWithWebSockets("localhost") + deviceManager.launchMagixService(deviceEndpoint, endpointId, Dispatchers.IO) + + } + + val trace = Bar { + context.launch(Dispatchers.IO) { + val monitorEndpoint = MagixEndpoint.zmq("localhost") + + val mutex = Mutex() + + val latest = HashMap() + val max = HashMap() + + monitorEndpoint.subscribe(DeviceManager.magixFormat).onEach { (magixMessage, payload) -> + mutex.withLock { + val delay = Clock.System.now() - payload.time + latest[magixMessage.sourceEndpoint] = Clock.System.now() - payload.time + max[magixMessage.sourceEndpoint] = + maxOf(delay, max[magixMessage.sourceEndpoint] ?: ZERO) + } + }.launchIn(this) + + while (isActive) { + delay(200) + mutex.withLock { + val sorted = max.mapKeys { it.key.substring(6).toInt() }.toSortedMap() + latest.clear() + max.clear() + x.numbers = sorted.keys + y.numbers = sorted.values.map { it.inWholeMicroseconds / 1000.0 + 0.0001 } + } } - - val deviceManager = deviceContext.request(DeviceManager) - - deviceManager.install("device$it", MassDevice) - - val endpointId = "device$it" - val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost") - deviceManager.launchMagixService(deviceEndpoint, endpointId) } } - val application = Plotly.serve(port = 9091, scope = context) { + val application = Plotly.serve(port = 9091) { updateMode = PlotlyUpdateMode.PUSH updateInterval = 1000 + page { container -> plot(renderer = container, config = PlotlyConfig { saveAsSvg() }) { layout { @@ -92,36 +130,7 @@ suspend fun main() { xaxis.title = "Device number" yaxis.title = "Maximum latency in ms" } - bar { - launch(Dispatchers.IO) { - val monitorEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") - - val mutex = Mutex() - - val latest = HashMap() - val max = HashMap() - - monitorEndpoint.subscribe(DeviceManager.magixFormat).onEach { (magixMessage, payload) -> - mutex.withLock { - val delay = Clock.System.now() - payload.time - latest[magixMessage.sourceEndpoint] = Clock.System.now() - payload.time - max[magixMessage.sourceEndpoint] = - maxOf(delay, max[magixMessage.sourceEndpoint] ?: ZERO) - } - }.launchIn(this) - - while (isActive) { - delay(200) - mutex.withLock { - val sorted = max.mapKeys { it.key.substring(6).toInt() }.toSortedMap() - latest.clear() - max.clear() - x.numbers = sorted.keys - y.numbers = sorted.values.map { it.inWholeMicroseconds / 1000.0 + 0.0001 } - } - } - } - } + traces(trace) } } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586..48c0a02 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.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 85c2910ee937dd66f3add473833d4f9ded1079a1 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 31 Mar 2024 16:13:02 +0300 Subject: [PATCH 68/71] Refactor ports --- CHANGELOG.md | 1 + .../controls/api/AsynchronousSocket.kt | 40 +++++ .../space/kscience/controls/api/Socket.kt | 32 ---- .../controls/ports/AsynchronousPort.kt | 125 ++++++++++++++++ .../space/kscience/controls/ports/Port.kt | 100 ------------- .../kscience/controls/ports/PortProxy.kt | 64 -------- .../space/kscience/controls/ports/Ports.kt | 31 +++- .../controls/ports/SynchronousPort.kt | 58 ++++++-- .../space/kscience/controls/ports/phrases.kt | 4 +- .../kscience/controls/ports/ChannelPort.kt | 138 +++++++++++------- .../kscience/controls/ports/JvmPortsPlugin.kt | 8 +- .../kscience/controls/ports/UdpSocketPort.kt | 44 +++--- ...ortIOTest.kt => AsynchronousPortIOTest.kt} | 8 +- .../controls/pi/AsynchronousPiPort.kt | 95 ++++++++++++ .../space/kscience/controls/pi/PiPlugin.kt | 11 +- .../kscience/controls/pi/PiSerialPort.kt | 82 ----------- .../kscience/controls/pi/SynchronousPiPort.kt | 98 +++++++++++++ .../controls/ports/KtorPortsPlugin.kt | 2 +- .../kscience/controls/ports/KtorTcpPort.kt | 67 ++++++--- .../kscience/controls/ports/KtorUdpPort.kt | 91 ++++++++---- .../controls/serial/AsynchronousSerialPort.kt | 134 +++++++++++++++++ .../controls/serial/JSerialCommPort.kt | 87 ----------- .../controls/serial/SerialPortPlugin.kt | 14 +- .../controls/serial/SynchronousSerialPort.kt | 127 ++++++++++++++++ .../sciprog/devices/mks/MksPdr900Device.kt | 3 +- .../pimotionmaster/PiMotionMasterDevice.kt | 8 +- .../PiMotionMasterVirtualDevice.kt | 58 +++++--- .../devices/pimotionmaster/piDebugServer.kt | 2 +- 28 files changed, 990 insertions(+), 542 deletions(-) create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/api/AsynchronousSocket.kt delete mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/ports/AsynchronousPort.kt delete mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt delete mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/ports/PortProxy.kt rename controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/{PortIOTest.kt => AsynchronousPortIOTest.kt} (85%) create mode 100644 controls-pi/src/main/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt delete mode 100644 controls-pi/src/main/kotlin/space/kscience/controls/pi/PiSerialPort.kt create mode 100644 controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt create mode 100644 controls-serial/src/main/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt delete mode 100644 controls-serial/src/main/kotlin/space/kscience/controls/serial/JSerialCommPort.kt create mode 100644 controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 760e00a..4a258d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Changed - Constructor properties return `DeviceStat` in order to be able to subscribe to them +- Refactored ports. Now we have `AsynchronousPort` as well as `SynchronousPort` ### Deprecated diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/AsynchronousSocket.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/AsynchronousSocket.kt new file mode 100644 index 0000000..defefb2 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/AsynchronousSocket.kt @@ -0,0 +1,40 @@ +package space.kscience.controls.api + +import kotlinx.coroutines.flow.Flow + +/** + * A generic bidirectional asynchronous sender/receiver object + */ +public interface AsynchronousSocket : AutoCloseable { + /** + * Send an object to the socket + */ + public suspend fun send(data: T) + + /** + * Flow of objects received from socket + */ + public fun subscribe(): Flow + + /** + * Start socket operation + */ + public fun open() + + /** + * Check if this socket is open + */ + public val isOpen: Boolean +} + +/** + * Connect an input to this socket. + * Multiple inputs could be connected to the same [AsynchronousSocket]. + * + * This method suspends indefinitely, so it should be started in a separate coroutine. + */ +public suspend fun AsynchronousSocket.sendFlow(flow: Flow) { + flow.collect { send(it) } +} + + diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt deleted file mode 100644 index 700db2c..0000000 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt +++ /dev/null @@ -1,32 +0,0 @@ -package space.kscience.controls.api - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.launch - -/** - * A generic bidirectional sender/receiver object - */ -public interface Socket : AutoCloseable { - /** - * Send an object to the socket - */ - public suspend fun send(data: T) - - /** - * Flow of objects received from socket - */ - public fun receiving(): Flow - public fun isOpen(): Boolean -} - -/** - * Connect an input to this socket using designated [scope] for it and return a handler [Job]. - * Multiple inputs could be connected to the same [Socket]. - */ -public fun Socket.connectInput(scope: CoroutineScope, flow: Flow): Job = scope.launch { - flow.collect { send(it) } -} - - diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/AsynchronousPort.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/AsynchronousPort.kt new file mode 100644 index 0000000..4462a42 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/AsynchronousPort.kt @@ -0,0 +1,125 @@ +package space.kscience.controls.ports + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.io.Buffer +import kotlinx.io.Source +import space.kscience.controls.api.AsynchronousSocket +import space.kscience.dataforge.context.* +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.int +import space.kscience.dataforge.meta.string +import kotlin.coroutines.CoroutineContext + +/** + * Raw [ByteArray] port + */ +public interface AsynchronousPort : ContextAware, AsynchronousSocket + +/** + * Capture [AsynchronousPort] output as kotlinx-io [Source]. + * [scope] controls the consummation. + * If the scope is canceled, the source stops producing. + */ +public fun AsynchronousPort.receiveAsSource(scope: CoroutineScope): Source { + val buffer = Buffer() + + subscribe().onEach { + buffer.write(it) + }.launchIn(scope) + + return buffer +} + + +/** + * Common abstraction for [AsynchronousPort] based on [Channel] + */ +public abstract class AbstractAsynchronousPort( + override val context: Context, + public val meta: Meta, + coroutineContext: CoroutineContext = context.coroutineContext, +) : AsynchronousPort { + + + protected val scope: CoroutineScope by lazy { + CoroutineScope( + coroutineContext + + SupervisorJob(coroutineContext[Job]) + + CoroutineExceptionHandler { _, throwable -> logger.error(throwable) { throwable.stackTraceToString() } } + + CoroutineName(toString()) + ) + } + + private val outgoing = Channel(meta["outgoing.capacity"].int?:100) + private val incoming = Channel(meta["incoming.capacity"].int?:100) + + /** + * Internal method to synchronously send data + */ + protected abstract suspend fun write(data: ByteArray) + + /** + * Internal method to receive data synchronously + */ + protected suspend fun receive(data: ByteArray) { + logger.debug { "$this RECEIVED: ${data.decodeToString()}" } + incoming.send(data) + } + + private var sendJob: Job? = null + + protected abstract fun onOpen() + + final override fun open() { + if (!isOpen) { + sendJob = scope.launch { + for (data in outgoing) { + try { + write(data) + logger.debug { "${this@AbstractAsynchronousPort} SENT: ${data.decodeToString()}" } + } catch (ex: Exception) { + if (ex is CancellationException) throw ex + logger.error(ex) { "Error while writing data to the port" } + } + } + } + onOpen() + } else { + logger.warn { "$this already opened" } + } + } + + + /** + * Send a data packet via the port + */ + override suspend fun send(data: ByteArray) { + outgoing.send(data) + } + + /** + * Raw flow of incoming data chunks. The chunks are not guaranteed to be complete phrases. + * To form phrases, some condition should be used on top of it. + * For example [stringsDelimitedIncoming] generates phrases with fixed delimiter. + */ + override fun subscribe(): Flow = incoming.receiveAsFlow() + + override fun close() { + outgoing.close() + incoming.close() + sendJob?.cancel() + } + + override fun toString(): String = meta["name"].string?:"ChannelPort[${hashCode().toString(16)}]" +} + +/** + * Send UTF-8 encoded string + */ +public suspend fun AsynchronousPort.send(string: String): Unit = send(string.encodeToByteArray()) \ No newline at end of file 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 deleted file mode 100644 index 9366604..0000000 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt +++ /dev/null @@ -1,100 +0,0 @@ -package space.kscience.controls.ports - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.receiveAsFlow -import space.kscience.controls.api.Socket -import space.kscience.dataforge.context.* -import space.kscience.dataforge.misc.DfType - -import kotlin.coroutines.CoroutineContext - -/** - * Raw [ByteArray] port - */ -public interface Port : ContextAware, Socket - -/** - * A specialized factory for [Port] - */ -@DfType(PortFactory.TYPE) -public interface PortFactory : Factory { - public val type: String - - public companion object { - public const val TYPE: String = "controls.port" - } -} - -/** - * Common abstraction for [Port] based on [Channel] - */ -public abstract class AbstractPort( - override val context: Context, - coroutineContext: CoroutineContext = context.coroutineContext, -) : Port { - - protected val scope: CoroutineScope = CoroutineScope(coroutineContext + SupervisorJob(coroutineContext[Job])) - - private val outgoing = Channel(100) - private val incoming = Channel(100) - - init { - scope.coroutineContext[Job]?.invokeOnCompletion { - close() - } - } - - /** - * Internal method to synchronously send data - */ - protected abstract suspend fun write(data: ByteArray) - - /** - * Internal method to receive data synchronously - */ - protected suspend fun receive(data: ByteArray) { - logger.debug { "${this@AbstractPort} RECEIVED: ${data.decodeToString()}" } - incoming.send(data) - } - - private val sendJob = scope.launch { - for (data in outgoing) { - try { - write(data) - logger.debug { "${this@AbstractPort} SENT: ${data.decodeToString()}" } - } catch (ex: Exception) { - if (ex is CancellationException) throw ex - logger.error(ex) { "Error while writing data to the port" } - } - } - } - - /** - * Send a data packet via the port - */ - override suspend fun send(data: ByteArray) { - outgoing.send(data) - } - - /** - * Raw flow of incoming data chunks. The chunks are not guaranteed to be complete phrases. - * To form phrases, some condition should be used on top of it. - * For example [stringsDelimitedIncoming] generates phrases with fixed delimiter. - */ - override fun receiving(): Flow = incoming.receiveAsFlow() - - override fun close() { - outgoing.close() - incoming.close() - scope.cancel() - } - - override fun isOpen(): Boolean = scope.isActive -} - -/** - * Send UTF-8 encoded string - */ -public suspend fun Port.send(string: String): Unit = send(string.encodeToByteArray()) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/PortProxy.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/PortProxy.kt deleted file mode 100644 index 4e51f6f..0000000 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/PortProxy.kt +++ /dev/null @@ -1,64 +0,0 @@ -package space.kscience.controls.ports - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import space.kscience.dataforge.context.* - -/** - * A port that could be closed multiple times and opens automatically on request - */ -public class PortProxy(override val context: Context = Global, public val factory: suspend () -> Port) : Port, ContextAware { - - private var actualPort: Port? = null - private val mutex: Mutex = Mutex() - - private suspend fun port(): Port { - return mutex.withLock { - if (actualPort?.isOpen() == true) { - actualPort!! - } else { - factory().also { - actualPort = it - } - } - } - } - - override suspend fun send(data: ByteArray) { - port().send(data) - } - - @OptIn(ExperimentalCoroutinesApi::class) - override fun receiving(): Flow = flow { - while (true) { - try { - //recreate port and Flow on connection problems - port().receiving().collect { - emit(it) - } - } catch (t: Throwable) { - logger.warn{"Port read failed: ${t.message}. Reconnecting."} - mutex.withLock { - actualPort?.close() - actualPort = null - } - } - } - } - - // open by default - override fun isOpen(): Boolean = true - - override fun close() { - context.launch { - mutex.withLock { - actualPort?.close() - actualPort = null - } - } - } -} \ No newline at end of file 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 3d01e62..565caee 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 @@ -11,26 +11,43 @@ public class Ports : AbstractPlugin() { override val tag: PluginTag get() = Companion.tag - private val portFactories by lazy { - context.gather(PortFactory.TYPE) + private val synchronousPortFactories by lazy { + context.gather>(SYNCHRONOUS_PORT_TYPE) } - private val portCache = mutableMapOf() + private val asynchronousPortFactories by lazy { + context.gather>(ASYNCHRONOUS_PORT_TYPE) + } /** - * Create a new [Port] according to specification + * Create a new [AsynchronousPort] according to specification */ - public fun buildPort(meta: Meta): Port = portCache.getOrPut(meta) { + public fun buildAsynchronousPort(meta: Meta): AsynchronousPort { val type by meta.string { error("Port type is not defined") } - val factory = portFactories.values.firstOrNull { it.type == type } + val factory = asynchronousPortFactories.entries + .firstOrNull { it.key.toString() == type }?.value ?: error("Port factory for type $type not found") - factory.build(context, meta) + return factory.build(context, meta) + } + + /** + * Create a [SynchronousPort] according to specification or wrap an asynchronous implementation + */ + public fun buildSynchronousPort(meta: Meta): SynchronousPort { + val type by meta.string { error("Port type is not defined") } + val factory = synchronousPortFactories.entries + .firstOrNull { it.key.toString() == type }?.value + ?: return buildAsynchronousPort(meta).asSynchronousPort() + return factory.build(context, meta) } public companion object : PluginFactory { override val tag: PluginTag = PluginTag("controls.ports", group = PluginTag.DATAFORGE_GROUP) + public const val ASYNCHRONOUS_PORT_TYPE: String = "controls.asynchronousPort" + public const val SYNCHRONOUS_PORT_TYPE: String = "controls.synchronousPort" + override fun build(context: Context, meta: Meta): Ports = Ports() } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt index 0ed4764..8d95eb0 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt @@ -4,25 +4,65 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.ContextAware /** - * A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended. - * The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment. + * A port handler for synchronous (request-response) communication with a port. + * Only one request could be active at a time (others are suspended). */ -public class SynchronousPort(public val port: Port, private val mutex: Mutex) : Port by port { +public interface SynchronousPort : ContextAware, AutoCloseable { + + public fun open() + + public val isOpen: Boolean + /** - * Send a single message and wait for the flow of respond messages. + * Send a single message and wait for the flow of response chunks. + * The consumer is responsible for calling a terminal operation on the flow. */ - public suspend fun respond(data: ByteArray, transform: suspend Flow.() -> R): R = mutex.withLock { - port.send(data) - transform(port.receiving()) + public suspend fun respond( + request: ByteArray, + transform: suspend Flow.() -> R, + ): R +} + +private class SynchronousOverAsynchronousPort( + val port: AsynchronousPort, + val mutex: Mutex, +) : SynchronousPort { + + override val context: Context get() = port.context + + override fun open() { + if (!port.isOpen) port.open() + } + + override val isOpen: Boolean get() = port.isOpen + + override fun close() { + if (port.isOpen) port.close() + } + + override suspend fun respond( + request: ByteArray, + transform: suspend Flow.() -> R, + ): R = mutex.withLock { + port.send(request) + transform(port.subscribe()) } } + /** - * Provide a synchronous wrapper for a port + * Provide a synchronous wrapper for an asynchronous port. + * Optionally provide external [mutex] for operation synchronization. + * + * If the [AsynchronousPort] is called directly, it could violate [SynchronousPort] contract + * of only one request running simultaneously. */ -public fun Port.synchronous(mutex: Mutex = Mutex()): SynchronousPort = SynchronousPort(this, mutex) +public fun AsynchronousPort.asSynchronousPort(mutex: Mutex = Mutex()): SynchronousPort = + SynchronousOverAsynchronousPort(this, mutex) /** * Send request and read incoming data blocks until the delimiter is encountered diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt index 03d21f5..89bee8a 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt @@ -77,9 +77,9 @@ public fun Flow.withStringDelimiter(delimiter: String): Flow /** * A flow of delimited phrases */ -public fun Port.delimitedIncoming(delimiter: ByteArray): Flow = receiving().withDelimiter(delimiter) +public fun AsynchronousPort.delimitedIncoming(delimiter: ByteArray): Flow = subscribe().withDelimiter(delimiter) /** * A flow of delimited phrases with string content */ -public fun Port.stringsDelimitedIncoming(delimiter: String): Flow = receiving().withStringDelimiter(delimiter) +public fun AsynchronousPort.stringsDelimitedIncoming(delimiter: String): Flow = subscribe().withStringDelimiter(delimiter) diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt index f450607..85c3d5c 100644 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/ChannelPort.kt @@ -1,10 +1,7 @@ package space.kscience.controls.ports import kotlinx.coroutines.* -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.error -import space.kscience.dataforge.context.info -import space.kscience.dataforge.context.logger +import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.* import java.net.InetSocketAddress import java.nio.ByteBuffer @@ -14,7 +11,10 @@ import java.nio.channels.DatagramChannel import java.nio.channels.SocketChannel import kotlin.coroutines.CoroutineContext -public fun ByteBuffer.toArray(limit: Int = limit()): ByteArray { +/** + * Copy the contents of this buffer to an array + */ +public fun ByteBuffer.copyToArray(limit: Int = limit()): ByteArray { rewind() val response = ByteArray(limit) get(response) @@ -27,35 +27,40 @@ public fun ByteBuffer.toArray(limit: Int = limit()): ByteArray { */ public class ChannelPort( context: Context, + meta: Meta, coroutineContext: CoroutineContext = context.coroutineContext, channelBuilder: suspend () -> ByteChannel, -) : AbstractPort(context, coroutineContext), AutoCloseable { - - private val futureChannel: Deferred = scope.async(Dispatchers.IO) { - channelBuilder() - } +) : AbstractAsynchronousPort(context, meta, coroutineContext), AutoCloseable { /** * A handler to await port connection */ - public val startJob: Job get() = futureChannel + private val futureChannel: Deferred = scope.async(Dispatchers.IO, start = CoroutineStart.LAZY) { + channelBuilder() + } - private val listenerJob = scope.launch(Dispatchers.IO) { - val channel = futureChannel.await() - val buffer = ByteBuffer.allocate(1024) - while (isActive && channel.isOpen) { - try { - val num = channel.read(buffer) - if (num > 0) { - receive(buffer.toArray(num)) - } - if (num < 0) cancel("The input channel is exhausted") - } catch (ex: Exception) { - if (ex is AsynchronousCloseException) { - logger.info { "Channel $channel closed" } - } else { - logger.error(ex) { "Channel read error, retrying in 1 second" } - delay(1000) + private var listenerJob: Job? = null + + override val isOpen: Boolean get() = listenerJob?.isActive == true + + override fun onOpen() { + listenerJob = scope.launch(Dispatchers.IO) { + val channel = futureChannel.await() + val buffer = ByteBuffer.allocate(1024) + while (isActive && channel.isOpen) { + try { + val num = channel.read(buffer) + if (num > 0) { + receive(buffer.copyToArray(num)) + } + if (num < 0) cancel("The input channel is exhausted") + } catch (ex: Exception) { + if (ex is AsynchronousCloseException) { + logger.info { "Channel $channel closed" } + } else { + logger.error(ex) { "Channel read error, retrying in 1 second" } + delay(1000) + } } } } @@ -67,6 +72,7 @@ public class ChannelPort( @OptIn(ExperimentalCoroutinesApi::class) override fun close() { + listenerJob?.cancel() if (futureChannel.isCompleted) { futureChannel.getCompleted().close() } @@ -75,61 +81,95 @@ public class ChannelPort( } /** - * A [PortFactory] for TCP connections + * A [Factory] for TCP connections */ -public object TcpPort : PortFactory { +public object TcpPort : Factory { - override val type: String = "tcp" + public fun build( + context: Context, + host: String, + port: Int, + coroutineContext: CoroutineContext = context.coroutineContext, + ): ChannelPort { + val meta = Meta { + "name" put "tcp://$host:$port" + "type" put "tcp" + "host" put host + "port" put port + } + return ChannelPort(context, meta, coroutineContext) { + SocketChannel.open(InetSocketAddress(host, port)) + } + } + /** + * Create and open TCP port + */ public fun open( context: Context, host: String, port: Int, coroutineContext: CoroutineContext = context.coroutineContext, - ): ChannelPort = ChannelPort(context, coroutineContext) { - SocketChannel.open(InetSocketAddress(host, port)) - } + ): ChannelPort = build(context, host, port, coroutineContext).apply { open() } override fun build(context: Context, meta: Meta): ChannelPort { val host = meta["host"].string ?: "localhost" val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta") - return open(context, host, port) + return build(context, host, port) } + } /** - * A [PortFactory] for UDP connections + * A [Factory] for UDP connections */ -public object UdpPort : PortFactory { +public object UdpPort : Factory { - override val type: String = "udp" + public fun build( + context: Context, + remoteHost: String, + remotePort: Int, + localPort: Int? = null, + localHost: String? = null, + coroutineContext: CoroutineContext = context.coroutineContext, + ): ChannelPort { + val meta = Meta { + "name" put "udp://$remoteHost:$remotePort" + "type" put "udp" + "remoteHost" put remoteHost + "remotePort" put remotePort + localHost?.let { "localHost" put it } + localPort?.let { "localPort" put it } + } + return ChannelPort(context, meta, coroutineContext) { + DatagramChannel.open().apply { + //bind the channel to a local port to receive messages + localPort?.let { bind(InetSocketAddress(localHost ?: "localhost", it)) } + //connect to remote port to send messages + connect(InetSocketAddress(remoteHost, remotePort.toInt())) + context.logger.info { "Connected to UDP $remotePort on $remoteHost" } + } + } + } /** * Connect a datagram channel to a remote host/port. If [localPort] is provided, it is used to bind local port for receiving messages. */ - public fun openChannel( + public fun open( context: Context, remoteHost: String, remotePort: Int, localPort: Int? = null, localHost: String = "localhost", - coroutineContext: CoroutineContext = context.coroutineContext, - ): ChannelPort = ChannelPort(context, coroutineContext) { - DatagramChannel.open().apply { - //bind the channel to a local port to receive messages - localPort?.let { bind(InetSocketAddress(localHost, localPort)) } - //connect to remote port to send messages - connect(InetSocketAddress(remoteHost, remotePort)) - context.logger.info { "Connected to UDP $remotePort on $remoteHost" } - } - } + ): ChannelPort = build(context, remoteHost, remotePort, localPort, localHost).apply { open() } + override fun build(context: Context, meta: Meta): ChannelPort { val remoteHost by meta.string { error("Remote host is not specified") } val remotePort by meta.number { error("Remote port is not specified") } val localHost: String? by meta.string() val localPort: Int? by meta.int() - return openChannel(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost") + return build(context, remoteHost, remotePort.toInt(), localPort, localHost) } } \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/JvmPortsPlugin.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/JvmPortsPlugin.kt index d9d87e2..82e1ac0 100644 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/JvmPortsPlugin.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/JvmPortsPlugin.kt @@ -6,7 +6,7 @@ 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 space.kscience.dataforge.names.parseAsName +import space.kscience.dataforge.names.asName /** * A plugin for loading JVM nio-based ports @@ -17,9 +17,9 @@ public class JvmPortsPlugin : AbstractPlugin() { override val tag: PluginTag get() = Companion.tag override fun content(target: String): Map = when(target){ - PortFactory.TYPE -> mapOf( - TcpPort.type.parseAsName() to TcpPort, - UdpPort.type.parseAsName() to UdpPort + Ports.ASYNCHRONOUS_PORT_TYPE -> mapOf( + "tcp".asName() to TcpPort, + "udp".asName() to UdpPort ) else -> emptyMap() } diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/UdpSocketPort.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/UdpSocketPort.kt index 27bb0d8..ae65c64 100644 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/UdpSocketPort.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/UdpSocketPort.kt @@ -1,10 +1,8 @@ package space.kscience.controls.ports -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.Meta import java.net.DatagramPacket import java.net.DatagramSocket import kotlin.coroutines.CoroutineContext @@ -14,33 +12,39 @@ import kotlin.coroutines.CoroutineContext */ public class UdpSocketPort( override val context: Context, + meta: Meta, private val socket: DatagramSocket, coroutineContext: CoroutineContext = context.coroutineContext, -) : AbstractPort(context, coroutineContext) { +) : AbstractAsynchronousPort(context, meta, coroutineContext) { - private val listenerJob = context.launch(Dispatchers.IO) { - while (isActive) { - val buf = ByteArray(socket.receiveBufferSize) + private var listenerJob: Job? = null - val packet = DatagramPacket( - buf, - buf.size, - ) - socket.receive(packet) + override fun onOpen() { + listenerJob = context.launch(Dispatchers.IO) { + while (isActive) { + val buf = ByteArray(socket.receiveBufferSize) - val bytes = packet.data.copyOfRange( - packet.offset, - packet.offset + packet.length - ) - receive(bytes) + val packet = DatagramPacket( + buf, + buf.size, + ) + socket.receive(packet) + + val bytes = packet.data.copyOfRange( + packet.offset, + packet.offset + packet.length + ) + receive(bytes) + } } } override fun close() { - listenerJob.cancel() + listenerJob?.cancel() + super.close() } - override fun isOpen(): Boolean = listenerJob.isActive + override val isOpen: Boolean get() = listenerJob?.isActive == true override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) { diff --git a/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt b/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/AsynchronousPortIOTest.kt similarity index 85% rename from controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt rename to controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/AsynchronousPortIOTest.kt index daa6173..b19cfad 100644 --- a/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt +++ b/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/AsynchronousPortIOTest.kt @@ -12,7 +12,7 @@ import space.kscience.dataforge.context.Global import kotlin.test.assertEquals -internal class PortIOTest { +internal class AsynchronousPortIOTest { @Test fun testDelimiteredByteArrayFlow() { @@ -29,8 +29,8 @@ internal class PortIOTest { @Test fun testUdpCommunication() = runTest { - val receiver = UdpPort.openChannel(Global, "localhost", 8811, localPort = 8812) - val sender = UdpPort.openChannel(Global, "localhost", 8812, localPort = 8811) + val receiver = UdpPort.open(Global, "localhost", 8811, localPort = 8812) + val sender = UdpPort.open(Global, "localhost", 8812, localPort = 8811) delay(30) repeat(10) { @@ -38,7 +38,7 @@ internal class PortIOTest { } val res = receiver - .receiving() + .subscribe() .withStringDelimiter("\n") .take(10) .toList() diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt b/controls-pi/src/main/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt new file mode 100644 index 0000000..1aee657 --- /dev/null +++ b/controls-pi/src/main/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt @@ -0,0 +1,95 @@ +package space.kscience.controls.pi + +import com.pi4j.io.serial.Baud +import com.pi4j.io.serial.Serial +import com.pi4j.io.serial.SerialConfigBuilder +import com.pi4j.ktx.io.serial +import kotlinx.coroutines.* +import space.kscience.controls.ports.AbstractAsynchronousPort +import space.kscience.controls.ports.AsynchronousPort +import space.kscience.controls.ports.copyToArray +import space.kscience.dataforge.context.* +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.enum +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.string +import java.nio.ByteBuffer +import kotlin.coroutines.CoroutineContext + +public class AsynchronousPiPort( + context: Context, + meta: Meta, + private val serial: Serial, + coroutineContext: CoroutineContext = context.coroutineContext, +) : AbstractAsynchronousPort(context, meta, coroutineContext) { + + + private var listenerJob: Job? = null + override fun onOpen() { + serial.open() + listenerJob = this.scope.launch(Dispatchers.IO) { + val buffer = ByteBuffer.allocate(1024) + while (isActive) { + try { + val num = serial.read(buffer) + if (num > 0) { + receive(buffer.copyToArray(num)) + } + if (num < 0) cancel("The input channel is exhausted") + } catch (ex: Exception) { + logger.error(ex) { "Channel read error" } + delay(1000) + } + } + } + } + + override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) { + serial.write(data) + } + + + override val isOpen: Boolean get() = listenerJob?.isActive == true + + override fun close() { + listenerJob?.cancel() + serial.close() + } + + public companion object : Factory { + + + public fun build( + context: Context, + device: String, + block: SerialConfigBuilder.() -> Unit, + ): AsynchronousPiPort { + val meta = Meta { + "name" put "pi://$device" + "type" put "serial" + } + val pi = context.request(PiPlugin) + + val serial = pi.piContext.serial(device, block) + return AsynchronousPiPort(context, meta, serial) + } + + public fun open( + context: Context, + device: String, + block: SerialConfigBuilder.() -> Unit, + ): AsynchronousPiPort = build(context, device, block).apply { open() } + + override fun build(context: Context, meta: Meta): AsynchronousPort { + val device: String = meta["device"].string ?: error("Device name not defined") + val baudRate: Baud = meta["baudRate"].enum() ?: Baud._9600 + val pi = context.request(PiPlugin) + val serial = pi.piContext.serial(device) { + baud8N1(baudRate) + } + return AsynchronousPiPort(context, meta, serial) + } + + } +} + diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt b/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt index 20d4c80..0f2bb02 100644 --- a/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt +++ b/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt @@ -2,7 +2,6 @@ package space.kscience.controls.pi import com.pi4j.Pi4J import space.kscience.controls.manager.DeviceManager -import space.kscience.controls.ports.PortFactory import space.kscience.controls.ports.Ports import space.kscience.dataforge.context.AbstractPlugin import space.kscience.dataforge.context.Context @@ -10,7 +9,7 @@ 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 space.kscience.dataforge.names.parseAsName +import space.kscience.dataforge.names.asName import com.pi4j.context.Context as PiContext public class PiPlugin : AbstractPlugin() { @@ -22,8 +21,11 @@ public class PiPlugin : AbstractPlugin() { public val piContext: PiContext by lazy { createPiContext(context, meta) } override fun content(target: String): Map = when (target) { - PortFactory.TYPE -> mapOf( - PiSerialPort.type.parseAsName() to PiSerialPort, + Ports.ASYNCHRONOUS_PORT_TYPE -> mapOf( + "serial".asName() to AsynchronousPiPort, + ) + Ports.SYNCHRONOUS_PORT_TYPE -> mapOf( + "serial".asName() to SynchronousPiPort, ) else -> super.content(target) @@ -40,6 +42,7 @@ public class PiPlugin : AbstractPlugin() { override fun build(context: Context, meta: Meta): PiPlugin = PiPlugin() + @Suppress("UNUSED_PARAMETER") public fun createPiContext(context: Context, meta: Meta): PiContext = Pi4J.newAutoContext() } diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiSerialPort.kt b/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiSerialPort.kt deleted file mode 100644 index 6b9662d..0000000 --- a/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiSerialPort.kt +++ /dev/null @@ -1,82 +0,0 @@ -package space.kscience.controls.pi - -import com.pi4j.io.serial.Baud -import com.pi4j.io.serial.Serial -import com.pi4j.io.serial.SerialConfigBuilder -import com.pi4j.ktx.io.serial -import kotlinx.coroutines.* -import space.kscience.controls.ports.AbstractPort -import space.kscience.controls.ports.Port -import space.kscience.controls.ports.PortFactory -import space.kscience.controls.ports.toArray -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.error -import space.kscience.dataforge.context.logger -import space.kscience.dataforge.context.request -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.enum -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.string -import java.nio.ByteBuffer -import kotlin.coroutines.CoroutineContext -import com.pi4j.context.Context as PiContext - -public class PiSerialPort( - context: Context, - coroutineContext: CoroutineContext = context.coroutineContext, - public val serialBuilder: PiContext.() -> Serial, -) : AbstractPort(context, coroutineContext) { - - private val serial: Serial by lazy { - val pi = context.request(PiPlugin) - pi.piContext.serialBuilder() - } - - - private val listenerJob = this.scope.launch(Dispatchers.IO) { - val buffer = ByteBuffer.allocate(1024) - while (isActive) { - try { - val num = serial.read(buffer) - if (num > 0) { - receive(buffer.toArray(num)) - } - if (num < 0) cancel("The input channel is exhausted") - } catch (ex: Exception) { - logger.error(ex) { "Channel read error" } - delay(1000) - } - } - } - - override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) { - serial.write(data) - } - - override fun close() { - listenerJob.cancel() - serial.close() - } - - public companion object : PortFactory { - override val type: String get() = "pi" - - public fun open( - context: Context, - device: String, - block: SerialConfigBuilder.() -> Unit, - ): PiSerialPort = PiSerialPort(context) { - serial(device, block) - } - - override fun build(context: Context, meta: Meta): Port = PiSerialPort(context) { - val device: String = meta["device"].string ?: error("Device name not defined") - val baudRate: Baud = meta["baudRate"].enum() ?: Baud._9600 - serial(device) { - baud8N1(baudRate) - } - } - - } -} - diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt b/controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt new file mode 100644 index 0000000..acefb1f --- /dev/null +++ b/controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt @@ -0,0 +1,98 @@ +package space.kscience.controls.pi + +import com.pi4j.io.serial.Baud +import com.pi4j.io.serial.Serial +import com.pi4j.io.serial.SerialConfigBuilder +import com.pi4j.ktx.io.serial +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import space.kscience.controls.ports.SynchronousPort +import space.kscience.controls.ports.copyToArray +import space.kscience.dataforge.context.* +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.enum +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.string +import java.nio.ByteBuffer + +public class SynchronousPiPort( + override val context: Context, + public val meta: Meta, + private val serial: Serial, + private val mutex: Mutex = Mutex(), +) : SynchronousPort { + + private val pi = context.request(PiPlugin) + override fun open() { + serial.open() + } + + override val isOpen: Boolean get() = serial.isOpen + + override suspend fun respond( + request: ByteArray, + transform: suspend Flow.() -> R, + ): R = mutex.withLock { + serial.drain() + serial.write(request) + flow { + val buffer = ByteBuffer.allocate(1024) + while (isOpen) { + try { + val num = serial.read(buffer) + if (num > 0) { + emit(buffer.copyToArray(num)) + } + if (num < 0) break + } catch (ex: Exception) { + logger.error(ex) { "Channel read error" } + delay(1000) + } + } + }.transform() + } + + override fun close() { + serial.close() + } + + public companion object : Factory { + + + public fun build( + context: Context, + device: String, + block: SerialConfigBuilder.() -> Unit, + ): SynchronousPiPort { + val meta = Meta { + "name" put "pi://$device" + "type" put "serial" + } + val pi = context.request(PiPlugin) + + val serial = pi.piContext.serial(device, block) + return SynchronousPiPort(context, meta, serial) + } + + public fun open( + context: Context, + device: String, + block: SerialConfigBuilder.() -> Unit, + ): SynchronousPiPort = build(context, device, block).apply { open() } + + override fun build(context: Context, meta: Meta): SynchronousPiPort { + val device: String = meta["device"].string ?: error("Device name not defined") + val baudRate: Baud = meta["baudRate"].enum() ?: Baud._9600 + val pi = context.request(PiPlugin) + val serial = pi.piContext.serial(device) { + baud8N1(baudRate) + } + return SynchronousPiPort(context, meta, serial) + } + + } +} + diff --git a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt b/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt index 9c6dbba..6e3041c 100644 --- a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt +++ b/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt @@ -13,7 +13,7 @@ public class KtorPortsPlugin : AbstractPlugin() { override val tag: PluginTag get() = Companion.tag override fun content(target: String): Map = when (target) { - PortFactory.TYPE -> mapOf("tcp".asName() to KtorTcpPort, "udp".asName() to KtorUdpPort) + Ports.ASYNCHRONOUS_PORT_TYPE -> mapOf("tcp".asName() to KtorTcpPort, "udp".asName() to KtorUdpPort) else -> emptyMap() } diff --git a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt b/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt index a7a698e..463e922 100644 --- a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt +++ b/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt @@ -8,42 +8,46 @@ import io.ktor.network.sockets.openWriteChannel import io.ktor.utils.io.consumeEachBufferRange import io.ktor.utils.io.core.Closeable import io.ktor.utils.io.writeAvailable -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.string +import java.nio.ByteBuffer import kotlin.coroutines.CoroutineContext public class KtorTcpPort internal constructor( context: Context, + meta: Meta, public val host: String, public val port: Int, coroutineContext: CoroutineContext = context.coroutineContext, - socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {} -) : AbstractPort(context, coroutineContext), Closeable { + socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {}, +) : AbstractAsynchronousPort(context, meta, coroutineContext), Closeable { override fun toString(): String = "port[tcp:$host:$port]" - private val futureSocket = scope.async { + private val futureSocket = scope.async(Dispatchers.IO, start = CoroutineStart.LAZY) { aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(host, port, socketOptions) } - private val writeChannel = scope.async { + private val writeChannel = scope.async(Dispatchers.IO, start = CoroutineStart.LAZY) { futureSocket.await().openWriteChannel(true) } - private val listenerJob = scope.launch { - val input = futureSocket.await().openReadChannel() - input.consumeEachBufferRange { buffer, _ -> - val array = ByteArray(buffer.remaining()) - buffer.get(array) - receive(array) - isActive + private var listenerJob: Job? = null + + override fun onOpen() { + listenerJob = scope.launch { + val input = futureSocket.await().openReadChannel() + input.consumeEachBufferRange { buffer: ByteBuffer, last -> + val array = ByteArray(buffer.remaining()) + buffer.get(array) + receive(array) + !last && isActive + } } } @@ -51,30 +55,45 @@ public class KtorTcpPort internal constructor( writeChannel.await().writeAvailable(data) } + override val isOpen: Boolean + get() = listenerJob?.isActive == true + override fun close() { - listenerJob.cancel() + listenerJob?.cancel() futureSocket.cancel() super.close() } - public companion object : PortFactory { + public companion object : Factory { - override val type: String = "tcp" + public fun build( + context: Context, + host: String, + port: Int, + coroutineContext: CoroutineContext = context.coroutineContext, + socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {}, + ): KtorTcpPort { + val meta = Meta { + "name" put "tcp://$host:$port" + "type" put "tcp" + "host" put host + "port" put port + } + return KtorTcpPort(context, meta, host, port, coroutineContext, socketOptions) + } public fun open( context: Context, host: String, port: Int, coroutineContext: CoroutineContext = context.coroutineContext, - socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {} - ): KtorTcpPort { - return KtorTcpPort(context, host, port, coroutineContext, socketOptions) - } + socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {}, + ): KtorTcpPort = build(context, host, port, coroutineContext, socketOptions).apply { open() } - override fun build(context: Context, meta: Meta): Port { + override fun build(context: Context, meta: Meta): AsynchronousPort { val host = meta["host"].string ?: "localhost" val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta") - return open(context, host, port) + return build(context, host, port) } } } \ No newline at end of file diff --git a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt b/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt index 1f30914..48096cf 100644 --- a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt +++ b/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt @@ -8,6 +8,7 @@ import io.ktor.utils.io.core.Closeable import io.ktor.utils.io.writeAvailable import kotlinx.coroutines.* import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.number @@ -16,17 +17,18 @@ import kotlin.coroutines.CoroutineContext public class KtorUdpPort internal constructor( context: Context, + meta: Meta, public val remoteHost: String, public val remotePort: Int, public val localPort: Int? = null, public val localHost: String = "localhost", coroutineContext: CoroutineContext = context.coroutineContext, - socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {} -) : AbstractPort(context, coroutineContext), Closeable { + socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {}, +) : AbstractAsynchronousPort(context, meta, coroutineContext), Closeable { override fun toString(): String = "port[udp:$remoteHost:$remotePort]" - private val futureSocket = scope.async { + private val futureSocket = scope.async(Dispatchers.IO, start = CoroutineStart.LAZY) { aSocket(ActorSelectorManager(Dispatchers.IO)).udp().connect( remoteAddress = InetSocketAddress(remoteHost, remotePort), localAddress = localPort?.let { InetSocketAddress(localHost, localPort) }, @@ -34,17 +36,21 @@ public class KtorUdpPort internal constructor( ) } - private val writeChannel: Deferred = scope.async { + private val writeChannel: Deferred = scope.async(Dispatchers.IO, start = CoroutineStart.LAZY) { futureSocket.await().openWriteChannel(true) } - private val listenerJob = scope.launch { - val input = futureSocket.await().openReadChannel() - input.consumeEachBufferRange { buffer, _ -> - val array = ByteArray(buffer.remaining()) - buffer.get(array) - receive(array) - isActive + private var listenerJob: Job? = null + + override fun onOpen() { + listenerJob = scope.launch { + val input = futureSocket.await().openReadChannel() + input.consumeEachBufferRange { buffer, last -> + val array = ByteArray(buffer.remaining()) + buffer.get(array) + receive(array) + !last && isActive + } } } @@ -52,16 +58,49 @@ public class KtorUdpPort internal constructor( writeChannel.await().writeAvailable(data) } + override val isOpen: Boolean + get() = listenerJob?.isActive == true + override fun close() { - listenerJob.cancel() + listenerJob?.cancel() futureSocket.cancel() super.close() } - public companion object : PortFactory { + public companion object : Factory { - override val type: String = "udp" + public fun build( + context: Context, + remoteHost: String, + remotePort: Int, + localPort: Int? = null, + localHost: String? = null, + coroutineContext: CoroutineContext = context.coroutineContext, + socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {}, + ): KtorUdpPort { + val meta = Meta { + "name" put "udp://$remoteHost:$remotePort" + "type" put "udp" + "remoteHost" put remoteHost + "remotePort" put remotePort + localHost?.let { "localHost" put it } + localPort?.let { "localPort" put it } + } + return KtorUdpPort( + context = context, + meta = meta, + remoteHost = remoteHost, + remotePort = remotePort, + localPort = localPort, + localHost = localHost ?: "localhost", + coroutineContext = coroutineContext, + socketOptions = socketOptions + ) + } + /** + * Create and open UDP port + */ public fun open( context: Context, remoteHost: String, @@ -69,23 +108,23 @@ public class KtorUdpPort internal constructor( localPort: Int? = null, localHost: String = "localhost", coroutineContext: CoroutineContext = context.coroutineContext, - socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {} - ): KtorUdpPort = KtorUdpPort( - context = context, - remoteHost = remoteHost, - remotePort = remotePort, - localPort = localPort, - localHost = localHost, - coroutineContext = coroutineContext, - socketOptions = socketOptions - ) + socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {}, + ): KtorUdpPort = build( + context, + remoteHost, + remotePort, + localPort, + localHost, + coroutineContext, + socketOptions + ).apply { open() } - override fun build(context: Context, meta: Meta): Port { + override fun build(context: Context, meta: Meta): AsynchronousPort { val remoteHost by meta.string { error("Remote host is not specified") } val remotePort by meta.number { error("Remote port is not specified") } val localHost: String? by meta.string() val localPort: Int? by meta.int() - return open(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost") + return build(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost") } } } \ No newline at end of file diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt b/controls-serial/src/main/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt new file mode 100644 index 0000000..b6e83bc --- /dev/null +++ b/controls-serial/src/main/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt @@ -0,0 +1,134 @@ +package space.kscience.controls.serial + +import com.fazecast.jSerialComm.SerialPort +import com.fazecast.jSerialComm.SerialPortDataListener +import com.fazecast.jSerialComm.SerialPortEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import space.kscience.controls.ports.AbstractAsynchronousPort +import space.kscience.controls.ports.AsynchronousPort +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.int +import space.kscience.dataforge.meta.string +import kotlin.coroutines.CoroutineContext + +/** + * A port based on JSerialComm + */ +public class AsynchronousSerialPort( + context: Context, + meta: Meta, + private val comPort: SerialPort, + coroutineContext: CoroutineContext = context.coroutineContext, +) : AbstractAsynchronousPort(context, meta, coroutineContext) { + + override fun toString(): String = "port[${comPort.descriptivePortName}]" + + private val serialPortListener = object : SerialPortDataListener { + override fun getListeningEvents(): Int = + SerialPort.LISTENING_EVENT_DATA_RECEIVED and SerialPort.LISTENING_EVENT_DATA_AVAILABLE + + override fun serialEvent(event: SerialPortEvent) { + when (event.eventType) { + SerialPort.LISTENING_EVENT_DATA_RECEIVED -> { + scope.launch { receive(event.receivedData) } + } + + SerialPort.LISTENING_EVENT_DATA_AVAILABLE -> { + scope.launch(Dispatchers.IO) { + val available = comPort.bytesAvailable() + if (available > 0) { + val buffer = ByteArray(available) + comPort.readBytes(buffer, available) + receive(buffer) + } + } + } + } + } + } + + override fun onOpen() { + comPort.openPort() + comPort.addDataListener(serialPortListener) + } + + override val isOpen: Boolean get() = comPort.isOpen + + override suspend fun write(data: ByteArray) { + comPort.writeBytes(data, data.size) + } + + override fun close() { + comPort.removeDataListener() + if (comPort.isOpen) { + comPort.closePort() + } + super.close() + } + + public companion object : Factory { + + public fun build( + context: Context, + portName: String, + baudRate: Int = 9600, + dataBits: Int = 8, + stopBits: Int = SerialPort.ONE_STOP_BIT, + parity: Int = SerialPort.NO_PARITY, + coroutineContext: CoroutineContext = context.coroutineContext, + additionalConfig: SerialPort.() -> Unit = {}, + ): AsynchronousSerialPort { + val serialPort = SerialPort.getCommPort(portName).apply { + setComPortParameters(baudRate, dataBits, stopBits, parity) + additionalConfig() + } + val meta = Meta { + "name" put "com://$portName" + "type" put "serial" + "baudRate" put serialPort.baudRate + "dataBits" put serialPort.numDataBits + "stopBits" put serialPort.numStopBits + "parity" put serialPort.parity + } + return AsynchronousSerialPort(context, meta, serialPort, coroutineContext) + } + + + /** + * Construct ComPort with given parameters + */ + public fun open( + context: Context, + portName: String, + baudRate: Int = 9600, + dataBits: Int = 8, + stopBits: Int = SerialPort.ONE_STOP_BIT, + parity: Int = SerialPort.NO_PARITY, + coroutineContext: CoroutineContext = context.coroutineContext, + additionalConfig: SerialPort.() -> Unit = {}, + ): AsynchronousSerialPort = build( + context = context, + portName = portName, + baudRate = baudRate, + dataBits = dataBits, + stopBits = stopBits, + parity = parity, + coroutineContext = coroutineContext, + additionalConfig = additionalConfig + ).apply { open() } + + + override fun build(context: Context, meta: Meta): AsynchronousPort { + val name by meta.string { error("Serial port name not defined") } + val baudRate by meta.int(9600) + val dataBits by meta.int(8) + val stopBits by meta.int(SerialPort.ONE_STOP_BIT) + val parity by meta.int(SerialPort.NO_PARITY) + return build(context, name, baudRate, dataBits, stopBits, parity) + } + } + +} \ No newline at end of file diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/JSerialCommPort.kt b/controls-serial/src/main/kotlin/space/kscience/controls/serial/JSerialCommPort.kt deleted file mode 100644 index 8a2caab..0000000 --- a/controls-serial/src/main/kotlin/space/kscience/controls/serial/JSerialCommPort.kt +++ /dev/null @@ -1,87 +0,0 @@ -package space.kscience.controls.serial - -import com.fazecast.jSerialComm.SerialPort -import com.fazecast.jSerialComm.SerialPortDataListener -import com.fazecast.jSerialComm.SerialPortEvent -import kotlinx.coroutines.launch -import space.kscience.controls.ports.AbstractPort -import space.kscience.controls.ports.Port -import space.kscience.controls.ports.PortFactory -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.int -import space.kscience.dataforge.meta.string -import kotlin.coroutines.CoroutineContext - -/** - * A port based on JSerialComm - */ -public class JSerialCommPort( - context: Context, - private val comPort: SerialPort, - coroutineContext: CoroutineContext = context.coroutineContext, -) : AbstractPort(context, coroutineContext) { - - override fun toString(): String = "port[${comPort.descriptivePortName}]" - - private val serialPortListener = object : SerialPortDataListener { - override fun getListeningEvents(): Int = SerialPort.LISTENING_EVENT_DATA_AVAILABLE - - override fun serialEvent(event: SerialPortEvent) { - if (event.eventType == SerialPort.LISTENING_EVENT_DATA_AVAILABLE && event.receivedData != null) { - scope.launch { receive(event.receivedData) } - } - } - } - - init { - comPort.addDataListener(serialPortListener) - } - - override suspend fun write(data: ByteArray) { - comPort.writeBytes(data, data.size) - } - - override fun close() { - comPort.removeDataListener() - if (comPort.isOpen) { - comPort.closePort() - } - super.close() - } - - public companion object : PortFactory { - - override val type: String = "com" - - - /** - * Construct ComPort with given parameters - */ - public fun open( - context: Context, - portName: String, - baudRate: Int = 9600, - dataBits: Int = 8, - stopBits: Int = SerialPort.ONE_STOP_BIT, - parity: Int = SerialPort.NO_PARITY, - coroutineContext: CoroutineContext = context.coroutineContext, - ): JSerialCommPort { - val serialPort = SerialPort.getCommPort(portName).apply { - setComPortParameters(baudRate, dataBits, stopBits, parity) - openPort() - } - return JSerialCommPort(context, serialPort, coroutineContext) - } - - override fun build(context: Context, meta: Meta): Port { - val name by meta.string { error("Serial port name not defined") } - val baudRate by meta.int(9600) - val dataBits by meta.int(8) - val stopBits by meta.int(SerialPort.ONE_STOP_BIT) - val parity by meta.int(SerialPort.NO_PARITY) - return open(context, name, baudRate, dataBits, stopBits, parity) - } - } - -} \ No newline at end of file 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 ae9d4fc..f0d099f 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 @@ -1,19 +1,27 @@ package space.kscience.controls.serial -import space.kscience.controls.ports.PortFactory +import space.kscience.controls.ports.Ports import space.kscience.dataforge.context.AbstractPlugin import space.kscience.dataforge.context.Context 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 space.kscience.dataforge.names.asName public class SerialPortPlugin : AbstractPlugin() { override val tag: PluginTag get() = Companion.tag - override fun content(target: String): Map = when(target){ - PortFactory.TYPE -> mapOf(Name.EMPTY to JSerialCommPort) + override fun content(target: String): Map = when (target) { + Ports.ASYNCHRONOUS_PORT_TYPE -> mapOf( + "serial".asName() to AsynchronousSerialPort, + ) + + Ports.SYNCHRONOUS_PORT_TYPE -> mapOf( + "serial".asName() to SynchronousSerialPort, + ) + else -> emptyMap() } diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt b/controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt new file mode 100644 index 0000000..a67b3f3 --- /dev/null +++ b/controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt @@ -0,0 +1,127 @@ +package space.kscience.controls.serial + +import com.fazecast.jSerialComm.SerialPort +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import space.kscience.controls.ports.SynchronousPort +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.context.error +import space.kscience.dataforge.context.logger +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.int +import space.kscience.dataforge.meta.string + +/** + * A port based on JSerialComm + */ +public class SynchronousSerialPort( + override val context: Context, + public val meta: Meta, + private val comPort: SerialPort, +) : SynchronousPort { + + override fun toString(): String = "port[${comPort.descriptivePortName}]" + + + override fun open() { + if (!isOpen) { + comPort.openPort() + } + } + + override val isOpen: Boolean get() = comPort.isOpen + + + override fun close() { + if (comPort.isOpen) { + comPort.closePort() + } + } + + private val mutex = Mutex() + + override suspend fun respond(request: ByteArray, transform: suspend Flow.() -> R): R = + mutex.withLock { + comPort.flushIOBuffers() + comPort.writeBytes(request, request.size) + flow { + while (isOpen) { + try { + val available = comPort.bytesAvailable() + if (available > 0) { + val buffer = ByteArray(available) + comPort.readBytes(buffer, available) + emit(buffer) + } else if (available < 0) break + } catch (ex: Exception) { + logger.error(ex) { "Channel read error" } + delay(1000) + } + } + }.transform() + } + + public companion object : Factory { + + public fun build( + context: Context, + portName: String, + baudRate: Int = 9600, + dataBits: Int = 8, + stopBits: Int = SerialPort.ONE_STOP_BIT, + parity: Int = SerialPort.NO_PARITY, + additionalConfig: SerialPort.() -> Unit = {}, + ): SynchronousSerialPort { + val serialPort = SerialPort.getCommPort(portName).apply { + setComPortParameters(baudRate, dataBits, stopBits, parity) + additionalConfig() + } + val meta = Meta { + "name" put "com://$portName" + "type" put "serial" + "baudRate" put serialPort.baudRate + "dataBits" put serialPort.numDataBits + "stopBits" put serialPort.numStopBits + "parity" put serialPort.parity + } + return SynchronousSerialPort(context, meta, serialPort) + } + + + /** + * Construct ComPort with given parameters + */ + public fun open( + context: Context, + portName: String, + baudRate: Int = 9600, + dataBits: Int = 8, + stopBits: Int = SerialPort.ONE_STOP_BIT, + parity: Int = SerialPort.NO_PARITY, + additionalConfig: SerialPort.() -> Unit = {}, + ): SynchronousSerialPort = build( + context = context, + portName = portName, + baudRate = baudRate, + dataBits = dataBits, + stopBits = stopBits, + parity = parity, + additionalConfig = additionalConfig + ).apply { open() } + + + override fun build(context: Context, meta: Meta): SynchronousPort { + val name by meta.string { error("Serial port name not defined") } + val baudRate by meta.int(9600) + val dataBits by meta.int(8) + val stopBits by meta.int(SerialPort.ONE_STOP_BIT) + val parity by meta.int(SerialPort.NO_PARITY) + return build(context, name, baudRate, dataBits, stopBits, parity) + } + } + +} \ No newline at end of file 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 1d48553..b0fd562 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 @@ -4,7 +4,6 @@ import kotlinx.coroutines.withTimeoutOrNull import space.kscience.controls.ports.Ports import space.kscience.controls.ports.SynchronousPort import space.kscience.controls.ports.respondStringWithDelimiter -import space.kscience.controls.ports.synchronous import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory @@ -22,7 +21,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec = KtorTcpPort, ) : DeviceBySpec(PiMotionMasterDevice, context), DeviceHub { - private var port: Port? = null + private var port: AsynchronousPort? = null //TODO make proxy work //PortProxy { portFactory(address ?: error("The device is not connected"), context) } @@ -83,7 +83,7 @@ class PiMotionMasterDevice( suspend fun getErrorCode(): Int = mutex.withLock { withTimeout(timeoutValue) { sendCommandInternal("ERR?") - val errorString = port?.receiving()?.withStringDelimiter("\n")?.first() ?: error("Not connected to device") + val errorString = port?.subscribe()?.withStringDelimiter("\n")?.first() ?: error("Not connected to device") errorString.trim().toInt() } } @@ -96,7 +96,7 @@ class PiMotionMasterDevice( try { withTimeout(timeoutValue) { sendCommandInternal(command, *arguments) - val phrases = port?.receiving()?.withStringDelimiter("\n") ?: error("Not connected to device") + val phrases = port?.subscribe()?.withStringDelimiter("\n") ?: error("Not connected to device") phrases.transformWhile { line -> emit(line) line.endsWith(" \n") diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt index 8efe4e9..f343d7c 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt @@ -5,14 +5,15 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import space.kscience.controls.api.Socket -import space.kscience.controls.ports.AbstractPort +import space.kscience.controls.api.AsynchronousSocket +import space.kscience.controls.ports.AbstractAsynchronousPort import space.kscience.controls.ports.withDelimiter import space.kscience.dataforge.context.* +import space.kscience.dataforge.meta.Meta import kotlin.math.abs import kotlin.time.Duration -abstract class VirtualDevice(val scope: CoroutineScope) : Socket { +abstract class VirtualDevice(val scope: CoroutineScope) : AsynchronousSocket { protected abstract suspend fun evaluateRequest(request: ByteArray) @@ -40,33 +41,42 @@ abstract class VirtualDevice(val scope: CoroutineScope) : Socket { toRespond.send(response) } - override fun receiving(): Flow = toRespond.receiveAsFlow() + override fun subscribe(): Flow = toRespond.receiveAsFlow() protected fun respondInFuture(delay: Duration, response: suspend () -> ByteArray): Job = scope.launch { delay(delay) respond(response()) } - override fun isOpen(): Boolean = scope.isActive + override val isOpen: Boolean + get() = scope.isActive override fun close() = scope.cancel() } -class VirtualPort(private val device: VirtualDevice, context: Context) : AbstractPort(context) { +class VirtualPort(private val device: VirtualDevice, context: Context) : AbstractAsynchronousPort(context, Meta.EMPTY) { - private val respondJob = device.receiving().onEach { - receive(it) - }.catch { - it.printStackTrace() - }.launchIn(scope) + private var respondJob: Job? = null + + override fun onOpen() { + respondJob = device.subscribe().onEach { + receive(it) + }.catch { + it.printStackTrace() + }.launchIn(scope) + + } override suspend fun write(data: ByteArray) { device.send(data) } + override val isOpen: Boolean + get() = respondJob?.isActive == true + override fun close() { - respondJob.cancel() + respondJob?.cancel() super.close() } } @@ -78,7 +88,7 @@ class PiMotionMasterVirtualDevice( scope: CoroutineScope = context, ) : VirtualDevice(scope), ContextAware { - init { + override fun open() { //add asynchronous send logic here } @@ -102,9 +112,11 @@ class PiMotionMasterVirtualDevice( abs(distance) < proposedStep -> { position = targetPosition } + targetPosition > position -> { position += proposedStep } + else -> { position -= proposedStep } @@ -180,8 +192,10 @@ class PiMotionMasterVirtualDevice( when (command) { "XXX" -> { } + "IDN?", "*IDN?" -> respond("(c)2015 Physik Instrumente(PI) Karlsruhe, C-885.M1 TCP-IP Master,0,1.0.0.1") - "VER?" -> respond(""" + "VER?" -> respond( + """ 2: (c)2017 Physik Instrumente (PI) GmbH & Co. KG, C-663.12C885, 018550039, 00.039 3: (c)2017 Physik Instrumente (PI) GmbH & Co. KG, C-663.12C885, 018550040, 00.039 4: (c)2017 Physik Instrumente (PI) GmbH & Co. KG, C-663.12C885, 018550041, 00.039 @@ -195,8 +209,11 @@ class PiMotionMasterVirtualDevice( 12: (c)2017 Physik Instrumente (PI) GmbH & Co. KG, C-663.12C885, 018550049, 00.039 13: (c)2017 Physik Instrumente (PI) GmbH & Co. KG, C-663.12C885, 018550051, 00.039 FW_ARM: V1.0.0.1 - """.trimIndent()) - "HLP?" -> respond(""" + """.trimIndent() + ) + + "HLP?" -> respond( + """ The following commands are valid: #4 Request Status Register #5 Request Motion Status @@ -235,11 +252,14 @@ class PiMotionMasterVirtualDevice( VEL? [{}] Get Closed-Loop Velocity VER? Get Versions Of Firmware And Drivers end of help - """.trimIndent()) + """.trimIndent() + ) + "ERR?" -> { respond(errorCode.toString()) errorCode = 0 } + "SAI?" -> respond(axisState.keys.joinToString(separator = " \n")) "CST?" -> respondForAllAxis(axisIds) { "L-220.20SG" } "RON?" -> respondForAllAxis(axisIds) { referenceMode } @@ -255,15 +275,19 @@ class PiMotionMasterVirtualDevice( "SVO" -> doForEachAxis(parts) { key, value -> axisState[key]?.servoMode = value.toInt() } + "MOV" -> doForEachAxis(parts) { key, value -> axisState[key]?.targetPosition = value.toDouble() } + "VEL" -> doForEachAxis(parts) { key, value -> axisState[key]?.velocity = value.toDouble() } + "INI" -> { logger.info { "Axes initialized!" } } + else -> { logger.warn { "Unknown command: $command in message ${String(request)}" } errorCode = 2 diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt index 021dff5..c69b7fe 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt @@ -29,7 +29,7 @@ fun Context.launchPiDebugServer(port: Int, axes: List): Job = launch(exc val output = socket.openWriteChannel() val sendJob = launch { - virtualDevice.receiving().collect { + virtualDevice.subscribe().collect { //println("Sending: ${it.decodeToString()}") output.writeAvailable(it) output.flush() From 58675f72f598aac1fbcdfbadfad0c61d286c5947 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 31 Mar 2024 16:33:22 +0300 Subject: [PATCH 69/71] Refactor ports --- .../controls/ports/SynchronousPort.kt | 19 ++++++++ .../kscience/controls/pi/SynchronousPiPort.kt | 9 ++++ .../controls/serial/SynchronousSerialPort.kt | 47 ++++++++++++------- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt index 8d95eb0..49c2b06 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt @@ -2,8 +2,11 @@ package space.kscience.controls.ports import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.io.Buffer +import kotlinx.io.readByteArray import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.ContextAware @@ -25,6 +28,22 @@ public interface SynchronousPort : ContextAware, AutoCloseable { request: ByteArray, transform: suspend Flow.() -> R, ): R + + /** + * Synchronously read fixed size response to a given [request]. Discard additional response bytes. + */ + public suspend fun respondFixedMessageSize( + request: ByteArray, + responseSize: Int, + ): ByteArray = respond(request) { + val buffer = Buffer() + takeWhile { + buffer.size < responseSize + }.collect { + buffer.write(it) + } + buffer.readByteArray(responseSize) + } } private class SynchronousOverAsynchronousPort( diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt b/controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt index acefb1f..6bc590c 100644 --- a/controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt +++ b/controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt @@ -7,6 +7,7 @@ import com.pi4j.ktx.io.serial import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import space.kscience.controls.ports.SynchronousPort @@ -55,6 +56,14 @@ public class SynchronousPiPort( }.transform() } + override suspend fun respondFixedMessageSize(request: ByteArray, responseSize: Int): ByteArray = mutex.withLock { + runInterruptible { + serial.drain() + serial.write(request) + serial.readNBytes(responseSize) + } + } + override fun close() { serial.close() } diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt b/controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt index a67b3f3..1a9b4a5 100644 --- a/controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt +++ b/controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt @@ -4,6 +4,7 @@ import com.fazecast.jSerialComm.SerialPort import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import space.kscience.controls.ports.SynchronousPort @@ -44,26 +45,38 @@ public class SynchronousSerialPort( private val mutex = Mutex() - override suspend fun respond(request: ByteArray, transform: suspend Flow.() -> R): R = - mutex.withLock { + override suspend fun respond( + request: ByteArray, + transform: suspend Flow.() -> R, + ): R = mutex.withLock { + comPort.flushIOBuffers() + comPort.writeBytes(request, request.size) + flow { + while (isOpen) { + try { + val available = comPort.bytesAvailable() + if (available > 0) { + val buffer = ByteArray(available) + comPort.readBytes(buffer, available) + emit(buffer) + } else if (available < 0) break + } catch (ex: Exception) { + logger.error(ex) { "Channel read error" } + delay(1000) + } + } + }.transform() + } + + override suspend fun respondFixedMessageSize(request: ByteArray, responseSize: Int): ByteArray = mutex.withLock { + runInterruptible { comPort.flushIOBuffers() comPort.writeBytes(request, request.size) - flow { - while (isOpen) { - try { - val available = comPort.bytesAvailable() - if (available > 0) { - val buffer = ByteArray(available) - comPort.readBytes(buffer, available) - emit(buffer) - } else if (available < 0) break - } catch (ex: Exception) { - logger.error(ex) { "Channel read error" } - delay(1000) - } - } - }.transform() + val buffer = ByteArray(responseSize) + comPort.readBytes(buffer, responseSize) + buffer } + } public companion object : Factory { From 977500223d206eb878b52f0ed149a2faeca96433 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 7 Apr 2024 10:07:23 +0300 Subject: [PATCH 70/71] Plc4X refactor --- controls-plc4x/build.gradle.kts | 6 +- .../kscience/controls/plc4x/Plc4XDevice.kt | 80 +++++++++++++++---- .../controls/plc4x/Plc4XDeviceBase.kt | 22 +++++ .../kscience/controls/plc4x/Plc4xProperty.kt | 14 +++- demo/constructor/src/jvmMain/kotlin/main.kt | 10 +-- 5 files changed, 105 insertions(+), 27 deletions(-) create mode 100644 controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDeviceBase.kt diff --git a/controls-plc4x/build.gradle.kts b/controls-plc4x/build.gradle.kts index 0836476..e7b063c 100644 --- a/controls-plc4x/build.gradle.kts +++ b/controls-plc4x/build.gradle.kts @@ -11,14 +11,14 @@ description = """ A plugin for Controls-kt device server on top of plc4x library """.trimIndent() -kscience{ +kscience { jvm() - jvmMain{ + jvmMain { api(projects.controlsCore) api("org.apache.plc4x:plc4j-spi:$plc4xVersion") } } -readme{ +readme { maturity = Maturity.EXPERIMENTAL } \ No newline at end of file diff --git a/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt index 996d194..9b08538 100644 --- a/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt +++ b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt @@ -2,27 +2,75 @@ package space.kscience.controls.plc4x import kotlinx.coroutines.future.await import org.apache.plc4x.java.api.PlcConnection +import org.apache.plc4x.java.api.messages.PlcBrowseItem +import org.apache.plc4x.java.api.messages.PlcTagResponse import org.apache.plc4x.java.api.messages.PlcWriteRequest +import org.apache.plc4x.java.api.messages.PlcWriteResponse +import org.apache.plc4x.java.api.types.PlcResponseCode import space.kscience.controls.api.Device import space.kscience.dataforge.meta.Meta -public interface Plc4XDevice: Device { - public val connection: PlcConnection +private val PlcTagResponse.responseCodes: Map + get() = tagNames.associateWith { getResponseCode(it) } - public suspend fun read(plc4xProperty: Plc4xProperty): Meta = with(plc4xProperty){ - val request = connection.readRequestBuilder().request().build() - val response = request.execute().await() - response.readProperty() - } - - public suspend fun write(plc4xProperty: Plc4xProperty, value: Meta): Unit = with(plc4xProperty){ - val request: PlcWriteRequest = connection.writeRequestBuilder().writeProperty(value).build() - request.execute().await() - } - - public suspend fun subscribe(propertyName: String, plc4xProperty: Plc4xProperty): Unit = with(plc4xProperty){ - - } +private val Map.isOK get() = values.all { it == PlcResponseCode.OK } +public class PlcException(public val codes: Map) : Exception() { + override val message: String + get() = "Plc request unsuccessful:" + codes.entries.joinToString(prefix = "\n\t", separator = "\n\t") { + "${it.key}: ${it.value.name}" + } } +private fun PlcTagResponse.throwOnFail() { + val codes = responseCodes + if (!codes.isOK) throw PlcException(codes) +} + + +public interface Plc4XDevice : Device { + public val connection: PlcConnection +} + + +/** + * Send ping request and suspend until it comes back + */ +public suspend fun Plc4XDevice.ping(): PlcResponseCode = connection.ping().await().responseCode + +/** + * Send browse request to list available tags + */ +public suspend fun Plc4XDevice.browse(): Map> { + require(connection.metadata.isBrowseSupported){"Browse actions are not supported on connection"} + val request = connection.browseRequestBuilder().build() + val response = request.execute().await() + + return response.queryNames.associateWith { response.getValues(it) } +} + +/** + * Send read request and suspend until it returns. Throw a [PlcException] if at least one tag read fails. + * + * @throws PlcException + */ +public suspend fun Plc4XDevice.read(plc4xProperty: Plc4xProperty): Meta = with(plc4xProperty) { + require(connection.metadata.isReadSupported) {"Read actions are not supported on connections"} + val request = connection.readRequestBuilder().request().build() + val response = request.execute().await() + response.throwOnFail() + response.readProperty() +} + + +/** + * Send write request and suspend until it finishes. Throw a [PlcException] if at least one tag write fails. + * + * @throws PlcException + */ +public suspend fun Plc4XDevice.write(plc4xProperty: Plc4xProperty, value: Meta): Unit = with(plc4xProperty) { + require(connection.metadata.isWriteSupported){"Write actions are not supported on connection"} + val request: PlcWriteRequest = connection.writeRequestBuilder().writeProperty(value).build() + val response: PlcWriteResponse = request.execute().await() + response.throwOnFail() +} \ No newline at end of file diff --git a/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDeviceBase.kt b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDeviceBase.kt new file mode 100644 index 0000000..e25a001 --- /dev/null +++ b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDeviceBase.kt @@ -0,0 +1,22 @@ +package space.kscience.controls.plc4x + +import org.apache.plc4x.java.api.PlcConnection +import space.kscience.controls.spec.DeviceActionSpec +import space.kscience.controls.spec.DeviceBase +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.Meta + +public class Plc4XDeviceBase( + context: Context, + meta: Meta, + override val connection: PlcConnection, +) : Plc4XDevice, DeviceBase(context, meta) { + override val properties: Map> + get() = TODO("Not yet implemented") + override val actions: Map> = emptyMap() + + override fun toString(): String { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt index 5b16dec..cfeedd2 100644 --- a/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt +++ b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt @@ -8,6 +8,8 @@ import space.kscience.dataforge.meta.Meta public interface Plc4xProperty { + public val keys: Set + public fun PlcReadRequest.Builder.request(): PlcReadRequest.Builder public fun PlcReadResponse.readProperty(): Meta @@ -15,17 +17,23 @@ public interface Plc4xProperty { public fun PlcWriteRequest.Builder.writeProperty(meta: Meta): PlcWriteRequest.Builder } -public class DefaultPlc4xProperty( +private class DefaultPlc4xProperty( private val address: String, private val plcValueType: PlcValueType, private val name: String = "@default", ) : Plc4xProperty { + override val keys: Set = setOf(name) + override fun PlcReadRequest.Builder.request(): PlcReadRequest.Builder = addTagAddress(name, address) + override fun PlcReadResponse.readProperty(): Meta = - asPlcValue.toMeta() + getPlcValue(name).toMeta() override fun PlcWriteRequest.Builder.writeProperty(meta: Meta): PlcWriteRequest.Builder = addTagAddress(name, address, meta.toPlcValue(plcValueType)) -} \ No newline at end of file +} + +public fun Plc4xProperty(address: String, plcValueType: PlcValueType, name: String = "@default"): Plc4xProperty = + DefaultPlc4xProperty(address, plcValueType, name) \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 26ca10a..9b06d2e 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -84,24 +84,24 @@ private fun Context.launchPidDevice( showDashboard { plot { - plotNumberState(context, state, maxAge = maxAge) { + plotNumberState(context, state, maxAge = maxAge, sampling = 50.milliseconds) { name = "real position" } - plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge) { + plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge, sampling = 50.milliseconds) { name = "read position" } - plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge) { + plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge, sampling = 50.milliseconds) { name = "target" } } plot { - plotDeviceProperty(device.start, LimitSwitch.locked.name, maxAge = maxAge) { + plotDeviceProperty(device.start, LimitSwitch.locked.name, maxAge = maxAge, sampling = 50.milliseconds) { name = "start measured" mode = ScatterMode.markers } - plotDeviceProperty(device.end, LimitSwitch.locked.name, maxAge = maxAge) { + plotDeviceProperty(device.end, LimitSwitch.locked.name, maxAge = maxAge, sampling = 50.milliseconds) { name = "end measured" mode = ScatterMode.markers } From 9eb583dfc6a832f6e855bd26d52d2ed930ccc4de Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 9 Apr 2024 15:13:41 +0300 Subject: [PATCH 71/71] Change plotAveragedDeviceProperty to show last value on miss. --- .../src/commonMain/kotlin/plotExtensions.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index 804fe9d..6dfdee0 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -205,14 +205,14 @@ private fun List.averageTime(): Instant { } /** - * Average property value by [averagingInterval]. Return [missingValue] on each sample interval if no events arrived. + * Average property value by [averagingInterval]. Return [startValue] on each sample interval if no events arrived. */ @DFExperimental public fun Plot.plotAveragedDeviceProperty( device: Device, propertyName: String, - missingValue: Double = 0.0, - extractValue: Meta.() -> Double = { value?.double ?: missingValue }, + startValue: Double = 0.0, + extractValue: Meta.() -> Double = { value?.double ?: startValue }, maxAge: Duration = defaultMaxAge, maxPoints: Int = defaultMaxPoints, minPoints: Int = defaultMinPoints, @@ -221,13 +221,15 @@ public fun Plot.plotAveragedDeviceProperty( configuration: Scatter.() -> Unit = {}, ): Job = scatter(configuration).run { val data = TimeData() + var lastValue = startValue device.propertyMessageFlow(propertyName).chunkedByPeriod(averagingInterval).transform { eventList -> if (eventList.isEmpty()) { - data.append(Clock.System.now(), missingValue.asValue()) + data.append(Clock.System.now(), lastValue.asValue()) } else { val time = eventList.map { it.time }.averageTime() val value = eventList.map { extractValue(it.value) }.average() data.append(time, value.asValue()) + lastValue = value } data.trim(maxAge, maxPoints, minPoints) emit(data)