kenwood_thd75/protocol/
scan.rs

1//! Scan commands: SR, SF, BS.
2//!
3//! Provides parsing of responses for scan-related CAT protocol commands.
4//!
5//! Firmware-verified:
6//! - SR has no read form (bare `SR\r` returns `?`). Write-only.
7//! - SF = Step Size, band-indexed (`SF band\r` returns `SF band,step`).
8//! - BS is band-indexed (`BS band\r` returns `BS band`).
9
10use crate::error::ProtocolError;
11use crate::types::Band;
12use crate::types::mode::StepSize;
13
14use super::Response;
15
16/// Parse a scan command response from mnemonic and payload.
17///
18/// Returns `None` if the mnemonic is not a scan command.
19///
20/// Note: SR has no read form on the TH-D75 (bare `SR\r` returns `?`).
21/// When a write echo `SR value` is received, it is treated as a write
22/// acknowledgment (`Ok`).
23pub(crate) fn parse_scan(mnemonic: &str, payload: &str) -> Option<Result<Response, ProtocolError>> {
24    match mnemonic {
25        "SR" => Some(Ok(Response::Ok)),
26        "SF" => Some(parse_sf(payload)),
27        "BS" => Some(parse_bs(payload)),
28        _ => None,
29    }
30}
31
32// ---------------------------------------------------------------------------
33// Helpers
34// ---------------------------------------------------------------------------
35
36/// Parse a `u8` from a string field.
37fn parse_u8_field(s: &str, cmd: &str, field: &str) -> Result<u8, ProtocolError> {
38    s.parse::<u8>().map_err(|_| ProtocolError::FieldParse {
39        command: cmd.to_owned(),
40        field: field.to_owned(),
41        detail: format!("invalid u8: {s:?}"),
42    })
43}
44
45// ---------------------------------------------------------------------------
46// Individual parsers
47// ---------------------------------------------------------------------------
48
49/// Parse SF (step size): `band,step`.
50///
51/// Firmware-verified: SF = Step Size. `SF band\r` returns `SF band,step`.
52fn parse_sf(payload: &str) -> Result<Response, ProtocolError> {
53    let parts: Vec<&str> = payload.splitn(2, ',').collect();
54    if parts.len() != 2 {
55        return Err(ProtocolError::FieldParse {
56            command: "SF".to_owned(),
57            field: "all".to_owned(),
58            detail: format!("expected band,step, got {payload:?}"),
59        });
60    }
61    let band_val = parse_u8_field(parts[0], "SF", "band")?;
62    let band = Band::try_from(band_val).map_err(|e| ProtocolError::FieldParse {
63        command: "SF".to_owned(),
64        field: "band".to_owned(),
65        detail: e.to_string(),
66    })?;
67    // Step value uses hex (TABLE C: A=50kHz, B=100kHz, confirmed by ARFC RE)
68    let step_val = u8::from_str_radix(parts[1], 16).map_err(|_| ProtocolError::FieldParse {
69        command: "SF".to_owned(),
70        field: "step".to_owned(),
71        detail: format!("invalid hex step: {:?}", parts[1]),
72    })?;
73    let step = StepSize::try_from(step_val).map_err(|e| ProtocolError::FieldParse {
74        command: "SF".to_owned(),
75        field: "step".to_owned(),
76        detail: e.to_string(),
77    })?;
78    Ok(Response::StepSize { band, step })
79}
80
81/// Parse BS (band scope): just a band number echoed back.
82fn parse_bs(payload: &str) -> Result<Response, ProtocolError> {
83    let band_val = parse_u8_field(payload.trim(), "BS", "band")?;
84    let band = Band::try_from(band_val).map_err(|e| ProtocolError::FieldParse {
85        command: "BS".to_owned(),
86        field: "band".to_owned(),
87        detail: e.to_string(),
88    })?;
89    Ok(Response::BandScope { band })
90}