From 4c93b5c9b3feda0321fe95d54266821b6df7b0c7 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Wed, 23 Aug 2023 16:37:35 +0300 Subject: [PATCH 001/125] 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 @@ [](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) -# Controls.kt +[](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 <altavir@gmail.com> Date: Thu, 24 Aug 2023 16:25:17 +0300 Subject: [PATCH 002/125] 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 <altavir@gmail.com> Date: Mon, 4 Sep 2023 14:56:18 +0300 Subject: [PATCH 003/125] 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 <altavir@gmail.com> Date: Sat, 16 Sep 2023 15:54:36 +0300 Subject: [PATCH 004/125] 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 <altavir@gmail.com> Date: Sat, 16 Sep 2023 16:09:47 +0300 Subject: [PATCH 005/125] 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 <altavir@gmail.com> Date: Mon, 18 Sep 2023 09:00:04 +0300 Subject: [PATCH 006/125] 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<Name, Any> = 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<PiPlugin> { 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>() ?: 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 @@ [](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) +[](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 <altavir@gmail.com> Date: Mon, 18 Sep 2023 13:38:45 +0300 Subject: [PATCH 007/125] 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 <altavir@gmail.com> Date: Sun, 24 Sep 2023 13:02:52 +0300 Subject: [PATCH 008/125] 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<PropertyChangedMessage>().onEach(callback).launchIn(this) +public fun Device.onPropertyChange(scope: CoroutineScope = this, callback: suspend PropertyChangedMessage.() -> Unit): Job = + messageFlow.filterIsInstance<PropertyChangedMessage>().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 : Device, T> D.propertyFlow(spec: DevicePropertySpec<D, T>): Flow< */ public fun <D : Device, T> D.onPropertyChange( spec: DevicePropertySpec<D, T>, + scope: CoroutineScope = this, callback: suspend PropertyChangedMessage.(T) -> Unit, ): Job = messageFlow .filterIsInstance<PropertyChangedMessage>() @@ -124,15 +125,16 @@ public fun <D : Device, T> 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 : Device, T> D.useProperty( spec: DevicePropertySpec<D, T>, + scope: CoroutineScope = this, callback: suspend (T) -> Unit, -): Job = launch { +): Job = scope.launch { callback(read(spec)) messageFlow .filterIsInstance<PropertyChangedMessage>() 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<ModbusDevice, Short> = object : ReadWriteProperty<ModbusDevice, Short> { From a337daee9319ab2de541dbe9caa0f9ba0e64be76 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 24 Sep 2023 13:21:01 +0300 Subject: [PATCH 009/125] 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<D : Device>( /** * 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<D : Device>( } /** - * Update logical state using given [spec] and its convertor + * Notify the device that a property with [spec] value is changed */ - public suspend fun <T> updateLogical(spec: DevicePropertySpec<D, T>, value: T) { - updateLogical(spec.name, spec.converter.objectToMeta(value)) + protected suspend fun <T> propertyChanged(spec: DevicePropertySpec<D, T>, value: T) { + propertyChanged(spec.name, spec.converter.objectToMeta(value)) } /** @@ -112,7 +112,7 @@ public abstract class DeviceBase<D : Device>( 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<D : Device>( 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<D : Device>( 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<Unit> get() = UnitMetaCon @OptIn(InternalDeviceAPI::class) public abstract class DeviceSpec<D : Device> { - //initializing meta property for everyone + //initializing the metadata property for everyone private val _properties = hashMapOf<String, DevicePropertySpec<D, *>>( 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<MksPdr900Devi if (powerOnValue) { val ans = talk("FP!ON") if (ans == "ON") { - updateLogical(powerOn, true) + propertyChanged(powerOn, true) } else { - updateLogical(error, "Failed to set power state") + propertyChanged(error, "Failed to set power state") } } else { val ans = talk("FP!OFF") if (ans == "OFF") { - updateLogical(powerOn, false) + propertyChanged(powerOn, false) } else { - updateLogical(error, "Failed to set power state") + propertyChanged(error, "Failed to set power state") } } } @@ -68,13 +68,13 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi invalidate(error) return if (answer.isNullOrEmpty()) { // updateState(PortSensor.CONNECTED_STATE, false) - updateLogical(error, "No connection") + propertyChanged(error, "No connection") null } else { val res = answer.toDouble() if (res <= 0) { - updateLogical(powerOn, false) - updateLogical(error, "No power") + propertyChanged(powerOn, false) + propertyChanged(error, "No power") null } else { res 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 4eff6c4..54573e4 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 @@ -172,7 +172,7 @@ class PiMotionMasterDevice( //Update port //address = portSpec.node port = portFactory(portSpec, context) - updateLogical(connected, true) + propertyChanged(connected, true) // connector.open() //Initialize axes val idn = read(identity) @@ -196,7 +196,7 @@ class PiMotionMasterDevice( it.close() } port = null - updateLogical(connected, false) + propertyChanged(connected, false) } From 34e7dd2c6d12b1c82a714dd3d3d5eedbe8d206d5 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 24 Sep 2023 13:29:15 +0300 Subject: [PATCH 010/125] 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<D : Device>( 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 <altavir@gmail.com> Date: Mon, 2 Oct 2023 21:24:01 +0300 Subject: [PATCH 011/125] 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 <D : Device, T> WritableDevicePropertySpec<D, T>.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 <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? = read(device)?.let(converter::objectToMeta) @@ -135,9 +141,14 @@ public abstract class DeviceBase<D : Device>( } 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<D : Device>( //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<D : Device> internal constructor( } } + /** + * Trigger [block] if one of register changes. + */ + private fun List<ObservableRegister>.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 <T> bind(key: ModbusRegistryKey.HoldingRange<T>, propertySpec: WritableDevicePropertySpec<D, T>) { 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<D : Device> 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<D : Device> internal constructor( * Bind the device to Modbus slave (server) image. */ public fun <D : Device> D.bindProcessImage( + unitId: Int = 0, openOnBind: Boolean = true, binding: DeviceProcessImageBuilder<D>.() -> 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 <T> ModbusRegistryKey.HoldingRange<T>.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 <altavir@gmail.com> Date: Mon, 2 Oct 2023 22:12:11 +0300 Subject: [PATCH 012/125] 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<D : Device>( 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 <altavir@gmail.com> Date: Thu, 5 Oct 2023 07:43:49 +0300 Subject: [PATCH 013/125] 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<InputRegister> = - master.readInputRegisters(clientId, address, count).toList() + master.readInputRegisters(unitId, address, count).toList() private fun Array<out InputRegister>.toBuffer(): ByteBuffer { val buffer: ByteBuffer = ByteBuffer.allocate(size * 2) @@ -129,10 +129,10 @@ private fun Array<out InputRegister>.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<Register> = - 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<Reg * @param count number of 2-bytes registers to read. Buffer size is 2*[count] */ public fun ModbusDevice.readHoldingRegistersToBuffer(address: Int, count: Int): ByteBuffer = - master.readMultipleRegisters(clientId, address, count).toBuffer() + master.readMultipleRegisters(unitId, address, count).toBuffer() public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): ByteReadPacket = - master.readMultipleRegisters(clientId, address, count).toPacket() + master.readMultipleRegisters(unitId, address, count).toPacket() public fun ModbusDevice.readDoubleRegister(address: Int): Double = readHoldingRegistersToBuffer(address, Double.SIZE_BYTES).getDouble() @@ -162,14 +162,14 @@ public fun ModbusDevice.readHoldingRegister(address: Int): Short = public fun ModbusDevice.writeHoldingRegisters(address: Int, values: ShortArray): Int = master.writeMultipleRegisters( - clientId, + unitId, address, Array<Register>(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<D: Device>( context: Context, spec: DeviceSpec<D>, - 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 <altavir@gmail.com> Date: Sat, 7 Oct 2023 18:34:44 +0300 Subject: [PATCH 014/125] 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 <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta */ public abstract class DeviceBase<D : Device>( 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<D : Device>( */ private val logicalState: HashMap<String, Meta?> = HashMap() - private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow() + private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow( + replay = meta["message.buffer"].int ?: 1000, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) public override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow From 290010fc8c2b71f789f8aa990080b8502b7325d8 Mon Sep 17 00:00:00 2001 From: darksnake <altavir@gmail.com> Date: Thu, 19 Oct 2023 16:38:50 +0300 Subject: [PATCH 015/125] 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<Unit>{ +public object UnitMetaConverter : MetaConverter<Unit> { override fun metaToObject(meta: Meta): Unit = Unit override fun objectToMeta(obj: Unit): Meta = Meta.EMPTY @@ -127,7 +127,8 @@ public abstract class DeviceSpec<D : Device> { PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> -> val propertyName = name ?: property.name val deviceProperty = object : WritableDevicePropertySpec<D, T> { - override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) + override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, writable = true) + .apply(descriptorBuilder) override val converter: MetaConverter<T> = 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 <altavir@gmail.com> Date: Fri, 20 Oct 2023 10:14:14 +0300 Subject: [PATCH 016/125] 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<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray> 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<T>( 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<T>( 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<ModbusRegistryKey, String> = mutableMapOf<ModbusRegistryKey, String>() @@ -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 <T> input( address: Int, count: Int, reader: IOFormat<T>, description: String = "", - ): ModbusRegistryKey.InputRange<T> = - register(ModbusRegistryKey.InputRange(address, count, reader), description) + ): ModbusRegistryKey.InputRange<T> = 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 <T> register( address: Int, count: Int, format: IOFormat<T>, description: String = "", - ): ModbusRegistryKey.HoldingRange<T> = - register(ModbusRegistryKey.HoldingRange(address, count, format), description) + ): ModbusRegistryKey.HoldingRange<T> = 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<Map.Entry<ModbusRegistryKey, String>?> { 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<Map.Entry<ModbusRegistryKey, String>?> { 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 <altavir@gmail.com> Date: Wed, 25 Oct 2023 22:31:36 +0300 Subject: [PATCH 017/125] 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<Drive>() { + public val target: DevicePropertySpec<Drive, Double> by property(MetaConverter.double, Drive::target) + + public val position: DevicePropertySpec<Drive, Double> by doubleProperty { position } + } +} + +/** + * Virtual [Drive] with speed limit + */ +public class VirtualDrive( + context: Context, + value: Double, + private val speed: Double, +) : DeviceBySpec<Drive>(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<LimitSwitch>() { + public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked } + } +} + +/** + * Virtual [LimitSwitch] + */ +public class VirtualLimitSwitch( + context: Context, + private val lockedFunction: () -> Boolean, +) : DeviceBySpec<LimitSwitch>(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<PidRegulator>() { + 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>(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<PidRegulator>.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<Device> { + 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 <D : Device> 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<D : Device>( override val actionDescriptors: Collection<ActionDescriptor> get() = actions.values.map { it.descriptor } + + private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = 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<D : Device>( */ private val logicalState: HashMap<String, Meta?> = HashMap() - private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow( - replay = meta["message.buffer"].int ?: 1000, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - public override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow @Suppress("UNCHECKED_CAST") @@ -180,18 +192,30 @@ public abstract class DeviceBase<D : Device>( 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<D : Device>( override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties override val actions: Map<String, DeviceActionSpec<D, *, *>> 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<in D : Device, T> { +public interface DevicePropertySpec<in D, T> { /** * Property descriptor */ @@ -53,7 +53,7 @@ public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySp } -public interface DeviceActionSpec<in D : Device, I, O> { +public interface DeviceActionSpec<in D, I, O> { /** * 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<D : Device> { val deviceProperty = object : DevicePropertySpec<D, T> { override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { //TODO add type from converter - writable = true + mutable = true }.apply(descriptorBuilder) override val converter: MetaConverter<T> = converter @@ -78,7 +78,7 @@ public abstract class DeviceSpec<D : Device> { override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { //TODO add the type from converter - writable = true + mutable = true }.apply(descriptorBuilder) override val converter: MetaConverter<T> = converter @@ -127,7 +127,7 @@ public abstract class DeviceSpec<D : Device> { PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> -> val propertyName = name ?: property.name val deviceProperty = object : WritableDevicePropertySpec<D, T> { - override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, writable = true) + override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, mutable = true) .apply(descriptorBuilder) override val converter: MetaConverter<T> = converter @@ -224,7 +224,7 @@ public fun <T, D : DeviceBase<D>> DeviceSpec<D>.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<T> = 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<NameToken, Factory<Device>>() + + public fun <D : Device> device(name: String, factory: Factory<Device>) { + childrenFactories[NameToken.parse(name)] = factory + } + } + + override val devices: Map<NameToken, Device> = 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 : Device> 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<D: Device>( private val disposeMasterOnClose: Boolean = true, meta: Meta = Meta.EMPTY, ) : ModbusDevice, DeviceBySpec<D>(spec, context, meta){ - override suspend fun open() { + override suspend fun onStart() { master.connect() - super<DeviceBySpec>.open() } - override fun close() { + override fun onStop() { if(disposeMasterOnClose){ master.disconnect() } - super<ModbusDevice>.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<D : Device>( } } - override fun close() { + override fun onStop() { client.disconnect() - super<DeviceBySpec>.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<IDemoDevice>(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<MagixVirtualCar> { 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<VirtualCar>(I } @OptIn(ExperimentalTime::class) - override suspend fun open() { - super<DeviceBySpec>.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 <altavir@gmail.com> Date: Fri, 27 Oct 2023 10:57:46 +0300 Subject: [PATCH 018/125] 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<PidRegulator>() { - 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>(PidRegulator, context), PidRegulator { - - private val mutex = Mutex() +) : DeviceBySpec<Regulator>(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<PidRegulator>.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<Device> { - 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<PidRegulator>() { +// 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>(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<PidRegulator>.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<Device> { +// 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<Drive>() { - public val target: DevicePropertySpec<Drive, Double> by property(MetaConverter.double, Drive::target) + public companion object : DeviceSpec<Regulator>() { + public val target: DevicePropertySpec<Regulator, Double> by property(MetaConverter.double, Regulator::target) - public val position: DevicePropertySpec<Drive, Double> by doubleProperty { position } + public val position: DevicePropertySpec<Regulator, Double> 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>(Drive, context), Drive { +) : DeviceBySpec<Regulator>(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<PropertyChangedMessage>().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<D : Device>( } @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<D : Device>( @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<D : Device>( @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 <altavir@gmail.com> Date: Sat, 28 Oct 2023 14:18:00 +0300 Subject: [PATCH 019/125] 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<T> { + public val converter: MetaConverter<T> + public val value: T + + public val valueFlow: Flow<T> + + public val metaFlow: Flow<Meta> +} + + +/** + * A mutable state of a device + */ +public interface MutableDeviceState<T> : DeviceState<T>{ + 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<Drive>() { + public val force: DevicePropertySpec<Drive, Double> by Drive.property( + MetaConverter.double, + Drive::force + ) + + public val position: DevicePropertySpec<Drive, Double> by doubleProperty { position } + } +} + +/** + * A virtual drive + */ +public class VirtualDrive( + context: Context, + private val mass: Double, + position: Double, +) : Drive, DeviceBySpec<Drive>(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, regulator.context), Regulator { +) : DeviceBySpec<Regulator>(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<PidRegulator>() { -// 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>(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<PidRegulator>.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<Device> { -// 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<Regulator, Double> by doubleProperty { position } } -} - -/** - * Virtual [Regulator] with speed limit - */ -public class VirtualRegulator( - context: Context, - value: Double, - private val speed: Double, -) : DeviceBySpec<Regulator>(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<DeviceMessage> { - + //TODO could we avoid using downstream scope? val outbox = MutableSharedFlow<DeviceMessage>() 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<D : Device>( @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<D : Device>( @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 <altavir@gmail.com> Date: Mon, 30 Oct 2023 21:35:46 +0300 Subject: [PATCH 020/125] 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<out Any>, + 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<NameToken, Device>() + + override val devices: Map<NameToken, Device> = _devices + + public fun <D : Device> 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<Name, Property> = hashMapOf() + + public fun property(descriptor: PropertyDescriptor, state: DeviceState<out Any>) { + 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<Name, Action> = hashMapOf() + + override val propertyDescriptors: Collection<PropertyDescriptor> + get() = properties.values.map { it.descriptor } + + override val actionDescriptors: Collection<ActionDescriptor> + 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<DeviceMessage>() + + override val messageFlow: Flow<DeviceMessage> + 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 <D : Device> 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 <D: Device> 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<Device>, 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<Device>, + 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 <T : Any> DeviceGroup.property( + name: String, + state: DeviceState<T>, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): DeviceState<T> { + property( + PropertyDescriptor(name).apply(descriptorBuilder), + state + ) + return state +} + +public fun <T : Any> DeviceGroup.mutableProperty( + name: String, + state: MutableDeviceState<T>, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): MutableDeviceState<T> { + property( + PropertyDescriptor(name).apply(descriptorBuilder), + state + ) + return state +} + +public fun <T : Any> DeviceGroup.virtualProperty( + name: String, + initialValue: T, + converter: MetaConverter<T>, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): MutableDeviceState<T> { + val state = VirtualDeviceState<T>(converter, initialValue) + return mutableProperty(name, state, descriptorBuilder) +} + +/** + * Create a virtual [MutableDeviceState], but do not register it to a device + */ +@Suppress("UnusedReceiverParameter") +public fun <T : Any> DeviceGroup.standAloneProperty( + initialValue: T, + converter: MetaConverter<T>, +): MutableDeviceState<T> = VirtualDeviceState<T>(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<T> { public val value: T public val valueFlow: Flow<T> - - public val metaFlow: Flow<Meta> } +public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::objectToMeta) + +public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.objectToMeta(value) + /** * A mutable state of a device */ -public interface MutableDeviceState<T> : DeviceState<T>{ +public interface MutableDeviceState<T> : DeviceState<T> { override var value: T -} \ No newline at end of file +} + +public var <T : Any> MutableDeviceState<T>.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<T>( + override val converter: MetaConverter<T>, + initialValue: T, +) : MutableDeviceState<T> { + private val flow = MutableStateFlow(initialValue) + override val valueFlow: Flow<T> get() = flow + + override var value: T by flow::value +} + +private open class BoundDeviceState<T>( + override val converter: MetaConverter<T>, + val device: Device, + val propertyName: String, + private val initialValue: T, +) : DeviceState<T> { + + override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().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 <T> Device.bindStateToProperty( + propertyName: String, + metaConverter: MetaConverter<T>, +): DeviceState<T> { + val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") + return BoundDeviceState(metaConverter, this, propertyName, initialValue) +} + +public suspend fun <D : Device, T> D.bindStateToProperty( + propertySpec: DevicePropertySpec<D, T>, +): DeviceState<T> = bindStateToProperty(propertySpec.name, propertySpec.converter) + +public fun <T, R> DeviceState<T>.map( + converter: MetaConverter<R>, mapper: (T) -> R, +): DeviceState<R> = object : DeviceState<R> { + override val converter: MetaConverter<R> = converter + override val value: R + get() = mapper(this@map.value) + + override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper) +} + +private class MutableBoundDeviceState<T>( + converter: MetaConverter<T>, + device: Device, + propertyName: String, + initialValue: T, +) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> { + + override var value: T + get() = valueFlow.value + set(newValue) { + device.launch { + device.writeProperty(propertyName, converter.objectToMeta(newValue)) + } + } +} + +public suspend fun <T> Device.bindMutableStateToProperty( + propertyName: String, + metaConverter: MetaConverter<T>, +): MutableDeviceState<T> { + val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") + return MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) +} + +public suspend fun <D : Device, T> D.bindMutableStateToProperty( + propertySpec: MutableDevicePropertySpec<D, T>, +): MutableDeviceState<T> = 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<NameToken, Factory<Device>>() - - public fun <D : Device> device(name: String, factory: Factory<Device>) { - childrenFactories[NameToken.parse(name)] = factory - } - } - - override val devices: Map<NameToken, Device> = 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<Drive>() { - public val force: DevicePropertySpec<Drive, Double> by Drive.property( + public val force: MutableDevicePropertySpec<Drive, Double> 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<Double>, ) : Drive, DeviceBySpec<Drive>(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<Double> = bindMutableStateToProperty(Drive.force) + +public fun DeviceGroup.virtualDrive( + name: String, + mass: Double, + positionState: MutableDeviceState<Double>, +): 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<Boolean>, ) : DeviceBySpec<LimitSwitch>(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<Boolean>): 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>(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<Regulator>() { - public val target: DevicePropertySpec<Regulator, Double> by property(MetaConverter.double, Regulator::target) + public val target: MutableDevicePropertySpec<Regulator, Double> by mutableProperty(MetaConverter.double, Regulator::target) public val position: DevicePropertySpec<Regulator, Double> 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<Double>, +) : MutableDeviceState<Double> { + + init { + require(initialValue in range) { "Initial value should be in range" } + } + + override val converter: MetaConverter<Double> = 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<Double> get() = _valueFlow + + /** + * A state showing that the range is on its lower boundary + */ + public val atStartState: DeviceState<Boolean> = map(MetaConverter.boolean) { it == range.start } + + /** + * A state showing that the range is on its higher boundary + */ + public val atEndState: DeviceState<Boolean> = 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<PropertyChangedMessage>().onEach(callback).launchIn(scope) +): Job = messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(scope) + +/** + * A [Flow] of property change messages for specific property. + */ +public fun Device.propertyMessageFlow(propertyName: String): Flow<PropertyChangedMessage> = messageFlow + .filterIsInstance<PropertyChangedMessage>() + .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<ClockManager> { + 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 <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) { +private suspend fun <D : Device, T> MutableDevicePropertySpec<D, T>.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 <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta public abstract class DeviceBase<D : Device>( 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<D : Device>( 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<D : Device>( } @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<in D, T> { public val DevicePropertySpec<*, *>.name: String get() = descriptor.name -public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> { +public interface MutableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> { /** * Write physical value to a device */ @@ -84,21 +81,20 @@ public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T> public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? = readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject) - -public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? = - getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject) +public suspend fun <T, D : Device> D.request(propertySpec: DevicePropertySpec<D, T>): T? = + propertySpec.converter.metaToObject(requestProperty(propertySpec.name)) /** * Write typed property state and invalidate logical state */ -public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, value: T) { +public suspend fun <T, D : Device> D.write(propertySpec: MutableDevicePropertySpec<D, T>, 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 <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Job = launch { +public fun <T, D : Device> D.writeAsync(propertySpec: MutableDevicePropertySpec<D, T>, value: T): Job = launch { write(propertySpec, value) } @@ -151,7 +147,7 @@ public fun <D : Device, T> D.useProperty( /** * Reset the logical state of a property */ -public suspend fun <D : Device> D.invalidate(propertySpec: DevicePropertySpec<D, *>) { +public suspend fun <D : CachingDevice> D.invalidate(propertySpec: DevicePropertySpec<D, *>) { 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<D : Device> { converter: MetaConverter<T>, readWriteProperty: KMutableProperty1<D, T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> = + ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, MutableDevicePropertySpec<D, T>>> = PropertyDelegateProvider { _, property -> - val deviceProperty = object : WritableDevicePropertySpec<D, T> { + val deviceProperty = object : MutableDevicePropertySpec<D, T> { override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { //TODO add the type from converter @@ -123,10 +123,10 @@ public abstract class DeviceSpec<D : Device> { name: String? = null, read: suspend D.() -> T?, write: suspend D.(T) -> Unit, - ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> = + ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> = PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> -> val propertyName = name ?: property.name - val deviceProperty = object : WritableDevicePropertySpec<D, T> { + val deviceProperty = object : MutableDevicePropertySpec<D, T> { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, mutable = true) .apply(descriptorBuilder) override val converter: MetaConverter<T> = converter @@ -138,7 +138,7 @@ public abstract class DeviceSpec<D : Device> { } } _properties[propertyName] = deviceProperty - ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>> { _, _ -> + ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>> { _, _ -> deviceProperty } } @@ -218,9 +218,9 @@ public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty( converter: MetaConverter<T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, -): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> = +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, MutableDevicePropertySpec<D, T>>> = PropertyDelegateProvider { _, property -> - val deviceProperty = object : WritableDevicePropertySpec<D, T> { + val deviceProperty = object : MutableDevicePropertySpec<D, T> { 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 <D : Device> DeviceSpec<D>.booleanProperty( name: String? = null, read: suspend D.() -> Boolean?, write: suspend D.(Boolean) -> Unit -): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> = +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Boolean>>> = mutableProperty( MetaConverter.boolean, { @@ -117,7 +117,7 @@ public fun <D : Device> DeviceSpec<D>.numberProperty( name: String? = null, read: suspend D.() -> Number, write: suspend D.(Number) -> Unit -): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> = +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Number>>> = mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) public fun <D : Device> DeviceSpec<D>.doubleProperty( @@ -125,7 +125,7 @@ public fun <D : Device> DeviceSpec<D>.doubleProperty( name: String? = null, read: suspend D.() -> Double, write: suspend D.(Double) -> Unit -): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> = +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Double>>> = mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) public fun <D : Device> DeviceSpec<D>.stringProperty( @@ -133,7 +133,7 @@ public fun <D : Device> DeviceSpec<D>.stringProperty( name: String? = null, read: suspend D.() -> String, write: suspend D.(String) -> Unit -): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> = +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, String>>> = mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) public fun <D : Device> DeviceSpec<D>.metaProperty( @@ -141,5 +141,5 @@ public fun <D : Device> DeviceSpec<D>.metaProperty( name: String? = null, read: suspend D.() -> Meta, write: suspend D.(Meta) -> Unit -): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Meta>>> = +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Meta>>> = 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<DeviceMessage>, 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<D : Device> internal constructor( @@ -29,10 +26,10 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor( public fun bind( key: ModbusRegistryKey.Coil, - propertySpec: WritableDevicePropertySpec<D, Boolean>, + propertySpec: MutableDevicePropertySpec<D, Boolean>, ): 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<D : Device> internal constructor( public fun bind( key: ModbusRegistryKey.HoldingRegister, - propertySpec: WritableDevicePropertySpec<D, Short>, + propertySpec: MutableDevicePropertySpec<D, Short>, ): 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<D : Device> internal constructor( /** * Trigger [block] if one of register changes. */ - private fun List<ObservableRegister>.onChange(block: (ByteReadPacket) -> Unit) { + private fun List<ObservableRegister>.onChange(block: suspend (ByteReadPacket) -> Unit) { var ready = false forEach { register -> @@ -147,7 +144,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor( } } - public fun <T> bind(key: ModbusRegistryKey.HoldingRange<T>, propertySpec: WritableDevicePropertySpec<D, T>) { + public fun <T> bind(key: ModbusRegistryKey.HoldingRange<T>, propertySpec: MutableDevicePropertySpec<D, T>) { val registers = List(key.count) { ObservableRegister() } @@ -157,7 +154,7 @@ public class DeviceProcessImageBuilder<D : Device> 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<Value> + 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<out Number>, + 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<VirtualCar>(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<VirtualCar>(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<VirtualCar>(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 @@ +<configuration> + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> + </encoder> + </appender> + + <root level="INFO"> + <appender-ref ref="STDOUT"/> + </root> +</configuration> \ 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<MksPdr900Devi val channel by logicalProperty(MetaConverter.int) val value by doubleProperty(read = { - readChannelData(get(channel) ?: DEFAULT_CHANNEL) + readChannelData(request(channel) ?: DEFAULT_CHANNEL) }) val error by logicalProperty(MetaConverter.string) diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt index 0328631..adef540 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt @@ -32,7 +32,7 @@ fun <D : Device, T : Any> D.fxProperty( } } -fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>): Property<T> = +fun <D : Device, T : Any> D.fxProperty(spec: MutableDevicePropertySpec<D, T>): Property<T> = object : ObjectPropertyBase<T>() { override fun getBean(): Any = this override fun getName(): String = spec.name @@ -51,7 +51,7 @@ fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>): 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 <altavir@gmail.com> Date: Mon, 30 Oct 2023 21:47:41 +0300 Subject: [PATCH 021/125] 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<out Number>, 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 <altavir@gmail.com> Date: Mon, 30 Oct 2023 22:51:17 +0300 Subject: [PATCH 022/125] 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<Boolean>, ) : DeviceBySpec<LimitSwitch>(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<Boolean> = map(MetaConverter.boolean) { it == range.start } + public val atStartState: DeviceState<Boolean> = map(MetaConverter.boolean) { it <= range.start } /** * A state showing that the range is on its higher boundary */ - public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it == range.endInclusive } + public val atEndState: DeviceState<Boolean> = 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<out Number>, 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<Boolean>, + 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 <altavir@gmail.com> Date: Thu, 2 Nov 2023 15:36:10 +0300 Subject: [PATCH 023/125] 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<Double>, ) : Drive, DeviceBySpec<Drive>(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 <altavir@gmail.com> Date: Sun, 5 Nov 2023 09:47:58 +0300 Subject: [PATCH 024/125] 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 <altavir@gmail.com> Date: Sun, 5 Nov 2023 10:18:26 +0300 Subject: [PATCH 025/125] 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 <altavir@gmail.com> Date: Mon, 6 Nov 2023 11:39:56 +0300 Subject: [PATCH 026/125] 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<T>(val value: T, val time: Instant) { + public companion object { + /** + * Create a [ValueWithTime] format for given value value [IOFormat] + */ + public fun <T> ioFormat( + valueFormat: IOFormat<T>, + ): IOFormat<ValueWithTime<T>> = ValueWithTimeIOFormat(valueFormat) + + /** + * Create a [MetaConverter] with time for given value [MetaConverter] + */ + public fun <T> metaConverter( + valueConverter: MetaConverter<T>, + ): MetaConverter<ValueWithTime<T>> = ValueWithTimeMetaConverter(valueConverter) + + + public const val META_TIME_KEY: String = "time" + public const val META_VALUE_KEY: String = "value" + } +} + +private class ValueWithTimeIOFormat<T>(val valueFormat: IOFormat<T>) : IOFormat<ValueWithTime<T>> { + override val type: KType get() = typeOf<ValueWithTime<T>>() + + override fun readObject(input: Input): ValueWithTime<T> { + val timestamp = InstantIOFormat.readObject(input) + val value = valueFormat.readObject(input) + return ValueWithTime(value, timestamp) + } + + override fun writeObject(output: Output, obj: ValueWithTime<T>) { + InstantIOFormat.writeObject(output, obj.time) + valueFormat.writeObject(output, obj.value) + } + +} + +private class ValueWithTimeMetaConverter<T>( + val valueConverter: MetaConverter<T>, +) : MetaConverter<ValueWithTime<T>> { + override fun metaToObject( + meta: Meta, + ): ValueWithTime<T>? = 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<T>): Meta = Meta { + ValueWithTime.META_TIME_KEY put obj.time.toMeta() + ValueWithTime.META_VALUE_KEY put valueConverter.objectToMeta(obj.value) + } +} + +public fun <T : Any> MetaConverter<T>.withTime(): MetaConverter<ValueWithTime<T>> = 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<Instant>, IOFormatFactory<Instant> { + override fun build(context: Context, meta: Meta): IOFormat<Instant> = this + + override val name: Name = "instant".asName() + + override val type: KType get() = typeOf<Instant>() + + 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<Value> get() = value?.list ?: emptyList() @@ -23,48 +32,126 @@ private var TraceValues.values: List<Value> value = ListValue(newValues) } + +private var TraceValues.times: List<Instant> + 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<ValueWithTime<Value>> = 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<ValueWithTime<Value>> { + 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 <T> Trace.updateFromState( + context: Context, + state: DeviceState<T>, + 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<T, TimeData> { + data.append(clock.now(), it.extractValue()) + data.trim(maxAge, maxPoints, minPoints) + }.onEach { + it.fillPlot(x, y) + }.launchIn(context) +} + +public fun <T> Plot.plotDeviceState( + context: Context, + state: DeviceState<T>, + 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<out Number>, - 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<Boolean>, - 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 <altavir@gmail.com> Date: Mon, 6 Nov 2023 16:46:16 +0300 Subject: [PATCH 027/125] 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 <D : Device> device( + factory: Factory<D>, + meta: Meta? = null, + nameOverride: Name? = null, + metaLocation: Name? = null, + ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = + PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> + val name = nameOverride ?: property.name.asName() + val device = registerDevice(name, factory, meta, metaLocation ?: name) + ReadOnlyProperty { _: DeviceConstructor, _ -> + device + } + } + + public fun <D : Device> device( + device: D, + nameOverride: Name? = null, + ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = + 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 <T : Any> property( + state: DeviceState<T>, + nameOverride: String? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit, + ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = + 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 <T : Any> property( + metaConverter: MetaConverter<T>, + reader: suspend () -> T, + readInterval: Duration, + initialState: T, + nameOverride: String? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit, + ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property( + DeviceState.external(this, metaConverter, readInterval, initialState, reader), + nameOverride, descriptorBuilder + ) + + + /** + * Register a mutable property and provide a direct reader for it + */ + public fun <T : Any> mutableProperty( + state: MutableDeviceState<T>, + nameOverride: String? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit, + ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = + PropertyDelegateProvider { _: DeviceConstructor, property -> + val name = nameOverride ?: property.name + val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) + registerProperty(descriptor, state) + object : ReadWriteProperty<DeviceConstructor, T> { + 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 <T : Any> mutableProperty( + metaConverter: MetaConverter<T>, + reader: suspend () -> T, + writer: suspend (T) -> Unit, + readInterval: Duration, + initialState: T, + nameOverride: String? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit, + ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = 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<NameToken, Device> = _devices - public fun <D : Device> 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 <D : Device> 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<Name, Property> = hashMapOf() - public fun property(descriptor: PropertyDescriptor, state: DeviceState<out Any>) { + /** + * Register a new property based on [DeviceState]. Properties could be modified dynamically + */ + public fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<out Any>) { 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 <D : Device> DeviceGroup.device(name: Name, device: D): D { +public fun <D : Device> 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 <D: Device> DeviceGroup.device(name: String, device: D): D = device(name.parseAsName(), device) +public fun <D : Device> 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<Device>, deviceMeta: Meta? = null): Device { - val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[name])) - device(name, newDevice) +public fun <D : Device> DeviceGroup.registerDevice( + name: Name, + factory: Factory<D>, + 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 <D : Device> DeviceGroup.registerDevice( name: String, - factory: Factory<Device>, + factory: Factory<D>, + 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 <T : Any> DeviceGroup.property( +/** + * Register read-only property based on [state] + */ +public fun <T : Any> DeviceGroup.registerProperty( name: String, state: DeviceState<T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): DeviceState<T> { - property( +) { + registerProperty( PropertyDescriptor(name).apply(descriptorBuilder), state ) - return state } -public fun <T : Any> DeviceGroup.mutableProperty( +/** + * Register a mutable property based on mutable [state] + */ +public fun <T : Any> DeviceGroup.registerMutableProperty( name: String, state: MutableDeviceState<T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): MutableDeviceState<T> { - property( +) { + registerProperty( PropertyDescriptor(name).apply(descriptorBuilder), state ) - return state } -public fun <T : Any> DeviceGroup.virtualProperty( - name: String, - initialValue: T, - converter: MetaConverter<T>, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): MutableDeviceState<T> { - val state = VirtualDeviceState<T>(converter, initialValue) - return mutableProperty(name, state, descriptorBuilder) -} /** * Create a virtual [MutableDeviceState], but do not register it to a device */ @Suppress("UnusedReceiverParameter") -public fun <T : Any> DeviceGroup.standAloneProperty( +public fun <T : Any> DeviceGroup.state( + converter: MetaConverter<T>, + initialValue: T, +): MutableDeviceState<T> = VirtualDeviceState<T>(converter, initialValue) + +/** + * Create a new virtual mutable state and a property based on it. + * @return the mutable state used in property + */ +public fun <T : Any> DeviceGroup.registerVirtualProperty( + name: String, initialValue: T, converter: MetaConverter<T>, -): MutableDeviceState<T> = VirtualDeviceState<T>(converter, initialValue) \ No newline at end of file + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): MutableDeviceState<T> { + 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<T> { public val value: T public val valueFlow: Flow<T> + + public companion object } public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::objectToMeta) @@ -55,7 +60,7 @@ private open class BoundDeviceState<T>( override val converter: MetaConverter<T>, val device: Device, val propertyName: String, - private val initialValue: T, + initialValue: T, ) : DeviceState<T> { override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter { @@ -121,3 +126,58 @@ public suspend fun <D : Device, T> D.bindMutableStateToProperty( ): MutableDeviceState<T> = bindMutableStateToProperty(propertySpec.name, propertySpec.converter) +private open class ExternalState<T>( + val scope: CoroutineScope, + override val converter: MetaConverter<T>, + val readInterval: Duration, + initialValue: T, + val reader: suspend () -> T, +) : DeviceState<T> { + + protected val flow: StateFlow<T> = flow { + while (true) { + delay(readInterval) + emit(reader()) + } + }.stateIn(scope, SharingStarted.Eagerly, initialValue) + + override val value: T get() = flow.value + override val valueFlow: Flow<T> get() = flow +} + +/** + * Create a [DeviceState] which is constructed by periodically reading external value + */ +public fun <T> DeviceState.Companion.external( + scope: CoroutineScope, + converter: MetaConverter<T>, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, +): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader) + +private class MutableExternalState<T>( + scope: CoroutineScope, + converter: MetaConverter<T>, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, + val writer: suspend (T) -> Unit, +) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> { + override var value: T + get() = super.value + set(value) { + scope.launch { + writer(value) + } + } +} + +public fun <T> DeviceState.Companion.external( + scope: CoroutineScope, + converter: MetaConverter<T>, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, + writer: suspend (T) -> Unit, +): MutableDeviceState<T> = 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<Double>, + ): Factory<Drive> = Factory { context, _ -> + VirtualDrive(context, mass, positionState) + } + } } public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = bindMutableStateToProperty(Drive.force) - -public fun DeviceGroup.virtualDrive( - name: String, - mass: Double, - positionState: MutableDeviceState<Double>, -): 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<LimitSwitch>() { public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked } + public fun factory(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = 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<Boolean>): 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<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive } -} \ No newline at end of file +} + +@Suppress("UnusedReceiverParameter") +public fun DeviceGroup.rangeState( + initialValue: Double, + range: ClosedFloatingPointRange<Double>, +): 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 <altavir@gmail.com> Date: Tue, 7 Nov 2023 08:46:56 +0300 Subject: [PATCH 028/125] 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<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = 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<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = 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 <T : Any> property( state: DeviceState<T>, nameOverride: String? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = 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<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property( DeviceState.external(this, metaConverter, readInterval, initialState, reader), nameOverride, descriptorBuilder @@ -91,8 +91,8 @@ public abstract class DeviceConstructor( public fun <T : Any> mutableProperty( state: MutableDeviceState<T>, nameOverride: String? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit, - ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + ): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = 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<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = mutableProperty( + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + ): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = 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<DeviceMessage>() + + override val messageFlow: Flow<DeviceMessage> + 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<NameToken, Device>() @@ -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 <D : Device> registerDevice(token: NameToken, device: D): D { + public fun <D : Device> 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<Name, Action> = hashMapOf() @@ -115,10 +126,6 @@ public open class DeviceGroup( property.valueAsMeta = value } - private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>() - - override val messageFlow: Flow<DeviceMessage> - 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 <D : Device> DeviceGroup.registerDevice(name: Name, device: D): D { +public fun <D : Device> 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 <D : Device> DeviceGroup.registerDevice(name: String, device: D): D = registerDevice(name.parseAsName(), device) +public fun <D : Device> DeviceGroup.install(name: String, device: D): D = + install(name.parseAsName(), device) + +public fun <D : Device> 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 <D : Device> 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 <D : Device> DeviceGroup.registerDevice( +public fun <D : Device> DeviceGroup.install( name: Name, factory: Factory<D>, 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 <D : Device> DeviceGroup.registerDevice( +public fun <D : Device> DeviceGroup.install( name: String, factory: Factory<D>, 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<T>( /** * Bind a read-only [DeviceState] to a [Device] property */ -public suspend fun <T> Device.bindStateToProperty( +public suspend fun <T> Device.propertyAsState( propertyName: String, metaConverter: MetaConverter<T>, ): DeviceState<T> { @@ -83,9 +83,9 @@ public suspend fun <T> Device.bindStateToProperty( return BoundDeviceState(metaConverter, this, propertyName, initialValue) } -public suspend fun <D : Device, T> D.bindStateToProperty( +public suspend fun <D : Device, T> D.propertyAsState( propertySpec: DevicePropertySpec<D, T>, -): DeviceState<T> = bindStateToProperty(propertySpec.name, propertySpec.converter) +): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter) public fun <T, R> DeviceState<T>.map( converter: MetaConverter<R>, mapper: (T) -> R, @@ -113,17 +113,28 @@ private class MutableBoundDeviceState<T>( } } -public suspend fun <T> Device.bindMutableStateToProperty( +public fun <T> Device.mutablePropertyAsState( + propertyName: String, + metaConverter: MetaConverter<T>, + initialValue: T, +): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) + +public suspend fun <T> Device.mutablePropertyAsState( propertyName: String, metaConverter: MetaConverter<T>, ): MutableDeviceState<T> { 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 : Device, T> D.bindMutableStateToProperty( +public suspend fun <D : Device, T> D.mutablePropertyAsState( propertySpec: MutableDevicePropertySpec<D, T>, -): MutableDeviceState<T> = bindMutableStateToProperty(propertySpec.name, propertySpec.converter) +): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter) + +public fun <D : Device, T> D.mutablePropertyAsState( + propertySpec: MutableDevicePropertySpec<D, T>, + initialValue: T, +): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue) private open class ExternalState<T>( 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<Double> = bindMutableStateToProperty(Drive.force) +public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = 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<D : Device>( 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 <altavir@gmail.com> Date: Wed, 8 Nov 2023 11:52:57 +0300 Subject: [PATCH 029/125] 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<*>> { table -> + vf.produceHtml { + vision { table.toVision() } + } + } + + render<Plot> { 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 <altavir@gmail.com> Date: Wed, 8 Nov 2023 15:31:12 +0300 Subject: [PATCH 030/125] 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 <altavir@gmail.com> Date: Wed, 8 Nov 2023 21:01:42 +0300 Subject: [PATCH 031/125] 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<*>> { table -> - vf.produceHtml { - vision { table.toVision() } - } - } +// render<Table<*>> { table -> +// vf.produceHtml { +// vision { table.toVision() } +// } +// } render<Plot> { 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 <altavir@gmail.com> Date: Wed, 8 Nov 2023 22:28:26 +0300 Subject: [PATCH 032/125] 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<Value> 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 <T> 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<T, TimeData> { + return state.valueFlow.debounce(debounceDuration).transform<T, TimeData> { data.append(clock.now(), it.extractValue()) data.trim(maxAge, maxPoints, minPoints) }.onEach { @@ -127,9 +134,10 @@ public fun <T> 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 <altavir@gmail.com> Date: Wed, 8 Nov 2023 22:33:49 +0300 Subject: [PATCH 033/125] 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 <T> 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<T, TimeData> { + return state.valueFlow.sample(sampling).transform<T, TimeData> { data.append(clock.now(), it.extractValue()) data.trim(maxAge, maxPoints, minPoints) }.onEach { @@ -134,10 +134,10 @@ public fun <T> 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 <altavir@gmail.com> Date: Fri, 17 Nov 2023 12:22:06 +0300 Subject: [PATCH 034/125] 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 <altavir@gmail.com> Date: Sat, 18 Nov 2023 14:49:23 +0300 Subject: [PATCH 035/125] `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<D : Device> { return deviceProperty } - public fun <T> property( + public inline fun <reified T> property( converter: MetaConverter<T>, - readOnlyProperty: KProperty1<D, T>, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, DevicePropertySpec<D, T>>> = - PropertyDelegateProvider { _, property -> - val deviceProperty = object : DevicePropertySpec<D, T> { - override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { - //TODO add type from converter - mutable = true - }.apply(descriptorBuilder) - - override val converter: MetaConverter<T> = converter - - override suspend fun read(device: D): T = withContext(device.coroutineContext) { - readOnlyProperty.get(device) - } - } - registerProperty(deviceProperty) - ReadOnlyProperty { _, _ -> - deviceProperty - } - } - - public fun <T> mutableProperty( - converter: MetaConverter<T>, - readWriteProperty: KMutableProperty1<D, T>, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, MutableDevicePropertySpec<D, T>>> = - PropertyDelegateProvider { _, property -> - val deviceProperty = object : MutableDevicePropertySpec<D, T> { - - override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { - //TODO add the type from converter - mutable = true - }.apply(descriptorBuilder) - - override val converter: MetaConverter<T> = 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 <T> property( - converter: MetaConverter<T>, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> T?, + crossinline read: suspend D.(propertyName: String) -> T?, ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = PropertyDelegateProvider { _: DeviceSpec<D>, property -> val propertyName = name ?: property.name @@ -109,7 +56,7 @@ public abstract class DeviceSpec<D : Device> { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) override val converter: MetaConverter<T> = 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<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ -> @@ -117,27 +64,30 @@ public abstract class DeviceSpec<D : Device> { } } - public fun <T> mutableProperty( + public inline fun <reified T> mutableProperty( converter: MetaConverter<T>, - 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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> = PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> -> val propertyName = name ?: property.name val deviceProperty = object : MutableDevicePropertySpec<D, T> { - override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, mutable = true) - .apply(descriptorBuilder) + override val descriptor: PropertyDescriptor = PropertyDescriptor( + propertyName, + mutable = true + ).apply(descriptorBuilder) override val converter: MetaConverter<T> = 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<DeviceSpec<D>, MutableDevicePropertySpec<D, T>> { _, _ -> deviceProperty } @@ -209,33 +159,42 @@ public abstract class DeviceSpec<D : Device> { } } +public inline fun <reified T, D : Device> DeviceSpec<D>.property( + converter: MetaConverter<T>, + readOnlyProperty: KProperty1<D, T>, + crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = property( + converter, + descriptorBuilder, + name = readOnlyProperty.name, + read = { readOnlyProperty.get(this) } +) + +public inline fun <reified T, D : Device> DeviceSpec<D>.mutableProperty( + converter: MetaConverter<T>, + readWriteProperty: KMutableProperty1<D, T>, + crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> = + 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 <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty( +public inline fun <reified T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty( converter: MetaConverter<T>, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, -): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, MutableDevicePropertySpec<D, T>>> = - PropertyDelegateProvider { _, property -> - val deviceProperty = object : MutableDevicePropertySpec<D, T> { - 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<T> = 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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> = + 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 <D : Device> DeviceSpec<D>.booleanProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> Boolean? + read: suspend D.(propertyName: String) -> Boolean? ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property( MetaConverter.boolean, { @@ -37,9 +37,9 @@ private inline fun numberDescriptor( } public fun <D : Device> DeviceSpec<D>.numberProperty( - name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - read: suspend D.() -> Number? + name: String? = null, + read: suspend D.(propertyName: String) -> Number? ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property( MetaConverter.number, numberDescriptor(descriptorBuilder), @@ -50,7 +50,7 @@ public fun <D : Device> DeviceSpec<D>.numberProperty( public fun <D : Device> DeviceSpec<D>.doubleProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> Double? + read: suspend D.(propertyName: String) -> Double? ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property( MetaConverter.double, numberDescriptor(descriptorBuilder), @@ -61,7 +61,7 @@ public fun <D : Device> DeviceSpec<D>.doubleProperty( public fun <D : Device> DeviceSpec<D>.stringProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> String? + read: suspend D.(propertyName: String) -> String? ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property( MetaConverter.string, { @@ -77,7 +77,7 @@ public fun <D : Device> DeviceSpec<D>.stringProperty( public fun <D : Device> DeviceSpec<D>.metaProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - read: suspend D.() -> Meta? + read: suspend D.(propertyName: String) -> Meta? ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property( MetaConverter.meta, { @@ -95,8 +95,8 @@ public fun <D : Device> DeviceSpec<D>.metaProperty( public fun <D : Device> DeviceSpec<D>.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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Boolean>>> = mutableProperty( MetaConverter.boolean, @@ -115,31 +115,31 @@ public fun <D : Device> DeviceSpec<D>.booleanProperty( public fun <D : Device> DeviceSpec<D>.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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Number>>> = mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) public fun <D : Device> DeviceSpec<D>.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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Double>>> = mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) public fun <D : Device> DeviceSpec<D>.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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, String>>> = mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) public fun <D : Device> DeviceSpec<D>.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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Meta>>> = 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<ControlVisionPlugin> +} + +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<ControlVisionPlugin> { + 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<ControlVisionPlugin> { + 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<IDemoDevice>(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<MksPdr900Devi override fun build(context: Context, meta: Meta): MksPdr900Device = MksPdr900Device(context, meta) - val powerOn by booleanProperty(read = MksPdr900Device::readPowerOn, write = MksPdr900Device::writePowerOn) + val powerOn by booleanProperty(read = { readPowerOn() }, write = { _, value -> 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 <altavir@gmail.com> Date: Sat, 18 Nov 2023 15:39:56 +0300 Subject: [PATCH 036/125] `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<Unit> { override fun metaToObject(meta: Meta): Unit = Unit @@ -44,11 +42,11 @@ public abstract class DeviceSpec<D : Device> { return deviceProperty } - public inline fun <reified T> property( + public fun <T> property( converter: MetaConverter<T>, - crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, - crossinline read: suspend D.(propertyName: String) -> T?, + read: suspend D.(propertyName: String) -> T?, ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = PropertyDelegateProvider { _: DeviceSpec<D>, property -> val propertyName = name ?: property.name @@ -56,7 +54,8 @@ public abstract class DeviceSpec<D : Device> { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) override val converter: MetaConverter<T> = 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<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ -> @@ -64,12 +63,12 @@ public abstract class DeviceSpec<D : Device> { } } - public inline fun <reified T> mutableProperty( + public fun <T> mutableProperty( converter: MetaConverter<T>, - 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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> = PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> -> val propertyName = name ?: property.name @@ -106,7 +105,7 @@ public abstract class DeviceSpec<D : Device> { name: String? = null, execute: suspend D.(I) -> O, ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> = - PropertyDelegateProvider { _: DeviceSpec<D>, property -> + PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> -> val actionName = name ?: property.name val deviceAction = object : DeviceActionSpec<D, I, O> { override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder) @@ -124,77 +123,39 @@ public abstract class DeviceSpec<D : Device> { } } - /** - * 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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> = - 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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Unit, Unit>>> = - action( - MetaConverter.Companion.unit, - MetaConverter.Companion.unit, - descriptorBuilder, - name - ) { - execute() - } } -public inline fun <reified T, D : Device> DeviceSpec<D>.property( - converter: MetaConverter<T>, - readOnlyProperty: KProperty1<D, T>, - crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = property( - converter, - descriptorBuilder, - name = readOnlyProperty.name, - read = { readOnlyProperty.get(this) } -) - -public inline fun <reified T, D : Device> DeviceSpec<D>.mutableProperty( - converter: MetaConverter<T>, - readWriteProperty: KMutableProperty1<D, T>, - crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> = - mutableProperty( - converter, +/** + * An action that takes no parameters and returns no values + */ +public fun <D : Device> DeviceSpec<D>.unitAction( + descriptorBuilder: ActionDescriptor.() -> Unit = {}, + name: String? = null, + execute: suspend D.() -> Unit, +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Unit, Unit>>> = + 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 <reified T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty( - converter: MetaConverter<T>, - crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +public fun <D : Device> DeviceSpec<D>.metaAction( + descriptorBuilder: ActionDescriptor.() -> Unit = {}, name: String? = null, -): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> = - mutableProperty( - converter, + execute: suspend D.(Meta) -> Meta, +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> = + 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 <T, D : Device> DeviceSpec<D>.property( + converter: MetaConverter<T>, + readOnlyProperty: KProperty1<D, T>, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = property( + converter, + descriptorBuilder, + name = readOnlyProperty.name, + read = { readOnlyProperty.get(this) } +) + +/** + * Mutable property that delegates reading and writing to a device [KMutableProperty1] + */ +public fun <T, D : Device> DeviceSpec<D>.mutableProperty( + converter: MetaConverter<T>, + readWriteProperty: KMutableProperty1<D, T>, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> = + 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 <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty( + converter: MetaConverter<T>, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + name: String? = null, +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> = + 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 <altavir@gmail.com> Date: Sat, 18 Nov 2023 19:02:56 +0300 Subject: [PATCH 037/125] 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<D : Device> { PropertyDelegateProvider { _: DeviceSpec<D>, property -> val propertyName = name ?: property.name val deviceProperty = object : DevicePropertySpec<D, T> { - override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) + + override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { + fromSpec(property) + descriptorBuilder() + } + override val converter: MetaConverter<T> = converter override suspend fun read(device: D): T? = @@ -76,7 +81,10 @@ public abstract class DeviceSpec<D : Device> { override val descriptor: PropertyDescriptor = PropertyDescriptor( propertyName, mutable = true - ).apply(descriptorBuilder) + ).apply { + fromSpec(property) + descriptorBuilder() + } override val converter: MetaConverter<T> = converter override suspend fun read(device: D): T? = @@ -108,7 +116,10 @@ public abstract class DeviceSpec<D : Device> { PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> -> val actionName = name ?: property.name val deviceAction = object : DeviceActionSpec<D, I, O> { - override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder) + override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply { + fromSpec(property) + descriptorBuilder() + } override val inputConverter: MetaConverter<I> = inputConverter override val outputConverter: MetaConverter<O> = 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<Description>()?.let { + description = it.content + } +} + +internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){ + property.findAnnotation<Description>()?.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 <altavir@gmail.com> Date: Wed, 22 Nov 2023 21:55:13 +0300 Subject: [PATCH 038/125] 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 <altavir@gmail.com> Date: Thu, 23 Nov 2023 16:52:07 +0300 Subject: [PATCH 039/125] 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 <altavir@gmail.com> Date: Tue, 12 Dec 2023 09:59:52 +0300 Subject: [PATCH 040/125] 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<T> : Closeable { +public interface Socket<T> : 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<T>(val value: T, val time: Instant) { private class ValueWithTimeIOFormat<T>(val valueFormat: IOFormat<T>) : IOFormat<ValueWithTime<T>> { override val type: KType get() = typeOf<ValueWithTime<T>>() - override fun readObject(input: Input): ValueWithTime<T> { - val timestamp = InstantIOFormat.readObject(input) - val value = valueFormat.readObject(input) + + override fun readFrom(source: Source): ValueWithTime<T> { + val timestamp = InstantIOFormat.readFrom(source) + val value = valueFormat.readFrom(source) return ValueWithTime(value, timestamp) } - override fun writeObject(output: Output, obj: ValueWithTime<T>) { - InstantIOFormat.writeObject(output, obj.time) - valueFormat.writeObject(output, obj.value) + override fun writeTo(sink: Sink, obj: ValueWithTime<T>) { + InstantIOFormat.writeTo(sink, obj.time) + valueFormat.writeTo(sink, obj.value) } } @@ -54,7 +55,10 @@ private class ValueWithTimeIOFormat<T>(val valueFormat: IOFormat<T>) : IOFormat< private class ValueWithTimeMetaConverter<T>( val valueConverter: MetaConverter<T>, ) : MetaConverter<ValueWithTime<T>> { - override fun metaToObject( + + override val type: KType = typeOf<ValueWithTime<T>>() + + override fun metaToObjectOrNull( meta: Meta, ): ValueWithTime<T>? = 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<Instant>, IOFormatFactory<Instant> { override val type: KType get() = typeOf<Instant>() - 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<ByteArray> /** * A specialized factory for [Port] */ -@Type(PortFactory.TYPE) +@DfType(PortFactory.TYPE) public interface PortFactory : Factory<Port> { 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<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray> { 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<Unit> { - override fun metaToObject(meta: Meta): Unit = Unit + + override val type: KType = typeOf<Unit>() + + 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<Duration> { - override fun metaToObject(meta: Meta): Duration = meta.value?.double?.toDuration(DurationUnit.SECONDS) + override val type: KType = typeOf<Duration>() + + override fun metaToObjectOrNull(meta: Meta): Duration = meta.value?.double?.toDuration(DurationUnit.SECONDS) ?: run { val unit: DurationUnit = meta["unit"].enum<DurationUnit>() ?: 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 <D : Device> DeviceSpec<D>.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 <D : Device> DeviceSpec<D>.stringProperty( MetaConverter.string, { metaDescriptor { - type(ValueType.STRING) + valueType(ValueType.STRING) } descriptorBuilder() }, @@ -131,7 +131,7 @@ public fun <D : Device> DeviceSpec<D>.metaProperty( MetaConverter.meta, { metaDescriptor { - type(ValueType.STRING) + valueType(ValueType.STRING) } descriptorBuilder() }, @@ -151,7 +151,7 @@ public fun <D : Device> DeviceSpec<D>.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 <altavir@gmail.com> Date: Wed, 13 Dec 2023 12:29:06 +0300 Subject: [PATCH 041/125] 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<T> : DeviceState<T> { public var <T : Any> MutableDeviceState<T>.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<ByteArray>(100) - private val incoming = Channel<ByteArray>(Channel.CONFLATED) + private val incoming = Channel<ByteArray>(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<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray> 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<ByteChannel> = this.scope.async(Dispatchers.IO) { + private val futureChannel: Deferred<ByteChannel> = 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<D : Device> internal constructor( @@ -106,11 +108,11 @@ public class DeviceProcessImageBuilder<D : Device> 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<D : Device> internal constructor( /** * Trigger [block] if one of register changes. */ - private fun List<ObservableRegister>.onChange(block: suspend (ByteReadPacket) -> Unit) { + private fun List<ObservableRegister>.onChange(block: suspend (Buffer) -> Unit) { var ready = false forEach { register -> @@ -128,7 +130,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor( } device.launch { - val builder = BytePacketBuilder() + val builder = Buffer() while (isActive) { delay(1) if (ready) { @@ -136,7 +138,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor( forEach { value -> writeShort(value.toShort()) } - }.build() + } block(packet) ready = false } @@ -154,15 +156,15 @@ public class DeviceProcessImageBuilder<D : Device> 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<D : Device> 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 <T> ModbusRegistryKey.InputRange<T>.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 <T> ModbusRegistryKey.HoldingRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T { val packet = readHoldingRegistersToPacket(address, count) - return format.readObject(packet) + return format.readFrom(packet) } public operator fun <T> ModbusRegistryKey.HoldingRange<T>.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<out InputRegister>.toBuffer(): ByteBuffer { return buffer } -private fun Array<out InputRegister>.toPacket(): ByteReadPacket = buildPacket { +private fun Array<out InputRegister>.toPacket(): Buffer = Buffer { forEach { value -> writeShort(value.toShort()) } @@ -131,7 +130,7 @@ private fun Array<out InputRegister>.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<Reg public fun ModbusDevice.readHoldingRegistersToBuffer(address: Int, count: Int): ByteBuffer = master.readMultipleRegisters(unitId, address, count).toBuffer() -public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): ByteReadPacket = +public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): Buffer = master.readMultipleRegisters(unitId, address, count).toPacket() public fun ModbusDevice.readDoubleRegister(address: Int): Double = @@ -183,6 +182,13 @@ public fun ModbusDevice.writeHoldingRegisters(address: Int, buffer: ByteBuffer): return writeHoldingRegisters(address, array) } +public fun ModbusDevice.writeHoldingRegisters(address: Int, byteArray: ByteArray): Int { + val buffer = ByteBuffer.wrap(byteArray) + val array: ShortArray = ShortArray(buffer.limit().floorDiv(2)) { buffer.getShort(it * 2) } + + return writeHoldingRegisters(address, array) +} + public fun ModbusDevice.modbusRegister( address: Int, ): ReadWriteProperty<ModbusDevice, Short> = object : ReadWriteProperty<ModbusDevice, Short> { 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<String, Meta>): 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 <reified T: Any> 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<IDemoDevice>(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<Vector2D> { - override fun metaToObject(meta: Meta): Vector2D = Vector2D( + + override val type: KType = typeOf<Vector2D>() + + 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<VirtualCar>(IVirtualCar, context, meta), IVirtualCar { +open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(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<String?> { - 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 <altavir@gmail.com> Date: Wed, 13 Dec 2023 14:50:56 +0300 Subject: [PATCH 042/125] 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 <altavir@gmail.com> Date: Wed, 13 Dec 2023 20:20:03 +0300 Subject: [PATCH 043/125] 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 <altavir@gmail.com> Date: Fri, 15 Dec 2023 16:55:56 +0300 Subject: [PATCH 044/125] 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<ByteWriteChannel> = 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 <altavir@gmail.com> Date: Fri, 22 Dec 2023 09:28:39 +0300 Subject: [PATCH 045/125] 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<DeviceMessage>() override val messageFlow: Flow<DeviceMessage> @@ -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 <D : Device> 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<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray> { 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 <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T> public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? = readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject) -public suspend fun <T, D : Device> D.request(propertySpec: DevicePropertySpec<D, T>): T? = +public suspend fun <T, D : Device> D.request(propertySpec: DevicePropertySpec<D, T>): 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<Value> get() = value?.list ?: emptyList() @@ -82,6 +84,11 @@ private class TimeData(private var points: MutableList<ValueWithTime<Value>> = 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 <T> Trace.updateFromState( context: Context, state: DeviceState<T>, - 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 <T> Plot.plotDeviceState( context: Context, state: DeviceState<T>, 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 <T> Plot.plotDeviceState( public fun Plot.plotNumberState( context: Context, state: DeviceState<out Number>, - 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<Boolean>, - 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 <altavir@gmail.com> Date: Mon, 25 Dec 2023 19:09:40 +0300 Subject: [PATCH 046/125] 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 <T : Any> property( state: DeviceState<T>, - nameOverride: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = 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<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property( DeviceState.external(this, metaConverter, readInterval, initialState, reader), - nameOverride, descriptorBuilder + descriptorBuilder, + nameOverride, ) @@ -90,8 +91,8 @@ public abstract class DeviceConstructor( */ public fun <T : Any> mutableProperty( state: MutableDeviceState<T>, - nameOverride: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, ): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = 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<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = mutableProperty( DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), + descriptorBuilder, + nameOverride, + ) + + /** + * Create and register a virtual property with optional [callback] + */ + public fun <T : Any> state( + metaConverter: MetaConverter<T>, + initialState: T, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, + callback: (T) -> Unit = {}, + ): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = 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<DeviceMessage> 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 <D : Device> 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 <D : Device> DeviceGroup.install(name: Name, device: D): D { public fun <D : Device> DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device) +public fun <D : Device> DeviceGroup.install(device: D): D = + install(device.id, device) + public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device) /** @@ -281,15 +285,6 @@ public fun <T : Any> DeviceGroup.registerMutableProperty( } -/** - * Create a virtual [MutableDeviceState], but do not register it to a device - */ -@Suppress("UnusedReceiverParameter") -public fun <T : Any> DeviceGroup.state( - converter: MetaConverter<T>, - initialValue: T, -): MutableDeviceState<T> = VirtualDeviceState<T>(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 <T : Any> DeviceGroup.registerVirtualProperty( initialValue: T, converter: MetaConverter<T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + callback: (T) -> Unit = {}, ): MutableDeviceState<T> { - val state = state(converter, initialValue) + val state = DeviceState.virtual<T>(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 <T : Any> MutableDeviceState<T>.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<T>( +private class VirtualDeviceState<T>( override val converter: MetaConverter<T>, initialValue: T, + private val callback: (T) -> Unit = {}, ) : MutableDeviceState<T> { private val flow = MutableStateFlow(initialValue) override val valueFlow: Flow<T> 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 <T> DeviceState.Companion.virtual( + converter: MetaConverter<T>, + initialValue: T, + callback: (T) -> Unit = {}, +): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback) + +private class StateFlowAsState<T>( + override val converter: MetaConverter<T>, + val flow: MutableStateFlow<T>, +) : MutableDeviceState<T> { + override var value: T by flow::value + override val valueFlow: Flow<T> get() = flow +} + +public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> = + StateFlowAsState(converter, this) + + private open class BoundDeviceState<T>( override val converter: MetaConverter<T>, 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 <D : Device> DeviceManager.install(name: String, device: D): D { return device } +public fun <D : Device> 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 <altavir@gmail.com> Date: Thu, 28 Dec 2023 21:09:23 +0300 Subject: [PATCH 047/125] 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<Name, Any> = 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<Name, Device> = 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<Name, Any> = 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<DeviceMessage> { 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<DeviceMessage>): Unit = respondText( + MagixEndpoint.magixJson.encodeToString(serializer<List<DeviceMessage>>(), 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<IDemoDevice>(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<IDemoDevice>(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 <altavir@gmail.com> Date: Thu, 28 Dec 2023 22:40:58 +0300 Subject: [PATCH 048/125] 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<Number> { 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<DataItem?>?) { subscription.onDataItemsCreated(dataItems) } From fa2414ef473feaa33402e13c68b6d89f949757c8 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Fri, 2 Feb 2024 16:04:41 +0300 Subject: [PATCH 049/125] 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 <altavir@gmail.com> Date: Mon, 5 Feb 2024 14:08:15 +0300 Subject: [PATCH 050/125] 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<T>( public fun <T> MagixEndpoint.subscribe( format: MagixFormat<T>, originFilter: Collection<String>? = null, - targetFilter: Collection<String>? = null, + targetFilter: Collection<String?>? = null, ): Flow<Pair<MagixMessage, T>> = 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<String>? = null, val source: Collection<String>? = null, - val target: Collection<String>? = null, + val target: Collection<String?>? = 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<String>?, + values: Collection<String?>?, ): 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 <altavir@gmail.com> Date: Thu, 15 Feb 2024 21:04:59 +0300 Subject: [PATCH 051/125] 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 <T> 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>(TestDevice, context, meta) { + private val rng = Random(meta["seed"].int ?: 0) + + private val randomValue get() = rng.nextDouble() + + companion object : DeviceSpec<TestDevice>(), Factory<TestDevice> { + + 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<MagixMessage> = 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 <altavir@gmail.com> Date: Mon, 19 Feb 2024 14:27:36 +0300 Subject: [PATCH 052/125] 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 <T> DeviceClient.read(propertySpec: DevicePropertySpec<*, T>): T = + propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Property read result is not valid") + + +public suspend fun <T> DeviceClient.request(propertySpec: DevicePropertySpec<*, T>): T = + propertySpec.converter.metaToObject(requestProperty(propertySpec.name)) + +public suspend fun <T> DeviceClient.write(propertySpec: MutableDevicePropertySpec<*, T>, value: T) { + writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value)) +} + +public fun <T> DeviceClient.writeAsync(propertySpec: MutableDevicePropertySpec<*, T>, value: T): Job = launch { + write(propertySpec, value) +} + +public fun <T> DeviceClient.propertyFlow(spec: DevicePropertySpec<*, T>): Flow<T> = messageFlow + .filterIsInstance<PropertyChangedMessage>() + .filter { it.property == spec.name } + .mapNotNull { spec.converter.metaToObject(it.value) } + +public fun <T> DeviceClient.onPropertyChange( + spec: DevicePropertySpec<*, T>, + scope: CoroutineScope = this, + callback: suspend PropertyChangedMessage.(T) -> Unit, +): Job = messageFlow + .filterIsInstance<PropertyChangedMessage>() + .filter { it.property == spec.name } + .onEach { change -> + val newValue = spec.converter.metaToObject(change.value) + if (newValue != null) { + change.callback(newValue) + } + }.launchIn(scope) + +public fun <T> DeviceClient.useProperty( + spec: DevicePropertySpec<*, T>, + scope: CoroutineScope = this, + callback: suspend (T) -> Unit, +): Job = scope.launch { + callback(read(spec)) + messageFlow + .filterIsInstance<PropertyChangedMessage>() + .filter { it.property == spec.name } + .collect { change -> + val newValue = spec.converter.metaToObject(change.value) + if (newValue != null) { + callback(newValue) + } + } +} + +public suspend fun <I, O> 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 <O> 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 <T> 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>(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 <altavir@gmail.com> Date: Mon, 19 Feb 2024 15:10:51 +0300 Subject: [PATCH 053/125] 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 <T> MagixEndpoint.controlsPropertyFlow( + endpointName: String, + deviceName: Name, + propertySpec: DevicePropertySpec<*, T>, +): Flow<T> { + val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(endpointName)).map { it.second } + + return subscription.filterIsInstance<PropertyChangedMessage>() + .filter { message -> + message.sourceDevice == deviceName && message.property == propertySpec.name + }.map { + propertySpec.converter.metaToObject(it.value) + } +} + +public suspend fun <T> 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 <T> MagixEndpoint.controlsPropertyMessageFlow( + endpointName: String, + deviceName: Name, + propertySpec: DevicePropertySpec<*, T>, +): Flow<Pair<PropertyChangedMessage, T>> { + val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(endpointName)).map { it.second } + + return subscription.filterIsInstance<PropertyChangedMessage>() + .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 <altavir@gmail.com> Date: Tue, 27 Feb 2024 10:31:35 +0300 Subject: [PATCH 054/125] 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<DeviceMessage> { 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<Dev /** * Collect all messages from given [DeviceHub], applying proper relative names. */ -public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow<DeviceMessage> { +public fun DeviceHub.hubMessageFlow(): Flow<DeviceMessage> { - //TODO could we avoid using downstream scope? - val outbox = MutableSharedFlow<DeviceMessage>() - 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>, ): DeviceMessageStorage = factory.build(context, meta) From cfd9eb053ca5b6f22237e4a592bf485010e0d80d Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 4 Mar 2024 11:11:56 +0300 Subject: [PATCH 055/125] 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 <altavir@gmail.com> Date: Mon, 4 Mar 2024 11:12:16 +0300 Subject: [PATCH 056/125] 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<T> { + /** + * 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<ValueWithTime<T>> +} + +/** + * An in-memory property values history collector + */ +public class CollectedPropertyHistory<T>( + public val scope: CoroutineScope, + eventFlow: Flow<DeviceMessage>, + public val propertyName: String, + public val converter: MetaConverter<T>, + maxSize: Int = 1000, +) : PropertyHistory<T> { + + private val store: SharedFlow<ValueWithTime<T>> = eventFlow + .filterIsInstance<PropertyChangedMessage>() + .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<ValueWithTime<T>> = + store.filter { it.time in from..until } +} + +/** + * Collect and store in memory device property changes for a given property + */ +public fun <T> Device.collectPropertyHistory( + scope: CoroutineScope = this, + propertyName: String, + converter: MetaConverter<T>, + maxSize: Int = 1000, +): PropertyHistory<T> = CollectedPropertyHistory(scope, messageFlow, propertyName, converter, maxSize) + +public fun <D : Device, T> D.collectPropertyHistory( + scope: CoroutineScope = this, + spec: DevicePropertySpec<D, T>, + maxSize: Int = 1000, +): PropertyHistory<T> = 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 <altavir@gmail.com> Date: Mon, 4 Mar 2024 12:47:40 +0300 Subject: [PATCH 057/125] 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<DeviceMessage> = entityStore.computeInReadonlyTransaction { transaction -> + override fun readAll(): Flow<DeviceMessage> = 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<Instant>?, sourceDevice: Name?, targetDevice: Name?, - ): List<DeviceMessage> = entityStore.computeInReadonlyTransaction { transaction -> + ): Flow<DeviceMessage> = 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 <reified T : DeviceMessage> XodusDeviceMessageStorage.query( - range: ClosedRange<Instant>? = null, - sourceDevice: Name? = null, - targetDevice: Name? = null, -): List<T> = read(serialDescriptor<T>().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<DeviceMessage> + /** + * Return all messages in a storage as a flow + */ + public fun readAll(): Flow<DeviceMessage> - 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<Instant>? = null, sourceDevice: Name? = null, targetDevice: Name? = null, - ): List<DeviceMessage> + ): Flow<DeviceMessage> public fun close() +} + +/** + * Query all messages of given type + */ +@OptIn(ExperimentalSerializationApi::class) +public inline fun <reified T : DeviceMessage> DeviceMessageStorage.read( + range: ClosedRange<Instant>? = null, + sourceDevice: Name? = null, + targetDevice: Name? = null, +): Flow<T> = read(serialDescriptor<T>().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 <T> DeviceMessageStorage.propertyHistory( + propertyName: String, + converter: MetaConverter<T>, +): PropertyHistory<T> = object : PropertyHistory<T> { + override fun flowHistory(from: Instant, until: Instant): Flow<ValueWithTime<T>> = + read<PropertyChangedMessage>(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<EventStorage>, -// meta: Meta = Meta.EMPTY, -//): List<PropertyChangedMessage> { -// 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 <altavir@gmail.com> Date: Mon, 4 Mar 2024 15:24:27 +0300 Subject: [PATCH 058/125] 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<T> { public companion object } -public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::objectToMeta) +public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::convert) -public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.objectToMeta(value) +public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.convert(value) /** @@ -38,9 +38,9 @@ public interface MutableDeviceState<T> : DeviceState<T> { } public var <T : Any> MutableDeviceState<T>.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<T>( override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().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 <T> Device.propertyAsState( propertyName: String, metaConverter: MetaConverter<T>, ): DeviceState<T> { - 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<T>( 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 <T> Device.mutablePropertyAsState( propertyName: String, metaConverter: MetaConverter<T>, ): MutableDeviceState<T> { - 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<T>( private val store: SharedFlow<ValueWithTime<T>> = eventFlow .filterIsInstance<PropertyChangedMessage>() .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<ValueWithTime<T>> = 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<T>(val value: T, val time: Instant) { } private class ValueWithTimeIOFormat<T>(val valueFormat: IOFormat<T>) : IOFormat<ValueWithTime<T>> { - override val type: KType get() = typeOf<ValueWithTime<T>>() - override fun readFrom(source: Source): ValueWithTime<T> { val timestamp = InstantIOFormat.readFrom(source) @@ -56,18 +52,18 @@ private class ValueWithTimeMetaConverter<T>( val valueConverter: MetaConverter<T>, ) : MetaConverter<ValueWithTime<T>> { - override val type: KType = typeOf<ValueWithTime<T>>() - override fun metaToObjectOrNull( - meta: Meta, - ): ValueWithTime<T>? = 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<T>? = 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<T>): Meta = Meta { + override fun convert(obj: ValueWithTime<T>): 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 <T : Any> MetaConverter<T>.withTime(): MetaConverter<ValueWithTime<T>> = 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 <D : Device, T> MutableDevicePropertySpec<D, T>.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 <D : Device, T> MutableDevicePropertySpec<D, T>.writeMeta(de */ @OptIn(InternalDeviceAPI::class) private suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? = - read(device)?.let(converter::objectToMeta) + read(device)?.let(converter::convert) private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.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<D : Device>( * Notify the device that a property with [spec] value is changed */ protected suspend fun <T> propertyChanged(spec: DevicePropertySpec<D, T>, 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<Device,Meta> { +internal object DeviceMetaPropertySpec : DevicePropertySpec<Device, Meta> { override val descriptor: PropertyDescriptor = PropertyDescriptor("@meta") override val converter: MetaConverter<Meta> = 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<in D, I, O> { public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, 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") /** * Read typed value and update/push event if needed. * Return null if property read is not successful or property is undefined. */ public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? = - readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject) + readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::readOrNull) -public suspend fun <T, D : Device> D.request(propertySpec: DevicePropertySpec<D, T>): T = - propertySpec.converter.metaToObject(requestProperty(propertySpec.name)) +public suspend fun <T, D : Device> D.getOrRead(propertySpec: DevicePropertySpec<D, T>): T = + propertySpec.converter.read(getOrReadProperty(propertySpec.name)) /** * Write typed property state and invalidate logical state */ public suspend fun <T, D : Device> D.write(propertySpec: MutableDevicePropertySpec<D, T>, value: T) { - writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value)) + writeProperty(propertySpec.name, propertySpec.converter.convert(value)) } /** @@ -104,7 +104,7 @@ public fun <T, D : Device> D.writeAsync(propertySpec: MutableDevicePropertySpec< public fun <D : Device, T> D.propertyFlow(spec: DevicePropertySpec<D, T>): Flow<T> = messageFlow .filterIsInstance<PropertyChangedMessage>() .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 : Device, T> D.onPropertyChange( .filterIsInstance<PropertyChangedMessage>() .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 : Device, T> D.useProperty( .filterIsInstance<PropertyChangedMessage>() .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<Unit> { - override val type: KType = typeOf<Unit>() + 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<Unit> 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<Duration> { - override val type: KType = typeOf<Duration>() - - 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>() ?: DurationUnit.SECONDS - val value = meta[Meta.VALUE_KEY].double ?: error("No value present for Duration") + val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: 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<Duration> 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 <T, D : DeviceBase<D>> DeviceSpec<D>.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 <T> 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 <T> 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 <T> 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 <T> 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 <T> DeviceClient.request(propertySpec: DevicePropertySpec<*, T>): T = - propertySpec.converter.metaToObject(requestProperty(propertySpec.name)) + propertySpec.converter.read(getOrReadProperty(propertySpec.name)) public suspend fun <T> DeviceClient.write(propertySpec: MutableDevicePropertySpec<*, T>, value: T) { - writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value)) + writeProperty(propertySpec.name, propertySpec.converter.convert(value)) } public fun <T> DeviceClient.writeAsync(propertySpec: MutableDevicePropertySpec<*, T>, value: T): Job = launch { @@ -34,7 +34,7 @@ public fun <T> DeviceClient.writeAsync(propertySpec: MutableDevicePropertySpec<* public fun <T> DeviceClient.propertyFlow(spec: DevicePropertySpec<*, T>): Flow<T> = messageFlow .filterIsInstance<PropertyChangedMessage>() .filter { it.property == spec.name } - .mapNotNull { spec.converter.metaToObject(it.value) } + .mapNotNull { spec.converter.readOrNull(it.value) } public fun <T> DeviceClient.onPropertyChange( spec: DevicePropertySpec<*, T>, @@ -44,7 +44,7 @@ public fun <T> DeviceClient.onPropertyChange( .filterIsInstance<PropertyChangedMessage>() .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 <T> DeviceClient.useProperty( .filterIsInstance<PropertyChangedMessage>() .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 <T> DeviceClient.useProperty( } public suspend fun <I, O> 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 <O> 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 <reified T: Any> 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 <reified T> 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 <reified T> OpcUaDevice.writeOpc( @@ -77,7 +77,7 @@ public suspend inline fun <reified T> OpcUaDevice.writeOpc( converter: MetaConverter<T>, 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<PropertyChangedMessage>( + storage.read<PropertyChangedMessage>( 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 <T> DeviceMessageStorage.propertyHistory( propertyName: String, @@ -16,5 +16,5 @@ public fun <T> DeviceMessageStorage.propertyHistory( override fun flowHistory(from: Instant, until: Instant): Flow<ValueWithTime<T>> = read<PropertyChangedMessage>(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 <T> Trace.updateFromState( public fun <T> Plot.plotDeviceState( context: Context, state: DeviceState<T>, - 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<Vector2D> { - override val type: KType = typeOf<Vector2D>() - - 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<MksPdr900Devi val channel by logicalProperty(MetaConverter.int) val value by doubleProperty(read = { - readChannelData(request(channel) ?: DEFAULT_CHANNEL) + readChannelData(getOrRead(channel)) }) val error by logicalProperty(MetaConverter.string) 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 dde2750..5d05993 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 @@ -16,11 +16,7 @@ import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.ports.* import space.kscience.controls.spec.* import space.kscience.dataforge.context.* -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.asValue -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 space.kscience.dataforge.names.NameToken import kotlin.collections.component1 import kotlin.collections.component2 From e8c6e90a0fc4b9d9ec255791058bea2e7fe9c765 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 4 Mar 2024 15:58:53 +0300 Subject: [PATCH 059/125] 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 @@ [](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) -[](https://zenodo.org/badge/latestdoi/240888288) +[](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 <init> ()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 <init> ()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 <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;)V - public synthetic fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;)V + public synthetic fun <init> (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 <init> (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 <init> ()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 <init> (Lspace/kscience/dataforge/context/Context;Lspace/kscience/controls/constructor/DoubleRangeState;DLspace/kscience/controls/constructor/PidParameters;Lspace/kscience/dataforge/meta/Meta;)V + public synthetic fun <init> (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 <altavir@gmail.com> Date: Mon, 4 Mar 2024 16:02:50 +0300 Subject: [PATCH 060/125] 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 4639fdb558dc313f43d6f1b64561699e3bef0c15 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar <ovsyannikov.alexey95@gmail.com> Date: Wed, 6 Mar 2024 00:21:55 +0600 Subject: [PATCH 061/125] update gradle wrapper --- gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/yarn.lock | 2042 ++++++++++++++++++++++ 2 files changed, 2043 insertions(+), 1 deletion(-) create mode 100644 gradle/yarn.lock diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fae0804..17655d0 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.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/yarn.lock b/gradle/yarn.lock new file mode 100644 index 0000000..d909f26 --- /dev/null +++ b/gradle/yarn.lock @@ -0,0 +1,2042 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/cors@^2.8.12": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.56.5" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.5.tgz#94b88cab77588fcecdd0771a6d576fa1c0af9d02" + integrity sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/json-schema@*", "@types/json-schema@^7.0.8": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/node@*", "@types/node@>=10.0.0": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.1": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.3": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abab@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +abort-controller@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.7.6: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +body-parser@^1.19.0: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserslist@^4.14.5: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== + dependencies: + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001587: + version "1.0.30001594" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001594.tgz#bea552414cd52c2d0c985ed9206314a696e685f5" + integrity sha512-VblSX6nYqyJVs8DKFMldE2IVCJjZ225LW00ydtUWwh5hk9IfkTOffO6r8gJNsH0qqqeAF8KrbMYA2VEwTlGW5g== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^3.5.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== + +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4.3.4, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +define-data-property@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.668: + version "1.4.692" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz#82139d20585a4b2318a02066af7593a3e6bec993" + integrity sha512-d5rZRka9n2Y3MkWRN74IoAsxR0HK3yaAt7T50e3iT9VZmCCQDT3geXUO5ZRMhDToa1pkCeQXuNo+0g+NfDOVPA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +engine.io-parser@~5.2.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" + integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw== + +engine.io@~6.5.2: + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.4.tgz#6822debf324e781add2254e912f8568508850cdc" + integrity sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + +enhanced-resolve@^5.13.0: + version "5.15.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.1.tgz#384391e025f099e67b4b00bfd7f0906a408214e1" + integrity sha512-3d3JRbwsCLJsYgvb6NuWEG44jjPSOMuS73L/6+7BZuoKm3W+qXnSoIYVHi8dG7Qcg4inAY4jbzkZ7MnskePeDg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +ent@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== + +envinfo@^7.7.3: + version "7.11.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.11.1.tgz#2ffef77591057081b0129a8fd8cf6118da1b94e1" + integrity sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5" + integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w== + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.7: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +follow-redirects@^1.0.0: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.3, glob@^7.1.7: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa" + integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA== + dependencies: + function-bind "^1.1.2" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isbinaryfile@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" + integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== + dependencies: + which "^1.2.1" + +karma-mocha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" + integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ== + dependencies: + minimist "^1.2.3" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.0.tgz#2a2c7b80163fe7ffd1010f83f5507f95ef39f840" + integrity sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + webpack-merge "^4.1.5" + +karma@6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.2.tgz#a983f874cee6f35990c4b2dcc3d274653714de8e" + integrity sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ== + dependencies: + "@colors/colors" "1.5.0" + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.5.1" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + glob "^7.1.7" + graceful-fs "^4.2.6" + http-proxy "^1.18.1" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.4.1" + mime "^2.5.2" + minimatch "^3.0.4" + mkdirp "^0.5.5" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^4.4.1" + source-map "^0.6.1" + tmp "^0.2.1" + ua-parser-js "^0.7.30" + yargs "^16.1.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log4js@^6.4.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" + integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rfdc@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" + integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.1.1, schema-utils@^3.1.2: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +set-function-length@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.1.tgz#47cc5945f2c771e2cf261c6737cf9684a2a5e425" + integrity sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g== + dependencies: + define-data-property "^1.1.2" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.1" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +socket.io-adapter@~2.5.2: + version "2.5.4" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz#4fdb1358667f6d68f25343353bd99bd11ee41006" + integrity sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg== + dependencies: + debug "~4.3.4" + ws "~8.11.0" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.4.1: + version "4.7.4" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.4.tgz#2401a2d7101e4bdc64da80b140d5d8b6a8c7738b" + integrity sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.5.2" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-loader@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.1.tgz#72f00d05f5d1f90f80974eda781cbd7107c125f2" + integrity sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA== + dependencies: + abab "^2.0.6" + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@8.1.1, supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.7: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + +terser@^5.26.0: + version "5.28.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.28.1.tgz#bf00f7537fd3a798c352c2d67d67d65c915d1b28" + integrity sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +tmp@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + +ua-parser-js@^0.7.30: + version "0.7.37" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832" + integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@^1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webpack-cli@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.0.tgz#abc4b1f44b50250f2632d8b8b536cfe2f6257891" + integrity sha512-a7KRJnCxejFoDpYTOwzm5o21ZXMaNqtRlvS183XzGDUPRdVEzJNImcQokqYZ8BNTnk9DkKiuWxw75+DCCoZ26w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.0" + "@webpack-cli/info" "^2.0.1" + "@webpack-cli/serve" "^2.0.3" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-merge@^4.1.5: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.82.0: + version "5.82.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.82.0.tgz#3c0d074dec79401db026b4ba0fb23d6333f88e7d" + integrity sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.13.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.2" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0, yargs@^16.1.1: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 4835376c0de47caa95cd6d0f362c140f1f6af7e8 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Wed, 6 Mar 2024 18:55:11 +0300 Subject: [PATCH 062/125] 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<T> { public class CollectedPropertyHistory<T>( public val scope: CoroutineScope, eventFlow: Flow<DeviceMessage>, + public val deviceName: Name, public val propertyName: String, public val converter: MetaConverter<T>, maxSize: Int = 1000, @@ -41,7 +43,7 @@ public class CollectedPropertyHistory<T>( private val store: SharedFlow<ValueWithTime<T>> = eventFlow .filterIsInstance<PropertyChangedMessage>() - .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<T>( */ public fun <T> Device.collectPropertyHistory( scope: CoroutineScope = this, + deviceName: Name, propertyName: String, converter: MetaConverter<T>, maxSize: Int = 1000, -): PropertyHistory<T> = CollectedPropertyHistory(scope, messageFlow, propertyName, converter, maxSize) +): PropertyHistory<T> = CollectedPropertyHistory(scope, messageFlow, deviceName, propertyName, converter, maxSize) public fun <D : Device, T> D.collectPropertyHistory( scope: CoroutineScope = this, + deviceName: Name, spec: DevicePropertySpec<D, T>, maxSize: Int = 1000, -): PropertyHistory<T> = collectPropertyHistory(scope, spec.name, spec.converter, maxSize) \ No newline at end of file +): PropertyHistory<T> = 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 <altavir@gmail.com> Date: Tue, 12 Mar 2024 22:26:43 +0300 Subject: [PATCH 063/125] 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<Pair<String, JsonElement>> = 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<MagixMessage> = 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 <altavir@gmail.com> Date: Mon, 18 Mar 2024 09:30:41 +0300 Subject: [PATCH 064/125] 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 <T : Any> property( - state: DeviceState<T>, + public fun <T : Any, S: DeviceState<T>> property( + state: S, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, - ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = + ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> = 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<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property( + ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property( DeviceState.external(this, metaConverter, readInterval, initialState, reader), descriptorBuilder, nameOverride, ) - /** - * Register a mutable property and provide a direct reader for it - */ - public fun <T : Any> mutableProperty( - state: MutableDeviceState<T>, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - nameOverride: String? = null, - ): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = - PropertyDelegateProvider { _: DeviceConstructor, property -> - val name = nameOverride ?: property.name - val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) - registerProperty(descriptor, state) - object : ReadWriteProperty<DeviceConstructor, T> { - 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 <T : Any> mutableProperty( metaConverter: MetaConverter<T>, @@ -119,22 +95,22 @@ public abstract class DeviceConstructor( initialState: T, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, - ): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = mutableProperty( + ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = 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 <T : Any> state( + public fun <T : Any> virtualProperty( metaConverter: MetaConverter<T>, initialState: T, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, callback: (T) -> Unit = {}, - ): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = mutableProperty( + ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = 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 <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(convert public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.convert(value) +public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value + +/** + * Collect values in a given [scope] + */ +public fun <T> DeviceState<T>.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<T> : DeviceState<T> { override var value: T } +public operator fun <T> MutableDeviceState<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) { + this.value = value +} + public var <T : Any> MutableDeviceState<T>.valueAsMeta: Meta get() = converter.convert(value) set(arg) { @@ -216,6 +229,9 @@ private class MutableExternalState<T>( } } +/** + * Create a [DeviceState] that regularly reads and caches an external value + */ public fun <T> DeviceState.Companion.external( scope: CoroutineScope, converter: MetaConverter<T>, 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 <T> Flow<T>.chunkedByPeriod(duration: Duration): Flow<List<T>> { + val collector: ArrayDeque<T> = ArrayDeque<T>() + return channelFlow { + coroutineScope { + launch { + while (isActive) { + delay(duration) + send(ArrayList(collector)) + collector.clear() + } + } + this@chunkedByPeriod.collect { + collector.add(it) + } + } + } +} + +private fun List<Instant>.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<ControlVisionPlugin> { 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<Double> 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 <altavir@gmail.com> Date: Mon, 18 Mar 2024 09:34:14 +0300 Subject: [PATCH 065/125] 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 <altavir@gmail.com> Date: Mon, 18 Mar 2024 17:15:39 +0300 Subject: [PATCH 066/125] 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 <T : Any, S: DeviceState<T>> property( + public fun <T, S: DeviceState<T>> 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 <T : Any> virtualProperty( + public fun <T> virtualProperty( metaConverter: MetaConverter<T>, 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<out Any>, + 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<out Any>) { + 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 <T> MutableDeviceState<T>.setValue(thisRef: Any?, property: this.value = value } -public var <T : Any> MutableDeviceState<T>.valueAsMeta: Meta +public var <T> MutableDeviceState<T>.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 <T> Flow<T>.chunkedByPeriod(duration: Duration): Flow<List<T>> { val collector: ArrayDeque<T> = ArrayDeque<T>() 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 <altavir@gmail.com> Date: Mon, 18 Mar 2024 17:18:31 +0300 Subject: [PATCH 067/125] 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 9a40d4f3404010b53b6ceacd17a346d42c98a6e0 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar <ovsyannikov.alexey95@gmail.com> Date: Wed, 20 Mar 2024 00:48:26 +0600 Subject: [PATCH 068/125] exclude ktor/rsocket/dataforge versions --- build.gradle.kts | 5 ----- controls-core/build.gradle.kts | 2 +- controls-magix/build.gradle.kts | 2 +- controls-ports-ktor/build.gradle.kts | 4 +--- controls-server/build.gradle.kts | 15 ++++++--------- .../controls-xodus/build.gradle.kts | 4 +--- demo/all-things/build.gradle.kts | 5 +---- demo/car/build.gradle.kts | 5 +---- demo/echo/build.gradle.kts | 5 +---- demo/many-devices/build.gradle.kts | 5 +---- gradle/libs.versions.toml | 17 +++++++++++++++++ magix/magix-rsocket/build.gradle.kts | 9 ++++----- magix/magix-server/build.gradle.kts | 5 ++--- .../magix-storage-xodus/build.gradle.kts | 4 +--- settings.gradle.kts | 11 +++++++++++ 15 files changed, 49 insertions(+), 49 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/build.gradle.kts b/build.gradle.kts index df7c664..02dde8d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,11 +6,6 @@ plugins { id("space.kscience.gradle.project") } -val dataforgeVersion: String by extra("0.6.2-dev-3") -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.2.0" diff --git a/controls-core/build.gradle.kts b/controls-core/build.gradle.kts index bbe32eb..05bb084 100644 --- a/controls-core/build.gradle.kts +++ b/controls-core/build.gradle.kts @@ -21,7 +21,7 @@ kscience { } useContextReceivers() dependencies { - api("space.kscience:dataforge-io:$dataforgeVersion") + api(libs.dataforge.io) api(spclibs.kotlinx.datetime) } } diff --git a/controls-magix/build.gradle.kts b/controls-magix/build.gradle.kts index 0296b8c..f0a6339 100644 --- a/controls-magix/build.gradle.kts +++ b/controls-magix/build.gradle.kts @@ -18,7 +18,7 @@ kscience { dependencies { api(projects.magix.magixApi) api(projects.controlsCore) - api("com.benasher44:uuid:0.8.0") + api(libs.uuid) } } diff --git a/controls-ports-ktor/build.gradle.kts b/controls-ports-ktor/build.gradle.kts index 4c8ad9a..1f1efda 100644 --- a/controls-ports-ktor/build.gradle.kts +++ b/controls-ports-ktor/build.gradle.kts @@ -9,11 +9,9 @@ description = """ Implementation of byte ports on top os ktor-io asynchronous API """.trimIndent() -val ktorVersion: String by rootProject.extra - dependencies { api(projects.controlsCore) - api("io.ktor:ktor-network:$ktorVersion") + api(spclibs.ktor.network) } readme{ diff --git a/controls-server/build.gradle.kts b/controls-server/build.gradle.kts index 43a9d61..5528b63 100644 --- a/controls-server/build.gradle.kts +++ b/controls-server/build.gradle.kts @@ -9,19 +9,16 @@ description = """ A combined Magix event loop server with web server for visualization. """.trimIndent() -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") + implementation(spclibs.ktor.server.cio) + implementation(spclibs.ktor.server.websockets) + implementation(spclibs.ktor.server.content.negotiation) + implementation(spclibs.ktor.serialization.kotlinx.json) + implementation(spclibs.ktor.server.html.builder) + implementation(spclibs.ktor.server.status.pages) } readme{ diff --git a/controls-storage/controls-xodus/build.gradle.kts b/controls-storage/controls-xodus/build.gradle.kts index 329f3ce..8cebd37 100644 --- a/controls-storage/controls-xodus/build.gradle.kts +++ b/controls-storage/controls-xodus/build.gradle.kts @@ -3,15 +3,13 @@ plugins { `maven-publish` } -val xodusVersion: String by rootProject.extra - description = """ An implementation of controls-storage on top of JetBrains Xodus. """.trimIndent() dependencies { api(projects.controlsStorage) - implementation("org.jetbrains.xodus:xodus-entity-store:$xodusVersion") + implementation(libs.xodus.entity.store) // implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion") // implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion") diff --git a/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts index 05d7395..9670921 100644 --- a/demo/all-things/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -10,9 +10,6 @@ repositories { maven("https://repo.kotlin.link") } -val ktorVersion: String by rootProject.extra -val rsocketVersion: String by rootProject.extra - dependencies { implementation(projects.controlsCore) //implementation(projects.controlsServer) @@ -22,7 +19,7 @@ dependencies { implementation(projects.magix.magixZmq) implementation(projects.controlsOpcua) - implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation(spclibs.ktor.client.cio) implementation("no.tornado:tornadofx:1.7.20") implementation("space.kscience:plotlykt-server:0.5.3") // implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") diff --git a/demo/car/build.gradle.kts b/demo/car/build.gradle.kts index 5d53f11..0d2de58 100644 --- a/demo/car/build.gradle.kts +++ b/demo/car/build.gradle.kts @@ -10,9 +10,6 @@ repositories { maven("https://repo.kotlin.link") } -val ktorVersion: String by rootProject.extra -val rsocketVersion: String by rootProject.extra - dependencies { implementation(projects.controlsCore) implementation(projects.magix.magixApi) @@ -24,7 +21,7 @@ dependencies { implementation(projects.magix.magixStorage.magixStorageXodus) // implementation(projects.controlsMongo) - implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation(spclibs.ktor.client.cio) implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.3.1") implementation("no.tornado:tornadofx:1.7.20") implementation("space.kscience:plotlykt-server:0.5.0") diff --git a/demo/echo/build.gradle.kts b/demo/echo/build.gradle.kts index 5563ba3..d10aee5 100644 --- a/demo/echo/build.gradle.kts +++ b/demo/echo/build.gradle.kts @@ -8,14 +8,11 @@ repositories { maven("https://repo.kotlin.link") } -val ktorVersion: String by rootProject.extra -val rsocketVersion: String by rootProject.extra - dependencies { implementation(projects.magix.magixServer) implementation(projects.magix.magixRsocket) implementation(projects.magix.magixZmq) - implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation(spclibs.ktor.client.cio) implementation("ch.qos.logback:logback-classic:1.2.11") } diff --git a/demo/many-devices/build.gradle.kts b/demo/many-devices/build.gradle.kts index 7248e42..250b5e7 100644 --- a/demo/many-devices/build.gradle.kts +++ b/demo/many-devices/build.gradle.kts @@ -9,16 +9,13 @@ repositories { maven("https://repo.kotlin.link") } -val ktorVersion: String by rootProject.extra -val rsocketVersion: String by rootProject.extra - dependencies { implementation(projects.magix.magixServer) implementation(projects.controlsMagix) implementation(projects.magix.magixRsocket) implementation(projects.magix.magixZmq) - implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation(spclibs.ktor.client.cio) implementation("space.kscience:plotlykt-server:0.6.0") implementation(spclibs.logback.classic) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..502a400 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,17 @@ +[versions] + +dataforge = "0.6.2-dev-3" +rsocket = "0.15.4" +xodus = "2.0.1" + +uuid = "0.8.0" + +[libraries] + +dataforge-io = { module = "space.kscience:dataforge-io", version.ref = "dataforge" } +uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } +xodus-entity-store = { module = "org.jetbrains.xodus:xodus-entity-store", version.ref = "xodus" } +rsocket-ktor-client = { module = "io.rsocket.kotlin:rsocket-ktor-client", version.ref = "rsocket" } +rsocket-ktor-server = { module = "io.rsocket.kotlin:rsocket-ktor-server", version.ref = "rsocket" } +rsocket-transport-ktor-tcp = { module = "io.rsocket.kotlin:rsocket-transport-ktor-tcp", version.ref = "rsocket" } + diff --git a/magix/magix-rsocket/build.gradle.kts b/magix/magix-rsocket/build.gradle.kts index b2046bb..ca016ce 100644 --- a/magix/magix-rsocket/build.gradle.kts +++ b/magix/magix-rsocket/build.gradle.kts @@ -10,7 +10,6 @@ description = """ """.trimIndent() val ktorVersion: String by rootProject.extra -val rsocketVersion: String by rootProject.extra kscience { jvm() @@ -21,11 +20,11 @@ kscience { } dependencies { api(projects.magix.magixApi) - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.rsocket.kotlin:rsocket-ktor-client:$rsocketVersion") + implementation(spclibs.ktor.client.core) + implementation(libs.rsocket.ktor.client) } dependencies(jvmMain) { - implementation("io.rsocket.kotlin:rsocket-transport-ktor-tcp:$rsocketVersion") + implementation(libs.rsocket.transport.ktor.tcp) } } @@ -33,7 +32,7 @@ kotlin { sourceSets { getByName("linuxX64Main") { dependencies { - implementation("io.rsocket.kotlin:rsocket-transport-ktor-tcp:$rsocketVersion") + implementation(libs.rsocket.transport.ktor.tcp) } } } diff --git a/magix/magix-server/build.gradle.kts b/magix/magix-server/build.gradle.kts index cb63049..f00b81e 100644 --- a/magix/magix-server/build.gradle.kts +++ b/magix/magix-server/build.gradle.kts @@ -17,7 +17,6 @@ kscience { } val dataforgeVersion: String by rootProject.extra -val rsocketVersion: String by rootProject.extra val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion dependencies{ @@ -28,8 +27,8 @@ dependencies{ api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") api("io.ktor:ktor-server-html-builder:$ktorVersion") - api("io.rsocket.kotlin:rsocket-ktor-server:$rsocketVersion") - api("io.rsocket.kotlin:rsocket-transport-ktor-tcp:$rsocketVersion") + api(libs.rsocket.ktor.server) + api(libs.rsocket.transport.ktor.tcp) } readme{ diff --git a/magix/magix-storage/magix-storage-xodus/build.gradle.kts b/magix/magix-storage/magix-storage-xodus/build.gradle.kts index ed1dc32..f50abe3 100644 --- a/magix/magix-storage/magix-storage-xodus/build.gradle.kts +++ b/magix/magix-storage/magix-storage-xodus/build.gradle.kts @@ -3,15 +3,13 @@ plugins { `maven-publish` } -val xodusVersion: String by rootProject.extra - kscience { useCoroutines() } dependencies { api(projects.magix.magixStorage) - implementation("org.jetbrains.xodus:xodus-entity-store:$xodusVersion") + implementation(libs.xodus.entity.store) // implementation("org.jetbrains.xodus:dnq:2.0.0") testImplementation(spclibs.kotlinx.coroutines.test) diff --git a/settings.gradle.kts b/settings.gradle.kts index 20ac44e..626da41 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,17 @@ dependencyResolutionManagement { versionCatalogs { create("spclibs") { from("space.kscience:version-catalog:$toolsVersion") + + library("ktor-client-core", "io.ktor", "ktor-client-core").versionRef("ktor") + library("ktor-client-cio", "io.ktor", "ktor-client-cio").versionRef("ktor") + library("ktor-network", "io.ktor", "ktor-network").versionRef("ktor") + library("ktor-serialization-kotlinx-json", "io.ktor", "ktor-serialization-kotlinx-json").versionRef("ktor") + + library("ktor-server-cio", "io.ktor", "ktor-server-cio").versionRef("ktor") + library("ktor-server-websockets", "io.ktor", "ktor-server-websockets").versionRef("ktor") + library("ktor-server-content-negotiation", "io.ktor", "ktor-server-content-negotiation").versionRef("ktor") + library("ktor-server-html-builder", "io.ktor", "ktor-server-html-builder").versionRef("ktor") + library("ktor-server-status-pages", "io.ktor", "ktor-server-status-pages").versionRef("ktor") } } } From 89656291517dfaccff3a32a15018caad93b7da68 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar <ovsyannikov.alexey95@gmail.com> Date: Wed, 20 Mar 2024 01:08:04 +0600 Subject: [PATCH 069/125] complete dependencies extraction --- controls-serial/build.gradle.kts | 2 +- demo/all-things/build.gradle.kts | 4 +-- demo/car/build.gradle.kts | 14 ++++---- demo/echo/build.gradle.kts | 2 +- demo/many-devices/build.gradle.kts | 2 +- demo/motors/build.gradle.kts | 2 +- gradle/libs.versions.toml | 32 +++++++++++++++++++ magix/magix-java-endpoint/build.gradle.kts | 2 +- magix/magix-mqtt/build.gradle.kts | 2 +- magix/magix-rabbit/build.gradle.kts | 2 +- .../magix-storage-mongo/build.gradle.kts | 4 +-- settings.gradle.kts | 2 ++ 12 files changed, 51 insertions(+), 19 deletions(-) diff --git a/controls-serial/build.gradle.kts b/controls-serial/build.gradle.kts index a9afc41..d44a029 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(libs.jSerialComm) } readme{ diff --git a/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts index 9670921..b6074ed 100644 --- a/demo/all-things/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -20,8 +20,8 @@ dependencies { implementation(projects.controlsOpcua) implementation(spclibs.ktor.client.cio) - implementation("no.tornado:tornadofx:1.7.20") - implementation("space.kscience:plotlykt-server:0.5.3") + implementation(libs.tornadofx) + implementation(libs.plotlykt.server) // implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") implementation(spclibs.logback.classic) } diff --git a/demo/car/build.gradle.kts b/demo/car/build.gradle.kts index 0d2de58..b8a7fb6 100644 --- a/demo/car/build.gradle.kts +++ b/demo/car/build.gradle.kts @@ -22,13 +22,13 @@ dependencies { // implementation(projects.controlsMongo) implementation(spclibs.ktor.client.cio) - implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.3.1") - implementation("no.tornado:tornadofx:1.7.20") - implementation("space.kscience:plotlykt-server:0.5.0") - implementation("ch.qos.logback:logback-classic:1.2.11") - implementation("org.jetbrains.xodus:xodus-entity-store:1.3.232") - implementation("org.jetbrains.xodus:xodus-environment:1.3.232") - implementation("org.jetbrains.xodus:xodus-vfs:1.3.232") + implementation(spclibs.kotlinx.datetime) + implementation(libs.tornadofx) + implementation(libs.plotlykt.server) + implementation(libs.logback.classic) + implementation(libs.xodus.entity.store) + implementation(libs.xodus.environment) + implementation(libs.xodus.vfs) // implementation("org.litote.kmongo:kmongo-coroutine-serialization:4.4.0") } diff --git a/demo/echo/build.gradle.kts b/demo/echo/build.gradle.kts index d10aee5..873220e 100644 --- a/demo/echo/build.gradle.kts +++ b/demo/echo/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { implementation(projects.magix.magixZmq) implementation(spclibs.ktor.client.cio) - implementation("ch.qos.logback:logback-classic:1.2.11") + implementation(libs.logback.classic) } kotlin{ jvmToolchain(11) diff --git a/demo/many-devices/build.gradle.kts b/demo/many-devices/build.gradle.kts index 250b5e7..64fbfe2 100644 --- a/demo/many-devices/build.gradle.kts +++ b/demo/many-devices/build.gradle.kts @@ -16,7 +16,7 @@ dependencies { implementation(projects.magix.magixZmq) implementation(spclibs.ktor.client.cio) - implementation("space.kscience:plotlykt-server:0.6.0") + implementation(libs.plotlykt.server) implementation(spclibs.logback.classic) } diff --git a/demo/motors/build.gradle.kts b/demo/motors/build.gradle.kts index 8626c72..dd64935 100644 --- a/demo/motors/build.gradle.kts +++ b/demo/motors/build.gradle.kts @@ -25,5 +25,5 @@ val dataforgeVersion: String by extra dependencies { implementation(project(":controls-ports-ktor")) implementation(projects.controlsMagix) - implementation("no.tornado:tornadofx:1.7.20") + implementation(libs.tornadofx) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 502a400..e39b1d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,12 +6,44 @@ xodus = "2.0.1" uuid = "0.8.0" +fazecast = "2.10.3" + +tornadofx = "1.7.20" + +plotlykt = "0.5.3" + +logback = "1.2.11" + +hivemq = "1.3.1" + +rabbitmq = "5.14.2" + +kmongo = "4.5.1" + [libraries] dataforge-io = { module = "space.kscience:dataforge-io", version.ref = "dataforge" } + uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } + xodus-entity-store = { module = "org.jetbrains.xodus:xodus-entity-store", version.ref = "xodus" } +xodus-environment = { module = "org.jetbrains.xodus:xodus-environment", version.ref = "xodus" } +xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" } + rsocket-ktor-client = { module = "io.rsocket.kotlin:rsocket-ktor-client", version.ref = "rsocket" } rsocket-ktor-server = { module = "io.rsocket.kotlin:rsocket-ktor-server", version.ref = "rsocket" } rsocket-transport-ktor-tcp = { module = "io.rsocket.kotlin:rsocket-transport-ktor-tcp", version.ref = "rsocket" } +jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "fazecast" } + +tornadofx = { module = "no.tornado:tornadofx", version.ref = "tornadofx" } + +plotlykt-server = { module = "space.kscience:plotlykt-server", version.ref = "plotlykt" } + +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } + +hivemq-mqtt-client = { module = "com.hivemq:hivemq-mqtt-client", version.ref = "hivemq" } + +rabbitmq-amqp-client = { module = "com.rabbitmq:amqp-client", version.ref = "rabbitmq" } + +kmongo-coroutine-serialization = { module = "org.litote.kmongo:kmongo-coroutine-serialization", version.ref = "kmongo" } diff --git a/magix/magix-java-endpoint/build.gradle.kts b/magix/magix-java-endpoint/build.gradle.kts index ff51835..ce20b5d 100644 --- a/magix/magix-java-endpoint/build.gradle.kts +++ b/magix/magix-java-endpoint/build.gradle.kts @@ -14,7 +14,7 @@ description = """ dependencies { implementation(project(":magix:magix-rsocket")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:${KScienceVersions.coroutinesVersion}") + implementation(spclibs.kotlinx.coroutines.jdk9) } //java { diff --git a/magix/magix-mqtt/build.gradle.kts b/magix/magix-mqtt/build.gradle.kts index e7037e6..5261a00 100644 --- a/magix/magix-mqtt/build.gradle.kts +++ b/magix/magix-mqtt/build.gradle.kts @@ -9,7 +9,7 @@ description = """ dependencies { api(projects.magix.magixApi) - implementation("com.hivemq:hivemq-mqtt-client:1.3.1") + implementation(libs.hivemq.mqtt.client) implementation(spclibs.kotlinx.coroutines.jdk8) } diff --git a/magix/magix-rabbit/build.gradle.kts b/magix/magix-rabbit/build.gradle.kts index 3d7bc4d..d5472a5 100644 --- a/magix/magix-rabbit/build.gradle.kts +++ b/magix/magix-rabbit/build.gradle.kts @@ -9,7 +9,7 @@ description = """ dependencies { api(projects.magix.magixApi) - implementation("com.rabbitmq:amqp-client:5.14.2") + implementation(libs.rabbitmq.amqp.client) } readme{ diff --git a/magix/magix-storage/magix-storage-mongo/build.gradle.kts b/magix/magix-storage/magix-storage-mongo/build.gradle.kts index 51ed9f4..b72a2ec 100644 --- a/magix/magix-storage/magix-storage-mongo/build.gradle.kts +++ b/magix/magix-storage/magix-storage-mongo/build.gradle.kts @@ -3,11 +3,9 @@ plugins { `maven-publish` } -val kmongoVersion = "4.5.1" - dependencies { implementation(projects.controlsStorage) - implementation("org.litote.kmongo:kmongo-coroutine-serialization:$kmongoVersion") + implementation(libs.kmongo.coroutine.serialization) } readme{ diff --git a/settings.gradle.kts b/settings.gradle.kts index 626da41..0753e55 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,6 +36,8 @@ dependencyResolutionManagement { create("spclibs") { from("space.kscience:version-catalog:$toolsVersion") + library("kotlinx-coroutines-jdk9", "org.jetbrains.kotlinx", "kotlinx-coroutines-jdk9").versionRef("kotlinx-coroutines") + library("ktor-client-core", "io.ktor", "ktor-client-core").versionRef("ktor") library("ktor-client-cio", "io.ktor", "ktor-client-cio").versionRef("ktor") library("ktor-network", "io.ktor", "ktor-network").versionRef("ktor") From d91296c47d73358b3d5e0ebcb018240902168cf7 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 25 Mar 2024 15:48:23 +0300 Subject: [PATCH 070/125] 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<ByteArray> = 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<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray> } } +private fun Flow<ByteArray>.withFixedMessageSize(messageSize: Int): Flow<ByteArray> { + 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<DeviceMessage> = 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<MassDevice>(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<String, Duration>() + val max = HashMap<String, Duration>() + + 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<String, Duration>() - val max = HashMap<String, Duration>() - - 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 <altavir@gmail.com> Date: Sun, 31 Mar 2024 16:13:02 +0300 Subject: [PATCH 071/125] 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<T> : AutoCloseable { + /** + * Send an object to the socket + */ + public suspend fun send(data: T) + + /** + * Flow of objects received from socket + */ + public fun subscribe(): Flow<T> + + /** + * 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 <T> AsynchronousSocket<T>.sendFlow(flow: Flow<T>) { + 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<T> : AutoCloseable { - /** - * Send an object to the socket - */ - public suspend fun send(data: T) - - /** - * Flow of objects received from socket - */ - public fun receiving(): Flow<T> - 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 <T> Socket<T>.connectInput(scope: CoroutineScope, flow: Flow<T>): 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<ByteArray> + +/** + * 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<ByteArray>(meta["outgoing.capacity"].int?:100) + private val incoming = Channel<ByteArray>(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<ByteArray> = 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<ByteArray> - -/** - * A specialized factory for [Port] - */ -@DfType(PortFactory.TYPE) -public interface PortFactory : Factory<Port> { - 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<ByteArray>(100) - private val incoming = Channel<ByteArray>(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<ByteArray> = 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<ByteArray> = 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>(PortFactory.TYPE) + private val synchronousPortFactories by lazy { + context.gather<Factory<SynchronousPort>>(SYNCHRONOUS_PORT_TYPE) } - private val portCache = mutableMapOf<Meta, Port>() + private val asynchronousPortFactories by lazy { + context.gather<Factory<AsynchronousPort>>(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<Ports> { 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 <R> respond(data: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R = mutex.withLock { - port.send(data) - transform(port.receiving()) + public suspend fun <R> respond( + request: ByteArray, + transform: suspend Flow<ByteArray>.() -> 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 <R> respond( + request: ByteArray, + transform: suspend Flow<ByteArray>.() -> 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<ByteArray>.withStringDelimiter(delimiter: String): Flow<String> /** * A flow of delimited phrases */ -public fun Port.delimitedIncoming(delimiter: ByteArray): Flow<ByteArray> = receiving().withDelimiter(delimiter) +public fun AsynchronousPort.delimitedIncoming(delimiter: ByteArray): Flow<ByteArray> = subscribe().withDelimiter(delimiter) /** * A flow of delimited phrases with string content */ -public fun Port.stringsDelimitedIncoming(delimiter: String): Flow<String> = receiving().withStringDelimiter(delimiter) +public fun AsynchronousPort.stringsDelimitedIncoming(delimiter: String): Flow<String> = 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<ByteChannel> = 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<ByteChannel> = 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<AsynchronousPort> { - 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<AsynchronousPort> { - 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<Name, Any> = 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<AsynchronousPort> { + + + 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>() ?: 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<Name, Any> = 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>() ?: 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 <R> respond( + request: ByteArray, + transform: suspend Flow<ByteArray>.() -> R, + ): R = mutex.withLock { + serial.drain() + serial.write(request) + flow<ByteArray> { + 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<SynchronousPort> { + + + 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>() ?: 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<Name, Any> = 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<AsynchronousPort> { - 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<ByteWriteChannel> = scope.async { + private val writeChannel: Deferred<ByteWriteChannel> = 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<AsynchronousPort> { - 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<AsynchronousPort> { + + 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<Name, Any> = when(target){ - PortFactory.TYPE -> mapOf(Name.EMPTY to JSerialCommPort) + override fun content(target: String): Map<Name, Any> = 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 <R> respond(request: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R = + mutex.withLock { + comPort.flushIOBuffers() + comPort.writeBytes(request, request.size) + flow<ByteArray> { + 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<SynchronousPort> { + + 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<MksPdr900Devi private val portDelegate = lazy { val ports = context.request(Ports) - ports.buildPort(meta["port"] ?: error("Port is not defined in device configuration")).synchronous() + ports.buildSynchronousPort(meta["port"] ?: error("Port is not defined in device configuration")) } private val port: SynchronousPort by portDelegate 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 5d05993..69dfb79 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 @@ -25,10 +25,10 @@ import kotlin.time.Duration.Companion.milliseconds class PiMotionMasterDevice( context: Context, - private val portFactory: PortFactory = KtorTcpPort, + private val portFactory: Factory<AsynchronousPort> = KtorTcpPort, ) : DeviceBySpec<PiMotionMasterDevice>(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<ByteArray> { +abstract class VirtualDevice(val scope: CoroutineScope) : AsynchronousSocket<ByteArray> { protected abstract suspend fun evaluateRequest(request: ByteArray) @@ -40,33 +41,42 @@ abstract class VirtualDevice(val scope: CoroutineScope) : Socket<ByteArray> { toRespond.send(response) } - override fun receiving(): Flow<ByteArray> = toRespond.receiveAsFlow() + override fun subscribe(): Flow<ByteArray> = 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? [{<AxisID>}] 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<String>): 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 <altavir@gmail.com> Date: Sun, 31 Mar 2024 16:33:22 +0300 Subject: [PATCH 072/125] 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<ByteArray>.() -> 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 <R> respond(request: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R = - mutex.withLock { + override suspend fun <R> respond( + request: ByteArray, + transform: suspend Flow<ByteArray>.() -> R, + ): R = mutex.withLock { + comPort.flushIOBuffers() + comPort.writeBytes(request, request.size) + flow<ByteArray> { + 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<ByteArray> { - 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<SynchronousPort> { From 977500223d206eb878b52f0ed149a2faeca96433 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 7 Apr 2024 10:07:23 +0300 Subject: [PATCH 073/125] 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<String, PlcResponseCode> + 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<String, PlcResponseCode>.isOK get() = values.all { it == PlcResponseCode.OK } +public class PlcException(public val codes: Map<String, PlcResponseCode>) : 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<String, MutableList<PlcBrowseItem>> { + 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<Plc4XDevice>(context, meta) { + override val properties: Map<String, DevicePropertySpec<Plc4XDevice, *>> + get() = TODO("Not yet implemented") + override val actions: Map<String, DeviceActionSpec<Plc4XDevice, *, *>> = 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<String> + 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<String> = 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 <altavir@gmail.com> Date: Tue, 9 Apr 2024 15:13:41 +0300 Subject: [PATCH 074/125] 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<Instant>.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) From 23bceed89da647fd461dd74aa5ca243ee6346720 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar <ovsyannikov.alexey95@gmail.com> Date: Mon, 29 Apr 2024 17:22:56 +0600 Subject: [PATCH 075/125] update dependencies --- build.gradle.kts | 1 + controls-modbus/build.gradle.kts | 2 +- controls-opcua/build.gradle.kts | 8 +++----- controls-pi/build.gradle.kts | 8 ++++---- gradle/libs.versions.toml | 26 ++++++++++++++++++++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- 6 files changed, 36 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 02dde8d..2463418 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ import space.kscience.gradle.useSPCTeam plugins { id("space.kscience.gradle.project") + alias(libs.plugins.versions) } allprojects { diff --git a/controls-modbus/build.gradle.kts b/controls-modbus/build.gradle.kts index aee64d5..2f280b6 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(libs.j2mod) } readme{ diff --git a/controls-opcua/build.gradle.kts b/controls-opcua/build.gradle.kts index e7e1da8..686ae62 100644 --- a/controls-opcua/build.gradle.kts +++ b/controls-opcua/build.gradle.kts @@ -11,15 +11,13 @@ description = """ val ktorVersion: String by rootProject.extra -val miloVersion: String = "0.6.10" - dependencies { api(projects.controlsCore) api(spclibs.kotlinx.coroutines.jdk8) - api("org.eclipse.milo:sdk-client:$miloVersion") - api("org.eclipse.milo:bsd-parser:$miloVersion") - api("org.eclipse.milo:sdk-server:$miloVersion") + api(libs.milo.client) + api(libs.milo.parser) + api(libs.milo.server) testImplementation(spclibs.kotlinx.coroutines.test) } diff --git a/controls-pi/build.gradle.kts b/controls-pi/build.gradle.kts index a763396..997fcf2 100644 --- a/controls-pi/build.gradle.kts +++ b/controls-pi/build.gradle.kts @@ -9,8 +9,8 @@ description = """ 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(libs.pi4j.ktx) // Kotlin DSL + api(libs.pi4j.core) + api(libs.pi4j.plugin.raspberrypi) + api(libs.pi4j.plugin.pigpio) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e39b1d9..da20aff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,15 @@ rabbitmq = "5.14.2" kmongo = "4.5.1" +j2mod = "3.2.1" + +milo = "0.6.12" + +pi4j = "2.3.0" +pi4j-ktx = "2.4.0" + +versions = "0.51.0" + [libraries] dataforge-io = { module = "space.kscience:dataforge-io", version.ref = "dataforge" } @@ -46,4 +55,21 @@ hivemq-mqtt-client = { module = "com.hivemq:hivemq-mqtt-client", version.ref = " rabbitmq-amqp-client = { module = "com.rabbitmq:amqp-client", version.ref = "rabbitmq" } +j2mod = { module = "com.ghgande:j2mod", version.ref = "j2mod" } + kmongo-coroutine-serialization = { module = "org.litote.kmongo:kmongo-coroutine-serialization", version.ref = "kmongo" } + +milo-client = { module = "org.eclipse.milo:sdk-client", version.ref = "milo" } +milo-parser = { module = "org.eclipse.milo:bsd-parser", version.ref = "milo" } +milo-server = { module = "org.eclipse.milo:sdk-server", version.ref = "milo" } + +pi4j-ktx = { module = "com.pi4j:pi4j-ktx", version.ref = "pi4j-ktx" } +pi4j-core = { module = "com.pi4j:pi4j-core", version.ref = "pi4j" } +pi4j-plugin-raspberrypi = { module = "com.pi4j:pi4j-plugin-raspberrypi", version.ref = "pi4j" } +pi4j-plugin-pigpio = { module = "com.pi4j:pi4j-plugin-pigpio", version.ref = "pi4j" } + +# Buildscript + +[plugins] + +versions = { id = "com.github.ben-manes.versions", version.ref = "versions" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 17655d0..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.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From e729cb1a794db44955d68b2ec8f676e4b65736ff Mon Sep 17 00:00:00 2001 From: InsanusMokrassar <ovsyannikov.alexey95@gmail.com> Date: Mon, 29 Apr 2024 18:38:14 +0600 Subject: [PATCH 076/125] upfixes --- controls-core/build.gradle.kts | 2 -- controls-jupyter/build.gradle.kts | 4 +--- controls-plc4x/build.gradle.kts | 4 +--- controls-server/build.gradle.kts | 24 ++++++++++++++---------- controls-vision/build.gradle.kts | 10 ++++------ gradle/libs.versions.toml | 14 +++++++++++++- magix/magix-utils/build.gradle.kts | 4 +--- 7 files changed, 34 insertions(+), 28 deletions(-) diff --git a/controls-core/build.gradle.kts b/controls-core/build.gradle.kts index 9b8c9f9..9624b35 100644 --- a/controls-core/build.gradle.kts +++ b/controls-core/build.gradle.kts @@ -9,8 +9,6 @@ description = """ Core interfaces for building a device server """.trimIndent() -val dataforgeVersion: String by rootProject.extra - kscience { jvm() js() diff --git a/controls-jupyter/build.gradle.kts b/controls-jupyter/build.gradle.kts index c8486bd..21a3a82 100644 --- a/controls-jupyter/build.gradle.kts +++ b/controls-jupyter/build.gradle.kts @@ -3,8 +3,6 @@ plugins { `maven-publish` } -val visionforgeVersion: String by rootProject.extra - kscience { fullStack("js/controls-jupyter.js") useKtor() @@ -12,7 +10,7 @@ kscience { jupyterLibrary("space.kscience.controls.jupyter.ControlsJupyter") dependencies { implementation(projects.controlsVision) - implementation("space.kscience:visionforge-jupyter:$visionforgeVersion") + implementation(libs.visionforge.jupiter) } jvmMain { implementation(spclibs.logback.classic) diff --git a/controls-plc4x/build.gradle.kts b/controls-plc4x/build.gradle.kts index e7b063c..01f7f68 100644 --- a/controls-plc4x/build.gradle.kts +++ b/controls-plc4x/build.gradle.kts @@ -5,8 +5,6 @@ plugins { `maven-publish` } -val plc4xVersion = "0.12.0" - description = """ A plugin for Controls-kt device server on top of plc4x library """.trimIndent() @@ -15,7 +13,7 @@ kscience { jvm() jvmMain { api(projects.controlsCore) - api("org.apache.plc4x:plc4j-spi:$plc4xVersion") + api(libs.plc4j.spi) } } diff --git a/controls-server/build.gradle.kts b/controls-server/build.gradle.kts index 3581a71..7a57d57 100644 --- a/controls-server/build.gradle.kts +++ b/controls-server/build.gradle.kts @@ -9,16 +9,20 @@ description = """ A combined Magix event loop server with web server for visualization. """.trimIndent() -dependencies { - implementation(projects.controlsCore) - implementation(projects.controlsPortsKtor) - implementation(projects.magix.magixServer) - implementation(spclibs.ktor.server.cio) - implementation(spclibs.ktor.server.websockets) - implementation(spclibs.ktor.server.content.negotiation) - implementation(spclibs.ktor.serialization.kotlinx.json) - implementation(spclibs.ktor.server.html.builder) - implementation(spclibs.ktor.server.status.pages) + +kscience { + jvm() + dependencies { + implementation(projects.controlsCore) + implementation(projects.controlsPortsKtor) + implementation(projects.magix.magixServer) + implementation(spclibs.ktor.server.cio) + implementation(spclibs.ktor.server.websockets) + implementation(spclibs.ktor.server.content.negotiation) + implementation(spclibs.ktor.serialization.kotlinx.json) + implementation(spclibs.ktor.server.html.builder) + implementation(spclibs.ktor.server.status.pages) + } } readme{ diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts index 9395b92..c36cdce 100644 --- a/controls-vision/build.gradle.kts +++ b/controls-vision/build.gradle.kts @@ -7,8 +7,6 @@ description = """ Dashboard and visualization extensions for devices """.trimIndent() -val visionforgeVersion: String by rootProject.extra - kscience { fullStack("js/controls-vision.js") useKtor() @@ -16,15 +14,15 @@ kscience { dependencies { api(projects.controlsCore) api(projects.controlsConstructor) - api("space.kscience:visionforge-plotly:$visionforgeVersion") - api("space.kscience:visionforge-markdown:$visionforgeVersion") + api(libs.visionforge.plotly) + api(libs.visionforge.markdown) // api("space.kscience:tables-kt:0.2.1") // api("space.kscience:visionforge-tables:$visionforgeVersion") } jvmMain{ - api("space.kscience:visionforge-server:$visionforgeVersion") - api("io.ktor:ktor-server-cio") + api(libs.visionforge.server) + api(spclibs.ktor.server.cio) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da20aff..242a120 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -dataforge = "0.6.2-dev-3" +dataforge = "0.8.0" rsocket = "0.15.4" xodus = "2.0.1" @@ -27,11 +27,16 @@ milo = "0.6.12" pi4j = "2.3.0" pi4j-ktx = "2.4.0" +plc4j = "0.12.0" + +visionforge = "0.4.1" + versions = "0.51.0" [libraries] dataforge-io = { module = "space.kscience:dataforge-io", version.ref = "dataforge" } +dataforge-meta = { module = "space.kscience:dataforge-meta", version.ref = "dataforge" } uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } @@ -68,6 +73,13 @@ pi4j-core = { module = "com.pi4j:pi4j-core", version.ref = "pi4j" } pi4j-plugin-raspberrypi = { module = "com.pi4j:pi4j-plugin-raspberrypi", version.ref = "pi4j" } pi4j-plugin-pigpio = { module = "com.pi4j:pi4j-plugin-pigpio", version.ref = "pi4j" } +plc4j-spi = { module = "org.apache.plc4x:plc4j-spi", version.ref = "plc4j" } + +visionforge-jupiter = { module = "space.kscience:visionforge-jupyter", version.ref = "visionforge" } +visionforge-plotly = { module = "space.kscience:visionforge-plotly", version.ref = "visionforge" } +visionforge-markdown = { module = "space.kscience:visionforge-markdown", version.ref = "visionforge" } +visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" } + # Buildscript [plugins] diff --git a/magix/magix-utils/build.gradle.kts b/magix/magix-utils/build.gradle.kts index b22b5f0..9bec805 100644 --- a/magix/magix-utils/build.gradle.kts +++ b/magix/magix-utils/build.gradle.kts @@ -9,8 +9,6 @@ description = """ Common utilities and services for Magix endpoints. """.trimIndent() -val dataforgeVersion: String by rootProject.extra - kscience { jvm() js() @@ -18,7 +16,7 @@ kscience { useSerialization() commonMain { api(projects.magix.magixApi) - api("space.kscience:dataforge-meta:$dataforgeVersion") + api(libs.dataforge.meta) } } From f974483a41cf480e2ed366f688ac4e0326db9271 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 29 Apr 2024 18:28:14 +0300 Subject: [PATCH 077/125] Update plotly version --- .../kotlin/space/kscience/controls/spec/DeviceBase.kt | 1 + gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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 dcb48b4..e8b9210 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,6 +72,7 @@ public abstract class DeviceBase<D : Device>( onBufferOverflow = BufferOverflow.DROP_OLDEST ) + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) override val coroutineContext: CoroutineContext = context.newCoroutineContext( SupervisorJob(context.coroutineContext[Job]) + CoroutineName("Device $this") + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 242a120..601b9d1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ fazecast = "2.10.3" tornadofx = "1.7.20" -plotlykt = "0.5.3" +plotlykt = "0.7.0" logback = "1.2.11" From 381da970bf1e2fd7a8b288c1c2103017b5149ec8 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Fri, 10 May 2024 11:33:00 +0300 Subject: [PATCH 078/125] make device stop suspended to properly await for lifecycle event. Add capabilities to Constructor --- build.gradle.kts | 2 +- controls-constructor/build.gradle.kts | 7 +- .../constructor/ConstructorBinding.kt | 26 ++ .../controls/constructor/DeviceConstructor.kt | 230 +++++++++++------- .../controls/constructor/DeviceGroup.kt | 51 ++-- .../controls/constructor/DeviceState.kt | 205 +++------------- .../controls/constructor/TimerState.kt | 41 ++++ .../controls/constructor/boundState.kt | 103 ++++++++ .../controls/constructor/customState.kt | 4 + .../controls/constructor/externalState.kt | 70 ++++++ .../controls/constructor/flowState.kt | 23 ++ .../controls/constructor/internalState.kt | 42 ++++ .../constructor/{ => library}/Drive.kt | 8 +- .../constructor/{ => library}/LimitSwitch.kt | 5 +- .../constructor/{ => library}/PidRegulator.kt | 11 +- .../constructor/{ => library}/Regulator.kt | 2 +- .../controls/constructor/DeviceGroupTest.kt | 43 ++++ .../controls/constructor/TimerTest.kt | 22 ++ .../space/kscience/controls/api/Device.kt | 4 +- .../kscience/controls/api/descriptors.kt | 19 +- .../kscience/controls/manager/ClockManager.kt | 8 +- .../controls/manager/DeviceManager.kt | 7 +- .../kscience/controls/spec/DeviceBase.kt | 39 ++- .../kscience/controls/spec/DeviceBySpec.kt | 2 +- .../kscience/controls/spec/DeviceSpec.kt | 25 ++ .../kscience/controls/spec/converters.kt | 41 ++++ .../controls/spec/deviceExtensions.kt | 50 ++-- .../space/kscience/controls/spec/fromSpec.kt | 2 - .../space/kscience/controls/spec/misc.kt | 22 -- .../kscience/controls/spec/fromSpec.jvm.kt | 5 +- controls-modbus/build.gradle.kts | 12 +- .../controls/modbus/DeviceProcessImage.kt | 0 .../kscience/controls/modbus/ModbusDevice.kt | 0 .../controls/modbus/ModbusDeviceBySpec.kt | 2 +- .../controls/modbus/ModbusRegistryMap.kt | 0 .../opcua/client/OpcUaDeviceBySpec.kt | 2 +- controls-pi/build.gradle.kts | 19 +- .../controls/pi/AsynchronousPiPort.kt | 0 .../space/kscience/controls/pi/PiPlugin.kt | 0 .../kscience/controls/pi/SynchronousPiPort.kt | 0 controls-ports-ktor/build.gradle.kts | 11 +- .../controls/ports/KtorPortsPlugin.kt | 0 .../kscience/controls/ports/KtorTcpPort.kt | 0 .../kscience/controls/ports/KtorUdpPort.kt | 0 controls-serial/build.gradle.kts | 11 +- .../controls/serial/AsynchronousSerialPort.kt | 0 .../controls/serial/SerialPortPlugin.kt | 0 .../controls/serial/SynchronousSerialPort.kt | 0 .../controls-xodus/build.gradle.kts | 15 +- .../xodus/XodusDeviceMessageStorage.kt | 0 .../kotlin/PropertyHistoryTest.kt | 0 .../controls/demo/DemoControllerView.kt | 7 +- .../controls/demo/car/VirtualCarController.kt | 7 +- demo/constructor/src/jvmMain/kotlin/main.kt | 18 +- magix/magix-server/build.gradle.kts | 36 +-- .../magix/server/RSocketMagixFlowPlugin.kt | 0 .../kscience/magix/server/magixModule.kt | 0 .../space/kscience/magix/server/server.kt | 0 .../kotlin/space/kscience/magix/server/sse.kt | 0 59 files changed, 818 insertions(+), 441 deletions(-) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorBinding.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt rename controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/{ => library}/Drive.kt (91%) rename controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/{ => library}/LimitSwitch.kt (83%) rename controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/{ => library}/PidRegulator.kt (83%) rename controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/{ => library}/Regulator.kt (92%) create mode 100644 controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/DeviceGroupTest.kt create mode 100644 controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/TimerTest.kt create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt delete mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt rename controls-modbus/src/{main => jvmMain}/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt (100%) rename controls-modbus/src/{main => jvmMain}/kotlin/space/kscience/controls/modbus/ModbusDevice.kt (100%) rename controls-modbus/src/{main => jvmMain}/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt (97%) rename controls-modbus/src/{main => jvmMain}/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt (100%) rename controls-pi/src/{main => jvmMain}/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt (100%) rename controls-pi/src/{main => jvmMain}/kotlin/space/kscience/controls/pi/PiPlugin.kt (100%) rename controls-pi/src/{main => jvmMain}/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt (100%) rename controls-ports-ktor/src/{main => jvmMain}/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt (100%) rename controls-ports-ktor/src/{main => jvmMain}/kotlin/space/kscience/controls/ports/KtorTcpPort.kt (100%) rename controls-ports-ktor/src/{main => jvmMain}/kotlin/space/kscience/controls/ports/KtorUdpPort.kt (100%) rename controls-serial/src/{main => jvmMain}/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt (100%) rename controls-serial/src/{main => jvmMain}/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt (100%) rename controls-serial/src/{main => jvmMain}/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt (100%) rename controls-storage/controls-xodus/src/{main => jvmMain}/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt (100%) rename controls-storage/controls-xodus/src/{test => jvmTest}/kotlin/PropertyHistoryTest.kt (100%) rename magix/magix-server/src/{main => jvmMain}/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt (100%) rename magix/magix-server/src/{main => jvmMain}/kotlin/space/kscience/magix/server/magixModule.kt (100%) rename magix/magix-server/src/{main => jvmMain}/kotlin/space/kscience/magix/server/server.kt (100%) rename magix/magix-server/src/{main => jvmMain}/kotlin/space/kscience/magix/server/sse.kt (100%) diff --git a/build.gradle.kts b/build.gradle.kts index 57cb978..aed37b5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { allprojects { group = "space.kscience" - version = "0.3.1-dev-1" + version = "0.4.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 index e62dd8e..e69c4d4 100644 --- a/controls-constructor/build.gradle.kts +++ b/controls-constructor/build.gradle.kts @@ -10,9 +10,14 @@ description = """ kscience{ jvm() js() - dependencies { + useCoroutines() + commonMain { api(projects.controlsCore) } + + commonTest{ + implementation(spclibs.logback.classic) + } } readme{ diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorBinding.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorBinding.kt new file mode 100644 index 0000000..7ad370e --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorBinding.kt @@ -0,0 +1,26 @@ +package space.kscience.controls.constructor + +import space.kscience.controls.api.Device + +public sealed interface ConstructorBinding + +/** + * A binding that exposes device property as read-only state + */ +public class PropertyBinding<T>( + public val device: Device, + public val propertyName: String, + public val state: DeviceState<T>, +) : ConstructorBinding + +/** + * A binding for independent state like a timer + */ +public class StateBinding<T>( + public val state: DeviceState<T> +) : ConstructorBinding + +public class ActionBinding( + public val reads: Collection<DeviceState<*>>, + public val writes: Collection<DeviceState<*>> +): ConstructorBinding \ No newline at end of file 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 63b5303..7824e3c 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 @@ -1,9 +1,16 @@ package space.kscience.controls.constructor +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.manager.ClockManager +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.MutableDevicePropertySpec 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.names.Name @@ -14,105 +21,154 @@ import kotlin.reflect.KProperty import kotlin.time.Duration /** - * A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices + * A base for strongly typed device constructor block. Has additional delegates for type-safe devices */ public abstract class DeviceConstructor( context: Context, - meta: Meta, + meta: Meta = Meta.EMPTY, ) : DeviceGroup(context, meta) { + private val _bindings: MutableList<ConstructorBinding> = mutableListOf() + public val bindings: List<ConstructorBinding> get() = _bindings + + public fun registerBinding(binding: ConstructorBinding) { + _bindings.add(binding) + } + + override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { + super.registerProperty(descriptor, state) + registerBinding(PropertyBinding(this, descriptor.name, state)) + } /** - * Register a device, provided by a given [factory] and + * Create and register a timer. Timer is not counted as a device property. */ - public fun <D : Device> device( - factory: Factory<D>, - meta: Meta? = null, - nameOverride: Name? = null, - metaLocation: Name? = null, - ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = - PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> - val name = nameOverride ?: property.name.asName() - val device = install(name, factory, meta, metaLocation ?: name) - ReadOnlyProperty { _: DeviceConstructor, _ -> - device - } + public fun timer(tick: Duration): TimerState = TimerState(context.request(ClockManager), tick) + .also { registerBinding(StateBinding(it)) } + + /** + * Launch action that is performed on each [DeviceState] value change. + * + * Optionally provide [writes] - a set of states that this change affects. + */ + public fun <T> DeviceState<T>.onChange( + vararg writes: DeviceState<*>, + reads: Collection<DeviceState<*>>, + onChange: suspend (T) -> Unit, + ): Job = valueFlow.onEach(onChange).launchIn(this@DeviceConstructor).also { + registerBinding(ActionBinding(setOf(this, *reads.toTypedArray()), setOf(*writes))) + } +} + +/** + * Register a device, provided by a given [factory] and + */ +public fun <D : Device> DeviceConstructor.device( + factory: Factory<D>, + meta: Meta? = null, + nameOverride: Name? = null, + metaLocation: Name? = null, +): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = + PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> + val name = nameOverride ?: property.name.asName() + val device = install(name, factory, meta, metaLocation ?: name) + ReadOnlyProperty { _: DeviceConstructor, _ -> + device } + } - public fun <D : Device> device( - device: D, - nameOverride: Name? = null, - ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = - PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> - val name = nameOverride ?: property.name.asName() - install(name, device) - ReadOnlyProperty { _: DeviceConstructor, _ -> - device - } +public fun <D : Device> DeviceConstructor.device( + device: D, + nameOverride: Name? = null, +): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = + PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> + val name = nameOverride ?: property.name.asName() + install(name, device) + ReadOnlyProperty { _: DeviceConstructor, _ -> + device } + } - - /** - * Register a property and provide a direct reader for it - */ - public fun <T, S: DeviceState<T>> property( - state: S, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - nameOverride: String? = null, - ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> = - PropertyDelegateProvider { _: DeviceConstructor, property -> - val name = nameOverride ?: property.name - val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) - registerProperty(descriptor, state) - ReadOnlyProperty { _: DeviceConstructor, _ -> - state - } +/** + * Register a property and provide a direct reader for it + */ +public fun <T, S : DeviceState<T>> DeviceConstructor.property( + state: S, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, +): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> = + PropertyDelegateProvider { _: DeviceConstructor, property -> + val name = nameOverride ?: property.name + val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) + registerProperty(descriptor, state) + ReadOnlyProperty { _: DeviceConstructor, _ -> + state } + } - /** - * Register external state as a property - */ - public fun <T : Any> property( - metaConverter: MetaConverter<T>, - reader: suspend () -> T, - readInterval: Duration, - initialState: T, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - nameOverride: String? = null, - ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property( - DeviceState.external(this, metaConverter, readInterval, initialState, reader), - descriptorBuilder, - nameOverride, - ) +/** + * Register external state as a property + */ +public fun <T : Any> DeviceConstructor.property( + metaConverter: MetaConverter<T>, + reader: suspend () -> T, + readInterval: Duration, + initialState: T, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, +): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property( + DeviceState.external(this, metaConverter, readInterval, initialState, reader), + descriptorBuilder, + nameOverride, +) - /** - * Register a mutable external state as a property - */ - public fun <T : Any> mutableProperty( - metaConverter: MetaConverter<T>, - reader: suspend () -> T, - writer: suspend (T) -> Unit, - readInterval: Duration, - initialState: T, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - nameOverride: String? = null, - ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( - DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), - descriptorBuilder, - nameOverride, - ) +/** + * Register a mutable external state as a property + */ +public fun <T : Any> DeviceConstructor.mutableProperty( + metaConverter: MetaConverter<T>, + reader: suspend () -> T, + writer: suspend (T) -> Unit, + readInterval: Duration, + initialState: T, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, +): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( + DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), + descriptorBuilder, + nameOverride, +) - /** - * Create and register a virtual mutable property with optional [callback] - */ - public fun <T> virtualProperty( - metaConverter: MetaConverter<T>, - initialState: T, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - nameOverride: String? = null, - callback: (T) -> Unit = {}, - ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( - DeviceState.virtual(metaConverter, initialState, callback), - descriptorBuilder, - nameOverride, - ) -} \ No newline at end of file +/** + * Create and register a virtual mutable property with optional [callback] + */ +public fun <T> DeviceConstructor.virtualProperty( + metaConverter: MetaConverter<T>, + initialState: T, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, + callback: (T) -> Unit = {}, +): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( + DeviceState.internal(metaConverter, initialState, callback), + descriptorBuilder, + nameOverride, +) + +/** + * Bind existing property provided by specification to this device + */ +public fun <T, D : Device> DeviceConstructor.deviceProperty( + device: D, + property: DevicePropertySpec<D, T>, + initialValue: T, +): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = + property(device.propertyAsState(property, initialValue)) + +/** + * Bind existing property provided by specification to this device + */ +public fun <T, D : Device> DeviceConstructor.deviceProperty( + device: D, + property: MutableDevicePropertySpec<D, T>, + initialValue: T, +): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = + property(device.mutablePropertyAsState(property, initialValue)) \ 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 ca05c27..6785b4b 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 @@ -9,9 +9,7 @@ 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 -import space.kscience.dataforge.context.Factory -import space.kscience.dataforge.context.request +import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.* import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.names.* @@ -57,6 +55,7 @@ public open class DeviceGroup( ) ) } + logger.error(throwable) { "Exception in device $id" } } ) @@ -69,7 +68,7 @@ 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 <D : Device> install(token: NameToken, device: D): D { + public open fun <D : Device> 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() } @@ -82,7 +81,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 open 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) @@ -126,37 +125,33 @@ public open class DeviceGroup( return action.invoke(argument) } - @DFExperimental - override var lifecycleState: DeviceLifecycleState = STOPPED - protected set(value) { - if (field != value) { - launch { - sharedMessageFlow.emit( - DeviceLifeCycleMessage(value) - ) - } - } - field = value - } + final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED + private set + + + private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) { + this.lifecycleState = lifecycleState + sharedMessageFlow.emit( + DeviceLifeCycleMessage(lifecycleState) + ) + } - @OptIn(DFExperimental::class) override suspend fun start() { - lifecycleState = STARTING + setLifecycleState(STARTING) super.start() devices.values.forEach { it.start() } - lifecycleState = STARTED + setLifecycleState(STARTED) } - @OptIn(DFExperimental::class) - override fun stop() { + override suspend fun stop() { devices.values.forEach { it.stop() } + setLifecycleState(STOPPED) super.stop() - lifecycleState = STOPPED } public companion object { @@ -210,13 +205,9 @@ public fun <D : Device> DeviceGroup.install(name: Name, device: D): D { } } -public fun <D : Device> DeviceGroup.install(name: String, device: D): D = - install(name.parseAsName(), device) +public fun <D : Device> DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device) -public fun <D : Device> DeviceGroup.install(device: D): D = - install(device.id, device) - -public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device) +public fun <D : Device> DeviceGroup.install(device: D): D = install(device.id, device) /** * Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error. @@ -292,7 +283,7 @@ public fun <T : Any> DeviceGroup.registerVirtualProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, callback: (T) -> Unit = {}, ): MutableDeviceState<T> { - val state = DeviceState.virtual<T>(converter, initialValue, callback) + val state = DeviceState.internal<T>(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 faa7b2f..9a25f00 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 @@ -2,18 +2,13 @@ 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 -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 kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaConverter import kotlin.reflect.KProperty -import kotlin.time.Duration /** * An observable state of a device @@ -24,6 +19,8 @@ public interface DeviceState<T> { public val valueFlow: Flow<T> + override fun toString(): String + public companion object } @@ -36,7 +33,7 @@ public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProper /** * Collect values in a given [scope] */ -public fun <T> DeviceState<T>.collectValuesIn(scope: CoroutineScope, block: suspend (T)->Unit): Job = +public fun <T> DeviceState<T>.collectValuesIn(scope: CoroutineScope, block: suspend (T) -> Unit): Job = valueFlow.onEach(block).launchIn(scope) /** @@ -57,186 +54,46 @@ public var <T> MutableDeviceState<T>.valueAsMeta: Meta } /** - * A [MutableDeviceState] that does not correspond to a physical state - * - * @param callback a synchronous callback that could be used without a scope + * Device state with a value that depends on other device states */ -private class VirtualDeviceState<T>( - override val converter: MetaConverter<T>, - initialValue: T, - private val callback: (T) -> Unit = {}, -) : MutableDeviceState<T> { - private val flow = MutableStateFlow(initialValue) - override val valueFlow: Flow<T> get() = flow - - 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 <T> DeviceState.Companion.virtual( - converter: MetaConverter<T>, - initialValue: T, - callback: (T) -> Unit = {}, -): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback) - -private class StateFlowAsState<T>( - override val converter: MetaConverter<T>, - val flow: MutableStateFlow<T>, -) : MutableDeviceState<T> { - override var value: T by flow::value - override val valueFlow: Flow<T> get() = flow -} - -public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> = - StateFlowAsState(converter, this) - - -private open class BoundDeviceState<T>( - override val converter: MetaConverter<T>, - val device: Device, - val propertyName: String, - initialValue: T, -) : DeviceState<T> { - - override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter { - it.property == propertyName - }.mapNotNull { - converter.read(it.value) - }.stateIn(device.context, SharingStarted.Eagerly, initialValue) - - override val value: T get() = valueFlow.value +public interface DeviceStateWithDependencies<T> : DeviceState<T> { + public val dependencies: Collection<DeviceState<*>> } /** - * Bind a read-only [DeviceState] to a [Device] property + * Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper]. */ -public suspend fun <T> Device.propertyAsState( - propertyName: String, - metaConverter: MetaConverter<T>, -): DeviceState<T> { - val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed") - return BoundDeviceState(metaConverter, this, propertyName, initialValue) -} - -public suspend fun <D : Device, T> D.propertyAsState( - propertySpec: DevicePropertySpec<D, T>, -): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter) - public fun <T, R> DeviceState<T>.map( converter: MetaConverter<R>, mapper: (T) -> R, -): DeviceState<R> = object : DeviceState<R> { +): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> { + override val dependencies = listOf(this) + override val converter: MetaConverter<R> = converter + override val value: R get() = mapper(this@map.value) override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper) -} -private class MutableBoundDeviceState<T>( - converter: MetaConverter<T>, - device: Device, - propertyName: String, - initialValue: T, -) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> { - - override var value: T - get() = valueFlow.value - set(newValue) { - device.launch { - device.writeProperty(propertyName, converter.convert(newValue)) - } - } -} - -public fun <T> Device.mutablePropertyAsState( - propertyName: String, - metaConverter: MetaConverter<T>, - initialValue: T, -): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) - -public suspend fun <T> Device.mutablePropertyAsState( - propertyName: String, - metaConverter: MetaConverter<T>, -): MutableDeviceState<T> { - val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed") - return mutablePropertyAsState(propertyName, metaConverter, initialValue) -} - -public suspend fun <D : Device, T> D.mutablePropertyAsState( - propertySpec: MutableDevicePropertySpec<D, T>, -): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter) - -public fun <D : Device, T> D.mutablePropertyAsState( - propertySpec: MutableDevicePropertySpec<D, T>, - initialValue: T, -): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue) - - -private open class ExternalState<T>( - val scope: CoroutineScope, - override val converter: MetaConverter<T>, - val readInterval: Duration, - initialValue: T, - val reader: suspend () -> T, -) : DeviceState<T> { - - protected val flow: StateFlow<T> = flow { - while (true) { - delay(readInterval) - emit(reader()) - } - }.stateIn(scope, SharingStarted.Eagerly, initialValue) - - override val value: T get() = flow.value - override val valueFlow: Flow<T> get() = flow + override fun toString(): String = "DeviceState.map(arg=${this@map}, converter=$converter)" } /** - * Create a [DeviceState] which is constructed by periodically reading external value + * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used. */ -public fun <T> DeviceState.Companion.external( - scope: CoroutineScope, - converter: MetaConverter<T>, - readInterval: Duration, - initialValue: T, - reader: suspend () -> T, -): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader) +public fun <T1, T2, R> combine( + state1: DeviceState<T1>, + state2: DeviceState<T2>, + converter: MetaConverter<R>, + mapper: (T1, T2) -> R, +): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> { + override val dependencies = listOf(state1, state2) -private class MutableExternalState<T>( - scope: CoroutineScope, - converter: MetaConverter<T>, - readInterval: Duration, - initialValue: T, - reader: suspend () -> T, - val writer: suspend (T) -> Unit, -) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> { - override var value: T - get() = super.value - set(value) { - scope.launch { - writer(value) - } - } + override val converter: MetaConverter<R> = converter + + override val value: R get() = mapper(state1.value, state2.value) + + override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper) + + override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2)" } - -/** - * Create a [DeviceState] that regularly reads and caches an external value - */ -public fun <T> DeviceState.Companion.external( - scope: CoroutineScope, - converter: MetaConverter<T>, - readInterval: Duration, - initialValue: T, - reader: suspend () -> T, - writer: suspend (T) -> Unit, -): MutableDeviceState<T> = 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/TimerState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt new file mode 100644 index 0000000..7b10cb9 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt @@ -0,0 +1,41 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.datetime.Instant +import space.kscience.controls.manager.ClockManager +import space.kscience.controls.spec.instant +import space.kscience.dataforge.meta.MetaConverter +import kotlin.time.Duration + +/** + * A dedicated [DeviceState] that operates with time. + * The state changes with [tick] interval and always shows the time of the last update. + * + * Both [tick] and current time are computed by [clockManager] enabling time manipulation. + * + * The timer runs indefinitely until the parent context is closed + */ +public class TimerState( + public val clockManager: ClockManager, + public val tick: Duration, +) : DeviceState<Instant> { + override val converter: MetaConverter<Instant> get() = MetaConverter.instant + + private val clock = MutableStateFlow(clockManager.clock.now()) + + private val updateJob = clockManager.context.launch { + while (isActive) { + clockManager.delay(tick) + clock.value = clockManager.clock.now() + } + } + + override val valueFlow: Flow<Instant> get() = clock + + override val value: Instant get() = clock.value + + override fun toString(): String = "TimerState(tick=$tick)" +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt new file mode 100644 index 0000000..37be770 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt @@ -0,0 +1,103 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import space.kscience.controls.api.Device +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.api.id +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.MutableDevicePropertySpec +import space.kscience.controls.spec.name +import space.kscience.dataforge.meta.MetaConverter + + +/** + * A copy-free [DeviceState] bound to a device property + */ +private open class BoundDeviceState<T>( + override val converter: MetaConverter<T>, + val device: Device, + val propertyName: String, + initialValue: T, +) : DeviceState<T> { + + override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter { + it.property == propertyName + }.mapNotNull { + converter.read(it.value) + }.stateIn(device.context, SharingStarted.Eagerly, initialValue) + + override val value: T get() = valueFlow.value + override fun toString(): String = + "BoundDeviceState(converter=$converter, device=${device.id}, propertyName='$propertyName')" + + +} + +public fun <T> Device.propertyAsState( + propertyName: String, + metaConverter: MetaConverter<T>, + initialValue: T, +): DeviceState<T> = BoundDeviceState(metaConverter, this, propertyName, initialValue) + +/** + * Bind a read-only [DeviceState] to a [Device] property + */ +public suspend fun <T> Device.propertyAsState( + propertyName: String, + metaConverter: MetaConverter<T>, +): DeviceState<T> = propertyAsState( + propertyName, + metaConverter, + metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed") +) + +public suspend fun <D : Device, T> D.propertyAsState( + propertySpec: DevicePropertySpec<D, T>, +): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter) + +public fun <D : Device, T> D.propertyAsState( + propertySpec: DevicePropertySpec<D, T>, + initialValue: T, +): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter, initialValue) + + +private class MutableBoundDeviceState<T>( + converter: MetaConverter<T>, + device: Device, + propertyName: String, + initialValue: T, +) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> { + + override var value: T + get() = valueFlow.value + set(newValue) { + device.launch { + device.writeProperty(propertyName, converter.convert(newValue)) + } + } +} + +public fun <T> Device.mutablePropertyAsState( + propertyName: String, + metaConverter: MetaConverter<T>, + initialValue: T, +): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) + +public suspend fun <T> Device.mutablePropertyAsState( + propertyName: String, + metaConverter: MetaConverter<T>, +): MutableDeviceState<T> { + val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed") + return mutablePropertyAsState(propertyName, metaConverter, initialValue) +} + +public suspend fun <D : Device, T> D.mutablePropertyAsState( + propertySpec: MutableDevicePropertySpec<D, T>, +): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter) + +public fun <D : Device, T> D.mutablePropertyAsState( + propertySpec: MutableDevicePropertySpec<D, T>, + initialValue: T, +): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue) + 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 100f755..881bdea 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,6 +38,10 @@ public class DoubleRangeState( * A state showing that the range is on its higher boundary */ public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive } + + override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)" + + } @Suppress("UnusedReceiverParameter") diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt new file mode 100644 index 0000000..c670a23 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt @@ -0,0 +1,70 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import space.kscience.dataforge.meta.MetaConverter +import kotlin.time.Duration + + +private open class ExternalState<T>( + val scope: CoroutineScope, + override val converter: MetaConverter<T>, + val readInterval: Duration, + initialValue: T, + val reader: suspend () -> T, +) : DeviceState<T> { + + protected val flow: StateFlow<T> = flow { + while (true) { + delay(readInterval) + emit(reader()) + } + }.stateIn(scope, SharingStarted.Eagerly, initialValue) + + override val value: T get() = flow.value + override val valueFlow: Flow<T> get() = flow + + override fun toString(): String = "ExternalState(converter=$converter)" +} + +/** + * Create a [DeviceState] which is constructed by regularly reading external value + */ +public fun <T> DeviceState.Companion.external( + scope: CoroutineScope, + converter: MetaConverter<T>, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, +): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader) + +private class MutableExternalState<T>( + scope: CoroutineScope, + converter: MetaConverter<T>, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, + val writer: suspend (T) -> Unit, +) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> { + override var value: T + get() = super.value + set(value) { + scope.launch { + writer(value) + } + } +} + +/** + * Create a [MutableDeviceState] which is constructed by regularly reading external value and allows writing + */ +public fun <T> DeviceState.Companion.external( + scope: CoroutineScope, + converter: MetaConverter<T>, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, + writer: suspend (T) -> Unit, +): MutableDeviceState<T> = 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/flowState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt new file mode 100644 index 0000000..434c074 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt @@ -0,0 +1,23 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import space.kscience.dataforge.meta.MetaConverter + + +private class StateFlowAsState<T>( + override val converter: MetaConverter<T>, + val flow: MutableStateFlow<T>, +) : MutableDeviceState<T> { + override var value: T by flow::value + override val valueFlow: Flow<T> get() = flow + + override fun toString(): String = "FlowAsState(converter=$converter)" +} + +/** + * Create a read-only [DeviceState] that wraps [MutableStateFlow]. + * No data copy is performed. + */ +public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> = + StateFlowAsState(converter, this) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt new file mode 100644 index 0000000..6f9d086 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt @@ -0,0 +1,42 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import space.kscience.dataforge.meta.MetaConverter + +/** + * A [MutableDeviceState] that does not correspond to a physical state + * + * @param callback a synchronous callback that could be used without a scope + */ +private class VirtualDeviceState<T>( + override val converter: MetaConverter<T>, + initialValue: T, + private val callback: (T) -> Unit = {}, +) : MutableDeviceState<T> { + private val flow = MutableStateFlow(initialValue) + override val valueFlow: Flow<T> get() = flow + + override var value: T + get() = flow.value + set(value) { + flow.value = value + callback(value) + } + + override fun toString(): String = "VirtualDeviceState(converter=$converter)" + + +} + + +/** + * A [MutableDeviceState] that does not correspond to a physical state + * + * @param callback a synchronous callback that could be used without a scope + */ +public fun <T> DeviceState.Companion.internal( + converter: MetaConverter<T>, + initialValue: T, + callback: (T) -> Unit = {}, +): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt similarity index 91% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt index 8301622..5678b34 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt @@ -1,10 +1,12 @@ -package space.kscience.controls.constructor +package space.kscience.controls.constructor.library import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import space.kscience.controls.api.Device +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.mutablePropertyAsState import space.kscience.controls.manager.clock import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context @@ -49,7 +51,7 @@ public class VirtualDrive( public val positionState: MutableDeviceState<Double>, ) : Drive, DeviceBySpec<Drive>(Drive, context) { - private val dt = meta["time.step"].double?.milliseconds ?: 1.milliseconds + private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds private val clock = context.clock override var force: Double = 0.0 @@ -82,7 +84,7 @@ public class VirtualDrive( } } - override fun onStop() { + override suspend 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/library/LimitSwitch.kt similarity index 83% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt index 977930d..895cee8 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt @@ -1,8 +1,9 @@ -package space.kscience.controls.constructor +package space.kscience.controls.constructor.library import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import space.kscience.controls.api.Device +import space.kscience.controls.constructor.DeviceState import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DeviceSpec @@ -20,7 +21,7 @@ public interface LimitSwitch : Device { public companion object : DeviceSpec<LimitSwitch>() { public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked } - public fun factory(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ -> + public operator fun invoke(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ -> VirtualLimitSwitch(context, lockedState) } } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt similarity index 83% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt index d782e05..7ce2054 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.constructor +package space.kscience.controls.constructor.library import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -7,8 +7,11 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Instant +import space.kscience.controls.constructor.DeviceGroup +import space.kscience.controls.constructor.install import space.kscience.controls.manager.clock import space.kscience.controls.spec.DeviceBySpec +import space.kscience.controls.spec.write import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit @@ -71,15 +74,17 @@ public class PidRegulator( lastTime = realTime lastPosition = drive.position - drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative + drive.write(Drive.force,pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative) + //drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative propertyChanged(Regulator.position, drive.position) } } } } - override fun onStop() { + override suspend fun onStop() { updateJob?.cancel() + drive.stop() } override val position: Double get() = drive.position diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt similarity index 92% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt index cbe6967..adf319b 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.constructor +package space.kscience.controls.constructor.library import space.kscience.controls.api.Device import space.kscience.controls.spec.* diff --git a/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/DeviceGroupTest.kt b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/DeviceGroupTest.kt new file mode 100644 index 0000000..40f00dc --- /dev/null +++ b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/DeviceGroupTest.kt @@ -0,0 +1,43 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import space.kscience.controls.api.Device +import space.kscience.controls.api.DeviceLifeCycleMessage +import space.kscience.controls.api.DeviceLifecycleState +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.controls.spec.doRecurring +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.context.Global +import space.kscience.dataforge.context.request +import space.kscience.dataforge.meta.Meta +import kotlin.test.Test +import kotlin.time.Duration.Companion.milliseconds + +class DeviceGroupTest { + + class TestDevice(context: Context) : DeviceConstructor(context) { + + companion object : Factory<Device> { + override fun build(context: Context, meta: Meta): Device = TestDevice(context) + } + } + + @Test + fun testRecurringRead() = runTest { + var counter = 10 + val testDevice = Global.request(DeviceManager).install("test", TestDevice) + testDevice.doRecurring(1.milliseconds) { + counter-- + println(counter) + if (counter <= 0) { + testDevice.stop() + } + error("Error!") + } + testDevice.messageFlow.first { it is DeviceLifeCycleMessage && it.state == DeviceLifecycleState.STOPPED } + println("stopped") + } +} \ No newline at end of file diff --git a/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/TimerTest.kt b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/TimerTest.kt new file mode 100644 index 0000000..323f8df --- /dev/null +++ b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/TimerTest.kt @@ -0,0 +1,22 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.test.runTest +import space.kscience.controls.manager.ClockManager +import space.kscience.dataforge.context.Global +import space.kscience.dataforge.context.request +import kotlin.test.Test +import kotlin.time.Duration.Companion.milliseconds + +class TimerTest { + + @Test + fun timer() = runTest { + val timer = TimerState(Global.request(ClockManager), 10.milliseconds) + timer.valueFlow.take(10).onEach { + println(it) + }.collect() + } +} \ 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 3226422..fc68bf6 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 @@ -12,7 +12,6 @@ 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 @@ -100,12 +99,11 @@ public interface Device : ContextAware, CoroutineScope { /** * Close and terminate the device. This function does not wait for the device to be closed. */ - public fun stop() { + public suspend fun stop() { logger.info { "Device $this is closed" } cancel("The device is closed") } - @DFExperimental public val lifecycleState: DeviceLifecycleState public companion object { 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 2adb89a..f3ffa93 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 @@ -15,18 +15,23 @@ public class PropertyDescriptor( public var description: String? = null, public var metaDescriptor: MetaDescriptor = MetaDescriptor(), public var readable: Boolean = true, - public var mutable: Boolean = false + public var mutable: Boolean = false, ) -public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){ - metaDescriptor = MetaDescriptor(block) +public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) { + metaDescriptor = MetaDescriptor { + from(metaDescriptor) + block() + } } /** * A descriptor for property */ @Serializable -public class ActionDescriptor(public val name: String) { - public var description: String? = null -} - +public class ActionDescriptor( + public val name: String, + public var description: String? = null, + public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor(), + public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor() +) 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 index ca69b91..2f852e1 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt @@ -6,15 +6,21 @@ 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 kotlin.time.Duration public class ClockManager : AbstractPlugin() { - override val tag: PluginTag get() = DeviceManager.tag + override val tag: PluginTag get() = Companion.tag public val clock: Clock by lazy { //TODO add clock customization Clock.System } + public suspend fun delay(duration: Duration) { + //TODO add time compression + kotlinx.coroutines.delay(duration) + } + public companion object : PluginFactory<ClockManager> { override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP) 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 c8c7ffc..93b0696 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 @@ -49,6 +49,10 @@ public fun <D : Device> DeviceManager.install(name: String, device: D): D { public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device) +public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device) + +public fun <D : Device> Context.install(device: D): D = request(DeviceManager).install(device.id, device) + /** * Register and start a device built by [factory] with current [Context] and [meta]. */ @@ -75,5 +79,4 @@ public inline fun <D : Device> DeviceManager.installing( current as D } } -} - +} \ 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 e8b9210..941eda9 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 @@ -9,11 +9,11 @@ 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.meta.get import space.kscience.dataforge.meta.int -import space.kscience.dataforge.misc.DFExperimental import kotlin.coroutines.CoroutineContext /** @@ -72,10 +72,10 @@ public abstract class DeviceBase<D : Device>( onBufferOverflow = BufferOverflow.DROP_OLDEST ) - @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + @OptIn(ExperimentalCoroutinesApi::class) override val coroutineContext: CoroutineContext = context.newCoroutineContext( SupervisorJob(context.coroutineContext[Job]) + - CoroutineName("Device $this") + + CoroutineName("Device $id") + CoroutineExceptionHandler { _, throwable -> launch { sharedMessageFlow.emit( @@ -86,6 +86,7 @@ public abstract class DeviceBase<D : Device>( ) ) } + logger.error(throwable) { "Exception in device $id" } } ) @@ -187,43 +188,39 @@ public abstract class DeviceBase<D : Device>( return spec.executeWithMeta(self, argument ?: Meta.EMPTY) } - @DFExperimental final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED - private set(value) { - if (field != value) { - launch { - sharedMessageFlow.emit( - DeviceLifeCycleMessage(value) - ) - } - } - field = value - } + private set + + + private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) { + this.lifecycleState = lifecycleState + sharedMessageFlow.emit( + DeviceLifeCycleMessage(lifecycleState) + ) + } protected open suspend fun onStart() { } - @OptIn(DFExperimental::class) final override suspend fun start() { if (lifecycleState == DeviceLifecycleState.STOPPED) { super.start() - lifecycleState = DeviceLifecycleState.STARTING + setLifecycleState(DeviceLifecycleState.STARTING) onStart() - lifecycleState = DeviceLifecycleState.STARTED + setLifecycleState(DeviceLifecycleState.STARTED) } else { logger.debug { "Device $this is already started" } } } - protected open fun onStop() { + protected open suspend fun onStop() { } - @OptIn(DFExperimental::class) - final override fun stop() { + final override suspend fun stop() { onStop() - lifecycleState = DeviceLifecycleState.STOPPED + setLifecycleState(DeviceLifecycleState.STOPPED) super.stop() } 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 31f4c09..f639fc7 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 @@ -20,7 +20,7 @@ public open class DeviceBySpec<D : Device>( self.onOpen() } - override fun onStop(): Unit = with(spec){ + override suspend fun onStop(): Unit = with(spec){ self.onClose() } 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 2f90cb8..07d831f 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 @@ -4,8 +4,10 @@ import kotlinx.coroutines.withContext import space.kscience.controls.api.ActionDescriptor 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.descriptors.MetaDescriptor import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty @@ -54,6 +56,11 @@ public abstract class DeviceSpec<D : Device> { val deviceProperty = object : DevicePropertySpec<D, T> { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { + converter.descriptor?.let { converterDescriptor -> + metaDescriptor { + from(converterDescriptor) + } + } fromSpec(property) descriptorBuilder() } @@ -83,6 +90,11 @@ public abstract class DeviceSpec<D : Device> { propertyName, mutable = true ).apply { + converter.descriptor?.let { converterDescriptor -> + metaDescriptor { + from(converterDescriptor) + } + } fromSpec(property) descriptorBuilder() } @@ -118,6 +130,19 @@ public abstract class DeviceSpec<D : Device> { val actionName = name ?: property.name val deviceAction = object : DeviceActionSpec<D, I, O> { override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply { + inputConverter.descriptor?.let { converterDescriptor -> + inputMetaDescriptor = MetaDescriptor { + from(converterDescriptor) + from(inputMetaDescriptor) + } + } + outputConverter.descriptor?.let { converterDescriptor -> + outputMetaDescriptor = MetaDescriptor { + from(converterDescriptor) + from(outputMetaDescriptor) + } + } + fromSpec(property) descriptorBuilder() } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt new file mode 100644 index 0000000..89a28da --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt @@ -0,0 +1,41 @@ +package space.kscience.controls.spec + +import kotlinx.datetime.Instant +import space.kscience.dataforge.meta.* +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +public fun Double.asMeta(): Meta = Meta(asValue()) + +/** + * Generate a nullable [MetaConverter] from non-nullable one + */ +public fun <T : Any> MetaConverter<T>.nullable(): MetaConverter<T?> = object : MetaConverter<T?> { + override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) }?: Meta(Null) + + override fun readOrNull(source: Meta): T? = if(source.value == Null) null else this@nullable.readOrNull(source) + +} + +//TODO to be moved to DF +private object DurationConverter : MetaConverter<Duration> { + override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS) + ?: run { + val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS + val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration") + return@run value.toDuration(unit) + } + + override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta() +} + +public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter + + +private object InstantConverter : MetaConverter<Instant> { + override fun readOrNull(source: Meta): Instant? = source.string?.let { Instant.parse(it) } + override fun convert(obj: Instant): Meta = Meta(obj.toString()) +} + +public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter \ 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 fefb65e..ee47ea0 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt @@ -1,14 +1,31 @@ package space.kscience.controls.spec -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import space.kscience.controls.api.Device import kotlin.time.Duration +/** + * Do a recurring (with a fixed delay) task on a device. + */ +public fun <D : Device> D.doRecurring( + interval: Duration, + debugTaskName: String? = null, + task: suspend D.() -> Unit, +): Job { + val taskName = debugTaskName ?: "task[${task.hashCode().toString(16)}]" + return launch(CoroutineName(taskName)) { + while (isActive) { + delay(interval) + //launch in parent scope to properly evaluate exceptions + this@doRecurring.launch { + task() + } + } + } +} + /** * 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. @@ -16,23 +33,12 @@ import kotlin.time.Duration * * The flow is canceled when the device scope is canceled */ -public fun <D : Device, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow { - while (isActive) { - delay(interval) - launch { - emit(reader()) - } - } -} - -/** - * Do a recurring (with a fixed delay) task on a device. - */ -public fun <D : Device> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch { - while (isActive) { - delay(interval) - launch { - task() - } +public fun <D : Device, R> D.readRecurring( + interval: Duration, + debugTaskName: String? = null, + reader: suspend D.() -> R, +): Flow<R> = flow { + doRecurring(interval, debugTaskName) { + emit(reader()) } } \ No newline at end of file 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 index 000458e..ad5862b 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt @@ -4,8 +4,6 @@ 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<*>) 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 deleted file mode 100644 index 1fc4649..0000000 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt +++ /dev/null @@ -1,22 +0,0 @@ -package space.kscience.controls.spec - -import space.kscience.dataforge.meta.* -import kotlin.time.Duration -import kotlin.time.DurationUnit -import kotlin.time.toDuration - -public fun Double.asMeta(): Meta = Meta(asValue()) - -//TODO to be moved to DF -public object DurationConverter : MetaConverter<Duration> { - override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS) - ?: run { - val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS - val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration") - return@run value.toDuration(unit) - } - - override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta() -} - -public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter \ 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 index 7ae572f..cb64fc3 100644 --- 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 @@ -2,17 +2,18 @@ package space.kscience.controls.spec import space.kscience.controls.api.ActionDescriptor import space.kscience.controls.api.PropertyDescriptor +import space.kscience.dataforge.descriptors.Description import kotlin.reflect.KProperty import kotlin.reflect.full.findAnnotation internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) { property.findAnnotation<Description>()?.let { - description = it.content + description = it.value } } internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){ property.findAnnotation<Description>()?.let { - description = it.content + description = it.value } } \ No newline at end of file diff --git a/controls-modbus/build.gradle.kts b/controls-modbus/build.gradle.kts index 2f280b6..8ef9ca8 100644 --- a/controls-modbus/build.gradle.kts +++ b/controls-modbus/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` } @@ -9,10 +9,12 @@ description = """ A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols """.trimIndent() - -dependencies { - api(projects.controlsCore) - api(libs.j2mod) +kscience { + jvm() + jvmMain { + api(projects.controlsCore) + api(libs.j2mod) + } } readme{ diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt similarity index 100% rename from controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt rename to controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDevice.kt similarity index 100% rename from controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt rename to controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDevice.kt diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt similarity index 97% rename from controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt rename to controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt index 995e0df..e0bd47a 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt +++ b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt @@ -24,7 +24,7 @@ public open class ModbusDeviceBySpec<D: Device>( master.connect() } - override fun onStop() { + override suspend fun onStop() { if(disposeMasterOnClose){ master.disconnect() } diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt similarity index 100% rename from controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt rename to controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt 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 7debc0e..ea85719 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,7 +63,7 @@ public open class OpcUaDeviceBySpec<D : Device>( } } - override fun onStop() { + override suspend fun onStop() { client.disconnect() } } diff --git a/controls-pi/build.gradle.kts b/controls-pi/build.gradle.kts index 997fcf2..850173d 100644 --- a/controls-pi/build.gradle.kts +++ b/controls-pi/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` } @@ -7,10 +7,15 @@ description = """ Utils to work with controls-kt on Raspberry pi """.trimIndent() -dependencies{ - api(project(":controls-core")) - api(libs.pi4j.ktx) // Kotlin DSL - api(libs.pi4j.core) - api(libs.pi4j.plugin.raspberrypi) - api(libs.pi4j.plugin.pigpio) +kscience { + jvm() + + + jvmMain { + api(project(":controls-core")) + api(libs.pi4j.ktx) // Kotlin DSL + api(libs.pi4j.core) + api(libs.pi4j.plugin.raspberrypi) + api(libs.pi4j.plugin.pigpio) + } } \ No newline at end of file diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt b/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt similarity index 100% rename from controls-pi/src/main/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt rename to controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt b/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/PiPlugin.kt similarity index 100% rename from controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt rename to controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/PiPlugin.kt diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt b/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt similarity index 100% rename from controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt rename to controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt diff --git a/controls-ports-ktor/build.gradle.kts b/controls-ports-ktor/build.gradle.kts index 1f1efda..c225097 100644 --- a/controls-ports-ktor/build.gradle.kts +++ b/controls-ports-ktor/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` } @@ -9,9 +9,12 @@ description = """ Implementation of byte ports on top os ktor-io asynchronous API """.trimIndent() -dependencies { - api(projects.controlsCore) - api(spclibs.ktor.network) +kscience { + jvm() + jvmMain { + api(projects.controlsCore) + api(spclibs.ktor.network) + } } readme{ diff --git a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt b/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt similarity index 100% rename from controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt rename to controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt diff --git a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt b/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorTcpPort.kt similarity index 100% rename from controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt rename to controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorTcpPort.kt diff --git a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt b/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorUdpPort.kt similarity index 100% rename from controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt rename to controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorUdpPort.kt diff --git a/controls-serial/build.gradle.kts b/controls-serial/build.gradle.kts index d44a029..75bf017 100644 --- a/controls-serial/build.gradle.kts +++ b/controls-serial/build.gradle.kts @@ -1,15 +1,18 @@ import space.kscience.gradle.Maturity plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` } description = "Implementation of direct serial port communication with JSerialComm" -dependencies{ - api(project(":controls-core")) - implementation(libs.jSerialComm) +kscience { + jvm() + jvmMain { + api(project(":controls-core")) + implementation(libs.jSerialComm) + } } readme{ diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt similarity index 100% rename from controls-serial/src/main/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt rename to controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt similarity index 100% rename from controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt rename to controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt similarity index 100% rename from controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt rename to controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt diff --git a/controls-storage/controls-xodus/build.gradle.kts b/controls-storage/controls-xodus/build.gradle.kts index 8cebd37..04be46a 100644 --- a/controls-storage/controls-xodus/build.gradle.kts +++ b/controls-storage/controls-xodus/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` } @@ -7,13 +7,18 @@ description = """ An implementation of controls-storage on top of JetBrains Xodus. """.trimIndent() -dependencies { - api(projects.controlsStorage) - implementation(libs.xodus.entity.store) +kscience { + jvm() + jvmMain { + api(projects.controlsStorage) + implementation(libs.xodus.entity.store) // implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion") // implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion") - testImplementation(spclibs.kotlinx.coroutines.test) + } + jvmTest{ + implementation(spclibs.kotlinx.coroutines.test) + } } readme{ diff --git a/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt b/controls-storage/controls-xodus/src/jvmMain/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt similarity index 100% rename from controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt rename to controls-storage/controls-xodus/src/jvmMain/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt diff --git a/controls-storage/controls-xodus/src/test/kotlin/PropertyHistoryTest.kt b/controls-storage/controls-xodus/src/jvmTest/kotlin/PropertyHistoryTest.kt similarity index 100% rename from controls-storage/controls-xodus/src/test/kotlin/PropertyHistoryTest.kt rename to controls-storage/controls-xodus/src/jvmTest/kotlin/PropertyHistoryTest.kt 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 2d2086b..f341256 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 @@ -8,6 +8,7 @@ import javafx.stage.Stage import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import org.eclipse.milo.opcua.sdk.server.OpcUaServer import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText @@ -91,7 +92,7 @@ class DemoController : Controller(), ContextAware { } } - fun shutdown() { + suspend fun shutdown() { logger.info { "Shutting down..." } opcUaServer.shutdown() logger.info { "OpcUa server stopped" } @@ -179,7 +180,9 @@ class DemoControllerApp : App(DemoControllerView::class) { } override fun stop() { - controller.shutdown() + runBlocking { + controller.shutdown() + } super.stop() } } 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 3a63003..e76cfe0 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 @@ -8,6 +8,7 @@ import javafx.scene.layout.Priority import javafx.stage.Stage import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import space.kscience.controls.client.launchMagixService import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration import space.kscience.controls.manager.DeviceManager @@ -67,7 +68,7 @@ class VirtualCarController : Controller(), ContextAware { } } - fun shutdown() { + suspend fun shutdown() { logger.info { "Shutting down..." } magixServer?.stop(1000, 5000) logger.info { "Magix server stopped" } @@ -137,7 +138,9 @@ class VirtualCarControllerApp : App(VirtualCarControllerView::class) { } override fun stop() { - controller.shutdown() + runBlocking { + controller.shutdown() + } super.stop() } } diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 9b06d2e..e426e1b 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -16,9 +16,11 @@ 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.constructor.library.* import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.clock +import space.kscience.controls.manager.install import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.name import space.kscience.controls.vision.plot @@ -48,13 +50,14 @@ class LinearDrive( 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 start by device(LimitSwitch(state.atStartState)) + val end by device(LimitSwitch(state.atEndState)) val positionState: DoubleRangeState by property(state) - private val targetState: MutableDeviceState<Double> by property(pid.mutablePropertyAsState(Regulator.target, 0.0)) - var target by targetState + + private val targetState: MutableDeviceState<Double> by deviceProperty(pid, Regulator.target, 0.0) + var target: Double by targetState } @@ -73,7 +76,6 @@ private fun Context.launchPidDevice( val timeFromStart = clock.now() - clockStart val t = timeFromStart.toDouble(DurationUnit.SECONDS) val freq = 0.1 - target = 5 * sin(2.0 * PI * freq * t) + sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep)) } @@ -150,7 +152,7 @@ fun main() = application { Row { Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) TextField( - String.format("%.2f",pidParameters.kp), + String.format("%.2f", pidParameters.kp), { pidParameters.kp = it.toDouble() }, Modifier.width(100.dp), enabled = false @@ -165,7 +167,7 @@ fun main() = application { Row { Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) TextField( - String.format("%.2f",pidParameters.ki), + String.format("%.2f", pidParameters.ki), { pidParameters.ki = it.toDouble() }, Modifier.width(100.dp), enabled = false @@ -181,7 +183,7 @@ fun main() = application { Row { Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) TextField( - String.format("%.2f",pidParameters.kd), + String.format("%.2f", pidParameters.kd), { pidParameters.kd = it.toDouble() }, Modifier.width(100.dp), enabled = false diff --git a/magix/magix-server/build.gradle.kts b/magix/magix-server/build.gradle.kts index f00b81e..28fe492 100644 --- a/magix/magix-server/build.gradle.kts +++ b/magix/magix-server/build.gradle.kts @@ -1,36 +1,38 @@ import space.kscience.gradle.Maturity plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` - application } description = """ A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes. """.trimIndent() -kscience { - useSerialization{ - json() - } -} - val dataforgeVersion: String by rootProject.extra val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion -dependencies{ - api(projects.magix.magixApi) - api("io.ktor:ktor-server-cio:$ktorVersion") - api("io.ktor:ktor-server-websockets:$ktorVersion") - api("io.ktor:ktor-server-content-negotiation:$ktorVersion") - api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - api("io.ktor:ktor-server-html-builder:$ktorVersion") +kscience { + jvm() + useSerialization{ + json() + } + + jvmMain{ + api(projects.magix.magixApi) + api("io.ktor:ktor-server-cio:$ktorVersion") + api("io.ktor:ktor-server-websockets:$ktorVersion") + api("io.ktor:ktor-server-content-negotiation:$ktorVersion") + api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + api("io.ktor:ktor-server-html-builder:$ktorVersion") + + api(libs.rsocket.ktor.server) + api(libs.rsocket.transport.ktor.tcp) + } - api(libs.rsocket.ktor.server) - api(libs.rsocket.transport.ktor.tcp) } + readme{ maturity = Maturity.EXPERIMENTAL } \ No newline at end of file diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt similarity index 100% rename from magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt rename to magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/magixModule.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt similarity index 100% rename from magix/magix-server/src/main/kotlin/space/kscience/magix/server/magixModule.kt rename to magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/server.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt similarity index 100% rename from magix/magix-server/src/main/kotlin/space/kscience/magix/server/server.kt rename to magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/sse.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/sse.kt similarity index 100% rename from magix/magix-server/src/main/kotlin/space/kscience/magix/server/sse.kt rename to magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/sse.kt From 24b6856f153a81ecef7ab9559707da129bb6e479 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sat, 11 May 2024 17:56:28 +0300 Subject: [PATCH 079/125] Move all-things to Compose --- build.gradle.kts | 1 - .../constructor/library/LimitSwitch.kt | 2 +- demo/all-things/build.gradle.kts | 51 ++-- .../controls/demo/DemoControllerView.kt | 225 +++++++++--------- .../controls/demo/generateMessageSchema.kt | 10 - demo/motors/build.gradle.kts | 17 +- settings.gradle.kts | 1 - 7 files changed, 146 insertions(+), 161 deletions(-) delete mode 100644 demo/all-things/src/main/kotlin/space/kscience/controls/demo/generateMessageSchema.kt diff --git a/build.gradle.kts b/build.gradle.kts index aed37b5..38d5c6a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,6 @@ import space.kscience.gradle.useSPCTeam plugins { id("space.kscience.gradle.project") - alias(libs.plugins.versions) } allprojects { diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt index 895cee8..af6436f 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt @@ -35,7 +35,7 @@ public class VirtualLimitSwitch( public val lockedState: DeviceState<Boolean>, ) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch { - init { + override suspend fun onStart() { lockedState.valueFlow.onEach { propertyChanged(LimitSwitch.locked, it) }.launchIn(this) diff --git a/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts index b6074ed..2abde80 100644 --- a/demo/all-things/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -1,7 +1,6 @@ plugins { kotlin("jvm") - id("org.openjfx.javafxplugin") version "0.0.13" - application + alias(spclibs.plugins.compose) } @@ -20,28 +19,46 @@ dependencies { implementation(projects.controlsOpcua) implementation(spclibs.ktor.client.cio) - implementation(libs.tornadofx) implementation(libs.plotlykt.server) // implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") + + implementation(compose.runtime) + implementation(compose.desktop.currentOs) + implementation(compose.material3) +// implementation("org.pushing-pixels:aurora-window:1.3.0") +// implementation("org.pushing-pixels:aurora-component:1.3.0") +// implementation("org.pushing-pixels:aurora-theming:1.3.0") + implementation(spclibs.logback.classic) } kotlin{ - jvmToolchain(11) -} - - -tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { - kotlinOptions { - freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") + jvmToolchain(17) + compilerOptions { + freeCompilerArgs.addAll("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") } } -javafx { - version = "17" - modules("javafx.controls") +compose{ + desktop{ + application{ + mainClass = "space.kscience.controls.demo.DemoControllerViewKt" + } + } } - -application { - mainClass.set("space.kscience.controls.demo.DemoControllerViewKt") -} \ No newline at end of file +// +// +//tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { +// kotlinOptions { +// freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") +// } +//} +// +//javafx { +// version = "17" +// modules("javafx.controls") +//} +// +//application { +// mainClass.set("space.kscience.controls.demo.DemoControllerViewKt") +//} \ 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 f341256..cc9d445 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 @@ -1,14 +1,19 @@ package space.kscience.controls.demo +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +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 androidx.compose.ui.window.rememberWindowState import io.ktor.server.engine.ApplicationEngine -import javafx.scene.Parent -import javafx.scene.control.Slider -import javafx.scene.layout.Priority -import javafx.stage.Stage +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import org.eclipse.milo.opcua.sdk.server.OpcUaServer import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText @@ -17,9 +22,6 @@ 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 import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install import space.kscience.controls.opcua.server.OpcUaServer @@ -35,16 +37,15 @@ import space.kscience.magix.rsocket.rSocketWithWebSockets import space.kscience.magix.server.RSocketMagixFlowPlugin import space.kscience.magix.server.startMagixServer import space.kscince.magix.zmq.ZmqMagixFlowPlugin -import tornadofx.* import java.awt.Desktop import java.net.URI -class DemoController : Controller(), ContextAware { +class DemoController : ContextAware { var device: DemoDevice? = null var magixServer: ApplicationEngine? = null var visualizer: ApplicationEngine? = null - var opcUaServer: OpcUaServer = OpcUaServer { + val opcUaServer: OpcUaServer = OpcUaServer { setApplicationName(LocalizedText.english("space.kscience.controls.opcua")) endpoint { @@ -60,39 +61,37 @@ class DemoController : Controller(), ContextAware { private val deviceManager = context.request(DeviceManager) - fun init() { - context.launch { - device = deviceManager.install("demo", DemoDevice) - //starting magix event loop - magixServer = startMagixServer( - RSocketMagixFlowPlugin(), //TCP rsocket support - ZmqMagixFlowPlugin() //ZMQ support - ) - //Launch a device client and connect it to the server - val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost") - deviceManager.launchMagixService(deviceEndpoint) - //connect visualization to a magix endpoint - val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") - visualizer = startDemoDeviceServer(visualEndpoint) + fun start(): Job = context.launch { + device = deviceManager.install("demo", DemoDevice) + //starting magix event loop + magixServer = startMagixServer( + RSocketMagixFlowPlugin(), //TCP rsocket support + ZmqMagixFlowPlugin() //ZMQ support + ) + //Launch a device client and connect it to the server + val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost") + deviceManager.launchMagixService(deviceEndpoint) + //connect visualization to a magix endpoint + val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + visualizer = startDemoDeviceServer(visualEndpoint) - //serve devices as OPC-UA namespace - opcUaServer.startup() - opcUaServer.serveDevices(deviceManager) + //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") + 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") - } } - suspend fun shutdown() { + fun shutdown(): Job = context.launch { logger.info { "Shutting down..." } opcUaServer.shutdown() logger.info { "OpcUa server stopped" } @@ -102,92 +101,82 @@ class DemoController : Controller(), ContextAware { logger.info { "Magix server stopped" } device?.stop() logger.info { "Device server stopped" } - context.close() + } +} + +@Composable +fun DemoControls(controller: DemoController) { + var timeScale by remember { mutableStateOf(5000f) } + var xScale by remember { mutableStateOf(1f) } + var yScale by remember { mutableStateOf(1f) } + + Surface(Modifier.padding(5.dp)) { + Column { + Row(Modifier.fillMaxWidth()) { + Text("Time Scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) + TextField(String.format("%.2f", timeScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) + Slider(timeScale, onValueChange = { timeScale = it }, steps = 20, valueRange = 1000f..5000f) + } + Row(Modifier.fillMaxWidth()) { + Text("X scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) + TextField(String.format("%.2f", xScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) + Slider(xScale, onValueChange = { xScale = it }, steps = 20, valueRange = 0.1f..2.0f) + } + Row(Modifier.fillMaxWidth()) { + Text("Y scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) + TextField(String.format("%.2f", yScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) + Slider(yScale, onValueChange = { yScale = it }, steps = 20, valueRange = 0.1f..2.0f) + } + Row(Modifier.fillMaxWidth()) { + Button( + onClick = { + controller.device?.run { + launch { + write(DemoDevice.timeScale, timeScale.toDouble()) + write(DemoDevice.sinScale, xScale.toDouble()) + write(DemoDevice.cosScale, yScale.toDouble()) + } + } + }, + Modifier.fillMaxWidth() + ) { + Text("Submit") + } + } + Row(Modifier.fillMaxWidth()) { + Button( + onClick = { + controller.visualizer?.run { + val host = "localhost"//environment.connectors.first().host + val port = environment.connectors.first().port + val uri = URI("http", null, host, port, "/", null, null) + Desktop.getDesktop().browse(uri) + } + }, + Modifier.fillMaxWidth() + ) { + Text("Show plots") + } + } + } + } } -class DemoControllerView : View(title = " Demo controller remote") { - private val controller: DemoController by inject() - private var timeScaleSlider: Slider by singleAssign() - private var xScaleSlider: Slider by singleAssign() - private var yScaleSlider: Slider by singleAssign() +fun main() = application { + val controller = remember { DemoController().apply { start() } } - override val root: Parent = vbox { - hbox { - label("Time scale") - pane { - hgrow = Priority.ALWAYS - } - timeScaleSlider = slider(1000..10000, 5000) { - isShowTickLabels = true - isShowTickMarks = true - } - } - hbox { - label("X scale") - pane { - hgrow = Priority.ALWAYS - } - xScaleSlider = slider(0.1..2.0, 1.0) { - isShowTickLabels = true - isShowTickMarks = true - } - } - hbox { - label("Y scale") - pane { - hgrow = Priority.ALWAYS - } - yScaleSlider = slider(0.1..2.0, 1.0) { - isShowTickLabels = true - isShowTickMarks = true - } - } - button("Submit") { - useMaxWidth = true - action { - controller.device?.run { - launch { - write(timeScale, timeScaleSlider.value) - write(sinScale, xScaleSlider.value) - write(cosScale, yScaleSlider.value) - } - } - } - } - button("Show plots") { - useMaxWidth = true - action { - controller.visualizer?.run { - val host = "localhost"//environment.connectors.first().host - val port = environment.connectors.first().port - val uri = URI("http", null, host, port, "/", null, null) - Desktop.getDesktop().browse(uri) - } - } - } - } -} - - -class DemoControllerApp : App(DemoControllerView::class) { - private val controller: DemoController by inject() - - override fun start(stage: Stage) { - super.start(stage) - controller.init() - } - - override fun stop() { - runBlocking { + Window( + title = "All things control", + onCloseRequest = { controller.shutdown() + exitApplication() + }, + state = rememberWindowState(width = 400.dp, height = 300.dp) + ) { + MaterialTheme { + DemoControls(controller) } - super.stop() } -} - - -fun main() { - launch<DemoControllerApp>() } \ No newline at end of file diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/generateMessageSchema.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/generateMessageSchema.kt deleted file mode 100644 index d50ec2c..0000000 --- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/generateMessageSchema.kt +++ /dev/null @@ -1,10 +0,0 @@ -package space.kscience.controls.demo - -//import com.github.ricky12awesome.jss.encodeToSchema -//import com.github.ricky12awesome.jss.globalJson -//import space.kscience.controls.api.DeviceMessage - -//fun main() { -// val schema = globalJson.encodeToSchema(DeviceMessage.serializer(), generateDefinitions = false) -// println(schema) -//} \ No newline at end of file diff --git a/demo/motors/build.gradle.kts b/demo/motors/build.gradle.kts index dd64935..faca2b0 100644 --- a/demo/motors/build.gradle.kts +++ b/demo/motors/build.gradle.kts @@ -1,19 +1,11 @@ plugins { id("space.kscience.gradle.jvm") - application - id("org.openjfx.javafxplugin") + alias(spclibs.plugins.compose) } -//TODO to be moved to a separate project - -javafx { - version = "17" - modules = listOf("javafx.controls") -} - -application{ - mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt") -} +//application{ +// mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt") +//} kotlin{ explicitApi = null @@ -25,5 +17,4 @@ val dataforgeVersion: String by extra dependencies { implementation(project(":controls-ports-ktor")) implementation(projects.controlsMagix) - implementation(libs.tornadofx) } diff --git a/settings.gradle.kts b/settings.gradle.kts index ed33e53..39adc72 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,7 +18,6 @@ pluginManagement { id("space.kscience.gradle.mpp") version toolsVersion id("space.kscience.gradle.jvm") version toolsVersion id("space.kscience.gradle.js") version toolsVersion - id("org.openjfx.javafxplugin") version "0.0.13" } } From 44514cd47772d494aee71683a5851836ab0dbf65 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 12 May 2024 13:52:00 +0300 Subject: [PATCH 080/125] [WIP] moving from java fx to compose in examples --- .../space/kscience/controls/api/DeviceHub.kt | 2 +- .../controls/demo/DemoControllerView.kt | 4 +- demo/motors/build.gradle.kts | 17 +- .../pimotionmaster/PiMotionMasterApp.kt | 327 ++++++++++-------- .../pimotionmaster/PiMotionMasterDevice.kt | 26 +- .../pimotionmaster/deviceProperties.kt | 15 + .../pimotionmaster/fxDeviceProperties.kt | 58 ---- .../devices/pimotionmaster/piDebugServer.kt | 60 ++-- 8 files changed, 259 insertions(+), 250 deletions(-) create mode 100644 demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/deviceProperties.kt delete mode 100644 demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt 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 72b0dc3..f6ca4ec 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 @@ -48,7 +48,7 @@ public operator fun DeviceHub.get(nameToken: NameToken): Device = public fun DeviceHub.getOrNull(name: Name): Device? = when { name.isEmpty() -> this as? Device - name.length == 1 -> get(name.firstOrNull()!!) + name.length == 1 -> devices[name.firstOrNull()!!] else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst()) } 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 cc9d445..ff99f32 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 @@ -1,7 +1,7 @@ package space.kscience.controls.demo import androidx.compose.foundation.layout.* -import androidx.compose.material.* +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -173,7 +173,7 @@ fun main() = application { controller.shutdown() exitApplication() }, - state = rememberWindowState(width = 400.dp, height = 300.dp) + state = rememberWindowState(width = 400.dp, height = 320.dp) ) { MaterialTheme { DemoControls(controller) diff --git a/demo/motors/build.gradle.kts b/demo/motors/build.gradle.kts index faca2b0..9241764 100644 --- a/demo/motors/build.gradle.kts +++ b/demo/motors/build.gradle.kts @@ -3,10 +3,6 @@ plugins { alias(spclibs.plugins.compose) } -//application{ -// mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt") -//} - kotlin{ explicitApi = null } @@ -17,4 +13,17 @@ val dataforgeVersion: String by extra dependencies { implementation(project(":controls-ports-ktor")) implementation(projects.controlsMagix) + + implementation(compose.runtime) + implementation(compose.desktop.currentOs) + implementation(compose.material3) + implementation(spclibs.logback.classic) +} + +compose{ + desktop{ + application{ + mainClass = "ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt" + } + } } diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt index 840b1d9..0762e9f 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt @@ -1,31 +1,193 @@ package ru.mipt.npm.devices.pimotionmaster -import javafx.beans.property.ReadOnlyProperty -import javafx.beans.property.SimpleIntegerProperty -import javafx.beans.property.SimpleObjectProperty -import javafx.beans.property.SimpleStringProperty -import javafx.geometry.Pos -import javafx.scene.Parent -import javafx.scene.layout.Priority -import javafx.scene.layout.VBox -import kotlinx.coroutines.CoroutineScope + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Button +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Slider +import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.maxPosition -import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.minPosition -import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.position import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.installing import space.kscience.controls.spec.read import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.request -import tornadofx.* -class PiMotionMasterApp : App(PiMotionMasterView::class) +//class PiMotionMasterApp : App(PiMotionMasterView::class) +// +//class PiMotionMasterController : Controller() { +// //initialize context +// val context = Context("piMotionMaster") { +// plugin(DeviceManager) +// } +// +// //initialize deviceManager plugin +// val deviceManager: DeviceManager = context.request(DeviceManager) +// +// // install device +// val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice) +//} -class PiMotionMasterController : Controller() { - //initialize context - val context = Context("piMotionMaster"){ +@Composable +fun ColumnScope.piMotionMasterAxis( + axisName: String, + axis: PiMotionMasterDevice.Axis, +) { + Row { + Text(axisName) + var min by remember { mutableStateOf(0f) } + var max by remember { mutableStateOf(0f) } + var targetPosition by remember { mutableStateOf(0f) } + val position: Double by axis.composeState(PiMotionMasterDevice.Axis.position, 0.0) + + val scope = rememberCoroutineScope() + + LaunchedEffect(axis) { + min = axis.read(PiMotionMasterDevice.Axis.minPosition).toFloat() + max = axis.read(PiMotionMasterDevice.Axis.maxPosition).toFloat() + targetPosition = axis.read(PiMotionMasterDevice.Axis.position).toFloat() + } + + Column { + Slider( + value = position.toFloat(), + enabled = false, + onValueChange = { }, + valueRange = min..max + ) + Slider( + value = targetPosition, + onValueChange = { newPosition -> + scope.launch { + axis.move(newPosition.toDouble()) + } + targetPosition = newPosition + }, + valueRange = min..max + ) + + } + } +} + +@Composable +fun AxisPane(axes: Map<String, PiMotionMasterDevice.Axis>) { + Column { + axes.forEach { (name, axis) -> + this.piMotionMasterAxis(name, axis) + } + } +} + + +@Composable +fun PiMotionMasterApp(device: PiMotionMasterDevice) { + + val scope = rememberCoroutineScope() + val connected by device.composeState(PiMotionMasterDevice.connected, false) + var debugServerJob by remember { mutableStateOf<Job?>(null) } + var axes by remember { mutableStateOf<Map<String, PiMotionMasterDevice.Axis>?>(null) } + //private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>() + var host by remember { mutableStateOf("127.0.0.1") } + var port by remember { mutableStateOf(10024) } + + Scaffold { + Column { + + + Text("Address:") + Row { + OutlinedTextField( + value = host, + onValueChange = { host = it }, + label = { Text("Host") }, + enabled = debugServerJob == null, + modifier = Modifier.weight(1f) + ) + var portError by remember { mutableStateOf(false) } + OutlinedTextField( + value = port.toString(), + onValueChange = { + it.toIntOrNull()?.let { value -> + port = value + portError = false + } ?: run { + portError = true + } + }, + label = { Text("Port") }, + enabled = debugServerJob == null, + isError = portError, + modifier = Modifier.weight(1f), + ) + } + Row { + Button( + onClick = { + if (debugServerJob == null) { + debugServerJob = device.context.launchPiDebugServer(port, listOf("1", "2", "3", "4")) + } else { + debugServerJob?.cancel() + debugServerJob = null + } + }, + modifier = Modifier.fillMaxWidth() + ) { + if (debugServerJob == null) { + Text("Start debug server") + } else { + Text("Stop debug server") + } + } + } + Row { + Button( + onClick = { + if (!connected) { + device.launch { + device.connect(host, port) + } + axes = device.axes + } else { + device.launch { + device.disconnect() + } + axes = null + } + }, + modifier = Modifier.fillMaxWidth() + ) { + if (!connected) { + Text("Connect") + } else { + Text("Disconnect") + } + } + } + + axes?.let { axes -> + AxisPane(axes) + } + } + } +} + + +fun main() = application { + + val context = Context("piMotionMaster") { plugin(DeviceManager) } @@ -34,131 +196,14 @@ class PiMotionMasterController : Controller() { // install device val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice) -} -fun VBox.piMotionMasterAxis( - axisName: String, - axis: PiMotionMasterDevice.Axis, - coroutineScope: CoroutineScope, -) = hbox { - alignment = Pos.CENTER - label(axisName) - coroutineScope.launch { - with(axis) { - val min: Double = read(minPosition) - val max: Double = read(maxPosition) - val positionProperty = fxProperty(position) - val startPosition = read(position) - runLater { - vbox { - hgrow = Priority.ALWAYS - slider(min..max, startPosition) { - minWidth = 300.0 - isShowTickLabels = true - isShowTickMarks = true - minorTickCount = 10 - majorTickUnit = 1.0 - valueProperty().onChange { - coroutineScope.launch { - axis.move(value) - } - } - } - slider(min..max) { - isDisable = true - valueProperty().bind(positionProperty) - } - } - } + Window( + title = "Pi motion master demo", + onCloseRequest = { exitApplication() }, + state = rememberWindowState(width = 400.dp, height = 300.dp) + ) { + MaterialTheme { + PiMotionMasterApp(motionMaster) } } -} - -fun Parent.axisPane(axes: Map<String, PiMotionMasterDevice.Axis>, coroutineScope: CoroutineScope) { - vbox { - axes.forEach { (name, axis) -> - this.piMotionMasterAxis(name, axis, coroutineScope) - } - } -} - - -class PiMotionMasterView : View() { - - private val controller: PiMotionMasterController by inject() - val device = controller.motionMaster - - private val connectedProperty: ReadOnlyProperty<Boolean> = device.fxProperty(PiMotionMasterDevice.connected) - private val debugServerJobProperty = SimpleObjectProperty<Job>() - private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null } - //private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>() - - override val root: Parent = borderpane { - top { - form { - val host = SimpleStringProperty("127.0.0.1") - val port = SimpleIntegerProperty(10024) - fieldset("Address:") { - field("Host:") { - textfield(host) { - enableWhen(debugServerStarted.not()) - } - } - field("Port:") { - textfield(port) { - stripNonNumeric() - } - button { - hgrow = Priority.ALWAYS - textProperty().bind(debugServerStarted.stringBinding { - if (it != true) { - "Start debug server" - } else { - "Stop debug server" - } - }) - action { - if (!debugServerStarted.get()) { - debugServerJobProperty.value = - controller.context.launchPiDebugServer(port.get(), listOf("1", "2", "3", "4")) - } else { - debugServerJobProperty.get().cancel() - debugServerJobProperty.value = null - } - } - } - } - } - - button { - hgrow = Priority.ALWAYS - textProperty().bind(connectedProperty.stringBinding { - if (it == false) { - "Connect" - } else { - "Disconnect" - } - }) - action { - if (!connectedProperty.value) { - device.connect(host.get(), port.get()) - center { - axisPane(device.axes,controller.context) - } - } else { - this@borderpane.center = null - device.disconnect() - } - } - } - - - } - } - - } -} - -fun main() { - launch<PiMotionMasterApp>() } \ No newline at end of file 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 69dfb79..4bde0b6 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 @@ -7,13 +7,15 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.transformWhile -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeout import space.kscience.controls.api.DeviceHub import space.kscience.controls.api.PropertyDescriptor -import space.kscience.controls.ports.* +import space.kscience.controls.ports.AsynchronousPort +import space.kscience.controls.ports.KtorTcpPort +import space.kscience.controls.ports.send +import space.kscience.controls.ports.withStringDelimiter import space.kscience.controls.spec.* import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.* @@ -33,10 +35,8 @@ class PiMotionMasterDevice( //PortProxy { portFactory(address ?: error("The device is not connected"), context) } - fun disconnect() { - runBlocking { - execute(disconnect) - } + suspend fun disconnect() { + execute(disconnect) } var timeoutValue: Duration = 200.milliseconds @@ -54,13 +54,11 @@ class PiMotionMasterDevice( if (errorCode != 0) error(message(errorCode)) } - fun connect(host: String, port: Int) { - runBlocking { - execute(connect, Meta { - "host" put host - "port" put port - }) - } + suspend fun connect(host: String, port: Int) { + execute(connect, Meta { + "host" put host + "port" put port + }) } private val mutex = Mutex() @@ -103,7 +101,7 @@ class PiMotionMasterDevice( }.toList() } } catch (ex: Throwable) { - logger.warn { "Error during PIMotionMaster request. Requesting error code." } + logger.error(ex) { "Error during PIMotionMaster request. Requesting error code." } val errorCode = getErrorCode() dispatchError(errorCode) logger.warn { "Error code $errorCode" } diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/deviceProperties.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/deviceProperties.kt new file mode 100644 index 0000000..00a4c55 --- /dev/null +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/deviceProperties.kt @@ -0,0 +1,15 @@ +package ru.mipt.npm.devices.pimotionmaster + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import space.kscience.controls.api.Device +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.propertyFlow + + +@Composable +fun <D : Device, T : Any> D.composeState( + spec: DevicePropertySpec<D, T>, + initialState: T, +): State<T> = propertyFlow(spec).collectAsState(initialState) \ No newline at end of file diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt deleted file mode 100644 index adef540..0000000 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt +++ /dev/null @@ -1,58 +0,0 @@ -package ru.mipt.npm.devices.pimotionmaster - -import javafx.beans.property.ObjectPropertyBase -import javafx.beans.property.Property -import javafx.beans.property.ReadOnlyProperty -import space.kscience.controls.api.Device -import space.kscience.controls.spec.* -import space.kscience.dataforge.context.info -import space.kscience.dataforge.context.logger -import tornadofx.* - -/** - * Bind a FX property to a device property with a given [spec] - */ -fun <D : Device, T : Any> D.fxProperty( - spec: DevicePropertySpec<D, T>, -): ReadOnlyProperty<T> = object : ObjectPropertyBase<T>() { - override fun getBean(): Any = this - override fun getName(): String = spec.name - - init { - //Read incoming changes - onPropertyChange(spec) { - runLater { - try { - set(it) - } catch (ex: Throwable) { - logger.info { "Failed to set property $name to $it" } - } - } - } - } -} - -fun <D : Device, T : Any> D.fxProperty(spec: MutableDevicePropertySpec<D, T>): Property<T> = - object : ObjectPropertyBase<T>() { - override fun getBean(): Any = this - override fun getName(): String = spec.name - - init { - //Read incoming changes - onPropertyChange(spec) { - runLater { - try { - set(it) - } catch (ex: Throwable) { - logger.info { "Failed to set property $name to $it" } - } - } - } - - onChange { newValue -> - if (newValue != null) { - writeAsync(spec, newValue) - } - } - } - } 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 c69b7fe..2de548d 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 @@ -18,41 +18,41 @@ val exceptionHandler = CoroutineExceptionHandler { _, throwable -> @OptIn(InternalAPI::class) fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) { val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes) - val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port) - println("Started virtual port server at ${server.localAddress}") + aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port).use { server -> + println("Started virtual port server at ${server.localAddress}") - while (isActive) { - val socket = server.accept() - launch(SupervisorJob(coroutineContext[Job])) { - println("Socket accepted: ${socket.remoteAddress}") - val input = socket.openReadChannel() - val output = socket.openWriteChannel() + while (isActive) { + val socket = server.accept() + launch(SupervisorJob(coroutineContext[Job])) { + println("Socket accepted: ${socket.remoteAddress}") + val input = socket.openReadChannel() + val output = socket.openWriteChannel() - val sendJob = launch { - virtualDevice.subscribe().collect { - //println("Sending: ${it.decodeToString()}") - output.writeAvailable(it) - output.flush() - } - } - - try { - while (isActive) { - input.read { buffer -> - val array = buffer.moveToByteArray() - launch { - virtualDevice.send(array) - } + val sendJob = launch { + virtualDevice.subscribe().collect { + //println("Sending: ${it.decodeToString()}") + output.writeAvailable(it) + output.flush() } } - } catch (e: Throwable) { - e.printStackTrace() - sendJob.cancel() - socket.close() - } finally { - println("Socket closed") - } + try { + while (isActive) { + input.read { buffer -> + val array = buffer.moveToByteArray() + launch { + virtualDevice.send(array) + } + } + } + } catch (e: Throwable) { + e.printStackTrace() + sendJob.cancel() + socket.close() + } finally { + println("Client socket closed") + } + } } } } From a9592d0372e07a32f9461a8493d2fb6573fca5ca Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 12 May 2024 14:14:31 +0300 Subject: [PATCH 081/125] Fixed part of motionMaster --- .../controls/ports/AsynchronousPort.kt | 1 + .../pimotionmaster/PiMotionMasterApp.kt | 32 ++++++++++--------- .../pimotionmaster/PiMotionMasterDevice.kt | 4 +-- .../devices/pimotionmaster/piDebugServer.kt | 15 +++++---- 4 files changed, 28 insertions(+), 24 deletions(-) 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 index 4462a42..3f89e75 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/AsynchronousPort.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/AsynchronousPort.kt @@ -100,6 +100,7 @@ public abstract class AbstractAsynchronousPort( * Send a data packet via the port */ override suspend fun send(data: ByteArray) { + check(isOpen){"The port is not opened"} outgoing.send(data) } diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt index 0762e9f..ab1c47a 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt @@ -45,20 +45,22 @@ fun ColumnScope.piMotionMasterAxis( axisName: String, axis: PiMotionMasterDevice.Axis, ) { + var min by remember { mutableStateOf(0f) } + var max by remember { mutableStateOf(1f) } + var targetPosition by remember { mutableStateOf(0f) } + val position: Double by axis.composeState(PiMotionMasterDevice.Axis.position, 0.0) + + val scope = rememberCoroutineScope() + + LaunchedEffect(axis) { + min = axis.read(PiMotionMasterDevice.Axis.minPosition).toFloat() + max = axis.read(PiMotionMasterDevice.Axis.maxPosition).toFloat() + targetPosition = axis.read(PiMotionMasterDevice.Axis.position).toFloat() + } + + Row { Text(axisName) - var min by remember { mutableStateOf(0f) } - var max by remember { mutableStateOf(0f) } - var targetPosition by remember { mutableStateOf(0f) } - val position: Double by axis.composeState(PiMotionMasterDevice.Axis.position, 0.0) - - val scope = rememberCoroutineScope() - - LaunchedEffect(axis) { - min = axis.read(PiMotionMasterDevice.Axis.minPosition).toFloat() - max = axis.read(PiMotionMasterDevice.Axis.maxPosition).toFloat() - targetPosition = axis.read(PiMotionMasterDevice.Axis.position).toFloat() - } Column { Slider( @@ -70,10 +72,10 @@ fun ColumnScope.piMotionMasterAxis( Slider( value = targetPosition, onValueChange = { newPosition -> + targetPosition = newPosition scope.launch { axis.move(newPosition.toDouble()) } - targetPosition = newPosition }, valueRange = min..max ) @@ -158,13 +160,13 @@ fun PiMotionMasterApp(device: PiMotionMasterDevice) { if (!connected) { device.launch { device.connect(host, port) + axes = device.axes } - axes = device.axes } else { device.launch { device.disconnect() + axes = null } - axes = null } }, modifier = Modifier.fillMaxWidth() 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 4bde0b6..6c271d7 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 @@ -165,12 +165,12 @@ class PiMotionMasterDevice( } //Update port //address = portSpec.node - port = portFactory(portSpec, context) - propertyChanged(connected, true) + port = portFactory(portSpec, context).apply { open() } // connector.open() //Initialize axes val idn = read(identity) failIfError { "Can't connect to $portSpec. Error code: $it" } + propertyChanged(connected, true) logger.info { "Connected to $idn on $portSpec" } val ids = request("SAI?").map { it.trim() } if (ids != axes.keys.toList()) { 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 2de548d..4c2b350 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 @@ -8,6 +8,8 @@ import io.ktor.util.InternalAPI import io.ktor.util.moveToByteArray import io.ktor.utils.io.writeAvailable import kotlinx.coroutines.* +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Global @@ -28,13 +30,12 @@ fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exc val input = socket.openReadChannel() val output = socket.openWriteChannel() - val sendJob = launch { - virtualDevice.subscribe().collect { - //println("Sending: ${it.decodeToString()}") - output.writeAvailable(it) - output.flush() - } - } + val sendJob = virtualDevice.subscribe().onEach { + //println("Sending: ${it.decodeToString()}") + output.writeAvailable(it) + output.flush() + }.launchIn(this) + try { while (isActive) { From 5921978122b501e04e6b744f7d4e7dbfdba79302 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Wed, 15 May 2024 22:49:08 +0300 Subject: [PATCH 082/125] [WIP] Refactor constructor --- README.md | 10 ++ controls-constructor/README.md | 4 +- .../{ConstructorBinding.kt => Binding.kt} | 17 +- .../controls/constructor/DeviceConstructor.kt | 10 +- .../controls/constructor/DeviceModel.kt | 12 ++ .../controls/constructor/DeviceState.kt | 16 +- .../controls/constructor/TimerState.kt | 5 +- .../controls/constructor/customState.kt | 20 ++- .../controls/constructor/internalState.kt | 2 - controls-core/README.md | 4 +- .../kscience/controls/manager/ClockManager.kt | 90 ++++++++-- .../controls/spec/deviceExtensions.kt | 6 +- controls-jupyter/README.md | 4 +- controls-magix/README.md | 4 +- controls-magix/build.gradle.kts | 3 +- controls-modbus/README.md | 4 +- controls-opcua/README.md | 4 +- controls-pi/README.md | 4 +- controls-ports-ktor/README.md | 4 +- controls-serial/README.md | 4 +- controls-server/README.md | 4 +- controls-storage/README.md | 4 +- controls-storage/controls-xodus/README.md | 4 +- controls-vision/README.md | 6 +- controls-vision/build.gradle.kts | 1 + controls-vision/docs/README-TEMPLATE.md | 15 ++ .../kotlin/BooleanIndicatorVision.kt | 24 +++ .../commonMain/kotlin/ControlVisionPlugin.kt | 3 +- .../src/commonMain/kotlin/IndicatorVision.kt | 20 --- .../src/commonMain/kotlin/plotExtensions.kt | 2 +- .../jsMain/kotlin/ControlsVisionPlugin.js.kt | 16 ++ .../src/jvmMain/kotlin/dashboard.kt | 47 ++++-- .../kotlin/{main.kt => LinearDrive.kt} | 155 ++++++++++-------- .../pimotionmaster/PiMotionMasterApp.kt | 2 +- docs/templates/ARTIFACT-TEMPLATE.md | 30 ---- magix/magix-api/README.md | 4 +- magix/magix-java-endpoint/README.md | 4 +- magix/magix-mqtt/README.md | 4 +- magix/magix-rabbit/README.md | 4 +- magix/magix-rsocket/README.md | 4 +- magix/magix-server/README.md | 4 +- magix/magix-storage/README.md | 4 +- .../magix-storage-xodus/README.md | 4 +- magix/magix-zmq/README.md | 4 +- 44 files changed, 367 insertions(+), 229 deletions(-) rename controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/{ConstructorBinding.kt => Binding.kt} (67%) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt create mode 100644 controls-vision/docs/README-TEMPLATE.md create mode 100644 controls-vision/src/commonMain/kotlin/BooleanIndicatorVision.kt delete mode 100644 controls-vision/src/commonMain/kotlin/IndicatorVision.kt rename demo/constructor/src/jvmMain/kotlin/{main.kt => LinearDrive.kt} (74%) delete mode 100644 docs/templates/ARTIFACT-TEMPLATE.md diff --git a/README.md b/README.md index d6a602b..d5baaf9 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,11 @@ Automatically checks consistency. > > **Maturity**: EXPERIMENTAL +### [controls-plc4x](controls-plc4x) +> A plugin for Controls-kt device server on top of plc4x library +> +> **Maturity**: EXPERIMENTAL + ### [controls-ports-ktor](controls-ports-ktor) > Implementation of byte ports on top os ktor-io asynchronous API > @@ -209,6 +214,11 @@ Automatically checks consistency. > > **Maturity**: PROTOTYPE +### [magix/magix-utils](magix/magix-utils) +> Common utilities and services for Magix endpoints. +> +> **Maturity**: EXPERIMENTAL + ### [magix/magix-zmq](magix/magix-zmq) > ZMQ client endpoint for Magix > diff --git a/controls-constructor/README.md b/controls-constructor/README.md index 9a8b27c..d388b01 100644 --- a/controls-constructor/README.md +++ b/controls-constructor/README.md @@ -6,7 +6,7 @@ A low-code constructor for composite devices simulation ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-constructor:0.3.0`. +The Maven coordinates of this project are `space.kscience:controls-constructor:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-constructor:0.3.0") + implementation("space.kscience:controls-constructor:0.4.0-dev-1") } ``` diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorBinding.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Binding.kt similarity index 67% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorBinding.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Binding.kt index 7ad370e..d712d62 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorBinding.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Binding.kt @@ -2,7 +2,10 @@ package space.kscience.controls.constructor import space.kscience.controls.api.Device -public sealed interface ConstructorBinding +/** + * A binding that is used to describe device functionality + */ +public sealed interface Binding /** * A binding that exposes device property as read-only state @@ -11,16 +14,22 @@ public class PropertyBinding<T>( public val device: Device, public val propertyName: String, public val state: DeviceState<T>, -) : ConstructorBinding +) : Binding /** * A binding for independent state like a timer */ public class StateBinding<T>( public val state: DeviceState<T> -) : ConstructorBinding +) : Binding public class ActionBinding( public val reads: Collection<DeviceState<*>>, public val writes: Collection<DeviceState<*>> -): ConstructorBinding \ No newline at end of file +): Binding + + +public interface BindingsContainer{ + public val bindings: List<Binding> + public fun registerBinding(binding: Binding) +} \ No newline at end of file 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 7824e3c..5b6ff9f 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 @@ -26,11 +26,11 @@ import kotlin.time.Duration public abstract class DeviceConstructor( context: Context, meta: Meta = Meta.EMPTY, -) : DeviceGroup(context, meta) { - private val _bindings: MutableList<ConstructorBinding> = mutableListOf() - public val bindings: List<ConstructorBinding> get() = _bindings +) : DeviceGroup(context, meta), BindingsContainer { + private val _bindings: MutableList<Binding> = mutableListOf() + override val bindings: List<Binding> get() = _bindings - public fun registerBinding(binding: ConstructorBinding) { + override fun registerBinding(binding: Binding) { _bindings.add(binding) } @@ -46,7 +46,7 @@ public abstract class DeviceConstructor( .also { registerBinding(StateBinding(it)) } /** - * Launch action that is performed on each [DeviceState] value change. + * Bind an action to a [DeviceState]. [onChange] block is performed on each state change * * Optionally provide [writes] - a set of states that this change affects. */ diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt new file mode 100644 index 0000000..08913a2 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt @@ -0,0 +1,12 @@ +package space.kscience.controls.constructor + +public abstract class DeviceModel : BindingsContainer { + + private val _bindings: MutableList<Binding> = mutableListOf() + + override val bindings: List<Binding> get() = _bindings + + override fun registerBinding(binding: Binding) { + _bindings.add(binding) + } +} \ 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 9a25f00..8a2914b 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 @@ -63,25 +63,25 @@ public interface DeviceStateWithDependencies<T> : DeviceState<T> { /** * Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper]. */ -public fun <T, R> DeviceState<T>.map( +public fun <T, R> DeviceState.Companion.map( + state: DeviceState<T>, converter: MetaConverter<R>, mapper: (T) -> R, ): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> { - override val dependencies = listOf(this) + override val dependencies = listOf(state) override val converter: MetaConverter<R> = converter - override val value: R - get() = mapper(this@map.value) + override val value: R get() = mapper(state.value) - override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper) + override val valueFlow: Flow<R> = state.valueFlow.map(mapper) - override fun toString(): String = "DeviceState.map(arg=${this@map}, converter=$converter)" + override fun toString(): String = "DeviceState.map(arg=${state}, converter=$converter)" } /** * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used. */ -public fun <T1, T2, R> combine( +public fun <T1, T2, R> DeviceState.Companion.combine( state1: DeviceState<T1>, state2: DeviceState<T2>, converter: MetaConverter<R>, @@ -96,4 +96,4 @@ public fun <T1, T2, R> combine( override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper) override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2)" -} +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt index 7b10cb9..8f36079 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt @@ -1,5 +1,6 @@ package space.kscience.controls.constructor +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.isActive @@ -26,9 +27,9 @@ public class TimerState( private val clock = MutableStateFlow(clockManager.clock.now()) - private val updateJob = clockManager.context.launch { + private val updateJob = clockManager.context.launch(clockManager.asDispatcher()) { while (isActive) { - clockManager.delay(tick) + delay(tick) clock.value = clockManager.clock.now() } } 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 881bdea..277271a 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 @@ -8,7 +8,7 @@ import space.kscience.dataforge.meta.MetaConverter /** * A state describing a [Double] value in the [range] */ -public class DoubleRangeState( +public class DoubleInRangeState( initialValue: Double, public val range: ClosedFloatingPointRange<Double>, ) : MutableDeviceState<Double> { @@ -32,20 +32,28 @@ public class DoubleRangeState( /** * A state showing that the range is on its lower boundary */ - public val atStartState: DeviceState<Boolean> = map(MetaConverter.boolean) { it <= range.start } + public val atStart: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) { + it <= range.start + } /** * A state showing that the range is on its higher boundary */ - public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive } + public val atEnd: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) { + it >= range.endInclusive + } override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)" } -@Suppress("UnusedReceiverParameter") -public fun DeviceGroup.rangeState( +/** + * Create and register a [DoubleInRangeState] + */ +public fun BindingsContainer.doubleInRangeState( initialValue: Double, range: ClosedFloatingPointRange<Double>, -): DoubleRangeState = DoubleRangeState(initialValue, range) \ No newline at end of file +): DoubleInRangeState = DoubleInRangeState(initialValue, range).also { + registerBinding(StateBinding(it)) +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt index 6f9d086..ed2d5d1 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt @@ -25,8 +25,6 @@ private class VirtualDeviceState<T>( } override fun toString(): String = "VirtualDeviceState(converter=$converter)" - - } diff --git a/controls-core/README.md b/controls-core/README.md index b1edbdc..71caf53 100644 --- a/controls-core/README.md +++ b/controls-core/README.md @@ -16,7 +16,7 @@ Core interfaces for building a device server ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-core:0.3.0`. +The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -26,6 +26,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-core:0.3.0") + implementation("space.kscience:controls-core:0.4.0-dev-1") } ``` 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 index 2f852e1..20ba47e 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt @@ -1,24 +1,80 @@ package space.kscience.controls.manager +import kotlinx.coroutines.* 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 kotlinx.datetime.Instant +import space.kscience.controls.api.Device +import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.Meta -import kotlin.time.Duration +import space.kscience.dataforge.meta.double +import kotlin.coroutines.CoroutineContext +import kotlin.math.roundToLong + +@OptIn(InternalCoroutinesApi::class) +private class CompressedTimeDispatcher( + val dispatcher: CoroutineDispatcher, + val compression: Double, +) : CoroutineDispatcher(), Delay { + + @InternalCoroutinesApi + override fun dispatchYield(context: CoroutineContext, block: Runnable) { + dispatcher.dispatchYield(context, block) + } + + override fun isDispatchNeeded(context: CoroutineContext): Boolean = dispatcher.isDispatchNeeded(context) + + @ExperimentalCoroutinesApi + override fun limitedParallelism(parallelism: Int): CoroutineDispatcher = dispatcher.limitedParallelism(parallelism) + + override fun dispatch(context: CoroutineContext, block: Runnable) { + dispatcher.dispatch(context, block) + } + + private val delay = ((dispatcher as? Delay) ?: (Dispatchers.Default as Delay)) + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) { + delay.scheduleResumeAfterDelay((timeMillis / compression).roundToLong(), continuation) + } + + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + return delay.invokeOnTimeout((timeMillis / compression).roundToLong(), block, context) + } +} + +private class CompressedClock( + val start: Instant, + val compression: Double, + val baseClock: Clock = Clock.System, +) : Clock { + override fun now(): Instant { + val elapsed = (baseClock.now() - start) + return start + elapsed / compression + } +} public class ClockManager : AbstractPlugin() { override val tag: PluginTag get() = Companion.tag + public val timeCompression: Double by meta.double(1.0) + public val clock: Clock by lazy { - //TODO add clock customization - Clock.System + if (timeCompression == 1.0) { + Clock.System + } else { + CompressedClock(Clock.System.now(), timeCompression) + } } - public suspend fun delay(duration: Duration) { - //TODO add time compression - kotlinx.coroutines.delay(duration) + /** + * Provide a [CoroutineDispatcher] with compressed time based on given [dispatcher] + */ + public fun asDispatcher( + dispatcher: CoroutineDispatcher = Dispatchers.Default, + ): CoroutineDispatcher = if (timeCompression == 1.0) { + dispatcher + } else { + CompressedTimeDispatcher(dispatcher, timeCompression) } public companion object : PluginFactory<ClockManager> { @@ -28,4 +84,16 @@ public class ClockManager : AbstractPlugin() { } } -public val Context.clock: Clock get() = plugins[ClockManager]?.clock ?: Clock.System \ No newline at end of file +public val Context.clock: Clock get() = plugins[ClockManager]?.clock ?: Clock.System + +public val Device.clock: Clock get() = context.clock + +public fun Device.getCoroutineDispatcher(dispatcher: CoroutineDispatcher = Dispatchers.Default): CoroutineDispatcher = + context.plugins[ClockManager]?.asDispatcher(dispatcher) ?: dispatcher + +public fun ContextBuilder.withTimeCompression(compression: Double) { + require(compression > 0.0) { "Time compression must be greater than zero." } + plugin(ClockManager) { + "timeCompression" put compression + } +} \ 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 ee47ea0..3344407 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 @@ -4,6 +4,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import space.kscience.controls.api.Device +import space.kscience.controls.manager.getCoroutineDispatcher import kotlin.time.Duration /** @@ -15,11 +16,12 @@ public fun <D : Device> D.doRecurring( task: suspend D.() -> Unit, ): Job { val taskName = debugTaskName ?: "task[${task.hashCode().toString(16)}]" - return launch(CoroutineName(taskName)) { + val dispatcher = getCoroutineDispatcher() + return launch(CoroutineName(taskName) + dispatcher) { while (isActive) { delay(interval) //launch in parent scope to properly evaluate exceptions - this@doRecurring.launch { + this@doRecurring.launch(CoroutineName("$taskName-recurring") + dispatcher) { task() } } diff --git a/controls-jupyter/README.md b/controls-jupyter/README.md index 15d8e2d..7d0fc4f 100644 --- a/controls-jupyter/README.md +++ b/controls-jupyter/README.md @@ -6,7 +6,7 @@ ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-jupyter:0.3.0`. +The Maven coordinates of this project are `space.kscience:controls-jupyter:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-jupyter:0.3.0") + implementation("space.kscience:controls-jupyter:0.4.0-dev-1") } ``` diff --git a/controls-magix/README.md b/controls-magix/README.md index 4048990..8c16ffd 100644 --- a/controls-magix/README.md +++ b/controls-magix/README.md @@ -12,7 +12,7 @@ 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.3.0`. +The Maven coordinates of this project are `space.kscience:controls-magix:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -22,6 +22,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-magix:0.3.0") + implementation("space.kscience:controls-magix:0.4.0-dev-1") } ``` diff --git a/controls-magix/build.gradle.kts b/controls-magix/build.gradle.kts index 055ec98..040c040 100644 --- a/controls-magix/build.gradle.kts +++ b/controls-magix/build.gradle.kts @@ -12,7 +12,8 @@ description = """ kscience { jvm() js() - useCoroutines("1.8.0") + native() + useCoroutines() useSerialization { json() } diff --git a/controls-modbus/README.md b/controls-modbus/README.md index 18c37ee..61e4b60 100644 --- a/controls-modbus/README.md +++ b/controls-modbus/README.md @@ -14,7 +14,7 @@ Automatically checks consistency. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-modbus:0.3.0`. +The Maven coordinates of this project are `space.kscience:controls-modbus:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -24,6 +24,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-modbus:0.3.0") + implementation("space.kscience:controls-modbus:0.4.0-dev-1") } ``` diff --git a/controls-opcua/README.md b/controls-opcua/README.md index 3befbb9..03dc32b 100644 --- a/controls-opcua/README.md +++ b/controls-opcua/README.md @@ -12,7 +12,7 @@ A client and server connectors for OPC-UA via Eclipse Milo ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-opcua:0.3.0`. +The Maven coordinates of this project are `space.kscience:controls-opcua:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -22,6 +22,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-opcua:0.3.0") + implementation("space.kscience:controls-opcua:0.4.0-dev-1") } ``` diff --git a/controls-pi/README.md b/controls-pi/README.md index 6235995..2873ac2 100644 --- a/controls-pi/README.md +++ b/controls-pi/README.md @@ -6,7 +6,7 @@ Utils to work with controls-kt on Raspberry pi ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-pi:0.3.0`. +The Maven coordinates of this project are `space.kscience:controls-pi:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-pi:0.3.0") + implementation("space.kscience:controls-pi:0.4.0-dev-1") } ``` diff --git a/controls-ports-ktor/README.md b/controls-ports-ktor/README.md index b703521..6b23d80 100644 --- a/controls-ports-ktor/README.md +++ b/controls-ports-ktor/README.md @@ -6,7 +6,7 @@ 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.3.0`. +The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-ports-ktor:0.3.0") + implementation("space.kscience:controls-ports-ktor:0.4.0-dev-1") } ``` diff --git a/controls-serial/README.md b/controls-serial/README.md index 861b6d3..a961f55 100644 --- a/controls-serial/README.md +++ b/controls-serial/README.md @@ -6,7 +6,7 @@ Implementation of direct serial port communication with JSerialComm ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-serial:0.3.0`. +The Maven coordinates of this project are `space.kscience:controls-serial:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-serial:0.3.0") + implementation("space.kscience:controls-serial:0.4.0-dev-1") } ``` diff --git a/controls-server/README.md b/controls-server/README.md index 86a5bf5..114f618 100644 --- a/controls-server/README.md +++ b/controls-server/README.md @@ -6,7 +6,7 @@ A combined Magix event loop server with web server for visualization. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-server:0.3.0`. +The Maven coordinates of this project are `space.kscience:controls-server:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-server:0.3.0") + implementation("space.kscience:controls-server:0.4.0-dev-1") } ``` diff --git a/controls-storage/README.md b/controls-storage/README.md index 3242de2..64751e8 100644 --- a/controls-storage/README.md +++ b/controls-storage/README.md @@ -6,7 +6,7 @@ An API for stand-alone Controls-kt device or a hub. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-storage:0.3.0`. +The Maven coordinates of this project are `space.kscience:controls-storage:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-storage:0.3.0") + implementation("space.kscience:controls-storage:0.4.0-dev-1") } ``` diff --git a/controls-storage/controls-xodus/README.md b/controls-storage/controls-xodus/README.md index 2a6c247..e423c3f 100644 --- a/controls-storage/controls-xodus/README.md +++ b/controls-storage/controls-xodus/README.md @@ -6,7 +6,7 @@ An implementation of controls-storage on top of JetBrains Xodus. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-xodus:0.3.0`. +The Maven coordinates of this project are `space.kscience:controls-xodus:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-xodus:0.3.0") + implementation("space.kscience:controls-xodus:0.4.0-dev-1") } ``` diff --git a/controls-vision/README.md b/controls-vision/README.md index 41483ce..cbfc917 100644 --- a/controls-vision/README.md +++ b/controls-vision/README.md @@ -2,11 +2,13 @@ Dashboard and visualization extensions for devices +Hello world! + ## Usage ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-vision:0.3.0`. +The Maven coordinates of this project are `space.kscience:controls-vision:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +18,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-vision:0.3.0") + implementation("space.kscience:controls-vision:0.4.0-dev-1") } ``` diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts index c36cdce..5c75242 100644 --- a/controls-vision/build.gradle.kts +++ b/controls-vision/build.gradle.kts @@ -10,6 +10,7 @@ description = """ kscience { fullStack("js/controls-vision.js") useKtor() + useSerialization() useContextReceivers() dependencies { api(projects.controlsCore) diff --git a/controls-vision/docs/README-TEMPLATE.md b/controls-vision/docs/README-TEMPLATE.md new file mode 100644 index 0000000..63e6d31 --- /dev/null +++ b/controls-vision/docs/README-TEMPLATE.md @@ -0,0 +1,15 @@ +# Module ${name} + +${description} + +<#if features?has_content> +## Features + +${features} + +</#if> +<#if published> +## Usage + +${artifact} +</#if> diff --git a/controls-vision/src/commonMain/kotlin/BooleanIndicatorVision.kt b/controls-vision/src/commonMain/kotlin/BooleanIndicatorVision.kt new file mode 100644 index 0000000..8783228 --- /dev/null +++ b/controls-vision/src/commonMain/kotlin/BooleanIndicatorVision.kt @@ -0,0 +1,24 @@ +package space.kscience.controls.vision + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import space.kscience.dataforge.meta.boolean +import space.kscience.visionforge.AbstractVision +import space.kscience.visionforge.Vision +import space.kscience.visionforge.html.VisionOfHtml + +/** + * A [Vision] that shows an indicator + */ +@Serializable +@SerialName("controls.indicator") +public class BooleanIndicatorVision : AbstractVision(), VisionOfHtml { + public val isOn: Boolean by properties.boolean(false) +} + +///** +// * 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/commonMain/kotlin/ControlVisionPlugin.kt b/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt index d587250..6395d53 100644 --- a/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt +++ b/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt @@ -6,7 +6,6 @@ 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<ControlVisionPlugin> @@ -14,6 +13,6 @@ public expect class ControlVisionPlugin: VisionPlugin{ internal val controlsVisionSerializersModule = SerializersModule { polymorphic(Vision::class) { - subclass(VisionOfPlotly.serializer()) + subclass(BooleanIndicatorVision.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 deleted file mode 100644 index 9ec2319..0000000 --- a/controls-vision/src/commonMain/kotlin/IndicatorVision.kt +++ /dev/null @@ -1,20 +0,0 @@ -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/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index 6dfdee0..88b870e 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -1,4 +1,4 @@ -@file:OptIn(FlowPreview::class) +@file:OptIn(FlowPreview::class, FlowPreview::class) package space.kscience.controls.vision diff --git a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt index 55074ae..d08772c 100644 --- a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt +++ b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt @@ -5,13 +5,29 @@ 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 import space.kscience.visionforge.VisionPlugin +import space.kscience.visionforge.html.ElementVisionRenderer + +private val indicatorRenderer = ElementVisionRenderer<BooleanIndicatorVision> { name, vision: BooleanIndicatorVision, meta -> + +} + public actual class ControlVisionPlugin : VisionPlugin() { override val tag: PluginTag get() = Companion.tag override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule + override fun content(target: String): Map<Name, Any> = when (target) { + ElementVisionRenderer.TYPE -> mapOf( + "indicator".asName() to indicatorRenderer + ) + + else -> super.content(target) + } + public actual companion object : PluginFactory<ControlVisionPlugin> { override val tag: PluginTag = PluginTag("controls.vision") diff --git a/controls-vision/src/jvmMain/kotlin/dashboard.kt b/controls-vision/src/jvmMain/kotlin/dashboard.kt index 29c4740..1b64b15 100644 --- a/controls-vision/src/jvmMain/kotlin/dashboard.kt +++ b/controls-vision/src/jvmMain/kotlin/dashboard.kt @@ -13,6 +13,8 @@ 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.markup.MarkupPlugin +import space.kscience.visionforge.plotly.PlotlyPlugin import space.kscience.visionforge.plotly.plotly import space.kscience.visionforge.server.VisionRoute import space.kscience.visionforge.server.close @@ -25,29 +27,38 @@ public fun Context.showDashboard( routes: Routing.() -> Unit = {}, configurationBuilder: VisionRoute.() -> Unit = {}, visionFragment: HtmlVisionFragment, -): ApplicationEngine = embeddedServer(CIO, port = port) { - routing { - staticResources("", null, null) - routes() +): ApplicationEngine { + //create a sub-context for visualization + val visualisationContext = buildContext { + plugin(PlotlyPlugin) + plugin(ControlVisionPlugin) + plugin(MarkupPlugin) } - visionPage( - visionManager, - VisionPage.scriptHeader("js/controls-vision.js"), - configurationBuilder = configurationBuilder, - visionFragment = visionFragment - ) -}.also { - it.start(false) - it.openInBrowser() + return visualisationContext.embeddedServer(CIO, port = port) { + routing { + staticResources("", null, null) + routes() + } + + visionPage( + visualisationContext.visionManager, + VisionPage.scriptHeader("js/controls-vision.js"), + configurationBuilder = configurationBuilder, + visionFragment = visionFragment + ) + }.also { + it.start(false) + it.openInBrowser() - println("Enter 'exit' to close server") - while (readlnOrNull() != "exit") { - // + println("Enter 'exit' to close server") + while (readlnOrNull() != "exit") { + // + } + + it.close() } - - it.close() } context(VisionTagConsumer<*>) diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/LinearDrive.kt similarity index 74% rename from demo/constructor/src/jvmMain/kotlin/main.kt rename to demo/constructor/src/jvmMain/kotlin/LinearDrive.kt index e426e1b..95df7ad 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/LinearDrive.kt @@ -14,8 +14,10 @@ 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.constructor.DeviceConstructor +import space.kscience.controls.constructor.DoubleInRangeState +import space.kscience.controls.constructor.device +import space.kscience.controls.constructor.deviceProperty import space.kscience.controls.constructor.library.* import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.DeviceManager @@ -40,77 +42,41 @@ import kotlin.time.DurationUnit class LinearDrive( + drive: Drive, + start: LimitSwitch, + end: LimitSwitch, + pidParameters: PidParameters, + meta: Meta = Meta.EMPTY, +) : DeviceConstructor(drive.context, meta) { + + val drive: Drive by device(drive) + val pid by device(PidRegulator(drive, pidParameters)) + + val start by device(start) + val end by device(end) + + val position by deviceProperty(drive, Drive.position, Double.NaN) + + val target by deviceProperty(pid, Regulator.target, 0.0) +} + +/** + * A shortcut to create a virtual [LimitSwitch] from [DoubleInRangeState] + */ +fun LinearDrive( context: Context, - state: DoubleRangeState, + positionState: DoubleInRangeState, mass: Double, pidParameters: PidParameters, meta: Meta = Meta.EMPTY, -) : DeviceConstructor(context, meta) { +): LinearDrive = LinearDrive( + drive = VirtualDrive(context, mass, positionState), + start = VirtualLimitSwitch(context, positionState.atStart), + end = VirtualLimitSwitch(context, positionState.atEnd), + pidParameters = pidParameters, + meta = meta +) - val drive by device(VirtualDrive.factory(mass, state)) - val pid by device(PidRegulator(drive, pidParameters)) - - val start by device(LimitSwitch(state.atStartState)) - val end by device(LimitSwitch(state.atEndState)) - - - val positionState: DoubleRangeState by property(state) - - private val targetState: MutableDeviceState<Double> by deviceProperty(pid, Regulator.target, 0.0) - var target: Double by targetState -} - - -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) { - val timeFromStart = clock.now() - clockStart - val t = timeFromStart.toDouble(DurationUnit.SECONDS) - val freq = 0.1 - target = 5 * sin(2.0 * PI * freq * t) + - sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep)) - } - } - - - val maxAge = 10.seconds - - showDashboard { - plot { - plotNumberState(context, state, maxAge = maxAge, sampling = 50.milliseconds) { - name = "real position" - } - plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge, sampling = 50.milliseconds) { - name = "read position" - } - - plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge, sampling = 50.milliseconds) { - name = "target" - } - } - - plot { - 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, sampling = 50.milliseconds) { - name = "end measured" - mode = ScatterMode.markers - } - } - - } -} fun main() = application { val context = Context { @@ -140,12 +106,57 @@ fun main() = application { ) } - context.launchPidDevice( - DoubleRangeState(0.0, -6.0..6.0), - pidParameters, - mass = 0.05 + val state = DoubleInRangeState(0.0, -6.0..6.0) + + val linearDrive = context.install( + "linearDrive", + LinearDrive(context, state, 0.05, pidParameters) ) + val clockStart = context.clock.now() + linearDrive.doRecurring(10.milliseconds) { + val timeFromStart = clock.now() - clockStart + val t = timeFromStart.toDouble(DurationUnit.SECONDS) + val freq = 0.1 + target.value = 5 * sin(2.0 * PI * freq * t) + + sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep)) + } + + + val maxAge = 10.seconds + + context.showDashboard { + plot { + plotNumberState(context, state, maxAge = maxAge, sampling = 50.milliseconds) { + name = "real position" + } + plotDeviceProperty(linearDrive.pid, Regulator.position.name, maxAge = maxAge, sampling = 50.milliseconds) { + name = "read position" + } + + plotDeviceProperty(linearDrive.pid, Regulator.target.name, maxAge = maxAge, sampling = 50.milliseconds) { + name = "target" + } + } + + plot { + plotDeviceProperty( + linearDrive.start, + LimitSwitch.locked.name, + maxAge = maxAge, + sampling = 50.milliseconds + ) { + name = "start measured" + mode = ScatterMode.markers + } + plotDeviceProperty(linearDrive.end, LimitSwitch.locked.name, maxAge = maxAge, sampling = 50.milliseconds) { + name = "end measured" + mode = ScatterMode.markers + } + } + + } + Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) { MaterialTheme { Column { diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt index ab1c47a..b64cd2d 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt @@ -97,7 +97,7 @@ fun AxisPane(axes: Map<String, PiMotionMasterDevice.Axis>) { @Composable fun PiMotionMasterApp(device: PiMotionMasterDevice) { - val scope = rememberCoroutineScope() +// val scope = rememberCoroutineScope() val connected by device.composeState(PiMotionMasterDevice.connected, false) var debugServerJob by remember { mutableStateOf<Job?>(null) } var axes by remember { mutableStateOf<Map<String, PiMotionMasterDevice.Axis>?>(null) } diff --git a/docs/templates/ARTIFACT-TEMPLATE.md b/docs/templates/ARTIFACT-TEMPLATE.md deleted file mode 100644 index a3e47e6..0000000 --- a/docs/templates/ARTIFACT-TEMPLATE.md +++ /dev/null @@ -1,30 +0,0 @@ -## Artifact: - -The Maven coordinates of this project are `${group}:${name}:${version}`. - -**Gradle:** -```groovy -repositories { - maven { url 'https://repo.kotlin.link' } - mavenCentral() - // development and snapshot versions - maven { url 'https://maven.pkg.jetbrains.space/spc/p/sci/dev' } -} - -dependencies { - implementation '${group}:${name}:${version}' -} -``` -**Gradle Kotlin DSL:** -```kotlin -repositories { - maven("https://repo.kotlin.link") - mavenCentral() - // development and snapshot versions - maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") -} - -dependencies { - implementation("${group}:${name}:${version}") -} -``` \ No newline at end of file diff --git a/magix/magix-api/README.md b/magix/magix-api/README.md index d624d84..6228a7e 100644 --- a/magix/magix-api/README.md +++ b/magix/magix-api/README.md @@ -6,7 +6,7 @@ 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.3.0`. +The Maven coordinates of this project are `space.kscience:magix-api:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-api:0.3.0") + implementation("space.kscience:magix-api:0.4.0-dev-1") } ``` diff --git a/magix/magix-java-endpoint/README.md b/magix/magix-java-endpoint/README.md index 40f4c8e..2db1d42 100644 --- a/magix/magix-java-endpoint/README.md +++ b/magix/magix-java-endpoint/README.md @@ -6,7 +6,7 @@ Java API to work with magix endpoints without Kotlin ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.3.0`. +The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-java-endpoint:0.3.0") + implementation("space.kscience:magix-java-endpoint:0.4.0-dev-1") } ``` diff --git a/magix/magix-mqtt/README.md b/magix/magix-mqtt/README.md index 35165a0..7896472 100644 --- a/magix/magix-mqtt/README.md +++ b/magix/magix-mqtt/README.md @@ -6,7 +6,7 @@ MQTT client magix endpoint ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-mqtt:0.3.0`. +The Maven coordinates of this project are `space.kscience:magix-mqtt:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-mqtt:0.3.0") + implementation("space.kscience:magix-mqtt:0.4.0-dev-1") } ``` diff --git a/magix/magix-rabbit/README.md b/magix/magix-rabbit/README.md index 609303d..eee4a21 100644 --- a/magix/magix-rabbit/README.md +++ b/magix/magix-rabbit/README.md @@ -6,7 +6,7 @@ RabbitMQ client magix endpoint ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-rabbit:0.3.0`. +The Maven coordinates of this project are `space.kscience:magix-rabbit:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-rabbit:0.3.0") + implementation("space.kscience:magix-rabbit:0.4.0-dev-1") } ``` diff --git a/magix/magix-rsocket/README.md b/magix/magix-rsocket/README.md index 0a1b34b..ef16b66 100644 --- a/magix/magix-rsocket/README.md +++ b/magix/magix-rsocket/README.md @@ -6,7 +6,7 @@ Magix endpoint (client) based on RSocket ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-rsocket:0.3.0`. +The Maven coordinates of this project are `space.kscience:magix-rsocket:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-rsocket:0.3.0") + implementation("space.kscience:magix-rsocket:0.4.0-dev-1") } ``` diff --git a/magix/magix-server/README.md b/magix/magix-server/README.md index 4175dcd..17bdbdf 100644 --- a/magix/magix-server/README.md +++ b/magix/magix-server/README.md @@ -6,7 +6,7 @@ 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.3.0`. +The Maven coordinates of this project are `space.kscience:magix-server:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-server:0.3.0") + implementation("space.kscience:magix-server:0.4.0-dev-1") } ``` diff --git a/magix/magix-storage/README.md b/magix/magix-storage/README.md index db1d340..dbb2729 100644 --- a/magix/magix-storage/README.md +++ b/magix/magix-storage/README.md @@ -6,7 +6,7 @@ Magix history database API ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-storage:0.3.0`. +The Maven coordinates of this project are `space.kscience:magix-storage:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-storage:0.3.0") + implementation("space.kscience:magix-storage:0.4.0-dev-1") } ``` diff --git a/magix/magix-storage/magix-storage-xodus/README.md b/magix/magix-storage/magix-storage-xodus/README.md index 2d9e202..90a0050 100644 --- a/magix/magix-storage/magix-storage-xodus/README.md +++ b/magix/magix-storage/magix-storage-xodus/README.md @@ -6,7 +6,7 @@ ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.3.0`. +The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-storage-xodus:0.3.0") + implementation("space.kscience:magix-storage-xodus:0.4.0-dev-1") } ``` diff --git a/magix/magix-zmq/README.md b/magix/magix-zmq/README.md index b016e6b..6e924db 100644 --- a/magix/magix-zmq/README.md +++ b/magix/magix-zmq/README.md @@ -6,7 +6,7 @@ ZMQ client endpoint for Magix ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-zmq:0.3.0`. +The Maven coordinates of this project are `space.kscience:magix-zmq:0.4.0-dev-1`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-zmq:0.3.0") + implementation("space.kscience:magix-zmq:0.4.0-dev-1") } ``` From ee83f81a049038ce9188211a4f49d68894b20f4e Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Thu, 16 May 2024 21:22:34 +0300 Subject: [PATCH 083/125] Fix small typo in event id generation --- .../kscience/controls/client/controlsMagix.kt | 4 +- .../controls/demo/DemoControllerView.kt | 40 +++++++++++++++---- 2 files changed, 34 insertions(+), 10 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 915d0a0..8db9351 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 @@ -29,7 +29,7 @@ public val DeviceManager.Companion.magixFormat: MagixFormat<DeviceMessage> get() internal fun generateId(request: MagixMessage): String = if (request.id != null) { "${request.id}.response" } else { - "controls[${request.payload.hashCode().toString(16)}" + "controls[${request.payload.hashCode().toUInt().toString(16)}]" } /** @@ -43,7 +43,7 @@ public fun DeviceManager.launchMagixService( coroutineContext: CoroutineContext = EmptyCoroutineContext, ): Job = context.launch(coroutineContext) { endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID, null)).onEach { (request, payload) -> - val responsePayload = respondHubMessage(payload) + val responsePayload: List<DeviceMessage> = respondHubMessage(payload) responsePayload.forEach { endpoint.send( format = controlsMagixFormat, 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 ff99f32..f107f56 100644 --- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt @@ -17,7 +17,6 @@ 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 @@ -30,6 +29,7 @@ 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.MagixMessage import space.kscience.magix.api.send import space.kscience.magix.api.subscribe import space.kscience.magix.rsocket.rSocketWithTcp @@ -40,6 +40,9 @@ import space.kscince.magix.zmq.ZmqMagixFlowPlugin import java.awt.Desktop import java.net.URI + +private val json = Json { prettyPrint = true } + class DemoController : ContextAware { var device: DemoDevice? = null @@ -70,7 +73,7 @@ class DemoController : ContextAware { ) //Launch a device client and connect it to the server val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost") - deviceManager.launchMagixService(deviceEndpoint) + deviceManager.launchMagixService(deviceEndpoint, "demoDevice") //connect visualization to a magix endpoint val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") visualizer = startDemoDeviceServer(visualEndpoint) @@ -81,13 +84,19 @@ class DemoController : ContextAware { val listenerEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") - listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (_, deviceMessage) -> + + listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> // print all messages that are not property change message if (deviceMessage !is PropertyChangedMessage) { - println(">> ${Json.encodeToString(DeviceMessage.serializer(), deviceMessage)}") + println(">> ${json.encodeToString(MagixMessage.serializer(), magixMessage)}") } }.launchIn(this) - listenerEndpoint.send(DeviceManager.magixFormat, GetDescriptionMessage(), "listener", "controls-kt") + listenerEndpoint.send( + format = DeviceManager.magixFormat, + payload = GetDescriptionMessage(), + source = "listener", +// target = "demoDevice" + ) } @@ -114,17 +123,32 @@ fun DemoControls(controller: DemoController) { Column { Row(Modifier.fillMaxWidth()) { Text("Time Scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) - TextField(String.format("%.2f", timeScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) + TextField( + String.format("%.2f", timeScale), + {}, + enabled = false, + modifier = Modifier.align(Alignment.CenterVertically).width(100.dp) + ) Slider(timeScale, onValueChange = { timeScale = it }, steps = 20, valueRange = 1000f..5000f) } Row(Modifier.fillMaxWidth()) { Text("X scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) - TextField(String.format("%.2f", xScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) + TextField( + String.format("%.2f", xScale), + {}, + enabled = false, + modifier = Modifier.align(Alignment.CenterVertically).width(100.dp) + ) Slider(xScale, onValueChange = { xScale = it }, steps = 20, valueRange = 0.1f..2.0f) } Row(Modifier.fillMaxWidth()) { Text("Y scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) - TextField(String.format("%.2f", yScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp)) + TextField( + String.format("%.2f", yScale), + {}, + enabled = false, + modifier = Modifier.align(Alignment.CenterVertically).width(100.dp) + ) Slider(yScale, onValueChange = { yScale = it }, steps = 20, valueRange = 0.1f..2.0f) } Row(Modifier.fillMaxWidth()) { From e5088ac8e466ddb6a48023c84ba79c8fbd033ef9 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Fri, 17 May 2024 23:01:20 +0300 Subject: [PATCH 084/125] Make remote device connection ask for descriptors before start --- .../space/kscience/controls/api/Device.kt | 2 +- controls-magix/build.gradle.kts | 6 ++- .../kscience/controls/client/DeviceClient.kt | 50 +++++++++++++------ .../kscience/controls/client/controlsMagix.kt | 3 +- .../controls/client/RemoteDeviceConnect.kt | 21 +++++--- .../space/kscience/magix/api/MagixFormat.kt | 2 +- 6 files changed, 59 insertions(+), 25 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 fc68bf6..240eaa0 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 @@ -100,8 +100,8 @@ public interface Device : ContextAware, CoroutineScope { * Close and terminate the device. This function does not wait for the device to be closed. */ public suspend fun stop() { + coroutineContext[Job]?.cancel("The device is closed") logger.info { "Device $this is closed" } - cancel("The device is closed") } public val lifecycleState: DeviceLifecycleState diff --git a/controls-magix/build.gradle.kts b/controls-magix/build.gradle.kts index 040c040..ef4a3af 100644 --- a/controls-magix/build.gradle.kts +++ b/controls-magix/build.gradle.kts @@ -17,11 +17,15 @@ kscience { useSerialization { json() } - dependencies { + commonMain { api(projects.magix.magixApi) api(projects.controlsCore) api(libs.uuid) } + + jvmTest{ + implementation(spclibs.logback.classic) + } } readme { 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 51836b9..4285c44 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,11 @@ package space.kscience.controls.client import com.benasher44.uuid.uuid4 +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import space.kscience.controls.api.* @@ -23,9 +26,11 @@ private fun stringUID() = uuid4().leastSignificantBits.toString(16) /** * A remote accessible device that relies on connection via Magix */ -public class DeviceClient( +public class DeviceClient internal constructor( override val context: Context, private val deviceName: Name, + override val propertyDescriptors: Collection<PropertyDescriptor>, + override val actionDescriptors: Collection<ActionDescriptor>, incomingFlow: Flow<DeviceMessage>, private val send: suspend (DeviceMessage) -> Unit, ) : CachingDevice { @@ -37,12 +42,6 @@ public class DeviceClient( private val propertyCache = HashMap<String, Meta>() - override var propertyDescriptors: Collection<PropertyDescriptor> = emptyList() - private set - - override var actionDescriptors: Collection<ActionDescriptor> = emptyList() - private set - private val flowInternal = incomingFlow.filter { it.sourceDevice == deviceName }.shareIn(this, started = SharingStarted.Eagerly).also { @@ -52,11 +51,6 @@ public class DeviceClient( propertyCache[message.property] = message.value } - is DescriptionMessage -> mutex.withLock { - propertyDescriptors = message.properties - actionDescriptors = message.actions - } - else -> { //ignore } @@ -112,14 +106,38 @@ public class DeviceClient( * @param targetEndpointName the name of endpoint in Magix to connect to * @param deviceName the name of device within endpoint */ -public fun MagixEndpoint.remoteDevice( +public suspend fun MagixEndpoint.remoteDevice( context: Context, sourceEndpointName: String, targetEndpointName: String, deviceName: Name, -): DeviceClient { +): DeviceClient = coroutineScope{ val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(targetEndpointName)).map { it.second } - return DeviceClient(context, deviceName, subscription) { + + val deferredDescriptorMessage = CompletableDeferred<DescriptionMessage>() + + launch { + deferredDescriptorMessage.complete(subscription.filterIsInstance<DescriptionMessage>().first()) + } + + send( + format = DeviceManager.magixFormat, + payload = GetDescriptionMessage(targetDevice = deviceName), + source = sourceEndpointName, + target = targetEndpointName, + id = stringUID() + ) + + + val descriptionMessage = deferredDescriptorMessage.await() + + DeviceClient( + context = context, + deviceName = deviceName, + propertyDescriptors = descriptionMessage.properties, + actionDescriptors = descriptionMessage.actions, + incomingFlow = subscription + ) { send( format = DeviceManager.magixFormat, payload = it, @@ -130,6 +148,8 @@ public fun MagixEndpoint.remoteDevice( } } +//public fun MagixEndpoint.remoteDeviceHub() + /** * Subscribe on specific property of a device without creating a device */ 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 8db9351..1c6f556 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 @@ -1,5 +1,6 @@ package space.kscience.controls.client +import com.benasher44.uuid.uuid4 import kotlinx.coroutines.Job import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn @@ -29,7 +30,7 @@ public val DeviceManager.Companion.magixFormat: MagixFormat<DeviceMessage> get() internal fun generateId(request: MagixMessage): String = if (request.id != null) { "${request.id}.response" } else { - "controls[${request.payload.hashCode().toUInt().toString(16)}]" + uuid4().leastSignificantBits.toULong().toString(16) } /** 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 1478899..ff82486 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 @@ -1,9 +1,12 @@ package space.kscience.controls.client import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json +import space.kscience.controls.api.DeviceMessage import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install import space.kscience.controls.manager.respondMessage @@ -46,7 +49,7 @@ internal class RemoteDeviceConnect { } @Test - fun wrapper() = runTest { + fun deviceClient() = runTest { val context = Context { plugin(DeviceManager) } @@ -56,11 +59,15 @@ internal class RemoteDeviceConnect { val virtualMagixEndpoint = object : MagixEndpoint { - override fun subscribe(filter: MagixMessageFilter): Flow<MagixMessage> = device.messageFlow.map { + private val additionalMessages = MutableSharedFlow<DeviceMessage>(10) + + override fun subscribe( + filter: MagixMessageFilter, + ): Flow<MagixMessage> = merge(device.messageFlow, additionalMessages).map { MagixMessage( format = DeviceManager.magixFormat.defaultFormat, payload = MagixEndpoint.magixJson.encodeToJsonElement(DeviceManager.magixFormat.serializer, it), - sourceEndpoint = "source", + sourceEndpoint = "device", ) } @@ -68,16 +75,18 @@ internal class RemoteDeviceConnect { device.respondMessage( Name.EMPTY, Json.decodeFromJsonElement(DeviceManager.magixFormat.serializer, message.payload) - ) + )?.let { + additionalMessages.emit(it) + } } override fun close() { // } } - - val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "source", "target", Name.EMPTY) + val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "client", "device", Name.EMPTY) assertContains(0.0..1.0, remoteDevice.read(TestDevice.value)) + } } \ No newline at end of file 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 42d55b7..b0316e3 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 @@ -56,6 +56,6 @@ public suspend fun <T> MagixEndpoint.send( parentId = parentId, user = user ) - broadcast(message) + send(message) } From 207064cd45fed2eab863e683a5f213f1593a8730 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 19 May 2024 10:44:34 +0300 Subject: [PATCH 085/125] Remove hierarchical device structure in Hubs --- CHANGELOG.md | 6 +- build.gradle.kts | 2 +- .../controls/constructor/DeviceGroup.kt | 56 +++++------- .../constructor/library/PidRegulator.kt | 4 +- .../space/kscience/controls/api/DeviceHub.kt | 73 ++++++--------- .../controls/manager/DeviceManager.kt | 16 ++-- .../controls/manager/respondMessage.kt | 4 +- .../kscience/controls/client/DeviceClient.kt | 88 +++++++++++++------ .../kscience/controls/client/tangoMagix.kt | 7 +- .../controls/client/RemoteDeviceConnect.kt | 2 +- .../controls/modbus/ModbusDeviceBySpec.kt | 6 +- .../controls/server/deviceWebServer.kt | 10 +-- .../pimotionmaster/PiMotionMasterDevice.kt | 5 +- 13 files changed, 140 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a258d7..2c7ea2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,14 @@ ### Added - Value averaging plot extension - PLC4X bindings +- Shortcuts to access all Controls devices in a magix network. +- `DeviceClient` properly evaluates lifecycle and logs ### Changed -- Constructor properties return `DeviceStat` in order to be able to subscribe to them +- Constructor properties return `DeviceState` in order to be able to subscribe to them - Refactored ports. Now we have `AsynchronousPort` as well as `SynchronousPort` +- `DeviceClient` now initializes property and action descriptors eagerly. +- `DeviceHub` now works with `Name` instead of `NameToken`. Tree-like structure is made using `Path`. Device messages no longer have access to sub-devices. ### Deprecated diff --git a/build.gradle.kts b/build.gradle.kts index 38d5c6a..0f00407 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { allprojects { group = "space.kscience" - version = "0.4.0-dev-1" + version = "0.4.0-dev-2" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt index 6785b4b..ef886dc 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -10,9 +10,14 @@ import space.kscience.controls.api.DeviceLifecycleState.* import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install import space.kscience.dataforge.context.* -import space.kscience.dataforge.meta.* +import space.kscience.dataforge.meta.Laminate +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.meta.MutableMeta import space.kscience.dataforge.misc.DFExperimental -import space.kscience.dataforge.names.* +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.get +import space.kscience.dataforge.names.parseAsName import kotlin.collections.set import kotlin.coroutines.CoroutineContext @@ -60,15 +65,15 @@ public open class DeviceGroup( ) - private val _devices = hashMapOf<NameToken, Device>() + private val _devices = hashMapOf<Name, Device>() - override val devices: Map<NameToken, Device> = _devices + override val devices: Map<Name, Device> = _devices /** * Register and initialize (synchronize child's lifecycle state with group state) a new device in this group */ @OptIn(DFExperimental::class) - public open fun <D : Device> install(token: NameToken, device: D): D { + public open fun <D : Device> install(token: Name, device: D): D { require(_devices[token] == null) { "A child device with name $token already exists" } //start the child device if needed if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() } @@ -175,35 +180,16 @@ public fun Context.registerDeviceGroup( block: DeviceGroup.() -> Unit, ): DeviceGroup = request(DeviceManager).registerDeviceGroup(name, meta, block) -private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { - return when (name.length) { - 0 -> this - 1 -> { - val token = name.first() - when (val d = devices[token]) { - null -> install( - token, - DeviceGroup(context, meta[token] ?: Meta.EMPTY) - ) - - else -> (d as? DeviceGroup) ?: error("Device $name is not a DeviceGroup") - } - } - - else -> getOrCreateGroup(name.first().asName()).getOrCreateGroup(name.cutFirst()) - } -} - -/** - * Register a device at given [name] path - */ -public fun <D : Device> DeviceGroup.install(name: Name, device: D): D { - return when (name.length) { - 0 -> error("Can't use empty name for a child device") - 1 -> install(name.first(), device) - else -> getOrCreateGroup(name.cutLast()).install(name.tokens.last(), device) - } -} +///** +// * Register a device at given [path] path +// */ +//public fun <D : Device> DeviceGroup.install(path: Path, device: D): D { +// return when (path.length) { +// 0 -> error("Can't use empty path for a child device") +// 1 -> install(path.first().name, device) +// else -> getOrCreateGroup(path.cutLast()).install(path.tokens.last(), device) +// } +//} public fun <D : Device> DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device) @@ -238,7 +224,7 @@ public fun <D : Device> DeviceGroup.install( * Create or edit a group with a given [name]. */ public fun DeviceGroup.registerDeviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup = - getOrCreateGroup(name).apply(block) + install(name, DeviceGroup(context, meta).apply(block)) public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup = registerDeviceGroup(name.parseAsName(), block) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt index 7ce2054..afb2cbd 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt @@ -8,10 +8,10 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Instant import space.kscience.controls.constructor.DeviceGroup -import space.kscience.controls.constructor.install import space.kscience.controls.manager.clock import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.write +import space.kscience.dataforge.names.parseAsName import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit @@ -94,4 +94,4 @@ public fun DeviceGroup.pid( name: String, drive: Drive, pidParameters: PidParameters, -): PidRegulator = install(name, PidRegulator(drive, pidParameters)) \ No newline at end of file +): PidRegulator = install(name.parseAsName(), PidRegulator(drive, pidParameters)) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt index f6ca4ec..077585b 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt @@ -1,40 +1,24 @@ package space.kscience.controls.api import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.names.* +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.provider.Path import space.kscience.dataforge.provider.Provider +import space.kscience.dataforge.provider.asPath +import space.kscience.dataforge.provider.plus /** * A hub that could locate multiple devices and redirect actions to them */ public interface DeviceHub : Provider { - public val devices: Map<NameToken, Device> + public val devices: Map<Name, Device> override val defaultTarget: String get() = Device.DEVICE_TARGET override val defaultChainTarget: String get() = Device.DEVICE_TARGET - /** - * List all devices, including sub-devices - */ - public fun buildDeviceTree(): Map<Name, Device> = 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<Name, Any> = if (target == Device.DEVICE_TARGET) { - buildDeviceTree() + devices } else { emptyMap() } @@ -42,38 +26,31 @@ public interface DeviceHub : Provider { public companion object } +/** + * List all devices, including sub-devices + */ +public fun DeviceHub.provideAllDevices(): Map<Path, Device> = buildMap { + fun putAll(prefix: Path, hub: DeviceHub) { + hub.devices.forEach { + put(prefix + it.key.asPath(), it.value) + } + } -public operator fun DeviceHub.get(nameToken: NameToken): Device = - devices[nameToken] ?: error("Device with name $nameToken not found in $this") - -public fun DeviceHub.getOrNull(name: Name): Device? = when { - name.isEmpty() -> this as? Device - name.length == 1 -> devices[name.firstOrNull()!!] - else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst()) + devices.forEach { + val name: Name = it.key + put(name.asPath(), it.value) + (it.value as? DeviceHub)?.let { hub -> + putAll(name.asPath(), hub) + } + } } -public operator fun DeviceHub.get(name: Name): Device = - getOrNull(name) ?: error("Device with name $name not found in $this") - -public fun DeviceHub.getOrNull(nameString: String): Device? = getOrNull(Name.parse(nameString)) - -public operator fun DeviceHub.get(nameString: String): Device = - getOrNull(nameString) ?: error("Device with name $nameString not found in $this") - public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta = - this[deviceName].readProperty(propertyName) + (devices[deviceName] ?: error("Device with name $deviceName not found in $this")).readProperty(propertyName) public suspend fun DeviceHub.writeProperty(deviceName: Name, propertyName: String, value: Meta) { - this[deviceName].writeProperty(propertyName, value) + (devices[deviceName] ?: error("Device with name $deviceName not found in $this")).writeProperty(propertyName, value) } public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: Meta?): Meta? = - this[deviceName].execute(command, argument) - - -//suspend fun DeviceHub.respond(request: Envelope): EnvelopeBuilder { -// val target = request.meta[DeviceMessage.TARGET_KEY].string ?: defaultTarget -// val device = this[target.toName()] -// -// return device.respond(device, target, request) -//} \ No newline at end of file + (devices[deviceName] ?: error("Device with name $deviceName not found in $this")).execute(command, argument) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt index 93b0696..689cb90 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt @@ -3,13 +3,13 @@ package space.kscience.controls.manager import kotlinx.coroutines.launch import space.kscience.controls.api.Device import space.kscience.controls.api.DeviceHub -import space.kscience.controls.api.getOrNull import space.kscience.controls.api.id import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MutableMeta import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.NameToken +import space.kscience.dataforge.names.get +import space.kscience.dataforge.names.parseAsName import kotlin.collections.set import kotlin.properties.ReadOnlyProperty @@ -22,11 +22,11 @@ public class DeviceManager : AbstractPlugin(), DeviceHub { /** * Actual list of connected devices */ - private val top = HashMap<NameToken, Device>() - override val devices: Map<NameToken, Device> get() = top + private val _devices = HashMap<Name, Device>() + override val devices: Map<Name, Device> get() = _devices - public fun registerDevice(name: NameToken, device: Device) { - top[name] = device + public fun registerDevice(name: Name, device: Device) { + _devices[name] = device } override fun content(target: String): Map<Name, Any> = super<DeviceHub>.content(target) @@ -39,7 +39,7 @@ public class DeviceManager : AbstractPlugin(), DeviceHub { } public fun <D : Device> DeviceManager.install(name: String, device: D): D { - registerDevice(NameToken(name), device) + registerDevice(name.parseAsName(), device) device.launch { device.start() } @@ -69,7 +69,7 @@ public inline fun <D : Device> DeviceManager.installing( val meta = Meta(builder) return ReadOnlyProperty { _, property -> val name = property.name - val current = getOrNull(name) + val current = devices[name] if (current == null) { install(name, factory, meta) } else if (current.meta != meta) { diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt index fc6fb5d..a15bcef 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt @@ -74,11 +74,11 @@ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): List<Dev return try { val targetName = request.targetDevice if (targetName == null) { - buildDeviceTree().mapNotNull { + devices.mapNotNull { it.value.respondMessage(it.key, request) } } else { - val device = getOrNull(targetName) ?: error("The device with name $targetName not found in $this") + val device = devices[targetName] ?: error("The device with name $targetName not found in $this") listOfNotNull(device.respondMessage(targetName, request)) } } catch (ex: Exception) { 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 4285c44..9418791 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 @@ -16,6 +16,8 @@ import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.length +import space.kscience.dataforge.names.startsWith import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.send import space.kscience.magix.api.subscribe @@ -29,13 +31,19 @@ private fun stringUID() = uuid4().leastSignificantBits.toString(16) public class DeviceClient internal constructor( override val context: Context, private val deviceName: Name, - override val propertyDescriptors: Collection<PropertyDescriptor>, - override val actionDescriptors: Collection<ActionDescriptor>, + propertyDescriptors: Collection<PropertyDescriptor>, + actionDescriptors: Collection<ActionDescriptor>, incomingFlow: Flow<DeviceMessage>, private val send: suspend (DeviceMessage) -> Unit, ) : CachingDevice { + override var actionDescriptors: Collection<ActionDescriptor> = actionDescriptors + internal set + + override var propertyDescriptors: Collection<PropertyDescriptor> = propertyDescriptors + internal set + override val coroutineContext: CoroutineContext = context.coroutineContext + Job(context.coroutineContext[Job]) private val mutex = Mutex() @@ -44,19 +52,17 @@ public class DeviceClient internal constructor( private val flowInternal = incomingFlow.filter { it.sourceDevice == deviceName - }.shareIn(this, started = SharingStarted.Eagerly).also { - it.onEach { message -> - when (message) { - is PropertyChangedMessage -> mutex.withLock { - propertyCache[message.property] = message.value - } - - else -> { - //ignore - } + }.onEach { message -> + when (message) { + is PropertyChangedMessage -> mutex.withLock { + propertyCache[message.property] = message.value } - }.launchIn(this) - } + + else -> { + //ignore + } + } + }.shareIn(this, started = SharingStarted.Eagerly) override val messageFlow: Flow<DeviceMessage> get() = flowInternal @@ -65,7 +71,7 @@ public class DeviceClient internal constructor( send( PropertyGetMessage(propertyName, targetDevice = deviceName) ) - return flowInternal.filterIsInstance<PropertyChangedMessage>().first { + return messageFlow.filterIsInstance<PropertyChangedMessage>().first { it.property == propertyName }.value } @@ -89,30 +95,33 @@ public class DeviceClient internal constructor( send( ActionExecuteMessage(actionName, argument, id, targetDevice = deviceName) ) - return flowInternal.filterIsInstance<ActionResultMessage>().first { + return messageFlow.filterIsInstance<ActionResultMessage>().first { it.action == actionName && it.requestId == id }.result } + private val lifecycleStateFlow = messageFlow.filterIsInstance<DeviceLifeCycleMessage>() + .map { it.state }.stateIn(this, started = SharingStarted.Eagerly, DeviceLifecycleState.STARTED) + @DFExperimental - override val lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STARTED + override val lifecycleState: DeviceLifecycleState get() = lifecycleStateFlow.value } /** * Connect to a remote device via this endpoint. * * @param context a [Context] to run device in - * @param sourceEndpointName the name of this endpoint - * @param targetEndpointName the name of endpoint in Magix to connect to + * @param thisEndpoint the name of this endpoint + * @param deviceEndpoint the name of endpoint in Magix to connect to * @param deviceName the name of device within endpoint */ public suspend fun MagixEndpoint.remoteDevice( context: Context, - sourceEndpointName: String, - targetEndpointName: String, + thisEndpoint: String, + deviceEndpoint: String, deviceName: Name, -): DeviceClient = coroutineScope{ - val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(targetEndpointName)).map { it.second } +): DeviceClient = coroutineScope { + val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)).map { it.second } val deferredDescriptorMessage = CompletableDeferred<DescriptionMessage>() @@ -123,8 +132,8 @@ public suspend fun MagixEndpoint.remoteDevice( send( format = DeviceManager.magixFormat, payload = GetDescriptionMessage(targetDevice = deviceName), - source = sourceEndpointName, - target = targetEndpointName, + source = thisEndpoint, + target = deviceEndpoint, id = stringUID() ) @@ -141,14 +150,37 @@ public suspend fun MagixEndpoint.remoteDevice( send( format = DeviceManager.magixFormat, payload = it, - source = sourceEndpointName, - target = targetEndpointName, + source = thisEndpoint, + target = deviceEndpoint, id = stringUID() ) } } -//public fun MagixEndpoint.remoteDeviceHub() +private class MapBasedDeviceHub(val deviceMap: Map<Name, Device>, val prefix: Name) : DeviceHub { + override val devices: Map<Name, Device> + get() = deviceMap.filterKeys { name: Name -> name == prefix || (name.startsWith(prefix) && name.length == prefix.length + 1) } + +} + +public fun MagixEndpoint.remoteDeviceHub( + context: Context, + thisEndpoint: String, + deviceEndpoint: String, +): DeviceHub { + val devices = mutableMapOf<Name, DeviceClient>() + val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)).map { it.second } + subscription.filterIsInstance<DescriptionMessage>().onEach { + + }.launchIn(context) + + + return object : DeviceHub { + override val devices: Map<Name, Device> + get() = TODO("Not yet implemented") + + } +} /** * Subscribe on specific property of a device without creating a device diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt index d0bdda4..8f3e742 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt @@ -5,12 +5,12 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import space.kscience.controls.api.get import space.kscience.controls.api.getOrReadProperty import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.context.error import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.names.get import space.kscience.magix.api.* public const val TANGO_MAGIX_FORMAT: String = "tango" @@ -88,7 +88,7 @@ public fun DeviceManager.launchTangoMagix( return context.launch { endpoint.subscribe(tangoMagixFormat).onEach { (request, payload) -> try { - val device = get(payload.device) + val device = devices[payload.device] ?: error("Device ${payload.device} not found") when (payload.action) { TangoAction.read -> { val value = device.getOrReadProperty(payload.name) @@ -99,6 +99,7 @@ public fun DeviceManager.launchTangoMagix( ) } } + TangoAction.write -> { payload.value?.let { value -> device.writeProperty(payload.name, value) @@ -112,6 +113,7 @@ public fun DeviceManager.launchTangoMagix( ) } } + TangoAction.exec -> { val result = device.execute(payload.name, payload.argin) respond(request, payload) { requestPayload -> @@ -121,6 +123,7 @@ public fun DeviceManager.launchTangoMagix( ) } } + TangoAction.pipe -> TODO("Pipe not implemented") } } catch (ex: Exception) { diff --git a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt index ff82486..db80848 100644 --- a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt +++ b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt @@ -59,7 +59,7 @@ internal class RemoteDeviceConnect { val virtualMagixEndpoint = object : MagixEndpoint { - private val additionalMessages = MutableSharedFlow<DeviceMessage>(10) + private val additionalMessages = MutableSharedFlow<DeviceMessage>(1) override fun subscribe( filter: MagixMessageFilter, diff --git a/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt index e0bd47a..8557f9d 100644 --- a/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt +++ b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt @@ -7,7 +7,7 @@ import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceSpec import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.names.NameToken +import space.kscience.dataforge.names.Name /** * A variant of [DeviceBySpec] that includes Modbus RTU/TCP/UDP client @@ -35,12 +35,12 @@ public open class ModbusDeviceBySpec<D: Device>( public class ModbusHub( public val context: Context, public val masterBuilder: () -> AbstractModbusMaster, - public val specs: Map<NameToken, Pair<Int, DeviceSpec<*>>>, + public val specs: Map<Name, Pair<Int, DeviceSpec<*>>>, ) : DeviceHub, AutoCloseable { public val master: AbstractModbusMaster by lazy(masterBuilder) - override val devices: Map<NameToken, ModbusDevice> by lazy { + override val devices: Map<Name, ModbusDevice> by lazy { specs.mapValues { (_, pair) -> ModbusDeviceBySpec( context, diff --git a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt index 04bb46d..4f37322 100644 --- a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt +++ b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt @@ -29,19 +29,18 @@ import kotlinx.serialization.json.put import space.kscience.controls.api.DeviceMessage import space.kscience.controls.api.PropertyGetMessage import space.kscience.controls.api.PropertySetMessage -import space.kscience.controls.api.getOrNull import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.respondHubMessage import space.kscience.dataforge.meta.toMeta import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName +import space.kscience.dataforge.names.get import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.MagixFlowPlugin import space.kscience.magix.api.MagixMessage import space.kscience.magix.server.magixModule - private fun Application.deviceServerModule(manager: DeviceManager) { install(StatusPages) { exception<IllegalArgumentException> { call, cause -> @@ -100,10 +99,9 @@ public fun Application.deviceManagerModule( h1 { +"Device server dashboard" } - deviceNames.forEach { deviceName -> - val device = - manager.getOrNull(deviceName) - ?: error("The device with name $deviceName not found in $manager") + deviceNames.forEach { deviceName: String -> + val device = manager.devices[deviceName] + ?: error("The device with name $deviceName not found in $manager") div { id = deviceName h2 { +deviceName } diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt index 6c271d7..106b077 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -19,7 +19,8 @@ import space.kscience.controls.ports.withStringDelimiter import space.kscience.controls.spec.* import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.* -import space.kscience.dataforge.names.NameToken +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.parseAsName import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.time.Duration @@ -47,7 +48,7 @@ class PiMotionMasterDevice( var axes: Map<String, Axis> = emptyMap() private set - override val devices: Map<NameToken, Axis> = axes.mapKeys { (key, _) -> NameToken(key) } + override val devices: Map<Name, Axis> = axes.mapKeys { (key, _) -> key.parseAsName() } private suspend fun failIfError(message: (Int) -> String = { "Failed with error code $it" }) { val errorCode = getErrorCode() From 4a10c3c44345b115d33612a4b3c6f44fcaa0223d Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 19 May 2024 18:50:56 +0300 Subject: [PATCH 086/125] Add test for remote hub --- .../space/kscience/controls/api/DeviceHub.kt | 5 + .../kscience/controls/client/DeviceClient.kt | 55 ++++++++-- .../controls/client/RemoteDeviceConnect.kt | 100 ++++++++++++------ 3 files changed, 122 insertions(+), 38 deletions(-) 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 077585b..e2f6331 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 @@ -26,6 +26,11 @@ public interface DeviceHub : Provider { public companion object } +public fun DeviceHub(deviceMap: Map<Name, Device>): DeviceHub = object : DeviceHub { + override val devices: Map<Name, Device> + get() = deviceMap +} + /** * List all devices, including sub-devices */ 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 9418791..859bce2 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 @@ -163,23 +163,64 @@ private class MapBasedDeviceHub(val deviceMap: Map<Name, Device>, val prefix: Na } -public fun MagixEndpoint.remoteDeviceHub( +/** + * Create a dynamic [DeviceHub] from incoming messages + */ +public suspend fun MagixEndpoint.remoteDeviceHub( context: Context, thisEndpoint: String, deviceEndpoint: String, ): DeviceHub { val devices = mutableMapOf<Name, DeviceClient>() val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)).map { it.second } - subscription.filterIsInstance<DescriptionMessage>().onEach { - + subscription.filterIsInstance<DescriptionMessage>().onEach { descriptionMessage -> + devices.getOrPut(descriptionMessage.sourceDevice) { + DeviceClient( + context = context, + deviceName = descriptionMessage.sourceDevice, + propertyDescriptors = descriptionMessage.properties, + actionDescriptors = descriptionMessage.actions, + incomingFlow = subscription + ) { + send( + format = DeviceManager.magixFormat, + payload = it, + source = thisEndpoint, + target = deviceEndpoint, + id = stringUID() + ) + } + }.run { + propertyDescriptors = descriptionMessage.properties + } }.launchIn(context) - return object : DeviceHub { - override val devices: Map<Name, Device> - get() = TODO("Not yet implemented") + send( + format = DeviceManager.magixFormat, + payload = GetDescriptionMessage(targetDevice = null), + source = thisEndpoint, + target = deviceEndpoint, + id = stringUID() + ) - } + return DeviceHub(devices) +} + +/** + * Request a description update for all devices on an endpoint + */ +public suspend fun MagixEndpoint.requestDeviceUpdate( + thisEndpoint: String, + deviceEndpoint: String, +) { + send( + format = DeviceManager.magixFormat, + payload = GetDescriptionMessage(), + source = thisEndpoint, + target = deviceEndpoint, + id = stringUID() + ) } /** 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 db80848..3c707b2 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 @@ -1,15 +1,21 @@ package space.kscience.controls.client +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json +import space.kscience.controls.api.DeviceHub import space.kscience.controls.api.DeviceMessage import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.hubMessageFlow import space.kscience.controls.manager.install -import space.kscience.controls.manager.respondMessage +import space.kscience.controls.manager.respondHubMessage import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory @@ -17,15 +23,43 @@ 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.dataforge.names.asName 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.test.assertEquals import kotlin.time.Duration.Companion.milliseconds +class VirtualMagixEndpoint(val hub: DeviceHub) : MagixEndpoint { + + private val additionalMessages = MutableSharedFlow<DeviceMessage>(1) + + override fun subscribe( + filter: MagixMessageFilter, + ): Flow<MagixMessage> = merge(hub.hubMessageFlow(), additionalMessages).map { + MagixMessage( + format = DeviceManager.magixFormat.defaultFormat, + payload = MagixEndpoint.magixJson.encodeToJsonElement(DeviceManager.magixFormat.serializer, it), + sourceEndpoint = "device", + ) + } + + override suspend fun broadcast(message: MagixMessage) { + hub.respondHubMessage( + Json.decodeFromJsonElement(DeviceManager.magixFormat.serializer, message.payload) + ).forEach { + additionalMessages.emit(it) + } + } + + override fun close() { + // + } +} + internal class RemoteDeviceConnect { @@ -53,40 +87,44 @@ internal class RemoteDeviceConnect { val context = Context { plugin(DeviceManager) } + val deviceManager = context.request(DeviceManager) - val device = context.request(DeviceManager).install("test", TestDevice) + deviceManager.install("test", TestDevice) - val virtualMagixEndpoint = object : MagixEndpoint { + val virtualMagixEndpoint = VirtualMagixEndpoint(deviceManager) - - private val additionalMessages = MutableSharedFlow<DeviceMessage>(1) - - override fun subscribe( - filter: MagixMessageFilter, - ): Flow<MagixMessage> = merge(device.messageFlow, additionalMessages).map { - MagixMessage( - format = DeviceManager.magixFormat.defaultFormat, - payload = MagixEndpoint.magixJson.encodeToJsonElement(DeviceManager.magixFormat.serializer, it), - sourceEndpoint = "device", - ) - } - - override suspend fun broadcast(message: MagixMessage) { - device.respondMessage( - Name.EMPTY, - Json.decodeFromJsonElement(DeviceManager.magixFormat.serializer, message.payload) - )?.let { - additionalMessages.emit(it) - } - } - - override fun close() { - // - } - } - val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "client", "device", Name.EMPTY) + val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "client", "device", "test".asName()) assertContains(0.0..1.0, remoteDevice.read(TestDevice.value)) } + + @Test + fun deviceHub() = runTest { + val context = Context { + plugin(DeviceManager) + } + val deviceManager = context.request(DeviceManager) + + launch { + delay(50) + repeat(10) { + deviceManager.install("test[$it]", TestDevice) + } + } + + val virtualMagixEndpoint = VirtualMagixEndpoint(deviceManager) + + val remoteHub = virtualMagixEndpoint.remoteDeviceHub(context, "client", "device") + + assertEquals(0, remoteHub.devices.size) + + delay(60) + //switch context to use actual delay + withContext(Dispatchers.Default) { + virtualMagixEndpoint.requestDeviceUpdate("client", "device") + delay(30) + assertEquals(10, remoteHub.devices.size) + } + } } \ No newline at end of file From a66e411848663c9c652250aa241d40c4befe19d7 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Tue, 21 May 2024 09:44:45 +0300 Subject: [PATCH 087/125] Fix Rsocket endpoint without filter. Add integration test with loop --- CHANGELOG.md | 1 + build.gradle.kts | 2 +- controls-magix/build.gradle.kts | 6 ++ .../kscience/controls/client/MagixLoopTest.kt | 60 +++++++++++++++++++ .../magix/server/RSocketMagixFlowPlugin.kt | 15 +++-- 5 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c7ea2f..5a1dad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### Removed ### Fixed +- Fix a problem with rsocket endpoint with no filter. ### Security diff --git a/build.gradle.kts b/build.gradle.kts index 0f00407..6423c3c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { allprojects { group = "space.kscience" - version = "0.4.0-dev-2" + version = "0.4.0-dev-3" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-magix/build.gradle.kts b/controls-magix/build.gradle.kts index ef4a3af..ea03a7e 100644 --- a/controls-magix/build.gradle.kts +++ b/controls-magix/build.gradle.kts @@ -17,6 +17,7 @@ kscience { useSerialization { json() } + commonMain { api(projects.magix.magixApi) api(projects.controlsCore) @@ -25,6 +26,11 @@ kscience { jvmTest{ implementation(spclibs.logback.classic) + implementation(projects.magix.magixServer) + implementation(projects.magix.magixRsocket) + implementation(spclibs.ktor.server.cio) + implementation(spclibs.ktor.server.websockets) + implementation(spclibs.ktor.client.cio) } } diff --git a/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt b/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt new file mode 100644 index 0000000..764dcad --- /dev/null +++ b/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt @@ -0,0 +1,60 @@ +package space.kscience.controls.client + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import space.kscience.controls.client.RemoteDeviceConnect.TestDevice +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.rsocket.rSocketWithWebSockets +import space.kscience.magix.server.startMagixServer +import kotlin.test.Test +import kotlin.test.assertEquals + +class MagixLoopTest { + + @Test + fun deviceHub() = runTest { + val context = Context { + plugin(DeviceManager) + } + + val server = context.startMagixServer() + + val deviceManager = context.request(DeviceManager) + + val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + +// deviceEndpoint.subscribe().onEach { +// println(it) +// }.launchIn(this) + + deviceManager.launchMagixService(deviceEndpoint, "device") + + launch { + delay(50) + repeat(10) { + deviceManager.install("test[$it]", TestDevice) + } + } + + val clientEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + + val remoteHub = clientEndpoint.remoteDeviceHub(context, "client", "device") + + assertEquals(0, remoteHub.devices.size) + + delay(60) + //switch context to use actual delay + withContext(Dispatchers.Default) { + clientEndpoint.requestDeviceUpdate("client", "device") + delay(60) + assertEquals(10, remoteHub.devices.size) + } + } +} \ No newline at end of file diff --git a/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt index a00c33f..699a281 100644 --- a/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt +++ b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt @@ -59,9 +59,12 @@ public class RSocketMagixFlowPlugin( RSocketRequestHandler(coroutineScope.coroutineContext) { //handler for request/stream requestStream { request: Payload -> - val filter = MagixEndpoint.magixJson.decodeFromString( + val requestText = request.data.readText() + val filter = if(requestText.isBlank()) { + MagixMessageFilter.ALL + } else MagixEndpoint.magixJson.decodeFromString( MagixMessageFilter.serializer(), - request.data.readText() + requestText ) receive.filter(filter).map { message -> @@ -89,12 +92,12 @@ public class RSocketMagixFlowPlugin( ) }.launchIn(this) - val filterText = request.use { it.data.readText() } + val filterText = request.data.readText() - val filter = if (filterText.isNotBlank()) { - MagixEndpoint.magixJson.decodeFromString(MagixMessageFilter.serializer(), filterText) + val filter = if (filterText.isBlank()) { + MagixMessageFilter.ALL } else { - MagixMessageFilter() + MagixEndpoint.magixJson.decodeFromString(MagixMessageFilter.serializer(), filterText) } receive.filter(filter).map { message -> From 673a7c89a6e6e06a089da89c60492f559284c745 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Tue, 21 May 2024 13:31:01 +0300 Subject: [PATCH 088/125] [WIP] working on constructor --- .../kscience/controls/constructor/Binding.kt | 35 --------- .../controls/constructor/DeviceConstructor.kt | 24 ++---- .../controls/constructor/DeviceModel.kt | 12 +-- .../controls/constructor/StateDescriptor.kt | 74 +++++++++++++++++++ .../{customState.kt => exoticState.kt} | 4 +- controls-vision/build.gradle.kts | 2 +- 6 files changed, 92 insertions(+), 59 deletions(-) delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Binding.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt rename controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/{customState.kt => exoticState.kt} (94%) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Binding.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Binding.kt deleted file mode 100644 index d712d62..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Binding.kt +++ /dev/null @@ -1,35 +0,0 @@ -package space.kscience.controls.constructor - -import space.kscience.controls.api.Device - -/** - * A binding that is used to describe device functionality - */ -public sealed interface Binding - -/** - * A binding that exposes device property as read-only state - */ -public class PropertyBinding<T>( - public val device: Device, - public val propertyName: String, - public val state: DeviceState<T>, -) : Binding - -/** - * A binding for independent state like a timer - */ -public class StateBinding<T>( - public val state: DeviceState<T> -) : Binding - -public class ActionBinding( - public val reads: Collection<DeviceState<*>>, - public val writes: Collection<DeviceState<*>> -): Binding - - -public interface BindingsContainer{ - public val bindings: List<Binding> - public fun registerBinding(binding: Binding) -} \ No newline at end of file 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 5b6ff9f..03a1e0e 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,12 +5,10 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor -import space.kscience.controls.manager.ClockManager import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.MutableDevicePropertySpec 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.names.Name @@ -26,25 +24,19 @@ import kotlin.time.Duration public abstract class DeviceConstructor( context: Context, meta: Meta = Meta.EMPTY, -) : DeviceGroup(context, meta), BindingsContainer { - private val _bindings: MutableList<Binding> = mutableListOf() - override val bindings: List<Binding> get() = _bindings +) : DeviceGroup(context, meta), StateContainer { + private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf() + override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors - override fun registerBinding(binding: Binding) { - _bindings.add(binding) + override fun registerState(stateDescriptor: StateDescriptor) { + _stateDescriptors.add(stateDescriptor) } override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { super.registerProperty(descriptor, state) - registerBinding(PropertyBinding(this, descriptor.name, state)) + registerState(PropertyStateDescriptor(this, descriptor.name, state)) } - /** - * Create and register a timer. Timer is not counted as a device property. - */ - public fun timer(tick: Duration): TimerState = TimerState(context.request(ClockManager), tick) - .also { registerBinding(StateBinding(it)) } - /** * Bind an action to a [DeviceState]. [onChange] block is performed on each state change * @@ -55,7 +47,7 @@ public abstract class DeviceConstructor( reads: Collection<DeviceState<*>>, onChange: suspend (T) -> Unit, ): Job = valueFlow.onEach(onChange).launchIn(this@DeviceConstructor).also { - registerBinding(ActionBinding(setOf(this, *reads.toTypedArray()), setOf(*writes))) + registerState(ConnectionStateDescriptor(setOf(this, *reads.toTypedArray()), setOf(*writes))) } } @@ -171,4 +163,4 @@ public fun <T, D : Device> DeviceConstructor.deviceProperty( property: MutableDevicePropertySpec<D, T>, initialValue: T, ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = - property(device.mutablePropertyAsState(property, initialValue)) \ No newline at end of file + property(device.mutablePropertyAsState(property, initialValue)) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt index 08913a2..6d44422 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt @@ -1,12 +1,14 @@ package space.kscience.controls.constructor -public abstract class DeviceModel : BindingsContainer { +import space.kscience.dataforge.context.Context - private val _bindings: MutableList<Binding> = mutableListOf() +public abstract class DeviceModel(override val context: Context) : StateContainer { - override val bindings: List<Binding> get() = _bindings + private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf() - override fun registerBinding(binding: Binding) { - _bindings.add(binding) + override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors + + override fun registerState(stateDescriptor: StateDescriptor) { + _stateDescriptors.add(stateDescriptor) } } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt new file mode 100644 index 0000000..b8f40b5 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt @@ -0,0 +1,74 @@ +package space.kscience.controls.constructor + +import space.kscience.controls.api.Device +import space.kscience.controls.manager.ClockManager +import space.kscience.dataforge.context.ContextAware +import space.kscience.dataforge.context.request +import space.kscience.dataforge.meta.MetaConverter +import kotlin.time.Duration + +/** + * A binding that is used to describe device functionality + */ +public sealed interface StateDescriptor + +/** + * A binding that exposes device property as read-only state + */ +public class PropertyStateDescriptor<T>( + public val device: Device, + public val propertyName: String, + public val state: DeviceState<T>, +) : StateDescriptor + +/** + * A binding for independent state like a timer + */ +public class StateBinding<T>( + public val state: DeviceState<T>, +) : StateDescriptor + +public class ConnectionStateDescriptor( + public val reads: Collection<DeviceState<*>>, + public val writes: Collection<DeviceState<*>>, +) : StateDescriptor + + +public interface StateContainer : ContextAware { + public val stateDescriptors: List<StateDescriptor> + public fun registerState(stateDescriptor: StateDescriptor) +} + +/** + * Register a [state] in this container. The state is not registered as a device property if [this] is a [DeviceConstructor] + */ +public fun <T, D : DeviceState<T>> StateContainer.state(state: D): D { + registerState(StateBinding(state)) + return state +} + +/** + * Create a register a [MutableDeviceState] with a given [converter] + */ +public fun <T> StateContainer.state(converter: MetaConverter<T>, initialValue: T): MutableDeviceState<T> = state( + DeviceState.internal(converter, initialValue) +) + +/** + * Create a register a mutable [Double] state + */ +public fun StateContainer.doubleState(initialValue: Double): MutableDeviceState<Double> = state( + MetaConverter.double, initialValue +) + +/** + * Create a register a mutable [String] state + */ +public fun StateContainer.stringState(initialValue: String): MutableDeviceState<String> = state( + MetaConverter.string, initialValue +) + +/** + * Create and register a timer state. + */ +public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(context.request(ClockManager), tick)) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt similarity index 94% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt index 277271a..d1d557f 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt @@ -51,9 +51,9 @@ public class DoubleInRangeState( /** * Create and register a [DoubleInRangeState] */ -public fun BindingsContainer.doubleInRangeState( +public fun StateContainer.doubleInRangeState( initialValue: Double, range: ClosedFloatingPointRange<Double>, ): DoubleInRangeState = DoubleInRangeState(initialValue, range).also { - registerBinding(StateBinding(it)) + registerState(StateBinding(it)) } \ No newline at end of file diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts index 5c75242..09c644b 100644 --- a/controls-vision/build.gradle.kts +++ b/controls-vision/build.gradle.kts @@ -12,7 +12,7 @@ kscience { useKtor() useSerialization() useContextReceivers() - dependencies { + commonMain { api(projects.controlsCore) api(projects.controlsConstructor) api(libs.visionforge.plotly) From 55bcb0866814bdbbf17d5eb74e2182ca408cea4a Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Thu, 23 May 2024 16:16:22 +0300 Subject: [PATCH 089/125] Force to use endpoint ID in `launchMagixService` --- .../kotlin/space/kscience/controls/client/controlsMagix.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1c6f556..5fe99c3 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 @@ -40,7 +40,7 @@ internal fun generateId(request: MagixMessage): String = if (request.id != null) */ public fun DeviceManager.launchMagixService( endpoint: MagixEndpoint, - endpointID: String = controlsMagixFormat.defaultFormat, + endpointID: String, coroutineContext: CoroutineContext = EmptyCoroutineContext, ): Job = context.launch(coroutineContext) { endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID, null)).onEach { (request, payload) -> From 05757aefdc1afbe674c67be6f0602ebf0ee37125 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Fri, 24 May 2024 13:56:54 +0300 Subject: [PATCH 090/125] First draft of model binding --- .space/CODEOWNERS | 1 - build.gradle.kts | 5 +- .../controls/constructor/DeviceConstructor.kt | 46 ++--- .../controls/constructor/DeviceGroup.kt | 56 ++++-- .../controls/constructor/DeviceModel.kt | 24 ++- .../controls/constructor/DeviceState.kt | 21 +- .../controls/constructor/StateDescriptor.kt | 169 +++++++++++++--- .../controls/constructor/TimerState.kt | 3 - .../controls/constructor/boundState.kt | 2 +- .../controls/constructor/exoticState.kt | 10 +- .../controls/constructor/externalState.kt | 13 +- .../controls/constructor/flowState.kt | 7 +- .../controls/constructor/internalState.kt | 9 +- .../space/kscience/controls/api/DeviceHub.kt | 1 + .../controls/{spec => misc}/converters.kt | 20 +- .../kscience/controls/api/MessageTest.kt | 3 +- .../kscience/controls/client/DeviceClient.kt | 18 +- .../kscience/controls/client/controlsMagix.kt | 3 +- .../kotlin/BooleanIndicatorVision.kt | 24 --- .../commonMain/kotlin/ControlVisionPlugin.kt | 3 +- .../src/commonMain/kotlin/controlsVisions.kt | 35 ++++ .../src/commonMain/kotlin/plotExtensions.kt | 2 +- .../jsMain/kotlin/ControlsVisionPlugin.js.kt | 32 ++- .../controls/demo/car/VirtualCarController.kt | 2 +- demo/constructor/build.gradle.kts | 12 +- .../src/jvmMain/kotlin/BodyOnSprings.kt | 185 ++++++++++++++++++ .../pimotionmaster/PiMotionMasterDevice.kt | 2 + gradle/libs.versions.toml | 1 + 28 files changed, 537 insertions(+), 172 deletions(-) delete mode 100644 .space/CODEOWNERS rename controls-core/src/commonMain/kotlin/space/kscience/controls/{spec => misc}/converters.kt (62%) delete mode 100644 controls-vision/src/commonMain/kotlin/BooleanIndicatorVision.kt create mode 100644 controls-vision/src/commonMain/kotlin/controlsVisions.kt create mode 100644 demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt diff --git a/.space/CODEOWNERS b/.space/CODEOWNERS deleted file mode 100644 index 9f836ea..0000000 --- a/.space/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -./space/* "Project Admin" diff --git a/build.gradle.kts b/build.gradle.kts index 6423c3c..50a61d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,10 +7,7 @@ plugins { allprojects { group = "space.kscience" - version = "0.4.0-dev-3" - repositories{ - maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") - } + version = "0.4.0-dev-4" } ksciencePublish { 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 03a1e0e..17294f1 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 @@ -1,8 +1,5 @@ package space.kscience.controls.constructor -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.spec.DevicePropertySpec @@ -25,29 +22,24 @@ public abstract class DeviceConstructor( context: Context, meta: Meta = Meta.EMPTY, ) : DeviceGroup(context, meta), StateContainer { - private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf() - override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors + private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf() + override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors override fun registerState(stateDescriptor: StateDescriptor) { _stateDescriptors.add(stateDescriptor) } - override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { - super.registerProperty(descriptor, state) - registerState(PropertyStateDescriptor(this, descriptor.name, state)) + override fun unregisterState(stateDescriptor: StateDescriptor) { + _stateDescriptors.remove(stateDescriptor) } - /** - * Bind an action to a [DeviceState]. [onChange] block is performed on each state change - * - * Optionally provide [writes] - a set of states that this change affects. - */ - public fun <T> DeviceState<T>.onChange( - vararg writes: DeviceState<*>, - reads: Collection<DeviceState<*>>, - onChange: suspend (T) -> Unit, - ): Job = valueFlow.onEach(onChange).launchIn(this@DeviceConstructor).also { - registerState(ConnectionStateDescriptor(setOf(this, *reads.toTypedArray()), setOf(*writes))) + override fun <T> registerProperty( + converter: MetaConverter<T>, + descriptor: PropertyDescriptor, + state: DeviceState<T>, + ) { + super.registerProperty(converter, descriptor, state) + registerState(StatePropertyDescriptor(this, descriptor.name, state)) } } @@ -84,6 +76,7 @@ public fun <D : Device> DeviceConstructor.device( * Register a property and provide a direct reader for it */ public fun <T, S : DeviceState<T>> DeviceConstructor.property( + converter: MetaConverter<T>, state: S, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, @@ -91,7 +84,7 @@ public fun <T, S : DeviceState<T>> DeviceConstructor.property( PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) - registerProperty(descriptor, state) + registerProperty(converter, descriptor, state) ReadOnlyProperty { _: DeviceConstructor, _ -> state } @@ -108,7 +101,8 @@ public fun <T : Any> DeviceConstructor.property( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property( - DeviceState.external(this, metaConverter, readInterval, initialState, reader), + metaConverter, + DeviceState.external(this, readInterval, initialState, reader), descriptorBuilder, nameOverride, ) @@ -125,7 +119,8 @@ public fun <T : Any> DeviceConstructor.mutableProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( - DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), + metaConverter, + DeviceState.external(this, readInterval, initialState, reader, writer), descriptorBuilder, nameOverride, ) @@ -140,7 +135,8 @@ public fun <T> DeviceConstructor.virtualProperty( nameOverride: String? = null, callback: (T) -> Unit = {}, ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( - DeviceState.internal(metaConverter, initialState, callback), + metaConverter, + MutableDeviceState(initialState, callback), descriptorBuilder, nameOverride, ) @@ -153,7 +149,7 @@ public fun <T, D : Device> DeviceConstructor.deviceProperty( property: DevicePropertySpec<D, T>, initialValue: T, ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = - property(device.propertyAsState(property, initialValue)) + property(property.converter, device.propertyAsState(property, initialValue)) /** * Bind existing property provided by specification to this device @@ -163,4 +159,4 @@ public fun <T, D : Device> DeviceConstructor.deviceProperty( property: MutableDevicePropertySpec<D, T>, initialValue: T, ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = - property(device.mutablePropertyAsState(property, initialValue)) + property(property.converter, device.mutablePropertyAsState(property, initialValue)) 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 ef886dc..ddac115 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 @@ -1,14 +1,12 @@ 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 kotlinx.coroutines.flow.* 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.controls.spec.DevicePropertySpec import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.Laminate import space.kscience.dataforge.meta.Meta @@ -30,12 +28,21 @@ public open class DeviceGroup( override val meta: Meta, ) : DeviceHub, CachingDevice { - internal class Property( - val state: DeviceState<*>, + private class Property<T>( + val state: DeviceState<T>, + val converter: MetaConverter<T>, val descriptor: PropertyDescriptor, - ) + ) { + val valueAsMeta get() = converter.convert(state.value) - internal class Action( + fun setMeta(meta: Meta) { + check(state is MutableDeviceState) { "Can't write to read-only property" } + + state.value = converter.read(meta) + } + } + + private class Action( val invoke: suspend (Meta?) -> Meta?, val descriptor: ActionDescriptor, ) @@ -81,16 +88,20 @@ public open class DeviceGroup( return device } - private val properties: MutableMap<Name, Property> = hashMapOf() + private val properties: MutableMap<Name, Property<*>> = hashMapOf() /** * Register a new property based on [DeviceState]. Properties could be modified dynamically */ - public open fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { + public open fun <T> registerProperty( + converter: MetaConverter<T>, + descriptor: PropertyDescriptor, + state: DeviceState<T>, + ) { 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 { + properties[name] = Property(state, converter, descriptor) + state.valueFlow.map(converter::convert).onEach { sharedMessageFlow.emit( PropertyChangedMessage( descriptor.name, @@ -109,19 +120,18 @@ public open class DeviceGroup( get() = actions.values.map { it.descriptor } override suspend fun readProperty(propertyName: String): Meta = - properties[propertyName.parseAsName()]?.state?.valueAsMeta + properties[propertyName.parseAsName()]?.valueAsMeta ?: error("Property with name $propertyName not found") - override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.state?.valueAsMeta + override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.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 + val property = properties[propertyName.parseAsName()] ?: error("Property with name $propertyName not found") + property.setMeta(value) } @@ -164,6 +174,10 @@ public open class DeviceGroup( } } +public fun <T> DeviceGroup.registerProperty(propertySpec: DevicePropertySpec<*, T>, state: DeviceState<T>) { + registerProperty(propertySpec.converter, propertySpec.descriptor, state) +} + public fun DeviceManager.registerDeviceGroup( name: String = "@group", meta: Meta = Meta.EMPTY, @@ -234,10 +248,12 @@ public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() - */ public fun <T : Any> DeviceGroup.registerProperty( name: String, + converter: MetaConverter<T>, state: DeviceState<T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ) { registerProperty( + converter, PropertyDescriptor(name).apply(descriptorBuilder), state ) @@ -248,10 +264,12 @@ public fun <T : Any> DeviceGroup.registerProperty( */ public fun <T : Any> DeviceGroup.registerMutableProperty( name: String, + converter: MetaConverter<T>, state: MutableDeviceState<T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ) { registerProperty( + converter, PropertyDescriptor(name).apply(descriptorBuilder), state ) @@ -269,7 +287,7 @@ public fun <T : Any> DeviceGroup.registerVirtualProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, callback: (T) -> Unit = {}, ): MutableDeviceState<T> { - val state = DeviceState.internal<T>(converter, initialValue, callback) - registerMutableProperty(name, state, descriptorBuilder) + val state = MutableDeviceState<T>(initialValue, callback) + registerMutableProperty(name, converter, state, descriptorBuilder) return state } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt index 6d44422..484b471 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt @@ -1,14 +1,32 @@ package space.kscience.controls.constructor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.newCoroutineContext import space.kscience.dataforge.context.Context +import kotlin.coroutines.CoroutineContext -public abstract class DeviceModel(override val context: Context) : StateContainer { +public abstract class DeviceModel( + final override val context: Context, + vararg dependencies: DeviceState<*>, +) : StateContainer, CoroutineScope { - private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf() + override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob()) - override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors + + private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf<StateDescriptor>().apply { + dependencies.forEach { + add(StateNodeDescriptor(it)) + } + } + + override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors override fun registerState(stateDescriptor: StateDescriptor) { _stateDescriptors.add(stateDescriptor) } + + override fun unregisterState(stateDescriptor: StateDescriptor) { + _stateDescriptors.remove(stateDescriptor) + } } \ 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 8a2914b..e74c4c0 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 @@ -6,15 +6,12 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.MetaConverter import kotlin.reflect.KProperty /** * An observable state of a device */ public interface DeviceState<T> { - public val converter: MetaConverter<T> public val value: T public val valueFlow: Flow<T> @@ -24,9 +21,6 @@ public interface DeviceState<T> { public companion object } -public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::convert) - -public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.convert(value) public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value @@ -47,12 +41,6 @@ public operator fun <T> MutableDeviceState<T>.setValue(thisRef: Any?, property: this.value = value } -public var <T> MutableDeviceState<T>.valueAsMeta: Meta - get() = converter.convert(value) - set(arg) { - value = converter.read(arg) - } - /** * Device state with a value that depends on other device states */ @@ -65,17 +53,15 @@ public interface DeviceStateWithDependencies<T> : DeviceState<T> { */ public fun <T, R> DeviceState.Companion.map( state: DeviceState<T>, - converter: MetaConverter<R>, mapper: (T) -> R, + mapper: (T) -> R, ): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> { override val dependencies = listOf(state) - override val converter: MetaConverter<R> = converter - override val value: R get() = mapper(state.value) override val valueFlow: Flow<R> = state.valueFlow.map(mapper) - override fun toString(): String = "DeviceState.map(arg=${state}, converter=$converter)" + override fun toString(): String = "DeviceState.map(arg=${state})" } /** @@ -84,13 +70,10 @@ public fun <T, R> DeviceState.Companion.map( public fun <T1, T2, R> DeviceState.Companion.combine( state1: DeviceState<T1>, state2: DeviceState<T2>, - converter: MetaConverter<R>, mapper: (T1, T2) -> R, ): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> { override val dependencies = listOf(state1, state2) - override val converter: MetaConverter<R> = converter - override val value: R get() = mapper(state1.value, state2.value) override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt index b8f40b5..30a0299 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt @@ -1,10 +1,14 @@ package space.kscience.controls.constructor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.runningFold import space.kscience.controls.api.Device import space.kscience.controls.manager.ClockManager import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.request -import space.kscience.dataforge.meta.MetaConverter import kotlin.time.Duration /** @@ -15,7 +19,7 @@ public sealed interface StateDescriptor /** * A binding that exposes device property as read-only state */ -public class PropertyStateDescriptor<T>( +public class StatePropertyDescriptor<T>( public val device: Device, public val propertyName: String, public val state: DeviceState<T>, @@ -24,51 +28,172 @@ public class PropertyStateDescriptor<T>( /** * A binding for independent state like a timer */ -public class StateBinding<T>( +public class StateNodeDescriptor<T>( public val state: DeviceState<T>, ) : StateDescriptor -public class ConnectionStateDescriptor( +public class StateConnectionDescriptor( public val reads: Collection<DeviceState<*>>, public val writes: Collection<DeviceState<*>>, ) : StateDescriptor -public interface StateContainer : ContextAware { - public val stateDescriptors: List<StateDescriptor> +public interface StateContainer : ContextAware, CoroutineScope { + public val stateDescriptors: Set<StateDescriptor> public fun registerState(stateDescriptor: StateDescriptor) + public fun unregisterState(stateDescriptor: StateDescriptor) + + + /** + * Bind an action to a [DeviceState]. [onChange] block is performed on each state change + * + * Optionally provide [writes] - a set of states that this change affects. + */ + public fun <T> DeviceState<T>.onNext( + vararg writes: DeviceState<*>, + alsoReads: Collection<DeviceState<*>> = emptySet(), + onChange: suspend (T) -> Unit, + ): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also { + registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes))) + } + + public fun <T> DeviceState<T>.onChange( + vararg writes: DeviceState<*>, + alsoReads: Collection<DeviceState<*>> = emptySet(), + onChange: suspend (prev: T, next: T) -> Unit, + ): Job = valueFlow.runningFold(Pair(value, value)) { pair, next -> + Pair(pair.second, next) + }.onEach { pair -> + if (pair.first != pair.second) { + onChange(pair.first, pair.second) + } + }.launchIn(this@StateContainer).also { + registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes))) + } } /** * Register a [state] in this container. The state is not registered as a device property if [this] is a [DeviceConstructor] */ public fun <T, D : DeviceState<T>> StateContainer.state(state: D): D { - registerState(StateBinding(state)) + registerState(StateNodeDescriptor(state)) return state } /** * Create a register a [MutableDeviceState] with a given [converter] */ -public fun <T> StateContainer.state(converter: MetaConverter<T>, initialValue: T): MutableDeviceState<T> = state( - DeviceState.internal(converter, initialValue) +public fun <T> StateContainer.mutableState(initialValue: T): MutableDeviceState<T> = state( + MutableDeviceState(initialValue) ) -/** - * Create a register a mutable [Double] state - */ -public fun StateContainer.doubleState(initialValue: Double): MutableDeviceState<Double> = state( - MetaConverter.double, initialValue -) - -/** - * Create a register a mutable [String] state - */ -public fun StateContainer.stringState(initialValue: String): MutableDeviceState<String> = state( - MetaConverter.string, initialValue -) +public fun <T : DeviceModel> StateContainer.model(model: T): T { + model.stateDescriptors.forEach { + registerState(it) + } + return model +} /** * Create and register a timer state. */ public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(context.request(ClockManager), tick)) + + +public fun <T, R> StateContainer.mapState( + state: DeviceState<T>, + transformation: (T) -> R, +): DeviceStateWithDependencies<R> = state(DeviceState.map(state, transformation)) + +/** + * Create a new state by combining two existing ones + */ +public fun <T1, T2, R> StateContainer.combineState( + first: DeviceState<T1>, + second: DeviceState<T2>, + transformation: (T1, T2) -> R, +): DeviceState<R> = state(DeviceState.combine(first, second, transformation)) + + +/** + * Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically + * transferred onto [targetState], but not vise versa. + * + * On resulting [Job] cancel the binding is unregistered + */ +public fun <T> StateContainer.bindTo(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job { + val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState)) + registerState(descriptor) + return sourceState.valueFlow.onEach { + targetState.value = it + }.launchIn(this).apply { + invokeOnCompletion { + unregisterState(descriptor) + } + } +} + +/** + * Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically + * transferred onto [targetState] via [transformation], but not vise versa. + * + * On resulting [Job] cancel the binding is unregistered + */ +public fun <T, R> StateContainer.transformTo( + sourceState: DeviceState<T>, + targetState: MutableDeviceState<R>, + transformation: suspend (T) -> R, +): Job { + val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState)) + registerState(descriptor) + return sourceState.valueFlow.onEach { + targetState.value = transformation(it) + }.launchIn(this).apply { + invokeOnCompletion { + unregisterState(descriptor) + } + } +} + +/** + * Register [StateDescriptor] that combines values from [sourceState1] and [sourceState2] using [transformation]. + * + * On resulting [Job] cancel the binding is unregistered + */ +public fun <T1, T2, R> StateContainer.combineTo( + sourceState1: DeviceState<T1>, + sourceState2: DeviceState<T2>, + targetState: MutableDeviceState<R>, + transformation: suspend (T1, T2) -> R, +): Job { + val descriptor = StateConnectionDescriptor(setOf(sourceState1, sourceState2), setOf(targetState)) + registerState(descriptor) + return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach { + targetState.value = it + }.launchIn(this).apply { + invokeOnCompletion { + unregisterState(descriptor) + } + } +} + +/** + * Register [StateDescriptor] that combines values from [sourceStates] using [transformation]. + * + * On resulting [Job] cancel the binding is unregistered + */ +public inline fun <reified T, R> StateContainer.combineTo( + sourceStates: Collection<DeviceState<T>>, + targetState: MutableDeviceState<R>, + noinline transformation: suspend (Array<T>) -> R, +): Job { + val descriptor = StateConnectionDescriptor(sourceStates, setOf(targetState)) + registerState(descriptor) + return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach { + targetState.value = it + }.launchIn(this).apply { + invokeOnCompletion { + unregisterState(descriptor) + } + } +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt index 8f36079..baf9646 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt @@ -7,8 +7,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.datetime.Instant import space.kscience.controls.manager.ClockManager -import space.kscience.controls.spec.instant -import space.kscience.dataforge.meta.MetaConverter import kotlin.time.Duration /** @@ -23,7 +21,6 @@ public class TimerState( public val clockManager: ClockManager, public val tick: Duration, ) : DeviceState<Instant> { - override val converter: MetaConverter<Instant> get() = MetaConverter.instant private val clock = MutableStateFlow(clockManager.clock.now()) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt index 37be770..f5c4480 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt @@ -15,7 +15,7 @@ import space.kscience.dataforge.meta.MetaConverter * A copy-free [DeviceState] bound to a device property */ private open class BoundDeviceState<T>( - override val converter: MetaConverter<T>, + val converter: MetaConverter<T>, val device: Device, val propertyName: String, initialValue: T, diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt index d1d557f..aabacfd 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt @@ -2,7 +2,6 @@ package space.kscience.controls.constructor import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import space.kscience.dataforge.meta.MetaConverter /** @@ -17,7 +16,6 @@ public class DoubleInRangeState( require(initialValue in range) { "Initial value should be in range" } } - override val converter: MetaConverter<Double> = MetaConverter.double private val _valueFlow = MutableStateFlow(initialValue) @@ -32,18 +30,18 @@ public class DoubleInRangeState( /** * A state showing that the range is on its lower boundary */ - public val atStart: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) { + public val atStart: DeviceState<Boolean> = DeviceState.map(this) { it <= range.start } /** * A state showing that the range is on its higher boundary */ - public val atEnd: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) { + public val atEnd: DeviceState<Boolean> = DeviceState.map(this) { it >= range.endInclusive } - override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)" + override fun toString(): String = "DoubleRangeState(range=$range)" } @@ -55,5 +53,5 @@ public fun StateContainer.doubleInRangeState( initialValue: Double, range: ClosedFloatingPointRange<Double>, ): DoubleInRangeState = DoubleInRangeState(initialValue, range).also { - registerState(StateBinding(it)) + registerState(StateNodeDescriptor(it)) } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt index c670a23..9b4a15f 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt @@ -4,13 +4,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import space.kscience.dataforge.meta.MetaConverter import kotlin.time.Duration private open class ExternalState<T>( val scope: CoroutineScope, - override val converter: MetaConverter<T>, val readInterval: Duration, initialValue: T, val reader: suspend () -> T, @@ -26,7 +24,7 @@ private open class ExternalState<T>( override val value: T get() = flow.value override val valueFlow: Flow<T> get() = flow - override fun toString(): String = "ExternalState(converter=$converter)" + override fun toString(): String = "ExternalState()" } /** @@ -34,20 +32,18 @@ private open class ExternalState<T>( */ public fun <T> DeviceState.Companion.external( scope: CoroutineScope, - converter: MetaConverter<T>, readInterval: Duration, initialValue: T, reader: suspend () -> T, -): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader) +): DeviceState<T> = ExternalState(scope, readInterval, initialValue, reader) private class MutableExternalState<T>( scope: CoroutineScope, - converter: MetaConverter<T>, readInterval: Duration, initialValue: T, reader: suspend () -> T, val writer: suspend (T) -> Unit, -) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> { +) : ExternalState<T>(scope, readInterval, initialValue, reader), MutableDeviceState<T> { override var value: T get() = super.value set(value) { @@ -62,9 +58,8 @@ private class MutableExternalState<T>( */ public fun <T> DeviceState.Companion.external( scope: CoroutineScope, - converter: MetaConverter<T>, readInterval: Duration, initialValue: T, reader: suspend () -> T, writer: suspend (T) -> Unit, -): MutableDeviceState<T> = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer) \ No newline at end of file +): MutableDeviceState<T> = MutableExternalState(scope, readInterval, initialValue, reader, writer) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt index 434c074..2bd9322 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt @@ -2,22 +2,19 @@ package space.kscience.controls.constructor import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import space.kscience.dataforge.meta.MetaConverter private class StateFlowAsState<T>( - override val converter: MetaConverter<T>, val flow: MutableStateFlow<T>, ) : MutableDeviceState<T> { override var value: T by flow::value override val valueFlow: Flow<T> get() = flow - override fun toString(): String = "FlowAsState(converter=$converter)" + override fun toString(): String = "FlowAsState()" } /** * Create a read-only [DeviceState] that wraps [MutableStateFlow]. * No data copy is performed. */ -public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> = - StateFlowAsState(converter, this) \ No newline at end of file +public fun <T> MutableStateFlow<T>.asDeviceState(): MutableDeviceState<T> = StateFlowAsState(this) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt index ed2d5d1..11c778d 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt @@ -2,7 +2,6 @@ package space.kscience.controls.constructor import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import space.kscience.dataforge.meta.MetaConverter /** * A [MutableDeviceState] that does not correspond to a physical state @@ -10,7 +9,6 @@ import space.kscience.dataforge.meta.MetaConverter * @param callback a synchronous callback that could be used without a scope */ private class VirtualDeviceState<T>( - override val converter: MetaConverter<T>, initialValue: T, private val callback: (T) -> Unit = {}, ) : MutableDeviceState<T> { @@ -24,7 +22,7 @@ private class VirtualDeviceState<T>( callback(value) } - override fun toString(): String = "VirtualDeviceState(converter=$converter)" + override fun toString(): String = "VirtualDeviceState()" } @@ -33,8 +31,7 @@ private class VirtualDeviceState<T>( * * @param callback a synchronous callback that could be used without a scope */ -public fun <T> DeviceState.Companion.internal( - converter: MetaConverter<T>, +public fun <T> MutableDeviceState( initialValue: T, callback: (T) -> Unit = {}, -): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback) +): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback) 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 e2f6331..4aef3ff 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 @@ -22,6 +22,7 @@ public interface DeviceHub : Provider { } else { emptyMap() } + //TODO send message on device change public companion object } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt similarity index 62% rename from controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt index 89a28da..4297d20 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.spec +package space.kscience.controls.misc import kotlinx.datetime.Instant import space.kscience.dataforge.meta.* @@ -12,9 +12,9 @@ public fun Double.asMeta(): Meta = Meta(asValue()) * Generate a nullable [MetaConverter] from non-nullable one */ public fun <T : Any> MetaConverter<T>.nullable(): MetaConverter<T?> = object : MetaConverter<T?> { - override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) }?: Meta(Null) + override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) } ?: Meta(Null) - override fun readOrNull(source: Meta): T? = if(source.value == Null) null else this@nullable.readOrNull(source) + override fun readOrNull(source: Meta): T? = if (source.value == Null) null else this@nullable.readOrNull(source) } @@ -38,4 +38,16 @@ private object InstantConverter : MetaConverter<Instant> { override fun convert(obj: Instant): Meta = Meta(obj.toString()) } -public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter \ No newline at end of file +public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter + +private object DoubleRangeConverter : MetaConverter<ClosedFloatingPointRange<Double>> { + override fun readOrNull(source: Meta): ClosedFloatingPointRange<Double>? = source.value?.doubleArray?.let { (start, end)-> + start..end + } + + override fun convert( + obj: ClosedFloatingPointRange<Double>, + ): Meta = Meta(doubleArrayOf(obj.start, obj.endInclusive).asValue()) +} + +public val MetaConverter.Companion.doubleRange: MetaConverter<ClosedFloatingPointRange<Double>> get() = DoubleRangeConverter \ No newline at end of file diff --git a/controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt b/controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt index 719738a..269b140 100644 --- a/controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt +++ b/controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt @@ -1,9 +1,8 @@ package space.kscience.controls.api -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import space.kscience.controls.spec.asMeta +import space.kscience.controls.misc.asMeta import kotlin.test.Test import kotlin.test.assertEquals 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 859bce2..f4a232e 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 @@ -16,8 +16,6 @@ import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.length -import space.kscience.dataforge.names.startsWith import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.send import space.kscience.magix.api.subscribe @@ -121,12 +119,18 @@ public suspend fun MagixEndpoint.remoteDevice( deviceEndpoint: String, deviceName: Name, ): DeviceClient = coroutineScope { - val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)).map { it.second } + val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)) + .map { it.second } + .filter { + it.sourceDevice == null || it.sourceDevice == deviceName + } val deferredDescriptorMessage = CompletableDeferred<DescriptionMessage>() launch { - deferredDescriptorMessage.complete(subscription.filterIsInstance<DescriptionMessage>().first()) + deferredDescriptorMessage.complete( + subscription.filterIsInstance<DescriptionMessage>().first() + ) } send( @@ -157,12 +161,6 @@ public suspend fun MagixEndpoint.remoteDevice( } } -private class MapBasedDeviceHub(val deviceMap: Map<Name, Device>, val prefix: Name) : DeviceHub { - override val devices: Map<Name, Device> - get() = deviceMap.filterKeys { name: Name -> name == prefix || (name.startsWith(prefix) && name.length == prefix.length + 1) } - -} - /** * Create a dynamic [DeviceHub] from incoming messages */ 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 5fe99c3..2ffad2a 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 @@ -1,6 +1,7 @@ package space.kscience.controls.client import com.benasher44.uuid.uuid4 +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn @@ -56,7 +57,7 @@ public fun DeviceManager.launchMagixService( ) } }.catch { error -> - logger.error(error) { "Error while responding to message: ${error.message}" } + if (error !is CancellationException) logger.error(error) { "Error while responding to message: ${error.message}" } }.launchIn(this) hubMessageFlow().onEach { payload -> diff --git a/controls-vision/src/commonMain/kotlin/BooleanIndicatorVision.kt b/controls-vision/src/commonMain/kotlin/BooleanIndicatorVision.kt deleted file mode 100644 index 8783228..0000000 --- a/controls-vision/src/commonMain/kotlin/BooleanIndicatorVision.kt +++ /dev/null @@ -1,24 +0,0 @@ -package space.kscience.controls.vision - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import space.kscience.dataforge.meta.boolean -import space.kscience.visionforge.AbstractVision -import space.kscience.visionforge.Vision -import space.kscience.visionforge.html.VisionOfHtml - -/** - * A [Vision] that shows an indicator - */ -@Serializable -@SerialName("controls.indicator") -public class BooleanIndicatorVision : AbstractVision(), VisionOfHtml { - public val isOn: Boolean by properties.boolean(false) -} - -///** -// * 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/commonMain/kotlin/ControlVisionPlugin.kt b/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt index 6395d53..d9ec746 100644 --- a/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt +++ b/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt @@ -13,6 +13,7 @@ public expect class ControlVisionPlugin: VisionPlugin{ internal val controlsVisionSerializersModule = SerializersModule { polymorphic(Vision::class) { - subclass(BooleanIndicatorVision.serializer()) + subclass(IndicatorVision.serializer()) + subclass(SliderVision.serializer()) } } \ No newline at end of file diff --git a/controls-vision/src/commonMain/kotlin/controlsVisions.kt b/controls-vision/src/commonMain/kotlin/controlsVisions.kt new file mode 100644 index 0000000..1b633ed --- /dev/null +++ b/controls-vision/src/commonMain/kotlin/controlsVisions.kt @@ -0,0 +1,35 @@ +package space.kscience.controls.vision + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import space.kscience.controls.misc.doubleRange +import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.meta.convertable +import space.kscience.dataforge.meta.double +import space.kscience.dataforge.meta.string +import space.kscience.visionforge.AbstractControlVision +import space.kscience.visionforge.AbstractVision +import space.kscience.visionforge.Vision + +/** + * A [Vision] that shows a colored indicator + */ +@Serializable +@SerialName("controls.indicator") +public class IndicatorVision : AbstractVision() { + public val color: String? by properties.string() +} + +@Serializable +@SerialName("controls.slider") +public class SliderVision : AbstractControlVision() { + public var position: Double? by properties.double() + public var range: ClosedFloatingPointRange<Double>? by properties.convertable(MetaConverter.doubleRange) +} + +///** +// * 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/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index 88b870e..8eeb9a4 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -145,7 +145,7 @@ private fun <T> Trace.updateFromState( public fun <T> Plot.plotDeviceState( context: Context, state: DeviceState<T>, - extractValue: T.() -> Value = { state.converter.convert(this).value ?: Null }, + extractValue: (T) -> Value = { Value.of(it) }, 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 d08772c..b966791 100644 --- a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt +++ b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt @@ -10,7 +10,34 @@ import space.kscience.dataforge.names.asName import space.kscience.visionforge.VisionPlugin import space.kscience.visionforge.html.ElementVisionRenderer -private val indicatorRenderer = ElementVisionRenderer<BooleanIndicatorVision> { name, vision: BooleanIndicatorVision, meta -> + +private val indicatorRenderer = ElementVisionRenderer<IndicatorVision> { name, vision: IndicatorVision, meta -> +// val ledSize = vision.properties["size"].int ?: 15 +// val color = vision.color ?: "LightGray" +// div("controls-indicator") { +// style = """ +// +// @keyframes blink { +// 0% { box-shadow: 0 0 10px; } +// 50% { box-shadow: 0 0 30px; } +// 100% { box-shadow: 0 0 10px; } +// } +// +// display: inline-block; +// margin: ${ledSize}px; +// width: ${ledSize}px; +// height: ${ledSize}px; +// border-radius: 50%; +// +// background: $color; +// border: 1px solid darken($color,5%); +// color: $color; +// animation: blink 3s infinite; +// """.trimIndent() +// } +} + +private val sliderRenderer = ElementVisionRenderer<SliderVision> { name, vision: SliderVision, meta -> } @@ -22,7 +49,8 @@ public actual class ControlVisionPlugin : VisionPlugin() { override fun content(target: String): Map<Name, Any> = when (target) { ElementVisionRenderer.TYPE -> mapOf( - "indicator".asName() to indicatorRenderer + "indicator".asName() to indicatorRenderer, + "slider".asName() to sliderRenderer ) else -> super.content(target) 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 e76cfe0..a598d4b 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 @@ -64,7 +64,7 @@ class VirtualCarController : Controller(), ContextAware { //mongoStorageJob = deviceManager.storeMessages(DefaultAsynchronousMongoClientFactory) //Launch device client and connect it to the server val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost") - deviceManager.launchMagixService(deviceEndpoint) + deviceManager.launchMagixService(deviceEndpoint, "car") } } diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index 6909639..8628dd1 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.compose.ExperimentalComposeLibrary import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode @@ -11,12 +12,15 @@ kscience { withJava() } useKtor() + useSerialization() useContextReceivers() - dependencies { - api(projects.controlsVision) + commonMain { + implementation(projects.controlsVision) + implementation(projects.controlsConstructor) +// implementation("io.github.koalaplot:koalaplot-core:0.6.0") } jvmMain { - implementation("io.ktor:ktor-server-cio") +// implementation("io.ktor:ktor-server-cio") implementation(spclibs.logback.classic) } } @@ -26,6 +30,8 @@ kotlin { jvmMain { dependencies { implementation(compose.desktop.currentOs) + @OptIn(ExperimentalComposeLibrary::class) + implementation(compose.desktop.components.splitPane) } } } diff --git a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt new file mode 100644 index 0000000..bb15c49 --- /dev/null +++ b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt @@ -0,0 +1,185 @@ +package space.kscience.controls.demo.constructor + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import kotlinx.serialization.Serializable +import space.kscience.controls.constructor.* +import space.kscience.dataforge.context.Context +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.math.pow +import kotlin.math.sqrt +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit + +@Serializable +data class XY(val x: Double, val y: Double) { + companion object { + val ZERO = XY(0.0, 0.0) + } +} + +operator fun XY.plus(other: XY): XY = XY(x + other.x, y + other.y) +operator fun XY.times(c: Double): XY = XY(x * c, y * c) +operator fun XY.div(c: Double): XY = XY(x / c, y / c) +// +//class XYPosition(context: Context, x0: Double, y0: Double) : DeviceModel(context) { +// val x: MutableDeviceState<Double> = mutableState(x0) +// val y: MutableDeviceState<Double> = mutableState(y0) +// +// val xy = combineState(x, y) { x, y -> XY(x, y) } +//} + +class Spring( + context: Context, + val k: Double, + val l0: Double, + val begin: DeviceState<XY>, + val end: DeviceState<XY>, +) : DeviceConstructor(context) { + + val length = combineState(begin, end) { begin, end -> + sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).pow(2)) + } + + val tension: DeviceState<Double> = mapState(length) { l -> + val delta = l - l0 + k * delta + } + + /** + * direction from start to end + */ + val direction = combineState(begin, end) { begin, end -> + val dx = end.x - begin.x + val dy = end.y - begin.y + val l = sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).pow(2)) + XY(dx / l, dy / l) + } + + val beginForce = combineState(direction, tension) { direction: XY, tension: Double -> + direction * (tension) + } + + + val endForce = combineState(direction, tension) { direction: XY, tension: Double -> + direction * (-tension) + } +} + +class MaterialPoint( + context: Context, + val mass: Double, + val force: DeviceState<XY>, + val position: MutableDeviceState<XY>, + val velocity: MutableDeviceState<XY> = MutableDeviceState(XY.ZERO), +) : DeviceModel(context, force) { + + private val timer: TimerState = timer(2.milliseconds) + + private val movement = timer.onChange( + position, velocity, + alsoReads = setOf(force, velocity, position) + ) { prev, next -> + val dt = (next - prev).toDouble(DurationUnit.SECONDS) + val a = force.value / mass + position.value += a * (dt * dt / 2) + velocity.value * dt + velocity.value += a * dt + } +} + + +class BodyOnSprings( + context: Context, + mass: Double, + k: Double, + startPosition: XY, + l0: Double = 1.0, + val xLeft: Double = 0.0, + val xRight: Double = 2.0, + val yBottom: Double = 0.0, + val yTop: Double = 2.0, +) : DeviceConstructor(context) { + + val width = xRight - xLeft + val height = yTop - yBottom + + val position = mutableState(startPosition) + + private val leftAnchor = mutableState(XY(xLeft, yTop + yBottom / 2)) + + val leftSpring by device( + Spring(context, k, l0, leftAnchor, position) + ) + + private val rightAnchor = mutableState(XY(xRight, yTop + yBottom / 2)) + + val rightSpring by device( + Spring(context, k, l0, rightAnchor, position) + ) + + val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, rignt -> + left + rignt + } + + + val body = model( + MaterialPoint( + context = context, + mass = mass, + force = force, + position = position + ) + ) +} + +@Composable +fun <T> DeviceState<T>.collect( + coroutineContext: CoroutineContext = EmptyCoroutineContext, +): State<T> = valueFlow.collectAsState(value, coroutineContext) + +fun main() = application { + val initialState = XY(1.1, 1.1) + + Window(title = "Ball on springs", onCloseRequest = ::exitApplication) { + MaterialTheme { + val context = remember { + Context("simulation") + } + + val model = remember { + BodyOnSprings(context, 100.0, 1000.0, initialState) + } + + val position: XY by model.body.position.collect() + Box(Modifier.size(400.dp)) { + Canvas(modifier = Modifier.fillMaxSize()) { + fun XY.toOffset() = Offset( + (x / model.width * size.width).toFloat(), + (y / model.height * size.height).toFloat() + ) + + drawCircle( + Color.Red, 10f, center = position.toOffset() + ) + drawLine(Color.Blue, model.leftSpring.begin.value.toOffset(), model.leftSpring.end.value.toOffset()) + drawLine( + Color.Blue, + model.rightSpring.begin.value.toOffset(), + model.rightSpring.end.value.toOffset() + ) + } + } + } + } +} \ No newline at end of file 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 106b077..b0282d0 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 @@ -12,6 +12,8 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeout import space.kscience.controls.api.DeviceHub import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.misc.asMeta +import space.kscience.controls.misc.duration import space.kscience.controls.ports.AsynchronousPort import space.kscience.controls.ports.KtorTcpPort import space.kscience.controls.ports.send diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 601b9d1..617933a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -79,6 +79,7 @@ visionforge-jupiter = { module = "space.kscience:visionforge-jupyter", version.r visionforge-plotly = { module = "space.kscience:visionforge-plotly", version.ref = "visionforge" } visionforge-markdown = { module = "space.kscience:visionforge-markdown", version.ref = "visionforge" } visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" } +visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" } # Buildscript From f72d7aa3fa46b2b45e5ccbe508568cc3563af4e0 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Tue, 28 May 2024 09:51:30 +0300 Subject: [PATCH 091/125] Synchronous port response consumeAsSource --- CHANGELOG.md | 1 + .../controls/ports/AsynchronousPort.kt | 23 +++++------------- .../controls/ports/SynchronousPort.kt | 24 ++++++++++++++++--- .../kscience/controls/ports/ioExtensions.kt | 21 +++++++++++++++- 4 files changed, 48 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a1dad9..92b2765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Refactored ports. Now we have `AsynchronousPort` as well as `SynchronousPort` - `DeviceClient` now initializes property and action descriptors eagerly. - `DeviceHub` now works with `Name` instead of `NameToken`. Tree-like structure is made using `Path`. Device messages no longer have access to sub-devices. +- Add some utility methods to ports. Synchronous port response could be now consumed as `Source`. ### Deprecated 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 index 3f89e75..9b37fd3 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/AsynchronousPort.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/AsynchronousPort.kt @@ -3,10 +3,7 @@ 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.* @@ -26,15 +23,7 @@ public interface AsynchronousPort : ContextAware, AsynchronousSocket<ByteArray> * [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 -} +public fun AsynchronousPort.receiveAsSource(scope: CoroutineScope): Source = subscribe().consumeAsSource(scope) /** @@ -51,13 +40,13 @@ public abstract class AbstractAsynchronousPort( CoroutineScope( coroutineContext + SupervisorJob(coroutineContext[Job]) + - CoroutineExceptionHandler { _, throwable -> logger.error(throwable) { throwable.stackTraceToString() } } + + CoroutineExceptionHandler { _, throwable -> logger.error(throwable) { "Asynchronous port error: " + throwable.stackTraceToString() } } + CoroutineName(toString()) ) } - private val outgoing = Channel<ByteArray>(meta["outgoing.capacity"].int?:100) - private val incoming = Channel<ByteArray>(meta["incoming.capacity"].int?:100) + private val outgoing = Channel<ByteArray>(meta["outgoing.capacity"].int ?: 100) + private val incoming = Channel<ByteArray>(meta["incoming.capacity"].int ?: 100) /** * Internal method to synchronously send data @@ -100,7 +89,7 @@ public abstract class AbstractAsynchronousPort( * Send a data packet via the port */ override suspend fun send(data: ByteArray) { - check(isOpen){"The port is not opened"} + check(isOpen) { "The port is not opened" } outgoing.send(data) } @@ -117,7 +106,7 @@ public abstract class AbstractAsynchronousPort( sendJob?.cancel() } - override fun toString(): String = meta["name"].string?:"ChannelPort[${hashCode().toString(16)}]" + override fun toString(): String = meta["name"].string ?: "ChannelPort[${hashCode().toString(16)}]" } /** 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 49c2b06..ac368c5 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 @@ -1,11 +1,11 @@ package space.kscience.controls.ports -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.io.Buffer +import kotlinx.io.Source import kotlinx.io.readByteArray import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.ContextAware @@ -46,6 +46,24 @@ public interface SynchronousPort : ContextAware, AutoCloseable { } } +/** + * Read response to a given message using [Source] abstraction + */ +public suspend fun <R> SynchronousPort.respondAsSource( + request: ByteArray, + transform: suspend Source.() -> R, +): R = respond(request) { + //suspend until the response is fully read + coroutineScope { + val buffer = Buffer() + val collectJob = onEach { buffer.write(it) }.launchIn(this) + val res = transform(buffer) + //cancel collection when the result is achieved + collectJob.cancel() + res + } +} + private class SynchronousOverAsynchronousPort( val port: AsynchronousPort, val mutex: Mutex, 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 index dbe7256..5d7d3bf 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/ioExtensions.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/ioExtensions.kt @@ -1,5 +1,24 @@ package space.kscience.controls.ports +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.io.Buffer +import kotlinx.io.Source import space.kscience.dataforge.io.Binary -public fun Binary.readShort(position: Int): Short = read(position) { readShort() } \ No newline at end of file +public fun Binary.readShort(position: Int): Short = read(position) { readShort() } + +/** + * Consume given flow of [ByteArray] as [Source]. The subscription is canceled when [scope] is closed. + */ +public fun Flow<ByteArray>.consumeAsSource(scope: CoroutineScope): Source { + val buffer = Buffer() + //subscription is canceled when the scope is canceled + onEach { + buffer.write(it) + }.launchIn(scope) + + return buffer +} \ No newline at end of file From 9edde7bdbd47688f208cb9f2a8fbe73519a8261a Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Wed, 29 May 2024 22:20:22 +0300 Subject: [PATCH 092/125] Major constructor refactoring --- build.gradle.kts | 3 + controls-constructor/build.gradle.kts | 1 + ...ateDescriptor.kt => ConstructorElement.kt} | 93 +++-- .../controls/constructor/ConstructorModel.kt | 33 ++ .../controls/constructor/DeviceConstructor.kt | 40 +- .../controls/constructor/DeviceModel.kt | 32 -- .../controls/constructor/DeviceState.kt | 6 + .../controls/constructor/boundState.kt | 4 +- .../controls/constructor/exoticState.kt | 2 +- .../controls/constructor/library/Converter.kt | 20 + .../controls/constructor/library/Drive.kt | 4 +- .../constructor/library/LimitSwitch.kt | 26 +- .../constructor/library/PidRegulator.kt | 30 +- .../controls/constructor/library/Regulator.kt | 4 +- .../constructor/units/NumericalValue.kt | 10 + .../constructor/units/UnitsOfMeasurement.kt | 30 ++ .../kscience/controls/manager/ClockManager.kt | 3 +- .../kscience/controls/spec/DeviceSpec.kt | 19 +- .../kscience/controls/client/MagixLoopTest.kt | 59 ++- .../controls/opcua/client/OpcUaDevice.kt | 2 +- ...otExtensions.kt => koalaPlotExtensions.kt} | 0 .../build.gradle.kts | 45 +++ .../src/commonMain/kotlin/TimeAxisModel.kt | 45 +++ .../src/commonMain/kotlin/composeState.kt | 31 ++ .../src/commonMain/kotlin/indicators.kt | 2 + .../src/commonMain/kotlin/koalaPlots.kt | 230 +++++++++++ .../src/commonMain/kotlin/sliders.kt | 51 +++ demo/constructor/build.gradle.kts | 10 +- .../src/jvmMain/kotlin/BodyOnSprings.kt | 92 +++-- .../src/jvmMain/kotlin/LinearDrive.kt | 372 ++++++++++-------- .../constructor/src/jvmMain/kotlin/Plotter.kt | 2 + settings.gradle.kts | 1 + 32 files changed, 900 insertions(+), 402 deletions(-) rename controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/{StateDescriptor.kt => ConstructorElement.kt} (63%) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorModel.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Converter.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt rename controls-vision/src/commonMain/kotlin/{plotExtensions.kt => koalaPlotExtensions.kt} (100%) create mode 100644 controls-visualisation-compose/build.gradle.kts create mode 100644 controls-visualisation-compose/src/commonMain/kotlin/TimeAxisModel.kt create mode 100644 controls-visualisation-compose/src/commonMain/kotlin/composeState.kt create mode 100644 controls-visualisation-compose/src/commonMain/kotlin/indicators.kt create mode 100644 controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt create mode 100644 controls-visualisation-compose/src/commonMain/kotlin/sliders.kt create mode 100644 demo/constructor/src/jvmMain/kotlin/Plotter.kt diff --git a/build.gradle.kts b/build.gradle.kts index 50a61d8..773272c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,9 @@ plugins { allprojects { group = "space.kscience" version = "0.4.0-dev-4" + repositories{ + google() + } } ksciencePublish { diff --git a/controls-constructor/build.gradle.kts b/controls-constructor/build.gradle.kts index e69c4d4..008a242 100644 --- a/controls-constructor/build.gradle.kts +++ b/controls-constructor/build.gradle.kts @@ -11,6 +11,7 @@ kscience{ jvm() js() useCoroutines() + useSerialization() commonMain { api(projects.controlsCore) } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt similarity index 63% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt index 30a0299..911a682 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt @@ -2,9 +2,7 @@ package space.kscience.controls.constructor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.flow.* import space.kscience.controls.api.Device import space.kscience.controls.manager.ClockManager import space.kscience.dataforge.context.ContextAware @@ -14,34 +12,38 @@ import kotlin.time.Duration /** * A binding that is used to describe device functionality */ -public sealed interface StateDescriptor +public sealed interface ConstructorElement /** * A binding that exposes device property as read-only state */ -public class StatePropertyDescriptor<T>( +public class PropertyConstructorElement<T>( public val device: Device, public val propertyName: String, public val state: DeviceState<T>, -) : StateDescriptor +) : ConstructorElement /** * A binding for independent state like a timer */ -public class StateNodeDescriptor<T>( +public class StateConstructorElement<T>( public val state: DeviceState<T>, -) : StateDescriptor +) : ConstructorElement -public class StateConnectionDescriptor( +public class ConnectionConstrucorElement( public val reads: Collection<DeviceState<*>>, public val writes: Collection<DeviceState<*>>, -) : StateDescriptor +) : ConstructorElement + +public class ModelConstructorElement( + public val model: ConstructorModel +) : ConstructorElement public interface StateContainer : ContextAware, CoroutineScope { - public val stateDescriptors: Set<StateDescriptor> - public fun registerState(stateDescriptor: StateDescriptor) - public fun unregisterState(stateDescriptor: StateDescriptor) + public val constructorElements: Set<ConstructorElement> + public fun registerElement(constructorElement: ConstructorElement) + public fun unregisterElement(constructorElement: ConstructorElement) /** @@ -50,16 +52,16 @@ public interface StateContainer : ContextAware, CoroutineScope { * Optionally provide [writes] - a set of states that this change affects. */ public fun <T> DeviceState<T>.onNext( - vararg writes: DeviceState<*>, - alsoReads: Collection<DeviceState<*>> = emptySet(), + writes: Collection<DeviceState<*>> = emptySet(), + reads: Collection<DeviceState<*>> = emptySet(), onChange: suspend (T) -> Unit, ): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also { - registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes))) + registerElement(ConnectionConstrucorElement(reads + this, writes)) } public fun <T> DeviceState<T>.onChange( - vararg writes: DeviceState<*>, - alsoReads: Collection<DeviceState<*>> = emptySet(), + writes: Collection<DeviceState<*>> = emptySet(), + reads: Collection<DeviceState<*>> = emptySet(), onChange: suspend (prev: T, next: T) -> Unit, ): Job = valueFlow.runningFold(Pair(value, value)) { pair, next -> Pair(pair.second, next) @@ -68,7 +70,7 @@ public interface StateContainer : ContextAware, CoroutineScope { onChange(pair.first, pair.second) } }.launchIn(this@StateContainer).also { - registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes))) + registerElement(ConnectionConstrucorElement(reads + this, writes)) } } @@ -76,21 +78,19 @@ public interface StateContainer : ContextAware, CoroutineScope { * Register a [state] in this container. The state is not registered as a device property if [this] is a [DeviceConstructor] */ public fun <T, D : DeviceState<T>> StateContainer.state(state: D): D { - registerState(StateNodeDescriptor(state)) + registerElement(StateConstructorElement(state)) return state } /** * Create a register a [MutableDeviceState] with a given [converter] */ -public fun <T> StateContainer.mutableState(initialValue: T): MutableDeviceState<T> = state( +public fun <T> StateContainer.stateOf(initialValue: T): MutableDeviceState<T> = state( MutableDeviceState(initialValue) ) -public fun <T : DeviceModel> StateContainer.model(model: T): T { - model.stateDescriptors.forEach { - registerState(it) - } +public fun <T : ConstructorModel> StateContainer.model(model: T): T { + registerElement(ModelConstructorElement(model)) return model } @@ -101,9 +101,20 @@ public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(c public fun <T, R> StateContainer.mapState( - state: DeviceState<T>, + origin: DeviceState<T>, transformation: (T) -> R, -): DeviceStateWithDependencies<R> = state(DeviceState.map(state, transformation)) +): DeviceStateWithDependencies<R> = state(DeviceState.map(origin, transformation)) + + +public fun <T, R> StateContainer.flowState( + origin: DeviceState<T>, + initialValue: R, + transformation: suspend FlowCollector<R>.(T) -> Unit +): DeviceStateWithDependencies<R> { + val state = MutableDeviceState(initialValue) + origin.valueFlow.transform(transformation).onEach { state.value = it }.launchIn(this) + return state(state.withDependencies(setOf(origin))) +} /** * Create a new state by combining two existing ones @@ -122,13 +133,13 @@ public fun <T1, T2, R> StateContainer.combineState( * On resulting [Job] cancel the binding is unregistered */ public fun <T> StateContainer.bindTo(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job { - val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState)) - registerState(descriptor) + val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState)) + registerElement(descriptor) return sourceState.valueFlow.onEach { targetState.value = it }.launchIn(this).apply { invokeOnCompletion { - unregisterState(descriptor) + unregisterElement(descriptor) } } } @@ -144,19 +155,19 @@ public fun <T, R> StateContainer.transformTo( targetState: MutableDeviceState<R>, transformation: suspend (T) -> R, ): Job { - val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState)) - registerState(descriptor) + val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState)) + registerElement(descriptor) return sourceState.valueFlow.onEach { targetState.value = transformation(it) }.launchIn(this).apply { invokeOnCompletion { - unregisterState(descriptor) + unregisterElement(descriptor) } } } /** - * Register [StateDescriptor] that combines values from [sourceState1] and [sourceState2] using [transformation]. + * Register [ConstructorElement] that combines values from [sourceState1] and [sourceState2] using [transformation]. * * On resulting [Job] cancel the binding is unregistered */ @@ -166,19 +177,19 @@ public fun <T1, T2, R> StateContainer.combineTo( targetState: MutableDeviceState<R>, transformation: suspend (T1, T2) -> R, ): Job { - val descriptor = StateConnectionDescriptor(setOf(sourceState1, sourceState2), setOf(targetState)) - registerState(descriptor) + val descriptor = ConnectionConstrucorElement(setOf(sourceState1, sourceState2), setOf(targetState)) + registerElement(descriptor) return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach { targetState.value = it }.launchIn(this).apply { invokeOnCompletion { - unregisterState(descriptor) + unregisterElement(descriptor) } } } /** - * Register [StateDescriptor] that combines values from [sourceStates] using [transformation]. + * Register [ConstructorElement] that combines values from [sourceStates] using [transformation]. * * On resulting [Job] cancel the binding is unregistered */ @@ -187,13 +198,13 @@ public inline fun <reified T, R> StateContainer.combineTo( targetState: MutableDeviceState<R>, noinline transformation: suspend (Array<T>) -> R, ): Job { - val descriptor = StateConnectionDescriptor(sourceStates, setOf(targetState)) - registerState(descriptor) + val descriptor = ConnectionConstrucorElement(sourceStates, setOf(targetState)) + registerElement(descriptor) return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach { targetState.value = it }.launchIn(this).apply { invokeOnCompletion { - unregisterState(descriptor) + unregisterElement(descriptor) } } } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorModel.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorModel.kt new file mode 100644 index 0000000..35a1e31 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorModel.kt @@ -0,0 +1,33 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.newCoroutineContext +import space.kscience.dataforge.context.Context +import kotlin.coroutines.CoroutineContext + +public abstract class ConstructorModel( + final override val context: Context, + vararg dependencies: DeviceState<*>, +) : StateContainer, CoroutineScope { + + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob()) + + + private val _constructorElements: MutableSet<ConstructorElement> = mutableSetOf<ConstructorElement>().apply { + dependencies.forEach { + add(StateConstructorElement(it)) + } + } + + override val constructorElements: Set<ConstructorElement> get() = _constructorElements + + override fun registerElement(constructorElement: ConstructorElement) { + _constructorElements.add(constructorElement) + } + + override fun unregisterElement(constructorElement: ConstructorElement) { + _constructorElements.remove(constructorElement) + } +} \ No newline at end of file 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 17294f1..8854247 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 @@ -3,7 +3,6 @@ package space.kscience.controls.constructor import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.spec.DevicePropertySpec -import space.kscience.controls.spec.MutableDevicePropertySpec import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta @@ -22,15 +21,15 @@ public abstract class DeviceConstructor( context: Context, meta: Meta = Meta.EMPTY, ) : DeviceGroup(context, meta), StateContainer { - private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf() - override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors + private val _constructorElements: MutableSet<ConstructorElement> = mutableSetOf() + override val constructorElements: Set<ConstructorElement> get() = _constructorElements - override fun registerState(stateDescriptor: StateDescriptor) { - _stateDescriptors.add(stateDescriptor) + override fun registerElement(constructorElement: ConstructorElement) { + _constructorElements.add(constructorElement) } - override fun unregisterState(stateDescriptor: StateDescriptor) { - _stateDescriptors.remove(stateDescriptor) + override fun unregisterElement(constructorElement: ConstructorElement) { + _constructorElements.remove(constructorElement) } override fun <T> registerProperty( @@ -39,7 +38,7 @@ public abstract class DeviceConstructor( state: DeviceState<T>, ) { super.registerProperty(converter, descriptor, state) - registerState(StatePropertyDescriptor(this, descriptor.name, state)) + registerElement(PropertyConstructorElement(this, descriptor.name, state)) } } @@ -108,7 +107,7 @@ public fun <T : Any> DeviceConstructor.property( ) /** - * Register a mutable external state as a property + * Create and register a mutable external state as a property */ public fun <T : Any> DeviceConstructor.mutableProperty( metaConverter: MetaConverter<T>, @@ -141,22 +140,7 @@ public fun <T> DeviceConstructor.virtualProperty( nameOverride, ) -/** - * Bind existing property provided by specification to this device - */ -public fun <T, D : Device> DeviceConstructor.deviceProperty( - device: D, - property: DevicePropertySpec<D, T>, - initialValue: T, -): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = - property(property.converter, device.propertyAsState(property, initialValue)) - -/** - * Bind existing property provided by specification to this device - */ -public fun <T, D : Device> DeviceConstructor.deviceProperty( - device: D, - property: MutableDevicePropertySpec<D, T>, - initialValue: T, -): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = - property(property.converter, device.mutablePropertyAsState(property, initialValue)) +public fun <T, S : DeviceState<T>> DeviceConstructor.property( + spec: DevicePropertySpec<*, T>, + state: S, +): Unit = registerProperty(spec.converter, spec.descriptor, state) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt deleted file mode 100644 index 484b471..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt +++ /dev/null @@ -1,32 +0,0 @@ -package space.kscience.controls.constructor - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.newCoroutineContext -import space.kscience.dataforge.context.Context -import kotlin.coroutines.CoroutineContext - -public abstract class DeviceModel( - final override val context: Context, - vararg dependencies: DeviceState<*>, -) : StateContainer, CoroutineScope { - - override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob()) - - - private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf<StateDescriptor>().apply { - dependencies.forEach { - add(StateNodeDescriptor(it)) - } - } - - override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors - - override fun registerState(stateDescriptor: StateDescriptor) { - _stateDescriptors.add(stateDescriptor) - } - - override fun unregisterState(stateDescriptor: StateDescriptor) { - _stateDescriptors.remove(stateDescriptor) - } -} \ 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 e74c4c0..db388d3 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 @@ -48,6 +48,12 @@ public interface DeviceStateWithDependencies<T> : DeviceState<T> { public val dependencies: Collection<DeviceState<*>> } +public fun <T> DeviceState<T>.withDependencies( + dependencies: Collection<DeviceState<*>> +): DeviceStateWithDependencies<T> = object : DeviceStateWithDependencies<T>, DeviceState<T> by this { + override val dependencies: Collection<DeviceState<*>> = dependencies +} + /** * Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper]. */ diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt index f5c4480..1c128fa 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt @@ -92,11 +92,11 @@ public suspend fun <T> Device.mutablePropertyAsState( return mutablePropertyAsState(propertyName, metaConverter, initialValue) } -public suspend fun <D : Device, T> D.mutablePropertyAsState( +public suspend fun <D : Device, T> D.propertyAsState( propertySpec: MutableDevicePropertySpec<D, T>, ): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter) -public fun <D : Device, T> D.mutablePropertyAsState( +public fun <D : Device, T> D.propertyAsState( propertySpec: MutableDevicePropertySpec<D, T>, initialValue: T, ): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt index aabacfd..96ea4fe 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt @@ -53,5 +53,5 @@ public fun StateContainer.doubleInRangeState( initialValue: Double, range: ClosedFloatingPointRange<Double>, ): DoubleInRangeState = DoubleInRangeState(initialValue, range).also { - registerState(StateNodeDescriptor(it)) + registerElement(StateConstructorElement(it)) } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Converter.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Converter.kt new file mode 100644 index 0000000..45cd4db --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Converter.kt @@ -0,0 +1,20 @@ +package space.kscience.controls.constructor.library + +import kotlinx.coroutines.flow.FlowCollector +import space.kscience.controls.constructor.DeviceConstructor +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.DeviceStateWithDependencies +import space.kscience.controls.constructor.flowState +import space.kscience.dataforge.context.Context + +/** + * A device that converts one type of physical quantity to another type + */ +public class Converter<T, R>( + context: Context, + input: DeviceState<T>, + initialValue: R, + transform: suspend FlowCollector<R>.(T) -> Unit, +) : DeviceConstructor(context) { + public val output: DeviceStateWithDependencies<R> = flowState(input, initialValue, transform) +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt index 5678b34..6a07db0 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import space.kscience.controls.api.Device import space.kscience.controls.constructor.MutableDeviceState -import space.kscience.controls.constructor.mutablePropertyAsState +import space.kscience.controls.constructor.propertyAsState import space.kscience.controls.manager.clock import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context @@ -98,4 +98,4 @@ public class VirtualDrive( } } -public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = mutablePropertyAsState(Drive.force) +public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = propertyAsState(Drive.force) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt index af6436f..876d2dc 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt @@ -1,15 +1,14 @@ package space.kscience.controls.constructor.library -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import space.kscience.controls.api.Device +import space.kscience.controls.constructor.DeviceConstructor import space.kscience.controls.constructor.DeviceState -import space.kscience.controls.spec.DeviceBySpec +import space.kscience.controls.constructor.property 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.context.Factory +import space.kscience.dataforge.meta.MetaConverter /** @@ -17,13 +16,10 @@ import space.kscience.dataforge.context.Factory */ public interface LimitSwitch : Device { - public val locked: Boolean + public fun isLocked(): Boolean public companion object : DeviceSpec<LimitSwitch>() { - public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked } - public operator fun invoke(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ -> - VirtualLimitSwitch(context, lockedState) - } + public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { isLocked() } } } @@ -32,14 +28,10 @@ public interface LimitSwitch : Device { */ public class VirtualLimitSwitch( context: Context, - public val lockedState: DeviceState<Boolean>, -) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch { + locked: DeviceState<Boolean>, +) : DeviceConstructor(context), LimitSwitch { - override suspend fun onStart() { - lockedState.valueFlow.onEach { - propertyChanged(LimitSwitch.locked, it) - }.launchIn(this) - } + public val locked: DeviceState<Boolean> by property(MetaConverter.boolean, locked) - override val locked: Boolean get() = lockedState.value + override fun isLocked(): Boolean = locked.value } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt index afb2cbd..9edf91e 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt @@ -16,32 +16,22 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit + /** * Pid regulator parameters */ -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) - +public data class PidParameters( + val kp: Double, + val ki: Double, + val kd: Double, + val timeStep: Duration = 1.milliseconds, +) /** * A drive with PID regulator */ public class PidRegulator( public val drive: Drive, - public val pidParameters: PidParameters, + public var pidParameters: PidParameters, // TODO expose as property ) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator { private val clock = drive.context.clock @@ -65,7 +55,7 @@ public class PidRegulator( delay(pidParameters.timeStep) mutex.withLock { val realTime = clock.now() - val delta = target - position + val delta = target - getPosition() val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) integral += delta * dtSeconds val derivative = (drive.position - lastPosition) / dtSeconds @@ -87,7 +77,7 @@ public class PidRegulator( drive.stop() } - override val position: Double get() = drive.position + override suspend fun getPosition(): Double = drive.position } public fun DeviceGroup.pid( diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt index adf319b..eb9a333 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt @@ -17,11 +17,11 @@ public interface Regulator : Device { /** * Current position value */ - public val position: Double + public suspend fun getPosition(): Double public companion object : DeviceSpec<Regulator>() { public val target: MutableDevicePropertySpec<Regulator, Double> by mutableProperty(MetaConverter.double, Regulator::target) - public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { position } + public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { getPosition() } } } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt new file mode 100644 index 0000000..c97784d --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt @@ -0,0 +1,10 @@ +package space.kscience.controls.constructor.units + +import kotlin.jvm.JvmInline + + +/** + * A value without identity coupled to units of measurements. + */ +@JvmInline +public value class NumericalValue<T: UnitsOfMeasurement>(public val value: Double) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt new file mode 100644 index 0000000..c29c1be --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt @@ -0,0 +1,30 @@ +package space.kscience.controls.constructor.units + + +public interface UnitsOfMeasurement + +/**/ + +public interface UnitsOfLength : UnitsOfMeasurement + +public data object Meters: UnitsOfLength + +/**/ + +public interface UnitsOfTime: UnitsOfMeasurement + +public data object Seconds: UnitsOfTime + +/**/ + +public interface UnitsOfVelocity: UnitsOfMeasurement + +public data object MetersPerSecond: UnitsOfVelocity + +/**/ + +public interface UnitsAngularOfVelocity: UnitsOfMeasurement + +public data object RadiansPerSecond: UnitsAngularOfVelocity + +public data object DegreesPerSecond: UnitsAngularOfVelocity \ No newline at end of file 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 index 20ba47e..a245d29 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt @@ -12,6 +12,7 @@ import kotlin.math.roundToLong @OptIn(InternalCoroutinesApi::class) private class CompressedTimeDispatcher( + val clockManager: ClockManager, val dispatcher: CoroutineDispatcher, val compression: Double, ) : CoroutineDispatcher(), Delay { @@ -74,7 +75,7 @@ public class ClockManager : AbstractPlugin() { ): CoroutineDispatcher = if (timeCompression == 1.0) { dispatcher } else { - CompressedTimeDispatcher(dispatcher, timeCompression) + CompressedTimeDispatcher(this, dispatcher, timeCompression) } public companion object : PluginFactory<ClockManager> { 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 07d831f..503df83 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 @@ -1,10 +1,7 @@ package space.kscience.controls.spec import kotlinx.coroutines.withContext -import space.kscience.controls.api.ActionDescriptor -import space.kscience.controls.api.Device -import space.kscience.controls.api.PropertyDescriptor -import space.kscience.controls.api.metaDescriptor +import space.kscience.controls.api.* import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.descriptors.MetaDescriptor @@ -159,7 +156,6 @@ public abstract class DeviceSpec<D : Device> { deviceAction } } - } /** @@ -196,3 +192,16 @@ public fun <D : Device> DeviceSpec<D>.metaAction( execute(it) } + +/** + * Throw an exception if device does not have all properties and actions defined by this specification + */ +public fun DeviceSpec<*>.validate(device: Device) { + properties.map { it.value.descriptor }.forEach { specProperty -> + check(specProperty in device.propertyDescriptors) { "Property ${specProperty.name} not registered in ${device.id}" } + } + + actions.map { it.value.descriptor }.forEach { specAction -> + check(specAction in device.actionDescriptors) { "Action ${specAction.name} not registered in ${device.id}" } + } +} diff --git a/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt b/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt index 764dcad..b08066b 100644 --- a/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt +++ b/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt @@ -19,42 +19,37 @@ import kotlin.test.assertEquals class MagixLoopTest { @Test - fun deviceHub() = runTest { - val context = Context { - plugin(DeviceManager) - } - - val server = context.startMagixServer() - - val deviceManager = context.request(DeviceManager) - - val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") - -// deviceEndpoint.subscribe().onEach { -// println(it) -// }.launchIn(this) - - deviceManager.launchMagixService(deviceEndpoint, "device") - - launch { - delay(50) - repeat(10) { - deviceManager.install("test[$it]", TestDevice) - } - } - - val clientEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") - - val remoteHub = clientEndpoint.remoteDeviceHub(context, "client", "device") - - assertEquals(0, remoteHub.devices.size) - - delay(60) - //switch context to use actual delay + fun realDeviceHub() = runTest { withContext(Dispatchers.Default) { + val context = Context { + plugin(DeviceManager) + } + + val server = context.startMagixServer() + + val deviceManager = context.request(DeviceManager) + + val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + + deviceManager.launchMagixService(deviceEndpoint, "device") + + launch { + delay(50) + repeat(10) { + deviceManager.install("test[$it]", TestDevice) + } + } + + val clientEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + + val remoteHub = clientEndpoint.remoteDeviceHub(context, "client", "device") + + assertEquals(0, remoteHub.devices.size) + delay(60) clientEndpoint.requestDeviceUpdate("client", "device") delay(60) assertEquals(10, remoteHub.devices.size) + server.stop() } } } \ No newline at end of file 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 08a84b0..41369dc 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 @@ -34,7 +34,7 @@ public suspend inline fun <reified T: Any> OpcUaDevice.readOpcWithTime( converter: MetaConverter<T>, magAge: Double = 500.0 ): Pair<T, DateTime> { - val data = client.readValue(magAge, TimestampsToReturn.Server, nodeId).await() + val data: DataValue = client.readValue(magAge, TimestampsToReturn.Server, nodeId).await() val time = data.serverTime ?: error("No server time provided") val meta: Meta = when (val content = data.value.value) { is T -> return content to time diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/koalaPlotExtensions.kt similarity index 100% rename from controls-vision/src/commonMain/kotlin/plotExtensions.kt rename to controls-vision/src/commonMain/kotlin/koalaPlotExtensions.kt diff --git a/controls-visualisation-compose/build.gradle.kts b/controls-visualisation-compose/build.gradle.kts new file mode 100644 index 0000000..48ae9cd --- /dev/null +++ b/controls-visualisation-compose/build.gradle.kts @@ -0,0 +1,45 @@ +import org.jetbrains.compose.ExperimentalComposeLibrary + +plugins { + id("space.kscience.gradle.mpp") + alias(spclibs.plugins.compose) + `maven-publish` +} + +description = """ + Visualisation extension using compose-multiplatform +""".trimIndent() + +kscience { + jvm() + useKtor() + useSerialization() + useContextReceivers() + commonMain { + api(projects.controlsConstructor) + api("io.github.koalaplot:koalaplot-core:0.6.0") + } +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(compose.foundation) + api(compose.material3) + @OptIn(ExperimentalComposeLibrary::class) + api(compose.desktop.components.splitPane) + } + } +// jvmMain { +// dependencies { +// implementation(compose.desktop.currentOs) +// } +// } + } +} + + +readme { + maturity = space.kscience.gradle.Maturity.PROTOTYPE +} \ No newline at end of file diff --git a/controls-visualisation-compose/src/commonMain/kotlin/TimeAxisModel.kt b/controls-visualisation-compose/src/commonMain/kotlin/TimeAxisModel.kt new file mode 100644 index 0000000..2c82802 --- /dev/null +++ b/controls-visualisation-compose/src/commonMain/kotlin/TimeAxisModel.kt @@ -0,0 +1,45 @@ +package space.kscience.controls.compose + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.github.koalaplot.core.xygraph.AxisModel +import io.github.koalaplot.core.xygraph.TickValues +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlin.math.floor +import kotlin.time.Duration +import kotlin.time.times + +public class TimeAxisModel( + override val minimumMajorTickSpacing: Dp = 50.dp, + private val rangeProvider: () -> ClosedRange<Instant>, +) : AxisModel<Instant> { + + override fun computeTickValues(axisLength: Dp): TickValues<Instant> { + val currentRange = rangeProvider() + val rangeLength = currentRange.endInclusive - currentRange.start + val numTicks = floor(axisLength / minimumMajorTickSpacing).toInt() + val numMinorTicks = numTicks * 2 + return object : TickValues<Instant> { + override val majorTickValues: List<Instant> = List(numTicks) { + currentRange.start + it.toDouble() / (numTicks - 1) * rangeLength + } + + override val minorTickValues: List<Instant> = List(numMinorTicks) { + currentRange.start + it.toDouble() / (numMinorTicks - 1) * rangeLength + } + } + } + + override fun computeOffset(point: Instant): Float { + val currentRange = rangeProvider() + return ((point - currentRange.start) / (currentRange.endInclusive - currentRange.start)).toFloat() + } + + public companion object { + public fun recent(duration: Duration, clock: Clock = Clock.System): TimeAxisModel = TimeAxisModel { + val now = clock.now() + (now - duration)..now + } + } +} \ No newline at end of file diff --git a/controls-visualisation-compose/src/commonMain/kotlin/composeState.kt b/controls-visualisation-compose/src/commonMain/kotlin/composeState.kt new file mode 100644 index 0000000..5b793d6 --- /dev/null +++ b/controls-visualisation-compose/src/commonMain/kotlin/composeState.kt @@ -0,0 +1,31 @@ +package space.kscience.controls.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.Flow +import space.kscience.controls.constructor.DeviceState +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + + +/** + * Represent this [DeviceState] as Compose multiplatform [State] + */ +@Composable +public fun <T> DeviceState<T>.asComposeState( + coroutineContext: CoroutineContext = EmptyCoroutineContext, +): State<T> = valueFlow.collectAsState(value, coroutineContext) + + +/** + * Represent this Compose [State] as [DeviceState] + */ +public fun <T> State<T>.asDeviceState(): DeviceState<T> = object : DeviceState<T> { + override val value: T get() = this@asDeviceState.value + + override val valueFlow: Flow<T> get() = snapshotFlow { this@asDeviceState.value } + + override fun toString(): String = "ComposeState(value=$value)" +} \ No newline at end of file diff --git a/controls-visualisation-compose/src/commonMain/kotlin/indicators.kt b/controls-visualisation-compose/src/commonMain/kotlin/indicators.kt new file mode 100644 index 0000000..ada0f10 --- /dev/null +++ b/controls-visualisation-compose/src/commonMain/kotlin/indicators.kt @@ -0,0 +1,2 @@ +package space.kscience.controls.compose + diff --git a/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt new file mode 100644 index 0000000..d721f0a --- /dev/null +++ b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt @@ -0,0 +1,230 @@ +package space.kscience.controls.compose + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.SolidColor +import io.github.koalaplot.core.line.LinePlot +import io.github.koalaplot.core.style.LineStyle +import io.github.koalaplot.core.xygraph.DefaultPoint +import io.github.koalaplot.core.xygraph.XYGraphScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import space.kscience.controls.api.Device +import space.kscience.controls.api.PropertyChangedMessage +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.Meta +import space.kscience.dataforge.meta.double +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + + +private val defaultMaxAge get() = 10.minutes +private val defaultMaxPoints get() = 800 +private val defaultMinPoints get() = 400 +private val defaultSampling get() = 1.seconds + + +internal fun <T> Flow<ValueWithTime<T>>.collectAndTrim( + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + clock: Clock = Clock.System, +): Flow<List<ValueWithTime<T>>> { + require(maxPoints > 2) + require(minPoints > 0) + require(maxPoints > minPoints) + val points = mutableListOf<ValueWithTime<T>>() + return transform { newPoint -> + points.add(newPoint) + val now = clock.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) + } + //return a protective copy + emit(ArrayList(points)) + } +} + +private val defaultLineStyle: LineStyle = LineStyle(SolidColor(androidx.compose.ui.graphics.Color.Black)) + + +@Composable +private fun <T> XYGraphScope<Instant, T>.PlotTimeSeries( + data: List<ValueWithTime<T>>, + lineStyle: LineStyle = defaultLineStyle, +) { + LinePlot( + data = data.map { DefaultPoint(it.time, it.value) }, + lineStyle = lineStyle + ) +} + + +/** + * Add a trace that shows a [Device] property change over time. Show only latest [maxPoints] . + * @return a [Job] that handles the listener + */ +@Composable +public fun XYGraphScope<Instant, Double>.PlotDeviceProperty( + device: Device, + propertyName: String, + extractValue: Meta.() -> Double = { value?.double ?: Double.NaN }, + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + sampling: Duration = defaultSampling, + lineStyle: LineStyle = defaultLineStyle, +) { + var points by remember { mutableStateOf<List<ValueWithTime<Double>>>(emptyList()) } + + LaunchedEffect(device, propertyName, maxAge, maxPoints, minPoints, sampling) { + device.propertyMessageFlow(propertyName) + .sample(sampling) + .map { ValueWithTime(it.value.extractValue(), it.time) } + .collectAndTrim(maxAge, maxPoints, minPoints, device.clock) + .onEach { points = it } + .launchIn(this) + } + + + PlotTimeSeries(points, lineStyle) +} + +@Composable +public fun XYGraphScope<Instant, Double>.PlotDeviceProperty( + device: Device, + property: DevicePropertySpec<*, out Number>, + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + sampling: Duration = defaultSampling, + lineStyle: LineStyle = LineStyle(SolidColor(androidx.compose.ui.graphics.Color.Black)), +): Unit = PlotDeviceProperty( + device = device, + propertyName = property.name, + extractValue = { property.converter.readOrNull(this)?.toDouble() ?: Double.NaN }, + maxAge = maxAge, + maxPoints = maxPoints, + minPoints = minPoints, + sampling = sampling, + lineStyle = lineStyle +) + +@Composable +public fun XYGraphScope<Instant, Double>.PlotNumberState( + context: Context, + state: DeviceState<out Number>, + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + sampling: Duration = defaultSampling, + lineStyle: LineStyle = defaultLineStyle, +): Unit { + var points by remember { mutableStateOf<List<ValueWithTime<Double>>>(emptyList()) } + + + LaunchedEffect(context, state, maxAge, maxPoints, minPoints, sampling) { + val clock = context.clock + + state.valueFlow.sample(sampling) + .map { ValueWithTime(it.toDouble(), clock.now()) } + .collectAndTrim(maxAge, maxPoints, minPoints, clock) + .onEach { points = it } + .launchIn(this) + } + + + PlotTimeSeries(points, lineStyle) +} + + +private fun List<Instant>.averageTime(): Instant { + val min = min() + val max = max() + val duration = max - min + return min + duration / 2 +} + +private fun <T> Flow<T>.chunkedByPeriod(duration: Duration): Flow<List<T>> { + val collector: ArrayDeque<T> = ArrayDeque<T>() + return channelFlow { + launch { + while (isActive) { + delay(duration) + send(ArrayList(collector)) + collector.clear() + } + } + this@chunkedByPeriod.collect { + collector.add(it) + } + } +} + + +/** + * Average property value by [averagingInterval]. Return [startValue] on each sample interval if no events arrived. + */ +@Composable +public fun XYGraphScope<Instant, Double>.PlotAveragedDeviceProperty( + device: Device, + propertyName: String, + startValue: Double = 0.0, + extractValue: Meta.() -> Double = { value?.double ?: startValue }, + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + averagingInterval: Duration = defaultSampling, + lineStyle: LineStyle = defaultLineStyle, +) { + + var points by remember { mutableStateOf<List<ValueWithTime<Double>>>(emptyList()) } + + LaunchedEffect(device, propertyName, startValue, maxAge, maxPoints, minPoints, averagingInterval) { + val clock = device.clock + var lastValue = startValue + device.propertyMessageFlow(propertyName) + .chunkedByPeriod(averagingInterval) + .transform<List<PropertyChangedMessage>, ValueWithTime<Double>> { eventList -> + if (eventList.isEmpty()) { + ValueWithTime(lastValue, clock.now()) + } else { + val time = eventList.map { it.time }.averageTime() + val value = eventList.map { extractValue(it.value) }.average() + ValueWithTime(value, time).also { + lastValue = value + } + } + }.collectAndTrim(maxAge, maxPoints, minPoints, clock) + .onEach { points = it } + .launchIn(this) + } + + PlotTimeSeries(points, lineStyle) +} \ No newline at end of file diff --git a/controls-visualisation-compose/src/commonMain/kotlin/sliders.kt b/controls-visualisation-compose/src/commonMain/kotlin/sliders.kt new file mode 100644 index 0000000..4adf952 --- /dev/null +++ b/controls-visualisation-compose/src/commonMain/kotlin/sliders.kt @@ -0,0 +1,51 @@ +package space.kscience.controls.compose + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.SliderColors +import androidx.compose.material3.SliderDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.MutableDeviceState + +@Composable +public fun Slider( + deviceState: MutableDeviceState<Number>, + modifier: Modifier = Modifier, + enabled: Boolean = true, + valueRange: ClosedFloatingPointRange<Float> = 0f..1f, + steps: Int = 0, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + colors: SliderColors = SliderDefaults.colors(), +) { + androidx.compose.material3.Slider( + value = deviceState.value.toFloat(), + onValueChange = { deviceState.value = it }, + modifier = modifier, + enabled = enabled, + valueRange = valueRange, + steps = steps, + interactionSource = interactionSource, + colors = colors, + ) +} + +@Composable +public fun SliderIndicator( + deviceState: DeviceState<Number>, + modifier: Modifier = Modifier, + valueRange: ClosedFloatingPointRange<Float> = 0f..1f, + steps: Int = 0, + colors: SliderColors = SliderDefaults.colors(), +) { + androidx.compose.material3.Slider( + value = deviceState.value.toFloat(), + onValueChange = { /*do nothing*/ }, + modifier = modifier, + enabled = false, + valueRange = valueRange, + steps = steps, + colors = colors, + ) +} \ No newline at end of file diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index 8628dd1..6b6f461 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.compose.ExperimentalComposeLibrary import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode @@ -8,14 +7,13 @@ plugins { } kscience { - jvm { - withJava() - } + jvm() useKtor() useSerialization() useContextReceivers() commonMain { - implementation(projects.controlsVision) + implementation(projects.controlsVisualisationCompose) +// implementation(projects.controlsVision) implementation(projects.controlsConstructor) // implementation("io.github.koalaplot:koalaplot-core:0.6.0") } @@ -30,8 +28,6 @@ kotlin { jvmMain { dependencies { implementation(compose.desktop.currentOs) - @OptIn(ExperimentalComposeLibrary::class) - implementation(compose.desktop.components.splitPane) } } } diff --git a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt index bb15c49..9f84cb7 100644 --- a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt +++ b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt @@ -5,7 +5,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.* +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -13,10 +14,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import kotlinx.serialization.Serializable +import space.kscience.controls.compose.asComposeState import space.kscience.controls.constructor.* import space.kscience.dataforge.context.Context -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.pow import kotlin.math.sqrt import kotlin.time.Duration.Companion.milliseconds @@ -29,16 +29,11 @@ data class XY(val x: Double, val y: Double) { } } +val XY.length: Double get() = sqrt(x.pow(2) + y.pow(2)) + operator fun XY.plus(other: XY): XY = XY(x + other.x, y + other.y) operator fun XY.times(c: Double): XY = XY(x * c, y * c) operator fun XY.div(c: Double): XY = XY(x / c, y / c) -// -//class XYPosition(context: Context, x0: Double, y0: Double) : DeviceModel(context) { -// val x: MutableDeviceState<Double> = mutableState(x0) -// val y: MutableDeviceState<Double> = mutableState(y0) -// -// val xy = combineState(x, y) { x, y -> XY(x, y) } -//} class Spring( context: Context, @@ -46,27 +41,25 @@ class Spring( val l0: Double, val begin: DeviceState<XY>, val end: DeviceState<XY>, -) : DeviceConstructor(context) { - - val length = combineState(begin, end) { begin, end -> - sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).pow(2)) - } - - val tension: DeviceState<Double> = mapState(length) { l -> - val delta = l - l0 - k * delta - } +) : ConstructorModel(context) { /** - * direction from start to end + * vector from start to end */ - val direction = combineState(begin, end) { begin, end -> + val direction = combineState(begin, end) { begin: XY, end: XY -> val dx = end.x - begin.x val dy = end.y - begin.y - val l = sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).pow(2)) + val l = sqrt(dx.pow(2) + dy.pow(2)) XY(dx / l, dy / l) } + val tension: DeviceState<Double> = combineState(begin, end) { begin: XY, end: XY -> + val dx = end.x - begin.x + val dy = end.y - begin.y + k * sqrt(dx.pow(2) + dy.pow(2)) + } + + val beginForce = combineState(direction, tension) { direction: XY, tension: Double -> direction * (tension) } @@ -83,13 +76,15 @@ class MaterialPoint( val force: DeviceState<XY>, val position: MutableDeviceState<XY>, val velocity: MutableDeviceState<XY> = MutableDeviceState(XY.ZERO), -) : DeviceModel(context, force) { +) : ConstructorModel(context, force, position, velocity) { private val timer: TimerState = timer(2.milliseconds) + //TODO synchronize force change + private val movement = timer.onChange( - position, velocity, - alsoReads = setOf(force, velocity, position) + writes = setOf(position, velocity), + reads = setOf(force, velocity, position) ) { prev, next -> val dt = (next - prev).toDouble(DurationUnit.SECONDS) val a = force.value / mass @@ -105,31 +100,31 @@ class BodyOnSprings( k: Double, startPosition: XY, l0: Double = 1.0, - val xLeft: Double = 0.0, - val xRight: Double = 2.0, - val yBottom: Double = 0.0, - val yTop: Double = 2.0, + val xLeft: Double = -1.0, + val xRight: Double = 1.0, + val yBottom: Double = -1.0, + val yTop: Double = 1.0, ) : DeviceConstructor(context) { val width = xRight - xLeft val height = yTop - yBottom - val position = mutableState(startPosition) + val position = stateOf(startPosition) - private val leftAnchor = mutableState(XY(xLeft, yTop + yBottom / 2)) + private val leftAnchor = stateOf(XY(xLeft, (yTop + yBottom) / 2)) - val leftSpring by device( + val leftSpring = model( Spring(context, k, l0, leftAnchor, position) ) - private val rightAnchor = mutableState(XY(xRight, yTop + yBottom / 2)) + private val rightAnchor = stateOf(XY(xRight, (yTop + yBottom) / 2)) - val rightSpring by device( + val rightSpring = model( Spring(context, k, l0, rightAnchor, position) ) - val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, rignt -> - left + rignt + val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, right -> + left + right } @@ -138,18 +133,13 @@ class BodyOnSprings( context = context, mass = mass, force = force, - position = position + position = position, ) ) } -@Composable -fun <T> DeviceState<T>.collect( - coroutineContext: CoroutineContext = EmptyCoroutineContext, -): State<T> = valueFlow.collectAsState(value, coroutineContext) - fun main() = application { - val initialState = XY(1.1, 1.1) + val initialState = XY(0.1, 0.2) Window(title = "Ball on springs", onCloseRequest = ::exitApplication) { MaterialTheme { @@ -161,12 +151,20 @@ fun main() = application { BodyOnSprings(context, 100.0, 1000.0, initialState) } - val position: XY by model.body.position.collect() + //TODO add ability to freeze model + +// LaunchedEffect(Unit){ +// model.position.valueFlow.onEach { +// model.position.value = it.copy(y = model.position.value.y.coerceIn(-1.0..1.0)) +// }.collect() +// } + + val position: XY by model.body.position.asComposeState() Box(Modifier.size(400.dp)) { Canvas(modifier = Modifier.fillMaxSize()) { fun XY.toOffset() = Offset( - (x / model.width * size.width).toFloat(), - (y / model.height * size.height).toFloat() + center.x + (x / model.width * size.width).toFloat(), + center.y - (y / model.height * size.height).toFloat() ) drawCircle( diff --git a/demo/constructor/src/jvmMain/kotlin/LinearDrive.kt b/demo/constructor/src/jvmMain/kotlin/LinearDrive.kt index 95df7ad..f64ef72 100644 --- a/demo/constructor/src/jvmMain/kotlin/LinearDrive.kt +++ b/demo/constructor/src/jvmMain/kotlin/LinearDrive.kt @@ -1,38 +1,40 @@ package space.kscience.controls.demo.constructor -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.foundation.background +import androidx.compose.foundation.layout.* 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.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import space.kscience.controls.constructor.DeviceConstructor -import space.kscience.controls.constructor.DoubleInRangeState -import space.kscience.controls.constructor.device -import space.kscience.controls.constructor.deviceProperty +import io.github.koalaplot.core.ChartLayout +import io.github.koalaplot.core.legend.FlowLegend +import io.github.koalaplot.core.style.LineStyle +import io.github.koalaplot.core.util.ExperimentalKoalaPlotApi +import io.github.koalaplot.core.util.toString +import io.github.koalaplot.core.xygraph.XYGraph +import io.github.koalaplot.core.xygraph.rememberDoubleLinearAxisModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.datetime.Instant +import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi +import org.jetbrains.compose.splitpane.HorizontalSplitPane +import space.kscience.controls.compose.PlotDeviceProperty +import space.kscience.controls.compose.PlotNumberState +import space.kscience.controls.compose.TimeAxisModel +import space.kscience.controls.constructor.* import space.kscience.controls.constructor.library.* import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.clock import space.kscience.controls.manager.install -import space.kscience.controls.spec.doRecurring -import space.kscience.controls.spec.name -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.meta.Meta -import space.kscience.plotly.models.ScatterMode -import space.kscience.visionforge.plotly.PlotlyPlugin +import java.awt.Dimension import kotlin.math.PI import kotlin.math.sin import kotlin.time.Duration @@ -49,15 +51,15 @@ class LinearDrive( meta: Meta = Meta.EMPTY, ) : DeviceConstructor(drive.context, meta) { - val drive: Drive by device(drive) + val drive by device(drive) val pid by device(PidRegulator(drive, pidParameters)) val start by device(start) val end by device(end) - val position by deviceProperty(drive, Drive.position, Double.NaN) + val position = drive.propertyAsState(Drive.position, Double.NaN) - val target by deviceProperty(pid, Regulator.target, 0.0) + val target = pid.propertyAsState(Regulator.target, 0.0) } /** @@ -77,163 +79,205 @@ fun LinearDrive( meta = meta ) +class Modulator( + context: Context, + target: MutableDeviceState<Double>, + var freq: Double = 0.1, + var timeStep: Duration = 5.milliseconds, +) : DeviceConstructor(context) { + private val clockStart = clock.now() + val timer = timer(10.milliseconds) + + private val modulation = timer.onNext { + val timeFromStart = clock.now() - clockStart + val t = timeFromStart.toDouble(DurationUnit.SECONDS) + target.value = 5 * sin(2.0 * PI * freq * t) + + sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / timeStep)) + } +} + + +private val maxAge = 10.seconds + +@OptIn(ExperimentalSplitPaneApi::class, ExperimentalKoalaPlotApi::class) fun main() = application { - val context = Context { - plugin(DeviceManager) - plugin(PlotlyPlugin) - plugin(ClockManager) + val context = remember { + Context { + plugin(DeviceManager) + 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 clock = remember { context.clock } + + + var pidParameters by remember { + mutableStateOf(PidParameters(kp = 2.5, ki = 0.0, kd = -0.1, timeStep = 0.005.seconds)) } - val pidParameters = remember { - MutablePidParameters( - kp = 2.5, - ki = 0.0, - kd = -0.1, - timeStep = 0.005.seconds + val state = remember { DoubleInRangeState(0.0, -6.0..6.0) } + + val linearDrive = remember { + context.install( + "linearDrive", + LinearDrive(context, state, 0.05, pidParameters) ) } - val state = DoubleInRangeState(0.0, -6.0..6.0) - - val linearDrive = context.install( - "linearDrive", - LinearDrive(context, state, 0.05, pidParameters) - ) - - val clockStart = context.clock.now() - linearDrive.doRecurring(10.milliseconds) { - val timeFromStart = clock.now() - clockStart - val t = timeFromStart.toDouble(DurationUnit.SECONDS) - val freq = 0.1 - target.value = 5 * sin(2.0 * PI * freq * t) + - sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep)) + val modulator = remember { + context.install( + "modulator", + Modulator(context, linearDrive.target) + ) } - - val maxAge = 10.seconds - - context.showDashboard { - plot { - plotNumberState(context, state, maxAge = maxAge, sampling = 50.milliseconds) { - name = "real position" - } - plotDeviceProperty(linearDrive.pid, Regulator.position.name, maxAge = maxAge, sampling = 50.milliseconds) { - name = "read position" - } - - plotDeviceProperty(linearDrive.pid, Regulator.target.name, maxAge = maxAge, sampling = 50.milliseconds) { - name = "target" - } - } - - plot { - plotDeviceProperty( - linearDrive.start, - LimitSwitch.locked.name, - maxAge = maxAge, - sampling = 50.milliseconds - ) { - name = "start measured" - mode = ScatterMode.markers - } - plotDeviceProperty(linearDrive.end, LimitSwitch.locked.name, maxAge = maxAge, sampling = 50.milliseconds) { - name = "end measured" - mode = ScatterMode.markers - } - } - + //bind pid parameters + LaunchedEffect(Unit) { + snapshotFlow { + pidParameters + }.onEach { + linearDrive.pid.pidParameters = pidParameters + }.collect() } Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) { + window.minimumSize = Dimension(800, 400) MaterialTheme { - Column { - Row { - Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) - 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..20f, - steps = 100 - ) - } - Row { - Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) - 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 = -10f..10f, - steps = 100 - ) - } - Row { - Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) - 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 = -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 - ) - } - Row { - Button({ - pidParameters.run { - kp = 2.5 - ki = 0.0 - kd = -0.1 - timeStep = 0.005.seconds + HorizontalSplitPane { + first(400.dp) { + Column(modifier = Modifier.background(color = Color.LightGray).fillMaxHeight()) { + Row { + Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) + TextField( + String.format("%.2f", pidParameters.kp), + { pidParameters = pidParameters.copy(kp = it.toDouble()) }, + Modifier.width(100.dp), + enabled = false + ) + Slider( + pidParameters.kp.toFloat(), + { pidParameters = pidParameters.copy(kp = it.toDouble()) }, + valueRange = 0f..20f, + steps = 100 + ) + } + Row { + Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) + TextField( + String.format("%.2f", pidParameters.ki), + { pidParameters = pidParameters.copy(ki = it.toDouble()) }, + Modifier.width(100.dp), + enabled = false + ) + + Slider( + pidParameters.ki.toFloat(), + { pidParameters = pidParameters.copy(ki = it.toDouble()) }, + valueRange = -10f..10f, + steps = 100 + ) + } + Row { + Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) + TextField( + String.format("%.2f", pidParameters.kd), + { pidParameters = pidParameters.copy(kd = it.toDouble()) }, + Modifier.width(100.dp), + enabled = false + ) + + Slider( + pidParameters.kd.toFloat(), + { pidParameters = pidParameters.copy(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 = pidParameters.copy(timeStep = it.toDouble().milliseconds) }, + Modifier.width(100.dp), + enabled = false + ) + + Slider( + pidParameters.timeStep.toDouble(DurationUnit.MILLISECONDS).toFloat(), + { pidParameters = pidParameters.copy(timeStep = it.toDouble().milliseconds) }, + valueRange = 0f..100f, + steps = 100 + ) + } + Row { + Button({ + pidParameters = PidParameters( + kp = 2.5, + ki = 0.0, + kd = -0.1, + timeStep = 0.005.seconds + ) + }) { + Text("Reset") + } + } + } + } + second(400.dp) { + ChartLayout { + XYGraph<Instant, Double>( + xAxisModel = remember { TimeAxisModel.recent(maxAge, clock) }, + yAxisModel = rememberDoubleLinearAxisModel(state.range), + xAxisTitle = { Text("Time in seconds relative to current") }, + xAxisLabels = { it: Instant -> + androidx.compose.material3.Text( + (clock.now() - it).toDouble( + DurationUnit.SECONDS + ).toString(2) + ) + }, + yAxisLabels = { it: Double -> Text(it.toString(2)) } + ) { + PlotNumberState( + context = context, + state = state, + maxAge = maxAge, + sampling = 50.milliseconds, + lineStyle = LineStyle(SolidColor(Color.Blue)) + ) + PlotDeviceProperty( + linearDrive.pid, + Regulator.position, + maxAge = maxAge, + sampling = 50.milliseconds, + ) + PlotDeviceProperty( + linearDrive.pid, + Regulator.target, + maxAge = maxAge, + sampling = 50.milliseconds, + lineStyle = LineStyle(SolidColor(Color.Red)) + ) + } + Surface { + FlowLegend(3, label = { + when (it) { + 0 -> { + Text("Body position", color = Color.Blue) + } + + 1 -> { + Text("Regulator position", color = Color.Black) + } + + 2 -> { + Text("Regulator target", color = Color.Red) + } + } + }) } - }) { - Text("Reset") } } } diff --git a/demo/constructor/src/jvmMain/kotlin/Plotter.kt b/demo/constructor/src/jvmMain/kotlin/Plotter.kt new file mode 100644 index 0000000..bd0d366 --- /dev/null +++ b/demo/constructor/src/jvmMain/kotlin/Plotter.kt @@ -0,0 +1,2 @@ +package space.kscience.controls.demo.constructor + diff --git a/settings.gradle.kts b/settings.gradle.kts index 39adc72..366c39e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -64,6 +64,7 @@ include( ":controls-storage", ":controls-storage:controls-xodus", ":controls-constructor", + ":controls-visualisation-compose", ":controls-vision", ":controls-jupyter", ":magix", From 54e915ef108b37f89092113334117f75db488f71 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sat, 1 Jun 2024 09:35:03 +0300 Subject: [PATCH 093/125] WIP Constructor update --- .../constructor/ConstructorElement.kt | 7 +- .../controls/constructor/DeviceConstructor.kt | 18 ++-- .../controls/constructor/DeviceGroup.kt | 17 ++-- .../controls/constructor/DeviceState.kt | 4 +- ...onstructorModel.kt => ModelConstructor.kt} | 2 +- .../controls/constructor/exoticState.kt | 57 ------------- .../controls/constructor/internalState.kt | 16 ++++ .../controls/constructor/library/Converter.kt | 20 ----- .../constructor/library/DoubleInRangeState.kt | 57 +++++++++++++ .../controls/constructor/library/Drive.kt | 3 +- .../constructor/library/LimitSwitch.kt | 48 ++++++----- .../constructor/library/PidRegulator.kt | 83 ++++++++----------- .../controls/constructor/library/Regulator.kt | 24 ++---- .../controls/constructor/library/StepDrive.kt | 33 ++++++++ .../constructor/library/Transmission.kt | 33 ++++++++ .../controls/constructor/units/Direction.kt | 6 ++ .../constructor/units/NumericalValue.kt | 29 ++++++- .../constructor/units/UnitsOfMeasurement.kt | 23 +++-- .../controls/constructor/TimerTest.kt | 3 +- .../kscience/controls/manager/ClockManager.kt | 9 ++ .../src/jvmMain/kotlin/BodyOnSprings.kt | 4 +- .../kotlin/{LinearDrive.kt => PidDemo.kt} | 36 ++++---- 22 files changed, 320 insertions(+), 212 deletions(-) rename controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/{ConstructorModel.kt => ModelConstructor.kt} (96%) delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Converter.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/DoubleInRangeState.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/StepDrive.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Transmission.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/Direction.kt rename demo/constructor/src/jvmMain/kotlin/{LinearDrive.kt => PidDemo.kt} (92%) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt index 911a682..0aa72a0 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt @@ -36,7 +36,7 @@ public class ConnectionConstrucorElement( ) : ConstructorElement public class ModelConstructorElement( - public val model: ConstructorModel + public val model: ModelConstructor ) : ConstructorElement @@ -89,7 +89,7 @@ public fun <T> StateContainer.stateOf(initialValue: T): MutableDeviceState<T> = MutableDeviceState(initialValue) ) -public fun <T : ConstructorModel> StateContainer.model(model: T): T { +public fun <T : ModelConstructor> StateContainer.model(model: T): T { registerElement(ModelConstructorElement(model)) return model } @@ -125,14 +125,13 @@ public fun <T1, T2, R> StateContainer.combineState( transformation: (T1, T2) -> R, ): DeviceState<R> = state(DeviceState.combine(first, second, transformation)) - /** * Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically * transferred onto [targetState], but not vise versa. * * On resulting [Job] cancel the binding is unregistered */ -public fun <T> StateContainer.bindTo(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job { +public fun <T> StateContainer.bind(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job { val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState)) registerElement(descriptor) return sourceState.valueFlow.onEach { 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 8854247..00d408e 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 @@ -32,13 +32,14 @@ public abstract class DeviceConstructor( _constructorElements.remove(constructorElement) } - override fun <T> registerProperty( + override fun <T, S: DeviceState<T>> registerAsProperty( converter: MetaConverter<T>, descriptor: PropertyDescriptor, - state: DeviceState<T>, - ) { - super.registerProperty(converter, descriptor, state) + state: S, + ): S { + val res = super.registerAsProperty(converter, descriptor, state) registerElement(PropertyConstructorElement(this, descriptor.name, state)) + return res } } @@ -83,7 +84,7 @@ public fun <T, S : DeviceState<T>> DeviceConstructor.property( PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) - registerProperty(converter, descriptor, state) + registerAsProperty(converter, descriptor, state) ReadOnlyProperty { _: DeviceConstructor, _ -> state } @@ -140,7 +141,10 @@ public fun <T> DeviceConstructor.virtualProperty( nameOverride, ) -public fun <T, S : DeviceState<T>> DeviceConstructor.property( +public fun <T, S : DeviceState<T>> DeviceConstructor.registerAsProperty( spec: DevicePropertySpec<*, T>, state: S, -): Unit = registerProperty(spec.converter, spec.descriptor, state) +): S { + registerAsProperty(spec.converter, spec.descriptor, state) + return state +} 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 ddac115..182d10b 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 @@ -93,11 +93,11 @@ public open class DeviceGroup( /** * Register a new property based on [DeviceState]. Properties could be modified dynamically */ - public open fun <T> registerProperty( + public open fun <T, S : DeviceState<T>> registerAsProperty( converter: MetaConverter<T>, descriptor: PropertyDescriptor, - state: DeviceState<T>, - ) { + state: S, + ): S { val name = descriptor.name.parseAsName() require(properties[name] == null) { "Can't add property with name $name. It already exists." } properties[name] = Property(state, converter, descriptor) @@ -109,6 +109,7 @@ public open class DeviceGroup( ) ) }.launchIn(this) + return state } private val actions: MutableMap<Name, Action> = hashMapOf() @@ -174,8 +175,8 @@ public open class DeviceGroup( } } -public fun <T> DeviceGroup.registerProperty(propertySpec: DevicePropertySpec<*, T>, state: DeviceState<T>) { - registerProperty(propertySpec.converter, propertySpec.descriptor, state) +public fun <T> DeviceGroup.registerAsProperty(propertySpec: DevicePropertySpec<*, T>, state: DeviceState<T>) { + registerAsProperty(propertySpec.converter, propertySpec.descriptor, state) } public fun DeviceManager.registerDeviceGroup( @@ -246,13 +247,13 @@ public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() - /** * Register read-only property based on [state] */ -public fun <T : Any> DeviceGroup.registerProperty( +public fun <T : Any> DeviceGroup.registerAsProperty( name: String, converter: MetaConverter<T>, state: DeviceState<T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ) { - registerProperty( + registerAsProperty( converter, PropertyDescriptor(name).apply(descriptorBuilder), state @@ -268,7 +269,7 @@ public fun <T : Any> DeviceGroup.registerMutableProperty( state: MutableDeviceState<T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ) { - registerProperty( + registerAsProperty( converter, PropertyDescriptor(name).apply(descriptorBuilder), 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 db388d3..5083347 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 @@ -67,9 +67,11 @@ public fun <T, R> DeviceState.Companion.map( override val valueFlow: Flow<R> = state.valueFlow.map(mapper) - override fun toString(): String = "DeviceState.map(arg=${state})" + override fun toString(): String = "DeviceState.map(state=${state})" } +public fun <T, R> DeviceState<T>.map(mapper: (T) -> R): DeviceStateWithDependencies<R> = DeviceState.map(this, mapper) + /** * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used. */ diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorModel.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ModelConstructor.kt similarity index 96% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorModel.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ModelConstructor.kt index 35a1e31..8fdc708 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorModel.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ModelConstructor.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.newCoroutineContext import space.kscience.dataforge.context.Context import kotlin.coroutines.CoroutineContext -public abstract class ConstructorModel( +public abstract class ModelConstructor( final override val context: Context, vararg dependencies: DeviceState<*>, ) : StateContainer, CoroutineScope { diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt deleted file mode 100644 index 96ea4fe..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt +++ /dev/null @@ -1,57 +0,0 @@ -package space.kscience.controls.constructor - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - - -/** - * A state describing a [Double] value in the [range] - */ -public class DoubleInRangeState( - initialValue: Double, - public val range: ClosedFloatingPointRange<Double>, -) : MutableDeviceState<Double> { - - init { - require(initialValue in range) { "Initial value should be in range" } - } - - - private val _valueFlow = MutableStateFlow(initialValue) - - override var value: Double - get() = _valueFlow.value - set(newValue) { - _valueFlow.value = newValue.coerceIn(range) - } - - override val valueFlow: StateFlow<Double> get() = _valueFlow - - /** - * A state showing that the range is on its lower boundary - */ - public val atStart: DeviceState<Boolean> = DeviceState.map(this) { - it <= range.start - } - - /** - * A state showing that the range is on its higher boundary - */ - public val atEnd: DeviceState<Boolean> = DeviceState.map(this) { - it >= range.endInclusive - } - - override fun toString(): String = "DoubleRangeState(range=$range)" - - -} - -/** - * Create and register a [DoubleInRangeState] - */ -public fun StateContainer.doubleInRangeState( - initialValue: Double, - range: ClosedFloatingPointRange<Double>, -): DoubleInRangeState = DoubleInRangeState(initialValue, range).also { - registerElement(StateConstructorElement(it)) -} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt index 11c778d..8bdfce1 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt @@ -2,6 +2,7 @@ package space.kscience.controls.constructor import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow /** * A [MutableDeviceState] that does not correspond to a physical state @@ -35,3 +36,18 @@ public fun <T> MutableDeviceState( initialValue: T, callback: (T) -> Unit = {}, ): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback) + + +/** + * Create a [DeviceState] with constant value + */ +public fun <T> DeviceState( + value: T +): DeviceState<T> = object : DeviceState<T> { + override val value: T get() = value + override val valueFlow: Flow<T> + get() = emptyFlow() + + override fun toString(): String = "ConstDeviceState($value)" + +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Converter.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Converter.kt deleted file mode 100644 index 45cd4db..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Converter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package space.kscience.controls.constructor.library - -import kotlinx.coroutines.flow.FlowCollector -import space.kscience.controls.constructor.DeviceConstructor -import space.kscience.controls.constructor.DeviceState -import space.kscience.controls.constructor.DeviceStateWithDependencies -import space.kscience.controls.constructor.flowState -import space.kscience.dataforge.context.Context - -/** - * A device that converts one type of physical quantity to another type - */ -public class Converter<T, R>( - context: Context, - input: DeviceState<T>, - initialValue: R, - transform: suspend FlowCollector<R>.(T) -> Unit, -) : DeviceConstructor(context) { - public val output: DeviceStateWithDependencies<R> = flowState(input, initialValue, transform) -} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/DoubleInRangeState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/DoubleInRangeState.kt new file mode 100644 index 0000000..10773da --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/DoubleInRangeState.kt @@ -0,0 +1,57 @@ +package space.kscience.controls.constructor.library + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.map + +/** + * A state describing a [Double] value in the [range] + */ +public open class DoubleInRangeState( + private val input: DeviceState<Double>, + public val range: ClosedFloatingPointRange<Double>, +) : DeviceState<Double> { + + override val valueFlow: Flow<Double> get() = input.valueFlow.map { it.coerceIn(range) } + + override val value: Double get() = input.value.coerceIn(range) + + /** + * A state showing that the range is on its lower boundary + */ + public val atStart: DeviceState<Boolean> = input.map { it <= range.start } + + /** + * A state showing that the range is on its higher boundary + */ + public val atEnd: DeviceState<Boolean> = input.map { it >= range.endInclusive } + + override fun toString(): String = "DoubleRangeState(value=${value},range=$range)" +} + +public class MutableDoubleInRangeState( + private val mutableInput: MutableDeviceState<Double>, + range: ClosedFloatingPointRange<Double> +) : DoubleInRangeState(mutableInput, range), MutableDeviceState<Double> { + override var value: Double + get() = super.value + set(value) { + mutableInput.value = value.coerceIn(range) + } +} + +public fun MutableDoubleInRangeState( + initialValue: Double, + range: ClosedFloatingPointRange<Double> +): MutableDoubleInRangeState = MutableDoubleInRangeState(MutableDeviceState(initialValue),range) + + +public fun DeviceState<Double>.coerceIn( + range: ClosedFloatingPointRange<Double> +): DoubleInRangeState = DoubleInRangeState(this, range) + +public fun MutableDeviceState<Double>.coerceIn( + range: ClosedFloatingPointRange<Double> +): MutableDoubleInRangeState = MutableDoubleInRangeState(this, range) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt index 6a07db0..77a16ca 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt @@ -98,4 +98,5 @@ public class VirtualDrive( } } -public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = propertyAsState(Drive.force) +public fun Drive.stateOfForce(initialForce: Double = 0.0): MutableDeviceState<Double> = + propertyAsState(Drive.force, initialForce) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt index 876d2dc..919d026 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt @@ -1,37 +1,45 @@ package space.kscience.controls.constructor.library -import space.kscience.controls.api.Device import space.kscience.controls.constructor.DeviceConstructor import space.kscience.controls.constructor.DeviceState -import space.kscience.controls.constructor.property +import space.kscience.controls.constructor.map +import space.kscience.controls.constructor.registerAsProperty +import space.kscience.controls.constructor.units.Direction +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.controls.constructor.units.UnitsOfMeasurement +import space.kscience.controls.constructor.units.compareTo 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.meta.MetaConverter -/** - * A limit switch device - */ -public interface LimitSwitch : Device { - - public fun isLocked(): Boolean - - public companion object : DeviceSpec<LimitSwitch>() { - public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { isLocked() } - } -} - /** * Virtual [LimitSwitch] */ -public class VirtualLimitSwitch( +public class LimitSwitch( context: Context, locked: DeviceState<Boolean>, -) : DeviceConstructor(context), LimitSwitch { +) : DeviceConstructor(context) { - public val locked: DeviceState<Boolean> by property(MetaConverter.boolean, locked) + public val locked: DeviceState<Boolean> = registerAsProperty(LimitSwitch.locked, locked) - override fun isLocked(): Boolean = locked.value -} \ No newline at end of file + public companion object : DeviceSpec<LimitSwitch>() { + public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked.value } + } +} + +public fun <U : UnitsOfMeasurement, T : NumericalValue<U>> LimitSwitch( + context: Context, + limit: T, + boundary: Direction, + position: DeviceState<T>, +): LimitSwitch = LimitSwitch( + context, + DeviceState.map(position) { + when (boundary) { + Direction.UP -> it >= limit + Direction.DOWN -> it <= limit + } + } +) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt index 9edf91e..27c3f95 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt @@ -1,19 +1,17 @@ package space.kscience.controls.constructor.library -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.Instant -import space.kscience.controls.constructor.DeviceGroup +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.state +import space.kscience.controls.constructor.stateOf import space.kscience.controls.manager.clock -import space.kscience.controls.spec.DeviceBySpec -import space.kscience.controls.spec.write -import space.kscience.dataforge.names.parseAsName +import space.kscience.dataforge.context.Context import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit @@ -24,64 +22,53 @@ public data class PidParameters( val kp: Double, val ki: Double, val kd: Double, - val timeStep: Duration = 1.milliseconds, + val timeStep: Duration, ) + /** - * A drive with PID regulator + * A PID regulator */ public class PidRegulator( - public val drive: Drive, + context: Context, + private val position: DeviceState<Double>, public var pidParameters: PidParameters, // TODO expose as property -) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator { + output: MutableDeviceState<Double> = MutableDeviceState(0.0), +) : Regulator<Double>(context) { - private val clock = drive.context.clock + override val target: MutableDeviceState<Double> = stateOf(0.0) + override val output: MutableDeviceState<Double> = state(output) - override var target: Double = drive.position - - private var lastTime: Instant = clock.now() - private var lastPosition: Double = target + private var lastPosition: Double = target.value private var integral: Double = 0.0 - - private var updateJob: Job? = null private val mutex = Mutex() + private var lastTime = clock.now() - override suspend fun onStart() { - drive.start() - updateJob = launch { - while (isActive) { - delay(pidParameters.timeStep) - mutex.withLock { - val realTime = clock.now() - val delta = target - getPosition() - val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) - integral += delta * dtSeconds - val derivative = (drive.position - lastPosition) / dtSeconds + private val updateJob = launch { + while (isActive) { + delay(pidParameters.timeStep) + mutex.withLock { + val realTime = clock.now() + val delta = target.value - position.value + val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) + integral += delta * dtSeconds + val derivative = (position.value - lastPosition) / dtSeconds - //set last time and value to new values - lastTime = realTime - lastPosition = drive.position + //set last time and value to new values + lastTime = realTime + lastPosition = position.value - drive.write(Drive.force,pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative) - //drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative - propertyChanged(Regulator.position, drive.position) - } + output.value = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative } } } - - override suspend fun onStop() { - updateJob?.cancel() - drive.stop() - } - - override suspend fun getPosition(): Double = drive.position } -public fun DeviceGroup.pid( - name: String, - drive: Drive, - pidParameters: PidParameters, -): PidRegulator = install(name.parseAsName(), PidRegulator(drive, pidParameters)) \ No newline at end of file +// +//public fun DeviceGroup.pid( +// name: String, +// drive: Drive, +// pidParameters: PidParameters, +//): PidRegulator = install(name.parseAsName(), PidRegulator(drive, pidParameters)) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt index eb9a333..628fb0e 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt @@ -1,27 +1,17 @@ package space.kscience.controls.constructor.library -import space.kscience.controls.api.Device -import space.kscience.controls.spec.* -import space.kscience.dataforge.meta.MetaConverter +import space.kscience.controls.constructor.DeviceConstructor +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.dataforge.context.Context /** * A regulator with target value and current position */ -public interface Regulator : Device { - /** - * Get or set target value - */ - public var target: Double +public abstract class Regulator<T>(context: Context) : DeviceConstructor(context) { - /** - * Current position value - */ - public suspend fun getPosition(): Double + public abstract val target: MutableDeviceState<T> - public companion object : DeviceSpec<Regulator>() { - public val target: MutableDevicePropertySpec<Regulator, Double> by mutableProperty(MetaConverter.double, Regulator::target) - - public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { getPosition() } - } + public abstract val output: DeviceState<T> } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/StepDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/StepDrive.kt new file mode 100644 index 0000000..47a9742 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/StepDrive.kt @@ -0,0 +1,33 @@ +package space.kscience.controls.constructor.library + +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.combineState +import space.kscience.controls.constructor.property +import space.kscience.controls.constructor.units.* +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.MetaConverter + +/** + * A step drive regulated by [input] + */ +public class StepDrive( + context: Context, + public val step: NumericalValue<Degrees>, + public val zero: NumericalValue<Degrees> = NumericalValue(0.0), + direction: Direction = Direction.UP, + input: MutableDeviceState<Int> = MutableDeviceState(0), + hold: MutableDeviceState<Boolean> = MutableDeviceState(false) +) : Transmission<Int, NumericalValue<Degrees>>(context) { + + override val input: MutableDeviceState<Int> by property(MetaConverter.int, input) + + public val hold: MutableDeviceState<Boolean> by property(MetaConverter.boolean, hold) + + override val output: DeviceState<NumericalValue<Degrees>> = combineState( + input, hold + ) { input, hold -> + //TODO use hold parameter + zero + input * direction.coef * step + } +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Transmission.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Transmission.kt new file mode 100644 index 0000000..5974a2f --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Transmission.kt @@ -0,0 +1,33 @@ +package space.kscience.controls.constructor.library + +import space.kscience.controls.constructor.DeviceConstructor +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.flowState +import space.kscience.dataforge.context.Context + + +/** + * A model for a device that converts one type of physical quantity to another type + */ +public abstract class Transmission<T, R>(context: Context) : DeviceConstructor(context) { + public abstract val input: MutableDeviceState<T> + public abstract val output: DeviceState<R> + + public companion object { + /** + * Create a device that is a hard connection between two physical quantities + */ + public suspend fun <T, R> direct( + context: Context, + input: MutableDeviceState<T>, + transform: suspend (T) -> R + ): Transmission<T, R> { + val initialValue = transform(input.value) + return object : Transmission<T, R>(context) { + override val input: MutableDeviceState<T> = input + override val output: DeviceState<R> = flowState(input, initialValue) { emit(transform(it)) } + } + } + } +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/Direction.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/Direction.kt new file mode 100644 index 0000000..3685c7e --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/Direction.kt @@ -0,0 +1,6 @@ +package space.kscience.controls.constructor.units + +public enum class Direction(public val coef: Int) { + UP(1), + DOWN(-1) +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt index c97784d..98fe418 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt @@ -7,4 +7,31 @@ import kotlin.jvm.JvmInline * A value without identity coupled to units of measurements. */ @JvmInline -public value class NumericalValue<T: UnitsOfMeasurement>(public val value: Double) \ No newline at end of file +public value class NumericalValue<U : UnitsOfMeasurement>(public val value: Double) + +public operator fun <U: UnitsOfMeasurement> NumericalValue<U>.compareTo(other: NumericalValue<U>): Int = + value.compareTo(other.value) + +public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.plus( + other: NumericalValue<U> +): NumericalValue<U> = NumericalValue(this.value + other.value) + +public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.minus( + other: NumericalValue<U> +): NumericalValue<U> = NumericalValue(this.value - other.value) + +public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.times( + c: Number +): NumericalValue<U> = NumericalValue(this.value * c.toDouble()) + +public operator fun <U : UnitsOfMeasurement> Number.times( + numericalValue: NumericalValue<U> +): NumericalValue<U> = NumericalValue(numericalValue.value * toDouble()) + +public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.times( + c: Double +): NumericalValue<U> = NumericalValue(this.value * c) + +public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div( + c: Number +): NumericalValue<U> = NumericalValue(this.value / c.toDouble()) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt index c29c1be..ed295ef 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt @@ -7,24 +7,31 @@ public interface UnitsOfMeasurement public interface UnitsOfLength : UnitsOfMeasurement -public data object Meters: UnitsOfLength +public data object Meters : UnitsOfLength /**/ -public interface UnitsOfTime: UnitsOfMeasurement +public interface UnitsOfTime : UnitsOfMeasurement -public data object Seconds: UnitsOfTime +public data object Seconds : UnitsOfTime /**/ -public interface UnitsOfVelocity: UnitsOfMeasurement +public interface UnitsOfVelocity : UnitsOfMeasurement -public data object MetersPerSecond: UnitsOfVelocity +public data object MetersPerSecond : UnitsOfVelocity /**/ -public interface UnitsAngularOfVelocity: UnitsOfMeasurement +public sealed interface UnitsOfAngles : UnitsOfMeasurement -public data object RadiansPerSecond: UnitsAngularOfVelocity +public data object Radians : UnitsOfAngles +public data object Degrees : UnitsOfAngles -public data object DegreesPerSecond: UnitsAngularOfVelocity \ No newline at end of file +/**/ + +public interface UnitsAngularOfVelocity : UnitsOfMeasurement + +public data object RadiansPerSecond : UnitsAngularOfVelocity + +public data object DegreesPerSecond : UnitsAngularOfVelocity \ No newline at end of file diff --git a/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/TimerTest.kt b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/TimerTest.kt index 323f8df..38f7d83 100644 --- a/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/TimerTest.kt +++ b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/TimerTest.kt @@ -15,8 +15,9 @@ class TimerTest { @Test fun timer() = runTest { val timer = TimerState(Global.request(ClockManager), 10.milliseconds) - timer.valueFlow.take(10).onEach { + timer.valueFlow.take(100).onEach { println(it) }.collect() + } } \ No newline at end of file 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 index a245d29..4204730 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt @@ -9,6 +9,7 @@ import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.double import kotlin.coroutines.CoroutineContext import kotlin.math.roundToLong +import kotlin.time.Duration @OptIn(InternalCoroutinesApi::class) private class CompressedTimeDispatcher( @@ -78,6 +79,14 @@ public class ClockManager : AbstractPlugin() { CompressedTimeDispatcher(this, dispatcher, timeCompression) } + public fun scheduleWithFixedDelay(tick: Duration, block: suspend () -> Unit): Job = context.launch(asDispatcher()) { + while (isActive) { + delay(tick) + block() + } + } + + public companion object : PluginFactory<ClockManager> { override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP) diff --git a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt index 9f84cb7..7fcb5ab 100644 --- a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt +++ b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt @@ -41,7 +41,7 @@ class Spring( val l0: Double, val begin: DeviceState<XY>, val end: DeviceState<XY>, -) : ConstructorModel(context) { +) : ModelConstructor(context) { /** * vector from start to end @@ -76,7 +76,7 @@ class MaterialPoint( val force: DeviceState<XY>, val position: MutableDeviceState<XY>, val velocity: MutableDeviceState<XY> = MutableDeviceState(XY.ZERO), -) : ConstructorModel(context, force, position, velocity) { +) : ModelConstructor(context, force, position, velocity) { private val timer: TimerState = timer(2.milliseconds) diff --git a/demo/constructor/src/jvmMain/kotlin/LinearDrive.kt b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt similarity index 92% rename from demo/constructor/src/jvmMain/kotlin/LinearDrive.kt rename to demo/constructor/src/jvmMain/kotlin/PidDemo.kt index f64ef72..fb0ab64 100644 --- a/demo/constructor/src/jvmMain/kotlin/LinearDrive.kt +++ b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt @@ -52,14 +52,18 @@ class LinearDrive( ) : DeviceConstructor(drive.context, meta) { val drive by device(drive) - val pid by device(PidRegulator(drive, pidParameters)) + val pid by device( + PidRegulator( + context = context, + position = drive.propertyAsState(Drive.position, 0.0), + pidParameters = pidParameters + ) + ) + + private val binding = bind(pid.output, drive.stateOfForce()) val start by device(start) val end by device(end) - - val position = drive.propertyAsState(Drive.position, Double.NaN) - - val target = pid.propertyAsState(Regulator.target, 0.0) } /** @@ -67,14 +71,14 @@ class LinearDrive( */ fun LinearDrive( context: Context, - positionState: DoubleInRangeState, + positionState: MutableDoubleInRangeState, mass: Double, pidParameters: PidParameters, meta: Meta = Meta.EMPTY, ): LinearDrive = LinearDrive( drive = VirtualDrive(context, mass, positionState), - start = VirtualLimitSwitch(context, positionState.atStart), - end = VirtualLimitSwitch(context, positionState.atEnd), + start = LimitSwitch(context, positionState.atStart), + end = LimitSwitch(context, positionState.atEnd), pidParameters = pidParameters, meta = meta ) @@ -116,7 +120,7 @@ fun main() = application { mutableStateOf(PidParameters(kp = 2.5, ki = 0.0, kd = -0.1, timeStep = 0.005.seconds)) } - val state = remember { DoubleInRangeState(0.0, -6.0..6.0) } + val state = remember { MutableDoubleInRangeState(0.0, -6.0..6.0) } val linearDrive = remember { context.install( @@ -128,7 +132,7 @@ fun main() = application { val modulator = remember { context.install( "modulator", - Modulator(context, linearDrive.target) + Modulator(context, linearDrive.pid.target) ) } @@ -207,7 +211,7 @@ fun main() = application { Slider( pidParameters.timeStep.toDouble(DurationUnit.MILLISECONDS).toFloat(), { pidParameters = pidParameters.copy(timeStep = it.toDouble().milliseconds) }, - valueRange = 0f..100f, + valueRange = 1f..100f, steps = 100 ) } @@ -248,14 +252,14 @@ fun main() = application { lineStyle = LineStyle(SolidColor(Color.Blue)) ) PlotDeviceProperty( - linearDrive.pid, - Regulator.position, + linearDrive.drive, + Drive.position, maxAge = maxAge, sampling = 50.milliseconds, ) - PlotDeviceProperty( - linearDrive.pid, - Regulator.target, + PlotNumberState( + context = context, + state = linearDrive.pid.target, maxAge = maxAge, sampling = 50.milliseconds, lineStyle = LineStyle(SolidColor(Color.Red)) From d0e3faea8818d27cb80529f0a596d472bd702f42 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 2 Jun 2024 17:58:38 +0300 Subject: [PATCH 094/125] Complete rework of PID demo and underlying devices --- .../constructor/ConstructorElement.kt | 50 ++++- .../controls/constructor/DeviceState.kt | 16 +- .../controls/constructor/devices/Drive.kt | 18 ++ .../constructor/devices/EncoderDevice.kt | 20 ++ .../{library => devices}/LimitSwitch.kt | 3 +- .../constructor/devices/LinearDrive.kt | 38 ++++ .../controls/constructor/devices/StepDrive.kt | 61 ++++++ .../constructor/library/DoubleInRangeState.kt | 57 ----- .../controls/constructor/library/Drive.kt | 102 --------- .../constructor/library/PidRegulator.kt | 74 ------- .../controls/constructor/library/Regulator.kt | 17 -- .../controls/constructor/library/StepDrive.kt | 33 --- .../constructor/library/Transmission.kt | 33 --- .../controls/constructor/models/Inertia.kt | 104 +++++++++ .../constructor/models/PidRegulator.kt | 73 +++++++ .../controls/constructor/models/RangeState.kt | 68 ++++++ .../controls/constructor/models/Reducer.kt | 25 +++ .../controls/constructor/models/ScrewDrive.kt | 22 ++ .../constructor/units/NumericalValue.kt | 38 +++- .../constructor/units/UnitsOfMeasurement.kt | 27 ++- .../src/commonMain/kotlin/NumberTextField.kt | 59 +++++ .../src/commonMain/kotlin/TimeAxisModel.kt | 5 +- .../src/commonMain/kotlin/koalaPlots.kt | 17 +- .../constructor/src/jvmMain/kotlin/PidDemo.kt | 204 +++++++++--------- 24 files changed, 719 insertions(+), 445 deletions(-) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/Drive.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/EncoderDevice.kt rename controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/{library => devices}/LimitSwitch.kt (92%) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LinearDrive.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/DoubleInRangeState.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/StepDrive.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Transmission.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/PidRegulator.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/RangeState.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Reducer.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt create mode 100644 controls-visualisation-compose/src/commonMain/kotlin/NumberTextField.kt diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt index 0aa72a0..8073689 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt @@ -3,11 +3,13 @@ package space.kscience.controls.constructor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* +import kotlinx.datetime.Instant import space.kscience.controls.api.Device import space.kscience.controls.manager.ClockManager import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.request import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds /** * A binding that is used to describe device functionality @@ -36,7 +38,7 @@ public class ConnectionConstrucorElement( ) : ConstructorElement public class ModelConstructorElement( - public val model: ModelConstructor + public val model: ModelConstructor, ) : ConstructorElement @@ -77,15 +79,15 @@ public interface StateContainer : ContextAware, CoroutineScope { /** * Register a [state] in this container. The state is not registered as a device property if [this] is a [DeviceConstructor] */ -public fun <T, D : DeviceState<T>> StateContainer.state(state: D): D { +public fun <T, D : DeviceState<T>> StateContainer.registerState(state: D): D { registerElement(StateConstructorElement(state)) return state } /** - * Create a register a [MutableDeviceState] with a given [converter] + * Create a register a [MutableDeviceState] */ -public fun <T> StateContainer.stateOf(initialValue: T): MutableDeviceState<T> = state( +public fun <T> StateContainer.stateOf(initialValue: T): MutableDeviceState<T> = registerState( MutableDeviceState(initialValue) ) @@ -97,23 +99,53 @@ public fun <T : ModelConstructor> StateContainer.model(model: T): T { /** * Create and register a timer state. */ -public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(context.request(ClockManager), tick)) +public fun StateContainer.timer(tick: Duration): TimerState = + registerState(TimerState(context.request(ClockManager), tick)) +/** + * Register a new timer and perform [block] on its change + */ +public fun StateContainer.onTimer( + tick: Duration, + writes: Collection<DeviceState<*>> = emptySet(), + reads: Collection<DeviceState<*>> = emptySet(), + block: suspend (prev: Instant, next: Instant) -> Unit, +): Job = timer(tick).onChange(writes = writes, reads = reads, onChange = block) + +public enum class DefaultTimer(public val duration: Duration){ + REALTIME(5.milliseconds), + VERY_FAST(10.milliseconds), + FAST(20.milliseconds), + MEDIUM(50.milliseconds), + SLOW(100.milliseconds), + VERY_SLOW(500.milliseconds), +} + +/** + * Perform an action on default timer + */ +public fun StateContainer.onTimer( + defaultTimer: DefaultTimer = DefaultTimer.FAST, + writes: Collection<DeviceState<*>> = emptySet(), + reads: Collection<DeviceState<*>> = emptySet(), + block: suspend (prev: Instant, next: Instant) -> Unit, +): Job = timer(defaultTimer.duration).onChange(writes = writes, reads = reads, onChange = block) +//TODO implement timer pooling public fun <T, R> StateContainer.mapState( origin: DeviceState<T>, transformation: (T) -> R, -): DeviceStateWithDependencies<R> = state(DeviceState.map(origin, transformation)) +): DeviceStateWithDependencies<R> = registerState(DeviceState.map(origin, transformation)) public fun <T, R> StateContainer.flowState( origin: DeviceState<T>, initialValue: R, - transformation: suspend FlowCollector<R>.(T) -> Unit + transformation: suspend FlowCollector<R>.(T) -> Unit, ): DeviceStateWithDependencies<R> { val state = MutableDeviceState(initialValue) origin.valueFlow.transform(transformation).onEach { state.value = it }.launchIn(this) - return state(state.withDependencies(setOf(origin))) + return registerState(state.withDependencies(setOf(origin))) } /** @@ -123,7 +155,7 @@ public fun <T1, T2, R> StateContainer.combineState( first: DeviceState<T1>, second: DeviceState<T2>, transformation: (T1, T2) -> R, -): DeviceState<R> = state(DeviceState.combine(first, second, transformation)) +): DeviceState<R> = registerState(DeviceState.combine(first, second, transformation)) /** * Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically 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 5083347..c326565 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 @@ -6,12 +6,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.controls.constructor.units.UnitsOfMeasurement import kotlin.reflect.KProperty /** * An observable state of a device */ -public interface DeviceState<T> { +public interface DeviceState<out T> { public val value: T public val valueFlow: Flow<T> @@ -49,7 +51,7 @@ public interface DeviceStateWithDependencies<T> : DeviceState<T> { } public fun <T> DeviceState<T>.withDependencies( - dependencies: Collection<DeviceState<*>> + dependencies: Collection<DeviceState<*>>, ): DeviceStateWithDependencies<T> = object : DeviceStateWithDependencies<T>, DeviceState<T> by this { override val dependencies: Collection<DeviceState<*>> = dependencies } @@ -72,6 +74,16 @@ public fun <T, R> DeviceState.Companion.map( public fun <T, R> DeviceState<T>.map(mapper: (T) -> R): DeviceStateWithDependencies<R> = DeviceState.map(this, mapper) +public fun DeviceState<out NumericalValue<out UnitsOfMeasurement>>.values(): DeviceState<Double> = object : DeviceState<Double> { + override val value: Double + get() = this@values.value.value + + override val valueFlow: Flow<Double> + get() = this@values.valueFlow.map { it.value } + + override fun toString(): String = this@values.toString() +} + /** * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used. */ diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/Drive.kt new file mode 100644 index 0000000..3436856 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/Drive.kt @@ -0,0 +1,18 @@ +package space.kscience.controls.constructor.devices + +import space.kscience.controls.constructor.DeviceConstructor +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.property +import space.kscience.controls.constructor.units.NewtonsMeters +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.controls.constructor.units.numerical +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.MetaConverter + + +public class Drive( + context: Context, + force: MutableDeviceState<NumericalValue<NewtonsMeters>> = MutableDeviceState(NumericalValue(0)), +) : DeviceConstructor(context) { + public val force: MutableDeviceState<NumericalValue<NewtonsMeters>> by property(MetaConverter.numerical(), force) +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/EncoderDevice.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/EncoderDevice.kt new file mode 100644 index 0000000..cb73cff --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/EncoderDevice.kt @@ -0,0 +1,20 @@ +package space.kscience.controls.constructor.devices + +import space.kscience.controls.constructor.DeviceConstructor +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.property +import space.kscience.controls.constructor.units.Degrees +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.controls.constructor.units.numerical +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.MetaConverter + +/** + * An encoder that can read an angle + */ +public class EncoderDevice( + context: Context, + position: DeviceState<NumericalValue<Degrees>> +) : DeviceConstructor(context) { + public val position: DeviceState<NumericalValue<Degrees>> by property(MetaConverter.numerical<Degrees>(), position) +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LimitSwitch.kt similarity index 92% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LimitSwitch.kt index 919d026..cba6b83 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LimitSwitch.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.constructor.library +package space.kscience.controls.constructor.devices import space.kscience.controls.constructor.DeviceConstructor import space.kscience.controls.constructor.DeviceState @@ -7,7 +7,6 @@ import space.kscience.controls.constructor.registerAsProperty import space.kscience.controls.constructor.units.Direction import space.kscience.controls.constructor.units.NumericalValue import space.kscience.controls.constructor.units.UnitsOfMeasurement -import space.kscience.controls.constructor.units.compareTo import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.booleanProperty diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LinearDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LinearDrive.kt new file mode 100644 index 0000000..51039d1 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LinearDrive.kt @@ -0,0 +1,38 @@ +package space.kscience.controls.constructor.devices + +import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.models.PidParameters +import space.kscience.controls.constructor.models.PidRegulator +import space.kscience.controls.constructor.units.Meters +import space.kscience.controls.constructor.units.NewtonsMeters +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.controls.constructor.units.numerical +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaConverter + +public class LinearDrive( + drive: Drive, + start: LimitSwitch, + end: LimitSwitch, + position: DeviceState<NumericalValue<Meters>>, + pidParameters: PidParameters, + context: Context = drive.context, + meta: Meta = Meta.EMPTY, +) : DeviceConstructor(context, meta) { + + public val position: DeviceState<NumericalValue<Meters>> by property(MetaConverter.numerical(), position) + + public val drive: Drive by device(drive) + public val pid: PidRegulator<Meters, NewtonsMeters> = model( + PidRegulator( + context = context, + position = position, + output = drive.force, + pidParameters = pidParameters + ) + ) + + public val startLimit: LimitSwitch by device(start) + public val endLimit: LimitSwitch by device(end) +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt new file mode 100644 index 0000000..072789b --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt @@ -0,0 +1,61 @@ +package space.kscience.controls.constructor.devices + +import kotlinx.coroutines.launch +import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.units.Degrees +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.controls.constructor.units.plus +import space.kscience.controls.constructor.units.times +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.MetaConverter +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.time.DurationUnit + +/** + * A step drive + * + * @param speed ticks per second + * @param target target ticks state + * @param writeTicks a hardware callback + */ +public class StepDrive( + context: Context, + speed: MutableDeviceState<Double>, + target: MutableDeviceState<Int> = MutableDeviceState(0), + private val writeTicks: suspend (ticks: Int, speed: Double) -> Unit, +) : DeviceConstructor(context) { + + public val target: MutableDeviceState<Int> by property(MetaConverter.int, target) + + public val speed: MutableDeviceState<Double> by property(MetaConverter.double, speed) + + private val positionState = stateOf(target.value) + + public val position: DeviceState<Int> by property(MetaConverter.int, positionState) + + private val ticker = onTimer { prev, next -> + val tickSpeed = speed.value + val timeDelta = (next - prev).toDouble(DurationUnit.SECONDS) + val ticksDelta: Int = target.value - position.value + val steps: Int = when { + ticksDelta > 0 -> min(ticksDelta, (timeDelta * tickSpeed).roundToInt()) + ticksDelta < 0 -> max(ticksDelta, (timeDelta * tickSpeed).roundToInt()) + else -> return@onTimer + } + launch { + writeTicks(steps, tickSpeed) + positionState.value += steps + } + } +} + +/** + * Compute a state using given tick-to-angle transformation + */ +public fun StepDrive.angle( + zero: NumericalValue<Degrees>, + step: NumericalValue<Degrees>, +): DeviceState<NumericalValue<Degrees>> = position.map { zero + it * step } + diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/DoubleInRangeState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/DoubleInRangeState.kt deleted file mode 100644 index 10773da..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/DoubleInRangeState.kt +++ /dev/null @@ -1,57 +0,0 @@ -package space.kscience.controls.constructor.library - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import space.kscience.controls.constructor.DeviceState -import space.kscience.controls.constructor.MutableDeviceState -import space.kscience.controls.constructor.map - -/** - * A state describing a [Double] value in the [range] - */ -public open class DoubleInRangeState( - private val input: DeviceState<Double>, - public val range: ClosedFloatingPointRange<Double>, -) : DeviceState<Double> { - - override val valueFlow: Flow<Double> get() = input.valueFlow.map { it.coerceIn(range) } - - override val value: Double get() = input.value.coerceIn(range) - - /** - * A state showing that the range is on its lower boundary - */ - public val atStart: DeviceState<Boolean> = input.map { it <= range.start } - - /** - * A state showing that the range is on its higher boundary - */ - public val atEnd: DeviceState<Boolean> = input.map { it >= range.endInclusive } - - override fun toString(): String = "DoubleRangeState(value=${value},range=$range)" -} - -public class MutableDoubleInRangeState( - private val mutableInput: MutableDeviceState<Double>, - range: ClosedFloatingPointRange<Double> -) : DoubleInRangeState(mutableInput, range), MutableDeviceState<Double> { - override var value: Double - get() = super.value - set(value) { - mutableInput.value = value.coerceIn(range) - } -} - -public fun MutableDoubleInRangeState( - initialValue: Double, - range: ClosedFloatingPointRange<Double> -): MutableDoubleInRangeState = MutableDoubleInRangeState(MutableDeviceState(initialValue),range) - - -public fun DeviceState<Double>.coerceIn( - range: ClosedFloatingPointRange<Double> -): DoubleInRangeState = DoubleInRangeState(this, range) - -public fun MutableDeviceState<Double>.coerceIn( - range: ClosedFloatingPointRange<Double> -): MutableDoubleInRangeState = MutableDoubleInRangeState(this, range) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt deleted file mode 100644 index 77a16ca..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt +++ /dev/null @@ -1,102 +0,0 @@ -package space.kscience.controls.constructor.library - -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import space.kscience.controls.api.Device -import space.kscience.controls.constructor.MutableDeviceState -import space.kscience.controls.constructor.propertyAsState -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 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<Drive>() { - public val force: MutableDevicePropertySpec<Drive, Double> by Drive.mutableProperty( - MetaConverter.double, - Drive::force - ) - - public val position: DevicePropertySpec<Drive, Double> by doubleProperty { position } - } -} - -/** - * A virtual drive - */ -public class VirtualDrive( - context: Context, - private val mass: Double, - public val positionState: MutableDeviceState<Double>, -) : Drive, DeviceBySpec<Drive>(Drive, context) { - - private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds - private val clock = context.clock - - override var force: Double = 0.0 - - override val position: Double get() = positionState.value - - 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 - 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 - } - } - } - - override suspend fun onStop() { - updateJob?.cancel() - } - - public companion object { - public fun factory( - mass: Double, - positionState: MutableDeviceState<Double>, - ): Factory<Drive> = Factory { context, _ -> - VirtualDrive(context, mass, positionState) - } - } -} - -public fun Drive.stateOfForce(initialForce: Double = 0.0): MutableDeviceState<Double> = - propertyAsState(Drive.force, initialForce) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt deleted file mode 100644 index 27c3f95..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt +++ /dev/null @@ -1,74 +0,0 @@ -package space.kscience.controls.constructor.library - -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import space.kscience.controls.constructor.DeviceState -import space.kscience.controls.constructor.MutableDeviceState -import space.kscience.controls.constructor.state -import space.kscience.controls.constructor.stateOf -import space.kscience.controls.manager.clock -import space.kscience.dataforge.context.Context -import kotlin.time.Duration -import kotlin.time.DurationUnit - - -/** - * Pid regulator parameters - */ -public data class PidParameters( - val kp: Double, - val ki: Double, - val kd: Double, - val timeStep: Duration, -) - -/** - * A PID regulator - */ -public class PidRegulator( - context: Context, - private val position: DeviceState<Double>, - public var pidParameters: PidParameters, // TODO expose as property - output: MutableDeviceState<Double> = MutableDeviceState(0.0), -) : Regulator<Double>(context) { - - override val target: MutableDeviceState<Double> = stateOf(0.0) - override val output: MutableDeviceState<Double> = state(output) - - private var lastPosition: Double = target.value - - private var integral: Double = 0.0 - - private val mutex = Mutex() - - private var lastTime = clock.now() - - private val updateJob = launch { - while (isActive) { - delay(pidParameters.timeStep) - mutex.withLock { - val realTime = clock.now() - val delta = target.value - position.value - val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) - integral += delta * dtSeconds - val derivative = (position.value - lastPosition) / dtSeconds - - //set last time and value to new values - lastTime = realTime - lastPosition = position.value - - output.value = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative - } - } - } -} - -// -//public fun DeviceGroup.pid( -// name: String, -// drive: Drive, -// pidParameters: PidParameters, -//): PidRegulator = install(name.parseAsName(), PidRegulator(drive, pidParameters)) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt deleted file mode 100644 index 628fb0e..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt +++ /dev/null @@ -1,17 +0,0 @@ -package space.kscience.controls.constructor.library - -import space.kscience.controls.constructor.DeviceConstructor -import space.kscience.controls.constructor.DeviceState -import space.kscience.controls.constructor.MutableDeviceState -import space.kscience.dataforge.context.Context - - -/** - * A regulator with target value and current position - */ -public abstract class Regulator<T>(context: Context) : DeviceConstructor(context) { - - public abstract val target: MutableDeviceState<T> - - public abstract val output: DeviceState<T> -} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/StepDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/StepDrive.kt deleted file mode 100644 index 47a9742..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/StepDrive.kt +++ /dev/null @@ -1,33 +0,0 @@ -package space.kscience.controls.constructor.library - -import space.kscience.controls.constructor.DeviceState -import space.kscience.controls.constructor.MutableDeviceState -import space.kscience.controls.constructor.combineState -import space.kscience.controls.constructor.property -import space.kscience.controls.constructor.units.* -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.meta.MetaConverter - -/** - * A step drive regulated by [input] - */ -public class StepDrive( - context: Context, - public val step: NumericalValue<Degrees>, - public val zero: NumericalValue<Degrees> = NumericalValue(0.0), - direction: Direction = Direction.UP, - input: MutableDeviceState<Int> = MutableDeviceState(0), - hold: MutableDeviceState<Boolean> = MutableDeviceState(false) -) : Transmission<Int, NumericalValue<Degrees>>(context) { - - override val input: MutableDeviceState<Int> by property(MetaConverter.int, input) - - public val hold: MutableDeviceState<Boolean> by property(MetaConverter.boolean, hold) - - override val output: DeviceState<NumericalValue<Degrees>> = combineState( - input, hold - ) { input, hold -> - //TODO use hold parameter - zero + input * direction.coef * step - } -} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Transmission.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Transmission.kt deleted file mode 100644 index 5974a2f..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Transmission.kt +++ /dev/null @@ -1,33 +0,0 @@ -package space.kscience.controls.constructor.library - -import space.kscience.controls.constructor.DeviceConstructor -import space.kscience.controls.constructor.DeviceState -import space.kscience.controls.constructor.MutableDeviceState -import space.kscience.controls.constructor.flowState -import space.kscience.dataforge.context.Context - - -/** - * A model for a device that converts one type of physical quantity to another type - */ -public abstract class Transmission<T, R>(context: Context) : DeviceConstructor(context) { - public abstract val input: MutableDeviceState<T> - public abstract val output: DeviceState<R> - - public companion object { - /** - * Create a device that is a hard connection between two physical quantities - */ - public suspend fun <T, R> direct( - context: Context, - input: MutableDeviceState<T>, - transform: suspend (T) -> R - ): Transmission<T, R> { - val initialValue = transform(input.value) - return object : Transmission<T, R>(context) { - override val input: MutableDeviceState<T> = input - override val output: DeviceState<R> = flowState(input, initialValue) { emit(transform(it)) } - } - } - } -} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt new file mode 100644 index 0000000..d6c3fc7 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt @@ -0,0 +1,104 @@ +package space.kscience.controls.constructor.models + +import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.units.* +import space.kscience.dataforge.context.Context +import kotlin.math.pow +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit + +/** + * A model for inertial movement. Both linear and angular + */ +public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>( + context: Context, + force: DeviceState<Double>, //TODO add system unit sets + inertia: Double, + public val position: MutableDeviceState<NumericalValue<U>>, + public val velocity: MutableDeviceState<NumericalValue<V>>, + timerPrecision: Duration = 10.milliseconds, +) : ModelConstructor(context) { + + init { + registerState(position) + registerState(velocity) + } + + private val movementTimer = timer(timerPrecision) + + private var currentForce = force.value + + private val movement = movementTimer.onChange { prev, next -> + val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS) + + // compute new value based on velocity and acceleration from the previous step + position.value += NumericalValue(velocity.value.value * dtSeconds + currentForce / inertia * dtSeconds.pow(2) / 2) + + // compute new velocity based on acceleration on the previous step + velocity.value += NumericalValue(currentForce / inertia * dtSeconds) + currentForce = force.value + } + + public companion object { + /** + * Linear inertial model with [force] in newtons and [mass] in kilograms + */ + public fun linear( + context: Context, + force: DeviceState<NumericalValue<Newtons>>, + mass: NumericalValue<Kilograms>, + position: MutableDeviceState<NumericalValue<Meters>>, + velocity: MutableDeviceState<NumericalValue<MetersPerSecond>> = MutableDeviceState(NumericalValue(0.0)), + ): Inertia<Meters, MetersPerSecond> = Inertia( + context = context, + force = force.values(), + inertia = mass.value, + position = position, + velocity = velocity + ) +// +// +// public fun linear( +// context: Context, +// force: DeviceState<NumericalValue<Newtons>>, +// mass: NumericalValue<Kilograms>, +// initialPosition: NumericalValue<Meters>, +// initialVelocity: NumericalValue<MetersPerSecond> = NumericalValue(0), +// ): Inertia<Meters, MetersPerSecond> = Inertia( +// context = context, +// force = force.values(), +// inertia = mass.value, +// position = MutableDeviceState(initialPosition), +// velocity = MutableDeviceState(initialVelocity) +// ) + + public fun circular( + context: Context, + force: DeviceState<NumericalValue<NewtonsMeters>>, + momentOfInertia: NumericalValue<KgM2>, + position: MutableDeviceState<NumericalValue<Degrees>>, + velocity: MutableDeviceState<NumericalValue<DegreesPerSecond>> = MutableDeviceState(NumericalValue(0.0)), + ): Inertia<Degrees, DegreesPerSecond> = Inertia( + context = context, + force = force.values(), + inertia = momentOfInertia.value, + position = position, + velocity = velocity + ) +// +// public fun circular( +// context: Context, +// force: DeviceState<NumericalValue<NewtonsMeters>>, +// momentOfInertia: NumericalValue<KgM2>, +// initialPosition: NumericalValue<Degrees>, +// initialVelocity: NumericalValue<DegreesPerSecond> = NumericalValue(0), +// ): Inertia<Degrees, DegreesPerSecond> = Inertia( +// context = context, +// force = force.values(), +// inertia = momentOfInertia.value, +// position = MutableDeviceState(initialPosition), +// velocity = MutableDeviceState(initialVelocity) +// ) + } +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/PidRegulator.kt new file mode 100644 index 0000000..8c97594 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/PidRegulator.kt @@ -0,0 +1,73 @@ +package space.kscience.controls.constructor.models + +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.units.* +import space.kscience.controls.manager.clock +import space.kscience.dataforge.context.Context +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit + + +/** + * Pid regulator parameters + */ +public data class PidParameters( + val kp: Double, + val ki: Double, + val kd: Double, + val timeStep: Duration = 10.milliseconds, +) + +/** + * A PID regulator + * + * @param P units of position values + * @param O units of output values + */ +public class PidRegulator<P : UnitsOfMeasurement, O : UnitsOfMeasurement>( + context: Context, + private val position: DeviceState<NumericalValue<P>>, + public var pidParameters: PidParameters, // TODO expose as property + output: MutableDeviceState<NumericalValue<O>> = MutableDeviceState(NumericalValue(0.0)), + private val convertOutput: (NumericalValue<P>) -> NumericalValue<O> = { NumericalValue(it.value) }, +) : ModelConstructor(context) { + + public val target: MutableDeviceState<NumericalValue<P>> = stateOf(NumericalValue(0.0)) + public val output: MutableDeviceState<NumericalValue<O>> = registerState(output) + + private val updateJob = launch { + var lastPosition: NumericalValue<P> = target.value + + var integral: NumericalValue<P> = NumericalValue(0.0) + + val mutex = Mutex() + + val clock = context.clock + + var lastTime = clock.now() + + while (isActive) { + delay(pidParameters.timeStep) + mutex.withLock { + val realTime = clock.now() + val delta: NumericalValue<P> = target.value - position.value + val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) + integral += delta * dtSeconds + val derivative = (position.value - lastPosition) / dtSeconds + + //set last time and value to new values + lastTime = realTime + lastPosition = position.value + + output.value = + convertOutput(pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative) + } + } + } +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/RangeState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/RangeState.kt new file mode 100644 index 0000000..c760699 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/RangeState.kt @@ -0,0 +1,68 @@ +package space.kscience.controls.constructor.models + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.map +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.controls.constructor.units.UnitsOfMeasurement + +/** + * A state describing a [T] value in the [range] + */ +public open class RangeState<T : Comparable<T>>( + private val input: DeviceState<T>, + public val range: ClosedRange<T>, +) : DeviceState<T> { + + override val valueFlow: Flow<T> get() = input.valueFlow.map { it.coerceIn(range) } + + override val value: T get() = input.value.coerceIn(range) + + /** + * A state showing that the range is on its lower boundary + */ + public val atStart: DeviceState<Boolean> = input.map { it <= range.start } + + /** + * A state showing that the range is on its higher boundary + */ + public val atEnd: DeviceState<Boolean> = input.map { it >= range.endInclusive } + + override fun toString(): String = "DoubleRangeState(value=${value},range=$range)" +} + +public class MutableRangeState<T : Comparable<T>>( + private val mutableInput: MutableDeviceState<T>, + range: ClosedRange<T>, +) : RangeState<T>(mutableInput, range), MutableDeviceState<T> { + override var value: T + get() = super.value + set(value) { + mutableInput.value = value.coerceIn(range) + } +} + +public fun <T : Comparable<T>> MutableRangeState( + initialValue: T, + range: ClosedRange<T>, +): MutableRangeState<T> = MutableRangeState<T>(MutableDeviceState(initialValue), range) + +public fun <U : UnitsOfMeasurement> MutableRangeState( + initialValue: Double, + range: ClosedRange<Double>, +): MutableRangeState<NumericalValue<U>> = MutableRangeState( + initialValue = NumericalValue(initialValue), + range = NumericalValue<U>(range.start)..NumericalValue<U>(range.endInclusive) +) + + +public fun <T : Comparable<T>> DeviceState<T>.coerceIn( + range: ClosedFloatingPointRange<T>, +): RangeState<T> = RangeState(this, range) + + +public fun <T : Comparable<T>> MutableDeviceState<T>.coerceIn( + range: ClosedFloatingPointRange<T>, +): MutableRangeState<T> = MutableRangeState(this, range) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Reducer.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Reducer.kt new file mode 100644 index 0000000..caba5ff --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Reducer.kt @@ -0,0 +1,25 @@ +package space.kscience.controls.constructor.models + +import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.units.Degrees +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.controls.constructor.units.times +import space.kscience.dataforge.context.Context + +/** + * A reducer device used for simulations only (no public properties) + */ +public class Reducer( + context: Context, + public val ratio: Double, + public val input: DeviceState<NumericalValue<Degrees>>, + public val output: MutableDeviceState<NumericalValue<Degrees>>, +) : ModelConstructor(context) { + init { + registerState(input) + registerState(output) + transformTo(input, output) { + it * ratio + } + } +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt new file mode 100644 index 0000000..6e2f86c --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt @@ -0,0 +1,22 @@ +package space.kscience.controls.constructor.models + +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.ModelConstructor +import space.kscience.controls.constructor.map +import space.kscience.controls.constructor.units.Meters +import space.kscience.controls.constructor.units.Newtons +import space.kscience.controls.constructor.units.NewtonsMeters +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.dataforge.context.Context + +public class ScrewDrive( + context: Context, + public val leverage: NumericalValue<Meters>, +) : ModelConstructor(context) { + public fun transformForce( + stateOfForce: DeviceState<NumericalValue<NewtonsMeters>>, + ): DeviceState<NumericalValue<Newtons>> = DeviceState.map(stateOfForce) { + NumericalValue(it.value * leverage.value) + } + +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt index 98fe418..60de195 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt @@ -1,5 +1,8 @@ package space.kscience.controls.constructor.units +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.meta.double import kotlin.jvm.JvmInline @@ -7,31 +10,46 @@ import kotlin.jvm.JvmInline * A value without identity coupled to units of measurements. */ @JvmInline -public value class NumericalValue<U : UnitsOfMeasurement>(public val value: Double) +public value class NumericalValue<U : UnitsOfMeasurement>(public val value: Double): Comparable<NumericalValue<U>> { + override fun compareTo(other: NumericalValue<U>): Int = value.compareTo(other.value) -public operator fun <U: UnitsOfMeasurement> NumericalValue<U>.compareTo(other: NumericalValue<U>): Int = - value.compareTo(other.value) +} + +public fun <U : UnitsOfMeasurement> NumericalValue( + number: Number, +): NumericalValue<U> = NumericalValue(number.toDouble()) public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.plus( - other: NumericalValue<U> + other: NumericalValue<U>, ): NumericalValue<U> = NumericalValue(this.value + other.value) public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.minus( - other: NumericalValue<U> + other: NumericalValue<U>, ): NumericalValue<U> = NumericalValue(this.value - other.value) public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.times( - c: Number + c: Number, ): NumericalValue<U> = NumericalValue(this.value * c.toDouble()) public operator fun <U : UnitsOfMeasurement> Number.times( - numericalValue: NumericalValue<U> + numericalValue: NumericalValue<U>, ): NumericalValue<U> = NumericalValue(numericalValue.value * toDouble()) public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.times( - c: Double + c: Double, ): NumericalValue<U> = NumericalValue(this.value * c) public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div( - c: Number -): NumericalValue<U> = NumericalValue(this.value / c.toDouble()) \ No newline at end of file + c: Number, +): NumericalValue<U> = NumericalValue(this.value / c.toDouble()) + + +private object NumericalValueMetaConverter : MetaConverter<NumericalValue<*>> { + override fun convert(obj: NumericalValue<*>): Meta = Meta(obj.value) + + override fun readOrNull(source: Meta): NumericalValue<*>? = source.double?.let { NumericalValue<Nothing>(it) } +} + +@Suppress("UNCHECKED_CAST") +public fun <U : UnitsOfMeasurement> MetaConverter.Companion.numerical(): MetaConverter<NumericalValue<U>> = + NumericalValueMetaConverter as MetaConverter<NumericalValue<U>> \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt index ed295ef..1264423 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/UnitsOfMeasurement.kt @@ -30,8 +30,31 @@ public data object Degrees : UnitsOfAngles /**/ -public interface UnitsAngularOfVelocity : UnitsOfMeasurement +public sealed interface UnitsAngularOfVelocity : UnitsOfMeasurement public data object RadiansPerSecond : UnitsAngularOfVelocity -public data object DegreesPerSecond : UnitsAngularOfVelocity \ No newline at end of file +public data object DegreesPerSecond : UnitsAngularOfVelocity + +/**/ +public interface UnitsOfForce: UnitsOfMeasurement + +public data object Newtons: UnitsOfForce + +/**/ + +public interface UnitsOfTorque: UnitsOfMeasurement + +public data object NewtonsMeters: UnitsOfTorque + +/**/ + +public interface UnitsOfMass: UnitsOfMeasurement + +public data object Kilograms : UnitsOfMass + +/**/ + +public interface UnitsOfMomentOfInertia: UnitsOfMeasurement + +public data object KgM2: UnitsOfMomentOfInertia \ No newline at end of file diff --git a/controls-visualisation-compose/src/commonMain/kotlin/NumberTextField.kt b/controls-visualisation-compose/src/commonMain/kotlin/NumberTextField.kt new file mode 100644 index 0000000..68e61ca --- /dev/null +++ b/controls-visualisation-compose/src/commonMain/kotlin/NumberTextField.kt @@ -0,0 +1,59 @@ +package space.kscience.controls.compose + +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowLeft +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle + +@Composable +public fun NumberTextField( + value: Number, + onValueChange: (Number) -> Unit, + step: Double = 0.0, + formatter: (Number) -> String = { it.toString() }, + modifier: Modifier = Modifier, + enabled: Boolean = true, + textStyle: TextStyle = LocalTextStyle.current, + label: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null, + shape: Shape = TextFieldDefaults.shape, + colors: TextFieldColors = TextFieldDefaults.colors(), +) { + var isError by remember { mutableStateOf(false) } + + Row (verticalAlignment = Alignment.CenterVertically, modifier = modifier) { + step.takeIf { it > 0.0 }?.let { + IconButton({ onValueChange(value.toDouble() - step) }, enabled = enabled) { + Icon(Icons.Default.KeyboardArrowLeft, "decrease value") + } + } + TextField( + value = formatter(value), + onValueChange = { stringValue: String -> + val number = stringValue.toDoubleOrNull() + number?.let { onValueChange(number) } + isError = number == null + }, + isError = isError, + enabled = enabled, + textStyle = textStyle, + label = label, + supportingText = supportingText, + singleLine = true, + shape = shape, + colors = colors, + modifier = Modifier.weight(1f) + ) + step.takeIf { it > 0.0 }?.let { + IconButton({ onValueChange(value.toDouble() + step) }, enabled = enabled) { + Icon(Icons.Default.KeyboardArrowRight, "increase value") + } + } + } +} \ No newline at end of file diff --git a/controls-visualisation-compose/src/commonMain/kotlin/TimeAxisModel.kt b/controls-visualisation-compose/src/commonMain/kotlin/TimeAxisModel.kt index 2c82802..65ce8d9 100644 --- a/controls-visualisation-compose/src/commonMain/kotlin/TimeAxisModel.kt +++ b/controls-visualisation-compose/src/commonMain/kotlin/TimeAxisModel.kt @@ -19,15 +19,12 @@ public class TimeAxisModel( val currentRange = rangeProvider() val rangeLength = currentRange.endInclusive - currentRange.start val numTicks = floor(axisLength / minimumMajorTickSpacing).toInt() - val numMinorTicks = numTicks * 2 return object : TickValues<Instant> { override val majorTickValues: List<Instant> = List(numTicks) { currentRange.start + it.toDouble() / (numTicks - 1) * rangeLength } - override val minorTickValues: List<Instant> = List(numMinorTicks) { - currentRange.start + it.toDouble() / (numMinorTicks - 1) * rangeLength - } + override val minorTickValues: List<Instant> = emptyList() } } diff --git a/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt index d721f0a..855de10 100644 --- a/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt +++ b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt @@ -17,6 +17,8 @@ import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyChangedMessage import space.kscience.controls.api.propertyMessageFlow import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.controls.constructor.values import space.kscience.controls.manager.clock import space.kscience.controls.misc.ValueWithTime import space.kscience.controls.spec.DevicePropertySpec @@ -139,7 +141,7 @@ public fun XYGraphScope<Instant, Double>.PlotDeviceProperty( @Composable public fun XYGraphScope<Instant, Double>.PlotNumberState( context: Context, - state: DeviceState<out Number>, + state: DeviceState<Number>, maxAge: Duration = defaultMaxAge, maxPoints: Int = defaultMaxPoints, minPoints: Int = defaultMinPoints, @@ -163,6 +165,19 @@ public fun XYGraphScope<Instant, Double>.PlotNumberState( PlotTimeSeries(points, lineStyle) } +@Composable +public fun XYGraphScope<Instant, Double>.PlotNumericState( + context: Context, + state: DeviceState<NumericalValue<*>>, + maxAge: Duration = defaultMaxAge, + maxPoints: Int = defaultMaxPoints, + minPoints: Int = defaultMinPoints, + sampling: Duration = defaultSampling, + lineStyle: LineStyle = defaultLineStyle, +): Unit { + PlotNumberState(context, state.values(), maxAge, maxPoints, minPoints, sampling, lineStyle) +} + private fun List<Instant>.averageTime(): Instant { val min = min() diff --git a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt index fb0ab64..9129a1c 100644 --- a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt +++ b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt @@ -23,17 +23,27 @@ import kotlinx.coroutines.flow.onEach import kotlinx.datetime.Instant import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane -import space.kscience.controls.compose.PlotDeviceProperty -import space.kscience.controls.compose.PlotNumberState +import space.kscience.controls.compose.NumberTextField +import space.kscience.controls.compose.PlotNumericState import space.kscience.controls.compose.TimeAxisModel -import space.kscience.controls.constructor.* -import space.kscience.controls.constructor.library.* +import space.kscience.controls.constructor.DeviceConstructor +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.devices.Drive +import space.kscience.controls.constructor.devices.LimitSwitch +import space.kscience.controls.constructor.devices.LinearDrive +import space.kscience.controls.constructor.models.Inertia +import space.kscience.controls.constructor.models.MutableRangeState +import space.kscience.controls.constructor.models.PidParameters +import space.kscience.controls.constructor.models.ScrewDrive +import space.kscience.controls.constructor.timer +import space.kscience.controls.constructor.units.Kilograms +import space.kscience.controls.constructor.units.Meters +import space.kscience.controls.constructor.units.NumericalValue import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.clock import space.kscience.controls.manager.install import space.kscience.dataforge.context.Context -import space.kscience.dataforge.meta.Meta import java.awt.Dimension import kotlin.math.PI import kotlin.math.sin @@ -43,67 +53,79 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit -class LinearDrive( - drive: Drive, - start: LimitSwitch, - end: LimitSwitch, - pidParameters: PidParameters, - meta: Meta = Meta.EMPTY, -) : DeviceConstructor(drive.context, meta) { - - val drive by device(drive) - val pid by device( - PidRegulator( - context = context, - position = drive.propertyAsState(Drive.position, 0.0), - pidParameters = pidParameters - ) - ) - - private val binding = bind(pid.output, drive.stateOfForce()) - - val start by device(start) - val end by device(end) -} - -/** - * A shortcut to create a virtual [LimitSwitch] from [DoubleInRangeState] - */ -fun LinearDrive( - context: Context, - positionState: MutableDoubleInRangeState, - mass: Double, - pidParameters: PidParameters, - meta: Meta = Meta.EMPTY, -): LinearDrive = LinearDrive( - drive = VirtualDrive(context, mass, positionState), - start = LimitSwitch(context, positionState.atStart), - end = LimitSwitch(context, positionState.atEnd), - pidParameters = pidParameters, - meta = meta -) - class Modulator( context: Context, - target: MutableDeviceState<Double>, + target: MutableDeviceState<NumericalValue<Meters>>, var freq: Double = 0.1, var timeStep: Duration = 5.milliseconds, ) : DeviceConstructor(context) { private val clockStart = clock.now() - val timer = timer(10.milliseconds) - - private val modulation = timer.onNext { + private val modulation = timer(10.milliseconds).onNext { val timeFromStart = clock.now() - clockStart val t = timeFromStart.toDouble(DurationUnit.SECONDS) - target.value = 5 * sin(2.0 * PI * freq * t) + - sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / timeStep)) + target.value = NumericalValue( + 5 * sin(2.0 * PI * freq * t) + + sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / timeStep)) + ) } } +private val inertia = NumericalValue<Kilograms>(0.1) + +private val leverage = NumericalValue<Meters>(0.05) + private val maxAge = 10.seconds +private val range = -6.0..6.0 + +/** + * The whole physical model is here + */ +private fun createLinearDriveModel(context: Context, pidParameters: PidParameters): LinearDrive { + + //create a drive model with zero starting force + val drive = Drive(context) + + //a screw drive to converse a rotational moment into a linear one + val screwDrive = ScrewDrive(context, leverage) + + // Create a physical position coerced in a given range + val position = MutableRangeState<Meters>(0.0, range) + + /** + * Create an inertia model. + * The inertia uses drive force as input. Position is used as both input and output + * + * Force is the input parameter, position is output parameter + * + */ + val inertia = Inertia.linear( + context = context, + force = screwDrive.transformForce(drive.force), + mass = inertia, + position = position + ) + + /** + * Create a limit switches from physical position + */ + val startLimitSwitch = LimitSwitch(context, position.atStart) + val endLimitSwitch = LimitSwitch(context, position.atEnd) + + return context.install( + "linearDrive", + LinearDrive(drive, startLimitSwitch, endLimitSwitch, position, pidParameters) + ) +} + + +private fun createModulator(linearDrive: LinearDrive): Modulator = linearDrive.context.install( + "modulator", + Modulator(linearDrive.context, linearDrive.pid.target) +) + @OptIn(ExperimentalSplitPaneApi::class, ExperimentalKoalaPlotApi::class) fun main() = application { val context = remember { @@ -113,27 +135,16 @@ fun main() = application { } } - val clock = remember { context.clock } - - var pidParameters by remember { - mutableStateOf(PidParameters(kp = 2.5, ki = 0.0, kd = -0.1, timeStep = 0.005.seconds)) + mutableStateOf(PidParameters(kp = 900.0, ki = 20.0, kd = -50.0, timeStep = 0.005.seconds)) } - val state = remember { MutableDoubleInRangeState(0.0, -6.0..6.0) } - - val linearDrive = remember { - context.install( - "linearDrive", - LinearDrive(context, state, 0.05, pidParameters) - ) + val linearDrive: LinearDrive = remember { + createLinearDriveModel(context, pidParameters) } val modulator = remember { - context.install( - "modulator", - Modulator(context, linearDrive.pid.target) - ) + createModulator(linearDrive) } //bind pid parameters @@ -145,6 +156,8 @@ fun main() = application { }.collect() } + val clock = remember { context.clock } + Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) { window.minimumSize = Dimension(800, 400) MaterialTheme { @@ -153,48 +166,51 @@ fun main() = application { Column(modifier = Modifier.background(color = Color.LightGray).fillMaxHeight()) { Row { Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) - TextField( - String.format("%.2f", pidParameters.kp), - { pidParameters = pidParameters.copy(kp = it.toDouble()) }, - Modifier.width(100.dp), - enabled = false + NumberTextField( + value = pidParameters.kp, + onValueChange = { pidParameters = pidParameters.copy(kp = it.toDouble()) }, + formatter = { String.format("%.2f", it.toDouble()) }, + step = 1.0, + modifier = Modifier.width(200.dp), ) Slider( pidParameters.kp.toFloat(), { pidParameters = pidParameters.copy(kp = it.toDouble()) }, - valueRange = 0f..20f, + valueRange = 0f..1000f, steps = 100 ) } Row { Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) - TextField( - String.format("%.2f", pidParameters.ki), - { pidParameters = pidParameters.copy(ki = it.toDouble()) }, - Modifier.width(100.dp), - enabled = false + NumberTextField( + value = pidParameters.ki, + onValueChange = { pidParameters = pidParameters.copy(ki = it.toDouble()) }, + formatter = { String.format("%.2f", it.toDouble()) }, + step = 0.1, + modifier = Modifier.width(200.dp), ) Slider( pidParameters.ki.toFloat(), { pidParameters = pidParameters.copy(ki = it.toDouble()) }, - valueRange = -10f..10f, + valueRange = -100f..100f, steps = 100 ) } Row { Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) - TextField( - String.format("%.2f", pidParameters.kd), - { pidParameters = pidParameters.copy(kd = it.toDouble()) }, - Modifier.width(100.dp), - enabled = false + NumberTextField( + value = pidParameters.kd, + onValueChange = { pidParameters = pidParameters.copy(kd = it.toDouble()) }, + formatter = { String.format("%.2f", it.toDouble()) }, + step = 0.1, + modifier = Modifier.width(200.dp), ) Slider( pidParameters.kd.toFloat(), { pidParameters = pidParameters.copy(kd = it.toDouble()) }, - valueRange = -10f..10f, + valueRange = -100f..100f, steps = 100 ) } @@ -204,7 +220,7 @@ fun main() = application { TextField( pidParameters.timeStep.toString(DurationUnit.MILLISECONDS), { pidParameters = pidParameters.copy(timeStep = it.toDouble().milliseconds) }, - Modifier.width(100.dp), + Modifier.width(200.dp), enabled = false ) @@ -233,7 +249,7 @@ fun main() = application { ChartLayout { XYGraph<Instant, Double>( xAxisModel = remember { TimeAxisModel.recent(maxAge, clock) }, - yAxisModel = rememberDoubleLinearAxisModel(state.range), + yAxisModel = rememberDoubleLinearAxisModel((range.start - 1.0)..(range.endInclusive + 1.0)), xAxisTitle = { Text("Time in seconds relative to current") }, xAxisLabels = { it: Instant -> androidx.compose.material3.Text( @@ -244,20 +260,14 @@ fun main() = application { }, yAxisLabels = { it: Double -> Text(it.toString(2)) } ) { - PlotNumberState( + PlotNumericState( context = context, - state = state, + state = linearDrive.position, maxAge = maxAge, sampling = 50.milliseconds, lineStyle = LineStyle(SolidColor(Color.Blue)) ) - PlotDeviceProperty( - linearDrive.drive, - Drive.position, - maxAge = maxAge, - sampling = 50.milliseconds, - ) - PlotNumberState( + PlotNumericState( context = context, state = linearDrive.pid.target, maxAge = maxAge, @@ -273,10 +283,6 @@ fun main() = application { } 1 -> { - Text("Regulator position", color = Color.Black) - } - - 2 -> { Text("Regulator target", color = Color.Red) } } From a2b7d1ecb096e66c4ca4eea6e9fb5cbd8bdd4911 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 3 Jun 2024 13:44:20 +0300 Subject: [PATCH 095/125] Fix step drive demo --- .../controls/constructor/DeviceConstructor.kt | 8 +- .../controls/constructor/DeviceGroup.kt | 44 ++++- .../controls/constructor/DeviceState.kt | 2 +- .../controls/constructor/devices/StepDrive.kt | 27 ++- .../controls/constructor/flowState.kt | 2 +- .../controls/constructor/internalState.kt | 2 +- .../controls/constructor/models/RangeState.kt | 8 +- .../controls/constructor/models/ScrewDrive.kt | 16 +- .../controls/constructor/models/XYPosition.kt | 17 ++ .../constructor/units/NumericalValue.kt | 5 +- .../src/jvmMain/kotlin/BodyOnSprings.kt | 16 +- .../constructor/src/jvmMain/kotlin/PidDemo.kt | 40 ++-- .../constructor/src/jvmMain/kotlin/Plotter.kt | 172 ++++++++++++++++++ 13 files changed, 298 insertions(+), 61 deletions(-) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/XYPosition.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 index 00d408e..371e94f 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 @@ -32,12 +32,12 @@ public abstract class DeviceConstructor( _constructorElements.remove(constructorElement) } - override fun <T, S: DeviceState<T>> registerAsProperty( + override fun <T, S: DeviceState<T>> registerProperty( converter: MetaConverter<T>, descriptor: PropertyDescriptor, state: S, ): S { - val res = super.registerAsProperty(converter, descriptor, state) + val res = super.registerProperty(converter, descriptor, state) registerElement(PropertyConstructorElement(this, descriptor.name, state)) return res } @@ -84,7 +84,7 @@ public fun <T, S : DeviceState<T>> DeviceConstructor.property( PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) - registerAsProperty(converter, descriptor, state) + registerProperty(converter, descriptor, state) ReadOnlyProperty { _: DeviceConstructor, _ -> state } @@ -145,6 +145,6 @@ public fun <T, S : DeviceState<T>> DeviceConstructor.registerAsProperty( spec: DevicePropertySpec<*, T>, state: S, ): S { - registerAsProperty(spec.converter, spec.descriptor, state) + registerProperty(spec.converter, spec.descriptor, state) return state } 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 182d10b..a41899a 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 @@ -42,10 +42,15 @@ public open class DeviceGroup( } } - private class Action( - val invoke: suspend (Meta?) -> Meta?, + private class Action<T, R>( + val inputConverter: MetaConverter<T>, + val outputConverter: MetaConverter<R>, val descriptor: ActionDescriptor, - ) + val action: suspend (T) -> R, + ) { + suspend operator fun invoke(argument: Meta?): Meta? = argument?.let { inputConverter.readOrNull(it) } + ?.let { action(it)?.let { outputConverter.convert(it) } } + } private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>() @@ -93,7 +98,7 @@ public open class DeviceGroup( /** * Register a new property based on [DeviceState]. Properties could be modified dynamically */ - public open fun <T, S : DeviceState<T>> registerAsProperty( + public open fun <T, S : DeviceState<T>> registerProperty( converter: MetaConverter<T>, descriptor: PropertyDescriptor, state: S, @@ -112,7 +117,26 @@ public open class DeviceGroup( return state } - private val actions: MutableMap<Name, Action> = hashMapOf() + private val actions: MutableMap<Name, Action<*, *>> = hashMapOf() + + public fun <T, R> registerAction( + inputConverter: MetaConverter<T>, + outputConverter: MetaConverter<R>, + descriptor: ActionDescriptor, + action: suspend (T) -> R, + ): suspend (T) -> R { + val name = descriptor.name.parseAsName() + require(actions[name] == null) { "Can't add action with name $name. It already exists." } + actions[name] = Action( + inputConverter = inputConverter, + outputConverter = outputConverter, + descriptor = descriptor, + action = action + ) + return { + action(it) + } + } override val propertyDescriptors: Collection<PropertyDescriptor> get() = properties.values.map { it.descriptor } @@ -137,8 +161,8 @@ public open class DeviceGroup( override suspend fun execute(actionName: String, argument: Meta?): Meta? { - val action = actions[actionName] ?: error("Action with name $actionName not found") - return action.invoke(argument) + val action: Action<*, *> = actions[actionName] ?: error("Action with name $actionName not found") + return action(argument) } final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED @@ -176,7 +200,7 @@ public open class DeviceGroup( } public fun <T> DeviceGroup.registerAsProperty(propertySpec: DevicePropertySpec<*, T>, state: DeviceState<T>) { - registerAsProperty(propertySpec.converter, propertySpec.descriptor, state) + registerProperty(propertySpec.converter, propertySpec.descriptor, state) } public fun DeviceManager.registerDeviceGroup( @@ -253,7 +277,7 @@ public fun <T : Any> DeviceGroup.registerAsProperty( state: DeviceState<T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ) { - registerAsProperty( + registerProperty( converter, PropertyDescriptor(name).apply(descriptorBuilder), state @@ -269,7 +293,7 @@ public fun <T : Any> DeviceGroup.registerMutableProperty( state: MutableDeviceState<T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ) { - registerAsProperty( + registerProperty( converter, PropertyDescriptor(name).apply(descriptorBuilder), 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 c326565..846a37b 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 @@ -74,7 +74,7 @@ public fun <T, R> DeviceState.Companion.map( public fun <T, R> DeviceState<T>.map(mapper: (T) -> R): DeviceStateWithDependencies<R> = DeviceState.map(this, mapper) -public fun DeviceState<out NumericalValue<out UnitsOfMeasurement>>.values(): DeviceState<Double> = object : DeviceState<Double> { +public fun DeviceState<NumericalValue<out UnitsOfMeasurement>>.values(): DeviceState<Double> = object : DeviceState<Double> { override val value: Double get() = this@values.value.value diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt index 072789b..a8d9894 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt @@ -1,6 +1,5 @@ package space.kscience.controls.constructor.devices -import kotlinx.coroutines.launch import space.kscience.controls.constructor.* import space.kscience.controls.constructor.units.Degrees import space.kscience.controls.constructor.units.NumericalValue @@ -16,38 +15,36 @@ import kotlin.time.DurationUnit /** * A step drive * - * @param speed ticks per second + * @param ticksPerSecond ticks per second * @param target target ticks state * @param writeTicks a hardware callback */ public class StepDrive( context: Context, - speed: MutableDeviceState<Double>, + ticksPerSecond: MutableDeviceState<Double>, target: MutableDeviceState<Int> = MutableDeviceState(0), - private val writeTicks: suspend (ticks: Int, speed: Double) -> Unit, + private val writeTicks: suspend (ticks: Int, speed: Double) -> Unit = { _, _ -> }, ) : DeviceConstructor(context) { public val target: MutableDeviceState<Int> by property(MetaConverter.int, target) - public val speed: MutableDeviceState<Double> by property(MetaConverter.double, speed) + public val speed: MutableDeviceState<Double> by property(MetaConverter.double, ticksPerSecond) private val positionState = stateOf(target.value) public val position: DeviceState<Int> by property(MetaConverter.int, positionState) - private val ticker = onTimer { prev, next -> - val tickSpeed = speed.value + private val ticker = onTimer(reads = setOf(target, position), writes = setOf(position)) { prev, next -> + val tickSpeed = ticksPerSecond.value val timeDelta = (next - prev).toDouble(DurationUnit.SECONDS) val ticksDelta: Int = target.value - position.value val steps: Int = when { ticksDelta > 0 -> min(ticksDelta, (timeDelta * tickSpeed).roundToInt()) - ticksDelta < 0 -> max(ticksDelta, (timeDelta * tickSpeed).roundToInt()) + ticksDelta < 0 -> max(ticksDelta, -(timeDelta * tickSpeed).roundToInt()) else -> return@onTimer } - launch { - writeTicks(steps, tickSpeed) - positionState.value += steps - } + writeTicks(steps, tickSpeed) + positionState.value += steps } } @@ -55,7 +52,9 @@ public class StepDrive( * Compute a state using given tick-to-angle transformation */ public fun StepDrive.angle( - zero: NumericalValue<Degrees>, step: NumericalValue<Degrees>, -): DeviceState<NumericalValue<Degrees>> = position.map { zero + it * step } + zero: NumericalValue<Degrees> = NumericalValue(0), +): DeviceState<NumericalValue<Degrees>> = position.map { + zero + it.toDouble() * step +} diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt index 2bd9322..34a4910 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt @@ -10,7 +10,7 @@ private class StateFlowAsState<T>( override var value: T by flow::value override val valueFlow: Flow<T> get() = flow - override fun toString(): String = "FlowAsState()" + override fun toString(): String = "FlowAsState($value)" } /** diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt index 8bdfce1..687804e 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt @@ -23,7 +23,7 @@ private class VirtualDeviceState<T>( callback(value) } - override fun toString(): String = "VirtualDeviceState()" + override fun toString(): String = "VirtualDeviceState($value)" } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/RangeState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/RangeState.kt index c760699..202fa71 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/RangeState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/RangeState.kt @@ -16,7 +16,9 @@ public open class RangeState<T : Comparable<T>>( public val range: ClosedRange<T>, ) : DeviceState<T> { - override val valueFlow: Flow<T> get() = input.valueFlow.map { it.coerceIn(range) } + override val valueFlow: Flow<T> get() = input.valueFlow.map { + it.coerceIn(range) + } override val value: T get() = input.value.coerceIn(range) @@ -59,10 +61,10 @@ public fun <U : UnitsOfMeasurement> MutableRangeState( public fun <T : Comparable<T>> DeviceState<T>.coerceIn( - range: ClosedFloatingPointRange<T>, + range: ClosedRange<T>, ): RangeState<T> = RangeState(this, range) public fun <T : Comparable<T>> MutableDeviceState<T>.coerceIn( - range: ClosedFloatingPointRange<T>, + range: ClosedRange<T>, ): MutableRangeState<T> = MutableRangeState(this, range) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt index 6e2f86c..5a6b6bc 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt @@ -3,20 +3,26 @@ package space.kscience.controls.constructor.models import space.kscience.controls.constructor.DeviceState import space.kscience.controls.constructor.ModelConstructor import space.kscience.controls.constructor.map -import space.kscience.controls.constructor.units.Meters -import space.kscience.controls.constructor.units.Newtons -import space.kscience.controls.constructor.units.NewtonsMeters -import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.controls.constructor.units.* import space.kscience.dataforge.context.Context +import kotlin.math.PI public class ScrewDrive( context: Context, public val leverage: NumericalValue<Meters>, ) : ModelConstructor(context) { + public fun transformForce( stateOfForce: DeviceState<NumericalValue<NewtonsMeters>>, ): DeviceState<NumericalValue<Newtons>> = DeviceState.map(stateOfForce) { - NumericalValue(it.value * leverage.value) + NumericalValue(it.value * leverage.value/2/ PI) + } + + public fun transformOffset( + stateOfAngle: DeviceState<NumericalValue<Degrees>>, + offset: NumericalValue<Meters> = NumericalValue(0), + ): DeviceState<NumericalValue<Meters>> = DeviceState.map(stateOfAngle) { + offset + NumericalValue(it.value * leverage.value/2/ PI) } } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/XYPosition.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/XYPosition.kt new file mode 100644 index 0000000..15411f8 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/XYPosition.kt @@ -0,0 +1,17 @@ +package space.kscience.controls.constructor.models + +import space.kscience.controls.constructor.ModelConstructor +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.stateOf +import space.kscience.controls.constructor.units.Meters +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.dataforge.context.Context + +public class XYPosition( + context: Context, + initialX: NumericalValue<Meters> = NumericalValue(0.0), + initialY: NumericalValue<Meters> = NumericalValue(0.0), +) : ModelConstructor(context) { + public val x: MutableDeviceState<NumericalValue<Meters>> = stateOf(initialX) + public val y: MutableDeviceState<NumericalValue<Meters>> = stateOf(initialY) +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt index 60de195..e503623 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt @@ -10,7 +10,7 @@ import kotlin.jvm.JvmInline * A value without identity coupled to units of measurements. */ @JvmInline -public value class NumericalValue<U : UnitsOfMeasurement>(public val value: Double): Comparable<NumericalValue<U>> { +public value class NumericalValue<U : UnitsOfMeasurement>(public val value: Double) : Comparable<NumericalValue<U>> { override fun compareTo(other: NumericalValue<U>): Int = value.compareTo(other.value) } @@ -43,6 +43,9 @@ public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div( c: Number, ): NumericalValue<U> = NumericalValue(this.value / c.toDouble()) +public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div(other: NumericalValue<U>): Double = + value / other.value + private object NumericalValueMetaConverter : MetaConverter<NumericalValue<*>> { override fun convert(obj: NumericalValue<*>): Meta = Meta(obj.value) diff --git a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt index 7fcb5ab..e4de468 100644 --- a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt +++ b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt @@ -23,19 +23,19 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit @Serializable -data class XY(val x: Double, val y: Double) { +private data class XY(val x: Double, val y: Double) { companion object { val ZERO = XY(0.0, 0.0) } } -val XY.length: Double get() = sqrt(x.pow(2) + y.pow(2)) +private val XY.length: Double get() = sqrt(x.pow(2) + y.pow(2)) -operator fun XY.plus(other: XY): XY = XY(x + other.x, y + other.y) -operator fun XY.times(c: Double): XY = XY(x * c, y * c) -operator fun XY.div(c: Double): XY = XY(x / c, y / c) +private operator fun XY.plus(other: XY): XY = XY(x + other.x, y + other.y) +private operator fun XY.times(c: Double): XY = XY(x * c, y * c) +private operator fun XY.div(c: Double): XY = XY(x / c, y / c) -class Spring( +private class Spring( context: Context, val k: Double, val l0: Double, @@ -70,7 +70,7 @@ class Spring( } } -class MaterialPoint( +private class MaterialPoint( context: Context, val mass: Double, val force: DeviceState<XY>, @@ -94,7 +94,7 @@ class MaterialPoint( } -class BodyOnSprings( +private class BodyOnSprings( context: Context, mass: Double, k: Double, diff --git a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt index 9129a1c..a77544f 100644 --- a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt +++ b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt @@ -72,7 +72,7 @@ class Modulator( } -private val inertia = NumericalValue<Kilograms>(0.1) +private val mass = NumericalValue<Kilograms>(0.1) private val leverage = NumericalValue<Meters>(0.05) @@ -83,7 +83,13 @@ private val range = -6.0..6.0 /** * The whole physical model is here */ -private fun createLinearDriveModel(context: Context, pidParameters: PidParameters): LinearDrive { +internal fun createLinearDriveModel( + context: Context, + pidParameters: PidParameters, + mass: NumericalValue<Kilograms>, + leverage: NumericalValue<Meters>, + position: MutableRangeState<NumericalValue<Meters>>, +): LinearDrive { //create a drive model with zero starting force val drive = Drive(context) @@ -91,8 +97,6 @@ private fun createLinearDriveModel(context: Context, pidParameters: PidParameter //a screw drive to converse a rotational moment into a linear one val screwDrive = ScrewDrive(context, leverage) - // Create a physical position coerced in a given range - val position = MutableRangeState<Meters>(0.0, range) /** * Create an inertia model. @@ -101,10 +105,10 @@ private fun createLinearDriveModel(context: Context, pidParameters: PidParameter * Force is the input parameter, position is output parameter * */ - val inertia = Inertia.linear( + val inertiaModel = Inertia.linear( context = context, force = screwDrive.transformForce(drive.force), - mass = inertia, + mass = mass, position = position ) @@ -114,12 +118,12 @@ private fun createLinearDriveModel(context: Context, pidParameters: PidParameter val startLimitSwitch = LimitSwitch(context, position.atStart) val endLimitSwitch = LimitSwitch(context, position.atEnd) - return context.install( - "linearDrive", - LinearDrive(drive, startLimitSwitch, endLimitSwitch, position, pidParameters) - ) -} + /** + * Install the resulting device + */ + return LinearDrive(drive, startLimitSwitch, endLimitSwitch, position, pidParameters) +} private fun createModulator(linearDrive: LinearDrive): Modulator = linearDrive.context.install( "modulator", @@ -140,11 +144,21 @@ fun main() = application { } val linearDrive: LinearDrive = remember { - createLinearDriveModel(context, pidParameters) + context.install( + "linearDrive", + createLinearDriveModel( + context = context, + pidParameters = pidParameters, + mass = mass, + leverage = leverage, + // Create a physical position coerced in a given range + position = MutableRangeState<Meters>(0.0, range) + ) + ) } val modulator = remember { - createModulator(linearDrive) + context.install("modulator", createModulator(linearDrive)) } //bind pid parameters diff --git a/demo/constructor/src/jvmMain/kotlin/Plotter.kt b/demo/constructor/src/jvmMain/kotlin/Plotter.kt index bd0d366..9d48965 100644 --- a/demo/constructor/src/jvmMain/kotlin/Plotter.kt +++ b/demo/constructor/src/jvmMain/kotlin/Plotter.kt @@ -1,2 +1,174 @@ package space.kscience.controls.demo.constructor +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import space.kscience.controls.constructor.DeviceConstructor +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.device +import space.kscience.controls.constructor.devices.StepDrive +import space.kscience.controls.constructor.devices.angle +import space.kscience.controls.constructor.models.RangeState +import space.kscience.controls.constructor.models.ScrewDrive +import space.kscience.controls.constructor.models.coerceIn +import space.kscience.controls.constructor.units.* +import space.kscience.controls.manager.ClockManager +import space.kscience.controls.manager.DeviceManager +import space.kscience.dataforge.context.Context +import java.awt.Dimension +import kotlin.random.Random + + +class Plotter( + context: Context, + xDrive: StepDrive, + yDrive: StepDrive, + val paint: suspend (Color) -> Unit, +) : DeviceConstructor(context) { + val xDrive by device(xDrive) + val yDrive by device(yDrive) + + public fun moveToXY(x: Int, y: Int) { + xDrive.target.value = x + yDrive.target.value = y + } + + //TODO add calibration + + // TODO add draw as action +} + +suspend fun Plotter.modernArt(xRange: IntRange, yRange: IntRange) { + while (isActive){ + val randomX = Random.nextInt(xRange.first, xRange.last) + val randomY = Random.nextInt(xRange.first, xRange.last) + moveToXY(randomX, randomY) + delay(500) + paint(Color(Random.nextInt())) + } +} + +suspend fun Plotter.square(xRange: IntRange, yRange: IntRange) { + while (isActive) { + moveToXY(xRange.first, yRange.first) + delay(1000) + paint(Color.Red) + + moveToXY(xRange.first, yRange.last) + delay(1000) + paint(Color.Red) + + moveToXY(xRange.last, yRange.last) + delay(1000) + paint(Color.Red) + + moveToXY(xRange.last, yRange.first) + delay(1000) + paint(Color.Red) + } +} + +private val xRange = NumericalValue<Meters>(-0.5)..NumericalValue<Meters>(0.5) +private val yRange = NumericalValue<Meters>(-0.5)..NumericalValue<Meters>(0.5) +private val ticksPerSecond = MutableDeviceState(250.0) +private val step = NumericalValue<Degrees>(1.2) + + +private data class PlotterPoint( + val x: NumericalValue<Meters>, + val y: NumericalValue<Meters>, + val color: Color = Color.Black, +) + +suspend fun main() = application { + Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) { + window.minimumSize = Dimension(400, 400) + + val points = remember { mutableStateListOf<PlotterPoint>() } + var position by remember { mutableStateOf(PlotterPoint(NumericalValue(0), NumericalValue(0))) } + + LaunchedEffect(Unit) { + val context = Context { + plugin(DeviceManager) + plugin(ClockManager) + } + + /* Here goes the device definition block */ + + + val xScrewDrive = ScrewDrive(context, NumericalValue(0.01)) + val xDrive = StepDrive(context, ticksPerSecond) + val x: RangeState<NumericalValue<Meters>> = xScrewDrive.transformOffset(xDrive.angle(step)).coerceIn(xRange) + + val yScrewDrive = ScrewDrive(context, NumericalValue(0.01)) + val yDrive = StepDrive(context, ticksPerSecond) + val y: RangeState<NumericalValue<Meters>> = yScrewDrive.transformOffset(yDrive.angle(step)).coerceIn(yRange) + + val plotter = Plotter(context, xDrive, yDrive) { color -> + println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color") + points.add(PlotterPoint(x.value, y.value, color)) + } + + + /* Start visualization program */ + + launch { + x.valueFlow.collect { + position = position.copy(x = it) + } + } + + launch { + y.valueFlow.collect { + position = position.copy(y = it) + } + } + + launch { + val range = -100..100 + plotter.modernArt(range, range) + //plotter.square(range, range) + } + } + + + /* Here goes the visualization block */ + + MaterialTheme { + Canvas(modifier = Modifier.fillMaxSize()) { + fun toOffset(x: NumericalValue<Meters>, y: NumericalValue<Meters>): Offset { + val canvasX = (x - xRange.start) / (xRange.endInclusive - xRange.start) * size.width + val canvasY = (y - yRange.start) / (yRange.endInclusive - yRange.start) * size.height + return Offset(canvasX.toFloat(), canvasY.toFloat()) + } + + val center = toOffset(position.x, position.y) + + + drawRect( + Color.LightGray, + topLeft = Offset(0f, center.y - 5f), + size = Size(size.width, 10f) + ) + + drawCircle(Color.Black, radius = 10f, center = center) + + + points.forEach { + drawCircle(it.color, radius = 2f, center = toOffset(it.x, it.y)) + } + } + } + } + +} \ No newline at end of file From 4a5f5fab8c953bb288749ef17f6a359ee20a8a78 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 3 Jun 2024 15:38:39 +0300 Subject: [PATCH 096/125] Finish plotter demo --- .../controls/constructor/devices/Drive.kt | 1 + .../controls/constructor/devices/StepDrive.kt | 22 ++++++------ .../controls/constructor/models/Inertia.kt | 33 +---------------- .../controls/constructor/models/ScrewDrive.kt | 14 ++++---- .../controls/constructor/models/XYPosition.kt | 17 --------- .../constructor/models/coordinates.kt | 8 +++++ .../constructor/src/jvmMain/kotlin/PidDemo.kt | 6 ++-- .../constructor/src/jvmMain/kotlin/Plotter.kt | 36 +++++++++++-------- 8 files changed, 53 insertions(+), 84 deletions(-) delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/XYPosition.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/Drive.kt index 3436856..9880508 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/Drive.kt @@ -9,6 +9,7 @@ import space.kscience.controls.constructor.units.numerical import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.MetaConverter +//TODO use current as input public class Drive( context: Context, diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt index a8d9894..be32f07 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt @@ -9,7 +9,7 @@ import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.MetaConverter import kotlin.math.max import kotlin.math.min -import kotlin.math.roundToInt +import kotlin.math.roundToLong import kotlin.time.DurationUnit /** @@ -22,25 +22,27 @@ import kotlin.time.DurationUnit public class StepDrive( context: Context, ticksPerSecond: MutableDeviceState<Double>, - target: MutableDeviceState<Int> = MutableDeviceState(0), - private val writeTicks: suspend (ticks: Int, speed: Double) -> Unit = { _, _ -> }, + target: MutableDeviceState<Long> = MutableDeviceState(0), + private val writeTicks: suspend (ticks: Long, speed: Double) -> Unit = { _, _ -> }, ) : DeviceConstructor(context) { - public val target: MutableDeviceState<Int> by property(MetaConverter.int, target) + public val target: MutableDeviceState<Long> by property(MetaConverter.long, target) public val speed: MutableDeviceState<Double> by property(MetaConverter.double, ticksPerSecond) private val positionState = stateOf(target.value) - public val position: DeviceState<Int> by property(MetaConverter.int, positionState) + public val position: DeviceState<Long> by property(MetaConverter.long, positionState) + + //FIXME round to zero problem private val ticker = onTimer(reads = setOf(target, position), writes = setOf(position)) { prev, next -> val tickSpeed = ticksPerSecond.value val timeDelta = (next - prev).toDouble(DurationUnit.SECONDS) - val ticksDelta: Int = target.value - position.value - val steps: Int = when { - ticksDelta > 0 -> min(ticksDelta, (timeDelta * tickSpeed).roundToInt()) - ticksDelta < 0 -> max(ticksDelta, -(timeDelta * tickSpeed).roundToInt()) + val ticksDelta: Long = target.value - position.value + val steps: Long = when { + ticksDelta > 0 -> min(ticksDelta, (timeDelta * tickSpeed).roundToLong()) + ticksDelta < 0 -> max(ticksDelta, -(timeDelta * tickSpeed).roundToLong()) else -> return@onTimer } writeTicks(steps, tickSpeed) @@ -55,6 +57,6 @@ public fun StepDrive.angle( step: NumericalValue<Degrees>, zero: NumericalValue<Degrees> = NumericalValue(0), ): DeviceState<NumericalValue<Degrees>> = position.map { - zero + it.toDouble() * step + zero + it * step } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt index d6c3fc7..88a377e 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt @@ -25,11 +25,9 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>( registerState(velocity) } - private val movementTimer = timer(timerPrecision) - private var currentForce = force.value - private val movement = movementTimer.onChange { prev, next -> + private val movement = onTimer (timerPrecision) { prev, next -> val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS) // compute new value based on velocity and acceleration from the previous step @@ -57,21 +55,6 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>( position = position, velocity = velocity ) -// -// -// public fun linear( -// context: Context, -// force: DeviceState<NumericalValue<Newtons>>, -// mass: NumericalValue<Kilograms>, -// initialPosition: NumericalValue<Meters>, -// initialVelocity: NumericalValue<MetersPerSecond> = NumericalValue(0), -// ): Inertia<Meters, MetersPerSecond> = Inertia( -// context = context, -// force = force.values(), -// inertia = mass.value, -// position = MutableDeviceState(initialPosition), -// velocity = MutableDeviceState(initialVelocity) -// ) public fun circular( context: Context, @@ -86,19 +69,5 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>( position = position, velocity = velocity ) -// -// public fun circular( -// context: Context, -// force: DeviceState<NumericalValue<NewtonsMeters>>, -// momentOfInertia: NumericalValue<KgM2>, -// initialPosition: NumericalValue<Degrees>, -// initialVelocity: NumericalValue<DegreesPerSecond> = NumericalValue(0), -// ): Inertia<Degrees, DegreesPerSecond> = Inertia( -// context = context, -// force = force.values(), -// inertia = momentOfInertia.value, -// position = MutableDeviceState(initialPosition), -// velocity = MutableDeviceState(initialVelocity) -// ) } } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt index 5a6b6bc..573a648 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt @@ -12,17 +12,17 @@ public class ScrewDrive( public val leverage: NumericalValue<Meters>, ) : ModelConstructor(context) { - public fun transformForce( - stateOfForce: DeviceState<NumericalValue<NewtonsMeters>>, - ): DeviceState<NumericalValue<Newtons>> = DeviceState.map(stateOfForce) { - NumericalValue(it.value * leverage.value/2/ PI) + public fun torqueToForce( + stateOfMomentum: DeviceState<NumericalValue<NewtonsMeters>>, + ): DeviceState<NumericalValue<Newtons>> = DeviceState.map(stateOfMomentum) { momentum -> + NumericalValue(momentum.value / leverage.value ) } - public fun transformOffset( + public fun degreesToMeters( stateOfAngle: DeviceState<NumericalValue<Degrees>>, offset: NumericalValue<Meters> = NumericalValue(0), - ): DeviceState<NumericalValue<Meters>> = DeviceState.map(stateOfAngle) { - offset + NumericalValue(it.value * leverage.value/2/ PI) + ): DeviceState<NumericalValue<Meters>> = DeviceState.map(stateOfAngle) { degrees -> + offset + NumericalValue(degrees.value * 2 * PI / 360 *leverage.value ) } } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/XYPosition.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/XYPosition.kt deleted file mode 100644 index 15411f8..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/XYPosition.kt +++ /dev/null @@ -1,17 +0,0 @@ -package space.kscience.controls.constructor.models - -import space.kscience.controls.constructor.ModelConstructor -import space.kscience.controls.constructor.MutableDeviceState -import space.kscience.controls.constructor.stateOf -import space.kscience.controls.constructor.units.Meters -import space.kscience.controls.constructor.units.NumericalValue -import space.kscience.dataforge.context.Context - -public class XYPosition( - context: Context, - initialX: NumericalValue<Meters> = NumericalValue(0.0), - initialY: NumericalValue<Meters> = NumericalValue(0.0), -) : ModelConstructor(context) { - public val x: MutableDeviceState<NumericalValue<Meters>> = stateOf(initialX) - public val y: MutableDeviceState<NumericalValue<Meters>> = stateOf(initialY) -} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt new file mode 100644 index 0000000..50db92b --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt @@ -0,0 +1,8 @@ +package space.kscience.controls.constructor.models + +import space.kscience.controls.constructor.units.NumericalValue +import space.kscience.controls.constructor.units.UnitsOfMeasurement + +public data class XY<U: UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>) + +public data class XYZ<U: UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>, val z: NumericalValue<U>) \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt index a77544f..b19e179 100644 --- a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt +++ b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt @@ -94,7 +94,7 @@ internal fun createLinearDriveModel( //create a drive model with zero starting force val drive = Drive(context) - //a screw drive to converse a rotational moment into a linear one + //a screw drive to convert a rotational moment into a force val screwDrive = ScrewDrive(context, leverage) @@ -107,7 +107,7 @@ internal fun createLinearDriveModel( */ val inertiaModel = Inertia.linear( context = context, - force = screwDrive.transformForce(drive.force), + force = screwDrive.torqueToForce(drive.force), mass = mass, position = position ) @@ -266,7 +266,7 @@ fun main() = application { yAxisModel = rememberDoubleLinearAxisModel((range.start - 1.0)..(range.endInclusive + 1.0)), xAxisTitle = { Text("Time in seconds relative to current") }, xAxisLabels = { it: Instant -> - androidx.compose.material3.Text( + Text( (clock.now() - it).toDouble( DurationUnit.SECONDS ).toString(2) diff --git a/demo/constructor/src/jvmMain/kotlin/Plotter.kt b/demo/constructor/src/jvmMain/kotlin/Plotter.kt index 9d48965..b605dd4 100644 --- a/demo/constructor/src/jvmMain/kotlin/Plotter.kt +++ b/demo/constructor/src/jvmMain/kotlin/Plotter.kt @@ -18,7 +18,6 @@ import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.device import space.kscience.controls.constructor.devices.StepDrive import space.kscience.controls.constructor.devices.angle -import space.kscience.controls.constructor.models.RangeState import space.kscience.controls.constructor.models.ScrewDrive import space.kscience.controls.constructor.models.coerceIn import space.kscience.controls.constructor.units.* @@ -38,21 +37,26 @@ class Plotter( val xDrive by device(xDrive) val yDrive by device(yDrive) - public fun moveToXY(x: Int, y: Int) { - xDrive.target.value = x - yDrive.target.value = y + public fun moveToXY(x: Number, y: Number) { + xDrive.target.value = x.toLong() + yDrive.target.value = y.toLong() } +// val position = combineState(xDrive.position, yDrive.position) { x, y -> +// space.kscience.controls.constructor.models.XY(x, y) +// } + //TODO add calibration // TODO add draw as action } suspend fun Plotter.modernArt(xRange: IntRange, yRange: IntRange) { - while (isActive){ + while (isActive) { val randomX = Random.nextInt(xRange.first, xRange.last) - val randomY = Random.nextInt(xRange.first, xRange.last) + val randomY = Random.nextInt(yRange.first, yRange.last) moveToXY(randomX, randomY) + //TODO wait for position instead of custom delay delay(500) paint(Color(Random.nextInt())) } @@ -80,8 +84,8 @@ suspend fun Plotter.square(xRange: IntRange, yRange: IntRange) { private val xRange = NumericalValue<Meters>(-0.5)..NumericalValue<Meters>(0.5) private val yRange = NumericalValue<Meters>(-0.5)..NumericalValue<Meters>(0.5) -private val ticksPerSecond = MutableDeviceState(250.0) -private val step = NumericalValue<Degrees>(1.2) +private val ticksPerSecond = MutableDeviceState(3000.0) +private val step = NumericalValue<Degrees>(1.8) private data class PlotterPoint( @@ -106,13 +110,13 @@ suspend fun main() = application { /* Here goes the device definition block */ - val xScrewDrive = ScrewDrive(context, NumericalValue(0.01)) val xDrive = StepDrive(context, ticksPerSecond) - val x: RangeState<NumericalValue<Meters>> = xScrewDrive.transformOffset(xDrive.angle(step)).coerceIn(xRange) + val xTransmission = ScrewDrive(context, NumericalValue(0.01)) + val x = xTransmission.degreesToMeters(xDrive.angle(step)).coerceIn(xRange) - val yScrewDrive = ScrewDrive(context, NumericalValue(0.01)) val yDrive = StepDrive(context, ticksPerSecond) - val y: RangeState<NumericalValue<Meters>> = yScrewDrive.transformOffset(yDrive.angle(step)).coerceIn(yRange) + val yTransmission = ScrewDrive(context, NumericalValue(0.01)) + val y = yTransmission.degreesToMeters(yDrive.angle(step)).coerceIn(yRange) val plotter = Plotter(context, xDrive, yDrive) { color -> println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color") @@ -134,10 +138,12 @@ suspend fun main() = application { } } + /* run program */ + launch { - val range = -100..100 - plotter.modernArt(range, range) - //plotter.square(range, range) + val range = -1000..1000 +// plotter.modernArt(range, range) + plotter.square(range, range) } } From 9f21a14f96fae6588f4802903ca8dfb829db4fc7 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 3 Jun 2024 18:02:57 +0300 Subject: [PATCH 097/125] Fix ball on springs demo --- .../controls/constructor/models/Inertia.kt | 5 +- .../constructor/models/MaterialPoint.kt | 42 ++++++ .../constructor/models/coordinates.kt | 8 -- .../constructor/units/NumericalValue.kt | 2 + .../controls/constructor/units/coordinates.kt | 36 +++++ .../src/jvmMain/kotlin/BodyOnSprings.kt | 134 ++++++------------ 6 files changed, 122 insertions(+), 105 deletions(-) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt index 88a377e..1c6a507 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt @@ -4,8 +4,6 @@ import space.kscience.controls.constructor.* import space.kscience.controls.constructor.units.* import space.kscience.dataforge.context.Context import kotlin.math.pow -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit /** @@ -17,7 +15,6 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>( inertia: Double, public val position: MutableDeviceState<NumericalValue<U>>, public val velocity: MutableDeviceState<NumericalValue<V>>, - timerPrecision: Duration = 10.milliseconds, ) : ModelConstructor(context) { init { @@ -27,7 +24,7 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>( private var currentForce = force.value - private val movement = onTimer (timerPrecision) { prev, next -> + private val movement = onTimer { prev, next -> val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS) // compute new value based on velocity and acceleration from the previous step diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt new file mode 100644 index 0000000..c03841c --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt @@ -0,0 +1,42 @@ +package space.kscience.controls.constructor.models + +import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.units.* +import space.kscience.dataforge.context.Context +import kotlin.math.pow +import kotlin.time.DurationUnit + + +/** + * 3D material point + */ +public class MaterialPoint( + context: Context, + force: DeviceState<XYZ<Newtons>>, + mass: NumericalValue<Kilograms>, + public val position: MutableDeviceState<XYZ<Meters>>, + public val velocity: MutableDeviceState<XYZ<MetersPerSecond>>, +) : ModelConstructor(context) { + + init { + registerState(position) + registerState(velocity) + } + + private var currentForce = force.value + + private val movement = onTimer( + reads = setOf(velocity, position), + writes = setOf(velocity, position) + ) { prev, next -> + val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS) + + // compute new value based on velocity and acceleration from the previous step + position.value += (velocity.value * dtSeconds).cast(Meters) + + (currentForce / mass.value * dtSeconds.pow(2) / 2).cast(Meters) + + // compute new velocity based on acceleration on the previous step + velocity.value += (currentForce / mass.value * dtSeconds).cast(MetersPerSecond) + currentForce = force.value + } +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt deleted file mode 100644 index 50db92b..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt +++ /dev/null @@ -1,8 +0,0 @@ -package space.kscience.controls.constructor.models - -import space.kscience.controls.constructor.units.NumericalValue -import space.kscience.controls.constructor.units.UnitsOfMeasurement - -public data class XY<U: UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>) - -public data class XYZ<U: UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>, val z: NumericalValue<U>) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt index e503623..8e77001 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt @@ -46,6 +46,8 @@ public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div( public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div(other: NumericalValue<U>): Double = value / other.value +public operator fun <U: UnitsOfMeasurement> NumericalValue<U>.unaryMinus(): NumericalValue<U> = NumericalValue(-value) + private object NumericalValueMetaConverter : MetaConverter<NumericalValue<*>> { override fun convert(obj: NumericalValue<*>): Meta = Meta(obj.value) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt new file mode 100644 index 0000000..88d7bd6 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt @@ -0,0 +1,36 @@ +package space.kscience.controls.constructor.units + +public data class XY<U : UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>) + +public fun <U : UnitsOfMeasurement> XY(x: Number, y: Number): XY<U> = XY(NumericalValue(x), NumericalValue((y))) + +public operator fun <U : UnitsOfMeasurement> XY<U>.plus(other: XY<U>): XY<U> = + XY(x + other.x, y + other.y) + +public operator fun <U : UnitsOfMeasurement> XY<U>.times(c: Number): XY<U> = XY(x * c, y * c) +public operator fun <U : UnitsOfMeasurement> XY<U>.div(c: Number): XY<U> = XY(x / c, y / c) + +public operator fun <U : UnitsOfMeasurement> XY<U>.unaryMinus(): XY<U> = XY(-x, -y) + +public data class XYZ<U : UnitsOfMeasurement>( + val x: NumericalValue<U>, + val y: NumericalValue<U>, + val z: NumericalValue<U>, +) + +public fun <U : UnitsOfMeasurement> XYZ(x: Number, y: Number, z: Number): XYZ<U> = + XYZ(NumericalValue(x), NumericalValue((y)), NumericalValue(z)) + +@Suppress("UNCHECKED_CAST", "UNUSED_PARAMETER") +public fun <U : UnitsOfMeasurement, R : UnitsOfMeasurement> XYZ<U>.cast(units: R): XYZ<R> = this as XYZ<R> + +public operator fun <U : UnitsOfMeasurement> XYZ<U>.plus(other: XYZ<U>): XYZ<U> = + XYZ(x + other.x, y + other.y, z + other.z) + +public operator fun <U : UnitsOfMeasurement> XYZ<U>.minus(other: XYZ<U>): XYZ<U> = + XYZ(x - other.x, y - other.y, z - other.z) + +public operator fun <U : UnitsOfMeasurement> XYZ<U>.times(c: Number): XYZ<U> = XYZ(x * c, y * c, z * c) +public operator fun <U : UnitsOfMeasurement> XYZ<U>.div(c: Number): XYZ<U> = XYZ(x / c, y / c, z / c) + +public operator fun <U : UnitsOfMeasurement> XYZ<U>.unaryMinus(): XYZ<U> = XYZ(-x, -y, -z) \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt index e4de468..e937d1b 100644 --- a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt +++ b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt @@ -1,105 +1,50 @@ package space.kscience.controls.demo.constructor import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size import androidx.compose.material.MaterialTheme import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import kotlinx.serialization.Serializable import space.kscience.controls.compose.asComposeState import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.models.MaterialPoint +import space.kscience.controls.constructor.units.* import space.kscience.dataforge.context.Context +import java.awt.Dimension import kotlin.math.pow import kotlin.math.sqrt -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.DurationUnit -@Serializable -private data class XY(val x: Double, val y: Double) { - companion object { - val ZERO = XY(0.0, 0.0) - } -} - -private val XY.length: Double get() = sqrt(x.pow(2) + y.pow(2)) - -private operator fun XY.plus(other: XY): XY = XY(x + other.x, y + other.y) -private operator fun XY.times(c: Double): XY = XY(x * c, y * c) -private operator fun XY.div(c: Double): XY = XY(x / c, y / c) private class Spring( context: Context, val k: Double, - val l0: Double, - val begin: DeviceState<XY>, - val end: DeviceState<XY>, + val l0: NumericalValue<Meters>, + val begin: DeviceState<XYZ<Meters>>, + val end: DeviceState<XYZ<Meters>>, ) : ModelConstructor(context) { /** - * vector from start to end + * Tension at the beginning point */ - val direction = combineState(begin, end) { begin: XY, end: XY -> - val dx = end.x - begin.x - val dy = end.y - begin.y - val l = sqrt(dx.pow(2) + dy.pow(2)) - XY(dx / l, dy / l) - } - - val tension: DeviceState<Double> = combineState(begin, end) { begin: XY, end: XY -> - val dx = end.x - begin.x - val dy = end.y - begin.y - k * sqrt(dx.pow(2) + dy.pow(2)) - } - - - val beginForce = combineState(direction, tension) { direction: XY, tension: Double -> - direction * (tension) - } - - - val endForce = combineState(direction, tension) { direction: XY, tension: Double -> - direction * (-tension) - } -} - -private class MaterialPoint( - context: Context, - val mass: Double, - val force: DeviceState<XY>, - val position: MutableDeviceState<XY>, - val velocity: MutableDeviceState<XY> = MutableDeviceState(XY.ZERO), -) : ModelConstructor(context, force, position, velocity) { - - private val timer: TimerState = timer(2.milliseconds) - - //TODO synchronize force change - - private val movement = timer.onChange( - writes = setOf(position, velocity), - reads = setOf(force, velocity, position) - ) { prev, next -> - val dt = (next - prev).toDouble(DurationUnit.SECONDS) - val a = force.value / mass - position.value += a * (dt * dt / 2) + velocity.value * dt - velocity.value += a * dt + val tension: DeviceState<XYZ<Newtons>> = combineState(begin, end) { begin: XYZ<Meters>, end: XYZ<Meters> -> + val delta = end - begin + val l = sqrt(delta.x.value.pow(2) + delta.y.value.pow(2) + delta.z.value.pow(2)) + ((delta / l) * k * (l - l0.value)).cast(Newtons) } } private class BodyOnSprings( context: Context, - mass: Double, + mass: NumericalValue<Kilograms>, k: Double, - startPosition: XY, - l0: Double = 1.0, + startPosition: XYZ<Meters>, + l0: NumericalValue<Meters> = NumericalValue(1.0), val xLeft: Double = -1.0, val xRight: Double = 1.0, val yBottom: Double = -1.0, @@ -110,22 +55,24 @@ private class BodyOnSprings( val height = yTop - yBottom val position = stateOf(startPosition) + val velocity: MutableDeviceState<XYZ<MetersPerSecond>> = stateOf(XYZ(0, 0, 0)) - private val leftAnchor = stateOf(XY(xLeft, (yTop + yBottom) / 2)) + private val leftAnchor = stateOf(XYZ<Meters>(xLeft, (yTop + yBottom) / 2, 0.0)) val leftSpring = model( Spring(context, k, l0, leftAnchor, position) ) - private val rightAnchor = stateOf(XY(xRight, (yTop + yBottom) / 2)) + private val rightAnchor = stateOf(XYZ<Meters>(xRight, (yTop + yBottom) / 2, 0.0)) val rightSpring = model( Spring(context, k, l0, rightAnchor, position) ) - val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, right -> - left + right - } + val force: DeviceState<XYZ<Newtons>> = + combineState(leftSpring.tension, rightSpring.tension) { left: XYZ<Newtons>, right -> + -left - right + } val body = model( @@ -134,21 +81,23 @@ private class BodyOnSprings( mass = mass, force = force, position = position, + velocity = velocity ) ) } fun main() = application { - val initialState = XY(0.1, 0.2) + val initialState = XYZ<Meters>(0.01, 0.1, 0) Window(title = "Ball on springs", onCloseRequest = ::exitApplication) { + window.minimumSize = Dimension(400, 400) MaterialTheme { val context = remember { Context("simulation") } val model = remember { - BodyOnSprings(context, 100.0, 1000.0, initialState) + BodyOnSprings(context, NumericalValue(10.0), 100.0, initialState) } //TODO add ability to freeze model @@ -159,24 +108,23 @@ fun main() = application { // }.collect() // } - val position: XY by model.body.position.asComposeState() - Box(Modifier.size(400.dp)) { - Canvas(modifier = Modifier.fillMaxSize()) { - fun XY.toOffset() = Offset( - center.x + (x / model.width * size.width).toFloat(), - center.y - (y / model.height * size.height).toFloat() - ) + val position: XYZ<Meters> by model.body.position.asComposeState() + Canvas(modifier = Modifier.fillMaxSize()) { + fun XYZ<Meters>.toOffset() = Offset( + ((x.value - model.xLeft) / model.width * size.width).toFloat(), + ((y.value - model.yBottom) / model.height * size.height).toFloat() - drawCircle( - Color.Red, 10f, center = position.toOffset() - ) - drawLine(Color.Blue, model.leftSpring.begin.value.toOffset(), model.leftSpring.end.value.toOffset()) - drawLine( - Color.Blue, - model.rightSpring.begin.value.toOffset(), - model.rightSpring.end.value.toOffset() - ) - } + ) + + drawCircle( + Color.Red, 10f, center = position.toOffset() + ) + drawLine(Color.Blue, model.leftSpring.begin.value.toOffset(), model.leftSpring.end.value.toOffset()) + drawLine( + Color.Blue, + model.rightSpring.begin.value.toOffset(), + model.rightSpring.end.value.toOffset() + ) } } } From 1799a9a9096b945d9354000234a517e9b41790fb Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 3 Jun 2024 18:34:08 +0300 Subject: [PATCH 098/125] Fix ball on springs demo --- .../controls/constructor/models/MaterialPoint.kt | 10 ++++++++-- .../kscience/controls/constructor/units/coordinates.kt | 8 ++++++++ demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt | 6 ++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt index c03841c..cc44a85 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt @@ -26,17 +26,23 @@ public class MaterialPoint( private var currentForce = force.value private val movement = onTimer( + DefaultTimer.REALTIME, reads = setOf(velocity, position), writes = setOf(velocity, position) ) { prev, next -> val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS) // compute new value based on velocity and acceleration from the previous step - position.value += (velocity.value * dtSeconds).cast(Meters) + + val deltaR = (velocity.value * dtSeconds).cast(Meters) + (currentForce / mass.value * dtSeconds.pow(2) / 2).cast(Meters) + position.value += deltaR // compute new velocity based on acceleration on the previous step - velocity.value += (currentForce / mass.value * dtSeconds).cast(MetersPerSecond) + val deltaV = (currentForce / mass.value * dtSeconds).cast(MetersPerSecond) + //TODO apply energy correction + //val work = deltaR.length.value * currentForce.length.value + velocity.value += deltaV + currentForce = force.value } } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt index 88d7bd6..027dbbb 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt @@ -1,5 +1,8 @@ package space.kscience.controls.constructor.units +import kotlin.math.pow +import kotlin.math.sqrt + public data class XY<U : UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>) public fun <U : UnitsOfMeasurement> XY(x: Number, y: Number): XY<U> = XY(NumericalValue(x), NumericalValue((y))) @@ -18,6 +21,11 @@ public data class XYZ<U : UnitsOfMeasurement>( val z: NumericalValue<U>, ) +public val <U : UnitsOfMeasurement> XYZ<U>.length: NumericalValue<U> + get() = NumericalValue( + sqrt(x.value.pow(2) + y.value.pow(2) + z.value.pow(2)) + ) + public fun <U : UnitsOfMeasurement> XYZ(x: Number, y: Number, z: Number): XYZ<U> = XYZ(NumericalValue(x), NumericalValue((y)), NumericalValue(z)) diff --git a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt index e937d1b..57bc426 100644 --- a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt +++ b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt @@ -16,8 +16,6 @@ import space.kscience.controls.constructor.models.MaterialPoint import space.kscience.controls.constructor.units.* import space.kscience.dataforge.context.Context import java.awt.Dimension -import kotlin.math.pow -import kotlin.math.sqrt private class Spring( @@ -33,7 +31,7 @@ private class Spring( */ val tension: DeviceState<XYZ<Newtons>> = combineState(begin, end) { begin: XYZ<Meters>, end: XYZ<Meters> -> val delta = end - begin - val l = sqrt(delta.x.value.pow(2) + delta.y.value.pow(2) + delta.z.value.pow(2)) + val l = delta.length.value ((delta / l) * k * (l - l0.value)).cast(Newtons) } } @@ -87,7 +85,7 @@ private class BodyOnSprings( } fun main() = application { - val initialState = XYZ<Meters>(0.01, 0.1, 0) + val initialState = XYZ<Meters>(0, 0.4, 0) Window(title = "Ball on springs", onCloseRequest = ::exitApplication) { window.minimumSize = Dimension(400, 400) From 91f860adf61a3d0ee49372a99c3dc385a30bed27 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 3 Jun 2024 18:48:12 +0300 Subject: [PATCH 099/125] Move plotter model out of visualization --- .../constructor/src/jvmMain/kotlin/Plotter.kt | 79 ++++++++++--------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/demo/constructor/src/jvmMain/kotlin/Plotter.kt b/demo/constructor/src/jvmMain/kotlin/Plotter.kt index b605dd4..af2c4dd 100644 --- a/demo/constructor/src/jvmMain/kotlin/Plotter.kt +++ b/demo/constructor/src/jvmMain/kotlin/Plotter.kt @@ -11,11 +11,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import space.kscience.controls.constructor.DeviceConstructor -import space.kscience.controls.constructor.MutableDeviceState -import space.kscience.controls.constructor.device +import space.kscience.controls.constructor.* import space.kscience.controls.constructor.devices.StepDrive import space.kscience.controls.constructor.devices.angle import space.kscience.controls.constructor.models.ScrewDrive @@ -28,7 +27,7 @@ import java.awt.Dimension import kotlin.random.Random -class Plotter( +private class Plotter( context: Context, xDrive: StepDrive, yDrive: StepDrive, @@ -42,16 +41,16 @@ class Plotter( yDrive.target.value = y.toLong() } -// val position = combineState(xDrive.position, yDrive.position) { x, y -> -// space.kscience.controls.constructor.models.XY(x, y) -// } + val ticks = combineState(xDrive.position, yDrive.position) { x, y -> + x to y + } //TODO add calibration // TODO add draw as action } -suspend fun Plotter.modernArt(xRange: IntRange, yRange: IntRange) { +private suspend fun Plotter.modernArt(xRange: IntRange, yRange: IntRange) { while (isActive) { val randomX = Random.nextInt(xRange.first, xRange.last) val randomY = Random.nextInt(yRange.first, yRange.last) @@ -62,7 +61,7 @@ suspend fun Plotter.modernArt(xRange: IntRange, yRange: IntRange) { } } -suspend fun Plotter.square(xRange: IntRange, yRange: IntRange) { +private suspend fun Plotter.square(xRange: IntRange, yRange: IntRange) { while (isActive) { moveToXY(xRange.first, yRange.first) delay(1000) @@ -94,12 +93,33 @@ private data class PlotterPoint( val color: Color = Color.Black, ) +private class PlotterModel( + context: Context, + val callback: (PlotterPoint) -> Unit, +) : ModelConstructor(context) { + + private val xDrive = StepDrive(context, ticksPerSecond) + private val xTransmission = ScrewDrive(context, NumericalValue(0.01)) + val x = xTransmission.degreesToMeters(xDrive.angle(step)).coerceIn(xRange) + + private val yDrive = StepDrive(context, ticksPerSecond) + private val yTransmission = ScrewDrive(context, NumericalValue(0.01)) + val y = yTransmission.degreesToMeters(yDrive.angle(step)).coerceIn(yRange) + + val xy: DeviceState<XY<Meters>> = combineState(x, y) { x, y -> XY(x, y) } + + val plotter = Plotter(context, xDrive, yDrive) { color -> + println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color") + callback(PlotterPoint(x.value, y.value, color)) + } +} + suspend fun main() = application { Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) { window.minimumSize = Dimension(400, 400) val points = remember { mutableStateListOf<PlotterPoint>() } - var position by remember { mutableStateOf(PlotterPoint(NumericalValue(0), NumericalValue(0))) } + var position by remember { mutableStateOf(XY<Meters>(0, 0)) } LaunchedEffect(Unit) { val context = Context { @@ -109,42 +129,23 @@ suspend fun main() = application { /* Here goes the device definition block */ - - val xDrive = StepDrive(context, ticksPerSecond) - val xTransmission = ScrewDrive(context, NumericalValue(0.01)) - val x = xTransmission.degreesToMeters(xDrive.angle(step)).coerceIn(xRange) - - val yDrive = StepDrive(context, ticksPerSecond) - val yTransmission = ScrewDrive(context, NumericalValue(0.01)) - val y = yTransmission.degreesToMeters(yDrive.angle(step)).coerceIn(yRange) - - val plotter = Plotter(context, xDrive, yDrive) { color -> - println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color") - points.add(PlotterPoint(x.value, y.value, color)) + val plotterModel = PlotterModel(context) { plotterPoint -> + points.add(plotterPoint) } - /* Start visualization program */ - launch { - x.valueFlow.collect { - position = position.copy(x = it) - } - } - - launch { - y.valueFlow.collect { - position = position.copy(y = it) - } - } + plotterModel.xy.valueFlow.onEach { + position = it + }.launchIn(this) /* run program */ - launch { - val range = -1000..1000 + + val range = -1000..1000 // plotter.modernArt(range, range) - plotter.square(range, range) - } + plotterModel.plotter.square(range, range) + } From c63c2db6517ef05a4fe6e22700f6fcbc0991de91 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Wed, 5 Jun 2024 17:19:20 +0300 Subject: [PATCH 100/125] Fix PID demo --- .gitignore | 1 + .../controls/constructor/models/Inertia.kt | 2 +- .../models/{ScrewDrive.kt => Leadscrew.kt} | 13 +++-- .../commonMain/kotlin/ControlVisionPlugin.kt | 10 +++- .../commonMain/kotlin/koalaPlotExtensions.kt | 2 +- .../jsMain/kotlin/ControlsVisionPlugin.js.kt | 8 ++-- .../kotlin/ControlsVisionPlugin.jvm.kt | 8 ++-- .../build.gradle.kts | 3 +- .../src/commonMain/kotlin/NumberTextField.kt | 8 ++-- demo/all-things/build.gradle.kts | 3 +- demo/constructor/build.gradle.kts | 3 +- .../src/jvmMain/kotlin/BodyOnSprings.kt | 2 +- .../constructor/src/jvmMain/kotlin/PidDemo.kt | 47 +++++++++---------- .../constructor/src/jvmMain/kotlin/Plotter.kt | 8 ++-- demo/devices-on-map/build.gradle.kts | 41 ++++++++++++++++ .../devices-on-map/src/jvmMain/kotlin/main.kt | 8 ++++ demo/motors/build.gradle.kts | 3 +- gradle.properties | 2 +- gradle/libs.versions.toml | 2 + settings.gradle.kts | 3 +- 20 files changed, 121 insertions(+), 56 deletions(-) rename controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/{ScrewDrive.kt => Leadscrew.kt} (72%) create mode 100644 demo/devices-on-map/build.gradle.kts create mode 100644 demo/devices-on-map/src/jvmMain/kotlin/main.kt diff --git a/.gitignore b/.gitignore index 3bf252e..e688053 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Created by .ignore support plugin (hsz.mobi) .idea/ .gradle +.kotlin *.iws *.iml diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt index 1c6a507..1259151 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt @@ -24,7 +24,7 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>( private var currentForce = force.value - private val movement = onTimer { prev, next -> + private val movement = onTimer(DefaultTimer.REALTIME) { prev, next -> val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS) // compute new value based on velocity and acceleration from the previous step diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Leadscrew.kt similarity index 72% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Leadscrew.kt index 573a648..ebe0d30 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/ScrewDrive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Leadscrew.kt @@ -7,22 +7,25 @@ import space.kscience.controls.constructor.units.* import space.kscience.dataforge.context.Context import kotlin.math.PI -public class ScrewDrive( +/** + * https://en.wikipedia.org/wiki/Leadscrew + */ +public class Leadscrew( context: Context, public val leverage: NumericalValue<Meters>, ) : ModelConstructor(context) { public fun torqueToForce( - stateOfMomentum: DeviceState<NumericalValue<NewtonsMeters>>, - ): DeviceState<NumericalValue<Newtons>> = DeviceState.map(stateOfMomentum) { momentum -> - NumericalValue(momentum.value / leverage.value ) + stateOfTorque: DeviceState<NumericalValue<NewtonsMeters>>, + ): DeviceState<NumericalValue<Newtons>> = DeviceState.map(stateOfTorque) { torque -> + NumericalValue(torque.value / leverage.value ) } public fun degreesToMeters( stateOfAngle: DeviceState<NumericalValue<Degrees>>, offset: NumericalValue<Meters> = NumericalValue(0), ): DeviceState<NumericalValue<Meters>> = DeviceState.map(stateOfAngle) { degrees -> - offset + NumericalValue(degrees.value * 2 * PI / 360 *leverage.value ) + offset + NumericalValue(degrees.value * 2 * PI / 360 * leverage.value ) } } \ No newline at end of file diff --git a/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt b/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt index d9ec746..b51d15c 100644 --- a/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt +++ b/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt @@ -3,12 +3,20 @@ 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.Context import space.kscience.dataforge.context.PluginFactory +import space.kscience.dataforge.context.PluginTag +import space.kscience.dataforge.meta.Meta import space.kscience.visionforge.Vision import space.kscience.visionforge.VisionPlugin public expect class ControlVisionPlugin: VisionPlugin{ - public companion object: PluginFactory<ControlVisionPlugin> + override val tag: PluginTag + override val visionSerializersModule: SerializersModule + public companion object: PluginFactory<ControlVisionPlugin>{ + override val tag: PluginTag + override fun build(context: Context, meta: Meta): ControlVisionPlugin + } } internal val controlsVisionSerializersModule = SerializersModule { diff --git a/controls-vision/src/commonMain/kotlin/koalaPlotExtensions.kt b/controls-vision/src/commonMain/kotlin/koalaPlotExtensions.kt index 8eeb9a4..8be10df 100644 --- a/controls-vision/src/commonMain/kotlin/koalaPlotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/koalaPlotExtensions.kt @@ -158,7 +158,7 @@ public fun <T> Plot.plotDeviceState( public fun Plot.plotNumberState( context: Context, - state: DeviceState<out Number>, + state: DeviceState<Number>, 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 b966791..f75630b 100644 --- a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt +++ b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt @@ -43,9 +43,9 @@ private val sliderRenderer = ElementVisionRenderer<SliderVision> { name, vision: public actual class ControlVisionPlugin : VisionPlugin() { - override val tag: PluginTag get() = Companion.tag + actual override val tag: PluginTag get() = Companion.tag - override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule + actual override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule override fun content(target: String): Map<Name, Any> = when (target) { ElementVisionRenderer.TYPE -> mapOf( @@ -57,9 +57,9 @@ public actual class ControlVisionPlugin : VisionPlugin() { } public actual companion object : PluginFactory<ControlVisionPlugin> { - override val tag: PluginTag = PluginTag("controls.vision") + actual override val tag: PluginTag = PluginTag("controls.vision") - override fun build(context: Context, meta: Meta): ControlVisionPlugin = ControlVisionPlugin() + actual 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 index 55074ae..53ada2e 100644 --- a/controls-vision/src/jvmMain/kotlin/ControlsVisionPlugin.jvm.kt +++ b/controls-vision/src/jvmMain/kotlin/ControlsVisionPlugin.jvm.kt @@ -8,14 +8,14 @@ import space.kscience.dataforge.meta.Meta import space.kscience.visionforge.VisionPlugin public actual class ControlVisionPlugin : VisionPlugin() { - override val tag: PluginTag get() = Companion.tag + actual override val tag: PluginTag get() = Companion.tag - override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule + actual override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule public actual companion object : PluginFactory<ControlVisionPlugin> { - override val tag: PluginTag = PluginTag("controls.vision") + actual override val tag: PluginTag = PluginTag("controls.vision") - override fun build(context: Context, meta: Meta): ControlVisionPlugin = ControlVisionPlugin() + actual override fun build(context: Context, meta: Meta): ControlVisionPlugin = ControlVisionPlugin() } } \ No newline at end of file diff --git a/controls-visualisation-compose/build.gradle.kts b/controls-visualisation-compose/build.gradle.kts index 48ae9cd..d9d8215 100644 --- a/controls-visualisation-compose/build.gradle.kts +++ b/controls-visualisation-compose/build.gradle.kts @@ -2,7 +2,8 @@ import org.jetbrains.compose.ExperimentalComposeLibrary plugins { id("space.kscience.gradle.mpp") - alias(spclibs.plugins.compose) + alias(spclibs.plugins.compose.compiler) + alias(spclibs.plugins.compose.jb) `maven-publish` } diff --git a/controls-visualisation-compose/src/commonMain/kotlin/NumberTextField.kt b/controls-visualisation-compose/src/commonMain/kotlin/NumberTextField.kt index 68e61ca..3ab10c6 100644 --- a/controls-visualisation-compose/src/commonMain/kotlin/NumberTextField.kt +++ b/controls-visualisation-compose/src/commonMain/kotlin/NumberTextField.kt @@ -2,8 +2,8 @@ package space.kscience.controls.compose import androidx.compose.foundation.layout.Row import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowLeft -import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -30,7 +30,7 @@ public fun NumberTextField( Row (verticalAlignment = Alignment.CenterVertically, modifier = modifier) { step.takeIf { it > 0.0 }?.let { IconButton({ onValueChange(value.toDouble() - step) }, enabled = enabled) { - Icon(Icons.Default.KeyboardArrowLeft, "decrease value") + Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "decrease value") } } TextField( @@ -52,7 +52,7 @@ public fun NumberTextField( ) step.takeIf { it > 0.0 }?.let { IconButton({ onValueChange(value.toDouble() + step) }, enabled = enabled) { - Icon(Icons.Default.KeyboardArrowRight, "increase value") + Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, "increase value") } } } diff --git a/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts index 2abde80..077e55d 100644 --- a/demo/all-things/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -1,6 +1,7 @@ plugins { kotlin("jvm") - alias(spclibs.plugins.compose) + alias(spclibs.plugins.compose.compiler) + alias(spclibs.plugins.compose.jb) } diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index 6b6f461..6d6d7f0 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -3,7 +3,8 @@ import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode plugins { id("space.kscience.gradle.mpp") - alias(spclibs.plugins.compose) + alias(spclibs.plugins.compose.compiler) + alias(spclibs.plugins.compose.jb) } kscience { diff --git a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt index 57bc426..af6da52 100644 --- a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt +++ b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt @@ -85,7 +85,7 @@ private class BodyOnSprings( } fun main() = application { - val initialState = XYZ<Meters>(0, 0.4, 0) + val initialState = XYZ<Meters>(0.05, 0.4, 0) Window(title = "Ball on springs", onCloseRequest = ::exitApplication) { window.minimumSize = Dimension(400, 400) diff --git a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt index b19e179..81a5923 100644 --- a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt +++ b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt @@ -32,10 +32,10 @@ import space.kscience.controls.constructor.devices.Drive import space.kscience.controls.constructor.devices.LimitSwitch import space.kscience.controls.constructor.devices.LinearDrive import space.kscience.controls.constructor.models.Inertia +import space.kscience.controls.constructor.models.Leadscrew import space.kscience.controls.constructor.models.MutableRangeState import space.kscience.controls.constructor.models.PidParameters -import space.kscience.controls.constructor.models.ScrewDrive -import space.kscience.controls.constructor.timer +import space.kscience.controls.constructor.onTimer import space.kscience.controls.constructor.units.Kilograms import space.kscience.controls.constructor.units.Meters import space.kscience.controls.constructor.units.NumericalValue @@ -56,13 +56,13 @@ import kotlin.time.DurationUnit class Modulator( context: Context, target: MutableDeviceState<NumericalValue<Meters>>, - var freq: Double = 0.1, var timeStep: Duration = 5.milliseconds, + var freq: Double = 0.1, ) : DeviceConstructor(context) { private val clockStart = clock.now() - private val modulation = timer(10.milliseconds).onNext { - val timeFromStart = clock.now() - clockStart + private val modulation = onTimer(timeStep) { _, next -> + val timeFromStart = next - clockStart val t = timeFromStart.toDouble(DurationUnit.SECONDS) target.value = NumericalValue( 5 * sin(2.0 * PI * freq * t) + @@ -72,9 +72,9 @@ class Modulator( } -private val mass = NumericalValue<Kilograms>(0.1) +private val mass = NumericalValue<Kilograms>(1) -private val leverage = NumericalValue<Meters>(0.05) +private val leverage = NumericalValue<Meters>(1.0) private val maxAge = 10.seconds @@ -95,7 +95,7 @@ internal fun createLinearDriveModel( val drive = Drive(context) //a screw drive to convert a rotational moment into a force - val screwDrive = ScrewDrive(context, leverage) + val leadscrew = Leadscrew(context, leverage) /** @@ -107,7 +107,7 @@ internal fun createLinearDriveModel( */ val inertiaModel = Inertia.linear( context = context, - force = screwDrive.torqueToForce(drive.force), + force = leadscrew.torqueToForce(drive.force), mass = mass, position = position ) @@ -130,6 +130,8 @@ private fun createModulator(linearDrive: LinearDrive): Modulator = linearDrive.c Modulator(linearDrive.context, linearDrive.pid.target) ) +private val startPid = PidParameters(kp = 250.0, ki = 0.0, kd = -20.0, timeStep = 20.milliseconds) + @OptIn(ExperimentalSplitPaneApi::class, ExperimentalKoalaPlotApi::class) fun main() = application { val context = remember { @@ -140,7 +142,7 @@ fun main() = application { } var pidParameters by remember { - mutableStateOf(PidParameters(kp = 900.0, ki = 20.0, kd = -50.0, timeStep = 0.005.seconds)) + mutableStateOf(startPid) } val linearDrive: LinearDrive = remember { @@ -183,14 +185,14 @@ fun main() = application { NumberTextField( value = pidParameters.kp, onValueChange = { pidParameters = pidParameters.copy(kp = it.toDouble()) }, - formatter = { String.format("%.2f", it.toDouble()) }, - step = 1.0, + formatter = { String.format("%.3f", it.toDouble()) }, + step = 0.01, modifier = Modifier.width(200.dp), ) Slider( pidParameters.kp.toFloat(), { pidParameters = pidParameters.copy(kp = it.toDouble()) }, - valueRange = 0f..1000f, + valueRange = 0f..100f, steps = 100 ) } @@ -199,15 +201,15 @@ fun main() = application { NumberTextField( value = pidParameters.ki, onValueChange = { pidParameters = pidParameters.copy(ki = it.toDouble()) }, - formatter = { String.format("%.2f", it.toDouble()) }, - step = 0.1, + formatter = { String.format("%.3f", it.toDouble()) }, + step = 0.01, modifier = Modifier.width(200.dp), ) Slider( pidParameters.ki.toFloat(), { pidParameters = pidParameters.copy(ki = it.toDouble()) }, - valueRange = -100f..100f, + valueRange = -10f..10f, steps = 100 ) } @@ -216,15 +218,15 @@ fun main() = application { NumberTextField( value = pidParameters.kd, onValueChange = { pidParameters = pidParameters.copy(kd = it.toDouble()) }, - formatter = { String.format("%.2f", it.toDouble()) }, - step = 0.1, + formatter = { String.format("%.3f", it.toDouble()) }, + step = 0.01, modifier = Modifier.width(200.dp), ) Slider( pidParameters.kd.toFloat(), { pidParameters = pidParameters.copy(kd = it.toDouble()) }, - valueRange = -100f..100f, + valueRange = -10f..10f, steps = 100 ) } @@ -247,12 +249,7 @@ fun main() = application { } Row { Button({ - pidParameters = PidParameters( - kp = 2.5, - ki = 0.0, - kd = -0.1, - timeStep = 0.005.seconds - ) + pidParameters = startPid }) { Text("Reset") } diff --git a/demo/constructor/src/jvmMain/kotlin/Plotter.kt b/demo/constructor/src/jvmMain/kotlin/Plotter.kt index af2c4dd..dfded59 100644 --- a/demo/constructor/src/jvmMain/kotlin/Plotter.kt +++ b/demo/constructor/src/jvmMain/kotlin/Plotter.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.isActive import space.kscience.controls.constructor.* import space.kscience.controls.constructor.devices.StepDrive import space.kscience.controls.constructor.devices.angle -import space.kscience.controls.constructor.models.ScrewDrive +import space.kscience.controls.constructor.models.Leadscrew import space.kscience.controls.constructor.models.coerceIn import space.kscience.controls.constructor.units.* import space.kscience.controls.manager.ClockManager @@ -99,11 +99,11 @@ private class PlotterModel( ) : ModelConstructor(context) { private val xDrive = StepDrive(context, ticksPerSecond) - private val xTransmission = ScrewDrive(context, NumericalValue(0.01)) + private val xTransmission = Leadscrew(context, NumericalValue(0.01)) val x = xTransmission.degreesToMeters(xDrive.angle(step)).coerceIn(xRange) private val yDrive = StepDrive(context, ticksPerSecond) - private val yTransmission = ScrewDrive(context, NumericalValue(0.01)) + private val yTransmission = Leadscrew(context, NumericalValue(0.01)) val y = yTransmission.degreesToMeters(yDrive.angle(step)).coerceIn(yRange) val xy: DeviceState<XY<Meters>> = combineState(x, y) { x, y -> XY(x, y) } @@ -143,7 +143,7 @@ suspend fun main() = application { val range = -1000..1000 -// plotter.modernArt(range, range) +// plotterModel.plotter.modernArt(range, range) plotterModel.plotter.square(range, range) } diff --git a/demo/devices-on-map/build.gradle.kts b/demo/devices-on-map/build.gradle.kts new file mode 100644 index 0000000..b81dc12 --- /dev/null +++ b/demo/devices-on-map/build.gradle.kts @@ -0,0 +1,41 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + +plugins { + id("space.kscience.gradle.mpp") + alias(spclibs.plugins.compose.compiler) + alias(spclibs.plugins.compose.jb) +} + +kscience { + jvm() + useSerialization() + useContextReceivers() + commonMain { + implementation(projects.controlsVisualisationCompose) + implementation(projects.controlsConstructor) + } + jvmMain { +// implementation("io.ktor:ktor-server-cio") + implementation(spclibs.logback.classic) + implementation(libs.sciprog.maps.compose) + } +} + +kotlin { + sourceSets { + jvmMain { + dependencies { + implementation(compose.desktop.currentOs) + } + } + } +} + +kotlin.explicitApi = ExplicitApiMode.Disabled + + +compose.desktop { + application { + mainClass = "space.kscience.controls.demo.map.MainKt" + } +} \ No newline at end of file diff --git a/demo/devices-on-map/src/jvmMain/kotlin/main.kt b/demo/devices-on-map/src/jvmMain/kotlin/main.kt new file mode 100644 index 0000000..648610e --- /dev/null +++ b/demo/devices-on-map/src/jvmMain/kotlin/main.kt @@ -0,0 +1,8 @@ +package space.kscience.controls.demo.map + +import androidx.compose.ui.window.application + + +fun main() = application { + +} \ No newline at end of file diff --git a/demo/motors/build.gradle.kts b/demo/motors/build.gradle.kts index 9241764..774df46 100644 --- a/demo/motors/build.gradle.kts +++ b/demo/motors/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("space.kscience.gradle.jvm") - alias(spclibs.plugins.compose) + alias(spclibs.plugins.compose.compiler) + alias(spclibs.plugins.compose.jb) } kotlin{ diff --git a/gradle.properties b/gradle.properties index 4f937f0..99c39d0 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.22 \ No newline at end of file +toolsVersion=0.15.4-kotlin-2.0.0 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 617933a..102c441 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -81,6 +81,8 @@ visionforge-markdown = { module = "space.kscience:visionforge-markdown", version visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" } visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" } +sciprog-maps-compose = "space.kscience:maps-kt-compose:0.3.0" + # Buildscript [plugins] diff --git a/settings.gradle.kts b/settings.gradle.kts index 366c39e..7aebf36 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -86,5 +86,6 @@ include( ":demo:motors", ":demo:echo", ":demo:mks-pdr900", - ":demo:constructor" + ":demo:constructor", + ":demo:devices-on-map" ) From a2b5880da99f6718afcff5a54a7f3cd9e11db767 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Thu, 6 Jun 2024 16:54:17 +0300 Subject: [PATCH 101/125] Add device collective demo --- .../controls/client/RemoteDeviceConnect.kt | 2 +- .../kscience/controls/client/MagixLoopTest.kt | 63 ++++++------ .../src/commonMain/kotlin/koalaPlots.kt | 7 +- .../build.gradle.kts | 1 + .../src/jvmMain/kotlin/DebounceDeviceState.kt | 20 ++++ .../jvmMain/kotlin/DeviceCollectiveModel.kt | 43 +++++++++ .../src/jvmMain/kotlin/GmcVelocity.kt | 24 +++++ .../src/jvmMain/kotlin/RemoteDevice.kt | 64 ++++++++++++ .../src/jvmMain/kotlin/debugModel.kt | 50 ++++++++++ .../src/jvmMain/kotlin/main.kt | 91 ++++++++++++++++++ .../src/jvmMain/resources/SPC-logo.png | Bin 0 -> 5166 bytes .../devices-on-map/src/jvmMain/kotlin/main.kt | 8 -- gradle/libs.versions.toml | 6 +- settings.gradle.kts | 2 +- 14 files changed, 333 insertions(+), 48 deletions(-) rename demo/{devices-on-map => device-collective}/build.gradle.kts (94%) create mode 100644 demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt create mode 100644 demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt create mode 100644 demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt create mode 100644 demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt create mode 100644 demo/device-collective/src/jvmMain/kotlin/debugModel.kt create mode 100644 demo/device-collective/src/jvmMain/kotlin/main.kt create mode 100644 demo/device-collective/src/jvmMain/resources/SPC-logo.png delete mode 100644 demo/devices-on-map/src/jvmMain/kotlin/main.kt 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 3c707b2..18a3dfc 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 @@ -93,7 +93,7 @@ internal class RemoteDeviceConnect { val virtualMagixEndpoint = VirtualMagixEndpoint(deviceManager) - val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "client", "device", "test".asName()) + val remoteDevice: DeviceClient = virtualMagixEndpoint.remoteDevice(context, "client", "device", "test".asName()) assertContains(0.0..1.0, remoteDevice.read(TestDevice.value)) diff --git a/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt b/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt index b08066b..31f73fc 100644 --- a/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt +++ b/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt @@ -1,10 +1,10 @@ package space.kscience.controls.client +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withContext import space.kscience.controls.client.RemoteDeviceConnect.TestDevice import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install @@ -20,36 +20,37 @@ class MagixLoopTest { @Test fun realDeviceHub() = runTest { - withContext(Dispatchers.Default) { - val context = Context { - plugin(DeviceManager) - } - - val server = context.startMagixServer() - - val deviceManager = context.request(DeviceManager) - - val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") - - deviceManager.launchMagixService(deviceEndpoint, "device") - - launch { - delay(50) - repeat(10) { - deviceManager.install("test[$it]", TestDevice) - } - } - - val clientEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") - - val remoteHub = clientEndpoint.remoteDeviceHub(context, "client", "device") - - assertEquals(0, remoteHub.devices.size) - delay(60) - clientEndpoint.requestDeviceUpdate("client", "device") - delay(60) - assertEquals(10, remoteHub.devices.size) - server.stop() + val context = Context { + coroutineContext(Dispatchers.Default) + plugin(DeviceManager) } + + val server = context.startMagixServer() + + val deviceManager = context.request(DeviceManager) + + val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + + deviceManager.launchMagixService(deviceEndpoint, "device") + + val trigger = CompletableDeferred<Unit>() + + context.launch { + repeat(10) { + deviceManager.install("test[$it]", TestDevice) + } + delay(100) + trigger.complete(Unit) + } + + val clientEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + + val remoteHub = clientEndpoint.remoteDeviceHub(context, "client", "device") + + assertEquals(0, remoteHub.devices.size) + clientEndpoint.requestDeviceUpdate("client", "device") + trigger.join() + assertEquals(10, remoteHub.devices.size) + server.stop() } } \ No newline at end of file diff --git a/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt index 855de10..87a9fca 100644 --- a/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt +++ b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt @@ -1,3 +1,5 @@ +@file:OptIn(FlowPreview::class) + package space.kscience.controls.compose import androidx.compose.runtime.* @@ -6,11 +8,8 @@ import io.github.koalaplot.core.line.LinePlot import io.github.koalaplot.core.style.LineStyle import io.github.koalaplot.core.xygraph.DefaultPoint import io.github.koalaplot.core.xygraph.XYGraphScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.Instant import space.kscience.controls.api.Device diff --git a/demo/devices-on-map/build.gradle.kts b/demo/device-collective/build.gradle.kts similarity index 94% rename from demo/devices-on-map/build.gradle.kts rename to demo/device-collective/build.gradle.kts index b81dc12..5520265 100644 --- a/demo/devices-on-map/build.gradle.kts +++ b/demo/device-collective/build.gradle.kts @@ -13,6 +13,7 @@ kscience { commonMain { implementation(projects.controlsVisualisationCompose) implementation(projects.controlsConstructor) + implementation(projects.magix.magixRsocket) } jvmMain { // implementation("io.ktor:ktor-server-cio") diff --git a/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt b/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt new file mode 100644 index 0000000..b507cd6 --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt @@ -0,0 +1,20 @@ +package space.kscience.controls.demo.map + +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.sample +import space.kscience.controls.constructor.DeviceState +import kotlin.time.Duration + +@OptIn(FlowPreview::class) +class DebounceDeviceState<T>( + val origin: DeviceState<T>, + val interval: Duration, +) : DeviceState<T> { + override val value: T get() = origin.value + override val valueFlow: Flow<T> get() = origin.valueFlow.sample(interval) + + override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" +} + +fun <T> DeviceState<T>.debounce(interval: Duration) = DebounceDeviceState(this, interval) \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt new file mode 100644 index 0000000..fdf13a1 --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -0,0 +1,43 @@ +package space.kscience.controls.demo.map + +import space.kscience.controls.constructor.ModelConstructor +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.onTimer +import space.kscience.dataforge.context.Context +import space.kscience.maps.coordinates.Gmc + + +typealias RemoteDeviceId = String + + +data class RemoteDeviceState( + val id: RemoteDeviceId, + val configuration: RemoteDeviceConfiguration, + val position: MutableDeviceState<Gmc>, + val velocity: MutableDeviceState<GmcVelocity>, +) + +public fun RemoteDeviceState( + id: RemoteDeviceId, + position: Gmc, + configuration: RemoteDeviceConfiguration.() -> Unit = {}, +) = RemoteDeviceState( + id, + RemoteDeviceConfiguration(configuration), + MutableDeviceState(position), + MutableDeviceState(GmcVelocity.zero) +) + + +class DeviceCollectiveModel( + context: Context, + val deviceStates: Collection<RemoteDeviceState>, +) : ModelConstructor(context) { + + private val movement = onTimer { prev, next -> + val delta = (next - prev) + deviceStates.forEach { state -> + state.position.value = state.position.value.moveWith(state.velocity.value, delta) + } + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt new file mode 100644 index 0000000..bea840d --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt @@ -0,0 +1,24 @@ +package space.kscience.controls.demo.map + +import kotlinx.serialization.Serializable +import space.kscience.kmath.geometry.Angle +import space.kscience.maps.coordinates.* +import kotlin.time.Duration +import kotlin.time.DurationUnit + +@Serializable +data class GmcVelocity(val bearing: Angle, val velocity: Distance, val elevation: Distance = 0.kilometers){ + companion object{ + val zero = GmcVelocity(Angle.zero, 0.kilometers) + } +} + + +fun Gmc.moveWith(velocity: GmcVelocity, duration: Duration): Gmc { + val seconds = duration.toDouble(DurationUnit.SECONDS) + + return GeoEllipsoid.WGS84.curveInDirection( + GmcPose(this, velocity.bearing), + velocity.velocity * seconds, + ).backward.coordinates +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt b/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt new file mode 100644 index 0000000..19a4e5b --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt @@ -0,0 +1,64 @@ +@file:OptIn(DFExperimental::class) + +package space.kscience.controls.demo.map + +import space.kscience.controls.api.Device +import space.kscience.controls.constructor.DeviceConstructor +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.registerAsProperty +import space.kscience.controls.spec.DeviceSpec +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.meta.Scheme +import space.kscience.dataforge.meta.SchemeSpec +import space.kscience.dataforge.misc.DFExperimental +import space.kscience.maps.coordinates.Gmc +import kotlin.time.Duration.Companion.milliseconds + +class RemoteDeviceConfiguration : Scheme() { + companion object : SchemeSpec<RemoteDeviceConfiguration>(::RemoteDeviceConfiguration) +} + + +interface RemoteDevice : Device { + + suspend fun getPosition(): Gmc + + suspend fun getVelocity(): GmcVelocity + + suspend fun setVelocity(value: GmcVelocity) + + + companion object : DeviceSpec<RemoteDevice>() { + val position by property<Gmc>( + converter = MetaConverter.serializable(), + read = { getPosition() } + ) + + val velocity by mutableProperty<GmcVelocity>( + converter = MetaConverter.serializable(), + read = { getVelocity() }, + write = { _, value -> setVelocity(value) } + ) + } +} + + +class RemoteDeviceConstructor( + context: Context, + val configuration: RemoteDeviceConfiguration, + position: MutableDeviceState<Gmc>, + velocity: MutableDeviceState<GmcVelocity>, +) : DeviceConstructor(context, configuration.meta), RemoteDevice { + + val position = registerAsProperty(RemoteDevice.position, position.debounce(500.milliseconds)) + val velocity = registerAsProperty(RemoteDevice.velocity, velocity) + + override suspend fun getPosition(): Gmc = position.value + + override suspend fun getVelocity(): GmcVelocity = velocity.value + + override suspend fun setVelocity(value: GmcVelocity) { + velocity.value = value + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt new file mode 100644 index 0000000..e352fcc --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -0,0 +1,50 @@ +package space.kscience.controls.demo.map + +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import space.kscience.controls.spec.write +import space.kscience.dataforge.context.Context +import space.kscience.kmath.geometry.degrees +import space.kscience.kmath.geometry.radians +import space.kscience.maps.coordinates.Gmc +import space.kscience.maps.coordinates.kilometers +import kotlin.math.PI +import kotlin.random.Random + +private val deviceVelocity = 0.1.kilometers + +private val center = Gmc.ofDegrees(55.925, 37.514) +private val radius = 0.01.degrees + + +internal fun generateModel(context: Context): DeviceCollectiveModel { + val devices: List<RemoteDeviceState> = buildList { + repeat(100) { + add( + RemoteDeviceState( + "device[$it]", + Gmc( + center.latitude + radius * Random.nextDouble(), + center.longitude + radius * Random.nextDouble() + ) + ) + ) + } + } + + val model = DeviceCollectiveModel(context, devices) + + return model +} + +fun RemoteDevice.moveInCircles(): Job = launch { + var bearing = Random.nextDouble(-PI, PI).radians + write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + while (isActive) { + delay(500) + bearing += 5.degrees + write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt new file mode 100644 index 0000000..0c7f7ad --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -0,0 +1,91 @@ +package space.kscience.controls.demo.map + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.spec.propertyFlow +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request +import space.kscience.maps.compose.MapView +import space.kscience.maps.compose.OpenStreetMapTileProvider +import space.kscience.maps.features.ViewConfig +import space.kscience.maps.features.circle +import space.kscience.maps.features.color +import space.kscience.maps.features.rectangle +import java.nio.file.Path + + +@Composable +fun rememberDeviceManager(): DeviceManager = remember { + val context = Context { + plugin(DeviceManager) + } + + context.request(DeviceManager) +} + + +@Composable +fun App() { + val scope = rememberCoroutineScope() + + + val deviceManager = rememberDeviceManager() + + + val collectiveModel = remember { + generateModel(deviceManager.context) + } + + val devices: Map<RemoteDeviceId, RemoteDevice> = remember { + collectiveModel.deviceStates.associate { + val device = RemoteDeviceConstructor(deviceManager.context, it.configuration, it.position, it.velocity) + device.moveInCircles() + it.id to device + } + } + + val mapTileProvider = remember { + OpenStreetMapTileProvider( + client = HttpClient(CIO), + cacheDirectory = Path.of("mapCache") + ) + } + + MapView( + mapTileProvider = mapTileProvider, + config = ViewConfig() + ) { + collectiveModel.deviceStates.forEach { device -> + circle(device.position.value, id = device.id + ".position").color(Color.Red) + device.position.valueFlow.onEach { + circle(device.position.value, id = device.id + ".position").color(Color.Red) + }.launchIn(scope) + } + + devices.forEach { (id, device) -> + device.propertyFlow(RemoteDevice.position).onEach { position -> + rectangle(position, id = id).color(Color.Blue) + }.launchIn(scope) + } + } +} + + +fun main() = application { + Window(onCloseRequest = ::exitApplication, title = "Maps-kt demo", icon = painterResource("SPC-logo.png")) { + MaterialTheme { + App() + } + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/resources/SPC-logo.png b/demo/device-collective/src/jvmMain/resources/SPC-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..953de16d4876765010935ffc20cbd1d37351dbb6 GIT binary patch literal 5166 zcmeHLdpML^7ax~0lFVx=62pw5QfYE6*WP)N%B7S`H#Lr1i6&$ya-6ANxji~v6pdRK zCATA82z8E2DWpUZP9{x_>tI|GzP+dDdA|R?|Gz)xnf0!{e!sQXdiUOI?Kj%d-bQ|& z$~*#rAaA=F+d&{mq{AO^F3>#7lKUI{$sXP87Dynh)r3C@_6>{UK)EEy$~DL-z$+-^ z;4x1^NJz+9U;o2_9tV$lt_?Wmo!n=tLLiV!Y_Ux{*(tqk6ZP%E;nl*B){=W&vFm>O zJ91vs!s|CI69ty82WVs^gNEgin?5{YYp_;4ti`nt>{(PuT~$Y2*EnB+vFXazxe}V0 zuV`J%gpny$_G{+sPU|gIA3o1;=r1YlpO`c=rJQ;aT-x`kZ0IFFa9_s<^38fvrZFMy zNrgayFnpOP;b_evtk6UW7Z56;1}70bF=FI;nzZE-L^3V*|HJ>CmMiH3iFKiajy;dx zs8<i~JAQ6oNIU&_TWIyWgA={Xmd2F@m7kL<ZzkKv_&VLH`1G719R90ISn>K6b9}I? zz;9en_{AfJ*LOl4FX=0@=qnqY8WWj~R-f1A-knN}T!t_|9GUnlhxg=tUi5}Sce0FH zen3ybL@wR8^NU)Ide~S)Qu)7^lFA3b@4)u&oi3vl0|NtDrRM9xw6>*&a?0(iK5+A{ zQmP*d^Nl+P9u-UsU#k|Cs=Gg4mRst->16*|m&&5%H8`KUho0N7aQ7OK?sYt@@&&Kf zMs0Dx2Eo8PkI{YEd<?7TT)vejGaYDmavDJ7joK8moySFIx6mrGS|{F?RF@=#A66Z@ z`Hzk0>XCbG;r&vq1|!{=yi?uh*q^U>XY(y}qe+>cBPU0q|8!v6s75^7^w!DxQ=*M} z!q&B=rfVO5DiK!fa6c3&T!#AHdp}W!>)D@j@6qG!dh29Z1APy<KVGr+(eR+_qhp>T z)h}}p<{w82-(C{_{%3p~uVegDk}(SvrEDL1QPLOZF!d&s)f+5#=awZunxtvy+gZVG zcAxZ28Eoe<V``1Exq4b>&YtBB>g2`OpB#B1Y_*|ebEU2=M`NEIZ}lgqP7d32Iw-W4 zht)h}pA?=q7I_SH-Z`9?$CbRM@0+Z4+$1whx_97Pb6vb#Ior9Xi0}W_sjBwgZ6{t* z=7N}<2QBL()=%GbEjqMIwCu}5ZEDlu!B6R9&tx28L)0gllGdf0q)x^*X0-OjDsYc` zeF*IM<E>Mg-qi_`OkDogd$(Crd<#UIGJ1c%%nBN_qjWTOy7<$Fcc+@#p&5o3wRmN( z;dSaxd#HXZmdd`RU)8kd2%;4Jwa2)}p;fecq5j2sj%((%{ldYqDi^*;FqC@tIfEfo z3OLF5KJw9qci$vpyP3>xJC1AZt5=RE`JJbtzn1juG`mrtWuHVkb^O_3-tAkLEA3+X zvba(?%h9`DZ}044z38<_$V@QRH+=uwF;Pm@)pZ9^)%By1t_4ib5BUR^1RGv~<)C6V z_Ua$jWRw+pVO>bwYHF1HGW706QkN{^r;ISKaaK6s2R>oANHN2d8kLApcR-DX17|ZC zDpV0BCN`PGaAYaxi=7RHhILc1e1BTSTm)Jak*S7BknZjkV!?K?Z7{9ET8vgAQ>{<K zF;fSQvH^NQ8yheI3lzGNM40o5+qtBM5L!hN2`Dn^F`TZ6g`${yG|6KNKFGn%6{%Gz z5FW0H#>!D^$h2=5N7)GS44)>_bD^^@QeaEULZV<ACJSwfw{cd8d0e7NH#jQ-`cm8q zj&;=}>0&`RaeLp>vy}MmfQ1>cf1JkS5vs~2=xHqw7*NP1QB|zO8X(v};XawFVhaUB zAZ!(D6san9Vhs=i1}wW9Y$&U+Zq`7GoA9MxeN~)AX-?R@>o_UrU+ltYadX{qT806J zH-RW!0`HwBHmrgX-20u_un`LS*N6>U#KIL~!)~!~k=Squy%3UrD%M6Snv|oBg-$Va z>@?A!D3}d98X9y)EKt#)b7DaQ4T^`tTMV~{+08glY;c2AI}z$e@m%N8Fg{EZy&c2% z!+bMMqe-PyknajMLJbqgEYv`U#WWsvoG;5^E~@~~9>^2B2+3%M`Bs*rVf|vF7L17E z)+nxQN0TlGQ$#C#5Gq>&!mR%wKHUo6Zb1P?Y_f#8ild_Wiy@QW3M49+l=1jcpjTy} zj&p^)TtE&NjloHG&yu=ouN9_}=ro$Ntk@}sxam9$%Z(<jsR5V98RAnSbY5UilU^$3 z4MwQWLwz0{Dt~I(os)?eY89-E8~K-^P%V;MT>0b0&_n7X3jE#=x*i?see2|-jC5MH z2Yzqm^-qsnH~v<y5O><BgEDG1;3&Fo9Z0J=G5K`D1SwEKIu(AHnGYKw2p!ZE)!+&$ zdI3x%_cMDXLZj_hO@rZrWOkm$aD%x%a7Hjkq;&2cm&mzL2B8xXLD9CEC-87PQF_Mm zn;j?2hassCg9EvlT#lL*(j`>j`vJTWis_UkaRhmo$u8OL)3~EUtk(=tmOGXuQ$@3s zg1}I;QJvPv@>nu6A%P4c$QKe{MM5B#6r4&7Ze-6+u-J~XG3ys}AFYvoan`H9V)_DK zbk@xYEKBq1!dc$Mh#*qy7ZHQAJNNl7C@FpMVa6<U63e>LG7CLqgd9pYoQ1Y*<?Oy$ zI}1Ion7;VyFR$TP*7D+CP$lHh?`wZS861_|7r&sS^ts)?ypCg8>EC`q4Ut1>=CjbN zTRAE>>t~@}is^HaH1Wm&c^bkU8N^qDAFnOT4a<^{_UZlkv_Z>R;Y-PtiJp~~-36c| z2*(R8RGe;YyUl&gmQ!k@3{8om1WR&`3ML$IVC80(If`Yyt9{VEB$YdQL$D-n1oRoK z8ei|KL2oVu6?)g<P8LT|QLJ-PNZ%aRP)>+*{TH)lmi*B-879oHe>zlB0~NBkuZ<ma z_fWLUrqw&Y&55~lOTWG6ieBeo{<S5^0r!1H7pjsWy1hI+Gi)vujiX}O<TO)>8giN! z0%FVH%Q|uiU9yfKwtt9;gg6d-(2I7I5^caFFKAnaN;dALH9CgRPlN+pf<ZFTz%><J z1_r=rtu`v(e3tZ+!vuGf3z~&dXpM^dW?)4z#G=iYXJEaW=$bdDXJOHfaz(SS{j|m% zM`mHk#3I+^S(rAuruoba%m{as(VvB>(HarE8CY-(G58rMhPZZWSTQ)g!rl+E>t)Qf zEM?)O+cZ(NHyo0@<nSq@_PzJh*GR8$h%B}ROMd-K$Y8~9Uqn^>ktlz1`hunPGj2zt z9joJPaEKX{&Qgb)Y!(Lg7}*1%!R(q!2$T>(V#CaoL<vOEOCQc=00PKwwqRd-%)|w^ zbrE&OT?9Fuvefpc18^KX+V)ch2L-K3?I%0al7r(0XMggsso*r)ul#eY^TwGwGOt#< zmyWg4A0IOIFYB#y9eC4vhyP=3{5@26uGTQw+3bd}&YZ8qnmT>ixjyY6>_01xiikd% z+G(cR8ckkI5y3|j#G=t;#W}!%({@!yq7V{@jmF?;*n&{-)ku^yG_giG2h5;}^2&PI zryJP|fcTDN56*O$ad@U1KpfKpCq0HsjI?ws=ifPEfIKV@U^&6^K$K9Cm0;?F%JV}* zriR%;HwB>bT!JtUD^Pi$Ls)rCAa|?*lHnpIQlMc0M6g8Z`(X)#-arYvCMIH&L79U@ zpv-y0)w&(<EQ36&fQcsXjsY4cNaG`kWax>7c}NCXEGQxwqLI|12ZZIaBv57VwM-R` z#OH;7+$OPwq;YjK$7c;>y1IqqBc9sTZ5$spvBsX`vj_?;mK+~>vB1m<BmAbC-(fB+ z(mknV*tRpWiQf}grpR|^`-<joW+`R}fv1qYM{^0DFHJ@zB{FtlxOL#1%lvuqhM=9; z3WK;01abCZ%V8td0Kp0hYe8GWfC6k(Hi<O|wFXAqU_irG!Z{Rg2K^6=81z5OVSHcK z!xjmefX3`0T=W?V7|s&gxZ@-q3Tey{&}+dr0DG<5@AbTij^_ytHlR7XVist<m)-6r zc|pT+_#%<6CFayan2lmi(7$CtPSDUTgo>yAI4KiS61m!_pFW%sn~pFu;hTFZz;Jmj zFgNMLTBsihj=7p3%nHbOpbd1G2q20Qk*TktM#F})S*$Qcn5ANq-!MEF`ie><Q=7Ix zABM5S0&BQT@;|~oVai}I=>1OWi_=8P6iU3FMM%U+QD=aQ!|zSU6qyD(&UbO1gSlCR zsCxpOq)jy!b$FSnrZ$lWG^0s}gF2)XDqA~(x2)b9bOIc#q2O2<>vl4yOpmweWZ$yv z1~x_P=dX>YuD{_4)H@u_ZqOI&x~WvBo($VN$r}fE0kYPS!hig|cP6Ag;_fM+<&HHN z&^>Fk=qEnMCV}o;bbY-=X_vP!cQiu3@B8>Zczc-HmMsVSHceFW%>#G)mAAdPb1OFc z<HwH=2bNy0bmOO}*PA`a2wM<zj!37qmicujjowzMPR}#RhZmwA;c}Lsdg{GsX)Myr zihcH%=L_D1%l<_Py8BfMz55n9-SpdG=DTgaf<m>Y#aM^&I3pErJv9;o?gwu6s9!#~ z9?{I%uUV`QCJE8D<dPw^%dP$2JyTdCN}W~7{yE^11>WR6P}P>GTl9w0yAASkxqoe^ zfIF*sNV}D=Hees3<C1fSD^;q2#$J^gY1Otj|ER_qqsr`ZSdw^FJ#=;1RE|z($28BR zVEFm1JABJLutxl=xFwa*)un6VHwf~R^nG`TQi_Tv+GRR30`@Pyyf-Y=bfPElZD87E za4#4-r*=QVs9Y$|dYQZ+p>%}pDN4b|+Ag*#L<HHL?61j&6OH+JuzbqVmZPkc7uK~_ z_!->%G~16Jh1YZ5w^!Yo3FegM@1Lq3elO39Kb*HmS;r+;^}*=eCOeLDNUL9wr>gOZ z)TF>CWpwY6vVvyH8yitepOJ!@VzaL6!Xc&A=f>p%qTSnr`?E?7*A-*<F|gKz+9rLZ zlD@|t+d4d}-v#s4JSHq!3=La@4*5N*{7gU6(wJlzyk#5Lbi*Z3#2r?Luj1~$zxvXe zJ(L8qvSktB^^>g8QiY0rqaV~mI));3l`KIcOz<oo;!p`W|NrRozkKX^`Kqw~ZcA>M dG+}ahdgIi>+tx>p41sTb1Y0Y6EEhcx^&il<db$7r literal 0 HcmV?d00001 diff --git a/demo/devices-on-map/src/jvmMain/kotlin/main.kt b/demo/devices-on-map/src/jvmMain/kotlin/main.kt deleted file mode 100644 index 648610e..0000000 --- a/demo/devices-on-map/src/jvmMain/kotlin/main.kt +++ /dev/null @@ -1,8 +0,0 @@ -package space.kscience.controls.demo.map - -import androidx.compose.ui.window.application - - -fun main() = application { - -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 102c441..7c5a861 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -dataforge = "0.8.0" +dataforge = "0.9.0" rsocket = "0.15.4" xodus = "2.0.1" @@ -10,7 +10,7 @@ fazecast = "2.10.3" tornadofx = "1.7.20" -plotlykt = "0.7.0" +plotlykt = "0.7.2" logback = "1.2.11" @@ -29,7 +29,7 @@ pi4j-ktx = "2.4.0" plc4j = "0.12.0" -visionforge = "0.4.1" +visionforge = "0.4.2" versions = "0.51.0" diff --git a/settings.gradle.kts b/settings.gradle.kts index 7aebf36..7e84c71 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -87,5 +87,5 @@ include( ":demo:echo", ":demo:mks-pdr900", ":demo:constructor", - ":demo:devices-on-map" + ":demo:device-collective" ) From 5c7d3d8a7a3513ede88320a895ef8d89cf6dc440 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Fri, 7 Jun 2024 10:52:28 +0300 Subject: [PATCH 102/125] Add PeerConnection --- CHANGELOG.md | 1 + .../kscience/controls/api/DeviceMessage.kt | 10 ++++- .../kscience/controls/peer/PeerConnection.kt | 42 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b2765..ae8c47f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - PLC4X bindings - Shortcuts to access all Controls devices in a magix network. - `DeviceClient` properly evaluates lifecycle and logs +- `PeerConnection` API for direct device-device binary sharing ### Changed - Constructor properties return `DeviceState` in order to be able to subscribe to them 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 dda89de..f91beb5 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 @@ -166,12 +166,18 @@ public data class ActionResultMessage( } /** - * Notifies listeners that a new binary with given [binaryID] is available. The binary itself could not be provided via [DeviceMessage] API. + * Notifies listeners that a new binary with given [contentId] and [contentMeta] is available. + * + * [contentMeta] includes public information that could be shared with loop subscribers. It should not contain sensitive data. + * + * The binary itself could not be provided via [DeviceMessage] API. + * [space.kscience.controls.peer.PeerConnection] must be used instead */ @Serializable @SerialName("binary.notification") public data class BinaryNotificationMessage( - val binaryID: String, + val contentId: String, + val contentMeta: Meta, override val sourceDevice: Name, override val targetDevice: Name? = null, override val comment: String? = null, diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt new file mode 100644 index 0000000..14b49ef --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt @@ -0,0 +1,42 @@ +package space.kscience.controls.peer + +import space.kscience.dataforge.io.Envelope +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.names.Name + +/** + * A manager that allows direct synchronous sending and receiving binary data + */ +public interface PeerConnection { + /** + * Receive an [Envelope] from a device with name [deviceName] on a given [address] with given [contentId]. + * + * The address depends on the specifics of given [PeerConnection]. For example, it could be a TCP/IP port or + * magix endpoint name. + * + * Depending on [PeerConnection] implementation, the resulting [Envelope] could be lazy loaded + * + * Additional metadata in [requestMeta] could be required for authentication. + */ + public suspend fun receive( + address: String, + deviceName: Name, + contentId: String, + requestMeta: Meta = Meta.EMPTY, + ): Envelope + + /** + * Send an [envelope] to a device with name [deviceName] on a given [address] + * + * The address depends on the specifics of given [PeerConnection]. For example, it could be a TCP/IP port or + * magix endpoint name. + * + * Additional metadata in [requestMeta] could be required for authentication. + */ + public suspend fun send( + address: String, + deviceName: Name, + envelope: Envelope, + requestMeta: Meta = Meta.EMPTY, + ) +} \ No newline at end of file From 13b80be8841761568ed17db6b23a2e9116591411 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Fri, 7 Jun 2024 20:20:39 +0300 Subject: [PATCH 103/125] Implement visibility range for collective device --- .gitignore | 5 +- .../src/commonMain/kotlin/misc.kt | 12 ++ .../constructor/src/jvmMain/kotlin/Plotter.kt | 19 ++- demo/device-collective/build.gradle.kts | 2 +- .../{RemoteDevice.kt => CollectiveDevice.kt} | 32 ++-- .../jvmMain/kotlin/DeviceCollectiveModel.kt | 42 +++-- .../src/jvmMain/kotlin/GmcVelocity.kt | 2 +- ...nceDeviceState.kt => SampleDeviceState.kt} | 6 +- .../src/jvmMain/kotlin/debugModel.kt | 31 ++-- .../src/jvmMain/kotlin/main.kt | 146 +++++++++++++++--- 10 files changed, 226 insertions(+), 71 deletions(-) create mode 100644 controls-visualisation-compose/src/commonMain/kotlin/misc.kt rename demo/device-collective/src/jvmMain/kotlin/{RemoteDevice.kt => CollectiveDevice.kt} (60%) rename demo/device-collective/src/jvmMain/kotlin/{DebounceDeviceState.kt => SampleDeviceState.kt} (76%) diff --git a/.gitignore b/.gitignore index e688053..5fab474 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,7 @@ out/ build/ -!gradle-wrapper.jar \ No newline at end of file + +!gradle-wrapper.jar + +/demo/device-collective/mapCache/ diff --git a/controls-visualisation-compose/src/commonMain/kotlin/misc.kt b/controls-visualisation-compose/src/commonMain/kotlin/misc.kt new file mode 100644 index 0000000..caf21e3 --- /dev/null +++ b/controls-visualisation-compose/src/commonMain/kotlin/misc.kt @@ -0,0 +1,12 @@ +package space.kscience.controls.compose + +import androidx.compose.ui.Modifier + +public inline fun Modifier.conditional( + condition: Boolean, + modifier: Modifier.() -> Modifier, +): Modifier = if (condition) { + then(modifier(Modifier)) +} else { + this +} \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/Plotter.kt b/demo/constructor/src/jvmMain/kotlin/Plotter.kt index dfded59..40f81f9 100644 --- a/demo/constructor/src/jvmMain/kotlin/Plotter.kt +++ b/demo/constructor/src/jvmMain/kotlin/Plotter.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.devices.LimitSwitch import space.kscience.controls.constructor.devices.StepDrive import space.kscience.controls.constructor.devices.angle import space.kscience.controls.constructor.models.Leadscrew @@ -31,10 +32,18 @@ private class Plotter( context: Context, xDrive: StepDrive, yDrive: StepDrive, + xStartLimit: LimitSwitch, + xEndLimit: LimitSwitch, + yStartLimit: LimitSwitch, + yEndLimit: LimitSwitch, val paint: suspend (Color) -> Unit, ) : DeviceConstructor(context) { val xDrive by device(xDrive) val yDrive by device(yDrive) + val xStartLimit by device(xStartLimit) + val xEndLimit by device(xEndLimit) + val yStartLimit by device(yStartLimit) + val yEndLimit by device(yEndLimit) public fun moveToXY(x: Number, y: Number) { xDrive.target.value = x.toLong() @@ -108,7 +117,15 @@ private class PlotterModel( val xy: DeviceState<XY<Meters>> = combineState(x, y) { x, y -> XY(x, y) } - val plotter = Plotter(context, xDrive, yDrive) { color -> + val plotter = Plotter( + context = context, + xDrive = xDrive, + yDrive = yDrive, + xStartLimit = LimitSwitch(context,x.atStart), + xEndLimit = LimitSwitch(context,x.atEnd), + yStartLimit = LimitSwitch(context,x.atStart), + yEndLimit = LimitSwitch(context,x.atEnd), + ) { color -> println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color") callback(PlotterPoint(x.value, y.value, color)) } diff --git a/demo/device-collective/build.gradle.kts b/demo/device-collective/build.gradle.kts index 5520265..8bb0597 100644 --- a/demo/device-collective/build.gradle.kts +++ b/demo/device-collective/build.gradle.kts @@ -37,6 +37,6 @@ kotlin.explicitApi = ExplicitApiMode.Disabled compose.desktop { application { - mainClass = "space.kscience.controls.demo.map.MainKt" + mainClass = "space.kscience.controls.demo.collective.MainKt" } } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt similarity index 60% rename from demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt rename to demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 19a4e5b..050794c 100644 --- a/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -1,6 +1,6 @@ @file:OptIn(DFExperimental::class) -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import space.kscience.controls.api.Device import space.kscience.controls.constructor.DeviceConstructor @@ -10,17 +10,20 @@ import space.kscience.controls.spec.DeviceSpec import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.Scheme -import space.kscience.dataforge.meta.SchemeSpec +import space.kscience.dataforge.meta.string import space.kscience.dataforge.misc.DFExperimental import space.kscience.maps.coordinates.Gmc import kotlin.time.Duration.Companion.milliseconds -class RemoteDeviceConfiguration : Scheme() { - companion object : SchemeSpec<RemoteDeviceConfiguration>(::RemoteDeviceConfiguration) +class CollectiveDeviceConfiguration(deviceId: DeviceId) : Scheme() { + var deviceId by string(deviceId) + var description by string() } -interface RemoteDevice : Device { +interface CollectiveDevice : Device { + + public val id: DeviceId suspend fun getPosition(): Gmc @@ -28,8 +31,10 @@ interface RemoteDevice : Device { suspend fun setVelocity(value: GmcVelocity) + suspend fun listVisible(): Collection<DeviceId> - companion object : DeviceSpec<RemoteDevice>() { + + companion object : DeviceSpec<CollectiveDevice>() { val position by property<Gmc>( converter = MetaConverter.serializable(), read = { getPosition() } @@ -44,15 +49,18 @@ interface RemoteDevice : Device { } -class RemoteDeviceConstructor( +class CollectiveDeviceConstructor( context: Context, - val configuration: RemoteDeviceConfiguration, + val configuration: CollectiveDeviceConfiguration, position: MutableDeviceState<Gmc>, velocity: MutableDeviceState<GmcVelocity>, -) : DeviceConstructor(context, configuration.meta), RemoteDevice { + private val listVisible: suspend () -> Collection<DeviceId>, +) : DeviceConstructor(context, configuration.meta), CollectiveDevice { - val position = registerAsProperty(RemoteDevice.position, position.debounce(500.milliseconds)) - val velocity = registerAsProperty(RemoteDevice.velocity, velocity) + override val id: DeviceId get() = configuration.deviceId + + val position = registerAsProperty(CollectiveDevice.position, position.sample(500.milliseconds)) + val velocity = registerAsProperty(CollectiveDevice.velocity, velocity) override suspend fun getPosition(): Gmc = position.value @@ -61,4 +69,6 @@ class RemoteDeviceConstructor( override suspend fun setVelocity(value: GmcVelocity) { velocity.value = value } + + override suspend fun listVisible(): Collection<DeviceId> = listVisible.invoke() } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index fdf13a1..4abedea 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -1,43 +1,59 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import space.kscience.controls.constructor.ModelConstructor import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.onTimer import space.kscience.dataforge.context.Context -import space.kscience.maps.coordinates.Gmc +import space.kscience.maps.coordinates.* -typealias RemoteDeviceId = String +typealias DeviceId = String -data class RemoteDeviceState( - val id: RemoteDeviceId, - val configuration: RemoteDeviceConfiguration, +internal data class VirtualDeviceState( + val id: DeviceId, + val configuration: CollectiveDeviceConfiguration, val position: MutableDeviceState<Gmc>, val velocity: MutableDeviceState<GmcVelocity>, ) -public fun RemoteDeviceState( - id: RemoteDeviceId, +internal fun VirtualDeviceState( + id: DeviceId, position: Gmc, - configuration: RemoteDeviceConfiguration.() -> Unit = {}, -) = RemoteDeviceState( + configuration: CollectiveDeviceConfiguration.() -> Unit = {}, +) = VirtualDeviceState( id, - RemoteDeviceConfiguration(configuration), + CollectiveDeviceConfiguration(id).apply(configuration), MutableDeviceState(position), MutableDeviceState(GmcVelocity.zero) ) -class DeviceCollectiveModel( +internal class DeviceCollectiveModel( context: Context, - val deviceStates: Collection<RemoteDeviceState>, + val deviceStates: Collection<VirtualDeviceState>, + val visibilityRange: Distance, ) : ModelConstructor(context) { + /** + * Propagate movement + */ private val movement = onTimer { prev, next -> val delta = (next - prev) deviceStates.forEach { state -> state.position.value = state.position.value.moveWith(state.velocity.value, delta) } } + + suspend fun locateVisible(id: DeviceId): Map<DeviceId, GmcCurve> { + val coordinatesSnapshot = deviceStates.associate { it.id to it.position.value } + + val selected = coordinatesSnapshot[id] ?: error("Can't find device with id $id") + + val allCurves = coordinatesSnapshot + .filterKeys { it != id } + .mapValues { GeoEllipsoid.WGS84.curveBetween(selected, it.value) } + + return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange } + } } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt index bea840d..9d356c6 100644 --- a/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt +++ b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import kotlinx.serialization.Serializable import space.kscience.kmath.geometry.Angle diff --git a/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt b/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt similarity index 76% rename from demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt rename to demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt index b507cd6..a2ef06c 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt +++ b/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow @@ -7,7 +7,7 @@ import space.kscience.controls.constructor.DeviceState import kotlin.time.Duration @OptIn(FlowPreview::class) -class DebounceDeviceState<T>( +class SampleDeviceState<T>( val origin: DeviceState<T>, val interval: Duration, ) : DeviceState<T> { @@ -17,4 +17,4 @@ class DebounceDeviceState<T>( override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" } -fun <T> DeviceState<T>.debounce(interval: Duration) = DebounceDeviceState(this, interval) \ No newline at end of file +fun <T> DeviceState<T>.sample(interval: Duration) = SampleDeviceState(this, interval) \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt index e352fcc..96c9ca8 100644 --- a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -20,31 +20,32 @@ private val radius = 0.01.degrees internal fun generateModel(context: Context): DeviceCollectiveModel { - val devices: List<RemoteDeviceState> = buildList { - repeat(100) { - add( - RemoteDeviceState( - "device[$it]", - Gmc( - center.latitude + radius * Random.nextDouble(), - center.longitude + radius * Random.nextDouble() - ) - ) + val devices: List<VirtualDeviceState> = List(100) { index -> + val id = "device[$index]" + + VirtualDeviceState( + id = id, + Gmc( + center.latitude + radius * Random.nextDouble(), + center.longitude + radius * Random.nextDouble() ) + ) { + deviceId = id + description = "Virtual remote device $id" } } - val model = DeviceCollectiveModel(context, devices) + val model = DeviceCollectiveModel(context, devices, 0.2.kilometers) return model } -fun RemoteDevice.moveInCircles(): Job = launch { +fun CollectiveDevice.moveInCircles(): Job = launch { var bearing = Random.nextDouble(-PI, PI).radians - write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) while (isActive) { delay(500) bearing += 5.degrees - write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) } } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 0c7f7ad..1db8621 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -1,27 +1,42 @@ -package space.kscience.controls.demo.map +@file:OptIn(ExperimentalFoundationApi::class, ExperimentalSplitPaneApi::class) +package space.kscience.controls.demo.collective + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.Checkbox +import androidx.compose.material.Text import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi +import org.jetbrains.compose.splitpane.HorizontalSplitPane +import org.jetbrains.compose.splitpane.rememberSplitPaneState +import space.kscience.controls.compose.conditional import space.kscience.controls.manager.DeviceManager -import space.kscience.controls.spec.propertyFlow +import space.kscience.controls.spec.useProperty import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.request import space.kscience.maps.compose.MapView import space.kscience.maps.compose.OpenStreetMapTileProvider -import space.kscience.maps.features.ViewConfig -import space.kscience.maps.features.circle -import space.kscience.maps.features.color -import space.kscience.maps.features.rectangle +import space.kscience.maps.features.* import java.nio.file.Path @@ -34,7 +49,6 @@ fun rememberDeviceManager(): DeviceManager = remember { context.request(DeviceManager) } - @Composable fun App() { val scope = rememberCoroutineScope() @@ -47,14 +61,24 @@ fun App() { generateModel(deviceManager.context) } - val devices: Map<RemoteDeviceId, RemoteDevice> = remember { + val devices: Map<DeviceId, CollectiveDevice> = remember { collectiveModel.deviceStates.associate { - val device = RemoteDeviceConstructor(deviceManager.context, it.configuration, it.position, it.velocity) + val device = CollectiveDeviceConstructor( + context = deviceManager.context, + configuration = it.configuration, + position = it.position, + velocity = it.velocity + ) { + collectiveModel.locateVisible(it.id).keys + } device.moveInCircles() it.id to device } } + var selectedDeviceId by remember { mutableStateOf<DeviceId?>(null) } + var showOnlyVisible by remember { mutableStateOf(false) } + val mapTileProvider = remember { OpenStreetMapTileProvider( client = HttpClient(CIO), @@ -62,21 +86,93 @@ fun App() { ) } - MapView( - mapTileProvider = mapTileProvider, - config = ViewConfig() + HorizontalSplitPane( + splitPaneState = rememberSplitPaneState(0.9f) ) { - collectiveModel.deviceStates.forEach { device -> - circle(device.position.value, id = device.id + ".position").color(Color.Red) - device.position.valueFlow.onEach { - circle(device.position.value, id = device.id + ".position").color(Color.Red) - }.launchIn(scope) - } + first(400.dp) { + MapView( + mapTileProvider = mapTileProvider, + config = ViewConfig() + ) { + collectiveModel.deviceStates.forEach { device -> + circle(device.position.value, id = device.id + ".position").color(Color.Red) + device.position.valueFlow.onEach { + circle(device.position.value, id = device.id + ".position", size = 3.dp) + .color(Color.Red) + .modifyAttribute(AlphaAttribute, 0.5f) // does not work right now + }.launchIn(scope) + } - devices.forEach { (id, device) -> - device.propertyFlow(RemoteDevice.position).onEach { position -> - rectangle(position, id = id).color(Color.Blue) - }.launchIn(scope) + devices.forEach { (id, device) -> + device.useProperty(CollectiveDevice.position, scope = scope) { position -> + + val activeDevice = selectedDeviceId?.let { devices[it] } + + if (activeDevice == null || id == selectedDeviceId || !showOnlyVisible || id in activeDevice.listVisible()) { + rectangle( + position, + id = id, + size = if (selectedDeviceId == id) DpSize(10.dp, 10.dp) else DpSize(5.dp, 5.dp) + ).color(if (selectedDeviceId == id) Color.Magenta else Color.Blue) + .modifyAttribute(AlphaAttribute, if (selectedDeviceId == id) 1f else 0.5f) + .onClick { selectedDeviceId = id } + } else { + removeFeature(id) + } + + } + } + } + } + second(200.dp) { + Column { + selectedDeviceId?.let { id -> + Column( + modifier = Modifier + .padding(8.dp) + .border(2.dp, Color.DarkGray) + ) { + Card( + elevation = 16.dp, + ) { + Text( + text = "Выбран: $id", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp).fillMaxWidth() + ) + } + devices[id]?.let { + Text(it.meta.toString(), Modifier.padding(10.dp)) + } + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(10.dp).fillMaxWidth()) { + Text("Показать только видимые") + Checkbox(showOnlyVisible, { showOnlyVisible = it }) + } + } + } + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + devices.forEach { (id, device) -> + Card( + elevation = 16.dp, + modifier = Modifier.padding(8.dp).onClick { + selectedDeviceId = id + }.conditional(id == selectedDeviceId) { + border(2.dp, Color.Blue) + } + ) { + Text( + text = id, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp).fillMaxWidth() + ) + } + } + } + } } } } From e9bde6867466d4fcea57b20a11c46d25856abd60 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 9 Jun 2024 15:09:43 +0300 Subject: [PATCH 104/125] [WIP] remote communication for CollectiveDevice --- .../kscience/controls/peer/PeerConnection.kt | 7 +- .../controls/server/deviceWebServer.kt | 2 + demo/device-collective/build.gradle.kts | 2 + .../src/jvmMain/kotlin/CollectiveDevice.kt | 33 ++- .../jvmMain/kotlin/DeviceCollectiveModel.kt | 86 ++++++- .../src/jvmMain/kotlin/SampleDeviceState.kt | 19 +- .../src/jvmMain/kotlin/debugModel.kt | 13 +- .../src/jvmMain/kotlin/main.kt | 218 +++++++++++------- .../kscience/controls/demo/MassDevice.kt | 3 +- .../kscience/magix/api/MagixFlowPlugin.kt | 13 +- .../kscience/magix/server/magixModule.kt | 57 +++-- .../space/kscience/magix/server/server.kt | 2 +- 12 files changed, 312 insertions(+), 143 deletions(-) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt index 14b49ef..a831207 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt @@ -2,14 +2,13 @@ package space.kscience.controls.peer import space.kscience.dataforge.io.Envelope import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.names.Name /** * A manager that allows direct synchronous sending and receiving binary data */ public interface PeerConnection { /** - * Receive an [Envelope] from a device with name [deviceName] on a given [address] with given [contentId]. + * Receive an [Envelope] from a device on a given [address] with given [contentId]. * * The address depends on the specifics of given [PeerConnection]. For example, it could be a TCP/IP port or * magix endpoint name. @@ -20,13 +19,12 @@ public interface PeerConnection { */ public suspend fun receive( address: String, - deviceName: Name, contentId: String, requestMeta: Meta = Meta.EMPTY, ): Envelope /** - * Send an [envelope] to a device with name [deviceName] on a given [address] + * Send an [envelope] to a device on a given [address] * * The address depends on the specifics of given [PeerConnection]. For example, it could be a TCP/IP port or * magix endpoint name. @@ -35,7 +33,6 @@ public interface PeerConnection { */ public suspend fun send( address: String, - deviceName: Name, envelope: Envelope, requestMeta: Meta = Meta.EMPTY, ) 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 4f37322..8fd104e 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 @@ -38,6 +38,7 @@ import space.kscience.dataforge.names.get import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.MagixFlowPlugin import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.start import space.kscience.magix.server.magixModule @@ -215,5 +216,6 @@ public fun Application.deviceManagerModule( plugins.forEach { it.start(this, magixFlow) } + magixModule(magixFlow) } \ No newline at end of file diff --git a/demo/device-collective/build.gradle.kts b/demo/device-collective/build.gradle.kts index 8bb0597..3290a0d 100644 --- a/demo/device-collective/build.gradle.kts +++ b/demo/device-collective/build.gradle.kts @@ -13,7 +13,9 @@ kscience { commonMain { implementation(projects.controlsVisualisationCompose) implementation(projects.controlsConstructor) + implementation(projects.magix.magixServer) implementation(projects.magix.magixRsocket) + implementation(projects.controlsMagix) } jvmMain { // implementation("io.ktor:ktor-server-cio") diff --git a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 050794c..9fce04d 100644 --- a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -6,24 +6,35 @@ import space.kscience.controls.api.Device import space.kscience.controls.constructor.DeviceConstructor import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.registerAsProperty +import space.kscience.controls.peer.PeerConnection import space.kscience.controls.spec.DeviceSpec +import space.kscience.controls.spec.unit import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.Scheme +import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.string import space.kscience.dataforge.misc.DFExperimental import space.kscience.maps.coordinates.Gmc +import space.kscience.maps.coordinates.GmcCurve import kotlin.time.Duration.Companion.milliseconds -class CollectiveDeviceConfiguration(deviceId: DeviceId) : Scheme() { +typealias CollectiveDeviceId = String + +class CollectiveDeviceConfiguration(deviceId: CollectiveDeviceId) : Scheme() { var deviceId by string(deviceId) var description by string() + var reportInterval by int(500) + var radioFrequency by string(default = "169 MHz") } +typealias CollectiveDeviceRoster = Map<CollectiveDeviceId, CollectiveDeviceConfiguration> interface CollectiveDevice : Device { - public val id: DeviceId + public val id: CollectiveDeviceId + + public val peerConnection: PeerConnection suspend fun getPosition(): Gmc @@ -31,8 +42,7 @@ interface CollectiveDevice : Device { suspend fun setVelocity(value: GmcVelocity) - suspend fun listVisible(): Collection<DeviceId> - + suspend fun listVisible(): Collection<CollectiveDeviceId> companion object : DeviceSpec<CollectiveDevice>() { val position by property<Gmc>( @@ -45,6 +55,10 @@ interface CollectiveDevice : Device { read = { getVelocity() }, write = { _, value -> setVelocity(value) } ) + + val listVisible by action(MetaConverter.unit, MetaConverter.valueList<String> { it.string }) { + listVisible().toList() + } } } @@ -54,13 +68,14 @@ class CollectiveDeviceConstructor( val configuration: CollectiveDeviceConfiguration, position: MutableDeviceState<Gmc>, velocity: MutableDeviceState<GmcVelocity>, - private val listVisible: suspend () -> Collection<DeviceId>, + override val peerConnection: PeerConnection, + private val observation: suspend () -> Map<CollectiveDeviceId, GmcCurve>, ) : DeviceConstructor(context, configuration.meta), CollectiveDevice { - override val id: DeviceId get() = configuration.deviceId + override val id: CollectiveDeviceId get() = configuration.deviceId - val position = registerAsProperty(CollectiveDevice.position, position.sample(500.milliseconds)) - val velocity = registerAsProperty(CollectiveDevice.velocity, velocity) + val position = registerAsProperty(CollectiveDevice.position, position.sample(configuration.reportInterval.milliseconds)) + val velocity = registerAsProperty(CollectiveDevice.velocity, velocity.sample(configuration.reportInterval.milliseconds)) override suspend fun getPosition(): Gmc = position.value @@ -70,5 +85,5 @@ class CollectiveDeviceConstructor( velocity.value = value } - override suspend fun listVisible(): Collection<DeviceId> = listVisible.invoke() + override suspend fun listVisible(): Collection<CollectiveDeviceId> = observation.invoke().keys } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index 4abedea..2c325b4 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -1,27 +1,41 @@ package space.kscience.controls.demo.collective +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import space.kscience.controls.client.launchMagixService import space.kscience.controls.constructor.ModelConstructor import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.onTimer +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.controls.peer.PeerConnection import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request +import space.kscience.dataforge.io.Envelope +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.names.parseAsName +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.rsocket.rSocketWithWebSockets +import space.kscience.magix.server.startMagixServer import space.kscience.maps.coordinates.* +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds -typealias DeviceId = String - - -internal data class VirtualDeviceState( - val id: DeviceId, +internal data class CollectiveDeviceState( + val id: CollectiveDeviceId, val configuration: CollectiveDeviceConfiguration, val position: MutableDeviceState<Gmc>, val velocity: MutableDeviceState<GmcVelocity>, ) internal fun VirtualDeviceState( - id: DeviceId, + id: CollectiveDeviceId, position: Gmc, configuration: CollectiveDeviceConfiguration.() -> Unit = {}, -) = VirtualDeviceState( +) = CollectiveDeviceState( id, CollectiveDeviceConfiguration(id).apply(configuration), MutableDeviceState(position), @@ -31,9 +45,11 @@ internal fun VirtualDeviceState( internal class DeviceCollectiveModel( context: Context, - val deviceStates: Collection<VirtualDeviceState>, - val visibilityRange: Distance, -) : ModelConstructor(context) { + val deviceStates: Collection<CollectiveDeviceState>, + val visibilityRange: Distance = 0.4.kilometers, + val radioRange: Distance = 5.kilometers, + val reportInterval: Duration = 1000.milliseconds +) : ModelConstructor(context), PeerConnection { /** * Propagate movement @@ -45,7 +61,7 @@ internal class DeviceCollectiveModel( } } - suspend fun locateVisible(id: DeviceId): Map<DeviceId, GmcCurve> { + private fun locateVisible(id: CollectiveDeviceId): Map<CollectiveDeviceId, GmcCurve> { val coordinatesSnapshot = deviceStates.associate { it.id to it.position.value } val selected = coordinatesSnapshot[id] ?: error("Can't find device with id $id") @@ -56,4 +72,52 @@ internal class DeviceCollectiveModel( return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange } } + + val devices = deviceStates.associate { + val device = CollectiveDeviceConstructor( + context = context, + configuration = it.configuration, + position = it.position, + velocity = it.velocity, + peerConnection = this, + ) { + locateVisible(it.id) + } + //start movement program + device.moveInCircles() + it.id to device + } + + val roster = deviceStates.associate { it.id to it.configuration } + + override suspend fun receive(address: String, contentId: String, requestMeta: Meta): Envelope { + TODO("Not yet implemented") + } + + override suspend fun send(address: String, envelope: Envelope, requestMeta: Meta) { +// devices.values.filter { it.configuration.radioFrequency == address }.forEach { device -> +// ``` +// } + } +} + +internal fun CoroutineScope.launchCollectiveMagixServer( + collectiveModel: DeviceCollectiveModel, +): Job = launch(Dispatchers.IO) { + val server = startMagixServer( +// RSocketMagixFlowPlugin() + ) + + collectiveModel.devices.forEach { (id, device) -> + val deviceContext = collectiveModel.context.buildContext(id.parseAsName()) { + coroutineContext(coroutineContext) + plugin(DeviceManager) + } + + deviceContext.install(id, device) + + val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + + deviceContext.request(DeviceManager).launchMagixService(deviceEndpoint, id) + } } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt b/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt index a2ef06c..499c903 100644 --- a/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt +++ b/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.sample import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.MutableDeviceState import kotlin.time.Duration @OptIn(FlowPreview::class) @@ -11,10 +12,24 @@ class SampleDeviceState<T>( val origin: DeviceState<T>, val interval: Duration, ) : DeviceState<T> { - override val value: T get() = origin.value + override val value: T by origin::value override val valueFlow: Flow<T> get() = origin.valueFlow.sample(interval) override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" } -fun <T> DeviceState<T>.sample(interval: Duration) = SampleDeviceState(this, interval) \ No newline at end of file + +fun <T> DeviceState<T>.sample(interval: Duration) = SampleDeviceState(this, interval) + +@OptIn(FlowPreview::class) +class MutableSampleDeviceState<T>( + val origin: MutableDeviceState<T>, + val interval: Duration, +) : MutableDeviceState<T> { + override var value: T by origin::value + override val valueFlow: Flow<T> get() = origin.valueFlow.sample(interval) + + override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" +} + +fun <T> MutableDeviceState<T>.sample(interval: Duration) = MutableSampleDeviceState(this, interval) \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt index 96c9ca8..d0ec384 100644 --- a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -12,6 +12,8 @@ import space.kscience.maps.coordinates.Gmc import space.kscience.maps.coordinates.kilometers import kotlin.math.PI import kotlin.random.Random +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds private val deviceVelocity = 0.1.kilometers @@ -19,8 +21,13 @@ private val center = Gmc.ofDegrees(55.925, 37.514) private val radius = 0.01.degrees -internal fun generateModel(context: Context): DeviceCollectiveModel { - val devices: List<VirtualDeviceState> = List(100) { index -> +internal fun generateModel( + context: Context, + size: Int = 50, + reportInterval: Duration = 500.milliseconds, + additionalConfiguration: CollectiveDeviceConfiguration.() -> Unit = {}, +): DeviceCollectiveModel { + val devices: List<CollectiveDeviceState> = List(size) { index -> val id = "device[$index]" VirtualDeviceState( @@ -32,6 +39,8 @@ internal fun generateModel(context: Context): DeviceCollectiveModel { ) { deviceId = id description = "Virtual remote device $id" + this.reportInterval = reportInterval.inWholeMilliseconds.toInt() + additionalConfiguration() } } diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 1db8621..9aff323 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -24,74 +24,95 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO +import kotlinx.coroutines.* import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.client.* import space.kscience.controls.compose.conditional import space.kscience.controls.manager.DeviceManager -import space.kscience.controls.spec.useProperty import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.request +import space.kscience.dataforge.context.ContextBuilder +import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.names.parseAsName +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.subscribe +import space.kscience.magix.rsocket.rSocketWithWebSockets import space.kscience.maps.compose.MapView import space.kscience.maps.compose.OpenStreetMapTileProvider +import space.kscience.maps.coordinates.Gmc +import space.kscience.maps.coordinates.meters import space.kscience.maps.features.* import java.nio.file.Path +import kotlin.time.Duration.Companion.seconds @Composable -fun rememberDeviceManager(): DeviceManager = remember { - val context = Context { - plugin(DeviceManager) - } - - context.request(DeviceManager) +fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {}): Context = remember { + Context(name, contextBuilder) } @Composable fun App() { val scope = rememberCoroutineScope() - - val deviceManager = rememberDeviceManager() - - - val collectiveModel = remember { - generateModel(deviceManager.context) + val parentContext = rememberContext("Parent") { + plugin(DeviceManager) } - val devices: Map<DeviceId, CollectiveDevice> = remember { - collectiveModel.deviceStates.associate { - val device = CollectiveDeviceConstructor( - context = deviceManager.context, - configuration = it.configuration, - position = it.position, - velocity = it.velocity - ) { - collectiveModel.locateVisible(it.id).keys + val collectiveModel = remember { + generateModel(parentContext, 60) + } + + val roster = remember { + collectiveModel.roster + } + + val client = remember { CompletableDeferred<MagixEndpoint>() } + + val devices = remember { mutableStateMapOf<CollectiveDeviceId, DeviceClient>() } + + + LaunchedEffect(collectiveModel) { + launchCollectiveMagixServer(collectiveModel) + withContext(Dispatchers.IO) { + val magixClient = MagixEndpoint.rSocketWithWebSockets("localhost") + + client.complete(magixClient) + + collectiveModel.roster.forEach { (id, config) -> + devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) } - device.moveInCircles() - it.id to device + } + + } + + var selectedDeviceId by remember { mutableStateOf<CollectiveDeviceId?>(null) } + + var currentPosition by remember { mutableStateOf<Gmc?>(null) } + + LaunchedEffect(selectedDeviceId, devices) { + selectedDeviceId?.let { devices[it] }?.propertyFlow(CollectiveDevice.position)?.collect { + currentPosition = it } } - var selectedDeviceId by remember { mutableStateOf<DeviceId?>(null) } var showOnlyVisible by remember { mutableStateOf(false) } - val mapTileProvider = remember { - OpenStreetMapTileProvider( - client = HttpClient(CIO), - cacheDirectory = Path.of("mapCache") - ) - } - HorizontalSplitPane( splitPaneState = rememberSplitPaneState(0.9f) ) { first(400.dp) { MapView( - mapTileProvider = mapTileProvider, + mapTileProvider = remember { + OpenStreetMapTileProvider( + client = HttpClient(CIO), + cacheDirectory = Path.of("mapCache") + ) + }, config = ViewConfig() ) { collectiveModel.deviceStates.forEach { device -> @@ -103,65 +124,61 @@ fun App() { }.launchIn(scope) } - devices.forEach { (id, device) -> - device.useProperty(CollectiveDevice.position, scope = scope) { position -> + scope.launch { - val activeDevice = selectedDeviceId?.let { devices[it] } + client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> + if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { + val id = magixMessage.sourceEndpoint + val position = MetaConverter.serializable<Gmc>().read(deviceMessage.value) + val activeDevice = selectedDeviceId?.let { devices[it] } - if (activeDevice == null || id == selectedDeviceId || !showOnlyVisible || id in activeDevice.listVisible()) { - rectangle( - position, - id = id, - size = if (selectedDeviceId == id) DpSize(10.dp, 10.dp) else DpSize(5.dp, 5.dp) - ).color(if (selectedDeviceId == id) Color.Magenta else Color.Blue) - .modifyAttribute(AlphaAttribute, if (selectedDeviceId == id) 1f else 0.5f) - .onClick { selectedDeviceId = id } - } else { - removeFeature(id) + suspend fun DeviceClient.idIsVisible() = try { + withTimeout(1.seconds) { + id in execute(CollectiveDevice.listVisible) + } + } catch (ex: Exception) { + ex.printStackTrace() + true + } + + if ( + activeDevice == null || + id == selectedDeviceId || + !showOnlyVisible || + activeDevice.idIsVisible() + ) { + rectangle( + position, + id = id, + size = if (selectedDeviceId == id) DpSize(10.dp, 10.dp) else DpSize(5.dp, 5.dp) + ).color(if (selectedDeviceId == id) Color.Magenta else Color.Blue) + .modifyAttribute(AlphaAttribute, if (selectedDeviceId == id) 1f else 0.5f) + .onClick { selectedDeviceId = id } + } else { + removeFeature(id) + } } + }.launchIn(scope) - } } } } second(200.dp) { - Column { - selectedDeviceId?.let { id -> - Column( - modifier = Modifier - .padding(8.dp) - .border(2.dp, Color.DarkGray) + + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + devices.forEach { (id, _) -> + Card( + elevation = 16.dp, + modifier = Modifier.padding(8.dp).onClick { + selectedDeviceId = id + }.conditional(id == selectedDeviceId) { + border(2.dp, Color.Blue) + } ) { - Card( - elevation = 16.dp, - ) { - Text( - text = "Выбран: $id", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(10.dp).fillMaxWidth() - ) - } - devices[id]?.let { - Text(it.meta.toString(), Modifier.padding(10.dp)) - } - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(10.dp).fillMaxWidth()) { - Text("Показать только видимые") - Checkbox(showOnlyVisible, { showOnlyVisible = it }) - } - } - } - Column( - modifier = Modifier.verticalScroll(rememberScrollState()) - ) { - devices.forEach { (id, device) -> - Card( - elevation = 16.dp, - modifier = Modifier.padding(8.dp).onClick { - selectedDeviceId = id - }.conditional(id == selectedDeviceId) { - border(2.dp, Color.Blue) - } + Column( + modifier = Modifier.padding(8.dp) ) { Text( text = id, @@ -169,16 +186,49 @@ fun App() { fontWeight = FontWeight.Bold, modifier = Modifier.padding(10.dp).fillMaxWidth() ) + if (id == selectedDeviceId) { + roster[id]?.let { + Text("Meta:", color = Color.Blue, fontWeight = FontWeight.Bold) + Card(elevation = 16.dp, modifier = Modifier.fillMaxWidth().padding(8.dp)) { + Text(it.toString()) + } + } + + currentPosition?.let { currentPosition -> + Text("Широта: ${String.format("%.3f", currentPosition.latitude.toDegrees().value)}") + Text( + "Долгота: ${ + String.format( + "%.3f", + currentPosition.longitude.toDegrees().value + ) + }" + ) + currentPosition.elevation?.let { + Text("Высота: ${String.format("%.1f", it.meters)} м") + } + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text("Показать только видимые") + Checkbox(showOnlyVisible, { showOnlyVisible = it }) + } + } } } } } + } } } fun main() = application { +// System.setProperty(IO_PARALLELISM_PROPERTY_NAME, 300.toString()) Window(onCloseRequest = ::exitApplication, title = "Maps-kt demo", icon = painterResource("SPC-logo.png")) { MaterialTheme { App() 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 5eafabf..53c4c7f 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 @@ -83,8 +83,7 @@ suspend fun main() { val endpointId = "device$it" val deviceEndpoint = MagixEndpoint.rSocketStreamWithWebSockets("localhost") - deviceManager.launchMagixService(deviceEndpoint, endpointId, Dispatchers.IO) - + deviceManager.launchMagixService(deviceEndpoint, endpointId) } val trace = Bar { diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFlowPlugin.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFlowPlugin.kt index 83c95cc..1a3dd75 100644 --- a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFlowPlugin.kt +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFlowPlugin.kt @@ -21,9 +21,10 @@ public fun interface MagixFlowPlugin { sendMessage: suspend (MagixMessage) -> Unit, ): Job - /** - * Use the same [MutableSharedFlow] to send and receive messages. Could be a bottleneck in case of many plugins. - */ - public fun start(scope: CoroutineScope, magixFlow: MutableSharedFlow<MagixMessage>): Job = - start(scope, magixFlow) { magixFlow.emit(it) } -} \ No newline at end of file +} + +/** + * Use the same [MutableSharedFlow] to send and receive messages. Could be a bottleneck in case of many plugins. + */ +public fun MagixFlowPlugin.start(scope: CoroutineScope, magixFlow: MutableSharedFlow<MagixMessage>): Job = + start(scope, magixFlow) { magixFlow.emit(it) } \ No newline at end of file diff --git a/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt index dc197ad..a1edec5 100644 --- a/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt +++ b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt @@ -10,7 +10,9 @@ import io.ktor.server.util.getValue import io.ktor.server.websocket.WebSockets import io.rsocket.kotlin.ktor.server.RSocketSupport import io.rsocket.kotlin.ktor.server.rSocket +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.map import kotlinx.html.* import kotlinx.serialization.encodeToString @@ -42,7 +44,11 @@ private fun ApplicationCall.buildFilter(): MagixMessageFilter { /** * Attach magix http/sse and websocket-based rsocket event loop + statistics page to existing [MutableSharedFlow] */ -public fun Application.magixModule(magixFlow: MutableSharedFlow<MagixMessage>, route: String = "/") { +public fun Application.magixModule( + magixFlow: Flow<MagixMessage>, + send: suspend (MagixMessage) -> Unit, + route: String = "/", +) { if (pluginOrNull(WebSockets) == null) { install(WebSockets) } @@ -62,27 +68,31 @@ public fun Application.magixModule(magixFlow: MutableSharedFlow<MagixMessage>, r routing { route(route) { - install(ContentNegotiation){ + install(ContentNegotiation) { json() } - get("state") { - call.respondHtml { - head { - meta { - httpEquiv = "refresh" - content = "2" + if (magixFlow is SharedFlow) { + get("state") { + call.respondHtml { + head { + meta { + httpEquiv = "refresh" + content = "2" + } } - } - body { - h1 { +"Magix loop statistics" } - h2 { +"Number of subscribers: ${magixFlow.subscriptionCount.value}" } - h3 { +"Replay cache size: ${magixFlow.replayCache.size}" } - h3 { +"Replay cache:" } - ol { - magixFlow.replayCache.forEach { message -> - li { - code { - +magixJson.encodeToString(message) + body { + h1 { +"Magix loop statistics" } + if (magixFlow is MutableSharedFlow) { + h2 { +"Number of subscribers: ${magixFlow.subscriptionCount.value}" } + } + h3 { +"Replay cache size: ${magixFlow.replayCache.size}" } + h3 { +"Replay cache:" } + ol { + magixFlow.replayCache.forEach { message -> + li { + code { + +magixJson.encodeToString(message) + } } } } @@ -102,17 +112,22 @@ public fun Application.magixModule(magixFlow: MutableSharedFlow<MagixMessage>, r } post("broadcast") { val message = call.receive<MagixMessage>() - magixFlow.emit(message) + send(message) } //rSocket WS server. Filter from Payload rSocket( "rsocket", - acceptor = RSocketMagixFlowPlugin.acceptor(application, magixFlow) { magixFlow.emit(it) } + acceptor = RSocketMagixFlowPlugin.acceptor(application, magixFlow) { send(it) } ) } } } +public fun Application.magixModule( + magixFlow: MutableSharedFlow<MagixMessage>, + route: String = "/", +): Unit = magixModule(magixFlow, { magixFlow.emit(it) }, route) + /** * Create a new loop [MutableSharedFlow] with given [buffer] and setup magix module based on it */ diff --git a/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt index 2396e25..aa26bee 100644 --- a/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt +++ b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import space.kscience.magix.api.MagixEndpoint.Companion.DEFAULT_MAGIX_HTTP_PORT import space.kscience.magix.api.MagixFlowPlugin import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.start /** @@ -22,7 +23,6 @@ public fun CoroutineScope.startMagixServer( val magixFlow = MutableSharedFlow<MagixMessage>( replay = buffer, - extraBufferCapacity = buffer, onBufferOverflow = BufferOverflow.DROP_OLDEST ) From c55ce2cf9a393f43829d792de9f14dd93027353f Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 9 Jun 2024 20:51:12 +0300 Subject: [PATCH 105/125] Fix visibility range for collective --- .../controls/constructor/DeviceState.kt | 15 ++--- .../kscience/controls/misc/converters.kt | 17 ++++-- .../controls/client/clientPropertyAccess.kt | 5 +- .../src/jvmMain/kotlin/CollectiveDevice.kt | 43 +++++++++++--- .../jvmMain/kotlin/DeviceCollectiveModel.kt | 8 +-- .../src/jvmMain/kotlin/main.kt | 57 +++++++++---------- 6 files changed, 89 insertions(+), 56 deletions(-) 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 846a37b..5b547f7 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 @@ -74,15 +74,16 @@ public fun <T, R> DeviceState.Companion.map( public fun <T, R> DeviceState<T>.map(mapper: (T) -> R): DeviceStateWithDependencies<R> = DeviceState.map(this, mapper) -public fun DeviceState<NumericalValue<out UnitsOfMeasurement>>.values(): DeviceState<Double> = object : DeviceState<Double> { - override val value: Double - get() = this@values.value.value +public fun DeviceState<NumericalValue<out UnitsOfMeasurement>>.values(): DeviceState<Double> = + object : DeviceState<Double> { + override val value: Double + get() = this@values.value.value - override val valueFlow: Flow<Double> - get() = this@values.valueFlow.map { it.value } + override val valueFlow: Flow<Double> + get() = this@values.valueFlow.map { it.value } - override fun toString(): String = this@values.toString() -} + override fun toString(): String = this@values.toString() + } /** * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used. diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt index 4297d20..c200758 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt @@ -41,13 +41,22 @@ private object InstantConverter : MetaConverter<Instant> { public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter private object DoubleRangeConverter : MetaConverter<ClosedFloatingPointRange<Double>> { - override fun readOrNull(source: Meta): ClosedFloatingPointRange<Double>? = source.value?.doubleArray?.let { (start, end)-> - start..end - } + override fun readOrNull(source: Meta): ClosedFloatingPointRange<Double>? = + source.value?.doubleArray?.let { (start, end) -> + start..end + } override fun convert( obj: ClosedFloatingPointRange<Double>, ): Meta = Meta(doubleArrayOf(obj.start, obj.endInclusive).asValue()) } -public val MetaConverter.Companion.doubleRange: MetaConverter<ClosedFloatingPointRange<Double>> get() = DoubleRangeConverter \ No newline at end of file +public val MetaConverter.Companion.doubleRange: MetaConverter<ClosedFloatingPointRange<Double>> get() = DoubleRangeConverter + +private object StringListConverter : MetaConverter<List<String>> { + override fun convert(obj: List<String>): Meta = Meta(obj.map { it.asValue() }.asValue()) + + override fun readOrNull(source: Meta): List<String>? = source.stringList ?: source["@jsonArray"]?.stringList +} + +public val MetaConverter.Companion.stringList: MetaConverter<List<String>> get() = StringListConverter 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 20c59e8..10b1196 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 @@ -19,10 +19,13 @@ import space.kscience.dataforge.meta.Meta public suspend fun <T> DeviceClient.read(propertySpec: DevicePropertySpec<*, T>): T = propertySpec.converter.readOrNull(readProperty(propertySpec.name)) ?: error("Property read result is not valid") - public suspend fun <T> DeviceClient.request(propertySpec: DevicePropertySpec<*, T>): T = propertySpec.converter.read(getOrReadProperty(propertySpec.name)) +public fun <T> DeviceClient.getCached(propertySpec: DevicePropertySpec<*, T>): T? = + getProperty(propertySpec.name)?.let { propertySpec.converter.read(it) } + + public suspend fun <T> DeviceClient.write(propertySpec: MutableDevicePropertySpec<*, T>, value: T) { writeProperty(propertySpec.name, propertySpec.converter.convert(value)) } diff --git a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 9fce04d..941068c 100644 --- a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -3,12 +3,10 @@ package space.kscience.controls.demo.collective import space.kscience.controls.api.Device -import space.kscience.controls.constructor.DeviceConstructor -import space.kscience.controls.constructor.MutableDeviceState -import space.kscience.controls.constructor.registerAsProperty +import space.kscience.controls.constructor.* +import space.kscience.controls.misc.stringList import space.kscience.controls.peer.PeerConnection import space.kscience.controls.spec.DeviceSpec -import space.kscience.controls.spec.unit import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.Scheme @@ -56,9 +54,16 @@ interface CollectiveDevice : Device { write = { _, value -> setVelocity(value) } ) - val listVisible by action(MetaConverter.unit, MetaConverter.valueList<String> { it.string }) { - listVisible().toList() - } + val visibleNeighbors by property( + MetaConverter.stringList, + read = { + listVisible().toList() + } + ) + +// val listVisible by action(MetaConverter.unit, MetaConverter.valueList<String> { it.string }) { +// listVisible().toList() +// } } } @@ -74,8 +79,28 @@ class CollectiveDeviceConstructor( override val id: CollectiveDeviceId get() = configuration.deviceId - val position = registerAsProperty(CollectiveDevice.position, position.sample(configuration.reportInterval.milliseconds)) - val velocity = registerAsProperty(CollectiveDevice.velocity, velocity.sample(configuration.reportInterval.milliseconds)) + val position = registerAsProperty( + CollectiveDevice.position, + position.sample(configuration.reportInterval.milliseconds) + ) + + val velocity = registerAsProperty( + CollectiveDevice.velocity, + velocity.sample(configuration.reportInterval.milliseconds) + ) + + private val _visibleNeighbors: MutableDeviceState<Collection<CollectiveDeviceId>> = stateOf(emptyList()) + + val visibleNeighbors = registerAsProperty( + CollectiveDevice.visibleNeighbors, + _visibleNeighbors.map { it.toList() } + ) + + init { + position.onNext { + _visibleNeighbors.value = observation.invoke().keys + } + } override suspend fun getPosition(): Gmc = position.value diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index 2c325b4..d8c64dd 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -20,8 +20,6 @@ import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.rsocket.rSocketWithWebSockets import space.kscience.magix.server.startMagixServer import space.kscience.maps.coordinates.* -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds internal data class CollectiveDeviceState( @@ -46,9 +44,8 @@ internal fun VirtualDeviceState( internal class DeviceCollectiveModel( context: Context, val deviceStates: Collection<CollectiveDeviceState>, - val visibilityRange: Distance = 0.4.kilometers, + val visibilityRange: Distance = 1.kilometers, val radioRange: Distance = 5.kilometers, - val reportInterval: Duration = 1000.milliseconds ) : ModelConstructor(context), PeerConnection { /** @@ -107,6 +104,7 @@ internal fun CoroutineScope.launchCollectiveMagixServer( val server = startMagixServer( // RSocketMagixFlowPlugin() ) + val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") collectiveModel.devices.forEach { (id, device) -> val deviceContext = collectiveModel.context.buildContext(id.parseAsName()) { @@ -116,7 +114,7 @@ internal fun CoroutineScope.launchCollectiveMagixServer( deviceContext.install(id, device) - val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") +// val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") deviceContext.request(DeviceManager).launchMagixService(deviceEndpoint, id) } diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 9aff323..1292a0e 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.Card import androidx.compose.material.Checkbox import androidx.compose.material.Text +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -24,9 +25,12 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO -import kotlinx.coroutines.* +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState @@ -47,7 +51,6 @@ import space.kscience.maps.coordinates.Gmc import space.kscience.maps.coordinates.meters import space.kscience.maps.features.* import java.nio.file.Path -import kotlin.time.Duration.Companion.seconds @Composable @@ -55,6 +58,8 @@ fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {} Context(name, contextBuilder) } +private val gmcMetaConverter = MetaConverter.serializable<Gmc>() + @Composable fun App() { val scope = rememberCoroutineScope() @@ -82,8 +87,7 @@ fun App() { val magixClient = MagixEndpoint.rSocketWithWebSockets("localhost") client.complete(magixClient) - - collectiveModel.roster.forEach { (id, config) -> + collectiveModel.roster.forEach { (id, config) -> devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) } } @@ -129,23 +133,14 @@ fun App() { client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { val id = magixMessage.sourceEndpoint - val position = MetaConverter.serializable<Gmc>().read(deviceMessage.value) + val position = gmcMetaConverter.read(deviceMessage.value) val activeDevice = selectedDeviceId?.let { devices[it] } - suspend fun DeviceClient.idIsVisible() = try { - withTimeout(1.seconds) { - id in execute(CollectiveDevice.listVisible) - } - } catch (ex: Exception) { - ex.printStackTrace() - true - } - if ( activeDevice == null || id == selectedDeviceId || !showOnlyVisible || - activeDevice.idIsVisible() + id in activeDevice.request(CollectiveDevice.visibleNeighbors) ) { rectangle( position, @@ -168,24 +163,29 @@ fun App() { Column( modifier = Modifier.verticalScroll(rememberScrollState()) ) { - devices.forEach { (id, _) -> + collectiveModel.roster.forEach { (id, _) -> Card( elevation = 16.dp, modifier = Modifier.padding(8.dp).onClick { selectedDeviceId = id }.conditional(id == selectedDeviceId) { border(2.dp, Color.Blue) - } + }, ) { Column( modifier = Modifier.padding(8.dp) ) { - Text( - text = id, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(10.dp).fillMaxWidth() - ) + Row(verticalAlignment = Alignment.CenterVertically) { + if (devices[id] == null) { + CircularProgressIndicator() + } + Text( + text = id, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp).fillMaxWidth(), + ) + } if (id == selectedDeviceId) { roster[id]?.let { Text("Meta:", color = Color.Blue, fontWeight = FontWeight.Bold) @@ -195,14 +195,11 @@ fun App() { } currentPosition?.let { currentPosition -> - Text("Широта: ${String.format("%.3f", currentPosition.latitude.toDegrees().value)}") Text( - "Долгота: ${ - String.format( - "%.3f", - currentPosition.longitude.toDegrees().value - ) - }" + "Широта: ${String.format("%.3f", currentPosition.latitude.toDegrees().value)}" + ) + Text( + "Долгота: ${String.format("%.3f", currentPosition.longitude.toDegrees().value)}" ) currentPosition.elevation?.let { Text("Высота: ${String.format("%.1f", it.meters)} м") From 60a693b1b3eaebc0ca9a2e43dbd2b7cbd096a418 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 9 Jun 2024 21:12:18 +0300 Subject: [PATCH 106/125] Fix visibility range for collective --- .../src/jvmMain/kotlin/DeviceCollectiveModel.kt | 2 +- .../src/jvmMain/kotlin/debugModel.kt | 2 +- demo/device-collective/src/jvmMain/kotlin/main.kt | 11 ++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index d8c64dd..510ab3e 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -44,7 +44,7 @@ internal fun VirtualDeviceState( internal class DeviceCollectiveModel( context: Context, val deviceStates: Collection<CollectiveDeviceState>, - val visibilityRange: Distance = 1.kilometers, + val visibilityRange: Distance = 0.5.kilometers, val radioRange: Distance = 5.kilometers, ) : ModelConstructor(context), PeerConnection { diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt index d0ec384..cdca5cd 100644 --- a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -44,7 +44,7 @@ internal fun generateModel( } } - val model = DeviceCollectiveModel(context, devices, 0.2.kilometers) + val model = DeviceCollectiveModel(context, devices) return model } diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 1292a0e..b05f038 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -51,6 +51,7 @@ import space.kscience.maps.coordinates.Gmc import space.kscience.maps.coordinates.meters import space.kscience.maps.features.* import java.nio.file.Path +import kotlin.time.Duration.Companion.seconds @Composable @@ -69,7 +70,7 @@ fun App() { } val collectiveModel = remember { - generateModel(parentContext, 60) + generateModel(parentContext, 100, reportInterval = 1.seconds) } val roster = remember { @@ -83,12 +84,16 @@ fun App() { LaunchedEffect(collectiveModel) { launchCollectiveMagixServer(collectiveModel) + withContext(Dispatchers.IO) { val magixClient = MagixEndpoint.rSocketWithWebSockets("localhost") client.complete(magixClient) - collectiveModel.roster.forEach { (id, config) -> - devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) + + collectiveModel.roster.forEach { (id, config) -> + scope.launch { + devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) + } } } From a5bb42706b1048854bf1223b4bd34f2e2172800e Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Wed, 12 Jun 2024 11:56:27 +0300 Subject: [PATCH 107/125] Change visualization for collective --- .../kscience/controls/peer/PeerConnection.kt | 2 +- .../src/jvmMain/kotlin/CollectiveDevice.kt | 4 +- ...eDeviceState.kt => DebounceDeviceState.kt} | 11 +-- .../jvmMain/kotlin/DeviceCollectiveModel.kt | 39 +++++--- .../src/jvmMain/kotlin/debugModel.kt | 10 +-- .../src/jvmMain/kotlin/main.kt | 88 +++++++++++++------ 6 files changed, 102 insertions(+), 52 deletions(-) rename demo/device-collective/src/jvmMain/kotlin/{SampleDeviceState.kt => DebounceDeviceState.kt} (70%) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt index a831207..55624b7 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt @@ -21,7 +21,7 @@ public interface PeerConnection { address: String, contentId: String, requestMeta: Meta = Meta.EMPTY, - ): Envelope + ): Envelope? /** * Send an [envelope] to a device on a given [address] diff --git a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 941068c..82170b9 100644 --- a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -81,12 +81,12 @@ class CollectiveDeviceConstructor( val position = registerAsProperty( CollectiveDevice.position, - position.sample(configuration.reportInterval.milliseconds) + position.debounce(configuration.reportInterval.milliseconds) ) val velocity = registerAsProperty( CollectiveDevice.velocity, - velocity.sample(configuration.reportInterval.milliseconds) + velocity.debounce(configuration.reportInterval.milliseconds) ) private val _visibleNeighbors: MutableDeviceState<Collection<CollectiveDeviceId>> = stateOf(emptyList()) diff --git a/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt b/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt similarity index 70% rename from demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt rename to demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt index 499c903..ac699e6 100644 --- a/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt @@ -2,27 +2,28 @@ package space.kscience.controls.demo.collective import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.sample import space.kscience.controls.constructor.DeviceState import space.kscience.controls.constructor.MutableDeviceState import kotlin.time.Duration @OptIn(FlowPreview::class) -class SampleDeviceState<T>( +class DebounceDeviceState<T>( val origin: DeviceState<T>, val interval: Duration, ) : DeviceState<T> { override val value: T by origin::value - override val valueFlow: Flow<T> get() = origin.valueFlow.sample(interval) + override val valueFlow: Flow<T> get() = origin.valueFlow.debounce(interval) override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" } -fun <T> DeviceState<T>.sample(interval: Duration) = SampleDeviceState(this, interval) +fun <T> DeviceState<T>.debounce(interval: Duration) = DebounceDeviceState(this, interval) @OptIn(FlowPreview::class) -class MutableSampleDeviceState<T>( +class MutableDebounceDeviceState<T>( val origin: MutableDeviceState<T>, val interval: Duration, ) : MutableDeviceState<T> { @@ -32,4 +33,4 @@ class MutableSampleDeviceState<T>( override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" } -fun <T> MutableDeviceState<T>.sample(interval: Duration) = MutableSampleDeviceState(this, interval) \ No newline at end of file +fun <T> MutableDeviceState<T>.debounce(interval: Duration) = MutableDebounceDeviceState(this, interval) \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index 510ab3e..320f477 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -4,16 +4,20 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import space.kscience.controls.api.DeviceMessage import space.kscience.controls.client.launchMagixService import space.kscience.controls.constructor.ModelConstructor import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.onTimer import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install +import space.kscience.controls.manager.respondMessage import space.kscience.controls.peer.PeerConnection import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.request import space.kscience.dataforge.io.Envelope +import space.kscience.dataforge.io.toByteArray import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.parseAsName import space.kscience.magix.api.MagixEndpoint @@ -40,13 +44,17 @@ internal fun VirtualDeviceState( MutableDeviceState(GmcVelocity.zero) ) +private val json = Json { + ignoreUnknownKeys = true + prettyPrint = true +} internal class DeviceCollectiveModel( context: Context, val deviceStates: Collection<CollectiveDeviceState>, val visibilityRange: Distance = 0.5.kilometers, val radioRange: Distance = 5.kilometers, -) : ModelConstructor(context), PeerConnection { +) : ModelConstructor(context) { /** * Propagate movement @@ -70,32 +78,39 @@ internal class DeviceCollectiveModel( return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange } } + inner class RadioPeerConnection(private val peerState: CollectiveDeviceState) : PeerConnection { + override suspend fun receive(address: String, contentId: String, requestMeta: Meta): Envelope? = null + + override suspend fun send(address: String, envelope: Envelope, requestMeta: Meta) { + devices.filter { it.value.configuration.radioFrequency == address }.filter { + GeoEllipsoid.WGS84.curveBetween(peerState.position.value, it.value.position.value).distance < radioRange + }.forEach { (id, target) -> + check(envelope.data != null) { "Envelope data is empty" } + val message = json.decodeFromString( + DeviceMessage.serializer(), + envelope.data?.toByteArray()?.decodeToString() ?: "" + ) + target.respondMessage(id.parseAsName(), message) + } + } + } + val devices = deviceStates.associate { val device = CollectiveDeviceConstructor( context = context, configuration = it.configuration, position = it.position, velocity = it.velocity, - peerConnection = this, + peerConnection = RadioPeerConnection(it), ) { locateVisible(it.id) } - //start movement program - device.moveInCircles() it.id to device } val roster = deviceStates.associate { it.id to it.configuration } - override suspend fun receive(address: String, contentId: String, requestMeta: Meta): Envelope { - TODO("Not yet implemented") - } - override suspend fun send(address: String, envelope: Envelope, requestMeta: Meta) { -// devices.values.filter { it.configuration.radioFrequency == address }.forEach { device -> -// ``` -// } - } } internal fun CoroutineScope.launchCollectiveMagixServer( diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt index cdca5cd..c9e59b6 100644 --- a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -1,10 +1,8 @@ package space.kscience.controls.demo.collective -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import space.kscience.controls.spec.write +import kotlinx.coroutines.* +import space.kscience.controls.client.DeviceClient +import space.kscience.controls.client.write import space.kscience.dataforge.context.Context import space.kscience.kmath.geometry.degrees import space.kscience.kmath.geometry.radians @@ -49,7 +47,7 @@ internal fun generateModel( return model } -fun CollectiveDevice.moveInCircles(): Job = launch { +fun DeviceClient.moveInCircles(scope: CoroutineScope = this): Job = scope.launch { var bearing = Random.nextDouble(-PI, PI).radians write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) while (isActive) { diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index b05f038..0d08cc8 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.Checkbox import androidx.compose.material.Text @@ -18,19 +19,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.sample import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState @@ -51,6 +49,7 @@ import space.kscience.maps.coordinates.Gmc import space.kscience.maps.coordinates.meters import space.kscience.maps.features.* import java.nio.file.Path +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -92,7 +91,8 @@ fun App() { collectiveModel.roster.forEach { (id, config) -> scope.launch { - devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) + val deviceClient = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) + devices[id] = deviceClient } } } @@ -111,6 +111,8 @@ fun App() { var showOnlyVisible by remember { mutableStateOf(false) } + var movementProgram: Job? by remember { mutableStateOf(null) } + HorizontalSplitPane( splitPaneState = rememberSplitPaneState(0.9f) ) { @@ -126,9 +128,28 @@ fun App() { ) { collectiveModel.deviceStates.forEach { device -> circle(device.position.value, id = device.id + ".position").color(Color.Red) - device.position.valueFlow.onEach { - circle(device.position.value, id = device.id + ".position", size = 3.dp) - .color(Color.Red) + device.position.valueFlow.sample(50.milliseconds).onEach { + val activeDevice = selectedDeviceId?.let { devices[it] } + val color = if (selectedDeviceId == device.id) { + Color.Magenta + } else if ( + showOnlyVisible && + activeDevice != null && + device.id in activeDevice.request(CollectiveDevice.visibleNeighbors) + ) { + Color.Cyan + } else { + Color.Red + } + + circle( + device.position.value, + id = device.id + ".position", + size = if (selectedDeviceId == device.id) 6.dp else 3.dp + ) + .color(color) + .modifyAttribute(ZAttribute, 10f) + .modifyAttribute(AlphaAttribute, if (selectedDeviceId == device.id) 1f else 0.5f) .modifyAttribute(AlphaAttribute, 0.5f) // does not work right now }.launchIn(scope) } @@ -139,24 +160,11 @@ fun App() { if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { val id = magixMessage.sourceEndpoint val position = gmcMetaConverter.read(deviceMessage.value) - val activeDevice = selectedDeviceId?.let { devices[it] } - if ( - activeDevice == null || - id == selectedDeviceId || - !showOnlyVisible || - id in activeDevice.request(CollectiveDevice.visibleNeighbors) - ) { - rectangle( - position, - id = id, - size = if (selectedDeviceId == id) DpSize(10.dp, 10.dp) else DpSize(5.dp, 5.dp) - ).color(if (selectedDeviceId == id) Color.Magenta else Color.Blue) - .modifyAttribute(AlphaAttribute, if (selectedDeviceId == id) 1f else 0.5f) - .onClick { selectedDeviceId = id } - } else { - removeFeature(id) - } + rectangle( + position, + id = id, + ).color(Color.Blue).onClick { selectedDeviceId = id } } }.launchIn(scope) @@ -168,6 +176,34 @@ fun App() { Column( modifier = Modifier.verticalScroll(rememberScrollState()) ) { + Button( + onClick = { + if (movementProgram == null) { + //start movement program + movementProgram = parentContext.launch { + devices.values.forEach { device -> + device.moveInCircles(this) + } + } + } else { + movementProgram?.cancel() + parentContext.launch { + devices.values.forEach { device -> + device.write(CollectiveDevice.velocity, GmcVelocity.zero) + } + } + movementProgram = null + } + }, + modifier = Modifier.fillMaxWidth() + ) { + if (movementProgram == null) { + Text("Move") + } else { + Text("Stop") + } + } + collectiveModel.roster.forEach { (id, _) -> Card( elevation = 16.dp, From eb126a60905ced9e30745f4a4e2b3d4f177ee548 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Wed, 12 Jun 2024 16:31:14 +0300 Subject: [PATCH 108/125] Finalize collective demo --- .../kscience/controls/api/DeviceMessage.kt | 2 +- .../controls/manager/respondMessage.kt | 2 +- .../src/jvmMain/kotlin/CollectiveDevice.kt | 8 +- .../jvmMain/kotlin/DeviceCollectiveModel.kt | 169 +++++++++++++++--- .../src/jvmMain/kotlin/debugModel.kt | 58 ------ .../src/jvmMain/kotlin/main.kt | 44 ++++- 6 files changed, 188 insertions(+), 95 deletions(-) delete mode 100644 demo/device-collective/src/jvmMain/kotlin/debugModel.kt diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt index f91beb5..1aeabf6 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 @@ -73,7 +73,7 @@ public data class PropertySetMessage( public val property: String, public val value: Meta, override val sourceDevice: Name? = null, - override val targetDevice: Name, + override val targetDevice: Name?, 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 a15bcef..3988c3c 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,7 +68,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess /** * Process incoming [DeviceMessage], using hub naming to find target. - * If the `targetDevice` is `null`, then message is sent to each device in this hub + * If the `targetDevice` is `null`, then the message is sent to each device in this hub */ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): List<DeviceMessage> { return try { diff --git a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 82170b9..c550830 100644 --- a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -23,7 +23,11 @@ class CollectiveDeviceConfiguration(deviceId: CollectiveDeviceId) : Scheme() { var deviceId by string(deviceId) var description by string() var reportInterval by int(500) - var radioFrequency by string(default = "169 MHz") + var radioFrequency by string(default = DEFAULT_FREQUENCY) + + companion object { + const val DEFAULT_FREQUENCY = "169 MHz" + } } typealias CollectiveDeviceRoster = Map<CollectiveDeviceId, CollectiveDeviceConfiguration> @@ -111,4 +115,4 @@ class CollectiveDeviceConstructor( } override suspend fun listVisible(): Collection<CollectiveDeviceId> = observation.invoke().keys -} \ No newline at end of file +} diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index 320f477..b5e67e4 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -1,12 +1,14 @@ package space.kscience.controls.demo.collective -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.io.writeString import kotlinx.serialization.json.Json import space.kscience.controls.api.DeviceMessage +import space.kscience.controls.api.PropertySetMessage +import space.kscience.controls.client.DeviceClient import space.kscience.controls.client.launchMagixService +import space.kscience.controls.client.write +import space.kscience.controls.constructor.DeviceState import space.kscience.controls.constructor.ModelConstructor import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.onTimer @@ -14,18 +16,38 @@ import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install import space.kscience.controls.manager.respondMessage import space.kscience.controls.peer.PeerConnection +import space.kscience.controls.spec.name +import space.kscience.controls.spec.write import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.request import space.kscience.dataforge.io.Envelope import space.kscience.dataforge.io.toByteArray import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.parseAsName +import space.kscience.kmath.geometry.degrees +import space.kscience.kmath.geometry.radians import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.rsocket.rSocketWithWebSockets import space.kscience.magix.server.startMagixServer import space.kscience.maps.coordinates.* +import kotlin.math.PI +import kotlin.random.Random +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +private val deviceVelocity = 0.1.kilometers + +private val center = Gmc.ofDegrees(55.925, 37.514) +private val radius = 0.01.degrees + +private val json = Json { + ignoreUnknownKeys = true + prettyPrint = true +} + internal data class CollectiveDeviceState( val id: CollectiveDeviceId, val configuration: CollectiveDeviceConfiguration, @@ -33,7 +55,7 @@ internal data class CollectiveDeviceState( val velocity: MutableDeviceState<GmcVelocity>, ) -internal fun VirtualDeviceState( +internal fun CollectiveDeviceState( id: CollectiveDeviceId, position: Gmc, configuration: CollectiveDeviceConfiguration.() -> Unit = {}, @@ -44,16 +66,11 @@ internal fun VirtualDeviceState( MutableDeviceState(GmcVelocity.zero) ) -private val json = Json { - ignoreUnknownKeys = true - prettyPrint = true -} - internal class DeviceCollectiveModel( context: Context, val deviceStates: Collection<CollectiveDeviceState>, val visibilityRange: Distance = 0.5.kilometers, - val radioRange: Distance = 5.kilometers, + val radioRange: Distance = 1.kilometers, ) : ModelConstructor(context) { /** @@ -78,41 +95,89 @@ internal class DeviceCollectiveModel( return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange } } - inner class RadioPeerConnection(private val peerState: CollectiveDeviceState) : PeerConnection { + inner class RadioPeerConnectionModel(private val position: DeviceState<Gmc>) : PeerConnection { override suspend fun receive(address: String, contentId: String, requestMeta: Meta): Envelope? = null override suspend fun send(address: String, envelope: Envelope, requestMeta: Meta) { - devices.filter { it.value.configuration.radioFrequency == address }.filter { - GeoEllipsoid.WGS84.curveBetween(peerState.position.value, it.value.position.value).distance < radioRange - }.forEach { (id, target) -> + devices.values.filter { it.configuration.radioFrequency == address }.filter { + GeoEllipsoid.WGS84.curveBetween(position.value, it.position.value).distance < radioRange + }.forEach { target -> check(envelope.data != null) { "Envelope data is empty" } val message = json.decodeFromString( DeviceMessage.serializer(), envelope.data?.toByteArray()?.decodeToString() ?: "" ) - target.respondMessage(id.parseAsName(), message) + target.respondMessage(target.configuration.deviceId.parseAsName(), message) } } } - val devices = deviceStates.associate { + val devices = deviceStates.associate { state -> val device = CollectiveDeviceConstructor( context = context, - configuration = it.configuration, - position = it.position, - velocity = it.velocity, - peerConnection = RadioPeerConnection(it), + configuration = state.configuration, + position = state.position, + velocity = state.velocity, + peerConnection = RadioPeerConnectionModel(state.position), ) { - locateVisible(it.id) + locateVisible(state.id) } - it.id to device + state.id to device + } + + internal fun createTrawler(position: Gmc, id: CollectiveDeviceId = "trawler"): CollectiveDeviceConstructor { + val state = CollectiveDeviceState( + id = id, + configuration = CollectiveDeviceConfiguration(id), + position = MutableDeviceState(position), + velocity = MutableDeviceState(GmcVelocity.zero) + ) + + val result = CollectiveDeviceConstructor( + context = context, + configuration = state.configuration, + position = state.position, + velocity = state.velocity, + peerConnection = RadioPeerConnectionModel(state.position), + ) { + locateVisible(state.id) + } + + // TODO move to CollectiveDeviceState + onTimer { prev, next -> + val delta = (next - prev) + state.position.value = state.position.value.moveWith(state.velocity.value, delta) + } + + result.onTimer(1.seconds) { _, _ -> + val envelope = Envelope { + data { + writeString( + json.encodeToString( + DeviceMessage.serializer(), + PropertySetMessage( + property = CollectiveDevice.velocity.name, + value = gmcVelocityMetaConverter.convert(state.velocity.value), + targetDevice = null + ) + ) + ) + } + } + + result.peerConnection.send( + CollectiveDeviceConfiguration.DEFAULT_FREQUENCY, + envelope + ) + } + + return result } val roster = deviceStates.associate { it.id to it.configuration } - - } + internal fun CoroutineScope.launchCollectiveMagixServer( collectiveModel: DeviceCollectiveModel, ): Job = launch(Dispatchers.IO) { @@ -133,4 +198,58 @@ internal fun CoroutineScope.launchCollectiveMagixServer( deviceContext.request(DeviceManager).launchMagixService(deviceEndpoint, id) } +} + + +internal fun generateModel( + context: Context, + size: Int = 50, + reportInterval: Duration = 500.milliseconds, + additionalConfiguration: CollectiveDeviceConfiguration.() -> Unit = {}, +): DeviceCollectiveModel { + val devices: List<CollectiveDeviceState> = List(size) { index -> + val id = "device[$index]" + + CollectiveDeviceState( + id = id, + Gmc( + center.latitude + radius * Random.nextDouble(), + center.longitude + radius * Random.nextDouble() + ) + ) { + deviceId = id + description = "Virtual remote device $id" + this.reportInterval = reportInterval.inWholeMilliseconds.toInt() + additionalConfiguration() + } + } + + val model = DeviceCollectiveModel(context, devices) + + return model +} + +fun DeviceClient.moveInCircles(scope: CoroutineScope = this): Job = scope.launch { + var bearing = Random.nextDouble(-PI, PI).radians + write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + while (isActive) { + delay(500) + bearing += 5.degrees + write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + } +} + + +internal fun CollectiveDeviceConstructor.moveTo( + targetPosition: Gmc, + speedLimit: Distance = deviceVelocity, + scope: CoroutineScope = this, +): Job = scope.launch { + do { + val curve = GeoEllipsoid.WGS84.curveBetween(position.value, targetPosition) + write(CollectiveDevice.velocity, GmcVelocity(curve.forward.bearing, speedLimit)) + delay(1.seconds) + } while (curve.distance > 0.1.kilometers) + write(CollectiveDevice.velocity, GmcVelocity.zero) + } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt deleted file mode 100644 index c9e59b6..0000000 --- a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt +++ /dev/null @@ -1,58 +0,0 @@ -package space.kscience.controls.demo.collective - -import kotlinx.coroutines.* -import space.kscience.controls.client.DeviceClient -import space.kscience.controls.client.write -import space.kscience.dataforge.context.Context -import space.kscience.kmath.geometry.degrees -import space.kscience.kmath.geometry.radians -import space.kscience.maps.coordinates.Gmc -import space.kscience.maps.coordinates.kilometers -import kotlin.math.PI -import kotlin.random.Random -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds - -private val deviceVelocity = 0.1.kilometers - -private val center = Gmc.ofDegrees(55.925, 37.514) -private val radius = 0.01.degrees - - -internal fun generateModel( - context: Context, - size: Int = 50, - reportInterval: Duration = 500.milliseconds, - additionalConfiguration: CollectiveDeviceConfiguration.() -> Unit = {}, -): DeviceCollectiveModel { - val devices: List<CollectiveDeviceState> = List(size) { index -> - val id = "device[$index]" - - VirtualDeviceState( - id = id, - Gmc( - center.latitude + radius * Random.nextDouble(), - center.longitude + radius * Random.nextDouble() - ) - ) { - deviceId = id - description = "Virtual remote device $id" - this.reportInterval = reportInterval.inWholeMilliseconds.toInt() - additionalConfiguration() - } - } - - val model = DeviceCollectiveModel(context, devices) - - return model -} - -fun DeviceClient.moveInCircles(scope: CoroutineScope = this): Job = scope.launch { - var bearing = Random.nextDouble(-PI, PI).radians - write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) - while (isActive) { - delay(500) - bearing += 5.degrees - write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) - } -} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 0d08cc8..f92b0eb 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -7,16 +7,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.Button -import androidx.compose.material.Card -import androidx.compose.material.Checkbox -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.isSecondaryPressed import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -58,7 +56,8 @@ fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {} Context(name, contextBuilder) } -private val gmcMetaConverter = MetaConverter.serializable<Gmc>() +internal val gmcMetaConverter = MetaConverter.serializable<Gmc>() +internal val gmcVelocityMetaConverter = MetaConverter.serializable<GmcVelocity>() @Composable fun App() { @@ -80,7 +79,6 @@ fun App() { val devices = remember { mutableStateMapOf<CollectiveDeviceId, DeviceClient>() } - LaunchedEffect(collectiveModel) { launchCollectiveMagixServer(collectiveModel) @@ -113,10 +111,27 @@ fun App() { var movementProgram: Job? by remember { mutableStateOf(null) } + val trawler: CollectiveDeviceConstructor = remember { + collectiveModel.createTrawler(Gmc.ofDegrees(55.925, 37.50)) + } + HorizontalSplitPane( splitPaneState = rememberSplitPaneState(0.9f) ) { first(400.dp) { + var clickPoint by remember { mutableStateOf<Gmc?>(null) } + + CursorDropdownMenu(clickPoint != null, { clickPoint = null }) { + clickPoint?.let { point -> + TextButton({ + trawler.moveTo(point) + clickPoint = null + }) { + Text("Move trawler here") + } + } + } + MapView( mapTileProvider = remember { OpenStreetMapTileProvider( @@ -124,8 +139,15 @@ fun App() { cacheDirectory = Path.of("mapCache") ) }, - config = ViewConfig() + config = ViewConfig( + onClick = { event, point -> + if (event.buttons.isSecondaryPressed) { + clickPoint = point.focus + } + } + ) ) { + //draw real positions collectiveModel.deviceStates.forEach { device -> circle(device.position.value, id = device.id + ".position").color(Color.Red) device.position.valueFlow.sample(50.milliseconds).onEach { @@ -154,8 +176,8 @@ fun App() { }.launchIn(scope) } + //draw received data scope.launch { - client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { val id = magixMessage.sourceEndpoint @@ -169,6 +191,12 @@ fun App() { }.launchIn(scope) } + + // draw trawler + + trawler.position.valueFlow.onEach { + circle(it, id = "trawler").color(Color.Black) + }.launchIn(scope) } } second(200.dp) { From 92c4355f483769af77ef0c6e938366428807e1df Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 17 Jun 2024 17:49:12 +0300 Subject: [PATCH 109/125] update device-collective readme --- README.md | 9 +++++++++ controls-constructor/README.md | 4 ++-- controls-core/README.md | 4 ++-- controls-jupyter/README.md | 4 ++-- controls-magix/README.md | 4 ++-- controls-modbus/README.md | 4 ++-- controls-opcua/README.md | 4 ++-- controls-pi/README.md | 4 ++-- controls-ports-ktor/README.md | 4 ++-- controls-serial/README.md | 4 ++-- controls-server/README.md | 4 ++-- controls-storage/README.md | 4 ++-- controls-storage/controls-xodus/README.md | 4 ++-- controls-vision/README.md | 6 ++---- demo/device-collective/README.md | 13 +++++++++++++ magix/magix-api/README.md | 4 ++-- magix/magix-java-endpoint/README.md | 4 ++-- magix/magix-mqtt/README.md | 4 ++-- magix/magix-rabbit/README.md | 4 ++-- magix/magix-rsocket/README.md | 4 ++-- magix/magix-server/README.md | 4 ++-- magix/magix-storage/README.md | 4 ++-- magix/magix-storage/magix-storage-xodus/README.md | 4 ++-- magix/magix-zmq/README.md | 4 ++-- 24 files changed, 66 insertions(+), 46 deletions(-) create mode 100644 demo/device-collective/README.md diff --git a/README.md b/README.md index d5baaf9..6b9c9cd 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,11 @@ Automatically checks consistency. > > **Maturity**: PROTOTYPE +### [controls-visualisation-compose](controls-visualisation-compose) +> Visualisation extension using compose-multiplatform +> +> **Maturity**: PROTOTYPE + ### [demo](demo) > > **Maturity**: EXPERIMENTAL @@ -159,6 +164,10 @@ Automatically checks consistency. > > **Maturity**: EXPERIMENTAL +### [demo/device-collective](demo/device-collective) +> +> **Maturity**: EXPERIMENTAL + ### [demo/echo](demo/echo) > > **Maturity**: EXPERIMENTAL diff --git a/controls-constructor/README.md b/controls-constructor/README.md index d388b01..7da4d0b 100644 --- a/controls-constructor/README.md +++ b/controls-constructor/README.md @@ -6,7 +6,7 @@ A low-code constructor for composite devices simulation ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-constructor:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-constructor:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-constructor:0.4.0-dev-1") + implementation("space.kscience:controls-constructor:0.4.0-dev-4") } ``` diff --git a/controls-core/README.md b/controls-core/README.md index 71caf53..26ed618 100644 --- a/controls-core/README.md +++ b/controls-core/README.md @@ -16,7 +16,7 @@ Core interfaces for building a device server ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -26,6 +26,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-core:0.4.0-dev-1") + implementation("space.kscience:controls-core:0.4.0-dev-4") } ``` diff --git a/controls-jupyter/README.md b/controls-jupyter/README.md index 7d0fc4f..6100ddd 100644 --- a/controls-jupyter/README.md +++ b/controls-jupyter/README.md @@ -6,7 +6,7 @@ ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-jupyter:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-jupyter:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-jupyter:0.4.0-dev-1") + implementation("space.kscience:controls-jupyter:0.4.0-dev-4") } ``` diff --git a/controls-magix/README.md b/controls-magix/README.md index 8c16ffd..c474221 100644 --- a/controls-magix/README.md +++ b/controls-magix/README.md @@ -12,7 +12,7 @@ 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.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-magix:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -22,6 +22,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-magix:0.4.0-dev-1") + implementation("space.kscience:controls-magix:0.4.0-dev-4") } ``` diff --git a/controls-modbus/README.md b/controls-modbus/README.md index 61e4b60..9705e81 100644 --- a/controls-modbus/README.md +++ b/controls-modbus/README.md @@ -14,7 +14,7 @@ Automatically checks consistency. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-modbus:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-modbus:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -24,6 +24,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-modbus:0.4.0-dev-1") + implementation("space.kscience:controls-modbus:0.4.0-dev-4") } ``` diff --git a/controls-opcua/README.md b/controls-opcua/README.md index 03dc32b..6867492 100644 --- a/controls-opcua/README.md +++ b/controls-opcua/README.md @@ -12,7 +12,7 @@ A client and server connectors for OPC-UA via Eclipse Milo ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-opcua:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-opcua:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -22,6 +22,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-opcua:0.4.0-dev-1") + implementation("space.kscience:controls-opcua:0.4.0-dev-4") } ``` diff --git a/controls-pi/README.md b/controls-pi/README.md index 2873ac2..0afb324 100644 --- a/controls-pi/README.md +++ b/controls-pi/README.md @@ -6,7 +6,7 @@ Utils to work with controls-kt on Raspberry pi ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-pi:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-pi:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-pi:0.4.0-dev-1") + implementation("space.kscience:controls-pi:0.4.0-dev-4") } ``` diff --git a/controls-ports-ktor/README.md b/controls-ports-ktor/README.md index 6b23d80..1cb277a 100644 --- a/controls-ports-ktor/README.md +++ b/controls-ports-ktor/README.md @@ -6,7 +6,7 @@ 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.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-ports-ktor:0.4.0-dev-1") + implementation("space.kscience:controls-ports-ktor:0.4.0-dev-4") } ``` diff --git a/controls-serial/README.md b/controls-serial/README.md index a961f55..ce95ee5 100644 --- a/controls-serial/README.md +++ b/controls-serial/README.md @@ -6,7 +6,7 @@ Implementation of direct serial port communication with JSerialComm ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-serial:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-serial:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-serial:0.4.0-dev-1") + implementation("space.kscience:controls-serial:0.4.0-dev-4") } ``` diff --git a/controls-server/README.md b/controls-server/README.md index 114f618..896020d 100644 --- a/controls-server/README.md +++ b/controls-server/README.md @@ -6,7 +6,7 @@ A combined Magix event loop server with web server for visualization. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-server:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-server:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-server:0.4.0-dev-1") + implementation("space.kscience:controls-server:0.4.0-dev-4") } ``` diff --git a/controls-storage/README.md b/controls-storage/README.md index 64751e8..dd4ab3d 100644 --- a/controls-storage/README.md +++ b/controls-storage/README.md @@ -6,7 +6,7 @@ An API for stand-alone Controls-kt device or a hub. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-storage:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-storage:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-storage:0.4.0-dev-1") + implementation("space.kscience:controls-storage:0.4.0-dev-4") } ``` diff --git a/controls-storage/controls-xodus/README.md b/controls-storage/controls-xodus/README.md index e423c3f..44cdf26 100644 --- a/controls-storage/controls-xodus/README.md +++ b/controls-storage/controls-xodus/README.md @@ -6,7 +6,7 @@ An implementation of controls-storage on top of JetBrains Xodus. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-xodus:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-xodus:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-xodus:0.4.0-dev-1") + implementation("space.kscience:controls-xodus:0.4.0-dev-4") } ``` diff --git a/controls-vision/README.md b/controls-vision/README.md index cbfc917..ff2b648 100644 --- a/controls-vision/README.md +++ b/controls-vision/README.md @@ -2,13 +2,11 @@ Dashboard and visualization extensions for devices -Hello world! - ## Usage ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-vision:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-vision:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -18,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-vision:0.4.0-dev-1") + implementation("space.kscience:controls-vision:0.4.0-dev-4") } ``` diff --git a/demo/device-collective/README.md b/demo/device-collective/README.md new file mode 100644 index 0000000..5f639a7 --- /dev/null +++ b/demo/device-collective/README.md @@ -0,0 +1,13 @@ +# Module device-collective + +# Running demo from gradle + +1. Install JDK 17-21 in the system (for example, from https://sdkman.io/jdks or https://github.com/ScoopInstaller/Java). +2. Clone the repository with Git. +3. Run `./gradlew :demo:device-collective:run` from the project root directory. + +# Install distribution + +1 and 2 from above. +3. Run `./gradlew :demo:device-collective:packageUberJarForCurrentOS` from the project root directory. +4. Go to `build/compose/jars/device-collective-<your OS>-<version>.jar`. You can copy it and run with `java -jar <full-name>.jar` diff --git a/magix/magix-api/README.md b/magix/magix-api/README.md index 6228a7e..6f16ac0 100644 --- a/magix/magix-api/README.md +++ b/magix/magix-api/README.md @@ -6,7 +6,7 @@ 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.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-api:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-api:0.4.0-dev-1") + implementation("space.kscience:magix-api:0.4.0-dev-4") } ``` diff --git a/magix/magix-java-endpoint/README.md b/magix/magix-java-endpoint/README.md index 2db1d42..87359f1 100644 --- a/magix/magix-java-endpoint/README.md +++ b/magix/magix-java-endpoint/README.md @@ -6,7 +6,7 @@ Java API to work with magix endpoints without Kotlin ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-java-endpoint:0.4.0-dev-1") + implementation("space.kscience:magix-java-endpoint:0.4.0-dev-4") } ``` diff --git a/magix/magix-mqtt/README.md b/magix/magix-mqtt/README.md index 7896472..0ee054a 100644 --- a/magix/magix-mqtt/README.md +++ b/magix/magix-mqtt/README.md @@ -6,7 +6,7 @@ MQTT client magix endpoint ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-mqtt:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-mqtt:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-mqtt:0.4.0-dev-1") + implementation("space.kscience:magix-mqtt:0.4.0-dev-4") } ``` diff --git a/magix/magix-rabbit/README.md b/magix/magix-rabbit/README.md index eee4a21..35b43c7 100644 --- a/magix/magix-rabbit/README.md +++ b/magix/magix-rabbit/README.md @@ -6,7 +6,7 @@ RabbitMQ client magix endpoint ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-rabbit:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-rabbit:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-rabbit:0.4.0-dev-1") + implementation("space.kscience:magix-rabbit:0.4.0-dev-4") } ``` diff --git a/magix/magix-rsocket/README.md b/magix/magix-rsocket/README.md index ef16b66..9f3a42c 100644 --- a/magix/magix-rsocket/README.md +++ b/magix/magix-rsocket/README.md @@ -6,7 +6,7 @@ Magix endpoint (client) based on RSocket ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-rsocket:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-rsocket:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-rsocket:0.4.0-dev-1") + implementation("space.kscience:magix-rsocket:0.4.0-dev-4") } ``` diff --git a/magix/magix-server/README.md b/magix/magix-server/README.md index 17bdbdf..8259e0f 100644 --- a/magix/magix-server/README.md +++ b/magix/magix-server/README.md @@ -6,7 +6,7 @@ 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.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-server:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-server:0.4.0-dev-1") + implementation("space.kscience:magix-server:0.4.0-dev-4") } ``` diff --git a/magix/magix-storage/README.md b/magix/magix-storage/README.md index dbb2729..672458e 100644 --- a/magix/magix-storage/README.md +++ b/magix/magix-storage/README.md @@ -6,7 +6,7 @@ Magix history database API ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-storage:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-storage:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-storage:0.4.0-dev-1") + implementation("space.kscience:magix-storage:0.4.0-dev-4") } ``` diff --git a/magix/magix-storage/magix-storage-xodus/README.md b/magix/magix-storage/magix-storage-xodus/README.md index 90a0050..2fb495a 100644 --- a/magix/magix-storage/magix-storage-xodus/README.md +++ b/magix/magix-storage/magix-storage-xodus/README.md @@ -6,7 +6,7 @@ ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-storage-xodus:0.4.0-dev-1") + implementation("space.kscience:magix-storage-xodus:0.4.0-dev-4") } ``` diff --git a/magix/magix-zmq/README.md b/magix/magix-zmq/README.md index 6e924db..952e02e 100644 --- a/magix/magix-zmq/README.md +++ b/magix/magix-zmq/README.md @@ -6,7 +6,7 @@ ZMQ client endpoint for Magix ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-zmq:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-zmq:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-zmq:0.4.0-dev-1") + implementation("space.kscience:magix-zmq:0.4.0-dev-4") } ``` From f13b7268d6f33a93842f64815f4444959ced2bad Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 17 Jun 2024 17:52:02 +0300 Subject: [PATCH 110/125] update device-collective readme --- demo/device-collective/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/device-collective/README.md b/demo/device-collective/README.md index 5f639a7..369ec04 100644 --- a/demo/device-collective/README.md +++ b/demo/device-collective/README.md @@ -8,6 +8,7 @@ # Install distribution -1 and 2 from above. +1. Install JDK 17-21 in the system (for example, from https://sdkman.io/jdks or https://github.com/ScoopInstaller/Java). +2. Clone the repository with Git. 3. Run `./gradlew :demo:device-collective:packageUberJarForCurrentOS` from the project root directory. 4. Go to `build/compose/jars/device-collective-<your OS>-<version>.jar`. You can copy it and run with `java -jar <full-name>.jar` From 8c7c017ab420c0625fec6e71da53e2458cd49f73 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Tue, 18 Jun 2024 19:25:55 +0300 Subject: [PATCH 111/125] Add linear drive calibration demo --- .../controls/constructor/devices/StepDrive.kt | 8 +-- .../jvmMain/kotlin/LinearDriveCalibration.kt | 66 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 demo/constructor/src/jvmMain/kotlin/LinearDriveCalibration.kt diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt index be32f07..ab33e39 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt @@ -22,6 +22,7 @@ import kotlin.time.DurationUnit public class StepDrive( context: Context, ticksPerSecond: MutableDeviceState<Double>, + position: MutableDeviceState<Long> = MutableDeviceState(0), target: MutableDeviceState<Long> = MutableDeviceState(0), private val writeTicks: suspend (ticks: Long, speed: Double) -> Unit = { _, _ -> }, ) : DeviceConstructor(context) { @@ -30,10 +31,7 @@ public class StepDrive( public val speed: MutableDeviceState<Double> by property(MetaConverter.double, ticksPerSecond) - private val positionState = stateOf(target.value) - - public val position: DeviceState<Long> by property(MetaConverter.long, positionState) - + public val position: DeviceState<Long> by property(MetaConverter.long, position) //FIXME round to zero problem private val ticker = onTimer(reads = setOf(target, position), writes = setOf(position)) { prev, next -> @@ -46,7 +44,7 @@ public class StepDrive( else -> return@onTimer } writeTicks(steps, tickSpeed) - positionState.value += steps + position.value += steps } } diff --git a/demo/constructor/src/jvmMain/kotlin/LinearDriveCalibration.kt b/demo/constructor/src/jvmMain/kotlin/LinearDriveCalibration.kt new file mode 100644 index 0000000..1a2d5e9 --- /dev/null +++ b/demo/constructor/src/jvmMain/kotlin/LinearDriveCalibration.kt @@ -0,0 +1,66 @@ +package space.kscience.controls.demo.constructor + +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.ensureActive +import space.kscience.controls.constructor.DeviceConstructor +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.device +import space.kscience.controls.constructor.devices.LimitSwitch +import space.kscience.controls.constructor.devices.StepDrive +import space.kscience.controls.constructor.models.MutableRangeState +import space.kscience.controls.manager.DeviceManager +import space.kscience.dataforge.context.Context + +private val ticksPerSecond = MutableDeviceState(3000.0) + +class LinearStepDrive( + context: Context, + drive: StepDrive, + atStart: LimitSwitch, + atEnd: LimitSwitch, +):DeviceConstructor(context){ + val drive by device(drive) + val atStart by device(atStart) + val atEnd by device(atEnd) +} + + +fun LinearStepDrive( + context: Context, + position: MutableRangeState<Long>, +): LinearStepDrive = LinearStepDrive( + context = context, + drive = StepDrive(context, ticksPerSecond, position), + atStart = LimitSwitch(context, position.atStart), + atEnd = LimitSwitch(context, position.atEnd) +) + +suspend fun LinearStepDrive.calibrate(step: Long = 10): ClosedRange<Long> = coroutineScope{ + do{ + ensureActive() + drive.target.value += step + } while (!atEnd.locked.value) + + val end = drive.position.value + + do{ + ensureActive() + drive.target.value -= step + } while (!atStart.locked.value) + + val start = drive.position.value + + return@coroutineScope start..end +} + +suspend fun main() = coroutineScope { + val context = Context{ + plugin(DeviceManager) + } + + val positionModel = MutableRangeState<Long>(0L, -1002L..1012L) + + val linearStepDrive = LinearStepDrive(context, positionModel) + + println(linearStepDrive.calibrate()) +} \ No newline at end of file From a9d58bfac2b1da7314d7d0468e87a2abb3db74f6 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Tue, 18 Jun 2024 20:18:18 +0300 Subject: [PATCH 112/125] Fix StepDrive parameters --- .../controls/constructor/devices/StepDrive.kt | 15 ++++--- .../jvmMain/kotlin/LinearDriveCalibration.kt | 41 ++++++++++++------- .../constructor/src/jvmMain/kotlin/Plotter.kt | 2 +- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt index ab33e39..9d552c5 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt @@ -21,21 +21,26 @@ import kotlin.time.DurationUnit */ public class StepDrive( context: Context, - ticksPerSecond: MutableDeviceState<Double>, + ticksPerSecond: Double, position: MutableDeviceState<Long> = MutableDeviceState(0), - target: MutableDeviceState<Long> = MutableDeviceState(0), private val writeTicks: suspend (ticks: Long, speed: Double) -> Unit = { _, _ -> }, ) : DeviceConstructor(context) { - public val target: MutableDeviceState<Long> by property(MetaConverter.long, target) + public val target: MutableDeviceState<Long> by property( + MetaConverter.long, + MutableDeviceState<Long>(position.value) + ) - public val speed: MutableDeviceState<Double> by property(MetaConverter.double, ticksPerSecond) + public val speed: MutableDeviceState<Double> by property( + MetaConverter.double, + MutableDeviceState<Double>(ticksPerSecond) + ) public val position: DeviceState<Long> by property(MetaConverter.long, position) //FIXME round to zero problem private val ticker = onTimer(reads = setOf(target, position), writes = setOf(position)) { prev, next -> - val tickSpeed = ticksPerSecond.value + val tickSpeed = speed.value val timeDelta = (next - prev).toDouble(DurationUnit.SECONDS) val ticksDelta: Long = target.value - position.value val steps: Long = when { diff --git a/demo/constructor/src/jvmMain/kotlin/LinearDriveCalibration.kt b/demo/constructor/src/jvmMain/kotlin/LinearDriveCalibration.kt index 1a2d5e9..02192b9 100644 --- a/demo/constructor/src/jvmMain/kotlin/LinearDriveCalibration.kt +++ b/demo/constructor/src/jvmMain/kotlin/LinearDriveCalibration.kt @@ -1,24 +1,26 @@ package space.kscience.controls.demo.constructor import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import space.kscience.controls.constructor.DeviceConstructor -import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.collectValuesIn import space.kscience.controls.constructor.device import space.kscience.controls.constructor.devices.LimitSwitch import space.kscience.controls.constructor.devices.StepDrive import space.kscience.controls.constructor.models.MutableRangeState import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.context.Context +import kotlin.time.Duration.Companion.seconds -private val ticksPerSecond = MutableDeviceState(3000.0) +private val ticksPerSecond = 3000.0 class LinearStepDrive( context: Context, drive: StepDrive, atStart: LimitSwitch, atEnd: LimitSwitch, -):DeviceConstructor(context){ +) : DeviceConstructor(context) { val drive by device(drive) val atStart by device(atStart) val atEnd by device(atEnd) @@ -28,39 +30,48 @@ class LinearStepDrive( fun LinearStepDrive( context: Context, position: MutableRangeState<Long>, -): LinearStepDrive = LinearStepDrive( +): LinearStepDrive = LinearStepDrive( context = context, drive = StepDrive(context, ticksPerSecond, position), atStart = LimitSwitch(context, position.atStart), atEnd = LimitSwitch(context, position.atEnd) ) -suspend fun LinearStepDrive.calibrate(step: Long = 10): ClosedRange<Long> = coroutineScope{ - do{ - ensureActive() - drive.target.value += step - } while (!atEnd.locked.value) - - val end = drive.position.value - - do{ +suspend fun LinearStepDrive.calibrate(step: Long = 10): ClosedRange<Long> = coroutineScope { + do { ensureActive() drive.target.value -= step + delay((step / ticksPerSecond).seconds) } while (!atStart.locked.value) val start = drive.position.value + + do { + ensureActive() + drive.target.value += step + delay((step / ticksPerSecond).seconds) + } while (!atEnd.locked.value) + + val end = drive.position.value + return@coroutineScope start..end } suspend fun main() = coroutineScope { - val context = Context{ + val context = Context { plugin(DeviceManager) } - val positionModel = MutableRangeState<Long>(0L, -1002L..1012L) + val positionModel = MutableRangeState<Long>(0L, -1000L..1012L) val linearStepDrive = LinearStepDrive(context, positionModel) + val printJob = linearStepDrive.drive.target.collectValuesIn(this){ + println("Move to $it") + } + println(linearStepDrive.calibrate()) + + printJob.cancel() } \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/Plotter.kt b/demo/constructor/src/jvmMain/kotlin/Plotter.kt index dfded59..3e139e3 100644 --- a/demo/constructor/src/jvmMain/kotlin/Plotter.kt +++ b/demo/constructor/src/jvmMain/kotlin/Plotter.kt @@ -83,7 +83,7 @@ private suspend fun Plotter.square(xRange: IntRange, yRange: IntRange) { private val xRange = NumericalValue<Meters>(-0.5)..NumericalValue<Meters>(0.5) private val yRange = NumericalValue<Meters>(-0.5)..NumericalValue<Meters>(0.5) -private val ticksPerSecond = MutableDeviceState(3000.0) +private const val ticksPerSecond = 3000.0 private val step = NumericalValue<Degrees>(1.8) From fbf79f0a3739f23b53e245921aaafebd9171f4cc Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Mon, 22 Jul 2024 18:46:58 +0300 Subject: [PATCH 113/125] Implement new visualization --- CHANGELOG.md | 1 + build.gradle.kts | 2 +- .../controls/constructor/internalState.kt | 4 +- controls-visualisation-compose/README.md | 21 +++ .../build.gradle.kts | 24 +--- .../src/commonMain/kotlin/DeviceDrawable2D.kt | 64 +++++++++ .../kotlin/DeviceDrawable2DStore.kt | 91 +++++++++++++ demo/car/build.gradle.kts | 4 +- .../constructor/src/jvmMain/kotlin/Plotter.kt | 126 ++++++++++++------ demo/echo/build.gradle.kts | 4 +- demo/many-devices/build.gradle.kts | 4 +- gradle/libs.versions.toml | 4 +- magix/magix-java-endpoint/build.gradle.kts | 17 +-- 13 files changed, 280 insertions(+), 86 deletions(-) create mode 100644 controls-visualisation-compose/README.md create mode 100644 controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2D.kt create mode 100644 controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2DStore.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index ae8c47f..29b1a5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Shortcuts to access all Controls devices in a magix network. - `DeviceClient` properly evaluates lifecycle and logs - `PeerConnection` API for direct device-device binary sharing +- DeviceDrawable2D intermediate visualization implementation ### Changed - Constructor properties return `DeviceState` in order to be able to subscribe to them diff --git a/build.gradle.kts b/build.gradle.kts index 773272c..7f7e534 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { allprojects { group = "space.kscience" - version = "0.4.0-dev-4" + version = "0.4.0-dev-5" repositories{ google() } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt index 687804e..684f666 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.emptyFlow */ private class VirtualDeviceState<T>( initialValue: T, - private val callback: (T) -> Unit = {}, + private val callback: (T) -> Unit = {} ) : MutableDeviceState<T> { private val flow = MutableStateFlow(initialValue) override val valueFlow: Flow<T> get() = flow @@ -34,7 +34,7 @@ private class VirtualDeviceState<T>( */ public fun <T> MutableDeviceState( initialValue: T, - callback: (T) -> Unit = {}, + callback: (T) -> Unit = {} ): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback) diff --git a/controls-visualisation-compose/README.md b/controls-visualisation-compose/README.md new file mode 100644 index 0000000..0f77d54 --- /dev/null +++ b/controls-visualisation-compose/README.md @@ -0,0 +1,21 @@ +# Module controls-visualisation-compose + +Visualisation extension using compose-multiplatform + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:controls-visualisation-compose:0.4.0-dev-4`. + +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:controls-visualisation-compose:0.4.0-dev-4") +} +``` diff --git a/controls-visualisation-compose/build.gradle.kts b/controls-visualisation-compose/build.gradle.kts index d9d8215..0c68988 100644 --- a/controls-visualisation-compose/build.gradle.kts +++ b/controls-visualisation-compose/build.gradle.kts @@ -18,25 +18,11 @@ kscience { useContextReceivers() commonMain { api(projects.controlsConstructor) - api("io.github.koalaplot:koalaplot-core:0.6.0") - } -} - -kotlin { - sourceSets { - commonMain { - dependencies { - api(compose.foundation) - api(compose.material3) - @OptIn(ExperimentalComposeLibrary::class) - api(compose.desktop.components.splitPane) - } - } -// jvmMain { -// dependencies { -// implementation(compose.desktop.currentOs) -// } -// } + api(libs.koala.plots) + api(compose.foundation) + api(compose.material3) + @OptIn(ExperimentalComposeLibrary::class) + api(compose.desktop.components.splitPane) } } diff --git a/controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2D.kt b/controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2D.kt new file mode 100644 index 0000000..464514b --- /dev/null +++ b/controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2D.kt @@ -0,0 +1,64 @@ +package space.kscience.controls.compose + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.rotate + +/** + * A single 2D drawable + */ +@Immutable +public sealed interface DeviceDrawable2D { + + public fun DrawScope.draw() + + override fun equals(other: Any?): Boolean +} + +@Immutable +public data class CircleDrawable2D(val position: Offset, val radius: Float, val color: Color) : DeviceDrawable2D { + override fun DrawScope.draw() { + drawCircle(color, radius = radius, center = position) + } +} + +@Drawable2DBuilder +public fun DeviceDrawable2DStore.circle(id: String, position: Offset, radius: Float, color: Color) { + emit(id, CircleDrawable2D(position, radius, color)) +} + +@Immutable +public data class RectangleDrawable2D( + val position: Offset, + val rectangleSize: Size, + val color: Color, + val rotateDegrees: Float = 0f, +) : DeviceDrawable2D { + override fun DrawScope.draw() { + rotate(rotateDegrees) { + drawRect( + color = color, + topLeft = Offset( + (position.x - rectangleSize.width / 2), + (position.y - rectangleSize.height / 2) + ), + size = Size(rectangleSize.width, rectangleSize.height) + ) + } + } +} + +@Drawable2DBuilder +public fun DeviceDrawable2DStore.rectangle( + id: String, + position: Offset, + rectangleSize: Size, + color: Color, + rotateDegrees: Float = 0f, +) { + emit(id, RectangleDrawable2D(position, rectangleSize, color, rotateDegrees)) +} + diff --git a/controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2DStore.kt b/controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2DStore.kt new file mode 100644 index 0000000..1c17aad --- /dev/null +++ b/controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2DStore.kt @@ -0,0 +1,91 @@ +package space.kscience.controls.compose + +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.unit.toSize +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import space.kscience.controls.api.Device +import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.propertyFlow + +@DslMarker +public annotation class Drawable2DBuilder + +@Drawable2DBuilder +public class DeviceDrawable2DStore(public val scope: CoroutineScope, public val size: Size) { + public val drawableFlow: MutableStateFlow<Map<String, DeviceDrawable2D>> = MutableStateFlow(emptyMap()) +} + +public fun DeviceDrawable2DStore.emit(id: String, drawable2D: DeviceDrawable2D){ + drawableFlow.value += (id to drawable2D) +} + +public fun DeviceDrawable2DStore.emitAll(drawables: Map<String, DeviceDrawable2D>){ + drawableFlow.value += drawables +} + + +/** + * Fill drawables from a flow + */ +public fun DeviceDrawable2DStore.observe(id: String, flow: Flow<DeviceDrawable2D>): Job = flow.onEach { + drawableFlow.value += (id to it) +}.launchIn(scope) + +/** + * Observe single [DeviceState] + */ +public fun <T> DeviceDrawable2DStore.observeState( + state: DeviceState<T>, + id: String = state.toString(), + transform: suspend DeviceDrawable2DStore.(T) -> DeviceDrawable2D, +): Job = observe(id, state.valueFlow.map { transform(this, it) }) + +/** + * Observe a single [Device] property + */ +public fun <T, D : Device, P : DevicePropertySpec<D, T>> DeviceDrawable2DStore.observeProperty( + device: D, + devicePropertySpec: DevicePropertySpec<D, T>, + id: String = devicePropertySpec.toString(), + transform: suspend DeviceDrawable2DStore.(T) -> DeviceDrawable2D, +): Job = observe(id, device.propertyFlow(devicePropertySpec).map { transform(this, it) }) + +@Composable +public fun Device2DCanvas( + modifier: Modifier = Modifier, + flowBuilder: suspend DeviceDrawable2DStore.() -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + var canvasSize by remember { mutableStateOf(Size(100f, 100f)) } + + val store = remember(canvasSize) { + DeviceDrawable2DStore(coroutineScope, canvasSize).apply { + coroutineScope.launch { + flowBuilder() + } + } + } + + val drawables by store.drawableFlow.collectAsState() + + key(store) { + Canvas(modifier.onGloballyPositioned { + canvasSize = it.size.toSize() + }) { + clipRect { + drawables.values.forEach { + with(it) { draw() } + } + } + } + } +} \ No newline at end of file diff --git a/demo/car/build.gradle.kts b/demo/car/build.gradle.kts index b8a7fb6..967b87d 100644 --- a/demo/car/build.gradle.kts +++ b/demo/car/build.gradle.kts @@ -37,8 +37,8 @@ kotlin{ } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { - kotlinOptions { - freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") + compilerOptions { + freeCompilerArgs.addAll("-Xjvm-default=all") } } diff --git a/demo/constructor/src/jvmMain/kotlin/Plotter.kt b/demo/constructor/src/jvmMain/kotlin/Plotter.kt index 474f5c2..16006d6 100644 --- a/demo/constructor/src/jvmMain/kotlin/Plotter.kt +++ b/demo/constructor/src/jvmMain/kotlin/Plotter.kt @@ -1,19 +1,29 @@ package space.kscience.controls.demo.constructor -import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.MaterialTheme +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi +import org.jetbrains.compose.splitpane.HorizontalSplitPane +import space.kscience.controls.compose.* import space.kscience.controls.constructor.* import space.kscience.controls.constructor.devices.LimitSwitch import space.kscience.controls.constructor.devices.StepDrive @@ -121,24 +131,30 @@ private class PlotterModel( context = context, xDrive = xDrive, yDrive = yDrive, - xStartLimit = LimitSwitch(context,x.atStart), - xEndLimit = LimitSwitch(context,x.atEnd), - yStartLimit = LimitSwitch(context,x.atStart), - yEndLimit = LimitSwitch(context,x.atEnd), + xStartLimit = LimitSwitch(context, x.atStart), + xEndLimit = LimitSwitch(context, x.atEnd), + yStartLimit = LimitSwitch(context, x.atStart), + yEndLimit = LimitSwitch(context, x.atEnd), ) { color -> println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color") callback(PlotterPoint(x.value, y.value, color)) } } +private val range = -1000..1000 + +@OptIn(ExperimentalSplitPaneApi::class) suspend fun main() = application { Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) { window.minimumSize = Dimension(400, 400) - val points = remember { mutableStateListOf<PlotterPoint>() } - var position by remember { mutableStateOf(XY<Meters>(0, 0)) } + val scope = rememberCoroutineScope() - LaunchedEffect(Unit) { + var updateJob: Job? = remember { null } + + var points by remember { mutableStateOf<List<PlotterPoint>>(emptyList()) } + + val plotterModel = remember { val context = Context { plugin(DeviceManager) plugin(ClockManager) @@ -146,53 +162,81 @@ suspend fun main() = application { /* Here goes the device definition block */ - val plotterModel = PlotterModel(context) { plotterPoint -> - points.add(plotterPoint) + PlotterModel(context) { plotterPoint -> + points += plotterPoint } - - /* Start visualization program */ - - plotterModel.xy.valueFlow.onEach { - position = it - }.launchIn(this) - - /* run program */ - - - val range = -1000..1000 -// plotterModel.plotter.modernArt(range, range) - plotterModel.plotter.square(range, range) - } - /* Here goes the visualization block */ MaterialTheme { - Canvas(modifier = Modifier.fillMaxSize()) { - fun toOffset(x: NumericalValue<Meters>, y: NumericalValue<Meters>): Offset { - val canvasX = (x - xRange.start) / (xRange.endInclusive - xRange.start) * size.width - val canvasY = (y - yRange.start) / (yRange.endInclusive - yRange.start) * size.height - return Offset(canvasX.toFloat(), canvasY.toFloat()) + HorizontalSplitPane { + first(200.dp) { + Column(modifier = Modifier.fillMaxHeight()) { + Button({ + updateJob?.cancel() + updateJob = scope.launch { + plotterModel.plotter.square(range, range) + } + }, modifier = Modifier.fillMaxWidth()) { + Text("Rectangle") + } + Button({ + updateJob?.cancel() + updateJob = scope.launch { + plotterModel.plotter.modernArt(range, range) + } + }, modifier = Modifier.fillMaxWidth()) { + Text("Modern Art") + } + Button({ + updateJob?.cancel() + }, modifier = Modifier.fillMaxWidth()) { + Text("Stop") + } + } + } + second { + Device2DCanvas(modifier = Modifier.fillMaxSize()) { + fun xToPx(x: NumericalValue<Meters>): Float = + ((x - xRange.start) / (xRange.endInclusive - xRange.start) * size.width).toFloat() - val center = toOffset(position.x, position.y) + fun yToPx(y: NumericalValue<Meters>): Float = + ((y - yRange.start) / (yRange.endInclusive - yRange.start) * size.height).toFloat() - drawRect( - Color.LightGray, - topLeft = Offset(0f, center.y - 5f), - size = Size(size.width, 10f) - ) + fun toOffset(xy: XY<Meters>): Offset = Offset(xToPx(xy.x), yToPx(xy.y)) - drawCircle(Color.Black, radius = 10f, center = center) + observeState(plotterModel.y, "beam") { y -> + RectangleDrawable2D( + position = Offset(size.width / 2, yToPx(y)), + rectangleSize = Size(size.width, 10f), + color = Color.LightGray + ) + } + observeState(plotterModel.xy, "head") { xy -> + CircleDrawable2D( + position = toOffset(xy), + radius = 10f, + color = Color.Black + ) + } - points.forEach { - drawCircle(it.color, radius = 2f, center = toOffset(it.x, it.y)) + snapshotFlow { points }.onEach { + it.forEachIndexed { index, plotterPoint -> + circle( + "point[$index]", + Offset(xToPx(plotterPoint.x), yToPx(plotterPoint.y)), + radius = 5f, + color = plotterPoint.color + ) + } + }.launchIn(scope) + } } } } } - } \ No newline at end of file diff --git a/demo/echo/build.gradle.kts b/demo/echo/build.gradle.kts index 873220e..ed722bf 100644 --- a/demo/echo/build.gradle.kts +++ b/demo/echo/build.gradle.kts @@ -21,8 +21,8 @@ kotlin{ } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { - kotlinOptions { - freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") + compilerOptions { + freeCompilerArgs.addAll("-Xjvm-default=all") } } diff --git a/demo/many-devices/build.gradle.kts b/demo/many-devices/build.gradle.kts index 64fbfe2..5743341 100644 --- a/demo/many-devices/build.gradle.kts +++ b/demo/many-devices/build.gradle.kts @@ -26,8 +26,8 @@ kotlin{ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { - kotlinOptions { - freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") + compilerOptions { + freeCompilerArgs.addAll("-Xjvm-default=all") } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c5a861..207964a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -81,7 +81,9 @@ visionforge-markdown = { module = "space.kscience:visionforge-markdown", version visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" } visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" } -sciprog-maps-compose = "space.kscience:maps-kt-compose:0.3.0" +sciprog-maps-compose = { module = "space.kscience:maps-kt-compose", version = "0.3.0" } + +koala-plots = { module = "io.github.koalaplot:koalaplot-core", version = "0.6.1" } # Buildscript diff --git a/magix/magix-java-endpoint/build.gradle.kts b/magix/magix-java-endpoint/build.gradle.kts index ce20b5d..68c7075 100644 --- a/magix/magix-java-endpoint/build.gradle.kts +++ b/magix/magix-java-endpoint/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import space.kscience.gradle.KScienceVersions import space.kscience.gradle.Maturity plugins { @@ -17,19 +15,6 @@ dependencies { implementation(spclibs.kotlinx.coroutines.jdk9) } -//java { -// sourceCompatibility = KScienceVersions.JVM_TARGET -// targetCompatibility = KScienceVersions.JVM_TARGET -//} - - -//FIXME https://youtrack.jetbrains.com/issue/KT-52815/Compiler-option-Xjdk-release-fails-to-compile-mixed-projects -tasks.withType<KotlinCompile>{ - kotlinOptions { - freeCompilerArgs -= "-Xjdk-release=11" - } -} - -readme{ +readme { maturity = Maturity.EXPERIMENTAL } \ No newline at end of file From f0f9d0e174ff3744490a22a678e60c38b32f30ee Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Tue, 23 Jul 2024 20:32:01 +0300 Subject: [PATCH 114/125] Add direct canvas customization for DeviceDrawable2D --- .../kscience/controls/constructor/devices/LimitSwitch.kt | 2 +- .../src/commonMain/kotlin/DeviceDrawable2DStore.kt | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LimitSwitch.kt index cba6b83..d06a385 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LimitSwitch.kt @@ -14,7 +14,7 @@ import space.kscience.dataforge.context.Context /** - * Virtual [LimitSwitch] + * A device that detects if a motor hits the end of its range */ public class LimitSwitch( context: Context, diff --git a/controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2DStore.kt b/controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2DStore.kt index 1c17aad..94bf76c 100644 --- a/controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2DStore.kt +++ b/controls-visualisation-compose/src/commonMain/kotlin/DeviceDrawable2DStore.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.Canvas import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.toSize @@ -24,11 +25,11 @@ public class DeviceDrawable2DStore(public val scope: CoroutineScope, public val public val drawableFlow: MutableStateFlow<Map<String, DeviceDrawable2D>> = MutableStateFlow(emptyMap()) } -public fun DeviceDrawable2DStore.emit(id: String, drawable2D: DeviceDrawable2D){ +public fun DeviceDrawable2DStore.emit(id: String, drawable2D: DeviceDrawable2D) { drawableFlow.value += (id to drawable2D) } -public fun DeviceDrawable2DStore.emitAll(drawables: Map<String, DeviceDrawable2D>){ +public fun DeviceDrawable2DStore.emitAll(drawables: Map<String, DeviceDrawable2D>) { drawableFlow.value += drawables } @@ -62,6 +63,7 @@ public fun <T, D : Device, P : DevicePropertySpec<D, T>> DeviceDrawable2DStore.o @Composable public fun Device2DCanvas( modifier: Modifier = Modifier, + onDraw: DrawScope.() -> Unit = {}, flowBuilder: suspend DeviceDrawable2DStore.() -> Unit, ) { val coroutineScope = rememberCoroutineScope() @@ -85,6 +87,7 @@ public fun Device2DCanvas( drawables.values.forEach { with(it) { draw() } } + onDraw() } } } From 47327aef19954032adba2233e58ca188c133913f Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Thu, 25 Jul 2024 12:28:58 +0300 Subject: [PATCH 115/125] Add message flow output for PidDemo --- .space.kts | 45 ------------------- build.gradle.kts | 1 + .../controls/demo/DemoControllerView.kt | 6 ++- .../constructor/src/jvmMain/kotlin/PidDemo.kt | 18 ++++++-- gradle/libs.versions.toml | 5 +-- 5 files changed, 21 insertions(+), 54 deletions(-) delete mode 100644 .space.kts diff --git a/.space.kts b/.space.kts deleted file mode 100644 index c5dd962..0000000 --- a/.space.kts +++ /dev/null @@ -1,45 +0,0 @@ -import kotlin.io.path.readText - -job("Build") { - gradlew("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3", "build") -} - -job("Publish") { - startOn { - gitPush { enabled = false } - } - container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") { - env["SPACE_USER"] = "{{ project:space_user }}" - env["SPACE_TOKEN"] = "{{ project:space_token }}" - kotlinScript { api -> - - val spaceUser = System.getenv("SPACE_USER") - val spaceToken = System.getenv("SPACE_TOKEN") - - // write the version to the build directory - api.gradlew("version") - - //read the version from build file - val version = java.nio.file.Path.of("build/project-version.txt").readText() - - val revisionSuffix = if (version.endsWith("SNAPSHOT")) { - "-" + api.gitRevision().take(7) - } else { - "" - } - - api.space().projects.automation.deployments.start( - project = api.projectIdentifier(), - targetIdentifier = TargetIdentifier.Key("maps-kt"), - version = version+revisionSuffix, - // automatically update deployment status based on the status of a job - syncWithAutomationJob = true - ) - api.gradlew( - "publishAllPublicationsToSpaceRepository", - "-Ppublishing.space.user=\"$spaceUser\"", - "-Ppublishing.space.token=\"$spaceToken\"", - ) - } - } -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 7f7e534..b9b6d19 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ import space.kscience.gradle.useSPCTeam plugins { id("space.kscience.gradle.project") + alias(libs.plugins.versions) } allprojects { 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 f107f56..fc72520 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 @@ -82,22 +82,24 @@ class DemoController : ContextAware { opcUaServer.startup() opcUaServer.serveDevices(deviceManager) - + //create a remote listener endpoint val listenerEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + // subscribe remote endpoint listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> // print all messages that are not property change message if (deviceMessage !is PropertyChangedMessage) { println(">> ${json.encodeToString(MagixMessage.serializer(), magixMessage)}") } }.launchIn(this) + + // send description request listenerEndpoint.send( format = DeviceManager.magixFormat, payload = GetDescriptionMessage(), source = "listener", // target = "demoDevice" ) - } fun shutdown(): Job = context.launch { diff --git a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt index 81a5923..9411321 100644 --- a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt +++ b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt @@ -19,10 +19,13 @@ import io.github.koalaplot.core.util.toString import io.github.koalaplot.core.xygraph.XYGraph import io.github.koalaplot.core.xygraph.rememberDoubleLinearAxisModel import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.datetime.Instant import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane +import space.kscience.controls.api.PropertyChangedMessage import space.kscience.controls.compose.NumberTextField import space.kscience.controls.compose.PlotNumericState import space.kscience.controls.compose.TimeAxisModel @@ -39,11 +42,9 @@ import space.kscience.controls.constructor.onTimer import space.kscience.controls.constructor.units.Kilograms import space.kscience.controls.constructor.units.Meters import space.kscience.controls.constructor.units.NumericalValue -import space.kscience.controls.manager.ClockManager -import space.kscience.controls.manager.DeviceManager -import space.kscience.controls.manager.clock -import space.kscience.controls.manager.install +import space.kscience.controls.manager.* import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request import java.awt.Dimension import kotlin.math.PI import kotlin.math.sin @@ -165,6 +166,15 @@ fun main() = application { //bind pid parameters LaunchedEffect(Unit) { + + // start listening to local device hub + context.request(DeviceManager).hubMessageFlow() + .filterIsInstance<PropertyChangedMessage>() // filter only property change messages + //.filter { it.sourceDevice == "linearDrive".asName()} //optionally filter by device name + .onEach { + println("${it.sourceDevice} >> ${it.property} changed to ${it.value}") + }.launchIn(this) + snapshotFlow { pidParameters }.onEach { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 207964a..daf1fd6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,8 +31,6 @@ plc4j = "0.12.0" visionforge = "0.4.2" -versions = "0.51.0" - [libraries] dataforge-io = { module = "space.kscience:dataforge-io", version.ref = "dataforge" } @@ -89,4 +87,5 @@ koala-plots = { module = "io.github.koalaplot:koalaplot-core", version = "0.6.1" [plugins] -versions = { id = "com.github.ben-manes.versions", version.ref = "versions" } +versions = "com.github.ben-manes.versions:0.51.0" +versions-update = "nl.littlerobots.version-catalog-update:0.8.4" From c12f1ce1cdd7f1423fd33e6037447bd9c8285d51 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sat, 3 Aug 2024 21:11:59 +0300 Subject: [PATCH 116/125] Add lifecycle to ports. Suspended device start --- CHANGELOG.md | 3 + .../controls/constructor/DeviceGroup.kt | 6 +- .../controls/constructor/DeviceGroupTest.kt | 4 +- .../controls/api/AsynchronousSocket.kt | 12 +--- .../space/kscience/controls/api/Device.kt | 47 ++++----------- .../kscience/controls/api/DeviceMessage.kt | 2 +- .../kscience/controls/api/WithLifeCycle.kt | 59 +++++++++++++++++++ .../controls/ports/AsynchronousPort.kt | 11 ++-- .../controls/ports/SynchronousPort.kt | 18 +++--- .../kscience/controls/spec/DeviceBase.kt | 13 ++-- .../kscience/controls/spec/DeviceSpec.kt | 2 +- .../kscience/controls/ports/ChannelPort.kt | 18 +++--- .../kscience/controls/ports/UdpSocketPort.kt | 9 +-- .../controls/ports/AsynchronousPortIOTest.kt | 8 +-- .../kscience/controls/client/DeviceClient.kt | 4 +- .../controls/pi/AsynchronousPiPort.kt | 10 ++-- .../kscience/controls/pi/SynchronousPiPort.kt | 15 +++-- .../kscience/controls/ports/KtorTcpPort.kt | 16 ++--- .../kscience/controls/ports/KtorUdpPort.kt | 16 ++--- .../controls/serial/AsynchronousSerialPort.kt | 13 ++-- .../controls/serial/SynchronousSerialPort.kt | 16 ++--- .../sciprog/devices/mks/MksPdr900Device.kt | 4 +- .../pimotionmaster/PiMotionMasterDevice.kt | 4 +- .../PiMotionMasterVirtualDevice.kt | 17 +++--- 24 files changed, 185 insertions(+), 142 deletions(-) create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/api/WithLifeCycle.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b1a5c..26cb25f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `DeviceClient` properly evaluates lifecycle and logs - `PeerConnection` API for direct device-device binary sharing - DeviceDrawable2D intermediate visualization implementation +- New interface `WithLifeCycle`. Change Port API to adhere to it. ### Changed - Constructor properties return `DeviceState` in order to be able to subscribe to them @@ -16,6 +17,8 @@ - `DeviceClient` now initializes property and action descriptors eagerly. - `DeviceHub` now works with `Name` instead of `NameToken`. Tree-like structure is made using `Path`. Device messages no longer have access to sub-devices. - Add some utility methods to ports. Synchronous port response could be now consumed as `Source`. +- `DeviceLifecycleState` is replaced by `LifecycleState`. + ### 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 index a41899a..79ff2ed 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,7 +3,7 @@ package space.kscience.controls.constructor import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import space.kscience.controls.api.* -import space.kscience.controls.api.DeviceLifecycleState.* +import space.kscience.controls.api.LifecycleState.* import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install import space.kscience.controls.spec.DevicePropertySpec @@ -165,11 +165,11 @@ public open class DeviceGroup( return action(argument) } - final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED + final override var lifecycleState: LifecycleState = LifecycleState.STOPPED private set - private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) { + private suspend fun setLifecycleState(lifecycleState: LifecycleState) { this.lifecycleState = lifecycleState sharedMessageFlow.emit( DeviceLifeCycleMessage(lifecycleState) diff --git a/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/DeviceGroupTest.kt b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/DeviceGroupTest.kt index 40f00dc..aa46133 100644 --- a/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/DeviceGroupTest.kt +++ b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/DeviceGroupTest.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import space.kscience.controls.api.Device import space.kscience.controls.api.DeviceLifeCycleMessage -import space.kscience.controls.api.DeviceLifecycleState +import space.kscience.controls.api.LifecycleState import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install import space.kscience.controls.spec.doRecurring @@ -37,7 +37,7 @@ class DeviceGroupTest { } error("Error!") } - testDevice.messageFlow.first { it is DeviceLifeCycleMessage && it.state == DeviceLifecycleState.STOPPED } + testDevice.messageFlow.first { it is DeviceLifeCycleMessage && it.state == LifecycleState.STOPPED } println("stopped") } } \ No newline at end of file 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 index defefb2..7c56084 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/AsynchronousSocket.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/AsynchronousSocket.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow /** * A generic bidirectional asynchronous sender/receiver object */ -public interface AsynchronousSocket<T> : AutoCloseable { +public interface AsynchronousSocket<T> : WithLifeCycle { /** * Send an object to the socket */ @@ -15,16 +15,6 @@ public interface AsynchronousSocket<T> : AutoCloseable { * Flow of objects received from socket */ public fun subscribe(): Flow<T> - - /** - * Start socket operation - */ - public fun open() - - /** - * Check if this socket is open - */ - public val isOpen: Boolean } /** 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 240eaa0..1e941bb 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 @@ -4,7 +4,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.* -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 @@ -15,40 +14,13 @@ import space.kscience.dataforge.meta.string import space.kscience.dataforge.misc.DfType import space.kscience.dataforge.names.parseAsName -/** - * A lifecycle state of a device - */ -@Serializable -public enum class DeviceLifecycleState { - - /** - * Device is initializing - */ - STARTING, - - /** - * The Device is initialized and running - */ - STARTED, - - /** - * The Device is closed - */ - STOPPED, - - /** - * The device encountered irrecoverable error - */ - ERROR -} - /** * General interface describing a managed Device. * [Device] is a supervisor scope encompassing all operations on a device. * When canceled, cancels all running processes. */ @DfType(DEVICE_TARGET) -public interface Device : ContextAware, CoroutineScope { +public interface Device : ContextAware, WithLifeCycle, CoroutineScope { /** * Initial configuration meta for the device @@ -94,18 +66,16 @@ public interface Device : ContextAware, CoroutineScope { * 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 + override suspend fun start(): Unit = Unit /** * Close and terminate the device. This function does not wait for the device to be closed. */ - public suspend fun stop() { + override suspend fun stop() { coroutineContext[Job]?.cancel("The device is closed") logger.info { "Device $this is closed" } } - public val lifecycleState: DeviceLifecycleState - public companion object { public const val DEVICE_TARGET: String = "device" } @@ -114,7 +84,7 @@ 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)}]" +public val Device.id: String get() = meta["id"].string ?: "device[${hashCode().toString(16)}]" /** * Device that caches properties values @@ -167,3 +137,12 @@ public fun Device.onPropertyChange( public fun Device.propertyMessageFlow(propertyName: String): Flow<PropertyChangedMessage> = messageFlow .filterIsInstance<PropertyChangedMessage>() .filter { it.property == propertyName } + +/** + * React on device lifecycle events + */ +public fun Device.onLifecycleEvent( + block: suspend (LifecycleState) -> Unit +): Job = messageFlow.filterIsInstance<DeviceLifeCycleMessage>().onEach { + block(it.state) +}.launchIn(this) \ No newline at end of file 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 1aeabf6..e93fe7d 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 @@ -240,7 +240,7 @@ public data class DeviceErrorMessage( @Serializable @SerialName("lifecycle") public data class DeviceLifeCycleMessage( - val state: DeviceLifecycleState, + val state: LifecycleState, override val sourceDevice: Name = Name.EMPTY, override val targetDevice: Name? = null, override val comment: String? = null, diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/WithLifeCycle.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/WithLifeCycle.kt new file mode 100644 index 0000000..631a66d --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/WithLifeCycle.kt @@ -0,0 +1,59 @@ +package space.kscience.controls.api + +import kotlinx.serialization.Serializable + +/** + * A lifecycle state of a device + */ +@Serializable +public enum class LifecycleState { + + /** + * Device is initializing + */ + STARTING, + + /** + * The Device is initialized and running + */ + STARTED, + + /** + * The Device is closed + */ + STOPPED, + + /** + * The device encountered irrecoverable error + */ + ERROR +} + + +/** + * An object that could be started or stopped functioning + */ +public interface WithLifeCycle { + + public suspend fun start() + + public suspend fun stop() + + public val lifecycleState: LifecycleState +} + +/** + * Bind this object lifecycle to a device lifecycle + * + * The starting and stopping are done in device scope + */ +public fun WithLifeCycle.bindToDeviceLifecycle(device: Device){ + device.onLifecycleEvent { + when(it){ + LifecycleState.STARTING -> start() + LifecycleState.STARTED -> {/*ignore*/} + LifecycleState.STOPPED -> stop() + LifecycleState.ERROR -> stop() + } + } +} \ No newline at end of file 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 index 9b37fd3..29502f9 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/AsynchronousPort.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/AsynchronousPort.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.io.Source import space.kscience.controls.api.AsynchronousSocket +import space.kscience.controls.api.LifecycleState import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get @@ -65,8 +66,8 @@ public abstract class AbstractAsynchronousPort( protected abstract fun onOpen() - final override fun open() { - if (!isOpen) { + final override suspend fun start() { + if (lifecycleState == LifecycleState.STOPPED) { sendJob = scope.launch { for (data in outgoing) { try { @@ -80,7 +81,7 @@ public abstract class AbstractAsynchronousPort( } onOpen() } else { - logger.warn { "$this already opened" } + logger.warn { "$this already started" } } } @@ -89,7 +90,7 @@ public abstract class AbstractAsynchronousPort( * Send a data packet via the port */ override suspend fun send(data: ByteArray) { - check(isOpen) { "The port is not opened" } + check(lifecycleState == LifecycleState.STARTED) { "The port is not opened" } outgoing.send(data) } @@ -100,7 +101,7 @@ public abstract class AbstractAsynchronousPort( */ override fun subscribe(): Flow<ByteArray> = incoming.receiveAsFlow() - override fun close() { + override suspend fun stop() { outgoing.close() incoming.close() sendJob?.cancel() 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 ac368c5..8427760 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 @@ -7,6 +7,8 @@ import kotlinx.coroutines.sync.withLock import kotlinx.io.Buffer import kotlinx.io.Source import kotlinx.io.readByteArray +import space.kscience.controls.api.LifecycleState +import space.kscience.controls.api.WithLifeCycle import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.ContextAware @@ -14,11 +16,7 @@ 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). */ -public interface SynchronousPort : ContextAware, AutoCloseable { - - public fun open() - - public val isOpen: Boolean +public interface SynchronousPort : ContextAware, WithLifeCycle { /** * Send a single message and wait for the flow of response chunks. @@ -71,14 +69,14 @@ private class SynchronousOverAsynchronousPort( override val context: Context get() = port.context - override fun open() { - if (!port.isOpen) port.open() + override suspend fun start() { + if (port.lifecycleState == LifecycleState.STOPPED) port.start() } - override val isOpen: Boolean get() = port.isOpen + override val lifecycleState: LifecycleState get() = port.lifecycleState - override fun close() { - if (port.isOpen) port.close() + override suspend fun stop() { + if (port.lifecycleState == LifecycleState.STARTED) port.stop() } override suspend fun <R> respond( 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 941eda9..a060f46 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,7 +72,6 @@ public abstract class DeviceBase<D : Device>( onBufferOverflow = BufferOverflow.DROP_OLDEST ) - @OptIn(ExperimentalCoroutinesApi::class) override val coroutineContext: CoroutineContext = context.newCoroutineContext( SupervisorJob(context.coroutineContext[Job]) + CoroutineName("Device $id") + @@ -188,11 +187,11 @@ public abstract class DeviceBase<D : Device>( return spec.executeWithMeta(self, argument ?: Meta.EMPTY) } - final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED + final override var lifecycleState: LifecycleState = LifecycleState.STOPPED private set - private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) { + private suspend fun setLifecycleState(lifecycleState: LifecycleState) { this.lifecycleState = lifecycleState sharedMessageFlow.emit( DeviceLifeCycleMessage(lifecycleState) @@ -204,11 +203,11 @@ public abstract class DeviceBase<D : Device>( } final override suspend fun start() { - if (lifecycleState == DeviceLifecycleState.STOPPED) { + if (lifecycleState == LifecycleState.STOPPED) { super.start() - setLifecycleState(DeviceLifecycleState.STARTING) + setLifecycleState(LifecycleState.STARTING) onStart() - setLifecycleState(DeviceLifecycleState.STARTED) + setLifecycleState(LifecycleState.STARTED) } else { logger.debug { "Device $this is already started" } } @@ -220,7 +219,7 @@ public abstract class DeviceBase<D : Device>( final override suspend fun stop() { onStop() - setLifecycleState(DeviceLifecycleState.STOPPED) + setLifecycleState(LifecycleState.STOPPED) super.stop() } 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 503df83..c6f8b0a 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 @@ -33,7 +33,7 @@ public abstract class DeviceSpec<D : Device> { public open suspend fun D.onOpen() { } - public open fun D.onClose() { + public open suspend fun D.onClose() { } 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 85c3d5c..e5430db 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,6 +1,7 @@ package space.kscience.controls.ports import kotlinx.coroutines.* +import space.kscience.controls.api.LifecycleState import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.* import java.net.InetSocketAddress @@ -30,7 +31,7 @@ public class ChannelPort( meta: Meta, coroutineContext: CoroutineContext = context.coroutineContext, channelBuilder: suspend () -> ByteChannel, -) : AbstractAsynchronousPort(context, meta, coroutineContext), AutoCloseable { +) : AbstractAsynchronousPort(context, meta, coroutineContext) { /** * A handler to await port connection @@ -41,7 +42,8 @@ public class ChannelPort( private var listenerJob: Job? = null - override val isOpen: Boolean get() = listenerJob?.isActive == true + override val lifecycleState: LifecycleState + get() = if(listenerJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED override fun onOpen() { listenerJob = scope.launch(Dispatchers.IO) { @@ -71,12 +73,12 @@ public class ChannelPort( } @OptIn(ExperimentalCoroutinesApi::class) - override fun close() { + override suspend fun stop() { listenerJob?.cancel() if (futureChannel.isCompleted) { futureChannel.getCompleted().close() } - super.close() + super.stop() } } @@ -105,12 +107,12 @@ public object TcpPort : Factory<AsynchronousPort> { /** * Create and open TCP port */ - public fun open( + public suspend fun start( context: Context, host: String, port: Int, coroutineContext: CoroutineContext = context.coroutineContext, - ): ChannelPort = build(context, host, port, coroutineContext).apply { open() } + ): ChannelPort = build(context, host, port, coroutineContext).apply { start() } override fun build(context: Context, meta: Meta): ChannelPort { val host = meta["host"].string ?: "localhost" @@ -156,13 +158,13 @@ public object UdpPort : Factory<AsynchronousPort> { /** * 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 suspend fun start( context: Context, remoteHost: String, remotePort: Int, localPort: Int? = null, localHost: String = "localhost", - ): ChannelPort = build(context, remoteHost, remotePort, localPort, localHost).apply { open() } + ): ChannelPort = build(context, remoteHost, remotePort, localPort, localHost).apply { start() } override fun build(context: Context, meta: Meta): ChannelPort { 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 ae65c64..39d4c13 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,6 +1,7 @@ package space.kscience.controls.ports import kotlinx.coroutines.* +import space.kscience.controls.api.LifecycleState import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.Meta import java.net.DatagramPacket @@ -39,13 +40,13 @@ public class UdpSocketPort( } } - override fun close() { + override suspend fun stop() { listenerJob?.cancel() - super.close() + super.stop() } - override val isOpen: Boolean get() = listenerJob?.isActive == true - + override val lifecycleState: LifecycleState + get() = if(listenerJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) { val packet = DatagramPacket( diff --git a/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/AsynchronousPortIOTest.kt b/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/AsynchronousPortIOTest.kt index b19cfad..6709f8c 100644 --- a/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/AsynchronousPortIOTest.kt +++ b/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/AsynchronousPortIOTest.kt @@ -29,8 +29,8 @@ internal class AsynchronousPortIOTest { @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.start(Global, "localhost", 8811, localPort = 8812) + val sender = UdpPort.start(Global, "localhost", 8812, localPort = 8811) delay(30) repeat(10) { @@ -44,7 +44,7 @@ internal class AsynchronousPortIOTest { .toList() assertEquals("Line number 3", res[3].trim()) - receiver.close() - sender.close() + receiver.stop() + sender.stop() } } \ 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 f4a232e..045ee02 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,10 +99,10 @@ public class DeviceClient internal constructor( } private val lifecycleStateFlow = messageFlow.filterIsInstance<DeviceLifeCycleMessage>() - .map { it.state }.stateIn(this, started = SharingStarted.Eagerly, DeviceLifecycleState.STARTED) + .map { it.state }.stateIn(this, started = SharingStarted.Eagerly, LifecycleState.STARTED) @DFExperimental - override val lifecycleState: DeviceLifecycleState get() = lifecycleStateFlow.value + override val lifecycleState: LifecycleState get() = lifecycleStateFlow.value } /** diff --git a/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt b/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt index 1aee657..019ac9e 100644 --- a/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt +++ b/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt @@ -5,6 +5,7 @@ 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.api.LifecycleState import space.kscience.controls.ports.AbstractAsynchronousPort import space.kscience.controls.ports.AsynchronousPort import space.kscience.controls.ports.copyToArray @@ -49,9 +50,10 @@ public class AsynchronousPiPort( } - override val isOpen: Boolean get() = listenerJob?.isActive == true + override val lifecycleState: LifecycleState + get() = if(listenerJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED - override fun close() { + override suspend fun stop() { listenerJob?.cancel() serial.close() } @@ -74,11 +76,11 @@ public class AsynchronousPiPort( return AsynchronousPiPort(context, meta, serial) } - public fun open( + public suspend fun start( context: Context, device: String, block: SerialConfigBuilder.() -> Unit, - ): AsynchronousPiPort = build(context, device, block).apply { open() } + ): AsynchronousPiPort = build(context, device, block).apply { start() } override fun build(context: Context, meta: Meta): AsynchronousPort { val device: String = meta["device"].string ?: error("Device name not defined") diff --git a/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt b/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt index 6bc590c..d91cd6c 100644 --- a/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt +++ b/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import space.kscience.controls.api.LifecycleState import space.kscience.controls.ports.SynchronousPort import space.kscience.controls.ports.copyToArray import space.kscience.dataforge.context.* @@ -27,11 +28,13 @@ public class SynchronousPiPort( ) : SynchronousPort { private val pi = context.request(PiPlugin) - override fun open() { + + override suspend fun start() { serial.open() } - override val isOpen: Boolean get() = serial.isOpen + override val lifecycleState: LifecycleState + get() = if(serial.isOpen) LifecycleState.STARTED else LifecycleState.STOPPED override suspend fun <R> respond( request: ByteArray, @@ -41,7 +44,7 @@ public class SynchronousPiPort( serial.write(request) flow<ByteArray> { val buffer = ByteBuffer.allocate(1024) - while (isOpen) { + while (serial.isOpen) { try { val num = serial.read(buffer) if (num > 0) { @@ -64,7 +67,7 @@ public class SynchronousPiPort( } } - override fun close() { + override suspend fun stop() { serial.close() } @@ -86,11 +89,11 @@ public class SynchronousPiPort( return SynchronousPiPort(context, meta, serial) } - public fun open( + public suspend fun start( context: Context, device: String, block: SerialConfigBuilder.() -> Unit, - ): SynchronousPiPort = build(context, device, block).apply { open() } + ): SynchronousPiPort = build(context, device, block).apply { start() } override fun build(context: Context, meta: Meta): SynchronousPiPort { val device: String = meta["device"].string ?: error("Device name not defined") diff --git a/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorTcpPort.kt b/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorTcpPort.kt index 463e922..d853e46 100644 --- a/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorTcpPort.kt +++ b/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorTcpPort.kt @@ -6,9 +6,9 @@ import io.ktor.network.sockets.aSocket import io.ktor.network.sockets.openReadChannel 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.* +import space.kscience.controls.api.LifecycleState import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta @@ -25,7 +25,7 @@ public class KtorTcpPort internal constructor( public val port: Int, coroutineContext: CoroutineContext = context.coroutineContext, socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {}, -) : AbstractAsynchronousPort(context, meta, coroutineContext), Closeable { +) : AbstractAsynchronousPort(context, meta, coroutineContext) { override fun toString(): String = "port[tcp:$host:$port]" @@ -55,13 +55,13 @@ public class KtorTcpPort internal constructor( writeChannel.await().writeAvailable(data) } - override val isOpen: Boolean - get() = listenerJob?.isActive == true + override val lifecycleState: LifecycleState + get() = if(listenerJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED - override fun close() { + override suspend fun stop() { listenerJob?.cancel() futureSocket.cancel() - super.close() + super.stop() } public companion object : Factory<AsynchronousPort> { @@ -82,13 +82,13 @@ public class KtorTcpPort internal constructor( return KtorTcpPort(context, meta, host, port, coroutineContext, socketOptions) } - public fun open( + public suspend fun start( context: Context, host: String, port: Int, coroutineContext: CoroutineContext = context.coroutineContext, socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {}, - ): KtorTcpPort = build(context, host, port, coroutineContext, socketOptions).apply { open() } + ): KtorTcpPort = build(context, host, port, coroutineContext, socketOptions).apply { start() } override fun build(context: Context, meta: Meta): AsynchronousPort { val host = meta["host"].string ?: "localhost" diff --git a/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorUdpPort.kt b/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorUdpPort.kt index 48096cf..267daa4 100644 --- a/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorUdpPort.kt +++ b/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorUdpPort.kt @@ -4,9 +4,9 @@ import io.ktor.network.selector.ActorSelectorManager 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.* +import space.kscience.controls.api.LifecycleState import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta @@ -24,7 +24,7 @@ public class KtorUdpPort internal constructor( public val localHost: String = "localhost", coroutineContext: CoroutineContext = context.coroutineContext, socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {}, -) : AbstractAsynchronousPort(context, meta, coroutineContext), Closeable { +) : AbstractAsynchronousPort(context, meta, coroutineContext) { override fun toString(): String = "port[udp:$remoteHost:$remotePort]" @@ -58,13 +58,13 @@ public class KtorUdpPort internal constructor( writeChannel.await().writeAvailable(data) } - override val isOpen: Boolean - get() = listenerJob?.isActive == true + override val lifecycleState: LifecycleState + get() = if(listenerJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED - override fun close() { + override suspend fun stop() { listenerJob?.cancel() futureSocket.cancel() - super.close() + super.stop() } public companion object : Factory<AsynchronousPort> { @@ -101,7 +101,7 @@ public class KtorUdpPort internal constructor( /** * Create and open UDP port */ - public fun open( + public suspend fun start( context: Context, remoteHost: String, remotePort: Int, @@ -117,7 +117,7 @@ public class KtorUdpPort internal constructor( localHost, coroutineContext, socketOptions - ).apply { open() } + ).apply { start() } override fun build(context: Context, meta: Meta): AsynchronousPort { val remoteHost by meta.string { error("Remote host is not specified") } diff --git a/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt index b6e83bc..a9c4ede 100644 --- a/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt +++ b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt @@ -5,6 +5,7 @@ import com.fazecast.jSerialComm.SerialPortDataListener import com.fazecast.jSerialComm.SerialPortEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import space.kscience.controls.api.LifecycleState import space.kscience.controls.ports.AbstractAsynchronousPort import space.kscience.controls.ports.AsynchronousPort import space.kscience.dataforge.context.Context @@ -55,18 +56,20 @@ public class AsynchronousSerialPort( comPort.addDataListener(serialPortListener) } - override val isOpen: Boolean get() = comPort.isOpen + override val lifecycleState: LifecycleState + get() = if(comPort.isOpen) LifecycleState.STARTED else LifecycleState.STOPPED + override suspend fun write(data: ByteArray) { comPort.writeBytes(data, data.size) } - override fun close() { + override suspend fun stop() { comPort.removeDataListener() if (comPort.isOpen) { comPort.closePort() } - super.close() + super.stop() } public companion object : Factory<AsynchronousPort> { @@ -100,7 +103,7 @@ public class AsynchronousSerialPort( /** * Construct ComPort with given parameters */ - public fun open( + public suspend fun start( context: Context, portName: String, baudRate: Int = 9600, @@ -118,7 +121,7 @@ public class AsynchronousSerialPort( parity = parity, coroutineContext = coroutineContext, additionalConfig = additionalConfig - ).apply { open() } + ).apply { start() } override fun build(context: Context, meta: Meta): AsynchronousPort { diff --git a/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt index 1a9b4a5..b9fc091 100644 --- a/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt +++ b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import space.kscience.controls.api.LifecycleState import space.kscience.controls.ports.SynchronousPort import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory @@ -28,16 +29,17 @@ public class SynchronousSerialPort( override fun toString(): String = "port[${comPort.descriptivePortName}]" - override fun open() { - if (!isOpen) { + override suspend fun start() { + if (!comPort.isOpen) { comPort.openPort() } } - override val isOpen: Boolean get() = comPort.isOpen + override val lifecycleState: LifecycleState + get() = if(comPort.isOpen) LifecycleState.STARTED else LifecycleState.STOPPED - override fun close() { + override suspend fun stop() { if (comPort.isOpen) { comPort.closePort() } @@ -52,7 +54,7 @@ public class SynchronousSerialPort( comPort.flushIOBuffers() comPort.writeBytes(request, request.size) flow<ByteArray> { - while (isOpen) { + while (comPort.isOpen) { try { val available = comPort.bytesAvailable() if (available > 0) { @@ -108,7 +110,7 @@ public class SynchronousSerialPort( /** * Construct ComPort with given parameters */ - public fun open( + public suspend fun start( context: Context, portName: String, baudRate: Int = 9600, @@ -124,7 +126,7 @@ public class SynchronousSerialPort( stopBits = stopBits, parity = parity, additionalConfig = additionalConfig - ).apply { open() } + ).apply { start() } override fun build(context: Context, meta: Meta): SynchronousPort { 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 b0fd562..d28acc0 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 @@ -99,9 +99,9 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi val error by logicalProperty(MetaConverter.string) - override fun MksPdr900Device.onClose() { + override suspend fun MksPdr900Device.onClose() { if (portDelegate.isInitialized()) { - port.close() + port.stop() } } } 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 b0282d0..04e03df 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 @@ -168,7 +168,7 @@ class PiMotionMasterDevice( } //Update port //address = portSpec.node - port = portFactory(portSpec, context).apply { open() } + port = portFactory(portSpec, context).apply { start() } // connector.open() //Initialize axes val idn = read(identity) @@ -190,7 +190,7 @@ class PiMotionMasterDevice( }) { port?.let { execute(stop) - it.close() + it.stop() } port = null propertyChanged(connected, false) 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 f343d7c..4ec21de 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 @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import space.kscience.controls.api.AsynchronousSocket +import space.kscience.controls.api.LifecycleState import space.kscience.controls.ports.AbstractAsynchronousPort import space.kscience.controls.ports.withDelimiter import space.kscience.dataforge.context.* @@ -48,10 +49,10 @@ abstract class VirtualDevice(val scope: CoroutineScope) : AsynchronousSocket<Byt respond(response()) } - override val isOpen: Boolean - get() = scope.isActive + override val lifecycleState: LifecycleState + get() = if(scope.isActive) LifecycleState.STARTED else LifecycleState.STOPPED - override fun close() = scope.cancel() + override suspend fun stop() = scope.cancel() } class VirtualPort(private val device: VirtualDevice, context: Context) : AbstractAsynchronousPort(context, Meta.EMPTY) { @@ -72,12 +73,12 @@ class VirtualPort(private val device: VirtualDevice, context: Context) : Abstrac device.send(data) } - override val isOpen: Boolean - get() = respondJob?.isActive == true + override val lifecycleState: LifecycleState + get() = if(respondJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED - override fun close() { + override suspend fun stop() { respondJob?.cancel() - super.close() + super.stop() } } @@ -88,7 +89,7 @@ class PiMotionMasterVirtualDevice( scope: CoroutineScope = context, ) : VirtualDevice(scope), ContextAware { - override fun open() { + override suspend fun start() { //add asynchronous send logic here } From 89d78c43bbc148d67bb61025502a6a085f8a68c5 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sat, 3 Aug 2024 21:26:50 +0300 Subject: [PATCH 117/125] Add wasm and native targets to core modules --- build.gradle.kts | 2 +- controls-constructor/build.gradle.kts | 2 ++ controls-core/build.gradle.kts | 1 + .../src/wasmJsMain/kotlin/fromSpec.wasm.kt | 9 +++++++++ controls-magix/build.gradle.kts | 1 + .../kscience/controls/demo/car/IVirtualCar.kt | 0 .../controls/demo/car/MagixVirtualCar.kt | 0 .../kscience/controls/demo/car/VirtualCar.kt | 0 .../controls/demo/car/VirtualCarController.kt | 0 .../src/{main => jvmMain}/kotlin/zmq.kt | 0 magix/magix-api/build.gradle.kts | 1 + magix/magix-mqtt/build.gradle.kts | 15 +++++++++------ .../magix/rabbit/RabbitMQMagixEndpoint.kt | 0 .../magix-storage-xodus/build.gradle.kts | 18 +++++++++++------- magix/magix-zmq/build.gradle.kts | 13 ++++++++----- .../kscince/magix/zmq/ZmqMagixEndpoint.kt | 0 .../kscince/magix/zmq/ZmqMagixFlowPlugin.kt | 1 - 17 files changed, 43 insertions(+), 20 deletions(-) create mode 100644 controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.kt rename demo/car/src/{main => jvmMain}/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt (100%) rename demo/car/src/{main => jvmMain}/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt (100%) rename demo/car/src/{main => jvmMain}/kotlin/space/kscience/controls/demo/car/VirtualCar.kt (100%) rename demo/car/src/{main => jvmMain}/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt (100%) rename demo/magix-demo/src/{main => jvmMain}/kotlin/zmq.kt (100%) rename magix/magix-rabbit/src/{main => jvmMain}/kotlin/space/kscience/magix/rabbit/RabbitMQMagixEndpoint.kt (100%) rename magix/magix-zmq/src/{main => jvmMain}/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt (100%) rename magix/magix-zmq/src/{main => jvmMain}/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt (97%) diff --git a/build.gradle.kts b/build.gradle.kts index b9b6d19..619a512 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { allprojects { group = "space.kscience" - version = "0.4.0-dev-5" + version = "0.4.0-dev-6" repositories{ google() } diff --git a/controls-constructor/build.gradle.kts b/controls-constructor/build.gradle.kts index 008a242..1947f91 100644 --- a/controls-constructor/build.gradle.kts +++ b/controls-constructor/build.gradle.kts @@ -10,6 +10,8 @@ description = """ kscience{ jvm() js() + native() + wasm() useCoroutines() useSerialization() commonMain { diff --git a/controls-core/build.gradle.kts b/controls-core/build.gradle.kts index 9624b35..868cdd0 100644 --- a/controls-core/build.gradle.kts +++ b/controls-core/build.gradle.kts @@ -13,6 +13,7 @@ kscience { jvm() js() native() + wasm() useCoroutines() useSerialization{ json() diff --git a/controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.kt b/controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.kt new file mode 100644 index 0000000..cd77248 --- /dev/null +++ b/controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.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-magix/build.gradle.kts b/controls-magix/build.gradle.kts index ea03a7e..1425eb0 100644 --- a/controls-magix/build.gradle.kts +++ b/controls-magix/build.gradle.kts @@ -13,6 +13,7 @@ kscience { jvm() js() native() + wasm() useCoroutines() useSerialization { json() diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt b/demo/car/src/jvmMain/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt similarity index 100% rename from demo/car/src/main/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt rename to demo/car/src/jvmMain/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt b/demo/car/src/jvmMain/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt similarity index 100% rename from demo/car/src/main/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt rename to demo/car/src/jvmMain/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt b/demo/car/src/jvmMain/kotlin/space/kscience/controls/demo/car/VirtualCar.kt similarity index 100% rename from demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt rename to demo/car/src/jvmMain/kotlin/space/kscience/controls/demo/car/VirtualCar.kt diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt b/demo/car/src/jvmMain/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt similarity index 100% rename from demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt rename to demo/car/src/jvmMain/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt diff --git a/demo/magix-demo/src/main/kotlin/zmq.kt b/demo/magix-demo/src/jvmMain/kotlin/zmq.kt similarity index 100% rename from demo/magix-demo/src/main/kotlin/zmq.kt rename to demo/magix-demo/src/jvmMain/kotlin/zmq.kt diff --git a/magix/magix-api/build.gradle.kts b/magix/magix-api/build.gradle.kts index 68151c3..253f3e3 100644 --- a/magix/magix-api/build.gradle.kts +++ b/magix/magix-api/build.gradle.kts @@ -13,6 +13,7 @@ kscience { jvm() js() native() + wasm() useCoroutines() useSerialization{ json() diff --git a/magix/magix-mqtt/build.gradle.kts b/magix/magix-mqtt/build.gradle.kts index 5261a00..9241929 100644 --- a/magix/magix-mqtt/build.gradle.kts +++ b/magix/magix-mqtt/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` } @@ -7,12 +7,15 @@ description = """ MQTT client magix endpoint """.trimIndent() -dependencies { - api(projects.magix.magixApi) - implementation(libs.hivemq.mqtt.client) - implementation(spclibs.kotlinx.coroutines.jdk8) +kscience { + jvm() + jvmMain { + api(projects.magix.magixApi) + implementation(libs.hivemq.mqtt.client) + implementation(spclibs.kotlinx.coroutines.jdk8) + } } -readme{ +readme { maturity = space.kscience.gradle.Maturity.PROTOTYPE } diff --git a/magix/magix-rabbit/src/main/kotlin/space/kscience/magix/rabbit/RabbitMQMagixEndpoint.kt b/magix/magix-rabbit/src/jvmMain/kotlin/space/kscience/magix/rabbit/RabbitMQMagixEndpoint.kt similarity index 100% rename from magix/magix-rabbit/src/main/kotlin/space/kscience/magix/rabbit/RabbitMQMagixEndpoint.kt rename to magix/magix-rabbit/src/jvmMain/kotlin/space/kscience/magix/rabbit/RabbitMQMagixEndpoint.kt diff --git a/magix/magix-storage/magix-storage-xodus/build.gradle.kts b/magix/magix-storage/magix-storage-xodus/build.gradle.kts index f50abe3..6c35cef 100644 --- a/magix/magix-storage/magix-storage-xodus/build.gradle.kts +++ b/magix/magix-storage/magix-storage-xodus/build.gradle.kts @@ -1,20 +1,24 @@ plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` } kscience { + jvm() useCoroutines() -} - -dependencies { - api(projects.magix.magixStorage) - implementation(libs.xodus.entity.store) + jvmMain { + api(projects.magix.magixStorage) + implementation(libs.xodus.entity.store) // implementation("org.jetbrains.xodus:dnq:2.0.0") - testImplementation(spclibs.kotlinx.coroutines.test) + } + + jvmTest{ + implementation(spclibs.kotlinx.coroutines.test) + } } + readme { maturity = space.kscience.gradle.Maturity.PROTOTYPE } diff --git a/magix/magix-zmq/build.gradle.kts b/magix/magix-zmq/build.gradle.kts index 20cc246..7eedafa 100644 --- a/magix/magix-zmq/build.gradle.kts +++ b/magix/magix-zmq/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` } @@ -9,10 +9,13 @@ description = """ ZMQ client endpoint for Magix """.trimIndent() -dependencies { - api(projects.magix.magixApi) - api("org.slf4j:slf4j-api:2.0.6") - api("org.zeromq:jeromq:0.5.3") +kscience { + jvm() + jvmMain { + api(projects.magix.magixApi) + api("org.slf4j:slf4j-api:2.0.6") + api("org.zeromq:jeromq:0.5.3") + } } readme { diff --git a/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt b/magix/magix-zmq/src/jvmMain/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt similarity index 100% rename from magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt rename to magix/magix-zmq/src/jvmMain/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt diff --git a/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt b/magix/magix-zmq/src/jvmMain/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt similarity index 97% rename from magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt rename to magix/magix-zmq/src/jvmMain/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt index 7813b8c..c35aa39 100644 --- a/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt +++ b/magix/magix-zmq/src/jvmMain/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import org.slf4j.LoggerFactory import org.zeromq.SocketType From 284f9feb9383431d03284408b0802630339a8f8c Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Wed, 13 Nov 2024 17:11:53 +0300 Subject: [PATCH 118/125] Simulation initial commit --- settings.gradle.kts | 1 + simulation-kt/build.gradle.kts | 32 +++++++ .../commonMain/kotlin/GeneratingTimeline.kt | 96 +++++++++++++++++++ .../src/commonMain/kotlin/MergedTimeline.kt | 49 ++++++++++ .../src/commonMain/kotlin/SharedTimeline.kt | 86 +++++++++++++++++ .../src/commonMain/kotlin/Timeline.kt | 76 +++++++++++++++ 6 files changed, 340 insertions(+) create mode 100644 simulation-kt/build.gradle.kts create mode 100644 simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt create mode 100644 simulation-kt/src/commonMain/kotlin/MergedTimeline.kt create mode 100644 simulation-kt/src/commonMain/kotlin/SharedTimeline.kt create mode 100644 simulation-kt/src/commonMain/kotlin/Timeline.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 7e84c71..702ce9b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -52,6 +52,7 @@ dependencyResolutionManagement { } include( + ":simulation-kt", ":controls-core", ":controls-ports-ktor", ":controls-serial", diff --git a/simulation-kt/build.gradle.kts b/simulation-kt/build.gradle.kts new file mode 100644 index 0000000..c30480a --- /dev/null +++ b/simulation-kt/build.gradle.kts @@ -0,0 +1,32 @@ +import space.kscience.gradle.Maturity + +plugins { + id("space.kscience.gradle.mpp") + `maven-publish` +} + +description = """ + Core interfaces for building a device server +""".trimIndent() + +kscience { + jvm() + js() + native() + wasm() + useCoroutines() + useContextReceivers() + + commonMain { + api(spclibs.kotlinx.datetime) + } + + jvmTest{ + implementation(spclibs.logback.classic) + } +} + + +readme{ + maturity = Maturity.EXPERIMENTAL +} \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt b/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt new file mode 100644 index 0000000..4e9da50 --- /dev/null +++ b/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt @@ -0,0 +1,96 @@ +package space.kscience.simulation + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.datetime.Instant +import kotlin.time.Duration + +/** + * @param lookaheadInterval an interval for generated events ahead of the last observed event. + */ +public class GeneratingTimeline<E : TimelineEvent>( + private val generationScope: CoroutineScope, + private val initialEvent: E, + private val lookaheadInterval: Duration, + private val generatorChain: suspend (E) -> E +) : Timeline<E> { + + private val mutex = Mutex() + + private val events = ArrayDeque<E>() + + private val observers: MutableSet<TimelineObserver> = mutableSetOf() + + override val lastEventTime: Instant? + get() = events.lastOrNull()?.time + + override val observedTime: Instant + get() = observers.minOfOrNull { it.lastCollectedEventTime } ?: Instant.DISTANT_PAST + + override fun flowUnobservedEvents(): Flow<E> = events.asFlow() + + override suspend fun advance(toTime: Instant) { + observers.forEach { + it.collect(toTime) + } + } + + private var generatorJob: Job = launchGenerateJob(initialEvent) + + private fun launchGenerateJob(event: E): Job = generationScope.launch { + var currentEvent = event + while(currentEvent.time < observedTime + lookaheadInterval) { + val nextEvent = generatorChain(currentEvent) + mutex.withLock { + events.add(nextEvent) + } + currentEvent = nextEvent + } + } + + private fun regenerate(event: E) { + generatorJob.cancel() + generatorJob = launchGenerateJob(event) + } + + /** + * Discard unconsumed events after [atTime]. + */ + override suspend fun interrupt(atTime: Instant): Unit { + if (atTime < observedTime) + error("Timeline interrupt at time $atTime is not possible because there are observed events before $observedTime") + mutex.withLock { + while (events.isNotEmpty() && events.last().time > atTime) { + events.removeLast() + } + } + } + + override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver { + val observer = object : TimelineObserver { + val observerMutex = Mutex() + override var lastCollectedEventTime: Instant = Instant.DISTANT_PAST + + override suspend fun collect(upTo: Instant) = observerMutex.withLock { + flowUnobservedEvents().takeWhile { it.time <= upTo }.onEach { + lastCollectedEventTime = it.time + }.collector() + //cleanup() + } + + override fun close() { + observers.remove(this) + } + + } + observers.add(observer) + return observer + } +} \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt b/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt new file mode 100644 index 0000000..991cdc4 --- /dev/null +++ b/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt @@ -0,0 +1,49 @@ +package space.kscience.simulation + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.takeWhile +import kotlinx.datetime.Instant + +public class MergedTimeline<E : TimelineEvent>( + private val timelines: List<Timeline<E>> +) : Timeline<E> { + override val lastEventTime: Instant? + get() = timelines.minOfOrNull { it.lastEventTime ?: Instant.DISTANT_PAST } + override val observedTime: Instant + get() = timelines.maxOfOrNull { it.observedTime } ?: Instant.DISTANT_FUTURE + + override fun flowUnobservedEvents(): Flow<E> = timelines.map { flowUnobservedEvents() }.merge() + + override suspend fun advance(toTime: Instant) { + timelines.forEach { it.advance(toTime) } + } + + override suspend fun interrupt(atTime: Instant) { + timelines.forEach { it.interrupt(atTime) } + } + + private val observers: MutableSet<TimelineObserver> = mutableSetOf() + + override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver { + val observer = object : TimelineObserver { + override var lastCollectedEventTime: Instant = Instant.DISTANT_PAST + + override suspend fun collect(upTo: Instant) = timelines + .map { flowUnobservedEvents() } + .merge() + .takeWhile { it.time <= upTo }.onEach { + lastCollectedEventTime = it.time + }.collector() + + + override fun close() { + observers.remove(this) + } + + } + observers.add(observer) + return observer + } +} \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt b/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt new file mode 100644 index 0000000..067d4d2 --- /dev/null +++ b/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt @@ -0,0 +1,86 @@ +package space.kscience.simulation + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.datetime.Instant + +/** + * A manually mutable [Timeline] that could be modified via [emit] method by multiple + */ +public class SharedTimeline<E : TimelineEvent> : Timeline<E> { + + private val mutex = Mutex() + + private val events = ArrayDeque<E>() + + private val observers: MutableSet<TimelineObserver> = mutableSetOf() + + override val lastEventTime: Instant? + get() = events.lastOrNull()?.time + + override val observedTime: Instant + get() = observers.minOfOrNull { it.lastCollectedEventTime } ?: Instant.DISTANT_PAST + + override fun flowUnobservedEvents(): Flow<E> = events.asFlow() + + /** + * Emit new event to the timeline + */ + public suspend fun emit(event: E): Boolean = mutex.withLock { + if (event.time < observedTime) error("Can't emit event $event because there are observed events after $observedTime") + events.add(event) + } + + override suspend fun advance(toTime: Instant) { + observers.forEach { + it.collect(toTime) + } + } + + /** + * Discard all events before [observedTime] + */ + private suspend fun cleanup(): Unit { + val threshold = observedTime + while (events.isNotEmpty() && events.last().time > threshold) { + events.removeFirst() + } + } + + /** + * Discard unconsumed events after [atTime]. + */ + override suspend fun interrupt(atTime: Instant): Unit = mutex.withLock { + val threshold = observedTime + if (atTime < threshold) + error("Timeline interrupt at time $atTime is not possible because there are observed events before $threshold") + while (events.isNotEmpty() && events.last().time > atTime) { + events.removeLast() + } + } + + override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver { + val observer = object : TimelineObserver { + val observerMutex = Mutex() + override var lastCollectedEventTime: Instant = Instant.DISTANT_PAST + + override suspend fun collect(upTo: Instant) = observerMutex.withLock { + flowUnobservedEvents().takeWhile { it.time <= upTo }.onEach { + lastCollectedEventTime = it.time + }.collector() + cleanup() + } + + override fun close() { + observers.remove(this) + } + + } + observers.add(observer) + return observer + } +} \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/Timeline.kt b/simulation-kt/src/commonMain/kotlin/Timeline.kt new file mode 100644 index 0000000..f9f4535 --- /dev/null +++ b/simulation-kt/src/commonMain/kotlin/Timeline.kt @@ -0,0 +1,76 @@ +package space.kscience.simulation + +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant + + +public interface TimelineEvent { + public val time: Instant +} + +public interface TimelineObserver: AutoCloseable { + /** + * The time of the last event collected by this collector + */ + public val lastCollectedEventTime: Instant + + /** + * Collect all uncollected events from [lastCollectedEventTime] to [upTo]. + * + * By default, collects all events. + */ + public suspend fun collect(upTo: Instant = Instant.DISTANT_FUTURE) +} + +/** + * A time-ordered sequence of events of type [E]. There time of events is strictly monotonic, meaning that the time of + * the next event is greater than the previous event time. + * + * Timeline guarantees that all collectors could read all events when they need. Meaning that all unread events are cached. + * + * Timeline guarantees that already read events won't change, but unread events could change. + */ +public interface Timeline<E : TimelineEvent> { + /** + * The timestamp of the last event in a timeline + */ + public val lastEventTime: Instant? + + /** + * The time of the last event that was observed by all observers + */ + public val observedTime: Instant + + /** + * Flow events from [observedTime] to [lastEventTime]. + * + * The resulting flow is finite and should not suspend. + * + * This method does not affect [observedTime]. + */ + public fun flowUnobservedEvents(): Flow<E> + + /** + * Attach observer to this [Timeline]. The observer collection is not triggered right away, but only on demand. + * + * Each collection shifts [TimelineObserver.lastCollectedEventTime] for this observer. + * The value of [observedTime] is the least of all observers [TimelineObserver.lastCollectedEventTime]. + */ + public suspend fun observe( + collector: suspend Flow<E>.() -> Unit + ): TimelineObserver + + /** + * Advance simulation time to [toTime]. This method forces all observers to collect all events in the given range. + * + * This method suspends until all advancement is done + */ + public suspend fun advance(toTime: Instant) + + /** + * Interrupt generation of this timeline and discard unconsumed events after [atTime]. + * + * Throw exception if at least one observer advanced + */ + public suspend fun interrupt(atTime: Instant): Unit +} \ No newline at end of file From becde944032c84074ec0b940680149988a2ac09b Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Thu, 21 Nov 2024 09:42:53 +0300 Subject: [PATCH 119/125] GeneratingTimeLine prototype is working --- .../commonMain/kotlin/GeneratingTimeline.kt | 108 ++++++++++++------ .../src/commonMain/kotlin/MergedTimeline.kt | 20 ++-- .../src/commonMain/kotlin/SharedTimeline.kt | 42 +++---- .../src/commonMain/kotlin/Timeline.kt | 43 ++++--- .../src/commonMain/kotlin/notNullUtils.kt | 35 ++++++ .../src/commonTest/kotlin/TimelineTests.kt | 34 ++++++ 6 files changed, 201 insertions(+), 81 deletions(-) create mode 100644 simulation-kt/src/commonMain/kotlin/notNullUtils.kt create mode 100644 simulation-kt/src/commonTest/kotlin/TimelineTests.kt diff --git a/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt b/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt index 4e9da50..f3d7d92 100644 --- a/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt @@ -2,10 +2,9 @@ package space.kscience.simulation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -20,21 +19,49 @@ public class GeneratingTimeline<E : TimelineEvent>( private val initialEvent: E, private val lookaheadInterval: Duration, private val generatorChain: suspend (E) -> E -) : Timeline<E> { +) : Timeline<E>, AutoCloseable { + + // push to this channel to trigger event generation + private val wakeupChannel = Channel<Unit>(onBufferOverflow = BufferOverflow.DROP_OLDEST) + + private suspend fun kickGenerator() { + wakeupChannel.send(Unit) + } private val mutex = Mutex() - private val events = ArrayDeque<E>() + private val history = ArrayDeque<E>() + + private val lastEvent = MutableSharedFlow<E>(replay = Int.MAX_VALUE) + + private val updateHistoryJob = generationScope.launch { + lastEvent.onEach { + mutex.withLock { + history.add(it) + //cleanup old events + val threshold = observedTime ?: return@withLock + while (history.isNotEmpty() && history.last().time > threshold) { + history.removeFirst() + } + + } + } + } private val observers: MutableSet<TimelineObserver> = mutableSetOf() - override val lastEventTime: Instant? - get() = events.lastOrNull()?.time + override val time: Instant + get() = history.lastOrNull()?.time ?: initialEvent.time - override val observedTime: Instant - get() = observers.minOfOrNull { it.lastCollectedEventTime } ?: Instant.DISTANT_PAST + override val observedTime: Instant? + get() = observers.minOfNotNullOrNull { it.time } - override fun flowUnobservedEvents(): Flow<E> = events.asFlow() + override fun flowUnobservedEvents(): Flow<E> = flow { + history.forEach { e -> + emit(e) + } + emitAll(lastEvent) + } override suspend fun advance(toTime: Instant) { observers.forEach { @@ -42,51 +69,60 @@ public class GeneratingTimeline<E : TimelineEvent>( } } - private var generatorJob: Job = launchGenerateJob(initialEvent) + private var generatorJob: Job = launchGenerator(initialEvent) - private fun launchGenerateJob(event: E): Job = generationScope.launch { + private fun launchGenerator(event: E): Job = generationScope.launch { + kickGenerator() var currentEvent = event - while(currentEvent.time < observedTime + lookaheadInterval) { - val nextEvent = generatorChain(currentEvent) - mutex.withLock { - events.add(nextEvent) + // for each wakeup generate all events in lookaheadInterval + for (u in wakeupChannel) { + while (currentEvent.time < (observedTime ?: event.time) + lookaheadInterval) { + val nextEvent = generatorChain(currentEvent) + lastEvent.emit(nextEvent) + currentEvent = nextEvent } - currentEvent = nextEvent } } - private fun regenerate(event: E) { - generatorJob.cancel() - generatorJob = launchGenerateJob(event) - } - /** - * Discard unconsumed events after [atTime]. - */ - override suspend fun interrupt(atTime: Instant): Unit { - if (atTime < observedTime) - error("Timeline interrupt at time $atTime is not possible because there are observed events before $observedTime") + public suspend fun interrupt(newStart: E) { + check(newStart.time > (observedTime ?: Instant.DISTANT_FUTURE)) { + "Can't interrupt generating timeline after observed event" + } mutex.withLock { - while (events.isNotEmpty() && events.last().time > atTime) { - events.removeLast() + while (history.isNotEmpty() && history.last().time > newStart.time) { + history.removeLast() } + generatorJob.cancel() + generatorJob = launchGenerator(newStart) + } + kickGenerator() + } + + override fun close() { + updateHistoryJob.cancel() + generatorJob.cancel() } override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver { val observer = object : TimelineObserver { - val observerMutex = Mutex() - override var lastCollectedEventTime: Instant = Instant.DISTANT_PAST + override var time: Instant = this@GeneratingTimeline.time - override suspend fun collect(upTo: Instant) = observerMutex.withLock { - flowUnobservedEvents().takeWhile { it.time <= upTo }.onEach { - lastCollectedEventTime = it.time + override suspend fun collect(upTo: Instant) { + flowUnobservedEvents().takeWhile { + it.time <= upTo + }.onEach { + time = it.time + kickGenerator() }.collector() - //cleanup() } override fun close() { observers.remove(this) + if(observers.isEmpty()){ + this@GeneratingTimeline.close() + } } } diff --git a/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt b/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt index 991cdc4..c498bb8 100644 --- a/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt @@ -6,13 +6,15 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.takeWhile import kotlinx.datetime.Instant + public class MergedTimeline<E : TimelineEvent>( private val timelines: List<Timeline<E>> ) : Timeline<E> { - override val lastEventTime: Instant? - get() = timelines.minOfOrNull { it.lastEventTime ?: Instant.DISTANT_PAST } - override val observedTime: Instant - get() = timelines.maxOfOrNull { it.observedTime } ?: Instant.DISTANT_FUTURE + override val time: Instant + get() = timelines.minOfNotNullOrNull { it.time } ?: Instant.DISTANT_PAST + + override val observedTime: Instant? + get() = timelines.maxOfNotNullOrNull { it.observedTime } override fun flowUnobservedEvents(): Flow<E> = timelines.map { flowUnobservedEvents() }.merge() @@ -20,21 +22,21 @@ public class MergedTimeline<E : TimelineEvent>( timelines.forEach { it.advance(toTime) } } - override suspend fun interrupt(atTime: Instant) { - timelines.forEach { it.interrupt(atTime) } - } +// override suspend fun interrupt(atTime: Instant) { +// timelines.forEach { it.interrupt(atTime) } +// } private val observers: MutableSet<TimelineObserver> = mutableSetOf() override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver { val observer = object : TimelineObserver { - override var lastCollectedEventTime: Instant = Instant.DISTANT_PAST + override var time: Instant = this@MergedTimeline.time override suspend fun collect(upTo: Instant) = timelines .map { flowUnobservedEvents() } .merge() .takeWhile { it.time <= upTo }.onEach { - lastCollectedEventTime = it.time + time = it.time }.collector() diff --git a/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt b/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt index 067d4d2..b5e4845 100644 --- a/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt @@ -19,11 +19,11 @@ public class SharedTimeline<E : TimelineEvent> : Timeline<E> { private val observers: MutableSet<TimelineObserver> = mutableSetOf() - override val lastEventTime: Instant? - get() = events.lastOrNull()?.time + override val time: Instant + get() = events.lastOrNull()?.time ?: Instant.DISTANT_PAST - override val observedTime: Instant - get() = observers.minOfOrNull { it.lastCollectedEventTime } ?: Instant.DISTANT_PAST + override val observedTime: Instant? + get() = observers.minOfNotNullOrNull { it.time } override fun flowUnobservedEvents(): Flow<E> = events.asFlow() @@ -31,7 +31,9 @@ public class SharedTimeline<E : TimelineEvent> : Timeline<E> { * Emit new event to the timeline */ public suspend fun emit(event: E): Boolean = mutex.withLock { - if (event.time < observedTime) error("Can't emit event $event because there are observed events after $observedTime") + if (event.time < (observedTime ?: Instant.DISTANT_PAST)) { + error("Can't emit event $event because there are observed events after $observedTime") + } events.add(event) } @@ -44,33 +46,33 @@ public class SharedTimeline<E : TimelineEvent> : Timeline<E> { /** * Discard all events before [observedTime] */ - private suspend fun cleanup(): Unit { - val threshold = observedTime + private suspend fun cleanup(): Unit = mutex.withLock { + val threshold = observedTime ?: return@withLock while (events.isNotEmpty() && events.last().time > threshold) { events.removeFirst() } } - /** - * Discard unconsumed events after [atTime]. - */ - override suspend fun interrupt(atTime: Instant): Unit = mutex.withLock { - val threshold = observedTime - if (atTime < threshold) - error("Timeline interrupt at time $atTime is not possible because there are observed events before $threshold") - while (events.isNotEmpty() && events.last().time > atTime) { - events.removeLast() - } - } +// /** +// * Discard unconsumed events after [atTime]. +// */ +// override suspend fun interrupt(atTime: Instant): Unit = mutex.withLock { +// val threshold = observedTime +// if (atTime < threshold) +// error("Timeline interrupt at time $atTime is not possible because there are observed events before $threshold") +// while (events.isNotEmpty() && events.last().time > atTime) { +// events.removeLast() +// } +// } override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver { val observer = object : TimelineObserver { val observerMutex = Mutex() - override var lastCollectedEventTime: Instant = Instant.DISTANT_PAST + override var time: Instant = this@SharedTimeline.time override suspend fun collect(upTo: Instant) = observerMutex.withLock { flowUnobservedEvents().takeWhile { it.time <= upTo }.onEach { - lastCollectedEventTime = it.time + time = it.time }.collector() cleanup() } diff --git a/simulation-kt/src/commonMain/kotlin/Timeline.kt b/simulation-kt/src/commonMain/kotlin/Timeline.kt index f9f4535..64974ff 100644 --- a/simulation-kt/src/commonMain/kotlin/Timeline.kt +++ b/simulation-kt/src/commonMain/kotlin/Timeline.kt @@ -2,20 +2,31 @@ package space.kscience.simulation import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant +import kotlin.time.Duration public interface TimelineEvent { public val time: Instant } -public interface TimelineObserver: AutoCloseable { +public interface TimelineInterval : TimelineEvent { + public val startTime: Instant + public val duration: Duration + + override val time: Instant + get() = startTime + duration +} + +public data class SimpleTimelineEvent<T>(override val time: Instant, val value: T) : TimelineEvent + +public interface TimelineObserver : AutoCloseable { /** - * The time of the last event collected by this collector + * The subjective time of this observer */ - public val lastCollectedEventTime: Instant + public val time: Instant /** - * Collect all uncollected events from [lastCollectedEventTime] to [upTo]. + * Collect all uncollected events from [time] to [upTo]. * * By default, collects all events. */ @@ -32,17 +43,17 @@ public interface TimelineObserver: AutoCloseable { */ public interface Timeline<E : TimelineEvent> { /** - * The timestamp of the last event in a timeline + * A subjective time of this timeline. The time could advance without events being produced. */ - public val lastEventTime: Instant? + public val time: Instant /** * The time of the last event that was observed by all observers */ - public val observedTime: Instant + public val observedTime: Instant? /** - * Flow events from [observedTime] to [lastEventTime]. + * Flow events from [observedTime] to [time]. * * The resulting flow is finite and should not suspend. * @@ -53,8 +64,8 @@ public interface Timeline<E : TimelineEvent> { /** * Attach observer to this [Timeline]. The observer collection is not triggered right away, but only on demand. * - * Each collection shifts [TimelineObserver.lastCollectedEventTime] for this observer. - * The value of [observedTime] is the least of all observers [TimelineObserver.lastCollectedEventTime]. + * Each collection shifts [TimelineObserver.time] for this observer. + * The value of [observedTime] is the least of all observers [TimelineObserver.time]. */ public suspend fun observe( collector: suspend Flow<E>.() -> Unit @@ -67,10 +78,10 @@ public interface Timeline<E : TimelineEvent> { */ public suspend fun advance(toTime: Instant) - /** - * Interrupt generation of this timeline and discard unconsumed events after [atTime]. - * - * Throw exception if at least one observer advanced - */ - public suspend fun interrupt(atTime: Instant): Unit +// /** +// * Interrupt generation of this timeline and discard unconsumed events after [atTime]. +// * +// * Throw exception if at least one observer advanced +// */ +// public suspend fun interrupt(atTime: Instant): Unit } \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/notNullUtils.kt b/simulation-kt/src/commonMain/kotlin/notNullUtils.kt new file mode 100644 index 0000000..5e7d64b --- /dev/null +++ b/simulation-kt/src/commonMain/kotlin/notNullUtils.kt @@ -0,0 +1,35 @@ +package space.kscience.simulation + +internal inline fun <T, R : Comparable<R>> Iterable<T>.minOfNotNullOrNull(selector: (T) -> R?): R? { + val iterator = iterator() + if (!iterator.hasNext()) return null + var minValue = selector(iterator.next()) + while (iterator.hasNext()) { + val v = selector(iterator.next()) + when { + minValue == null -> minValue = v + v == null -> {/*do nothing*/} + minValue > v -> { + minValue = v + } + } + } + return minValue +} + +internal inline fun <T, R : Comparable<R>> Iterable<T>.maxOfNotNullOrNull(selector: (T) -> R?): R? { + val iterator = iterator() + if (!iterator.hasNext()) return null + var maxValue = selector(iterator.next()) + while (iterator.hasNext()) { + val v = selector(iterator.next()) + when { + maxValue == null -> maxValue = v + v == null -> {/*do nothing*/} + maxValue < v -> { + maxValue = v + } + } + } + return maxValue +} \ No newline at end of file diff --git a/simulation-kt/src/commonTest/kotlin/TimelineTests.kt b/simulation-kt/src/commonTest/kotlin/TimelineTests.kt new file mode 100644 index 0000000..a1bf632 --- /dev/null +++ b/simulation-kt/src/commonTest/kotlin/TimelineTests.kt @@ -0,0 +1,34 @@ +package space.kscience.simulation + +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +class TimelineTests { + + + @Test + fun testGeneration() = runTest(timeout = 5.seconds) { + val startTime = Instant.parse("2020-01-01T00:00:00.000Z") + + val generation = GeneratingTimeline<SimpleTimelineEvent<DoubleArray>>( + this, + initialEvent = SimpleTimelineEvent(startTime, List(10) { it.toDouble() }.toDoubleArray()), + lookaheadInterval = 1.seconds + ) { event -> + val time = event.time + 0.1.seconds + println("Emit: $time") + SimpleTimelineEvent(time, event.value.map { it + 1.0 }.toDoubleArray()) + } + + val collector = generation.observe { + collect { + println("Consume: ${it.time}") + } + } + + collector.collect(startTime + 2.seconds) + collector.close() + } +} \ No newline at end of file From 9a0c55b24aed4a8831e0d7113804342199be5405 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Fri, 29 Nov 2024 10:04:55 +0300 Subject: [PATCH 120/125] Fix flag in serial port --- .../space/kscience/controls/serial/AsynchronousSerialPort.kt | 2 +- .../kotlin/space/kscience/controls/serial/SerialPortPlugin.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt index a9c4ede..b581405 100644 --- a/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt +++ b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt @@ -29,7 +29,7 @@ public class AsynchronousSerialPort( private val serialPortListener = object : SerialPortDataListener { override fun getListeningEvents(): Int = - SerialPort.LISTENING_EVENT_DATA_RECEIVED and SerialPort.LISTENING_EVENT_DATA_AVAILABLE + SerialPort.LISTENING_EVENT_DATA_RECEIVED or SerialPort.LISTENING_EVENT_DATA_AVAILABLE override fun serialEvent(event: SerialPortEvent) { when (event.eventType) { diff --git a/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt index f0d099f..b43fc00 100644 --- a/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt +++ b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt @@ -11,6 +11,8 @@ import space.kscience.dataforge.names.asName public class SerialPortPlugin : AbstractPlugin() { + public val ports: Ports by require(Ports) + override val tag: PluginTag get() = Companion.tag override fun content(target: String): Map<Name, Any> = when (target) { From b4b534df1d55665fb440c373521aaf2e91162497 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Fri, 29 Nov 2024 12:58:24 +0300 Subject: [PATCH 121/125] Change property delegates names --- build.gradle.kts | 2 +- .../controls/spec/propertySpecDelegates.kt | 43 +++++++++++++------ .../sciprog/devices/mks/MksPdr900Device.kt | 6 +-- .../pimotionmaster/PiMotionMasterDevice.kt | 6 +-- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 619a512..7e06cfa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { allprojects { group = "space.kscience" - version = "0.4.0-dev-6" + version = "0.4.0-dev-7" repositories{ google() } 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 8bd22b6..efb66a7 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 @@ -41,25 +41,24 @@ public fun <T, D : Device> DeviceSpec<D>.mutableProperty( write = { _, value: T -> readWriteProperty.set(this, value) } ) +//read only delegates + /** - * Register a mutable logical property (without a corresponding physical state) for a device + * Register a read-only logical property + * (without a corresponding physical state or with a state that is updated asynchronously) for a device */ -public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty( +public fun <T, D : DeviceBase<D>> DeviceSpec<D>.property( converter: MetaConverter<T>, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, -): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> = - mutableProperty( +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = + property( converter, descriptorBuilder, name, read = { propertyName -> getProperty(propertyName)?.let(converter::readOrNull) }, - write = { propertyName, value -> writeProperty(propertyName, converter.convert(value)) } ) - -//read only delegates - public fun <D : Device> DeviceSpec<D>.booleanProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, @@ -141,7 +140,25 @@ public fun <D : Device> DeviceSpec<D>.metaProperty( //read-write delegates -public fun <D : Device> DeviceSpec<D>.booleanProperty( + +/** + * Register a mutable logical property + * (without a corresponding physical state or with a state that is updated asynchronously) for a device + */ +public fun <T, D : DeviceBase<D>> DeviceSpec<D>.mutableProperty( + converter: MetaConverter<T>, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + name: String? = null, +): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> = + mutableProperty( + converter, + descriptorBuilder, + name, + read = { propertyName -> getProperty(propertyName)?.let(converter::readOrNull) }, + write = { propertyName, value -> writeProperty(propertyName, converter.convert(value)) } + ) + +public fun <D : Device> DeviceSpec<D>.mutableBooleanProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Boolean?, @@ -161,7 +178,7 @@ public fun <D : Device> DeviceSpec<D>.booleanProperty( ) -public fun <D : Device> DeviceSpec<D>.numberProperty( +public fun <D : Device> DeviceSpec<D>.mutableNumberProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Number, @@ -169,7 +186,7 @@ public fun <D : Device> DeviceSpec<D>.numberProperty( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Number>>> = mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) -public fun <D : Device> DeviceSpec<D>.doubleProperty( +public fun <D : Device> DeviceSpec<D>.mutableDoubleProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Double, @@ -177,7 +194,7 @@ public fun <D : Device> DeviceSpec<D>.doubleProperty( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Double>>> = mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) -public fun <D : Device> DeviceSpec<D>.stringProperty( +public fun <D : Device> DeviceSpec<D>.mutableStringProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> String, @@ -185,7 +202,7 @@ public fun <D : Device> DeviceSpec<D>.stringProperty( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, String>>> = mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) -public fun <D : Device> DeviceSpec<D>.metaProperty( +public fun <D : Device> DeviceSpec<D>.mutableMetaProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Meta, 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 d28acc0..892d465 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 @@ -88,15 +88,15 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi override fun build(context: Context, meta: Meta): MksPdr900Device = MksPdr900Device(context, meta) - val powerOn by booleanProperty(read = { readPowerOn() }, write = { _, value -> writePowerOn(value) }) + val powerOn by mutableBooleanProperty(read = { readPowerOn() }, write = { _, value -> writePowerOn(value) }) - val channel by logicalProperty(MetaConverter.int) + val channel by property(MetaConverter.int) val value by doubleProperty(read = { readChannelData(getOrRead(channel)) }) - val error by logicalProperty(MetaConverter.string) + val error by property(MetaConverter.string) override suspend fun MksPdr900Device.onClose() { 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 04e03df..4324b9b 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 @@ -23,8 +23,6 @@ import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.* import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.parseAsName -import kotlin.collections.component1 -import kotlin.collections.component2 import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -238,7 +236,7 @@ class PiMotionMasterDevice( private fun axisBooleanProperty( command: String, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - ) = booleanProperty( + ) = mutableBooleanProperty( read = { readAxisBoolean("$command?") }, @@ -251,7 +249,7 @@ class PiMotionMasterDevice( private fun axisNumberProperty( command: String, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - ) = doubleProperty( + ) = mutableDoubleProperty( read = { mm.requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull() ?: error("Malformed $command response. Should include float value for $axisId") From bb09a747105bb055798a98f68056935eca923246 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 8 Dec 2024 10:37:47 +0300 Subject: [PATCH 122/125] GeneratingTimeline fully functional --- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 4 + simulation-kt/build.gradle.kts | 2 +- .../src/commonMain/kotlin/AbstractTimeline.kt | 70 +++++++++ .../commonMain/kotlin/GeneratingTimeline.kt | 143 +++++------------- .../src/commonMain/kotlin/MergedTimeline.kt | 54 ++----- .../src/commonMain/kotlin/SharedTimeline.kt | 84 ++-------- .../src/commonMain/kotlin/Timeline.kt | 27 +--- .../src/commonTest/kotlin/TimelineTests.kt | 38 +++-- 9 files changed, 176 insertions(+), 248 deletions(-) create mode 100644 simulation-kt/src/commonMain/kotlin/AbstractTimeline.kt diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 48c0a02..81aa1c0 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.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 702ce9b..1c6b8b2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,10 @@ pluginManagement { } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} + dependencyResolutionManagement { val toolsVersion: String by extra diff --git a/simulation-kt/build.gradle.kts b/simulation-kt/build.gradle.kts index c30480a..b365d98 100644 --- a/simulation-kt/build.gradle.kts +++ b/simulation-kt/build.gradle.kts @@ -28,5 +28,5 @@ kscience { readme{ - maturity = Maturity.EXPERIMENTAL + maturity = Maturity.PROTOTYPE } \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/AbstractTimeline.kt b/simulation-kt/src/commonMain/kotlin/AbstractTimeline.kt new file mode 100644 index 0000000..9876083 --- /dev/null +++ b/simulation-kt/src/commonMain/kotlin/AbstractTimeline.kt @@ -0,0 +1,70 @@ +package space.kscience.simulation + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.datetime.Instant + +public abstract class AbstractTimeline<E : TimelineEvent>( + protected val timelineScope: CoroutineScope, + protected var startTime: Instant, +) : Timeline<E> { + + private val observers: MutableSet<TimelineObserver> = mutableSetOf() + + private val feedbackChannel = Channel<Unit>(onBufferOverflow = BufferOverflow.DROP_OLDEST) + + override val time: StateFlow<Instant> = feedbackChannel.consumeAsFlow().map { + maxOf(startTime,observers.maxOfOrNull { it.time.value } ?: startTime) + }.stateIn(timelineScope, SharingStarted.Lazily, startTime) + + override suspend fun advance(toTime: Instant) { + observers.forEach { + it.collect(toTime) + } + } + + /** + * Flow unobserved events starting at [time]. The flow could be interrupted if timeline changes + */ + protected abstract fun events(): Flow<E> + + override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver { + val context = currentCoroutineContext() + val observer = object : TimelineObserver { + // observed time + override val time = MutableStateFlow(startTime) + + private val channel = Channel<E>() + + private val collectJob = timelineScope.launch(context) { + channel.consumeAsFlow().onEach { + time.emit(it.time) + feedbackChannel.send(Unit) + }.collector() + } + + + override suspend fun collect(upTo: Instant) = coroutineScope { + require(upTo >= time.value) { "Requested time $upTo is lower than observed ${time.value}" } + events().takeWhile { + it.time <= upTo + }.collect { + channel.send(it) + } + } + + override fun close() { + collectJob.cancel() + observers.remove(this) + } + + } + observers.add(observer) + return observer + } +} \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt b/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt index f3d7d92..6f29266 100644 --- a/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt @@ -1,132 +1,65 @@ package space.kscience.simulation import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Instant import kotlin.time.Duration +/** + * Suspend the collection of this [Flow] until event time is lower that threshold + */ +public fun <E : TimelineEvent> Flow<E>.withTimeThreshold( + threshold: Flow<Instant> +): Flow<E> = transform { event -> + threshold.first { it > event.time } + emit(event) +} + /** * @param lookaheadInterval an interval for generated events ahead of the last observed event. */ public class GeneratingTimeline<E : TimelineEvent>( private val generationScope: CoroutineScope, - private val initialEvent: E, + private val origin: E, private val lookaheadInterval: Duration, - private val generatorChain: suspend (E) -> E -) : Timeline<E>, AutoCloseable { + private val generator: suspend FlowCollector<E>.(E) -> Unit +) : AbstractTimeline<E>(generationScope, origin.time) { - // push to this channel to trigger event generation - private val wakeupChannel = Channel<Unit>(onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val startEventFlow = MutableStateFlow(origin) - private suspend fun kickGenerator() { - wakeupChannel.send(Unit) + private data class EventWithOrigin<E : TimelineEvent>(val origin: E, val event: E) : TimelineEvent { + override val time: Instant get() = event.time } - private val mutex = Mutex() - - private val history = ArrayDeque<E>() - - private val lastEvent = MutableSharedFlow<E>(replay = Int.MAX_VALUE) - - private val updateHistoryJob = generationScope.launch { - lastEvent.onEach { - mutex.withLock { - history.add(it) - //cleanup old events - val threshold = observedTime ?: return@withLock - while (history.isNotEmpty() && history.last().time > threshold) { - history.removeFirst() - } - + private val events: SharedFlow<E> = flow { + coroutineScope { + startEventFlow.collect { startEvent -> + emitAll( + flow { generator(startEvent) }.takeWhile { startEvent == startEventFlow.value }.map { + EventWithOrigin(startEvent, it) + } + ) } } - } - - private val observers: MutableSet<TimelineObserver> = mutableSetOf() - - override val time: Instant - get() = history.lastOrNull()?.time ?: initialEvent.time - - override val observedTime: Instant? - get() = observers.minOfNotNullOrNull { it.time } - - override fun flowUnobservedEvents(): Flow<E> = flow { - history.forEach { e -> - emit(e) - } - emitAll(lastEvent) - } - - override suspend fun advance(toTime: Instant) { - observers.forEach { - it.collect(toTime) - } - } - - private var generatorJob: Job = launchGenerator(initialEvent) - - private fun launchGenerator(event: E): Job = generationScope.launch { - kickGenerator() - var currentEvent = event - // for each wakeup generate all events in lookaheadInterval - for (u in wakeupChannel) { - while (currentEvent.time < (observedTime ?: event.time) + lookaheadInterval) { - val nextEvent = generatorChain(currentEvent) - lastEvent.emit(nextEvent) - currentEvent = nextEvent - } - } - } + }.withTimeThreshold( + threshold = time.map { it + lookaheadInterval } + ).buffer(Channel.UNLIMITED).mapNotNull { + //it.event + it.takeIf { it.origin == startEventFlow.value }?.event + }.shareIn( + scope = generationScope, + started = SharingStarted.Eagerly, + ) + override fun events(): Flow<E> = events public suspend fun interrupt(newStart: E) { - check(newStart.time > (observedTime ?: Instant.DISTANT_FUTURE)) { + check(newStart.time >= time.value) { "Can't interrupt generating timeline after observed event" } - mutex.withLock { - while (history.isNotEmpty() && history.last().time > newStart.time) { - history.removeLast() - } - generatorJob.cancel() - generatorJob = launchGenerator(newStart) - - } - kickGenerator() - } - - override fun close() { - updateHistoryJob.cancel() - generatorJob.cancel() - } - - override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver { - val observer = object : TimelineObserver { - override var time: Instant = this@GeneratingTimeline.time - - override suspend fun collect(upTo: Instant) { - flowUnobservedEvents().takeWhile { - it.time <= upTo - }.onEach { - time = it.time - kickGenerator() - }.collector() - } - - override fun close() { - observers.remove(this) - if(observers.isEmpty()){ - this@GeneratingTimeline.close() - } - } - - } - observers.add(observer) - return observer + startTime = newStart.time + startEventFlow.emit(newStart) } } \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt b/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt index c498bb8..334e8c7 100644 --- a/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt @@ -1,51 +1,25 @@ package space.kscience.simulation +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.takeWhile -import kotlinx.datetime.Instant +import kotlinx.coroutines.flow.flow public class MergedTimeline<E : TimelineEvent>( + timelineScope: CoroutineScope, private val timelines: List<Timeline<E>> -) : Timeline<E> { - override val time: Instant - get() = timelines.minOfNotNullOrNull { it.time } ?: Instant.DISTANT_PAST +) : AbstractTimeline<E>(timelineScope, timelines.minOf { it.time.value }) { - override val observedTime: Instant? - get() = timelines.maxOfNotNullOrNull { it.observedTime } - - override fun flowUnobservedEvents(): Flow<E> = timelines.map { flowUnobservedEvents() }.merge() - - override suspend fun advance(toTime: Instant) { - timelines.forEach { it.advance(toTime) } + override fun events(): Flow<E> = flow { + val buffer = TODO() +// +// timelines.forEach { timeline -> +// timeline.observe { +// collect{ +// buffer.add(it) +// } +// } +// } } -// override suspend fun interrupt(atTime: Instant) { -// timelines.forEach { it.interrupt(atTime) } -// } - - private val observers: MutableSet<TimelineObserver> = mutableSetOf() - - override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver { - val observer = object : TimelineObserver { - override var time: Instant = this@MergedTimeline.time - - override suspend fun collect(upTo: Instant) = timelines - .map { flowUnobservedEvents() } - .merge() - .takeWhile { it.time <= upTo }.onEach { - time = it.time - }.collector() - - - override fun close() { - observers.remove(this) - } - - } - observers.add(observer) - return observer - } } \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt b/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt index b5e4845..d9962d8 100644 --- a/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt @@ -1,88 +1,30 @@ package space.kscience.simulation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.takeWhile -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.datetime.Instant /** * A manually mutable [Timeline] that could be modified via [emit] method by multiple */ -public class SharedTimeline<E : TimelineEvent> : Timeline<E> { +public class SharedTimeline<E : TimelineEvent>( + timelineScope: CoroutineScope, + startTime: Instant +) : AbstractTimeline<E>(timelineScope, startTime) { - private val mutex = Mutex() + private val events = MutableSharedFlow<E>(replay = Channel.UNLIMITED) - private val events = ArrayDeque<E>() - - private val observers: MutableSet<TimelineObserver> = mutableSetOf() - - override val time: Instant - get() = events.lastOrNull()?.time ?: Instant.DISTANT_PAST - - override val observedTime: Instant? - get() = observers.minOfNotNullOrNull { it.time } - - override fun flowUnobservedEvents(): Flow<E> = events.asFlow() + override fun events(): Flow<E> = events /** * Emit new event to the timeline */ - public suspend fun emit(event: E): Boolean = mutex.withLock { - if (event.time < (observedTime ?: Instant.DISTANT_PAST)) { - error("Can't emit event $event because there are observed events after $observedTime") + public suspend fun emit(event: E) { + if (event.time < (events.replayCache.lastOrNull()?.time ?: time.value)) { + error("Can't emit event $event because timeline monotony is broken") } - events.add(event) - } - - override suspend fun advance(toTime: Instant) { - observers.forEach { - it.collect(toTime) - } - } - - /** - * Discard all events before [observedTime] - */ - private suspend fun cleanup(): Unit = mutex.withLock { - val threshold = observedTime ?: return@withLock - while (events.isNotEmpty() && events.last().time > threshold) { - events.removeFirst() - } - } - -// /** -// * Discard unconsumed events after [atTime]. -// */ -// override suspend fun interrupt(atTime: Instant): Unit = mutex.withLock { -// val threshold = observedTime -// if (atTime < threshold) -// error("Timeline interrupt at time $atTime is not possible because there are observed events before $threshold") -// while (events.isNotEmpty() && events.last().time > atTime) { -// events.removeLast() -// } -// } - - override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver { - val observer = object : TimelineObserver { - val observerMutex = Mutex() - override var time: Instant = this@SharedTimeline.time - - override suspend fun collect(upTo: Instant) = observerMutex.withLock { - flowUnobservedEvents().takeWhile { it.time <= upTo }.onEach { - time = it.time - }.collector() - cleanup() - } - - override fun close() { - observers.remove(this) - } - - } - observers.add(observer) - return observer + events.emit(event) } } \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/Timeline.kt b/simulation-kt/src/commonMain/kotlin/Timeline.kt index 64974ff..27219cd 100644 --- a/simulation-kt/src/commonMain/kotlin/Timeline.kt +++ b/simulation-kt/src/commonMain/kotlin/Timeline.kt @@ -1,6 +1,7 @@ package space.kscience.simulation import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.datetime.Instant import kotlin.time.Duration @@ -21,9 +22,9 @@ public data class SimpleTimelineEvent<T>(override val time: Instant, val value: public interface TimelineObserver : AutoCloseable { /** - * The subjective time of this observer + * The subjective time of this observer (last observed time) */ - public val time: Instant + public val time: StateFlow<Instant> /** * Collect all uncollected events from [time] to [upTo]. @@ -33,6 +34,8 @@ public interface TimelineObserver : AutoCloseable { public suspend fun collect(upTo: Instant = Instant.DISTANT_FUTURE) } +public suspend fun TimelineObserver.collect(duration: Duration) = collect(time.value + duration) + /** * A time-ordered sequence of events of type [E]. There time of events is strictly monotonic, meaning that the time of * the next event is greater than the previous event time. @@ -43,29 +46,15 @@ public interface TimelineObserver : AutoCloseable { */ public interface Timeline<E : TimelineEvent> { /** - * A subjective time of this timeline. The time could advance without events being produced. + * A subjective time of this timeline. The subjective time is the last observed time. */ - public val time: Instant + public val time: StateFlow<Instant> - /** - * The time of the last event that was observed by all observers - */ - public val observedTime: Instant? - - /** - * Flow events from [observedTime] to [time]. - * - * The resulting flow is finite and should not suspend. - * - * This method does not affect [observedTime]. - */ - public fun flowUnobservedEvents(): Flow<E> /** * Attach observer to this [Timeline]. The observer collection is not triggered right away, but only on demand. * * Each collection shifts [TimelineObserver.time] for this observer. - * The value of [observedTime] is the least of all observers [TimelineObserver.time]. */ public suspend fun observe( collector: suspend Flow<E>.() -> Unit @@ -84,4 +73,4 @@ public interface Timeline<E : TimelineEvent> { // * Throw exception if at least one observer advanced // */ // public suspend fun interrupt(atTime: Instant): Unit -} \ No newline at end of file +} diff --git a/simulation-kt/src/commonTest/kotlin/TimelineTests.kt b/simulation-kt/src/commonTest/kotlin/TimelineTests.kt index a1bf632..a052d64 100644 --- a/simulation-kt/src/commonTest/kotlin/TimelineTests.kt +++ b/simulation-kt/src/commonTest/kotlin/TimelineTests.kt @@ -1,34 +1,50 @@ package space.kscience.simulation +import kotlinx.coroutines.isActive import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant import kotlin.test.Test +import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds class TimelineTests { @Test - fun testGeneration() = runTest(timeout = 5.seconds) { + fun testGeneration() = runTest{ val startTime = Instant.parse("2020-01-01T00:00:00.000Z") val generation = GeneratingTimeline<SimpleTimelineEvent<DoubleArray>>( this, - initialEvent = SimpleTimelineEvent(startTime, List(10) { it.toDouble() }.toDoubleArray()), + origin = SimpleTimelineEvent(startTime, List(10) { it.toDouble() }.toDoubleArray()), lookaheadInterval = 1.seconds ) { event -> - val time = event.time + 0.1.seconds - println("Emit: $time") - SimpleTimelineEvent(time, event.value.map { it + 1.0 }.toDoubleArray()) - } - - val collector = generation.observe { - collect { - println("Consume: ${it.time}") + var time = event.time + while (isActive) { + time += 0.1.seconds + println("Emit: ${time - startTime}") + emit(SimpleTimelineEvent(time, event.value.map { it + 1.0 }.toDoubleArray())) } } - collector.collect(startTime + 2.seconds) + val result = mutableListOf<Duration>() + + val collector = generation.observe { + collect { + println("Consume: ${it.time - startTime}") + result.add(it.time - startTime) + } + } + + collector.collect(2.seconds) + println("First collection complete") + collector.collect(2.seconds) + println("Second collection complete") + println("Interrupt") + generation.interrupt(SimpleTimelineEvent(startTime + 6.seconds, List(10) { it.toDouble() }.toDoubleArray())) + println("Collecting second") + collector.collect(startTime + 6.seconds + 2.5.seconds) + println(result) collector.close() } } \ No newline at end of file From 203a8c157006a8e8dc5719de4835dd031af83507 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 8 Dec 2024 11:12:08 +0300 Subject: [PATCH 123/125] GeneratingTimeline fully functional --- .../src/commonMain/kotlin/AbstractTimeline.kt | 22 ++++++++++---- .../commonMain/kotlin/GeneratingTimeline.kt | 13 ++++---- .../src/commonMain/kotlin/MergedTimeline.kt | 9 +++--- .../src/commonMain/kotlin/SharedTimeline.kt | 9 +++--- .../src/commonMain/kotlin/Timeline.kt | 21 ++++++++----- .../src/commonTest/kotlin/TimelineTests.kt | 30 +++++++++---------- 6 files changed, 60 insertions(+), 44 deletions(-) diff --git a/simulation-kt/src/commonMain/kotlin/AbstractTimeline.kt b/simulation-kt/src/commonMain/kotlin/AbstractTimeline.kt index 9876083..b4d243f 100644 --- a/simulation-kt/src/commonMain/kotlin/AbstractTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/AbstractTimeline.kt @@ -1,18 +1,23 @@ package space.kscience.simulation -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import kotlinx.datetime.Instant +import kotlin.coroutines.CoroutineContext public abstract class AbstractTimeline<E : TimelineEvent>( - protected val timelineScope: CoroutineScope, protected var startTime: Instant, -) : Timeline<E> { + coroutineContext: CoroutineContext +) : Timeline<E>, AutoCloseable { + + protected val timelineScope: CoroutineScope = CoroutineScope( + coroutineContext + + SupervisorJob(coroutineContext[Job]) + + CoroutineExceptionHandler{ _, throwable -> throwable.printStackTrace() } + + CoroutineName("Timeline") + ) private val observers: MutableSet<TimelineObserver> = mutableSetOf() @@ -67,4 +72,9 @@ public abstract class AbstractTimeline<E : TimelineEvent>( observers.add(observer) return observer } + + override fun close() { + observers.forEach { it.close() } + timelineScope.cancel() + } } \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt b/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt index 6f29266..5690d7c 100644 --- a/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt @@ -1,10 +1,11 @@ package space.kscience.simulation -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.datetime.Instant +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext import kotlin.time.Duration /** @@ -21,11 +22,11 @@ public fun <E : TimelineEvent> Flow<E>.withTimeThreshold( * @param lookaheadInterval an interval for generated events ahead of the last observed event. */ public class GeneratingTimeline<E : TimelineEvent>( - private val generationScope: CoroutineScope, private val origin: E, private val lookaheadInterval: Duration, + coroutineContext: CoroutineContext = EmptyCoroutineContext, private val generator: suspend FlowCollector<E>.(E) -> Unit -) : AbstractTimeline<E>(generationScope, origin.time) { +) : AbstractTimeline<E>(origin.time, coroutineContext) { private val startEventFlow = MutableStateFlow(origin) @@ -46,11 +47,11 @@ public class GeneratingTimeline<E : TimelineEvent>( }.withTimeThreshold( threshold = time.map { it + lookaheadInterval } ).buffer(Channel.UNLIMITED).mapNotNull { - //it.event + //a barrier to avoid leaking stale events after interruption from buffer it.takeIf { it.origin == startEventFlow.value }?.event }.shareIn( - scope = generationScope, - started = SharingStarted.Eagerly, + scope = timelineScope, + started = SharingStarted.Lazily, ) override fun events(): Flow<E> = events diff --git a/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt b/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt index 334e8c7..0da6227 100644 --- a/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt @@ -1,14 +1,15 @@ package space.kscience.simulation -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext public class MergedTimeline<E : TimelineEvent>( - timelineScope: CoroutineScope, - private val timelines: List<Timeline<E>> -) : AbstractTimeline<E>(timelineScope, timelines.minOf { it.time.value }) { + private val timelines: List<Timeline<E>>, + coroutineContext: CoroutineContext = EmptyCoroutineContext +) : AbstractTimeline<E>(timelines.minOf { it.time.value }, coroutineContext) { override fun events(): Flow<E> = flow { val buffer = TODO() diff --git a/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt b/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt index d9962d8..1b45002 100644 --- a/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt @@ -1,18 +1,19 @@ package space.kscience.simulation -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.datetime.Instant +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext /** * A manually mutable [Timeline] that could be modified via [emit] method by multiple */ public class SharedTimeline<E : TimelineEvent>( - timelineScope: CoroutineScope, - startTime: Instant -) : AbstractTimeline<E>(timelineScope, startTime) { + startTime: Instant, + coroutineContext: CoroutineContext = EmptyCoroutineContext +) : AbstractTimeline<E>(startTime, coroutineContext) { private val events = MutableSharedFlow<E>(replay = Channel.UNLIMITED) diff --git a/simulation-kt/src/commonMain/kotlin/Timeline.kt b/simulation-kt/src/commonMain/kotlin/Timeline.kt index 27219cd..20a0746 100644 --- a/simulation-kt/src/commonMain/kotlin/Timeline.kt +++ b/simulation-kt/src/commonMain/kotlin/Timeline.kt @@ -34,7 +34,10 @@ public interface TimelineObserver : AutoCloseable { public suspend fun collect(upTo: Instant = Instant.DISTANT_FUTURE) } -public suspend fun TimelineObserver.collect(duration: Duration) = collect(time.value + duration) +/** + * Collect events for a fixed [duration] since last observed time + */ +public suspend fun TimelineObserver.collect(duration: Duration): Unit = collect(time.value + duration) /** * A time-ordered sequence of events of type [E]. There time of events is strictly monotonic, meaning that the time of @@ -66,11 +69,13 @@ public interface Timeline<E : TimelineEvent> { * This method suspends until all advancement is done */ public suspend fun advance(toTime: Instant) - -// /** -// * Interrupt generation of this timeline and discard unconsumed events after [atTime]. -// * -// * Throw exception if at least one observer advanced -// */ -// public suspend fun interrupt(atTime: Instant): Unit } + +/** + * Perform [collector] action on each event + */ +public suspend fun <E : TimelineEvent> Timeline<E>.observeEach( + collector: suspend (E) -> Unit +): TimelineObserver = observe { + collect(collector) +} \ No newline at end of file diff --git a/simulation-kt/src/commonTest/kotlin/TimelineTests.kt b/simulation-kt/src/commonTest/kotlin/TimelineTests.kt index a052d64..b9f53dd 100644 --- a/simulation-kt/src/commonTest/kotlin/TimelineTests.kt +++ b/simulation-kt/src/commonTest/kotlin/TimelineTests.kt @@ -11,40 +11,38 @@ class TimelineTests { @Test - fun testGeneration() = runTest{ + fun testGeneration() = runTest(timeout = 1.seconds) { val startTime = Instant.parse("2020-01-01T00:00:00.000Z") - val generation = GeneratingTimeline<SimpleTimelineEvent<DoubleArray>>( - this, - origin = SimpleTimelineEvent(startTime, List(10) { it.toDouble() }.toDoubleArray()), + val generation = GeneratingTimeline( + origin = SimpleTimelineEvent(startTime, Unit), lookaheadInterval = 1.seconds ) { event -> var time = event.time while (isActive) { time += 0.1.seconds println("Emit: ${time - startTime}") - emit(SimpleTimelineEvent(time, event.value.map { it + 1.0 }.toDoubleArray())) + emit(SimpleTimelineEvent(time, Unit)) } } val result = mutableListOf<Duration>() - val collector = generation.observe { - collect { - println("Consume: ${it.time - startTime}") - result.add(it.time - startTime) - } + val observer = generation.observeEach { + println("Consume: ${it.time - startTime}") + result.add(it.time - startTime) } - collector.collect(2.seconds) + observer.collect(2.seconds) println("First collection complete") - collector.collect(2.seconds) + observer.collect(2.seconds) println("Second collection complete") println("Interrupt") - generation.interrupt(SimpleTimelineEvent(startTime + 6.seconds, List(10) { it.toDouble() }.toDoubleArray())) - println("Collecting second") - collector.collect(startTime + 6.seconds + 2.5.seconds) + generation.interrupt(SimpleTimelineEvent(startTime + 6.seconds, Unit)) + println("Collecting after interruption") + observer.collect(startTime + 6.seconds + 2.5.seconds) println(result) - collector.close() + generation.close() + } } \ No newline at end of file From f708bb93e238ac100dc9ff26a2e2219e84762fa4 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 8 Dec 2024 12:04:11 +0300 Subject: [PATCH 124/125] Re-implement MergedTimeline --- .../commonMain/kotlin/GeneratingTimeline.kt | 4 +- .../src/commonMain/kotlin/MergedTimeline.kt | 82 ++++++++++++++++--- ...bstractTimeline.kt => ProducerTimeline.kt} | 7 +- .../src/commonMain/kotlin/SharedTimeline.kt | 2 +- 4 files changed, 77 insertions(+), 18 deletions(-) rename simulation-kt/src/commonMain/kotlin/{AbstractTimeline.kt => ProducerTimeline.kt} (90%) diff --git a/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt b/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt index 5690d7c..0698b45 100644 --- a/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/GeneratingTimeline.kt @@ -22,11 +22,11 @@ public fun <E : TimelineEvent> Flow<E>.withTimeThreshold( * @param lookaheadInterval an interval for generated events ahead of the last observed event. */ public class GeneratingTimeline<E : TimelineEvent>( - private val origin: E, + origin: E, private val lookaheadInterval: Duration, coroutineContext: CoroutineContext = EmptyCoroutineContext, private val generator: suspend FlowCollector<E>.(E) -> Unit -) : AbstractTimeline<E>(origin.time, coroutineContext) { +) : ProducerTimeline<E>(origin.time, coroutineContext) { private val startEventFlow = MutableStateFlow(origin) diff --git a/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt b/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt index 0da6227..4614fbb 100644 --- a/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/MergedTimeline.kt @@ -1,7 +1,11 @@ package space.kscience.simulation -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.datetime.Instant import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -9,18 +13,70 @@ import kotlin.coroutines.EmptyCoroutineContext public class MergedTimeline<E : TimelineEvent>( private val timelines: List<Timeline<E>>, coroutineContext: CoroutineContext = EmptyCoroutineContext -) : AbstractTimeline<E>(timelines.minOf { it.time.value }, coroutineContext) { +) : Timeline<E> { - override fun events(): Flow<E> = flow { - val buffer = TODO() -// -// timelines.forEach { timeline -> -// timeline.observe { -// collect{ -// buffer.add(it) -// } -// } -// } + protected val timelineScope: CoroutineScope = CoroutineScope( + coroutineContext + + SupervisorJob(coroutineContext[Job]) + + CoroutineExceptionHandler{ _, throwable -> throwable.printStackTrace() } + + CoroutineName("MergedTimeline") + ) + + override val time: StateFlow<Instant> = combine(timelines.map { it.time }){ array-> + array.max() + }.stateIn(timelineScope, SharingStarted.Lazily, timelines.maxOf { it.time.value }) + + override suspend fun advance(toTime: Instant) { + observers.forEach { + it.collect(toTime) + } + } + + private val observers: MutableSet<TimelineObserver> = mutableSetOf() + + override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver { + val context = currentCoroutineContext() + val buffer = mutableListOf<E>() + + val timelineObservers = timelines.map { + it.observeEach { event -> + buffer.add(event) + } + } + + val observer = object : TimelineObserver { + + private val channel = Channel<E>() + + override val time = MutableStateFlow(this@MergedTimeline.time.value) + + private val collectJob = timelineScope.launch(context) { + channel.consumeAsFlow().onEach { + time.emit(it.time) + }.collector() + } + + private val mutex = Mutex() + + override suspend fun collect(upTo: Instant) = mutex.withLock{ + timelineObservers.forEach { + it.collect(upTo) + } + buffer.sortedBy { it.time }.forEach { + channel.send(it) + buffer.remove(it) + } + } + + override fun close() { + collectJob.cancel() + observers.remove(this) + } + + } + + observers.add(observer) + return observer } } \ No newline at end of file diff --git a/simulation-kt/src/commonMain/kotlin/AbstractTimeline.kt b/simulation-kt/src/commonMain/kotlin/ProducerTimeline.kt similarity index 90% rename from simulation-kt/src/commonMain/kotlin/AbstractTimeline.kt rename to simulation-kt/src/commonMain/kotlin/ProducerTimeline.kt index b4d243f..a201d92 100644 --- a/simulation-kt/src/commonMain/kotlin/AbstractTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/ProducerTimeline.kt @@ -4,10 +4,12 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Instant import kotlin.coroutines.CoroutineContext -public abstract class AbstractTimeline<E : TimelineEvent>( +public abstract class ProducerTimeline<E : TimelineEvent>( protected var startTime: Instant, coroutineContext: CoroutineContext ) : Timeline<E>, AutoCloseable { @@ -53,8 +55,9 @@ public abstract class AbstractTimeline<E : TimelineEvent>( }.collector() } + private val mutex = Mutex() - override suspend fun collect(upTo: Instant) = coroutineScope { + override suspend fun collect(upTo: Instant) = mutex.withLock { require(upTo >= time.value) { "Requested time $upTo is lower than observed ${time.value}" } events().takeWhile { it.time <= upTo diff --git a/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt b/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt index 1b45002..051524e 100644 --- a/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt +++ b/simulation-kt/src/commonMain/kotlin/SharedTimeline.kt @@ -13,7 +13,7 @@ import kotlin.coroutines.EmptyCoroutineContext public class SharedTimeline<E : TimelineEvent>( startTime: Instant, coroutineContext: CoroutineContext = EmptyCoroutineContext -) : AbstractTimeline<E>(startTime, coroutineContext) { +) : ProducerTimeline<E>(startTime, coroutineContext) { private val events = MutableSharedFlow<E>(replay = Channel.UNLIMITED) From e9a37f40fd1da68a1f469cb4b28f190b311ca4b1 Mon Sep 17 00:00:00 2001 From: Alexander Nozik <altavir@gmail.com> Date: Sun, 8 Dec 2024 12:15:22 +0300 Subject: [PATCH 125/125] reademe update --- README.md | 9 +++++++ controls-constructor/README.md | 4 +-- controls-core/README.md | 4 +-- controls-jupyter/README.md | 4 +-- controls-magix/README.md | 4 +-- controls-modbus/README.md | 4 +-- controls-opcua/README.md | 4 +-- controls-pi/README.md | 4 +-- controls-plc4x/README.md | 21 +++++++++++++++ controls-ports-ktor/README.md | 4 +-- controls-serial/README.md | 4 +-- controls-server/README.md | 4 +-- controls-storage/README.md | 4 +-- controls-storage/controls-xodus/README.md | 4 +-- controls-vision/README.md | 4 +-- controls-visualisation-compose/README.md | 4 +-- demo/device-collective/README.md | 10 ------- magix/magix-api/README.md | 4 +-- magix/magix-java-endpoint/README.md | 4 +-- magix/magix-mqtt/README.md | 4 +-- magix/magix-rabbit/README.md | 4 +-- magix/magix-rsocket/README.md | 4 +-- magix/magix-server/README.md | 4 +-- magix/magix-storage/README.md | 4 +-- .../magix-storage-xodus/README.md | 4 +-- magix/magix-utils/README.md | 21 +++++++++++++++ magix/magix-zmq/README.md | 4 +-- simulation-kt/README.md | 26 +++++++++++++++++++ simulation-kt/build.gradle.kts | 13 +++++----- 29 files changed, 130 insertions(+), 62 deletions(-) create mode 100644 controls-plc4x/README.md create mode 100644 magix/magix-utils/README.md create mode 100644 simulation-kt/README.md diff --git a/README.md b/README.md index 6b9c9cd..256b87d 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,15 @@ Automatically checks consistency. > > **Maturity**: EXPERIMENTAL +### [simulation-kt](simulation-kt) +> A framework for combination of asynchronous simulations. +> +> **Maturity**: PROTOTYPE +> +> **Features:** +> - [timeline](simulation-kt/#) : Timeline is an ordered discrete history containing TimeLineEvent + + ### [controls-storage/controls-xodus](controls-storage/controls-xodus) > An implementation of controls-storage on top of JetBrains Xodus. > diff --git a/controls-constructor/README.md b/controls-constructor/README.md index 7da4d0b..f2186bd 100644 --- a/controls-constructor/README.md +++ b/controls-constructor/README.md @@ -6,7 +6,7 @@ A low-code constructor for composite devices simulation ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-constructor:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-constructor:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-constructor:0.4.0-dev-4") + implementation("space.kscience:controls-constructor:0.4.0-dev-7") } ``` diff --git a/controls-core/README.md b/controls-core/README.md index 26ed618..f7bc0bd 100644 --- a/controls-core/README.md +++ b/controls-core/README.md @@ -16,7 +16,7 @@ Core interfaces for building a device server ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -26,6 +26,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-core:0.4.0-dev-4") + implementation("space.kscience:controls-core:0.4.0-dev-7") } ``` diff --git a/controls-jupyter/README.md b/controls-jupyter/README.md index 6100ddd..045b8d0 100644 --- a/controls-jupyter/README.md +++ b/controls-jupyter/README.md @@ -6,7 +6,7 @@ ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-jupyter:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-jupyter:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-jupyter:0.4.0-dev-4") + implementation("space.kscience:controls-jupyter:0.4.0-dev-7") } ``` diff --git a/controls-magix/README.md b/controls-magix/README.md index c474221..f2dd35f 100644 --- a/controls-magix/README.md +++ b/controls-magix/README.md @@ -12,7 +12,7 @@ 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.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-magix:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -22,6 +22,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-magix:0.4.0-dev-4") + implementation("space.kscience:controls-magix:0.4.0-dev-7") } ``` diff --git a/controls-modbus/README.md b/controls-modbus/README.md index 9705e81..ef52403 100644 --- a/controls-modbus/README.md +++ b/controls-modbus/README.md @@ -14,7 +14,7 @@ Automatically checks consistency. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-modbus:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-modbus:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -24,6 +24,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-modbus:0.4.0-dev-4") + implementation("space.kscience:controls-modbus:0.4.0-dev-7") } ``` diff --git a/controls-opcua/README.md b/controls-opcua/README.md index 6867492..1d65c9d 100644 --- a/controls-opcua/README.md +++ b/controls-opcua/README.md @@ -12,7 +12,7 @@ A client and server connectors for OPC-UA via Eclipse Milo ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-opcua:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-opcua:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -22,6 +22,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-opcua:0.4.0-dev-4") + implementation("space.kscience:controls-opcua:0.4.0-dev-7") } ``` diff --git a/controls-pi/README.md b/controls-pi/README.md index 0afb324..9c36e60 100644 --- a/controls-pi/README.md +++ b/controls-pi/README.md @@ -6,7 +6,7 @@ Utils to work with controls-kt on Raspberry pi ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-pi:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-pi:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-pi:0.4.0-dev-4") + implementation("space.kscience:controls-pi:0.4.0-dev-7") } ``` diff --git a/controls-plc4x/README.md b/controls-plc4x/README.md new file mode 100644 index 0000000..21cf398 --- /dev/null +++ b/controls-plc4x/README.md @@ -0,0 +1,21 @@ +# Module controls-plc4x + +A plugin for Controls-kt device server on top of plc4x library + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:controls-plc4x:0.4.0-dev-7`. + +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:controls-plc4x:0.4.0-dev-7") +} +``` diff --git a/controls-ports-ktor/README.md b/controls-ports-ktor/README.md index 1cb277a..376bd13 100644 --- a/controls-ports-ktor/README.md +++ b/controls-ports-ktor/README.md @@ -6,7 +6,7 @@ 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.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-ports-ktor:0.4.0-dev-4") + implementation("space.kscience:controls-ports-ktor:0.4.0-dev-7") } ``` diff --git a/controls-serial/README.md b/controls-serial/README.md index ce95ee5..44598d0 100644 --- a/controls-serial/README.md +++ b/controls-serial/README.md @@ -6,7 +6,7 @@ Implementation of direct serial port communication with JSerialComm ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-serial:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-serial:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-serial:0.4.0-dev-4") + implementation("space.kscience:controls-serial:0.4.0-dev-7") } ``` diff --git a/controls-server/README.md b/controls-server/README.md index 896020d..f143d50 100644 --- a/controls-server/README.md +++ b/controls-server/README.md @@ -6,7 +6,7 @@ A combined Magix event loop server with web server for visualization. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-server:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-server:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-server:0.4.0-dev-4") + implementation("space.kscience:controls-server:0.4.0-dev-7") } ``` diff --git a/controls-storage/README.md b/controls-storage/README.md index dd4ab3d..116792d 100644 --- a/controls-storage/README.md +++ b/controls-storage/README.md @@ -6,7 +6,7 @@ An API for stand-alone Controls-kt device or a hub. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-storage:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-storage:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-storage:0.4.0-dev-4") + implementation("space.kscience:controls-storage:0.4.0-dev-7") } ``` diff --git a/controls-storage/controls-xodus/README.md b/controls-storage/controls-xodus/README.md index 44cdf26..e206f1e 100644 --- a/controls-storage/controls-xodus/README.md +++ b/controls-storage/controls-xodus/README.md @@ -6,7 +6,7 @@ An implementation of controls-storage on top of JetBrains Xodus. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-xodus:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-xodus:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-xodus:0.4.0-dev-4") + implementation("space.kscience:controls-xodus:0.4.0-dev-7") } ``` diff --git a/controls-vision/README.md b/controls-vision/README.md index ff2b648..750f60f 100644 --- a/controls-vision/README.md +++ b/controls-vision/README.md @@ -6,7 +6,7 @@ Dashboard and visualization extensions for devices ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-vision:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-vision:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-vision:0.4.0-dev-4") + implementation("space.kscience:controls-vision:0.4.0-dev-7") } ``` diff --git a/controls-visualisation-compose/README.md b/controls-visualisation-compose/README.md index 0f77d54..da0d9ef 100644 --- a/controls-visualisation-compose/README.md +++ b/controls-visualisation-compose/README.md @@ -6,7 +6,7 @@ Visualisation extension using compose-multiplatform ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-visualisation-compose:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:controls-visualisation-compose:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-visualisation-compose:0.4.0-dev-4") + implementation("space.kscience:controls-visualisation-compose:0.4.0-dev-7") } ``` diff --git a/demo/device-collective/README.md b/demo/device-collective/README.md index 369ec04..433a46f 100644 --- a/demo/device-collective/README.md +++ b/demo/device-collective/README.md @@ -1,14 +1,4 @@ # Module device-collective -# Running demo from gradle -1. Install JDK 17-21 in the system (for example, from https://sdkman.io/jdks or https://github.com/ScoopInstaller/Java). -2. Clone the repository with Git. -3. Run `./gradlew :demo:device-collective:run` from the project root directory. -# Install distribution - -1. Install JDK 17-21 in the system (for example, from https://sdkman.io/jdks or https://github.com/ScoopInstaller/Java). -2. Clone the repository with Git. -3. Run `./gradlew :demo:device-collective:packageUberJarForCurrentOS` from the project root directory. -4. Go to `build/compose/jars/device-collective-<your OS>-<version>.jar`. You can copy it and run with `java -jar <full-name>.jar` diff --git a/magix/magix-api/README.md b/magix/magix-api/README.md index 6f16ac0..0feb75d 100644 --- a/magix/magix-api/README.md +++ b/magix/magix-api/README.md @@ -6,7 +6,7 @@ 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.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:magix-api:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-api:0.4.0-dev-4") + implementation("space.kscience:magix-api:0.4.0-dev-7") } ``` diff --git a/magix/magix-java-endpoint/README.md b/magix/magix-java-endpoint/README.md index 87359f1..5b269a0 100644 --- a/magix/magix-java-endpoint/README.md +++ b/magix/magix-java-endpoint/README.md @@ -6,7 +6,7 @@ Java API to work with magix endpoints without Kotlin ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-java-endpoint:0.4.0-dev-4") + implementation("space.kscience:magix-java-endpoint:0.4.0-dev-7") } ``` diff --git a/magix/magix-mqtt/README.md b/magix/magix-mqtt/README.md index 0ee054a..3661749 100644 --- a/magix/magix-mqtt/README.md +++ b/magix/magix-mqtt/README.md @@ -6,7 +6,7 @@ MQTT client magix endpoint ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-mqtt:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:magix-mqtt:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-mqtt:0.4.0-dev-4") + implementation("space.kscience:magix-mqtt:0.4.0-dev-7") } ``` diff --git a/magix/magix-rabbit/README.md b/magix/magix-rabbit/README.md index 35b43c7..db0de2c 100644 --- a/magix/magix-rabbit/README.md +++ b/magix/magix-rabbit/README.md @@ -6,7 +6,7 @@ RabbitMQ client magix endpoint ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-rabbit:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:magix-rabbit:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-rabbit:0.4.0-dev-4") + implementation("space.kscience:magix-rabbit:0.4.0-dev-7") } ``` diff --git a/magix/magix-rsocket/README.md b/magix/magix-rsocket/README.md index 9f3a42c..b3cf1ba 100644 --- a/magix/magix-rsocket/README.md +++ b/magix/magix-rsocket/README.md @@ -6,7 +6,7 @@ Magix endpoint (client) based on RSocket ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-rsocket:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:magix-rsocket:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-rsocket:0.4.0-dev-4") + implementation("space.kscience:magix-rsocket:0.4.0-dev-7") } ``` diff --git a/magix/magix-server/README.md b/magix/magix-server/README.md index 8259e0f..4e0968e 100644 --- a/magix/magix-server/README.md +++ b/magix/magix-server/README.md @@ -6,7 +6,7 @@ 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.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:magix-server:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-server:0.4.0-dev-4") + implementation("space.kscience:magix-server:0.4.0-dev-7") } ``` diff --git a/magix/magix-storage/README.md b/magix/magix-storage/README.md index 672458e..f8d3c09 100644 --- a/magix/magix-storage/README.md +++ b/magix/magix-storage/README.md @@ -6,7 +6,7 @@ Magix history database API ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-storage:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:magix-storage:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-storage:0.4.0-dev-4") + implementation("space.kscience:magix-storage:0.4.0-dev-7") } ``` diff --git a/magix/magix-storage/magix-storage-xodus/README.md b/magix/magix-storage/magix-storage-xodus/README.md index 2fb495a..3a4a469 100644 --- a/magix/magix-storage/magix-storage-xodus/README.md +++ b/magix/magix-storage/magix-storage-xodus/README.md @@ -6,7 +6,7 @@ ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-storage-xodus:0.4.0-dev-4") + implementation("space.kscience:magix-storage-xodus:0.4.0-dev-7") } ``` diff --git a/magix/magix-utils/README.md b/magix/magix-utils/README.md new file mode 100644 index 0000000..95f51a9 --- /dev/null +++ b/magix/magix-utils/README.md @@ -0,0 +1,21 @@ +# Module magix-utils + +Common utilities and services for Magix endpoints. + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:magix-utils:0.4.0-dev-7`. + +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:magix-utils:0.4.0-dev-7") +} +``` diff --git a/magix/magix-zmq/README.md b/magix/magix-zmq/README.md index 952e02e..23c17c7 100644 --- a/magix/magix-zmq/README.md +++ b/magix/magix-zmq/README.md @@ -6,7 +6,7 @@ ZMQ client endpoint for Magix ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-zmq:0.4.0-dev-4`. +The Maven coordinates of this project are `space.kscience:magix-zmq:0.4.0-dev-7`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-zmq:0.4.0-dev-4") + implementation("space.kscience:magix-zmq:0.4.0-dev-7") } ``` diff --git a/simulation-kt/README.md b/simulation-kt/README.md new file mode 100644 index 0000000..ed4308e --- /dev/null +++ b/simulation-kt/README.md @@ -0,0 +1,26 @@ +# Module simulation-kt + + + +## Features + + - [timeline](#) : Timeline is an ordered discrete history containing TimeLineEvent + + +## Usage + +## Artifact: + +The Maven coordinates of this project are `space.kscience:simulation-kt:0.4.0-dev-7`. + +**Gradle Kotlin DSL:** +```kotlin +repositories { + maven("https://repo.kotlin.link") + mavenCentral() +} + +dependencies { + implementation("space.kscience:simulation-kt:0.4.0-dev-7") +} +``` diff --git a/simulation-kt/build.gradle.kts b/simulation-kt/build.gradle.kts index b365d98..a80b1df 100644 --- a/simulation-kt/build.gradle.kts +++ b/simulation-kt/build.gradle.kts @@ -5,10 +5,6 @@ plugins { `maven-publish` } -description = """ - Core interfaces for building a device server -""".trimIndent() - kscience { jvm() js() @@ -21,12 +17,17 @@ kscience { api(spclibs.kotlinx.datetime) } - jvmTest{ + jvmTest { implementation(spclibs.logback.classic) } } -readme{ +readme { maturity = Maturity.PROTOTYPE + description = """ + A framework for combination of asynchronous simulations. + """.trimIndent() + + feature("timeline") { "Timeline is an ordered discrete history containing TimeLineEvent" } } \ No newline at end of file