1use base64::Engine;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
6pub enum ProtocolError {
7 #[error("message too short: need at least 4 bytes, got {0}")]
9 TooShort(usize),
10 #[error("invalid start byte: expected 0xAA, got 0x{0:02X}")]
12 InvalidStartByte(u8),
13 #[error("invalid base64: {0}")]
15 InvalidBase64(#[from] base64::DecodeError),
16 #[error("length mismatch: header says {expected} bytes, got {actual}")]
18 LengthMismatch {
19 expected: usize,
21 actual: usize,
23 },
24 #[error("unexpected command 0x{0:02X} for RoomCleanStatusResponse (expected 0x15)")]
26 UnexpectedCommand(u8),
27 #[error("payload too short for RoomCleanStatusResponse")]
29 PayloadTooShort,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[repr(u8)]
35pub enum CommandType {
36 SetVirtualWall = 0x12,
38 VirtualWallStatus = 0x13,
40 SetRoomClean = 0x14,
42 RoomCleanStatus = 0x15,
44 RequestAreaClean = 0x17,
46 SetVirtualArea = 0x1A,
48 VirtualAreaStatus = 0x1B,
50 SetZoneClean = 0x28,
52 ZoneCleanStatus = 0x29,
54 CustomizeData = 0x31,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[repr(u8)]
61pub enum ForbiddenMode {
62 FullBan = 0x00,
64 NoSweep = 0x01,
66 NoMop = 0x02,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct ForbiddenZone {
73 pub mode: ForbiddenMode,
75 pub zone: Zone,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct Wall {
84 pub start: (i16, i16),
86 pub end: (i16, i16),
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct RoomCleanCommand {
107 pub clean_times: u8,
109 pub room_ids: Vec<u8>,
111}
112
113impl RoomCleanCommand {
114 pub fn encode(&self) -> Vec<u8> {
116 let cmd: u8 = 0x14;
117 let num_rooms = self.room_ids.len() as u8;
118 let payload_len = 1 + 1 + 1 + self.room_ids.len();
120
121 let mut buf = Vec::with_capacity(3 + payload_len + 1);
122 buf.push(0xAA);
123 buf.push((payload_len >> 8) as u8);
124 buf.push(payload_len as u8);
125 buf.push(cmd);
126 buf.push(self.clean_times);
127 buf.push(num_rooms);
128 buf.extend_from_slice(&self.room_ids);
129
130 let checksum: u8 = buf[3..].iter().copied().fold(0u16, |acc, b| acc + b as u16) as u8;
132 buf.push(checksum);
133 buf
134 }
135
136 pub fn encode_base64(&self) -> String {
138 base64::engine::general_purpose::STANDARD.encode(self.encode())
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct Zone {
149 pub vertices: [(i16, i16); 4],
151}
152
153impl Zone {
154 pub fn rect(x1: i16, y1: i16, x2: i16, y2: i16) -> Self {
170 Zone {
171 vertices: [
172 (x1, y2), (x2, y2), (x2, y1), (x1, y1), ],
177 }
178 }
179
180 pub fn rotated_rect(x1: i16, y1: i16, x2: i16, y2: i16, angle_deg: f64) -> Self {
204 let cx = (x1 as f64 + x2 as f64) / 2.0;
205 let cy = (y1 as f64 + y2 as f64) / 2.0;
206 let cos = angle_deg.to_radians().cos();
207 let sin = angle_deg.to_radians().sin();
208
209 let rotate = |x: f64, y: f64| -> (i16, i16) {
210 let dx = x - cx;
211 let dy = y - cy;
212 let rx = cx + dx * cos - dy * sin;
213 let ry = cy + dx * sin + dy * cos;
214 (rx.round() as i16, ry.round() as i16)
215 };
216
217 let corners: [(f64, f64); 4] = [
218 (x1 as f64, y2 as f64), (x2 as f64, y2 as f64), (x2 as f64, y1 as f64), (x1 as f64, y1 as f64), ];
223
224 Zone {
225 vertices: [
226 rotate(corners[0].0, corners[0].1),
227 rotate(corners[1].0, corners[1].1),
228 rotate(corners[2].0, corners[2].1),
229 rotate(corners[3].0, corners[3].1),
230 ],
231 }
232 }
233}
234
235fn encode_zone_frame(cmd: u8, first_byte: u8, zones: &[Zone]) -> Vec<u8> {
239 let mut payload_len = 1 + 1 + 1;
241 for zone in zones {
242 payload_len += 1 + zone.vertices.len() * 4;
243 }
244
245 let mut buf = Vec::with_capacity(3 + payload_len + 1);
246 buf.push(0xAA);
247 buf.push((payload_len >> 8) as u8);
248 buf.push(payload_len as u8);
249 buf.push(cmd);
250 buf.push(first_byte);
251 buf.push(zones.len() as u8);
252 for zone in zones {
253 buf.push(zone.vertices.len() as u8);
254 for &(x, y) in &zone.vertices {
255 buf.extend_from_slice(&x.to_be_bytes());
256 buf.extend_from_slice(&y.to_be_bytes());
257 }
258 }
259
260 let checksum: u8 = buf[3..].iter().copied().fold(0u16, |acc, b| acc + b as u16) as u8;
262 buf.push(checksum);
263 buf
264}
265
266#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct ZoneCleanCommand {
287 pub clean_times: u8,
289 pub zones: Vec<Zone>,
291}
292
293impl ZoneCleanCommand {
294 pub fn encode(&self) -> Vec<u8> {
300 encode_zone_frame(0x28, self.clean_times, &self.zones)
301 }
302
303 pub fn encode_base64(&self) -> String {
305 base64::engine::general_purpose::STANDARD.encode(self.encode())
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
336pub struct ForbiddenZoneCommand {
337 pub zones: Vec<ForbiddenZone>,
339}
340
341impl ForbiddenZoneCommand {
342 pub fn encode(&self) -> Vec<u8> {
344 let mut payload_len = 1 + 1;
346 for fz in &self.zones {
347 payload_len += 1 + 1 + fz.zone.vertices.len() * 4;
348 }
349
350 let mut buf = Vec::with_capacity(3 + payload_len + 1);
351 buf.push(0xAA);
352 buf.push((payload_len >> 8) as u8);
353 buf.push(payload_len as u8);
354 buf.push(0x1A);
355 buf.push(self.zones.len() as u8);
356 for fz in &self.zones {
357 buf.push(fz.mode as u8);
358 buf.push(fz.zone.vertices.len() as u8);
359 for &(x, y) in &fz.zone.vertices {
360 buf.extend_from_slice(&x.to_be_bytes());
361 buf.extend_from_slice(&y.to_be_bytes());
362 }
363 }
364
365 let checksum: u8 = buf[3..].iter().copied().fold(0u16, |acc, b| acc + b as u16) as u8;
366 buf.push(checksum);
367 buf
368 }
369
370 pub fn encode_base64(&self) -> String {
372 base64::engine::general_purpose::STANDARD.encode(self.encode())
373 }
374
375 pub fn clear() -> Self {
377 Self { zones: vec![] }
378 }
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
404pub struct VirtualWallCommand {
405 pub walls: Vec<Wall>,
407}
408
409impl VirtualWallCommand {
410 pub fn encode(&self) -> Vec<u8> {
412 let payload_len = 1 + 1 + self.walls.len() * 8;
414
415 let mut buf = Vec::with_capacity(3 + payload_len + 1);
416 buf.push(0xAA);
417 buf.push((payload_len >> 8) as u8);
418 buf.push(payload_len as u8);
419 buf.push(0x12);
420 buf.push(self.walls.len() as u8);
421 for wall in &self.walls {
422 buf.extend_from_slice(&wall.start.0.to_be_bytes());
423 buf.extend_from_slice(&wall.start.1.to_be_bytes());
424 buf.extend_from_slice(&wall.end.0.to_be_bytes());
425 buf.extend_from_slice(&wall.end.1.to_be_bytes());
426 }
427
428 let checksum: u8 = buf[3..].iter().copied().fold(0u16, |acc, b| acc + b as u16) as u8;
429 buf.push(checksum);
430 buf
431 }
432
433 pub fn encode_base64(&self) -> String {
435 base64::engine::general_purpose::STANDARD.encode(self.encode())
436 }
437
438 pub fn clear() -> Self {
440 Self { walls: vec![] }
441 }
442}
443
444pub fn build_sweeper_frame(cmd: u8, data: &[u8]) -> Vec<u8> {
448 let payload_len = 1 + data.len();
449 let mut frame = Vec::with_capacity(3 + payload_len + 1);
450 frame.push(0xAA);
451 frame.push((payload_len >> 8) as u8);
452 frame.push(payload_len as u8);
453 frame.push(cmd);
454 frame.extend_from_slice(data);
455 let checksum: u8 = frame[3..]
456 .iter()
457 .copied()
458 .fold(0u16, |acc, b| acc + b as u16) as u8;
459 frame.push(checksum);
460 frame
461}
462
463#[derive(Debug, Clone, PartialEq, Eq)]
465pub struct SweeperMessage {
466 pub cmd: u8,
468 pub data: Vec<u8>,
470 pub checksum_ok: bool,
472}
473
474impl SweeperMessage {
475 pub fn decode(bytes: &[u8]) -> Result<Self, ProtocolError> {
488 if bytes.len() < 4 {
489 return Err(ProtocolError::TooShort(bytes.len()));
490 }
491 if bytes[0] != 0xAA {
492 return Err(ProtocolError::InvalidStartByte(bytes[0]));
493 }
494
495 let payload_len = ((bytes[1] as usize) << 8) | (bytes[2] as usize);
496 let expected_total = 3 + payload_len + 1;
498 if bytes.len() < expected_total {
499 return Err(ProtocolError::LengthMismatch {
500 expected: expected_total,
501 actual: bytes.len(),
502 });
503 }
504
505 let cmd = bytes[3];
506 let data = bytes[4..3 + payload_len].to_vec();
507 let received_checksum = bytes[3 + payload_len];
508
509 let computed: u8 = bytes[3..3 + payload_len]
510 .iter()
511 .copied()
512 .fold(0u16, |acc, b| acc + b as u16) as u8;
513
514 Ok(SweeperMessage {
515 cmd,
516 data,
517 checksum_ok: computed == received_checksum,
518 })
519 }
520
521 pub fn decode_base64(s: &str) -> Result<Self, ProtocolError> {
533 let bytes = base64::engine::general_purpose::STANDARD.decode(s)?;
534 Self::decode(&bytes)
535 }
536}
537
538#[derive(Debug, Clone, PartialEq, Eq)]
540pub struct RoomCleanStatusResponse {
541 pub clean_times: u8,
543 pub num_rooms: u8,
545 pub room_ids: Vec<u8>,
547}
548
549impl TryFrom<&SweeperMessage> for RoomCleanStatusResponse {
550 type Error = ProtocolError;
551
552 fn try_from(msg: &SweeperMessage) -> Result<Self, Self::Error> {
553 if msg.cmd != 0x15 {
554 return Err(ProtocolError::UnexpectedCommand(msg.cmd));
555 }
556 if msg.data.len() < 2 {
557 return Err(ProtocolError::PayloadTooShort);
558 }
559 let clean_times = msg.data[0];
560 let num_rooms = msg.data[1];
561 let room_ids = msg.data[2..].to_vec();
562 Ok(RoomCleanStatusResponse {
563 clean_times,
564 num_rooms,
565 room_ids,
566 })
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
575 fn encode_single_room() {
576 let cmd = RoomCleanCommand {
577 clean_times: 1,
578 room_ids: vec![4],
579 };
580 assert_eq!(
581 cmd.encode(),
582 vec![0xAA, 0x00, 0x04, 0x14, 0x01, 0x01, 0x04, 0x1A]
583 );
584 }
585
586 #[test]
587 fn encode_multi_room() {
588 let cmd = RoomCleanCommand {
589 clean_times: 2,
590 room_ids: vec![0, 2, 3],
591 };
592 let encoded = cmd.encode();
593 assert_eq!(encoded[0], 0xAA);
595 assert_eq!(encoded[1], 0x00);
596 assert_eq!(encoded[2], 0x06); assert_eq!(encoded[3], 0x14);
598 assert_eq!(encoded[4], 0x02);
599 assert_eq!(encoded[5], 0x03);
600 assert_eq!(encoded[6], 0x00);
601 assert_eq!(encoded[7], 0x02);
602 assert_eq!(encoded[8], 0x03);
603 assert_eq!(encoded[9], 0x1E);
605 }
606
607 #[test]
608 fn decode_room_clean_status() {
609 let bytes = [0xAA, 0x00, 0x04, 0x15, 0x01, 0x01, 0x04, 0x1B];
610 let msg = SweeperMessage::decode(&bytes).unwrap();
611 assert_eq!(msg.cmd, 0x15);
612 assert_eq!(msg.data, vec![0x01, 0x01, 0x04]);
613 assert!(msg.checksum_ok);
614 }
615
616 #[test]
617 fn decode_base64_room_clean_status() {
618 let msg = SweeperMessage::decode_base64("qgAEFQEBBBs=").unwrap();
619 assert_eq!(msg.cmd, 0x15);
620 assert_eq!(msg.data, vec![0x01, 0x01, 0x04]);
621 assert!(msg.checksum_ok);
622 }
623
624 #[test]
625 fn decode_bad_checksum() {
626 let bytes = [0xAA, 0x00, 0x04, 0x15, 0x01, 0x01, 0x04, 0xFF];
627 let msg = SweeperMessage::decode(&bytes).unwrap();
628 assert!(!msg.checksum_ok);
629 }
630
631 #[test]
632 fn decode_too_short() {
633 assert!(SweeperMessage::decode(&[0xAA, 0x00]).is_err());
634 }
635
636 #[test]
637 fn decode_bad_start_byte() {
638 assert!(SweeperMessage::decode(&[0xBB, 0x00, 0x01, 0x14, 0x14]).is_err());
639 }
640
641 #[test]
642 fn room_clean_status_try_from() {
643 let msg = SweeperMessage {
644 cmd: 0x15,
645 data: vec![0x01, 0x01, 0x04],
646 checksum_ok: true,
647 };
648 let resp = RoomCleanStatusResponse::try_from(&msg).unwrap();
649 assert_eq!(resp.clean_times, 1);
650 assert_eq!(resp.num_rooms, 1);
651 assert_eq!(resp.room_ids, vec![4]);
652 }
653
654 #[test]
655 fn room_clean_status_wrong_cmd() {
656 let msg = SweeperMessage {
657 cmd: 0x14,
658 data: vec![1, 1, 4],
659 checksum_ok: true,
660 };
661 assert!(RoomCleanStatusResponse::try_from(&msg).is_err());
662 }
663
664 #[test]
665 fn multi_room_encode_decode_roundtrip() {
666 let cmd = RoomCleanCommand {
667 clean_times: 2,
668 room_ids: vec![0, 2, 3],
669 };
670 let encoded = cmd.encode();
671 let b64 = cmd.encode_base64();
672
673 let msg = SweeperMessage::decode(&encoded).unwrap();
674 assert!(msg.checksum_ok);
675 assert_eq!(msg.cmd, 0x14);
676
677 let msg2 = SweeperMessage::decode_base64(&b64).unwrap();
678 assert_eq!(msg, msg2);
679 }
680
681 #[test]
684 fn zone_encode_sala_zone() {
685 let cmd = ZoneCleanCommand {
689 clean_times: 1,
690 zones: vec![Zone {
691 vertices: [(82, 203), (453, 203), (453, -13), (82, -13)],
692 }],
693 };
694 let encoded = cmd.encode();
695 let expected: Vec<u8> = vec![
696 0xAA, 0x00, 0x14, 0x28, 0x01, 0x01, 0x04, 0x00, 0x52, 0x00, 0xCB, 0x01, 0xC5, 0x00, 0xCB, 0x01, 0xC5, 0xFF, 0xF3, 0x00, 0x52, 0xFF, 0xF3, 0xD8, ];
711 assert_eq!(encoded, expected);
712 }
713
714 #[test]
715 fn zone_rect_helper() {
716 let zone = Zone::rect(82, -13, 453, 203);
719 assert_eq!(
720 zone.vertices,
721 [
722 (82, 203), (453, 203), (453, -13), (82, -13), ]
727 );
728 }
729
730 #[test]
731 fn zone_rotated_rect_zero_angle() {
732 let plain = Zone::rect(100, 200, 300, 400);
734 let rotated = Zone::rotated_rect(100, 200, 300, 400, 0.0);
735 assert_eq!(plain.vertices, rotated.vertices);
736 }
737
738 #[test]
739 fn zone_rotated_rect_90_degrees() {
740 let zone = Zone::rotated_rect(100, 200, 300, 400, 90.0);
742 let [v0, v1, v2, v3] = zone.vertices;
746 let dist = |a: (i16, i16), b: (i16, i16)| {
747 let dx = (b.0 - a.0) as f64;
748 let dy = (b.1 - a.1) as f64;
749 (dx * dx + dy * dy).sqrt()
750 };
751 let side_a = dist(v0, v1);
752 let side_b = dist(v1, v2);
753 assert!((dist(v0, v1) - dist(v2, v3)).abs() < 1.0);
755 assert!((dist(v1, v2) - dist(v3, v0)).abs() < 1.0);
756 assert!((dist(v0, v2) - dist(v1, v3)).abs() < 1.0);
758 assert!((side_a - side_b).abs() < 1.0);
760 }
761
762 #[test]
763 fn zone_rotated_rect_small_angle() {
764 let zone = Zone::rotated_rect(82, -13, 453, 203, 2.68);
766 let [v0, v1, v2, v3] = zone.vertices;
767 let dist = |a: (i16, i16), b: (i16, i16)| {
769 let dx = (b.0 - a.0) as f64;
770 let dy = (b.1 - a.1) as f64;
771 (dx * dx + dy * dy).sqrt()
772 };
773 assert!((dist(v0, v1) - dist(v2, v3)).abs() < 2.0);
774 assert!((dist(v1, v2) - dist(v3, v0)).abs() < 2.0);
775 let plain = Zone::rect(82, -13, 453, 203);
777 assert_ne!(zone.vertices, plain.vertices);
778 let center_r = (
780 zone.vertices.iter().map(|v| v.0 as i32).sum::<i32>(),
781 zone.vertices.iter().map(|v| v.1 as i32).sum::<i32>(),
782 );
783 let center_p = (
784 plain.vertices.iter().map(|v| v.0 as i32).sum::<i32>(),
785 plain.vertices.iter().map(|v| v.1 as i32).sum::<i32>(),
786 );
787 assert!((center_r.0 - center_p.0).abs() <= 2);
788 assert!((center_r.1 - center_p.1).abs() <= 2);
789 }
790
791 #[test]
792 fn zone_encode_single() {
793 let cmd = ZoneCleanCommand {
794 clean_times: 1,
795 zones: vec![Zone::rect(82, -13, 453, 203)],
796 };
797 let encoded = cmd.encode();
798 assert_eq!(encoded[0], 0xAA);
799 assert_eq!(encoded[1], 0x00);
801 assert_eq!(encoded[2], 0x14);
802 assert_eq!(encoded[3], 0x28); assert_eq!(encoded[4], 0x01); assert_eq!(encoded[5], 0x01); assert_eq!(encoded[6], 0x04); assert_eq!(encoded.len(), 24);
808 let sum: u16 = encoded[3..encoded.len() - 1]
810 .iter()
811 .map(|&b| b as u16)
812 .sum();
813 assert_eq!(encoded.last().copied().unwrap(), (sum & 0xFF) as u8);
814 }
815
816 #[test]
817 fn zone_encode_multi() {
818 let cmd = ZoneCleanCommand {
819 clean_times: 2,
820 zones: vec![
821 Zone::rect(100, 200, 300, 400),
822 Zone::rect(500, 600, 700, 800),
823 ],
824 };
825 let encoded = cmd.encode();
826 assert_eq!(encoded[1], 0x00);
828 assert_eq!(encoded[2], 0x25);
829 assert_eq!(encoded[3], 0x28); assert_eq!(encoded[4], 0x02); assert_eq!(encoded[5], 0x02); assert_eq!(encoded[6], 0x04); assert_eq!(encoded.len(), 41);
835 let sum: u16 = encoded[3..encoded.len() - 1]
837 .iter()
838 .map(|&b| b as u16)
839 .sum();
840 assert_eq!(encoded.last().copied().unwrap(), (sum & 0xFF) as u8);
841 }
842
843 #[test]
844 fn zone_encode_negative_coords() {
845 let cmd = ZoneCleanCommand {
846 clean_times: 1,
847 zones: vec![Zone::rect(-100, -200, 100, 200)],
848 };
849 let encoded = cmd.encode();
850 assert_eq!(encoded[7], 0xFF);
852 assert_eq!(encoded[8], 0x9C);
853 assert_eq!(encoded[9], 0x00);
854 assert_eq!(encoded[10], 0xC8);
855 }
856
857 #[test]
858 fn zone_encode_decode_roundtrip() {
859 let cmd = ZoneCleanCommand {
860 clean_times: 1,
861 zones: vec![Zone::rect(82, -13, 453, 203)],
862 };
863 let encoded = cmd.encode();
864 let msg = SweeperMessage::decode(&encoded).unwrap();
865 assert!(msg.checksum_ok);
866 assert_eq!(msg.cmd, 0x28);
867 assert_eq!(msg.data.len(), 19);
869 assert_eq!(msg.data[0], 1); assert_eq!(msg.data[1], 1); assert_eq!(msg.data[2], 4); }
873
874 #[test]
877 fn forbidden_zone_encode_no_sweep() {
878 let cmd = ForbiddenZoneCommand {
880 zones: vec![ForbiddenZone {
881 mode: ForbiddenMode::NoSweep,
882 zone: Zone::rect(82, -13, 453, 203),
883 }],
884 };
885 let encoded = cmd.encode();
886 assert_eq!(encoded[3], 0x1A);
887 assert_eq!(encoded[5], 0x01); assert_eq!(encoded[7], 0x00);
890 assert_eq!(encoded[8], 0x52); }
892
893 #[test]
894 fn forbidden_zone_matches_verified_frame() {
895 let cmd = ForbiddenZoneCommand {
898 zones: vec![ForbiddenZone {
899 mode: ForbiddenMode::FullBan,
900 zone: Zone::rect(82, -13, 453, 203),
901 }],
902 };
903 let encoded = cmd.encode();
904 let expected: Vec<u8> = vec![
905 0xAA, 0x00, 0x14, 0x1A, 0x01, 0x00, 0x04, 0x00, 0x52, 0x00, 0xCB, 0x01, 0xC5, 0x00,
906 0xCB, 0x01, 0xC5, 0xFF, 0xF3, 0x00, 0x52, 0xFF, 0xF3, 0xC9,
907 ];
908 assert_eq!(encoded, expected);
909 }
910
911 #[test]
912 fn forbidden_zone_clear() {
913 let cmd = ForbiddenZoneCommand::clear();
915 let encoded = cmd.encode();
916 assert_eq!(encoded, vec![0xAA, 0x00, 0x02, 0x1A, 0x00, 0x1A]);
917 }
918
919 #[test]
920 fn forbidden_zone_encode_decode_roundtrip() {
921 let cmd = ForbiddenZoneCommand {
922 zones: vec![ForbiddenZone {
923 mode: ForbiddenMode::NoSweep,
924 zone: Zone::rect(100, 200, 300, 400),
925 }],
926 };
927 let encoded = cmd.encode();
928 let msg = SweeperMessage::decode(&encoded).unwrap();
929 assert!(msg.checksum_ok);
930 assert_eq!(msg.cmd, 0x1A);
931 assert_eq!(msg.data[0], 0x01); assert_eq!(msg.data[1], 0x01); assert_eq!(msg.data[2], 0x04); }
935
936 #[test]
937 fn forbidden_zone_multi_mode() {
938 let cmd = ForbiddenZoneCommand {
940 zones: vec![
941 ForbiddenZone {
942 mode: ForbiddenMode::FullBan,
943 zone: Zone::rect(0, 0, 100, 100),
944 },
945 ForbiddenZone {
946 mode: ForbiddenMode::NoSweep,
947 zone: Zone::rect(200, 200, 300, 300),
948 },
949 ],
950 };
951 let encoded = cmd.encode();
952 assert_eq!(encoded[3], 0x1A);
953 assert_eq!(encoded[4], 0x02); assert_eq!(encoded[5], 0x00); assert_eq!(encoded[6], 0x04); assert_eq!(encoded[23], 0x01); assert_eq!(encoded[24], 0x04); }
960
961 #[test]
964 fn virtual_wall_encode_horizontal() {
965 let cmd = VirtualWallCommand {
967 walls: vec![Wall {
968 start: (100, 100),
969 end: (400, 100),
970 }],
971 };
972 let encoded = cmd.encode();
973 let expected: Vec<u8> = vec![
974 0xAA, 0x00, 0x0A, 0x12, 0x01, 0x00, 0x64, 0x00, 0x64, 0x01, 0x90, 0x00, 0x64, 0xD0, ];
981 assert_eq!(encoded, expected);
982 }
983
984 #[test]
985 fn virtual_wall_encode_diagonal() {
986 let cmd = VirtualWallCommand {
988 walls: vec![Wall {
989 start: (100, -100),
990 end: (400, 200),
991 }],
992 };
993 let encoded = cmd.encode();
994 assert_eq!(encoded[3], 0x12);
995 assert_eq!(encoded[4], 0x01); assert_eq!(encoded[5], 0x00);
998 assert_eq!(encoded[6], 0x64);
999 assert_eq!(encoded[7], 0xFF);
1000 assert_eq!(encoded[8], 0x9C);
1001 assert_eq!(encoded[9], 0x01);
1003 assert_eq!(encoded[10], 0x90);
1004 assert_eq!(encoded[11], 0x00);
1005 assert_eq!(encoded[12], 0xC8);
1006 let sum: u16 = encoded[3..encoded.len() - 1]
1007 .iter()
1008 .map(|&b| b as u16)
1009 .sum();
1010 assert_eq!(encoded.last().copied().unwrap(), (sum & 0xFF) as u8);
1011 }
1012
1013 #[test]
1014 fn virtual_wall_clear() {
1015 let cmd = VirtualWallCommand::clear();
1017 let encoded = cmd.encode();
1018 assert_eq!(encoded, vec![0xAA, 0x00, 0x02, 0x12, 0x00, 0x12]);
1019 }
1020
1021 #[test]
1022 fn virtual_wall_encode_decode_roundtrip() {
1023 let cmd = VirtualWallCommand {
1024 walls: vec![Wall {
1025 start: (-50, 100),
1026 end: (300, -200),
1027 }],
1028 };
1029 let encoded = cmd.encode();
1030 let msg = SweeperMessage::decode(&encoded).unwrap();
1031 assert!(msg.checksum_ok);
1032 assert_eq!(msg.cmd, 0x12);
1033 assert_eq!(msg.data[0], 0x01); assert_eq!(msg.data.len(), 9); }
1036
1037 #[test]
1038 fn virtual_wall_multi() {
1039 let cmd = VirtualWallCommand {
1040 walls: vec![
1041 Wall {
1042 start: (0, 0),
1043 end: (100, 0),
1044 },
1045 Wall {
1046 start: (0, 0),
1047 end: (0, 100),
1048 },
1049 ],
1050 };
1051 let encoded = cmd.encode();
1052 assert_eq!(encoded[2], 0x12);
1054 assert_eq!(encoded[4], 0x02); assert_eq!(encoded.len(), 3 + 18 + 1); let sum: u16 = encoded[3..encoded.len() - 1]
1057 .iter()
1058 .map(|&b| b as u16)
1059 .sum();
1060 assert_eq!(encoded.last().copied().unwrap(), (sum & 0xFF) as u8);
1061 }
1062}