Fix property names and types for DeviceSpec
This commit is contained in:
parent
3aa00ec491
commit
553d819c54
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("ru.mipt.npm.gradle.project")
|
id("ru.mipt.npm.gradle.project")
|
||||||
}
|
}
|
||||||
|
|
||||||
val dataforgeVersion: String by extra("0.5.0-dev-7")
|
val dataforgeVersion: String by extra("0.5.1")
|
||||||
val ktorVersion: String by extra(ru.mipt.npm.gradle.KScienceVersions.ktorVersion)
|
val ktorVersion: String by extra(ru.mipt.npm.gradle.KScienceVersions.ktorVersion)
|
||||||
val rsocketVersion by extra("0.13.1")
|
val rsocketVersion by extra("0.13.1")
|
||||||
|
|
||||||
|
@ -2,8 +2,12 @@ package ru.mipt.npm.controls.api
|
|||||||
|
|
||||||
import io.ktor.utils.io.core.Closeable
|
import io.ktor.utils.io.core.Closeable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.Flow
|
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 ru.mipt.npm.controls.api.Device.Companion.DEVICE_TARGET
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import space.kscience.dataforge.context.ContextAware
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
@ -70,7 +74,6 @@ public interface Device : Closeable, ContextAware, CoroutineScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the logical state of property or suspend to read the physical value.
|
* Get the logical state of property or suspend to read the physical value.
|
||||||
*/
|
*/
|
||||||
@ -86,4 +89,11 @@ public fun Device.getProperties(): Meta = Meta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe on property changes for the whole device
|
||||||
|
*/
|
||||||
|
public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job =
|
||||||
|
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this)
|
||||||
|
|
||||||
|
|
||||||
//public suspend fun Device.execute(name: String, meta: Meta?): Meta? = execute(name, meta?.let { MetaNode(it) })
|
//public suspend fun Device.execute(name: String, meta: Meta?): Meta? = execute(name, meta?.let { MetaNode(it) })
|
@ -42,14 +42,13 @@ public sealed class DeviceMessage {
|
|||||||
/**
|
/**
|
||||||
* Notify that property is changed. [sourceDevice] is mandatory.
|
* Notify that property is changed. [sourceDevice] is mandatory.
|
||||||
* [property] corresponds to property name.
|
* [property] corresponds to property name.
|
||||||
* [value] could be null if the property is invalidated.
|
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("property.changed")
|
@SerialName("property.changed")
|
||||||
public data class PropertyChangedMessage(
|
public data class PropertyChangedMessage(
|
||||||
public val property: String,
|
public val property: String,
|
||||||
public val value: Meta?,
|
public val value: Meta,
|
||||||
override val sourceDevice: Name = Name.EMPTY,
|
override val sourceDevice: Name = Name.EMPTY,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
|
@ -1,22 +1,32 @@
|
|||||||
package ru.mipt.npm.controls.api
|
package ru.mipt.npm.controls.api
|
||||||
|
|
||||||
import space.kscience.dataforge.meta.Scheme
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.dataforge.meta.string
|
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||||
|
import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder
|
||||||
|
|
||||||
|
//TODO add proper builders
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A descriptor for property
|
* A descriptor for property
|
||||||
*/
|
*/
|
||||||
public class PropertyDescriptor(name: String) : Scheme() {
|
@Serializable
|
||||||
public val name: String by string(name)
|
public class PropertyDescriptor(
|
||||||
public var info: String? by string()
|
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
|
* A descriptor for property
|
||||||
*/
|
*/
|
||||||
public class ActionDescriptor(name: String) : Scheme() {
|
@Serializable
|
||||||
public val name: String by string(name)
|
public class ActionDescriptor(public val name: String) {
|
||||||
public var info: String? by string()
|
public var info: String? = null
|
||||||
//var descriptor by spec(ItemDescriptor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,8 +5,11 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
import ru.mipt.npm.controls.api.*
|
import ru.mipt.npm.controls.api.*
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.toMeta
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.plus
|
import space.kscience.dataforge.names.plus
|
||||||
|
|
||||||
@ -48,12 +51,12 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
|||||||
val descriptionMeta = Meta {
|
val descriptionMeta = Meta {
|
||||||
"properties" put {
|
"properties" put {
|
||||||
propertyDescriptors.map { descriptor ->
|
propertyDescriptors.map { descriptor ->
|
||||||
descriptor.name put descriptor.toMeta()
|
descriptor.name put Json.encodeToJsonElement(descriptor).toMeta()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"actions" put {
|
"actions" put {
|
||||||
actionDescriptors.map { descriptor ->
|
actionDescriptors.map { descriptor ->
|
||||||
descriptor.name put descriptor.toMeta()
|
descriptor.name put Json.encodeToJsonElement(descriptor).toMeta()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
|
|||||||
private val _actions = HashMap<String, DeviceActionSpec<D, *, *>>()
|
private val _actions = HashMap<String, DeviceActionSpec<D, *, *>>()
|
||||||
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = _actions
|
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = _actions
|
||||||
|
|
||||||
public fun <T : Any> registerProperty(deviceProperty: DevicePropertySpec<D, T>): DevicePropertySpec<D, T> {
|
public fun <T : Any, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P {
|
||||||
_properties[deviceProperty.name] = deviceProperty
|
_properties[deviceProperty.name] = deviceProperty
|
||||||
return deviceProperty
|
return deviceProperty
|
||||||
}
|
}
|
||||||
@ -43,26 +43,32 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
|
|||||||
return registerProperty(deviceProperty)
|
return registerProperty(deviceProperty)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <T : Any> registerProperty(
|
public fun <T : Any> property(
|
||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
readWriteProperty: KMutableProperty1<D, T>,
|
readWriteProperty: KMutableProperty1<D, T>,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
|
||||||
): WritableDevicePropertySpec<D, T> {
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
|
||||||
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
PropertyDelegateProvider { _, property ->
|
||||||
override val name: String = readWriteProperty.name
|
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
||||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
|
override val name: String = property.name
|
||||||
override val converter: MetaConverter<T> = converter
|
override val descriptor: PropertyDescriptor = PropertyDescriptor(name).apply {
|
||||||
override suspend fun read(device: D): T = withContext(device.coroutineContext) {
|
//TODO add type from converter
|
||||||
readWriteProperty.get(device)
|
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) = withContext(device.coroutineContext) {
|
override suspend fun write(device: D, value: T) = withContext(device.coroutineContext) {
|
||||||
readWriteProperty.set(device, value)
|
readWriteProperty.set(device, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerProperty(deviceProperty)
|
||||||
|
ReadOnlyProperty { _, _ ->
|
||||||
|
deviceProperty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
registerProperty(deviceProperty)
|
|
||||||
return deviceProperty
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <T : Any> property(
|
public fun <T : Any> property(
|
||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
@ -79,7 +85,7 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
|
|||||||
|
|
||||||
override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() }
|
override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() }
|
||||||
}
|
}
|
||||||
_properties[propertyName] = deviceProperty
|
registerProperty(deviceProperty)
|
||||||
ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ ->
|
ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ ->
|
||||||
deviceProperty
|
deviceProperty
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package ru.mipt.npm.controls.properties
|
package ru.mipt.npm.controls.properties
|
||||||
|
|
||||||
import ru.mipt.npm.controls.api.PropertyDescriptor
|
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.Meta
|
||||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
|
import space.kscience.dataforge.values.ValueType
|
||||||
import kotlin.properties.PropertyDelegateProvider
|
import kotlin.properties.PropertyDelegateProvider
|
||||||
import kotlin.properties.ReadOnlyProperty
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
|
||||||
@ -12,37 +14,80 @@ public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
|
|||||||
name: String? = null,
|
name: String? = null,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> Boolean
|
read: suspend D.() -> Boolean
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
|
||||||
property(MetaConverter.boolean, name, descriptorBuilder, read)
|
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(
|
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> Number
|
read: suspend D.() -> Number
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
|
||||||
property(MetaConverter.number, name, descriptorBuilder, read)
|
MetaConverter.number,
|
||||||
|
name,
|
||||||
|
numberDescriptor(descriptorBuilder),
|
||||||
|
read
|
||||||
|
)
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
|
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> Double
|
read: suspend D.() -> Double
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
|
||||||
property(MetaConverter.double, name, descriptorBuilder, read)
|
MetaConverter.double,
|
||||||
|
name,
|
||||||
|
numberDescriptor(descriptorBuilder),
|
||||||
|
read
|
||||||
|
)
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
|
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> String
|
read: suspend D.() -> String
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
|
||||||
property(MetaConverter.string, name, descriptorBuilder, read)
|
MetaConverter.string,
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
metaDescriptor {
|
||||||
|
type(ValueType.STRING)
|
||||||
|
}
|
||||||
|
descriptorBuilder()
|
||||||
|
},
|
||||||
|
read
|
||||||
|
)
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty(
|
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty(
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> Meta
|
read: suspend D.() -> Meta
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
|
||||||
property(MetaConverter.meta, name, descriptorBuilder, read)
|
MetaConverter.meta,
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
metaDescriptor {
|
||||||
|
type(ValueType.STRING)
|
||||||
|
}
|
||||||
|
descriptorBuilder()
|
||||||
|
},
|
||||||
|
read
|
||||||
|
)
|
||||||
|
|
||||||
//read-write delegates
|
//read-write delegates
|
||||||
|
|
||||||
@ -52,7 +97,18 @@ public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
|
|||||||
read: suspend D.() -> Boolean,
|
read: suspend D.() -> Boolean,
|
||||||
write: suspend D.(Boolean) -> Unit
|
write: suspend D.(Boolean) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
|
||||||
property(MetaConverter.boolean, name, descriptorBuilder, read, write)
|
property(
|
||||||
|
MetaConverter.boolean,
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
metaDescriptor {
|
||||||
|
type(ValueType.BOOLEAN)
|
||||||
|
}
|
||||||
|
descriptorBuilder()
|
||||||
|
},
|
||||||
|
read,
|
||||||
|
write
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
|
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
|
||||||
@ -61,7 +117,7 @@ public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
|
|||||||
read: suspend D.() -> Number,
|
read: suspend D.() -> Number,
|
||||||
write: suspend D.(Number) -> Unit
|
write: suspend D.(Number) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> =
|
||||||
property(MetaConverter.number, name, descriptorBuilder, read, write)
|
property(MetaConverter.number, name, numberDescriptor(descriptorBuilder), read, write)
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
|
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
@ -69,7 +125,7 @@ public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
|
|||||||
read: suspend D.() -> Double,
|
read: suspend D.() -> Double,
|
||||||
write: suspend D.(Double) -> Unit
|
write: suspend D.(Double) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> =
|
||||||
property(MetaConverter.double, name, descriptorBuilder, read, write)
|
property(MetaConverter.double, name, numberDescriptor(descriptorBuilder), read, write)
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
|
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
|
@ -9,7 +9,9 @@ val miloVersion: String = "0.6.3"
|
|||||||
dependencies {
|
dependencies {
|
||||||
api(project(":controls-core"))
|
api(project(":controls-core"))
|
||||||
api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${ru.mipt.npm.gradle.KScienceVersions.coroutinesVersion}")
|
api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${ru.mipt.npm.gradle.KScienceVersions.coroutinesVersion}")
|
||||||
implementation("org.eclipse.milo:sdk-client:$miloVersion")
|
|
||||||
implementation("org.eclipse.milo:bsd-parser:$miloVersion")
|
api("org.eclipse.milo:sdk-client:$miloVersion")
|
||||||
implementation("org.eclipse.milo:dictionary-reader:$miloVersion")
|
api("org.eclipse.milo:bsd-parser:$miloVersion")
|
||||||
|
|
||||||
|
api("org.eclipse.milo:sdk-server:$miloVersion")
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package ru.mipt.npm.controls.opcua
|
package ru.mipt.npm.controls.opcua.client
|
||||||
|
|
||||||
import org.eclipse.milo.opcua.binaryschema.AbstractCodec
|
import org.eclipse.milo.opcua.binaryschema.AbstractCodec
|
||||||
import org.eclipse.milo.opcua.binaryschema.parser.BsdParser
|
import org.eclipse.milo.opcua.binaryschema.parser.BsdParser
|
@ -1,11 +1,13 @@
|
|||||||
package ru.mipt.npm.controls.opcua
|
package ru.mipt.npm.controls.opcua.client
|
||||||
|
|
||||||
import kotlinx.coroutines.future.await
|
import kotlinx.coroutines.future.await
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
|
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.builtin.*
|
||||||
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn
|
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn
|
||||||
import ru.mipt.npm.controls.api.Device
|
import ru.mipt.npm.controls.api.Device
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.MetaSerializer
|
||||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
|
|
||||||
|
|
||||||
@ -24,7 +26,11 @@ public interface MiloDevice : Device {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public suspend inline fun <reified T> MiloDevice.readOpcWithTime(
|
/**
|
||||||
|
* 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,
|
nodeId: NodeId,
|
||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
magAge: Double = 500.0
|
magAge: Double = 500.0
|
||||||
@ -38,21 +44,29 @@ public suspend inline fun <reified T> MiloDevice.readOpcWithTime(
|
|||||||
else -> error("Incompatible OPC property value $content")
|
else -> error("Incompatible OPC property value $content")
|
||||||
}
|
}
|
||||||
|
|
||||||
val res = converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
|
val res: T = converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
|
||||||
return res to time
|
return res to time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read and coerce value from OPC-UA
|
||||||
|
*/
|
||||||
public suspend inline fun <reified T> MiloDevice.readOpc(
|
public suspend inline fun <reified T> MiloDevice.readOpc(
|
||||||
nodeId: NodeId,
|
nodeId: NodeId,
|
||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
magAge: Double = 500.0
|
magAge: Double = 500.0
|
||||||
): T {
|
): T {
|
||||||
val data = client.readValue(magAge, TimestampsToReturn.Neither, nodeId).await()
|
val data: DataValue = client.readValue(magAge, TimestampsToReturn.Neither, nodeId).await()
|
||||||
|
|
||||||
val meta: Meta = when (val content = data.value.value) {
|
val content = data.value.value
|
||||||
is T -> return content
|
if(content is T) return content
|
||||||
content is Meta -> content as Meta
|
val meta: Meta = when (content) {
|
||||||
content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta
|
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")
|
else -> error("Incompatible OPC property value $content")
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package ru.mipt.npm.controls.opcua
|
package ru.mipt.npm.controls.opcua.client
|
||||||
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
@ -1,10 +1,9 @@
|
|||||||
package ru.mipt.npm.controls.opcua
|
package ru.mipt.npm.controls.opcua.client
|
||||||
|
|
||||||
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
|
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.config.OpcUaClientConfigBuilder
|
||||||
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider
|
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.sdk.client.api.identity.IdentityProvider
|
||||||
import org.eclipse.milo.opcua.sdk.client.dtd.DataTypeDictionarySessionInitializer
|
|
||||||
import org.eclipse.milo.opcua.stack.client.security.DefaultClientCertificateValidator
|
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.DefaultTrustListManager
|
||||||
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy
|
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy
|
||||||
@ -17,6 +16,9 @@ import space.kscience.dataforge.context.logger
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
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(
|
internal fun Context.createMiloClient(
|
||||||
@ -41,9 +43,7 @@ internal fun Context.createMiloClient(
|
|||||||
return OpcUaClient.create(
|
return OpcUaClient.create(
|
||||||
endpointUrl,
|
endpointUrl,
|
||||||
{ endpoints: List<EndpointDescription?> ->
|
{ endpoints: List<EndpointDescription?> ->
|
||||||
endpoints.stream()
|
endpoints.firstOrNull(endpointFilter).toOptional()
|
||||||
.filter(endpointFilter)
|
|
||||||
.findFirst()
|
|
||||||
}
|
}
|
||||||
) { configBuilder: OpcUaClientConfigBuilder ->
|
) { configBuilder: OpcUaClientConfigBuilder ->
|
||||||
configBuilder
|
configBuilder
|
||||||
@ -56,7 +56,8 @@ internal fun Context.createMiloClient(
|
|||||||
.setIdentityProvider(identityProvider)
|
.setIdentityProvider(identityProvider)
|
||||||
.setRequestTimeout(uint(5000))
|
.setRequestTimeout(uint(5000))
|
||||||
.build()
|
.build()
|
||||||
}.apply {
|
|
||||||
addSessionInitializer(DataTypeDictionarySessionInitializer(MetaBsdParser()))
|
|
||||||
}
|
}
|
||||||
|
// .apply {
|
||||||
|
// addSessionInitializer(DataTypeDictionarySessionInitializer(MetaBsdParser()))
|
||||||
|
// }
|
||||||
}
|
}
|
@ -0,0 +1,209 @@
|
|||||||
|
package ru.mipt.npm.controls.opcua.server
|
||||||
|
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
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.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()?.let {
|
||||||
|
node.value = it
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to node value changes
|
||||||
|
*/
|
||||||
|
node.addAttributeObserver { uaNode: 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 ->
|
||||||
|
node.value = value.toOpc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//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,30 @@
|
|||||||
|
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.*
|
||||||
|
|
||||||
|
internal fun Meta.toOpc(statusCode: StatusCode = StatusCode.GOOD, time: 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, time)
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
@ -22,8 +22,10 @@ import io.ktor.websocket.WebSockets
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import ru.mipt.npm.controls.api.DeviceMessage
|
import ru.mipt.npm.controls.api.DeviceMessage
|
||||||
import ru.mipt.npm.controls.api.PropertyGetMessage
|
import ru.mipt.npm.controls.api.PropertyGetMessage
|
||||||
@ -35,7 +37,6 @@ import ru.mipt.npm.magix.api.MagixEndpoint
|
|||||||
import ru.mipt.npm.magix.server.GenericMagixMessage
|
import ru.mipt.npm.magix.server.GenericMagixMessage
|
||||||
import ru.mipt.npm.magix.server.launchMagixServerRawRSocket
|
import ru.mipt.npm.magix.server.launchMagixServerRawRSocket
|
||||||
import ru.mipt.npm.magix.server.magixModule
|
import ru.mipt.npm.magix.server.magixModule
|
||||||
import space.kscience.dataforge.meta.toJson
|
|
||||||
import space.kscience.dataforge.meta.toMeta
|
import space.kscience.dataforge.meta.toMeta
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.asName
|
import space.kscience.dataforge.names.asName
|
||||||
@ -116,7 +117,7 @@ public fun Application.deviceManagerModule(
|
|||||||
li {
|
li {
|
||||||
a(href = "../$deviceName/${property.name}/get") { +"${property.name}: " }
|
a(href = "../$deviceName/${property.name}/get") { +"${property.name}: " }
|
||||||
code {
|
code {
|
||||||
+property.toMeta().toJson().toString()
|
+Json.encodeToString(property)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,7 +128,7 @@ public fun Application.deviceManagerModule(
|
|||||||
li {
|
li {
|
||||||
+("${action.name}: ")
|
+("${action.name}: ")
|
||||||
code {
|
code {
|
||||||
+action.toMeta().toJson().toString()
|
+Json.encodeToString(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,12 +145,12 @@ public fun Application.deviceManagerModule(
|
|||||||
put("target", name.toString())
|
put("target", name.toString())
|
||||||
put("properties", buildJsonArray {
|
put("properties", buildJsonArray {
|
||||||
device.propertyDescriptors.forEach { descriptor ->
|
device.propertyDescriptors.forEach { descriptor ->
|
||||||
add(descriptor.toMeta().toJson())
|
add(Json.encodeToJsonElement(descriptor))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
put("actions", buildJsonArray {
|
put("actions", buildJsonArray {
|
||||||
device.actionDescriptors.forEach { actionDescriptor ->
|
device.actionDescriptors.forEach { actionDescriptor ->
|
||||||
add(actionDescriptor.toMeta().toJson())
|
add(Json.encodeToJsonElement(actionDescriptor))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,8 @@ dependencies{
|
|||||||
implementation(projects.controlsMagixClient)
|
implementation(projects.controlsMagixClient)
|
||||||
implementation(projects.magix.magixRsocket)
|
implementation(projects.magix.magixRsocket)
|
||||||
implementation(projects.magix.magixZmq)
|
implementation(projects.magix.magixZmq)
|
||||||
|
implementation(projects.controlsOpcua)
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||||
implementation("no.tornado:tornadofx:1.7.20")
|
implementation("no.tornado:tornadofx:1.7.20")
|
||||||
implementation("space.kscience:plotlykt-server:0.5.0-dev-1")
|
implementation("space.kscience:plotlykt-server:0.5.0-dev-1")
|
||||||
|
@ -6,6 +6,8 @@ import javafx.scene.control.Slider
|
|||||||
import javafx.scene.layout.Priority
|
import javafx.scene.layout.Priority
|
||||||
import javafx.stage.Stage
|
import javafx.stage.Stage
|
||||||
import kotlinx.coroutines.launch
|
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.api.DeviceMessage
|
||||||
import ru.mipt.npm.controls.client.connectToMagix
|
import ru.mipt.npm.controls.client.connectToMagix
|
||||||
import ru.mipt.npm.controls.controllers.DeviceManager
|
import ru.mipt.npm.controls.controllers.DeviceManager
|
||||||
@ -13,6 +15,9 @@ import ru.mipt.npm.controls.controllers.install
|
|||||||
import ru.mipt.npm.controls.demo.DemoDevice.Companion.cosScale
|
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.sinScale
|
||||||
import ru.mipt.npm.controls.demo.DemoDevice.Companion.timeScale
|
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.api.MagixEndpoint
|
||||||
import ru.mipt.npm.magix.rsocket.rSocketWithTcp
|
import ru.mipt.npm.magix.rsocket.rSocketWithTcp
|
||||||
import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets
|
import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets
|
||||||
@ -27,6 +32,13 @@ class DemoController : Controller(), ContextAware {
|
|||||||
var device: DemoDevice? = null
|
var device: DemoDevice? = null
|
||||||
var magixServer: ApplicationEngine? = null
|
var magixServer: ApplicationEngine? = null
|
||||||
var visualizer: 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") {
|
override val context = Context("demoDevice") {
|
||||||
plugin(DeviceManager)
|
plugin(DeviceManager)
|
||||||
@ -44,11 +56,16 @@ class DemoController : Controller(), ContextAware {
|
|||||||
deviceManager.connectToMagix(deviceEndpoint)
|
deviceManager.connectToMagix(deviceEndpoint)
|
||||||
val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost", DeviceMessage.serializer())
|
val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost", DeviceMessage.serializer())
|
||||||
visualizer = visualEndpoint.startDemoDeviceServer()
|
visualizer = visualEndpoint.startDemoDeviceServer()
|
||||||
|
|
||||||
|
opcUaServer.startup()
|
||||||
|
opcUaServer.serveDevices(deviceManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
logger.info { "Shutting down..." }
|
logger.info { "Shutting down..." }
|
||||||
|
opcUaServer.shutdown()
|
||||||
|
logger.info { "OpcUa server stopped" }
|
||||||
visualizer?.stop(1000, 5000)
|
visualizer?.stop(1000, 5000)
|
||||||
logger.info { "Visualization server stopped" }
|
logger.info { "Visualization server stopped" }
|
||||||
magixServer?.stop(1000, 5000)
|
magixServer?.stop(1000, 5000)
|
||||||
|
@ -15,9 +15,9 @@ class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
|
|||||||
|
|
||||||
companion object : DeviceSpec<DemoDevice>(::DemoDevice) {
|
companion object : DeviceSpec<DemoDevice>(::DemoDevice) {
|
||||||
// register virtual properties based on actual object state
|
// register virtual properties based on actual object state
|
||||||
val timeScale = registerProperty(MetaConverter.double, DemoDevice::timeScaleState)
|
val timeScale by property(MetaConverter.double, DemoDevice::timeScaleState)
|
||||||
val sinScale = registerProperty(MetaConverter.double, DemoDevice::sinScaleState)
|
val sinScale by property(MetaConverter.double, DemoDevice::sinScaleState)
|
||||||
val cosScale = registerProperty(MetaConverter.double, DemoDevice::cosScaleState)
|
val cosScale by property(MetaConverter.double, DemoDevice::cosScaleState)
|
||||||
|
|
||||||
val sin by doubleProperty {
|
val sin by doubleProperty {
|
||||||
val time = Instant.now()
|
val time = Instant.now()
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
rootProject.name = "controls-kt"
|
rootProject.name = "controls-kt"
|
||||||
|
|
||||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||||
|
enableFeaturePreview("VERSION_CATALOGS")
|
||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
val toolsVersion = "0.10.2"
|
val toolsVersion = "0.10.4"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
|
Loading…
Reference in New Issue
Block a user