feat(iwlwifi): expand MLD into directory module with 6 subsystems
Splits mld.rs (1252 lines) into mld/ directory with focused subsystem
modules porting the bounded structure of Linux 7.1 mld/*.c:
mld/mod.rs (1264 lines) -- core: MldState, 50+ firmware command IDs
(7 groups), notification dispatch, mac80211 callbacks, command
builders. Added frame_count/byte_count to Txq for TX accounting.
Added FwNotRunning/InvalidState/InvalidId to MldError.
mld/sta.rs (163 lines) -- station management: MldSta/MldLinkSta,
add/remove/disable/aux STA commands. Restored from 41c5926.
mld/scan.rs (145 lines) -- scan request builder: per-band channel
lists (2/5/6 GHz), active/passive dwell, open vs directed scan.
mld/link.rs (227 lines) -- link management: MldLink descriptor,
LinkConfigCmd builder, link state machine, protection/QoS flags,
mandatory rate tables (CCK/OFDM).
mld/tx.rs (225 lines) -- TX path: TxCmd structure, TID->AC mapping
(802.11e), TX status decode, TX command builder, queue accounting.
mld/key.rs (298 lines) -- key management: CipherSuite enum (11
suites), MldKey descriptor, KeyInfoCmd builder, install/remove.
mld/agg.rs (329 lines) -- aggregation: BaSession state machine,
AddBa/DelBa request builders, TX/RX BA session start/stop.
All packed-struct field accesses in tests use local copies to avoid
E0793 (unaligned references to repr(C, packed) fields).
51 tests pass (23 original + 28 new). Compiles clean.
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
//! MLD aggregation (AMPDU/AMSDU) — Rust port of Linux mld/agg.c
|
||||
//!
|
||||
//! Reference: Linux 7.1 drivers/net/wireless/intel/iwlwifi/mld/agg.c (~686 LOC)
|
||||
//! Manages BlockAck session establishment/teardown via the ADD_BA / DEL_BA
|
||||
//! mac80211 callbacks. Constructs SCD (Scheduler) queue requests, programs
|
||||
//! aggregation thresholds, and tracks per-TID BA state for each station.
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── AMPDU action codes (Linux include/net/mac80211.h IEEE80211_AMPDU_*) ─
|
||||
|
||||
pub const AMPDU_TX_START: u32 = 0;
|
||||
pub const AMPDU_TX_STOP_CONT: u32 = 1;
|
||||
pub const AMPDU_TX_STOP_FLUSH: u32 = 2;
|
||||
pub const AMPDU_TX_STOP_FLUSH_CONT: u32 = 3;
|
||||
pub const AMPDU_TX_OPERATIONAL: u32 = 4;
|
||||
pub const AMPDU_RX_START: u32 = 5;
|
||||
pub const AMPDU_RX_STOP: u32 = 6;
|
||||
|
||||
// ── BA session state (Linux mld/agg.h struct iwl_mld_ba_data) ─────
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum BaSessionState {
|
||||
#[default]
|
||||
Idle,
|
||||
StartRequested,
|
||||
Pending,
|
||||
Active,
|
||||
Stopping,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct BaSession {
|
||||
pub tid: u8,
|
||||
pub sta_id: u32,
|
||||
pub state: BaSessionState,
|
||||
pub timeout_ms: u32,
|
||||
pub ssn: u16,
|
||||
pub window_size: u16,
|
||||
pub txq_id: u16,
|
||||
pub is_tx: bool,
|
||||
pub last_seq: u16,
|
||||
pub mpdus_queued: u32,
|
||||
pub mpdus_acked: u32,
|
||||
pub mpdus_lost: u32,
|
||||
}
|
||||
|
||||
impl BaSession {
|
||||
pub fn new_tx(sta_id: u32, tid: u8, txq_id: u16) -> Self {
|
||||
Self {
|
||||
tid,
|
||||
sta_id,
|
||||
state: BaSessionState::Idle,
|
||||
timeout_ms: 5000,
|
||||
window_size: 64,
|
||||
txq_id,
|
||||
is_tx: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_rx(sta_id: u32, tid: u8) -> Self {
|
||||
Self {
|
||||
tid,
|
||||
sta_id,
|
||||
state: BaSessionState::Idle,
|
||||
timeout_ms: 5000,
|
||||
window_size: 64,
|
||||
is_tx: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_expired(&self, now_ms: u32) -> bool {
|
||||
self.timeout_ms > 0 && now_ms.saturating_sub(self.ssn as u32) > self.timeout_ms
|
||||
}
|
||||
|
||||
pub fn ack_ratio(&self) -> f32 {
|
||||
if self.mpdus_queued == 0 {
|
||||
return 1.0;
|
||||
}
|
||||
self.mpdus_acked as f32 / self.mpdus_queued as f32
|
||||
}
|
||||
}
|
||||
|
||||
// ── AddBA request (Linux fw/api/datapath.h struct iwl_scd_txq_cfg) ─
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct AddBaRequest {
|
||||
pub sta_id: u32,
|
||||
pub tid: u32,
|
||||
pub scd_queue: u32,
|
||||
pub flags: u32,
|
||||
pub ssn: u32,
|
||||
pub agg_window_size: u32,
|
||||
pub byte_limit: u32,
|
||||
pub frame_limit: u32,
|
||||
}
|
||||
|
||||
pub const ADD_BA_FLAGS_TX: u32 = 1 << 0;
|
||||
pub const ADD_BA_FLAGS_START: u32 = 1 << 1;
|
||||
|
||||
pub fn build_add_ba_request(session: &BaSession) -> AddBaRequest {
|
||||
AddBaRequest {
|
||||
sta_id: session.sta_id,
|
||||
tid: session.tid as u32,
|
||||
scd_queue: session.txq_id as u32,
|
||||
flags: if session.is_tx {
|
||||
ADD_BA_FLAGS_TX | ADD_BA_FLAGS_START
|
||||
} else {
|
||||
ADD_BA_FLAGS_START
|
||||
},
|
||||
ssn: session.ssn as u32,
|
||||
agg_window_size: session.window_size as u32,
|
||||
byte_limit: 0,
|
||||
frame_limit: 64,
|
||||
}
|
||||
}
|
||||
|
||||
// ── DelBA request ─────────────────────────────────────────────────
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct DelBaRequest {
|
||||
pub sta_id: u32,
|
||||
pub tid: u32,
|
||||
pub scd_queue: u32,
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
pub fn build_del_ba_request(session: &BaSession) -> DelBaRequest {
|
||||
DelBaRequest {
|
||||
sta_id: session.sta_id,
|
||||
tid: session.tid as u32,
|
||||
scd_queue: session.txq_id as u32,
|
||||
flags: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// ── State machine helpers ─────────────────────────────────────────
|
||||
|
||||
pub fn transition_ba(
|
||||
current: BaSessionState,
|
||||
target: BaSessionState,
|
||||
) -> Result<BaSessionState, MldError> {
|
||||
use BaSessionState::*;
|
||||
match (current, target) {
|
||||
(Idle, StartRequested) => Ok(StartRequested),
|
||||
(StartRequested, Pending) => Ok(Pending),
|
||||
(Pending, Active) => Ok(Active),
|
||||
(Active, Stopping) => Ok(Stopping),
|
||||
(Stopping, Idle) => Ok(Idle),
|
||||
(_, Idle) => Ok(Idle), // abort allowed
|
||||
_ => Err(MldError::InvalidState),
|
||||
}
|
||||
}
|
||||
|
||||
// ── MldState helpers for aggregation ──────────────────────────────
|
||||
|
||||
impl MldState {
|
||||
pub fn agg_start_tx(
|
||||
&mut self,
|
||||
sta_id: u32,
|
||||
tid: u8,
|
||||
txq_id: u16,
|
||||
ssn: u16,
|
||||
window: u16,
|
||||
) -> Result<BaSession, MldError> {
|
||||
if !self.fw_running {
|
||||
return Err(MldError::FwNotRunning);
|
||||
}
|
||||
if tid > 7 {
|
||||
return Err(MldError::InvalidArgument);
|
||||
}
|
||||
let mut session = BaSession::new_tx(sta_id, tid, txq_id);
|
||||
session.ssn = ssn;
|
||||
session.window_size = window.max(16).min(256);
|
||||
session.state = transition_ba(BaSessionState::Idle, BaSessionState::StartRequested)?;
|
||||
let req = build_add_ba_request(&session);
|
||||
let bytes = unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
&req as *const AddBaRequest as *const u8,
|
||||
core::mem::size_of::<AddBaRequest>(),
|
||||
)
|
||||
};
|
||||
self.send_hcmd(CMD_RLC_CONFIG, bytes)?;
|
||||
session.state = BaSessionState::Pending;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
pub fn agg_start_rx(
|
||||
&mut self,
|
||||
sta_id: u32,
|
||||
tid: u8,
|
||||
ssn: u16,
|
||||
window: u16,
|
||||
) -> Result<BaSession, MldError> {
|
||||
if !self.fw_running {
|
||||
return Err(MldError::FwNotRunning);
|
||||
}
|
||||
if tid > 7 {
|
||||
return Err(MldError::InvalidArgument);
|
||||
}
|
||||
let mut session = BaSession::new_rx(sta_id, tid);
|
||||
session.ssn = ssn;
|
||||
session.window_size = window.max(16).min(256);
|
||||
session.state = transition_ba(BaSessionState::Idle, BaSessionState::StartRequested)?;
|
||||
let req = build_add_ba_request(&session);
|
||||
let bytes = unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
&req as *const AddBaRequest as *const u8,
|
||||
core::mem::size_of::<AddBaRequest>(),
|
||||
)
|
||||
};
|
||||
self.send_hcmd(CMD_RLC_CONFIG, bytes)?;
|
||||
session.state = BaSessionState::Active;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
pub fn agg_stop(&mut self, session: &mut BaSession) -> Result<(), MldError> {
|
||||
session.state = transition_ba(session.state, BaSessionState::Stopping)?;
|
||||
let req = build_del_ba_request(session);
|
||||
let bytes = unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
&req as *const DelBaRequest as *const u8,
|
||||
core::mem::size_of::<DelBaRequest>(),
|
||||
)
|
||||
};
|
||||
self.send_hcmd(CMD_RLC_CONFIG, bytes)?;
|
||||
session.state = BaSessionState::Idle;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn agg_mark_operational(&mut self, session: &mut BaSession) -> Result<(), MldError> {
|
||||
session.state = transition_ba(session.state, BaSessionState::Active)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ba_session_tx_creation() {
|
||||
let s = BaSession::new_tx(2, 5, 17);
|
||||
assert_eq!(s.sta_id, 2);
|
||||
assert_eq!(s.tid, 5);
|
||||
assert_eq!(s.txq_id, 17);
|
||||
assert!(s.is_tx);
|
||||
assert_eq!(s.state, BaSessionState::Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ba_session_rx_creation() {
|
||||
let s = BaSession::new_rx(4, 0);
|
||||
assert_eq!(s.sta_id, 4);
|
||||
assert!(!s.is_tx);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ba_state_machine_legal() {
|
||||
use BaSessionState::*;
|
||||
let s = transition_ba(Idle, StartRequested).unwrap();
|
||||
assert_eq!(s, StartRequested);
|
||||
let s = transition_ba(StartRequested, Pending).unwrap();
|
||||
assert_eq!(s, Pending);
|
||||
let s = transition_ba(Pending, Active).unwrap();
|
||||
assert_eq!(s, Active);
|
||||
let s = transition_ba(Active, Stopping).unwrap();
|
||||
assert_eq!(s, Stopping);
|
||||
let s = transition_ba(Stopping, Idle).unwrap();
|
||||
assert_eq!(s, Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ba_state_machine_illegal() {
|
||||
let res = transition_ba(BaSessionState::Idle, BaSessionState::Active);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ba_state_machine_abort_from_anywhere() {
|
||||
for from in [
|
||||
BaSessionState::Idle,
|
||||
BaSessionState::StartRequested,
|
||||
BaSessionState::Pending,
|
||||
BaSessionState::Active,
|
||||
BaSessionState::Stopping,
|
||||
] {
|
||||
assert_eq!(transition_ba(from, BaSessionState::Idle).unwrap(), BaSessionState::Idle);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ba_request_has_tx_flag_for_tx_session() {
|
||||
let session = BaSession::new_tx(1, 3, 8);
|
||||
let req = build_add_ba_request(&session);
|
||||
let (sta_id, tid, scd_queue, flags) = (req.sta_id, req.tid, req.scd_queue, req.flags);
|
||||
assert_eq!(sta_id, 1);
|
||||
assert_eq!(tid, 3);
|
||||
assert_eq!(scd_queue, 8);
|
||||
assert!(flags & ADD_BA_FLAGS_TX != 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ba_request_no_tx_flag_for_rx_session() {
|
||||
let session = BaSession::new_rx(1, 3);
|
||||
let req = build_add_ba_request(&session);
|
||||
assert_eq!(req.flags & ADD_BA_FLAGS_TX, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ack_ratio_zero_when_no_traffic() {
|
||||
let s = BaSession::default();
|
||||
assert_eq!(s.ack_ratio(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ack_ratio_computes_correctly() {
|
||||
let mut s = BaSession::default();
|
||||
s.mpdus_queued = 100;
|
||||
s.mpdus_acked = 75;
|
||||
assert!((s.ack_ratio() - 0.75).abs() < 0.001);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
//! MLD key management — Rust port of Linux mld/key.c
|
||||
//!
|
||||
//! Reference: Linux 7.1 drivers/net/wireless/intel/iwlwifi/mld/key.c (~472 LOC)
|
||||
//! Manages pairwise and group encryption keys via STA_CONFIG and
|
||||
//! RLC (Radio Link Control) commands. Maps mac80211 cipher suites to
|
||||
//! firmware key type IDs and tracks key state per link/station.
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Cipher suites (Linux fw/api/sta.h enum iwl_sta_key_flag_key_type) ─
|
||||
|
||||
pub const KEY_TYPE_NONE: u32 = 0;
|
||||
pub const KEY_TYPE_WEP40: u32 = 1;
|
||||
pub const KEY_TYPE_WEP104: u32 = 2;
|
||||
pub const KEY_TYPE_TKIP: u32 = 3;
|
||||
pub const KEY_TYPE_CCMP: u32 = 4;
|
||||
pub const KEY_TYPE_WEP: u32 = 5;
|
||||
pub const KEY_TYPE_BIP_CMAC_128: u32 = 6;
|
||||
pub const KEY_TYPE_GCM_128: u32 = 7;
|
||||
pub const KEY_TYPE_GCM_256: u32 = 8;
|
||||
pub const KEY_TYPE_CCMP_256: u32 = 9;
|
||||
pub const KEY_TYPE_BIP_GMAC_128: u32 = 10;
|
||||
pub const KEY_TYPE_BIP_GMAC_256: u32 = 11;
|
||||
|
||||
// ── Key flags (Linux fw/api/sta.h STA_KEY_FLG_*) ──────────────────
|
||||
|
||||
pub const STA_KEY_FLG_CCM: u32 = 1 << 0;
|
||||
pub const STA_KEY_FLG_TKIP: u32 = 1 << 1;
|
||||
pub const STA_KEY_FLG_WEP: u32 = 1 << 2;
|
||||
pub const STA_KEY_FLG_PAIRWISE: u32 = 1 << 4;
|
||||
pub const STA_KEY_FLG_GROUP: u32 = 1 << 5;
|
||||
pub const STA_KEY_FLG_MFP: u32 = 1 << 6;
|
||||
pub const STA_KEY_FLG_UNKNOWN: u32 = 1 << 7;
|
||||
|
||||
// ── Max key length (CCMP-256 = 32 bytes) ──────────────────────────
|
||||
|
||||
pub const MAX_KEY_LEN: usize = 32;
|
||||
pub const MAX_KEY_IDX: usize = 4;
|
||||
|
||||
// ── mac80211 cipher suite OUI+type (Linux include/linux/ieee80211.h) ─
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CipherSuite {
|
||||
None,
|
||||
Wep40,
|
||||
Wep104,
|
||||
Tkip,
|
||||
Ccmp,
|
||||
Ccmp256,
|
||||
Gcmp128,
|
||||
Gcmp256,
|
||||
BipCmac128,
|
||||
BipGmac128,
|
||||
BipGmac256,
|
||||
}
|
||||
|
||||
impl CipherSuite {
|
||||
pub fn to_fw_key_type(self) -> u32 {
|
||||
match self {
|
||||
Self::None => KEY_TYPE_NONE,
|
||||
Self::Wep40 => KEY_TYPE_WEP40,
|
||||
Self::Wep104 => KEY_TYPE_WEP104,
|
||||
Self::Tkip => KEY_TYPE_TKIP,
|
||||
Self::Ccmp => KEY_TYPE_CCMP,
|
||||
Self::Ccmp256 => KEY_TYPE_CCMP_256,
|
||||
Self::Gcmp128 => KEY_TYPE_GCM_128,
|
||||
Self::Gcmp256 => KEY_TYPE_GCM_256,
|
||||
Self::BipCmac128 => KEY_TYPE_BIP_CMAC_128,
|
||||
Self::BipGmac128 => KEY_TYPE_BIP_GMAC_128,
|
||||
Self::BipGmac256 => KEY_TYPE_BIP_GMAC_256,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_key_len(self) -> usize {
|
||||
match self {
|
||||
Self::None => 0,
|
||||
Self::Wep40 => 5,
|
||||
Self::Wep104 => 13,
|
||||
Self::Tkip => 32, // 16 key + 16 MIC
|
||||
Self::Ccmp => 16,
|
||||
Self::Ccmp256 => 32,
|
||||
Self::Gcmp128 => 16,
|
||||
Self::Gcmp256 => 32,
|
||||
Self::BipCmac128 => 16,
|
||||
Self::BipGmac128 | Self::BipGmac256 => 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Key descriptor (Linux mld/key.h struct iwl_mld_key) ───────────
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MldKey {
|
||||
pub key_idx: u8,
|
||||
pub sta_id: u32,
|
||||
pub link_id: u16,
|
||||
pub cipher: CipherSuite,
|
||||
pub pairwise: bool,
|
||||
pub mfp: bool,
|
||||
pub key: Vec<u8>,
|
||||
pub rx_mic: Option<Vec<u8>>,
|
||||
pub tx_mic: Option<Vec<u8>>,
|
||||
pub rsc: u64,
|
||||
}
|
||||
|
||||
impl Default for MldKey {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
key_idx: 0,
|
||||
sta_id: 0,
|
||||
link_id: 0,
|
||||
cipher: CipherSuite::None,
|
||||
pairwise: false,
|
||||
mfp: false,
|
||||
key: Vec::new(),
|
||||
rx_mic: None,
|
||||
tx_mic: None,
|
||||
rsc: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MldKey {
|
||||
pub fn new_ccmp(sta_id: u32, key_idx: u8, pairwise: bool, key: &[u8]) -> Result<Self, MldError> {
|
||||
if key.len() != 16 && key.len() != 32 {
|
||||
return Err(MldError::InvalidArgument);
|
||||
}
|
||||
let cipher = if key.len() == 32 {
|
||||
CipherSuite::Ccmp256
|
||||
} else {
|
||||
CipherSuite::Ccmp
|
||||
};
|
||||
Ok(Self {
|
||||
sta_id,
|
||||
key_idx,
|
||||
pairwise,
|
||||
cipher,
|
||||
key: key.to_vec(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Key command structure (Linux fw/api/sta.h struct iwl_mvm_key_info) ─
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct KeyInfoCmd {
|
||||
pub sta_id: u32,
|
||||
pub key_idx: u32,
|
||||
pub key_flags: u32,
|
||||
pub rx_secur_seq_cnt: u64,
|
||||
pub key_type: u32,
|
||||
pub key: [u8; MAX_KEY_LEN],
|
||||
pub key_len: u16,
|
||||
pub tx_mic: [u8; 8],
|
||||
pub rx_mic: [u8; 8],
|
||||
}
|
||||
|
||||
pub fn build_key_info_cmd(key: &MldKey) -> KeyInfoCmd {
|
||||
let mut flags = 0u32;
|
||||
if key.pairwise {
|
||||
flags |= STA_KEY_FLG_PAIRWISE;
|
||||
} else {
|
||||
flags |= STA_KEY_FLG_GROUP;
|
||||
}
|
||||
if key.mfp {
|
||||
flags |= STA_KEY_FLG_MFP;
|
||||
}
|
||||
flags |= match key.cipher {
|
||||
CipherSuite::Ccmp | CipherSuite::Ccmp256 => STA_KEY_FLG_CCM,
|
||||
CipherSuite::Tkip => STA_KEY_FLG_TKIP,
|
||||
CipherSuite::Wep40 | CipherSuite::Wep104 => STA_KEY_FLG_WEP,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let mut key_buf = [0u8; MAX_KEY_LEN];
|
||||
let copy_len = key.key.len().min(MAX_KEY_LEN);
|
||||
key_buf[..copy_len].copy_from_slice(&key.key[..copy_len]);
|
||||
|
||||
let mut tx_mic = [0u8; 8];
|
||||
let mut rx_mic = [0u8; 8];
|
||||
if let Some(m) = &key.tx_mic {
|
||||
let n = m.len().min(8);
|
||||
tx_mic[..n].copy_from_slice(&m[..n]);
|
||||
}
|
||||
if let Some(m) = &key.rx_mic {
|
||||
let n = m.len().min(8);
|
||||
rx_mic[..n].copy_from_slice(&m[..n]);
|
||||
}
|
||||
|
||||
KeyInfoCmd {
|
||||
sta_id: key.sta_id,
|
||||
key_idx: key.key_idx as u32,
|
||||
key_flags: flags,
|
||||
rx_secur_seq_cnt: key.rsc,
|
||||
key_type: key.cipher.to_fw_key_type(),
|
||||
key: key_buf,
|
||||
key_len: key.key.len() as u16,
|
||||
tx_mic,
|
||||
rx_mic,
|
||||
}
|
||||
}
|
||||
|
||||
// ── MldState helpers for key install/remove ───────────────────────
|
||||
|
||||
impl MldState {
|
||||
pub fn install_key(&self, key: &MldKey) -> Result<(), MldError> {
|
||||
if !self.fw_running {
|
||||
return Err(MldError::FwNotRunning);
|
||||
}
|
||||
if key.key_idx as usize >= MAX_KEY_IDX {
|
||||
return Err(MldError::InvalidArgument);
|
||||
}
|
||||
let cmd = build_key_info_cmd(key);
|
||||
let bytes = unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
&cmd as *const KeyInfoCmd as *const u8,
|
||||
core::mem::size_of::<KeyInfoCmd>(),
|
||||
)
|
||||
};
|
||||
self.send_hcmd(CMD_STA_HE_CTXT, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_key(&self, sta_id: u32, key_idx: u8) -> Result<(), MldError> {
|
||||
let mut key = MldKey::default();
|
||||
key.sta_id = sta_id;
|
||||
key.key_idx = key_idx;
|
||||
key.cipher = CipherSuite::None;
|
||||
let cmd = build_key_info_cmd(&key);
|
||||
let bytes = unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
&cmd as *const KeyInfoCmd as *const u8,
|
||||
core::mem::size_of::<KeyInfoCmd>(),
|
||||
)
|
||||
};
|
||||
self.send_hcmd(CMD_STA_HE_CTXT, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cipher_to_fw_key_type_roundtrip() {
|
||||
assert_eq!(CipherSuite::Ccmp.to_fw_key_type(), KEY_TYPE_CCMP);
|
||||
assert_eq!(CipherSuite::Gcmp256.to_fw_key_type(), KEY_TYPE_GCM_256);
|
||||
assert_eq!(CipherSuite::Tkip.to_fw_key_type(), KEY_TYPE_TKIP);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_key_lengths() {
|
||||
assert_eq!(CipherSuite::Ccmp.default_key_len(), 16);
|
||||
assert_eq!(CipherSuite::Ccmp256.default_key_len(), 32);
|
||||
assert_eq!(CipherSuite::Wep40.default_key_len(), 5);
|
||||
assert_eq!(CipherSuite::Wep104.default_key_len(), 13);
|
||||
assert_eq!(CipherSuite::None.default_key_len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ccmp_key_create_valid() {
|
||||
let key_bytes = [0xAA; 16];
|
||||
let key = MldKey::new_ccmp(3, 0, true, &key_bytes).unwrap();
|
||||
assert_eq!(key.cipher, CipherSuite::Ccmp);
|
||||
assert!(key.pairwise);
|
||||
assert_eq!(key.key, key_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ccmp_key_create_invalid_length() {
|
||||
let bad = [0; 7];
|
||||
assert!(MldKey::new_ccmp(0, 0, false, &bad).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_key_info_cmd_sets_pairwise_flag() {
|
||||
let key = MldKey {
|
||||
sta_id: 1,
|
||||
key_idx: 0,
|
||||
pairwise: true,
|
||||
mfp: true,
|
||||
cipher: CipherSuite::Ccmp,
|
||||
key: vec![0xBB; 16],
|
||||
..Default::default()
|
||||
};
|
||||
let cmd = build_key_info_cmd(&key);
|
||||
let (key_flags, key_type) = (cmd.key_flags, cmd.key_type);
|
||||
assert!(key_flags & STA_KEY_FLG_PAIRWISE != 0);
|
||||
assert!(key_flags & STA_KEY_FLG_MFP != 0);
|
||||
assert!(key_flags & STA_KEY_FLG_CCM != 0);
|
||||
assert_eq!(key_type, KEY_TYPE_CCMP);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
//! MLD link management — Rust port of Linux mld/link.c
|
||||
//!
|
||||
//! Reference: Linux 7.1 drivers/net/wireless/intel/iwlwifi/mld/link.c (~1.3K LOC)
|
||||
//! The full Linux link.c builds LINK_CONFIG_CMD with rate tables, protection
|
||||
//! flags, QoS params, and handles link activation/deactivation for both
|
||||
//! legacy and MLO. This bounded port provides the link descriptor state
|
||||
//! and helpers to construct LINK_CONFIG_CMD payloads.
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Link descriptor (Linux mld/link.h struct iwl_mld_link) ─────────
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct MldLink {
|
||||
pub fw_id: u32,
|
||||
pub mac_id: u32,
|
||||
pub phy_id: u32,
|
||||
pub link_id: u16,
|
||||
pub active: bool,
|
||||
pub local_addr: [u8; 6],
|
||||
pub cck_rates: u32,
|
||||
pub ofdm_rates: u32,
|
||||
pub protection_flags: u32,
|
||||
pub qos_flags: u32,
|
||||
}
|
||||
|
||||
impl MldLink {
|
||||
pub fn new(link_id: u16, mac_id: u32, phy_id: u32) -> Self {
|
||||
Self {
|
||||
fw_id: 0xFF,
|
||||
mac_id,
|
||||
phy_id,
|
||||
link_id,
|
||||
active: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_link_config_cmd(&self, action: u32, modify_mask: u32) -> LinkConfigCmd {
|
||||
LinkConfigCmd {
|
||||
action,
|
||||
link_id: self.link_id as u32,
|
||||
mac_id: self.mac_id,
|
||||
phy_id: self.phy_id,
|
||||
local_link_addr: self.local_addr,
|
||||
reserved_for_local_link_addr: 0,
|
||||
modify_mask,
|
||||
active: if self.active { 1 } else { 0 },
|
||||
cck_rates: self.cck_rates,
|
||||
ofdm_rates: self.ofdm_rates,
|
||||
cck_short_preamble: 0,
|
||||
short_slot: 1,
|
||||
protection_flags: self.protection_flags,
|
||||
qos_flags: self.qos_flags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Protection / QoS flag bits (Linux fw/api/mac-cfg.h) ───────────
|
||||
|
||||
pub const PROT_FLAG_USE_CTS_PROT: u32 = 1 << 0;
|
||||
pub const PROT_FLAG_USE_SHORT_PREAMBLE: u32 = 1 << 1;
|
||||
pub const PROT_FLAG_USE_SHORT_SLOT: u32 = 1 << 2;
|
||||
|
||||
pub const QOS_FLAG_EDCA: u32 = 1 << 0;
|
||||
pub const QOS_FLAG_UAPSD: u32 = 1 << 1;
|
||||
|
||||
// ── Action codes (Linux fw/api/mac-cfg.h FW_CTXT_ACTION_*) ────────
|
||||
|
||||
pub const FW_CTXT_ACTION_ADD_HW: u32 = 0x03;
|
||||
pub const FW_CTXT_ACTION_MODIFY: u32 = 0x01;
|
||||
pub const FW_CTXT_ACTION_REMOVE: u32 = 0x02;
|
||||
|
||||
// ── Default rate tables (OFDM 6/12/24 Mbps mandatory) ─────────────
|
||||
|
||||
pub const OFDM_RATE_6M: u32 = 1 << 0;
|
||||
pub const OFDM_RATE_12M: u32 = 1 << 2;
|
||||
pub const OFDM_RATE_24M: u32 = 1 << 4;
|
||||
pub const OFDM_MANDATORY_RATES: u32 = OFDM_RATE_6M | OFDM_RATE_12M | OFDM_RATE_24M;
|
||||
|
||||
pub const CCK_RATE_1M: u32 = 1 << 0;
|
||||
pub const CCK_RATE_2M: u32 = 1 << 1;
|
||||
pub const CCK_RATE_5M: u32 = 1 << 2;
|
||||
pub const CCK_RATE_11M: u32 = 1 << 3;
|
||||
pub const CCK_MANDATORY_RATES: u32 = CCK_RATE_1M | CCK_RATE_2M | CCK_RATE_5M | CCK_RATE_11M;
|
||||
|
||||
// ── Link state machine (Linux mld/link.c iwl_mld_change_link_state) ─
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LinkState {
|
||||
Inactive,
|
||||
Adding,
|
||||
Active,
|
||||
Removing,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl Default for LinkState {
|
||||
fn default() -> Self {
|
||||
Self::Inactive
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transition(state: LinkState, target: LinkState) -> Result<LinkState, MldError> {
|
||||
use LinkState::*;
|
||||
match (state, target) {
|
||||
(Inactive, Adding) => Ok(Adding),
|
||||
(Adding, Active) => Ok(Active),
|
||||
(Active, Removing) => Ok(Removing),
|
||||
(Removing, Inactive) => Ok(Inactive),
|
||||
(Adding, Failed) | (Removing, Failed) => Ok(Failed),
|
||||
(Failed, Inactive) => Ok(Inactive),
|
||||
_ => Err(MldError::InvalidState),
|
||||
}
|
||||
}
|
||||
|
||||
// ── MldState helpers for link management ──────────────────────────
|
||||
|
||||
impl MldState {
|
||||
pub fn link_activate(&mut self, link: &MldLink) -> Result<(), MldError> {
|
||||
if !self.fw_running {
|
||||
return Err(MldError::FwNotRunning);
|
||||
}
|
||||
let cmd = link.build_link_config_cmd(FW_CTXT_ACTION_ADD_HW, 0);
|
||||
let bytes = unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
&cmd as *const LinkConfigCmd as *const u8,
|
||||
core::mem::size_of::<LinkConfigCmd>(),
|
||||
)
|
||||
};
|
||||
self.send_hcmd(CMD_LINK_CONFIG, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn link_deactivate(&mut self, link: &MldLink) -> Result<(), MldError> {
|
||||
if !self.fw_running {
|
||||
return Err(MldError::FwNotRunning);
|
||||
}
|
||||
let cmd = link.build_link_config_cmd(FW_CTXT_ACTION_REMOVE, 0);
|
||||
let bytes = unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
&cmd as *const LinkConfigCmd as *const u8,
|
||||
core::mem::size_of::<LinkConfigCmd>(),
|
||||
)
|
||||
};
|
||||
self.send_hcmd(CMD_LINK_CONFIG, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn link_apply_rates(
|
||||
&mut self,
|
||||
link: &MldLink,
|
||||
cck: u32,
|
||||
ofdm: u32,
|
||||
) -> Result<(), MldError> {
|
||||
let mut updated = *link;
|
||||
updated.cck_rates = cck;
|
||||
updated.ofdm_rates = ofdm;
|
||||
let cmd = updated.build_link_config_cmd(
|
||||
FW_CTXT_ACTION_MODIFY,
|
||||
LINK_CTX_MODIFY_RATES_INFO,
|
||||
);
|
||||
let bytes = unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
&cmd as *const LinkConfigCmd as *const u8,
|
||||
core::mem::size_of::<LinkConfigCmd>(),
|
||||
)
|
||||
};
|
||||
self.send_hcmd(CMD_LINK_CONFIG, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn link_new_initial_state() {
|
||||
let l = MldLink::new(2, 0x100, 0x200);
|
||||
assert_eq!(l.link_id, 2);
|
||||
assert_eq!(l.fw_id, 0xFF);
|
||||
assert!(!l.active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn link_config_cmd_add_action() {
|
||||
let mut l = MldLink::new(1, 0x10, 0x20);
|
||||
l.active = true;
|
||||
l.ofdm_rates = OFDM_MANDATORY_RATES;
|
||||
let cmd = l.build_link_config_cmd(FW_CTXT_ACTION_ADD_HW, 0);
|
||||
// Copy packed fields to locals before comparison (packed field refs are unaligned).
|
||||
let (action, link_id, mac_id) = (cmd.action, cmd.link_id, cmd.mac_id);
|
||||
let (active, ofdm_rates) = (cmd.active, cmd.ofdm_rates);
|
||||
assert_eq!(action, FW_CTXT_ACTION_ADD_HW);
|
||||
assert_eq!(link_id, 1);
|
||||
assert_eq!(mac_id, 0x10);
|
||||
assert_eq!(active, 1);
|
||||
assert_eq!(ofdm_rates, OFDM_MANDATORY_RATES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn link_state_machine_legal_transitions() {
|
||||
let s = transition(LinkState::Inactive, LinkState::Adding).unwrap();
|
||||
assert_eq!(s, LinkState::Adding);
|
||||
let s = transition(LinkState::Adding, LinkState::Active).unwrap();
|
||||
assert_eq!(s, LinkState::Active);
|
||||
let s = transition(LinkState::Active, LinkState::Removing).unwrap();
|
||||
assert_eq!(s, LinkState::Removing);
|
||||
let s = transition(LinkState::Removing, LinkState::Inactive).unwrap();
|
||||
assert_eq!(s, LinkState::Inactive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn link_state_machine_illegal_transition() {
|
||||
let res = transition(LinkState::Inactive, LinkState::Active);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mandatory_rate_tables_nonzero() {
|
||||
assert_eq!(OFDM_MANDATORY_RATES & OFDM_RATE_6M, OFDM_RATE_6M);
|
||||
assert_eq!(CCK_MANDATORY_RATES & CCK_RATE_11M, CCK_RATE_11M);
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -18,6 +18,13 @@
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod sta;
|
||||
pub mod scan;
|
||||
pub mod link;
|
||||
pub mod tx;
|
||||
pub mod key;
|
||||
pub mod agg;
|
||||
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
// ── Firmware command group IDs (Linux fw/api/commands.h) ──────────
|
||||
@@ -166,6 +173,8 @@ pub struct Txq {
|
||||
pub fw_id: u16,
|
||||
pub allocated: bool,
|
||||
pub stop_full: bool,
|
||||
pub frame_count: u32,
|
||||
pub byte_count: u64,
|
||||
}
|
||||
|
||||
// ── Scan state (Linux mld/scan.h struct iwl_mld_scan) ─────────────
|
||||
@@ -430,7 +439,7 @@ impl MldState {
|
||||
|
||||
pub fn callback_bss_info_changed(
|
||||
&mut self,
|
||||
mac_id: u8,
|
||||
_mac_id: u8,
|
||||
changed: u64,
|
||||
) -> Result<(), MldError> {
|
||||
if changed & 0x01 != 0 {
|
||||
@@ -564,6 +573,9 @@ pub enum MldError {
|
||||
ScanAlreadyRunning,
|
||||
NotInitialized,
|
||||
InvalidArgument,
|
||||
FwNotRunning,
|
||||
InvalidState,
|
||||
InvalidId,
|
||||
}
|
||||
|
||||
// ── Firmware command structures (Linux fw/api/mac-cfg.h) ──────────
|
||||
@@ -0,0 +1,145 @@
|
||||
//! MLD scan subsystem — Rust port of Linux mld/scan.c
|
||||
//!
|
||||
//! Reference: Linux 7.1 drivers/net/wireless/intel/iwlwifi/mld/scan.c (~2.3K LOC)
|
||||
//! The full Linux scan.c builds UMAC scan request structures (v17/v18) with
|
||||
//! channel lists, dwell times, SSID arrays, probe requests, and 6GHz config.
|
||||
//! This bounded port provides the scan state machine + channel planning +
|
||||
//! request builder that the transport sends as SCAN_REQUEST_UMAC.
|
||||
|
||||
use super::*;
|
||||
|
||||
pub const MAX_UMAC_SCANS: usize = 8;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TrafficLoad {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ScanChannel {
|
||||
pub channel: u16,
|
||||
pub band: u8,
|
||||
pub active: bool,
|
||||
pub dwell: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScanRequest {
|
||||
pub ssid: Option<String>,
|
||||
pub channels_2ghz: Vec<u16>,
|
||||
pub channels_5ghz: Vec<u16>,
|
||||
pub channels_6ghz: Vec<u16>,
|
||||
pub active_dwell: u32,
|
||||
pub passive_dwell: u32,
|
||||
pub max_out_time: u32,
|
||||
pub suspend_time: u32,
|
||||
pub flags: u8,
|
||||
pub n_ssids: u32,
|
||||
}
|
||||
|
||||
impl Default for ScanRequest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ssid: None,
|
||||
channels_2ghz: Vec::new(),
|
||||
channels_5ghz: Vec::new(),
|
||||
channels_6ghz: Vec::new(),
|
||||
active_dwell: 100,
|
||||
passive_dwell: 200,
|
||||
max_out_time: 0,
|
||||
suspend_time: 0,
|
||||
flags: SCAN_FLAG_ACTIVE,
|
||||
n_ssids: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScanRequest {
|
||||
pub fn default_channels() -> (Vec<u16>, Vec<u16>, Vec<u16>) {
|
||||
let ch2: Vec<u16> = (1..=11).map(|c| 2412 + (c - 1) * 5).collect();
|
||||
let ch5: Vec<u16> = vec![
|
||||
5180, 5200, 5220, 5240,
|
||||
5260, 5280, 5300, 5320,
|
||||
5500, 5520, 5540, 5560, 5580, 5600, 5620, 5640, 5660, 5680, 5700,
|
||||
5745, 5765, 5785, 5805, 5825,
|
||||
];
|
||||
let ch6: Vec<u16> = (0..93).map(|i| 5955 + i * 5).collect();
|
||||
(ch2, ch5, ch6)
|
||||
}
|
||||
|
||||
pub fn open_scan() -> Self {
|
||||
let (ch2, ch5, ch6) = Self::default_channels();
|
||||
Self {
|
||||
ssid: None,
|
||||
channels_2ghz: ch2,
|
||||
channels_5ghz: ch5,
|
||||
channels_6ghz: ch6,
|
||||
active_dwell: 100,
|
||||
passive_dwell: 200,
|
||||
flags: SCAN_FLAG_ACTIVE,
|
||||
n_ssids: 0,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn directed_scan(ssid: &str) -> Self {
|
||||
let mut req = Self::open_scan();
|
||||
req.ssid = Some(ssid.to_string());
|
||||
req.n_ssids = 1;
|
||||
req
|
||||
}
|
||||
|
||||
pub fn total_channels(&self) -> usize {
|
||||
self.channels_2ghz.len() + self.channels_5ghz.len() + self.channels_6ghz.len()
|
||||
}
|
||||
|
||||
pub fn estimated_duration_ms(&self) -> u32 {
|
||||
let n_active = self.channels_2ghz.len() + self.channels_5ghz.len();
|
||||
let n_passive = self.channels_6ghz.len();
|
||||
(n_active as u32 * self.active_dwell) + (n_passive as u32 * self.passive_dwell)
|
||||
}
|
||||
}
|
||||
|
||||
impl MldState {
|
||||
pub fn scan_build_request(&self, ssid: Option<&str>) -> ScanRequest {
|
||||
match ssid {
|
||||
Some(s) if !s.is_empty() => ScanRequest::directed_scan(s),
|
||||
_ => ScanRequest::open_scan(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scan_traffic_load(&self) -> TrafficLoad {
|
||||
TrafficLoad::Low
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn open_scan_covers_all_bands() {
|
||||
let req = ScanRequest::open_scan();
|
||||
assert_eq!(req.channels_2ghz.len(), 11);
|
||||
assert!(req.channels_5ghz.len() >= 20);
|
||||
assert!(req.channels_6ghz.len() >= 50);
|
||||
assert!(req.total_channels() >= 81);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directed_scan_sets_ssid() {
|
||||
let req = ScanRequest::directed_scan("TestSSID");
|
||||
assert_eq!(req.ssid.as_deref(), Some("TestSSID"));
|
||||
assert_eq!(req.n_ssids, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_duration_estimate() {
|
||||
let req = ScanRequest::open_scan();
|
||||
let dur = req.estimated_duration_ms();
|
||||
assert!(dur > 0);
|
||||
assert!(dur < 60_000);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
//! fw/api/sta.h, fw/api/datapath.h.
|
||||
|
||||
use super::*;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
// ── Station types (Linux mld/sta.h) ───────────────────────────────
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
//! MLD TX path — Rust port of Linux mld/tx.c
|
||||
//!
|
||||
//! Reference: Linux 7.1 drivers/net/wireless/intel/iwlwifi/mld/tx.c (~1.4K LOC)
|
||||
//! The full Linux tx.c manages TVQM (Transmit Queue Manager) queue allocation,
|
||||
//! TX command construction (TX_CMD), TX status response handling, AMPDU
|
||||
//! aggregation, and rate control via TLC (Transmission Link Control).
|
||||
//! This bounded port provides TX queue classification, TX command builder,
|
||||
//! and queue state helpers that complement the existing MldState methods.
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Access Category mapping (Linux net/mac80211.h ieee80211_ac_numbers) ─
|
||||
|
||||
pub const AC_BK: u8 = 0;
|
||||
pub const AC_BE: u8 = 1;
|
||||
pub const AC_VI: u8 = 2;
|
||||
pub const AC_VO: u8 = 3;
|
||||
|
||||
pub const NUM_ACS: usize = 4;
|
||||
|
||||
// ── TX command flags (Linux fw/api/tx.h TX_CMD_FLG_*) ─────────────
|
||||
|
||||
pub const TX_CMD_FLG_ACK: u32 = 1 << 0;
|
||||
pub const TX_CMD_FLG_STA_RATE: u32 = 1 << 2;
|
||||
pub const TX_CMD_FLG_BAR: u32 = 1 << 4;
|
||||
pub const TX_CMD_FLG_TXOP_PROT: u32 = 1 << 6;
|
||||
pub const TX_CMD_FLG_VHT_NDPA: u32 = 1 << 7;
|
||||
pub const TX_CMD_FLG_HT_NDPA: u32 = 1 << 8;
|
||||
|
||||
// ── TX status codes (Linux fw/api/tx.h TX_STATUS_*) ───────────────
|
||||
|
||||
pub const TX_STATUS_SUCCESS: u8 = 0x00;
|
||||
pub const TX_STATUS_DIRECT_DONE: u8 = 0x01;
|
||||
pub const TX_STATUS_FAILURE_SHORT_LIMIT: u8 = 0x02;
|
||||
pub const TX_STATUS_FAILURE_LONG_LIMIT: u8 = 0x03;
|
||||
pub const TX_STATUS_FAIL_FIFO_PARSE_ERR: u8 = 0x04;
|
||||
pub const TX_STATUS_FAIL_SEC_DONT_SAVE: u8 = 0x05;
|
||||
pub const TX_STATUS_FAILURE_NOT_CONNECTED: u8 = 0x06;
|
||||
pub const TX_STATUS_FAILURE_LIFE_EXPIRE: u8 = 0x07;
|
||||
|
||||
// ── TX queue descriptor (Linux mld/tx.h struct iwl_mld_txq) ───────
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct TxQueueState {
|
||||
pub fw_id: u16,
|
||||
pub mac_id: u32,
|
||||
pub sta_id: u32,
|
||||
pub tid: u8,
|
||||
pub ac: u8,
|
||||
pub allocated: bool,
|
||||
pub stopped: bool,
|
||||
pub ampdu: bool,
|
||||
pub frame_count: u32,
|
||||
pub byte_count: u64,
|
||||
}
|
||||
|
||||
impl TxQueueState {
|
||||
pub fn new(fw_id: u16, mac_id: u32, sta_id: u32, tid: u8, ac: u8) -> Self {
|
||||
Self {
|
||||
fw_id,
|
||||
mac_id,
|
||||
sta_id,
|
||||
tid,
|
||||
ac,
|
||||
allocated: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── TX command structure (Linux fw/api/tx.h struct iwl_tx_cmd) ────
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct TxCmd {
|
||||
pub len: u16,
|
||||
pub off: u16,
|
||||
pub flags: u32,
|
||||
pub rate_n_flags: u32,
|
||||
pub sta_id: u32,
|
||||
pub sec_ctl: u32,
|
||||
pub initial_rate_index: u32,
|
||||
pub driver_txop: u16,
|
||||
pub next_frame: u16,
|
||||
pub life_time: u32,
|
||||
}
|
||||
|
||||
// ── TID → AC mapping (Linux net/wireless.h ieee802_1d_to_ac) ──────
|
||||
|
||||
pub fn tid_to_ac(tid: u8) -> u8 {
|
||||
match tid {
|
||||
0 => AC_BE,
|
||||
1 => AC_BK,
|
||||
2 => AC_BK,
|
||||
3 => AC_BE,
|
||||
4 => AC_VI,
|
||||
5 => AC_VI,
|
||||
6 => AC_VO,
|
||||
7 => AC_VO,
|
||||
_ => AC_BE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ac_for_priority(user_priority: u8) -> u8 {
|
||||
tid_to_ac(user_priority & 0x07)
|
||||
}
|
||||
|
||||
// ── TX command builder ────────────────────────────────────────────
|
||||
|
||||
pub fn build_tx_cmd(sta_id: u32, tid: u8, frame_len: u16) -> TxCmd {
|
||||
TxCmd {
|
||||
len: frame_len,
|
||||
off: 0,
|
||||
flags: TX_CMD_FLG_ACK,
|
||||
rate_n_flags: 0,
|
||||
sta_id,
|
||||
sec_ctl: 0,
|
||||
initial_rate_index: 0,
|
||||
driver_txop: 0,
|
||||
next_frame: 0,
|
||||
life_time: 0x00FF_FFFF,
|
||||
}
|
||||
}
|
||||
|
||||
// ── TX status decode ──────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TxOutcome {
|
||||
Success,
|
||||
RetryNeeded,
|
||||
Dropped,
|
||||
}
|
||||
|
||||
pub fn decode_tx_status(status: u8) -> TxOutcome {
|
||||
match status {
|
||||
TX_STATUS_SUCCESS => TxOutcome::Success,
|
||||
TX_STATUS_DIRECT_DONE => TxOutcome::Success,
|
||||
TX_STATUS_FAILURE_SHORT_LIMIT
|
||||
| TX_STATUS_FAILURE_LONG_LIMIT
|
||||
| TX_STATUS_FAILURE_LIFE_EXPIRE => TxOutcome::Dropped,
|
||||
_ => TxOutcome::RetryNeeded,
|
||||
}
|
||||
}
|
||||
|
||||
// ── MldState helpers ──────────────────────────────────────────────
|
||||
|
||||
impl MldState {
|
||||
pub fn tx_enqueue(&mut self, fw_id: u16, bytes: u64) -> Result<(), MldError> {
|
||||
let txq = self
|
||||
.txqs
|
||||
.get_mut(fw_id as usize)
|
||||
.ok_or(MldError::InvalidId)?;
|
||||
if !txq.allocated {
|
||||
return Err(MldError::InvalidState);
|
||||
}
|
||||
txq.frame_count = txq.frame_count.saturating_add(1);
|
||||
txq.byte_count = txq.byte_count.saturating_add(bytes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn tx_complete(&mut self, fw_id: u16, status: u8) -> Result<TxOutcome, MldError> {
|
||||
let txq = self
|
||||
.txqs
|
||||
.get_mut(fw_id as usize)
|
||||
.ok_or(MldError::InvalidId)?;
|
||||
if !txq.allocated {
|
||||
return Err(MldError::InvalidState);
|
||||
}
|
||||
let outcome = decode_tx_status(status);
|
||||
txq.frame_count = txq.frame_count.saturating_sub(1);
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
pub fn tx_stop_queue(&mut self, fw_id: u16) -> Result<(), MldError> {
|
||||
let txq = self
|
||||
.txqs
|
||||
.get_mut(fw_id as usize)
|
||||
.ok_or(MldError::InvalidId)?;
|
||||
txq.stop_full = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tid_to_ac_standard_mapping() {
|
||||
assert_eq!(tid_to_ac(0), AC_BE); // Best effort
|
||||
assert_eq!(tid_to_ac(1), AC_BK); // Background
|
||||
assert_eq!(tid_to_ac(2), AC_BK);
|
||||
assert_eq!(tid_to_ac(3), AC_BE);
|
||||
assert_eq!(tid_to_ac(4), AC_VI); // Video
|
||||
assert_eq!(tid_to_ac(5), AC_VI);
|
||||
assert_eq!(tid_to_ac(6), AC_VO); // Voice
|
||||
assert_eq!(tid_to_ac(7), AC_VO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tid_to_ac_unknown_falls_back_to_be() {
|
||||
assert_eq!(tid_to_ac(255), AC_BE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tx_cmd_default_flags_include_ack() {
|
||||
let cmd = build_tx_cmd(2, 5, 1400);
|
||||
let (flags, sta_id, len) = (cmd.flags, cmd.sta_id, cmd.len);
|
||||
assert_eq!(flags & TX_CMD_FLG_ACK, TX_CMD_FLG_ACK);
|
||||
assert_eq!(sta_id, 2);
|
||||
assert_eq!(len, 1400);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tx_status_decode_outcomes() {
|
||||
assert_eq!(decode_tx_status(TX_STATUS_SUCCESS), TxOutcome::Success);
|
||||
assert_eq!(
|
||||
decode_tx_status(TX_STATUS_FAILURE_LONG_LIMIT),
|
||||
TxOutcome::Dropped
|
||||
);
|
||||
assert_eq!(decode_tx_status(0x42), TxOutcome::RetryNeeded);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user