kenwood_thd75/types/
dtmf.rs

1//! DTMF (Dual-Tone Multi-Frequency) configuration and memory types.
2//!
3//! DTMF is the tone signaling system used by touch-tone telephones and
4//! amateur radio for dialing, auto-patching, and remote control. The
5//! TH-D75 supports 10 DTMF memory channels (Menu No. 163) for storing
6//! digit sequences, plus 10 dedicated `EchoLink` memory channels (Menu
7//! No. 164), configurable encode speed, pause time, and TX hold behavior.
8//!
9//! Per User Manual Chapter 11:
10//!
11//! - **Manual dialing**: press `[PTT]` then press keypad keys to send
12//!   DTMF tones in real time.
13//! - **Automatic dialer**: store up to 16 digits per channel with an
14//!   optional name (up to 16 characters). Transmit by pressing `[PTT]`,
15//!   then `[ENT]`, selecting a channel, then `[ENT]` again.
16//! - **DTMF Hold** (Menu No. 162): when enabled, the transmitter stays
17//!   keyed for 2 seconds after each keypress without holding `[PTT]`.
18//! - **DTMF Key Lock** (Menu No. 961): locks DTMF keys to prevent
19//!   accidental transmission while PTT is held.
20//! - **Encode speed** (Menu No. 160): 50 / 100 / 150 ms per digit.
21//!   Some repeaters may not respond correctly at fast speed.
22//! - **Pause time** (Menu No. 161): 100-2000 ms between digit groups.
23//!
24//! These types model DTMF settings from the TH-D75's menu system
25//! (Chapter 11 of the user manual). Derived from the capability gap
26//! analysis features 128-132.
27
28// ---------------------------------------------------------------------------
29// DTMF memory slot
30// ---------------------------------------------------------------------------
31
32/// A DTMF memory slot.
33///
34/// The TH-D75 provides 16 DTMF memory slots (0-15), each storing a
35/// name and a sequence of DTMF digits for the auto dialer function.
36/// Valid DTMF digits are `0`-`9`, `A`-`D`, `*`, and `#`.
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct DtmfMemory {
39    /// Slot index (0-15).
40    pub slot: DtmfSlot,
41    /// Memory name (up to 8 characters).
42    pub name: DtmfName,
43    /// DTMF digit sequence.
44    pub digits: DtmfDigits,
45}
46
47/// DTMF memory slot index (0-15).
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct DtmfSlot(u8);
50
51impl DtmfSlot {
52    /// Maximum slot index.
53    pub const MAX: u8 = 15;
54
55    /// Total number of DTMF memory slots.
56    pub const COUNT: usize = 16;
57
58    /// Creates a new DTMF memory slot index.
59    ///
60    /// # Errors
61    ///
62    /// Returns `None` if the index exceeds 15.
63    #[must_use]
64    pub const fn new(index: u8) -> Option<Self> {
65        if index <= Self::MAX {
66            Some(Self(index))
67        } else {
68            None
69        }
70    }
71
72    /// Returns the slot index.
73    #[must_use]
74    pub const fn index(self) -> u8 {
75        self.0
76    }
77}
78
79/// DTMF memory name (up to 8 characters).
80#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
81pub struct DtmfName(String);
82
83impl DtmfName {
84    /// Maximum length of a DTMF memory name.
85    pub const MAX_LEN: usize = 8;
86
87    /// Creates a new DTMF memory name.
88    ///
89    /// # Errors
90    ///
91    /// Returns `None` if the text exceeds 8 characters.
92    #[must_use]
93    pub fn new(text: &str) -> Option<Self> {
94        if text.len() <= Self::MAX_LEN {
95            Some(Self(text.to_owned()))
96        } else {
97            None
98        }
99    }
100
101    /// Returns the name as a string slice.
102    #[must_use]
103    pub fn as_str(&self) -> &str {
104        &self.0
105    }
106}
107
108/// DTMF digit sequence (valid characters: `0`-`9`, `A`-`D`, `*`, `#`).
109///
110/// The maximum length of a DTMF digit sequence on the TH-D75 is 16
111/// characters.
112#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
113pub struct DtmfDigits(String);
114
115impl DtmfDigits {
116    /// Maximum length of a DTMF digit sequence.
117    pub const MAX_LEN: usize = 16;
118
119    /// Creates a new DTMF digit sequence after validating all characters.
120    ///
121    /// # Errors
122    ///
123    /// Returns `None` if the sequence exceeds 16 characters or contains
124    /// invalid DTMF digits.
125    #[must_use]
126    pub fn new(digits: &str) -> Option<Self> {
127        if digits.len() <= Self::MAX_LEN && digits.chars().all(is_valid_dtmf) {
128            Some(Self(digits.to_owned()))
129        } else {
130            None
131        }
132    }
133
134    /// Returns the digit sequence as a string slice.
135    #[must_use]
136    pub fn as_str(&self) -> &str {
137        &self.0
138    }
139
140    /// Returns the number of digits in the sequence.
141    #[must_use]
142    pub const fn len(&self) -> usize {
143        self.0.len()
144    }
145
146    /// Returns `true` if the digit sequence is empty.
147    #[must_use]
148    pub const fn is_empty(&self) -> bool {
149        self.0.is_empty()
150    }
151}
152
153// ---------------------------------------------------------------------------
154// DTMF configuration
155// ---------------------------------------------------------------------------
156
157/// DTMF encoder and dialer configuration.
158///
159/// Controls the speed at which DTMF tones are generated, the pause
160/// duration between digit groups, TX hold behavior, and whether DTMF
161/// can be transmitted on a busy channel.
162#[derive(Debug, Clone, PartialEq, Eq, Hash)]
163pub struct DtmfConfig {
164    /// DTMF tone encode speed.
165    pub encode_speed: DtmfSpeed,
166    /// Pause time between DTMF digit groups.
167    pub pause_time: DtmfPause,
168    /// TX hold -- keep transmitter keyed between DTMF digit groups.
169    pub tx_hold: bool,
170    /// Allow DTMF transmission on a busy (occupied) channel.
171    pub tx_on_busy: bool,
172}
173
174impl Default for DtmfConfig {
175    fn default() -> Self {
176        Self {
177            encode_speed: DtmfSpeed::Slow,
178            pause_time: DtmfPause::Ms500,
179            tx_hold: false,
180            tx_on_busy: false,
181        }
182    }
183}
184
185/// DTMF tone encode speed (Menu No. 160).
186///
187/// Controls how long each DTMF tone is transmitted. Per User Manual
188/// Chapter 11: some repeaters may not respond correctly at fast speed.
189/// The user manual lists 50, 100, and 150 ms options. Default: 100 ms.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
191pub enum DtmfSpeed {
192    /// Slow encode speed (100 ms per digit).
193    Slow,
194    /// Fast encode speed (50 ms per digit).
195    Fast,
196}
197
198/// DTMF pause time between digit groups.
199///
200/// When a DTMF sequence contains a pause marker, the transmitter
201/// waits for the configured duration before continuing.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
203pub enum DtmfPause {
204    /// 100 ms pause.
205    Ms100,
206    /// 250 ms pause.
207    Ms250,
208    /// 500 ms pause.
209    Ms500,
210    /// 750 ms pause.
211    Ms750,
212    /// 1000 ms pause.
213    Ms1000,
214    /// 1500 ms pause.
215    Ms1500,
216    /// 2000 ms pause.
217    Ms2000,
218}
219
220// ---------------------------------------------------------------------------
221// Validation helper
222// ---------------------------------------------------------------------------
223
224/// Returns `true` if the character is a valid DTMF digit.
225///
226/// Valid DTMF digits are: `0`-`9`, `A`-`D`, `*`, and `#`.
227#[must_use]
228pub const fn is_valid_dtmf(c: char) -> bool {
229    matches!(c, '0'..='9' | 'A'..='D' | '*' | '#')
230}
231
232// ---------------------------------------------------------------------------
233// Tests
234// ---------------------------------------------------------------------------
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn dtmf_slot_valid_range() {
242        for i in 0u8..=15 {
243            assert!(DtmfSlot::new(i).is_some());
244        }
245    }
246
247    #[test]
248    fn dtmf_slot_invalid() {
249        assert!(DtmfSlot::new(16).is_none());
250    }
251
252    #[test]
253    fn dtmf_name_valid() {
254        let name = DtmfName::new("AUTOPAT").unwrap();
255        assert_eq!(name.as_str(), "AUTOPAT");
256    }
257
258    #[test]
259    fn dtmf_name_max_length() {
260        let name = DtmfName::new("12345678").unwrap();
261        assert_eq!(name.as_str().len(), 8);
262    }
263
264    #[test]
265    fn dtmf_name_too_long() {
266        assert!(DtmfName::new("123456789").is_none());
267    }
268
269    #[test]
270    fn dtmf_digits_valid() {
271        let digits = DtmfDigits::new("123A*#BD").unwrap();
272        assert_eq!(digits.as_str(), "123A*#BD");
273        assert_eq!(digits.len(), 8);
274        assert!(!digits.is_empty());
275    }
276
277    #[test]
278    fn dtmf_digits_empty() {
279        let digits = DtmfDigits::new("").unwrap();
280        assert!(digits.is_empty());
281    }
282
283    #[test]
284    fn dtmf_digits_all_valid_chars() {
285        assert!(DtmfDigits::new("0123456789ABCD*#").is_some());
286    }
287
288    #[test]
289    fn dtmf_digits_invalid_char() {
290        assert!(DtmfDigits::new("123E").is_none());
291    }
292
293    #[test]
294    fn dtmf_digits_lowercase_rejected() {
295        assert!(DtmfDigits::new("123a").is_none());
296    }
297
298    #[test]
299    fn dtmf_digits_too_long() {
300        assert!(DtmfDigits::new("01234567890123456").is_none());
301    }
302
303    #[test]
304    fn dtmf_config_default() {
305        let cfg = DtmfConfig::default();
306        assert_eq!(cfg.encode_speed, DtmfSpeed::Slow);
307        assert_eq!(cfg.pause_time, DtmfPause::Ms500);
308        assert!(!cfg.tx_hold);
309        assert!(!cfg.tx_on_busy);
310    }
311
312    #[test]
313    fn is_valid_dtmf_chars() {
314        for c in '0'..='9' {
315            assert!(is_valid_dtmf(c));
316        }
317        for c in 'A'..='D' {
318            assert!(is_valid_dtmf(c));
319        }
320        assert!(is_valid_dtmf('*'));
321        assert!(is_valid_dtmf('#'));
322    }
323
324    #[test]
325    fn is_invalid_dtmf_chars() {
326        assert!(!is_valid_dtmf('E'));
327        assert!(!is_valid_dtmf('a'));
328        assert!(!is_valid_dtmf(' '));
329        assert!(!is_valid_dtmf('@'));
330    }
331}