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)> { 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()); }