//! Appendix C wire vectors from `PROTOCOL.md`. //! //! Each test pins one or more complete wire-framed byte sequences from //! the spec. If a future refactor accidentally redefines a field layout //! or a CRC variant, the test fails with a concrete hex diff against //! the normative document — not just against the crate's own round-trip. #![allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)] use donglora_protocol::{ Command, DeviceMessage, ErrorCode, FrameDecoder, FrameResult, Info, LoRaBandwidth, LoRaCodingRate, LoRaConfig, LoRaHeaderMode, MAX_WIRE_FRAME, Modulation, OkPayload, Owner, RadioChipId, RxOrigin, RxPayload, SetConfigResult, SetConfigResultCode, TxDonePayload, TxFlags, TxResult, cap, commands, encode_frame, }; use heapless::Vec as HVec; // ── Helpers ───────────────────────────────────────────────────────── /// Encode a command as a complete wire-ready frame, tag-correlated. fn encode_cmd_frame(cmd: &Command, tag: u16) -> HVec { let mut payload_buf = [0u8; 320]; let payload_len = cmd.encode_payload(&mut payload_buf).unwrap(); let mut wire = [0u8; MAX_WIRE_FRAME]; let n = encode_frame(cmd.type_id(), tag, &payload_buf[..payload_len], &mut wire).unwrap(); let mut out = HVec::new(); out.extend_from_slice(&wire[..n]).unwrap(); out } /// Encode a device message as a complete wire-ready frame, tag-correlated. fn encode_dev_frame(msg: &DeviceMessage, tag: u16) -> HVec { let mut payload_buf = [0u8; 320]; let payload_len = msg.encode_payload(&mut payload_buf).unwrap(); let mut wire = [0u8; MAX_WIRE_FRAME]; let n = encode_frame(msg.type_id(), tag, &payload_buf[..payload_len], &mut wire).unwrap(); let mut out = HVec::new(); out.extend_from_slice(&wire[..n]).unwrap(); out } /// Decode a single frame out of `wire` and assert everything about it. fn decode_single_frame(wire: &[u8]) -> (u8, u16, HVec) { let mut decoder = FrameDecoder::new(); let mut out: Option<(u8, u16, HVec)> = None; decoder.feed(wire, |res| match res { FrameResult::Ok { type_id, tag, payload, } => { let mut p = HVec::new(); p.extend_from_slice(payload).unwrap(); out = Some((type_id, tag, p)); } FrameResult::Err(e) => panic!("decode error: {:?}", e), }); out.expect("expected one frame") } // ── §C.2.1 PING / OK ──────────────────────────────────────────────── #[test] fn c21_ping_encodes_to_spec_bytes() { // H→D PING tag=0x0001 → wire: 03 01 01 03 9D C8 00 let wire = encode_cmd_frame(&Command::Ping, 0x0001); assert_eq!(wire.as_slice(), &[0x03, 0x01, 0x01, 0x03, 0x9D, 0xC8, 0x00]); } #[test] fn c21_ok_encodes_to_spec_bytes() { // D→H OK tag=0x0001 → wire: 03 80 01 03 F7 C4 00 let wire = encode_dev_frame(&DeviceMessage::Ok(OkPayload::Empty), 0x0001); assert_eq!(wire.as_slice(), &[0x03, 0x80, 0x01, 0x03, 0xF7, 0xC4, 0x00]); } #[test] fn c21_decode_roundtrip() { let (type_id, tag, payload) = decode_single_frame(&[0x03, 0x01, 0x01, 0x03, 0x9D, 0xC8, 0x00]); assert_eq!(type_id, commands::TYPE_PING); assert_eq!(tag, 0x0001); assert_eq!(payload.as_slice(), &[]); assert_eq!(Command::parse(type_id, &payload).unwrap(), Command::Ping); } // ── §C.2.3 SET_CONFIG LoRa EU868 ──────────────────────────────────── fn c23_lora() -> LoRaConfig { LoRaConfig { freq_hz: 868_100_000, sf: 7, bw: LoRaBandwidth::Khz125, cr: LoRaCodingRate::Cr4_5, preamble_len: 8, sync_word: 0x1424, tx_power_dbm: 14, header_mode: LoRaHeaderMode::Explicit, payload_crc: true, iq_invert: false, } } #[test] fn c23_set_config_encodes_to_spec_bytes() { // H→D SET_CONFIG tag=0x0003 → wire: // 03 03 03 08 01 A0 27 BE 33 07 07 02 08 04 24 14 0E 02 01 03 D9 1F 00 let cmd = Command::SetConfig(Modulation::LoRa(c23_lora())); let wire = encode_cmd_frame(&cmd, 0x0003); let expected: &[u8] = &[ 0x03, 0x03, 0x03, 0x08, 0x01, 0xA0, 0x27, 0xBE, 0x33, 0x07, 0x07, 0x02, 0x08, 0x04, 0x24, 0x14, 0x0E, 0x02, 0x01, 0x03, 0xD9, 0x1F, 0x00, ]; assert_eq!(wire.as_slice(), expected); } #[test] fn c23_set_config_ok_response() { // D→H OK tag=0x0003 result=APPLIED owner=MINE current=[same LoRa] let result = SetConfigResult { result: SetConfigResultCode::Applied, owner: Owner::Mine, current: Modulation::LoRa(c23_lora()), }; let msg = DeviceMessage::Ok(OkPayload::SetConfig(result)); let wire = encode_dev_frame(&msg, 0x0003); let expected: &[u8] = &[ 0x03, 0x80, 0x03, 0x01, 0x09, 0x01, 0x01, 0xA0, 0x27, 0xBE, 0x33, 0x07, 0x07, 0x02, 0x08, 0x04, 0x24, 0x14, 0x0E, 0x02, 0x01, 0x03, 0xC8, 0x91, 0x00, ]; assert_eq!(wire.as_slice(), expected); } // ── §C.2.4 TX with default CAD, packet "Hello" ────────────────────── #[test] fn c24_tx_with_cad() { // H→D TX tag=0x0004 flags=0x00 data="Hello" // wire: 03 04 04 01 08 48 65 6C 6C 6F 26 40 00 let mut data = HVec::new(); data.extend_from_slice(b"Hello").unwrap(); let cmd = Command::Tx { flags: TxFlags::default(), data, }; let wire = encode_cmd_frame(&cmd, 0x0004); let expected: &[u8] = &[ 0x03, 0x04, 0x04, 0x01, 0x08, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x26, 0x40, 0x00, ]; assert_eq!(wire.as_slice(), expected); } #[test] fn c24_tx_ok() { let wire = encode_dev_frame(&DeviceMessage::Ok(OkPayload::Empty), 0x0004); assert_eq!(wire.as_slice(), &[0x03, 0x80, 0x04, 0x03, 0x02, 0x3B, 0x00]); } #[test] fn c24_tx_done() { let td = TxDonePayload { result: TxResult::Transmitted, airtime_us: 30_976, }; let wire = encode_dev_frame(&DeviceMessage::TxDone(td), 0x0004); let expected: &[u8] = &[ 0x03, 0xC1, 0x04, 0x01, 0x01, 0x02, 0x79, 0x01, 0x03, 0xE3, 0xFA, 0x00, ]; assert_eq!(wire.as_slice(), expected); } // ── §C.2.5 TX with skip_cad ───────────────────────────────────────── #[test] fn c25_tx_skip_cad() { // H→D TX tag=0x0005 flags=0x01 data="URGENT" // wire: 03 04 05 0A 01 55 52 47 45 4E 54 DB 1C 00 let mut data = HVec::new(); data.extend_from_slice(b"URGENT").unwrap(); let cmd = Command::Tx { flags: TxFlags { skip_cad: true }, data, }; let wire = encode_cmd_frame(&cmd, 0x0005); let expected: &[u8] = &[ 0x03, 0x04, 0x05, 0x0A, 0x01, 0x55, 0x52, 0x47, 0x45, 0x4E, 0x54, 0xDB, 0x1C, 0x00, ]; assert_eq!(wire.as_slice(), expected); } #[test] fn c25_tx_done() { let td = TxDonePayload { result: TxResult::Transmitted, airtime_us: 33_792, }; let wire = encode_dev_frame(&DeviceMessage::TxDone(td), 0x0005); let expected: &[u8] = &[ 0x03, 0xC1, 0x05, 0x01, 0x01, 0x02, 0x84, 0x01, 0x03, 0x81, 0xE3, 0x00, ]; assert_eq!(wire.as_slice(), expected); } // ── §C.2.6 RX event ───────────────────────────────────────────────── #[test] fn c26_rx_start() { let wire = encode_cmd_frame(&Command::RxStart, 0x0006); assert_eq!(wire.as_slice(), &[0x03, 0x05, 0x06, 0x03, 0xCA, 0x8D, 0x00]); } #[test] fn c26_rx_event() { let mut data = HVec::new(); data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]).unwrap(); let rx = RxPayload { rssi_tenths_dbm: -735, snr_tenths_db: 95, freq_err_hz: -125, timestamp_us: 42_000_000, crc_valid: true, packets_dropped: 0, origin: RxOrigin::Ota, data, }; let wire = encode_dev_frame(&DeviceMessage::Rx(rx), 0x0000); let expected: &[u8] = &[ 0x02, 0xC0, 0x01, 0x04, 0x21, 0xFD, 0x5F, 0x09, 0x83, 0xFF, 0xFF, 0xFF, 0x80, 0xDE, 0x80, 0x02, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x07, 0x01, 0x02, 0x03, 0x04, 0xB9, 0x8E, 0x00, ]; assert_eq!(wire.as_slice(), expected); } #[test] fn c26_rx_stop() { let wire = encode_cmd_frame(&Command::RxStop, 0x0008); assert_eq!(wire.as_slice(), &[0x03, 0x06, 0x08, 0x03, 0x95, 0xF7, 0x00]); } // ── §C.5.1 ERR(ENOTCONFIGURED) ────────────────────────────────────── #[test] fn c51_err_notconfigured() { let wire = encode_dev_frame(&DeviceMessage::Err(ErrorCode::ENotConfigured), 0x0028); let expected: &[u8] = &[0x03, 0x81, 0x28, 0x02, 0x03, 0x03, 0x53, 0x7E, 0x00]; assert_eq!(wire.as_slice(), expected); } // ── §C.5.3 ERR(EPARAM) ────────────────────────────────────────────── #[test] fn c53_err_eparam() { let wire = encode_dev_frame(&DeviceMessage::Err(ErrorCode::EParam), 0x002A); let expected: &[u8] = &[0x03, 0x81, 0x2A, 0x02, 0x01, 0x03, 0x59, 0xF5, 0x00]; assert_eq!(wire.as_slice(), expected); } // ── §C.5.4 ERR(EBUSY) ─────────────────────────────────────────────── #[test] fn c54_err_ebusy() { let wire = encode_dev_frame(&DeviceMessage::Err(ErrorCode::EBusy), 0x002B); let expected: &[u8] = &[0x03, 0x81, 0x2B, 0x02, 0x06, 0x03, 0x7A, 0x1A, 0x00]; assert_eq!(wire.as_slice(), expected); } // ── §C.5.6 Unknown command type byte ──────────────────────────────── #[test] fn c56_unknown_command_type_encodes_to_spec() { // The host sends type=0x10 with payload DE AD. We can't use // Command::encode_payload (no such variant), but the frame encoder // is type-agnostic so we can directly produce the wire bytes. let mut wire = [0u8; MAX_WIRE_FRAME]; let n = encode_frame(0x10, 0x003C, &[0xDE, 0xAD], &mut wire).unwrap(); let expected: &[u8] = &[0x03, 0x10, 0x3C, 0x05, 0xDE, 0xAD, 0xE2, 0x24, 0x00]; assert_eq!(&wire[..n], expected); } #[test] fn c56_unknown_command_decoder_still_parses_frame() { // The decoder is payload-agnostic: it yields (type_id=0x10, tag=0x003C, // payload=DE AD). The command parser then rejects it cleanly with // UnknownType so the caller can respond with ERR(EUNKNOWN_CMD). let wire: &[u8] = &[0x03, 0x10, 0x3C, 0x05, 0xDE, 0xAD, 0xE2, 0x24, 0x00]; let (type_id, tag, payload) = decode_single_frame(wire); assert_eq!(type_id, 0x10); assert_eq!(tag, 0x003C); assert_eq!(payload.as_slice(), &[0xDE, 0xAD]); assert!(matches!( Command::parse(type_id, &payload), Err(donglora_protocol::CommandParseError::UnknownType) )); } #[test] fn c56_err_eunknown_cmd_reply() { // D→H ERR tag=0x003C code=0x0005 // on wire: 03 81 3C 02 05 03 A3 05 00 let wire = encode_dev_frame(&DeviceMessage::Err(ErrorCode::EUnknownCmd), 0x003C); let expected: &[u8] = &[0x03, 0x81, 0x3C, 0x02, 0x05, 0x03, 0xA3, 0x05, 0x00]; assert_eq!(wire.as_slice(), expected); } // ── §C.6.2 RX with crc_valid = 0 ──────────────────────────────────── #[test] fn c62_rx_bad_crc_delivered_anyway() { let mut data = HVec::new(); data.extend_from_slice(&[0x41, 0x42, 0xFF, 0x00, 0xCC]) .unwrap(); let rx = RxPayload { rssi_tenths_dbm: -1050, snr_tenths_db: -80, freq_err_hz: 2200, timestamp_us: 120_000_000, crc_valid: false, packets_dropped: 0, origin: RxOrigin::Ota, data, }; let wire = encode_dev_frame(&DeviceMessage::Rx(rx), 0x0000); let expected: &[u8] = &[ 0x02, 0xC0, 0x01, 0x07, 0xE6, 0xFB, 0xB0, 0xFF, 0x98, 0x08, 0x01, 0x01, 0x04, 0x0E, 0x27, 0x07, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x04, 0x41, 0x42, 0xFF, 0x04, 0xCC, 0x04, 0x3C, 0x00, ]; assert_eq!(wire.as_slice(), expected); } // ── §C.6.4 Async ERR(EFRAME) ──────────────────────────────────────── #[test] fn c64_async_err_eframe() { let wire = encode_dev_frame(&DeviceMessage::Err(ErrorCode::EFrame), 0x0000); let expected: &[u8] = &[0x02, 0x81, 0x01, 0x05, 0x02, 0x01, 0xCE, 0xEF, 0x00]; assert_eq!(wire.as_slice(), expected); } // ── §C.6.5 Async ERR(ERADIO) ──────────────────────────────────────── #[test] fn c65_async_err_eradio() { let wire = encode_dev_frame(&DeviceMessage::Err(ErrorCode::ERadio), 0x0000); let expected: &[u8] = &[0x02, 0x81, 0x01, 0x05, 0x01, 0x01, 0x9D, 0xBA, 0x00]; assert_eq!(wire.as_slice(), expected); } // ── §C.8.1 COBS edge: no zeros in body ────────────────────────────── #[test] fn c81_ping_no_zeros_in_body() { // H→D PING tag=0x0101 → no zero bytes in tag bytes 01 01. // wire: 06 01 01 01 BC D8 00 let wire = encode_cmd_frame(&Command::Ping, 0x0101); assert_eq!(wire.as_slice(), &[0x06, 0x01, 0x01, 0x01, 0xBC, 0xD8, 0x00]); } // ── §C.2.2 GET_INFO ───────────────────────────────────────────────── #[test] fn c22_get_info_request() { let wire = encode_cmd_frame(&Command::GetInfo, 0x0002); assert_eq!(wire.as_slice(), &[0x03, 0x02, 0x02, 0x03, 0x9E, 0xC4, 0x00]); } #[test] fn c22_get_info_response() { // Full SX1262 GET_INFO response encoded as a complete wire frame. let mut mcu = [0u8; donglora_protocol::MAX_MCU_UID_LEN]; mcu[..8].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67]); let info = Info { proto_major: 1, proto_minor: 0, fw_major: 0, fw_minor: 1, fw_patch: 0, radio_chip_id: RadioChipId::Sx1262.as_u16(), capability_bitmap: cap::LORA | cap::FSK | cap::CAD_BEFORE_TX, supported_sf_bitmap: 0x1FE0, supported_bw_bitmap: 0x03FF, max_payload_bytes: 255, rx_queue_capacity: 64, tx_queue_capacity: 16, freq_min_hz: 150_000_000, freq_max_hz: 960_000_000, tx_power_min_dbm: -9, tx_power_max_dbm: 22, mcu_uid_len: 8, mcu_uid: mcu, radio_uid_len: 0, radio_uid: [0u8; donglora_protocol::MAX_RADIO_UID_LEN], }; let wire = encode_dev_frame(&DeviceMessage::Ok(OkPayload::Info(info)), 0x0002); let expected: &[u8] = &[ 0x03, 0x80, 0x02, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x06, 0xE0, 0x1F, 0xFF, 0x03, 0xFF, 0x02, 0x40, 0x02, 0x10, 0x05, 0x80, 0xD1, 0xF0, 0x08, 0x0F, 0x70, 0x38, 0x39, 0xF7, 0x16, 0x08, 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x03, 0xFA, 0xA4, 0x00, ]; assert_eq!(wire.as_slice(), expected); } // ── Decode-side roundtrip for the key command shapes ─────────────── #[test] fn decode_tx_roundtrip() { let mut data = HVec::new(); data.extend_from_slice(b"Hello").unwrap(); let cmd = Command::Tx { flags: TxFlags::default(), data, }; let wire = encode_cmd_frame(&cmd, 0x0004); let (type_id, tag, payload) = decode_single_frame(&wire); assert_eq!(type_id, commands::TYPE_TX); assert_eq!(tag, 0x0004); assert_eq!(Command::parse(type_id, &payload).unwrap(), cmd); } #[test] fn decode_set_config_roundtrip() { let cmd = Command::SetConfig(Modulation::LoRa(c23_lora())); let wire = encode_cmd_frame(&cmd, 0x0003); let (type_id, tag, payload) = decode_single_frame(&wire); assert_eq!(type_id, commands::TYPE_SET_CONFIG); assert_eq!(tag, 0x0003); assert_eq!(Command::parse(type_id, &payload).unwrap(), cmd); } #[test] fn decode_rx_roundtrip() { let mut data = HVec::new(); data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]).unwrap(); let rx = RxPayload { rssi_tenths_dbm: -735, snr_tenths_db: 95, freq_err_hz: -125, timestamp_us: 42_000_000, crc_valid: true, packets_dropped: 0, origin: RxOrigin::Ota, data: data.clone(), }; let wire = encode_dev_frame(&DeviceMessage::Rx(rx.clone()), 0x0000); let (type_id, tag, payload) = decode_single_frame(&wire); assert_eq!(type_id, 0xC0); assert_eq!(tag, 0x0000); let decoded = DeviceMessage::parse(type_id, &payload, None).unwrap(); assert_eq!(decoded, DeviceMessage::Rx(rx)); }