Обновление зависимостей #10
24
.github/workflows/build.yml
vendored
Normal file
24
.github/workflows/build.yml
vendored
Normal file
@ -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
|
17
.github/workflows/gradle.yml
vendored
17
.github/workflows/gradle.yml
vendored
@ -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
|
31
.github/workflows/pages.yml
vendored
Normal file
31
.github/workflows/pages.yml
vendored
Normal file
@ -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
|
50
.github/workflows/publish.yml
vendored
Normal file
50
.github/workflows/publish.yml
vendored
Normal file
@ -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 }}
|
45
.space.kts
Normal file
45
.space.kts
Normal file
@ -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\"",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
173
README.md
173
README.md
@ -7,14 +7,12 @@ 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).
|
||||
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
|
||||
@ -33,19 +31,158 @@ Among other things, you can:
|
||||
- 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
|
||||
|
||||
@ -54,7 +191,3 @@ the current time. The device is configurable via a simple TornadoFX-based contro
|
||||
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)
|
||||
|
@ -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
|
||||
}
|
||||
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
@ -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()
|
||||
}
|
||||
}
|
@ -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<ActionDescriptor>
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
@ -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,
|
||||
|
@ -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<DeviceManager> {
|
||||
override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP)
|
||||
override val type: KClass<out DeviceManager> = 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 <D : Device> DeviceManager.install(name: String, factory: Factory<D>, meta: Meta = Meta.EMPTY): D {
|
||||
val device = factory(meta, context)
|
||||
registerDevice(NameToken(name), device)
|
||||
@ -45,6 +50,9 @@ public fun <D : Device> DeviceManager.install(name: String, factory: Factory<D>,
|
||||
return device
|
||||
}
|
||||
|
||||
/**
|
||||
* A delegate that initializes device on the first use
|
||||
*/
|
||||
public inline fun <D : Device> DeviceManager.installing(
|
||||
factory: Factory<D>,
|
||||
builder: MutableMeta.() -> Unit = {},
|
||||
@ -52,7 +60,15 @@ public inline fun <D : Device> DeviceManager.installing(
|
||||
val meta = Meta(builder)
|
||||
return ReadOnlyProperty { _, property ->
|
||||
val name = property.name
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<DeviceMessage> {
|
||||
|
@ -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<ByteArray>
|
||||
|
||||
/**
|
||||
* A specialized factory for [Port]
|
||||
*/
|
||||
@Type(PortFactory.TYPE)
|
||||
public interface PortFactory: Factory<Port>{
|
||||
public val type: String
|
||||
@ -20,6 +26,9 @@ public interface PortFactory: Factory<Port>{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<ByteArray> = incoming.receiveAsFlow()
|
||||
|
@ -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<Meta, Port>()
|
||||
|
||||
/**
|
||||
* 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<out Ports> = Ports::class
|
||||
|
||||
override fun build(context: Context, meta: Meta): Ports = Ports()
|
||||
|
||||
}
|
||||
|
@ -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<D : DeviceBase<D>>(
|
||||
private suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
|
||||
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
|
||||
}
|
||||
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
private suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
|
||||
read(device)?.let(converter::objectToMeta)
|
||||
|
||||
|
||||
private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.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<D : Device>(
|
||||
override val context: Context = Global,
|
||||
override val meta: Meta = Meta.EMPTY,
|
||||
) : Device {
|
||||
@ -72,7 +93,7 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
|
||||
/**
|
||||
* Update logical state using given [spec] and its convertor
|
||||
*/
|
||||
protected suspend fun <T> updateLogical(spec: DevicePropertySpec<D, T>, value: T) {
|
||||
public suspend fun <T> updateLogical(spec: DevicePropertySpec<D, T>, value: T) {
|
||||
updateLogical(spec.name, spec.converter.objectToMeta(value))
|
||||
}
|
||||
|
||||
@ -81,10 +102,20 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
|
||||
* 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<D : DeviceBase<D>>(
|
||||
}
|
||||
|
||||
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<D, out Any?>)?.let {
|
||||
invalidate(propertyName)
|
||||
it.writeMeta(self, value)
|
||||
} ?: run {
|
||||
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)
|
||||
}
|
||||
|
||||
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 <T> DevicePropertySpec<D, T>.readOrNull(): T? {
|
||||
val res = read(self) ?: return null
|
||||
updateLogical(name, converter.objectToMeta(res))
|
||||
return res
|
||||
else -> {
|
||||
error("Property $property is not writeable")
|
||||
}
|
||||
|
||||
public suspend fun <T> DevicePropertySpec<D, T>.read(): T =
|
||||
readOrNull() ?: error("Failed to read property $name state")
|
||||
|
||||
public fun <T> DevicePropertySpec<D, T>.get(): T? = getProperty(name)?.let(converter::metaToObject)
|
||||
|
||||
/**
|
||||
* Write typed property state and invalidate logical state
|
||||
*/
|
||||
public suspend fun <T> WritableDevicePropertySpec<D, T>.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<D, *>.invalidate() {
|
||||
invalidate(name)
|
||||
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 operator fun <I, O> DeviceActionSpec<D, I, O>.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<D : DeviceBySpec<D>>(
|
||||
public val spec: DeviceSpec<in D>,
|
||||
context: Context = Global,
|
||||
meta: Meta = Meta.EMPTY,
|
||||
) : DeviceBase<D>(context, meta) {
|
||||
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
|
||||
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
|
||||
|
||||
override suspend fun open(): Unit = with(spec) {
|
||||
super.open()
|
||||
self.onOpen()
|
||||
}
|
||||
|
||||
override fun close(): Unit = with(spec) {
|
||||
self.onClose()
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<D : Device>(
|
||||
public val spec: DeviceSpec<in D>,
|
||||
context: Context = Global,
|
||||
meta: Meta = Meta.EMPTY,
|
||||
) : DeviceBase<D>(context, meta) {
|
||||
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
|
||||
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
|
||||
|
||||
override suspend fun open(): Unit = with(spec) {
|
||||
super.open()
|
||||
self.onOpen()
|
||||
}
|
||||
|
||||
override fun close(): Unit = with(spec) {
|
||||
self.onClose()
|
||||
super.close()
|
||||
}
|
||||
}
|
@ -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<in D : Device, T> {
|
||||
/**
|
||||
* Property descriptor
|
||||
@ -27,7 +30,7 @@ public interface DevicePropertySpec<in D : Device, T> {
|
||||
public val descriptor: PropertyDescriptor
|
||||
|
||||
/**
|
||||
* Meta item converter for resulting type
|
||||
* Meta item converter for the resulting type
|
||||
*/
|
||||
public val converter: MetaConverter<T>
|
||||
|
||||
@ -39,14 +42,10 @@ public interface DevicePropertySpec<in D : Device, T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Property name, should be unique in device
|
||||
* Property name should be unique in a device
|
||||
*/
|
||||
public val DevicePropertySpec<*, *>.name: String get() = descriptor.name
|
||||
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
public suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
|
||||
read(device)?.let(converter::objectToMeta)
|
||||
|
||||
|
||||
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
|
||||
/**
|
||||
@ -54,11 +53,7 @@ public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySp
|
||||
*/
|
||||
@InternalDeviceAPI
|
||||
public suspend fun write(device: D, value: T)
|
||||
}
|
||||
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
public suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
|
||||
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
|
||||
}
|
||||
|
||||
public interface DeviceActionSpec<in D : Device, I, O> {
|
||||
@ -78,45 +73,40 @@ public interface DeviceActionSpec<in D : Device, I, O> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <D : Device, I, O> DeviceActionSpec<D, I, O>.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 <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>): 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 <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||
readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||
|
||||
|
||||
public suspend fun <D : DeviceBase<D>, T : Any> D.read(
|
||||
propertySpec: DevicePropertySpec<D, T>,
|
||||
): T = propertySpec.read()
|
||||
public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||
getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||
|
||||
public suspend fun <D : Device, T : Any> D.read(
|
||||
propertySpec: DevicePropertySpec<D, T>,
|
||||
): T = propertySpec.converter.metaToObject(readProperty(propertySpec.name))
|
||||
?: error("Property meta converter returned null")
|
||||
|
||||
public fun <D : Device, T> D.write(
|
||||
propertySpec: WritableDevicePropertySpec<D, T>,
|
||||
value: T,
|
||||
): Job = launch {
|
||||
/**
|
||||
* Write typed property state and invalidate logical state
|
||||
*/
|
||||
public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, value: T) {
|
||||
writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
|
||||
}
|
||||
|
||||
public fun <D : DeviceBase<D>, T> D.write(
|
||||
propertySpec: WritableDevicePropertySpec<D, T>,
|
||||
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 <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, 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 <D : Device, T> Device.onPropertyChange(
|
||||
spec: DevicePropertySpec<D, T>,
|
||||
@ -127,3 +117,16 @@ public fun <D : Device, T> Device.onPropertyChange(
|
||||
.onEach { change ->
|
||||
change.callback(spec.converter.metaToObject(change.value))
|
||||
}.launchIn(this)
|
||||
|
||||
/**
|
||||
* Reset the logical state of a property
|
||||
*/
|
||||
public suspend fun <D : Device> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
|
||||
invalidate(propertySpec.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the action with name according to [actionSpec]
|
||||
*/
|
||||
public suspend fun <I, O, D : Device> D.execute(actionSpec: DeviceActionSpec<D, I, O>, input: I? = null): O? =
|
||||
actionSpec.execute(this, input)
|
@ -12,6 +12,7 @@ import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
public abstract class DeviceSpec<D : Device> {
|
||||
//initializing meta property for everyone
|
||||
@ -36,20 +37,24 @@ public abstract class DeviceSpec<D : Device> {
|
||||
return deviceProperty
|
||||
}
|
||||
|
||||
public fun <T> registerProperty(
|
||||
converter: MetaConverter<T>,
|
||||
readOnlyProperty: KProperty1<D, T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
): DevicePropertySpec<D, T> {
|
||||
val deviceProperty = object : DevicePropertySpec<D, T> {
|
||||
override val descriptor: PropertyDescriptor =
|
||||
PropertyDescriptor(readOnlyProperty.name).apply(descriptorBuilder)
|
||||
override val converter: MetaConverter<T> = converter
|
||||
override suspend fun read(device: D): T =
|
||||
withContext(device.coroutineContext) { readOnlyProperty.get(device) }
|
||||
}
|
||||
return registerProperty(deviceProperty)
|
||||
}
|
||||
// public fun <T> registerProperty(
|
||||
// converter: MetaConverter<T>,
|
||||
// readOnlyProperty: KProperty1<D, T>,
|
||||
// descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
// ): DevicePropertySpec<D, T> {
|
||||
// val deviceProperty = object : DevicePropertySpec<D, T> {
|
||||
//
|
||||
// override val descriptor: PropertyDescriptor = PropertyDescriptor(readOnlyProperty.name)
|
||||
// .apply(descriptorBuilder)
|
||||
//
|
||||
// override val converter: MetaConverter<T> = converter
|
||||
//
|
||||
// override suspend fun read(device: D): T = withContext(device.coroutineContext) {
|
||||
// readOnlyProperty.get(device)
|
||||
// }
|
||||
// }
|
||||
// return registerProperty(deviceProperty)
|
||||
// }
|
||||
|
||||
public fun <T> property(
|
||||
converter: MetaConverter<T>,
|
||||
|
@ -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 <D : DeviceBase<D>, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
|
||||
public fun <D : Device, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
|
||||
while (isActive) {
|
||||
kotlinx.coroutines.delay(interval)
|
||||
delay(interval)
|
||||
launch {
|
||||
emit(reader())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a recurring (with a fixed delay) task on a device.
|
||||
*/
|
||||
public fun <D : DeviceBase<D>> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
|
||||
public fun <D : Device> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
|
||||
while (isActive) {
|
||||
kotlinx.coroutines.delay(interval)
|
||||
delay(interval)
|
||||
launch {
|
||||
task()
|
||||
}
|
||||
}
|
||||
}
|
@ -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 <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Boolean?
|
||||
@ -35,7 +36,7 @@ private inline fun numberDescriptor(
|
||||
descriptorBuilder()
|
||||
}
|
||||
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.numberProperty(
|
||||
name: String? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
read: suspend D.() -> Number?
|
||||
@ -46,7 +47,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
|
||||
read
|
||||
)
|
||||
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Double?
|
||||
@ -57,7 +58,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
|
||||
read
|
||||
)
|
||||
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.stringProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> String?
|
||||
@ -73,7 +74,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
|
||||
read
|
||||
)
|
||||
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.metaProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Meta?
|
||||
@ -91,7 +92,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
|
||||
|
||||
//read-write delegates
|
||||
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Boolean?,
|
||||
@ -111,7 +112,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
|
||||
)
|
||||
|
||||
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.numberProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Number,
|
||||
@ -119,7 +120,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> =
|
||||
mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
|
||||
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Double,
|
||||
@ -127,7 +128,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> =
|
||||
mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
|
||||
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.stringProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> String,
|
||||
@ -135,7 +136,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> =
|
||||
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
|
||||
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.metaProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Meta,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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<out TcpPortPlugin> = TcpPortPlugin::class
|
||||
|
||||
override fun build(context: Context, meta: Meta): TcpPortPlugin = TcpPortPlugin()
|
||||
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Blocking property get call
|
||||
*/
|
||||
public operator fun <D : DeviceBase<D>, T : Any> D.get(
|
||||
propertySpec: DevicePropertySpec<D, T>
|
||||
): T? = runBlocking { read(propertySpec) }
|
4
controls-ktor-tcp/README.md
Normal file
4
controls-ktor-tcp/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module controls-ktor-tcp
|
||||
|
||||
|
||||
|
@ -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<out KtorTcpPortPlugin> = KtorTcpPortPlugin::class
|
||||
|
||||
override fun build(context: Context, meta: Meta): KtorTcpPortPlugin = KtorTcpPortPlugin()
|
||||
|
||||
}
|
||||
|
32
controls-magix-client/README.md
Normal file
32
controls-magix-client/README.md
Normal file
@ -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")
|
||||
}
|
||||
```
|
@ -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 {
|
||||
|
||||
}
|
@ -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<DeviceMessage>,
|
||||
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<String, Meta>()
|
||||
|
||||
override var propertyDescriptors: Collection<PropertyDescriptor> = emptyList()
|
||||
private set
|
||||
|
||||
override var actionDescriptors: Collection<ActionDescriptor> = emptyList()
|
||||
private set
|
||||
|
||||
private val flowInternal = incomingFlow.filter {
|
||||
it.sourceDevice == deviceName
|
||||
}.shareIn(this, started = SharingStarted.Eagerly).also {
|
||||
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<DeviceMessage> get() = flowInternal
|
||||
|
||||
|
||||
override suspend fun readProperty(propertyName: String): Meta {
|
||||
send(
|
||||
PropertyGetMessage(propertyName, targetDevice = deviceName)
|
||||
)
|
||||
return flowInternal.filterIsInstance<PropertyChangedMessage>().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<ActionResultMessage>().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())
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
4
controls-modbus/README.md
Normal file
4
controls-modbus/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module controls-modbus
|
||||
|
||||
A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
|
||||
|
13
controls-modbus/build.gradle.kts
Normal file
13
controls-modbus/build.gradle.kts
Normal file
@ -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")
|
||||
}
|
@ -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<InputRegister> =
|
||||
master.readInputRegisters(clientId, ref, count).toList()
|
||||
|
||||
private fun Array<out InputRegister>.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<Register> =
|
||||
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<Register>(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<ModbusDevice, Short> = object : ReadWriteProperty<ModbusDevice, Short> {
|
||||
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<ModbusDevice, Double> = object : ReadWriteProperty<ModbusDevice, Double> {
|
||||
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 <reified T> ModbusDevice.opcDouble(
|
||||
//): ReadWriteProperty<Any?, Double> = ma
|
||||
//
|
||||
//public inline fun <reified T> ModbusDeviceBySpec<*>.opcInt(
|
||||
// nodeId: NodeId,
|
||||
// magAge: Double = 1.0,
|
||||
//): ReadWriteProperty<Any?, Int> = opc(nodeId, MetaConverter.int, magAge)
|
||||
//
|
||||
//public inline fun <reified T> ModbusDeviceBySpec<*>.opcString(
|
||||
// nodeId: NodeId,
|
||||
// magAge: Double = 1.0,
|
||||
//): ReadWriteProperty<Any?, String> = opc(nodeId, MetaConverter.string, magAge)
|
@ -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<D: Device>(
|
||||
context: Context,
|
||||
spec: DeviceSpec<D>,
|
||||
override val clientId: Int,
|
||||
override val master: AbstractModbusMaster,
|
||||
meta: Meta = Meta.EMPTY,
|
||||
) : ModbusDevice, DeviceBySpec<D>(spec, context, meta)
|
||||
|
||||
|
||||
public class ModbusHub(
|
||||
public val context: Context,
|
||||
public val masterBuilder: () -> AbstractModbusMaster,
|
||||
public val specs: Map<NameToken, Pair<Int, DeviceSpec<*>>>,
|
||||
) : DeviceHub, AutoCloseable {
|
||||
|
||||
public val master: AbstractModbusMaster by lazy(masterBuilder)
|
||||
|
||||
override val devices: Map<NameToken, ModbusDevice> by lazy {
|
||||
specs.mapValues { (_, pair) ->
|
||||
ModbusDeviceBySpec(
|
||||
context,
|
||||
pair.second,
|
||||
pair.first,
|
||||
master
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
master.disconnect()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
4
controls-opcua/README.md
Normal file
4
controls-opcua/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module controls-opcua
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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<D : MiloDeviceBySpec<D>>(
|
||||
spec: DeviceSpec<D>,
|
||||
context: Context = Global,
|
||||
meta: Meta = Meta.EMPTY
|
||||
) : MiloDevice, DeviceBySpec<D>(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<MiloDevice>.close()
|
||||
super<DeviceBySpec>.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A device-bound OPC-UA property. Does not trigger device properties change.
|
||||
*/
|
||||
public inline fun <reified T> MiloDeviceBySpec<*>.opc(
|
||||
nodeId: NodeId,
|
||||
converter: MetaConverter<T>,
|
||||
magAge: Double = 500.0
|
||||
): ReadWriteProperty<Any?, T> = object : ReadWriteProperty<Any?, T> {
|
||||
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 <reified T> MiloDeviceBySpec<*>.opcDouble(
|
||||
nodeId: NodeId,
|
||||
magAge: Double = 1.0
|
||||
): ReadWriteProperty<Any?, Double> = opc(nodeId, MetaConverter.double, magAge)
|
||||
|
||||
public inline fun <reified T> MiloDeviceBySpec<*>.opcInt(
|
||||
nodeId: NodeId,
|
||||
magAge: Double = 1.0
|
||||
): ReadWriteProperty<Any?, Int> = opc(nodeId, MetaConverter.int, magAge)
|
||||
|
||||
public inline fun <reified T> MiloDeviceBySpec<*>.opcString(
|
||||
nodeId: NodeId,
|
||||
magAge: Double = 1.0
|
||||
): ReadWriteProperty<Any?, String> = opc(nodeId, MetaConverter.string, magAge)
|
@ -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 <reified T: Any> MiloDevice.readOpcWithTime(
|
||||
public suspend inline fun <reified T: Any> OpcUaDevice.readOpcWithTime(
|
||||
nodeId: NodeId,
|
||||
converter: MetaConverter<T>,
|
||||
magAge: Double = 500.0
|
||||
@ -51,7 +50,7 @@ public suspend inline fun <reified T: Any> MiloDevice.readOpcWithTime(
|
||||
/**
|
||||
* Read and coerce value from OPC-UA
|
||||
*/
|
||||
public suspend inline fun <reified T> MiloDevice.readOpc(
|
||||
public suspend inline fun <reified T> OpcUaDevice.readOpc(
|
||||
nodeId: NodeId,
|
||||
converter: MetaConverter<T>,
|
||||
magAge: Double = 500.0
|
||||
@ -73,7 +72,7 @@ public suspend inline fun <reified T> MiloDevice.readOpc(
|
||||
return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
|
||||
}
|
||||
|
||||
public suspend inline fun <reified T> MiloDevice.writeOpc(
|
||||
public suspend inline fun <reified T> OpcUaDevice.writeOpc(
|
||||
nodeId: NodeId,
|
||||
converter: MetaConverter<T>,
|
||||
value: T
|
||||
@ -81,3 +80,47 @@ public suspend inline fun <reified T> MiloDevice.writeOpc(
|
||||
val meta = converter.objectToMeta(value)
|
||||
return client.writeValue(nodeId, DataValue(Variant(meta))).await()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A device-bound OPC-UA property. Does not trigger device properties change.
|
||||
*/
|
||||
public inline fun <reified T> OpcUaDevice.opc(
|
||||
nodeId: NodeId,
|
||||
converter: MetaConverter<T>,
|
||||
magAge: Double = 500.0
|
||||
): ReadWriteProperty<Any?, T> = object : ReadWriteProperty<Any?, T> {
|
||||
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<Any?, Double> = opc<Double>(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<Any?, Int> = 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<Any?, String> = opc(nodeId, MetaConverter.string, magAge)
|
@ -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>(::MiloUsername)
|
||||
}
|
||||
|
||||
//public class MiloKeyPair : MiloIdentity() {
|
||||
//
|
||||
// public companion object : SchemeSpec<MiloUsername>(::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>(::MiloConfiguration)
|
||||
}
|
||||
|
||||
/**
|
||||
* A variant of [DeviceBySpec] that includes OPC-UA client
|
||||
*/
|
||||
public open class OpcUaDeviceBySpec<D : Device>(
|
||||
spec: DeviceSpec<D>,
|
||||
config: MiloConfiguration,
|
||||
context: Context = Global,
|
||||
) : OpcUaDevice, DeviceBySpec<D>(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<DeviceBySpec>.close()
|
||||
}
|
||||
}
|
@ -18,10 +18,10 @@ import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
|
||||
public fun <T:Any> T?.toOptional(): Optional<T> = if(this == null) Optional.empty() else Optional.of(this)
|
||||
internal fun <T:Any> T?.toOptional(): Optional<T> = 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(),
|
||||
|
@ -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>(DemoOpcUaDevice, config) {
|
||||
|
||||
//val randomDouble by opcDouble(NodeId(2, "Dynamic/RandomDouble"))
|
||||
|
||||
suspend fun readRandomDouble() = readOpc(NodeId(2, "Dynamic/RandomDouble"), MetaConverter.double)
|
||||
|
||||
|
||||
companion object : DeviceSpec<DemoOpcUaDevice>() {
|
||||
/**
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
32
controls-serial/README.md
Normal file
32
controls-serial/README.md
Normal file
@ -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")
|
||||
}
|
||||
```
|
@ -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<out SerialPortPlugin> = SerialPortPlugin::class
|
||||
|
||||
override fun build(context: Context, meta: Meta): SerialPortPlugin = SerialPortPlugin()
|
||||
|
||||
}
|
||||
|
32
controls-server/README.md
Normal file
32
controls-server/README.md
Normal file
@ -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")
|
||||
}
|
||||
```
|
@ -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")
|
||||
|
@ -43,10 +43,6 @@ import space.kscience.magix.server.magixModule
|
||||
|
||||
|
||||
private fun Application.deviceServerModule(manager: DeviceManager) {
|
||||
install(WebSockets)
|
||||
// install(CORS) {
|
||||
// anyHost()
|
||||
// }
|
||||
install(StatusPages) {
|
||||
exception<IllegalArgumentException> { call, cause ->
|
||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "")
|
||||
|
@ -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")
|
||||
}
|
||||
```
|
||||
|
32
controls-storage/controls-xodus/README.md
Normal file
32
controls-storage/controls-xodus/README.md
Normal file
@ -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")
|
||||
}
|
||||
```
|
@ -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{
|
||||
|
@ -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"
|
||||
|
@ -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())
|
||||
}
|
4
demo/README.md
Normal file
4
demo/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module demo
|
||||
|
||||
|
||||
|
4
demo/all-things/README.md
Normal file
4
demo/all-things/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module all-things
|
||||
|
||||
|
||||
|
@ -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<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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>(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<DemoDevice>(), Factory<DemoDevice> {
|
||||
class DemoDevice(context: Context, meta: Meta) : DeviceBySpec<IDemoDevice>(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<IDemoDevice>(), Factory<DemoDevice> {
|
||||
|
||||
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<DemoDevice>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Iterable<Pair<Double, Double>>>) {
|
||||
}
|
||||
|
||||
|
||||
@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<Meta?>()// = device.sin.flow()
|
||||
val cosFlow = MutableSharedFlow<Meta?>()// = 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<Iterable<Pair<Double, Double>>> = sinCosFlow.windowed(30)
|
||||
val flow: Flow<Iterable<Pair<Double, Double>>> = 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() }
|
||||
|
||||
}
|
||||
|
||||
|
4
demo/car/README.md
Normal file
4
demo/car/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module car
|
||||
|
||||
|
||||
|
@ -35,9 +35,12 @@ dependencies {
|
||||
// implementation("org.litote.kmongo:kmongo-coroutine-serialization:4.4.0")
|
||||
}
|
||||
|
||||
kotlin{
|
||||
jvmToolchain(11)
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<VirtualCar>(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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
|
4
demo/echo/README.md
Normal file
4
demo/echo/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module echo
|
||||
|
||||
|
||||
|
@ -19,10 +19,12 @@ dependencies {
|
||||
|
||||
implementation("ch.qos.logback:logback-classic:1.2.11")
|
||||
}
|
||||
kotlin{
|
||||
jvmToolchain(11)
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
4
demo/magix-demo/README.md
Normal file
4
demo/magix-demo/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module magix-demo
|
||||
|
||||
|
||||
|
@ -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{
|
||||
|
4
demo/many-devices/README.md
Normal file
4
demo/many-devices/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module many-devices
|
||||
|
||||
|
||||
|
40
demo/many-devices/build.gradle.kts
Normal file
40
demo/many-devices/build.gradle.kts
Normal file
@ -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<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
application {
|
||||
mainClass.set("space.kscience.controls.demo.MassDeviceKt")
|
||||
}
|
@ -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>(MassDevice, context, meta) {
|
||||
private val rng = Random(meta["seed"].int ?: 0)
|
||||
|
||||
private val randomValue get() = rng.nextDouble()
|
||||
|
||||
companion object : DeviceSpec<MassDevice>(), Factory<MassDevice> {
|
||||
|
||||
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<String, Instant>()
|
||||
|
||||
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()) {
|
||||
|
||||
}
|
||||
}
|
4
demo/mks-pdr900/README.md
Normal file
4
demo/mks-pdr900/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module mks-pdr900
|
||||
|
||||
|
||||
|
@ -45,7 +45,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
|
||||
|
||||
|
||||
public suspend fun writePowerOn(powerOnValue: Boolean) {
|
||||
error.invalidate()
|
||||
invalidate(error)
|
||||
if (powerOnValue) {
|
||||
val ans = talk("FP!ON")
|
||||
if (ans == "ON") {
|
||||
@ -65,7 +65,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
|
||||
|
||||
public suspend fun readChannelData(channel: Int): Double? {
|
||||
val answer: String? = talk("PR$channel?")
|
||||
error.invalidate()
|
||||
invalidate(error)
|
||||
return if (answer.isNullOrEmpty()) {
|
||||
// updateState(PortSensor.CONNECTED_STATE, false)
|
||||
updateLogical(error, "No connection")
|
||||
@ -94,7 +94,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
|
||||
val channel by logicalProperty(MetaConverter.int)
|
||||
|
||||
val value by doubleProperty(read = {
|
||||
readChannelData(channel.get() ?: DEFAULT_CHANNEL)
|
||||
readChannelData(get(channel) ?: DEFAULT_CHANNEL)
|
||||
})
|
||||
|
||||
val error by logicalProperty(MetaConverter.string)
|
||||
|
4
demo/motors/README.md
Normal file
4
demo/motors/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module motors
|
||||
|
||||
|
||||
|
@ -16,8 +16,9 @@ import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.mi
|
||||
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.position
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
import space.kscience.controls.manager.installing
|
||||
import space.kscience.controls.spec.read
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.fetch
|
||||
import space.kscience.dataforge.context.request
|
||||
import tornadofx.*
|
||||
|
||||
class PiMotionMasterApp : App(PiMotionMasterView::class)
|
||||
@ -29,7 +30,7 @@ class PiMotionMasterController : Controller() {
|
||||
}
|
||||
|
||||
//initialize deviceManager plugin
|
||||
val deviceManager: DeviceManager = context.fetch(DeviceManager)
|
||||
val deviceManager: DeviceManager = context.request(DeviceManager)
|
||||
|
||||
// install device
|
||||
val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice)
|
||||
@ -44,10 +45,10 @@ fun VBox.piMotionMasterAxis(
|
||||
label(axisName)
|
||||
coroutineScope.launch {
|
||||
with(axis) {
|
||||
val min: Double = minPosition.read()
|
||||
val max: Double = maxPosition.read()
|
||||
val min: Double = read(minPosition)
|
||||
val max: Double = read(maxPosition)
|
||||
val positionProperty = fxProperty(position)
|
||||
val startPosition = position.read()
|
||||
val startPosition = read(position)
|
||||
runLater {
|
||||
vbox {
|
||||
hgrow = Priority.ALWAYS
|
||||
|
@ -39,7 +39,7 @@ class PiMotionMasterDevice(
|
||||
|
||||
fun disconnect() {
|
||||
runBlocking {
|
||||
disconnect.invoke()
|
||||
execute(disconnect)
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ class PiMotionMasterDevice(
|
||||
|
||||
fun connect(host: String, port: Int) {
|
||||
runBlocking {
|
||||
connect(Meta {
|
||||
execute(connect, Meta {
|
||||
"host" put host
|
||||
"port" put port
|
||||
})
|
||||
@ -176,7 +176,7 @@ class PiMotionMasterDevice(
|
||||
// connector.open()
|
||||
//Initialize axes
|
||||
if (portSpec != null) {
|
||||
val idn = identity.read()
|
||||
val idn = read(identity)
|
||||
failIfError { "Can't connect to $portSpec. Error code: $it" }
|
||||
logger.info { "Connected to $idn on $portSpec" }
|
||||
val ids = request("SAI?").map { it.trim() }
|
||||
@ -185,7 +185,7 @@ class PiMotionMasterDevice(
|
||||
axes = ids.associateWith { Axis(this, it) }
|
||||
}
|
||||
Meta(ids.map { it.asValue() }.asValue())
|
||||
initialize()
|
||||
execute(initialize)
|
||||
failIfError()
|
||||
}
|
||||
null
|
||||
@ -195,7 +195,7 @@ class PiMotionMasterDevice(
|
||||
info = "Disconnect the program from the device if it is connected"
|
||||
}) {
|
||||
port?.let{
|
||||
stop()
|
||||
execute(stop)
|
||||
it.close()
|
||||
}
|
||||
port = null
|
||||
@ -237,7 +237,7 @@ class PiMotionMasterDevice(
|
||||
}
|
||||
|
||||
suspend fun move(target: Double) {
|
||||
move(target.asMeta())
|
||||
execute(move, target.asMeta())
|
||||
}
|
||||
|
||||
companion object : DeviceSpec<Axis>() {
|
||||
@ -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
|
||||
|
@ -59,7 +59,7 @@ fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>):
|
||||
|
||||
onChange { newValue ->
|
||||
if (newValue != null) {
|
||||
write(spec, newValue)
|
||||
set(spec, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
147
docs/Device and DeviceSpec.md
Normal file
147
docs/Device and DeviceSpec.md
Normal file
@ -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<IDemoDevice>(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<IDemoDevice>(), Factory<DemoDevice> {
|
||||
|
||||
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.
|
||||
|
30
docs/templates/ARTIFACT-TEMPLATE.md
vendored
Normal file
30
docs/templates/ARTIFACT-TEMPLATE.md
vendored
Normal file
@ -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}")
|
||||
}
|
||||
```
|
52
docs/templates/README-TEMPLATE.md
vendored
Normal file
52
docs/templates/README-TEMPLATE.md
vendored
Normal file
@ -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.
|
@ -7,4 +7,4 @@ org.gradle.parallel=true
|
||||
publishing.github=false
|
||||
publishing.sonatype=false
|
||||
|
||||
toolsVersion=0.14.3-kotlin-1.8.10
|
||||
toolsVersion=0.14.6-kotlin-1.8.20
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
4
magix/README.md
Normal file
4
magix/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module magix
|
||||
|
||||
|
||||
|
32
magix/magix-api/README.md
Normal file
32
magix/magix-api/README.md
Normal file
@ -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")
|
||||
}
|
||||
```
|
@ -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
|
||||
|
@ -10,6 +10,12 @@ public data class MagixMessageFilter(
|
||||
val origin: Collection<String?>? = null,
|
||||
val target: Collection<String?>? = 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<MagixMessage>.filter(filter: MagixMessageFilter): Flow<MagixMess
|
||||
if (filter == MagixMessageFilter.ALL) {
|
||||
return this
|
||||
}
|
||||
return filter { message ->
|
||||
filter.format?.contains(message.format) ?: true
|
||||
&& filter.origin?.contains(message.origin) ?: true
|
||||
&& filter.target?.contains(message.target) ?: true
|
||||
}
|
||||
return filter(filter::accepts)
|
||||
}
|
32
magix/magix-java-client/README.md
Normal file
32
magix/magix-java-client/README.md
Normal file
@ -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")
|
||||
}
|
||||
```
|
32
magix/magix-mqtt/README.md
Normal file
32
magix/magix-mqtt/README.md
Normal file
@ -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")
|
||||
}
|
||||
```
|
18
magix/magix-mqtt/build.gradle.kts
Normal file
18
magix/magix-mqtt/build.gradle.kts
Normal file
@ -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
|
||||
}
|
@ -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<MagixMessage> = 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"
|
||||
}
|
||||
}
|
32
magix/magix-rabbit/README.md
Normal file
32
magix/magix-rabbit/README.md
Normal file
@ -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")
|
||||
}
|
||||
```
|
32
magix/magix-rsocket/README.md
Normal file
32
magix/magix-rsocket/README.md
Normal file
@ -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")
|
||||
}
|
||||
```
|
@ -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<MagixMessage> {
|
||||
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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
32
magix/magix-server/README.md
Normal file
32
magix/magix-server/README.md
Normal file
@ -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")
|
||||
}
|
||||
```
|
@ -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<Payload> ->
|
||||
input.onEach {
|
||||
magixFlow.emit(MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), it.data.readText()))
|
||||
|
4
magix/magix-storage/README.md
Normal file
4
magix/magix-storage/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module magix-storage
|
||||
|
||||
|
||||
|
32
magix/magix-storage/magix-storage-xodus/README.md
Normal file
32
magix/magix-storage/magix-storage-xodus/README.md
Normal file
@ -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")
|
||||
}
|
||||
```
|
@ -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{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user