diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/mld/agg.rs b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/agg.rs new file mode 100644 index 0000000000..58337c4503 --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/agg.rs @@ -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 { + 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 { + 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::(), + ) + }; + 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 { + 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::(), + ) + }; + 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::(), + ) + }; + 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); + } +} diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/mld/key.rs b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/key.rs new file mode 100644 index 0000000000..ccc13fb4b3 --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/key.rs @@ -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, + pub rx_mic: Option>, + pub tx_mic: Option>, + 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 { + 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::(), + ) + }; + 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::(), + ) + }; + 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); + } +} diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/mld/link.rs b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/link.rs new file mode 100644 index 0000000000..c98745538a --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/link.rs @@ -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 { + 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::(), + ) + }; + 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::(), + ) + }; + 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::(), + ) + }; + 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); + } +} diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/mld.rs b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/mod.rs similarity index 99% rename from local/recipes/drivers/redbear-iwlwifi/source/src/mld.rs rename to local/recipes/drivers/redbear-iwlwifi/source/src/mld/mod.rs index 8da64c988e..56dcbea86d 100644 --- a/local/recipes/drivers/redbear-iwlwifi/source/src/mld.rs +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/mod.rs @@ -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) ────────── diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/mld/scan.rs b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/scan.rs new file mode 100644 index 0000000000..d700b9d902 --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/scan.rs @@ -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, + pub channels_2ghz: Vec, + pub channels_5ghz: Vec, + pub channels_6ghz: Vec, + 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, Vec, Vec) { + let ch2: Vec = (1..=11).map(|c| 2412 + (c - 1) * 5).collect(); + let ch5: Vec = 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 = (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); + } +} diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/mld/sta.rs b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/sta.rs index 9952b46927..a354eba6df 100644 --- a/local/recipes/drivers/redbear-iwlwifi/source/src/mld/sta.rs +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/sta.rs @@ -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) ─────────────────────────────── diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/mld/tx.rs b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/tx.rs new file mode 100644 index 0000000000..f0ebcf734c --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/mld/tx.rs @@ -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 { + 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); + } +}