kenwood_thd75/types/
mode.rs

1//! Operating mode, power level, shift direction, and step size types.
2
3use std::fmt;
4
5use crate::error::ValidationError;
6
7/// Operating mode as returned by the `MD` (mode) CAT command.
8///
9/// The TH-D75 supports 10 modes (0-9) via the `MD` command. Modes 0-7
10/// are confirmed by firmware RE and the KI4LAX CAT command reference.
11/// Modes 8 (WFM) and 9 (CW-R) are confirmed by the ARFC-D75
12/// decompilation.
13///
14/// Note: the `FO`/`ME` commands use a **different** mode encoding
15/// (0=FM, 1=DV, 2=NFM, 3=AM) stored as a raw `u8` in [`ChannelMemory`].
16/// This enum is only used for the `MD` command.
17///
18/// # Band restrictions (per Kenwood Operating Tips §5.9)
19///
20/// Not all modes are available on both bands:
21///
22/// - **Band A** supports only **FM** and **DV**. Band A is the amateur
23///   TX/RX band (144/220/430 MHz). Its receiver chain (VCO/PLL IC800,
24///   IF IC IC900) is a double super heterodyne with 1st IF at 57.15 MHz
25///   and 2nd IF at 450 kHz — it has no third IF stage, so AM/SSB/CW
26///   demodulation is not possible in hardware (service manual §2.1.3).
27/// - **Band B** supports all modes: FM, DV, AM, LSB, USB, CW, NFM, DR,
28///   WFM, and CW-R. Band B's receiver chain (VCO/PLL IC700, IF IC
29///   IC1002) adds a third mixer (IC1001) producing a 3rd IF at 10.8 kHz,
30///   which feeds into the CODEC (IC2011) for AM/SSB/CW demodulation.
31///   This triple super heterodyne architecture is what enables the
32///   wideband mode support (service manual §2.1.3.2).
33/// - **DR** (D-STAR repeater mode) is only available on **Band A**.
34///   Attempting to set DR on Band B via `MD` will be rejected by the
35///   firmware with a `?` error.
36///
37/// Attempting to set an unsupported mode on a band via the `MD` command
38/// will result in the radio returning a `?` error response.
39///
40/// # Mode cycling on the radio (per User Manual Chapter 5)
41///
42/// Pressing `[MODE]` cycles through available modes:
43/// - Band A: FM/NFM -> DR (DV) -> (back to FM/NFM)
44/// - Band B: FM/NFM -> DR (DV) -> AM -> LSB -> USB -> CW -> (back to FM/NFM)
45///
46/// Switching between DV and DR requires the Digital Function Menu, not
47/// `[MODE]`. Switching between FM and NFM requires Menu No. 103
48/// (FM Narrow), not `[MODE]`.
49///
50/// # WFM (Wide FM)
51///
52/// WFM is `MD` mode 8, confirmed by ARFC-D75 decompilation. It is the
53/// FM broadcast radio mode used on Band B for the 76-108 MHz range.
54/// The radio's display shows "WFM" in this mode.
55///
56/// # CW-R (CW Reverse)
57///
58/// CW-R is `MD` mode 9, confirmed by ARFC-D75 decompilation. It uses
59/// LSB detection for CW reception instead of the default USB detection
60/// used by standard CW mode.
61///
62/// [`ChannelMemory`]: crate::types::ChannelMemory
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum Mode {
65    /// FM modulation (index 0). Available on both Band A and Band B.
66    Fm = 0,
67    /// D-STAR digital voice (index 1). Available on both Band A and Band B.
68    Dv = 1,
69    /// AM modulation (index 2). Band B only — Band A lacks the 3rd IF
70    /// stage (10.8 kHz via IC1001) required for AM envelope detection.
71    Am = 2,
72    /// Lower sideband (index 3). Band B only — requires the 3rd IF at
73    /// 10.8 kHz (via 3rd mixer IC1001 and 460.8 kHz local oscillation).
74    Lsb = 3,
75    /// Upper sideband (index 4). Band B only — requires the 3rd IF at
76    /// 10.8 kHz (via 3rd mixer IC1001 and 460.8 kHz local oscillation).
77    Usb = 4,
78    /// CW / Morse code (index 5). Band B only — requires the 3rd IF at
79    /// 10.8 kHz (via 3rd mixer IC1001 and 460.8 kHz local oscillation).
80    Cw = 5,
81    /// Narrow FM modulation (index 6). Band B only — Band A supports
82    /// only standard FM deviation.
83    Nfm = 6,
84    /// D-STAR repeater mode (index 7). Band A only — DR requires the
85    /// CTRL/PTT band for gateway access and callsign routing.
86    Dr = 7,
87    /// Wide FM (index 8). Band B only — FM broadcast reception mode
88    /// for the 76-108 MHz range. Confirmed by ARFC-D75 decompilation.
89    Wfm = 8,
90    /// CW Reverse (index 9). Band B only — uses LSB detection for CW
91    /// reception instead of the default USB. Confirmed by ARFC-D75
92    /// decompilation.
93    CwReverse = 9,
94}
95
96impl Mode {
97    /// Number of valid mode values (0-9).
98    pub const COUNT: u8 = 10;
99}
100
101impl fmt::Display for Mode {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            Self::Fm => f.write_str("FM"),
105            Self::Dv => f.write_str("DV"),
106            Self::Am => f.write_str("AM"),
107            Self::Lsb => f.write_str("LSB"),
108            Self::Usb => f.write_str("USB"),
109            Self::Cw => f.write_str("CW"),
110            Self::Nfm => f.write_str("NFM"),
111            Self::Dr => f.write_str("DR"),
112            Self::Wfm => f.write_str("WFM"),
113            Self::CwReverse => f.write_str("CW-R"),
114        }
115    }
116}
117
118impl TryFrom<u8> for Mode {
119    type Error = ValidationError;
120
121    fn try_from(value: u8) -> Result<Self, Self::Error> {
122        match value {
123            0 => Ok(Self::Fm),
124            1 => Ok(Self::Dv),
125            2 => Ok(Self::Am),
126            3 => Ok(Self::Lsb),
127            4 => Ok(Self::Usb),
128            5 => Ok(Self::Cw),
129            6 => Ok(Self::Nfm),
130            7 => Ok(Self::Dr),
131            8 => Ok(Self::Wfm),
132            9 => Ok(Self::CwReverse),
133            _ => Err(ValidationError::ModeOutOfRange(value)),
134        }
135    }
136}
137
138impl From<Mode> for u8 {
139    fn from(mode: Mode) -> Self {
140        mode as Self
141    }
142}
143
144/// Transmit power level.
145///
146/// Maps to the power field in the `PC`, `FO`, and `ME` commands.
147/// The D75 firmware RE confirms 4 levels: Hi (0), Mid (1), Lo (2), EL (3).
148///
149/// Per User Manual Chapter 5 and Chapter 28: power output with external
150/// DC 13.8 V or battery 7.4 V:
151///
152/// | Level | Output | Current (DC IN) | Current (Batt) |
153/// |-------|--------|-----------------|----------------|
154/// | High | 5 W | 1.4 A | 2.0 A |
155/// | Medium | 2 W | 0.9 A | 1.3 A |
156/// | Low | 0.5 W | 0.6 A | 0.8 A |
157/// | EL | 0.05 W | 0.4 A | 0.5 A |
158///
159/// Power settings can be programmed independently for Band A and Band B.
160/// The optional KBP-9 alkaline battery case supports Low power only.
161/// Power level cannot be changed while transmitting.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
163pub enum PowerLevel {
164    /// High power — 5 W (index 0).
165    High = 0,
166    /// Medium power — 2 W (index 1).
167    Medium = 1,
168    /// Low power — 0.5 W (index 2).
169    Low = 2,
170    /// Extra-low power — 50 mW (index 3). D75-specific; not present on the TH-D74.
171    ExtraLow = 3,
172}
173
174impl PowerLevel {
175    /// Number of valid power level values (0-3).
176    pub const COUNT: u8 = 4;
177}
178
179impl fmt::Display for PowerLevel {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        match self {
182            Self::High => f.write_str("High"),
183            Self::Medium => f.write_str("Medium"),
184            Self::Low => f.write_str("Low"),
185            Self::ExtraLow => f.write_str("EL"),
186        }
187    }
188}
189
190impl TryFrom<u8> for PowerLevel {
191    type Error = ValidationError;
192
193    fn try_from(value: u8) -> Result<Self, Self::Error> {
194        match value {
195            0 => Ok(Self::High),
196            1 => Ok(Self::Medium),
197            2 => Ok(Self::Low),
198            3 => Ok(Self::ExtraLow),
199            _ => Err(ValidationError::PowerLevelOutOfRange(value)),
200        }
201    }
202}
203
204impl From<PowerLevel> for u8 {
205    fn from(level: PowerLevel) -> Self {
206        level as Self
207    }
208}
209
210/// Repeater shift direction, stored as a raw firmware value.
211///
212/// Maps to the shift field (4-bit, low nibble of byte 0x08) in the `FO`
213/// and `ME` commands. Known values: 0 = Simplex, 1 = Up, 2 = Down,
214/// 3 = Split. Values 4-15 are used by VFO mode for extended shift
215/// configurations whose meaning is not yet fully documented.
216///
217/// Accepts any value in the 4-bit range 0-15 to avoid parse failures
218/// when reading VFO state from the radio.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
220pub struct ShiftDirection(u8);
221
222impl ShiftDirection {
223    /// Simplex, no shift (value 0).
224    pub const SIMPLEX: Self = Self(0);
225    /// Positive shift (value 1).
226    pub const UP: Self = Self(1);
227    /// Negative shift (value 2).
228    pub const DOWN: Self = Self(2);
229    /// Split: separate TX frequency (value 3).
230    pub const SPLIT: Self = Self(3);
231
232    /// Creates a new `ShiftDirection` from a raw 4-bit value.
233    ///
234    /// # Errors
235    ///
236    /// Returns [`ValidationError::ShiftOutOfRange`] if `value > 15`.
237    pub const fn new(value: u8) -> Result<Self, ValidationError> {
238        if value <= 15 {
239            Ok(Self(value))
240        } else {
241            Err(ValidationError::ShiftOutOfRange(value))
242        }
243    }
244
245    /// Returns the raw firmware value.
246    #[must_use]
247    pub const fn as_u8(self) -> u8 {
248        self.0
249    }
250
251    /// Returns `true` if this is a well-known shift direction (0-3).
252    #[must_use]
253    pub const fn is_known(self) -> bool {
254        self.0 <= 3
255    }
256}
257
258impl TryFrom<u8> for ShiftDirection {
259    type Error = ValidationError;
260
261    fn try_from(value: u8) -> Result<Self, Self::Error> {
262        Self::new(value)
263    }
264}
265
266impl From<ShiftDirection> for u8 {
267    fn from(dir: ShiftDirection) -> Self {
268        dir.0
269    }
270}
271
272/// Frequency step size for tuning.
273///
274/// Maps to the step field in the `FO` and `ME` commands.
275/// The variant name encodes the step in Hz (e.g. `Hz5000` = 5.0 kHz).
276///
277/// Per User Manual Chapter 12: each band can have a separate step size.
278/// Step size can only be changed in VFO mode and not while in FM
279/// broadcast mode. Band-specific restrictions:
280///
281/// - 8.33 kHz is selectable only in the 118 MHz (airband) range.
282/// - 9.0 kHz is selectable only in the LF/MF (AM broadcast) range.
283///
284/// Default step sizes per band (TH-D75A): 144 MHz = 5 kHz, 220 MHz =
285/// 20 kHz, 430 MHz = 25 kHz. TH-D75E defaults: 144 MHz = 12.5 kHz,
286/// 430 MHz = 25 kHz.
287///
288/// Changing step size may correct the displayed frequency. For example,
289/// if 144.995 MHz is shown with 5 kHz steps, switching to 12.5 kHz
290/// steps changes it to 144.9875 MHz.
291///
292/// # KI4LAX TABLE C reference
293///
294/// The hex index-to-step-size mapping (TABLE C in the KI4LAX CAT command
295/// reference) is as follows:
296///
297/// | Index (hex) | Step size |
298/// |-------------|-----------|
299/// | 0x0 | 5.0 kHz |
300/// | 0x1 | 6.25 kHz |
301/// | 0x2 | 8.33 kHz |
302/// | 0x3 | 9.0 kHz |
303/// | 0x4 | 10.0 kHz |
304/// | 0x5 | 12.5 kHz |
305/// | 0x6 | 15.0 kHz |
306/// | 0x7 | 20.0 kHz |
307/// | 0x8 | 25.0 kHz |
308/// | 0x9 | 30.0 kHz |
309/// | 0xA | 50.0 kHz |
310/// | 0xB | 100.0 kHz |
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
312pub enum StepSize {
313    /// 5.000 kHz step (index 0).
314    Hz5000 = 0,
315    /// 6.250 kHz step (index 1).
316    Hz6250 = 1,
317    /// 8.330 kHz step (index 2).
318    Hz8330 = 2,
319    /// 9.000 kHz step (index 3).
320    Hz9000 = 3,
321    /// 10.000 kHz step (index 4).
322    Hz10000 = 4,
323    /// 12.500 kHz step (index 5).
324    Hz12500 = 5,
325    /// 15.000 kHz step (index 6).
326    Hz15000 = 6,
327    /// 20.000 kHz step (index 7).
328    Hz20000 = 7,
329    /// 25.000 kHz step (index 8).
330    Hz25000 = 8,
331    /// 30.000 kHz step (index 9).
332    Hz30000 = 9,
333    /// 50.000 kHz step (index 10).
334    Hz50000 = 10,
335    /// 100.000 kHz step (index 11).
336    Hz100000 = 11,
337}
338
339impl StepSize {
340    /// Number of valid step size values (0-11).
341    pub const COUNT: u8 = 12;
342
343    /// Returns the step size in Hz.
344    #[must_use]
345    pub const fn as_hz(self) -> u32 {
346        match self {
347            Self::Hz5000 => 5_000,
348            Self::Hz6250 => 6_250,
349            Self::Hz8330 => 8_330,
350            Self::Hz9000 => 9_000,
351            Self::Hz10000 => 10_000,
352            Self::Hz12500 => 12_500,
353            Self::Hz15000 => 15_000,
354            Self::Hz20000 => 20_000,
355            Self::Hz25000 => 25_000,
356            Self::Hz30000 => 30_000,
357            Self::Hz50000 => 50_000,
358            Self::Hz100000 => 100_000,
359        }
360    }
361
362    /// Returns the step size as a kHz display string (e.g. `"5.0"`, `"6.25"`).
363    #[must_use]
364    pub const fn as_khz_str(self) -> &'static str {
365        match self {
366            Self::Hz5000 => "5.0",
367            Self::Hz6250 => "6.25",
368            Self::Hz8330 => "8.33",
369            Self::Hz9000 => "9.0",
370            Self::Hz10000 => "10.0",
371            Self::Hz12500 => "12.5",
372            Self::Hz15000 => "15.0",
373            Self::Hz20000 => "20.0",
374            Self::Hz25000 => "25.0",
375            Self::Hz30000 => "30.0",
376            Self::Hz50000 => "50.0",
377            Self::Hz100000 => "100.0",
378        }
379    }
380}
381
382impl fmt::Display for StepSize {
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        write!(f, "{} kHz", self.as_khz_str())
385    }
386}
387
388impl TryFrom<u8> for StepSize {
389    type Error = ValidationError;
390
391    fn try_from(value: u8) -> Result<Self, Self::Error> {
392        match value {
393            0 => Ok(Self::Hz5000),
394            1 => Ok(Self::Hz6250),
395            2 => Ok(Self::Hz8330),
396            3 => Ok(Self::Hz9000),
397            4 => Ok(Self::Hz10000),
398            5 => Ok(Self::Hz12500),
399            6 => Ok(Self::Hz15000),
400            7 => Ok(Self::Hz20000),
401            8 => Ok(Self::Hz25000),
402            9 => Ok(Self::Hz30000),
403            10 => Ok(Self::Hz50000),
404            11 => Ok(Self::Hz100000),
405            _ => Err(ValidationError::StepSizeOutOfRange(value)),
406        }
407    }
408}
409
410impl From<StepSize> for u8 {
411    fn from(step: StepSize) -> Self {
412        step as Self
413    }
414}
415
416/// Coarse tuning step multiplier.
417///
418/// Discovered via ARFC-D75 decompilation. The ARFC application multiplies
419/// the base step size by this factor before sending `UP`/`DW` commands,
420/// enabling faster tuning in large frequency ranges. This is a
421/// client-side feature — the radio itself has no coarse step command.
422///
423/// For example, with a 25.0 kHz base step and a `X10` multiplier, each
424/// `UP`/`DW` press tunes 250.0 kHz.
425#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
426pub enum CoarseStepMultiplier {
427    /// 1x — no multiplication, same as normal step (index 0).
428    X1 = 0,
429    /// 2x multiplication (index 1).
430    X2 = 1,
431    /// 5x multiplication (index 2).
432    X5 = 2,
433    /// 10x multiplication (index 3).
434    X10 = 3,
435    /// 50x multiplication (index 4).
436    X50 = 4,
437    /// 100x multiplication (index 5).
438    X100 = 5,
439}
440
441impl CoarseStepMultiplier {
442    /// Number of valid coarse step multiplier values (0-5).
443    pub const COUNT: u8 = 6;
444
445    /// Returns the numeric multiplier value.
446    #[must_use]
447    pub const fn multiplier(self) -> u16 {
448        match self {
449            Self::X1 => 1,
450            Self::X2 => 2,
451            Self::X5 => 5,
452            Self::X10 => 10,
453            Self::X50 => 50,
454            Self::X100 => 100,
455        }
456    }
457}
458
459impl fmt::Display for CoarseStepMultiplier {
460    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461        write!(f, "x{}", self.multiplier())
462    }
463}
464
465impl TryFrom<u8> for CoarseStepMultiplier {
466    type Error = ValidationError;
467
468    fn try_from(value: u8) -> Result<Self, Self::Error> {
469        match value {
470            0 => Ok(Self::X1),
471            1 => Ok(Self::X2),
472            2 => Ok(Self::X5),
473            3 => Ok(Self::X10),
474            4 => Ok(Self::X50),
475            5 => Ok(Self::X100),
476            _ => Err(ValidationError::SettingOutOfRange {
477                name: "coarse step multiplier",
478                value,
479                detail: "must be 0-5",
480            }),
481        }
482    }
483}
484
485impl From<CoarseStepMultiplier> for u8 {
486    fn from(mult: CoarseStepMultiplier) -> Self {
487        mult as Self
488    }
489}
490
491/// Operating mode as stored in the flash memory image.
492///
493/// This enum represents the mode encoding used in the MCP programming
494/// memory (channel data byte 0x09 bits \[6:4\]). It differs from [`Mode`]
495/// which represents the CAT wire format.
496///
497/// # Flash encoding
498///
499/// | Value | Mode |
500/// |-------|------|
501/// | 0 | FM |
502/// | 1 | DV (D-STAR digital voice) |
503/// | 2 | AM |
504/// | 3 | LSB (lower sideband) |
505/// | 4 | USB (upper sideband) |
506/// | 5 | CW (Morse code) |
507/// | 6 | NFM (narrow FM) |
508/// | 7 | DR (D-STAR repeater) |
509///
510/// # CAT encoding (for comparison)
511///
512/// The CAT protocol (`FO`/`ME` commands) uses a different mapping:
513/// 0=FM, 1=DV, 2=NFM, 3=AM. The memory image encoding adds LSB, USB,
514/// CW, and DR modes that are not available via CAT.
515#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
516pub enum MemoryMode {
517    /// FM modulation (flash value 0).
518    Fm = 0,
519    /// D-STAR digital voice (flash value 1).
520    Dv = 1,
521    /// AM modulation (flash value 2).
522    Am = 2,
523    /// Lower sideband (flash value 3).
524    Lsb = 3,
525    /// Upper sideband (flash value 4).
526    Usb = 4,
527    /// CW / Morse code (flash value 5).
528    Cw = 5,
529    /// Narrow FM modulation (flash value 6).
530    Nfm = 6,
531    /// D-STAR repeater mode (flash value 7).
532    Dr = 7,
533}
534
535impl MemoryMode {
536    /// Number of valid memory mode values (0-7).
537    pub const COUNT: u8 = 8;
538}
539
540impl fmt::Display for MemoryMode {
541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542        match self {
543            Self::Fm => f.write_str("FM"),
544            Self::Dv => f.write_str("DV"),
545            Self::Am => f.write_str("AM"),
546            Self::Lsb => f.write_str("LSB"),
547            Self::Usb => f.write_str("USB"),
548            Self::Cw => f.write_str("CW"),
549            Self::Nfm => f.write_str("NFM"),
550            Self::Dr => f.write_str("DR"),
551        }
552    }
553}
554
555impl TryFrom<u8> for MemoryMode {
556    type Error = ValidationError;
557
558    fn try_from(value: u8) -> Result<Self, Self::Error> {
559        match value {
560            0 => Ok(Self::Fm),
561            1 => Ok(Self::Dv),
562            2 => Ok(Self::Am),
563            3 => Ok(Self::Lsb),
564            4 => Ok(Self::Usb),
565            5 => Ok(Self::Cw),
566            6 => Ok(Self::Nfm),
567            7 => Ok(Self::Dr),
568            _ => Err(ValidationError::MemoryModeOutOfRange(value)),
569        }
570    }
571}
572
573impl From<MemoryMode> for u8 {
574    fn from(mode: MemoryMode) -> Self {
575        mode as Self
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use crate::error::ValidationError;
583
584    // --- Mode ---
585
586    #[test]
587    fn mode_valid_range() {
588        for i in 0u8..Mode::COUNT {
589            let val = Mode::try_from(i).unwrap();
590            assert_eq!(u8::from(val), i, "Mode round-trip failed at {i}");
591        }
592    }
593
594    #[test]
595    fn mode_invalid() {
596        assert!(Mode::try_from(Mode::COUNT).is_err());
597        assert!(Mode::try_from(255).is_err());
598    }
599
600    #[test]
601    fn mode_round_trip() {
602        for i in 0u8..Mode::COUNT {
603            let val = Mode::try_from(i).unwrap();
604            assert_eq!(u8::from(val), i);
605        }
606    }
607
608    #[test]
609    fn mode_error_variant() {
610        let err = Mode::try_from(Mode::COUNT).unwrap_err();
611        assert!(matches!(err, ValidationError::ModeOutOfRange(10)));
612    }
613
614    #[test]
615    fn mode_display() {
616        assert_eq!(Mode::Fm.to_string(), "FM");
617        assert_eq!(Mode::Dv.to_string(), "DV");
618        assert_eq!(Mode::Am.to_string(), "AM");
619        assert_eq!(Mode::Lsb.to_string(), "LSB");
620        assert_eq!(Mode::Usb.to_string(), "USB");
621        assert_eq!(Mode::Cw.to_string(), "CW");
622        assert_eq!(Mode::Nfm.to_string(), "NFM");
623        assert_eq!(Mode::Dr.to_string(), "DR");
624        assert_eq!(Mode::Wfm.to_string(), "WFM");
625        assert_eq!(Mode::CwReverse.to_string(), "CW-R");
626    }
627
628    // --- PowerLevel ---
629
630    #[test]
631    fn power_level_valid_range() {
632        for i in 0u8..PowerLevel::COUNT {
633            let val = PowerLevel::try_from(i).unwrap();
634            assert_eq!(u8::from(val), i, "PowerLevel round-trip failed at {i}");
635        }
636    }
637
638    #[test]
639    fn power_level_invalid() {
640        assert!(PowerLevel::try_from(PowerLevel::COUNT).is_err());
641        assert!(PowerLevel::try_from(255).is_err());
642    }
643
644    #[test]
645    fn power_level_round_trip() {
646        for i in 0u8..PowerLevel::COUNT {
647            let val = PowerLevel::try_from(i).unwrap();
648            assert_eq!(u8::from(val), i);
649        }
650    }
651
652    #[test]
653    fn power_level_error_variant() {
654        let err = PowerLevel::try_from(PowerLevel::COUNT).unwrap_err();
655        assert!(matches!(err, ValidationError::PowerLevelOutOfRange(4)));
656    }
657
658    #[test]
659    fn power_level_display() {
660        assert_eq!(PowerLevel::High.to_string(), "High");
661        assert_eq!(PowerLevel::Medium.to_string(), "Medium");
662        assert_eq!(PowerLevel::Low.to_string(), "Low");
663        assert_eq!(PowerLevel::ExtraLow.to_string(), "EL");
664    }
665
666    // --- ShiftDirection ---
667
668    #[test]
669    fn shift_direction_valid_range() {
670        // All 4-bit values (0-15) are valid.
671        for i in 0u8..=15 {
672            let val = ShiftDirection::try_from(i).unwrap();
673            assert_eq!(u8::from(val), i, "ShiftDirection round-trip failed at {i}");
674        }
675    }
676
677    #[test]
678    fn shift_direction_invalid() {
679        assert!(ShiftDirection::try_from(16).is_err());
680        assert!(ShiftDirection::try_from(255).is_err());
681    }
682
683    #[test]
684    fn shift_direction_round_trip() {
685        for i in 0u8..=15 {
686            let val = ShiftDirection::try_from(i).unwrap();
687            assert_eq!(u8::from(val), i);
688        }
689    }
690
691    #[test]
692    fn shift_direction_known_constants() {
693        assert_eq!(ShiftDirection::SIMPLEX.as_u8(), 0);
694        assert_eq!(ShiftDirection::UP.as_u8(), 1);
695        assert_eq!(ShiftDirection::DOWN.as_u8(), 2);
696        assert_eq!(ShiftDirection::SPLIT.as_u8(), 3);
697        assert!(ShiftDirection::SIMPLEX.is_known());
698        assert!(ShiftDirection::SPLIT.is_known());
699    }
700
701    #[test]
702    fn shift_direction_extended_vfo_values() {
703        // Values 4-15 are valid but not "known" named shift modes.
704        let ext = ShiftDirection::new(8).unwrap();
705        assert_eq!(ext.as_u8(), 8);
706        assert!(!ext.is_known());
707    }
708
709    #[test]
710    fn shift_direction_error_variant() {
711        let err = ShiftDirection::try_from(16).unwrap_err();
712        assert!(matches!(err, ValidationError::ShiftOutOfRange(16)));
713    }
714
715    // --- StepSize ---
716
717    #[test]
718    fn step_size_valid_range() {
719        for i in 0u8..StepSize::COUNT {
720            let val = StepSize::try_from(i).unwrap();
721            assert_eq!(u8::from(val), i, "StepSize round-trip failed at {i}");
722        }
723    }
724
725    #[test]
726    fn step_size_invalid() {
727        assert!(StepSize::try_from(StepSize::COUNT).is_err());
728        assert!(StepSize::try_from(255).is_err());
729    }
730
731    #[test]
732    fn step_size_round_trip() {
733        for i in 0u8..StepSize::COUNT {
734            let val = StepSize::try_from(i).unwrap();
735            assert_eq!(u8::from(val), i);
736        }
737    }
738
739    #[test]
740    fn step_size_error_variant() {
741        let err = StepSize::try_from(StepSize::COUNT).unwrap_err();
742        assert!(matches!(err, ValidationError::StepSizeOutOfRange(12)));
743    }
744
745    #[test]
746    fn step_size_as_hz() {
747        assert_eq!(StepSize::Hz5000.as_hz(), 5_000);
748        assert_eq!(StepSize::Hz6250.as_hz(), 6_250);
749        assert_eq!(StepSize::Hz8330.as_hz(), 8_330);
750        assert_eq!(StepSize::Hz9000.as_hz(), 9_000);
751        assert_eq!(StepSize::Hz10000.as_hz(), 10_000);
752        assert_eq!(StepSize::Hz12500.as_hz(), 12_500);
753        assert_eq!(StepSize::Hz15000.as_hz(), 15_000);
754        assert_eq!(StepSize::Hz20000.as_hz(), 20_000);
755        assert_eq!(StepSize::Hz25000.as_hz(), 25_000);
756        assert_eq!(StepSize::Hz30000.as_hz(), 30_000);
757        assert_eq!(StepSize::Hz50000.as_hz(), 50_000);
758        assert_eq!(StepSize::Hz100000.as_hz(), 100_000);
759    }
760
761    #[test]
762    fn step_size_as_khz_str() {
763        assert_eq!(StepSize::Hz5000.as_khz_str(), "5.0");
764        assert_eq!(StepSize::Hz6250.as_khz_str(), "6.25");
765        assert_eq!(StepSize::Hz8330.as_khz_str(), "8.33");
766        assert_eq!(StepSize::Hz9000.as_khz_str(), "9.0");
767        assert_eq!(StepSize::Hz10000.as_khz_str(), "10.0");
768        assert_eq!(StepSize::Hz12500.as_khz_str(), "12.5");
769        assert_eq!(StepSize::Hz15000.as_khz_str(), "15.0");
770        assert_eq!(StepSize::Hz20000.as_khz_str(), "20.0");
771        assert_eq!(StepSize::Hz25000.as_khz_str(), "25.0");
772        assert_eq!(StepSize::Hz30000.as_khz_str(), "30.0");
773        assert_eq!(StepSize::Hz50000.as_khz_str(), "50.0");
774        assert_eq!(StepSize::Hz100000.as_khz_str(), "100.0");
775    }
776
777    #[test]
778    fn step_size_display() {
779        assert_eq!(StepSize::Hz5000.to_string(), "5.0 kHz");
780        assert_eq!(StepSize::Hz25000.to_string(), "25.0 kHz");
781        assert_eq!(StepSize::Hz8330.to_string(), "8.33 kHz");
782    }
783
784    // --- MemoryMode ---
785
786    #[test]
787    fn memory_mode_valid_range() {
788        for i in 0u8..MemoryMode::COUNT {
789            let val = MemoryMode::try_from(i).unwrap();
790            assert_eq!(u8::from(val), i, "MemoryMode round-trip failed at {i}");
791        }
792    }
793
794    #[test]
795    fn memory_mode_invalid() {
796        assert!(MemoryMode::try_from(MemoryMode::COUNT).is_err());
797        assert!(MemoryMode::try_from(255).is_err());
798    }
799
800    #[test]
801    fn memory_mode_round_trip() {
802        for i in 0u8..MemoryMode::COUNT {
803            let val = MemoryMode::try_from(i).unwrap();
804            assert_eq!(u8::from(val), i);
805        }
806    }
807
808    #[test]
809    fn memory_mode_error_variant() {
810        let err = MemoryMode::try_from(MemoryMode::COUNT).unwrap_err();
811        assert!(matches!(err, ValidationError::MemoryModeOutOfRange(8)));
812    }
813
814    #[test]
815    fn memory_mode_display() {
816        assert_eq!(MemoryMode::Fm.to_string(), "FM");
817        assert_eq!(MemoryMode::Dv.to_string(), "DV");
818        assert_eq!(MemoryMode::Am.to_string(), "AM");
819        assert_eq!(MemoryMode::Lsb.to_string(), "LSB");
820        assert_eq!(MemoryMode::Usb.to_string(), "USB");
821        assert_eq!(MemoryMode::Cw.to_string(), "CW");
822        assert_eq!(MemoryMode::Nfm.to_string(), "NFM");
823        assert_eq!(MemoryMode::Dr.to_string(), "DR");
824    }
825
826    #[test]
827    fn cat_mode_matches_flash_encoding() {
828        // CAT MD and flash memory use the same encoding for all 8 modes (0-7).
829        assert_eq!(u8::from(Mode::Fm), u8::from(MemoryMode::Fm));
830        assert_eq!(u8::from(Mode::Dv), u8::from(MemoryMode::Dv));
831        assert_eq!(u8::from(Mode::Am), u8::from(MemoryMode::Am));
832        assert_eq!(u8::from(Mode::Lsb), u8::from(MemoryMode::Lsb));
833        assert_eq!(u8::from(Mode::Usb), u8::from(MemoryMode::Usb));
834        assert_eq!(u8::from(Mode::Cw), u8::from(MemoryMode::Cw));
835        assert_eq!(u8::from(Mode::Nfm), u8::from(MemoryMode::Nfm));
836        assert_eq!(u8::from(Mode::Dr), u8::from(MemoryMode::Dr));
837    }
838
839    // --- CoarseStepMultiplier ---
840
841    #[test]
842    fn coarse_step_multiplier_valid_range() {
843        for i in 0u8..CoarseStepMultiplier::COUNT {
844            let val = CoarseStepMultiplier::try_from(i).unwrap();
845            assert_eq!(
846                u8::from(val),
847                i,
848                "CoarseStepMultiplier round-trip failed at {i}"
849            );
850        }
851    }
852
853    #[test]
854    fn coarse_step_multiplier_invalid() {
855        assert!(CoarseStepMultiplier::try_from(CoarseStepMultiplier::COUNT).is_err());
856        assert!(CoarseStepMultiplier::try_from(255).is_err());
857    }
858
859    #[test]
860    fn coarse_step_multiplier_round_trip() {
861        for i in 0u8..CoarseStepMultiplier::COUNT {
862            let val = CoarseStepMultiplier::try_from(i).unwrap();
863            assert_eq!(u8::from(val), i);
864        }
865    }
866
867    #[test]
868    fn coarse_step_multiplier_values() {
869        assert_eq!(CoarseStepMultiplier::X1.multiplier(), 1);
870        assert_eq!(CoarseStepMultiplier::X2.multiplier(), 2);
871        assert_eq!(CoarseStepMultiplier::X5.multiplier(), 5);
872        assert_eq!(CoarseStepMultiplier::X10.multiplier(), 10);
873        assert_eq!(CoarseStepMultiplier::X50.multiplier(), 50);
874        assert_eq!(CoarseStepMultiplier::X100.multiplier(), 100);
875    }
876
877    #[test]
878    fn coarse_step_multiplier_display() {
879        assert_eq!(CoarseStepMultiplier::X1.to_string(), "x1");
880        assert_eq!(CoarseStepMultiplier::X2.to_string(), "x2");
881        assert_eq!(CoarseStepMultiplier::X5.to_string(), "x5");
882        assert_eq!(CoarseStepMultiplier::X10.to_string(), "x10");
883        assert_eq!(CoarseStepMultiplier::X50.to_string(), "x50");
884        assert_eq!(CoarseStepMultiplier::X100.to_string(), "x100");
885    }
886}