use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::csi_frame::Bandwidth;
pub const MAX_SETUP_ID: u8 = 127;
pub const MIN_PERIOD_MS: u32 = 10;
pub const MAX_PERIOD_MS: u32 = 3_600_000;
pub const MAX_BURST_INSTANCES: u8 = 64;
pub const MAX_REPORT_SUBCARRIERS: u16 = 484;
#[derive(Debug, Clone, PartialEq, Error)]
pub enum BfError {
#[error("invalid measurement setup ID {value} (valid 0..={MAX_SETUP_ID})")]
InvalidSetupId { value: u8 },
#[error("measurement period {period_ms} ms out of range ({MIN_PERIOD_MS}..={MAX_PERIOD_MS})")]
InvalidPeriod { period_ms: u32 },
#[error("burst instance count {count} out of range (1..={MAX_BURST_INSTANCES})")]
InvalidBurstInstances { count: u8 },
#[error("reporting threshold {value}% out of range (0..=100)")]
InvalidThreshold { value: u8 },
#[error("transceiver roles leave no sensing transmitter/receiver pair")]
InvalidTransceiverRoles,
#[error("sensing disabled by consent policy")]
SensingDisabledByPolicy,
#[error("report payload empty")]
EmptyPayload,
#[error("report payload claims {count} subcarriers (max {MAX_REPORT_SUBCARRIERS})")]
PayloadTooLarge { count: u16 },
#[error(
"report payload length mismatch: declared {declared}, amplitudes {amplitudes}, phases {phases}"
)]
PayloadLengthMismatch {
declared: usize,
amplitudes: usize,
phases: usize,
},
#[error("report payload value at index {index} is not finite (or negative amplitude)")]
PayloadValueInvalid { index: usize },
#[error("setup ID mismatch: session {expected}, frame {got}")]
SetupIdMismatch { expected: u8, got: u8 },
#[error("negotiation timed out for setup {setup_id} after {attempts} attempts")]
NegotiationTimeout { setup_id: u8, attempts: u8 },
#[error("command not valid in state {state}")]
InvalidStateForCommand { state: &'static str },
#[error("invalid CSI batch size {got} (must be >= 1)")]
InvalidBatchSize { got: usize },
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SpecProfile {
DraftCompatible,
Ieee80211Bf2025,
VendorExtension(String),
}
impl SpecProfile {
pub fn accepts(&self, requested: &SpecProfile) -> bool {
self == requested
|| matches!(
(self, requested),
(SpecProfile::Ieee80211Bf2025, SpecProfile::DraftCompatible)
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ConsentMode {
LabOnly,
ExplicitConsent,
ManagedEnterprisePolicy,
Disabled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SensingRole {
Initiator,
Responder,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransceiverRole {
Transmitter,
Receiver,
TransmitterReceiver,
}
impl TransceiverRole {
pub fn is_transmitter(self) -> bool {
matches!(self, Self::Transmitter | Self::TransmitterReceiver)
}
pub fn is_receiver(self) -> bool {
matches!(self, Self::Receiver | Self::TransmitterReceiver)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "u8", into = "u8")]
pub struct MeasurementSetupId(u8);
impl MeasurementSetupId {
pub fn new(value: u8) -> Result<Self, BfError> {
if value > MAX_SETUP_ID {
Err(BfError::InvalidSetupId { value })
} else {
Ok(Self(value))
}
}
pub fn value(self) -> u8 {
self.0
}
}
impl TryFrom<u8> for MeasurementSetupId {
type Error = BfError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<MeasurementSetupId> for u8 {
fn from(id: MeasurementSetupId) -> u8 {
id.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MeasurementInstanceId(u8);
impl MeasurementInstanceId {
pub fn new(value: u8) -> Self {
Self(value)
}
pub fn value(self) -> u8 {
self.0
}
pub fn wrapping_next(self) -> Self {
Self(self.0.wrapping_add(1))
}
}
pub fn bandwidth_mhz(bw: Bandwidth) -> u16 {
match bw {
Bandwidth::Bw20 => 20,
Bandwidth::Bw40 => 40,
Bandwidth::Bw80 => 80,
Bandwidth::Bw160 => 160,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "RawThresholdParams")]
pub struct ThresholdParams {
delta_percent: u8,
}
#[derive(Deserialize)]
struct RawThresholdParams {
delta_percent: u8,
}
impl TryFrom<RawThresholdParams> for ThresholdParams {
type Error = BfError;
fn try_from(raw: RawThresholdParams) -> Result<Self, Self::Error> {
Self::new(raw.delta_percent)
}
}
impl ThresholdParams {
pub fn new(delta_percent: u8) -> Result<Self, BfError> {
if delta_percent > 100 {
Err(BfError::InvalidThreshold {
value: delta_percent,
})
} else {
Ok(Self { delta_percent })
}
}
pub fn delta_percent(self) -> u8 {
self.delta_percent
}
pub fn exceeds(self, previous: f64, current: f64) -> bool {
let denom = previous.abs().max(f64::EPSILON);
((current - previous).abs() / denom) * 100.0 >= self.delta_percent as f64
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReportingConfig {
EveryInstance,
ThresholdBased(ThresholdParams),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MeasurementSetupParams {
pub bandwidth: Bandwidth,
pub period_ms: u32,
pub burst_instances: u8,
pub reporting: ReportingConfig,
pub initiator_role: TransceiverRole,
pub responder_role: TransceiverRole,
pub consent: ConsentMode,
}
impl MeasurementSetupParams {
pub fn validate(&self) -> Result<(), BfError> {
if self.period_ms < MIN_PERIOD_MS || self.period_ms > MAX_PERIOD_MS {
return Err(BfError::InvalidPeriod {
period_ms: self.period_ms,
});
}
if self.burst_instances == 0 || self.burst_instances > MAX_BURST_INSTANCES {
return Err(BfError::InvalidBurstInstances {
count: self.burst_instances,
});
}
let has_tx = self.initiator_role.is_transmitter() || self.responder_role.is_transmitter();
let has_rx = self.initiator_role.is_receiver() || self.responder_role.is_receiver();
if !has_tx || !has_rx {
return Err(BfError::InvalidTransceiverRoles);
}
if self.consent == ConsentMode::Disabled {
return Err(BfError::SensingDisabledByPolicy);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SensingCapabilities {
pub sub_7_ghz: bool,
pub dmg: bool,
pub edmg: bool,
pub csi_report: bool,
pub threshold_reporting: bool,
pub sensing_by_proxy: bool,
pub max_bandwidth_mhz: u16,
pub max_period_ms: u32,
pub max_active_setups: u16,
}
impl SensingCapabilities {
pub fn sim_full() -> Self {
Self {
sub_7_ghz: true,
dmg: false,
edmg: false,
csi_report: true,
threshold_reporting: true,
sensing_by_proxy: true,
max_bandwidth_mhz: 160,
max_period_ms: MAX_PERIOD_MS,
max_active_setups: 8,
}
}
pub fn esp32_opportunistic() -> Self {
Self {
sub_7_ghz: true,
dmg: false,
edmg: false,
csi_report: true,
threshold_reporting: true,
sensing_by_proxy: false,
max_bandwidth_mhz: 40,
max_period_ms: 60_000,
max_active_setups: 4,
}
}
pub fn evaluate(&self, params: &MeasurementSetupParams) -> Result<(), SetupStatus> {
if !self.sub_7_ghz || !self.csi_report {
return Err(SetupStatus::RejectedUnsupportedParams);
}
if bandwidth_mhz(params.bandwidth) > self.max_bandwidth_mhz {
return Err(SetupStatus::RejectedUnsupportedParams);
}
if params.period_ms > self.max_period_ms {
return Err(SetupStatus::RejectedUnsupportedParams);
}
if matches!(params.reporting, ReportingConfig::ThresholdBased(_))
&& !self.threshold_reporting
{
return Err(SetupStatus::RejectedUnsupportedParams);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SetupStatus {
Accepted,
RejectedNotSupported,
RejectedUnsupportedParams,
RejectedSetupIdCollision,
RejectedIncompatibleProfile,
RejectedByPolicy,
RejectedCapacity,
}