Compare commits
44 Commits
master
...
refactor/d
Author | SHA1 | Date | |
---|---|---|---|
a12cf440e8 | |||
606c2cf5b1 | |||
fb03fcc982 | |||
cf129b6242 | |||
81d6b672cf | |||
07cc41c645 | |||
0c647cff30 | |||
b539c2046a | |||
afee2f0a02 | |||
fb8ee59f14 | |||
74301afb42 | |||
fe98a836f8 | |||
0c128bce36 | |||
4e17c9051c | |||
0f687c3c51 | |||
53fc240c75 | |||
825f1a4d04 | |||
0443fdc3c0 | |||
78b18ebda6 | |||
0e963a7b13 | |||
2698cee80b | |||
811477a636 | |||
984e7f12ef | |||
1414cf5a2f | |||
1fcdbdc9f4 | |||
4f028ccee8 | |||
1619fdadf2 | |||
7f71d0c9e9 | |||
290010fc8c | |||
80cc62e25b | |||
f1b63c3951 | |||
01606af307 | |||
2cc0a5bcbc | |||
efe9a2e842 | |||
34e7dd2c6d | |||
a337daee93 | |||
a51510606f | |||
aef94767c5 | |||
8b6a6abd92 | |||
bc5037b256 | |||
036bef1adb | |||
cc36ef805b | |||
0f610a5e19 | |||
4c93b5c9b3 |
37
CHANGELOG.md
37
CHANGELOG.md
@ -2,6 +2,33 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
### 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.
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### 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.
|
||||
|
||||
### Security
|
||||
|
||||
## 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
|
||||
- Magix service for binding controls devices (both as RPC client and server)
|
||||
@ -20,13 +47,3 @@
|
||||
- A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
||||
- Magix history database API
|
||||
- ZMQ client endpoint for Magix
|
||||
|
||||
### Changed
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
### Security
|
||||
|
@ -1,12 +1,14 @@
|
||||
[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
|
||||
|
||||
# Controls.kt
|
||||
[![DOI](https://zenodo.org/badge/240888288.svg)](https://zenodo.org/badge/latestdoi/240888288)
|
||||
|
||||
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
|
||||
|
||||
Controls-kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
|
||||
This repository contains a prototype of API and simple implementation
|
||||
of a slow control system, including a demo.
|
||||
|
||||
Controls.kt uses some concepts and modules of DataForge,
|
||||
Controls-kt uses some concepts and modules of DataForge,
|
||||
such as `Meta` (tree-like value structure).
|
||||
|
||||
To learn more about DataForge, please consult the following URLs:
|
||||
|
@ -1,4 +1,3 @@
|
||||
import space.kscience.gradle.isInDevelopment
|
||||
import space.kscience.gradle.useApache2Licence
|
||||
import space.kscience.gradle.useSPCTeam
|
||||
|
||||
@ -6,32 +5,27 @@ plugins {
|
||||
id("space.kscience.gradle.project")
|
||||
}
|
||||
|
||||
val dataforgeVersion: String by extra("0.6.2-dev-3")
|
||||
val dataforgeVersion: String by extra("0.7.1")
|
||||
val visionforgeVersion by extra("0.3.0-RC")
|
||||
val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion)
|
||||
val rsocketVersion by extra("0.15.4")
|
||||
val xodusVersion by extra("2.0.1")
|
||||
|
||||
allprojects {
|
||||
group = "space.kscience"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0-dev-4"
|
||||
repositories{
|
||||
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||
}
|
||||
}
|
||||
|
||||
ksciencePublish {
|
||||
pom("https://github.com/SciProgCentre/controls.kt") {
|
||||
pom("https://github.com/SciProgCentre/controls-kt") {
|
||||
useApache2Licence()
|
||||
useSPCTeam()
|
||||
}
|
||||
github("controls.kt", "SciProgCentre")
|
||||
space(
|
||||
if (isInDevelopment) {
|
||||
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
|
||||
} else {
|
||||
"https://maven.pkg.jetbrains.space/spc/p/sci/maven"
|
||||
}
|
||||
)
|
||||
repository("spc","https://maven.sciprog.center/kscience")
|
||||
sonatype("https://oss.sonatype.org")
|
||||
}
|
||||
|
||||
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
20
controls-constructor/build.gradle.kts
Normal file
20
controls-constructor/build.gradle.kts
Normal file
@ -0,0 +1,20 @@
|
||||
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
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
import space.kscience.dataforge.context.Factory
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.asName
|
||||
import kotlin.properties.PropertyDelegateProvider
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
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(
|
||||
deviceManager: DeviceManager,
|
||||
meta: Meta,
|
||||
) : DeviceGroup(deviceManager, 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 : Any> property(
|
||||
state: DeviceState<T>,
|
||||
nameOverride: String? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> =
|
||||
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
||||
val name = nameOverride ?: property.name
|
||||
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
||||
registerProperty(descriptor, state)
|
||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||
state.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register external state as a property
|
||||
*/
|
||||
public fun <T : Any> property(
|
||||
metaConverter: MetaConverter<T>,
|
||||
reader: suspend () -> T,
|
||||
readInterval: Duration,
|
||||
initialState: T,
|
||||
nameOverride: String? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property(
|
||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
|
||||
nameOverride, descriptorBuilder
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Register a mutable property and provide a direct reader for it
|
||||
*/
|
||||
public fun <T : Any> mutableProperty(
|
||||
state: MutableDeviceState<T>,
|
||||
nameOverride: String? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> =
|
||||
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
||||
val name = nameOverride ?: property.name
|
||||
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
||||
registerProperty(descriptor, state)
|
||||
object : ReadWriteProperty<DeviceConstructor, T> {
|
||||
override fun getValue(thisRef: DeviceConstructor, property: KProperty<*>): T = state.value
|
||||
|
||||
override fun setValue(thisRef: DeviceConstructor, property: KProperty<*>, value: T) {
|
||||
state.value = value
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register external state as a property
|
||||
*/
|
||||
public fun <T : Any> mutableProperty(
|
||||
metaConverter: MetaConverter<T>,
|
||||
reader: suspend () -> T,
|
||||
writer: suspend (T) -> Unit,
|
||||
readInterval: Duration,
|
||||
initialState: T,
|
||||
nameOverride: String? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = mutableProperty(
|
||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
|
||||
nameOverride,
|
||||
descriptorBuilder
|
||||
)
|
||||
}
|
@ -0,0 +1,309 @@
|
||||
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.Laminate
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.MutableMeta
|
||||
import space.kscience.dataforge.meta.get
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
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(
|
||||
public val deviceManager: DeviceManager,
|
||||
override val meta: Meta,
|
||||
) : DeviceHub, CachingDevice {
|
||||
|
||||
internal class Property(
|
||||
val state: DeviceState<out Any>,
|
||||
val descriptor: PropertyDescriptor,
|
||||
)
|
||||
|
||||
internal class Action(
|
||||
val invoke: suspend (Meta?) -> Meta?,
|
||||
val descriptor: ActionDescriptor,
|
||||
)
|
||||
|
||||
|
||||
override final val context: Context get() = deviceManager.context
|
||||
|
||||
|
||||
private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
|
||||
|
||||
override val messageFlow: Flow<DeviceMessage>
|
||||
get() = sharedMessageFlow
|
||||
|
||||
override val coroutineContext: CoroutineContext = context.newCoroutineContext(
|
||||
SupervisorJob(context.coroutineContext[Job]) +
|
||||
CoroutineName("Device $this") +
|
||||
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<out Any>) {
|
||||
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(this, 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(deviceManager, 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> 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(deviceManager.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 virtual [MutableDeviceState], but do not register it to a device
|
||||
*/
|
||||
@Suppress("UnusedReceiverParameter")
|
||||
public fun <T : Any> DeviceGroup.state(
|
||||
converter: MetaConverter<T>,
|
||||
initialValue: T,
|
||||
): MutableDeviceState<T> = VirtualDeviceState<T>(converter, initialValue)
|
||||
|
||||
/**
|
||||
* 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 = {},
|
||||
): MutableDeviceState<T> {
|
||||
val state = state(converter, initialValue)
|
||||
registerMutableProperty(name, state, descriptorBuilder)
|
||||
return state
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
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.transformations.MetaConverter
|
||||
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::objectToMeta)
|
||||
|
||||
public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.objectToMeta(value)
|
||||
|
||||
|
||||
/**
|
||||
* A mutable state of a device
|
||||
*/
|
||||
public interface MutableDeviceState<T> : DeviceState<T> {
|
||||
override var value: T
|
||||
}
|
||||
|
||||
public var <T : Any> MutableDeviceState<T>.valueAsMeta: Meta
|
||||
get() = converter.objectToMeta(value)
|
||||
set(arg) {
|
||||
value = converter.metaToObject(arg)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [MutableDeviceState] that does not correspond to a physical state
|
||||
*/
|
||||
public class VirtualDeviceState<T>(
|
||||
override val converter: MetaConverter<T>,
|
||||
initialValue: T,
|
||||
) : MutableDeviceState<T> {
|
||||
private val flow = MutableStateFlow(initialValue)
|
||||
override val valueFlow: Flow<T> get() = flow
|
||||
|
||||
override var value: T by flow::value
|
||||
}
|
||||
|
||||
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.metaToObject(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.metaToObject(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.objectToMeta(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.metaToObject(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
@ -0,0 +1,99 @@
|
||||
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.double
|
||||
import space.kscience.dataforge.meta.get
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
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)
|
@ -0,0 +1,44 @@
|
||||
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
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
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))
|
@ -0,0 +1,27 @@
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.*
|
||||
import space.kscience.dataforge.meta.transformations.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 }
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import space.kscience.dataforge.meta.transformations.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)
|
@ -20,10 +20,14 @@ kscience {
|
||||
json()
|
||||
}
|
||||
useContextReceivers()
|
||||
dependencies {
|
||||
commonMain {
|
||||
api("space.kscience:dataforge-io:$dataforgeVersion")
|
||||
api(spclibs.kotlinx.datetime)
|
||||
}
|
||||
|
||||
jvmTest{
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -3,26 +3,42 @@ package space.kscience.controls.api
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
||||
import space.kscience.dataforge.context.ContextAware
|
||||
import space.kscience.dataforge.context.info
|
||||
import space.kscience.dataforge.context.logger
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.misc.DFExperimental
|
||||
import space.kscience.dataforge.misc.Type
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.misc.DfType
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
|
||||
/**
|
||||
* A lifecycle state of a device
|
||||
*/
|
||||
public enum class DeviceLifecycleState{
|
||||
INIT,
|
||||
OPEN,
|
||||
CLOSED
|
||||
@Serializable
|
||||
public enum class DeviceLifecycleState {
|
||||
|
||||
/**
|
||||
* Device is initializing
|
||||
*/
|
||||
STARTING,
|
||||
|
||||
/**
|
||||
* The Device is initialized and running
|
||||
*/
|
||||
STARTED,
|
||||
|
||||
/**
|
||||
* The Device is closed
|
||||
*/
|
||||
STOPPED,
|
||||
|
||||
/**
|
||||
* The device encountered irrecoverable error
|
||||
*/
|
||||
ERROR
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,14 +46,15 @@ public enum class DeviceLifecycleState{
|
||||
* [Device] is a supervisor scope encompassing all operations on a device.
|
||||
* When canceled, cancels all running processes.
|
||||
*/
|
||||
@Type(DEVICE_TARGET)
|
||||
public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
||||
@DfType(DEVICE_TARGET)
|
||||
public interface Device : ContextAware, CoroutineScope {
|
||||
|
||||
/**
|
||||
* Initial configuration meta for the device
|
||||
*/
|
||||
public val meta: Meta get() = Meta.EMPTY
|
||||
|
||||
|
||||
/**
|
||||
* List of supported property descriptors
|
||||
*/
|
||||
@ -54,18 +71,6 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
||||
*/
|
||||
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].
|
||||
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
|
||||
@ -85,14 +90,15 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
||||
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 open(): Unit = Unit
|
||||
public suspend fun start(): Unit = Unit
|
||||
|
||||
/**
|
||||
* Close and terminate the device. This function does not wait for the device to be closed.
|
||||
*/
|
||||
override fun close() {
|
||||
public fun stop() {
|
||||
logger.info { "Device $this is closed" }
|
||||
cancel("The device is closed")
|
||||
}
|
||||
@ -105,24 +111,54 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
|
||||
public suspend fun Device.requestProperty(propertyName: String): Meta = if (this is CachingDevice) {
|
||||
getProperty(propertyName) ?: readProperty(propertyName)
|
||||
} else {
|
||||
readProperty(propertyName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a snapshot of the device logical state
|
||||
*
|
||||
*/
|
||||
public fun Device.getAllProperties(): Meta = Meta {
|
||||
public fun CachingDevice.getAllProperties(): Meta = Meta {
|
||||
for (descriptor in propertyDescriptors) {
|
||||
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
|
||||
set(descriptor.name.parseAsName(), getProperty(descriptor.name))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe on property changes for the whole device
|
||||
*/
|
||||
public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job =
|
||||
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this)
|
||||
public fun Device.onPropertyChange(
|
||||
scope: CoroutineScope = 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 }
|
||||
|
@ -1,4 +1,4 @@
|
||||
@file:OptIn(ExperimentalSerializationApi::class)
|
||||
@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class)
|
||||
|
||||
package space.kscience.controls.api
|
||||
|
||||
@ -25,7 +25,7 @@ public sealed class DeviceMessage {
|
||||
public abstract val time: Instant?
|
||||
|
||||
/**
|
||||
* Update the source device name for composition. If the original name is null, resulting name is also null.
|
||||
* Update the source device name for composition. If the original name is null, the resulting name is also null.
|
||||
*/
|
||||
public abstract fun changeSource(block: (Name) -> Name): DeviceMessage
|
||||
|
||||
@ -71,7 +71,7 @@ public data class PropertyChangedMessage(
|
||||
@SerialName("property.set")
|
||||
public data class PropertySetMessage(
|
||||
public val property: String,
|
||||
public val value: Meta?,
|
||||
public val value: Meta,
|
||||
override val sourceDevice: Name? = null,
|
||||
override val targetDevice: Name,
|
||||
override val comment: String? = null,
|
||||
@ -203,12 +203,12 @@ public data class EmptyDeviceMessage(
|
||||
public data class DeviceLogMessage(
|
||||
val message: String,
|
||||
val data: Meta? = null,
|
||||
override val sourceDevice: Name? = null,
|
||||
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() {
|
||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -220,7 +220,22 @@ public data class DeviceErrorMessage(
|
||||
public val errorMessage: String?,
|
||||
public val errorType: String? = null,
|
||||
public val errorStackTrace: String? = null,
|
||||
override val sourceDevice: Name,
|
||||
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() {
|
||||
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(),
|
||||
|
@ -1,6 +1,5 @@
|
||||
package space.kscience.controls.api
|
||||
|
||||
import io.ktor.utils.io.core.Closeable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -9,7 +8,7 @@ import kotlinx.coroutines.launch
|
||||
/**
|
||||
* A generic bidirectional sender/receiver object
|
||||
*/
|
||||
public interface Socket<T> : Closeable {
|
||||
public interface Socket<T> : AutoCloseable {
|
||||
/**
|
||||
* Send an object to the socket
|
||||
*/
|
||||
|
@ -12,10 +12,10 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder
|
||||
@Serializable
|
||||
public class PropertyDescriptor(
|
||||
public val name: String,
|
||||
public var info: String? = null,
|
||||
public var description: String? = null,
|
||||
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||
public var readable: Boolean = true,
|
||||
public var writable: Boolean = false
|
||||
public var mutable: Boolean = false
|
||||
)
|
||||
|
||||
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){
|
||||
@ -27,6 +27,6 @@ public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Un
|
||||
*/
|
||||
@Serializable
|
||||
public class ActionDescriptor(public val name: String) {
|
||||
public var info: String? = null
|
||||
public var description: String? = null
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
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
|
@ -40,7 +40,7 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
|
||||
public fun <D : Device> DeviceManager.install(name: String, device: D): D {
|
||||
registerDevice(NameToken(name), device)
|
||||
device.launch {
|
||||
device.open()
|
||||
device.start()
|
||||
}
|
||||
return device
|
||||
}
|
||||
|
@ -17,21 +17,17 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
||||
is PropertyGetMessage -> {
|
||||
PropertyChangedMessage(
|
||||
property = request.property,
|
||||
value = getOrReadProperty(request.property),
|
||||
value = requestProperty(request.property),
|
||||
sourceDevice = deviceTarget,
|
||||
targetDevice = request.sourceDevice
|
||||
)
|
||||
}
|
||||
|
||||
is PropertySetMessage -> {
|
||||
if (request.value == null) {
|
||||
invalidate(request.property)
|
||||
} else {
|
||||
writeProperty(request.property, request.value)
|
||||
}
|
||||
writeProperty(request.property, request.value)
|
||||
PropertyChangedMessage(
|
||||
property = request.property,
|
||||
value = getOrReadProperty(request.property),
|
||||
value = requestProperty(request.property),
|
||||
sourceDevice = deviceTarget,
|
||||
targetDevice = request.sourceDevice
|
||||
)
|
||||
@ -64,6 +60,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
||||
is DeviceErrorMessage,
|
||||
is EmptyDeviceMessage,
|
||||
is DeviceLogMessage,
|
||||
is DeviceLifeCycleMessage,
|
||||
-> null
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
@ -87,7 +84,7 @@ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMe
|
||||
* Collect all messages from given [DeviceHub], applying proper relative names.
|
||||
*/
|
||||
public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow<DeviceMessage> {
|
||||
|
||||
|
||||
//TODO could we avoid using downstream scope?
|
||||
val outbox = MutableSharedFlow<DeviceMessage>()
|
||||
if (this is Device) {
|
||||
|
@ -0,0 +1,73 @@
|
||||
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.get
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
/**
|
||||
* 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 val type: KType get() = typeOf<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 val type: KType = typeOf<ValueWithTime<T>>()
|
||||
|
||||
override fun metaToObjectOrNull(
|
||||
meta: Meta,
|
||||
): ValueWithTime<T>? = valueConverter.metaToObject(meta[ValueWithTime.META_VALUE_KEY] ?: Meta.EMPTY)?.let {
|
||||
ValueWithTime(it, meta[ValueWithTime.META_TIME_KEY]?.instant ?: Instant.DISTANT_PAST)
|
||||
}
|
||||
|
||||
override fun objectToMeta(obj: ValueWithTime<T>): Meta = Meta {
|
||||
ValueWithTime.META_TIME_KEY put obj.time.toMeta()
|
||||
ValueWithTime.META_VALUE_KEY put valueConverter.objectToMeta(obj.value)
|
||||
}
|
||||
}
|
||||
|
||||
public fun <T : Any> MetaConverter<T>.withTime(): MetaConverter<ValueWithTime<T>> = ValueWithTimeMetaConverter(this)
|
@ -0,0 +1,42 @@
|
||||
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) }
|
@ -1,18 +0,0 @@
|
||||
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,
|
||||
)
|
@ -6,7 +6,8 @@ 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 space.kscience.dataforge.misc.DfType
|
||||
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
@ -17,7 +18,7 @@ public interface Port : ContextAware, Socket<ByteArray>
|
||||
/**
|
||||
* A specialized factory for [Port]
|
||||
*/
|
||||
@Type(PortFactory.TYPE)
|
||||
@DfType(PortFactory.TYPE)
|
||||
public interface PortFactory : Factory<Port> {
|
||||
public val type: String
|
||||
|
||||
@ -37,7 +38,7 @@ public abstract class AbstractPort(
|
||||
protected val scope: CoroutineScope = CoroutineScope(coroutineContext + SupervisorJob(coroutineContext[Job]))
|
||||
|
||||
private val outgoing = Channel<ByteArray>(100)
|
||||
private val incoming = Channel<ByteArray>(Channel.CONFLATED)
|
||||
private val incoming = Channel<ByteArray>(100)
|
||||
|
||||
init {
|
||||
scope.coroutineContext[Job]?.invokeOnCompletion {
|
||||
@ -87,7 +88,6 @@ public abstract class AbstractPort(
|
||||
override fun close() {
|
||||
outgoing.close()
|
||||
incoming.close()
|
||||
sendJob.cancel()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
package space.kscience.controls.ports
|
||||
|
||||
import space.kscience.dataforge.io.Binary
|
||||
|
||||
public fun Binary.readShort(position: Int): Short = read(position) { readShort() }
|
@ -1,11 +1,11 @@
|
||||
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.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
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.
|
||||
@ -13,9 +13,13 @@ import kotlinx.coroutines.flow.transform
|
||||
public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray> {
|
||||
require(delimiter.isNotEmpty()) { "Delimiter must not be empty" }
|
||||
|
||||
val output = BytePacketBuilder()
|
||||
val output = Buffer()
|
||||
var matcherPosition = 0
|
||||
|
||||
onCompletion {
|
||||
output.close()
|
||||
}
|
||||
|
||||
return transform { chunk ->
|
||||
chunk.forEach { byte ->
|
||||
output.writeByte(byte)
|
||||
@ -24,9 +28,8 @@ public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray>
|
||||
matcherPosition++
|
||||
if (matcherPosition == delimiter.size) {
|
||||
//full match achieved, sending result
|
||||
val bytes = output.build()
|
||||
emit(bytes.readBytes())
|
||||
output.reset()
|
||||
emit(output.readByteArray())
|
||||
output.clear()
|
||||
matcherPosition = 0
|
||||
}
|
||||
} else if (matcherPosition > 0) {
|
||||
|
@ -1,24 +1,32 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import space.kscience.controls.api.*
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.error
|
||||
import space.kscience.dataforge.context.debug
|
||||
import space.kscience.dataforge.context.logger
|
||||
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 kotlin.coroutines.CoroutineContext
|
||||
|
||||
|
||||
/**
|
||||
* Write a meta [item] to [device]
|
||||
*/
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
private suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
|
||||
private suspend fun <D : Device, T> MutableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
|
||||
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Read Meta item from the [device]
|
||||
*/
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
private suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
|
||||
read(device)?.let(converter::objectToMeta)
|
||||
@ -39,8 +47,8 @@ private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta
|
||||
*/
|
||||
public abstract class DeviceBase<D : Device>(
|
||||
final override val context: Context,
|
||||
override val meta: Meta = Meta.EMPTY,
|
||||
) : Device {
|
||||
final override val meta: Meta = Meta.EMPTY,
|
||||
) : CachingDevice {
|
||||
|
||||
/**
|
||||
* Collection of property specifications
|
||||
@ -58,15 +66,27 @@ public abstract class DeviceBase<D : Device>(
|
||||
override val actionDescriptors: Collection<ActionDescriptor>
|
||||
get() = actions.values.map { it.descriptor }
|
||||
|
||||
override val coroutineContext: CoroutineContext by lazy {
|
||||
context.newCoroutineContext(
|
||||
SupervisorJob(context.coroutineContext[Job]) +
|
||||
CoroutineName("Device $this") +
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
logger.error(throwable) { "Exception in device $this job" }
|
||||
|
||||
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow(
|
||||
replay = meta["message.buffer"].int ?: 1000,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
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()
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
@ -74,8 +94,6 @@ public abstract class DeviceBase<D : Device>(
|
||||
*/
|
||||
private val logicalState: HashMap<String, Meta?> = HashMap()
|
||||
|
||||
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow()
|
||||
|
||||
public override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ -87,7 +105,7 @@ public abstract class DeviceBase<D : Device>(
|
||||
/**
|
||||
* Update logical property state and notify listeners
|
||||
*/
|
||||
protected suspend fun updateLogical(propertyName: String, value: Meta?) {
|
||||
protected suspend fun propertyChanged(propertyName: String, value: Meta?) {
|
||||
if (value != logicalState[propertyName]) {
|
||||
stateLock.withLock {
|
||||
logicalState[propertyName] = value
|
||||
@ -99,10 +117,10 @@ public abstract class DeviceBase<D : Device>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Update logical state using given [spec] and its convertor
|
||||
* Notify the device that a property with [spec] value is changed
|
||||
*/
|
||||
public suspend fun <T> updateLogical(spec: DevicePropertySpec<D, T>, value: T) {
|
||||
updateLogical(spec.name, spec.converter.objectToMeta(value))
|
||||
protected suspend fun <T> propertyChanged(spec: DevicePropertySpec<D, T>, value: T) {
|
||||
propertyChanged(spec.name, spec.converter.objectToMeta(value))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -112,7 +130,7 @@ public abstract class DeviceBase<D : Device>(
|
||||
override suspend fun readProperty(propertyName: String): Meta {
|
||||
val spec = properties[propertyName] ?: error("Property with name $propertyName not found")
|
||||
val meta = spec.readMeta(self) ?: error("Failed to read property $propertyName")
|
||||
updateLogical(propertyName, meta)
|
||||
propertyChanged(propertyName, meta)
|
||||
return meta
|
||||
}
|
||||
|
||||
@ -122,7 +140,7 @@ public abstract class DeviceBase<D : Device>(
|
||||
public suspend fun readPropertyOrNull(propertyName: String): Meta? {
|
||||
val spec = properties[propertyName] ?: return null
|
||||
val meta = spec.readMeta(self) ?: return null
|
||||
updateLogical(propertyName, meta)
|
||||
propertyChanged(propertyName, meta)
|
||||
return meta
|
||||
}
|
||||
|
||||
@ -135,15 +153,26 @@ public abstract class DeviceBase<D : Device>(
|
||||
}
|
||||
|
||||
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]) {
|
||||
null -> {
|
||||
//If there is a physical property with a given name, invalidate logical property and write physical one
|
||||
updateLogical(propertyName, value)
|
||||
//If there are no registered physical properties with given name, write a logical one.
|
||||
propertyChanged(propertyName, value)
|
||||
}
|
||||
|
||||
is WritableDevicePropertySpec -> {
|
||||
is MutableDevicePropertySpec -> {
|
||||
//if there is a writeable property with a given name, invalidate logical and write physical
|
||||
invalidate(propertyName)
|
||||
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 -> {
|
||||
@ -158,21 +187,46 @@ public abstract class DeviceBase<D : Device>(
|
||||
}
|
||||
|
||||
@DFExperimental
|
||||
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.INIT
|
||||
protected set
|
||||
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
||||
private set(value) {
|
||||
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)
|
||||
override fun close() {
|
||||
lifecycleState = DeviceLifecycleState.CLOSED
|
||||
super.close()
|
||||
final override suspend fun start() {
|
||||
if (lifecycleState == DeviceLifecycleState.STOPPED) {
|
||||
super.start()
|
||||
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
|
||||
|
||||
}
|
||||
|
@ -16,15 +16,14 @@ public open class DeviceBySpec<D : Device>(
|
||||
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
|
||||
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
|
||||
|
||||
override suspend fun open(): Unit = with(spec) {
|
||||
super.open()
|
||||
override suspend fun onStart(): Unit = with(spec) {
|
||||
self.onOpen()
|
||||
}
|
||||
|
||||
override fun close(): Unit = with(spec) {
|
||||
override fun onStop(): Unit = with(spec){
|
||||
self.onClose()
|
||||
super.close()
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String = "Device(spec=$spec)"
|
||||
}
|
@ -4,10 +4,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import space.kscience.controls.api.ActionDescriptor
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.api.PropertyChangedMessage
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.api.*
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
|
||||
|
||||
@ -20,7 +17,7 @@ public annotation class InternalDeviceAPI
|
||||
/**
|
||||
* Specification for a device read-only property
|
||||
*/
|
||||
public interface DevicePropertySpec<in D : Device, T> {
|
||||
public interface DevicePropertySpec<in D, T> {
|
||||
/**
|
||||
* Property descriptor
|
||||
*/
|
||||
@ -44,7 +41,7 @@ public interface DevicePropertySpec<in D : Device, T> {
|
||||
public val DevicePropertySpec<*, *>.name: String get() = descriptor.name
|
||||
|
||||
|
||||
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
|
||||
public interface MutableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
|
||||
/**
|
||||
* Write physical value to a device
|
||||
*/
|
||||
@ -53,7 +50,7 @@ public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySp
|
||||
|
||||
}
|
||||
|
||||
public interface DeviceActionSpec<in D : Device, I, O> {
|
||||
public interface DeviceActionSpec<in D, I, O> {
|
||||
/**
|
||||
* Action descriptor
|
||||
*/
|
||||
@ -84,21 +81,20 @@ public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>
|
||||
public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||
readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||
|
||||
|
||||
public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||
getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||
public suspend fun <T, D : Device> D.request(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||
propertySpec.converter.metaToObject(requestProperty(propertySpec.name))
|
||||
|
||||
/**
|
||||
* Write typed property state and invalidate logical state
|
||||
*/
|
||||
public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, value: T) {
|
||||
public suspend fun <T, D : Device> D.write(propertySpec: MutableDevicePropertySpec<D, T>, value: T) {
|
||||
writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope
|
||||
*/
|
||||
public operator fun <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Job = launch {
|
||||
public fun <T, D : Device> D.writeAsync(propertySpec: MutableDevicePropertySpec<D, T>, value: T): Job = launch {
|
||||
write(propertySpec, value)
|
||||
}
|
||||
|
||||
@ -115,6 +111,7 @@ public fun <D : Device, T> D.propertyFlow(spec: DevicePropertySpec<D, T>): Flow<
|
||||
*/
|
||||
public fun <D : Device, T> D.onPropertyChange(
|
||||
spec: DevicePropertySpec<D, T>,
|
||||
scope: CoroutineScope = this,
|
||||
callback: suspend PropertyChangedMessage.(T) -> Unit,
|
||||
): Job = messageFlow
|
||||
.filterIsInstance<PropertyChangedMessage>()
|
||||
@ -124,15 +121,16 @@ public fun <D : Device, T> D.onPropertyChange(
|
||||
if (newValue != null) {
|
||||
change.callback(newValue)
|
||||
}
|
||||
}.launchIn(this)
|
||||
}.launchIn(scope)
|
||||
|
||||
/**
|
||||
* Call [callback] on initial property value and each value change
|
||||
*/
|
||||
public fun <D : Device, T> D.useProperty(
|
||||
spec: DevicePropertySpec<D, T>,
|
||||
scope: CoroutineScope = this,
|
||||
callback: suspend (T) -> Unit,
|
||||
): Job = launch {
|
||||
): Job = scope.launch {
|
||||
callback(read(spec))
|
||||
messageFlow
|
||||
.filterIsInstance<PropertyChangedMessage>()
|
||||
@ -149,7 +147,7 @@ public fun <D : Device, T> D.useProperty(
|
||||
/**
|
||||
* Reset the logical state of a property
|
||||
*/
|
||||
public suspend fun <D : Device> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
|
||||
public suspend fun <D : CachingDevice> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
|
||||
invalidate(propertySpec.name)
|
||||
}
|
||||
|
||||
|
@ -8,12 +8,15 @@ import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import kotlin.properties.PropertyDelegateProvider
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
public object UnitMetaConverter: MetaConverter<Unit>{
|
||||
override fun metaToObject(meta: Meta): Unit = Unit
|
||||
public object UnitMetaConverter : MetaConverter<Unit> {
|
||||
|
||||
override val type: KType = typeOf<Unit>()
|
||||
|
||||
override fun metaToObjectOrNull(meta: Meta): Unit = Unit
|
||||
|
||||
override fun objectToMeta(obj: Unit): Meta = Meta.EMPTY
|
||||
}
|
||||
@ -22,7 +25,7 @@ public val MetaConverter.Companion.unit: MetaConverter<Unit> get() = UnitMetaCon
|
||||
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
public abstract class DeviceSpec<D : Device> {
|
||||
//initializing meta property for everyone
|
||||
//initializing the metadata property for everyone
|
||||
private val _properties = hashMapOf<String, DevicePropertySpec<D, *>>(
|
||||
DeviceMetaPropertySpec.name to DeviceMetaPropertySpec
|
||||
)
|
||||
@ -44,72 +47,25 @@ public abstract class DeviceSpec<D : Device> {
|
||||
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(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> T?,
|
||||
read: suspend D.(propertyName: String) -> T?,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
|
||||
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
||||
val propertyName = name ?: property.name
|
||||
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 suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
|
||||
override suspend fun read(device: D): T? =
|
||||
withContext(device.coroutineContext) { device.read(propertyName) }
|
||||
}
|
||||
registerProperty(deviceProperty)
|
||||
ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ ->
|
||||
@ -121,23 +77,30 @@ public abstract class DeviceSpec<D : Device> {
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> T?,
|
||||
write: suspend D.(T) -> Unit,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> =
|
||||
read: suspend D.(propertyName: String) -> T?,
|
||||
write: suspend D.(propertyName: String, value: T) -> Unit,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
|
||||
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
|
||||
val propertyName = name ?: property.name
|
||||
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
|
||||
val deviceProperty = object : MutableDevicePropertySpec<D, T> {
|
||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(
|
||||
propertyName,
|
||||
mutable = true
|
||||
).apply {
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
override val converter: MetaConverter<T> = converter
|
||||
|
||||
override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
|
||||
override suspend fun read(device: D): T? =
|
||||
withContext(device.coroutineContext) { device.read(propertyName) }
|
||||
|
||||
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
|
||||
device.write(value)
|
||||
device.write(propertyName, value)
|
||||
}
|
||||
}
|
||||
_properties[propertyName] = deviceProperty
|
||||
ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>> { _, _ ->
|
||||
registerProperty(deviceProperty)
|
||||
ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>> { _, _ ->
|
||||
deviceProperty
|
||||
}
|
||||
}
|
||||
@ -155,10 +118,13 @@ public abstract class DeviceSpec<D : Device> {
|
||||
name: String? = null,
|
||||
execute: suspend D.(I) -> O,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
|
||||
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
||||
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
|
||||
val actionName = name ?: property.name
|
||||
val deviceAction = object : DeviceActionSpec<D, I, O> {
|
||||
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder)
|
||||
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply {
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
|
||||
override val inputConverter: MetaConverter<I> = inputConverter
|
||||
override val outputConverter: MetaConverter<O> = outputConverter
|
||||
@ -173,68 +139,39 @@ 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a mutable logical property for a device
|
||||
* An action that takes [Meta] and returns [Meta]. No conversions are done
|
||||
*/
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
public fun <D : Device> DeviceSpec<D>.metaAction(
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
|
||||
PropertyDelegateProvider { _, property ->
|
||||
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
||||
val propertyName = name ?: property.name
|
||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
|
||||
//TODO add type from converter
|
||||
writable = true
|
||||
}.apply(descriptorBuilder)
|
||||
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)
|
||||
}
|
||||
|
||||
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.
|
||||
* The flow is lazy, so action is not performed unless flow is consumed.
|
||||
* The flow uses called context. In order to call it on device context, use `flowOn(coroutineContext)`.
|
||||
* The flow uses caller context. To call it on device context, use `flowOn(coroutineContext)`.
|
||||
*
|
||||
* The flow is canceled when the device scope is canceled
|
||||
*/
|
||||
|
@ -0,0 +1,12 @@
|
||||
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<*>)
|
@ -2,6 +2,8 @@ package space.kscience.controls.spec
|
||||
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
@ -10,7 +12,9 @@ public fun Double.asMeta(): Meta = Meta(asValue())
|
||||
|
||||
//TODO to be moved to DF
|
||||
public object DurationConverter : MetaConverter<Duration> {
|
||||
override fun metaToObject(meta: Meta): Duration = meta.value?.double?.toDuration(DurationUnit.SECONDS)
|
||||
override val type: KType = typeOf<Duration>()
|
||||
|
||||
override fun metaToObjectOrNull(meta: Meta): Duration = meta.value?.double?.toDuration(DurationUnit.SECONDS)
|
||||
?: run {
|
||||
val unit: DurationUnit = meta["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
|
||||
val value = meta[Meta.VALUE_KEY].double ?: error("No value present for Duration")
|
||||
|
@ -8,18 +8,67 @@ import space.kscience.dataforge.meta.ValueType
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import kotlin.properties.PropertyDelegateProvider
|
||||
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::metaToObject) },
|
||||
write = { propertyName, value -> writeProperty(propertyName, converter.objectToMeta(value)) }
|
||||
)
|
||||
|
||||
|
||||
//read only delegates
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Boolean?
|
||||
read: suspend D.(propertyName: String) -> Boolean?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
|
||||
MetaConverter.boolean,
|
||||
{
|
||||
metaDescriptor {
|
||||
type(ValueType.BOOLEAN)
|
||||
valueType(ValueType.BOOLEAN)
|
||||
}
|
||||
descriptorBuilder()
|
||||
},
|
||||
@ -31,15 +80,15 @@ private inline fun numberDescriptor(
|
||||
crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}
|
||||
): PropertyDescriptor.() -> Unit = {
|
||||
metaDescriptor {
|
||||
type(ValueType.NUMBER)
|
||||
valueType(ValueType.NUMBER)
|
||||
}
|
||||
descriptorBuilder()
|
||||
}
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.numberProperty(
|
||||
name: String? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
read: suspend D.() -> Number?
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Number?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
|
||||
MetaConverter.number,
|
||||
numberDescriptor(descriptorBuilder),
|
||||
@ -50,7 +99,7 @@ public fun <D : Device> DeviceSpec<D>.numberProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Double?
|
||||
read: suspend D.(propertyName: String) -> Double?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
|
||||
MetaConverter.double,
|
||||
numberDescriptor(descriptorBuilder),
|
||||
@ -61,12 +110,12 @@ public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.stringProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> String?
|
||||
read: suspend D.(propertyName: String) -> String?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
|
||||
MetaConverter.string,
|
||||
{
|
||||
metaDescriptor {
|
||||
type(ValueType.STRING)
|
||||
valueType(ValueType.STRING)
|
||||
}
|
||||
descriptorBuilder()
|
||||
},
|
||||
@ -77,12 +126,12 @@ public fun <D : Device> DeviceSpec<D>.stringProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.metaProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Meta?
|
||||
read: suspend D.(propertyName: String) -> Meta?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
|
||||
MetaConverter.meta,
|
||||
{
|
||||
metaDescriptor {
|
||||
type(ValueType.STRING)
|
||||
valueType(ValueType.STRING)
|
||||
}
|
||||
descriptorBuilder()
|
||||
},
|
||||
@ -95,14 +144,14 @@ public fun <D : Device> DeviceSpec<D>.metaProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Boolean?,
|
||||
write: suspend D.(Boolean) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
|
||||
read: suspend D.(propertyName: String) -> Boolean?,
|
||||
write: suspend D.(propertyName: String, value: Boolean) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Boolean>>> =
|
||||
mutableProperty(
|
||||
MetaConverter.boolean,
|
||||
{
|
||||
metaDescriptor {
|
||||
type(ValueType.BOOLEAN)
|
||||
valueType(ValueType.BOOLEAN)
|
||||
}
|
||||
descriptorBuilder()
|
||||
},
|
||||
@ -115,31 +164,31 @@ public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
||||
public fun <D : Device> DeviceSpec<D>.numberProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Number,
|
||||
write: suspend D.(Number) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> =
|
||||
read: suspend D.(propertyName: String) -> Number,
|
||||
write: suspend D.(propertyName: String, value: Number) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Number>>> =
|
||||
mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Double,
|
||||
write: suspend D.(Double) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> =
|
||||
read: suspend D.(propertyName: String) -> Double,
|
||||
write: suspend D.(propertyName: String, value: Double) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Double>>> =
|
||||
mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.stringProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> String,
|
||||
write: suspend D.(String) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> =
|
||||
read: suspend D.(propertyName: String) -> String,
|
||||
write: suspend D.(propertyName: String, value: String) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, String>>> =
|
||||
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.metaProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Meta,
|
||||
write: suspend D.(Meta) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Meta>>> =
|
||||
read: suspend D.(propertyName: String) -> Meta,
|
||||
write: suspend D.(propertyName: String, value: Meta) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Meta>>> =
|
||||
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)
|
@ -0,0 +1,9 @@
|
||||
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<*>){}
|
@ -8,6 +8,7 @@ import space.kscience.dataforge.context.logger
|
||||
import space.kscience.dataforge.meta.*
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.AsynchronousCloseException
|
||||
import java.nio.channels.ByteChannel
|
||||
import java.nio.channels.DatagramChannel
|
||||
import java.nio.channels.SocketChannel
|
||||
@ -30,7 +31,7 @@ public class ChannelPort(
|
||||
channelBuilder: suspend () -> ByteChannel,
|
||||
) : AbstractPort(context, coroutineContext), AutoCloseable {
|
||||
|
||||
private val futureChannel: Deferred<ByteChannel> = this.scope.async(Dispatchers.IO) {
|
||||
private val futureChannel: Deferred<ByteChannel> = scope.async(Dispatchers.IO) {
|
||||
channelBuilder()
|
||||
}
|
||||
|
||||
@ -39,10 +40,10 @@ public class ChannelPort(
|
||||
*/
|
||||
public val startJob: Job get() = futureChannel
|
||||
|
||||
private val listenerJob = this.scope.launch(Dispatchers.IO) {
|
||||
private val listenerJob = scope.launch(Dispatchers.IO) {
|
||||
val channel = futureChannel.await()
|
||||
val buffer = ByteBuffer.allocate(1024)
|
||||
while (isActive) {
|
||||
while (isActive && channel.isOpen) {
|
||||
try {
|
||||
val num = channel.read(buffer)
|
||||
if (num > 0) {
|
||||
@ -50,8 +51,12 @@ public class ChannelPort(
|
||||
}
|
||||
if (num < 0) cancel("The input channel is exhausted")
|
||||
} catch (ex: Exception) {
|
||||
logger.error(ex) { "Channel read error" }
|
||||
delay(1000)
|
||||
if (ex is AsynchronousCloseException) {
|
||||
logger.info { "Channel $channel closed" }
|
||||
} else {
|
||||
logger.error(ex) { "Channel read error, retrying in 1 second" }
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -62,11 +67,8 @@ public class ChannelPort(
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun close() {
|
||||
listenerJob.cancel()
|
||||
if (futureChannel.isCompleted) {
|
||||
futureChannel.getCompleted().close()
|
||||
} else {
|
||||
futureChannel.cancel()
|
||||
}
|
||||
super.close()
|
||||
}
|
||||
@ -106,7 +108,7 @@ public object UdpPort : PortFactory {
|
||||
/**
|
||||
* Connect a datagram channel to a remote host/port. If [localPort] is provided, it is used to bind local port for receiving messages.
|
||||
*/
|
||||
public fun open(
|
||||
public fun openChannel(
|
||||
context: Context,
|
||||
remoteHost: String,
|
||||
remotePort: Int,
|
||||
@ -128,6 +130,6 @@ public object UdpPort : PortFactory {
|
||||
val remotePort by meta.number { error("Remote port is not specified") }
|
||||
val localHost: String? by meta.string()
|
||||
val localPort: Int? by meta.int()
|
||||
return open(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost")
|
||||
return openChannel(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost")
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
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,25 +1,50 @@
|
||||
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 PortIOTest{
|
||||
internal class PortIOTest {
|
||||
|
||||
@Test
|
||||
fun testDelimiteredByteArrayFlow(){
|
||||
val flow = flowOf("bb?b","ddd?",":defgb?:ddf","34fb?:--").map { it.encodeToByteArray() }
|
||||
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("bb?bddd?:", result[0].decodeToString())
|
||||
assertEquals("defgb?:", result[1].decodeToString())
|
||||
assertEquals("ddf34fb?:", result[2].decodeToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUdpCommunication() = runTest {
|
||||
val receiver = UdpPort.openChannel(Global, "localhost", 8811, localPort = 8812)
|
||||
val sender = UdpPort.openChannel(Global, "localhost", 8812, localPort = 8811)
|
||||
|
||||
delay(30)
|
||||
repeat(10) {
|
||||
sender.send("Line number $it\n")
|
||||
}
|
||||
|
||||
val res = receiver
|
||||
.receiving()
|
||||
.withStringDelimiter("\n")
|
||||
.take(10)
|
||||
.toList()
|
||||
|
||||
assertEquals("Line number 3", res[3].trim())
|
||||
receiver.close()
|
||||
sender.close()
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
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<*>){}
|
20
controls-jupyter/build.gradle.kts
Normal file
20
controls-jupyter/build.gradle.kts
Normal file
@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
val visionforgeVersion: String by rootProject.extra
|
||||
|
||||
kscience {
|
||||
fullStack("js/controls-jupyter.js")
|
||||
useKtor()
|
||||
useContextReceivers()
|
||||
jupyterLibrary("space.kscience.controls.jupyter.ControlsJupyter")
|
||||
dependencies {
|
||||
implementation(projects.controlsVision)
|
||||
implementation("space.kscience:visionforge-jupyter:$visionforgeVersion")
|
||||
}
|
||||
jvmMain {
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
}
|
14
controls-jupyter/src/jsMain/kotlin/commonJupyter.kt
Normal file
14
controls-jupyter/src/jsMain/kotlin/commonJupyter.kt
Normal file
@ -0,0 +1,14 @@
|
||||
import space.kscience.visionforge.jupyter.VFNotebookClient
|
||||
import space.kscience.visionforge.markup.MarkupPlugin
|
||||
import space.kscience.visionforge.plotly.PlotlyPlugin
|
||||
import space.kscience.visionforge.runVisionClient
|
||||
|
||||
public fun main(): Unit = runVisionClient {
|
||||
// plugin(DeviceManager)
|
||||
// plugin(ClockManager)
|
||||
plugin(PlotlyPlugin)
|
||||
plugin(MarkupPlugin)
|
||||
// plugin(TableVisionJsPlugin)
|
||||
plugin(VFNotebookClient)
|
||||
}
|
||||
|
71
controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt
Normal file
71
controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt
Normal file
@ -0,0 +1,71 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ public class DeviceClient(
|
||||
private val deviceName: Name,
|
||||
incomingFlow: Flow<DeviceMessage>,
|
||||
private val send: suspend (DeviceMessage) -> Unit,
|
||||
) : Device {
|
||||
) : CachingDevice {
|
||||
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext)
|
||||
@ -99,7 +99,7 @@ public class DeviceClient(
|
||||
}
|
||||
|
||||
@DFExperimental
|
||||
override val lifecycleState: DeviceLifecycleState = DeviceLifecycleState.OPEN
|
||||
override val lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STARTED
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import space.kscience.controls.api.get
|
||||
import space.kscience.controls.api.getOrReadProperty
|
||||
import space.kscience.controls.api.requestProperty
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
import space.kscience.dataforge.context.error
|
||||
import space.kscience.dataforge.context.logger
|
||||
@ -91,7 +91,7 @@ public fun DeviceManager.launchTangoMagix(
|
||||
val device = get(payload.device)
|
||||
when (payload.action) {
|
||||
TangoAction.read -> {
|
||||
val value = device.getOrReadProperty(payload.name)
|
||||
val value = device.requestProperty(payload.name)
|
||||
respond(request, payload) { requestPayload ->
|
||||
requestPayload.copy(
|
||||
value = value,
|
||||
@ -104,7 +104,7 @@ public fun DeviceManager.launchTangoMagix(
|
||||
device.writeProperty(payload.name, value)
|
||||
}
|
||||
//wait for value to be written and return final state
|
||||
val value = device.getOrReadProperty(payload.name)
|
||||
val value = device.requestProperty(payload.name)
|
||||
respond(request, payload) { requestPayload ->
|
||||
requestPayload.copy(
|
||||
value = value,
|
||||
|
@ -12,7 +12,7 @@ description = """
|
||||
|
||||
dependencies {
|
||||
api(projects.controlsCore)
|
||||
api("com.ghgande:j2mod:3.1.1")
|
||||
api("com.ghgande:j2mod:3.2.0")
|
||||
}
|
||||
|
||||
readme{
|
||||
|
@ -1,15 +1,14 @@
|
||||
package space.kscience.controls.modbus
|
||||
|
||||
import com.ghgande.j2mod.modbus.procimg.*
|
||||
import io.ktor.utils.io.core.buildPacket
|
||||
import io.ktor.utils.io.core.readByteBuffer
|
||||
import io.ktor.utils.io.core.writeShort
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.io.Buffer
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.DevicePropertySpec
|
||||
import space.kscience.controls.spec.WritableDevicePropertySpec
|
||||
import space.kscience.controls.spec.set
|
||||
import space.kscience.controls.spec.useProperty
|
||||
import space.kscience.controls.ports.readShort
|
||||
import space.kscience.controls.spec.*
|
||||
import space.kscience.dataforge.io.Binary
|
||||
|
||||
|
||||
public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||
@ -29,10 +28,10 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||
|
||||
public fun bind(
|
||||
key: ModbusRegistryKey.Coil,
|
||||
propertySpec: WritableDevicePropertySpec<D, Boolean>,
|
||||
propertySpec: MutableDevicePropertySpec<D, Boolean>,
|
||||
): ObservableDigitalOut = bind(key) { coil ->
|
||||
coil.addObserver { _, _ ->
|
||||
device[propertySpec] = coil.isSet
|
||||
device.writeAsync(propertySpec, coil.isSet)
|
||||
}
|
||||
device.useProperty(propertySpec) { value ->
|
||||
coil.set(value)
|
||||
@ -89,10 +88,10 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||
|
||||
public fun bind(
|
||||
key: ModbusRegistryKey.HoldingRegister,
|
||||
propertySpec: WritableDevicePropertySpec<D, Short>,
|
||||
propertySpec: MutableDevicePropertySpec<D, Short>,
|
||||
): ObservableRegister = bind(key) { register ->
|
||||
register.addObserver { _, _ ->
|
||||
device[propertySpec] = register.toShort()
|
||||
device.writeAsync(propertySpec, register.toShort())
|
||||
}
|
||||
device.useProperty(propertySpec) { value ->
|
||||
register.setValue(value)
|
||||
@ -109,37 +108,63 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||
}
|
||||
|
||||
device.useProperty(propertySpec) { value ->
|
||||
val packet = buildPacket {
|
||||
key.format.writeObject(this, value)
|
||||
}.readByteBuffer()
|
||||
val binary = Binary {
|
||||
key.format.writeTo(this, value)
|
||||
}
|
||||
registers.forEachIndexed { index, register ->
|
||||
register.setValue(packet.getShort(index * 2))
|
||||
register.setValue(binary.readShort(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) {
|
||||
ObservableRegister()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
registers.onChange { packet ->
|
||||
device.write(propertySpec, key.format.readFrom(packet))
|
||||
}
|
||||
|
||||
device.useProperty(propertySpec) { value ->
|
||||
val packet = buildPacket {
|
||||
key.format.writeObject(this, value)
|
||||
}.readByteBuffer()
|
||||
val binary = Binary {
|
||||
key.format.writeTo(this, value)
|
||||
}
|
||||
registers.forEachIndexed { index, observableRegister ->
|
||||
observableRegister.setValue(packet.getShort(index * 2))
|
||||
observableRegister.setValue(binary.readShort(index * 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -182,20 +207,17 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||
val registers = List(key.count) {
|
||||
ObservableRegister()
|
||||
}
|
||||
|
||||
registers.forEachIndexed { index, register ->
|
||||
register.addObserver { _, _ ->
|
||||
val packet = buildPacket {
|
||||
registers.forEach { value ->
|
||||
writeShort(value.toShort())
|
||||
}
|
||||
}
|
||||
device.launch {
|
||||
device.action(key.format.readObject(packet))
|
||||
}
|
||||
}
|
||||
image.addRegister(key.address + index, register)
|
||||
}
|
||||
|
||||
registers.onChange { packet ->
|
||||
device.launch {
|
||||
device.action(key.format.readFrom(packet))
|
||||
}
|
||||
}
|
||||
|
||||
return registers
|
||||
}
|
||||
|
||||
@ -205,14 +227,16 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
|
||||
* Bind the device to Modbus slave (server) image.
|
||||
*/
|
||||
public fun <D : Device> D.bindProcessImage(
|
||||
unitId: Int = 0,
|
||||
openOnBind: Boolean = true,
|
||||
binding: DeviceProcessImageBuilder<D>.() -> Unit,
|
||||
): ProcessImage {
|
||||
val image = SimpleProcessImage()
|
||||
val image = SimpleProcessImage(unitId)
|
||||
DeviceProcessImageBuilder(this, image).apply(binding)
|
||||
image.setLocked(true)
|
||||
if (openOnBind) {
|
||||
launch {
|
||||
open()
|
||||
start()
|
||||
}
|
||||
}
|
||||
return image
|
||||
|
@ -5,11 +5,10 @@ import com.ghgande.j2mod.modbus.procimg.InputRegister
|
||||
import com.ghgande.j2mod.modbus.procimg.Register
|
||||
import com.ghgande.j2mod.modbus.procimg.SimpleInputRegister
|
||||
import com.ghgande.j2mod.modbus.util.BitVector
|
||||
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 kotlinx.io.Buffer
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.dataforge.io.Buffer
|
||||
import space.kscience.dataforge.io.ByteArray
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
@ -21,9 +20,9 @@ import kotlin.reflect.KProperty
|
||||
public interface ModbusDevice : Device {
|
||||
|
||||
/**
|
||||
* Client id for this specific device
|
||||
* Unit id for this specific device
|
||||
*/
|
||||
public val clientId: Int
|
||||
public val unitId: Int
|
||||
|
||||
/**
|
||||
* The modubus master connector
|
||||
@ -45,7 +44,7 @@ public interface ModbusDevice : Device {
|
||||
|
||||
public operator fun <T> ModbusRegistryKey.InputRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||
val packet = readInputRegistersToPacket(address, count)
|
||||
return format.readObject(packet)
|
||||
return format.readFrom(packet)
|
||||
}
|
||||
|
||||
|
||||
@ -61,8 +60,8 @@ public interface ModbusDevice : Device {
|
||||
}
|
||||
|
||||
public operator fun <T> ModbusRegistryKey.HoldingRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||
val packet = readInputRegistersToPacket(address, count)
|
||||
return format.readObject(packet)
|
||||
val packet = readHoldingRegistersToPacket(address, count)
|
||||
return format.readFrom(packet)
|
||||
}
|
||||
|
||||
public operator fun <T> ModbusRegistryKey.HoldingRange<T>.setValue(
|
||||
@ -70,9 +69,9 @@ public interface ModbusDevice : Device {
|
||||
property: KProperty<*>,
|
||||
value: T,
|
||||
) {
|
||||
val buffer = buildPacket {
|
||||
format.writeObject(this, value)
|
||||
}.readByteBuffer()
|
||||
val buffer = ByteArray {
|
||||
format.writeTo(this, value)
|
||||
}
|
||||
writeHoldingRegisters(address, buffer)
|
||||
}
|
||||
|
||||
@ -82,35 +81,35 @@ public interface ModbusDevice : Device {
|
||||
* Read multiple sequential modbus coils (bit-values)
|
||||
*/
|
||||
public fun ModbusDevice.readCoils(address: Int, count: Int): BitVector =
|
||||
master.readCoils(clientId, address, count)
|
||||
master.readCoils(unitId, address, count)
|
||||
|
||||
public fun ModbusDevice.readCoil(address: Int): Boolean =
|
||||
master.readCoils(clientId, address, 1).getBit(0)
|
||||
master.readCoils(unitId, address, 1).getBit(0)
|
||||
|
||||
public fun ModbusDevice.writeCoils(address: Int, values: BooleanArray) {
|
||||
val bitVector = BitVector(values.size)
|
||||
values.forEachIndexed { index, value ->
|
||||
bitVector.setBit(index, value)
|
||||
}
|
||||
master.writeMultipleCoils(clientId, address, bitVector)
|
||||
master.writeMultipleCoils(unitId, address, bitVector)
|
||||
}
|
||||
|
||||
public fun ModbusDevice.writeCoil(address: Int, value: Boolean) {
|
||||
master.writeCoil(clientId, address, value)
|
||||
master.writeCoil(unitId, address, value)
|
||||
}
|
||||
|
||||
public fun ModbusDevice.writeCoil(key: ModbusRegistryKey.Coil, value: Boolean) {
|
||||
master.writeCoil(clientId, key.address, value)
|
||||
master.writeCoil(unitId, key.address, value)
|
||||
}
|
||||
|
||||
public fun ModbusDevice.readInputDiscretes(address: Int, count: Int): BitVector =
|
||||
master.readInputDiscretes(clientId, address, count)
|
||||
master.readInputDiscretes(unitId, address, count)
|
||||
|
||||
public fun ModbusDevice.readInputDiscrete(address: Int): Boolean =
|
||||
master.readInputDiscretes(clientId, address, 1).getBit(0)
|
||||
master.readInputDiscretes(unitId, address, 1).getBit(0)
|
||||
|
||||
public fun ModbusDevice.readInputRegisters(address: Int, count: Int): List<InputRegister> =
|
||||
master.readInputRegisters(clientId, address, count).toList()
|
||||
master.readInputRegisters(unitId, address, count).toList()
|
||||
|
||||
private fun Array<out InputRegister>.toBuffer(): ByteBuffer {
|
||||
val buffer: ByteBuffer = ByteBuffer.allocate(size * 2)
|
||||
@ -122,17 +121,17 @@ private fun Array<out InputRegister>.toBuffer(): ByteBuffer {
|
||||
return buffer
|
||||
}
|
||||
|
||||
private fun Array<out InputRegister>.toPacket(): ByteReadPacket = buildPacket {
|
||||
private fun Array<out InputRegister>.toPacket(): Buffer = Buffer {
|
||||
forEach { value ->
|
||||
writeShort(value.toShort())
|
||||
}
|
||||
}
|
||||
|
||||
public fun ModbusDevice.readInputRegistersToBuffer(address: Int, count: Int): ByteBuffer =
|
||||
master.readInputRegisters(clientId, address, count).toBuffer()
|
||||
master.readInputRegisters(unitId, address, count).toBuffer()
|
||||
|
||||
public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): ByteReadPacket =
|
||||
master.readInputRegisters(clientId, address, count).toPacket()
|
||||
public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): Buffer =
|
||||
master.readInputRegisters(unitId, address, count).toPacket()
|
||||
|
||||
public fun ModbusDevice.readDoubleInput(address: Int): Double =
|
||||
readInputRegistersToBuffer(address, Double.SIZE_BYTES).getDouble()
|
||||
@ -141,7 +140,7 @@ public fun ModbusDevice.readInputRegister(address: Int): Short =
|
||||
readInputRegisters(address, 1).first().toShort()
|
||||
|
||||
public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List<Register> =
|
||||
master.readMultipleRegisters(clientId, address, count).toList()
|
||||
master.readMultipleRegisters(unitId, address, count).toList()
|
||||
|
||||
/**
|
||||
* Read a number of registers to a [ByteBuffer]
|
||||
@ -149,10 +148,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]
|
||||
*/
|
||||
public fun ModbusDevice.readHoldingRegistersToBuffer(address: Int, count: Int): ByteBuffer =
|
||||
master.readMultipleRegisters(clientId, address, count).toBuffer()
|
||||
master.readMultipleRegisters(unitId, address, count).toBuffer()
|
||||
|
||||
public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): ByteReadPacket =
|
||||
master.readMultipleRegisters(clientId, address, count).toPacket()
|
||||
public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): Buffer =
|
||||
master.readMultipleRegisters(unitId, address, count).toPacket()
|
||||
|
||||
public fun ModbusDevice.readDoubleRegister(address: Int): Double =
|
||||
readHoldingRegistersToBuffer(address, Double.SIZE_BYTES).getDouble()
|
||||
@ -162,14 +161,14 @@ public fun ModbusDevice.readHoldingRegister(address: Int): Short =
|
||||
|
||||
public fun ModbusDevice.writeHoldingRegisters(address: Int, values: ShortArray): Int =
|
||||
master.writeMultipleRegisters(
|
||||
clientId,
|
||||
unitId,
|
||||
address,
|
||||
Array<Register>(values.size) { SimpleInputRegister(values[it].toInt()) }
|
||||
)
|
||||
|
||||
public fun ModbusDevice.writeHoldingRegister(address: Int, value: Short): Int =
|
||||
master.writeSingleRegister(
|
||||
clientId,
|
||||
unitId,
|
||||
address,
|
||||
SimpleInputRegister(value.toInt())
|
||||
)
|
||||
@ -183,8 +182,11 @@ public fun ModbusDevice.writeHoldingRegisters(address: Int, buffer: ByteBuffer):
|
||||
return writeHoldingRegisters(address, array)
|
||||
}
|
||||
|
||||
public fun ModbusDevice.writeShortRegister(address: Int, value: Short) {
|
||||
master.writeSingleRegister(address, SimpleInputRegister(value.toInt()))
|
||||
public fun ModbusDevice.writeHoldingRegisters(address: Int, byteArray: ByteArray): Int {
|
||||
val buffer = ByteBuffer.wrap(byteArray)
|
||||
val array: ShortArray = ShortArray(buffer.limit().floorDiv(2)) { buffer.getShort(it * 2) }
|
||||
|
||||
return writeHoldingRegisters(address, array)
|
||||
}
|
||||
|
||||
public fun ModbusDevice.modbusRegister(
|
||||
|
@ -15,21 +15,19 @@ import space.kscience.dataforge.names.NameToken
|
||||
public open class ModbusDeviceBySpec<D: Device>(
|
||||
context: Context,
|
||||
spec: DeviceSpec<D>,
|
||||
override val clientId: Int,
|
||||
override val unitId: Int,
|
||||
override val master: AbstractModbusMaster,
|
||||
private val disposeMasterOnClose: Boolean = true,
|
||||
meta: Meta = Meta.EMPTY,
|
||||
) : ModbusDevice, DeviceBySpec<D>(spec, context, meta){
|
||||
override suspend fun open() {
|
||||
override suspend fun onStart() {
|
||||
master.connect()
|
||||
super<DeviceBySpec>.open()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
override fun onStop() {
|
||||
if(disposeMasterOnClose){
|
||||
master.disconnect()
|
||||
}
|
||||
super<ModbusDevice>.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,15 @@
|
||||
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
|
||||
|
||||
|
||||
/**
|
||||
* Modbus registry key
|
||||
*/
|
||||
public sealed class ModbusRegistryKey {
|
||||
public abstract val address: Int
|
||||
public open val count: Int = 1
|
||||
@ -25,6 +32,9 @@ public sealed class ModbusRegistryKey {
|
||||
override fun toString(): String = "InputRegister(address=$address)"
|
||||
}
|
||||
|
||||
/**
|
||||
* A range of read-only register encoding a single value
|
||||
*/
|
||||
public class InputRange<T>(
|
||||
address: Int,
|
||||
override val count: Int,
|
||||
@ -36,10 +46,16 @@ public sealed class ModbusRegistryKey {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A single read-write register
|
||||
*/
|
||||
public open class HoldingRegister(override val address: Int) : ModbusRegistryKey() {
|
||||
override fun toString(): String = "HoldingRegister(address=$address)"
|
||||
}
|
||||
|
||||
/**
|
||||
* A range of read-write registers encoding a single value
|
||||
*/
|
||||
public class HoldingRange<T>(
|
||||
address: Int,
|
||||
override val count: Int,
|
||||
@ -52,6 +68,9 @@ public sealed class ModbusRegistryKey {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A base class for modbus registers
|
||||
*/
|
||||
public abstract class ModbusRegistryMap {
|
||||
|
||||
private val _entries: MutableMap<ModbusRegistryKey, String> = mutableMapOf<ModbusRegistryKey, String>()
|
||||
@ -63,36 +82,56 @@ public abstract class ModbusRegistryMap {
|
||||
return key
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a [ModbusRegistryKey.Coil] key and return it
|
||||
*/
|
||||
protected fun coil(address: Int, description: String = ""): ModbusRegistryKey.Coil =
|
||||
register(ModbusRegistryKey.Coil(address), description)
|
||||
|
||||
|
||||
/**
|
||||
* Register a [ModbusRegistryKey.DiscreteInput] key and return it
|
||||
*/
|
||||
protected fun discrete(address: Int, description: String = ""): ModbusRegistryKey.DiscreteInput =
|
||||
register(ModbusRegistryKey.DiscreteInput(address), description)
|
||||
|
||||
/**
|
||||
* Register a [ModbusRegistryKey.InputRegister] key and return it
|
||||
*/
|
||||
protected fun input(address: Int, description: String = ""): ModbusRegistryKey.InputRegister =
|
||||
register(ModbusRegistryKey.InputRegister(address), description)
|
||||
|
||||
/**
|
||||
* Register a [ModbusRegistryKey.InputRange] key and return it
|
||||
*/
|
||||
protected fun <T> input(
|
||||
address: Int,
|
||||
count: Int,
|
||||
reader: IOFormat<T>,
|
||||
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 =
|
||||
register(ModbusRegistryKey.HoldingRegister(address), description)
|
||||
|
||||
/**
|
||||
* Register a [ModbusRegistryKey.HoldingRange] key and return it
|
||||
*/
|
||||
protected fun <T> register(
|
||||
address: Int,
|
||||
count: Int,
|
||||
format: IOFormat<T>,
|
||||
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 {
|
||||
|
||||
/**
|
||||
* Validate the register map. Throw an error if the map is invalid
|
||||
*/
|
||||
public fun validate(map: ModbusRegistryMap) {
|
||||
var lastCoil: ModbusRegistryKey.Coil? = null
|
||||
var lastDiscreteInput: ModbusRegistryKey.DiscreteInput? = null
|
||||
@ -127,36 +166,62 @@ public abstract class ModbusRegistryMap {
|
||||
}
|
||||
}
|
||||
|
||||
private val ModbusRegistryKey.sectionNumber
|
||||
get() = when (this) {
|
||||
is ModbusRegistryKey.Coil -> 1
|
||||
is ModbusRegistryKey.DiscreteInput -> 2
|
||||
is ModbusRegistryKey.HoldingRegister -> 4
|
||||
is ModbusRegistryKey.InputRegister -> 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun print(map: ModbusRegistryMap, to: Appendable = System.out) {
|
||||
validate(map)
|
||||
map.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}"
|
||||
}
|
||||
to.appendLine("${typeString}\t$rangeString\t$description")
|
||||
}
|
||||
private val ModbusRegistryKey.sectionNumber
|
||||
get() = when (this) {
|
||||
is ModbusRegistryKey.Coil -> 1
|
||||
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 {
|
||||
ModbusRegistryMap.validate(this@toJson)
|
||||
entries.forEach { (key, description) ->
|
||||
|
||||
val entry = buildJsonObject {
|
||||
put(
|
||||
"type",
|
||||
when (key) {
|
||||
is ModbusRegistryKey.Coil -> "Coil"
|
||||
is ModbusRegistryKey.DiscreteInput -> "Discrete"
|
||||
is ModbusRegistryKey.HoldingRegister -> "Register"
|
||||
is ModbusRegistryKey.InputRegister -> "Input"
|
||||
}
|
||||
)
|
||||
put("address", key.address)
|
||||
if (key.count > 1) {
|
||||
put("count", key.count)
|
||||
}
|
||||
put("description", description)
|
||||
}
|
||||
|
||||
add(entry)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,7 +107,7 @@ internal class MetaStructureCodec(
|
||||
|
||||
override fun createStructure(name: String, members: LinkedHashMap<String, Meta>): Meta = Meta {
|
||||
members.forEach { (property: String, value: Meta?) ->
|
||||
setMeta(Name.parse(property), value)
|
||||
set(Name.parse(property), value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,7 +147,7 @@ internal class MetaStructureCodec(
|
||||
"Float" -> member.value?.numberOrNull?.toFloat()
|
||||
"Double" -> member.value?.numberOrNull?.toDouble()
|
||||
"String" -> member.string
|
||||
"DateTime" -> DateTime(member.instant().toJavaInstant())
|
||||
"DateTime" -> member.instant?.toJavaInstant()?.let { DateTime(it) }
|
||||
"Guid" -> member.string?.let { UUID.fromString(it) }
|
||||
"ByteString" -> member.value?.list?.let { list ->
|
||||
ByteString(list.map { it.number.toByte() }.toByteArray())
|
||||
|
@ -43,7 +43,7 @@ public suspend inline fun <reified T: Any> OpcUaDevice.readOpcWithTime(
|
||||
else -> error("Incompatible OPC property value $content")
|
||||
}
|
||||
|
||||
val res: T = converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
|
||||
val res: T = converter.metaToObject(meta)
|
||||
return res to time
|
||||
}
|
||||
|
||||
|
@ -63,8 +63,7 @@ public open class OpcUaDeviceBySpec<D : Device>(
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
override fun onStop() {
|
||||
client.disconnect()
|
||||
super<DeviceBySpec>.close()
|
||||
}
|
||||
}
|
||||
|
@ -19,10 +19,7 @@ import org.eclipse.milo.opcua.stack.core.AttributeId
|
||||
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.LocalizedText
|
||||
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.api.*
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.MetaSerializer
|
||||
@ -31,7 +28,7 @@ import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.plus
|
||||
|
||||
|
||||
public operator fun Device.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name)
|
||||
public operator fun CachingDevice.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name)
|
||||
|
||||
public suspend fun Device.read(propertyDescriptor: PropertyDescriptor): Meta = readProperty(propertyDescriptor.name)
|
||||
|
||||
@ -73,11 +70,11 @@ public class DeviceNameSpace(
|
||||
//for now, use DF paths as ids
|
||||
nodeId = newNodeId("${deviceName.tokens.joinToString("/")}/$propertyName")
|
||||
when {
|
||||
descriptor.readable && descriptor.writable -> {
|
||||
descriptor.readable && descriptor.mutable -> {
|
||||
setAccessLevel(AccessLevel.READ_WRITE)
|
||||
setUserAccessLevel(AccessLevel.READ_WRITE)
|
||||
}
|
||||
descriptor.writable -> {
|
||||
descriptor.mutable -> {
|
||||
setAccessLevel(AccessLevel.WRITE_ONLY)
|
||||
setUserAccessLevel(AccessLevel.WRITE_ONLY)
|
||||
}
|
||||
@ -106,9 +103,11 @@ public class DeviceNameSpace(
|
||||
setTypeDefinition(Identifiers.BaseDataVariableType)
|
||||
}.build()
|
||||
|
||||
|
||||
device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let {
|
||||
node.value = it
|
||||
// Update initial value, but only if it is cached
|
||||
if(device is CachingDevice) {
|
||||
device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let {
|
||||
node.value = it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,7 +29,7 @@ class OpcUaClientTest {
|
||||
return DemoOpcUaDevice(config)
|
||||
}
|
||||
|
||||
val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble)
|
||||
val randomDouble by doubleProperty { readRandomDouble() }
|
||||
|
||||
}
|
||||
|
||||
@ -40,9 +40,10 @@ class OpcUaClientTest {
|
||||
@Test
|
||||
@Ignore
|
||||
fun testReadDouble() = runTest {
|
||||
DemoOpcUaDevice.build().use{
|
||||
println(it.read(DemoOpcUaDevice.randomDouble))
|
||||
}
|
||||
val device = DemoOpcUaDevice.build()
|
||||
device.start()
|
||||
println(device.read(DemoOpcUaDevice.randomDouble))
|
||||
device.stop()
|
||||
}
|
||||
|
||||
}
|
@ -7,10 +7,12 @@ description = """
|
||||
Utils to work with controls-kt on Raspberry pi
|
||||
""".trimIndent()
|
||||
|
||||
val pi4jVerstion = "2.3.0"
|
||||
|
||||
dependencies{
|
||||
api(project(":controls-core"))
|
||||
api("com.pi4j:pi4j-ktx:2.4.0") // Kotlin DSL
|
||||
api("com.pi4j:pi4j-core:2.3.0")
|
||||
api("com.pi4j:pi4j-plugin-raspberrypi:2.3.0")
|
||||
api("com.pi4j:pi4j-plugin-pigpio:2.3.0")
|
||||
api("com.pi4j:pi4j-core:$pi4jVerstion")
|
||||
api("com.pi4j:pi4j-plugin-raspberrypi:$pi4jVerstion")
|
||||
api("com.pi4j:pi4j-plugin-pigpio:$pi4jVerstion")
|
||||
}
|
@ -1,22 +1,46 @@
|
||||
package space.kscience.controls.pi
|
||||
|
||||
import com.pi4j.Pi4J
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
import space.kscience.controls.ports.PortFactory
|
||||
import space.kscience.controls.ports.Ports
|
||||
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
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
import com.pi4j.context.Context as PiContext
|
||||
|
||||
public class PiPlugin : AbstractPlugin() {
|
||||
public val ports: Ports by require(Ports)
|
||||
public val devices: DeviceManager by require(DeviceManager)
|
||||
|
||||
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) {
|
||||
PortFactory.TYPE -> mapOf(
|
||||
PiSerialPort.type.parseAsName() to PiSerialPort,
|
||||
)
|
||||
|
||||
else -> super.content(target)
|
||||
}
|
||||
|
||||
override fun detach() {
|
||||
piContext.shutdown()
|
||||
super.detach()
|
||||
}
|
||||
|
||||
public companion object : PluginFactory<PiPlugin> {
|
||||
|
||||
override val tag: PluginTag = PluginTag("controls.ports.pi", group = PluginTag.DATAFORGE_GROUP)
|
||||
|
||||
override fun build(context: Context, meta: Meta): PiPlugin = PiPlugin()
|
||||
|
||||
public fun createPiContext(context: Context, meta: Meta): PiContext = Pi4J.newAutoContext()
|
||||
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
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
|
||||
@ -13,20 +12,25 @@ 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.context.request
|
||||
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
|
||||
import com.pi4j.context.Context as PiContext
|
||||
|
||||
public class PiSerialPort(
|
||||
context: Context,
|
||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||
public val serialBuilder: () -> Serial,
|
||||
public val serialBuilder: PiContext.() -> Serial,
|
||||
) : AbstractPort(context, coroutineContext) {
|
||||
|
||||
private val serial: Serial by lazy { serialBuilder() }
|
||||
private val serial: Serial by lazy {
|
||||
val pi = context.request(PiPlugin)
|
||||
pi.piContext.serialBuilder()
|
||||
}
|
||||
|
||||
|
||||
private val listenerJob = this.scope.launch(Dispatchers.IO) {
|
||||
@ -57,15 +61,18 @@ public class PiSerialPort(
|
||||
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)
|
||||
}
|
||||
public fun open(
|
||||
context: Context,
|
||||
device: String,
|
||||
block: SerialConfigBuilder.() -> Unit,
|
||||
): PiSerialPort = PiSerialPort(context) {
|
||||
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) {
|
||||
serial(device) {
|
||||
baud8N1(baudRate)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import space.kscience.gradle.Maturity
|
||||
|
||||
plugins {
|
||||
id("space.kscience.gradle.jvm")
|
||||
id("space.kscience.gradle.mpp")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
@ -12,16 +12,20 @@ description = """
|
||||
val dataforgeVersion: String by rootProject.extra
|
||||
val ktorVersion: String by rootProject.extra
|
||||
|
||||
dependencies {
|
||||
implementation(projects.controlsCore)
|
||||
implementation(projects.controlsPortsKtor)
|
||||
implementation(projects.magix.magixServer)
|
||||
implementation("io.ktor:ktor-server-cio:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-websockets:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-html-builder:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-status-pages:$ktorVersion")
|
||||
|
||||
kscience {
|
||||
jvm()
|
||||
dependencies {
|
||||
implementation(projects.controlsCore)
|
||||
implementation(projects.controlsPortsKtor)
|
||||
implementation(projects.magix.magixServer)
|
||||
implementation("io.ktor:ktor-server-cio:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-websockets:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-html-builder:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-status-pages:$ktorVersion")
|
||||
}
|
||||
}
|
||||
|
||||
readme{
|
||||
|
33
controls-vision/build.gradle.kts
Normal file
33
controls-vision/build.gradle.kts
Normal file
@ -0,0 +1,33 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
description = """
|
||||
Dashboard and visualization extensions for devices
|
||||
""".trimIndent()
|
||||
|
||||
val visionforgeVersion: String by rootProject.extra
|
||||
|
||||
kscience {
|
||||
fullStack("js/controls-vision.js")
|
||||
useKtor()
|
||||
useContextReceivers()
|
||||
dependencies {
|
||||
api(projects.controlsCore)
|
||||
api(projects.controlsConstructor)
|
||||
api("space.kscience:visionforge-plotly:$visionforgeVersion")
|
||||
api("space.kscience:visionforge-markdown:$visionforgeVersion")
|
||||
// api("space.kscience:tables-kt:0.2.1")
|
||||
// api("space.kscience:visionforge-tables:$visionforgeVersion")
|
||||
}
|
||||
|
||||
jvmMain{
|
||||
api("space.kscience:visionforge-server:$visionforgeVersion")
|
||||
api("io.ktor:ktor-server-cio")
|
||||
}
|
||||
}
|
||||
|
||||
readme {
|
||||
maturity = space.kscience.gradle.Maturity.PROTOTYPE
|
||||
}
|
19
controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt
Normal file
19
controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt
Normal file
@ -0,0 +1,19 @@
|
||||
package space.kscience.controls.vision
|
||||
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
import space.kscience.dataforge.context.PluginFactory
|
||||
import space.kscience.visionforge.Vision
|
||||
import space.kscience.visionforge.VisionPlugin
|
||||
import space.kscience.visionforge.plotly.VisionOfPlotly
|
||||
|
||||
public expect class ControlVisionPlugin: VisionPlugin{
|
||||
public companion object: PluginFactory<ControlVisionPlugin>
|
||||
}
|
||||
|
||||
internal val controlsVisionSerializersModule = SerializersModule {
|
||||
polymorphic(Vision::class) {
|
||||
subclass(VisionOfPlotly.serializer())
|
||||
}
|
||||
}
|
20
controls-vision/src/commonMain/kotlin/IndicatorVision.kt
Normal file
20
controls-vision/src/commonMain/kotlin/IndicatorVision.kt
Normal file
@ -0,0 +1,20 @@
|
||||
package space.kscience.controls.vision
|
||||
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.node
|
||||
import space.kscience.visionforge.AbstractVision
|
||||
import space.kscience.visionforge.Vision
|
||||
|
||||
/**
|
||||
* A [Vision] that shows an indicator
|
||||
*/
|
||||
public class IndicatorVision: AbstractVision() {
|
||||
public val value: Meta? by properties.node()
|
||||
}
|
||||
|
||||
///**
|
||||
// * A [Vision] that allows both showing the value and changing it
|
||||
// */
|
||||
//public interface RegulatorVision: IndicatorVision{
|
||||
//
|
||||
//}
|
167
controls-vision/src/commonMain/kotlin/plotExtensions.kt
Normal file
167
controls-vision/src/commonMain/kotlin/plotExtensions.kt
Normal file
@ -0,0 +1,167 @@
|
||||
@file:OptIn(FlowPreview::class)
|
||||
|
||||
package space.kscience.controls.vision
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.api.propertyMessageFlow
|
||||
import space.kscience.controls.constructor.DeviceState
|
||||
import space.kscience.controls.manager.clock
|
||||
import space.kscience.controls.misc.ValueWithTime
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.plotly.Plot
|
||||
import space.kscience.plotly.bar
|
||||
import space.kscience.plotly.models.Bar
|
||||
import space.kscience.plotly.models.Scatter
|
||||
import space.kscience.plotly.models.Trace
|
||||
import space.kscience.plotly.models.TraceValues
|
||||
import space.kscience.plotly.scatter
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
private var TraceValues.values: List<Value>
|
||||
get() = value?.list ?: emptyList()
|
||||
set(newValues) {
|
||||
value = ListValue(newValues)
|
||||
}
|
||||
|
||||
|
||||
private var TraceValues.times: List<Instant>
|
||||
get() = value?.list?.map { Instant.parse(it.string) } ?: emptyList()
|
||||
set(newValues) {
|
||||
value = ListValue(newValues.map { it.toString().asValue() })
|
||||
}
|
||||
|
||||
|
||||
private class TimeData(private var points: MutableList<ValueWithTime<Value>> = mutableListOf()) {
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun append(time: Instant, value: Value) = mutex.withLock {
|
||||
points.add(ValueWithTime(value, time))
|
||||
}
|
||||
|
||||
suspend fun trim(maxAge: Duration, maxPoints: Int = 800, minPoints: Int = 400) {
|
||||
require(maxPoints > 2)
|
||||
require(minPoints > 0)
|
||||
require(maxPoints > minPoints)
|
||||
val now = Clock.System.now()
|
||||
// filter old points
|
||||
points.removeAll { now - it.time > maxAge }
|
||||
|
||||
if (points.size > maxPoints) {
|
||||
val durationBetweenPoints = maxAge / minPoints
|
||||
val markedForRemoval = buildList<ValueWithTime<Value>> {
|
||||
var lastTime: Instant? = null
|
||||
points.forEach { point ->
|
||||
if (lastTime?.let { point.time - it < durationBetweenPoints } == true) {
|
||||
add(point)
|
||||
} else {
|
||||
lastTime = point.time
|
||||
}
|
||||
}
|
||||
}
|
||||
points.removeAll(markedForRemoval)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fillPlot(x: TraceValues, y: TraceValues) = mutex.withLock {
|
||||
x.strings = points.map { it.time.toString() }
|
||||
y.values = points.map { it.value }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a trace that shows a [Device] property change over time. Show only latest [maxPoints] .
|
||||
* @return a [Job] that handles the listener
|
||||
*/
|
||||
public fun Plot.plotDeviceProperty(
|
||||
device: Device,
|
||||
propertyName: String,
|
||||
extractValue: Meta.() -> Value = { value ?: Null },
|
||||
maxAge: Duration = 1.hours,
|
||||
maxPoints: Int = 800,
|
||||
minPoints: Int = 400,
|
||||
sampling: Duration = 10.milliseconds,
|
||||
coroutineScope: CoroutineScope = device.context,
|
||||
configuration: Scatter.() -> Unit = {},
|
||||
): Job = scatter(configuration).run {
|
||||
val clock = device.context.clock
|
||||
val data = TimeData()
|
||||
device.propertyMessageFlow(propertyName).sample(sampling).transform {
|
||||
data.append(it.time ?: clock.now(), it.value.extractValue())
|
||||
data.trim(maxAge, maxPoints, minPoints)
|
||||
emit(data)
|
||||
}.onEach {
|
||||
it.fillPlot(x, y)
|
||||
}.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
private fun <T> Trace.updateFromState(
|
||||
context: Context,
|
||||
state: DeviceState<T>,
|
||||
extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: space.kscience.dataforge.meta.Null },
|
||||
maxAge: Duration = 1.hours,
|
||||
maxPoints: Int = 800,
|
||||
minPoints: Int = 400,
|
||||
sampling: Duration = 10.milliseconds,
|
||||
): Job {
|
||||
val clock = context.clock
|
||||
val data = TimeData()
|
||||
return state.valueFlow.sample(sampling).transform<T, TimeData> {
|
||||
data.append(clock.now(), it.extractValue())
|
||||
data.trim(maxAge, maxPoints, minPoints)
|
||||
}.onEach {
|
||||
it.fillPlot(x, y)
|
||||
}.launchIn(context)
|
||||
}
|
||||
|
||||
public fun <T> Plot.plotDeviceState(
|
||||
context: Context,
|
||||
state: DeviceState<T>,
|
||||
extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: Null },
|
||||
maxAge: Duration = 1.hours,
|
||||
maxPoints: Int = 800,
|
||||
minPoints: Int = 400,
|
||||
sampling: Duration = 10.milliseconds,
|
||||
configuration: Scatter.() -> Unit = {},
|
||||
): Job = scatter(configuration).run {
|
||||
updateFromState(context, state, extractValue, maxAge, maxPoints, minPoints, sampling)
|
||||
}
|
||||
|
||||
|
||||
public fun Plot.plotNumberState(
|
||||
context: Context,
|
||||
state: DeviceState<out Number>,
|
||||
maxAge: Duration = 1.hours,
|
||||
maxPoints: Int = 800,
|
||||
minPoints: Int = 400,
|
||||
sampling: Duration = 10.milliseconds,
|
||||
configuration: Scatter.() -> Unit = {},
|
||||
): Job = scatter(configuration).run {
|
||||
updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, sampling)
|
||||
}
|
||||
|
||||
|
||||
public fun Plot.plotBooleanState(
|
||||
context: Context,
|
||||
state: DeviceState<Boolean>,
|
||||
maxAge: Duration = 1.hours,
|
||||
maxPoints: Int = 800,
|
||||
minPoints: Int = 400,
|
||||
sampling: Duration = 10.milliseconds,
|
||||
configuration: Bar.() -> Unit = {},
|
||||
): Job = bar(configuration).run {
|
||||
updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, sampling)
|
||||
}
|
34
controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt
Normal file
34
controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt
Normal file
@ -0,0 +1,34 @@
|
||||
package space.kscience.controls.vision
|
||||
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import org.w3c.dom.Element
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.PluginFactory
|
||||
import space.kscience.dataforge.context.PluginTag
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.visionforge.ElementVisionRenderer
|
||||
import space.kscience.visionforge.Vision
|
||||
import space.kscience.visionforge.VisionClient
|
||||
import space.kscience.visionforge.VisionPlugin
|
||||
|
||||
public actual class ControlVisionPlugin : VisionPlugin(), ElementVisionRenderer {
|
||||
override val tag: PluginTag get() = Companion.tag
|
||||
|
||||
override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule
|
||||
|
||||
override fun rateVision(vision: Vision): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
public actual companion object : PluginFactory<ControlVisionPlugin> {
|
||||
override val tag: PluginTag = PluginTag("controls.vision")
|
||||
|
||||
override fun build(context: Context, meta: Meta): ControlVisionPlugin = ControlVisionPlugin()
|
||||
|
||||
}
|
||||
}
|
11
controls-vision/src/jsMain/kotlin/client.kt
Normal file
11
controls-vision/src/jsMain/kotlin/client.kt
Normal file
@ -0,0 +1,11 @@
|
||||
package space.kscience.controls.vision
|
||||
|
||||
import space.kscience.visionforge.markup.MarkupPlugin
|
||||
import space.kscience.visionforge.plotly.PlotlyPlugin
|
||||
import space.kscience.visionforge.runVisionClient
|
||||
|
||||
public fun main(): Unit = runVisionClient {
|
||||
plugin(PlotlyPlugin)
|
||||
plugin(MarkupPlugin)
|
||||
// plugin(TableVisionJsPlugin)
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package space.kscience.controls.vision
|
||||
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.PluginFactory
|
||||
import space.kscience.dataforge.context.PluginTag
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.visionforge.VisionPlugin
|
||||
|
||||
public actual class ControlVisionPlugin : VisionPlugin() {
|
||||
override val tag: PluginTag get() = Companion.tag
|
||||
|
||||
override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule
|
||||
|
||||
public actual companion object : PluginFactory<ControlVisionPlugin> {
|
||||
override val tag: PluginTag = PluginTag("controls.vision")
|
||||
|
||||
override fun build(context: Context, meta: Meta): ControlVisionPlugin = ControlVisionPlugin()
|
||||
|
||||
}
|
||||
}
|
61
controls-vision/src/jvmMain/kotlin/dashboard.kt
Normal file
61
controls-vision/src/jvmMain/kotlin/dashboard.kt
Normal file
@ -0,0 +1,61 @@
|
||||
package space.kscience.controls.vision
|
||||
|
||||
import io.ktor.server.cio.CIO
|
||||
import io.ktor.server.engine.ApplicationEngine
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.http.content.staticResources
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.routing
|
||||
import kotlinx.html.TagConsumer
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.plotly.Plot
|
||||
import space.kscience.plotly.PlotlyConfig
|
||||
import space.kscience.visionforge.html.HtmlVisionFragment
|
||||
import space.kscience.visionforge.html.VisionPage
|
||||
import space.kscience.visionforge.html.VisionTagConsumer
|
||||
import space.kscience.visionforge.plotly.plotly
|
||||
import space.kscience.visionforge.server.VisionRoute
|
||||
import space.kscience.visionforge.server.close
|
||||
import space.kscience.visionforge.server.openInBrowser
|
||||
import space.kscience.visionforge.server.visionPage
|
||||
import space.kscience.visionforge.visionManager
|
||||
|
||||
public fun Context.showDashboard(
|
||||
port: Int = 7777,
|
||||
routes: Routing.() -> Unit = {},
|
||||
configurationBuilder: VisionRoute.() -> Unit = {},
|
||||
visionFragment: HtmlVisionFragment,
|
||||
): ApplicationEngine = embeddedServer(CIO, port = port) {
|
||||
routing {
|
||||
staticResources("", null, null)
|
||||
routes()
|
||||
}
|
||||
|
||||
visionPage(
|
||||
visionManager,
|
||||
VisionPage.scriptHeader("js/controls-vision.js"),
|
||||
configurationBuilder = configurationBuilder,
|
||||
visionFragment = visionFragment
|
||||
)
|
||||
}.also {
|
||||
it.start(false)
|
||||
it.openInBrowser()
|
||||
|
||||
|
||||
println("Enter 'exit' to close server")
|
||||
while (readlnOrNull() != "exit") {
|
||||
//
|
||||
}
|
||||
|
||||
it.close()
|
||||
}
|
||||
|
||||
context(VisionTagConsumer<*>)
|
||||
public fun TagConsumer<*>.plot(
|
||||
config: PlotlyConfig = PlotlyConfig(),
|
||||
block: Plot.() -> Unit,
|
||||
) {
|
||||
vision {
|
||||
plotly(config, block)
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ dependencies {
|
||||
|
||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||
implementation("no.tornado:tornadofx:1.7.20")
|
||||
implementation("space.kscience:plotlykt-server:0.5.3")
|
||||
implementation("space.kscience:plotlykt-server:0.6.0")
|
||||
// implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ class DemoController : Controller(), ContextAware {
|
||||
logger.info { "Visualization server stopped" }
|
||||
magixServer?.stop(1000, 5000)
|
||||
logger.info { "Magix server stopped" }
|
||||
device?.close()
|
||||
device?.stop()
|
||||
logger.info { "Device server stopped" }
|
||||
context.close()
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import kotlin.math.sin
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
interface IDemoDevice: Device {
|
||||
interface IDemoDevice : Device {
|
||||
var timeScaleState: Double
|
||||
var sinScaleState: Double
|
||||
var cosScaleState: Double
|
||||
@ -42,16 +42,16 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec<IDemoDevice>(Compa
|
||||
// register virtual properties based on actual object state
|
||||
val timeScale by mutableProperty(MetaConverter.double, IDemoDevice::timeScaleState) {
|
||||
metaDescriptor {
|
||||
type(ValueType.NUMBER)
|
||||
valueType(ValueType.NUMBER)
|
||||
}
|
||||
info = "Real to virtual time scale"
|
||||
description = "Real to virtual time scale"
|
||||
}
|
||||
|
||||
val sinScale by mutableProperty(MetaConverter.double, IDemoDevice::sinScaleState)
|
||||
val cosScale by mutableProperty(MetaConverter.double, IDemoDevice::cosScaleState)
|
||||
|
||||
val sin by doubleProperty(read = IDemoDevice::sinValue)
|
||||
val cos by doubleProperty(read = IDemoDevice::cosValue)
|
||||
val sin by doubleProperty { sinValue() }
|
||||
val cos by doubleProperty { cosValue() }
|
||||
|
||||
val coordinates by metaProperty(
|
||||
descriptorBuilder = {
|
||||
|
@ -57,15 +57,15 @@ fun CoroutineScope.startDemoDeviceServer(magixEndpoint: MagixEndpoint): Applicat
|
||||
//share subscription to a parse message only once
|
||||
val subscription = magixEndpoint.subscribe(DeviceManager.magixFormat).shareIn(this, SharingStarted.Lazily)
|
||||
|
||||
val sinFlow = subscription.mapNotNull { (_, payload) ->
|
||||
val sinFlow = subscription.mapNotNull { (_, payload) ->
|
||||
(payload as? PropertyChangedMessage)?.takeIf { it.property == DemoDevice.sin.name }
|
||||
}.map { it.value }
|
||||
|
||||
val cosFlow = subscription.mapNotNull { (_, payload) ->
|
||||
val cosFlow = subscription.mapNotNull { (_, payload) ->
|
||||
(payload as? PropertyChangedMessage)?.takeIf { it.property == DemoDevice.cos.name }
|
||||
}.map { it.value }
|
||||
|
||||
val sinCosFlow = subscription.mapNotNull { (_, payload) ->
|
||||
val sinCosFlow = subscription.mapNotNull { (_, payload) ->
|
||||
(payload as? PropertyChangedMessage)?.takeIf { it.property == DemoDevice.coordinates.name }
|
||||
}.map { it.value }
|
||||
|
||||
|
@ -2,6 +2,8 @@ package space.kscience.controls.demo.car
|
||||
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.DeviceSpec
|
||||
import space.kscience.controls.spec.mutableProperty
|
||||
import space.kscience.controls.spec.property
|
||||
|
||||
interface IVirtualCar : Device {
|
||||
var speedState: Vector2D
|
||||
|
@ -14,7 +14,6 @@ import space.kscience.dataforge.names.Name
|
||||
import space.kscience.magix.api.MagixEndpoint
|
||||
import space.kscience.magix.api.subscribe
|
||||
import space.kscience.magix.rsocket.rSocketWithWebSockets
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
class MagixVirtualCar(context: Context, meta: Meta) : VirtualCar(context, meta) {
|
||||
|
||||
@ -31,17 +30,13 @@ class MagixVirtualCar(context: Context, meta: Meta) : VirtualCar(context, meta)
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override suspend fun open() {
|
||||
super.open()
|
||||
override suspend fun onStart() {
|
||||
|
||||
val magixEndpoint = MagixEndpoint.rSocketWithWebSockets(
|
||||
meta["magixServerHost"].string ?: "localhost",
|
||||
)
|
||||
|
||||
launch {
|
||||
magixEndpoint.launchMagixVirtualCarUpdate()
|
||||
}
|
||||
magixEndpoint.launchMagixVirtualCarUpdate()
|
||||
}
|
||||
|
||||
companion object : Factory<MagixVirtualCar> {
|
||||
|
@ -4,8 +4,8 @@ package space.kscience.controls.demo.car
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import space.kscience.controls.manager.clock
|
||||
import space.kscience.controls.spec.DeviceBySpec
|
||||
import space.kscience.controls.spec.doRecurring
|
||||
import space.kscience.controls.spec.read
|
||||
@ -17,6 +17,8 @@ import space.kscience.dataforge.meta.double
|
||||
import space.kscience.dataforge.meta.get
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import kotlin.math.pow
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.ExperimentalTime
|
||||
@ -28,7 +30,10 @@ data class Vector2D(var x: Double = 0.0, var y: Double = 0.0) : MetaRepr {
|
||||
operator fun div(arg: Double): Vector2D = Vector2D(x / arg, y / arg)
|
||||
|
||||
companion object CoordinatesMetaConverter : MetaConverter<Vector2D> {
|
||||
override fun metaToObject(meta: Meta): Vector2D = Vector2D(
|
||||
|
||||
override val type: KType = typeOf<Vector2D>()
|
||||
|
||||
override fun metaToObjectOrNull(meta: Meta): Vector2D = Vector2D(
|
||||
meta["x"].double ?: 0.0,
|
||||
meta["y"].double ?: 0.0
|
||||
)
|
||||
@ -40,7 +45,10 @@ data class Vector2D(var x: Double = 0.0, var y: Double = 0.0) : MetaRepr {
|
||||
}
|
||||
}
|
||||
|
||||
open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(IVirtualCar, context, meta), IVirtualCar {
|
||||
open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(IVirtualCar, context, meta),
|
||||
IVirtualCar {
|
||||
private val clock = context.clock
|
||||
|
||||
private val timeScale = 1e-3
|
||||
|
||||
private val mass by meta.double(1000.0) // mass in kilograms
|
||||
@ -57,7 +65,7 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(I
|
||||
|
||||
private var timeState: Instant? = null
|
||||
|
||||
private fun update(newTime: Instant = Clock.System.now()) {
|
||||
private fun update(newTime: Instant = clock.now()) {
|
||||
//initialize time if it is not initialized
|
||||
if (timeState == null) {
|
||||
timeState = newTime
|
||||
@ -100,10 +108,9 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(I
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override suspend fun open() {
|
||||
super<DeviceBySpec>.open()
|
||||
override suspend fun onStart() {
|
||||
//initializing the clock
|
||||
timeState = Clock.System.now()
|
||||
timeState = clock.now()
|
||||
//starting regular updates
|
||||
doRecurring(100.milliseconds) {
|
||||
update()
|
||||
|
@ -71,9 +71,9 @@ class VirtualCarController : Controller(), ContextAware {
|
||||
logger.info { "Shutting down..." }
|
||||
magixServer?.stop(1000, 5000)
|
||||
logger.info { "Magix server stopped" }
|
||||
magixVirtualCar?.close()
|
||||
magixVirtualCar?.stop()
|
||||
logger.info { "Magix virtual car server stopped" }
|
||||
virtualCar?.close()
|
||||
virtualCar?.stop()
|
||||
logger.info { "Virtual car server stopped" }
|
||||
context.close()
|
||||
}
|
||||
|
51
demo/constructor/build.gradle.kts
Normal file
51
demo/constructor/build.gradle.kts
Normal file
@ -0,0 +1,51 @@
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
|
||||
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
id("org.jetbrains.compose") version "1.5.11"
|
||||
}
|
||||
|
||||
kscience {
|
||||
jvm {
|
||||
withJava()
|
||||
}
|
||||
useKtor()
|
||||
useContextReceivers()
|
||||
dependencies {
|
||||
api(projects.controlsVision)
|
||||
}
|
||||
jvmMain {
|
||||
implementation("io.ktor:ktor-server-cio")
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
jvmMain {
|
||||
dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//application {
|
||||
// mainClass.set("space.kscience.controls.demo.constructor.MainKt")
|
||||
//}
|
||||
|
||||
kotlin.explicitApi = ExplicitApiMode.Disabled
|
||||
|
||||
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "space.kscience.controls.demo.constructor.MainKt"
|
||||
|
||||
nativeDistributions {
|
||||
targetFormats(TargetFormat.Exe)
|
||||
packageName = "PidConstructor"
|
||||
packageVersion = "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
197
demo/constructor/src/jvmMain/kotlin/main.kt
Normal file
197
demo/constructor/src/jvmMain/kotlin/main.kt
Normal file
@ -0,0 +1,197 @@
|
||||
package space.kscience.controls.demo.constructor
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import kotlinx.coroutines.launch
|
||||
import space.kscience.controls.constructor.*
|
||||
import space.kscience.controls.manager.ClockManager
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
import space.kscience.controls.manager.clock
|
||||
import space.kscience.controls.spec.doRecurring
|
||||
import space.kscience.controls.spec.name
|
||||
import space.kscience.controls.vision.plot
|
||||
import space.kscience.controls.vision.plotDeviceProperty
|
||||
import space.kscience.controls.vision.plotNumberState
|
||||
import space.kscience.controls.vision.showDashboard
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.request
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.plotly.models.ScatterMode
|
||||
import space.kscience.visionforge.plotly.PlotlyPlugin
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
|
||||
class LinearDrive(
|
||||
context: Context,
|
||||
state: DoubleRangeState,
|
||||
mass: Double,
|
||||
pidParameters: PidParameters,
|
||||
meta: Meta = Meta.EMPTY,
|
||||
) : DeviceConstructor(context.request(DeviceManager), meta) {
|
||||
|
||||
val drive by device(VirtualDrive.factory(mass, state))
|
||||
val pid by device(PidRegulator(drive, pidParameters))
|
||||
|
||||
val start by device(LimitSwitch.factory(state.atStartState))
|
||||
val end by device(LimitSwitch.factory(state.atEndState))
|
||||
|
||||
|
||||
val position by property(state)
|
||||
var target by mutableProperty(pid.mutablePropertyAsState(Regulator.target, 0.0))
|
||||
}
|
||||
|
||||
|
||||
private fun Context.launchPidDevice(
|
||||
state: DoubleRangeState,
|
||||
pidParameters: PidParameters,
|
||||
mass: Double,
|
||||
) = launch {
|
||||
val device = install(
|
||||
"device",
|
||||
LinearDrive(this@launchPidDevice, state, mass, pidParameters)
|
||||
).apply {
|
||||
val clock = context.clock
|
||||
val clockStart = clock.now()
|
||||
doRecurring(10.milliseconds) {
|
||||
val timeFromStart = clock.now() - clockStart
|
||||
val t = timeFromStart.toDouble(DurationUnit.SECONDS)
|
||||
val freq = 0.1
|
||||
|
||||
target = 5 * sin(2.0 * PI * freq * t) +
|
||||
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val maxAge = 10.seconds
|
||||
|
||||
showDashboard {
|
||||
plot {
|
||||
plotNumberState(context, state, maxAge = maxAge) {
|
||||
name = "real position"
|
||||
}
|
||||
plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge) {
|
||||
name = "read position"
|
||||
}
|
||||
|
||||
plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge) {
|
||||
name = "target"
|
||||
}
|
||||
}
|
||||
|
||||
plot {
|
||||
plotDeviceProperty(device.start, LimitSwitch.locked.name, maxAge = maxAge) {
|
||||
name = "start measured"
|
||||
mode = ScatterMode.markers
|
||||
}
|
||||
plotDeviceProperty(device.end, LimitSwitch.locked.name, maxAge = maxAge) {
|
||||
name = "end measured"
|
||||
mode = ScatterMode.markers
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = application {
|
||||
val context = Context {
|
||||
plugin(DeviceManager)
|
||||
plugin(PlotlyPlugin)
|
||||
plugin(ClockManager)
|
||||
}
|
||||
|
||||
class MutablePidParameters(
|
||||
kp: Double,
|
||||
ki: Double,
|
||||
kd: Double,
|
||||
timeStep: Duration,
|
||||
) : PidParameters {
|
||||
override var kp by mutableStateOf(kp)
|
||||
override var ki by mutableStateOf(ki)
|
||||
override var kd by mutableStateOf(kd)
|
||||
override var timeStep by mutableStateOf(timeStep)
|
||||
}
|
||||
|
||||
val pidParameters = remember {
|
||||
MutablePidParameters(
|
||||
kp = 2.5,
|
||||
ki = 0.0,
|
||||
kd = -0.1,
|
||||
timeStep = 0.005.seconds
|
||||
)
|
||||
}
|
||||
|
||||
context.launchPidDevice(
|
||||
DoubleRangeState(0.0, -6.0..6.0),
|
||||
pidParameters,
|
||||
mass = 0.05
|
||||
)
|
||||
|
||||
Window(onCloseRequest = ::exitApplication) {
|
||||
MaterialTheme {
|
||||
Column {
|
||||
Row {
|
||||
Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||
TextField(pidParameters.kp.toString(),{pidParameters.kp = it.toDouble()}, enabled = false)
|
||||
Slider(
|
||||
pidParameters.kp.toFloat(),
|
||||
{ pidParameters.kp = it.toDouble()},
|
||||
valueRange = 0f..10f,
|
||||
steps = 100
|
||||
)
|
||||
}
|
||||
Row {
|
||||
Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||
TextField(pidParameters.ki.toString(),{pidParameters.ki = it.toDouble()}, enabled = false)
|
||||
|
||||
Slider(
|
||||
pidParameters.ki.toFloat(),
|
||||
{ pidParameters.ki = it.toDouble()},
|
||||
valueRange = -5f..5f,
|
||||
steps = 100
|
||||
)
|
||||
}
|
||||
Row {
|
||||
Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||
TextField(pidParameters.kd.toString(),{pidParameters.kd = it.toDouble()}, enabled = false)
|
||||
|
||||
Slider(
|
||||
pidParameters.kd.toFloat(),
|
||||
{ pidParameters.kd = it.toDouble()},
|
||||
valueRange = -5f..5f,
|
||||
steps = 100
|
||||
)
|
||||
}
|
||||
Row {
|
||||
Button({
|
||||
pidParameters.run {
|
||||
kp = 2.5
|
||||
ki = 0.0
|
||||
kd = -0.1
|
||||
timeStep = 0.005.seconds
|
||||
}
|
||||
}){
|
||||
Text("Reset")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
demo/constructor/src/jvmMain/resources/logback.xml
Normal file
11
demo/constructor/src/jvmMain/resources/logback.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
@ -23,10 +23,7 @@ import space.kscience.magix.rsocket.rSocketWithTcp
|
||||
import space.kscience.magix.rsocket.rSocketWithWebSockets
|
||||
import space.kscience.magix.server.RSocketMagixFlowPlugin
|
||||
import space.kscience.magix.server.startMagixServer
|
||||
import space.kscience.plotly.Plotly
|
||||
import space.kscience.plotly.bar
|
||||
import space.kscience.plotly.layout
|
||||
import space.kscience.plotly.plot
|
||||
import space.kscience.plotly.*
|
||||
import space.kscience.plotly.server.PlotlyUpdateMode
|
||||
import space.kscience.plotly.server.serve
|
||||
import space.kscience.plotly.server.show
|
||||
@ -88,9 +85,10 @@ suspend fun main() {
|
||||
updateMode = PlotlyUpdateMode.PUSH
|
||||
updateInterval = 1000
|
||||
page { container ->
|
||||
plot(renderer = container) {
|
||||
plot(renderer = container, config = PlotlyConfig { saveAsSvg() }) {
|
||||
layout {
|
||||
title = "Latest event"
|
||||
// title = "Latest event"
|
||||
|
||||
xaxis.title = "Device number"
|
||||
yaxis.title = "Maximum latency in ms"
|
||||
}
|
||||
@ -119,7 +117,7 @@ suspend fun main() {
|
||||
latest.clear()
|
||||
max.clear()
|
||||
x.numbers = sorted.keys
|
||||
y.numbers = sorted.values.map { it.inWholeMilliseconds / 1000.0 + 0.0001 }
|
||||
y.numbers = sorted.values.map { it.inWholeMicroseconds / 1000.0 + 0.0001 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,16 +49,16 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
|
||||
if (powerOnValue) {
|
||||
val ans = talk("FP!ON")
|
||||
if (ans == "ON") {
|
||||
updateLogical(powerOn, true)
|
||||
propertyChanged(powerOn, true)
|
||||
} else {
|
||||
updateLogical(error, "Failed to set power state")
|
||||
propertyChanged(error, "Failed to set power state")
|
||||
}
|
||||
} else {
|
||||
val ans = talk("FP!OFF")
|
||||
if (ans == "OFF") {
|
||||
updateLogical(powerOn, false)
|
||||
propertyChanged(powerOn, false)
|
||||
} else {
|
||||
updateLogical(error, "Failed to set power state")
|
||||
propertyChanged(error, "Failed to set power state")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -68,13 +68,13 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
|
||||
invalidate(error)
|
||||
return if (answer.isNullOrEmpty()) {
|
||||
// updateState(PortSensor.CONNECTED_STATE, false)
|
||||
updateLogical(error, "No connection")
|
||||
propertyChanged(error, "No connection")
|
||||
null
|
||||
} else {
|
||||
val res = answer.toDouble()
|
||||
if (res <= 0) {
|
||||
updateLogical(powerOn, false)
|
||||
updateLogical(error, "No power")
|
||||
propertyChanged(powerOn, false)
|
||||
propertyChanged(error, "No power")
|
||||
null
|
||||
} else {
|
||||
res
|
||||
@ -89,12 +89,12 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
|
||||
|
||||
override fun build(context: Context, meta: Meta): MksPdr900Device = MksPdr900Device(context, meta)
|
||||
|
||||
val powerOn by booleanProperty(read = MksPdr900Device::readPowerOn, write = MksPdr900Device::writePowerOn)
|
||||
val powerOn by booleanProperty(read = { readPowerOn() }, write = { _, value -> writePowerOn(value) })
|
||||
|
||||
val channel by logicalProperty(MetaConverter.int)
|
||||
|
||||
val value by doubleProperty(read = {
|
||||
readChannelData(get(channel) ?: DEFAULT_CHANNEL)
|
||||
readChannelData(request(channel) ?: DEFAULT_CHANNEL)
|
||||
})
|
||||
|
||||
val error by logicalProperty(MetaConverter.string)
|
||||
|
@ -1,10 +0,0 @@
|
||||
package center.sciprog.devices.mks
|
||||
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.string
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
|
||||
object NullableStringMetaConverter : MetaConverter<String?> {
|
||||
override fun metaToObject(meta: Meta): String? = meta.string
|
||||
override fun objectToMeta(obj: String?): Meta = Meta {}
|
||||
}
|
@ -138,7 +138,7 @@ class PiMotionMasterDevice(
|
||||
override fun build(context: Context, meta: Meta): PiMotionMasterDevice = PiMotionMasterDevice(context)
|
||||
|
||||
val connected by booleanProperty(descriptorBuilder = {
|
||||
info = "True if the connection address is defined and the device is initialized"
|
||||
description = "True if the connection address is defined and the device is initialized"
|
||||
}) {
|
||||
port != null
|
||||
}
|
||||
@ -157,13 +157,13 @@ class PiMotionMasterDevice(
|
||||
}
|
||||
|
||||
val stop by unitAction({
|
||||
info = "Stop all axis"
|
||||
description = "Stop all axis"
|
||||
}) {
|
||||
send("STP")
|
||||
}
|
||||
|
||||
val connect by action(MetaConverter.meta, MetaConverter.unit, descriptorBuilder = {
|
||||
info = "Connect to specific port and initialize axis"
|
||||
description = "Connect to specific port and initialize axis"
|
||||
}) { portSpec ->
|
||||
//Clear current actions if present
|
||||
if (port != null) {
|
||||
@ -172,7 +172,7 @@ class PiMotionMasterDevice(
|
||||
//Update port
|
||||
//address = portSpec.node
|
||||
port = portFactory(portSpec, context)
|
||||
updateLogical(connected, true)
|
||||
propertyChanged(connected, true)
|
||||
// connector.open()
|
||||
//Initialize axes
|
||||
val idn = read(identity)
|
||||
@ -189,19 +189,19 @@ class PiMotionMasterDevice(
|
||||
}
|
||||
|
||||
val disconnect by unitAction({
|
||||
info = "Disconnect the program from the device if it is connected"
|
||||
description = "Disconnect the program from the device if it is connected"
|
||||
}) {
|
||||
port?.let {
|
||||
execute(stop)
|
||||
it.close()
|
||||
}
|
||||
port = null
|
||||
updateLogical(connected, false)
|
||||
propertyChanged(connected, false)
|
||||
}
|
||||
|
||||
|
||||
val timeout by mutableProperty(MetaConverter.duration, PiMotionMasterDevice::timeoutValue) {
|
||||
info = "Timeout"
|
||||
description = "Timeout"
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,8 +245,8 @@ class PiMotionMasterDevice(
|
||||
read = {
|
||||
readAxisBoolean("$command?")
|
||||
},
|
||||
write = {
|
||||
writeAxisBoolean(command, it)
|
||||
write = { _, value ->
|
||||
writeAxisBoolean(command, value)
|
||||
},
|
||||
descriptorBuilder = descriptorBuilder
|
||||
)
|
||||
@ -259,7 +259,7 @@ class PiMotionMasterDevice(
|
||||
mm.requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed $command response. Should include float value for $axisId")
|
||||
},
|
||||
write = { newValue ->
|
||||
write = { _, newValue ->
|
||||
mm.send(command, axisId, newValue.toString())
|
||||
mm.failIfError()
|
||||
},
|
||||
@ -267,7 +267,7 @@ class PiMotionMasterDevice(
|
||||
)
|
||||
|
||||
val enabled by axisBooleanProperty("EAX") {
|
||||
info = "Motor enable state."
|
||||
description = "Motor enable state."
|
||||
}
|
||||
|
||||
val halt by unitAction {
|
||||
@ -275,20 +275,20 @@ class PiMotionMasterDevice(
|
||||
}
|
||||
|
||||
val targetPosition by axisNumberProperty("MOV") {
|
||||
info = """
|
||||
description = """
|
||||
Sets a new absolute target position for the specified axis.
|
||||
Servo mode must be switched on for the commanded axis prior to using this command (closed-loop operation).
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
val onTarget by booleanProperty({
|
||||
info = "Queries the on-target state of the specified axis."
|
||||
description = "Queries the on-target state of the specified axis."
|
||||
}) {
|
||||
readAxisBoolean("ONT?")
|
||||
}
|
||||
|
||||
val reference by booleanProperty({
|
||||
info = "Get Referencing Result"
|
||||
description = "Get Referencing Result"
|
||||
}) {
|
||||
readAxisBoolean("FRF?")
|
||||
}
|
||||
@ -298,36 +298,36 @@ class PiMotionMasterDevice(
|
||||
}
|
||||
|
||||
val minPosition by doubleProperty({
|
||||
info = "Minimal position value for the axis"
|
||||
description = "Minimal position value for the axis"
|
||||
}) {
|
||||
mm.requestAndParse("TMN?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed `TMN?` response. Should include float value for $axisId")
|
||||
}
|
||||
|
||||
val maxPosition by doubleProperty({
|
||||
info = "Maximal position value for the axis"
|
||||
description = "Maximal position value for the axis"
|
||||
}) {
|
||||
mm.requestAndParse("TMX?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed `TMX?` response. Should include float value for $axisId")
|
||||
}
|
||||
|
||||
val position by doubleProperty({
|
||||
info = "The current axis position."
|
||||
description = "The current axis position."
|
||||
}) {
|
||||
mm.requestAndParse("POS?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed `POS?` response. Should include float value for $axisId")
|
||||
}
|
||||
|
||||
val openLoopTarget by axisNumberProperty("OMA") {
|
||||
info = "Position for open-loop operation."
|
||||
description = "Position for open-loop operation."
|
||||
}
|
||||
|
||||
val closedLoop by axisBooleanProperty("SVO") {
|
||||
info = "Servo closed loop mode"
|
||||
description = "Servo closed loop mode"
|
||||
}
|
||||
|
||||
val velocity by axisNumberProperty("VEL") {
|
||||
info = "Velocity value for closed-loop operation"
|
||||
description = "Velocity value for closed-loop operation"
|
||||
}
|
||||
|
||||
val move by action(MetaConverter.meta, MetaConverter.unit) {
|
||||
|
@ -32,7 +32,7 @@ fun <D : Device, T : Any> D.fxProperty(
|
||||
}
|
||||
}
|
||||
|
||||
fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>): Property<T> =
|
||||
fun <D : Device, T : Any> D.fxProperty(spec: MutableDevicePropertySpec<D, T>): Property<T> =
|
||||
object : ObjectPropertyBase<T>() {
|
||||
override fun getBean(): Any = this
|
||||
override fun getName(): String = spec.name
|
||||
@ -51,7 +51,7 @@ fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>):
|
||||
|
||||
onChange { newValue ->
|
||||
if (newValue != null) {
|
||||
set(spec, newValue)
|
||||
writeAsync(spec, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
195
demo/notebooks/constructor.ipynb
Normal file
195
demo/notebooks/constructor.ipynb
Normal file
@ -0,0 +1,195 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"//import space.kscience.controls.jupyter.ControlsJupyter\n",
|
||||
"\n",
|
||||
"//USE(ControlsJupyter())\n",
|
||||
"USE{\n",
|
||||
" repositories{\n",
|
||||
" maven(\"https://repo.kotlin.link\")\n",
|
||||
" }\n",
|
||||
" dependencies{\n",
|
||||
" implementation(\"space.kscience:controls-jupyter-jvm:0.3.0-dev-2\")\n",
|
||||
" }\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class LinearDrive(\n",
|
||||
" context: Context,\n",
|
||||
" state: DoubleRangeState,\n",
|
||||
" mass: Double,\n",
|
||||
" pidParameters: PidParameters,\n",
|
||||
" meta: Meta = Meta.EMPTY,\n",
|
||||
") : DeviceConstructor(context.request(DeviceManager), meta) {\n",
|
||||
"\n",
|
||||
" val drive by device(VirtualDrive.factory(mass, state))\n",
|
||||
" val pid by device(PidRegulator(drive, pidParameters))\n",
|
||||
"\n",
|
||||
" val start by device(LimitSwitch.factory(state.atStartState))\n",
|
||||
" val end by device(LimitSwitch.factory(state.atEndState))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
" val position by property(state)\n",
|
||||
" var target by mutableProperty(pid.mutablePropertyAsState(Regulator.target, 0.0))\n",
|
||||
"}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import kotlin.time.Duration.Companion.milliseconds\n",
|
||||
"import kotlin.time.Duration.Companion.seconds\n",
|
||||
"\n",
|
||||
"val state = DoubleRangeState(0.0, -5.0..5.0)\n",
|
||||
"\n",
|
||||
"val pidParameters = PidParameters(\n",
|
||||
" kp = 2.5,\n",
|
||||
" ki = 0.0,\n",
|
||||
" kd = -0.1,\n",
|
||||
" timeStep = 0.005.seconds\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"val device = context.install(\"device\", LinearDrive(context, state, 0.005, pidParameters))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"\n",
|
||||
"val job = device.run {\n",
|
||||
" val clock = context.clock\n",
|
||||
" val clockStart = clock.now()\n",
|
||||
" doRecurring(10.milliseconds) {\n",
|
||||
" val timeFromStart = clock.now() - clockStart\n",
|
||||
" val t = timeFromStart.toDouble(DurationUnit.SECONDS)\n",
|
||||
" val freq = 0.1\n",
|
||||
"\n",
|
||||
" target = 5 * sin(2.0 * PI * freq * t) +\n",
|
||||
" sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))\n",
|
||||
" }\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"val maxAge = 10.seconds\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"VisionForge.fragment {\n",
|
||||
" vision {\n",
|
||||
" plotly {\n",
|
||||
" \n",
|
||||
" plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge) {\n",
|
||||
" name = \"target\"\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" plotNumberState(context, state, maxAge = maxAge) {\n",
|
||||
" name = \"real position\"\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge) {\n",
|
||||
" name = \"read position\"\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" vision {\n",
|
||||
" plotly {\n",
|
||||
" plotDeviceProperty(device.start, LimitSwitch.locked.name, maxAge = maxAge) {\n",
|
||||
" name = \"start measured\"\n",
|
||||
" mode = ScatterMode.markers\n",
|
||||
" }\n",
|
||||
" plotDeviceProperty(device.end, LimitSwitch.locked.name, maxAge = maxAge) {\n",
|
||||
" name = \"end measured\"\n",
|
||||
" mode = ScatterMode.markers\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import kotlinx.coroutines.cancel\n",
|
||||
"\n",
|
||||
"job.cancel()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"outputs": [],
|
||||
"source": [],
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Kotlin",
|
||||
"language": "kotlin",
|
||||
"name": "kotlin"
|
||||
},
|
||||
"ktnbPluginMetadata": {
|
||||
"projectDependencies": [
|
||||
"controls-kt.controls-jupyter.jvmMain"
|
||||
]
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": "text/x-kotlin",
|
||||
"file_extension": ".kt",
|
||||
"mimetype": "text/x-kotlin",
|
||||
"name": "kotlin",
|
||||
"nbconvert_exporter": "",
|
||||
"pygments_lexer": "kotlin",
|
||||
"version": "1.8.20"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
2
docs/templates/README-TEMPLATE.md
vendored
2
docs/templates/README-TEMPLATE.md
vendored
@ -1,5 +1,7 @@
|
||||
[![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 (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
|
||||
|
@ -4,10 +4,7 @@ kotlin.native.ignoreDisabledTargets=true
|
||||
|
||||
org.gradle.parallel=true
|
||||
|
||||
publishing.github=false
|
||||
publishing.sonatype=false
|
||||
|
||||
org.gradle.configureondemand=true
|
||||
org.gradle.jvmargs=-Xmx4096m
|
||||
|
||||
toolsVersion=0.14.10-kotlin-1.9.0
|
||||
toolsVersion=0.15.2-kotlin-1.9.21
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
4
magix/README.md
Normal file
4
magix/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Module magix
|
||||
|
||||
|
||||
|
@ -17,6 +17,10 @@ kscience {
|
||||
useSerialization{
|
||||
json()
|
||||
}
|
||||
|
||||
commonMain{
|
||||
implementation(spclibs.atomicfu)
|
||||
}
|
||||
}
|
||||
|
||||
readme{
|
||||
|
@ -12,7 +12,7 @@ description = """
|
||||
dependencies {
|
||||
api(projects.magix.magixApi)
|
||||
api("org.slf4j:slf4j-api:2.0.6")
|
||||
api("org.zeromq:jeromq:0.5.2")
|
||||
api("org.zeromq:jeromq:0.5.3")
|
||||
}
|
||||
|
||||
readme {
|
||||
|
@ -50,6 +50,9 @@ include(
|
||||
// ":controls-mongo",
|
||||
":controls-storage",
|
||||
":controls-storage:controls-xodus",
|
||||
":controls-constructor",
|
||||
":controls-vision",
|
||||
":controls-jupyter",
|
||||
":magix",
|
||||
":magix:magix-api",
|
||||
":magix:magix-server",
|
||||
@ -67,5 +70,6 @@ include(
|
||||
":demo:car",
|
||||
":demo:motors",
|
||||
":demo:echo",
|
||||
":demo:mks-pdr900"
|
||||
":demo:mks-pdr900",
|
||||
":demo:constructor"
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user