Functions 01, 03, 05, 06 and test with pty.
authorBenjamin Braatz <benjamin.braatz@graph-it.com>
Wed, 7 Apr 2021 11:19:36 +0000 (13:19 +0200)
committerBenjamin Braatz <benjamin.braatz@graph-it.com>
Wed, 7 Apr 2021 11:19:36 +0000 (13:19 +0200)
conf.json
controlpi_plugins/modbus.py

index c3f5a18e57d3f826b3f1326877b1e3722622ccda..4bcfeaae7ce9822ab7d4ef15b9f4d7ffc195e5b4 100644 (file)
--- a/conf.json
+++ b/conf.json
@@ -7,27 +7,15 @@
                    "location": "Debug" }
         }
     },
-    "Modbus": {
+    "Test Master": {
+        "plugin": "ModbusMaster",
         "device": "/dev/ttyS0",
-        "test device": "/dev/ttyUSB0",
-        "baudrate": 115200,
-        "slave types": {
-            "Hitachi SJ-P1": {
-                "slaves": { "FU-1": 1 },
-                "coils": {
-                    "Op": {
-                        "address": 0,
-                        "name": "Operation command"
-                    }
-                },
-                "holding registers": {
-                    "Set frequency": {
-                        "address": 10501,
-                        "name": "RS485 Set frequency",
-                        "count": 2
-                    }
-                }
-            }
-        }
+        "parity": "none"
+    },
+    "Test Slave": {
+        "plugin": "ModbusSlave",
+        "device": "/dev/ttyUSB0",
+        "slave": 1,
+        "parity": "none"
     }
 }
index 3c996549055ec95592abeb36eb807e6b7c335381..dad43c780dd6d55f634e1e2ec44ac45f041ffaba 100644 (file)
@@ -81,32 +81,190 @@ for dict_key in range(256):
 class ModbusMaster(BasePlugin):
     """Modbus-RTU master plugin.
 
-    >>> import controlpi
-    >>> asyncio.run(controlpi.test(
-    ...     {"Test Master": {"plugin": "ModbusMaster",
-    ...                      "device": "/dev/pts/4",
-    ...                      "parity": "none"},
-    ...      "Test Slave": {"plugin": "ModbusSlave",
-    ...                     "device": "/dev/pts/5",
-    ...                     "slave": 1,
-    ...                     "parity": "none"}},
-    ...     [{"target": "Test Master", "command": "read coils",
-    ...       "slave": 1, "start": 1, "quantity": 2},
-    ...      {"target": "Test Master", "command": "write single coil",
-    ...       "slave": 1, "address": 1, "value": False},
-    ...      {"target": "Test Master", "command": "write single coil",
-    ...       "slave": 1, "address": 2, "value": True},
-    ...      {"target": "Test Master", "command": "read coils",
-    ...       "slave": 1, "start": 1, "quantity": 2},
-    ...      {"target": "Test Master", "command": "read holding registers",
-    ...       "slave": 1, "start": 7, "quantity": 2},
-    ...      {"target": "Test Master", "command": "write single register",
-    ...       "slave": 1, "address": 7, "value": 42},
-    ...      {"target": "Test Master", "command": "write single register",
-    ...       "slave": 1, "address": 8, "value": 42042},
-    ...      {"target": "Test Master", "command": "read holding registers",
-    ...       "slave": 1, "start": 7, "quantity": 2}], 0.5))
+    >>> import pty, os, controlpi
+    >>> master_controller, master = pty.openpty()
+    >>> slave_controller, slave = pty.openpty()
+    >>> def relay(source, target):
+    ...     content = os.read(source, 2147483647)
+    ...     os.write(target, content)
+    >>> async def test():
+    ...     loop = asyncio.get_event_loop()
+    ...     loop.add_reader(master_controller, relay,
+    ...                     master_controller, slave_controller)
+    ...     loop.add_reader(slave_controller, relay,
+    ...                     slave_controller, master_controller)
+    ...     await controlpi.test(
+    ...         {"Test Master": {"plugin": "ModbusMaster",
+    ...                          "device": os.ttyname(master),
+    ...                          "parity": "none"},
+    ...          "Test Slave": {"plugin": "ModbusSlave",
+    ...                         "device": os.ttyname(slave),
+    ...                         "slave": 1,
+    ...                         "parity": "none"}},
+    ...         [{"target": "Test Master", "command": "read coils",
+    ...           "slave": 1, "start": 1, "quantity": 2},
+    ...          {"target": "Test Master", "command": "write single coil",
+    ...           "slave": 1, "address": 1, "value": False},
+    ...          {"target": "Test Master", "command": "write single coil",
+    ...           "slave": 1, "address": 2, "value": True},
+    ...          {"target": "Test Master", "command": "read coils",
+    ...           "slave": 1, "start": 1, "quantity": 2},
+    ...          {"target": "Test Master", "command": "read holding registers",
+    ...           "slave": 1, "start": 7, "quantity": 2},
+    ...          {"target": "Test Master", "command": "write single register",
+    ...           "slave": 1, "address": 7, "value": 42},
+    ...          {"target": "Test Master", "command": "write single register",
+    ...           "slave": 1, "address": 8, "value": 42042},
+    ...          {"target": "Test Master", "command": "read holding registers",
+    ...           "slave": 1, "start": 7, "quantity": 2}], 0.1)
+    >>> asyncio.run(test())
     ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {'sender': '', 'event': 'registered',
+             'client': 'Test Master', 'plugin': 'ModbusMaster',
+             'sends': [{'event': {'const': 'error'}},
+                       {'event': {'const': 'response'},
+                        'function': {'const': 'read coils'},
+                        'slave': {'type': 'integer',
+                                  'minimum': 1, 'maximum': 247},
+                        'values': {'type': 'array',
+                                   'items': {'type': 'boolean'}}},
+                       {'event': {'const': 'response'},
+                        'function': {'const': 'read holding registers'},
+                        'slave': {'type': 'integer',
+                                  'minimum': 1, 'maximum': 247},
+                        'values': {'type': 'array',
+                                   'items': {'type': 'integer'}}},
+                       {'event': {'const': 'response'},
+                        'function': {'const': 'write single coil'},
+                        'slave': {'type': 'integer',
+                                  'minimum': 1, 'maximum': 247},
+                        'address': {'type': 'integer',
+                                    'minimum': 1, 'maximum': 65536},
+                        'value': {'type': 'boolean'}},
+                       {'event': {'const': 'response'},
+                        'function': {'const': 'write single register'},
+                        'slave': {'type': 'integer',
+                                  'minimum': 1, 'maximum': 247},
+                        'address': {'type': 'integer',
+                                    'minimum': 1, 'maximum': 65536},
+                        'value': {'type': 'integer',
+                                  'minimum': 0, 'maximum': 65535}}],
+             'receives': [{'target': {'const': 'Test Master'},
+                           'command': {'const': 'read coils'},
+                           'slave': {'type': 'integer',
+                                     'minimum': 1, 'maximum': 247},
+                           'start': {'type': 'integer',
+                                     'minimum': 1, 'maximum': 65536},
+                           'quantity': {'type': 'integer',
+                                        'minimum': 1, 'maximum': 2000}},
+                          {'target': {'const': 'Test Master'},
+                           'command': {'const': 'read holding registers'},
+                           'slave': {'type': 'integer',
+                                     'minimum': 1, 'maximum': 247},
+                           'start': {'type': 'integer',
+                                     'minimum': 1, 'maximum': 65536},
+                           'quantity': {'type': 'integer',
+                                        'minimum': 1, 'maximum': 125}},
+                          {'target': {'const': 'Test Master'},
+                           'command': {'const': 'write single coil'},
+                           'slave': {'type': 'integer',
+                                     'minimum': 1, 'maximum': 247},
+                           'address': {'type': 'integer',
+                                       'minimum': 1, 'maximum': 65536},
+                           'value': {'type': 'boolean'}},
+                          {'target': {'const': 'Test Master'},
+                           'command': {'const': 'write single register'},
+                           'slave': {'type': 'integer',
+                                     'minimum': 1, 'maximum': 247},
+                           'address': {'type': 'integer',
+                                       'minimum': 1, 'maximum': 65536},
+                           'value': {'type': 'integer',
+                                     'minimum': 0, 'maximum': 65535}}]}
+    test(): {'sender': '', 'event': 'registered',
+             'client': 'Test Slave', 'plugin': 'ModbusSlave',
+             'sends': [{'event': {'const': 'received'}},
+                       {'event': {'const': 'sent'}}],
+             'receives': []}
+    test(): {'sender': 'test()', 'target': 'Test Master',
+             'command': 'read coils', 'slave': 1,
+             'start': 1, 'quantity': 2}
+    test(): {'sender': 'test()', 'target': 'Test Master',
+             'command': 'write single coil', 'slave': 1,
+             'address': 1, 'value': False}
+    test(): {'sender': 'test()', 'target': 'Test Master',
+             'command': 'write single coil', 'slave': 1,
+             'address': 2, 'value': True}
+    test(): {'sender': 'test()', 'target': 'Test Master',
+             'command': 'read coils', 'slave': 1,
+             'start': 1, 'quantity': 2}
+    test(): {'sender': 'test()', 'target': 'Test Master',
+             'command': 'read holding registers', 'slave': 1,
+             'start': 7, 'quantity': 2}
+    test(): {'sender': 'test()', 'target': 'Test Master',
+             'command': 'write single register', 'slave': 1,
+             'address': 7, 'value': 42}
+    test(): {'sender': 'test()', 'target': 'Test Master',
+             'command': 'write single register', 'slave': 1,
+             'address': 8, 'value': 42042}
+    test(): {'sender': 'test()', 'target': 'Test Master',
+             'command': 'read holding registers', 'slave': 1,
+             'start': 7, 'quantity': 2}
+    test(): {'sender': 'Test Slave', 'event': 'received',
+             'message': '010100000002bdcb'}
+    test(): {'sender': 'Test Slave', 'event': 'sent',
+             'message': '018102c191'}
+    test(): {'sender': 'Test Master', 'event': 'error',
+             'function': 'read coils', 'slave': 1,
+             'description': 'Illegal data address'}
+    test(): {'sender': 'Test Slave', 'event': 'received',
+             'message': '010500000000cdca'}
+    test(): {'sender': 'Test Slave', 'event': 'sent',
+             'message': '010500000000cdca'}
+    test(): {'sender': 'Test Master', 'event': 'response',
+             'function': 'write single coil', 'slave': 1, 'address': 1,
+             'value': False}
+    test(): {'sender': 'Test Slave', 'event': 'received',
+             'message': '01050001ff00ddfa'}
+    test(): {'sender': 'Test Slave', 'event': 'sent',
+             'message': '01050001ff00ddfa'}
+    test(): {'sender': 'Test Master', 'event': 'response',
+             'function': 'write single coil', 'slave': 1, 'address': 2,
+             'value': True}
+    test(): {'sender': 'Test Slave', 'event': 'received',
+             'message': '010100000002bdcb'}
+    test(): {'sender': 'Test Slave', 'event': 'sent',
+             'message': '01010102d049'}
+    test(): {'sender': 'Test Master', 'event': 'response',
+             'function': 'read coils', 'slave': 1,
+             'values': [False, True, False, False, False, False, False, False]}
+    test(): {'sender': 'Test Slave', 'event': 'received',
+             'message': '010300060002240a'}
+    test(): {'sender': 'Test Slave', 'event': 'sent',
+             'message': '018302c0f1'}
+    test(): {'sender': 'Test Master', 'event': 'error',
+             'function': 'read holding registers', 'slave': 1,
+             'description': 'Illegal data address'}
+    test(): {'sender': 'Test Slave', 'event': 'received',
+             'message': '01060006002ae814'}
+    test(): {'sender': 'Test Slave', 'event': 'sent',
+             'message': '01060006002ae814'}
+    test(): {'sender': 'Test Master', 'event': 'response',
+             'function': 'write single register', 'slave': 1, 'address': 7,
+             'value': 42}
+    test(): {'sender': 'Test Slave', 'event': 'received',
+             'message': '01060007a43ac2d8'}
+    test(): {'sender': 'Test Slave', 'event': 'sent',
+             'message': '01060007a43ac2d8'}
+    test(): {'sender': 'Test Master', 'event': 'response',
+             'function': 'write single register', 'slave': 1, 'address': 8,
+             'value': 42042}
+    test(): {'sender': 'Test Slave', 'event': 'received',
+             'message': '010300060002240a'}
+    test(): {'sender': 'Test Slave', 'event': 'sent',
+             'message': '010304002aa43a2128'}
+    test(): {'sender': 'Test Master', 'event': 'response',
+             'function': 'read holding registers', 'slave': 1,
+             'values': [42, 42042]}
     """
 
     CONF_SCHEMA = {'properties':
@@ -340,13 +498,13 @@ class ModbusMaster(BasePlugin):
                 message['message'] = modbus_message.hex()
                 await self.bus.send(message)
                 return
-            address_hi = modbus_message[3]
-            address_lo = modbus_message[4]
+            address_hi = modbus_message[2]
+            address_lo = modbus_message[3]
             message['address'] = address_hi * 256 + address_lo + 1
             if function_code == 0x05:
-                if modbus_message[5] == 0xFF and modbus_message[6] == 0x00:
+                if modbus_message[4] == 0xFF and modbus_message[5] == 0x00:
                     message['value'] = True
-                elif modbus_message[5] == 0x00 and modbus_message[6] == 0x00:
+                elif modbus_message[4] == 0x00 and modbus_message[5] == 0x00:
                     message['value'] = False
                 else:
                     message['event'] = 'error'
@@ -355,8 +513,8 @@ class ModbusMaster(BasePlugin):
                     await self.bus.send(message)
                     return
             if function_code == 0x06:
-                value_hi = modbus_message[5]
-                value_lo = modbus_message[6]
+                value_hi = modbus_message[4]
+                value_lo = modbus_message[5]
                 message['value'] = value_hi * 256 + value_lo
         await self.bus.send(message)
 
@@ -369,7 +527,7 @@ class ModbusMaster(BasePlugin):
             modbus_request = await self._queue.get()
             slave = modbus_request[0]
             tries = 0
-            while tries < self.conf['retries']:
+            while tries <= self.conf['retries']:
                 tries += 1
                 writer.write(modbus_request)
                 if slave == 0:
@@ -522,18 +680,65 @@ class ModbusSlave(BasePlugin):
                 quantity = quantity_hi * 256 + quantity_lo
                 if function_code == 0x01:
                     okay = True
+                    data = []
+                    byte = 0
+                    bit = 0
                     for i in range(quantity):
                         if start + i not in self._coils:
                             modbus_response += bytes([0x81, 0x02])
                             okay = False
                             break
-                        else:
-                            if self._coils[start + i]:
-                                pass
+                        if self._coils[start + i]:
+                            byte += 2**bit
+                        bit += 1
+                        if bit == 8:
+                            data.append(byte)
+                            byte = 0
+                            bit = 0
+                    if okay:
+                        if bit > 0:
+                            data.append(byte)
+                        modbus_response += bytes([0x01, len(data)])
+                        modbus_response += bytes(data)
                 if function_code == 0x03:
-                    pass
+                    okay = True
+                    data = []
+                    for i in range(quantity):
+                        if start + i not in self._registers:
+                            modbus_response += bytes([0x83, 0x02])
+                            okay = False
+                            break
+                        data.append(self._registers[start + i] >> 8)
+                        data.append(self._registers[start + i] & 0xFF)
+                    if okay:
+                        modbus_response += bytes([0x03, len(data)])
+                        modbus_response += bytes(data)
         elif function_code == 0x05 or function_code == 0x06:
-            pass
+            if len(modbus_request) != 8:
+                modbus_response += bytes([function_code | 0x80, 0x03])
+            else:
+                address_hi = modbus_request[2]
+                address_lo = modbus_request[3]
+                address = address_hi * 256 + address_lo
+                if function_code == 0x05:
+                    okay = True
+                    if (modbus_request[4] == 0xFF and
+                            modbus_request[5] == 0x00):
+                        self._coils[address] = True
+                    elif (modbus_request[4] == 0x00 and
+                            modbus_request[5] == 0x00):
+                        self._coils[address] = False
+                    else:
+                        modbus_response += bytes([0x85, 0x03])
+                        okay = False
+                    if okay:
+                        modbus_response = modbus_request[:-2]
+                if function_code == 0x06:
+                    value_hi = modbus_request[4]
+                    value_lo = modbus_request[5]
+                    value = value_hi * 256 + value_lo
+                    self._registers[address] = value
+                    modbus_response = modbus_request[:-2]
         else:
             modbus_response += bytes([function_code | 0x80, 0x01])
         modbus_response += crc(modbus_response)