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}