#[cfg(any(feature = "async-print", feature = "auto"))]
use embassy_time::with_timeout;
use embassy_futures::join::{join, join3};
use embassy_futures::select::{Either, select};
use embassy_time::{Duration, Timer};
use enumset::EnumSet;
#[cfg(feature = "esp32c5")]
use esp_radio::wifi::BandMode;
use esp_radio::wifi::sta::StationConfig;
use esp_radio::wifi::{Interfaces, Protocol, Protocols, SecondaryChannel, WifiController};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use portable_atomic::Ordering;
use crate::collector::ap::{ap_init, run_ap};
use crate::collector::sta::{run_sta_connect, sta_init};
use crate::config::CsiConfig as CsiConfiguration;
use crate::central::esp_now::run_esp_now_central;
use crate::central::esp_now_fast::run_esp_now_fast_collector;
use crate::central_peripheral::{CentralOpMode, PeripheralOpMode};
use crate::emitter::{EmitterConfig, run_emitter};
use crate::peripheral::esp_now::run_esp_now_peripheral;
use crate::peripheral::esp_now_fast::run_esp_now_fast_source;
use crate::profile::{RadioProfile, StandardProfile};
use crate::csi::delivery::{
CSINodeClient, CSI_OUTPUT_ENABLED, build_csi_config, run_process_csi_packet, set_csi,
};
use crate::log_ln;
use crate::radio::{apply_ht40_channel, suppress_espnow_rx};
#[cfg(feature = "esp32c5")]
use crate::radio::{apply_band_auto, apply_band_for_channel};
use crate::stats::set_seq_drop_detection;
pub(crate) static STOP_SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
#[cfg(feature = "esp32c5")]
const C5_RADIO_SETTLE_MS: u64 = 60;
async fn c5_radio_settle() {
#[cfg(feature = "esp32c5")]
Timer::after(Duration::from_millis(C5_RADIO_SETTLE_MS)).await;
}
async fn csi_data_collection(client: &mut CSINodeClient, duration: u64) {
#[cfg(any(feature = "async-print", feature = "auto"))]
if crate::logging::logging::is_async_logging_active() {
with_timeout(Duration::from_secs(duration), async {
loop {
client.print_csi_w_metadata().await;
}
})
.await
.unwrap_err();
client.send_stop().await;
return;
}
#[cfg(not(any(feature = "async-print", feature = "auto")))]
{
let _ = client;
}
Timer::after(Duration::from_secs(duration)).await;
client.send_stop().await;
}
async fn wait_for_stop() {
STOP_SIGNAL.wait().await;
STOP_SIGNAL.signal(());
}
async fn stop_after_duration(duration: u64) {
match select(
STOP_SIGNAL.wait(),
Timer::after(Duration::from_secs(duration)),
)
.await
{
Either::First(_) | Either::Second(_) => STOP_SIGNAL.signal(()),
}
}
#[derive(Debug, Clone)]
pub struct WifiSnifferConfig {
#[allow(dead_code)]
mac_filter: Option<[u8; 6]>,
channel: u8,
}
impl Default for WifiSnifferConfig {
fn default() -> Self {
Self {
mac_filter: None,
channel: 1,
}
}
}
impl WifiSnifferConfig {
pub fn with_channel(mut self, channel: u8) -> Self {
self.channel = channel;
self
}
pub fn channel(&self) -> u8 {
self.channel
}
}
#[derive(Debug, Clone)]
pub struct WifiStationConfig {
pub client_config: StationConfig,
pub channel_hint: Option<u8>,
}
impl WifiStationConfig {
pub fn new(client_config: StationConfig) -> Self {
Self {
client_config,
channel_hint: None,
}
}
pub fn with_channel_hint(mut self, channel: u8) -> Self {
self.channel_hint = Some(channel);
self
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for WifiStationConfig {
fn format(&self, fmt: defmt::Formatter<'_>) {
defmt::write!(fmt, "WifiStationConfig {{ client_config: <opaque> }}");
}
}
pub struct WifiApConfig {
pub ap_config: esp_radio::wifi::ap::AccessPointConfig,
pub channel: u8,
pub secondary_channel: Option<SecondaryChannel>,
pub ap_ipv4: core::net::Ipv4Addr,
pub lease_ipv4: core::net::Ipv4Addr,
pub lease_count: u8,
pub serve_dhcp: bool,
pub sync_burst: bool,
}
impl WifiApConfig {
pub fn new(
ap_config: esp_radio::wifi::ap::AccessPointConfig,
channel: u8,
secondary: Option<SecondaryChannel>,
) -> Self {
Self {
ap_config,
channel,
secondary_channel: secondary,
ap_ipv4: core::net::Ipv4Addr::new(192, 168, 13, 1),
lease_ipv4: core::net::Ipv4Addr::new(192, 168, 13, 2),
lease_count: 1,
serve_dhcp: true,
sync_burst: false,
}
}
pub fn with_ipv4(mut self, ap: core::net::Ipv4Addr, lease: core::net::Ipv4Addr) -> Self {
self.ap_ipv4 = ap;
self.lease_ipv4 = lease;
self
}
pub fn with_lease_pool(mut self, count: u8) -> Self {
self.lease_count = count.max(1);
self
}
pub fn lease_ip_at(&self, index: u8) -> core::net::Ipv4Addr {
let idx = index.min(self.lease_count.saturating_sub(1));
let mut oct = self.lease_ipv4.octets();
oct[3] = oct[3].saturating_add(idx);
core::net::Ipv4Addr::from(oct)
}
pub fn lease_pool(&self) -> heapless::Vec<core::net::Ipv4Addr, 8> {
let mut v = heapless::Vec::new();
for i in 0..self.lease_count.min(8) {
let _ = v.push(self.lease_ip_at(i));
}
v
}
pub fn with_dhcp_server(mut self, enabled: bool) -> Self {
self.serve_dhcp = enabled;
self
}
pub fn with_sync_burst(mut self, enabled: bool) -> Self {
self.sync_burst = enabled;
self
}
pub fn channel(&self) -> u8 {
self.channel
}
pub fn secondary_channel(&self) -> Option<SecondaryChannel> {
self.secondary_channel
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for WifiApConfig {
fn format(&self, fmt: defmt::Formatter<'_>) {
defmt::write!(fmt, "WifiApConfig {{ ap_config: <opaque> }}");
}
}
pub enum CollectorMode {
Sniffer(WifiSnifferConfig),
Station(WifiStationConfig),
AccessPoint(WifiApConfig),
}
pub enum NodeRole {
Emitter(EmitterConfig),
Collector(CollectorMode),
Central(CentralOpMode),
Peripheral(PeripheralOpMode),
}
const UNSET_MAC: [u8; 6] = [0; 6];
pub use NodeRole as Node;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IOTaskConfig {
pub tx_enabled: bool,
pub rx_enabled: bool,
}
impl IOTaskConfig {
pub const fn new(tx_enabled: bool, rx_enabled: bool) -> Self {
Self {
tx_enabled,
rx_enabled,
}
}
}
impl Default for IOTaskConfig {
fn default() -> Self {
Self::new(true, true)
}
}
pub struct NodeHardware<'a> {
interfaces: &'a mut Interfaces<'static>,
controller: &'a mut WifiController<'static>,
}
impl<'a> NodeHardware<'a> {
pub fn new(
interfaces: &'a mut Interfaces<'static>,
controller: &'a mut WifiController<'static>,
) -> Self {
Self {
interfaces,
controller,
}
}
}
pub(crate) fn reset_globals() {
crate::csi::delivery::reset();
}
pub struct CSINode<'a> {
role: NodeRole,
csi_output_enabled: bool,
io_tasks: IOTaskConfig,
csi_config: Option<CsiConfiguration>,
traffic_freq_hz: Option<u16>,
hardware: NodeHardware<'a>,
protocol: Option<Protocol>,
flood_unsolicited_reply: bool,
profile: &'static dyn RadioProfile,
collection_mode: crate::CollectionMode,
esp_now_rate: Option<esp_radio::esp_now::WifiPhyRate>,
}
impl<'a> CSINode<'a> {
pub fn new(
role: NodeRole,
csi_config: Option<CsiConfiguration>,
traffic_freq_hz: Option<u16>,
hardware: NodeHardware<'a>,
) -> Self {
Self {
role,
csi_output_enabled: true,
io_tasks: IOTaskConfig::default(),
csi_config,
traffic_freq_hz,
hardware,
collection_mode: crate::CollectionMode::Collector,
esp_now_rate: None,
protocol: None,
flood_unsolicited_reply: false,
profile: &StandardProfile,
}
}
pub fn new_collector(
mode: CollectorMode,
csi_config: Option<CsiConfiguration>,
traffic_freq_hz: Option<u16>,
hardware: NodeHardware<'a>,
) -> Self {
Self::new(
NodeRole::Collector(mode),
csi_config,
traffic_freq_hz,
hardware,
)
}
pub fn new_emitter(config: EmitterConfig, hardware: NodeHardware<'a>) -> Self {
Self::new(NodeRole::Emitter(config), None, None, hardware)
}
pub fn get_role(&self) -> &NodeRole {
&self.role
}
pub fn csi_output_enabled(&self) -> bool {
self.csi_output_enabled
}
pub fn get_collector_mode(&self) -> Option<&CollectorMode> {
match &self.role {
NodeRole::Collector(mode) => Some(mode),
_ => None,
}
}
pub fn get_emitter_config(&self) -> Option<&EmitterConfig> {
match &self.role {
NodeRole::Emitter(config) => Some(config),
_ => None,
}
}
pub fn get_central_mode(&self) -> Option<&CentralOpMode> {
match &self.role {
NodeRole::Central(mode) => Some(mode),
_ => None,
}
}
pub fn get_peripheral_mode(&self) -> Option<&PeripheralOpMode> {
match &self.role {
NodeRole::Peripheral(mode) => Some(mode),
_ => None,
}
}
pub fn set_csi_config(&mut self, config: CsiConfiguration) {
self.csi_config = Some(config);
}
pub fn set_station_config(&mut self, config: WifiStationConfig) {
if let NodeRole::Collector(CollectorMode::Station(_)) = &mut self.role {
self.role = NodeRole::Collector(CollectorMode::Station(config));
}
}
pub fn set_traffic_frequency(&mut self, freq_hz: u16) {
self.traffic_freq_hz = Some(freq_hz);
}
pub fn set_csi_output_enabled(&mut self, enabled: bool) {
self.csi_output_enabled = enabled;
}
pub fn set_collection_mode(&mut self, mode: crate::CollectionMode) {
self.collection_mode = mode;
}
pub fn collection_mode(&self) -> crate::CollectionMode {
self.collection_mode
}
pub fn set_rate(&mut self, rate: esp_radio::esp_now::WifiPhyRate) {
self.esp_now_rate = Some(rate);
}
pub fn set_io_tasks(&mut self, io_tasks: IOTaskConfig) {
self.io_tasks = io_tasks;
}
pub fn set_tx_enabled(&mut self, enabled: bool) {
self.io_tasks.tx_enabled = enabled;
}
pub fn set_rx_enabled(&mut self, enabled: bool) {
self.io_tasks.rx_enabled = enabled;
}
pub fn get_io_tasks(&self) -> IOTaskConfig {
self.io_tasks
}
pub fn set_role(&mut self, role: NodeRole) {
self.role = role;
}
pub fn set_protocol(&mut self, protocol: Protocol) {
self.protocol = Some(protocol);
}
pub fn set_radio_profile(&mut self, profile: &'static dyn RadioProfile) {
self.profile = profile;
}
pub fn set_flood_unsolicited_reply(&mut self, enabled: bool) {
self.flood_unsolicited_reply = enabled;
}
pub async fn run_duration(&mut self, duration: u64, client: &mut CSINodeClient) {
self.run_inner(Some(duration), Some(client)).await;
}
async fn run_inner(&mut self, duration: Option<u64>, client: Option<&mut CSINodeClient>) {
#[cfg(feature = "statistics")]
crate::stats::stats_begin_run();
let interfaces = &mut self.hardware.interfaces;
let controller = &mut self.hardware.controller;
crate::collector::sta::set_icmp_flood_unsolicited(self.flood_unsolicited_reply);
crate::set_runtime_collection_mode(
self.collection_mode == crate::CollectionMode::Collector,
);
if matches!(&self.role, NodeRole::Central(_) | NodeRole::Peripheral(_)) {
crate::esp_now_pool::install();
} else {
suppress_espnow_rx();
}
c5_radio_settle().await;
let is_ap = matches!(
&self.role,
NodeRole::Collector(CollectorMode::AccessPoint(_))
);
let is_sniffer = matches!(&self.role, NodeRole::Collector(CollectorMode::Sniffer(_)));
let is_emitter = matches!(&self.role, NodeRole::Emitter(_));
let rx_enabled = self.io_tasks.rx_enabled && !is_emitter;
let profile = self.profile;
let bringup = profile.wants_bringup(&self.role, self.protocol);
if let Some(protocol) = self.protocol.take() {
if !is_emitter {
let base = Protocols::default().with_2_4(protocol_ladder_2_4(protocol));
let protocols = profile.tune_protocols(&self.role, protocol, base);
controller.set_protocols(protocols).unwrap();
c5_radio_settle().await;
}
self.protocol = Some(protocol);
}
if bringup && !is_emitter {
profile.apply_bandwidth(controller);
c5_radio_settle().await;
}
let sta_interface =
if let NodeRole::Collector(CollectorMode::Station(config)) = &self.role {
let ifaces = sta_init(
&mut interfaces.station,
config,
controller,
profile,
bringup,
);
#[cfg(feature = "esp32c5")]
{
match config.channel_hint {
Some(channel) => apply_band_for_channel(controller, channel),
None => apply_band_auto(controller),
}
c5_radio_settle().await;
}
Some(ifaces)
} else {
None
};
if bringup && sta_interface.is_some() {
profile.apply_protocols_post(controller);
c5_radio_settle().await;
}
let ap_interface = if let NodeRole::Collector(CollectorMode::AccessPoint(config)) =
&self.role
{
#[cfg(feature = "esp32c5")]
if config.secondary_channel().is_none() {
apply_band_for_channel(controller, config.channel());
}
if let Some(secondary) = config.secondary_channel() {
apply_ht40_channel(controller, config.channel(), secondary);
c5_radio_settle().await;
}
let ifaces = ap_init(
&mut interfaces.access_point,
config,
controller,
profile,
bringup,
);
if bringup {
profile.apply_protocols_post(controller);
}
c5_radio_settle().await;
Some(ifaces)
} else {
None
};
let mut config = match self.csi_config {
Some(ref config) => {
log_ln!("CSI Configuration Set: {:?}", config);
build_csi_config(config)
}
None => {
let default_config = CsiConfiguration::default();
log_ln!(
"No CSI Configuration Provided. Going with defaults: {:?}",
default_config
);
build_csi_config(&default_config)
}
};
profile.tune_csi_acquisition(&mut config);
log_ln!("Wi-Fi Controller Started");
CSI_OUTPUT_ENABLED.store(self.csi_output_enabled, Ordering::Relaxed);
set_seq_drop_detection(!is_emitter);
let csi_config_for_recovery = config.clone();
if rx_enabled && !is_sniffer && !is_ap {
set_csi(controller, config.clone());
c5_radio_settle().await;
}
let sniffer = &interfaces.sniffer;
match &self.role {
NodeRole::Emitter(emitter_config) => {
let main_task = run_emitter(controller, interfaces, emitter_config);
drive_main(main_task, false, duration, client).await;
}
NodeRole::Collector(mode) => match mode {
CollectorMode::Sniffer(sniffer_config) => {
#[cfg(feature = "esp32c5")]
{
let band = if sniffer_config.channel() >= 36 {
BandMode::_5G
} else {
BandMode::_2_4G
};
controller.set_band_mode(band).unwrap();
}
sniffer.set_promiscuous_mode(true).unwrap();
controller
.set_channel(sniffer_config.channel(), SecondaryChannel::None)
.unwrap();
if bringup {
profile.apply_sniffer_radio(controller);
c5_radio_settle().await;
}
if rx_enabled {
set_csi(controller, config.clone());
}
match (duration, rx_enabled) {
(Some(d), true) => {
join(
run_process_csi_packet(),
csi_data_collection(client.unwrap(), d),
)
.await;
run_process_csi_packet().await;
}
(Some(d), false) => stop_after_duration(d).await,
(None, true) => run_process_csi_packet().await,
(None, false) => wait_for_stop().await,
}
sniffer.set_promiscuous_mode(false).unwrap();
}
CollectorMode::AccessPoint(ap_config) => {
let (ap_stack, ap_runner) = ap_interface.unwrap();
let main_task = run_ap(
controller,
ap_stack,
ap_runner,
ap_config,
csi_config_for_recovery,
self.io_tasks,
self.traffic_freq_hz,
);
drive_main(main_task, rx_enabled, duration, client).await;
sniffer.set_promiscuous_mode(false).unwrap();
}
CollectorMode::Station(_sta_config) => {
let (sta_stack, sta_runner) = sta_interface.unwrap();
let main_task = run_sta_connect(
controller,
self.traffic_freq_hz,
sta_stack,
sta_runner,
csi_config_for_recovery,
self.io_tasks,
);
drive_main(main_task, rx_enabled, duration, client).await;
sniffer.set_promiscuous_mode(false).unwrap();
}
},
NodeRole::Central(mode) => match mode {
CentralOpMode::EspNow(cfg) => {
let main_task = run_esp_now_central(
&mut interfaces.esp_now,
UNSET_MAC,
cfg,
self.traffic_freq_hz,
crate::IS_COLLECTOR.load(Ordering::Relaxed),
self.io_tasks,
);
drive_main(main_task, rx_enabled, duration, client).await;
}
CentralOpMode::EspNowFastCollector(cfg) => {
let main_task = run_esp_now_fast_collector(
&mut interfaces.esp_now,
cfg,
self.io_tasks,
);
drive_main(main_task, rx_enabled, duration, client).await;
}
CentralOpMode::WifiStation(_) | CentralOpMode::WifiAccessPoint(_) => {
log_ln!(
"central Wi-Fi modes are served by NodeRole::Collector — \
build CollectorMode::Station / ::AccessPoint instead"
);
}
},
NodeRole::Peripheral(mode) => match mode {
PeripheralOpMode::EspNow(cfg) => {
let main_task = run_esp_now_peripheral(
&mut interfaces.esp_now,
cfg,
self.traffic_freq_hz,
self.io_tasks,
);
drive_main(main_task, rx_enabled, duration, client).await;
}
PeripheralOpMode::EspNowFastSource(cfg) => {
let main_task = run_esp_now_fast_source(
&mut interfaces.esp_now,
cfg,
self.traffic_freq_hz,
self.io_tasks,
);
drive_main(main_task, false, duration, client).await;
}
PeripheralOpMode::WifiSniffer(_) => {
log_ln!(
"peripheral sniffer is served by NodeRole::Collector — \
build CollectorMode::Sniffer instead"
);
}
},
}
STOP_SIGNAL.reset();
reset_globals();
}
pub async fn run(&mut self) {
self.run_inner(None, None).await;
}
}
async fn drive_main(
main_task: impl core::future::Future,
rx_enabled: bool,
duration: Option<u64>,
client: Option<&mut CSINodeClient>,
) {
match (duration, rx_enabled) {
(Some(d), true) => {
join3(
main_task,
run_process_csi_packet(),
csi_data_collection(client.unwrap(), d),
)
.await;
}
(Some(d), false) => {
join3(main_task, wait_for_stop(), stop_after_duration(d)).await;
}
(None, true) => {
join(main_task, run_process_csi_packet()).await;
}
(None, false) => {
join(main_task, wait_for_stop()).await;
}
}
}
fn protocol_ladder_2_4(protocol: Protocol) -> EnumSet<Protocol> {
match protocol {
Protocol::B => EnumSet::only(Protocol::B),
Protocol::G => Protocol::B | Protocol::G,
Protocol::N => Protocol::B | Protocol::G | Protocol::N,
Protocol::AX => Protocol::B | Protocol::G | Protocol::N | Protocol::AX,
other => EnumSet::only(other),
}
}
#[cfg(test)]
mod protocol_ladder_tests {
use super::*;
#[test]
fn each_rung_carries_the_ones_beneath_it() {
assert_eq!(protocol_ladder_2_4(Protocol::B), EnumSet::only(Protocol::B));
assert_eq!(protocol_ladder_2_4(Protocol::G), Protocol::B | Protocol::G);
assert_eq!(
protocol_ladder_2_4(Protocol::N),
Protocol::B | Protocol::G | Protocol::N
);
assert_eq!(
protocol_ladder_2_4(Protocol::AX),
Protocol::B | Protocol::G | Protocol::N | Protocol::AX
);
}
#[test]
fn n_is_never_advertised_alone() {
let set = protocol_ladder_2_4(Protocol::N);
assert!(set.contains(Protocol::N));
assert!(set.contains(Protocol::G), "11n needs 11g beneath it");
assert!(set.contains(Protocol::B), "11n needs 11b beneath it");
assert_ne!(set, EnumSet::only(Protocol::N));
}
#[test]
fn the_ht_rung_matches_the_emitters_own_set() {
assert_eq!(
protocol_ladder_2_4(Protocol::N),
Protocol::B | Protocol::G | Protocol::N
);
}
#[test]
fn lr_stays_on_its_own() {
assert_eq!(protocol_ladder_2_4(Protocol::LR), EnumSet::only(Protocol::LR));
}
#[test]
fn five_ghz_rungs_are_untouched() {
for p in [Protocol::A, Protocol::AC] {
assert_eq!(protocol_ladder_2_4(p), EnumSet::only(p));
}
}
}