kenwood_thd75/memory/
gps.rs1use crate::types::gps::{GpsOperatingMode, GpsPositionAmbiguity};
16
17const GPS_ESTIMATED_OFFSET: usize = 0x19000;
22
23const GPS_ESTIMATED_SIZE: usize = 0x1000; const GPS_ENABLED_REL: usize = 0x00;
37
38const GPS_PC_OUTPUT_REL: usize = 0x01;
41
42const GPS_OPERATING_MODE_REL: usize = 0x02;
45
46const GPS_BATTERY_SAVER_REL: usize = 0x03;
49
50const GPS_POSITION_AMBIGUITY_REL: usize = 0x04;
53
54const GPS_NMEA_FLAGS_REL: usize = 0x05;
59
60const GPS_CHANNEL_INDEX_OFFSET: usize = 0x4_D000;
74
75const GPS_CHANNEL_INDEX_COUNT: usize = 100;
77
78const GPS_INDEX_UNUSED: u8 = 0xFF;
80
81const GPS_WAYPOINT_BASE_INDEX: usize = 0x2608;
85
86const GPS_WAYPOINT_RECORD_SIZE: usize = 0x20;
88
89#[derive(Debug)]
109pub struct GpsAccess<'a> {
110 image: &'a [u8],
111}
112
113impl<'a> GpsAccess<'a> {
114 pub(crate) const fn new(image: &'a [u8]) -> Self {
116 Self { image }
117 }
118
119 #[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 #[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 #[must_use]
150 pub const fn estimated_region_size(&self) -> usize {
151 GPS_ESTIMATED_SIZE
152 }
153
154 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 if record.len() < 0x19 {
384 return String::new();
385 }
386 let name_bytes = &record[0x10..0x19];
387 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#[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 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)); assert!(!gps.nmea_sentence_enabled(1)); assert!(!gps.nmea_sentence_enabled(2)); assert!(!gps.nmea_sentence_enabled(3)); assert!(gps.nmea_sentence_enabled(4)); assert!(!gps.nmea_sentence_enabled(5)); }
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 #[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 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 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 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; 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 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 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}