kenwood_thd75/memory/
gps.rs

1//! Typed access to the GPS configuration region of the memory image.
2//!
3//! The GPS configuration is estimated to occupy ~4,096 bytes around
4//! byte offset `0x19000` in the MCP address space. This includes GPS
5//! receiver settings, position memory slots, track log configuration,
6//! and NMEA output selection.
7//!
8//! # Offset confidence
9//!
10//! The GPS region boundaries are estimated from the overall memory
11//! layout analysis. No GPS offsets have been confirmed via differential
12//! dump on a D75. All typed accessors in this module are marked with
13//! `# Verification` in their doc comments.
14
15use crate::types::gps::{GpsOperatingMode, GpsPositionAmbiguity};
16
17/// Estimated byte offset of the GPS configuration region.
18///
19/// This is an estimate based on the overall memory layout analysis.
20/// The actual start may differ by a few pages.
21const GPS_ESTIMATED_OFFSET: usize = 0x19000;
22
23/// Estimated size of the GPS configuration region.
24const GPS_ESTIMATED_SIZE: usize = 0x1000; // 4 KB
25
26// ---------------------------------------------------------------------------
27// Estimated field offsets within the GPS region
28//
29// These offsets are relative to GPS_ESTIMATED_OFFSET and are rough
30// estimates based on menu ordering and typical Kenwood layout patterns.
31// None have been verified on hardware.
32// ---------------------------------------------------------------------------
33
34/// Estimated offset for GPS enabled (1 byte, 0 = off, 1 = on).
35/// Relative to `GPS_ESTIMATED_OFFSET`.
36const GPS_ENABLED_REL: usize = 0x00;
37
38/// Estimated offset for GPS PC output (1 byte, 0 = off, 1 = on).
39/// Relative to `GPS_ESTIMATED_OFFSET`.
40const GPS_PC_OUTPUT_REL: usize = 0x01;
41
42/// Estimated offset for GPS operating mode (1 byte, enum index).
43/// Relative to `GPS_ESTIMATED_OFFSET`.
44const GPS_OPERATING_MODE_REL: usize = 0x02;
45
46/// Estimated offset for GPS battery saver (1 byte, 0 = off, 1 = on).
47/// Relative to `GPS_ESTIMATED_OFFSET`.
48const GPS_BATTERY_SAVER_REL: usize = 0x03;
49
50/// Estimated offset for position ambiguity (1 byte, 0-4).
51/// Relative to `GPS_ESTIMATED_OFFSET`.
52const GPS_POSITION_AMBIGUITY_REL: usize = 0x04;
53
54/// Estimated offset for NMEA sentence flags (1 byte, bit field:
55/// bit 0 = GGA, bit 1 = GLL, bit 2 = GSA, bit 3 = GSV,
56/// bit 4 = RMC, bit 5 = VTG).
57/// Relative to `GPS_ESTIMATED_OFFSET`.
58const GPS_NMEA_FLAGS_REL: usize = 0x05;
59
60// ---------------------------------------------------------------------------
61// GPS channel index
62//
63// The GPS channel index at byte offset 0x4D000 contains 100 entries of
64// 1 byte each. A value of 0xFF indicates an unused slot; other values
65// are indices into the waypoint data area.
66//
67// Waypoint data for entry with index value V is located at:
68//   (V + 0x2608) * 0x20
69// Each waypoint record is 0x20 (32) bytes.
70// ---------------------------------------------------------------------------
71
72/// Byte offset of the GPS channel index (100 x 1 byte).
73const GPS_CHANNEL_INDEX_OFFSET: usize = 0x4_D000;
74
75/// Number of GPS channel index entries.
76const GPS_CHANNEL_INDEX_COUNT: usize = 100;
77
78/// Marker value for unused GPS channel index entries.
79const GPS_INDEX_UNUSED: u8 = 0xFF;
80
81/// Base offset for waypoint data address calculation.
82///
83/// Waypoint data address = `(index_value + GPS_WAYPOINT_BASE_INDEX) * GPS_WAYPOINT_RECORD_SIZE`.
84const GPS_WAYPOINT_BASE_INDEX: usize = 0x2608;
85
86/// Size of a single GPS waypoint record in bytes.
87const GPS_WAYPOINT_RECORD_SIZE: usize = 0x20;
88
89// ---------------------------------------------------------------------------
90// GpsAccess (read-only)
91// ---------------------------------------------------------------------------
92
93/// Read-only access to the GPS configuration region.
94///
95/// Provides raw byte access and typed field accessors for the estimated
96/// GPS settings region. All offsets are estimates and need verification
97/// via differential memory dumps.
98///
99/// # Known settings (from menu analysis, offsets estimated)
100///
101/// - Built-in GPS on/off
102/// - My Position (5 manual slots, each with lat/lon/alt)
103/// - Position ambiguity setting
104/// - GPS operating mode (standalone/SBAS)
105/// - PC output format (NMEA sentences enabled/disabled)
106/// - Track log settings (record method, interval, distance)
107/// - GPS data TX settings (auto TX, interval)
108#[derive(Debug)]
109pub struct GpsAccess<'a> {
110    image: &'a [u8],
111}
112
113impl<'a> GpsAccess<'a> {
114    /// Create a new GPS accessor borrowing the raw image.
115    pub(crate) const fn new(image: &'a [u8]) -> Self {
116        Self { image }
117    }
118
119    /// Get the raw bytes at the estimated GPS region.
120    ///
121    /// Returns the bytes at offset `0x19000` through `0x19FFF`. These
122    /// boundaries are estimates and may not perfectly align with the
123    /// actual GPS configuration data.
124    #[must_use]
125    pub fn estimated_region(&self) -> Option<&[u8]> {
126        let end = GPS_ESTIMATED_OFFSET + GPS_ESTIMATED_SIZE;
127        if end <= self.image.len() {
128            Some(&self.image[GPS_ESTIMATED_OFFSET..end])
129        } else {
130            None
131        }
132    }
133
134    /// Read an arbitrary byte range from the image.
135    ///
136    /// The offset is an absolute MCP byte address. Returns `None` if
137    /// the range extends past the image.
138    #[must_use]
139    pub fn read_bytes(&self, offset: usize, len: usize) -> Option<&[u8]> {
140        let end = offset + len;
141        if end <= self.image.len() {
142            Some(&self.image[offset..end])
143        } else {
144            None
145        }
146    }
147
148    /// Get the estimated size of the GPS region in bytes.
149    #[must_use]
150    pub const fn estimated_region_size(&self) -> usize {
151        GPS_ESTIMATED_SIZE
152    }
153
154    // -----------------------------------------------------------------------
155    // Typed GPS accessors (estimated offsets)
156    // -----------------------------------------------------------------------
157
158    /// Read GPS enabled setting.
159    ///
160    /// # Offset
161    ///
162    /// Estimated at `0x19000` (first byte of the GPS region).
163    ///
164    /// # Verification
165    ///
166    /// Offset is estimated, not hardware-verified.
167    #[must_use]
168    pub fn gps_enabled(&self) -> bool {
169        self.image
170            .get(GPS_ESTIMATED_OFFSET + GPS_ENABLED_REL)
171            .is_some_and(|&b| b != 0)
172    }
173
174    /// Read GPS PC output setting.
175    ///
176    /// # Offset
177    ///
178    /// Estimated at `0x19001`.
179    ///
180    /// # Verification
181    ///
182    /// Offset is estimated, not hardware-verified.
183    #[must_use]
184    pub fn pc_output(&self) -> bool {
185        self.image
186            .get(GPS_ESTIMATED_OFFSET + GPS_PC_OUTPUT_REL)
187            .is_some_and(|&b| b != 0)
188    }
189
190    /// Read GPS operating mode.
191    ///
192    /// # Offset
193    ///
194    /// Estimated at `0x19002`.
195    ///
196    /// # Verification
197    ///
198    /// Offset is estimated, not hardware-verified.
199    #[must_use]
200    pub fn operating_mode(&self) -> GpsOperatingMode {
201        match self
202            .image
203            .get(GPS_ESTIMATED_OFFSET + GPS_OPERATING_MODE_REL)
204            .copied()
205            .unwrap_or(0)
206        {
207            1 => GpsOperatingMode::Sbas,
208            2 => GpsOperatingMode::Manual,
209            _ => GpsOperatingMode::Standalone,
210        }
211    }
212
213    /// Read GPS battery saver setting.
214    ///
215    /// # Offset
216    ///
217    /// Estimated at `0x19003`.
218    ///
219    /// # Verification
220    ///
221    /// Offset is estimated, not hardware-verified.
222    #[must_use]
223    pub fn battery_saver(&self) -> bool {
224        self.image
225            .get(GPS_ESTIMATED_OFFSET + GPS_BATTERY_SAVER_REL)
226            .is_some_and(|&b| b != 0)
227    }
228
229    /// Read GPS position ambiguity level.
230    ///
231    /// # Offset
232    ///
233    /// Estimated at `0x19004`.
234    ///
235    /// # Verification
236    ///
237    /// Offset is estimated, not hardware-verified.
238    #[must_use]
239    pub fn position_ambiguity(&self) -> GpsPositionAmbiguity {
240        match self
241            .image
242            .get(GPS_ESTIMATED_OFFSET + GPS_POSITION_AMBIGUITY_REL)
243            .copied()
244            .unwrap_or(0)
245        {
246            1 => GpsPositionAmbiguity::Level1,
247            2 => GpsPositionAmbiguity::Level2,
248            3 => GpsPositionAmbiguity::Level3,
249            4 => GpsPositionAmbiguity::Level4,
250            _ => GpsPositionAmbiguity::Full,
251        }
252    }
253
254    /// Read NMEA sentence output flags as a raw byte.
255    ///
256    /// Bit field: bit 0 = GGA, bit 1 = GLL, bit 2 = GSA, bit 3 = GSV,
257    /// bit 4 = RMC, bit 5 = VTG. Returns `0x3F` (all enabled) if
258    /// unreadable.
259    ///
260    /// # Offset
261    ///
262    /// Estimated at `0x19005`.
263    ///
264    /// # Verification
265    ///
266    /// Offset is estimated, not hardware-verified.
267    #[must_use]
268    pub fn nmea_sentence_flags(&self) -> u8 {
269        self.image
270            .get(GPS_ESTIMATED_OFFSET + GPS_NMEA_FLAGS_REL)
271            .copied()
272            .unwrap_or(0x3F)
273    }
274
275    /// Check if a specific NMEA sentence is enabled.
276    ///
277    /// `bit` selects the sentence: 0 = GGA, 1 = GLL, 2 = GSA,
278    /// 3 = GSV, 4 = RMC, 5 = VTG.
279    ///
280    /// # Offset
281    ///
282    /// Estimated at `0x19005`.
283    ///
284    /// # Verification
285    ///
286    /// Offset is estimated, not hardware-verified.
287    #[must_use]
288    pub fn nmea_sentence_enabled(&self, bit: u8) -> bool {
289        if bit > 5 {
290            return false;
291        }
292        (self.nmea_sentence_flags() >> bit) & 1 != 0
293    }
294
295    // -----------------------------------------------------------------------
296    // GPS channel index accessors
297    // -----------------------------------------------------------------------
298
299    /// Get the raw GPS channel index (100 bytes at `0x4D000`).
300    ///
301    /// Each byte is either `0xFF` (unused) or an index into the waypoint
302    /// data area.
303    ///
304    /// Returns `None` if the region extends past the image.
305    #[must_use]
306    pub fn channel_index_raw(&self) -> Option<&[u8]> {
307        let end = GPS_CHANNEL_INDEX_OFFSET + GPS_CHANNEL_INDEX_COUNT;
308        if end <= self.image.len() {
309            Some(&self.image[GPS_CHANNEL_INDEX_OFFSET..end])
310        } else {
311            None
312        }
313    }
314
315    /// Get the GPS channel index value for a given slot (0-99).
316    ///
317    /// Returns `None` if the slot is unused (`0xFF`) or out of range.
318    /// Otherwise returns the waypoint data index.
319    #[must_use]
320    pub fn channel_index(&self, slot: u8) -> Option<u8> {
321        let slot_usize = slot as usize;
322        if slot_usize >= GPS_CHANNEL_INDEX_COUNT {
323            return None;
324        }
325        let offset = GPS_CHANNEL_INDEX_OFFSET + slot_usize;
326        let value = self.image.get(offset).copied()?;
327        if value == GPS_INDEX_UNUSED {
328            None
329        } else {
330            Some(value)
331        }
332    }
333
334    /// Count the number of active (non-empty) GPS waypoint slots.
335    ///
336    /// Iterates the 100-entry GPS channel index and counts entries that
337    /// are not `0xFF`.
338    #[must_use]
339    pub fn waypoint_count(&self) -> usize {
340        (0..GPS_CHANNEL_INDEX_COUNT)
341            .filter(|&i| {
342                let offset = GPS_CHANNEL_INDEX_OFFSET + i;
343                self.image
344                    .get(offset)
345                    .is_some_and(|&b| b != GPS_INDEX_UNUSED)
346            })
347            .count()
348    }
349
350    /// Get the raw waypoint record for a given channel index slot (0-99).
351    ///
352    /// Looks up the waypoint data index from the GPS channel index, then
353    /// reads the 32-byte waypoint record at the calculated address:
354    /// `(index_value + 0x2608) * 0x20`.
355    ///
356    /// Returns `None` if the slot is unused, out of range, or the record
357    /// extends past the image.
358    #[must_use]
359    pub fn waypoint_raw(&self, slot: u8) -> Option<&[u8]> {
360        let index_value = self.channel_index(slot)? as usize;
361        let data_offset = (index_value + GPS_WAYPOINT_BASE_INDEX) * GPS_WAYPOINT_RECORD_SIZE;
362        let end = data_offset + GPS_WAYPOINT_RECORD_SIZE;
363        if end <= self.image.len() {
364            Some(&self.image[data_offset..end])
365        } else {
366            None
367        }
368    }
369
370    /// Read the name field from a GPS waypoint record (up to 8 characters).
371    ///
372    /// Returns an empty string if the slot is unused or the record cannot
373    /// be read. The name is at offset 0x10 within the 32-byte record,
374    /// 9 bytes (8 characters + null terminator). A first byte of `0xFE`
375    /// indicates an unused name.
376    #[must_use]
377    pub fn waypoint_name(&self, slot: u8) -> String {
378        let Some(record) = self.waypoint_raw(slot) else {
379            return String::new();
380        };
381
382        // Name is at record offset 0x10, 9 bytes.
383        if record.len() < 0x19 {
384            return String::new();
385        }
386        let name_bytes = &record[0x10..0x19];
387        // 0xFE in the first byte means unused.
388        if name_bytes[0] == 0xFE {
389            return String::new();
390        }
391        let nul = name_bytes
392            .iter()
393            .position(|&b| b == 0)
394            .unwrap_or(name_bytes.len());
395        String::from_utf8_lossy(&name_bytes[..nul])
396            .trim()
397            .to_owned()
398    }
399}
400
401// ---------------------------------------------------------------------------
402// Tests
403// ---------------------------------------------------------------------------
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use crate::protocol::programming::TOTAL_SIZE;
409    use crate::types::gps::{GpsOperatingMode, GpsPositionAmbiguity};
410
411    fn make_gps_image() -> Vec<u8> {
412        vec![0u8; TOTAL_SIZE]
413    }
414
415    #[test]
416    fn gps_estimated_region_accessible() {
417        let image = vec![0xBB_u8; TOTAL_SIZE];
418        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
419        let gps = mi.gps();
420        let region = gps.estimated_region().unwrap();
421        assert_eq!(region.len(), GPS_ESTIMATED_SIZE);
422        assert!(region.iter().all(|&b| b == 0xBB));
423    }
424
425    #[test]
426    fn gps_read_bytes() {
427        let mut image = make_gps_image();
428        image[GPS_ESTIMATED_OFFSET..GPS_ESTIMATED_OFFSET + 4]
429            .copy_from_slice(&[0x01, 0x02, 0x03, 0x04]);
430
431        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
432        let gps = mi.gps();
433        let bytes = gps.read_bytes(GPS_ESTIMATED_OFFSET, 4).unwrap();
434        assert_eq!(bytes, &[0x01, 0x02, 0x03, 0x04]);
435    }
436
437    #[test]
438    fn gps_region_size() {
439        let image = make_gps_image();
440        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
441        let gps = mi.gps();
442        assert_eq!(gps.estimated_region_size(), 0x1000);
443    }
444
445    #[test]
446    fn gps_enabled() {
447        let mut image = make_gps_image();
448        image[GPS_ESTIMATED_OFFSET + GPS_ENABLED_REL] = 1;
449        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
450        assert!(mi.gps().gps_enabled());
451    }
452
453    #[test]
454    fn gps_enabled_off() {
455        let image = make_gps_image();
456        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
457        assert!(!mi.gps().gps_enabled());
458    }
459
460    #[test]
461    fn gps_pc_output() {
462        let mut image = make_gps_image();
463        image[GPS_ESTIMATED_OFFSET + GPS_PC_OUTPUT_REL] = 1;
464        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
465        assert!(mi.gps().pc_output());
466    }
467
468    #[test]
469    fn gps_operating_mode() {
470        let mut image = make_gps_image();
471        image[GPS_ESTIMATED_OFFSET + GPS_OPERATING_MODE_REL] = 1;
472        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
473        assert_eq!(mi.gps().operating_mode(), GpsOperatingMode::Sbas);
474    }
475
476    #[test]
477    fn gps_operating_mode_manual() {
478        let mut image = make_gps_image();
479        image[GPS_ESTIMATED_OFFSET + GPS_OPERATING_MODE_REL] = 2;
480        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
481        assert_eq!(mi.gps().operating_mode(), GpsOperatingMode::Manual);
482    }
483
484    #[test]
485    fn gps_operating_mode_default() {
486        let image = make_gps_image();
487        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
488        assert_eq!(mi.gps().operating_mode(), GpsOperatingMode::Standalone);
489    }
490
491    #[test]
492    fn gps_battery_saver() {
493        let mut image = make_gps_image();
494        image[GPS_ESTIMATED_OFFSET + GPS_BATTERY_SAVER_REL] = 1;
495        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
496        assert!(mi.gps().battery_saver());
497    }
498
499    #[test]
500    fn gps_position_ambiguity() {
501        let mut image = make_gps_image();
502        image[GPS_ESTIMATED_OFFSET + GPS_POSITION_AMBIGUITY_REL] = 3;
503        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
504        assert_eq!(mi.gps().position_ambiguity(), GpsPositionAmbiguity::Level3);
505    }
506
507    #[test]
508    fn gps_position_ambiguity_default() {
509        let image = make_gps_image();
510        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
511        assert_eq!(mi.gps().position_ambiguity(), GpsPositionAmbiguity::Full);
512    }
513
514    #[test]
515    fn gps_nmea_flags() {
516        let mut image = make_gps_image();
517        // Enable GGA (bit 0) and RMC (bit 4) = 0b00010001 = 0x11.
518        image[GPS_ESTIMATED_OFFSET + GPS_NMEA_FLAGS_REL] = 0x11;
519        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
520        let gps = mi.gps();
521        assert_eq!(gps.nmea_sentence_flags(), 0x11);
522        assert!(gps.nmea_sentence_enabled(0)); // GGA
523        assert!(!gps.nmea_sentence_enabled(1)); // GLL
524        assert!(!gps.nmea_sentence_enabled(2)); // GSA
525        assert!(!gps.nmea_sentence_enabled(3)); // GSV
526        assert!(gps.nmea_sentence_enabled(4)); // RMC
527        assert!(!gps.nmea_sentence_enabled(5)); // VTG
528    }
529
530    #[test]
531    fn gps_nmea_sentence_out_of_range() {
532        let image = make_gps_image();
533        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
534        assert!(!mi.gps().nmea_sentence_enabled(6));
535        assert!(!mi.gps().nmea_sentence_enabled(255));
536    }
537
538    // -----------------------------------------------------------------------
539    // GPS channel index tests
540    // -----------------------------------------------------------------------
541
542    #[test]
543    fn gps_channel_index_raw_accessible() {
544        let image = vec![0xFF_u8; TOTAL_SIZE];
545        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
546        let gps = mi.gps();
547        let index = gps.channel_index_raw().unwrap();
548        assert_eq!(index.len(), GPS_CHANNEL_INDEX_COUNT);
549        // All 0xFF = unused.
550        assert!(index.iter().all(|&b| b == 0xFF));
551    }
552
553    #[test]
554    fn gps_channel_index_unused() {
555        let image = vec![0xFF_u8; TOTAL_SIZE];
556        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
557        assert!(mi.gps().channel_index(0).is_none());
558        assert!(mi.gps().channel_index(99).is_none());
559    }
560
561    #[test]
562    fn gps_channel_index_out_of_range() {
563        let image = vec![0xFF_u8; TOTAL_SIZE];
564        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
565        assert!(mi.gps().channel_index(100).is_none());
566        assert!(mi.gps().channel_index(255).is_none());
567    }
568
569    #[test]
570    fn gps_channel_index_populated() {
571        let mut image = vec![0xFF_u8; TOTAL_SIZE];
572        // Set slot 0 to waypoint index 5.
573        image[GPS_CHANNEL_INDEX_OFFSET] = 5;
574
575        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
576        assert_eq!(mi.gps().channel_index(0), Some(5));
577    }
578
579    #[test]
580    fn gps_waypoint_count_all_empty() {
581        let image = vec![0xFF_u8; TOTAL_SIZE];
582        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
583        assert_eq!(mi.gps().waypoint_count(), 0);
584    }
585
586    #[test]
587    fn gps_waypoint_count_with_entries() {
588        let mut image = vec![0xFF_u8; TOTAL_SIZE];
589        // Set 3 slots as used.
590        image[GPS_CHANNEL_INDEX_OFFSET] = 0;
591        image[GPS_CHANNEL_INDEX_OFFSET + 1] = 1;
592        image[GPS_CHANNEL_INDEX_OFFSET + 50] = 10;
593
594        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
595        assert_eq!(mi.gps().waypoint_count(), 3);
596    }
597
598    #[test]
599    fn gps_waypoint_raw_empty_slot() {
600        let image = vec![0xFF_u8; TOTAL_SIZE];
601        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
602        assert!(mi.gps().waypoint_raw(0).is_none());
603    }
604
605    #[test]
606    fn gps_waypoint_raw_populated() {
607        let mut image = vec![0xFF_u8; TOTAL_SIZE];
608        image[GPS_CHANNEL_INDEX_OFFSET] = 0; // Waypoint index 0.
609        // Waypoint data at (0 + 0x2608) * 0x20 = 0x4C100.
610        let wp_offset = GPS_WAYPOINT_BASE_INDEX * GPS_WAYPOINT_RECORD_SIZE;
611        if wp_offset + GPS_WAYPOINT_RECORD_SIZE <= image.len() {
612            image[wp_offset..wp_offset + 4].copy_from_slice(&[0x01, 0x02, 0x03, 0x04]);
613            let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
614            let gps = mi.gps();
615            let raw = gps.waypoint_raw(0).unwrap();
616            assert_eq!(raw.len(), GPS_WAYPOINT_RECORD_SIZE);
617            assert_eq!(&raw[..4], &[0x01, 0x02, 0x03, 0x04]);
618        }
619    }
620
621    #[test]
622    fn gps_waypoint_name() {
623        let mut image = vec![0xFF_u8; TOTAL_SIZE];
624        image[GPS_CHANNEL_INDEX_OFFSET] = 0;
625        let wp_offset = GPS_WAYPOINT_BASE_INDEX * GPS_WAYPOINT_RECORD_SIZE;
626        if wp_offset + GPS_WAYPOINT_RECORD_SIZE <= image.len() {
627            // Write name at waypoint record offset 0x10.
628            let name = b"HOME\0\0\0\0\0";
629            image[wp_offset + 0x10..wp_offset + 0x19].copy_from_slice(name);
630            let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
631            assert_eq!(mi.gps().waypoint_name(0), "HOME");
632        }
633    }
634
635    #[test]
636    fn gps_waypoint_name_empty_slot() {
637        let image = vec![0xFF_u8; TOTAL_SIZE];
638        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
639        assert_eq!(mi.gps().waypoint_name(0), "");
640    }
641
642    #[test]
643    fn gps_waypoint_name_unused_marker() {
644        let mut image = vec![0xFF_u8; TOTAL_SIZE];
645        image[GPS_CHANNEL_INDEX_OFFSET] = 0;
646        let wp_offset = GPS_WAYPOINT_BASE_INDEX * GPS_WAYPOINT_RECORD_SIZE;
647        if wp_offset + GPS_WAYPOINT_RECORD_SIZE <= image.len() {
648            // 0xFE as first byte of name = unused.
649            image[wp_offset + 0x10] = 0xFE;
650            let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
651            assert_eq!(mi.gps().waypoint_name(0), "");
652        }
653    }
654}