package org.point85.app.opc.ua; import java.lang.reflect.Array; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.eclipse.milo.opcua.stack.core.BuiltinDataType; import org.eclipse.milo.opcua.stack.core.Identifiers; import org.eclipse.milo.opcua.stack.core.NamespaceTable; import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; 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.ExpandedNodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode; import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass; import org.eclipse.milo.opcua.stack.core.types.enumerated.ServerState; import org.eclipse.milo.opcua.stack.core.types.structured.BuildInfo; import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription; import org.point85.app.AppUtils; import org.point85.app.ImageManager; import org.point85.app.Images; import org.point85.app.designer.ConnectionState; import org.point85.app.designer.DesignerApplication; import org.point85.app.designer.DesignerLocalizer; import org.point85.domain.DomainUtils; import org.point85.domain.collector.CollectorDataSource; import org.point85.domain.collector.DataSourceType; import org.point85.domain.opc.ua.OpcUaServerStatus; import org.point85.domain.opc.ua.OpcUaSource; import org.point85.domain.persistence.PersistenceService; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.ComboBox; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import javafx.scene.image.Image; import javafx.scene.image.ImageView; public class OpcUaBrowserController extends OpcUaController { // selected node private OpcUaTreeNode selectedTreeNode; // list of servers and ports private final ObservableList<String> servers = FXCollections.observableArrayList(new ArrayList<>()); // list of OPC DA tags being monitored private final List<String> monitoredItemIds = new ArrayList<>(); @FXML private TextField tfConnectionName; @FXML private TextField tfHost; @FXML private TextField tfUserName; @FXML private PasswordField pfPassword; @FXML private TextField tfPort; @FXML private TextField tfPath; @FXML private ComboBox<String> cbDataSources; @FXML private TextField tfDescription; @FXML private Button btNew; @FXML private Button btSave; @FXML private Button btDelete; @FXML private TreeView<OpcUaTreeNode> tvBrowser; @FXML private Label lbState; @FXML private Label lbEndpoint; @FXML private Label lbStartTime; @FXML private Label lbProduct; @FXML private Label lbManufacturer; @FXML private Button btConnect; @FXML private Button btDisconnect; @FXML private Button btCancelConnect; @FXML private ProgressIndicator piConnection; @FXML private Label lbNodeId; @FXML private Label lbNodeDescription; @FXML private Label lbNodeType; @FXML private TextArea taNodeValue; @FXML private Label lbNodeTimestamp; @FXML private Button btClearAuthentication; @FXML private ComboBox<SecurityPolicy> cbSecurityPolicies; @FXML private ComboBox<MessageSecurityMode> cbMessageModes; @FXML private TextField tfKeystoreFileName; @FXML private PasswordField pfKeystorePassword; public void initialize(DesignerApplication app) throws Exception { // main app setApp(app); // button images setImages(); // init OPC UA connection initializeConnection(); // retrieve the defined data sources populateDataSources(); initializeSecuritySettings(); initializeTreeView(); } private void initializeConnection() { // indicator for connection progress piConnection.setVisible(false); // defined servers cbDataSources.setItems(servers); } private void initializeTreeView() throws Exception { tvBrowser.setShowRoot(false); // tree node listener tvBrowser.getSelectionModel().selectedItemProperty() .addListener((observable, oldValue, newValue) -> populateAvailableNodes(newValue)); } public void arrayToStringRecursive(Object someArray, StringBuilder sb) { if (someArray == null) { sb.append(""); return; } Class<?> clazz = someArray.getClass(); if (clazz.isArray()) { // iterate over its elements int length = Array.getLength(someArray); sb.append('['); for (int i = 0; i < length; i++) { // let's test if array is multidimensional if (clazz.getComponentType().isArray()) { arrayToStringRecursive(Array.get(someArray, i), sb); } else { // not an array sb.append(Array.get(someArray, i)); if (i < length - 1) sb.append(", "); } } sb.append(']'); } else { sb.append(someArray).append(DesignerLocalizer.instance().getErrorString("not.array")); } } private String arrayToString(Object[] values) { String valueText = null; StringBuilder sb = new StringBuilder(); sb.append('['); int end = values.length; for (int i = 0; i < end; i++) { valueText = values[i].toString(); sb.append(valueText); if (i < (end - 1)) { sb.append(", "); } } sb.append(']'); return sb.toString(); } private void onSelectNode(OpcUaTreeNode treeNode) throws Exception { selectedTreeNode = treeNode; NamespaceTable nst = getApp().getOpcUaClient().getNamespaceTable(); NodeId nodeId = treeNode.getNodeId(nst); // fill in attributes ReferenceDescription ref = treeNode.getReferenceDescription(); NodeClass nodeClass = ref.getNodeClass(); ExpandedNodeId nodeDataType = null; Class<?> javaType = null; boolean clazzIsArray = false; Object value = null; OffsetDateTime zdt = null; String valueText = null; String typeText = null; if (nodeClass.equals(NodeClass.Variable)) { DataValue dataValue = getApp().getOpcUaClient().readSynch(nodeId); Variant variant = dataValue.getValue(); value = variant.getValue(); if (value != null) { clazzIsArray = value.getClass().isArray(); // data type Optional<ExpandedNodeId> dataType = dataValue.getValue().getDataType(); if (dataType.isPresent()) { nodeDataType = dataType.get(); } javaType = BuiltinDataType.getBackingClass(nodeDataType); selectedTreeNode.setNodeDataType(nodeDataType); // timestamp zdt = DomainUtils.localTimeFromDateTime(dataValue.getServerTime()); } } if (value != null) { if (!clazzIsArray) { typeText = javaType.getSimpleName(); if (javaType.equals(DateTime.class)) { valueText = DomainUtils.utcTimeFromDateTime((DateTime) value).toString(); } else { valueText = value.toString(); } } else { // array or matrix UInteger[] dims = getApp().getOpcUaClient().getArrayDimensions(nodeId); // check for matrix if (dims != null) { if (dims.length == 1) { typeText = DesignerLocalizer.instance().getLangString("array.of", javaType.getSimpleName(), arrayToString(dims)); } else { typeText = DesignerLocalizer.instance().getLangString("matrix.of", javaType.getSimpleName(), arrayToString(dims)); } } StringBuilder sb = new StringBuilder(); arrayToStringRecursive(value, sb); valueText = sb.toString(); } } this.lbNodeId.setText(nodeId.toParseableString()); this.lbNodeDescription.setText(ref.getBrowseName().getName()); this.lbNodeType.setText(typeText); if (valueText != null) { this.taNodeValue.setText(valueText); } else { this.taNodeValue.clear(); } if (zdt != null) { this.lbNodeTimestamp.setText(zdt.toString()); } else { this.lbNodeTimestamp.setText(null); } } // images for buttons @Override protected void setImages() throws Exception { super.setImages(); // connect btConnect.setGraphic(ImageManager.instance().getImageView(Images.CONNECT)); btConnect.setContentDisplay(ContentDisplay.LEFT); // disconnect btDisconnect.setGraphic(ImageManager.instance().getImageView(Images.DISCONNECT)); btDisconnect.setContentDisplay(ContentDisplay.LEFT); // cancel connect btCancelConnect.setGraphic(ImageManager.instance().getImageView(Images.CANCEL)); btCancelConnect.setContentDisplay(ContentDisplay.LEFT); // new btNew.setGraphic(ImageManager.instance().getImageView(Images.NEW)); btNew.setContentDisplay(ContentDisplay.LEFT); // save btSave.setGraphic(ImageManager.instance().getImageView(Images.SAVE)); btSave.setContentDisplay(ContentDisplay.LEFT); // delete btDelete.setGraphic(ImageManager.instance().getImageView(Images.DELETE)); btDelete.setContentDisplay(ContentDisplay.LEFT); // clear btClearAuthentication.setGraphic(ImageManager.instance().getImageView(Images.CLEAR)); } private void updateConnectionStatus(ConnectionState state) throws Exception { connectionState = state; switch (state) { case CONNECTED: piConnection.setVisible(false); OpcUaServerStatus status = getApp().getOpcUaClient().getServerStatus(); if (status != null) { // state ServerState serverState = status.getState(); if (serverState != null) { lbState.setText(serverState.toString()); lbState.setTextFill(ConnectionState.CONNECTED_COLOR); } // start time OffsetDateTime start = DomainUtils.utcTimeFromDateTime(status.getStartTime()); lbStartTime.setText(DomainUtils.offsetDateTimeToString(start, DomainUtils.OFFSET_DATE_TIME_PATTERN)); // product & manufacturer BuildInfo info = status.getBuildInfo(); if (info != null) { lbProduct.setText(info.getProductName() != null ? info.getProductName() : ""); lbManufacturer.setText(info.getManufacturerName() != null ? info.getManufacturerName() : ""); } // endpoint lbEndpoint.setText(getSource().getEndpointUrl()); } else { lbState.setText(ConnectionState.DISCONNECTED.toString()); lbState.setTextFill(ConnectionState.DISCONNECTED_COLOR); } break; case CONNECTING: piConnection.setVisible(true); lbState.setText(ConnectionState.CONNECTING.toString()); lbState.setTextFill(ConnectionState.CONNECTING_COLOR); break; case DISCONNECTED: // on callback Platform.runLater(() -> { piConnection.setVisible(false); lbState.setText(ConnectionState.DISCONNECTED.toString()); lbState.setTextFill(ConnectionState.DISCONNECTED_COLOR); lbNodeId.setText(null); lbNodeDescription.setText(null); lbNodeType.setText(null); taNodeValue.clear(); lbNodeTimestamp.setText(null); tvBrowser.setRoot(null); }); break; default: break; } } private Image getNodeImage(ReferenceDescription ref) throws Exception { NodeClass nodeClass = ref.getNodeClass(); Image image = null; if (nodeClass.equals(NodeClass.Object)) { image = ImageManager.instance().getImage(Images.FOLDER); } else if (nodeClass.equals(NodeClass.Variable)) { image = ImageManager.instance().getImage(Images.VALUE); } return image; } private void showRootNodes() throws Exception { TreeItem<OpcUaTreeNode> rootItem = new TreeItem<>(); tvBrowser.setRoot(rootItem); rootItem.setExpanded(true); // browse root folder List<ReferenceDescription> refs = getApp().getOpcUaClient().browseSynch(Identifiers.RootFolder); for (ReferenceDescription ref : refs) { // child nodes TreeItem<OpcUaTreeNode> childItem = new TreeItem<>(new OpcUaTreeNode(ref)); ImageView imageView = new ImageView(getNodeImage(ref)); childItem.setGraphic(imageView); rootItem.getChildren().add(childItem); } } protected void onConnectionSucceeded() throws Exception { updateConnectionStatus(ConnectionState.CONNECTED); showRootNodes(); } @FXML private void onConnect() { try { if (connectionState.equals(ConnectionState.CONNECTED)) { // disconnect first onDisconnect(); } // connect updateConnectionStatus(ConnectionState.CONNECTING); startConnectionService(); } catch (Exception e) { AppUtils.showErrorDialog(e); } } @FXML private void onDisconnect() { try { // disconnect terminateConnectionService(); updateConnectionStatus(ConnectionState.DISCONNECTED); onNewDataSource(); } catch (Exception e) { AppUtils.showErrorDialog(e); } } @FXML protected void onCancelConnect() { try { cancelConnectionService(); updateConnectionStatus(ConnectionState.DISCONNECTED); } catch (Exception e) { AppUtils.showErrorDialog(e); } } @Override @FXML protected void onCancel() { // close dialog with current tag set to null this.selectedTreeNode = null; super.onCancel(); } @FXML private void onSelectDataSource() { try { String name = getDataSourceId(); if (name == null || name.length() == 0) { return; } // retrieve data source by name OpcUaSource source = PersistenceService.instance().fetchOpcUaSourceByName(name); if (source != null) { setSource(source); } else { // not saved yet return; } this.tfConnectionName.setText(source.getName()); this.tfHost.setText(source.getHost()); this.tfPort.setText(String.valueOf(source.getPort())); this.tfDescription.setText(source.getDescription()); this.tfPath.setText(source.getEndpointPath()); // security this.cbSecurityPolicies.getSelectionModel().select(source.getSecurityPolicy()); this.cbMessageModes.getSelectionModel().select(source.getMessageSecurityMode()); // authentication this.tfUserName.setText(source.getUserName()); this.pfPassword.setText(source.getUserPassword()); this.tfKeystoreFileName.setText(source.getKeystore()); this.pfKeystorePassword.setText(source.getKeystorePassword()); } catch (Exception e) { AppUtils.showErrorDialog(e); } } @FXML private void onDeleteDataSource() { try { // delete OpcUaSource source = getSource(); if (source != null) { // confirm ButtonType type = AppUtils.showConfirmationDialog( DesignerLocalizer.instance().getLangString("object.delete", source.toString())); if (type.equals(ButtonType.CANCEL)) { return; } PersistenceService.instance().delete(source); servers.remove(getSource().getName()); cbDataSources.setItems(servers); onNewDataSource(); } } catch (Exception e) { AppUtils.showErrorDialog(e); } } @FXML private void onNewDataSource() { try { this.tfConnectionName.clear(); this.tfHost.clear(); this.tfUserName.clear(); this.pfPassword.clear(); this.tfDescription.clear(); this.tfPort.clear(); this.tfPath.clear(); this.cbDataSources.getSelectionModel().clearSelection(); this.cbSecurityPolicies.getSelectionModel().select(SecurityPolicy.None); this.cbMessageModes.getSelectionModel().select(MessageSecurityMode.None); this.tfKeystoreFileName.clear(); this.pfKeystorePassword.clear(); this.setSource(null); } catch (Exception e) { AppUtils.showErrorDialog(e); } } @FXML private void onSaveDataSource() { // set attributes try { OpcUaSource dataSource = getSource(); dataSource.setName(getConnectionName()); dataSource.setHost(getHost()); dataSource.setPort(getPort()); dataSource.setDescription(getDescription()); dataSource.setEndpointPath(getPath()); // security dataSource.setSecurityPolicy(getSecurityPolicy()); dataSource.setMessageSecurityMode(getMessageMode()); // authentication dataSource.setUserName(getUserName()); dataSource.setPassword(getPassword()); dataSource.setKeystore(getKeystoreFileName()); dataSource.setKeystorePassword(getKeystorePassword()); // save data source OpcUaSource savedSource = (OpcUaSource) PersistenceService.instance().save(dataSource); setSource(savedSource); // update list populateDataSources(); } catch (Exception e) { AppUtils.showErrorDialog(e); } } private void initializeSecuritySettings() { ObservableList<SecurityPolicy> policies = cbSecurityPolicies.getItems(); policies.clear(); for (SecurityPolicy policy : SecurityPolicy.values()) { policies.add(policy); } ObservableList<MessageSecurityMode> modes = cbMessageModes.getItems(); modes.clear(); for (MessageSecurityMode mode : MessageSecurityMode.values()) { if (!mode.equals(MessageSecurityMode.Invalid)) { modes.add(mode); } } } private void populateDataSources() { // fetch the sources List<CollectorDataSource> sources = PersistenceService.instance().fetchDataSources(DataSourceType.OPC_UA); servers.clear(); for (CollectorDataSource source : sources) { servers.add(((OpcUaSource) source).getName()); } if (servers.size() == 1) { this.cbDataSources.getSelectionModel().select(0); onSelectDataSource(); } } String getConnectionName() { return this.tfConnectionName.getText(); } String getHost() { return this.tfHost.getText(); } String getUserName() { return this.tfUserName.getText(); } String getPassword() { return this.pfPassword.getText(); } String getKeystoreFileName() { return this.tfKeystoreFileName.getText(); } String getKeystorePassword() { return this.pfKeystorePassword.getText(); } String getPath() { return this.tfPath.getText(); } Integer getPort() { return Integer.valueOf(tfPort.getText()); } String getDataSourceId() { return this.cbDataSources.getSelectionModel().getSelectedItem(); } String getDescription() { return this.tfDescription.getText(); } SecurityPolicy getSecurityPolicy() { return this.cbSecurityPolicies.getSelectionModel().getSelectedItem(); } void setSecurityPolicy(SecurityPolicy policy) { this.cbSecurityPolicies.getSelectionModel().select(policy); } MessageSecurityMode getMessageMode() { return this.cbMessageModes.getSelectionModel().getSelectedItem(); } void setMessageMode(MessageSecurityMode mode) { this.cbMessageModes.getSelectionModel().select(mode); } private void populateAvailableNodes(TreeItem<OpcUaTreeNode> selectedItem) { try { if (selectedItem == null) { return; } OpcUaTreeNode treeNode = selectedItem.getValue(); // update node info onSelectNode(treeNode); if (treeNode.isBrowsed()) { // check to see if a variable node to update the value NodeClass nodeClass = treeNode.getReferenceDescription().getNodeClass(); if (!nodeClass.equals(NodeClass.Variable)) { return; } } // fill in the child nodes ReferenceDescription parentRef = treeNode.getReferenceDescription(); NamespaceTable nst = getApp().getOpcUaClient().getNamespaceTable(); NodeId nodeId = parentRef.getNodeId().local(nst).orElse(null); // keep browsing down the tree List<ReferenceDescription> childRefs = getApp().getOpcUaClient().browseSynch(nodeId); treeNode.setBrowsed(true); for (ReferenceDescription childRef : childRefs) { NodeId childId = childRef.getNodeId().local(nst).orElse(null); String id = childId.toString(); if (!monitoredItemIds.contains(id)) { TreeItem<OpcUaTreeNode> childItem = new TreeItem<>(new OpcUaTreeNode(childRef)); childItem.setGraphic(new ImageView(getNodeImage(childRef))); selectedItem.getChildren().add(childItem); } } } catch (Exception e) { AppUtils.showErrorDialog(e); } } @FXML private void onClearAuthentication() { this.tfKeystoreFileName.clear(); this.pfKeystorePassword.clear(); this.tfUserName.clear(); this.pfPassword.clear(); } public OpcUaTreeNode getSelectedNodeId() { return selectedTreeNode; } }