Dev #6
.gitignoreREADME.mdbuild.gradle.kts
controls-core
build.gradle.kts
src
commonMain/kotlin/ru/mipt/npm/controls
api
base
DeviceAction.ktDeviceBase.ktDeviceProperty.ktTypedDeviceProperty.ktactionDelegates.ktdevicePropertyDelegates.ktmisc.kt
controllers
ports
properties
jvmMain/kotlin/ru/mipt/npm/controls
jvmTest/kotlin/ru/mipt/npm/controls/ports
controls-magix-client
build.gradle.kts
src/commonMain/kotlin/ru/mipt/npm/controls/client
controls-opcua
build.gradle.kts
src/main/kotlin/ru/mipt/npm/controls/opcua
controls-serial
controls-server
controls-tcp
dataforge-device-client
dataforge-device-core
build.gradle.kts
src/commonMain/kotlin/hep/dataforge/control
dataforge-device-server
demo
docs
pictures
schemes
uml
gradle/wrapper
gradlewgradlew.batmagix
build.gradle.kts
magix-api
build.gradle.kts
src/commonMain/kotlin/ru/mipt/npm/magix/api
magix-demo
magix-java-client
build.gradle.kts
src/main
4
.gitignore
vendored
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
|
||||
}
|
24
controls-core/build.gradle.kts
Normal file
24
controls-core/build.gradle.kts
Normal file
@ -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
|
||||
}
|
283
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/devicePropertyDelegates.kt
Normal file
283
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/devicePropertyDelegates.kt
Normal file
@ -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
|
54
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceManager.kt
Normal file
54
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceManager.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
118
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt
Normal file
118
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/SynchronousPortHandler.kt
Normal file
34
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/SynchronousPortHandler.kt
Normal file
@ -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)
|
||||
}
|
85
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt
Normal file
85
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt
Normal file
@ -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()
|
||||
}
|
||||
}
|
32
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceExtensions.kt
Normal file
32
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceExtensions.kt
Normal file
@ -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()
|
||||
}
|
||||
}
|
144
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt
Normal file
144
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt
Normal file
@ -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)
|
10
controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/getDeviceProperty.kt
Normal file
10
controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/getDeviceProperty.kt
Normal file
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
21
controls-magix-client/build.gradle.kts
Normal file
21
controls-magix-client/build.gradle.kts
Normal file
@ -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)
|
||||
}
|
||||
|
||||
|
119
controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/doocsMagix.kt
Normal file
119
controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/doocsMagix.kt
Normal file
@ -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?
|
||||
)
|
146
controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt
Normal file
146
controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
17
controls-opcua/build.gradle.kts
Normal file
17
controls-opcua/build.gradle.kts
Normal file
@ -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))
|
||||
}
|
9
controls-serial/build.gradle.kts
Normal file
9
controls-serial/build.gradle.kts
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
21
controls-server/build.gradle.kts
Normal file
21
controls-server/build.gradle.kts
Normal file
@ -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
|
||||
)
|
17
controls-tcp/build.gradle.kts
Normal file
17
controls-tcp/build.gradle.kts
Normal file
@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
257
dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt
257
dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt
@ -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)
|
149
dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/MessageController.kt
149
dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/MessageController.kt
@ -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>()
|
||||
}
|
60
demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt
Normal file
60
demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt
Normal file
@ -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)
|
||||
}
|
BIN
docs/pictures/async-to sync.png
Normal file
BIN
docs/pictures/async-to sync.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 13 KiB |
BIN
docs/pictures/sync-to-async.png
Normal file
BIN
docs/pictures/sync-to-async.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 15 KiB |
BIN
docs/schemes/direct-vs-loop.vsdx
Normal file
BIN
docs/schemes/direct-vs-loop.vsdx
Normal file
Binary file not shown.
18
docs/uml/async-to sync.puml
Normal file
18
docs/uml/async-to sync.puml
Normal file
@ -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
|
||||
|
25
docs/uml/device-properties.puml
Normal file
25
docs/uml/device-properties.puml
Normal file
@ -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
|
23
docs/uml/sync-to-async.puml
Normal file
23
docs/uml/sync-to-async.puml
Normal file
@ -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
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-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
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
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
3
magix/build.gradle.kts
Normal file
@ -0,0 +1,3 @@
|
||||
subprojects{
|
||||
|
||||
}
|
12
magix/magix-api/build.gradle.kts
Normal file
12
magix/magix-api/build.gradle.kts
Normal file
@ -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)
|
20
magix/magix-demo/build.gradle.kts
Normal file
20
magix/magix-demo/build.gradle.kts
Normal file
@ -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")
|
||||
}
|
70
magix/magix-demo/src/main/kotlin/zmq.kt
Normal file
70
magix/magix-demo/src/main/kotlin/zmq.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
10
magix/magix-java-client/build.gradle.kts
Normal file
10
magix/magix-java-client/build.gradle.kts
Normal file
@ -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);
|
||||
}
|
||||
}
|
49
magix/magix-java-client/src/main/kotlin/ru/mipt/npm/magix/client/ControlsMagixClient.kt
Normal file
49
magix/magix-java-client/src/main/kotlin/ru/mipt/npm/magix/client/ControlsMagixClient.kt
Normal file
@ -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
Loading…
x
Reference in New Issue
Block a user