first commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
1053
Cargo.lock
generated
Normal file
1053
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "JE-Skin-Cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
clap = "4.6.1"
|
||||||
|
fern = {version = "0.7.1", features=["colored", "date-based"]}
|
||||||
|
humantime = "2.3.0"
|
||||||
|
log = "0.4.29"
|
||||||
|
tokio = {version = "1.52.1", features=["full"]}
|
||||||
|
tokio-serial = "5.4.5"
|
||||||
|
tokio-util = "0.7.18"
|
||||||
|
csv = "1.4.0"
|
||||||
|
chrono = "0.4.44"
|
||||||
|
crc = "3.4.0"
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
serde = { version = "1.0.228", features=["derive"]}
|
||||||
|
serde_json = "1.0.149"
|
||||||
35
src/app.rs
Normal file
35
src/app.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use std::{sync::{Arc, Mutex}, thread::JoinHandle};
|
||||||
|
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use crate::serial_core::{TactileARecording, error::SerialError};
|
||||||
|
|
||||||
|
struct SerialSession {
|
||||||
|
port: String,
|
||||||
|
cancel: CancellationToken,
|
||||||
|
task: JoinHandle<()>,
|
||||||
|
current_record: Arc<Mutex<TactileARecording>>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SerialConnectionState {
|
||||||
|
session: Mutex<Option<SerialSession>>,
|
||||||
|
last_record: Mutex<Option<Arc<Mutex<TactileARecording>>>>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serial_connect(
|
||||||
|
port: String,
|
||||||
|
state: Arc<SerialConnectionState>
|
||||||
|
) -> Result<(), SerialError> {
|
||||||
|
let port_name = port.trim().to_string();
|
||||||
|
if port_name.is_empty() {
|
||||||
|
return Err(SerialError::InvalidConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||||
|
if session.is_some() {
|
||||||
|
return Err(SerialError::AlreadyConnected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
58
src/flog.rs
Normal file
58
src/flog.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
use fern::{Dispatch, colors::{ColoredLevelConfig, Color}, DateBased};
|
||||||
|
use log::{debug};
|
||||||
|
use std::time::SystemTime;
|
||||||
|
pub fn setup_logger() {
|
||||||
|
let colors_line = ColoredLevelConfig::new()
|
||||||
|
.error(Color::Red)
|
||||||
|
.warn(Color::Yellow)
|
||||||
|
.info(Color::Green)
|
||||||
|
.debug(Color::White)
|
||||||
|
.trace(Color::BrightBlack);
|
||||||
|
|
||||||
|
let colors_level = colors_line.info(Color::Green);
|
||||||
|
let level = if cfg!(debug_assertions) {
|
||||||
|
log::LevelFilter::Debug
|
||||||
|
} else {
|
||||||
|
log::LevelFilter::Info
|
||||||
|
};
|
||||||
|
|
||||||
|
let console_config = fern::Dispatch::new()
|
||||||
|
.format(move |out, message, record| {
|
||||||
|
out.finish(
|
||||||
|
format_args!(
|
||||||
|
"{colors_line}[{data} {level} {target} {colors_line}] {message}\x1B[0m",
|
||||||
|
colors_line = format_args!(
|
||||||
|
"\x1B[{}m",
|
||||||
|
colors_line.get_color(&record.level()).to_fg_str()
|
||||||
|
),
|
||||||
|
data = humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||||
|
target = record.target(),
|
||||||
|
level = colors_level.color(record.level()),
|
||||||
|
message = message,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.level(level)
|
||||||
|
.chain(std::io::stdout());
|
||||||
|
|
||||||
|
let data_based_config = fern::Dispatch::new()
|
||||||
|
.format(move |out, message, record| {
|
||||||
|
out.finish(
|
||||||
|
format_args!(
|
||||||
|
"[{data} {level} {target}] {message}",
|
||||||
|
data = humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||||
|
target = record.target(),
|
||||||
|
level = colors_level.color(record.level()),
|
||||||
|
message = message,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.level(level)
|
||||||
|
.chain(fern::DateBased::new("program.log", "%Y-%m-%d"));
|
||||||
|
Dispatch::new()
|
||||||
|
.level(log::LevelFilter::Debug)
|
||||||
|
.chain(console_config)
|
||||||
|
.chain(data_based_config)
|
||||||
|
.apply()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
6
src/main.rs
Normal file
6
src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod serial_core;
|
||||||
|
pub mod flog;
|
||||||
|
pub mod app;
|
||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
||||||
6
src/serial_core/codec.rs
Normal file
6
src/serial_core/codec.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
5
src/serial_core/codecs/mod.rs
Normal file
5
src/serial_core/codecs/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use crate::serial_core::{frame::TestFrame, record::Recording};
|
||||||
|
|
||||||
|
pub mod test;
|
||||||
|
pub mod tactile_a;
|
||||||
|
pub type TestRecording = Recording<TestFrame>;
|
||||||
382
src/serial_core/codecs/tactile_a.rs
Normal file
382
src/serial_core/codecs/tactile_a.rs
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
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<u8>,
|
||||||
|
expected_data_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TactileACsvExporter {
|
||||||
|
channels: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TactileACsvImporter {
|
||||||
|
channels: usize,
|
||||||
|
data_row: usize,
|
||||||
|
packets: Vec<TactileADataPacket>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TactileAHandler;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TactileADataPacket {
|
||||||
|
pub data: Vec<i32>,
|
||||||
|
pub dts_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> 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<TactileADataPacket, Self::Error> {
|
||||||
|
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<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) -> anyhow::Result<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;
|
||||||
|
Ok(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<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 {
|
||||||
|
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<Vec<u8>, crate::serial_core::error::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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FrameHandler<TactileAFrame, i32> for TactileAHandler {
|
||||||
|
async fn on_frame(&mut self, frame: &TactileAFrame) -> anyhow::Result<Option<Vec<i32>>> {
|
||||||
|
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<TactileARepFrame> for TactileACsvExporter {
|
||||||
|
type Error = CodecError;
|
||||||
|
fn csv_header(&self, _recording: &Recording<TactileARepFrame>) -> Vec<String> {
|
||||||
|
let mut header: Vec<String> = 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<TactileARepFrame>,
|
||||||
|
) -> anyhow::Result<Vec<String>> {
|
||||||
|
let packet = TactileADataPacket::try_from(&item.frame)?;
|
||||||
|
let summary: i32 = packet.data.iter().sum();
|
||||||
|
let mut row: Vec<String> = 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<TactileAFrame> for TactileACsvExporter {
|
||||||
|
type Error = CodecError;
|
||||||
|
|
||||||
|
fn csv_header(&self, _recording: &Recording<TactileAFrame>) -> Vec<String> {
|
||||||
|
let mut header: Vec<String> = 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<TactileAFrame>,
|
||||||
|
) -> anyhow::Result<Vec<String>> {
|
||||||
|
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<String> = 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<TactileADataPacket> {
|
||||||
|
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::<i32>()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dts_cell = record
|
||||||
|
.get(self.channels)
|
||||||
|
.ok_or_else(|| anyhow!("missing dts cell"))?;
|
||||||
|
let dts_ms = dts_cell.parse::<u64>()?;
|
||||||
|
|
||||||
|
Ok(TactileADataPacket {
|
||||||
|
data: data,
|
||||||
|
dts_ms: dts_ms,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CsvImporter<TactileADataPacket> for TactileACsvImporter {
|
||||||
|
fn load<R: Read>(&mut self, reader: R) -> anyhow::Result<Vec<TactileADataPacket>> {
|
||||||
|
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<W>(recording: &Recording<TactileAFrame>, 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)
|
||||||
|
}
|
||||||
256
src/serial_core/codecs/test.rs
Normal file
256
src/serial_core/codecs/test.rs
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
use std::time::Instant;
|
||||||
|
use crate::serial_core::frame::{FrameHandler};
|
||||||
|
use crate::serial_core::{codec::Codec, error::CodecError, frame::TestFrame};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use csv::StringRecord;
|
||||||
|
use crate::serial_core::record::{write_csv, CsvExporter, CsvImporter, RecordedFrame, Recording};
|
||||||
|
use crate::serial_core::utils::{
|
||||||
|
elapsed_millis,
|
||||||
|
usize_to_u16_be_bytes
|
||||||
|
};
|
||||||
|
pub struct TestCodec {
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestHandler;
|
||||||
|
|
||||||
|
impl TestCodec {
|
||||||
|
pub fn new() -> TestCodec {
|
||||||
|
Self { buffer: Vec::new() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Codec<TestFrame> for TestCodec {
|
||||||
|
fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result<Vec<TestFrame>, CodecError> {
|
||||||
|
self.buffer.extend_from_slice(input);
|
||||||
|
let mut frames = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if self.buffer.len() < 6 {
|
||||||
|
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() < 6 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd = self.buffer[2];
|
||||||
|
let length_bytes = [self.buffer[3], self.buffer[4]];
|
||||||
|
let length = u16::from_be_bytes(length_bytes) as usize;
|
||||||
|
let frame_length = (length + 6) as usize;
|
||||||
|
if self.buffer.len() < frame_length {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let payload = self.buffer[5..5 + length].to_vec();
|
||||||
|
// let checksum = crc8(payload.as_slice());
|
||||||
|
let crc8_alg = crc::Crc::<u8>::new(&crc::CRC_8_SMBUS);
|
||||||
|
let checksum = crc8_alg.checksum(payload.as_slice());
|
||||||
|
if self.buffer[frame_length - 1] != checksum {
|
||||||
|
self.buffer.drain(0..1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let dts = elapsed_millis(session_started_at);
|
||||||
|
println!("dts_ms: {dts}");
|
||||||
|
frames.push(TestFrame {
|
||||||
|
header: [0xAA, 0x55],
|
||||||
|
cmd: cmd,
|
||||||
|
length: length,
|
||||||
|
payload: payload,
|
||||||
|
checksum: checksum,
|
||||||
|
dts_ms: dts,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.buffer.drain(0..frame_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(frames)
|
||||||
|
}
|
||||||
|
fn encode(&self, frame: &TestFrame) -> Result<Vec<u8>, CodecError> {
|
||||||
|
let _ = u16::try_from(frame.payload.len()).map_err(|_| CodecError::PayloadTooLarge)?;
|
||||||
|
let mut out = Vec::with_capacity(6 + frame.length);
|
||||||
|
out.extend_from_slice(&frame.header);
|
||||||
|
out.push(frame.cmd);
|
||||||
|
out.extend_from_slice(&usize_to_u16_be_bytes(frame.length));
|
||||||
|
out.extend_from_slice(&frame.payload);
|
||||||
|
out.push(frame.checksum);
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FrameHandler<TestFrame, i32> for TestHandler {
|
||||||
|
async fn on_frame(&mut self, frame: &TestFrame) -> anyhow::Result<Option<Vec<i32>>> {
|
||||||
|
match frame.cmd {
|
||||||
|
0x01 => {
|
||||||
|
let vals = parse_data_frame(&frame.payload)?;
|
||||||
|
Ok(Some(vals))
|
||||||
|
}
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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| u16::from_be_bytes([chunk[0], chunk[1]]) as i32)
|
||||||
|
.collect::<Vec<i32>>();
|
||||||
|
|
||||||
|
Ok(vals)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestCsvExporter;
|
||||||
|
pub struct TestCsvImporter {
|
||||||
|
channels: usize,
|
||||||
|
data_row: usize,
|
||||||
|
packets: Vec<TestDataPacket>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TestDataPacket {
|
||||||
|
pub data: Vec<i32>,
|
||||||
|
pub dts_ms: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&TestFrame> for TestDataPacket {
|
||||||
|
type Error = CodecError;
|
||||||
|
fn try_from(frame: &TestFrame) -> Result<TestDataPacket, Self::Error> {
|
||||||
|
let data = parse_data_frame(&frame.payload)?;
|
||||||
|
let dts = frame.dts_ms;
|
||||||
|
Ok(TestDataPacket { data: data, dts_ms: dts })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// impl From<TestFrame> for TestDataPacket {
|
||||||
|
// fn from(frame: TestFrame) -> Self {
|
||||||
|
// let data = parse_data_frame(&frame.payload)?;
|
||||||
|
// let dts = frame.dts_ms;
|
||||||
|
// TestDataPacket { data: data, dts_ms: dts }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
impl CsvExporter<TestFrame> for TestCsvExporter {
|
||||||
|
type Error = CodecError;
|
||||||
|
fn csv_header(&self, recording: &Recording<TestFrame>) -> Vec<String> {
|
||||||
|
let channel_nb = recording
|
||||||
|
.frames
|
||||||
|
.iter()
|
||||||
|
.find_map(|frame| parse_data_frame(&frame.frame.payload).ok().map(|vals| vals.len()))
|
||||||
|
.unwrap_or(0);
|
||||||
|
let mut header: Vec<String> = Vec::new();
|
||||||
|
for i in 0..channel_nb {
|
||||||
|
header.push(format!("channel{}", i + 1));
|
||||||
|
}
|
||||||
|
header.push("dts".to_string());
|
||||||
|
|
||||||
|
header
|
||||||
|
}
|
||||||
|
|
||||||
|
fn csv_row(&self, item: &RecordedFrame<TestFrame>) -> anyhow::Result<Vec<String>> {
|
||||||
|
let packet: TestDataPacket = TestDataPacket::try_from(&item.frame)?;
|
||||||
|
let mut row: Vec<String> = packet.data.iter().map(|&x| x.to_string()).collect();
|
||||||
|
row.push(packet.dts_ms.to_string());
|
||||||
|
Ok(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestCsvImporter {
|
||||||
|
pub fn new(_path: &str) -> TestCsvImporter {
|
||||||
|
Self {
|
||||||
|
channels: 0,
|
||||||
|
data_row: 0,
|
||||||
|
packets: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_record(&mut self, record: StringRecord) -> anyhow::Result<TestDataPacket>{
|
||||||
|
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::<i32>()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dts_cell = record
|
||||||
|
.get(self.channels)
|
||||||
|
.ok_or_else(|| anyhow!("missing dts cell"))?;
|
||||||
|
let dts_ms = dts_cell.parse::<u64>()?;
|
||||||
|
|
||||||
|
Ok(TestDataPacket {
|
||||||
|
data: data,
|
||||||
|
dts_ms: dts_ms,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CsvImporter<TestDataPacket> for TestCsvImporter {
|
||||||
|
fn load<R: Read>(&mut self, reader: R) -> anyhow::Result<Vec<TestDataPacket>> {
|
||||||
|
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<W>(recording: &Recording<TestFrame>, writer: W) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
W: std::io::Write,
|
||||||
|
{
|
||||||
|
write_csv(recording, &TestCsvExporter, writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use csv::Reader;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_csv_basic() -> anyhow::Result<()> {
|
||||||
|
let mut rdr = Reader::from_path("recording_20260329_125238.csv")?;
|
||||||
|
let headers = rdr.headers()?;
|
||||||
|
println!("headers: {:?}", headers);
|
||||||
|
|
||||||
|
for result in rdr.records() {
|
||||||
|
let record = result?;
|
||||||
|
println!("record: {:?}", record);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/serial_core/error.rs
Normal file
54
src/serial_core/error.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub enum SerialError {
|
||||||
|
OpenError,
|
||||||
|
CloseError,
|
||||||
|
ScanError,
|
||||||
|
InvalidConfig,
|
||||||
|
AlreadyConnected,
|
||||||
|
StateError,
|
||||||
|
NoRecordedData,
|
||||||
|
ExportError,
|
||||||
|
ImportError,
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
SerialError::ExportError => write!(f, "Export Error"),
|
||||||
|
SerialError::ImportError => write!(f, "Import Error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {}
|
||||||
57
src/serial_core/frame.rs
Normal file
57
src/serial_core/frame.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TestFrame {
|
||||||
|
pub header: [u8; 2],
|
||||||
|
pub cmd: u8,
|
||||||
|
pub length: usize,
|
||||||
|
pub payload: Vec<u8>,
|
||||||
|
pub checksum: u8,
|
||||||
|
pub dts_ms: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 status: u8,
|
||||||
|
// pub payload_data: Vec<u8>,
|
||||||
|
pub checksum: u8,
|
||||||
|
// pub dts_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait FrameHandler<F, T>: Send {
|
||||||
|
async fn on_frame(&mut self, frame: &F) -> Result<Option<Vec<T>>>;
|
||||||
|
}
|
||||||
|
|
||||||
43
src/serial_core/mod.rs
Normal file
43
src/serial_core/mod.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use tokio_serial::available_ports;
|
||||||
|
|
||||||
|
use crate::serial_core::{
|
||||||
|
error::SerialError, frame::{TactileAFrame, TestFrame}, record::Recording
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod codec;
|
||||||
|
pub mod codecs;
|
||||||
|
pub mod error;
|
||||||
|
pub mod frame;
|
||||||
|
pub mod model;
|
||||||
|
pub mod serial;
|
||||||
|
pub mod record;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
pub type TestRecording = Recording<TestFrame>;
|
||||||
|
pub type TactileARecording = Recording<TactileAFrame>;
|
||||||
|
|
||||||
|
pub struct SerialConnection {
|
||||||
|
pub port: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect(port: &str) -> Result<SerialConnection, String> {
|
||||||
|
let port = port.trim();
|
||||||
|
|
||||||
|
if port.is_empty() {
|
||||||
|
return Err("Serial port is required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SerialConnection {
|
||||||
|
port: port.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serial_enum() -> Result<Vec<String>, SerialError> {
|
||||||
|
let ports = available_ports()
|
||||||
|
.map_err(|_| SerialError::ScanError)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|info| info.port_name)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(ports)
|
||||||
|
}
|
||||||
500
src/serial_core/model.rs
Normal file
500
src/serial_core/model.rs
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
use crate::serial_core::frame::TestFrame;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
const MAX_POINTS: usize = 28;
|
||||||
|
const MAX_SUMMARY_POINTS: usize = 42;
|
||||||
|
const PANEL_STALE_AFTER: Duration = Duration::from_millis(2400);
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HudPacket {
|
||||||
|
pub ts: u64,
|
||||||
|
pub panels: Vec<HudSignalPanel>,
|
||||||
|
pub summary: HudSummary,
|
||||||
|
pub pressure_matrix: Option<Vec<f32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HudSummary {
|
||||||
|
pub label: String,
|
||||||
|
pub points: Vec<f32>,
|
||||||
|
pub latest: Option<f32>,
|
||||||
|
pub min: Option<f32>,
|
||||||
|
pub max: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone, Copy)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum HudPanelSide {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone, Copy)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum HudTone {
|
||||||
|
Cyan,
|
||||||
|
Lime,
|
||||||
|
Orange,
|
||||||
|
Violet,
|
||||||
|
Gold,
|
||||||
|
Rose,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HudSignalPanel {
|
||||||
|
pub id: String,
|
||||||
|
pub code: String,
|
||||||
|
pub title: String,
|
||||||
|
pub side: HudPanelSide,
|
||||||
|
pub active: bool,
|
||||||
|
pub series: Vec<HudSignalSeries>,
|
||||||
|
pub icons: Vec<HudSignalIcon>,
|
||||||
|
pub latest: Option<f32>,
|
||||||
|
pub min: Option<f32>,
|
||||||
|
pub max: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HudSignalSeries {
|
||||||
|
pub id: String,
|
||||||
|
pub tone: HudTone,
|
||||||
|
pub points: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HudSignalIcon {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub tone: HudTone,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HudPanelUpdate {
|
||||||
|
source_id: String,
|
||||||
|
values: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PanelEntry {
|
||||||
|
panel: HudSignalPanel,
|
||||||
|
last_seen: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HudChartState {
|
||||||
|
panels: HashMap<String, PanelEntry>,
|
||||||
|
order: Vec<String>,
|
||||||
|
summary_points: Vec<f32>,
|
||||||
|
pressure_matrix: Option<Vec<f32>>,
|
||||||
|
last_frame_seen: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HudChartState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
panels: HashMap::new(),
|
||||||
|
order: Vec::new(),
|
||||||
|
summary_points: Vec::new(),
|
||||||
|
pressure_matrix: None,
|
||||||
|
last_frame_seen: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_summary(&mut self, value: f32) {
|
||||||
|
push_summary_point(&mut self.summary_points, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_pressure_matrix(&mut self, values: &[i32]) {
|
||||||
|
if values.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pressure_matrix = Some(values.iter().map(|value| *value as f32).collect());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_frame(&mut self, frame: &TestFrame, decoded_values: Option<&[i32]>) -> HudPacket {
|
||||||
|
let now = Instant::now();
|
||||||
|
self.last_frame_seen = Some(now);
|
||||||
|
|
||||||
|
for update in expand_frame_updates(frame, decoded_values) {
|
||||||
|
self.apply_update(update, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prune_stale_at(now);
|
||||||
|
self.snapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prune_stale(&mut self) -> Option<HudPacket> {
|
||||||
|
let before = self.panels.len();
|
||||||
|
let summary_points_before = self.summary_points.len();
|
||||||
|
self.prune_stale_at(Instant::now());
|
||||||
|
|
||||||
|
if before == self.panels.len() && summary_points_before == self.summary_points.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(self.snapshot())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_update(&mut self, update: HudPanelUpdate, now: Instant) {
|
||||||
|
if update.values.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.panels.contains_key(&update.source_id) {
|
||||||
|
let next_side = side_for_index(self.order.len());
|
||||||
|
self.order.push(update.source_id.clone());
|
||||||
|
self.panels.insert(
|
||||||
|
update.source_id.clone(),
|
||||||
|
PanelEntry {
|
||||||
|
panel: build_panel(&update.source_id, next_side, update.values.len()),
|
||||||
|
last_seen: now,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = self
|
||||||
|
.panels
|
||||||
|
.get_mut(&update.source_id)
|
||||||
|
.expect("panel entry should exist after insertion");
|
||||||
|
|
||||||
|
entry.last_seen = now;
|
||||||
|
entry.panel.active = true;
|
||||||
|
ensure_panel_channels(&mut entry.panel, update.values.len());
|
||||||
|
|
||||||
|
for (index, value) in update.values.into_iter().enumerate() {
|
||||||
|
if let Some(series) = entry.panel.series.get_mut(index) {
|
||||||
|
push_point(&mut series.points, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_panel_stats(&mut entry.panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prune_stale_at(&mut self, now: Instant) {
|
||||||
|
self.panels
|
||||||
|
.retain(|_, entry| now.duration_since(entry.last_seen) <= PANEL_STALE_AFTER);
|
||||||
|
self.order.retain(|id| self.panels.contains_key(id));
|
||||||
|
|
||||||
|
let summary_stale = self
|
||||||
|
.last_frame_seen
|
||||||
|
.map(|last_seen| now.duration_since(last_seen) > PANEL_STALE_AFTER)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if summary_stale {
|
||||||
|
self.summary_points.clear();
|
||||||
|
self.pressure_matrix = None;
|
||||||
|
self.last_frame_seen = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(&mut self) -> HudPacket {
|
||||||
|
self.rebalance_sides();
|
||||||
|
|
||||||
|
let panels = self
|
||||||
|
.order
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| self.panels.get(id).map(|entry| entry.panel.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
HudPacket {
|
||||||
|
ts: now_millis(),
|
||||||
|
panels,
|
||||||
|
summary: build_summary(&self.summary_points),
|
||||||
|
pressure_matrix: self.pressure_matrix.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebalance_sides(&mut self) {
|
||||||
|
for (index, id) in self.order.iter().enumerate() {
|
||||||
|
if let Some(entry) = self.panels.get_mut(id) {
|
||||||
|
entry.panel.side = side_for_index(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HudChartState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_panel(source_id: &str, side: HudPanelSide, channel_count: usize) -> HudSignalPanel {
|
||||||
|
HudSignalPanel {
|
||||||
|
id: format!("panel-{source_id}"),
|
||||||
|
code: source_id.to_string(),
|
||||||
|
title: format!("Source {source_id}"),
|
||||||
|
side,
|
||||||
|
active: true,
|
||||||
|
series: build_panel_series(source_id, channel_count, &[]),
|
||||||
|
icons: build_panel_icons(source_id, channel_count),
|
||||||
|
latest: None,
|
||||||
|
min: None,
|
||||||
|
max: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_frame_updates(frame: &TestFrame, decoded_values: Option<&[i32]>) -> Vec<HudPanelUpdate> {
|
||||||
|
if let Some(values) = decoded_values {
|
||||||
|
if values.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec![HudPanelUpdate {
|
||||||
|
source_id: format_source_id(frame.cmd),
|
||||||
|
values: values.iter().map(|value| *value as f32).collect(),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunks = frame.payload.chunks_exact(4);
|
||||||
|
|
||||||
|
if !frame.payload.is_empty() && chunks.remainder().is_empty() {
|
||||||
|
return chunks.map(build_update_from_chunk).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![HudPanelUpdate {
|
||||||
|
source_id: format_source_id(frame.cmd),
|
||||||
|
values: fallback_values(frame),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_update_from_chunk(chunk: &[u8]) -> HudPanelUpdate {
|
||||||
|
HudPanelUpdate {
|
||||||
|
source_id: format_source_id(chunk[0]),
|
||||||
|
values: chunk[1..]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, byte)| normalize_value(*byte, tone_for_index(index)))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_values(frame: &TestFrame) -> Vec<f32> {
|
||||||
|
let mut bytes = frame.payload.clone();
|
||||||
|
|
||||||
|
if bytes.is_empty() {
|
||||||
|
bytes.extend([
|
||||||
|
frame.cmd,
|
||||||
|
frame.length as u8,
|
||||||
|
frame.checksum,
|
||||||
|
frame.cmd.wrapping_add(frame.checksum),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
while bytes.len() < 3 {
|
||||||
|
let previous = *bytes.last().unwrap_or(&frame.cmd);
|
||||||
|
bytes.push(
|
||||||
|
previous
|
||||||
|
.wrapping_add(frame.cmd)
|
||||||
|
.wrapping_add(bytes.len() as u8),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, byte)| normalize_value(byte, tone_for_index(index)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_value(byte: u8, tone: HudTone) -> f32 {
|
||||||
|
let base = (byte as f32 / 255.0) * 100.0;
|
||||||
|
let offset = match tone {
|
||||||
|
HudTone::Cyan => 6.0,
|
||||||
|
HudTone::Lime => 0.0,
|
||||||
|
HudTone::Orange => -6.0,
|
||||||
|
HudTone::Violet => 10.0,
|
||||||
|
HudTone::Gold => -10.0,
|
||||||
|
HudTone::Rose => 3.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
(base + offset).clamp(0.0, 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_source_id(byte: u8) -> String {
|
||||||
|
if byte.is_ascii_alphanumeric() {
|
||||||
|
(byte as char).to_ascii_uppercase().to_string()
|
||||||
|
} else {
|
||||||
|
format!("CH{:02X}", byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn side_for_index(index: usize) -> HudPanelSide {
|
||||||
|
if index % 2 == 0 {
|
||||||
|
HudPanelSide::Left
|
||||||
|
} else {
|
||||||
|
HudPanelSide::Right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_point(points: &mut Vec<f32>, value: f32) {
|
||||||
|
if points.len() >= MAX_POINTS {
|
||||||
|
points.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
points.push((value * 10.0).round() / 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_panel_series(
|
||||||
|
source_id: &str,
|
||||||
|
channel_count: usize,
|
||||||
|
previous: &[HudSignalSeries],
|
||||||
|
) -> Vec<HudSignalSeries> {
|
||||||
|
(0..channel_count)
|
||||||
|
.map(|index| HudSignalSeries {
|
||||||
|
id: format!("{source_id}-series-{}", index + 1),
|
||||||
|
tone: tone_for_index(index),
|
||||||
|
points: previous
|
||||||
|
.get(index)
|
||||||
|
.map(|series| series.points.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_panel_icons(source_id: &str, channel_count: usize) -> Vec<HudSignalIcon> {
|
||||||
|
(0..channel_count)
|
||||||
|
.map(|index| HudSignalIcon {
|
||||||
|
id: format!("{source_id}-icon-{}", index + 1),
|
||||||
|
label: if channel_count == 1 {
|
||||||
|
"TOTAL".to_string()
|
||||||
|
} else {
|
||||||
|
format!("{source_id}-{}", index + 1)
|
||||||
|
},
|
||||||
|
tone: tone_for_index(index),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_panel_channels(panel: &mut HudSignalPanel, channel_count: usize) {
|
||||||
|
if panel.series.len() == channel_count && panel.icons.len() == channel_count {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.series = build_panel_series(&panel.code, channel_count, &panel.series);
|
||||||
|
panel.icons = build_panel_icons(&panel.code, channel_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_panel_stats(panel: &mut HudSignalPanel) {
|
||||||
|
let latest_values: Vec<f32> = panel
|
||||||
|
.series
|
||||||
|
.iter()
|
||||||
|
.filter_map(|series| series.points.last().copied())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
panel.latest = if latest_values.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(latest_values.iter().sum::<f32>() / latest_values.len() as f32)
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.min = panel
|
||||||
|
.series
|
||||||
|
.iter()
|
||||||
|
.flat_map(|series| series.points.iter().copied())
|
||||||
|
.reduce(f32::min);
|
||||||
|
|
||||||
|
panel.max = panel
|
||||||
|
.series
|
||||||
|
.iter()
|
||||||
|
.flat_map(|series| series.points.iter().copied())
|
||||||
|
.reduce(f32::max);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tone_for_index(index: usize) -> HudTone {
|
||||||
|
match index % 6 {
|
||||||
|
0 => HudTone::Cyan,
|
||||||
|
1 => HudTone::Lime,
|
||||||
|
2 => HudTone::Orange,
|
||||||
|
3 => HudTone::Violet,
|
||||||
|
4 => HudTone::Gold,
|
||||||
|
_ => HudTone::Rose,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_summary_point(points: &mut Vec<f32>, value: f32) {
|
||||||
|
if points.len() >= MAX_SUMMARY_POINTS {
|
||||||
|
points.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
points.push((value * 10.0).round() / 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_summary(points: &[f32]) -> HudSummary {
|
||||||
|
HudSummary {
|
||||||
|
label: "TOTAL".to_string(),
|
||||||
|
points: points.to_vec(),
|
||||||
|
latest: points.last().copied(),
|
||||||
|
min: points.iter().copied().reduce(f32::min),
|
||||||
|
max: points.iter().copied().reduce(f32::max),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now_millis() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_millis() as u64)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[cfg(test)]
|
||||||
|
// mod tests {
|
||||||
|
// use super::*;
|
||||||
|
//
|
||||||
|
// fn sample_frame() -> TestFrame {
|
||||||
|
// TestFrame {
|
||||||
|
// header: [0xAA, 0x55],
|
||||||
|
// cmd: 0x01,
|
||||||
|
// length: 4,
|
||||||
|
// payload: vec![0x00, 0x0A, 0x00, 0x14],
|
||||||
|
// checksum: 0,
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[test]
|
||||||
|
// fn prune_stale_clears_panels_and_summary_after_timeout() {
|
||||||
|
// let mut state = HudChartState::new();
|
||||||
|
// let frame = sample_frame();
|
||||||
|
//
|
||||||
|
// state.record_summary(30.0);
|
||||||
|
// let _ = state.apply_frame(&frame, Some(&[10, 20]));
|
||||||
|
//
|
||||||
|
// let stale_now = Instant::now();
|
||||||
|
// let stale_seen = stale_now - PANEL_STALE_AFTER - Duration::from_millis(1);
|
||||||
|
//
|
||||||
|
// state.last_frame_seen = Some(stale_seen);
|
||||||
|
//
|
||||||
|
// for entry in state.panels.values_mut() {
|
||||||
|
// entry.last_seen = stale_seen;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let packet = state
|
||||||
|
// .prune_stale()
|
||||||
|
// .expect("stale data should emit an update");
|
||||||
|
//
|
||||||
|
// assert!(packet.panels.is_empty());
|
||||||
|
// assert!(packet.summary.points.is_empty());
|
||||||
|
// assert!(state.panels.is_empty());
|
||||||
|
// assert!(state.summary_points.is_empty());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[test]
|
||||||
|
// fn prune_stale_keeps_recent_summary_points() {
|
||||||
|
// let mut state = HudChartState::new();
|
||||||
|
// let frame = sample_frame();
|
||||||
|
//
|
||||||
|
// state.record_summary(30.0);
|
||||||
|
// let _ = state.apply_frame(&frame, Some(&[10, 20]));
|
||||||
|
//
|
||||||
|
// state.last_frame_seen = Some(Instant::now());
|
||||||
|
//
|
||||||
|
// assert!(state.prune_stale().is_none());
|
||||||
|
// assert_eq!(state.summary_points, vec![30.0]);
|
||||||
|
// assert_eq!(state.panels.len(), 1);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
56
src/serial_core/record.rs
Normal file
56
src/serial_core/record.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FrameTiming {
|
||||||
|
pub pts_ms: Option<u64>,
|
||||||
|
pub dts_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RecordedFrame<F> {
|
||||||
|
pub timing: FrameTiming,
|
||||||
|
pub frame: F
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct Recording<F> {
|
||||||
|
pub frames: Vec<RecordedFrame<F>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F> Recording<F> {
|
||||||
|
pub fn new() -> Recording<F> { Self { frames: Vec::new() } }
|
||||||
|
pub fn push(&mut self, ite: RecordedFrame<F>) {
|
||||||
|
self.frames.push(ite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CsvExporter<F> {
|
||||||
|
type Error: std::error::Error + Send + Sync + 'static;
|
||||||
|
fn csv_header(&self, recording: &Recording<F>) -> Vec<String>;
|
||||||
|
fn csv_row(&self, item: &RecordedFrame<F>) -> anyhow::Result<Vec<String>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: CsvImporter
|
||||||
|
pub trait CsvImporter<P> {
|
||||||
|
fn load<R: std::io::Read>(&mut self, reader: R) -> anyhow::Result<Vec<P>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_csv<F, E, W>(
|
||||||
|
recording: &Recording<F>,
|
||||||
|
exporter: &E,
|
||||||
|
writer: W,
|
||||||
|
) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
E: CsvExporter<F>,
|
||||||
|
W: std::io::Write,
|
||||||
|
{
|
||||||
|
let header = exporter.csv_header(&recording);
|
||||||
|
let mut wrt = csv::Writer::from_writer(writer);
|
||||||
|
wrt.write_record(header)?;
|
||||||
|
for f in &recording.frames {
|
||||||
|
let row = exporter.csv_row(f)?;
|
||||||
|
wrt.write_record(&row)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrt.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
251
src/serial_core/serial.rs
Normal file
251
src/serial_core/serial.rs
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
use crate::serial_core::codec::Codec;
|
||||||
|
use crate::serial_core::codecs::tactile_a::TactileACodec;
|
||||||
|
use crate::serial_core::frame::{FrameHandler, TactileAFrame, TestFrame};
|
||||||
|
use crate::serial_core::model::{HudChartState, HudPacket};
|
||||||
|
use crate::serial_core::record::Recording;
|
||||||
|
use anyhow::Result;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::time::{self, Duration, MissedTickBehavior};
|
||||||
|
use tokio_serial::SerialStream;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use std::future::pending;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Instant;
|
||||||
|
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
||||||
|
|
||||||
|
pub enum PollMode<F> {
|
||||||
|
Disable,
|
||||||
|
Enabled(Box<dyn PollRequester<F>>)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SerialFrame: Clone + Send + 'static {
|
||||||
|
fn dts_ms(&self) -> u64;
|
||||||
|
|
||||||
|
fn to_hud_packet(
|
||||||
|
&self,
|
||||||
|
chart_state: &mut HudChartState,
|
||||||
|
display_values: Option<&[i32]>,
|
||||||
|
) -> Option<HudPacket>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub trait PollRequester<F>: Send {
|
||||||
|
fn poll_interval(&self) -> Option<Duration> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_request(&mut self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_request(&mut self) -> Result<Option<F>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_rx_frame(&mut self, _frame: &F) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NoopPollRequester;
|
||||||
|
|
||||||
|
impl<F> PollRequester<F> for NoopPollRequester {}
|
||||||
|
|
||||||
|
pub struct TactileAPollRequester {
|
||||||
|
period: Duration,
|
||||||
|
cols: usize,
|
||||||
|
rows: usize,
|
||||||
|
awaiting_reply: bool,
|
||||||
|
last_request_at: Option<Instant>,
|
||||||
|
reply_timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TactileAPollRequester {
|
||||||
|
pub fn new(period: Duration, cols: usize, rows: usize, reply_timeout: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
period,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
awaiting_reply: false,
|
||||||
|
last_request_at: None,
|
||||||
|
reply_timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PollRequester<TactileAFrame> for TactileAPollRequester {
|
||||||
|
fn poll_interval(&self) -> Option<Duration> {
|
||||||
|
Some(self.period)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_request(&mut self) -> bool {
|
||||||
|
if !self.awaiting_reply {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let timed_out = self
|
||||||
|
.last_request_at
|
||||||
|
.map(|t| t.elapsed() >= self.reply_timeout)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if timed_out {
|
||||||
|
self.awaiting_reply = false;
|
||||||
|
self.last_request_at = None;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_request(&mut self) -> Result<Option<TactileAFrame>> {
|
||||||
|
let req = TactileACodec::build_req_frame(self.cols, self.rows)?;
|
||||||
|
self.awaiting_reply = true;
|
||||||
|
self.last_request_at = Some(Instant::now());
|
||||||
|
Ok(Some(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_rx_frame(&mut self, frame: &TactileAFrame) {
|
||||||
|
if matches!(frame, TactileAFrame::Rep(_)) {
|
||||||
|
self.awaiting_reply = false;
|
||||||
|
self.last_request_at = None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_serial<C, H, T, F>(
|
||||||
|
port: SerialStream,
|
||||||
|
codec: C,
|
||||||
|
handler: H,
|
||||||
|
session_started_at: Instant,
|
||||||
|
recording: Arc<Mutex<Recording<F>>>,
|
||||||
|
cancel: CancellationToken,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
F: SerialFrame,
|
||||||
|
C: Codec<F> + Send + 'static,
|
||||||
|
H: FrameHandler<F, T> + Send + 'static,
|
||||||
|
T: Into<i32>
|
||||||
|
{
|
||||||
|
run_serial_with_poll(
|
||||||
|
port, codec, handler, session_started_at, recording, cancel, PollMode::Disable
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_serial_with_poll<C, H, T, F>(
|
||||||
|
mut port: SerialStream,
|
||||||
|
mut codec: C,
|
||||||
|
mut handler: H,
|
||||||
|
session_started_at: Instant,
|
||||||
|
recording: Arc<Mutex<Recording<F>>>,
|
||||||
|
cancel: CancellationToken,
|
||||||
|
poll_mode: PollMode<F>
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
F: SerialFrame,
|
||||||
|
C: Codec<F> + Send + 'static,
|
||||||
|
H: FrameHandler<F, T> + Send + 'static,
|
||||||
|
T: Into<i32>,
|
||||||
|
{
|
||||||
|
let mut requester = match poll_mode {
|
||||||
|
PollMode::Disable => None,
|
||||||
|
PollMode::Enabled(r) => Some(r),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut poll_interval = requester
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|r| r.poll_interval())
|
||||||
|
.map(|d| {
|
||||||
|
let mut it = time::interval(d);
|
||||||
|
it.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||||
|
it
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut chart_state = HudChartState::new();
|
||||||
|
let mut buffer = [0u8; 1024];
|
||||||
|
let mut prune_interval = time::interval(Duration::from_millis(450));
|
||||||
|
prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||||
|
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.cancelled() => break,
|
||||||
|
_ = async {
|
||||||
|
match poll_interval.as_mut() {
|
||||||
|
Some(it) => {
|
||||||
|
it.tick().await;
|
||||||
|
}
|
||||||
|
None => pending::<()>().await,
|
||||||
|
}
|
||||||
|
} => {
|
||||||
|
if let Some(r) = requester.as_mut() {
|
||||||
|
if r.should_request() {
|
||||||
|
if let Some(req) = r.next_request()? {
|
||||||
|
let bytes = codec.encode(&req)?;
|
||||||
|
port.write_all(&bytes).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read_result = port.read(&mut buffer) => {
|
||||||
|
let n = read_result?;
|
||||||
|
if n == 0 {
|
||||||
|
// Some serial drivers can resolve reads with 0 bytes repeatedly.
|
||||||
|
// Yield here so timer-driven poll requests are not starved by a busy loop.
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let frames = codec.decode(&buffer[..n], session_started_at)?;
|
||||||
|
for frame in frames {
|
||||||
|
if let Some(r) = requester.as_mut() {
|
||||||
|
r.on_rx_frame(&frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
let decode_res = handler
|
||||||
|
.on_frame(&frame)
|
||||||
|
.await?
|
||||||
|
.map(|vals| vals.into_iter().map(Into::into).collect::<Vec<i32>>());
|
||||||
|
|
||||||
|
let mut record = recording.lock().map_err(|_| anyhow::anyhow!("recording state poisoned"))?;
|
||||||
|
record.push(RecordedFrame{
|
||||||
|
timing: FrameTiming { pts_ms: None, dts_ms: frame.dts_ms() },
|
||||||
|
frame: frame.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_to_g1(raw: u32) -> f64 {
|
||||||
|
const X: [u32; 11] = [
|
||||||
|
0, 74602, 105503, 131459, 153512, 172041, 193794, 218947, 240580, 295118, 332346,
|
||||||
|
];
|
||||||
|
|
||||||
|
const Y: [f64; 11] = [
|
||||||
|
0.0, 160.0, 260.0, 360.0, 460.0, 560.0, 660.0, 860.0, 1060.0, 1560.0, 2060.0,
|
||||||
|
];
|
||||||
|
|
||||||
|
let n = X.len();
|
||||||
|
if raw <= X[0] {
|
||||||
|
return Y[0] / 100.0;
|
||||||
|
}
|
||||||
|
if raw >= X[n - 1] {
|
||||||
|
return Y[n - 1] / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut left = 0;
|
||||||
|
let mut right = n - 1;
|
||||||
|
|
||||||
|
while left + 1 < right {
|
||||||
|
let mid = (left + right) / 2;
|
||||||
|
if raw < X[mid] {
|
||||||
|
right = mid;
|
||||||
|
} else {
|
||||||
|
left = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ratio = (raw - X[left]) as f64 / (X[right] - X[left]) as f64;
|
||||||
|
Y[left] / 100.0 + ratio * (Y[right] - Y[left]) / 100.0
|
||||||
|
}
|
||||||
59
src/serial_core/utils.rs
Normal file
59
src/serial_core/utils.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
pub fn usize_to_u16_be_bytes(n: usize) -> [u8; 2] {
|
||||||
|
(n as u16).to_be_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn usize_to_u16_le_bytes(n: usize) -> [u8; 2] {
|
||||||
|
(n as u16).to_be_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn u16_to_hex_be_bytes(n: u16) -> [u8; 2] {
|
||||||
|
(n as u16).to_be_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn u16_to_hex_le_bytes(n: u16) -> [u8; 2] {
|
||||||
|
(n as u16).to_le_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calc_crc8_smbus(c: &[u8]) -> u8 {
|
||||||
|
let crc8_smbus = crc::Crc::<u8>::new(&crc::CRC_8_SMBUS);
|
||||||
|
let checksum = crc8_smbus.checksum(c);
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calc_crc8_itu(c: &[u8]) -> u8 {
|
||||||
|
let crc8_itu_alg = crc::Crc::<u8>::new(&crc::CRC_8_I_432_1);
|
||||||
|
let checksum = crc8_itu_alg.checksum(c);
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn elapsed_millis(start_at: Instant) -> u64 {
|
||||||
|
start_at.elapsed().as_millis() as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use anyhow::Ok;
|
||||||
|
|
||||||
|
use crate::serial_core::utils::{calc_crc8_itu, calc_crc8_smbus};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_crc8_itu() -> anyhow::Result<()> {
|
||||||
|
let req_vec = vec![0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB, 0x00, 0x1C, 0x00, 0x00, 0x18, 0x00];
|
||||||
|
let checksum = calc_crc8_itu(req_vec.as_slice());
|
||||||
|
assert_eq!(checksum, 0x7A);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_crc8_smbus() -> anyhow::Result<()> {
|
||||||
|
let req_vec = vec![0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB, 0x00, 0x1C, 0x00, 0x00, 0x18, 0x00];
|
||||||
|
let checksum = calc_crc8_smbus(req_vec.as_slice());
|
||||||
|
assert_eq!(checksum, 0x2F);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user