acoustic_ofdm/
packet.rs

1// Copyright (c) 2026 Elias S. G. Carotti
2
3use crate::config::{FecMode, Modulation, OfdmConfig};
4use crate::crc::crc16_ccitt;
5
6#[derive(Clone, Debug)]
7pub struct PacketInfo {
8    /// Packet format version from the serialized header.
9    pub version: u8,
10    /// On-wire modulation identifier from the packet header.
11    pub mod_id: u8,
12    /// Session identifier used to group fragments from the same transmission.
13    pub session_id: u16,
14    /// Zero-based fragment index within the session payload.
15    pub frag_index: u8,
16    /// Total number of fragments in the session payload.
17    pub frag_count: u8,
18    /// Application payload bytes carried by this fragment.
19    pub payload: Vec<u8>,
20}
21
22/// Splits payload bytes into fixed-size chunks.
23///
24/// Parameters:
25/// - `payload`: full payload bytes.
26/// - `chunk_size`: max bytes per chunk.
27/// Returns:
28/// - `Vec<Vec<u8>>`: ordered chunks.
29pub fn split_payload(payload: &[u8], chunk_size: usize) -> Vec<Vec<u8>> {
30    payload
31        .chunks(chunk_size)
32        .map(|c| c.to_vec())
33        .collect::<Vec<_>>()
34}
35
36/// Builds one packet (header + payload + CRC).
37///
38/// Parameters:
39/// - `payload`: fragment payload bytes.
40/// - `frag_index`: zero-based fragment index.
41/// - `frag_count`: total fragment count.
42/// - `cfg`: modem configuration (session/modulation fields used).
43/// Returns:
44/// - `Vec<u8>`: serialized packet bytes.
45pub fn build_packet_bytes(
46    payload: &[u8],
47    frag_index: u8,
48    frag_count: u8,
49    cfg: &OfdmConfig,
50) -> Vec<u8> {
51    let mut body = Vec::with_capacity(11 + payload.len());
52    body.extend_from_slice(&[0xA5, 0x5A]);
53    body.push(1); // version
54    body.push(cfg.modulation.mod_id());
55    body.extend_from_slice(&cfg.session_id.to_be_bytes());
56    body.push(frag_index);
57    body.push(frag_count);
58    body.push(payload.len() as u8);
59    body.extend_from_slice(payload);
60    let crc = crc16_ccitt(&body);
61    body.extend_from_slice(&crc.to_be_bytes());
62    body
63}
64
65/// Parses and validates one packet from a byte stream prefix.
66///
67/// Parameters:
68/// - `rx`: received byte slice.
69/// Returns:
70/// - `Option<(PacketInfo, usize)>`: parsed packet and consumed byte count, or `None` if invalid.
71pub fn parse_packet_bytes(rx: &[u8]) -> Option<(PacketInfo, usize)> {
72    if rx.len() < 11 || rx[0] != 0xA5 || rx[1] != 0x5A {
73        return None;
74    }
75    let plen = rx[8] as usize;
76    let total = 9 + plen + 2;
77    if rx.len() < total {
78        return None;
79    }
80    let body = &rx[..9 + plen];
81    let rx_crc = u16::from_be_bytes([rx[9 + plen], rx[10 + plen]]);
82    if crc16_ccitt(body) != rx_crc {
83        return None;
84    }
85    Some((
86        PacketInfo {
87            version: rx[2],
88            mod_id: rx[3],
89            session_id: u16::from_be_bytes([rx[4], rx[5]]),
90            frag_index: rx[6],
91            frag_count: rx[7],
92            payload: rx[9..9 + plen].to_vec(),
93        },
94        total,
95    ))
96}
97
98#[derive(Clone, Debug)]
99pub struct PacketParseAttempt {
100    /// Whether the leading sync/preamble bytes matched `0xA5 0x5A`.
101    pub preamble_ok: bool,
102    /// Whether enough bytes were available to read the fixed-size header.
103    pub enough_for_header: bool,
104    /// Parsed payload length from the header when available.
105    pub payload_len: Option<usize>,
106    /// Total packet length implied by the header when available.
107    pub total_len: Option<usize>,
108    /// Whether the received byte stream was long enough for the full packet.
109    pub enough_for_total: bool,
110    /// Whether the packet CRC matched.
111    pub crc_ok: bool,
112    /// Fully parsed packet when both header and CRC checks succeeded.
113    pub parsed: Option<PacketInfo>,
114}
115
116pub fn inspect_packet_bytes(rx: &[u8]) -> PacketParseAttempt {
117    if rx.len() < 9 {
118        return PacketParseAttempt {
119            preamble_ok: rx.len() >= 2 && rx[0] == 0xA5 && rx[1] == 0x5A,
120            enough_for_header: false,
121            payload_len: None,
122            total_len: None,
123            enough_for_total: false,
124            crc_ok: false,
125            parsed: None,
126        };
127    }
128
129    let preamble_ok = rx[0] == 0xA5 && rx[1] == 0x5A;
130    let plen = rx[8] as usize;
131    let total = 9 + plen + 2;
132    let enough_for_total = rx.len() >= total;
133    let crc_ok = if preamble_ok && enough_for_total {
134        let body = &rx[..9 + plen];
135        let rx_crc = u16::from_be_bytes([rx[9 + plen], rx[10 + plen]]);
136        crc16_ccitt(body) == rx_crc
137    } else {
138        false
139    };
140    let parsed = if preamble_ok && crc_ok {
141        Some(PacketInfo {
142            version: rx[2],
143            mod_id: rx[3],
144            session_id: u16::from_be_bytes([rx[4], rx[5]]),
145            frag_index: rx[6],
146            frag_count: rx[7],
147            payload: rx[9..9 + plen].to_vec(),
148        })
149    } else {
150        None
151    };
152    PacketParseAttempt {
153        preamble_ok,
154        enough_for_header: true,
155        payload_len: Some(plen),
156        total_len: Some(total),
157        enough_for_total,
158        crc_ok,
159        parsed,
160    }
161}
162
163/// Expands bytes into MSB-first bits.
164///
165/// Parameters:
166/// - `bytes`: input bytes.
167/// Returns:
168/// - `Vec<u8>`: bit vector with values in `{0,1}`.
169pub fn bytes_to_bits(bytes: &[u8]) -> Vec<u8> {
170    let mut bits = Vec::with_capacity(bytes.len() * 8);
171    for &b in bytes {
172        for shift in (0..8).rev() {
173            bits.push((b >> shift) & 1);
174        }
175    }
176    bits
177}
178
179/// Packs MSB-first bits into bytes (truncates incomplete trailing byte).
180///
181/// Parameters:
182/// - `bits`: input bits (`0/1` values).
183/// Returns:
184/// - `Vec<u8>`: packed bytes.
185pub fn bits_to_bytes(bits: &[u8]) -> Vec<u8> {
186    let nbytes = bits.len() / 8;
187    let mut out = vec![0u8; nbytes];
188    for i in 0..nbytes {
189        let mut v = 0u8;
190        for b in 0..8 {
191            v = (v << 1) | (bits[i * 8 + b] & 1);
192        }
193        out[i] = v;
194    }
195    out
196}
197
198pub fn fec_encoded_bits_len(raw_bits_len: usize, mode: FecMode) -> usize {
199    match mode {
200        FecMode::None => raw_bits_len,
201        FecMode::Hamming74 => raw_bits_len.div_ceil(4) * 7,
202    }
203}
204
205pub fn fec_encode_bits(bits: &[u8], mode: FecMode) -> Vec<u8> {
206    match mode {
207        FecMode::None => bits.to_vec(),
208        FecMode::Hamming74 => {
209            let mut out = Vec::with_capacity(fec_encoded_bits_len(bits.len(), mode));
210            let mut i = 0usize;
211            while i < bits.len() {
212                let d1 = bits.get(i).copied().unwrap_or(0) & 1;
213                let d2 = bits.get(i + 1).copied().unwrap_or(0) & 1;
214                let d3 = bits.get(i + 2).copied().unwrap_or(0) & 1;
215                let d4 = bits.get(i + 3).copied().unwrap_or(0) & 1;
216                let p1 = d1 ^ d2 ^ d4;
217                let p2 = d1 ^ d3 ^ d4;
218                let p3 = d2 ^ d3 ^ d4;
219                out.extend_from_slice(&[p1, p2, d1, p3, d2, d3, d4]);
220                i += 4;
221            }
222            out
223        }
224    }
225}
226
227pub fn fec_decode_bits(bits: &[u8], mode: FecMode) -> Vec<u8> {
228    match mode {
229        FecMode::None => bits.to_vec(),
230        FecMode::Hamming74 => {
231            let mut out = Vec::with_capacity((bits.len() / 7) * 4);
232            for chunk in bits.chunks_exact(7) {
233                let mut c = [
234                    chunk[0] & 1,
235                    chunk[1] & 1,
236                    chunk[2] & 1,
237                    chunk[3] & 1,
238                    chunk[4] & 1,
239                    chunk[5] & 1,
240                    chunk[6] & 1,
241                ];
242                let s1 = c[0] ^ c[2] ^ c[4] ^ c[6];
243                let s2 = c[1] ^ c[2] ^ c[5] ^ c[6];
244                let s3 = c[3] ^ c[4] ^ c[5] ^ c[6];
245                let syndrome = (s1 | (s2 << 1) | (s3 << 2)) as usize;
246                if (1..=7).contains(&syndrome) {
247                    c[syndrome - 1] ^= 1;
248                }
249                out.extend_from_slice(&[c[2], c[4], c[5], c[6]]);
250            }
251            out
252        }
253    }
254}
255
256/// Converts header modulation ID to enum.
257///
258/// Parameters:
259/// - `id`: modulation ID from packet header.
260/// Returns:
261/// - `Option<Modulation>`: matching modulation, or `None` if unknown.
262pub fn modulation_from_id(id: u8) -> Option<Modulation> {
263    match id {
264        1 => Some(Modulation::Bpsk),
265        2 => Some(Modulation::Qpsk),
266        _ => None,
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    /// Checks deterministic payload chunking.
276    fn split_payload_chunks_correctly() {
277        let p: Vec<u8> = (0..10).collect();
278        let chunks = split_payload(&p, 4);
279        assert_eq!(chunks.len(), 3);
280        assert_eq!(chunks[0], vec![0, 1, 2, 3]);
281        assert_eq!(chunks[1], vec![4, 5, 6, 7]);
282        assert_eq!(chunks[2], vec![8, 9]);
283    }
284
285    #[test]
286    /// Ensures bits<->bytes conversions are inverse.
287    fn bits_bytes_roundtrip() {
288        let data = vec![0x00, 0xA5, 0x5A, 0xFF, 0x13];
289        let bits = bytes_to_bits(&data);
290        let back = bits_to_bytes(&bits);
291        assert_eq!(back, data);
292    }
293
294    #[test]
295    /// Verifies packet build/parse consistency.
296    fn packet_build_parse_roundtrip() {
297        let cfg = OfdmConfig::default();
298        let payload = vec![1, 2, 3, 4, 5];
299        let pkt = build_packet_bytes(&payload, 2, 9, &cfg);
300        let (info, used) = parse_packet_bytes(&pkt).expect("packet must parse");
301        assert_eq!(used, pkt.len());
302        assert_eq!(info.version, 1);
303        assert_eq!(info.mod_id, cfg.modulation.mod_id());
304        assert_eq!(info.session_id, cfg.session_id);
305        assert_eq!(info.frag_index, 2);
306        assert_eq!(info.frag_count, 9);
307        assert_eq!(info.payload, payload);
308    }
309
310    #[test]
311    /// Ensures packets with CRC corruption are rejected.
312    fn packet_crc_error_is_rejected() {
313        let cfg = OfdmConfig::default();
314        let payload = vec![10, 20, 30];
315        let mut pkt = build_packet_bytes(&payload, 0, 1, &cfg);
316        let n = pkt.len();
317        pkt[n - 1] ^= 0x01;
318        assert!(parse_packet_bytes(&pkt).is_none());
319    }
320
321    #[test]
322    fn hamming74_roundtrip_and_single_bit_correction() {
323        let raw = bytes_to_bits(&[0xA5, 0x5A, 0x13, 0x7C]);
324        let mut enc = fec_encode_bits(&raw, FecMode::Hamming74);
325        assert_eq!(enc.len(), raw.len() / 4 * 7);
326        enc[5] ^= 1;
327        enc[20] ^= 1;
328        let dec = fec_decode_bits(&enc, FecMode::Hamming74);
329        assert_eq!(dec, raw);
330    }
331}
332
333// vim: set ts=4 sw=4 et: