use embassy_futures::select::{Either3, select3};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use embassy_sync::waitqueue::AtomicWaker;
#[cfg(feature = "statistics")]
use embassy_time::Instant;
use embassy_time::Timer;
use portable_atomic::{AtomicBool, Ordering};
use esp_radio::wifi::WifiController;
use esp_radio::wifi::csi::CsiConfig;
use super::CsiPacket;
use crate::STOP_SIGNAL;
#[cfg(feature = "statistics")]
use crate::stats::STATS;
#[cfg(feature = "statistics")]
use crate::stats::{RESET_SEQ_TRACKER, record_loss, seq_drop_detection_enabled};
static CSI_QUEUE: heapless::mpmc::Q32<CsiPacket> = heapless::mpmc::Q32::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum OverflowPolicy {
DropNewest,
DropOldest,
}
static OVERFLOW_DROP_OLDEST: AtomicBool = AtomicBool::new(false);
pub fn set_csi_queue_overflow(policy: OverflowPolicy) {
OVERFLOW_DROP_OLDEST.store(policy == OverflowPolicy::DropOldest, Ordering::Relaxed);
}
pub fn csi_queue_overflow() -> OverflowPolicy {
if OVERFLOW_DROP_OLDEST.load(Ordering::Relaxed) {
OverflowPolicy::DropOldest
} else {
OverflowPolicy::DropNewest
}
}
static CSI_WAKER: AtomicWaker = AtomicWaker::new();
pub(crate) static REPORTS: AtomicBool = AtomicBool::new(false);
pub(crate) static REPORTING_CHANGED: Signal<CriticalSectionRawMutex, ()> = Signal::new();
pub(crate) static CSI_OUTPUT_ENABLED: AtomicBool = AtomicBool::new(true);
static USER_PUBLISH_INTENT: AtomicBool = AtomicBool::new(false);
static CSI_PUBLISH_ENABLED: AtomicBool = AtomicBool::new(false);
pub(crate) static CSI_OUTPUT_CHANGED: Signal<CriticalSectionRawMutex, ()> = Signal::new();
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum CsiDeliveryMode {
Off = 0,
Callback = 1,
Async = 2,
}
static CSI_DELIVERY_MODE: portable_atomic::AtomicU8 = portable_atomic::AtomicU8::new(0);
static CSI_CALLBACK: core::sync::atomic::AtomicPtr<()> =
core::sync::atomic::AtomicPtr::new(core::ptr::null_mut());
static CSI_RAW_CALLBACK: core::sync::atomic::AtomicPtr<()> =
core::sync::atomic::AtomicPtr::new(core::ptr::null_mut());
static CSI_INLINE_LOG_ENABLED: AtomicBool = AtomicBool::new(false);
pub fn set_csi_logging_enabled(enabled: bool) {
CSI_INLINE_LOG_ENABLED.store(enabled, Ordering::Release);
USER_PUBLISH_INTENT.store(enabled, Ordering::Release);
apply_publish_gate();
}
pub fn csi_logging_enabled() -> bool {
CSI_INLINE_LOG_ENABLED.load(Ordering::Relaxed)
}
pub fn set_csi_delivery_mode(mode: CsiDeliveryMode) {
CSI_DELIVERY_MODE.store(mode as u8, Ordering::Release);
}
pub fn csi_delivery_mode() -> CsiDeliveryMode {
match CSI_DELIVERY_MODE.load(Ordering::Relaxed) {
1 => CsiDeliveryMode::Callback,
2 => CsiDeliveryMode::Async,
_ => CsiDeliveryMode::Off,
}
}
pub fn set_csi_callback(cb: fn(&CsiPacket)) {
CSI_CALLBACK.store(cb as *mut (), core::sync::atomic::Ordering::Release);
CSI_DELIVERY_MODE.store(CsiDeliveryMode::Callback as u8, Ordering::Release);
USER_PUBLISH_INTENT.store(true, Ordering::Release);
apply_publish_gate();
}
pub fn clear_csi_callback() {
CSI_DELIVERY_MODE.store(CsiDeliveryMode::Off as u8, Ordering::Release);
CSI_CALLBACK.store(core::ptr::null_mut(), core::sync::atomic::Ordering::Release);
}
pub fn set_csi_raw_callback(cb: fn()) {
CSI_RAW_CALLBACK.store(cb as *mut (), core::sync::atomic::Ordering::Release);
USER_PUBLISH_INTENT.store(true, Ordering::Release);
apply_publish_gate();
}
pub fn set_csi_output_enabled(enabled: bool) {
CSI_OUTPUT_ENABLED.store(enabled, Ordering::Relaxed);
apply_publish_gate();
CSI_OUTPUT_CHANGED.signal(());
}
pub fn csi_output_enabled() -> bool {
CSI_OUTPUT_ENABLED.load(Ordering::Relaxed)
}
static CSI_PEER_FILTER: portable_atomic::AtomicU64 = portable_atomic::AtomicU64::new(0);
static CSI_MIN_SIG_MODE: portable_atomic::AtomicU8 = portable_atomic::AtomicU8::new(0);
static CSI_FILTER_ACTIVE: AtomicBool = AtomicBool::new(false);
#[inline(always)]
fn pack_mac(mac: [u8; 6]) -> u64 {
let mut v = 0u64;
let mut i = 0;
while i < 6 {
v = (v << 8) | mac[i] as u64;
i += 1;
}
v
}
fn refresh_filter_active() {
let active = CSI_PEER_FILTER.load(Ordering::Relaxed) != 0
|| CSI_MIN_SIG_MODE.load(Ordering::Relaxed) != 0;
CSI_FILTER_ACTIVE.store(active, Ordering::Relaxed);
}
pub fn set_csi_peer_filter(mac: [u8; 6]) {
CSI_PEER_FILTER.store(pack_mac(mac), Ordering::Relaxed);
refresh_filter_active();
}
pub fn csi_peer_filter() -> [u8; 6] {
let v = CSI_PEER_FILTER.load(Ordering::Relaxed);
let mut mac = [0u8; 6];
let mut i = 0;
while i < 6 {
mac[i] = (v >> (8 * (5 - i))) as u8;
i += 1;
}
mac
}
pub fn set_csi_min_sig_mode(min: u8) {
CSI_MIN_SIG_MODE.store(min, Ordering::Relaxed);
refresh_filter_active();
}
pub fn csi_min_sig_mode() -> u8 {
CSI_MIN_SIG_MODE.load(Ordering::Relaxed)
}
#[inline(always)]
fn csi_passes_filter(info: &esp_radio::wifi::csi::WifiCsiInfo<'_>) -> bool {
if !CSI_FILTER_ACTIVE.load(Ordering::Relaxed) {
return true;
}
let want = CSI_PEER_FILTER.load(Ordering::Relaxed);
if want != 0 && pack_mac(*info.mac()) != want {
return false;
}
#[cfg(not(any(feature = "esp32c5", feature = "esp32c6")))]
{
let min = CSI_MIN_SIG_MODE.load(Ordering::Relaxed);
if min != 0 && (info.packet_mode() as u8) < min {
return false;
}
}
true
}
pub fn runtime_reporting() -> crate::Reporting {
match POLICY_KIND.load(Ordering::Relaxed) {
POLICY_REPORT => crate::Reporting::Report {
to: if REPORT_LOCAL.load(Ordering::Relaxed) {
crate::ReportTo::Local
} else {
crate::ReportTo::Host
},
},
POLICY_THRESHOLD => crate::Reporting::ThresholdBased(crate::Threshold {
level: POLICY_A.load(Ordering::Relaxed),
hold_ms: POLICY_B.load(Ordering::Relaxed),
variation_only: POLICY_VARIATION.load(Ordering::Relaxed),
}),
POLICY_DECIMATE => crate::Reporting::Decimate(POLICY_A.load(Ordering::Relaxed)),
_ => crate::Reporting::None,
}
}
fn apply_publish_gate() {
let open = USER_PUBLISH_INTENT.load(Ordering::Relaxed)
&& REPORTS.load(Ordering::Relaxed)
&& CSI_OUTPUT_ENABLED.load(Ordering::Relaxed);
CSI_PUBLISH_ENABLED.store(open, Ordering::Release);
}
const POLICY_NONE: u8 = 0;
const POLICY_REPORT: u8 = 1;
const POLICY_THRESHOLD: u8 = 2;
const POLICY_DECIMATE: u8 = 3;
static POLICY_KIND: portable_atomic::AtomicU8 = portable_atomic::AtomicU8::new(POLICY_NONE);
static POLICY_A: portable_atomic::AtomicU16 = portable_atomic::AtomicU16::new(0);
static POLICY_B: portable_atomic::AtomicU16 = portable_atomic::AtomicU16::new(0);
static POLICY_VARIATION: AtomicBool = AtomicBool::new(false);
static REPORT_LOCAL: AtomicBool = AtomicBool::new(false);
static POLICY_RESET: AtomicBool = AtomicBool::new(false);
pub(crate) fn set_runtime_reporting(policy: crate::Reporting) {
let (kind, a, b, var) = match policy {
crate::Reporting::Report { .. } => (POLICY_REPORT, 0, 0, false),
crate::Reporting::None => (POLICY_NONE, 0, 0, false),
crate::Reporting::ThresholdBased(t) => {
(POLICY_THRESHOLD, t.level, t.hold_ms, t.variation_only)
}
crate::Reporting::Decimate(n) => (POLICY_DECIMATE, n.max(1), 0, false),
};
REPORT_LOCAL.store(
matches!(policy, crate::Reporting::Report { to: crate::ReportTo::Local }),
Ordering::Relaxed,
);
POLICY_A.store(a, Ordering::Relaxed);
POLICY_B.store(b, Ordering::Relaxed);
POLICY_VARIATION.store(var, Ordering::Relaxed);
POLICY_RESET.store(true, Ordering::Relaxed);
POLICY_KIND.store(kind, Ordering::Release);
REPORTS.store(policy.reports(), Ordering::Relaxed);
apply_publish_gate();
REPORTING_CHANGED.signal(());
}
pub fn set_reporting(reporting: crate::Reporting) {
set_runtime_reporting(reporting);
}
pub(crate) fn publish_open() -> bool {
CSI_PUBLISH_ENABLED.load(Ordering::Relaxed)
}
pub(crate) fn policy_admits_frame(frame: &mut crate::wire::CsiFrame) -> bool {
let kind = POLICY_KIND.load(Ordering::Relaxed);
if kind == POLICY_REPORT {
return true;
}
let buf: &[i8] = match &frame.payload {
crate::wire::CsiPayload::EspRaw { bytes, .. } => bytes,
_ if kind == POLICY_THRESHOLD => return true,
_ => &[],
};
let (keep, score) = policy_admits(buf, embassy_time::Instant::now().as_micros());
if !keep {
#[cfg(feature = "statistics")]
STATS.rx_policy_suppressed.fetch_add(1, Ordering::Relaxed);
return false;
}
if kind == POLICY_THRESHOLD && POLICY_VARIATION.load(Ordering::Relaxed) {
frame.payload = crate::wire::CsiPayload::Variation { value: score };
}
true
}
#[inline]
fn policy_admits(buf: &[i8], now_us: u64) -> (bool, u16) {
use super::policy::{Decimator, ThresholdState};
static mut THRESHOLD: ThresholdState = ThresholdState::new();
static mut DECIMATOR: Decimator = Decimator::new();
#[allow(static_mut_refs)]
unsafe {
if POLICY_RESET.swap(false, Ordering::Relaxed) {
THRESHOLD.reset();
DECIMATOR.reset();
}
match POLICY_KIND.load(Ordering::Relaxed) {
POLICY_THRESHOLD => {
let score = THRESHOLD.score(buf);
let keep = THRESHOLD.decide(
score,
POLICY_A.load(Ordering::Relaxed),
POLICY_B.load(Ordering::Relaxed),
now_us,
);
(keep, score)
}
POLICY_DECIMATE => (DECIMATOR.tick(POLICY_A.load(Ordering::Relaxed)), 0),
_ => (true, 0),
}
}
}
pub(crate) fn reset() {
CSI_INLINE_LOG_ENABLED.store(false, Ordering::Release);
USER_PUBLISH_INTENT.store(false, Ordering::Release);
CSI_PUBLISH_ENABLED.store(false, Ordering::Release);
CSI_OUTPUT_ENABLED.store(true, Ordering::Relaxed);
CSI_DELIVERY_MODE.store(CsiDeliveryMode::Off as u8, Ordering::Release);
CSI_CALLBACK.store(core::ptr::null_mut(), core::sync::atomic::Ordering::Release);
}
pub struct CsiNodeClient {
_private: (),
}
impl CsiNodeClient {
pub fn new() -> Self {
Self { _private: () }
}
pub async fn next_csi_packet(&mut self) -> CsiPacket {
if CSI_DELIVERY_MODE.load(Ordering::Relaxed) == CsiDeliveryMode::Off as u8 {
CSI_DELIVERY_MODE.store(CsiDeliveryMode::Async as u8, Ordering::Release);
USER_PUBLISH_INTENT.store(true, Ordering::Release);
apply_publish_gate();
}
core::future::poll_fn(|cx| {
if let Some(p) = CSI_QUEUE.dequeue() {
return core::task::Poll::Ready(p);
}
CSI_WAKER.register(cx.waker());
if let Some(p) = CSI_QUEUE.dequeue() {
core::task::Poll::Ready(p)
} else {
core::task::Poll::Pending
}
})
.await
}
pub async fn get_csi_data(&mut self) -> CsiPacket {
self.next_csi_packet().await
}
pub async fn print_csi_w_metadata(&mut self) {
let packet = self.next_csi_packet().await;
crate::logging::logging::log_csi(packet);
embassy_futures::yield_now().await;
}
pub async fn send_stop(&self) {
STOP_SIGNAL.signal(());
}
}
impl Default for CsiNodeClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "esp32c5")]
pub(crate) fn build_csi_config(csi_config: &crate::CsiConfig) -> CsiConfig {
CsiConfig {
enable: csi_config.enable,
acquire_csi_legacy: csi_config.acquire_csi_legacy,
acquire_csi_force_lltf: csi_config.acquire_csi_force_lltf,
acquire_csi_ht20: csi_config.acquire_csi_ht20,
acquire_csi_ht40: csi_config.acquire_csi_ht40,
acquire_csi_vht: csi_config.acquire_csi_vht,
acquire_csi_su: csi_config.acquire_csi_su,
acquire_csi_mu: csi_config.acquire_csi_mu,
acquire_csi_dcm: csi_config.acquire_csi_dcm,
acquire_csi_beamformed: csi_config.acquire_csi_beamformed,
acquire_csi_he_stbc: csi_config.acquire_csi_he_stbc,
val_scale_cfg: csi_config.val_scale_cfg,
dump_ack_en: csi_config.dump_ack_en,
reserved: csi_config.reserved,
}
}
#[cfg(feature = "esp32c6")]
pub(crate) fn build_csi_config(csi_config: &crate::CsiConfig) -> CsiConfig {
CsiConfig {
enable: csi_config.enable,
acquire_csi_legacy: csi_config.acquire_csi_legacy,
acquire_csi_ht20: csi_config.acquire_csi_ht20,
acquire_csi_ht40: csi_config.acquire_csi_ht40,
acquire_csi_su: csi_config.acquire_csi_su,
acquire_csi_mu: csi_config.acquire_csi_mu,
acquire_csi_dcm: csi_config.acquire_csi_dcm,
acquire_csi_beamformed: csi_config.acquire_csi_beamformed,
acquire_csi_he_stbc: csi_config.acquire_csi_he_stbc,
val_scale_cfg: csi_config.val_scale_cfg,
dump_ack_en: csi_config.dump_ack_en,
reserved: csi_config.reserved,
}
}
#[cfg(not(any(feature = "esp32c5", feature = "esp32c6")))]
pub(crate) fn build_csi_config(csi_config: &crate::CsiConfig) -> CsiConfig {
CsiConfig {
lltf_en: csi_config.lltf_en,
htltf_en: csi_config.htltf_en,
stbc_htltf2_en: csi_config.stbc_htltf2_en,
ltf_merge_en: csi_config.ltf_merge_en,
channel_filter_en: csi_config.channel_filter_en,
manu_scale: csi_config.manu_scale,
shift: csi_config.shift,
dump_ack_en: csi_config.dump_ack_en,
}
}
#[cfg(feature = "esp32c5")]
fn c5_bitfield_workaround(want: CsiConfig) -> CsiConfig {
CsiConfig {
enable: want.enable,
acquire_csi_legacy: want.acquire_csi_legacy,
acquire_csi_ht20: want.acquire_csi_force_lltf as u32,
acquire_csi_ht40: want.acquire_csi_ht20,
acquire_csi_su: want.acquire_csi_ht40,
acquire_csi_mu: want.acquire_csi_vht as u32,
acquire_csi_dcm: want.acquire_csi_su,
acquire_csi_beamformed: want.acquire_csi_mu,
acquire_csi_he_stbc: want.acquire_csi_dcm,
val_scale_cfg: want.acquire_csi_beamformed,
dump_ack_en: want.acquire_csi_he_stbc,
reserved: want.val_scale_cfg,
acquire_csi_force_lltf: false,
acquire_csi_vht: false,
}
}
pub(crate) fn set_csi(controller: &mut WifiController, config: CsiConfig) {
#[cfg(feature = "esp32c5")]
let config = c5_bitfield_workaround(config);
controller
.set_csi(config, |info: esp_radio::wifi::csi::WifiCsiInfo<'_>| {
capture_csi_info(info);
})
.unwrap();
}
fn capture_csi_info(info: esp_radio::wifi::csi::WifiCsiInfo<'_>) {
#[cfg(feature = "statistics")]
STATS.rx_count.fetch_add(1, Ordering::Relaxed);
#[cfg(all(feature = "statistics", any(feature = "esp32c5", feature = "esp32c6")))]
crate::stats::record_cur_bb_format(info.cur_bb_format() as u32);
let raw_cb_ptr = CSI_RAW_CALLBACK.load(Ordering::Relaxed);
if !raw_cb_ptr.is_null() {
let raw_cb: fn() = unsafe { core::mem::transmute::<*mut (), fn()>(raw_cb_ptr) };
raw_cb();
return;
}
if !CSI_PUBLISH_ENABLED.load(Ordering::Relaxed) {
return;
}
if !csi_passes_filter(&info) {
#[cfg(feature = "statistics")]
STATS.rx_filtered.fetch_add(1, Ordering::Relaxed);
return;
}
let (admitted, score) = if POLICY_KIND.load(Ordering::Relaxed) == POLICY_REPORT {
(true, 0)
} else {
policy_admits(info.buf(), embassy_time::Instant::now().as_micros())
};
if !admitted {
#[cfg(feature = "statistics")]
STATS.rx_policy_suppressed.fetch_add(1, Ordering::Relaxed);
return;
}
#[allow(unused_mut)]
let mut csi_packet = match CsiPacket::from_wifi_csi_info(&info) {
Some(p) => p,
None => {
#[cfg(feature = "statistics")]
record_loss(&STATS.rx_oversize, 1);
return;
}
};
if POLICY_VARIATION.load(Ordering::Relaxed)
&& POLICY_KIND.load(Ordering::Relaxed) == POLICY_THRESHOLD
{
csi_packet.frame.payload = crate::wire::CsiPayload::Variation { value: score };
}
deliver(csi_packet);
}
pub(crate) fn deliver(csi_packet: CsiPacket) {
#[cfg(feature = "statistics")]
if seq_drop_detection_enabled() {
#[cfg(not(feature = "esp32c5"))]
let seq = csi_packet.frame.meta.frame_seq;
#[cfg(feature = "esp32c5")]
let seq = None;
let retry = csi_packet.frame.header.map(|h| h.is_retry());
crate::stats::record_tx_frame(csi_packet.mac(), seq, retry);
}
match CSI_DELIVERY_MODE.load(Ordering::Relaxed) {
m if m == CsiDeliveryMode::Callback as u8 => {
let cb_ptr = CSI_CALLBACK.load(core::sync::atomic::Ordering::Relaxed);
if !cb_ptr.is_null() {
let cb: fn(&CsiPacket) =
unsafe { core::mem::transmute::<*mut (), fn(&CsiPacket)>(cb_ptr) };
cb(&csi_packet);
}
return;
}
m if m == CsiDeliveryMode::Async as u8 => {
match CSI_QUEUE.enqueue(csi_packet) {
Ok(()) => CSI_WAKER.wake(),
Err(packet) => {
#[cfg(feature = "statistics")]
record_loss(&STATS.rx_queue_full, 1);
if OVERFLOW_DROP_OLDEST.load(Ordering::Relaxed) {
let _ = CSI_QUEUE.dequeue();
if CSI_QUEUE.enqueue(packet).is_ok() {
CSI_WAKER.wake();
}
}
}
}
return;
}
_ => {}
}
if CSI_INLINE_LOG_ENABLED.load(Ordering::Relaxed) && !REPORT_LOCAL.load(Ordering::Relaxed) {
crate::logging::logging::log_csi(csi_packet);
}
}
pub async fn run_process_csi_packet() {
#[cfg(feature = "statistics")]
STATS
.capture_start_time
.store(Instant::now().as_ticks(), Ordering::Relaxed);
#[cfg(feature = "statistics")]
let mut last_rate_update = Instant::now();
#[cfg(feature = "statistics")]
let mut last_rx_count = STATS.rx_count.load(Ordering::Relaxed);
#[cfg(feature = "statistics")]
let mut last_tx_count = STATS.tx_count.load(Ordering::Relaxed);
loop {
match select3(
STOP_SIGNAL.wait(),
CSI_OUTPUT_CHANGED.wait(),
Timer::after_millis(500),
)
.await
{
Either3::First(_) => {
STOP_SIGNAL.signal(());
break;
}
Either3::Second(_) => {
CSI_OUTPUT_CHANGED.reset();
#[cfg(feature = "statistics")]
{
STATS
.capture_start_time
.store(Instant::now().as_ticks(), Ordering::Relaxed);
last_rate_update = Instant::now();
last_rx_count = STATS.rx_count.load(Ordering::Relaxed);
last_tx_count = STATS.tx_count.load(Ordering::Relaxed);
RESET_SEQ_TRACKER.store(true, Ordering::Relaxed);
}
}
Either3::Third(_) => {
#[cfg(feature = "statistics")]
{
let elapsed_secs = last_rate_update.elapsed().as_secs();
if elapsed_secs >= 1 {
let current_rx = STATS.rx_count.load(Ordering::Relaxed);
let current_tx = STATS.tx_count.load(Ordering::Relaxed);
let rx_rate =
((current_rx.saturating_sub(last_rx_count)) / elapsed_secs) as u32;
let tx_rate =
((current_tx.saturating_sub(last_tx_count)) / elapsed_secs) as u32;
STATS.rx_rate_hz.store(rx_rate, Ordering::Relaxed);
STATS.tx_rate_hz.store(tx_rate, Ordering::Relaxed);
last_rx_count = current_rx;
last_tx_count = current_tx;
last_rate_update = Instant::now();
}
}
}
}
}
}