Files
xserial/crates/pipeview-core/src/pipeline.rs

439 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::frame::Framer;
use crate::protocol::{DecodedData, ProtocolDecoder};
use tracing::debug;
/// A self-contained framing + decoding pipeline.
///
/// Each pipeline has its own `Framer` (with independent internal buffer)
/// and its own `ProtocolDecoder`. Feed the same byte chunk to multiple
/// pipelines and each will extract and decode only the frames it understands.
pub struct Pipeline {
name: String,
framer: Box<dyn Framer>,
decoder: Box<dyn ProtocolDecoder>,
}
impl Pipeline {
pub fn new(
name: impl Into<String>,
framer: Box<dyn Framer>,
decoder: Box<dyn ProtocolDecoder>,
) -> Self {
Self {
name: name.into(),
framer,
decoder,
}
}
pub fn name(&self) -> &str {
&self.name
}
fn feed_inner(&mut self, data: &[u8]) -> Vec<DecodedData> {
self.framer
.feed(data)
.into_iter()
.filter_map(|frame| self.decoder.decode(&frame))
.collect()
}
fn flush_inner(&mut self) -> Vec<DecodedData> {
self.framer
.flush()
.into_iter()
.filter_map(|frame| self.decoder.decode(&frame))
.collect()
}
fn reset_inner(&mut self) {
self.framer.reset();
}
}
/// One decoded result from a pipeline, tagged with the pipeline name.
#[derive(Debug, Clone)]
pub struct PipelineResult {
pub pipeline_name: String,
pub data: DecodedData,
}
/// Manages multiple [`Pipeline`]s, feeding the same byte stream to all of them.
///
/// ```
/// use pipeview_core::pipeline::{MultiPipeline, Pipeline};
/// use pipeview_core::frame::line::{LineFramer, LineConfig};
/// use pipeview_core::frame::fixed::FixedLengthFramer;
/// use pipeview_core::protocol::text::{TextDecoder, TextEncoding};
/// use pipeview_core::protocol::hex::{HexDecoder, HexConfig};
///
/// let mut mp = MultiPipeline::new();
/// mp.add(
/// Pipeline::new("text",
/// Box::new(LineFramer::new(LineConfig::default())),
/// Box::new(TextDecoder::new(TextEncoding::Utf8)),
/// )
/// );
/// mp.add(
/// Pipeline::new("hex",
/// Box::new(FixedLengthFramer::new(8)),
/// Box::new(HexDecoder::new(HexConfig::default())),
/// )
/// );
///
/// let results = mp.feed(b"hello\n");
/// // "text" pipeline produced one line, "hex" pipeline is still buffering
/// ```
pub struct MultiPipeline {
pipelines: Vec<Pipeline>,
}
impl MultiPipeline {
pub fn new() -> Self {
Self {
pipelines: Vec::new(),
}
}
pub fn add(&mut self, pipeline: Pipeline) {
self.pipelines.push(pipeline);
}
pub fn pipeline_count(&self) -> usize {
self.pipelines.len()
}
/// Feed a chunk of bytes to every pipeline.
///
/// Returns all decoded results from all pipelines, in the order the
/// pipelines were added. Pipelines that produced no complete frames
/// (or whose decoder returned `None`) are simply absent from the output.
pub fn feed(&mut self, data: &[u8]) -> Vec<PipelineResult> {
let mut results = Vec::new();
for p in &mut self.pipelines {
let name = p.name.clone();
for decoded in p.feed_inner(data) {
results.push(PipelineResult {
pipeline_name: name.clone(),
data: decoded,
});
}
}
if !results.is_empty() {
debug!(
count = results.len(),
bytes = data.len(),
"pipeline produced results"
);
}
results
}
/// Flush every pipeline.
///
/// Call after the transport disconnects to drain any incomplete
/// frames still buffered inside the framers.
pub fn flush(&mut self) -> Vec<PipelineResult> {
let mut results = Vec::new();
for p in &mut self.pipelines {
let name = p.name.clone();
for decoded in p.flush_inner() {
results.push(PipelineResult {
pipeline_name: name.clone(),
data: decoded,
});
}
}
results
}
/// Reset every pipeline to a clean state (discards all buffered data).
pub fn reset(&mut self) {
for p in &mut self.pipelines {
p.reset_inner();
}
}
/// Return `(name, pending_bytes)` for each pipeline that has buffered data.
pub fn pending_bytes(&self) -> Vec<(&str, usize)> {
self.pipelines
.iter()
.map(|p| (p.name.as_str(), p.framer.pending_len()))
.filter(|(_, len)| *len > 0)
.collect()
}
}
impl Default for MultiPipeline {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
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};
#[test]
fn multi_text_and_hex_same_stream() {
let mut mp = MultiPipeline::new();
mp.add(Pipeline::new(
"text",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(TextDecoder::new(TextEncoding::Utf8)),
));
mp.add(Pipeline::new(
"hex",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(HexDecoder::new(HexConfig::default())),
));
// Both pipelines use LineFramer → both see "hello\n" and "42\n"
let results = mp.feed(b"hello\n42\n");
assert_eq!(results.len(), 4);
assert_eq!(results[0].pipeline_name, "text");
assert!(matches!(&results[0].data, DecodedData::Text(s) if s == "hello"));
assert_eq!(results[1].pipeline_name, "text");
assert!(matches!(&results[1].data, DecodedData::Text(s) if s == "42"));
assert_eq!(results[2].pipeline_name, "hex");
assert!(matches!(&results[2].data, DecodedData::Hex(s) if s == "68 65 6c 6c 6f"));
assert_eq!(results[3].pipeline_name, "hex");
assert!(matches!(&results[3].data, DecodedData::Hex(s) if s == "34 32"));
}
#[test]
fn multi_text_success_hex_silent_on_binary_trash() {
let mut mp = MultiPipeline::new();
mp.add(Pipeline::new(
"text",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(TextDecoder::new(TextEncoding::Utf8)),
));
mp.add(Pipeline::new(
"hex",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(HexDecoder::new(HexConfig::default())),
));
let results = mp.feed(b"valid\n\xff\xfe\xfd\n");
// Text pipeline: "valid" ok, binary line fails UTF-8 → only 1 result
// Hex pipeline: both lines decode successfully → 2 results
assert_eq!(results.len(), 3);
assert_eq!(results[0].pipeline_name, "text");
assert!(matches!(&results[0].data, DecodedData::Text(s) if s == "valid"));
assert_eq!(results[1].pipeline_name, "hex");
assert!(matches!(&results[1].data, DecodedData::Hex(_)));
assert_eq!(results[2].pipeline_name, "hex");
assert!(matches!(&results[2].data, DecodedData::Hex(_)));
}
#[test]
fn multi_different_framers_per_pipeline() {
let mut mp = MultiPipeline::new();
// Text uses LineFramer, hex uses FixedLengthFramer(4)
mp.add(Pipeline::new(
"text",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(TextDecoder::new(TextEncoding::Utf8)),
));
mp.add(Pipeline::new(
"hex",
Box::new(FixedLengthFramer::new(4)),
Box::new(HexDecoder::new(HexConfig::default())),
));
let results = mp.feed(b"abc\n1234");
// text: sees "abc\n" → produces "abc"
// hex: sees 8 bytes → produces 2 fixed frames: "abc\n" and "1234"
assert_eq!(results.len(), 3);
assert_eq!(results[0].pipeline_name, "text");
assert!(matches!(&results[0].data, DecodedData::Text(s) if s == "abc"));
let hex_results: Vec<_> = results
.iter()
.filter(|r| r.pipeline_name == "hex")
.collect();
assert_eq!(hex_results.len(), 2);
}
#[test]
fn multi_flush_drains_all() {
let mut mp = MultiPipeline::new();
mp.add(Pipeline::new(
"text",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(TextDecoder::new(TextEncoding::Utf8)),
));
mp.add(Pipeline::new(
"hex",
Box::new(FixedLengthFramer::new(4)),
Box::new(HexDecoder::new(HexConfig::default())),
));
// "partial" = 7 bytes. Line: no \n → buffers. Fixed(4): 1 frame "part", 3 left.
mp.feed(b"partial");
let flushed = mp.flush();
// Line flush → "partial" as text
// Fixed flush → "ial" as hex ("69 61 6c")
assert_eq!(flushed.len(), 2);
assert_eq!(flushed[0].pipeline_name, "text");
assert!(matches!(&flushed[0].data, DecodedData::Text(s) if s == "partial"));
assert_eq!(flushed[1].pipeline_name, "hex");
assert!(matches!(&flushed[1].data, DecodedData::Hex(s) if s == "69 61 6c"));
}
#[test]
fn multi_reset_clears_all_state() {
let mut mp = MultiPipeline::new();
mp.add(Pipeline::new(
"text",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(TextDecoder::new(TextEncoding::Utf8)),
));
mp.add(Pipeline::new(
"hex",
Box::new(FixedLengthFramer::new(4)),
Box::new(HexDecoder::new(HexConfig::default())),
));
mp.feed(b"unused data here that never completes");
assert_eq!(mp.pending_bytes().len(), 2);
mp.reset();
assert!(mp.pending_bytes().is_empty());
// After reset, pipelines work normally
let results = mp.feed(b"ok\nabcd");
assert!(!results.is_empty());
}
#[test]
fn multi_pending_bytes_reporting() {
let mut mp = MultiPipeline::new();
mp.add(Pipeline::new(
"t",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(TextDecoder::new(TextEncoding::Utf8)),
));
mp.add(Pipeline::new(
"h",
Box::new(FixedLengthFramer::new(100)),
Box::new(HexDecoder::new(HexConfig::default())),
));
mp.feed(b"hello world");
let pending: Vec<_> = mp.pending_bytes();
assert_eq!(pending.len(), 2);
// Both have the same 11 bytes buffered
assert_eq!(pending[0], ("t", 11));
assert_eq!(pending[1], ("h", 11));
}
#[test]
fn multi_empty_feed_no_results() {
let mut mp = MultiPipeline::new();
mp.add(Pipeline::new(
"text",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(TextDecoder::new(TextEncoding::Utf8)),
));
let results = mp.feed(b"");
assert!(results.is_empty());
}
#[test]
fn multi_no_pipelines() {
let mut mp = MultiPipeline::new();
let results = mp.feed(b"data");
assert!(results.is_empty());
assert_eq!(mp.pipeline_count(), 0);
}
#[test]
fn multi_pipeline_count() {
let mut mp = MultiPipeline::new();
assert_eq!(mp.pipeline_count(), 0);
mp.add(Pipeline::new(
"a",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(TextDecoder::new(TextEncoding::Utf8)),
));
assert_eq!(mp.pipeline_count(), 1);
}
#[test]
fn multi_text_and_plot_mixed_stream() {
let mut mp = MultiPipeline::new();
mp.add(Pipeline::new(
"text",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(TextDecoder::new(TextEncoding::Utf8)),
));
mp.add(Pipeline::new(
"plot",
Box::new(FixedLengthFramer::new(8)), // 2 × f32 LE per frame
Box::new(PlotDecoder::new(PlotConfig {
sample_type: SampleType::F32,
endian: Endian::Little,
channels: 1,
format: PlotFormat::Interleaved,
})),
));
// 10 bytes of text + 8 bytes of f32 samples = 18 total.
// FixedLengthFramer(8) extracts 2 frames. The first frame is ASCII
// bytes which happen to decode as valid (but meaningless) f32 values
// — PlotDecoder can't reject them. This is by design: garbage-in,
// garbage-out; the UI layer decides what to display.
let mut data = b"status ok\n".to_vec();
data.extend_from_slice(&1.0f32.to_le_bytes());
data.extend_from_slice(&2.0f32.to_le_bytes());
let results = mp.feed(&data);
// text: 1 line, plot: 2 fixed frames (first = garbage f32, second = real)
assert_eq!(results.len(), 3);
assert_eq!(results[0].pipeline_name, "text");
assert!(matches!(&results[0].data, DecodedData::Text(s) if s == "status ok"));
}
#[test]
fn multi_length_prefixed_and_line_stacked() {
let mut mp = MultiPipeline::new();
mp.add(Pipeline::new(
"line",
Box::new(LineFramer::new(LineConfig::default())),
Box::new(TextDecoder::new(TextEncoding::Utf8)),
));
// Second pipeline uses a different framer on the same bytes.
// The length framer sees "he" (from "hello\n") as a bogus length header
// and silently produces no frames — that's expected.
mp.add(Pipeline::new(
"len",
Box::new(LengthPrefixedFramer::new(LengthConfig::default())),
Box::new(HexDecoder::new(HexConfig::default())),
));
let mut data = b"hello\n".to_vec();
data.extend_from_slice(&3u16.to_be_bytes());
data.extend_from_slice(b"abc");
let results = mp.feed(&data);
// Only the line pipeline produces output; length pipeline silently
// ignores bytes that don't form valid length-prefixed frames.
assert_eq!(results.len(), 1);
assert!(matches!(&results[0].data, DecodedData::Text(s) if s == "hello"));
}
}