kenwood_thd75/memory/
settings.rs

1//! Typed access to the system settings region of the memory image.
2//!
3//! The system settings occupy bytes `0x0000`-`0x1FFF` (32 pages, 8,192
4//! bytes). This region stores the radio's global configuration including
5//! VFO state, squelch levels, display settings, audio settings, and more.
6//!
7//! # Known offsets
8//!
9//! | Region | Offset | Confidence |
10//! |--------|--------|------------|
11//! | Power-on message | `0x11C0` | D74 dev notes |
12//! | Model name | `0x11D0` | D74 dev notes |
13//! | Callsign data | `0x1300` | D74 dev notes |
14//! | Power level A | `0x0359` | Hardware verified |
15//! | Attenuator A | `0x035C` | Hardware verified |
16//! | Dual band | `0x0396` | Hardware verified |
17//! | VOX enable | `0x101B` | Hardware verified |
18//! | VOX gain | `0x101C` | Hardware verified |
19//! | Lock | `0x1060` | Hardware verified |
20//! | Key beep | `0x1071` | Hardware verified |
21//! | Bluetooth | `0x1078` | Hardware verified |
22//! | Squelch A | `0x100D` | Firmware analysis |
23//! | Squelch B | `0x100E` | Firmware analysis |
24//! | Language | `0x1006` | Firmware analysis |
25//! | Beep volume | `0x1072` | Firmware analysis |
26//! | Backlight control | `0x1069` | Firmware analysis |
27//! | Auto power off | `0x10D0` | Firmware analysis |
28//! | Battery saver | `0x10C0` | Firmware analysis |
29//! | Key lock type | `0x1061` | Firmware analysis |
30//! | VOX delay | `0x101D` | Firmware analysis |
31
32use crate::protocol::programming;
33use crate::types::settings::{
34    AltitudeRainUnit, AutoPowerOff, DisplayUnits, KeyLockType, Language, SpeedDistanceUnit,
35    TemperatureUnit,
36};
37use crate::types::{Frequency, MemoryMode, PowerLevel};
38
39// ---------------------------------------------------------------------------
40// System settings region (0x0000 - 0x1FFF)
41// ---------------------------------------------------------------------------
42
43/// Byte offset of the system settings region.
44const SETTINGS_OFFSET: usize = 0x0000;
45
46/// Size of the system settings region in bytes.
47const SETTINGS_SIZE: usize = (programming::SETTINGS_END as usize + 1
48    - programming::SETTINGS_START as usize)
49    * programming::PAGE_SIZE;
50
51/// Byte offset of the power-on message (16 bytes, null-terminated ASCII).
52const POWER_ON_MESSAGE_OFFSET: usize = 0x11C0;
53
54/// Size of the power-on message field.
55const POWER_ON_MESSAGE_SIZE: usize = 16;
56
57/// Byte offset of the internal model name (16 bytes, null-terminated ASCII).
58const MODEL_NAME_OFFSET: usize = 0x11D0;
59
60/// Size of the model name field.
61const MODEL_NAME_SIZE: usize = 16;
62
63/// Byte offset of callsign data.
64const CALLSIGN_OFFSET: usize = 0x1300;
65
66// ---------------------------------------------------------------------------
67// Hardware-verified settings offsets
68//
69// Each of these offsets was confirmed on a real TH-D75 by toggling the
70// setting individually and identifying the changed byte in the MCP image.
71// ---------------------------------------------------------------------------
72
73/// Hardware-verified offset for Band A power level (1 byte, 0=Hi, 1=Mid, 2=Lo, 3=EL).
74const POWER_LEVEL_A_OFFSET: usize = 0x0359;
75
76/// Hardware-verified offset for Band A attenuator on/off (1 byte, 0=off, 1=on).
77const ATTENUATOR_A_OFFSET: usize = 0x035C;
78
79/// Hardware-verified offset for dual-band display (1 byte, 0=single, 1=dual).
80const DUAL_BAND_OFFSET: usize = 0x0396;
81
82/// Hardware-verified offset for VOX enabled (1 byte, 0=off, 1=on).
83const VOX_ENABLED_OFFSET: usize = 0x101B;
84
85/// Hardware-verified offset for VOX gain (1 byte, range 0-9).
86const VOX_GAIN_OFFSET: usize = 0x101C;
87
88/// Hardware-verified offset for lock on/off (1 byte, 0=unlocked, 1=locked).
89const LOCK_OFFSET: usize = 0x1060;
90
91/// Hardware-verified offset for key beep on/off (1 byte, 0=off, 1=on).
92const KEY_BEEP_OFFSET: usize = 0x1071;
93
94/// Hardware-verified offset for Bluetooth on/off (1 byte, 0=off, 1=on).
95const BLUETOOTH_OFFSET: usize = 0x1078;
96
97// ---------------------------------------------------------------------------
98// Settings offsets from firmware analysis
99// ---------------------------------------------------------------------------
100
101// --- RX Settings ---
102/// Band A squelch level (1 byte, range 0-6).
103const SQUELCH_A_OFFSET: usize = 0x100D;
104/// Band B squelch level (1 byte, range 0-6).
105const SQUELCH_B_OFFSET: usize = 0x100E;
106/// FM narrow setting (1 byte).
107const FM_NARROW_OFFSET: usize = 0x100F;
108/// SSB high cut filter (1 byte).
109const SSB_HIGH_CUT_OFFSET: usize = 0x1011;
110/// CW high cut filter (1 byte).
111const CW_HIGH_CUT_OFFSET: usize = 0x1012;
112/// AM high cut filter (1 byte).
113const AM_HIGH_CUT_OFFSET: usize = 0x1013;
114/// Auto filter setting (1 byte).
115const AUTO_FILTER_OFFSET: usize = 0x100C;
116
117// --- Scan ---
118/// Scan resume setting (1 byte).
119const SCAN_RESUME_OFFSET: usize = 0x1007;
120/// Digital scan resume setting (1 byte).
121const DIGITAL_SCAN_RESUME_OFFSET: usize = 0x1008;
122/// Scan restart time (1 byte).
123const SCAN_RESTART_TIME_OFFSET: usize = 0x1009;
124/// Scan restart carrier setting (1 byte).
125const SCAN_RESTART_CARRIER_OFFSET: usize = 0x100A;
126
127// --- TX ---
128/// Timeout timer (1 byte).
129const TIMEOUT_TIMER_OFFSET: usize = 0x1018;
130/// TX inhibit setting (1 byte).
131const TX_INHIBIT_OFFSET: usize = 0x1019;
132/// Beat shift setting (1 byte).
133const BEAT_SHIFT_OFFSET: usize = 0x101A;
134
135// --- VOX (0x101B and 0x101C hardware-verified above) ---
136/// VOX delay (1 byte, in 100 ms units).
137const VOX_DELAY_OFFSET: usize = 0x101D;
138/// VOX TX on busy setting (1 byte, 0=off, 1=on).
139const VOX_TX_ON_BUSY_OFFSET: usize = 0x101E;
140
141// --- CW ---
142/// CW break-in setting (1 byte).
143const CW_BREAK_IN_OFFSET: usize = 0x101F;
144/// CW delay time (1 byte).
145const CW_DELAY_TIME_OFFSET: usize = 0x1020;
146/// CW pitch (1 byte).
147const CW_PITCH_OFFSET: usize = 0x1021;
148
149// --- DTMF ---
150/// DTMF speed (1 byte).
151const DTMF_SPEED_OFFSET: usize = 0x1024;
152/// DTMF pause time (1 byte).
153const DTMF_PAUSE_TIME_OFFSET: usize = 0x1026;
154/// DTMF TX hold setting (1 byte).
155const DTMF_TX_HOLD_OFFSET: usize = 0x1027;
156
157// --- Repeater ---
158/// Repeater auto offset setting (1 byte).
159const REPEATER_AUTO_OFFSET_OFFSET: usize = 0x1030;
160/// Repeater call key setting (1 byte).
161const REPEATER_CALL_KEY_OFFSET: usize = 0x1031;
162
163// --- Auxiliary ---
164/// Microphone sensitivity (1 byte).
165const MIC_SENSITIVITY_OFFSET: usize = 0x1040;
166/// PF key 1 assignment (1 byte).
167const PF_KEY1_OFFSET: usize = 0x1041;
168/// PF key 2 assignment (1 byte).
169const PF_KEY2_OFFSET: usize = 0x1042;
170
171// --- Lock (0x1060 hardware-verified above) ---
172/// Key lock type (1 byte, enum index).
173const KEY_LOCK_TYPE_OFFSET: usize = 0x1061;
174/// Lock key A setting (1 byte).
175const LOCK_KEY_A_OFFSET: usize = 0x1062;
176/// Lock key B setting (1 byte).
177const LOCK_KEY_B_OFFSET: usize = 0x1063;
178/// Lock key C setting (1 byte).
179const LOCK_KEY_C_OFFSET: usize = 0x1064;
180/// Lock PTT key setting (1 byte).
181const LOCK_KEY_PTT_OFFSET: usize = 0x1065;
182/// APRS lock setting (1 byte).
183const APRS_LOCK_OFFSET: usize = 0x1097;
184
185// --- Display ---
186/// Dual display size (1 byte).
187const DUAL_DISPLAY_SIZE_OFFSET: usize = 0x1066;
188/// Display area (1 byte).
189const DISPLAY_AREA_OFFSET: usize = 0x1067;
190/// Info line setting (1 byte).
191const INFO_LINE_OFFSET: usize = 0x1068;
192/// Backlight control (1 byte).
193const BACKLIGHT_CONTROL_OFFSET: usize = 0x1069;
194/// Backlight timer (1 byte).
195const BACKLIGHT_TIMER_OFFSET: usize = 0x106A;
196/// Display hold time (1 byte).
197const DISPLAY_HOLD_TIME_OFFSET: usize = 0x106B;
198/// Display method (1 byte).
199const DISPLAY_METHOD_OFFSET: usize = 0x106C;
200/// Power-on display setting (1 byte).
201const POWER_ON_DISPLAY_OFFSET: usize = 0x106D;
202
203// --- Audio (0x1071 key beep hardware-verified above) ---
204/// EMR volume level (1 byte).
205const EMR_VOLUME_LEVEL_OFFSET: usize = 0x106E;
206/// Auto mute return time (1 byte).
207const AUTO_MUTE_RETURN_TIME_OFFSET: usize = 0x106F;
208/// Announce setting (1 byte).
209const ANNOUNCE_OFFSET: usize = 0x1070;
210/// Beep volume (1 byte, range 1-7).
211const BEEP_VOLUME_OFFSET: usize = 0x1072;
212/// Voice language (1 byte).
213const VOICE_LANGUAGE_OFFSET: usize = 0x1073;
214/// Voice volume (1 byte).
215const VOICE_VOLUME_OFFSET: usize = 0x1074;
216/// Voice speed (1 byte).
217const VOICE_SPEED_OFFSET: usize = 0x1075;
218/// Volume lock (1 byte).
219const VOLUME_LOCK_OFFSET: usize = 0x1076;
220
221// --- Units ---
222/// Speed/distance unit (1 byte, enum index).
223const SPEED_DISTANCE_UNIT_OFFSET: usize = 0x1077;
224/// Altitude/rain unit (1 byte, enum index).
225const ALTITUDE_RAIN_UNIT_OFFSET: usize = 0x1083;
226/// Temperature unit (1 byte, enum index).
227const TEMPERATURE_UNIT_OFFSET: usize = 0x1084;
228
229// --- Bluetooth (0x1078 hardware-verified above) ---
230/// Bluetooth auto-connect setting (1 byte).
231const BT_AUTO_CONNECT_OFFSET: usize = 0x1079;
232
233// --- Interface ---
234/// GPS Bluetooth interface (1 byte).
235const GPS_BT_INTERFACE_OFFSET: usize = 0x1080;
236/// PC output mode (1 byte).
237const PC_OUTPUT_MODE_OFFSET: usize = 0x1085;
238/// APRS USB mode (1 byte).
239const APRS_USB_MODE_OFFSET: usize = 0x1086;
240/// USB audio output setting (1 byte).
241const USB_AUDIO_OUTPUT_OFFSET: usize = 0x1094;
242/// Internet link setting (1 byte).
243const INTERNET_LINK_OFFSET: usize = 0x1095;
244
245// --- System ---
246/// Power-on message flag (1 byte).
247const POWER_ON_MESSAGE_FLAG_OFFSET: usize = 0x1087;
248/// Language setting (1 byte, 0=English, 1=Japanese).
249const LANGUAGE_OFFSET: usize = 0x1006;
250
251// --- Battery ---
252/// Battery saver (1 byte, 0=off, 1=on).
253const BATTERY_SAVER_OFFSET: usize = 0x10C0;
254/// Auto power off (1 byte, enum index).
255const AUTO_POWER_OFF_OFFSET: usize = 0x10D0;
256
257// --- DualBand (also at 0x0396 in VFO region) ---
258/// Dual band MCP setting (1 byte).
259const DUAL_BAND_MCP_OFFSET: usize = 0x1096;
260
261// ---------------------------------------------------------------------------
262// VFO data block (confirmed via memory dump analysis)
263//
264// The VFO data block at 0x0020 contains 6 VFO entries, each 40 bytes
265// (same format as channel memory data). These represent the current
266// VFO state for each band. Confirmed by both the memory map document
267// and visual inspection of the memory dump (valid frequency data at
268// 0x0020).
269// ---------------------------------------------------------------------------
270
271/// Byte offset of the VFO data block (6 entries x 40 bytes).
272const VFO_DATA_OFFSET: usize = 0x0020;
273
274/// Number of VFO entries in the VFO data block.
275const VFO_ENTRY_COUNT: usize = 6;
276
277/// Size of each VFO entry in bytes (same as channel record).
278const VFO_ENTRY_SIZE: usize = programming::CHANNEL_RECORD_SIZE; // 40
279
280// ---------------------------------------------------------------------------
281// SettingsAccess (read-only)
282// ---------------------------------------------------------------------------
283
284/// Read-only access to the system settings region.
285///
286/// Provides raw byte access and typed field accessors for the settings
287/// region at bytes `0x0000`-`0x1FFF`. Hardware-verified offsets are
288/// confirmed on a real TH-D75; remaining offsets are from firmware
289/// analysis and marked accordingly.
290#[derive(Debug)]
291pub struct SettingsAccess<'a> {
292    image: &'a [u8],
293}
294
295impl<'a> SettingsAccess<'a> {
296    /// Create a new settings accessor borrowing the raw image.
297    pub(crate) const fn new(image: &'a [u8]) -> Self {
298        Self { image }
299    }
300
301    /// Get the raw system settings bytes (0x0000-0x1FFF).
302    ///
303    /// Returns `None` if the image is too small.
304    #[must_use]
305    pub fn raw(&self) -> Option<&[u8]> {
306        let end = SETTINGS_OFFSET + SETTINGS_SIZE;
307        if end <= self.image.len() {
308            Some(&self.image[SETTINGS_OFFSET..end])
309        } else {
310            None
311        }
312    }
313
314    /// Get the power-on message (up to 16 characters).
315    ///
316    /// Stored at MCP offset `0x11C0`. Returns the null-terminated ASCII
317    /// string.
318    #[must_use]
319    pub fn power_on_message(&self) -> String {
320        extract_string(self.image, POWER_ON_MESSAGE_OFFSET, POWER_ON_MESSAGE_SIZE)
321    }
322
323    /// Get the internal model name (up to 16 characters).
324    ///
325    /// Stored at MCP offset `0x11D0`.
326    #[must_use]
327    pub fn model_name(&self) -> String {
328        extract_string(self.image, MODEL_NAME_OFFSET, MODEL_NAME_SIZE)
329    }
330
331    /// Get the raw callsign data at MCP offset `0x1300`.
332    ///
333    /// The exact structure of this region is not yet fully mapped.
334    /// Returns up to `len` bytes, or `None` if out of bounds.
335    #[must_use]
336    pub fn callsign_raw(&self, len: usize) -> Option<&[u8]> {
337        let end = CALLSIGN_OFFSET + len;
338        if end <= self.image.len() {
339            Some(&self.image[CALLSIGN_OFFSET..end])
340        } else {
341            None
342        }
343    }
344
345    /// Read an arbitrary byte range from the settings region.
346    ///
347    /// The offset is relative to the start of the image (MCP byte
348    /// address). Returns `None` if the range extends past the image.
349    #[must_use]
350    pub fn read_bytes(&self, offset: usize, len: usize) -> Option<&[u8]> {
351        let end = offset + len;
352        if end <= self.image.len() {
353            Some(&self.image[offset..end])
354        } else {
355            None
356        }
357    }
358
359    // -----------------------------------------------------------------------
360    // Typed settings accessors
361    // -----------------------------------------------------------------------
362
363    /// Read key beep setting (0=off, 1=on).
364    ///
365    /// MCP offset `0x1071`.
366    #[must_use]
367    pub fn key_beep(&self) -> bool {
368        self.image.get(KEY_BEEP_OFFSET).is_some_and(|&b| b != 0)
369    }
370
371    /// Read beep volume (1-7, 0 if unreadable).
372    ///
373    /// MCP offset `0x1072`.
374    #[must_use]
375    pub fn beep_volume(&self) -> u8 {
376        self.image
377            .get(BEEP_VOLUME_OFFSET)
378            .copied()
379            .map_or(0, |b| b.min(7))
380    }
381
382    /// Read LCD backlight control setting (0 if unreadable).
383    ///
384    /// MCP offset `0x1069`.
385    #[must_use]
386    pub fn backlight(&self) -> u8 {
387        self.image
388            .get(BACKLIGHT_CONTROL_OFFSET)
389            .copied()
390            .unwrap_or(0)
391    }
392
393    /// Read auto power off setting.
394    ///
395    /// MCP offset `0x10D0`.
396    #[must_use]
397    pub fn auto_power_off(&self) -> AutoPowerOff {
398        match self.image.get(AUTO_POWER_OFF_OFFSET).copied().unwrap_or(0) {
399            1 => AutoPowerOff::Min30,
400            2 => AutoPowerOff::Min60,
401            3 => AutoPowerOff::Min90,
402            4 => AutoPowerOff::Min120,
403            _ => AutoPowerOff::Off,
404        }
405    }
406
407    /// Read battery saver setting.
408    ///
409    /// MCP offset `0x10C0`.
410    #[must_use]
411    pub fn battery_saver(&self) -> bool {
412        self.image
413            .get(BATTERY_SAVER_OFFSET)
414            .is_some_and(|&b| b != 0)
415    }
416
417    /// Read key lock type.
418    ///
419    /// MCP offset `0x1061`.
420    #[must_use]
421    pub fn key_lock_type(&self) -> KeyLockType {
422        match self.image.get(KEY_LOCK_TYPE_OFFSET).copied().unwrap_or(0) {
423            1 => KeyLockType::KeyAndPtt,
424            2 => KeyLockType::KeyPttAndDial,
425            _ => KeyLockType::KeyOnly,
426        }
427    }
428
429    /// Read display unit settings.
430    ///
431    /// MCP offsets `0x1077` (speed/distance), `0x1083` (altitude/rain),
432    /// `0x1084` (temperature).
433    #[must_use]
434    pub fn display_units(&self) -> DisplayUnits {
435        let speed_distance = match self
436            .image
437            .get(SPEED_DISTANCE_UNIT_OFFSET)
438            .copied()
439            .unwrap_or(0)
440        {
441            1 => SpeedDistanceUnit::KilometersPerHour,
442            2 => SpeedDistanceUnit::Knots,
443            _ => SpeedDistanceUnit::MilesPerHour,
444        };
445
446        let altitude_rain = match self
447            .image
448            .get(ALTITUDE_RAIN_UNIT_OFFSET)
449            .copied()
450            .unwrap_or(0)
451        {
452            1 => AltitudeRainUnit::MetersMm,
453            _ => AltitudeRainUnit::FeetInch,
454        };
455
456        let temperature = match self
457            .image
458            .get(TEMPERATURE_UNIT_OFFSET)
459            .copied()
460            .unwrap_or(0)
461        {
462            1 => TemperatureUnit::Celsius,
463            _ => TemperatureUnit::Fahrenheit,
464        };
465
466        DisplayUnits {
467            speed_distance,
468            altitude_rain,
469            temperature,
470        }
471    }
472
473    /// Read language setting.
474    ///
475    /// MCP offset `0x1006`.
476    #[must_use]
477    pub fn language(&self) -> Language {
478        match self.image.get(LANGUAGE_OFFSET).copied().unwrap_or(0) {
479            1 => Language::Japanese,
480            _ => Language::English,
481        }
482    }
483
484    /// Read VOX enabled setting (0=off, 1=on).
485    ///
486    /// MCP offset `0x101B`.
487    #[must_use]
488    pub fn vox_enabled(&self) -> bool {
489        self.image.get(VOX_ENABLED_OFFSET).is_some_and(|&b| b != 0)
490    }
491
492    /// Read VOX gain (0-9, 0 if unreadable).
493    ///
494    /// MCP offset `0x101C`.
495    #[must_use]
496    pub fn vox_gain(&self) -> u8 {
497        self.image
498            .get(VOX_GAIN_OFFSET)
499            .copied()
500            .map_or(0, |b| b.min(9))
501    }
502
503    /// Read VOX delay (in 100 ms units, 0 if unreadable).
504    ///
505    /// MCP offset `0x101D`.
506    #[must_use]
507    pub fn vox_delay(&self) -> u8 {
508        self.image.get(VOX_DELAY_OFFSET).copied().unwrap_or(0)
509    }
510
511    /// Read squelch level for Band A (0-6, 0 if unreadable).
512    ///
513    /// MCP offset `0x100D`.
514    #[must_use]
515    pub fn squelch_a(&self) -> u8 {
516        self.image
517            .get(SQUELCH_A_OFFSET)
518            .copied()
519            .map_or(0, |b| b.min(6))
520    }
521
522    /// Read squelch level for Band B (0-6, 0 if unreadable).
523    ///
524    /// MCP offset `0x100E`.
525    #[must_use]
526    pub fn squelch_b(&self) -> u8 {
527        self.image
528            .get(SQUELCH_B_OFFSET)
529            .copied()
530            .map_or(0, |b| b.min(6))
531    }
532
533    // -----------------------------------------------------------------------
534    // Accessors for settings from firmware analysis
535    // -----------------------------------------------------------------------
536
537    /// Read FM narrow setting (0 if unreadable).
538    ///
539    /// MCP offset `0x100F`.
540    #[must_use]
541    pub fn fm_narrow(&self) -> u8 {
542        self.image.get(FM_NARROW_OFFSET).copied().unwrap_or(0)
543    }
544
545    /// Read SSB high-cut filter setting (0 if unreadable).
546    ///
547    /// MCP offset `0x1011`.
548    #[must_use]
549    pub fn ssb_high_cut(&self) -> u8 {
550        self.image.get(SSB_HIGH_CUT_OFFSET).copied().unwrap_or(0)
551    }
552
553    /// Read CW high-cut filter setting (0 if unreadable).
554    ///
555    /// MCP offset `0x1012`.
556    #[must_use]
557    pub fn cw_high_cut(&self) -> u8 {
558        self.image.get(CW_HIGH_CUT_OFFSET).copied().unwrap_or(0)
559    }
560
561    /// Read AM high-cut filter setting (0 if unreadable).
562    ///
563    /// MCP offset `0x1013`.
564    #[must_use]
565    pub fn am_high_cut(&self) -> u8 {
566        self.image.get(AM_HIGH_CUT_OFFSET).copied().unwrap_or(0)
567    }
568
569    /// Read auto filter setting (0 if unreadable).
570    ///
571    /// MCP offset `0x100C`.
572    #[must_use]
573    pub fn auto_filter(&self) -> u8 {
574        self.image.get(AUTO_FILTER_OFFSET).copied().unwrap_or(0)
575    }
576
577    /// Read scan resume setting (0 if unreadable).
578    ///
579    /// MCP offset `0x1007`.
580    #[must_use]
581    pub fn scan_resume(&self) -> u8 {
582        self.image.get(SCAN_RESUME_OFFSET).copied().unwrap_or(0)
583    }
584
585    /// Read digital scan resume setting (0 if unreadable).
586    ///
587    /// MCP offset `0x1008`.
588    #[must_use]
589    pub fn digital_scan_resume(&self) -> u8 {
590        self.image
591            .get(DIGITAL_SCAN_RESUME_OFFSET)
592            .copied()
593            .unwrap_or(0)
594    }
595
596    /// Read scan restart time (0 if unreadable).
597    ///
598    /// MCP offset `0x1009`.
599    #[must_use]
600    pub fn scan_restart_time(&self) -> u8 {
601        self.image
602            .get(SCAN_RESTART_TIME_OFFSET)
603            .copied()
604            .unwrap_or(0)
605    }
606
607    /// Read scan restart carrier setting (0 if unreadable).
608    ///
609    /// MCP offset `0x100A`.
610    #[must_use]
611    pub fn scan_restart_carrier(&self) -> u8 {
612        self.image
613            .get(SCAN_RESTART_CARRIER_OFFSET)
614            .copied()
615            .unwrap_or(0)
616    }
617
618    /// Read timeout timer setting (0 if unreadable).
619    ///
620    /// MCP offset `0x1018`.
621    #[must_use]
622    pub fn timeout_timer(&self) -> u8 {
623        self.image.get(TIMEOUT_TIMER_OFFSET).copied().unwrap_or(0)
624    }
625
626    /// Read TX inhibit setting (false if unreadable).
627    ///
628    /// MCP offset `0x1019`.
629    #[must_use]
630    pub fn tx_inhibit(&self) -> bool {
631        self.image.get(TX_INHIBIT_OFFSET).is_some_and(|&b| b != 0)
632    }
633
634    /// Read beat shift setting (false if unreadable).
635    ///
636    /// MCP offset `0x101A`.
637    #[must_use]
638    pub fn beat_shift(&self) -> bool {
639        self.image.get(BEAT_SHIFT_OFFSET).is_some_and(|&b| b != 0)
640    }
641
642    /// Read VOX TX-on-busy setting (false if unreadable).
643    ///
644    /// MCP offset `0x101E`.
645    #[must_use]
646    pub fn vox_tx_on_busy(&self) -> bool {
647        self.image
648            .get(VOX_TX_ON_BUSY_OFFSET)
649            .is_some_and(|&b| b != 0)
650    }
651
652    /// Read CW break-in setting (false if unreadable).
653    ///
654    /// MCP offset `0x101F`.
655    #[must_use]
656    pub fn cw_break_in(&self) -> bool {
657        self.image.get(CW_BREAK_IN_OFFSET).is_some_and(|&b| b != 0)
658    }
659
660    /// Read CW delay time (0 if unreadable).
661    ///
662    /// MCP offset `0x1020`.
663    #[must_use]
664    pub fn cw_delay_time(&self) -> u8 {
665        self.image.get(CW_DELAY_TIME_OFFSET).copied().unwrap_or(0)
666    }
667
668    /// Read CW pitch (0 if unreadable).
669    ///
670    /// MCP offset `0x1021`.
671    #[must_use]
672    pub fn cw_pitch(&self) -> u8 {
673        self.image.get(CW_PITCH_OFFSET).copied().unwrap_or(0)
674    }
675
676    /// Read DTMF speed (0 if unreadable).
677    ///
678    /// MCP offset `0x1024`.
679    #[must_use]
680    pub fn dtmf_speed(&self) -> u8 {
681        self.image.get(DTMF_SPEED_OFFSET).copied().unwrap_or(0)
682    }
683
684    /// Read DTMF pause time (0 if unreadable).
685    ///
686    /// MCP offset `0x1026`.
687    #[must_use]
688    pub fn dtmf_pause_time(&self) -> u8 {
689        self.image.get(DTMF_PAUSE_TIME_OFFSET).copied().unwrap_or(0)
690    }
691
692    /// Read DTMF TX hold setting (false if unreadable).
693    ///
694    /// MCP offset `0x1027`.
695    #[must_use]
696    pub fn dtmf_tx_hold(&self) -> bool {
697        self.image.get(DTMF_TX_HOLD_OFFSET).is_some_and(|&b| b != 0)
698    }
699
700    /// Read repeater auto offset setting (false if unreadable).
701    ///
702    /// MCP offset `0x1030`.
703    #[must_use]
704    pub fn repeater_auto_offset(&self) -> bool {
705        self.image
706            .get(REPEATER_AUTO_OFFSET_OFFSET)
707            .is_some_and(|&b| b != 0)
708    }
709
710    /// Read repeater call key setting (0 if unreadable).
711    ///
712    /// MCP offset `0x1031`.
713    #[must_use]
714    pub fn repeater_call_key(&self) -> u8 {
715        self.image
716            .get(REPEATER_CALL_KEY_OFFSET)
717            .copied()
718            .unwrap_or(0)
719    }
720
721    /// Read microphone sensitivity (0 if unreadable).
722    ///
723    /// MCP offset `0x1040`.
724    #[must_use]
725    pub fn mic_sensitivity(&self) -> u8 {
726        self.image.get(MIC_SENSITIVITY_OFFSET).copied().unwrap_or(0)
727    }
728
729    /// Read PF key 1 assignment (0 if unreadable).
730    ///
731    /// MCP offset `0x1041`.
732    #[must_use]
733    pub fn pf_key1(&self) -> u8 {
734        self.image.get(PF_KEY1_OFFSET).copied().unwrap_or(0)
735    }
736
737    /// Read PF key 2 assignment (0 if unreadable).
738    ///
739    /// MCP offset `0x1042`.
740    #[must_use]
741    pub fn pf_key2(&self) -> u8 {
742        self.image.get(PF_KEY2_OFFSET).copied().unwrap_or(0)
743    }
744
745    /// Read lock key A setting (false if unreadable).
746    ///
747    /// MCP offset `0x1062`.
748    #[must_use]
749    pub fn lock_key_a(&self) -> bool {
750        self.image.get(LOCK_KEY_A_OFFSET).is_some_and(|&b| b != 0)
751    }
752
753    /// Read lock key B setting (false if unreadable).
754    ///
755    /// MCP offset `0x1063`.
756    #[must_use]
757    pub fn lock_key_b(&self) -> bool {
758        self.image.get(LOCK_KEY_B_OFFSET).is_some_and(|&b| b != 0)
759    }
760
761    /// Read lock key C setting (false if unreadable).
762    ///
763    /// MCP offset `0x1064`.
764    #[must_use]
765    pub fn lock_key_c(&self) -> bool {
766        self.image.get(LOCK_KEY_C_OFFSET).is_some_and(|&b| b != 0)
767    }
768
769    /// Read lock PTT key setting (false if unreadable).
770    ///
771    /// MCP offset `0x1065`.
772    #[must_use]
773    pub fn lock_key_ptt(&self) -> bool {
774        self.image.get(LOCK_KEY_PTT_OFFSET).is_some_and(|&b| b != 0)
775    }
776
777    /// Read APRS lock setting (false if unreadable).
778    ///
779    /// MCP offset `0x1097`.
780    #[must_use]
781    pub fn aprs_lock(&self) -> bool {
782        self.image.get(APRS_LOCK_OFFSET).is_some_and(|&b| b != 0)
783    }
784
785    /// Read dual display size (0 if unreadable).
786    ///
787    /// MCP offset `0x1066`.
788    #[must_use]
789    pub fn dual_display_size(&self) -> u8 {
790        self.image
791            .get(DUAL_DISPLAY_SIZE_OFFSET)
792            .copied()
793            .unwrap_or(0)
794    }
795
796    /// Read display area setting (0 if unreadable).
797    ///
798    /// MCP offset `0x1067`.
799    #[must_use]
800    pub fn display_area(&self) -> u8 {
801        self.image.get(DISPLAY_AREA_OFFSET).copied().unwrap_or(0)
802    }
803
804    /// Read info line setting (0 if unreadable).
805    ///
806    /// MCP offset `0x1068`.
807    #[must_use]
808    pub fn info_line(&self) -> u8 {
809        self.image.get(INFO_LINE_OFFSET).copied().unwrap_or(0)
810    }
811
812    /// Read backlight control setting (0 if unreadable).
813    ///
814    /// MCP offset `0x1069`.
815    #[must_use]
816    pub fn backlight_control(&self) -> u8 {
817        self.image
818            .get(BACKLIGHT_CONTROL_OFFSET)
819            .copied()
820            .unwrap_or(0)
821    }
822
823    /// Read backlight timer (0 if unreadable).
824    ///
825    /// MCP offset `0x106A`.
826    #[must_use]
827    pub fn backlight_timer(&self) -> u8 {
828        self.image.get(BACKLIGHT_TIMER_OFFSET).copied().unwrap_or(0)
829    }
830
831    /// Read display hold time (0 if unreadable).
832    ///
833    /// MCP offset `0x106B`.
834    #[must_use]
835    pub fn display_hold_time(&self) -> u8 {
836        self.image
837            .get(DISPLAY_HOLD_TIME_OFFSET)
838            .copied()
839            .unwrap_or(0)
840    }
841
842    /// Read display method (0 if unreadable).
843    ///
844    /// MCP offset `0x106C`.
845    #[must_use]
846    pub fn display_method(&self) -> u8 {
847        self.image.get(DISPLAY_METHOD_OFFSET).copied().unwrap_or(0)
848    }
849
850    /// Read power-on display setting (0 if unreadable).
851    ///
852    /// MCP offset `0x106D`.
853    #[must_use]
854    pub fn power_on_display(&self) -> u8 {
855        self.image
856            .get(POWER_ON_DISPLAY_OFFSET)
857            .copied()
858            .unwrap_or(0)
859    }
860
861    /// Read EMR volume level (0 if unreadable).
862    ///
863    /// MCP offset `0x106E`.
864    #[must_use]
865    pub fn emr_volume_level(&self) -> u8 {
866        self.image
867            .get(EMR_VOLUME_LEVEL_OFFSET)
868            .copied()
869            .unwrap_or(0)
870    }
871
872    /// Read auto mute return time (0 if unreadable).
873    ///
874    /// MCP offset `0x106F`.
875    #[must_use]
876    pub fn auto_mute_return_time(&self) -> u8 {
877        self.image
878            .get(AUTO_MUTE_RETURN_TIME_OFFSET)
879            .copied()
880            .unwrap_or(0)
881    }
882
883    /// Read announce setting (false if unreadable).
884    ///
885    /// MCP offset `0x1070`.
886    #[must_use]
887    pub fn announce(&self) -> bool {
888        self.image.get(ANNOUNCE_OFFSET).is_some_and(|&b| b != 0)
889    }
890
891    /// Read voice language setting (0 if unreadable).
892    ///
893    /// MCP offset `0x1073`.
894    #[must_use]
895    pub fn voice_language(&self) -> u8 {
896        self.image.get(VOICE_LANGUAGE_OFFSET).copied().unwrap_or(0)
897    }
898
899    /// Read voice volume (0 if unreadable).
900    ///
901    /// MCP offset `0x1074`.
902    #[must_use]
903    pub fn voice_volume(&self) -> u8 {
904        self.image.get(VOICE_VOLUME_OFFSET).copied().unwrap_or(0)
905    }
906
907    /// Read voice speed (0 if unreadable).
908    ///
909    /// MCP offset `0x1075`.
910    #[must_use]
911    pub fn voice_speed(&self) -> u8 {
912        self.image.get(VOICE_SPEED_OFFSET).copied().unwrap_or(0)
913    }
914
915    /// Read volume lock setting (false if unreadable).
916    ///
917    /// MCP offset `0x1076`.
918    #[must_use]
919    pub fn volume_lock(&self) -> bool {
920        self.image.get(VOLUME_LOCK_OFFSET).is_some_and(|&b| b != 0)
921    }
922
923    /// Read Bluetooth auto-connect setting (false if unreadable).
924    ///
925    /// MCP offset `0x1079`.
926    #[must_use]
927    pub fn bt_auto_connect(&self) -> bool {
928        self.image
929            .get(BT_AUTO_CONNECT_OFFSET)
930            .is_some_and(|&b| b != 0)
931    }
932
933    /// Read GPS Bluetooth interface setting (0 if unreadable).
934    ///
935    /// MCP offset `0x1080`.
936    #[must_use]
937    pub fn gps_bt_interface(&self) -> u8 {
938        self.image
939            .get(GPS_BT_INTERFACE_OFFSET)
940            .copied()
941            .unwrap_or(0)
942    }
943
944    /// Read PC output mode (0 if unreadable).
945    ///
946    /// MCP offset `0x1085`.
947    #[must_use]
948    pub fn pc_output_mode(&self) -> u8 {
949        self.image.get(PC_OUTPUT_MODE_OFFSET).copied().unwrap_or(0)
950    }
951
952    /// Read APRS USB mode (0 if unreadable).
953    ///
954    /// MCP offset `0x1086`.
955    #[must_use]
956    pub fn aprs_usb_mode(&self) -> u8 {
957        self.image.get(APRS_USB_MODE_OFFSET).copied().unwrap_or(0)
958    }
959
960    /// Read USB audio output setting (false if unreadable).
961    ///
962    /// MCP offset `0x1094`.
963    #[must_use]
964    pub fn usb_audio_output(&self) -> bool {
965        self.image
966            .get(USB_AUDIO_OUTPUT_OFFSET)
967            .is_some_and(|&b| b != 0)
968    }
969
970    /// Read internet link setting (false if unreadable).
971    ///
972    /// MCP offset `0x1095`.
973    #[must_use]
974    pub fn internet_link(&self) -> bool {
975        self.image
976            .get(INTERNET_LINK_OFFSET)
977            .is_some_and(|&b| b != 0)
978    }
979
980    /// Read power-on message flag (false if unreadable).
981    ///
982    /// MCP offset `0x1087`.
983    #[must_use]
984    pub fn power_on_message_flag(&self) -> bool {
985        self.image
986            .get(POWER_ON_MESSAGE_FLAG_OFFSET)
987            .is_some_and(|&b| b != 0)
988    }
989
990    /// Read dual band MCP setting (false if unreadable).
991    ///
992    /// MCP offset `0x1096`.
993    #[must_use]
994    pub fn dual_band_mcp(&self) -> bool {
995        self.image
996            .get(DUAL_BAND_MCP_OFFSET)
997            .is_some_and(|&b| b != 0)
998    }
999
1000    // -----------------------------------------------------------------------
1001    // Hardware-verified settings accessors
1002    // -----------------------------------------------------------------------
1003
1004    /// Read Band A power level.
1005    ///
1006    /// MCP offset `0x0359`.
1007    /// Returns `High` if the byte is out of range or unreadable.
1008    #[must_use]
1009    pub fn power_level_a(&self) -> PowerLevel {
1010        self.image
1011            .get(POWER_LEVEL_A_OFFSET)
1012            .copied()
1013            .and_then(|b| PowerLevel::try_from(b).ok())
1014            .unwrap_or(PowerLevel::High)
1015    }
1016
1017    /// Read Band A attenuator setting (0=off, 1=on).
1018    ///
1019    /// MCP offset `0x035C`.
1020    #[must_use]
1021    pub fn attenuator_a(&self) -> bool {
1022        self.image.get(ATTENUATOR_A_OFFSET).is_some_and(|&b| b != 0)
1023    }
1024
1025    /// Read dual-band display setting (0=single, 1=dual).
1026    ///
1027    /// MCP offset `0x0396`.
1028    #[must_use]
1029    pub fn dual_band(&self) -> bool {
1030        self.image.get(DUAL_BAND_OFFSET).is_some_and(|&b| b != 0)
1031    }
1032
1033    /// Read lock setting (0=unlocked, 1=locked).
1034    ///
1035    /// MCP offset `0x1060`.
1036    #[must_use]
1037    pub fn lock(&self) -> bool {
1038        self.image.get(LOCK_OFFSET).is_some_and(|&b| b != 0)
1039    }
1040
1041    /// Read Bluetooth on/off setting (0=off, 1=on).
1042    ///
1043    /// MCP offset `0x1078`.
1044    #[must_use]
1045    pub fn bluetooth(&self) -> bool {
1046        self.image.get(BLUETOOTH_OFFSET).is_some_and(|&b| b != 0)
1047    }
1048
1049    // -----------------------------------------------------------------------
1050    // VFO data accessors (confirmed via memory dump analysis)
1051    // -----------------------------------------------------------------------
1052
1053    /// Read the raw 40-byte VFO entry for a given index (0-5).
1054    ///
1055    /// The VFO data block at `0x0020` contains 6 entries in the same
1056    /// 40-byte format as channel memory records. Returns `None` if the
1057    /// index is out of range or the region extends past the image.
1058    ///
1059    /// # VFO index mapping (estimated)
1060    ///
1061    /// The exact band-to-index mapping needs confirmation via differential
1062    /// dump, but typical Kenwood convention is:
1063    ///
1064    /// | Index | Band |
1065    /// |-------|------|
1066    /// | 0 | Band A VHF |
1067    /// | 1 | Band A 220 MHz |
1068    /// | 2 | Band A UHF |
1069    /// | 3 | Band B VHF |
1070    /// | 4 | Band B 220 MHz |
1071    /// | 5 | Band B UHF |
1072    #[must_use]
1073    pub fn vfo_raw(&self, index: usize) -> Option<&[u8]> {
1074        if index >= VFO_ENTRY_COUNT {
1075            return None;
1076        }
1077        let offset = VFO_DATA_OFFSET + index * VFO_ENTRY_SIZE;
1078        let end = offset + VFO_ENTRY_SIZE;
1079        if end <= self.image.len() {
1080            Some(&self.image[offset..end])
1081        } else {
1082            None
1083        }
1084    }
1085
1086    /// Read the RX frequency from a VFO entry (0-5).
1087    ///
1088    /// Returns the frequency in Hz as a [`Frequency`], or `None` if the
1089    /// index is out of range or the VFO entry is empty (all `0xFF`).
1090    ///
1091    /// Located at `0x0020 + index * 40`. Confirmed via memory dump.
1092    #[must_use]
1093    pub fn vfo_frequency(&self, index: usize) -> Option<Frequency> {
1094        let raw = self.vfo_raw(index)?;
1095        // Check for empty entry (all 0xFF).
1096        if raw.iter().all(|&b| b == 0xFF) {
1097            return None;
1098        }
1099        // Check for zeroed entry.
1100        if raw[..4].iter().all(|&b| b == 0x00) {
1101            return None;
1102        }
1103        let freq = Frequency::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
1104        Some(freq)
1105    }
1106
1107    /// Read the operating mode from a VFO entry (0-5).
1108    ///
1109    /// Returns the flash-encoded mode, or `None` if the index is out of
1110    /// range or the VFO entry is empty. The mode is in byte 0x09 bits
1111    /// \[6:4\] of the 40-byte VFO record.
1112    ///
1113    /// Located at `0x0020 + index * 40 + 0x09`. Confirmed via memory dump.
1114    #[must_use]
1115    pub fn vfo_mode(&self, index: usize) -> Option<MemoryMode> {
1116        let raw = self.vfo_raw(index)?;
1117        if raw.iter().all(|&b| b == 0xFF) {
1118            return None;
1119        }
1120        let mode_bits = (raw[0x09] >> 4) & 0x07;
1121        MemoryMode::try_from(mode_bits).ok()
1122    }
1123
1124    /// Read the TX offset or split frequency from a VFO entry (0-5).
1125    ///
1126    /// Returns the offset/split frequency in Hz, or `None` if the index
1127    /// is out of range or the VFO entry is empty.
1128    ///
1129    /// Located at `0x0020 + index * 40 + 0x04`. Confirmed via memory dump.
1130    #[must_use]
1131    pub fn vfo_tx_offset(&self, index: usize) -> Option<Frequency> {
1132        let raw = self.vfo_raw(index)?;
1133        if raw.iter().all(|&b| b == 0xFF) {
1134            return None;
1135        }
1136        let offset = Frequency::from_le_bytes([raw[4], raw[5], raw[6], raw[7]]);
1137        Some(offset)
1138    }
1139
1140    /// Get the number of non-empty VFO entries (out of 6).
1141    #[must_use]
1142    pub fn vfo_count(&self) -> usize {
1143        (0..VFO_ENTRY_COUNT)
1144            .filter(|&i| self.vfo_frequency(i).is_some())
1145            .count()
1146    }
1147
1148    // -----------------------------------------------------------------------
1149    // Raw numeric accessors for enum-typed settings (for TUI +/- cycling)
1150    // -----------------------------------------------------------------------
1151
1152    /// Read key lock type as raw byte (0=KeyOnly, 1=KeyAndPtt, 2=KeyPttAndDial).
1153    ///
1154    /// MCP offset `0x1061`.
1155    #[must_use]
1156    pub fn key_lock_type_raw(&self) -> u8 {
1157        self.image
1158            .get(KEY_LOCK_TYPE_OFFSET)
1159            .copied()
1160            .unwrap_or(0)
1161            .min(2)
1162    }
1163
1164    /// Read auto power off as raw byte (0=Off, 1=30m, 2=60m, 3=90m, 4=120m).
1165    ///
1166    /// MCP offset `0x10D0`.
1167    #[must_use]
1168    pub fn auto_power_off_raw(&self) -> u8 {
1169        self.image
1170            .get(AUTO_POWER_OFF_OFFSET)
1171            .copied()
1172            .unwrap_or(0)
1173            .min(4)
1174    }
1175
1176    /// Read speed/distance unit as raw byte (0=mph, 1=km/h, 2=knots).
1177    ///
1178    /// MCP offset `0x1077`.
1179    #[must_use]
1180    pub fn speed_distance_unit_raw(&self) -> u8 {
1181        self.image
1182            .get(SPEED_DISTANCE_UNIT_OFFSET)
1183            .copied()
1184            .unwrap_or(0)
1185            .min(2)
1186    }
1187
1188    /// Read altitude/rain unit as raw byte (0=ft/in, 1=m/mm).
1189    ///
1190    /// MCP offset `0x1083`.
1191    #[must_use]
1192    pub fn altitude_rain_unit_raw(&self) -> u8 {
1193        self.image
1194            .get(ALTITUDE_RAIN_UNIT_OFFSET)
1195            .copied()
1196            .unwrap_or(0)
1197            .min(1)
1198    }
1199
1200    /// Read temperature unit as raw byte (0=°F, 1=°C).
1201    ///
1202    /// MCP offset `0x1084`.
1203    #[must_use]
1204    pub fn temperature_unit_raw(&self) -> u8 {
1205        self.image
1206            .get(TEMPERATURE_UNIT_OFFSET)
1207            .copied()
1208            .unwrap_or(0)
1209            .min(1)
1210    }
1211}
1212
1213// ---------------------------------------------------------------------------
1214// SettingsWriter (mutable access)
1215// ---------------------------------------------------------------------------
1216
1217/// Mutable access to the system settings region of the memory image.
1218///
1219/// Provides write methods for settings with verified offsets. Only
1220/// settings with hardware-verified offsets have write accessors to
1221/// prevent corrupting the memory image with unconfirmed offsets.
1222#[derive(Debug)]
1223pub struct SettingsWriter<'a> {
1224    image: &'a mut [u8],
1225}
1226
1227impl<'a> SettingsWriter<'a> {
1228    /// Create a new mutable settings accessor.
1229    pub(crate) const fn new(image: &'a mut [u8]) -> Self {
1230        Self { image }
1231    }
1232
1233    /// Set LCD backlight control setting.
1234    ///
1235    /// MCP offset `0x1069`.
1236    pub fn set_backlight(&mut self, level: u8) {
1237        if let Some(b) = self.image.get_mut(BACKLIGHT_CONTROL_OFFSET) {
1238            *b = level;
1239        }
1240    }
1241
1242    /// Set backlight control (same as `set_backlight`, named for clarity).
1243    ///
1244    /// MCP offset `0x1069`.
1245    pub fn set_backlight_control(&mut self, value: u8) {
1246        if let Some(b) = self.image.get_mut(BACKLIGHT_CONTROL_OFFSET) {
1247            *b = value;
1248        }
1249    }
1250
1251    /// Set backlight timer.
1252    ///
1253    /// MCP offset `0x106A`.
1254    pub fn set_backlight_timer(&mut self, value: u8) {
1255        if let Some(b) = self.image.get_mut(BACKLIGHT_TIMER_OFFSET) {
1256            *b = value;
1257        }
1258    }
1259
1260    /// Set beep volume (1-7).
1261    ///
1262    /// Values above 7 are clamped to 7.
1263    ///
1264    /// MCP offset `0x1072`.
1265    pub fn set_beep_volume(&mut self, volume: u8) {
1266        if let Some(b) = self.image.get_mut(BEEP_VOLUME_OFFSET) {
1267            *b = volume.min(7);
1268        }
1269    }
1270
1271    /// Set auto power off.
1272    ///
1273    /// MCP offset `0x10D0`.
1274    pub fn set_auto_power_off(&mut self, value: AutoPowerOff) {
1275        if let Some(b) = self.image.get_mut(AUTO_POWER_OFF_OFFSET) {
1276            *b = match value {
1277                AutoPowerOff::Off => 0,
1278                AutoPowerOff::Min30 => 1,
1279                AutoPowerOff::Min60 => 2,
1280                AutoPowerOff::Min90 => 3,
1281                AutoPowerOff::Min120 => 4,
1282            };
1283        }
1284    }
1285
1286    /// Set battery saver on/off.
1287    ///
1288    /// MCP offset `0x10C0`.
1289    pub fn set_battery_saver(&mut self, enabled: bool) {
1290        if let Some(b) = self.image.get_mut(BATTERY_SAVER_OFFSET) {
1291            *b = u8::from(enabled);
1292        }
1293    }
1294
1295    /// Set key lock type.
1296    ///
1297    /// MCP offset `0x1061`.
1298    pub fn set_key_lock_type(&mut self, value: KeyLockType) {
1299        if let Some(b) = self.image.get_mut(KEY_LOCK_TYPE_OFFSET) {
1300            *b = match value {
1301                KeyLockType::KeyOnly => 0,
1302                KeyLockType::KeyAndPtt => 1,
1303                KeyLockType::KeyPttAndDial => 2,
1304            };
1305        }
1306    }
1307
1308    /// Set language.
1309    ///
1310    /// MCP offset `0x1006`.
1311    pub fn set_language(&mut self, value: Language) {
1312        if let Some(b) = self.image.get_mut(LANGUAGE_OFFSET) {
1313            *b = match value {
1314                Language::English => 0,
1315                Language::Japanese => 1,
1316            };
1317        }
1318    }
1319
1320    /// Set speed/distance display unit.
1321    ///
1322    /// MCP offset `0x1077`.
1323    pub fn set_speed_distance_unit(&mut self, value: SpeedDistanceUnit) {
1324        if let Some(b) = self.image.get_mut(SPEED_DISTANCE_UNIT_OFFSET) {
1325            *b = match value {
1326                SpeedDistanceUnit::MilesPerHour => 0,
1327                SpeedDistanceUnit::KilometersPerHour => 1,
1328                SpeedDistanceUnit::Knots => 2,
1329            };
1330        }
1331    }
1332
1333    /// Set altitude/rain display unit.
1334    ///
1335    /// MCP offset `0x1083`.
1336    pub fn set_altitude_rain_unit(&mut self, value: AltitudeRainUnit) {
1337        if let Some(b) = self.image.get_mut(ALTITUDE_RAIN_UNIT_OFFSET) {
1338            *b = match value {
1339                AltitudeRainUnit::FeetInch => 0,
1340                AltitudeRainUnit::MetersMm => 1,
1341            };
1342        }
1343    }
1344
1345    /// Set temperature display unit.
1346    ///
1347    /// MCP offset `0x1084`.
1348    pub fn set_temperature_unit(&mut self, value: TemperatureUnit) {
1349        if let Some(b) = self.image.get_mut(TEMPERATURE_UNIT_OFFSET) {
1350            *b = match value {
1351                TemperatureUnit::Fahrenheit => 0,
1352                TemperatureUnit::Celsius => 1,
1353            };
1354        }
1355    }
1356
1357    /// Set VOX delay (in 100 ms units, clamped to 30).
1358    ///
1359    /// MCP offset `0x101D`.
1360    pub fn set_vox_delay(&mut self, delay: u8) {
1361        if let Some(b) = self.image.get_mut(VOX_DELAY_OFFSET) {
1362            *b = delay.min(30);
1363        }
1364    }
1365
1366    /// Set VOX TX-on-busy on/off.
1367    ///
1368    /// MCP offset `0x101E`.
1369    pub fn set_vox_tx_on_busy(&mut self, enabled: bool) {
1370        if let Some(b) = self.image.get_mut(VOX_TX_ON_BUSY_OFFSET) {
1371            *b = u8::from(enabled);
1372        }
1373    }
1374
1375    /// Set squelch level for Band A (0-6).
1376    ///
1377    /// MCP offset `0x100D`.
1378    pub fn set_squelch_a(&mut self, level: u8) {
1379        if let Some(b) = self.image.get_mut(SQUELCH_A_OFFSET) {
1380            *b = level.min(6);
1381        }
1382    }
1383
1384    /// Set squelch level for Band B (0-6).
1385    ///
1386    /// MCP offset `0x100E`.
1387    pub fn set_squelch_b(&mut self, level: u8) {
1388        if let Some(b) = self.image.get_mut(SQUELCH_B_OFFSET) {
1389            *b = level.min(6);
1390        }
1391    }
1392
1393    /// Set FM narrow setting.
1394    ///
1395    /// MCP offset `0x100F`.
1396    pub fn set_fm_narrow(&mut self, value: u8) {
1397        if let Some(b) = self.image.get_mut(FM_NARROW_OFFSET) {
1398            *b = value;
1399        }
1400    }
1401
1402    /// Set auto filter setting.
1403    ///
1404    /// MCP offset `0x100C`.
1405    pub fn set_auto_filter(&mut self, value: u8) {
1406        if let Some(b) = self.image.get_mut(AUTO_FILTER_OFFSET) {
1407            *b = value;
1408        }
1409    }
1410
1411    /// Set scan resume setting.
1412    ///
1413    /// MCP offset `0x1007`.
1414    pub fn set_scan_resume(&mut self, value: u8) {
1415        if let Some(b) = self.image.get_mut(SCAN_RESUME_OFFSET) {
1416            *b = value;
1417        }
1418    }
1419
1420    /// Set digital scan resume setting.
1421    ///
1422    /// MCP offset `0x1008`.
1423    pub fn set_digital_scan_resume(&mut self, value: u8) {
1424        if let Some(b) = self.image.get_mut(DIGITAL_SCAN_RESUME_OFFSET) {
1425            *b = value;
1426        }
1427    }
1428
1429    /// Set timeout timer.
1430    ///
1431    /// MCP offset `0x1018`.
1432    pub fn set_timeout_timer(&mut self, value: u8) {
1433        if let Some(b) = self.image.get_mut(TIMEOUT_TIMER_OFFSET) {
1434            *b = value;
1435        }
1436    }
1437
1438    /// Set TX inhibit on/off.
1439    ///
1440    /// MCP offset `0x1019`.
1441    pub fn set_tx_inhibit(&mut self, enabled: bool) {
1442        if let Some(b) = self.image.get_mut(TX_INHIBIT_OFFSET) {
1443            *b = u8::from(enabled);
1444        }
1445    }
1446
1447    /// Set beat shift on/off.
1448    ///
1449    /// MCP offset `0x101A`.
1450    pub fn set_beat_shift(&mut self, enabled: bool) {
1451        if let Some(b) = self.image.get_mut(BEAT_SHIFT_OFFSET) {
1452            *b = u8::from(enabled);
1453        }
1454    }
1455
1456    /// Set CW break-in on/off.
1457    ///
1458    /// MCP offset `0x101F`.
1459    pub fn set_cw_break_in(&mut self, enabled: bool) {
1460        if let Some(b) = self.image.get_mut(CW_BREAK_IN_OFFSET) {
1461            *b = u8::from(enabled);
1462        }
1463    }
1464
1465    /// Set CW pitch.
1466    ///
1467    /// MCP offset `0x1021`.
1468    pub fn set_cw_pitch(&mut self, value: u8) {
1469        if let Some(b) = self.image.get_mut(CW_PITCH_OFFSET) {
1470            *b = value;
1471        }
1472    }
1473
1474    /// Set DTMF speed.
1475    ///
1476    /// MCP offset `0x1024`.
1477    pub fn set_dtmf_speed(&mut self, value: u8) {
1478        if let Some(b) = self.image.get_mut(DTMF_SPEED_OFFSET) {
1479            *b = value;
1480        }
1481    }
1482
1483    /// Set mic sensitivity.
1484    ///
1485    /// MCP offset `0x1040`.
1486    pub fn set_mic_sensitivity(&mut self, value: u8) {
1487        if let Some(b) = self.image.get_mut(MIC_SENSITIVITY_OFFSET) {
1488            *b = value;
1489        }
1490    }
1491
1492    /// Set PF key 1 assignment.
1493    ///
1494    /// MCP offset `0x1041`.
1495    pub fn set_pf_key1(&mut self, value: u8) {
1496        if let Some(b) = self.image.get_mut(PF_KEY1_OFFSET) {
1497            *b = value;
1498        }
1499    }
1500
1501    /// Set PF key 2 assignment.
1502    ///
1503    /// MCP offset `0x1042`.
1504    pub fn set_pf_key2(&mut self, value: u8) {
1505        if let Some(b) = self.image.get_mut(PF_KEY2_OFFSET) {
1506            *b = value;
1507        }
1508    }
1509
1510    /// Set APRS lock on/off.
1511    ///
1512    /// MCP offset `0x1097`.
1513    pub fn set_aprs_lock(&mut self, enabled: bool) {
1514        if let Some(b) = self.image.get_mut(APRS_LOCK_OFFSET) {
1515            *b = u8::from(enabled);
1516        }
1517    }
1518
1519    /// Set dual display size.
1520    ///
1521    /// MCP offset `0x1066`.
1522    pub fn set_dual_display_size(&mut self, value: u8) {
1523        if let Some(b) = self.image.get_mut(DUAL_DISPLAY_SIZE_OFFSET) {
1524            *b = value;
1525        }
1526    }
1527
1528    /// Set display area.
1529    ///
1530    /// MCP offset `0x1067`.
1531    pub fn set_display_area(&mut self, value: u8) {
1532        if let Some(b) = self.image.get_mut(DISPLAY_AREA_OFFSET) {
1533            *b = value;
1534        }
1535    }
1536
1537    /// Set info line setting.
1538    ///
1539    /// MCP offset `0x1068`.
1540    pub fn set_info_line(&mut self, value: u8) {
1541        if let Some(b) = self.image.get_mut(INFO_LINE_OFFSET) {
1542            *b = value;
1543        }
1544    }
1545
1546    /// Set volume lock on/off.
1547    ///
1548    /// MCP offset `0x1076`.
1549    pub fn set_volume_lock(&mut self, enabled: bool) {
1550        if let Some(b) = self.image.get_mut(VOLUME_LOCK_OFFSET) {
1551            *b = u8::from(enabled);
1552        }
1553    }
1554
1555    /// Set Bluetooth auto-connect on/off.
1556    ///
1557    /// MCP offset `0x1079`.
1558    pub fn set_bt_auto_connect(&mut self, enabled: bool) {
1559        if let Some(b) = self.image.get_mut(BT_AUTO_CONNECT_OFFSET) {
1560            *b = u8::from(enabled);
1561        }
1562    }
1563
1564    /// Set PC output mode.
1565    ///
1566    /// MCP offset `0x1085`.
1567    pub fn set_pc_output_mode(&mut self, value: u8) {
1568        if let Some(b) = self.image.get_mut(PC_OUTPUT_MODE_OFFSET) {
1569            *b = value;
1570        }
1571    }
1572
1573    /// Set APRS USB mode.
1574    ///
1575    /// MCP offset `0x1086`.
1576    pub fn set_aprs_usb_mode(&mut self, value: u8) {
1577        if let Some(b) = self.image.get_mut(APRS_USB_MODE_OFFSET) {
1578            *b = value;
1579        }
1580    }
1581
1582    /// Set power-on message flag on/off.
1583    ///
1584    /// MCP offset `0x1087`.
1585    pub fn set_power_on_message_flag(&mut self, enabled: bool) {
1586        if let Some(b) = self.image.get_mut(POWER_ON_MESSAGE_FLAG_OFFSET) {
1587            *b = u8::from(enabled);
1588        }
1589    }
1590
1591    /// Set dual band MCP setting on/off.
1592    ///
1593    /// MCP offset `0x1096`.
1594    pub fn set_dual_band_mcp(&mut self, enabled: bool) {
1595        if let Some(b) = self.image.get_mut(DUAL_BAND_MCP_OFFSET) {
1596            *b = u8::from(enabled);
1597        }
1598    }
1599
1600    /// Set key beep on/off.
1601    ///
1602    /// MCP offset `0x1071`.
1603    pub fn set_key_beep(&mut self, enabled: bool) {
1604        if let Some(b) = self.image.get_mut(KEY_BEEP_OFFSET) {
1605            *b = u8::from(enabled);
1606        }
1607    }
1608
1609    /// Set VOX enabled on/off.
1610    ///
1611    /// MCP offset `0x101B`.
1612    pub fn set_vox_enabled(&mut self, enabled: bool) {
1613        if let Some(b) = self.image.get_mut(VOX_ENABLED_OFFSET) {
1614            *b = u8::from(enabled);
1615        }
1616    }
1617
1618    /// Set VOX gain level (0-9).
1619    ///
1620    /// Values above 9 are clamped to 9.
1621    ///
1622    /// MCP offset `0x101C`.
1623    pub fn set_vox_gain(&mut self, gain: u8) {
1624        if let Some(b) = self.image.get_mut(VOX_GAIN_OFFSET) {
1625            *b = gain.min(9);
1626        }
1627    }
1628
1629    /// Set lock on/off.
1630    ///
1631    /// MCP offset `0x1060`.
1632    pub fn set_lock(&mut self, locked: bool) {
1633        if let Some(b) = self.image.get_mut(LOCK_OFFSET) {
1634            *b = u8::from(locked);
1635        }
1636    }
1637
1638    /// Set dual-band display on/off.
1639    ///
1640    /// MCP offset `0x0396`.
1641    pub fn set_dual_band(&mut self, enabled: bool) {
1642        if let Some(b) = self.image.get_mut(DUAL_BAND_OFFSET) {
1643            *b = u8::from(enabled);
1644        }
1645    }
1646
1647    /// Set Band A attenuator on/off.
1648    ///
1649    /// MCP offset `0x035C`.
1650    pub fn set_attenuator_a(&mut self, enabled: bool) {
1651        if let Some(b) = self.image.get_mut(ATTENUATOR_A_OFFSET) {
1652            *b = u8::from(enabled);
1653        }
1654    }
1655
1656    /// Set Band A power level.
1657    ///
1658    /// MCP offset `0x0359`.
1659    pub fn set_power_level_a(&mut self, level: PowerLevel) {
1660        if let Some(b) = self.image.get_mut(POWER_LEVEL_A_OFFSET) {
1661            *b = u8::from(level);
1662        }
1663    }
1664
1665    /// Set Bluetooth on/off.
1666    ///
1667    /// MCP offset `0x1078`.
1668    pub fn set_bluetooth(&mut self, enabled: bool) {
1669        if let Some(b) = self.image.get_mut(BLUETOOTH_OFFSET) {
1670            *b = u8::from(enabled);
1671        }
1672    }
1673
1674    // -----------------------------------------------------------------------
1675    // Additional writer methods for settings not yet covered above
1676    // -----------------------------------------------------------------------
1677
1678    /// Set SSB high-cut filter setting.
1679    ///
1680    /// MCP offset `0x1011`.
1681    pub fn set_ssb_high_cut(&mut self, value: u8) {
1682        if let Some(b) = self.image.get_mut(SSB_HIGH_CUT_OFFSET) {
1683            *b = value;
1684        }
1685    }
1686
1687    /// Set CW high-cut filter setting.
1688    ///
1689    /// MCP offset `0x1012`.
1690    pub fn set_cw_high_cut(&mut self, value: u8) {
1691        if let Some(b) = self.image.get_mut(CW_HIGH_CUT_OFFSET) {
1692            *b = value;
1693        }
1694    }
1695
1696    /// Set AM high-cut filter setting.
1697    ///
1698    /// MCP offset `0x1013`.
1699    pub fn set_am_high_cut(&mut self, value: u8) {
1700        if let Some(b) = self.image.get_mut(AM_HIGH_CUT_OFFSET) {
1701            *b = value;
1702        }
1703    }
1704
1705    /// Set scan restart time.
1706    ///
1707    /// MCP offset `0x1009`.
1708    pub fn set_scan_restart_time(&mut self, value: u8) {
1709        if let Some(b) = self.image.get_mut(SCAN_RESTART_TIME_OFFSET) {
1710            *b = value;
1711        }
1712    }
1713
1714    /// Set scan restart carrier setting.
1715    ///
1716    /// MCP offset `0x100A`.
1717    pub fn set_scan_restart_carrier(&mut self, value: u8) {
1718        if let Some(b) = self.image.get_mut(SCAN_RESTART_CARRIER_OFFSET) {
1719            *b = value;
1720        }
1721    }
1722
1723    /// Set CW delay time.
1724    ///
1725    /// MCP offset `0x1020`.
1726    pub fn set_cw_delay_time(&mut self, value: u8) {
1727        if let Some(b) = self.image.get_mut(CW_DELAY_TIME_OFFSET) {
1728            *b = value;
1729        }
1730    }
1731
1732    /// Set DTMF pause time.
1733    ///
1734    /// MCP offset `0x1026`.
1735    pub fn set_dtmf_pause_time(&mut self, value: u8) {
1736        if let Some(b) = self.image.get_mut(DTMF_PAUSE_TIME_OFFSET) {
1737            *b = value;
1738        }
1739    }
1740
1741    /// Set DTMF TX hold on/off.
1742    ///
1743    /// MCP offset `0x1027`.
1744    pub fn set_dtmf_tx_hold(&mut self, enabled: bool) {
1745        if let Some(b) = self.image.get_mut(DTMF_TX_HOLD_OFFSET) {
1746            *b = u8::from(enabled);
1747        }
1748    }
1749
1750    /// Set repeater auto offset on/off.
1751    ///
1752    /// MCP offset `0x1030`.
1753    pub fn set_repeater_auto_offset(&mut self, enabled: bool) {
1754        if let Some(b) = self.image.get_mut(REPEATER_AUTO_OFFSET_OFFSET) {
1755            *b = u8::from(enabled);
1756        }
1757    }
1758
1759    /// Set repeater call key function.
1760    ///
1761    /// MCP offset `0x1031`.
1762    pub fn set_repeater_call_key(&mut self, value: u8) {
1763        if let Some(b) = self.image.get_mut(REPEATER_CALL_KEY_OFFSET) {
1764            *b = value;
1765        }
1766    }
1767
1768    /// Set lock key A on/off.
1769    ///
1770    /// MCP offset `0x1062`.
1771    pub fn set_lock_key_a(&mut self, enabled: bool) {
1772        if let Some(b) = self.image.get_mut(LOCK_KEY_A_OFFSET) {
1773            *b = u8::from(enabled);
1774        }
1775    }
1776
1777    /// Set lock key B on/off.
1778    ///
1779    /// MCP offset `0x1063`.
1780    pub fn set_lock_key_b(&mut self, enabled: bool) {
1781        if let Some(b) = self.image.get_mut(LOCK_KEY_B_OFFSET) {
1782            *b = u8::from(enabled);
1783        }
1784    }
1785
1786    /// Set lock key C on/off.
1787    ///
1788    /// MCP offset `0x1064`.
1789    pub fn set_lock_key_c(&mut self, enabled: bool) {
1790        if let Some(b) = self.image.get_mut(LOCK_KEY_C_OFFSET) {
1791            *b = u8::from(enabled);
1792        }
1793    }
1794
1795    /// Set lock PTT key on/off.
1796    ///
1797    /// MCP offset `0x1065`.
1798    pub fn set_lock_key_ptt(&mut self, enabled: bool) {
1799        if let Some(b) = self.image.get_mut(LOCK_KEY_PTT_OFFSET) {
1800            *b = u8::from(enabled);
1801        }
1802    }
1803
1804    /// Set display hold time.
1805    ///
1806    /// MCP offset `0x106B`.
1807    pub fn set_display_hold_time(&mut self, value: u8) {
1808        if let Some(b) = self.image.get_mut(DISPLAY_HOLD_TIME_OFFSET) {
1809            *b = value;
1810        }
1811    }
1812
1813    /// Set display method.
1814    ///
1815    /// MCP offset `0x106C`.
1816    pub fn set_display_method(&mut self, value: u8) {
1817        if let Some(b) = self.image.get_mut(DISPLAY_METHOD_OFFSET) {
1818            *b = value;
1819        }
1820    }
1821
1822    /// Set power-on display setting.
1823    ///
1824    /// MCP offset `0x106D`.
1825    pub fn set_power_on_display(&mut self, value: u8) {
1826        if let Some(b) = self.image.get_mut(POWER_ON_DISPLAY_OFFSET) {
1827            *b = value;
1828        }
1829    }
1830
1831    /// Set EMR volume level.
1832    ///
1833    /// MCP offset `0x106E`.
1834    pub fn set_emr_volume_level(&mut self, value: u8) {
1835        if let Some(b) = self.image.get_mut(EMR_VOLUME_LEVEL_OFFSET) {
1836            *b = value;
1837        }
1838    }
1839
1840    /// Set auto mute return time.
1841    ///
1842    /// MCP offset `0x106F`.
1843    pub fn set_auto_mute_return_time(&mut self, value: u8) {
1844        if let Some(b) = self.image.get_mut(AUTO_MUTE_RETURN_TIME_OFFSET) {
1845            *b = value;
1846        }
1847    }
1848
1849    /// Set announce on/off.
1850    ///
1851    /// MCP offset `0x1070`.
1852    pub fn set_announce(&mut self, enabled: bool) {
1853        if let Some(b) = self.image.get_mut(ANNOUNCE_OFFSET) {
1854            *b = u8::from(enabled);
1855        }
1856    }
1857
1858    /// Set voice language.
1859    ///
1860    /// MCP offset `0x1073`.
1861    pub fn set_voice_language(&mut self, value: u8) {
1862        if let Some(b) = self.image.get_mut(VOICE_LANGUAGE_OFFSET) {
1863            *b = value;
1864        }
1865    }
1866
1867    /// Set voice volume.
1868    ///
1869    /// MCP offset `0x1074`.
1870    pub fn set_voice_volume(&mut self, value: u8) {
1871        if let Some(b) = self.image.get_mut(VOICE_VOLUME_OFFSET) {
1872            *b = value;
1873        }
1874    }
1875
1876    /// Set voice speed.
1877    ///
1878    /// MCP offset `0x1075`.
1879    pub fn set_voice_speed(&mut self, value: u8) {
1880        if let Some(b) = self.image.get_mut(VOICE_SPEED_OFFSET) {
1881            *b = value;
1882        }
1883    }
1884
1885    /// Set USB audio output on/off.
1886    ///
1887    /// MCP offset `0x1094`.
1888    pub fn set_usb_audio_output(&mut self, enabled: bool) {
1889        if let Some(b) = self.image.get_mut(USB_AUDIO_OUTPUT_OFFSET) {
1890            *b = u8::from(enabled);
1891        }
1892    }
1893
1894    /// Set internet link on/off.
1895    ///
1896    /// MCP offset `0x1095`.
1897    pub fn set_internet_link(&mut self, enabled: bool) {
1898        if let Some(b) = self.image.get_mut(INTERNET_LINK_OFFSET) {
1899            *b = u8::from(enabled);
1900        }
1901    }
1902
1903    /// Set GPS/BT interface setting.
1904    ///
1905    /// MCP offset `0x1080`.
1906    pub fn set_gps_bt_interface(&mut self, value: u8) {
1907        if let Some(b) = self.image.get_mut(GPS_BT_INTERFACE_OFFSET) {
1908            *b = value;
1909        }
1910    }
1911
1912    // -----------------------------------------------------------------------
1913    // Raw numeric setters for enum-typed settings (for TUI +/- cycling)
1914    // -----------------------------------------------------------------------
1915
1916    /// Set key lock type as raw byte (0=KeyOnly, 1=KeyAndPtt, 2=KeyPttAndDial).
1917    ///
1918    /// MCP offset `0x1061`.
1919    pub fn set_key_lock_type_raw(&mut self, value: u8) {
1920        if let Some(b) = self.image.get_mut(KEY_LOCK_TYPE_OFFSET) {
1921            *b = value.min(2);
1922        }
1923    }
1924
1925    /// Set auto power off as raw byte (0=Off, 1=30m, 2=60m, 3=90m, 4=120m).
1926    ///
1927    /// MCP offset `0x10D0`.
1928    pub fn set_auto_power_off_raw(&mut self, value: u8) {
1929        if let Some(b) = self.image.get_mut(AUTO_POWER_OFF_OFFSET) {
1930            *b = value.min(4);
1931        }
1932    }
1933
1934    /// Set speed/distance unit as raw byte (0=mph, 1=km/h, 2=knots).
1935    ///
1936    /// MCP offset `0x1077`.
1937    pub fn set_speed_distance_unit_raw(&mut self, value: u8) {
1938        if let Some(b) = self.image.get_mut(SPEED_DISTANCE_UNIT_OFFSET) {
1939            *b = value.min(2);
1940        }
1941    }
1942
1943    /// Set altitude/rain unit as raw byte (0=ft/in, 1=m/mm).
1944    ///
1945    /// MCP offset `0x1083`.
1946    pub fn set_altitude_rain_unit_raw(&mut self, value: u8) {
1947        if let Some(b) = self.image.get_mut(ALTITUDE_RAIN_UNIT_OFFSET) {
1948            *b = value.min(1);
1949        }
1950    }
1951
1952    /// Set temperature unit as raw byte (0=°F, 1=°C).
1953    ///
1954    /// MCP offset `0x1084`.
1955    pub fn set_temperature_unit_raw(&mut self, value: u8) {
1956        if let Some(b) = self.image.get_mut(TEMPERATURE_UNIT_OFFSET) {
1957            *b = value.min(1);
1958        }
1959    }
1960}
1961
1962/// Extract a null-terminated ASCII string from the image at a given offset.
1963fn extract_string(image: &[u8], offset: usize, max_len: usize) -> String {
1964    let end = offset + max_len;
1965    if end > image.len() {
1966        return String::new();
1967    }
1968    let slice = &image[offset..end];
1969    let nul = slice.iter().position(|&b| b == 0).unwrap_or(max_len);
1970    String::from_utf8_lossy(&slice[..nul]).trim().to_string()
1971}
1972
1973// ---------------------------------------------------------------------------
1974// Tests
1975// ---------------------------------------------------------------------------
1976
1977#[cfg(test)]
1978mod tests {
1979    use super::*;
1980    use crate::protocol::programming::TOTAL_SIZE;
1981    use crate::types::settings::{
1982        AltitudeRainUnit, AutoPowerOff, KeyLockType, Language, SpeedDistanceUnit, TemperatureUnit,
1983    };
1984
1985    fn make_settings_image() -> Vec<u8> {
1986        let mut image = vec![0x00_u8; TOTAL_SIZE];
1987
1988        // Write a power-on message at 0x11C0.
1989        let msg = b"Hello D75!\0\0\0\0\0\0";
1990        image[POWER_ON_MESSAGE_OFFSET..POWER_ON_MESSAGE_OFFSET + 16].copy_from_slice(msg);
1991
1992        // Write a model name at 0x11D0.
1993        let model = b"TH-D75A\0\0\0\0\0\0\0\0\0";
1994        image[MODEL_NAME_OFFSET..MODEL_NAME_OFFSET + 16].copy_from_slice(model);
1995
1996        image
1997    }
1998
1999    #[test]
2000    fn settings_power_on_message() {
2001        let image = make_settings_image();
2002        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2003        let settings = mi.settings();
2004        assert_eq!(settings.power_on_message(), "Hello D75!");
2005    }
2006
2007    #[test]
2008    fn settings_model_name() {
2009        let image = make_settings_image();
2010        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2011        let settings = mi.settings();
2012        assert_eq!(settings.model_name(), "TH-D75A");
2013    }
2014
2015    #[test]
2016    fn settings_raw_not_none() {
2017        let image = make_settings_image();
2018        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2019        let settings = mi.settings();
2020        let raw = settings.raw().unwrap();
2021        assert_eq!(raw.len(), SETTINGS_SIZE);
2022    }
2023
2024    #[test]
2025    fn settings_read_bytes() {
2026        let image = make_settings_image();
2027        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2028        let settings = mi.settings();
2029        // Read the power-on message via raw bytes.
2030        let bytes = settings.read_bytes(POWER_ON_MESSAGE_OFFSET, 10).unwrap();
2031        assert_eq!(&bytes[..10], b"Hello D75!");
2032    }
2033
2034    // -----------------------------------------------------------------------
2035    // Read accessor tests (verified offsets)
2036    // -----------------------------------------------------------------------
2037
2038    #[test]
2039    fn settings_key_beep() {
2040        let mut image = make_settings_image();
2041        image[KEY_BEEP_OFFSET] = 1;
2042        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2043        assert!(mi.settings().key_beep());
2044    }
2045
2046    #[test]
2047    fn settings_key_beep_off() {
2048        let image = make_settings_image();
2049        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2050        assert!(!mi.settings().key_beep());
2051    }
2052
2053    #[test]
2054    fn settings_vox() {
2055        let mut image = make_settings_image();
2056        image[VOX_ENABLED_OFFSET] = 1;
2057        image[VOX_GAIN_OFFSET] = 7;
2058        image[VOX_DELAY_OFFSET] = 5;
2059        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2060        let settings = mi.settings();
2061        assert!(settings.vox_enabled());
2062        assert_eq!(settings.vox_gain(), 7);
2063        assert_eq!(settings.vox_delay(), 5);
2064    }
2065
2066    #[test]
2067    fn settings_lock() {
2068        let mut image = make_settings_image();
2069        image[LOCK_OFFSET] = 1;
2070        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2071        assert!(mi.settings().lock());
2072    }
2073
2074    #[test]
2075    fn settings_lock_off() {
2076        let image = make_settings_image();
2077        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2078        assert!(!mi.settings().lock());
2079    }
2080
2081    #[test]
2082    fn settings_dual_band() {
2083        let mut image = make_settings_image();
2084        image[DUAL_BAND_OFFSET] = 1;
2085        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2086        assert!(mi.settings().dual_band());
2087    }
2088
2089    #[test]
2090    fn settings_dual_band_off() {
2091        let image = make_settings_image();
2092        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2093        assert!(!mi.settings().dual_band());
2094    }
2095
2096    #[test]
2097    fn settings_attenuator_a() {
2098        let mut image = make_settings_image();
2099        image[ATTENUATOR_A_OFFSET] = 1;
2100        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2101        assert!(mi.settings().attenuator_a());
2102    }
2103
2104    #[test]
2105    fn settings_attenuator_a_off() {
2106        let image = make_settings_image();
2107        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2108        assert!(!mi.settings().attenuator_a());
2109    }
2110
2111    #[test]
2112    fn settings_power_level_a() {
2113        let mut image = make_settings_image();
2114        image[POWER_LEVEL_A_OFFSET] = 2; // Lo
2115        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2116        assert_eq!(mi.settings().power_level_a(), PowerLevel::Low);
2117    }
2118
2119    #[test]
2120    fn settings_power_level_a_default() {
2121        let image = make_settings_image();
2122        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2123        // 0x00 maps to High.
2124        assert_eq!(mi.settings().power_level_a(), PowerLevel::High);
2125    }
2126
2127    #[test]
2128    fn settings_power_level_a_invalid_defaults_to_high() {
2129        let mut image = make_settings_image();
2130        image[POWER_LEVEL_A_OFFSET] = 0xFF;
2131        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2132        assert_eq!(mi.settings().power_level_a(), PowerLevel::High);
2133    }
2134
2135    #[test]
2136    fn settings_bluetooth() {
2137        let mut image = make_settings_image();
2138        image[BLUETOOTH_OFFSET] = 1;
2139        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2140        assert!(mi.settings().bluetooth());
2141    }
2142
2143    #[test]
2144    fn settings_bluetooth_off() {
2145        let image = make_settings_image();
2146        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2147        assert!(!mi.settings().bluetooth());
2148    }
2149
2150    // -----------------------------------------------------------------------
2151    // Read accessor tests (firmware analysis offsets)
2152    // -----------------------------------------------------------------------
2153
2154    #[test]
2155    fn settings_beep_volume() {
2156        let mut image = make_settings_image();
2157        image[BEEP_VOLUME_OFFSET] = 5;
2158        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2159        assert_eq!(mi.settings().beep_volume(), 5);
2160    }
2161
2162    #[test]
2163    fn settings_beep_volume_clamped() {
2164        let mut image = make_settings_image();
2165        image[BEEP_VOLUME_OFFSET] = 0xFF;
2166        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2167        assert_eq!(mi.settings().beep_volume(), 7);
2168    }
2169
2170    #[test]
2171    fn settings_backlight() {
2172        let mut image = make_settings_image();
2173        image[BACKLIGHT_CONTROL_OFFSET] = 4;
2174        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2175        assert_eq!(mi.settings().backlight(), 4);
2176    }
2177
2178    #[test]
2179    fn settings_auto_power_off() {
2180        let mut image = make_settings_image();
2181        image[AUTO_POWER_OFF_OFFSET] = 2;
2182        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2183        assert_eq!(mi.settings().auto_power_off(), AutoPowerOff::Min60);
2184    }
2185
2186    #[test]
2187    fn settings_auto_power_off_unknown_defaults_to_off() {
2188        let mut image = make_settings_image();
2189        image[AUTO_POWER_OFF_OFFSET] = 0xFF;
2190        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2191        assert_eq!(mi.settings().auto_power_off(), AutoPowerOff::Off);
2192    }
2193
2194    #[test]
2195    fn settings_battery_saver() {
2196        let mut image = make_settings_image();
2197        image[BATTERY_SAVER_OFFSET] = 1;
2198        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2199        assert!(mi.settings().battery_saver());
2200    }
2201
2202    #[test]
2203    fn settings_key_lock_type() {
2204        let mut image = make_settings_image();
2205        image[KEY_LOCK_TYPE_OFFSET] = 2;
2206        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2207        assert_eq!(mi.settings().key_lock_type(), KeyLockType::KeyPttAndDial);
2208    }
2209
2210    #[test]
2211    fn settings_display_units() {
2212        let mut image = make_settings_image();
2213        image[SPEED_DISTANCE_UNIT_OFFSET] = 1; // km/h
2214        image[ALTITUDE_RAIN_UNIT_OFFSET] = 1; // m/mm
2215        image[TEMPERATURE_UNIT_OFFSET] = 1; // Celsius
2216        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2217        let units = mi.settings().display_units();
2218        assert_eq!(units.speed_distance, SpeedDistanceUnit::KilometersPerHour);
2219        assert_eq!(units.altitude_rain, AltitudeRainUnit::MetersMm);
2220        assert_eq!(units.temperature, TemperatureUnit::Celsius);
2221    }
2222
2223    #[test]
2224    fn settings_language() {
2225        let mut image = make_settings_image();
2226        image[LANGUAGE_OFFSET] = 1;
2227        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2228        assert_eq!(mi.settings().language(), Language::Japanese);
2229    }
2230
2231    #[test]
2232    fn settings_squelch() {
2233        let mut image = make_settings_image();
2234        image[SQUELCH_A_OFFSET] = 3;
2235        image[SQUELCH_B_OFFSET] = 4;
2236        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2237        let settings = mi.settings();
2238        assert_eq!(settings.squelch_a(), 3);
2239        assert_eq!(settings.squelch_b(), 4);
2240    }
2241
2242    #[test]
2243    fn settings_squelch_clamped() {
2244        let mut image = make_settings_image();
2245        image[SQUELCH_A_OFFSET] = 0xFF;
2246        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2247        assert_eq!(mi.settings().squelch_a(), 6);
2248    }
2249
2250    // -----------------------------------------------------------------------
2251    // Write accessor tests (SettingsWriter)
2252    // -----------------------------------------------------------------------
2253
2254    #[test]
2255    fn write_key_beep() {
2256        let image = make_settings_image();
2257        let mut mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2258        assert!(!mi.settings().key_beep());
2259        mi.settings_mut().set_key_beep(true);
2260        assert!(mi.settings().key_beep());
2261        mi.settings_mut().set_key_beep(false);
2262        assert!(!mi.settings().key_beep());
2263    }
2264
2265    #[test]
2266    fn write_vox_enabled() {
2267        let image = make_settings_image();
2268        let mut mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2269        assert!(!mi.settings().vox_enabled());
2270        mi.settings_mut().set_vox_enabled(true);
2271        assert!(mi.settings().vox_enabled());
2272        mi.settings_mut().set_vox_enabled(false);
2273        assert!(!mi.settings().vox_enabled());
2274    }
2275
2276    #[test]
2277    fn write_vox_gain() {
2278        let image = make_settings_image();
2279        let mut mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2280        mi.settings_mut().set_vox_gain(7);
2281        assert_eq!(mi.settings().vox_gain(), 7);
2282    }
2283
2284    #[test]
2285    fn write_vox_gain_clamped() {
2286        let image = make_settings_image();
2287        let mut mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2288        mi.settings_mut().set_vox_gain(0xFF);
2289        assert_eq!(mi.settings().vox_gain(), 9);
2290    }
2291
2292    #[test]
2293    fn write_lock() {
2294        let image = make_settings_image();
2295        let mut mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2296        assert!(!mi.settings().lock());
2297        mi.settings_mut().set_lock(true);
2298        assert!(mi.settings().lock());
2299        mi.settings_mut().set_lock(false);
2300        assert!(!mi.settings().lock());
2301    }
2302
2303    #[test]
2304    fn write_dual_band() {
2305        let image = make_settings_image();
2306        let mut mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2307        assert!(!mi.settings().dual_band());
2308        mi.settings_mut().set_dual_band(true);
2309        assert!(mi.settings().dual_band());
2310        mi.settings_mut().set_dual_band(false);
2311        assert!(!mi.settings().dual_band());
2312    }
2313
2314    #[test]
2315    fn write_attenuator_a() {
2316        let image = make_settings_image();
2317        let mut mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2318        assert!(!mi.settings().attenuator_a());
2319        mi.settings_mut().set_attenuator_a(true);
2320        assert!(mi.settings().attenuator_a());
2321        mi.settings_mut().set_attenuator_a(false);
2322        assert!(!mi.settings().attenuator_a());
2323    }
2324
2325    #[test]
2326    fn write_power_level_a() {
2327        let image = make_settings_image();
2328        let mut mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2329        mi.settings_mut().set_power_level_a(PowerLevel::Low);
2330        assert_eq!(mi.settings().power_level_a(), PowerLevel::Low);
2331        mi.settings_mut().set_power_level_a(PowerLevel::ExtraLow);
2332        assert_eq!(mi.settings().power_level_a(), PowerLevel::ExtraLow);
2333        mi.settings_mut().set_power_level_a(PowerLevel::High);
2334        assert_eq!(mi.settings().power_level_a(), PowerLevel::High);
2335    }
2336
2337    #[test]
2338    fn write_bluetooth() {
2339        let image = make_settings_image();
2340        let mut mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2341        assert!(!mi.settings().bluetooth());
2342        mi.settings_mut().set_bluetooth(true);
2343        assert!(mi.settings().bluetooth());
2344        mi.settings_mut().set_bluetooth(false);
2345        assert!(!mi.settings().bluetooth());
2346    }
2347
2348    #[test]
2349    fn write_roundtrip_all_verified() {
2350        let image = make_settings_image();
2351        let mut mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2352
2353        // Set everything to non-default values.
2354        mi.settings_mut().set_key_beep(true);
2355        mi.settings_mut().set_vox_enabled(true);
2356        mi.settings_mut().set_vox_gain(9);
2357        mi.settings_mut().set_lock(true);
2358        mi.settings_mut().set_dual_band(true);
2359        mi.settings_mut().set_attenuator_a(true);
2360        mi.settings_mut().set_power_level_a(PowerLevel::ExtraLow);
2361        mi.settings_mut().set_bluetooth(true);
2362
2363        // Verify reads match.
2364        let s = mi.settings();
2365        assert!(s.key_beep());
2366        assert!(s.vox_enabled());
2367        assert_eq!(s.vox_gain(), 9);
2368        assert!(s.lock());
2369        assert!(s.dual_band());
2370        assert!(s.attenuator_a());
2371        assert_eq!(s.power_level_a(), PowerLevel::ExtraLow);
2372        assert!(s.bluetooth());
2373
2374        // Verify raw bytes at the verified offsets.
2375        let raw = mi.as_raw();
2376        assert_eq!(raw[KEY_BEEP_OFFSET], 1);
2377        assert_eq!(raw[VOX_ENABLED_OFFSET], 1);
2378        assert_eq!(raw[VOX_GAIN_OFFSET], 9);
2379        assert_eq!(raw[LOCK_OFFSET], 1);
2380        assert_eq!(raw[DUAL_BAND_OFFSET], 1);
2381        assert_eq!(raw[ATTENUATOR_A_OFFSET], 1);
2382        assert_eq!(raw[POWER_LEVEL_A_OFFSET], 3); // ExtraLow = 3
2383        assert_eq!(raw[BLUETOOTH_OFFSET], 1);
2384    }
2385
2386    // -----------------------------------------------------------------------
2387    // VFO data accessor tests
2388    // -----------------------------------------------------------------------
2389
2390    #[test]
2391    fn vfo_raw_accessible() {
2392        let mut image = make_settings_image();
2393        // Write a known pattern at VFO entry 0.
2394        image[VFO_DATA_OFFSET..VFO_DATA_OFFSET + 4].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
2395        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2396        let settings = mi.settings();
2397        let raw = settings.vfo_raw(0).unwrap();
2398        assert_eq!(raw.len(), VFO_ENTRY_SIZE);
2399        assert_eq!(&raw[..4], &[0xDE, 0xAD, 0xBE, 0xEF]);
2400    }
2401
2402    #[test]
2403    fn vfo_raw_out_of_range() {
2404        let image = make_settings_image();
2405        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2406        assert!(mi.settings().vfo_raw(6).is_none());
2407    }
2408
2409    #[test]
2410    fn vfo_frequency_valid() {
2411        let mut image = make_settings_image();
2412        // VFO entry 0 at offset 0x0020: 146.520 MHz.
2413        let freq: u32 = 146_520_000;
2414        let offset = VFO_DATA_OFFSET;
2415        image[offset..offset + 4].copy_from_slice(&freq.to_le_bytes());
2416        // Make the entry non-zero past the frequency.
2417        image[offset + 8] = 0x50; // step/shift byte
2418
2419        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2420        let f = mi.settings().vfo_frequency(0).unwrap();
2421        assert_eq!(f.as_hz(), 146_520_000);
2422    }
2423
2424    #[test]
2425    fn vfo_frequency_empty_entry() {
2426        let mut image = make_settings_image();
2427        // Fill VFO entry 0 with 0xFF (empty).
2428        let offset = VFO_DATA_OFFSET;
2429        image[offset..offset + VFO_ENTRY_SIZE].fill(0xFF);
2430
2431        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2432        assert!(mi.settings().vfo_frequency(0).is_none());
2433    }
2434
2435    #[test]
2436    fn vfo_frequency_zeroed_entry() {
2437        let image = make_settings_image(); // All zeros.
2438        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2439        // Frequency bytes are all zero -> returns None.
2440        assert!(mi.settings().vfo_frequency(0).is_none());
2441    }
2442
2443    #[test]
2444    fn vfo_mode_fm() {
2445        let mut image = make_settings_image();
2446        let offset = VFO_DATA_OFFSET;
2447        // Non-empty entry with some frequency data.
2448        image[offset..offset + 4].copy_from_slice(&146_520_000_u32.to_le_bytes());
2449        // Byte 0x09: mode bits [6:4] = 0 (FM).
2450        image[offset + 0x09] = 0x00;
2451
2452        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2453        let mode = mi.settings().vfo_mode(0).unwrap();
2454        assert_eq!(mode, MemoryMode::Fm);
2455    }
2456
2457    #[test]
2458    fn vfo_mode_am() {
2459        let mut image = make_settings_image();
2460        let offset = VFO_DATA_OFFSET;
2461        image[offset..offset + 4].copy_from_slice(&7_100_000_u32.to_le_bytes());
2462        // Byte 0x09: mode bits [6:4] = 2 (AM in flash encoding).
2463        image[offset + 0x09] = 0x20;
2464
2465        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2466        let mode = mi.settings().vfo_mode(0).unwrap();
2467        assert_eq!(mode, MemoryMode::Am);
2468    }
2469
2470    #[test]
2471    fn vfo_mode_lsb() {
2472        let mut image = make_settings_image();
2473        let offset = VFO_DATA_OFFSET;
2474        image[offset..offset + 4].copy_from_slice(&7_100_000_u32.to_le_bytes());
2475        // Byte 0x09: mode bits [6:4] = 3 (LSB in flash encoding).
2476        image[offset + 0x09] = 0x30;
2477
2478        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2479        let mode = mi.settings().vfo_mode(0).unwrap();
2480        assert_eq!(mode, MemoryMode::Lsb);
2481    }
2482
2483    #[test]
2484    fn vfo_tx_offset() {
2485        let mut image = make_settings_image();
2486        let offset = VFO_DATA_OFFSET;
2487        image[offset..offset + 4].copy_from_slice(&146_520_000_u32.to_le_bytes());
2488        image[offset + 4..offset + 8].copy_from_slice(&600_000_u32.to_le_bytes());
2489
2490        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2491        let tx_off = mi.settings().vfo_tx_offset(0).unwrap();
2492        assert_eq!(tx_off.as_hz(), 600_000);
2493    }
2494
2495    #[test]
2496    fn vfo_count_none_populated() {
2497        let image = make_settings_image(); // All zeros.
2498        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2499        assert_eq!(mi.settings().vfo_count(), 0);
2500    }
2501
2502    #[test]
2503    fn vfo_count_with_entries() {
2504        let mut image = make_settings_image();
2505        // Populate VFO entries 0 and 2.
2506        let offset0 = VFO_DATA_OFFSET;
2507        image[offset0..offset0 + 4].copy_from_slice(&146_520_000_u32.to_le_bytes());
2508        let offset2 = VFO_DATA_OFFSET + 2 * VFO_ENTRY_SIZE;
2509        image[offset2..offset2 + 4].copy_from_slice(&446_000_000_u32.to_le_bytes());
2510
2511        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2512        assert_eq!(mi.settings().vfo_count(), 2);
2513    }
2514
2515    #[test]
2516    fn vfo_second_entry_frequency() {
2517        let mut image = make_settings_image();
2518        let offset = VFO_DATA_OFFSET + VFO_ENTRY_SIZE; // Entry 1.
2519        image[offset..offset + 4].copy_from_slice(&222_100_000_u32.to_le_bytes());
2520
2521        let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
2522        let f = mi.settings().vfo_frequency(1).unwrap();
2523        assert_eq!(f.as_hz(), 222_100_000);
2524    }
2525}