kenwood_thd75/types/
settings.rs

1//! Radio-wide system, audio, and display settings for the TH-D75.
2//!
3//! These types cover the radio's global configuration accessible through
4//! the menu system (Configuration, Audio, Display sections). They model
5//! settings from the capability gap analysis features 123-197 that are
6//! not subsystem-specific (not APRS, D-STAR, or GPS).
7
8use crate::error::ValidationError;
9
10// ---------------------------------------------------------------------------
11// Display settings
12// ---------------------------------------------------------------------------
13
14/// Display and illumination settings.
15///
16/// Controls the TH-D75's LCD backlight, color theme, power-on message,
17/// and meter display. Derived from capability gap analysis features 159-169.
18///
19/// # Menu numbers (per Operating Tips §5.2, User Manual Chapter 12)
20///
21/// - Menu No. 900: Backlight control — `Auto` (keys/encoder turn on,
22///   timer turns off; also lights on APRS interrupt or scan pause),
23///   `Auto (DC-IN)` (same as Auto on battery, always-on on DC),
24///   `Manual` (only `[Power]` toggles), `On` (always on).
25/// - Menu No. 901: Backlight timer — 3 to 60 seconds, default 10.
26/// - Menu No. 902: LCD brightness — High / Medium / Low.
27/// - Menu No. 903: Power-on message — up to 16 characters, default
28///   "HELLO !!". Displayed for approximately 2 seconds at power-on.
29///   MCP-D75 software can also set a custom bitmap graphic.
30/// - Menu No. 904: Single Band Display — Off / GPS(Altitude) /
31///   GPS(GS) / Date / Demodulation Mode.
32/// - Menu No. 905: Meter Type — Type 1 / Type 2 / Type 3 (S/RF meter
33///   design variants).
34/// - Menu No. 906: Background Color — Black / White.
35/// - Menu No. 907: Info Backlight — Off / LCD / LCD+Key. Controls
36///   whether the backlight turns on for APRS or D-STAR interrupt
37///   display and scan pause/stop events.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct DisplaySettings {
40    /// LCD backlight control mode.
41    pub backlight_control: BacklightControl,
42    /// Backlight auto-off timer in seconds (0 = always on).
43    pub backlight_timer: u8,
44    /// LCD brightness level (1-6, 1 = dimmest, 6 = brightest).
45    pub lcd_brightness: u8,
46    /// Background color theme.
47    pub background_color: BackgroundColor,
48    /// Power-on message displayed at startup (up to 16 characters).
49    pub power_on_message: PowerOnMessage,
50    /// Single-band display mode (show only one band at a time).
51    pub single_band_display: bool,
52    /// S-meter and power meter display type.
53    pub meter_type: MeterType,
54    /// Display method for the dual-band screen.
55    pub display_method: DisplayMethod,
56    /// LED indicator control.
57    pub led_control: LedControl,
58    /// Info backlight on receive.
59    pub info_backlight: bool,
60    /// Display hold time for transient information (seconds).
61    pub display_hold_time: DisplayHoldTime,
62}
63
64impl Default for DisplaySettings {
65    fn default() -> Self {
66        Self {
67            backlight_control: BacklightControl::Auto,
68            backlight_timer: 5,
69            lcd_brightness: 4,
70            background_color: BackgroundColor::Blue,
71            power_on_message: PowerOnMessage::default(),
72            single_band_display: false,
73            meter_type: MeterType::Bar,
74            display_method: DisplayMethod::Dual,
75            led_control: LedControl::On,
76            info_backlight: true,
77            display_hold_time: DisplayHoldTime::Sec3,
78        }
79    }
80}
81
82/// LCD backlight control mode (Menu No. 900).
83///
84/// Per User Manual Chapter 12: temporary lighting can also be triggered
85/// by pressing `[Power]`, which illuminates the display and keys for the
86/// timer duration (Menu No. 901). Pressing `[Power]` while lit turns
87/// the light off immediately.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89pub enum BacklightControl {
90    /// Backlight always on while power is on.
91    On,
92    /// Backlight auto (turns on with key press or encoder rotation,
93    /// off after the timer in Menu No. 901 expires). Also lights on
94    /// APRS interrupt reception and scan pause/stop.
95    Auto,
96    /// Backlight always off (only `[Power]` can trigger temporary
97    /// lighting in Manual mode, per User Manual Chapter 12).
98    Off,
99}
100
101/// Background color theme for the LCD display (Menu No. 906).
102///
103/// Per User Manual Chapter 12: the user manual defines only Black
104/// and White options. The Operating Tips previously referenced Amber,
105/// Green, Blue, and White. The actual available values depend on
106/// firmware version.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108pub enum BackgroundColor {
109    /// Amber / warm color theme.
110    Amber,
111    /// Green color theme.
112    Green,
113    /// Blue color theme (default).
114    Blue,
115    /// White color theme.
116    White,
117}
118
119/// Power-on message text (up to 16 characters).
120#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
121pub struct PowerOnMessage(String);
122
123impl PowerOnMessage {
124    /// Maximum length of the power-on message.
125    pub const MAX_LEN: usize = 16;
126
127    /// Creates a new power-on message.
128    ///
129    /// # Errors
130    ///
131    /// Returns `None` if the text exceeds 16 characters.
132    #[must_use]
133    pub fn new(text: &str) -> Option<Self> {
134        if text.len() <= Self::MAX_LEN {
135            Some(Self(text.to_owned()))
136        } else {
137            None
138        }
139    }
140
141    /// Returns the power-on message as a string slice.
142    #[must_use]
143    pub fn as_str(&self) -> &str {
144        &self.0
145    }
146}
147
148/// S-meter and power meter display type.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150pub enum MeterType {
151    /// Bar graph meter display.
152    Bar,
153    /// Numeric (digital) meter display.
154    Numeric,
155}
156
157/// Display method for the main screen.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
159pub enum DisplayMethod {
160    /// Show both bands simultaneously.
161    Dual,
162    /// Show single band only.
163    Single,
164}
165
166/// LED indicator control.
167///
168/// Per Operating Tips §5.2: Menu No. 181 controls the RX LED and
169/// FM Radio LED independently. When enabled, the LED lights on
170/// signal reception and during FM broadcast radio playback.
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
172pub enum LedControl {
173    /// LED indicators enabled.
174    On,
175    /// LED indicators disabled.
176    Off,
177}
178
179/// Display hold time for transient information.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
181pub enum DisplayHoldTime {
182    /// 3 second hold time.
183    Sec3,
184    /// 5 second hold time.
185    Sec5,
186    /// 10 second hold time.
187    Sec10,
188    /// Continuous (hold until dismissed).
189    Continuous,
190}
191
192// ---------------------------------------------------------------------------
193// Audio settings
194// ---------------------------------------------------------------------------
195
196/// Audio and sound settings.
197///
198/// Controls the TH-D75's beep, equalizer, microphone sensitivity,
199/// and voice guidance features. Derived from capability gap analysis
200/// features 123-148.
201///
202/// # Audio equalizer (per User Manual Chapter 12)
203///
204/// The TH-D75 has independent TX and RX parametric equalizers:
205///
206/// - **TX EQ** (Menu No. 911/912): 4-band (0.4/0.8/1.6/3.2 kHz),
207///   range -9 to +3 dB per band. Separate enable for FM/NFM and DV modes.
208/// - **RX EQ** (Menu No. 911/913): 5-band (0.4/0.8/1.6/3.2/6.4 kHz),
209///   range -9 to +9 dB per band. The 6.4 kHz band has no effect in
210///   DV/DR mode since digital audio bandwidth is limited to 4 kHz.
211///
212/// # Volume balance (per User Manual Chapter 5)
213///
214/// Menu No. 910 controls audio balance between Band A and Band B.
215/// The `Operation Band Only` setting outputs sound only from the
216/// operation band when both bands are simultaneously busy.
217#[allow(clippy::struct_excessive_bools)]
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct AudioSettings {
220    /// Key beep on/off.
221    pub beep: bool,
222    /// Beep volume level (1-7).
223    pub beep_volume: u8,
224    /// TX audio equalizer preset (for FM/NFM mode).
225    pub tx_equalizer_fm: EqSetting,
226    /// TX audio equalizer preset (for DV mode).
227    pub tx_equalizer_dv: EqSetting,
228    /// RX audio equalizer preset.
229    pub rx_equalizer: EqSetting,
230    /// Microphone sensitivity level.
231    pub mic_sensitivity: MicSensitivity,
232    /// Voice guidance on/off.
233    pub voice_guidance: bool,
234    /// Voice guidance volume (1-7).
235    pub voice_guidance_volume: u8,
236    /// Voice guidance speed.
237    pub voice_guidance_speed: VoiceGuideSpeed,
238    /// Audio balance between Band A and Band B (0 = A only, 50 = equal,
239    /// 100 = B only).
240    pub balance: u8,
241    /// TX monitor on/off (hear own transmit audio).
242    pub tx_monitor: bool,
243    /// USB audio output level.
244    pub usb_audio_output_level: u8,
245}
246
247impl Default for AudioSettings {
248    fn default() -> Self {
249        Self {
250            beep: true,
251            beep_volume: 4,
252            tx_equalizer_fm: EqSetting::Off,
253            tx_equalizer_dv: EqSetting::Off,
254            rx_equalizer: EqSetting::Off,
255            mic_sensitivity: MicSensitivity::Medium,
256            voice_guidance: false,
257            voice_guidance_volume: 4,
258            voice_guidance_speed: VoiceGuideSpeed::Normal,
259            balance: 50,
260            tx_monitor: false,
261            usb_audio_output_level: 4,
262        }
263    }
264}
265
266/// Audio equalizer setting (TX or RX).
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
268pub enum EqSetting {
269    /// Equalizer disabled (flat response).
270    Off,
271    /// High-boost preset.
272    HighBoost,
273    /// Low-boost preset.
274    LowBoost,
275    /// Full-boost preset.
276    FullBoost,
277}
278
279/// Microphone sensitivity level (Menu No. 112).
280///
281/// Per User Manual Chapter 12: applies to both the internal microphone
282/// and an external microphone. Default: Medium.
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
284pub enum MicSensitivity {
285    /// Low sensitivity.
286    Low,
287    /// Medium sensitivity (default).
288    Medium,
289    /// High sensitivity.
290    High,
291}
292
293/// Voice guidance speed.
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
295pub enum VoiceGuideSpeed {
296    /// Slow voice guidance.
297    Slow,
298    /// Normal speed voice guidance.
299    Normal,
300    /// Fast voice guidance.
301    Fast,
302}
303
304// ---------------------------------------------------------------------------
305// System settings
306// ---------------------------------------------------------------------------
307
308/// System-wide radio settings.
309///
310/// Covers global configuration such as power management, key lock,
311/// display units, language, and programmable function keys.
312/// Derived from capability gap analysis features 170-197.
313///
314/// # USB charging (per Operating Tips §5.1)
315///
316/// The TH-D75 charges via USB but does not support USB Power Delivery
317/// (PD). It always draws 5V from USB; an internal DC-DC converter
318/// boosts this to 7.4V for the battery. Two charging current modes:
319/// - 1.5A: approximately 5.5 hours to full charge
320/// - 0.5A: approximately 13 hours to full charge
321///
322/// **Power must be off during charging.** Menu No. 923 can disable
323/// charging at power-on to prevent unintended charge sessions.
324///
325/// # Battery saver (per Operating Tips §5.1)
326///
327/// Menu No. 920 controls the battery saver, which cycles the receiver
328/// on and off to reduce power consumption. In DV/DR mode, the off
329/// duration is fixed at 200 ms regardless of the configured value.
330/// Battery saver is automatically disabled when APRS or KISS mode
331/// is active.
332///
333/// # Auto Power Off (per Operating Tips §5.1)
334///
335/// Menu No. 921 controls Auto Power Off. Default is 30 minutes.
336/// The radio powers off automatically after the configured period
337/// of inactivity.
338#[allow(clippy::struct_excessive_bools)]
339#[derive(Debug, Clone, PartialEq, Eq)]
340pub struct SystemSettings {
341    /// Battery saver on/off (reduce power in standby by cycling the
342    /// receiver).
343    pub battery_saver: bool,
344    /// Auto power off timer.
345    pub auto_power_off: AutoPowerOff,
346    /// Key lock enabled.
347    pub key_lock: bool,
348    /// Key lock type (which keys are affected).
349    pub key_lock_type: KeyLockType,
350    /// Volume lock (prevent accidental volume changes).
351    pub volume_lock: bool,
352    /// DTMF key lock (lock the DTMF keypad separately).
353    pub dtmf_lock: bool,
354    /// Mic key lock (lock microphone keys).
355    pub mic_lock: bool,
356    /// Display unit system.
357    pub display_units: DisplayUnits,
358    /// Language selection.
359    pub language: Language,
360    /// Time-out timer in seconds (0 = disabled, 30-600).
361    /// Automatically stops TX after the timeout.
362    ///
363    /// Menu No. 111. Per User Manual Chapter 12: available values are
364    /// 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, and 10.0
365    /// minutes. Default: 10.0 minutes. This function cannot be turned
366    /// off entirely -- it protects the transceiver from thermal damage.
367    /// A warning beep sounds just before TX is cut off. After timeout,
368    /// the transceiver beeps even if beep is disabled.
369    pub time_out_timer: u16,
370    /// Programmable function key PF1 (front panel) assignment.
371    pub pf1_key: PfKeyFunction,
372    /// Programmable function key PF2 (front panel) assignment.
373    pub pf2_key: PfKeyFunction,
374    /// Programmable function key PF1 (mic) assignment.
375    pub pf1_mic: PfKeyFunction,
376    /// Programmable function key PF2 (mic) assignment.
377    pub pf2_mic: PfKeyFunction,
378    /// Programmable function key PF3 (mic) assignment.
379    pub pf3_mic: PfKeyFunction,
380    /// WX alert on/off (automatic weather channel scan; TH-D75A only).
381    pub wx_alert: bool,
382    /// Secret access code enabled (require code to power on).
383    pub secret_access_code: bool,
384    /// Date format.
385    pub date_format: DateFormat,
386    /// Time zone offset from UTC (e.g. -5 for EST).
387    pub time_zone_offset: i8,
388}
389
390impl Default for SystemSettings {
391    fn default() -> Self {
392        Self {
393            battery_saver: true,
394            auto_power_off: AutoPowerOff::Off,
395            key_lock: false,
396            key_lock_type: KeyLockType::KeyOnly,
397            volume_lock: false,
398            dtmf_lock: false,
399            mic_lock: false,
400            display_units: DisplayUnits::default(),
401            language: Language::English,
402            time_out_timer: 0,
403            pf1_key: PfKeyFunction::Monitor,
404            pf2_key: PfKeyFunction::VoiceAlert,
405            pf1_mic: PfKeyFunction::Monitor,
406            pf2_mic: PfKeyFunction::VoiceAlert,
407            pf3_mic: PfKeyFunction::VoiceAlert,
408            wx_alert: false,
409            secret_access_code: false,
410            date_format: DateFormat::YearMonthDay,
411            time_zone_offset: 0,
412        }
413    }
414}
415
416/// Auto power off timer duration (Menu No. 921).
417///
418/// Per User Manual Chapter 12: after the time limit with no operations,
419/// APO turns the power off. One minute before power-off, "APO" blinks
420/// on the display and a warning tone sounds (even if beep is disabled).
421/// APO does not operate during scanning.
422///
423/// The User Manual menu table lists options: Off / 15 / 30 / 60 minutes
424/// (default: 30). The firmware MCP binary encoding may support additional
425/// values (90, 120 minutes) not shown in the manual.
426#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
427pub enum AutoPowerOff {
428    /// Auto power off disabled.
429    Off,
430    /// Power off after 30 minutes of inactivity.
431    Min30,
432    /// Power off after 60 minutes of inactivity.
433    Min60,
434    /// Power off after 90 minutes of inactivity.
435    Min90,
436    /// Power off after 120 minutes of inactivity.
437    Min120,
438}
439
440/// Key lock type -- which controls are affected by key lock (Menu No. 960).
441///
442/// Per User Manual Chapter 12: key lock is toggled by pressing and
443/// holding `[F]`. The `[MONI]`, `[PTT]`, `[Power]`, and `[VOL]`
444/// controls can never be locked.
445///
446/// The User Manual lists options as `Key Lock` and/or `Frequency Lock`
447/// (checkboxes), with different combined behaviors:
448/// - Key Lock only: locks all front panel keys.
449/// - Frequency Lock only: locks frequency/channel controls.
450/// - Both: locks all keys and the encoder control.
451#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
452pub enum KeyLockType {
453    /// Lock front panel keys only.
454    KeyOnly,
455    /// Lock front panel keys and PTT.
456    KeyAndPtt,
457    /// Lock front panel keys, PTT, and dial.
458    KeyPttAndDial,
459}
460
461/// Display unit preferences.
462///
463/// Controls measurement units displayed on the radio screen.
464#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
465pub struct DisplayUnits {
466    /// Speed and distance units.
467    pub speed_distance: SpeedDistanceUnit,
468    /// Altitude and rainfall units.
469    pub altitude_rain: AltitudeRainUnit,
470    /// Temperature units.
471    pub temperature: TemperatureUnit,
472}
473
474impl Default for DisplayUnits {
475    fn default() -> Self {
476        Self {
477            speed_distance: SpeedDistanceUnit::MilesPerHour,
478            altitude_rain: AltitudeRainUnit::FeetInch,
479            temperature: TemperatureUnit::Fahrenheit,
480        }
481    }
482}
483
484/// Speed and distance measurement units.
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
486pub enum SpeedDistanceUnit {
487    /// Miles per hour / miles.
488    MilesPerHour,
489    /// Kilometers per hour / kilometers.
490    KilometersPerHour,
491    /// Knots / nautical miles.
492    Knots,
493}
494
495/// Altitude and rainfall measurement units.
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
497pub enum AltitudeRainUnit {
498    /// Feet / inches.
499    FeetInch,
500    /// Meters / millimeters.
501    MetersMm,
502}
503
504/// Temperature measurement units.
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
506pub enum TemperatureUnit {
507    /// Fahrenheit.
508    Fahrenheit,
509    /// Celsius.
510    Celsius,
511}
512
513/// Language selection (Menu No. 990).
514#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
515pub enum Language {
516    /// English.
517    English,
518    /// Japanese.
519    Japanese,
520}
521
522/// Date display format.
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
524pub enum DateFormat {
525    /// Year/Month/Day (e.g. 2026/03/28).
526    YearMonthDay,
527    /// Month/Day/Year (e.g. 03/28/2026).
528    MonthDayYear,
529    /// Day/Month/Year (e.g. 28/03/2026).
530    DayMonthYear,
531}
532
533/// Programmable function key assignment.
534///
535/// The TH-D75 has 2 front-panel PF keys (Menu No. 940/941) and 3
536/// microphone PF keys (Menu No. 942/943/944), each assignable to one
537/// of these functions.
538///
539/// Per User Manual Chapter 12: the microphone PF keys support a larger
540/// set of functions than the front-panel keys, including MODE, MENU,
541/// A/B, VFO, MR, CALL, MSG, LIST, BCON, REV, TONE, MHz, MARK, DUAL,
542/// APRS, OBJ, ATT, FINE, POS, BAND, MONI, UP, DOWN, and Screen Capture.
543/// Front-panel PF keys additionally support M.IN (memory registration).
544#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
545pub enum PfKeyFunction {
546    /// Monitor (open squelch).
547    Monitor,
548    /// Voice alert toggle.
549    VoiceAlert,
550    /// Weather channel.
551    Wx,
552    /// Scan start/stop.
553    Scan,
554    /// Frequency direct entry.
555    DirectEntry,
556    /// VFO/Memory mode toggle.
557    VfoMr,
558    /// Screen capture (save to SD card).
559    ScreenCapture,
560    /// Backlight toggle.
561    Backlight,
562    /// Voice guidance toggle.
563    VoiceGuidance,
564    /// Lock toggle.
565    Lock,
566    /// 1750 Hz tone burst.
567    Tone1750,
568    /// APRS beacon transmit.
569    AprsBeacon,
570    /// Recording start/stop.
571    Recording,
572}
573
574// ---------------------------------------------------------------------------
575// TryFrom<u8> implementations for MCP binary parsing
576// ---------------------------------------------------------------------------
577
578impl TryFrom<u8> for BacklightControl {
579    type Error = ValidationError;
580
581    fn try_from(value: u8) -> Result<Self, Self::Error> {
582        match value {
583            0 => Ok(Self::On),
584            1 => Ok(Self::Auto),
585            2 => Ok(Self::Off),
586            _ => Err(ValidationError::SettingOutOfRange {
587                name: "backlight control",
588                value,
589                detail: "must be 0-2",
590            }),
591        }
592    }
593}
594
595impl TryFrom<u8> for BackgroundColor {
596    type Error = ValidationError;
597
598    fn try_from(value: u8) -> Result<Self, Self::Error> {
599        match value {
600            0 => Ok(Self::Amber),
601            1 => Ok(Self::Green),
602            2 => Ok(Self::Blue),
603            3 => Ok(Self::White),
604            _ => Err(ValidationError::SettingOutOfRange {
605                name: "background color",
606                value,
607                detail: "must be 0-3",
608            }),
609        }
610    }
611}
612
613impl TryFrom<u8> for MeterType {
614    type Error = ValidationError;
615
616    fn try_from(value: u8) -> Result<Self, Self::Error> {
617        match value {
618            0 => Ok(Self::Bar),
619            1 => Ok(Self::Numeric),
620            _ => Err(ValidationError::SettingOutOfRange {
621                name: "meter type",
622                value,
623                detail: "must be 0-1",
624            }),
625        }
626    }
627}
628
629impl TryFrom<u8> for DisplayMethod {
630    type Error = ValidationError;
631
632    fn try_from(value: u8) -> Result<Self, Self::Error> {
633        match value {
634            0 => Ok(Self::Dual),
635            1 => Ok(Self::Single),
636            _ => Err(ValidationError::SettingOutOfRange {
637                name: "display method",
638                value,
639                detail: "must be 0-1",
640            }),
641        }
642    }
643}
644
645impl TryFrom<u8> for LedControl {
646    type Error = ValidationError;
647
648    fn try_from(value: u8) -> Result<Self, Self::Error> {
649        match value {
650            0 => Ok(Self::On),
651            1 => Ok(Self::Off),
652            _ => Err(ValidationError::SettingOutOfRange {
653                name: "LED control",
654                value,
655                detail: "must be 0-1",
656            }),
657        }
658    }
659}
660
661impl TryFrom<u8> for DisplayHoldTime {
662    type Error = ValidationError;
663
664    fn try_from(value: u8) -> Result<Self, Self::Error> {
665        match value {
666            0 => Ok(Self::Sec3),
667            1 => Ok(Self::Sec5),
668            2 => Ok(Self::Sec10),
669            3 => Ok(Self::Continuous),
670            _ => Err(ValidationError::SettingOutOfRange {
671                name: "display hold time",
672                value,
673                detail: "must be 0-3",
674            }),
675        }
676    }
677}
678
679impl TryFrom<u8> for EqSetting {
680    type Error = ValidationError;
681
682    fn try_from(value: u8) -> Result<Self, Self::Error> {
683        match value {
684            0 => Ok(Self::Off),
685            1 => Ok(Self::HighBoost),
686            2 => Ok(Self::LowBoost),
687            3 => Ok(Self::FullBoost),
688            _ => Err(ValidationError::SettingOutOfRange {
689                name: "EQ setting",
690                value,
691                detail: "must be 0-3",
692            }),
693        }
694    }
695}
696
697impl TryFrom<u8> for MicSensitivity {
698    type Error = ValidationError;
699
700    fn try_from(value: u8) -> Result<Self, Self::Error> {
701        match value {
702            0 => Ok(Self::Low),
703            1 => Ok(Self::Medium),
704            2 => Ok(Self::High),
705            _ => Err(ValidationError::SettingOutOfRange {
706                name: "mic sensitivity",
707                value,
708                detail: "must be 0-2",
709            }),
710        }
711    }
712}
713
714impl TryFrom<u8> for VoiceGuideSpeed {
715    type Error = ValidationError;
716
717    fn try_from(value: u8) -> Result<Self, Self::Error> {
718        match value {
719            0 => Ok(Self::Slow),
720            1 => Ok(Self::Normal),
721            2 => Ok(Self::Fast),
722            _ => Err(ValidationError::SettingOutOfRange {
723                name: "voice guide speed",
724                value,
725                detail: "must be 0-2",
726            }),
727        }
728    }
729}
730
731impl TryFrom<u8> for AutoPowerOff {
732    type Error = ValidationError;
733
734    fn try_from(value: u8) -> Result<Self, Self::Error> {
735        match value {
736            0 => Ok(Self::Off),
737            1 => Ok(Self::Min30),
738            2 => Ok(Self::Min60),
739            3 => Ok(Self::Min90),
740            4 => Ok(Self::Min120),
741            _ => Err(ValidationError::SettingOutOfRange {
742                name: "auto power off",
743                value,
744                detail: "must be 0-4",
745            }),
746        }
747    }
748}
749
750impl KeyLockType {
751    /// Number of valid key lock type values (0-2).
752    pub const COUNT: u8 = 3;
753}
754
755impl TryFrom<u8> for KeyLockType {
756    type Error = ValidationError;
757
758    fn try_from(value: u8) -> Result<Self, Self::Error> {
759        match value {
760            0 => Ok(Self::KeyOnly),
761            1 => Ok(Self::KeyAndPtt),
762            2 => Ok(Self::KeyPttAndDial),
763            _ => Err(ValidationError::SettingOutOfRange {
764                name: "key lock type",
765                value,
766                detail: "must be 0-2",
767            }),
768        }
769    }
770}
771
772impl From<KeyLockType> for u8 {
773    fn from(klt: KeyLockType) -> Self {
774        klt as Self
775    }
776}
777
778impl TryFrom<u8> for Language {
779    type Error = ValidationError;
780
781    fn try_from(value: u8) -> Result<Self, Self::Error> {
782        match value {
783            0 => Ok(Self::English),
784            1 => Ok(Self::Japanese),
785            _ => Err(ValidationError::SettingOutOfRange {
786                name: "language",
787                value,
788                detail: "must be 0-1",
789            }),
790        }
791    }
792}
793
794impl TryFrom<u8> for DateFormat {
795    type Error = ValidationError;
796
797    fn try_from(value: u8) -> Result<Self, Self::Error> {
798        match value {
799            0 => Ok(Self::YearMonthDay),
800            1 => Ok(Self::MonthDayYear),
801            2 => Ok(Self::DayMonthYear),
802            _ => Err(ValidationError::SettingOutOfRange {
803                name: "date format",
804                value,
805                detail: "must be 0-2",
806            }),
807        }
808    }
809}
810
811// ---------------------------------------------------------------------------
812// Tests
813// ---------------------------------------------------------------------------
814
815#[cfg(test)]
816mod tests {
817    use super::*;
818
819    #[test]
820    fn display_settings_default() {
821        let ds = DisplaySettings::default();
822        assert_eq!(ds.backlight_control, BacklightControl::Auto);
823        assert_eq!(ds.background_color, BackgroundColor::Blue);
824    }
825
826    #[test]
827    fn audio_settings_default() {
828        let a = AudioSettings::default();
829        assert!(a.beep);
830        assert_eq!(a.beep_volume, 4);
831        assert_eq!(a.mic_sensitivity, MicSensitivity::Medium);
832    }
833
834    #[test]
835    fn system_settings_default() {
836        let s = SystemSettings::default();
837        assert!(s.battery_saver);
838        assert_eq!(s.auto_power_off, AutoPowerOff::Off);
839        assert_eq!(s.language, Language::English);
840        assert_eq!(s.time_out_timer, 0);
841    }
842
843    #[test]
844    fn power_on_message_valid() {
845        let msg = PowerOnMessage::new("TH-D75 Ready").unwrap();
846        assert_eq!(msg.as_str(), "TH-D75 Ready");
847    }
848
849    #[test]
850    fn power_on_message_max_length() {
851        let msg = PowerOnMessage::new("1234567890123456").unwrap();
852        assert_eq!(msg.as_str().len(), 16);
853    }
854
855    #[test]
856    fn power_on_message_too_long() {
857        assert!(PowerOnMessage::new("12345678901234567").is_none());
858    }
859
860    #[test]
861    fn display_units_default() {
862        let u = DisplayUnits::default();
863        assert_eq!(u.speed_distance, SpeedDistanceUnit::MilesPerHour);
864        assert_eq!(u.temperature, TemperatureUnit::Fahrenheit);
865    }
866}