1use crate::error::{Error, ProtocolError, TransportError};
41use crate::protocol::programming::{self, ChannelFlag};
42use crate::transport::Transport;
43use crate::types::FlashChannel;
44
45use super::Radio;
46
47const PROGRAMMING_BAUD: u32 = 9600;
53
54const FAST_TRANSFER_BAUD: u32 = 115_200;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum McpSpeed {
72 #[default]
74 Safe,
75 Fast,
81}
82
83const FULL_DUMP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(120);
89
90impl<T: Transport> Radio<T> {
91 pub async fn read_memory_image(&mut self) -> Result<Vec<u8>, Error> {
105 self.read_memory_image_with_progress(|_, _| {}).await
106 }
107
108 pub async fn read_memory_image_with_progress<F>(
118 &mut self,
119 mut on_progress: F,
120 ) -> Result<Vec<u8>, Error>
121 where
122 F: FnMut(u16, u16),
123 {
124 let saved_timeout = self.timeout;
125 self.timeout = FULL_DUMP_TIMEOUT;
126
127 self.enter_programming_mode().await?;
128
129 let result = self
130 .read_pages_raw(0, programming::TOTAL_PAGES, &mut on_progress)
131 .await;
132
133 let exit_result = self.exit_programming_mode().await;
134 self.timeout = saved_timeout;
135
136 let image = result?;
137 exit_result?;
138
139 Ok(image)
140 }
141
142 pub async fn write_memory_image(&mut self, image: &[u8]) -> Result<(), Error> {
153 self.write_memory_image_with_progress(image, |_, _| {})
154 .await
155 }
156
157 pub async fn write_memory_image_with_progress<F>(
168 &mut self,
169 image: &[u8],
170 mut on_progress: F,
171 ) -> Result<(), Error>
172 where
173 F: FnMut(u16, u16),
174 {
175 if image.len() != programming::TOTAL_SIZE {
176 return Err(Error::InvalidImageSize {
177 actual: image.len(),
178 expected: programming::TOTAL_SIZE,
179 });
180 }
181
182 let saved_timeout = self.timeout;
183 self.timeout = FULL_DUMP_TIMEOUT;
184
185 self.enter_programming_mode().await?;
186
187 let writable_pages = programming::TOTAL_PAGES - programming::FACTORY_CAL_PAGES;
189 let result = self
190 .write_pages_raw(
191 0,
192 &image[..writable_pages as usize * programming::PAGE_SIZE],
193 &mut on_progress,
194 )
195 .await;
196
197 let exit_result = self.exit_programming_mode().await;
198 self.timeout = saved_timeout;
199
200 result?;
201 exit_result?;
202
203 Ok(())
204 }
205
206 pub async fn read_memory_pages(
220 &mut self,
221 start_page: u16,
222 count: u16,
223 ) -> Result<Vec<u8>, Error> {
224 self.enter_programming_mode().await?;
225
226 let result = self.read_pages_raw(start_page, count, &mut |_, _| {}).await;
227
228 let exit_result = self.exit_programming_mode().await;
229
230 let data = result?;
231 exit_result?;
232
233 Ok(data)
234 }
235
236 pub async fn write_memory_pages(&mut self, start_page: u16, data: &[u8]) -> Result<(), Error> {
249 let page_count = data.len() / programming::PAGE_SIZE;
250 for i in 0..page_count {
252 #[allow(clippy::cast_possible_truncation)]
255 let offset = i as u16;
256 let page = start_page + offset;
257 if programming::is_factory_calibration_page(page) {
258 return Err(Error::MemoryWriteProtected { page });
259 }
260 }
261
262 self.enter_programming_mode().await?;
263
264 let result = self.write_pages_raw(start_page, data, &mut |_, _| {}).await;
265
266 let exit_result = self.exit_programming_mode().await;
267
268 result?;
269 exit_result?;
270
271 Ok(())
272 }
273
274 pub async fn read_page(&mut self, page: u16) -> Result<[u8; programming::PAGE_SIZE], Error> {
287 self.enter_programming_mode().await?;
288
289 let result = self.read_single_page(page).await;
290
291 let exit_result = self.exit_programming_mode().await;
292
293 let data = result?;
294 exit_result?;
295
296 Ok(data)
297 }
298
299 pub async fn write_page(
310 &mut self,
311 page: u16,
312 data: &[u8; programming::PAGE_SIZE],
313 ) -> Result<(), Error> {
314 if programming::is_factory_calibration_page(page) {
315 return Err(Error::MemoryWriteProtected { page });
316 }
317
318 self.enter_programming_mode().await?;
319
320 let result = self.write_single_page(page, data).await;
321
322 let exit_result = self.exit_programming_mode().await;
323
324 result?;
325 exit_result?;
326
327 Ok(())
328 }
329
330 pub async fn modify_memory_page<F>(&mut self, page: u16, modify: F) -> Result<(), Error>
355 where
356 F: FnOnce(&mut [u8; programming::PAGE_SIZE]),
357 {
358 if programming::is_factory_calibration_page(page) {
359 return Err(Error::MemoryWriteProtected { page });
360 }
361
362 self.enter_programming_mode().await?;
363
364 let result: Result<(), Error> = async {
365 let mut page_data = self.read_single_page(page).await?;
367
368 modify(&mut page_data);
370
371 self.write_single_page(page, &page_data).await?;
373
374 Ok(())
375 }
376 .await;
377
378 let exit_result = self.exit_programming_mode().await;
380
381 result?;
382 exit_result?;
383
384 Ok(())
385 }
386
387 pub async fn read_channel_names(&mut self) -> Result<Vec<String>, Error> {
407 self.enter_programming_mode().await?;
408
409 let result = self.read_name_pages().await;
410
411 let exit_result = self.exit_programming_mode().await;
413
414 let names = result?;
416 exit_result?;
417
418 Ok(names)
419 }
420
421 pub async fn read_all_channel_names(&mut self) -> Result<Vec<String>, Error> {
440 self.enter_programming_mode().await?;
441
442 let result = self.read_all_name_pages().await;
443
444 let exit_result = self.exit_programming_mode().await;
445
446 let names = result?;
447 exit_result?;
448
449 Ok(names)
450 }
451
452 pub async fn write_channel_name(&mut self, channel: u16, name: &str) -> Result<(), Error> {
471 #[allow(clippy::cast_possible_truncation)]
473 const MAX_CHANNEL: u16 = programming::TOTAL_CHANNEL_ENTRIES as u16;
474 if channel >= MAX_CHANNEL {
475 return Err(Error::Validation(
476 crate::error::ValidationError::ChannelOutOfRange {
477 channel,
478 max: MAX_CHANNEL - 1,
479 },
480 ));
481 }
482 let page = programming::CHANNEL_NAMES_START + (channel / 16);
483 let offset = (channel % 16) as usize * programming::NAME_ENTRY_SIZE;
484
485 tracing::info!(channel, name, page, offset, "writing channel name via MCP");
486 self.modify_memory_page(page, |data| {
487 data[offset..offset + programming::NAME_ENTRY_SIZE].fill(0);
489 let name_bytes = name.as_bytes();
491 let len = name_bytes.len().min(programming::NAME_ENTRY_SIZE - 1);
492 data[offset..offset + len].copy_from_slice(&name_bytes[..len]);
493 })
494 .await
495 }
496
497 pub async fn read_channel_flags(&mut self) -> Result<Vec<ChannelFlag>, Error> {
507 self.enter_programming_mode().await?;
508
509 let page_count = programming::CHANNEL_FLAGS_END - programming::CHANNEL_FLAGS_START + 1;
510 let result = self
511 .read_pages_raw(programming::CHANNEL_FLAGS_START, page_count, &mut |_, _| {})
512 .await;
513
514 let exit_result = self.exit_programming_mode().await;
515
516 let raw = result?;
517 exit_result?;
518
519 let mut flags = Vec::with_capacity(programming::TOTAL_CHANNEL_ENTRIES);
521 for i in 0..programming::TOTAL_CHANNEL_ENTRIES {
522 let offset = i * programming::FLAG_RECORD_SIZE;
523 if offset + programming::FLAG_RECORD_SIZE <= raw.len()
524 && let Some(flag) = programming::parse_channel_flag(&raw[offset..])
525 {
526 flags.push(flag);
527 }
528 }
529
530 tracing::info!(count = flags.len(), "channel flags read");
531 Ok(flags)
532 }
533
534 pub async fn read_all_channels(&mut self) -> Result<Vec<FlashChannel>, Error> {
546 self.enter_programming_mode().await?;
547
548 let page_count = programming::CHANNEL_DATA_END - programming::CHANNEL_DATA_START + 1;
549 let result = self
550 .read_pages_raw(programming::CHANNEL_DATA_START, page_count, &mut |_, _| {})
551 .await;
552
553 let exit_result = self.exit_programming_mode().await;
554
555 let raw = result?;
556 exit_result?;
557
558 let mut channels = Vec::with_capacity(programming::TOTAL_CHANNEL_ENTRIES);
561 for memgroup_idx in 0..programming::MEMGROUP_COUNT {
562 let group_offset = memgroup_idx * programming::PAGE_SIZE;
563 for slot in 0..programming::CHANNELS_PER_MEMGROUP {
564 let ch_offset = group_offset + slot * programming::CHANNEL_RECORD_SIZE;
565 if ch_offset + programming::CHANNEL_RECORD_SIZE <= raw.len() {
566 match FlashChannel::from_bytes(&raw[ch_offset..]) {
567 Ok(ch) => channels.push(ch),
568 Err(e) => {
569 tracing::warn!(
570 memgroup = memgroup_idx,
571 slot,
572 error = %e,
573 "failed to parse flash channel record, using default"
574 );
575 channels.push(FlashChannel::default());
576 }
577 }
578 }
579 }
580 }
581
582 tracing::info!(count = channels.len(), "channel memory records read");
583 Ok(channels)
584 }
585
586 pub async fn read_configuration(&mut self) -> Result<crate::memory::MemoryImage, Error> {
603 let raw = self.read_memory_image().await?;
604 crate::memory::MemoryImage::from_raw(raw).map_err(|e| {
605 Error::Protocol(ProtocolError::FieldParse {
606 command: "read_configuration".into(),
607 field: "memory_image".into(),
608 detail: e.to_string(),
609 })
610 })
611 }
612
613 pub async fn read_configuration_with_progress<F>(
623 &mut self,
624 on_progress: F,
625 ) -> Result<crate::memory::MemoryImage, Error>
626 where
627 F: FnMut(u16, u16),
628 {
629 let raw = self.read_memory_image_with_progress(on_progress).await?;
630 crate::memory::MemoryImage::from_raw(raw).map_err(|e| {
631 Error::Protocol(ProtocolError::FieldParse {
632 command: "read_configuration".into(),
633 field: "memory_image".into(),
634 detail: e.to_string(),
635 })
636 })
637 }
638
639 pub async fn write_configuration(
649 &mut self,
650 image: &crate::memory::MemoryImage,
651 ) -> Result<(), Error> {
652 self.write_memory_image(image.as_raw()).await
653 }
654
655 pub async fn write_configuration_with_progress<F>(
665 &mut self,
666 image: &crate::memory::MemoryImage,
667 on_progress: F,
668 ) -> Result<(), Error>
669 where
670 F: FnMut(u16, u16),
671 {
672 self.write_memory_image_with_progress(image.as_raw(), on_progress)
673 .await
674 }
675
676 async fn enter_programming_mode(&mut self) -> Result<(), Error> {
695 tracing::info!("entering programming mode at 9600 baud");
696
697 self.transport
699 .set_baud_rate(PROGRAMMING_BAUD)
700 .map_err(Error::Transport)?;
701
702 self.transport
703 .write(programming::ENTER_PROGRAMMING)
704 .await
705 .map_err(Error::Transport)?;
706
707 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
709
710 let mut buf = [0u8; 64];
712 let mut received = Vec::new();
713
714 let result = tokio::time::timeout(self.timeout, async {
715 loop {
716 let n = self
717 .transport
718 .read(&mut buf)
719 .await
720 .map_err(Error::Transport)?;
721 if n == 0 {
722 return Err(Error::Transport(TransportError::Disconnected(
723 std::io::Error::new(
724 std::io::ErrorKind::UnexpectedEof,
725 "connection closed during programming mode entry",
726 ),
727 )));
728 }
729 received.extend_from_slice(&buf[..n]);
730 if received.windows(3).any(|w| w == b"0M\r") {
732 return Ok(());
733 }
734 if received.len() > 20 {
735 return Err(Error::Protocol(ProtocolError::UnexpectedResponse {
737 expected: "0M\\r".to_string(),
738 actual: received.clone(),
739 }));
740 }
741 }
742 })
743 .await
744 .map_err(|_| Error::Timeout(self.timeout))?;
745 result?;
746
747 if self.mcp_speed == McpSpeed::Fast {
750 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
751 self.transport
752 .set_baud_rate(FAST_TRANSFER_BAUD)
753 .map_err(Error::Transport)?;
754 let mut sync = [0u8; 1];
758 match tokio::time::timeout(
759 std::time::Duration::from_secs(2),
760 self.transport.read(&mut sync),
761 )
762 .await
763 {
764 Ok(Ok(n)) if n > 0 => {
765 tracing::info!(
766 sync_byte = sync[0],
767 "programming mode entered, switched to {FAST_TRANSFER_BAUD} baud (fast)"
768 );
769 }
770 Ok(Ok(_)) => {
771 tracing::error!("fast mode sync read returned 0 bytes — baud mismatch likely");
772 return Err(Error::Protocol(ProtocolError::MalformedFrame(
773 b"fast mode sync byte not received".to_vec(),
774 )));
775 }
776 Ok(Err(e)) => {
777 tracing::error!("fast mode sync read failed: {e}");
778 return Err(Error::Transport(e));
779 }
780 Err(_) => {
781 tracing::error!(
782 "fast mode sync byte timed out — radio may not have switched baud"
783 );
784 return Err(Error::Timeout(std::time::Duration::from_secs(2)));
785 }
786 }
787 } else {
788 tracing::info!("programming mode entered, staying at {PROGRAMMING_BAUD} baud");
789 }
790
791 Ok(())
792 }
793
794 async fn exit_programming_mode(&mut self) -> Result<(), Error> {
803 tracing::info!("exiting programming mode");
804
805 self.transport
806 .write(&[programming::EXIT])
807 .await
808 .map_err(Error::Transport)?;
809
810 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
812
813 if self.mcp_speed == McpSpeed::Fast {
815 if let Err(e) = self
816 .transport
817 .set_baud_rate(crate::transport::SerialTransport::DEFAULT_BAUD)
818 {
819 tracing::warn!("failed to restore baud rate after fast MCP exit: {e}");
820 }
821 tracing::info!("programming mode exited, restored default baud rate");
822 } else {
823 tracing::info!("programming mode exited, staying at 9600 baud");
827 }
828
829 Ok(())
830 }
831
832 async fn read_pages_raw<F>(
844 &mut self,
845 start_page: u16,
846 count: u16,
847 on_progress: &mut F,
848 ) -> Result<Vec<u8>, Error>
849 where
850 F: FnMut(u16, u16),
851 {
852 let mut image = Vec::with_capacity(count as usize * programming::PAGE_SIZE);
853
854 for i in 0..count {
855 let page = start_page + i;
856 let data = match self.read_single_page(page).await {
857 Ok(d) => d,
858 Err(Error::Timeout(_)) => {
859 tracing::warn!(page, "page read timed out, retrying once");
860 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
862 self.read_single_page(page).await?
863 }
864 Err(e) => return Err(e),
865 };
866 image.extend_from_slice(&data);
867 on_progress(i + 1, count);
868 }
869
870 Ok(image)
871 }
872
873 async fn write_pages_raw<F>(
877 &mut self,
878 start_page: u16,
879 data: &[u8],
880 on_progress: &mut F,
881 ) -> Result<(), Error>
882 where
883 F: FnMut(u16, u16),
884 {
885 let page_count = data.len() / programming::PAGE_SIZE;
886
887 for i in 0..page_count {
888 #[allow(clippy::cast_possible_truncation)]
890 let page_offset = i as u16;
891 let page = start_page + page_offset;
892 let byte_offset = i * programming::PAGE_SIZE;
893 let page_data: &[u8; programming::PAGE_SIZE] = data
894 [byte_offset..byte_offset + programming::PAGE_SIZE]
895 .try_into()
896 .expect("slice is exactly PAGE_SIZE bytes");
897 self.write_single_page(page, page_data).await?;
898 #[allow(clippy::cast_possible_truncation)]
899 let total = page_count as u16;
900 on_progress(page_offset + 1, total);
901 }
902
903 Ok(())
904 }
905
906 async fn read_single_page(&mut self, page: u16) -> Result<[u8; programming::PAGE_SIZE], Error> {
908 let cmd = programming::build_read_command(page);
909
910 tracing::debug!(page, "reading page");
911
912 self.transport.write(&cmd).await.map_err(Error::Transport)?;
914 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
915
916 let mut received = Vec::with_capacity(programming::W_RESPONSE_SIZE);
918 let mut buf = [0u8; 512];
919 let result = tokio::time::timeout(self.timeout, async {
920 while received.len() < programming::W_RESPONSE_SIZE {
921 let n = self
922 .transport
923 .read(&mut buf)
924 .await
925 .map_err(Error::Transport)?;
926 if n == 0 {
927 return Err(Error::Transport(TransportError::Disconnected(
928 std::io::Error::new(
929 std::io::ErrorKind::UnexpectedEof,
930 "connection closed during page read",
931 ),
932 )));
933 }
934 received.extend_from_slice(&buf[..n]);
935 }
936 Ok(())
937 })
938 .await
939 .map_err(|_| Error::Timeout(self.timeout))?;
940 result?;
941
942 let (_page_addr, data) = programming::parse_write_response(&received)
944 .map_err(|e| Error::Protocol(ProtocolError::MalformedFrame(e.into_bytes())))?;
945
946 let mut page_data = [0u8; programming::PAGE_SIZE];
948 page_data.copy_from_slice(data);
949
950 self.transport
952 .write(&[programming::ACK])
953 .await
954 .map_err(Error::Transport)?;
955 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
956 let mut ack_buf = [0u8; 1];
957 let _ = tokio::time::timeout(
958 std::time::Duration::from_millis(1000),
959 self.transport.read(&mut ack_buf),
960 )
961 .await;
962
963 Ok(page_data)
964 }
965
966 async fn write_single_page(
968 &mut self,
969 page: u16,
970 data: &[u8; programming::PAGE_SIZE],
971 ) -> Result<(), Error> {
972 if programming::is_factory_calibration_page(page) {
973 return Err(Error::MemoryWriteProtected { page });
974 }
975
976 let cmd = programming::build_write_command(page, data);
977
978 tracing::debug!(page, "writing page");
979
980 self.transport.write(&cmd).await.map_err(Error::Transport)?;
982 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
983
984 let mut ack_buf = [0u8; 1];
986 let result = tokio::time::timeout(self.timeout, async {
987 let n = self
988 .transport
989 .read(&mut ack_buf)
990 .await
991 .map_err(Error::Transport)?;
992 if n == 0 {
993 return Err(Error::Transport(TransportError::Disconnected(
994 std::io::Error::new(
995 std::io::ErrorKind::UnexpectedEof,
996 "connection closed waiting for write ACK",
997 ),
998 )));
999 }
1000 Ok(())
1001 })
1002 .await
1003 .map_err(|_| Error::Timeout(self.timeout))?;
1004 result?;
1005
1006 if ack_buf[0] != programming::ACK {
1007 return Err(Error::WriteNotAcknowledged {
1008 page,
1009 got: ack_buf[0],
1010 });
1011 }
1012
1013 Ok(())
1014 }
1015
1016 async fn read_name_pages(&mut self) -> Result<Vec<String>, Error> {
1025 let mut names = Vec::with_capacity(programming::MAX_CHANNELS);
1026
1027 for page_offset in 0..programming::NAME_PAGE_COUNT {
1028 let page = programming::NAME_START_PAGE + page_offset;
1029 let data = self.read_single_page(page).await?;
1030
1031 for i in 0..programming::NAMES_PER_PAGE {
1033 let start = i * programming::NAME_ENTRY_SIZE;
1034 if start + programming::NAME_ENTRY_SIZE <= data.len() {
1035 let name = programming::extract_name(
1036 &data[start..start + programming::NAME_ENTRY_SIZE],
1037 );
1038 names.push(name);
1039 }
1040 }
1041
1042 if names.len() >= programming::MAX_CHANNELS {
1044 names.truncate(programming::MAX_CHANNELS);
1045 break;
1046 }
1047 }
1048
1049 tracing::info!(count = names.len(), "channel names read");
1050 Ok(names)
1051 }
1052
1053 async fn read_all_name_pages(&mut self) -> Result<Vec<String>, Error> {
1057 let mut names = Vec::with_capacity(programming::TOTAL_CHANNEL_ENTRIES);
1058
1059 for page_offset in 0..programming::NAME_ALL_PAGE_COUNT {
1060 let page = programming::NAME_START_PAGE + page_offset;
1061 let data = self.read_single_page(page).await?;
1062
1063 for i in 0..programming::NAMES_PER_PAGE {
1064 let start = i * programming::NAME_ENTRY_SIZE;
1065 if start + programming::NAME_ENTRY_SIZE <= data.len() {
1066 let name = programming::extract_name(
1067 &data[start..start + programming::NAME_ENTRY_SIZE],
1068 );
1069 names.push(name);
1070 }
1071 }
1072
1073 if names.len() >= programming::TOTAL_CHANNEL_ENTRIES {
1074 names.truncate(programming::TOTAL_CHANNEL_ENTRIES);
1075 break;
1076 }
1077 }
1078
1079 tracing::info!(
1080 count = names.len(),
1081 "all channel names read (including extended)"
1082 );
1083 Ok(names)
1084 }
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089 use crate::protocol::programming;
1090 use crate::radio::Radio;
1091 use crate::transport::MockTransport;
1092
1093 fn build_w_response(page: u16, data: &[u8]) -> Vec<u8> {
1096 assert_eq!(data.len(), 256, "W response payload must be 256 bytes");
1097 let addr = page.to_be_bytes();
1098 let mut resp = vec![b'W', addr[0], addr[1], 0x00, 0x00];
1100 resp.extend_from_slice(data);
1101 resp
1102 }
1103
1104 fn build_name_page(names: &[&str]) -> Vec<u8> {
1106 let mut data = vec![0u8; 256];
1107 for (i, name) in names.iter().enumerate().take(16) {
1108 let start = i * 16;
1109 let bytes = name.as_bytes();
1110 data[start..start + bytes.len()].copy_from_slice(bytes);
1111 }
1112 data
1113 }
1114
1115 #[tokio::test]
1116 async fn read_channel_names_full_sequence() {
1117 let mut mock = MockTransport::new();
1120
1121 mock.expect(b"0M PROGRAM\r", b"0M\r");
1123
1124 let first_page_data = build_name_page(&["ForestCityPD", "RPT1", "", "NOAA WX"]);
1126 let read_cmd = programming::build_read_command(256);
1127 mock.expect(&read_cmd, &build_w_response(256, &first_page_data));
1128
1129 for page_offset in 1..programming::NAME_PAGE_COUNT {
1131 mock.expect(&[programming::ACK], &[programming::ACK]);
1132
1133 let page = programming::NAME_START_PAGE + page_offset;
1134 let cmd = programming::build_read_command(page);
1135 let empty = vec![0u8; 256];
1136 mock.expect(&cmd, &build_w_response(page, &empty));
1137 }
1138
1139 mock.expect(&[programming::ACK], &[programming::ACK]);
1141
1142 mock.expect(b"E", &[]);
1144
1145 let mut radio = Radio::connect(mock).await.unwrap();
1146 let names = radio.read_channel_names().await.unwrap();
1147
1148 assert_eq!(names.len(), 1000);
1150 assert_eq!(names[0], "ForestCityPD");
1151 assert_eq!(names[1], "RPT1");
1152 assert_eq!(names[2], "");
1153 assert_eq!(names[3], "NOAA WX");
1154 for name in &names[4..16] {
1155 assert!(name.is_empty());
1156 }
1157 }
1158
1159 #[tokio::test]
1160 async fn read_single_page_round_trip() {
1161 let mut mock = MockTransport::new();
1162
1163 mock.expect(b"0M PROGRAM\r", b"0M\r");
1165
1166 let page: u16 = 0x0020;
1168 let mut page_data = vec![0xABu8; 256];
1169 page_data[0] = 0x00; let cmd = programming::build_read_command(page);
1171 mock.expect(&cmd, &build_w_response(page, &page_data));
1172
1173 mock.expect(&[programming::ACK], &[programming::ACK]);
1175
1176 mock.expect(b"E", &[]);
1178
1179 let mut radio = Radio::connect(mock).await.unwrap();
1180 let result = radio.read_page(page).await.unwrap();
1181 assert_eq!(result[0], 0x00);
1182 assert_eq!(result[1], 0xAB);
1183 }
1184
1185 #[tokio::test]
1186 async fn write_single_page_round_trip() {
1187 let mut mock = MockTransport::new();
1188
1189 mock.expect(b"0M PROGRAM\r", b"0M\r");
1191
1192 let page: u16 = 0x0100;
1194 let page_data = [0xCDu8; 256];
1195 let write_cmd = programming::build_write_command(page, &page_data);
1196 mock.expect(&write_cmd, &[programming::ACK]);
1197
1198 mock.expect(b"E", &[]);
1200
1201 let mut radio = Radio::connect(mock).await.unwrap();
1202 radio.write_page(page, &page_data).await.unwrap();
1203 }
1204
1205 #[tokio::test]
1206 async fn write_factory_cal_page_rejected() {
1207 let mock = MockTransport::new();
1208 let mut radio = Radio::connect(mock).await.unwrap();
1209
1210 let data = [0u8; 256];
1211 let result = radio.write_page(0x07A1, &data).await;
1212 assert!(result.is_err());
1213 let err = result.unwrap_err();
1214 assert!(
1215 err.to_string().contains("protected"),
1216 "error should mention protected: {err}"
1217 );
1218 }
1219
1220 #[tokio::test]
1221 async fn write_memory_image_wrong_size_rejected() {
1222 let mock = MockTransport::new();
1223 let mut radio = Radio::connect(mock).await.unwrap();
1224
1225 let bad_image = vec![0u8; 1000]; let result = radio.write_memory_image(&bad_image).await;
1227 assert!(result.is_err());
1228 let err = result.unwrap_err();
1229 assert!(
1230 err.to_string().contains("invalid memory image size"),
1231 "error should mention size: {err}"
1232 );
1233 }
1234
1235 #[tokio::test]
1236 async fn read_memory_pages_small_range() {
1237 let mut mock = MockTransport::new();
1238
1239 mock.expect(b"0M PROGRAM\r", b"0M\r");
1241
1242 for i in 0..2u16 {
1244 let page = 0x0040 + i;
1245 #[allow(clippy::cast_possible_truncation)]
1246 let data = vec![i as u8; 256];
1247 let cmd = programming::build_read_command(page);
1248 mock.expect(&cmd, &build_w_response(page, &data));
1249 mock.expect(&[programming::ACK], &[programming::ACK]);
1250 }
1251
1252 mock.expect(b"E", &[]);
1254
1255 let mut radio = Radio::connect(mock).await.unwrap();
1256 let data = radio.read_memory_pages(0x0040, 2).await.unwrap();
1257 assert_eq!(data.len(), 512);
1258 assert!(data[..256].iter().all(|&b| b == 0x00));
1260 assert!(data[256..].iter().all(|&b| b == 0x01));
1261 }
1262
1263 #[tokio::test]
1264 async fn write_memory_pages_protected_range_rejected() {
1265 let mock = MockTransport::new();
1266 let mut radio = Radio::connect(mock).await.unwrap();
1267
1268 let data = vec![0u8; 768]; let result = radio.write_memory_pages(0x07A0, &data).await;
1271 assert!(result.is_err());
1272 }
1273
1274 #[tokio::test]
1275 async fn read_channel_flags_sequence() {
1276 let mut mock = MockTransport::new();
1277
1278 mock.expect(b"0M PROGRAM\r", b"0M\r");
1280
1281 let page_count = programming::CHANNEL_FLAGS_END - programming::CHANNEL_FLAGS_START + 1;
1283 for i in 0..page_count {
1284 let page = programming::CHANNEL_FLAGS_START + i;
1285 let mut data = vec![0xFF_u8; 256];
1288 if i == 0 {
1289 data[0] = 0x00; data[1] = 0x00; data[2] = 0x00; data[3] = 0xFF;
1294 data[4] = 0x02; data[5] = 0x01; data[6] = 0x05; data[7] = 0xFF;
1299 }
1300 let cmd = programming::build_read_command(page);
1301 mock.expect(&cmd, &build_w_response(page, &data));
1302 mock.expect(&[programming::ACK], &[programming::ACK]);
1303 }
1304
1305 mock.expect(b"E", &[]);
1307
1308 let mut radio = Radio::connect(mock).await.unwrap();
1309 let flags = radio.read_channel_flags().await.unwrap();
1310
1311 assert_eq!(flags.len(), programming::TOTAL_CHANNEL_ENTRIES);
1313
1314 assert!(!flags[0].is_empty());
1316 assert_eq!(flags[0].used, programming::FLAG_VHF);
1317 assert!(!flags[0].lockout);
1318 assert_eq!(flags[0].group, 0);
1319
1320 assert!(!flags[1].is_empty());
1321 assert_eq!(flags[1].used, programming::FLAG_UHF);
1322 assert!(flags[1].lockout);
1323 assert_eq!(flags[1].group, 5);
1324
1325 assert!(flags[2].is_empty());
1327 }
1328
1329 #[tokio::test]
1330 async fn progress_callback_invoked() {
1331 let mut mock = MockTransport::new();
1332
1333 mock.expect(b"0M PROGRAM\r", b"0M\r");
1335
1336 for i in 0..3u16 {
1338 let page = 0x0100 + i;
1339 let data = vec![0u8; 256];
1340 let cmd = programming::build_read_command(page);
1341 mock.expect(&cmd, &build_w_response(page, &data));
1342 mock.expect(&[programming::ACK], &[programming::ACK]);
1343 }
1344
1345 mock.expect(b"E", &[]);
1347
1348 let mut radio = Radio::connect(mock).await.unwrap();
1349
1350 let data = radio.read_memory_pages(0x0100, 3).await.unwrap();
1354 assert_eq!(data.len(), 768);
1355 }
1356
1357 #[tokio::test]
1358 async fn modify_memory_page_read_modify_write() {
1359 let mut mock = MockTransport::new();
1360
1361 let page: u16 = 0x0010;
1363 let byte_index: usize = 0x71; let mut original_data = vec![0u8; 256];
1367 original_data[byte_index] = 0x00; let mut expected_data = original_data.clone();
1371 expected_data[byte_index] = 0x01;
1372
1373 mock.expect(b"0M PROGRAM\r", b"0M\r");
1375
1376 let read_cmd = programming::build_read_command(page);
1378 mock.expect(&read_cmd, &build_w_response(page, &original_data));
1379
1380 mock.expect(&[programming::ACK], &[programming::ACK]);
1382
1383 let expected_array: [u8; 256] = expected_data.clone().try_into().unwrap();
1385 let write_cmd = programming::build_write_command(page, &expected_array);
1386 mock.expect(&write_cmd, &[programming::ACK]);
1387
1388 mock.expect(b"E", &[]);
1390
1391 let mut radio = Radio::connect(mock).await.unwrap();
1392 radio
1393 .modify_memory_page(page, |data| {
1394 data[byte_index] = 0x01;
1395 })
1396 .await
1397 .unwrap();
1398 }
1399
1400 #[tokio::test]
1401 async fn modify_memory_page_factory_cal_rejected() {
1402 let mock = MockTransport::new();
1403 let mut radio = Radio::connect(mock).await.unwrap();
1404
1405 let result = radio
1406 .modify_memory_page(0x07A1, |_data| {
1407 })
1409 .await;
1410 assert!(result.is_err());
1411 let err = result.unwrap_err();
1412 assert!(
1413 err.to_string().contains("protected"),
1414 "error should mention protected: {err}"
1415 );
1416 }
1417
1418 #[tokio::test]
1419 async fn write_channel_name_round_trip() {
1420 let mut mock = MockTransport::new();
1421
1422 let page: u16 = 0x0100;
1424 let offset = 5 * programming::NAME_ENTRY_SIZE;
1425
1426 let original_data = vec![0u8; 256];
1428
1429 let mut expected_data = original_data.clone();
1431 let name = b"TestCh";
1432 expected_data[offset..offset + name.len()].copy_from_slice(name);
1433
1434 mock.expect(b"0M PROGRAM\r", b"0M\r");
1436
1437 let read_cmd = programming::build_read_command(page);
1439 mock.expect(&read_cmd, &build_w_response(page, &original_data));
1440
1441 mock.expect(&[programming::ACK], &[programming::ACK]);
1443
1444 let expected_array: [u8; 256] = expected_data.try_into().unwrap();
1446 let write_cmd = programming::build_write_command(page, &expected_array);
1447 mock.expect(&write_cmd, &[programming::ACK]);
1448
1449 mock.expect(b"E", &[]);
1451
1452 let mut radio = Radio::connect(mock).await.unwrap();
1453 radio.write_channel_name(5, "TestCh").await.unwrap();
1454 }
1455
1456 #[tokio::test]
1457 async fn write_channel_name_out_of_range_rejected() {
1458 let mock = MockTransport::new();
1459 let mut radio = Radio::connect(mock).await.unwrap();
1460
1461 let result = radio.write_channel_name(1200, "Bad").await;
1462 assert!(result.is_err());
1463 let err = result.unwrap_err();
1464 assert!(
1465 err.to_string().contains("out of range"),
1466 "error should mention out of range: {err}"
1467 );
1468 }
1469
1470 #[tokio::test]
1471 async fn write_channel_name_truncates_long_name() {
1472 let mut mock = MockTransport::new();
1473
1474 let page: u16 = 0x0100;
1476 let original_data = vec![0u8; 256];
1477
1478 let long_name = "ABCDEFGHIJKLMNOP"; let mut expected_data = original_data.clone();
1481 expected_data[..15].copy_from_slice(&long_name.as_bytes()[..15]);
1483
1484 mock.expect(b"0M PROGRAM\r", b"0M\r");
1485 let read_cmd = programming::build_read_command(page);
1486 mock.expect(&read_cmd, &build_w_response(page, &original_data));
1487 mock.expect(&[programming::ACK], &[programming::ACK]);
1488 let expected_array: [u8; 256] = expected_data.try_into().unwrap();
1489 let write_cmd = programming::build_write_command(page, &expected_array);
1490 mock.expect(&write_cmd, &[programming::ACK]);
1491 mock.expect(b"E", &[]);
1492
1493 let mut radio = Radio::connect(mock).await.unwrap();
1494 radio.write_channel_name(0, long_name).await.unwrap();
1495 }
1496
1497 #[tokio::test]
1498 async fn read_all_channel_names_returns_1200() {
1499 let mut mock = MockTransport::new();
1500
1501 mock.expect(b"0M PROGRAM\r", b"0M\r");
1503
1504 let first_page_data = build_name_page(&["AllCh0", "AllCh1"]);
1506 let read_cmd = programming::build_read_command(programming::CHANNEL_NAMES_START);
1507 mock.expect(
1508 &read_cmd,
1509 &build_w_response(programming::CHANNEL_NAMES_START, &first_page_data),
1510 );
1511
1512 for page_offset in 1..programming::NAME_ALL_PAGE_COUNT {
1514 mock.expect(&[programming::ACK], &[programming::ACK]);
1515
1516 let page = programming::NAME_START_PAGE + page_offset;
1517 let cmd = programming::build_read_command(page);
1518 let empty = vec![0u8; 256];
1519 mock.expect(&cmd, &build_w_response(page, &empty));
1520 }
1521
1522 mock.expect(&[programming::ACK], &[programming::ACK]);
1524
1525 mock.expect(b"E", &[]);
1527
1528 let mut radio = Radio::connect(mock).await.unwrap();
1529 let names = radio.read_all_channel_names().await.unwrap();
1530
1531 assert_eq!(names.len(), 1200);
1533 assert_eq!(names[0], "AllCh0");
1534 assert_eq!(names[1], "AllCh1");
1535 for name in &names[2..] {
1536 assert!(name.is_empty());
1537 }
1538 }
1539}