Skip to content

Commit eaf0eea

Browse files
committed
Fix opcua hierarchy
1 parent 956e98c commit eaf0eea

File tree

5 files changed

+106
-74
lines changed

5 files changed

+106
-74
lines changed

controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package space.kscience.controls.opcua.server
22

3+
import kotlinx.coroutines.CoroutineScope
34
import kotlinx.coroutines.launch
45
import kotlinx.datetime.toJavaInstant
56
import org.eclipse.milo.opcua.sdk.core.AccessLevel
@@ -21,11 +22,8 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
2122
import space.kscience.controls.api.*
2223
import space.kscience.controls.manager.DeviceManager
2324
import space.kscience.controls.opcua.client.opcToMeta
24-
import space.kscience.dataforge.context.Context
2525
import space.kscience.dataforge.meta.Meta
2626
import space.kscience.dataforge.meta.ValueType
27-
import space.kscience.dataforge.names.Name
28-
import space.kscience.dataforge.names.plus
2927

3028

3129
public operator fun CachingDevice.get(propertyDescriptor: PropertyDescriptor): Meta? =
@@ -38,21 +36,24 @@ https://github.com/eclipse/milo/blob/master/milo-examples/server-examples/src/ma
3836
*/
3937

4038
public class DeviceNameSpace(
41-
private val context: Context,
39+
private val scope: CoroutineScope,
4240
server: OpcUaServer,
4341
public val deviceHub: DeviceHub
4442
) : ManagedNamespaceWithLifecycle(server, NAMESPACE_URI) {
4543

4644
private val subscription = SubscriptionModel(server, this)
4745

48-
private fun UaFolderNode.registerDeviceNodes(deviceName: Name, device: Device) {
46+
/**
47+
* Register device node within existing folder
48+
*/
49+
private fun UaFolderNode.registerDeviceNodes(deviceName: String, device: Device) {
4950
val nodes = device.propertyDescriptors.associate { descriptor ->
5051
val propertyName = descriptor.name
5152

5253

5354
val node: UaVariableNode = UaVariableNode.UaVariableNodeBuilder(nodeContext).apply {
5455
//for now, use DF paths as ids
55-
nodeId = newNodeId("${deviceName.tokens.joinToString("/")}/$propertyName")
56+
nodeId = newNodeId("$deviceName/$propertyName")
5657
when {
5758
descriptor.readable && descriptor.mutable -> {
5859
setAccessLevel(AccessLevel.READ_WRITE)
@@ -90,7 +91,7 @@ public class DeviceNameSpace(
9091
setTypeDefinition(Identifiers.BaseDataVariableType)
9192
}.build()
9293

93-
// Update initial value, but only if it is cached
94+
// Update the initial value, but only if it is cached
9495
if (device is CachingDevice) {
9596
device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let {
9697
node.value = it
@@ -105,7 +106,7 @@ public class DeviceNameSpace(
105106
node.addAttributeObserver { _: UaNode, attributeId: AttributeId, value: Any? ->
106107
if (attributeId == AttributeId.Value) {
107108
val meta: Meta = opcToMeta(value)
108-
context.launch {
109+
scope.launch {
109110
device.writeProperty(propertyName, meta)
110111
}
111112
}
@@ -127,39 +128,70 @@ public class DeviceNameSpace(
127128
}
128129
}
129130
}
131+
130132
//recursively add sub-devices
131133
if (device is DeviceHub) {
132-
nodeContext.registerHub(device, deviceName)
134+
device.devices.forEach { (childDeviceName, device) ->
135+
136+
val deviceFolder = UaFolderNode(
137+
nodeContext,
138+
newNodeId("$deviceName/$childDeviceName"),
139+
newQualifiedName("$deviceName/$childDeviceName"),
140+
LocalizedText.english(childDeviceName.toString())
141+
)
142+
143+
deviceFolder.registerDeviceNodes("$deviceName/$childDeviceName", device)
144+
145+
nodeManager.addNode(deviceFolder)
146+
addOrganizes(deviceFolder)
147+
}
133148
}
134149
}
135150

136-
private fun UaNodeContext.registerHub(hub: DeviceHub, namePrefix: Name) {
151+
private fun UaNodeContext.registerTopLevelHub(hub: DeviceHub) {
152+
val rootNode = UaFolderNode(
153+
nodeContext,
154+
newNodeId("Controls"),
155+
newQualifiedName("Controls"),
156+
LocalizedText.english("Controls")
157+
)
158+
137159
hub.devices.forEach { (deviceName, device) ->
138-
val tokenAsString = deviceName.toString()
160+
val nameAsString = "$deviceName"
161+
139162
val deviceFolder = UaFolderNode(
140-
this,
141-
newNodeId(tokenAsString),
142-
newQualifiedName(tokenAsString),
143-
LocalizedText.english(tokenAsString)
144-
)
145-
deviceFolder.addReference(
146-
Reference(
147-
deviceFolder.nodeId,
148-
Identifiers.Organizes,
149-
Identifiers.ObjectsFolder.expanded(),
150-
false
151-
)
163+
nodeContext,
164+
newNodeId(nameAsString),
165+
newQualifiedName(nameAsString),
166+
LocalizedText.english(nameAsString)
152167
)
153-
deviceFolder.registerDeviceNodes(namePrefix + deviceName, device)
154-
this.nodeManager.addNode(deviceFolder)
168+
169+
deviceFolder.registerDeviceNodes(deviceName.toString(), device)
170+
171+
nodeManager.addNode(deviceFolder)
172+
173+
rootNode.addOrganizes(deviceFolder)
155174
}
175+
176+
nodeManager.addNode(rootNode)
177+
178+
rootNode.addReference(
179+
Reference(
180+
rootNode.nodeId,
181+
Identifiers.Organizes,
182+
Identifiers.ObjectsFolder.expanded(),
183+
false
184+
)
185+
)
186+
187+
156188
}
157189

158190
init {
159191
lifecycleManager.addLifecycle(subscription)
160192

161193
lifecycleManager.addStartupTask {
162-
nodeContext.registerHub(deviceHub, Name.EMPTY)
194+
nodeContext.registerTopLevelHub(deviceHub)
163195
}
164196

165197
lifecycleManager.addLifecycle(object : Lifecycle {
@@ -195,8 +227,8 @@ public class DeviceNameSpace(
195227
}
196228

197229

198-
public fun OpcUaServer.serveDevices(context: Context, deviceHub: DeviceHub): DeviceNameSpace =
199-
DeviceNameSpace(context, this, deviceHub).apply { startup() }
230+
public fun OpcUaServer.serveDevices(scope: CoroutineScope, deviceHub: DeviceHub): DeviceNameSpace =
231+
DeviceNameSpace(scope, this, deviceHub).apply { startup() }
200232

201233
/**
202234
* Serve devices from [deviceManager] as OPC-UA

demo/thermo/build.gradle.kts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ kotlin {
99
}
1010

1111
kscience {
12-
jvm()
12+
jvm {
13+
binaries {
14+
executable {
15+
mainClass = "center.sciprog.controls.demo.thermo.PanelKt"
16+
}
17+
}
18+
}
1319
useSerialization {
1420
json()
1521
}
@@ -27,10 +33,18 @@ kscience {
2733
}
2834
}
2935

30-
compose{
31-
desktop{
32-
application{
36+
compose {
37+
desktop {
38+
application {
3339
mainClass = "center.sciprog.controls.demo.thermo.PanelKt"
40+
41+
nativeDistributions {
42+
packageName = "ControlsThermoSensor"
43+
packageVersion = "1.0.0"
44+
windows {
45+
includeAllModules = true
46+
}
47+
}
3448
}
3549
}
3650
}
Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,16 @@
11
package center.sciprog.controls.demo.thermo
22

3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.Job
35
import org.eclipse.milo.opcua.sdk.server.OpcUaServer
46
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
5-
import space.kscience.controls.manager.DeviceManager
7+
import space.kscience.controls.api.DeviceHub
68
import space.kscience.controls.opcua.server.OpcUaServer
79
import space.kscience.controls.opcua.server.endpoint
810
import space.kscience.controls.opcua.server.serveDevices
9-
import space.kscience.dataforge.context.Context
10-
import space.kscience.dataforge.context.ContextAware
11-
import space.kscience.dataforge.context.request
1211

1312

14-
class ThermoHubController(
15-
val deviceManager: DeviceManager,
16-
val opcUaServer: OpcUaServer,
17-
val sensorHub: ThermoSensorHub
18-
) : ContextAware, AutoCloseable {
19-
20-
override val context: Context get() = deviceManager.context
21-
22-
fun start() {
23-
opcUaServer.startup()
24-
opcUaServer.serveDevices(deviceManager)
25-
}
26-
27-
override fun close() {
28-
opcUaServer.shutdown()
29-
}
30-
31-
}
32-
33-
fun ThermoHubController(sensorHub: ThermoSensorHub): ThermoHubController {
34-
35-
val context = sensorHub.context
36-
val deviceManager = context.request(DeviceManager)
13+
fun DeviceHub.serveOpc(scope: CoroutineScope): OpcUaServer {
3714

3815
val opcUaServer: OpcUaServer = OpcUaServer {
3916
setApplicationName(LocalizedText.english("center.sciprog.controls.thermo"))
@@ -43,9 +20,13 @@ fun ThermoHubController(sensorHub: ThermoSensorHub): ThermoHubController {
4320
}
4421
}
4522

46-
opcUaServer.serveDevices(deviceManager)
47-
// opcUaServer.startup()
23+
opcUaServer.serveDevices(scope, this)
24+
opcUaServer.startup()
4825

4926

50-
return ThermoHubController(deviceManager, opcUaServer, sensorHub)
27+
scope.coroutineContext[Job]?.invokeOnCompletion {
28+
opcUaServer.shutdown()
29+
}
30+
31+
return opcUaServer
5132
}

demo/thermo/src/jvmMain/kotlin/panel.kt

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ private val timeFormat = LocalDateTime.Format {
6868

6969
@OptIn(ExperimentalSplitPaneApi::class, ExperimentalKoalaPlotApi::class)
7070
@Composable
71-
private fun MainScreen(controller: ThermoHubController) {
71+
private fun MainScreen(hub: ThermoSensorHub) {
7272

7373
val plotEnabled = remember {
7474
SnapshotStateList<String>()
@@ -99,7 +99,7 @@ private fun MainScreen(controller: ThermoHubController) {
9999
.fillMaxHeight()
100100
.verticalScroll(rememberScrollState())
101101
) {
102-
controller.sensorHub.sensors.forEach { (sensorName, sensor) ->
102+
hub.sensors.forEach { (sensorName, sensor) ->
103103

104104
val temperature by sensor.temperature.asComposeState()
105105
val state by sensor.status.asComposeState()
@@ -164,16 +164,16 @@ private fun MainScreen(controller: ThermoHubController) {
164164
legendLocation = LegendLocation.BOTTOM
165165
) {
166166
XYGraph<Instant, Double>(
167-
xAxisModel = remember { TimeAxisModel.recent(maxAge, controller.context.clock, 100.dp) },
167+
xAxisModel = remember { TimeAxisModel.recent(maxAge, hub.context.clock, 100.dp) },
168168
yAxisModel = rememberDoubleLinearAxisModel(-10.0..110.0, minimumMajorTickIncrement = 10.0),
169169
xAxisTitle = "Time",
170170
xAxisLabels = { time -> time.toLocalDateTime(TimeZone.currentSystemDefault()).format(timeFormat) },
171171
yAxisLabels = { value -> String.format("%.2f", value)}
172172
) {
173173
plotEnabled.forEachIndexed { index, sensorName ->
174-
controller.sensorHub.sensors[sensorName]?.let { sensor ->
174+
hub.sensors[sensorName]?.let { sensor ->
175175
PlotNumberState(
176-
context = controller.context,
176+
context = hub.context,
177177
state = sensor.temperature,
178178
maxAge = maxAge,
179179
lineStyle = LineStyle(SolidColor(palette[index]))
@@ -193,7 +193,9 @@ fun main() = application {
193193
plugin(ClockManager)
194194
}
195195

196-
val configuration: Map<String, ThermoSensorConfig> = generateTestConfig()
196+
val configuration: Map<String, ThermoSensorConfig> = generateTestConfig(
197+
numberOfUnits = 1
198+
)
197199
context.launchModbusSimulator(configuration)
198200
Thread.sleep(200)
199201

@@ -202,8 +204,8 @@ fun main() = application {
202204

203205
val thermoHub = ModbusThermoSensorHub(context.request(DeviceManager), modbusMaster, configuration)
204206

205-
val controller = ThermoHubController(thermoHub)
206-
controller.start()
207+
thermoHub.serveOpc(context)
208+
207209

208210
Window(title = "ThermoSensor dashboard", onCloseRequest = {
209211
modbusMaster.disconnect()
@@ -212,7 +214,7 @@ fun main() = application {
212214
}) {
213215
window.minimumSize = Dimension(800, 400)
214216
MaterialTheme {
215-
MainScreen(controller)
217+
MainScreen(thermoHub)
216218
}
217219
}
218220
}

demo/thermo/src/jvmMain/kotlin/simulator.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,12 @@ internal fun CoroutineScope.launchModbusSimulator(configuration: Map<String, The
5757
}
5858
}
5959

60-
internal fun generateTestConfig(): Map<String, ThermoSensorConfig> = buildMap {
61-
repeat(10) { unit ->
62-
repeat(10) { address ->
60+
internal fun generateTestConfig(
61+
numberOfUnits: Int = 10,
62+
sensorsPerUnit: Int = 10
63+
): Map<String, ThermoSensorConfig> = buildMap {
64+
repeat(numberOfUnits) { unit ->
65+
repeat(sensorsPerUnit) { address ->
6366
put("$unit-$address", ThermoSensorConfig(unit, 1000 + address))
6467

6568
}

0 commit comments

Comments
 (0)