diff --git a/src/trx-protocol/src/codec.rs b/src/trx-protocol/src/codec.rs index 71d86cb..79a7b17 100644 --- a/src/trx-protocol/src/codec.rs +++ b/src/trx-protocol/src/codec.rs @@ -56,7 +56,11 @@ pub fn parse_envelope(input: &str) -> Result Ok(envelope) => Ok(envelope), Err(_) => { let cmd = serde_json::from_str::(input)?; - Ok(ClientEnvelope { token: None, cmd }) + Ok(ClientEnvelope { + token: None, + rig_id: None, + cmd, + }) } } } @@ -196,4 +200,83 @@ mod tests { let envelope = parse_envelope(json).unwrap(); assert_eq!(envelope.token, Some("Bearer abc123xyz".to_string())); } + + // --- MR-09: multi-rig protocol tests --- + + #[test] + fn test_parse_envelope_absent_rig_id_defaults_to_none() { + let json = r#"{"cmd":"get_state"}"#; + let envelope = parse_envelope(json).unwrap(); + assert_eq!(envelope.rig_id, None, "absent rig_id should parse as None"); + } + + #[test] + fn test_parse_envelope_with_rig_id() { + let json = r#"{"rig_id":"hf","cmd":"get_state"}"#; + let envelope = parse_envelope(json).unwrap(); + assert_eq!(envelope.rig_id, Some("hf".to_string())); + assert!(matches!(envelope.cmd, ClientCommand::GetState)); + } + + #[test] + fn test_parse_envelope_get_rigs_command() { + let json = r#"{"cmd":"get_rigs"}"#; + let envelope = parse_envelope(json).unwrap(); + assert!(matches!(envelope.cmd, ClientCommand::GetRigs)); + assert_eq!(envelope.rig_id, None); + } + + #[test] + fn test_parse_envelope_get_rigs_with_rig_id_ignored() { + // rig_id is parsed and available even though GetRigs is intercepted + // before routing — the listener should ignore it for this command. + let json = r#"{"rig_id":"sdr","cmd":"get_rigs"}"#; + let envelope = parse_envelope(json).unwrap(); + assert!(matches!(envelope.cmd, ClientCommand::GetRigs)); + assert_eq!(envelope.rig_id, Some("sdr".to_string())); + } + + #[test] + fn test_client_response_rig_id_roundtrip() { + use crate::types::ClientResponse; + let resp = ClientResponse { + success: true, + rig_id: Some("hf".to_string()), + state: None, + rigs: None, + error: None, + }; + let json = serde_json::to_string(&resp).unwrap(); + assert!(json.contains(r#""rig_id":"hf""#)); + let decoded: ClientResponse = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.rig_id, Some("hf".to_string())); + } + + #[test] + fn test_client_response_omits_rig_id_when_none() { + use crate::types::ClientResponse; + let resp = ClientResponse { + success: false, + rig_id: None, + state: None, + rigs: None, + error: Some("bad".to_string()), + }; + let json = serde_json::to_string(&resp).unwrap(); + assert!(!json.contains("rig_id"), "rig_id=None should be omitted from JSON"); + } + + #[test] + fn test_client_response_omits_rigs_when_none() { + use crate::types::ClientResponse; + let resp = ClientResponse { + success: true, + rig_id: Some("server".to_string()), + state: None, + rigs: None, + error: None, + }; + let json = serde_json::to_string(&resp).unwrap(); + assert!(!json.contains("\"rigs\""), "rigs=None should be omitted from JSON"); + } } diff --git a/src/trx-protocol/src/lib.rs b/src/trx-protocol/src/lib.rs index a165e4a..426cb43 100644 --- a/src/trx-protocol/src/lib.rs +++ b/src/trx-protocol/src/lib.rs @@ -16,4 +16,4 @@ pub mod types; 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}; -pub use types::{ClientCommand, ClientEnvelope, ClientResponse}; +pub use types::{ClientCommand, ClientEnvelope, ClientResponse, RigEntry}; diff --git a/src/trx-protocol/src/mapping.rs b/src/trx-protocol/src/mapping.rs index 9dd110a..785c08d 100644 --- a/src/trx-protocol/src/mapping.rs +++ b/src/trx-protocol/src/mapping.rs @@ -16,6 +16,9 @@ use crate::types::ClientCommand; /// mode strings into RigMode values. pub fn client_command_to_rig(cmd: ClientCommand) -> RigCommand { match cmd { + ClientCommand::GetRigs => { + unreachable!("GetRigs is handled in the listener before reaching rig_task") + } ClientCommand::GetState => RigCommand::GetSnapshot, ClientCommand::SetFreq { freq_hz } => RigCommand::SetFreq(Freq { hz: freq_hz }), ClientCommand::SetMode { mode } => RigCommand::SetMode(parse_mode(&mode)), diff --git a/src/trx-protocol/src/types.rs b/src/trx-protocol/src/types.rs index 922201c..aab525f 100644 --- a/src/trx-protocol/src/types.rs +++ b/src/trx-protocol/src/types.rs @@ -13,6 +13,7 @@ use trx_core::rig::state::RigSnapshot; #[serde(tag = "cmd", rename_all = "snake_case")] pub enum ClientCommand { GetState, + GetRigs, SetFreq { freq_hz: u64 }, SetMode { mode: String }, SetPtt { ptt: bool }, @@ -36,18 +37,34 @@ pub enum ClientCommand { ResetWsprDecoder, } -/// Envelope for client commands with optional authentication token. +/// Envelope for client commands with optional authentication token and rig routing. #[derive(Debug, Serialize, Deserialize)] pub struct ClientEnvelope { pub token: Option, + /// Target rig ID. When absent, the first/default rig is used (backward compat). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rig_id: Option, #[serde(flatten)] pub cmd: ClientCommand, } +/// One entry in the GetRigs response: a rig's ID and its current snapshot. +#[derive(Debug, Serialize, Deserialize)] +pub struct RigEntry { + pub rig_id: String, + pub state: RigSnapshot, +} + /// Response sent to network clients over TCP. #[derive(Debug, Serialize, Deserialize)] pub struct ClientResponse { pub success: bool, + /// The rig this response pertains to. Set by the listener from MR-06 onward. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rig_id: Option, pub state: Option, + /// Populated only for GetRigs responses. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rigs: Option>, pub error: Option, }