Files
xserial/crates/xserial-core/src/protocol/plot.rs
FallenSigh 530986d068 feat: xserial-client with session management, mlua integration, and 238 tests
- Session: background I/O task with SessionHandle for send/read/close/reconfigure
- SessionManager: multi-session collection with aggregated event broadcast
- Config: serializable SessionConfig/PipelineConfig/FramerConfig/DecoderConfig
- Lua: xserial global (open/list_ports/sleep/log) + session userdata API
- RingBuffer: bounded history buffer with pipeline filtering
- Core: added Serialize/Deserialize+Default to Endian/TextEncoding/PlotFormat
- Core: added Send bound to Framer trait for tokio task compatibility
- 238 tests (193 core + 10 config + 7 Lua + 9 session + 18 pipeline + 1 doctest)
2026-06-08 00:25:59 +08:00

508 lines
16 KiB
Rust

use super::{DecodedData, ProtocolDecoder};
use crate::protocol::Endian;
use serde::{Deserialize, Serialize};
/// Numeric type of samples in a plot frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SampleType {
I8,
U8,
I16,
U16,
I32,
U32,
I64,
U64,
F32,
F64,
}
impl SampleType {
pub fn name(&self) -> &str {
match self {
SampleType::I8 => "i8",
SampleType::U8 => "u8",
SampleType::I16 => "i16",
SampleType::U16 => "u16",
SampleType::I32 => "i32",
SampleType::U32 => "u32",
SampleType::I64 => "i64",
SampleType::U64 => "u64",
SampleType::F32 => "f32",
SampleType::F64 => "f64",
}
}
pub fn byte_size(&self) -> usize {
match self {
SampleType::I8 | SampleType::U8 => 1,
SampleType::I16 | SampleType::U16 => 2,
SampleType::I32 | SampleType::U32 | SampleType::F32 => 4,
SampleType::I64 | SampleType::U64 | SampleType::F64 => 8,
}
}
}
/// Channel layout for multi-channel plot data.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum PlotFormat {
#[default]
Interleaved,
Block,
XY,
}
/// Configuration for the plot protocol decoder.
#[derive(Debug, Clone)]
pub struct PlotConfig {
pub sample_type: SampleType,
pub endian: Endian,
pub channels: usize,
pub format: PlotFormat,
}
impl Default for PlotConfig {
fn default() -> Self {
Self {
sample_type: SampleType::F32,
endian: Endian::Little,
channels: 1,
format: PlotFormat::Interleaved,
}
}
}
/// A decoded plot frame containing numeric channel data.
#[derive(Debug, Clone)]
pub struct PlotFrame {
pub channels: Vec<Vec<f64>>,
pub raw: Vec<u8>,
pub sample_type: SampleType,
}
impl PlotFrame {
pub fn sample_count(&self) -> usize {
self.channels.first().map_or(0, |c| c.len())
}
}
/// Plot protocol decoder.
///
/// Interprets binary frames as numeric samples and organizes them into
/// channels according to the configured layout.
#[derive(Debug, Clone)]
pub struct PlotDecoder {
config: PlotConfig,
}
impl PlotDecoder {
pub fn new(config: PlotConfig) -> Self {
Self { config }
}
pub fn config(&self) -> &PlotConfig {
&self.config
}
fn read_one_sample(&self, data: &[u8]) -> Option<f64> {
let size = self.config.sample_type.byte_size();
if data.len() < size {
return None;
}
let bytes = &data[..size];
let raw = match self.config.sample_type {
SampleType::I8 => bytes[0] as i8 as f64,
SampleType::U8 => bytes[0] as f64,
SampleType::I16 => {
let b = [bytes[0], bytes[1]];
let val = match self.config.endian {
Endian::Big => i16::from_be_bytes(b),
Endian::Little => i16::from_le_bytes(b),
};
val as f64
}
SampleType::U16 => {
let b = [bytes[0], bytes[1]];
let val = match self.config.endian {
Endian::Big => u16::from_be_bytes(b),
Endian::Little => u16::from_le_bytes(b),
};
val as f64
}
SampleType::I32 => {
let b = [bytes[0], bytes[1], bytes[2], bytes[3]];
let val = match self.config.endian {
Endian::Big => i32::from_be_bytes(b),
Endian::Little => i32::from_le_bytes(b),
};
val as f64
}
SampleType::U32 => {
let b = [bytes[0], bytes[1], bytes[2], bytes[3]];
let val = match self.config.endian {
Endian::Big => u32::from_be_bytes(b),
Endian::Little => u32::from_le_bytes(b),
};
val as f64
}
SampleType::I64 => {
let b = [
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),
Endian::Little => i64::from_le_bytes(b),
};
val as f64
}
SampleType::U64 => {
let b = [
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),
Endian::Little => u64::from_le_bytes(b),
};
val as f64
}
SampleType::F32 => {
let b = [bytes[0], bytes[1], bytes[2], bytes[3]];
let val = match self.config.endian {
Endian::Big => f32::from_be_bytes(b),
Endian::Little => f32::from_le_bytes(b),
};
val as f64
}
SampleType::F64 => {
let b = [
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),
Endian::Little => f64::from_le_bytes(b),
}
}
};
Some(raw)
}
}
impl ProtocolDecoder for PlotDecoder {
fn name(&self) -> &str {
"Plot"
}
fn decode(&self, frame: &[u8]) -> Option<DecodedData> {
let sample_size = self.config.sample_type.byte_size();
let num_channels = match self.config.format {
PlotFormat::XY => 2,
_ => self.config.channels.max(1),
};
let total_samples = frame.len() / sample_size;
if total_samples == 0 {
return None;
}
let samples_per_channel = total_samples / num_channels;
if samples_per_channel == 0 {
return None;
}
// Read all samples
let mut flat: Vec<f64> = Vec::with_capacity(total_samples);
let mut offset = 0;
while offset + sample_size <= frame.len() {
let val = self.read_one_sample(&frame[offset..])?;
flat.push(val);
offset += sample_size;
}
// Distribute into 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() {
let ch = i % num_channels;
if channels[ch].len() < samples_per_channel {
channels[ch].push(val);
}
}
}
PlotFormat::Block => {
for (ch, channel) in channels.iter_mut().enumerate() {
let start = ch * samples_per_channel;
let end = start + samples_per_channel;
if end <= flat.len() {
channel.extend_from_slice(&flat[start..end]);
}
}
}
}
Some(DecodedData::Plot(PlotFrame {
channels,
raw: frame.to_vec(),
sample_type: self.config.sample_type,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sample_type_names() {
assert_eq!(SampleType::I8.name(), "i8");
assert_eq!(SampleType::U16.name(), "u16");
assert_eq!(SampleType::F32.name(), "f32");
assert_eq!(SampleType::F64.name(), "f64");
}
#[test]
fn sample_type_sizes() {
assert_eq!(SampleType::I8.byte_size(), 1);
assert_eq!(SampleType::U8.byte_size(), 1);
assert_eq!(SampleType::I16.byte_size(), 2);
assert_eq!(SampleType::U16.byte_size(), 2);
assert_eq!(SampleType::I32.byte_size(), 4);
assert_eq!(SampleType::F32.byte_size(), 4);
assert_eq!(SampleType::I64.byte_size(), 8);
assert_eq!(SampleType::F64.byte_size(), 8);
}
#[test]
fn plot_single_channel_u8() {
let cfg = PlotConfig {
sample_type: SampleType::U8,
channels: 1,
..PlotConfig::default()
};
let d = PlotDecoder::new(cfg);
let data = vec![10u8, 20, 30, 40];
let result = d.decode(&data).unwrap();
match result {
DecodedData::Plot(frame) => {
assert_eq!(frame.channels.len(), 1);
assert_eq!(frame.channels[0], vec![10.0, 20.0, 30.0, 40.0]);
assert_eq!(frame.sample_count(), 4);
}
other => panic!("expected Plot, got {:?}", other),
}
}
#[test]
fn plot_single_channel_i16_le() {
let cfg = PlotConfig {
sample_type: SampleType::I16,
endian: Endian::Little,
channels: 1,
..PlotConfig::default()
};
let d = PlotDecoder::new(cfg);
let data = vec![0x00, 0x80, 0xff, 0x7f]; // -32768, 32767 in LE
let result = d.decode(&data).unwrap();
match result {
DecodedData::Plot(frame) => {
assert_eq!(frame.channels.len(), 1);
assert_eq!(frame.channels[0], vec![-32768.0, 32767.0]);
}
other => panic!("expected Plot, got {:?}", other),
}
}
#[test]
fn plot_single_channel_f32_le() {
let cfg = PlotConfig {
sample_type: SampleType::F32,
endian: Endian::Little,
channels: 1,
..PlotConfig::default()
};
let d = PlotDecoder::new(cfg);
// 1.0, -2.5 in f32 LE
let mut data = Vec::new();
data.extend_from_slice(&1.0f32.to_le_bytes());
data.extend_from_slice(&(-2.5f32).to_le_bytes());
let result = d.decode(&data).unwrap();
match result {
DecodedData::Plot(frame) => {
assert_eq!(frame.channels.len(), 1);
assert_eq!(frame.channels[0].len(), 2);
assert!((frame.channels[0][0] - 1.0).abs() < 1e-6);
assert!((frame.channels[0][1] - (-2.5)).abs() < 1e-6);
}
other => panic!("expected Plot, got {:?}", other),
}
}
#[test]
fn plot_two_channels_interleaved_u16_le() {
let cfg = PlotConfig {
sample_type: SampleType::U16,
endian: Endian::Little,
channels: 2,
format: PlotFormat::Interleaved,
};
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>>();
let result = d.decode(&data).unwrap();
match result {
DecodedData::Plot(frame) => {
assert_eq!(frame.channels.len(), 2);
assert_eq!(frame.channels[0], vec![100.0, 300.0]);
assert_eq!(frame.channels[1], vec![200.0, 400.0]);
}
other => panic!("expected Plot, got {:?}", other),
}
}
#[test]
fn plot_two_channels_block_u16_be() {
let cfg = PlotConfig {
sample_type: SampleType::U16,
endian: Endian::Big,
channels: 2,
format: PlotFormat::Block,
};
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>>();
let result = d.decode(&data).unwrap();
match result {
DecodedData::Plot(frame) => {
assert_eq!(frame.channels.len(), 2);
assert_eq!(frame.channels[0], vec![10.0, 20.0]);
assert_eq!(frame.channels[1], vec![30.0, 40.0]);
}
other => panic!("expected Plot, got {:?}", other),
}
}
#[test]
fn plot_xy_format() {
let cfg = PlotConfig {
sample_type: SampleType::U16,
endian: Endian::Little,
channels: 0, // ignored for XY
format: PlotFormat::XY,
};
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>>();
let result = d.decode(&data).unwrap();
match result {
DecodedData::Plot(frame) => {
assert_eq!(frame.channels.len(), 2);
assert_eq!(frame.channels[0], vec![10.0, 20.0]); // X
assert_eq!(frame.channels[1], vec![100.0, 200.0]); // Y
}
other => panic!("expected Plot, got {:?}", other),
}
}
#[test]
fn plot_empty_frame_returns_none() {
let d = PlotDecoder::new(PlotConfig::default());
assert!(d.decode(&[]).is_none());
}
#[test]
fn plot_too_few_bytes_returns_none() {
let cfg = PlotConfig {
sample_type: SampleType::F64,
channels: 1,
..PlotConfig::default()
};
let d = PlotDecoder::new(cfg);
assert!(d.decode(&[0x00, 0x01, 0x02]).is_none());
}
#[test]
fn plot_insufficient_for_channels_returns_none() {
let cfg = PlotConfig {
sample_type: SampleType::U8,
endian: Endian::Little,
channels: 3,
format: PlotFormat::Interleaved,
};
let d = PlotDecoder::new(cfg);
// 2 bytes for 3 channels → not enough for 1 full sample per channel
assert!(d.decode(&[1, 2]).is_none());
}
#[test]
fn plot_trailing_partial_sample_ignored() {
let cfg = PlotConfig {
sample_type: SampleType::U32,
channels: 1,
..PlotConfig::default()
};
let d = PlotDecoder::new(cfg);
// 7 bytes: 1 full u32 (4 bytes) + 3 trailing bytes
let data = vec![1u32.to_le_bytes().to_vec(), vec![0xff; 3]].concat();
let result = d.decode(&data).unwrap();
match result {
DecodedData::Plot(frame) => {
assert_eq!(frame.channels.len(), 1);
assert_eq!(frame.channels[0], vec![1.0]);
}
other => panic!("expected Plot, got {:?}", other),
}
}
#[test]
fn plot_name() {
let d = PlotDecoder::new(PlotConfig::default());
assert_eq!(d.name(), "Plot");
}
#[test]
fn plot_frame_sample_count_empty() {
let frame = PlotFrame {
channels: vec![],
raw: vec![],
sample_type: SampleType::U8,
};
assert_eq!(frame.sample_count(), 0);
}
#[test]
fn plot_raw_preserved() {
let cfg = PlotConfig {
sample_type: SampleType::U8,
channels: 1,
..PlotConfig::default()
};
let d = PlotDecoder::new(cfg);
let data = vec![1, 2, 3];
let result = d.decode(&data).unwrap();
match result {
DecodedData::Plot(frame) => {
assert_eq!(frame.raw, data);
}
other => panic!("expected Plot, got {:?}", other),
}
}
#[test]
fn plot_decoder_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<PlotDecoder>();
}
}