Fix property names and types for DeviceSpec

This commit is contained in:
Alexander Nozik 2021-09-29 10:43:19 +03:00
parent 3aa00ec491
commit 553d819c54
21 changed files with 540 additions and 73 deletions

View File

@ -2,7 +2,7 @@ plugins {
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 rsocketVersion by extra("0.13.1")

View File

@ -2,8 +2,12 @@ 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
@ -70,7 +74,6 @@ public interface Device : Closeable, ContextAware, CoroutineScope {
}
}
/**
* 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) })

View File

@ -42,14 +42,13 @@ public sealed class DeviceMessage {
/**
* Notify that property is changed. [sourceDevice] is mandatory.
* [property] corresponds to property name.
* [value] could be null if the property is invalidated.
*
*/
@Serializable
@SerialName("property.changed")
public data class PropertyChangedMessage(
public val property: String,
public val value: Meta?,
public val value: Meta,
override val sourceDevice: Name = Name.EMPTY,
override val targetDevice: Name? = null,
override val comment: String? = null,

View File

@ -1,22 +1,32 @@
package ru.mipt.npm.controls.api
import space.kscience.dataforge.meta.Scheme
import space.kscience.dataforge.meta.string
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
*/
public class PropertyDescriptor(name: String) : Scheme() {
public val name: String by string(name)
public var info: String? by string()
@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
*/
public class ActionDescriptor(name: String) : Scheme() {
public val name: String by string(name)
public var info: String? by string()
//var descriptor by spec(ItemDescriptor)
@Serializable
public class ActionDescriptor(public val name: String) {
public var info: String? = null
}

View File

@ -5,8 +5,11 @@ 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
@ -48,12 +51,12 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
val descriptionMeta = Meta {
"properties" put {
propertyDescriptors.map { descriptor ->
descriptor.name put descriptor.toMeta()
descriptor.name put Json.encodeToJsonElement(descriptor).toMeta()
}
}
"actions" put {
actionDescriptors.map { descriptor ->
descriptor.name put descriptor.toMeta()
descriptor.name put Json.encodeToJsonElement(descriptor).toMeta()
}
}
}

View File

@ -23,7 +23,7 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
private val _actions = HashMap<String, DeviceActionSpec<D, *, *>>()
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
return deviceProperty
}
@ -43,26 +43,32 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
return registerProperty(deviceProperty)
}
public fun <T : Any> registerProperty(
public fun <T : Any> property(
converter: MetaConverter<T>,
readWriteProperty: KMutableProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): WritableDevicePropertySpec<D, T> {
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
override val name: String = readWriteProperty.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) {
readWriteProperty.get(device)
}
): 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) = withContext(device.coroutineContext) {
readWriteProperty.set(device, value)
override suspend fun write(device: D, value: T) = withContext(device.coroutineContext) {
readWriteProperty.set(device, value)
}
}
registerProperty(deviceProperty)
ReadOnlyProperty { _, _ ->
deviceProperty
}
}
registerProperty(deviceProperty)
return deviceProperty
}
public fun <T : Any> property(
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() }
}
_properties[propertyName] = deviceProperty
registerProperty(deviceProperty)
ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ ->
deviceProperty
}

View File

@ -1,8 +1,10 @@
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
@ -12,37 +14,80 @@ 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, descriptorBuilder, read)
): 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, descriptorBuilder, read)
): 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, descriptorBuilder, read)
): 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, descriptorBuilder, read)
): 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, descriptorBuilder, read)
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
MetaConverter.meta,
name,
{
metaDescriptor {
type(ValueType.STRING)
}
descriptorBuilder()
},
read
)
//read-write delegates
@ -52,7 +97,18 @@ public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
read: suspend D.() -> Boolean,
write: suspend D.(Boolean) -> Unit
): 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(
@ -61,7 +117,7 @@ public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
read: suspend D.() -> Number,
write: suspend D.(Number) -> Unit
): 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(
name: String? = null,
@ -69,7 +125,7 @@ public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
read: suspend D.() -> Double,
write: suspend D.(Double) -> Unit
): 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(
name: String? = null,

View File

@ -9,7 +9,9 @@ val miloVersion: String = "0.6.3"
dependencies {
api(project(":controls-core"))
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")
implementation("org.eclipse.milo:dictionary-reader:$miloVersion")
api("org.eclipse.milo:sdk-client:$miloVersion")
api("org.eclipse.milo:bsd-parser:$miloVersion")
api("org.eclipse.milo:sdk-server:$miloVersion")
}

View File

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

View File

@ -1,11 +1,13 @@
package ru.mipt.npm.controls.opcua
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
@ -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,
converter: MetaConverter<T>,
magAge: Double = 500.0
@ -38,21 +44,29 @@ public suspend inline fun <reified T> MiloDevice.readOpcWithTime(
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
}
/**
* 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 = 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) {
is T -> return content
content is Meta -> content as Meta
content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta
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")
}

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.opcua
package ru.mipt.npm.controls.opcua.client
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

View File

@ -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.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.sdk.client.dtd.DataTypeDictionarySessionInitializer
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
@ -17,6 +16,9 @@ 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(
@ -41,9 +43,7 @@ internal fun Context.createMiloClient(
return OpcUaClient.create(
endpointUrl,
{ endpoints: List<EndpointDescription?> ->
endpoints.stream()
.filter(endpointFilter)
.findFirst()
endpoints.firstOrNull(endpointFilter).toOptional()
}
) { configBuilder: OpcUaClientConfigBuilder ->
configBuilder
@ -56,7 +56,8 @@ internal fun Context.createMiloClient(
.setIdentityProvider(identityProvider)
.setRequestTimeout(uint(5000))
.build()
}.apply {
addSessionInitializer(DataTypeDictionarySessionInitializer(MetaBsdParser()))
}
// .apply {
// addSessionInitializer(DataTypeDictionarySessionInitializer(MetaBsdParser()))
// }
}

View File

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

View File

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

View File

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

View File

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

View File

@ -22,8 +22,10 @@ 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
@ -35,7 +37,6 @@ 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.toJson
import space.kscience.dataforge.meta.toMeta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
@ -116,7 +117,7 @@ public fun Application.deviceManagerModule(
li {
a(href = "../$deviceName/${property.name}/get") { +"${property.name}: " }
code {
+property.toMeta().toJson().toString()
+Json.encodeToString(property)
}
}
}
@ -127,7 +128,7 @@ public fun Application.deviceManagerModule(
li {
+("${action.name}: ")
code {
+action.toMeta().toJson().toString()
+Json.encodeToString(action)
}
}
}
@ -144,12 +145,12 @@ public fun Application.deviceManagerModule(
put("target", name.toString())
put("properties", buildJsonArray {
device.propertyDescriptors.forEach { descriptor ->
add(descriptor.toMeta().toJson())
add(Json.encodeToJsonElement(descriptor))
}
})
put("actions", buildJsonArray {
device.actionDescriptors.forEach { actionDescriptor ->
add(actionDescriptor.toMeta().toJson())
add(Json.encodeToJsonElement(actionDescriptor))
}
})
}

View File

@ -22,6 +22,8 @@ dependencies{
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("space.kscience:plotlykt-server:0.5.0-dev-1")

View File

@ -6,6 +6,8 @@ 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
@ -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.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
@ -27,6 +32,13 @@ 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)
@ -44,11 +56,16 @@ class DemoController : Controller(), ContextAware {
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)

View File

@ -15,9 +15,9 @@ class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
companion object : DeviceSpec<DemoDevice>(::DemoDevice) {
// register virtual properties based on actual object state
val timeScale = registerProperty(MetaConverter.double, DemoDevice::timeScaleState)
val sinScale = registerProperty(MetaConverter.double, DemoDevice::sinScaleState)
val cosScale = registerProperty(MetaConverter.double, DemoDevice::cosScaleState)
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()

View File

@ -1,11 +1,13 @@
rootProject.name = "controls-kt"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
enableFeaturePreview("VERSION_CATALOGS")
pluginManagement {
val toolsVersion = "0.10.2"
val toolsVersion = "0.10.4"
repositories {
mavenLocal()
maven("https://repo.kotlin.link")
mavenCentral()
gradlePluginPortal()