Files
protocol-rs/tests/vectors.rs
T
swaits 6acc12cde8 test: canonical wire-byte vectors anchored to PROTOCOL.md
Hand-encoded byte sequences for RadioConfig, every Command tag, and
every Response tag, taken directly from the spec. Fields use hex-clean
values (freq_hz = 0x12345678, sync_word = 0xABCD, etc.) so each
little-endian byte is visually verifiable.

Locks firmware and host to the written spec — a refactor that quietly
changes a field layout fails here, not in the field.
2026-04-13 12:12:12 -06:00

276 lines
8.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<u8, 64> = 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<u8, 128> {
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<u8, 256> = 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],
);
}