Compare commits

...

44 Commits

Author SHA1 Message Date
a12cf440e8 Finish migration to kotlinx-io 2023-12-13 20:20:03 +03:00
606c2cf5b1 Finish migration to kotlinx-io 2023-12-13 14:50:56 +03:00
fb03fcc982 Finish migration to kotlinx-io 2023-12-13 12:29:06 +03:00
cf129b6242 Migrate to DF 0.7 2023-12-12 09:59:52 +03:00
81d6b672cf Add compose controls to pid simulator 2023-11-22 21:55:13 +03:00
07cc41c645 Automatic description generation for spec properties (JVM only) 2023-11-18 19:02:56 +03:00
0c647cff30 DeviceSpec properties no explicitly pass property name to getters and setters 2023-11-18 15:39:56 +03:00
b539c2046a DeviceSpec properties no explicitly pass property name to getters and setters 2023-11-18 14:49:23 +03:00
afee2f0a02 minor update to constructor 2023-11-17 12:22:06 +03:00
fb8ee59f14 replace debounce by sample 2023-11-08 22:33:49 +03:00
74301afb42 Return notifications about pid and drive updates. Introduce debounce 2023-11-08 22:28:26 +03:00
fe98a836f8 Update jupyter integration 2023-11-08 21:01:42 +03:00
0c128bce36 Merge remote-tracking branch 'spc/dev' into dev
# Conflicts:
#	demo/constructor/src/jvmMain/kotlin/main.kt
2023-11-08 15:31:55 +03:00
4e17c9051c Update jupyter integration 2023-11-08 15:31:12 +03:00
0f687c3c51 Update jupyter integration 2023-11-08 11:52:57 +03:00
53fc240c75 Test device constructor 2023-11-07 08:46:56 +03:00
825f1a4d04 Add DeviceConstructor 2023-11-06 16:46:16 +03:00
0443fdc3c0 Add fixed age plots for properties and states. 2023-11-06 11:39:56 +03:00
78b18ebda6 Move server to controls-vision 2023-11-05 10:18:26 +03:00
0e963a7b13 Simplify UI management in constructor 2023-11-05 09:47:58 +03:00
2698cee80b Remove automatic reads from virtual drive and pid 2023-11-02 15:36:10 +03:00
811477a636 add limit readers 2023-10-30 22:51:17 +03:00
984e7f12ef Add JVM application for constructor demo 2023-10-30 21:47:41 +03:00
1414cf5a2f implement constructor 2023-10-30 21:35:46 +03:00
1fcdbdc9f4 Update constructor 2023-10-28 14:18:00 +03:00
4f028ccee8 Lifecycle change 2023-10-27 10:57:46 +03:00
1619fdadf2 Refactoring. Developing composer 2023-10-25 22:31:36 +03:00
7f71d0c9e9 modbus registry to json rendering 2023-10-20 10:14:14 +03:00
290010fc8c Add writeable flag to mutable properties 2023-10-19 16:38:50 +03:00
80cc62e25b Merge remote-tracking branch 'spc/dev' into dev
# Conflicts:
#	demo/all-things/build.gradle.kts
2023-10-19 16:21:19 +03:00
f1b63c3951 Add buffer to device messages 2023-10-07 18:34:44 +03:00
01606af307 clientId -> unitId 2023-10-05 07:43:49 +03:00
2cc0a5bcbc Fixex in modbus and write-protection for same meta 2023-10-02 22:12:11 +03:00
efe9a2e842 Fixex in modbus and write-protection for same meta 2023-10-02 21:24:01 +03:00
34e7dd2c6d Add read-after-write for DeviceBase property writers 2023-09-24 13:29:15 +03:00
a337daee93 Add read-after-write for DeviceBase property writers 2023-09-24 13:21:01 +03:00
a51510606f add customizeable scopes to property listeners 2023-09-24 13:02:52 +03:00
aef94767c5 Fix all-things demo 2023-09-18 13:38:45 +03:00
8b6a6abd92 Update to PiPlugin logic 2023-09-18 09:00:04 +03:00
bc5037b256 fix dataforge version 2023-09-16 16:09:47 +03:00
036bef1adb fix dataforge version 2023-09-16 15:54:36 +03:00
cc36ef805b update version 2023-09-04 14:56:18 +03:00
0f610a5e19 Fix mass demo plot 2023-08-24 16:25:17 +03:00
4c93b5c9b3 Update readme 2023-08-23 16:37:35 +03:00
92 changed files with 2889 additions and 568 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,10 +20,14 @@ kscience {
json()
}
useContextReceivers()
dependencies {
commonMain {
api("space.kscience:dataforge-io:$dataforgeVersion")
api(spclibs.kotlinx.datetime)
}
jvmTest{
implementation(spclibs.logback.classic)
}
}

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package space.kscience.controls.api
import io.ktor.utils.io.core.Closeable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.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
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ description = """
dependencies {
api(projects.controlsCore)
api("com.ghgande:j2mod:3.1.1")
api("com.ghgande:j2mod:3.2.0")
}
readme{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,8 +63,7 @@ public open class OpcUaDeviceBySpec<D : Device>(
}
}
override fun close() {
override fun onStop() {
client.disconnect()
super<DeviceBySpec>.close()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

@ -17,6 +17,10 @@ kscience {
useSerialization{
json()
}
commonMain{
implementation(spclibs.atomicfu)
}
}
readme{

View File

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

View File

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