feat(framing): framing done

refactor(framing): re-name hamming codec to 7,4 to reflect algorithm used
This commit is contained in:
2025-11-30 13:44:41 +01:00
parent 0489d8fc31
commit 5bf5bfec5d
4 changed files with 77 additions and 75 deletions

46
framing.py Normal file
View File

@@ -0,0 +1,46 @@
from header import Onbeat_Header, PROTOCOL_IDENTIFIER
from reedsolo import RSCodec
import numpy as np
PROTOCOL_V1_NOFEC = 1
PROTOCOL_V1_FEC = 2
RSC = RSCodec(255-223,255)
def decode_packet(packet: list[int]) -> tuple[Onbeat_Header, list[int]]:
if len(packet) < 32:
raise OverflowError(f"Packet is too small to be valid, len: {len(packet)}")
header_raw = packet[0:32]
header = Onbeat_Header(0, 0, "", 0, 0)
if header.decode(header_raw) != True:
raise RuntimeError("invalid CRC")
if header.protocol_id != PROTOCOL_IDENTIFIER:
raise ValueError(f"Protocol ID mismatch, got{header.protocol_id}, expected {PROTOCOL_IDENTIFIER}")
if header.pkt_len > len(packet) - 32:
raise OverflowError(f"Header not fully captured, len:{header.pkt_len}, maximal: {len(packet) - 32}")
elif header.pkt_len == 0:
return header, []
payload = packet[32:32+header.pkt_len]
if header.protocol_configuration == PROTOCOL_V1_FEC:
payload_decoded = np.empty((4, min(len(payload)//4-32, 255)), dtype=int)
for i in range(0,4):
payload_decoded[i] = RSC.decode(bytearray(payload[i::4]))[0]
payload = payload_decoded.flatten(order='F').tolist()
elif header.protocol_configuration == PROTOCOL_V1_NOFEC:
pass
else:
raise ValueError(f"Unknown protocol configuration: {header.protocol_configuration}")
return header, payload
def encode_packet(payload: list[int], callsign: str, sequence: int, rs_coding: bool = False) -> list[int]:
protocol_configuration = PROTOCOL_V1_NOFEC
if rs_coding == True:
protocol_configuration = PROTOCOL_V1_FEC
if len(payload) != 0:
for _ in range(0, len(payload)%4):
payload.append(0)
payload_encoded = np.empty((4, min((len(payload))//4+32, 255)), dtype=int)
for i in range(0,4):
payload_encoded[i] = RSC.encode(bytearray(payload[i::4]))
payload = payload_encoded.flatten(order='F').tolist()
header = Onbeat_Header(0, protocol_configuration, callsign, len(payload), sequence)
return header.encode() + payload

View File

@@ -19,7 +19,7 @@ DECODER_MATRIX = np.asarray([(0, 0, 1, 0, 0, 0, 0,),
def number_to_list(number: int, N: int) -> npt.NDArray: def number_to_list(number: int, N: int) -> npt.NDArray:
"""Return the last N bits of a number as an MSB-first array""" """Return the last N bits of a number as an MSB-first array"""
return np.array([(number >> (N-1-i)) % 2 for i in range(0, N)]) return np.array([(number >> (N-1-i)) % 2 for i in range(0, N)],dtype=int)
def list_to_number(array: npt.NDArray, N: int) -> int: def list_to_number(array: npt.NDArray, N: int) -> int:
@@ -54,7 +54,8 @@ def decode_nibble(data: int) -> int:
syndrome = (PARITY_MATRIX @ data_asarr) % 2 syndrome = (PARITY_MATRIX @ data_asarr) % 2
error = list_to_number(syndrome, 3) error = list_to_number(syndrome, 3)
if error == 0: if error == 0:
return list_to_number(data_asarr, 4) data_decoded = (DECODER_MATRIX @ data_asarr) % 2
return list_to_number(data_decoded, 4)
data_asarr[error - 1] ^= 1 data_asarr[error - 1] ^= 1
data_decoded = (DECODER_MATRIX @ data_asarr) % 2 data_decoded = (DECODER_MATRIX @ data_asarr) % 2
return list_to_number(data_decoded, 4) return list_to_number(data_decoded, 4)
@@ -86,12 +87,10 @@ if __name__ == "__main__":
data_corrected_2bit = hamming_7_4_decode(data_corrupted_2bit) data_corrected_2bit = hamming_7_4_decode(data_corrupted_2bit)
data_corrected_2bit_str = [int.to_bytes(int(i), 1, "big").decode( data_corrected_2bit_str = [int.to_bytes(int(i), 1, "big").decode(
encoding="utf-8", errors="ignore") for i in data_corrected_2bit] encoding="utf-8", errors="ignore") for i in data_corrected_2bit]
print(f"Recovered data from 1-bit errors: {data_corrected_2bit_str}") print(f"Recovered data from 2-bit errors: {data_corrected_2bit_str}")
data_corrupted_3bit = [data ^ (7 << (idx % 7))
for idx, data in enumerate(data_encoded)]
# print(f"Corrupt data with 3-bit errors: {data_corrupted_3bit}") # print(f"Corrupt data with 3-bit errors: {data_corrupted_3bit}")
data_corrected_3bit = hamming_7_4_decode(data_corrupted_3bit) data_corrected_noerr = hamming_7_4_decode(data_encoded)
data_corrected_3bit_str = [int.to_bytes(int(i), 1, "big").decode( data_corrected_noerr_str = [int.to_bytes(int(i), 1, "big").decode(
encoding="utf-8", errors="ignore") for i in data_corrected_3bit] encoding="utf-8", errors="ignore") for i in data_corrected_noerr]
print(f"Recovered data from 1-bit errors: {data_corrected_3bit_str}") print(f"Recovered data from no errors: {data_corrected_noerr_str}")

View File

@@ -1,12 +1,12 @@
from hamming_8_4_codec import hamming_8_4_encode, hamming_8_4_decode from hamming_7_4_codec import hamming_7_4_encode, hamming_7_4_decode
from crc import Configuration, Crc16 from crc import Calculator, Crc16
PROTOCOL_IDENTIFIER = 0x0 PROTOCOL_IDENTIFIER = 0x0
# CRC_POLY = (0xed2f << 1) + 1 # from https://users.ece.cmu.edu/~koopman/crc/index.html as "best 16-bit CRC" on 2025-10-24 # CRC_POLY = (0xed2f << 1) + 1 # from https://users.ece.cmu.edu/~koopman/crc/index.html as "best 16-bit CRC" on 2025-10-24
ONBEAT_CRC = Configuration(Crc16.IBM_3740) ONBEAT_CRC = Calculator(Crc16.IBM_3740)
class Onbeat_Header: class Onbeat_Header:
"""This class represents a header for a single packet of PLSTV.""" """This class represents a header for a single packet of ONBEAT."""
def __init__(self, protocol_id, protocol_configuration: int, callsign: str, pkt_len: int, pkt_sequence_id: int): def __init__(self, protocol_id, protocol_configuration: int, callsign: str, pkt_len: int, pkt_sequence_id: int):
if protocol_id >= 2 << 4: if protocol_id >= 2 << 4:
@@ -20,9 +20,11 @@ class Onbeat_Header:
self.protocol_configuration = protocol_configuration self.protocol_configuration = protocol_configuration
call_len = len(callsign.encode()) call_len = len(callsign.encode())
if call_len >= 10: if call_len > 10:
raise OverflowError( raise OverflowError(
f"Callsign must be confined to 10 bytes, got {callsign} with length {call_len} (using UTF-8)") f"Callsign must be confined to 10 bytes, got {callsign} with length {call_len} (using UTF-8)")
for _ in range(call_len, 10):
callsign += "\0"
self.callsign = callsign self.callsign = callsign
if pkt_len >= 2 << 16: if pkt_len >= 2 << 16:
@@ -30,7 +32,7 @@ class Onbeat_Header:
f"Maximum allowed packet size is {2 << 16 - 1} got {pkt_len}") f"Maximum allowed packet size is {2 << 16 - 1} got {pkt_len}")
self.pkt_len = pkt_len self.pkt_len = pkt_len
if pkt_sequence_id >= 2 << 8: if pkt_sequence_id >= 256:
raise OverflowError( raise OverflowError(
f"Packet sequence ID must be confined to 8 bits, got {pkt_sequence_id}") f"Packet sequence ID must be confined to 8 bits, got {pkt_sequence_id}")
self.pkt_sequence_id = pkt_sequence_id self.pkt_sequence_id = pkt_sequence_id
@@ -39,27 +41,18 @@ class Onbeat_Header:
header_asints = [] header_asints = []
header_asints += [(PROTOCOL_IDENTIFIER << 4) + header_asints += [(PROTOCOL_IDENTIFIER << 4) +
self.protocol_configuration] self.protocol_configuration]
header_asints += [(int.from_bytes(self.callsign.encode()) header_asints += [ord(c) for c in self.callsign]
>> 8*i) % 2 << 8 for i in range(0, 10)] header_asints += [(self.pkt_len >> 8), self.pkt_len % 256]
header_asints += [(self.pkt_len >> 8), self.pkt_len % 2 << 8]
header_asints += [self.pkt_sequence_id] header_asints += [self.pkt_sequence_id]
header_crc = ONBEAT_CRC.checksum(bytes(header_asints)) header_crc = ONBEAT_CRC.checksum(bytes(header_asints))
header_asints += [header_crc] header_asints += [header_crc >> 8, header_crc % 256]
return hamming_8_4_encode(header_asints) return hamming_7_4_encode(header_asints)
def decode(self, header_encoded: list[int]): def decode(self, header_encoded: list[int]) -> bool:
header_corrected = hamming_8_4_decode(header_encoded) header_corrected = hamming_7_4_decode(header_encoded)
checksum = ONBEAT_CRC.checksum( self.protocol_id = int(header_corrected[0] >> 4)
bytes(header_corrected[14] << 8 + header_corrected[15])) self.protocol_configuration = int(header_corrected[0] % 16)
if checksum != 0: self.callsign = bytes(header_corrected[1:11]).decode(errors="ignore")
raise ValueError( self.pkt_len = int((header_corrected[11] << 8) + header_corrected[12])
"Checksum of header is non-zero, packet is invalid") self.pkt_sequence_id = int(header_corrected[13])
self.protocol_id = header_corrected[0] >> 4 return ONBEAT_CRC.verify(bytearray(header_corrected[0:14]), (header_corrected[14] << 8) + header_corrected[15])
self.protocol_configuration = header_corrected[0] % 16
callsign_numeric = 0
for i in range(1, 11):
callsign_numeric += header_corrected[i]
callsign_numeric <<= 8
self.callsign = int.to_bytes(callsign_numeric, byteorder="big").decode()
self.pkt_len = header_corrected[11] << 8 + header_corrected[12]
self.pkt_sequence_id = header_corrected[13]

View File

@@ -90,7 +90,7 @@ blocks:
key: pmt.intern("packet_len") key: pmt.intern("packet_len")
offset: '0' offset: '0'
src: pmt.intern("src") src: pmt.intern("src")
value: pmt.from_long(64) value: pmt.from_long(8)
states: states:
bus_sink: false bus_sink: false
bus_source: false bus_source: false
@@ -252,23 +252,6 @@ blocks:
coordinate: [272, 504.0] coordinate: [272, 504.0]
rotation: 0 rotation: 0
state: disabled state: disabled
- name: data_len
id: parameter
parameters:
alias: ''
comment: ''
hide: none
label: ''
short_id: ''
type: ''
value: '1024'
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [976, 24.0]
rotation: 0
state: enabled
- name: digital_constellation_modulator_0 - name: digital_constellation_modulator_0
id: digital_constellation_modulator id: digital_constellation_modulator
parameters: parameters:
@@ -288,7 +271,7 @@ blocks:
bus_sink: false bus_sink: false
bus_source: false bus_source: false
bus_structure: null bus_structure: null
coordinate: [1088, 296.0] coordinate: [1096, 312.0]
rotation: 0 rotation: 0
state: enabled state: enabled
- name: pad_sink_0 - name: pad_sink_0
@@ -306,25 +289,7 @@ blocks:
bus_sink: false bus_sink: false
bus_source: false bus_source: false
bus_structure: null bus_structure: null
coordinate: [1352, 320.0] coordinate: [1352, 336.0]
rotation: 0
state: enabled
- name: pad_sink_0_0
id: pad_sink
parameters:
affinity: ''
alias: ''
comment: ''
label: dbg
num_streams: '1'
optional: 'True'
type: byte
vlen: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [576, 136.0]
rotation: 0 rotation: 0
state: enabled state: enabled
- name: pad_source_0 - name: pad_source_0
@@ -386,7 +351,6 @@ connections:
- [blocks_repack_bits_bb_1_0, '0', blocks_tagged_stream_mux_0, '1'] - [blocks_repack_bits_bb_1_0, '0', blocks_tagged_stream_mux_0, '1']
- [blocks_tagged_stream_mux_0, '0', digital_constellation_modulator_0, '0'] - [blocks_tagged_stream_mux_0, '0', digital_constellation_modulator_0, '0']
- [blocks_vector_source_x_0_0, '0', blocks_tagged_stream_mux_0, '0'] - [blocks_vector_source_x_0_0, '0', blocks_tagged_stream_mux_0, '0']
- [blocks_vector_source_x_0_0, '0', pad_sink_0_0, '0']
- [blocks_vector_source_x_0_0_0, '0', blocks_repack_bits_bb_1_0, '0'] - [blocks_vector_source_x_0_0_0, '0', blocks_repack_bits_bb_1_0, '0']
- [blocks_vector_source_x_0_0_0_0, '0', blocks_repack_bits_bb_1_0_0, '0'] - [blocks_vector_source_x_0_0_0_0, '0', blocks_repack_bits_bb_1_0_0, '0']
- [digital_constellation_modulator_0, '0', pad_sink_0, '0'] - [digital_constellation_modulator_0, '0', pad_sink_0, '0']