1use serde::{Deserialize, Serialize};
9use std::sync::atomic::{AtomicBool, Ordering};
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
30pub struct DeviceConfig {
31 pub wifi: WifiSection,
32 pub collection: CollectionSection,
33 pub csi_config: CsiConfigSection,
34 pub log_mode: Option<String>,
35 pub csi_delivery_mode: Option<String>,
36 pub csi_logging_enabled: Option<bool>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, Default)]
41pub struct WifiSection {
42 pub mode: Option<String>,
44 pub channel: Option<u8>,
46 pub sta_ssid: Option<String>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, Default)]
52pub struct CollectionSection {
53 pub mode: Option<String>,
55 pub traffic_hz: Option<u64>,
57 pub phy_rate: Option<String>,
59 pub io_tx_enabled: Option<bool>,
61 pub io_rx_enabled: Option<bool>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, Default)]
76pub struct CsiConfigSection {
77 pub lltf_enabled: Option<bool>,
80 pub htltf_enabled: Option<bool>,
82 pub stbc_htltf_enabled: Option<bool>,
84 pub ltf_merge_enabled: Option<bool>,
86 pub channel_filter_enabled: Option<bool>,
88 pub manual_scale: Option<bool>,
90 pub shift: Option<u8>,
92 pub dump_ack_enabled: Option<bool>,
94
95 pub acquire_csi: Option<u32>,
98 pub acquire_csi_legacy: Option<u32>,
100 pub acquire_csi_ht20: Option<u32>,
102 pub acquire_csi_ht40: Option<u32>,
104 pub acquire_csi_su: Option<u32>,
106 pub acquire_csi_mu: Option<u32>,
108 pub acquire_csi_dcm: Option<u32>,
110 pub acquire_csi_beamformed: Option<u32>,
112 pub csi_he_stbc: Option<u32>,
114 pub val_scale_cfg: Option<u32>,
116}
117
118impl DeviceConfig {
119 pub fn firmware_defaults() -> Self {
129 Self {
130 wifi: WifiSection {
131 mode: Some("sniffer".to_string()),
132 channel: Some(1),
133 sta_ssid: Some(String::new()),
134 },
135 collection: CollectionSection {
136 mode: Some("collector".to_string()),
137 traffic_hz: Some(100),
138 phy_rate: Some("mcs0-lgi".to_string()),
139 io_tx_enabled: Some(true),
140 io_rx_enabled: Some(true),
141 },
142 csi_config: CsiConfigSection {
143 lltf_enabled: Some(true),
145 htltf_enabled: Some(true),
146 stbc_htltf_enabled: Some(true),
147 ltf_merge_enabled: Some(true),
148 channel_filter_enabled: Some(false),
149 manual_scale: Some(false),
150 shift: Some(0),
151 dump_ack_enabled: Some(false),
152 acquire_csi: Some(1),
154 acquire_csi_legacy: Some(1),
155 acquire_csi_ht20: Some(1),
156 acquire_csi_ht40: Some(1),
157 acquire_csi_su: Some(1),
158 acquire_csi_mu: Some(1),
159 acquire_csi_dcm: Some(1),
160 acquire_csi_beamformed: Some(1),
161 csi_he_stbc: Some(2),
162 val_scale_cfg: Some(2),
163 },
164 log_mode: None,
165 csi_delivery_mode: None,
166 csi_logging_enabled: None,
167 }
168 }
169}
170
171fn quote_cli_arg(s: &str) -> Result<String, String> {
181 if s.contains('\n') || s.contains('\r') {
182 return Err("value cannot contain newline characters".to_string());
183 }
184 if !s.contains('\'') {
185 Ok(format!("'{s}'"))
186 } else if !s.contains('"') {
187 Ok(format!("\"{s}\""))
188 } else {
189 Err("value cannot contain both single and double quote characters".to_string())
190 }
191}
192
193#[derive(Debug, Deserialize)]
196pub struct WifiConfig {
197 pub mode: String,
199 pub sta_ssid: Option<String>,
200 pub sta_password: Option<String>,
201 pub channel: Option<u8>,
202}
203
204impl WifiConfig {
205 pub fn to_cli_command(&self) -> Result<String, String> {
207 match self.mode.as_str() {
208 "station" | "sniffer" | "esp-now-central" | "esp-now-peripheral" => {}
209 other => {
210 return Err(format!(
211 "Unknown wifi mode '{other}'; expected station, sniffer, esp-now-central, or esp-now-peripheral"
212 ));
213 }
214 }
215
216 let mut cmd = format!("set-wifi --mode={}", self.mode);
217
218 if let Some(ssid) = &self.sta_ssid {
219 if ssid.len() > 32 {
220 return Err(format!(
221 "sta_ssid is {} bytes; firmware limit is 32 bytes",
222 ssid.len()
223 ));
224 }
225 cmd.push_str(&format!(" --sta-ssid={}", quote_cli_arg(ssid)?));
226 }
227
228 if let Some(pass) = &self.sta_password {
229 if pass.len() > 32 {
230 return Err(format!(
231 "sta_password is {} bytes; firmware limit is 32 bytes",
232 pass.len()
233 ));
234 }
235 cmd.push_str(&format!(" --sta-password={}", quote_cli_arg(pass)?));
236 }
237
238 if let Some(ch) = self.channel {
239 cmd.push_str(&format!(" --set-channel={ch}"));
240 }
241
242 Ok(cmd)
243 }
244}
245
246#[derive(Debug, Deserialize)]
247pub struct TrafficConfig {
248 pub frequency_hz: u64,
250}
251
252impl TrafficConfig {
253 pub fn to_cli_command(&self) -> String {
254 format!("set-traffic --frequency-hz={}", self.frequency_hz)
255 }
256}
257
258#[derive(Debug, Deserialize)]
263pub struct CsiConfig {
264 pub disable_lltf: Option<bool>,
266 pub disable_htltf: Option<bool>,
267 pub disable_stbc_htltf: Option<bool>,
268 pub disable_ltf_merge: Option<bool>,
269 pub disable_csi: Option<bool>,
271 pub disable_csi_legacy: Option<bool>,
272 pub disable_csi_ht20: Option<bool>,
273 pub disable_csi_ht40: Option<bool>,
274 pub disable_csi_su: Option<bool>,
275 pub disable_csi_mu: Option<bool>,
276 pub disable_csi_dcm: Option<bool>,
277 pub disable_csi_beamformed: Option<bool>,
278 pub csi_he_stbc: Option<u32>,
280 pub val_scale_cfg: Option<u32>,
282}
283
284impl CsiConfig {
285 pub fn to_cli_command(&self) -> String {
286 let mut cmd = "set-csi".to_string();
287 if self.disable_lltf.unwrap_or(false) {
288 cmd.push_str(" --disable-lltf");
289 }
290 if self.disable_htltf.unwrap_or(false) {
291 cmd.push_str(" --disable-htltf");
292 }
293 if self.disable_stbc_htltf.unwrap_or(false) {
294 cmd.push_str(" --disable-stbc-htltf");
295 }
296 if self.disable_ltf_merge.unwrap_or(false) {
297 cmd.push_str(" --disable-ltf-merge");
298 }
299 if self.disable_csi.unwrap_or(false) {
300 cmd.push_str(" --disable-csi");
301 }
302 if self.disable_csi_legacy.unwrap_or(false) {
303 cmd.push_str(" --disable-csi-legacy");
304 }
305 if self.disable_csi_ht20.unwrap_or(false) {
306 cmd.push_str(" --disable-csi-ht20");
307 }
308 if self.disable_csi_ht40.unwrap_or(false) {
309 cmd.push_str(" --disable-csi-ht40");
310 }
311 if self.disable_csi_su.unwrap_or(false) {
312 cmd.push_str(" --disable-csi-su");
313 }
314 if self.disable_csi_mu.unwrap_or(false) {
315 cmd.push_str(" --disable-csi-mu");
316 }
317 if self.disable_csi_dcm.unwrap_or(false) {
318 cmd.push_str(" --disable-csi-dcm");
319 }
320 if self.disable_csi_beamformed.unwrap_or(false) {
321 cmd.push_str(" --disable-csi-beamformed");
322 }
323 if let Some(stbc) = self.csi_he_stbc {
324 cmd.push_str(&format!(" --csi-he-stbc={stbc}"));
325 }
326 if let Some(scale) = self.val_scale_cfg {
327 cmd.push_str(&format!(" --val-scale-cfg={scale}"));
328 }
329 cmd
330 }
331}
332
333#[derive(Debug, Deserialize)]
334pub struct CollectionModeConfig {
335 pub mode: String,
337}
338
339impl CollectionModeConfig {
340 pub fn to_cli_command(&self) -> Result<String, String> {
341 match self.mode.as_str() {
342 "collector" | "listener" => {
343 Ok(format!("set-collection-mode --mode={}", self.mode))
344 }
345 other => Err(format!(
346 "Unknown collection mode '{other}'; expected collector or listener"
347 )),
348 }
349 }
350}
351
352#[derive(Debug, Deserialize)]
353pub struct LogModeConfig {
354 pub mode: LogMode,
355}
356
357impl LogModeConfig {
358 pub fn to_cli_command(&self) -> String {
359 format!("set-log-mode --mode={}", self.mode.as_cli_value())
360 }
361}
362
363#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
365#[serde(rename_all = "kebab-case")]
366pub enum LogMode {
367 Text,
369 #[default]
371 ArrayList,
372 Serialized,
374 EspCsiTool,
376}
377
378impl LogMode {
379 pub fn as_cli_value(&self) -> &'static str {
380 match self {
381 Self::Text => "text",
382 Self::ArrayList => "array-list",
383 Self::Serialized => "serialized",
384 Self::EspCsiTool => "esp-csi-tool",
385 }
386 }
387}
388
389#[derive(Debug, Deserialize)]
390pub struct StartConfig {
391 pub duration: Option<u64>,
393}
394
395impl StartConfig {
396 pub fn to_cli_command(&self) -> String {
397 match self.duration {
398 Some(d) => format!("start --duration={d}"),
399 None => "start".to_string(),
400 }
401 }
402}
403
404#[derive(Debug, Deserialize)]
407pub struct RateConfig {
408 pub rate: String,
411}
412
413impl RateConfig {
414 pub fn to_cli_command(&self) -> String {
415 format!("set-rate --rate={}", self.rate)
416 }
417}
418
419#[derive(Debug, Deserialize)]
423pub struct IoTasksConfig {
424 pub tx: Option<bool>,
425 pub rx: Option<bool>,
426}
427
428impl IoTasksConfig {
429 pub fn to_cli_command(&self) -> Result<String, String> {
430 if self.tx.is_none() && self.rx.is_none() {
431 return Err("at least one of tx or rx must be provided".to_string());
432 }
433 let mut cmd = "set-io-tasks".to_string();
434 if let Some(tx) = self.tx {
435 cmd.push_str(&format!(" --tx={}", if tx { "on" } else { "off" }));
436 }
437 if let Some(rx) = self.rx {
438 cmd.push_str(&format!(" --rx={}", if rx { "on" } else { "off" }));
439 }
440 Ok(cmd)
441 }
442}
443
444#[derive(Debug, Deserialize)]
447pub struct CsiDeliveryConfig {
448 pub mode: Option<String>,
450 pub logging: Option<bool>,
452}
453
454impl CsiDeliveryConfig {
455 pub fn to_cli_command(&self) -> Result<String, String> {
456 if self.mode.is_none() && self.logging.is_none() {
457 return Err("at least one of mode or logging must be provided".to_string());
458 }
459 let mut cmd = "set-csi-delivery".to_string();
460 if let Some(mode) = &self.mode {
461 match mode.as_str() {
462 "off" | "callback" | "async" => {}
463 other => {
464 return Err(format!(
465 "Unknown csi-delivery mode '{other}'; expected off, callback, or async"
466 ));
467 }
468 }
469 cmd.push_str(&format!(" --mode={mode}"));
470 }
471 if let Some(logging) = self.logging {
472 cmd.push_str(&format!(
473 " --logging={}",
474 if logging { "on" } else { "off" }
475 ));
476 }
477 Ok(cmd)
478 }
479}
480
481#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
485#[serde(rename_all = "lowercase")]
486pub enum OutputMode {
487 #[default]
489 Stream,
490 Dump,
492 Both,
494}
495
496#[derive(Debug, Deserialize)]
497pub struct OutputModeConfig {
498 pub mode: String,
499}
500
501#[derive(Debug, Serialize)]
504pub struct ApiResponse {
505 pub success: bool,
506 pub message: String,
507}
508
509#[derive(Debug, Clone, Serialize)]
518pub struct DeviceInfo {
519 pub banner_version: String,
521 pub name: Option<String>,
523 pub version: Option<String>,
525 pub chip: Option<String>,
527 pub protocol: Option<u32>,
531 pub features: Vec<String>,
533}
534
535#[derive(Debug, Serialize)]
538pub struct CollectionStatusResponse {
539 pub serial_connected: bool,
540 pub collection_running: bool,
541 pub port_path: String,
542}
543
544impl CollectionStatusResponse {
545 pub fn from_state(
546 serial_connected: &AtomicBool,
547 collection_running: &AtomicBool,
548 port_path: String,
549 ) -> Self {
550 Self {
551 serial_connected: serial_connected.load(Ordering::SeqCst),
552 collection_running: collection_running.load(Ordering::SeqCst),
553 port_path,
554 }
555 }
556}