#[cfg(not(any(feature = "esp32c5", feature = "esp32c6")))]
pub mod espnow;
pub mod frame;
pub mod phy;
use embassy_futures::select::{Either, select};
use embassy_time::{Duration, Timer};
use esp_radio::wifi::ap::AccessPointConfig;
use esp_radio::wifi::sta::StationConfig;
use esp_radio::wifi::{Config, Interfaces, SecondaryChannel, WifiController};
use crate::radio::apply_band_for_channel;
use crate::{STOP_SIGNAL, log_ln};
use frame::{BROADCAST, PROBE_FRAME_LEN, build_probe_frame};
#[cfg(any(feature = "esp32c5", feature = "esp32c6"))]
use frame::inject_probe_once;
#[cfg(feature = "cpu-test-tx")]
const CPU_TEST_MAX_FRAME_LEN: usize = 1500;
#[cfg(feature = "cpu-test-tx")]
const _: () = assert!(CPU_TEST_MAX_FRAME_LEN >= PROBE_FRAME_LEN);
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum HtBandwidth {
Ht20,
Ht40Above,
Ht40Below,
}
impl HtBandwidth {
pub fn is_forty(self) -> bool {
!matches!(self, HtBandwidth::Ht20)
}
pub fn secondary(self) -> SecondaryChannel {
match self {
HtBandwidth::Ht20 => SecondaryChannel::None,
HtBandwidth::Ht40Above => SecondaryChannel::Above,
HtBandwidth::Ht40Below => SecondaryChannel::Below,
}
}
}
#[derive(Clone, Copy)]
pub struct EmitterConfig {
pub channel: u8,
pub bandwidth: HtBandwidth,
pub dst_mac: [u8; 6],
pub period: Duration,
pub use_sta_if: bool,
}
impl EmitterConfig {
pub fn new(channel: u8, bandwidth: HtBandwidth) -> Self {
Self {
channel,
bandwidth,
dst_mac: BROADCAST,
period: Duration::from_millis(20),
use_sta_if: true,
}
}
pub fn with_dst_mac(mut self, dst_mac: [u8; 6]) -> Self {
self.dst_mac = dst_mac;
self
}
pub fn with_period(mut self, period: Duration) -> Self {
self.period = period;
self
}
pub fn with_ap_interface(mut self) -> Self {
self.use_sta_if = false;
self
}
}
impl Default for EmitterConfig {
fn default() -> Self {
Self::new(1, HtBandwidth::Ht20)
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for EmitterConfig {
fn format(&self, fmt: defmt::Formatter<'_>) {
defmt::write!(
fmt,
"EmitterConfig {{ channel: {}, forty: {}, period_ms: {} }}",
self.channel,
self.bandwidth.is_forty(),
self.period.as_millis()
);
}
}
fn bringup(controller: &mut WifiController<'_>, cfg: &EmitterConfig) {
let forty = cfg.bandwidth.is_forty();
let rc = if cfg.use_sta_if {
let rc = if forty {
phy::force_ht40_tx_sta_before_start()
} else {
phy::force_ht20_tx_sta_before_start()
};
if controller
.set_config(&Config::Station(StationConfig::default()))
.is_err()
{
log_ln!("emitter: set_config(Station) failed");
}
rc
} else {
let rc = if forty {
phy::force_ht40_tx_ap_before_start()
} else {
phy::force_ht20_tx_ap_before_start()
};
if controller
.set_config(&Config::AccessPoint(AccessPointConfig::default()))
.is_err()
{
log_ln!("emitter: set_config(AccessPoint) failed");
}
rc
};
if rc != 0 {
log_ln!(
"Emitter: forced TX PHY rejected (rc={}); frames will not use the requested format",
rc
);
}
apply_band_for_channel(controller, cfg.channel);
phy::apply_ht_bandwidth(controller, forty);
phy::apply_ht_protocols(controller);
if controller
.set_channel(cfg.channel, cfg.bandwidth.secondary())
.is_err()
{
log_ln!("emitter: set_channel failed");
}
let rc_post = if cfg.use_sta_if {
if forty {
phy::force_ht40_tx_sta_before_start()
} else {
phy::force_ht20_tx_sta_before_start()
}
} else if forty {
phy::force_ht40_tx_ap_before_start()
} else {
phy::force_ht20_tx_ap_before_start()
};
log_ln!("Emitter: forced TX PHY rc pre-start={} post-start={}", rc, rc_post);
}
pub async fn run_emitter(
controller: &mut WifiController<'static>,
interfaces: &mut Interfaces<'static>,
cfg: &EmitterConfig,
) {
bringup(controller, cfg);
#[cfg(not(any(feature = "esp32c5", feature = "esp32c6")))]
espnow::bringup(
controller,
&interfaces.esp_now,
&cfg.dst_mac,
cfg.channel,
cfg.bandwidth.is_forty(),
);
let src = if cfg.use_sta_if {
interfaces.station.mac_address()
} else {
interfaces.access_point.mac_address()
};
#[cfg(feature = "cpu-test-tx")]
let mut frame = [0u8; CPU_TEST_MAX_FRAME_LEN];
#[cfg(not(feature = "cpu-test-tx"))]
let mut frame = [0u8; PROBE_FRAME_LEN];
let len = build_probe_frame(&src, &cfg.dst_mac, &mut frame);
if interfaces.sniffer.set_promiscuous_mode(true).is_err() {
log_ln!("emitter: enabling promiscuous mode failed; raw TX may not radiate");
}
log_ln!(
"Emitter running: ch {}, {} MHz, {} ms period, src {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
cfg.channel,
if cfg.bandwidth.is_forty() { 40 } else { 20 },
cfg.period.as_millis(),
src[0],
src[1],
src[2],
src[3],
src[4],
src[5]
);
let mut reported_failure = false;
loop {
#[cfg(feature = "cpu-test-tx")]
let (period, len, paused) = {
use portable_atomic::Ordering;
let rate = crate::TEST_TX_RATE_HZ.load(Ordering::Relaxed).max(1);
let want = crate::TEST_TX_PAYLOAD_B.load(Ordering::Relaxed) as usize;
(
Duration::from_micros(1_000_000 / rate as u64),
len.max(want.min(CPU_TEST_MAX_FRAME_LEN)),
crate::TEST_TX_PAUSED.load(Ordering::Relaxed),
)
};
#[cfg(not(feature = "cpu-test-tx"))]
let (period, len, paused) = (cfg.period, len, false);
if !paused {
#[cfg(not(any(feature = "esp32c5", feature = "esp32c6")))]
let sent = espnow::send_once(&mut interfaces.esp_now, &cfg.dst_mac, &frame[..len]);
#[cfg(any(feature = "esp32c5", feature = "esp32c6"))]
let sent =
inject_probe_once(&mut interfaces.sniffer, cfg.use_sta_if, &frame[..len]).is_ok();
match sent {
true => {
#[cfg(feature = "statistics")]
crate::stats::record_tx();
}
false => {
if !reported_failure {
reported_failure = true;
log_ln!("Emitter: ESP-NOW send rejected by the driver");
}
}
}
}
match select(STOP_SIGNAL.wait(), Timer::after(period)).await {
Either::First(_) => {
log_ln!("STOP signal received, shutting down emitter...");
STOP_SIGNAL.signal(());
return;
}
Either::Second(_) => {}
}
}
}