"""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