kenwood_thd75/radio/
programming.rs

1//! Programming mode access for full radio memory read/write.
2//!
3//! The TH-D75 stores all radio configuration in a 500,480-byte flash
4//! memory (1,955 pages of 256 bytes), accessible only via the binary
5//! programming protocol (`0M PROGRAM`). This module provides methods to
6//! read and write individual pages, memory regions, or the entire image.
7//!
8//! # Protocol
9//!
10//! By default the entire programming session runs at 9600 baud -- no
11//! baud rate switching. This is the safe, proven approach. Switching to
12//! 57600 baud after entry crashes the radio into MCP error mode.
13//!
14//! An optional [`McpSpeed::Fast`] mode switches the serial port to
15//! 115200 baud after the initial handshake (~8 seconds for a full dump
16//! instead of ~55 seconds). Enable it with [`Radio::set_mcp_speed`].
17//!
18//! # Warning
19//!
20//! Entering programming mode makes the radio stop responding to normal
21//! CAT commands. The display shows "PROG MCP". Always call
22//! `exit_programming_mode` when done,
23//! even on error. The high-level methods handle entry/exit automatically.
24//!
25//! # Connection Lifetime
26//!
27//! The USB connection does not survive the programming mode transition.
28//! The radio's USB stack resets when exiting MCP mode. After calling
29//! any method in this module, the `Radio` instance should be dropped
30//! and a fresh connection established for subsequent CAT commands.
31//!
32//! # Safety
33//!
34//! The last 2 pages (1953-1954) contain factory calibration data and are
35//! **never** written by this library. Attempts to write these pages return
36//! [`Error::MemoryWriteProtected`].
37//!
38//! The `0M` handler is at firmware address `0xC002F01C`.
39
40use crate::error::{Error, ProtocolError, TransportError};
41use crate::protocol::programming::{self, ChannelFlag};
42use crate::transport::Transport;
43use crate::types::FlashChannel;
44
45use super::Radio;
46
47/// Baud rate for the programming mode handshake.
48///
49/// The `0M PROGRAM\r` entry command is always sent at 9600 baud.
50/// The data transfer phase may stay at 9600 or switch to 115200
51/// depending on the configured [`McpSpeed`].
52const PROGRAMMING_BAUD: u32 = 9600;
53
54/// Baud rate for fast MCP transfers.
55const FAST_TRANSFER_BAUD: u32 = 115_200;
56
57/// MCP transfer speed options.
58///
59/// The default (`Safe`) keeps the entire programming session at 9600
60/// baud, which is proven reliable across all platforms. The `Fast`
61/// option switches the serial port to 115200 baud after the initial
62/// handshake for faster transfers.
63///
64/// # Caution
65///
66/// `Fast` mode has not been tested on all USB host controllers and
67/// operating systems. If you experience transfer errors, fall back to
68/// `Safe` mode. The 57600 baud switch is known to crash the radio
69/// and is **not** offered as an option.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum McpSpeed {
72    /// 9600 baud throughout (proven reliable, ~55 s for full dump).
73    #[default]
74    Safe,
75    /// 115200 baud for the binary transfer phase (~8 s for full dump).
76    ///
77    /// After the `0M PROGRAM` handshake at 9600 baud, the serial port
78    /// is switched to 115200 baud. A sync byte is read and discarded.
79    /// On exit the baud rate is restored.
80    Fast,
81}
82
83/// Timeout for a full memory dump.
84///
85/// At 9600 baud: 1955 pages x 261 bytes x 10 bits/byte / 9600 bps ~ 53 s.
86/// At 115200 baud: the same transfer takes ~ 4.4 s.
87/// The 120-second ceiling provides ample margin for both modes.
88const FULL_DUMP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(120);
89
90impl<T: Transport> Radio<T> {
91    // -----------------------------------------------------------------------
92    // High-level: full memory image
93    // -----------------------------------------------------------------------
94
95    /// Read the entire radio memory image (500,480 bytes).
96    ///
97    /// Enters programming mode, reads all 1,955 pages, and exits.
98    /// This takes approximately 55 seconds at 9600 baud.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if entry, any page read, or exit fails.
103    /// Programming mode is always exited, even on error.
104    pub async fn read_memory_image(&mut self) -> Result<Vec<u8>, Error> {
105        self.read_memory_image_with_progress(|_, _| {}).await
106    }
107
108    /// Read the entire radio memory image with a progress callback.
109    ///
110    /// The callback receives `(current_page, total_pages)` after each
111    /// page is read, allowing progress display for the ~55-second dump.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if entry, any page read, or exit fails.
116    /// Programming mode is always exited, even on error.
117    pub async fn read_memory_image_with_progress<F>(
118        &mut self,
119        mut on_progress: F,
120    ) -> Result<Vec<u8>, Error>
121    where
122        F: FnMut(u16, u16),
123    {
124        let saved_timeout = self.timeout;
125        self.timeout = FULL_DUMP_TIMEOUT;
126
127        self.enter_programming_mode().await?;
128
129        let result = self
130            .read_pages_raw(0, programming::TOTAL_PAGES, &mut on_progress)
131            .await;
132
133        let exit_result = self.exit_programming_mode().await;
134        self.timeout = saved_timeout;
135
136        let image = result?;
137        exit_result?;
138
139        Ok(image)
140    }
141
142    /// Write a complete memory image back to the radio.
143    ///
144    /// **WARNING:** This overwrites ALL radio settings except factory
145    /// calibration (last 2 pages). The image must be exactly 500,480 bytes.
146    ///
147    /// # Errors
148    ///
149    /// Returns [`Error::InvalidImageSize`] if the image is the wrong size.
150    /// Returns an error if entry, any page write, or exit fails.
151    /// Programming mode is always exited, even on error.
152    pub async fn write_memory_image(&mut self, image: &[u8]) -> Result<(), Error> {
153        self.write_memory_image_with_progress(image, |_, _| {})
154            .await
155    }
156
157    /// Write a complete memory image with a progress callback.
158    ///
159    /// The callback receives `(current_page, total_pages)` after each
160    /// page is written.
161    ///
162    /// # Errors
163    ///
164    /// Returns [`Error::InvalidImageSize`] if the image is the wrong size.
165    /// Returns an error if entry, any page write, or exit fails.
166    /// Programming mode is always exited, even on error.
167    pub async fn write_memory_image_with_progress<F>(
168        &mut self,
169        image: &[u8],
170        mut on_progress: F,
171    ) -> Result<(), Error>
172    where
173        F: FnMut(u16, u16),
174    {
175        if image.len() != programming::TOTAL_SIZE {
176            return Err(Error::InvalidImageSize {
177                actual: image.len(),
178                expected: programming::TOTAL_SIZE,
179            });
180        }
181
182        let saved_timeout = self.timeout;
183        self.timeout = FULL_DUMP_TIMEOUT;
184
185        self.enter_programming_mode().await?;
186
187        // Write all pages except factory calibration (last 2).
188        let writable_pages = programming::TOTAL_PAGES - programming::FACTORY_CAL_PAGES;
189        let result = self
190            .write_pages_raw(
191                0,
192                &image[..writable_pages as usize * programming::PAGE_SIZE],
193                &mut on_progress,
194            )
195            .await;
196
197        let exit_result = self.exit_programming_mode().await;
198        self.timeout = saved_timeout;
199
200        result?;
201        exit_result?;
202
203        Ok(())
204    }
205
206    // -----------------------------------------------------------------------
207    // High-level: page range read/write
208    // -----------------------------------------------------------------------
209
210    /// Read a range of pages from radio memory.
211    ///
212    /// Enters programming mode, reads `count` pages starting at
213    /// `start_page`, and exits. Returns the raw bytes.
214    ///
215    /// # Errors
216    ///
217    /// Returns an error if entry, any page read, or exit fails.
218    /// Programming mode is always exited, even on error.
219    pub async fn read_memory_pages(
220        &mut self,
221        start_page: u16,
222        count: u16,
223    ) -> Result<Vec<u8>, Error> {
224        self.enter_programming_mode().await?;
225
226        let result = self.read_pages_raw(start_page, count, &mut |_, _| {}).await;
227
228        let exit_result = self.exit_programming_mode().await;
229
230        let data = result?;
231        exit_result?;
232
233        Ok(data)
234    }
235
236    /// Write a range of pages to radio memory.
237    ///
238    /// Enters programming mode, writes pages starting at `start_page`
239    /// with the provided data, and exits. The data length must be a
240    /// multiple of 256 (one or more full pages).
241    ///
242    /// # Errors
243    ///
244    /// Returns [`Error::MemoryWriteProtected`] if any target page falls
245    /// within the factory calibration region.
246    /// Returns an error if entry, any page write, or exit fails.
247    /// Programming mode is always exited, even on error.
248    pub async fn write_memory_pages(&mut self, start_page: u16, data: &[u8]) -> Result<(), Error> {
249        let page_count = data.len() / programming::PAGE_SIZE;
250        // Validate no factory calibration pages are in range.
251        for i in 0..page_count {
252            // page_count is bounded by data.len() / 256, which fits in u16
253            // because the maximum image is 500,480 bytes (1955 pages).
254            #[allow(clippy::cast_possible_truncation)]
255            let offset = i as u16;
256            let page = start_page + offset;
257            if programming::is_factory_calibration_page(page) {
258                return Err(Error::MemoryWriteProtected { page });
259            }
260        }
261
262        self.enter_programming_mode().await?;
263
264        let result = self.write_pages_raw(start_page, data, &mut |_, _| {}).await;
265
266        let exit_result = self.exit_programming_mode().await;
267
268        result?;
269        exit_result?;
270
271        Ok(())
272    }
273
274    // -----------------------------------------------------------------------
275    // High-level: single page read/write
276    // -----------------------------------------------------------------------
277
278    /// Read a single memory page (256 bytes).
279    ///
280    /// Enters programming mode, reads the page, and exits.
281    ///
282    /// # Errors
283    ///
284    /// Returns an error if entry, the page read, or exit fails.
285    /// Programming mode is always exited, even on error.
286    pub async fn read_page(&mut self, page: u16) -> Result<[u8; programming::PAGE_SIZE], Error> {
287        self.enter_programming_mode().await?;
288
289        let result = self.read_single_page(page).await;
290
291        let exit_result = self.exit_programming_mode().await;
292
293        let data = result?;
294        exit_result?;
295
296        Ok(data)
297    }
298
299    /// Write a single memory page (256 bytes).
300    ///
301    /// Enters programming mode, writes the page, and exits.
302    ///
303    /// # Errors
304    ///
305    /// Returns [`Error::MemoryWriteProtected`] if the page is in the
306    /// factory calibration region.
307    /// Returns an error if entry, the page write, or exit fails.
308    /// Programming mode is always exited, even on error.
309    pub async fn write_page(
310        &mut self,
311        page: u16,
312        data: &[u8; programming::PAGE_SIZE],
313    ) -> Result<(), Error> {
314        if programming::is_factory_calibration_page(page) {
315            return Err(Error::MemoryWriteProtected { page });
316        }
317
318        self.enter_programming_mode().await?;
319
320        let result = self.write_single_page(page, data).await;
321
322        let exit_result = self.exit_programming_mode().await;
323
324        result?;
325        exit_result?;
326
327        Ok(())
328    }
329
330    // -----------------------------------------------------------------------
331    // High-level: read-modify-write
332    // -----------------------------------------------------------------------
333
334    /// Read a memory page, apply in-place modifications, and write it back
335    /// in a single MCP programming session.
336    ///
337    /// This is the key primitive for changing individual settings via MCP
338    /// without reading or writing the entire 500 KB image. The three steps
339    /// (read, modify, write) happen inside one programming mode session so
340    /// the radio only enters and exits MCP mode once.
341    ///
342    /// # Connection lifetime
343    ///
344    /// The USB connection does not survive the programming mode transition.
345    /// After this method returns, the `Radio` instance should be dropped
346    /// and a fresh connection established for subsequent CAT commands.
347    ///
348    /// # Errors
349    ///
350    /// Returns [`Error::MemoryWriteProtected`] if the page is in the
351    /// factory calibration region.
352    /// Returns an error if entry, the page read, the page write, or exit
353    /// fails. Programming mode is always exited, even on error.
354    pub async fn modify_memory_page<F>(&mut self, page: u16, modify: F) -> Result<(), Error>
355    where
356        F: FnOnce(&mut [u8; programming::PAGE_SIZE]),
357    {
358        if programming::is_factory_calibration_page(page) {
359            return Err(Error::MemoryWriteProtected { page });
360        }
361
362        self.enter_programming_mode().await?;
363
364        let result: Result<(), Error> = async {
365            // Read the current page contents.
366            let mut page_data = self.read_single_page(page).await?;
367
368            // Apply the caller's modifications in place.
369            modify(&mut page_data);
370
371            // Write the modified page back.
372            self.write_single_page(page, &page_data).await?;
373
374            Ok(())
375        }
376        .await;
377
378        // Always exit programming mode, even on error.
379        let exit_result = self.exit_programming_mode().await;
380
381        result?;
382        exit_result?;
383
384        Ok(())
385    }
386
387    // -----------------------------------------------------------------------
388    // High-level: structured data accessors
389    // -----------------------------------------------------------------------
390
391    /// Read all channel display names from the radio.
392    ///
393    /// This enters programming mode, reads the channel name memory pages,
394    /// and exits programming mode. The radio will briefly show "PROG MCP"
395    /// on its display during this operation.
396    ///
397    /// Returns a `Vec` of up to 1,000 channel names indexed by channel
398    /// number. Channels without a user-assigned name are returned as
399    /// empty strings.
400    ///
401    /// # Errors
402    ///
403    /// Returns an error if the radio fails to enter programming mode,
404    /// if a page read fails, or if the connection is lost. On error, an
405    /// attempt is still made to exit programming mode before returning.
406    pub async fn read_channel_names(&mut self) -> Result<Vec<String>, Error> {
407        self.enter_programming_mode().await?;
408
409        let result = self.read_name_pages().await;
410
411        // Always attempt to exit, even if reading failed.
412        let exit_result = self.exit_programming_mode().await;
413
414        // Propagate the read error first, then the exit error.
415        let names = result?;
416        exit_result?;
417
418        Ok(names)
419    }
420
421    /// Read all 1,200 channel display names from the radio, including
422    /// extended entries (scan edges, WX, and call channels).
423    ///
424    /// This reads 75 pages (0x0100-0x014A) instead of the 63 pages read
425    /// by [`read_channel_names`](Self::read_channel_names), which only
426    /// returns the first 1,000 standard channel names.
427    ///
428    /// # Connection lifetime
429    ///
430    /// This enters MCP programming mode. The USB connection drops after
431    /// exit. The `Radio` instance should be dropped and a fresh connection
432    /// established for subsequent CAT commands.
433    ///
434    /// # Errors
435    ///
436    /// Returns an error if the radio fails to enter programming mode,
437    /// if a page read fails, or if the connection is lost. On error, an
438    /// attempt is still made to exit programming mode before returning.
439    pub async fn read_all_channel_names(&mut self) -> Result<Vec<String>, Error> {
440        self.enter_programming_mode().await?;
441
442        let result = self.read_all_name_pages().await;
443
444        let exit_result = self.exit_programming_mode().await;
445
446        let names = result?;
447        exit_result?;
448
449        Ok(names)
450    }
451
452    /// Write a single channel display name via MCP programming mode.
453    ///
454    /// Enters programming mode, reads the containing name page, modifies
455    /// the 16-byte slot for the given channel, writes the page back, and
456    /// exits. The name is truncated to 15 bytes (leaving room for a null
457    /// terminator) and null-padded to fill the 16-byte slot.
458    ///
459    /// # Connection lifetime
460    ///
461    /// This enters MCP programming mode. The USB connection drops after
462    /// exit. The `Radio` instance should be dropped and a fresh connection
463    /// established for subsequent CAT commands.
464    ///
465    /// # Errors
466    ///
467    /// Returns [`Error::Validation`] if the channel number is 1200 or greater.
468    /// Returns an error if entering programming mode, reading the page,
469    /// writing the page, or exiting programming mode fails.
470    pub async fn write_channel_name(&mut self, channel: u16, name: &str) -> Result<(), Error> {
471        // TOTAL_CHANNEL_ENTRIES is 1200, which fits in u16.
472        #[allow(clippy::cast_possible_truncation)]
473        const MAX_CHANNEL: u16 = programming::TOTAL_CHANNEL_ENTRIES as u16;
474        if channel >= MAX_CHANNEL {
475            return Err(Error::Validation(
476                crate::error::ValidationError::ChannelOutOfRange {
477                    channel,
478                    max: MAX_CHANNEL - 1,
479                },
480            ));
481        }
482        let page = programming::CHANNEL_NAMES_START + (channel / 16);
483        let offset = (channel % 16) as usize * programming::NAME_ENTRY_SIZE;
484
485        tracing::info!(channel, name, page, offset, "writing channel name via MCP");
486        self.modify_memory_page(page, |data| {
487            // Clear the 16-byte slot.
488            data[offset..offset + programming::NAME_ENTRY_SIZE].fill(0);
489            // Write the name (truncated to 15 bytes, leaving null terminator).
490            let name_bytes = name.as_bytes();
491            let len = name_bytes.len().min(programming::NAME_ENTRY_SIZE - 1);
492            data[offset..offset + len].copy_from_slice(&name_bytes[..len]);
493        })
494        .await
495    }
496
497    /// Read channel flags for all 1,200 channel entries.
498    ///
499    /// Each flag indicates whether a channel slot is used (and which band),
500    /// whether it is locked out from scanning, and its group assignment.
501    ///
502    /// # Errors
503    ///
504    /// Returns an error if entry, any page read, or exit fails.
505    /// Programming mode is always exited, even on error.
506    pub async fn read_channel_flags(&mut self) -> Result<Vec<ChannelFlag>, Error> {
507        self.enter_programming_mode().await?;
508
509        let page_count = programming::CHANNEL_FLAGS_END - programming::CHANNEL_FLAGS_START + 1;
510        let result = self
511            .read_pages_raw(programming::CHANNEL_FLAGS_START, page_count, &mut |_, _| {})
512            .await;
513
514        let exit_result = self.exit_programming_mode().await;
515
516        let raw = result?;
517        exit_result?;
518
519        // Parse 4-byte flag records, 1200 entries.
520        let mut flags = Vec::with_capacity(programming::TOTAL_CHANNEL_ENTRIES);
521        for i in 0..programming::TOTAL_CHANNEL_ENTRIES {
522            let offset = i * programming::FLAG_RECORD_SIZE;
523            if offset + programming::FLAG_RECORD_SIZE <= raw.len()
524                && let Some(flag) = programming::parse_channel_flag(&raw[offset..])
525            {
526                flags.push(flag);
527            }
528        }
529
530        tracing::info!(count = flags.len(), "channel flags read");
531        Ok(flags)
532    }
533
534    /// Read all channel memory data (frequencies, modes, tones, etc.)
535    /// for all 1,200 channel entries.
536    ///
537    /// Channels whose flag indicates empty (`0xFF`) will still be returned
538    /// with whatever data is in the slot (typically zeroed). Check the
539    /// corresponding [`ChannelFlag`] to determine which slots are in use.
540    ///
541    /// # Errors
542    ///
543    /// Returns an error if entry, any page read, or exit fails.
544    /// Programming mode is always exited, even on error.
545    pub async fn read_all_channels(&mut self) -> Result<Vec<FlashChannel>, Error> {
546        self.enter_programming_mode().await?;
547
548        let page_count = programming::CHANNEL_DATA_END - programming::CHANNEL_DATA_START + 1;
549        let result = self
550            .read_pages_raw(programming::CHANNEL_DATA_START, page_count, &mut |_, _| {})
551            .await;
552
553        let exit_result = self.exit_programming_mode().await;
554
555        let raw = result?;
556        exit_result?;
557
558        // Parse memgroups: each 256-byte page is one memgroup containing
559        // 6 channel records of 40 bytes + 16 bytes padding.
560        let mut channels = Vec::with_capacity(programming::TOTAL_CHANNEL_ENTRIES);
561        for memgroup_idx in 0..programming::MEMGROUP_COUNT {
562            let group_offset = memgroup_idx * programming::PAGE_SIZE;
563            for slot in 0..programming::CHANNELS_PER_MEMGROUP {
564                let ch_offset = group_offset + slot * programming::CHANNEL_RECORD_SIZE;
565                if ch_offset + programming::CHANNEL_RECORD_SIZE <= raw.len() {
566                    match FlashChannel::from_bytes(&raw[ch_offset..]) {
567                        Ok(ch) => channels.push(ch),
568                        Err(e) => {
569                            tracing::warn!(
570                                memgroup = memgroup_idx,
571                                slot,
572                                error = %e,
573                                "failed to parse flash channel record, using default"
574                            );
575                            channels.push(FlashChannel::default());
576                        }
577                    }
578                }
579            }
580        }
581
582        tracing::info!(count = channels.len(), "channel memory records read");
583        Ok(channels)
584    }
585
586    // -----------------------------------------------------------------------
587    // High-level: typed memory image
588    // -----------------------------------------------------------------------
589
590    /// Read and parse the full radio configuration.
591    ///
592    /// Reads the entire 500,480-byte memory image and returns a
593    /// [`crate::memory::MemoryImage`] with typed access to all settings, channels,
594    /// and subsystem configurations.
595    ///
596    /// This takes approximately 55 seconds at 9600 baud.
597    ///
598    /// # Errors
599    ///
600    /// Returns an error if the read fails. Programming mode is always
601    /// exited, even on error.
602    pub async fn read_configuration(&mut self) -> Result<crate::memory::MemoryImage, Error> {
603        let raw = self.read_memory_image().await?;
604        crate::memory::MemoryImage::from_raw(raw).map_err(|e| {
605            Error::Protocol(ProtocolError::FieldParse {
606                command: "read_configuration".into(),
607                field: "memory_image".into(),
608                detail: e.to_string(),
609            })
610        })
611    }
612
613    /// Read and parse the full radio configuration with progress.
614    ///
615    /// The callback receives `(current_page, total_pages)` after each
616    /// page is read.
617    ///
618    /// # Errors
619    ///
620    /// Returns an error if the read fails. Programming mode is always
621    /// exited, even on error.
622    pub async fn read_configuration_with_progress<F>(
623        &mut self,
624        on_progress: F,
625    ) -> Result<crate::memory::MemoryImage, Error>
626    where
627        F: FnMut(u16, u16),
628    {
629        let raw = self.read_memory_image_with_progress(on_progress).await?;
630        crate::memory::MemoryImage::from_raw(raw).map_err(|e| {
631            Error::Protocol(ProtocolError::FieldParse {
632                command: "read_configuration".into(),
633                field: "memory_image".into(),
634                detail: e.to_string(),
635            })
636        })
637    }
638
639    /// Write a full radio configuration back to the radio.
640    ///
641    /// Takes a [`crate::memory::MemoryImage`] (possibly modified via its typed
642    /// accessors) and writes it to the radio's flash memory.
643    ///
644    /// # Errors
645    ///
646    /// Returns an error if the write fails. Programming mode is always
647    /// exited, even on error.
648    pub async fn write_configuration(
649        &mut self,
650        image: &crate::memory::MemoryImage,
651    ) -> Result<(), Error> {
652        self.write_memory_image(image.as_raw()).await
653    }
654
655    /// Write a full radio configuration with progress.
656    ///
657    /// The callback receives `(current_page, total_pages)` after each
658    /// page is written.
659    ///
660    /// # Errors
661    ///
662    /// Returns an error if the write fails. Programming mode is always
663    /// exited, even on error.
664    pub async fn write_configuration_with_progress<F>(
665        &mut self,
666        image: &crate::memory::MemoryImage,
667        on_progress: F,
668    ) -> Result<(), Error>
669    where
670        F: FnMut(u16, u16),
671    {
672        self.write_memory_image_with_progress(image.as_raw(), on_progress)
673            .await
674    }
675
676    // -----------------------------------------------------------------------
677    // Internal: programming mode entry/exit
678    // -----------------------------------------------------------------------
679
680    /// Enter programming mode (`0M PROGRAM`).
681    ///
682    /// Switches to 9600 baud and sends `0M PROGRAM\r`. The radio
683    /// responds with `0M\r` and enters MCP mode. The session stays
684    /// at 9600 baud for all subsequent R/W/ACK exchanges.
685    ///
686    /// The radio stops responding to normal CAT commands and displays
687    /// "PROG MCP" until [`exit_programming_mode`](Self::exit_programming_mode)
688    /// is called.
689    ///
690    /// # Errors
691    ///
692    /// Returns an error if the entry command fails or the radio does
693    /// not respond with the expected `0M\r` acknowledgement.
694    async fn enter_programming_mode(&mut self) -> Result<(), Error> {
695        tracing::info!("entering programming mode at 9600 baud");
696
697        // Switch to 9600 baud for the entire programming session.
698        self.transport
699            .set_baud_rate(PROGRAMMING_BAUD)
700            .map_err(Error::Transport)?;
701
702        self.transport
703            .write(programming::ENTER_PROGRAMMING)
704            .await
705            .map_err(Error::Transport)?;
706
707        // 10ms delay after write.
708        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
709
710        // Read response -- expect "0M\r" (3 bytes).
711        let mut buf = [0u8; 64];
712        let mut received = Vec::new();
713
714        let result = tokio::time::timeout(self.timeout, async {
715            loop {
716                let n = self
717                    .transport
718                    .read(&mut buf)
719                    .await
720                    .map_err(Error::Transport)?;
721                if n == 0 {
722                    return Err(Error::Transport(TransportError::Disconnected(
723                        std::io::Error::new(
724                            std::io::ErrorKind::UnexpectedEof,
725                            "connection closed during programming mode entry",
726                        ),
727                    )));
728                }
729                received.extend_from_slice(&buf[..n]);
730                // Look for "0M\r" anywhere in the received data.
731                if received.windows(3).any(|w| w == b"0M\r") {
732                    return Ok(());
733                }
734                if received.len() > 20 {
735                    // Too much data without finding "0M\r".
736                    return Err(Error::Protocol(ProtocolError::UnexpectedResponse {
737                        expected: "0M\\r".to_string(),
738                        actual: received.clone(),
739                    }));
740                }
741            }
742        })
743        .await
744        .map_err(|_| Error::Timeout(self.timeout))?;
745        result?;
746
747        // If Fast mode is requested, switch to 115200 baud for the data
748        // transfer phase.
749        if self.mcp_speed == McpSpeed::Fast {
750            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
751            self.transport
752                .set_baud_rate(FAST_TRANSFER_BAUD)
753                .map_err(Error::Transport)?;
754            // Read sync byte — verifies the radio switched baud rates.
755            // If this times out, the radio is likely still at 9600 and all
756            // subsequent reads will produce garbage.
757            let mut sync = [0u8; 1];
758            match tokio::time::timeout(
759                std::time::Duration::from_secs(2),
760                self.transport.read(&mut sync),
761            )
762            .await
763            {
764                Ok(Ok(n)) if n > 0 => {
765                    tracing::info!(
766                        sync_byte = sync[0],
767                        "programming mode entered, switched to {FAST_TRANSFER_BAUD} baud (fast)"
768                    );
769                }
770                Ok(Ok(_)) => {
771                    tracing::error!("fast mode sync read returned 0 bytes — baud mismatch likely");
772                    return Err(Error::Protocol(ProtocolError::MalformedFrame(
773                        b"fast mode sync byte not received".to_vec(),
774                    )));
775                }
776                Ok(Err(e)) => {
777                    tracing::error!("fast mode sync read failed: {e}");
778                    return Err(Error::Transport(e));
779                }
780                Err(_) => {
781                    tracing::error!(
782                        "fast mode sync byte timed out — radio may not have switched baud"
783                    );
784                    return Err(Error::Timeout(std::time::Duration::from_secs(2)));
785                }
786            }
787        } else {
788            tracing::info!("programming mode entered, staying at {PROGRAMMING_BAUD} baud");
789        }
790
791        Ok(())
792    }
793
794    /// Exit programming mode (`E` command).
795    ///
796    /// Sends the exit byte. The radio resets its USB stack after exiting
797    /// MCP mode, so the connection should be considered dead after this.
798    ///
799    /// # Errors
800    ///
801    /// Returns an error if the exit byte cannot be written.
802    async fn exit_programming_mode(&mut self) -> Result<(), Error> {
803        tracing::info!("exiting programming mode");
804
805        self.transport
806            .write(&[programming::EXIT])
807            .await
808            .map_err(Error::Transport)?;
809
810        // Give the radio time to leave MCP mode and resume CAT.
811        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
812
813        // If we were in fast mode, restore the default baud rate.
814        if self.mcp_speed == McpSpeed::Fast {
815            if let Err(e) = self
816                .transport
817                .set_baud_rate(crate::transport::SerialTransport::DEFAULT_BAUD)
818            {
819                tracing::warn!("failed to restore baud rate after fast MCP exit: {e}");
820            }
821            tracing::info!("programming mode exited, restored default baud rate");
822        } else {
823            // Stay at 9600 baud -- changing baud rate via SET_LINE_CODING
824            // causes the USB CDC connection to drop on some platforms.
825            // CAT commands work at 9600 baud (CDC ACM ignores line coding).
826            tracing::info!("programming mode exited, staying at 9600 baud");
827        }
828
829        Ok(())
830    }
831
832    // -----------------------------------------------------------------------
833    // Internal: raw page I/O (caller must hold programming mode)
834    // -----------------------------------------------------------------------
835
836    /// Read a contiguous range of pages while already in programming mode.
837    ///
838    /// Returns a `Vec<u8>` containing `count * 256` bytes.
839    ///
840    /// If a page read times out, it is retried once before failing. This
841    /// improves reliability during long memory dumps where occasional
842    /// serial hiccups can occur.
843    async fn read_pages_raw<F>(
844        &mut self,
845        start_page: u16,
846        count: u16,
847        on_progress: &mut F,
848    ) -> Result<Vec<u8>, Error>
849    where
850        F: FnMut(u16, u16),
851    {
852        let mut image = Vec::with_capacity(count as usize * programming::PAGE_SIZE);
853
854        for i in 0..count {
855            let page = start_page + i;
856            let data = match self.read_single_page(page).await {
857                Ok(d) => d,
858                Err(Error::Timeout(_)) => {
859                    tracing::warn!(page, "page read timed out, retrying once");
860                    // Brief pause before retry to let the serial bus settle.
861                    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
862                    self.read_single_page(page).await?
863                }
864                Err(e) => return Err(e),
865            };
866            image.extend_from_slice(&data);
867            on_progress(i + 1, count);
868        }
869
870        Ok(image)
871    }
872
873    /// Write a contiguous range of pages while already in programming mode.
874    ///
875    /// `data.len()` must be a multiple of 256.
876    async fn write_pages_raw<F>(
877        &mut self,
878        start_page: u16,
879        data: &[u8],
880        on_progress: &mut F,
881    ) -> Result<(), Error>
882    where
883        F: FnMut(u16, u16),
884    {
885        let page_count = data.len() / programming::PAGE_SIZE;
886
887        for i in 0..page_count {
888            // page_count is bounded by TOTAL_PAGES (1955), which fits in u16.
889            #[allow(clippy::cast_possible_truncation)]
890            let page_offset = i as u16;
891            let page = start_page + page_offset;
892            let byte_offset = i * programming::PAGE_SIZE;
893            let page_data: &[u8; programming::PAGE_SIZE] = data
894                [byte_offset..byte_offset + programming::PAGE_SIZE]
895                .try_into()
896                .expect("slice is exactly PAGE_SIZE bytes");
897            self.write_single_page(page, page_data).await?;
898            #[allow(clippy::cast_possible_truncation)]
899            let total = page_count as u16;
900            on_progress(page_offset + 1, total);
901        }
902
903        Ok(())
904    }
905
906    /// Read a single 256-byte page (caller must be in programming mode).
907    async fn read_single_page(&mut self, page: u16) -> Result<[u8; programming::PAGE_SIZE], Error> {
908        let cmd = programming::build_read_command(page);
909
910        tracing::debug!(page, "reading page");
911
912        // Send R command (5 bytes).
913        self.transport.write(&cmd).await.map_err(Error::Transport)?;
914        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
915
916        // Read 261-byte W response (W + 4-byte addr + 256-byte data).
917        let mut received = Vec::with_capacity(programming::W_RESPONSE_SIZE);
918        let mut buf = [0u8; 512];
919        let result = tokio::time::timeout(self.timeout, async {
920            while received.len() < programming::W_RESPONSE_SIZE {
921                let n = self
922                    .transport
923                    .read(&mut buf)
924                    .await
925                    .map_err(Error::Transport)?;
926                if n == 0 {
927                    return Err(Error::Transport(TransportError::Disconnected(
928                        std::io::Error::new(
929                            std::io::ErrorKind::UnexpectedEof,
930                            "connection closed during page read",
931                        ),
932                    )));
933                }
934                received.extend_from_slice(&buf[..n]);
935            }
936            Ok(())
937        })
938        .await
939        .map_err(|_| Error::Timeout(self.timeout))?;
940        result?;
941
942        // Parse: W(1) + addr(4) + data(256).
943        let (_page_addr, data) = programming::parse_write_response(&received)
944            .map_err(|e| Error::Protocol(ProtocolError::MalformedFrame(e.into_bytes())))?;
945
946        // Copy into a fixed-size array.
947        let mut page_data = [0u8; programming::PAGE_SIZE];
948        page_data.copy_from_slice(data);
949
950        // Send ACK, read ACK back.
951        self.transport
952            .write(&[programming::ACK])
953            .await
954            .map_err(Error::Transport)?;
955        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
956        let mut ack_buf = [0u8; 1];
957        let _ = tokio::time::timeout(
958            std::time::Duration::from_millis(1000),
959            self.transport.read(&mut ack_buf),
960        )
961        .await;
962
963        Ok(page_data)
964    }
965
966    /// Write a single 256-byte page (caller must be in programming mode).
967    async fn write_single_page(
968        &mut self,
969        page: u16,
970        data: &[u8; programming::PAGE_SIZE],
971    ) -> Result<(), Error> {
972        if programming::is_factory_calibration_page(page) {
973            return Err(Error::MemoryWriteProtected { page });
974        }
975
976        let cmd = programming::build_write_command(page, data);
977
978        tracing::debug!(page, "writing page");
979
980        // Send W command (261 bytes).
981        self.transport.write(&cmd).await.map_err(Error::Transport)?;
982        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
983
984        // Read 1-byte ACK from radio.
985        let mut ack_buf = [0u8; 1];
986        let result = tokio::time::timeout(self.timeout, async {
987            let n = self
988                .transport
989                .read(&mut ack_buf)
990                .await
991                .map_err(Error::Transport)?;
992            if n == 0 {
993                return Err(Error::Transport(TransportError::Disconnected(
994                    std::io::Error::new(
995                        std::io::ErrorKind::UnexpectedEof,
996                        "connection closed waiting for write ACK",
997                    ),
998                )));
999            }
1000            Ok(())
1001        })
1002        .await
1003        .map_err(|_| Error::Timeout(self.timeout))?;
1004        result?;
1005
1006        if ack_buf[0] != programming::ACK {
1007            return Err(Error::WriteNotAcknowledged {
1008                page,
1009                got: ack_buf[0],
1010            });
1011        }
1012
1013        Ok(())
1014    }
1015
1016    // -----------------------------------------------------------------------
1017    // Internal: channel name page reading
1018    // -----------------------------------------------------------------------
1019
1020    /// Read all channel name pages from the radio while in programming mode.
1021    ///
1022    /// Iterates over 63 pages starting at [`NAME_START_PAGE`](programming::NAME_START_PAGE),
1023    /// extracting 16 names per page, and truncating to 1,000 channels.
1024    async fn read_name_pages(&mut self) -> Result<Vec<String>, Error> {
1025        let mut names = Vec::with_capacity(programming::MAX_CHANNELS);
1026
1027        for page_offset in 0..programming::NAME_PAGE_COUNT {
1028            let page = programming::NAME_START_PAGE + page_offset;
1029            let data = self.read_single_page(page).await?;
1030
1031            // Extract 16 names from the 256-byte page.
1032            for i in 0..programming::NAMES_PER_PAGE {
1033                let start = i * programming::NAME_ENTRY_SIZE;
1034                if start + programming::NAME_ENTRY_SIZE <= data.len() {
1035                    let name = programming::extract_name(
1036                        &data[start..start + programming::NAME_ENTRY_SIZE],
1037                    );
1038                    names.push(name);
1039                }
1040            }
1041
1042            // Stop once we have enough names.
1043            if names.len() >= programming::MAX_CHANNELS {
1044                names.truncate(programming::MAX_CHANNELS);
1045                break;
1046            }
1047        }
1048
1049        tracing::info!(count = names.len(), "channel names read");
1050        Ok(names)
1051    }
1052
1053    /// Read all 1,200 channel name entries from the radio while in programming mode.
1054    ///
1055    /// Iterates over 75 pages (0x0100-0x014A), extracting 16 names per page.
1056    async fn read_all_name_pages(&mut self) -> Result<Vec<String>, Error> {
1057        let mut names = Vec::with_capacity(programming::TOTAL_CHANNEL_ENTRIES);
1058
1059        for page_offset in 0..programming::NAME_ALL_PAGE_COUNT {
1060            let page = programming::NAME_START_PAGE + page_offset;
1061            let data = self.read_single_page(page).await?;
1062
1063            for i in 0..programming::NAMES_PER_PAGE {
1064                let start = i * programming::NAME_ENTRY_SIZE;
1065                if start + programming::NAME_ENTRY_SIZE <= data.len() {
1066                    let name = programming::extract_name(
1067                        &data[start..start + programming::NAME_ENTRY_SIZE],
1068                    );
1069                    names.push(name);
1070                }
1071            }
1072
1073            if names.len() >= programming::TOTAL_CHANNEL_ENTRIES {
1074                names.truncate(programming::TOTAL_CHANNEL_ENTRIES);
1075                break;
1076            }
1077        }
1078
1079        tracing::info!(
1080            count = names.len(),
1081            "all channel names read (including extended)"
1082        );
1083        Ok(names)
1084    }
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089    use crate::protocol::programming;
1090    use crate::radio::Radio;
1091    use crate::transport::MockTransport;
1092
1093    /// Build a mock 261-byte W response with the given page address and
1094    /// a 256-byte data payload.
1095    fn build_w_response(page: u16, data: &[u8]) -> Vec<u8> {
1096        assert_eq!(data.len(), 256, "W response payload must be 256 bytes");
1097        let addr = page.to_be_bytes();
1098        // W + 2-byte page + 0x00 0x00 + 256 data = 261 bytes.
1099        let mut resp = vec![b'W', addr[0], addr[1], 0x00, 0x00];
1100        resp.extend_from_slice(data);
1101        resp
1102    }
1103
1104    /// Build a 256-byte page payload with the given names in 16-byte slots.
1105    fn build_name_page(names: &[&str]) -> Vec<u8> {
1106        let mut data = vec![0u8; 256];
1107        for (i, name) in names.iter().enumerate().take(16) {
1108            let start = i * 16;
1109            let bytes = name.as_bytes();
1110            data[start..start + bytes.len()].copy_from_slice(bytes);
1111        }
1112        data
1113    }
1114
1115    #[tokio::test]
1116    async fn read_channel_names_full_sequence() {
1117        // Mock the full programming mode sequence at 9600 baud throughout:
1118        // enter -> 63 page R/W/ACK loops -> exit.
1119        let mut mock = MockTransport::new();
1120
1121        // Enter programming mode (no baud switch, no sync byte).
1122        mock.expect(b"0M PROGRAM\r", b"0M\r");
1123
1124        // First page (256): has real names in slots 0-3.
1125        let first_page_data = build_name_page(&["ForestCityPD", "RPT1", "", "NOAA WX"]);
1126        let read_cmd = programming::build_read_command(256);
1127        mock.expect(&read_cmd, &build_w_response(256, &first_page_data));
1128
1129        // ACK exchange after first page, then remaining 62 pages.
1130        for page_offset in 1..programming::NAME_PAGE_COUNT {
1131            mock.expect(&[programming::ACK], &[programming::ACK]);
1132
1133            let page = programming::NAME_START_PAGE + page_offset;
1134            let cmd = programming::build_read_command(page);
1135            let empty = vec![0u8; 256];
1136            mock.expect(&cmd, &build_w_response(page, &empty));
1137        }
1138
1139        // Final ACK after last page.
1140        mock.expect(&[programming::ACK], &[programming::ACK]);
1141
1142        // Exit programming mode.
1143        mock.expect(b"E", &[]);
1144
1145        let mut radio = Radio::connect(mock).await.unwrap();
1146        let names = radio.read_channel_names().await.unwrap();
1147
1148        // 16 names per page * 63 pages = 1008, truncated to 1000.
1149        assert_eq!(names.len(), 1000);
1150        assert_eq!(names[0], "ForestCityPD");
1151        assert_eq!(names[1], "RPT1");
1152        assert_eq!(names[2], "");
1153        assert_eq!(names[3], "NOAA WX");
1154        for name in &names[4..16] {
1155            assert!(name.is_empty());
1156        }
1157    }
1158
1159    #[tokio::test]
1160    async fn read_single_page_round_trip() {
1161        let mut mock = MockTransport::new();
1162
1163        // Enter.
1164        mock.expect(b"0M PROGRAM\r", b"0M\r");
1165
1166        // Read page 0x0020.
1167        let page: u16 = 0x0020;
1168        let mut page_data = vec![0xABu8; 256];
1169        page_data[0] = 0x00; // VHF flag
1170        let cmd = programming::build_read_command(page);
1171        mock.expect(&cmd, &build_w_response(page, &page_data));
1172
1173        // ACK exchange.
1174        mock.expect(&[programming::ACK], &[programming::ACK]);
1175
1176        // Exit.
1177        mock.expect(b"E", &[]);
1178
1179        let mut radio = Radio::connect(mock).await.unwrap();
1180        let result = radio.read_page(page).await.unwrap();
1181        assert_eq!(result[0], 0x00);
1182        assert_eq!(result[1], 0xAB);
1183    }
1184
1185    #[tokio::test]
1186    async fn write_single_page_round_trip() {
1187        let mut mock = MockTransport::new();
1188
1189        // Enter.
1190        mock.expect(b"0M PROGRAM\r", b"0M\r");
1191
1192        // Write page 0x0100.
1193        let page: u16 = 0x0100;
1194        let page_data = [0xCDu8; 256];
1195        let write_cmd = programming::build_write_command(page, &page_data);
1196        mock.expect(&write_cmd, &[programming::ACK]);
1197
1198        // Exit.
1199        mock.expect(b"E", &[]);
1200
1201        let mut radio = Radio::connect(mock).await.unwrap();
1202        radio.write_page(page, &page_data).await.unwrap();
1203    }
1204
1205    #[tokio::test]
1206    async fn write_factory_cal_page_rejected() {
1207        let mock = MockTransport::new();
1208        let mut radio = Radio::connect(mock).await.unwrap();
1209
1210        let data = [0u8; 256];
1211        let result = radio.write_page(0x07A1, &data).await;
1212        assert!(result.is_err());
1213        let err = result.unwrap_err();
1214        assert!(
1215            err.to_string().contains("protected"),
1216            "error should mention protected: {err}"
1217        );
1218    }
1219
1220    #[tokio::test]
1221    async fn write_memory_image_wrong_size_rejected() {
1222        let mock = MockTransport::new();
1223        let mut radio = Radio::connect(mock).await.unwrap();
1224
1225        let bad_image = vec![0u8; 1000]; // wrong size
1226        let result = radio.write_memory_image(&bad_image).await;
1227        assert!(result.is_err());
1228        let err = result.unwrap_err();
1229        assert!(
1230            err.to_string().contains("invalid memory image size"),
1231            "error should mention size: {err}"
1232        );
1233    }
1234
1235    #[tokio::test]
1236    async fn read_memory_pages_small_range() {
1237        let mut mock = MockTransport::new();
1238
1239        // Enter.
1240        mock.expect(b"0M PROGRAM\r", b"0M\r");
1241
1242        // Read 2 pages starting at 0x0040.
1243        for i in 0..2u16 {
1244            let page = 0x0040 + i;
1245            #[allow(clippy::cast_possible_truncation)]
1246            let data = vec![i as u8; 256];
1247            let cmd = programming::build_read_command(page);
1248            mock.expect(&cmd, &build_w_response(page, &data));
1249            mock.expect(&[programming::ACK], &[programming::ACK]);
1250        }
1251
1252        // Exit.
1253        mock.expect(b"E", &[]);
1254
1255        let mut radio = Radio::connect(mock).await.unwrap();
1256        let data = radio.read_memory_pages(0x0040, 2).await.unwrap();
1257        assert_eq!(data.len(), 512);
1258        // First page is all 0x00, second is all 0x01.
1259        assert!(data[..256].iter().all(|&b| b == 0x00));
1260        assert!(data[256..].iter().all(|&b| b == 0x01));
1261    }
1262
1263    #[tokio::test]
1264    async fn write_memory_pages_protected_range_rejected() {
1265        let mock = MockTransport::new();
1266        let mut radio = Radio::connect(mock).await.unwrap();
1267
1268        // Try to write 3 pages starting at 0x07A0 -- page 0x07A1 is protected.
1269        let data = vec![0u8; 768]; // 3 pages
1270        let result = radio.write_memory_pages(0x07A0, &data).await;
1271        assert!(result.is_err());
1272    }
1273
1274    #[tokio::test]
1275    async fn read_channel_flags_sequence() {
1276        let mut mock = MockTransport::new();
1277
1278        // Enter.
1279        mock.expect(b"0M PROGRAM\r", b"0M\r");
1280
1281        // Channel flags span pages 0x0020 through 0x0032 (19 pages).
1282        let page_count = programming::CHANNEL_FLAGS_END - programming::CHANNEL_FLAGS_START + 1;
1283        for i in 0..page_count {
1284            let page = programming::CHANNEL_FLAGS_START + i;
1285            // Build page with flag records:
1286            // first 4 bytes = channel flag, rest = empty (0xFF).
1287            let mut data = vec![0xFF_u8; 256];
1288            if i == 0 {
1289                // Channel 0: VHF, not locked, group 0
1290                data[0] = 0x00; // used = VHF
1291                data[1] = 0x00; // not locked
1292                data[2] = 0x00; // group 0
1293                data[3] = 0xFF;
1294                // Channel 1: UHF, locked, group 5
1295                data[4] = 0x02; // used = UHF
1296                data[5] = 0x01; // locked
1297                data[6] = 0x05; // group 5
1298                data[7] = 0xFF;
1299            }
1300            let cmd = programming::build_read_command(page);
1301            mock.expect(&cmd, &build_w_response(page, &data));
1302            mock.expect(&[programming::ACK], &[programming::ACK]);
1303        }
1304
1305        // Exit.
1306        mock.expect(b"E", &[]);
1307
1308        let mut radio = Radio::connect(mock).await.unwrap();
1309        let flags = radio.read_channel_flags().await.unwrap();
1310
1311        // Should have 1200 flags.
1312        assert_eq!(flags.len(), programming::TOTAL_CHANNEL_ENTRIES);
1313
1314        // Check the first two we programmed.
1315        assert!(!flags[0].is_empty());
1316        assert_eq!(flags[0].used, programming::FLAG_VHF);
1317        assert!(!flags[0].lockout);
1318        assert_eq!(flags[0].group, 0);
1319
1320        assert!(!flags[1].is_empty());
1321        assert_eq!(flags[1].used, programming::FLAG_UHF);
1322        assert!(flags[1].lockout);
1323        assert_eq!(flags[1].group, 5);
1324
1325        // The rest should be empty.
1326        assert!(flags[2].is_empty());
1327    }
1328
1329    #[tokio::test]
1330    async fn progress_callback_invoked() {
1331        let mut mock = MockTransport::new();
1332
1333        // Enter.
1334        mock.expect(b"0M PROGRAM\r", b"0M\r");
1335
1336        // Read 3 pages.
1337        for i in 0..3u16 {
1338            let page = 0x0100 + i;
1339            let data = vec![0u8; 256];
1340            let cmd = programming::build_read_command(page);
1341            mock.expect(&cmd, &build_w_response(page, &data));
1342            mock.expect(&[programming::ACK], &[programming::ACK]);
1343        }
1344
1345        // Exit.
1346        mock.expect(b"E", &[]);
1347
1348        let mut radio = Radio::connect(mock).await.unwrap();
1349
1350        // Use read_memory_pages (which doesn't expose progress), but we
1351        // can test the internal progress via read_memory_image_with_progress
1352        // indirectly. For now, just verify read_memory_pages works with 3 pages.
1353        let data = radio.read_memory_pages(0x0100, 3).await.unwrap();
1354        assert_eq!(data.len(), 768);
1355    }
1356
1357    #[tokio::test]
1358    async fn modify_memory_page_read_modify_write() {
1359        let mut mock = MockTransport::new();
1360
1361        // Page 0x0010 contains MCP offset 0x1000-0x10FF.
1362        let page: u16 = 0x0010;
1363        let byte_index: usize = 0x71; // offset 0x1071 within this page
1364
1365        // Original page data: all zeros.
1366        let mut original_data = vec![0u8; 256];
1367        original_data[byte_index] = 0x00; // beep off
1368
1369        // Expected modified data: byte at 0x71 set to 1.
1370        let mut expected_data = original_data.clone();
1371        expected_data[byte_index] = 0x01;
1372
1373        // Enter programming mode.
1374        mock.expect(b"0M PROGRAM\r", b"0M\r");
1375
1376        // Read page.
1377        let read_cmd = programming::build_read_command(page);
1378        mock.expect(&read_cmd, &build_w_response(page, &original_data));
1379
1380        // ACK exchange after read.
1381        mock.expect(&[programming::ACK], &[programming::ACK]);
1382
1383        // Write modified page.
1384        let expected_array: [u8; 256] = expected_data.clone().try_into().unwrap();
1385        let write_cmd = programming::build_write_command(page, &expected_array);
1386        mock.expect(&write_cmd, &[programming::ACK]);
1387
1388        // Exit programming mode.
1389        mock.expect(b"E", &[]);
1390
1391        let mut radio = Radio::connect(mock).await.unwrap();
1392        radio
1393            .modify_memory_page(page, |data| {
1394                data[byte_index] = 0x01;
1395            })
1396            .await
1397            .unwrap();
1398    }
1399
1400    #[tokio::test]
1401    async fn modify_memory_page_factory_cal_rejected() {
1402        let mock = MockTransport::new();
1403        let mut radio = Radio::connect(mock).await.unwrap();
1404
1405        let result = radio
1406            .modify_memory_page(0x07A1, |_data| {
1407                // Should never be called.
1408            })
1409            .await;
1410        assert!(result.is_err());
1411        let err = result.unwrap_err();
1412        assert!(
1413            err.to_string().contains("protected"),
1414            "error should mention protected: {err}"
1415        );
1416    }
1417
1418    #[tokio::test]
1419    async fn write_channel_name_round_trip() {
1420        let mut mock = MockTransport::new();
1421
1422        // Channel 5 lives on page 0x0100 (5 / 16 = 0), offset = 5 * 16 = 80.
1423        let page: u16 = 0x0100;
1424        let offset = 5 * programming::NAME_ENTRY_SIZE;
1425
1426        // Original page: all zeros (empty names).
1427        let original_data = vec![0u8; 256];
1428
1429        // Expected: "TestCh" written at offset 80, null-padded.
1430        let mut expected_data = original_data.clone();
1431        let name = b"TestCh";
1432        expected_data[offset..offset + name.len()].copy_from_slice(name);
1433
1434        // Enter programming mode.
1435        mock.expect(b"0M PROGRAM\r", b"0M\r");
1436
1437        // Read page.
1438        let read_cmd = programming::build_read_command(page);
1439        mock.expect(&read_cmd, &build_w_response(page, &original_data));
1440
1441        // ACK exchange after read.
1442        mock.expect(&[programming::ACK], &[programming::ACK]);
1443
1444        // Write modified page.
1445        let expected_array: [u8; 256] = expected_data.try_into().unwrap();
1446        let write_cmd = programming::build_write_command(page, &expected_array);
1447        mock.expect(&write_cmd, &[programming::ACK]);
1448
1449        // Exit programming mode.
1450        mock.expect(b"E", &[]);
1451
1452        let mut radio = Radio::connect(mock).await.unwrap();
1453        radio.write_channel_name(5, "TestCh").await.unwrap();
1454    }
1455
1456    #[tokio::test]
1457    async fn write_channel_name_out_of_range_rejected() {
1458        let mock = MockTransport::new();
1459        let mut radio = Radio::connect(mock).await.unwrap();
1460
1461        let result = radio.write_channel_name(1200, "Bad").await;
1462        assert!(result.is_err());
1463        let err = result.unwrap_err();
1464        assert!(
1465            err.to_string().contains("out of range"),
1466            "error should mention out of range: {err}"
1467        );
1468    }
1469
1470    #[tokio::test]
1471    async fn write_channel_name_truncates_long_name() {
1472        let mut mock = MockTransport::new();
1473
1474        // Channel 0 on page 0x0100, offset 0.
1475        let page: u16 = 0x0100;
1476        let original_data = vec![0u8; 256];
1477
1478        // A name longer than 15 bytes should be truncated to 15.
1479        let long_name = "ABCDEFGHIJKLMNOP"; // 16 chars
1480        let mut expected_data = original_data.clone();
1481        // Only first 15 bytes written (leaving null terminator).
1482        expected_data[..15].copy_from_slice(&long_name.as_bytes()[..15]);
1483
1484        mock.expect(b"0M PROGRAM\r", b"0M\r");
1485        let read_cmd = programming::build_read_command(page);
1486        mock.expect(&read_cmd, &build_w_response(page, &original_data));
1487        mock.expect(&[programming::ACK], &[programming::ACK]);
1488        let expected_array: [u8; 256] = expected_data.try_into().unwrap();
1489        let write_cmd = programming::build_write_command(page, &expected_array);
1490        mock.expect(&write_cmd, &[programming::ACK]);
1491        mock.expect(b"E", &[]);
1492
1493        let mut radio = Radio::connect(mock).await.unwrap();
1494        radio.write_channel_name(0, long_name).await.unwrap();
1495    }
1496
1497    #[tokio::test]
1498    async fn read_all_channel_names_returns_1200() {
1499        let mut mock = MockTransport::new();
1500
1501        // Enter programming mode.
1502        mock.expect(b"0M PROGRAM\r", b"0M\r");
1503
1504        // First page has some names.
1505        let first_page_data = build_name_page(&["AllCh0", "AllCh1"]);
1506        let read_cmd = programming::build_read_command(programming::CHANNEL_NAMES_START);
1507        mock.expect(
1508            &read_cmd,
1509            &build_w_response(programming::CHANNEL_NAMES_START, &first_page_data),
1510        );
1511
1512        // Remaining 74 pages are empty.
1513        for page_offset in 1..programming::NAME_ALL_PAGE_COUNT {
1514            mock.expect(&[programming::ACK], &[programming::ACK]);
1515
1516            let page = programming::NAME_START_PAGE + page_offset;
1517            let cmd = programming::build_read_command(page);
1518            let empty = vec![0u8; 256];
1519            mock.expect(&cmd, &build_w_response(page, &empty));
1520        }
1521
1522        // Final ACK after last page.
1523        mock.expect(&[programming::ACK], &[programming::ACK]);
1524
1525        // Exit programming mode.
1526        mock.expect(b"E", &[]);
1527
1528        let mut radio = Radio::connect(mock).await.unwrap();
1529        let names = radio.read_all_channel_names().await.unwrap();
1530
1531        // 16 names per page * 75 pages = 1200.
1532        assert_eq!(names.len(), 1200);
1533        assert_eq!(names[0], "AllCh0");
1534        assert_eq!(names[1], "AllCh1");
1535        for name in &names[2..] {
1536            assert!(name.is_empty());
1537        }
1538    }
1539}