1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum Tab {
6 #[default]
7 Dashboard,
8 Config,
9 Control,
10 Stream,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum WiFiMode {
16 Sta,
17 Monitor,
18 Sniffer,
19}
20
21impl WiFiMode {
22 pub fn as_api_value(self) -> &'static str {
24 match self {
25 Self::Sta => "sta",
26 Self::Monitor => "monitor",
27 Self::Sniffer => "sniffer",
28 }
29 }
30}
31
32impl Default for WiFiMode {
33 fn default() -> Self {
34 Self::Sta
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum CollectionMode {
41 Collector,
42 Listener,
43}
44
45impl CollectionMode {
46 pub fn as_api_value(self) -> &'static str {
48 match self {
49 Self::Collector => "collector",
50 Self::Listener => "listener",
51 }
52 }
53}
54
55impl Default for CollectionMode {
56 fn default() -> Self {
57 Self::Collector
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum LogMode {
64 Text,
65 ArrayList,
66 Serialized,
67}
68
69impl LogMode {
70 pub fn as_api_value(self) -> &'static str {
72 match self {
73 Self::Text => "text",
74 Self::ArrayList => "array-list",
75 Self::Serialized => "serialized",
76 }
77 }
78}
79
80impl Default for LogMode {
81 fn default() -> Self {
82 Self::ArrayList
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum OutputMode {
89 Stream,
90 Dump,
91 Both,
92}
93
94impl OutputMode {
95 pub fn as_api_value(self) -> &'static str {
97 match self {
98 Self::Stream => "stream",
99 Self::Dump => "dump",
100 Self::Both => "both",
101 }
102 }
103}
104
105impl Default for OutputMode {
106 fn default() -> Self {
107 Self::Stream
108 }
109}
110
111#[derive(Debug, Clone, Default)]
113pub struct WiFiForm {
114 pub mode: WiFiMode,
115 pub sta_ssid: String,
116 pub sta_password: String,
117 pub channel: String,
118}
119
120#[derive(Debug, Clone)]
122pub struct TrafficForm {
123 pub frequency_hz: String,
124}
125
126impl Default for TrafficForm {
127 fn default() -> Self {
128 Self {
129 frequency_hz: "100".to_owned(),
130 }
131 }
132}
133
134#[derive(Debug, Clone)]
136pub struct CsiForm {
137 pub disable_lltf: bool,
138 pub disable_htltf: bool,
139 pub disable_stbc_htltf: bool,
140 pub disable_ltf_merge: bool,
141 pub disable_csi: bool,
142 pub disable_csi_legacy: bool,
143 pub disable_csi_ht20: bool,
144 pub disable_csi_ht40: bool,
145 pub disable_csi_su: bool,
146 pub disable_csi_mu: bool,
147 pub disable_csi_dcm: bool,
148 pub disable_csi_beamformed: bool,
149 pub csi_he_stbc: String,
150 pub val_scale_cfg: String,
151}
152
153impl Default for CsiForm {
154 fn default() -> Self {
155 Self {
156 disable_lltf: false,
157 disable_htltf: false,
158 disable_stbc_htltf: false,
159 disable_ltf_merge: false,
160 disable_csi: false,
161 disable_csi_legacy: false,
162 disable_csi_ht20: false,
163 disable_csi_ht40: false,
164 disable_csi_su: false,
165 disable_csi_mu: false,
166 disable_csi_dcm: false,
167 disable_csi_beamformed: false,
168 csi_he_stbc: "0".to_owned(),
169 val_scale_cfg: "0".to_owned(),
170 }
171 }
172}
173
174#[derive(Debug, Clone, Default)]
176pub struct PersistentState {
177 pub server_host: String,
178 pub server_port: String,
179 pub wifi: WiFiForm,
180 pub traffic: TrafficForm,
181 pub csi: CsiForm,
182 pub collection_mode: CollectionMode,
183 pub log_mode: LogMode,
184 pub output_mode: OutputMode,
185 pub start_duration_seconds: String,
186}
187
188#[derive(Debug, Clone)]
190pub struct TransientUiState {
191 pub active_tab: Tab,
192 pub status_message: String,
193 pub error_message: String,
194 pub auto_scroll_stream: bool,
195}
196
197impl Default for TransientUiState {
198 fn default() -> Self {
199 Self {
200 active_tab: Tab::Dashboard,
201 status_message: "Ready".to_owned(),
202 error_message: String::new(),
203 auto_scroll_stream: true,
204 }
205 }
206}
207
208#[derive(Debug, Clone, Default)]
210pub struct FrameSummary {
211 pub timestamp: String,
212 pub length: usize,
213 pub preview_hex: String,
214}
215
216#[derive(Debug, Clone, Default)]
218pub struct RuntimeState {
219 pub ws_connected: bool,
220 pub collection_active_estimate: bool,
222 pub frames_received: u64,
223 pub bytes_received: u64,
224 pub recent_frames: Vec<FrameSummary>,
225 pub events: Vec<String>,
226 pub last_http_status: Option<u16>,
227 pub latest_config: Option<DeviceConfig>,
228}
229
230#[derive(Debug, Clone)]
232pub enum UserIntent {
233 FetchConfig,
234 ResetConfig,
235 SetWifi(WiFiForm),
236 SetTraffic(TrafficForm),
237 SetCsi(CsiForm),
238 SetCollectionMode(CollectionMode),
239 SetLogMode(LogMode),
240 SetOutputMode(OutputMode),
241 StartCollection { duration_seconds: String },
242 ResetDevice,
243 ConnectWebSocket,
244 DisconnectWebSocket,
245 ClearFrames,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, Default)]
250pub struct DeviceConfig {
251 pub wifi_mode: Option<String>,
252 pub channel: Option<u16>,
253 pub sta_ssid: Option<String>,
254 pub traffic_hz: Option<u16>,
255 pub collection_mode: Option<String>,
256 pub log_mode: Option<String>,
257 pub log_format: Option<String>,
258}
259
260#[derive(Debug, Clone, Default)]
264pub struct AppState {
265 pub persistent: PersistentState,
266 pub transient: TransientUiState,
267 pub runtime: RuntimeState,
268 intent_queue: Vec<UserIntent>,
269}
270
271impl AppState {
272 pub fn with_defaults() -> Self {
274 let mut state = Self::default();
275 state.persistent.server_host = "127.0.0.1".to_owned();
276 state.persistent.server_port = "3000".to_owned();
277 state
278 }
279
280 pub fn push_intent(&mut self, intent: UserIntent) {
282 self.intent_queue.push(intent);
283 }
284
285 pub fn drain_intents(&mut self) -> Vec<UserIntent> {
287 std::mem::take(&mut self.intent_queue)
288 }
289
290 pub fn push_event(&mut self, message: impl Into<String>) {
292 self.runtime.events.push(message.into());
293 if self.runtime.events.len() > 300 {
294 let drain_to = self.runtime.events.len() - 300;
295 self.runtime.events.drain(0..drain_to);
296 }
297 }
298
299 pub fn push_frame(&mut self, bytes: &[u8]) {
301 self.runtime.frames_received = self.runtime.frames_received.saturating_add(1);
302 self.runtime.bytes_received = self.runtime.bytes_received.saturating_add(bytes.len() as u64);
303
304 let preview = bytes
305 .iter()
306 .take(24)
307 .map(|b| format!("{b:02X}"))
308 .collect::<Vec<_>>()
309 .join(" ");
310
311 self.runtime.recent_frames.push(FrameSummary {
312 timestamp: chrono::Local::now().format("%H:%M:%S").to_string(),
313 length: bytes.len(),
314 preview_hex: preview,
315 });
316
317 if self.runtime.recent_frames.len() > 300 {
318 let drain_to = self.runtime.recent_frames.len() - 300;
319 self.runtime.recent_frames.drain(0..drain_to);
320 }
321 }
322
323 pub fn base_http_url(&self) -> String {
325 format!(
326 "http://{}:{}",
327 self.persistent.server_host.trim(),
328 self.persistent.server_port.trim()
329 )
330 }
331
332 pub fn base_ws_url(&self) -> String {
334 format!(
335 "ws://{}:{}/api/ws",
336 self.persistent.server_host.trim(),
337 self.persistent.server_port.trim()
338 )
339 }
340
341 pub fn apply_device_config(&mut self, config: DeviceConfig) {
343 if let Some(mode) = config.wifi_mode.as_deref() {
344 self.persistent.wifi.mode = match mode {
345 "monitor" => WiFiMode::Monitor,
346 "sniffer" => WiFiMode::Sniffer,
347 _ => WiFiMode::Sta,
348 };
349 }
350
351 if let Some(channel) = config.channel {
352 self.persistent.wifi.channel = channel.to_string();
353 }
354
355 if let Some(ssid) = &config.sta_ssid {
356 self.persistent.wifi.sta_ssid = ssid.clone();
357 }
358
359 if let Some(traffic_hz) = config.traffic_hz {
360 self.persistent.traffic.frequency_hz = traffic_hz.to_string();
361 }
362
363 if let Some(mode) = config.collection_mode.as_deref() {
364 self.persistent.collection_mode = if mode == "listener" {
365 CollectionMode::Listener
366 } else {
367 CollectionMode::Collector
368 };
369 }
370
371 if let Some(mode) = config.log_mode.as_deref().or(config.log_format.as_deref()) {
372 self.persistent.log_mode = match mode {
373 "text" => LogMode::Text,
374 "cobs" | "serialized" => LogMode::Serialized,
376 _ => LogMode::ArrayList,
377 };
378 }
379
380 self.runtime.latest_config = Some(config);
381 }
382}