kenwood_thd75/memory/
channels.rs

1//! Typed access to channel data within a memory image.
2//!
3//! Channels are stored across three separate memory regions:
4//!
5//! - **Flags** at byte offset `0x2000`: 4 bytes per entry, 1,200 entries.
6//! - **Data** at byte offset `0x4000`: 40 bytes per channel in 192 memgroups
7//!   of 6 channels each (256 bytes per memgroup including 16 bytes padding).
8//! - **Names** at byte offset `0x10000`: 16 bytes per name, 1,200 entries.
9//!
10//! # Address verification
11//!
12//! These MCP byte offsets are confirmed by the memory dump fixture and are
13//! consistent with the memory map documentation. Note that some tools use
14//! file-based addressing (offset by +0x100 for the `.d75` file header),
15//! so addresses `0x2100`, `0x0100`, `0x10100` correspond to MCP byte
16//! addresses `0x2000`, `0x0000`, `0x10000` respectively. Our offsets are
17//! MCP byte addresses (no file header offset).
18//!
19//! The [`ChannelAccess`] struct borrows the raw image and provides methods
20//! to read individual channels or iterate over all populated channels.
21
22use crate::protocol::programming::{
23    self, CHANNEL_RECORD_SIZE, CHANNELS_PER_MEMGROUP, ChannelFlag, FLAG_EMPTY, FLAG_RECORD_SIZE,
24    MEMGROUP_COUNT, NAME_ENTRY_SIZE, PAGE_SIZE,
25};
26use crate::sdcard::config::ChannelEntry;
27use crate::types::channel::FlashChannel;
28
29use super::MemoryError;
30
31// ---------------------------------------------------------------------------
32// Byte offsets within the MCP memory image
33// ---------------------------------------------------------------------------
34
35/// Byte offset of channel flags (1,200 entries x 4 bytes).
36const FLAGS_OFFSET: usize = 0x2000;
37
38/// Byte offset of channel memory data (192 memgroups x 256 bytes).
39const DATA_OFFSET: usize = 0x4000;
40
41/// Byte offset of channel names (1,200 entries x 16 bytes).
42const NAMES_OFFSET: usize = 0x10000;
43
44/// Maximum regular channel number (0-999).
45const MAX_REGULAR_CHANNEL: u16 = 999;
46
47/// Total channel entries including extended channels.
48const TOTAL_ENTRIES: usize = programming::TOTAL_CHANNEL_ENTRIES; // 1200
49
50/// Maximum channel index as u16 (1199). `TOTAL_ENTRIES` is 1200, which
51/// always fits in u16, so this truncation is safe.
52#[allow(clippy::cast_possible_truncation)]
53const MAX_ENTRY_INDEX: u16 = (TOTAL_ENTRIES - 1) as u16;
54
55// ---------------------------------------------------------------------------
56// ChannelAccess (read-only)
57// ---------------------------------------------------------------------------
58
59/// Read-only access to channel data within a memory image.
60///
61/// This struct borrows the raw image bytes and provides methods to
62/// read individual channels by number, iterate over populated channels,
63/// and check channel status without copying data.
64#[derive(Debug)]
65pub struct ChannelAccess<'a> {
66    image: &'a [u8],
67}
68
69impl<'a> ChannelAccess<'a> {
70    /// Create a new channel accessor borrowing the raw image.
71    pub(crate) const fn new(image: &'a [u8]) -> Self {
72        Self { image }
73    }
74
75    /// Get the number of populated (non-empty) regular channels (0-999).
76    #[must_use]
77    pub fn count(&self) -> usize {
78        (0..=MAX_REGULAR_CHANNEL)
79            .filter(|&ch| self.is_used(ch))
80            .count()
81    }
82
83    /// Check if a channel slot is in use.
84    ///
85    /// Returns `false` for out-of-range channel numbers.
86    #[must_use]
87    pub fn is_used(&self, number: u16) -> bool {
88        let number_usize = number as usize;
89        if number_usize >= TOTAL_ENTRIES {
90            return false;
91        }
92        let offset = FLAGS_OFFSET + number_usize * FLAG_RECORD_SIZE;
93        if offset >= self.image.len() {
94            return false;
95        }
96        self.image[offset] != FLAG_EMPTY
97    }
98
99    /// Get a specific channel by number.
100    ///
101    /// Returns `None` if the channel number is out of range or if the
102    /// channel data cannot be read from the image.
103    #[must_use]
104    pub fn get(&self, number: u16) -> Option<ChannelEntry> {
105        let number_usize = number as usize;
106        if number_usize >= TOTAL_ENTRIES {
107            return None;
108        }
109
110        let flag = self.flag(number)?;
111        let used = flag.used != FLAG_EMPTY;
112        let flash = self.flash(number)?;
113        let name = self.name(number);
114
115        Some(ChannelEntry {
116            number,
117            name,
118            flash,
119            used,
120            lockout: flag.lockout,
121        })
122    }
123
124    /// Get all populated regular channels (0-999).
125    ///
126    /// Skips empty channel slots. The returned entries are in channel
127    /// number order.
128    #[must_use]
129    pub fn all(&self) -> Vec<ChannelEntry> {
130        (0..=MAX_REGULAR_CHANNEL)
131            .filter_map(|ch| {
132                let entry = self.get(ch)?;
133                if entry.used { Some(entry) } else { None }
134            })
135            .collect()
136    }
137
138    /// Get all channel entries (0-999), including empty slots.
139    #[must_use]
140    pub fn all_slots(&self) -> Vec<ChannelEntry> {
141        (0..=MAX_REGULAR_CHANNEL)
142            .filter_map(|ch| self.get(ch))
143            .collect()
144    }
145
146    /// Get the display name for a channel.
147    ///
148    /// Returns an empty string for channels without a user-assigned name
149    /// or for out-of-range channel numbers.
150    #[must_use]
151    pub fn name(&self, number: u16) -> String {
152        let number_usize = number as usize;
153        if number_usize >= TOTAL_ENTRIES {
154            return String::new();
155        }
156        let offset = NAMES_OFFSET + number_usize * NAME_ENTRY_SIZE;
157        if offset + NAME_ENTRY_SIZE > self.image.len() {
158            return String::new();
159        }
160        programming::extract_name(&self.image[offset..offset + NAME_ENTRY_SIZE])
161    }
162
163    /// Get the channel flag (used/band, lockout, group) for a channel.
164    ///
165    /// Returns `None` for out-of-range channel numbers.
166    #[must_use]
167    pub fn flag(&self, number: u16) -> Option<ChannelFlag> {
168        let number_usize = number as usize;
169        if number_usize >= TOTAL_ENTRIES {
170            return None;
171        }
172        let offset = FLAGS_OFFSET + number_usize * FLAG_RECORD_SIZE;
173        if offset + FLAG_RECORD_SIZE > self.image.len() {
174            return None;
175        }
176        programming::parse_channel_flag(&self.image[offset..])
177    }
178
179    /// Get the 40-byte flash channel record for a channel.
180    ///
181    /// Returns `None` for out-of-range channel numbers or if the data
182    /// cannot be parsed. Uses the flash memory encoding ([`FlashChannel`])
183    /// which includes all 8 operating modes and structured D-STAR fields.
184    #[must_use]
185    pub fn flash(&self, number: u16) -> Option<FlashChannel> {
186        let number_usize = number as usize;
187        if number_usize >= TOTAL_ENTRIES {
188            return None;
189        }
190
191        // Channel data layout: memgroup = ch / 6, slot = ch % 6
192        // byte_offset = 0x4000 + memgroup * 256 + slot * 40
193        let memgroup = number_usize / CHANNELS_PER_MEMGROUP;
194        let slot = number_usize % CHANNELS_PER_MEMGROUP;
195
196        if memgroup >= MEMGROUP_COUNT {
197            return None;
198        }
199
200        let offset = DATA_OFFSET + memgroup * PAGE_SIZE + slot * CHANNEL_RECORD_SIZE;
201        if offset + CHANNEL_RECORD_SIZE > self.image.len() {
202            return None;
203        }
204        FlashChannel::from_bytes(&self.image[offset..]).ok()
205    }
206
207    /// Get all channel names (0-999) as a vector of strings.
208    ///
209    /// Empty names are represented as empty strings.
210    #[must_use]
211    pub fn names(&self) -> Vec<String> {
212        (0..=MAX_REGULAR_CHANNEL).map(|ch| self.name(ch)).collect()
213    }
214
215    /// Get a group name by group index (0-29).
216    ///
217    /// Group names are stored at name indices 1152-1181.
218    #[must_use]
219    pub fn group_name(&self, group: u8) -> String {
220        if group >= 30 {
221            return String::new();
222        }
223        let name_index = 1152 + group as usize;
224        let offset = NAMES_OFFSET + name_index * NAME_ENTRY_SIZE;
225        if offset + NAME_ENTRY_SIZE > self.image.len() {
226            return String::new();
227        }
228        programming::extract_name(&self.image[offset..offset + NAME_ENTRY_SIZE])
229    }
230
231    /// Get all 30 group names.
232    #[must_use]
233    pub fn group_names(&self) -> Vec<String> {
234        (0..30).map(|g| self.group_name(g)).collect()
235    }
236}
237
238// ---------------------------------------------------------------------------
239// ChannelWriter (mutable access)
240// ---------------------------------------------------------------------------
241
242/// Mutable access to channel data within a memory image.
243///
244/// Created via [`MemoryImage::channels_mut`](super::MemoryImage).
245#[derive(Debug)]
246pub struct ChannelWriter<'a> {
247    image: &'a mut [u8],
248}
249
250impl<'a> ChannelWriter<'a> {
251    /// Create a new mutable channel accessor.
252    pub(crate) const fn new(image: &'a mut [u8]) -> Self {
253        Self { image }
254    }
255
256    /// Write a channel entry into the memory image.
257    ///
258    /// Updates the flag, memory data, and name regions for the given
259    /// channel number.
260    ///
261    /// # Errors
262    ///
263    /// Returns [`MemoryError::ChannelOutOfRange`] if the channel number
264    /// exceeds the maximum.
265    pub fn set(&mut self, entry: &ChannelEntry) -> Result<(), MemoryError> {
266        let number = entry.number as usize;
267        if number >= TOTAL_ENTRIES {
268            return Err(MemoryError::ChannelOutOfRange {
269                channel: entry.number,
270                max: MAX_ENTRY_INDEX,
271            });
272        }
273
274        // Write flag.
275        self.set_flag(entry.number, entry.used, entry.lockout)?;
276
277        // Write flash channel data.
278        self.set_flash(entry.number, &entry.flash)?;
279
280        // Write name.
281        self.set_name(entry.number, &entry.name)?;
282
283        Ok(())
284    }
285
286    /// Write a channel flag.
287    fn set_flag(&mut self, number: u16, used: bool, lockout: bool) -> Result<(), MemoryError> {
288        let number_usize = number as usize;
289        let offset = FLAGS_OFFSET + number_usize * FLAG_RECORD_SIZE;
290        if offset + FLAG_RECORD_SIZE > self.image.len() {
291            return Err(MemoryError::ChannelOutOfRange {
292                channel: number,
293                max: MAX_ENTRY_INDEX,
294            });
295        }
296
297        if used {
298            // Preserve the existing band indicator (byte 0) if already set.
299            // If transitioning from empty to used, default to 0x00 (VHF).
300            if self.image[offset] == FLAG_EMPTY {
301                self.image[offset] = 0x00; // VHF default
302            }
303        } else {
304            self.image[offset] = FLAG_EMPTY;
305        }
306
307        // Byte 1: lockout in bit 0, preserve other bits.
308        if lockout {
309            self.image[offset + 1] |= 0x01;
310        } else {
311            self.image[offset + 1] &= !0x01;
312        }
313
314        Ok(())
315    }
316
317    /// Write the 40-byte flash channel record.
318    fn set_flash(&mut self, number: u16, memory: &FlashChannel) -> Result<(), MemoryError> {
319        let number_usize = number as usize;
320        let memgroup = number_usize / CHANNELS_PER_MEMGROUP;
321        let slot = number_usize % CHANNELS_PER_MEMGROUP;
322
323        if memgroup >= MEMGROUP_COUNT {
324            return Err(MemoryError::ChannelOutOfRange {
325                channel: number,
326                max: MAX_ENTRY_INDEX,
327            });
328        }
329
330        let offset = DATA_OFFSET + memgroup * PAGE_SIZE + slot * CHANNEL_RECORD_SIZE;
331        if offset + CHANNEL_RECORD_SIZE > self.image.len() {
332            return Err(MemoryError::ChannelOutOfRange {
333                channel: number,
334                max: MAX_ENTRY_INDEX,
335            });
336        }
337
338        let bytes = memory.to_bytes();
339        self.image[offset..offset + CHANNEL_RECORD_SIZE].copy_from_slice(&bytes);
340        Ok(())
341    }
342
343    /// Write a channel display name (up to 16 bytes, null-padded).
344    fn set_name(&mut self, number: u16, name: &str) -> Result<(), MemoryError> {
345        let number_usize = number as usize;
346        let offset = NAMES_OFFSET + number_usize * NAME_ENTRY_SIZE;
347        if offset + NAME_ENTRY_SIZE > self.image.len() {
348            return Err(MemoryError::ChannelOutOfRange {
349                channel: number,
350                max: MAX_ENTRY_INDEX,
351            });
352        }
353
354        let mut buf = [0u8; NAME_ENTRY_SIZE];
355        let src = name.as_bytes();
356        let copy_len = src.len().min(NAME_ENTRY_SIZE);
357        buf[..copy_len].copy_from_slice(&src[..copy_len]);
358        self.image[offset..offset + NAME_ENTRY_SIZE].copy_from_slice(&buf);
359        Ok(())
360    }
361
362    /// Write a group name (up to 16 bytes, null-padded).
363    ///
364    /// Group indices are 0-29.
365    ///
366    /// # Errors
367    ///
368    /// Returns [`MemoryError::ChannelOutOfRange`] if the group index
369    /// is out of range.
370    pub fn set_group_name(&mut self, group: u8, name: &str) -> Result<(), MemoryError> {
371        if group >= 30 {
372            return Err(MemoryError::ChannelOutOfRange {
373                channel: u16::from(group),
374                max: 29,
375            });
376        }
377        let name_index = 1152 + group as usize;
378        let offset = NAMES_OFFSET + name_index * NAME_ENTRY_SIZE;
379        if offset + NAME_ENTRY_SIZE > self.image.len() {
380            return Err(MemoryError::ChannelOutOfRange {
381                channel: u16::from(group),
382                max: 29,
383            });
384        }
385
386        let mut buf = [0u8; NAME_ENTRY_SIZE];
387        let src = name.as_bytes();
388        let copy_len = src.len().min(NAME_ENTRY_SIZE);
389        buf[..copy_len].copy_from_slice(&src[..copy_len]);
390        self.image[offset..offset + NAME_ENTRY_SIZE].copy_from_slice(&buf);
391        Ok(())
392    }
393}
394
395// ---------------------------------------------------------------------------
396// Tests
397// ---------------------------------------------------------------------------
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402    use crate::protocol::programming::TOTAL_SIZE;
403    use crate::types::Frequency;
404
405    /// Create a test image with known channel data.
406    fn make_test_image() -> Vec<u8> {
407        let mut image = vec![0xFF_u8; TOTAL_SIZE];
408
409        // Zero out the names region (real radio uses null bytes for empty names).
410        let names_end = NAMES_OFFSET + TOTAL_ENTRIES * NAME_ENTRY_SIZE;
411        image[NAMES_OFFSET..names_end].fill(0x00);
412
413        // Set up channel 0 as a used VHF channel.
414        // Flag at 0x2000: [0x00 (VHF), 0x00 (no lockout), 0x00 (group 0), 0xFF]
415        image[0x2000] = 0x00; // used = VHF
416        image[0x2001] = 0x00; // no lockout
417        image[0x2002] = 0x00; // group 0
418        image[0x2003] = 0xFF;
419
420        // Channel 0 data at memgroup 0, slot 0 = offset 0x4000.
421        // Write a valid 40-byte channel record with 146.520 MHz.
422        let freq: u32 = 146_520_000;
423        let freq_bytes = freq.to_le_bytes();
424        image[0x4000..0x4004].copy_from_slice(&freq_bytes);
425        // TX offset = 0
426        image[0x4004..0x4008].copy_from_slice(&[0, 0, 0, 0]);
427        // Step size 0 (5 kHz) | shift 0 (simplex)
428        image[0x4008] = 0x00;
429        // Mode/flags byte 0x09: all zero (FM, no reverse, no tone, CTCSS off)
430        image[0x4009] = 0x00;
431        // Byte 0x0A: DCS off, etc.
432        image[0x400A] = 0x00;
433        // Tone/CTCSS/DCS indices
434        image[0x400B] = 0x00;
435        image[0x400C] = 0x00;
436        image[0x400D] = 0x00;
437        // Data speed / lockout
438        image[0x400E] = 0x00;
439        // URCALL: 24 bytes of zeros (empty callsign)
440        // data_mode
441        image[0x4027] = 0x00;
442
443        // Channel 0 name at 0x10000: "2M CALL"
444        let name = b"2M CALL\0\0\0\0\0\0\0\0\0";
445        image[0x10000..0x10010].copy_from_slice(name);
446
447        // Set up channel 1 as empty (default 0xFF in flags is already there).
448
449        // Set up channel 5 as used UHF (to test crossing memgroup boundary
450        // -- ch 5 is still in memgroup 0, slot 5).
451        image[0x2000 + 5 * 4] = 0x02; // used = UHF
452        image[0x2000 + 5 * 4 + 1] = 0x01; // lockout = yes
453        image[0x2000 + 5 * 4 + 2] = 0x03; // group 3
454        image[0x2000 + 5 * 4 + 3] = 0xFF;
455
456        // Channel 5 data at memgroup 0, slot 5 = offset 0x4000 + 5 * 40 = 0x40C8.
457        let ch5_freq: u32 = 446_000_000;
458        let ch5_freq_bytes = ch5_freq.to_le_bytes();
459        image[0x40C8..0x40CC].copy_from_slice(&ch5_freq_bytes);
460        image[0x40CC..0x40D0].copy_from_slice(&[0, 0, 0, 0]);
461        image[0x40D0] = 0x00;
462        image[0x40D1] = 0x00;
463        image[0x40D2] = 0x00;
464        image[0x40D3] = 0x00;
465        image[0x40D4] = 0x00;
466        image[0x40D5] = 0x00;
467        image[0x40D6] = 0x00;
468        image[0x40EF] = 0x00;
469
470        // Channel 5 name.
471        let name5 = b"UHF CHAN\0\0\0\0\0\0\0\0";
472        image[0x10000 + 5 * 16..0x10000 + 5 * 16 + 16].copy_from_slice(name5);
473
474        image
475    }
476
477    #[test]
478    fn from_raw_valid_size() {
479        let image = vec![0u8; TOTAL_SIZE];
480        assert!(super::super::MemoryImage::from_raw(image).is_ok());
481    }
482
483    #[test]
484    fn from_raw_invalid_size() {
485        let image = vec![0u8; 1000];
486        let err = super::super::MemoryImage::from_raw(image).unwrap_err();
487        assert!(matches!(err, MemoryError::InvalidSize { .. }));
488    }
489
490    #[test]
491    fn channel_is_used() {
492        let image = make_test_image();
493        let mi = super::super::MemoryImage::from_raw(image).unwrap();
494        let ch = mi.channels();
495        assert!(ch.is_used(0));
496        assert!(!ch.is_used(1));
497        assert!(ch.is_used(5));
498    }
499
500    #[test]
501    fn channel_count() {
502        let image = make_test_image();
503        let mi = super::super::MemoryImage::from_raw(image).unwrap();
504        let ch = mi.channels();
505        assert_eq!(ch.count(), 2); // channels 0 and 5
506    }
507
508    #[test]
509    fn channel_get_name() {
510        let image = make_test_image();
511        let mi = super::super::MemoryImage::from_raw(image).unwrap();
512        let ch = mi.channels();
513        assert_eq!(ch.name(0), "2M CALL");
514        assert_eq!(ch.name(5), "UHF CHAN");
515        assert_eq!(ch.name(1), ""); // empty channel
516    }
517
518    #[test]
519    fn channel_get_entry() {
520        let image = make_test_image();
521        let mi = super::super::MemoryImage::from_raw(image).unwrap();
522        let ch = mi.channels();
523
524        let entry0 = ch.get(0).unwrap();
525        assert!(entry0.used);
526        assert!(!entry0.lockout);
527        assert_eq!(entry0.name, "2M CALL");
528        assert_eq!(entry0.flash.rx_frequency.as_hz(), 146_520_000);
529
530        let entry5 = ch.get(5).unwrap();
531        assert!(entry5.used);
532        assert!(entry5.lockout);
533        assert_eq!(entry5.name, "UHF CHAN");
534        assert_eq!(entry5.flash.rx_frequency.as_hz(), 446_000_000);
535    }
536
537    #[test]
538    fn channel_get_out_of_range() {
539        let image = make_test_image();
540        let mi = super::super::MemoryImage::from_raw(image).unwrap();
541        let ch = mi.channels();
542        assert!(ch.get(1200).is_none());
543    }
544
545    #[test]
546    fn channel_all_returns_only_used() {
547        let image = make_test_image();
548        let mi = super::super::MemoryImage::from_raw(image).unwrap();
549        let ch = mi.channels();
550        let all = ch.all();
551        assert_eq!(all.len(), 2);
552        assert_eq!(all[0].number, 0);
553        assert_eq!(all[1].number, 5);
554    }
555
556    #[test]
557    fn channel_flag() {
558        let image = make_test_image();
559        let mi = super::super::MemoryImage::from_raw(image).unwrap();
560        let ch = mi.channels();
561
562        let flag0 = ch.flag(0).unwrap();
563        assert_eq!(flag0.used, 0x00); // VHF
564        assert!(!flag0.lockout);
565        assert_eq!(flag0.group, 0);
566
567        let flag5 = ch.flag(5).unwrap();
568        assert_eq!(flag5.used, 0x02); // UHF
569        assert!(flag5.lockout);
570        assert_eq!(flag5.group, 3);
571    }
572
573    #[test]
574    fn channel_group_names() {
575        let mut image = make_test_image();
576        // Write a group name at index 1152 (group 0).
577        let name = b"Ham Radio\0\0\0\0\0\0\0";
578        let offset = 0x10000 + 1152 * 16;
579        image[offset..offset + 16].copy_from_slice(name);
580
581        let mi = super::super::MemoryImage::from_raw(image).unwrap();
582        let ch = mi.channels();
583        assert_eq!(ch.group_name(0), "Ham Radio");
584        assert_eq!(ch.group_name(1), ""); // no name set
585    }
586
587    #[test]
588    fn channel_writer_set() {
589        let image = make_test_image();
590        let mut mi = super::super::MemoryImage::from_raw(image).unwrap();
591
592        let entry = ChannelEntry {
593            number: 10,
594            name: "TEST CH".to_owned(),
595            flash: FlashChannel {
596                rx_frequency: Frequency::new(145_000_000),
597                ..FlashChannel::default()
598            },
599            used: true,
600            lockout: false,
601        };
602
603        {
604            let mut writer = ChannelWriter::new(mi.as_raw_mut());
605            writer.set(&entry).unwrap();
606        }
607
608        let ch = mi.channels();
609        assert!(ch.is_used(10));
610        let read_back = ch.get(10).unwrap();
611        assert!(read_back.used);
612        assert_eq!(read_back.name, "TEST CH");
613        assert_eq!(read_back.flash.rx_frequency.as_hz(), 145_000_000);
614    }
615
616    #[test]
617    fn channel_writer_group_name() {
618        let image = make_test_image();
619        let mut mi = super::super::MemoryImage::from_raw(image).unwrap();
620
621        {
622            let mut writer = ChannelWriter::new(mi.as_raw_mut());
623            writer.set_group_name(0, "My Group").unwrap();
624        }
625
626        let ch = mi.channels();
627        assert_eq!(ch.group_name(0), "My Group");
628    }
629
630    #[test]
631    fn channel_writer_out_of_range() {
632        let image = make_test_image();
633        let mut mi = super::super::MemoryImage::from_raw(image).unwrap();
634
635        let entry = ChannelEntry {
636            number: 1200,
637            name: String::new(),
638            flash: FlashChannel::default(),
639            used: false,
640            lockout: false,
641        };
642
643        let mut writer = ChannelWriter::new(mi.as_raw_mut());
644        let result = writer.set(&entry);
645        assert!(result.is_err());
646    }
647}