//! Canonical wire-byte vectors anchored to `PROTOCOL.md`. //! //! These are hand-encoded byte sequences taken directly from the spec. //! They lock the firmware and host implementations to the *written* //! protocol, not just to each other — so a refactor that accidentally //! redefines a field layout fails here before it ships. //! //! Every vector is expressed twice: as a hex `&[u8]` decoded into a //! typed value, and as a typed value encoded back into bytes. Byte-for- //! byte equality in both directions is the invariant. #![allow(clippy::unwrap_used, clippy::panic)] use donglora_protocol::{ Bandwidth, Command, ErrorCode, RADIO_CONFIG_SIZE, RadioConfig, Response, TX_POWER_MAX, }; use heapless::Vec as HVec; // ── A well-known RadioConfig with hex-clean field values ────────────── // // Fields are picked so each multi-byte field has a visually obvious // little-endian byte pattern, and the config as a whole still validates // (freq in 150–960 MHz range, sf in 5–12, cr in 5–8). const KNOWN_FREQ_HZ: u32 = 0x1234_5678; // 305_419_896 Hz ≈ 305 MHz const KNOWN_SYNC: u16 = 0xABCD; const KNOWN_PREAMBLE: u16 = 0x00AB; /// Matches the layout documented in `PROTOCOL.md §RadioConfig (13 bytes)`. const CONFIG_BYTES: [u8; RADIO_CONFIG_SIZE] = [ 0x78, 0x56, 0x34, 0x12, // freq_hz = 0x12345678 LE 0x07, // bw = Khz125 0x07, // sf = 7 0x05, // cr = 5 (4/5) 0xCD, 0xAB, // sync_word = 0xABCD LE 0x16, // tx_power_dbm = 22 0xAB, 0x00, // preamble_len = 0x00AB LE 0x01, // cad = enabled ]; fn known_config() -> RadioConfig { RadioConfig { freq_hz: KNOWN_FREQ_HZ, bw: Bandwidth::Khz125, sf: 7, cr: 5, sync_word: KNOWN_SYNC, tx_power_dbm: 22, preamble_len: KNOWN_PREAMBLE, cad: 1, } } // ── RadioConfig ───────────────────────────────────────────────────── #[test] fn radio_config_canonical_bytes_decode() { assert_eq!(RadioConfig::from_bytes(&CONFIG_BYTES), Some(known_config())); } #[test] fn radio_config_canonical_bytes_encode() { let mut buf = [0u8; RADIO_CONFIG_SIZE]; let n = known_config().write_to(&mut buf); assert_eq!(n, RADIO_CONFIG_SIZE); assert_eq!(buf, CONFIG_BYTES); } #[test] fn radio_config_tx_power_max_sentinel_is_minus_128() { // PROTOCOL.md: "Set tx_power_dbm to -128 (0x80) to use the board's maximum." assert_eq!(TX_POWER_MAX, -128); let cfg = RadioConfig { tx_power_dbm: TX_POWER_MAX, ..known_config() }; let mut buf = [0u8; RADIO_CONFIG_SIZE]; cfg.write_to(&mut buf); assert_eq!(buf[9], 0x80, "TX_POWER_MAX must serialize as 0x80"); } #[test] fn radio_config_preamble_default_sentinel_is_zero_le() { // PROTOCOL.md: "Set preamble_len to 0 to use the firmware default." let cfg = RadioConfig { preamble_len: 0, ..known_config() }; let mut buf = [0u8; RADIO_CONFIG_SIZE]; cfg.write_to(&mut buf); assert_eq!(&buf[10..12], &[0x00, 0x00]); } #[test] fn bandwidth_table_matches_protocol_md() { // PROTOCOL.md: 0..=9 enumerated in order 7.8…500 kHz. let expected = [ (0u8, Bandwidth::Khz7), (1, Bandwidth::Khz10), (2, Bandwidth::Khz15), (3, Bandwidth::Khz20), (4, Bandwidth::Khz31), (5, Bandwidth::Khz41), (6, Bandwidth::Khz62), (7, Bandwidth::Khz125), (8, Bandwidth::Khz250), (9, Bandwidth::Khz500), ]; let mut buf = [0u8; RADIO_CONFIG_SIZE]; for (wire, bw) in expected { RadioConfig { bw, ..known_config() } .write_to(&mut buf); assert_eq!(buf[4], wire, "bandwidth {:?} must wire as {}", bw, wire); let decoded = RadioConfig::from_bytes(&buf).unwrap(); assert_eq!(decoded.bw, bw); } } #[test] fn bandwidth_invalid_values_reject() { // PROTOCOL.md restricts bw to 0..=9. let mut buf = CONFIG_BYTES; for bad in [10u8, 11, 100, 255] { buf[4] = bad; assert!( RadioConfig::from_bytes(&buf).is_none(), "bandwidth {bad} must not decode", ); } } // ── Commands ──────────────────────────────────────────────────────── #[test] fn command_tags_match_protocol_md() { // Tag table from PROTOCOL.md §Commands. Unit-variant parse-only. assert_eq!(Command::from_bytes(&[0]), Some(Command::Ping)); assert_eq!(Command::from_bytes(&[1]), Some(Command::GetConfig)); assert_eq!(Command::from_bytes(&[3]), Some(Command::StartRx)); assert_eq!(Command::from_bytes(&[4]), Some(Command::StopRx)); assert_eq!(Command::from_bytes(&[6]), Some(Command::DisplayOn)); assert_eq!(Command::from_bytes(&[7]), Some(Command::DisplayOff)); assert_eq!(Command::from_bytes(&[8]), Some(Command::GetMac)); } #[test] fn command_set_config_canonical_bytes() { // tag(2) + CONFIG_BYTES let mut bytes = [0u8; 1 + RADIO_CONFIG_SIZE]; bytes[0] = 2; bytes[1..].copy_from_slice(&CONFIG_BYTES); assert_eq!( Command::from_bytes(&bytes), Some(Command::SetConfig(known_config())) ); } #[test] fn command_transmit_no_config_canonical_bytes() { // PROTOCOL.md §Commands tag 5: has_config(1) + [RadioConfig if 1] + len(u16 LE) + payload let bytes = [ 0x05, // Transmit 0x00, // has_config = false 0x05, 0x00, // len = 5 LE b'h', b'e', b'l', b'l', b'o', ]; match Command::from_bytes(&bytes).unwrap() { Command::Transmit { config, payload } => { assert!(config.is_none()); assert_eq!(payload.as_slice(), b"hello"); } other => panic!("expected Transmit, got {:?}", other), } } #[test] fn command_transmit_with_config_canonical_bytes() { let mut bytes: HVec = HVec::new(); bytes.push(0x05).unwrap(); // Transmit bytes.push(0x01).unwrap(); // has_config = true bytes.extend_from_slice(&CONFIG_BYTES).unwrap(); bytes.extend_from_slice(&[0x02, 0x00]).unwrap(); // len = 2 bytes.extend_from_slice(b"OK").unwrap(); match Command::from_bytes(&bytes).unwrap() { Command::Transmit { config, payload } => { assert_eq!(config, Some(known_config())); assert_eq!(payload.as_slice(), b"OK"); } other => panic!("expected Transmit, got {:?}", other), } } #[test] fn command_unknown_tags_reject() { // Defined tags: 0..=8. Everything else must reject. for bad in [9u8, 10, 100, 255] { assert!(Command::from_bytes(&[bad]).is_none()); } } // ── Responses ─────────────────────────────────────────────────────── fn encode_response_bytes(r: Response) -> HVec { let mut buf = [0u8; 128]; let n = r.write_to(&mut buf); HVec::from_slice(&buf[..n]).unwrap() } #[test] fn response_canonical_unit_tags() { assert_eq!(encode_response_bytes(Response::Pong).as_slice(), &[0x00]); assert_eq!(encode_response_bytes(Response::TxDone).as_slice(), &[0x03]); assert_eq!(encode_response_bytes(Response::Ok).as_slice(), &[0x04]); } #[test] fn response_config_canonical_bytes() { let mut expected = [0u8; 1 + RADIO_CONFIG_SIZE]; expected[0] = 0x01; // Config tag expected[1..].copy_from_slice(&CONFIG_BYTES); assert_eq!( encode_response_bytes(Response::Config(known_config())).as_slice(), &expected ); } #[test] fn response_rx_packet_canonical_bytes() { let mut payload: HVec = HVec::new(); payload.extend_from_slice(b"data").unwrap(); let bytes = encode_response_bytes(Response::RxPacket { rssi: -80, snr: 10, payload, }); // tag(2) + rssi(-80 = 0xFFB0 LE) + snr(10 LE) + len(4 LE) + "data" let expected: &[u8] = &[ 0x02, 0xB0, 0xFF, 0x0A, 0x00, 0x04, 0x00, b'd', b'a', b't', b'a', ]; assert_eq!(bytes.as_slice(), expected); } #[test] fn response_error_canonical_bytes() { // PROTOCOL.md §Error codes: InvalidConfig=0, RadioBusy=1, TxTimeout=2, // NotConfigured=4, NoDisplay=5. Tag 3 is reserved (was CrcError). let pairs = [ (ErrorCode::InvalidConfig, 0u8), (ErrorCode::RadioBusy, 1), (ErrorCode::TxTimeout, 2), (ErrorCode::NotConfigured, 4), (ErrorCode::NoDisplay, 5), ]; for (code, byte) in pairs { assert_eq!( encode_response_bytes(Response::Error(code)).as_slice(), &[0x05, byte], ); } } #[test] fn response_mac_address_canonical_bytes() { let mac = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]; assert_eq!( encode_response_bytes(Response::MacAddress(mac)).as_slice(), &[0x06, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], ); }