use super::calc_crc8;
use super::types::{Command, ErrorCode};
use crate::{Result, V4Error};
const STX: u8 = 0xA5;
const MAX_PAYLOAD_SIZE: usize = 512;
#[derive(Debug, Clone)]
pub struct Frame {
pub command: Command,
pub payload: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
pub error_code: ErrorCode,
pub word_indices: Vec<u16>,
pub data: Vec<u8>,
}
impl Frame {
pub fn new(command: Command, payload: Vec<u8>) -> Result<Self> {
if payload.len() > MAX_PAYLOAD_SIZE {
return Err(V4Error::Protocol(format!(
"Payload too large: {} bytes (max {})",
payload.len(),
MAX_PAYLOAD_SIZE
)));
}
Ok(Self { command, payload })
}
pub fn encode(&self) -> Vec<u8> {
let length = self.payload.len() as u16;
let mut frame = Vec::with_capacity(5 + self.payload.len());
frame.push(STX);
frame.push((length & 0xFF) as u8);
frame.push(((length >> 8) & 0xFF) as u8);
frame.push(self.command as u8);
frame.extend_from_slice(&self.payload);
let crc = calc_crc8(&frame[1..]);
frame.push(crc);
frame
}
pub fn decode_response(data: &[u8]) -> Result<Response> {
if data.len() < 5 {
return Err(V4Error::Protocol(format!(
"Response too short: {} bytes (expected at least 5)",
data.len()
)));
}
if data[0] != STX {
return Err(V4Error::Protocol(format!(
"Invalid STX: {:#04x} (expected {:#04x})",
data[0], STX
)));
}
let length = u16::from_le_bytes([data[1], data[2]]) as usize;
let expected_frame_len = 4 + length;
if data.len() < expected_frame_len {
return Err(V4Error::Protocol(format!(
"Response too short: {} bytes (expected {})",
data.len(),
expected_frame_len
)));
}
let err_code = data[3];
let payload_start = 4;
let payload_end = 4 + length - 1; let payload = &data[payload_start..payload_end];
let expected_crc = calc_crc8(&data[1..payload_end]);
let actual_crc = data[payload_end];
if expected_crc != actual_crc {
return Err(V4Error::CrcMismatch {
expected: expected_crc,
actual: actual_crc,
});
}
let err_code = ErrorCode::from_u8(err_code)
.ok_or_else(|| V4Error::Protocol(format!("Unknown error code: {:#04x}", err_code)))?;
let word_indices = if !payload.is_empty() {
let word_count = payload[0] as usize;
let mut indices = Vec::with_capacity(word_count);
for i in 0..word_count {
let offset = 1 + i * 2;
if offset + 1 < payload.len() {
let idx = u16::from_le_bytes([payload[offset], payload[offset + 1]]);
indices.push(idx);
}
}
indices
} else {
Vec::new()
};
Ok(Response {
error_code: err_code,
word_indices,
data: payload.to_vec(),
})
}
}
pub struct FrameBuilder {
command: Command,
payload: Vec<u8>,
}
impl FrameBuilder {
pub fn new(command: Command) -> Self {
Self {
command,
payload: Vec::new(),
}
}
pub fn payload(mut self, payload: Vec<u8>) -> Self {
self.payload = payload;
self
}
pub fn build(self) -> Result<Frame> {
Frame::new(self.command, self.payload)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ping_frame_encoding() {
let frame = Frame::new(Command::Ping, vec![]).unwrap();
let encoded = frame.encode();
assert_eq!(encoded[0], 0xA5); assert_eq!(encoded[1], 0x00); assert_eq!(encoded[2], 0x00); assert_eq!(encoded[3], 0x20); assert_eq!(encoded.len(), 5);
let expected_crc = calc_crc8(&[0x00, 0x00, 0x20]);
assert_eq!(encoded[4], expected_crc);
}
#[test]
fn test_exec_frame_with_payload() {
let payload = vec![0x42, 0x43];
let frame = Frame::new(Command::Exec, payload.clone()).unwrap();
let encoded = frame.encode();
assert_eq!(encoded[0], 0xA5); assert_eq!(encoded[1], 0x02); assert_eq!(encoded[2], 0x00); assert_eq!(encoded[3], 0x10); assert_eq!(encoded[4], 0x42); assert_eq!(encoded[5], 0x43);
assert_eq!(encoded.len(), 7);
let expected_crc = calc_crc8(&[0x02, 0x00, 0x10, 0x42, 0x43]);
assert_eq!(encoded[6], expected_crc);
}
#[test]
fn test_response_decode_ok() {
let response_data = vec![0x01, 0x00, 0x00]; let crc = calc_crc8(&response_data);
let mut response = vec![0xA5];
response.extend_from_slice(&response_data);
response.push(crc);
let result = Frame::decode_response(&response).unwrap();
assert_eq!(result.error_code, ErrorCode::Ok);
assert_eq!(result.word_indices.len(), 0);
}
#[test]
fn test_response_decode_error() {
let response_data = vec![0x01, 0x00, 0x01]; let crc = calc_crc8(&response_data);
let mut response = vec![0xA5];
response.extend_from_slice(&response_data);
response.push(crc);
let result = Frame::decode_response(&response).unwrap();
assert_eq!(result.error_code, ErrorCode::Error);
assert_eq!(result.word_indices.len(), 0);
}
#[test]
fn test_response_decode_with_word_index() {
let response_data = vec![0x04, 0x00, 0x00, 0x01, 0x00, 0x00]; let crc = calc_crc8(&response_data);
let mut response = vec![0xA5];
response.extend_from_slice(&response_data);
response.push(crc);
let result = Frame::decode_response(&response).unwrap();
assert_eq!(result.error_code, ErrorCode::Ok);
assert_eq!(result.word_indices.len(), 1);
assert_eq!(result.word_indices[0], 0);
}
#[test]
fn test_response_decode_crc_mismatch() {
let response = vec![0xA5, 0x01, 0x00, 0x00, 0xFF];
let result = Frame::decode_response(&response);
assert!(matches!(result, Err(V4Error::CrcMismatch { .. })));
}
#[test]
fn test_payload_too_large() {
let payload = vec![0; MAX_PAYLOAD_SIZE + 1];
let result = Frame::new(Command::Exec, payload);
assert!(matches!(result, Err(V4Error::Protocol(_))));
}
#[test]
fn test_frame_builder() {
let frame = FrameBuilder::new(Command::Reset)
.payload(vec![])
.build()
.unwrap();
assert_eq!(frame.command as u8, 0xFF);
assert_eq!(frame.payload.len(), 0);
}
}