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}