154 lines
4.4 KiB
Python
154 lines
4.4 KiB
Python
from typing import Literal, Self
|
|
|
|
from pydantic import BaseModel, model_validator
|
|
|
|
Host = tuple[str, int]
|
|
|
|
|
|
class Preprocessing(BaseModel):
|
|
class Bandpass(BaseModel):
|
|
lowcut: int
|
|
highcut: int
|
|
|
|
@property
|
|
def bounds(self) -> tuple[int, int]:
|
|
return (self.lowcut, self.highcut)
|
|
|
|
bandpass: Bandpass | None = None
|
|
subcarrier_step: int = 1
|
|
denoising: Literal["none", "median", "mean"] = "median"
|
|
denoising_period: float = 1
|
|
|
|
steps: list[
|
|
Literal[
|
|
"fill_pilots",
|
|
"skip_subcarriers",
|
|
"remove_agc",
|
|
"remove_sfo",
|
|
"remove_sto",
|
|
"bandpass",
|
|
]
|
|
] = ["fill_pilots", "skip_subcarriers", "remove_agc", "remove_sfo"]
|
|
|
|
@model_validator(mode="after")
|
|
def skip_in_steps(self) -> Self:
|
|
if "skip_subcarriers" not in self.steps and self.subcarrier_step != 1:
|
|
raise ValueError(
|
|
"subcarrier_step must be 1 if skip_subcarriers is not in steps"
|
|
)
|
|
return self
|
|
|
|
@model_validator(mode="after")
|
|
def bandpass_in_steps(self) -> Self:
|
|
if "bandpass" not in self.steps and self.bandpass is not None:
|
|
raise ValueError(
|
|
"bandpass must be in preprocessing steps if bandpass configuration is "
|
|
"provided"
|
|
)
|
|
|
|
if "bandpass" in self.steps and self.bandpass is None:
|
|
raise ValueError(
|
|
"bandpass configuration must be provided if bandpass is in "
|
|
"preprocessing steps"
|
|
)
|
|
return self
|
|
|
|
|
|
class MUSIC(BaseModel):
|
|
eigval_threshold: float
|
|
window_size: int
|
|
|
|
class Heatmap(BaseModel):
|
|
theta_resolution: int
|
|
tof_resolution: int
|
|
tof_max: float
|
|
|
|
heatmap: Heatmap
|
|
|
|
|
|
class Antennas(BaseModel):
|
|
spacing: float
|
|
order: list[tuple[Host, int]]
|
|
|
|
@property
|
|
def count(self) -> int:
|
|
return len(self.order)
|
|
|
|
|
|
class Config(BaseModel):
|
|
receive_hosts: list[Host]
|
|
transmit_host: Host
|
|
|
|
antennas: Antennas
|
|
|
|
collection_sample_rate: int
|
|
processing_sample_rate: int
|
|
central_freq: int
|
|
channel_width: Literal[20, 40, 80, 160]
|
|
frame_format: Literal["NOHT", "HT", "VHT", "HESU"]
|
|
|
|
preprocessing: Preprocessing
|
|
music: MUSIC
|
|
|
|
@property
|
|
def central_freq_hz(self) -> int:
|
|
return self.central_freq * 1_000_000
|
|
|
|
@property
|
|
def band(self) -> Literal["2.4", "5", "6"]:
|
|
if self.central_freq in range(2412, 2484):
|
|
return "2.4"
|
|
if self.central_freq in range(5180, 5320):
|
|
return "5"
|
|
if self.central_freq in range(5955, 7115):
|
|
return "6"
|
|
raise ValueError(f"{self.central_freq} is not a valid Wi-Fi channel")
|
|
|
|
@property
|
|
def num_guards(self) -> int:
|
|
if self.channel_width == 20:
|
|
return 7
|
|
return 11
|
|
|
|
@property
|
|
def subcarriers(self) -> int:
|
|
return (
|
|
int((self.channel_width * 1e6) // self.delta_f) - self.num_guards
|
|
) // self.preprocessing.subcarrier_step
|
|
|
|
@property
|
|
def _delta_f_no_skipping(self) -> int:
|
|
if self.frame_format == "HESU":
|
|
return 78_125
|
|
return 312_500
|
|
|
|
@property
|
|
def delta_f(self) -> int:
|
|
return self._delta_f_no_skipping * self.preprocessing.subcarrier_step
|
|
|
|
@property
|
|
def subcarrier_frequencies(self) -> list[int]:
|
|
num_subcarriers = self.channel_width * 1_000_000 // self._delta_f_no_skipping
|
|
return [
|
|
self.central_freq_hz + i * self._delta_f_no_skipping
|
|
for i in range(-num_subcarriers // 2, num_subcarriers // 2)
|
|
]
|
|
|
|
@model_validator(mode="after")
|
|
def channels(self) -> Self:
|
|
band_start = 2412 if self.band == "2.4" else 5180 if self.band == "5" else 5955
|
|
if self.band == "2.4" and self.channel_width not in [20, 40]:
|
|
raise ValueError(
|
|
f"2.4GHz channel {self.central_freq} must "
|
|
"have a channel width of 20 or 40 MHz"
|
|
)
|
|
|
|
if (self.band == "2.4" and (self.central_freq - band_start) % 5 != 0) or (
|
|
self.band in ["5", "6"]
|
|
and ((self.central_freq - band_start) % self.channel_width != 0)
|
|
):
|
|
raise ValueError(
|
|
f"Central frequency {self.central_freq} must be a channel as in https://en.wikipedia.org/wiki/List_of_WLAN_channels"
|
|
)
|
|
return self
|