kenwood_thd75/protocol/
gps.rs

1//! GPS commands: GP, GM, GS.
2//!
3//! Provides parsing of responses for the 3 GPS-related CAT protocol
4//! commands:
5//! - GP: GPS configuration (enabled + PC output)
6//! - GM: GPS/Radio mode (single value)
7//! - GS: GPS NMEA sentence enable flags (6 booleans)
8
9use crate::error::ProtocolError;
10use crate::types::GpsRadioMode;
11
12use super::Response;
13
14/// Parse a GPS command response from mnemonic and payload.
15///
16/// Returns `None` if the mnemonic is not a GPS command.
17pub(crate) fn parse_gps(mnemonic: &str, payload: &str) -> Option<Result<Response, ProtocolError>> {
18    match mnemonic {
19        "GP" => Some(parse_gp(payload)),
20        "GM" => Some(parse_gm(payload)),
21        "GS" => Some(parse_gs(payload)),
22        _ => None,
23    }
24}
25
26// ---------------------------------------------------------------------------
27// Helpers
28// ---------------------------------------------------------------------------
29
30/// Parse a `u8` from a string field.
31fn parse_u8_field(s: &str, cmd: &str, field: &str) -> Result<u8, ProtocolError> {
32    s.parse::<u8>().map_err(|_| ProtocolError::FieldParse {
33        command: cmd.to_owned(),
34        field: field.to_owned(),
35        detail: format!("invalid u8: {s:?}"),
36    })
37}
38
39// ---------------------------------------------------------------------------
40// Individual parsers
41// ---------------------------------------------------------------------------
42
43/// Parse GP (GPS config): `gps_enabled,pc_output`.
44///
45/// Two comma-separated values, each 0 or 1.
46fn parse_gp(payload: &str) -> Result<Response, ProtocolError> {
47    let parts: Vec<&str> = payload.split(',').collect();
48    if parts.len() != 2 {
49        return Err(ProtocolError::FieldParse {
50            command: "GP".to_owned(),
51            field: "all".to_owned(),
52            detail: format!("expected 2 fields (gps_enabled,pc_output), got {payload:?}"),
53        });
54    }
55    let gps_val = parse_u8_field(parts[0], "GP", "gps_enabled")?;
56    let pc_val = parse_u8_field(parts[1], "GP", "pc_output")?;
57    Ok(Response::GpsConfig {
58        gps_enabled: gps_val != 0,
59        pc_output: pc_val != 0,
60    })
61}
62
63/// Parse GM (GPS mode): single value (0=Normal, 1=GPS Receiver).
64///
65/// Firmware-verified: `cat_gm_handler` guard `local_18 < 2`.
66fn parse_gm(payload: &str) -> Result<Response, ProtocolError> {
67    let raw = parse_u8_field(payload, "GM", "mode")?;
68    let mode = GpsRadioMode::try_from(raw).map_err(|e| ProtocolError::FieldParse {
69        command: "GM".into(),
70        field: "mode".into(),
71        detail: e.to_string(),
72    })?;
73    Ok(Response::GpsMode { mode })
74}
75
76/// Parse GS (GPS NMEA sentences): `gga,gll,gsa,gsv,rmc,vtg`.
77///
78/// Six comma-separated values, each 0 or 1.
79#[allow(clippy::similar_names)]
80fn parse_gs(payload: &str) -> Result<Response, ProtocolError> {
81    let parts: Vec<&str> = payload.split(',').collect();
82    if parts.len() != 6 {
83        return Err(ProtocolError::FieldParse {
84            command: "GS".to_owned(),
85            field: "all".to_owned(),
86            detail: format!("expected 6 fields, got {}", parts.len()),
87        });
88    }
89    let gga = parse_u8_field(parts[0], "GS", "gga")? != 0;
90    let gll = parse_u8_field(parts[1], "GS", "gll")? != 0;
91    let gsa = parse_u8_field(parts[2], "GS", "gsa")? != 0;
92    let gsv = parse_u8_field(parts[3], "GS", "gsv")? != 0;
93    let rmc = parse_u8_field(parts[4], "GS", "rmc")? != 0;
94    let vtg = parse_u8_field(parts[5], "GS", "vtg")? != 0;
95    Ok(Response::GpsSentences {
96        gga,
97        gll,
98        gsa,
99        gsv,
100        rmc,
101        vtg,
102    })
103}