Update core modules and add GUI panels
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use super::Framer;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
/// COBS (Consistent Overhead Byte Stuffing) framer.
|
||||
///
|
||||
@@ -38,9 +39,16 @@ impl Framer for CobsFramer {
|
||||
for &byte in data {
|
||||
if byte == 0x00 {
|
||||
if !self.buf.is_empty() {
|
||||
// Decode failed (corrupt packet) — discard and continue
|
||||
let buf_len = self.buf.len();
|
||||
if let Some(decoded) = cobs_decode(&self.buf) {
|
||||
debug!(
|
||||
encoded_len = buf_len,
|
||||
decoded_len = decoded.len(),
|
||||
"COBS frame decoded"
|
||||
);
|
||||
frames.push(decoded);
|
||||
} else {
|
||||
warn!(len = buf_len, "corrupt COBS packet discarded");
|
||||
}
|
||||
self.buf.clear();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::{Endian, Framer};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
/// Configuration for the length-prefixed framer.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -96,6 +97,11 @@ impl Framer for LengthPrefixedFramer {
|
||||
|
||||
// Discard length, re-sync on corrupt frame
|
||||
if self.config.max_payload > 0 && payload_len > self.config.max_payload {
|
||||
warn!(
|
||||
claimed = payload_len,
|
||||
max = self.config.max_payload,
|
||||
"corrupt length frame, skipping"
|
||||
);
|
||||
self.buf.drain(..self.config.len_bytes);
|
||||
continue;
|
||||
}
|
||||
@@ -115,6 +121,7 @@ impl Framer for LengthPrefixedFramer {
|
||||
break;
|
||||
}
|
||||
let frame: Vec<u8> = self.buf.drain(..expected).collect();
|
||||
debug!(len = frame.len(), "length-prefixed frame extracted");
|
||||
frames.push(frame);
|
||||
self.state = State::ReadingLength;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::Framer;
|
||||
use tracing::debug;
|
||||
|
||||
/// Line-delimited framer.
|
||||
///
|
||||
@@ -54,6 +55,13 @@ impl Framer for LineFramer {
|
||||
}
|
||||
}
|
||||
|
||||
if !frames.is_empty() {
|
||||
debug!(
|
||||
count = frames.len(),
|
||||
pending = self.buf.len(),
|
||||
"line frames extracted"
|
||||
);
|
||||
}
|
||||
frames
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod line;
|
||||
pub mod cobs;
|
||||
pub mod fixed;
|
||||
pub mod length;
|
||||
pub mod cobs;
|
||||
pub mod line;
|
||||
|
||||
pub use crate::protocol::Endian;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod error;
|
||||
pub mod transport;
|
||||
pub mod protocol;
|
||||
pub mod frame;
|
||||
pub mod pipeline;
|
||||
pub mod protocol;
|
||||
pub mod transport;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::frame::Framer;
|
||||
use crate::protocol::{DecodedData, ProtocolDecoder};
|
||||
use tracing::debug;
|
||||
|
||||
/// A self-contained framing + decoding pipeline.
|
||||
///
|
||||
@@ -118,6 +119,13 @@ impl MultiPipeline {
|
||||
});
|
||||
}
|
||||
}
|
||||
if !results.is_empty() {
|
||||
debug!(
|
||||
count = results.len(),
|
||||
bytes = data.len(),
|
||||
"pipeline produced results"
|
||||
);
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
@@ -168,10 +176,10 @@ mod tests {
|
||||
use crate::frame::fixed::FixedLengthFramer;
|
||||
use crate::frame::length::{LengthConfig, LengthPrefixedFramer};
|
||||
use crate::frame::line::{LineConfig, LineFramer};
|
||||
use crate::protocol::Endian;
|
||||
use crate::protocol::hex::{HexConfig, HexDecoder};
|
||||
use crate::protocol::plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType};
|
||||
use crate::protocol::text::{TextDecoder, TextEncoding};
|
||||
use crate::protocol::Endian;
|
||||
|
||||
#[test]
|
||||
fn multi_text_and_hex_same_stream() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod text;
|
||||
pub mod hex;
|
||||
pub mod plot;
|
||||
pub mod text;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
@@ -147,8 +147,7 @@ impl PlotDecoder {
|
||||
}
|
||||
SampleType::I64 => {
|
||||
let b = [
|
||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
||||
bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
];
|
||||
let val = match self.config.endian {
|
||||
Endian::Big => i64::from_be_bytes(b),
|
||||
@@ -158,8 +157,7 @@ impl PlotDecoder {
|
||||
}
|
||||
SampleType::U64 => {
|
||||
let b = [
|
||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
||||
bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
];
|
||||
let val = match self.config.endian {
|
||||
Endian::Big => u64::from_be_bytes(b),
|
||||
@@ -177,8 +175,7 @@ impl PlotDecoder {
|
||||
}
|
||||
SampleType::F64 => {
|
||||
let b = [
|
||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
||||
bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
];
|
||||
match self.config.endian {
|
||||
Endian::Big => f64::from_be_bytes(b),
|
||||
@@ -222,7 +219,8 @@ impl ProtocolDecoder for PlotDecoder {
|
||||
}
|
||||
|
||||
// Distribute into channels
|
||||
let mut channels: Vec<Vec<f64>> = vec![Vec::with_capacity(samples_per_channel); num_channels];
|
||||
let mut channels: Vec<Vec<f64>> =
|
||||
vec![Vec::with_capacity(samples_per_channel); num_channels];
|
||||
match self.config.format {
|
||||
PlotFormat::Interleaved | PlotFormat::XY => {
|
||||
for (i, val) in flat.into_iter().enumerate() {
|
||||
@@ -351,9 +349,14 @@ mod tests {
|
||||
let d = PlotDecoder::new(cfg);
|
||||
// ch0: 100, 300 ch1: 200, 400
|
||||
let data = vec![
|
||||
100u16.to_le_bytes(), 200u16.to_le_bytes(),
|
||||
300u16.to_le_bytes(), 400u16.to_le_bytes(),
|
||||
].into_iter().flatten().collect::<Vec<u8>>();
|
||||
100u16.to_le_bytes(),
|
||||
200u16.to_le_bytes(),
|
||||
300u16.to_le_bytes(),
|
||||
400u16.to_le_bytes(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<u8>>();
|
||||
let result = d.decode(&data).unwrap();
|
||||
match result {
|
||||
DecodedData::Plot(frame) => {
|
||||
@@ -376,9 +379,14 @@ mod tests {
|
||||
let d = PlotDecoder::new(cfg);
|
||||
// ch0: 10, 20 ch1: 30, 40
|
||||
let data = vec![
|
||||
10u16.to_be_bytes(), 20u16.to_be_bytes(),
|
||||
30u16.to_be_bytes(), 40u16.to_be_bytes(),
|
||||
].into_iter().flatten().collect::<Vec<u8>>();
|
||||
10u16.to_be_bytes(),
|
||||
20u16.to_be_bytes(),
|
||||
30u16.to_be_bytes(),
|
||||
40u16.to_be_bytes(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<u8>>();
|
||||
let result = d.decode(&data).unwrap();
|
||||
match result {
|
||||
DecodedData::Plot(frame) => {
|
||||
@@ -401,9 +409,14 @@ mod tests {
|
||||
let d = PlotDecoder::new(cfg);
|
||||
// (x,y) pairs: (10, 100), (20, 200)
|
||||
let data = vec![
|
||||
10u16.to_le_bytes(), 100u16.to_le_bytes(),
|
||||
20u16.to_le_bytes(), 200u16.to_le_bytes(),
|
||||
].into_iter().flatten().collect::<Vec<u8>>();
|
||||
10u16.to_le_bytes(),
|
||||
100u16.to_le_bytes(),
|
||||
20u16.to_le_bytes(),
|
||||
200u16.to_le_bytes(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<u8>>();
|
||||
let result = d.decode(&data).unwrap();
|
||||
match result {
|
||||
DecodedData::Plot(frame) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tracing::info;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::transport::serial::SerialTransport;
|
||||
use crate::transport::tcp::TcpTransport;
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use crate::error::Result;
|
||||
|
||||
pub mod serial;
|
||||
pub mod tcp;
|
||||
@@ -15,7 +16,7 @@ pub mod udp;
|
||||
pub enum TransportType {
|
||||
Serial,
|
||||
Tcp,
|
||||
Udp
|
||||
Udp,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TransportType {
|
||||
@@ -23,7 +24,7 @@ impl std::fmt::Display for TransportType {
|
||||
match self {
|
||||
TransportType::Serial => write!(f, "Serial"),
|
||||
TransportType::Tcp => write!(f, "Tcp"),
|
||||
TransportType::Udp => write!(f, "Udp")
|
||||
TransportType::Udp => write!(f, "Udp"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,15 +33,15 @@ impl std::fmt::Display for TransportType {
|
||||
pub enum TransportConfig {
|
||||
Serial {
|
||||
port: String,
|
||||
baud_rate: u32
|
||||
baud_rate: u32,
|
||||
},
|
||||
Tcp {
|
||||
addr: String
|
||||
addr: String,
|
||||
},
|
||||
Udp {
|
||||
bind_addr: String,
|
||||
remote_addr: Option<String>
|
||||
}
|
||||
remote_addr: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -98,6 +99,7 @@ impl Connection {
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self) -> Result<()> {
|
||||
info!(transport = ?self.transport_type(), name = self.name(), "Connection connecting");
|
||||
match self {
|
||||
Connection::Serial(t) => t.connect().await,
|
||||
Connection::Tcp(t) => t.connect().await,
|
||||
@@ -106,6 +108,7 @@ impl Connection {
|
||||
}
|
||||
|
||||
pub async fn disconnect(&mut self) -> Result<()> {
|
||||
info!(name = self.name(), "Connection disconnecting");
|
||||
match self {
|
||||
Connection::Serial(t) => t.disconnect().await,
|
||||
Connection::Tcp(t) => t.disconnect().await,
|
||||
@@ -237,7 +240,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_transport_type_roundtrip() {
|
||||
for original in [TransportType::Serial, TransportType::Tcp, TransportType::Udp] {
|
||||
for original in [
|
||||
TransportType::Serial,
|
||||
TransportType::Tcp,
|
||||
TransportType::Udp,
|
||||
] {
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let roundtripped: TransportType = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(original, roundtripped);
|
||||
@@ -503,8 +510,7 @@ mod tests {
|
||||
unsafe fn wake(_: *const ()) {}
|
||||
unsafe fn wake_by_ref(_: *const ()) {}
|
||||
unsafe fn drop(_: *const ()) {}
|
||||
static VTABLE: RawWakerVTable =
|
||||
RawWakerVTable::new(clone, wake, wake_by_ref, drop);
|
||||
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
|
||||
unsafe { std::task::Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
|
||||
}
|
||||
|
||||
@@ -530,9 +536,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_connection_poll_read_not_connected() {
|
||||
use tokio::io::ReadBuf;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::ReadBuf;
|
||||
|
||||
for mut conn in [serial_conn(), tcp_conn(), udp_conn()] {
|
||||
let pinned = Pin::new(&mut conn);
|
||||
@@ -719,9 +725,13 @@ mod tests {
|
||||
|
||||
let server = tokio::spawn(async move {
|
||||
let (mut stream, _) = listener.accept().await.unwrap();
|
||||
AsyncWriteExt::write_all(&mut stream, b"hello").await.unwrap();
|
||||
AsyncWriteExt::write_all(&mut stream, b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
let mut buf = [0u8; 5];
|
||||
AsyncReadExt::read_exact(&mut stream, &mut buf).await.unwrap();
|
||||
AsyncReadExt::read_exact(&mut stream, &mut buf)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(&buf, b"world");
|
||||
});
|
||||
|
||||
@@ -761,7 +771,9 @@ mod tests {
|
||||
remote.send_to(b"world", from).await.unwrap();
|
||||
|
||||
let mut read_buf = [0u8; 5];
|
||||
AsyncReadExt::read_exact(&mut conn, &mut read_buf).await.unwrap();
|
||||
AsyncReadExt::read_exact(&mut conn, &mut read_buf)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(&read_buf, b"world");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,17 +109,22 @@ impl Transport for SerialTransport {
|
||||
|
||||
async fn connect(&mut self) -> Result<()> {
|
||||
if self.is_connected() {
|
||||
return Ok(())
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Opening serial port {} at {} baud", self.port_name, self.baud_rate);
|
||||
info!(
|
||||
"Opening serial port {} at {} baud",
|
||||
self.port_name, self.baud_rate
|
||||
);
|
||||
|
||||
let port = tokio_serial::new(&self.port_name, self.baud_rate)
|
||||
.open_native_async()
|
||||
.map_err(|e| crate::error::Error::ConnectionFailed(format!(
|
||||
"Failed to open {}: {}",
|
||||
self.port_name, e
|
||||
)))?;
|
||||
.map_err(|e| {
|
||||
crate::error::Error::ConnectionFailed(format!(
|
||||
"Failed to open {}: {}",
|
||||
self.port_name, e
|
||||
))
|
||||
})?;
|
||||
|
||||
debug!("Serial port {} opened successfully", self.port_name);
|
||||
self.port = Some(port);
|
||||
@@ -152,12 +157,8 @@ mod tests {
|
||||
unsafe fn raw_wake_by_ref(_: *const ()) {}
|
||||
unsafe fn raw_drop(_: *const ()) {}
|
||||
|
||||
static RAW_VTABLE: RawWakerVTable = RawWakerVTable::new(
|
||||
raw_clone,
|
||||
raw_wake,
|
||||
raw_wake_by_ref,
|
||||
raw_drop,
|
||||
);
|
||||
static RAW_VTABLE: RawWakerVTable =
|
||||
RawWakerVTable::new(raw_clone, raw_wake, raw_wake_by_ref, raw_drop);
|
||||
|
||||
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &RAW_VTABLE)) }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tokio::net::TcpStream;
|
||||
@@ -10,15 +9,12 @@ use crate::error::Result;
|
||||
#[derive(Debug)]
|
||||
pub struct TcpTransport {
|
||||
stream: Option<TcpStream>,
|
||||
addr: String
|
||||
addr: String,
|
||||
}
|
||||
|
||||
impl TcpTransport {
|
||||
pub fn new(addr: String) -> Self {
|
||||
Self {
|
||||
stream: None,
|
||||
addr
|
||||
}
|
||||
Self { stream: None, addr }
|
||||
}
|
||||
|
||||
pub fn addr(&self) -> &str {
|
||||
@@ -281,7 +277,10 @@ mod tests {
|
||||
async fn test_disconnect_without_connect() {
|
||||
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
|
||||
let result = transport.disconnect().await;
|
||||
assert!(result.is_ok(), "disconnect without connect should be a safe no-op");
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"disconnect without connect should be a safe no-op"
|
||||
);
|
||||
assert!(!transport.is_connected());
|
||||
}
|
||||
|
||||
@@ -322,4 +321,4 @@ mod tests {
|
||||
// Ensure server task completed without panic
|
||||
server_handle.await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub struct UdpTransport {
|
||||
read_buf: Vec<u8>,
|
||||
read_pos: usize,
|
||||
|
||||
temp_recv_buf: Vec<u8>,
|
||||
temp_recv_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl UdpTransport {
|
||||
@@ -49,7 +49,7 @@ impl AsyncRead for UdpTransport {
|
||||
if this.read_pos < this.read_buf.len() {
|
||||
let remaining = &this.read_buf[this.read_pos..];
|
||||
let to_copy = std::cmp::min(remaining.len(), buf.remaining());
|
||||
|
||||
|
||||
buf.put_slice(&remaining[..to_copy]);
|
||||
this.read_pos += to_copy;
|
||||
|
||||
@@ -81,14 +81,16 @@ impl AsyncRead for UdpTransport {
|
||||
socket.poll_recv(cx, &mut temp_buf)
|
||||
} else {
|
||||
// poll_recv_from 会返回 (usize, SocketAddr),我们将其映射回统一的 () 类型
|
||||
socket.poll_recv_from(cx, &mut temp_buf).map(|res| res.map(|_| ()))
|
||||
socket
|
||||
.poll_recv_from(cx, &mut temp_buf)
|
||||
.map(|res| res.map(|_| ()))
|
||||
};
|
||||
|
||||
match poll_result {
|
||||
Poll::Ready(Ok(())) => {
|
||||
let filled = temp_buf.filled();
|
||||
let to_copy = std::cmp::min(filled.len(), buf.remaining());
|
||||
|
||||
|
||||
buf.put_slice(&filled[..to_copy]);
|
||||
|
||||
// 核心逻辑:如果本次读取到的 UDP 包大于外面提供的 buf 的剩余空间
|
||||
@@ -268,9 +270,7 @@ mod tests {
|
||||
let result = Pin::new(&mut t).poll_read(&mut cx, &mut buf);
|
||||
match result {
|
||||
Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
|
||||
other => panic!(
|
||||
"expected Poll::Ready(Err(NotConnected)), got {other:?}"
|
||||
),
|
||||
other => panic!("expected Poll::Ready(Err(NotConnected)), got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,9 +282,7 @@ mod tests {
|
||||
let result = Pin::new(&mut t).poll_write(&mut cx, data);
|
||||
match result {
|
||||
Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
|
||||
other => panic!(
|
||||
"expected Poll::Ready(Err(NotConnected)), got {other:?}"
|
||||
),
|
||||
other => panic!("expected Poll::Ready(Err(NotConnected)), got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +320,9 @@ mod tests {
|
||||
let remote_addr = remote.local_addr().unwrap().to_string();
|
||||
|
||||
let mut t = UdpTransport::new("127.0.0.1:0".into(), Some(remote_addr));
|
||||
t.connect().await.expect("connect with remote should succeed");
|
||||
t.connect()
|
||||
.await
|
||||
.expect("connect with remote should succeed");
|
||||
assert!(t.is_connected());
|
||||
|
||||
t.disconnect().await.expect("disconnect should succeed");
|
||||
|
||||
@@ -5,26 +5,23 @@ use tokio::net::{TcpListener, UdpSocket};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use xserial_core::frame::{
|
||||
Endian, Framer,
|
||||
cobs::CobsFramer,
|
||||
fixed::FixedLengthFramer,
|
||||
length::{LengthConfig, LengthPrefixedFramer},
|
||||
line::{LineConfig, LineFramer},
|
||||
Endian, Framer,
|
||||
};
|
||||
use xserial_core::protocol::{
|
||||
DecodedData, ProtocolDecoder,
|
||||
hex::{HexConfig, HexDecoder},
|
||||
plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType},
|
||||
text::{TextDecoder, TextEncoding},
|
||||
DecodedData, ProtocolDecoder,
|
||||
};
|
||||
use xserial_core::transport::{Connection, TransportConfig, TransportType};
|
||||
|
||||
const TEST_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
async fn read_to_framer(
|
||||
conn: &mut Connection,
|
||||
framer: &mut dyn Framer,
|
||||
) -> Vec<Vec<u8>> {
|
||||
async fn read_to_framer(conn: &mut Connection, framer: &mut dyn Framer) -> Vec<Vec<u8>> {
|
||||
let mut buf = [0u8; 4096];
|
||||
let mut all_frames = Vec::new();
|
||||
|
||||
@@ -384,9 +381,7 @@ async fn connection_reconnect_tcp() {
|
||||
stream.write_all(b"first\n").await.unwrap();
|
||||
});
|
||||
|
||||
let mut conn = Connection::new(TransportConfig::Tcp {
|
||||
addr: addr.clone(),
|
||||
});
|
||||
let mut conn = Connection::new(TransportConfig::Tcp { addr: addr.clone() });
|
||||
conn.connect().await.unwrap();
|
||||
|
||||
let mut framer = LineFramer::new(LineConfig::default());
|
||||
|
||||
Reference in New Issue
Block a user