use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Tab {
#[default]
Dashboard,
Config,
Control,
Stream,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WiFiMode {
Sta,
Monitor,
Sniffer,
}
impl WiFiMode {
pub fn as_api_value(self) -> &'static str {
match self {
Self::Sta => "sta",
Self::Monitor => "monitor",
Self::Sniffer => "sniffer",
}
}
}
impl Default for WiFiMode {
fn default() -> Self {
Self::Sta
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CollectionMode {
Collector,
Listener,
}
impl CollectionMode {
pub fn as_api_value(self) -> &'static str {
match self {
Self::Collector => "collector",
Self::Listener => "listener",
}
}
}
impl Default for CollectionMode {
fn default() -> Self {
Self::Collector
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogMode {
Text,
ArrayList,
Serialized,
}
impl LogMode {
pub fn as_api_value(self) -> &'static str {
match self {
Self::Text => "text",
Self::ArrayList => "array-list",
Self::Serialized => "serialized",
}
}
}
impl Default for LogMode {
fn default() -> Self {
Self::ArrayList
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
Stream,
Dump,
Both,
}
impl OutputMode {
pub fn as_api_value(self) -> &'static str {
match self {
Self::Stream => "stream",
Self::Dump => "dump",
Self::Both => "both",
}
}
}
impl Default for OutputMode {
fn default() -> Self {
Self::Stream
}
}
#[derive(Debug, Clone, Default)]
pub struct WiFiForm {
pub mode: WiFiMode,
pub sta_ssid: String,
pub sta_password: String,
pub channel: String,
}
#[derive(Debug, Clone)]
pub struct TrafficForm {
pub frequency_hz: String,
}
impl Default for TrafficForm {
fn default() -> Self {
Self {
frequency_hz: "100".to_owned(),
}
}
}
#[derive(Debug, Clone)]
pub struct CsiForm {
pub disable_lltf: bool,
pub disable_htltf: bool,
pub disable_stbc_htltf: bool,
pub disable_ltf_merge: bool,
pub disable_csi: bool,
pub disable_csi_legacy: bool,
pub disable_csi_ht20: bool,
pub disable_csi_ht40: bool,
pub disable_csi_su: bool,
pub disable_csi_mu: bool,
pub disable_csi_dcm: bool,
pub disable_csi_beamformed: bool,
pub csi_he_stbc: String,
pub val_scale_cfg: String,
}
impl Default for CsiForm {
fn default() -> Self {
Self {
disable_lltf: false,
disable_htltf: false,
disable_stbc_htltf: false,
disable_ltf_merge: false,
disable_csi: false,
disable_csi_legacy: false,
disable_csi_ht20: false,
disable_csi_ht40: false,
disable_csi_su: false,
disable_csi_mu: false,
disable_csi_dcm: false,
disable_csi_beamformed: false,
csi_he_stbc: "0".to_owned(),
val_scale_cfg: "0".to_owned(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PersistentState {
pub server_host: String,
pub server_port: String,
pub wifi: WiFiForm,
pub traffic: TrafficForm,
pub csi: CsiForm,
pub collection_mode: CollectionMode,
pub log_mode: LogMode,
pub output_mode: OutputMode,
pub start_duration_seconds: String,
}
#[derive(Debug, Clone)]
pub struct TransientUiState {
pub active_tab: Tab,
pub status_message: String,
pub error_message: String,
pub auto_scroll_stream: bool,
}
impl Default for TransientUiState {
fn default() -> Self {
Self {
active_tab: Tab::Dashboard,
status_message: "Ready".to_owned(),
error_message: String::new(),
auto_scroll_stream: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FrameSummary {
pub timestamp: String,
pub length: usize,
pub preview_hex: String,
}
#[derive(Debug, Clone, Default)]
pub struct RuntimeState {
pub ws_connected: bool,
pub collection_active_estimate: bool,
pub frames_received: u64,
pub bytes_received: u64,
pub recent_frames: Vec<FrameSummary>,
pub events: Vec<String>,
pub last_http_status: Option<u16>,
pub latest_config: Option<DeviceConfig>,
}
#[derive(Debug, Clone)]
pub enum UserIntent {
FetchConfig,
ResetConfig,
SetWifi(WiFiForm),
SetTraffic(TrafficForm),
SetCsi(CsiForm),
SetCollectionMode(CollectionMode),
SetLogMode(LogMode),
SetOutputMode(OutputMode),
StartCollection { duration_seconds: String },
ResetDevice,
ConnectWebSocket,
DisconnectWebSocket,
ClearFrames,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DeviceConfig {
pub wifi_mode: Option<String>,
pub channel: Option<u16>,
pub sta_ssid: Option<String>,
pub traffic_hz: Option<u16>,
pub collection_mode: Option<String>,
pub log_mode: Option<String>,
pub log_format: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct AppState {
pub persistent: PersistentState,
pub transient: TransientUiState,
pub runtime: RuntimeState,
intent_queue: Vec<UserIntent>,
}
impl AppState {
pub fn with_defaults() -> Self {
let mut state = Self::default();
state.persistent.server_host = "127.0.0.1".to_owned();
state.persistent.server_port = "3000".to_owned();
state
}
pub fn push_intent(&mut self, intent: UserIntent) {
self.intent_queue.push(intent);
}
pub fn drain_intents(&mut self) -> Vec<UserIntent> {
std::mem::take(&mut self.intent_queue)
}
pub fn push_event(&mut self, message: impl Into<String>) {
self.runtime.events.push(message.into());
if self.runtime.events.len() > 300 {
let drain_to = self.runtime.events.len() - 300;
self.runtime.events.drain(0..drain_to);
}
}
pub fn push_frame(&mut self, bytes: &[u8]) {
self.runtime.frames_received = self.runtime.frames_received.saturating_add(1);
self.runtime.bytes_received = self.runtime.bytes_received.saturating_add(bytes.len() as u64);
let preview = bytes
.iter()
.take(24)
.map(|b| format!("{b:02X}"))
.collect::<Vec<_>>()
.join(" ");
self.runtime.recent_frames.push(FrameSummary {
timestamp: chrono::Local::now().format("%H:%M:%S").to_string(),
length: bytes.len(),
preview_hex: preview,
});
if self.runtime.recent_frames.len() > 300 {
let drain_to = self.runtime.recent_frames.len() - 300;
self.runtime.recent_frames.drain(0..drain_to);
}
}
pub fn base_http_url(&self) -> String {
format!(
"http://{}:{}",
self.persistent.server_host.trim(),
self.persistent.server_port.trim()
)
}
pub fn base_ws_url(&self) -> String {
format!(
"ws://{}:{}/api/ws",
self.persistent.server_host.trim(),
self.persistent.server_port.trim()
)
}
pub fn apply_device_config(&mut self, config: DeviceConfig) {
if let Some(mode) = config.wifi_mode.as_deref() {
self.persistent.wifi.mode = match mode {
"monitor" => WiFiMode::Monitor,
"sniffer" => WiFiMode::Sniffer,
_ => WiFiMode::Sta,
};
}
if let Some(channel) = config.channel {
self.persistent.wifi.channel = channel.to_string();
}
if let Some(ssid) = &config.sta_ssid {
self.persistent.wifi.sta_ssid = ssid.clone();
}
if let Some(traffic_hz) = config.traffic_hz {
self.persistent.traffic.frequency_hz = traffic_hz.to_string();
}
if let Some(mode) = config.collection_mode.as_deref() {
self.persistent.collection_mode = if mode == "listener" {
CollectionMode::Listener
} else {
CollectionMode::Collector
};
}
if let Some(mode) = config.log_mode.as_deref().or(config.log_format.as_deref()) {
self.persistent.log_mode = match mode {
"text" => LogMode::Text,
"cobs" | "serialized" => LogMode::Serialized,
_ => LogMode::ArrayList,
};
}
self.runtime.latest_config = Some(config);
}
}