diff --git a/build.gradle.kts b/build.gradle.kts index c88c3c8..6d7df71 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt index c64af12..9d6e9ca 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt @@ -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().onEach(callback).launchIn(this) + + //public suspend fun Device.execute(name: String, meta: Meta?): Meta? = execute(name, meta?.let { MetaNode(it) }) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt index 1558a4b..b85df5b 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt @@ -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, diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/descriptors.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/descriptors.kt index d7013e2..1e70962 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/descriptors.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/descriptors.kt @@ -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 } diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt index 75781e6..0480669 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt @@ -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() } } } diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt index 35e59ba..a1405c9 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt @@ -23,7 +23,7 @@ public abstract class DeviceSpec>( private val _actions = HashMap>() public val actions: Map> get() = _actions - public fun registerProperty(deviceProperty: DevicePropertySpec): DevicePropertySpec { + public fun > registerProperty(deviceProperty: P): P { _properties[deviceProperty.name] = deviceProperty return deviceProperty } @@ -43,26 +43,32 @@ public abstract class DeviceSpec>( return registerProperty(deviceProperty) } - public fun registerProperty( + public fun property( converter: MetaConverter, readWriteProperty: KMutableProperty1, descriptorBuilder: PropertyDescriptor.() -> Unit = {} - ): WritableDevicePropertySpec { - val deviceProperty = object : WritableDevicePropertySpec { - override val name: String = readWriteProperty.name - override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder) - override val converter: MetaConverter = converter - override suspend fun read(device: D): T = withContext(device.coroutineContext) { - readWriteProperty.get(device) - } + ): PropertyDelegateProvider, ReadOnlyProperty>> = + PropertyDelegateProvider { _, property -> + val deviceProperty = object : WritableDevicePropertySpec { + 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 = 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 property( converter: MetaConverter, @@ -79,7 +85,7 @@ public abstract class DeviceSpec>( override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() } } - _properties[propertyName] = deviceProperty + registerProperty(deviceProperty) ReadOnlyProperty, DevicePropertySpec> { _, _ -> deviceProperty } diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt index 9261066..d087505 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt @@ -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 > DeviceSpec.booleanProperty( name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, read: suspend D.() -> Boolean -): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = - property(MetaConverter.boolean, name, descriptorBuilder, read) +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = 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 > DeviceSpec.numberProperty( name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, read: suspend D.() -> Number -): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = - property(MetaConverter.number, name, descriptorBuilder, read) +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( + MetaConverter.number, + name, + numberDescriptor(descriptorBuilder), + read +) public fun > DeviceSpec.doubleProperty( name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, read: suspend D.() -> Double -): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = - property(MetaConverter.double, name, descriptorBuilder, read) +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( + MetaConverter.double, + name, + numberDescriptor(descriptorBuilder), + read +) public fun > DeviceSpec.stringProperty( name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, read: suspend D.() -> String -): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = - property(MetaConverter.string, name, descriptorBuilder, read) +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( + MetaConverter.string, + name, + { + metaDescriptor { + type(ValueType.STRING) + } + descriptorBuilder() + }, + read +) public fun > DeviceSpec.metaProperty( name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, read: suspend D.() -> Meta -): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = - property(MetaConverter.meta, name, descriptorBuilder, read) +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( + MetaConverter.meta, + name, + { + metaDescriptor { + type(ValueType.STRING) + } + descriptorBuilder() + }, + read +) //read-write delegates @@ -52,7 +97,18 @@ public fun > DeviceSpec.booleanProperty( read: suspend D.() -> Boolean, write: suspend D.(Boolean) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = - property(MetaConverter.boolean, name, descriptorBuilder, read, write) + property( + MetaConverter.boolean, + name, + { + metaDescriptor { + type(ValueType.BOOLEAN) + } + descriptorBuilder() + }, + read, + write + ) public fun > DeviceSpec.numberProperty( @@ -61,7 +117,7 @@ public fun > DeviceSpec.numberProperty( read: suspend D.() -> Number, write: suspend D.(Number) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = - property(MetaConverter.number, name, descriptorBuilder, read, write) + property(MetaConverter.number, name, numberDescriptor(descriptorBuilder), read, write) public fun > DeviceSpec.doubleProperty( name: String? = null, @@ -69,7 +125,7 @@ public fun > DeviceSpec.doubleProperty( read: suspend D.() -> Double, write: suspend D.(Double) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = - property(MetaConverter.double, name, descriptorBuilder, read, write) + property(MetaConverter.double, name, numberDescriptor(descriptorBuilder), read, write) public fun > DeviceSpec.stringProperty( name: String? = null, diff --git a/controls-opcua/build.gradle.kts b/controls-opcua/build.gradle.kts index c3874f2..a2a225d 100644 --- a/controls-opcua/build.gradle.kts +++ b/controls-opcua/build.gradle.kts @@ -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") } diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MetaBsdParser.kt b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MetaBsdParser.kt similarity index 99% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MetaBsdParser.kt rename to controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MetaBsdParser.kt index aaa8346..171b74e 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MetaBsdParser.kt +++ b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MetaBsdParser.kt @@ -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 diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDevice.kt b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDevice.kt similarity index 64% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDevice.kt rename to controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDevice.kt index 7ea36de..de56325 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDevice.kt +++ b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDevice.kt @@ -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 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 MiloDevice.readOpcWithTime( nodeId: NodeId, converter: MetaConverter, magAge: Double = 500.0 @@ -38,21 +44,29 @@ public suspend inline fun 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 MiloDevice.readOpc( nodeId: NodeId, converter: MetaConverter, 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") } diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDeviceBySpec.kt b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDeviceBySpec.kt similarity index 97% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDeviceBySpec.kt rename to controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDeviceBySpec.kt index b60498e..351115c 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDeviceBySpec.kt +++ b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDeviceBySpec.kt @@ -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 diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/miloClient.kt b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/miloClient.kt similarity index 88% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/miloClient.kt rename to controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/miloClient.kt index 596bf88..2d489d6 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/miloClient.kt +++ b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/miloClient.kt @@ -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?.toOptional(): Optional = 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 -> - 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())) +// } } \ No newline at end of file diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/DeviceNameSpace.kt b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/DeviceNameSpace.kt new file mode 100644 index 0000000..5c4a6f5 --- /dev/null +++ b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/DeviceNameSpace.kt @@ -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?) { + subscription.onDataItemsCreated(dataItems) + } + + override fun onDataItemsModified(dataItems: List?) { + subscription.onDataItemsModified(dataItems) + } + + override fun onDataItemsDeleted(dataItems: List?) { + subscription.onDataItemsDeleted(dataItems) + } + + override fun onMonitoringModeChanged(monitoredItems: List?) { + 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() } \ No newline at end of file diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/metaToOpc.kt b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/metaToOpc.kt new file mode 100644 index 0000000..ff23bf6 --- /dev/null +++ b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/metaToOpc.kt @@ -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) +} \ No newline at end of file diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/nodeUtils.kt b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/nodeUtils.kt new file mode 100644 index 0000000..783c229 --- /dev/null +++ b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/nodeUtils.kt @@ -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 +//) + + diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/serverUtils.kt b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/serverUtils.kt new file mode 100644 index 0000000..6ba3d36 --- /dev/null +++ b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/serverUtils.kt @@ -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)) +} diff --git a/controls-server/src/main/kotlin/ru/mipt/npm/controls/server/deviceWebServer.kt b/controls-server/src/main/kotlin/ru/mipt/npm/controls/server/deviceWebServer.kt index cb4639a..f896a24 100644 --- a/controls-server/src/main/kotlin/ru/mipt/npm/controls/server/deviceWebServer.kt +++ b/controls-server/src/main/kotlin/ru/mipt/npm/controls/server/deviceWebServer.kt @@ -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)) } }) } diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index c9f3956..0fb18d9 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -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") diff --git a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoControllerView.kt b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoControllerView.kt index 4f61194..7fa6252 100644 --- a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoControllerView.kt +++ b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoControllerView.kt @@ -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) diff --git a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt index 8d88199..42a7bd8 100644 --- a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt +++ b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt @@ -15,9 +15,9 @@ class DemoDevice : DeviceBySpec(DemoDevice) { companion object : DeviceSpec(::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() diff --git a/settings.gradle.kts b/settings.gradle.kts index 8b8e683..ddaeba6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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()