From a1d4ad6ae34695c31b5856f0b1b3bc74f92cb9ca Mon Sep 17 00:00:00 2001 From: Benjamin Braatz Date: Wed, 7 Apr 2021 15:33:27 +0200 Subject: [PATCH] Implement CRC as class with byte-wise update. --- controlpi_plugins/modbus.py | 88 +++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/controlpi_plugins/modbus.py b/controlpi_plugins/modbus.py index dad43c7..f9b0f41 100644 --- a/controlpi_plugins/modbus.py +++ b/controlpi_plugins/modbus.py @@ -12,6 +12,94 @@ from controlpi import BasePlugin, Message, MessageTemplate from typing import Dict +class CRC: + """Calculate CRC for message. + + The CRC as specified for Modbus is computed using a precomputed table + for its inner loop. + + We define 12 test messages with their expected results from the manual + of the Hitachi PJ series Modbus devices: + >>> tests = [(bytes.fromhex('08 01 0006 0006'), bytes.fromhex('5C90')), + ... (bytes.fromhex('08 01 01 17'), bytes.fromhex('121A')), + ... (bytes.fromhex('05 03 03E8 0003'), bytes.fromhex('843F')), + ... (bytes.fromhex('05 03 06 0007 0000 1770'), + ... bytes.fromhex('A861')), + ... (bytes.fromhex('0A 05 0000 FF00'), bytes.fromhex('8D41')), + ... (bytes.fromhex('01 06 2F4D 1388'), bytes.fromhex('1C5F')), + ... (bytes.fromhex('05 0F 0006 0006 02 1700'), + ... bytes.fromhex('DB3E')), + ... (bytes.fromhex('05 0F 0006 0006'), bytes.fromhex('344C')), + ... (bytes.fromhex('01 10 2B01 0002 04 0004 93E0'), + ... bytes.fromhex('F42B')), # error in manual + ... (bytes.fromhex('01 10 2B01 0002'), + ... bytes.fromhex('19EC')), # error in manual + ... (bytes.fromhex('01 17 2710 0002 2AF8 0002 04 0000 1388'), + ... bytes.fromhex('964D')), # error in manual + ... (bytes.fromhex('01 17 04 0000 1388'), + ... bytes.fromhex('F471'))] + + Now, we test the crc function against this list: + >>> crc_zero = bytes.fromhex('0000') + >>> for (message, expected) in tests: + ... crc = CRC(message) + ... assert bytes(crc) == expected + ... assert not bool(crc) + ... for byte in bytes(crc): + ... crc.update(byte) + ... assert bool(crc) + """ + + def __init__(self, message: bytes = b'') -> None: + self._crc = 0xFFFF + self._message = b'' + for byte in message: + self.update(byte) + + def update(self, byte: int) -> None: + """Update CRC with one additional byte.""" + if not self._precomputed: + print("Precomputing CRC values.") + self.precompute() + self._message += bytes([byte]) + self._crc ^= byte + key = self._crc & 0xFF + self._crc >>= 8 + self._crc ^= self._precomputed[key] + + _precomputed: Dict[int, int] = {} + + @classmethod + def precompute(cls) -> None: + """Precompute inner loop of CRC algorithm.""" + for key in range(256): + value = key + for bit in range(8): + lsb = value & 1 + value >>= 1 + if lsb: + value ^= 0xA001 + cls._precomputed[key] = value + + def __repr__(self) -> str: + return f"CRC(bytes.fromhex({self._message.hex().upper()}))" + + def __str__(self) -> str: + return f"crc({self._message.hex().upper()}) = 0x{self._crc:04X}" + + def __bool__(self) -> bool: + return self._crc == 0 + + def __int__(self) -> int: + return self._crc + + def __bytes__(self) -> bytes: + return bytes((self._crc & 0xFF, self._crc >> 8)) + + +CRC.precompute() + + def crc(message: bytes) -> bytes: """Calculate CRC for message. -- 2.34.1