diff --git a/app/src/modbus.py b/app/src/modbus.py index 717909c..82c2f01 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -1,7 +1,7 @@ import struct import logging import asyncio -from typing import Generator +from typing import Generator, Callable if __name__ == "app.src.modbus": from app.src.infos import Register @@ -25,10 +25,13 @@ CRC_INIT = 0xFFFF class Modbus(): INV_ADDR = 1 + '''MODBUS slave address of the TSUN inverter''' READ_REGS = 3 + '''MODBUS function code: Read Holding Register''' READ_INPUTS = 4 + '''MODBUS function code: Read Input Register''' WRITE_SINGLE_REG = 6 - '''Modbus function codes''' + '''Modbus function code: Write Single Register''' __crc_tab = [] map = { @@ -66,21 +69,26 @@ class Modbus(): 0x3029: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 } - def __init__(self, snd_handler, timeout: int = 1): + def __init__(self, snd_handler: Callable[[bool], None], timeout: int = 1): if not len(self.__crc_tab): self.__build_crc_tab(CRC_POLY) self.que = asyncio.Queue(100) self.snd_handler = snd_handler + '''Send handler to transmit a MODBUS request''' self.rsp_handler = None + '''Response handler to forward the response''' self.timeout = timeout + '''MODBUS response timeout in seconds''' self.max_retries = 3 + '''Max retransmit for MODBUS requests''' self.retry_cnt = 0 self.last_req = b'' self.counter = {} + '''Dictenary with statistic counter''' self.counter['timeouts'] = 0 self.counter['retries'] = {} - for i in range(0, self.max_retries): - self.counter['retries'][i] = 0 + for i in range(0, self.max_retries+1): + self.counter['retries'][f'{i}'] = 0 self.last_addr = 0 self.last_fcode = 0 self.last_len = 0 @@ -94,80 +102,67 @@ class Modbus(): if type(self.counter) is not None: logging.info(f'Modbus __del__:\n {self.counter}') - def start_timer(self): - if self.req_pend: - return - self.req_pend = True - self.tim = self.loop.call_later(self.timeout, self.timeout_cb) - # logging.debug(f'Modbus start timer {self}') + def build_msg(self, addr: int, func: int, reg: int, val: int) -> None: + """Build MODBUS RTU message frame and add it to the tx queue - def stop_timer(self): - self.req_pend = False - # logging.debug(f'Modbus stop timer {self}') - if self.tim: - self.tim.cancel() - - def timeout_cb(self): - self.req_pend = False - - if self.retry_cnt < self.max_retries: - logging.debug(f'Modbus retrans {self}') - self.retry_cnt += 1 - self.start_timer() - self.snd_handler(self.last_req, retrans=True) - else: - logging.info(f'Modbus timeout {self}') - self.counter['timeouts'] += 1 - self.get_next_req() - - def get_next_req(self) -> None: - if self.req_pend: - return - try: - item = self.que.get_nowait() - req = item['req'] - self.last_req = req - self.rsp_handler = item['rsp_hdl'] - self.last_addr = req[0] - self.last_fcode = req[1] - - res = struct.unpack_from('>HH', req, 2) - self.last_reg = res[0] - self.last_len = res[1] - self.retry_cnt = 0 - self.start_timer() - self.snd_handler(self.last_req, retrans=False) - except asyncio.QueueEmpty: - pass - - def build_msg(self, addr, func, reg, val) -> None: + Keyword arguments: + addr: RTU slave address + func: MODBUS function code + reg: 16-bit register number + val: 16 bit value + """ msg = struct.pack('>BBHH', addr, func, reg, val) msg += struct.pack(' bool: + def recv_req(self, buf: bytearray, + rsp_handler: Callable[[None], None] = None) -> bool: + """Add the received Modbus request to the tx queue + + Keyword arguments: + buf: Modbus RTU pdu incl ADDR byte and trailing CRC + rsp_handler: Callback, if the received pdu is valid + + Returns: + True: PDU was added to the queue + False: PDU was ignored, due to an error + """ # logging.info(f'recv_req: first byte modbus:{buf[0]} len:{len(buf)}') - if not self.check_crc(buf): + if not self.__check_crc(buf): self.err = 1 logging.error('Modbus recv: CRC error') return False self.que.put_nowait({'req': buf, 'rsp_hdl': rsp_handler}) if self.que.qsize() == 1: - self.get_next_req() + self.__send_next_from_que() return True def recv_resp(self, info_db, buf: bytearray, node_id: str) -> \ - Generator[tuple[str, bool, any], None, None]: + Generator[tuple[str, bool, int | float | str], None, None]: + """Generator which check and parse a received MODBUS response. + + Keyword arguments: + info_db: database for info lockups + buf: received Modbus RTU response frame + node_id: string for logging which identifies the slave + + Returns on error and set Self.err to: + 1: CRC error + 2: Wrong server address + 3: Unexpected function code + 4: Unexpected data length + 5: No MODBUS request pending + """ # logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}') if not self.req_pend: self.err = 5 return - if not self.check_crc(buf): + if not self.__check_crc(buf): logging.error('Modbus resp: CRC error') self.err = 1 return @@ -188,7 +183,7 @@ class Modbus(): self.err = 4 return first_reg = self.last_reg # save last_reg before sending next pdu - self.stop_timer() # stop timer and send next pdu + self.__stop_timer() # stop timer and send next pdu for i in range(0, elmlen): addr = first_reg+i @@ -215,23 +210,81 @@ class Modbus(): f'[\'{node_id}\']MODBUS: {name}' f' : {result}{unit}') else: - self.stop_timer() - self.counter['retries'][self.retry_cnt] += 1 + self.__stop_timer() + + self.counter['retries'][f'{self.retry_cnt}'] += 1 if self.rsp_handler: self.rsp_handler() - self.get_next_req() + self.__send_next_from_que() - def check_crc(self, msg) -> bool: + ''' + MODBUS response timer + ''' + def __start_timer(self) -> None: + '''Start response timer and set `req_pend` to True''' + self.req_pend = True + self.tim = self.loop.call_later(self.timeout, self.__timeout_cb) + # logging.debug(f'Modbus start timer {self}') + + def __stop_timer(self) -> None: + '''Stop response timer and set `req_pend` to False''' + self.req_pend = False + # logging.debug(f'Modbus stop timer {self}') + if self.tim: + self.tim.cancel() + + def __timeout_cb(self) -> None: + '''Rsponse timeout handler retransmit pdu or send next pdu''' + self.req_pend = False + + if self.retry_cnt < self.max_retries: + logging.debug(f'Modbus retrans {self}') + self.retry_cnt += 1 + self.__start_timer() + self.snd_handler(self.last_req, retrans=True) + else: + logging.info(f'Modbus timeout {self}') + self.counter['timeouts'] += 1 + self.__send_next_from_que() + + def __send_next_from_que(self) -> None: + '''Get next MODBUS pdu from queue and transmit it''' + if self.req_pend: + return + try: + item = self.que.get_nowait() + req = item['req'] + self.last_req = req + self.rsp_handler = item['rsp_hdl'] + self.last_addr = req[0] + self.last_fcode = req[1] + + res = struct.unpack_from('>HH', req, 2) + self.last_reg = res[0] + self.last_len = res[1] + self.retry_cnt = 0 + self.__start_timer() + self.snd_handler(self.last_req, retrans=False) + except asyncio.QueueEmpty: + pass + + ''' + Helper function for CRC-16 handling + ''' + def __check_crc(self, msg: bytearray) -> bool: + '''Check CRC-16 and returns True if valid''' return 0 == self.__calc_crc(msg) - def __calc_crc(self, buffer: bytes) -> int: + def __calc_crc(self, buffer: bytearray) -> int: + '''Build CRC-16 for buffer and returns it''' crc = CRC_INIT for cur in buffer: crc = (crc >> 8) ^ self.__crc_tab[(crc ^ cur) & 0xFF] return crc - def __build_crc_tab(self, poly) -> None: + def __build_crc_tab(self, poly: int) -> None: + '''Build CRC-16 helper table, must be called exactly one time''' for index in range(256): data = index << 1 crc = 0 diff --git a/app/tests/test_modbus.py b/app/tests/test_modbus.py index fed9c09..1985ee4 100644 --- a/app/tests/test_modbus.py +++ b/app/tests/test_modbus.py @@ -21,24 +21,25 @@ def test_modbus_crc(): mb = Modbus(None) assert 0x0b02 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x04') assert 0 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x04\x02\x0b') - assert mb.check_crc(b'\x01\x06\x20\x08\x00\x04\x02\x0b') + assert mb._Modbus__check_crc(b'\x01\x06\x20\x08\x00\x04\x02\x0b') assert 0xc803 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x00') assert 0 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x00\x03\xc8') - assert mb.check_crc(b'\x01\x06\x20\x08\x00\x00\x03\xc8') + assert mb._Modbus__check_crc(b'\x01\x06\x20\x08\x00\x00\x03\xc8') assert 0x5c75 == mb._Modbus__calc_crc(b'\x01\x03\x08\x01\x2c\x00\x2c\x02\x2c\x2c\x46') + def test_build_modbus_pdu(): mb = TestHelper() mb.build_msg(1,6,0x2000,0x12) - mb.get_next_req() + mb._Modbus__send_next_from_que() assert mb.pdu == b'\x01\x06\x20\x00\x00\x12\x02\x07' - assert mb.check_crc(mb.pdu) + assert mb._Modbus__check_crc(mb.pdu) def test_recv_req_crc(): mb = TestHelper() mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x08') - mb.get_next_req() + mb._Modbus__send_next_from_que() assert mb.last_fcode == 0 assert mb.last_reg == 0 assert mb.last_len == 0 @@ -47,7 +48,7 @@ def test_recv_req_crc(): def test_recv_req_addr(): mb = TestHelper() mb.recv_req(b'\x02\x06\x20\x00\x00\x12\x02\x34') - mb.get_next_req() + mb._Modbus__send_next_from_que() assert mb.last_addr == 2 assert mb.last_fcode == 6 assert mb.last_reg == 0x2000 @@ -56,7 +57,7 @@ def test_recv_req_addr(): def test_recv_req(): mb = TestHelper() mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x07') - mb.get_next_req() + mb._Modbus__send_next_from_que() assert mb.last_fcode == 6 assert mb.last_reg == 0x2000 assert mb.last_len == 0x12 @@ -88,7 +89,7 @@ def test_recv_recv_addr(): assert mb.err == 2 assert 0 == call assert mb.que.qsize() == 0 - mb.stop_timer() + mb._Modbus__stop_timer() assert not mb.req_pend def test_recv_recv_fcode(): @@ -104,7 +105,7 @@ def test_recv_recv_fcode(): assert mb.err == 3 assert 0 == call assert mb.que.qsize() == 0 - mb.stop_timer() + mb._Modbus__stop_timer() assert not mb.req_pend def test_recv_recv_len(): @@ -120,7 +121,7 @@ def test_recv_recv_len(): assert mb.err == 4 assert 0 == call assert mb.que.qsize() == 0 - mb.stop_timer() + mb._Modbus__stop_timer() assert not mb.req_pend def test_build_recv(): @@ -162,7 +163,7 @@ def test_build_recv(): assert 0 == mb.err assert 5 == call assert mb.que.qsize() == 0 - mb.stop_timer() + mb._Modbus__stop_timer() assert not mb.req_pend def test_build_long(): @@ -186,7 +187,7 @@ def test_build_long(): assert 0 == mb.err assert 3 == call assert mb.que.qsize() == 0 - mb.stop_timer() + mb._Modbus__stop_timer() assert not mb.req_pend def test_queue(): @@ -198,12 +199,12 @@ def test_queue(): assert mb.send_calls == 1 assert mb.pdu == b'\x01\x030"\x00\x04\xeb\x03' mb.pdu = None - mb.get_next_req() + mb._Modbus__send_next_from_que() assert mb.send_calls == 1 assert mb.pdu == None assert mb.que.qsize() == 0 - mb.stop_timer() + mb._Modbus__stop_timer() assert not mb.req_pend def test_queue2(): @@ -215,7 +216,7 @@ def test_queue2(): assert mb.send_calls == 1 assert mb.pdu == b'\x01\x030\x07\x00\x06{\t' - mb.get_next_req() + mb._Modbus__send_next_from_que() assert mb.send_calls == 1 call = 0 exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]