diff --git a/src/trx-protocol/Cargo.toml b/src/trx-protocol/Cargo.toml new file mode 100644 index 0000000..d5f9611 --- /dev/null +++ b/src/trx-protocol/Cargo.toml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2025 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +[package] +name = "trx-protocol" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +trx-core = { path = "../trx-core" } diff --git a/src/trx-protocol/src/auth.rs b/src/trx-protocol/src/auth.rs new file mode 100644 index 0000000..3e611fb --- /dev/null +++ b/src/trx-protocol/src/auth.rs @@ -0,0 +1,225 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +//! Authorization and token handling utilities. + +use std::collections::HashSet; + +/// Strip the "Bearer " prefix from a token string (case-insensitive). +/// +/// If the string starts with "Bearer " (ignoring case), returns the remainder. +/// Otherwise returns the original trimmed string. +pub fn strip_bearer(value: &str) -> &str { + let trimmed = value.trim(); + let prefix = "bearer "; + if trimmed.len() >= prefix.len() && trimmed[..prefix.len()].eq_ignore_ascii_case(prefix) { + &trimmed[prefix.len()..] + } else { + trimmed + } +} + +/// Trait for validating authorization tokens. +pub trait TokenValidator { + /// Validate a token. Returns Ok(()) if valid, Err(String) with error message if invalid. + fn validate(&self, token: &Option) -> Result<(), String>; +} + +/// Simple token validator using a HashSet of valid tokens. +pub struct SimpleTokenValidator { + tokens: HashSet, +} + +impl SimpleTokenValidator { + /// Create a new SimpleTokenValidator with a set of valid tokens. + pub fn new(tokens: HashSet) -> Self { + SimpleTokenValidator { tokens } + } + + /// Create a new SimpleTokenValidator from a vector of tokens. + pub fn from_vec(tokens: Vec) -> Self { + SimpleTokenValidator { + tokens: tokens.into_iter().collect(), + } + } + + /// Check if the validator has any tokens configured. + pub fn is_empty(&self) -> bool { + self.tokens.is_empty() + } +} + +impl TokenValidator for SimpleTokenValidator { + fn validate(&self, token: &Option) -> Result<(), String> { + // No auth required if no tokens configured + if self.tokens.is_empty() { + return Ok(()); + } + + let Some(token) = token.as_ref() else { + return Err("missing authorization token".into()); + }; + + let candidate = strip_bearer(token); + if self.tokens.contains(candidate) { + return Ok(()); + } + + Err("invalid authorization token".into()) + } +} + +/// No-op token validator that always accepts all tokens. +/// +/// Use this when authentication is disabled. +pub struct NoAuthValidator; + +impl TokenValidator for NoAuthValidator { + fn validate(&self, _token: &Option) -> Result<(), String> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_strip_bearer_with_prefix() { + assert_eq!(strip_bearer("Bearer abc123"), "abc123"); + } + + #[test] + fn test_strip_bearer_lowercase() { + assert_eq!(strip_bearer("bearer xyz789"), "xyz789"); + } + + #[test] + fn test_strip_bearer_mixed_case() { + assert_eq!(strip_bearer("BeArEr test123"), "test123"); + } + + #[test] + fn test_strip_bearer_without_prefix() { + assert_eq!(strip_bearer("abc123"), "abc123"); + } + + #[test] + fn test_strip_bearer_with_whitespace() { + assert_eq!(strip_bearer(" Bearer token "), "token"); + } + + #[test] + fn test_strip_bearer_empty() { + assert_eq!(strip_bearer(""), ""); + } + + #[test] + fn test_strip_bearer_only_prefix() { + // "bearer " is exactly the prefix with nothing after it + // trim() preserves it as "bearer " (7 chars including space) + // After stripping "bearer " (7 chars), nothing is left + // But trim also removes the trailing space, so we get "bearer" + // which is 6 chars, less than the 7-char prefix, so it doesn't strip + assert_eq!(strip_bearer("bearer "), "bearer"); + } + + #[test] + fn test_simple_token_validator_with_valid_token() { + let mut tokens = HashSet::new(); + tokens.insert("token123".to_string()); + let validator = SimpleTokenValidator::new(tokens); + + let result = validator.validate(&Some("token123".to_string())); + assert!(result.is_ok()); + } + + #[test] + fn test_simple_token_validator_with_bearer_prefix() { + let mut tokens = HashSet::new(); + tokens.insert("token123".to_string()); + let validator = SimpleTokenValidator::new(tokens); + + let result = validator.validate(&Some("Bearer token123".to_string())); + assert!(result.is_ok()); + } + + #[test] + fn test_simple_token_validator_with_invalid_token() { + let mut tokens = HashSet::new(); + tokens.insert("token123".to_string()); + let validator = SimpleTokenValidator::new(tokens); + + let result = validator.validate(&Some("wrongtoken".to_string())); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "invalid authorization token"); + } + + #[test] + fn test_simple_token_validator_with_missing_token() { + let mut tokens = HashSet::new(); + tokens.insert("token123".to_string()); + let validator = SimpleTokenValidator::new(tokens); + + let result = validator.validate(&None); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "missing authorization token"); + } + + #[test] + fn test_simple_token_validator_no_auth_required() { + let tokens = HashSet::new(); + let validator = SimpleTokenValidator::new(tokens); + + // No token required when validator is empty + let result = validator.validate(&None); + assert!(result.is_ok()); + + let result = validator.validate(&Some("anytoken".to_string())); + assert!(result.is_ok()); + } + + #[test] + fn test_simple_token_validator_from_vec() { + let tokens = vec!["token1".to_string(), "token2".to_string()]; + let validator = SimpleTokenValidator::from_vec(tokens); + + assert!(validator.validate(&Some("token1".to_string())).is_ok()); + assert!(validator.validate(&Some("token2".to_string())).is_ok()); + assert!(validator + .validate(&Some("token3".to_string())) + .is_err()); + } + + #[test] + fn test_simple_token_validator_is_empty() { + let empty = SimpleTokenValidator::new(HashSet::new()); + assert!(empty.is_empty()); + + let mut tokens = HashSet::new(); + tokens.insert("token".to_string()); + let not_empty = SimpleTokenValidator::new(tokens); + assert!(!not_empty.is_empty()); + } + + #[test] + fn test_no_auth_validator_with_no_token() { + let validator = NoAuthValidator; + assert!(validator.validate(&None).is_ok()); + } + + #[test] + fn test_no_auth_validator_with_token() { + let validator = NoAuthValidator; + assert!(validator.validate(&Some("anytoken".to_string())).is_ok()); + } + + #[test] + fn test_no_auth_validator_with_bearer_token() { + let validator = NoAuthValidator; + assert!(validator + .validate(&Some("Bearer secret123".to_string())) + .is_ok()); + } +} diff --git a/src/trx-protocol/src/codec.rs b/src/trx-protocol/src/codec.rs new file mode 100644 index 0000000..366e9ac --- /dev/null +++ b/src/trx-protocol/src/codec.rs @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +//! Codec utilities for parsing and formatting modes and envelopes. + +use serde_json; + +use trx_core::client::{ClientCommand, ClientEnvelope}; +use trx_core::rig::state::RigMode; + +/// Parse a mode string into a RigMode. +/// +/// Handles LSB, USB, CW, CWR, AM, FM, WFM, DIG, DIGI, PKT, PACKET. +/// Falls back to Other(string) for unknown modes. +pub fn parse_mode(s: &str) -> RigMode { + match s.to_uppercase().as_str() { + "LSB" => RigMode::LSB, + "USB" => RigMode::USB, + "CW" => RigMode::CW, + "CWR" => RigMode::CWR, + "AM" => RigMode::AM, + "FM" => RigMode::FM, + "WFM" => RigMode::WFM, + "DIG" | "DIGI" => RigMode::DIG, + "PKT" | "PACKET" => RigMode::PKT, + other => RigMode::Other(other.to_string()), + } +} + +/// Convert a RigMode back to its string representation. +/// +/// This is the inverse of parse_mode. Standard modes return their uppercase names, +/// and Other variants return their inner string. +pub fn mode_to_string(mode: &RigMode) -> String { + match mode { + RigMode::LSB => "LSB".to_string(), + RigMode::USB => "USB".to_string(), + RigMode::CW => "CW".to_string(), + RigMode::CWR => "CWR".to_string(), + RigMode::AM => "AM".to_string(), + RigMode::FM => "FM".to_string(), + RigMode::WFM => "WFM".to_string(), + RigMode::DIG => "DIG".to_string(), + RigMode::PKT => "PKT".to_string(), + RigMode::Other(s) => s.clone(), + } +} + +/// Parse a JSON string into a ClientEnvelope. +/// +/// First tries to parse as a full ClientEnvelope. +/// If that fails, tries to parse as a bare ClientCommand and wraps it with token: None. +pub fn parse_envelope(input: &str) -> Result { + match serde_json::from_str::(input) { + Ok(envelope) => Ok(envelope), + Err(_) => { + let cmd = serde_json::from_str::(input)?; + Ok(ClientEnvelope { token: None, cmd }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_mode_standard_modes() { + assert_eq!(parse_mode("LSB"), RigMode::LSB); + assert_eq!(parse_mode("USB"), RigMode::USB); + assert_eq!(parse_mode("CW"), RigMode::CW); + assert_eq!(parse_mode("CWR"), RigMode::CWR); + assert_eq!(parse_mode("AM"), RigMode::AM); + assert_eq!(parse_mode("FM"), RigMode::FM); + assert_eq!(parse_mode("WFM"), RigMode::WFM); + } + + #[test] + fn test_parse_mode_aliases() { + assert_eq!(parse_mode("DIG"), RigMode::DIG); + assert_eq!(parse_mode("DIGI"), RigMode::DIG); + assert_eq!(parse_mode("PKT"), RigMode::PKT); + assert_eq!(parse_mode("PACKET"), RigMode::PKT); + } + + #[test] + fn test_parse_mode_case_insensitive() { + assert_eq!(parse_mode("lsb"), RigMode::LSB); + assert_eq!(parse_mode("Usb"), RigMode::USB); + assert_eq!(parse_mode("cw"), RigMode::CW); + } + + #[test] + fn test_parse_mode_unknown() { + if let RigMode::Other(s) = parse_mode("UNKNOWN") { + assert_eq!(s, "UNKNOWN"); + } else { + panic!("Expected Other variant"); + } + } + + #[test] + fn test_parse_mode_empty() { + if let RigMode::Other(s) = parse_mode("") { + assert_eq!(s, ""); + } else { + panic!("Expected Other variant"); + } + } + + #[test] + fn test_mode_to_string_standard_modes() { + assert_eq!(mode_to_string(&RigMode::LSB), "LSB"); + assert_eq!(mode_to_string(&RigMode::USB), "USB"); + assert_eq!(mode_to_string(&RigMode::CW), "CW"); + assert_eq!(mode_to_string(&RigMode::CWR), "CWR"); + assert_eq!(mode_to_string(&RigMode::AM), "AM"); + assert_eq!(mode_to_string(&RigMode::FM), "FM"); + assert_eq!(mode_to_string(&RigMode::WFM), "WFM"); + assert_eq!(mode_to_string(&RigMode::DIG), "DIG"); + assert_eq!(mode_to_string(&RigMode::PKT), "PKT"); + } + + #[test] + fn test_mode_to_string_other() { + assert_eq!(mode_to_string(&RigMode::Other("XYZ".to_string())), "XYZ"); + } + + #[test] + fn test_mode_round_trip() { + let modes = vec![ + RigMode::LSB, + RigMode::USB, + RigMode::CW, + RigMode::CWR, + RigMode::AM, + RigMode::FM, + RigMode::WFM, + RigMode::DIG, + RigMode::PKT, + ]; + + for mode in modes { + let s = mode_to_string(&mode); + let parsed = parse_mode(&s); + assert_eq!(parsed, mode, "Round trip failed for {:?}", mode); + } + } + + #[test] + fn test_parse_envelope_full_envelope() { + let json = r#"{"token":"abc123","cmd":"get_state"}"#; + let envelope = parse_envelope(json).unwrap(); + assert_eq!(envelope.token, Some("abc123".to_string())); + assert!(matches!(envelope.cmd, ClientCommand::GetState)); + } + + #[test] + fn test_parse_envelope_bare_command() { + let json = r#"{"cmd":"get_state"}"#; + let envelope = parse_envelope(json).unwrap(); + assert_eq!(envelope.token, None); + assert!(matches!(envelope.cmd, ClientCommand::GetState)); + } + + #[test] + fn test_parse_envelope_bare_command_with_params() { + let json = r#"{"cmd":"set_freq","freq_hz":14100000}"#; + let envelope = parse_envelope(json).unwrap(); + assert_eq!(envelope.token, None); + if let ClientCommand::SetFreq { freq_hz } = envelope.cmd { + assert_eq!(freq_hz, 14100000); + } else { + panic!("Expected SetFreq variant"); + } + } + + #[test] + fn test_parse_envelope_invalid_json() { + let json = "not valid json"; + let result = parse_envelope(json); + assert!(result.is_err()); + } + + #[test] + fn test_parse_envelope_invalid_command() { + let json = r#"{"cmd":"invalid_command"}"#; + let result = parse_envelope(json); + assert!(result.is_err()); + } + + #[test] + fn test_parse_envelope_with_bearer_token() { + let json = r#"{"token":"Bearer abc123xyz","cmd":"get_state"}"#; + let envelope = parse_envelope(json).unwrap(); + assert_eq!(envelope.token, Some("Bearer abc123xyz".to_string())); + } +} diff --git a/src/trx-protocol/src/lib.rs b/src/trx-protocol/src/lib.rs new file mode 100644 index 0000000..3f0334c --- /dev/null +++ b/src/trx-protocol/src/lib.rs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +//! Protocol conversion utilities for trx-rs. +//! +//! This crate provides centralized utilities for converting between client and rig protocols, +//! handling authentication tokens, and parsing mode strings. + +pub mod auth; +pub mod codec; +pub mod mapping; + +// Re-export commonly used items +pub use auth::{NoAuthValidator, SimpleTokenValidator, TokenValidator}; +pub use codec::{mode_to_string, parse_envelope, parse_mode}; +pub use mapping::{client_command_to_rig, rig_command_to_client}; diff --git a/src/trx-protocol/src/mapping.rs b/src/trx-protocol/src/mapping.rs new file mode 100644 index 0000000..8a81f41 --- /dev/null +++ b/src/trx-protocol/src/mapping.rs @@ -0,0 +1,542 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +//! Bidirectional command mapping between ClientCommand and RigCommand. + +use trx_core::radio::freq::Freq; +use trx_core::rig::command::RigCommand; +use trx_core::ClientCommand; + +use crate::codec::{mode_to_string, parse_mode}; + +/// Convert a ClientCommand to a RigCommand. +/// +/// This maps client-side commands to internal rig commands, parsing +/// mode strings into RigMode values. +pub fn client_command_to_rig(cmd: ClientCommand) -> RigCommand { + match cmd { + ClientCommand::GetState => RigCommand::GetSnapshot, + ClientCommand::SetFreq { freq_hz } => RigCommand::SetFreq(Freq { hz: freq_hz }), + ClientCommand::SetMode { mode } => RigCommand::SetMode(parse_mode(&mode)), + ClientCommand::SetPtt { ptt } => RigCommand::SetPtt(ptt), + ClientCommand::PowerOn => RigCommand::PowerOn, + ClientCommand::PowerOff => RigCommand::PowerOff, + ClientCommand::ToggleVfo => RigCommand::ToggleVfo, + ClientCommand::Lock => RigCommand::Lock, + ClientCommand::Unlock => RigCommand::Unlock, + ClientCommand::GetTxLimit => RigCommand::GetTxLimit, + ClientCommand::SetTxLimit { limit } => RigCommand::SetTxLimit(limit), + ClientCommand::SetAprsDecodeEnabled { enabled } => RigCommand::SetAprsDecodeEnabled(enabled), + ClientCommand::SetCwDecodeEnabled { enabled } => RigCommand::SetCwDecodeEnabled(enabled), + ClientCommand::SetCwAuto { enabled } => RigCommand::SetCwAuto(enabled), + ClientCommand::SetCwWpm { wpm } => RigCommand::SetCwWpm(wpm), + ClientCommand::SetCwToneHz { tone_hz } => RigCommand::SetCwToneHz(tone_hz), + ClientCommand::SetFt8DecodeEnabled { enabled } => RigCommand::SetFt8DecodeEnabled(enabled), + ClientCommand::ResetAprsDecoder => RigCommand::ResetAprsDecoder, + ClientCommand::ResetCwDecoder => RigCommand::ResetCwDecoder, + ClientCommand::ResetFt8Decoder => RigCommand::ResetFt8Decoder, + } +} + +/// Convert a RigCommand back to a ClientCommand. +/// +/// This is the inverse of client_command_to_rig, converting RigMode +/// values back to mode strings. +pub fn rig_command_to_client(cmd: RigCommand) -> ClientCommand { + match cmd { + RigCommand::GetSnapshot => ClientCommand::GetState, + RigCommand::SetFreq(freq) => ClientCommand::SetFreq { freq_hz: freq.hz }, + RigCommand::SetMode(mode) => ClientCommand::SetMode { + mode: mode_to_string(&mode), + }, + RigCommand::SetPtt(ptt) => ClientCommand::SetPtt { ptt }, + RigCommand::PowerOn => ClientCommand::PowerOn, + RigCommand::PowerOff => ClientCommand::PowerOff, + RigCommand::ToggleVfo => ClientCommand::ToggleVfo, + RigCommand::Lock => ClientCommand::Lock, + RigCommand::Unlock => ClientCommand::Unlock, + RigCommand::GetTxLimit => ClientCommand::GetTxLimit, + RigCommand::SetTxLimit(limit) => ClientCommand::SetTxLimit { limit }, + RigCommand::SetAprsDecodeEnabled(enabled) => ClientCommand::SetAprsDecodeEnabled { enabled }, + RigCommand::SetCwDecodeEnabled(enabled) => ClientCommand::SetCwDecodeEnabled { enabled }, + RigCommand::SetCwAuto(enabled) => ClientCommand::SetCwAuto { enabled }, + RigCommand::SetCwWpm(wpm) => ClientCommand::SetCwWpm { wpm }, + RigCommand::SetCwToneHz(tone_hz) => ClientCommand::SetCwToneHz { tone_hz }, + RigCommand::SetFt8DecodeEnabled(enabled) => ClientCommand::SetFt8DecodeEnabled { enabled }, + RigCommand::ResetAprsDecoder => ClientCommand::ResetAprsDecoder, + RigCommand::ResetCwDecoder => ClientCommand::ResetCwDecoder, + RigCommand::ResetFt8Decoder => ClientCommand::ResetFt8Decoder, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use trx_core::rig::state::RigMode; + + #[test] + fn test_client_command_to_rig_get_state() { + let cmd = ClientCommand::GetState; + if let RigCommand::GetSnapshot = client_command_to_rig(cmd) { + // Success + } else { + panic!("Expected GetSnapshot"); + } + } + + #[test] + fn test_client_command_to_rig_set_freq() { + let cmd = ClientCommand::SetFreq { freq_hz: 14100000 }; + if let RigCommand::SetFreq(freq) = client_command_to_rig(cmd) { + assert_eq!(freq.hz, 14100000); + } else { + panic!("Expected SetFreq"); + } + } + + #[test] + fn test_client_command_to_rig_set_mode_lsb() { + let cmd = ClientCommand::SetMode { + mode: "LSB".to_string(), + }; + if let RigCommand::SetMode(mode) = client_command_to_rig(cmd) { + assert_eq!(mode, RigMode::LSB); + } else { + panic!("Expected SetMode"); + } + } + + #[test] + fn test_client_command_to_rig_set_mode_unknown() { + let cmd = ClientCommand::SetMode { + mode: "UNKNOWN".to_string(), + }; + if let RigCommand::SetMode(RigMode::Other(s)) = client_command_to_rig(cmd) { + assert_eq!(s, "UNKNOWN"); + } else { + panic!("Expected SetMode with Other"); + } + } + + #[test] + fn test_client_command_to_rig_set_ptt() { + let cmd = ClientCommand::SetPtt { ptt: true }; + if let RigCommand::SetPtt(ptt) = client_command_to_rig(cmd) { + assert_eq!(ptt, true); + } else { + panic!("Expected SetPtt"); + } + } + + #[test] + fn test_client_command_to_rig_power_on() { + let cmd = ClientCommand::PowerOn; + if let RigCommand::PowerOn = client_command_to_rig(cmd) { + // Success + } else { + panic!("Expected PowerOn"); + } + } + + #[test] + fn test_client_command_to_rig_power_off() { + let cmd = ClientCommand::PowerOff; + if let RigCommand::PowerOff = client_command_to_rig(cmd) { + // Success + } else { + panic!("Expected PowerOff"); + } + } + + #[test] + fn test_client_command_to_rig_toggle_vfo() { + let cmd = ClientCommand::ToggleVfo; + if let RigCommand::ToggleVfo = client_command_to_rig(cmd) { + // Success + } else { + panic!("Expected ToggleVfo"); + } + } + + #[test] + fn test_client_command_to_rig_lock() { + let cmd = ClientCommand::Lock; + if let RigCommand::Lock = client_command_to_rig(cmd) { + // Success + } else { + panic!("Expected Lock"); + } + } + + #[test] + fn test_client_command_to_rig_unlock() { + let cmd = ClientCommand::Unlock; + if let RigCommand::Unlock = client_command_to_rig(cmd) { + // Success + } else { + panic!("Expected Unlock"); + } + } + + #[test] + fn test_client_command_to_rig_get_tx_limit() { + let cmd = ClientCommand::GetTxLimit; + if let RigCommand::GetTxLimit = client_command_to_rig(cmd) { + // Success + } else { + panic!("Expected GetTxLimit"); + } + } + + #[test] + fn test_client_command_to_rig_set_tx_limit() { + let cmd = ClientCommand::SetTxLimit { limit: 50 }; + if let RigCommand::SetTxLimit(limit) = client_command_to_rig(cmd) { + assert_eq!(limit, 50); + } else { + panic!("Expected SetTxLimit"); + } + } + + #[test] + fn test_client_command_to_rig_set_aprs_decode_enabled() { + let cmd = ClientCommand::SetAprsDecodeEnabled { enabled: true }; + if let RigCommand::SetAprsDecodeEnabled(enabled) = client_command_to_rig(cmd) { + assert_eq!(enabled, true); + } else { + panic!("Expected SetAprsDecodeEnabled"); + } + } + + #[test] + fn test_client_command_to_rig_set_cw_decode_enabled() { + let cmd = ClientCommand::SetCwDecodeEnabled { enabled: false }; + if let RigCommand::SetCwDecodeEnabled(enabled) = client_command_to_rig(cmd) { + assert_eq!(enabled, false); + } else { + panic!("Expected SetCwDecodeEnabled"); + } + } + + #[test] + fn test_client_command_to_rig_set_cw_auto() { + let cmd = ClientCommand::SetCwAuto { enabled: true }; + if let RigCommand::SetCwAuto(enabled) = client_command_to_rig(cmd) { + assert_eq!(enabled, true); + } else { + panic!("Expected SetCwAuto"); + } + } + + #[test] + fn test_client_command_to_rig_set_cw_wpm() { + let cmd = ClientCommand::SetCwWpm { wpm: 25 }; + if let RigCommand::SetCwWpm(wpm) = client_command_to_rig(cmd) { + assert_eq!(wpm, 25); + } else { + panic!("Expected SetCwWpm"); + } + } + + #[test] + fn test_client_command_to_rig_set_cw_tone_hz() { + let cmd = ClientCommand::SetCwToneHz { tone_hz: 800 }; + if let RigCommand::SetCwToneHz(tone_hz) = client_command_to_rig(cmd) { + assert_eq!(tone_hz, 800); + } else { + panic!("Expected SetCwToneHz"); + } + } + + #[test] + fn test_client_command_to_rig_set_ft8_decode_enabled() { + let cmd = ClientCommand::SetFt8DecodeEnabled { enabled: true }; + if let RigCommand::SetFt8DecodeEnabled(enabled) = client_command_to_rig(cmd) { + assert_eq!(enabled, true); + } else { + panic!("Expected SetFt8DecodeEnabled"); + } + } + + #[test] + fn test_client_command_to_rig_reset_aprs_decoder() { + let cmd = ClientCommand::ResetAprsDecoder; + if let RigCommand::ResetAprsDecoder = client_command_to_rig(cmd) { + // Success + } else { + panic!("Expected ResetAprsDecoder"); + } + } + + #[test] + fn test_client_command_to_rig_reset_cw_decoder() { + let cmd = ClientCommand::ResetCwDecoder; + if let RigCommand::ResetCwDecoder = client_command_to_rig(cmd) { + // Success + } else { + panic!("Expected ResetCwDecoder"); + } + } + + #[test] + fn test_client_command_to_rig_reset_ft8_decoder() { + let cmd = ClientCommand::ResetFt8Decoder; + if let RigCommand::ResetFt8Decoder = client_command_to_rig(cmd) { + // Success + } else { + panic!("Expected ResetFt8Decoder"); + } + } + + #[test] + fn test_rig_command_to_client_get_snapshot() { + let cmd = RigCommand::GetSnapshot; + if let ClientCommand::GetState = rig_command_to_client(cmd) { + // Success + } else { + panic!("Expected GetState"); + } + } + + #[test] + fn test_rig_command_to_client_set_freq() { + let cmd = RigCommand::SetFreq(Freq { hz: 14100000 }); + if let ClientCommand::SetFreq { freq_hz } = rig_command_to_client(cmd) { + assert_eq!(freq_hz, 14100000); + } else { + panic!("Expected SetFreq"); + } + } + + #[test] + fn test_rig_command_to_client_set_mode_lsb() { + let cmd = RigCommand::SetMode(RigMode::LSB); + if let ClientCommand::SetMode { mode } = rig_command_to_client(cmd) { + assert_eq!(mode, "LSB"); + } else { + panic!("Expected SetMode"); + } + } + + #[test] + fn test_rig_command_to_client_set_mode_other() { + let cmd = RigCommand::SetMode(RigMode::Other("CUSTOM".to_string())); + if let ClientCommand::SetMode { mode } = rig_command_to_client(cmd) { + assert_eq!(mode, "CUSTOM"); + } else { + panic!("Expected SetMode"); + } + } + + #[test] + fn test_rig_command_to_client_set_ptt() { + let cmd = RigCommand::SetPtt(true); + if let ClientCommand::SetPtt { ptt } = rig_command_to_client(cmd) { + assert_eq!(ptt, true); + } else { + panic!("Expected SetPtt"); + } + } + + #[test] + fn test_rig_command_to_client_power_on() { + let cmd = RigCommand::PowerOn; + if let ClientCommand::PowerOn = rig_command_to_client(cmd) { + // Success + } else { + panic!("Expected PowerOn"); + } + } + + #[test] + fn test_rig_command_to_client_power_off() { + let cmd = RigCommand::PowerOff; + if let ClientCommand::PowerOff = rig_command_to_client(cmd) { + // Success + } else { + panic!("Expected PowerOff"); + } + } + + #[test] + fn test_rig_command_to_client_toggle_vfo() { + let cmd = RigCommand::ToggleVfo; + if let ClientCommand::ToggleVfo = rig_command_to_client(cmd) { + // Success + } else { + panic!("Expected ToggleVfo"); + } + } + + #[test] + fn test_rig_command_to_client_lock() { + let cmd = RigCommand::Lock; + if let ClientCommand::Lock = rig_command_to_client(cmd) { + // Success + } else { + panic!("Expected Lock"); + } + } + + #[test] + fn test_rig_command_to_client_unlock() { + let cmd = RigCommand::Unlock; + if let ClientCommand::Unlock = rig_command_to_client(cmd) { + // Success + } else { + panic!("Expected Unlock"); + } + } + + #[test] + fn test_rig_command_to_client_get_tx_limit() { + let cmd = RigCommand::GetTxLimit; + if let ClientCommand::GetTxLimit = rig_command_to_client(cmd) { + // Success + } else { + panic!("Expected GetTxLimit"); + } + } + + #[test] + fn test_rig_command_to_client_set_tx_limit() { + let cmd = RigCommand::SetTxLimit(50); + if let ClientCommand::SetTxLimit { limit } = rig_command_to_client(cmd) { + assert_eq!(limit, 50); + } else { + panic!("Expected SetTxLimit"); + } + } + + #[test] + fn test_rig_command_to_client_set_aprs_decode_enabled() { + let cmd = RigCommand::SetAprsDecodeEnabled(true); + if let ClientCommand::SetAprsDecodeEnabled { enabled } = rig_command_to_client(cmd) { + assert_eq!(enabled, true); + } else { + panic!("Expected SetAprsDecodeEnabled"); + } + } + + #[test] + fn test_rig_command_to_client_set_cw_decode_enabled() { + let cmd = RigCommand::SetCwDecodeEnabled(false); + if let ClientCommand::SetCwDecodeEnabled { enabled } = rig_command_to_client(cmd) { + assert_eq!(enabled, false); + } else { + panic!("Expected SetCwDecodeEnabled"); + } + } + + #[test] + fn test_rig_command_to_client_set_cw_auto() { + let cmd = RigCommand::SetCwAuto(true); + if let ClientCommand::SetCwAuto { enabled } = rig_command_to_client(cmd) { + assert_eq!(enabled, true); + } else { + panic!("Expected SetCwAuto"); + } + } + + #[test] + fn test_rig_command_to_client_set_cw_wpm() { + let cmd = RigCommand::SetCwWpm(25); + if let ClientCommand::SetCwWpm { wpm } = rig_command_to_client(cmd) { + assert_eq!(wpm, 25); + } else { + panic!("Expected SetCwWpm"); + } + } + + #[test] + fn test_rig_command_to_client_set_cw_tone_hz() { + let cmd = RigCommand::SetCwToneHz(800); + if let ClientCommand::SetCwToneHz { tone_hz } = rig_command_to_client(cmd) { + assert_eq!(tone_hz, 800); + } else { + panic!("Expected SetCwToneHz"); + } + } + + #[test] + fn test_rig_command_to_client_set_ft8_decode_enabled() { + let cmd = RigCommand::SetFt8DecodeEnabled(true); + if let ClientCommand::SetFt8DecodeEnabled { enabled } = rig_command_to_client(cmd) { + assert_eq!(enabled, true); + } else { + panic!("Expected SetFt8DecodeEnabled"); + } + } + + #[test] + fn test_rig_command_to_client_reset_aprs_decoder() { + let cmd = RigCommand::ResetAprsDecoder; + if let ClientCommand::ResetAprsDecoder = rig_command_to_client(cmd) { + // Success + } else { + panic!("Expected ResetAprsDecoder"); + } + } + + #[test] + fn test_rig_command_to_client_reset_cw_decoder() { + let cmd = RigCommand::ResetCwDecoder; + if let ClientCommand::ResetCwDecoder = rig_command_to_client(cmd) { + // Success + } else { + panic!("Expected ResetCwDecoder"); + } + } + + #[test] + fn test_rig_command_to_client_reset_ft8_decoder() { + let cmd = RigCommand::ResetFt8Decoder; + if let ClientCommand::ResetFt8Decoder = rig_command_to_client(cmd) { + // Success + } else { + panic!("Expected ResetFt8Decoder"); + } + } + + #[test] + fn test_round_trip_set_freq() { + let original = ClientCommand::SetFreq { freq_hz: 7050000 }; + let rig_cmd = client_command_to_rig(original); + let client_cmd = rig_command_to_client(rig_cmd); + + if let ClientCommand::SetFreq { freq_hz } = client_cmd { + assert_eq!(freq_hz, 7050000); + } else { + panic!("Round trip failed"); + } + } + + #[test] + fn test_round_trip_set_mode_standard() { + let original = ClientCommand::SetMode { + mode: "USB".to_string(), + }; + let rig_cmd = client_command_to_rig(original); + let client_cmd = rig_command_to_client(rig_cmd); + + if let ClientCommand::SetMode { mode } = client_cmd { + assert_eq!(mode, "USB"); + } else { + panic!("Round trip failed"); + } + } + + #[test] + fn test_round_trip_set_ptt() { + let original = ClientCommand::SetPtt { ptt: false }; + let rig_cmd = client_command_to_rig(original); + let client_cmd = rig_command_to_client(rig_cmd); + + if let ClientCommand::SetPtt { ptt } = client_cmd { + assert_eq!(ptt, false); + } else { + panic!("Round trip failed"); + } + } +}