96 lines
4.0 KiB
Python
96 lines
4.0 KiB
Python
"""PM35 MODBUS 03/06, including the legacy 128-word/zero-byte-count reply."""
|
|
from .set_uart import crc16, number
|
|
|
|
|
|
def parse_pm35(raw, role):
|
|
raw = bytes(raw)
|
|
if len(raw) < 5:
|
|
raise ValueError('Incomplete PM35 frame')
|
|
if crc16(raw[:-2], True) != number(raw[-2:]):
|
|
raise ValueError('PM35 CRC mismatch')
|
|
address, function = raw[:2]
|
|
result = dict(protocol='PM35', controller=address, function=function, raw=raw)
|
|
if function in (0x83, 0x86):
|
|
if len(raw) != 5 or role == 'request':
|
|
raise ValueError('Invalid exception response')
|
|
result['summary'] = 'PM35 node=%d exception fn=%02X code=%d' % (address, function, raw[2])
|
|
elif function == 6 or function == 3 and role == 'request':
|
|
if len(raw) != 8:
|
|
raise ValueError('PM35 request/06 must contain 8 bytes')
|
|
register, value = number(raw[2:4], 'big'), number(raw[4:6], 'big')
|
|
if register >= 128 or function == 3 and not 1 <= value <= 128 - register:
|
|
raise ValueError('PM35 register range outside 0..127')
|
|
result.update(register=register, value=value)
|
|
result['summary'] = 'PM35 node=%d %s R%d %s=%d' % (address,
|
|
'READ' if function == 3 else 'WRITE/echo', register,
|
|
'count' if function == 3 else 'value', value)
|
|
if function == 6 and register == 127:
|
|
result['summary'] += ' command_bits=%04X' % value
|
|
elif function == 3:
|
|
count = raw[2] or 256
|
|
if count % 2 or len(raw) != count + 5:
|
|
raise ValueError('Invalid PM35 response byte count')
|
|
result['values'] = [number(raw[i:i+2], 'big') for i in range(3, len(raw)-2, 2)]
|
|
result['summary'] = 'PM35 node=%d READ response %d words: %s' % (address,
|
|
len(result['values']), ' '.join('%04X' % v for v in result['values']))
|
|
else:
|
|
raise ValueError('Unsupported PM35 function')
|
|
return result
|
|
|
|
|
|
class PM35Parser:
|
|
def __init__(self, role):
|
|
self.role = role
|
|
self.buffer = []
|
|
|
|
def flush(self):
|
|
if not self.buffer:
|
|
return []
|
|
ss, es = self.buffer[0][1], self.buffer[-1][2]
|
|
self.buffer = []
|
|
return [(ss, es, None, 'Incomplete PM35 frame')]
|
|
|
|
def feed(self, value, ss, es):
|
|
self.buffer.append((value, ss, es))
|
|
events = []
|
|
while len(self.buffer) >= 3:
|
|
raw = bytes(item[0] for item in self.buffer)
|
|
fn = raw[1]
|
|
if fn not in (3, 6, 0x83, 0x86):
|
|
del self.buffer[0]
|
|
continue
|
|
role = self.role
|
|
total = 5 if fn & 128 else 8
|
|
if role == 'auto' and fn == 3:
|
|
candidates = [(8, 'request'), ((raw[2] or 256) + 5, 'response')]
|
|
found = None
|
|
for length, candidate_role in candidates:
|
|
if len(raw) >= length:
|
|
try:
|
|
found = (length, candidate_role, parse_pm35(raw[:length], candidate_role))
|
|
break
|
|
except ValueError:
|
|
pass
|
|
if found:
|
|
length, candidate_role, result = found
|
|
events.append((self.buffer[0][1], self.buffer[length-1][2], result, None))
|
|
del self.buffer[:length]
|
|
continue
|
|
if len(raw) < max(length for length, _ in candidates):
|
|
break
|
|
events.append((self.buffer[0][1], self.buffer[-1][2], None, 'Invalid PM35 frame/CRC'))
|
|
del self.buffer[0]
|
|
continue
|
|
if fn == 3 and role == 'response':
|
|
total = (raw[2] or 256) + 5
|
|
if len(raw) < total:
|
|
break
|
|
result, error = None, None
|
|
try:
|
|
result = parse_pm35(raw[:total], role)
|
|
except ValueError as exc:
|
|
error = str(exc)
|
|
events.append((self.buffer[0][1], self.buffer[total-1][2], result, error))
|
|
del self.buffer[:1 if error else total]
|
|
return events
|