Files

348 lines
10 KiB
Rust

use dd3_protocol::{
crc16_ccitt, decode_ack_down_payload, decode_batch_v3, decode_frame, encode_ack_down_payload,
encode_batch_v3, encode_frame, push_chunk, AckDownPayload, BatchInputV3, FrameDecodeError, MsgKind,
ReassemblyState, ReassemblyStatus,
};
const FIX_FRAME_OK: &[u8] = include_bytes!("../../../fixtures/protocol/frames/batchup_f19c_payload_0102a5.bin");
const FIX_FRAME_BAD_CRC: &[u8] =
include_bytes!("../../../fixtures/protocol/frames/batchup_f19c_payload_0102a5_bad_crc.bin");
const FIX_CHUNK_OK: &[u8] = include_bytes!("../../../fixtures/protocol/chunks/in_order_ok.bin");
const FIX_CHUNK_MISSING: &[u8] = include_bytes!("../../../fixtures/protocol/chunks/missing_chunk.bin");
const FIX_CHUNK_OUT_OF_ORDER: &[u8] =
include_bytes!("../../../fixtures/protocol/chunks/out_of_order_start.bin");
const FIX_CHUNK_WRONG_LEN: &[u8] =
include_bytes!("../../../fixtures/protocol/chunks/wrong_total_len.bin");
const FIX_SYNC_EMPTY: &[u8] = include_bytes!("../../../fixtures/protocol/payload_v3/sync_empty.bin");
const FIX_SPARSE_5: &[u8] = include_bytes!("../../../fixtures/protocol/payload_v3/sparse_5.bin");
const FIX_FULL_30: &[u8] = include_bytes!("../../../fixtures/protocol/payload_v3/full_30.bin");
fn parse_chunk_records(bytes: &[u8]) -> Vec<(u16, u8, u8, u16, Vec<u8>)> {
let mut out = Vec::new();
let mut pos = 0usize;
while pos < bytes.len() {
let batch_id = u16::from_le_bytes([bytes[pos], bytes[pos + 1]]);
pos += 2;
let idx = bytes[pos];
pos += 1;
let count = bytes[pos];
pos += 1;
let total_len = u16::from_le_bytes([bytes[pos], bytes[pos + 1]]);
pos += 2;
let chunk_len = bytes[pos] as usize;
pos += 1;
let chunk = bytes[pos..pos + chunk_len].to_vec();
pos += chunk_len;
out.push((batch_id, idx, count, total_len, chunk));
}
out
}
fn fill_sparse_batch() -> BatchInputV3 {
let mut input = BatchInputV3::default();
input.sender_id = 1;
input.batch_id = 42;
input.t_last = 1_700_000_000;
input.present_mask = (1u32 << 0) | (1u32 << 2) | (1u32 << 3) | (1u32 << 10) | (1u32 << 29);
input.n = 5;
input.battery_mv = 3750;
input.err_m = 2;
input.err_d = 1;
input.err_tx = 3;
input.err_last = 2;
input.err_rx_reject = 1;
input.energy_wh[0] = 100_000;
input.energy_wh[1] = 100_001;
input.energy_wh[2] = 100_050;
input.energy_wh[3] = 100_050;
input.energy_wh[4] = 100_200;
input.p1_w[0] = -120;
input.p1_w[1] = -90;
input.p1_w[2] = 1910;
input.p1_w[3] = -90;
input.p1_w[4] = 500;
input.p2_w[0] = 50;
input.p2_w[1] = -1950;
input.p2_w[2] = 60;
input.p2_w[3] = 2060;
input.p2_w[4] = -10;
input.p3_w[0] = 0;
input.p3_w[1] = 10;
input.p3_w[2] = -1990;
input.p3_w[3] = 10;
input.p3_w[4] = 20;
input
}
fn fill_full_batch() -> BatchInputV3 {
let mut input = BatchInputV3::default();
input.sender_id = 1;
input.batch_id = 0xBEEF;
input.t_last = 1_769_904_999;
input.present_mask = 0x3FFF_FFFF;
input.n = 30;
input.battery_mv = 4095;
input.err_m = 10;
input.err_d = 20;
input.err_tx = 30;
input.err_last = 3;
input.err_rx_reject = 6;
for i in 0..30usize {
input.energy_wh[i] = 500_000 + (i as u32 * i as u32 * 3);
input.p1_w[i] = -1000 + (i as i16 * 25);
input.p2_w[i] = 500 - (i as i16 * 30);
input.p3_w[i] = if i % 2 == 0 {
100 + (i as i16 * 5)
} else {
-100 + (i as i16 * 5)
};
}
input
}
#[test]
fn crc16_known_vectors() {
assert_eq!(0x29B1, crc16_ccitt(b"123456789"));
assert_eq!(0x1C0F, crc16_ccitt(&[0x00, 0x01, 0x02, 0x03, 0x04]));
}
#[test]
fn frame_encode_decode_and_crc_reject() {
let encoded = encode_frame(MsgKind::BatchUp, 0xF19C, &[0x01, 0x02, 0xA5]);
assert_eq!(FIX_FRAME_OK, encoded.as_slice());
let frame = decode_frame(FIX_FRAME_OK, MsgKind::AckDown as u8).unwrap();
assert_eq!(MsgKind::BatchUp, frame.msg_kind);
assert_eq!(0xF19C, frame.short_id);
assert_eq!(&[0x01, 0x02, 0xA5], frame.payload.as_slice());
let err = decode_frame(FIX_FRAME_BAD_CRC, MsgKind::AckDown as u8).unwrap_err();
assert_eq!(FrameDecodeError::CrcFail, err);
}
#[test]
fn frame_rejects_invalid_msg_kind_and_short_length() {
let mut frame = encode_frame(MsgKind::BatchUp, 0xF19C, &[0x42]);
frame[0] = 2;
let crc = crc16_ccitt(&frame[..frame.len() - 2]);
let n = frame.len();
frame[n - 2] = (crc >> 8) as u8;
frame[n - 1] = (crc & 0xFF) as u8;
let bad_kind = decode_frame(&frame, MsgKind::AckDown as u8).unwrap_err();
assert_eq!(FrameDecodeError::InvalidMsgKind, bad_kind);
let short_len = decode_frame(&frame[..4], MsgKind::AckDown as u8).unwrap_err();
assert_eq!(FrameDecodeError::LengthMismatch, short_len);
}
#[test]
fn ack_payload_fixed_7byte_contract() {
let payload = AckDownPayload {
time_valid: true,
batch_id: 0x1234,
epoch_utc: 1_769_904_000,
};
let encoded = encode_ack_down_payload(payload);
assert_eq!(7, encoded.len());
assert_eq!(1, encoded[0]);
assert_eq!([0x12, 0x34], [encoded[1], encoded[2]]);
let decoded = decode_ack_down_payload(&encoded).unwrap();
assert_eq!(payload, decoded);
assert!(decode_ack_down_payload(&encoded[..6]).is_err());
}
#[test]
fn chunk_reassembly_in_order_success() {
let records = parse_chunk_records(FIX_CHUNK_OK);
let mut state = ReassemblyState::default();
let mut buffer = [0u8; 32];
let mut status = ReassemblyStatus::InProgress;
for (i, rec) in records.iter().enumerate() {
status = push_chunk(
&mut state,
rec.0,
rec.1,
rec.2,
rec.3,
&rec.4,
1000 + (i as u32 * 100),
5000,
32,
&mut buffer,
);
}
assert_eq!(ReassemblyStatus::Complete { complete_len: 7 }, status);
assert_eq!(&[1, 2, 3, 4, 5, 6, 7], &buffer[..7]);
assert!(!state.active);
}
#[test]
fn chunk_reassembly_missing_or_out_of_order_fails_deterministically() {
let records = parse_chunk_records(FIX_CHUNK_MISSING);
let mut state = ReassemblyState::default();
let mut buffer = [0u8; 32];
let s0 = push_chunk(
&mut state,
records[0].0,
records[0].1,
records[0].2,
records[0].3,
&records[0].4,
1000,
5000,
32,
&mut buffer,
);
assert_eq!(ReassemblyStatus::InProgress, s0);
let s1 = push_chunk(
&mut state,
records[1].0,
records[1].1,
records[1].2,
records[1].3,
&records[1].4,
1100,
5000,
32,
&mut buffer,
);
assert_eq!(ReassemblyStatus::ErrorReset, s1);
assert!(!state.active);
let out_records = parse_chunk_records(FIX_CHUNK_OUT_OF_ORDER);
let s2 = push_chunk(
&mut state,
out_records[0].0,
out_records[0].1,
out_records[0].2,
out_records[0].3,
&out_records[0].4,
1200,
5000,
32,
&mut buffer,
);
assert_eq!(ReassemblyStatus::ErrorReset, s2);
}
#[test]
fn chunk_reassembly_wrong_total_length_fails() {
let records = parse_chunk_records(FIX_CHUNK_WRONG_LEN);
let mut state = ReassemblyState::default();
let mut buffer = [0u8; 8];
let s0 = push_chunk(
&mut state,
records[0].0,
records[0].1,
records[0].2,
records[0].3,
&records[0].4,
1000,
5000,
8,
&mut buffer,
);
assert_eq!(ReassemblyStatus::InProgress, s0);
let s1 = push_chunk(
&mut state,
records[1].0,
records[1].1,
records[1].2,
records[1].3,
&records[1].4,
1100,
5000,
8,
&mut buffer,
);
assert_eq!(ReassemblyStatus::ErrorReset, s1);
assert!(!state.active);
}
#[test]
fn payload_golden_vectors_roundtrip() {
let decoded_sync = decode_batch_v3(FIX_SYNC_EMPTY).unwrap();
assert_eq!(1, decoded_sync.sender_id);
assert_eq!(0x1234, decoded_sync.batch_id);
assert_eq!(1_769_904_100, decoded_sync.t_last);
assert_eq!(0, decoded_sync.present_mask);
assert_eq!(0, decoded_sync.n);
assert_eq!(3750, decoded_sync.battery_mv);
let decoded_sparse = decode_batch_v3(FIX_SPARSE_5).unwrap();
assert_eq!(fill_sparse_batch(), decoded_sparse);
let decoded_full = decode_batch_v3(FIX_FULL_30).unwrap();
assert_eq!(fill_full_batch(), decoded_full);
assert_eq!(FIX_SYNC_EMPTY, encode_batch_v3(&decoded_sync).unwrap().as_slice());
assert_eq!(FIX_SPARSE_5, encode_batch_v3(&decoded_sparse).unwrap().as_slice());
assert_eq!(FIX_FULL_30, encode_batch_v3(&decoded_full).unwrap().as_slice());
}
#[test]
fn payload_decode_rejects_bad_magic_schema_flags() {
let mut bad_magic = FIX_SPARSE_5.to_vec();
bad_magic[0] = 0x00;
assert!(decode_batch_v3(&bad_magic).is_err());
let mut bad_schema = FIX_SPARSE_5.to_vec();
bad_schema[2] = 0x02;
assert!(decode_batch_v3(&bad_schema).is_err());
let mut bad_flags = FIX_SPARSE_5.to_vec();
bad_flags[3] = 0x00;
assert!(decode_batch_v3(&bad_flags).is_err());
}
#[test]
fn payload_decode_rejects_truncated_and_trailing() {
assert!(decode_batch_v3(&FIX_SPARSE_5[..FIX_SPARSE_5.len() - 1]).is_err());
assert!(decode_batch_v3(&FIX_SPARSE_5[..12]).is_err());
let mut with_tail = FIX_SPARSE_5.to_vec();
with_tail.push(0xAA);
assert!(decode_batch_v3(&with_tail).is_err());
}
#[test]
fn payload_encode_decode_reject_invalid_mask_and_n() {
let mut input = fill_sparse_batch();
input.present_mask = 0x4000_0000;
assert!(encode_batch_v3(&input).is_err());
let mut input2 = fill_sparse_batch();
input2.n = 31;
assert!(encode_batch_v3(&input2).is_err());
let mut input3 = fill_sparse_batch();
input3.n = 0;
input3.present_mask = 1;
assert!(encode_batch_v3(&input3).is_err());
let mut invalid_bits = FIX_SPARSE_5.to_vec();
invalid_bits[15] |= 0x40;
assert!(decode_batch_v3(&invalid_bits).is_err());
let mut bitcount_mismatch = FIX_SPARSE_5.to_vec();
bitcount_mismatch[16] = 0x01;
assert!(decode_batch_v3(&bitcount_mismatch).is_err());
}