Add mixed text/plot transport and improve plot UI

This commit is contained in:
2026-06-09 00:51:34 +08:00
parent 25433038d5
commit d5b6bb52c8
18 changed files with 2101 additions and 192 deletions

View File

@@ -0,0 +1,215 @@
use super::Framer;
use crate::frame::cobs::cobs_decode;
use crate::protocol::mixed::{
MIXED_FRAME_KIND_PLOT, MIXED_FRAME_KIND_TEXT, MIXED_PLOT_ESCAPE, MIXED_PLOT_MARKER,
};
#[derive(Debug, Clone)]
pub struct MixedTextPlotConfig {
pub strip_cr: bool,
pub max_line_len: usize,
pub max_plot_frame: usize,
}
impl Default for MixedTextPlotConfig {
fn default() -> Self {
Self {
strip_cr: true,
max_line_len: 1024 * 1024,
max_plot_frame: 1024 * 1024,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Text,
Escape,
Plot,
}
#[derive(Debug, Clone)]
pub struct MixedTextPlotFramer {
state: State,
text_buf: Vec<u8>,
plot_buf: Vec<u8>,
plot_overflow: bool,
config: MixedTextPlotConfig,
}
impl MixedTextPlotFramer {
pub fn new(config: MixedTextPlotConfig) -> Self {
Self {
state: State::Text,
text_buf: Vec::new(),
plot_buf: Vec::new(),
plot_overflow: false,
config,
}
}
fn push_text_byte(&mut self, byte: u8, frames: &mut Vec<Vec<u8>>) {
if byte == b'\n' {
let mut line = std::mem::take(&mut self.text_buf);
if self.config.strip_cr && line.last() == Some(&b'\r') {
line.pop();
}
if line.len() <= self.config.max_line_len {
frames.push(tag_text_frame(line));
}
return;
}
if self.text_buf.len() < self.config.max_line_len.saturating_add(1) {
self.text_buf.push(byte);
}
}
fn finish_plot_frame(&mut self, frames: &mut Vec<Vec<u8>>) {
if self.plot_overflow || self.plot_buf.is_empty() {
self.plot_buf.clear();
self.plot_overflow = false;
return;
}
if let Some(decoded) = cobs_decode(&self.plot_buf) {
if decoded.len() <= self.config.max_plot_frame {
frames.push(tag_plot_frame(decoded));
}
}
self.plot_buf.clear();
self.plot_overflow = false;
}
}
impl Framer for MixedTextPlotFramer {
fn feed(&mut self, data: &[u8]) -> Vec<Vec<u8>> {
let mut frames = Vec::new();
for &byte in data {
match self.state {
State::Text => {
if byte == MIXED_PLOT_ESCAPE {
self.state = State::Escape;
} else {
self.push_text_byte(byte, &mut frames);
}
}
State::Escape => {
self.state = State::Text;
match byte {
MIXED_PLOT_ESCAPE => self.push_text_byte(MIXED_PLOT_ESCAPE, &mut frames),
MIXED_PLOT_MARKER => {
self.plot_buf.clear();
self.plot_overflow = false;
self.state = State::Plot;
}
_ => {
self.push_text_byte(MIXED_PLOT_ESCAPE, &mut frames);
self.push_text_byte(byte, &mut frames);
}
}
}
State::Plot => {
if byte == 0x00 {
self.finish_plot_frame(&mut frames);
self.state = State::Text;
} else if !self.plot_overflow {
let max_encoded_len = self
.config
.max_plot_frame
.saturating_add(self.config.max_plot_frame / 254)
.saturating_add(8);
if self.plot_buf.len() < max_encoded_len {
self.plot_buf.push(byte);
} else {
self.plot_overflow = true;
}
}
}
}
}
frames
}
fn flush(&mut self) -> Option<Vec<u8>> {
if matches!(self.state, State::Escape) {
self.text_buf.push(MIXED_PLOT_ESCAPE);
}
self.state = State::Text;
self.plot_buf.clear();
self.plot_overflow = false;
if self.text_buf.is_empty() {
None
} else {
Some(tag_text_frame(std::mem::take(&mut self.text_buf)))
}
}
fn reset(&mut self) {
self.state = State::Text;
self.text_buf.clear();
self.plot_buf.clear();
self.plot_overflow = false;
}
fn pending_len(&self) -> usize {
self.text_buf.len() + self.plot_buf.len()
}
}
fn tag_text_frame(mut payload: Vec<u8>) -> Vec<u8> {
let mut frame = Vec::with_capacity(payload.len() + 1);
frame.push(MIXED_FRAME_KIND_TEXT);
frame.append(&mut payload);
frame
}
fn tag_plot_frame(mut payload: Vec<u8>) -> Vec<u8> {
let mut frame = Vec::with_capacity(payload.len() + 1);
frame.push(MIXED_FRAME_KIND_PLOT);
frame.append(&mut payload);
frame
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frame::cobs::cobs_encode;
#[test]
fn mixed_text_lines_pass_through() {
let mut framer = MixedTextPlotFramer::new(MixedTextPlotConfig::default());
let frames = framer.feed(b"hello\nworld\n");
assert_eq!(frames, vec![b"\0hello".to_vec(), b"\0world".to_vec()]);
}
#[test]
fn mixed_escaped_rs_stays_in_text() {
let mut framer = MixedTextPlotFramer::new(MixedTextPlotConfig::default());
let frames = framer.feed(&[b'o', b'k', MIXED_PLOT_ESCAPE, MIXED_PLOT_ESCAPE, b'\n']);
assert_eq!(frames, vec![vec![0x00, b'o', b'k', MIXED_PLOT_ESCAPE]]);
}
#[test]
fn mixed_extracts_plot_packet() {
let mut framer = MixedTextPlotFramer::new(MixedTextPlotConfig::default());
let payload = [b'X', b'P', 1, 0, 8, 0, 1, 1, 0, 4, 0, 0, 0, 0, 0x80, 0x3f];
let encoded = cobs_encode(&payload);
let mut stream = vec![MIXED_PLOT_ESCAPE, MIXED_PLOT_MARKER];
stream.extend_from_slice(&encoded);
stream.push(0x00);
let frames = framer.feed(&stream);
assert_eq!(frames, vec![[vec![0x01], payload.to_vec()].concat()]);
}
#[test]
fn mixed_ignores_truncated_plot_on_flush() {
let mut framer = MixedTextPlotFramer::new(MixedTextPlotConfig::default());
let _ = framer.feed(&[MIXED_PLOT_ESCAPE, MIXED_PLOT_MARKER, 0x03, b'a']);
assert_eq!(framer.flush(), None);
}
}