feat(framing): framing done
refactor(framing): re-name hamming codec to 7,4 to reflect algorithm used
This commit is contained in:
46
framing.py
Normal file
46
framing.py
Normal 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
|
||||||
@@ -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}")
|
||||||
|
|||||||
47
header.py
47
header.py
@@ -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]
|
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
Reference in New Issue
Block a user