kenwood_thd75/types/
frequency.rs

1//! Radio frequency type for the TH-D75 transceiver.
2
3use std::fmt;
4
5use crate::error::ProtocolError;
6
7/// Radio frequency in Hz.
8///
9/// Stored as a `u32`, matching the firmware's internal representation.
10/// Range: 0 to 4,294,967,295 Hz (0 to ~4.295 GHz).
11///
12/// # TH-D75 band frequency ranges
13///
14/// Per service manual §2.1.2 (Table 1) and User Manual Chapter 28, the
15/// radio enforces hardware-specific frequency limits per band. The
16/// service manual frequency configuration points (A-E) map to the
17/// signal path in the receiver block diagrams (§2.1.3):
18///
19/// ## TH-D75A (K type)
20///
21/// | Point | Frequency range | Function |
22/// |-------|----------------|----------|
23/// | A (TX/RX) | 144.000-147.995, 222.000-224.995, 430.000-449.995 MHz | VCO/PLL output → 1st mixer |
24/// | B (RX) | 136.000-173.995, 216.000-259.995, 410.000-469.995 MHz | RF AMP → distribution circuit |
25/// | C (RX) | 0.100-75.995, 108.000-523.995 MHz | Band B wideband RX input |
26/// | D (1st IF) | 193.150-231.145, 158.850-202.845, 352.850-412.845 MHz | After 1st mixer (Band A) |
27/// | E (1st IF) | 58.150-134.045, 166.050-465.945 MHz | After 1st mixer (Band B) |
28///
29/// ## TH-D75E (E, T types)
30///
31/// | Point | Frequency range | Function |
32/// |-------|----------------|----------|
33/// | A (TX/RX) | 144.000-145.995, 430.000-439.995 MHz | VCO/PLL output → 1st mixer |
34/// | B (RX) | 136.000-173.995, 410.000-469.995 MHz | RF AMP → distribution circuit |
35/// | C (RX) | 0.100-75.995, 108.000-523.995 MHz | Band B wideband RX input |
36///
37/// Band A uses double super heterodyne (1st IF 57.15 MHz, 2nd IF
38/// 450 kHz). Band B uses triple super heterodyne (1st IF 58.05 MHz,
39/// 2nd IF 450 kHz, 3rd IF 10.8 kHz for AM/SSB/CW).
40///
41/// Frequencies outside these ranges will be **rejected by the radio**
42/// when sent via CAT commands such as `FQ` or `FO`. The firmware
43/// validates the frequency against the target band's allowed range and
44/// returns a `?` error response if the value is out of bounds. This
45/// library does not pre-validate frequencies to avoid duplicating
46/// firmware logic that may vary by region or firmware version.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
48pub struct Frequency(u32);
49
50impl Frequency {
51    /// Creates a new `Frequency` from a value in Hz.
52    ///
53    /// No validation is performed; the full `u32` range is accepted
54    /// to match firmware behaviour.
55    #[must_use]
56    pub const fn new(hz: u32) -> Self {
57        Self(hz)
58    }
59
60    /// Returns the frequency in Hz.
61    #[must_use]
62    pub const fn as_hz(self) -> u32 {
63        self.0
64    }
65
66    /// Returns the frequency in kHz as a floating-point value.
67    #[must_use]
68    #[allow(clippy::cast_precision_loss)]
69    pub fn as_khz(self) -> f64 {
70        f64::from(self.0) / 1_000.0
71    }
72
73    /// Returns the frequency in MHz as a floating-point value.
74    #[must_use]
75    #[allow(clippy::cast_precision_loss)]
76    pub fn as_mhz(self) -> f64 {
77        f64::from(self.0) / 1_000_000.0
78    }
79
80    /// Formats the frequency as a 10-digit zero-padded decimal string
81    /// for CAT protocol wire transmission.
82    ///
83    /// Example: 145 MHz becomes `"0145000000"`.
84    #[must_use]
85    pub fn to_wire_string(self) -> String {
86        format!("{:010}", self.0)
87    }
88
89    /// Parses a 10-digit decimal string from the CAT protocol into a
90    /// `Frequency`.
91    ///
92    /// # Errors
93    ///
94    /// Returns [`ProtocolError::FieldParse`] if the string is not
95    /// exactly 10 characters or contains non-numeric characters.
96    pub fn from_wire_string(s: &str) -> Result<Self, ProtocolError> {
97        if s.len() != 10 {
98            return Err(ProtocolError::FieldParse {
99                command: "FQ".to_owned(),
100                field: "frequency".to_owned(),
101                detail: format!("expected 10-digit string, got {} chars", s.len()),
102            });
103        }
104        let hz: u32 = s.parse().map_err(|_| ProtocolError::FieldParse {
105            command: "FQ".to_owned(),
106            field: "frequency".to_owned(),
107            detail: format!("non-numeric frequency string: {s:?}"),
108        })?;
109        Ok(Self(hz))
110    }
111
112    /// Returns the frequency as a 4-byte little-endian array.
113    #[must_use]
114    pub const fn to_le_bytes(self) -> [u8; 4] {
115        self.0.to_le_bytes()
116    }
117
118    /// Creates a `Frequency` from a 4-byte little-endian array.
119    #[must_use]
120    pub const fn from_le_bytes(bytes: [u8; 4]) -> Self {
121        Self(u32::from_le_bytes(bytes))
122    }
123}
124
125impl fmt::Display for Frequency {
126    /// Formats the frequency in MHz with three decimal places.
127    ///
128    /// Example: `Frequency::new(145_190_000)` displays as `"145.190 MHz"`.
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        let mhz_whole = self.0 / 1_000_000;
131        let mhz_frac = (self.0 % 1_000_000) / 1_000;
132        write!(f, "{mhz_whole}.{mhz_frac:03} MHz")
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn frequency_construction() {
142        let f = Frequency::new(145_000_000);
143        assert_eq!(f.as_hz(), 145_000_000);
144    }
145
146    #[test]
147    fn frequency_display_mhz() {
148        let f = Frequency::new(145_000_000);
149        assert!((f.as_mhz() - 145.0).abs() < f64::EPSILON);
150    }
151
152    #[test]
153    fn frequency_display_khz() {
154        let f = Frequency::new(145_500_000);
155        assert!((f.as_khz() - 145_500.0).abs() < f64::EPSILON);
156    }
157
158    #[test]
159    fn frequency_wire_format() {
160        let f = Frequency::new(145_000_000);
161        assert_eq!(f.to_wire_string(), "0145000000");
162    }
163
164    #[test]
165    fn frequency_from_wire() {
166        let f = Frequency::from_wire_string("0145000000").unwrap();
167        assert_eq!(f.as_hz(), 145_000_000);
168    }
169
170    #[test]
171    fn frequency_from_wire_invalid() {
172        assert!(Frequency::from_wire_string("not_a_number").is_err());
173        assert!(Frequency::from_wire_string("12345").is_err()); // wrong length
174    }
175
176    #[test]
177    fn frequency_display_formatted() {
178        assert_eq!(Frequency::new(145_190_000).to_string(), "145.190 MHz");
179        assert_eq!(Frequency::new(445_000_000).to_string(), "445.000 MHz");
180        assert_eq!(Frequency::new(50_125_000).to_string(), "50.125 MHz");
181        assert_eq!(Frequency::new(0).to_string(), "0.000 MHz");
182    }
183
184    #[test]
185    fn frequency_from_bytes_le() {
186        let bytes = 145_000_000u32.to_le_bytes();
187        let f = Frequency::from_le_bytes(bytes);
188        assert_eq!(f.as_hz(), 145_000_000);
189    }
190
191    #[test]
192    fn frequency_to_bytes_le() {
193        let f = Frequency::new(145_000_000);
194        assert_eq!(f.to_le_bytes(), 145_000_000u32.to_le_bytes());
195    }
196}