├── AlarmConverter └── Python │ ├── README.md │ ├── adapter_server.py │ ├── alarm_converter.py │ ├── custom_logger.py │ ├── doc │ └── alarmConverter.PNG │ ├── gen_certificate.py │ ├── machineconfiguration.xml │ ├── machineconfiguration_ExampleGateway.xml │ ├── machineconfiguration_QuickstartServer.xml │ └── machines_adapter_client.py ├── Doc ├── Attachments │ ├── JobOrders.xsd │ ├── examplePackage1.tpp │ └── examplePackage2.tpp ├── connectionstate.md ├── guidelines.md ├── opcuaclients.md ├── opcuagateway.md ├── productionplan.md ├── signalsusecases.md └── transferType.png ├── Examples ├── NetCore │ ├── .gitignore │ ├── TrumpfNetCoreClientExamples.sln │ ├── TrumpfNetCoreClientExamples │ │ ├── AlarmsExample.cs │ │ ├── BaseClient.cs │ │ ├── ComplexTypeExample.cs │ │ ├── Model │ │ │ └── TsSheetTech.cs │ │ ├── Opc.Ua.BasicClient.Config.xml │ │ ├── Program.cs │ │ └── TrumpfNetCoreClientExamples.csproj │ └── readme.md ├── NodeRed │ ├── README.md │ ├── doc │ │ └── resolveNamespace.PNG │ └── flow_resolveNamespace.json └── Python │ ├── LICENSE │ ├── README.md │ ├── gen_certificate.py │ └── show_current_alarms.py ├── LICENSE ├── MachineDemoServer └── Python │ ├── MachineNodeTree.xml │ ├── PythonMachineDemoServer.py │ ├── README.md │ ├── ReplayConfiguration.xml │ ├── recordTruLaser.json │ ├── recordTruLaserCenter.json │ ├── server-certificate.der │ └── server-privatekey.pem └── README.md /AlarmConverter/Python/README.md: -------------------------------------------------------------------------------- 1 | ## Python Alarm Converter 2 | 3 | ### Introduction 4 | This is a complete example application which converts OPC UA Alarms and Conditions events to standard OPC UA data items. The application includes a client and a server. The client subscribes to the alarms and the server provides them as data items. It might be interesting if the used client application is not yet able to consume OPC UA events. 5 | 6 | The default configuration is for the [PythonMachineDemoServer](../../MachineDemoServer/Python). 7 | 8 | A simple example on how to just consume alarms, can be found [here](../../Examples/Python). 9 | 10 | It is made as an example to be used with Trumpf machines, but might also be adapted or tested with other OPC UA alarms and conditions servers. 11 | 12 | Feel free to use it, adapt it to your needs and contribute back to the open source project. There is no offical TRUMPF support for the OPC UA open source examples and applications. 13 | 14 | ##### View of converted alarms 15 | ![Picture Alarms](doc/alarmConverter.PNG) 16 | 17 | ### Quickstart 18 | Download python libraries 'asyncua' and 'aioconsole'. Adapt configuration in `machineconfiguration.xml` and execute `alarm_converter.py`. 19 | 20 | ### Requirements 21 | - Python >= Python 3.7 22 | - Version >= Python 3.11 is recommended due to startup performance improvements. 23 | - Python opcua-asyncio library >= v1.0.1 24 | - Python aioconsole library 25 | 26 | Tested with opcua-asyncio v1.0.1. If it will not run, use exactly that version and create an issue with error description and used library version. 27 | 28 | ### Description 29 | For each configured machine an OPC UA object with the given machine name is created. That object contains 20 alarm objects which are placeholder slots for upcoming alarms. Each alarm contains data variables, which provide the relevant alarm information. 30 | 31 | In case an alarm occurs on the machine, the first unused alarm slot is assigned and the variables are updated with the alarm information. As long as the alarm is pending, the data is available. When the alarm disappears, the variables are reset to empty values. 32 | 33 | Furthermore for each machine, there is a variable "AlarmList" which contains the current pending AlarmIdentifiers as a list of strings. On each change (alarm appearing or disappearing), the list is updated. 34 | 35 | During execution of the alarm_converter there are console outputs and some console commands which can be entered. 36 | 37 | ``` 38 | --------------------------------------- 39 | --> exit - exit the application 40 | --> verbose - set log level to verbose 41 | --> standard - set log level to standard 42 | --> config - show loaded config 43 | --> show - show active connections 44 | --> dir - show the current directory 45 | ---------------------------------------- 46 | ``` 47 | 48 | ### Installation and Execution 49 | With Python installed, the script file can be exuted with `python alarm_converter.py`. As an alternative a fully self contained .exe can be created with PyInstaller. 50 | 51 | ##### Detailed instruction: 52 | - [Download and install Python](https://www.python.org/downloads/) >= Python 3.7. On installation set checkbox for adding to system path. 53 | - Install opcua-asnycio library >= 1.0.1 and aioconsole library with 54 | `pip3 install asyncua aioconsole` or upgrade with 55 | `pip3 install --upgrade asyncua aioconsole` 56 | Behind a company proxy you may need to add the proxy server and trust servers. Search for proxy settings and look for the manual proxy server. 57 | `pip3 install --trusted-host pypi.org --trusted-host files.pythonhosted.org --proxy=http://username:password@proxyserver:port asyncua aioconsole` 58 | 59 | - Get all files and copy them to a folder. Easiest way is to download all files of the github repository. [Download zip](https://github.com/TRUMPF-IoT/OpcUaMachineTools/archive/main.zip). Or use a git client. 60 | - Enter the folder containing alarm_converter.py and execute the server with `python alarm_converter.py` 61 | - On Linux if Python 2.x and Python 3.x are installed execute with `python3 alarm_converter.py`. 62 | 63 | 64 | ### Configuration 65 | 66 | Adapt configuration in `machineconfiguration.xml`. 67 | 68 | #### Basic Configuration 69 | 70 | | Attribute | Description | 71 | | -------------- | ----------- | 72 | | machinesServer | Set the OPC UA server uri of the server providing the alarms | 73 | | isTrumpfServer | "false" for non trumpf machines. Then alarms will be subscribed on the server node and not on a trumpf specific node. | 74 | | adapterEndpoint | Set the OPC UA server uri the converted alarms will be provided as items | 75 | | traceLevel | "standard" for normal operation. "verbose" for diagnose purposes. | 76 | 77 | #### Machines configuration 78 | 79 | For each machine on the trumpf OPC UA gateway add an machine entry. 80 | 81 | | Attribute | Description | 82 | | -------------- | ----------- | 83 | | machineName | Set the desired name of the OPC UA object containing the converted alarm items | 84 | | ns | Set the namespace of the machine | 85 | 86 | #### Provided example configurations 87 | - machineconfiguration.xml --> Configuration for PythonMachineDemoServer 88 | - machineconfiguration_ExampleGateway.xml --> Configuration example for a Trumpf opc gateway. 89 | - machineconfiguration_QuickstartServer.xml --> Configuration example for opc foundation [AlarmCondition quickstart sample](https://github.com/OPCFoundation/UA-.NETStandard-Samples). 90 | 91 | 92 | ### OPC UA Client for testing 93 | A generic OPC UA client can be used to explore and browse the OPC UA server. A recommended free client is "UaExpert" from UnifiedAutomation. [Download link.](https://www.unified-automation.com/downloads/opc-ua-clients.html) 94 | 95 | #### How to view Alarms and conditions 96 | After starting UaExpert and connecting to the server, open the **Event View** with "Document -> Add -> Document Type: Event View". 97 | 98 | Drag the "Messages" node to the Configuration section in the Event View. Now you can observe all incoming Events in the Events-Tab and all pending Alarms/Warnings in the Alarms-Tab. 99 | 100 | 101 | ### License 102 | The Python Alarm Converter is licensed under the MIT License. 103 | 104 | ``` 105 | MIT License 106 | 107 | Copyright (c) 2022 TRUMPF Werkzeugmaschinen SE + Co. KG 108 | 109 | Permission is hereby granted, free of charge, to any person obtaining a copy 110 | of this software and associated documentation files (the "Software"), to deal 111 | in the Software without restriction, including without limitation the rights 112 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 113 | copies of the Software, and to permit persons to whom the Software is 114 | furnished to do so, subject to the following conditions: 115 | 116 | The above copyright notice and this permission notice shall be included in all 117 | copies or substantial portions of the Software. 118 | 119 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 120 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 121 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 122 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 123 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 124 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 125 | SOFTWARE. 126 | ``` 127 | -------------------------------------------------------------------------------- /AlarmConverter/Python/adapter_server.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022 TRUMPF Werkzeugmaschinen SE + Co. KG 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import asyncio 24 | import logging 25 | from asyncua import Server, ua 26 | from datetime import datetime 27 | 28 | 29 | ALARM_PARAMETERS = { 30 | "AlarmIdentifier": 31 | { "type": ua.VariantType.String, "default": "", "eventKey": "identifier", "trumpfOnly": True }, 32 | "ConditionName": 33 | { "type": ua.VariantType.String, "default": "", "eventKey": "name", "trumpfOnly": False }, 34 | "SourceName": 35 | { "type": ua.VariantType.String, "default": "", "eventKey": "source", "trumpfOnly": False }, 36 | "Text": 37 | { "type": ua.VariantType.String, "default": "", "eventKey": "text", "trumpfOnly": False }, 38 | "Severity": 39 | { "type": ua.VariantType.UInt16, "default": 0, "eventKey": "severity", "trumpfOnly": False }, 40 | "Time": 41 | { "type": ua.VariantType.DateTime, "default": datetime(1900,1,1), "eventKey": "time", "trumpfOnly": False }, 42 | } 43 | 44 | class AdapterServer: 45 | 46 | def __init__(self, endpointUri, sourceIsTrumpf): 47 | self.logger = logging.getLogger(__name__) 48 | self.server = Server() 49 | self.sourceIsTrumpf = sourceIsTrumpf 50 | self.endpointUri = endpointUri 51 | self.isRunning = False 52 | self.hasStopped = False 53 | self.idx = None 54 | 55 | 56 | async def initialize(self): 57 | self.logger.trace("Initializing Server. Please wait.") 58 | await self.server.init() 59 | self.server.set_endpoint(self.endpointUri) 60 | self.server.set_server_name("AlarmConverterServer") 61 | # set all possible endpoint policies for clients to connect through 62 | self.server.set_security_policy([ua.SecurityPolicyType.NoSecurity,]) 63 | # setup our own namespace 64 | ns = "http://trumpf.com/alarmconverter/" 65 | self.idx = await self.server.register_namespace(ns) 66 | 67 | 68 | async def run(self): 69 | # starting! 70 | self.isRunning = True 71 | self.hasStopped = False 72 | async with self.server: 73 | self.logger.trace("Server started!") 74 | while self.isRunning: # Todo Variable to check if to stop 75 | await asyncio.sleep(1) 76 | self.hasStopped = True 77 | 78 | 79 | async def stop(self): 80 | self.isRunning = False 81 | while not self.hasStopped: 82 | await asyncio.sleep(0.25) 83 | self.logger.trace("Server stopped!") 84 | 85 | 86 | async def add_machine(self, machineName): 87 | machineNode = await self.server.nodes.objects.add_object(ua.NodeId(machineName, self.idx), machineName) 88 | # Add array variable for list of active alarm identifiers 89 | await machineNode.add_variable(ua.NodeId(f"{machineName}.AlarmList", self.idx), "AlarmList", 90 | ua.Variant([], ua.VariantType.String, is_array=True)) 91 | # Add 20 alarm slots with variables 92 | for i in range(0,20): 93 | alarmId = f"Alarm{i}" 94 | alarmNode = await machineNode.add_object(ua.NodeId(f"{machineName}.{alarmId}", self.idx), alarmId) 95 | for key,v in ALARM_PARAMETERS.items(): 96 | if not v["trumpfOnly"] or ( v["trumpfOnly"] and self.sourceIsTrumpf ): 97 | await alarmNode.add_variable(ua.NodeId(f"{machineName}.{alarmId}.{key}", self.idx), key, v["default"], v["type"]) 98 | 99 | 100 | async def update_alarm_list(self, machineName, pendingAlarms): 101 | node = self.server.get_node(ua.NodeId(f"{machineName}.AlarmList", self.idx)) 102 | identifiers = [x["identifier"] for x in pendingAlarms.values()] 103 | await node.write_value(identifiers, ua.VariantType.String) 104 | 105 | 106 | async def update_alarm_values(self, machineName, slotNumber, eventAsDict): 107 | alarmId = f"Alarm{slotNumber}" 108 | for key,v in ALARM_PARAMETERS.items(): 109 | if not v["trumpfOnly"] or ( v["trumpfOnly"] and self.sourceIsTrumpf ): 110 | node = self.server.get_node(ua.NodeId(f"{machineName}.{alarmId}.{key}", self.idx)) 111 | newValue = eventAsDict[v["eventKey"]] 112 | await node.write_value(newValue, v["type"]) 113 | 114 | 115 | async def reset_alarm_values(self, machineName, slotNumber, eventAsDict): 116 | # reset to default values 117 | for key,v in ALARM_PARAMETERS.items(): 118 | if not v["trumpfOnly"] or ( v["trumpfOnly"] and self.sourceIsTrumpf ): 119 | eventAsDict[v["eventKey"]] = v["default"] 120 | await self.update_alarm_values(machineName, slotNumber, eventAsDict) 121 | 122 | -------------------------------------------------------------------------------- /AlarmConverter/Python/alarm_converter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # MIT License 4 | 5 | # Copyright (c) 2022 TRUMPF Werkzeugmaschinen SE + Co. KG 6 | 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | import asyncio 26 | import logging 27 | import custom_logger 28 | import os 29 | import sys 30 | from machines_adapter_client import MachinesAdapterClient 31 | from adapter_server import AdapterServer 32 | from aioconsole import ainput 33 | from xml.dom import minidom 34 | 35 | 36 | async def interactive(machinesAdapter): 37 | while True: 38 | print('---------------------------------------') 39 | print('--> exit - exit the application') 40 | print('--> verbose - set log level to verbose') 41 | print('--> standard - set log level to standard') 42 | print('--> config - show loaded config') 43 | print('--> show - show active connections') 44 | print('--> dir - show the current directory') 45 | print('----------------------------------------') 46 | line = await ainput("") 47 | if line == 'dir': 48 | print(f"DIRECTORY --- [{sys.path[0]}]") 49 | elif line == 'verbose': 50 | print("LOG LEVEL --- [Set log level to verbose.]") 51 | custom_logger.set_level('verbose') 52 | elif line == 'standard': 53 | print("LOG LEVEL --- [Set log level to standard.]") 54 | custom_logger.set_level('standard') 55 | elif line == 'config': 56 | print(f"CONFIG --- URL=[{machinesAdapter.uri}]") 57 | elif line == 'show': 58 | for m in machinesAdapter.machines: 59 | if m.sub is not None: 60 | print(f"CONNECTED --- [{m.ns}]") 61 | else: 62 | print(f"NOT CONNECTED --- [{m.ns}]") 63 | elif line == 'exit': 64 | print("EXIT --- [Exit application.]") 65 | break 66 | 67 | 68 | async def main(): 69 | # set runtime dir to directory of script file 70 | os.chdir(sys.path[0]) 71 | custom_logger.setup_logging("./logs/", "logger.log") 72 | logger = logging.getLogger(__name__) 73 | 74 | try: 75 | logger.trace("Directory=%s", sys.path[0]) 76 | 77 | # Read configuration XML Document 78 | doc = minidom.parse("machineconfiguration.xml") 79 | custom_logger.set_level(doc.documentElement.getAttribute("traceLevel") ) 80 | 81 | # Create and start server 82 | isTrumpfServer = doc.documentElement.getAttribute("isTrumpfServer") == "true" 83 | endpointUri = doc.documentElement.getAttribute("adapterEndpoint") 84 | adapterServer = AdapterServer(endpointUri, isTrumpfServer) 85 | await adapterServer.initialize() 86 | asyncio.create_task(adapterServer.run()) 87 | 88 | # Add machines and start adapter 89 | machines = doc.getElementsByTagName("machine") 90 | uri = doc.documentElement.getAttribute("machinesServer") 91 | machinesAdapter = MachinesAdapterClient(uri, adapterServer, isTrumpfServer) 92 | logger.trace("Add Machines.") 93 | for m in machines: 94 | await machinesAdapter.add_machine(m.getAttribute("machineName"), m.getAttribute("ns")) 95 | asyncio.create_task(machinesAdapter.run()) 96 | 97 | # wait for everything is set up 98 | await asyncio.sleep(10) 99 | 100 | # here code stops until user types exit 101 | await interactive(machinesAdapter) 102 | 103 | # exit 104 | await machinesAdapter.stop() 105 | await adapterServer.stop() 106 | 107 | except Exception: 108 | logger.exception("Exception") 109 | 110 | 111 | if __name__ == "__main__": 112 | # execute if run as a script 113 | asyncio.run(main()) 114 | -------------------------------------------------------------------------------- /AlarmConverter/Python/custom_logger.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022 TRUMPF Werkzeugmaschinen SE + Co. KG 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | import logging.handlers 25 | import os 26 | 27 | # Add custom log level TRACE between Info and Warning. Warnings in libraries are logged, but not Infos. 28 | TRACE_LOG_LEVEL = 25 # log levels https://docs.python.org/3.5/howto/logging.html#logging-levels 29 | logging.addLevelName(TRACE_LOG_LEVEL, "TRACE") 30 | def trace(self, message, *args, **kws): 31 | if self.isEnabledFor(TRACE_LOG_LEVEL): 32 | # Yes, logger takes its '*args' as 'args'. 33 | self._log(TRACE_LOG_LEVEL, message, args, **kws) 34 | logging.Logger.trace = trace 35 | 36 | 37 | def setup_logging(basePath, fileName): 38 | os.makedirs(basePath, exist_ok=True) 39 | loggingFullPath = os.path.join(basePath, fileName) 40 | rootLogger = logging.getLogger() 41 | rootLogger.setLevel(TRACE_LOG_LEVEL) 42 | logFormatter = logging.Formatter("%(asctime)s [%(name)-30.30s] %(message)s") 43 | fileHandler = logging.handlers.RotatingFileHandler(loggingFullPath, maxBytes=1000000, backupCount=7, encoding='utf-8') 44 | fileHandler.setFormatter(logFormatter) 45 | consoleHandler = logging.StreamHandler() 46 | consoleHandler.setFormatter(logFormatter) 47 | rootLogger.addHandler(fileHandler) 48 | rootLogger.addHandler(consoleHandler) 49 | 50 | 51 | def set_level(log_level): 52 | if log_level == "standard": 53 | logging.getLogger().setLevel(TRACE_LOG_LEVEL) 54 | elif log_level == "verbose": 55 | logging.getLogger().setLevel(logging.INFO) 56 | 57 | -------------------------------------------------------------------------------- /AlarmConverter/Python/doc/alarmConverter.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRUMPF-IoT/OpcUaMachineTools/bc2a03b52795215bb4fe4cb566bdc2917d776a99/AlarmConverter/Python/doc/alarmConverter.PNG -------------------------------------------------------------------------------- /AlarmConverter/Python/gen_certificate.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022 TRUMPF Werkzeugmaschinen SE + Co. KG 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from cryptography.hazmat.primitives import serialization 24 | from cryptography.hazmat.primitives.asymmetric import rsa 25 | from cryptography import x509 26 | from cryptography.x509.oid import NameOID 27 | from cryptography.hazmat.primitives import hashes 28 | import datetime 29 | 30 | def get_application_uri(hostname, applicationName): 31 | return f"urn:{hostname}:freeopcua:{applicationName}" 32 | 33 | 34 | def gen_certificates(privateKeyFullPath, certificateFullPath, hostname, applicationName): 35 | 36 | # Generate the key 37 | key = rsa.generate_private_key( 38 | public_exponent=65537, 39 | key_size=2048, 40 | ) 41 | 42 | privateKeyBytes = key.private_bytes( 43 | encoding=serialization.Encoding.PEM, 44 | format=serialization.PrivateFormat.TraditionalOpenSSL, 45 | encryption_algorithm=serialization.NoEncryption(), 46 | ) 47 | 48 | # Write our key to disk for safe keeping 49 | if (privateKeyFullPath): 50 | with open(privateKeyFullPath, "wb") as f: 51 | f.write(privateKeyBytes) 52 | 53 | # Various details about who we are. For a self-signed certificate the 54 | # subject and issuer are always the same. 55 | # Did not set country, state and locality 56 | # https://cryptography.io/en/latest/x509/reference.html 57 | # https://cryptography.io/en/latest/x509/reference.html#general-name-classes 58 | # https://cryptography.io/en/latest/x509/reference.html#x-509-extensions 59 | theName = f"{applicationName}@{hostname}" 60 | serialNumber = x509.random_serial_number() 61 | subject = issuer = x509.Name([ 62 | x509.NameAttribute(NameOID.LOCALITY_NAME, applicationName), 63 | x509.NameAttribute(NameOID.ORGANIZATION_NAME, f"{applicationName}_Organization"), 64 | x509.NameAttribute(NameOID.COMMON_NAME, theName), 65 | ]) 66 | 67 | # Create early, so that ski.digest can be used in the certificate builder 68 | ski = x509.SubjectKeyIdentifier.from_public_key(key.public_key()) 69 | 70 | cert = ( 71 | x509.CertificateBuilder() 72 | .subject_name(subject) 73 | .issuer_name(issuer) 74 | .public_key(key.public_key()) 75 | .serial_number(serialNumber) 76 | # Start validity period from yesterday 77 | .not_valid_before(datetime.datetime.today() - datetime.timedelta(days=1)) 78 | # Our certificate will be valid for 10 years 79 | .not_valid_after(datetime.datetime.utcnow() + (datetime.timedelta(days=365)*10)) 80 | # ------------------------------------------------------------------------------------------------- 81 | # If an extension is marked as critical (critical True), it can not be ignored by an application. 82 | # The application must recognise and process the extension. 83 | # ------------------------------------------------------------------------------------------------- 84 | .add_extension( 85 | # The subject key identifier extension provides a means of identifying 86 | # certificates that contain a particular public key. 87 | ski, critical=False) 88 | .add_extension( 89 | # The authority key identifier extension provides a means of 90 | # identifying the public key corresponding to the private key used to sign a certificate. 91 | x509.AuthorityKeyIdentifier(ski.digest, [x509.DirectoryName(issuer)], serialNumber), critical=False) 92 | # Subject alternative name is an X.509 extension that provides a list of general name instances 93 | # that provide a set of identities for which the certificate is valid. 94 | .add_extension( 95 | x509.SubjectAlternativeName([ 96 | # URI must be first entry of SubjectAlternativeName. Must exactly match the client.application_uri 97 | x509.UniformResourceIdentifier(get_application_uri(hostname, applicationName)), 98 | x509.DNSName(hostname) 99 | ]), critical=False) 100 | # The key usage extension defines the purpose of the key contained in the certificate. 101 | .add_extension( 102 | # Basic constraints is an X.509 extension type that defines whether a given certificate is 103 | # allowed to sign additional certificates and what path length restrictions may exist. 104 | x509.BasicConstraints(ca=False, path_length=None), critical=True) 105 | .add_extension( 106 | x509.KeyUsage( 107 | digital_signature=True, content_commitment=True, key_encipherment=True, 108 | data_encipherment=True, key_agreement=False, key_cert_sign=True, crl_sign=False, 109 | encipher_only=False, decipher_only=False), critical=True) 110 | .add_extension( 111 | # This extension indicates one or more purposes for which the certified public key may be used, 112 | # in addition to or in place of the basic purposes indicated in the key usage extension. 113 | x509.ExtendedKeyUsage([x509.OID_SERVER_AUTH, x509.OID_CLIENT_AUTH]), critical=True) 114 | # Sign our certificate with our private key 115 | .sign(key, algorithm=hashes.SHA256())) 116 | 117 | certificateBytes = cert.public_bytes(serialization.Encoding.DER) 118 | 119 | if (certificateFullPath): 120 | with open(certificateFullPath, "wb") as f: 121 | f.write(certificateBytes) 122 | 123 | return (privateKeyBytes, certificateBytes) -------------------------------------------------------------------------------- /AlarmConverter/Python/machineconfiguration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /AlarmConverter/Python/machineconfiguration_ExampleGateway.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /AlarmConverter/Python/machineconfiguration_QuickstartServer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /AlarmConverter/Python/machines_adapter_client.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2022 TRUMPF Werkzeugmaschinen SE + Co. KG 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import asyncio 24 | import logging 25 | import os, sys 26 | import socket 27 | from asyncua import ua 28 | from asyncua import Client 29 | from asyncua.ua.uaprotocol_auto import MessageSecurityMode 30 | from gen_certificate import gen_certificates, get_application_uri 31 | 32 | 33 | class Machine: 34 | def __init__(self, machineName, namespace): 35 | self.machineName = machineName 36 | self.ns = namespace 37 | self.subCallback = None 38 | self.unsubHandle = None 39 | self.sub = None 40 | 41 | 42 | class SubCallback: 43 | def __init__(self, machineName, server, isTrumpfServer): 44 | self.logger = logging.getLogger("subcallback") 45 | self.machineName = machineName 46 | self.server = server 47 | self.isTrumpfServer = isTrumpfServer 48 | self.freeAlarmSlots = list(range(19, -1, -1)) 49 | self.pendingAlarms = {} 50 | 51 | 52 | async def status_change_notification(s1,s2): 53 | None 54 | 55 | 56 | async def event_notification(self, event): 57 | # To avoid special event for ConditionRefresh 'Condition refresh started for subscription X.' 58 | if (event.NodeId): 59 | eventAsDict = self.event_to_dictionary(event) 60 | conditionId = event.NodeId.to_string() 61 | conditionKeys = self.pendingAlarms.keys() 62 | # A condition/alarm appears with Retain=True and disappears with Retain=False 63 | if event.Retain and not conditionId in conditionKeys: 64 | if (len(self.freeAlarmSlots) > 0): 65 | # get first free slot of end of list 66 | slotNumber = self.freeAlarmSlots.pop() 67 | eventAsDict["slot"] = slotNumber 68 | self.pendingAlarms[conditionId] = eventAsDict 69 | self.logger.info("MachineName: %s, Alarm added: %s, Slot: %s", self.machineName, conditionId, slotNumber) 70 | await self.server.update_alarm_values(self.machineName, slotNumber, eventAsDict) 71 | await self.server.update_alarm_list(self.machineName, self.pendingAlarms) 72 | if not event.Retain and conditionId in conditionKeys: 73 | slotNumber = self.pendingAlarms[conditionId]["slot"] 74 | # return now free slot to list 75 | self.freeAlarmSlots.append(slotNumber) 76 | self.freeAlarmSlots.sort(reverse=True) 77 | del self.pendingAlarms[conditionId] 78 | # reset to default values 79 | self.logger.info("MachineName: %s, Alarm removed: %s, Slot: %s", self.machineName, conditionId, slotNumber) 80 | await self.server.reset_alarm_values(self.machineName, slotNumber, eventAsDict) 81 | await self.server.update_alarm_list(self.machineName, self.pendingAlarms) 82 | self.logger.info("MachineName: %s, Current conditions: %s", self.machineName, conditionKeys) 83 | 84 | 85 | def event_to_dictionary(self, event): 86 | eventDict = {} 87 | eventDict["time"] = event.Time # Change here for special time string if needed 88 | if self.isTrumpfServer: 89 | eventDict["identifier"] = event.AlarmIdentifier 90 | eventDict["source"] = event.SourceName 91 | eventDict["name"] = event.ConditionName 92 | eventDict["severity"] = event.Severity 93 | eventDict["retain"] = event.Retain 94 | if type(event.Message) is str: 95 | eventDict["text"] = event.Message # Trumpf server delivers type string 96 | elif type(event.Message) is ua.uatypes.LocalizedText: 97 | eventDict["text"] = event.Message.Text # Trumpf server with new alarm number system delivers LocalizedText Type 98 | return eventDict 99 | 100 | 101 | class MachinesAdapterClient: 102 | 103 | def __init__(self, uri, server, isTrumpfServer): 104 | self.logger = logging.getLogger(__name__) 105 | self.logger.trace("init MachineAdapter") 106 | self.isRunning = False 107 | self.uri = uri 108 | self.server = server 109 | self.isTrumpfServer = isTrumpfServer 110 | self.machines = [] 111 | self.client = Client(self.uri, 10) 112 | self.foundationNS = "http://opcfoundation.org/UA/" 113 | self.isLastStateConnected = False 114 | 115 | 116 | async def add_machine(self, machineName, namespace): 117 | self.logger.trace("add_machine machineName=%s, namespace=%s", machineName, namespace) 118 | # Add machine node to server 119 | await self.server.add_machine(machineName) 120 | self.machines.append(Machine(machineName, namespace)) 121 | 122 | 123 | async def create_machine_subscription(self, machine): 124 | self.logger.trace("CONNECT machine %s.", machine.machineName) 125 | self.logger.trace("create_machine_subscription ns=%s", machine.ns) 126 | # get nodes 127 | conditionType = self.client.get_node("ns=0;i=2782") 128 | alarmConditionType = self.client.get_node("ns=0;i=2915") 129 | if self.isTrumpfServer: 130 | idx = await self.client.get_namespace_index(machine.ns) 131 | subType = await alarmConditionType.get_child([f"{idx}:TcMachineAlarmType"]) 132 | subObject = self.client.get_node(f"ns={idx};s=179") 133 | else: 134 | subType = alarmConditionType 135 | subObject = self.client.nodes.server 136 | # subscribe 137 | machine.subCallback = SubCallback(machine.machineName, self.server, self.isTrumpfServer) 138 | machine.sub = await self.client.create_subscription(100, machine.subCallback) 139 | machine.unsubHandle = await machine.sub.subscribe_alarms_and_conditions(subObject, subType) 140 | # Call ConditionRefresh to get the current conditions with retain = true 141 | await conditionType.call_method("0:ConditionRefresh", ua.Variant(machine.sub.subscription_id, ua.VariantType.UInt32)) 142 | 143 | 144 | async def remove_machine_subscription(self, machine): 145 | if machine.sub is not None: 146 | self.logger.trace("DISCONNECT machine %s.", machine.machineName) 147 | try: 148 | self.logger.trace("remove_machine_subscription ns=%s", machine.ns) 149 | await machine.sub.delete() 150 | except Exception as ex: 151 | None 152 | finally: 153 | machine.sub = None 154 | 155 | 156 | async def is_connected(self, namespace, nodeString): 157 | self.logger.info('is_connected namespace=%s, nodeString=%s:', namespace, nodeString) 158 | isConnected = False 159 | try: 160 | # Check if node is reachable 161 | idx = await self.client.get_namespace_index(namespace) 162 | var = self.client.get_node(f"ns={idx};{nodeString}") 163 | val = await var.read_display_name() 164 | if val is not None: 165 | isConnected = True 166 | except Exception as ex: 167 | # log only often in verbose mode (info) 168 | self.logger.info('is not connected Info: %s', ex) 169 | finally: 170 | return isConnected 171 | 172 | 173 | async def setup_security_and_certificates(self, applicationName): 174 | try: 175 | theHostname = socket.gethostname() 176 | if not os.path.exists("certificate.der"): 177 | self.logger.trace("setup_certificates") 178 | gen_certificates("privatekey.pem", "certificate.der", theHostname, applicationName) 179 | # Hint: The client.application_uri must exactly match the SubjectAlternativeName uri in the certificate 180 | self.client.application_uri = get_application_uri(theHostname, applicationName) 181 | self.logger.trace("application uri=%s", self.client.application_uri) 182 | # Check whether Mode SignAndEncrypt is available 183 | endpoints = await self.client.connect_and_get_server_endpoints() 184 | for e in endpoints: 185 | if e.SecurityMode == MessageSecurityMode.SignAndEncrypt: 186 | self.logger.trace("set client security string with SignAndEncrypt") 187 | await self.client.set_security_string("Basic256Sha256,SignAndEncrypt,certificate.der,privatekey.pem") 188 | break 189 | except Exception as ex: 190 | self.logger.trace('Unexpected error in setup certificates: %s, %s', ex, sys.exc_info()[0]) 191 | 192 | 193 | async def update_subscriptions(self): 194 | for machine in self.machines: 195 | # check node 1 of trumpf namespace 196 | nodeString = "s=1" 197 | ns = machine.ns 198 | if not self.isTrumpfServer: 199 | nodeString = "i=2259" 200 | ns = self.foundationNS 201 | if await self.is_connected(ns, nodeString): 202 | if machine.sub is None: 203 | await self.create_machine_subscription(machine) 204 | else: 205 | # Reset subscription 206 | await self.remove_machine_subscription(machine) 207 | 208 | 209 | async def clean_up_old_connection(self): 210 | try: 211 | self.logger.trace("Clean up old connection.") 212 | self.isLastStateConnected = False 213 | for m in self.machines: 214 | await self.remove_machine_subscription(m) 215 | await self.client.disconnect() 216 | except: 217 | None 218 | 219 | 220 | async def update_connection(self): 221 | # Check server connection, server state variable is 2259 222 | if not await self.is_connected(self.foundationNS, "i=2259"): 223 | if self.isLastStateConnected: 224 | await self.clean_up_old_connection() 225 | self.logger.trace("Try to connect to server=%s", self.uri) 226 | await self.client.connect() 227 | self.isLastStateConnected = True # only set if no exception 228 | 229 | 230 | async def run(self): 231 | self.logger.trace("run Start") 232 | await self.setup_security_and_certificates("alarmconverter") 233 | self.isRunning = True 234 | while self.isRunning: 235 | try: 236 | self.logger.trace("...is active") 237 | await self.update_connection() # if not possible -> Exception 238 | await self.update_subscriptions() 239 | except asyncio.CancelledError: # happens on shutdown 240 | self.logger.trace("MachineAdapter Task cancelled") 241 | raise 242 | except asyncio.TimeoutError: 243 | self.logger.trace('Timeout error. Connection not possible: %s', sys.exc_info()[0]) 244 | except Exception as ex: 245 | self.logger.trace('Unexpected error: %s, %s', ex, sys.exc_info()[0]) 246 | # only log full exception stack in verbose mode (info) 247 | self.logger.info('Exception:', exc_info=True) 248 | finally: 249 | await asyncio.sleep(10) 250 | 251 | 252 | async def stop(self): 253 | self.logger.trace("stop") 254 | self.isRunning = False 255 | if await self.is_connected(self.foundationNS, "i=2259"): 256 | for machine in self.machines: 257 | await self.remove_machine_subscription(machine) 258 | await self.client.disconnect() 259 | -------------------------------------------------------------------------------- /Doc/Attachments/JobOrders.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Only for tube and 3d laser machines. 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | Obsolete: equal to Automatic 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /Doc/Attachments/examplePackage1.tpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Doc/Attachments/examplePackage2.tpp: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Doc/connectionstate.md: -------------------------------------------------------------------------------- 1 | ## How to determine the connection state of a machine aggregated in the OPC UA gateway 2 | 3 | #### Possibility 1 (recommended): 4 | Subscribe to an item in the namespace of the machine which always has a value. For example FeedrateOverride (s=33). If the callback delivers a value and the StatusCode is Good (0x00000000), the machine is connected. If the callback delivers another StatusCode, for example BadOutOfService (0x808D0000) or BadConnectionClosed (0x80AE0000), then the machine should be regarded as disconnected. 5 | 6 | Overview of StatusCodes: 7 | http://www.opcfoundation.org/UA/schemas/1.04/StatusCode.csv 8 | #### Possibility 2: 9 | Subscribe to a special ServerStatus variable in the OPC UA gateway for each machine. The node id of the ServerStatus is ```ns=1;s=OpcUaServers.{connectionName}.ServerStatus```. If the value is "Connected", then the machine is connected. If the value is not "Connected", then the machine is disconnected. That method will not work with the OPC UA retrofit cube. 10 | 11 | ServerStatus browse path: 12 | Objects -> DeviceSet -> OPC UA Servers -> {ConnectionName} -> Diagnostics -> ServerStatus -------------------------------------------------------------------------------- /Doc/guidelines.md: -------------------------------------------------------------------------------- 1 | ## Remarks 2 | - Signal documentation 3 | - *Sampling rate / Trigger* and *Initial condition* are relevant for the initial signal state after machine restart. 4 | - The *"Trigger"* defines the moment the signal is updated. Until a signal is updated for the first time after restarting the machine, the signal is in state "BadOutOfService" or in state "BadWaitingForInitialData". 5 | - The OPC UA standard defines two ways to receive data. Both are used by TRUMPF. 6 | - Data Items which can be read or subscribed to. 7 | - Events which can be subscribed to. 8 | - Events 9 | - Events can be subscribed on object nodes whose attribute "EventNotifier" is set to "SubscribeToEvents". 10 | - The OPC UA Alarms and Conditions ([OPC 10000-9: UA Part 9: Alarms and Conditions](https://reference.opcfoundation.org/Core/Part9/v105/docs/)) standard is based on events. 11 | - In the TRUMPF address space, alarm events can be subscribed on ID 179. Other object nodes are defined to provide certain events, for example ID 444: SheetLoadingFinished. 12 | - Events can be viewed in UaExpert by adding a new document tab with Document->Add->Event view. 13 | 14 | 15 | ## Guidelines 16 | 17 | 18 | - Do not hardcode **namespace indexes**. Resolve namespace index from namespace URI before accessing it. Read section "[Node Namespace](https://opclabs.doc-that.com/files/onlinedocs/PicoOpc/1.0/BrowserHelp/Node%20Identification.html)". 19 | - To support **complex data types** like structs, use the automatic mechanisms of your SDK to auto create the necessary classes and types. For example complexTypeSystem.Load() in the opc foundation .NET SDK. In the future, defined structs might be extended via inheritance. To stay compatible, the auto type mechanisms must be used. 20 | Example of complex type: 21 | ``` 22 | TsPositionOffset 23 | { 24 | double XOffset; 25 | double YOffset; 26 | double ZOffset; 27 | doubl XRotation; 28 | double YRotation; 29 | double ZRotation; 30 | } 31 | ``` 32 | - Node hierarchy might be reorganised in a future TRUMPF OPC UA interface release. Do **not rely on (i.e. hardcode) node hierarchy**. 33 | - Access the nodes programmatically using nodeIDs OR 34 | - Browse for ObjectType and dynamically resolve node IDs if possible. 35 | - Do not depend on browse names or display names for signals that are not part of the OPC UA companion specifications. They may be changed in future TRUMPF OPC UA interface releases. The node IDs will remain stable. 36 | - There is no guarantee that all signals related to the same trigger (e.g. program started) will be updated on the same millisecond and with the exact same timestamp. 37 | - Nodes can remain without a value until the first trigger event. Until then the OPC UA status code is "BadWaitingForInitialData 0x80320000" or "BadOutOfService 0x808D0000". That is a standard response. (When certain nodes are set at a certain trigger, they remain uninitialized until the first trigger event occurs.) 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Doc/opcuaclients.md: -------------------------------------------------------------------------------- 1 | ## General recommendations 2 | 3 | #### To connect a new client to the TRUMPF OPC UA Gateway 4 | - Usage of a self-signed certificate on client side is necessary. 5 | - A connection attempt must be made with the still untrusted client, which will be declined by the OPC UA Proxy / OPC Gateway. 6 | - Afterwards use the TRUMPF OPC UA Proxy / OPC Gateway configuration tool to trust the client. 7 | 8 | #### Explore and test the OPC UA interface with UaExpert 9 | - Download and use the generic free OPC UA client [UaExpert](https://www.unified-automation.com/products/development-tools/uaexpert.html). 10 | - OPC UA has two different mechanism to subscribe to data via DATA ITEMS or via EVENTS. 11 | - For subscriptions to **Data Items** use the "Data Access View" (`UaExpert -> Document -> Add -> Data Access View`). 12 | - For subscriptions to **Events** use the "Event View" (`UaExpert -> Document -> Add -> Event View`). 13 | - TRUMPF machines, if supported, provide the machine **alarms/messages** via the “OPC UA Alarms and Conditions” Standard, which is based on the OPC UA Events mechanism (see ID 179: Messages). 14 | - Examples on how to consume alarms/messages can be found [here](../Examples/NetCore) and [here](../Examples/Python). 15 | - To see all alarms/messages from a machine with UaExpert: 16 | - Drag the Messages node (179: Messages) to the Configuration Area in the Event View. 17 | - Select “`TcMachineAlarmType`” in the Configuration Area and press “**Apply**” to see all parameters in future alarms (`ConditionType → AckConditionType → AlarmConditionType → …`) 18 | - Observe the flow of events in the events tab. See the current pending alarms in the alarms tab. 19 | -------------------------------------------------------------------------------- /Doc/opcuagateway.md: -------------------------------------------------------------------------------- 1 | ## How to access the data / signals of different machines aggregated in the OPC UA Gateway 2 | 3 | All machines are aggregated via different OPC UA Namespaces. Each machine gets a unique namespace prefix. To access the data of a desired machine, first connect to the OPC UA gateway, then determine the corresponding namespace index using the unique namespace URI of that machine. 4 | 5 | With the obtained namespace index and the node identifiers, all data items / signals of the machine can be accessed. 6 | 7 | #### Namespaces schema for TRUMPF machines: 8 | - ```urn:X0REPLACE0X:TRUMPF:UAInterfaces/http://trumpf.com/TRUMPF-Interfaces/``` 9 | 10 | Just replace *"X0REPLACE0X"* with the *hostname* of the TRUMPF machine. 11 | 12 | ##### Possible usage of the namespace URIs: 13 | - For example in the configuration of your OPC UA client software which items shall be subscribed. Configuration should always contain the namespace URI and never the namespace index. 14 | - In your programming SDK usually there is a method which returns the namespace index e.g. ```session.NamespaceUris.GetIndex(machineNamespace)``` or ```client.get_namespace_index(machineNamespace)``` 15 | - Each OPC UA server has a standard node "NamespaceArray". That node can be read to get the list of all namespace URIs with their corresponding index. It can always be accessed via namespace index 0 and integer identifier 2255. 16 | 17 | ##### General remarks: 18 | After connecting to any OPC UA server the first step is to resolve the desired namespace URI to the namespace index. In OPC UA there is no guarantee that namespace indices will stay the same between new client sessions or restarts of the server. 19 | 20 | Therefore configuration files must never contain namespace indices. They must contain the namespace URIs. For example if there is a configuration file for subscriptions, each node must be referenced via a combination of namespace URI and identifier. 21 | 22 | If your vendor only allows to configure namespace indices and not namespace URIs, please tell them to allow URIs. 23 | 24 | ###### Example bad configuration: 25 | ```ns=5;i=33``` 26 | 27 | ###### Example good configuration: 28 | ```ns=urn:X0REPLACE0X:TRUMPF:UAInterfaces/http://trumpf.com/TRUMPF-Interfaces/;i=33``` 29 | 30 | The only fixed and reserved namespace is the namespace for the URI ```http://opcfoundation.org/UA/```, it's index is always 0. 31 | -------------------------------------------------------------------------------- /Doc/productionplan.md: -------------------------------------------------------------------------------- 1 | ## How to add orders to the production plan of the machine 2 | 3 | There is no OPC UA method. Orders are added to the production plan via the file interface LST or TPP. 4 | 5 | ## Definitions for cutting machines 6 | 7 | Since different parts are nested to one sheet, there might be a 1:1 or 1:N relationship to customer orders, depending on the company process. 8 | 9 | ##### Machine: ProgramName 10 | The Program describes how to cut one sheet metal. 11 | 12 | ##### Machine: ProductionOrderIdentifier [TruTops Boost: Manufacturing order description] 13 | The ProductionOrder defines a concrete production task. A ProductionOrder defines the task to cut a Program n-times and defines a name for the task, the ProductionOrderIdentifier. Apart from the program count other additional parameters, like the sequence number for loading and unloading, can be set in the ProductionOrder. 14 | 15 | ##### Machine: ProductionPackageName [TruTops Boost: Job no., Job name] 16 | The third level of aggreagation is the ProductionPackage. It can be used to combine several ProductionOrders and give them a common name, the ProductionPackageName. For example to relate several ProductionOrders to an assembly group. 17 | 18 | ##### Machine: Production plan 19 | The production plan is the "playlist" on the machine and consists of production orders, which might be grouped to production packages. 20 | 21 | 22 | ## Production plan functionalities via LST file: 23 | 24 | Production orders can be appended to the production plan of the machine using a FERTIGUNG_AUFTRAG_TMP section in the LST file. 25 | 26 | 27 | ``` 28 | BEGIN_FERTIGUNG_AUFTRAG_TMP 29 | C 30 | ZA,MM,6 31 | MM,AT,1, 10,1,1,,'Jobname' ,,'',T 32 | MM,AT,1, 20,1,1,,'FertAuftrBez' ,,'',T 33 | MM,AT,1, 30,1,1,,'FertStatus' ,,'',T 34 | MM,AT,1, 40,1,1,,'Programmname' ,,'',T 35 | MM,AT,1, 50,1,1,,'SollAnzahl' ,,'',Z 36 | MM,AT,1, 60,1,1,,'IstAnzahl' ,,'',Z 37 | C 38 | ZA,DA,2 39 | DA,'JOB43','JOB43_1','0','JOB43_1',1,0 40 | DA,'JOB47','JOB47_1','0','JOB47_1',4,0 41 | C 42 | ENDE_FERTIGUNG_AUFTRAG_TMP 43 | 44 | ``` 45 | 46 | | Id | Name | Comments | 47 | | ----- | ----------------| --------------------- | 48 | | 10 | JobName, ProductionPackageName | | 49 | | 20 | ManufacturingOrder, ProductionOrderIdentifier | Must be unique in the production plan. | 50 | | 30 | ManufacturingStatus, ProductionOrderState | (optional) 0=Released, 9=Disabled | 51 | | 40 | ProgramName | 52 | | 50 | TargetQuantitiy | Number of sheet metal program runs. 53 | | 60 | CurrentQuantity | (optional) default=0 54 | 55 | If a LST containing a FERTIGUNG_AUFRAG_TMP section is imported, the production plan gets filled. The FERTIUNG_AUFTRAG_TMP section can exist in addition to the program in the LST or as the sole entry. 56 | 57 | #### Activate FERTIGUNG_AUFTRAG_TMP section in TruTops Boost 58 | 59 | To auto generate the section, set the **Transfer type** to "Production Package" in the workplace settings of a machine in HomeZone. 60 | 61 | ![Transfer type](transferType.png) 62 | 63 | 64 | ## Production plan functionalities via TPP file: 65 | 66 | TPP files can be imported on TRUMPF cutting machines. With TPP files, production orders can be appended to the production plan of the machine. Their content is XML and the file ending must be .tpp. TPP is an abbreviation for "TRUMPF Production Package". 67 | 68 | Example TPP files: 69 | - [examplePackage1.tpp](Attachments/examplePackage1.tpp) 70 | - [examplePackage2.tpp](Attachments/examplePackage2.tpp) 71 | 72 | XML schema file: 73 | - [JobOrders.xsd](Attachments/JobOrders.xsd) 74 | 75 | | XML-Object Name | Description | Attributes | 76 | | ------------------ | ------------------| ------------------- | 77 | | Jobs | List of Jobs | | 78 | | Job | Collection of production orders and their sequence in the production plan. | JobName | 79 | | DeviceOrderChain | Not in useage any more. But each device order must be embedded into a DeviceOrderChain element. | | 80 | | DeviceOrder | Production order at 2D-cutting machines. Defines a production order for a device. The desired "work plan" is defined via the SequenceListId. It defines whether loading/unloading/processing/sorting steps are executed or not. | ProgName, SequenceListId, DesiredQuantity, CurrentQuantity, State | 81 | | Description | Production order description. | | 82 | | Comment | Additional comment for the production order. | | 83 | | Store | Defines the storage data: which raw material should be fetched from where, on which pallets the finished parts should be unloaded. Platforms or double carts are also referred to as "storage facilities". | | 84 | | Process | Defines a process: For which storage station(s) the data record applies: Raw material, scrap skeleton, maxi parts, emergency, sorting parts, machine pallet, system pallet for machine pallet, scrap. | LocationTypeProperty | 85 | | Group | Additional grouping feature if several data records have to be kept apart for a LocationTypeProperty. Example: When sorting small parts, several pallets can be used on several stations at the same time. | LocationTypePropertyGroup, OneLocationTypePropertyGroupRequired | 86 | | Data | Defines the pallet, the material or the stock group type to be requested. Or the location at which a function is to be carried out. | Sequence, Quantity, RequestType, RequestValue, RequestValue2 | 87 | | SortMaster / SortData | Settings for 2D-Laser sorting device SortMaster. | SortMode, CleanPallet, RetryCount | 88 | | OrderSettings | Settings of production orders. | SortingEnabled, CleanPalletEnabled, StopForManualUnloadEnabled, EmptyPalletOrderStartEnabled, PalletChangerUseMode | 89 | | PartOrderInformation | Defines the relationship between the parts in the DeviceOrders and the customer orders. (Which part belongs to which customer). | SequenceInPartInformation, CustomerOrder, Operation | 90 | | ProductionOrder | Production order at 3D-cutting and tube machines. | Name, OrderType, OrderValueText, CurrentQuantity, DesiredQuantity, MaxQuantity, State, ContinuousMachining | 91 | 92 | If a machine supports the OPC UA interface package MCI V01.00.00, the current production plan can be exported in TPP format via the OPC UA method: `ID 331: GetProductionPlanAsTPP` -------------------------------------------------------------------------------- /Doc/signalsusecases.md: -------------------------------------------------------------------------------- 1 | > [!NOTE] 2 | > Specific signals might or might not be available on a machine, depending on technology, age and software release version. 3 | 4 | ### Monitoring basic machine status 5 | 6 | | ID | Description | OPC UA Signal Package | OPC Mechanism | 7 | | ------------- | ------------| --------------------- | ------------- | 8 | | `ID 4 [Running]` | Indicates that the production plan on the machine is active. | Production Status Interface V3.0 | DATA ITEM | 9 | | `ID 32 [Name]` | Current active program name. | Production Status Interface V2.0 | DATA ITEM | 10 | | `ID 33 [FeedrateOverride]` | Indicates that the production plan on the machine is active. | Production Status Interface V2.0 | DATA ITEM | 11 | | `ID 35.Running` | Basic program state. All Boolean. Active state is True. | Production Status Interface V2.0 | DATA ITEM | 12 | | `ID 35.Stopped` | | | | 13 | | `ID 35.Aborted` | | | | 14 | | `ID 35.Ended` | | | | 15 | | `ID 275 [ProgramState]` | Alternative to ID35.x, basic program state as Enum. | Production Status Interface V2.1 | DATA ITEM | 16 | 17 | ### Getting machine alarms and error messages 18 | 19 | | ID | Description | OPC UA Signal Package | OPC Mechanism | 20 | | ------------- | ------------| --------------------- | ------------- | 21 | | `ID 179 [Messages]` | Machine alarm/messages events. Node used to register for events of type `TcMachineAlarmType`. | Production Status Interface V2.0 | EVENT | 22 | 23 | ### Reading parts and material related information 24 | 25 | | ID | Description | OPC UA Signal Package | OPC Mechanism | 26 | | ------------- | ------------| --------------------- | ------------- | 27 | | `ID 145 [PartsInProgramList]` | Total count of parts on current sheet of current active program. If program is finished with state “Ended”, those parts can be counted as produced parts. | Material Flow Interface V1.0 | DATA ITEM | 28 | | `ID 147 [SheetTechnologyList]` | Parameters of sheet used in current active program. | Material Flow Interface V1.0 | DATA ITEM | 29 | | `ID 443 [PartUnloadingFinished]` | This node sends events of types: `PartUnloadingFinishedEventType`; Trigger: At successful part unload | Material Flow Interface V2.0 | EVENT | 30 | | `ID 444 [SheetLoadingFinished]` | This node sends events of types: `SheetLoadingFinishedEventType`; Trigger: Loading of a sheet is finished | Material Flow Interface V2.0 | EVENT | 31 | | `ID 445 [SheetUnloadingFinished]` | This node sends events of types: `SheetUnloadingFinishedEventType`; Trigger: Unloading of a sheet is finished | Material Flow Interface V2.0 | EVENT | 32 | | `ID 455 [PartUnloadingStarted]` | This node sends events of types: `PartUnloadingStartedEventType`; Trigger: At successful start of part unload | Material Flow Interface V2.0 | EVENT | 33 | 34 | ### Other interesting signals 35 | 36 | | ID | Description | OPC UA Signal Package | OPC Mechanism | 37 | | ------------- | ------------| --------------------- | ------------- | 38 | | `ID 59 [NetBeamOnTime]` | The accumulated total laser beam on time of the current active or last executed program until the next program is started. | Production Status Interface V2.0 | DATA ITEM | 39 | | `ID 69 [EmergencyStop]` | Signals that an emergency stop is active. | Production Status Interface V2.0 | DATA ITEM | 40 | | `ID 73 ... ID 77 [LightBarriers]` | The safety circuit for light barrier #1 has been interrupted. | Production Status Interface V2.0 | DATA ITEM | 41 | | `ID 330 [LaserTechnologyList]` | Laser processing parameters in current active program. | Process Parameters Interface V1.0 | DATA ITEM | 42 | | `ID 343 [WorkpiecePositionOffset]` | Offset of the workpiece position related to the programmed zero point. | Material Flow Interface V1.0 | DATA ITEM | 43 | | `ID 345 [PalletPosition]` | Position of machine pallets A and B. Signal change when the pallet end position is reached. Status Undefined when pallet in motion and on change preparation position (pallet B outside and bottom, pallet A outside and top, pallet A and B outside and top). | Material Flow Interface V1.0 | DATA ITEM | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Doc/transferType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRUMPF-IoT/OpcUaMachineTools/bc2a03b52795215bb4fe4cb566bdc2917d776a99/Doc/transferType.png -------------------------------------------------------------------------------- /Examples/NetCore/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml 402 | 403 | ## 404 | ## Visual studio for Mac 405 | ## 406 | 407 | 408 | # globs 409 | Makefile.in 410 | *.userprefs 411 | *.usertasks 412 | config.make 413 | config.status 414 | aclocal.m4 415 | install-sh 416 | autom4te.cache/ 417 | *.tar.gz 418 | tarballs/ 419 | test-results/ 420 | 421 | # Mac bundle stuff 422 | *.dmg 423 | *.app 424 | 425 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 426 | # General 427 | .DS_Store 428 | .AppleDouble 429 | .LSOverride 430 | 431 | # Icon must end with two \r 432 | Icon 433 | 434 | 435 | # Thumbnails 436 | ._* 437 | 438 | # Files that might appear in the root of a volume 439 | .DocumentRevisions-V100 440 | .fseventsd 441 | .Spotlight-V100 442 | .TemporaryItems 443 | .Trashes 444 | .VolumeIcon.icns 445 | .com.apple.timemachine.donotpresent 446 | 447 | # Directories potentially created on remote AFP share 448 | .AppleDB 449 | .AppleDesktop 450 | Network Trash Folder 451 | Temporary Items 452 | .apdisk 453 | 454 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 455 | # Windows thumbnail cache files 456 | Thumbs.db 457 | ehthumbs.db 458 | ehthumbs_vista.db 459 | 460 | # Dump file 461 | *.stackdump 462 | 463 | # Folder config file 464 | [Dd]esktop.ini 465 | 466 | # Recycle Bin used on file shares 467 | $RECYCLE.BIN/ 468 | 469 | # Windows Installer files 470 | *.cab 471 | *.msi 472 | *.msix 473 | *.msm 474 | *.msp 475 | 476 | # Windows shortcuts 477 | *.lnk 478 | -------------------------------------------------------------------------------- /Examples/NetCore/TrumpfNetCoreClientExamples.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31911.196 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TrumpfNetCoreClientExamples", "TrumpfNetCoreClientExamples\TrumpfNetCoreClientExamples.csproj", "{C6AE5ABF-E03C-4E6E-8FB4-A2B0AA62DDE0}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {C6AE5ABF-E03C-4E6E-8FB4-A2B0AA62DDE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {C6AE5ABF-E03C-4E6E-8FB4-A2B0AA62DDE0}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {C6AE5ABF-E03C-4E6E-8FB4-A2B0AA62DDE0}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {C6AE5ABF-E03C-4E6E-8FB4-A2B0AA62DDE0}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {41A34CD0-FD60-4642-BA96-633847DE49A1} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Examples/NetCore/TrumpfNetCoreClientExamples/AlarmsExample.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022 TRUMPF Werkzeugmaschinen GmbH + Co. KG 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | using Opc.Ua; 24 | using Opc.Ua.Client; 25 | using System; 26 | using System.Collections.Generic; 27 | 28 | namespace TrumpfNetCoreClientExamples 29 | { 30 | class AlarmsExample 31 | { 32 | NodeId mTcMachineAlarmTypeId; 33 | // Dictionary of all currently pending/existing alarms 34 | Dictionary mPendingAlarms = new Dictionary(); 35 | 36 | public void Start(BaseClient client) 37 | { 38 | Session session = client.ClientSession; 39 | ushort customNamespaceIndex = (ushort)session.NamespaceUris.GetIndex("http://trumpf.com/TRUMPF-Interfaces/"); 40 | mTcMachineAlarmTypeId = new NodeId(1006, customNamespaceIndex); 41 | 42 | // declate callback. 43 | var monitoredItem_Notification = new MonitoredItemNotificationEventHandler(MonitoredItem_Notification); 44 | 45 | // create a subscription for alarm events 46 | Subscription sub = new Subscription(session.DefaultSubscription) { PublishingInterval = 1000, MaxNotificationsPerPublish = 1000 }; 47 | 48 | // Define the events which shall be monitored 49 | var monItem = new MonitoredItem(sub.DefaultItem) 50 | { 51 | StartNodeId = new NodeId("179", customNamespaceIndex), // messages node 52 | NodeClass = NodeClass.Object, 53 | AttributeId = Attributes.EventNotifier, // for events 54 | SamplingInterval = 0, // 0 for events 55 | QueueSize = UInt32.MaxValue, 56 | Filter = CreateEventFilter(customNamespaceIndex) 57 | }; 58 | 59 | // set up callback for notifications. 60 | monItem.Notification += monitoredItem_Notification; 61 | sub.AddItem(monItem); 62 | 63 | session.AddSubscription(sub); 64 | sub.Create(); 65 | 66 | // Currently no effect on trumpf python demo server 67 | // Usually necessary to get the refresh of events for the currently pending alarms 68 | RefreshMessages(sub); 69 | } 70 | 71 | private void RefreshMessages(Subscription sub) 72 | { 73 | sub.Session.Call(ObjectTypeIds.ConditionType, MethodIds.ConditionType_ConditionRefresh, new Variant(sub.Id)); 74 | } 75 | 76 | 77 | private EventFilter CreateEventFilter(ushort myCustomNamespaceIndex) 78 | { 79 | // For very generic filter creations, not easy to understand: 80 | // https://github.com/OPCFoundation/UA-.NETStandard-Samples/blob/master/Workshop/AlarmCondition/Client/FilterDefinition.cs 81 | // https://github.com/OPCFoundation/UA-.NETStandard-Samples/blob/master/Samples/ClientControls.Net4/Common/FilterDeclaration.cs 82 | 83 | var eventFilter = new EventFilter(); 84 | 85 | // Select Clause -> Deliver those Attributes in the event 86 | eventFilter.AddSelectClause(ObjectTypeIds.BaseEventType, "EventType", Attributes.Value); 87 | eventFilter.AddSelectClause(ObjectTypeIds.BaseEventType, "Message", Attributes.Value); 88 | eventFilter.AddSelectClause(ObjectTypeIds.BaseEventType, "SourceName", Attributes.Value); 89 | eventFilter.AddSelectClause(ObjectTypeIds.BaseEventType, "Severity", Attributes.Value); 90 | eventFilter.AddSelectClause(ObjectTypeIds.BaseEventType, "Time", Attributes.Value); 91 | eventFilter.AddSelectClause(ObjectTypeIds.ConditionType, "Retain", Attributes.Value); 92 | eventFilter.AddSelectClause(ObjectTypeIds.AlarmConditionType, "0:ActiveState/Id", Attributes.Value); 93 | eventFilter.AddSelectClause(ObjectTypeIds.ConditionType, string.Empty, Attributes.NodeId); // ConditionId 94 | eventFilter.AddSelectClause(mTcMachineAlarmTypeId, $"{myCustomNamespaceIndex}:AlarmIdentifier", Attributes.Value); 95 | 96 | // Where Clause -> Only deliver events of the specified type 97 | ContentFilter whereClause = new ContentFilter(); 98 | LiteralOperand operandType = new LiteralOperand(); 99 | operandType.Value = new Variant(mTcMachineAlarmTypeId); 100 | ContentFilterElement elementFirstType = whereClause.Push(FilterOperator.OfType, operandType); 101 | 102 | return eventFilter; 103 | } 104 | 105 | private void MonitoredItem_Notification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e) 106 | { 107 | EventFieldList notification = e.NotificationValue as EventFieldList; 108 | if (notification != null) 109 | { 110 | var eType = monitoredItem.GetEventType(notification); 111 | if (eType.NodeId == mTcMachineAlarmTypeId) 112 | { 113 | NodeId conditionId = (NodeId)notification.EventFields[7].Value; 114 | // Alternative: 115 | // bool retain = (bool)monitoredItem.GetFieldValue(notification, ObjectTypeIds.ConditionType, "Retain"); 116 | bool retain = (bool)notification.EventFields[5].Value; 117 | // New Alarm 118 | if (retain && !mPendingAlarms.ContainsKey(conditionId)) 119 | { 120 | object m = notification.EventFields[1].Value; 121 | // Older Trumpf server delivers type string 122 | string message = m is string ? (string)m : ((LocalizedText)m).Text; 123 | var alarm = new TcMachineAlarm 124 | { 125 | EventType = (NodeId)notification.EventFields[0].Value, 126 | Message = message, 127 | SourceName = (string)notification.EventFields[2].Value, 128 | Severity = (ushort)notification.EventFields[3].Value, 129 | Time = (DateTime)notification.EventFields[4].Value, 130 | Retain = retain, 131 | ActiveStateId = (bool)notification.EventFields[6].Value, 132 | AlarmIdentifier = (string)notification.EventFields[8].Value 133 | }; 134 | mPendingAlarms[conditionId] = alarm; 135 | } 136 | // Alarm does not exist any more 137 | if (!retain && mPendingAlarms.ContainsKey(conditionId)) 138 | { 139 | mPendingAlarms.Remove(conditionId); 140 | } 141 | 142 | if (mPendingAlarms.Count > 0) 143 | { 144 | Console.WriteLine("=================================================================================="); 145 | foreach (var kvp in mPendingAlarms) 146 | { 147 | Console.WriteLine(kvp.Value.ToString()); 148 | } 149 | Console.WriteLine("=================================================================================="); 150 | } 151 | } 152 | } 153 | } 154 | private class TcMachineAlarm 155 | { 156 | public NodeId EventType { get; set; } 157 | public string Message { get; set; } 158 | public string SourceName { get; set; } 159 | public ushort Severity { get; set; } 160 | public DateTime Time { get; set; } 161 | public bool Retain { get; set; } 162 | public bool ActiveStateId { get; set; } 163 | public string AlarmIdentifier { get; set; } 164 | public override string ToString() 165 | { 166 | return $"{Time} {AlarmIdentifier} {Message} {SourceName} {Severity}"; 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Examples/NetCore/TrumpfNetCoreClientExamples/BaseClient.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022 TRUMPF Werkzeugmaschinen GmbH + Co. KG 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // -------------------------------------------------------------------------------- 23 | 24 | // Based on work of OPC Foundation 25 | 26 | /* ======================================================================== 27 | * Copyright (c) 2005-2019 The OPC Foundation, Inc. All rights reserved. 28 | * 29 | * OPC Foundation MIT License 1.00 30 | * 31 | * Permission is hereby granted, free of charge, to any person 32 | * obtaining a copy of this software and associated documentation 33 | * files (the "Software"), to deal in the Software without 34 | * restriction, including without limitation the rights to use, 35 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 36 | * copies of the Software, and to permit persons to whom the 37 | * Software is furnished to do so, subject to the following 38 | * conditions: 39 | * 40 | * The above copyright notice and this permission notice shall be 41 | * included in all copies or substantial portions of the Software. 42 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 43 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 44 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 45 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 46 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 47 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 48 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 49 | * OTHER DEALINGS IN THE SOFTWARE. 50 | * 51 | * The complete license agreement can be found here: 52 | * http://opcfoundation.org/License/MIT/1.00/ 53 | * ======================================================================*/ 54 | 55 | using System; 56 | using System.Collections.Generic; 57 | using System.Threading; 58 | using System.Threading.Tasks; 59 | using Opc.Ua; 60 | using Opc.Ua.Client; 61 | using Opc.Ua.Configuration; 62 | 63 | namespace TrumpfNetCoreClientExamples 64 | { 65 | public class BaseClient 66 | { 67 | private bool autoAcceptServerCertificate = true; 68 | private string endpointURL = string.Empty; 69 | 70 | public Session ClientSession { get; set; } 71 | 72 | public BaseClient(string _endpointURL) 73 | { 74 | endpointURL = _endpointURL; 75 | } 76 | 77 | public async Task InitConnection() 78 | { 79 | //Session session; 80 | Console.WriteLine("1 - Create an Application Configuration."); 81 | 82 | ApplicationInstance application = new ApplicationInstance(); 83 | application.ApplicationType = ApplicationType.Client; 84 | application.ConfigSectionName = "Opc.Ua.BasicClient"; 85 | 86 | // load the application configuration. 87 | ApplicationConfiguration config = await application.LoadApplicationConfiguration(false); 88 | application.ApplicationName = config.ApplicationName; 89 | 90 | // check the application certificate. If not already there, it is auto created 91 | bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, 0); 92 | if (!haveAppCertificate) 93 | { 94 | throw new Exception("Application instance certificate invalid!"); 95 | } 96 | 97 | config.ApplicationUri = X509Utils.GetApplicationUriFromCertificate(config.SecurityConfiguration.ApplicationCertificate.Certificate); 98 | if (config.SecurityConfiguration.AutoAcceptUntrustedCertificates) 99 | { 100 | autoAcceptServerCertificate = true; 101 | } 102 | config.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_CertificateValidation); 103 | 104 | Console.WriteLine("2 - Discover endpoints of {0}.", endpointURL); 105 | var selectedEndpoint = CoreClientUtils.SelectEndpoint(endpointURL, haveAppCertificate, 15000); 106 | Console.WriteLine(" Selected endpoint uses: {0}", 107 | selectedEndpoint.SecurityPolicyUri.Substring(selectedEndpoint.SecurityPolicyUri.LastIndexOf('#') + 1)); 108 | 109 | Console.WriteLine("3 - Create a session with OPC UA server."); 110 | var endpointConfiguration = EndpointConfiguration.Create(config); 111 | var endpoint = new ConfiguredEndpoint(null, selectedEndpoint, endpointConfiguration); 112 | ClientSession = await Session.Create(config, endpoint, false, "Alarm Client Session", 60000, new UserIdentity(new AnonymousIdentityToken()), null); 113 | 114 | 115 | Console.WriteLine("5 - Create a subscription with publishing interval of 1 second."); 116 | var subscription = new Subscription(ClientSession.DefaultSubscription) { PublishingInterval = 1000 }; 117 | 118 | Console.WriteLine("6 - Add a list of items (server current time and status) to the subscription."); 119 | var list = new List { 120 | new MonitoredItem(subscription.DefaultItem) 121 | { 122 | DisplayName = "ServerStatusCurrentTime", 123 | StartNodeId = "i="+Variables.Server_ServerStatus_CurrentTime.ToString() 124 | } 125 | }; 126 | list.ForEach(i => i.Notification += OnNotification); 127 | subscription.AddItems(list); 128 | 129 | Console.WriteLine("7 - Add the subscription to the session."); 130 | ClientSession.AddSubscription(subscription); 131 | subscription.Create(); 132 | } 133 | 134 | public void WaitForExit() 135 | { 136 | Console.WriteLine("8 - Running...Press Ctrl-C to exit..."); 137 | 138 | ManualResetEvent quitEvent = new ManualResetEvent(false); 139 | try 140 | { 141 | Console.CancelKeyPress += (sender, eArgs) => 142 | { 143 | quitEvent.Set(); 144 | eArgs.Cancel = true; 145 | }; 146 | } 147 | catch 148 | { 149 | } 150 | 151 | // wait for timeout or Ctrl-C 152 | quitEvent.WaitOne(Timeout.Infinite); 153 | } 154 | 155 | private void OnNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e) 156 | { 157 | foreach (var value in item.DequeueValues()) 158 | { 159 | Console.WriteLine("{0}: {1}, {2}, {3}", item.DisplayName, value.Value, value.SourceTimestamp, value.StatusCode); 160 | } 161 | } 162 | 163 | 164 | // Server certificate is accepted when e.Accept is set to true. 165 | private void CertificateValidator_CertificateValidation(CertificateValidator validator, CertificateValidationEventArgs e) 166 | { 167 | if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted) 168 | { 169 | e.Accept = autoAcceptServerCertificate; 170 | if (autoAcceptServerCertificate) 171 | { 172 | Console.WriteLine("Accepted Certificate: {0}", e.Certificate.Subject); 173 | } 174 | else 175 | { 176 | Console.WriteLine("Rejected Certificate: {0}", e.Certificate.Subject); 177 | } 178 | } 179 | } 180 | } 181 | } 182 | 183 | -------------------------------------------------------------------------------- /Examples/NetCore/TrumpfNetCoreClientExamples/ComplexTypeExample.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022 TRUMPF Werkzeugmaschinen GmbH + Co. KG 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | using Newtonsoft.Json; 24 | using Opc.Ua; 25 | using Opc.Ua.Client; 26 | using Opc.Ua.Client.ComplexTypes; 27 | using System; 28 | using System.IO; 29 | using System.Threading; 30 | 31 | namespace TrumpfNetCoreClientExamples 32 | { 33 | class ComplexTypeExample 34 | { 35 | public void Start(BaseClient client) 36 | { 37 | Session session = client.ClientSession; 38 | ushort customNamespaceIndex = (ushort)session.NamespaceUris.GetIndex("http://trumpf.com/TRUMPF-Interfaces/"); 39 | 40 | // Load necessary complex types as types. Here struct TsSheetTechnology i=3002 41 | var complexTypeSystem = new ComplexTypeSystem(session); 42 | ExpandedNodeId sheetTechType = new ExpandedNodeId(3002, customNamespaceIndex); 43 | Type systemType1 = complexTypeSystem.LoadType(sheetTechType).Result; 44 | // Alternative auto load all types, but takes some seconds 45 | // complexTypeSystem.Load() 46 | 47 | NodeId sheetTechListId = new NodeId("147", customNamespaceIndex); // 147 SheetTechnologyList 48 | 49 | for (int i = 0; i < 20; i++) 50 | { 51 | // If session.ReadValue is used then exception needs to be handled for certain statuscode 52 | // Better allways use session.Read(...) with UaNetStandard SDK 53 | DataValue dv = ReadSingleValue(session, sheetTechListId); 54 | if (dv.StatusCode == 0) 55 | { 56 | // Data as a json 57 | var jsonEncoder = new JsonEncoder(session.MessageContext, false); 58 | jsonEncoder.WriteDataValue("TsSheetTech", dv); 59 | var textbuffer = jsonEncoder.CloseAndReturnText(); 60 | string intendedJson = JsonIntend(textbuffer); 61 | Console.WriteLine(intendedJson); 62 | 63 | // Data as a struct 64 | var sheetTechListValue = (ExtensionObject[])dv.Value; 65 | BaseComplexType sheetTech = (BaseComplexType)sheetTechListValue[0].Body; 66 | TsSheetTech st = new TsSheetTech(sheetTech); // Fill struct or class 67 | } 68 | else 69 | { 70 | Console.WriteLine($"Error on reading value. StatusCode={dv.StatusCode}"); 71 | } 72 | Thread.Sleep(3000); 73 | } 74 | } 75 | 76 | public static DataValue ReadSingleValue(Session session, NodeId readId) 77 | { 78 | ReadValueIdCollection nodesToRead = new ReadValueIdCollection(); 79 | ReadValueId nodeToRead = new ReadValueId(); 80 | nodeToRead.NodeId = readId; 81 | nodeToRead.AttributeId = Attributes.Value; 82 | nodesToRead.Add(nodeToRead); 83 | 84 | // read all values. 85 | DataValueCollection results = null; 86 | DiagnosticInfoCollection diagnosticInfos = null; 87 | 88 | session.Read( 89 | null, 90 | 0, 91 | TimestampsToReturn.Both, 92 | nodesToRead, 93 | out results, 94 | out diagnosticInfos); 95 | 96 | ClientBase.ValidateResponse(results, nodesToRead); 97 | ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToRead); 98 | 99 | return results[0]; 100 | } 101 | 102 | public static string JsonIntend(string json) 103 | { 104 | using (var stringReader = new StringReader(json)) 105 | using (var stringWriter = new StringWriter()) 106 | { 107 | var jsonReader = new JsonTextReader(stringReader); 108 | var jsonWriter = new JsonTextWriter(stringWriter) { Formatting = Formatting.Indented }; 109 | jsonWriter.WriteToken(jsonReader); 110 | return stringWriter.ToString(); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Examples/NetCore/TrumpfNetCoreClientExamples/Model/TsSheetTech.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022 TRUMPF Werkzeugmaschinen GmbH + Co. KG 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | using Opc.Ua.Client.ComplexTypes; 24 | 25 | namespace TrumpfNetCoreClientExamples 26 | { 27 | public struct TsSheetTech 28 | { 29 | public string DatasetName; 30 | public double SheetDimensionX; 31 | public double SheetDimensionY; 32 | public double Thickness; 33 | public int Type; 34 | public int ScratchFree; 35 | public int MagazinePositionClamp1; 36 | public int MagazinePositionClamp2; 37 | public int MagazinePositionClamp3; 38 | public int MagazinePositionClamp4; 39 | public int MagazinePositionClamp5; 40 | public int MagazinePositionClamp6; 41 | public double XLength; 42 | public double YLength; 43 | public string Grade; 44 | public double Density; 45 | public int DynamicLevel; 46 | public string MaterialGroup; 47 | public int ScratchFreeDieOn; 48 | public int AdvancedEvaporateSwitch; 49 | 50 | public TsSheetTech(BaseComplexType sheetTech) 51 | { 52 | DatasetName = (string)sheetTech["DatasetName"]; 53 | SheetDimensionX = (double)sheetTech["SheetDimensionX"]; 54 | SheetDimensionY = (double)sheetTech["SheetDimensionY"]; 55 | Thickness = (double)sheetTech["Thickness"]; 56 | Type = (int)sheetTech["Type"]; 57 | ScratchFree = (int)sheetTech["ScratchFree"]; 58 | MagazinePositionClamp1 = (int)sheetTech["MagazinePositionClamp1"]; 59 | MagazinePositionClamp2 = (int)sheetTech["MagazinePositionClamp2"]; 60 | MagazinePositionClamp3 = (int)sheetTech["MagazinePositionClamp3"]; 61 | MagazinePositionClamp4 = (int)sheetTech["MagazinePositionClamp4"]; 62 | MagazinePositionClamp5 = (int)sheetTech["MagazinePositionClamp5"]; 63 | MagazinePositionClamp6 = (int)sheetTech["MagazinePositionClamp6"]; 64 | XLength = (double)sheetTech["XLength"]; 65 | YLength = (double)sheetTech["YLength"]; 66 | Grade = (string)sheetTech["Grade"]; 67 | Density = (double)sheetTech["Density"]; 68 | DynamicLevel = (int)sheetTech["DynamicLevel"]; 69 | MaterialGroup = (string)sheetTech["MaterialGroup"]; 70 | ScratchFreeDieOn = (int)sheetTech["ScratchFreeDieOn"]; 71 | AdvancedEvaporateSwitch = (int)sheetTech["AdvancedEvaporateSwitch"]; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Examples/NetCore/TrumpfNetCoreClientExamples/Opc.Ua.BasicClient.Config.xml: -------------------------------------------------------------------------------- 1 |  2 | 7 | Trumpf Alarm Client Example 8 | urn:localhost:OPCFoundation:BasicClientSample 9 | http://opcfoundation.org/UA/BasicClientSample 10 | Client_1 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | X509Store 22 | CurrentUser\My 23 | CN=Trumpf Basic Client Example, C=DE, S=Ditzingen, O=Trumpf, DC=localhost 24 | 25 | 26 | 27 | 32 | 33 | 34 | 35 | Directory 36 | %LocalApplicationData%/OPC Foundation/pki/issuer 37 | 38 | 39 | 40 | 41 | Directory 42 | %LocalApplicationData%/OPC Foundation/pki/trusted 43 | 44 | 45 | 46 | 47 | Directory 48 | %LocalApplicationData%/OPC Foundation/pki/rejected 49 | 50 | 51 | 53 | false 54 | 55 | 56 | 57 | 58 | 59 | 60 | 600000 61 | 1048576 62 | 4194304 63 | 65535 64 | 4194304 65 | 65535 66 | 300000 67 | 3600000 68 | 69 | 70 | 71 | 72 | 73 | 74 | 600000 75 | 76 | 78 | 79 | opc.tcp://{0}:4840/UADiscovery 80 | 81 | 82 | 83 | 84 | 85 | 88 | 10000 89 | 90 | 91 | 92 | %LocalApplicationData%/Logs/Opc.Ua.BasicClientSample.log.txt 93 | true 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | true 110 | 111 | -------------------------------------------------------------------------------- /Examples/NetCore/TrumpfNetCoreClientExamples/Program.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022 TRUMPF Werkzeugmaschinen GmbH + Co. KG 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | namespace TrumpfNetCoreClientExamples 24 | { 25 | class Program 26 | { 27 | static void Main(string[] args) 28 | { 29 | string endpointURL = "opc.tcp://localhost:50000"; 30 | BaseClient client = new BaseClient(endpointURL); 31 | client.InitConnection().Wait(); 32 | 33 | // ------------------------------------------- 34 | // Uncomment the example you want to run 35 | // ------------------------------------------- 36 | 37 | // Example how to consume machine alarms/messages 38 | AlarmsExample alarmsExample = new AlarmsExample(); 39 | alarmsExample.Start(client); 40 | 41 | // Example how to read a complex type 42 | //ComplexTypeExample complexExample = new ComplexTypeExample(); 43 | //complexExample.Start(client); 44 | 45 | client.WaitForExit(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Examples/NetCore/TrumpfNetCoreClientExamples/TrumpfNetCoreClientExamples.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Examples/NetCore/readme.md: -------------------------------------------------------------------------------- 1 | ## .NET Core OPC UA Client Examples 2 | 3 | ### Introduction 4 | Everything is provided as open source to provide examples on how to work and program with OPC UA. For the open source tools and examples there is no official TRUMPF support. Feel free to participate and contribute. 5 | 6 | ### Requirements 7 | - .Net Core 3.1 8 | - OPCFoundation [.NetStandard](https://github.com/OPCFoundation/UA-.NETStandard) OPC UA nuget packages 9 | - OPCFoundation.NetStandard.Opc.Ua.Client.Debug 10 | - OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes.Debug 11 | 12 | ### Configuration 13 | 14 | The basic configuration for applications using the OPCFoundation .NetStandard SDK is done within the Config.xml file of the application. Here it is Opc.Ua.BasicClient.Config.xml. 15 | 16 | The server endpoint is configured in Program.cs via the endpointURL. The default URL is the setting for the TRUMPF Python Demo Server. 17 | 18 | #### Certificate handling 19 | Getting some knowledge about certificate handling is necessary. Ignoring the topic will not lead to success. 20 | 21 | During a connection attempt client and server exchange certificates. A connection is only established if both sides have accepted/trusted the certificates of each other. 22 | 23 | *How does the .NetStandard Client gets his client certificate?* 24 | 25 | -> If there is no self signed client certificate yet, a certificate is auto created during ```await application.CheckApplicationInstanceCertificate```. 26 | 27 | *Where is the storage location of certificates?* 28 | 29 | -> The storage locations are defined in Opc.Ua.BasicClient.Config.xml. On Windows, X509Store and CurrentUser\My lead to the windows certificate store. The client certificate (ApplicationCertificate) can be viewed or deleted with the Windows Certificate Management Console (certmgr.msc). If there are issues, try to run the client with admin credentials or configure a different certificate store in the xml. 30 | 31 | Auto acceptance of server certificates can also be activated in the xml config. Accepting a server certificate is done by moving the certificate into the trusted folder. Initially a server certificate is stored in the rejected folder. 32 | 33 | ### Examples 34 | 35 | Run an example by uncommenting in Program.cs. The examples can be run in conjunction with the TRUMPF Python Demo Server. 36 | 37 | **AlarmsExample** 38 | 39 | Shows how to consume Alarms and Conditions and how to determine the currently pending alarms. 40 | 41 | **ComplexTypeExample** 42 | 43 | Shows how to read items with complex data types using the complex type system. 44 | 45 | 46 | ### License 47 | The .NET Core OPC UA Client examples are licensed under the MIT License. 48 | -------------------------------------------------------------------------------- /Examples/NodeRed/README.md: -------------------------------------------------------------------------------- 1 | ## NodeRed OPC UA Client Examples 2 | 3 | ### Introduction 4 | 5 | Everything is provided as open source to provide examples on how to work and program with OPC UA. For the open source tools and examples there is no official TRUMPF support. Feel free to participate. 6 | 7 | ### Examples 8 | 9 | #### Resolve namespaces - flow_resolveNamespace.json 10 | Example how to resolve namespace uri to namespace index. Namespace index should never be hardcoded in OPC UA, which is currently not done well in the node-red opc ua module. 11 | 12 | ![Picture Alarms](doc/resolveNamespace.PNG) 13 | 14 | ### Installation and Execution 15 | Download the example flow files and import them in node-red. 16 | Easiest way is to download all files of the github repository. [Download zip](https://github.com/TRUMPF-IoT/OpcUaMachineTools/archive/main.zip). 17 | 18 | ### License 19 | The NodeRed OPC UA examples are licensed under the MIT License. 20 | -------------------------------------------------------------------------------- /Examples/NodeRed/doc/resolveNamespace.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRUMPF-IoT/OpcUaMachineTools/bc2a03b52795215bb4fe4cb566bdc2917d776a99/Examples/NodeRed/doc/resolveNamespace.PNG -------------------------------------------------------------------------------- /Examples/NodeRed/flow_resolveNamespace.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "8485f7b58648aa20", 4 | "type": "tab", 5 | "label": "Resolve Namespace", 6 | "disabled": false, 7 | "info": "", 8 | "env": [] 9 | }, 10 | { 11 | "id": "8c33f06834d0cd18", 12 | "type": "OpcUa-Client", 13 | "z": "8485f7b58648aa20", 14 | "endpoint": "7d33b075c39d4b68", 15 | "action": "read", 16 | "deadbandtype": "a", 17 | "deadbandvalue": 1, 18 | "time": 10, 19 | "timeUnit": "s", 20 | "certificate": "n", 21 | "localfile": "", 22 | "localkeyfile": "", 23 | "securitymode": "None", 24 | "securitypolicy": "None", 25 | "folderName4PKI": "", 26 | "name": "Read Namespaces", 27 | "x": 550, 28 | "y": 140, 29 | "wires": [ 30 | [ 31 | "a8f059ecd98a5527" 32 | ] 33 | ] 34 | }, 35 | { 36 | "id": "3aa3f1b49ee566d2", 37 | "type": "OpcUa-Item", 38 | "z": "8485f7b58648aa20", 39 | "item": "i=2255", 40 | "datatype": "String Array", 41 | "value": "", 42 | "name": "NameSpaces Item", 43 | "x": 330, 44 | "y": 140, 45 | "wires": [ 46 | [ 47 | "8c33f06834d0cd18" 48 | ] 49 | ] 50 | }, 51 | { 52 | "id": "5a04775cd61cf6ef", 53 | "type": "inject", 54 | "z": "8485f7b58648aa20", 55 | "name": "", 56 | "props": [ 57 | { 58 | "p": "payload" 59 | }, 60 | { 61 | "p": "topic", 62 | "vt": "str" 63 | } 64 | ], 65 | "repeat": "", 66 | "crontab": "", 67 | "once": false, 68 | "onceDelay": 0.1, 69 | "topic": "", 70 | "payload": "", 71 | "payloadType": "date", 72 | "x": 140, 73 | "y": 140, 74 | "wires": [ 75 | [ 76 | "3aa3f1b49ee566d2" 77 | ] 78 | ] 79 | }, 80 | { 81 | "id": "6bdb24ecce34bd30", 82 | "type": "debug", 83 | "z": "8485f7b58648aa20", 84 | "name": "debug ns", 85 | "active": true, 86 | "tosidebar": true, 87 | "console": false, 88 | "tostatus": false, 89 | "complete": "payload", 90 | "targetType": "msg", 91 | "statusVal": "", 92 | "statusType": "auto", 93 | "x": 980, 94 | "y": 140, 95 | "wires": [] 96 | }, 97 | { 98 | "id": "a8f059ecd98a5527", 99 | "type": "function", 100 | "z": "8485f7b58648aa20", 101 | "name": "store namespaces", 102 | "func": "var nsUris = {};\nfor (var i = 0; i < msg.payload.length; i++) {\n nsUris[msg.payload[i]] = i;\n}\n\nflow.set(\"nsUris\", nsUris);\nreturn msg;", 103 | "outputs": 1, 104 | "noerr": 0, 105 | "initialize": "", 106 | "finalize": "", 107 | "libs": [], 108 | "x": 790, 109 | "y": 140, 110 | "wires": [ 111 | [ 112 | "6bdb24ecce34bd30" 113 | ] 114 | ] 115 | }, 116 | { 117 | "id": "8332acd79782f8cb", 118 | "type": "comment", 119 | "z": "8485f7b58648aa20", 120 | "name": "example: ns= http://trumpf.com/TRUMPF-Interfaces/", 121 | "info": "", 122 | "x": 250, 123 | "y": 240, 124 | "wires": [] 125 | }, 126 | { 127 | "id": "fd1ba67d7d4dcc7b", 128 | "type": "complete", 129 | "z": "8485f7b58648aa20", 130 | "name": "Store NS complete", 131 | "scope": [ 132 | "a8f059ecd98a5527" 133 | ], 134 | "uncaught": false, 135 | "x": 150, 136 | "y": 300, 137 | "wires": [ 138 | [ 139 | "4f5e2d95740f6dad" 140 | ] 141 | ] 142 | }, 143 | { 144 | "id": "4f5e2d95740f6dad", 145 | "type": "function", 146 | "z": "8485f7b58648aa20", 147 | "name": "Item Definition", 148 | "func": "var ns = \"http://trumpf.com/TRUMPF-Interfaces/\";\nvar nsPrefix = \"ns=\" + flow.get(\"nsUris\")[ns] + \";\";\n\nvar newMsg = {}\nnewMsg.topic = nsPrefix + \"s=33\"; // Item Node\nnode.status({ text: newMsg.topic });\nreturn newMsg;", 149 | "outputs": 1, 150 | "noerr": 0, 151 | "initialize": "", 152 | "finalize": "", 153 | "libs": [], 154 | "x": 380, 155 | "y": 300, 156 | "wires": [ 157 | [ 158 | "cc4f6240f22da4df" 159 | ] 160 | ] 161 | }, 162 | { 163 | "id": "cc4f6240f22da4df", 164 | "type": "OpcUa-Client", 165 | "z": "8485f7b58648aa20", 166 | "endpoint": "7d33b075c39d4b68", 167 | "action": "read", 168 | "deadbandtype": "a", 169 | "deadbandvalue": 1, 170 | "time": 10, 171 | "timeUnit": "s", 172 | "certificate": "n", 173 | "localfile": "", 174 | "localkeyfile": "", 175 | "securitymode": "None", 176 | "securitypolicy": "None", 177 | "folderName4PKI": "", 178 | "name": "", 179 | "x": 620, 180 | "y": 300, 181 | "wires": [ 182 | [ 183 | "6dd3b89ce06256d2" 184 | ] 185 | ] 186 | }, 187 | { 188 | "id": "6dd3b89ce06256d2", 189 | "type": "debug", 190 | "z": "8485f7b58648aa20", 191 | "name": "debug read item", 192 | "active": true, 193 | "tosidebar": true, 194 | "console": false, 195 | "tostatus": false, 196 | "complete": "payload", 197 | "targetType": "msg", 198 | "statusVal": "", 199 | "statusType": "auto", 200 | "x": 860, 201 | "y": 300, 202 | "wires": [] 203 | }, 204 | { 205 | "id": "7d33b075c39d4b68", 206 | "type": "OpcUa-Endpoint", 207 | "endpoint": "opc.tcp://127.0.0.1:50000", 208 | "secpol": "None", 209 | "secmode": "None", 210 | "none": true, 211 | "login": false, 212 | "usercert": false, 213 | "usercertificate": "", 214 | "userprivatekey": "" 215 | } 216 | ] -------------------------------------------------------------------------------- /Examples/Python/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 TRUMPF Werkzeugmaschinen GmbH + Co. KG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Examples/Python/README.md: -------------------------------------------------------------------------------- 1 | ## Python OPC UA Client Examples 2 | 3 | ### Introduction 4 | 5 | Everything is provided as open source to provide examples on how to work and program with OPC UA. For the open source tools and examples there is no official TRUMPF support. Feel free to participate. 6 | 7 | ### Requirements 8 | * Python >= Python 3.7 9 | * Version >= Python 3.11 is recommended due to startup performance improvements. 10 | * Python opcua-asyncio library >= 0.9.14 11 | 12 | ### Examples 13 | 14 | #### Generate certificate - gen_certificate.py 15 | Library used in all examples to create the certificate if not yet created. 16 | 17 | #### Show pending alarms - show_current_alarms.py 18 | Example client to show the alarms and messages via OPC UA Alarms and Conditions. 19 | 20 | 21 | ### Installation and Execution 22 | With Python installed, the script file can be exuted with `python exampleName.py`. As an alternative a fully self contained .exe can be created with PyInstaller. 23 | 24 | ##### Detailed instruction: 25 | * [Download and install Python](https://www.python.org/downloads/) >= Python 3.7. On installation set checkbox for adding to system path. 26 | * Install opcua-asnycio library >= 0.9.14 with 27 | `pip3 install asyncua` or upgrade with `pip3 install --upgrade asyncua` 28 | Behind a company proxy you may need to add the proxy server and trust servers. Search for proxy settings and look for the manual proxy server. 29 | `pip3 install --trusted-host pypi.org --trusted-host files.pythonhosted.org --proxy=http://username:password@proxyserver:port asyncua` 30 | 31 | * Download all files and copy them to a folder. Easiest way is to download all files of the github repository. [Download zip](https://github.com/TRUMPF-IoT/OpcUaMachineTools/archive/main.zip). 32 | * Enter the folder containing the examples and execute the client example with `python exampleName.py` 33 | * On Linux if Python 2.x and Python 3.x are installed execute with `python3 exampleName.py`. 34 | 35 | To create a self contained .exe on Windows which can be used on systems without Python installed: 36 | * Install PyInstaller with: `pip3 install pyinstaller` 37 | * Comment out line `os.chdir(sys.path[0])` if it exists in python file. 38 | * Switch to folder and execute `pyinstaller.exe --onefile exampleName.py`. The result is in the "dist" directory. 39 | 40 | 41 | ### License 42 | The Python OPC UA Client examples are licensed under the MIT License. 43 | -------------------------------------------------------------------------------- /Examples/Python/gen_certificate.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2021 TRUMPF Werkzeugmaschinen GmbH + Co. KG 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from cryptography.hazmat.primitives import serialization 24 | from cryptography.hazmat.primitives.asymmetric import rsa 25 | from cryptography import x509 26 | from cryptography.x509.oid import NameOID 27 | from cryptography.hazmat.primitives import hashes 28 | import datetime 29 | 30 | def get_application_uri(hostname, applicationName): 31 | return f"urn:{hostname}:freeopcua:{applicationName}" 32 | 33 | 34 | def gen_certificates(privateKeyFullPath, certificateFullPath, hostname, applicationName): 35 | 36 | # Generate the key 37 | key = rsa.generate_private_key( 38 | public_exponent=65537, 39 | key_size=2048, 40 | ) 41 | 42 | privateKeyBytes = key.private_bytes( 43 | encoding=serialization.Encoding.PEM, 44 | format=serialization.PrivateFormat.TraditionalOpenSSL, 45 | encryption_algorithm=serialization.NoEncryption(), 46 | ) 47 | 48 | # Write our key to disk for safe keeping 49 | if (privateKeyFullPath): 50 | with open(privateKeyFullPath, "wb") as f: 51 | f.write(privateKeyBytes) 52 | 53 | # Various details about who we are. For a self-signed certificate the 54 | # subject and issuer are always the same. 55 | # Did not set country, state and locality 56 | # https://cryptography.io/en/latest/x509/reference.html 57 | # https://cryptography.io/en/latest/x509/reference.html#general-name-classes 58 | # https://cryptography.io/en/latest/x509/reference.html#x-509-extensions 59 | theName = f"{applicationName}@{hostname}" 60 | serialNumber = x509.random_serial_number() 61 | subject = issuer = x509.Name([ 62 | x509.NameAttribute(NameOID.LOCALITY_NAME, applicationName), 63 | x509.NameAttribute(NameOID.ORGANIZATION_NAME, f"{applicationName}_Organization"), 64 | x509.NameAttribute(NameOID.COMMON_NAME, theName), 65 | ]) 66 | 67 | # Create early, so that ski.digest can be used in the certificate builder 68 | ski = x509.SubjectKeyIdentifier.from_public_key(key.public_key()) 69 | 70 | cert = ( 71 | x509.CertificateBuilder() 72 | .subject_name(subject) 73 | .issuer_name(issuer) 74 | .public_key(key.public_key()) 75 | .serial_number(serialNumber) 76 | # Start validity period from yesterday 77 | .not_valid_before(datetime.datetime.today() - datetime.timedelta(days=1)) 78 | # Our certificate will be valid for 10 years 79 | .not_valid_after(datetime.datetime.utcnow() + (datetime.timedelta(days=365)*10)) 80 | # ------------------------------------------------------------------------------------------------- 81 | # If an extension is marked as critical (critical True), it can not be ignored by an application. 82 | # The application must recognise and process the extension. 83 | # ------------------------------------------------------------------------------------------------- 84 | .add_extension( 85 | # The subject key identifier extension provides a means of identifying 86 | # certificates that contain a particular public key. 87 | ski, critical=False) 88 | .add_extension( 89 | # The authority key identifier extension provides a means of 90 | # identifying the public key corresponding to the private key used to sign a certificate. 91 | x509.AuthorityKeyIdentifier(ski.digest, [x509.DirectoryName(issuer)], serialNumber), critical=False) 92 | # Subject alternative name is an X.509 extension that provides a list of general name instances 93 | # that provide a set of identities for which the certificate is valid. 94 | .add_extension( 95 | x509.SubjectAlternativeName([ 96 | # URI must be first entry of SubjectAlternativeName. Must exactly match the client.application_uri 97 | x509.UniformResourceIdentifier(get_application_uri(hostname, applicationName)), 98 | x509.DNSName(hostname) 99 | ]), critical=False) 100 | # The key usage extension defines the purpose of the key contained in the certificate. 101 | .add_extension( 102 | # Basic constraints is an X.509 extension type that defines whether a given certificate is 103 | # allowed to sign additional certificates and what path length restrictions may exist. 104 | x509.BasicConstraints(ca=False, path_length=None), critical=True) 105 | .add_extension( 106 | x509.KeyUsage( 107 | digital_signature=True, content_commitment=True, key_encipherment=True, 108 | data_encipherment=True, key_agreement=False, key_cert_sign=True, crl_sign=False, 109 | encipher_only=False, decipher_only=False), critical=True) 110 | .add_extension( 111 | # This extension indicates one or more purposes for which the certified public key may be used, 112 | # in addition to or in place of the basic purposes indicated in the key usage extension. 113 | x509.ExtendedKeyUsage([x509.OID_SERVER_AUTH, x509.OID_CLIENT_AUTH]), critical=True) 114 | # Sign our certificate with our private key 115 | .sign(key, algorithm=hashes.SHA256())) 116 | 117 | certificateBytes = cert.public_bytes(serialization.Encoding.DER) 118 | 119 | if (certificateFullPath): 120 | with open(certificateFullPath, "wb") as f: 121 | f.write(certificateBytes) 122 | 123 | return (privateKeyBytes, certificateBytes) -------------------------------------------------------------------------------- /Examples/Python/show_current_alarms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # MIT License 4 | 5 | # Copyright (c) 2021 TRUMPF Werkzeugmaschinen GmbH + Co. KG 6 | 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | import asyncio 26 | import os 27 | import sys 28 | import socket 29 | from asyncua import Client 30 | from gen_certificate import gen_certificates, get_application_uri 31 | from asyncua import ua 32 | 33 | class SubscriptionCallback: 34 | 35 | def __init__(self): 36 | self.pendingConditions = {} 37 | 38 | def event_notification(self, event): 39 | # To avoid special event for ConditionRefresh 'Condition refresh started for subscription X.' 40 | if (event.NodeId): 41 | eventAsDict = self.event_to_dictionary(event) 42 | conditionId = event.NodeId.to_string() 43 | conditionKeys = self.pendingConditions.keys() 44 | # A condition appears with Retain=True and disappears with Retain=False 45 | if event.Retain and not conditionId in conditionKeys: 46 | self.pendingConditions[conditionId] = eventAsDict 47 | if not event.Retain and conditionId in conditionKeys: 48 | del self.pendingConditions[conditionId] 49 | print(self.pendingConditions) 50 | 51 | def event_to_dictionary(self, event): 52 | eventDict = {} 53 | eventDict["time"] = event.Time 54 | eventDict["identifier"] = event.AlarmIdentifier 55 | eventDict["source"] = event.SourceName 56 | eventDict["type"] = event.SourceName 57 | eventDict["severity"] = event.Severity 58 | eventDict["retain"] = event.Retain 59 | if type(event.Message) is str: 60 | eventDict["text"] = event.Message # Older Trumpf server delivers type string 61 | elif type(event.Message) is ua.uatypes.LocalizedText: 62 | eventDict["text"] = event.Message.Text # Trumpf server with new alarm number system delivers LocalizedText Type 63 | return eventDict 64 | 65 | 66 | async def setup_security_and_certificates(client, applicationName): 67 | try: 68 | theHostname = socket.gethostname() 69 | if not os.path.exists("certificate.der"): 70 | gen_certificates("privatekey.pem", "certificate.der", theHostname, applicationName) 71 | # Hint: The client.application_uri must exactly match the SubjectAlternativeName uri in the certificate 72 | client.application_uri = get_application_uri(theHostname, applicationName) 73 | await client.set_security_string("Basic256Sha256,SignAndEncrypt,certificate.der,privatekey.pem") 74 | except Exception as ex: 75 | print("Unexpected error in setup security and certificates: ", ex) 76 | 77 | 78 | async def create_machine_subscription(client, nsIndex): 79 | # get nodes 80 | conditionType = client.get_node("ns=0;i=2782") 81 | tcMachineAlarmType = client.get_node(f"ns={nsIndex};i=1006") 82 | subNode = client.get_node(f"ns={nsIndex};s=179") 83 | 84 | # subscribe 85 | sub = await client.create_subscription(1, SubscriptionCallback()) 86 | unsubHandle = await sub.subscribe_alarms_and_conditions(subNode, tcMachineAlarmType) 87 | 88 | # Call ConditionRefresh to get the current conditions with retain = true 89 | try: 90 | await conditionType.call_method("0:ConditionRefresh", ua.Variant(sub.subscription_id, ua.VariantType.UInt32)) 91 | except: 92 | pass 93 | 94 | 95 | async def main(): 96 | try: 97 | # Use commented out namespace and url for usage with python machine demo server 98 | # machineNamespace ="http://trumpf.com/TRUMPF-Interfaces/" 99 | # serverUrl = "opc.tcp://localhost:50000" 100 | machineNamespace = "urn:X0REPLACE0X:TRUMPF:UAInterfaces/http://trumpf.com/TRUMPF-Interfaces/" 101 | serverUrl = "opc.tcp://myServer:11878" 102 | 103 | # set runtime dir to directory of script file 104 | os.chdir(sys.path[0]) 105 | 106 | # create the opc ua client 107 | client = Client(serverUrl, 10) 108 | await setup_security_and_certificates(client, "exampleApplication") 109 | await client.connect() 110 | 111 | nsIndex = await client.get_namespace_index(machineNamespace) 112 | await create_machine_subscription(client, nsIndex) 113 | 114 | # Output of alarms is in event_notification method in SubscriptionCallback class 115 | # Wait for some time and watch output 116 | await asyncio.sleep(100) 117 | # Application ends 118 | 119 | except Exception as ex: 120 | print(ex) 121 | raise 122 | 123 | 124 | if __name__ == "__main__": 125 | # execute if run as a script 126 | asyncio.run(main()) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 TRUMPF Werkzeugmaschinen GmbH + Co. KG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MachineDemoServer/Python/PythonMachineDemoServer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # MIT License 4 | 5 | # Copyright (c) 2021 TRUMPF Werkzeugmaschinen GmbH + Co. KG 6 | 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | import asyncio 26 | import json 27 | import importlib 28 | import datetime 29 | import os, sys 30 | from asyncua.common import ua_utils 31 | from dateutil.parser import isoparse 32 | from datetime import timedelta, datetime 33 | from asyncua import ua, Server, Node, uamethod 34 | from xml.dom import minidom 35 | from enum import Enum 36 | 37 | _nodeIdToTypeInfoDict = {} 38 | _attributeNameToTypeInfoDict = {} 39 | _complexTypeNameMapping = {"local":"Local", "text":"Text", "id":"Identifier", 40 | "nodeId":"Identifier", "nsIdx":"NamespaceIndex"} 41 | 42 | class NodeTypeInfo: 43 | def __init__(self): 44 | self.isSimpleDataType = None 45 | self.isArray = None 46 | self.dataTypeName = None 47 | self.variantType = None 48 | 49 | 50 | def create_instance_of_complex_data_type_class(nodeTypeInfo, value): 51 | global _complexTypeNameMapping 52 | instance = None 53 | try: 54 | if nodeTypeInfo.dataTypeName in ["DateTime", "Time", "UtcTime", "TimeZoneDataType"]: # TIME 55 | instance = parse_time_string(value) 56 | else: 57 | ua_module = importlib.import_module("asyncua.ua") 58 | myType = getattr(ua_module, nodeTypeInfo.dataTypeName) 59 | if isinstance(myType, type(Enum)): # Custom ENUM 60 | instance = value 61 | else: # CLASS 62 | # Attention with NodeId types, 63 | # Per default TwoByteNodeIds are created without NamespaceIndex -> Maybe needs to be corrected 64 | instance = myType() 65 | for name,value in value.items(): 66 | if value is not None: 67 | if name in _complexTypeNameMapping: 68 | name = _complexTypeNameMapping[name] 69 | object.__setattr__(instance, name, value) # needed because of frozen dataclass 70 | except Exception as ex: 71 | print("ComplexDataTypeError:", nodeTypeInfo.dataTypeName, ex) 72 | return instance 73 | 74 | 75 | def get_complex_value_instance_object(nodeTypeInfo, value): 76 | if nodeTypeInfo.isArray: 77 | return [create_instance_of_complex_data_type_class(nodeTypeInfo,v) for v in value] 78 | else: 79 | return create_instance_of_complex_data_type_class(nodeTypeInfo, value) 80 | 81 | 82 | async def get_type_information(server, node): 83 | global _nodeIdToTypeInfoDict 84 | if node.nodeid in _nodeIdToTypeInfoDict: 85 | return _nodeIdToTypeInfoDict[node.nodeid] 86 | else: 87 | newTypeInfo = NodeTypeInfo() 88 | newTypeInfo.isArray = (await node.read_value_rank()) > 0 89 | newTypeInfo.variantType = await node.read_data_type_as_variant_type() 90 | nodeIdDT = await node.read_data_type() 91 | isNs0 = (nodeIdDT.NamespaceIndex == 0) 92 | id = nodeIdDT.Identifier 93 | newTypeInfo.isSimpleDataType = isNs0 and ((id < 13) or (id > 25 and id < 30)) # ua.ObjectIds 94 | if newTypeInfo.isSimpleDataType: 95 | newTypeInfo.dataTypeName = ua.ObjectIdNames[id] 96 | else: 97 | dataTypeNode = server.get_node(nodeIdDT) 98 | newTypeInfo.dataTypeName = (await dataTypeNode.read_browse_name()).Name 99 | _nodeIdToTypeInfoDict[node.nodeid] = newTypeInfo 100 | return newTypeInfo 101 | 102 | 103 | async def create_attribute_name_to_type_info_dictionary(server, etype): 104 | allTypes = await ua_utils.get_node_supertypes(etype, includeitself=True, skipbase=False) 105 | fieldTypeInfos= {} 106 | allFields = [] 107 | for t in allTypes: 108 | allFields.extend(await t.get_properties()) 109 | allFields.extend(await t.get_variables()) 110 | for fieldNode in allFields: 111 | attributeName = (await fieldNode.read_browse_name()).Name 112 | fieldTypeInfos[attributeName] = await get_type_information(server, fieldNode) 113 | # Collect sub properties 114 | for subProp in await fieldNode.get_properties(): 115 | subPropName = (await subProp.read_browse_name()).Name 116 | newName = f"{attributeName}/{subPropName}" 117 | fieldTypeInfos[newName] = await get_type_information(server, subProp) 118 | return fieldTypeInfos 119 | 120 | 121 | async def create_machine_alarm(evgen, nsIdx, entry): 122 | global _attributeNameToTypeInfoDict 123 | try: 124 | event = evgen.event 125 | for attribute in entry["fieldValues"]: 126 | name = attribute["field"]["browseName"]["name"] 127 | # ConditionId is called NodeId in the event object 128 | if name == "ConditionId": 129 | name = "NodeId" 130 | value = attribute["value"] 131 | if value is not None: 132 | typeInfo = _attributeNameToTypeInfoDict[name] 133 | if typeInfo.isSimpleDataType: 134 | object.__setattr__(event, name, value) # needed because of frozen dataclass 135 | else: 136 | complexValue = get_complex_value_instance_object(typeInfo, value) 137 | object.__setattr__(event, name, complexValue) # needed because of frozen dataclass 138 | event.NodeId = ua.StringNodeId(event.NodeId.Identifier) # transform to StringNodeId 139 | event.EventType = ua.NodeId(event.EventType.Identifier, nsIdx) # set EventType correct Namespace 140 | return event.Time 141 | except Exception as ex: 142 | print("create_machine_alarm - Unexpected error:", ex) 143 | 144 | 145 | async def prepare_for_machine_alarms(server, nsIdx): 146 | global _attributeNameToTypeInfoDict 147 | machineAlarmType = server.get_node(f"ns={nsIdx};i=1006") 148 | # For ConditionId add NodeId property manually. Necessary till implemented in python asyncua library 149 | await machineAlarmType.add_property(2, 'NodeId', ua.Variant(VariantType=ua.VariantType.NodeId)) 150 | messagesNode = server.get_node(f"ns={nsIdx};s=179") 151 | _attributeNameToTypeInfoDict = await create_attribute_name_to_type_info_dictionary(server, machineAlarmType) 152 | evgen = await server.get_event_generator(machineAlarmType, messagesNode) 153 | return evgen 154 | 155 | 156 | async def init_all_variables_waiting_for_initial_data(server, topNode): 157 | statusWaitingInitialData = ua.DataValue(StatusCode_=ua.StatusCode(ua.StatusCodes.BadWaitingForInitialData)) 158 | nodeList = await ua_utils.get_node_children(topNode) 159 | for n in nodeList: 160 | nodeClass = await n.read_node_class() 161 | if nodeClass == ua.NodeClass.Variable: 162 | await n.write_value(statusWaitingInitialData) 163 | 164 | def parse_time_string(timestring): 165 | if (len(timestring) == 33): 166 | # Remove last millisecond digit, does not work on linux isoparse 167 | ts = timestring[:26] + timestring[27:] 168 | return isoparse(ts) 169 | else: 170 | return isoparse(timestring) 171 | 172 | @uamethod 173 | def condition_refresh(parent, sub_id): 174 | None 175 | 176 | @uamethod 177 | def condition_refresh2(parent, sub_id, mid): 178 | None 179 | 180 | async def main(): 181 | # set runtime dir to directory of script file 182 | os.chdir(sys.path[0]) 183 | 184 | # Read configuration XML Document 185 | doc = minidom.parse("ReplayConfiguration.xml") 186 | playSpeedFactor = int(doc.getElementsByTagName("execution")[0].getAttribute("playSpeedFactor")) 187 | sourceFileName = doc.getElementsByTagName("execution")[0].getAttribute("sourceFileName") 188 | endpoint = doc.getElementsByTagName("endpoint")[0].getAttribute("url") 189 | 190 | server = Server() 191 | await server.init() 192 | await server.load_certificate("server-certificate.der") 193 | await server.load_private_key("server-privatekey.pem") 194 | server._application_uri = "urn:ServerHost:TRUMPF:MachineDemoServer" 195 | server.product_uri = "urn:Demo:TRUMPF:MachineDemoServer" 196 | server.set_endpoint(endpoint) 197 | server.set_server_name("TRUMPF Python Demo Server") 198 | server.set_security_policy([ 199 | ua.SecurityPolicyType.NoSecurity, 200 | ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt]) 201 | 202 | # mock condition refresh methods 203 | isession = server.iserver.isession 204 | condition_refresh_method = Node(isession, ua.NodeId(ua.ObjectIds.ConditionType_ConditionRefresh)) 205 | isession.add_method_callback(condition_refresh_method.nodeid, condition_refresh) 206 | condition_refresh2_method = Node(isession, ua.NodeId(ua.ObjectIds.ConditionType_ConditionRefresh2)) 207 | isession.add_method_callback(condition_refresh2_method.nodeid, condition_refresh2) 208 | 209 | await server.import_xml("MachineNodeTree.xml") 210 | await server.load_data_type_definitions() 211 | idx = await server.get_namespace_index("http://trumpf.com/TRUMPF-Interfaces/") 212 | 213 | # Prepare tree 214 | machineNode = server.get_node(f"ns={idx};s=1") 215 | await init_all_variables_waiting_for_initial_data(server, machineNode) 216 | evgen = await prepare_for_machine_alarms(server, idx) 217 | 218 | # Load record json 219 | with open(sourceFileName) as f: 220 | hdaJson = json.load(f) 221 | 222 | async with server: 223 | await asyncio.sleep(2) 224 | while True: 225 | counter = 0 226 | previousTimestamp = None 227 | currentTimestamp = None 228 | 229 | for entry in hdaJson: 230 | try: 231 | counter = counter + 1 232 | nodeId = entry["nodeId"]["id"] 233 | print(f"--------------------------\nNodeId={nodeId}, Counter={counter}") 234 | isAlarm = (nodeId == "179") 235 | timeDiff = timedelta(0) 236 | if isAlarm: 237 | currentTimestamp = await create_machine_alarm(evgen, idx, entry) 238 | else: 239 | currentTimestamp = parse_time_string(entry["value"]["serverTimestamp"]) 240 | if previousTimestamp and currentTimestamp: 241 | timeDiff = currentTimestamp - previousTimestamp 242 | previousTimestamp = currentTimestamp 243 | waitingTime = timeDiff.total_seconds() / playSpeedFactor 244 | print(f"Sleep={waitingTime}s, isAlarm={isAlarm}") 245 | await asyncio.sleep(waitingTime) 246 | if isAlarm: 247 | await evgen.trigger() # a new time is set automatically 248 | else: 249 | node = server.get_node(f"ns={idx};s={nodeId}") 250 | value = entry["value"]["value"] 251 | isEmptyList = (type(value) is list) and (len(value) == 0) 252 | if value is not None and not isEmptyList: 253 | nodeTypeInfo = await get_type_information(server, node) 254 | utcnow = datetime.utcnow() 255 | myValue = value 256 | if not nodeTypeInfo.isSimpleDataType: 257 | myValue = get_complex_value_instance_object(nodeTypeInfo, value) 258 | # DataValue as workaround to set ServerTimestamp, can be removed when implemented in library 259 | valueAsVariant = ua.Variant(myValue, VariantType=nodeTypeInfo.variantType) 260 | datavalue = ua.DataValue(valueAsVariant, SourceTimestamp=utcnow, ServerTimestamp=utcnow) 261 | # VariantType needed because of new type check in write_value 262 | await node.write_value(datavalue, varianttype=nodeTypeInfo.variantType) 263 | except Exception as ex: 264 | print("Unexpected error:", nodeId, ex) 265 | await asyncio.sleep(2) # And redo record file 266 | 267 | if __name__ == "__main__": 268 | asyncio.run(main()) -------------------------------------------------------------------------------- /MachineDemoServer/Python/README.md: -------------------------------------------------------------------------------- 1 | ## Python Machine Demo Server 2 | 3 | ### Introduction 4 | This is a **demo server** which is intended to provide a first impression regarding the address space, data types, some exemplary signals and events of OPC UA signals on TRUMPF machines. To achieve this the demo server is capable of replaying a set of prerecorded signals in a loop. 5 | The demo server is not exactly the same as the OPC UA server on the machines, but it is pretty similar in many regards. 6 | 7 | Be aware that some signals of the example set might not be available on certain machine types or are only available on newer machine generations. Also signals can be part of different OPC UA interface packages, i.e. licensable machine options, which will influence availability on concrete machine instances in the field. 8 | 9 | Do not rely on details of the demo server! 10 | 11 | Everything is provided as open source to provide examples on how to work and program with OPC UA. For the open source tools and examples there is no official TRUMPF support. Feel free to participate. 12 | 13 | 14 | ### Specialities of the demo server 15 | The demo server provides two endpoints, one without security policy and one with security policy type *Basic256Sha256*. In contrast, the TRUMPF OPC UA proxy only provides the secure *Basic256Sha256* endpoint. 16 | 17 | Furthermore the demo server just accepts any client certificate and does not perform any checks regarding the content or structure of the certificate. The TRUMPF OPC UA proxy is much more restrictive in those regards. 18 | 19 | Alarms and Conditions can only be subscribed on the "Messages" node. The ConditionRefresh Method for Alarms and Conditions is not supported by the demo server. (How to view Alarms and Conditions see the UaExpert example at the bottom of the page) 20 | 21 | 22 | 23 | ### Requirements 24 | - Python >= Python 3.7 25 | - Version >= Python 3.11 is recommended due to startup performance improvements. 26 | - Python opcua-asyncio library >= 0.9.95 27 | 28 | Tested with 0.9.95. If it will not run, use exactly that version and create an issue with error description and used library version. 29 | 30 | ### Installation and Execution 31 | With Python installed, the script file can be exuted with `python PythonMachineDemoServer.py`. As an alternative a fully self contained .exe can be created with PyInstaller. 32 | 33 | ##### Detailed instruction: 34 | - [Download and install Python](https://www.python.org/downloads/) >= Python 3.7. On installation set checkbox for adding to system path. 35 | - Install opcua-asnycio library >= 0.9.95 with 36 | `pip3 install asyncua` or upgrade with `pip3 install --upgrade asyncua` 37 | Behind a company proxy you may need to add the proxy server and trust servers. Search for proxy settings and look for the manual proxy server. 38 | `pip3 install --trusted-host pypi.org --trusted-host files.pythonhosted.org --proxy=http://username:password@proxyserver:port asyncua` 39 | 40 | - Download all files and copy them to a folder. Easiest way is to download all files of the github repository. [Download zip](https://github.com/TRUMPF-IoT/OpcUaMachineTools/archive/main.zip). 41 | - Enter the folder containing PythonMachineDemoServer.py and execute the server with `python PythonMachineDemoServer.py` 42 | - On Linux if Python 2.x and Python 3.x are installed execute with `python3 PythonMachineDemoServer.py`. 43 | 44 | To create a self contained .exe on Windows which can be used on systems without Python installed: 45 | - Install PyInstaller with: `pip3 install pyinstaller` 46 | - Comment out line `os.chdir(sys.path[0])` in PythonMachineDemoServer.py. That can cause issues. 47 | - Switch to folder and execute `pyinstaller.exe --onefile PythonMachineDemoServer.py`. The result is in the "dist" directory. 48 | 49 | ### Configuration 50 | Basic configuration is done via **ReplayConfiguration.xml**. The following configurations can be done: 51 | - "url" defines the endpoint URL where the clients connect to. 52 | - "sourceFileName" defines the file with prerecorded signals. 53 | - "playSpeedFactor" defines the time multiplier in replaying the signals. 54 | 55 | ### OPC UA Client 56 | A generic OPC UA client can be used to explore and browse the OPC UA server. A recommended free client is "UaExpert" from UnifiedAutomation. [Download link.](https://www.unified-automation.com/downloads/opc-ua-clients.html) 57 | 58 | #### How to view Alarms and conditions 59 | After starting UaExpert and connecting to the server, open the **Event View** with "Document -> Add -> Document Type: Event View". 60 | 61 | Drag the "Messages" node to the Configuration section in the Event View. Now you can observe all incoming Events in the Events-Tab and all pending Alarms/Warnings in the Alarms-Tab. 62 | 63 | 64 | ### License 65 | The Python Machine Demo Server is licensed under the MIT License. 66 | -------------------------------------------------------------------------------- /MachineDemoServer/Python/ReplayConfiguration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /MachineDemoServer/Python/server-certificate.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRUMPF-IoT/OpcUaMachineTools/bc2a03b52795215bb4fe4cb566bdc2917d776a99/MachineDemoServer/Python/server-certificate.der -------------------------------------------------------------------------------- /MachineDemoServer/Python/server-privatekey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA133zBPULhdzLJd3tD5onZouT+HquL0LC9AC9vEyq8OZSnNoE 3 | 3mtrzcj5knasEyAx72ivb+QWPBKgjLqrna4ieGAFubcTIqgO91zPOMnVOR8L2mgm 4 | xCULFRgyoPP2gbZIkkbatcxM3XH/e+f5Wbsf6dycjYe1U7YEUQfCYAL1nxSefQOl 5 | cKzUXJi3E1Uy4Ba2cKpOnbO2m3idTi4EhuI2nOBVZ4UqoarV6zhuhb2KNrlX2psg 6 | pF0q1MiSzew3GrAMqLyPYF+JKr/JueNszuPI5uUQraUXup1QXj5/IIc+jJecX58W 7 | RHg07v7LVd/CTV3pVdlSOs40xQVsM6Ku5ycE5QIDAQABAoIBAGp4y6NOZAQfBKzQ 8 | CzpjQ0ZyfokOLJQjW2nuF9E63FcspfLj8fXng5toyo8oXXsRtDqDMfOJ8cZ6uaLu 9 | 9K5zBIsPfqS1JRpBiSuFSsnXR6fyhAvE3Cqb1u70Rsep4slSRGcp5RRgPjZIBiC1 10 | jEleoLUPELcJL3mN/HIA8HQ54hxXzp/HGvDH1Wj7hH+RDpbqo6nUJj8Gqdd/nEU0 11 | SNQvsFSh2hbS10KSVvLB73pakOwHnwbUaFiYfatvyu49u/V5CvvPoL18rOg6Eyvk 12 | p96bQCXdBsNd2F8jOUFn1S0OWwB7wvlU5RRoRf8OQQ6r2sPhdFUTpi79Q4kZ/G8r 13 | /mVu8T0CgYEA7J85wlOhOylPYxSP9C9Ln35xQMiHDdQ6x3+QQugm5+Jv4EpNDRQ5 14 | YENqRjCNwbsPd+FVXvSzJC38Gdym65GvzYI90C44cLZz/R0Nzf5RQi5Kp/ke+/GC 15 | ouCs95H0wfTasv/3G961mFuO2EKZjMU5d2NAAbXlJJEnWJRXGriUO8MCgYEA6SO7 16 | 52Y7Ux3CYwwceBhn5eL1RsLxuNmCKNgLJkwusIuKS8rvecNpbld/lk8dVijSUiWQ 17 | zj2diFkSjBMMFis8wS3r+ljC/wWZJawqUV3jjCzdkoQmo6n1UcDzE1FaVLFtDQEy 18 | ptiK8XVqru1h0iQrv5pSc8AnSUe1BGj8hgbsOjcCgYEAiBr6fUXfiwkzCdntB9cg 19 | l6iCenIeBR1bhh9hGqswnddI5Om4Mlq8uhttCNyq3ZG8zwcFNS5p0NjGlxWtyfit 20 | 9/b/mTzM6EB6rVBF/YRYt0mrVb1dTixYKVo1A96nu90c4zOKrzRGnlGj888zRZ64 21 | dGzZh2JaYNNBn5kMFWmwkkMCgYEAxt4rZ5yF2EVXTiPDmRHAdpEdhim+BV7ML0jy 22 | Yc20OfYdlr9ZfTUaFvxeIfoEXT1fAqF2nuZiHS9VqdSJh9OD9IjWdOsIEn9U/pSY 23 | WGXNNwICUvuU9iCA2SbMcdsAQaRDEEfITBgElSkCQorM2XLvOnZKBOCQ4mpfV46y 24 | cSUwlgsCgYAzTmyo9BZhyN/PRJbaz0MHozFdej/UD0Qguh7m5+2Q+WXEbwDQ9cg+ 25 | 9hwJ7plNVg5rx+ObmERe+jhZI5DaQCozy/dB01SoeqRBLrBYVON+mNsEP8QjuIx1 26 | /PO+BTS687vv/hBFycrFEaDl2ordkYsyHGyUV1RxeC3mPgM8CNo9+g== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpcUaMachineTools 2 | Examples, small programs and how-tos regarding OPC UA on machine tools. 3 | 4 | ### Server examples 5 | * [Python machine demo server](/MachineDemoServer/Python) 6 | * [Python alarms and conditions to data items converter](/AlarmConverter/Python) 7 | 8 | ### Client examples 9 | * [Python examples](/Examples/Python) 10 | * [NetCore examples](/Examples/NetCore) 11 | * [NodeRed examples](/Examples/NodeRed) 12 | 13 | ### How-tos 14 | * [How to connect to different machines via the OPC UA Gateway](/Doc/opcuagateway.md) 15 | * [How to determine the connection state of a machine aggregated in the OPC UA gateway](/Doc/connectionstate.md) 16 | * [How to add orders to the production plan of the machine](/Doc/productionplan.md) 17 | 18 | ### Guidelines and best practices 19 | * [General guidelines and remarks](/Doc/guidelines.md) 20 | * [Recommendations for usage of OPC UA clients](/Doc/opcuaclients.md) 21 | * [Signal recommendations for different use cases](/Doc/signalsusecases.md) 22 | --------------------------------------------------------------------------------