1 package de.dlr.bt.stc.opcuaserver;
2
3 import java.util.HashSet;
4 import java.util.List;
5 import java.util.Map;
6 import java.util.Set;
7 import java.util.StringJoiner;
8 import java.util.function.BiFunction;
9 import java.util.function.Function;
10
11 import org.eclipse.milo.opcua.sdk.core.AccessLevel;
12 import org.eclipse.milo.opcua.sdk.core.Reference;
13 import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
14 import org.eclipse.milo.opcua.sdk.server.api.DataItem;
15 import org.eclipse.milo.opcua.sdk.server.api.ManagedNamespaceWithLifecycle;
16 import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem;
17 import org.eclipse.milo.opcua.sdk.server.api.methods.AbstractMethodInvocationHandler;
18 import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode;
19 import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode;
20 import org.eclipse.milo.opcua.sdk.server.nodes.UaNode;
21 import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode;
22 import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectTypeNode;
23 import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode;
24 import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilterContext;
25 import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilters;
26 import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel;
27 import org.eclipse.milo.opcua.stack.core.Identifiers;
28 import org.eclipse.milo.opcua.stack.core.UaException;
29 import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
30 import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
31 import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
32 import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
33 import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
34 import org.greenrobot.eventbus.EventBus;
35 import org.greenrobot.eventbus.Subscribe;
36
37 import de.dlr.bt.stc.config.ACfg;
38 import de.dlr.bt.stc.eventbus.ConfigurationModifiedEvent;
39 import de.dlr.bt.stc.eventbus.ConfigurationModifiedEvent.ConfigurationModificationType;
40 import de.dlr.bt.stc.eventbus.TaskModifiedEvent;
41 import de.dlr.bt.stc.eventbus.TaskModifiedEvent.TaskModificationType;
42 import de.dlr.bt.stc.opcuaserver.method.QuitMethod;
43 import de.dlr.bt.stc.opcuaserver.method.ReloadConfigurationMethod;
44 import de.dlr.bt.stc.opcuaserver.method.StartMethod;
45 import de.dlr.bt.stc.opcuaserver.method.StopMethod;
46 import de.dlr.bt.stc.opcuaserver.method.UnloadConfigurationMethod;
47 import lombok.Value;
48 import lombok.extern.slf4j.Slf4j;
49
50 @Slf4j
51 public class STCNamespace extends ManagedNamespaceWithLifecycle {
52 private static final String NAMESPACE_URI = "urn:dlr:stc:namespace";
53
54 private final SubscriptionModel subscriptionModel;
55
56 private final EventBus eventBus;
57
58 @Value
59 public static class Folders {
60 private UaFolderNode stcFolder;
61 private UaFolderNode configFolder;
62 private UaFolderNode sourceFolder;
63 private UaFolderNode sinkFolder;
64 private UaFolderNode bridgeFolder;
65 }
66
67 private Folders folders;
68
69 private Map<Class<?>, INodeCreator> nodeCreators;
70
71 public STCNamespace(OpcUaServer server, EventBus eventBus) {
72 super(server, NAMESPACE_URI);
73
74 this.eventBus = eventBus;
75
76 subscriptionModel = new SubscriptionModel(server, this);
77
78 getLifecycleManager().addLifecycle(subscriptionModel);
79
80 getLifecycleManager().addStartupTask(this::createAndAddNodes);
81 getLifecycleManager().addShutdownTask(() -> eventBus.unregister(this));
82
83 nodeCreators = NodeFactory.getInstance().getCreatorInstances(this);
84 }
85
86 private void createAndAddNodes() {
87 var stcFolder = createFolderNode("STC", newNodeId("stc"), Identifiers.ObjectsFolder);
88 var configFolder = createFolderNode("Configuration", newNodeId("stc", "configuration"), stcFolder.getNodeId());
89 var sourceFolder = createFolderNode("Sources", newNodeId("stc", "sources"), stcFolder.getNodeId());
90 var sinkFolder = createFolderNode("Sinks", newNodeId("stc", "sinks"), stcFolder.getNodeId());
91 var bridgeFolder = createFolderNode("Bridges", newNodeId("stc", "bridges"), stcFolder.getNodeId());
92
93 folders = new Folders(stcFolder, configFolder, sourceFolder, sinkFolder, bridgeFolder);
94
95
96 for (var creator : nodeCreators.values()) {
97 creator.createObjectType();
98 }
99
100 createManagementNodes();
101
102 eventBus.register(this);
103 }
104
105 public NodeId newNodeId(String... path) {
106 StringJoiner sj = new StringJoiner("/");
107 for (var p : path)
108 sj.add(p.toLowerCase());
109 return newNodeId(sj.toString());
110 }
111
112 @Subscribe
113 public void onConfigurationModified(ConfigurationModifiedEvent cme) {
114 log.info("Configuration modified event {}", cme);
115
116 var modtype = cme.getModification();
117
118 var creator = nodeCreators.get(cme.getConfiguration().getClass());
119 if (modtype == ConfigurationModificationType.ADD) {
120
121
122 if (creator != null)
123 creator.createInstance(cme.getConfiguration(), folders);
124 else
125 addGenericConfigurationNode(cme.getKey(), cme.getConfiguration());
126 } else if (modtype == ConfigurationModificationType.REMOVE) {
127 if (creator != null)
128 creator.removeInstance(cme.getConfiguration(), folders);
129 else
130 removeGenericConfigurationNode(cme.getKey());
131 }
132 }
133
134 @Subscribe
135 public void onTaskModified(TaskModifiedEvent tme) {
136 log.info("Task modified event: {}", tme);
137
138 var creator = nodeCreators.get(tme.getTask().getClass());
139 TaskModificationType modtype = tme.getModification();
140 if (modtype == TaskModificationType.INITIALIZE && creator != null) {
141 creator.createInstance(tme.getTask(), folders);
142 } else if (modtype == TaskModificationType.REMOVE && creator != null) {
143 creator.removeInstance(tme.getTask(), folders);
144 }
145 }
146
147 private void createManagementNodes() {
148 createMethodNode("stop", "stc/stop", "Stop sTC", StopMethod::new, folders.stcFolder.getNodeId());
149
150 createMethodNode("quit", "stc/quit", "Quit sTC", QuitMethod::new, folders.stcFolder.getNodeId());
151
152 createMethodNode("start", "stc/start", "Start sTC", StartMethod::new, folders.stcFolder.getNodeId());
153
154 createMethodNode("reloadConfiguration", "stc/reloadConfiguration", "Reload sTC configuration",
155 ReloadConfigurationMethod::new, folders.stcFolder.getNodeId());
156
157 createMethodNode("unloadConfiguration", "stc/unloadConfiguration", "Reload sTC configuration",
158 UnloadConfigurationMethod::new, folders.stcFolder.getNodeId());
159
160 }
161
162 private void createMethodNode(String qualifiedName, String nodeId, String description,
163 BiFunction<UaMethodNode, EventBus, AbstractMethodInvocationHandler> handlerFunc, NodeId folder) {
164 UaMethodNode methodNode = UaMethodNode.builder(getNodeContext()).setNodeId(newNodeId(nodeId))
165 .setBrowseName(newQualifiedName(qualifiedName)).setDisplayName(LocalizedText.english(qualifiedName))
166 .setDescription(LocalizedText.english(description)).build();
167
168 var handler = handlerFunc.apply(methodNode, eventBus);
169
170 methodNode.setInputArguments(handler.getInputArguments());
171 methodNode.setOutputArguments(handler.getOutputArguments());
172 methodNode.setInvocationHandler(handler);
173
174 getNodeManager().addNode(methodNode);
175
176 methodNode.addReference(
177 new Reference(methodNode.getNodeId(), Identifiers.HasComponent, folder.expanded(), false));
178 }
179
180
181
182
183
184
185
186
187
188 private void addGenericConfigurationNode(String key, ACfg configuration) {
189 UaVariableNode configNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
190 .setNodeId(newNodeId("stc/config/" + key)).setAccessLevel(AccessLevel.READ_ONLY)
191 .setUserAccessLevel(AccessLevel.READ_ONLY).setBrowseName(newQualifiedName(key))
192 .setDisplayName(LocalizedText.english(key)).setDataType(Identifiers.String)
193 .setTypeDefinition(Identifiers.BaseDataVariableType).build();
194
195 configNode.setValue(new DataValue(new Variant(configuration.toString())));
196
197 getNodeManager().addNode(configNode);
198
199 folders.getConfigFolder().addOrganizes(configNode);
200 }
201
202
203
204
205
206
207
208 private void removeGenericConfigurationNode(String key) {
209 UaNode configNode = getNodeManager().get(newNodeId("stc/config/" + key));
210 if (configNode != null)
211 removeObjectNode(configNode);
212 }
213
214 public UaVariableNode createVariableNode(String name, NodeId newNodeId, NodeId dataType) {
215 var node = new UaVariableNode.UaVariableNodeBuilder(getNodeContext()).setNodeId(newNodeId)
216 .setBrowseName(newQualifiedName(name.toLowerCase())).setDisplayName(LocalizedText.english(name))
217 .setDataType(dataType).setAccessLevel(AccessLevel.READ_ONLY)
218 .setTypeDefinition(Identifiers.BaseDataVariableType).build();
219
220 getNodeManager().addNode(node);
221
222 return node;
223 }
224
225 public UaFolderNode createFolderNode(String name, NodeId newNodeId, NodeId parent) {
226 var fldr = new UaFolderNode(getNodeContext(), newNodeId, newQualifiedName(name.toLowerCase()),
227 LocalizedText.english(name));
228
229 getNodeManager().addNode(fldr);
230
231 fldr.addReference(new Reference(fldr.getNodeId(), Identifiers.Organizes, parent.expanded(), false));
232
233 return fldr;
234 }
235
236 public UaObjectNode createObjectNode(UaObjectTypeNode type, String name, NodeId newNodeId, UaNode parentFolder)
237 throws UaException {
238 UaObjectNode on = (UaObjectNode) getNodeFactory().createNode(newNodeId, type.getNodeId());
239
240 on.setBrowseName(newQualifiedName(name.toLowerCase()));
241 on.setDisplayName(LocalizedText.english(name));
242
243 if (parentFolder instanceof UaFolderNode folder)
244 folder.addOrganizes(on);
245 else if (parentFolder instanceof UaObjectNode pon)
246 pon.addComponent(on);
247
248 return on;
249 }
250
251 public void removeObjectNode(UaNode node) {
252 for (var ref : node.getReferences()) {
253 if (ref.isForward()) {
254 Set<ExpandedNodeId> nodesToRemove = new HashSet<>();
255 if (ref.subtypeOf(Identifiers.HierarchicalReferences)) {
256 nodesToRemove.add(ref.getTargetNodeId());
257 }
258
259 for (var nd : nodesToRemove) {
260 UaNode fnd = getNodeManager().get(nd, getServer().getNamespaceTable());
261 if (fnd != null)
262 removeObjectNode(fnd);
263 }
264 } else {
265 log.debug("Removing reference {}", ref);
266 getNodeManager().removeReferences(ref, getServer().getNamespaceTable());
267 }
268 }
269
270 log.debug("Remove UA Node {}", node.getNodeId());
271 getNodeManager().removeNode(node);
272 }
273
274 public UaObjectTypeNode createObjectTypeNode(String name, NodeId newNodeId, UaNode... subNodes) {
275 var otn = UaObjectTypeNode.builder(getNodeContext()).setBrowseName(newQualifiedName(name.toLowerCase()))
276 .setDisplayName(LocalizedText.english(name)).setNodeId(newNodeId).setIsAbstract(false).build();
277
278 for (var sn : subNodes)
279 otn.addComponent(sn);
280
281 getServer().getObjectTypeManager().registerObjectType(otn.getNodeId(), UaObjectNode.class, UaObjectNode::new);
282
283 otn.addReference(
284 new Reference(otn.getNodeId(), Identifiers.HasSubtype, Identifiers.BaseObjectType.expanded(), false));
285
286 getNodeManager().addNode(otn);
287
288 for (var sn : subNodes)
289 getNodeManager().addNode(sn);
290
291 return otn;
292 }
293
294 public UaVariableNode createObjectTypeComponent(String name, NodeId newNodeId, NodeId dataType) {
295 var node = new UaVariableNode.UaVariableNodeBuilder(getNodeContext()).setNodeId(newNodeId)
296 .setBrowseName(newQualifiedName(name.toLowerCase())).setDisplayName(LocalizedText.english(name))
297 .setDataType(dataType).setAccessLevel(AccessLevel.READ_ONLY)
298 .setTypeDefinition(Identifiers.BaseDataVariableType).build();
299
300 node.addReference(new Reference(node.getNodeId(), Identifiers.HasModellingRule,
301 Identifiers.ModellingRule_Mandatory.expanded(), true));
302
303 return node;
304 }
305
306 public UaObjectNode createObjectTypeComponent(String name, NodeId newNodeId, UaObjectTypeNode objectType,
307 NodeId modellingRule) throws UaException {
308 UaObjectNode on = (UaObjectNode) getNodeFactory().createNode(newNodeId, objectType.getNodeId());
309
310 on.setBrowseName(newQualifiedName(name.toLowerCase()));
311 on.setDisplayName(LocalizedText.english(name));
312
313 on.addReference(new Reference(on.getNodeId(), Identifiers.HasModellingRule, modellingRule.expanded(), true));
314
315 return on;
316 }
317
318 public void setObjectNodeComponent(UaObjectNode on, String component, Variant value) {
319 var optNode = on.findNode(newQualifiedName(component.toLowerCase()));
320
321 if (optNode.isPresent()) {
322 var node = optNode.get();
323 if (node instanceof UaVariableNode varnode) {
324 varnode.setValue(new DataValue(value));
325 }
326 }
327 }
328
329 public void setObjectNodeComponent(UaObjectNode on, String component,
330 Function<AttributeFilterContext.GetAttributeContext, DataValue> getter) {
331 var optNode = on.findNode(newQualifiedName(component.toLowerCase()));
332 if (optNode.isPresent()) {
333 var node = optNode.get();
334 if (node instanceof UaVariableNode varnode) {
335 varnode.getFilterChain().addLast(AttributeFilters.getValue(getter));
336 }
337 }
338 }
339
340 public UaVariableNode getObjectNodeComponent(UaObjectNode on, String component) {
341 var optNode = on.findNode(newQualifiedName(component.toLowerCase()));
342 return optNode.isPresent() && optNode.get() instanceof UaVariableNode vn ? vn : null;
343 }
344
345 public UaObjectNode getObjectNodeInstance(UaNode parentNode, String component) {
346 var optNode = parentNode.findNode(newQualifiedName(component.toLowerCase()));
347 if (optNode.isPresent() && optNode.get() instanceof UaObjectNode uon)
348 return uon;
349
350 return null;
351 }
352
353 @Override
354 public void onDataItemsCreated(List<DataItem> dataItems) {
355 subscriptionModel.onDataItemsCreated(dataItems);
356 }
357
358 @Override
359 public void onDataItemsModified(List<DataItem> dataItems) {
360 subscriptionModel.onDataItemsModified(dataItems);
361 }
362
363 @Override
364 public void onDataItemsDeleted(List<DataItem> dataItems) {
365 subscriptionModel.onDataItemsDeleted(dataItems);
366 }
367
368 @Override
369 public void onMonitoringModeChanged(List<MonitoredItem> monitoredItems) {
370 subscriptionModel.onMonitoringModeChanged(monitoredItems);
371 }
372
373 }