From f36974a334ed90c2e1d36e565b9807df7834de2b Mon Sep 17 00:00:00 2001 From: Ian Connor Date: Wed, 21 Jul 2021 17:01:48 +1000 Subject: [PATCH 1/3] Add serial option --- modbus4mqtt/modbus4mqtt.py | 6 ++++-- modbus4mqtt/modbus_interface.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/modbus4mqtt/modbus4mqtt.py b/modbus4mqtt/modbus4mqtt.py index 33b9eda..0741662 100755 --- a/modbus4mqtt/modbus4mqtt.py +++ b/modbus4mqtt/modbus4mqtt.py @@ -47,12 +47,14 @@ def connect_modbus(self): else: word_order = modbus_interface.WordOrder.HighLow - self._mb = modbus_interface.modbus_interface(self.config['ip'], + self._mb = modbus_interface.modbus_interface(self.config.get('ip', None), self.config.get('port', 502), self.config.get('update_rate', 5), variant=self.config.get('variant', None), scan_batching=self.config.get('scan_batching', None), - word_order=word_order) + word_order=word_order, + baudrate=self.config.get('baudrate', 19200), + method=self.config.get('method', 'rtu')) failed_attempts = 1 while self._mb.connect(): logging.warning("Modbus connection attempt {} failed. Retrying...".format(failed_attempts)) diff --git a/modbus4mqtt/modbus_interface.py b/modbus4mqtt/modbus_interface.py index b480737..e904fec 100644 --- a/modbus4mqtt/modbus_interface.py +++ b/modbus4mqtt/modbus_interface.py @@ -2,7 +2,7 @@ from enum import Enum import logging from queue import Queue -from pymodbus.client.sync import ModbusTcpClient, ModbusSocketFramer +from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusSocketFramer from pymodbus import exceptions from SungrowModbusTcpClient import SungrowModbusTcpClient @@ -20,9 +20,11 @@ class WordOrder(Enum): class modbus_interface(): - def __init__(self, ip, port=502, update_rate_s=DEFAULT_SCAN_RATE_S, variant=None, scan_batching=None, word_order=WordOrder.HighLow): + def __init__(self, ip=None, port=502, update_rate_s=DEFAULT_SCAN_RATE_S, variant=None, scan_batching=None, word_order=WordOrder.HighLow, method='rtu', baudrate=9600): self._ip = ip self._port = port + self._method = method + self._baudrate = baudrate # This is a dict of sets. Each key represents one table of modbus registers. # At the moment it has 'input' and 'holding' self._tables = {'input': set(), 'holding': set()} @@ -54,6 +56,10 @@ def connect(self): self._mb = SungrowModbusTcpClient.SungrowModbusTcpClient(host=self._ip, port=self._port, framer=ModbusSocketFramer, timeout=1, RetryOnEmpty=True, retries=1) + elif self._variant == 'serial': + self._mb = ModbusSerialClient(method=self._method, port=self._port, baudrate=self._baudrate, + bytesize=8, parity='N', stopbits=1, + timeout=1, retries=1) else: self._mb = ModbusTcpClient(self._ip, self._port, framer=ModbusSocketFramer, timeout=1, From 0ff97015da868d6557a051627070deedc911b4d9 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Tue, 11 Oct 2022 16:01:56 +0000 Subject: [PATCH 2/3] Use (modbus standard) even parity for serial --- modbus4mqtt/modbus_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modbus4mqtt/modbus_interface.py b/modbus4mqtt/modbus_interface.py index e904fec..778fea7 100644 --- a/modbus4mqtt/modbus_interface.py +++ b/modbus4mqtt/modbus_interface.py @@ -58,7 +58,7 @@ def connect(self): RetryOnEmpty=True, retries=1) elif self._variant == 'serial': self._mb = ModbusSerialClient(method=self._method, port=self._port, baudrate=self._baudrate, - bytesize=8, parity='N', stopbits=1, + bytesize=8, parity='E', stopbits=1, timeout=1, retries=1) else: self._mb = ModbusTcpClient(self._ip, self._port, From 47bd8e818c634b4aa7269dd1a6fc0901b3fa6611 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Tue, 11 Oct 2022 15:58:08 +0000 Subject: [PATCH 3/3] Make the slave address configurable This allows a top-level "slave" entry in the config to set the slave address globally (defaulting to 1 as before), but also allows overriding the slave address for individual registers (which allows communicating with multiple slaves on the same bus). --- modbus4mqtt/modbus4mqtt.py | 8 +++- modbus4mqtt/modbus_interface.py | 74 +++++++++++++++++---------------- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/modbus4mqtt/modbus4mqtt.py b/modbus4mqtt/modbus4mqtt.py index 0741662..c3e1089 100755 --- a/modbus4mqtt/modbus4mqtt.py +++ b/modbus4mqtt/modbus4mqtt.py @@ -31,6 +31,7 @@ def __init__(self, hostname, port, username, password, config_file, mqtt_topic_p self.prefix = mqtt_topic_prefix self.address_offset = self.config.get('address_offset', 0) self.registers = self.config['registers'] + self.default_slave = self.config.get('slave', 1) for register in self.registers: register['address'] += self.address_offset self.modbus_connect_retries = -1 # Retry forever by default @@ -67,7 +68,8 @@ def connect_modbus(self): sleep(self.modbus_reconnect_sleep_interval) # Tells the modbus interface about the registers we consider interesting. for register in self.registers: - self._mb.add_monitor_register(register.get('table', 'holding'), register['address'], register.get('type', 'uint16')) + slave = register.get('slave', self.default_slave) + self._mb.add_monitor_register(register.get('table', 'holding'), slave, register['address'], register.get('type', 'uint16')) register['value'] = None def modbus_connection_failed(self): @@ -105,6 +107,7 @@ def poll(self): for register in self._get_registers_with('pub_topic'): try: value = self._mb.get_value( register.get('table', 'holding'), + register.get('slave', self.default_slave), register['address'], register.get('type', 'uint16')) except Exception: @@ -197,7 +200,8 @@ def _on_message(self, client, userdata, msg): "Bad/missing value_map? Topic: {}, Value: {}".format(topic, value)) continue type = register.get('type', 'uint16') - self._mb.set_value(register.get('table', 'holding'), register['address'], int(value), + slave = register.get('slave', self.default_slave) + self._mb.set_value(register.get('table', 'holding'), slave, register['address'], int(value), register.get('mask', 0xFFFF), type) # This throws ValueError exceptions if the imported registers are invalid diff --git a/modbus4mqtt/modbus_interface.py b/modbus4mqtt/modbus_interface.py index 778fea7..0cb45b6 100644 --- a/modbus4mqtt/modbus_interface.py +++ b/modbus4mqtt/modbus_interface.py @@ -5,6 +5,7 @@ from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusSocketFramer from pymodbus import exceptions from SungrowModbusTcpClient import SungrowModbusTcpClient +from collections import defaultdict DEFAULT_SCAN_RATE_S = 5 DEFAULT_SCAN_BATCHING = 100 @@ -27,10 +28,10 @@ def __init__(self, ip=None, port=502, update_rate_s=DEFAULT_SCAN_RATE_S, variant self._baudrate = baudrate # This is a dict of sets. Each key represents one table of modbus registers. # At the moment it has 'input' and 'holding' - self._tables = {'input': set(), 'holding': set()} + self._tables = defaultdict(lambda: {'input': set(), 'holding': set()}) # This is a dicts of dicts. These hold the current values of the interesting registers - self._values = {'input': {}, 'holding': {}} + self._values = defaultdict(lambda: {'input': {}, 'holding': {}}) self._planned_writes = Queue() self._writing = False @@ -65,39 +66,40 @@ def connect(self): framer=ModbusSocketFramer, timeout=1, RetryOnEmpty=True, retries=1) - def add_monitor_register(self, table, addr, type='uint16'): + def add_monitor_register(self, table, slave, addr, type='uint16'): # Accepts a modbus register and table to monitor - if table not in self._tables: + if table not in self._tables[slave]: raise ValueError("Unsupported table type. Please only use: {}".format(self._tables.keys())) # Register enough sequential addresses to fill the size of the register type. # Note: Each address provides 2 bytes of data. for i in range(type_length(type)): - self._tables[table].add(addr+i) + self._tables[slave][table].add(addr+i) def poll(self): # Polls for the values marked as interesting in self._tables. - for table in self._tables: - # This batches up modbus reads in chunks of self._scan_batching - start = -1 - for k in sorted(self._tables[table]): - group = int(k) - int(k) % self._scan_batching - if (start < group): - try: - values = self._scan_value_range(table, group, self._scan_batching) - for x in range(0, self._scan_batching): - key = group + x - self._values[table][key] = values[x] - # Avoid back-to-back read operations that could overwhelm some modbus devices. - sleep(DEFAULT_READ_SLEEP_S) - except ValueError as e: - logging.exception("{}".format(e)) - start = group + self._scan_batching-1 + for slave in self._tables: + for table in self._tables[slave]: + # This batches up modbus reads in chunks of self._scan_batching + start = -1 + for k in sorted(self._tables[slave][table]): + group = int(k) - int(k) % self._scan_batching + if (start < group): + try: + values = self._scan_value_range(table, slave, group, self._scan_batching) + for x in range(0, self._scan_batching): + key = group + x + self._values[slave][table][key] = values[x] + # Avoid back-to-back read operations that could overwhelm some modbus devices. + sleep(DEFAULT_READ_SLEEP_S) + except ValueError as e: + logging.exception("{}".format(e)) + start = group + self._scan_batching-1 self._process_writes() - def get_value(self, table, addr, type='uint16'): - if table not in self._values: - raise ValueError("Unsupported table type. Please only use: {}".format(self._values.keys())) - if addr not in self._values[table]: + def get_value(self, table, slave, addr, type='uint16'): + if table not in self._values[slave]: + raise ValueError("Unsupported table type. Please only use: {}".format(self._values[slave].keys())) + if addr not in self._values[slave][table]: raise ValueError("Unpolled address. Use add_monitor_register(addr, table) to add a register to the polled list.") # Read sequential addresses to get enough bytes to satisfy the type of this register. # Note: Each address provides 2 bytes of data. @@ -105,14 +107,14 @@ def get_value(self, table, addr, type='uint16'): type_len = type_length(type) for i in range(type_len): if self._word_order == WordOrder.HighLow: - data = self._values[table][addr + i] + data = self._values[slave][table][addr + i] else: - data = self._values[table][addr + (type_len-i-1)] + data = self._values[slave][table][addr + (type_len-i-1)] value += data.to_bytes(2,'big') value = _convert_from_bytes_to_type(value, type) return value - def set_value(self, table, addr, value, mask=0xFFFF, type='uint16'): + def set_value(self, table, slave, addr, value, mask=0xFFFF, type='uint16'): if table != 'holding': # I'm not sure if this is true for all devices. I might support writing to coils later, # so leave this door open. @@ -127,7 +129,7 @@ def set_value(self, table, addr, value, mask=0xFFFF, type='uint16'): value = _convert_from_bytes_to_type(bytes_to_write[i*2:i*2+2], 'uint16') else: value = _convert_from_bytes_to_type(bytes_to_write[(type_len-i-1)*2:(type_len-i-1)*2+2], 'uint16') - self._planned_writes.put((addr+i, value, mask)) + self._planned_writes.put((slave, addr+i, value, mask)) self._process_writes() @@ -142,9 +144,9 @@ def _process_writes(self, max_block_s=DEFAULT_WRITE_BLOCK_INTERVAL_S): try: self._writing = True while not self._planned_writes.empty() and (time() - write_start_time) < max_block_s: - addr, value, mask = self._planned_writes.get() + slave, addr, value, mask = self._planned_writes.get() if mask == 0xFFFF: - self._mb.write_register(addr, value, unit=0x01) + self._mb.write_register(addr, value, unit=slave) else: # https://pymodbus.readthedocs.io/en/latest/source/library/pymodbus.client.html?highlight=mask_write_register#pymodbus.client.common.ModbusClientMixin.mask_write_register # https://www.mathworks.com/help/instrument/modify-the-contents-of-a-holding-register-using-a-mask-write.html @@ -156,11 +158,11 @@ def _process_writes(self, max_block_s=DEFAULT_WRITE_BLOCK_INTERVAL_S): # I suspect it's a different modbus opcode that tries to do clever things that my device doesn't support. # result = self._mb.mask_write_register(address=addr, and_mask=(1<<16)-1-mask, or_mask=value, unit=0x01) # print("Result: {}".format(result)) - old_value = self._scan_value_range('holding', addr, 1)[0] + old_value = self._scan_value_range('holding', slave, addr, 1)[0] and_mask = (1<<16)-1-mask or_mask = value new_value = (old_value & and_mask) | (or_mask & (mask)) - self._mb.write_register(addr, new_value, unit=0x01) + self._mb.write_register(addr, new_value, unit=slave) sleep(DEFAULT_WRITE_SLEEP_S) except Exception as e: # BUG catch only the specific exception that means pymodbus failed to write to a register @@ -169,12 +171,12 @@ def _process_writes(self, max_block_s=DEFAULT_WRITE_BLOCK_INTERVAL_S): finally: self._writing = False - def _scan_value_range(self, table, start, count): + def _scan_value_range(self, table, slave, start, count): result = None if table == 'input': - result = self._mb.read_input_registers(start, count, unit=0x01) + result = self._mb.read_input_registers(start, count, unit=slave) elif table == 'holding': - result = self._mb.read_holding_registers(start, count, unit=0x01) + result = self._mb.read_holding_registers(start, count, unit=slave) try: return result.registers except: