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}