Compare commits
No commits in common. "e729cb1a794db44955d68b2ec8f676e4b65736ff" and "23bceed89da647fd461dd74aa5ca243ee6346720" have entirely different histories.
e729cb1a79
...
23bceed89d
57
CHANGELOG.md
57
CHANGELOG.md
@ -3,53 +3,6 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Value averaging plot extension
|
|
||||||
- PLC4X bindings
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Constructor properties return `DeviceStat` in order to be able to subscribe to them
|
|
||||||
- Refactored ports. Now we have `AsynchronousPort` as well as `SynchronousPort`
|
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
## 0.3.0 - 2024-03-04
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Device lifecycle message
|
|
||||||
- Low-code constructor
|
|
||||||
- Automatic description generation for spec properties (JVM only)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Property caching moved from core `Device` to the `CachingDevice`
|
|
||||||
- `DeviceSpec` properties no explicitly pass property name to getters and setters.
|
|
||||||
- `DeviceHub.respondHubMessage` now returns a list of messages to allow querying multiple devices. Device server also returns an array.
|
|
||||||
- DataForge 0.8.0
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Property writing does not trigger change if logical state already is the same as value to be set.
|
|
||||||
- Modbus-slave triggers only once for multi-register write.
|
|
||||||
- Removed unnecessary scope in hub messageFlow
|
|
||||||
|
|
||||||
## 0.2.2-dev-1 - 2023-09-24
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- updating logical state in `DeviceBase` is now protected and called `propertyChanged()`
|
|
||||||
- `DeviceBase` tries to read property after write if the writer does not set the value.
|
|
||||||
|
|
||||||
## 0.2.1 - 2023-09-24
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Core interfaces for building a device server
|
- Core interfaces for building a device server
|
||||||
- Magix service for binding controls devices (both as RPC client and server)
|
- Magix service for binding controls devices (both as RPC client and server)
|
||||||
- A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
|
- A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
|
||||||
@ -67,3 +20,13 @@
|
|||||||
- A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
- A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
||||||
- Magix history database API
|
- Magix history database API
|
||||||
- ZMQ client endpoint for Magix
|
- ZMQ client endpoint for Magix
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
### Security
|
||||||
|
20
README.md
20
README.md
@ -1,7 +1,5 @@
|
|||||||
[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
|
[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
|
||||||
|
|
||||||
[![](https://maven.sciprog.center/api/badge/latest/kscience/space/kscience/controls-core-jvm?color=40c14a&name=repo.kotlin.link&prefix=v)](https://maven.sciprog.center/)
|
|
||||||
|
|
||||||
# Controls.kt
|
# Controls.kt
|
||||||
|
|
||||||
Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
|
Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
|
||||||
@ -44,11 +42,6 @@ Example view of a demo:
|
|||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
|
|
||||||
### [controls-constructor](controls-constructor)
|
|
||||||
> A low-code constructor for composite devices simulation
|
|
||||||
>
|
|
||||||
> **Maturity**: PROTOTYPE
|
|
||||||
|
|
||||||
### [controls-core](controls-core)
|
### [controls-core](controls-core)
|
||||||
> Core interfaces for building a device server
|
> Core interfaces for building a device server
|
||||||
>
|
>
|
||||||
@ -63,10 +56,6 @@ Example view of a demo:
|
|||||||
> - [ports](controls-core/src/commonMain/kotlin/space/kscience/controls/ports) : Working with asynchronous data sending and receiving raw byte arrays
|
> - [ports](controls-core/src/commonMain/kotlin/space/kscience/controls/ports) : Working with asynchronous data sending and receiving raw byte arrays
|
||||||
|
|
||||||
|
|
||||||
### [controls-jupyter](controls-jupyter)
|
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
|
||||||
|
|
||||||
### [controls-magix](controls-magix)
|
### [controls-magix](controls-magix)
|
||||||
> Magix service for binding controls devices (both as RPC client and server)
|
> Magix service for binding controls devices (both as RPC client and server)
|
||||||
>
|
>
|
||||||
@ -124,11 +113,6 @@ Automatically checks consistency.
|
|||||||
>
|
>
|
||||||
> **Maturity**: PROTOTYPE
|
> **Maturity**: PROTOTYPE
|
||||||
|
|
||||||
### [controls-vision](controls-vision)
|
|
||||||
> Dashboard and visualization extensions for devices
|
|
||||||
>
|
|
||||||
> **Maturity**: PROTOTYPE
|
|
||||||
|
|
||||||
### [demo](demo)
|
### [demo](demo)
|
||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
@ -150,10 +134,6 @@ Automatically checks consistency.
|
|||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [demo/constructor](demo/constructor)
|
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
|
||||||
|
|
||||||
### [demo/echo](demo/echo)
|
### [demo/echo](demo/echo)
|
||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import space.kscience.gradle.isInDevelopment
|
||||||
import space.kscience.gradle.useApache2Licence
|
import space.kscience.gradle.useApache2Licence
|
||||||
import space.kscience.gradle.useSPCTeam
|
import space.kscience.gradle.useSPCTeam
|
||||||
|
|
||||||
@ -8,19 +9,25 @@ plugins {
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "space.kscience"
|
group = "space.kscience"
|
||||||
version = "0.3.1-dev-1"
|
version = "0.2.0"
|
||||||
repositories{
|
repositories{
|
||||||
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ksciencePublish {
|
ksciencePublish {
|
||||||
pom("https://github.com/SciProgCentre/controls-kt") {
|
pom("https://github.com/SciProgCentre/controls.kt") {
|
||||||
useApache2Licence()
|
useApache2Licence()
|
||||||
useSPCTeam()
|
useSPCTeam()
|
||||||
}
|
}
|
||||||
repository("spc","https://maven.sciprog.center/kscience")
|
github("controls.kt", "SciProgCentre")
|
||||||
sonatype("https://oss.sonatype.org")
|
space(
|
||||||
|
if (isInDevelopment) {
|
||||||
|
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
|
||||||
|
} else {
|
||||||
|
"https://maven.pkg.jetbrains.space/spc/p/sci/maven"
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
@ -1,21 +0,0 @@
|
|||||||
# Module controls-constructor
|
|
||||||
|
|
||||||
A low-code constructor for composite devices simulation
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
## Artifact:
|
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-constructor:0.3.0`.
|
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
|
||||||
```kotlin
|
|
||||||
repositories {
|
|
||||||
maven("https://repo.kotlin.link")
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("space.kscience:controls-constructor:0.3.0")
|
|
||||||
}
|
|
||||||
```
|
|
@ -1,20 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("space.kscience.gradle.mpp")
|
|
||||||
`maven-publish`
|
|
||||||
}
|
|
||||||
|
|
||||||
description = """
|
|
||||||
A low-code constructor for composite devices simulation
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
kscience{
|
|
||||||
jvm()
|
|
||||||
js()
|
|
||||||
dependencies {
|
|
||||||
api(projects.controlsCore)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readme{
|
|
||||||
maturity = space.kscience.gradle.Maturity.PROTOTYPE
|
|
||||||
}
|
|
@ -1,118 +0,0 @@
|
|||||||
package space.kscience.controls.constructor
|
|
||||||
|
|
||||||
import space.kscience.controls.api.Device
|
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
import space.kscience.dataforge.names.Name
|
|
||||||
import space.kscience.dataforge.names.asName
|
|
||||||
import kotlin.properties.PropertyDelegateProvider
|
|
||||||
import kotlin.properties.ReadOnlyProperty
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
import kotlin.time.Duration
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices
|
|
||||||
*/
|
|
||||||
public abstract class DeviceConstructor(
|
|
||||||
context: Context,
|
|
||||||
meta: Meta,
|
|
||||||
) : DeviceGroup(context, meta) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a device, provided by a given [factory] and
|
|
||||||
*/
|
|
||||||
public fun <D : Device> device(
|
|
||||||
factory: Factory<D>,
|
|
||||||
meta: Meta? = null,
|
|
||||||
nameOverride: Name? = null,
|
|
||||||
metaLocation: Name? = null,
|
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
|
||||||
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
|
||||||
val name = nameOverride ?: property.name.asName()
|
|
||||||
val device = install(name, factory, meta, metaLocation ?: name)
|
|
||||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
|
||||||
device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <D : Device> device(
|
|
||||||
device: D,
|
|
||||||
nameOverride: Name? = null,
|
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
|
||||||
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
|
||||||
val name = nameOverride ?: property.name.asName()
|
|
||||||
install(name, device)
|
|
||||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
|
||||||
device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a property and provide a direct reader for it
|
|
||||||
*/
|
|
||||||
public fun <T, S: DeviceState<T>> property(
|
|
||||||
state: S,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
nameOverride: String? = null,
|
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> =
|
|
||||||
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
|
||||||
val name = nameOverride ?: property.name
|
|
||||||
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
|
||||||
registerProperty(descriptor, state)
|
|
||||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register external state as a property
|
|
||||||
*/
|
|
||||||
public fun <T : Any> property(
|
|
||||||
metaConverter: MetaConverter<T>,
|
|
||||||
reader: suspend () -> T,
|
|
||||||
readInterval: Duration,
|
|
||||||
initialState: T,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
nameOverride: String? = null,
|
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
|
|
||||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
|
|
||||||
descriptorBuilder,
|
|
||||||
nameOverride,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a mutable external state as a property
|
|
||||||
*/
|
|
||||||
public fun <T : Any> mutableProperty(
|
|
||||||
metaConverter: MetaConverter<T>,
|
|
||||||
reader: suspend () -> T,
|
|
||||||
writer: suspend (T) -> Unit,
|
|
||||||
readInterval: Duration,
|
|
||||||
initialState: T,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
nameOverride: String? = null,
|
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
|
||||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
|
|
||||||
descriptorBuilder,
|
|
||||||
nameOverride,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and register a virtual mutable property with optional [callback]
|
|
||||||
*/
|
|
||||||
public fun <T> virtualProperty(
|
|
||||||
metaConverter: MetaConverter<T>,
|
|
||||||
initialState: T,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
nameOverride: String? = null,
|
|
||||||
callback: (T) -> Unit = {},
|
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
|
||||||
DeviceState.virtual(metaConverter, initialState, callback),
|
|
||||||
descriptorBuilder,
|
|
||||||
nameOverride,
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,298 +0,0 @@
|
|||||||
package space.kscience.controls.constructor
|
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import space.kscience.controls.api.*
|
|
||||||
import space.kscience.controls.api.DeviceLifecycleState.*
|
|
||||||
import space.kscience.controls.manager.DeviceManager
|
|
||||||
import space.kscience.controls.manager.install
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.context.request
|
|
||||||
import space.kscience.dataforge.meta.*
|
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
|
||||||
import space.kscience.dataforge.names.*
|
|
||||||
import kotlin.collections.set
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A mutable group of devices and properties to be used for lightweight design and simulations.
|
|
||||||
*/
|
|
||||||
public open class DeviceGroup(
|
|
||||||
final override val context: Context,
|
|
||||||
override val meta: Meta,
|
|
||||||
) : DeviceHub, CachingDevice {
|
|
||||||
|
|
||||||
internal class Property(
|
|
||||||
val state: DeviceState<*>,
|
|
||||||
val descriptor: PropertyDescriptor,
|
|
||||||
)
|
|
||||||
|
|
||||||
internal class Action(
|
|
||||||
val invoke: suspend (Meta?) -> Meta?,
|
|
||||||
val descriptor: ActionDescriptor,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
|
|
||||||
|
|
||||||
override val messageFlow: Flow<DeviceMessage>
|
|
||||||
get() = sharedMessageFlow
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
override val coroutineContext: CoroutineContext = context.newCoroutineContext(
|
|
||||||
SupervisorJob(context.coroutineContext[Job]) +
|
|
||||||
CoroutineName("Device $id") +
|
|
||||||
CoroutineExceptionHandler { _, throwable ->
|
|
||||||
context.launch {
|
|
||||||
sharedMessageFlow.emit(
|
|
||||||
DeviceErrorMessage(
|
|
||||||
errorMessage = throwable.message,
|
|
||||||
errorType = throwable::class.simpleName,
|
|
||||||
errorStackTrace = throwable.stackTraceToString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
private val _devices = hashMapOf<NameToken, Device>()
|
|
||||||
|
|
||||||
override val devices: Map<NameToken, Device> = _devices
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register and initialize (synchronize child's lifecycle state with group state) a new device in this group
|
|
||||||
*/
|
|
||||||
@OptIn(DFExperimental::class)
|
|
||||||
public fun <D : Device> install(token: NameToken, device: D): D {
|
|
||||||
require(_devices[token] == null) { "A child device with name $token already exists" }
|
|
||||||
//start the child device if needed
|
|
||||||
if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() }
|
|
||||||
_devices[token] = device
|
|
||||||
return device
|
|
||||||
}
|
|
||||||
|
|
||||||
private val properties: MutableMap<Name, Property> = hashMapOf()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a new property based on [DeviceState]. Properties could be modified dynamically
|
|
||||||
*/
|
|
||||||
public fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
|
||||||
val name = descriptor.name.parseAsName()
|
|
||||||
require(properties[name] == null) { "Can't add property with name $name. It already exists." }
|
|
||||||
properties[name] = Property(state, descriptor)
|
|
||||||
state.metaFlow.onEach {
|
|
||||||
sharedMessageFlow.emit(
|
|
||||||
PropertyChangedMessage(
|
|
||||||
descriptor.name,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}.launchIn(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val actions: MutableMap<Name, Action> = hashMapOf()
|
|
||||||
|
|
||||||
override val propertyDescriptors: Collection<PropertyDescriptor>
|
|
||||||
get() = properties.values.map { it.descriptor }
|
|
||||||
|
|
||||||
override val actionDescriptors: Collection<ActionDescriptor>
|
|
||||||
get() = actions.values.map { it.descriptor }
|
|
||||||
|
|
||||||
override suspend fun readProperty(propertyName: String): Meta =
|
|
||||||
properties[propertyName.parseAsName()]?.state?.valueAsMeta
|
|
||||||
?: error("Property with name $propertyName not found")
|
|
||||||
|
|
||||||
override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.state?.valueAsMeta
|
|
||||||
|
|
||||||
override suspend fun invalidate(propertyName: String) {
|
|
||||||
//does nothing for this implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun writeProperty(propertyName: String, value: Meta) {
|
|
||||||
val property = (properties[propertyName.parseAsName()]?.state as? MutableDeviceState)
|
|
||||||
?: error("Property with name $propertyName not found")
|
|
||||||
property.valueAsMeta = value
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override suspend fun execute(actionName: String, argument: Meta?): Meta? {
|
|
||||||
val action = actions[actionName] ?: error("Action with name $actionName not found")
|
|
||||||
return action.invoke(argument)
|
|
||||||
}
|
|
||||||
|
|
||||||
@DFExperimental
|
|
||||||
override var lifecycleState: DeviceLifecycleState = STOPPED
|
|
||||||
protected set(value) {
|
|
||||||
if (field != value) {
|
|
||||||
launch {
|
|
||||||
sharedMessageFlow.emit(
|
|
||||||
DeviceLifeCycleMessage(value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(DFExperimental::class)
|
|
||||||
override suspend fun start() {
|
|
||||||
lifecycleState = STARTING
|
|
||||||
super.start()
|
|
||||||
devices.values.forEach {
|
|
||||||
it.start()
|
|
||||||
}
|
|
||||||
lifecycleState = STARTED
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(DFExperimental::class)
|
|
||||||
override fun stop() {
|
|
||||||
devices.values.forEach {
|
|
||||||
it.stop()
|
|
||||||
}
|
|
||||||
super.stop()
|
|
||||||
lifecycleState = STOPPED
|
|
||||||
}
|
|
||||||
|
|
||||||
public companion object {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun DeviceManager.registerDeviceGroup(
|
|
||||||
name: String = "@group",
|
|
||||||
meta: Meta = Meta.EMPTY,
|
|
||||||
block: DeviceGroup.() -> Unit,
|
|
||||||
): DeviceGroup {
|
|
||||||
val group = DeviceGroup(context, meta).apply(block)
|
|
||||||
install(name, group)
|
|
||||||
return group
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun Context.registerDeviceGroup(
|
|
||||||
name: String = "@group",
|
|
||||||
meta: Meta = Meta.EMPTY,
|
|
||||||
block: DeviceGroup.() -> Unit,
|
|
||||||
): DeviceGroup = request(DeviceManager).registerDeviceGroup(name, meta, block)
|
|
||||||
|
|
||||||
private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup {
|
|
||||||
return when (name.length) {
|
|
||||||
0 -> this
|
|
||||||
1 -> {
|
|
||||||
val token = name.first()
|
|
||||||
when (val d = devices[token]) {
|
|
||||||
null -> install(
|
|
||||||
token,
|
|
||||||
DeviceGroup(context, meta[token] ?: Meta.EMPTY)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> (d as? DeviceGroup) ?: error("Device $name is not a DeviceGroup")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> getOrCreateGroup(name.first().asName()).getOrCreateGroup(name.cutFirst())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a device at given [name] path
|
|
||||||
*/
|
|
||||||
public fun <D : Device> DeviceGroup.install(name: Name, device: D): D {
|
|
||||||
return when (name.length) {
|
|
||||||
0 -> error("Can't use empty name for a child device")
|
|
||||||
1 -> install(name.first(), device)
|
|
||||||
else -> getOrCreateGroup(name.cutLast()).install(name.tokens.last(), device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <D : Device> DeviceGroup.install(name: String, device: D): D =
|
|
||||||
install(name.parseAsName(), device)
|
|
||||||
|
|
||||||
public fun <D : Device> DeviceGroup.install(device: D): D =
|
|
||||||
install(device.id, device)
|
|
||||||
|
|
||||||
public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error.
|
|
||||||
* @param name the name of the device in the group
|
|
||||||
* @param factory a factory used to create a device
|
|
||||||
* @param deviceMeta meta override for this specific device
|
|
||||||
* @param metaLocation location of the template meta in parent group meta
|
|
||||||
*/
|
|
||||||
public fun <D : Device> DeviceGroup.install(
|
|
||||||
name: Name,
|
|
||||||
factory: Factory<D>,
|
|
||||||
deviceMeta: Meta? = null,
|
|
||||||
metaLocation: Name = name,
|
|
||||||
): D {
|
|
||||||
val newDevice = factory.build(context, Laminate(deviceMeta, meta[metaLocation]))
|
|
||||||
install(name, newDevice)
|
|
||||||
return newDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <D : Device> DeviceGroup.install(
|
|
||||||
name: String,
|
|
||||||
factory: Factory<D>,
|
|
||||||
metaLocation: Name = name.parseAsName(),
|
|
||||||
metaBuilder: (MutableMeta.() -> Unit)? = null,
|
|
||||||
): D = install(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }, metaLocation)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create or edit a group with a given [name].
|
|
||||||
*/
|
|
||||||
public fun DeviceGroup.registerDeviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup =
|
|
||||||
getOrCreateGroup(name).apply(block)
|
|
||||||
|
|
||||||
public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup =
|
|
||||||
registerDeviceGroup(name.parseAsName(), block)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register read-only property based on [state]
|
|
||||||
*/
|
|
||||||
public fun <T : Any> DeviceGroup.registerProperty(
|
|
||||||
name: String,
|
|
||||||
state: DeviceState<T>,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
) {
|
|
||||||
registerProperty(
|
|
||||||
PropertyDescriptor(name).apply(descriptorBuilder),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a mutable property based on mutable [state]
|
|
||||||
*/
|
|
||||||
public fun <T : Any> DeviceGroup.registerMutableProperty(
|
|
||||||
name: String,
|
|
||||||
state: MutableDeviceState<T>,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
) {
|
|
||||||
registerProperty(
|
|
||||||
PropertyDescriptor(name).apply(descriptorBuilder),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new virtual mutable state and a property based on it.
|
|
||||||
* @return the mutable state used in property
|
|
||||||
*/
|
|
||||||
public fun <T : Any> DeviceGroup.registerVirtualProperty(
|
|
||||||
name: String,
|
|
||||||
initialValue: T,
|
|
||||||
converter: MetaConverter<T>,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
callback: (T) -> Unit = {},
|
|
||||||
): MutableDeviceState<T> {
|
|
||||||
val state = DeviceState.virtual<T>(converter, initialValue, callback)
|
|
||||||
registerMutableProperty(name, state, descriptorBuilder)
|
|
||||||
return state
|
|
||||||
}
|
|
@ -1,242 +0,0 @@
|
|||||||
package space.kscience.controls.constructor
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import space.kscience.controls.api.Device
|
|
||||||
import space.kscience.controls.api.PropertyChangedMessage
|
|
||||||
import space.kscience.controls.spec.DevicePropertySpec
|
|
||||||
import space.kscience.controls.spec.MutableDevicePropertySpec
|
|
||||||
import space.kscience.controls.spec.name
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
import kotlin.time.Duration
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An observable state of a device
|
|
||||||
*/
|
|
||||||
public interface DeviceState<T> {
|
|
||||||
public val converter: MetaConverter<T>
|
|
||||||
public val value: T
|
|
||||||
|
|
||||||
public val valueFlow: Flow<T>
|
|
||||||
|
|
||||||
public companion object
|
|
||||||
}
|
|
||||||
|
|
||||||
public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::convert)
|
|
||||||
|
|
||||||
public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.convert(value)
|
|
||||||
|
|
||||||
public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect values in a given [scope]
|
|
||||||
*/
|
|
||||||
public fun <T> DeviceState<T>.collectValuesIn(scope: CoroutineScope, block: suspend (T)->Unit): Job =
|
|
||||||
valueFlow.onEach(block).launchIn(scope)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A mutable state of a device
|
|
||||||
*/
|
|
||||||
public interface MutableDeviceState<T> : DeviceState<T> {
|
|
||||||
override var value: T
|
|
||||||
}
|
|
||||||
|
|
||||||
public operator fun <T> MutableDeviceState<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
public var <T> MutableDeviceState<T>.valueAsMeta: Meta
|
|
||||||
get() = converter.convert(value)
|
|
||||||
set(arg) {
|
|
||||||
value = converter.read(arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [MutableDeviceState] that does not correspond to a physical state
|
|
||||||
*
|
|
||||||
* @param callback a synchronous callback that could be used without a scope
|
|
||||||
*/
|
|
||||||
private class VirtualDeviceState<T>(
|
|
||||||
override val converter: MetaConverter<T>,
|
|
||||||
initialValue: T,
|
|
||||||
private val callback: (T) -> Unit = {},
|
|
||||||
) : MutableDeviceState<T> {
|
|
||||||
private val flow = MutableStateFlow(initialValue)
|
|
||||||
override val valueFlow: Flow<T> get() = flow
|
|
||||||
|
|
||||||
override var value: T
|
|
||||||
get() = flow.value
|
|
||||||
set(value) {
|
|
||||||
flow.value = value
|
|
||||||
callback(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [MutableDeviceState] that does not correspond to a physical state
|
|
||||||
*
|
|
||||||
* @param callback a synchronous callback that could be used without a scope
|
|
||||||
*/
|
|
||||||
public fun <T> DeviceState.Companion.virtual(
|
|
||||||
converter: MetaConverter<T>,
|
|
||||||
initialValue: T,
|
|
||||||
callback: (T) -> Unit = {},
|
|
||||||
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)
|
|
||||||
|
|
||||||
private class StateFlowAsState<T>(
|
|
||||||
override val converter: MetaConverter<T>,
|
|
||||||
val flow: MutableStateFlow<T>,
|
|
||||||
) : MutableDeviceState<T> {
|
|
||||||
override var value: T by flow::value
|
|
||||||
override val valueFlow: Flow<T> get() = flow
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> =
|
|
||||||
StateFlowAsState(converter, this)
|
|
||||||
|
|
||||||
|
|
||||||
private open class BoundDeviceState<T>(
|
|
||||||
override val converter: MetaConverter<T>,
|
|
||||||
val device: Device,
|
|
||||||
val propertyName: String,
|
|
||||||
initialValue: T,
|
|
||||||
) : DeviceState<T> {
|
|
||||||
|
|
||||||
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
|
|
||||||
it.property == propertyName
|
|
||||||
}.mapNotNull {
|
|
||||||
converter.read(it.value)
|
|
||||||
}.stateIn(device.context, SharingStarted.Eagerly, initialValue)
|
|
||||||
|
|
||||||
override val value: T get() = valueFlow.value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind a read-only [DeviceState] to a [Device] property
|
|
||||||
*/
|
|
||||||
public suspend fun <T> Device.propertyAsState(
|
|
||||||
propertyName: String,
|
|
||||||
metaConverter: MetaConverter<T>,
|
|
||||||
): DeviceState<T> {
|
|
||||||
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
|
|
||||||
return BoundDeviceState(metaConverter, this, propertyName, initialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun <D : Device, T> D.propertyAsState(
|
|
||||||
propertySpec: DevicePropertySpec<D, T>,
|
|
||||||
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
|
|
||||||
|
|
||||||
public fun <T, R> DeviceState<T>.map(
|
|
||||||
converter: MetaConverter<R>, mapper: (T) -> R,
|
|
||||||
): DeviceState<R> = object : DeviceState<R> {
|
|
||||||
override val converter: MetaConverter<R> = converter
|
|
||||||
override val value: R
|
|
||||||
get() = mapper(this@map.value)
|
|
||||||
|
|
||||||
override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MutableBoundDeviceState<T>(
|
|
||||||
converter: MetaConverter<T>,
|
|
||||||
device: Device,
|
|
||||||
propertyName: String,
|
|
||||||
initialValue: T,
|
|
||||||
) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> {
|
|
||||||
|
|
||||||
override var value: T
|
|
||||||
get() = valueFlow.value
|
|
||||||
set(newValue) {
|
|
||||||
device.launch {
|
|
||||||
device.writeProperty(propertyName, converter.convert(newValue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <T> Device.mutablePropertyAsState(
|
|
||||||
propertyName: String,
|
|
||||||
metaConverter: MetaConverter<T>,
|
|
||||||
initialValue: T,
|
|
||||||
): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
|
|
||||||
|
|
||||||
public suspend fun <T> Device.mutablePropertyAsState(
|
|
||||||
propertyName: String,
|
|
||||||
metaConverter: MetaConverter<T>,
|
|
||||||
): MutableDeviceState<T> {
|
|
||||||
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
|
|
||||||
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun <D : Device, T> D.mutablePropertyAsState(
|
|
||||||
propertySpec: MutableDevicePropertySpec<D, T>,
|
|
||||||
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
|
|
||||||
|
|
||||||
public fun <D : Device, T> D.mutablePropertyAsState(
|
|
||||||
propertySpec: MutableDevicePropertySpec<D, T>,
|
|
||||||
initialValue: T,
|
|
||||||
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)
|
|
||||||
|
|
||||||
|
|
||||||
private open class ExternalState<T>(
|
|
||||||
val scope: CoroutineScope,
|
|
||||||
override val converter: MetaConverter<T>,
|
|
||||||
val readInterval: Duration,
|
|
||||||
initialValue: T,
|
|
||||||
val reader: suspend () -> T,
|
|
||||||
) : DeviceState<T> {
|
|
||||||
|
|
||||||
protected val flow: StateFlow<T> = flow {
|
|
||||||
while (true) {
|
|
||||||
delay(readInterval)
|
|
||||||
emit(reader())
|
|
||||||
}
|
|
||||||
}.stateIn(scope, SharingStarted.Eagerly, initialValue)
|
|
||||||
|
|
||||||
override val value: T get() = flow.value
|
|
||||||
override val valueFlow: Flow<T> get() = flow
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [DeviceState] which is constructed by periodically reading external value
|
|
||||||
*/
|
|
||||||
public fun <T> DeviceState.Companion.external(
|
|
||||||
scope: CoroutineScope,
|
|
||||||
converter: MetaConverter<T>,
|
|
||||||
readInterval: Duration,
|
|
||||||
initialValue: T,
|
|
||||||
reader: suspend () -> T,
|
|
||||||
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader)
|
|
||||||
|
|
||||||
private class MutableExternalState<T>(
|
|
||||||
scope: CoroutineScope,
|
|
||||||
converter: MetaConverter<T>,
|
|
||||||
readInterval: Duration,
|
|
||||||
initialValue: T,
|
|
||||||
reader: suspend () -> T,
|
|
||||||
val writer: suspend (T) -> Unit,
|
|
||||||
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
|
|
||||||
override var value: T
|
|
||||||
get() = super.value
|
|
||||||
set(value) {
|
|
||||||
scope.launch {
|
|
||||||
writer(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [DeviceState] that regularly reads and caches an external value
|
|
||||||
*/
|
|
||||||
public fun <T> DeviceState.Companion.external(
|
|
||||||
scope: CoroutineScope,
|
|
||||||
converter: MetaConverter<T>,
|
|
||||||
readInterval: Duration,
|
|
||||||
initialValue: T,
|
|
||||||
reader: suspend () -> T,
|
|
||||||
writer: suspend (T) -> Unit,
|
|
||||||
): MutableDeviceState<T> = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer)
|
|
@ -1,99 +0,0 @@
|
|||||||
package space.kscience.controls.constructor
|
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import space.kscience.controls.api.Device
|
|
||||||
import space.kscience.controls.manager.clock
|
|
||||||
import space.kscience.controls.spec.*
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
import space.kscience.dataforge.meta.double
|
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import kotlin.math.pow
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
import kotlin.time.DurationUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A classic drive regulated by force with encoder
|
|
||||||
*/
|
|
||||||
public interface Drive : Device {
|
|
||||||
/**
|
|
||||||
* Get or set drive force or momentum
|
|
||||||
*/
|
|
||||||
public var force: Double
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current position value
|
|
||||||
*/
|
|
||||||
public val position: Double
|
|
||||||
|
|
||||||
public companion object : DeviceSpec<Drive>() {
|
|
||||||
public val force: MutableDevicePropertySpec<Drive, Double> by Drive.mutableProperty(
|
|
||||||
MetaConverter.double,
|
|
||||||
Drive::force
|
|
||||||
)
|
|
||||||
|
|
||||||
public val position: DevicePropertySpec<Drive, Double> by doubleProperty { position }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A virtual drive
|
|
||||||
*/
|
|
||||||
public class VirtualDrive(
|
|
||||||
context: Context,
|
|
||||||
private val mass: Double,
|
|
||||||
public val positionState: MutableDeviceState<Double>,
|
|
||||||
) : Drive, DeviceBySpec<Drive>(Drive, context) {
|
|
||||||
|
|
||||||
private val dt = meta["time.step"].double?.milliseconds ?: 1.milliseconds
|
|
||||||
private val clock = context.clock
|
|
||||||
|
|
||||||
override var force: Double = 0.0
|
|
||||||
|
|
||||||
override val position: Double get() = positionState.value
|
|
||||||
|
|
||||||
public var velocity: Double = 0.0
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var updateJob: Job? = null
|
|
||||||
|
|
||||||
override suspend fun onStart() {
|
|
||||||
updateJob = launch {
|
|
||||||
var lastTime = clock.now()
|
|
||||||
while (isActive) {
|
|
||||||
delay(dt)
|
|
||||||
val realTime = clock.now()
|
|
||||||
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
|
||||||
|
|
||||||
//set last time and value to new values
|
|
||||||
lastTime = realTime
|
|
||||||
|
|
||||||
// compute new value based on velocity and acceleration from the previous step
|
|
||||||
positionState.value += velocity * dtSeconds + force / mass * dtSeconds.pow(2) / 2
|
|
||||||
propertyChanged(Drive.position, positionState.value)
|
|
||||||
|
|
||||||
// compute new velocity based on acceleration on the previous step
|
|
||||||
velocity += force / mass * dtSeconds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
updateJob?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
public companion object {
|
|
||||||
public fun factory(
|
|
||||||
mass: Double,
|
|
||||||
positionState: MutableDeviceState<Double>,
|
|
||||||
): Factory<Drive> = Factory { context, _ ->
|
|
||||||
VirtualDrive(context, mass, positionState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = mutablePropertyAsState(Drive.force)
|
|
@ -1,44 +0,0 @@
|
|||||||
package space.kscience.controls.constructor
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import space.kscience.controls.api.Device
|
|
||||||
import space.kscience.controls.spec.DeviceBySpec
|
|
||||||
import space.kscience.controls.spec.DevicePropertySpec
|
|
||||||
import space.kscience.controls.spec.DeviceSpec
|
|
||||||
import space.kscience.controls.spec.booleanProperty
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A limit switch device
|
|
||||||
*/
|
|
||||||
public interface LimitSwitch : Device {
|
|
||||||
|
|
||||||
public val locked: Boolean
|
|
||||||
|
|
||||||
public companion object : DeviceSpec<LimitSwitch>() {
|
|
||||||
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked }
|
|
||||||
public fun factory(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ ->
|
|
||||||
VirtualLimitSwitch(context, lockedState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Virtual [LimitSwitch]
|
|
||||||
*/
|
|
||||||
public class VirtualLimitSwitch(
|
|
||||||
context: Context,
|
|
||||||
public val lockedState: DeviceState<Boolean>,
|
|
||||||
) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch {
|
|
||||||
|
|
||||||
init {
|
|
||||||
lockedState.valueFlow.onEach {
|
|
||||||
propertyChanged(LimitSwitch.locked, it)
|
|
||||||
}.launchIn(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val locked: Boolean get() = lockedState.value
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
package space.kscience.controls.constructor
|
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import space.kscience.controls.manager.clock
|
|
||||||
import space.kscience.controls.spec.DeviceBySpec
|
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
import kotlin.time.DurationUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pid regulator parameters
|
|
||||||
*/
|
|
||||||
public interface PidParameters {
|
|
||||||
public val kp: Double
|
|
||||||
public val ki: Double
|
|
||||||
public val kd: Double
|
|
||||||
public val timeStep: Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class PidParametersImpl(
|
|
||||||
override val kp: Double,
|
|
||||||
override val ki: Double,
|
|
||||||
override val kd: Double,
|
|
||||||
override val timeStep: Duration,
|
|
||||||
) : PidParameters
|
|
||||||
|
|
||||||
public fun PidParameters(kp: Double, ki: Double, kd: Double, timeStep: Duration = 1.milliseconds): PidParameters =
|
|
||||||
PidParametersImpl(kp, ki, kd, timeStep)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A drive with PID regulator
|
|
||||||
*/
|
|
||||||
public class PidRegulator(
|
|
||||||
public val drive: Drive,
|
|
||||||
public val pidParameters: PidParameters,
|
|
||||||
) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator {
|
|
||||||
|
|
||||||
private val clock = drive.context.clock
|
|
||||||
|
|
||||||
override var target: Double = drive.position
|
|
||||||
|
|
||||||
private var lastTime: Instant = clock.now()
|
|
||||||
private var lastPosition: Double = target
|
|
||||||
|
|
||||||
private var integral: Double = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
private var updateJob: Job? = null
|
|
||||||
private val mutex = Mutex()
|
|
||||||
|
|
||||||
|
|
||||||
override suspend fun onStart() {
|
|
||||||
drive.start()
|
|
||||||
updateJob = launch {
|
|
||||||
while (isActive) {
|
|
||||||
delay(pidParameters.timeStep)
|
|
||||||
mutex.withLock {
|
|
||||||
val realTime = clock.now()
|
|
||||||
val delta = target - position
|
|
||||||
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
|
||||||
integral += delta * dtSeconds
|
|
||||||
val derivative = (drive.position - lastPosition) / dtSeconds
|
|
||||||
|
|
||||||
//set last time and value to new values
|
|
||||||
lastTime = realTime
|
|
||||||
lastPosition = drive.position
|
|
||||||
|
|
||||||
drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative
|
|
||||||
propertyChanged(Regulator.position, drive.position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
updateJob?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val position: Double get() = drive.position
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun DeviceGroup.pid(
|
|
||||||
name: String,
|
|
||||||
drive: Drive,
|
|
||||||
pidParameters: PidParameters,
|
|
||||||
): PidRegulator = install(name, PidRegulator(drive, pidParameters))
|
|
@ -1,27 +0,0 @@
|
|||||||
package space.kscience.controls.constructor
|
|
||||||
|
|
||||||
import space.kscience.controls.api.Device
|
|
||||||
import space.kscience.controls.spec.*
|
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A regulator with target value and current position
|
|
||||||
*/
|
|
||||||
public interface Regulator : Device {
|
|
||||||
/**
|
|
||||||
* Get or set target value
|
|
||||||
*/
|
|
||||||
public var target: Double
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current position value
|
|
||||||
*/
|
|
||||||
public val position: Double
|
|
||||||
|
|
||||||
public companion object : DeviceSpec<Regulator>() {
|
|
||||||
public val target: MutableDevicePropertySpec<Regulator, Double> by mutableProperty(MetaConverter.double, Regulator::target)
|
|
||||||
|
|
||||||
public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { position }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
package space.kscience.controls.constructor
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A state describing a [Double] value in the [range]
|
|
||||||
*/
|
|
||||||
public class DoubleRangeState(
|
|
||||||
initialValue: Double,
|
|
||||||
public val range: ClosedFloatingPointRange<Double>,
|
|
||||||
) : MutableDeviceState<Double> {
|
|
||||||
|
|
||||||
init {
|
|
||||||
require(initialValue in range) { "Initial value should be in range" }
|
|
||||||
}
|
|
||||||
|
|
||||||
override val converter: MetaConverter<Double> = MetaConverter.double
|
|
||||||
|
|
||||||
private val _valueFlow = MutableStateFlow(initialValue)
|
|
||||||
|
|
||||||
override var value: Double
|
|
||||||
get() = _valueFlow.value
|
|
||||||
set(newValue) {
|
|
||||||
_valueFlow.value = newValue.coerceIn(range)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val valueFlow: StateFlow<Double> get() = _valueFlow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A state showing that the range is on its lower boundary
|
|
||||||
*/
|
|
||||||
public val atStartState: DeviceState<Boolean> = map(MetaConverter.boolean) { it <= range.start }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A state showing that the range is on its higher boundary
|
|
||||||
*/
|
|
||||||
public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
|
||||||
public fun DeviceGroup.rangeState(
|
|
||||||
initialValue: Double,
|
|
||||||
range: ClosedFloatingPointRange<Double>,
|
|
||||||
): DoubleRangeState = DoubleRangeState(initialValue, range)
|
|
@ -16,16 +16,18 @@ Core interfaces for building a device server
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-core:0.3.0`.
|
The Maven coordinates of this project are `space.kscience:controls-core:0.2.0`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
//uncomment to access development builds
|
||||||
|
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-core:0.3.0")
|
implementation("space.kscience:controls-core:0.2.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -9,6 +9,8 @@ description = """
|
|||||||
Core interfaces for building a device server
|
Core interfaces for building a device server
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
|
val dataforgeVersion: String by rootProject.extra
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
jvm()
|
jvm()
|
||||||
js()
|
js()
|
||||||
@ -18,14 +20,10 @@ kscience {
|
|||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
useContextReceivers()
|
useContextReceivers()
|
||||||
commonMain {
|
dependencies {
|
||||||
api(libs.dataforge.io)
|
api(libs.dataforge.io)
|
||||||
api(spclibs.kotlinx.datetime)
|
api(spclibs.kotlinx.datetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
jvmTest{
|
|
||||||
implementation(spclibs.logback.classic)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
package space.kscience.controls.api
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A generic bidirectional asynchronous sender/receiver object
|
|
||||||
*/
|
|
||||||
public interface AsynchronousSocket<T> : AutoCloseable {
|
|
||||||
/**
|
|
||||||
* Send an object to the socket
|
|
||||||
*/
|
|
||||||
public suspend fun send(data: T)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flow of objects received from socket
|
|
||||||
*/
|
|
||||||
public fun subscribe(): Flow<T>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start socket operation
|
|
||||||
*/
|
|
||||||
public fun open()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this socket is open
|
|
||||||
*/
|
|
||||||
public val isOpen: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect an input to this socket.
|
|
||||||
* Multiple inputs could be connected to the same [AsynchronousSocket].
|
|
||||||
*
|
|
||||||
* This method suspends indefinitely, so it should be started in a separate coroutine.
|
|
||||||
*/
|
|
||||||
public suspend fun <T> AsynchronousSocket<T>.sendFlow(flow: Flow<T>) {
|
|
||||||
flow.collect { send(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -3,44 +3,26 @@ package space.kscience.controls.api
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import space.kscience.dataforge.context.ContextAware
|
||||||
import space.kscience.dataforge.context.info
|
import space.kscience.dataforge.context.info
|
||||||
import space.kscience.dataforge.context.logger
|
import space.kscience.dataforge.context.logger
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.string
|
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
import space.kscience.dataforge.misc.DFExperimental
|
||||||
import space.kscience.dataforge.misc.DfType
|
import space.kscience.dataforge.misc.Type
|
||||||
import space.kscience.dataforge.names.parseAsName
|
import space.kscience.dataforge.names.Name
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A lifecycle state of a device
|
* A lifecycle state of a device
|
||||||
*/
|
*/
|
||||||
@Serializable
|
public enum class DeviceLifecycleState{
|
||||||
public enum class DeviceLifecycleState {
|
INIT,
|
||||||
|
OPEN,
|
||||||
/**
|
CLOSED
|
||||||
* Device is initializing
|
|
||||||
*/
|
|
||||||
STARTING,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Device is initialized and running
|
|
||||||
*/
|
|
||||||
STARTED,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Device is closed
|
|
||||||
*/
|
|
||||||
STOPPED,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The device encountered irrecoverable error
|
|
||||||
*/
|
|
||||||
ERROR
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,15 +30,14 @@ public enum class DeviceLifecycleState {
|
|||||||
* [Device] is a supervisor scope encompassing all operations on a device.
|
* [Device] is a supervisor scope encompassing all operations on a device.
|
||||||
* When canceled, cancels all running processes.
|
* When canceled, cancels all running processes.
|
||||||
*/
|
*/
|
||||||
@DfType(DEVICE_TARGET)
|
@Type(DEVICE_TARGET)
|
||||||
public interface Device : ContextAware, CoroutineScope {
|
public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial configuration meta for the device
|
* Initial configuration meta for the device
|
||||||
*/
|
*/
|
||||||
public val meta: Meta get() = Meta.EMPTY
|
public val meta: Meta get() = Meta.EMPTY
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of supported property descriptors
|
* List of supported property descriptors
|
||||||
*/
|
*/
|
||||||
@ -73,6 +54,18 @@ public interface Device : ContextAware, CoroutineScope {
|
|||||||
*/
|
*/
|
||||||
public suspend fun readProperty(propertyName: String): Meta
|
public suspend fun readProperty(propertyName: String): Meta
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the logical state of property or return null if it is invalid
|
||||||
|
*/
|
||||||
|
public fun getProperty(propertyName: String): Meta?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate property (set logical state to invalid)
|
||||||
|
*
|
||||||
|
* This message is suspended to provide lock-free local property changes (they require coroutine context).
|
||||||
|
*/
|
||||||
|
public suspend fun invalidate(propertyName: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set property [value] for a property with name [propertyName].
|
* Set property [value] for a property with name [propertyName].
|
||||||
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
|
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
|
||||||
@ -92,15 +85,14 @@ public interface Device : ContextAware, CoroutineScope {
|
|||||||
public suspend fun execute(actionName: 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.
|
* Initialize the device. This function suspends until the device is finished initialization
|
||||||
* Does nothing if the device is started or is starting
|
|
||||||
*/
|
*/
|
||||||
public suspend fun start(): Unit = Unit
|
public suspend fun open(): Unit = Unit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close and terminate the device. This function does not wait for the device to be closed.
|
* Close and terminate the device. This function does not wait for the device to be closed.
|
||||||
*/
|
*/
|
||||||
public fun stop() {
|
override fun close() {
|
||||||
logger.info { "Device $this is closed" }
|
logger.info { "Device $this is closed" }
|
||||||
cancel("The device is closed")
|
cancel("The device is closed")
|
||||||
}
|
}
|
||||||
@ -113,59 +105,24 @@ public interface Device : ContextAware, CoroutineScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Inner id of a device. Not necessary corresponds to the name in the parent container
|
|
||||||
*/
|
|
||||||
public val Device.id: String get() = meta["id"].string?: "device[${hashCode().toString(16)}]"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device that caches properties values
|
|
||||||
*/
|
|
||||||
public interface CachingDevice : Device {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Immediately (without waiting) get the cached (logical) state of property or return null if it is invalid
|
|
||||||
*/
|
|
||||||
public fun getProperty(propertyName: String): Meta?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate property (set logical state to invalid).
|
|
||||||
*
|
|
||||||
* This message is suspended to provide lock-free local property changes (they require coroutine context).
|
|
||||||
*/
|
|
||||||
public suspend fun invalidate(propertyName: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the logical state of property or suspend to read the physical value.
|
* Get the logical state of property or suspend to read the physical value.
|
||||||
*/
|
*/
|
||||||
public suspend fun Device.getOrReadProperty(propertyName: String): Meta = if (this is CachingDevice) {
|
public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
|
||||||
getProperty(propertyName) ?: readProperty(propertyName)
|
getProperty(propertyName) ?: readProperty(propertyName)
|
||||||
} else {
|
|
||||||
readProperty(propertyName)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a snapshot of the device logical state
|
* Get a snapshot of the device logical state
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public fun CachingDevice.getAllProperties(): Meta = Meta {
|
public fun Device.getAllProperties(): Meta = Meta {
|
||||||
for (descriptor in propertyDescriptors) {
|
for (descriptor in propertyDescriptors) {
|
||||||
set(descriptor.name.parseAsName(), getProperty(descriptor.name))
|
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe on property changes for the whole device
|
* Subscribe on property changes for the whole device
|
||||||
*/
|
*/
|
||||||
public fun Device.onPropertyChange(
|
public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job =
|
||||||
scope: CoroutineScope = this,
|
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this)
|
||||||
callback: suspend PropertyChangedMessage.() -> Unit,
|
|
||||||
): Job = messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(scope)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [Flow] of property change messages for specific property.
|
|
||||||
*/
|
|
||||||
public fun Device.propertyMessageFlow(propertyName: String): Flow<PropertyChangedMessage> = messageFlow
|
|
||||||
.filterIsInstance<PropertyChangedMessage>()
|
|
||||||
.filter { it.property == propertyName }
|
|
||||||
|
@ -14,27 +14,22 @@ public interface DeviceHub : Provider {
|
|||||||
|
|
||||||
override val defaultChainTarget: String get() = Device.DEVICE_TARGET
|
override val defaultChainTarget: String get() = Device.DEVICE_TARGET
|
||||||
|
|
||||||
/**
|
|
||||||
* List all devices, including sub-devices
|
|
||||||
*/
|
|
||||||
public fun buildDeviceTree(): Map<Name, Device> = buildMap {
|
|
||||||
fun putAll(prefix: Name, hub: DeviceHub) {
|
|
||||||
hub.devices.forEach {
|
|
||||||
put(prefix + it.key, it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
devices.forEach {
|
|
||||||
val name = it.key.asName()
|
|
||||||
put(name, it.value)
|
|
||||||
(it.value as? DeviceHub)?.let { hub ->
|
|
||||||
putAll(name, hub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = if (target == Device.DEVICE_TARGET) {
|
override fun content(target: String): Map<Name, Any> = if (target == Device.DEVICE_TARGET) {
|
||||||
buildDeviceTree()
|
buildMap {
|
||||||
|
fun putAll(prefix: Name, hub: DeviceHub) {
|
||||||
|
hub.devices.forEach {
|
||||||
|
put(prefix + it.key, it.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
devices.forEach {
|
||||||
|
val name = it.key.asName()
|
||||||
|
put(name, it.value)
|
||||||
|
(it.value as? DeviceHub)?.let { hub ->
|
||||||
|
putAll(name, hub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
emptyMap()
|
emptyMap()
|
||||||
}
|
}
|
||||||
@ -42,7 +37,6 @@ public interface DeviceHub : Provider {
|
|||||||
public companion object
|
public companion object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public operator fun DeviceHub.get(nameToken: NameToken): Device =
|
public operator fun DeviceHub.get(nameToken: NameToken): Device =
|
||||||
devices[nameToken] ?: error("Device with name $nameToken not found in $this")
|
devices[nameToken] ?: error("Device with name $nameToken not found in $this")
|
||||||
|
|
||||||
|
@ -22,10 +22,10 @@ public sealed class DeviceMessage {
|
|||||||
public abstract val sourceDevice: Name?
|
public abstract val sourceDevice: Name?
|
||||||
public abstract val targetDevice: Name?
|
public abstract val targetDevice: Name?
|
||||||
public abstract val comment: String?
|
public abstract val comment: String?
|
||||||
public abstract val time: Instant
|
public abstract val time: Instant?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the source device name for composition. If the original name is null, the resulting name is also null.
|
* Update the source device name for composition. If the original name is null, resulting name is also null.
|
||||||
*/
|
*/
|
||||||
public abstract fun changeSource(block: (Name) -> Name): DeviceMessage
|
public abstract fun changeSource(block: (Name) -> Name): DeviceMessage
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ public data class PropertyChangedMessage(
|
|||||||
override val sourceDevice: Name = Name.EMPTY,
|
override val sourceDevice: Name = Name.EMPTY,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
@ -71,11 +71,11 @@ public data class PropertyChangedMessage(
|
|||||||
@SerialName("property.set")
|
@SerialName("property.set")
|
||||||
public data class PropertySetMessage(
|
public data class PropertySetMessage(
|
||||||
public val property: String,
|
public val property: String,
|
||||||
public val value: Meta,
|
public val value: Meta?,
|
||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name,
|
override val targetDevice: Name,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -91,7 +91,7 @@ public data class PropertyGetMessage(
|
|||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name,
|
override val targetDevice: Name,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -103,9 +103,9 @@ public data class PropertyGetMessage(
|
|||||||
@SerialName("description.get")
|
@SerialName("description.get")
|
||||||
public data class GetDescriptionMessage(
|
public data class GetDescriptionMessage(
|
||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -122,7 +122,7 @@ public data class DescriptionMessage(
|
|||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
@ -141,7 +141,7 @@ public data class ActionExecuteMessage(
|
|||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name,
|
override val targetDevice: Name,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -160,7 +160,7 @@ public data class ActionResultMessage(
|
|||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
@ -175,7 +175,7 @@ public data class BinaryNotificationMessage(
|
|||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
@ -190,7 +190,7 @@ public data class EmptyDeviceMessage(
|
|||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -203,12 +203,12 @@ public data class EmptyDeviceMessage(
|
|||||||
public data class DeviceLogMessage(
|
public data class DeviceLogMessage(
|
||||||
val message: String,
|
val message: String,
|
||||||
val data: Meta? = null,
|
val data: Meta? = null,
|
||||||
override val sourceDevice: Name = Name.EMPTY,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -220,25 +220,10 @@ public data class DeviceErrorMessage(
|
|||||||
public val errorMessage: String?,
|
public val errorMessage: String?,
|
||||||
public val errorType: String? = null,
|
public val errorType: String? = null,
|
||||||
public val errorStackTrace: String? = null,
|
public val errorStackTrace: String? = null,
|
||||||
override val sourceDevice: Name = Name.EMPTY,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device [Device.lifecycleState] is changed
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
@SerialName("lifecycle")
|
|
||||||
public data class DeviceLifeCycleMessage(
|
|
||||||
val state: DeviceLifecycleState,
|
|
||||||
override val sourceDevice: Name = Name.EMPTY,
|
|
||||||
override val targetDevice: Name? = null,
|
|
||||||
override val comment: String? = null,
|
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
package space.kscience.controls.api
|
||||||
|
|
||||||
|
import io.ktor.utils.io.core.Closeable
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic bidirectional sender/receiver object
|
||||||
|
*/
|
||||||
|
public interface Socket<T> : Closeable {
|
||||||
|
/**
|
||||||
|
* Send an object to the socket
|
||||||
|
*/
|
||||||
|
public suspend fun send(data: T)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flow of objects received from socket
|
||||||
|
*/
|
||||||
|
public fun receiving(): Flow<T>
|
||||||
|
public fun isOpen(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect an input to this socket using designated [scope] for it and return a handler [Job].
|
||||||
|
* Multiple inputs could be connected to the same [Socket].
|
||||||
|
*/
|
||||||
|
public fun <T> Socket<T>.connectInput(scope: CoroutineScope, flow: Flow<T>): Job = scope.launch {
|
||||||
|
flow.collect { send(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -12,10 +12,10 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder
|
|||||||
@Serializable
|
@Serializable
|
||||||
public class PropertyDescriptor(
|
public class PropertyDescriptor(
|
||||||
public val name: String,
|
public val name: String,
|
||||||
public var description: String? = null,
|
public var info: String? = null,
|
||||||
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||||
public var readable: Boolean = true,
|
public var readable: Boolean = true,
|
||||||
public var mutable: Boolean = false
|
public var writable: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){
|
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){
|
||||||
@ -27,6 +27,6 @@ public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Un
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
public class ActionDescriptor(public val name: String) {
|
public class ActionDescriptor(public val name: String) {
|
||||||
public var description: String? = null
|
public var info: String? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
package space.kscience.controls.manager
|
|
||||||
|
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import space.kscience.dataforge.context.AbstractPlugin
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.context.PluginFactory
|
|
||||||
import space.kscience.dataforge.context.PluginTag
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
|
|
||||||
public class ClockManager : AbstractPlugin() {
|
|
||||||
override val tag: PluginTag get() = DeviceManager.tag
|
|
||||||
|
|
||||||
public val clock: Clock by lazy {
|
|
||||||
//TODO add clock customization
|
|
||||||
Clock.System
|
|
||||||
}
|
|
||||||
|
|
||||||
public companion object : PluginFactory<ClockManager> {
|
|
||||||
override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP)
|
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): ClockManager = ClockManager()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public val Context.clock: Clock get() = plugins[ClockManager]?.clock ?: Clock.System
|
|
@ -4,7 +4,6 @@ import kotlinx.coroutines.launch
|
|||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.api.DeviceHub
|
import space.kscience.controls.api.DeviceHub
|
||||||
import space.kscience.controls.api.getOrNull
|
import space.kscience.controls.api.getOrNull
|
||||||
import space.kscience.controls.api.id
|
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MutableMeta
|
import space.kscience.dataforge.meta.MutableMeta
|
||||||
@ -41,13 +40,11 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
|
|||||||
public fun <D : Device> DeviceManager.install(name: String, device: D): D {
|
public fun <D : Device> DeviceManager.install(name: String, device: D): D {
|
||||||
registerDevice(NameToken(name), device)
|
registerDevice(NameToken(name), device)
|
||||||
device.launch {
|
device.launch {
|
||||||
device.start()
|
device.open()
|
||||||
}
|
}
|
||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device)
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register and start a device built by [factory] with current [Context] and [meta].
|
* Register and start a device built by [factory] with current [Context] and [meta].
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
package space.kscience.controls.manager
|
package space.kscience.controls.manager
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.onEach
|
||||||
import space.kscience.controls.api.*
|
import space.kscience.controls.api.*
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.plus
|
import space.kscience.dataforge.names.plus
|
||||||
@ -23,7 +24,11 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
|||||||
}
|
}
|
||||||
|
|
||||||
is PropertySetMessage -> {
|
is PropertySetMessage -> {
|
||||||
writeProperty(request.property, request.value)
|
if (request.value == null) {
|
||||||
|
invalidate(request.property)
|
||||||
|
} else {
|
||||||
|
writeProperty(request.property, request.value)
|
||||||
|
}
|
||||||
PropertyChangedMessage(
|
PropertyChangedMessage(
|
||||||
property = request.property,
|
property = request.property,
|
||||||
value = getOrReadProperty(request.property),
|
value = getOrReadProperty(request.property),
|
||||||
@ -59,7 +64,6 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
|||||||
is DeviceErrorMessage,
|
is DeviceErrorMessage,
|
||||||
is EmptyDeviceMessage,
|
is EmptyDeviceMessage,
|
||||||
is DeviceLogMessage,
|
is DeviceLogMessage,
|
||||||
is DeviceLifeCycleMessage,
|
|
||||||
-> null
|
-> null
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
@ -67,41 +71,42 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process incoming [DeviceMessage], using hub naming to find target.
|
* Process incoming [DeviceMessage], using hub naming to evaluate target.
|
||||||
* If the `targetDevice` is `null`, then message is sent to each device in this hub
|
|
||||||
*/
|
*/
|
||||||
public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): List<DeviceMessage> {
|
public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMessage? {
|
||||||
return try {
|
return try {
|
||||||
val targetName = request.targetDevice
|
val targetName = request.targetDevice ?: return null
|
||||||
if (targetName == null) {
|
val device = getOrNull(targetName) ?: error("The device with name $targetName not found in $this")
|
||||||
buildDeviceTree().mapNotNull {
|
device.respondMessage(targetName, request)
|
||||||
it.value.respondMessage(it.key, request)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val device = getOrNull(targetName) ?: error("The device with name $targetName not found in $this")
|
|
||||||
listOfNotNull(device.respondMessage(targetName, request))
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
listOf(DeviceMessage.error(ex, sourceDevice = Name.EMPTY, targetDevice = request.sourceDevice))
|
DeviceMessage.error(ex, sourceDevice = Name.EMPTY, targetDevice = request.sourceDevice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect all messages from given [DeviceHub], applying proper relative names.
|
* Collect all messages from given [DeviceHub], applying proper relative names.
|
||||||
*/
|
*/
|
||||||
public fun DeviceHub.hubMessageFlow(): Flow<DeviceMessage> {
|
public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow<DeviceMessage> {
|
||||||
|
|
||||||
val deviceMessageFlow = if (this is Device) messageFlow else emptyFlow()
|
//TODO could we avoid using downstream scope?
|
||||||
|
val outbox = MutableSharedFlow<DeviceMessage>()
|
||||||
val childrenFlows = devices.map { (token, childDevice) ->
|
if (this is Device) {
|
||||||
if (childDevice is DeviceHub) {
|
messageFlow.onEach {
|
||||||
childDevice.hubMessageFlow()
|
outbox.emit(it)
|
||||||
|
}.launchIn(scope)
|
||||||
|
}
|
||||||
|
//TODO maybe better create map of all devices to limit copying
|
||||||
|
devices.forEach { (token, childDevice) ->
|
||||||
|
val flow = if (childDevice is DeviceHub) {
|
||||||
|
childDevice.hubMessageFlow(scope)
|
||||||
} else {
|
} else {
|
||||||
childDevice.messageFlow
|
childDevice.messageFlow
|
||||||
}.map { deviceMessage ->
|
|
||||||
deviceMessage.changeSource { token + it }
|
|
||||||
}
|
}
|
||||||
|
flow.onEach { deviceMessage ->
|
||||||
|
outbox.emit(
|
||||||
|
deviceMessage.changeSource { token + it }
|
||||||
|
)
|
||||||
|
}.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
return outbox
|
||||||
return merge(deviceMessageFlow, *childrenFlows.toTypedArray())
|
|
||||||
}
|
}
|
@ -1,70 +0,0 @@
|
|||||||
package space.kscience.controls.misc
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import space.kscience.controls.api.Device
|
|
||||||
import space.kscience.controls.api.DeviceMessage
|
|
||||||
import space.kscience.controls.api.PropertyChangedMessage
|
|
||||||
import space.kscience.controls.spec.DevicePropertySpec
|
|
||||||
import space.kscience.controls.spec.name
|
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
import space.kscience.dataforge.names.Name
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface for device property history.
|
|
||||||
*/
|
|
||||||
public interface PropertyHistory<T> {
|
|
||||||
/**
|
|
||||||
* Flow property values filtered by a time range. The implementation could flow it as a chunk or provide paging.
|
|
||||||
* So the resulting flow is allowed to suspend.
|
|
||||||
*
|
|
||||||
* If [until] is in the future, the resulting flow is potentially unlimited.
|
|
||||||
* Theoretically, it could be also unlimited if the event source keeps producing new event with timestamp in a given range.
|
|
||||||
*/
|
|
||||||
public fun flowHistory(
|
|
||||||
from: Instant = Instant.DISTANT_PAST,
|
|
||||||
until: Instant = Clock.System.now(),
|
|
||||||
): Flow<ValueWithTime<T>>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An in-memory property values history collector
|
|
||||||
*/
|
|
||||||
public class CollectedPropertyHistory<T>(
|
|
||||||
public val scope: CoroutineScope,
|
|
||||||
eventFlow: Flow<DeviceMessage>,
|
|
||||||
public val deviceName: Name,
|
|
||||||
public val propertyName: String,
|
|
||||||
public val converter: MetaConverter<T>,
|
|
||||||
maxSize: Int = 1000,
|
|
||||||
) : PropertyHistory<T> {
|
|
||||||
|
|
||||||
private val store: SharedFlow<ValueWithTime<T>> = eventFlow
|
|
||||||
.filterIsInstance<PropertyChangedMessage>()
|
|
||||||
.filter { it.sourceDevice == deviceName && it.property == propertyName }
|
|
||||||
.map { ValueWithTime(converter.read(it.value), it.time) }
|
|
||||||
.shareIn(scope, started = SharingStarted.Eagerly, replay = maxSize)
|
|
||||||
|
|
||||||
override fun flowHistory(from: Instant, until: Instant): Flow<ValueWithTime<T>> =
|
|
||||||
store.filter { it.time in from..until }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect and store in memory device property changes for a given property
|
|
||||||
*/
|
|
||||||
public fun <T> Device.collectPropertyHistory(
|
|
||||||
scope: CoroutineScope = this,
|
|
||||||
deviceName: Name,
|
|
||||||
propertyName: String,
|
|
||||||
converter: MetaConverter<T>,
|
|
||||||
maxSize: Int = 1000,
|
|
||||||
): PropertyHistory<T> = CollectedPropertyHistory(scope, messageFlow, deviceName, propertyName, converter, maxSize)
|
|
||||||
|
|
||||||
public fun <D : Device, T> D.collectPropertyHistory(
|
|
||||||
scope: CoroutineScope = this,
|
|
||||||
deviceName: Name,
|
|
||||||
spec: DevicePropertySpec<D, T>,
|
|
||||||
maxSize: Int = 1000,
|
|
||||||
): PropertyHistory<T> = collectPropertyHistory(scope, deviceName, spec.name, spec.converter, maxSize)
|
|
@ -1,69 +0,0 @@
|
|||||||
package space.kscience.controls.misc
|
|
||||||
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.io.Sink
|
|
||||||
import kotlinx.io.Source
|
|
||||||
import space.kscience.dataforge.io.IOFormat
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A value coupled to a time it was obtained at
|
|
||||||
*/
|
|
||||||
public data class ValueWithTime<T>(val value: T, val time: Instant) {
|
|
||||||
public companion object {
|
|
||||||
/**
|
|
||||||
* Create a [ValueWithTime] format for given value value [IOFormat]
|
|
||||||
*/
|
|
||||||
public fun <T> ioFormat(
|
|
||||||
valueFormat: IOFormat<T>,
|
|
||||||
): IOFormat<ValueWithTime<T>> = ValueWithTimeIOFormat(valueFormat)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [MetaConverter] with time for given value [MetaConverter]
|
|
||||||
*/
|
|
||||||
public fun <T> metaConverter(
|
|
||||||
valueConverter: MetaConverter<T>,
|
|
||||||
): MetaConverter<ValueWithTime<T>> = ValueWithTimeMetaConverter(valueConverter)
|
|
||||||
|
|
||||||
|
|
||||||
public const val META_TIME_KEY: String = "time"
|
|
||||||
public const val META_VALUE_KEY: String = "value"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ValueWithTimeIOFormat<T>(val valueFormat: IOFormat<T>) : IOFormat<ValueWithTime<T>> {
|
|
||||||
|
|
||||||
override fun readFrom(source: Source): ValueWithTime<T> {
|
|
||||||
val timestamp = InstantIOFormat.readFrom(source)
|
|
||||||
val value = valueFormat.readFrom(source)
|
|
||||||
return ValueWithTime(value, timestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeTo(sink: Sink, obj: ValueWithTime<T>) {
|
|
||||||
InstantIOFormat.writeTo(sink, obj.time)
|
|
||||||
valueFormat.writeTo(sink, obj.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ValueWithTimeMetaConverter<T>(
|
|
||||||
val valueConverter: MetaConverter<T>,
|
|
||||||
) : MetaConverter<ValueWithTime<T>> {
|
|
||||||
|
|
||||||
|
|
||||||
override fun readOrNull(
|
|
||||||
source: Meta,
|
|
||||||
): ValueWithTime<T>? = valueConverter.read(source[ValueWithTime.META_VALUE_KEY] ?: Meta.EMPTY)?.let {
|
|
||||||
ValueWithTime(it, source[ValueWithTime.META_TIME_KEY]?.instant ?: Instant.DISTANT_PAST)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(obj: ValueWithTime<T>): Meta = Meta {
|
|
||||||
ValueWithTime.META_TIME_KEY put obj.time.toMeta()
|
|
||||||
ValueWithTime.META_VALUE_KEY put valueConverter.convert(obj.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public fun <T : Any> MetaConverter<T>.withTime(): MetaConverter<ValueWithTime<T>> = ValueWithTimeMetaConverter(this)
|
|
@ -1,42 +0,0 @@
|
|||||||
package space.kscience.controls.misc
|
|
||||||
|
|
||||||
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.io.Sink
|
|
||||||
import kotlinx.io.Source
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.io.IOFormat
|
|
||||||
import space.kscience.dataforge.io.IOFormatFactory
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.string
|
|
||||||
import space.kscience.dataforge.names.Name
|
|
||||||
import space.kscience.dataforge.names.asName
|
|
||||||
import kotlin.reflect.KType
|
|
||||||
import kotlin.reflect.typeOf
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An [IOFormat] for [Instant]
|
|
||||||
*/
|
|
||||||
public object InstantIOFormat : IOFormat<Instant>, IOFormatFactory<Instant> {
|
|
||||||
override fun build(context: Context, meta: Meta): IOFormat<Instant> = this
|
|
||||||
|
|
||||||
override val name: Name = "instant".asName()
|
|
||||||
|
|
||||||
override val type: KType get() = typeOf<Instant>()
|
|
||||||
|
|
||||||
override fun writeTo(sink: Sink, obj: Instant) {
|
|
||||||
sink.writeLong(obj.epochSeconds)
|
|
||||||
sink.writeInt(obj.nanosecondsOfSecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun readFrom(source: Source): Instant {
|
|
||||||
val seconds = source.readLong()
|
|
||||||
val nanoseconds = source.readInt()
|
|
||||||
return Instant.fromEpochSeconds(seconds, nanoseconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun Instant.toMeta(): Meta = Meta(toString())
|
|
||||||
|
|
||||||
public val Meta.instant: Instant? get() = value?.string?.let { Instant.parse(it) }
|
|
@ -0,0 +1,18 @@
|
|||||||
|
package space.kscience.controls.misc
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.get
|
||||||
|
import space.kscience.dataforge.meta.long
|
||||||
|
|
||||||
|
// TODO move to core
|
||||||
|
|
||||||
|
public fun Instant.toMeta(): Meta = Meta {
|
||||||
|
"seconds" put epochSeconds
|
||||||
|
"nanos" put nanosecondsOfSecond
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun Meta.instant(): Instant = value?.long?.let { Instant.fromEpochMilliseconds(it) } ?: Instant.fromEpochSeconds(
|
||||||
|
get("seconds")?.long ?: 0L,
|
||||||
|
get("nanos")?.long ?: 0L,
|
||||||
|
)
|
@ -1,125 +0,0 @@
|
|||||||
package space.kscience.controls.ports
|
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
|
||||||
import kotlinx.io.Buffer
|
|
||||||
import kotlinx.io.Source
|
|
||||||
import space.kscience.controls.api.AsynchronousSocket
|
|
||||||
import space.kscience.dataforge.context.*
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.int
|
|
||||||
import space.kscience.dataforge.meta.string
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Raw [ByteArray] port
|
|
||||||
*/
|
|
||||||
public interface AsynchronousPort : ContextAware, AsynchronousSocket<ByteArray>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Capture [AsynchronousPort] output as kotlinx-io [Source].
|
|
||||||
* [scope] controls the consummation.
|
|
||||||
* If the scope is canceled, the source stops producing.
|
|
||||||
*/
|
|
||||||
public fun AsynchronousPort.receiveAsSource(scope: CoroutineScope): Source {
|
|
||||||
val buffer = Buffer()
|
|
||||||
|
|
||||||
subscribe().onEach {
|
|
||||||
buffer.write(it)
|
|
||||||
}.launchIn(scope)
|
|
||||||
|
|
||||||
return buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common abstraction for [AsynchronousPort] based on [Channel]
|
|
||||||
*/
|
|
||||||
public abstract class AbstractAsynchronousPort(
|
|
||||||
override val context: Context,
|
|
||||||
public val meta: Meta,
|
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
|
||||||
) : AsynchronousPort {
|
|
||||||
|
|
||||||
|
|
||||||
protected val scope: CoroutineScope by lazy {
|
|
||||||
CoroutineScope(
|
|
||||||
coroutineContext +
|
|
||||||
SupervisorJob(coroutineContext[Job]) +
|
|
||||||
CoroutineExceptionHandler { _, throwable -> logger.error(throwable) { throwable.stackTraceToString() } } +
|
|
||||||
CoroutineName(toString())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val outgoing = Channel<ByteArray>(meta["outgoing.capacity"].int?:100)
|
|
||||||
private val incoming = Channel<ByteArray>(meta["incoming.capacity"].int?:100)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal method to synchronously send data
|
|
||||||
*/
|
|
||||||
protected abstract suspend fun write(data: ByteArray)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal method to receive data synchronously
|
|
||||||
*/
|
|
||||||
protected suspend fun receive(data: ByteArray) {
|
|
||||||
logger.debug { "$this RECEIVED: ${data.decodeToString()}" }
|
|
||||||
incoming.send(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var sendJob: Job? = null
|
|
||||||
|
|
||||||
protected abstract fun onOpen()
|
|
||||||
|
|
||||||
final override fun open() {
|
|
||||||
if (!isOpen) {
|
|
||||||
sendJob = scope.launch {
|
|
||||||
for (data in outgoing) {
|
|
||||||
try {
|
|
||||||
write(data)
|
|
||||||
logger.debug { "${this@AbstractAsynchronousPort} SENT: ${data.decodeToString()}" }
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (ex is CancellationException) throw ex
|
|
||||||
logger.error(ex) { "Error while writing data to the port" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onOpen()
|
|
||||||
} else {
|
|
||||||
logger.warn { "$this already opened" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a data packet via the port
|
|
||||||
*/
|
|
||||||
override suspend fun send(data: ByteArray) {
|
|
||||||
outgoing.send(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Raw flow of incoming data chunks. The chunks are not guaranteed to be complete phrases.
|
|
||||||
* To form phrases, some condition should be used on top of it.
|
|
||||||
* For example [stringsDelimitedIncoming] generates phrases with fixed delimiter.
|
|
||||||
*/
|
|
||||||
override fun subscribe(): Flow<ByteArray> = incoming.receiveAsFlow()
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
outgoing.close()
|
|
||||||
incoming.close()
|
|
||||||
sendJob?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String = meta["name"].string?:"ChannelPort[${hashCode().toString(16)}]"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send UTF-8 encoded string
|
|
||||||
*/
|
|
||||||
public suspend fun AsynchronousPort.send(string: String): Unit = send(string.encodeToByteArray())
|
|
@ -0,0 +1,100 @@
|
|||||||
|
package space.kscience.controls.ports
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import space.kscience.controls.api.Socket
|
||||||
|
import space.kscience.dataforge.context.*
|
||||||
|
import space.kscience.dataforge.misc.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
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
public const val TYPE: String = "controls.port"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common abstraction for [Port] based on [Channel]
|
||||||
|
*/
|
||||||
|
public abstract class AbstractPort(
|
||||||
|
override val context: Context,
|
||||||
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
|
) : Port {
|
||||||
|
|
||||||
|
protected val scope: CoroutineScope = CoroutineScope(coroutineContext + SupervisorJob(coroutineContext[Job]))
|
||||||
|
|
||||||
|
private val outgoing = Channel<ByteArray>(100)
|
||||||
|
private val incoming = Channel<ByteArray>(Channel.CONFLATED)
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.coroutineContext[Job]?.invokeOnCompletion {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal method to synchronously send data
|
||||||
|
*/
|
||||||
|
protected abstract suspend fun write(data: ByteArray)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal method to receive data synchronously
|
||||||
|
*/
|
||||||
|
protected suspend fun receive(data: ByteArray) {
|
||||||
|
logger.debug { "${this@AbstractPort} RECEIVED: ${data.decodeToString()}" }
|
||||||
|
incoming.send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sendJob = scope.launch {
|
||||||
|
for (data in outgoing) {
|
||||||
|
try {
|
||||||
|
write(data)
|
||||||
|
logger.debug { "${this@AbstractPort} SENT: ${data.decodeToString()}" }
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
if (ex is CancellationException) throw ex
|
||||||
|
logger.error(ex) { "Error while writing data to the port" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a data packet via the port
|
||||||
|
*/
|
||||||
|
override suspend fun send(data: ByteArray) {
|
||||||
|
outgoing.send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw flow of incoming data chunks. The chunks are not guaranteed to be complete phrases.
|
||||||
|
* In order to form phrases, some condition should be used on top of it.
|
||||||
|
* For example [stringsDelimitedIncoming] generates phrases with fixed delimiter.
|
||||||
|
*/
|
||||||
|
override fun receiving(): Flow<ByteArray> = incoming.receiveAsFlow()
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
outgoing.close()
|
||||||
|
incoming.close()
|
||||||
|
sendJob.cancel()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOpen(): Boolean = scope.isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send UTF-8 encoded string
|
||||||
|
*/
|
||||||
|
public suspend fun Port.send(string: String): Unit = send(string.encodeToByteArray())
|
@ -0,0 +1,64 @@
|
|||||||
|
package space.kscience.controls.ports
|
||||||
|
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import space.kscience.dataforge.context.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A port that could be closed multiple times and opens automatically on request
|
||||||
|
*/
|
||||||
|
public class PortProxy(override val context: Context = Global, public val factory: suspend () -> Port) : Port, ContextAware {
|
||||||
|
|
||||||
|
private var actualPort: Port? = null
|
||||||
|
private val mutex: Mutex = Mutex()
|
||||||
|
|
||||||
|
private suspend fun port(): Port {
|
||||||
|
return mutex.withLock {
|
||||||
|
if (actualPort?.isOpen() == true) {
|
||||||
|
actualPort!!
|
||||||
|
} else {
|
||||||
|
factory().also {
|
||||||
|
actualPort = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun send(data: ByteArray) {
|
||||||
|
port().send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
override fun receiving(): Flow<ByteArray> = flow {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
//recreate port and Flow on connection problems
|
||||||
|
port().receiving().collect {
|
||||||
|
emit(it)
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
logger.warn{"Port read failed: ${t.message}. Reconnecting."}
|
||||||
|
mutex.withLock {
|
||||||
|
actualPort?.close()
|
||||||
|
actualPort = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// open by default
|
||||||
|
override fun isOpen(): Boolean = true
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
context.launch {
|
||||||
|
mutex.withLock {
|
||||||
|
actualPort?.close()
|
||||||
|
actualPort = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,43 +11,26 @@ public class Ports : AbstractPlugin() {
|
|||||||
|
|
||||||
override val tag: PluginTag get() = Companion.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
private val synchronousPortFactories by lazy {
|
private val portFactories by lazy {
|
||||||
context.gather<Factory<SynchronousPort>>(SYNCHRONOUS_PORT_TYPE)
|
context.gather<PortFactory>(PortFactory.TYPE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val asynchronousPortFactories by lazy {
|
private val portCache = mutableMapOf<Meta, Port>()
|
||||||
context.gather<Factory<AsynchronousPort>>(ASYNCHRONOUS_PORT_TYPE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new [AsynchronousPort] according to specification
|
* Create a new [Port] according to specification
|
||||||
*/
|
*/
|
||||||
public fun buildAsynchronousPort(meta: Meta): AsynchronousPort {
|
public fun buildPort(meta: Meta): Port = portCache.getOrPut(meta) {
|
||||||
val type by meta.string { error("Port type is not defined") }
|
val type by meta.string { error("Port type is not defined") }
|
||||||
val factory = asynchronousPortFactories.entries
|
val factory = portFactories.values.firstOrNull { it.type == type }
|
||||||
.firstOrNull { it.key.toString() == type }?.value
|
|
||||||
?: error("Port factory for type $type not found")
|
?: error("Port factory for type $type not found")
|
||||||
return factory.build(context, meta)
|
factory.build(context, meta)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [SynchronousPort] according to specification or wrap an asynchronous implementation
|
|
||||||
*/
|
|
||||||
public fun buildSynchronousPort(meta: Meta): SynchronousPort {
|
|
||||||
val type by meta.string { error("Port type is not defined") }
|
|
||||||
val factory = synchronousPortFactories.entries
|
|
||||||
.firstOrNull { it.key.toString() == type }?.value
|
|
||||||
?: return buildAsynchronousPort(meta).asSynchronousPort()
|
|
||||||
return factory.build(context, meta)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public companion object : PluginFactory<Ports> {
|
public companion object : PluginFactory<Ports> {
|
||||||
|
|
||||||
override val tag: PluginTag = PluginTag("controls.ports", group = PluginTag.DATAFORGE_GROUP)
|
override val tag: PluginTag = PluginTag("controls.ports", group = PluginTag.DATAFORGE_GROUP)
|
||||||
|
|
||||||
public const val ASYNCHRONOUS_PORT_TYPE: String = "controls.asynchronousPort"
|
|
||||||
public const val SYNCHRONOUS_PORT_TYPE: String = "controls.synchronousPort"
|
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): Ports = Ports()
|
override fun build(context: Context, meta: Meta): Ports = Ports()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,86 +2,27 @@ package space.kscience.controls.ports
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.io.Buffer
|
|
||||||
import kotlinx.io.readByteArray
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.context.ContextAware
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A port handler for synchronous (request-response) communication with a port.
|
* A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended.
|
||||||
* Only one request could be active at a time (others are suspended).
|
* The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment.
|
||||||
*/
|
*/
|
||||||
public interface SynchronousPort : ContextAware, AutoCloseable {
|
public class SynchronousPort(public val port: Port, private val mutex: Mutex) : Port by port {
|
||||||
|
|
||||||
public fun open()
|
|
||||||
|
|
||||||
public val isOpen: Boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a single message and wait for the flow of response chunks.
|
* Send a single message and wait for the flow of respond messages.
|
||||||
* The consumer is responsible for calling a terminal operation on the flow.
|
|
||||||
*/
|
*/
|
||||||
public suspend fun <R> respond(
|
public suspend fun <R> respond(data: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R = mutex.withLock {
|
||||||
request: ByteArray,
|
port.send(data)
|
||||||
transform: suspend Flow<ByteArray>.() -> R,
|
transform(port.receiving())
|
||||||
): R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronously read fixed size response to a given [request]. Discard additional response bytes.
|
|
||||||
*/
|
|
||||||
public suspend fun respondFixedMessageSize(
|
|
||||||
request: ByteArray,
|
|
||||||
responseSize: Int,
|
|
||||||
): ByteArray = respond(request) {
|
|
||||||
val buffer = Buffer()
|
|
||||||
takeWhile {
|
|
||||||
buffer.size < responseSize
|
|
||||||
}.collect {
|
|
||||||
buffer.write(it)
|
|
||||||
}
|
|
||||||
buffer.readByteArray(responseSize)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SynchronousOverAsynchronousPort(
|
|
||||||
val port: AsynchronousPort,
|
|
||||||
val mutex: Mutex,
|
|
||||||
) : SynchronousPort {
|
|
||||||
|
|
||||||
override val context: Context get() = port.context
|
|
||||||
|
|
||||||
override fun open() {
|
|
||||||
if (!port.isOpen) port.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val isOpen: Boolean get() = port.isOpen
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
if (port.isOpen) port.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun <R> respond(
|
|
||||||
request: ByteArray,
|
|
||||||
transform: suspend Flow<ByteArray>.() -> R,
|
|
||||||
): R = mutex.withLock {
|
|
||||||
port.send(request)
|
|
||||||
transform(port.subscribe())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide a synchronous wrapper for an asynchronous port.
|
* Provide a synchronous wrapper for a port
|
||||||
* Optionally provide external [mutex] for operation synchronization.
|
|
||||||
*
|
|
||||||
* If the [AsynchronousPort] is called directly, it could violate [SynchronousPort] contract
|
|
||||||
* of only one request running simultaneously.
|
|
||||||
*/
|
*/
|
||||||
public fun AsynchronousPort.asSynchronousPort(mutex: Mutex = Mutex()): SynchronousPort =
|
public fun Port.synchronous(mutex: Mutex = Mutex()): SynchronousPort = SynchronousPort(this, mutex)
|
||||||
SynchronousOverAsynchronousPort(this, mutex)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send request and read incoming data blocks until the delimiter is encountered
|
* Send request and read incoming data blocks until the delimiter is encountered
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
package space.kscience.controls.ports
|
|
||||||
|
|
||||||
import space.kscience.dataforge.io.Binary
|
|
||||||
|
|
||||||
public fun Binary.readShort(position: Int): Short = read(position) { readShort() }
|
|
@ -1,27 +1,21 @@
|
|||||||
package space.kscience.controls.ports
|
package space.kscience.controls.ports
|
||||||
|
|
||||||
|
import io.ktor.utils.io.core.BytePacketBuilder
|
||||||
|
import io.ktor.utils.io.core.readBytes
|
||||||
|
import io.ktor.utils.io.core.reset
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
|
||||||
import kotlinx.coroutines.flow.transform
|
import kotlinx.coroutines.flow.transform
|
||||||
import kotlinx.io.Buffer
|
|
||||||
import kotlinx.io.readByteArray
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform byte fragments into complete phrases using given delimiter. Not thread safe.
|
* Transform byte fragments into complete phrases using given delimiter. Not thread safe.
|
||||||
*
|
|
||||||
* TODO add type wrapper for phrases
|
|
||||||
*/
|
*/
|
||||||
public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray> {
|
public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray> {
|
||||||
require(delimiter.isNotEmpty()) { "Delimiter must not be empty" }
|
require(delimiter.isNotEmpty()) { "Delimiter must not be empty" }
|
||||||
|
|
||||||
val output = Buffer()
|
val output = BytePacketBuilder()
|
||||||
var matcherPosition = 0
|
var matcherPosition = 0
|
||||||
|
|
||||||
onCompletion {
|
|
||||||
output.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return transform { chunk ->
|
return transform { chunk ->
|
||||||
chunk.forEach { byte ->
|
chunk.forEach { byte ->
|
||||||
output.writeByte(byte)
|
output.writeByte(byte)
|
||||||
@ -30,8 +24,9 @@ public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray>
|
|||||||
matcherPosition++
|
matcherPosition++
|
||||||
if (matcherPosition == delimiter.size) {
|
if (matcherPosition == delimiter.size) {
|
||||||
//full match achieved, sending result
|
//full match achieved, sending result
|
||||||
emit(output.readByteArray())
|
val bytes = output.build()
|
||||||
output.clear()
|
emit(bytes.readBytes())
|
||||||
|
output.reset()
|
||||||
matcherPosition = 0
|
matcherPosition = 0
|
||||||
}
|
}
|
||||||
} else if (matcherPosition > 0) {
|
} else if (matcherPosition > 0) {
|
||||||
@ -42,31 +37,6 @@ public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Flow<ByteArray>.withFixedMessageSize(messageSize: Int): Flow<ByteArray> {
|
|
||||||
require(messageSize > 0) { "Message size should be positive" }
|
|
||||||
|
|
||||||
val output = Buffer()
|
|
||||||
|
|
||||||
onCompletion {
|
|
||||||
output.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return transform { chunk ->
|
|
||||||
val remaining: Int = (messageSize - output.size).toInt()
|
|
||||||
if (chunk.size >= remaining) {
|
|
||||||
output.write(chunk, endIndex = remaining)
|
|
||||||
emit(output.readByteArray())
|
|
||||||
output.clear()
|
|
||||||
//write the remaining chunk fragment
|
|
||||||
if(chunk.size> remaining) {
|
|
||||||
output.write(chunk, startIndex = remaining)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
output.write(chunk)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
|
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
|
||||||
*/
|
*/
|
||||||
@ -77,9 +47,9 @@ public fun Flow<ByteArray>.withStringDelimiter(delimiter: String): Flow<String>
|
|||||||
/**
|
/**
|
||||||
* A flow of delimited phrases
|
* A flow of delimited phrases
|
||||||
*/
|
*/
|
||||||
public fun AsynchronousPort.delimitedIncoming(delimiter: ByteArray): Flow<ByteArray> = subscribe().withDelimiter(delimiter)
|
public fun Port.delimitedIncoming(delimiter: ByteArray): Flow<ByteArray> = receiving().withDelimiter(delimiter)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flow of delimited phrases with string content
|
* A flow of delimited phrases with string content
|
||||||
*/
|
*/
|
||||||
public fun AsynchronousPort.stringsDelimitedIncoming(delimiter: String): Flow<String> = subscribe().withStringDelimiter(delimiter)
|
public fun Port.stringsDelimitedIncoming(delimiter: String): Flow<String> = receiving().withStringDelimiter(delimiter)
|
||||||
|
@ -1,44 +1,36 @@
|
|||||||
package space.kscience.controls.spec
|
package space.kscience.controls.spec
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import space.kscience.controls.api.*
|
import space.kscience.controls.api.*
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.debug
|
import space.kscience.dataforge.context.error
|
||||||
import space.kscience.dataforge.context.logger
|
import space.kscience.dataforge.context.logger
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.int
|
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
import space.kscience.dataforge.misc.DFExperimental
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
/**
|
|
||||||
* Write a meta [item] to [device]
|
|
||||||
*/
|
|
||||||
@OptIn(InternalDeviceAPI::class)
|
@OptIn(InternalDeviceAPI::class)
|
||||||
private suspend fun <D : Device, T> MutableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
|
private suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
|
||||||
write(device, converter.readOrNull(item) ?: error("Meta $item could not be read with $converter"))
|
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Read Meta item from the [device]
|
|
||||||
*/
|
|
||||||
@OptIn(InternalDeviceAPI::class)
|
@OptIn(InternalDeviceAPI::class)
|
||||||
private suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
|
private suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
|
||||||
read(device)?.let(converter::convert)
|
read(device)?.let(converter::objectToMeta)
|
||||||
|
|
||||||
|
|
||||||
private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta(
|
private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta(
|
||||||
device: D,
|
device: D,
|
||||||
item: Meta,
|
item: Meta,
|
||||||
): Meta? {
|
): Meta? {
|
||||||
val arg: I = inputConverter.readOrNull(item) ?: error("Failed to convert $item with $inputConverter")
|
val arg: I = inputConverter.metaToObject(item) ?: error("Failed to convert $item with $inputConverter")
|
||||||
val res = execute(device, arg)
|
val res = execute(device, arg)
|
||||||
return res?.let { outputConverter.convert(res) }
|
return res?.let { outputConverter.objectToMeta(res) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -47,8 +39,8 @@ private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta
|
|||||||
*/
|
*/
|
||||||
public abstract class DeviceBase<D : Device>(
|
public abstract class DeviceBase<D : Device>(
|
||||||
final override val context: Context,
|
final override val context: Context,
|
||||||
final override val meta: Meta = Meta.EMPTY,
|
override val meta: Meta = Meta.EMPTY,
|
||||||
) : CachingDevice {
|
) : Device {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection of property specifications
|
* Collection of property specifications
|
||||||
@ -66,27 +58,15 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
override val actionDescriptors: Collection<ActionDescriptor>
|
override val actionDescriptors: Collection<ActionDescriptor>
|
||||||
get() = actions.values.map { it.descriptor }
|
get() = actions.values.map { it.descriptor }
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext by lazy {
|
||||||
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow(
|
context.newCoroutineContext(
|
||||||
replay = meta["message.buffer"].int ?: 1000,
|
SupervisorJob(context.coroutineContext[Job]) +
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
CoroutineName("Device $this") +
|
||||||
)
|
CoroutineExceptionHandler { _, throwable ->
|
||||||
|
logger.error(throwable) { "Exception in device $this job" }
|
||||||
override val coroutineContext: CoroutineContext = context.newCoroutineContext(
|
|
||||||
SupervisorJob(context.coroutineContext[Job]) +
|
|
||||||
CoroutineName("Device $this") +
|
|
||||||
CoroutineExceptionHandler { _, throwable ->
|
|
||||||
launch {
|
|
||||||
sharedMessageFlow.emit(
|
|
||||||
DeviceErrorMessage(
|
|
||||||
errorMessage = throwable.message,
|
|
||||||
errorType = throwable::class.simpleName,
|
|
||||||
errorStackTrace = throwable.stackTraceToString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,6 +74,8 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
*/
|
*/
|
||||||
private val logicalState: HashMap<String, Meta?> = HashMap()
|
private val logicalState: HashMap<String, Meta?> = HashMap()
|
||||||
|
|
||||||
|
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow()
|
||||||
|
|
||||||
public override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow
|
public override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@ -105,7 +87,7 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
/**
|
/**
|
||||||
* Update logical property state and notify listeners
|
* Update logical property state and notify listeners
|
||||||
*/
|
*/
|
||||||
protected suspend fun propertyChanged(propertyName: String, value: Meta?) {
|
protected suspend fun updateLogical(propertyName: String, value: Meta?) {
|
||||||
if (value != logicalState[propertyName]) {
|
if (value != logicalState[propertyName]) {
|
||||||
stateLock.withLock {
|
stateLock.withLock {
|
||||||
logicalState[propertyName] = value
|
logicalState[propertyName] = value
|
||||||
@ -117,10 +99,10 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify the device that a property with [spec] value is changed
|
* Update logical state using given [spec] and its convertor
|
||||||
*/
|
*/
|
||||||
protected suspend fun <T> propertyChanged(spec: DevicePropertySpec<D, T>, value: T) {
|
public suspend fun <T> updateLogical(spec: DevicePropertySpec<D, T>, value: T) {
|
||||||
propertyChanged(spec.name, spec.converter.convert(value))
|
updateLogical(spec.name, spec.converter.objectToMeta(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -130,7 +112,7 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
override suspend fun readProperty(propertyName: String): Meta {
|
override suspend fun readProperty(propertyName: String): Meta {
|
||||||
val spec = properties[propertyName] ?: error("Property with name $propertyName not found")
|
val spec = properties[propertyName] ?: error("Property with name $propertyName not found")
|
||||||
val meta = spec.readMeta(self) ?: error("Failed to read property $propertyName")
|
val meta = spec.readMeta(self) ?: error("Failed to read property $propertyName")
|
||||||
propertyChanged(propertyName, meta)
|
updateLogical(propertyName, meta)
|
||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +122,7 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
public suspend fun readPropertyOrNull(propertyName: String): Meta? {
|
public suspend fun readPropertyOrNull(propertyName: String): Meta? {
|
||||||
val spec = properties[propertyName] ?: return null
|
val spec = properties[propertyName] ?: return null
|
||||||
val meta = spec.readMeta(self) ?: return null
|
val meta = spec.readMeta(self) ?: return null
|
||||||
propertyChanged(propertyName, meta)
|
updateLogical(propertyName, meta)
|
||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,26 +135,15 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeProperty(propertyName: String, value: Meta): Unit {
|
override suspend fun writeProperty(propertyName: String, value: Meta): Unit {
|
||||||
//bypass property setting if it already has that value
|
|
||||||
if (logicalState[propertyName] == value) {
|
|
||||||
logger.debug { "Skipping setting $propertyName to $value because value is already set" }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
when (val property = properties[propertyName]) {
|
when (val property = properties[propertyName]) {
|
||||||
null -> {
|
null -> {
|
||||||
//If there are no registered physical properties with given name, write a logical one.
|
//If there is a physical property with a given name, invalidate logical property and write physical one
|
||||||
propertyChanged(propertyName, value)
|
updateLogical(propertyName, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
is MutableDevicePropertySpec -> {
|
is WritableDevicePropertySpec -> {
|
||||||
//if there is a writeable property with a given name, invalidate logical and write physical
|
|
||||||
invalidate(propertyName)
|
invalidate(propertyName)
|
||||||
property.writeMeta(self, value)
|
property.writeMeta(self, value)
|
||||||
// perform read after writing if the writer did not set the value and the value is still in invalid state
|
|
||||||
if (logicalState[propertyName] == null) {
|
|
||||||
val meta = property.readMeta(self)
|
|
||||||
propertyChanged(propertyName, meta)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@ -187,46 +158,21 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@DFExperimental
|
@DFExperimental
|
||||||
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.INIT
|
||||||
private set(value) {
|
protected set
|
||||||
if (field != value) {
|
|
||||||
launch {
|
|
||||||
sharedMessageFlow.emit(
|
|
||||||
DeviceLifeCycleMessage(value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open suspend fun onStart() {
|
|
||||||
|
|
||||||
|
@OptIn(DFExperimental::class)
|
||||||
|
override suspend fun open() {
|
||||||
|
super.open()
|
||||||
|
lifecycleState = DeviceLifecycleState.OPEN
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DFExperimental::class)
|
@OptIn(DFExperimental::class)
|
||||||
final override suspend fun start() {
|
override fun close() {
|
||||||
if (lifecycleState == DeviceLifecycleState.STOPPED) {
|
lifecycleState = DeviceLifecycleState.CLOSED
|
||||||
super.start()
|
super.close()
|
||||||
lifecycleState = DeviceLifecycleState.STARTING
|
|
||||||
onStart()
|
|
||||||
lifecycleState = DeviceLifecycleState.STARTED
|
|
||||||
} else {
|
|
||||||
logger.debug { "Device $this is already started" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onStop() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(DFExperimental::class)
|
|
||||||
final override fun stop() {
|
|
||||||
onStop()
|
|
||||||
lifecycleState = DeviceLifecycleState.STOPPED
|
|
||||||
super.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
abstract override fun toString(): String
|
abstract override fun toString(): String
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,14 +16,15 @@ public open class DeviceBySpec<D : Device>(
|
|||||||
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
|
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
|
||||||
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
|
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
|
||||||
|
|
||||||
override suspend fun onStart(): Unit = with(spec) {
|
override suspend fun open(): Unit = with(spec) {
|
||||||
|
super.open()
|
||||||
self.onOpen()
|
self.onOpen()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop(): Unit = with(spec){
|
override fun close(): Unit = with(spec) {
|
||||||
self.onClose()
|
self.onClose()
|
||||||
|
super.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun toString(): String = "Device(spec=$spec)"
|
override fun toString(): String = "Device(spec=$spec)"
|
||||||
}
|
}
|
@ -3,9 +3,9 @@ package space.kscience.controls.spec
|
|||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
|
|
||||||
internal object DeviceMetaPropertySpec : DevicePropertySpec<Device, Meta> {
|
internal object DeviceMetaPropertySpec: DevicePropertySpec<Device,Meta> {
|
||||||
override val descriptor: PropertyDescriptor = PropertyDescriptor("@meta")
|
override val descriptor: PropertyDescriptor = PropertyDescriptor("@meta")
|
||||||
|
|
||||||
override val converter: MetaConverter<Meta> = MetaConverter.meta
|
override val converter: MetaConverter<Meta> = MetaConverter.meta
|
||||||
|
@ -4,8 +4,11 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.controls.api.*
|
import space.kscience.controls.api.ActionDescriptor
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.api.PropertyChangedMessage
|
||||||
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,7 +20,7 @@ public annotation class InternalDeviceAPI
|
|||||||
/**
|
/**
|
||||||
* Specification for a device read-only property
|
* Specification for a device read-only property
|
||||||
*/
|
*/
|
||||||
public interface DevicePropertySpec<in D, T> {
|
public interface DevicePropertySpec<in D : Device, T> {
|
||||||
/**
|
/**
|
||||||
* Property descriptor
|
* Property descriptor
|
||||||
*/
|
*/
|
||||||
@ -41,7 +44,7 @@ public interface DevicePropertySpec<in D, T> {
|
|||||||
public val DevicePropertySpec<*, *>.name: String get() = descriptor.name
|
public val DevicePropertySpec<*, *>.name: String get() = descriptor.name
|
||||||
|
|
||||||
|
|
||||||
public interface MutableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
|
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
|
||||||
/**
|
/**
|
||||||
* Write physical value to a device
|
* Write physical value to a device
|
||||||
*/
|
*/
|
||||||
@ -50,7 +53,7 @@ public interface MutableDevicePropertySpec<in D : Device, T> : DevicePropertySpe
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface DeviceActionSpec<in D, I, O> {
|
public interface DeviceActionSpec<in D : Device, I, O> {
|
||||||
/**
|
/**
|
||||||
* Action descriptor
|
* Action descriptor
|
||||||
*/
|
*/
|
||||||
@ -72,29 +75,30 @@ public interface DeviceActionSpec<in D, I, O> {
|
|||||||
public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name
|
public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name
|
||||||
|
|
||||||
public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>): T =
|
public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>): T =
|
||||||
propertySpec.converter.readOrNull(readProperty(propertySpec.name)) ?: error("Property read result is not valid")
|
propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Property read result is not valid")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read typed value and update/push event if needed.
|
* Read typed value and update/push event if needed.
|
||||||
* Return null if property read is not successful or property is undefined.
|
* 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? =
|
public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||||
readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::readOrNull)
|
readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||||
|
|
||||||
public suspend fun <T, D : Device> D.getOrRead(propertySpec: DevicePropertySpec<D, T>): T =
|
|
||||||
propertySpec.converter.read(getOrReadProperty(propertySpec.name))
|
public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||||
|
getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write typed property state and invalidate logical state
|
* Write typed property state and invalidate logical state
|
||||||
*/
|
*/
|
||||||
public suspend fun <T, D : Device> D.write(propertySpec: MutableDevicePropertySpec<D, T>, value: T) {
|
public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, value: T) {
|
||||||
writeProperty(propertySpec.name, propertySpec.converter.convert(value))
|
writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope
|
* Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope
|
||||||
*/
|
*/
|
||||||
public fun <T, D : Device> D.writeAsync(propertySpec: MutableDevicePropertySpec<D, T>, value: T): Job = launch {
|
public operator fun <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Job = launch {
|
||||||
write(propertySpec, value)
|
write(propertySpec, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,39 +108,37 @@ public fun <T, D : Device> D.writeAsync(propertySpec: MutableDevicePropertySpec<
|
|||||||
public fun <D : Device, T> D.propertyFlow(spec: DevicePropertySpec<D, T>): Flow<T> = messageFlow
|
public fun <D : Device, T> D.propertyFlow(spec: DevicePropertySpec<D, T>): Flow<T> = messageFlow
|
||||||
.filterIsInstance<PropertyChangedMessage>()
|
.filterIsInstance<PropertyChangedMessage>()
|
||||||
.filter { it.property == spec.name }
|
.filter { it.property == spec.name }
|
||||||
.mapNotNull { spec.converter.read(it.value) }
|
.mapNotNull { spec.converter.metaToObject(it.value) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A type safe property change listener. Uses the device [CoroutineScope].
|
* A type safe property change listener. Uses the device [CoroutineScope].
|
||||||
*/
|
*/
|
||||||
public fun <D : Device, T> D.onPropertyChange(
|
public fun <D : Device, T> D.onPropertyChange(
|
||||||
spec: DevicePropertySpec<D, T>,
|
spec: DevicePropertySpec<D, T>,
|
||||||
scope: CoroutineScope = this,
|
|
||||||
callback: suspend PropertyChangedMessage.(T) -> Unit,
|
callback: suspend PropertyChangedMessage.(T) -> Unit,
|
||||||
): Job = messageFlow
|
): Job = messageFlow
|
||||||
.filterIsInstance<PropertyChangedMessage>()
|
.filterIsInstance<PropertyChangedMessage>()
|
||||||
.filter { it.property == spec.name }
|
.filter { it.property == spec.name }
|
||||||
.onEach { change ->
|
.onEach { change ->
|
||||||
val newValue = spec.converter.read(change.value)
|
val newValue = spec.converter.metaToObject(change.value)
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
change.callback(newValue)
|
change.callback(newValue)
|
||||||
}
|
}
|
||||||
}.launchIn(scope)
|
}.launchIn(this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call [callback] on initial property value and each value change
|
* Call [callback] on initial property value and each value change
|
||||||
*/
|
*/
|
||||||
public fun <D : Device, T> D.useProperty(
|
public fun <D : Device, T> D.useProperty(
|
||||||
spec: DevicePropertySpec<D, T>,
|
spec: DevicePropertySpec<D, T>,
|
||||||
scope: CoroutineScope = this,
|
|
||||||
callback: suspend (T) -> Unit,
|
callback: suspend (T) -> Unit,
|
||||||
): Job = scope.launch {
|
): Job = launch {
|
||||||
callback(read(spec))
|
callback(read(spec))
|
||||||
messageFlow
|
messageFlow
|
||||||
.filterIsInstance<PropertyChangedMessage>()
|
.filterIsInstance<PropertyChangedMessage>()
|
||||||
.filter { it.property == spec.name }
|
.filter { it.property == spec.name }
|
||||||
.collect { change ->
|
.collect { change ->
|
||||||
val newValue = spec.converter.readOrNull(change.value)
|
val newValue = spec.converter.metaToObject(change.value)
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
callback(newValue)
|
callback(newValue)
|
||||||
}
|
}
|
||||||
@ -147,7 +149,7 @@ public fun <D : Device, T> D.useProperty(
|
|||||||
/**
|
/**
|
||||||
* Reset the logical state of a property
|
* Reset the logical state of a property
|
||||||
*/
|
*/
|
||||||
public suspend fun <D : CachingDevice> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
|
public suspend fun <D : Device> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
|
||||||
invalidate(propertySpec.name)
|
invalidate(propertySpec.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,23 +5,24 @@ import space.kscience.controls.api.ActionDescriptor
|
|||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
import kotlin.properties.PropertyDelegateProvider
|
import kotlin.properties.PropertyDelegateProvider
|
||||||
import kotlin.properties.ReadOnlyProperty
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
import kotlin.reflect.KMutableProperty1
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
public object UnitMetaConverter : MetaConverter<Unit> {
|
public object UnitMetaConverter: MetaConverter<Unit>{
|
||||||
|
override fun metaToObject(meta: Meta): Unit = Unit
|
||||||
|
|
||||||
override fun readOrNull(source: Meta): Unit = Unit
|
override fun objectToMeta(obj: Unit): Meta = Meta.EMPTY
|
||||||
|
|
||||||
override fun convert(obj: Unit): Meta = Meta.EMPTY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public val MetaConverter.Companion.unit: MetaConverter<Unit> get() = UnitMetaConverter
|
public val MetaConverter.Companion.unit: MetaConverter<Unit> get() = UnitMetaConverter
|
||||||
|
|
||||||
@OptIn(InternalDeviceAPI::class)
|
@OptIn(InternalDeviceAPI::class)
|
||||||
public abstract class DeviceSpec<D : Device> {
|
public abstract class DeviceSpec<D : Device> {
|
||||||
//initializing the metadata property for everyone
|
//initializing meta property for everyone
|
||||||
private val _properties = hashMapOf<String, DevicePropertySpec<D, *>>(
|
private val _properties = hashMapOf<String, DevicePropertySpec<D, *>>(
|
||||||
DeviceMetaPropertySpec.name to DeviceMetaPropertySpec
|
DeviceMetaPropertySpec.name to DeviceMetaPropertySpec
|
||||||
)
|
)
|
||||||
@ -43,25 +44,72 @@ public abstract class DeviceSpec<D : Device> {
|
|||||||
return deviceProperty
|
return deviceProperty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun <T> property(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
readOnlyProperty: KProperty1<D, T>,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, DevicePropertySpec<D, T>>> =
|
||||||
|
PropertyDelegateProvider { _, property ->
|
||||||
|
val deviceProperty = object : DevicePropertySpec<D, T> {
|
||||||
|
override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
|
||||||
|
//TODO add type from converter
|
||||||
|
writable = true
|
||||||
|
}.apply(descriptorBuilder)
|
||||||
|
|
||||||
|
override val converter: MetaConverter<T> = converter
|
||||||
|
|
||||||
|
override suspend fun read(device: D): T = withContext(device.coroutineContext) {
|
||||||
|
readOnlyProperty.get(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerProperty(deviceProperty)
|
||||||
|
ReadOnlyProperty { _, _ ->
|
||||||
|
deviceProperty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <T> mutableProperty(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
readWriteProperty: KMutableProperty1<D, T>,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
|
||||||
|
PropertyDelegateProvider { _, property ->
|
||||||
|
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
||||||
|
|
||||||
|
override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
|
||||||
|
//TODO add the type from converter
|
||||||
|
writable = true
|
||||||
|
}.apply(descriptorBuilder)
|
||||||
|
|
||||||
|
override val converter: MetaConverter<T> = converter
|
||||||
|
|
||||||
|
override suspend fun read(device: D): T = withContext(device.coroutineContext) {
|
||||||
|
readWriteProperty.get(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
|
||||||
|
readWriteProperty.set(device, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerProperty(deviceProperty)
|
||||||
|
ReadOnlyProperty { _, _ ->
|
||||||
|
deviceProperty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public fun <T> property(
|
public fun <T> property(
|
||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> T?,
|
read: suspend D.() -> T?,
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
|
||||||
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
||||||
val propertyName = name ?: property.name
|
val propertyName = name ?: property.name
|
||||||
val deviceProperty = object : DevicePropertySpec<D, T> {
|
val deviceProperty = object : DevicePropertySpec<D, T> {
|
||||||
|
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
|
||||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
|
|
||||||
fromSpec(property)
|
|
||||||
descriptorBuilder()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val converter: MetaConverter<T> = converter
|
override val converter: MetaConverter<T> = converter
|
||||||
|
|
||||||
override suspend fun read(device: D): T? =
|
override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
|
||||||
withContext(device.coroutineContext) { device.read(propertyName) }
|
|
||||||
}
|
}
|
||||||
registerProperty(deviceProperty)
|
registerProperty(deviceProperty)
|
||||||
ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ ->
|
ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ ->
|
||||||
@ -73,30 +121,23 @@ public abstract class DeviceSpec<D : Device> {
|
|||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> T?,
|
read: suspend D.() -> T?,
|
||||||
write: suspend D.(propertyName: String, value: T) -> Unit,
|
write: suspend D.(T) -> Unit,
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> =
|
||||||
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
|
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
|
||||||
val propertyName = name ?: property.name
|
val propertyName = name ?: property.name
|
||||||
val deviceProperty = object : MutableDevicePropertySpec<D, T> {
|
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
||||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(
|
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
|
||||||
propertyName,
|
|
||||||
mutable = true
|
|
||||||
).apply {
|
|
||||||
fromSpec(property)
|
|
||||||
descriptorBuilder()
|
|
||||||
}
|
|
||||||
override val converter: MetaConverter<T> = converter
|
override val converter: MetaConverter<T> = converter
|
||||||
|
|
||||||
override suspend fun read(device: D): T? =
|
override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
|
||||||
withContext(device.coroutineContext) { device.read(propertyName) }
|
|
||||||
|
|
||||||
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
|
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
|
||||||
device.write(propertyName, value)
|
device.write(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
registerProperty(deviceProperty)
|
_properties[propertyName] = deviceProperty
|
||||||
ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>> { _, _ ->
|
ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>> { _, _ ->
|
||||||
deviceProperty
|
deviceProperty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,13 +155,10 @@ public abstract class DeviceSpec<D : Device> {
|
|||||||
name: String? = null,
|
name: String? = null,
|
||||||
execute: suspend D.(I) -> O,
|
execute: suspend D.(I) -> O,
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
|
||||||
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
|
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
||||||
val actionName = name ?: property.name
|
val actionName = name ?: property.name
|
||||||
val deviceAction = object : DeviceActionSpec<D, I, O> {
|
val deviceAction = object : DeviceActionSpec<D, I, O> {
|
||||||
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply {
|
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder)
|
||||||
fromSpec(property)
|
|
||||||
descriptorBuilder()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val inputConverter: MetaConverter<I> = inputConverter
|
override val inputConverter: MetaConverter<I> = inputConverter
|
||||||
override val outputConverter: MetaConverter<O> = outputConverter
|
override val outputConverter: MetaConverter<O> = outputConverter
|
||||||
@ -135,39 +173,68 @@ public abstract class DeviceSpec<D : Device> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that takes [Meta] and returns [Meta]. No conversions are done
|
||||||
|
*/
|
||||||
|
public fun metaAction(
|
||||||
|
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||||
|
name: String? = null,
|
||||||
|
execute: suspend D.(Meta) -> Meta,
|
||||||
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
|
||||||
|
action(
|
||||||
|
MetaConverter.Companion.meta,
|
||||||
|
MetaConverter.Companion.meta,
|
||||||
|
descriptorBuilder,
|
||||||
|
name
|
||||||
|
) {
|
||||||
|
execute(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that takes no parameters and returns no values
|
||||||
|
*/
|
||||||
|
public fun unitAction(
|
||||||
|
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||||
|
name: String? = null,
|
||||||
|
execute: suspend D.() -> Unit,
|
||||||
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Unit, Unit>>> =
|
||||||
|
action(
|
||||||
|
MetaConverter.Companion.unit,
|
||||||
|
MetaConverter.Companion.unit,
|
||||||
|
descriptorBuilder,
|
||||||
|
name
|
||||||
|
) {
|
||||||
|
execute()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* An action that takes no parameters and returns no values
|
|
||||||
*/
|
|
||||||
public fun <D : Device> DeviceSpec<D>.unitAction(
|
|
||||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
|
||||||
name: String? = null,
|
|
||||||
execute: suspend D.() -> Unit,
|
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Unit, Unit>>> =
|
|
||||||
action(
|
|
||||||
MetaConverter.Companion.unit,
|
|
||||||
MetaConverter.Companion.unit,
|
|
||||||
descriptorBuilder,
|
|
||||||
name
|
|
||||||
) {
|
|
||||||
execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An action that takes [Meta] and returns [Meta]. No conversions are done
|
* Register a mutable logical property for a device
|
||||||
*/
|
*/
|
||||||
public fun <D : Device> DeviceSpec<D>.metaAction(
|
@OptIn(InternalDeviceAPI::class)
|
||||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
execute: suspend D.(Meta) -> Meta,
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
|
PropertyDelegateProvider { _, property ->
|
||||||
action(
|
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
||||||
MetaConverter.Companion.meta,
|
val propertyName = name ?: property.name
|
||||||
MetaConverter.Companion.meta,
|
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
|
||||||
descriptorBuilder,
|
//TODO add type from converter
|
||||||
name
|
writable = true
|
||||||
) {
|
}.apply(descriptorBuilder)
|
||||||
execute(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
override val converter: MetaConverter<T> = converter
|
||||||
|
|
||||||
|
override suspend fun read(device: D): T? = device.getProperty(propertyName)?.let(converter::metaToObject)
|
||||||
|
|
||||||
|
override suspend fun write(device: D, value: T): Unit =
|
||||||
|
device.writeProperty(propertyName, converter.objectToMeta(value))
|
||||||
|
}
|
||||||
|
registerProperty(deviceProperty)
|
||||||
|
ReadOnlyProperty { _, _ ->
|
||||||
|
deviceProperty
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@ import kotlin.time.Duration
|
|||||||
/**
|
/**
|
||||||
* Perform a recurring asynchronous read action and return a flow of results.
|
* 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 caller context. To call it on device context, use `flowOn(coroutineContext)`.
|
* 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
|
* The flow is canceled when the device scope is canceled
|
||||||
*/
|
*/
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
package space.kscience.controls.spec
|
|
||||||
|
|
||||||
import space.kscience.controls.api.ActionDescriptor
|
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
|
|
||||||
public annotation class Description(val content: String)
|
|
||||||
|
|
||||||
internal expect fun PropertyDescriptor.fromSpec(property: KProperty<*>)
|
|
||||||
|
|
||||||
internal expect fun ActionDescriptor.fromSpec(property: KProperty<*>)
|
|
@ -1,6 +1,7 @@
|
|||||||
package space.kscience.controls.spec
|
package space.kscience.controls.spec
|
||||||
|
|
||||||
import space.kscience.dataforge.meta.*
|
import space.kscience.dataforge.meta.*
|
||||||
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
import kotlin.time.toDuration
|
import kotlin.time.toDuration
|
||||||
@ -9,14 +10,14 @@ public fun Double.asMeta(): Meta = Meta(asValue())
|
|||||||
|
|
||||||
//TODO to be moved to DF
|
//TODO to be moved to DF
|
||||||
public object DurationConverter : MetaConverter<Duration> {
|
public object DurationConverter : MetaConverter<Duration> {
|
||||||
override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS)
|
override fun metaToObject(meta: Meta): Duration = meta.value?.double?.toDuration(DurationUnit.SECONDS)
|
||||||
?: run {
|
?: run {
|
||||||
val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
|
val unit: DurationUnit = meta["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
|
||||||
val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration")
|
val value = meta[Meta.VALUE_KEY].double ?: error("No value present for Duration")
|
||||||
return@run value.toDuration(unit)
|
return@run value.toDuration(unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta()
|
override fun objectToMeta(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta()
|
||||||
}
|
}
|
||||||
|
|
||||||
public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter
|
public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter
|
@ -4,71 +4,22 @@ import space.kscience.controls.api.Device
|
|||||||
import space.kscience.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
import space.kscience.controls.api.metaDescriptor
|
import space.kscience.controls.api.metaDescriptor
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
import space.kscience.dataforge.meta.ValueType
|
import space.kscience.dataforge.meta.ValueType
|
||||||
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
import kotlin.properties.PropertyDelegateProvider
|
import kotlin.properties.PropertyDelegateProvider
|
||||||
import kotlin.properties.ReadOnlyProperty
|
import kotlin.properties.ReadOnlyProperty
|
||||||
import kotlin.reflect.KMutableProperty1
|
|
||||||
import kotlin.reflect.KProperty1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A read-only device property that delegates reading to a device [KProperty1]
|
|
||||||
*/
|
|
||||||
public fun <T, D : Device> DeviceSpec<D>.property(
|
|
||||||
converter: MetaConverter<T>,
|
|
||||||
readOnlyProperty: KProperty1<D, T>,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = property(
|
|
||||||
converter,
|
|
||||||
descriptorBuilder,
|
|
||||||
name = readOnlyProperty.name,
|
|
||||||
read = { readOnlyProperty.get(this) }
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mutable property that delegates reading and writing to a device [KMutableProperty1]
|
|
||||||
*/
|
|
||||||
public fun <T, D : Device> DeviceSpec<D>.mutableProperty(
|
|
||||||
converter: MetaConverter<T>,
|
|
||||||
readWriteProperty: KMutableProperty1<D, T>,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
|
|
||||||
mutableProperty(
|
|
||||||
converter,
|
|
||||||
descriptorBuilder,
|
|
||||||
readWriteProperty.name,
|
|
||||||
read = { _ -> readWriteProperty.get(this) },
|
|
||||||
write = { _, value: T -> readWriteProperty.set(this, value) }
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a mutable logical property (without a corresponding physical state) for a device
|
|
||||||
*/
|
|
||||||
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty(
|
|
||||||
converter: MetaConverter<T>,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
name: String? = null,
|
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
|
|
||||||
mutableProperty(
|
|
||||||
converter,
|
|
||||||
descriptorBuilder,
|
|
||||||
name,
|
|
||||||
read = { propertyName -> getProperty(propertyName)?.let(converter::readOrNull) },
|
|
||||||
write = { propertyName, value -> writeProperty(propertyName, converter.convert(value)) }
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
//read only delegates
|
//read only delegates
|
||||||
|
|
||||||
public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> Boolean?
|
read: suspend D.() -> Boolean?
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
|
||||||
MetaConverter.boolean,
|
MetaConverter.boolean,
|
||||||
{
|
{
|
||||||
metaDescriptor {
|
metaDescriptor {
|
||||||
valueType(ValueType.BOOLEAN)
|
type(ValueType.BOOLEAN)
|
||||||
}
|
}
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
},
|
},
|
||||||
@ -80,15 +31,15 @@ private inline fun numberDescriptor(
|
|||||||
crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}
|
crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}
|
||||||
): PropertyDescriptor.() -> Unit = {
|
): PropertyDescriptor.() -> Unit = {
|
||||||
metaDescriptor {
|
metaDescriptor {
|
||||||
valueType(ValueType.NUMBER)
|
type(ValueType.NUMBER)
|
||||||
}
|
}
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <D : Device> DeviceSpec<D>.numberProperty(
|
public fun <D : Device> DeviceSpec<D>.numberProperty(
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> Number?
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
read: suspend D.() -> Number?
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
|
||||||
MetaConverter.number,
|
MetaConverter.number,
|
||||||
numberDescriptor(descriptorBuilder),
|
numberDescriptor(descriptorBuilder),
|
||||||
@ -99,7 +50,7 @@ public fun <D : Device> DeviceSpec<D>.numberProperty(
|
|||||||
public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> Double?
|
read: suspend D.() -> Double?
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
|
||||||
MetaConverter.double,
|
MetaConverter.double,
|
||||||
numberDescriptor(descriptorBuilder),
|
numberDescriptor(descriptorBuilder),
|
||||||
@ -110,12 +61,12 @@ public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
|||||||
public fun <D : Device> DeviceSpec<D>.stringProperty(
|
public fun <D : Device> DeviceSpec<D>.stringProperty(
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> String?
|
read: suspend D.() -> String?
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
|
||||||
MetaConverter.string,
|
MetaConverter.string,
|
||||||
{
|
{
|
||||||
metaDescriptor {
|
metaDescriptor {
|
||||||
valueType(ValueType.STRING)
|
type(ValueType.STRING)
|
||||||
}
|
}
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
},
|
},
|
||||||
@ -126,12 +77,12 @@ public fun <D : Device> DeviceSpec<D>.stringProperty(
|
|||||||
public fun <D : Device> DeviceSpec<D>.metaProperty(
|
public fun <D : Device> DeviceSpec<D>.metaProperty(
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> Meta?
|
read: suspend D.() -> Meta?
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
|
||||||
MetaConverter.meta,
|
MetaConverter.meta,
|
||||||
{
|
{
|
||||||
metaDescriptor {
|
metaDescriptor {
|
||||||
valueType(ValueType.STRING)
|
type(ValueType.STRING)
|
||||||
}
|
}
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
},
|
},
|
||||||
@ -144,14 +95,14 @@ public fun <D : Device> DeviceSpec<D>.metaProperty(
|
|||||||
public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> Boolean?,
|
read: suspend D.() -> Boolean?,
|
||||||
write: suspend D.(propertyName: String, value: Boolean) -> Unit
|
write: suspend D.(Boolean) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Boolean>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
|
||||||
mutableProperty(
|
mutableProperty(
|
||||||
MetaConverter.boolean,
|
MetaConverter.boolean,
|
||||||
{
|
{
|
||||||
metaDescriptor {
|
metaDescriptor {
|
||||||
valueType(ValueType.BOOLEAN)
|
type(ValueType.BOOLEAN)
|
||||||
}
|
}
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
},
|
},
|
||||||
@ -164,31 +115,31 @@ public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
|||||||
public fun <D : Device> DeviceSpec<D>.numberProperty(
|
public fun <D : Device> DeviceSpec<D>.numberProperty(
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> Number,
|
read: suspend D.() -> Number,
|
||||||
write: suspend D.(propertyName: String, value: Number) -> Unit
|
write: suspend D.(Number) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Number>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> =
|
||||||
mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
|
mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
|
||||||
|
|
||||||
public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> Double,
|
read: suspend D.() -> Double,
|
||||||
write: suspend D.(propertyName: String, value: Double) -> Unit
|
write: suspend D.(Double) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Double>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> =
|
||||||
mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
|
mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
|
||||||
|
|
||||||
public fun <D : Device> DeviceSpec<D>.stringProperty(
|
public fun <D : Device> DeviceSpec<D>.stringProperty(
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> String,
|
read: suspend D.() -> String,
|
||||||
write: suspend D.(propertyName: String, value: String) -> Unit
|
write: suspend D.(String) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, String>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> =
|
||||||
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
|
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
|
||||||
|
|
||||||
public fun <D : Device> DeviceSpec<D>.metaProperty(
|
public fun <D : Device> DeviceSpec<D>.metaProperty(
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
read: suspend D.(propertyName: String) -> Meta,
|
read: suspend D.() -> Meta,
|
||||||
write: suspend D.(propertyName: String, value: Meta) -> Unit
|
write: suspend D.(Meta) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Meta>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Meta>>> =
|
||||||
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)
|
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)
|
@ -1,9 +0,0 @@
|
|||||||
package space.kscience.controls.spec
|
|
||||||
|
|
||||||
import space.kscience.controls.api.ActionDescriptor
|
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>){}
|
|
||||||
|
|
||||||
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){}
|
|
@ -1,20 +1,19 @@
|
|||||||
package space.kscience.controls.ports
|
package space.kscience.controls.ports
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.error
|
||||||
|
import space.kscience.dataforge.context.info
|
||||||
|
import space.kscience.dataforge.context.logger
|
||||||
import space.kscience.dataforge.meta.*
|
import space.kscience.dataforge.meta.*
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.channels.AsynchronousCloseException
|
|
||||||
import java.nio.channels.ByteChannel
|
import java.nio.channels.ByteChannel
|
||||||
import java.nio.channels.DatagramChannel
|
import java.nio.channels.DatagramChannel
|
||||||
import java.nio.channels.SocketChannel
|
import java.nio.channels.SocketChannel
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
/**
|
public fun ByteBuffer.toArray(limit: Int = limit()): ByteArray {
|
||||||
* Copy the contents of this buffer to an array
|
|
||||||
*/
|
|
||||||
public fun ByteBuffer.copyToArray(limit: Int = limit()): ByteArray {
|
|
||||||
rewind()
|
rewind()
|
||||||
val response = ByteArray(limit)
|
val response = ByteArray(limit)
|
||||||
get(response)
|
get(response)
|
||||||
@ -27,41 +26,32 @@ public fun ByteBuffer.copyToArray(limit: Int = limit()): ByteArray {
|
|||||||
*/
|
*/
|
||||||
public class ChannelPort(
|
public class ChannelPort(
|
||||||
context: Context,
|
context: Context,
|
||||||
meta: Meta,
|
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
channelBuilder: suspend () -> ByteChannel,
|
channelBuilder: suspend () -> ByteChannel,
|
||||||
) : AbstractAsynchronousPort(context, meta, coroutineContext), AutoCloseable {
|
) : AbstractPort(context, coroutineContext), AutoCloseable {
|
||||||
|
|
||||||
|
private val futureChannel: Deferred<ByteChannel> = this.scope.async(Dispatchers.IO) {
|
||||||
|
channelBuilder()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A handler to await port connection
|
* A handler to await port connection
|
||||||
*/
|
*/
|
||||||
private val futureChannel: Deferred<ByteChannel> = scope.async(Dispatchers.IO, start = CoroutineStart.LAZY) {
|
public val startJob: Job get() = futureChannel
|
||||||
channelBuilder()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var listenerJob: Job? = null
|
private val listenerJob = this.scope.launch(Dispatchers.IO) {
|
||||||
|
val channel = futureChannel.await()
|
||||||
override val isOpen: Boolean get() = listenerJob?.isActive == true
|
val buffer = ByteBuffer.allocate(1024)
|
||||||
|
while (isActive) {
|
||||||
override fun onOpen() {
|
try {
|
||||||
listenerJob = scope.launch(Dispatchers.IO) {
|
val num = channel.read(buffer)
|
||||||
val channel = futureChannel.await()
|
if (num > 0) {
|
||||||
val buffer = ByteBuffer.allocate(1024)
|
receive(buffer.toArray(num))
|
||||||
while (isActive && channel.isOpen) {
|
|
||||||
try {
|
|
||||||
val num = channel.read(buffer)
|
|
||||||
if (num > 0) {
|
|
||||||
receive(buffer.copyToArray(num))
|
|
||||||
}
|
|
||||||
if (num < 0) cancel("The input channel is exhausted")
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (ex is AsynchronousCloseException) {
|
|
||||||
logger.info { "Channel $channel closed" }
|
|
||||||
} else {
|
|
||||||
logger.error(ex) { "Channel read error, retrying in 1 second" }
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (num < 0) cancel("The input channel is exhausted")
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
logger.error(ex) { "Channel read error" }
|
||||||
|
delay(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,86 +62,46 @@ public class ChannelPort(
|
|||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override fun close() {
|
override fun close() {
|
||||||
listenerJob?.cancel()
|
listenerJob.cancel()
|
||||||
if (futureChannel.isCompleted) {
|
if (futureChannel.isCompleted) {
|
||||||
futureChannel.getCompleted().close()
|
futureChannel.getCompleted().close()
|
||||||
|
} else {
|
||||||
|
futureChannel.cancel()
|
||||||
}
|
}
|
||||||
super.close()
|
super.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Factory] for TCP connections
|
* A [PortFactory] for TCP connections
|
||||||
*/
|
*/
|
||||||
public object TcpPort : Factory<AsynchronousPort> {
|
public object TcpPort : PortFactory {
|
||||||
|
|
||||||
public fun build(
|
override val type: String = "tcp"
|
||||||
context: Context,
|
|
||||||
host: String,
|
|
||||||
port: Int,
|
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
|
||||||
): ChannelPort {
|
|
||||||
val meta = Meta {
|
|
||||||
"name" put "tcp://$host:$port"
|
|
||||||
"type" put "tcp"
|
|
||||||
"host" put host
|
|
||||||
"port" put port
|
|
||||||
}
|
|
||||||
return ChannelPort(context, meta, coroutineContext) {
|
|
||||||
SocketChannel.open(InetSocketAddress(host, port))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and open TCP port
|
|
||||||
*/
|
|
||||||
public fun open(
|
public fun open(
|
||||||
context: Context,
|
context: Context,
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
): ChannelPort = build(context, host, port, coroutineContext).apply { open() }
|
): ChannelPort = ChannelPort(context, coroutineContext) {
|
||||||
|
SocketChannel.open(InetSocketAddress(host, port))
|
||||||
|
}
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): ChannelPort {
|
override fun build(context: Context, meta: Meta): ChannelPort {
|
||||||
val host = meta["host"].string ?: "localhost"
|
val host = meta["host"].string ?: "localhost"
|
||||||
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
|
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
|
||||||
return build(context, host, port)
|
return open(context, host, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Factory] for UDP connections
|
* A [PortFactory] for UDP connections
|
||||||
*/
|
*/
|
||||||
public object UdpPort : Factory<AsynchronousPort> {
|
public object UdpPort : PortFactory {
|
||||||
|
|
||||||
public fun build(
|
override val type: String = "udp"
|
||||||
context: Context,
|
|
||||||
remoteHost: String,
|
|
||||||
remotePort: Int,
|
|
||||||
localPort: Int? = null,
|
|
||||||
localHost: String? = null,
|
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
|
||||||
): ChannelPort {
|
|
||||||
val meta = Meta {
|
|
||||||
"name" put "udp://$remoteHost:$remotePort"
|
|
||||||
"type" put "udp"
|
|
||||||
"remoteHost" put remoteHost
|
|
||||||
"remotePort" put remotePort
|
|
||||||
localHost?.let { "localHost" put it }
|
|
||||||
localPort?.let { "localPort" put it }
|
|
||||||
}
|
|
||||||
return ChannelPort(context, meta, coroutineContext) {
|
|
||||||
DatagramChannel.open().apply {
|
|
||||||
//bind the channel to a local port to receive messages
|
|
||||||
localPort?.let { bind(InetSocketAddress(localHost ?: "localhost", it)) }
|
|
||||||
//connect to remote port to send messages
|
|
||||||
connect(InetSocketAddress(remoteHost, remotePort.toInt()))
|
|
||||||
context.logger.info { "Connected to UDP $remotePort on $remoteHost" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect a datagram channel to a remote host/port. If [localPort] is provided, it is used to bind local port for receiving messages.
|
* Connect a datagram channel to a remote host/port. If [localPort] is provided, it is used to bind local port for receiving messages.
|
||||||
@ -162,14 +112,22 @@ public object UdpPort : Factory<AsynchronousPort> {
|
|||||||
remotePort: Int,
|
remotePort: Int,
|
||||||
localPort: Int? = null,
|
localPort: Int? = null,
|
||||||
localHost: String = "localhost",
|
localHost: String = "localhost",
|
||||||
): ChannelPort = build(context, remoteHost, remotePort, localPort, localHost).apply { open() }
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
|
): ChannelPort = ChannelPort(context, coroutineContext) {
|
||||||
|
DatagramChannel.open().apply {
|
||||||
|
//bind the channel to a local port to receive messages
|
||||||
|
localPort?.let { bind(InetSocketAddress(localHost, localPort)) }
|
||||||
|
//connect to remote port to send messages
|
||||||
|
connect(InetSocketAddress(remoteHost, remotePort))
|
||||||
|
context.logger.info { "Connected to UDP $remotePort on $remoteHost" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): ChannelPort {
|
override fun build(context: Context, meta: Meta): ChannelPort {
|
||||||
val remoteHost by meta.string { error("Remote host is not specified") }
|
val remoteHost by meta.string { error("Remote host is not specified") }
|
||||||
val remotePort by meta.number { error("Remote port is not specified") }
|
val remotePort by meta.number { error("Remote port is not specified") }
|
||||||
val localHost: String? by meta.string()
|
val localHost: String? by meta.string()
|
||||||
val localPort: Int? by meta.int()
|
val localPort: Int? by meta.int()
|
||||||
return build(context, remoteHost, remotePort.toInt(), localPort, localHost)
|
return open(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost")
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,7 +6,7 @@ import space.kscience.dataforge.context.PluginFactory
|
|||||||
import space.kscience.dataforge.context.PluginTag
|
import space.kscience.dataforge.context.PluginTag
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.asName
|
import space.kscience.dataforge.names.parseAsName
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A plugin for loading JVM nio-based ports
|
* A plugin for loading JVM nio-based ports
|
||||||
@ -17,9 +17,9 @@ public class JvmPortsPlugin : AbstractPlugin() {
|
|||||||
override val tag: PluginTag get() = Companion.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = when(target){
|
override fun content(target: String): Map<Name, Any> = when(target){
|
||||||
Ports.ASYNCHRONOUS_PORT_TYPE -> mapOf(
|
PortFactory.TYPE -> mapOf(
|
||||||
"tcp".asName() to TcpPort,
|
TcpPort.type.parseAsName() to TcpPort,
|
||||||
"udp".asName() to UdpPort
|
UdpPort.type.parseAsName() to UdpPort
|
||||||
)
|
)
|
||||||
else -> emptyMap()
|
else -> emptyMap()
|
||||||
}
|
}
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
package space.kscience.controls.ports
|
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import java.net.DatagramPacket
|
|
||||||
import java.net.DatagramSocket
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A port based on [DatagramSocket] for cases, where [ChannelPort] does not work for some reason
|
|
||||||
*/
|
|
||||||
public class UdpSocketPort(
|
|
||||||
override val context: Context,
|
|
||||||
meta: Meta,
|
|
||||||
private val socket: DatagramSocket,
|
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
|
||||||
) : AbstractAsynchronousPort(context, meta, coroutineContext) {
|
|
||||||
|
|
||||||
private var listenerJob: Job? = null
|
|
||||||
|
|
||||||
override fun onOpen() {
|
|
||||||
listenerJob = context.launch(Dispatchers.IO) {
|
|
||||||
while (isActive) {
|
|
||||||
val buf = ByteArray(socket.receiveBufferSize)
|
|
||||||
|
|
||||||
val packet = DatagramPacket(
|
|
||||||
buf,
|
|
||||||
buf.size,
|
|
||||||
)
|
|
||||||
socket.receive(packet)
|
|
||||||
|
|
||||||
val bytes = packet.data.copyOfRange(
|
|
||||||
packet.offset,
|
|
||||||
packet.offset + packet.length
|
|
||||||
)
|
|
||||||
receive(bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
listenerJob?.cancel()
|
|
||||||
super.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val isOpen: Boolean get() = listenerJob?.isActive == true
|
|
||||||
|
|
||||||
|
|
||||||
override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) {
|
|
||||||
val packet = DatagramPacket(
|
|
||||||
data,
|
|
||||||
data.size,
|
|
||||||
socket.remoteSocketAddress
|
|
||||||
)
|
|
||||||
socket.send(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package space.kscience.controls.spec
|
|
||||||
|
|
||||||
import space.kscience.controls.api.ActionDescriptor
|
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
import kotlin.reflect.full.findAnnotation
|
|
||||||
|
|
||||||
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {
|
|
||||||
property.findAnnotation<Description>()?.let {
|
|
||||||
description = it.content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){
|
|
||||||
property.findAnnotation<Description>()?.let {
|
|
||||||
description = it.content
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
package space.kscience.controls.ports
|
|
||||||
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.take
|
|
||||||
import kotlinx.coroutines.flow.toList
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import space.kscience.dataforge.context.Global
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
|
|
||||||
internal class AsynchronousPortIOTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testDelimiteredByteArrayFlow() {
|
|
||||||
val flow = flowOf("bb?b", "ddd?", ":defgb?:ddf", "34fb?:--").map { it.encodeToByteArray() }
|
|
||||||
val chunked = flow.withDelimiter("?:".encodeToByteArray())
|
|
||||||
runBlocking {
|
|
||||||
val result = chunked.toList()
|
|
||||||
assertEquals(3, result.size)
|
|
||||||
assertEquals("bb?bddd?:", result[0].decodeToString())
|
|
||||||
assertEquals("defgb?:", result[1].decodeToString())
|
|
||||||
assertEquals("ddf34fb?:", result[2].decodeToString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testUdpCommunication() = runTest {
|
|
||||||
val receiver = UdpPort.open(Global, "localhost", 8811, localPort = 8812)
|
|
||||||
val sender = UdpPort.open(Global, "localhost", 8812, localPort = 8811)
|
|
||||||
|
|
||||||
delay(30)
|
|
||||||
repeat(10) {
|
|
||||||
sender.send("Line number $it\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
val res = receiver
|
|
||||||
.subscribe()
|
|
||||||
.withStringDelimiter("\n")
|
|
||||||
.take(10)
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
assertEquals("Line number 3", res[3].trim())
|
|
||||||
receiver.close()
|
|
||||||
sender.close()
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,25 @@
|
|||||||
|
package space.kscience.controls.ports
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
|
||||||
|
internal class PortIOTest{
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDelimiteredByteArrayFlow(){
|
||||||
|
val flow = flowOf("bb?b","ddd?",":defgb?:ddf","34fb?:--").map { it.encodeToByteArray() }
|
||||||
|
val chunked = flow.withDelimiter("?:".encodeToByteArray())
|
||||||
|
runBlocking {
|
||||||
|
val result = chunked.toList()
|
||||||
|
assertEquals(3, result.size)
|
||||||
|
assertEquals("bb?bddd?:",result[0].decodeToString())
|
||||||
|
assertEquals("defgb?:", result[1].decodeToString())
|
||||||
|
assertEquals("ddf34fb?:", result[2].decodeToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
package space.kscience.controls.spec
|
|
||||||
|
|
||||||
import space.kscience.controls.api.ActionDescriptor
|
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {}
|
|
||||||
|
|
||||||
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){}
|
|
@ -1,21 +0,0 @@
|
|||||||
# Module controls-jupyter
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
## Artifact:
|
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-jupyter:0.3.0`.
|
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
|
||||||
```kotlin
|
|
||||||
repositories {
|
|
||||||
maven("https://repo.kotlin.link")
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("space.kscience:controls-jupyter:0.3.0")
|
|
||||||
}
|
|
||||||
```
|
|
@ -1,8 +0,0 @@
|
|||||||
public final class space/kscience/controls/jupyter/ControlsJupyter : space/kscience/visionforge/jupyter/VisionForgeIntegration {
|
|
||||||
public static final field Companion Lspace/kscience/controls/jupyter/ControlsJupyter$Companion;
|
|
||||||
public fun <init> ()V
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class space/kscience/controls/jupyter/ControlsJupyter$Companion {
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("space.kscience.gradle.mpp")
|
|
||||||
`maven-publish`
|
|
||||||
}
|
|
||||||
|
|
||||||
kscience {
|
|
||||||
fullStack("js/controls-jupyter.js")
|
|
||||||
useKtor()
|
|
||||||
useContextReceivers()
|
|
||||||
jupyterLibrary("space.kscience.controls.jupyter.ControlsJupyter")
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.controlsVision)
|
|
||||||
implementation(libs.visionforge.jupiter)
|
|
||||||
}
|
|
||||||
jvmMain {
|
|
||||||
implementation(spclibs.logback.classic)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
import space.kscience.visionforge.html.runVisionClient
|
|
||||||
import space.kscience.visionforge.jupyter.VFNotebookClient
|
|
||||||
import space.kscience.visionforge.markup.MarkupPlugin
|
|
||||||
import space.kscience.visionforge.plotly.PlotlyPlugin
|
|
||||||
|
|
||||||
public fun main(): Unit = runVisionClient {
|
|
||||||
// plugin(DeviceManager)
|
|
||||||
// plugin(ClockManager)
|
|
||||||
plugin(PlotlyPlugin)
|
|
||||||
plugin(MarkupPlugin)
|
|
||||||
// plugin(TableVisionJsPlugin)
|
|
||||||
plugin(VFNotebookClient)
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
|||||||
package space.kscience.controls.jupyter
|
|
||||||
|
|
||||||
import org.jetbrains.kotlinx.jupyter.api.declare
|
|
||||||
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
|
|
||||||
import space.kscience.controls.manager.ClockManager
|
|
||||||
import space.kscience.controls.manager.DeviceManager
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
|
||||||
import space.kscience.plotly.Plot
|
|
||||||
import space.kscience.visionforge.jupyter.VisionForge
|
|
||||||
import space.kscience.visionforge.jupyter.VisionForgeIntegration
|
|
||||||
import space.kscience.visionforge.markup.MarkupPlugin
|
|
||||||
import space.kscience.visionforge.plotly.PlotlyPlugin
|
|
||||||
import space.kscience.visionforge.plotly.asVision
|
|
||||||
import space.kscience.visionforge.visionManager
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(DFExperimental::class)
|
|
||||||
public class ControlsJupyter : VisionForgeIntegration(CONTEXT.visionManager) {
|
|
||||||
|
|
||||||
override fun Builder.afterLoaded(vf: VisionForge) {
|
|
||||||
|
|
||||||
resources {
|
|
||||||
js("controls-jupyter") {
|
|
||||||
classPath("js/controls-jupyter.js")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoaded {
|
|
||||||
declare("context" to CONTEXT)
|
|
||||||
}
|
|
||||||
|
|
||||||
import(
|
|
||||||
"kotlin.time.*",
|
|
||||||
"kotlin.time.Duration.Companion.milliseconds",
|
|
||||||
"kotlin.time.Duration.Companion.seconds",
|
|
||||||
// "space.kscience.tables.*",
|
|
||||||
"space.kscience.dataforge.meta.*",
|
|
||||||
"space.kscience.dataforge.context.*",
|
|
||||||
"space.kscience.plotly.*",
|
|
||||||
"space.kscience.plotly.models.*",
|
|
||||||
"space.kscience.visionforge.plotly.*",
|
|
||||||
"space.kscience.controls.manager.*",
|
|
||||||
"space.kscience.controls.constructor.*",
|
|
||||||
"space.kscience.controls.vision.*",
|
|
||||||
"space.kscience.controls.spec.*"
|
|
||||||
)
|
|
||||||
|
|
||||||
// render<Table<*>> { table ->
|
|
||||||
// vf.produceHtml {
|
|
||||||
// vision { table.toVision() }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
render<Plot> { plot ->
|
|
||||||
vf.produceHtml {
|
|
||||||
vision { plot.asVision() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public companion object {
|
|
||||||
private val CONTEXT: Context = Context("controls-jupyter") {
|
|
||||||
plugin(DeviceManager)
|
|
||||||
plugin(ClockManager)
|
|
||||||
plugin(PlotlyPlugin)
|
|
||||||
// plugin(TableVisionPlugin)
|
|
||||||
plugin(MarkupPlugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,16 +12,18 @@ Magix service for binding controls devices (both as RPC client and server)
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-magix:0.3.0`.
|
The Maven coordinates of this project are `space.kscience:controls-magix:0.2.0`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
//uncomment to access development builds
|
||||||
|
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-magix:0.3.0")
|
implementation("space.kscience:controls-magix:0.2.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -12,7 +12,6 @@ description = """
|
|||||||
kscience {
|
kscience {
|
||||||
jvm()
|
jvm()
|
||||||
js()
|
js()
|
||||||
useCoroutines("1.8.0")
|
|
||||||
useSerialization {
|
useSerialization {
|
||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
package space.kscience.controls.client
|
package space.kscience.controls.client
|
||||||
|
|
||||||
import com.benasher44.uuid.uuid4
|
import com.benasher44.uuid.uuid4
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.newCoroutineContext
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import space.kscience.controls.api.*
|
import space.kscience.controls.api.*
|
||||||
import space.kscience.controls.manager.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
import space.kscience.controls.spec.DevicePropertySpec
|
|
||||||
import space.kscience.controls.spec.name
|
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
import space.kscience.dataforge.misc.DFExperimental
|
||||||
@ -28,10 +26,10 @@ public class DeviceClient(
|
|||||||
private val deviceName: Name,
|
private val deviceName: Name,
|
||||||
incomingFlow: Flow<DeviceMessage>,
|
incomingFlow: Flow<DeviceMessage>,
|
||||||
private val send: suspend (DeviceMessage) -> Unit,
|
private val send: suspend (DeviceMessage) -> Unit,
|
||||||
) : CachingDevice {
|
) : Device {
|
||||||
|
|
||||||
|
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||||
override val coroutineContext: CoroutineContext = context.coroutineContext + Job(context.coroutineContext[Job])
|
override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext)
|
||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
@ -101,82 +99,19 @@ public class DeviceClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@DFExperimental
|
@DFExperimental
|
||||||
override val lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STARTED
|
override val lifecycleState: DeviceLifecycleState = DeviceLifecycleState.OPEN
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to a remote device via this endpoint.
|
* Connect to a remote device via this endpoint.
|
||||||
*
|
*
|
||||||
* @param context a [Context] to run device in
|
* @param context a [Context] to run device in
|
||||||
* @param sourceEndpointName the name of this endpoint
|
* @param endpointName the name of endpoint in Magix to connect to
|
||||||
* @param targetEndpointName the name of endpoint in Magix to connect to
|
|
||||||
* @param deviceName the name of device within endpoint
|
* @param deviceName the name of device within endpoint
|
||||||
*/
|
*/
|
||||||
public fun MagixEndpoint.remoteDevice(
|
public fun MagixEndpoint.remoteDevice(context: Context, endpointName: String, deviceName: Name): DeviceClient {
|
||||||
context: Context,
|
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(endpointName)).map { it.second }
|
||||||
sourceEndpointName: String,
|
|
||||||
targetEndpointName: String,
|
|
||||||
deviceName: Name,
|
|
||||||
): DeviceClient {
|
|
||||||
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(targetEndpointName)).map { it.second }
|
|
||||||
return DeviceClient(context, deviceName, subscription) {
|
return DeviceClient(context, deviceName, subscription) {
|
||||||
send(
|
send(DeviceManager.magixFormat, it, endpointName, id = stringUID())
|
||||||
format = DeviceManager.magixFormat,
|
|
||||||
payload = it,
|
|
||||||
source = sourceEndpointName,
|
|
||||||
target = targetEndpointName,
|
|
||||||
id = stringUID()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe on specific property of a device without creating a device
|
|
||||||
*/
|
|
||||||
public fun <T> MagixEndpoint.controlsPropertyFlow(
|
|
||||||
endpointName: String,
|
|
||||||
deviceName: Name,
|
|
||||||
propertySpec: DevicePropertySpec<*, T>,
|
|
||||||
): Flow<T> {
|
|
||||||
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(endpointName)).map { it.second }
|
|
||||||
|
|
||||||
return subscription.filterIsInstance<PropertyChangedMessage>()
|
|
||||||
.filter { message ->
|
|
||||||
message.sourceDevice == deviceName && message.property == propertySpec.name
|
|
||||||
}.map {
|
|
||||||
propertySpec.converter.read(it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun <T> MagixEndpoint.sendControlsPropertyChange(
|
|
||||||
sourceEndpointName: String,
|
|
||||||
targetEndpointName: String,
|
|
||||||
deviceName: Name,
|
|
||||||
propertySpec: DevicePropertySpec<*, T>,
|
|
||||||
value: T,
|
|
||||||
) {
|
|
||||||
val message = PropertySetMessage(
|
|
||||||
property = propertySpec.name,
|
|
||||||
value = propertySpec.converter.convert(value),
|
|
||||||
targetDevice = deviceName
|
|
||||||
)
|
|
||||||
send(DeviceManager.magixFormat, message, source = sourceEndpointName, target = targetEndpointName)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe on property change messages together with property values
|
|
||||||
*/
|
|
||||||
public fun <T> MagixEndpoint.controlsPropertyMessageFlow(
|
|
||||||
endpointName: String,
|
|
||||||
deviceName: Name,
|
|
||||||
propertySpec: DevicePropertySpec<*, T>,
|
|
||||||
): Flow<Pair<PropertyChangedMessage, T>> {
|
|
||||||
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(endpointName)).map { it.second }
|
|
||||||
|
|
||||||
return subscription.filterIsInstance<PropertyChangedMessage>()
|
|
||||||
.filter { message ->
|
|
||||||
message.sourceDevice == deviceName && message.property == propertySpec.name
|
|
||||||
}.map {
|
|
||||||
it to propertySpec.converter.read(it.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,79 +0,0 @@
|
|||||||
package space.kscience.controls.client
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import space.kscience.controls.api.PropertyChangedMessage
|
|
||||||
import space.kscience.controls.api.getOrReadProperty
|
|
||||||
import space.kscience.controls.spec.DeviceActionSpec
|
|
||||||
import space.kscience.controls.spec.DevicePropertySpec
|
|
||||||
import space.kscience.controls.spec.MutableDevicePropertySpec
|
|
||||||
import space.kscience.controls.spec.name
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An accessor that allows DeviceClient to connect to any property without type checks
|
|
||||||
*/
|
|
||||||
public suspend fun <T> DeviceClient.read(propertySpec: DevicePropertySpec<*, T>): T =
|
|
||||||
propertySpec.converter.readOrNull(readProperty(propertySpec.name)) ?: error("Property read result is not valid")
|
|
||||||
|
|
||||||
|
|
||||||
public suspend fun <T> DeviceClient.request(propertySpec: DevicePropertySpec<*, T>): T =
|
|
||||||
propertySpec.converter.read(getOrReadProperty(propertySpec.name))
|
|
||||||
|
|
||||||
public suspend fun <T> DeviceClient.write(propertySpec: MutableDevicePropertySpec<*, T>, value: T) {
|
|
||||||
writeProperty(propertySpec.name, propertySpec.converter.convert(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <T> DeviceClient.writeAsync(propertySpec: MutableDevicePropertySpec<*, T>, value: T): Job = launch {
|
|
||||||
write(propertySpec, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <T> DeviceClient.propertyFlow(spec: DevicePropertySpec<*, T>): Flow<T> = messageFlow
|
|
||||||
.filterIsInstance<PropertyChangedMessage>()
|
|
||||||
.filter { it.property == spec.name }
|
|
||||||
.mapNotNull { spec.converter.readOrNull(it.value) }
|
|
||||||
|
|
||||||
public fun <T> DeviceClient.onPropertyChange(
|
|
||||||
spec: DevicePropertySpec<*, T>,
|
|
||||||
scope: CoroutineScope = this,
|
|
||||||
callback: suspend PropertyChangedMessage.(T) -> Unit,
|
|
||||||
): Job = messageFlow
|
|
||||||
.filterIsInstance<PropertyChangedMessage>()
|
|
||||||
.filter { it.property == spec.name }
|
|
||||||
.onEach { change ->
|
|
||||||
val newValue = spec.converter.readOrNull(change.value)
|
|
||||||
if (newValue != null) {
|
|
||||||
change.callback(newValue)
|
|
||||||
}
|
|
||||||
}.launchIn(scope)
|
|
||||||
|
|
||||||
public fun <T> DeviceClient.useProperty(
|
|
||||||
spec: DevicePropertySpec<*, T>,
|
|
||||||
scope: CoroutineScope = this,
|
|
||||||
callback: suspend (T) -> Unit,
|
|
||||||
): Job = scope.launch {
|
|
||||||
callback(read(spec))
|
|
||||||
messageFlow
|
|
||||||
.filterIsInstance<PropertyChangedMessage>()
|
|
||||||
.filter { it.property == spec.name }
|
|
||||||
.collect { change ->
|
|
||||||
val newValue = spec.converter.readOrNull(change.value)
|
|
||||||
if (newValue != null) {
|
|
||||||
callback(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun <I, O> DeviceClient.execute(actionSpec: DeviceActionSpec<*, I, O>, input: I): O {
|
|
||||||
val inputMeta = actionSpec.inputConverter.convert(input)
|
|
||||||
val res = execute(actionSpec.name, inputMeta)
|
|
||||||
return actionSpec.outputConverter.read(res ?: Meta.EMPTY)
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun <O> DeviceClient.execute(actionSpec: DeviceActionSpec<*, Unit, O>): O {
|
|
||||||
val res = execute(actionSpec.name, Meta.EMPTY)
|
|
||||||
return actionSpec.outputConverter.read(res ?: Meta.EMPTY)
|
|
||||||
}
|
|
@ -12,8 +12,6 @@ import space.kscience.controls.manager.respondHubMessage
|
|||||||
import space.kscience.dataforge.context.error
|
import space.kscience.dataforge.context.error
|
||||||
import space.kscience.dataforge.context.logger
|
import space.kscience.dataforge.context.logger
|
||||||
import space.kscience.magix.api.*
|
import space.kscience.magix.api.*
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
|
|
||||||
internal val controlsMagixFormat: MagixFormat<DeviceMessage> = MagixFormat(
|
internal val controlsMagixFormat: MagixFormat<DeviceMessage> = MagixFormat(
|
||||||
@ -34,20 +32,17 @@ internal fun generateId(request: MagixMessage): String = if (request.id != null)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Communicate with server in [Magix format](https://github.com/waltz-controls/rfc/tree/master/1)
|
* Communicate with server in [Magix format](https://github.com/waltz-controls/rfc/tree/master/1)
|
||||||
*
|
|
||||||
* Accepts messages with target that equals [endpointID] or null (broadcast messages)
|
|
||||||
*/
|
*/
|
||||||
public fun DeviceManager.launchMagixService(
|
public fun DeviceManager.launchMagixService(
|
||||||
endpoint: MagixEndpoint,
|
endpoint: MagixEndpoint,
|
||||||
endpointID: String = controlsMagixFormat.defaultFormat,
|
endpointID: String = controlsMagixFormat.defaultFormat,
|
||||||
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
): Job = context.launch {
|
||||||
): Job = context.launch(coroutineContext) {
|
endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID)).onEach { (request, payload) ->
|
||||||
endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID, null)).onEach { (request, payload) ->
|
|
||||||
val responsePayload = respondHubMessage(payload)
|
val responsePayload = respondHubMessage(payload)
|
||||||
responsePayload.forEach {
|
if (responsePayload != null) {
|
||||||
endpoint.send(
|
endpoint.send(
|
||||||
format = controlsMagixFormat,
|
format = controlsMagixFormat,
|
||||||
payload = it,
|
payload = responsePayload,
|
||||||
source = endpointID,
|
source = endpointID,
|
||||||
target = request.sourceEndpoint,
|
target = request.sourceEndpoint,
|
||||||
id = generateId(request),
|
id = generateId(request),
|
||||||
@ -58,7 +53,7 @@ public fun DeviceManager.launchMagixService(
|
|||||||
logger.error(error) { "Error while responding to message: ${error.message}" }
|
logger.error(error) { "Error while responding to message: ${error.message}" }
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
|
|
||||||
hubMessageFlow().onEach { payload ->
|
hubMessageFlow(this).onEach { payload ->
|
||||||
endpoint.send(
|
endpoint.send(
|
||||||
format = controlsMagixFormat,
|
format = controlsMagixFormat,
|
||||||
payload = payload,
|
payload = payload,
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
package space.kscience.controls.client
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import space.kscience.controls.manager.DeviceManager
|
|
||||||
import space.kscience.controls.manager.install
|
|
||||||
import space.kscience.controls.manager.respondMessage
|
|
||||||
import space.kscience.controls.spec.*
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.context.request
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.int
|
|
||||||
import space.kscience.dataforge.names.Name
|
|
||||||
import space.kscience.magix.api.MagixEndpoint
|
|
||||||
import space.kscience.magix.api.MagixMessage
|
|
||||||
import space.kscience.magix.api.MagixMessageFilter
|
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertContains
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
|
|
||||||
internal class RemoteDeviceConnect {
|
|
||||||
|
|
||||||
class TestDevice(context: Context, meta: Meta) : DeviceBySpec<TestDevice>(TestDevice, context, meta) {
|
|
||||||
private val rng = Random(meta["seed"].int ?: 0)
|
|
||||||
|
|
||||||
private val randomValue get() = rng.nextDouble()
|
|
||||||
|
|
||||||
companion object : DeviceSpec<TestDevice>(), Factory<TestDevice> {
|
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): TestDevice = TestDevice(context, meta)
|
|
||||||
|
|
||||||
val value by doubleProperty { randomValue }
|
|
||||||
|
|
||||||
override suspend fun TestDevice.onOpen() {
|
|
||||||
doRecurring((meta["delay"].int ?: 10).milliseconds) {
|
|
||||||
read(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun wrapper() = runTest {
|
|
||||||
val context = Context {
|
|
||||||
plugin(DeviceManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
val device = context.request(DeviceManager).install("test", TestDevice)
|
|
||||||
|
|
||||||
val virtualMagixEndpoint = object : MagixEndpoint {
|
|
||||||
|
|
||||||
|
|
||||||
override fun subscribe(filter: MagixMessageFilter): Flow<MagixMessage> = device.messageFlow.map {
|
|
||||||
MagixMessage(
|
|
||||||
format = DeviceManager.magixFormat.defaultFormat,
|
|
||||||
payload = MagixEndpoint.magixJson.encodeToJsonElement(DeviceManager.magixFormat.serializer, it),
|
|
||||||
sourceEndpoint = "source",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun broadcast(message: MagixMessage) {
|
|
||||||
device.respondMessage(
|
|
||||||
Name.EMPTY,
|
|
||||||
Json.decodeFromJsonElement(DeviceManager.magixFormat.serializer, message.payload)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "source", "target", Name.EMPTY)
|
|
||||||
|
|
||||||
assertContains(0.0..1.0, remoteDevice.read(TestDevice.value))
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,16 +14,18 @@ Automatically checks consistency.
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-modbus:0.3.0`.
|
The Maven coordinates of this project are `space.kscience:controls-modbus:0.2.0`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
//uncomment to access development builds
|
||||||
|
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-modbus:0.3.0")
|
implementation("space.kscience:controls-modbus:0.2.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
package space.kscience.controls.modbus
|
package space.kscience.controls.modbus
|
||||||
|
|
||||||
import com.ghgande.j2mod.modbus.procimg.*
|
import com.ghgande.j2mod.modbus.procimg.*
|
||||||
import kotlinx.coroutines.delay
|
import io.ktor.utils.io.core.buildPacket
|
||||||
import kotlinx.coroutines.isActive
|
import io.ktor.utils.io.core.readByteBuffer
|
||||||
|
import io.ktor.utils.io.core.writeShort
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.io.Buffer
|
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.ports.readShort
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
import space.kscience.controls.spec.*
|
import space.kscience.controls.spec.WritableDevicePropertySpec
|
||||||
import space.kscience.dataforge.io.Binary
|
import space.kscience.controls.spec.set
|
||||||
|
import space.kscience.controls.spec.useProperty
|
||||||
|
|
||||||
|
|
||||||
public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||||
@ -28,10 +29,10 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
|||||||
|
|
||||||
public fun bind(
|
public fun bind(
|
||||||
key: ModbusRegistryKey.Coil,
|
key: ModbusRegistryKey.Coil,
|
||||||
propertySpec: MutableDevicePropertySpec<D, Boolean>,
|
propertySpec: WritableDevicePropertySpec<D, Boolean>,
|
||||||
): ObservableDigitalOut = bind(key) { coil ->
|
): ObservableDigitalOut = bind(key) { coil ->
|
||||||
coil.addObserver { _, _ ->
|
coil.addObserver { _, _ ->
|
||||||
device.writeAsync(propertySpec, coil.isSet)
|
device[propertySpec] = coil.isSet
|
||||||
}
|
}
|
||||||
device.useProperty(propertySpec) { value ->
|
device.useProperty(propertySpec) { value ->
|
||||||
coil.set(value)
|
coil.set(value)
|
||||||
@ -88,10 +89,10 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
|||||||
|
|
||||||
public fun bind(
|
public fun bind(
|
||||||
key: ModbusRegistryKey.HoldingRegister,
|
key: ModbusRegistryKey.HoldingRegister,
|
||||||
propertySpec: MutableDevicePropertySpec<D, Short>,
|
propertySpec: WritableDevicePropertySpec<D, Short>,
|
||||||
): ObservableRegister = bind(key) { register ->
|
): ObservableRegister = bind(key) { register ->
|
||||||
register.addObserver { _, _ ->
|
register.addObserver { _, _ ->
|
||||||
device.writeAsync(propertySpec, register.toShort())
|
device[propertySpec] = register.toShort()
|
||||||
}
|
}
|
||||||
device.useProperty(propertySpec) { value ->
|
device.useProperty(propertySpec) { value ->
|
||||||
register.setValue(value)
|
register.setValue(value)
|
||||||
@ -108,63 +109,37 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
device.useProperty(propertySpec) { value ->
|
device.useProperty(propertySpec) { value ->
|
||||||
val binary = Binary {
|
val packet = buildPacket {
|
||||||
key.format.writeTo(this, value)
|
key.format.writeObject(this, value)
|
||||||
}
|
}.readByteBuffer()
|
||||||
registers.forEachIndexed { index, register ->
|
registers.forEachIndexed { index, register ->
|
||||||
register.setValue(binary.readShort(index * 2))
|
register.setValue(packet.getShort(index * 2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public fun <T> bind(key: ModbusRegistryKey.HoldingRange<T>, propertySpec: WritableDevicePropertySpec<D, T>) {
|
||||||
* Trigger [block] if one of register changes.
|
|
||||||
*/
|
|
||||||
private fun List<ObservableRegister>.onChange(block: suspend (Buffer) -> Unit) {
|
|
||||||
var ready = false
|
|
||||||
|
|
||||||
forEach { register ->
|
|
||||||
register.addObserver { _, _ ->
|
|
||||||
ready = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
device.launch {
|
|
||||||
val builder = Buffer()
|
|
||||||
while (isActive) {
|
|
||||||
delay(1)
|
|
||||||
if (ready) {
|
|
||||||
val packet = builder.apply {
|
|
||||||
forEach { value ->
|
|
||||||
writeShort(value.toShort())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
block(packet)
|
|
||||||
ready = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <T> bind(key: ModbusRegistryKey.HoldingRange<T>, propertySpec: MutableDevicePropertySpec<D, T>) {
|
|
||||||
val registers = List(key.count) {
|
val registers = List(key.count) {
|
||||||
ObservableRegister()
|
ObservableRegister()
|
||||||
}
|
}
|
||||||
|
|
||||||
registers.forEachIndexed { index, register ->
|
registers.forEachIndexed { index, register ->
|
||||||
|
register.addObserver { _, _ ->
|
||||||
|
val packet = buildPacket {
|
||||||
|
registers.forEach { value ->
|
||||||
|
writeShort(value.toShort())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
device[propertySpec] = key.format.readObject(packet)
|
||||||
|
}
|
||||||
image.addRegister(key.address + index, register)
|
image.addRegister(key.address + index, register)
|
||||||
}
|
}
|
||||||
|
|
||||||
registers.onChange { packet ->
|
|
||||||
device.write(propertySpec, key.format.readFrom(packet))
|
|
||||||
}
|
|
||||||
|
|
||||||
device.useProperty(propertySpec) { value ->
|
device.useProperty(propertySpec) { value ->
|
||||||
val binary = Binary {
|
val packet = buildPacket {
|
||||||
key.format.writeTo(this, value)
|
key.format.writeObject(this, value)
|
||||||
}
|
}.readByteBuffer()
|
||||||
registers.forEachIndexed { index, observableRegister ->
|
registers.forEachIndexed { index, observableRegister ->
|
||||||
observableRegister.setValue(binary.readShort(index * 2))
|
observableRegister.setValue(packet.getShort(index * 2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -207,15 +182,18 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
|||||||
val registers = List(key.count) {
|
val registers = List(key.count) {
|
||||||
ObservableRegister()
|
ObservableRegister()
|
||||||
}
|
}
|
||||||
|
|
||||||
registers.forEachIndexed { index, register ->
|
registers.forEachIndexed { index, register ->
|
||||||
image.addRegister(key.address + index, register)
|
register.addObserver { _, _ ->
|
||||||
}
|
val packet = buildPacket {
|
||||||
|
registers.forEach { value ->
|
||||||
registers.onChange { packet ->
|
writeShort(value.toShort())
|
||||||
device.launch {
|
}
|
||||||
device.action(key.format.readFrom(packet))
|
}
|
||||||
|
device.launch {
|
||||||
|
device.action(key.format.readObject(packet))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
image.addRegister(key.address + index, register)
|
||||||
}
|
}
|
||||||
|
|
||||||
return registers
|
return registers
|
||||||
@ -227,16 +205,14 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
|||||||
* Bind the device to Modbus slave (server) image.
|
* Bind the device to Modbus slave (server) image.
|
||||||
*/
|
*/
|
||||||
public fun <D : Device> D.bindProcessImage(
|
public fun <D : Device> D.bindProcessImage(
|
||||||
unitId: Int = 0,
|
|
||||||
openOnBind: Boolean = true,
|
openOnBind: Boolean = true,
|
||||||
binding: DeviceProcessImageBuilder<D>.() -> Unit,
|
binding: DeviceProcessImageBuilder<D>.() -> Unit,
|
||||||
): ProcessImage {
|
): ProcessImage {
|
||||||
val image = SimpleProcessImage(unitId)
|
val image = SimpleProcessImage()
|
||||||
DeviceProcessImageBuilder(this, image).apply(binding)
|
DeviceProcessImageBuilder(this, image).apply(binding)
|
||||||
image.setLocked(true)
|
|
||||||
if (openOnBind) {
|
if (openOnBind) {
|
||||||
launch {
|
launch {
|
||||||
start()
|
open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return image
|
return image
|
||||||
|
@ -5,10 +5,11 @@ import com.ghgande.j2mod.modbus.procimg.InputRegister
|
|||||||
import com.ghgande.j2mod.modbus.procimg.Register
|
import com.ghgande.j2mod.modbus.procimg.Register
|
||||||
import com.ghgande.j2mod.modbus.procimg.SimpleInputRegister
|
import com.ghgande.j2mod.modbus.procimg.SimpleInputRegister
|
||||||
import com.ghgande.j2mod.modbus.util.BitVector
|
import com.ghgande.j2mod.modbus.util.BitVector
|
||||||
import kotlinx.io.Buffer
|
import io.ktor.utils.io.core.ByteReadPacket
|
||||||
|
import io.ktor.utils.io.core.buildPacket
|
||||||
|
import io.ktor.utils.io.core.readByteBuffer
|
||||||
|
import io.ktor.utils.io.core.writeShort
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.dataforge.io.Buffer
|
|
||||||
import space.kscience.dataforge.io.ByteArray
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
@ -20,9 +21,9 @@ import kotlin.reflect.KProperty
|
|||||||
public interface ModbusDevice : Device {
|
public interface ModbusDevice : Device {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit id for this specific device
|
* Client id for this specific device
|
||||||
*/
|
*/
|
||||||
public val unitId: Int
|
public val clientId: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The modubus master connector
|
* The modubus master connector
|
||||||
@ -44,7 +45,7 @@ public interface ModbusDevice : Device {
|
|||||||
|
|
||||||
public operator fun <T> ModbusRegistryKey.InputRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
|
public operator fun <T> ModbusRegistryKey.InputRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||||
val packet = readInputRegistersToPacket(address, count)
|
val packet = readInputRegistersToPacket(address, count)
|
||||||
return format.readFrom(packet)
|
return format.readObject(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -60,8 +61,8 @@ public interface ModbusDevice : Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public operator fun <T> ModbusRegistryKey.HoldingRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
|
public operator fun <T> ModbusRegistryKey.HoldingRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||||
val packet = readHoldingRegistersToPacket(address, count)
|
val packet = readInputRegistersToPacket(address, count)
|
||||||
return format.readFrom(packet)
|
return format.readObject(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
public operator fun <T> ModbusRegistryKey.HoldingRange<T>.setValue(
|
public operator fun <T> ModbusRegistryKey.HoldingRange<T>.setValue(
|
||||||
@ -69,9 +70,9 @@ public interface ModbusDevice : Device {
|
|||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: T,
|
value: T,
|
||||||
) {
|
) {
|
||||||
val buffer = ByteArray {
|
val buffer = buildPacket {
|
||||||
format.writeTo(this, value)
|
format.writeObject(this, value)
|
||||||
}
|
}.readByteBuffer()
|
||||||
writeHoldingRegisters(address, buffer)
|
writeHoldingRegisters(address, buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,35 +82,35 @@ public interface ModbusDevice : Device {
|
|||||||
* Read multiple sequential modbus coils (bit-values)
|
* Read multiple sequential modbus coils (bit-values)
|
||||||
*/
|
*/
|
||||||
public fun ModbusDevice.readCoils(address: Int, count: Int): BitVector =
|
public fun ModbusDevice.readCoils(address: Int, count: Int): BitVector =
|
||||||
master.readCoils(unitId, address, count)
|
master.readCoils(clientId, address, count)
|
||||||
|
|
||||||
public fun ModbusDevice.readCoil(address: Int): Boolean =
|
public fun ModbusDevice.readCoil(address: Int): Boolean =
|
||||||
master.readCoils(unitId, address, 1).getBit(0)
|
master.readCoils(clientId, address, 1).getBit(0)
|
||||||
|
|
||||||
public fun ModbusDevice.writeCoils(address: Int, values: BooleanArray) {
|
public fun ModbusDevice.writeCoils(address: Int, values: BooleanArray) {
|
||||||
val bitVector = BitVector(values.size)
|
val bitVector = BitVector(values.size)
|
||||||
values.forEachIndexed { index, value ->
|
values.forEachIndexed { index, value ->
|
||||||
bitVector.setBit(index, value)
|
bitVector.setBit(index, value)
|
||||||
}
|
}
|
||||||
master.writeMultipleCoils(unitId, address, bitVector)
|
master.writeMultipleCoils(clientId, address, bitVector)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun ModbusDevice.writeCoil(address: Int, value: Boolean) {
|
public fun ModbusDevice.writeCoil(address: Int, value: Boolean) {
|
||||||
master.writeCoil(unitId, address, value)
|
master.writeCoil(clientId, address, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun ModbusDevice.writeCoil(key: ModbusRegistryKey.Coil, value: Boolean) {
|
public fun ModbusDevice.writeCoil(key: ModbusRegistryKey.Coil, value: Boolean) {
|
||||||
master.writeCoil(unitId, key.address, value)
|
master.writeCoil(clientId, key.address, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun ModbusDevice.readInputDiscretes(address: Int, count: Int): BitVector =
|
public fun ModbusDevice.readInputDiscretes(address: Int, count: Int): BitVector =
|
||||||
master.readInputDiscretes(unitId, address, count)
|
master.readInputDiscretes(clientId, address, count)
|
||||||
|
|
||||||
public fun ModbusDevice.readInputDiscrete(address: Int): Boolean =
|
public fun ModbusDevice.readInputDiscrete(address: Int): Boolean =
|
||||||
master.readInputDiscretes(unitId, address, 1).getBit(0)
|
master.readInputDiscretes(clientId, address, 1).getBit(0)
|
||||||
|
|
||||||
public fun ModbusDevice.readInputRegisters(address: Int, count: Int): List<InputRegister> =
|
public fun ModbusDevice.readInputRegisters(address: Int, count: Int): List<InputRegister> =
|
||||||
master.readInputRegisters(unitId, address, count).toList()
|
master.readInputRegisters(clientId, address, count).toList()
|
||||||
|
|
||||||
private fun Array<out InputRegister>.toBuffer(): ByteBuffer {
|
private fun Array<out InputRegister>.toBuffer(): ByteBuffer {
|
||||||
val buffer: ByteBuffer = ByteBuffer.allocate(size * 2)
|
val buffer: ByteBuffer = ByteBuffer.allocate(size * 2)
|
||||||
@ -121,17 +122,17 @@ private fun Array<out InputRegister>.toBuffer(): ByteBuffer {
|
|||||||
return buffer
|
return buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Array<out InputRegister>.toPacket(): Buffer = Buffer {
|
private fun Array<out InputRegister>.toPacket(): ByteReadPacket = buildPacket {
|
||||||
forEach { value ->
|
forEach { value ->
|
||||||
writeShort(value.toShort())
|
writeShort(value.toShort())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun ModbusDevice.readInputRegistersToBuffer(address: Int, count: Int): ByteBuffer =
|
public fun ModbusDevice.readInputRegistersToBuffer(address: Int, count: Int): ByteBuffer =
|
||||||
master.readInputRegisters(unitId, address, count).toBuffer()
|
master.readInputRegisters(clientId, address, count).toBuffer()
|
||||||
|
|
||||||
public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): Buffer =
|
public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): ByteReadPacket =
|
||||||
master.readInputRegisters(unitId, address, count).toPacket()
|
master.readInputRegisters(clientId, address, count).toPacket()
|
||||||
|
|
||||||
public fun ModbusDevice.readDoubleInput(address: Int): Double =
|
public fun ModbusDevice.readDoubleInput(address: Int): Double =
|
||||||
readInputRegistersToBuffer(address, Double.SIZE_BYTES).getDouble()
|
readInputRegistersToBuffer(address, Double.SIZE_BYTES).getDouble()
|
||||||
@ -140,7 +141,7 @@ public fun ModbusDevice.readInputRegister(address: Int): Short =
|
|||||||
readInputRegisters(address, 1).first().toShort()
|
readInputRegisters(address, 1).first().toShort()
|
||||||
|
|
||||||
public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List<Register> =
|
public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List<Register> =
|
||||||
master.readMultipleRegisters(unitId, address, count).toList()
|
master.readMultipleRegisters(clientId, address, count).toList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read a number of registers to a [ByteBuffer]
|
* Read a number of registers to a [ByteBuffer]
|
||||||
@ -148,10 +149,10 @@ public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List<Reg
|
|||||||
* @param count number of 2-bytes registers to read. Buffer size is 2*[count]
|
* @param count number of 2-bytes registers to read. Buffer size is 2*[count]
|
||||||
*/
|
*/
|
||||||
public fun ModbusDevice.readHoldingRegistersToBuffer(address: Int, count: Int): ByteBuffer =
|
public fun ModbusDevice.readHoldingRegistersToBuffer(address: Int, count: Int): ByteBuffer =
|
||||||
master.readMultipleRegisters(unitId, address, count).toBuffer()
|
master.readMultipleRegisters(clientId, address, count).toBuffer()
|
||||||
|
|
||||||
public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): Buffer =
|
public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): ByteReadPacket =
|
||||||
master.readMultipleRegisters(unitId, address, count).toPacket()
|
master.readMultipleRegisters(clientId, address, count).toPacket()
|
||||||
|
|
||||||
public fun ModbusDevice.readDoubleRegister(address: Int): Double =
|
public fun ModbusDevice.readDoubleRegister(address: Int): Double =
|
||||||
readHoldingRegistersToBuffer(address, Double.SIZE_BYTES).getDouble()
|
readHoldingRegistersToBuffer(address, Double.SIZE_BYTES).getDouble()
|
||||||
@ -161,14 +162,14 @@ public fun ModbusDevice.readHoldingRegister(address: Int): Short =
|
|||||||
|
|
||||||
public fun ModbusDevice.writeHoldingRegisters(address: Int, values: ShortArray): Int =
|
public fun ModbusDevice.writeHoldingRegisters(address: Int, values: ShortArray): Int =
|
||||||
master.writeMultipleRegisters(
|
master.writeMultipleRegisters(
|
||||||
unitId,
|
clientId,
|
||||||
address,
|
address,
|
||||||
Array<Register>(values.size) { SimpleInputRegister(values[it].toInt()) }
|
Array<Register>(values.size) { SimpleInputRegister(values[it].toInt()) }
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun ModbusDevice.writeHoldingRegister(address: Int, value: Short): Int =
|
public fun ModbusDevice.writeHoldingRegister(address: Int, value: Short): Int =
|
||||||
master.writeSingleRegister(
|
master.writeSingleRegister(
|
||||||
unitId,
|
clientId,
|
||||||
address,
|
address,
|
||||||
SimpleInputRegister(value.toInt())
|
SimpleInputRegister(value.toInt())
|
||||||
)
|
)
|
||||||
@ -182,11 +183,8 @@ public fun ModbusDevice.writeHoldingRegisters(address: Int, buffer: ByteBuffer):
|
|||||||
return writeHoldingRegisters(address, array)
|
return writeHoldingRegisters(address, array)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun ModbusDevice.writeHoldingRegisters(address: Int, byteArray: ByteArray): Int {
|
public fun ModbusDevice.writeShortRegister(address: Int, value: Short) {
|
||||||
val buffer = ByteBuffer.wrap(byteArray)
|
master.writeSingleRegister(address, SimpleInputRegister(value.toInt()))
|
||||||
val array: ShortArray = ShortArray(buffer.limit().floorDiv(2)) { buffer.getShort(it * 2) }
|
|
||||||
|
|
||||||
return writeHoldingRegisters(address, array)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun ModbusDevice.modbusRegister(
|
public fun ModbusDevice.modbusRegister(
|
||||||
|
@ -15,19 +15,21 @@ import space.kscience.dataforge.names.NameToken
|
|||||||
public open class ModbusDeviceBySpec<D: Device>(
|
public open class ModbusDeviceBySpec<D: Device>(
|
||||||
context: Context,
|
context: Context,
|
||||||
spec: DeviceSpec<D>,
|
spec: DeviceSpec<D>,
|
||||||
override val unitId: Int,
|
override val clientId: Int,
|
||||||
override val master: AbstractModbusMaster,
|
override val master: AbstractModbusMaster,
|
||||||
private val disposeMasterOnClose: Boolean = true,
|
private val disposeMasterOnClose: Boolean = true,
|
||||||
meta: Meta = Meta.EMPTY,
|
meta: Meta = Meta.EMPTY,
|
||||||
) : ModbusDevice, DeviceBySpec<D>(spec, context, meta){
|
) : ModbusDevice, DeviceBySpec<D>(spec, context, meta){
|
||||||
override suspend fun onStart() {
|
override suspend fun open() {
|
||||||
master.connect()
|
master.connect()
|
||||||
|
super<DeviceBySpec>.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun close() {
|
||||||
if(disposeMasterOnClose){
|
if(disposeMasterOnClose){
|
||||||
master.disconnect()
|
master.disconnect()
|
||||||
}
|
}
|
||||||
|
super<ModbusDevice>.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,8 @@
|
|||||||
package space.kscience.controls.modbus
|
package space.kscience.controls.modbus
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonArray
|
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import space.kscience.dataforge.io.IOFormat
|
import space.kscience.dataforge.io.IOFormat
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modbus registry key
|
|
||||||
*/
|
|
||||||
public sealed class ModbusRegistryKey {
|
public sealed class ModbusRegistryKey {
|
||||||
public abstract val address: Int
|
public abstract val address: Int
|
||||||
public open val count: Int = 1
|
public open val count: Int = 1
|
||||||
@ -32,9 +25,6 @@ public sealed class ModbusRegistryKey {
|
|||||||
override fun toString(): String = "InputRegister(address=$address)"
|
override fun toString(): String = "InputRegister(address=$address)"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A range of read-only register encoding a single value
|
|
||||||
*/
|
|
||||||
public class InputRange<T>(
|
public class InputRange<T>(
|
||||||
address: Int,
|
address: Int,
|
||||||
override val count: Int,
|
override val count: Int,
|
||||||
@ -46,16 +36,10 @@ public sealed class ModbusRegistryKey {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A single read-write register
|
|
||||||
*/
|
|
||||||
public open class HoldingRegister(override val address: Int) : ModbusRegistryKey() {
|
public open class HoldingRegister(override val address: Int) : ModbusRegistryKey() {
|
||||||
override fun toString(): String = "HoldingRegister(address=$address)"
|
override fun toString(): String = "HoldingRegister(address=$address)"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A range of read-write registers encoding a single value
|
|
||||||
*/
|
|
||||||
public class HoldingRange<T>(
|
public class HoldingRange<T>(
|
||||||
address: Int,
|
address: Int,
|
||||||
override val count: Int,
|
override val count: Int,
|
||||||
@ -68,9 +52,6 @@ public sealed class ModbusRegistryKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A base class for modbus registers
|
|
||||||
*/
|
|
||||||
public abstract class ModbusRegistryMap {
|
public abstract class ModbusRegistryMap {
|
||||||
|
|
||||||
private val _entries: MutableMap<ModbusRegistryKey, String> = mutableMapOf<ModbusRegistryKey, String>()
|
private val _entries: MutableMap<ModbusRegistryKey, String> = mutableMapOf<ModbusRegistryKey, String>()
|
||||||
@ -82,56 +63,36 @@ public abstract class ModbusRegistryMap {
|
|||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a [ModbusRegistryKey.Coil] key and return it
|
|
||||||
*/
|
|
||||||
protected fun coil(address: Int, description: String = ""): ModbusRegistryKey.Coil =
|
protected fun coil(address: Int, description: String = ""): ModbusRegistryKey.Coil =
|
||||||
register(ModbusRegistryKey.Coil(address), description)
|
register(ModbusRegistryKey.Coil(address), description)
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a [ModbusRegistryKey.DiscreteInput] key and return it
|
|
||||||
*/
|
|
||||||
protected fun discrete(address: Int, description: String = ""): ModbusRegistryKey.DiscreteInput =
|
protected fun discrete(address: Int, description: String = ""): ModbusRegistryKey.DiscreteInput =
|
||||||
register(ModbusRegistryKey.DiscreteInput(address), description)
|
register(ModbusRegistryKey.DiscreteInput(address), description)
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a [ModbusRegistryKey.InputRegister] key and return it
|
|
||||||
*/
|
|
||||||
protected fun input(address: Int, description: String = ""): ModbusRegistryKey.InputRegister =
|
protected fun input(address: Int, description: String = ""): ModbusRegistryKey.InputRegister =
|
||||||
register(ModbusRegistryKey.InputRegister(address), description)
|
register(ModbusRegistryKey.InputRegister(address), description)
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a [ModbusRegistryKey.InputRange] key and return it
|
|
||||||
*/
|
|
||||||
protected fun <T> input(
|
protected fun <T> input(
|
||||||
address: Int,
|
address: Int,
|
||||||
count: Int,
|
count: Int,
|
||||||
reader: IOFormat<T>,
|
reader: IOFormat<T>,
|
||||||
description: String = "",
|
description: String = "",
|
||||||
): ModbusRegistryKey.InputRange<T> = register(ModbusRegistryKey.InputRange(address, count, reader), description)
|
): ModbusRegistryKey.InputRange<T> =
|
||||||
|
register(ModbusRegistryKey.InputRange(address, count, reader), description)
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a [ModbusRegistryKey.HoldingRegister] key and return it
|
|
||||||
*/
|
|
||||||
protected fun register(address: Int, description: String = ""): ModbusRegistryKey.HoldingRegister =
|
protected fun register(address: Int, description: String = ""): ModbusRegistryKey.HoldingRegister =
|
||||||
register(ModbusRegistryKey.HoldingRegister(address), description)
|
register(ModbusRegistryKey.HoldingRegister(address), description)
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a [ModbusRegistryKey.HoldingRange] key and return it
|
|
||||||
*/
|
|
||||||
protected fun <T> register(
|
protected fun <T> register(
|
||||||
address: Int,
|
address: Int,
|
||||||
count: Int,
|
count: Int,
|
||||||
format: IOFormat<T>,
|
format: IOFormat<T>,
|
||||||
description: String = "",
|
description: String = "",
|
||||||
): ModbusRegistryKey.HoldingRange<T> = register(ModbusRegistryKey.HoldingRange(address, count, format), description)
|
): ModbusRegistryKey.HoldingRange<T> =
|
||||||
|
register(ModbusRegistryKey.HoldingRange(address, count, format), description)
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the register map. Throw an error if the map is invalid
|
|
||||||
*/
|
|
||||||
public fun validate(map: ModbusRegistryMap) {
|
public fun validate(map: ModbusRegistryMap) {
|
||||||
var lastCoil: ModbusRegistryKey.Coil? = null
|
var lastCoil: ModbusRegistryKey.Coil? = null
|
||||||
var lastDiscreteInput: ModbusRegistryKey.DiscreteInput? = null
|
var lastDiscreteInput: ModbusRegistryKey.DiscreteInput? = null
|
||||||
@ -166,62 +127,36 @@ public abstract class ModbusRegistryMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private val ModbusRegistryKey.sectionNumber
|
||||||
}
|
get() = when (this) {
|
||||||
|
is ModbusRegistryKey.Coil -> 1
|
||||||
private val ModbusRegistryKey.sectionNumber
|
is ModbusRegistryKey.DiscreteInput -> 2
|
||||||
get() = when (this) {
|
is ModbusRegistryKey.HoldingRegister -> 4
|
||||||
is ModbusRegistryKey.Coil -> 1
|
is ModbusRegistryKey.InputRegister -> 3
|
||||||
is ModbusRegistryKey.DiscreteInput -> 2
|
|
||||||
is ModbusRegistryKey.HoldingRegister -> 4
|
|
||||||
is ModbusRegistryKey.InputRegister -> 3
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun ModbusRegistryMap.print(to: Appendable = System.out) {
|
|
||||||
ModbusRegistryMap.validate(this)
|
|
||||||
entries.entries
|
|
||||||
.sortedWith(
|
|
||||||
Comparator.comparingInt<Map.Entry<ModbusRegistryKey, String>?> { it.key.sectionNumber }
|
|
||||||
.thenComparingInt { it.key.address }
|
|
||||||
)
|
|
||||||
.forEach { (key, description) ->
|
|
||||||
val typeString = when (key) {
|
|
||||||
is ModbusRegistryKey.Coil -> "Coil"
|
|
||||||
is ModbusRegistryKey.DiscreteInput -> "Discrete"
|
|
||||||
is ModbusRegistryKey.HoldingRegister -> "Register"
|
|
||||||
is ModbusRegistryKey.InputRegister -> "Input"
|
|
||||||
}
|
}
|
||||||
val rangeString = if (key.count == 1) {
|
|
||||||
key.address.toString()
|
|
||||||
} else {
|
|
||||||
"${key.address} - ${key.address + key.count - 1}"
|
|
||||||
}
|
|
||||||
to.appendLine("${typeString}\t$rangeString\t$description")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun ModbusRegistryMap.toJson(): JsonArray = buildJsonArray {
|
public fun print(map: ModbusRegistryMap, to: Appendable = System.out) {
|
||||||
ModbusRegistryMap.validate(this@toJson)
|
validate(map)
|
||||||
entries.forEach { (key, description) ->
|
map.entries.entries
|
||||||
|
.sortedWith(
|
||||||
val entry = buildJsonObject {
|
Comparator.comparingInt<Map.Entry<ModbusRegistryKey, String>?> { it.key.sectionNumber }
|
||||||
put(
|
.thenComparingInt { it.key.address }
|
||||||
"type",
|
)
|
||||||
when (key) {
|
.forEach { (key, description) ->
|
||||||
is ModbusRegistryKey.Coil -> "Coil"
|
val typeString = when (key) {
|
||||||
is ModbusRegistryKey.DiscreteInput -> "Discrete"
|
is ModbusRegistryKey.Coil -> "Coil"
|
||||||
is ModbusRegistryKey.HoldingRegister -> "Register"
|
is ModbusRegistryKey.DiscreteInput -> "Discrete"
|
||||||
is ModbusRegistryKey.InputRegister -> "Input"
|
is ModbusRegistryKey.HoldingRegister -> "Register"
|
||||||
|
is ModbusRegistryKey.InputRegister -> "Input"
|
||||||
|
}
|
||||||
|
val rangeString = if (key.count == 1) {
|
||||||
|
key.address.toString()
|
||||||
|
} else {
|
||||||
|
"${key.address} - ${key.address + key.count}"
|
||||||
|
}
|
||||||
|
to.appendLine("${typeString}\t$rangeString\t$description")
|
||||||
}
|
}
|
||||||
)
|
|
||||||
put("address", key.address)
|
|
||||||
if (key.count > 1) {
|
|
||||||
put("count", key.count)
|
|
||||||
}
|
|
||||||
put("description", description)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add(entry)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,16 +12,18 @@ A client and server connectors for OPC-UA via Eclipse Milo
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-opcua:0.3.0`.
|
The Maven coordinates of this project are `space.kscience:controls-opcua:0.2.0`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
//uncomment to access development builds
|
||||||
|
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-opcua:0.3.0")
|
implementation("space.kscience:controls-opcua:0.2.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -56,7 +56,6 @@ internal class MetaEnumCodec : OpcUaBinaryDataTypeCodec<Number> {
|
|||||||
|
|
||||||
internal fun opcToMeta(value: Any?): Meta = when (value) {
|
internal fun opcToMeta(value: Any?): Meta = when (value) {
|
||||||
null -> Meta(Null)
|
null -> Meta(Null)
|
||||||
is Variant -> opcToMeta(value.value)
|
|
||||||
is Meta -> value
|
is Meta -> value
|
||||||
is Value -> Meta(value)
|
is Value -> Meta(value)
|
||||||
is Number -> when (value) {
|
is Number -> when (value) {
|
||||||
@ -80,17 +79,12 @@ internal fun opcToMeta(value: Any?): Meta = when (value) {
|
|||||||
"text" put value.text?.asValue()
|
"text" put value.text?.asValue()
|
||||||
}
|
}
|
||||||
is DataValue -> Meta {
|
is DataValue -> Meta {
|
||||||
val variant= opcToMeta(value.value)
|
"value" put opcToMeta(value.value) // need SerializationContext to do that properly
|
||||||
update(variant)// need SerializationContext to do that properly
|
value.statusCode?.value?.let { "status" put Meta(it.asValue()) }
|
||||||
//TODO remove after DF 0.7.2
|
value.sourceTime?.javaInstant?.let { "sourceTime" put it.toKotlinInstant().toMeta() }
|
||||||
this.value = variant.value
|
value.sourcePicoseconds?.let { "sourcePicoseconds" put Meta(it.asValue()) }
|
||||||
"@opc" put {
|
value.serverTime?.javaInstant?.let { "serverTime" put it.toKotlinInstant().toMeta() }
|
||||||
value.statusCode?.value?.let { "status" put Meta(it.asValue()) }
|
value.serverPicoseconds?.let { "serverPicoseconds" put Meta(it.asValue()) }
|
||||||
value.sourceTime?.javaInstant?.let { "sourceTime" put it.toKotlinInstant().toMeta() }
|
|
||||||
value.sourcePicoseconds?.let { "sourcePicoseconds" put Meta(it.asValue()) }
|
|
||||||
value.serverTime?.javaInstant?.let { "serverTime" put it.toKotlinInstant().toMeta() }
|
|
||||||
value.serverPicoseconds?.let { "serverPicoseconds" put Meta(it.asValue()) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is ByteString -> Meta(value.bytesOrEmpty().asValue())
|
is ByteString -> Meta(value.bytesOrEmpty().asValue())
|
||||||
is XmlElement -> Meta(value.fragment?.asValue() ?: Null)
|
is XmlElement -> Meta(value.fragment?.asValue() ?: Null)
|
||||||
@ -113,7 +107,7 @@ internal class MetaStructureCodec(
|
|||||||
|
|
||||||
override fun createStructure(name: String, members: LinkedHashMap<String, Meta>): Meta = Meta {
|
override fun createStructure(name: String, members: LinkedHashMap<String, Meta>): Meta = Meta {
|
||||||
members.forEach { (property: String, value: Meta?) ->
|
members.forEach { (property: String, value: Meta?) ->
|
||||||
set(Name.parse(property), value)
|
setMeta(Name.parse(property), value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +147,7 @@ internal class MetaStructureCodec(
|
|||||||
"Float" -> member.value?.numberOrNull?.toFloat()
|
"Float" -> member.value?.numberOrNull?.toFloat()
|
||||||
"Double" -> member.value?.numberOrNull?.toDouble()
|
"Double" -> member.value?.numberOrNull?.toDouble()
|
||||||
"String" -> member.string
|
"String" -> member.string
|
||||||
"DateTime" -> member.instant?.toJavaInstant()?.let { DateTime(it) }
|
"DateTime" -> DateTime(member.instant().toJavaInstant())
|
||||||
"Guid" -> member.string?.let { UUID.fromString(it) }
|
"Guid" -> member.string?.let { UUID.fromString(it) }
|
||||||
"ByteString" -> member.value?.list?.let { list ->
|
"ByteString" -> member.value?.list?.let { list ->
|
||||||
ByteString(list.map { it.number.toByte() }.toByteArray())
|
ByteString(list.map { it.number.toByte() }.toByteArray())
|
||||||
|
@ -9,8 +9,8 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.*
|
|||||||
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn
|
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
|
||||||
import space.kscience.dataforge.meta.MetaSerializer
|
import space.kscience.dataforge.meta.MetaSerializer
|
||||||
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ public suspend inline fun <reified T: Any> OpcUaDevice.readOpcWithTime(
|
|||||||
else -> error("Incompatible OPC property value $content")
|
else -> error("Incompatible OPC property value $content")
|
||||||
}
|
}
|
||||||
|
|
||||||
val res: T = converter.read(meta)
|
val res: T = converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
|
||||||
return res to time
|
return res to time
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ public suspend inline fun <reified T> OpcUaDevice.readOpc(
|
|||||||
else -> error("Incompatible OPC property value $content")
|
else -> error("Incompatible OPC property value $content")
|
||||||
}
|
}
|
||||||
|
|
||||||
return converter.readOrNull(meta) ?: error("Meta $meta could not be converted to ${T::class}")
|
return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
|
||||||
}
|
}
|
||||||
|
|
||||||
public suspend inline fun <reified T> OpcUaDevice.writeOpc(
|
public suspend inline fun <reified T> OpcUaDevice.writeOpc(
|
||||||
@ -77,7 +77,7 @@ public suspend inline fun <reified T> OpcUaDevice.writeOpc(
|
|||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
value: T
|
value: T
|
||||||
): StatusCode {
|
): StatusCode {
|
||||||
val meta = converter.convert(value)
|
val meta = converter.objectToMeta(value)
|
||||||
return client.writeValue(nodeId, DataValue(Variant(meta))).await()
|
return client.writeValue(nodeId, DataValue(Variant(meta))).await()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ public class MiloConfiguration : Scheme() {
|
|||||||
|
|
||||||
public var endpointUrl: String by string { error("Endpoint url is not defined") }
|
public var endpointUrl: String by string { error("Endpoint url is not defined") }
|
||||||
|
|
||||||
public var username: MiloUsername? by schemeOrNull(MiloUsername)
|
public var username: MiloUsername? by specOrNull(MiloUsername)
|
||||||
|
|
||||||
public var securityPolicy: SecurityPolicy by enum(SecurityPolicy.None)
|
public var securityPolicy: SecurityPolicy by enum(SecurityPolicy.None)
|
||||||
|
|
||||||
@ -63,7 +63,8 @@ public open class OpcUaDeviceBySpec<D : Device>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun close() {
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
|
super<DeviceBySpec>.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package space.kscience.controls.opcua.server
|
|||||||
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.toJavaInstant
|
import kotlinx.datetime.toJavaInstant
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.eclipse.milo.opcua.sdk.core.AccessLevel
|
import org.eclipse.milo.opcua.sdk.core.AccessLevel
|
||||||
import org.eclipse.milo.opcua.sdk.core.Reference
|
import org.eclipse.milo.opcua.sdk.core.Reference
|
||||||
import org.eclipse.milo.opcua.sdk.server.Lifecycle
|
import org.eclipse.milo.opcua.sdk.server.Lifecycle
|
||||||
@ -18,17 +19,19 @@ import org.eclipse.milo.opcua.stack.core.AttributeId
|
|||||||
import org.eclipse.milo.opcua.stack.core.Identifiers
|
import org.eclipse.milo.opcua.stack.core.Identifiers
|
||||||
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime
|
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime
|
||||||
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
|
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
|
||||||
import space.kscience.controls.api.*
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.api.DeviceHub
|
||||||
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
|
import space.kscience.controls.api.onPropertyChange
|
||||||
import space.kscience.controls.manager.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
import space.kscience.controls.opcua.client.opcToMeta
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.MetaSerializer
|
||||||
import space.kscience.dataforge.meta.ValueType
|
import space.kscience.dataforge.meta.ValueType
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.plus
|
import space.kscience.dataforge.names.plus
|
||||||
|
|
||||||
|
|
||||||
public operator fun CachingDevice.get(propertyDescriptor: PropertyDescriptor): Meta? =
|
public operator fun Device.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name)
|
||||||
getProperty(propertyDescriptor.name)
|
|
||||||
|
|
||||||
public suspend fun Device.read(propertyDescriptor: PropertyDescriptor): Meta = readProperty(propertyDescriptor.name)
|
public suspend fun Device.read(propertyDescriptor: PropertyDescriptor): Meta = readProperty(propertyDescriptor.name)
|
||||||
|
|
||||||
@ -38,11 +41,29 @@ https://github.com/eclipse/milo/blob/master/milo-examples/server-examples/src/ma
|
|||||||
|
|
||||||
public class DeviceNameSpace(
|
public class DeviceNameSpace(
|
||||||
server: OpcUaServer,
|
server: OpcUaServer,
|
||||||
public val deviceManager: DeviceManager,
|
public val deviceManager: DeviceManager
|
||||||
) : ManagedNamespaceWithLifecycle(server, NAMESPACE_URI) {
|
) : ManagedNamespaceWithLifecycle(server, NAMESPACE_URI) {
|
||||||
|
|
||||||
private val subscription = SubscriptionModel(server, this)
|
private val subscription = SubscriptionModel(server, this)
|
||||||
|
|
||||||
|
init {
|
||||||
|
lifecycleManager.addLifecycle(subscription)
|
||||||
|
|
||||||
|
lifecycleManager.addStartupTask {
|
||||||
|
nodeContext.registerHub(deviceManager, Name.EMPTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleManager.addLifecycle(object : Lifecycle {
|
||||||
|
override fun startup() {
|
||||||
|
server.addressSpaceManager.register(this@DeviceNameSpace)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shutdown() {
|
||||||
|
server.addressSpaceManager.unregister(this@DeviceNameSpace)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private fun UaFolderNode.registerDeviceNodes(deviceName: Name, device: Device) {
|
private fun UaFolderNode.registerDeviceNodes(deviceName: Name, device: Device) {
|
||||||
val nodes = device.propertyDescriptors.associate { descriptor ->
|
val nodes = device.propertyDescriptors.associate { descriptor ->
|
||||||
val propertyName = descriptor.name
|
val propertyName = descriptor.name
|
||||||
@ -52,21 +73,18 @@ public class DeviceNameSpace(
|
|||||||
//for now, use DF paths as ids
|
//for now, use DF paths as ids
|
||||||
nodeId = newNodeId("${deviceName.tokens.joinToString("/")}/$propertyName")
|
nodeId = newNodeId("${deviceName.tokens.joinToString("/")}/$propertyName")
|
||||||
when {
|
when {
|
||||||
descriptor.readable && descriptor.mutable -> {
|
descriptor.readable && descriptor.writable -> {
|
||||||
setAccessLevel(AccessLevel.READ_WRITE)
|
setAccessLevel(AccessLevel.READ_WRITE)
|
||||||
setUserAccessLevel(AccessLevel.READ_WRITE)
|
setUserAccessLevel(AccessLevel.READ_WRITE)
|
||||||
}
|
}
|
||||||
|
descriptor.writable -> {
|
||||||
descriptor.mutable -> {
|
|
||||||
setAccessLevel(AccessLevel.WRITE_ONLY)
|
setAccessLevel(AccessLevel.WRITE_ONLY)
|
||||||
setUserAccessLevel(AccessLevel.WRITE_ONLY)
|
setUserAccessLevel(AccessLevel.WRITE_ONLY)
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptor.readable -> {
|
descriptor.readable -> {
|
||||||
setAccessLevel(AccessLevel.READ_ONLY)
|
setAccessLevel(AccessLevel.READ_ONLY)
|
||||||
setUserAccessLevel(AccessLevel.READ_ONLY)
|
setUserAccessLevel(AccessLevel.READ_ONLY)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
setAccessLevel(AccessLevel.NONE)
|
setAccessLevel(AccessLevel.NONE)
|
||||||
setUserAccessLevel(AccessLevel.NONE)
|
setUserAccessLevel(AccessLevel.NONE)
|
||||||
@ -75,7 +93,7 @@ public class DeviceNameSpace(
|
|||||||
|
|
||||||
browseName = newQualifiedName(propertyName)
|
browseName = newQualifiedName(propertyName)
|
||||||
displayName = LocalizedText.english(propertyName)
|
displayName = LocalizedText.english(propertyName)
|
||||||
dataType = if (descriptor.metaDescriptor.nodes.isNotEmpty()) {
|
dataType = if (descriptor.metaDescriptor.children.isNotEmpty()) {
|
||||||
Identifiers.String
|
Identifiers.String
|
||||||
} else when (descriptor.metaDescriptor.valueTypes?.first()) {
|
} else when (descriptor.metaDescriptor.valueTypes?.first()) {
|
||||||
null, ValueType.STRING, ValueType.NULL -> Identifiers.String
|
null, ValueType.STRING, ValueType.NULL -> Identifiers.String
|
||||||
@ -88,24 +106,25 @@ public class DeviceNameSpace(
|
|||||||
setTypeDefinition(Identifiers.BaseDataVariableType)
|
setTypeDefinition(Identifiers.BaseDataVariableType)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
// Update initial value, but only if it is cached
|
|
||||||
if (device is CachingDevice) {
|
device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let {
|
||||||
device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let {
|
node.value = it
|
||||||
node.value = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (descriptor.mutable) {
|
/**
|
||||||
|
* Subscribe to node value changes
|
||||||
/**
|
*/
|
||||||
* Subscribe to node value changes
|
node.addAttributeObserver { _: UaNode, attributeId: AttributeId, value: Any ->
|
||||||
*/
|
if (attributeId == AttributeId.Value) {
|
||||||
node.addAttributeObserver { _: UaNode, attributeId: AttributeId, value: Any? ->
|
val meta: Meta = when (value) {
|
||||||
if (attributeId == AttributeId.Value) {
|
is Meta -> value
|
||||||
val meta: Meta = opcToMeta(value)
|
is Boolean -> Meta(value)
|
||||||
deviceManager.context.launch {
|
is Number -> Meta(value)
|
||||||
device.writeProperty(propertyName, meta)
|
is String -> Json.decodeFromString(MetaSerializer, value)
|
||||||
}
|
else -> return@addAttributeObserver //TODO("other types not implemented")
|
||||||
|
}
|
||||||
|
deviceManager.context.launch {
|
||||||
|
device.writeProperty(propertyName, meta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,11 +137,8 @@ public class DeviceNameSpace(
|
|||||||
//Subscribe on properties updates
|
//Subscribe on properties updates
|
||||||
device.onPropertyChange {
|
device.onPropertyChange {
|
||||||
nodes[property]?.let { node ->
|
nodes[property]?.let { node ->
|
||||||
val sourceTime = DateTime(time.toJavaInstant())
|
val sourceTime = time?.let { DateTime(it.toJavaInstant()) }
|
||||||
val newValue = value.toOpc(sourceTime = sourceTime)
|
node.value = value.toOpc(sourceTime = sourceTime)
|
||||||
if (node.value.value != newValue.value) {
|
|
||||||
node.value = newValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//recursively add sub-devices
|
//recursively add sub-devices
|
||||||
@ -153,24 +169,6 @@ public class DeviceNameSpace(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
lifecycleManager.addLifecycle(subscription)
|
|
||||||
|
|
||||||
lifecycleManager.addStartupTask {
|
|
||||||
nodeContext.registerHub(deviceManager, Name.EMPTY)
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleManager.addLifecycle(object : Lifecycle {
|
|
||||||
override fun startup() {
|
|
||||||
server.addressSpaceManager.register(this@DeviceNameSpace)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shutdown() {
|
|
||||||
server.addressSpaceManager.unregister(this@DeviceNameSpace)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDataItemsCreated(dataItems: List<DataItem?>?) {
|
override fun onDataItemsCreated(dataItems: List<DataItem?>?) {
|
||||||
subscription.onDataItemsCreated(dataItems)
|
subscription.onDataItemsCreated(dataItems)
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test
|
|||||||
import space.kscience.controls.spec.DeviceSpec
|
import space.kscience.controls.spec.DeviceSpec
|
||||||
import space.kscience.controls.spec.doubleProperty
|
import space.kscience.controls.spec.doubleProperty
|
||||||
import space.kscience.controls.spec.read
|
import space.kscience.controls.spec.read
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
import kotlin.test.Ignore
|
import kotlin.test.Ignore
|
||||||
|
|
||||||
class OpcUaClientTest {
|
class OpcUaClientTest {
|
||||||
@ -29,7 +29,7 @@ class OpcUaClientTest {
|
|||||||
return DemoOpcUaDevice(config)
|
return DemoOpcUaDevice(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
val randomDouble by doubleProperty { readRandomDouble() }
|
val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,10 +40,9 @@ class OpcUaClientTest {
|
|||||||
@Test
|
@Test
|
||||||
@Ignore
|
@Ignore
|
||||||
fun testReadDouble() = runTest {
|
fun testReadDouble() = runTest {
|
||||||
val device = DemoOpcUaDevice.build()
|
DemoOpcUaDevice.build().use{
|
||||||
device.start()
|
println(it.read(DemoOpcUaDevice.randomDouble))
|
||||||
println(device.read(DemoOpcUaDevice.randomDouble))
|
}
|
||||||
device.stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -6,16 +6,18 @@ Utils to work with controls-kt on Raspberry pi
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-pi:0.3.0`.
|
The Maven coordinates of this project are `space.kscience:controls-pi:0.2.0`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
//uncomment to access development builds
|
||||||
|
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-pi:0.3.0")
|
implementation("space.kscience:controls-pi:0.2.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
public final class space/kscience/controls/pi/PiPlugin : space/kscience/dataforge/context/AbstractPlugin {
|
public final class space/kscience/controls/pi/PiPlugin : space/kscience/dataforge/context/AbstractPlugin {
|
||||||
public static final field Companion Lspace/kscience/controls/pi/PiPlugin$Companion;
|
public static final field Companion Lspace/kscience/controls/pi/PiPlugin$Companion;
|
||||||
public fun <init> ()V
|
public fun <init> ()V
|
||||||
public fun content (Ljava/lang/String;)Ljava/util/Map;
|
|
||||||
public fun detach ()V
|
|
||||||
public final fun getDevices ()Lspace/kscience/controls/manager/DeviceManager;
|
|
||||||
public final fun getPiContext ()Lcom/pi4j/context/Context;
|
|
||||||
public final fun getPorts ()Lspace/kscience/controls/ports/Ports;
|
public final fun getPorts ()Lspace/kscience/controls/ports/Ports;
|
||||||
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
|
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
|
||||||
}
|
}
|
||||||
@ -12,16 +8,15 @@ public final class space/kscience/controls/pi/PiPlugin : space/kscience/dataforg
|
|||||||
public final class space/kscience/controls/pi/PiPlugin$Companion : space/kscience/dataforge/context/PluginFactory {
|
public final class space/kscience/controls/pi/PiPlugin$Companion : space/kscience/dataforge/context/PluginFactory {
|
||||||
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
|
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
|
||||||
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/pi/PiPlugin;
|
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/pi/PiPlugin;
|
||||||
public final fun createPiContext (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lcom/pi4j/context/Context;
|
|
||||||
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
|
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class space/kscience/controls/pi/PiSerialPort : space/kscience/controls/ports/AbstractPort {
|
public final class space/kscience/controls/pi/PiSerialPort : space/kscience/controls/ports/AbstractPort {
|
||||||
public static final field Companion Lspace/kscience/controls/pi/PiSerialPort$Companion;
|
public static final field Companion Lspace/kscience/controls/pi/PiSerialPort$Companion;
|
||||||
public fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;)V
|
public fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;)V
|
||||||
public synthetic fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
public synthetic fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
public fun close ()V
|
public fun close ()V
|
||||||
public final fun getSerialBuilder ()Lkotlin/jvm/functions/Function1;
|
public final fun getSerialBuilder ()Lkotlin/jvm/functions/Function0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class space/kscience/controls/pi/PiSerialPort$Companion : space/kscience/controls/ports/PortFactory {
|
public final class space/kscience/controls/pi/PiSerialPort$Companion : space/kscience/controls/ports/PortFactory {
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
package space.kscience.controls.pi
|
|
||||||
|
|
||||||
import com.pi4j.io.serial.Baud
|
|
||||||
import com.pi4j.io.serial.Serial
|
|
||||||
import com.pi4j.io.serial.SerialConfigBuilder
|
|
||||||
import com.pi4j.ktx.io.serial
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import space.kscience.controls.ports.AbstractAsynchronousPort
|
|
||||||
import space.kscience.controls.ports.AsynchronousPort
|
|
||||||
import space.kscience.controls.ports.copyToArray
|
|
||||||
import space.kscience.dataforge.context.*
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.enum
|
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.string
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
public class AsynchronousPiPort(
|
|
||||||
context: Context,
|
|
||||||
meta: Meta,
|
|
||||||
private val serial: Serial,
|
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
|
||||||
) : AbstractAsynchronousPort(context, meta, coroutineContext) {
|
|
||||||
|
|
||||||
|
|
||||||
private var listenerJob: Job? = null
|
|
||||||
override fun onOpen() {
|
|
||||||
serial.open()
|
|
||||||
listenerJob = this.scope.launch(Dispatchers.IO) {
|
|
||||||
val buffer = ByteBuffer.allocate(1024)
|
|
||||||
while (isActive) {
|
|
||||||
try {
|
|
||||||
val num = serial.read(buffer)
|
|
||||||
if (num > 0) {
|
|
||||||
receive(buffer.copyToArray(num))
|
|
||||||
}
|
|
||||||
if (num < 0) cancel("The input channel is exhausted")
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
logger.error(ex) { "Channel read error" }
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) {
|
|
||||||
serial.write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override val isOpen: Boolean get() = listenerJob?.isActive == true
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
listenerJob?.cancel()
|
|
||||||
serial.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
public companion object : Factory<AsynchronousPort> {
|
|
||||||
|
|
||||||
|
|
||||||
public fun build(
|
|
||||||
context: Context,
|
|
||||||
device: String,
|
|
||||||
block: SerialConfigBuilder.() -> Unit,
|
|
||||||
): AsynchronousPiPort {
|
|
||||||
val meta = Meta {
|
|
||||||
"name" put "pi://$device"
|
|
||||||
"type" put "serial"
|
|
||||||
}
|
|
||||||
val pi = context.request(PiPlugin)
|
|
||||||
|
|
||||||
val serial = pi.piContext.serial(device, block)
|
|
||||||
return AsynchronousPiPort(context, meta, serial)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun open(
|
|
||||||
context: Context,
|
|
||||||
device: String,
|
|
||||||
block: SerialConfigBuilder.() -> Unit,
|
|
||||||
): AsynchronousPiPort = build(context, device, block).apply { open() }
|
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): AsynchronousPort {
|
|
||||||
val device: String = meta["device"].string ?: error("Device name not defined")
|
|
||||||
val baudRate: Baud = meta["baudRate"].enum<Baud>() ?: Baud._9600
|
|
||||||
val pi = context.request(PiPlugin)
|
|
||||||
val serial = pi.piContext.serial(device) {
|
|
||||||
baud8N1(baudRate)
|
|
||||||
}
|
|
||||||
return AsynchronousPiPort(context, meta, serial)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +1,22 @@
|
|||||||
package space.kscience.controls.pi
|
package space.kscience.controls.pi
|
||||||
|
|
||||||
import com.pi4j.Pi4J
|
|
||||||
import space.kscience.controls.manager.DeviceManager
|
|
||||||
import space.kscience.controls.ports.Ports
|
import space.kscience.controls.ports.Ports
|
||||||
import space.kscience.dataforge.context.AbstractPlugin
|
import space.kscience.dataforge.context.AbstractPlugin
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.PluginFactory
|
import space.kscience.dataforge.context.PluginFactory
|
||||||
import space.kscience.dataforge.context.PluginTag
|
import space.kscience.dataforge.context.PluginTag
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.names.Name
|
|
||||||
import space.kscience.dataforge.names.asName
|
|
||||||
import com.pi4j.context.Context as PiContext
|
|
||||||
|
|
||||||
public class PiPlugin : AbstractPlugin() {
|
public class PiPlugin : AbstractPlugin() {
|
||||||
public val ports: Ports by require(Ports)
|
public val ports: Ports by require(Ports)
|
||||||
public val devices: DeviceManager by require(DeviceManager)
|
|
||||||
|
|
||||||
override val tag: PluginTag get() = Companion.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
public val piContext: PiContext by lazy { createPiContext(context, meta) }
|
|
||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = when (target) {
|
|
||||||
Ports.ASYNCHRONOUS_PORT_TYPE -> mapOf(
|
|
||||||
"serial".asName() to AsynchronousPiPort,
|
|
||||||
)
|
|
||||||
Ports.SYNCHRONOUS_PORT_TYPE -> mapOf(
|
|
||||||
"serial".asName() to SynchronousPiPort,
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> super.content(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun detach() {
|
|
||||||
piContext.shutdown()
|
|
||||||
super.detach()
|
|
||||||
}
|
|
||||||
|
|
||||||
public companion object : PluginFactory<PiPlugin> {
|
public companion object : PluginFactory<PiPlugin> {
|
||||||
|
|
||||||
override val tag: PluginTag = PluginTag("controls.ports.pi", group = PluginTag.DATAFORGE_GROUP)
|
override val tag: PluginTag = PluginTag("controls.ports.pi", group = PluginTag.DATAFORGE_GROUP)
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): PiPlugin = PiPlugin()
|
override fun build(context: Context, meta: Meta): PiPlugin = PiPlugin()
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
public fun createPiContext(context: Context, meta: Meta): PiContext = Pi4J.newAutoContext()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package space.kscience.controls.pi
|
||||||
|
|
||||||
|
import com.pi4j.Pi4J
|
||||||
|
import com.pi4j.io.serial.Baud
|
||||||
|
import com.pi4j.io.serial.Serial
|
||||||
|
import com.pi4j.io.serial.SerialConfigBuilder
|
||||||
|
import com.pi4j.ktx.io.serial
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import space.kscience.controls.ports.AbstractPort
|
||||||
|
import space.kscience.controls.ports.Port
|
||||||
|
import space.kscience.controls.ports.PortFactory
|
||||||
|
import space.kscience.controls.ports.toArray
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.error
|
||||||
|
import space.kscience.dataforge.context.logger
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.enum
|
||||||
|
import space.kscience.dataforge.meta.get
|
||||||
|
import space.kscience.dataforge.meta.string
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
public class PiSerialPort(
|
||||||
|
context: Context,
|
||||||
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
|
public val serialBuilder: () -> Serial,
|
||||||
|
) : AbstractPort(context, coroutineContext) {
|
||||||
|
|
||||||
|
private val serial: Serial by lazy { serialBuilder() }
|
||||||
|
|
||||||
|
|
||||||
|
private val listenerJob = this.scope.launch(Dispatchers.IO) {
|
||||||
|
val buffer = ByteBuffer.allocate(1024)
|
||||||
|
while (isActive) {
|
||||||
|
try {
|
||||||
|
val num = serial.read(buffer)
|
||||||
|
if (num > 0) {
|
||||||
|
receive(buffer.toArray(num))
|
||||||
|
}
|
||||||
|
if (num < 0) cancel("The input channel is exhausted")
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
logger.error(ex) { "Channel read error" }
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) {
|
||||||
|
serial.write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
listenerJob.cancel()
|
||||||
|
serial.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object : PortFactory {
|
||||||
|
override val type: String get() = "pi"
|
||||||
|
|
||||||
|
public fun open(context: Context, device: String, block: SerialConfigBuilder.() -> Unit): PiSerialPort =
|
||||||
|
PiSerialPort(context) {
|
||||||
|
Pi4J.newAutoContext().serial(device, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun build(context: Context, meta: Meta): Port = PiSerialPort(context) {
|
||||||
|
val device: String = meta["device"].string ?: error("Device name not defined")
|
||||||
|
val baudRate: Baud = meta["baudRate"].enum<Baud>() ?: Baud._9600
|
||||||
|
Pi4J.newAutoContext().serial(device) {
|
||||||
|
baud8N1(baudRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,107 +0,0 @@
|
|||||||
package space.kscience.controls.pi
|
|
||||||
|
|
||||||
import com.pi4j.io.serial.Baud
|
|
||||||
import com.pi4j.io.serial.Serial
|
|
||||||
import com.pi4j.io.serial.SerialConfigBuilder
|
|
||||||
import com.pi4j.ktx.io.serial
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import space.kscience.controls.ports.SynchronousPort
|
|
||||||
import space.kscience.controls.ports.copyToArray
|
|
||||||
import space.kscience.dataforge.context.*
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.enum
|
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.string
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
|
|
||||||
public class SynchronousPiPort(
|
|
||||||
override val context: Context,
|
|
||||||
public val meta: Meta,
|
|
||||||
private val serial: Serial,
|
|
||||||
private val mutex: Mutex = Mutex(),
|
|
||||||
) : SynchronousPort {
|
|
||||||
|
|
||||||
private val pi = context.request(PiPlugin)
|
|
||||||
override fun open() {
|
|
||||||
serial.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val isOpen: Boolean get() = serial.isOpen
|
|
||||||
|
|
||||||
override suspend fun <R> respond(
|
|
||||||
request: ByteArray,
|
|
||||||
transform: suspend Flow<ByteArray>.() -> R,
|
|
||||||
): R = mutex.withLock {
|
|
||||||
serial.drain()
|
|
||||||
serial.write(request)
|
|
||||||
flow<ByteArray> {
|
|
||||||
val buffer = ByteBuffer.allocate(1024)
|
|
||||||
while (isOpen) {
|
|
||||||
try {
|
|
||||||
val num = serial.read(buffer)
|
|
||||||
if (num > 0) {
|
|
||||||
emit(buffer.copyToArray(num))
|
|
||||||
}
|
|
||||||
if (num < 0) break
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
logger.error(ex) { "Channel read error" }
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.transform()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun respondFixedMessageSize(request: ByteArray, responseSize: Int): ByteArray = mutex.withLock {
|
|
||||||
runInterruptible {
|
|
||||||
serial.drain()
|
|
||||||
serial.write(request)
|
|
||||||
serial.readNBytes(responseSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
serial.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
public companion object : Factory<SynchronousPort> {
|
|
||||||
|
|
||||||
|
|
||||||
public fun build(
|
|
||||||
context: Context,
|
|
||||||
device: String,
|
|
||||||
block: SerialConfigBuilder.() -> Unit,
|
|
||||||
): SynchronousPiPort {
|
|
||||||
val meta = Meta {
|
|
||||||
"name" put "pi://$device"
|
|
||||||
"type" put "serial"
|
|
||||||
}
|
|
||||||
val pi = context.request(PiPlugin)
|
|
||||||
|
|
||||||
val serial = pi.piContext.serial(device, block)
|
|
||||||
return SynchronousPiPort(context, meta, serial)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun open(
|
|
||||||
context: Context,
|
|
||||||
device: String,
|
|
||||||
block: SerialConfigBuilder.() -> Unit,
|
|
||||||
): SynchronousPiPort = build(context, device, block).apply { open() }
|
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): SynchronousPiPort {
|
|
||||||
val device: String = meta["device"].string ?: error("Device name not defined")
|
|
||||||
val baudRate: Baud = meta["baudRate"].enum<Baud>() ?: Baud._9600
|
|
||||||
val pi = context.request(PiPlugin)
|
|
||||||
val serial = pi.piContext.serial(device) {
|
|
||||||
baud8N1(baudRate)
|
|
||||||
}
|
|
||||||
return SynchronousPiPort(context, meta, serial)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
|||||||
import space.kscience.gradle.Maturity
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id("space.kscience.gradle.mpp")
|
|
||||||
`maven-publish`
|
|
||||||
}
|
|
||||||
|
|
||||||
description = """
|
|
||||||
A plugin for Controls-kt device server on top of plc4x library
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
kscience {
|
|
||||||
jvm()
|
|
||||||
jvmMain {
|
|
||||||
api(projects.controlsCore)
|
|
||||||
api(libs.plc4j.spi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readme {
|
|
||||||
maturity = Maturity.EXPERIMENTAL
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
package space.kscience.controls.plc4x
|
|
||||||
|
|
||||||
import kotlinx.coroutines.future.await
|
|
||||||
import org.apache.plc4x.java.api.PlcConnection
|
|
||||||
import org.apache.plc4x.java.api.messages.PlcBrowseItem
|
|
||||||
import org.apache.plc4x.java.api.messages.PlcTagResponse
|
|
||||||
import org.apache.plc4x.java.api.messages.PlcWriteRequest
|
|
||||||
import org.apache.plc4x.java.api.messages.PlcWriteResponse
|
|
||||||
import org.apache.plc4x.java.api.types.PlcResponseCode
|
|
||||||
import space.kscience.controls.api.Device
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
|
|
||||||
private val PlcTagResponse.responseCodes: Map<String, PlcResponseCode>
|
|
||||||
get() = tagNames.associateWith { getResponseCode(it) }
|
|
||||||
|
|
||||||
private val Map<String, PlcResponseCode>.isOK get() = values.all { it == PlcResponseCode.OK }
|
|
||||||
|
|
||||||
public class PlcException(public val codes: Map<String, PlcResponseCode>) : Exception() {
|
|
||||||
override val message: String
|
|
||||||
get() = "Plc request unsuccessful:" + codes.entries.joinToString(prefix = "\n\t", separator = "\n\t") {
|
|
||||||
"${it.key}: ${it.value.name}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun PlcTagResponse.throwOnFail() {
|
|
||||||
val codes = responseCodes
|
|
||||||
if (!codes.isOK) throw PlcException(codes)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface Plc4XDevice : Device {
|
|
||||||
public val connection: PlcConnection
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send ping request and suspend until it comes back
|
|
||||||
*/
|
|
||||||
public suspend fun Plc4XDevice.ping(): PlcResponseCode = connection.ping().await().responseCode
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send browse request to list available tags
|
|
||||||
*/
|
|
||||||
public suspend fun Plc4XDevice.browse(): Map<String, MutableList<PlcBrowseItem>> {
|
|
||||||
require(connection.metadata.isBrowseSupported){"Browse actions are not supported on connection"}
|
|
||||||
val request = connection.browseRequestBuilder().build()
|
|
||||||
val response = request.execute().await()
|
|
||||||
|
|
||||||
return response.queryNames.associateWith { response.getValues(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send read request and suspend until it returns. Throw a [PlcException] if at least one tag read fails.
|
|
||||||
*
|
|
||||||
* @throws PlcException
|
|
||||||
*/
|
|
||||||
public suspend fun Plc4XDevice.read(plc4xProperty: Plc4xProperty): Meta = with(plc4xProperty) {
|
|
||||||
require(connection.metadata.isReadSupported) {"Read actions are not supported on connections"}
|
|
||||||
val request = connection.readRequestBuilder().request().build()
|
|
||||||
val response = request.execute().await()
|
|
||||||
response.throwOnFail()
|
|
||||||
response.readProperty()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send write request and suspend until it finishes. Throw a [PlcException] if at least one tag write fails.
|
|
||||||
*
|
|
||||||
* @throws PlcException
|
|
||||||
*/
|
|
||||||
public suspend fun Plc4XDevice.write(plc4xProperty: Plc4xProperty, value: Meta): Unit = with(plc4xProperty) {
|
|
||||||
require(connection.metadata.isWriteSupported){"Write actions are not supported on connection"}
|
|
||||||
val request: PlcWriteRequest = connection.writeRequestBuilder().writeProperty(value).build()
|
|
||||||
val response: PlcWriteResponse = request.execute().await()
|
|
||||||
response.throwOnFail()
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package space.kscience.controls.plc4x
|
|
||||||
|
|
||||||
import org.apache.plc4x.java.api.PlcConnection
|
|
||||||
import space.kscience.controls.spec.DeviceActionSpec
|
|
||||||
import space.kscience.controls.spec.DeviceBase
|
|
||||||
import space.kscience.controls.spec.DevicePropertySpec
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
|
|
||||||
public class Plc4XDeviceBase(
|
|
||||||
context: Context,
|
|
||||||
meta: Meta,
|
|
||||||
override val connection: PlcConnection,
|
|
||||||
) : Plc4XDevice, DeviceBase<Plc4XDevice>(context, meta) {
|
|
||||||
override val properties: Map<String, DevicePropertySpec<Plc4XDevice, *>>
|
|
||||||
get() = TODO("Not yet implemented")
|
|
||||||
override val actions: Map<String, DeviceActionSpec<Plc4XDevice, *, *>> = emptyMap()
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
package space.kscience.controls.plc4x
|
|
||||||
|
|
||||||
import org.apache.plc4x.java.api.messages.PlcReadRequest
|
|
||||||
import org.apache.plc4x.java.api.messages.PlcReadResponse
|
|
||||||
import org.apache.plc4x.java.api.messages.PlcWriteRequest
|
|
||||||
import org.apache.plc4x.java.api.types.PlcValueType
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
|
|
||||||
public interface Plc4xProperty {
|
|
||||||
|
|
||||||
public val keys: Set<String>
|
|
||||||
|
|
||||||
public fun PlcReadRequest.Builder.request(): PlcReadRequest.Builder
|
|
||||||
|
|
||||||
public fun PlcReadResponse.readProperty(): Meta
|
|
||||||
|
|
||||||
public fun PlcWriteRequest.Builder.writeProperty(meta: Meta): PlcWriteRequest.Builder
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DefaultPlc4xProperty(
|
|
||||||
private val address: String,
|
|
||||||
private val plcValueType: PlcValueType,
|
|
||||||
private val name: String = "@default",
|
|
||||||
) : Plc4xProperty {
|
|
||||||
|
|
||||||
override val keys: Set<String> = setOf(name)
|
|
||||||
|
|
||||||
override fun PlcReadRequest.Builder.request(): PlcReadRequest.Builder =
|
|
||||||
addTagAddress(name, address)
|
|
||||||
|
|
||||||
override fun PlcReadResponse.readProperty(): Meta =
|
|
||||||
getPlcValue(name).toMeta()
|
|
||||||
|
|
||||||
override fun PlcWriteRequest.Builder.writeProperty(meta: Meta): PlcWriteRequest.Builder =
|
|
||||||
addTagAddress(name, address, meta.toPlcValue(plcValueType))
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun Plc4xProperty(address: String, plcValueType: PlcValueType, name: String = "@default"): Plc4xProperty =
|
|
||||||
DefaultPlc4xProperty(address, plcValueType, name)
|
|
@ -1,123 +0,0 @@
|
|||||||
package space.kscience.controls.plc4x
|
|
||||||
|
|
||||||
import org.apache.plc4x.java.api.types.PlcValueType
|
|
||||||
import org.apache.plc4x.java.api.value.PlcValue
|
|
||||||
import org.apache.plc4x.java.spi.values.*
|
|
||||||
import space.kscience.dataforge.meta.*
|
|
||||||
import space.kscience.dataforge.names.asName
|
|
||||||
import java.math.BigInteger
|
|
||||||
|
|
||||||
internal fun PlcValue.toMeta(): Meta = Meta {
|
|
||||||
when (plcValueType) {
|
|
||||||
null, PlcValueType.NULL -> value = Null
|
|
||||||
PlcValueType.BOOL -> value = this@toMeta.boolean.asValue()
|
|
||||||
PlcValueType.BYTE -> this@toMeta.byte.asValue()
|
|
||||||
PlcValueType.WORD -> this@toMeta.short.asValue()
|
|
||||||
PlcValueType.DWORD -> this@toMeta.int.asValue()
|
|
||||||
PlcValueType.LWORD -> this@toMeta.long.asValue()
|
|
||||||
PlcValueType.USINT -> this@toMeta.short.asValue()
|
|
||||||
PlcValueType.UINT -> this@toMeta.int.asValue()
|
|
||||||
PlcValueType.UDINT -> this@toMeta.long.asValue()
|
|
||||||
PlcValueType.ULINT -> this@toMeta.bigInteger.asValue()
|
|
||||||
PlcValueType.SINT -> this@toMeta.byte.asValue()
|
|
||||||
PlcValueType.INT -> this@toMeta.short.asValue()
|
|
||||||
PlcValueType.DINT -> this@toMeta.int.asValue()
|
|
||||||
PlcValueType.LINT -> this@toMeta.long.asValue()
|
|
||||||
PlcValueType.REAL -> this@toMeta.float.asValue()
|
|
||||||
PlcValueType.LREAL -> this@toMeta.double.asValue()
|
|
||||||
PlcValueType.CHAR -> this@toMeta.int.asValue()
|
|
||||||
PlcValueType.WCHAR -> this@toMeta.short.asValue()
|
|
||||||
PlcValueType.STRING -> this@toMeta.string.asValue()
|
|
||||||
PlcValueType.WSTRING -> this@toMeta.string.asValue()
|
|
||||||
PlcValueType.TIME -> this@toMeta.duration.toString().asValue()
|
|
||||||
PlcValueType.LTIME -> this@toMeta.duration.toString().asValue()
|
|
||||||
PlcValueType.DATE -> this@toMeta.date.toString().asValue()
|
|
||||||
PlcValueType.LDATE -> this@toMeta.date.toString().asValue()
|
|
||||||
PlcValueType.TIME_OF_DAY -> this@toMeta.time.toString().asValue()
|
|
||||||
PlcValueType.LTIME_OF_DAY -> this@toMeta.time.toString().asValue()
|
|
||||||
PlcValueType.DATE_AND_TIME -> this@toMeta.dateTime.toString().asValue()
|
|
||||||
PlcValueType.DATE_AND_LTIME -> this@toMeta.dateTime.toString().asValue()
|
|
||||||
PlcValueType.LDATE_AND_TIME -> this@toMeta.dateTime.toString().asValue()
|
|
||||||
PlcValueType.Struct -> this@toMeta.struct.forEach { (name, item) ->
|
|
||||||
set(name, item.toMeta())
|
|
||||||
}
|
|
||||||
|
|
||||||
PlcValueType.List -> {
|
|
||||||
val listOfMeta = this@toMeta.list.map { it.toMeta() }
|
|
||||||
if (listOfMeta.all { it.items.isEmpty() }) {
|
|
||||||
value = listOfMeta.map { it.value ?: Null }.asValue()
|
|
||||||
} else {
|
|
||||||
setIndexed("@list".asName(), list.map { it.toMeta() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlcValueType.RAW_BYTE_ARRAY -> this@toMeta.raw.asValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Value.toPlcValue(): PlcValue = when (type) {
|
|
||||||
ValueType.NUMBER -> when (val number = number) {
|
|
||||||
is Short -> PlcINT(number.toShort())
|
|
||||||
is Int -> PlcDINT(number.toInt())
|
|
||||||
is Long -> PlcLINT(number.toLong())
|
|
||||||
is Float -> PlcREAL(number.toFloat())
|
|
||||||
else -> PlcLREAL(number.toDouble())
|
|
||||||
}
|
|
||||||
|
|
||||||
ValueType.STRING -> PlcSTRING(string)
|
|
||||||
ValueType.BOOLEAN -> PlcBOOL(boolean)
|
|
||||||
ValueType.NULL -> PlcNull()
|
|
||||||
ValueType.LIST -> TODO()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun Meta.toPlcValue(hint: PlcValueType): PlcValue = when (hint) {
|
|
||||||
PlcValueType.Struct -> PlcStruct(
|
|
||||||
items.entries.associate { (token, item) ->
|
|
||||||
token.toString() to item.toPlcValue(PlcValueType.Struct)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
PlcValueType.NULL -> PlcNull()
|
|
||||||
PlcValueType.BOOL -> PlcBOOL(boolean)
|
|
||||||
PlcValueType.BYTE -> PlcBYTE(int)
|
|
||||||
PlcValueType.WORD -> PlcWORD(int)
|
|
||||||
PlcValueType.DWORD -> PlcDWORD(int)
|
|
||||||
PlcValueType.LWORD -> PlcLWORD(long)
|
|
||||||
PlcValueType.USINT -> PlcLWORD(short)
|
|
||||||
PlcValueType.UINT -> PlcUINT(int)
|
|
||||||
PlcValueType.UDINT -> PlcDINT(long)
|
|
||||||
PlcValueType.ULINT -> (number as? BigInteger)?.let { PlcULINT(it) } ?: PlcULINT(long)
|
|
||||||
PlcValueType.SINT -> PlcSINT(int)
|
|
||||||
PlcValueType.INT -> PlcINT(int)
|
|
||||||
PlcValueType.DINT -> PlcDINT(int)
|
|
||||||
PlcValueType.LINT -> PlcLINT(long)
|
|
||||||
PlcValueType.REAL -> PlcREAL(float)
|
|
||||||
PlcValueType.LREAL -> PlcLREAL(double)
|
|
||||||
PlcValueType.CHAR -> PlcCHAR(int)
|
|
||||||
PlcValueType.WCHAR -> PlcWCHAR(short)
|
|
||||||
PlcValueType.STRING -> PlcSTRING(string)
|
|
||||||
PlcValueType.WSTRING -> PlcWSTRING(string)
|
|
||||||
PlcValueType.TIME -> PlcTIME(string?.let { java.time.Duration.parse(it) })
|
|
||||||
PlcValueType.LTIME -> PlcLTIME(string?.let { java.time.Duration.parse(it) })
|
|
||||||
PlcValueType.DATE -> PlcDATE(string?.let { java.time.LocalDate.parse(it) })
|
|
||||||
PlcValueType.LDATE -> PlcLDATE(string?.let { java.time.LocalDate.parse(it) })
|
|
||||||
PlcValueType.TIME_OF_DAY -> PlcTIME_OF_DAY(string?.let { java.time.LocalTime.parse(it) })
|
|
||||||
PlcValueType.LTIME_OF_DAY -> PlcLTIME_OF_DAY(string?.let { java.time.LocalTime.parse(it) })
|
|
||||||
PlcValueType.DATE_AND_TIME -> PlcDATE_AND_TIME(string?.let { java.time.LocalDateTime.parse(it) })
|
|
||||||
PlcValueType.DATE_AND_LTIME -> PlcDATE_AND_LTIME(string?.let { java.time.LocalDateTime.parse(it) })
|
|
||||||
PlcValueType.LDATE_AND_TIME -> PlcLDATE_AND_TIME(string?.let { java.time.LocalDateTime.parse(it) })
|
|
||||||
PlcValueType.List -> PlcList().apply {
|
|
||||||
value?.list?.forEach { add(it.toPlcValue()) }
|
|
||||||
getIndexed("@list").forEach { (_, meta) ->
|
|
||||||
if (meta.items.isEmpty()) {
|
|
||||||
meta.value?.let { add(it.toPlcValue()) }
|
|
||||||
} else {
|
|
||||||
add(meta.toPlcValue(PlcValueType.Struct))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlcValueType.RAW_BYTE_ARRAY -> PlcRawByteArray(
|
|
||||||
value?.list?.map { it.number.toByte() }?.toByteArray() ?: error("The meta content is not byte array")
|
|
||||||
)
|
|
||||||
}
|
|
@ -6,16 +6,18 @@ Implementation of byte ports on top os ktor-io asynchronous API
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.3.0`.
|
The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.2.0`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
//uncomment to access development builds
|
||||||
|
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-ports-ktor:0.3.0")
|
implementation("space.kscience:controls-ports-ktor:0.2.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -13,7 +13,7 @@ public class KtorPortsPlugin : AbstractPlugin() {
|
|||||||
override val tag: PluginTag get() = Companion.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = when (target) {
|
override fun content(target: String): Map<Name, Any> = when (target) {
|
||||||
Ports.ASYNCHRONOUS_PORT_TYPE -> mapOf("tcp".asName() to KtorTcpPort, "udp".asName() to KtorUdpPort)
|
PortFactory.TYPE -> mapOf("tcp".asName() to KtorTcpPort, "udp".asName() to KtorUdpPort)
|
||||||
else -> emptyMap()
|
else -> emptyMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,53 +1,47 @@
|
|||||||
package space.kscience.controls.ports
|
package space.kscience.controls.ports
|
||||||
|
|
||||||
import io.ktor.network.selector.ActorSelectorManager
|
import io.ktor.network.selector.ActorSelectorManager
|
||||||
import io.ktor.network.sockets.SocketOptions
|
|
||||||
import io.ktor.network.sockets.aSocket
|
import io.ktor.network.sockets.aSocket
|
||||||
import io.ktor.network.sockets.openReadChannel
|
import io.ktor.network.sockets.openReadChannel
|
||||||
import io.ktor.network.sockets.openWriteChannel
|
import io.ktor.network.sockets.openWriteChannel
|
||||||
import io.ktor.utils.io.consumeEachBufferRange
|
import io.ktor.utils.io.consumeEachBufferRange
|
||||||
import io.ktor.utils.io.core.Closeable
|
import io.ktor.utils.io.core.Closeable
|
||||||
import io.ktor.utils.io.writeAvailable
|
import io.ktor.utils.io.writeAvailable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.get
|
import space.kscience.dataforge.meta.get
|
||||||
import space.kscience.dataforge.meta.int
|
import space.kscience.dataforge.meta.int
|
||||||
import space.kscience.dataforge.meta.string
|
import space.kscience.dataforge.meta.string
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
public class KtorTcpPort internal constructor(
|
public class KtorTcpPort internal constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
meta: Meta,
|
|
||||||
public val host: String,
|
public val host: String,
|
||||||
public val port: Int,
|
public val port: Int,
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {},
|
) : AbstractPort(context, coroutineContext), Closeable {
|
||||||
) : AbstractAsynchronousPort(context, meta, coroutineContext), Closeable {
|
|
||||||
|
|
||||||
override fun toString(): String = "port[tcp:$host:$port]"
|
override fun toString(): String = "port[tcp:$host:$port]"
|
||||||
|
|
||||||
private val futureSocket = scope.async(Dispatchers.IO, start = CoroutineStart.LAZY) {
|
private val futureSocket = scope.async {
|
||||||
aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(host, port, socketOptions)
|
aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(host, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val writeChannel = scope.async(Dispatchers.IO, start = CoroutineStart.LAZY) {
|
private val writeChannel = scope.async {
|
||||||
futureSocket.await().openWriteChannel(true)
|
futureSocket.await().openWriteChannel(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var listenerJob: Job? = null
|
private val listenerJob = scope.launch {
|
||||||
|
val input = futureSocket.await().openReadChannel()
|
||||||
override fun onOpen() {
|
input.consumeEachBufferRange { buffer, _ ->
|
||||||
listenerJob = scope.launch {
|
val array = ByteArray(buffer.remaining())
|
||||||
val input = futureSocket.await().openReadChannel()
|
buffer.get(array)
|
||||||
input.consumeEachBufferRange { buffer: ByteBuffer, last ->
|
receive(array)
|
||||||
val array = ByteArray(buffer.remaining())
|
isActive
|
||||||
buffer.get(array)
|
|
||||||
receive(array)
|
|
||||||
!last && isActive
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,45 +49,29 @@ public class KtorTcpPort internal constructor(
|
|||||||
writeChannel.await().writeAvailable(data)
|
writeChannel.await().writeAvailable(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val isOpen: Boolean
|
|
||||||
get() = listenerJob?.isActive == true
|
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
listenerJob?.cancel()
|
listenerJob.cancel()
|
||||||
futureSocket.cancel()
|
futureSocket.cancel()
|
||||||
super.close()
|
super.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
public companion object : Factory<AsynchronousPort> {
|
public companion object : PortFactory {
|
||||||
|
|
||||||
public fun build(
|
override val type: String = "tcp"
|
||||||
context: Context,
|
|
||||||
host: String,
|
|
||||||
port: Int,
|
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
|
||||||
socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {},
|
|
||||||
): KtorTcpPort {
|
|
||||||
val meta = Meta {
|
|
||||||
"name" put "tcp://$host:$port"
|
|
||||||
"type" put "tcp"
|
|
||||||
"host" put host
|
|
||||||
"port" put port
|
|
||||||
}
|
|
||||||
return KtorTcpPort(context, meta, host, port, coroutineContext, socketOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun open(
|
public fun open(
|
||||||
context: Context,
|
context: Context,
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {},
|
): KtorTcpPort {
|
||||||
): KtorTcpPort = build(context, host, port, coroutineContext, socketOptions).apply { open() }
|
return KtorTcpPort(context, host, port, coroutineContext)
|
||||||
|
}
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): AsynchronousPort {
|
override fun build(context: Context, meta: Meta): Port {
|
||||||
val host = meta["host"].string ?: "localhost"
|
val host = meta["host"].string ?: "localhost"
|
||||||
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
|
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
|
||||||
return build(context, host, port)
|
return open(context, host, port)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,14 +1,18 @@
|
|||||||
package space.kscience.controls.ports
|
package space.kscience.controls.ports
|
||||||
|
|
||||||
import io.ktor.network.selector.ActorSelectorManager
|
import io.ktor.network.selector.ActorSelectorManager
|
||||||
import io.ktor.network.sockets.*
|
import io.ktor.network.sockets.InetSocketAddress
|
||||||
import io.ktor.utils.io.ByteWriteChannel
|
import io.ktor.network.sockets.aSocket
|
||||||
|
import io.ktor.network.sockets.openReadChannel
|
||||||
|
import io.ktor.network.sockets.openWriteChannel
|
||||||
import io.ktor.utils.io.consumeEachBufferRange
|
import io.ktor.utils.io.consumeEachBufferRange
|
||||||
import io.ktor.utils.io.core.Closeable
|
import io.ktor.utils.io.core.Closeable
|
||||||
import io.ktor.utils.io.writeAvailable
|
import io.ktor.utils.io.writeAvailable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.int
|
import space.kscience.dataforge.meta.int
|
||||||
import space.kscience.dataforge.meta.number
|
import space.kscience.dataforge.meta.number
|
||||||
@ -17,40 +21,33 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
|
|
||||||
public class KtorUdpPort internal constructor(
|
public class KtorUdpPort internal constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
meta: Meta,
|
|
||||||
public val remoteHost: String,
|
public val remoteHost: String,
|
||||||
public val remotePort: Int,
|
public val remotePort: Int,
|
||||||
public val localPort: Int? = null,
|
public val localPort: Int? = null,
|
||||||
public val localHost: String = "localhost",
|
public val localHost: String = "localhost",
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {},
|
) : AbstractPort(context, coroutineContext), Closeable {
|
||||||
) : AbstractAsynchronousPort(context, meta, coroutineContext), Closeable {
|
|
||||||
|
|
||||||
override fun toString(): String = "port[udp:$remoteHost:$remotePort]"
|
override fun toString(): String = "port[udp:$remoteHost:$remotePort]"
|
||||||
|
|
||||||
private val futureSocket = scope.async(Dispatchers.IO, start = CoroutineStart.LAZY) {
|
private val futureSocket = scope.async {
|
||||||
aSocket(ActorSelectorManager(Dispatchers.IO)).udp().connect(
|
aSocket(ActorSelectorManager(Dispatchers.IO)).udp().connect(
|
||||||
remoteAddress = InetSocketAddress(remoteHost, remotePort),
|
remoteAddress = InetSocketAddress(remoteHost, remotePort),
|
||||||
localAddress = localPort?.let { InetSocketAddress(localHost, localPort) },
|
localAddress = localPort?.let { InetSocketAddress(localHost, localPort) }
|
||||||
configure = socketOptions
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val writeChannel: Deferred<ByteWriteChannel> = scope.async(Dispatchers.IO, start = CoroutineStart.LAZY) {
|
private val writeChannel = scope.async {
|
||||||
futureSocket.await().openWriteChannel(true)
|
futureSocket.await().openWriteChannel(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var listenerJob: Job? = null
|
private val listenerJob = scope.launch {
|
||||||
|
val input = futureSocket.await().openReadChannel()
|
||||||
override fun onOpen() {
|
input.consumeEachBufferRange { buffer, _ ->
|
||||||
listenerJob = scope.launch {
|
val array = ByteArray(buffer.remaining())
|
||||||
val input = futureSocket.await().openReadChannel()
|
buffer.get(array)
|
||||||
input.consumeEachBufferRange { buffer, last ->
|
receive(array)
|
||||||
val array = ByteArray(buffer.remaining())
|
isActive
|
||||||
buffer.get(array)
|
|
||||||
receive(array)
|
|
||||||
!last && isActive
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,49 +55,16 @@ public class KtorUdpPort internal constructor(
|
|||||||
writeChannel.await().writeAvailable(data)
|
writeChannel.await().writeAvailable(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val isOpen: Boolean
|
|
||||||
get() = listenerJob?.isActive == true
|
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
listenerJob?.cancel()
|
listenerJob.cancel()
|
||||||
futureSocket.cancel()
|
futureSocket.cancel()
|
||||||
super.close()
|
super.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
public companion object : Factory<AsynchronousPort> {
|
public companion object : PortFactory {
|
||||||
|
|
||||||
public fun build(
|
override val type: String = "udp"
|
||||||
context: Context,
|
|
||||||
remoteHost: String,
|
|
||||||
remotePort: Int,
|
|
||||||
localPort: Int? = null,
|
|
||||||
localHost: String? = null,
|
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
|
||||||
socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {},
|
|
||||||
): KtorUdpPort {
|
|
||||||
val meta = Meta {
|
|
||||||
"name" put "udp://$remoteHost:$remotePort"
|
|
||||||
"type" put "udp"
|
|
||||||
"remoteHost" put remoteHost
|
|
||||||
"remotePort" put remotePort
|
|
||||||
localHost?.let { "localHost" put it }
|
|
||||||
localPort?.let { "localPort" put it }
|
|
||||||
}
|
|
||||||
return KtorUdpPort(
|
|
||||||
context = context,
|
|
||||||
meta = meta,
|
|
||||||
remoteHost = remoteHost,
|
|
||||||
remotePort = remotePort,
|
|
||||||
localPort = localPort,
|
|
||||||
localHost = localHost ?: "localhost",
|
|
||||||
coroutineContext = coroutineContext,
|
|
||||||
socketOptions = socketOptions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and open UDP port
|
|
||||||
*/
|
|
||||||
public fun open(
|
public fun open(
|
||||||
context: Context,
|
context: Context,
|
||||||
remoteHost: String,
|
remoteHost: String,
|
||||||
@ -108,23 +72,16 @@ public class KtorUdpPort internal constructor(
|
|||||||
localPort: Int? = null,
|
localPort: Int? = null,
|
||||||
localHost: String = "localhost",
|
localHost: String = "localhost",
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {},
|
): KtorUdpPort {
|
||||||
): KtorUdpPort = build(
|
return KtorUdpPort(context, remoteHost, remotePort, localPort, localHost, coroutineContext)
|
||||||
context,
|
}
|
||||||
remoteHost,
|
|
||||||
remotePort,
|
|
||||||
localPort,
|
|
||||||
localHost,
|
|
||||||
coroutineContext,
|
|
||||||
socketOptions
|
|
||||||
).apply { open() }
|
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): AsynchronousPort {
|
override fun build(context: Context, meta: Meta): Port {
|
||||||
val remoteHost by meta.string { error("Remote host is not specified") }
|
val remoteHost by meta.string { error("Remote host is not specified") }
|
||||||
val remotePort by meta.number { error("Remote port is not specified") }
|
val remotePort by meta.number { error("Remote port is not specified") }
|
||||||
val localHost: String? by meta.string()
|
val localHost: String? by meta.string()
|
||||||
val localPort: Int? by meta.int()
|
val localPort: Int? by meta.int()
|
||||||
return build(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost")
|
return open(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,16 +6,18 @@ Implementation of direct serial port communication with JSerialComm
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-serial:0.3.0`.
|
The Maven coordinates of this project are `space.kscience:controls-serial:0.2.0`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
//uncomment to access development builds
|
||||||
|
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-serial:0.3.0")
|
implementation("space.kscience:controls-serial:0.2.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1,134 +0,0 @@
|
|||||||
package space.kscience.controls.serial
|
|
||||||
|
|
||||||
import com.fazecast.jSerialComm.SerialPort
|
|
||||||
import com.fazecast.jSerialComm.SerialPortDataListener
|
|
||||||
import com.fazecast.jSerialComm.SerialPortEvent
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import space.kscience.controls.ports.AbstractAsynchronousPort
|
|
||||||
import space.kscience.controls.ports.AsynchronousPort
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.int
|
|
||||||
import space.kscience.dataforge.meta.string
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A port based on JSerialComm
|
|
||||||
*/
|
|
||||||
public class AsynchronousSerialPort(
|
|
||||||
context: Context,
|
|
||||||
meta: Meta,
|
|
||||||
private val comPort: SerialPort,
|
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
|
||||||
) : AbstractAsynchronousPort(context, meta, coroutineContext) {
|
|
||||||
|
|
||||||
override fun toString(): String = "port[${comPort.descriptivePortName}]"
|
|
||||||
|
|
||||||
private val serialPortListener = object : SerialPortDataListener {
|
|
||||||
override fun getListeningEvents(): Int =
|
|
||||||
SerialPort.LISTENING_EVENT_DATA_RECEIVED and SerialPort.LISTENING_EVENT_DATA_AVAILABLE
|
|
||||||
|
|
||||||
override fun serialEvent(event: SerialPortEvent) {
|
|
||||||
when (event.eventType) {
|
|
||||||
SerialPort.LISTENING_EVENT_DATA_RECEIVED -> {
|
|
||||||
scope.launch { receive(event.receivedData) }
|
|
||||||
}
|
|
||||||
|
|
||||||
SerialPort.LISTENING_EVENT_DATA_AVAILABLE -> {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
val available = comPort.bytesAvailable()
|
|
||||||
if (available > 0) {
|
|
||||||
val buffer = ByteArray(available)
|
|
||||||
comPort.readBytes(buffer, available)
|
|
||||||
receive(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOpen() {
|
|
||||||
comPort.openPort()
|
|
||||||
comPort.addDataListener(serialPortListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val isOpen: Boolean get() = comPort.isOpen
|
|
||||||
|
|
||||||
override suspend fun write(data: ByteArray) {
|
|
||||||
comPort.writeBytes(data, data.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
comPort.removeDataListener()
|
|
||||||
if (comPort.isOpen) {
|
|
||||||
comPort.closePort()
|
|
||||||
}
|
|
||||||
super.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
public companion object : Factory<AsynchronousPort> {
|
|
||||||
|
|
||||||
public fun build(
|
|
||||||
context: Context,
|
|
||||||
portName: String,
|
|
||||||
baudRate: Int = 9600,
|
|
||||||
dataBits: Int = 8,
|
|
||||||
stopBits: Int = SerialPort.ONE_STOP_BIT,
|
|
||||||
parity: Int = SerialPort.NO_PARITY,
|
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
|
||||||
additionalConfig: SerialPort.() -> Unit = {},
|
|
||||||
): AsynchronousSerialPort {
|
|
||||||
val serialPort = SerialPort.getCommPort(portName).apply {
|
|
||||||
setComPortParameters(baudRate, dataBits, stopBits, parity)
|
|
||||||
additionalConfig()
|
|
||||||
}
|
|
||||||
val meta = Meta {
|
|
||||||
"name" put "com://$portName"
|
|
||||||
"type" put "serial"
|
|
||||||
"baudRate" put serialPort.baudRate
|
|
||||||
"dataBits" put serialPort.numDataBits
|
|
||||||
"stopBits" put serialPort.numStopBits
|
|
||||||
"parity" put serialPort.parity
|
|
||||||
}
|
|
||||||
return AsynchronousSerialPort(context, meta, serialPort, coroutineContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct ComPort with given parameters
|
|
||||||
*/
|
|
||||||
public fun open(
|
|
||||||
context: Context,
|
|
||||||
portName: String,
|
|
||||||
baudRate: Int = 9600,
|
|
||||||
dataBits: Int = 8,
|
|
||||||
stopBits: Int = SerialPort.ONE_STOP_BIT,
|
|
||||||
parity: Int = SerialPort.NO_PARITY,
|
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
|
||||||
additionalConfig: SerialPort.() -> Unit = {},
|
|
||||||
): AsynchronousSerialPort = build(
|
|
||||||
context = context,
|
|
||||||
portName = portName,
|
|
||||||
baudRate = baudRate,
|
|
||||||
dataBits = dataBits,
|
|
||||||
stopBits = stopBits,
|
|
||||||
parity = parity,
|
|
||||||
coroutineContext = coroutineContext,
|
|
||||||
additionalConfig = additionalConfig
|
|
||||||
).apply { open() }
|
|
||||||
|
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): AsynchronousPort {
|
|
||||||
val name by meta.string { error("Serial port name not defined") }
|
|
||||||
val baudRate by meta.int(9600)
|
|
||||||
val dataBits by meta.int(8)
|
|
||||||
val stopBits by meta.int(SerialPort.ONE_STOP_BIT)
|
|
||||||
val parity by meta.int(SerialPort.NO_PARITY)
|
|
||||||
return build(context, name, baudRate, dataBits, stopBits, parity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,87 @@
|
|||||||
|
package space.kscience.controls.serial
|
||||||
|
|
||||||
|
import com.fazecast.jSerialComm.SerialPort
|
||||||
|
import com.fazecast.jSerialComm.SerialPortDataListener
|
||||||
|
import com.fazecast.jSerialComm.SerialPortEvent
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import space.kscience.controls.ports.AbstractPort
|
||||||
|
import space.kscience.controls.ports.Port
|
||||||
|
import space.kscience.controls.ports.PortFactory
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.int
|
||||||
|
import space.kscience.dataforge.meta.string
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A port based on JSerialComm
|
||||||
|
*/
|
||||||
|
public class JSerialCommPort(
|
||||||
|
context: Context,
|
||||||
|
private val comPort: SerialPort,
|
||||||
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
|
) : AbstractPort(context, coroutineContext) {
|
||||||
|
|
||||||
|
override fun toString(): String = "port[${comPort.descriptivePortName}]"
|
||||||
|
|
||||||
|
private val serialPortListener = object : SerialPortDataListener {
|
||||||
|
override fun getListeningEvents(): Int = SerialPort.LISTENING_EVENT_DATA_AVAILABLE
|
||||||
|
|
||||||
|
override fun serialEvent(event: SerialPortEvent) {
|
||||||
|
if (event.eventType == SerialPort.LISTENING_EVENT_DATA_AVAILABLE) {
|
||||||
|
scope.launch { receive(event.receivedData) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
comPort.addDataListener(serialPortListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun write(data: ByteArray) {
|
||||||
|
comPort.writeBytes(data, data.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
comPort.removeDataListener()
|
||||||
|
if (comPort.isOpen) {
|
||||||
|
comPort.closePort()
|
||||||
|
}
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object : PortFactory {
|
||||||
|
|
||||||
|
override val type: String = "com"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct ComPort with given parameters
|
||||||
|
*/
|
||||||
|
public fun open(
|
||||||
|
context: Context,
|
||||||
|
portName: String,
|
||||||
|
baudRate: Int = 9600,
|
||||||
|
dataBits: Int = 8,
|
||||||
|
stopBits: Int = SerialPort.ONE_STOP_BIT,
|
||||||
|
parity: Int = SerialPort.NO_PARITY,
|
||||||
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
|
): JSerialCommPort {
|
||||||
|
val serialPort = SerialPort.getCommPort(portName).apply {
|
||||||
|
setComPortParameters(baudRate, dataBits, stopBits, parity)
|
||||||
|
openPort()
|
||||||
|
}
|
||||||
|
return JSerialCommPort(context, serialPort, coroutineContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun build(context: Context, meta: Meta): Port {
|
||||||
|
val name by meta.string { error("Serial port name not defined") }
|
||||||
|
val baudRate by meta.int(9600)
|
||||||
|
val dataBits by meta.int(8)
|
||||||
|
val stopBits by meta.int(SerialPort.ONE_STOP_BIT)
|
||||||
|
val parity by meta.int(SerialPort.NO_PARITY)
|
||||||
|
return open(context, name, baudRate, dataBits, stopBits, parity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,27 +1,19 @@
|
|||||||
package space.kscience.controls.serial
|
package space.kscience.controls.serial
|
||||||
|
|
||||||
import space.kscience.controls.ports.Ports
|
import space.kscience.controls.ports.PortFactory
|
||||||
import space.kscience.dataforge.context.AbstractPlugin
|
import space.kscience.dataforge.context.AbstractPlugin
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.PluginFactory
|
import space.kscience.dataforge.context.PluginFactory
|
||||||
import space.kscience.dataforge.context.PluginTag
|
import space.kscience.dataforge.context.PluginTag
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.asName
|
|
||||||
|
|
||||||
public class SerialPortPlugin : AbstractPlugin() {
|
public class SerialPortPlugin : AbstractPlugin() {
|
||||||
|
|
||||||
override val tag: PluginTag get() = Companion.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = when (target) {
|
override fun content(target: String): Map<Name, Any> = when(target){
|
||||||
Ports.ASYNCHRONOUS_PORT_TYPE -> mapOf(
|
PortFactory.TYPE -> mapOf(Name.EMPTY to JSerialCommPort)
|
||||||
"serial".asName() to AsynchronousSerialPort,
|
|
||||||
)
|
|
||||||
|
|
||||||
Ports.SYNCHRONOUS_PORT_TYPE -> mapOf(
|
|
||||||
"serial".asName() to SynchronousSerialPort,
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> emptyMap()
|
else -> emptyMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,140 +0,0 @@
|
|||||||
package space.kscience.controls.serial
|
|
||||||
|
|
||||||
import com.fazecast.jSerialComm.SerialPort
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import space.kscience.controls.ports.SynchronousPort
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.context.error
|
|
||||||
import space.kscience.dataforge.context.logger
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.int
|
|
||||||
import space.kscience.dataforge.meta.string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A port based on JSerialComm
|
|
||||||
*/
|
|
||||||
public class SynchronousSerialPort(
|
|
||||||
override val context: Context,
|
|
||||||
public val meta: Meta,
|
|
||||||
private val comPort: SerialPort,
|
|
||||||
) : SynchronousPort {
|
|
||||||
|
|
||||||
override fun toString(): String = "port[${comPort.descriptivePortName}]"
|
|
||||||
|
|
||||||
|
|
||||||
override fun open() {
|
|
||||||
if (!isOpen) {
|
|
||||||
comPort.openPort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val isOpen: Boolean get() = comPort.isOpen
|
|
||||||
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
if (comPort.isOpen) {
|
|
||||||
comPort.closePort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mutex = Mutex()
|
|
||||||
|
|
||||||
override suspend fun <R> respond(
|
|
||||||
request: ByteArray,
|
|
||||||
transform: suspend Flow<ByteArray>.() -> R,
|
|
||||||
): R = mutex.withLock {
|
|
||||||
comPort.flushIOBuffers()
|
|
||||||
comPort.writeBytes(request, request.size)
|
|
||||||
flow<ByteArray> {
|
|
||||||
while (isOpen) {
|
|
||||||
try {
|
|
||||||
val available = comPort.bytesAvailable()
|
|
||||||
if (available > 0) {
|
|
||||||
val buffer = ByteArray(available)
|
|
||||||
comPort.readBytes(buffer, available)
|
|
||||||
emit(buffer)
|
|
||||||
} else if (available < 0) break
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
logger.error(ex) { "Channel read error" }
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.transform()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun respondFixedMessageSize(request: ByteArray, responseSize: Int): ByteArray = mutex.withLock {
|
|
||||||
runInterruptible {
|
|
||||||
comPort.flushIOBuffers()
|
|
||||||
comPort.writeBytes(request, request.size)
|
|
||||||
val buffer = ByteArray(responseSize)
|
|
||||||
comPort.readBytes(buffer, responseSize)
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public companion object : Factory<SynchronousPort> {
|
|
||||||
|
|
||||||
public fun build(
|
|
||||||
context: Context,
|
|
||||||
portName: String,
|
|
||||||
baudRate: Int = 9600,
|
|
||||||
dataBits: Int = 8,
|
|
||||||
stopBits: Int = SerialPort.ONE_STOP_BIT,
|
|
||||||
parity: Int = SerialPort.NO_PARITY,
|
|
||||||
additionalConfig: SerialPort.() -> Unit = {},
|
|
||||||
): SynchronousSerialPort {
|
|
||||||
val serialPort = SerialPort.getCommPort(portName).apply {
|
|
||||||
setComPortParameters(baudRate, dataBits, stopBits, parity)
|
|
||||||
additionalConfig()
|
|
||||||
}
|
|
||||||
val meta = Meta {
|
|
||||||
"name" put "com://$portName"
|
|
||||||
"type" put "serial"
|
|
||||||
"baudRate" put serialPort.baudRate
|
|
||||||
"dataBits" put serialPort.numDataBits
|
|
||||||
"stopBits" put serialPort.numStopBits
|
|
||||||
"parity" put serialPort.parity
|
|
||||||
}
|
|
||||||
return SynchronousSerialPort(context, meta, serialPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct ComPort with given parameters
|
|
||||||
*/
|
|
||||||
public fun open(
|
|
||||||
context: Context,
|
|
||||||
portName: String,
|
|
||||||
baudRate: Int = 9600,
|
|
||||||
dataBits: Int = 8,
|
|
||||||
stopBits: Int = SerialPort.ONE_STOP_BIT,
|
|
||||||
parity: Int = SerialPort.NO_PARITY,
|
|
||||||
additionalConfig: SerialPort.() -> Unit = {},
|
|
||||||
): SynchronousSerialPort = build(
|
|
||||||
context = context,
|
|
||||||
portName = portName,
|
|
||||||
baudRate = baudRate,
|
|
||||||
dataBits = dataBits,
|
|
||||||
stopBits = stopBits,
|
|
||||||
parity = parity,
|
|
||||||
additionalConfig = additionalConfig
|
|
||||||
).apply { open() }
|
|
||||||
|
|
||||||
|
|
||||||
override fun build(context: Context, meta: Meta): SynchronousPort {
|
|
||||||
val name by meta.string { error("Serial port name not defined") }
|
|
||||||
val baudRate by meta.int(9600)
|
|
||||||
val dataBits by meta.int(8)
|
|
||||||
val stopBits by meta.int(SerialPort.ONE_STOP_BIT)
|
|
||||||
val parity by meta.int(SerialPort.NO_PARITY)
|
|
||||||
return build(context, name, baudRate, dataBits, stopBits, parity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -6,16 +6,18 @@ A combined Magix event loop server with web server for visualization.
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-server:0.3.0`.
|
The Maven coordinates of this project are `space.kscience:controls-server:0.2.0`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
//uncomment to access development builds
|
||||||
|
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-server:0.3.0")
|
implementation("space.kscience:controls-server:0.2.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import space.kscience.gradle.Maturity
|
import space.kscience.gradle.Maturity
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("space.kscience.gradle.mpp")
|
id("space.kscience.gradle.jvm")
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,20 +9,16 @@ description = """
|
|||||||
A combined Magix event loop server with web server for visualization.
|
A combined Magix event loop server with web server for visualization.
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
|
dependencies {
|
||||||
kscience {
|
implementation(projects.controlsCore)
|
||||||
jvm()
|
implementation(projects.controlsPortsKtor)
|
||||||
dependencies {
|
implementation(projects.magix.magixServer)
|
||||||
implementation(projects.controlsCore)
|
implementation(spclibs.ktor.server.cio)
|
||||||
implementation(projects.controlsPortsKtor)
|
implementation(spclibs.ktor.server.websockets)
|
||||||
implementation(projects.magix.magixServer)
|
implementation(spclibs.ktor.server.content.negotiation)
|
||||||
implementation(spclibs.ktor.server.cio)
|
implementation(spclibs.ktor.serialization.kotlinx.json)
|
||||||
implementation(spclibs.ktor.server.websockets)
|
implementation(spclibs.ktor.server.html.builder)
|
||||||
implementation(spclibs.ktor.server.content.negotiation)
|
implementation(spclibs.ktor.server.status.pages)
|
||||||
implementation(spclibs.ktor.serialization.kotlinx.json)
|
|
||||||
implementation(spclibs.ktor.server.html.builder)
|
|
||||||
implementation(spclibs.ktor.server.status.pages)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readme{
|
readme{
|
||||||
|
@ -157,8 +157,8 @@ public fun Application.deviceManagerModule(
|
|||||||
val body = call.receiveText()
|
val body = call.receiveText()
|
||||||
val request: DeviceMessage = MagixEndpoint.magixJson.decodeFromString(DeviceMessage.serializer(), body)
|
val request: DeviceMessage = MagixEndpoint.magixJson.decodeFromString(DeviceMessage.serializer(), body)
|
||||||
val response = manager.respondHubMessage(request)
|
val response = manager.respondHubMessage(request)
|
||||||
if (response.isNotEmpty()) {
|
if (response != null) {
|
||||||
call.respondMessages(response)
|
call.respondMessage(response)
|
||||||
} else {
|
} else {
|
||||||
call.respondText("No response")
|
call.respondText("No response")
|
||||||
}
|
}
|
||||||
@ -177,9 +177,9 @@ public fun Application.deviceManagerModule(
|
|||||||
property = property,
|
property = property,
|
||||||
)
|
)
|
||||||
|
|
||||||
val responses = manager.respondHubMessage(request)
|
val response = manager.respondHubMessage(request)
|
||||||
if (responses.isNotEmpty()) {
|
if (response != null) {
|
||||||
call.respondMessages(responses)
|
call.respondMessage(response)
|
||||||
} else {
|
} else {
|
||||||
call.respond(HttpStatusCode.InternalServerError)
|
call.respond(HttpStatusCode.InternalServerError)
|
||||||
}
|
}
|
||||||
@ -197,9 +197,9 @@ public fun Application.deviceManagerModule(
|
|||||||
value = json.toMeta()
|
value = json.toMeta()
|
||||||
)
|
)
|
||||||
|
|
||||||
val responses = manager.respondHubMessage(request)
|
val response = manager.respondHubMessage(request)
|
||||||
if (responses.isNotEmpty()) {
|
if (response != null) {
|
||||||
call.respondMessages(responses)
|
call.respondMessage(response)
|
||||||
} else {
|
} else {
|
||||||
call.respond(HttpStatusCode.InternalServerError)
|
call.respond(HttpStatusCode.InternalServerError)
|
||||||
}
|
}
|
@ -5,7 +5,6 @@ import io.ktor.server.application.ApplicationCall
|
|||||||
import io.ktor.server.response.respondText
|
import io.ktor.server.response.respondText
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
import kotlinx.serialization.json.JsonObjectBuilder
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.serializer
|
|
||||||
import space.kscience.controls.api.DeviceMessage
|
import space.kscience.controls.api.DeviceMessage
|
||||||
import space.kscience.magix.api.MagixEndpoint
|
import space.kscience.magix.api.MagixEndpoint
|
||||||
|
|
||||||
@ -26,7 +25,7 @@ internal suspend fun ApplicationCall.respondJson(builder: JsonObjectBuilder.() -
|
|||||||
respondText(json.toString(), contentType = ContentType.Application.Json)
|
respondText(json.toString(), contentType = ContentType.Application.Json)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun ApplicationCall.respondMessages(messages: List<DeviceMessage>): Unit = respondText(
|
internal suspend fun ApplicationCall.respondMessage(message: DeviceMessage): Unit = respondText(
|
||||||
MagixEndpoint.magixJson.encodeToString(serializer<List<DeviceMessage>>(), messages),
|
MagixEndpoint.magixJson.encodeToString(DeviceMessage.serializer(), message),
|
||||||
contentType = ContentType.Application.Json
|
contentType = ContentType.Application.Json
|
||||||
)
|
)
|
@ -6,16 +6,18 @@ An API for stand-alone Controls-kt device or a hub.
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-storage:0.3.0`.
|
The Maven coordinates of this project are `space.kscience:controls-storage:0.2.0`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
//uncomment to access development builds
|
||||||
|
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-storage:0.3.0")
|
implementation("space.kscience:controls-storage:0.2.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -6,16 +6,18 @@ An implementation of controls-storage on top of JetBrains Xodus.
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-xodus:0.3.0`.
|
The Maven coordinates of this project are `space.kscience:controls-xodus:0.2.0`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
//uncomment to access development builds
|
||||||
|
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-xodus:0.3.0")
|
implementation("space.kscience:controls-xodus:0.2.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user