pub const RX_DESC_SIZE: usize = 24;
pub const DEFAULT_RX_TRANSFER_SIZE: usize = 32 * 1024;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum RxDescriptorKind {
#[default]
Jaguar1,
Jaguar2,
Jaguar3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RxPacketType {
NormalRx,
C2hPacket,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RxPacketAttrib {
pub pkt_len: u16,
pub physt: bool,
pub drvinfo_sz: u8,
pub shift_sz: u8,
pub qos: bool,
pub priority: u8,
pub mdata: bool,
pub seq_num: u16,
pub frag_num: u8,
pub mfrag: bool,
pub bdecrypted: bool,
pub encrypt: u8,
pub crc_err: bool,
pub icv_err: bool,
pub tsfl: u32,
pub data_rate: u8,
pub bw: u8,
pub stbc: u8,
pub ldpc: u8,
pub sgi: u8,
pub scrambler: u8,
pub rssi: [u8; 4],
pub snr: [i8; 4],
pub evm: [i8; 4],
pub pkt_rpt_type: RxPacketType,
}
impl Default for RxPacketAttrib {
fn default() -> Self {
Self {
pkt_len: 0,
physt: false,
drvinfo_sz: 0,
shift_sz: 0,
qos: false,
priority: 0,
mdata: false,
seq_num: 0,
frag_num: 0,
mfrag: false,
bdecrypted: false,
encrypt: 0,
crc_err: false,
icv_err: false,
tsfl: 0,
data_rate: 0,
bw: 0,
stbc: 0,
ldpc: 0,
sgi: 0,
scrambler: 0,
rssi: [0; 4],
snr: [0; 4],
evm: [0; 4],
pkt_rpt_type: RxPacketType::NormalRx,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RealtekRxPacket<'a> {
pub attrib: RxPacketAttrib,
pub data: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct C2hPacket<'a> {
pub cmd_id: u8,
pub seq: Option<u8>,
pub payload: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TxStatusReport8814 {
pub header_offset: usize,
pub queue_select: u8,
pub packet_broadcast: bool,
pub lifetime_over: bool,
pub retry_over: bool,
pub mac_id: u8,
pub data_retry_count: u8,
pub queue_time_us: u32,
pub final_data_rate: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AggregateError {
DescriptorTooShort,
InvalidPacketLength {
pkt_len: u16,
pkt_offset: usize,
remaining: usize,
},
}
impl std::fmt::Display for AggregateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DescriptorTooShort => write!(f, "RX descriptor is shorter than {RX_DESC_SIZE} bytes"),
Self::InvalidPacketLength {
pkt_len,
pkt_offset,
remaining,
} => write!(
f,
"invalid RX packet length: pkt_len={pkt_len}, pkt_offset={pkt_offset}, remaining={remaining}"
),
}
}
}
impl std::error::Error for AggregateError {}
pub fn parse_rx_descriptor(desc: &[u8]) -> Result<RxPacketAttrib, AggregateError> {
parse_rx_descriptor_with_kind(desc, RxDescriptorKind::Jaguar1)
}
pub fn parse_rx_descriptor_with_kind(
desc: &[u8],
kind: RxDescriptorKind,
) -> Result<RxPacketAttrib, AggregateError> {
if desc.len() < RX_DESC_SIZE {
return Err(AggregateError::DescriptorTooShort);
}
let d0 = le32(desc, 0);
let d1 = le32(desc, 4);
let d2 = le32(desc, 8);
let d3 = le32(desc, 12);
let d4 = le32(desc, 16);
let d5 = le32(desc, 20);
let mut attrib = RxPacketAttrib {
pkt_len: bits(d0, 0, 14) as u16,
crc_err: bits(d0, 14, 1) != 0,
icv_err: bits(d0, 15, 1) != 0,
drvinfo_sz: (bits(d0, 16, 4) * 8) as u8,
shift_sz: bits(d0, 24, 2) as u8,
physt: bits(d0, 26, 1) != 0,
data_rate: bits(d3, 0, 7) as u8,
pkt_rpt_type: if bits(d2, 28, 1) != 0 {
RxPacketType::C2hPacket
} else {
RxPacketType::NormalRx
},
..Default::default()
};
if kind == RxDescriptorKind::Jaguar1 {
attrib.encrypt = bits(d0, 20, 3) as u8;
attrib.qos = bits(d0, 23, 1) != 0;
attrib.bdecrypted = bits(d0, 27, 1) == 0;
attrib.priority = bits(d1, 8, 4) as u8;
attrib.mdata = bits(d1, 26, 1) != 0;
attrib.mfrag = bits(d1, 27, 1) != 0;
attrib.seq_num = bits(d2, 0, 12) as u16;
attrib.frag_num = bits(d2, 12, 4) as u8;
attrib.sgi = bits(d4, 0, 1) as u8;
attrib.ldpc = bits(d4, 1, 1) as u8;
attrib.stbc = bits(d4, 2, 1) as u8;
attrib.bw = bits(d4, 4, 2) as u8;
attrib.scrambler = bits(d4, 9, 7) as u8;
attrib.tsfl = d5;
}
Ok(attrib)
}
pub fn parse_rx_aggregate(buf: &[u8]) -> Result<Vec<RealtekRxPacket<'_>>, AggregateError> {
parse_rx_aggregate_with_kind(buf, RxDescriptorKind::Jaguar1)
}
pub fn parse_rx_aggregate_with_kind(
buf: &[u8],
kind: RxDescriptorKind,
) -> Result<Vec<RealtekRxPacket<'_>>, AggregateError> {
let mut packets = Vec::new();
let mut offset = 0usize;
while offset < buf.len() {
let remaining = buf.len() - offset;
if remaining < RX_DESC_SIZE {
break;
}
let desc = &buf[offset..offset + RX_DESC_SIZE];
let mut attrib = parse_rx_descriptor_with_kind(desc, kind)?;
let data_start =
offset + RX_DESC_SIZE + attrib.drvinfo_sz as usize + attrib.shift_sz as usize;
let pkt_offset = RX_DESC_SIZE
+ attrib.drvinfo_sz as usize
+ attrib.shift_sz as usize
+ attrib.pkt_len as usize;
if attrib.pkt_len == 0 || pkt_offset > remaining {
return Err(AggregateError::InvalidPacketLength {
pkt_len: attrib.pkt_len,
pkt_offset,
remaining,
});
}
if attrib.pkt_rpt_type == RxPacketType::NormalRx {
let phy_start = offset + RX_DESC_SIZE;
let phy_end = phy_start + attrib.drvinfo_sz as usize;
parse_phy_status(&mut attrib, &buf[phy_start..phy_end]);
}
let data_end = data_start + attrib.pkt_len as usize;
packets.push(RealtekRxPacket {
attrib,
data: &buf[data_start..data_end],
});
let aligned = round_up_8(pkt_offset);
if aligned >= remaining {
break;
}
offset += aligned;
}
Ok(packets)
}
pub fn parse_c2h_packet(data: &[u8]) -> Option<C2hPacket<'_>> {
let (&cmd_id, rest) = data.split_first()?;
let seq = rest.first().copied();
let payload = if rest.len() > 1 { &rest[1..] } else { rest };
Some(C2hPacket {
cmd_id,
seq,
payload,
})
}
pub fn parse_8814_tx_status_reports(data: &[u8]) -> Vec<TxStatusReport8814> {
[1usize, 2usize]
.into_iter()
.filter_map(|offset| parse_8814_tx_status_report_at(data, offset))
.collect()
}
pub fn parse_8814_tx_status_report_at(
data: &[u8],
header_offset: usize,
) -> Option<TxStatusReport8814> {
let h = data.get(header_offset..header_offset + 6)?;
let queue_time_raw = u16::from_le_bytes([h[3], h[4]]);
Some(TxStatusReport8814 {
header_offset,
queue_select: h[0] & 0x1f,
packet_broadcast: h[0] & (1 << 5) != 0,
lifetime_over: h[0] & (1 << 6) != 0,
retry_over: h[0] & (1 << 7) != 0,
mac_id: h[1],
data_retry_count: h[2] & 0x3f,
queue_time_us: u32::from(queue_time_raw) * 256,
final_data_rate: h[5],
})
}
const fn round_up_8(value: usize) -> usize {
(value + 7) & !7
}
fn le32(bytes: &[u8], offset: usize) -> u32 {
u32::from_le_bytes(
bytes[offset..offset + 4]
.try_into()
.expect("descriptor length checked"),
)
}
const fn bits(word: u32, offset: u8, len: u8) -> u32 {
if len == 32 {
word
} else {
(word >> offset) & ((1u32 << len) - 1)
}
}
fn parse_phy_status(attrib: &mut RxPacketAttrib, phy: &[u8]) {
if phy.len() < 2 {
return;
}
attrib.rssi[0] = phy[0];
attrib.rssi[1] = phy[1];
if phy.len() < 28 {
return;
}
attrib.rssi[2] = phy[23];
attrib.rssi[3] = phy[24];
attrib.snr[0] = phy[15] as i8;
attrib.snr[1] = phy[16] as i8;
attrib.snr[2] = phy[21] as i8;
attrib.snr[3] = phy[22] as i8;
attrib.evm[0] = phy[13] as i8;
attrib.evm[1] = phy[14] as i8;
attrib.evm[2] = phy[19] as i8;
attrib.evm[3] = phy[20] as i8;
}
#[cfg(test)]
mod tests {
use super::*;
fn put_bits(word: &mut u32, offset: u8, len: u8, value: u32) {
let mask = ((1u32 << len) - 1) << offset;
*word = (*word & !mask) | ((value << offset) & mask);
}
fn descriptor(pkt_len: u16, drvinfo_units: u8, shift: u8, seq: u16) -> [u8; RX_DESC_SIZE] {
let mut desc = [0; RX_DESC_SIZE];
let mut d0 = 0u32;
put_bits(&mut d0, 0, 14, pkt_len as u32);
put_bits(&mut d0, 16, 4, drvinfo_units as u32);
put_bits(&mut d0, 24, 2, shift as u32);
let mut d2 = 0u32;
put_bits(&mut d2, 0, 12, seq as u32);
desc[0..4].copy_from_slice(&d0.to_le_bytes());
desc[8..12].copy_from_slice(&d2.to_le_bytes());
desc
}
fn jaguar3_descriptor(
pkt_len: u16,
drvinfo_units: u8,
shift: u8,
rx_rate: u8,
c2h: bool,
) -> [u8; RX_DESC_SIZE] {
let mut desc = [0; RX_DESC_SIZE];
let mut d0 = 0u32;
put_bits(&mut d0, 0, 14, pkt_len as u32);
put_bits(&mut d0, 14, 1, 1);
put_bits(&mut d0, 15, 1, 1);
put_bits(&mut d0, 16, 4, drvinfo_units as u32);
put_bits(&mut d0, 24, 2, shift as u32);
let mut d2 = 0u32;
put_bits(&mut d2, 28, 1, u32::from(c2h));
let mut d3 = 0u32;
put_bits(&mut d3, 0, 7, rx_rate as u32);
desc[0..4].copy_from_slice(&d0.to_le_bytes());
desc[8..12].copy_from_slice(&d2.to_le_bytes());
desc[12..16].copy_from_slice(&d3.to_le_bytes());
desc
}
#[test]
fn parses_single_rx_packet() {
let mut aggregate = Vec::new();
aggregate.extend_from_slice(&descriptor(4, 0, 0, 77));
aggregate.extend_from_slice(&[1, 2, 3, 4]);
let packets = parse_rx_aggregate(&aggregate).unwrap();
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].attrib.pkt_len, 4);
assert_eq!(packets[0].attrib.seq_num, 77);
assert_eq!(packets[0].data, &[1, 2, 3, 4]);
}
#[test]
fn ignores_short_tail_after_aligned_packet() {
let mut aggregate = Vec::new();
aggregate.extend_from_slice(&descriptor(4, 0, 0, 77));
aggregate.extend_from_slice(&[1, 2, 3, 4]);
aggregate.extend_from_slice(&[0, 0, 0, 0]);
aggregate.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
let packets = parse_rx_aggregate(&aggregate).unwrap();
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].data, &[1, 2, 3, 4]);
}
#[test]
fn rejects_zero_length_descriptor() {
let aggregate = descriptor(0, 0, 0, 1);
let err = parse_rx_aggregate(&aggregate).unwrap_err();
assert_eq!(
err,
AggregateError::InvalidPacketLength {
pkt_len: 0,
pkt_offset: RX_DESC_SIZE,
remaining: RX_DESC_SIZE,
}
);
}
#[test]
fn rejects_descriptor_payload_past_transfer() {
let mut aggregate = Vec::new();
aggregate.extend_from_slice(&descriptor(8, 0, 0, 1));
aggregate.extend_from_slice(&[1, 2, 3, 4]);
let err = parse_rx_aggregate(&aggregate).unwrap_err();
assert_eq!(
err,
AggregateError::InvalidPacketLength {
pkt_len: 8,
pkt_offset: RX_DESC_SIZE + 8,
remaining: RX_DESC_SIZE + 4,
}
);
}
#[test]
fn surfaces_crc_and_icv_flags_from_jaguar1_descriptor() {
let mut desc = descriptor(4, 0, 0, 9);
let mut d0 = u32::from_le_bytes(desc[0..4].try_into().unwrap());
put_bits(&mut d0, 14, 1, 1);
put_bits(&mut d0, 15, 1, 1);
desc[0..4].copy_from_slice(&d0.to_le_bytes());
let mut aggregate = Vec::new();
aggregate.extend_from_slice(&desc);
aggregate.extend_from_slice(&[1, 2, 3, 4]);
let packets = parse_rx_aggregate(&aggregate).unwrap();
assert!(packets[0].attrib.crc_err);
assert!(packets[0].attrib.icv_err);
assert_eq!(packets[0].data, &[1, 2, 3, 4]);
}
#[test]
fn does_not_decode_payload_as_phy_status_when_drvinfo_absent() {
let mut payload = vec![0u8; 32];
payload[0] = 55;
payload[1] = 66;
payload[13] = 0x80;
payload[15] = 0x7f;
payload[23] = 77;
let mut aggregate = Vec::new();
aggregate.extend_from_slice(&descriptor(payload.len() as u16, 0, 0, 1));
aggregate.extend_from_slice(&payload);
let packets = parse_rx_aggregate(&aggregate).unwrap();
assert_eq!(packets[0].attrib.rssi, [0; 4]);
assert_eq!(packets[0].attrib.snr, [0; 4]);
assert_eq!(packets[0].attrib.evm, [0; 4]);
assert_eq!(packets[0].data, payload);
}
#[test]
fn parses_phy_status_only_from_driver_info_bytes() {
let mut phy = vec![0u8; 32];
phy[0] = 42;
phy[1] = 43;
phy[13] = 0xf0;
phy[14] = 0x0f;
phy[15] = 0x80;
phy[16] = 0x7f;
phy[19] = 0xee;
phy[20] = 0xdd;
phy[21] = 0xcc;
phy[22] = 0xbb;
phy[23] = 44;
phy[24] = 45;
let mut aggregate = Vec::new();
aggregate.extend_from_slice(&descriptor(3, 4, 1, 1));
aggregate.extend_from_slice(&phy);
aggregate.push(0xaa);
aggregate.extend_from_slice(&[1, 2, 3]);
let packets = parse_rx_aggregate(&aggregate).unwrap();
assert_eq!(packets[0].attrib.drvinfo_sz, 32);
assert_eq!(packets[0].attrib.shift_sz, 1);
assert_eq!(packets[0].attrib.rssi, [42, 43, 44, 45]);
assert_eq!(packets[0].attrib.snr, [-128, 127, -52, -69]);
assert_eq!(packets[0].attrib.evm, [-16, 15, -18, -35]);
assert_eq!(packets[0].data, &[1, 2, 3]);
}
#[test]
fn parses_c2h_packet_header() {
let packet = parse_c2h_packet(&[0x42, 0x7f, 1, 2]).unwrap();
assert_eq!(packet.cmd_id, 0x42);
assert_eq!(packet.seq, Some(0x7f));
assert_eq!(packet.payload, &[1, 2]);
}
#[test]
fn parses_8814_tx_status_at_devourer_offsets() {
let bytes = [0xaa, 0x83, 0x09, 0x25, 0x34, 0x12, 0x6c, 0xbb];
let reports = parse_8814_tx_status_reports(&bytes);
assert_eq!(reports.len(), 2);
assert_eq!(reports[0].header_offset, 1);
assert_eq!(reports[0].queue_select, 3);
assert!(reports[0].retry_over);
assert_eq!(reports[0].mac_id, 9);
assert_eq!(reports[0].data_retry_count, 0x25);
assert_eq!(reports[0].queue_time_us, 0x1234 * 256);
assert_eq!(reports[0].final_data_rate, 0x6c);
}
#[test]
fn advances_by_jaguar_eight_byte_alignment() {
let mut aggregate = Vec::new();
aggregate.extend_from_slice(&descriptor(5, 0, 0, 1));
aggregate.extend_from_slice(&[1, 2, 3, 4, 5]);
aggregate.extend_from_slice(&[0, 0, 0]);
aggregate.extend_from_slice(&descriptor(3, 0, 0, 2));
aggregate.extend_from_slice(&[6, 7, 8]);
let packets = parse_rx_aggregate(&aggregate).unwrap();
assert_eq!(packets.len(), 2);
assert_eq!(packets[0].data, &[1, 2, 3, 4, 5]);
assert_eq!(packets[1].data, &[6, 7, 8]);
}
#[test]
fn jaguar3_descriptor_matches_devourer_field_positions() {
let mut aggregate = Vec::new();
aggregate.extend_from_slice(&jaguar3_descriptor(4, 1, 2, 0x2c, false));
aggregate.extend_from_slice(&[41, 42, 0, 0, 0, 0, 0, 0]);
aggregate.extend_from_slice(&[0xaa, 0xbb]);
aggregate.extend_from_slice(&[1, 2, 3, 4]);
let packets = parse_rx_aggregate_with_kind(&aggregate, RxDescriptorKind::Jaguar3).unwrap();
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].attrib.pkt_len, 4);
assert_eq!(packets[0].attrib.drvinfo_sz, 8);
assert_eq!(packets[0].attrib.shift_sz, 2);
assert_eq!(packets[0].attrib.data_rate, 0x2c);
assert!(packets[0].attrib.crc_err);
assert!(packets[0].attrib.icv_err);
assert_eq!(packets[0].attrib.pkt_rpt_type, RxPacketType::NormalRx);
assert_eq!(packets[0].data, &[1, 2, 3, 4]);
}
#[test]
fn jaguar3_c2h_bit_is_report_type() {
let mut aggregate = Vec::new();
aggregate.extend_from_slice(&jaguar3_descriptor(3, 0, 0, 0, true));
aggregate.extend_from_slice(&[0x61, 0x01, 0x02]);
let packets = parse_rx_aggregate_with_kind(&aggregate, RxDescriptorKind::Jaguar3).unwrap();
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].attrib.pkt_rpt_type, RxPacketType::C2hPacket);
assert_eq!(packets[0].data, &[0x61, 0x01, 0x02]);
}
#[test]
fn c2h_payload_respects_drvinfo_and_shift_offsets() {
let mut desc = descriptor(3, 1, 2, 0);
let mut d2 = u32::from_le_bytes(desc[8..12].try_into().unwrap());
put_bits(&mut d2, 28, 1, 1);
desc[8..12].copy_from_slice(&d2.to_le_bytes());
let mut aggregate = Vec::new();
aggregate.extend_from_slice(&desc);
aggregate.extend_from_slice(&[0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48]);
aggregate.extend_from_slice(&[0xaa, 0xbb]);
aggregate.extend_from_slice(&[0x61, 0x07, 0xcc]);
let packets = parse_rx_aggregate(&aggregate).unwrap();
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].attrib.pkt_rpt_type, RxPacketType::C2hPacket);
assert_eq!(packets[0].attrib.drvinfo_sz, 8);
assert_eq!(packets[0].attrib.shift_sz, 2);
assert_eq!(packets[0].data, &[0x61, 0x07, 0xcc]);
assert_eq!(
parse_c2h_packet(packets[0].data),
Some(C2hPacket {
cmd_id: 0x61,
seq: Some(0x07),
payload: &[0xcc],
})
);
}
}