From e7cfb1d2ba7dfb8dbf6b58aab632a78fe2c8e819 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 16 Apr 2023 12:29:24 +0300 Subject: [PATCH] Add opcua test --- controls-opcua/build.gradle.kts | 8 +- .../controls/opcua/client/MiloDevice.kt | 55 +++++++++-- .../controls/opcua/client/MiloDeviceBySpec.kt | 91 +++++++++---------- .../controls/opcua/client/OpcUaClientTest.kt | 47 ++++++++++ 4 files changed, 145 insertions(+), 56 deletions(-) create mode 100644 controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt diff --git a/controls-opcua/build.gradle.kts b/controls-opcua/build.gradle.kts index 8fe7564..1be4f75 100644 --- a/controls-opcua/build.gradle.kts +++ b/controls-opcua/build.gradle.kts @@ -4,14 +4,16 @@ plugins { val ktorVersion: String by rootProject.extra -val miloVersion: String = "0.6.7" +val miloVersion: String = "0.6.9" dependencies { - api(project(":controls-core")) - api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${space.kscience.gradle.KScienceVersions.coroutinesVersion}") + api(projects.controlsCore) + api(spclibs.kotlinx.coroutines.jdk8) api("org.eclipse.milo:sdk-client:$miloVersion") api("org.eclipse.milo:bsd-parser:$miloVersion") api("org.eclipse.milo:sdk-server:$miloVersion") + + testImplementation(spclibs.kotlinx.coroutines.test) } diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt index 7172e22..8132835 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt @@ -1,6 +1,8 @@ package space.kscience.controls.opcua.client import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import org.eclipse.milo.opcua.sdk.client.OpcUaClient import org.eclipse.milo.opcua.stack.core.types.builtin.* @@ -9,6 +11,8 @@ import space.kscience.controls.api.Device import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaSerializer import space.kscience.dataforge.meta.transformations.MetaConverter +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty /** @@ -19,11 +23,6 @@ public interface MiloDevice : Device { * The OPC-UA client initialized on first use */ public val client: OpcUaClient - - override fun close() { - client.disconnect() - super.close() - } } /** @@ -80,4 +79,48 @@ public suspend inline fun MiloDevice.writeOpc( ): StatusCode { val meta = converter.objectToMeta(value) return client.writeValue(nodeId, DataValue(Variant(meta))).await() -} \ No newline at end of file +} + + +/** + * A device-bound OPC-UA property. Does not trigger device properties change. + */ +public inline fun MiloDevice.opc( + nodeId: NodeId, + converter: MetaConverter, + magAge: Double = 500.0 +): ReadWriteProperty = object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking { + readOpc(nodeId, converter, magAge) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + launch { + writeOpc(nodeId, converter, value) + } + } +} + +/** + * Register a mutable OPC-UA based [Double] property in a device spec + */ +public fun MiloDevice.opcDouble( + nodeId: NodeId, + magAge: Double = 1.0 +): ReadWriteProperty = opc(nodeId, MetaConverter.double, magAge) + +/** + * Register a mutable OPC-UA based [Int] property in a device spec + */ +public fun MiloDevice.opcInt( + nodeId: NodeId, + magAge: Double = 1.0 +): ReadWriteProperty = opc(nodeId, MetaConverter.int, magAge) + +/** + * Register a mutable OPC-UA based [String] property in a device spec + */ +public fun MiloDevice.opcString( + nodeId: NodeId, + magAge: Double = 1.0 +): ReadWriteProperty = opc(nodeId, MetaConverter.string, magAge) \ No newline at end of file diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt index cae0f5e..2a130d7 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt @@ -1,69 +1,66 @@ package space.kscience.controls.opcua.client -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.eclipse.milo.opcua.sdk.client.OpcUaClient -import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider +import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider +import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceSpec import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Global -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.Scheme +import space.kscience.dataforge.meta.SchemeSpec +import space.kscience.dataforge.meta.specOrNull import space.kscience.dataforge.meta.string -import space.kscience.dataforge.meta.transformations.MetaConverter -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty + +public sealed class MiloIdentity: Scheme() + +public class MiloUsername : MiloIdentity() { + + public var username: String by string{ error("Username not defined") } + public var password: String by string{ error("Password not defined") } + + public companion object : SchemeSpec(::MiloUsername) +} + +//public class MiloKeyPair : MiloIdentity() { +// +// public companion object : SchemeSpec(::MiloUsername) +//} + +public class MiloConfiguration : Scheme() { + + public var endpointUrl: String by string { error("Endpoint url is not defined") } + + public var username: MiloUsername? by specOrNull(MiloUsername) + + public companion object : SchemeSpec(::MiloConfiguration) +} + +/** + * A variant of [DeviceBySpec] that includes OPC-UA client + */ public open class MiloDeviceBySpec>( spec: DeviceSpec, + config: MiloConfiguration, context: Context = Global, - meta: Meta = Meta.EMPTY -) : MiloDevice, DeviceBySpec(spec, context, meta) { +) : MiloDevice, DeviceBySpec(spec, context, config.meta) { override val client: OpcUaClient by lazy { - val endpointUrl = meta["endpointUrl"].string ?: error("Endpoint url is not defined") - context.createMiloClient(endpointUrl).apply { + context.createMiloClient( + config.endpointUrl, + securityPolicy = SecurityPolicy.None, + identityProvider = config.username?.let { + UsernameProvider(it.username,it.password) + } ?: AnonymousProvider() + ).apply { connect().get() } } override fun close() { - super.close() + client.disconnect() super.close() } } - -/** - * A device-bound OPC-UA property. Does not trigger device properties change. - */ -public inline fun MiloDeviceBySpec<*>.opc( - nodeId: NodeId, - converter: MetaConverter, - magAge: Double = 500.0 -): ReadWriteProperty = object : ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking { - readOpc(nodeId, converter, magAge) - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - launch { - writeOpc(nodeId, converter, value) - } - } -} - -public inline fun MiloDeviceBySpec<*>.opcDouble( - nodeId: NodeId, - magAge: Double = 1.0 -): ReadWriteProperty = opc(nodeId, MetaConverter.double, magAge) - -public inline fun MiloDeviceBySpec<*>.opcInt( - nodeId: NodeId, - magAge: Double = 1.0 -): ReadWriteProperty = opc(nodeId, MetaConverter.int, magAge) - -public inline fun MiloDeviceBySpec<*>.opcString( - nodeId: NodeId, - magAge: Double = 1.0 -): ReadWriteProperty = opc(nodeId, MetaConverter.string, magAge) \ No newline at end of file diff --git a/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt new file mode 100644 index 0000000..88d7e8d --- /dev/null +++ b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt @@ -0,0 +1,47 @@ +package space.kscience.controls.opcua.client + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.junit.jupiter.api.Test +import space.kscience.controls.opcua.client.OpcUaClientTest.DemoMiloDevice.Companion.randomDouble +import space.kscience.controls.spec.DeviceSpec +import space.kscience.controls.spec.doubleProperty +import space.kscience.dataforge.meta.transformations.MetaConverter + +class OpcUaClientTest { + class DemoMiloDevice(config: MiloConfiguration) : MiloDeviceBySpec(DemoMiloDevice, config) { + + //val randomDouble by opcDouble(NodeId(2, "Dynamic/RandomDouble")) + + suspend fun readRandomDouble() = readOpc(NodeId(2, "Dynamic/RandomDouble"), MetaConverter.double) + + + companion object : DeviceSpec() { + fun build(): DemoMiloDevice { + val config = MiloConfiguration { + endpointUrl = "opc.tcp://milo.digitalpetri.com:62541/milo" +// username = MiloUsername{ +// username = "user1" +// password = "password" +// } + } + return DemoMiloDevice(config) + } + + inline fun use(block: DemoMiloDevice.() -> R): R = build().use(block) + + val randomDouble by doubleProperty(read = DemoMiloDevice::readRandomDouble) + + } + + } + + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testReadDouble() = runTest { + println(DemoMiloDevice.use { randomDouble.read() }) + } + +} \ No newline at end of file