Update core modules and add GUI panels
This commit is contained in:
@@ -1,83 +1,149 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use xserial_core::frame::{
|
use xserial_core::frame::{
|
||||||
cobs::CobsFramer, fixed::FixedLengthFramer,
|
Endian, Framer,
|
||||||
|
cobs::CobsFramer,
|
||||||
|
fixed::FixedLengthFramer,
|
||||||
length::{LengthConfig, LengthPrefixedFramer},
|
length::{LengthConfig, LengthPrefixedFramer},
|
||||||
line::{LineConfig, LineFramer}, Endian, Framer,
|
line::{LineConfig, LineFramer},
|
||||||
};
|
};
|
||||||
use xserial_core::protocol::{
|
use xserial_core::protocol::{
|
||||||
|
ProtocolDecoder,
|
||||||
hex::{HexConfig, HexDecoder},
|
hex::{HexConfig, HexDecoder},
|
||||||
plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType},
|
plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType},
|
||||||
text::{TextDecoder, TextEncoding}, ProtocolDecoder,
|
text::{TextDecoder, TextEncoding},
|
||||||
};
|
};
|
||||||
use xserial_core::transport::TransportConfig;
|
use xserial_core::transport::TransportConfig;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum FramerConfig {
|
pub enum FramerConfig {
|
||||||
Line {
|
Line {
|
||||||
#[serde(default = "default_true")] strip_cr: bool,
|
#[serde(default = "default_true")]
|
||||||
#[serde(default = "default_max_line")] max_line_len: usize,
|
strip_cr: bool,
|
||||||
|
#[serde(default = "default_max_line")]
|
||||||
|
max_line_len: usize,
|
||||||
|
},
|
||||||
|
Fixed {
|
||||||
|
frame_len: usize,
|
||||||
},
|
},
|
||||||
Fixed { frame_len: usize },
|
|
||||||
Length {
|
Length {
|
||||||
#[serde(default = "default_len_bytes")] len_bytes: usize,
|
#[serde(default = "default_len_bytes")]
|
||||||
#[serde(default)] endian: Endian,
|
len_bytes: usize,
|
||||||
#[serde(default)] length_includes_self: bool,
|
#[serde(default)]
|
||||||
#[serde(default = "default_max_payload")] max_payload: usize,
|
endian: Endian,
|
||||||
|
#[serde(default)]
|
||||||
|
length_includes_self: bool,
|
||||||
|
#[serde(default = "default_max_payload")]
|
||||||
|
max_payload: usize,
|
||||||
},
|
},
|
||||||
Cobs {
|
Cobs {
|
||||||
#[serde(default = "default_max_frame")] max_frame: usize,
|
#[serde(default = "default_max_frame")]
|
||||||
|
max_frame: usize,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_true() -> bool { true }
|
fn default_true() -> bool {
|
||||||
fn default_max_line() -> usize { 1024 * 1024 }
|
true
|
||||||
fn default_len_bytes() -> usize { 2 }
|
}
|
||||||
fn default_max_payload() -> usize { 1024 * 1024 }
|
fn default_max_line() -> usize {
|
||||||
fn default_max_frame() -> usize { 1024 * 1024 }
|
1024 * 1024
|
||||||
|
}
|
||||||
|
fn default_len_bytes() -> usize {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
fn default_max_payload() -> usize {
|
||||||
|
1024 * 1024
|
||||||
|
}
|
||||||
|
fn default_max_frame() -> usize {
|
||||||
|
1024 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
impl FramerConfig {
|
impl FramerConfig {
|
||||||
pub fn build(self) -> Box<dyn Framer> {
|
pub fn build(self) -> Box<dyn Framer> {
|
||||||
match self {
|
match self {
|
||||||
FramerConfig::Line { strip_cr, max_line_len } =>
|
FramerConfig::Line {
|
||||||
Box::new(LineFramer::new(LineConfig { strip_cr, max_line_len })),
|
strip_cr,
|
||||||
FramerConfig::Fixed { frame_len } =>
|
max_line_len,
|
||||||
Box::new(FixedLengthFramer::new(frame_len)),
|
} => Box::new(LineFramer::new(LineConfig {
|
||||||
FramerConfig::Length { len_bytes, endian, length_includes_self, max_payload } =>
|
strip_cr,
|
||||||
Box::new(LengthPrefixedFramer::new(LengthConfig { len_bytes, endian, length_includes_self, max_payload })),
|
max_line_len,
|
||||||
FramerConfig::Cobs { max_frame } =>
|
})),
|
||||||
Box::new(CobsFramer::new(max_frame)),
|
FramerConfig::Fixed { frame_len } => Box::new(FixedLengthFramer::new(frame_len)),
|
||||||
|
FramerConfig::Length {
|
||||||
|
len_bytes,
|
||||||
|
endian,
|
||||||
|
length_includes_self,
|
||||||
|
max_payload,
|
||||||
|
} => Box::new(LengthPrefixedFramer::new(LengthConfig {
|
||||||
|
len_bytes,
|
||||||
|
endian,
|
||||||
|
length_includes_self,
|
||||||
|
max_payload,
|
||||||
|
})),
|
||||||
|
FramerConfig::Cobs { max_frame } => Box::new(CobsFramer::new(max_frame)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum DecoderConfig {
|
pub enum DecoderConfig {
|
||||||
Text { #[serde(default)] encoding: TextEncoding },
|
Text {
|
||||||
|
#[serde(default)]
|
||||||
|
encoding: TextEncoding,
|
||||||
|
},
|
||||||
Hex {
|
Hex {
|
||||||
#[serde(default)] uppercase: bool,
|
#[serde(default)]
|
||||||
#[serde(default = "default_sep")] separator: String,
|
uppercase: bool,
|
||||||
#[serde(default = "default_one")] bytes_per_group: usize,
|
#[serde(default = "default_sep")]
|
||||||
#[serde(default)] endian: Endian,
|
separator: String,
|
||||||
|
#[serde(default = "default_one")]
|
||||||
|
bytes_per_group: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
endian: Endian,
|
||||||
},
|
},
|
||||||
Plot {
|
Plot {
|
||||||
sample_type: SampleType,
|
sample_type: SampleType,
|
||||||
#[serde(default)] endian: Endian,
|
#[serde(default)]
|
||||||
#[serde(default = "default_one")] channels: usize,
|
endian: Endian,
|
||||||
#[serde(default)] format: PlotFormat,
|
#[serde(default = "default_one")]
|
||||||
|
channels: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
format: PlotFormat,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_sep() -> String { String::from(" ") }
|
fn default_sep() -> String {
|
||||||
fn default_one() -> usize { 1 }
|
String::from(" ")
|
||||||
|
}
|
||||||
|
fn default_one() -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
impl DecoderConfig {
|
impl DecoderConfig {
|
||||||
pub fn build(self) -> Box<dyn ProtocolDecoder> {
|
pub fn build(self) -> Box<dyn ProtocolDecoder> {
|
||||||
match self {
|
match self {
|
||||||
DecoderConfig::Text { encoding } => Box::new(TextDecoder::new(encoding)),
|
DecoderConfig::Text { encoding } => Box::new(TextDecoder::new(encoding)),
|
||||||
DecoderConfig::Hex { uppercase, separator, bytes_per_group, endian } =>
|
DecoderConfig::Hex {
|
||||||
Box::new(HexDecoder::new(HexConfig { uppercase, separator, bytes_per_group, endian })),
|
uppercase,
|
||||||
DecoderConfig::Plot { sample_type, endian, channels, format } =>
|
separator,
|
||||||
Box::new(PlotDecoder::new(PlotConfig { sample_type, endian, channels, format })),
|
bytes_per_group,
|
||||||
|
endian,
|
||||||
|
} => Box::new(HexDecoder::new(HexConfig {
|
||||||
|
uppercase,
|
||||||
|
separator,
|
||||||
|
bytes_per_group,
|
||||||
|
endian,
|
||||||
|
})),
|
||||||
|
DecoderConfig::Plot {
|
||||||
|
sample_type,
|
||||||
|
endian,
|
||||||
|
channels,
|
||||||
|
format,
|
||||||
|
} => Box::new(PlotDecoder::new(PlotConfig {
|
||||||
|
sample_type,
|
||||||
|
endian,
|
||||||
|
channels,
|
||||||
|
format,
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,17 +158,24 @@ pub struct PipelineConfig {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SessionConfig {
|
pub struct SessionConfig {
|
||||||
pub transport: TransportConfig,
|
pub transport: TransportConfig,
|
||||||
#[serde(default)] pub pipelines: Vec<PipelineConfig>,
|
#[serde(default)]
|
||||||
#[serde(default = "default_history")] pub history_limit: usize,
|
pub pipelines: Vec<PipelineConfig>,
|
||||||
#[serde(default)] pub auto_reconnect: bool,
|
#[serde(default = "default_history")]
|
||||||
|
pub history_limit: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub auto_reconnect: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_history() -> usize { 10_000 }
|
fn default_history() -> usize {
|
||||||
|
10_000
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for SessionConfig {
|
impl Default for SessionConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
transport: TransportConfig::Tcp { addr: "127.0.0.1:8080".into() },
|
transport: TransportConfig::Tcp {
|
||||||
|
addr: "127.0.0.1:8080".into(),
|
||||||
|
},
|
||||||
pipelines: Vec::new(),
|
pipelines: Vec::new(),
|
||||||
history_limit: default_history(),
|
history_limit: default_history(),
|
||||||
auto_reconnect: false,
|
auto_reconnect: false,
|
||||||
@@ -113,9 +186,9 @@ impl Default for SessionConfig {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use xserial_core::protocol::text::TextEncoding;
|
|
||||||
use xserial_core::protocol::plot::{PlotFormat, SampleType};
|
|
||||||
use xserial_core::protocol::Endian;
|
use xserial_core::protocol::Endian;
|
||||||
|
use xserial_core::protocol::plot::{PlotFormat, SampleType};
|
||||||
|
use xserial_core::protocol::text::TextEncoding;
|
||||||
use xserial_core::transport::TransportConfig;
|
use xserial_core::transport::TransportConfig;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -123,7 +196,10 @@ mod tests {
|
|||||||
let json = r#"{"Line":{"strip_cr":true,"max_line_len":4096}}"#;
|
let json = r#"{"Line":{"strip_cr":true,"max_line_len":4096}}"#;
|
||||||
let cfg: FramerConfig = serde_json::from_str(json).unwrap();
|
let cfg: FramerConfig = serde_json::from_str(json).unwrap();
|
||||||
match &cfg {
|
match &cfg {
|
||||||
FramerConfig::Line { strip_cr, max_line_len } => {
|
FramerConfig::Line {
|
||||||
|
strip_cr,
|
||||||
|
max_line_len,
|
||||||
|
} => {
|
||||||
assert!(*strip_cr);
|
assert!(*strip_cr);
|
||||||
assert_eq!(*max_line_len, 4096);
|
assert_eq!(*max_line_len, 4096);
|
||||||
}
|
}
|
||||||
@@ -144,7 +220,12 @@ mod tests {
|
|||||||
let json = r#"{"Length":{"len_bytes":4,"endian":"Little","length_includes_self":true}}"#;
|
let json = r#"{"Length":{"len_bytes":4,"endian":"Little","length_includes_self":true}}"#;
|
||||||
let cfg: FramerConfig = serde_json::from_str(json).unwrap();
|
let cfg: FramerConfig = serde_json::from_str(json).unwrap();
|
||||||
match cfg {
|
match cfg {
|
||||||
FramerConfig::Length { len_bytes, endian, length_includes_self, .. } => {
|
FramerConfig::Length {
|
||||||
|
len_bytes,
|
||||||
|
endian,
|
||||||
|
length_includes_self,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
assert_eq!(len_bytes, 4);
|
assert_eq!(len_bytes, 4);
|
||||||
assert_eq!(endian, Endian::Little);
|
assert_eq!(endian, Endian::Little);
|
||||||
assert!(length_includes_self);
|
assert!(length_includes_self);
|
||||||
@@ -170,10 +251,16 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn decoder_hex_serde() {
|
fn decoder_hex_serde() {
|
||||||
let json = r#"{"Hex":{"uppercase":true,"separator":":","bytes_per_group":2,"endian":"Little"}}"#;
|
let json =
|
||||||
|
r#"{"Hex":{"uppercase":true,"separator":":","bytes_per_group":2,"endian":"Little"}}"#;
|
||||||
let cfg: DecoderConfig = serde_json::from_str(json).unwrap();
|
let cfg: DecoderConfig = serde_json::from_str(json).unwrap();
|
||||||
match cfg {
|
match cfg {
|
||||||
DecoderConfig::Hex { uppercase, separator, bytes_per_group, endian } => {
|
DecoderConfig::Hex {
|
||||||
|
uppercase,
|
||||||
|
separator,
|
||||||
|
bytes_per_group,
|
||||||
|
endian,
|
||||||
|
} => {
|
||||||
assert!(uppercase);
|
assert!(uppercase);
|
||||||
assert_eq!(separator, ":");
|
assert_eq!(separator, ":");
|
||||||
assert_eq!(bytes_per_group, 2);
|
assert_eq!(bytes_per_group, 2);
|
||||||
@@ -188,7 +275,12 @@ mod tests {
|
|||||||
let json = r#"{"Plot":{"sample_type":"F32","endian":"Little","channels":2,"format":"Interleaved"}}"#;
|
let json = r#"{"Plot":{"sample_type":"F32","endian":"Little","channels":2,"format":"Interleaved"}}"#;
|
||||||
let cfg: DecoderConfig = serde_json::from_str(json).unwrap();
|
let cfg: DecoderConfig = serde_json::from_str(json).unwrap();
|
||||||
match cfg {
|
match cfg {
|
||||||
DecoderConfig::Plot { sample_type, endian, channels, format } => {
|
DecoderConfig::Plot {
|
||||||
|
sample_type,
|
||||||
|
endian,
|
||||||
|
channels,
|
||||||
|
format,
|
||||||
|
} => {
|
||||||
assert_eq!(sample_type, SampleType::F32);
|
assert_eq!(sample_type, SampleType::F32);
|
||||||
assert_eq!(channels, 2);
|
assert_eq!(channels, 2);
|
||||||
assert_eq!(format, PlotFormat::Interleaved);
|
assert_eq!(format, PlotFormat::Interleaved);
|
||||||
@@ -213,7 +305,9 @@ mod tests {
|
|||||||
assert!(cfg.auto_reconnect);
|
assert!(cfg.auto_reconnect);
|
||||||
assert_eq!(cfg.history_limit, 5000);
|
assert_eq!(cfg.history_limit, 5000);
|
||||||
assert_eq!(cfg.pipelines.len(), 2);
|
assert_eq!(cfg.pipelines.len(), 2);
|
||||||
assert!(matches!(&cfg.transport, TransportConfig::Tcp { addr } if addr == "192.168.1.1:9999"));
|
assert!(
|
||||||
|
matches!(&cfg.transport, TransportConfig::Tcp { addr } if addr == "192.168.1.1:9999")
|
||||||
|
);
|
||||||
let back = serde_json::to_string_pretty(&cfg).unwrap();
|
let back = serde_json::to_string_pretty(&cfg).unwrap();
|
||||||
let _: SessionConfig = serde_json::from_str(&back).unwrap();
|
let _: SessionConfig = serde_json::from_str(&back).unwrap();
|
||||||
}
|
}
|
||||||
@@ -221,9 +315,17 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn framer_build_does_not_panic() {
|
fn framer_build_does_not_panic() {
|
||||||
for cfg in [
|
for cfg in [
|
||||||
FramerConfig::Line { strip_cr: true, max_line_len: 256 },
|
FramerConfig::Line {
|
||||||
|
strip_cr: true,
|
||||||
|
max_line_len: 256,
|
||||||
|
},
|
||||||
FramerConfig::Fixed { frame_len: 8 },
|
FramerConfig::Fixed { frame_len: 8 },
|
||||||
FramerConfig::Length { len_bytes: 2, endian: Endian::Big, length_includes_self: false, max_payload: 65536 },
|
FramerConfig::Length {
|
||||||
|
len_bytes: 2,
|
||||||
|
endian: Endian::Big,
|
||||||
|
length_includes_self: false,
|
||||||
|
max_payload: 65536,
|
||||||
|
},
|
||||||
FramerConfig::Cobs { max_frame: 1024 },
|
FramerConfig::Cobs { max_frame: 1024 },
|
||||||
] {
|
] {
|
||||||
let f = cfg.build();
|
let f = cfg.build();
|
||||||
@@ -233,8 +335,35 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn decoder_build_returns_correct_name() {
|
fn decoder_build_returns_correct_name() {
|
||||||
assert_eq!(DecoderConfig::Text { encoding: TextEncoding::Utf8 }.build().name(), "Text");
|
assert_eq!(
|
||||||
assert_eq!(DecoderConfig::Hex { uppercase: false, separator: " ".into(), bytes_per_group: 1, endian: Endian::Big }.build().name(), "Hex");
|
DecoderConfig::Text {
|
||||||
assert_eq!(DecoderConfig::Plot { sample_type: SampleType::I16, endian: Endian::Big, channels: 2, format: PlotFormat::XY }.build().name(), "Plot");
|
encoding: TextEncoding::Utf8
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
.name(),
|
||||||
|
"Text"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DecoderConfig::Hex {
|
||||||
|
uppercase: false,
|
||||||
|
separator: " ".into(),
|
||||||
|
bytes_per_group: 1,
|
||||||
|
endian: Endian::Big
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
.name(),
|
||||||
|
"Hex"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DecoderConfig::Plot {
|
||||||
|
sample_type: SampleType::I16,
|
||||||
|
endian: Endian::Big,
|
||||||
|
channels: 2,
|
||||||
|
format: PlotFormat::XY
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
.name(),
|
||||||
|
"Plot"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::collections::VecDeque;
|
|
||||||
use crate::event::DecodedEntry;
|
use crate::event::DecodedEntry;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
pub struct RingBuffer<T> {
|
pub struct RingBuffer<T> {
|
||||||
buf: VecDeque<T>,
|
buf: VecDeque<T>,
|
||||||
@@ -8,17 +8,33 @@ pub struct RingBuffer<T> {
|
|||||||
|
|
||||||
impl<T> RingBuffer<T> {
|
impl<T> RingBuffer<T> {
|
||||||
pub fn new(limit: usize) -> Self {
|
pub fn new(limit: usize) -> Self {
|
||||||
Self { buf: VecDeque::with_capacity(limit.min(1024)), limit }
|
Self {
|
||||||
|
buf: VecDeque::with_capacity(limit.min(1024)),
|
||||||
|
limit,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn push(&mut self, item: T) {
|
pub fn push(&mut self, item: T) {
|
||||||
if self.buf.len() >= self.limit { self.buf.pop_front(); }
|
if self.buf.len() >= self.limit {
|
||||||
|
self.buf.pop_front();
|
||||||
|
}
|
||||||
self.buf.push_back(item);
|
self.buf.push_back(item);
|
||||||
}
|
}
|
||||||
pub fn len(&self) -> usize { self.buf.len() }
|
pub fn len(&self) -> usize {
|
||||||
pub fn is_empty(&self) -> bool { self.buf.is_empty() }
|
self.buf.len()
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &T> { self.buf.iter() }
|
}
|
||||||
pub fn clear(&mut self) { self.buf.clear(); }
|
pub fn is_empty(&self) -> bool {
|
||||||
pub fn drain_recent(&self, count: usize) -> Vec<T> where T: Clone {
|
self.buf.is_empty()
|
||||||
|
}
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||||
|
self.buf.iter()
|
||||||
|
}
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.buf.clear();
|
||||||
|
}
|
||||||
|
pub fn drain_recent(&self, count: usize) -> Vec<T>
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
{
|
||||||
let start = self.buf.len().saturating_sub(count);
|
let start = self.buf.len().saturating_sub(count);
|
||||||
self.buf.iter().skip(start).cloned().collect()
|
self.buf.iter().skip(start).cloned().collect()
|
||||||
}
|
}
|
||||||
@@ -26,6 +42,9 @@ impl<T> RingBuffer<T> {
|
|||||||
|
|
||||||
impl RingBuffer<DecodedEntry> {
|
impl RingBuffer<DecodedEntry> {
|
||||||
pub fn entries_for_pipeline(&self, name: &str) -> Vec<&DecodedEntry> {
|
pub fn entries_for_pipeline(&self, name: &str) -> Vec<&DecodedEntry> {
|
||||||
self.buf.iter().filter(|e| e.pipeline_name == name).collect()
|
self.buf
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.pipeline_name == name)
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod event;
|
|
||||||
pub mod cmd;
|
pub mod cmd;
|
||||||
|
pub mod config;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod event;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod session;
|
|
||||||
pub mod manager;
|
|
||||||
pub mod lua;
|
pub mod lua;
|
||||||
|
pub mod manager;
|
||||||
|
pub mod session;
|
||||||
|
|
||||||
pub use config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig};
|
pub use config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig};
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ use std::time::Duration;
|
|||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use xserial_client::config::{
|
|
||||||
DecoderConfig, FramerConfig, PipelineConfig, SessionConfig,
|
|
||||||
};
|
|
||||||
use xserial_client::session::{Session, SessionEvent};
|
|
||||||
use xserial_client::SessionManager;
|
use xserial_client::SessionManager;
|
||||||
|
use xserial_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig};
|
||||||
|
use xserial_client::session::{Session, SessionEvent};
|
||||||
use xserial_core::protocol::DecodedData;
|
use xserial_core::protocol::DecodedData;
|
||||||
use xserial_core::transport::TransportConfig;
|
use xserial_core::transport::TransportConfig;
|
||||||
|
|
||||||
@@ -78,13 +76,17 @@ async fn session_multiple_reads() {
|
|||||||
SessionEvent::Data(_, entry) => {
|
SessionEvent::Data(_, entry) => {
|
||||||
if let DecodedData::Text(s) = entry.data {
|
if let DecodedData::Text(s) = entry.data {
|
||||||
texts.push(s);
|
texts.push(s);
|
||||||
if texts.len() == 3 { break; }
|
if texts.len() == 3 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).await.unwrap();
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(texts, vec!["a", "b", "c"]);
|
assert_eq!(texts, vec!["a", "b", "c"]);
|
||||||
handle.close().await.unwrap();
|
handle.close().await.unwrap();
|
||||||
@@ -128,12 +130,16 @@ async fn session_multi_pipeline_text_and_hex() {
|
|||||||
match rx.recv().await.unwrap() {
|
match rx.recv().await.unwrap() {
|
||||||
SessionEvent::Data(_, entry) => {
|
SessionEvent::Data(_, entry) => {
|
||||||
results.push((entry.pipeline_name.clone(), entry.data.clone()));
|
results.push((entry.pipeline_name.clone(), entry.data.clone()));
|
||||||
if results.len() == 2 { break; }
|
if results.len() == 2 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).await.unwrap();
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(results.len(), 2);
|
assert_eq!(results.len(), 2);
|
||||||
assert_eq!(results[0].0, "text");
|
assert_eq!(results[0].0, "text");
|
||||||
@@ -313,11 +319,15 @@ async fn session_handle_subscribe_sees_events() {
|
|||||||
|
|
||||||
// Should see Connected then Data
|
// Should see Connected then Data
|
||||||
let e1 = tokio::time::timeout(Duration::from_secs(5), rx.recv())
|
let e1 = tokio::time::timeout(Duration::from_secs(5), rx.recv())
|
||||||
.await.unwrap().unwrap();
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
assert!(matches!(e1, SessionEvent::Connected(7)));
|
assert!(matches!(e1, SessionEvent::Connected(7)));
|
||||||
|
|
||||||
let e2 = tokio::time::timeout(Duration::from_secs(5), rx.recv())
|
let e2 = tokio::time::timeout(Duration::from_secs(5), rx.recv())
|
||||||
.await.unwrap().unwrap();
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
match e2 {
|
match e2 {
|
||||||
SessionEvent::Data(7, entry) => {
|
SessionEvent::Data(7, entry) => {
|
||||||
assert_eq!(entry.pipeline_name, "text");
|
assert_eq!(entry.pipeline_name, "text");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::Framer;
|
use super::Framer;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
/// COBS (Consistent Overhead Byte Stuffing) framer.
|
/// COBS (Consistent Overhead Byte Stuffing) framer.
|
||||||
///
|
///
|
||||||
@@ -38,9 +39,16 @@ impl Framer for CobsFramer {
|
|||||||
for &byte in data {
|
for &byte in data {
|
||||||
if byte == 0x00 {
|
if byte == 0x00 {
|
||||||
if !self.buf.is_empty() {
|
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) {
|
if let Some(decoded) = cobs_decode(&self.buf) {
|
||||||
|
debug!(
|
||||||
|
encoded_len = buf_len,
|
||||||
|
decoded_len = decoded.len(),
|
||||||
|
"COBS frame decoded"
|
||||||
|
);
|
||||||
frames.push(decoded);
|
frames.push(decoded);
|
||||||
|
} else {
|
||||||
|
warn!(len = buf_len, "corrupt COBS packet discarded");
|
||||||
}
|
}
|
||||||
self.buf.clear();
|
self.buf.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::{Endian, Framer};
|
use super::{Endian, Framer};
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
/// Configuration for the length-prefixed framer.
|
/// Configuration for the length-prefixed framer.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -96,6 +97,11 @@ impl Framer for LengthPrefixedFramer {
|
|||||||
|
|
||||||
// Discard length, re-sync on corrupt frame
|
// Discard length, re-sync on corrupt frame
|
||||||
if self.config.max_payload > 0 && payload_len > self.config.max_payload {
|
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);
|
self.buf.drain(..self.config.len_bytes);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -115,6 +121,7 @@ impl Framer for LengthPrefixedFramer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let frame: Vec<u8> = self.buf.drain(..expected).collect();
|
let frame: Vec<u8> = self.buf.drain(..expected).collect();
|
||||||
|
debug!(len = frame.len(), "length-prefixed frame extracted");
|
||||||
frames.push(frame);
|
frames.push(frame);
|
||||||
self.state = State::ReadingLength;
|
self.state = State::ReadingLength;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::Framer;
|
use super::Framer;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
/// Line-delimited framer.
|
/// 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
|
frames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
pub mod line;
|
pub mod cobs;
|
||||||
pub mod fixed;
|
pub mod fixed;
|
||||||
pub mod length;
|
pub mod length;
|
||||||
pub mod cobs;
|
pub mod line;
|
||||||
|
|
||||||
pub use crate::protocol::Endian;
|
pub use crate::protocol::Endian;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod transport;
|
|
||||||
pub mod protocol;
|
|
||||||
pub mod frame;
|
pub mod frame;
|
||||||
pub mod pipeline;
|
pub mod pipeline;
|
||||||
|
pub mod protocol;
|
||||||
|
pub mod transport;
|
||||||
|
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::frame::Framer;
|
use crate::frame::Framer;
|
||||||
use crate::protocol::{DecodedData, ProtocolDecoder};
|
use crate::protocol::{DecodedData, ProtocolDecoder};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
/// A self-contained framing + decoding pipeline.
|
/// 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
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,10 +176,10 @@ mod tests {
|
|||||||
use crate::frame::fixed::FixedLengthFramer;
|
use crate::frame::fixed::FixedLengthFramer;
|
||||||
use crate::frame::length::{LengthConfig, LengthPrefixedFramer};
|
use crate::frame::length::{LengthConfig, LengthPrefixedFramer};
|
||||||
use crate::frame::line::{LineConfig, LineFramer};
|
use crate::frame::line::{LineConfig, LineFramer};
|
||||||
|
use crate::protocol::Endian;
|
||||||
use crate::protocol::hex::{HexConfig, HexDecoder};
|
use crate::protocol::hex::{HexConfig, HexDecoder};
|
||||||
use crate::protocol::plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType};
|
use crate::protocol::plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType};
|
||||||
use crate::protocol::text::{TextDecoder, TextEncoding};
|
use crate::protocol::text::{TextDecoder, TextEncoding};
|
||||||
use crate::protocol::Endian;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multi_text_and_hex_same_stream() {
|
fn multi_text_and_hex_same_stream() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pub mod text;
|
|
||||||
pub mod hex;
|
pub mod hex;
|
||||||
pub mod plot;
|
pub mod plot;
|
||||||
|
pub mod text;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|||||||
@@ -147,8 +147,7 @@ impl PlotDecoder {
|
|||||||
}
|
}
|
||||||
SampleType::I64 => {
|
SampleType::I64 => {
|
||||||
let b = [
|
let b = [
|
||||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||||
bytes[4], bytes[5], bytes[6], bytes[7],
|
|
||||||
];
|
];
|
||||||
let val = match self.config.endian {
|
let val = match self.config.endian {
|
||||||
Endian::Big => i64::from_be_bytes(b),
|
Endian::Big => i64::from_be_bytes(b),
|
||||||
@@ -158,8 +157,7 @@ impl PlotDecoder {
|
|||||||
}
|
}
|
||||||
SampleType::U64 => {
|
SampleType::U64 => {
|
||||||
let b = [
|
let b = [
|
||||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||||
bytes[4], bytes[5], bytes[6], bytes[7],
|
|
||||||
];
|
];
|
||||||
let val = match self.config.endian {
|
let val = match self.config.endian {
|
||||||
Endian::Big => u64::from_be_bytes(b),
|
Endian::Big => u64::from_be_bytes(b),
|
||||||
@@ -177,8 +175,7 @@ impl PlotDecoder {
|
|||||||
}
|
}
|
||||||
SampleType::F64 => {
|
SampleType::F64 => {
|
||||||
let b = [
|
let b = [
|
||||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||||
bytes[4], bytes[5], bytes[6], bytes[7],
|
|
||||||
];
|
];
|
||||||
match self.config.endian {
|
match self.config.endian {
|
||||||
Endian::Big => f64::from_be_bytes(b),
|
Endian::Big => f64::from_be_bytes(b),
|
||||||
@@ -222,7 +219,8 @@ impl ProtocolDecoder for PlotDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Distribute into channels
|
// 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 {
|
match self.config.format {
|
||||||
PlotFormat::Interleaved | PlotFormat::XY => {
|
PlotFormat::Interleaved | PlotFormat::XY => {
|
||||||
for (i, val) in flat.into_iter().enumerate() {
|
for (i, val) in flat.into_iter().enumerate() {
|
||||||
@@ -351,9 +349,14 @@ mod tests {
|
|||||||
let d = PlotDecoder::new(cfg);
|
let d = PlotDecoder::new(cfg);
|
||||||
// ch0: 100, 300 ch1: 200, 400
|
// ch0: 100, 300 ch1: 200, 400
|
||||||
let data = vec![
|
let data = vec![
|
||||||
100u16.to_le_bytes(), 200u16.to_le_bytes(),
|
100u16.to_le_bytes(),
|
||||||
300u16.to_le_bytes(), 400u16.to_le_bytes(),
|
200u16.to_le_bytes(),
|
||||||
].into_iter().flatten().collect::<Vec<u8>>();
|
300u16.to_le_bytes(),
|
||||||
|
400u16.to_le_bytes(),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<u8>>();
|
||||||
let result = d.decode(&data).unwrap();
|
let result = d.decode(&data).unwrap();
|
||||||
match result {
|
match result {
|
||||||
DecodedData::Plot(frame) => {
|
DecodedData::Plot(frame) => {
|
||||||
@@ -376,9 +379,14 @@ mod tests {
|
|||||||
let d = PlotDecoder::new(cfg);
|
let d = PlotDecoder::new(cfg);
|
||||||
// ch0: 10, 20 ch1: 30, 40
|
// ch0: 10, 20 ch1: 30, 40
|
||||||
let data = vec![
|
let data = vec![
|
||||||
10u16.to_be_bytes(), 20u16.to_be_bytes(),
|
10u16.to_be_bytes(),
|
||||||
30u16.to_be_bytes(), 40u16.to_be_bytes(),
|
20u16.to_be_bytes(),
|
||||||
].into_iter().flatten().collect::<Vec<u8>>();
|
30u16.to_be_bytes(),
|
||||||
|
40u16.to_be_bytes(),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<u8>>();
|
||||||
let result = d.decode(&data).unwrap();
|
let result = d.decode(&data).unwrap();
|
||||||
match result {
|
match result {
|
||||||
DecodedData::Plot(frame) => {
|
DecodedData::Plot(frame) => {
|
||||||
@@ -401,9 +409,14 @@ mod tests {
|
|||||||
let d = PlotDecoder::new(cfg);
|
let d = PlotDecoder::new(cfg);
|
||||||
// (x,y) pairs: (10, 100), (20, 200)
|
// (x,y) pairs: (10, 100), (20, 200)
|
||||||
let data = vec![
|
let data = vec![
|
||||||
10u16.to_le_bytes(), 100u16.to_le_bytes(),
|
10u16.to_le_bytes(),
|
||||||
20u16.to_le_bytes(), 200u16.to_le_bytes(),
|
100u16.to_le_bytes(),
|
||||||
].into_iter().flatten().collect::<Vec<u8>>();
|
20u16.to_le_bytes(),
|
||||||
|
200u16.to_le_bytes(),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<u8>>();
|
||||||
let result = d.decode(&data).unwrap();
|
let result = d.decode(&data).unwrap();
|
||||||
match result {
|
match result {
|
||||||
DecodedData::Plot(frame) => {
|
DecodedData::Plot(frame) => {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
use crate::transport::serial::SerialTransport;
|
use crate::transport::serial::SerialTransport;
|
||||||
use crate::transport::tcp::TcpTransport;
|
use crate::transport::tcp::TcpTransport;
|
||||||
use crate::transport::udp::UdpTransport;
|
use crate::transport::udp::UdpTransport;
|
||||||
use crate::error::Result;
|
|
||||||
|
|
||||||
pub mod serial;
|
pub mod serial;
|
||||||
pub mod tcp;
|
pub mod tcp;
|
||||||
@@ -15,7 +16,7 @@ pub mod udp;
|
|||||||
pub enum TransportType {
|
pub enum TransportType {
|
||||||
Serial,
|
Serial,
|
||||||
Tcp,
|
Tcp,
|
||||||
Udp
|
Udp,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for TransportType {
|
impl std::fmt::Display for TransportType {
|
||||||
@@ -23,7 +24,7 @@ impl std::fmt::Display for TransportType {
|
|||||||
match self {
|
match self {
|
||||||
TransportType::Serial => write!(f, "Serial"),
|
TransportType::Serial => write!(f, "Serial"),
|
||||||
TransportType::Tcp => write!(f, "Tcp"),
|
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 {
|
pub enum TransportConfig {
|
||||||
Serial {
|
Serial {
|
||||||
port: String,
|
port: String,
|
||||||
baud_rate: u32
|
baud_rate: u32,
|
||||||
},
|
},
|
||||||
Tcp {
|
Tcp {
|
||||||
addr: String
|
addr: String,
|
||||||
},
|
},
|
||||||
Udp {
|
Udp {
|
||||||
bind_addr: String,
|
bind_addr: String,
|
||||||
remote_addr: Option<String>
|
remote_addr: Option<String>,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -98,6 +99,7 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect(&mut self) -> Result<()> {
|
pub async fn connect(&mut self) -> Result<()> {
|
||||||
|
info!(transport = ?self.transport_type(), name = self.name(), "Connection connecting");
|
||||||
match self {
|
match self {
|
||||||
Connection::Serial(t) => t.connect().await,
|
Connection::Serial(t) => t.connect().await,
|
||||||
Connection::Tcp(t) => t.connect().await,
|
Connection::Tcp(t) => t.connect().await,
|
||||||
@@ -106,6 +108,7 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn disconnect(&mut self) -> Result<()> {
|
pub async fn disconnect(&mut self) -> Result<()> {
|
||||||
|
info!(name = self.name(), "Connection disconnecting");
|
||||||
match self {
|
match self {
|
||||||
Connection::Serial(t) => t.disconnect().await,
|
Connection::Serial(t) => t.disconnect().await,
|
||||||
Connection::Tcp(t) => t.disconnect().await,
|
Connection::Tcp(t) => t.disconnect().await,
|
||||||
@@ -237,7 +240,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_transport_type_roundtrip() {
|
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 json = serde_json::to_string(&original).unwrap();
|
||||||
let roundtripped: TransportType = serde_json::from_str(&json).unwrap();
|
let roundtripped: TransportType = serde_json::from_str(&json).unwrap();
|
||||||
assert_eq!(original, roundtripped);
|
assert_eq!(original, roundtripped);
|
||||||
@@ -503,8 +510,7 @@ mod tests {
|
|||||||
unsafe fn wake(_: *const ()) {}
|
unsafe fn wake(_: *const ()) {}
|
||||||
unsafe fn wake_by_ref(_: *const ()) {}
|
unsafe fn wake_by_ref(_: *const ()) {}
|
||||||
unsafe fn drop(_: *const ()) {}
|
unsafe fn drop(_: *const ()) {}
|
||||||
static VTABLE: RawWakerVTable =
|
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
|
||||||
RawWakerVTable::new(clone, wake, wake_by_ref, drop);
|
|
||||||
unsafe { std::task::Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
|
unsafe { std::task::Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,9 +536,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_connection_poll_read_not_connected() {
|
fn test_connection_poll_read_not_connected() {
|
||||||
use tokio::io::ReadBuf;
|
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
use tokio::io::ReadBuf;
|
||||||
|
|
||||||
for mut conn in [serial_conn(), tcp_conn(), udp_conn()] {
|
for mut conn in [serial_conn(), tcp_conn(), udp_conn()] {
|
||||||
let pinned = Pin::new(&mut conn);
|
let pinned = Pin::new(&mut conn);
|
||||||
@@ -719,9 +725,13 @@ mod tests {
|
|||||||
|
|
||||||
let server = tokio::spawn(async move {
|
let server = tokio::spawn(async move {
|
||||||
let (mut stream, _) = listener.accept().await.unwrap();
|
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];
|
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");
|
assert_eq!(&buf, b"world");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -761,7 +771,9 @@ mod tests {
|
|||||||
remote.send_to(b"world", from).await.unwrap();
|
remote.send_to(b"world", from).await.unwrap();
|
||||||
|
|
||||||
let mut read_buf = [0u8; 5];
|
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");
|
assert_eq!(&read_buf, b"world");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,17 +109,22 @@ impl Transport for SerialTransport {
|
|||||||
|
|
||||||
async fn connect(&mut self) -> Result<()> {
|
async fn connect(&mut self) -> Result<()> {
|
||||||
if self.is_connected() {
|
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)
|
let port = tokio_serial::new(&self.port_name, self.baud_rate)
|
||||||
.open_native_async()
|
.open_native_async()
|
||||||
.map_err(|e| crate::error::Error::ConnectionFailed(format!(
|
.map_err(|e| {
|
||||||
"Failed to open {}: {}",
|
crate::error::Error::ConnectionFailed(format!(
|
||||||
self.port_name, e
|
"Failed to open {}: {}",
|
||||||
)))?;
|
self.port_name, e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
debug!("Serial port {} opened successfully", self.port_name);
|
debug!("Serial port {} opened successfully", self.port_name);
|
||||||
self.port = Some(port);
|
self.port = Some(port);
|
||||||
@@ -152,12 +157,8 @@ mod tests {
|
|||||||
unsafe fn raw_wake_by_ref(_: *const ()) {}
|
unsafe fn raw_wake_by_ref(_: *const ()) {}
|
||||||
unsafe fn raw_drop(_: *const ()) {}
|
unsafe fn raw_drop(_: *const ()) {}
|
||||||
|
|
||||||
static RAW_VTABLE: RawWakerVTable = RawWakerVTable::new(
|
static RAW_VTABLE: RawWakerVTable =
|
||||||
raw_clone,
|
RawWakerVTable::new(raw_clone, raw_wake, raw_wake_by_ref, raw_drop);
|
||||||
raw_wake,
|
|
||||||
raw_wake_by_ref,
|
|
||||||
raw_drop,
|
|
||||||
);
|
|
||||||
|
|
||||||
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &RAW_VTABLE)) }
|
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &RAW_VTABLE)) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
@@ -10,15 +9,12 @@ use crate::error::Result;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TcpTransport {
|
pub struct TcpTransport {
|
||||||
stream: Option<TcpStream>,
|
stream: Option<TcpStream>,
|
||||||
addr: String
|
addr: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TcpTransport {
|
impl TcpTransport {
|
||||||
pub fn new(addr: String) -> Self {
|
pub fn new(addr: String) -> Self {
|
||||||
Self {
|
Self { stream: None, addr }
|
||||||
stream: None,
|
|
||||||
addr
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn addr(&self) -> &str {
|
pub fn addr(&self) -> &str {
|
||||||
@@ -281,7 +277,10 @@ mod tests {
|
|||||||
async fn test_disconnect_without_connect() {
|
async fn test_disconnect_without_connect() {
|
||||||
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
|
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
|
||||||
let result = transport.disconnect().await;
|
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());
|
assert!(!transport.is_connected());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ impl AsyncRead for UdpTransport {
|
|||||||
socket.poll_recv(cx, &mut temp_buf)
|
socket.poll_recv(cx, &mut temp_buf)
|
||||||
} else {
|
} else {
|
||||||
// poll_recv_from 会返回 (usize, SocketAddr),我们将其映射回统一的 () 类型
|
// 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 {
|
match poll_result {
|
||||||
@@ -268,9 +270,7 @@ mod tests {
|
|||||||
let result = Pin::new(&mut t).poll_read(&mut cx, &mut buf);
|
let result = Pin::new(&mut t).poll_read(&mut cx, &mut buf);
|
||||||
match result {
|
match result {
|
||||||
Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
|
Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
|
||||||
other => panic!(
|
other => panic!("expected Poll::Ready(Err(NotConnected)), got {other:?}"),
|
||||||
"expected Poll::Ready(Err(NotConnected)), got {other:?}"
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,9 +282,7 @@ mod tests {
|
|||||||
let result = Pin::new(&mut t).poll_write(&mut cx, data);
|
let result = Pin::new(&mut t).poll_write(&mut cx, data);
|
||||||
match result {
|
match result {
|
||||||
Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
|
Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
|
||||||
other => panic!(
|
other => panic!("expected Poll::Ready(Err(NotConnected)), got {other:?}"),
|
||||||
"expected Poll::Ready(Err(NotConnected)), got {other:?}"
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +320,9 @@ mod tests {
|
|||||||
let remote_addr = remote.local_addr().unwrap().to_string();
|
let remote_addr = remote.local_addr().unwrap().to_string();
|
||||||
|
|
||||||
let mut t = UdpTransport::new("127.0.0.1:0".into(), Some(remote_addr));
|
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());
|
assert!(t.is_connected());
|
||||||
|
|
||||||
t.disconnect().await.expect("disconnect should succeed");
|
t.disconnect().await.expect("disconnect should succeed");
|
||||||
|
|||||||
@@ -5,26 +5,23 @@ use tokio::net::{TcpListener, UdpSocket};
|
|||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
|
||||||
use xserial_core::frame::{
|
use xserial_core::frame::{
|
||||||
|
Endian, Framer,
|
||||||
cobs::CobsFramer,
|
cobs::CobsFramer,
|
||||||
fixed::FixedLengthFramer,
|
fixed::FixedLengthFramer,
|
||||||
length::{LengthConfig, LengthPrefixedFramer},
|
length::{LengthConfig, LengthPrefixedFramer},
|
||||||
line::{LineConfig, LineFramer},
|
line::{LineConfig, LineFramer},
|
||||||
Endian, Framer,
|
|
||||||
};
|
};
|
||||||
use xserial_core::protocol::{
|
use xserial_core::protocol::{
|
||||||
|
DecodedData, ProtocolDecoder,
|
||||||
hex::{HexConfig, HexDecoder},
|
hex::{HexConfig, HexDecoder},
|
||||||
plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType},
|
plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType},
|
||||||
text::{TextDecoder, TextEncoding},
|
text::{TextDecoder, TextEncoding},
|
||||||
DecodedData, ProtocolDecoder,
|
|
||||||
};
|
};
|
||||||
use xserial_core::transport::{Connection, TransportConfig, TransportType};
|
use xserial_core::transport::{Connection, TransportConfig, TransportType};
|
||||||
|
|
||||||
const TEST_TIMEOUT: Duration = Duration::from_secs(5);
|
const TEST_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
async fn read_to_framer(
|
async fn read_to_framer(conn: &mut Connection, framer: &mut dyn Framer) -> Vec<Vec<u8>> {
|
||||||
conn: &mut Connection,
|
|
||||||
framer: &mut dyn Framer,
|
|
||||||
) -> Vec<Vec<u8>> {
|
|
||||||
let mut buf = [0u8; 4096];
|
let mut buf = [0u8; 4096];
|
||||||
let mut all_frames = Vec::new();
|
let mut all_frames = Vec::new();
|
||||||
|
|
||||||
@@ -384,9 +381,7 @@ async fn connection_reconnect_tcp() {
|
|||||||
stream.write_all(b"first\n").await.unwrap();
|
stream.write_all(b"first\n").await.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut conn = Connection::new(TransportConfig::Tcp {
|
let mut conn = Connection::new(TransportConfig::Tcp { addr: addr.clone() });
|
||||||
addr: addr.clone(),
|
|
||||||
});
|
|
||||||
conn.connect().await.unwrap();
|
conn.connect().await.unwrap();
|
||||||
|
|
||||||
let mut framer = LineFramer::new(LineConfig::default());
|
let mut framer = LineFramer::new(LineConfig::default());
|
||||||
|
|||||||
137
crates/xserial-gui/src/app.rs
Normal file
137
crates/xserial-gui/src/app.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
use crate::panels::{config, sidebar};
|
||||||
|
use tracing::debug;
|
||||||
|
use xserial_client::SessionManager;
|
||||||
|
use xserial_client::session::SessionEvent;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ConnectionStatus {
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
Connecting,
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SessionTab {
|
||||||
|
pub id: u64,
|
||||||
|
pub status: ConnectionStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct XserialApp {
|
||||||
|
manager: SessionManager,
|
||||||
|
tabs: Vec<SessionTab>,
|
||||||
|
active: usize,
|
||||||
|
config_open: bool,
|
||||||
|
config_form: config::ConfigForm,
|
||||||
|
event_rx: Option<tokio::sync::broadcast::Receiver<SessionEvent>>,
|
||||||
|
pending: Vec<SessionEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl XserialApp {
|
||||||
|
pub fn new(m: SessionManager, rx: tokio::sync::broadcast::Receiver<SessionEvent>) -> Self {
|
||||||
|
Self {
|
||||||
|
manager: m,
|
||||||
|
tabs: vec![],
|
||||||
|
active: 0,
|
||||||
|
config_open: false,
|
||||||
|
config_form: config::ConfigForm::default(),
|
||||||
|
event_rx: Some(rx),
|
||||||
|
pending: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for XserialApp {
|
||||||
|
fn update(&mut self, _ctx: &egui::Context, _frame: &mut eframe::Frame) {}
|
||||||
|
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||||
|
// 拉取 SessionManager 事件
|
||||||
|
if let Some(ref mut rx) = self.event_rx {
|
||||||
|
while let Ok(e) = rx.try_recv() {
|
||||||
|
debug!(event = ?e, "GUI received event");
|
||||||
|
self.pending.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for e in self.pending.drain(..) {
|
||||||
|
match e {
|
||||||
|
SessionEvent::Connected(id) => {
|
||||||
|
debug!(session_id = id, "GUI: Connected");
|
||||||
|
if let Some(t) = self.tabs.iter_mut().find(|t| t.id == id) {
|
||||||
|
t.status = ConnectionStatus::Connected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionEvent::Disconnected(id) | SessionEvent::Closed(id) => {
|
||||||
|
debug!(session_id = id, "GUI: Disconnected/Closed");
|
||||||
|
if let Some(t) = self.tabs.iter_mut().find(|t| t.id == id) {
|
||||||
|
t.status = ConnectionStatus::Disconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionEvent::Error(id, msg) => {
|
||||||
|
debug!(session_id = id, error = %msg, "GUI: Error");
|
||||||
|
if let Some(t) = self.tabs.iter_mut().find(|t| t.id == id) {
|
||||||
|
t.status = ConnectionStatus::Error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionEvent::Data(id, entry) => {
|
||||||
|
debug!(session_id = id, pipeline = %entry.pipeline_name, "GUI: Data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config 弹窗
|
||||||
|
if self.config_open {
|
||||||
|
egui::Window::new("Session Config").show(ui.ctx(), |ui| {
|
||||||
|
if let Some(cfg) = config::render(ui, &mut self.config_form) {
|
||||||
|
let h = self.manager.create(cfg);
|
||||||
|
self.tabs.push(SessionTab {
|
||||||
|
id: h.id,
|
||||||
|
status: ConnectionStatus::Connecting,
|
||||||
|
});
|
||||||
|
self.active = self.tabs.len() - 1;
|
||||||
|
self.config_open = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
let mut on_new = false;
|
||||||
|
let mut on_delete: Option<usize> = None;
|
||||||
|
egui::Panel::left("sidebar")
|
||||||
|
.resizable(false)
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
let ss: Vec<(u64, ConnectionStatus)> =
|
||||||
|
self.tabs.iter().map(|t| (t.id, t.status.clone())).collect();
|
||||||
|
sidebar::render(ui, &ss, &mut self.active, &mut on_new, &mut on_delete);
|
||||||
|
});
|
||||||
|
if on_new {
|
||||||
|
self.config_open = true;
|
||||||
|
}
|
||||||
|
if let Some(i) = on_delete {
|
||||||
|
if let Some(tab) = self.tabs.get(i) {
|
||||||
|
self.manager.remove(tab.id);
|
||||||
|
}
|
||||||
|
self.tabs.remove(i);
|
||||||
|
if self.active >= self.tabs.len() && !self.tabs.is_empty() {
|
||||||
|
self.active = self.tabs.len() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Central
|
||||||
|
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||||
|
if self.tabs.is_empty() {
|
||||||
|
ui.heading("No sessions.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.active >= self.tabs.len() {
|
||||||
|
self.active = self.tabs.len() - 1;
|
||||||
|
}
|
||||||
|
let tab = &self.tabs[self.active];
|
||||||
|
let txt = match &tab.status {
|
||||||
|
ConnectionStatus::Connected => "🟢 Connected",
|
||||||
|
ConnectionStatus::Disconnected => "⚫ Disconnected",
|
||||||
|
ConnectionStatus::Connecting => "🟡 Connecting...",
|
||||||
|
ConnectionStatus::Error(e) => &format!("🔴 {}", e),
|
||||||
|
};
|
||||||
|
ui.heading(format!("Session {} — {}", tab.id, txt));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
|
mod app;
|
||||||
|
mod panels;
|
||||||
|
|
||||||
|
use xserial_client::SessionManager;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("xserial-gui");
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
|
||||||
|
let _guard = rt.enter();
|
||||||
|
|
||||||
|
let mgr = SessionManager::new();
|
||||||
|
let rx = mgr.subscribe();
|
||||||
|
|
||||||
|
let _ = eframe::run_native(
|
||||||
|
"xserial",
|
||||||
|
eframe::NativeOptions::default(),
|
||||||
|
Box::new(|_cc| Ok(Box::new(app::XserialApp::new(mgr, rx)))),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
245
crates/xserial-gui/src/panels/config.rs
Normal file
245
crates/xserial-gui/src/panels/config.rs
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
use egui::{ComboBox, DragValue, ScrollArea, TextEdit, Ui};
|
||||||
|
use xserial_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig};
|
||||||
|
use xserial_core::transport::TransportConfig;
|
||||||
|
|
||||||
|
// ── 表单数据结构(简化版,方便 UI 渲染)────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ConfigForm {
|
||||||
|
pub transport: TransportChoice,
|
||||||
|
pub port: String,
|
||||||
|
pub baud: u32,
|
||||||
|
pub addr: String,
|
||||||
|
pub udp_bind: String,
|
||||||
|
pub udp_remote: String,
|
||||||
|
pub pipelines: Vec<(String, FramerChoice, DecoderChoice)>,
|
||||||
|
pub history: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum TransportChoice {
|
||||||
|
Serial,
|
||||||
|
Tcp,
|
||||||
|
Udp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum FramerChoice {
|
||||||
|
Line,
|
||||||
|
Fixed(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum DecoderChoice {
|
||||||
|
Text,
|
||||||
|
Hex,
|
||||||
|
Plot,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ConfigForm {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
transport: TransportChoice::Tcp,
|
||||||
|
port: "/dev/ttyUSB0".into(),
|
||||||
|
baud: 115200,
|
||||||
|
addr: "127.0.0.1:8080".into(),
|
||||||
|
udp_bind: "0.0.0.0:9000".into(),
|
||||||
|
udp_remote: String::new(),
|
||||||
|
pipelines: vec![("text".into(), FramerChoice::Line, DecoderChoice::Text)],
|
||||||
|
history: 10000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigForm {
|
||||||
|
// 把表单数据转换成 xserial-client 的 SessionConfig
|
||||||
|
pub fn to_session_config(&self) -> SessionConfig {
|
||||||
|
SessionConfig {
|
||||||
|
transport: match self.transport {
|
||||||
|
TransportChoice::Serial => TransportConfig::Serial {
|
||||||
|
port: self.port.clone(),
|
||||||
|
baud_rate: self.baud,
|
||||||
|
},
|
||||||
|
TransportChoice::Tcp => TransportConfig::Tcp {
|
||||||
|
addr: self.addr.clone(),
|
||||||
|
},
|
||||||
|
TransportChoice::Udp => TransportConfig::Udp {
|
||||||
|
bind_addr: self.udp_bind.clone(),
|
||||||
|
remote_addr: if self.udp_remote.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.udp_remote.clone())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pipelines: self
|
||||||
|
.pipelines
|
||||||
|
.iter()
|
||||||
|
.map(|(name, fc, dc)| PipelineConfig {
|
||||||
|
name: name.clone(),
|
||||||
|
framer: match fc {
|
||||||
|
FramerChoice::Line => FramerConfig::Line {
|
||||||
|
strip_cr: true,
|
||||||
|
max_line_len: 1_048_576,
|
||||||
|
},
|
||||||
|
FramerChoice::Fixed(n) => FramerConfig::Fixed { frame_len: *n },
|
||||||
|
},
|
||||||
|
decoder: match dc {
|
||||||
|
DecoderChoice::Text => DecoderConfig::Text {
|
||||||
|
encoding: xserial_core::protocol::text::TextEncoding::Utf8,
|
||||||
|
},
|
||||||
|
DecoderChoice::Hex => DecoderConfig::Hex {
|
||||||
|
uppercase: false,
|
||||||
|
separator: " ".into(),
|
||||||
|
bytes_per_group: 1,
|
||||||
|
endian: xserial_core::protocol::Endian::Big,
|
||||||
|
},
|
||||||
|
DecoderChoice::Plot => DecoderConfig::Plot {
|
||||||
|
sample_type: xserial_core::protocol::plot::SampleType::F32,
|
||||||
|
endian: xserial_core::protocol::Endian::Little,
|
||||||
|
channels: 1,
|
||||||
|
format: xserial_core::protocol::plot::PlotFormat::Interleaved,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
history_limit: self.history,
|
||||||
|
auto_reconnect: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 渲染函数 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn render(ui: &mut Ui, form: &mut ConfigForm) -> Option<SessionConfig> {
|
||||||
|
let mut result = None;
|
||||||
|
|
||||||
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
ui.heading("Transport");
|
||||||
|
|
||||||
|
// 传输类型下拉
|
||||||
|
ComboBox::from_label("Type")
|
||||||
|
.selected_text(match form.transport {
|
||||||
|
TransportChoice::Serial => "Serial",
|
||||||
|
TransportChoice::Tcp => "TCP",
|
||||||
|
TransportChoice::Udp => "UDP",
|
||||||
|
})
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
ui.selectable_value(&mut form.transport, TransportChoice::Serial, "Serial");
|
||||||
|
ui.selectable_value(&mut form.transport, TransportChoice::Tcp, "TCP");
|
||||||
|
ui.selectable_value(&mut form.transport, TransportChoice::Udp, "UDP");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据类型显示不同字段
|
||||||
|
match form.transport {
|
||||||
|
TransportChoice::Serial => {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Port:");
|
||||||
|
ui.text_edit_singleline(&mut form.port);
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Baud:");
|
||||||
|
ui.add(DragValue::new(&mut form.baud).range(300..=12_000_000));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
TransportChoice::Tcp => {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Addr:");
|
||||||
|
ui.text_edit_singleline(&mut form.addr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
TransportChoice::Udp => {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Bind:");
|
||||||
|
ui.text_edit_singleline(&mut form.udp_bind);
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Remote:");
|
||||||
|
ui.text_edit_singleline(&mut form.udp_remote);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
ui.heading("Pipelines");
|
||||||
|
|
||||||
|
let mut remove = None;
|
||||||
|
|
||||||
|
// 管道列表
|
||||||
|
for (i, (name, fc, dc)) in form.pipelines.iter_mut().enumerate() {
|
||||||
|
ui.group(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Name:");
|
||||||
|
ui.add(TextEdit::singleline(name).desired_width(60.0));
|
||||||
|
|
||||||
|
// Framer 选择
|
||||||
|
let fsel = match fc {
|
||||||
|
FramerChoice::Line => "Line".to_string(),
|
||||||
|
FramerChoice::Fixed(n) => format!("Fixed({})", n),
|
||||||
|
};
|
||||||
|
ComboBox::from_id_salt(format!("f{}", i))
|
||||||
|
.selected_text(fsel)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
if ui.selectable_label(false, "Line").clicked() {
|
||||||
|
*fc = FramerChoice::Line;
|
||||||
|
}
|
||||||
|
if ui.selectable_label(false, "Fixed").clicked() {
|
||||||
|
*fc = FramerChoice::Fixed(8);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let FramerChoice::Fixed(n) = fc {
|
||||||
|
ui.add(DragValue::new(n).range(1..=65536));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decoder 选择
|
||||||
|
ComboBox::from_id_salt(format!("d{}", i))
|
||||||
|
.selected_text(match dc {
|
||||||
|
DecoderChoice::Text => "Text",
|
||||||
|
DecoderChoice::Hex => "Hex",
|
||||||
|
DecoderChoice::Plot => "Plot",
|
||||||
|
})
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
if ui.selectable_label(false, "Text").clicked() {
|
||||||
|
*dc = DecoderChoice::Text;
|
||||||
|
}
|
||||||
|
if ui.selectable_label(false, "Hex").clicked() {
|
||||||
|
*dc = DecoderChoice::Hex;
|
||||||
|
}
|
||||||
|
if ui.selectable_label(false, "Plot").clicked() {
|
||||||
|
*dc = DecoderChoice::Plot;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ui.button("✕").clicked() {
|
||||||
|
remove = Some(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(i) = remove {
|
||||||
|
form.pipelines.remove(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("+ Add Pipeline").clicked() {
|
||||||
|
form.pipelines.push((
|
||||||
|
format!("p{}", form.pipelines.len()),
|
||||||
|
FramerChoice::Line,
|
||||||
|
DecoderChoice::Text,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("History:");
|
||||||
|
ui.add(DragValue::new(&mut form.history).range(100..=1_000_000));
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
if ui.button("✓ Create Session").clicked() {
|
||||||
|
result = Some(form.to_session_config());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
2
crates/xserial-gui/src/panels/mod.rs
Normal file
2
crates/xserial-gui/src/panels/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod config;
|
||||||
|
pub mod sidebar;
|
||||||
51
crates/xserial-gui/src/panels/sidebar.rs
Normal file
51
crates/xserial-gui/src/panels/sidebar.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use crate::app::ConnectionStatus;
|
||||||
|
use egui::{Color32, RichText, Ui};
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
ui: &mut Ui,
|
||||||
|
sessions: &[(u64, ConnectionStatus)], // (id, 连接状态)
|
||||||
|
active: &mut usize, // 当前选中的会话索引
|
||||||
|
on_new: &mut bool, // 点击了"新建"
|
||||||
|
on_delete: &mut Option<usize>, // 点击了"删除"
|
||||||
|
) {
|
||||||
|
ui.heading("Sessions");
|
||||||
|
|
||||||
|
// 新建按钮 — 绿色文字
|
||||||
|
if ui
|
||||||
|
.button(RichText::new("+ New Session").color(Color32::GREEN))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
*on_new = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// 会话列表
|
||||||
|
for (i, (id, status)) in sessions.iter().enumerate() {
|
||||||
|
let (color, icon) = match status {
|
||||||
|
ConnectionStatus::Connected => (Color32::GREEN, "●"),
|
||||||
|
ConnectionStatus::Disconnected => (Color32::GRAY, "○"),
|
||||||
|
ConnectionStatus::Connecting => (Color32::YELLOW, "◌"),
|
||||||
|
ConnectionStatus::Error(_) => (Color32::RED, "⨉"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// selectable_label: 点击高亮,clicked() 判断是否被点击
|
||||||
|
let label = format!("{} Session {}", icon, id);
|
||||||
|
if ui
|
||||||
|
.selectable_label(*active == i, RichText::new(label).color(color))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
*active = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// 删除按钮 — 红色文字
|
||||||
|
if ui
|
||||||
|
.button(RichText::new("Delete").color(Color32::RED))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
*on_delete = Some(*active);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1 @@
|
|||||||
fn main() {
|
fn main() {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user