Wire live serial data into matrix renderer

This commit is contained in:
lennlouisgeek
2026-05-20 01:23:02 +08:00
parent a7b617419d
commit d2c9fad556
18 changed files with 874 additions and 63 deletions

7
src/serial_core/codec.rs Normal file
View File

@@ -0,0 +1,7 @@
use crate::serial_core::error::CodecError;
use std::time::Instant;
pub trait Codec<F> {
fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result<Vec<F>, CodecError>;
fn encode(&self, frame: &F) -> Result<Vec<u8>, CodecError>;
}

View File

@@ -0,0 +1 @@
pub mod tactile_a;

View File

@@ -0,0 +1,188 @@
use crate::serial_core::codec::Codec;
use crate::serial_core::error::CodecError;
use crate::serial_core::frame::{
TactileAFrame, TactileAFrameMetaData, TactileAFrameStatusCode, TactileARepFrame,
TactileAReqFrame,
};
use crate::serial_core::utils::{calc_crc8_itu, elapsed_millis};
const FRAME_BUFFER_MIN_LENGTH: usize = 15;
pub struct TactileACodec {
buffer: Vec<u8>,
expected_data_len: usize,
}
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<Vec<i32>, CodecError> {
if data.len() % 2 != 0 {
return Err(CodecError::InvalidLength);
}
let vals: Vec<i32> = 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::<Vec<i32>>();
Ok(vals)
}
pub fn build_req_frame(cols: usize, rows: usize) -> TactileAFrame {
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;
TactileAFrame::Req(TactileAReqFrame {
meta: TactileAFrameMetaData {
header,
payload_len,
device_addr,
extend_code,
func_code,
start_addr,
except_data_len,
checksum,
},
})
}
}
impl Codec<TactileAFrame> for TactileACodec {
fn decode(
&mut self,
input: &[u8],
session_started_at: std::time::Instant,
) -> Result<Vec<TactileAFrame>, CodecError> {
self.buffer.extend_from_slice(input);
let mut frames: Vec<TactileAFrame> = Vec::new();
loop {
if self.buffer.len() < FRAME_BUFFER_MIN_LENGTH {
break;
}
// Search for response header: [0xAA, 0x55]
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 = match self.buffer[13] {
0 => TactileAFrameStatusCode::Success,
_ => TactileAFrameStatusCode::Failure,
};
if except_data_len != self.expected_data_len {
log::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::<u8>::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 {
log::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 {
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<Vec<u8>, CodecError> {
match frame {
TactileAFrame::Req(f) => {
let mut req_bytes: Vec<u8> = 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),
}
}
}

51
src/serial_core/error.rs Normal file
View File

@@ -0,0 +1,51 @@
use std::fmt;
#[derive(Debug)]
pub enum SerialError {
OpenError,
CloseError,
ScanError,
InvalidConfig,
AlreadyConnected,
StateError,
NoRecordedData,
}
impl fmt::Display for SerialError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SerialError::OpenError => write!(f, "Opening Error"),
SerialError::CloseError => write!(f, "Closing Error"),
SerialError::ScanError => write!(f, "Scan Error"),
SerialError::InvalidConfig => write!(f, "Invalid Config"),
SerialError::AlreadyConnected => write!(f, "Already Connected"),
SerialError::StateError => write!(f, "State Error"),
SerialError::NoRecordedData => write!(f, "No Recorded Data"),
}
}
}
impl std::error::Error for SerialError {}
#[derive(Debug)]
pub enum CodecError {
InvalidHeader,
InvalidTail,
InvalidLength,
InvalidFrameType,
PayloadTooLarge,
}
impl fmt::Display for CodecError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CodecError::InvalidHeader => write!(f, "Invalid Header"),
CodecError::InvalidTail => write!(f, "Invalid Tail"),
CodecError::InvalidLength => write!(f, "Invalid Length"),
CodecError::InvalidFrameType => write!(f, "Invalid Frame Type"),
CodecError::PayloadTooLarge => write!(f, "Payload too large"),
}
}
}
impl std::error::Error for CodecError {}

36
src/serial_core/frame.rs Normal file
View File

@@ -0,0 +1,36 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TactileAFrameMetaData {
pub header: [u8; 2],
pub payload_len: usize,
pub device_addr: u8,
pub extend_code: u8,
pub func_code: u8,
pub start_addr: u32,
pub except_data_len: usize,
pub checksum: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TactileAReqFrame {
pub meta: TactileAFrameMetaData,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TactileARepFrame {
pub meta: TactileAFrameMetaData,
pub status: TactileAFrameStatusCode,
pub payload: Vec<u8>,
pub dts_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TactileAFrameStatusCode {
Success,
Failure,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TactileAFrame {
Req(TactileAReqFrame),
Rep(TactileARepFrame),
}

6
src/serial_core/mod.rs Normal file
View File

@@ -0,0 +1,6 @@
pub mod codec;
pub mod codecs;
pub mod error;
pub mod frame;
pub mod serial;
pub mod utils;

96
src/serial_core/serial.rs Normal file
View File

@@ -0,0 +1,96 @@
use crate::serial_core::codec::Codec;
use crate::serial_core::codecs::tactile_a::TactileACodec;
use crate::serial_core::frame::TactileAFrame;
use crate::serial_core::utils::elapsed_millis;
use crossbeam_channel::{Receiver, Sender, TryRecvError};
use std::io::{Read, Write};
use std::time::{Duration, Instant};
const POLL_INTERVAL_MS: u64 = 10;
/// Runs the serial polling loop on the calling (background) thread.
/// Sends decoded pressure matrix data (Vec<i32>) to the output channel.
pub fn run_serial_loop(
port: &mut dyn ReadWrite,
rows: usize,
cols: usize,
cancel_rx: &Receiver<()>,
sample_tx: &Sender<Vec<i32>>,
) {
let session_started_at = Instant::now();
let mut codec = TactileACodec::new(cols, rows);
let req_frame = TactileACodec::build_req_frame(cols, rows);
let mut buffer = [0u8; 1024];
let mut poll_interval = Duration::from_millis(POLL_INTERVAL_MS);
loop {
// Check cancel
if cancel_rx.try_recv().is_ok() {
break;
}
// Send poll request
if let Ok(req_bytes) = codec.encode(&req_frame) {
let _ = port.write_all(&req_bytes);
}
// Read response with poll interval
let deadline = Instant::now() + poll_interval;
loop {
if Instant::now() >= deadline {
break;
}
match port.read(&mut buffer) {
Ok(n) if n > 0 => {
if let Ok(frames) = codec.decode(&buffer[..n], session_started_at) {
for frame in frames {
if let TactileAFrame::Rep(rep) = frame {
if let Ok(vals) = TactileACodec::parse_data_frame(&rep.payload) {
let _ = sample_tx.try_send(vals);
}
}
}
}
}
Ok(_) => {
std::thread::sleep(Duration::from_millis(1));
}
Err(ref e) if e.kind() == std::io::ErrorKind::TimedOut => {
continue;
}
Err(e) => {
eprintln!("[serial] read error: {e}");
return;
}
}
}
}
}
/// Trait abstracting read+write for the serial port
pub trait ReadWrite: Send {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()>;
}
/// Wrapper for serialport's Box<dyn SerialPort>
pub struct SerialPortReadWrite {
inner: Box<dyn serialport::SerialPort>,
}
impl SerialPortReadWrite {
pub fn new(port: Box<dyn serialport::SerialPort>) -> Self {
Self { inner: port }
}
}
impl ReadWrite for SerialPortReadWrite {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.read(buf)
}
fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
self.inner.write_all(buf)
}
}

10
src/serial_core/utils.rs Normal file
View File

@@ -0,0 +1,10 @@
use std::time::Instant;
pub fn calc_crc8_itu(c: &[u8]) -> u8 {
let crc8_itu_alg = crc::Crc::<u8>::new(&crc::CRC_8_I_432_1);
crc8_itu_alg.checksum(c)
}
pub fn elapsed_millis(start_at: Instant) -> u64 {
start_at.elapsed().as_millis() as u64
}