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)
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
.idea/
|
.idea/
|
||||||
.gradle
|
.gradle
|
||||||
|
|
||||||
*.iws
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
out/
|
out/
|
||||||
build/
|
build/
|
||||||
!gradle-wrapper.jar
|
!gradle-wrapper.jar
|
@ -2,13 +2,12 @@
|
|||||||
|
|
||||||
# Controls.kt
|
# Controls.kt
|
||||||
|
|
||||||
Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is
|
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.
|
||||||
based on DataForge, a software framework for automated data processing.
|
|
||||||
This repository contains a prototype of API and simple implementation
|
This repository contains a prototype of API and simple implementation
|
||||||
of a slow control system, including a demo.
|
of a slow control system, including a demo.
|
||||||
|
|
||||||
Controls.kt uses some concepts and modules of DataForge,
|
Controls.kt uses some concepts and modules of DataForge,
|
||||||
such as `Meta` (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
|
includes a scalar value, or a tree of values, easily convertable to/from JSON
|
||||||
if needed).
|
if needed).
|
||||||
|
|
||||||
@ -37,12 +36,12 @@ Among other things, you can:
|
|||||||
### `dataforge-control-core` module packages
|
### `dataforge-control-core` module packages
|
||||||
|
|
||||||
- `api` - defines API for device management. The main class here is
|
- `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
|
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).
|
can optionally be applied on a device (may or may not affect properties).
|
||||||
|
|
||||||
- `base` - contains baseline `Device` implementation
|
- `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.
|
and property implementation, including property asynchronous flows.
|
||||||
|
|
||||||
- `controllers` - implements Message Controller that can be attached to the event bus, Message
|
- `controllers` - implements Message Controller that can be attached to the event bus, Message
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
val dataforgeVersion by extra("0.1.8")
|
plugins {
|
||||||
val plotlyVersion by extra("0.2.0-dev-12")
|
id("ru.mipt.npm.gradle.project")
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val githubProject by extra("dataforge-control")
|
val dataforgeVersion: String by extra("0.5.1")
|
||||||
val bintrayRepo by extra("dataforge")
|
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.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import ru.mipt.npm.controls.api.PropertyDescriptor
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read-only device property
|
* Read-only device property
|
||||||
*/
|
*/
|
||||||
interface ReadOnlyDeviceProperty {
|
public interface ReadOnlyDeviceProperty {
|
||||||
/**
|
/**
|
||||||
* Property name, should be unique in device
|
* Property name, should be unique in device
|
||||||
*/
|
*/
|
||||||
val name: String
|
public val name: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Property descriptor
|
* 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]
|
* 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
|
* 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.
|
* 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.
|
* The [Flow] representing future logical states of the property.
|
||||||
* Produces null when the state is invalidated
|
* 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.
|
* 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) {
|
while (isActive) {
|
||||||
read(true)
|
read(true)
|
||||||
delay(duration)
|
delay(duration)
|
||||||
@ -64,11 +64,11 @@ fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch {
|
|||||||
/**
|
/**
|
||||||
* A writeable device property with non-suspended write
|
* A writeable device property with non-suspended write
|
||||||
*/
|
*/
|
||||||
interface DeviceProperty : ReadOnlyDeviceProperty {
|
public interface DeviceProperty : ReadOnlyDeviceProperty {
|
||||||
override var value: MetaItem<*>?
|
override var value: Meta?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write value to physical device. Invalidates logical value, but does not update it automatically
|
* 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 {
|
plugins {
|
||||||
kotlin("jvm") version "1.3.72"
|
kotlin("jvm")
|
||||||
id("org.openjfx.javafxplugin") version "0.0.8"
|
id("org.openjfx.javafxplugin") version "0.0.9"
|
||||||
application
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
val plotlyVersion: String by rootProject.extra
|
|
||||||
|
|
||||||
repositories{
|
repositories{
|
||||||
|
mavenCentral()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
maven("https://repo.kotlin.link")
|
||||||
maven("https://kotlin.bintray.com/kotlinx")
|
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{
|
dependencies{
|
||||||
implementation(project(":dataforge-device-core"))
|
implementation(projects.controlsCore)
|
||||||
implementation(project(":dataforge-device-server"))
|
//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("no.tornado:tornadofx:1.7.20")
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation("space.kscience:plotlykt-server:0.5.0-dev-1")
|
||||||
implementation("scientifik:plotlykt-server:$plotlyVersion")
|
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 {
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "11"
|
jvmTarget = "11"
|
||||||
|
freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,5 +44,5 @@ javafx{
|
|||||||
}
|
}
|
||||||
|
|
||||||
application{
|
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 io.ktor.application.install
|
||||||
import hep.dataforge.control.server.whenStarted
|
import io.ktor.features.CORS
|
||||||
import hep.dataforge.meta.double
|
import io.ktor.server.cio.CIO
|
||||||
import io.ktor.application.uninstall
|
|
||||||
import io.ktor.server.engine.ApplicationEngine
|
import io.ktor.server.engine.ApplicationEngine
|
||||||
|
import io.ktor.server.engine.embeddedServer
|
||||||
import io.ktor.websocket.WebSockets
|
import io.ktor.websocket.WebSockets
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import io.rsocket.kotlin.transport.ktor.server.RSocketSupport
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.html.div
|
import kotlinx.html.div
|
||||||
import kotlinx.html.link
|
import kotlinx.html.link
|
||||||
import scientifik.plotly.layout
|
import ru.mipt.npm.controls.api.DeviceMessage
|
||||||
import scientifik.plotly.models.Trace
|
import ru.mipt.npm.controls.api.PropertyChangedMessage
|
||||||
import scientifik.plotly.plot
|
import ru.mipt.npm.magix.api.MagixEndpoint
|
||||||
import scientifik.plotly.server.PlotlyServerConfig
|
import space.kscience.dataforge.meta.Meta
|
||||||
import scientifik.plotly.server.PlotlyUpdateMode
|
import space.kscience.dataforge.meta.double
|
||||||
import scientifik.plotly.server.plotlyModule
|
import space.kscience.plotly.layout
|
||||||
import scientifik.plotly.trace
|
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
|
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 {
|
install(CORS) {
|
||||||
val server = startDeviceServer(mapOf("demo" to device))
|
anyHost()
|
||||||
server.whenStarted {
|
}
|
||||||
uninstall(WebSockets)
|
|
||||||
plotlyModule(
|
val sinFlow = MutableSharedFlow<Meta?>()// = device.sin.flow()
|
||||||
"plots",
|
val cosFlow = MutableSharedFlow<Meta?>()// = device.cos.flow()
|
||||||
PlotlyServerConfig { updateMode = PlotlyUpdateMode.PUSH; updateInterval = 50 }
|
|
||||||
) { container ->
|
launch {
|
||||||
val sinFlow = device.sin.flow()
|
subscribe().collect { magix ->
|
||||||
val cosFlow = device.cos.flow()
|
(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 ->
|
val sinCosFlow = sinFlow.zip(cosFlow) { sin, cos ->
|
||||||
sin.double!! to cos.double!!
|
sin.double!! to cos.double!!
|
||||||
}
|
}
|
||||||
@ -72,7 +92,7 @@ fun CoroutineScope.startDemoDeviceServer(device: DemoDevice): ApplicationEngine
|
|||||||
}
|
}
|
||||||
div("row") {
|
div("row") {
|
||||||
div("col-6") {
|
div("col-6") {
|
||||||
plot(container = container) {
|
plot(renderer = container) {
|
||||||
layout {
|
layout {
|
||||||
title = "sin property"
|
title = "sin property"
|
||||||
xaxis.title = "point index"
|
xaxis.title = "point index"
|
||||||
@ -87,7 +107,7 @@ fun CoroutineScope.startDemoDeviceServer(device: DemoDevice): ApplicationEngine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
div("col-6") {
|
div("col-6") {
|
||||||
plot(container = container) {
|
plot(renderer = container) {
|
||||||
layout {
|
layout {
|
||||||
title = "cos property"
|
title = "cos property"
|
||||||
xaxis.title = "point index"
|
xaxis.title = "point index"
|
||||||
@ -104,7 +124,7 @@ fun CoroutineScope.startDemoDeviceServer(device: DemoDevice): ApplicationEngine
|
|||||||
}
|
}
|
||||||
div("row") {
|
div("row") {
|
||||||
div("col-12") {
|
div("col-12") {
|
||||||
plot(container = container) {
|
plot(renderer = container) {
|
||||||
layout {
|
layout {
|
||||||
title = "cos vs sin"
|
title = "cos vs sin"
|
||||||
xaxis.title = "sin"
|
xaxis.title = "sin"
|
||||||
@ -121,7 +141,5 @@ fun CoroutineScope.startDemoDeviceServer(device: DemoDevice): ApplicationEngine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}.apply { start() }
|
||||||
return server
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
@startuml
|
||||||
title Simple call with callback
|
title Simple call with callback
|
||||||
|
|
||||||
Main -> Async: call
|
Main -> Async: call
|
||||||
|
activate Main
|
||||||
activate Async
|
activate Async
|
||||||
|
|
||||||
Async -> Main: result
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
2
gradlew
vendored
2
gradlew
vendored
@ -130,7 +130,7 @@ fi
|
|||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
# 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
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
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_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
@ -64,21 +64,6 @@ echo location of your Java installation.
|
|||||||
|
|
||||||
goto fail
|
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
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@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
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@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