kenwood_thd75/types/wireless.rs
1//! Wireless remote control types (TH-D75A only).
2//!
3//! The TH-D75A supports wireless remote control of a Kenwood multi-band
4//! mobile transceiver via DTMF signaling. A "control" radio sends
5//! DTMF commands over air to a "target" radio, which decodes them and
6//! executes the corresponding function. Access is protected by a
7//! 3-digit secret access code (Menu No. 946, range 000-999).
8//!
9//! Per User Manual Chapter 25:
10//!
11//! - FCC rules permit sending control codes only on the 440 MHz band.
12//! - The target mobile transceiver must have both the secret number and
13//! Remote Control functions.
14//! - The DTMF format is `AXXX#YA#` where `XXX` is the 3-digit secret
15//! code and `Y` is a single-digit control command.
16//!
17//! # Remote control commands (per User Manual Chapter 25)
18//!
19//! | RM# | Name | Operation |
20//! |-----|------|-----------|
21//! | RM0 | LOW | Toggle TX power |
22//! | RM1 | On | DCS ON / Reverse ON / Tone Alert ON |
23//! | RM2 | TONE On | Tone ON |
24//! | RM3 | CTCSS On | CTCSS ON |
25//! | RM4 | Off | DCS OFF / Reverse OFF / Tone Alert OFF |
26//! | RM5 | TONE Off | Tone OFF |
27//! | RM6 | CTCSS Off | CTCSS OFF |
28//! | RM7 | CALL | Call mode ON |
29//! | RM8 | VFO | VFO mode ON |
30//! | RM9 | MR | Memory mode ON |
31//! | RMA | Freq. Enter | Frequency or channel direct entry |
32//! | RMB | Tone Select | DCS code / Tone freq / CTCSS freq setup |
33//! | RMC | REPEATER On | Repeater ON |
34//! | RMD | REPEATER Off | Repeater OFF |
35//! | RM\* | DOWN | Step frequency/channel down |
36//! | RM# | UP | Step frequency/channel up |
37//!
38//! These types model wireless control settings from Chapter 25 of the
39//! TH-D75 user manual.
40
41// ---------------------------------------------------------------------------
42// Wireless control configuration
43// ---------------------------------------------------------------------------
44
45/// Wireless remote control configuration.
46///
47/// When enabled, the radio listens for incoming DTMF command sequences
48/// and executes them if the correct password prefix is received.
49/// The password is a 4-digit DTMF code (digits `0`-`9`, `A`-`D`,
50/// `*`, `#`).
51#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
52pub struct WirelessControlConfig {
53 /// Enable wireless remote control reception.
54 pub enabled: bool,
55 /// 4-digit DTMF password for wireless control access.
56 pub password: WirelessPassword,
57}
58
59/// Wireless control DTMF password.
60///
61/// The password must be exactly 4 valid DTMF characters (`0`-`9`,
62/// `A`-`D`, `*`, `#`).
63///
64/// Note: the User Manual Chapter 25 describes a 3-digit numeric secret
65/// access code (000-999, Menu No. 946) for the over-the-air protocol.
66/// This 4-character DTMF password is the MCP/firmware internal
67/// representation which may include extended DTMF characters.
68#[derive(Debug, Clone, PartialEq, Eq, Hash)]
69pub struct WirelessPassword(String);
70
71impl WirelessPassword {
72 /// Required password length (exactly 4 characters).
73 pub const LEN: usize = 4;
74
75 /// Creates a new wireless control password.
76 ///
77 /// # Errors
78 ///
79 /// Returns `None` if the password is not exactly 4 characters or
80 /// contains invalid DTMF digits.
81 #[must_use]
82 pub fn new(password: &str) -> Option<Self> {
83 if password.len() == Self::LEN && password.chars().all(super::dtmf::is_valid_dtmf) {
84 Some(Self(password.to_owned()))
85 } else {
86 None
87 }
88 }
89
90 /// Returns the password as a string slice.
91 #[must_use]
92 pub fn as_str(&self) -> &str {
93 &self.0
94 }
95}
96
97impl Default for WirelessPassword {
98 fn default() -> Self {
99 // Default password "0000" (all zeros).
100 Self("0000".to_owned())
101 }
102}
103
104// ---------------------------------------------------------------------------
105// Tests
106// ---------------------------------------------------------------------------
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn wireless_config_default() {
114 let cfg = WirelessControlConfig::default();
115 assert!(!cfg.enabled);
116 assert_eq!(cfg.password.as_str(), "0000");
117 }
118
119 #[test]
120 fn wireless_password_valid() {
121 let pwd = WirelessPassword::new("1234").unwrap();
122 assert_eq!(pwd.as_str(), "1234");
123 }
124
125 #[test]
126 fn wireless_password_dtmf_chars() {
127 let pwd = WirelessPassword::new("A*#B").unwrap();
128 assert_eq!(pwd.as_str(), "A*#B");
129 }
130
131 #[test]
132 fn wireless_password_too_short() {
133 assert!(WirelessPassword::new("123").is_none());
134 }
135
136 #[test]
137 fn wireless_password_too_long() {
138 assert!(WirelessPassword::new("12345").is_none());
139 }
140
141 #[test]
142 fn wireless_password_invalid_chars() {
143 assert!(WirelessPassword::new("12E4").is_none());
144 }
145
146 #[test]
147 fn wireless_password_lowercase_rejected() {
148 assert!(WirelessPassword::new("12a4").is_none());
149 }
150}