Dev #6

Merged
altavir merged 75 commits from dev into master 2021-10-23 11:02:48 +03:00
118 changed files with 6391 additions and 1502 deletions
.gitignoreREADME.mdbuild.gradle.kts
controls-core
controls-magix-client
build.gradle.kts
src/commonMain/kotlin/ru/mipt/npm/controls/client
controls-opcua
controls-serial
build.gradle.kts
src/main/kotlin/ru/mipt/npm/controls/serial
controls-server
build.gradle.kts
src/main/kotlin/ru/mipt/npm/controls/server
controls-tcp
build.gradle.kts
src/jvmMain/kotlin/ru/mipt/npm/controls/ports
dataforge-device-client
dataforge-device-core
dataforge-device-server
build.gradle.kts
src/main/kotlin/hep/dataforge/control/server
demo
docs
gradle/wrapper
gradlewgradlew.bat
magix
build.gradle.kts
magix-api
magix-demo
build.gradle.kts
src/main/kotlin
magix-java-client
build.gradle.kts
src/main
java/ru/mipt/npm/magix/client
kotlin/ru/mipt/npm/magix/client

4
.gitignore vendored

@ -1,7 +1,11 @@
# Created by .ignore support plugin (hsz.mobi)
.idea/
.gradle
*.iws
*.iml
*.ipr
out/
build/
!gradle-wrapper.jar

@ -2,13 +2,12 @@
# Controls.kt
Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is
based on DataForge, a software framework for automated data processing.
Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
This repository contains a prototype of API and simple implementation
of a slow control system, including a demo.
Controls.kt uses some concepts and modules of DataForge,
such as `Meta` (immutable tree-like structure) and `MetaItem` (which
such as `Meta` (immutable tree-like structure) and `Meta` (which
includes a scalar value, or a tree of values, easily convertable to/from JSON
if needed).
@ -37,12 +36,12 @@ Among other things, you can:
### `dataforge-control-core` module packages
- `api` - defines API for device management. The main class here is
[`Device`](dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt).
[`Device`](controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt).
Generally, a Device has Properties that can be read and written. Also, some Actions
can optionally be applied on a device (may or may not affect properties).
- `base` - contains baseline `Device` implementation
[`DeviceBase`](dataforge-control-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt)
[`DeviceBase`](controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt)
and property implementation, including property asynchronous flows.
- `controllers` - implements Message Controller that can be attached to the event bus, Message

@ -1,18 +1,21 @@
val dataforgeVersion by extra("0.1.8")
val plotlyVersion by extra("0.2.0-dev-12")
allprojects {
repositories {
mavenLocal()
maven("https://dl.bintray.com/pdvrieze/maven")
maven("http://maven.jzy3d.org/releases")
maven("https://kotlin.bintray.com/js-externals")
}
group = "hep.dataforge"
version = "0.0.1"
plugins {
id("ru.mipt.npm.gradle.project")
}
val githubProject by extra("dataforge-control")
val bintrayRepo by extra("dataforge")
val dataforgeVersion: String by extra("0.5.1")
val ktorVersion: String by extra(ru.mipt.npm.gradle.KScienceVersions.ktorVersion)
val rsocketVersion by extra("0.13.1")
allprojects {
group = "ru.mipt.npm"
version = "0.1.1"
}
ksciencePublish {
github("controls.kt")
space()
}
apiValidation {
validationDisabled = true
}

@ -0,0 +1,24 @@
plugins {
id("ru.mipt.npm.gradle.mpp")
`maven-publish`
}
val dataforgeVersion: String by rootProject.extra
kscience {
useCoroutines("1.4.1")
useSerialization{
json()
}
}
kotlin {
sourceSets {
commonMain{
dependencies {
api("space.kscience:dataforge-io:$dataforgeVersion")
api(npm.kotlinx.datetime)
}
}
}
}

@ -0,0 +1,100 @@
package ru.mipt.npm.controls.api
import io.ktor.utils.io.core.Closeable
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 ru.mipt.npm.controls.api.Device.Companion.DEVICE_TARGET
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.names.Name
/**
* General interface describing a managed Device.
* Device is a supervisor scope encompassing all operations on a device. When canceled, cancels all running processes.
*/
@Type(DEVICE_TARGET)
public interface Device : Closeable, ContextAware, CoroutineScope {
/**
* List of supported property descriptors
*/
public val propertyDescriptors: Collection<PropertyDescriptor>
/**
* List of supported action descriptors. Action is a request to the device that
* may or may not change the properties
*/
public val actionDescriptors: Collection<ActionDescriptor>
/**
* Read physical state of property and update/push notifications if needed.
*/
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.
*/
public suspend fun writeProperty(propertyName: String, value: Meta)
/**
* A subscription-based [Flow] of [DeviceMessage] provided by device. The flow is guaranteed to be readable
* multiple times
*/
public val messageFlow: Flow<DeviceMessage>
/**
* Send an action request and suspend caller while request is being processed.
* Could return null if request does not return a meaningful answer.
*/
public suspend fun execute(action: String, argument: Meta? = null): Meta?
override fun close() {
cancel("The device is closed")
}
public companion object {
public const val DEVICE_TARGET: String = "device"
}
}
/**
* Get the logical state of property or suspend to read the physical value.
*/
public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
getProperty(propertyName) ?: readProperty(propertyName)
/**
* Get a snapshot of logical state of the device
*
* TODO currently this
*/
public fun Device.getProperties(): Meta = Meta {
for (descriptor in propertyDescriptors) {
setMeta(Name.parse(descriptor.name), 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)

@ -0,0 +1,75 @@
package ru.mipt.npm.controls.api
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.*
import space.kscience.dataforge.provider.Provider
/**
* A hub that could locate multiple devices and redirect actions to them
*/
public interface DeviceHub : Provider {
public val devices: Map<NameToken, Device>
override val defaultTarget: String get() = Device.DEVICE_TARGET
override val defaultChainTarget: String get() = Device.DEVICE_TARGET
override fun content(target: String): Map<Name, Any> {
if (target == Device.DEVICE_TARGET) {
return buildMap {
fun putAll(prefix: Name, hub: DeviceHub) {
hub.devices.forEach {
put(prefix + it.key, it.value)
}
}
devices.forEach {
val name = it.key.asName()
put(name, it.value)
(it.value as? DeviceHub)?.let { hub ->
putAll(name, hub)
}
}
}
} else {
throw IllegalArgumentException("Target $target is not supported for $this")
}
}
public companion object
}
public operator fun DeviceHub.get(nameToken: NameToken): Device =
devices[nameToken] ?: error("Device with name $nameToken not found in $this")
public fun DeviceHub.getOrNull(name: Name): Device? = when {
name.isEmpty() -> this as? Device
name.length == 1 -> get(name.firstOrNull()!!)
else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst())
}
public operator fun DeviceHub.get(name: Name): Device =
getOrNull(name) ?: error("Device with name $name not found in $this")
public fun DeviceHub.getOrNull(nameString: String): Device? = getOrNull(Name.parse(nameString))
public operator fun DeviceHub.get(nameString: String): Device =
getOrNull(nameString) ?: error("Device with name $nameString not found in $this")
public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta =
this[deviceName].readProperty(propertyName)
public suspend fun DeviceHub.writeProperty(deviceName: Name, propertyName: String, value: Meta) {
this[deviceName].writeProperty(propertyName, value)
}
public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: Meta?): Meta? =
this[deviceName].execute(command, argument)
//suspend fun DeviceHub.respond(request: Envelope): EnvelopeBuilder {
// val target = request.meta[DeviceMessage.TARGET_KEY].string ?: defaultTarget
// val device = this[target.toName()]
//
// return device.respond(device, target, request)
//}

@ -0,0 +1,222 @@
package ru.mipt.npm.controls.api
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
import space.kscience.dataforge.io.SimpleEnvelope
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.toJson
import space.kscience.dataforge.meta.toMeta
import space.kscience.dataforge.names.Name
@Serializable
public sealed class DeviceMessage {
public abstract val sourceDevice: Name?
public abstract val targetDevice: Name?
public abstract val comment: String?
public abstract val time: Instant?
/**
* Update the source device name for composition. If the original name is null, resulting name is also null.
*/
public abstract fun changeSource(block: (Name) -> Name): DeviceMessage
public companion object {
public fun error(
cause: Throwable,
sourceDevice: Name,
targetDevice: Name? = null,
): DeviceErrorMessage = DeviceErrorMessage(
errorMessage = cause.message,
errorType = cause::class.simpleName,
errorStackTrace = cause.stackTraceToString(),
sourceDevice = sourceDevice,
targetDevice = targetDevice
)
public fun fromMeta(meta: Meta): DeviceMessage = Json.decodeFromJsonElement(meta.toJson())
}
}
/**
* Notify that property is changed. [sourceDevice] is mandatory.
* [property] corresponds to property name.
*
*/
@Serializable
@SerialName("property.changed")
public data class PropertyChangedMessage(
public val property: String,
public val value: Meta,
override val sourceDevice: Name = Name.EMPTY,
override val targetDevice: Name? = null,
override val comment: String? = null,
override val time: Instant? = Clock.System.now()
) : DeviceMessage(){
override fun changeSource(block: (Name) -> Name):DeviceMessage = copy(sourceDevice = block(sourceDevice))
}
/**
* A command to set or invalidate property. [targetDevice] is mandatory.
*/
@Serializable
@SerialName("property.set")
public data class PropertySetMessage(
public val property: String,
public val value: Meta?,
override val sourceDevice: Name? = null,
override val targetDevice: Name,
override val comment: String? = null,
override val time: Instant? = Clock.System.now()
) : DeviceMessage(){
override fun changeSource(block: (Name) -> Name):DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* A command to request property value asynchronously. [targetDevice] is mandatory.
* The property value should be returned asynchronously via [PropertyChangedMessage].
*/
@Serializable
@SerialName("property.get")
public data class PropertyGetMessage(
public val property: String,
override val sourceDevice: Name? = null,
override val targetDevice: Name,
override val comment: String? = null,
override val time: Instant? = Clock.System.now()
) : DeviceMessage(){
override fun changeSource(block: (Name) -> Name):DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* Request device description. The result is returned in form of [DescriptionMessage]
*/
@Serializable
@SerialName("description.get")
public data class GetDescriptionMessage(
override val sourceDevice: Name? = null,
override val targetDevice: Name,
override val comment: String? = null,
override val time: Instant? = Clock.System.now()
) : DeviceMessage(){
override fun changeSource(block: (Name) -> Name):DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* The full device description message
*/
@Serializable
@SerialName("description")
public data class DescriptionMessage(
val description: Meta,
override val sourceDevice: Name,
override val targetDevice: Name? = null,
override val comment: String? = null,
override val time: Instant? = Clock.System.now()
) : DeviceMessage(){
override fun changeSource(block: (Name) -> Name):DeviceMessage = copy(sourceDevice = block(sourceDevice))
}
/**
* A request to execute an action. [targetDevice] is mandatory
*/
@Serializable
@SerialName("action.execute")
public data class ActionExecuteMessage(
public val action: String,
public val argument: Meta?,
override val sourceDevice: Name? = null,
override val targetDevice: Name,
override val comment: String? = null,
override val time: Instant? = Clock.System.now()
) : DeviceMessage(){
override fun changeSource(block: (Name) -> Name):DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* Asynchronous action result. [sourceDevice] is mandatory
*/
@Serializable
@SerialName("action.result")
public data class ActionResultMessage(
public val action: String,
public val result: Meta?,
override val sourceDevice: Name,
override val targetDevice: Name? = null,
override val comment: String? = null,
override val time: Instant? = Clock.System.now()
) : DeviceMessage(){
override fun changeSource(block: (Name) -> Name):DeviceMessage = copy(sourceDevice = block(sourceDevice))
}
/**
* Notifies listeners that a new binary with given [binaryID] is available. The binary itself could not be provided via [DeviceMessage] API.
*/
@Serializable
@SerialName("binary.notification")
public data class BinaryNotificationMessage(
val binaryID: String,
override val sourceDevice: Name,
override val targetDevice: Name? = null,
override val comment: String? = null,
override val time: Instant? = Clock.System.now()
) : DeviceMessage(){
override fun changeSource(block: (Name) -> Name):DeviceMessage = copy(sourceDevice = block(sourceDevice))
}
/**
* The message states that the message is received, but no meaningful response is produced.
* This message could be used for a heartbeat.
*/
@Serializable
@SerialName("empty")
public data class EmptyDeviceMessage(
override val sourceDevice: Name? = null,
override val targetDevice: Name? = null,
override val comment: String? = null,
override val time: Instant? = Clock.System.now()
) : DeviceMessage(){
override fun changeSource(block: (Name) -> Name):DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* Information log message
*/
@Serializable
@SerialName("log")
public data class DeviceLogMessage(
val message: String,
val data: Meta? = null,
override val sourceDevice: Name? = null,
override val targetDevice: Name? = null,
override val comment: String? = null,
override val time: Instant? = Clock.System.now()
) : DeviceMessage(){
override fun changeSource(block: (Name) -> Name):DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* The evaluation of the message produced a service error
*/
@Serializable
@SerialName("error")
public data class DeviceErrorMessage(
public val errorMessage: String?,
public val errorType: String? = null,
public val errorStackTrace: String? = null,
override val sourceDevice: Name,
override val targetDevice: Name? = null,
override val comment: String? = null,
override val time: Instant? = Clock.System.now()
) : DeviceMessage(){
override fun changeSource(block: (Name) -> Name):DeviceMessage = copy(sourceDevice = block(sourceDevice))
}
public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta()
public fun DeviceMessage.toEnvelope(): SimpleEnvelope = SimpleEnvelope(toMeta(), null)

@ -0,0 +1,34 @@
package ru.mipt.npm.controls.api
import io.ktor.utils.io.core.Closeable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/**
* A generic bi-directional sender/receiver object
*/
public interface Socket<T> : Closeable {
/**
* Send an object to the socket
*/
public suspend fun send(data: T)
/**
* Flow of objects received from socket
*/
public fun receiving(): Flow<T>
public fun isOpen(): Boolean
}
/**
* Connect an input to this socket using designated [scope] for it and return a handler [Job].
* Multiple inputs could be connected to the same [Socket].
*/
public fun <T> Socket<T>.connectInput(scope: CoroutineScope, flow: Flow<T>): Job = scope.launch {
flow.collect { send(it) }
}

@ -0,0 +1,32 @@
package ru.mipt.npm.controls.api
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder
//TODO add proper builders
/**
* A descriptor for property
*/
@Serializable
public class PropertyDescriptor(
public val name: String,
public var info: String? = null,
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
public var readable: Boolean = true,
public var writable: Boolean = false
)
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){
metaDescriptor = MetaDescriptor(block)
}
/**
* A descriptor for property
*/
@Serializable
public class ActionDescriptor(public val name: String) {
public var info: String? = null
}

@ -0,0 +1,10 @@
package ru.mipt.npm.controls.base
import ru.mipt.npm.controls.api.ActionDescriptor
import space.kscience.dataforge.meta.Meta
public interface DeviceAction {
public val name: String
public val descriptor: ActionDescriptor
public suspend operator fun invoke(arg: Meta? = null): Meta?
}

@ -0,0 +1,252 @@
package ru.mipt.npm.controls.base
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import ru.mipt.npm.controls.api.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DFExperimental
import kotlin.collections.set
import kotlin.coroutines.CoroutineContext
//TODO move to DataForge-core
@DFExperimental
public data class LogEntry(val content: String, val priority: Int = 0)
@OptIn(ExperimentalCoroutinesApi::class)
private open class BasicReadOnlyDeviceProperty(
val device: DeviceBase,
override val name: String,
default: Meta?,
override val descriptor: PropertyDescriptor,
private val getter: suspend (before: Meta?) -> Meta,
) : ReadOnlyDeviceProperty {
override val scope: CoroutineScope get() = device
private val state: MutableStateFlow<Meta?> = MutableStateFlow(default)
override val value: Meta? get() = state.value
override suspend fun invalidate() {
state.value = null
}
override fun updateLogical(item: Meta) {
state.value = item
scope.launch {
device.sharedMessageFlow.emit(
PropertyChangedMessage(
property = name,
value = item,
)
)
}
}
override suspend fun read(force: Boolean): Meta {
//backup current value
val currentValue = value
return if (force || currentValue == null) {
//all device operations should be run on device context
//propagate error, but do not fail scope
val res = withContext(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) {
getter(currentValue)
}
updateLogical(res)
res
} else {
currentValue
}
}
override fun flow(): StateFlow<Meta?> = state
}
@OptIn(ExperimentalCoroutinesApi::class)
private class BasicDeviceProperty(
device: DeviceBase,
name: String,
default: Meta?,
descriptor: PropertyDescriptor,
getter: suspend (Meta?) -> Meta,
private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
) : BasicReadOnlyDeviceProperty(device, name, default, descriptor, getter), DeviceProperty {
override var value: Meta?
get() = super.value
set(value) {
scope.launch {
if (value == null) {
invalidate()
} else {
write(value)
}
}
}
private val writeLock = Mutex()
override suspend fun write(item: Meta) {
writeLock.withLock {
//fast return if value is not changed
if (item == value) return@withLock
val oldValue = value
//all device operations should be run on device context
withContext(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) {
setter(oldValue, item)?.let {
updateLogical(it)
}
}
}
}
}
/**
* Baseline implementation of [Device] interface
*/
@Suppress("EXPERIMENTAL_API_USAGE")
public abstract class DeviceBase(final override val context: Context) : Device {
override val coroutineContext: CoroutineContext =
context.coroutineContext + SupervisorJob(context.coroutineContext[Job])
private val _properties = HashMap<String, ReadOnlyDeviceProperty>()
public val properties: Map<String, ReadOnlyDeviceProperty> get() = _properties
private val _actions = HashMap<String, DeviceAction>()
public val actions: Map<String, DeviceAction> get() = _actions
internal val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow
private val sharedLogFlow = MutableSharedFlow<LogEntry>()
/**
* The [SharedFlow] of log messages
*/
@DFExperimental
public val logFlow: SharedFlow<LogEntry>
get() = sharedLogFlow
protected suspend fun log(message: String, priority: Int = 0) {
sharedLogFlow.emit(LogEntry(message, priority))
}
override val propertyDescriptors: Collection<PropertyDescriptor>
get() = _properties.values.map { it.descriptor }
override val actionDescriptors: Collection<ActionDescriptor>
get() = _actions.values.map { it.descriptor }
private fun <P : ReadOnlyDeviceProperty> registerProperty(name: String, property: P) {
if (_properties.contains(name)) error("Property with name $name already registered")
_properties[name] = property
}
internal fun registerAction(name: String, action: DeviceAction) {
if (_actions.contains(name)) error("Action with name $name already registered")
_actions[name] = action
}
override suspend fun readProperty(propertyName: String): Meta =
(_properties[propertyName] ?: error("Property with name $propertyName not defined")).read()
override fun getProperty(propertyName: String): Meta? =
(_properties[propertyName] ?: error("Property with name $propertyName not defined")).value
override suspend fun invalidate(propertyName: String) {
(_properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate()
}
override suspend fun writeProperty(propertyName: String, value: Meta) {
(_properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write(
value
)
}
override suspend fun execute(action: String, argument: Meta?): Meta? =
(_actions[action] ?: error("Request with name $action not defined")).invoke(argument)
/**
* Create a bound read-only property with given [getter]
*/
public fun createReadOnlyProperty(
name: String,
default: Meta?,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Meta?) -> Meta,
): ReadOnlyDeviceProperty {
val property = BasicReadOnlyDeviceProperty(
this,
name,
default,
PropertyDescriptor(name).apply(descriptorBuilder),
getter
)
registerProperty(name, property)
return property
}
/**
* Create a bound mutable property with given [getter] and [setter]
*/
internal fun createMutableProperty(
name: String,
default: Meta?,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Meta?) -> Meta,
setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
): DeviceProperty {
val property = BasicDeviceProperty(
this,
name,
default,
PropertyDescriptor(name).apply(descriptorBuilder),
getter,
setter
)
registerProperty(name, property)
return property
}
/**
* A stand-alone action
*/
private inner class BasicDeviceAction(
override val name: String,
override val descriptor: ActionDescriptor,
private val block: suspend (Meta?) -> Meta?,
) : DeviceAction {
override suspend fun invoke(arg: Meta?): Meta? =
withContext(coroutineContext) {
block(arg)
}
}
/**
* Create a new bound action
*/
internal fun createAction(
name: String,
descriptorBuilder: ActionDescriptor.() -> Unit = {},
block: suspend (Meta?) -> Meta?,
): DeviceAction {
val action = BasicDeviceAction(name, ActionDescriptor(name).apply(descriptorBuilder), block)
registerAction(name, action)
return action
}
public companion object {
}
}

@ -1,60 +1,60 @@
package hep.dataforge.control.base
package ru.mipt.npm.controls.base
import hep.dataforge.control.api.PropertyDescriptor
import hep.dataforge.meta.MetaItem
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import ru.mipt.npm.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import kotlin.time.Duration
/**
* Read-only device property
*/
interface ReadOnlyDeviceProperty {
public interface ReadOnlyDeviceProperty {
/**
* Property name, should be unique in device
*/
val name: String
public val name: String
/**
* Property descriptor
*/
val descriptor: PropertyDescriptor
public val descriptor: PropertyDescriptor
val scope: CoroutineScope
public val scope: CoroutineScope
/**
* Erase logical value and force re-read from device on next [read]
*/
suspend fun invalidate()
public suspend fun invalidate()
/**
* Directly update property logical value and notify listener without writing it to device
*/
public fun updateLogical(item: Meta)
// /**
// * Update property logical value and notify listener without writing it to device
// */
// suspend fun update(item: MetaItem<*>)
//
/**
* Get cached value and return null if value is invalid or not initialized
*/
val value: MetaItem<*>?
public val value: Meta?
/**
* Read value either from cache if cache is valid or directly from physical device.
* If [force], reread
* If [force], reread from physical state even if the logical state is set.
*/
suspend fun read(force: Boolean = false): MetaItem<*>
public suspend fun read(force: Boolean = false): Meta
/**
* The [Flow] representing future logical states of the property.
* Produces null when the state is invalidated
*/
fun flow(): Flow<MetaItem<*>?>
public fun flow(): Flow<Meta?>
}
/**
* Launch recurring force re-read job on a property scope with given [duration] between reads.
*/
fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch {
public fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch {
while (isActive) {
read(true)
delay(duration)
@ -64,11 +64,11 @@ fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch {
/**
* A writeable device property with non-suspended write
*/
interface DeviceProperty : ReadOnlyDeviceProperty {
override var value: MetaItem<*>?
public interface DeviceProperty : ReadOnlyDeviceProperty {
override var value: Meta?
/**
* Write value to physical device. Invalidates logical value, but does not update it automatically
*/
suspend fun write(item: MetaItem<*>)
public suspend fun write(item: Meta)
}

@ -0,0 +1,58 @@
package ru.mipt.npm.controls.base
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
/**
* A type-safe wrapper on top of read-only property
*/
public open class TypedReadOnlyDeviceProperty<T : Any>(
private val property: ReadOnlyDeviceProperty,
protected val converter: MetaConverter<T>,
) : ReadOnlyDeviceProperty by property {
public fun updateLogical(obj: T) {
property.updateLogical(converter.objectToMeta(obj))
}
public open val typedValue: T? get() = value?.let { converter.metaToObject(it) }
public suspend fun readTyped(force: Boolean = false): T {
val meta = read(force)
return converter.metaToObject(meta)
?: error("Meta $meta could not be converted by $converter")
}
public fun flowTyped(): Flow<T?> = flow().map { it?.let { converter.metaToObject(it) } }
}
/**
* A type-safe wrapper for a read-write device property
*/
public class TypedDeviceProperty<T : Any>(
private val property: DeviceProperty,
converter: MetaConverter<T>,
) : TypedReadOnlyDeviceProperty<T>(property, converter), DeviceProperty {
override var value: Meta?
get() = property.value
set(arg) {
property.value = arg
}
public override var typedValue: T?
get() = value?.let { converter.metaToObject(it) }
set(arg) {
property.value = arg?.let { converter.objectToMeta(arg) }
}
override suspend fun write(item: Meta) {
property.write(item)
}
public suspend fun write(obj: T) {
property.write(converter.objectToMeta(obj))
}
}

@ -0,0 +1,58 @@
package ru.mipt.npm.controls.base
import ru.mipt.npm.controls.api.ActionDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.values.Value
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
private fun <D : DeviceBase> D.provideAction(): ReadOnlyProperty<D, DeviceAction> =
ReadOnlyProperty { _: D, property: KProperty<*> ->
val name = property.name
return@ReadOnlyProperty actions[name]!!
}
public typealias ActionDelegate = ReadOnlyProperty<DeviceBase, DeviceAction>
private class ActionProvider<D : DeviceBase>(
val owner: D,
val descriptorBuilder: ActionDescriptor.() -> Unit = {},
val block: suspend (Meta?) -> Meta?,
) : PropertyDelegateProvider<D, ActionDelegate> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ActionDelegate {
val name = property.name
owner.createAction(name, descriptorBuilder, block)
return owner.provideAction()
}
}
public fun DeviceBase.requesting(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
action: suspend (Meta?) -> Meta?,
): PropertyDelegateProvider<DeviceBase, ActionDelegate> = ActionProvider(this, descriptorBuilder, action)
public fun <D : DeviceBase> D.requestingValue(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
action: suspend (Meta?) -> Any?,
): PropertyDelegateProvider<D, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
val res = action(it)
Meta(Value.of(res))
}
public fun <D : DeviceBase> D.requestingMeta(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
action: suspend MutableMeta.(Meta?) -> Unit,
): PropertyDelegateProvider<D, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
Meta { action(it) }
}
public fun DeviceBase.acting(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
action: suspend (Meta?) -> Unit,
): PropertyDelegateProvider<DeviceBase, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
action(it)
null
}

@ -0,0 +1,283 @@
package ru.mipt.npm.controls.base
import ru.mipt.npm.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.boolean
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.values.Null
import space.kscience.dataforge.values.Value
import space.kscience.dataforge.values.asValue
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
private fun <D : DeviceBase> D.provideProperty(name: String): ReadOnlyProperty<D, ReadOnlyDeviceProperty> =
ReadOnlyProperty { _: D, _: KProperty<*> ->
return@ReadOnlyProperty properties.getValue(name)
}
private fun <D : DeviceBase, T : Any> D.provideProperty(
name: String,
converter: MetaConverter<T>,
): ReadOnlyProperty<D, TypedReadOnlyDeviceProperty<T>> =
ReadOnlyProperty { _: D, _: KProperty<*> ->
return@ReadOnlyProperty TypedReadOnlyDeviceProperty(properties.getValue(name), converter)
}
public typealias ReadOnlyPropertyDelegate = ReadOnlyProperty<DeviceBase, ReadOnlyDeviceProperty>
public typealias TypedReadOnlyPropertyDelegate<T> = ReadOnlyProperty<DeviceBase, TypedReadOnlyDeviceProperty<T>>
private class ReadOnlyDevicePropertyProvider<D : DeviceBase>(
val owner: D,
val default: Meta?,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (Meta?) -> Meta,
) : PropertyDelegateProvider<D, ReadOnlyPropertyDelegate> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ReadOnlyPropertyDelegate {
val name = property.name
owner.createReadOnlyProperty(name, default, descriptorBuilder, getter)
return owner.provideProperty(name)
}
}
private class TypedReadOnlyDevicePropertyProvider<D : DeviceBase, T : Any>(
val owner: D,
val default: Meta?,
val converter: MetaConverter<T>,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (Meta?) -> Meta,
) : PropertyDelegateProvider<D, TypedReadOnlyPropertyDelegate<T>> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedReadOnlyPropertyDelegate<T> {
val name = property.name
owner.createReadOnlyProperty(name, default, descriptorBuilder, getter)
return owner.provideProperty(name, converter)
}
}
public fun DeviceBase.reading(
default: Meta? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Meta?) -> Meta,
): PropertyDelegateProvider<DeviceBase, ReadOnlyPropertyDelegate> = ReadOnlyDevicePropertyProvider(
this,
default,
descriptorBuilder,
getter
)
public fun DeviceBase.readingValue(
default: Value? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Any?,
): PropertyDelegateProvider<DeviceBase, ReadOnlyPropertyDelegate> = ReadOnlyDevicePropertyProvider(
this,
default?.let { Meta(it) },
descriptorBuilder,
getter = { Meta(Value.of(getter())) }
)
public fun DeviceBase.readingNumber(
default: Number? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Number,
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Number>> = TypedReadOnlyDevicePropertyProvider(
this,
default?.let { Meta(it.asValue()) },
MetaConverter.number,
descriptorBuilder,
getter = {
val number = getter()
Meta(number.asValue())
}
)
public fun DeviceBase.readingDouble(
default: Number? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Double,
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Double>> = TypedReadOnlyDevicePropertyProvider(
this,
default?.let { Meta(it.asValue()) },
MetaConverter.double,
descriptorBuilder,
getter = {
val number = getter()
Meta(number.asValue())
}
)
public fun DeviceBase.readingString(
default: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> String,
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<String>> = TypedReadOnlyDevicePropertyProvider(
this,
default?.let { Meta(it.asValue()) },
MetaConverter.string,
descriptorBuilder,
getter = {
val number = getter()
Meta(number.asValue())
}
)
public fun DeviceBase.readingBoolean(
default: Boolean? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Boolean,
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Boolean>> = TypedReadOnlyDevicePropertyProvider(
this,
default?.let { Meta(it.asValue()) },
MetaConverter.boolean,
descriptorBuilder,
getter = {
val boolean = getter()
Meta(boolean.asValue())
}
)
public fun DeviceBase.readingMeta(
default: Meta? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend MutableMeta.() -> Unit,
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Meta>> = TypedReadOnlyDevicePropertyProvider(
this,
default,
MetaConverter.meta,
descriptorBuilder,
getter = {
Meta { getter() }
}
)
private fun DeviceBase.provideMutableProperty(name: String): ReadOnlyProperty<DeviceBase, DeviceProperty> =
ReadOnlyProperty { _: DeviceBase, _: KProperty<*> ->
return@ReadOnlyProperty properties[name] as DeviceProperty
}
private fun <T : Any> DeviceBase.provideMutableProperty(
name: String,
converter: MetaConverter<T>,
): ReadOnlyProperty<DeviceBase, TypedDeviceProperty<T>> =
ReadOnlyProperty { _: DeviceBase, _: KProperty<*> ->
return@ReadOnlyProperty TypedDeviceProperty(properties[name] as DeviceProperty, converter)
}
public typealias PropertyDelegate = ReadOnlyProperty<DeviceBase, DeviceProperty>
public typealias TypedPropertyDelegate<T> = ReadOnlyProperty<DeviceBase, TypedDeviceProperty<T>>
private class DevicePropertyProvider<D : DeviceBase>(
val owner: D,
val default: Meta?,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (Meta?) -> Meta,
private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
) : PropertyDelegateProvider<D, PropertyDelegate> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): PropertyDelegate {
val name = property.name
owner.createMutableProperty(name, default, descriptorBuilder, getter, setter)
return owner.provideMutableProperty(name)
}
}
private class TypedDevicePropertyProvider<D : DeviceBase, T : Any>(
val owner: D,
val default: Meta?,
val converter: MetaConverter<T>,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (Meta?) -> Meta,
private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
) : PropertyDelegateProvider<D, TypedPropertyDelegate<T>> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedPropertyDelegate<T> {
val name = property.name
owner.createMutableProperty(name, default, descriptorBuilder, getter, setter)
return owner.provideMutableProperty(name, converter)
}
}
public fun DeviceBase.writing(
default: Meta? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Meta?) -> Meta,
setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
): PropertyDelegateProvider<DeviceBase, PropertyDelegate> = DevicePropertyProvider(
this,
default,
descriptorBuilder,
getter,
setter
)
public fun DeviceBase.writingVirtual(
default: Meta,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceBase, PropertyDelegate> = writing(
default,
descriptorBuilder,
getter = { it ?: default },
setter = { _, newItem -> newItem }
)
public fun DeviceBase.writingVirtual(
default: Value,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceBase, PropertyDelegate> = writing(
Meta(default),
descriptorBuilder,
getter = { it ?: Meta(default) },
setter = { _, newItem -> newItem }
)
public fun <D : DeviceBase> D.writingDouble(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Double) -> Double,
setter: suspend (oldValue: Double?, newValue: Double) -> Double?,
): PropertyDelegateProvider<D, TypedPropertyDelegate<Double>> {
val innerGetter: suspend (Meta?) -> Meta = {
Meta(getter(it.double ?: Double.NaN).asValue())
}
val innerSetter: suspend (oldValue: Meta?, newValue: Meta) -> Meta? = { oldValue, newValue ->
setter(oldValue.double, newValue.double ?: Double.NaN)?.asMeta()
}
return TypedDevicePropertyProvider(
this,
Meta(Double.NaN.asValue()),
MetaConverter.double,
descriptorBuilder,
innerGetter,
innerSetter
)
}
public fun <D : DeviceBase> D.writingBoolean(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Boolean?) -> Boolean,
setter: suspend (oldValue: Boolean?, newValue: Boolean) -> Boolean?,
): PropertyDelegateProvider<D, TypedPropertyDelegate<Boolean>> {
val innerGetter: suspend (Meta?) -> Meta = {
Meta(getter(it.boolean).asValue())
}
val innerSetter: suspend (oldValue: Meta?, newValue: Meta) -> Meta? = { oldValue, newValue ->
setter(oldValue.boolean, newValue.boolean ?: error("Can't convert $newValue to boolean"))?.asValue()
?.let { Meta(it) }
}
return TypedDevicePropertyProvider(
this,
Meta(Null),
MetaConverter.boolean,
descriptorBuilder,
innerGetter,
innerSetter
)
}

@ -0,0 +1,28 @@
package ru.mipt.npm.controls.base
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.enum
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.values.asValue
import space.kscience.dataforge.values.double
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
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)
?: run {
val unit: DurationUnit = meta["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
val value = meta[Meta.VALUE_KEY].double ?: error("No value present for Duration")
return@run value.toDuration(unit)
}
override fun objectToMeta(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta()
}
public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter

@ -0,0 +1,54 @@
package ru.mipt.npm.controls.controllers
import ru.mipt.npm.controls.api.Device
import ru.mipt.npm.controls.api.DeviceHub
import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import kotlin.collections.set
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KClass
public class DeviceManager : AbstractPlugin(), DeviceHub {
override val tag: PluginTag get() = Companion.tag
/**
* Actual list of connected devices
*/
private val top = HashMap<NameToken, Device>()
override val devices: Map<NameToken, Device> get() = top
public fun registerDevice(name: NameToken, device: Device) {
top[name] = device
}
override fun content(target: String): Map<Name, Any> = super<DeviceHub>.content(target)
public companion object : PluginFactory<DeviceManager> {
override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out DeviceManager> = DeviceManager::class
override fun invoke(meta: Meta, context: Context): DeviceManager = DeviceManager()
}
}
public fun <D : Device> DeviceManager.install(name: String, factory: Factory<D>, meta: Meta = Meta.EMPTY): D {
val device = factory(meta, context)
registerDevice(NameToken(name), device)
return device
}
public inline fun <D : Device> DeviceManager.installing(
factory: Factory<D>,
builder: MutableMeta.() -> Unit = {},
): ReadOnlyProperty<Any?, D> {
val meta = Meta(builder)
return ReadOnlyProperty { _, property ->
val name = property.name
install(name, factory, meta)
}
}

@ -0,0 +1,118 @@
package ru.mipt.npm.controls.controllers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
import ru.mipt.npm.controls.api.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.toMeta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.plus
public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMessage): DeviceMessage? = try {
when (request) {
is PropertyGetMessage -> {
PropertyChangedMessage(
property = request.property,
value = getOrReadProperty(request.property),
sourceDevice = deviceTarget,
targetDevice = request.sourceDevice
)
}
is PropertySetMessage -> {
if (request.value == null) {
invalidate(request.property)
} else {
writeProperty(request.property, request.value)
}
PropertyChangedMessage(
property = request.property,
value = getOrReadProperty(request.property),
sourceDevice = deviceTarget,
targetDevice = request.sourceDevice
)
}
is ActionExecuteMessage -> {
ActionResultMessage(
action = request.action,
result = execute(request.action, request.argument),
sourceDevice = deviceTarget,
targetDevice = request.sourceDevice
)
}
is GetDescriptionMessage -> {
val descriptionMeta = Meta {
"properties" put {
propertyDescriptors.map { descriptor ->
descriptor.name put Json.encodeToJsonElement(descriptor).toMeta()
}
}
"actions" put {
actionDescriptors.map { descriptor ->
descriptor.name put Json.encodeToJsonElement(descriptor).toMeta()
}
}
}
DescriptionMessage(
description = descriptionMeta,
sourceDevice = deviceTarget,
targetDevice = request.sourceDevice
)
}
is DescriptionMessage,
is PropertyChangedMessage,
is ActionResultMessage,
is BinaryNotificationMessage,
is DeviceErrorMessage,
is EmptyDeviceMessage,
is DeviceLogMessage,
-> null
}
} catch (ex: Exception) {
DeviceMessage.error(ex, sourceDevice = deviceTarget, targetDevice = request.sourceDevice)
}
public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMessage? {
return try {
val targetName = request.targetDevice ?: return null
val device = getOrNull(targetName) ?: error("The device with name $targetName not found in $this")
device.respondMessage(targetName, request)
} catch (ex: Exception) {
DeviceMessage.error(ex, sourceDevice = Name.EMPTY, targetDevice = request.sourceDevice)
}
}
/**
* Collect all messages from given [DeviceHub], applying proper relative names
*/
public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow<DeviceMessage> {
val outbox = MutableSharedFlow<DeviceMessage>()
if (this is Device) {
messageFlow.onEach {
outbox.emit(it)
}.launchIn(scope)
}
//TODO maybe better create map of all devices to limit copying
devices.forEach { (token, childDevice) ->
val flow = if (childDevice is DeviceHub) {
childDevice.hubMessageFlow(scope)
} else {
childDevice.messageFlow
}
flow.onEach { deviceMessage ->
outbox.emit(
deviceMessage.changeSource { token + it }
)
}.launchIn(scope)
}
return outbox
}

@ -0,0 +1,87 @@
package ru.mipt.npm.controls.ports
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import ru.mipt.npm.controls.api.Socket
import space.kscience.dataforge.context.*
import kotlin.coroutines.CoroutineContext
public interface Port : ContextAware, Socket<ByteArray>
public typealias PortFactory = Factory<Port>
public abstract class AbstractPort(
override val context: Context,
coroutineContext: CoroutineContext = context.coroutineContext,
) : Port {
protected val scope: CoroutineScope = CoroutineScope(coroutineContext + SupervisorJob(coroutineContext[Job]))
private val outgoing = Channel<ByteArray>(100)
private val incoming = Channel<ByteArray>(Channel.CONFLATED)
init {
scope.coroutineContext[Job]?.invokeOnCompletion {
close()
}
}
/**
* Internal method to synchronously send data
*/
protected abstract suspend fun write(data: ByteArray)
/**
* Internal method to receive data synchronously
*/
protected fun receive(data: ByteArray) {
scope.launch {
logger.debug { "${this@AbstractPort} RECEIVED: ${data.decodeToString()}" }
incoming.send(data)
}
}
private val sendJob = scope.launch {
for (data in outgoing) {
try {
write(data)
logger.debug { "${this@AbstractPort} SENT: ${data.decodeToString()}" }
} catch (ex: Exception) {
if (ex is CancellationException) throw ex
logger.error(ex) { "Error while writing data to the port" }
}
}
}
/**
* Send a data packet via the port
*/
override suspend fun send(data: ByteArray) {
outgoing.send(data)
}
/**
* Raw flow of incoming data chunks. The chunks are not guaranteed to be complete phrases.
* In order to form phrases some condition should used on top of it.
* For example [delimitedIncoming] generates phrases with fixed delimiter.
*/
override fun receiving(): Flow<ByteArray> {
return incoming.receiveAsFlow()
}
override fun close() {
outgoing.close()
incoming.close()
sendJob.cancel()
scope.cancel()
}
override fun isOpen(): Boolean = scope.isActive
}
/**
* Send UTF-8 encoded string
*/
public suspend fun Port.send(string: String): Unit = send(string.encodeToByteArray())

@ -0,0 +1,65 @@
package ru.mipt.npm.controls.ports
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import space.kscience.dataforge.context.*
/**
* A port that could be closed multiple times and opens automatically on request
*/
public class PortProxy(override val context: Context = Global, public val factory: suspend () -> Port) : Port, ContextAware {
private var actualPort: Port? = null
private val mutex: Mutex = Mutex()
private suspend fun port(): Port {
return mutex.withLock {
if (actualPort?.isOpen() == true) {
actualPort!!
} else {
factory().also {
actualPort = it
}
}
}
}
override suspend fun send(data: ByteArray) {
port().send(data)
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun receiving(): Flow<ByteArray> = flow {
while (true) {
try {
//recreate port and Flow on connection problems
port().receiving().collect {
emit(it)
}
} catch (t: Throwable) {
logger.warn{"Port read failed: ${t.message}. Reconnecting."}
mutex.withLock {
actualPort?.close()
actualPort = null
}
}
}
}
// open by default
override fun isOpen(): Boolean = true
override fun close() {
context.launch {
mutex.withLock {
actualPort?.close()
actualPort = null
}
}
}
}

@ -0,0 +1,34 @@
package ru.mipt.npm.controls.ports
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended.
* The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment.
*
*/
public class SynchronousPortHandler(public val port: Port) {
private val mutex = Mutex()
/**
* Send a single message and wait for the flow of respond messages.
*/
public suspend fun <R> respond(data: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R {
return mutex.withLock {
port.send(data)
transform(port.receiving())
}
}
}
/**
* Send request and read incoming data blocks until the delimiter is encountered
*/
public suspend fun SynchronousPortHandler.respondWithDelimiter(data: ByteArray, delimiter: ByteArray): ByteArray {
return respond(data) {
withDelimiter(delimiter).first()
}
}

@ -0,0 +1,51 @@
package ru.mipt.npm.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.transform
/**
* Transform byte fragments into complete phrases using given delimiter. Not thread safe.
*/
public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow<ByteArray> {
require(delimiter.isNotEmpty()) { "Delimiter must not be empty" }
val output = BytePacketBuilder(expectedMessageSize)
var matcherPosition = 0
return transform { chunk ->
chunk.forEach { byte ->
output.writeByte(byte)
//matching current symbol in delimiter
if (byte == delimiter[matcherPosition]) {
matcherPosition++
if (matcherPosition == delimiter.size) {
//full match achieved, sending result
val bytes = output.build()
emit(bytes.readBytes())
output.reset()
matcherPosition = 0
}
} else if (matcherPosition > 0) {
//Reset matcher since full match not achieved
matcherPosition = 0
}
}
}
}
/**
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
*/
public fun Flow<ByteArray>.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow<String> {
return withDelimiter(delimiter.encodeToByteArray(), expectedMessageSize).map { it.decodeToString() }
}
/**
* A flow of delimited phrases
*/
public suspend fun Port.delimitedIncoming(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow<ByteArray> =
receiving().withDelimiter(delimiter, expectedMessageSize)

@ -0,0 +1,141 @@
package ru.mipt.npm.controls.properties
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import ru.mipt.npm.controls.api.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta
import kotlin.coroutines.CoroutineContext
/**
* A device generated from specification
* @param D recursive self-type for properties and actions
*/
@OptIn(InternalDeviceAPI::class)
public open class DeviceBySpec<D : DeviceBySpec<D>>(
public val spec: DeviceSpec<D>,
context: Context = Global,
meta: Meta = Meta.EMPTY
) : Device {
override var context: Context = context
internal set
public var meta: Meta = meta
internal set
public val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
override val propertyDescriptors: Collection<PropertyDescriptor>
get() = properties.values.map { it.descriptor }
override val actionDescriptors: Collection<ActionDescriptor>
get() = actions.values.map { it.descriptor }
override val coroutineContext: CoroutineContext by lazy {
context.coroutineContext + SupervisorJob(context.coroutineContext[Job])
}
private val logicalState: HashMap<String, Meta?> = HashMap()
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow()
public override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow
@Suppress("UNCHECKED_CAST")
internal val self: D
get() = this as D
private val stateLock = Mutex()
/**
* Update logical property state and notify listeners
*/
protected suspend fun updateLogical(propertyName: String, value: Meta?) {
if (value != logicalState[propertyName]) {
stateLock.withLock {
logicalState[propertyName] = value
}
if (value != null) {
sharedMessageFlow.emit(PropertyChangedMessage(propertyName, value))
}
}
}
/**
* Force read physical value and push an update if it is changed. It does not matter if logical state is present.
* The logical state is updated after read
*/
override suspend fun readProperty(propertyName: String): Meta {
val newValue = properties[propertyName]?.readMeta(self)
?: error("A property with name $propertyName is not registered in $this")
updateLogical(propertyName, newValue)
return newValue
}
override fun getProperty(propertyName: String): Meta? = logicalState[propertyName]
override suspend fun invalidate(propertyName: String) {
stateLock.withLock {
logicalState.remove(propertyName)
}
}
override suspend fun writeProperty(propertyName: String, value: Meta): Unit {
//If there is a physical property with given name, invalidate logical property and write physical one
(properties[propertyName] as? WritableDevicePropertySpec<D, out Any?>)?.let {
invalidate(propertyName)
it.writeMeta(self, value)
} ?: run {
updateLogical(propertyName, value)
}
}
override suspend fun execute(action: String, argument: Meta?): Meta? =
actions[action]?.executeMeta(self, argument)
/**
* Read typed value and update/push event if needed
*/
public suspend fun <T> DevicePropertySpec<D, T>.read(): T {
val res = read(self)
updateLogical(name, converter.objectToMeta(res))
return res
}
public fun <T> DevicePropertySpec<D, T>.get(): T? = getProperty(name)?.let(converter::metaToObject)
/**
* Write typed property state and invalidate logical state
*/
public suspend fun <T> WritableDevicePropertySpec<D, T>.write(value: T) {
invalidate(name)
write(self, value)
//perform asynchronous read and update after write
launch {
read()
}
}
override fun close() {
with(spec) { self.onShutdown() }
super.close()
}
}
public suspend fun <D : DeviceBySpec<D>, T : Any> D.read(
propertySpec: DevicePropertySpec<D, T>
): T = propertySpec.read()
public fun <D : DeviceBySpec<D>, T> D.write(
propertySpec: WritableDevicePropertySpec<D, T>,
value: T
): Job = launch {
propertySpec.write(value)
}

@ -0,0 +1,85 @@
package ru.mipt.npm.controls.properties
import ru.mipt.npm.controls.api.ActionDescriptor
import ru.mipt.npm.controls.api.Device
import ru.mipt.npm.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
/**
* This API is internal and should not be used in user code
*/
@RequiresOptIn
public annotation class InternalDeviceAPI
public interface DevicePropertySpec<in D : Device, T> {
/**
* Property name, should be unique in device
*/
public val name: String
/**
* Property descriptor
*/
public val descriptor: PropertyDescriptor
/**
* Meta item converter for resulting type
*/
public val converter: MetaConverter<T>
/**
* Read physical value from the given [device]
*/
@InternalDeviceAPI
public suspend fun read(device: D): T
}
@OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta =
converter.objectToMeta(read(device))
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
/**
* Write physical value to a device
*/
@InternalDeviceAPI
public suspend fun write(device: D, value: T)
}
@OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
}
public interface DeviceActionSpec<in D : Device, I, O> {
/**
* Action name, should be unique in device
*/
public val name: String
/**
* Action descriptor
*/
public val descriptor: ActionDescriptor
public val inputConverter: MetaConverter<I>
public val outputConverter: MetaConverter<O>
/**
* Execute action on a device
*/
public suspend fun execute(device: D, input: I?): O?
}
public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeMeta(
device: D,
item: Meta?
): Meta? {
val arg = item?.let { inputConverter.metaToObject(item) }
val res = execute(device, arg)
return res?.let { outputConverter.objectToMeta(res) }
}

@ -0,0 +1,171 @@
package ru.mipt.npm.controls.properties
import kotlinx.coroutines.withContext
import ru.mipt.npm.controls.api.ActionDescriptor
import ru.mipt.npm.controls.api.PropertyDescriptor
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
@OptIn(InternalDeviceAPI::class)
public abstract class DeviceSpec<D : DeviceBySpec<D>>(
private val buildDevice: () -> D
) : Factory<D> {
private val _properties = HashMap<String, DevicePropertySpec<D, *>>()
public val properties: Map<String, DevicePropertySpec<D, *>> get() = _properties
private val _actions = HashMap<String, DeviceActionSpec<D, *, *>>()
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = _actions
public fun <T : Any, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P {
_properties[deviceProperty.name] = deviceProperty
return deviceProperty
}
public fun <T : Any> registerProperty(
converter: MetaConverter<T>,
readOnlyProperty: KProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): DevicePropertySpec<D, T> {
val deviceProperty = object : DevicePropertySpec<D, T> {
override val name: String = readOnlyProperty.name
override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T =
withContext(device.coroutineContext) { readOnlyProperty.get(device) }
}
return registerProperty(deviceProperty)
}
public fun <T : Any> property(
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 name: String = property.name
override val descriptor: PropertyDescriptor = PropertyDescriptor(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) {
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 : Any> property(
converter: MetaConverter<T>,
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> 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 name: String = propertyName
override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() }
}
registerProperty(deviceProperty)
ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ ->
deviceProperty
}
}
public fun <T : Any> property(
converter: MetaConverter<T>,
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> T,
write: suspend D.(T) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
val propertyName = name ?: property.name
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
override val name: String = propertyName
override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() }
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
device.write(value)
}
}
_properties[propertyName] = deviceProperty
ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>> { _, _ ->
deviceProperty
}
}
public fun <I : Any, O : Any> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O> {
_actions[deviceAction.name] = deviceAction
return deviceAction
}
public fun <I : Any, O : Any> action(
inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>,
name: String? = null,
descriptorBuilder: ActionDescriptor.() -> Unit = {},
execute: suspend D.(I?) -> O?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
val actionName = name ?: property.name
val deviceAction = object : DeviceActionSpec<D, I, O> {
override val name: String = actionName
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder)
override val inputConverter: MetaConverter<I> = inputConverter
override val outputConverter: MetaConverter<O> = outputConverter
override suspend fun execute(device: D, input: I?): O? = withContext(device.coroutineContext) {
device.execute(input)
}
}
_actions[actionName] = deviceAction
ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>> { _, _ ->
deviceAction
}
}
/**
* The function is executed right after device initialization is finished
*/
public open fun D.onStartup() {}
/**
* The function is executed before device is shut down
*/
public open fun D.onShutdown() {}
override fun invoke(meta: Meta, context: Context): D = buildDevice().apply {
this.context = context
this.meta = meta
onStartup()
}
}

@ -0,0 +1,32 @@
package ru.mipt.npm.controls.properties
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
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 is canceled when the device scope is canceled
*/
public fun <D : DeviceBySpec<D>, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
while (isActive) {
kotlinx.coroutines.delay(interval)
emit(reader())
}
}
/**
* Do a recurring task on a device. The task could
*/
public fun <D : DeviceBySpec<D>> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
while (isActive) {
kotlinx.coroutines.delay(interval)
task()
}
}

@ -0,0 +1,144 @@
package ru.mipt.npm.controls.properties
import ru.mipt.npm.controls.api.PropertyDescriptor
import ru.mipt.npm.controls.api.metaDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.values.ValueType
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
//read only delegates
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Boolean
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
MetaConverter.boolean,
name,
{
metaDescriptor {
type(ValueType.BOOLEAN)
}
descriptorBuilder()
},
read
)
private inline fun numberDescriptor(
crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): PropertyDescriptor.() -> Unit = {
metaDescriptor {
type(ValueType.NUMBER)
}
descriptorBuilder()
}
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Number
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
MetaConverter.number,
name,
numberDescriptor(descriptorBuilder),
read
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Double
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
MetaConverter.double,
name,
numberDescriptor(descriptorBuilder),
read
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> String
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
MetaConverter.string,
name,
{
metaDescriptor {
type(ValueType.STRING)
}
descriptorBuilder()
},
read
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Meta
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
MetaConverter.meta,
name,
{
metaDescriptor {
type(ValueType.STRING)
}
descriptorBuilder()
},
read
)
//read-write delegates
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Boolean,
write: suspend D.(Boolean) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
property(
MetaConverter.boolean,
name,
{
metaDescriptor {
type(ValueType.BOOLEAN)
}
descriptorBuilder()
},
read,
write
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Number,
write: suspend D.(Number) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> =
property(MetaConverter.number, name, numberDescriptor(descriptorBuilder), read, write)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Double,
write: suspend D.(Double) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> =
property(MetaConverter.double, name, numberDescriptor(descriptorBuilder), read, write)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> String,
write: suspend D.(String) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> =
property(MetaConverter.string, name, descriptorBuilder, read, write)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Meta,
write: suspend D.(Meta) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Meta>>> =
property(MetaConverter.meta, name, descriptorBuilder, read, write)

@ -0,0 +1,19 @@
package ru.mipt.npm.controls.misc
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.long
import space.kscience.dataforge.values.long
import java.time.Instant
// TODO move to core
public fun Instant.toMeta(): Meta = Meta {
"seconds" put epochSecond
"nanos" put nano
}
public fun Meta.instant(): Instant = value?.long?.let { Instant.ofEpochMilli(it) } ?: Instant.ofEpochSecond(
get("seconds")?.long ?: 0L,
get("nanos")?.long ?: 0L,
)

@ -0,0 +1,91 @@
package ru.mipt.npm.controls.ports
import kotlinx.coroutines.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.string
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.channels.SocketChannel
import kotlin.coroutines.CoroutineContext
internal fun ByteBuffer.readArray(limit: Int = limit()): ByteArray {
rewind()
val response = ByteArray(limit)
get(response)
rewind()
return response
}
public class TcpPort private constructor(
context: Context,
public val host: String,
public val port: Int,
coroutineContext: CoroutineContext = context.coroutineContext,
) : AbstractPort(context, coroutineContext), AutoCloseable {
override fun toString(): String = "port[tcp:$host:$port]"
private val futureChannel: Deferred<SocketChannel> = this.scope.async(Dispatchers.IO) {
SocketChannel.open(InetSocketAddress(host, port)).apply {
configureBlocking(false)
}
}
/**
* A handler to await port connection
*/
public val startJob: Job get() = futureChannel
private val listenerJob = this.scope.launch(Dispatchers.IO) {
val channel = futureChannel.await()
val buffer = ByteBuffer.allocate(1024)
while (isActive) {
try {
val num = channel.read(buffer)
if (num > 0) {
receive(buffer.readArray(num))
}
if (num < 0) cancel("The input channel is exhausted")
} catch (ex: Exception) {
logger.error(ex){"Channel read error"}
delay(1000)
}
}
}
override suspend fun write(data: ByteArray) {
futureChannel.await().write(ByteBuffer.wrap(data))
}
override fun close() {
listenerJob.cancel()
if(futureChannel.isCompleted){
futureChannel.getCompleted().close()
} else {
futureChannel.cancel()
}
super.close()
}
public companion object : PortFactory {
public fun open(
context: Context,
host: String,
port: Int,
coroutineContext: CoroutineContext = context.coroutineContext,
): TcpPort {
return TcpPort(context, host, port, coroutineContext)
}
override fun invoke(meta: Meta, context: Context): Port {
val host = meta["host"].string ?: "localhost"
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
return open(context, host, port)
}
}
}

@ -0,0 +1,89 @@
package ru.mipt.npm.controls.controllers
import kotlinx.coroutines.runBlocking
import ru.mipt.npm.controls.base.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlin.time.Duration
/**
* Blocking read of the value
*/
public operator fun ReadOnlyDeviceProperty.getValue(thisRef: Any?, property: KProperty<*>): Meta =
runBlocking(scope.coroutineContext) {
read()
}
public operator fun <T: Any> TypedReadOnlyDeviceProperty<T>.getValue(thisRef: Any?, property: KProperty<*>): T =
runBlocking(scope.coroutineContext) {
readTyped()
}
public operator fun DeviceProperty.setValue(thisRef: Any?, property: KProperty<*>, value: Meta) {
this.value = value
}
public operator fun <T: Any> TypedDeviceProperty<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.typedValue = value
}
public fun <T : Any> ReadOnlyDeviceProperty.convert(
metaConverter: MetaConverter<T>,
forceRead: Boolean,
): ReadOnlyProperty<Any?, T> {
return ReadOnlyProperty { _, _ ->
runBlocking(scope.coroutineContext) {
val meta = read(forceRead)
metaConverter.metaToObject(meta)?: error("Meta $meta could not be converted by $metaConverter")
}
}
}
public fun <T : Any> DeviceProperty.convert(
metaConverter: MetaConverter<T>,
forceRead: Boolean,
): ReadWriteProperty<Any?, T> {
return object : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking(scope.coroutineContext) {
val meta = read(forceRead)
metaConverter.metaToObject(meta)?: error("Meta $meta could not be converted by $metaConverter")
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this@convert.setValue(thisRef, property, value.let { metaConverter.objectToMeta(it) })
}
}
}
public fun ReadOnlyDeviceProperty.double(forceRead: Boolean = false): ReadOnlyProperty<Any?, Double> =
convert(MetaConverter.double, forceRead)
public fun DeviceProperty.double(forceRead: Boolean = false): ReadWriteProperty<Any?, Double> =
convert(MetaConverter.double, forceRead)
public fun ReadOnlyDeviceProperty.int(forceRead: Boolean = false): ReadOnlyProperty<Any?, Int> =
convert(MetaConverter.int, forceRead)
public fun DeviceProperty.int(forceRead: Boolean = false): ReadWriteProperty<Any?, Int> =
convert(MetaConverter.int, forceRead)
public fun ReadOnlyDeviceProperty.string(forceRead: Boolean = false): ReadOnlyProperty<Any?, String> =
convert(MetaConverter.string, forceRead)
public fun DeviceProperty.string(forceRead: Boolean = false): ReadWriteProperty<Any?, String> =
convert(MetaConverter.string, forceRead)
public fun ReadOnlyDeviceProperty.boolean(forceRead: Boolean = false): ReadOnlyProperty<Any?, Boolean> =
convert(MetaConverter.boolean, forceRead)
public fun DeviceProperty.boolean(forceRead: Boolean = false): ReadWriteProperty<Any?, Boolean> =
convert(MetaConverter.boolean, forceRead)
public fun ReadOnlyDeviceProperty.duration(forceRead: Boolean = false): ReadOnlyProperty<Any?, Duration> =
convert(DurationConverter, forceRead)
public fun DeviceProperty.duration(forceRead: Boolean = false): ReadWriteProperty<Any?, Duration> =
convert(DurationConverter, forceRead)

@ -0,0 +1,10 @@
package ru.mipt.npm.controls.properties
import kotlinx.coroutines.runBlocking
/**
* Blocking property get call
*/
public operator fun <D : DeviceBySpec<D>, T : Any> D.get(
propertySpec: DevicePropertySpec<D, T>
): T = runBlocking { read(propertySpec) }

@ -0,0 +1,25 @@
package ru.mipt.npm.controls.ports
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
internal class PortIOTest{
@Test
fun testDelimiteredByteArrayFlow(){
val flow = flowOf("bb?b","ddd?",":defgb?:ddf","34fb?:--").map { it.encodeToByteArray() }
val chunked = flow.withDelimiter("?:".encodeToByteArray())
runBlocking {
val result = chunked.toList()
assertEquals(3, result.size)
assertEquals("bb?bddd?:",result[0].decodeToString())
assertEquals("defgb?:", result[1].decodeToString())
assertEquals("ddf34fb?:", result[2].decodeToString())
}
}
}

@ -0,0 +1,21 @@
plugins {
id("ru.mipt.npm.gradle.mpp")
`maven-publish`
}
kscience{
useSerialization {
json()
}
}
kotlin {
sourceSets {
commonMain {
dependencies {
implementation(project(":magix:magix-rsocket"))
implementation(project(":controls-core"))
}
}
}
}

@ -0,0 +1,64 @@
package ru.mipt.npm.controls.client
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import ru.mipt.npm.controls.api.DeviceMessage
import ru.mipt.npm.controls.controllers.DeviceManager
import ru.mipt.npm.controls.controllers.hubMessageFlow
import ru.mipt.npm.controls.controllers.respondHubMessage
import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.api.MagixMessage
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
public const val DATAFORGE_MAGIX_FORMAT: String = "dataforge"
internal fun generateId(request: MagixMessage<*>): String = if (request.id != null) {
"${request.id}.response"
} else {
"df[${request.payload.hashCode()}"
}
/**
* Communicate with server in [Magix format](https://github.com/waltz-controls/rfc/tree/master/1)
*/
public fun DeviceManager.connectToMagix(
endpoint: MagixEndpoint<DeviceMessage>,
endpointID: String = DATAFORGE_MAGIX_FORMAT,
): Job = context.launch {
endpoint.subscribe().onEach { request ->
val responsePayload = respondHubMessage(request.payload)
if (responsePayload != null) {
val response = MagixMessage(
format = DATAFORGE_MAGIX_FORMAT,
id = generateId(request),
parentId = request.id,
origin = endpointID,
payload = responsePayload
)
endpoint.broadcast(response)
}
}.catch { error ->
logger.error(error) { "Error while responding to message" }
}.launchIn(this)
hubMessageFlow(this).onEach { payload ->
endpoint.broadcast(
MagixMessage(
format = DATAFORGE_MAGIX_FORMAT,
id = "df[${payload.hashCode()}]",
origin = endpointID,
payload = payload
)
)
}.catch { error ->
logger.error(error) { "Error while sending a message" }
}.launchIn(this)
}

@ -0,0 +1,119 @@
package ru.mipt.npm.controls.client
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
/*
"action":"get|set",
"eq_address": "string",
"eq_data": {
"type_id": "int[required]",
"type": "string[optional]",
"value": "object|value",
"event_id": "int[optional]",
"error": "int[optional]",
"time": "long[optional]",
"message": "string[optional]"
}
*/
@Serializable
public enum class DoocsAction {
get,
set,
names
}
@Serializable
public data class EqData(
@SerialName("type_id")
val typeId: Int,
val type: String? = null,
val value: Meta? = null,
@SerialName("event_id")
val eventId: Int? = null,
val error: Int? = null,
val time: Long? = null,
val message: String? = null
) {
public companion object {
internal const val DATA_NULL: Int = 0
internal const val DATA_INT: Int = 1
internal const val DATA_FLOAT: Int = 2
internal const val DATA_STRING: Int = 3
internal const val DATA_BOOL: Int = 4
internal const val DATA_STRING16: Int = 5
internal const val DATA_DOUBLE: Int = 6
internal const val DATA_TEXT: Int = 7
internal const val DATA_TDS: Int = 12
internal const val DATA_XY: Int = 13
internal const val DATA_IIII: Int = 14
internal const val DATA_IFFF: Int = 15
internal const val DATA_USTR: Int = 16
internal const val DATA_TTII: Int = 18
internal const val DATA_SPECTRUM: Int = 19
internal const val DATA_XML: Int = 20
internal const val DATA_XYZS: Int = 21
internal const val DATA_IMAGE: Int = 22
internal const val DATA_GSPECTRUM: Int = 24
internal const val DATA_SHORT: Int = 25
internal const val DATA_LONG: Int = 26
internal const val DATA_USHORT: Int = 27
internal const val DATA_UINT: Int = 28
internal const val DATA_ULONG: Int = 29
internal const val DATA_A_FLOAT: Int = 100
internal const val DATA_A_TDS: Int = 101
internal const val DATA_A_XY: Int = 102
internal const val DATA_A_USTR: Int = 103
internal const val DATA_A_INT: Int = 105
internal const val DATA_A_BYTE: Int = 106
internal const val DATA_A_XYZS: Int = 108
internal const val DATA_MDA_FLOAT: Int = 109
internal const val DATA_A_DOUBLE: Int = 110
internal const val DATA_A_BOOL: Int = 111
internal const val DATA_A_STRING: Int = 112
internal const val DATA_A_SHORT: Int = 113
internal const val DATA_A_LONG: Int = 114
internal const val DATA_MDA_DOUBLE: Int = 115
internal const val DATA_A_USHORT: Int = 116
internal const val DATA_A_UINT: Int = 117
internal const val DATA_A_ULONG: Int = 118
internal const val DATA_A_THUMBNAIL: Int = 120
internal const val DATA_A_TS_BOOL: Int = 1000
internal const val DATA_A_TS_INT: Int = 1001
internal const val DATA_A_TS_FLOAT: Int = 1002
internal const val DATA_A_TS_DOUBLE: Int = 1003
internal const val DATA_A_TS_LONG: Int = 1004
internal const val DATA_A_TS_STRING: Int = 1005
internal const val DATA_A_TS_USTR: Int = 1006
internal const val DATA_A_TS_XML: Int = 1007
internal const val DATA_A_TS_XY: Int = 1008
internal const val DATA_A_TS_IIII: Int = 1009
internal const val DATA_A_TS_IFFF: Int = 1010
internal const val DATA_A_TS_SPECTRUM: Int = 1013
internal const val DATA_A_TS_XYZS: Int = 1014
internal const val DATA_A_TS_GSPECTRUM: Int = 1015
internal const val DATA_KEYVAL: Int = 1016
internal const val DATA_A_TS_SHORT: Int = 1017
internal const val DATA_A_TS_USHORT: Int = 1018
internal const val DATA_A_TS_UINT: Int = 1019
internal const val DATA_A_TS_ULONG: Int = 1020
}
}
@Serializable
public data class DoocsPayload(
val action: DoocsAction,
@SerialName("eq_address")
val address: String,
@SerialName("eq_data")
val data: EqData?
)

@ -0,0 +1,146 @@
package ru.mipt.npm.controls.client
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import ru.mipt.npm.controls.api.get
import ru.mipt.npm.controls.api.getOrReadProperty
import ru.mipt.npm.controls.controllers.DeviceManager
import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.api.MagixMessage
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta
public const val TANGO_MAGIX_FORMAT: String = "tango"
/*
See https://github.com/waltz-controls/rfc/tree/master/4 for details
"action":"read|write|exec|pipe",
"timestamp": "int",
"host":"tango_host",
"device":"device name",
"name":"attribute, command or pipe's name",
"[value]":"attribute's value",
"[quality]":"VALID|WARNING|ALARM",
"[argin]":"command argin",
"[argout]":"command argout",
"[data]":"pipe's data",
"[errors]":[]
*/
@Serializable
public enum class TangoAction {
read,
write,
exec,
pipe
}
@Serializable
public enum class TangoQuality {
VALID,
WARNING,
ALARM
}
@Serializable
public data class TangoPayload(
val action: TangoAction,
val timestamp: Int,
val host: String,
val device: String,
val name: String,
val value: Meta? = null,
val quality: TangoQuality = TangoQuality.VALID,
val argin: Meta? = null,
val argout: Meta? = null,
val data: Meta? = null,
val errors: List<String>? = null
)
public fun DeviceManager.launchTangoMagix(
endpoint: MagixEndpoint<TangoPayload>,
endpointID: String = TANGO_MAGIX_FORMAT,
): Job {
suspend fun respond(request: MagixMessage<TangoPayload>, payloadBuilder: (TangoPayload) -> TangoPayload) {
endpoint.broadcast(
request.copy(
id = generateId(request),
parentId = request.id,
origin = endpointID,
payload = payloadBuilder(request.payload)
)
)
}
return context.launch {
endpoint.subscribe().onEach { request ->
try {
val device = get(request.payload.device)
when (request.payload.action) {
TangoAction.read -> {
val value = device.getOrReadProperty(request.payload.name)
respond(request) { requestPayload ->
requestPayload.copy(
value = value,
quality = TangoQuality.VALID
)
}
}
TangoAction.write -> {
request.payload.value?.let { value ->
device.writeProperty(request.payload.name, value)
}
//wait for value to be written and return final state
val value = device.getOrReadProperty(request.payload.name)
respond(request) { requestPayload ->
requestPayload.copy(
value = value,
quality = TangoQuality.VALID
)
}
}
TangoAction.exec -> {
val result = device.execute(request.payload.name, request.payload.argin)
respond(request) { requestPayload ->
requestPayload.copy(
argout = result,
quality = TangoQuality.VALID
)
}
}
TangoAction.pipe -> TODO("Pipe not implemented")
}
} catch (ex: Exception) {
logger.error(ex) { "Error while responding to message" }
endpoint.broadcast(
request.copy(
id = generateId(request),
parentId = request.id,
origin = endpointID,
payload = request.payload.copy(quality = TangoQuality.WARNING)
)
)
}
}.launchIn(this)
//TODO implement subscriptions?
// controller.messageOutput().onEach { payload ->
// endpoint.broadcast(
// MagixMessage(
// format = TANGO_MAGIX_FORMAT,
// id = "df[${payload.hashCode()}]",
// origin = endpointID,
// payload = payload
// )
// )
// }.catch { error ->
// logger.error(error) { "Error while sending a message" }
// }.launchIn(this)
}
}

@ -0,0 +1,17 @@
plugins {
id("ru.mipt.npm.gradle.jvm")
}
val ktorVersion: String by rootProject.extra
val miloVersion: String = "0.6.3"
dependencies {
api(project(":controls-core"))
api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${ru.mipt.npm.gradle.KScienceVersions.coroutinesVersion}")
api("org.eclipse.milo:sdk-client:$miloVersion")
api("org.eclipse.milo:bsd-parser:$miloVersion")
api("org.eclipse.milo:sdk-server:$miloVersion")
}

@ -0,0 +1,208 @@
package ru.mipt.npm.controls.opcua.client
import org.eclipse.milo.opcua.binaryschema.AbstractCodec
import org.eclipse.milo.opcua.binaryschema.parser.BsdParser
import org.eclipse.milo.opcua.stack.core.UaSerializationException
import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamDecoder
import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamEncoder
import org.eclipse.milo.opcua.stack.core.serialization.SerializationContext
import org.eclipse.milo.opcua.stack.core.serialization.codecs.OpcUaBinaryDataTypeCodec
import org.eclipse.milo.opcua.stack.core.types.builtin.*
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.*
import org.opcfoundation.opcua.binaryschema.EnumeratedType
import org.opcfoundation.opcua.binaryschema.StructuredType
import ru.mipt.npm.controls.misc.instant
import ru.mipt.npm.controls.misc.toMeta
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.values.*
import java.util.*
public class MetaBsdParser : BsdParser() {
override fun getEnumCodec(enumeratedType: EnumeratedType): OpcUaBinaryDataTypeCodec<*> {
return MetaEnumCodec()
}
override fun getStructCodec(structuredType: StructuredType): OpcUaBinaryDataTypeCodec<*> {
return MetaStructureCodec(structuredType)
}
}
internal class MetaEnumCodec : OpcUaBinaryDataTypeCodec<Number> {
override fun getType(): Class<Number> {
return Number::class.java
}
@Throws(UaSerializationException::class)
override fun encode(
context: SerializationContext,
encoder: OpcUaBinaryStreamEncoder,
value: Number
) {
encoder.writeInt32(value.toInt())
}
@Throws(UaSerializationException::class)
override fun decode(
context: SerializationContext,
decoder: OpcUaBinaryStreamDecoder
): Number {
return decoder.readInt32()
}
}
internal fun opcToMeta(value: Any?): Meta = when (value) {
null -> Meta(Null)
is Meta -> value
is Value -> Meta(value)
is Number -> when (value) {
is UByte -> Meta(value.toShort().asValue())
is UShort -> Meta(value.toInt().asValue())
is UInteger -> Meta(value.toLong().asValue())
is ULong -> Meta(value.toBigInteger().asValue())
else -> Meta(value.asValue())
}
is Boolean -> Meta(value.asValue())
is String -> Meta(value.asValue())
is Char -> Meta(value.toString().asValue())
is DateTime -> value.javaInstant.toMeta()
is UUID -> Meta(value.toString().asValue())
is QualifiedName -> Meta {
"namespaceIndex" put value.namespaceIndex
"name" put value.name?.asValue()
}
is LocalizedText -> Meta {
"locale" put value.locale?.asValue()
"text" put value.text?.asValue()
}
is DataValue -> Meta {
"value" put opcToMeta(value.value) // need SerializationContext to do that properly
value.statusCode?.value?.let { "status" put Meta(it.asValue()) }
value.sourceTime?.javaInstant?.let { "sourceTime" put it.toMeta() }
value.sourcePicoseconds?.let { "sourcePicoseconds" put Meta(it.asValue()) }
value.serverTime?.javaInstant?.let { "serverTime" put it.toMeta() }
value.serverPicoseconds?.let { "serverPicoseconds" put Meta(it.asValue()) }
}
is ByteString -> Meta(value.bytesOrEmpty().asValue())
is XmlElement -> Meta(value.fragment?.asValue() ?: Null)
is NodeId -> Meta(value.toParseableString().asValue())
is ExpandedNodeId -> Meta(value.toParseableString().asValue())
is StatusCode -> Meta(value.value.asValue())
//is ExtensionObject -> value.decode(client.getDynamicSerializationContext())
else -> error("Could not create Meta for value: $value")
}
/**
* based on https://github.com/eclipse/milo/blob/master/opc-ua-stack/bsd-parser-gson/src/main/java/org/eclipse/milo/opcua/binaryschema/gson/JsonStructureCodec.java
*/
internal class MetaStructureCodec(
structuredType: StructuredType?
) : AbstractCodec<Meta, Meta>(structuredType) {
override fun getType(): Class<Meta> = Meta::class.java
override fun createStructure(name: String, members: LinkedHashMap<String, Meta>): Meta = Meta {
members.forEach { (property: String, value: Meta?) ->
setMeta(Name.parse(property), value)
}
}
override fun opcUaToMemberTypeScalar(name: String, value: Any?, typeName: String): Meta = opcToMeta(value)
override fun opcUaToMemberTypeArray(name: String, values: Any?, typeName: String): Meta = if (values == null) {
Meta(Null)
} else {
// This is a bit array...
when (values) {
is DoubleArray -> Meta(values.asValue())
is FloatArray -> Meta(values.asValue())
is IntArray -> Meta(values.asValue())
is ByteArray -> Meta(values.asValue())
is ShortArray -> Meta(values.asValue())
is Array<*> -> Meta {
setIndexed(Name.parse(name), values.map { opcUaToMemberTypeScalar(name, it, typeName) })
}
is Number -> Meta(values.asValue())
else -> error("Could not create Meta for value: $values")
}
}
override fun memberTypeToOpcUaScalar(member: Meta?, typeName: String): Any? =
if (member == null || member.isEmpty()) {
null
} else when (typeName) {
"Boolean" -> member.boolean
"SByte" -> member.value?.numberOrNull?.toByte()
"Int16" -> member.value?.numberOrNull?.toShort()
"Int32" -> member.value?.numberOrNull?.toInt()
"Int64" -> member.value?.numberOrNull?.toLong()
"Byte" -> member.value?.numberOrNull?.toShort()?.let { Unsigned.ubyte(it) }
"UInt16" -> member.value?.numberOrNull?.toInt()?.let { Unsigned.ushort(it) }
"UInt32" -> member.value?.numberOrNull?.toLong()?.let { Unsigned.uint(it) }
"UInt64" -> member.value?.numberOrNull?.toLong()?.let { Unsigned.ulong(it) }
"Float" -> member.value?.numberOrNull?.toFloat()
"Double" -> member.value?.numberOrNull?.toDouble()
"String" -> member.string
"DateTime" -> DateTime(member.instant())
"Guid" -> member.string?.let { UUID.fromString(it) }
"ByteString" -> member.value?.list?.let { list ->
ByteString(list.map { it.number.toByte() }.toByteArray())
}
"XmlElement" -> member.string?.let { XmlElement(it) }
"NodeId" -> member.string?.let { NodeId.parse(it) }
"ExpandedNodeId" -> member.string?.let { ExpandedNodeId.parse(it) }
"StatusCode" -> member.long?.let { StatusCode(it) }
"QualifiedName" -> QualifiedName(
member["namespaceIndex"].int ?: 0,
member["name"].string
)
"LocalizedText" -> LocalizedText(
member["locale"].string,
member["text"].string
)
else -> member.toString()
}
override fun memberTypeToOpcUaArray(member: Meta, typeName: String): Any = if ("Bit" == typeName) {
member.value?.int ?: error("Meta node does not contain int value")
} else {
when (typeName) {
"SByte" -> member.value?.list?.map { it.number.toByte() }?.toByteArray() ?: emptyArray<Byte>()
"Int16" -> member.value?.list?.map { it.number.toShort() }?.toShortArray() ?: emptyArray<Short>()
"Int32" -> member.value?.list?.map { it.number.toInt() }?.toIntArray() ?: emptyArray<Int>()
"Int64" -> member.value?.list?.map { it.number.toLong() }?.toLongArray() ?: emptyArray<Long>()
"Byte" -> member.value?.list?.map {
Unsigned.ubyte(it.number.toShort())
}?.toTypedArray() ?: emptyArray<UByte>()
"UInt16" -> member.value?.list?.map {
Unsigned.ushort(it.number.toInt())
}?.toTypedArray() ?: emptyArray<UShort>()
"UInt32" -> member.value?.list?.map {
Unsigned.uint(it.number.toLong())
}?.toTypedArray() ?: emptyArray<UInteger>()
"UInt64" -> member.value?.list?.map {
Unsigned.ulong(it.number.toLong())
}?.toTypedArray() ?: emptyArray<kotlin.ULong>()
"Float" -> member.value?.list?.map { it.number.toFloat() }?.toFloatArray() ?: emptyArray<Float>()
"Double" -> member.value?.list?.map { it.number.toDouble() }?.toDoubleArray() ?: emptyArray<Double>()
else -> member.getIndexed(Meta.JSON_ARRAY_KEY.asName()).map {
memberTypeToOpcUaScalar(it.value, typeName)
}.toTypedArray()
}
}
override fun getMembers(value: Meta): Map<String, Meta> = value.items.mapKeys { it.toString() }
}
public fun Variant.toMeta(serializationContext: SerializationContext): Meta = (value as? ExtensionObject)?.let {
it.decode(serializationContext) as Meta
} ?: opcToMeta(value)
//public fun Meta.toVariant(): Variant = if (items.isEmpty()) {
// Variant(value?.value)
//} else {
// TODO()
//}

@ -0,0 +1,83 @@
package ru.mipt.npm.controls.opcua.client
import kotlinx.coroutines.future.await
import kotlinx.serialization.json.Json
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.stack.core.types.builtin.*
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn
import ru.mipt.npm.controls.api.Device
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.transformations.MetaConverter
/**
* An OPC-UA device backed by Eclipse Milo client
*/
public interface MiloDevice : Device {
/**
* The OPC-UA client initialized on first use
*/
public val client: OpcUaClient
override fun close() {
client.disconnect()
super.close()
}
}
/**
* Read OPC-UA value with timestamp
* @param T the type of property to read. The value is coerced to it.
*/
public suspend inline fun <reified T: Any> MiloDevice.readOpcWithTime(
nodeId: NodeId,
converter: MetaConverter<T>,
magAge: Double = 500.0
): Pair<T, DateTime> {
val data = client.readValue(magAge, TimestampsToReturn.Server, nodeId).await()
val time = data.serverTime ?: error("No server time provided")
val meta: Meta = when (val content = data.value.value) {
is T -> return content to time
content is Meta -> content as Meta
content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta
else -> error("Incompatible OPC property value $content")
}
val res: T = converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
return res to time
}
/**
* Read and coerce value from OPC-UA
*/
public suspend inline fun <reified T> MiloDevice.readOpc(
nodeId: NodeId,
converter: MetaConverter<T>,
magAge: Double = 500.0
): T {
val data: DataValue = client.readValue(magAge, TimestampsToReturn.Neither, nodeId).await()
val content = data.value.value
if(content is T) return content
val meta: Meta = when (content) {
is Meta -> content
//Always decode string as Json meta
is String -> Json.decodeFromString(MetaSerializer, content)
is Number -> Meta(content)
is Boolean -> Meta(content)
//content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta
else -> error("Incompatible OPC property value $content")
}
return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
}
public suspend inline fun <reified T> MiloDevice.writeOpc(
nodeId: NodeId,
converter: MetaConverter<T>,
value: T
): StatusCode {
val meta = converter.objectToMeta(value)
return client.writeValue(nodeId, DataValue(Variant(meta))).await()
}

@ -0,0 +1,69 @@
package ru.mipt.npm.controls.opcua.client
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId
import ru.mipt.npm.controls.properties.DeviceBySpec
import ru.mipt.npm.controls.properties.DeviceSpec
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
public open class MiloDeviceBySpec<D : MiloDeviceBySpec<D>>(
spec: DeviceSpec<D>,
context: Context = Global,
meta: Meta = Meta.EMPTY
) : MiloDevice, DeviceBySpec<D>(spec, context, meta) {
override val client: OpcUaClient by lazy {
val endpointUrl = meta["endpointUrl"].string ?: error("Endpoint url is not defined")
context.createMiloClient(endpointUrl).apply {
connect().get()
}
}
override fun close() {
super<MiloDevice>.close()
super<DeviceBySpec>.close()
}
}
/**
* A device-bound OPC-UA property. Does not trigger device properties change.
*/
public inline fun <reified T> MiloDeviceBySpec<*>.opc(
nodeId: NodeId,
converter: MetaConverter<T>,
magAge: Double = 500.0
): ReadWriteProperty<Any?, T> = object : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking {
readOpc(nodeId, converter, magAge)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
launch {
writeOpc(nodeId, converter, value)
}
}
}
public inline fun <reified T> MiloDeviceBySpec<*>.opcDouble(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, Double> = opc(nodeId, MetaConverter.double, magAge)
public inline fun <reified T> MiloDeviceBySpec<*>.opcInt(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, Int> = opc(nodeId, MetaConverter.int, magAge)
public inline fun <reified T> MiloDeviceBySpec<*>.opcString(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, String> = opc(nodeId, MetaConverter.string, magAge)

@ -0,0 +1,63 @@
package ru.mipt.npm.controls.opcua.client
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider
import org.eclipse.milo.opcua.sdk.client.api.identity.IdentityProvider
import org.eclipse.milo.opcua.stack.client.security.DefaultClientCertificateValidator
import org.eclipse.milo.opcua.stack.core.security.DefaultTrustListManager
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.info
import space.kscience.dataforge.context.logger
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
public fun <T:Any> T?.toOptional(): Optional<T> = if(this == null) Optional.empty() else Optional.of(this)
internal fun Context.createMiloClient(
endpointUrl: String, //"opc.tcp://localhost:12686/milo"
securityPolicy: SecurityPolicy = SecurityPolicy.Basic256Sha256,
identityProvider: IdentityProvider = AnonymousProvider(),
endpointFilter: (EndpointDescription?) -> Boolean = { securityPolicy.uri == it?.securityPolicyUri }
): OpcUaClient {
val securityTempDir: Path = Paths.get(System.getProperty("java.io.tmpdir"), "client", "security")
Files.createDirectories(securityTempDir)
check(Files.exists(securityTempDir)) { "Unable to create security dir: $securityTempDir" }
val pkiDir: Path = securityTempDir.resolve("pki")
logger.info { "Milo client security dir: ${securityTempDir.toAbsolutePath()}" }
logger.info { "Security pki dir: ${pkiDir.toAbsolutePath()}" }
//val loader: KeyStoreLoader = KeyStoreLoader().load(securityTempDir)
val trustListManager = DefaultTrustListManager(pkiDir.toFile())
val certificateValidator = DefaultClientCertificateValidator(trustListManager)
return OpcUaClient.create(
endpointUrl,
{ endpoints: List<EndpointDescription?> ->
endpoints.firstOrNull(endpointFilter).toOptional()
}
) { configBuilder: OpcUaClientConfigBuilder ->
configBuilder
.setApplicationName(LocalizedText.english("Controls.kt"))
.setApplicationUri("urn:ru.mipt:npm:controls:opcua")
// .setKeyPair(loader.getClientKeyPair())
// .setCertificate(loader.getClientCertificate())
// .setCertificateChain(loader.getClientCertificateChain())
.setCertificateValidator(certificateValidator)
.setIdentityProvider(identityProvider)
.setRequestTimeout(uint(5000))
.build()
}
// .apply {
// addSessionInitializer(DataTypeDictionarySessionInitializer(MetaBsdParser()))
// }
}

@ -0,0 +1,212 @@
package ru.mipt.npm.controls.opcua.server
import kotlinx.coroutines.launch
import kotlinx.datetime.toJavaInstant
import kotlinx.serialization.json.Json
import org.eclipse.milo.opcua.sdk.core.AccessLevel
import org.eclipse.milo.opcua.sdk.core.Reference
import org.eclipse.milo.opcua.sdk.server.Lifecycle
import org.eclipse.milo.opcua.sdk.server.OpcUaServer
import org.eclipse.milo.opcua.sdk.server.api.DataItem
import org.eclipse.milo.opcua.sdk.server.api.ManagedNamespaceWithLifecycle
import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem
import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode
import org.eclipse.milo.opcua.sdk.server.nodes.UaNode
import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode
import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel
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 ru.mipt.npm.controls.api.Device
import ru.mipt.npm.controls.api.DeviceHub
import ru.mipt.npm.controls.api.PropertyDescriptor
import ru.mipt.npm.controls.api.onPropertyChange
import ru.mipt.npm.controls.controllers.DeviceManager
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.plus
import space.kscience.dataforge.values.ValueType
public operator fun Device.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name)
public suspend fun Device.read(propertyDescriptor: PropertyDescriptor): Meta = readProperty(propertyDescriptor.name)
/*
https://github.com/eclipse/milo/blob/master/milo-examples/server-examples/src/main/java/org/eclipse/milo/examples/server/ExampleNamespace.java
*/
public class DeviceNameSpace(
server: OpcUaServer,
public val deviceManager: DeviceManager
) : ManagedNamespaceWithLifecycle(server, NAMESPACE_URI) {
private val subscription = SubscriptionModel(server, this)
init {
lifecycleManager.addLifecycle(subscription)
lifecycleManager.addStartupTask {
deviceManager.devices.forEach { (deviceName, device) ->
val tokenAsString = deviceName.toString()
val deviceFolder = UaFolderNode(
this.nodeContext,
newNodeId(tokenAsString),
newQualifiedName(tokenAsString),
LocalizedText.english(tokenAsString)
)
deviceFolder.addReference(
Reference(
deviceFolder.nodeId,
Identifiers.Organizes,
Identifiers.ObjectsFolder.expanded(),
false
)
)
deviceFolder.registerDeviceNodes(deviceName.asName(), device)
this.nodeManager.addNode(deviceFolder)
}
}
lifecycleManager.addLifecycle(object : Lifecycle {
override fun startup() {
server.addressSpaceManager.register(this@DeviceNameSpace)
}
override fun shutdown() {
server.addressSpaceManager.unregister(this@DeviceNameSpace)
}
})
}
private fun UaFolderNode.registerDeviceNodes(deviceName: Name, device: Device) {
val nodes = device.propertyDescriptors.associate { descriptor ->
val propertyName = descriptor.name
val node: UaVariableNode = UaVariableNode.UaVariableNodeBuilder(nodeContext).apply {
//for now use DF path as id
nodeId = newNodeId("${deviceName.tokens.joinToString("/")}/$propertyName")
when {
descriptor.readable && descriptor.writable -> {
setAccessLevel(AccessLevel.READ_WRITE)
setUserAccessLevel(AccessLevel.READ_WRITE)
}
descriptor.writable -> {
setAccessLevel(AccessLevel.WRITE_ONLY)
setUserAccessLevel(AccessLevel.WRITE_ONLY)
}
descriptor.readable -> {
setAccessLevel(AccessLevel.READ_ONLY)
setUserAccessLevel(AccessLevel.READ_ONLY)
}
else -> {
setAccessLevel(AccessLevel.NONE)
setUserAccessLevel(AccessLevel.NONE)
}
}
browseName = newQualifiedName(propertyName)
displayName = LocalizedText.english(propertyName)
dataType = if (descriptor.metaDescriptor.children.isNotEmpty()) {
Identifiers.String
} else when (descriptor.metaDescriptor.valueTypes?.first()) {
null, ValueType.STRING, ValueType.NULL -> Identifiers.String
ValueType.NUMBER -> Identifiers.Number
ValueType.BOOLEAN -> Identifiers.Boolean
ValueType.LIST -> Identifiers.ArrayItemType
}
setTypeDefinition(Identifiers.BaseDataVariableType)
}.build()
device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let {
node.value = it
}
/**
* Subscribe to node value changes
*/
node.addAttributeObserver { _: UaNode, attributeId: AttributeId, value: Any ->
if (attributeId == AttributeId.Value) {
val meta: Meta = when (value) {
is Meta -> value
is Boolean -> Meta(value)
is Number -> Meta(value)
is String -> Json.decodeFromString(MetaSerializer, value)
else -> return@addAttributeObserver //TODO("other types not implemented")
}
deviceManager.context.launch {
device.writeProperty(propertyName, meta)
}
}
}
nodeManager.addNode(node)
addOrganizes(node)
propertyName to node
}
//Subscribe on properties updates
device.onPropertyChange {
nodes[property]?.let { node ->
val sourceTime = time?.let { DateTime(it.toJavaInstant()) }
node.value = value.toOpc(sourceTime = sourceTime)
}
}
//recursively add sub-devices
if (device is DeviceHub) {
registerHub(device, deviceName)
}
}
private fun UaNode.registerHub(hub: DeviceHub, namePrefix: Name) {
hub.devices.forEach { (deviceName, device) ->
val tokenAsString = deviceName.toString()
val deviceFolder = UaFolderNode(
this.nodeContext,
newNodeId(tokenAsString),
newQualifiedName(tokenAsString),
LocalizedText.english(tokenAsString)
)
deviceFolder.addReference(
Reference(
deviceFolder.nodeId,
Identifiers.Organizes,
Identifiers.ObjectsFolder.expanded(),
false
)
)
deviceFolder.registerDeviceNodes(namePrefix + deviceName, device)
this.nodeManager.addNode(deviceFolder)
}
}
override fun onDataItemsCreated(dataItems: List<DataItem?>?) {
subscription.onDataItemsCreated(dataItems)
}
override fun onDataItemsModified(dataItems: List<DataItem?>?) {
subscription.onDataItemsModified(dataItems)
}
override fun onDataItemsDeleted(dataItems: List<DataItem?>?) {
subscription.onDataItemsDeleted(dataItems)
}
override fun onMonitoringModeChanged(monitoredItems: List<MonitoredItem?>?) {
subscription.onMonitoringModeChanged(monitoredItems)
}
public companion object {
public const val NAMESPACE_URI: String = "urn:space:kscience:controls:opcua:server"
}
}
public fun OpcUaServer.serveDevices(deviceManager: DeviceManager): DeviceNameSpace =
DeviceNameSpace(this, deviceManager).apply { startup() }

@ -0,0 +1,38 @@
package ru.mipt.npm.controls.opcua.server
import kotlinx.serialization.json.Json
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.isLeaf
import space.kscience.dataforge.values.*
import java.time.Instant
/**
* Convert Meta to OPC data value using
*/
internal fun Meta.toOpc(
statusCode: StatusCode = StatusCode.GOOD,
sourceTime: DateTime? = null,
serverTime: DateTime? = null
): DataValue {
val variant: Variant = if (isLeaf) {
when (value?.type) {
null, ValueType.NULL -> Variant.NULL_VALUE
ValueType.NUMBER -> Variant(value!!.number)
ValueType.STRING -> Variant(value!!.string)
ValueType.BOOLEAN -> Variant(value!!.boolean)
ValueType.LIST -> if (value!!.list.all { it.type == ValueType.NUMBER }) {
Variant(value!!.doubleArray.toTypedArray())
} else {
Variant(value!!.stringList.toTypedArray())
}
}
} else {
Variant(Json.encodeToString(MetaSerializer,this))
}
return DataValue(variant, statusCode, sourceTime,serverTime ?: DateTime(Instant.now()))
}

@ -0,0 +1,77 @@
package ru.mipt.npm.controls.opcua.server
import org.eclipse.milo.opcua.sdk.core.AccessLevel
import org.eclipse.milo.opcua.sdk.core.Reference
import org.eclipse.milo.opcua.sdk.server.nodes.UaNode
import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext
import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode
import org.eclipse.milo.opcua.stack.core.Identifiers
import org.eclipse.milo.opcua.stack.core.types.builtin.*
internal fun UaNode.inverseReferenceTo(targetNodeId: NodeId, typeId: NodeId) {
addReference(
Reference(
nodeId,
typeId,
targetNodeId.expanded(),
Reference.Direction.INVERSE
)
)
}
internal fun NodeId.resolve(child: String): NodeId {
val id = this.identifier.toString()
return NodeId(this.namespaceIndex, "$id/$child")
}
internal fun UaNodeContext.addVariableNode(
parentNodeId: NodeId,
name: String,
nodeId: NodeId = parentNodeId.resolve(name),
dataTypeId: NodeId,
value: Any,
referenceTypeId: NodeId = Identifiers.HasComponent
): UaVariableNode {
val variableNode: UaVariableNode = UaVariableNode.UaVariableNodeBuilder(this).apply {
setNodeId(nodeId)
setAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE))
setUserAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE))
setBrowseName(QualifiedName(parentNodeId.namespaceIndex, name))
setDisplayName(LocalizedText.english(name))
setDataType(dataTypeId)
setTypeDefinition(Identifiers.BaseDataVariableType)
setMinimumSamplingInterval(100.0)
setValue(DataValue(Variant(value)))
}.build()
// variableNode.filterChain.addFirst(AttributeLoggingFilter())
nodeManager.addNode(variableNode)
variableNode.inverseReferenceTo(
parentNodeId,
referenceTypeId
)
return variableNode
}
//
//fun UaNodeContext.addVariableNode(
// parentNodeId: NodeId,
// name: String,
// nodeId: NodeId = parentNodeId.resolve(name),
// dataType: BuiltinDataType = BuiltinDataType.Int32,
// referenceTypeId: NodeId = Identifiers.HasComponent
//): UaVariableNode = addVariableNode(
// parentNodeId,
// name,
// nodeId,
// dataType.nodeId,
// dataType.defaultValue(),
// referenceTypeId
//)

@ -0,0 +1,28 @@
package ru.mipt.npm.controls.opcua.server
import org.eclipse.milo.opcua.sdk.server.OpcUaServer
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfigBuilder
import org.eclipse.milo.opcua.stack.server.EndpointConfiguration
public fun OpcUaServer(block: OpcUaServerConfigBuilder.() -> Unit): OpcUaServer {
// .setProductUri(DemoServer.PRODUCT_URI)
// .setApplicationUri("${DemoServer.APPLICATION_URI}:$applicationUuid")
// .setApplicationName(LocalizedText.english("Eclipse Milo OPC UA Demo Server"))
// .setBuildInfo(buildInfo())
// .setTrustListManager(trustListManager)
// .setCertificateManager(certificateManager)
// .setCertificateValidator(certificateValidator)
// .setIdentityValidator(identityValidator)
// .setEndpoints(endpoints)
// .setLimits(ServerLimits)
val config = OpcUaServerConfig.builder().apply(block)
return OpcUaServer(config.build())
}
public fun OpcUaServerConfigBuilder.endpoint(block: EndpointConfiguration.Builder.() -> Unit) {
val endpoint = EndpointConfiguration.Builder().apply(block).build()
setEndpoints(setOf(endpoint))
}

@ -0,0 +1,9 @@
plugins {
id("ru.mipt.npm.gradle.jvm")
`maven-publish`
}
dependencies{
api(project(":controls-core"))
implementation("org.scream3r:jssc:2.8.0")
}

@ -0,0 +1,89 @@
package ru.mipt.npm.controls.serial
import jssc.SerialPort.*
import jssc.SerialPortEventListener
import ru.mipt.npm.controls.ports.AbstractPort
import ru.mipt.npm.controls.ports.Port
import ru.mipt.npm.controls.ports.PortFactory
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.string
import kotlin.coroutines.CoroutineContext
import jssc.SerialPort as JSSCPort
/**
* COM/USB port
*/
public class SerialPort private constructor(
context: Context,
private val jssc: JSSCPort,
coroutineContext: CoroutineContext = context.coroutineContext,
) : AbstractPort(context, coroutineContext) {
override fun toString(): String = "port[${jssc.portName}]"
private val serialPortListener = SerialPortEventListener { event ->
if (event.isRXCHAR) {
val chars = event.eventValue
val bytes = jssc.readBytes(chars)
receive(bytes)
}
}
init {
jssc.addEventListener(serialPortListener)
}
/**
* Clear current input and output buffers
*/
internal fun clearPort() {
jssc.purgePort(PURGE_RXCLEAR or PURGE_TXCLEAR)
}
override suspend fun write(data: ByteArray) {
jssc.writeBytes(data)
}
@Throws(Exception::class)
override fun close() {
jssc.removeEventListener()
clearPort()
if (jssc.isOpened) {
jssc.closePort()
}
super.close()
}
public companion object : PortFactory {
/**
* Construct ComPort with given parameters
*/
public fun open(
context: Context,
portName: String,
baudRate: Int = BAUDRATE_9600,
dataBits: Int = DATABITS_8,
stopBits: Int = STOPBITS_1,
parity: Int = PARITY_NONE,
coroutineContext: CoroutineContext = context.coroutineContext,
): SerialPort {
val jssc = JSSCPort(portName).apply {
openPort()
setParams(baudRate, dataBits, stopBits, parity)
}
return SerialPort(context, jssc, coroutineContext)
}
override fun invoke(meta: Meta, context: Context): Port {
val name by meta.string { error("Serial port name not defined") }
val baudRate by meta.int(BAUDRATE_9600)
val dataBits by meta.int(DATABITS_8)
val stopBits by meta.int(STOPBITS_1)
val parity by meta.int(PARITY_NONE)
return open(context, name, baudRate, dataBits, stopBits, parity)
}
}
}

@ -0,0 +1,21 @@
plugins {
id("ru.mipt.npm.gradle.jvm")
`maven-publish`
}
description = """
A magix event loop server with web server for visualization.
""".trimIndent()
val dataforgeVersion: String by rootProject.extra
val ktorVersion: String by rootProject.extra
dependencies {
implementation(project(":controls-core"))
implementation(project(":controls-tcp"))
implementation(projects.magix.magixServer)
implementation("io.ktor:ktor-server-cio:$ktorVersion")
implementation("io.ktor:ktor-websockets:$ktorVersion")
implementation("io.ktor:ktor-serialization:$ktorVersion")
implementation("io.ktor:ktor-html-builder:$ktorVersion")
}

@ -0,0 +1,223 @@
package ru.mipt.npm.controls.server
import io.ktor.application.*
import io.ktor.features.CORS
import io.ktor.features.StatusPages
import io.ktor.html.respondHtml
import io.ktor.http.HttpStatusCode
import io.ktor.request.receiveText
import io.ktor.response.respond
import io.ktor.response.respondRedirect
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer
import io.ktor.util.getValue
import io.ktor.websocket.WebSockets
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.html.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put
import ru.mipt.npm.controls.api.DeviceMessage
import ru.mipt.npm.controls.api.PropertyGetMessage
import ru.mipt.npm.controls.api.PropertySetMessage
import ru.mipt.npm.controls.api.getOrNull
import ru.mipt.npm.controls.controllers.DeviceManager
import ru.mipt.npm.controls.controllers.respondHubMessage
import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.server.GenericMagixMessage
import ru.mipt.npm.magix.server.launchMagixServerRawRSocket
import ru.mipt.npm.magix.server.magixModule
import space.kscience.dataforge.meta.toMeta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
/**
* Create and start a web server for several devices
*/
public fun CoroutineScope.startDeviceServer(
manager: DeviceManager,
port: Int = MagixEndpoint.DEFAULT_MAGIX_HTTP_PORT,
host: String = "localhost",
): ApplicationEngine {
return this.embeddedServer(CIO, port, host) {
install(WebSockets)
install(CORS) {
anyHost()
}
install(StatusPages) {
exception<IllegalArgumentException> { cause ->
call.respond(HttpStatusCode.BadRequest, cause.message ?: "")
}
}
deviceManagerModule(manager)
routing {
get("/") {
call.respondRedirect("/dashboard")
}
}
}.start()
}
public fun ApplicationEngine.whenStarted(callback: Application.() -> Unit) {
environment.monitor.subscribe(ApplicationStarted, callback)
}
public val WEB_SERVER_TARGET: Name = "@webServer".asName()
public fun Application.deviceManagerModule(
manager: DeviceManager,
deviceNames: Collection<String> = manager.devices.keys.map { it.toString() },
route: String = "/",
rawSocketPort: Int = MagixEndpoint.DEFAULT_MAGIX_RAW_PORT,
buffer: Int = 100,
) {
if (featureOrNull(WebSockets) == null) {
install(WebSockets)
}
if (featureOrNull(CORS) == null) {
install(CORS) {
anyHost()
}
}
routing {
route(route) {
get("dashboard") {
call.respondHtml {
head {
title("Device server dashboard")
}
body {
h1 {
+"Device server dashboard"
}
deviceNames.forEach { deviceName ->
val device =
manager.getOrNull(deviceName)
?: error("The device with name $deviceName not found in $manager")
div {
id = deviceName
h2 { +deviceName }
h3 { +"Properties" }
ul {
device.propertyDescriptors.forEach { property ->
li {
a(href = "../$deviceName/${property.name}/get") { +"${property.name}: " }
code {
+Json.encodeToString(property)
}
}
}
}
h3 { +"Actions" }
ul {
device.actionDescriptors.forEach { action ->
li {
+("${action.name}: ")
code {
+Json.encodeToString(action)
}
}
}
}
}
}
}
}
}
get("list") {
call.respondJson {
manager.devices.forEach { (name, device) ->
put("target", name.toString())
put("properties", buildJsonArray {
device.propertyDescriptors.forEach { descriptor ->
add(Json.encodeToJsonElement(descriptor))
}
})
put("actions", buildJsonArray {
device.actionDescriptors.forEach { actionDescriptor ->
add(Json.encodeToJsonElement(actionDescriptor))
}
})
}
}
}
post("message") {
val body = call.receiveText()
val request: DeviceMessage = MagixEndpoint.magixJson.decodeFromString(DeviceMessage.serializer(), body)
val response = manager.respondHubMessage(request)
if (response != null) {
call.respondMessage(response)
} else {
call.respondText("No response")
}
}
route("{target}") {
//global route for the device
route("{property}") {
get("get") {
val target: String by call.parameters
val property: String by call.parameters
val request = PropertyGetMessage(
sourceDevice = WEB_SERVER_TARGET,
targetDevice = Name.parse(target),
property = property,
)
val response = manager.respondHubMessage(request)
if (response != null) {
call.respondMessage(response)
} else {
call.respond(HttpStatusCode.InternalServerError)
}
}
post("set") {
val target: String by call.parameters
val property: String by call.parameters
val body = call.receiveText()
val json = Json.parseToJsonElement(body)
val request = PropertySetMessage(
sourceDevice = WEB_SERVER_TARGET,
targetDevice = Name.parse(target),
property = property,
value = json.toMeta()
)
val response = manager.respondHubMessage(request)
if (response != null) {
call.respondMessage(response)
} else {
call.respond(HttpStatusCode.InternalServerError)
}
}
}
}
}
}
val magixFlow = MutableSharedFlow<GenericMagixMessage>(
buffer,
extraBufferCapacity = buffer
)
launchMagixServerRawRSocket(magixFlow, rawSocketPort)
magixModule(magixFlow)
}

@ -0,0 +1,31 @@
package ru.mipt.npm.controls.server
import io.ktor.application.ApplicationCall
import io.ktor.http.ContentType
import io.ktor.response.respondText
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.buildJsonObject
import ru.mipt.npm.controls.api.DeviceMessage
import ru.mipt.npm.magix.api.MagixEndpoint
//internal fun Frame.toEnvelope(): Envelope {
// return data.asBinary().readWith(TaggedEnvelopeFormat)
//}
//
//internal fun Envelope.toFrame(): Frame {
// val data = buildByteArray {
// writeWith(TaggedEnvelopeFormat, this@toFrame)
// }
// return Frame.Binary(false, data)
//}
internal suspend fun ApplicationCall.respondJson(builder: JsonObjectBuilder.() -> Unit) {
val json = buildJsonObject(builder)
respondText(json.toString(), contentType = ContentType.Application.Json)
}
internal suspend fun ApplicationCall.respondMessage(message: DeviceMessage): Unit = respondText(
MagixEndpoint.magixJson.encodeToString(DeviceMessage.serializer(), message),
contentType = ContentType.Application.Json
)

@ -0,0 +1,17 @@
plugins {
id("ru.mipt.npm.gradle.mpp")
}
val ktorVersion: String by rootProject.extra
kotlin {
sourceSets {
commonMain {
dependencies {
api(project(":controls-core"))
api("io.ktor:ktor-network:$ktorVersion")
}
}
}
}

@ -0,0 +1,75 @@
package ru.mipt.npm.controls.ports
import io.ktor.network.selector.ActorSelectorManager
import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel
import io.ktor.utils.io.consumeEachBufferRange
import io.ktor.utils.io.core.Closeable
import io.ktor.utils.io.writeAvailable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.string
import java.net.InetSocketAddress
import kotlin.coroutines.CoroutineContext
public class KtorTcpPort internal constructor(
context: Context,
public val host: String,
public val port: Int,
coroutineContext: CoroutineContext = context.coroutineContext,
) : AbstractPort(context, coroutineContext), Closeable {
override fun toString(): String = "port[tcp:$host:$port]"
private val futureSocket = scope.async {
aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(InetSocketAddress(host, port))
}
private val writeChannel = scope.async {
futureSocket.await().openWriteChannel(true)
}
private val listenerJob = scope.launch {
val input = futureSocket.await().openReadChannel()
input.consumeEachBufferRange { buffer, last ->
val array = ByteArray(buffer.remaining())
buffer.get(array)
receive(array)
isActive
}
}
override suspend fun write(data: ByteArray) {
writeChannel.await().writeAvailable(data)
}
override fun close() {
listenerJob.cancel()
futureSocket.cancel()
super.close()
}
public companion object: PortFactory {
public fun open(
context: Context,
host: String,
port: Int,
coroutineContext: CoroutineContext = context.coroutineContext,
): KtorTcpPort {
return KtorTcpPort(context, host, port, coroutineContext)
}
override fun invoke(meta: Meta, context: Context): Port {
val host = meta["host"].string ?: "localhost"
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
return open(context, host, port)
}
}
}

@ -1,18 +0,0 @@
plugins {
id("scientifik.mpp")
id("scientifik.publish")
}
val ktorVersion: String by extra("1.3.2")
kotlin {
sourceSets {
commonMain{
dependencies {
implementation(project(":dataforge-device-core"))
implementation("io.ktor:ktor-client-cio:$ktorVersion")
}
}
}
}

@ -1,23 +0,0 @@
import scientifik.useCoroutines
import scientifik.useSerialization
plugins {
id("scientifik.mpp")
id("scientifik.publish")
}
val dataforgeVersion: String by rootProject.extra
useCoroutines(version = "1.3.7")
useSerialization()
kotlin {
sourceSets {
commonMain{
dependencies {
api("hep.dataforge:dataforge-io:$dataforgeVersion")
//implementation("org.jetbrains.kotlinx:atomicfu-common:0.14.3")
}
}
}
}

@ -1,69 +0,0 @@
package hep.dataforge.control.api
import hep.dataforge.meta.Meta
import hep.dataforge.meta.MetaItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.io.Closeable
/**
* General interface describing a managed Device
*/
interface Device: Closeable {
/**
* List of supported property descriptors
*/
val propertyDescriptors: Collection<PropertyDescriptor>
/**
* List of supported action descriptors. Action is a request to the device that
* may or may not change the properties
*/
val actionDescriptors: Collection<ActionDescriptor>
/**
* The scope encompassing all operations on a device. When canceled, cancels all running processes
*/
val scope: CoroutineScope
/**
* Register a new property change listener for this device.
* [owner] is provided optionally in order for listener to be
* easily removable
*/
fun registerListener(listener: DeviceListener, owner: Any? = listener)
/**
* Remove all listeners belonging to the specified owner
*/
fun removeListeners(owner: Any?)
/**
* Get the value of the property or throw error if property in not defined.
* Suspend if property value is not available
*/
suspend fun getProperty(propertyName: String): MetaItem<*>
/**
* Invalidate property and force recalculate
*/
suspend fun invalidateProperty(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.
*/
suspend fun setProperty(propertyName: String, value: MetaItem<*>)
/**
* Send an action request and suspend caller while request is being processed.
* Could return null if request does not return a meaningful answer.
*/
suspend fun exec(action: String, argument: MetaItem<*>? = null): MetaItem<*>?
override fun close() {
scope.cancel("The device is closed")
}
}
suspend fun Device.exec(name: String, meta: Meta?) = exec(name, meta?.let { MetaItem.NodeItem(it) })

@ -1,23 +0,0 @@
package hep.dataforge.control.api
import hep.dataforge.meta.MetaItem
/**
* A hub that could locate multiple devices and redirect actions to them
*/
interface DeviceHub {
fun getDevice(deviceName: String): Device?
}
suspend fun DeviceHub.getProperty(deviceName: String, propertyName: String): MetaItem<*> =
(getDevice(deviceName) ?: error("Device with name $deviceName not found in the hub"))
.getProperty(propertyName)
suspend fun DeviceHub.setProperty(deviceName: String, propertyName: String, value: MetaItem<*>) {
(getDevice(deviceName) ?: error("Device with name $deviceName not found in the hub"))
.setProperty(propertyName, value)
}
suspend fun DeviceHub.exec(deviceName: String, command: String, argument: MetaItem<*>?): MetaItem<*>? =
(getDevice(deviceName) ?: error("Device with name $deviceName not found in the hub"))
.exec(command, argument)

@ -1,12 +0,0 @@
package hep.dataforge.control.api
import hep.dataforge.meta.MetaItem
/**
* PropertyChangeListener Interface
* [value] is a new value that property has after a change; null is for invalid state.
*/
interface DeviceListener {
fun propertyChanged(propertyName: String, value: MetaItem<*>?)
//TODO add general message listener method
}

@ -1,20 +0,0 @@
package hep.dataforge.control.api
import hep.dataforge.meta.Scheme
import hep.dataforge.meta.string
/**
* A descriptor for property
*/
class PropertyDescriptor(name: String) : Scheme() {
val name by string(name)
}
/**
* A descriptor for property
*/
class ActionDescriptor(name: String) : Scheme() {
val name by string(name)
//var descriptor by spec(ItemDescriptor)
}

@ -1,64 +0,0 @@
package hep.dataforge.control.base
import hep.dataforge.control.api.ActionDescriptor
import hep.dataforge.meta.MetaBuilder
import hep.dataforge.meta.MetaItem
import hep.dataforge.values.Value
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
interface Action {
val name: String
val descriptor: ActionDescriptor
suspend operator fun invoke(arg: MetaItem<*>? = null): MetaItem<*>?
}
class SimpleAction(
override val name: String,
override val descriptor: ActionDescriptor,
val block: suspend (MetaItem<*>?) -> MetaItem<*>?
) : Action {
override suspend fun invoke(arg: MetaItem<*>?): MetaItem<*>? = block(arg)
}
class ActionDelegate<D : DeviceBase>(
val owner: D,
val descriptorBuilder: ActionDescriptor.()->Unit = {},
val block: suspend (MetaItem<*>?) -> MetaItem<*>?
) : ReadOnlyProperty<D, Action> {
override fun getValue(thisRef: D, property: KProperty<*>): Action {
val name = property.name
return owner.resolveAction(name) {
SimpleAction(name, ActionDescriptor(name).apply(descriptorBuilder), block)
}
}
}
fun <D : DeviceBase> D.request(
descriptorBuilder: ActionDescriptor.()->Unit = {},
block: suspend (MetaItem<*>?) -> MetaItem<*>?
): ActionDelegate<D> = ActionDelegate(this, descriptorBuilder, block)
fun <D : DeviceBase> D.requestValue(
descriptorBuilder: ActionDescriptor.()->Unit = {},
block: suspend (MetaItem<*>?) -> Any?
): ActionDelegate<D> = ActionDelegate(this, descriptorBuilder){
val res = block(it)
MetaItem.ValueItem(Value.of(res))
}
fun <D : DeviceBase> D.requestMeta(
descriptorBuilder: ActionDescriptor.()->Unit = {},
block: suspend MetaBuilder.(MetaItem<*>?) -> Unit
): ActionDelegate<D> = ActionDelegate(this, descriptorBuilder){
val res = MetaBuilder().apply { block(it)}
MetaItem.NodeItem(res)
}
fun <D : DeviceBase> D.action(
descriptorBuilder: ActionDescriptor.()->Unit = {},
block: suspend (MetaItem<*>?) -> Unit
): ActionDelegate<D> = ActionDelegate(this, descriptorBuilder) {
block(it)
null
}

@ -1,67 +0,0 @@
package hep.dataforge.control.base
import hep.dataforge.control.api.ActionDescriptor
import hep.dataforge.control.api.Device
import hep.dataforge.control.api.DeviceListener
import hep.dataforge.control.api.PropertyDescriptor
import hep.dataforge.meta.MetaItem
/**
* Baseline implementation of [Device] interface
*/
abstract class DeviceBase : Device {
private val properties = HashMap<String, ReadOnlyDeviceProperty>()
private val actions = HashMap<String, Action>()
private val listeners = ArrayList<Pair<Any?, DeviceListener>>(4)
override fun registerListener(listener: DeviceListener, owner: Any?) {
listeners.add(owner to listener)
}
override fun removeListeners(owner: Any?) {
listeners.removeAll { it.first == owner }
}
internal fun propertyChanged(propertyName: String, value: MetaItem<*>?) {
listeners.forEach { it.second.propertyChanged(propertyName, value) }
}
override val propertyDescriptors: Collection<PropertyDescriptor>
get() = properties.values.map { it.descriptor }
override val actionDescriptors: Collection<ActionDescriptor>
get() = actions.values.map { it.descriptor }
internal fun resolveProperty(name: String, builder: () -> ReadOnlyDeviceProperty): ReadOnlyDeviceProperty {
return properties.getOrPut(name, builder)
}
internal fun resolveAction(name: String, builder: () -> Action): Action {
return actions.getOrPut(name, builder)
}
override suspend fun getProperty(propertyName: String): MetaItem<*> =
(properties[propertyName] ?: error("Property with name $propertyName not defined")).read()
override suspend fun invalidateProperty(propertyName: String) {
(properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate()
}
override suspend fun setProperty(propertyName: String, value: MetaItem<*>) {
(properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write(
value
)
}
override suspend fun exec(action: String, argument: MetaItem<*>?): MetaItem<*>? =
(actions[action] ?: error("Request with name $action not defined")).invoke(argument)
companion object {
}
}

@ -1,257 +0,0 @@
package hep.dataforge.control.base
import hep.dataforge.control.api.PropertyDescriptor
import hep.dataforge.meta.Meta
import hep.dataforge.meta.MetaBuilder
import hep.dataforge.meta.MetaItem
import hep.dataforge.meta.double
import hep.dataforge.values.Value
import hep.dataforge.values.asValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* A stand-alone [ReadOnlyDeviceProperty] implementation not directly attached to a device
*/
@OptIn(ExperimentalCoroutinesApi::class)
open class IsolatedReadOnlyDeviceProperty(
override val name: String,
default: MetaItem<*>?,
override val descriptor: PropertyDescriptor,
override val scope: CoroutineScope,
private val updateCallback: (name: String, item: MetaItem<*>) -> Unit,
private val getter: suspend (before: MetaItem<*>?) -> MetaItem<*>
) : ReadOnlyDeviceProperty {
private val state: MutableStateFlow<MetaItem<*>?> = MutableStateFlow(default)
override val value: MetaItem<*>? get() = state.value
override suspend fun invalidate() {
state.value = null
}
protected fun update(item: MetaItem<*>) {
state.value = item
updateCallback(name, item)
}
override suspend fun read(force: Boolean): MetaItem<*> {
//backup current value
val currentValue = value
return if (force || currentValue == null) {
val res = withContext(scope.coroutineContext) {
//all device operations should be run on device context
//TODO add error catching
getter(currentValue)
}
update(res)
res
} else {
currentValue
}
}
override fun flow(): StateFlow<MetaItem<*>?> = state
}
private class ReadOnlyDevicePropertyDelegate<D : DeviceBase>(
val owner: D,
val default: MetaItem<*>?,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (MetaItem<*>?) -> MetaItem<*>
) : ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> {
override fun getValue(thisRef: D, property: KProperty<*>): IsolatedReadOnlyDeviceProperty {
val name = property.name
return owner.resolveProperty(name) {
@OptIn(ExperimentalCoroutinesApi::class)
IsolatedReadOnlyDeviceProperty(
name,
default,
PropertyDescriptor(name).apply(descriptorBuilder),
owner.scope,
owner::propertyChanged,
getter
)
} as IsolatedReadOnlyDeviceProperty
}
}
fun <D : DeviceBase> D.reading(
default: MetaItem<*>? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (MetaItem<*>?) -> MetaItem<*>
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this,
default,
descriptorBuilder,
getter
)
fun <D : DeviceBase> D.readingValue(
default: Value? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Any
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this,
default?.let { MetaItem.ValueItem(it) },
descriptorBuilder,
getter = { MetaItem.ValueItem(Value.of(getter())) }
)
fun <D : DeviceBase> D.readingNumber(
default: Number? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Number
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this,
default?.let { MetaItem.ValueItem(it.asValue()) },
descriptorBuilder,
getter = {
val number = getter()
MetaItem.ValueItem(number.asValue())
}
)
fun <D : DeviceBase> D.readingMeta(
default: Meta? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend MetaBuilder.() -> Unit
): ReadOnlyProperty<D, IsolatedReadOnlyDeviceProperty> = ReadOnlyDevicePropertyDelegate(
this,
default?.let { MetaItem.NodeItem(it) },
descriptorBuilder,
getter = {
MetaItem.NodeItem(MetaBuilder().apply { getter() })
}
)
@OptIn(ExperimentalCoroutinesApi::class)
class IsolatedDeviceProperty(
name: String,
default: MetaItem<*>?,
descriptor: PropertyDescriptor,
scope: CoroutineScope,
updateCallback: (name: String, item: MetaItem<*>?) -> Unit,
getter: suspend (MetaItem<*>?) -> MetaItem<*>,
private val setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>?
) : IsolatedReadOnlyDeviceProperty(name, default, descriptor, scope, updateCallback, getter), DeviceProperty {
override var value: MetaItem<*>?
get() = super.value
set(value) {
scope.launch {
if (value == null) {
invalidate()
} else {
write(value)
}
}
}
private val writeLock = Mutex()
override suspend fun write(item: MetaItem<*>) {
writeLock.withLock {
//fast return if value is not changed
if (item == value) return@withLock
val oldValue = value
//all device operations should be run on device context
withContext(scope.coroutineContext) {
//TODO add error catching
setter(oldValue, item)?.let {
update(it)
}
}
}
}
}
private class DevicePropertyDelegate<D : DeviceBase>(
val owner: D,
val default: MetaItem<*>?,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (MetaItem<*>?) -> MetaItem<*>,
private val setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>?
) : ReadOnlyProperty<D, IsolatedDeviceProperty> {
override fun getValue(thisRef: D, property: KProperty<*>): IsolatedDeviceProperty {
val name = property.name
return owner.resolveProperty(name) {
@OptIn(ExperimentalCoroutinesApi::class)
IsolatedDeviceProperty(
name,
default,
PropertyDescriptor(name).apply(descriptorBuilder),
owner.scope,
owner::propertyChanged,
getter,
setter
)
} as IsolatedDeviceProperty
}
}
fun <D : DeviceBase> D.writing(
default: MetaItem<*>? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (MetaItem<*>?) -> MetaItem<*>,
setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>?
): ReadOnlyProperty<D, IsolatedDeviceProperty> = DevicePropertyDelegate(
this,
default,
descriptorBuilder,
getter,
setter
)
fun <D : DeviceBase> D.writingVirtual(
default: MetaItem<*>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): ReadOnlyProperty<D, IsolatedDeviceProperty> = writing(
default,
descriptorBuilder,
getter = { it ?: default },
setter = { _, newItem -> newItem }
)
fun <D : DeviceBase> D.writingVirtual(
default: Value,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): ReadOnlyProperty<D, IsolatedDeviceProperty> = writing(
MetaItem.ValueItem(default),
descriptorBuilder,
getter = { it ?: MetaItem.ValueItem(default) },
setter = { _, newItem -> newItem }
)
fun <D : DeviceBase> D.writingDouble(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Double) -> Double,
setter: suspend (oldValue: Double?, newValue: Double) -> Double?
): ReadOnlyProperty<D, IsolatedDeviceProperty> {
val innerGetter: suspend (MetaItem<*>?) -> MetaItem<*> = {
MetaItem.ValueItem(getter(it.double ?: Double.NaN).asValue())
}
val innerSetter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? = { oldValue, newValue ->
setter(oldValue.double, newValue.double ?: Double.NaN)?.asMetaItem()
}
return DevicePropertyDelegate(
this,
MetaItem.ValueItem(Double.NaN.asValue()),
descriptorBuilder,
innerGetter,
innerSetter
)
}

@ -1,6 +0,0 @@
package hep.dataforge.control.base
import hep.dataforge.meta.MetaItem
import hep.dataforge.values.asValue
fun Double.asMetaItem(): MetaItem.ValueItem = MetaItem.ValueItem(asValue())

@ -1,73 +0,0 @@
package hep.dataforge.control.controllers
import hep.dataforge.control.controllers.DeviceMessage.Companion.PAYLOAD_VALUE_KEY
import hep.dataforge.meta.*
import hep.dataforge.names.asName
import kotlinx.serialization.*
@Serializable
class DeviceMessage : Scheme() {
var id by string()
var parent by string()
var origin by string()
var target by string()
var action by string(default = MessageController.GET_PROPERTY_ACTION, key = MESSAGE_ACTION_KEY)
var comment by string()
var status by string(RESPONSE_OK_STATUS)
var payload: List<MessagePayload>
get() = config.getIndexed(MESSAGE_PAYLOAD_KEY).values.map { MessagePayload.wrap(it.node!!) }
set(value) {
config[MESSAGE_PAYLOAD_KEY] = value.map { it.config }
}
/**
* Append a payload to this message according to the given scheme
*/
fun <T : Configurable> append(spec: Specification<T>, block: T.() -> Unit): T =
spec.invoke(block).also { config.append(MESSAGE_PAYLOAD_KEY, it) }
companion object : SchemeSpec<DeviceMessage>(::DeviceMessage), KSerializer<DeviceMessage> {
val MESSAGE_ACTION_KEY = "action".asName()
val MESSAGE_PAYLOAD_KEY = "payload".asName()
val PAYLOAD_VALUE_KEY = "value".asName()
const val RESPONSE_OK_STATUS = "response.OK"
const val RESPONSE_FAIL_STATUS = "response.FAIL"
const val PROPERTY_CHANGED_ACTION = "event.propertyChange"
inline fun ok(
request: DeviceMessage? = null,
block: DeviceMessage.() -> Unit = {}
): DeviceMessage = DeviceMessage {
parent = request?.id
}.apply(block)
inline fun fail(
request: DeviceMessage? = null,
block: DeviceMessage.() -> Unit = {}
): DeviceMessage = DeviceMessage {
parent = request?.id
status = RESPONSE_FAIL_STATUS
}.apply(block)
override val descriptor: SerialDescriptor = MetaSerializer.descriptor
override fun deserialize(decoder: Decoder): DeviceMessage {
val meta = MetaSerializer.deserialize(decoder)
return wrap(meta)
}
override fun serialize(encoder: Encoder, value: DeviceMessage) {
MetaSerializer.serialize(encoder, value.toMeta())
}
}
}
class MessagePayload : Scheme() {
var name by string { error("Property name could not be empty") }
var value by item(key = PAYLOAD_VALUE_KEY)
companion object : SchemeSpec<MessagePayload>(::MessagePayload)
}
@DFBuilder
fun DeviceMessage.property(block: MessagePayload.() -> Unit): MessagePayload = append(MessagePayload, block)

@ -1,149 +0,0 @@
package hep.dataforge.control.controllers
import hep.dataforge.control.api.Device
import hep.dataforge.control.api.DeviceListener
import hep.dataforge.control.controllers.DeviceMessage.Companion.PROPERTY_CHANGED_ACTION
import hep.dataforge.io.Envelope
import hep.dataforge.io.Responder
import hep.dataforge.io.SimpleEnvelope
import hep.dataforge.meta.MetaItem
import hep.dataforge.meta.wrap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
import kotlinx.io.Binary
/**
* A consumer of envelopes
*/
interface Consumer {
fun consume(message: Envelope): Unit
}
class MessageController(
val device: Device,
val deviceTarget: String,
val scope: CoroutineScope = device.scope
) : Consumer, Responder, DeviceListener {
init {
device.registerListener(this, this)
}
private val outputChannel = Channel<Envelope>(Channel.CONFLATED)
suspend fun respondMessage(
request: DeviceMessage
): DeviceMessage = if (request.target != null && request.target != deviceTarget) {
DeviceMessage.fail {
comment = "Wrong target name $deviceTarget expected but ${request.target} found"
}
} else try {
val result: List<MessagePayload> = when (val action = request.action) {
GET_PROPERTY_ACTION -> {
request.payload.map { property ->
MessagePayload {
name = property.name
value = device.getProperty(name)
}
}
}
SET_PROPERTY_ACTION -> {
request.payload.map { property ->
val propertyName: String = property.name
val propertyValue = property.value
if (propertyValue == null) {
device.invalidateProperty(propertyName)
} else {
device.setProperty(propertyName, propertyValue)
}
MessagePayload {
name = propertyName
value = device.getProperty(propertyName)
}
}
}
EXECUTE_ACTION -> {
request.payload.map { payload ->
MessagePayload {
name = payload.name
value = device.exec(payload.name, payload.value)
}
}
}
PROPERTY_LIST_ACTION -> {
device.propertyDescriptors.map { descriptor ->
MessagePayload {
name = descriptor.name
value = MetaItem.NodeItem(descriptor.config)
}
}
}
ACTION_LIST_ACTION -> {
device.actionDescriptors.map { descriptor ->
MessagePayload {
name = descriptor.name
value = MetaItem.NodeItem(descriptor.config)
}
}
}
else -> {
error("Unrecognized action $action")
}
}
DeviceMessage.ok {
this.parent = request.id
this.origin = deviceTarget
this.target = request.origin
this.payload = result
}
} catch (ex: Exception) {
DeviceMessage.fail {
comment = ex.message
}
}
override fun consume(message: Envelope) {
// Fire the respond procedure and forget about the result
scope.launch {
respond(message)
}
}
override suspend fun respond(request: Envelope): Envelope {
val requestMessage = DeviceMessage.wrap(request.meta)
val responseMessage = respondMessage(requestMessage)
return SimpleEnvelope(responseMessage.toMeta(), Binary.EMPTY)
}
override fun propertyChanged(propertyName: String, value: MetaItem<*>?) {
if (value == null) return
scope.launch {
val change = DeviceMessage.ok {
this.origin = deviceTarget
action = PROPERTY_CHANGED_ACTION
property {
name = propertyName
this.value = value
}
}
val envelope = SimpleEnvelope(change.toMeta(), Binary.EMPTY)
outputChannel.send(envelope)
}
}
fun output() = outputChannel.consumeAsFlow()
companion object {
const val GET_PROPERTY_ACTION = "read"
const val SET_PROPERTY_ACTION = "write"
const val EXECUTE_ACTION = "execute"
const val PROPERTY_LIST_ACTION = "propertyList"
const val ACTION_LIST_ACTION = "actionList"
}
}

@ -1,28 +0,0 @@
package hep.dataforge.control.controllers
import hep.dataforge.control.api.Device
import hep.dataforge.control.api.DeviceListener
import hep.dataforge.meta.MetaItem
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
@ExperimentalCoroutinesApi
suspend fun Device.flowValues(): Flow<Pair<String, MetaItem<*>>> = callbackFlow {
val listener = object : DeviceListener {
override fun propertyChanged(propertyName: String, value: MetaItem<*>?) {
if (value != null) {
launch {
send(propertyName to value)
}
}
}
}
registerListener(listener)
awaitClose {
removeListeners(listener)
}
}

@ -1,40 +0,0 @@
package hep.dataforge.control.controllers
import hep.dataforge.control.base.DeviceProperty
import hep.dataforge.control.base.ReadOnlyDeviceProperty
import hep.dataforge.meta.MetaItem
import hep.dataforge.meta.transformations.MetaConverter
import hep.dataforge.values.Null
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
operator fun ReadOnlyDeviceProperty.getValue(thisRef: Any?, property: KProperty<*>): MetaItem<*> =
value ?: MetaItem.ValueItem(Null)
operator fun DeviceProperty.setValue(thisRef: Any?, property: KProperty<*>, value: MetaItem<*>) {
this.value = value
}
fun <T : Any> ReadOnlyDeviceProperty.convert(metaConverter: MetaConverter<T>): ReadOnlyProperty<Any?, T> {
return object : ReadOnlyProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return this@convert.getValue(thisRef, property).let { metaConverter.itemToObject(it) }
}
}
}
fun <T : Any> DeviceProperty.convert(metaConverter: MetaConverter<T>): ReadWriteProperty<Any?, T> {
return object : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return this@convert.getValue(thisRef, property).let { metaConverter.itemToObject(it) }
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this@convert.setValue(thisRef, property, value.let { metaConverter.objectToMetaItem(it) })
}
}
}
fun ReadOnlyDeviceProperty.double() = convert(MetaConverter.double)
fun DeviceProperty.double() = convert(MetaConverter.double)

@ -1,19 +0,0 @@
import scientifik.useSerialization
plugins {
id("scientifik.jvm")
id("scientifik.publish")
}
useSerialization()
val dataforgeVersion: String by rootProject.extra
val ktorVersion: String by extra("1.3.2")
dependencies{
implementation(project(":dataforge-device-core"))
implementation("io.ktor:ktor-server-cio:$ktorVersion")
implementation("io.ktor:ktor-websockets:$ktorVersion")
implementation("io.ktor:ktor-serialization:$ktorVersion")
implementation("io.ktor:ktor-html-builder:$ktorVersion")
}

@ -1,43 +0,0 @@
package hep.dataforge.control.server
import hep.dataforge.control.controllers.DeviceMessage
import hep.dataforge.io.*
import hep.dataforge.meta.MetaSerializer
import io.ktor.application.ApplicationCall
import io.ktor.http.ContentType
import io.ktor.http.cio.websocket.Frame
import io.ktor.response.respondText
import kotlinx.io.asBinary
import kotlinx.serialization.UnstableDefault
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.json
fun Frame.toEnvelope(): Envelope {
return data.asBinary().readWith(TaggedEnvelopeFormat)
}
fun Envelope.toFrame(): Frame {
val data = buildByteArray {
writeWith(TaggedEnvelopeFormat,this@toFrame)
}
return Frame.Binary(false, data)
}
suspend fun ApplicationCall.respondJson(builder: JsonObjectBuilder.() -> Unit) {
val json = json(builder)
respondText(json.toString(), contentType = ContentType.Application.Json)
}
@OptIn(UnstableDefault::class)
suspend fun ApplicationCall.respondMessage(message: DeviceMessage) {
respondText(Json.stringify(MetaSerializer,message.toMeta()), contentType = ContentType.Application.Json)
}
suspend fun ApplicationCall.respondMessage(builder: DeviceMessage.() -> Unit) {
respondMessage(DeviceMessage(builder))
}
suspend fun ApplicationCall.respondFail(builder: DeviceMessage.() -> Unit) {
respondMessage(DeviceMessage.fail(null, builder))
}

@ -1,230 +0,0 @@
@file:OptIn(ExperimentalCoroutinesApi::class, KtorExperimentalAPI::class, FlowPreview::class, UnstableDefault::class)
package hep.dataforge.control.server
import hep.dataforge.control.api.Device
import hep.dataforge.control.controllers.DeviceMessage
import hep.dataforge.control.controllers.MessageController
import hep.dataforge.control.controllers.MessageController.Companion.GET_PROPERTY_ACTION
import hep.dataforge.control.controllers.MessageController.Companion.SET_PROPERTY_ACTION
import hep.dataforge.control.controllers.property
import hep.dataforge.meta.toJson
import hep.dataforge.meta.toMeta
import hep.dataforge.meta.toMetaItem
import hep.dataforge.meta.wrap
import io.ktor.application.*
import io.ktor.features.CORS
import io.ktor.features.StatusPages
import io.ktor.html.respondHtml
import io.ktor.http.HttpStatusCode
import io.ktor.request.receiveText
import io.ktor.response.respond
import io.ktor.response.respondRedirect
import io.ktor.routing.*
import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer
import io.ktor.util.KtorExperimentalAPI
import io.ktor.util.getValue
import io.ktor.websocket.WebSockets
import io.ktor.websocket.webSocket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.html.body
import kotlinx.html.h1
import kotlinx.html.head
import kotlinx.html.title
import kotlinx.serialization.UnstableDefault
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
/**
* Create and start a web server for several devices
*/
fun CoroutineScope.startDeviceServer(
devices: Map<String, Device>,
port: Int = 8111,
host: String = "localhost"
): ApplicationEngine {
val controllers = devices.mapValues {
MessageController(it.value, it.key, this)
}
return this.embeddedServer(CIO, port, host) {
install(WebSockets)
install(CORS) {
anyHost()
}
// install(ContentNegotiation) {
// json()
// }
install(StatusPages) {
exception<IllegalArgumentException> { cause ->
call.respond(HttpStatusCode.BadRequest, cause.message ?: "")
}
}
deviceModule(controllers)
routing {
get("/") {
call.respondRedirect("/dashboard")
}
}
}.start()
}
fun ApplicationEngine.whenStarted(callback: Application.() -> Unit){
environment.monitor.subscribe(ApplicationStarted, callback)
}
const val WEB_SERVER_TARGET = "@webServer"
private suspend fun ApplicationCall.message(target: MessageController) {
val body = receiveText()
val json = Json.parseJson(body) as? JsonObject
?: throw IllegalArgumentException("The body is not a json object")
val meta = json.toMeta()
val request = DeviceMessage.wrap(meta)
val response = target.respondMessage(request)
respondMessage(response)
}
private suspend fun ApplicationCall.getProperty(target: MessageController) {
val property: String by parameters
val request = DeviceMessage {
action = GET_PROPERTY_ACTION
origin = WEB_SERVER_TARGET
this.target = target.deviceTarget
property {
name = property
}
}
val response = target.respondMessage(request)
respondMessage(response)
}
private suspend fun ApplicationCall.setProperty(target: MessageController) {
val property: String by parameters
val body = receiveText()
val json = Json.parseJson(body)
val request = DeviceMessage {
action = SET_PROPERTY_ACTION
origin = WEB_SERVER_TARGET
this.target = target.deviceTarget
property {
name = property
value = json.toMetaItem()
}
}
val response = target.respondMessage(request)
respondMessage(response)
}
@OptIn(KtorExperimentalAPI::class)
fun Application.deviceModule(targets: Map<String, MessageController>, route: String = "/") {
if(featureOrNull(WebSockets) == null) {
install(WebSockets)
}
if(featureOrNull(CORS)==null){
install(CORS) {
anyHost()
}
}
fun generateFlow(target: String?) = if (target == null) {
targets.values.asFlow().flatMapMerge { it.output() }
} else {
targets[target]?.output() ?: error("The device with target $target not found")
}
routing {
route(route) {
get("dashboard") {
call.respondHtml {
head {
title("Device server dashboard")
}
body {
h1 {
+"Under construction"
}
}
}
}
get("list") {
call.respondJson {
targets.values.forEach { controller ->
"target" to controller.deviceTarget
val device = controller.device
"properties" to jsonArray {
device.propertyDescriptors.forEach { descriptor ->
+descriptor.config.toJson()
}
}
"actions" to jsonArray {
device.actionDescriptors.forEach { actionDescriptor ->
+actionDescriptor.config.toJson()
}
}
}
}
}
//Check if application supports websockets and if it does add a push channel
if (this.application.featureOrNull(WebSockets) != null) {
webSocket("ws") {
//subscribe on device
val target: String? by call.request.queryParameters
try {
application.log.debug("Opened server socket for ${call.request.queryParameters}")
generateFlow(target).collect {
outgoing.send(it.toFrame())
}
} catch (ex: Exception) {
application.log.debug("Closed server socket for ${call.request.queryParameters}")
}
}
}
post("message") {
val target: String by call.request.queryParameters
val controller =
targets[target] ?: throw IllegalArgumentException("Target $target not found in $targets")
call.message(controller)
}
route("{target}") {
//global route for the device
route("{property}") {
get("get") {
val target: String by call.parameters
val controller = targets[target]
?: throw IllegalArgumentException("Target $target not found in $targets")
call.getProperty(controller)
}
post("set") {
val target: String by call.parameters
val controller =
targets[target] ?: throw IllegalArgumentException("Target $target not found in $targets")
call.setProperty(controller)
}
}
}
}
}
}

@ -1,41 +0,0 @@
package hep.dataforge.control.server
import io.ktor.application.ApplicationCall
import io.ktor.http.CacheControl
import io.ktor.http.ContentType
import io.ktor.response.cacheControl
import io.ktor.response.respondTextWriter
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
/**
* The data class representing a SSE Event that will be sent to the client.
*/
data class SseEvent(val data: String, val event: String? = null, val id: String? = null)
/**
* Method that responds an [ApplicationCall] by reading all the [SseEvent]s from the specified [events] [ReceiveChannel]
* and serializing them in a way that is compatible with the Server-Sent Events specification.
*
* You can read more about it here: https://www.html5rocks.com/en/tutorials/eventsource/basics/
*/
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun ApplicationCall.respondSse(events: Flow<SseEvent>) {
response.cacheControl(CacheControl.NoCache(null))
respondTextWriter(contentType = ContentType.Text.EventStream) {
events.collect { event->
if (event.id != null) {
write("id: ${event.id}\n")
}
if (event.event != null) {
write("event: ${event.event}\n")
}
for (dataLine in event.data.lines()) {
write("data: $dataLine\n")
}
write("\n")
flush()
}
}
}

@ -1,31 +1,40 @@
plugins {
kotlin("jvm") version "1.3.72"
id("org.openjfx.javafxplugin") version "0.0.8"
kotlin("jvm")
id("org.openjfx.javafxplugin") version "0.0.9"
application
}
val plotlyVersion: String by rootProject.extra
repositories{
mavenCentral()
jcenter()
maven("https://repo.kotlin.link")
maven("https://kotlin.bintray.com/kotlinx")
maven("https://dl.bintray.com/kotlin/kotlin-eap")
maven("https://dl.bintray.com/mipt-npm/dataforge")
maven("https://dl.bintray.com/mipt-npm/scientifik")
maven("https://dl.bintray.com/mipt-npm/dev")
}
val ktorVersion: String by rootProject.extra
val rsocketVersion: String by rootProject.extra
dependencies{
implementation(project(":dataforge-device-core"))
implementation(project(":dataforge-device-server"))
implementation(projects.controlsCore)
//implementation(projects.controlsServer)
implementation(projects.magix.magixServer)
implementation(projects.controlsMagixClient)
implementation(projects.magix.magixRsocket)
implementation(projects.magix.magixZmq)
implementation(projects.controlsOpcua)
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("no.tornado:tornadofx:1.7.20")
implementation(kotlin("stdlib-jdk8"))
implementation("scientifik:plotlykt-server:$plotlyVersion")
implementation("space.kscience:plotlykt-server:0.5.0-dev-1")
implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
implementation("ch.qos.logback:logback-classic:1.2.3")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all"
}
}
@ -35,5 +44,5 @@ javafx{
}
application{
mainClassName = "hep.dataforge.control.demo.DemoControllerViewKt"
mainClass.set("ru.mipt.npm.controls.demo.DemoControllerViewKt")
}

@ -1,119 +0,0 @@
package hep.dataforge.control.demo
import io.ktor.server.engine.ApplicationEngine
import javafx.scene.Parent
import javafx.scene.control.Slider
import javafx.scene.layout.Priority
import javafx.stage.Stage
import kotlinx.coroutines.*
import org.slf4j.LoggerFactory
import tornadofx.*
import java.awt.Desktop
import java.net.URI
import kotlin.coroutines.CoroutineContext
val logger = LoggerFactory.getLogger("Demo")
class DemoController : Controller(), CoroutineScope {
var device: DemoDevice? = null
var server: ApplicationEngine? = null
override val coroutineContext: CoroutineContext = GlobalScope.newCoroutineContext(Dispatchers.Default) + Job()
fun init() {
launch {
device = DemoDevice(this)
server = device?.let { this.startDemoDeviceServer(it) }
}
}
fun shutdown() {
logger.info("Shutting down...")
server?.stop(1000, 5000)
logger.info("Visualization server stopped")
device?.close()
logger.info("Device server stopped")
cancel("Application context closed")
}
}
class DemoControllerView : View(title = " Demo controller remote") {
private val controller: DemoController by inject()
private var timeScaleSlider: Slider by singleAssign()
private var xScaleSlider: Slider by singleAssign()
private var yScaleSlider: Slider by singleAssign()
override val root: Parent = vbox {
hbox {
label("Time scale")
pane {
hgrow = Priority.ALWAYS
}
timeScaleSlider = slider(1000..10000, 5000) {
isShowTickLabels = true
isShowTickMarks = true
}
}
hbox {
label("X scale")
pane {
hgrow = Priority.ALWAYS
}
xScaleSlider = slider(0.0..2.0, 1.0) {
isShowTickLabels = true
isShowTickMarks = true
}
}
hbox {
label("Y scale")
pane {
hgrow = Priority.ALWAYS
}
yScaleSlider = slider(0.0..2.0, 1.0) {
isShowTickLabels = true
isShowTickMarks = true
}
}
button("Submit") {
useMaxWidth = true
action {
controller.device?.apply {
timeScaleValue = timeScaleSlider.value
sinScaleValue = xScaleSlider.value
cosScaleValue = yScaleSlider.value
}
}
}
button("Show plots") {
useMaxWidth = true
action {
controller.server?.run {
val host = "localhost"//environment.connectors.first().host
val port = environment.connectors.first().port
val uri = URI("http", null, host, port, "/plots", null, null)
Desktop.getDesktop().browse(uri)
}
}
}
}
}
class DemoControllerApp : App(DemoControllerView::class) {
private val controller: DemoController by inject()
override fun start(stage: Stage) {
super.start(stage)
controller.init()
}
override fun stop() {
controller.shutdown()
super.stop()
}
}
fun main() {
launch<DemoControllerApp>()
}

@ -1,67 +0,0 @@
package hep.dataforge.control.demo
import hep.dataforge.control.base.*
import hep.dataforge.control.controllers.double
import hep.dataforge.values.asValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher
import java.time.Instant
import java.util.concurrent.Executors
import kotlin.math.cos
import kotlin.math.sin
import kotlin.time.ExperimentalTime
import kotlin.time.seconds
@OptIn(ExperimentalTime::class)
class DemoDevice(parentScope: CoroutineScope = GlobalScope) : DeviceBase() {
private val executor = Executors.newSingleThreadExecutor()
override val scope: CoroutineScope = CoroutineScope(
parentScope.coroutineContext + executor.asCoroutineDispatcher() + Job(parentScope.coroutineContext[Job])
)
val timeScale: IsolatedDeviceProperty by writingVirtual(5000.0.asValue())
var timeScaleValue by timeScale.double()
val sinScale by writingVirtual(1.0.asValue())
var sinScaleValue by sinScale.double()
val sin by readingNumber {
val time = Instant.now()
sin(time.toEpochMilli().toDouble() / timeScaleValue)*sinScaleValue
}
val cosScale by writingVirtual(1.0.asValue())
var cosScaleValue by cosScale.double()
val cos by readingNumber {
val time = Instant.now()
cos(time.toEpochMilli().toDouble() / timeScaleValue)*cosScaleValue
}
val coordinates by readingMeta {
val time = Instant.now()
"time" put time.toEpochMilli()
"x" put sin(time.toEpochMilli().toDouble() / timeScaleValue)*sinScaleValue
"y" put cos(time.toEpochMilli().toDouble() / timeScaleValue)*cosScaleValue
}
val resetScale: Action by action {
timeScaleValue = 5000.0
sinScaleValue = 1.0
cosScaleValue = 1.0
}
init {
sin.readEvery(0.2.seconds)
cos.readEvery(0.2.seconds)
coordinates.readEvery(0.3.seconds)
}
override fun close() {
super.close()
executor.shutdown()
}
}

@ -0,0 +1,160 @@
package ru.mipt.npm.controls.demo
import io.ktor.server.engine.ApplicationEngine
import javafx.scene.Parent
import javafx.scene.control.Slider
import javafx.scene.layout.Priority
import javafx.stage.Stage
import kotlinx.coroutines.launch
import org.eclipse.milo.opcua.sdk.server.OpcUaServer
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
import ru.mipt.npm.controls.api.DeviceMessage
import ru.mipt.npm.controls.client.connectToMagix
import ru.mipt.npm.controls.controllers.DeviceManager
import ru.mipt.npm.controls.controllers.install
import ru.mipt.npm.controls.demo.DemoDevice.Companion.cosScale
import ru.mipt.npm.controls.demo.DemoDevice.Companion.sinScale
import ru.mipt.npm.controls.demo.DemoDevice.Companion.timeScale
import ru.mipt.npm.controls.opcua.server.OpcUaServer
import ru.mipt.npm.controls.opcua.server.endpoint
import ru.mipt.npm.controls.opcua.server.serveDevices
import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.rsocket.rSocketWithTcp
import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets
import ru.mipt.npm.magix.server.startMagixServer
import space.kscience.dataforge.context.*
import tornadofx.*
import java.awt.Desktop
import java.net.URI
class DemoController : Controller(), ContextAware {
var device: DemoDevice? = null
var magixServer: ApplicationEngine? = null
var visualizer: ApplicationEngine? = null
var opcUaServer: OpcUaServer = OpcUaServer {
setApplicationName(LocalizedText.english("ru.mipt.npm.controls.opcua"))
endpoint {
setBindPort(9999)
//use default endpoint
}
}
override val context = Context("demoDevice") {
plugin(DeviceManager)
}
private val deviceManager = context.fetch(DeviceManager)
fun init() {
context.launch {
device = deviceManager.install("demo", DemoDevice)
//starting magix event loop
magixServer = startMagixServer(enableRawRSocket = true, enableZmq = true)
//Launch device client and connect it to the server
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost", DeviceMessage.serializer())
deviceManager.connectToMagix(deviceEndpoint)
val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost", DeviceMessage.serializer())
visualizer = visualEndpoint.startDemoDeviceServer()
opcUaServer.startup()
opcUaServer.serveDevices(deviceManager)
}
}
fun shutdown() {
logger.info { "Shutting down..." }
opcUaServer.shutdown()
logger.info { "OpcUa server stopped" }
visualizer?.stop(1000, 5000)
logger.info { "Visualization server stopped" }
magixServer?.stop(1000, 5000)
logger.info { "Magix server stopped" }
device?.close()
logger.info { "Device server stopped" }
context.close()
}
}
class DemoControllerView : View(title = " Demo controller remote") {
private val controller: DemoController by inject()
private var timeScaleSlider: Slider by singleAssign()
private var xScaleSlider: Slider by singleAssign()
private var yScaleSlider: Slider by singleAssign()
override val root: Parent = vbox {
hbox {
label("Time scale")
pane {
hgrow = Priority.ALWAYS
}
timeScaleSlider = slider(1000..10000, 5000) {
isShowTickLabels = true
isShowTickMarks = true
}
}
hbox {
label("X scale")
pane {
hgrow = Priority.ALWAYS
}
xScaleSlider = slider(0.1..2.0, 1.0) {
isShowTickLabels = true
isShowTickMarks = true
}
}
hbox {
label("Y scale")
pane {
hgrow = Priority.ALWAYS
}
yScaleSlider = slider(0.1..2.0, 1.0) {
isShowTickLabels = true
isShowTickMarks = true
}
}
button("Submit") {
useMaxWidth = true
action {
controller.device?.run {
launch {
timeScale.write(timeScaleSlider.value)
sinScale.write(xScaleSlider.value)
cosScale.write(yScaleSlider.value)
}
}
}
}
button("Show plots") {
useMaxWidth = true
action {
controller.visualizer?.run {
val host = "localhost"//environment.connectors.first().host
val port = environment.connectors.first().port
val uri = URI("http", null, host, port, "/", null, null)
Desktop.getDesktop().browse(uri)
}
}
}
}
}
class DemoControllerApp : App(DemoControllerView::class) {
private val controller: DemoController by inject()
override fun start(stage: Stage) {
super.start(stage)
controller.init()
}
override fun stop() {
controller.shutdown()
super.stop()
}
}
fun main() {
launch<DemoControllerApp>()
}

@ -0,0 +1,60 @@
package ru.mipt.npm.controls.demo
import kotlinx.coroutines.launch
import ru.mipt.npm.controls.properties.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import java.time.Instant
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
private var timeScaleState = 5000.0
private var sinScaleState = 1.0
private var cosScaleState = 1.0
companion object : DeviceSpec<DemoDevice>(::DemoDevice) {
// register virtual properties based on actual object state
val timeScale by property(MetaConverter.double, DemoDevice::timeScaleState)
val sinScale by property(MetaConverter.double, DemoDevice::sinScaleState)
val cosScale by property(MetaConverter.double, DemoDevice::cosScaleState)
val sin by doubleProperty {
val time = Instant.now()
kotlin.math.sin(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState
}
val cos by doubleProperty {
val time = Instant.now()
kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState
}
val coordinates by metaProperty {
Meta {
val time = Instant.now()
"time" put time.toEpochMilli()
"x" put read(sin)
"y" put read(cos)
}
}
val resetScale by action(MetaConverter.meta, MetaConverter.meta) {
timeScale.write(5000.0)
sinScale.write(1.0)
cosScale.write(1.0)
null
}
@OptIn(ExperimentalTime::class)
override fun DemoDevice.onStartup() {
launch {
sinScale.read()
cosScale.read()
}
doRecurring(Duration.milliseconds(50)){
coordinates.read()
}
}
}
}

@ -1,23 +1,27 @@
package hep.dataforge.control.demo
package ru.mipt.npm.controls.demo
import hep.dataforge.control.server.startDeviceServer
import hep.dataforge.control.server.whenStarted
import hep.dataforge.meta.double
import io.ktor.application.uninstall
import io.ktor.application.install
import io.ktor.features.CORS
import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer
import io.ktor.websocket.WebSockets
import kotlinx.coroutines.CoroutineScope
import io.rsocket.kotlin.transport.ktor.server.RSocketSupport
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.html.div
import kotlinx.html.link
import scientifik.plotly.layout
import scientifik.plotly.models.Trace
import scientifik.plotly.plot
import scientifik.plotly.server.PlotlyServerConfig
import scientifik.plotly.server.PlotlyUpdateMode
import scientifik.plotly.server.plotlyModule
import scientifik.plotly.trace
import ru.mipt.npm.controls.api.DeviceMessage
import ru.mipt.npm.controls.api.PropertyChangedMessage
import ru.mipt.npm.magix.api.MagixEndpoint
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.double
import space.kscience.plotly.layout
import space.kscience.plotly.models.Trace
import space.kscience.plotly.plot
import space.kscience.plotly.server.PlotlyUpdateMode
import space.kscience.plotly.server.plotlyModule
import space.kscience.plotly.trace
import java.util.concurrent.ConcurrentLinkedQueue
/**
@ -50,17 +54,33 @@ suspend fun Trace.updateXYFrom(flow: Flow<Iterable<Pair<Double, Double>>>) {
}
suspend fun MagixEndpoint<DeviceMessage>.startDemoDeviceServer(): ApplicationEngine =
embeddedServer(CIO, 9090) {
install(WebSockets)
install(RSocketSupport)
fun CoroutineScope.startDemoDeviceServer(device: DemoDevice): ApplicationEngine {
val server = startDeviceServer(mapOf("demo" to device))
server.whenStarted {
uninstall(WebSockets)
plotlyModule(
"plots",
PlotlyServerConfig { updateMode = PlotlyUpdateMode.PUSH; updateInterval = 50 }
) { container ->
val sinFlow = device.sin.flow()
val cosFlow = device.cos.flow()
install(CORS) {
anyHost()
}
val sinFlow = MutableSharedFlow<Meta?>()// = device.sin.flow()
val cosFlow = MutableSharedFlow<Meta?>()// = device.cos.flow()
launch {
subscribe().collect { magix ->
(magix.payload as? PropertyChangedMessage)?.let { message ->
when (message.property) {
"sin" -> sinFlow.emit(message.value)
"cos" -> cosFlow.emit(message.value)
}
}
}
}
plotlyModule().apply {
updateMode = PlotlyUpdateMode.PUSH
updateInterval = 50
}.page { container ->
val sinCosFlow = sinFlow.zip(cosFlow) { sin, cos ->
sin.double!! to cos.double!!
}
@ -72,7 +92,7 @@ fun CoroutineScope.startDemoDeviceServer(device: DemoDevice): ApplicationEngine
}
div("row") {
div("col-6") {
plot(container = container) {
plot(renderer = container) {
layout {
title = "sin property"
xaxis.title = "point index"
@ -87,7 +107,7 @@ fun CoroutineScope.startDemoDeviceServer(device: DemoDevice): ApplicationEngine
}
}
div("col-6") {
plot(container = container) {
plot(renderer = container) {
layout {
title = "cos property"
xaxis.title = "point index"
@ -104,7 +124,7 @@ fun CoroutineScope.startDemoDeviceServer(device: DemoDevice): ApplicationEngine
}
div("row") {
div("col-12") {
plot(container = container) {
plot(renderer = container) {
layout {
title = "cos vs sin"
xaxis.title = "sin"
@ -121,7 +141,5 @@ fun CoroutineScope.startDemoDeviceServer(device: DemoDevice): ApplicationEngine
}
}
}
}
return server
}
}.apply { start() }

@ -0,0 +1,10 @@
package ru.mipt.npm.controls.demo
import com.github.ricky12awesome.jss.encodeToSchema
import com.github.ricky12awesome.jss.globalJson
import ru.mipt.npm.controls.api.DeviceMessage
fun main() {
val schema = globalJson.encodeToSchema(DeviceMessage.serializer(), generateDefinitions = false)
println(schema)
}

Binary file not shown.

After

(image error) Size: 13 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

@ -0,0 +1,18 @@
@startuml
title Transform asynchronous to synchronous
participant Synchronous
participant Adapter
participant Asynchronous
activate Adapter
Asynchronous -> Adapter: message with ID
Adapter -> Synchronous
activate Synchronous
hnote over Adapter : create a waiting thread
Synchronous -> Adapter
deactivate Synchronous
Adapter -> Asynchronous: message with ID
@enduml

@ -1,6 +1,8 @@
@startuml
title Simple call with callback
Main -> Async: call
activate Main
activate Async
Async -> Main: result

@ -0,0 +1,25 @@
@startuml
participant Physical
participant Logical
participant Remote
group Asynchronous update
Physical -> Logical: Notify changed
Logical -> Remote: Send event
end
group Timed update
Logical -> Logical: Timed check
Logical -> Physical: Request value
Physical -> Logical: Respond
Logical --> Remote: Send event if changed
end
group Request update
Remote -> Logical: Request value
Logical --> Physical: Request if needed
Physical --> Logical: Respond
Logical -> Remote: Force send event
end
@enduml

@ -0,0 +1,23 @@
@startuml
title Transform synchronous to asynchronous
participant Synchronous
participant Adapter
participant Asynchronous
activate Synchronous
Synchronous -> Adapter: call and block
deactivate Synchronous
activate Adapter
Adapter -> Asynchronous: message with ID
hnote over Adapter : create a waiting thread
Asynchronous -> Adapter: message with ID
Adapter -> Synchronous: return result
deactivate Adapter
activate Synchronous
@enduml

Binary file not shown.

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

2
gradlew vendored

@ -130,7 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath

21
gradlew.bat vendored

@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -64,21 +64,6 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

3
magix/build.gradle.kts Normal file

@ -0,0 +1,3 @@
subprojects{
}

@ -0,0 +1,12 @@
plugins {
id("ru.mipt.npm.gradle.mpp")
`maven-publish`
}
kscience {
useCoroutines()
useSerialization{
json()
}
}

@ -0,0 +1,83 @@
package ru.mipt.npm.magix.api
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
/**
* Inwards API of magix endpoint used to build services
*/
public interface MagixEndpoint<T> {
/**
* Subscribe to a [Flow] of messages
*/
public fun subscribe(
filter: MagixMessageFilter = MagixMessageFilter.ALL,
): Flow<MagixMessage<T>>
/**
* Send an event
*/
public suspend fun broadcast(
message: MagixMessage<T>,
)
public companion object {
/**
* A default port for HTTP/WS connections
*/
public const val DEFAULT_MAGIX_HTTP_PORT: Int = 7777
/**
* A default port for raw TCP connections
*/
public const val DEFAULT_MAGIX_RAW_PORT: Int = 7778
/**
* A default PUB port for ZMQ connections
*/
public const val DEFAULT_MAGIX_ZMQ_PUB_PORT: Int = 7781
/**
* A default PULL port for ZMQ connections
*/
public const val DEFAULT_MAGIX_ZMQ_PULL_PORT: Int = 7782
public val magixJson: Json = Json {
ignoreUnknownKeys = true
encodeDefaults = false
}
}
}
/**
* Specialize this raw json endpoint to use specific serializer
*/
public fun <T : Any> MagixEndpoint<JsonElement>.specialize(
payloadSerializer: KSerializer<T>
): MagixEndpoint<T> = object : MagixEndpoint<T> {
override fun subscribe(
filter: MagixMessageFilter
): Flow<MagixMessage<T>> = this@specialize.subscribe(filter).map { message ->
message.replacePayload { payload ->
MagixEndpoint.magixJson.decodeFromJsonElement(payloadSerializer, payload)
}
}
override suspend fun broadcast(message: MagixMessage<T>) {
this@specialize.broadcast(
message.replacePayload { payload ->
MagixEndpoint.magixJson.encodeToJsonElement(
payloadSerializer,
payload
)
}
)
}
}

@ -0,0 +1,42 @@
package ru.mipt.npm.magix.api
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
/*
* {
* "format": "string[required]",
* "id":"string|number[optional, but desired]",
* "parentId": "string|number[optional]",
* "target":"string[optional]",
* "origin":"string[required]",
* "user":"string[optional]",
* "action":"string[optional, default='heartbeat']",
* "payload":"object[optional]"
* }
*/
/**
*
* Magix message according to [magix specification](https://github.com/piazza-controls/rfc/tree/master/1)
* with a [correction](https://github.com/piazza-controls/rfc/issues/12)
*
*/
@Serializable
public data class MagixMessage<T>(
val format: String,
val origin: String,
val payload: T,
val target: String? = null,
val id: String? = null,
val parentId: String? = null,
val user: JsonElement? = null,
)
/**
* Create message with same field but replaced payload
*/
@Suppress("UNCHECKED_CAST")
public fun <T, R> MagixMessage<T>.replacePayload(payloadTransform: (T) -> R): MagixMessage<R> =
MagixMessage(format, origin, payloadTransform(payload), target, id, parentId, user)

@ -0,0 +1,31 @@
package ru.mipt.npm.magix.api
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.serialization.Serializable
@Serializable
public data class MagixMessageFilter(
val format: List<String?>? = null,
val origin: List<String?>? = null,
val target: List<String?>? = null,
) {
public companion object {
public val ALL: MagixMessageFilter = MagixMessageFilter()
}
}
/**
* Filter a [Flow] of messages based on given filter
*/
public fun <T> Flow<MagixMessage<T>>.filter(filter: MagixMessageFilter): Flow<MagixMessage<T>> {
if (filter == MagixMessageFilter.ALL) {
return this
}
return filter { message ->
filter.format?.contains(message.format) ?: true
&& filter.origin?.contains(message.origin) ?: true
&& filter.origin?.contains(message.origin) ?: true
&& filter.target?.contains(message.target) ?: true
}
}

@ -0,0 +1,30 @@
package ru.mipt.npm.magix.api
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
/**
* Launch magix message converter service
*/
public fun <T, R> CoroutineScope.launchMagixConverter(
inputEndpoint: MagixEndpoint<T>,
outputEndpoint: MagixEndpoint<R>,
filter: MagixMessageFilter,
outputFormat: String,
newOrigin: String? = null,
transformer: suspend (T) -> R,
): Job = inputEndpoint.subscribe(filter).onEach { message->
val newPayload = transformer(message.payload)
val transformed: MagixMessage<R> = MagixMessage(
outputFormat,
newOrigin ?: message.origin,
newPayload,
message.target,
message.id,
message.parentId,
message.user
)
outputEndpoint.broadcast(transformed)
}.launchIn(this)

@ -0,0 +1,20 @@
plugins {
id("ru.mipt.npm.gradle.jvm")
application
}
dependencies{
implementation(projects.magix.magixServer)
implementation(projects.magix.magixZmq)
implementation(projects.magix.magixRsocket)
implementation("ch.qos.logback:logback-classic:1.2.3")
}
kotlin{
explicitApi = null
}
application{
mainClass.set("ZmqKt")
}

@ -0,0 +1,70 @@
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.serialization.json.*
import org.slf4j.LoggerFactory
import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.api.MagixMessage
import ru.mipt.npm.magix.server.startMagixServer
import ru.mipt.npm.magix.zmq.ZmqMagixEndpoint
import java.awt.Desktop
import java.net.URI
suspend fun MagixEndpoint<JsonObject>.sendJson(
origin: String,
format: String = "json",
target: String? = null,
id: String? = null,
parentId: String? = null,
user: JsonElement? = null,
builder: JsonObjectBuilder.() -> Unit
): Unit = broadcast(MagixMessage(format, origin, buildJsonObject(builder), target, id, parentId, user))
internal const val numberOfMessages = 100
suspend fun main(): Unit = coroutineScope {
val logger = LoggerFactory.getLogger("magix-demo")
logger.info("Starting magix server")
val server = startMagixServer(
buffer = 10,
enableRawRSocket = false //Disable rsocket to avoid kotlin 1.5 compatibility issue
)
server.apply {
val host = "localhost"//environment.connectors.first().host
val port = environment.connectors.first().port
val uri = URI("http", null, host, port, "/state", null, null)
Desktop.getDesktop().browse(uri)
}
logger.info("Starting client")
//Create zmq magix endpoint and wait for to finish
ZmqMagixEndpoint("tcp://localhost", JsonObject.serializer()).use { client ->
logger.info("Starting subscription")
client.subscribe().onEach {
println(it.payload)
if (it.payload["index"]?.jsonPrimitive?.int == numberOfMessages) {
logger.info("Index $numberOfMessages reached. Terminating")
cancel()
}
}.catch { it.printStackTrace() }.launchIn(this)
var counter = 0
while (isActive) {
delay(500)
val index = (counter++).toString()
logger.info("Sending message number $index")
client.sendJson("magix-demo", id = index) {
put("message", "Hello world!")
put("index", index)
}
}
}
}

@ -0,0 +1,10 @@
plugins {
java
id("ru.mipt.npm.gradle.jvm")
`maven-publish`
}
dependencies {
implementation(project(":magix:magix-rsocket"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:${ru.mipt.npm.gradle.KScienceVersions.coroutinesVersion}")
}

@ -0,0 +1,39 @@
package ru.mipt.npm.magix.client;
import kotlinx.serialization.json.JsonElement;
import ru.mipt.npm.magix.api.MagixMessage;
import java.io.IOException;
import java.util.concurrent.Flow;
/**
* See https://github.com/waltz-controls/rfc/tree/master/2
*
* @param <T>
*/
public interface MagixClient<T> {
void broadcast(MagixMessage<T> msg) throws IOException;
Flow.Publisher<MagixMessage<T>> subscribe();
/**
* Create a magix endpoint client using RSocket with raw tcp connection
* @param host host name of magix server event loop
* @param port port of magix server event loop
* @return the client
*/
static MagixClient<JsonElement> rSocketTcp(String host, int port) {
return ControlsMagixClient.Companion.rSocketTcp(host, port, JsonElement.Companion.serializer());
}
/**
*
* @param host host name of magix server event loop
* @param port port of magix server event loop
* @param path
* @return
*/
static MagixClient<JsonElement> rSocketWs(String host, int port, String path) {
return ControlsMagixClient.Companion.rSocketWs(host, port, JsonElement.Companion.serializer(), path);
}
}

@ -0,0 +1,49 @@
package ru.mipt.npm.magix.client
import kotlinx.coroutines.jdk9.asPublisher
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.KSerializer
import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.api.MagixMessage
import ru.mipt.npm.magix.api.MagixMessageFilter
import ru.mipt.npm.magix.rsocket.rSocketWithTcp
import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets
import java.util.concurrent.Flow
internal class ControlsMagixClient<T>(
private val endpoint: MagixEndpoint<T>,
private val filter: MagixMessageFilter,
) : MagixClient<T> {
override fun broadcast(msg: MagixMessage<T>): Unit = runBlocking {
endpoint.broadcast(msg)
}
override fun subscribe(): Flow.Publisher<MagixMessage<T>> = endpoint.subscribe(filter).asPublisher()
companion object {
fun <T> rSocketTcp(
host: String,
port: Int,
payloadSerializer: KSerializer<T>
): ControlsMagixClient<T> {
val endpoint = runBlocking {
MagixEndpoint.rSocketWithTcp(host, payloadSerializer, port)
}
return ControlsMagixClient(endpoint, MagixMessageFilter())
}
fun <T> rSocketWs(
host: String,
port: Int,
payloadSerializer: KSerializer<T>,
path: String = "/rsocket"
): ControlsMagixClient<T> {
val endpoint = runBlocking {
MagixEndpoint.rSocketWithWebSockets(host, payloadSerializer, port, path)
}
return ControlsMagixClient(endpoint, MagixMessageFilter())
}
}
}

Some files were not shown because too many files have changed in this diff Show More