use crate::serial_core::error::CodecError; use crate::serial_core::frame::{ FrameHandler, TactileAFrameMetaData, TactileARepFrame, TactileAReqFrame, }; use crate::serial_core::record::{write_csv, CsvExporter, CsvImporter, RecordedFrame, Recording}; use crate::serial_core::utils::{calc_crc8_itu, elapsed_millis}; use crate::serial_core::{ codec::Codec, frame::{TactileAFrame, TactileAFrameStatusCode}, }; use async_trait::async_trait; use csv::StringRecord; use anyhow::anyhow; use std::io::Read; use log::debug; const FRAME_BUFFER_MIN_LENGTH: usize = 15; pub struct TactileACodec { buffer: Vec, expected_data_len: usize, } pub struct TactileACsvExporter { channels: usize, } pub struct TactileACsvImporter { channels: usize, data_row: usize, packets: Vec, } pub struct TactileAHandler; #[derive(Clone)] pub struct TactileADataPacket { pub data: Vec, pub dts_ms: u64, } impl From for TactileAFrameStatusCode { fn from(value: u8) -> Self { match value { 0 => TactileAFrameStatusCode::Success, _ => TactileAFrameStatusCode::Failure, } } } impl TryFrom<&TactileARepFrame> for TactileADataPacket { type Error = CodecError; fn try_from(value: &TactileARepFrame) -> Result { let data = TactileACodec::parse_data_frame(&value.payload)?; let dts_ms = value.dts_ms; Ok(TactileADataPacket { data: data, dts_ms: dts_ms, }) } } impl TactileACodec { pub fn new(cols: usize, rows: usize) -> TactileACodec { Self { buffer: Vec::new(), expected_data_len: cols * rows * 2, } } pub fn parse_data_frame(data: &[u8]) -> Result, CodecError> { if data.len() % 2 != 0 { return Err(CodecError::InvalidLength); } let vals: Vec = data .chunks_exact(2) .map(|chunk| { let raw = u16::from_le_bytes([chunk[0], chunk[1]]) as i32; if raw < 15 { 0 } else { raw } }) .collect::>(); Ok(vals) } pub fn build_req_frame(cols: usize, rows: usize) -> anyhow::Result { let header = [0x55, 0xAA]; let payload_len: usize = 9; let device_addr: u8 = 0x34; let extend_code: u8 = 0x00; let func_code: u8 = 0xFB; let start_addr: u32 = 7168; let except_data_len: usize = cols * rows * 2; let checksum: u8 = 0; Ok(TactileAFrame::Req(TactileAReqFrame { meta: TactileAFrameMetaData { header, payload_len, device_addr, extend_code, func_code, start_addr, except_data_len, checksum, }, })) } } impl Codec for TactileACodec { fn decode( &mut self, input: &[u8], session_started_at: std::time::Instant, ) -> Result, CodecError> { self.buffer.extend_from_slice(input); let mut frames: Vec = Vec::new(); loop { if self.buffer.len() < FRAME_BUFFER_MIN_LENGTH { break; } let header_pos = self.buffer.windows(2).position(|w| w == [0xAA, 0x55]); let Some(pos) = header_pos else { self.buffer.clear(); break; }; if pos > 0 { self.buffer.drain(0..pos); } if self.buffer.len() < FRAME_BUFFER_MIN_LENGTH { break; } let header = [self.buffer[0], self.buffer[1]]; let payload_len = u16::from_le_bytes([self.buffer[2], self.buffer[3]]) as usize; let device_addr = self.buffer[4]; let extend_code = self.buffer[5]; let func_code = self.buffer[6]; let start_addr = u32::from_le_bytes([ self.buffer[7], self.buffer[8], self.buffer[9], self.buffer[10], ]); let except_data_len = u16::from_le_bytes([self.buffer[11], self.buffer[12]]) as usize; let status = TactileAFrameStatusCode::from(self.buffer[13]); if except_data_len != self.expected_data_len { debug!( "unexpected payload length: expected {}, got {}, buffer_len={}", self.expected_data_len, except_data_len, self.buffer.len() ); self.buffer.drain(0..1); continue; } let frame_length = except_data_len + FRAME_BUFFER_MIN_LENGTH; if self.buffer.len() < frame_length { break; } let need_check_data = self.buffer[0..14 + except_data_len].to_vec(); let payload = self.buffer[14..14 + except_data_len].to_vec(); let crc8_itu_alg = crc::Crc::::new(&crc::CRC_8_I_432_1); let checksum = crc8_itu_alg.checksum(&need_check_data.as_slice()); if self.buffer[frame_length - 1] != checksum { debug!( "checksum mismatch: expected {:02X}, got {:02X}, frame_len={}", checksum, self.buffer[frame_length - 1], frame_length ); self.buffer.drain(0..1); continue; } let dts_ms = elapsed_millis(session_started_at); let meta: TactileAFrameMetaData = TactileAFrameMetaData { header, payload_len, device_addr, extend_code, func_code, start_addr, except_data_len, checksum, }; frames.push(TactileAFrame::Rep({ TactileARepFrame { meta, status, payload, dts_ms, } })); self.buffer.drain(0..frame_length); } Ok(frames) } fn encode( &self, frame: &TactileAFrame, ) -> Result, crate::serial_core::error::CodecError> { match frame { TactileAFrame::Req(f) => { let mut req_bytes: Vec = Vec::new(); req_bytes.extend_from_slice(f.meta.header.as_slice()); req_bytes.extend_from_slice((f.meta.payload_len as u16).to_le_bytes().as_slice()); req_bytes.push(f.meta.device_addr); req_bytes.push(f.meta.extend_code); req_bytes.push(f.meta.func_code); req_bytes.extend_from_slice(f.meta.start_addr.to_le_bytes().as_slice()); req_bytes.extend_from_slice((f.meta.except_data_len as u16).to_le_bytes().as_slice()); let checksum = calc_crc8_itu(req_bytes.as_slice()); req_bytes.push(checksum); Ok(req_bytes) } _ => { Err(CodecError::InvalidFrameType) } } } } #[async_trait] impl FrameHandler for TactileAHandler { async fn on_frame(&mut self, frame: &TactileAFrame) -> anyhow::Result>> { match frame { TactileAFrame::Rep(rep) => { let vals = TactileACodec::parse_data_frame(&rep.payload)?; Ok(Some(vals)) } _ => Ok(None), } } } impl TactileACsvExporter { fn new(channels: usize) -> Self { TactileACsvExporter { channels } } } impl CsvExporter for TactileACsvExporter { type Error = CodecError; fn csv_header(&self, _recording: &Recording) -> Vec { let mut header: Vec = Vec::new(); for i in 0..self.channels { header.push(format!("channel{}", i + 1)); } header.push("dts".to_string()); header.push("summary".to_string()); header } fn csv_row( &self, item: &RecordedFrame, ) -> anyhow::Result> { let packet = TactileADataPacket::try_from(&item.frame)?; let summary: i32 = packet.data.iter().sum(); let mut row: Vec = packet.data.iter().map(|x| x.to_string()).collect(); row.push(packet.dts_ms.to_string()); row.push(summary.to_string()); Ok(row) } } impl CsvExporter for TactileACsvExporter { type Error = CodecError; fn csv_header(&self, _recording: &Recording) -> Vec { let mut header: Vec = Vec::new(); for i in 0..self.channels { header.push(format!("channel{}", i + 1)); } header.push("dts".to_string()); header } fn csv_row( &self, item: &RecordedFrame, ) -> anyhow::Result> { let rep = match &item.frame { TactileAFrame::Rep(rep) => rep, TactileAFrame::Req(_) => return Err(anyhow!("request frame cannot be exported to csv row")), }; let packet = TactileADataPacket::try_from(rep)?; let mut row: Vec = packet.data.iter().map(|x| x.to_string()).collect(); row.push(packet.dts_ms.to_string()); Ok(row) } } impl TactileACsvImporter { pub fn new(_path: &str) -> TactileACsvImporter { Self { channels: 0, data_row: 0, packets: Vec::new(), } } fn parse_record(&mut self, record: StringRecord) -> anyhow::Result { if self.channels == 0 { return Err(anyhow!("csv header is missing channel columns")); } if record.len() < self.channels + 1 { return Err(anyhow!("csv row has insufficient columns")); } let mut data = Vec::with_capacity(self.channels); for index in 0..self.channels { let cell = record.get(index).ok_or_else(|| anyhow!("missing channel cell"))?; data.push(cell.parse::()?); } let dts_cell = record .get(self.channels) .ok_or_else(|| anyhow!("missing dts cell"))?; let dts_ms = dts_cell.parse::()?; Ok(TactileADataPacket { data: data, dts_ms: dts_ms, }) } } impl CsvImporter for TactileACsvImporter { fn load(&mut self, reader: R) -> anyhow::Result> { let mut rdr = csv::Reader::from_reader(reader); let headers = rdr.headers()?.clone(); self.channels = headers.len().saturating_sub(1); self.data_row = 0; self.packets.clear(); for record in rdr.records() { let record = record?; let packet = self.parse_record(record)?; self.packets.push(packet); self.data_row += 1; } Ok(self.packets.clone()) } } pub fn export_recording_csv(recording: &Recording, writer: W) -> anyhow::Result<()> where W: std::io::Write, { let channel_nb = recording .frames .iter() .find_map(|frame| match &frame.frame { TactileAFrame::Rep(rep) => Some(rep.payload.len() / 2), TactileAFrame::Req(_) => None, }) .unwrap_or(0); let exporter = TactileACsvExporter::new(channel_nb); write_csv(recording, &exporter, writer) }