1use std::fmt;
60
61use crate::error::{ProtocolError, ValidationError};
62use crate::types::dstar::DstarCallsign;
63use crate::types::frequency::Frequency;
64use crate::types::mode::{MemoryMode, ShiftDirection, StepSize};
65use crate::types::tone::{CtcssMode, DcsCode, ToneCode};
66
67#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
80pub struct ChannelName(String);
81
82impl ChannelName {
83 pub fn new(name: &str) -> Result<Self, ValidationError> {
90 let len = name.len();
91 if len > 8 {
92 return Err(ValidationError::ChannelNameTooLong { len });
93 }
94 Ok(Self(name.to_owned()))
95 }
96
97 #[must_use]
99 pub fn as_str(&self) -> &str {
100 &self.0
101 }
102
103 #[must_use]
105 pub fn to_bytes(&self) -> [u8; 24] {
106 let mut buf = [0u8; 24];
107 let src = self.0.as_bytes();
108 buf[..src.len()].copy_from_slice(src);
109 buf
110 }
111
112 #[must_use]
117 pub fn from_bytes(bytes: &[u8; 24]) -> Self {
118 let end = bytes.iter().position(|&b| b == 0).unwrap_or(8).min(8);
119 let s = String::from_utf8_lossy(&bytes[..end]);
120 Self(s.into_owned())
121 }
122}
123
124#[allow(clippy::struct_excessive_bools)]
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct ChannelMemory {
138 pub rx_frequency: Frequency,
140 pub tx_offset: Frequency,
142 pub step_size: StepSize,
144 pub mode_flags_raw: u8,
157 pub shift: ShiftDirection,
163 pub reverse: bool,
165 pub tone_enable: bool,
167 pub ctcss_mode: CtcssMode,
169 pub dcs_enable: bool,
171 pub cross_tone_reverse: bool,
173 pub flags_0a_raw: u8,
187 pub tone_code: ToneCode,
189 pub ctcss_code: ToneCode,
191 pub dcs_code: DcsCode,
193 pub cross_tone_combo: CrossToneType,
198 pub digital_squelch: FlashDigitalSquelch,
204 pub urcall: ChannelName,
210 pub data_mode: u8,
212}
213
214impl ChannelMemory {
215 pub const BYTE_SIZE: usize = 40;
217
218 #[must_use]
220 pub fn to_bytes(&self) -> [u8; 40] {
221 let mut buf = [0u8; 40];
222
223 buf[0..4].copy_from_slice(&self.rx_frequency.to_le_bytes());
225
226 buf[4..8].copy_from_slice(&self.tx_offset.to_le_bytes());
228
229 buf[0x08] = (u8::from(self.step_size) << 4) | u8::from(self.shift);
231
232 buf[0x09] = self.mode_flags_raw;
234
235 buf[0x0A] = self.flags_0a_raw;
239
240 buf[0x0B] = self.tone_code.index();
242
243 buf[0x0C] = self.ctcss_code.index();
245
246 buf[0x0D] = self.dcs_code.index();
248
249 buf[0x0E] = ((u8::from(self.cross_tone_combo) & 0x03) << 4)
251 | 0x0C
252 | (u8::from(self.digital_squelch) & 0x03);
253
254 buf[0x0F..0x27].copy_from_slice(&self.urcall.to_bytes());
256
257 buf[0x27] = self.data_mode;
259
260 buf
261 }
262
263 #[allow(clippy::similar_names, clippy::too_many_lines)]
270 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ProtocolError> {
271 if bytes.len() < Self::BYTE_SIZE {
272 return Err(ProtocolError::FieldParse {
273 command: "channel".into(),
274 field: "length".into(),
275 detail: format!(
276 "expected at least {} bytes, got {}",
277 Self::BYTE_SIZE,
278 bytes.len()
279 ),
280 });
281 }
282
283 let rx_frequency = Frequency::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
284 let tx_offset = Frequency::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
285
286 let step_size =
287 StepSize::try_from(bytes[0x08] >> 4).map_err(|e| ProtocolError::FieldParse {
288 command: "channel".into(),
289 field: "step_size".into(),
290 detail: e.to_string(),
291 })?;
292
293 let shift = ShiftDirection::try_from(bytes[0x08] & 0x0F).map_err(|e| {
294 ProtocolError::FieldParse {
295 command: "channel".into(),
296 field: "shift".into(),
297 detail: e.to_string(),
298 }
299 })?;
300
301 let mode_flags_raw = bytes[0x09];
303
304 let flags_0a_raw = bytes[0x0A];
306 let tone_enable = (flags_0a_raw >> 7) & 1 != 0;
307 let ctcss_enable = (flags_0a_raw >> 6) & 1 != 0;
308 let dcs_enable = (flags_0a_raw >> 5) & 1 != 0;
309 let cross_tone_reverse = (flags_0a_raw >> 4) & 1 != 0;
310 let reverse = (flags_0a_raw >> 3) & 1 != 0;
311
312 let ctcss_mode = if ctcss_enable {
313 CtcssMode::try_from(1u8).map_err(|e| ProtocolError::FieldParse {
314 command: "channel".into(),
315 field: "ctcss_mode".into(),
316 detail: e.to_string(),
317 })?
318 } else {
319 CtcssMode::try_from(0u8).map_err(|e| ProtocolError::FieldParse {
320 command: "channel".into(),
321 field: "ctcss_mode".into(),
322 detail: e.to_string(),
323 })?
324 };
325
326 let tone_code = ToneCode::new(bytes[0x0B]).map_err(|e| ProtocolError::FieldParse {
327 command: "channel".into(),
328 field: "tone_code".into(),
329 detail: e.to_string(),
330 })?;
331
332 let ctcss_code = ToneCode::new(bytes[0x0C]).map_err(|e| ProtocolError::FieldParse {
333 command: "channel".into(),
334 field: "ctcss_code".into(),
335 detail: e.to_string(),
336 })?;
337
338 let dcs_code = DcsCode::new(bytes[0x0D]).map_err(|e| ProtocolError::FieldParse {
339 command: "channel".into(),
340 field: "dcs_code".into(),
341 detail: e.to_string(),
342 })?;
343
344 let cross_tone_combo = CrossToneType::try_from((bytes[0x0E] >> 4) & 0x03).map_err(|e| {
346 ProtocolError::FieldParse {
347 command: "channel".into(),
348 field: "cross_tone_combo".into(),
349 detail: e.to_string(),
350 }
351 })?;
352 let digital_squelch = FlashDigitalSquelch::try_from(bytes[0x0E] & 0x03).map_err(|e| {
353 ProtocolError::FieldParse {
354 command: "channel".into(),
355 field: "digital_squelch".into(),
356 detail: e.to_string(),
357 }
358 })?;
359
360 let mut urcall_arr = [0u8; 24];
361 urcall_arr.copy_from_slice(&bytes[0x0F..0x27]);
362 let urcall = ChannelName::from_bytes(&urcall_arr);
363
364 let data_mode = bytes[0x27];
365
366 Ok(Self {
367 rx_frequency,
368 tx_offset,
369 step_size,
370 mode_flags_raw,
371 shift,
372 reverse,
373 tone_enable,
374 ctcss_mode,
375 dcs_enable,
376 cross_tone_reverse,
377 flags_0a_raw,
378 tone_code,
379 ctcss_code,
380 dcs_code,
381 cross_tone_combo,
382 digital_squelch,
383 urcall,
384 data_mode,
385 })
386 }
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
398pub enum FlashDuplex {
399 Simplex = 0,
401 Plus = 1,
403 Minus = 2,
405}
406
407impl FlashDuplex {
408 pub const COUNT: u8 = 3;
410}
411
412impl TryFrom<u8> for FlashDuplex {
413 type Error = ValidationError;
414
415 fn try_from(value: u8) -> Result<Self, Self::Error> {
416 match value {
417 0 => Ok(Self::Simplex),
418 1 => Ok(Self::Plus),
419 2 => Ok(Self::Minus),
420 _ => Err(ValidationError::ShiftOutOfRange(value)),
421 }
422 }
423}
424
425impl From<FlashDuplex> for u8 {
426 fn from(d: FlashDuplex) -> Self {
427 d as Self
428 }
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
447pub enum CrossToneType {
448 DcsOff = 0,
450 ToneDcs = 1,
452 DcsCtcss = 2,
454 ToneCtcss = 3,
456}
457
458impl CrossToneType {
459 pub const COUNT: u8 = 4;
461}
462
463impl TryFrom<u8> for CrossToneType {
464 type Error = ValidationError;
465
466 fn try_from(value: u8) -> Result<Self, Self::Error> {
467 match value {
468 0 => Ok(Self::DcsOff),
469 1 => Ok(Self::ToneDcs),
470 2 => Ok(Self::DcsCtcss),
471 3 => Ok(Self::ToneCtcss),
472 _ => Err(ValidationError::CrossToneTypeOutOfRange(value)),
473 }
474 }
475}
476
477impl From<CrossToneType> for u8 {
478 fn from(ct: CrossToneType) -> Self {
479 ct as Self
480 }
481}
482
483#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
489pub enum FlashDigitalSquelch {
490 Off = 0,
492 Code = 1,
494 Callsign = 2,
496}
497
498impl FlashDigitalSquelch {
499 pub const COUNT: u8 = 3;
501}
502
503impl TryFrom<u8> for FlashDigitalSquelch {
504 type Error = ValidationError;
505
506 fn try_from(value: u8) -> Result<Self, Self::Error> {
507 match value {
508 0 => Ok(Self::Off),
509 1 => Ok(Self::Code),
510 2 => Ok(Self::Callsign),
511 _ => Err(ValidationError::FlashDigitalSquelchOutOfRange(value)),
512 }
513 }
514}
515
516impl From<FlashDigitalSquelch> for u8 {
517 fn from(ds: FlashDigitalSquelch) -> Self {
518 ds as Self
519 }
520}
521
522#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
536pub enum FineStep {
537 Hz20 = 0,
539 Hz100 = 1,
541 Hz500 = 2,
543 Hz1000 = 3,
545}
546
547impl FineStep {
548 pub const COUNT: u8 = 4;
550}
551
552impl TryFrom<u8> for FineStep {
553 type Error = ValidationError;
554
555 fn try_from(value: u8) -> Result<Self, Self::Error> {
556 match value {
557 0 => Ok(Self::Hz20),
558 1 => Ok(Self::Hz100),
559 2 => Ok(Self::Hz500),
560 3 => Ok(Self::Hz1000),
561 _ => Err(ValidationError::FineStepOutOfRange(value)),
562 }
563 }
564}
565
566impl From<FineStep> for u8 {
567 fn from(fs: FineStep) -> Self {
568 fs as Self
569 }
570}
571
572impl fmt::Display for FineStep {
573 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
574 match self {
575 Self::Hz20 => f.write_str("20 Hz"),
576 Self::Hz100 => f.write_str("100 Hz"),
577 Self::Hz500 => f.write_str("500 Hz"),
578 Self::Hz1000 => f.write_str("1000 Hz"),
579 }
580 }
581}
582
583#[allow(clippy::struct_excessive_bools)]
603#[derive(Debug, Clone, PartialEq, Eq)]
604pub struct FlashChannel {
605 pub rx_frequency: Frequency,
607 pub tx_offset: Frequency,
609 pub step_size: StepSize,
611 pub byte08_low: u8,
613 pub mode: MemoryMode,
615 pub narrow: bool,
617 pub fine_mode: bool,
619 pub fine_step: FineStep,
621 pub byte09_bit7: bool,
623 pub tone_enabled: bool,
625 pub ctcss_enabled: bool,
627 pub dtcs_enabled: bool,
629 pub cross_tone: bool,
631 pub byte0a_bit3: bool,
633 pub split: bool,
636 pub duplex: FlashDuplex,
638 pub tone_code: ToneCode,
640 pub ctcss_code: ToneCode,
642 pub byte0c_high: u8,
644 pub dcs_code: DcsCode,
646 pub byte0d_bit7: bool,
648 pub cross_tone_type: CrossToneType,
650 pub digital_squelch: FlashDigitalSquelch,
652 pub byte0e_reserved: u8,
655 pub ur_call: DstarCallsign,
657 pub rpt1: DstarCallsign,
659 pub rpt2: DstarCallsign,
661 pub dv_code: u8,
663 pub byte27_bit7: bool,
665}
666
667impl FlashChannel {
668 pub const BYTE_SIZE: usize = 40;
670
671 #[allow(clippy::similar_names, clippy::too_many_lines)]
678 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ProtocolError> {
679 if bytes.len() < Self::BYTE_SIZE {
680 return Err(ProtocolError::FieldParse {
681 command: "flash_channel".into(),
682 field: "length".into(),
683 detail: format!(
684 "expected at least {} bytes, got {}",
685 Self::BYTE_SIZE,
686 bytes.len()
687 ),
688 });
689 }
690
691 let rx_frequency = Frequency::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
692 let tx_offset = Frequency::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
693
694 let step_size =
696 StepSize::try_from(bytes[0x08] >> 4).map_err(|e| ProtocolError::FieldParse {
697 command: "flash_channel".into(),
698 field: "step_size".into(),
699 detail: e.to_string(),
700 })?;
701 let byte08_low = bytes[0x08] & 0x0F;
702
703 let byte09 = bytes[0x09];
705 let byte09_bit7 = (byte09 >> 7) & 1 != 0;
706 let mode =
707 MemoryMode::try_from((byte09 >> 4) & 0x07).map_err(|e| ProtocolError::FieldParse {
708 command: "flash_channel".into(),
709 field: "mode".into(),
710 detail: e.to_string(),
711 })?;
712 let narrow = (byte09 >> 3) & 1 != 0;
713 let fine_mode = (byte09 >> 2) & 1 != 0;
714 let fine_step =
715 FineStep::try_from(byte09 & 0x03).map_err(|e| ProtocolError::FieldParse {
716 command: "flash_channel".into(),
717 field: "fine_step".into(),
718 detail: e.to_string(),
719 })?;
720
721 let byte0a = bytes[0x0A];
723 let tone_enabled = (byte0a >> 7) & 1 != 0;
724 let ctcss_enabled = (byte0a >> 6) & 1 != 0;
725 let dtcs_enabled = (byte0a >> 5) & 1 != 0;
726 let cross_tone = (byte0a >> 4) & 1 != 0;
727 let byte0a_bit3 = (byte0a >> 3) & 1 != 0;
728 let split = (byte0a >> 2) & 1 != 0;
729 let duplex =
730 FlashDuplex::try_from(byte0a & 0x03).map_err(|e| ProtocolError::FieldParse {
731 command: "flash_channel".into(),
732 field: "duplex".into(),
733 detail: e.to_string(),
734 })?;
735
736 let tone_code = ToneCode::new(bytes[0x0B]).map_err(|e| ProtocolError::FieldParse {
738 command: "flash_channel".into(),
739 field: "tone_code".into(),
740 detail: e.to_string(),
741 })?;
742
743 let byte0c_high = (bytes[0x0C] >> 6) & 0x03;
745 let ctcss_code =
746 ToneCode::new(bytes[0x0C] & 0x3F).map_err(|e| ProtocolError::FieldParse {
747 command: "flash_channel".into(),
748 field: "ctcss_code".into(),
749 detail: e.to_string(),
750 })?;
751
752 let byte0d_bit7 = (bytes[0x0D] >> 7) & 1 != 0;
754 let dcs_code = DcsCode::new(bytes[0x0D] & 0x7F).map_err(|e| ProtocolError::FieldParse {
755 command: "flash_channel".into(),
756 field: "dcs_code".into(),
757 detail: e.to_string(),
758 })?;
759
760 let byte0e = bytes[0x0E];
762 let byte0e_reserved = (byte0e & 0xC0) | (byte0e & 0x0C);
763 let cross_tone_type = CrossToneType::try_from((byte0e >> 4) & 0x03).map_err(|e| {
764 ProtocolError::FieldParse {
765 command: "flash_channel".into(),
766 field: "cross_tone_type".into(),
767 detail: e.to_string(),
768 }
769 })?;
770 let digital_squelch = FlashDigitalSquelch::try_from(byte0e & 0x03).map_err(|e| {
771 ProtocolError::FieldParse {
772 command: "flash_channel".into(),
773 field: "digital_squelch".into(),
774 detail: e.to_string(),
775 }
776 })?;
777
778 let mut ur_arr = [0u8; 8];
780 ur_arr.copy_from_slice(&bytes[0x0F..0x17]);
781 let ur_call = DstarCallsign::from_wire_bytes(&ur_arr);
782
783 let mut rpt1_arr = [0u8; 8];
785 rpt1_arr.copy_from_slice(&bytes[0x17..0x1F]);
786 let rpt1 = DstarCallsign::from_wire_bytes(&rpt1_arr);
787
788 let mut rpt2_arr = [0u8; 8];
790 rpt2_arr.copy_from_slice(&bytes[0x1F..0x27]);
791 let rpt2 = DstarCallsign::from_wire_bytes(&rpt2_arr);
792
793 let byte27_bit7 = (bytes[0x27] >> 7) & 1 != 0;
795 let dv_code = bytes[0x27] & 0x7F;
796
797 Ok(Self {
798 rx_frequency,
799 tx_offset,
800 step_size,
801 byte08_low,
802 mode,
803 narrow,
804 fine_mode,
805 fine_step,
806 byte09_bit7,
807 tone_enabled,
808 ctcss_enabled,
809 dtcs_enabled,
810 cross_tone,
811 byte0a_bit3,
812 split,
813 duplex,
814 tone_code,
815 ctcss_code,
816 byte0c_high,
817 dcs_code,
818 byte0d_bit7,
819 cross_tone_type,
820 digital_squelch,
821 byte0e_reserved,
822 ur_call,
823 rpt1,
824 rpt2,
825 dv_code,
826 byte27_bit7,
827 })
828 }
829
830 #[must_use]
832 pub fn to_bytes(&self) -> [u8; 40] {
833 let mut buf = [0u8; 40];
834
835 buf[0..4].copy_from_slice(&self.rx_frequency.to_le_bytes());
837
838 buf[4..8].copy_from_slice(&self.tx_offset.to_le_bytes());
840
841 buf[0x08] = (u8::from(self.step_size) << 4) | (self.byte08_low & 0x0F);
843
844 buf[0x09] = (u8::from(self.byte09_bit7) << 7)
846 | (u8::from(self.mode) << 4)
847 | (u8::from(self.narrow) << 3)
848 | (u8::from(self.fine_mode) << 2)
849 | u8::from(self.fine_step);
850
851 buf[0x0A] = (u8::from(self.tone_enabled) << 7)
853 | (u8::from(self.ctcss_enabled) << 6)
854 | (u8::from(self.dtcs_enabled) << 5)
855 | (u8::from(self.cross_tone) << 4)
856 | (u8::from(self.byte0a_bit3) << 3)
857 | (u8::from(self.split) << 2)
858 | u8::from(self.duplex);
859
860 buf[0x0B] = self.tone_code.index();
862
863 buf[0x0C] = (self.byte0c_high << 6) | (self.ctcss_code.index() & 0x3F);
865
866 buf[0x0D] = (u8::from(self.byte0d_bit7) << 7) | (self.dcs_code.index() & 0x7F);
868
869 buf[0x0E] = (self.byte0e_reserved & 0xCC)
871 | (u8::from(self.cross_tone_type) << 4)
872 | u8::from(self.digital_squelch);
873
874 buf[0x0F..0x17].copy_from_slice(&self.ur_call.to_wire_bytes());
876
877 buf[0x17..0x1F].copy_from_slice(&self.rpt1.to_wire_bytes());
879
880 buf[0x1F..0x27].copy_from_slice(&self.rpt2.to_wire_bytes());
882
883 buf[0x27] = (u8::from(self.byte27_bit7) << 7) | (self.dv_code & 0x7F);
885
886 buf
887 }
888}
889
890impl Default for FlashChannel {
891 fn default() -> Self {
892 Self {
893 rx_frequency: Frequency::new(0),
894 tx_offset: Frequency::new(0),
895 step_size: StepSize::Hz5000,
896 byte08_low: 0,
897 mode: MemoryMode::Fm,
898 narrow: false,
899 fine_mode: false,
900 fine_step: FineStep::Hz20,
901 byte09_bit7: false,
902 tone_enabled: false,
903 ctcss_enabled: false,
904 dtcs_enabled: false,
905 cross_tone: false,
906 byte0a_bit3: false,
907 split: false,
908 duplex: FlashDuplex::Simplex,
909 tone_code: ToneCode::new(0).expect("0 is valid tone code"),
911 ctcss_code: ToneCode::new(0).expect("0 is valid tone code"),
912 byte0c_high: 0,
913 dcs_code: DcsCode::new(0).expect("0 is valid DCS code"),
915 byte0d_bit7: false,
916 cross_tone_type: CrossToneType::DcsOff,
917 digital_squelch: FlashDigitalSquelch::Off,
918 byte0e_reserved: 0,
919 ur_call: DstarCallsign::default(),
920 rpt1: DstarCallsign::default(),
921 rpt2: DstarCallsign::default(),
922 dv_code: 0,
923 byte27_bit7: false,
924 }
925 }
926}
927
928impl Default for ChannelMemory {
929 fn default() -> Self {
930 Self {
931 rx_frequency: Frequency::new(0),
932 tx_offset: Frequency::new(0),
933 step_size: StepSize::Hz5000,
934 mode_flags_raw: 0,
935 shift: ShiftDirection::SIMPLEX,
936 reverse: false,
937 tone_enable: false,
938 ctcss_mode: CtcssMode::Off,
939 dcs_enable: false,
940 cross_tone_reverse: false,
941 flags_0a_raw: 0,
942 tone_code: ToneCode::new(0).expect("0 is valid tone code"),
944 ctcss_code: ToneCode::new(0).expect("0 is valid tone code"),
946 dcs_code: DcsCode::new(0).expect("0 is valid DCS code"),
948 cross_tone_combo: CrossToneType::DcsOff,
949 digital_squelch: FlashDigitalSquelch::Off,
950 urcall: ChannelName::default(),
951 data_mode: 0,
952 }
953 }
954}
955
956#[cfg(test)]
957mod tests {
958 use super::*;
959 use crate::error::ValidationError;
960 use crate::types::frequency::Frequency;
961 use crate::types::mode::{ShiftDirection, StepSize};
962 use crate::types::tone::{CtcssMode, DcsCode, ToneCode};
963
964 #[test]
965 fn channel_name_valid() {
966 let name = ChannelName::new("RPT1").unwrap();
967 assert_eq!(name.as_str(), "RPT1");
968 }
969
970 #[test]
971 fn channel_name_empty() {
972 let name = ChannelName::new("").unwrap();
973 assert_eq!(name.as_str(), "");
974 }
975
976 #[test]
977 fn channel_name_max_length() {
978 let name = ChannelName::new("12345678").unwrap();
979 assert_eq!(name.as_str(), "12345678");
980 }
981
982 #[test]
983 fn channel_name_too_long() {
984 let err = ChannelName::new("123456789").unwrap_err();
985 assert!(matches!(
986 err,
987 ValidationError::ChannelNameTooLong { len: 9 }
988 ));
989 }
990
991 #[test]
992 fn channel_name_to_bytes_padded() {
993 let name = ChannelName::new("RPT1").unwrap();
994 let bytes = name.to_bytes();
995 assert_eq!(bytes.len(), 24);
996 assert_eq!(&bytes[..4], b"RPT1");
997 assert!(bytes[4..].iter().all(|&b| b == 0));
998 }
999
1000 #[test]
1001 fn channel_name_from_bytes() {
1002 let mut bytes = [0u8; 24];
1003 bytes[..4].copy_from_slice(b"RPT1");
1004 let name = ChannelName::from_bytes(&bytes);
1005 assert_eq!(name.as_str(), "RPT1");
1006 }
1007
1008 #[test]
1011 fn channel_memory_byte_layout_size() {
1012 assert_eq!(ChannelMemory::BYTE_SIZE, 40);
1013 }
1014
1015 #[test]
1016 fn channel_memory_round_trip_simplex_vhf() {
1017 let ch = ChannelMemory {
1020 rx_frequency: Frequency::new(145_000_000),
1021 tx_offset: Frequency::new(600_000),
1022 step_size: StepSize::Hz12500,
1023 mode_flags_raw: 0,
1024 shift: ShiftDirection::UP,
1025 reverse: false,
1026 tone_enable: true,
1027 ctcss_mode: CtcssMode::Off,
1028 dcs_enable: false,
1029 cross_tone_reverse: false,
1030 flags_0a_raw: 0x81, tone_code: ToneCode::new(8).unwrap(),
1032 ctcss_code: ToneCode::new(8).unwrap(),
1033 dcs_code: DcsCode::new(0).unwrap(),
1034 cross_tone_combo: CrossToneType::DcsOff,
1035 digital_squelch: FlashDigitalSquelch::Off,
1036 urcall: ChannelName::new("").unwrap(),
1037 data_mode: 0,
1038 };
1039 let bytes = ch.to_bytes();
1040 assert_eq!(bytes.len(), 40);
1041 let parsed = ChannelMemory::from_bytes(&bytes).unwrap();
1042 assert_eq!(parsed, ch);
1043 }
1044
1045 #[test]
1046 fn channel_memory_byte08_packing() {
1047 let ch = ChannelMemory {
1048 step_size: StepSize::Hz12500, shift: ShiftDirection::UP, ..ChannelMemory::default()
1051 };
1052 let bytes = ch.to_bytes();
1053 assert_eq!(bytes[0x08], 0x51); }
1055
1056 #[test]
1057 fn channel_memory_byte09_packing() {
1058 let ch = ChannelMemory {
1060 reverse: true,
1061 tone_enable: true,
1062 ctcss_mode: CtcssMode::On,
1063 ..ChannelMemory::default()
1064 };
1065 let bytes = ch.to_bytes();
1066 assert_eq!(bytes[0x09], 0x00);
1067 }
1068
1069 #[test]
1070 fn channel_memory_byte0a_packing() {
1071 let ch = ChannelMemory {
1074 dcs_enable: true,
1075 cross_tone_reverse: true,
1076 flags_0a_raw: 0xB0, ..ChannelMemory::default()
1078 };
1079 let bytes = ch.to_bytes();
1080 assert_eq!(bytes[0x0A], 0xB0);
1081 }
1082
1083 #[test]
1084 fn channel_memory_unknown_bits_passthrough() {
1085 let ch = ChannelMemory {
1086 dcs_enable: false,
1087 cross_tone_reverse: false,
1088 flags_0a_raw: 0x2B,
1089 ..ChannelMemory::default()
1090 };
1091 let bytes = ch.to_bytes();
1092 assert_eq!(bytes[0x0A], 0x2B);
1093 let parsed = ChannelMemory::from_bytes(&bytes).unwrap();
1094 assert_eq!(parsed.flags_0a_raw, 0x2B);
1095 }
1096
1097 #[test]
1098 fn channel_memory_byte0e_packing() {
1099 let ch = ChannelMemory {
1100 cross_tone_combo: CrossToneType::ToneDcs,
1101 digital_squelch: FlashDigitalSquelch::Code,
1102 ..ChannelMemory::default()
1103 };
1104 let bytes = ch.to_bytes();
1105 assert_eq!(bytes[0x0E], 0x1D); }
1107
1108 #[test]
1109 fn channel_memory_frequency_le_bytes() {
1110 let ch = ChannelMemory {
1111 rx_frequency: Frequency::new(145_000_000),
1112 tx_offset: Frequency::new(600_000),
1113 ..ChannelMemory::default()
1114 };
1115 let bytes = ch.to_bytes();
1116 let rx = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1117 assert_eq!(rx, 145_000_000);
1118 let tx = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
1119 assert_eq!(tx, 600_000);
1120 }
1121
1122 #[test]
1125 fn flash_channel_byte_size() {
1126 assert_eq!(FlashChannel::BYTE_SIZE, 40);
1127 }
1128
1129 #[test]
1130 fn flash_channel_default_round_trip() {
1131 let ch = FlashChannel::default();
1132 let bytes = ch.to_bytes();
1133 let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1134 assert_eq!(parsed, ch);
1135 }
1136
1137 #[test]
1138 fn flash_channel_mode_encoding() {
1139 use crate::types::mode::MemoryMode;
1140 for (raw, expected) in [
1142 (0, MemoryMode::Fm),
1143 (1, MemoryMode::Dv),
1144 (2, MemoryMode::Am),
1145 (3, MemoryMode::Lsb),
1146 (4, MemoryMode::Usb),
1147 (5, MemoryMode::Cw),
1148 (6, MemoryMode::Nfm),
1149 (7, MemoryMode::Dr),
1150 ] {
1151 let ch = FlashChannel {
1152 mode: expected,
1153 ..FlashChannel::default()
1154 };
1155 let bytes = ch.to_bytes();
1156 assert_eq!(
1158 (bytes[0x09] >> 4) & 0x07,
1159 raw,
1160 "mode {expected} should encode as {raw}"
1161 );
1162 let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1163 assert_eq!(parsed.mode, expected);
1164 }
1165 }
1166
1167 #[test]
1168 fn flash_channel_mode_matches_cat() {
1169 use crate::types::mode::{MemoryMode, Mode};
1171 assert_eq!(u8::from(MemoryMode::Am), u8::from(Mode::Am));
1172 assert_eq!(u8::from(MemoryMode::Nfm), u8::from(Mode::Nfm));
1173 assert_eq!(u8::from(MemoryMode::Fm), u8::from(Mode::Fm));
1174 assert_eq!(u8::from(MemoryMode::Dr), u8::from(Mode::Dr));
1175 }
1176
1177 #[test]
1178 fn flash_channel_byte09_packing() {
1179 let ch = FlashChannel {
1180 mode: MemoryMode::Am, narrow: true, fine_mode: true, fine_step: FineStep::Hz1000, byte09_bit7: false,
1185 ..FlashChannel::default()
1186 };
1187 let bytes = ch.to_bytes();
1188 assert_eq!(bytes[0x09], 0x2F);
1190 let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1191 assert_eq!(parsed.mode, MemoryMode::Am);
1192 assert!(parsed.narrow);
1193 assert!(parsed.fine_mode);
1194 assert_eq!(parsed.fine_step, FineStep::Hz1000);
1195 }
1196
1197 #[test]
1198 fn flash_channel_byte0a_tone_duplex() {
1199 let ch = FlashChannel {
1200 tone_enabled: true, ctcss_enabled: false, dtcs_enabled: true, cross_tone: false, byte0a_bit3: false, split: true, duplex: FlashDuplex::Minus, ..FlashChannel::default()
1208 };
1209 let bytes = ch.to_bytes();
1210 assert_eq!(bytes[0x0A], 0xA6);
1212 let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1213 assert!(parsed.tone_enabled);
1214 assert!(!parsed.ctcss_enabled);
1215 assert!(parsed.dtcs_enabled);
1216 assert!(!parsed.cross_tone);
1217 assert!(parsed.split);
1218 assert_eq!(parsed.duplex, FlashDuplex::Minus);
1219 }
1220
1221 #[test]
1222 fn flash_channel_dstar_callsigns() {
1223 use crate::types::dstar::DstarCallsign;
1224 let ch = FlashChannel {
1225 ur_call: DstarCallsign::new("CQCQCQ").unwrap(),
1226 rpt1: DstarCallsign::new("W4BFB B").unwrap(),
1227 rpt2: DstarCallsign::new("W4BFB G").unwrap(),
1228 ..FlashChannel::default()
1229 };
1230 let bytes = ch.to_bytes();
1231 assert_eq!(&bytes[0x0F..0x17], b"CQCQCQ ");
1233 assert_eq!(&bytes[0x17..0x1F], b"W4BFB B ");
1235 assert_eq!(&bytes[0x1F..0x27], b"W4BFB G ");
1237
1238 let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1239 assert_eq!(parsed.ur_call.as_str(), "CQCQCQ");
1240 assert_eq!(parsed.rpt1.as_str(), "W4BFB B");
1241 assert_eq!(parsed.rpt2.as_str(), "W4BFB G");
1242 }
1243
1244 #[test]
1245 fn flash_channel_byte0e_cross_tone_digital_squelch() {
1246 let ch = FlashChannel {
1247 cross_tone_type: CrossToneType::DcsCtcss, digital_squelch: FlashDigitalSquelch::Callsign, byte0e_reserved: 0,
1250 ..FlashChannel::default()
1251 };
1252 let bytes = ch.to_bytes();
1253 assert_eq!(bytes[0x0E], 0x22);
1255 let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1256 assert_eq!(parsed.cross_tone_type, CrossToneType::DcsCtcss);
1257 assert_eq!(parsed.digital_squelch, FlashDigitalSquelch::Callsign);
1258 }
1259
1260 #[test]
1261 fn flash_channel_dv_code() {
1262 let ch = FlashChannel {
1263 dv_code: 42,
1264 byte27_bit7: true,
1265 ..FlashChannel::default()
1266 };
1267 let bytes = ch.to_bytes();
1268 assert_eq!(bytes[0x27], 0x80 | 0x2A);
1269 let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1270 assert_eq!(parsed.dv_code, 42);
1271 assert!(parsed.byte27_bit7);
1272 }
1273
1274 #[test]
1275 fn flash_channel_full_round_trip() {
1276 use crate::types::dstar::DstarCallsign;
1277 let ch = FlashChannel {
1278 rx_frequency: Frequency::new(146_520_000),
1279 tx_offset: Frequency::new(600_000),
1280 step_size: StepSize::Hz12500,
1281 byte08_low: 0x03,
1282 mode: MemoryMode::Dv,
1283 narrow: false,
1284 fine_mode: true,
1285 fine_step: FineStep::Hz100,
1286 byte09_bit7: false,
1287 tone_enabled: true,
1288 ctcss_enabled: true,
1289 dtcs_enabled: false,
1290 cross_tone: false,
1291 byte0a_bit3: false,
1292 split: false,
1293 duplex: FlashDuplex::Plus,
1294 tone_code: ToneCode::new(8).unwrap(),
1295 ctcss_code: ToneCode::new(12).unwrap(),
1296 byte0c_high: 0,
1297 dcs_code: DcsCode::new(5).unwrap(),
1298 byte0d_bit7: false,
1299 cross_tone_type: CrossToneType::ToneDcs,
1300 digital_squelch: FlashDigitalSquelch::Code,
1301 byte0e_reserved: 0,
1302 ur_call: DstarCallsign::new("CQCQCQ").unwrap(),
1303 rpt1: DstarCallsign::new("W4BFB B").unwrap(),
1304 rpt2: DstarCallsign::new("W4BFB G").unwrap(),
1305 dv_code: 99,
1306 byte27_bit7: false,
1307 };
1308 let bytes = ch.to_bytes();
1309 assert_eq!(bytes.len(), 40);
1310 let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1311 assert_eq!(parsed, ch);
1312 }
1313
1314 #[test]
1315 fn flash_channel_too_short() {
1316 let bytes = [0u8; 39];
1317 let err = FlashChannel::from_bytes(&bytes);
1318 assert!(err.is_err());
1319 }
1320
1321 #[test]
1322 fn flash_channel_reserved_bits_preserved() {
1323 let ch = FlashChannel {
1324 byte0c_high: 0x03,
1325 byte0d_bit7: true,
1326 byte0e_reserved: 0xCC, byte0a_bit3: true,
1328 byte09_bit7: true,
1329 ..FlashChannel::default()
1330 };
1331 let bytes = ch.to_bytes();
1332 let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1333 assert_eq!(parsed.byte0c_high, 0x03);
1334 assert!(parsed.byte0d_bit7);
1335 assert_eq!(parsed.byte0e_reserved, 0xCC);
1336 assert!(parsed.byte0a_bit3);
1337 assert!(parsed.byte09_bit7);
1338 }
1339
1340 #[test]
1341 fn flash_fine_step_display() {
1342 assert_eq!(FineStep::Hz20.to_string(), "20 Hz");
1343 assert_eq!(FineStep::Hz100.to_string(), "100 Hz");
1344 assert_eq!(FineStep::Hz500.to_string(), "500 Hz");
1345 assert_eq!(FineStep::Hz1000.to_string(), "1000 Hz");
1346 }
1347
1348 #[test]
1349 fn flash_duplex_round_trip() {
1350 for i in 0u8..FlashDuplex::COUNT {
1351 let d = FlashDuplex::try_from(i).unwrap();
1352 assert_eq!(u8::from(d), i);
1353 }
1354 assert!(FlashDuplex::try_from(FlashDuplex::COUNT).is_err());
1355 }
1356
1357 #[test]
1358 fn cross_tone_type_round_trip() {
1359 for i in 0u8..CrossToneType::COUNT {
1360 let ct = CrossToneType::try_from(i).unwrap();
1361 assert_eq!(u8::from(ct), i);
1362 }
1363 assert!(CrossToneType::try_from(CrossToneType::COUNT).is_err());
1364 }
1365
1366 #[test]
1367 fn flash_digital_squelch_round_trip() {
1368 for i in 0u8..FlashDigitalSquelch::COUNT {
1369 let ds = FlashDigitalSquelch::try_from(i).unwrap();
1370 assert_eq!(u8::from(ds), i);
1371 }
1372 assert!(FlashDigitalSquelch::try_from(FlashDigitalSquelch::COUNT).is_err());
1373 }
1374}