Skip to main content

radio_utils_protocol/
packets.rs

1//! Stateless Protocol 1 packet parsing and building functions.
2//!
3//! This module provides pure, synchronous functions for working with Protocol 1
4//! USB-frame-level packets. It has no dependency on tokio, async, or the "client"
5//! feature, so it can be used from both native and WASM targets.
6
7use num_complex::Complex;
8
9use crate::unpack_iq_24bit;
10
11// ---------------------------------------------------------------------------
12// P1 status from response control bytes
13// ---------------------------------------------------------------------------
14
15/// Status fields extracted from Protocol 1 response control bytes.
16#[derive(Debug, Clone, Default)]
17pub struct P1Status {
18    /// PTT state from the hardware.
19    pub ptt: bool,
20    /// ADC overflow indicator.
21    pub adc_overflow: u8,
22    /// Forward power ADC count.
23    pub forward_power: u16,
24    /// Reverse power ADC count.
25    pub reverse_power: u16,
26    /// Exciter power ADC count.
27    pub exciter_power: u16,
28    /// Supply voltage ADC count.
29    pub supply_voltage: u16,
30    /// Response address from C0 byte (masked with 0x7E).
31    pub response_addr: u8,
32}
33
34// ---------------------------------------------------------------------------
35// Parsed RX frame
36// ---------------------------------------------------------------------------
37
38/// Parsed IQ data from one Protocol 1 USB frame (1032 bytes).
39pub struct P1RxFrame {
40    /// IQ samples per DDC channel. `samples[ddc_index]` is a `Vec<Complex<f64>>`.
41    pub samples: Vec<Vec<Complex<f64>>>,
42    /// Protocol status extracted from the response control bytes.
43    pub status: P1Status,
44}
45
46// ---------------------------------------------------------------------------
47// RX packet parsing
48// ---------------------------------------------------------------------------
49
50/// Parse a 1032-byte Protocol 1 RX packet into IQ samples per DDC.
51///
52/// The P1 USB frame contains two 512-byte sub-frames, each starting with a
53/// SYNC pattern (0x7F 0x7F 0x7F) followed by 5 control bytes (C0-C4) and
54/// then interleaved IQ data for `nddc` DDC channels.
55///
56/// Each IQ sample is 6 bytes (24-bit I, 24-bit Q). Samples are interleaved
57/// across DDCs: DDC0 sample, DDC1 sample, ..., DDCn sample, DDC0 sample, ...
58pub fn parse_rx_packet(data: &[u8], nddc: usize) -> Option<P1RxFrame> {
59    if data.len() < 1032 || nddc == 0 {
60        return None;
61    }
62
63    let mut samples: Vec<Vec<Complex<f64>>> = (0..nddc).map(|_| Vec::new()).collect();
64    let mut status = P1Status::default();
65
66    for frame_idx in 0..2 {
67        let base = 8 + frame_idx * 512; // skip 8-byte header (seq + 2x sync)
68        let sync_ok = data[base] == 0x7F && data[base + 1] == 0x7F && data[base + 2] == 0x7F;
69        if !sync_ok {
70            return None;
71        }
72
73        // Parse control bytes
74        let c0 = data[base + 3];
75        let c1 = data[base + 4];
76        let c2 = data[base + 5];
77        let c3 = data[base + 6];
78        let c4 = data[base + 7];
79
80        parse_response_control_bytes(&mut status, c0, c1, c2, c3, c4);
81
82        // Parse IQ samples: 504 bytes of IQ data after the 8-byte header.
83        // Each row: [IQ(6B)] x nddc + [Mic(2B)] = 6*nddc + 2 bytes.
84        let iq_start = base + 8;
85        let iq_end = base + 512;
86        let bytes_per_sample = 6; // 24-bit I + 24-bit Q
87        let bytes_per_row = bytes_per_sample * nddc + 2; // +2 for mic sample
88
89        let mut offset = iq_start;
90        while offset + bytes_per_row <= iq_end {
91            for (ddc, ddc_samples) in samples.iter_mut().enumerate().take(nddc) {
92                let sample_offset = offset + ddc * bytes_per_sample;
93                if sample_offset + bytes_per_sample <= data.len() {
94                    let iq = unpack_iq_24bit(data, sample_offset);
95                    ddc_samples.push(iq);
96                }
97            }
98            offset += bytes_per_row;
99        }
100    }
101
102    Some(P1RxFrame { samples, status })
103}
104
105/// Parse the 5 response control bytes (C0-C4) from a radio-to-host sub-frame
106/// and update the accumulated status.
107///
108/// The response address is extracted from C0 bits 6:1 (masked with 0x7E).
109/// PTT is bit 0 of C0.
110pub fn parse_response_control_bytes(status: &mut P1Status, c0: u8, c1: u8, c2: u8, c3: u8, c4: u8) {
111    let addr = c0 & 0x7E;
112    status.ptt = (c0 & 0x01) != 0;
113    status.response_addr = addr;
114
115    match addr {
116        0x00 => {
117            status.adc_overflow = c1;
118        }
119        0x08 => {
120            status.exciter_power = ((c1 as u16) << 8) | (c2 as u16);
121            status.forward_power = ((c3 as u16) << 8) | (c4 as u16);
122        }
123        0x10 => {
124            status.reverse_power = ((c1 as u16) << 8) | (c2 as u16);
125        }
126        0x18 => {
127            status.supply_voltage = ((c3 as u16) << 8) | (c4 as u16);
128        }
129        _ => {}
130    }
131}
132
133// ---------------------------------------------------------------------------
134// TX packet building
135// ---------------------------------------------------------------------------
136
137/// Build a 1032-byte Protocol 1 host-to-radio (TX) packet.
138///
139/// Layout:
140///   [0..4]     HPSDR header: 0xEF 0xFE 0x01 0x02 (EP2)
141///   [4..8]     Sequence number (big-endian)
142///   [8..520]   Sub-frame 0: SYNC + C0-C4 + TX IQ data
143///   [520..1032] Sub-frame 1: SYNC + C0-C4 + TX IQ data
144///
145/// `tx_iq`: TX IQ samples to pack (16-bit resolution on the wire).
146/// `seq`: Packet sequence number.
147/// `control_bytes`: Array of 5 bytes [C0, C1, C2, C3, C4] for the first sub-frame.
148/// `control_bytes2`: Array of 5 bytes for the second sub-frame.
149pub fn build_tx_packet(
150    seq: u32,
151    tx_iq: &[Complex<f64>],
152    control_bytes: [u8; 5],
153    control_bytes2: [u8; 5],
154) -> Vec<u8> {
155    let mut packet = vec![0u8; 1032];
156
157    // HPSDR header
158    packet[0] = 0xEF;
159    packet[1] = 0xFE;
160    packet[2] = 0x01; // data packet
161    packet[3] = 0x02; // EP2 (host -> radio)
162
163    // Sequence number (big-endian)
164    packet[4] = ((seq >> 24) & 0xFF) as u8;
165    packet[5] = ((seq >> 16) & 0xFF) as u8;
166    packet[6] = ((seq >> 8) & 0xFF) as u8;
167    packet[7] = (seq & 0xFF) as u8;
168
169    // Two sub-frames
170    for frame_idx in 0..2u32 {
171        let base = 8 + (frame_idx as usize) * 512;
172
173        // SYNC
174        packet[base] = 0x7F;
175        packet[base + 1] = 0x7F;
176        packet[base + 2] = 0x7F;
177
178        // Control bytes
179        let cb = if frame_idx == 0 {
180            &control_bytes
181        } else {
182            &control_bytes2
183        };
184        packet[base + 3] = cb[0];
185        packet[base + 4] = cb[1];
186        packet[base + 5] = cb[2];
187        packet[base + 6] = cb[3];
188        packet[base + 7] = cb[4];
189
190        // TX samples: 8 bytes per block [L(2B) R(2B) I(2B) Q(2B)], big-endian
191        let iq_start = base + 8;
192        let iq_end = base + 512;
193        let bytes_per_block = 8; // L(2) + R(2) + I(2) + Q(2)
194        let max_samples = (iq_end - iq_start) / bytes_per_block; // 63
195
196        let sample_offset = (frame_idx as usize) * max_samples;
197        for i in 0..max_samples {
198            let sample_idx = sample_offset + i;
199            let (iv, qv) = if sample_idx < tx_iq.len() {
200                let s = &tx_iq[sample_idx];
201                let i_val = (s.re.clamp(-1.0, 1.0) * 32767.0) as i16;
202                // Negate Q for HPSDR wire convention
203                let q_val = ((-s.im).clamp(-1.0, 1.0) * 32767.0) as i16;
204                (i_val, q_val)
205            } else {
206                (0i16, 0i16)
207            };
208
209            let offset = iq_start + i * bytes_per_block;
210            // L and R audio bytes (zero -- we don't send mic audio here)
211            packet[offset] = 0;
212            packet[offset + 1] = 0;
213            packet[offset + 2] = 0;
214            packet[offset + 3] = 0;
215            // I and Q (big-endian i16)
216            packet[offset + 4] = ((iv as u16) >> 8) as u8;
217            packet[offset + 5] = (iv as u16 & 0xFF) as u8;
218            packet[offset + 6] = ((qv as u16) >> 8) as u8;
219            packet[offset + 7] = (qv as u16 & 0xFF) as u8;
220        }
221    }
222
223    packet
224}
225
226// ---------------------------------------------------------------------------
227// Control byte building
228// ---------------------------------------------------------------------------
229
230/// Build the 5 control bytes [C0, C1, C2, C3, C4] for a given address in the
231/// P1 control rotation.
232///
233/// This covers the common host-to-radio control addresses used by simple
234/// clients. Addresses handled:
235///
236/// - `0x00`: Sample rate, NDC count
237/// - `0x02`..`0x0E`: NCO frequency for DDC channels (TX freq or RX freq)
238/// - `0x12`: TX drive level
239/// - `0x14`: RX step attenuator
240///
241/// `frequency`: VFO frequency in Hz.
242/// `sample_rate_index`: Sample rate index (0=48k, 1=96k, 2=192k, 3=384k).
243/// `ptt`: Push-to-talk state.
244/// `nddc`: Number of active DDC channels.
245/// `rx_attenuation`: RX attenuation in dB (0-31).
246/// `tx_drive`: TX drive level (0-255).
247pub fn build_control_bytes(
248    address: u8,
249    frequency: u64,
250    sample_rate_index: u8,
251    ptt: bool,
252    nddc: u8,
253    rx_attenuation: u8,
254    tx_drive: u8,
255) -> [u8; 5] {
256    let mut cb = [0u8; 5];
257    let ptt_bit = if ptt { 0x01 } else { 0x00 };
258    cb[0] = address | ptt_bit;
259
260    match address {
261        0x00 => {
262            // C1: speed(1:0) in bits 1:0
263            cb[1] = sample_rate_index & 0x03;
264            // C4: NDC count in bits 5:3 (wire value = nddc - 1) + duplex bit [2]
265            // Duplex (0x04) is required by HL2 and harmless for other hardware.
266            cb[4] = (nddc.saturating_sub(1) & 0x07) << 3 | 0x04;
267        }
268        0x02 | 0x04 | 0x06 | 0x08 | 0x0A | 0x0C | 0x0E => {
269            // NCO frequency for DDC (address 0x02 = DDC0, etc.)
270            let freq = frequency.min(u32::MAX as u64) as u32;
271            cb[1] = ((freq >> 24) & 0xFF) as u8;
272            cb[2] = ((freq >> 16) & 0xFF) as u8;
273            cb[3] = ((freq >> 8) & 0xFF) as u8;
274            cb[4] = (freq & 0xFF) as u8;
275        }
276        0x12 => {
277            // TX drive level: C1 = drive value (0-255)
278            cb[1] = tx_drive;
279        }
280        0x14 => {
281            // RX step attenuator: C4 = attenuation value (0-31)
282            cb[4] = rx_attenuation & 0x1F;
283        }
284        _ => {}
285    }
286
287    cb
288}
289
290// ---------------------------------------------------------------------------
291// Tests
292// ---------------------------------------------------------------------------
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn parse_rx_packet_too_short() {
300        let data = vec![0u8; 100];
301        assert!(parse_rx_packet(&data, 1).is_none());
302    }
303
304    #[test]
305    fn parse_rx_packet_zero_nddc() {
306        let data = vec![0u8; 1032];
307        assert!(parse_rx_packet(&data, 0).is_none());
308    }
309
310    #[test]
311    fn parse_rx_packet_bad_sync() {
312        let mut data = vec![0u8; 1032];
313        // First sub-frame sync is wrong (all zeros)
314        assert!(parse_rx_packet(&data, 1).is_none());
315
316        // Set first sync correctly but second is wrong
317        data[8] = 0x7F;
318        data[9] = 0x7F;
319        data[10] = 0x7F;
320        assert!(parse_rx_packet(&data, 1).is_none());
321    }
322
323    #[test]
324    fn parse_rx_packet_valid_frame() {
325        let mut data = vec![0u8; 1032];
326        // Set sync for both sub-frames
327        data[8] = 0x7F;
328        data[9] = 0x7F;
329        data[10] = 0x7F;
330        data[520] = 0x7F;
331        data[521] = 0x7F;
332        data[522] = 0x7F;
333
334        let frame = parse_rx_packet(&data, 1).unwrap();
335        assert_eq!(frame.samples.len(), 1);
336        // With 1 DDC: row = 6+2=8 bytes, 504/8 = 63 samples per sub-frame, 126 total
337        assert_eq!(frame.samples[0].len(), 126);
338    }
339
340    #[test]
341    fn build_tx_packet_structure() {
342        let tx_iq = vec![Complex::new(0.5, -0.5); 126];
343        let cb1 = [0x00, 0x01, 0x00, 0x00, 0x00];
344        let cb2 = [0x02, 0x00, 0x07, 0x07, 0x40];
345
346        let pkt = build_tx_packet(42, &tx_iq, cb1, cb2);
347        assert_eq!(pkt.len(), 1032);
348
349        // Header
350        assert_eq!(pkt[0], 0xEF);
351        assert_eq!(pkt[1], 0xFE);
352        assert_eq!(pkt[2], 0x01);
353        assert_eq!(pkt[3], 0x02);
354
355        // Sequence
356        assert_eq!(u32::from_be_bytes([pkt[4], pkt[5], pkt[6], pkt[7]]), 42);
357
358        // Sub-frame 0 sync
359        assert_eq!(&pkt[8..11], &[0x7F, 0x7F, 0x7F]);
360        // Sub-frame 0 control bytes
361        assert_eq!(&pkt[11..16], &cb1);
362
363        // Sub-frame 1 sync
364        assert_eq!(&pkt[520..523], &[0x7F, 0x7F, 0x7F]);
365        // Sub-frame 1 control bytes
366        assert_eq!(&pkt[523..528], &cb2);
367    }
368
369    #[test]
370    fn build_control_bytes_address_0x00() {
371        let cb = build_control_bytes(0x00, 7_074_000, 1, false, 2, 0, 0);
372        assert_eq!(cb[0], 0x00); // address, no PTT
373        assert_eq!(cb[1], 0x01); // sample rate index = 1 (96k)
374        assert_eq!(cb[4], (1 << 3) | 0x04); // nddc-1 = 1, shifted left 3, plus duplex bit
375    }
376
377    #[test]
378    fn build_control_bytes_address_0x00_ptt() {
379        let cb = build_control_bytes(0x00, 7_074_000, 0, true, 1, 0, 0);
380        assert_eq!(cb[0], 0x01); // address 0x00 | PTT bit
381    }
382
383    #[test]
384    fn build_control_bytes_frequency() {
385        let freq: u64 = 14_200_000;
386        let cb = build_control_bytes(0x04, freq, 0, false, 1, 0, 0);
387        assert_eq!(cb[0], 0x04);
388        let parsed_freq = u32::from_be_bytes([cb[1], cb[2], cb[3], cb[4]]);
389        assert_eq!(parsed_freq, freq as u32);
390    }
391
392    #[test]
393    fn build_control_bytes_attenuation() {
394        let cb = build_control_bytes(0x14, 0, 0, false, 1, 20, 0);
395        assert_eq!(cb[0], 0x14);
396        assert_eq!(cb[4], 20);
397    }
398
399    #[test]
400    fn build_control_bytes_tx_drive() {
401        let cb = build_control_bytes(0x12, 0, 0, false, 1, 0, 200);
402        assert_eq!(cb[0], 0x12);
403        assert_eq!(cb[1], 200);
404    }
405
406    #[test]
407    fn parse_response_control_bytes_addr_0x08() {
408        let mut status = P1Status::default();
409        // Simulate address 0x08 with exciter_power=0x1234, forward_power=0x5678
410        parse_response_control_bytes(&mut status, 0x08, 0x12, 0x34, 0x56, 0x78);
411        assert_eq!(status.response_addr, 0x08);
412        assert_eq!(status.exciter_power, 0x1234);
413        assert_eq!(status.forward_power, 0x5678);
414        assert!(!status.ptt);
415    }
416
417    #[test]
418    fn parse_response_control_bytes_ptt() {
419        let mut status = P1Status::default();
420        parse_response_control_bytes(&mut status, 0x01, 0, 0, 0, 0); // addr 0x00 | PTT
421        assert!(status.ptt);
422        assert_eq!(status.response_addr, 0x00);
423    }
424
425    #[test]
426    fn roundtrip_tx_rx() {
427        // Build a TX packet and verify it can be parsed back
428        let tx_iq = vec![Complex::new(0.0, 0.0); 126];
429        let cb1 = [0x00, 0x00, 0x00, 0x00, 0x00];
430        let cb2 = [0x00, 0x00, 0x00, 0x00, 0x00];
431
432        let pkt = build_tx_packet(0, &tx_iq, cb1, cb2);
433        assert_eq!(pkt.len(), 1032);
434        // TX packets have sync at [8..11] and [520..523]
435        assert_eq!(&pkt[8..11], &[0x7F, 0x7F, 0x7F]);
436        assert_eq!(&pkt[520..523], &[0x7F, 0x7F, 0x7F]);
437    }
438}