refactor(serial): 新增 hand_gateway 编解码模块,修正模块名拼写
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::serial_core::codecs::hand_gateway::HandGatewayCodec;
|
||||
use crate::serial_core::codecs::tactile_a::{
|
||||
export_recording_csv, TactileACodec, TactileACsvImporter, TactileAHandler,
|
||||
};
|
||||
@@ -6,7 +7,7 @@ use crate::serial_core::record::CsvImporter;
|
||||
use crate::serial_core::serial::{PollMode, TactileAPollRequester};
|
||||
use crate::serial_core::{serial, TactileARecording};
|
||||
use log::info;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::Cursor;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -21,6 +22,10 @@ const DEFAULT_TACTILE_ROWS: usize = 12;
|
||||
const DEFAULT_TACTILE_POLL_INTERVAL_MS: u64 = 10;
|
||||
const DEFAULT_TACTILE_REPLY_TIMEOUT_MS: u64 = 140;
|
||||
|
||||
// MCU currently uses a fixed node layout. Update this list to match the firmware:
|
||||
// one entry per Config bit, containing that node's number of u16 samples.
|
||||
const DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS: &[u16] = &[84];
|
||||
|
||||
type SharedTactileRecording = Arc<Mutex<TactileARecording>>;
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -31,6 +36,13 @@ pub struct SerialConnectResponse {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SerialProtocol {
|
||||
TactileA,
|
||||
HandGateway,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SerialExportResponse {
|
||||
@@ -67,7 +79,7 @@ struct SerialSession {
|
||||
port: String,
|
||||
cancel: CancellationToken,
|
||||
task: JoinHandle<()>,
|
||||
current_record: SharedTactileRecording,
|
||||
current_record: Option<SharedTactileRecording>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -78,7 +90,7 @@ pub struct SerialConnectionState {
|
||||
|
||||
pub async fn shutdown_active_session(
|
||||
state: &SerialConnectionState,
|
||||
) -> Result<Option<(String, SharedTactileRecording)>, SerialError> {
|
||||
) -> Result<Option<(String, Option<SharedTactileRecording>)>, SerialError> {
|
||||
let session = {
|
||||
let mut guard = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||
guard.take()
|
||||
@@ -98,13 +110,15 @@ pub async fn shutdown_active_session(
|
||||
let _ = task.await;
|
||||
|
||||
let frame_count = current_record
|
||||
.lock()
|
||||
.map(|record| record.frames.len())
|
||||
.as_ref()
|
||||
.and_then(|record| record.lock().ok().map(|record| record.frames.len()))
|
||||
.unwrap_or(0);
|
||||
|
||||
info!("last_record has {} frames", frame_count);
|
||||
|
||||
if let Ok(mut last_record) = state.last_record.lock() {
|
||||
if let (Some(current_record), Ok(mut last_record)) =
|
||||
(current_record.as_ref(), state.last_record.lock())
|
||||
{
|
||||
*last_record = Some(current_record.clone());
|
||||
}
|
||||
|
||||
@@ -133,6 +147,7 @@ pub fn serial_enum() -> Result<Vec<String>, SerialError> {
|
||||
pub async fn serial_connect(
|
||||
app: AppHandle,
|
||||
port: String,
|
||||
protocol: SerialProtocol,
|
||||
state: State<'_, SerialConnectionState>,
|
||||
) -> Result<SerialConnectResponse, SerialError> {
|
||||
let port_name = port.trim().to_string();
|
||||
@@ -148,7 +163,10 @@ pub async fn serial_connect(
|
||||
}
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let current_record = Arc::new(Mutex::new(TactileARecording::new()));
|
||||
let current_record = match protocol {
|
||||
SerialProtocol::TactileA => Some(Arc::new(Mutex::new(TactileARecording::new()))),
|
||||
SerialProtocol::HandGateway => None,
|
||||
};
|
||||
let task_record = current_record.clone();
|
||||
let task_cancel = cancel.clone();
|
||||
let task_app = app.clone();
|
||||
@@ -160,32 +178,51 @@ pub async fn serial_connect(
|
||||
let session_started_at = Instant::now();
|
||||
|
||||
let task = tauri::async_runtime::spawn(async move {
|
||||
let codec = TactileACodec::new(DEFAULT_TACTILE_COLS, DEFAULT_TACTILE_ROWS);
|
||||
let handler = TactileAHandler;
|
||||
let poll_mode = PollMode::Enabled(Box::new(TactileAPollRequester::new(
|
||||
Duration::from_millis(DEFAULT_TACTILE_POLL_INTERVAL_MS),
|
||||
DEFAULT_TACTILE_COLS,
|
||||
DEFAULT_TACTILE_ROWS,
|
||||
Duration::from_millis(DEFAULT_TACTILE_REPLY_TIMEOUT_MS),
|
||||
)));
|
||||
let run_result = match protocol {
|
||||
SerialProtocol::TactileA => {
|
||||
let codec = TactileACodec::new(DEFAULT_TACTILE_COLS, DEFAULT_TACTILE_ROWS);
|
||||
let handler = TactileAHandler;
|
||||
let poll_mode = PollMode::Enabled(Box::new(TactileAPollRequester::new(
|
||||
Duration::from_millis(DEFAULT_TACTILE_POLL_INTERVAL_MS),
|
||||
DEFAULT_TACTILE_COLS,
|
||||
DEFAULT_TACTILE_ROWS,
|
||||
Duration::from_millis(DEFAULT_TACTILE_REPLY_TIMEOUT_MS),
|
||||
)));
|
||||
|
||||
if let Err(error) = serial::run_serial_with_poll(
|
||||
task_app.clone(),
|
||||
port,
|
||||
codec,
|
||||
handler,
|
||||
session_started_at,
|
||||
task_record.clone(),
|
||||
task_cancel,
|
||||
poll_mode,
|
||||
)
|
||||
.await
|
||||
{
|
||||
serial::run_serial_with_poll(
|
||||
task_app.clone(),
|
||||
port,
|
||||
codec,
|
||||
handler,
|
||||
session_started_at,
|
||||
task_record
|
||||
.clone()
|
||||
.expect("tactile protocol must have a recording"),
|
||||
task_cancel,
|
||||
poll_mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
SerialProtocol::HandGateway => {
|
||||
let codec = HandGatewayCodec::new(DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS);
|
||||
serial::run_hand_gateway_serial(
|
||||
task_app.clone(),
|
||||
port,
|
||||
codec,
|
||||
session_started_at,
|
||||
task_cancel,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = run_result {
|
||||
eprintln!("serial task exited with error: {error}");
|
||||
}
|
||||
|
||||
let manager = task_app.state::<SerialConnectionState>();
|
||||
if let Ok(mut last_record) = manager.last_record.lock() {
|
||||
if let (Some(task_record), Ok(mut last_record)) = (task_record, manager.last_record.lock())
|
||||
{
|
||||
*last_record = Some(task_record);
|
||||
}
|
||||
|
||||
@@ -360,7 +397,7 @@ fn resolve_record_for_export(
|
||||
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||
session
|
||||
.as_ref()
|
||||
.map(|current_session| current_session.current_record.clone())
|
||||
.and_then(|current_session| current_session.current_record.clone())
|
||||
};
|
||||
|
||||
if let Some(recording) = current_record {
|
||||
@@ -381,7 +418,7 @@ fn snapshot_record_frame_count(
|
||||
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||
session
|
||||
.as_ref()
|
||||
.map(|current_session| current_session.current_record.clone())
|
||||
.and_then(|current_session| current_session.current_record.clone())
|
||||
};
|
||||
|
||||
if let Some(record) = current_record {
|
||||
|
||||
390
src-tauri/src/serial_core/codecs/hand_gateway.rs
Normal file
390
src-tauri/src/serial_core/codecs/hand_gateway.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
use crate::serial_core::codec::Codec;
|
||||
use crate::serial_core::error::CodecError;
|
||||
use crate::serial_core::frame::{
|
||||
HandGatewayDataRepFrame, HandGatewayFrame, HandGatewayFrameMetaData, HandGatewayFrameNode,
|
||||
};
|
||||
use crate::serial_core::utils::{calc_crc8_itu, elapsed_millis};
|
||||
use log::debug;
|
||||
use std::time::Instant;
|
||||
|
||||
const FRAME_HEADER: [u8; 2] = [0x55, 0xAA];
|
||||
const RESPONSE_DATA_CMD: u8 = 0x81;
|
||||
const RESPONSE_DATA_FIXED_LENGTH: usize = 16;
|
||||
const RESPONSE_DATA_PREFIX_LENGTH: usize = 19;
|
||||
const MIN_FRAME_LENGTH: usize = 7;
|
||||
const DEFAULT_MAX_FRAME_LENGTH: usize = 64 * 1024;
|
||||
|
||||
/// Describes one fixed MCU data block.
|
||||
///
|
||||
/// `config_mask` identifies the bit associated with this block in the protocol's
|
||||
/// Config/ValidConfig fields. `sample_count` is the number of little-endian u16
|
||||
/// samples in the block.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HandGatewayNodeConfig {
|
||||
pub config_mask: u32,
|
||||
pub sample_count: usize,
|
||||
}
|
||||
|
||||
impl HandGatewayNodeConfig {
|
||||
pub const fn new(config_mask: u32, sample_count: usize) -> Self {
|
||||
Self {
|
||||
config_mask,
|
||||
sample_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn payload_len(&self, bytes_per_sample: usize) -> Option<usize> {
|
||||
self.sample_count.checked_mul(bytes_per_sample)
|
||||
}
|
||||
}
|
||||
|
||||
/// Host-side description of the configuration currently hard-coded in the MCU.
|
||||
///
|
||||
/// Keep the node order identical to the MCU Config bit order. When the MCU gains
|
||||
/// GET_CONFIG support, construct this value from that response instead of changing
|
||||
/// the decoder.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HandGatewayConfig {
|
||||
pub protocol_version: u8,
|
||||
pub bytes_per_sample: usize,
|
||||
pub max_frame_length: usize,
|
||||
pub nodes: Vec<HandGatewayNodeConfig>,
|
||||
}
|
||||
|
||||
impl HandGatewayConfig {
|
||||
pub fn new(nodes: Vec<HandGatewayNodeConfig>) -> Self {
|
||||
Self {
|
||||
protocol_version: 0x01,
|
||||
bytes_per_sample: 2,
|
||||
max_frame_length: DEFAULT_MAX_FRAME_LENGTH,
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), CodecError> {
|
||||
if self.bytes_per_sample == 0 || self.max_frame_length < MIN_FRAME_LENGTH {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
let mut used_masks = 0u32;
|
||||
for node in &self.nodes {
|
||||
if node.config_mask == 0
|
||||
|| node.config_mask.count_ones() != 1
|
||||
|| used_masks & node.config_mask != 0
|
||||
|| node.payload_len(self.bytes_per_sample).is_none()
|
||||
{
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
used_masks |= node.config_mask;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HandGatewayCodec {
|
||||
buffer: Vec<u8>,
|
||||
config: HandGatewayConfig,
|
||||
}
|
||||
|
||||
impl HandGatewayCodec {
|
||||
/// Convenience constructor for sequential Config bits (bit 0, bit 1, ...).
|
||||
/// Each entry is the u16 sample count of one MCU node.
|
||||
pub fn new(node_sample_counts: &[u16]) -> Self {
|
||||
let nodes = node_sample_counts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, &sample_count)| {
|
||||
let config_mask = 1u32.checked_shl(index as u32).unwrap_or(0);
|
||||
HandGatewayNodeConfig::new(config_mask, usize::from(sample_count))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
buffer: Vec::new(),
|
||||
config: HandGatewayConfig::new(nodes),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_config(config: HandGatewayConfig) -> Result<Self, CodecError> {
|
||||
config.validate()?;
|
||||
Ok(Self {
|
||||
buffer: Vec::new(),
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &HandGatewayConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
pub fn parse_node_payload(data: &[u8]) -> Result<Vec<u16>, CodecError> {
|
||||
if data.len() % 2 != 0 {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
Ok(data
|
||||
.chunks_exact(2)
|
||||
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn retain_possible_header_prefix(&mut self) {
|
||||
if self.buffer.last() == Some(&FRAME_HEADER[0]) {
|
||||
self.buffer.drain(..self.buffer.len() - 1);
|
||||
} else {
|
||||
self.buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_data_response(
|
||||
&self,
|
||||
frame: &[u8],
|
||||
length: u16,
|
||||
session_started_at: Instant,
|
||||
) -> Result<HandGatewayFrame, CodecError> {
|
||||
let payload_len = usize::from(length)
|
||||
.checked_sub(RESPONSE_DATA_FIXED_LENGTH)
|
||||
.ok_or(CodecError::InvalidLength)?;
|
||||
let payload_end = RESPONSE_DATA_PREFIX_LENGTH
|
||||
.checked_add(payload_len)
|
||||
.ok_or(CodecError::PayloadTooLarge)?;
|
||||
|
||||
if payload_end + 1 != frame.len() {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
let version = frame[5];
|
||||
if version != self.config.protocol_version {
|
||||
return Err(CodecError::InvalidFrameType);
|
||||
}
|
||||
|
||||
let timestamp = u32::from_le_bytes(frame[6..10].try_into().unwrap());
|
||||
let config = u32::from_le_bytes(frame[10..14].try_into().unwrap());
|
||||
let valid_config = u32::from_le_bytes(frame[14..18].try_into().unwrap());
|
||||
let block_count = frame[18];
|
||||
|
||||
let active_nodes: Vec<&HandGatewayNodeConfig> = self
|
||||
.config
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| config & node.config_mask != 0)
|
||||
.collect();
|
||||
|
||||
if active_nodes.len() != usize::from(block_count) {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
let expected_payload_len = active_nodes.iter().try_fold(0usize, |total, node| {
|
||||
let node_len = node
|
||||
.payload_len(self.config.bytes_per_sample)
|
||||
.ok_or(CodecError::PayloadTooLarge)?;
|
||||
total
|
||||
.checked_add(node_len)
|
||||
.ok_or(CodecError::PayloadTooLarge)
|
||||
})?;
|
||||
|
||||
if expected_payload_len != payload_len {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
let mut cursor = RESPONSE_DATA_PREFIX_LENGTH;
|
||||
let mut nodes = Vec::with_capacity(active_nodes.len());
|
||||
for node in active_nodes {
|
||||
let node_len = node
|
||||
.payload_len(self.config.bytes_per_sample)
|
||||
.ok_or(CodecError::PayloadTooLarge)?;
|
||||
let next = cursor + node_len;
|
||||
nodes.push(HandGatewayFrameNode {
|
||||
config_mask: node.config_mask,
|
||||
valid: valid_config & node.config_mask != 0,
|
||||
payload: frame[cursor..next].to_vec(),
|
||||
});
|
||||
cursor = next;
|
||||
}
|
||||
|
||||
Ok(HandGatewayFrame::DataRep(HandGatewayDataRepFrame {
|
||||
meta: HandGatewayFrameMetaData {
|
||||
header: FRAME_HEADER,
|
||||
length,
|
||||
cmd: RESPONSE_DATA_CMD,
|
||||
version,
|
||||
checksum: frame[frame.len() - 1],
|
||||
},
|
||||
timestamp,
|
||||
config,
|
||||
valid_config,
|
||||
block_count,
|
||||
nodes,
|
||||
dts_ms: elapsed_millis(session_started_at),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Codec<HandGatewayFrame> for HandGatewayCodec {
|
||||
fn decode(
|
||||
&mut self,
|
||||
input: &[u8],
|
||||
session_started_at: Instant,
|
||||
) -> Result<Vec<HandGatewayFrame>, CodecError> {
|
||||
self.config.validate()?;
|
||||
self.buffer.extend_from_slice(input);
|
||||
let mut frames = Vec::new();
|
||||
|
||||
loop {
|
||||
let Some(header_pos) = self
|
||||
.buffer
|
||||
.windows(FRAME_HEADER.len())
|
||||
.position(|window| window == FRAME_HEADER)
|
||||
else {
|
||||
self.retain_possible_header_prefix();
|
||||
break;
|
||||
};
|
||||
|
||||
if header_pos > 0 {
|
||||
self.buffer.drain(..header_pos);
|
||||
}
|
||||
|
||||
if self.buffer.len() < 4 {
|
||||
break;
|
||||
}
|
||||
|
||||
let length = u16::from_le_bytes([self.buffer[2], self.buffer[3]]);
|
||||
let frame_len = usize::from(length)
|
||||
.checked_add(4)
|
||||
.ok_or(CodecError::PayloadTooLarge)?;
|
||||
|
||||
if frame_len < MIN_FRAME_LENGTH || frame_len > self.config.max_frame_length {
|
||||
debug!("invalid hand gateway frame length: {frame_len}");
|
||||
self.buffer.drain(..1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.buffer.len() < frame_len {
|
||||
break;
|
||||
}
|
||||
|
||||
let expected_checksum = calc_crc8_itu(&self.buffer[..frame_len - 1]);
|
||||
let received_checksum = self.buffer[frame_len - 1];
|
||||
if expected_checksum != received_checksum {
|
||||
debug!(
|
||||
"hand gateway checksum mismatch: expected {expected_checksum:02X}, got {received_checksum:02X}"
|
||||
);
|
||||
self.buffer.drain(..1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.buffer[4] == RESPONSE_DATA_CMD {
|
||||
match self.parse_data_response(
|
||||
&self.buffer[..frame_len],
|
||||
length,
|
||||
session_started_at,
|
||||
) {
|
||||
Ok(frame) => frames.push(frame),
|
||||
Err(error) => debug!("invalid hand gateway data response: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
self.buffer.drain(..frame_len);
|
||||
}
|
||||
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
fn encode(&self, _frame: &HandGatewayFrame) -> Result<Vec<u8>, CodecError> {
|
||||
Err(CodecError::InvalidFrameType)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_codec() -> HandGatewayCodec {
|
||||
HandGatewayCodec::with_config(HandGatewayConfig::new(vec![
|
||||
HandGatewayNodeConfig::new(1 << 0, 2),
|
||||
HandGatewayNodeConfig::new(1 << 1, 1),
|
||||
]))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn data_response(config: u32, valid_config: u32, payload: &[u8]) -> Vec<u8> {
|
||||
let block_count = config.count_ones() as u8;
|
||||
let length = (RESPONSE_DATA_FIXED_LENGTH + payload.len()) as u16;
|
||||
let mut frame = Vec::new();
|
||||
frame.extend_from_slice(&FRAME_HEADER);
|
||||
frame.extend_from_slice(&length.to_le_bytes());
|
||||
frame.push(RESPONSE_DATA_CMD);
|
||||
frame.push(0x01);
|
||||
frame.extend_from_slice(&123_456u32.to_le_bytes());
|
||||
frame.extend_from_slice(&config.to_le_bytes());
|
||||
frame.extend_from_slice(&valid_config.to_le_bytes());
|
||||
frame.push(block_count);
|
||||
frame.extend_from_slice(payload);
|
||||
frame.push(calc_crc8_itu(&frame));
|
||||
frame
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_configured_nodes() {
|
||||
let mut codec = test_codec();
|
||||
let bytes = data_response(0b11, 0b01, &[1, 0, 2, 0, 3, 0]);
|
||||
let frames = codec.decode(&bytes, Instant::now()).unwrap();
|
||||
|
||||
let HandGatewayFrame::DataRep(frame) = &frames[0];
|
||||
assert_eq!(frame.timestamp, 123_456);
|
||||
assert_eq!(frame.block_count, 2);
|
||||
assert_eq!(frame.nodes.len(), 2);
|
||||
assert_eq!(frame.nodes[0].config_mask, 1);
|
||||
assert!(frame.nodes[0].valid);
|
||||
assert_eq!(
|
||||
HandGatewayCodec::parse_node_payload(&frame.nodes[0].payload).unwrap(),
|
||||
[1, 2]
|
||||
);
|
||||
assert!(!frame.nodes[1].valid);
|
||||
assert_eq!(
|
||||
HandGatewayCodec::parse_node_payload(&frame.nodes[1].payload).unwrap(),
|
||||
[3]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_split_and_sticky_packets() {
|
||||
let mut codec = test_codec();
|
||||
let frame = data_response(0b01, 0b01, &[1, 0, 2, 0]);
|
||||
let split = frame.len() / 2;
|
||||
|
||||
assert!(codec
|
||||
.decode(&frame[..split], Instant::now())
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
|
||||
let mut remainder_and_next = frame[split..].to_vec();
|
||||
remainder_and_next.extend_from_slice(&frame);
|
||||
let frames = codec.decode(&remainder_and_next, Instant::now()).unwrap();
|
||||
assert_eq!(frames.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resynchronizes_after_noise_and_bad_crc() {
|
||||
let mut codec = test_codec();
|
||||
let valid = data_response(0b10, 0b10, &[9, 0]);
|
||||
let mut bad = valid.clone();
|
||||
let last = bad.len() - 1;
|
||||
bad[last] ^= 0xFF;
|
||||
|
||||
let mut bytes = vec![0x13, 0x55, 0x12];
|
||||
bytes.extend_from_slice(&bad);
|
||||
bytes.extend_from_slice(&valid);
|
||||
|
||||
let frames = codec.decode(&bytes, Instant::now()).unwrap();
|
||||
assert_eq!(frames.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_payload_that_does_not_match_fixed_config() {
|
||||
let mut codec = test_codec();
|
||||
let bytes = data_response(0b01, 0b01, &[1, 0]);
|
||||
assert!(codec.decode(&bytes, Instant::now()).unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@ use crate::serial_core::{frame::TestFrame, record::Recording};
|
||||
|
||||
pub mod test;
|
||||
pub mod tactile_a;
|
||||
pub type TestRecording = Recording<TestFrame>;
|
||||
pub mod hand_gateway;
|
||||
pub type TestRecording = Recording<TestFrame>;
|
||||
|
||||
@@ -14,7 +14,7 @@ use anyhow::anyhow;
|
||||
use std::io::Read;
|
||||
use log::debug;
|
||||
|
||||
const FRAME_BUFFER_MIN_LENGTH: usize = 15;
|
||||
const FINGER_FRAME_BUFFER_MIN_LENGTH: usize = 15;
|
||||
|
||||
pub struct TactileACodec {
|
||||
buffer: Vec<u8>,
|
||||
@@ -123,7 +123,7 @@ impl Codec<TactileAFrame> for TactileACodec {
|
||||
let mut frames: Vec<TactileAFrame> = Vec::new();
|
||||
|
||||
loop {
|
||||
if self.buffer.len() < FRAME_BUFFER_MIN_LENGTH {
|
||||
if self.buffer.len() < FINGER_FRAME_BUFFER_MIN_LENGTH {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ impl Codec<TactileAFrame> for TactileACodec {
|
||||
self.buffer.drain(0..pos);
|
||||
}
|
||||
|
||||
if self.buffer.len() < FRAME_BUFFER_MIN_LENGTH {
|
||||
if self.buffer.len() < FINGER_FRAME_BUFFER_MIN_LENGTH {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ impl Codec<TactileAFrame> for TactileACodec {
|
||||
continue;
|
||||
}
|
||||
|
||||
let frame_length = except_data_len + FRAME_BUFFER_MIN_LENGTH;
|
||||
let frame_length = except_data_len + FINGER_FRAME_BUFFER_MIN_LENGTH;
|
||||
if self.buffer.len() < frame_length {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ pub struct TestFrame {
|
||||
pub length: usize,
|
||||
pub payload: Vec<u8>,
|
||||
pub checksum: u8,
|
||||
pub dts_ms: u64
|
||||
pub dts_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -30,28 +30,59 @@ pub struct TactileAReqFrame {
|
||||
pub meta: TactileAFrameMetaData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TactileARepFrame {
|
||||
pub meta: TactileAFrameMetaData,
|
||||
pub status: TactileAFrameStatusCode,
|
||||
pub payload: Vec<u8>,
|
||||
pub dts_ms: u64
|
||||
pub dts_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TactileAFrameStatusCode {
|
||||
Success,
|
||||
Failure
|
||||
Failure,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TactileAFrame {
|
||||
Req(TactileAReqFrame),
|
||||
Rep(TactileARepFrame)
|
||||
Rep(TactileARepFrame),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HandGatewayFrameMetaData {
|
||||
pub header: [u8; 2],
|
||||
pub length: u16,
|
||||
pub cmd: u8,
|
||||
pub version: u8,
|
||||
pub checksum: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HandGatewayFrameNode {
|
||||
pub config_mask: u32,
|
||||
pub valid: bool,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HandGatewayDataRepFrame {
|
||||
pub meta: HandGatewayFrameMetaData,
|
||||
pub timestamp: u32,
|
||||
pub config: u32,
|
||||
pub valid_config: u32,
|
||||
pub block_count: u8,
|
||||
pub nodes: Vec<HandGatewayFrameNode>,
|
||||
pub dts_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum HandGatewayFrame {
|
||||
DataRep(HandGatewayDataRepFrame),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait FrameHandler<F, T>: Send {
|
||||
async fn on_frame(&mut self, frame: &F) -> Result<Option<Vec<T>>>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::ad_solver::solve_for_x;
|
||||
#[cfg(feature = "devkit")]
|
||||
use crate::devkit::{proto::SensorFrame, DevKitState};
|
||||
use crate::serial_core::codec::Codec;
|
||||
use crate::serial_core::codecs::hand_gateway::HandGatewayCodec;
|
||||
use crate::serial_core::codecs::tactile_a::TactileACodec;
|
||||
use crate::serial_core::frame::{FrameHandler, TactileAFrame, TestFrame};
|
||||
use crate::serial_core::model::{HudChartState, HudPacket, HudSpatialForce};
|
||||
@@ -10,6 +12,7 @@ use crate::serial_core::record::Recording;
|
||||
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
||||
use anyhow::Result;
|
||||
use log::debug;
|
||||
use log::info;
|
||||
use std::future::pending;
|
||||
#[cfg(feature = "devkit")]
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -22,7 +25,6 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::time::{self, Duration, MissedTickBehavior};
|
||||
use tokio_serial::SerialStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use crate::ad_solver::solve_for_x;
|
||||
const AUTO_SUB_INTERVAL: Duration = Duration::from_nanos(16_666_667);
|
||||
|
||||
pub enum PollMode<F> {
|
||||
@@ -200,6 +202,61 @@ where
|
||||
.await
|
||||
}
|
||||
|
||||
/// Runs the hand gateway in passive mode. The MCU continuously uploads data, so
|
||||
/// this loop never writes request frames to the serial port.
|
||||
pub async fn run_hand_gateway_serial(
|
||||
app: AppHandle,
|
||||
mut port: SerialStream,
|
||||
mut codec: HandGatewayCodec,
|
||||
session_started_at: Instant,
|
||||
cancel: CancellationToken,
|
||||
) -> Result<()> {
|
||||
let mut buffer = [0u8; 4096];
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => break,
|
||||
read_result = port.read(&mut buffer) => {
|
||||
let n = read_result?;
|
||||
if n == 0 {
|
||||
tokio::task::yield_now().await;
|
||||
continue;
|
||||
}
|
||||
|
||||
let raw = &buffer[..n];
|
||||
app.emit("hand_gateway_raw", raw.to_vec())?;
|
||||
let raw_hex = raw
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
info!("[hand_gateway][raw] {raw_hex}");
|
||||
|
||||
for frame in codec.decode(raw, session_started_at)? {
|
||||
let crate::serial_core::frame::HandGatewayFrame::DataRep(frame) = frame;
|
||||
info!(
|
||||
"[hand_gateway][frame] timestamp_us={} config=0x{:08X} valid_config=0x{:08X} block_count={}",
|
||||
frame.timestamp,
|
||||
frame.config,
|
||||
frame.valid_config,
|
||||
frame.block_count,
|
||||
);
|
||||
for node in frame.nodes {
|
||||
info!(
|
||||
"[hand_gateway][node] mask=0x{:08X} valid={} payload={:02X?}",
|
||||
node.config_mask,
|
||||
node.valid,
|
||||
node.payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_serial_with_poll<C, H, T, F>(
|
||||
app: AppHandle,
|
||||
mut port: SerialStream,
|
||||
@@ -333,7 +390,7 @@ where
|
||||
{
|
||||
let summary = vals.iter().copied().sum::<i32>();
|
||||
let force = raw_to_g1(summary as u32);
|
||||
|
||||
|
||||
push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
||||
}
|
||||
|
||||
@@ -421,7 +478,8 @@ fn infer_matrix_shape(len: usize) -> (u32, u32) {
|
||||
|
||||
fn raw_to_g1(raw: u32) -> f64 {
|
||||
const X: [u32; 13] = [
|
||||
0, 16811, 41350, 79241, 94615, 127446, 149559, 175900, 195056, 237852, 267810, 322472, 378511,
|
||||
0, 16811, 41350, 79241, 94615, 127446, 149559, 175900, 195056, 237852, 267810, 322472,
|
||||
378511,
|
||||
];
|
||||
|
||||
const Y: [f64; 13] = [
|
||||
|
||||
Reference in New Issue
Block a user