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