cleanup
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import struct
|
import struct
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Generator
|
from typing import Generator, Callable
|
||||||
|
|
||||||
if __name__ == "app.src.modbus":
|
if __name__ == "app.src.modbus":
|
||||||
from app.src.infos import Register
|
from app.src.infos import Register
|
||||||
@@ -25,10 +25,13 @@ CRC_INIT = 0xFFFF
|
|||||||
|
|
||||||
class Modbus():
|
class Modbus():
|
||||||
INV_ADDR = 1
|
INV_ADDR = 1
|
||||||
|
'''MODBUS slave address of the TSUN inverter'''
|
||||||
READ_REGS = 3
|
READ_REGS = 3
|
||||||
|
'''MODBUS function code: Read Holding Register'''
|
||||||
READ_INPUTS = 4
|
READ_INPUTS = 4
|
||||||
|
'''MODBUS function code: Read Input Register'''
|
||||||
WRITE_SINGLE_REG = 6
|
WRITE_SINGLE_REG = 6
|
||||||
'''Modbus function codes'''
|
'''Modbus function code: Write Single Register'''
|
||||||
|
|
||||||
__crc_tab = []
|
__crc_tab = []
|
||||||
map = {
|
map = {
|
||||||
@@ -66,21 +69,26 @@ class Modbus():
|
|||||||
0x3029: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
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):
|
if not len(self.__crc_tab):
|
||||||
self.__build_crc_tab(CRC_POLY)
|
self.__build_crc_tab(CRC_POLY)
|
||||||
self.que = asyncio.Queue(100)
|
self.que = asyncio.Queue(100)
|
||||||
self.snd_handler = snd_handler
|
self.snd_handler = snd_handler
|
||||||
|
'''Send handler to transmit a MODBUS request'''
|
||||||
self.rsp_handler = None
|
self.rsp_handler = None
|
||||||
|
'''Response handler to forward the response'''
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
'''MODBUS response timeout in seconds'''
|
||||||
self.max_retries = 3
|
self.max_retries = 3
|
||||||
|
'''Max retransmit for MODBUS requests'''
|
||||||
self.retry_cnt = 0
|
self.retry_cnt = 0
|
||||||
self.last_req = b''
|
self.last_req = b''
|
||||||
self.counter = {}
|
self.counter = {}
|
||||||
|
'''Dictenary with statistic counter'''
|
||||||
self.counter['timeouts'] = 0
|
self.counter['timeouts'] = 0
|
||||||
self.counter['retries'] = {}
|
self.counter['retries'] = {}
|
||||||
for i in range(0, self.max_retries):
|
for i in range(0, self.max_retries+1):
|
||||||
self.counter['retries'][i] = 0
|
self.counter['retries'][f'{i}'] = 0
|
||||||
self.last_addr = 0
|
self.last_addr = 0
|
||||||
self.last_fcode = 0
|
self.last_fcode = 0
|
||||||
self.last_len = 0
|
self.last_len = 0
|
||||||
@@ -94,80 +102,67 @@ class Modbus():
|
|||||||
if type(self.counter) is not None:
|
if type(self.counter) is not None:
|
||||||
logging.info(f'Modbus __del__:\n {self.counter}')
|
logging.info(f'Modbus __del__:\n {self.counter}')
|
||||||
|
|
||||||
def start_timer(self):
|
def build_msg(self, addr: int, func: int, reg: int, val: int) -> None:
|
||||||
if self.req_pend:
|
"""Build MODBUS RTU message frame and add it to the tx queue
|
||||||
return
|
|
||||||
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):
|
Keyword arguments:
|
||||||
self.req_pend = False
|
addr: RTU slave address
|
||||||
# logging.debug(f'Modbus stop timer {self}')
|
func: MODBUS function code
|
||||||
if self.tim:
|
reg: 16-bit register number
|
||||||
self.tim.cancel()
|
val: 16 bit value
|
||||||
|
"""
|
||||||
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:
|
|
||||||
msg = struct.pack('>BBHH', addr, func, reg, val)
|
msg = struct.pack('>BBHH', addr, func, reg, val)
|
||||||
msg += struct.pack('<H', self.__calc_crc(msg))
|
msg += struct.pack('<H', self.__calc_crc(msg))
|
||||||
self.que.put_nowait({'req': msg,
|
self.que.put_nowait({'req': msg,
|
||||||
'rsp_hdl': None})
|
'rsp_hdl': None})
|
||||||
if self.que.qsize() == 1:
|
if self.que.qsize() == 1:
|
||||||
self.get_next_req()
|
self.__send_next_from_que()
|
||||||
|
|
||||||
def recv_req(self, buf: bytearray, rsp_handler=None) -> 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)}')
|
# 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
|
self.err = 1
|
||||||
logging.error('Modbus recv: CRC error')
|
logging.error('Modbus recv: CRC error')
|
||||||
return False
|
return False
|
||||||
self.que.put_nowait({'req': buf,
|
self.que.put_nowait({'req': buf,
|
||||||
'rsp_hdl': rsp_handler})
|
'rsp_hdl': rsp_handler})
|
||||||
if self.que.qsize() == 1:
|
if self.que.qsize() == 1:
|
||||||
self.get_next_req()
|
self.__send_next_from_que()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def recv_resp(self, info_db, buf: bytearray, node_id: str) -> \
|
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)}')
|
# logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}')
|
||||||
if not self.req_pend:
|
if not self.req_pend:
|
||||||
self.err = 5
|
self.err = 5
|
||||||
return
|
return
|
||||||
if not self.check_crc(buf):
|
if not self.__check_crc(buf):
|
||||||
logging.error('Modbus resp: CRC error')
|
logging.error('Modbus resp: CRC error')
|
||||||
self.err = 1
|
self.err = 1
|
||||||
return
|
return
|
||||||
@@ -188,7 +183,7 @@ class Modbus():
|
|||||||
self.err = 4
|
self.err = 4
|
||||||
return
|
return
|
||||||
first_reg = self.last_reg # save last_reg before sending next pdu
|
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):
|
for i in range(0, elmlen):
|
||||||
addr = first_reg+i
|
addr = first_reg+i
|
||||||
@@ -215,23 +210,81 @@ class Modbus():
|
|||||||
f'[\'{node_id}\']MODBUS: {name}'
|
f'[\'{node_id}\']MODBUS: {name}'
|
||||||
f' : {result}{unit}')
|
f' : {result}{unit}')
|
||||||
else:
|
else:
|
||||||
self.stop_timer()
|
self.__stop_timer()
|
||||||
self.counter['retries'][self.retry_cnt] += 1
|
|
||||||
|
self.counter['retries'][f'{self.retry_cnt}'] += 1
|
||||||
if self.rsp_handler:
|
if self.rsp_handler:
|
||||||
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)
|
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
|
crc = CRC_INIT
|
||||||
|
|
||||||
for cur in buffer:
|
for cur in buffer:
|
||||||
crc = (crc >> 8) ^ self.__crc_tab[(crc ^ cur) & 0xFF]
|
crc = (crc >> 8) ^ self.__crc_tab[(crc ^ cur) & 0xFF]
|
||||||
return crc
|
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):
|
for index in range(256):
|
||||||
data = index << 1
|
data = index << 1
|
||||||
crc = 0
|
crc = 0
|
||||||
|
|||||||
@@ -21,24 +21,25 @@ def test_modbus_crc():
|
|||||||
mb = Modbus(None)
|
mb = Modbus(None)
|
||||||
assert 0x0b02 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x04')
|
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 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 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 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')
|
assert 0x5c75 == mb._Modbus__calc_crc(b'\x01\x03\x08\x01\x2c\x00\x2c\x02\x2c\x2c\x46')
|
||||||
|
|
||||||
def test_build_modbus_pdu():
|
def test_build_modbus_pdu():
|
||||||
mb = TestHelper()
|
mb = TestHelper()
|
||||||
mb.build_msg(1,6,0x2000,0x12)
|
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.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():
|
def test_recv_req_crc():
|
||||||
mb = TestHelper()
|
mb = TestHelper()
|
||||||
mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x08')
|
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_fcode == 0
|
||||||
assert mb.last_reg == 0
|
assert mb.last_reg == 0
|
||||||
assert mb.last_len == 0
|
assert mb.last_len == 0
|
||||||
@@ -47,7 +48,7 @@ def test_recv_req_crc():
|
|||||||
def test_recv_req_addr():
|
def test_recv_req_addr():
|
||||||
mb = TestHelper()
|
mb = TestHelper()
|
||||||
mb.recv_req(b'\x02\x06\x20\x00\x00\x12\x02\x34')
|
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_addr == 2
|
||||||
assert mb.last_fcode == 6
|
assert mb.last_fcode == 6
|
||||||
assert mb.last_reg == 0x2000
|
assert mb.last_reg == 0x2000
|
||||||
@@ -56,7 +57,7 @@ def test_recv_req_addr():
|
|||||||
def test_recv_req():
|
def test_recv_req():
|
||||||
mb = TestHelper()
|
mb = TestHelper()
|
||||||
mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x07')
|
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_fcode == 6
|
||||||
assert mb.last_reg == 0x2000
|
assert mb.last_reg == 0x2000
|
||||||
assert mb.last_len == 0x12
|
assert mb.last_len == 0x12
|
||||||
@@ -88,7 +89,7 @@ def test_recv_recv_addr():
|
|||||||
assert mb.err == 2
|
assert mb.err == 2
|
||||||
assert 0 == call
|
assert 0 == call
|
||||||
assert mb.que.qsize() == 0
|
assert mb.que.qsize() == 0
|
||||||
mb.stop_timer()
|
mb._Modbus__stop_timer()
|
||||||
assert not mb.req_pend
|
assert not mb.req_pend
|
||||||
|
|
||||||
def test_recv_recv_fcode():
|
def test_recv_recv_fcode():
|
||||||
@@ -104,7 +105,7 @@ def test_recv_recv_fcode():
|
|||||||
assert mb.err == 3
|
assert mb.err == 3
|
||||||
assert 0 == call
|
assert 0 == call
|
||||||
assert mb.que.qsize() == 0
|
assert mb.que.qsize() == 0
|
||||||
mb.stop_timer()
|
mb._Modbus__stop_timer()
|
||||||
assert not mb.req_pend
|
assert not mb.req_pend
|
||||||
|
|
||||||
def test_recv_recv_len():
|
def test_recv_recv_len():
|
||||||
@@ -120,7 +121,7 @@ def test_recv_recv_len():
|
|||||||
assert mb.err == 4
|
assert mb.err == 4
|
||||||
assert 0 == call
|
assert 0 == call
|
||||||
assert mb.que.qsize() == 0
|
assert mb.que.qsize() == 0
|
||||||
mb.stop_timer()
|
mb._Modbus__stop_timer()
|
||||||
assert not mb.req_pend
|
assert not mb.req_pend
|
||||||
|
|
||||||
def test_build_recv():
|
def test_build_recv():
|
||||||
@@ -162,7 +163,7 @@ def test_build_recv():
|
|||||||
assert 0 == mb.err
|
assert 0 == mb.err
|
||||||
assert 5 == call
|
assert 5 == call
|
||||||
assert mb.que.qsize() == 0
|
assert mb.que.qsize() == 0
|
||||||
mb.stop_timer()
|
mb._Modbus__stop_timer()
|
||||||
assert not mb.req_pend
|
assert not mb.req_pend
|
||||||
|
|
||||||
def test_build_long():
|
def test_build_long():
|
||||||
@@ -186,7 +187,7 @@ def test_build_long():
|
|||||||
assert 0 == mb.err
|
assert 0 == mb.err
|
||||||
assert 3 == call
|
assert 3 == call
|
||||||
assert mb.que.qsize() == 0
|
assert mb.que.qsize() == 0
|
||||||
mb.stop_timer()
|
mb._Modbus__stop_timer()
|
||||||
assert not mb.req_pend
|
assert not mb.req_pend
|
||||||
|
|
||||||
def test_queue():
|
def test_queue():
|
||||||
@@ -198,12 +199,12 @@ def test_queue():
|
|||||||
assert mb.send_calls == 1
|
assert mb.send_calls == 1
|
||||||
assert mb.pdu == b'\x01\x030"\x00\x04\xeb\x03'
|
assert mb.pdu == b'\x01\x030"\x00\x04\xeb\x03'
|
||||||
mb.pdu = None
|
mb.pdu = None
|
||||||
mb.get_next_req()
|
mb._Modbus__send_next_from_que()
|
||||||
assert mb.send_calls == 1
|
assert mb.send_calls == 1
|
||||||
assert mb.pdu == None
|
assert mb.pdu == None
|
||||||
|
|
||||||
assert mb.que.qsize() == 0
|
assert mb.que.qsize() == 0
|
||||||
mb.stop_timer()
|
mb._Modbus__stop_timer()
|
||||||
assert not mb.req_pend
|
assert not mb.req_pend
|
||||||
|
|
||||||
def test_queue2():
|
def test_queue2():
|
||||||
@@ -215,7 +216,7 @@ def test_queue2():
|
|||||||
|
|
||||||
assert mb.send_calls == 1
|
assert mb.send_calls == 1
|
||||||
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
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
|
assert mb.send_calls == 1
|
||||||
call = 0
|
call = 0
|
||||||
exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]
|
exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]
|
||||||
|
|||||||
Reference in New Issue
Block a user