kenwood_thd75/types/
radio_params.rs

1//! Validated parameter types for radio CAT command methods.
2//!
3//! These newtypes and enums enforce valid ranges at construction time
4//! for parameters that the radio methods previously accepted as raw `u8`.
5
6use std::fmt;
7
8use crate::error::ValidationError;
9
10// ---------------------------------------------------------------------------
11// SquelchLevel (0-6)
12// ---------------------------------------------------------------------------
13
14/// Squelch threshold level (0-6).
15///
16/// 0 = open (no squelch), 6 = maximum squelch. Used by the `SQ` CAT command.
17/// Squelch can be set independently for Band A and Band B.
18///
19/// Per User Manual Chapter 5: the squelch mutes the speaker when no signals
20/// are present. The higher the level, the stronger the signal must be to
21/// open squelch. Adjust with `[F]`, `[MONI]` on the radio.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct SquelchLevel(u8);
24
25impl SquelchLevel {
26    /// Open squelch (level 0).
27    pub const OPEN: Self = Self(0);
28    /// Maximum squelch (level 6).
29    pub const MAX: Self = Self(6);
30    /// Number of valid squelch levels (0-6).
31    pub const COUNT: u8 = 7;
32
33    /// Creates a new `SquelchLevel` from a raw value.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`ValidationError::SettingOutOfRange`] if `value > 6`.
38    pub const fn new(value: u8) -> Result<Self, ValidationError> {
39        if value > 6 {
40            Err(ValidationError::SettingOutOfRange {
41                name: "squelch level",
42                value,
43                detail: "must be 0-6",
44            })
45        } else {
46            Ok(Self(value))
47        }
48    }
49
50    /// Returns the raw `u8` value.
51    #[must_use]
52    pub const fn as_u8(self) -> u8 {
53        self.0
54    }
55}
56
57impl TryFrom<u8> for SquelchLevel {
58    type Error = ValidationError;
59
60    fn try_from(value: u8) -> Result<Self, Self::Error> {
61        Self::new(value)
62    }
63}
64
65impl From<SquelchLevel> for u8 {
66    fn from(level: SquelchLevel) -> Self {
67        level.0
68    }
69}
70
71impl fmt::Display for SquelchLevel {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        write!(f, "SQ{}", self.0)
74    }
75}
76
77// ---------------------------------------------------------------------------
78// AfGainLevel (0-99)
79// ---------------------------------------------------------------------------
80
81/// Audio frequency gain level.
82///
83/// Controls the volume output level. Used by the `AG` CAT command.
84/// The wire format is a bare 3-digit zero-padded decimal (`AG 015\r`).
85///
86/// The write range is 0-99 per KI4LAX spec, but the radio's read response
87/// can return values up to 255 when the volume knob is turned beyond the
88/// write-command range. The type accepts the full 0-255 range to avoid
89/// parse errors on hardware-observed values (e.g., AG 113).
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub struct AfGainLevel(u8);
92
93impl AfGainLevel {
94    /// Creates a new `AfGainLevel` from a raw value.
95    ///
96    /// Accepts the full `u8` range (0-255) since the radio can return
97    /// values above 99 on read, even though writes are limited to 0-99.
98    #[must_use]
99    pub const fn new(value: u8) -> Self {
100        Self(value)
101    }
102
103    /// Returns the raw `u8` value.
104    #[must_use]
105    pub const fn as_u8(self) -> u8 {
106        self.0
107    }
108}
109
110impl From<u8> for AfGainLevel {
111    fn from(value: u8) -> Self {
112        Self::new(value)
113    }
114}
115
116impl From<AfGainLevel> for u8 {
117    fn from(level: AfGainLevel) -> Self {
118        level.0
119    }
120}
121
122impl fmt::Display for AfGainLevel {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(f, "{}", self.0)
125    }
126}
127
128// ---------------------------------------------------------------------------
129// SMeterReading (0-5)
130// ---------------------------------------------------------------------------
131
132/// S-meter reading (0-5).
133///
134/// The radio returns 0-5 via the `SM` command, mapping to signal strengths
135/// S0, S1, S3, S5, S7, S9 respectively.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
137pub struct SMeterReading(u8);
138
139impl SMeterReading {
140    /// Number of valid S-meter reading values (0-5).
141    pub const COUNT: u8 = 6;
142
143    /// Creates a new `SMeterReading` from a raw value.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`ValidationError::SettingOutOfRange`] if `value > 5`.
148    pub const fn new(value: u8) -> Result<Self, ValidationError> {
149        if value > 5 {
150            Err(ValidationError::SettingOutOfRange {
151                name: "S-meter reading",
152                value,
153                detail: "must be 0-5",
154            })
155        } else {
156            Ok(Self(value))
157        }
158    }
159
160    /// Returns the raw `u8` value.
161    #[must_use]
162    pub const fn as_u8(self) -> u8 {
163        self.0
164    }
165
166    /// Returns the approximate S-unit string.
167    #[must_use]
168    pub const fn s_unit(&self) -> &'static str {
169        match self.0 {
170            0 => "S0",
171            1 => "S1",
172            2 => "S3",
173            3 => "S5",
174            4 => "S7",
175            5 => "S9",
176            _ => "S?",
177        }
178    }
179}
180
181impl TryFrom<u8> for SMeterReading {
182    type Error = ValidationError;
183
184    fn try_from(value: u8) -> Result<Self, Self::Error> {
185        Self::new(value)
186    }
187}
188
189impl From<SMeterReading> for u8 {
190    fn from(reading: SMeterReading) -> Self {
191        reading.0
192    }
193}
194
195impl fmt::Display for SMeterReading {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        f.write_str(self.s_unit())
198    }
199}
200
201// ---------------------------------------------------------------------------
202// VfoMemoryMode
203// ---------------------------------------------------------------------------
204
205/// VFO/Memory/Call/Weather operating mode.
206///
207/// Controls which channel selection mode the band is in.
208/// Used by the `VM` CAT command.
209///
210/// Per User Manual Chapter 5:
211///
212/// - **VFO mode** (`[VFO]`): manually tune to any frequency using the
213///   encoder dial, up/down keys, or direct frequency entry via keypad.
214///   The default step size varies by band and model (e.g., TH-D75A:
215///   5 kHz on 144 MHz, 20 kHz on 220 MHz, 25 kHz on 430 MHz).
216/// - **Memory mode** (`[MR]`): recall one of 1000 stored memory channels
217///   (0-999) plus 100 program scan memories and 1 priority channel.
218/// - **Call mode** (`[CALL]`): quick-access channel for emergency/group
219///   use. Default call channels: TH-D75A 146.520 FM (VHF), 446.000 FM
220///   (UHF); TH-D75E 145.500 FM (VHF), 433.500 FM (UHF).
221/// - **Weather mode**: NOAA weather channels (TH-D75A only, 10 channels
222///   A1-A10 at 161.650-163.275 MHz).
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
224pub enum VfoMemoryMode {
225    /// VFO mode — frequency entered directly (index 0).
226    Vfo = 0,
227    /// Memory channel mode — recalls stored channels (index 1).
228    Memory = 1,
229    /// Call channel mode — quick-access channel (index 2).
230    Call = 2,
231    /// Weather channel mode — NOAA weather frequencies (index 3).
232    Weather = 3,
233}
234
235impl VfoMemoryMode {
236    /// Number of valid VFO/memory mode values (0-3).
237    pub const COUNT: u8 = 4;
238}
239
240impl fmt::Display for VfoMemoryMode {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        match self {
243            Self::Vfo => f.write_str("VFO"),
244            Self::Memory => f.write_str("Memory"),
245            Self::Call => f.write_str("Call"),
246            Self::Weather => f.write_str("Weather"),
247        }
248    }
249}
250
251impl TryFrom<u8> for VfoMemoryMode {
252    type Error = ValidationError;
253
254    fn try_from(value: u8) -> Result<Self, Self::Error> {
255        match value {
256            0 => Ok(Self::Vfo),
257            1 => Ok(Self::Memory),
258            2 => Ok(Self::Call),
259            3 => Ok(Self::Weather),
260            _ => Err(ValidationError::SettingOutOfRange {
261                name: "VFO/memory mode",
262                value,
263                detail: "must be 0-3",
264            }),
265        }
266    }
267}
268
269impl From<VfoMemoryMode> for u8 {
270    fn from(mode: VfoMemoryMode) -> Self {
271        mode as Self
272    }
273}
274
275// ---------------------------------------------------------------------------
276// FilterMode
277// ---------------------------------------------------------------------------
278
279/// Receiver filter mode selection.
280///
281/// Selects which demodulator's filter width to read or set.
282/// Used by the `SF` CAT command.
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
284pub enum FilterMode {
285    /// SSB (LSB/USB) filter (index 0).
286    Ssb = 0,
287    /// CW filter (index 1).
288    Cw = 1,
289    /// AM filter (index 2).
290    Am = 2,
291}
292
293impl FilterMode {
294    /// Number of valid filter mode values (0-2).
295    pub const COUNT: u8 = 3;
296}
297
298impl fmt::Display for FilterMode {
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        match self {
301            Self::Ssb => f.write_str("SSB"),
302            Self::Cw => f.write_str("CW"),
303            Self::Am => f.write_str("AM"),
304        }
305    }
306}
307
308impl TryFrom<u8> for FilterMode {
309    type Error = ValidationError;
310
311    fn try_from(value: u8) -> Result<Self, Self::Error> {
312        match value {
313            0 => Ok(Self::Ssb),
314            1 => Ok(Self::Cw),
315            2 => Ok(Self::Am),
316            _ => Err(ValidationError::SettingOutOfRange {
317                name: "filter mode",
318                value,
319                detail: "must be 0-2",
320            }),
321        }
322    }
323}
324
325impl From<FilterMode> for u8 {
326    fn from(mode: FilterMode) -> Self {
327        mode as Self
328    }
329}
330
331// ---------------------------------------------------------------------------
332// BatteryLevel (0-4)
333// ---------------------------------------------------------------------------
334
335/// Battery charge level (0-4).
336///
337/// Reported by the `BL` CAT command. Read-only on the TH-D75.
338/// Menu No. 922 displays the battery level on the radio.
339///
340/// - 0 = Empty (Red)
341/// - 1 = 1/3 (Yellow)
342/// - 2 = 2/3 (Green)
343/// - 3 = Full (Green)
344/// - 4 = Charging (USB power connected)
345///
346/// Per User Manual Chapter 28: the supplied KNB-75LA is 1820 mAh,
347/// 7.4 V Li-ion. Battery life at TX:RX:standby = 6:6:48 ratio with
348/// GPS off and battery saver on: H=6 hrs, M=8 hrs, L=12 hrs, EL=15 hrs.
349/// GPS on reduces battery life by approximately 10%.
350/// The optional KBP-9 case uses 6x AAA alkaline batteries (Low power
351/// only, approximately 3.5 hours).
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
353pub enum BatteryLevel {
354    /// Empty — red battery indicator (index 0).
355    Empty = 0,
356    /// One-third — yellow battery indicator (index 1).
357    OneThird = 1,
358    /// Two-thirds — green battery indicator (index 2).
359    TwoThirds = 2,
360    /// Full — green battery indicator (index 3).
361    Full = 3,
362    /// Charging — USB power connected (index 4).
363    Charging = 4,
364}
365
366impl BatteryLevel {
367    /// Number of valid battery level values (0-4).
368    pub const COUNT: u8 = 5;
369}
370
371impl fmt::Display for BatteryLevel {
372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373        match self {
374            Self::Empty => f.write_str("Empty"),
375            Self::OneThird => f.write_str("1/3"),
376            Self::TwoThirds => f.write_str("2/3"),
377            Self::Full => f.write_str("Full"),
378            Self::Charging => f.write_str("Charging"),
379        }
380    }
381}
382
383impl TryFrom<u8> for BatteryLevel {
384    type Error = ValidationError;
385
386    fn try_from(value: u8) -> Result<Self, Self::Error> {
387        match value {
388            0 => Ok(Self::Empty),
389            1 => Ok(Self::OneThird),
390            2 => Ok(Self::TwoThirds),
391            3 => Ok(Self::Full),
392            4 => Ok(Self::Charging),
393            _ => Err(ValidationError::SettingOutOfRange {
394                name: "battery level",
395                value,
396                detail: "must be 0-4",
397            }),
398        }
399    }
400}
401
402impl From<BatteryLevel> for u8 {
403    fn from(level: BatteryLevel) -> Self {
404        level as Self
405    }
406}
407
408// ---------------------------------------------------------------------------
409// VoxGain (0-9)
410// ---------------------------------------------------------------------------
411
412/// VOX gain level (0-9).
413///
414/// Controls the microphone sensitivity threshold for VOX activation.
415/// Used by the `VG` CAT command. VOX must be enabled (`VX 1`) first.
416/// Menu No. 151. Default: 4.
417///
418/// Per User Manual Chapter 12: gain 9 transmits even on a quiet voice;
419/// gain 0 effectively disables VOX triggering. A headset must be used
420/// because the internal speaker and microphone are too close together
421/// for VOX to function reliably.
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
423pub struct VoxGain(u8);
424
425impl VoxGain {
426    /// Maximum valid VOX gain value (inclusive).
427    pub const MAX: u8 = 9;
428
429    /// Creates a new `VoxGain` from a raw value.
430    ///
431    /// # Errors
432    ///
433    /// Returns [`ValidationError::SettingOutOfRange`] if `value > 9`.
434    pub const fn new(value: u8) -> Result<Self, ValidationError> {
435        if value > 9 {
436            Err(ValidationError::SettingOutOfRange {
437                name: "VOX gain",
438                value,
439                detail: "must be 0-9",
440            })
441        } else {
442            Ok(Self(value))
443        }
444    }
445
446    /// Returns the raw `u8` value.
447    #[must_use]
448    pub const fn as_u8(self) -> u8 {
449        self.0
450    }
451}
452
453impl TryFrom<u8> for VoxGain {
454    type Error = ValidationError;
455
456    fn try_from(value: u8) -> Result<Self, Self::Error> {
457        Self::new(value)
458    }
459}
460
461impl From<VoxGain> for u8 {
462    fn from(gain: VoxGain) -> Self {
463        gain.0
464    }
465}
466
467impl fmt::Display for VoxGain {
468    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469        write!(f, "{}", self.0)
470    }
471}
472
473// ---------------------------------------------------------------------------
474// VoxDelay (0-30)
475// ---------------------------------------------------------------------------
476
477/// VOX delay in 100ms units (0-30, i.e. 0ms to 3000ms).
478///
479/// Controls how long the transmitter stays keyed after voice stops.
480/// Used by the `VD` CAT command. VOX must be enabled (`VX 1`) first.
481/// Menu No. 152. Default: 500 ms.
482///
483/// Per User Manual Chapter 12: available values are 250, 500, 750,
484/// 1000, 1500, 2000, and 3000 ms. If you press `[PTT]` while VOX is
485/// active, the delay time is not applied. If DCS is active, the radio
486/// transmits a Turn-Off Code after the delay expires.
487#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
488pub struct VoxDelay(u8);
489
490impl VoxDelay {
491    /// Maximum valid VOX delay value (inclusive).
492    pub const MAX: u8 = 30;
493
494    /// Creates a new `VoxDelay` from a raw value.
495    ///
496    /// # Errors
497    ///
498    /// Returns [`ValidationError::SettingOutOfRange`] if `value > 30`.
499    pub const fn new(value: u8) -> Result<Self, ValidationError> {
500        if value > 30 {
501            Err(ValidationError::SettingOutOfRange {
502                name: "VOX delay",
503                value,
504                detail: "must be 0-30",
505            })
506        } else {
507            Ok(Self(value))
508        }
509    }
510
511    /// Returns the raw `u8` value.
512    #[must_use]
513    pub const fn as_u8(self) -> u8 {
514        self.0
515    }
516
517    /// Returns the delay in milliseconds.
518    #[must_use]
519    pub const fn as_millis(self) -> u16 {
520        self.0 as u16 * 100
521    }
522}
523
524impl TryFrom<u8> for VoxDelay {
525    type Error = ValidationError;
526
527    fn try_from(value: u8) -> Result<Self, Self::Error> {
528        Self::new(value)
529    }
530}
531
532impl From<VoxDelay> for u8 {
533    fn from(delay: VoxDelay) -> Self {
534        delay.0
535    }
536}
537
538impl fmt::Display for VoxDelay {
539    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540        write!(f, "{}ms", self.as_millis())
541    }
542}
543
544// ---------------------------------------------------------------------------
545// TncBaud
546// ---------------------------------------------------------------------------
547
548/// TNC data baud rate.
549///
550/// Controls the APRS/KISS data speed. Used by the `DS` CAT command.
551#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
552pub enum TncBaud {
553    /// 1200 bps AFSK (index 0).
554    Bps1200 = 0,
555    /// 9600 bps GMSK (index 1).
556    Bps9600 = 1,
557}
558
559impl TncBaud {
560    /// Number of valid TNC baud rate values (0-1).
561    pub const COUNT: u8 = 2;
562}
563
564impl fmt::Display for TncBaud {
565    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566        match self {
567            Self::Bps1200 => f.write_str("1200 bps"),
568            Self::Bps9600 => f.write_str("9600 bps"),
569        }
570    }
571}
572
573impl TryFrom<u8> for TncBaud {
574    type Error = ValidationError;
575
576    fn try_from(value: u8) -> Result<Self, Self::Error> {
577        match value {
578            0 => Ok(Self::Bps1200),
579            1 => Ok(Self::Bps9600),
580            _ => Err(ValidationError::SettingOutOfRange {
581                name: "TNC baud rate",
582                value,
583                detail: "must be 0-1",
584            }),
585        }
586    }
587}
588
589impl From<TncBaud> for u8 {
590    fn from(baud: TncBaud) -> Self {
591        baud as Self
592    }
593}
594
595// ---------------------------------------------------------------------------
596// BeaconMode
597// ---------------------------------------------------------------------------
598
599/// APRS beacon transmission mode.
600///
601/// Controls how the radio sends APRS position beacons.
602/// Used by the `BN` CAT command.
603#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
604pub enum BeaconMode {
605    /// Beaconing off (index 0).
606    Off = 0,
607    /// Manual beacon — press button to transmit (index 1).
608    Manual = 1,
609    /// PTT beacon — transmit position on each PTT keyup (index 2).
610    Ptt = 2,
611    /// Auto beacon — transmit at configured interval (index 3).
612    Auto = 3,
613    /// `SmartBeaconing` — adaptive interval based on speed/heading (index 4).
614    SmartBeaconing = 4,
615}
616
617impl BeaconMode {
618    /// Number of valid beacon mode values (0-4).
619    pub const COUNT: u8 = 5;
620}
621
622impl fmt::Display for BeaconMode {
623    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
624        match self {
625            Self::Off => f.write_str("Off"),
626            Self::Manual => f.write_str("Manual"),
627            Self::Ptt => f.write_str("PTT"),
628            Self::Auto => f.write_str("Auto"),
629            Self::SmartBeaconing => f.write_str("SmartBeaconing"),
630        }
631    }
632}
633
634impl TryFrom<u8> for BeaconMode {
635    type Error = ValidationError;
636
637    fn try_from(value: u8) -> Result<Self, Self::Error> {
638        match value {
639            0 => Ok(Self::Off),
640            1 => Ok(Self::Manual),
641            2 => Ok(Self::Ptt),
642            3 => Ok(Self::Auto),
643            4 => Ok(Self::SmartBeaconing),
644            _ => Err(ValidationError::SettingOutOfRange {
645                name: "beacon mode",
646                value,
647                detail: "must be 0-4",
648            }),
649        }
650    }
651}
652
653impl From<BeaconMode> for u8 {
654    fn from(mode: BeaconMode) -> Self {
655        mode as Self
656    }
657}
658
659// ---------------------------------------------------------------------------
660// DstarSlot (1-6)
661// ---------------------------------------------------------------------------
662
663/// D-STAR memory slot index (1-6).
664///
665/// Identifies one of the 6 D-STAR callsign memory slots.
666/// Used by the `SD` and `CS` CAT commands.
667#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
668pub struct DstarSlot(u8);
669
670impl DstarSlot {
671    /// Minimum valid D-STAR slot index.
672    pub const MIN: u8 = 1;
673    /// Maximum valid D-STAR slot index.
674    pub const MAX: u8 = 6;
675
676    /// Creates a new `DstarSlot` from a raw value.
677    ///
678    /// # Errors
679    ///
680    /// Returns [`ValidationError::SettingOutOfRange`] if `value` is not 1-6.
681    pub const fn new(value: u8) -> Result<Self, ValidationError> {
682        if value == 0 || value > 6 {
683            Err(ValidationError::SettingOutOfRange {
684                name: "D-STAR slot",
685                value,
686                detail: "must be 1-6",
687            })
688        } else {
689            Ok(Self(value))
690        }
691    }
692
693    /// Returns the raw `u8` value.
694    #[must_use]
695    pub const fn as_u8(self) -> u8 {
696        self.0
697    }
698}
699
700impl TryFrom<u8> for DstarSlot {
701    type Error = ValidationError;
702
703    fn try_from(value: u8) -> Result<Self, Self::Error> {
704        Self::new(value)
705    }
706}
707
708impl From<DstarSlot> for u8 {
709    fn from(slot: DstarSlot) -> Self {
710        slot.0
711    }
712}
713
714impl fmt::Display for DstarSlot {
715    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
716        write!(f, "Slot {}", self.0)
717    }
718}
719
720// ---------------------------------------------------------------------------
721// CallsignSlot (0-10)
722// ---------------------------------------------------------------------------
723
724/// D-STAR active callsign slot index (0-10).
725///
726/// Selects which callsign from the repeater list is active.
727/// Used by the `CS` CAT command.
728#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
729pub struct CallsignSlot(u8);
730
731impl CallsignSlot {
732    /// Maximum valid callsign slot index (inclusive).
733    pub const MAX: u8 = 10;
734
735    /// Creates a new `CallsignSlot` from a raw value.
736    ///
737    /// # Errors
738    ///
739    /// Returns [`ValidationError::SettingOutOfRange`] if `value > 10`.
740    pub const fn new(value: u8) -> Result<Self, ValidationError> {
741        if value > 10 {
742            Err(ValidationError::SettingOutOfRange {
743                name: "callsign slot",
744                value,
745                detail: "must be 0-10",
746            })
747        } else {
748            Ok(Self(value))
749        }
750    }
751
752    /// Returns the raw `u8` value.
753    #[must_use]
754    pub const fn as_u8(self) -> u8 {
755        self.0
756    }
757}
758
759impl TryFrom<u8> for CallsignSlot {
760    type Error = ValidationError;
761
762    fn try_from(value: u8) -> Result<Self, Self::Error> {
763        Self::new(value)
764    }
765}
766
767impl From<CallsignSlot> for u8 {
768    fn from(slot: CallsignSlot) -> Self {
769        slot.0
770    }
771}
772
773impl fmt::Display for CallsignSlot {
774    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
775        write!(f, "Slot {}", self.0)
776    }
777}
778
779// ---------------------------------------------------------------------------
780// DetectOutputMode (IO command)
781// ---------------------------------------------------------------------------
782
783/// AF/IF/Detect output mode (Menu No. 102).
784///
785/// Controls what signal is output via the USB connector to a PC.
786/// Used by the `IO` CAT command. Band B single-band mode must be
787/// active to select IF or Detect.
788///
789/// Per User Manual Chapter 12:
790///
791/// - When IF or Detect is selected, Band A is hidden and its audio
792///   output stops. Beeps and voice guidance are also suppressed.
793/// - Special PC software is required to process IF or Detect signals.
794/// - KISS mode prevents selecting IF or Detect.
795/// - DV mode prevents selecting Detect.
796/// - For IF 12 kHz output, the demodulation mode can be AM/LSB/USB/CW.
797///
798/// Source: User Manual Chapter 12 "AF/IF/DETECT OUTPUT MODE".
799#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
800pub enum DetectOutputMode {
801    /// AF output — received audio sound (index 0).
802    Af = 0,
803    /// IF output — received IF signal of Band B to PC (index 1).
804    If = 1,
805    /// Detect output — decoded signal of Band B to PC (index 2).
806    Detect = 2,
807}
808
809impl DetectOutputMode {
810    /// Number of valid detect output mode values (0-2).
811    pub const COUNT: u8 = 3;
812}
813
814impl fmt::Display for DetectOutputMode {
815    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
816        match self {
817            Self::Af => f.write_str("AF"),
818            Self::If => f.write_str("IF"),
819            Self::Detect => f.write_str("Detect"),
820        }
821    }
822}
823
824impl TryFrom<u8> for DetectOutputMode {
825    type Error = ValidationError;
826
827    fn try_from(value: u8) -> Result<Self, Self::Error> {
828        match value {
829            0 => Ok(Self::Af),
830            1 => Ok(Self::If),
831            2 => Ok(Self::Detect),
832            _ => Err(ValidationError::SettingOutOfRange {
833                name: "detect output mode",
834                value,
835                detail: "must be 0-2",
836            }),
837        }
838    }
839}
840
841impl From<DetectOutputMode> for u8 {
842    fn from(mode: DetectOutputMode) -> Self {
843        mode as Self
844    }
845}
846
847// ---------------------------------------------------------------------------
848// DvGatewayMode
849// ---------------------------------------------------------------------------
850
851/// DV Gateway operating mode (Menu 650).
852///
853/// Controls whether the radio acts as a DV Gateway for D-STAR reflector
854/// access via USB or Bluetooth using third-party MMDVM applications.
855/// Used by the `GW` CAT command.
856///
857/// Source: User Manual §16-13, firmware decompilation of `cat_gw_handler`.
858#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
859pub enum DvGatewayMode {
860    /// DV Gateway off (index 0).
861    Off = 0,
862    /// Reflector Terminal Mode enabled (index 1).
863    ReflectorTerminal = 1,
864    /// Access Point mode (index 2). Discovered via ARFC-D75 decompilation
865    /// which shows 3 gateway modes (0/1/2). Needs hardware verification
866    /// to confirm exact behavior — may be "Auto" or "Access Point" mode
867    /// for D-STAR hotspot operation.
868    AccessPoint = 2,
869}
870
871impl DvGatewayMode {
872    /// Number of valid DV gateway mode values (0-2).
873    pub const COUNT: u8 = 3;
874}
875
876impl fmt::Display for DvGatewayMode {
877    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
878        match self {
879            Self::Off => f.write_str("Off"),
880            Self::ReflectorTerminal => f.write_str("Reflector TERM"),
881            Self::AccessPoint => f.write_str("Access Point"),
882        }
883    }
884}
885
886impl TryFrom<u8> for DvGatewayMode {
887    type Error = ValidationError;
888
889    fn try_from(value: u8) -> Result<Self, Self::Error> {
890        match value {
891            0 => Ok(Self::Off),
892            1 => Ok(Self::ReflectorTerminal),
893            2 => Ok(Self::AccessPoint),
894            _ => Err(ValidationError::SettingOutOfRange {
895                name: "DV gateway mode",
896                value,
897                detail: "must be 0-2",
898            }),
899        }
900    }
901}
902
903impl From<DvGatewayMode> for u8 {
904    fn from(mode: DvGatewayMode) -> Self {
905        mode as Self
906    }
907}
908
909// ---------------------------------------------------------------------------
910// TncMode
911// ---------------------------------------------------------------------------
912
913/// TNC operating mode.
914///
915/// Controls the built-in TNC's protocol mode. Used by the `TN` CAT command.
916/// The second field of TN is the data speed (0=1200, 1=9600).
917///
918/// Source: firmware validation (mode < 4), Operating Tips §2.7-2.8 (KISS),
919/// §4.5 (Reflector Terminal/MMDVM), firmware string table (NAVITRA).
920#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
921pub enum TncMode {
922    /// APRS mode — standard packet operation (index 0).
923    Aprs = 0,
924    /// NAVITRA mode — Japanese APRS variant (index 1).
925    Navitra = 1,
926    /// KISS mode — PC-based packet via KISS protocol (index 2).
927    /// Enter with `TN 2,0` (Band A) or `TN 2,1` (Band B).
928    /// See Operating Tips §2.7, User Manual Chapter 15.
929    /// The built-in TNC has 4 KB TX and RX buffers and supports only
930    /// KISS mode (no Command mode or Converse mode).
931    Kiss = 2,
932    /// MMDVM/Reflector Terminal mode — D-STAR reflector access (index 3).
933    /// Uses MMDVM serial commands via USB or Bluetooth.
934    /// See Operating Tips §4.5.
935    Mmdvm = 3,
936}
937
938impl TncMode {
939    /// Number of valid TNC mode values (0-3).
940    pub const COUNT: u8 = 4;
941}
942
943impl fmt::Display for TncMode {
944    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
945        match self {
946            Self::Aprs => f.write_str("APRS"),
947            Self::Navitra => f.write_str("NAVITRA"),
948            Self::Kiss => f.write_str("KISS"),
949            Self::Mmdvm => f.write_str("MMDVM"),
950        }
951    }
952}
953
954impl TryFrom<u8> for TncMode {
955    type Error = ValidationError;
956
957    fn try_from(value: u8) -> Result<Self, Self::Error> {
958        match value {
959            0 => Ok(Self::Aprs),
960            1 => Ok(Self::Navitra),
961            2 => Ok(Self::Kiss),
962            3 => Ok(Self::Mmdvm),
963            _ => Err(ValidationError::SettingOutOfRange {
964                name: "TNC mode",
965                value,
966                detail: "must be 0-3",
967            }),
968        }
969    }
970}
971
972impl From<TncMode> for u8 {
973    fn from(mode: TncMode) -> Self {
974        mode as Self
975    }
976}
977
978// ---------------------------------------------------------------------------
979// FilterWidthIndex (SH command)
980// ---------------------------------------------------------------------------
981
982/// IF receive filter width index for the SH (filter width) command.
983///
984/// The valid range depends on the filter mode:
985/// - **SSB** (mode 0): 0-4 -> 2.2 / 2.4 / 2.6 / 2.8 / 3.0 kHz high-cut
986///   (Menu No. 120, default 2.4 kHz). Low cut is fixed at 200 Hz.
987/// - **CW** (mode 1): 0-4 -> 0.3 / 0.5 / 1.0 / 1.5 / 2.0 kHz bandwidth
988///   (Menu No. 121, default 1.0 kHz). The filter is centered on the
989///   pitch frequency (Menu No. 170).
990/// - **AM** (mode 2): 0-3 -> 3.0 / 4.5 / 6.0 / 7.5 kHz high-cut
991///   (Menu No. 122, default 6.0 kHz). Low cut is fixed at 200 Hz.
992///
993/// Per User Manual Chapter 12: these filters reduce interference and
994/// noise in SSB, CW, and AM modes to improve reception. Band B only.
995///
996/// Source: Kenwood TH-D75A/E Operating Tips §5.10 (May 2024).
997/// Hardware-verified: `SH mode,width\r` returns echo on success.
998#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
999pub struct FilterWidthIndex(u8);
1000
1001impl FilterWidthIndex {
1002    /// Maximum valid index for SSB and CW modes.
1003    const MAX_SSB_CW: u8 = 4;
1004    /// Maximum valid index for AM mode.
1005    const MAX_AM: u8 = 3;
1006
1007    /// Creates a new `FilterWidthIndex`, validating against the given mode.
1008    ///
1009    /// # Errors
1010    ///
1011    /// Returns [`ValidationError::SettingOutOfRange`] if `value` exceeds the
1012    /// mode-specific maximum (4 for SSB/CW, 3 for AM).
1013    pub const fn new(value: u8, mode: FilterMode) -> Result<Self, ValidationError> {
1014        let max = match mode {
1015            FilterMode::Ssb | FilterMode::Cw => Self::MAX_SSB_CW,
1016            FilterMode::Am => Self::MAX_AM,
1017        };
1018        if value > max {
1019            Err(ValidationError::SettingOutOfRange {
1020                name: "filter width index",
1021                value,
1022                detail: match mode {
1023                    FilterMode::Ssb | FilterMode::Cw => "must be 0-4 for SSB/CW",
1024                    FilterMode::Am => "must be 0-3 for AM",
1025                },
1026            })
1027        } else {
1028            Ok(Self(value))
1029        }
1030    }
1031
1032    /// Creates a `FilterWidthIndex` from a raw value without mode checking.
1033    ///
1034    /// Uses the maximum range (0-4) which covers all modes. Use this when
1035    /// parsing responses where the mode is known but the width may come from
1036    /// hardware that could return extended values.
1037    ///
1038    /// # Errors
1039    ///
1040    /// Returns [`ValidationError::SettingOutOfRange`] if `value > 4`.
1041    pub const fn from_raw(value: u8) -> Result<Self, ValidationError> {
1042        if value > Self::MAX_SSB_CW {
1043            Err(ValidationError::SettingOutOfRange {
1044                name: "filter width index",
1045                value,
1046                detail: "must be 0-4",
1047            })
1048        } else {
1049            Ok(Self(value))
1050        }
1051    }
1052
1053    /// Returns the raw `u8` value.
1054    #[must_use]
1055    pub const fn as_u8(self) -> u8 {
1056        self.0
1057    }
1058}
1059
1060impl TryFrom<u8> for FilterWidthIndex {
1061    type Error = ValidationError;
1062
1063    fn try_from(value: u8) -> Result<Self, Self::Error> {
1064        Self::from_raw(value)
1065    }
1066}
1067
1068impl From<FilterWidthIndex> for u8 {
1069    fn from(idx: FilterWidthIndex) -> Self {
1070        idx.0
1071    }
1072}
1073
1074impl fmt::Display for FilterWidthIndex {
1075    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1076        write!(f, "{}", self.0)
1077    }
1078}
1079
1080// ---------------------------------------------------------------------------
1081// GpsRadioMode (GM command)
1082// ---------------------------------------------------------------------------
1083
1084/// GPS/Radio operating mode (GM command).
1085///
1086/// Controls whether the radio operates in normal transceiver mode or
1087/// switches to GPS-receiver-only mode.
1088///
1089/// # Firmware verification
1090///
1091/// The `cat_gm_handler` at `0xC002EC52` guards with `local_18 < 2`,
1092/// confirming only values 0 and 1 are valid.
1093///
1094/// # Warning
1095///
1096/// Setting this to `GpsReceiver` (1) via `GM 1\r` **reboots the radio**
1097/// into GPS-only mode. The radio becomes unresponsive to CAT commands
1098/// until manually power-cycled back to normal mode.
1099#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1100pub enum GpsRadioMode {
1101    /// Normal transceiver mode (index 0).
1102    Normal = 0,
1103    /// GPS receiver mode (index 1) — **reboots the radio**.
1104    GpsReceiver = 1,
1105}
1106
1107impl GpsRadioMode {
1108    /// Number of valid GPS radio mode values (0-1).
1109    pub const COUNT: u8 = 2;
1110}
1111
1112impl TryFrom<u8> for GpsRadioMode {
1113    type Error = ValidationError;
1114
1115    fn try_from(value: u8) -> Result<Self, Self::Error> {
1116        match value {
1117            0 => Ok(Self::Normal),
1118            1 => Ok(Self::GpsReceiver),
1119            _ => Err(ValidationError::SettingOutOfRange {
1120                name: "GPS radio mode",
1121                value,
1122                detail: "must be 0-1",
1123            }),
1124        }
1125    }
1126}
1127
1128impl From<GpsRadioMode> for u8 {
1129    fn from(mode: GpsRadioMode) -> Self {
1130        mode as Self
1131    }
1132}
1133
1134impl fmt::Display for GpsRadioMode {
1135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1136        match self {
1137            Self::Normal => write!(f, "Normal"),
1138            Self::GpsReceiver => write!(f, "GPS Receiver"),
1139        }
1140    }
1141}
1142
1143// ---------------------------------------------------------------------------
1144// Tests
1145// ---------------------------------------------------------------------------
1146
1147#[cfg(test)]
1148mod tests {
1149    use super::*;
1150
1151    #[test]
1152    fn squelch_level_valid() {
1153        for v in 0..SquelchLevel::COUNT {
1154            let val = SquelchLevel::new(v).unwrap();
1155            assert_eq!(val.as_u8(), v, "SquelchLevel round-trip failed at {v}");
1156        }
1157        assert!(SquelchLevel::new(SquelchLevel::COUNT).is_err());
1158    }
1159
1160    #[test]
1161    fn squelch_level_round_trip() {
1162        let sq = SquelchLevel::new(4).unwrap();
1163        assert_eq!(u8::from(sq), 4);
1164        assert_eq!(sq.as_u8(), 4);
1165    }
1166
1167    #[test]
1168    fn af_gain_valid() {
1169        // AfGainLevel accepts full u8 range — radio reads can exceed 99
1170        assert_eq!(AfGainLevel::new(0).as_u8(), 0);
1171        assert_eq!(AfGainLevel::new(99).as_u8(), 99);
1172        assert_eq!(AfGainLevel::new(113).as_u8(), 113);
1173        assert_eq!(AfGainLevel::new(255).as_u8(), 255);
1174    }
1175
1176    #[test]
1177    fn smeter_s_units() {
1178        assert_eq!(SMeterReading::new(0).unwrap().s_unit(), "S0");
1179        assert_eq!(
1180            SMeterReading::new(SMeterReading::COUNT - 1)
1181                .unwrap()
1182                .s_unit(),
1183            "S9"
1184        );
1185        assert!(SMeterReading::new(SMeterReading::COUNT).is_err());
1186    }
1187
1188    #[test]
1189    fn vfo_memory_mode_round_trip() {
1190        for v in 0..VfoMemoryMode::COUNT {
1191            let mode = VfoMemoryMode::try_from(v).unwrap();
1192            assert_eq!(u8::from(mode), v);
1193        }
1194        assert!(VfoMemoryMode::try_from(VfoMemoryMode::COUNT).is_err());
1195    }
1196
1197    #[test]
1198    fn filter_mode_round_trip() {
1199        for v in 0..FilterMode::COUNT {
1200            let mode = FilterMode::try_from(v).unwrap();
1201            assert_eq!(u8::from(mode), v);
1202        }
1203        assert!(FilterMode::try_from(FilterMode::COUNT).is_err());
1204    }
1205
1206    #[test]
1207    fn battery_level_round_trip() {
1208        for v in 0..BatteryLevel::COUNT {
1209            let bl = BatteryLevel::try_from(v).unwrap();
1210            assert_eq!(u8::from(bl), v);
1211        }
1212        assert!(BatteryLevel::try_from(BatteryLevel::COUNT).is_err());
1213    }
1214
1215    #[test]
1216    fn battery_level_charging() {
1217        assert_eq!(BatteryLevel::try_from(4).unwrap(), BatteryLevel::Charging);
1218    }
1219
1220    #[test]
1221    fn vox_gain_valid() {
1222        assert!(VoxGain::new(0).is_ok());
1223        assert!(VoxGain::new(VoxGain::MAX).is_ok());
1224        assert!(VoxGain::new(VoxGain::MAX + 1).is_err());
1225    }
1226
1227    #[test]
1228    fn vox_delay_millis() {
1229        let d = VoxDelay::new(15).unwrap();
1230        assert_eq!(d.as_millis(), 1500);
1231        assert!(VoxDelay::new(VoxDelay::MAX + 1).is_err());
1232    }
1233
1234    #[test]
1235    fn tnc_baud_round_trip() {
1236        for v in 0..TncBaud::COUNT {
1237            let val = TncBaud::try_from(v).unwrap();
1238            assert_eq!(u8::from(val), v, "TncBaud round-trip failed at {v}");
1239        }
1240        assert!(TncBaud::try_from(TncBaud::COUNT).is_err());
1241    }
1242
1243    #[test]
1244    fn beacon_mode_round_trip() {
1245        for v in 0..BeaconMode::COUNT {
1246            let mode = BeaconMode::try_from(v).unwrap();
1247            assert_eq!(u8::from(mode), v);
1248        }
1249        assert!(BeaconMode::try_from(BeaconMode::COUNT).is_err());
1250    }
1251
1252    #[test]
1253    fn dstar_slot_valid() {
1254        assert!(DstarSlot::new(DstarSlot::MIN - 1).is_err());
1255        assert!(DstarSlot::new(DstarSlot::MIN).is_ok());
1256        assert!(DstarSlot::new(DstarSlot::MAX).is_ok());
1257        assert!(DstarSlot::new(DstarSlot::MAX + 1).is_err());
1258    }
1259
1260    #[test]
1261    fn tnc_mode_round_trip() {
1262        for v in 0..TncMode::COUNT {
1263            let mode = TncMode::try_from(v).unwrap();
1264            assert_eq!(u8::from(mode), v);
1265        }
1266        assert!(TncMode::try_from(TncMode::COUNT).is_err());
1267    }
1268
1269    #[test]
1270    fn tnc_mode_kiss() {
1271        assert_eq!(TncMode::try_from(2).unwrap(), TncMode::Kiss);
1272    }
1273
1274    #[test]
1275    fn callsign_slot_valid() {
1276        assert!(CallsignSlot::new(0).is_ok());
1277        assert!(CallsignSlot::new(CallsignSlot::MAX).is_ok());
1278        assert!(CallsignSlot::new(CallsignSlot::MAX + 1).is_err());
1279    }
1280
1281    #[test]
1282    fn filter_width_ssb_cw_range() {
1283        for v in 0..=4 {
1284            assert!(FilterWidthIndex::new(v, FilterMode::Ssb).is_ok());
1285            assert!(FilterWidthIndex::new(v, FilterMode::Cw).is_ok());
1286        }
1287        assert!(FilterWidthIndex::new(5, FilterMode::Ssb).is_err());
1288        assert!(FilterWidthIndex::new(5, FilterMode::Cw).is_err());
1289    }
1290
1291    #[test]
1292    fn filter_width_am_range() {
1293        for v in 0..=3 {
1294            assert!(FilterWidthIndex::new(v, FilterMode::Am).is_ok());
1295        }
1296        assert!(FilterWidthIndex::new(4, FilterMode::Am).is_err());
1297    }
1298
1299    #[test]
1300    fn filter_width_from_raw() {
1301        assert!(FilterWidthIndex::from_raw(4).is_ok());
1302        assert!(FilterWidthIndex::from_raw(5).is_err());
1303    }
1304
1305    #[test]
1306    fn detect_output_mode_round_trip() {
1307        for v in 0..DetectOutputMode::COUNT {
1308            let val = DetectOutputMode::try_from(v).unwrap();
1309            assert_eq!(
1310                u8::from(val),
1311                v,
1312                "DetectOutputMode round-trip failed at {v}"
1313            );
1314        }
1315        assert!(DetectOutputMode::try_from(DetectOutputMode::COUNT).is_err());
1316    }
1317
1318    #[test]
1319    fn dv_gateway_mode_round_trip() {
1320        for v in 0..DvGatewayMode::COUNT {
1321            let val = DvGatewayMode::try_from(v).unwrap();
1322            assert_eq!(u8::from(val), v, "DvGatewayMode round-trip failed at {v}");
1323        }
1324        assert!(DvGatewayMode::try_from(DvGatewayMode::COUNT).is_err());
1325    }
1326
1327    #[test]
1328    fn gps_radio_mode_round_trip() {
1329        for v in 0..GpsRadioMode::COUNT {
1330            let val = GpsRadioMode::try_from(v).unwrap();
1331            assert_eq!(u8::from(val), v, "GpsRadioMode round-trip failed at {v}");
1332        }
1333        assert!(GpsRadioMode::try_from(GpsRadioMode::COUNT).is_err());
1334    }
1335}