kenwood_thd75/types/
echolink.rs

1//! `EchoLink` memory types (Menu No. 164).
2//!
3//! `EchoLink` is a `VoIP` system that links amateur radio stations over the
4//! internet. The TH-D75 supports 10 `EchoLink` memory slots for storing
5//! frequently used node numbers and their associated station names for
6//! quick access via DTMF dialing.
7//!
8//! Per User Manual Chapter 11:
9//!
10//! - `EchoLink` memory channels are separate from normal DTMF memory.
11//! - They do NOT store operating frequencies, tones, or power information.
12//! - Each slot stores a callsign/name (up to 8 characters) and a node
13//!   number or DTMF code (up to 8 digits).
14//! - The radio supports `EchoLink` "Connect by Call" (prefix `C`) and
15//!   "Query by Call" (prefix `07`) functions with automatic callsign-to-DTMF
16//!   conversion.
17//! - When only a name is stored (no code), the "Connect Call" function
18//!   automatically converts the callsign to DTMF with `C` prefix and `#` suffix.
19//!
20//! These types model `EchoLink` settings from the TH-D75's menu system.
21//! Derived from the capability gap analysis feature 138.
22
23// ---------------------------------------------------------------------------
24// EchoLink memory slot
25// ---------------------------------------------------------------------------
26
27/// An `EchoLink` memory slot.
28///
29/// The TH-D75 provides 10 `EchoLink` memory slots (0-9), each storing
30/// a station name and node number. Node numbers are dialed via DTMF
31/// to connect to the remote `EchoLink` station through a repeater's
32/// `EchoLink` interface.
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34pub struct EchoLinkMemory {
35    /// Slot index (0-9).
36    pub slot: EchoLinkSlot,
37    /// Station name or callsign (up to 8 characters).
38    pub name: EchoLinkName,
39    /// `EchoLink` node number (up to 6 digits).
40    pub node_number: EchoLinkNode,
41}
42
43/// `EchoLink` memory slot index (0-9).
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub struct EchoLinkSlot(u8);
46
47impl EchoLinkSlot {
48    /// Maximum slot index.
49    pub const MAX: u8 = 9;
50
51    /// Total number of `EchoLink` memory slots.
52    pub const COUNT: usize = 10;
53
54    /// Creates a new `EchoLink` memory slot index.
55    ///
56    /// # Errors
57    ///
58    /// Returns `None` if the index exceeds 9.
59    #[must_use]
60    pub const fn new(index: u8) -> Option<Self> {
61        if index <= Self::MAX {
62            Some(Self(index))
63        } else {
64            None
65        }
66    }
67
68    /// Returns the slot index.
69    #[must_use]
70    pub const fn index(self) -> u8 {
71        self.0
72    }
73}
74
75/// `EchoLink` station name (up to 8 characters).
76#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
77pub struct EchoLinkName(String);
78
79impl EchoLinkName {
80    /// Maximum length of an `EchoLink` station name.
81    pub const MAX_LEN: usize = 8;
82
83    /// Creates a new `EchoLink` station name.
84    ///
85    /// # Errors
86    ///
87    /// Returns `None` if the text exceeds 8 characters.
88    #[must_use]
89    pub fn new(text: &str) -> Option<Self> {
90        if text.len() <= Self::MAX_LEN {
91            Some(Self(text.to_owned()))
92        } else {
93            None
94        }
95    }
96
97    /// Returns the name as a string slice.
98    #[must_use]
99    pub fn as_str(&self) -> &str {
100        &self.0
101    }
102}
103
104/// `EchoLink` node number (up to 6 digits).
105///
106/// `EchoLink` node numbers are numeric identifiers assigned to each
107/// registered station. They are transmitted via DTMF tones through
108/// a repeater to initiate an `EchoLink` connection.
109#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
110pub struct EchoLinkNode(String);
111
112impl EchoLinkNode {
113    /// Maximum length of an `EchoLink` node number.
114    pub const MAX_LEN: usize = 6;
115
116    /// Creates a new `EchoLink` node number.
117    ///
118    /// # Errors
119    ///
120    /// Returns `None` if the string exceeds 6 characters or contains
121    /// non-digit characters.
122    #[must_use]
123    pub fn new(number: &str) -> Option<Self> {
124        if number.len() <= Self::MAX_LEN && number.chars().all(|c| c.is_ascii_digit()) {
125            Some(Self(number.to_owned()))
126        } else {
127            None
128        }
129    }
130
131    /// Returns the node number as a string slice.
132    #[must_use]
133    pub fn as_str(&self) -> &str {
134        &self.0
135    }
136
137    /// Returns `true` if the node number is empty.
138    #[must_use]
139    pub const fn is_empty(&self) -> bool {
140        self.0.is_empty()
141    }
142}
143
144// ---------------------------------------------------------------------------
145// Tests
146// ---------------------------------------------------------------------------
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn echolink_slot_valid_range() {
154        for i in 0u8..=9 {
155            assert!(EchoLinkSlot::new(i).is_some());
156        }
157    }
158
159    #[test]
160    fn echolink_slot_invalid() {
161        assert!(EchoLinkSlot::new(10).is_none());
162    }
163
164    #[test]
165    fn echolink_slot_index() {
166        let slot = EchoLinkSlot::new(5).unwrap();
167        assert_eq!(slot.index(), 5);
168    }
169
170    #[test]
171    fn echolink_name_valid() {
172        let name = EchoLinkName::new("W1AW").unwrap();
173        assert_eq!(name.as_str(), "W1AW");
174    }
175
176    #[test]
177    fn echolink_name_max_length() {
178        let name = EchoLinkName::new("12345678").unwrap();
179        assert_eq!(name.as_str().len(), 8);
180    }
181
182    #[test]
183    fn echolink_name_too_long() {
184        assert!(EchoLinkName::new("123456789").is_none());
185    }
186
187    #[test]
188    fn echolink_node_valid() {
189        let node = EchoLinkNode::new("123456").unwrap();
190        assert_eq!(node.as_str(), "123456");
191        assert!(!node.is_empty());
192    }
193
194    #[test]
195    fn echolink_node_short() {
196        let node = EchoLinkNode::new("1").unwrap();
197        assert_eq!(node.as_str(), "1");
198    }
199
200    #[test]
201    fn echolink_node_empty() {
202        let node = EchoLinkNode::new("").unwrap();
203        assert!(node.is_empty());
204    }
205
206    #[test]
207    fn echolink_node_too_long() {
208        assert!(EchoLinkNode::new("1234567").is_none());
209    }
210
211    #[test]
212    fn echolink_node_non_digit() {
213        assert!(EchoLinkNode::new("12A456").is_none());
214    }
215
216    #[test]
217    fn echolink_node_special_chars() {
218        assert!(EchoLinkNode::new("12*456").is_none());
219    }
220}