Source code for utils.crc16_wmbus

"""
CRC16 for wm-bus
****************

:synopsis: CRC16 calculator for EN 13757
:author: Janus Bo Andersen
:date: October 2020

Overview:
---------

- This function performs the CRC16 algorithm.
- The result can be used to confirm data integrity of received payload in a wm-bus message.
- Wm-bus follows the CRC16 standard outlined in EN 13757.

A `CrcCheckException` class is also implemented, which is used to raise exceptions
if a CRC check fails.

The IM871-A transceiver removes the outer CRC16 (last two bytes of a message)
and replaces it with its own, which follows another standard, CRC16-CCITT.
So the outer CRC16 can not be checked with the function in this module.

CRC16 EN 13757:
---------------

- CRC16 uses a generator polynomial, g(x), described in EN 13757-4.
- See p. 42 for data-link layer CRC, and an example with a C1 telegram on p. 84.
- See p. 58 for transport layer CRC polynomial.

g(x) = x^16 + x^13 + x^12 + x^11 + x^10 + x^8 + x^6 + x^5 + x^2 + 1

In binary (excluding x^16 as it is shifted out anyway), this g(x) is represented as

+-------------+-----------------+-----------+
| Byte 1      | Byte 2          | Hex value |
+=============+=================+===========+
|0011 1101    |0110 0101        | 0x3D65    |
+-------------+-----------------+-----------+
|MSbit = x^15 | LSbit = x^0 = 1 |           |
+-------------+-----------------+-----------+

See EN 13757-4, table 43, p. 50 for expected structure of ELL for a CI=0x8D telegram.
PayloadCRC is included in the encrypted part of telegram.

Algorithm rules:
----------------

- Treats data most-significant bit first
- Final CRC shall be complemented
- Multi-byte data is transmitted LSB first
- CRC is transmitted MSB first

Math background:
----------------

- CRC uses a finite field F=[0, 1], so we do subtraction using XOR.
- CRC is the final remainder from repeated long division of message by polynomial,\
when no further division is possible.
- The output CRC is complemented by XOR with 0xFFFF.

Algorithm implementation comments:
----------------------------------
The implemented algorithm uses Python's ability for 'infinite' width of integers.
That is slightly inefficient, and can't be ported to C code on an embedded device.
But it is significantly easier to debug and understand than byte-wise algorithms or lookup tables.

"""

from binascii import hexlify
from struct import pack


[docs]def crc16_wmbus(message: bytes) -> bytes: """ Takes a bytes object with a message (ascii encoded hex values). Returns the CRC16 value for the message encoded in a bytes object. Example: f(b'79138C4491CE000000000000000300000000000000') -> b'1170'. """ crc_bits = 16 hex_radix = 16 g = 0x3d65 # Generator polynomial, g(x) m = int(message, hex_radix) << crc_bits # Message, m(x), shifted to make space for 16-bit CRC crc = m # Start with m(x)<<16 (initial crc=remainder is 0x0000) m_bitlen = len(bin(m)[2:]) # Hacky method to get number of bits req. to represent m(x) g_bitlen = crc_bits # Poly is 16 bits # Loop over each bit, from highest to lowest # Continue while remainder is larger than polynomial (i.e. still divisions to perform) for n in range(m_bitlen - 1, g_bitlen - 1, -1): # Step 1 Check if most significant bit is 1 if crc & (1 << n): # If yes, perform division and subtract to get remainder (XOR) g_shift = g << (n - g_bitlen) # Shift polynomial crc = (crc ^ g_shift) % 2**n # mod 2^n is to emulate << 1 (but Py doesn't shift out to the left) else: # If not, move on to next pass # Repeat # Perform final complement crc = crc ^ 0xFFFF # Return as little-endian 16-bit to match how CRC16's are stored in telegrams crc_hex = hexlify(pack('<H', crc)) return crc_hex
[docs]def crc16_check(payload: bytes) -> bool: """ Takes a payload and splits into CRC16-field and message. Computes CRC16 on the message and compares to CRC16-field. Return True if match. Raises CrcCheckException if no match. """ crc16_recv = payload[0:4] crc16_calc = crc16_wmbus(payload[4:]) # Perform comparison on lowercase, just in case something is UPPER'ed # Raise an exception if CRC check fails if crc16_recv.lower() == crc16_calc.lower(): return True else: raise CrcCheckException(crc16_recv, crc16_calc, "CRC check fail. No match.")
[docs]class CrcCheckException(Exception): """ Use this to raise an exception when a CRC16 check has failed. """ def __init__(self, crc_recv: bytes, crc_calc: bytes, exception_message: str): self.crc_recv = crc_recv self.crc_calc = crc_calc self.exception_message = exception_message # Invoke constructor for base class super().__init__(self.exception_message) def __str__(self) -> str: # String representation of exception if printed return "CRC received ({}) does not match CRC calculated ({}).".format(self.crc_recv.decode(), self.crc_calc.decode())
if __name__ == '__main__': # Self-test if run as main from command line # Example from p. 84 expected_crc = b'c57a' # Reverse order of example data = b'1444AE0C7856341201078C2027780B13436587' assert crc16_wmbus(data) == expected_crc # From actual telegram, payload CRC expected_crc = b'bb52' data = b'79138C7976CE000000000000000400000000000000' assert crc16_wmbus(data) == expected_crc # Another example from captured real OmniPower telegram expected_crc = b'1170' data = b'79138C4491CE000000000000000300000000000000' assert crc16_wmbus(data) == expected_crc # Telegram with data record headers expected_crc = b'0fe6' data = b'780404CE00000004843C00000000042B0300000004AB3C00000000' assert crc16_wmbus(data) == expected_crc