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}