From fd680858f654bbce7df51dfb77ce490cec23ae8e Mon Sep 17 00:00:00 2001 From: FallenSigh Date: Fri, 12 Jun 2026 21:56:20 +0800 Subject: [PATCH] Rewrite README in Chinese and English, add drone examples Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .gitignore | 3 + README.md | 640 +++++++++++++++++++++------------------- README_en.md | 378 ++++++++++++++++++++++++ examples/drone_plot.lua | 16 + examples/drone_text.lua | 33 +++ tools/test_drone.py | 102 +++++++ 6 files changed, 866 insertions(+), 306 deletions(-) create mode 100644 README_en.md create mode 100644 examples/drone_plot.lua create mode 100644 examples/drone_text.lua create mode 100644 tools/test_drone.py diff --git a/.gitignore b/.gitignore index 4ac97ad..74d8896 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ # Compiled binaries in tools/ tools/test_plot_c tools/test_text_c + +# Python +__pycache__/ diff --git a/README.md b/README.md index b26bc6a..537cbc3 100644 --- a/README.md +++ b/README.md @@ -1,218 +1,318 @@ # xserial -**跨平台串口 / TCP / UDP 数据观测工具** — 可配置分帧、协议解码、多会话管理,支持文本、十六进制和实时波形绘图。 +**跨平台串口 / TCP / UDP 数据观测工具** — 可配置分帧、协议解码、多会话管理、实时波形绘图,单二进制零依赖。 -基于 Rust workspace,GUI 使用 [egui](https://github.com/emilk/egui) + [egui_plot](https://github.com/emilk/egui/tree/master/crates/egui_plot),支持 Lua 脚本扩展分帧与解码逻辑。 +基于 Rust + [egui](https://github.com/emilk/egui) 构建,支持 Lua 脚本扩展。 + +[English](README_en.md) + +--- + +## 目录 + +- [功能特性](#功能特性) +- [快速开始](#快速开始) +- [完整示例:无人机遥测解析](#完整示例无人机遥测解析) +- [Lua 脚本开发指南](#lua-脚本开发指南) +- [架构](#架构) +- [配置与持久化](#配置与持久化) +- [开发工具](#开发工具) +- [技术栈](#技术栈) +- [License](#license) + +--- + +## 功能特性 + +### 传输层 + +| 类型 | 说明 | 配置项 | +|------|------|--------| +| **Serial** | 串口通信 | 端口名、波特率 (300–12M)、数据位 (5/6/7/8)、校验位 (None/Odd/Even)、停止位 (1/2)、流控 (None/Software/Hardware)、DTR/RTS 控制 | +| **TCP** | TCP 客户端 | 目标地址 `host:port` | +| **UDP** | UDP 通信 | 绑定地址 `host:port`,可选远端地址 | + +### 分帧器(Framer) + +将原始字节流切分为独立帧。每个 Session 可配置**多条独立管线**,同一字节流并行送入多个 framer→decoder 链路。 + +| 分帧器 | 说明 | 配置参数 | +|--------|------|----------| +| **Line** | 按 `\n` 分割文本行 | `strip_cr`(去除 `\r`)、`max_line_len`(最大行长度) | +| **Fixed** | 固定字节数为一帧 | `frame_len`(帧长度) | +| **Length** | 长度前缀协议 | `len_bytes` (1/2/4/8)、`endian` (大小端)、`length_includes_self`、`max_payload` | +| **COBS** | [Consistent Overhead Byte Stuffing](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) 编码 | `max_frame`(最大帧长) | +| **MixedTextPlot** | 单连接混合文本行 + COBS 编码的 plot 帧 | `strip_cr`、`max_line_len`、`max_plot_frame` | +| **Lua** | 用户自定义分帧脚本 | `script_path`(Lua 脚本路径) | + +### 解码器(Decoder) + +将帧数据解析为可展示的内容。 + +| 解码器 | 输出类型 | 配置参数 | +|--------|----------|----------| +| **Text** | 文本 | `encoding`(UTF-8 / Latin1 / ASCII) | +| **Hex** | 十六进制 | `uppercase`、`separator`、`bytes_per_group`、`endian` | +| **Plot** | 波形数据 | `sample_type` (i8–f64)、`endian`、`channels` (1–64)、`format` (Interleaved / Block / XY) | +| **MixedTextPlot** | 混合文本 + 波形 | `encoding` | +| **Lua** | 自定义 | `script_path`(Lua 脚本路径) | + +**Plot 采样格式:** + +| 格式 | 说明 | 字节排列 | +|------|------|----------| +| **Interleaved** | 多通道交叉排列 | `[ch0_s0, ch1_s0, ch2_s0, ch0_s1, ch1_s1, …]` | +| **Block** | 按通道分块排列 | `[ch0_s0, ch0_s1, …, ch1_s0, ch1_s1, …]` | +| **XY** | 2 通道交替 x/y | `[x0, y0, x1, y1, …]` | + +支持的采样类型:`i8`、`u8`、`i16`、`u16`、`i32`、`u32`、`i64`、`u64`、`f32`、`f64` + +### 视图 + +- **文本视图** — 带时间戳、方向标记 (`[IN]`/`[OUT]`)、管线标签的格式化文本流。支持搜索(`Ctrl+F`)、大小写匹配、匹配计数和高亮。 +- **十六进制视图** — hex dump 与 ASCII 侧栏并排展示,可按分组和大小端解析多字节数值。 +- **波形视图** — 基于 [egui_plot](https://github.com/emilk/egui/tree/master/crates/egui_plot) 的实时波形。支持自动缩放、框选缩放、坐标轴锁定、跟随最新数据、浮动窗口、通道图例。 + +### 会话管理 + +- **多会话并发** — 标签页切换,每个 Session 独立配置 +- **连接控制** — Connect / Disconnect / Reconnect,支持断线自动重连 +- **运行时重配置** — 修改分帧器/解码器参数无需断开连接 +- **数据发送** — Text 模式(UTF-8,可选换行符)和 Hex 模式(十六进制字节)。支持 `None` / `LF` / `CR` / `CRLF` 四种行尾 +- **环形缓冲** — 默认 10,000 条历史记录,可配置 100–1,000,000 +- **日志到文件** — 每行数据实时写入,1KB 缓冲 + 后台线程,不阻塞 UI +- **搜索** — `Ctrl+F` 呼出搜索栏,大小写敏感切换,F3/Shift+F3 前后跳转 + +### Lua 脚本扩展 + +内置 LuaJIT 运行时: + +- **自定义分帧器** — 实现 `feed(bytes)`、`flush()`、`reset()`、`pending_len()` 四个函数 +- **自定义解码器** — 实现 `decode(frame)` 函数,返回 text/hex/plot/binary 四种类型 +- **会话 API** — `xserial.open()` 创建 Session、`session:on_data()` 事件回调、`session:send()` 发送数据 +- **工具函数** — `xserial.list_ports()`、`xserial.sleep(ms)`、`xserial.poll(limit)`、`xserial.log(msg)` + +### 快捷键 + +| 快捷键 | 操作 | +|--------|------| +| `Ctrl+N` | 新建会话 | +| `Ctrl+E` | 编辑当前会话 | +| `Ctrl+W` | 删除当前会话 | +| `Ctrl+F5` | 切换连接 | +| `Ctrl+T` / `Ctrl+H` / `Ctrl+P` | 切换到文本 / 十六进制 / 波形视图 | +| `Ctrl+Tab` / `Ctrl+Shift+Tab` | 下一个 / 上一个标签页 | +| `Ctrl+L` | 清空输出 | +| `Ctrl+F` | 搜索 | +| `F3` / `Shift+F3` | 上一个 / 下一个匹配 | +| `Ctrl+,` | UI 设置 | +| `Esc` | 关闭浮层 | + +--- + +## 快速开始 + +### 前置条件 + +| 平台 | 依赖 | +|------|------| +| **Linux** | `libudev-dev`(`apt install libudev-dev`) | +| **macOS** | 无需额外依赖 | +| **Windows** | 无需额外依赖 | +| **所有平台** | Rust ≥ 1.85、C 编译器(GCC / Clang / MSVC) | + +### 构建与运行 + +```bash +# 克隆项目 +git clone https://github.com/your-org/xserial.git +cd xserial + +# 编译运行 +cargo run -p xserial-gui + +# 运行测试 +cargo test --workspace # ~308 个测试 +cargo clippy --workspace --all-targets -- -D warnings +``` + +### 发布构建 + +```bash +cargo build -p xserial-gui --release +# 二进制位于 target/release/xserial-gui (Linux/macOS) +# 或 target/release/xserial-gui.exe (Windows) +``` + +--- + +## 完整示例:无人机遥测解析 + +以 Betaflight/INAV 飞控遥测为例,数据格式为: + +``` +AHRS q:1.0000,0.0998,0.0499,0.0200|YPR:8.87,4.44,2.96|Gyro:14.78,8.87,5.92|RC:1559,1544,1500,1519|M:1612,1588,1603,1597|L:0 F:1 C:0 +``` + +### 步骤 1:启动测试数据源 + +```bash +python tools/test_drone.py --rate 10 --port 8091 +``` + +输出: + +``` +Listening on 127.0.0.1:8091, waiting for connections... +``` + +### 步骤 2:配置 Session + +在 xserial-gui 中创建 Session: + +| 配置项 | 值 | +|--------|-----| +| Transport | `TCP 127.0.0.1:8091` | +| Pipeline 1 | Framer: `Line` → Decoder: `Lua` → 选择 `examples/drone_text.lua` | +| Pipeline 2 | Framer: `Line` → Decoder: `Lua` → 选择 `examples/drone_plot.lua` | + +### 步骤 3:查看结果 + +- **文本视图** — 显示格式化的传感器数据 +- **波形视图** — 显示 Gyro 三轴实时曲线 (gz/gy/gx) + +`examples/drone_plot.lua` 的核心逻辑: + +```lua +return { + decode = function(frame) + local gz, gy, gx = frame:match("Gyro:([%d%.%-]+),([%d%.%-]+),([%d%.%-]+)") + return { + kind = "plot", + channels = { { tonumber(gz) }, { tonumber(gy) }, { tonumber(gx) } }, + sample_type = "F64", + format = "Block", + } + end, +} +``` + +--- + +## Lua 脚本开发指南 + +### 分帧器 API + +分帧器将原始字节流切分为帧,Lua 脚本必须返回包含以下函数的 table: + +```lua +return { + -- 输入新到达的字节,返回帧数组(Lua strings) + feed = function(bytes) + -- bytes: Lua string(原始字节) + -- 返回: { frame1, frame2, ... } 或 nil + end, + + -- 刷新缓冲区中残留的数据 + flush = function() + -- 返回: 最后一帧(Lua string)或 nil + end, + + -- 重置内部状态 + reset = function() + end, + + -- 返回缓冲区中待处理的字节数 + pending_len = function() + -- 返回: number + end, +} +``` + +参考实现:`tests/lua_line_framer.lua`(按 `\n` 分割的行分帧器) + +### 解码器 API + +解码器将帧解析为结构化数据,Lua 脚本必须返回包含 `decode` 函数的 table: + +```lua +return { + decode = function(frame) + -- frame: Lua string(来自分帧器的一帧) + -- 返回 nil → 跳过此帧 + -- 返回 string → 自动视为 Text + -- 返回 table → 必须包含 kind 字段 + end, +} +``` + +**返回值格式:** + +| `kind` | 必需字段 | 可选字段 | 用途 | +|--------|----------|----------|------| +| `"text"` | `data: string` | — | 文本视图 | +| `"hex"` | `data: string` | — | 十六进制视图 | +| `"binary"` | `data: string` | — | 原始二进制 | +| `"plot"` | `channels: {{number,…},…}` | `sample_type`、`format` | 波形视图 | + +**Plot 返回值示例:** + +```lua +return { + kind = "plot", + channels = { + { 1.0, 2.0, 3.0 }, -- channel 0 + { 4.0, 5.0, 6.0 }, -- channel 1 + }, + sample_type = "F64", -- 默认 F64,可选 I8/U8/I16/U16/I32/U32/I64/U64/F32 + format = "Block", -- 默认 Interleaved,可选 Block/XY +} +``` + +更多示例见 `examples/` 目录。 --- ## 架构 ``` -┌─────────────────────────────────────────────────────┐ -│ xserial-gui (egui) xserial-tui (ratatui) │ -│ 面板: sidebar, console, 占位实现 │ -│ hex_view, plot_view, │ -│ config │ -├─────────────────────────────────────────────────────┤ -│ xserial-client │ -│ SessionManager · Session · Config · History │ -│ Lua Runtime (mlua/LuaJIT) │ -│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ -│ │ LuaFramer│ │LuaDecoder│ │ Lua Session API │ │ -│ └──────────┘ └──────────┘ └──────────────────┘ │ -├─────────────────────────────────────────────────────┤ -│ xserial-core │ -│ Transport ──▶ Frame ──▶ Protocol ──▶ Pipeline │ -│ (Serial, (Line, (Text, Hex, (MultiPipeline) │ -│ TCP, UDP) Fixed, Plot, │ -│ Length, MixedTextPlot) │ -│ Cobs, │ -│ MixedTextPlot) │ -└─────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────┐ +│ xserial-gui (egui) xserial-tui (ratatui) │ +├──────────────────────────────────────────────────┤ +│ xserial-client │ +│ SessionManager · Session · Config · History │ +│ Lua Runtime (mlua / LuaJIT) │ +├──────────────────────────────────────────────────┤ +│ xserial-core │ +│ Transport ──▶ Frame ──▶ Protocol ──▶ Pipeline │ +└──────────────────────────────────────────────────┘ ``` | Crate | 类型 | 职责 | |-------|------|------| -| `xserial-core` | library | 传输层、分帧器、协议解码器、Pipeline | -| `xserial-client` | library | 会话管理、配置持久化、事件分发、历史缓冲、Lua 运行时 | -| `xserial-gui` | binary | egui 桌面应用(主要入口) | -| `xserial-tui` | binary | ratatui 终端应用(占位,未达功能对等) | +| `xserial-core` | library | 传输层(Serial/TCP/UDP)、分帧器(Line/Fixed/Length/COBS/Mixed/Lua)、协议解码器(Text/Hex/Plot)、MultiPipeline | +| `xserial-client` | library | Session 生命周期管理、SessionManager、事件广播(tokio broadcast)、RingBuffer 历史、Lua 运行时及会话 API | +| `xserial-gui` | binary | egui 桌面应用,包含 sidebar/config/console/text/hex/plot 面板、键盘快捷键、字体管理、性能分析 | +| `xserial-tui` | binary | ratatui 终端应用(功能未对齐 GUI,仍在开发中) | -依赖方向: `core ← client ← {gui, tui}` +**依赖方向:** `core ← client ← {gui, tui}` + +**数据流:** + +``` +[Transport] → read bytes → [MultiPipeline] + → Pipeline 1: Framer → Decoder → DecodedEntry → broadcast → GUI buffers + → Pipeline 2: Framer → Decoder → DecodedEntry → broadcast → GUI buffers + → ... +``` + +每条管线独立分帧、解码,互不干扰。只有成功解码的管线产生输出。 --- -## 构建 +## 配置与持久化 -**前置条件**: - -- **Rust ≥ 1.85**(workspace 使用 `edition = "2024"`) -- **Linux**: `libudev-dev`(`serialport` 依赖) -- **所有平台**: C 编译器(`mlua` 使用 `vendored` 特性从源码编译 LuaJIT) - -```bash -# 编译检查 -cargo check --workspace - -# 运行测试(~266 个) -cargo test --workspace - -# 格式化 + Clippy -cargo fmt --all -cargo clippy --workspace --all-targets -- -D warnings -``` - ---- - -## 运行 - -### GUI(主要入口) - -```bash -cargo run -p xserial-gui -``` - -### TUI(占位实现) - -```bash -cargo run -p xserial-tui -``` - ---- - -## 功能 - -### 传输层 - -| 类型 | 配置项 | -|------|--------| -| **Serial** | 端口名、波特率、数据位 (5/6/7/8)、校验位 (None/Odd/Even)、停止位 (1/2)、流控 (None/Software/Hardware)、DTR、RTS | -| **TCP** | 目标地址 `host:port` | -| **UDP** | 绑定地址 `host:port`,可选远端地址 | - -DTR/RTS 控制仅在 Serial 连接时可用。 - -### 分帧器 - -| 分帧器 | 说明 | 配置 | -|--------|------|------| -| **Line** | 按换行符 `\n` 分割,可选去除 `\r` | `strip_cr`, `max_line_len` | -| **Fixed** | 固定字节数为一帧 | `frame_len` | -| **Length** | 长度前缀协议帧 | `len_bytes` (1/2/4/8), `endian`, `length_includes_self`, `max_payload` | -| **Cobs** | [COBS](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) 编码,`0x00` 分隔 | `max_frame` | -| **MixedTextPlot** | 单连接混合文本行 + COBS plot 帧 | `strip_cr`, `max_line_len`, `max_plot_frame` | -| **Lua** | 用户自定义 Lua 脚本 | `script_path` | - -### 协议解码器 - -| 解码器 | 输出 | 配置 | -|--------|------|------| -| **Text** | UTF-8 / Latin1 文本 | `encoding` | -| **Hex** | 十六进制字符串 | `uppercase`, `separator`, `bytes_per_group`, `endian` | -| **Plot** | 数值波形数据 | `sample_type` (i8–f64), `endian`, `channels`, `format` (Interleaved / Block / XY) | -| **MixedTextPlot** | 混合流(文本 + 波形) | `encoding` | -| **Lua** | 用户自定义解码 | `script_path` | - -#### Plot 采样格式 - -| 格式 | 说明 | -|------|------| -| **Interleaved** | 多通道交叉排列: `[ch0_s0, ch1_s0, ch0_s1, ch1_s1, …]` | -| **Block** | 按通道分块: `[ch0_s0, ch0_s1, …, ch1_s0, ch1_s1, …]` | -| **XY** | 2 通道,交替 x/y: `[x0, y0, x1, y1, …]` | - -支持的采样类型: `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `f32`, `f64` - -### Pipeline - -每个 Session 可配置多个 Pipeline。同一个字节流被送入所有 Pipeline,每个 Pipeline 独立分帧、解码,互不干扰。例如: - -- Pipeline "text": LineFramer → TextDecoder → 文本视图 -- Pipeline "hex": FixedFramer(16) → HexDecoder → 十六进制视图 -- Pipeline "plot": FixedFramer(256) → PlotDecoder → 波形视图 - -当数据到达时,只有成功解码的 Pipeline 产生输出。 - -### 会话管理 - -- **多会话**: 同时运行多个独立 session -- **连接控制**: Connect / Disconnect / Reconnect -- **自动重连**: 断线后每秒自动重试 -- **动态重配置**: 运行时修改分帧器/解码器配置 -- **数据发送**: 通过 Console 面板发送原始字节到连接 -- **历史缓冲**: 环形缓冲区(默认 10,000 条),可配置上限 -- **状态持久化**: Session 配置自动保存到 `gui-state.json` - -### Lua 脚本扩展 - -内置 LuaJIT 运行时,支持: - -- **自定义分帧器**: 实现 `feed(bytes)`, `flush()`, `reset()`, `pending_len()` 四个函数 -- **自定义解码器**: 实现 `decode(frame)` 函数,返回 `{kind="text"|"hex"|"binary"|"plot", data=…}` -- **会话 API**: `xserial.open(config)` 创建 session,支持 `on_data(callback)` 事件回调 -- **辅助函数**: `xserial.list_ports()`, `xserial.sleep(ms)`, `xserial.poll(limit)`, `xserial.log(msg)` - -示例 Lua Line Framer: - -```lua -local buffer = "" -return { - feed = function(bytes) - buffer = buffer .. bytes - local frames = {} - while true do - local i = buffer:find("\n", 1, true) - if not i then break end - frames[#frames + 1] = buffer:sub(1, i - 1) - buffer = buffer:sub(i + 1) - end - return frames - end, - flush = function() - if #buffer == 0 then return nil end - local frame = buffer; buffer = ""; return frame - end, - reset = function() buffer = "" end, - pending_len = function() return #buffer end, -} -``` - ---- - -## GUI - -### 面板 - -| 面板 | 功能 | -|------|------| -| **Sidebar** | Session 列表、创建/删除/编辑 session、连接切换 | -| **Config** | 传输参数、Pipeline 配置(分帧器 + 解码器类型及参数) | -| **Console** | 数据发送输入框,发送按钮 | -| **Text View** | 解码后的文本流,支持时间戳和方向标记 | -| **Hex View** | 十六进制字节展示,可配置分组和分隔符 | -| **Plot View** | 实时波形图,支持内嵌和浮动窗口两种模式 | - -### 快捷键 - -| 快捷键 | 操作 | -|--------|------| -| `Ctrl+Tab` | 下一个 Tab | -| `Ctrl+Shift+Tab` | 上一个 Tab | -| `Ctrl+T` | 切换到 Text 视图 | -| `Ctrl+H` | 切换到 Hex 视图 | -| `Ctrl+P` | 切换到 Plot 视图 | -| `Ctrl+N` | 新建 Session | -| `Ctrl+E` | 编辑当前 Session | -| `Ctrl+W` | 删除当前 Session | -| `Ctrl+F5` | 切换连接 | -| `Ctrl+L` | 清空输出 | -| `Ctrl+,` | UI 设置 | -| `Esc` | 关闭浮层 | - -### 状态持久化 - -GUI 状态(session 配置、活动 tab、显示选项)自动保存到: +GUI 状态自动保存,路径遵循各平台规范: | 平台 | 路径 | |------|------| @@ -220,147 +320,75 @@ GUI 状态(session 配置、活动 tab、显示选项)自动保存到: | macOS | `~/Library/Application Support/xserial/gui-state.json` | | Windows | `%APPDATA%\xserial\gui-state.json` | +持久化的内容包括:Session 配置(传输参数、管线设置)、日志开关及路径、活动标签页、显示选项。 + +日志文件默认保存在配置目录的 `logs/` 子目录下,文件命名格式为 `session_{id}_{timestamp}.log`。 + --- ## 开发工具 -### Plot 测试服务器 +### 测试数据生成器 ```bash -# 启动 TCP 波形数据源 +# Plot 波形测试数据 python tools/test_plot.py --wire-format mixed --host 127.0.0.1 --port 8091 - -# XY 格式,2 通道 python tools/test_plot.py --wire-format mixed --format xy --channels 2 - -# 固定长度 raw plot 帧 python tools/test_plot.py --wire-format raw --channels 2 --framelen 256 -``` -GUI 中配置: Transport `TCP 127.0.0.1:8091`, Framer `MixedTextPlot`, Decoder `MixedTextPlot` +# 无人机遥测测试数据 +python tools/test_drone.py --rate 10 --port 8092 +``` ### 性能分析 ```bash -# 开启性能日志 RUST_LOG=xserial_gui::perf=info XSERIAL_GUI_PROFILE=1 cargo run -p xserial-gui - -# 自定义统计间隔(默认 1000ms) XSERIAL_GUI_PROFILE_INTERVAL_MS=500 cargo run -p xserial-gui ``` -日志输出: frame 耗时、事件 drain 耗时、text/hex/plot 渲染耗时、plot 点数统计 +输出每帧的耗时、事件 drain 耗时、text/hex/plot 渲染耗时、plot 点数统计。 ### 日志 ```bash -RUST_LOG=info cargo run -p xserial-gui -RUST_LOG=xserial_gui=debug cargo run -p xserial-gui +RUST_LOG=info cargo run -p xserial-gui # 应用日志 +RUST_LOG=xserial_gui=debug cargo run -p xserial-gui # 详细日志 ``` -使用 `tracing-subscriber` + `RUST_LOG` 环境变量控制日志级别。 +使用 `tracing-subscriber` + `RUST_LOG` 环境变量控制。 -### 测试 - -```bash -# 全部测试 -cargo test --workspace - -# 特定 crate -cargo test -p xserial-core -cargo test -p xserial-client - -# 集成测试 -cargo test -p xserial-core --test pipeline -cargo test -p xserial-client --test session_lifecycle -cargo test -p xserial-client --test lua_tests - -# 带输出 -cargo test -p xserial-core -- --nocapture -``` - -所有测试自包含(TCP/UDP 绑定 `127.0.0.1:0`,串口测试使用虚拟端口名),无外部依赖。 - ---- - -## 目录结构 +### 项目结构 ``` crates/ - xserial-core/ # 传输、分帧、协议解码、Pipeline - src/ - transport/ # Serial, TCP, UDP - frame/ # Line, Fixed, Length, Cobs, MixedTextPlot - protocol/ # Text, Hex, Plot, MixedTextPlot - pipeline.rs # MultiPipeline - xserial-client/ # 会话管理、Lua 运行时 - src/ - session.rs # Session 事件循环 - manager.rs # SessionManager - config.rs # SessionConfig, FramerConfig, DecoderConfig - event.rs # DecodedEntry - history.rs # RingBuffer - cmd.rs # SessionCmd - lua/ # Lua 运行时、自定义 codec、会话 API - xserial-gui/ # egui 桌面应用 - src/ - main.rs - app.rs # XserialApp 主控制器 - panels/ # sidebar, config, console, hex_view, plot_view - shortcuts.rs # 键盘快捷键 - ui_fonts.rs # 字体加载 - perf.rs # 性能分析 - app_state.rs # 状态持久化 - buffers.rs # UI 缓冲管理 - xserial-tui/ # ratatui 终端应用 (占位) -tests/ # Lua 测试 fixture - lua_line_framer.lua - lua_text_decoder.lua -tools/ # 开发辅助工具 - test_plot.py # TCP 波形测试服务器 - test_plot_serial.c # C 串口 plot 测试客户端 - test_text_serial.c # C 串口文本测试客户端 - xs_mixed_plot.h # MixedTextPlot 线格式 C 参考头文件 + xserial-core/ # 传输、分帧、协议 + xserial-client/ # 会话管理、Lua 运行时 + xserial-gui/ # egui 桌面应用 + xserial-tui/ # ratatui 终端应用 +examples/ # Lua 脚本示例 + drone_plot.lua # 飞控遥测波形解码器 + drone_text.lua # 飞控遥测文本解码器 +tests/ # Lua 测试 fixture +tools/ # 开发辅助工具 + test_plot.py # 波形测试数据生成器 + test_drone.py # 飞控测试数据生成器 + test_plot_serial.c # C 串口波形测试客户端 + test_text_serial.c # C 串口文本测试客户端 + xs_mixed_plot.h # MixedTextPlot 协议参考头文件 ``` --- -## MixedTextPlot 协议 +## 技术栈 -单连接上混合传输文本行和波形数据的线格式。 +- **运行时**:tokio (multi-thread) +- **串口**:`tokio-serial` + `serialport` +- **GUI**:egui + egui_plot +- **Lua**:mlua 0.11, LuaJIT (vendored 编译), async/serde/send +- **事件分发**:`tokio::sync::broadcast`(多订阅者) +- **零 feature flags**、零 build script、零条件编译 -**文本帧**: 普通文本行,以 `\n` 分隔。 +## License -**Plot 帧**: -``` -0x1E 'P' | COBS(plot_packet) | 0x00 -``` - -**Plot Packet** (13 字节头 + payload): -``` -'X' 'P' (magic) -version:u8 (固定 1) -format:u8 (0=Interleaved, 1=Block, 2=XY) -sample_type:u8 (F32=8) -endian:u8 (0=Little) -channels:u8 -samples_per_channel:u16le -payload_len:u32le -payload:bytes (channels × samples_per_channel × sample_byte_size) -``` - -参考实现: `tools/xs_mixed_plot.h`(C header) - ---- - -## 技术细节 - -- **异步运行时**: tokio (multi-thread) -- **串口**: `tokio-serial` + `serialport` -- **分帧器特征**: `feed()`, `flush()`, `reset()`, `pending_len()` — 有状态字节流分帧 -- **解码器特征**: `name()`, `decode(&[u8]) -> Option` — 无状态帧解码 -- **Session 事件循环**: `tokio::select!` 多路复用命令、读取、重连定时器 -- **事件分发**: `tokio::sync::broadcast` — 多订阅者 -- **Lua**: `mlua` 0.11, LuaJIT (vendored 编译), 支持 async/serde/send -- **无 feature flags**: 零条件编译,无 build script -- **无 CI**: 本地自检 `cargo test --workspace` + `cargo clippy` +MIT diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000..186f7f5 --- /dev/null +++ b/README_en.md @@ -0,0 +1,378 @@ +# xserial + +**Cross-platform serial / TCP / UDP data inspection tool** with configurable framing, protocol decoding, multi-session management, and real-time waveform plotting — all in a single binary. + +Built with Rust + [egui](https://github.com/emilk/egui), scriptable with Lua. + +[中文文档](README.md) + +--- + +## Table of Contents + +- [Features](#features) +- [Quick Start](#quick-start) +- [Walkthrough: Drone Telemetry](#walkthrough-drone-telemetry) +- [Lua Scripting Guide](#lua-scripting-guide) +- [Architecture](#architecture) +- [Configuration & Persistence](#configuration--persistence) +- [Development Tools](#development-tools) +- [Tech Stack](#tech-stack) +- [License](#license) + +--- + +## Features + +### Transport + +| Type | Description | Options | +|------|-------------|---------| +| **Serial** | Serial port communication | Port name, baud rate (300–12M), data bits (5/6/7/8), parity (None/Odd/Even), stop bits (1/2), flow control (None/Software/Hardware), DTR/RTS control | +| **TCP** | TCP client | Target address `host:port` | +| **UDP** | UDP communication | Bind address `host:port`, optional remote address | + +### Framers + +Framers split raw byte streams into discrete frames. Each session supports **multiple independent pipelines** — the same byte stream feeds multiple framer→decoder chains in parallel. + +| Framer | Description | Parameters | +|--------|-------------|------------| +| **Line** | Split by `\n` | `strip_cr`, `max_line_len` | +| **Fixed** | Fixed-length frames | `frame_len` | +| **Length** | Length-prefixed protocol | `len_bytes` (1/2/4/8), `endian`, `length_includes_self`, `max_payload` | +| **COBS** | [Consistent Overhead Byte Stuffing](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) | `max_frame` | +| **MixedTextPlot** | Hybrid text lines + COBS-encoded plot frames on one stream | `strip_cr`, `max_line_len`, `max_plot_frame` | +| **Lua** | User-defined framer script | `script_path` | + +### Decoders + +Decoders parse framed data into displayable content. + +| Decoder | Output | Parameters | +|---------|--------|------------| +| **Text** | Text | `encoding` (UTF-8 / Latin1 / ASCII) | +| **Hex** | Hexadecimal | `uppercase`, `separator`, `bytes_per_group`, `endian` | +| **Plot** | Waveform | `sample_type` (i8–f64), `endian`, `channels` (1–64), `format` (Interleaved / Block / XY) | +| **MixedTextPlot** | Hybrid text + waveform | `encoding` | +| **Lua** | Custom | `script_path` | + +**Plot sample formats:** + +| Format | Description | Byte layout | +|--------|-------------|-------------| +| **Interleaved** | Channels interleaved | `[ch0_s0, ch1_s0, ch2_s0, ch0_s1, ch1_s1, …]` | +| **Block** | Channel-contiguous blocks | `[ch0_s0, ch0_s1, …, ch1_s0, ch1_s1, …]` | +| **XY** | 2-channel x/y pairs | `[x0, y0, x1, y1, …]` | + +Supported sample types: `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `f32`, `f64` + +### Views + +- **Text View** — formatted text with timestamps, direction markers (`[IN]`/`[OUT]`), and pipeline labels. Built-in search (`Ctrl+F`) with case-sensitive toggle, match counter, and highlighting. +- **Hex View** — hex dump alongside ASCII sidebar. Configurable byte grouping and endianness. +- **Plot View** — real-time waveforms via [egui_plot](https://github.com/emilk/egui/tree/master/crates/egui_plot). Auto-fit, box zoom, axis locking, follow-latest, detached window, channel legend. + +### Sessions + +- **Multi-session** — tabbed interface, each session independently configured +- **Connection control** — Connect / Disconnect / Reconnect with auto-reconnect on drop +- **Live reconfiguration** — change framer/decoder settings without restarting +- **Data sending** — Text mode (UTF-8, configurable line endings) and Hex mode (raw bytes). Four line ending options: None / LF / CR / CRLF +- **Ring buffer** — default 10,000 entries, configurable 100–1,000,000 +- **Log to file** — background writer thread with 1 KB buffer, non-blocking +- **Search** — `Ctrl+F` search bar, case-sensitive toggle, F3/Shift+F3 navigation + +### Lua Scripting + +Built-in LuaJIT runtime: + +- **Custom framers** — implement `feed(bytes)`, `flush()`, `reset()`, `pending_len()` +- **Custom decoders** — implement `decode(frame)`, return text/hex/plot/binary +- **Session API** — `xserial.open()` to create sessions, `session:on_data()` for event callbacks, `session:send()` to transmit data +- **Utilities** — `xserial.list_ports()`, `xserial.sleep(ms)`, `xserial.poll(limit)`, `xserial.log(msg)` + +### Keyboard Shortcuts + +| Shortcut | Action | +|----------|--------| +| `Ctrl+N` | New session | +| `Ctrl+E` | Edit current session | +| `Ctrl+W` | Delete current session | +| `Ctrl+F5` | Toggle connection | +| `Ctrl+T` / `Ctrl+H` / `Ctrl+P` | Switch to Text / Hex / Plot view | +| `Ctrl+Tab` / `Ctrl+Shift+Tab` | Next / Previous tab | +| `Ctrl+L` | Clear output | +| `Ctrl+F` | Search | +| `F3` / `Shift+F3` | Next / Previous match | +| `Ctrl+,` | UI settings | +| `Esc` | Close overlay | + +--- + +## Quick Start + +### Prerequisites + +| Platform | Dependencies | +|----------|-------------| +| **Linux** | `libudev-dev` (`apt install libudev-dev`) | +| **macOS** | None | +| **Windows** | None | +| **All** | Rust ≥ 1.85, C compiler (GCC / Clang / MSVC) | + +### Build & Run + +```bash +git clone https://github.com/your-org/xserial.git +cd xserial + +cargo run -p xserial-gui + +# Run tests +cargo test --workspace # ~308 tests +cargo clippy --workspace --all-targets -- -D warnings +``` + +### Release Build + +```bash +cargo build -p xserial-gui --release +# Binary: target/release/xserial-gui (Linux/macOS) +# target/release/xserial-gui.exe (Windows) +``` + +--- + +## Walkthrough: Drone Telemetry + +Parse Betaflight/INAV flight controller telemetry in the format: + +``` +AHRS q:1.0000,0.0998,0.0499,0.0200|YPR:8.87,4.44,2.96|Gyro:14.78,8.87,5.92|RC:1559,1544,1500,1519|M:1612,1588,1603,1597|L:0 F:1 C:0 +``` + +### Step 1: Start the data source + +```bash +python tools/test_drone.py --rate 10 --port 8091 +``` + +### Step 2: Configure the session + +In xserial-gui, create a session with two pipelines: + +| Setting | Value | +|---------|-------| +| Transport | `TCP 127.0.0.1:8091` | +| Pipeline 1 | Framer: `Line` → Decoder: `Lua` → `examples/drone_text.lua` | +| Pipeline 2 | Framer: `Line` → Decoder: `Lua` → `examples/drone_plot.lua` | + +### Step 3: View the results + +- **Text View** — formatted sensor readouts +- **Plot View** — real-time gyroscope curves (gz/gy/gx) + +The decoder in `examples/drone_plot.lua`: + +```lua +return { + decode = function(frame) + local gz, gy, gx = frame:match("Gyro:([%d%.%-]+),([%d%.%-]+),([%d%.%-]+)") + return { + kind = "plot", + channels = { { tonumber(gz) }, { tonumber(gy) }, { tonumber(gx) } }, + sample_type = "F64", + format = "Block", + } + end, +} +``` + +--- + +## Lua Scripting Guide + +### Framer API + +A Lua framer splits raw bytes into frames. The script must return a table with these functions: + +```lua +return { + feed = function(bytes) + -- bytes: Lua string (raw bytes) + -- Returns: { frame1, frame2, ... } or nil + end, + + flush = function() + -- Returns: last buffered frame (string) or nil + end, + + reset = function() + end, + + pending_len = function() + -- Returns: number of bytes pending in buffer + end, +} +``` + +Reference: `tests/lua_line_framer.lua` (line-based framer splitting on `\n`). + +### Decoder API + +A Lua decoder parses a frame into structured data. The script must return a table with a `decode` function: + +```lua +return { + decode = function(frame) + -- frame: Lua string (one frame from the framer) + -- Returns nil → skip this frame + -- Returns string → treated as Text + -- Returns table → must contain "kind" field + end, +} +``` + +**Return value formats:** + +| `kind` | Required | Optional | Purpose | +|--------|----------|----------|---------| +| `"text"` | `data: string` | — | Text view | +| `"hex"` | `data: string` | — | Hex view | +| `"binary"` | `data: string` | — | Raw bytes | +| `"plot"` | `channels: {{number,…},…}` | `sample_type`, `format` | Plot view | + +**Plot return value example:** + +```lua +return { + kind = "plot", + channels = { + { 1.0, 2.0, 3.0 }, -- channel 0 + { 4.0, 5.0, 6.0 }, -- channel 1 + }, + sample_type = "F64", -- default F64; also I8/U8/I16/U32/U64/F32 + format = "Block", -- default Interleaved; also Block/XY +} +``` + +More examples in `examples/`. + +--- + +## Architecture + +``` +┌──────────────────────────────────────────────────┐ +│ xserial-gui (egui) xserial-tui (ratatui) │ +├──────────────────────────────────────────────────┤ +│ xserial-client │ +│ SessionManager · Session · Config · History │ +│ Lua Runtime (mlua / LuaJIT) │ +├──────────────────────────────────────────────────┤ +│ xserial-core │ +│ Transport ──▶ Frame ──▶ Protocol ──▶ Pipeline │ +└──────────────────────────────────────────────────┘ +``` + +| Crate | Type | Purpose | +|-------|------|---------| +| `xserial-core` | library | Transport (Serial/TCP/UDP), framers (Line/Fixed/Length/COBS/Mixed/Lua), decoders (Text/Hex/Plot), MultiPipeline | +| `xserial-client` | library | Session lifecycle, SessionManager, event broadcast (tokio broadcast), RingBuffer history, Lua runtime & session API | +| `xserial-gui` | binary | egui desktop app: sidebar, config, console, text/hex/plot panels, keyboard shortcuts, font management, profiling | +| `xserial-tui` | binary | ratatui terminal app (feature-incomplete, under development) | + +**Dependency flow:** `core ← client ← {gui, tui}` + +**Data flow:** + +``` +[Transport] → read bytes → [MultiPipeline] + → Pipeline 1: Framer → Decoder → DecodedEntry → broadcast → GUI buffers + → Pipeline 2: Framer → Decoder → DecodedEntry → broadcast → GUI buffers + → ... +``` + +Each pipeline frames and decodes independently. Only successful decodes produce output. + +--- + +## Configuration & Persistence + +GUI state is saved automatically to platform-standard locations: + +| Platform | Path | +|----------|------| +| Linux | `$XDG_CONFIG_HOME/xserial/gui-state.json` or `~/.config/xserial/gui-state.json` | +| macOS | `~/Library/Application Support/xserial/gui-state.json` | +| Windows | `%APPDATA%\xserial\gui-state.json` | + +Persisted data includes: session configurations (transport, pipelines), log settings, active tab, and display options. + +Log files are written to `logs/` under the config directory, named `session_{id}_{timestamp}.log`. + +--- + +## Development Tools + +### Test Data Generators + +```bash +# Plot waveform test data +python tools/test_plot.py --wire-format mixed --host 127.0.0.1 --port 8091 +python tools/test_plot.py --wire-format mixed --format xy --channels 2 +python tools/test_plot.py --wire-format raw --channels 2 --framelen 256 + +# Drone telemetry test data +python tools/test_drone.py --rate 10 --port 8092 +``` + +### Profiling + +```bash +RUST_LOG=xserial_gui::perf=info XSERIAL_GUI_PROFILE=1 cargo run -p xserial-gui +XSERIAL_GUI_PROFILE_INTERVAL_MS=500 cargo run -p xserial-gui +``` + +### Tracing + +```bash +RUST_LOG=info cargo run -p xserial-gui +RUST_LOG=xserial_gui=debug cargo run -p xserial-gui +``` + +### Project Structure + +``` +crates/ + xserial-core/ # Transport, framing, protocol + xserial-client/ # Session management, Lua runtime + xserial-gui/ # egui desktop application + xserial-tui/ # ratatui terminal application +examples/ # Lua script examples + drone_plot.lua # Drone telemetry plot decoder + drone_text.lua # Drone telemetry text decoder +tests/ # Lua test fixtures +tools/ # Development utilities + test_plot.py # Waveform test data generator + test_drone.py # Drone test data generator + test_plot_serial.c # C serial plot test client + test_text_serial.c # C serial text test client + xs_mixed_plot.h # MixedTextPlot protocol reference +``` + +--- + +## Tech Stack + +- **Runtime**: tokio (multi-threaded) +- **Serial**: `tokio-serial` + `serialport` +- **GUI**: egui + egui_plot +- **Lua**: mlua 0.11, LuaJIT (vendored), async/serde/send +- **Events**: `tokio::sync::broadcast` (multi-subscriber) +- **Zero feature flags**, zero build scripts, zero conditional compilation + +## License + +MIT diff --git a/examples/drone_plot.lua b/examples/drone_plot.lua new file mode 100644 index 0000000..0af1ecb --- /dev/null +++ b/examples/drone_plot.lua @@ -0,0 +1,16 @@ +return { + decode = function(frame) + if #frame == 0 then return nil end + + local gyro_str = frame:match("Gyro:([%d%.%-]+),([%d%.%-]+),([%d%.%-]+)") + if not gyro_str then return nil end + + local gz, gy, gx = frame:match("Gyro:([%d%.%-]+),([%d%.%-]+),([%d%.%-]+)") + return { + kind = "plot", + channels = { { tonumber(gz) }, { tonumber(gy) }, { tonumber(gx) } }, + sample_type = "F64", + format = "Block", + } + end, +} diff --git a/examples/drone_text.lua b/examples/drone_text.lua new file mode 100644 index 0000000..ca90f5c --- /dev/null +++ b/examples/drone_text.lua @@ -0,0 +1,33 @@ +local LABELS = { + AHRS = "Quaternion", + YPR = "Yaw/Pitch/Roll", + Gyro = "Gyro (deg/s)", + RC = "RC Channels", + M = "Motors", + L = "L", + F = "F", + C = "C", +} + +return { + decode = function(frame) + if #frame == 0 then return nil end + + local lines = {} + for segment in frame:gmatch("[^|]+") do + local colon = segment:find(":") + if colon then + local key = segment:sub(1, colon - 1) + local val = segment:sub(colon + 1) + local label = LABELS[key] or key + lines[#lines + 1] = string.format("%-18s %s", label .. ":", val) + end + end + lines[#lines + 1] = string.rep("-", 40) + + return { + kind = "text", + data = table.concat(lines, "\n"), + } + end, +} diff --git a/tools/test_drone.py b/tools/test_drone.py new file mode 100644 index 0000000..2543663 --- /dev/null +++ b/tools/test_drone.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Generate fake drone telemetry data for testing xserial Lua decoder.""" + +import argparse +import math +import random +import socket +import time + + +def generate_line(t: float) -> str: + """Generate one telemetry line with simulated sensor values.""" + # Quaternion (w, x, y, z) — gently rotating + angle = t * 0.5 + qw = math.cos(angle) + qx = math.sin(angle) * 0.1 + qy = math.sin(angle) * 0.05 + qz = math.sin(angle) * 0.02 + + # Yaw/Pitch/Roll — sine wave simulation + yaw = math.degrees(math.sin(t * 0.3)) * 30 + pitch = math.degrees(math.sin(t * 0.5)) * 15 + roll = math.degrees(math.sin(t * 0.7)) * 10 + + # Gyro (deg/s) + gz = math.sin(t * 0.3) * 50 + random.uniform(-2, 2) + gy = math.sin(t * 0.5) * 30 + random.uniform(-2, 2) + gx = math.sin(t * 0.7) * 20 + random.uniform(-2, 2) + + # RC channels (1000-2000 us) + rc_r = int(1500 + math.sin(t * 0.3) * 200) + rc_p = int(1500 + math.sin(t * 0.5) * 150) + rc_t = int(1000 + (math.sin(t * 0.1) + 1) * 500) # throttle 1000-2000 + rc_y = int(1500 + math.sin(t * 0.3) * 100) + + # Motors (1000-2000) + base = 1200 + int((math.sin(t * 0.1) + 1) * 400) + m1 = base + random.randint(-20, 20) + m2 = base + random.randint(-20, 20) + m3 = base + random.randint(-20, 20) + m4 = base + random.randint(-20, 20) + + return ( + f"AHRS q:{qw:.4f},{qx:.4f},{qy:.4f},{qz:.4f}|" + f"YPR:{yaw:.2f},{pitch:.2f},{roll:.2f}|" + f"Gyro:{gz:.2f},{gy:.2f},{gx:.2f}|" + f"RC:{rc_r},{rc_p},{rc_t},{rc_y}|" + f"M:{m1},{m2},{m3},{m4}|" + f"L:0 F:1 C:0\n" + ) + + +def main(): + parser = argparse.ArgumentParser(description="Drone telemetry test data generator") + parser.add_argument("--host", default="127.0.0.1", help="TCP host") + parser.add_argument("--port", type=int, default=8092, help="TCP port") + parser.add_argument("--rate", type=float, default=10, help="Lines per second") + parser.add_argument("--duration", type=float, default=0, help="Seconds to run (0 = forever)") + args = parser.parse_args() + + interval = 1.0 / args.rate + print(f"Sending drone telemetry to {args.host}:{args.port} at {args.rate} Hz") + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind((args.host, args.port)) + server.listen(1) + print(f"Listening on {args.host}:{args.port}, waiting for connections...") + + while True: + sock, addr = server.accept() + print(f"Client connected from {addr}") + + t0 = time.time() + seq = 0 + + try: + while True: + t = time.time() - t0 + line = generate_line(t) + try: + sock.sendall(line.encode()) + except (BrokenPipeError, ConnectionResetError): + break + seq += 1 + + if args.duration > 0 and t >= args.duration: + break + + elapsed = time.time() - t0 + target = (seq + 1) * interval + sleep_time = target - elapsed + if sleep_time > 0: + time.sleep(sleep_time) + except (BrokenPipeError, ConnectionResetError): + pass + + print(f"Client disconnected. Sent {seq} lines. Waiting for new connection...") + + +if __name__ == "__main__": + main()