use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DeviceConfig {
pub wifi_mode: Option<String>,
pub channel: Option<u8>,
pub sta_ssid: Option<String>,
pub traffic_hz: Option<u64>,
pub collection_mode: Option<String>,
pub log_mode: Option<String>,
pub phy_rate: Option<String>,
pub io_tx_enabled: Option<bool>,
pub io_rx_enabled: Option<bool>,
pub csi_delivery_mode: Option<String>,
pub csi_logging_enabled: Option<bool>,
}
fn quote_cli_arg(s: &str) -> Result<String, String> {
if s.contains('\n') || s.contains('\r') {
return Err("value cannot contain newline characters".to_string());
}
if !s.contains('\'') {
Ok(format!("'{s}'"))
} else if !s.contains('"') {
Ok(format!("\"{s}\""))
} else {
Err("value cannot contain both single and double quote characters".to_string())
}
}
#[derive(Debug, Deserialize)]
pub struct WifiConfig {
pub mode: String,
pub sta_ssid: Option<String>,
pub sta_password: Option<String>,
pub channel: Option<u8>,
}
impl WifiConfig {
pub fn to_cli_command(&self) -> Result<String, String> {
match self.mode.as_str() {
"station" | "sniffer" | "esp-now-central" | "esp-now-peripheral" => {}
other => {
return Err(format!(
"Unknown wifi mode '{other}'; expected station, sniffer, esp-now-central, or esp-now-peripheral"
));
}
}
let mut cmd = format!("set-wifi --mode={}", self.mode);
if let Some(ssid) = &self.sta_ssid {
if ssid.len() > 32 {
return Err(format!(
"sta_ssid is {} bytes; firmware limit is 32 bytes",
ssid.len()
));
}
cmd.push_str(&format!(" --sta-ssid={}", quote_cli_arg(ssid)?));
}
if let Some(pass) = &self.sta_password {
if pass.len() > 32 {
return Err(format!(
"sta_password is {} bytes; firmware limit is 32 bytes",
pass.len()
));
}
cmd.push_str(&format!(" --sta-password={}", quote_cli_arg(pass)?));
}
if let Some(ch) = self.channel {
cmd.push_str(&format!(" --set-channel={ch}"));
}
Ok(cmd)
}
}
#[derive(Debug, Deserialize)]
pub struct TrafficConfig {
pub frequency_hz: u64,
}
impl TrafficConfig {
pub fn to_cli_command(&self) -> String {
format!("set-traffic --frequency-hz={}", self.frequency_hz)
}
}
#[derive(Debug, Deserialize)]
pub struct CsiConfig {
pub disable_lltf: Option<bool>,
pub disable_htltf: Option<bool>,
pub disable_stbc_htltf: Option<bool>,
pub disable_ltf_merge: Option<bool>,
pub disable_csi: Option<bool>,
pub disable_csi_legacy: Option<bool>,
pub disable_csi_ht20: Option<bool>,
pub disable_csi_ht40: Option<bool>,
pub disable_csi_su: Option<bool>,
pub disable_csi_mu: Option<bool>,
pub disable_csi_dcm: Option<bool>,
pub disable_csi_beamformed: Option<bool>,
pub csi_he_stbc: Option<u32>,
pub val_scale_cfg: Option<u32>,
}
impl CsiConfig {
pub fn to_cli_command(&self) -> String {
let mut cmd = "set-csi".to_string();
if self.disable_lltf.unwrap_or(false) {
cmd.push_str(" --disable-lltf");
}
if self.disable_htltf.unwrap_or(false) {
cmd.push_str(" --disable-htltf");
}
if self.disable_stbc_htltf.unwrap_or(false) {
cmd.push_str(" --disable-stbc-htltf");
}
if self.disable_ltf_merge.unwrap_or(false) {
cmd.push_str(" --disable-ltf-merge");
}
if self.disable_csi.unwrap_or(false) {
cmd.push_str(" --disable-csi");
}
if self.disable_csi_legacy.unwrap_or(false) {
cmd.push_str(" --disable-csi-legacy");
}
if self.disable_csi_ht20.unwrap_or(false) {
cmd.push_str(" --disable-csi-ht20");
}
if self.disable_csi_ht40.unwrap_or(false) {
cmd.push_str(" --disable-csi-ht40");
}
if self.disable_csi_su.unwrap_or(false) {
cmd.push_str(" --disable-csi-su");
}
if self.disable_csi_mu.unwrap_or(false) {
cmd.push_str(" --disable-csi-mu");
}
if self.disable_csi_dcm.unwrap_or(false) {
cmd.push_str(" --disable-csi-dcm");
}
if self.disable_csi_beamformed.unwrap_or(false) {
cmd.push_str(" --disable-csi-beamformed");
}
if let Some(stbc) = self.csi_he_stbc {
cmd.push_str(&format!(" --csi-he-stbc={stbc}"));
}
if let Some(scale) = self.val_scale_cfg {
cmd.push_str(&format!(" --val-scale-cfg={scale}"));
}
cmd
}
}
#[derive(Debug, Deserialize)]
pub struct CollectionModeConfig {
pub mode: String,
}
impl CollectionModeConfig {
pub fn to_cli_command(&self) -> Result<String, String> {
match self.mode.as_str() {
"collector" | "listener" => {
Ok(format!("set-collection-mode --mode={}", self.mode))
}
other => Err(format!(
"Unknown collection mode '{other}'; expected collector or listener"
)),
}
}
}
#[derive(Debug, Deserialize)]
pub struct LogModeConfig {
pub mode: LogMode,
}
impl LogModeConfig {
pub fn to_cli_command(&self) -> String {
format!("set-log-mode --mode={}", self.mode.as_cli_value())
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LogMode {
Text,
#[default]
ArrayList,
Serialized,
EspCsiTool,
}
impl LogMode {
pub fn as_cli_value(&self) -> &'static str {
match self {
Self::Text => "text",
Self::ArrayList => "array-list",
Self::Serialized => "serialized",
Self::EspCsiTool => "esp-csi-tool",
}
}
}
#[derive(Debug, Deserialize)]
pub struct StartConfig {
pub duration: Option<u64>,
}
impl StartConfig {
pub fn to_cli_command(&self) -> String {
match self.duration {
Some(d) => format!("start --duration={d}"),
None => "start".to_string(),
}
}
}
#[derive(Debug, Deserialize)]
pub struct RateConfig {
pub rate: String,
}
impl RateConfig {
pub fn to_cli_command(&self) -> String {
format!("set-rate --rate={}", self.rate)
}
}
#[derive(Debug, Deserialize)]
pub struct IoTasksConfig {
pub tx: Option<bool>,
pub rx: Option<bool>,
}
impl IoTasksConfig {
pub fn to_cli_command(&self) -> Result<String, String> {
if self.tx.is_none() && self.rx.is_none() {
return Err("at least one of tx or rx must be provided".to_string());
}
let mut cmd = "set-io-tasks".to_string();
if let Some(tx) = self.tx {
cmd.push_str(&format!(" --tx={}", if tx { "on" } else { "off" }));
}
if let Some(rx) = self.rx {
cmd.push_str(&format!(" --rx={}", if rx { "on" } else { "off" }));
}
Ok(cmd)
}
}
#[derive(Debug, Deserialize)]
pub struct CsiDeliveryConfig {
pub mode: Option<String>,
pub logging: Option<bool>,
}
impl CsiDeliveryConfig {
pub fn to_cli_command(&self) -> Result<String, String> {
if self.mode.is_none() && self.logging.is_none() {
return Err("at least one of mode or logging must be provided".to_string());
}
let mut cmd = "set-csi-delivery".to_string();
if let Some(mode) = &self.mode {
match mode.as_str() {
"off" | "callback" | "async" => {}
other => {
return Err(format!(
"Unknown csi-delivery mode '{other}'; expected off, callback, or async"
));
}
}
cmd.push_str(&format!(" --mode={mode}"));
}
if let Some(logging) = self.logging {
cmd.push_str(&format!(
" --logging={}",
if logging { "on" } else { "off" }
));
}
Ok(cmd)
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputMode {
#[default]
Stream,
Dump,
Both,
}
#[derive(Debug, Deserialize)]
pub struct OutputModeConfig {
pub mode: String,
}
#[derive(Debug, Serialize)]
pub struct ApiResponse {
pub success: bool,
pub message: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct DeviceInfo {
pub banner_version: String,
pub name: Option<String>,
pub version: Option<String>,
pub chip: Option<String>,
pub protocol: Option<u32>,
pub features: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct CollectionStatusResponse {
pub serial_connected: bool,
pub collection_running: bool,
pub port_path: String,
}
impl CollectionStatusResponse {
pub fn from_state(
serial_connected: &AtomicBool,
collection_running: &AtomicBool,
port_path: String,
) -> Self {
Self {
serial_connected: serial_connected.load(Ordering::SeqCst),
collection_running: collection_running.load(Ordering::SeqCst),
port_path,
}
}
}