kenwood_thd75/radio/
gps.rs

1//! GPS subsystem methods.
2//!
3//! The TH-D75 has a built-in GPS receiver that provides position data for APRS beaconing,
4//! waypoint navigation, and time synchronization. The GPS integrates directly with the APRS
5//! TNC — when APRS beaconing is enabled and the GPS has a fix, position reports are
6//! automatically included in transmitted beacons.
7//!
8//! The `pc_output` flag in the GPS configuration controls whether raw NMEA sentences are
9//! forwarded over the serial (USB/BT) connection. This is useful for feeding GPS data to
10//! mapping software, but **competes with CAT command I/O** on the same serial channel.
11//!
12//! # Related commands
13//!
14//! - **GP**: GPS enable and PC output configuration
15//! - **GS**: NMEA sentence selection (which sentence types to output)
16//! - **GM**: GPS/Radio mode (bare read only — `GM 1` reboots the radio into GPS-only mode)
17
18use crate::error::{Error, ProtocolError};
19use crate::protocol::{Command, Response};
20use crate::transport::Transport;
21use crate::types::GpsRadioMode;
22
23use super::Radio;
24
25impl<T: Transport> Radio<T> {
26    /// Get GPS configuration (GP read).
27    ///
28    /// Returns `(gps_enabled, pc_output)`.
29    ///
30    /// # Errors
31    ///
32    /// Returns an error if the command fails or the response is unexpected.
33    pub async fn get_gps_config(&mut self) -> Result<(bool, bool), Error> {
34        tracing::debug!("reading GPS config");
35        let response = self.execute(Command::GetGpsConfig).await?;
36        match response {
37            Response::GpsConfig {
38                gps_enabled,
39                pc_output,
40            } => Ok((gps_enabled, pc_output)),
41            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
42                expected: "GpsConfig".into(),
43                actual: format!("{other:?}").into_bytes(),
44            })),
45        }
46    }
47
48    /// Get GPS NMEA sentence enable flags (GS read).
49    ///
50    /// Returns `(gga, gll, gsa, gsv, rmc, vtg)` — six booleans indicating which NMEA 0183
51    /// sentence types are enabled for output when `pc_output` is active.
52    ///
53    /// # Sentence types
54    ///
55    /// - **GGA** (Global Positioning System Fix Data): time, position, fix quality, number of
56    ///   satellites, HDOP, altitude. The primary fix sentence.
57    /// - **GLL** (Geographic Position - Latitude/Longitude): position and time, simpler than GGA.
58    /// - **GSA** (GNSS DOP and Active Satellites): fix type (2D/3D), satellite IDs in use,
59    ///   PDOP/HDOP/VDOP dilution of precision values.
60    /// - **GSV** (GNSS Satellites in View): satellite count, PRN numbers, elevation, azimuth,
61    ///   and SNR for each satellite. Multiple sentences for all visible satellites.
62    /// - **RMC** (Recommended Minimum Navigation Information): time, position, speed over
63    ///   ground, course, date, magnetic variation. The most commonly used sentence.
64    /// - **VTG** (Course Over Ground and Ground Speed): track (true and magnetic) and speed
65    ///   (knots and km/h).
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the command fails or the response is unexpected.
70    pub async fn get_gps_sentences(
71        &mut self,
72    ) -> Result<(bool, bool, bool, bool, bool, bool), Error> {
73        tracing::debug!("reading GPS NMEA sentence flags");
74        let response = self.execute(Command::GetGpsSentences).await?;
75        match response {
76            Response::GpsSentences {
77                gga,
78                gll,
79                gsa,
80                gsv,
81                rmc,
82                vtg,
83            } => Ok((gga, gll, gsa, gsv, rmc, vtg)),
84            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
85                expected: "GpsSentences".into(),
86                actual: format!("{other:?}").into_bytes(),
87            })),
88        }
89    }
90
91    /// Set GPS configuration (GP write).
92    ///
93    /// - `gps_enabled`: turns the GPS receiver on or off. When off, no position fix is
94    ///   available for APRS beaconing or display.
95    /// - `pc_output`: when `true`, the radio outputs raw NMEA sentences over the serial
96    ///   connection (USB or Bluetooth SPP). **This competes with CAT command I/O** — NMEA
97    ///   data will be interleaved with CAT responses on the same serial channel, which can
98    ///   confuse the protocol parser. Only enable this if you are prepared to handle mixed
99    ///   NMEA/CAT traffic, or if you are using the serial port exclusively for GPS data.
100    ///
101    /// # Errors
102    ///
103    /// Returns an error if the command fails or the response is unexpected.
104    pub async fn set_gps_config(
105        &mut self,
106        gps_enabled: bool,
107        pc_output: bool,
108    ) -> Result<(), Error> {
109        tracing::info!(gps_enabled, pc_output, "setting GPS config");
110        let response = self
111            .execute(Command::SetGpsConfig {
112                gps_enabled,
113                pc_output,
114            })
115            .await?;
116        match response {
117            Response::GpsConfig { .. } => Ok(()),
118            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
119                expected: "GpsConfig".into(),
120                actual: format!("{other:?}").into_bytes(),
121            })),
122        }
123    }
124
125    /// Set GPS NMEA sentence enable flags (GS write).
126    ///
127    /// Sets 6 boolean flags controlling which NMEA sentences are output:
128    /// GGA, GLL, GSA, GSV, RMC, VTG.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the command fails or the response is unexpected.
133    #[allow(clippy::fn_params_excessive_bools, clippy::similar_names)]
134    pub async fn set_gps_sentences(
135        &mut self,
136        gga: bool,
137        gll: bool,
138        gsa: bool,
139        gsv: bool,
140        rmc: bool,
141        vtg: bool,
142    ) -> Result<(), Error> {
143        tracing::info!(gga, gll, gsa, gsv, rmc, vtg, "setting GPS NMEA sentences");
144        let response = self
145            .execute(Command::SetGpsSentences {
146                gga,
147                gll,
148                gsa,
149                gsv,
150                rmc,
151                vtg,
152            })
153            .await?;
154        match response {
155            Response::GpsSentences { .. } => Ok(()),
156            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
157                expected: "GpsSentences".into(),
158                actual: format!("{other:?}").into_bytes(),
159            })),
160        }
161    }
162
163    /// Get GPS/Radio mode status (GM bare read).
164    ///
165    /// Returns the current GPS/Radio operating mode. `Normal` (0) means
166    /// standard transceiver operation. `GpsReceiver` (1) means GPS-only mode.
167    ///
168    /// # Warning
169    /// Only the bare `GM\r` read is safe. Sending `GM 1\r` would reboot
170    /// the radio into GPS-only mode. This method only sends the bare read.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the command fails or the response is unexpected.
175    pub async fn get_gps_mode(&mut self) -> Result<GpsRadioMode, Error> {
176        tracing::debug!("reading GPS/Radio mode");
177        let response = self.execute(Command::GetGpsMode).await?;
178        match response {
179            Response::GpsMode { mode } => Ok(mode),
180            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
181                expected: "GpsMode".into(),
182                actual: format!("{other:?}").into_bytes(),
183            })),
184        }
185    }
186}