kenwood_thd75/protocol/
tone.rs

1//! TNC, D-STAR callsign, and real-time clock commands: TN, DC, RT.
2//!
3//! Hardware-verified command behavior:
4//! - TN: TNC mode (bare read, returns `mode,setting`)
5//! - DC: D-STAR callsign slots 1-6 (slot-indexed, returns `slot,callsign,suffix`)
6//! - RT: Real-time clock (bare read, returns `YYMMDDHHmmss`)
7//!
8//! The D75 firmware RE misidentified these as tone-related commands.
9//! Hardware testing confirmed the actual semantics documented here.
10
11use crate::error::ProtocolError;
12use crate::types::DstarSlot;
13use crate::types::radio_params::{TncBaud, TncMode};
14
15use super::Response;
16
17/// Parse a TN/DC/RT command response from mnemonic and payload.
18///
19/// Returns `None` if the mnemonic is not one of TN, DC, RT.
20pub(crate) fn parse_tone(mnemonic: &str, payload: &str) -> Option<Result<Response, ProtocolError>> {
21    match mnemonic {
22        "TN" => Some(parse_tn(payload)),
23        "DC" => Some(parse_dc(payload)),
24        "RT" => Some(parse_rt(payload)),
25        _ => None,
26    }
27}
28
29// ---------------------------------------------------------------------------
30// Helpers
31// ---------------------------------------------------------------------------
32
33/// Parse a `u8` from a string field.
34fn parse_u8_field(s: &str, cmd: &str, field: &str) -> Result<u8, ProtocolError> {
35    s.parse::<u8>().map_err(|_| ProtocolError::FieldParse {
36        command: cmd.to_owned(),
37        field: field.to_owned(),
38        detail: format!("invalid u8: {s:?}"),
39    })
40}
41
42// ---------------------------------------------------------------------------
43// Individual parsers
44// ---------------------------------------------------------------------------
45
46/// Parse TN (TNC mode): `"mode,setting"` format.
47///
48/// Hardware-verified: bare `TN\r` returns `TN mode,setting` (e.g., `TN 0,0`).
49fn parse_tn(payload: &str) -> Result<Response, ProtocolError> {
50    let parts: Vec<&str> = payload.splitn(2, ',').collect();
51    if parts.len() != 2 {
52        return Err(ProtocolError::FieldParse {
53            command: "TN".to_owned(),
54            field: "all".to_owned(),
55            detail: format!("expected mode,setting, got {payload:?}"),
56        });
57    }
58    let mode_raw = parse_u8_field(parts[0], "TN", "mode")?;
59    let mode = TncMode::try_from(mode_raw).map_err(|e| ProtocolError::FieldParse {
60        command: "TN".to_owned(),
61        field: "mode".to_owned(),
62        detail: e.to_string(),
63    })?;
64    let setting_raw = parse_u8_field(parts[1], "TN", "setting")?;
65    let setting = TncBaud::try_from(setting_raw).map_err(|e| ProtocolError::FieldParse {
66        command: "TN".to_owned(),
67        field: "setting".to_owned(),
68        detail: e.to_string(),
69    })?;
70    Ok(Response::TncMode { mode, setting })
71}
72
73/// Parse DC (D-STAR callsign): `"slot,callsign,suffix"` format.
74///
75/// Hardware-verified: `DC slot\r` returns `DC slot,callsign,suffix`.
76/// Example: `DC 1,KQ4NIT  ,D75A`.
77fn parse_dc(payload: &str) -> Result<Response, ProtocolError> {
78    let parts: Vec<&str> = payload.splitn(3, ',').collect();
79    if parts.len() != 3 {
80        return Err(ProtocolError::FieldParse {
81            command: "DC".to_owned(),
82            field: "all".to_owned(),
83            detail: format!("expected slot,callsign,suffix, got {payload:?}"),
84        });
85    }
86    let raw_slot = parse_u8_field(parts[0], "DC", "slot")?;
87    let slot = DstarSlot::new(raw_slot).map_err(|e| ProtocolError::FieldParse {
88        command: "DC".into(),
89        field: "slot".into(),
90        detail: e.to_string(),
91    })?;
92    let callsign = parts[1].to_owned();
93    let suffix = parts[2].to_owned();
94    Ok(Response::DstarCallsign {
95        slot,
96        callsign,
97        suffix,
98    })
99}
100
101/// Parse RT (real-time clock): bare datetime string.
102///
103/// Hardware-verified: bare `RT\r` returns `RT YYMMDDHHmmss`.
104/// Example: `RT 240104095700`.
105fn parse_rt(payload: &str) -> Result<Response, ProtocolError> {
106    if payload.is_empty() {
107        return Err(ProtocolError::FieldParse {
108            command: "RT".to_owned(),
109            field: "datetime".to_owned(),
110            detail: "empty datetime payload".to_owned(),
111        });
112    }
113    Ok(Response::RealTimeClock {
114        datetime: payload.to_owned(),
115    })
116}