#[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, SecondaryChannel, WifiController};
use crate::radio::RadioInterfaces;
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)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum EmitterPhy {
Ht20,
Ht40Above,
Ht40Below,
#[cfg(any(feature = "esp32c5", feature = "esp32c6"))]
He20,
}
#[deprecated(since = "0.12.0", note = "renamed to `EmitterPhy`")]
pub type HtBandwidth = EmitterPhy;
impl EmitterPhy {
pub fn is_forty(self) -> bool {
matches!(self, EmitterPhy::Ht40Above | EmitterPhy::Ht40Below)
}
pub fn is_he(self) -> bool {
#[cfg(any(feature = "esp32c5", feature = "esp32c6"))]
{
matches!(self, EmitterPhy::He20)
}
#[cfg(not(any(feature = "esp32c5", feature = "esp32c6")))]
{
false
}
}
pub fn secondary(self) -> SecondaryChannel {
match self {
EmitterPhy::Ht40Above => SecondaryChannel::Above,
EmitterPhy::Ht40Below => SecondaryChannel::Below,
_ => SecondaryChannel::None,
}
}
fn phymode(self) -> u32 {
match self {
EmitterPhy::Ht20 => phy::WIFI_PHY_MODE_HT20,
EmitterPhy::Ht40Above | EmitterPhy::Ht40Below => phy::WIFI_PHY_MODE_HT40,
#[cfg(any(feature = "esp32c5", feature = "esp32c6"))]
EmitterPhy::He20 => phy::WIFI_PHY_MODE_HE20,
}
}
}
#[derive(Clone, Copy)]
pub struct EmitterConfig {
pub channel: u8,
pub phy: EmitterPhy,
pub dst_mac: [u8; 6],
pub period: Duration,
pub use_sta_if: bool,
}
impl EmitterConfig {
pub fn new(channel: u8, phy: EmitterPhy) -> Self {
Self {
channel,
phy,
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
}
pub const fn session_role() -> Option<crate::SessionRole> {
None
}
pub const fn sensing_role() -> crate::SensingRole {
crate::SensingRole::Transmitter
}
pub const fn get_reporting() -> crate::Reporting {
crate::Reporting::None
}
}
impl Default for EmitterConfig {
fn default() -> Self {
Self::new(1, EmitterPhy::Ht20)
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for EmitterConfig {
fn format(&self, fmt: defmt::Formatter<'_>) {
defmt::write!(
fmt,
"EmitterConfig {{ channel: {}, phy: {}, period_ms: {} }}",
self.channel,
self.phy,
self.period.as_millis()
);
}
}
fn bringup(controller: &mut WifiController<'_>, cfg: &EmitterConfig) {
let forty = cfg.phy.is_forty();
let phymode = cfg.phy.phymode();
let force = |sta: bool| {
if sta {
phy::force_tx_sta_before_start(phymode)
} else {
phy::force_tx_ap(phymode)
}
};
let rc = force(cfg.use_sta_if);
let started = if cfg.use_sta_if {
controller.set_config(&Config::Station(StationConfig::default()))
} else {
controller.set_config(&Config::AccessPoint(AccessPointConfig::default()))
};
if started.is_err() {
log_ln!("emitter: set_config failed");
}
apply_band_for_channel(controller, cfg.channel);
phy::apply_ht_bandwidth(controller, forty);
phy::apply_tx_protocols(controller, cfg.phy.is_he());
if controller
.set_channel(cfg.channel, cfg.phy.secondary())
.is_err()
{
log_ln!("emitter: set_channel failed");
}
let rc_post = force(cfg.use_sta_if);
if rc_post != 0 {
log_ln!(
"Emitter: forced TX PHY rejected (pre-start rc={}, post-start rc={}); frames will not use the requested format",
rc,
rc_post
);
}
}
pub(crate) async fn run_emitter(
controller: &mut WifiController<'static>,
interfaces: &mut RadioInterfaces,
cfg: &EmitterConfig,
) {
bringup(controller, cfg);
#[cfg(not(any(feature = "esp32c5", feature = "esp32c6")))]
espnow::bringup(
controller,
&interfaces.esp_now,
&cfg.dst_mac,
cfg.channel,
cfg.phy.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 {}, {:?}, {} ms period, src {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
cfg.channel,
cfg.phy,
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(_) => {}
}
}
}