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")
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
|
@ -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) })
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
@ -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()))
|
||||
// }
|
||||
}
|
@ -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.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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user