├── .gitignore ├── README.md ├── device.py ├── device.xml ├── driver_ModbusRTU.py ├── helpers.py ├── server-minimal.py └── ua_object.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | .idea/workspace.xml 91 | .idea/modules.xml 92 | .idea/encodings.xml 93 | .idea/python-opcua-modbus-driver.iml 94 | .idea/misc.xml 95 | .idea/vcs.xml 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-opcua-modbus-driver 2 | Python OPC UA Modbus Driver 3 | 4 | Elementary implementaion which bridges Modbus RTU data to OPC UA server. 5 | 6 | The basic principle is this: 7 | 1. Start `server_minimal.py` 8 | 2. This will import the `driver_ModbusRTU.py` which will kick off a thread which scans a modbus network 9 | 3. The server will import an XML file which has an Object Type for a hardware device, the example has one device (MyDevice) 10 | 4. The server will instantiate a python `Device` object which has an `update` method for getting data from Modbus 11 | 5. The `Device` object subclasses `UaObject` which will auto subscribe to all children and keep the python class in sync with the OPC UA object 12 | 6. MyDevice links to the modbus driver via parameters from OPC UA, so it can be configued from an HMI or UA client 13 | 14 | This example is not tested and has never been run. It is a generic starting point created from a working implementation. 15 | 16 | Dependencies: 17 | python-opcua 18 | modbus_tk 19 | -------------------------------------------------------------------------------- /device.py: -------------------------------------------------------------------------------- 1 | import random 2 | import driver_ModbusRTU 3 | 4 | from ua_object import UaObject 5 | from opcua import ua 6 | from helpers import scale_value 7 | 8 | 9 | class Device(UaObject): 10 | """ 11 | Definition of OPC UA object which represents a physical device 12 | This class mirrors it's UA counterpart and semi-configures itself according to the UA model (from XML) 13 | """ 14 | def __init__(self, opcua_server, ua_node): 15 | super().__init__(opcua_server, ua_node) 16 | 17 | self.RawValue = 0 18 | self.Value = 0.0 19 | self.Mode = 0 20 | self.DataSource = "" 21 | self.DataAddress = "" 22 | self.SimLowLimit = 0.0 23 | self.SimHighLimit = 0.0 24 | self.Location = "" 25 | self.Description = "" 26 | self.Units = "" 27 | self.ScalingEnable = False 28 | self.RawMin = 0.0 29 | self.RawMax = 0.0 30 | self.ScaledMin = 0.0 31 | self.ScaledMax = 0.0 32 | self.Divisor = 0.0 33 | self.User1 = 0.0 34 | self.User2 = 0.0 35 | self.User3 = 0.0 36 | self.User4 = 0.0 37 | 38 | def update(self): 39 | # Mode=0 is disabled/readonly 40 | # Mode=1 is normal: get data from configured external data source such as Modbus RTU, etc. 41 | # Mode=2 is simulation: the tag.Value will be set by the OPC UA server based on the configured limits 42 | if self.Mode == 0: # use this mode as "read only" for now; may need to add a separate mode later 43 | pass 44 | elif self.Mode == 1: 45 | if self.DataSource not in (None, '') and self.DataAddress not in (None, ''): 46 | if self.DataSource == "ModbusRTU": 47 | _data_address = self.DataAddress.split(':') 48 | _slave = int(_data_address[0]) - 1 # slave is offset in the list by 1 (slave 1 is position 0) 49 | _address = int(_data_address[1]) 50 | 51 | # handle raw value 52 | _address_value = driver_ModbusRTU.slave_list[_slave].Data[_address] / self.Divisor 53 | _dv_raw = ua.DataValue(ua.Variant(_address_value, ua.VariantType.Double)) 54 | 55 | # handle scaled value 56 | if self.ScalingEnable: 57 | _scaled_value = scale_value(_address_value, self.RawMin, self.RawMax, self.ScaledMin, self.ScaledMax) 58 | _dv_val = ua.DataValue(ua.Variant(_scaled_value, ua.VariantType.Double)) 59 | else: 60 | _dv_val = _dv_raw 61 | 62 | elif self.DataSource == "OPCUAClient": 63 | pass # fill in with client to external opc ua server later 64 | 65 | else: 66 | _dv_raw = ua.DataValue(ua.Variant(0.0, ua.VariantType.Double)) 67 | _dv_val = ua.DataValue(ua.Variant(0.0, ua.VariantType.Double)) 68 | 69 | # value gets set from configured data source here 70 | self.nodes['RawValue'].set_data_value(_dv_raw) 71 | self.nodes['Value'].set_data_value(_dv_val) 72 | else: 73 | print("Device {} is on scan has but has invalid configuration".format(self.b_name)) 74 | elif self.Mode == 2: 75 | self.nodes['Value'].set_value(random.uniform(self.SimLowLimit, self.SimHighLimit)) 76 | -------------------------------------------------------------------------------- /device.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | i=40 5 | i=45 6 | i=46 7 | i=47 8 | i=35 9 | i=37 10 | i=9 11 | i=11 12 | i=12 13 | i=15 14 | i=1 15 | 16 | 17 | http://myserver/OPC/ 18 | 19 | 20 | TypeDictionary 21 | Collects the data type descriptions of http://buhlergroup.com/SVS/ 22 | 23 | i=93 24 | i=72 25 | ns=1;i=6696 26 | 27 | 28 | 29 | NamespaceUri 30 | 31 | http://buhlergroup.com/SVS/ 32 | 33 | 34 | ns=1;i=6695 35 | i=68 36 | 37 | 38 | 39 | TypeDictionary 40 | Collects the data type descriptions of http://buhlergroup.com/SVS/ 41 | 42 | i=92 43 | i=72 44 | ns=1;i=6698 45 | 46 | 47 | 48 | NamespaceUri 49 | 50 | http://buhlergroup.com/SVS/Types.xsd 51 | 52 | 53 | ns=1;i=6697 54 | i=68 55 | 56 | 57 | 58 | ProfileType 59 | 60 | i=63 61 | ns=1;i=6702 62 | ns=1;i=6703 63 | ns=1;i=6701 64 | 65 | 66 | 67 | LogKill 68 | 69 | ns=1;i=2001 70 | i=63 71 | i=78 72 | 73 | 74 | 75 | NutTemperature 76 | 77 | ns=1;i=2001 78 | i=63 79 | i=78 80 | 81 | 82 | 83 | Position 84 | 85 | ns=1;i=2001 86 | i=63 87 | i=78 88 | 89 | 90 | 91 | Device 92 | 93 | i=58 94 | ns=1;i=6006 95 | ns=1;i=6007 96 | ns=1;i=6008 97 | ns=1;i=6009 98 | ns=1;i=6180 99 | ns=1;i=6010 100 | ns=1;i=6011 101 | ns=1;i=6013 102 | ns=1;i=6012 103 | ns=1;i=6584 104 | ns=1;i=6015 105 | ns=1;i=6014 106 | ns=1;i=6016 107 | ns=1;i=6017 108 | ns=1;i=6018 109 | ns=1;i=6019 110 | ns=1;i=6020 111 | ns=1;i=6021 112 | ns=1;i=6022 113 | ns=1;i=6546 114 | 115 | 116 | 117 | DataAddress 118 | 119 | 0:0 120 | 121 | 122 | ns=1;i=1004 123 | i=68 124 | i=78 125 | 126 | 127 | 128 | DataSource 129 | 130 | ModbusRTU 131 | 132 | 133 | ns=1;i=1004 134 | i=68 135 | i=78 136 | 137 | 138 | 139 | Description 140 | 141 | Device description 142 | 143 | 144 | ns=1;i=1004 145 | i=68 146 | i=78 147 | 148 | 149 | 150 | Divisor 151 | 152 | 1.0 153 | 154 | 155 | ns=1;i=1004 156 | i=68 157 | i=78 158 | 159 | 160 | 161 | ScalingEnable 162 | 163 | False 164 | 165 | 166 | ns=1;i=1004 167 | i=68 168 | i=78 169 | 170 | 171 | 172 | Location 173 | 174 | Zone0 175 | 176 | 177 | ns=1;i=1004 178 | i=68 179 | i=78 180 | 181 | 182 | 183 | Mode 184 | 185 | 2 186 | 187 | 188 | ns=1;i=1004 189 | i=68 190 | i=78 191 | 192 | 193 | 194 | RawMax 195 | 196 | 463.5 197 | 198 | 199 | ns=1;i=1004 200 | i=68 201 | i=78 202 | 203 | 204 | 205 | RawMin 206 | 207 | 0.0 208 | 209 | 210 | ns=1;i=1004 211 | i=68 212 | i=78 213 | 214 | 215 | 216 | RawValue 217 | 218 | 0.0 219 | 220 | 221 | ns=1;i=1004 222 | i=63 223 | i=78 224 | 225 | 226 | 227 | ScaledMax 228 | 229 | 463.5 230 | 231 | 232 | ns=1;i=1004 233 | i=68 234 | i=78 235 | 236 | 237 | 238 | ScaledMin 239 | 240 | 0.0 241 | 242 | 243 | ns=1;i=1004 244 | i=68 245 | i=78 246 | 247 | 248 | 249 | SimHighLimit 250 | 251 | 0.0 252 | 253 | 254 | ns=1;i=1004 255 | i=68 256 | i=78 257 | 258 | 259 | 260 | SimLowLimit 261 | 262 | 0.0 263 | 264 | 265 | ns=1;i=1004 266 | i=68 267 | i=78 268 | 269 | 270 | 271 | Units 272 | 273 | C 274 | 275 | 276 | ns=1;i=1004 277 | i=68 278 | i=78 279 | 280 | 281 | 282 | User1 283 | 284 | 0.0 285 | 286 | 287 | ns=1;i=1004 288 | i=68 289 | i=78 290 | 291 | 292 | 293 | User2 294 | 295 | 0.0 296 | 297 | 298 | ns=1;i=1004 299 | i=68 300 | i=78 301 | 302 | 303 | 304 | User3 305 | 306 | 0.0 307 | 308 | 309 | ns=1;i=1004 310 | i=68 311 | i=78 312 | 313 | 314 | 315 | User4 316 | 317 | 0.0 318 | 319 | 320 | ns=1;i=1004 321 | i=68 322 | i=78 323 | 324 | 325 | 326 | Value 327 | 328 | 0.0 329 | 330 | 331 | ns=1;i=1004 332 | i=63 333 | i=78 334 | 335 | 336 | 337 | Device 338 | 339 | i=85 340 | ns=1;i=1004 341 | i=20003 342 | i=20004 343 | i=20005 344 | i=20006 345 | i=20007 346 | i=20008 347 | i=20009 348 | i=20010 349 | i=20011 350 | i=20012 351 | i=20013 352 | i=20014 353 | i=20015 354 | i=20016 355 | i=20017 356 | i=20018 357 | i=20019 358 | i=20020 359 | i=20021 360 | i=20022 361 | 362 | 363 | 364 | DataAddress 365 | 366 | 0:0 367 | 368 | 369 | i=20002 370 | i=68 371 | 372 | 373 | 374 | DataSource 375 | 376 | ModbusRTU 377 | 378 | 379 | i=20002 380 | i=68 381 | 382 | 383 | 384 | Description 385 | 386 | Device description 387 | 388 | 389 | i=20002 390 | i=68 391 | 392 | 393 | 394 | Divisor 395 | 396 | 1.0 397 | 398 | 399 | i=20002 400 | i=68 401 | 402 | 403 | 404 | ScalingEnable 405 | 406 | False 407 | 408 | 409 | i=20002 410 | i=68 411 | 412 | 413 | 414 | Location 415 | 416 | Zone0 417 | 418 | 419 | i=20002 420 | i=68 421 | 422 | 423 | 424 | Mode 425 | 426 | 2 427 | 428 | 429 | i=20002 430 | i=68 431 | 432 | 433 | 434 | RawMax 435 | 436 | 463.5 437 | 438 | 439 | i=20002 440 | i=68 441 | 442 | 443 | 444 | RawMin 445 | 446 | 0.0 447 | 448 | 449 | i=20002 450 | i=68 451 | 452 | 453 | 454 | RawValue 455 | 456 | 0.0 457 | 458 | 459 | i=20002 460 | i=63 461 | 462 | 463 | 464 | ScaledMax 465 | 466 | 463.5 467 | 468 | 469 | i=20002 470 | i=68 471 | 472 | 473 | 474 | ScaledMin 475 | 476 | 0.0 477 | 478 | 479 | i=20002 480 | i=68 481 | 482 | 483 | 484 | SimHighLimit 485 | 486 | 0.0 487 | 488 | 489 | i=20002 490 | i=68 491 | 492 | 493 | 494 | SimLowLimit 495 | 496 | 0.0 497 | 498 | 499 | i=20002 500 | i=68 501 | 502 | 503 | 504 | Units 505 | 506 | C 507 | 508 | 509 | i=20002 510 | i=68 511 | 512 | 513 | 514 | User1 515 | 516 | 0.0 517 | 518 | 519 | i=20002 520 | i=68 521 | 522 | 523 | 524 | User2 525 | 526 | 0.0 527 | 528 | 529 | i=20002 530 | i=68 531 | 532 | 533 | 534 | User3 535 | 536 | 0.0 537 | 538 | 539 | i=20002 540 | i=68 541 | 542 | 543 | 544 | User4 545 | 546 | 0.0 547 | 548 | 549 | i=20002 550 | i=68 551 | 552 | 553 | 554 | Value 555 | 556 | 0.0 557 | 558 | 559 | i=20002 560 | i=63 561 | 562 | 563 | 564 | -------------------------------------------------------------------------------- /driver_ModbusRTU.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf_8 -*- 3 | """ 4 | Modbus TestKit: Implementation of Modbus protocol in python 5 | 6 | (C)2009 - Luc Jean - luc.jean@gmail.com 7 | (C)2009 - Apidev - http://www.apidev.fr 8 | 9 | This is distributed under GNU LGPL license, see license.txt 10 | """ 11 | 12 | import serial 13 | import time 14 | import modbus_tk 15 | import modbus_tk.defines as function 16 | from modbus_tk import modbus_rtu 17 | import threading 18 | 19 | from serial import serialutil 20 | 21 | # Constants 22 | TIMEOUT = 1 23 | REQUEST_DELAY = 0.05 24 | 25 | # Create logger object 26 | logger = modbus_tk.utils.create_logger("console") 27 | 28 | poll_enable = True 29 | 30 | 31 | class Slave(object): 32 | """ 33 | Slave object of IO node with relevant data 34 | """ 35 | 36 | def __init__(self, slave_address, starting_address, read_length, divider, enable): 37 | self.SlaveAddress = slave_address # Modbus slave address number 38 | self.StartingAddress = starting_address # Modbus starting address 39 | self.ReadLength = read_length # Modbus number of registers to read 40 | self.PollingError = True # Initialize this slave with an error as no poll attempt has been made yet 41 | self.Divider = divider # describes decimal position because slaves only send ints 42 | self.Enable = enable 43 | self.Data = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 44 | 45 | 46 | def poll(slave): 47 | """ 48 | Poll an individual slave for required data 49 | """ 50 | 51 | try: 52 | # inter-message delay time 53 | time.sleep(REQUEST_DELAY) 54 | 55 | try: 56 | # logger.info(master.execute(slave.SlaveAddress, function.READ_HOLDING_REGISTERS, 0, slave.ReadLength)) 57 | response_data = master.execute(slave.SlaveAddress, 58 | function.READ_HOLDING_REGISTERS, 59 | slave.StartingAddress, 60 | slave.ReadLength) 61 | 62 | except modbus_tk.exceptions.ModbusInvalidResponseError as R_exc: 63 | logger.error("%s- Code=%d Slave=%d", R_exc, 0, slave.SlaveAddress) 64 | slave.PollingError = True # flag this slave as having an unsuccessful read 65 | 66 | # print("Invalid response from slave") 67 | 68 | else: 69 | # HANDLE MODBUS RESPONSE HERE 70 | # POPULATE SLAVE OBJECT WITH DATA 71 | slave.PollingError = False # reset polling error flag on success 72 | slave.Data = response_data # copy the modbus data (tuple) to the slave object 73 | # print("Slave: ", slave.SlaveAddress, ", Data: ", slave.Data) 74 | 75 | except modbus_tk.modbus.ModbusError as M_exc: 76 | logger.error("%s- Code=%d", M_exc, M_exc.get_exception_code()) 77 | slave.PollingError = True # flag this slave as having an unsuccessful read 78 | 79 | 80 | # polling for all the individual slaves on the network wrapped into a single function for use in a separate thread 81 | def poll_all(slaves): 82 | """ 83 | Poll all slaves on the network forever - meant to be called in a thread 84 | """ 85 | 86 | while poll_enable: 87 | # start_time = time.time() 88 | for slave in slaves: 89 | if slave.Enable is True: 90 | poll(slave) 91 | 92 | # end_time = time.time() 93 | # update_time = (end_time-start_time)*1000 94 | # logger.info("Network update time: %d", update_time) 95 | 96 | 97 | # Instantiate slave objects with configuration data in a list 98 | # SlaveObject(Slave Address, Starting Data Address, Read Length, Divider, Enable) 99 | slave_list = [Slave(1, 0, 5, 10, True), 100 | Slave(2, 0, 10, 10, True), 101 | Slave(3, 0, 10, 10, True), 102 | Slave(4, 0, 6, 100, True)] 103 | 104 | 105 | serial_error = False 106 | 107 | # Initialize connection when module is called 108 | try: 109 | # Connect to the slave (note that the port is 1 less than shown in windows) 110 | master = modbus_rtu.RtuMaster(serial.Serial(port='COM4', 111 | baudrate=115200, 112 | bytesize=8, 113 | parity='N', 114 | stopbits=2, 115 | xonxoff=0)) 116 | master.set_timeout(TIMEOUT) # serial timeout in seconds 117 | master.set_verbose(False) # logger detail level 118 | logger.info("RS485 Connected") 119 | 120 | except serial.serialutil.SerialException as err: 121 | print("Serial Error:", err) 122 | serial_error = True 123 | 124 | except modbus_tk.modbus.ModbusError as exc: 125 | logger.error("%s- Code=%d", exc, exc.get_exception_code()) 126 | 127 | # Start the thread which communicates to the slaves if there were no serial exceptions raised 128 | if serial_error is False: 129 | 130 | t1 = threading.Thread(target=poll_all, args=[slave_list]) 131 | t1.setDaemon(True) 132 | t1.start() 133 | 134 | # Modbus driver testing 135 | if __name__ == "__main__": 136 | while True: 137 | time.sleep(1) 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class SubHandler(object): 4 | """ 5 | Subscription Handler. To receive events from server for a subscription. 6 | The handler forwards updates to it's referenced python object 7 | """ 8 | 9 | def __init__(self, obj): 10 | self.obj = obj 11 | 12 | def datachange_notification(self, node, val, data): 13 | # print("Python: New data change event", node, val, data) 14 | 15 | _node_name = node.get_browse_name() 16 | setattr(self.obj, _node_name.Name, data.monitored_item.Value.Value.Value) 17 | 18 | 19 | def find_o_types(node, type_to_find): 20 | """ 21 | Search a nodes children for nodes of a specific object type 22 | :param node: Parent node which will be searched 23 | :param type_to_find: Object type node to find 24 | :return: List of nodes that match the desired object type 25 | """ 26 | found_nodes = [] 27 | for child in node.get_children(): 28 | if child.get_type_definition() == type_to_find.nodeid: 29 | found_nodes.append(child) 30 | 31 | return found_nodes 32 | 33 | 34 | def node_search(parent, browse_name): 35 | """ 36 | Search a parent node's children for a specific browse name and return that node 37 | """ 38 | _objects_children = parent.get_children() 39 | for node in _objects_children: 40 | _child_name = node.get_browse_name() 41 | if _child_name.Name == browse_name: 42 | return node 43 | return None 44 | 45 | 46 | def scale_value(value, raw_min, raw_max, scaled_min, scaled_max): 47 | """ 48 | convert a raw value to one scaled in engineering units 49 | Args: 50 | value: unscaled value 51 | raw_min: unscaled value minimum 52 | raw_max: unscaled value maximum 53 | scaled_min: engineering minimum 54 | scaled_max: engineering maximum 55 | 56 | Returns: value scaled to engineering min and max 57 | 58 | """ 59 | return (value - raw_min) * (scaled_max - scaled_min / raw_max - raw_min) + scaled_min 60 | -------------------------------------------------------------------------------- /server-minimal.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.insert(0, "..") 3 | import time 4 | import driver_ModbusRTU # importing will start the modbus communications thread 5 | import threading 6 | 7 | from opcua import ua, Server, Node 8 | from helpers import find_o_types 9 | from device import Device 10 | 11 | 12 | class OPCUAServer(object): 13 | 14 | def __init__(self): 15 | self.server = None 16 | self.uri = "" 17 | self.idx = 0 18 | self.scan_thread = None 19 | self.scan_enable = False 20 | self.devices = [] 21 | 22 | # setup our server 23 | self.server = Server() 24 | self.server.set_endpoint("opc.tcp://0.0.0.0:4840/freeopcua/server/") 25 | 26 | # setup our own namespace, not really necessary but should as spec 27 | uri = "http://examples.freeopcua.github.io" 28 | idx = self.server.register_namespace(uri) 29 | 30 | # import address space which has a device object 31 | self.server.import_xml("device.xml") 32 | 33 | # get Objects node 34 | objects = self.server.get_objects_node() 35 | 36 | # get the device object type so we can find all the devices in the address space 37 | hw_type = self.server.nodes.base_object_type.get_child("2:Device") 38 | device_nodes = find_o_types(objects, hw_type) 39 | 40 | # instantiate a device python object and make it an attribute of the server class 41 | # FIXME do your own organization, most likely an object oriented model 42 | for device_node in device_nodes: 43 | device = Device(self.server, device_node) 44 | setattr(self, device.b_name, device) 45 | # keep track of the device because so that we can update it cyclically using the driver 46 | self.devices.append(device) 47 | 48 | def _scan(self): 49 | while self.scan_enable: 50 | # update all device data from devices' configured driver, such as modbus) 51 | for device in self.devices: 52 | device.update() 53 | 54 | time.sleep(1) 55 | 56 | def scan_on(self): 57 | # Start the thread which handles the cyclic updates of devices 58 | self.scan_thread = threading.Thread(target=self._scan) 59 | # self.scan_thread.setDaemon(True) 60 | self.scan_enable = True 61 | self.scan_thread.start() 62 | 63 | def scan_off(self): 64 | # Stop the thread which handles cyclic updates 65 | self.scan_enable = False 66 | self.scan_thread.join() 67 | 68 | 69 | if __name__ == "__main__": 70 | 71 | # Create a server 72 | my_server = OPCUAServer() 73 | 74 | # start scanning for cyclic updates 75 | my_server.scan_on() 76 | -------------------------------------------------------------------------------- /ua_object.py: -------------------------------------------------------------------------------- 1 | from helpers import SubHandler 2 | from opcua import ua 3 | 4 | 5 | class UaObject(object): 6 | """ 7 | Python object which mirrors an OPC UA object 8 | Child UA variables/properties are auto subscribed to to synchronize python with UA server 9 | Python can write to children via publish method, which will trigger an update for UA clients 10 | """ 11 | def __init__(self, opcua_server, ua_node): 12 | self.opcua_server = opcua_server 13 | self.nodes = {} 14 | self.b_name = ua_node.get_browse_name().Name 15 | 16 | # keep track of the children of this object (in case python needs to write, or get more info from UA server) 17 | for _child in ua_node.get_children(): 18 | _child_name = _child.get_browse_name() 19 | self.nodes[_child_name.Name] = _child 20 | 21 | # find all children which can be subscribed to (python object is kept up to date via subscription) 22 | sub_children = ua_node.get_properties() 23 | sub_children.extend(ua_node.get_variables()) 24 | 25 | # subscribe to properties/variables 26 | handler = SubHandler(self) 27 | sub = opcua_server.server.create_subscription(500, handler) 28 | handle = sub.subscribe_data_change(sub_children) 29 | 30 | def publish(self, attr=None): 31 | if attr is None: 32 | for k, node in self.nodes.items(): 33 | node_class = node.get_node_class() 34 | if node_class == ua.NodeClass.Variable: 35 | node.set_value(getattr(self, k)) 36 | else: 37 | self.nodes[attr].set_value(getattr(self, attr)) 38 | --------------------------------------------------------------------------------