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/// Type of packet carried by a Realtek RX descriptor.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum RxPacketType {
9    /// Normal received 802.11 frame.
10    NormalRx,
11    /// Command-to-host report generated by the chip firmware.
12    C2hPacket,
13}
14
15/// Parsed metadata from a Realtek USB RX descriptor.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct RxPacketAttrib {
18    /// Number of payload bytes following descriptor, PHY status, and shift bytes.
19    pub pkt_len: u16,
20    /// Whether PHY status bytes are present after the descriptor.
21    pub physt: bool,
22    /// Driver-info/PHY-status byte count.
23    pub drvinfo_sz: u8,
24    /// Extra alignment shift before payload bytes.
25    pub shift_sz: u8,
26    /// Whether the original 802.11 frame had QoS metadata.
27    pub qos: bool,
28    /// Realtek priority field.
29    pub priority: u8,
30    /// More-data bit from the descriptor.
31    pub mdata: bool,
32    /// 802.11 sequence number reported by the descriptor.
33    pub seq_num: u16,
34    /// 802.11 fragment number reported by the descriptor.
35    pub frag_num: u8,
36    /// More-fragments bit from the descriptor.
37    pub mfrag: bool,
38    /// Whether the chip reports the packet as decrypted.
39    pub bdecrypted: bool,
40    /// Realtek encryption type field.
41    pub encrypt: u8,
42    /// Hardware-reported CRC/FCS failure.
43    pub crc_err: bool,
44    /// Hardware-reported ICV failure.
45    pub icv_err: bool,
46    /// TSF low timestamp from the descriptor.
47    pub tsfl: u32,
48    /// Realtek data-rate code.
49    pub data_rate: u8,
50    /// Realtek bandwidth code.
51    pub bw: u8,
52    /// STBC flag from PHY status.
53    pub stbc: u8,
54    /// LDPC flag from PHY status.
55    pub ldpc: u8,
56    /// Short guard interval flag from PHY status.
57    pub sgi: u8,
58    /// Scrambler seed from PHY status.
59    pub scrambler: u8,
60    /// Per-path raw RSSI readings when available.
61    pub rssi: [u8; 4],
62    /// Per-path raw SNR readings when available.
63    pub snr: [i8; 4],
64    /// Per-path raw EVM readings when available.
65    pub evm: [i8; 4],
66    /// Whether this descriptor carries a received frame or a C2H report.
67    pub pkt_rpt_type: RxPacketType,
68}
69
70impl Default for RxPacketAttrib {
71    fn default() -> Self {
72        Self {
73            pkt_len: 0,
74            physt: false,
75            drvinfo_sz: 0,
76            shift_sz: 0,
77            qos: false,
78            priority: 0,
79            mdata: false,
80            seq_num: 0,
81            frag_num: 0,
82            mfrag: false,
83            bdecrypted: false,
84            encrypt: 0,
85            crc_err: false,
86            icv_err: false,
87            tsfl: 0,
88            data_rate: 0,
89            bw: 0,
90            stbc: 0,
91            ldpc: 0,
92            sgi: 0,
93            scrambler: 0,
94            rssi: [0; 4],
95            snr: [0; 4],
96            evm: [0; 4],
97            pkt_rpt_type: RxPacketType::NormalRx,
98        }
99    }
100}
101
102/// One packet extracted from a Realtek USB RX aggregate.
103#[derive(Debug, Clone, Copy)]
104pub struct RealtekRxPacket<'a> {
105    /// Parsed descriptor metadata.
106    pub attrib: RxPacketAttrib,
107    /// Borrowed packet bytes after descriptor, PHY status, and alignment shift.
108    pub data: &'a [u8],
109}
110
111/// Parsed command-to-host report carried in an RX aggregate.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct C2hPacket<'a> {
114    /// Firmware command/report identifier.
115    pub cmd_id: u8,
116    /// Optional sequence byte when the report includes one.
117    pub seq: Option<u8>,
118    /// Remaining C2H payload bytes.
119    pub payload: &'a [u8],
120}
121
122/// RTL8814A transmit status report extracted from a C2H packet.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct TxStatusReport8814 {
125    /// Offset at which this report was found in the C2H payload.
126    pub header_offset: usize,
127    /// Hardware queue selector.
128    pub queue_select: u8,
129    /// Whether the transmitted packet was broadcast.
130    pub packet_broadcast: bool,
131    /// Whether the packet exceeded the lifetime limit.
132    pub lifetime_over: bool,
133    /// Whether the packet exceeded the retry limit.
134    pub retry_over: bool,
135    /// MAC ID associated with the report.
136    pub mac_id: u8,
137    /// Number of data retries reported by hardware.
138    pub data_retry_count: u8,
139    /// Queue residency time in microseconds.
140    pub queue_time_us: u32,
141    /// Final data-rate code used by hardware.
142    pub final_data_rate: u8,
143}
144
145/// Error returned while parsing Realtek RX aggregates.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub enum AggregateError {
148    /// Fewer than `RX_DESC_SIZE` bytes were available for a descriptor.
149    DescriptorTooShort,
150    /// Descriptor payload length points beyond the remaining aggregate bytes.
151    InvalidPacketLength {
152        /// Packet length read from descriptor.
153        pkt_len: u16,
154        /// Descriptor + metadata + packet offset implied by the descriptor.
155        pkt_offset: usize,
156        /// Bytes remaining in the aggregate at the current descriptor.
157        remaining: usize,
158    },
159}
160
161impl std::fmt::Display for AggregateError {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        match self {
164            Self::DescriptorTooShort => write!(f, "RX descriptor is shorter than {RX_DESC_SIZE} bytes"),
165            Self::InvalidPacketLength {
166                pkt_len,
167                pkt_offset,
168                remaining,
169            } => write!(
170                f,
171                "invalid RX packet length: pkt_len={pkt_len}, pkt_offset={pkt_offset}, remaining={remaining}"
172            ),
173        }
174    }
175}
176
177impl std::error::Error for AggregateError {}
178
179/// Parse a single Realtek RX descriptor.
180pub fn parse_rx_descriptor(desc: &[u8]) -> Result<RxPacketAttrib, AggregateError> {
181    if desc.len() < RX_DESC_SIZE {
182        return Err(AggregateError::DescriptorTooShort);
183    }
184
185    let d0 = le32(desc, 0);
186    let d1 = le32(desc, 4);
187    let d2 = le32(desc, 8);
188    let d3 = le32(desc, 12);
189    let d4 = le32(desc, 16);
190    let d5 = le32(desc, 20);
191
192    Ok(RxPacketAttrib {
193        pkt_len: bits(d0, 0, 14) as u16,
194        crc_err: bits(d0, 14, 1) != 0,
195        icv_err: bits(d0, 15, 1) != 0,
196        drvinfo_sz: (bits(d0, 16, 4) * 8) as u8,
197        encrypt: bits(d0, 20, 3) as u8,
198        qos: bits(d0, 23, 1) != 0,
199        shift_sz: bits(d0, 24, 2) as u8,
200        physt: bits(d0, 26, 1) != 0,
201        bdecrypted: bits(d0, 27, 1) == 0,
202        priority: bits(d1, 8, 4) as u8,
203        mdata: bits(d1, 26, 1) != 0,
204        mfrag: bits(d1, 27, 1) != 0,
205        seq_num: bits(d2, 0, 12) as u16,
206        frag_num: bits(d2, 12, 4) as u8,
207        pkt_rpt_type: if bits(d2, 28, 1) != 0 {
208            RxPacketType::C2hPacket
209        } else {
210            RxPacketType::NormalRx
211        },
212        data_rate: bits(d3, 0, 7) as u8,
213        sgi: bits(d4, 0, 1) as u8,
214        ldpc: bits(d4, 1, 1) as u8,
215        stbc: bits(d4, 2, 1) as u8,
216        bw: bits(d4, 4, 2) as u8,
217        scrambler: bits(d4, 9, 7) as u8,
218        tsfl: d5,
219        ..Default::default()
220    })
221}
222
223/// Split a Realtek USB bulk-IN transfer into packet descriptors and payloads.
224///
225/// Realtek devices aggregate one or more RX descriptors into each USB transfer.
226/// Each returned packet borrows from `buf`, so callers can parse metadata without
227/// copying frame bytes.
228pub fn parse_rx_aggregate(buf: &[u8]) -> Result<Vec<RealtekRxPacket<'_>>, AggregateError> {
229    let mut packets = Vec::new();
230    let mut offset = 0usize;
231
232    while offset < buf.len() {
233        let remaining = buf.len() - offset;
234        if remaining < RX_DESC_SIZE {
235            break;
236        }
237
238        let desc = &buf[offset..offset + RX_DESC_SIZE];
239        let mut attrib = parse_rx_descriptor(desc)?;
240        let data_start =
241            offset + RX_DESC_SIZE + attrib.drvinfo_sz as usize + attrib.shift_sz as usize;
242        let pkt_offset = RX_DESC_SIZE
243            + attrib.drvinfo_sz as usize
244            + attrib.shift_sz as usize
245            + attrib.pkt_len as usize;
246        if attrib.pkt_len == 0 || pkt_offset > remaining {
247            return Err(AggregateError::InvalidPacketLength {
248                pkt_len: attrib.pkt_len,
249                pkt_offset,
250                remaining,
251            });
252        }
253
254        if attrib.pkt_rpt_type == RxPacketType::NormalRx {
255            let phy_start = offset + RX_DESC_SIZE;
256            let phy_end = phy_start + attrib.drvinfo_sz as usize;
257            parse_phy_status(&mut attrib, &buf[phy_start..phy_end]);
258        }
259
260        let data_end = data_start + attrib.pkt_len as usize;
261        packets.push(RealtekRxPacket {
262            attrib,
263            data: &buf[data_start..data_end],
264        });
265
266        let aligned = round_up_8(pkt_offset);
267        if aligned >= remaining {
268            break;
269        }
270        offset += aligned;
271    }
272
273    Ok(packets)
274}
275
276/// Parse a C2H report header from a Realtek RX packet payload.
277pub fn parse_c2h_packet(data: &[u8]) -> Option<C2hPacket<'_>> {
278    let (&cmd_id, rest) = data.split_first()?;
279    let seq = rest.first().copied();
280    let payload = if rest.len() > 1 { &rest[1..] } else { rest };
281    Some(C2hPacket {
282        cmd_id,
283        seq,
284        payload,
285    })
286}
287
288/// Parse RTL8814A TX status reports from a C2H payload.
289pub fn parse_8814_tx_status_reports(data: &[u8]) -> Vec<TxStatusReport8814> {
290    [1usize, 2usize]
291        .into_iter()
292        .filter_map(|offset| parse_8814_tx_status_report_at(data, offset))
293        .collect()
294}
295
296/// Parse one RTL8814A TX status report at a known payload offset.
297pub fn parse_8814_tx_status_report_at(
298    data: &[u8],
299    header_offset: usize,
300) -> Option<TxStatusReport8814> {
301    let h = data.get(header_offset..header_offset + 6)?;
302    let queue_time_raw = u16::from_le_bytes([h[3], h[4]]);
303    Some(TxStatusReport8814 {
304        header_offset,
305        queue_select: h[0] & 0x1f,
306        packet_broadcast: h[0] & (1 << 5) != 0,
307        lifetime_over: h[0] & (1 << 6) != 0,
308        retry_over: h[0] & (1 << 7) != 0,
309        mac_id: h[1],
310        data_retry_count: h[2] & 0x3f,
311        queue_time_us: u32::from(queue_time_raw) * 256,
312        final_data_rate: h[5],
313    })
314}
315
316const fn round_up_8(value: usize) -> usize {
317    (value + 7) & !7
318}
319
320fn le32(bytes: &[u8], offset: usize) -> u32 {
321    u32::from_le_bytes(
322        bytes[offset..offset + 4]
323            .try_into()
324            .expect("descriptor length checked"),
325    )
326}
327
328const fn bits(word: u32, offset: u8, len: u8) -> u32 {
329    if len == 32 {
330        word
331    } else {
332        (word >> offset) & ((1u32 << len) - 1)
333    }
334}
335
336fn parse_phy_status(attrib: &mut RxPacketAttrib, phy: &[u8]) {
337    if phy.len() < 2 {
338        return;
339    }
340
341    attrib.rssi[0] = phy[0];
342    attrib.rssi[1] = phy[1];
343
344    if phy.len() < 28 {
345        return;
346    }
347
348    attrib.rssi[2] = phy[23];
349    attrib.rssi[3] = phy[24];
350    attrib.snr[0] = phy[15] as i8;
351    attrib.snr[1] = phy[16] as i8;
352    attrib.snr[2] = phy[21] as i8;
353    attrib.snr[3] = phy[22] as i8;
354    attrib.evm[0] = phy[13] as i8;
355    attrib.evm[1] = phy[14] as i8;
356    attrib.evm[2] = phy[19] as i8;
357    attrib.evm[3] = phy[20] as i8;
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    fn put_bits(word: &mut u32, offset: u8, len: u8, value: u32) {
365        let mask = ((1u32 << len) - 1) << offset;
366        *word = (*word & !mask) | ((value << offset) & mask);
367    }
368
369    fn descriptor(pkt_len: u16, drvinfo_units: u8, shift: u8, seq: u16) -> [u8; RX_DESC_SIZE] {
370        let mut desc = [0; RX_DESC_SIZE];
371        let mut d0 = 0u32;
372        put_bits(&mut d0, 0, 14, pkt_len as u32);
373        put_bits(&mut d0, 16, 4, drvinfo_units as u32);
374        put_bits(&mut d0, 24, 2, shift as u32);
375        let mut d2 = 0u32;
376        put_bits(&mut d2, 0, 12, seq as u32);
377        desc[0..4].copy_from_slice(&d0.to_le_bytes());
378        desc[8..12].copy_from_slice(&d2.to_le_bytes());
379        desc
380    }
381
382    #[test]
383    fn parses_single_rx_packet() {
384        let mut aggregate = Vec::new();
385        aggregate.extend_from_slice(&descriptor(4, 0, 0, 77));
386        aggregate.extend_from_slice(&[1, 2, 3, 4]);
387
388        let packets = parse_rx_aggregate(&aggregate).unwrap();
389        assert_eq!(packets.len(), 1);
390        assert_eq!(packets[0].attrib.pkt_len, 4);
391        assert_eq!(packets[0].attrib.seq_num, 77);
392        assert_eq!(packets[0].data, &[1, 2, 3, 4]);
393    }
394
395    #[test]
396    fn parses_c2h_packet_header() {
397        let packet = parse_c2h_packet(&[0x42, 0x7f, 1, 2]).unwrap();
398        assert_eq!(packet.cmd_id, 0x42);
399        assert_eq!(packet.seq, Some(0x7f));
400        assert_eq!(packet.payload, &[1, 2]);
401    }
402
403    #[test]
404    fn parses_8814_tx_status_at_devourer_offsets() {
405        let bytes = [0xaa, 0x83, 0x09, 0x25, 0x34, 0x12, 0x6c, 0xbb];
406        let reports = parse_8814_tx_status_reports(&bytes);
407        assert_eq!(reports.len(), 2);
408        assert_eq!(reports[0].header_offset, 1);
409        assert_eq!(reports[0].queue_select, 3);
410        assert!(reports[0].retry_over);
411        assert_eq!(reports[0].mac_id, 9);
412        assert_eq!(reports[0].data_retry_count, 0x25);
413        assert_eq!(reports[0].queue_time_us, 0x1234 * 256);
414        assert_eq!(reports[0].final_data_rate, 0x6c);
415    }
416
417    #[test]
418    fn advances_by_jaguar_eight_byte_alignment() {
419        let mut aggregate = Vec::new();
420        aggregate.extend_from_slice(&descriptor(5, 0, 0, 1));
421        aggregate.extend_from_slice(&[1, 2, 3, 4, 5]);
422        aggregate.extend_from_slice(&[0, 0, 0]);
423        aggregate.extend_from_slice(&descriptor(3, 0, 0, 2));
424        aggregate.extend_from_slice(&[6, 7, 8]);
425
426        let packets = parse_rx_aggregate(&aggregate).unwrap();
427        assert_eq!(packets.len(), 2);
428        assert_eq!(packets[0].data, &[1, 2, 3, 4, 5]);
429        assert_eq!(packets[1].data, &[6, 7, 8]);
430    }
431}