Skip to content

Coding

softlora.coding

calc_lora_crc16(payload)

Recompute the LoRa CRC-16 (CCITT-16, poly 0x1021) for a payload.

Returns the two CRC bytes that must accompany payload for the decoder's CRC check to pass.

Parameters:

Name Type Description Default
payload array_like

Payload bytes (without the CRC).

required

Returns:

Type Description
ndarray

The 2 CRC bytes (uint8).

Source code in softlora/coding.py
def calc_lora_crc16(payload):
    """Recompute the LoRa CRC-16 (CCITT-16, poly 0x1021) for a payload.

    Returns the two CRC bytes that must accompany ``payload`` for the
    decoder's CRC check to pass.

    Parameters
    ----------
    payload : array_like
        Payload bytes (without the CRC).

    Returns
    -------
    ndarray
        The 2 CRC bytes (uint8).
    """
    if len(payload) < 2:
        return np.array([0, 0], dtype=np.uint8)
    crc = 0x0000
    for byte in payload[:-2]:
        b = int(byte)
        for _ in range(8):
            if ((crc & 0x8000) >> 8) ^ (b & 0x80):
                crc = ((crc << 1) ^ 0x1021) & 0xFFFF
            else:
                crc = (crc << 1) & 0xFFFF
            b <<= 1
    crc = crc ^ int(payload[-1]) ^ (int(payload[-2]) << 8)
    return np.array([crc & 0xFF, (crc >> 8) & 0xFF], dtype=np.uint8)

calc_payload_sym_num(payload_len, has_crc, sf, cr, ldro=False, impl_header=False)

Number of symbols a packet of payload_len bytes occupies.

Includes the 8 header symbols (explicit header) or the in-header data symbols (implicit header), plus the cr + 4-symbol coding blocks.

Parameters:

Name Type Description Default
payload_len int

Payload length in bytes.

required
has_crc bool

Whether the packet carries a 2-byte CRC.

required
sf int

Spreading factor.

required
cr int

Code rate (1-4).

required
ldro bool

Low-data-rate optimization.

False
impl_header bool

Implicit header (no header transmitted).

False

Returns:

Type Description
int

Total number of symbols.

Source code in softlora/coding.py
def calc_payload_sym_num(payload_len, has_crc, sf, cr, ldro=False, impl_header=False):
    """Number of symbols a packet of ``payload_len`` bytes occupies.

    Includes the 8 header symbols (explicit header) or the in-header data
    symbols (implicit header), plus the ``cr + 4``-symbol coding blocks.

    Parameters
    ----------
    payload_len : int
        Payload length in bytes.
    has_crc : bool
        Whether the packet carries a 2-byte CRC.
    sf : int
        Spreading factor.
    cr : int
        Code rate (1-4).
    ldro : bool
        Low-data-rate optimization.
    impl_header : bool
        Implicit header (no header transmitted).

    Returns
    -------
    int
        Total number of symbols.
    """
    total_bytes = payload_len + (2 if has_crc else 0)
    total_nibbles = total_bytes * 2
    # The explicit header decodes to ``sf - 2`` Hamming nibbles, of which 5
    # carry the header fields and the remaining ``sf - 7`` are the first data
    # nibbles (a fixed ``3`` only holds for SF=10).  Implicit headers carry
    # the whole payload in the data region.
    header_data_nibbles = (sf - 2) if impl_header else max(0, sf - 7)
    sf_eff = (sf - 2) if ldro else sf
    rdd = cr + 4
    remaining = max(0, total_nibbles - header_data_nibbles)
    blocks = (remaining + sf_eff - 1) // sf_eff
    return 8 + blocks * rdd

decode(data_symbols, sf=10, impl_header=False, forced_payload_len=None, forced_has_crc=True, forced_cr=1)

Decode a full payload from its symbol bins.

Handles both explicit headers (the first 8 symbols carry length / CRC / code rate) and implicit headers (forced parameters). Returns the payload and CRC bytes together with the decoded header information.

Parameters:

Name Type Description Default
data_symbols array_like

Demodulated symbol bins (header + payload).

required
sf int

Spreading factor.

10
impl_header bool

Implicit header mode.

False
forced_payload_len int or None

Payload length when impl_header is True.

None
forced_has_crc bool

CRC presence when impl_header is True.

True
forced_cr int

Code rate when impl_header is True.

1

Returns:

Name Type Description
payload ndarray

Decoded payload bytes (uint8).

crc_bytes ndarray

The 2 CRC bytes (empty when the packet has no CRC).

info dict

{'payload_len': ..., 'has_crc': ..., 'cr': ...}.

Source code in softlora/coding.py
def decode(data_symbols, sf=10, impl_header=False,
           forced_payload_len=None, forced_has_crc=True, forced_cr=1):
    """Decode a full payload from its symbol bins.

    Handles both explicit headers (the first 8 symbols carry length / CRC /
    code rate) and implicit headers (forced parameters).  Returns the payload
    and CRC bytes together with the decoded header information.

    Parameters
    ----------
    data_symbols : array_like
        Demodulated symbol bins (header + payload).
    sf : int
        Spreading factor.
    impl_header : bool
        Implicit header mode.
    forced_payload_len : int or None
        Payload length when ``impl_header`` is True.
    forced_has_crc : bool
        CRC presence when ``impl_header`` is True.
    forced_cr : int
        Code rate when ``impl_header`` is True.

    Returns
    -------
    payload : ndarray
        Decoded payload bytes (uint8).
    crc_bytes : ndarray
        The 2 CRC bytes (empty when the packet has no CRC).
    info : dict
        ``{'payload_len': ..., 'has_crc': ..., 'cr': ...}``.
    """
    if impl_header:
        if forced_payload_len is None or forced_payload_len < 1 or forced_payload_len > 255:
            raise ValueError(f'Invalid forced_payload_len={forced_payload_len}')
        if forced_cr < 1 or forced_cr > 4:
            raise ValueError(f'Invalid forced_cr={forced_cr}')
        first_nibbles = decode_symbols(data_symbols[:8], sf, cr=1, is_header=True)
        data_nibbles = list(first_nibbles)
        payload_len = forced_payload_len
        has_crc = forced_has_crc
        cr = forced_cr
    else:
        payload_len, has_crc, cr, hdr_nib = decode_header(data_symbols[:8], sf)
        data_nibbles = list(hdr_nib[5:])

    rdd = cr + 4
    for i in range(0, len(data_symbols[8:]) - rdd + 1, rdd):
        block = data_symbols[8+i:8+i+rdd]
        nibs = decode_symbols(block, sf, cr, is_header=False)
        data_nibbles.extend(nibs)
    return dewhiten(np.array(data_nibbles, dtype=np.uint8), payload_len, has_crc) + ({'payload_len': payload_len, 'has_crc': has_crc, 'cr': cr},)

decode_header(hdr_syms, sf)

Decode the 8-symbol explicit header.

Extracts the payload length, CRC presence flag and code rate. The header checksum is computed and compared but a mismatch is not warned on -- it is surfaced through the decode result (crc_valid / error text) instead.

Parameters:

Name Type Description Default
hdr_syms array_like

The 8 demodulated header symbols.

required
sf int

Spreading factor.

required

Returns:

Name Type Description
payload_len int

Declared payload length in bytes.

has_crc bool

Whether the packet carries a CRC.

cr int

Code rate (1-4).

hdr_nib ndarray

The decoded header nibbles.

Raises:

Type Description
ValueError

When the header cannot be decoded or declares an invalid length/rate.

Source code in softlora/coding.py
def decode_header(hdr_syms, sf):
    """Decode the 8-symbol explicit header.

    Extracts the payload length, CRC presence flag and code rate.  The header
    checksum is computed and compared but a mismatch is not warned on -- it is
    surfaced through the decode result (``crc_valid`` / error text) instead.

    Parameters
    ----------
    hdr_syms : array_like
        The 8 demodulated header symbols.
    sf : int
        Spreading factor.

    Returns
    -------
    payload_len : int
        Declared payload length in bytes.
    has_crc : bool
        Whether the packet carries a CRC.
    cr : int
        Code rate (1-4).
    hdr_nib : ndarray
        The decoded header nibbles.

    Raises
    ------
    ValueError
        When the header cannot be decoded or declares an invalid length/rate.
    """
    hdr_nib = decode_symbols(hdr_syms, sf, cr=1, is_header=True)
    if len(hdr_nib) < 5:
        raise ValueError('Header decode failed: insufficient nibbles')
    payload_len = int(hdr_nib[0] * 16 + hdr_nib[1])
    has_crc = bool(hdr_nib[2] & 1)
    cr = int(hdr_nib[2] >> 1)
    if payload_len > 255 or payload_len < 1 or cr < 1 or cr > 4:
        raise ValueError(
            f'Invalid header: payload_len={payload_len}, cr={cr}'
        )
    if not verify_header_checksum(hdr_nib):
        raise ValueError('Invalid header: checksum mismatch')
    return payload_len, has_crc, cr, hdr_nib

decode_symbols(symbols, sf, cr, is_header, ldro=False)

Full symbol decode: Gray -> deinterleave -> Hamming -> nibbles.

Parameters:

Name Type Description Default
symbols array_like

Demodulated symbol bins.

required
sf int

Spreading factor.

required
cr int

Code rate (1-4).

required
is_header bool

Header block (uses the 4/5 header code).

required
ldro bool

Low-data-rate optimization.

False

Returns:

Type Description
ndarray

Decoded data nibbles (uint8).

Source code in softlora/coding.py
def decode_symbols(symbols, sf, cr, is_header, ldro=False):
    """Full symbol decode: Gray -> deinterleave -> Hamming -> nibbles.

    Parameters
    ----------
    symbols : array_like
        Demodulated symbol bins.
    sf : int
        Spreading factor.
    cr : int
        Code rate (1-4).
    is_header : bool
        Header block (uses the 4/5 header code).
    ldro : bool
        Low-data-rate optimization.

    Returns
    -------
    ndarray
        Decoded data nibbles (uint8).
    """
    grayed = gray_coding_rx(symbols, sf, is_header=is_header, ldro=ldro)
    codewords = deinterleave(grayed, sf, cr, is_header, ldro)
    nibbles = hamming_decode(codewords, cr, is_header)
    return nibbles

deinterleave(symbols, sf, cr, is_header, ldro=False)

Undo the LoRa symbol interleaver for one coding block.

Parameters:

Name Type Description Default
symbols array_like

Demodulated (Gray-decoded) symbol bins for one cw_len block.

required
sf int

Spreading factor.

required
cr int

Code rate (1-4).

required
is_header bool

Header blocks use sf-2 effective bits and 8-symbol codewords.

required
ldro bool

Low-data-rate optimization.

False

Returns:

Type Description
ndarray

The deinterleaved coded bytes (one per effective symbol).

Source code in softlora/coding.py
def deinterleave(symbols, sf, cr, is_header, ldro=False):
    """Undo the LoRa symbol interleaver for one coding block.

    Parameters
    ----------
    symbols : array_like
        Demodulated (Gray-decoded) symbol bins for one ``cw_len`` block.
    sf : int
        Spreading factor.
    cr : int
        Code rate (1-4).
    is_header : bool
        Header blocks use ``sf-2`` effective bits and 8-symbol codewords.
    ldro : bool
        Low-data-rate optimization.

    Returns
    -------
    ndarray
        The deinterleaved coded bytes (one per effective symbol).
    """
    sf_app = (sf - 2) if (is_header or ldro) else sf
    cw_len = 8 if is_header else (cr + 4)
    symbols = np.asarray(symbols, dtype=np.uint16)[:cw_len]
    bits = np.zeros((cw_len, sf_app), dtype=np.uint8)
    for i in range(sf_app):
        bits[:, i] = (symbols >> (sf_app - 1 - i)) & 1
    deinter = np.zeros((sf_app, cw_len), dtype=np.uint8)
    for i in range(cw_len):
        for j in range(sf_app):
            deinter[(i - j - 1) % sf_app, i] = bits[i, j]
    out = np.zeros(sf_app, dtype=np.uint8)
    for i in range(sf_app):
        val = 0
        for b in range(cw_len):
            val = (val << 1) | int(deinter[i, b])
        out[i] = np.uint8(val)
    return out

dewhiten(nibbles, payload_len, crc_presence=False)

De-whiten decoded nibbles into payload (and CRC) bytes.

Parameters:

Name Type Description Default
nibbles array_like

Decoded data nibbles (payload region is XORed with the whitening sequence; the CRC nibbles are read raw).

required
payload_len int

Expected payload length in bytes.

required
crc_presence bool

Whether the packet carries a 2-byte CRC.

False

Returns:

Name Type Description
payload ndarray

Decoded payload bytes (uint8).

crc_bytes ndarray

The 2 CRC bytes (empty when crc_presence is False).

Raises:

Type Description
ValueError

When fewer nibbles than needed are supplied.

Source code in softlora/coding.py
def dewhiten(nibbles, payload_len, crc_presence=False):
    """De-whiten decoded nibbles into payload (and CRC) bytes.

    Parameters
    ----------
    nibbles : array_like
        Decoded data nibbles (payload region is XORed with the whitening
        sequence; the CRC nibbles are read raw).
    payload_len : int
        Expected payload length in bytes.
    crc_presence : bool
        Whether the packet carries a 2-byte CRC.

    Returns
    -------
    payload : ndarray
        Decoded payload bytes (uint8).
    crc_bytes : ndarray
        The 2 CRC bytes (empty when ``crc_presence`` is False).

    Raises
    ------
    ValueError
        When fewer nibbles than needed are supplied.
    """
    nibbles = np.asarray(nibbles, dtype=np.uint8)
    needed = 2 * (payload_len + (2 if crc_presence else 0))
    if len(nibbles) < needed:
        raise ValueError(
            f'dewhiten: need {needed} nibbles for payload_len={payload_len}, '
            f'crc={crc_presence}, but only have {len(nibbles)}'
        )
    payload = np.zeros(payload_len, dtype=np.uint8)
    for i in range(payload_len):
        ws = _WHITENING_BYTES[i]
        low = (nibbles[2*i] ^ (ws & 0x0F)) & 0x0F
        high = (nibbles[2*i+1] ^ (ws >> 4)) & 0x0F
        payload[i] = (high << 4) | low
    crc_bytes = np.zeros(0, dtype=np.uint8)
    if crc_presence:
        for j in range(2):
            i = payload_len + j
            low = nibbles[2*i] & 0x0F
            high = nibbles[2*i+1] & 0x0F
            crc_bytes = np.append(crc_bytes, np.uint8((high << 4) | low))
    return payload, crc_bytes

gray_coding_rx(symbols, sf, is_header=False, ldro=False)

Apply the LoRa Gray decode (RX side) to symbol bins.

Undoes the TX Gray code: symbol - 1 (data frames) or symbol // 4 (header/LDRO frames), then x ^ (x >> 1).

Parameters:

Name Type Description Default
symbols array_like

Demodulated symbol bins.

required
sf int

Spreading factor.

required
is_header bool

Header frames use the symbol // 4 mapping.

False
ldro bool

Low-data-rate optimization frames behave like headers.

False

Returns:

Type Description
ndarray

Gray-decoded symbol values (uint16).

Source code in softlora/coding.py
def gray_coding_rx(symbols, sf, is_header=False, ldro=False):
    """Apply the LoRa Gray decode (RX side) to symbol bins.

    Undoes the TX Gray code: ``symbol - 1`` (data frames) or ``symbol // 4``
    (header/LDRO frames), then ``x ^ (x >> 1)``.

    Parameters
    ----------
    symbols : array_like
        Demodulated symbol bins.
    sf : int
        Spreading factor.
    is_header : bool
        Header frames use the ``symbol // 4`` mapping.
    ldro : bool
        Low-data-rate optimization frames behave like headers.

    Returns
    -------
    ndarray
        Gray-decoded symbol values (uint16).
    """
    symbols = np.array(symbols, dtype=float)
    symbols = symbols % (2**sf)
    if is_header:
        symbols = np.floor(symbols / 4)
    elif ldro:
        symbols = np.floor(symbols / 4)
    else:
        symbols = (symbols - 1) % (2**sf)
    s = symbols.astype(np.uint16)
    return np.array([int(x ^ (x >> 1)) for x in s], dtype=np.uint16)

hamming_decode(codewords, cr, is_header)

Decode a block of Hamming codewords into data nibbles.

Each codeword is cr + 4 (or 8 for headers) bits wide. Single-bit errors are corrected via the syndrome; code rate 4 additionally enforces even parity over the whole codeword.

Parameters:

Name Type Description Default
codewords array_like

Deinterleaved coded bytes.

required
cr int

Code rate (1-4); headers always use 4/5.

required
is_header bool

Whether this is a header block.

required

Returns:

Type Description
ndarray

Decoded data nibbles (uint8).

Source code in softlora/coding.py
def hamming_decode(codewords, cr, is_header):
    """Decode a block of Hamming codewords into data nibbles.

    Each codeword is ``cr + 4`` (or 8 for headers) bits wide.  Single-bit
    errors are corrected via the syndrome; code rate 4 additionally enforces
    even parity over the whole codeword.

    Parameters
    ----------
    codewords : array_like
        Deinterleaved coded bytes.
    cr : int
        Code rate (1-4); headers always use 4/5.
    is_header : bool
        Whether this is a header block.

    Returns
    -------
    ndarray
        Decoded data nibbles (uint8).
    """
    cr_app = 4 if is_header else cr
    cw_len = cr_app + 4
    codewords = np.asarray(codewords, dtype=np.uint8)
    n = len(codewords)
    out = np.zeros(n, dtype=np.uint8)
    for i in range(n):
        cw = _int2bits(int(codewords[i]), cw_len)
        data_nibble = [cw[3], cw[2], cw[1], cw[0]]
        if cr_app == 4:
            if not (sum(cw) % 2):
                out[i] = _bits2int(data_nibble)
                continue
            s0 = cw[0] ^ cw[1] ^ cw[2] ^ cw[4]
            s1 = cw[1] ^ cw[2] ^ cw[3] ^ cw[5]
            s2 = cw[0] ^ cw[1] ^ cw[3] ^ cw[6]
            _apply_syndrome(data_nibble, int(s0) | (int(s1) << 1) | (int(s2) << 2))
        elif cr_app == 3:
            s0 = cw[0] ^ cw[1] ^ cw[2] ^ cw[4]
            s1 = cw[1] ^ cw[2] ^ cw[3] ^ cw[5]
            s2 = cw[0] ^ cw[1] ^ cw[3] ^ cw[6]
            _apply_syndrome(data_nibble, int(s0) | (int(s1) << 1) | (int(s2) << 2))
        out[i] = _bits2int(data_nibble)
    return out

nibbles_to_bytes(nibbles)

Pack nibbles into bytes (low nibble first).

Parameters:

Name Type Description Default
nibbles array_like

Data nibbles.

required

Returns:

Type Description
ndarray

Packed bytes (uint8).

Source code in softlora/coding.py
def nibbles_to_bytes(nibbles):
    """Pack nibbles into bytes (low nibble first).

    Parameters
    ----------
    nibbles : array_like
        Data nibbles.

    Returns
    -------
    ndarray
        Packed bytes (uint8).
    """
    num_bytes = min(255, len(nibbles) // 2)
    out = np.zeros(num_bytes, dtype=np.uint8)
    for i in range(num_bytes):
        out[i] = nibbles[2*i] | (nibbles[2*i+1] << 4)
    return out

verify_header_checksum(hdr_nib)

True when the 5-bit explicit-header checksum matches the header.

Source code in softlora/coding.py
def verify_header_checksum(hdr_nib):
    """True when the 5-bit explicit-header checksum matches the header."""
    n0, n1, n2 = int(hdr_nib[0]), int(hdr_nib[1]), int(hdr_nib[2])

    def b(v, i):
        return (v >> i) & 1

    calc = [
        b(n0, 3) ^ b(n0, 2) ^ b(n0, 1) ^ b(n0, 0),
        b(n0, 3) ^ b(n1, 3) ^ b(n1, 2) ^ b(n1, 1) ^ b(n2, 0),
        b(n0, 2) ^ b(n1, 3) ^ b(n1, 0) ^ b(n2, 3) ^ b(n2, 1),
        b(n0, 1) ^ b(n1, 2) ^ b(n1, 0) ^ b(n2, 2) ^ b(n2, 1) ^ b(n2, 0),
        b(n0, 0) ^ b(n1, 1) ^ b(n2, 3) ^ b(n2, 2) ^ b(n2, 1) ^ b(n2, 0),
    ]
    rx = [int(hdr_nib[3]) & 1] + [(int(hdr_nib[4]) >> (3 - i)) & 1
                                  for i in range(4)]
    return calc == rx