kenwood_thd75/types/
tone.rs

1//! Tone, DCS (Digital-Coded Squelch), and related signaling types for the
2//! TH-D75 transceiver.
3//!
4//! Contains CTCSS (Continuous Tone-Coded Squelch System) frequency and DCS
5//! code lookup tables, along with validated newtype wrappers and signaling
6//! mode enums.
7//!
8//! Per User Manual Chapter 10:
9//!
10//! - CTCSS, Tone, and DCS cannot be active simultaneously on a channel.
11//! - Pressing `[TONE]` cycles: Tone -> CTCSS (CT) -> DCS -> Cross Tone -> Off.
12//!   When APRS Voice Alert is configured, Voice Alert ON is added to the cycle.
13//! - CTCSS/DCS settings can be applied independently per VFO, Memory Channel,
14//!   and Call mode. Changes in Memory/Call mode are temporary unless stored.
15//! - Both CTCSS and DCS support frequency/code scanning (`[F]` + hold `[TONE]`)
16//!   to identify an incoming signal's tone or code.
17//!
18//! See User Manual Chapters 7 and 10 for full CTCSS/DCS/Cross Tone details.
19
20use crate::error::ValidationError;
21
22/// CTCSS (Continuous Tone-Coded Squelch System) frequency table.
23///
24/// 51 entries: 50 sub-audible CTCSS tone frequencies (indices 0-49) plus
25/// the 1750 Hz tone burst at index 50. Indexed by [`ToneCode`]. The CTCSS
26/// table is at firmware address `0xC003C694`.
27///
28/// The D75 supports indices 0-49 (50 CTCSS tones), extending the D74's
29/// 35-tone table with 15 additional tones including interleaved entries
30/// in the 159-200 Hz range (159.8, 165.5, 171.3, 177.3, 183.5, 189.9,
31/// 196.6, 199.5) and high-frequency tones (210.7-254.1 Hz).
32///
33/// Index 50 (1750.0 Hz) is the European repeater access tone burst,
34/// confirmed by ARFC-D75 decompilation. It is NOT a CTCSS tone — it is
35/// a short audio-frequency burst used to open European repeaters.
36///
37/// This table corresponds to **KI4LAX TABLE A** in the CAT command
38/// reference, which maps hex indices 0x00-0x31 to CTCSS tone frequencies.
39/// Index 0x32 (50) for the 1750 Hz tone burst is from ARFC-D75 RE.
40pub const CTCSS_FREQUENCIES: [f64; 51] = [
41    67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, // 0-9
42    94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, // 10-19
43    131.8, 136.5, 141.3, 146.2, 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, // 20-29
44    171.3, 173.8, 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, // 30-39
45    203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, 250.3, 254.1,  // 40-49
46    1750.0, // 50: 1750 Hz tone burst (European repeater access, NOT a CTCSS tone)
47];
48
49/// DCS (Digital-Coded Squelch) code table.
50///
51/// 104 digital squelch codes used for selective calling. Indexed by
52/// [`DcsCode`]. Table is at firmware address `0xC0086FC4`.
53///
54/// This table corresponds to **KI4LAX TABLE B** in the CAT command
55/// reference, which maps hex indices 0x00-0x67 to DCS code numbers.
56pub const DCS_CODES: [u16; 104] = [
57    23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131,
58    132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, 205, 212, 223, 225, 226, 243, 244, 245,
59    246, 251, 252, 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, 331, 332, 343, 346, 351,
60    356, 364, 365, 371, 411, 412, 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, 465, 466,
61    503, 506, 516, 523, 526, 532, 546, 565, 606, 612, 624, 627, 631, 632, 654, 662, 664, 703, 712,
62    723, 731, 732, 734, 743, 754,
63];
64
65/// Validated tone code (index into [`CTCSS_FREQUENCIES`]).
66///
67/// Wraps a `u8` index in the range 0..=50. Indices 0-49 are standard
68/// CTCSS sub-audible tones. Index 50 is the 1750 Hz tone burst used for
69/// European repeater access — it is NOT a CTCSS tone but a short
70/// audio-frequency burst. Confirmed by ARFC-D75 decompilation.
71///
72/// Use [`ToneCode::frequency_hz`] to look up the corresponding frequency.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
74pub struct ToneCode(u8);
75
76impl ToneCode {
77    /// Maximum valid tone code index (inclusive).
78    pub const MAX_INDEX: u8 = 50;
79
80    /// Creates a new `ToneCode` from a raw index.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`ValidationError::ToneCodeOutOfRange`] if `index > 50`.
85    pub const fn new(index: u8) -> Result<Self, ValidationError> {
86        if index <= 50 {
87            Ok(Self(index))
88        } else {
89            Err(ValidationError::ToneCodeOutOfRange(index))
90        }
91    }
92
93    /// Returns the raw index into the CTCSS frequency table.
94    #[must_use]
95    pub const fn index(self) -> u8 {
96        self.0
97    }
98
99    /// Returns the CTCSS frequency in Hz for this tone code.
100    #[must_use]
101    pub const fn frequency_hz(self) -> f64 {
102        CTCSS_FREQUENCIES[self.0 as usize]
103    }
104}
105
106impl std::fmt::Display for ToneCode {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "{} ({} Hz)", self.0, CTCSS_FREQUENCIES[self.0 as usize])
109    }
110}
111
112/// Validated DCS code (index into [`DCS_CODES`]).
113///
114/// Wraps a `u8` index in the range 0..=103. Use [`DcsCode::code_value`]
115/// to look up the corresponding DCS code number.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
117pub struct DcsCode(u8);
118
119impl DcsCode {
120    /// Number of valid DCS code indices (0-103).
121    pub const COUNT: u8 = 104;
122
123    /// Maximum valid DCS code index (inclusive).
124    pub const MAX_INDEX: u8 = 103;
125
126    /// Creates a new `DcsCode` from a raw index.
127    ///
128    /// # Errors
129    ///
130    /// Returns [`ValidationError::DcsCodeInvalid`] if `index >= 104`.
131    pub const fn new(index: u8) -> Result<Self, ValidationError> {
132        if index < 104 {
133            Ok(Self(index))
134        } else {
135            Err(ValidationError::DcsCodeInvalid(index))
136        }
137    }
138
139    /// Returns the raw index into the DCS code table.
140    #[must_use]
141    pub const fn index(self) -> u8 {
142        self.0
143    }
144
145    /// Returns the DCS code value for this index.
146    #[must_use]
147    pub const fn code_value(self) -> u16 {
148        DCS_CODES[self.0 as usize]
149    }
150}
151
152impl std::fmt::Display for DcsCode {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        write!(f, "D{:03}", DCS_CODES[self.0 as usize])
155    }
156}
157
158/// Tone signaling mode for a channel.
159///
160/// Maps to the tone-mode field in the `FO` and `ME` commands.
161/// Corresponds to **KI4LAX TABLE F** in the CAT command reference
162/// (index 0 = Off, 1 = CTCSS, 2 = DCS).
163///
164/// Per User Manual Chapter 10: CTCSS does not make conversations
165/// private -- it only relieves you from hearing unwanted conversations.
166/// When CTCSS or DCS is active during scan, scan stops on any signal
167/// but immediately resumes if the signal lacks the matching tone/code.
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169pub enum ToneMode {
170    /// No tone signaling (index 0).
171    Off = 0,
172    /// CTCSS tone (index 1).
173    Ctcss = 1,
174    /// DCS code (index 2).
175    Dcs = 2,
176    /// Cross-tone mode (index 3). Separate encode/decode signaling types.
177    /// Confirmed by ARFC-D75 decompilation (`a1` enum, 4 values).
178    CrossTone = 3,
179}
180
181impl ToneMode {
182    /// Number of valid tone mode values (0-3).
183    pub const COUNT: u8 = 4;
184}
185
186impl std::fmt::Display for ToneMode {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        match self {
189            Self::Off => f.write_str("Off"),
190            Self::Ctcss => f.write_str("CTCSS"),
191            Self::Dcs => f.write_str("DCS"),
192            Self::CrossTone => f.write_str("Cross Tone"),
193        }
194    }
195}
196
197impl TryFrom<u8> for ToneMode {
198    type Error = ValidationError;
199
200    fn try_from(value: u8) -> Result<Self, Self::Error> {
201        match value {
202            0 => Ok(Self::Off),
203            1 => Ok(Self::Ctcss),
204            2 => Ok(Self::Dcs),
205            3 => Ok(Self::CrossTone),
206            _ => Err(ValidationError::ToneModeOutOfRange(value)),
207        }
208    }
209}
210
211impl From<ToneMode> for u8 {
212    fn from(mode: ToneMode) -> Self {
213        mode as Self
214    }
215}
216
217/// CTCSS encode/decode mode (byte 0x09 bits \[1:0\]).
218///
219/// Controls whether CTCSS tones are encoded on transmit, decoded on
220/// receive, or both. Uses [`ValidationError::ToneModeOutOfRange`] for
221/// out-of-range values since it shares the same valid range (0-2).
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
223pub enum CtcssMode {
224    /// CTCSS disabled (index 0).
225    Off = 0,
226    /// Encode and decode CTCSS (index 1).
227    On = 1,
228    /// Encode CTCSS on transmit only (index 2).
229    EncodeOnly = 2,
230}
231
232impl CtcssMode {
233    /// Number of valid CTCSS mode values (0-2).
234    pub const COUNT: u8 = 3;
235}
236
237impl TryFrom<u8> for CtcssMode {
238    type Error = ValidationError;
239
240    fn try_from(value: u8) -> Result<Self, Self::Error> {
241        match value {
242            0 => Ok(Self::Off),
243            1 => Ok(Self::On),
244            2 => Ok(Self::EncodeOnly),
245            _ => Err(ValidationError::ToneModeOutOfRange(value)),
246        }
247    }
248}
249
250impl From<CtcssMode> for u8 {
251    fn from(mode: CtcssMode) -> Self {
252        mode as Self
253    }
254}
255
256/// Data speed for packet/digital modes.
257///
258/// Maps to the data-speed field in the `FO` and `ME` commands.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
260pub enum DataSpeed {
261    /// 1200 bps (index 0).
262    Bps1200 = 0,
263    /// 9600 bps (index 1).
264    Bps9600 = 1,
265}
266
267impl DataSpeed {
268    /// Number of valid data speed values (0-1).
269    pub const COUNT: u8 = 2;
270}
271
272impl std::fmt::Display for DataSpeed {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        match self {
275            Self::Bps1200 => f.write_str("1200 bps"),
276            Self::Bps9600 => f.write_str("9600 bps"),
277        }
278    }
279}
280
281impl TryFrom<u8> for DataSpeed {
282    type Error = ValidationError;
283
284    fn try_from(value: u8) -> Result<Self, Self::Error> {
285        match value {
286            0 => Ok(Self::Bps1200),
287            1 => Ok(Self::Bps9600),
288            _ => Err(ValidationError::DataSpeedOutOfRange(value)),
289        }
290    }
291}
292
293impl From<DataSpeed> for u8 {
294    fn from(speed: DataSpeed) -> Self {
295        speed as Self
296    }
297}
298
299/// Channel lockout mode for scan operations.
300///
301/// Maps to the lockout field in the `ME` command.
302///
303/// Per User Manual Chapter 9: lockout can be set individually for all
304/// 1000 memory channels but cannot be set for program scan memory
305/// (L0/U0 through L49/U49). The lockout icon appears to the right of
306/// the channel number when a locked-out channel is recalled. Lockout
307/// cannot be toggled in VFO or CALL channel mode.
308#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
309pub enum LockoutMode {
310    /// Not locked out (index 0).
311    Off = 0,
312    /// Locked out of scan (index 1).
313    On = 1,
314    /// Group lockout (index 2).
315    Group = 2,
316}
317
318impl LockoutMode {
319    /// Number of valid lockout mode values (0-2).
320    pub const COUNT: u8 = 3;
321}
322
323impl std::fmt::Display for LockoutMode {
324    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325        match self {
326            Self::Off => f.write_str("Off"),
327            Self::On => f.write_str("Locked Out"),
328            Self::Group => f.write_str("Group Lockout"),
329        }
330    }
331}
332
333impl TryFrom<u8> for LockoutMode {
334    type Error = ValidationError;
335
336    fn try_from(value: u8) -> Result<Self, Self::Error> {
337        match value {
338            0 => Ok(Self::Off),
339            1 => Ok(Self::On),
340            2 => Ok(Self::Group),
341            _ => Err(ValidationError::LockoutOutOfRange(value)),
342        }
343    }
344}
345
346impl From<LockoutMode> for u8 {
347    fn from(mode: LockoutMode) -> Self {
348        mode as Self
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn tone_code_valid_range() {
358        for i in 0u8..=ToneCode::MAX_INDEX {
359            let val = ToneCode::new(i).unwrap();
360            assert_eq!(val.index(), i, "ToneCode round-trip failed at {i}");
361        }
362    }
363
364    #[test]
365    fn tone_code_invalid() {
366        assert!(ToneCode::new(ToneCode::MAX_INDEX + 1).is_err());
367        assert!(ToneCode::new(255).is_err());
368    }
369
370    #[test]
371    fn tone_code_frequency_lookup() {
372        let tc = ToneCode::new(0).unwrap();
373        assert!((tc.frequency_hz() - 67.0).abs() < f64::EPSILON);
374        let tc = ToneCode::new(42).unwrap();
375        assert!((tc.frequency_hz() - 210.7).abs() < f64::EPSILON);
376        let tc = ToneCode::new(49).unwrap();
377        assert!((tc.frequency_hz() - 254.1).abs() < f64::EPSILON);
378        // Code 50: 1750 Hz tone burst (European repeater access).
379        let tc = ToneCode::new(50).unwrap();
380        assert!((tc.frequency_hz() - 1750.0).abs() < f64::EPSILON);
381    }
382
383    #[test]
384    fn ctcss_table_completeness() {
385        assert_eq!(CTCSS_FREQUENCIES.len(), 51);
386        assert!((CTCSS_FREQUENCIES[0] - 67.0).abs() < f64::EPSILON);
387        assert!((CTCSS_FREQUENCIES[42] - 210.7).abs() < f64::EPSILON);
388        assert!((CTCSS_FREQUENCIES[43] - 218.1).abs() < f64::EPSILON);
389        assert!((CTCSS_FREQUENCIES[49] - 254.1).abs() < f64::EPSILON);
390        assert!((CTCSS_FREQUENCIES[50] - 1750.0).abs() < f64::EPSILON);
391    }
392
393    #[test]
394    fn dcs_code_valid() {
395        assert!(DcsCode::new(0).is_ok());
396        assert!(DcsCode::new(DcsCode::MAX_INDEX).is_ok());
397    }
398
399    #[test]
400    fn dcs_code_invalid() {
401        assert!(DcsCode::new(DcsCode::COUNT).is_err());
402        assert!(DcsCode::new(255).is_err());
403    }
404
405    #[test]
406    fn dcs_code_table_completeness() {
407        assert_eq!(DCS_CODES.len(), 104);
408        assert_eq!(DCS_CODES[0], 23);
409        assert_eq!(DCS_CODES[103], 754);
410    }
411
412    #[test]
413    fn dcs_code_value_lookup() {
414        let dc = DcsCode::new(0).unwrap();
415        assert_eq!(dc.code_value(), 23);
416    }
417
418    #[test]
419    fn tone_mode_valid_range() {
420        for i in 0u8..ToneMode::COUNT {
421            let val = ToneMode::try_from(i).unwrap();
422            assert_eq!(u8::from(val), i, "ToneMode round-trip failed at {i}");
423        }
424    }
425
426    #[test]
427    fn tone_mode_invalid() {
428        assert!(ToneMode::try_from(ToneMode::COUNT).is_err());
429    }
430
431    #[test]
432    fn data_speed_valid() {
433        for i in 0u8..DataSpeed::COUNT {
434            let val = DataSpeed::try_from(i).unwrap();
435            assert_eq!(u8::from(val), i, "DataSpeed round-trip failed at {i}");
436        }
437        assert!(DataSpeed::try_from(DataSpeed::COUNT).is_err());
438    }
439
440    #[test]
441    fn lockout_mode_valid() {
442        for i in 0u8..LockoutMode::COUNT {
443            let val = LockoutMode::try_from(i).unwrap();
444            assert_eq!(u8::from(val), i, "LockoutMode round-trip failed at {i}");
445        }
446        assert!(LockoutMode::try_from(LockoutMode::COUNT).is_err());
447    }
448
449    #[test]
450    fn ctcss_mode_valid() {
451        for i in 0u8..CtcssMode::COUNT {
452            let val = CtcssMode::try_from(i).unwrap();
453            assert_eq!(u8::from(val), i, "CtcssMode round-trip failed at {i}");
454        }
455        assert!(CtcssMode::try_from(CtcssMode::COUNT).is_err());
456    }
457}