1use crate::error::ValidationError;
21
22pub const CTCSS_FREQUENCIES: [f64; 51] = [
41 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, 250.3, 254.1, 1750.0, ];
48
49pub const DCS_CODES: [u16; 104] = [
57 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131,
58 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, 205, 212, 223, 225, 226, 243, 244, 245,
59 246, 251, 252, 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, 331, 332, 343, 346, 351,
60 356, 364, 365, 371, 411, 412, 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, 465, 466,
61 503, 506, 516, 523, 526, 532, 546, 565, 606, 612, 624, 627, 631, 632, 654, 662, 664, 703, 712,
62 723, 731, 732, 734, 743, 754,
63];
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
74pub struct ToneCode(u8);
75
76impl ToneCode {
77 pub const MAX_INDEX: u8 = 50;
79
80 pub const fn new(index: u8) -> Result<Self, ValidationError> {
86 if index <= 50 {
87 Ok(Self(index))
88 } else {
89 Err(ValidationError::ToneCodeOutOfRange(index))
90 }
91 }
92
93 #[must_use]
95 pub const fn index(self) -> u8 {
96 self.0
97 }
98
99 #[must_use]
101 pub const fn frequency_hz(self) -> f64 {
102 CTCSS_FREQUENCIES[self.0 as usize]
103 }
104}
105
106impl std::fmt::Display for ToneCode {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(f, "{} ({} Hz)", self.0, CTCSS_FREQUENCIES[self.0 as usize])
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
117pub struct DcsCode(u8);
118
119impl DcsCode {
120 pub const COUNT: u8 = 104;
122
123 pub const MAX_INDEX: u8 = 103;
125
126 pub const fn new(index: u8) -> Result<Self, ValidationError> {
132 if index < 104 {
133 Ok(Self(index))
134 } else {
135 Err(ValidationError::DcsCodeInvalid(index))
136 }
137 }
138
139 #[must_use]
141 pub const fn index(self) -> u8 {
142 self.0
143 }
144
145 #[must_use]
147 pub const fn code_value(self) -> u16 {
148 DCS_CODES[self.0 as usize]
149 }
150}
151
152impl std::fmt::Display for DcsCode {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(f, "D{:03}", DCS_CODES[self.0 as usize])
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169pub enum ToneMode {
170 Off = 0,
172 Ctcss = 1,
174 Dcs = 2,
176 CrossTone = 3,
179}
180
181impl ToneMode {
182 pub const COUNT: u8 = 4;
184}
185
186impl std::fmt::Display for ToneMode {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self {
189 Self::Off => f.write_str("Off"),
190 Self::Ctcss => f.write_str("CTCSS"),
191 Self::Dcs => f.write_str("DCS"),
192 Self::CrossTone => f.write_str("Cross Tone"),
193 }
194 }
195}
196
197impl TryFrom<u8> for ToneMode {
198 type Error = ValidationError;
199
200 fn try_from(value: u8) -> Result<Self, Self::Error> {
201 match value {
202 0 => Ok(Self::Off),
203 1 => Ok(Self::Ctcss),
204 2 => Ok(Self::Dcs),
205 3 => Ok(Self::CrossTone),
206 _ => Err(ValidationError::ToneModeOutOfRange(value)),
207 }
208 }
209}
210
211impl From<ToneMode> for u8 {
212 fn from(mode: ToneMode) -> Self {
213 mode as Self
214 }
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
223pub enum CtcssMode {
224 Off = 0,
226 On = 1,
228 EncodeOnly = 2,
230}
231
232impl CtcssMode {
233 pub const COUNT: u8 = 3;
235}
236
237impl TryFrom<u8> for CtcssMode {
238 type Error = ValidationError;
239
240 fn try_from(value: u8) -> Result<Self, Self::Error> {
241 match value {
242 0 => Ok(Self::Off),
243 1 => Ok(Self::On),
244 2 => Ok(Self::EncodeOnly),
245 _ => Err(ValidationError::ToneModeOutOfRange(value)),
246 }
247 }
248}
249
250impl From<CtcssMode> for u8 {
251 fn from(mode: CtcssMode) -> Self {
252 mode as Self
253 }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
260pub enum DataSpeed {
261 Bps1200 = 0,
263 Bps9600 = 1,
265}
266
267impl DataSpeed {
268 pub const COUNT: u8 = 2;
270}
271
272impl std::fmt::Display for DataSpeed {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 match self {
275 Self::Bps1200 => f.write_str("1200 bps"),
276 Self::Bps9600 => f.write_str("9600 bps"),
277 }
278 }
279}
280
281impl TryFrom<u8> for DataSpeed {
282 type Error = ValidationError;
283
284 fn try_from(value: u8) -> Result<Self, Self::Error> {
285 match value {
286 0 => Ok(Self::Bps1200),
287 1 => Ok(Self::Bps9600),
288 _ => Err(ValidationError::DataSpeedOutOfRange(value)),
289 }
290 }
291}
292
293impl From<DataSpeed> for u8 {
294 fn from(speed: DataSpeed) -> Self {
295 speed as Self
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
309pub enum LockoutMode {
310 Off = 0,
312 On = 1,
314 Group = 2,
316}
317
318impl LockoutMode {
319 pub const COUNT: u8 = 3;
321}
322
323impl std::fmt::Display for LockoutMode {
324 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325 match self {
326 Self::Off => f.write_str("Off"),
327 Self::On => f.write_str("Locked Out"),
328 Self::Group => f.write_str("Group Lockout"),
329 }
330 }
331}
332
333impl TryFrom<u8> for LockoutMode {
334 type Error = ValidationError;
335
336 fn try_from(value: u8) -> Result<Self, Self::Error> {
337 match value {
338 0 => Ok(Self::Off),
339 1 => Ok(Self::On),
340 2 => Ok(Self::Group),
341 _ => Err(ValidationError::LockoutOutOfRange(value)),
342 }
343 }
344}
345
346impl From<LockoutMode> for u8 {
347 fn from(mode: LockoutMode) -> Self {
348 mode as Self
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn tone_code_valid_range() {
358 for i in 0u8..=ToneCode::MAX_INDEX {
359 let val = ToneCode::new(i).unwrap();
360 assert_eq!(val.index(), i, "ToneCode round-trip failed at {i}");
361 }
362 }
363
364 #[test]
365 fn tone_code_invalid() {
366 assert!(ToneCode::new(ToneCode::MAX_INDEX + 1).is_err());
367 assert!(ToneCode::new(255).is_err());
368 }
369
370 #[test]
371 fn tone_code_frequency_lookup() {
372 let tc = ToneCode::new(0).unwrap();
373 assert!((tc.frequency_hz() - 67.0).abs() < f64::EPSILON);
374 let tc = ToneCode::new(42).unwrap();
375 assert!((tc.frequency_hz() - 210.7).abs() < f64::EPSILON);
376 let tc = ToneCode::new(49).unwrap();
377 assert!((tc.frequency_hz() - 254.1).abs() < f64::EPSILON);
378 let tc = ToneCode::new(50).unwrap();
380 assert!((tc.frequency_hz() - 1750.0).abs() < f64::EPSILON);
381 }
382
383 #[test]
384 fn ctcss_table_completeness() {
385 assert_eq!(CTCSS_FREQUENCIES.len(), 51);
386 assert!((CTCSS_FREQUENCIES[0] - 67.0).abs() < f64::EPSILON);
387 assert!((CTCSS_FREQUENCIES[42] - 210.7).abs() < f64::EPSILON);
388 assert!((CTCSS_FREQUENCIES[43] - 218.1).abs() < f64::EPSILON);
389 assert!((CTCSS_FREQUENCIES[49] - 254.1).abs() < f64::EPSILON);
390 assert!((CTCSS_FREQUENCIES[50] - 1750.0).abs() < f64::EPSILON);
391 }
392
393 #[test]
394 fn dcs_code_valid() {
395 assert!(DcsCode::new(0).is_ok());
396 assert!(DcsCode::new(DcsCode::MAX_INDEX).is_ok());
397 }
398
399 #[test]
400 fn dcs_code_invalid() {
401 assert!(DcsCode::new(DcsCode::COUNT).is_err());
402 assert!(DcsCode::new(255).is_err());
403 }
404
405 #[test]
406 fn dcs_code_table_completeness() {
407 assert_eq!(DCS_CODES.len(), 104);
408 assert_eq!(DCS_CODES[0], 23);
409 assert_eq!(DCS_CODES[103], 754);
410 }
411
412 #[test]
413 fn dcs_code_value_lookup() {
414 let dc = DcsCode::new(0).unwrap();
415 assert_eq!(dc.code_value(), 23);
416 }
417
418 #[test]
419 fn tone_mode_valid_range() {
420 for i in 0u8..ToneMode::COUNT {
421 let val = ToneMode::try_from(i).unwrap();
422 assert_eq!(u8::from(val), i, "ToneMode round-trip failed at {i}");
423 }
424 }
425
426 #[test]
427 fn tone_mode_invalid() {
428 assert!(ToneMode::try_from(ToneMode::COUNT).is_err());
429 }
430
431 #[test]
432 fn data_speed_valid() {
433 for i in 0u8..DataSpeed::COUNT {
434 let val = DataSpeed::try_from(i).unwrap();
435 assert_eq!(u8::from(val), i, "DataSpeed round-trip failed at {i}");
436 }
437 assert!(DataSpeed::try_from(DataSpeed::COUNT).is_err());
438 }
439
440 #[test]
441 fn lockout_mode_valid() {
442 for i in 0u8..LockoutMode::COUNT {
443 let val = LockoutMode::try_from(i).unwrap();
444 assert_eq!(u8::from(val), i, "LockoutMode round-trip failed at {i}");
445 }
446 assert!(LockoutMode::try_from(LockoutMode::COUNT).is_err());
447 }
448
449 #[test]
450 fn ctcss_mode_valid() {
451 for i in 0u8..CtcssMode::COUNT {
452 let val = CtcssMode::try_from(i).unwrap();
453 assert_eq!(u8::from(val), i, "CtcssMode round-trip failed at {i}");
454 }
455 assert!(CtcssMode::try_from(CtcssMode::COUNT).is_err());
456 }
457}