Skip to main content

openipc_core/
realtek.rs

1/// Size of a Realtek USB RX descriptor before any PHY status and payload bytes.
2pub const RX_DESC_SIZE: usize = 24;
3/// Default native USB bulk-IN transfer size used by the receiver.
4pub const DEFAULT_RX_TRANSFER_SIZE: usize = 32 * 1024;
5
6/// Realtek USB RX descriptor layout family.
7///
8/// Jaguar1 is used by RTL8812AU/RTL8821AU and the existing RTL8814AU path in
9/// this project. Jaguar2 and Jaguar3 use the common 24-byte HalMAC 88xx
10/// descriptor while moving several fields from the Jaguar1 layout.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12pub enum RxDescriptorKind {
13    /// RTL8812AU/RTL8821AU/RTL8814AU-style descriptor layout.
14    #[default]
15    Jaguar1,
16    /// RTL8812BU/RTL8822BU common HalMAC descriptor layout.
17    Jaguar2,
18    /// RTL8812CU/EU and RTL8822CU/EU descriptor layout.
19    Jaguar3,
20}
21
22/// Type of packet carried by a Realtek RX descriptor.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum RxPacketType {
25    /// Normal received 802.11 frame.
26    NormalRx,
27    /// Command-to-host report generated by the chip firmware.
28    C2hPacket,
29}
30
31/// Parsed metadata from a Realtek USB RX descriptor.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct RxPacketAttrib {
34    /// Number of payload bytes following descriptor, PHY status, and shift bytes.
35    pub pkt_len: u16,
36    /// Whether PHY status bytes are present after the descriptor.
37    pub physt: bool,
38    /// Driver-info/PHY-status byte count.
39    pub drvinfo_sz: u8,
40    /// Extra alignment shift before payload bytes.
41    pub shift_sz: u8,
42    /// Whether the original 802.11 frame had QoS metadata.
43    pub qos: bool,
44    /// Realtek priority field.
45    pub priority: u8,
46    /// More-data bit from the descriptor.
47    pub mdata: bool,
48    /// 802.11 sequence number reported by the descriptor.
49    pub seq_num: u16,
50    /// 802.11 fragment number reported by the descriptor.
51    pub frag_num: u8,
52    /// More-fragments bit from the descriptor.
53    pub mfrag: bool,
54    /// Whether the chip reports the packet as decrypted.
55    pub bdecrypted: bool,
56    /// Realtek encryption type field.
57    pub encrypt: u8,
58    /// Hardware-reported CRC/FCS failure.
59    pub crc_err: bool,
60    /// Hardware-reported ICV failure.
61    pub icv_err: bool,
62    /// TSF low timestamp from the descriptor.
63    pub tsfl: u32,
64    /// Realtek data-rate code.
65    pub data_rate: u8,
66    /// Realtek bandwidth code.
67    pub bw: u8,
68    /// STBC flag from PHY status.
69    pub stbc: u8,
70    /// LDPC flag from PHY status.
71    pub ldpc: u8,
72    /// Short guard interval flag from PHY status.
73    pub sgi: u8,
74    /// Scrambler seed from PHY status.
75    pub scrambler: u8,
76    /// Per-path raw RSSI readings when available.
77    pub rssi: [u8; 4],
78    /// Per-path raw SNR readings when available.
79    pub snr: [i8; 4],
80    /// Per-path raw EVM readings when available.
81    pub evm: [i8; 4],
82    /// Whether this descriptor carries a received frame or a C2H report.
83    pub pkt_rpt_type: RxPacketType,
84}
85
86impl Default for RxPacketAttrib {
87    fn default() -> Self {
88        Self {
89            pkt_len: 0,
90            physt: false,
91            drvinfo_sz: 0,
92            shift_sz: 0,
93            qos: false,
94            priority: 0,
95            mdata: false,
96            seq_num: 0,
97            frag_num: 0,
98            mfrag: false,
99            bdecrypted: false,
100            encrypt: 0,
101            crc_err: false,
102            icv_err: false,
103            tsfl: 0,
104            data_rate: 0,
105            bw: 0,
106            stbc: 0,
107            ldpc: 0,
108            sgi: 0,
109            scrambler: 0,
110            rssi: [0; 4],
111            snr: [0; 4],
112            evm: [0; 4],
113            pkt_rpt_type: RxPacketType::NormalRx,
114        }
115    }
116}
117
118/// One packet extracted from a Realtek USB RX aggregate.
119#[derive(Debug, Clone, Copy)]
120pub struct RealtekRxPacket<'a> {
121    /// Parsed descriptor metadata.
122    pub attrib: RxPacketAttrib,
123    /// Borrowed packet bytes after descriptor, PHY status, and alignment shift.
124    pub data: &'a [u8],
125}
126
127/// Parsed command-to-host report carried in an RX aggregate.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct C2hPacket<'a> {
130    /// Firmware command/report identifier.
131    pub cmd_id: u8,
132    /// Optional sequence byte when the report includes one.
133    pub seq: Option<u8>,
134    /// Remaining C2H payload bytes.
135    pub payload: &'a [u8],
136}
137
138/// RTL8814A transmit status report extracted from a C2H packet.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct TxStatusReport8814 {
141    /// Offset at which this report was found in the C2H payload.
142    pub header_offset: usize,
143    /// Hardware queue selector.
144    pub queue_select: u8,
145    /// Whether the transmitted packet was broadcast.
146    pub packet_broadcast: bool,
147    /// Whether the packet exceeded the lifetime limit.
148    pub lifetime_over: bool,
149    /// Whether the packet exceeded the retry limit.
150    pub retry_over: bool,
151    /// MAC ID associated with the report.
152    pub mac_id: u8,
153    /// Number of data retries reported by hardware.
154    pub data_retry_count: u8,
155    /// Queue residency time in microseconds.
156    pub queue_time_us: u32,
157    /// Final data-rate code used by hardware.
158    pub final_data_rate: u8,
159}
160
161/// Error returned while parsing Realtek RX aggregates.
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum AggregateError {
164    /// Fewer than `RX_DESC_SIZE` bytes were available for a descriptor.
165    DescriptorTooShort,
166    /// Descriptor payload length points beyond the remaining aggregate bytes.
167    InvalidPacketLength {
168        /// Packet length read from descriptor.
169        pkt_len: u16,
170        /// Descriptor + metadata + packet offset implied by the descriptor.
171        pkt_offset: usize,
172        /// Bytes remaining in the aggregate at the current descriptor.
173        remaining: usize,
174    },
175}
176
177impl std::fmt::Display for AggregateError {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        match self {
180            Self::DescriptorTooShort => write!(f, "RX descriptor is shorter than {RX_DESC_SIZE} bytes"),
181            Self::InvalidPacketLength {
182                pkt_len,
183                pkt_offset,
184                remaining,
185            } => write!(
186                f,
187                "invalid RX packet length: pkt_len={pkt_len}, pkt_offset={pkt_offset}, remaining={remaining}"
188            ),
189        }
190    }
191}
192
193impl std::error::Error for AggregateError {}
194
195/// Parse a single Realtek RX descriptor.
196pub fn parse_rx_descriptor(desc: &[u8]) -> Result<RxPacketAttrib, AggregateError> {
197    parse_rx_descriptor_with_kind(desc, RxDescriptorKind::Jaguar1)
198}
199
200/// Parse a single Realtek RX descriptor using an explicit layout.
201pub fn parse_rx_descriptor_with_kind(
202    desc: &[u8],
203    kind: RxDescriptorKind,
204) -> Result<RxPacketAttrib, AggregateError> {
205    if desc.len() < RX_DESC_SIZE {
206        return Err(AggregateError::DescriptorTooShort);
207    }
208
209    let d0 = le32(desc, 0);
210    let d1 = le32(desc, 4);
211    let d2 = le32(desc, 8);
212    let d3 = le32(desc, 12);
213    let d4 = le32(desc, 16);
214    let d5 = le32(desc, 20);
215
216    let mut attrib = RxPacketAttrib {
217        pkt_len: bits(d0, 0, 14) as u16,
218        crc_err: bits(d0, 14, 1) != 0,
219        icv_err: bits(d0, 15, 1) != 0,
220        drvinfo_sz: (bits(d0, 16, 4) * 8) as u8,
221        shift_sz: bits(d0, 24, 2) as u8,
222        physt: bits(d0, 26, 1) != 0,
223        data_rate: bits(d3, 0, 7) as u8,
224        pkt_rpt_type: if bits(d2, 28, 1) != 0 {
225            RxPacketType::C2hPacket
226        } else {
227            RxPacketType::NormalRx
228        },
229        ..Default::default()
230    };
231
232    if kind == RxDescriptorKind::Jaguar1 {
233        attrib.encrypt = bits(d0, 20, 3) as u8;
234        attrib.qos = bits(d0, 23, 1) != 0;
235        attrib.bdecrypted = bits(d0, 27, 1) == 0;
236        attrib.priority = bits(d1, 8, 4) as u8;
237        attrib.mdata = bits(d1, 26, 1) != 0;
238        attrib.mfrag = bits(d1, 27, 1) != 0;
239        attrib.seq_num = bits(d2, 0, 12) as u16;
240        attrib.frag_num = bits(d2, 12, 4) as u8;
241        attrib.sgi = bits(d4, 0, 1) as u8;
242        attrib.ldpc = bits(d4, 1, 1) as u8;
243        attrib.stbc = bits(d4, 2, 1) as u8;
244        attrib.bw = bits(d4, 4, 2) as u8;
245        attrib.scrambler = bits(d4, 9, 7) as u8;
246        attrib.tsfl = d5;
247    }
248
249    Ok(attrib)
250}
251
252/// Split a Realtek USB bulk-IN transfer into packet descriptors and payloads.
253///
254/// Realtek devices aggregate one or more RX descriptors into each USB transfer.
255/// Each returned packet borrows from `buf`, so callers can parse metadata without
256/// copying frame bytes.
257pub fn parse_rx_aggregate(buf: &[u8]) -> Result<Vec<RealtekRxPacket<'_>>, AggregateError> {
258    parse_rx_aggregate_with_kind(buf, RxDescriptorKind::Jaguar1)
259}
260
261/// Split a Realtek USB bulk-IN transfer using an explicit RX descriptor layout.
262pub fn parse_rx_aggregate_with_kind(
263    buf: &[u8],
264    kind: RxDescriptorKind,
265) -> Result<Vec<RealtekRxPacket<'_>>, AggregateError> {
266    let mut packets = Vec::new();
267    let mut offset = 0usize;
268
269    while offset < buf.len() {
270        let remaining = buf.len() - offset;
271        if remaining < RX_DESC_SIZE {
272            break;
273        }
274
275        let desc = &buf[offset..offset + RX_DESC_SIZE];
276        let mut attrib = parse_rx_descriptor_with_kind(desc, kind)?;
277        let data_start =
278            offset + RX_DESC_SIZE + attrib.drvinfo_sz as usize + attrib.shift_sz as usize;
279        let pkt_offset = RX_DESC_SIZE
280            + attrib.drvinfo_sz as usize
281            + attrib.shift_sz as usize
282            + attrib.pkt_len as usize;
283        if attrib.pkt_len == 0 || pkt_offset > remaining {
284            return Err(AggregateError::InvalidPacketLength {
285                pkt_len: attrib.pkt_len,
286                pkt_offset,
287                remaining,
288            });
289        }
290
291        if attrib.pkt_rpt_type == RxPacketType::NormalRx {
292            let phy_start = offset + RX_DESC_SIZE;
293            let phy_end = phy_start + attrib.drvinfo_sz as usize;
294            parse_phy_status(&mut attrib, &buf[phy_start..phy_end], kind);
295        }
296
297        let data_end = data_start + attrib.pkt_len as usize;
298        packets.push(RealtekRxPacket {
299            attrib,
300            data: &buf[data_start..data_end],
301        });
302
303        let aligned = round_up_8(pkt_offset);
304        if aligned >= remaining {
305            break;
306        }
307        offset += aligned;
308    }
309
310    Ok(packets)
311}
312
313/// Parse a C2H report header from a Realtek RX packet payload.
314pub fn parse_c2h_packet(data: &[u8]) -> Option<C2hPacket<'_>> {
315    let (&cmd_id, rest) = data.split_first()?;
316    let seq = rest.first().copied();
317    let payload = if rest.len() > 1 { &rest[1..] } else { rest };
318    Some(C2hPacket {
319        cmd_id,
320        seq,
321        payload,
322    })
323}
324
325/// Parse RTL8814A TX status reports from a C2H payload.
326pub fn parse_8814_tx_status_reports(data: &[u8]) -> Vec<TxStatusReport8814> {
327    [1usize, 2usize]
328        .into_iter()
329        .filter_map(|offset| parse_8814_tx_status_report_at(data, offset))
330        .collect()
331}
332
333/// Parse one RTL8814A TX status report at a known payload offset.
334pub fn parse_8814_tx_status_report_at(
335    data: &[u8],
336    header_offset: usize,
337) -> Option<TxStatusReport8814> {
338    let h = data.get(header_offset..header_offset + 6)?;
339    let queue_time_raw = u16::from_le_bytes([h[3], h[4]]);
340    Some(TxStatusReport8814 {
341        header_offset,
342        queue_select: h[0] & 0x1f,
343        packet_broadcast: h[0] & (1 << 5) != 0,
344        lifetime_over: h[0] & (1 << 6) != 0,
345        retry_over: h[0] & (1 << 7) != 0,
346        mac_id: h[1],
347        data_retry_count: h[2] & 0x3f,
348        queue_time_us: u32::from(queue_time_raw) * 256,
349        final_data_rate: h[5],
350    })
351}
352
353const fn round_up_8(value: usize) -> usize {
354    (value + 7) & !7
355}
356
357fn le32(bytes: &[u8], offset: usize) -> u32 {
358    u32::from_le_bytes(
359        bytes[offset..offset + 4]
360            .try_into()
361            .expect("descriptor length checked"),
362    )
363}
364
365const fn bits(word: u32, offset: u8, len: u8) -> u32 {
366    if len == 32 {
367        word
368    } else {
369        (word >> offset) & ((1u32 << len) - 1)
370    }
371}
372
373fn parse_phy_status(attrib: &mut RxPacketAttrib, phy: &[u8], kind: RxDescriptorKind) {
374    if kind != RxDescriptorKind::Jaguar1 {
375        parse_phy_status_jaguar23(attrib, phy, kind);
376        return;
377    }
378    if phy.len() < 2 {
379        return;
380    }
381
382    attrib.rssi[0] = phy[0];
383    attrib.rssi[1] = phy[1];
384
385    if phy.len() < 28 {
386        return;
387    }
388
389    attrib.rssi[2] = phy[23];
390    attrib.rssi[3] = phy[24];
391    attrib.snr[0] = phy[15] as i8;
392    attrib.snr[1] = phy[16] as i8;
393    attrib.snr[2] = phy[21] as i8;
394    attrib.snr[3] = phy[22] as i8;
395    attrib.evm[0] = phy[13] as i8;
396    attrib.evm[1] = phy[14] as i8;
397    attrib.evm[2] = phy[19] as i8;
398    attrib.evm[3] = phy[20] as i8;
399}
400
401fn parse_phy_status_jaguar23(attrib: &mut RxPacketAttrib, phy: &[u8], kind: RxDescriptorKind) {
402    if phy.len() < 28 {
403        return;
404    }
405    let is_cck = if kind == RxDescriptorKind::Jaguar3 {
406        phy[0] & 0x0f == 0
407    } else {
408        attrib.data_rate <= 3
409    };
410    if is_cck {
411        attrib.rssi[0] = phy[1];
412        return;
413    }
414    attrib.rssi.copy_from_slice(&phy[1..5]);
415    attrib.ldpc = (phy[7] >> 5) & 1;
416    attrib.stbc = (phy[7] >> 6) & 1;
417    let l_rxsc = phy[5] & 0x0f;
418    let ht_rxsc = (phy[5] >> 4) & 0x0f;
419    let rxsc = if (4..=11).contains(&attrib.data_rate) {
420        l_rxsc
421    } else {
422        ht_rxsc
423    };
424    attrib.bw = if rxsc >= 13 {
425        2
426    } else if rxsc >= 9 {
427        1
428    } else {
429        0
430    };
431
432    // Jaguar3 pages other than type 1 reuse these bytes for unrelated data.
433    if kind == RxDescriptorKind::Jaguar2 || phy[0] & 0x0f == 1 {
434        for path in 0..4 {
435            attrib.evm[path] = phy[16 + path] as i8;
436            attrib.snr[path] = phy[24 + path] as i8;
437        }
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    fn put_bits(word: &mut u32, offset: u8, len: u8, value: u32) {
446        let mask = ((1u32 << len) - 1) << offset;
447        *word = (*word & !mask) | ((value << offset) & mask);
448    }
449
450    fn descriptor(pkt_len: u16, drvinfo_units: u8, shift: u8, seq: u16) -> [u8; RX_DESC_SIZE] {
451        let mut desc = [0; RX_DESC_SIZE];
452        let mut d0 = 0u32;
453        put_bits(&mut d0, 0, 14, pkt_len as u32);
454        put_bits(&mut d0, 16, 4, drvinfo_units as u32);
455        put_bits(&mut d0, 24, 2, shift as u32);
456        let mut d2 = 0u32;
457        put_bits(&mut d2, 0, 12, seq as u32);
458        desc[0..4].copy_from_slice(&d0.to_le_bytes());
459        desc[8..12].copy_from_slice(&d2.to_le_bytes());
460        desc
461    }
462
463    fn jaguar3_descriptor(
464        pkt_len: u16,
465        drvinfo_units: u8,
466        shift: u8,
467        rx_rate: u8,
468        c2h: bool,
469    ) -> [u8; RX_DESC_SIZE] {
470        let mut desc = [0; RX_DESC_SIZE];
471        let mut d0 = 0u32;
472        put_bits(&mut d0, 0, 14, pkt_len as u32);
473        put_bits(&mut d0, 14, 1, 1);
474        put_bits(&mut d0, 15, 1, 1);
475        put_bits(&mut d0, 16, 4, drvinfo_units as u32);
476        put_bits(&mut d0, 24, 2, shift as u32);
477        let mut d2 = 0u32;
478        put_bits(&mut d2, 28, 1, u32::from(c2h));
479        let mut d3 = 0u32;
480        put_bits(&mut d3, 0, 7, rx_rate as u32);
481        desc[0..4].copy_from_slice(&d0.to_le_bytes());
482        desc[8..12].copy_from_slice(&d2.to_le_bytes());
483        desc[12..16].copy_from_slice(&d3.to_le_bytes());
484        desc
485    }
486
487    #[test]
488    fn parses_single_rx_packet() {
489        let mut aggregate = Vec::new();
490        aggregate.extend_from_slice(&descriptor(4, 0, 0, 77));
491        aggregate.extend_from_slice(&[1, 2, 3, 4]);
492
493        let packets = parse_rx_aggregate(&aggregate).unwrap();
494        assert_eq!(packets.len(), 1);
495        assert_eq!(packets[0].attrib.pkt_len, 4);
496        assert_eq!(packets[0].attrib.seq_num, 77);
497        assert_eq!(packets[0].data, &[1, 2, 3, 4]);
498    }
499
500    #[test]
501    fn ignores_short_tail_after_aligned_packet() {
502        let mut aggregate = Vec::new();
503        aggregate.extend_from_slice(&descriptor(4, 0, 0, 77));
504        aggregate.extend_from_slice(&[1, 2, 3, 4]);
505        aggregate.extend_from_slice(&[0, 0, 0, 0]);
506        aggregate.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
507
508        let packets = parse_rx_aggregate(&aggregate).unwrap();
509        assert_eq!(packets.len(), 1);
510        assert_eq!(packets[0].data, &[1, 2, 3, 4]);
511    }
512
513    #[test]
514    fn rejects_zero_length_descriptor() {
515        let aggregate = descriptor(0, 0, 0, 1);
516
517        let err = parse_rx_aggregate(&aggregate).unwrap_err();
518        assert_eq!(
519            err,
520            AggregateError::InvalidPacketLength {
521                pkt_len: 0,
522                pkt_offset: RX_DESC_SIZE,
523                remaining: RX_DESC_SIZE,
524            }
525        );
526    }
527
528    #[test]
529    fn rejects_descriptor_payload_past_transfer() {
530        let mut aggregate = Vec::new();
531        aggregate.extend_from_slice(&descriptor(8, 0, 0, 1));
532        aggregate.extend_from_slice(&[1, 2, 3, 4]);
533
534        let err = parse_rx_aggregate(&aggregate).unwrap_err();
535        assert_eq!(
536            err,
537            AggregateError::InvalidPacketLength {
538                pkt_len: 8,
539                pkt_offset: RX_DESC_SIZE + 8,
540                remaining: RX_DESC_SIZE + 4,
541            }
542        );
543    }
544
545    #[test]
546    fn surfaces_crc_and_icv_flags_from_jaguar1_descriptor() {
547        let mut desc = descriptor(4, 0, 0, 9);
548        let mut d0 = u32::from_le_bytes(desc[0..4].try_into().unwrap());
549        put_bits(&mut d0, 14, 1, 1);
550        put_bits(&mut d0, 15, 1, 1);
551        desc[0..4].copy_from_slice(&d0.to_le_bytes());
552
553        let mut aggregate = Vec::new();
554        aggregate.extend_from_slice(&desc);
555        aggregate.extend_from_slice(&[1, 2, 3, 4]);
556
557        let packets = parse_rx_aggregate(&aggregate).unwrap();
558        assert!(packets[0].attrib.crc_err);
559        assert!(packets[0].attrib.icv_err);
560        assert_eq!(packets[0].data, &[1, 2, 3, 4]);
561    }
562
563    #[test]
564    fn does_not_decode_payload_as_phy_status_when_drvinfo_absent() {
565        let mut payload = vec![0u8; 32];
566        payload[0] = 55;
567        payload[1] = 66;
568        payload[13] = 0x80;
569        payload[15] = 0x7f;
570        payload[23] = 77;
571
572        let mut aggregate = Vec::new();
573        aggregate.extend_from_slice(&descriptor(payload.len() as u16, 0, 0, 1));
574        aggregate.extend_from_slice(&payload);
575
576        let packets = parse_rx_aggregate(&aggregate).unwrap();
577        assert_eq!(packets[0].attrib.rssi, [0; 4]);
578        assert_eq!(packets[0].attrib.snr, [0; 4]);
579        assert_eq!(packets[0].attrib.evm, [0; 4]);
580        assert_eq!(packets[0].data, payload);
581    }
582
583    #[test]
584    fn parses_phy_status_only_from_driver_info_bytes() {
585        let mut phy = vec![0u8; 32];
586        phy[0] = 42;
587        phy[1] = 43;
588        phy[13] = 0xf0;
589        phy[14] = 0x0f;
590        phy[15] = 0x80;
591        phy[16] = 0x7f;
592        phy[19] = 0xee;
593        phy[20] = 0xdd;
594        phy[21] = 0xcc;
595        phy[22] = 0xbb;
596        phy[23] = 44;
597        phy[24] = 45;
598
599        let mut aggregate = Vec::new();
600        aggregate.extend_from_slice(&descriptor(3, 4, 1, 1));
601        aggregate.extend_from_slice(&phy);
602        aggregate.push(0xaa);
603        aggregate.extend_from_slice(&[1, 2, 3]);
604
605        let packets = parse_rx_aggregate(&aggregate).unwrap();
606        assert_eq!(packets[0].attrib.drvinfo_sz, 32);
607        assert_eq!(packets[0].attrib.shift_sz, 1);
608        assert_eq!(packets[0].attrib.rssi, [42, 43, 44, 45]);
609        assert_eq!(packets[0].attrib.snr, [-128, 127, -52, -69]);
610        assert_eq!(packets[0].attrib.evm, [-16, 15, -18, -35]);
611        assert_eq!(packets[0].data, &[1, 2, 3]);
612    }
613
614    #[test]
615    fn parses_c2h_packet_header() {
616        let packet = parse_c2h_packet(&[0x42, 0x7f, 1, 2]).unwrap();
617        assert_eq!(packet.cmd_id, 0x42);
618        assert_eq!(packet.seq, Some(0x7f));
619        assert_eq!(packet.payload, &[1, 2]);
620    }
621
622    #[test]
623    fn parses_8814_tx_status_at_devourer_offsets() {
624        let bytes = [0xaa, 0x83, 0x09, 0x25, 0x34, 0x12, 0x6c, 0xbb];
625        let reports = parse_8814_tx_status_reports(&bytes);
626        assert_eq!(reports.len(), 2);
627        assert_eq!(reports[0].header_offset, 1);
628        assert_eq!(reports[0].queue_select, 3);
629        assert!(reports[0].retry_over);
630        assert_eq!(reports[0].mac_id, 9);
631        assert_eq!(reports[0].data_retry_count, 0x25);
632        assert_eq!(reports[0].queue_time_us, 0x1234 * 256);
633        assert_eq!(reports[0].final_data_rate, 0x6c);
634    }
635
636    #[test]
637    fn advances_by_jaguar_eight_byte_alignment() {
638        let mut aggregate = Vec::new();
639        aggregate.extend_from_slice(&descriptor(5, 0, 0, 1));
640        aggregate.extend_from_slice(&[1, 2, 3, 4, 5]);
641        aggregate.extend_from_slice(&[0, 0, 0]);
642        aggregate.extend_from_slice(&descriptor(3, 0, 0, 2));
643        aggregate.extend_from_slice(&[6, 7, 8]);
644
645        let packets = parse_rx_aggregate(&aggregate).unwrap();
646        assert_eq!(packets.len(), 2);
647        assert_eq!(packets[0].data, &[1, 2, 3, 4, 5]);
648        assert_eq!(packets[1].data, &[6, 7, 8]);
649    }
650
651    #[test]
652    fn jaguar3_descriptor_matches_devourer_field_positions() {
653        let mut aggregate = Vec::new();
654        aggregate.extend_from_slice(&jaguar3_descriptor(4, 1, 2, 0x2c, false));
655        aggregate.extend_from_slice(&[41, 42, 0, 0, 0, 0, 0, 0]);
656        aggregate.extend_from_slice(&[0xaa, 0xbb]);
657        aggregate.extend_from_slice(&[1, 2, 3, 4]);
658
659        let packets = parse_rx_aggregate_with_kind(&aggregate, RxDescriptorKind::Jaguar3).unwrap();
660        assert_eq!(packets.len(), 1);
661        assert_eq!(packets[0].attrib.pkt_len, 4);
662        assert_eq!(packets[0].attrib.drvinfo_sz, 8);
663        assert_eq!(packets[0].attrib.shift_sz, 2);
664        assert_eq!(packets[0].attrib.data_rate, 0x2c);
665        assert!(packets[0].attrib.crc_err);
666        assert!(packets[0].attrib.icv_err);
667        assert_eq!(packets[0].attrib.pkt_rpt_type, RxPacketType::NormalRx);
668        assert_eq!(packets[0].data, &[1, 2, 3, 4]);
669    }
670
671    #[test]
672    fn jaguar3_c2h_bit_is_report_type() {
673        let mut aggregate = Vec::new();
674        aggregate.extend_from_slice(&jaguar3_descriptor(3, 0, 0, 0, true));
675        aggregate.extend_from_slice(&[0x61, 0x01, 0x02]);
676
677        let packets = parse_rx_aggregate_with_kind(&aggregate, RxDescriptorKind::Jaguar3).unwrap();
678        assert_eq!(packets.len(), 1);
679        assert_eq!(packets[0].attrib.pkt_rpt_type, RxPacketType::C2hPacket);
680        assert_eq!(packets[0].data, &[0x61, 0x01, 0x02]);
681    }
682
683    #[test]
684    fn jaguar3_type1_phy_status_uses_jgr3_offsets() {
685        let mut phy = [0u8; 32];
686        phy[0] = 1;
687        phy[1..5].copy_from_slice(&[71, 57, 0, 0]);
688        phy[5] = 13 << 4;
689        phy[7] = (1 << 5) | (1 << 6);
690        phy[16..20].copy_from_slice(&[0xd5, 0x80, 0, 0]);
691        phy[24..28].copy_from_slice(&[43, 25, 0, 0]);
692        let mut aggregate = Vec::new();
693        aggregate.extend_from_slice(&jaguar3_descriptor(3, 4, 0, 13, false));
694        aggregate.extend_from_slice(&phy);
695        aggregate.extend_from_slice(&[1, 2, 3]);
696
697        let packet = parse_rx_aggregate_with_kind(&aggregate, RxDescriptorKind::Jaguar3)
698            .unwrap()
699            .remove(0);
700        assert_eq!(packet.attrib.rssi, [71, 57, 0, 0]);
701        assert_eq!(packet.attrib.evm[0], -43);
702        assert_eq!(packet.attrib.snr[..2], [43, 25]);
703        assert_eq!(packet.attrib.bw, 2);
704        assert_eq!((packet.attrib.ldpc, packet.attrib.stbc), (1, 1));
705    }
706
707    #[test]
708    fn jaguar3_non_type1_page_does_not_decode_evm_or_snr() {
709        let mut phy = [0u8; 32];
710        phy[0] = 2;
711        phy[1] = 64;
712        phy[16] = 0x80;
713        phy[24] = 0x7f;
714        let mut aggregate = Vec::new();
715        aggregate.extend_from_slice(&jaguar3_descriptor(1, 4, 0, 13, false));
716        aggregate.extend_from_slice(&phy);
717        aggregate.push(0xaa);
718        let packet = parse_rx_aggregate_with_kind(&aggregate, RxDescriptorKind::Jaguar3)
719            .unwrap()
720            .remove(0);
721        assert_eq!(packet.attrib.rssi[0], 64);
722        assert_eq!(packet.attrib.evm, [0; 4]);
723        assert_eq!(packet.attrib.snr, [0; 4]);
724    }
725
726    #[test]
727    fn c2h_payload_respects_drvinfo_and_shift_offsets() {
728        let mut desc = descriptor(3, 1, 2, 0);
729        let mut d2 = u32::from_le_bytes(desc[8..12].try_into().unwrap());
730        put_bits(&mut d2, 28, 1, 1);
731        desc[8..12].copy_from_slice(&d2.to_le_bytes());
732
733        let mut aggregate = Vec::new();
734        aggregate.extend_from_slice(&desc);
735        aggregate.extend_from_slice(&[0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48]);
736        aggregate.extend_from_slice(&[0xaa, 0xbb]);
737        aggregate.extend_from_slice(&[0x61, 0x07, 0xcc]);
738
739        let packets = parse_rx_aggregate(&aggregate).unwrap();
740        assert_eq!(packets.len(), 1);
741        assert_eq!(packets[0].attrib.pkt_rpt_type, RxPacketType::C2hPacket);
742        assert_eq!(packets[0].attrib.drvinfo_sz, 8);
743        assert_eq!(packets[0].attrib.shift_sz, 2);
744        assert_eq!(packets[0].data, &[0x61, 0x07, 0xcc]);
745        assert_eq!(
746            parse_c2h_packet(packets[0].data),
747            Some(C2hPacket {
748                cmd_id: 0x61,
749                seq: Some(0x07),
750                payload: &[0xcc],
751            })
752        );
753    }
754}