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}