kenwood_thd75/types/
gps.rs

1//! GPS (Global Positioning System) configuration and data types.
2//!
3//! The TH-D75 has a built-in GPS receiver that provides position data in
4//! NMEA (National Marine Electronics Association) format. GPS data is used
5//! for APRS position beaconing, D-STAR position reporting, waypoint
6//! navigation, track logging, and manual position storage.
7//!
8//! These types model every GPS setting accessible through the TH-D75's
9//! menu system (Chapter 13 of the user manual) and CAT commands (GP, GM, GS).
10
11// ---------------------------------------------------------------------------
12// Top-level GPS configuration
13// ---------------------------------------------------------------------------
14
15/// Complete GPS configuration for the TH-D75.
16///
17/// Covers all settings from the radio's GPS menu tree, including
18/// receiver control, output format, track logging, and position memory.
19/// Derived from the capability gap analysis features 95-109.
20///
21/// Per User Manual Chapter 13 and Chapter 28:
22///
23/// # GPS specifications
24///
25/// - TTFF (cold start): approximately 40 seconds
26/// - TTFF (hot start): approximately 5 seconds
27/// - Horizontal accuracy: 10 m or less
28/// - Receive sensitivity: approximately -141 dBm (acquisition)
29/// - GPS logger mode current consumption: 125 mA
30///
31/// # GPS Receiver mode (per User Manual Chapter 13, Menu No. 403)
32///
33/// GPS Receiver mode turns off the transceiver function entirely to
34/// prolong battery life during GPS track logging. Only GPS information
35/// is displayed. The FM broadcast radio still works in this mode.
36/// Limited key operations are available: `[MENU]`, `[MARK]`, `[F]`
37/// (toggle North Up / Heading Up), and navigation between GPS screens.
38///
39/// # My Position (per User Manual Chapter 13, Menu No. 401)
40///
41/// 5 position memory channels store latitude, longitude, and a name
42/// (up to 8 characters) for locations from which you frequently
43/// transmit APRS packets. Select `GPS` to use the live GPS position
44/// or `My Position 1-5` for a fixed stored position.
45///
46/// # Position Ambiguity (per User Manual Chapter 13, Menu No. 402)
47///
48/// Controls how many trailing digits of position data are omitted
49/// from APRS packets: Off (full precision), 1-Digit, 2-Digit,
50/// 3-Digit, or 4-Digit.
51///
52/// # Position Memory (per User Manual Chapter 13)
53///
54/// The radio provides 100 position memory slots, each storing
55/// latitude, longitude, altitude, timestamp, name (up to 8 characters),
56/// and APRS icon. Memories can be sorted by name or date/time, used
57/// as target points for navigation, or cleared individually or all
58/// at once.
59///
60/// # GPS Battery Saver (per User Manual Chapter 13, Menu No. 404)
61///
62/// Options: Off / 1 / 2 / 4 / 8 minutes / Auto. The Auto setting
63/// progressively increases the GPS off-time: 1 min -> 2 min -> 4 min
64/// -> 8 min (stays at 8 min). If position is later acquired then lost,
65/// the cycle restarts at 1 minute.
66///
67/// # GPS PC Output (per User Manual Chapter 13, Menu No. 405)
68///
69/// When enabled, NMEA data is output via USB or Bluetooth (selected
70/// by Menu No. 981). Configurable sentences (Menu No. 406): `$GPGGA`,
71/// `$GPGLL`, `$GPGSA`, `$GPGSV`, `$GPRMC`, `$GPVTG`. At least one
72/// sentence must remain selected.
73///
74/// # Track Log (per User Manual Chapter 13, Menu No. 410-414)
75///
76/// Records movement to a microSD memory card. Record methods: Time
77/// (interval 2-1800 seconds), Distance (0.01-9.99 miles/km/nm), or
78/// Beacon (synced with APRS beacon transmissions). Track log files
79/// are named by start date/time (e.g., `05122024_124705.nme`).
80/// Track logging pauses when the microSD card is full.
81#[derive(Debug, Clone, PartialEq)]
82pub struct GpsConfig {
83    /// Built-in GPS receiver on/off.
84    pub enabled: bool,
85    /// GPS PC output mode (send NMEA data to the serial port).
86    pub pc_output: bool,
87    /// GPS operating mode.
88    pub operating_mode: GpsOperatingMode,
89    /// GPS battery saver (reduce GPS power consumption by cycling
90    /// the receiver on and off).
91    pub battery_saver: bool,
92    /// NMEA sentence output selection (which sentences to include in
93    /// PC output).
94    pub sentence_output: NmeaSentences,
95    /// Track log recording configuration.
96    pub track_log: TrackLogConfig,
97    /// Manual position memory slots (5 available: "My Position 1"
98    /// through "My Position 5").
99    pub my_positions: [PositionMemory; 5],
100    /// Position ambiguity level (shared with APRS, but configured
101    /// in GPS menu).
102    pub position_ambiguity: GpsPositionAmbiguity,
103    /// GPS data TX configuration (auto-transmit position on DV mode).
104    pub data_tx: GpsDataTx,
105    /// Target point for navigation (bearing/distance display).
106    pub target_point: Option<TargetPoint>,
107}
108
109impl Default for GpsConfig {
110    fn default() -> Self {
111        Self {
112            enabled: true,
113            pc_output: false,
114            operating_mode: GpsOperatingMode::Standalone,
115            battery_saver: false,
116            sentence_output: NmeaSentences::default(),
117            track_log: TrackLogConfig::default(),
118            my_positions: Default::default(),
119            position_ambiguity: GpsPositionAmbiguity::Full,
120            data_tx: GpsDataTx::default(),
121            target_point: None,
122        }
123    }
124}
125
126// ---------------------------------------------------------------------------
127// Operating mode
128// ---------------------------------------------------------------------------
129
130/// GPS receiver operating mode.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
132pub enum GpsOperatingMode {
133    /// Standalone GPS receiver (internal GPS only).
134    Standalone,
135    /// SBAS (Satellite Based Augmentation System) enabled.
136    /// Uses WAAS/EGNOS/MSAS for improved accuracy.
137    Sbas,
138    /// Manual position entry (GPS receiver off, use stored coordinates).
139    Manual,
140}
141
142impl TryFrom<u8> for GpsOperatingMode {
143    type Error = crate::error::ValidationError;
144
145    fn try_from(value: u8) -> Result<Self, Self::Error> {
146        match value {
147            0 => Ok(Self::Standalone),
148            1 => Ok(Self::Sbas),
149            2 => Ok(Self::Manual),
150            _ => Err(crate::error::ValidationError::SettingOutOfRange {
151                name: "GPS operating mode",
152                value,
153                detail: "must be 0-2",
154            }),
155        }
156    }
157}
158
159// ---------------------------------------------------------------------------
160// NMEA sentences
161// ---------------------------------------------------------------------------
162
163/// NMEA sentence output selection.
164///
165/// Controls which NMEA 0183 sentences are included when GPS data is
166/// output to the PC serial port. Each sentence provides different
167/// navigation data.
168#[allow(clippy::struct_excessive_bools)]
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
170pub struct NmeaSentences {
171    /// GGA -- Global Positioning System Fix Data.
172    /// Contains time, position, fix quality, number of satellites, HDOP,
173    /// altitude, and geoid separation.
174    pub gga: bool,
175    /// GLL -- Geographic Position (latitude/longitude).
176    /// Contains position and time with status.
177    pub gll: bool,
178    /// GSA -- GPS DOP (Dilution of Precision) and Active Satellites.
179    /// Contains fix mode, satellite PRNs, PDOP, HDOP, VDOP.
180    pub gsa: bool,
181    /// GSV -- GPS Satellites in View.
182    /// Contains satellite PRN, elevation, azimuth, and SNR for each
183    /// visible satellite.
184    pub gsv: bool,
185    /// RMC -- Recommended Minimum Specific GNSS Data.
186    /// Contains time, status, position, speed, course, date, and
187    /// magnetic variation. This is the most commonly used sentence.
188    pub rmc: bool,
189    /// VTG -- Course Over Ground and Ground Speed.
190    /// Contains true/magnetic course and speed in knots/km/h.
191    pub vtg: bool,
192}
193
194impl Default for NmeaSentences {
195    fn default() -> Self {
196        Self {
197            gga: true,
198            gll: true,
199            gsa: true,
200            gsv: true,
201            rmc: true,
202            vtg: true,
203        }
204    }
205}
206
207// ---------------------------------------------------------------------------
208// Track log
209// ---------------------------------------------------------------------------
210
211/// Track log recording configuration.
212///
213/// The TH-D75 records GPS track logs to the microSD card at
214/// `/KENWOOD/TH-D75/GPS_LOG/` in NMEA format (`.nme` files).
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
216pub struct TrackLogConfig {
217    /// Track log recording method.
218    pub record_method: TrackRecordMethod,
219    /// Recording interval in seconds (range 1-9999).
220    /// Used when `record_method` is `Interval`.
221    pub interval_seconds: u16,
222    /// Recording distance in meters (range 10-9999).
223    /// Used when `record_method` is `Distance`.
224    pub distance_meters: u16,
225}
226
227impl Default for TrackLogConfig {
228    fn default() -> Self {
229        Self {
230            record_method: TrackRecordMethod::Off,
231            interval_seconds: 5,
232            distance_meters: 100,
233        }
234    }
235}
236
237/// Track log recording trigger method.
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
239pub enum TrackRecordMethod {
240    /// Track log recording disabled.
241    Off,
242    /// Record at a fixed time interval.
243    Interval,
244    /// Record when the distance threshold is exceeded.
245    Distance,
246}
247
248impl TryFrom<u8> for TrackRecordMethod {
249    type Error = crate::error::ValidationError;
250
251    fn try_from(value: u8) -> Result<Self, Self::Error> {
252        match value {
253            0 => Ok(Self::Off),
254            1 => Ok(Self::Interval),
255            2 => Ok(Self::Distance),
256            _ => Err(crate::error::ValidationError::SettingOutOfRange {
257                name: "track record method",
258                value,
259                detail: "must be 0-2",
260            }),
261        }
262    }
263}
264
265// ---------------------------------------------------------------------------
266// Position memory
267// ---------------------------------------------------------------------------
268
269/// GPS position memory slot.
270///
271/// The TH-D75 provides 5 "My Position" slots ("My Position 1" through
272/// "My Position 5") for storing known locations. These can be used as
273/// manual position references when GPS is unavailable.
274///
275/// Per Operating Tips §5.14.4: the radio also has 100 general-purpose
276/// position memory slots (separate from these 5 "My Position" entries)
277/// that store latitude, longitude, altitude, timestamp, name, and APRS
278/// icon. A position memory entry can be copied to one of these "My
279/// Position" slots (§5.14.5) or to an APRS Object for transmission.
280#[derive(Debug, Clone, PartialEq)]
281pub struct PositionMemory {
282    /// Descriptive name for the position (up to 8 characters).
283    pub name: PositionName,
284    /// Latitude in decimal degrees (positive = North, negative = South).
285    /// Range: -90.0 to +90.0.
286    pub latitude: f64,
287    /// Longitude in decimal degrees (positive = East, negative = West).
288    /// Range: -180.0 to +180.0.
289    pub longitude: f64,
290    /// Altitude in meters above mean sea level.
291    pub altitude: f64,
292}
293
294impl Default for PositionMemory {
295    fn default() -> Self {
296        Self {
297            name: PositionName::default(),
298            latitude: 0.0,
299            longitude: 0.0,
300            altitude: 0.0,
301        }
302    }
303}
304
305/// Position memory name (up to 8 characters).
306#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
307pub struct PositionName(String);
308
309impl PositionName {
310    /// Maximum length of a position name.
311    pub const MAX_LEN: usize = 8;
312
313    /// Creates a new position name.
314    ///
315    /// # Errors
316    ///
317    /// Returns `None` if the name exceeds 8 characters.
318    #[must_use]
319    pub fn new(name: &str) -> Option<Self> {
320        if name.len() <= Self::MAX_LEN {
321            Some(Self(name.to_owned()))
322        } else {
323            None
324        }
325    }
326
327    /// Returns the name as a string slice.
328    #[must_use]
329    pub fn as_str(&self) -> &str {
330        &self.0
331    }
332}
333
334// ---------------------------------------------------------------------------
335// Position ambiguity (GPS-specific)
336// ---------------------------------------------------------------------------
337
338/// GPS position ambiguity level.
339///
340/// Each level removes one digit of precision from the transmitted
341/// position, progressively obscuring the exact location. Identical
342/// in concept to APRS position ambiguity but configured via the GPS menu.
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
344pub enum GpsPositionAmbiguity {
345    /// Full precision (approximately 60 feet).
346    Full,
347    /// 1 digit removed (approximately 1/10 mile).
348    Level1,
349    /// 2 digits removed (approximately 1 mile).
350    Level2,
351    /// 3 digits removed (approximately 10 miles).
352    Level3,
353    /// 4 digits removed (approximately 60 miles).
354    Level4,
355}
356
357// ---------------------------------------------------------------------------
358// GPS data TX
359// ---------------------------------------------------------------------------
360
361/// GPS data TX configuration for D-STAR mode.
362///
363/// Controls automatic transmission of GPS position data in D-STAR DV
364/// frame headers.
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
366pub struct GpsDataTx {
367    /// Enable automatic GPS data transmission on DV mode.
368    pub auto_tx: bool,
369    /// Auto TX interval in seconds (range 1-9999).
370    pub interval_seconds: u16,
371}
372
373impl Default for GpsDataTx {
374    fn default() -> Self {
375        Self {
376            auto_tx: false,
377            interval_seconds: 60,
378        }
379    }
380}
381
382// ---------------------------------------------------------------------------
383// GPS position (parsed from NMEA)
384// ---------------------------------------------------------------------------
385
386/// Parsed GPS position from the receiver.
387///
388/// Represents the current GPS fix data as parsed from NMEA sentences
389/// (GGA, RMC, etc.). This is a read-only data type populated by the
390/// GPS receiver.
391#[derive(Debug, Clone, PartialEq)]
392pub struct GpsPosition {
393    /// Latitude in decimal degrees (positive = North, negative = South).
394    pub latitude: f64,
395    /// Longitude in decimal degrees (positive = East, negative = West).
396    pub longitude: f64,
397    /// Altitude above mean sea level in meters.
398    pub altitude: f64,
399    /// Ground speed in km/h.
400    pub speed: f64,
401    /// Course over ground in degrees (0.0 = true north, 90.0 = east).
402    pub course: f64,
403    /// GPS fix quality.
404    pub fix: GpsFix,
405    /// Number of satellites used in the fix.
406    pub satellites: u8,
407    /// Horizontal dilution of precision (HDOP). Lower is better.
408    /// Typical values: 1.0 = excellent, 2.0 = good, 5.0 = moderate.
409    pub hdop: f64,
410    /// UTC timestamp in "`HHMMSSss`" format (hours, minutes, seconds,
411    /// hundredths), or `None` if time is not available.
412    pub timestamp: Option<String>,
413    /// UTC date in "DDMMYY" format, or `None` if date is not available.
414    pub date: Option<String>,
415    /// Maidenhead grid square locator (4 or 6 characters).
416    pub grid_square: Option<String>,
417}
418
419impl Default for GpsPosition {
420    fn default() -> Self {
421        Self {
422            latitude: 0.0,
423            longitude: 0.0,
424            altitude: 0.0,
425            speed: 0.0,
426            course: 0.0,
427            fix: GpsFix::NoFix,
428            satellites: 0,
429            hdop: 99.9,
430            timestamp: None,
431            date: None,
432            grid_square: None,
433        }
434    }
435}
436
437/// GPS fix quality/type.
438#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
439pub enum GpsFix {
440    /// No fix available.
441    NoFix,
442    /// 2D fix (latitude and longitude only, no altitude).
443    Fix2D,
444    /// 3D fix (latitude, longitude, and altitude).
445    Fix3D,
446    /// Differential GPS fix (DGPS/SBAS-corrected position).
447    DGps,
448}
449
450// ---------------------------------------------------------------------------
451// Target point
452// ---------------------------------------------------------------------------
453
454/// Navigation target point.
455///
456/// When set, the radio displays bearing and distance from the current
457/// GPS position to the target point. The firmware outputs `$GPWPL` NMEA
458/// sentences for waypoint data (handler at `0xC00D0FA0`).
459#[derive(Debug, Clone, PartialEq)]
460pub struct TargetPoint {
461    /// Target latitude in decimal degrees (positive = North).
462    pub latitude: f64,
463    /// Target longitude in decimal degrees (positive = East).
464    pub longitude: f64,
465    /// Optional descriptive name for the target.
466    pub name: Option<String>,
467}
468
469// ---------------------------------------------------------------------------
470// Coordinate display format
471// ---------------------------------------------------------------------------
472
473/// Latitude/longitude display format.
474///
475/// Controls how coordinates are displayed on the radio's screen.
476/// Configured in the "Units" menu section.
477#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
478pub enum CoordinateFormat {
479    /// Degrees, minutes, seconds (DD MM'SS").
480    Dms,
481    /// Degrees, decimal minutes (DD MM.MMM').
482    Dmm,
483    /// Decimal degrees (DD.DDDDD).
484    Dd,
485}
486
487/// Grid square format for Maidenhead locator display.
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
489pub enum GridSquareFormat {
490    /// 4-character grid square (e.g. "EM85").
491    Four,
492    /// 6-character grid square (e.g. "`EM85qd`").
493    Six,
494}
495
496// ---------------------------------------------------------------------------
497// Tests
498// ---------------------------------------------------------------------------
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn gps_config_default() {
506        let cfg = GpsConfig::default();
507        assert!(cfg.enabled);
508        assert!(!cfg.pc_output);
509        assert_eq!(cfg.operating_mode, GpsOperatingMode::Standalone);
510    }
511
512    #[test]
513    fn nmea_sentences_default_all_enabled() {
514        let s = NmeaSentences::default();
515        assert!(s.gga);
516        assert!(s.gll);
517        assert!(s.gsa);
518        assert!(s.gsv);
519        assert!(s.rmc);
520        assert!(s.vtg);
521    }
522
523    #[test]
524    fn track_log_default_off() {
525        let tl = TrackLogConfig::default();
526        assert_eq!(tl.record_method, TrackRecordMethod::Off);
527        assert_eq!(tl.interval_seconds, 5);
528        assert_eq!(tl.distance_meters, 100);
529    }
530
531    #[test]
532    fn position_memory_default() {
533        let pm = PositionMemory::default();
534        assert_eq!(pm.name.as_str(), "");
535        assert!((pm.latitude - 0.0).abs() < f64::EPSILON);
536        assert!((pm.longitude - 0.0).abs() < f64::EPSILON);
537    }
538
539    #[test]
540    fn position_name_valid() {
541        let name = PositionName::new("Home").unwrap();
542        assert_eq!(name.as_str(), "Home");
543    }
544
545    #[test]
546    fn position_name_max_length() {
547        let name = PositionName::new("12345678").unwrap();
548        assert_eq!(name.as_str(), "12345678");
549    }
550
551    #[test]
552    fn position_name_too_long() {
553        assert!(PositionName::new("123456789").is_none());
554    }
555
556    #[test]
557    fn gps_position_default_no_fix() {
558        let pos = GpsPosition::default();
559        assert_eq!(pos.fix, GpsFix::NoFix);
560        assert_eq!(pos.satellites, 0);
561    }
562
563    #[test]
564    fn gps_data_tx_default() {
565        let dtx = GpsDataTx::default();
566        assert!(!dtx.auto_tx);
567        assert_eq!(dtx.interval_seconds, 60);
568    }
569
570    #[test]
571    fn gps_fix_variants() {
572        assert_ne!(GpsFix::NoFix, GpsFix::Fix2D);
573        assert_ne!(GpsFix::Fix2D, GpsFix::Fix3D);
574        assert_ne!(GpsFix::Fix3D, GpsFix::DGps);
575    }
576
577    #[test]
578    fn target_point_construction() {
579        let tp = TargetPoint {
580            latitude: 35.6762,
581            longitude: 139.6503,
582            name: Some("Tokyo".to_owned()),
583        };
584        assert!((tp.latitude - 35.6762).abs() < 0.0001);
585    }
586}