kenwood_thd75/protocol/
vfo.rs

1//! VFO (Variable Frequency Oscillator) commands: AG, SQ, SM, MD, FS, FT, SH, UP, RA.
2//!
3//! These commands control per-band settings including AF (Audio Frequency)
4//! gain, squelch level, S-meter reading, operating mode, fine step,
5//! filter width, and attenuator.
6
7use crate::error::ProtocolError;
8use crate::types::Band;
9use crate::types::channel::FineStep;
10use crate::types::mode::Mode;
11use crate::types::radio_params::{
12    AfGainLevel, FilterMode, FilterWidthIndex, SMeterReading, SquelchLevel,
13};
14
15use super::Response;
16
17/// Parse a VFO command response from mnemonic and payload.
18///
19/// Returns `None` if the mnemonic is not a VFO command.
20pub(crate) fn parse_vfo(mnemonic: &str, payload: &str) -> Option<Result<Response, ProtocolError>> {
21    match mnemonic {
22        "AG" => Some(parse_ag(payload)),
23        "SQ" => Some(parse_sq(payload)),
24        "SM" => Some(parse_sm(payload)),
25        "MD" => Some(parse_md(payload)),
26        "FS" => Some(parse_fs(payload)),
27        "FT" => Some(parse_ft(payload)),
28        "SH" => Some(parse_sh(payload)),
29        "UP" => Some(Ok(Response::Ok)),
30        "RA" => Some(parse_ra(payload)),
31        _ => None,
32    }
33}
34
35// ---------------------------------------------------------------------------
36// Helpers
37// ---------------------------------------------------------------------------
38
39/// Parse a `u8` from a string field.
40fn parse_u8_field(s: &str, cmd: &str, field: &str) -> Result<u8, ProtocolError> {
41    s.parse::<u8>().map_err(|_| ProtocolError::FieldParse {
42        command: cmd.to_owned(),
43        field: field.to_owned(),
44        detail: format!("invalid u8: {s:?}"),
45    })
46}
47
48/// Split a `"band,value"` payload into (band, `value_str`).
49fn split_band_value<'a>(payload: &'a str, cmd: &str) -> Result<(Band, &'a str), ProtocolError> {
50    let parts: Vec<&str> = payload.splitn(2, ',').collect();
51    if parts.len() != 2 {
52        return Err(ProtocolError::FieldParse {
53            command: cmd.to_owned(),
54            field: "all".to_owned(),
55            detail: format!("expected band,value, got {payload:?}"),
56        });
57    }
58    let band_val = parse_u8_field(parts[0], cmd, "band")?;
59    let band = Band::try_from(band_val).map_err(|e| ProtocolError::FieldParse {
60        command: cmd.to_owned(),
61        field: "band".to_owned(),
62        detail: e.to_string(),
63    })?;
64    Ok((band, parts[1]))
65}
66
67// ---------------------------------------------------------------------------
68// Individual parsers
69// ---------------------------------------------------------------------------
70
71/// Parse AG (AF gain): bare `"level"` format (no band).
72///
73/// Hardware observation: bare `AG\r` returns a global gain level (e.g., `091`).
74/// Band-indexed `AG 0\r` returns `?`.
75fn parse_ag(payload: &str) -> Result<Response, ProtocolError> {
76    let raw = parse_u8_field(payload.trim(), "AG", "level")?;
77    let level = AfGainLevel::from(raw);
78    Ok(Response::AfGain { level })
79}
80
81/// Parse SQ (squelch): "band,ll" (zero-padded 2 digits).
82fn parse_sq(payload: &str) -> Result<Response, ProtocolError> {
83    let (band, val_str) = split_band_value(payload, "SQ")?;
84    let raw = parse_u8_field(val_str, "SQ", "level")?;
85    let level = SquelchLevel::try_from(raw).map_err(|e| ProtocolError::FieldParse {
86        command: "SQ".to_owned(),
87        field: "level".to_owned(),
88        detail: e.to_string(),
89    })?;
90    Ok(Response::Squelch { band, level })
91}
92
93/// Parse SM (S-meter): "band,level" (hardware may return 1-4 digits).
94fn parse_sm(payload: &str) -> Result<Response, ProtocolError> {
95    let (band, val_str) = split_band_value(payload, "SM")?;
96    let raw = parse_u8_field(val_str, "SM", "level")?;
97    let level = SMeterReading::try_from(raw).map_err(|e| ProtocolError::FieldParse {
98        command: "SM".to_owned(),
99        field: "level".to_owned(),
100        detail: e.to_string(),
101    })?;
102    Ok(Response::Smeter { band, level })
103}
104
105/// Parse MD (mode): "band,mode".
106fn parse_md(payload: &str) -> Result<Response, ProtocolError> {
107    let (band, val_str) = split_band_value(payload, "MD")?;
108    let mode_val = parse_u8_field(val_str, "MD", "mode")?;
109    let mode = Mode::try_from(mode_val).map_err(|e| ProtocolError::FieldParse {
110        command: "MD".to_owned(),
111        field: "mode".to_owned(),
112        detail: e.to_string(),
113    })?;
114    Ok(Response::Mode { band, mode })
115}
116
117/// Parse FS (fine step): bare `"value"` format (no band).
118///
119/// Firmware-verified: bare `FS\r` returns `FS value` (single value, no comma).
120/// Value is a fine step index 0-3.
121fn parse_fs(payload: &str) -> Result<Response, ProtocolError> {
122    let step_val = parse_u8_field(payload.trim(), "FS", "step")?;
123    let step = FineStep::try_from(step_val).map_err(|e| ProtocolError::FieldParse {
124        command: "FS".to_owned(),
125        field: "step".to_owned(),
126        detail: e.to_string(),
127    })?;
128    Ok(Response::FineStep { step })
129}
130
131/// Parse FT (function type): bare data (no band).
132///
133/// Response to `FT\r` is a data value, possibly prefixed by band
134/// in "band,data" format for backward compatibility.
135fn parse_ft(payload: &str) -> Result<Response, ProtocolError> {
136    // Handle both bare "N" and "band,N" formats
137    let data_str = if let Some((_prefix, val)) = payload.split_once(',') {
138        val
139    } else {
140        payload
141    };
142    let value = parse_u8_field(data_str, "FT", "value")?;
143    Ok(Response::FunctionType {
144        enabled: value != 0,
145    })
146}
147
148/// Parse SH (filter width): `mode_index,width`.
149///
150/// The response to `SH N\r` includes the mode index and filter width.
151fn parse_sh(payload: &str) -> Result<Response, ProtocolError> {
152    let parts: Vec<&str> = payload.splitn(2, ',').collect();
153    if parts.len() == 2 {
154        let mode_raw = parse_u8_field(parts[0], "SH", "mode")?;
155        let mode = FilterMode::try_from(mode_raw).map_err(|e| ProtocolError::FieldParse {
156            command: "SH".to_owned(),
157            field: "mode".to_owned(),
158            detail: e.to_string(),
159        })?;
160        let width_raw = parse_u8_field(parts[1], "SH", "width")?;
161        let width =
162            FilterWidthIndex::from_raw(width_raw).map_err(|e| ProtocolError::FieldParse {
163                command: "SH".into(),
164                field: "width".into(),
165                detail: e.to_string(),
166            })?;
167        Ok(Response::FilterWidth { mode, width })
168    } else {
169        // Bare response - treat payload as width with mode SSB
170        let width_raw = parse_u8_field(payload, "SH", "width")?;
171        let width =
172            FilterWidthIndex::from_raw(width_raw).map_err(|e| ProtocolError::FieldParse {
173                command: "SH".into(),
174                field: "width".into(),
175                detail: e.to_string(),
176            })?;
177        Ok(Response::FilterWidth {
178            mode: FilterMode::Ssb,
179            width,
180        })
181    }
182}
183
184/// Parse RA (attenuator): "band,enabled".
185fn parse_ra(payload: &str) -> Result<Response, ProtocolError> {
186    let (band, val_str) = split_band_value(payload, "RA")?;
187    let val = parse_u8_field(val_str, "RA", "enabled")?;
188    Ok(Response::Attenuator {
189        band,
190        enabled: val != 0,
191    })
192}