Documentation update

This commit is contained in:
Alexander Nozik 2023-05-03 11:05:54 +03:00
parent e7cfb1d2ba
commit 8cd191bb0d
54 changed files with 1136 additions and 81 deletions

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

3
.space.kts Normal file
View File

@ -0,0 +1,3 @@
job("Build and run tests") {
gradlew("amazoncorretto:17-alpine", "build")
}

178
README.md
View File

@ -3,58 +3,182 @@
# Controls.kt
Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
This repository contains a prototype of API and simple implementation
This repository contains a prototype of API and simple implementation
of a slow control system, including a demo.
Controls.kt uses some concepts and modules of DataForge,
such as `Meta` (immutable tree-like structure) and `Meta` (which
includes a scalar value, or a tree of values, easily convertable to/from JSON
if needed).
Controls.kt uses some concepts and modules of DataForge,
such as `Meta` (tree-like value structure).
To learn more about DataForge, please consult the following URLs:
* [Kotlin multiplatform implementation of DataForge](https://github.com/mipt-npm/dataforge-core)
* [DataForge documentation](http://npm.mipt.ru/dataforge/)
* [Original implementation of DataForge](https://bitbucket.org/Altavir/dataforge/src/default/)
* [Kotlin multiplatform implementation of DataForge](https://github.com/mipt-npm/dataforge-core)
* [DataForge documentation](http://npm.mipt.ru/dataforge/)
* [Original implementation of DataForge](https://bitbucket.org/Altavir/dataforge/src/default/)
DataForge-control is a [Kotlin-multiplatform](https://kotlinlang.org/docs/reference/multiplatform.html)
application. Asynchronous operations are implemented with
application. Asynchronous operations are implemented with
[kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) library.
## Materials and publications
* Video - [A general overview seminar](https://youtu.be/LO-qjWgXMWc)
* Video - [A seminar about the system mechanics](https://youtu.be/wES0RV5GpoQ)
* Article - [A Novel Solution for Controlling Hardware Components of Accelerators and Beamlines](https://www.preprints.org/manuscript/202108.0336/v1)
* Article - [A Novel Solution for Controlling Hardware Components of Accelerators and Beamlines](https://www.preprints.org/manuscript/202108.0336/v1)
### Features
Among other things, you can:
- Describe devices and their properties.
- Describe devices and their properties.
- Collect data from devices and execute arbitrary actions supported by a device.
- Property values can be cached in the system and requested from devices as needed, asynchronously.
- Connect devices to event bus via bidirectional message flows.
### `dataforge-control-core` module packages
Example view of a demo:
- `api` - defines API for device management. The main class here is
[`Device`](controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt).
Generally, a Device has Properties that can be read and written. Also, some Actions
can optionally be applied on a device (may or may not affect properties).
![](docs/pictures/demo-view.png)
- `base` - contains baseline `Device` implementation
[`DeviceBase`](controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt)
and property implementation, including property asynchronous flows.
## 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/mks-pdr900](demo/mks-pdr900)
>
>
> **Maturity**: EXPERIMENTAL
### [demo/motors](demo/motors)
>
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-api](magix/magix-api)
>
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-java-client](magix/magix-java-client)
>
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-mqtt](magix/magix-mqtt)
>
>
> **Maturity**: PROTOTYPE
### [magix/magix-rabbit](magix/magix-rabbit)
>
>
> **Maturity**: PROTOTYPE
### [magix/magix-rsocket](magix/magix-rsocket)
>
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-server](magix/magix-server)
>
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-storage](magix/magix-storage)
>
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-zmq](magix/magix-zmq)
>
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-storage/magix-storage-xodus](magix/magix-storage/magix-storage-xodus)
>
>
> **Maturity**: PROTOTYPE
- `controllers` - implements Message Controller that can be attached to the event bus, Message
and Property flows.
### `demo` module
The demo includes a simple mock device with a few properties changing as `sin` and `cos` of
the current time. The device is configurable via a simple TornadoFX-based control panel.
You can run a demo by executing `application/run` Gradle task.
the current time. The device is configurable via a simple TornadoFX-based control panel.
You can run a demo by executing `application/run` Gradle task.
The graphs are displayed using [plotly.kt](https://github.com/mipt-npm/plotly.kt) library.
Example view of a demo:
![](docs/pictures/demo-view.png)

View File

@ -35,6 +35,8 @@ ksciencePublish {
space("https://maven.pkg.jetbrains.space/spc/p/controls/maven")
}
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
apiValidation {
validationDisabled = true
}

View File

@ -18,3 +18,28 @@ kscience {
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
@ -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

@ -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
@ -12,6 +13,9 @@ 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
@ -36,6 +40,9 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
}
/**
* 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 +52,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 +62,15 @@ public inline fun <D : Device> DeviceManager.installing(
val meta = Meta(builder)
return ReadOnlyProperty { _, property ->
val name = property.name
install(name, factory, meta)
val current = getOrNull(name)
if (current == null) {
install(name, factory, meta)
} else if (current.meta != meta) {
error("Meta mismatch. Current device meta: ${current.meta}, but factory meta is $meta")
} else {
@Suppress("UNCHECKED_CAST")
current as D
}
}
}

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 -> {
@ -66,6 +69,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 +83,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

@ -5,6 +5,9 @@ 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 +18,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 }

View File

@ -14,6 +14,9 @@ import space.kscience.dataforge.meta.Meta
import kotlin.coroutines.CoroutineContext
/**
* A base abstractions for [Device], introducing specifications for properties
*/
@OptIn(InternalDeviceAPI::class)
public abstract class DeviceBase<D : DeviceBase<D>>(
override val context: Context = Global,

View File

@ -27,7 +27,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>
@ -78,7 +78,7 @@ 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

View File

@ -12,6 +12,8 @@ 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

View File

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

View File

@ -0,0 +1,32 @@
# Module controls-magix-client
## 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

@ -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,45 @@
package space.kscience.controls.modbus
import com.ghgande.j2mod.modbus.facade.AbstractModbusMaster
import space.kscience.controls.api.DeviceHub
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DeviceSpec
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.NameToken
/**
* A variant of [DeviceBySpec] that includes Modbus RTU/TCP/UDP client
*/
public open class ModbusDeviceBySpec(
context: Context,
spec: DeviceSpec<ModbusDeviceBySpec>,
override val clientId: Int,
override val master: AbstractModbusMaster,
meta: Meta = Meta.EMPTY,
) : ModbusDevice, DeviceBySpec<ModbusDeviceBySpec>(spec, context, meta)
public class ModbusHub(
public val context: Context,
public val masterBuilder: () -> AbstractModbusMaster,
public val specs: Map<NameToken, Pair<Int, DeviceSpec<ModbusDeviceBySpec>>>,
) : 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

@ -8,10 +8,7 @@ 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.Scheme
import space.kscience.dataforge.meta.SchemeSpec
import space.kscience.dataforge.meta.specOrNull
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.meta.*
public sealed class MiloIdentity: Scheme()
@ -35,6 +32,8 @@ public class MiloConfiguration : Scheme() {
public var username: MiloUsername? by specOrNull(MiloUsername)
public var securityPolicy: SecurityPolicy by enum(SecurityPolicy.None)
public companion object : SchemeSpec<MiloConfiguration>(::MiloConfiguration)
}
@ -50,7 +49,7 @@ public open class MiloDeviceBySpec<D : MiloDeviceBySpec<D>>(
override val client: OpcUaClient by lazy {
context.createMiloClient(
config.endpointUrl,
securityPolicy = SecurityPolicy.None,
securityPolicy = config.securityPolicy,
identityProvider = config.username?.let {
UsernameProvider(it.username,it.password)
} ?: AnonymousProvider()

View File

@ -4,7 +4,6 @@ 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.opcua.client.OpcUaClientTest.DemoMiloDevice.Companion.randomDouble
import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.doubleProperty
import space.kscience.dataforge.meta.transformations.MetaConverter
@ -21,10 +20,6 @@ class OpcUaClientTest {
fun build(): DemoMiloDevice {
val config = MiloConfiguration {
endpointUrl = "opc.tcp://milo.digitalpetri.com:62541/milo"
// username = MiloUsername{
// username = "user1"
// password = "password"
// }
}
return DemoMiloDevice(config)
}
@ -41,7 +36,7 @@ class OpcUaClientTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun testReadDouble() = runTest {
println(DemoMiloDevice.use { randomDouble.read() })
println(DemoMiloDevice.use { DemoMiloDevice.randomDouble.read() })
}
}

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

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

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

@ -16,7 +16,7 @@ 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.meta.Meta
import space.kscience.dataforge.meta.get
@ -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

@ -8,9 +8,6 @@ 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() {

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

4
demo/car/README.md Normal file
View File

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

4
demo/echo/README.md Normal file
View File

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

View File

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

View File

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

4
demo/motors/README.md Normal file
View File

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

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

48
docs/templates/README-TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,48 @@
[![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)
## 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

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-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

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

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

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

32
magix/magix-zmq/README.md Normal file
View File

@ -0,0 +1,32 @@
# Module magix-zmq
ZMQ client endpoint for Magix
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:magix-zmq:0.1.1-SNAPSHOT`.
**Gradle Groovy:**
```groovy
repositories {
maven { url 'https://repo.kotlin.link' }
mavenCentral()
}
dependencies {
implementation 'space.kscience:magix-zmq:0.1.1-SNAPSHOT'
}
```
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
mavenCentral()
}
dependencies {
implementation("space.kscience:magix-zmq:0.1.1-SNAPSHOT")
}
```

View File

@ -45,6 +45,7 @@ include(
":controls-serial",
":controls-server",
":controls-opcua",
":controls-modbus",
// ":controls-mongo",
":controls-storage",
":controls-storage:controls-xodus",