├── 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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------