Обновление зависимостей #10

Merged
altavir merged 11 commits from support/update_dependencies into dev 2024-04-29 18:29:58 +03:00
104 changed files with 2233 additions and 512 deletions
Showing only changes of commit 64e240f6c2 - Show all commits

24
.github/workflows/build.yml vendored Normal file
View 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

View File

@ -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
View 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
View 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
View 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\"",
)
}
}
}

167
README.md
View File

@ -7,9 +7,7 @@ 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)
@ -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)

View File

@ -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")

View File

@ -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()
}
}

View File

@ -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) {

View File

@ -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,

View File

@ -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
}
}
}

View File

@ -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> {

View File

@ -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()

View File

@ -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()
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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)

View File

@ -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>,

View File

@ -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()
}
}
}

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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) }

View File

@ -0,0 +1,4 @@
# Module controls-ktor-tcp

View File

@ -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()
}

View 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")
}
```

View File

@ -3,6 +3,10 @@ plugins {
`maven-publish`
}
description = """
Magix service for binding controls devices (both as RPC client and server
""".trimIndent()
kscience {
jvm()
js()
@ -10,7 +14,12 @@ kscience{
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 {
}

View File

@ -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())
}
}

View File

@ -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)
}

View File

@ -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,

View File

@ -0,0 +1,4 @@
# Module controls-modbus
A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols

View 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")
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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
View File

@ -0,0 +1,4 @@
# Module controls-opcua

View File

@ -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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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(),

View File

@ -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
View 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")
}
```

View File

@ -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
View 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")
}
```

View File

@ -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")

View File

@ -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 ?: "")

View File

@ -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")
}
```

View 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")
}
```

View File

@ -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{

View File

@ -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"

View File

@ -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
View File

@ -0,0 +1,4 @@
# Module demo

View File

@ -0,0 +1,4 @@
# Module all-things

View File

@ -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")
}
}

View File

@ -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()

View File

@ -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)
}
}
}
}

View File

@ -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
View File

@ -0,0 +1,4 @@
# Module car

View File

@ -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")
}
}

View File

@ -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))
}
}
}

View File

@ -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)
}
}

View File

@ -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
View File

@ -0,0 +1,4 @@
# Module echo

View File

@ -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")
}
}

View File

@ -0,0 +1,4 @@
# Module magix-demo

View File

@ -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{

View File

@ -0,0 +1,4 @@
# Module many-devices

View 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")
}

View File

@ -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()) {
}
}

View File

@ -0,0 +1,4 @@
# Module mks-pdr900

View File

@ -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
View File

@ -0,0 +1,4 @@
# Module motors

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}

View 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
View 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
View 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.

View File

@ -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

View File

@ -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
View File

@ -0,0 +1,4 @@
# Module magix

32
magix/magix-api/README.md Normal file
View 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")
}
```

View File

@ -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

View File

@ -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)
}

View 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")
}
```

View 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")
}
```

View 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
}

View File

@ -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"
}
}

View 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")
}
```

View 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")
}
```

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View 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")
}
```

View File

@ -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()))

View File

@ -0,0 +1,4 @@
# Module magix-storage

View 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")
}
```

View File

@ -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