Compare commits
2 Commits
a2c7c0fa71
...
227a459712
| Author | SHA1 | Date | |
|---|---|---|---|
| 227a459712 | |||
| 367800a426 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,3 +3,6 @@
|
|||||||
# Compiled binaries in tools/
|
# Compiled binaries in tools/
|
||||||
tools/test_plot_c
|
tools/test_plot_c
|
||||||
tools/test_text_c
|
tools/test_text_c
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
|||||||
383
README.md
383
README.md
@@ -1,159 +1,58 @@
|
|||||||
# xserial
|
# 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 脚本扩展。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 架构
|
## 功能特性
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ 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) │
|
|
||||||
└─────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
| Crate | 类型 | 职责 |
|
|
||||||
|-------|------|------|
|
|
||||||
| `xserial-core` | library | 传输层、分帧器、协议解码器、Pipeline |
|
|
||||||
| `xserial-client` | library | 会话管理、配置持久化、事件分发、历史缓冲、Lua 运行时 |
|
|
||||||
| `xserial-gui` | binary | egui 桌面应用(主要入口) |
|
|
||||||
| `xserial-tui` | binary | ratatui 终端应用(占位,未达功能对等) |
|
|
||||||
|
|
||||||
依赖方向: `core ← client ← {gui, tui}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
**前置条件**:
|
|
||||||
|
|
||||||
- **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
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 功能
|
|
||||||
|
|
||||||
### 传输层
|
### 传输层
|
||||||
|
|
||||||
| 类型 | 配置项 |
|
- **串口** — 波特率 (300–12M)、数据位 (5/6/7/8)、校验位 (None/Odd/Even)、停止位 (1/2)、流控 (None/Software/Hardware)、DTR/RTS 控制
|
||||||
|------|--------|
|
- **TCP** — 原始 TCP 客户端
|
||||||
| **Serial** | 端口名、波特率、数据位 (5/6/7/8)、校验位 (None/Odd/Even)、停止位 (1/2)、流控 (None/Software/Hardware)、DTR、RTS |
|
- **UDP** — 绑定地址 + 可选远端地址
|
||||||
| **TCP** | 目标地址 `host:port` |
|
|
||||||
| **UDP** | 绑定地址 `host:port`,可选远端地址 |
|
|
||||||
|
|
||||||
DTR/RTS 控制仅在 Serial 连接时可用。
|
### 分帧与解码
|
||||||
|
|
||||||
### 分帧器
|
每个 Session 可配置**多条独立管线** — 同一字节流并行送入多个 framer→decoder 链路。
|
||||||
|
|
||||||
| 分帧器 | 说明 | 配置 |
|
| 分帧器 | 说明 |
|
||||||
|--------|------|------|
|
|--------|------|
|
||||||
| **Line** | 按换行符 `\n` 分割,可选去除 `\r` | `strip_cr`, `max_line_len` |
|
| Line | 按 `\n` 分割,可选去除 `\r` |
|
||||||
| **Fixed** | 固定字节数为一帧 | `frame_len` |
|
| Fixed | 固定字节数为一帧 |
|
||||||
| **Length** | 长度前缀协议帧 | `len_bytes` (1/2/4/8), `endian`, `length_includes_self`, `max_payload` |
|
| Length | 长度前缀协议 (1/2/4/8 字节头) |
|
||||||
| **Cobs** | [COBS](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) 编码,`0x00` 分隔 | `max_frame` |
|
| COBS | [Consistent Overhead Byte Stuffing](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) |
|
||||||
| **MixedTextPlot** | 单连接混合文本行 + COBS plot 帧 | `strip_cr`, `max_line_len`, `max_plot_frame` |
|
| MixedTextPlot | 单连接混合文本行 + COBS plot 帧 |
|
||||||
| **Lua** | 用户自定义 Lua 脚本 | `script_path` |
|
| Lua | 用户自定义分帧脚本 |
|
||||||
|
|
||||||
### 协议解码器
|
| 解码器 | 输出 |
|
||||||
|
|--------|------|
|
||||||
|
| Text | UTF-8 / Latin1 / ASCII |
|
||||||
|
| Hex | 可配置分组、大小端 |
|
||||||
|
| Plot | Interleaved / Block / XY 波形 (i8–f64) |
|
||||||
|
| MixedTextPlot | 混合文本 + 波形 |
|
||||||
|
| Lua | 用户自定义解码脚本 |
|
||||||
|
|
||||||
| 解码器 | 输出 | 配置 |
|
### 视图
|
||||||
|--------|------|------|
|
|
||||||
| **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 采样格式
|
- **文本视图** — 带时间戳、方向标记、管线标签
|
||||||
|
- **十六进制视图** — hex dump + ASCII 侧栏
|
||||||
| 格式 | 说明 |
|
- **波形视图** — 实时波形:自适应、框选缩放、锁定坐标轴、浮动窗口
|
||||||
|------|------|
|
|
||||||
| **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 面板发送原始字节到连接
|
- **日志到文件** — 后台线程写入,不阻塞 UI
|
||||||
- **历史缓冲**: 环形缓冲区(默认 10,000 条),可配置上限
|
- 配置自动持久化到 `gui-state.json`
|
||||||
- **状态持久化**: Session 配置自动保存到 `gui-state.json`
|
|
||||||
|
|
||||||
### Lua 脚本扩展
|
### Lua 脚本
|
||||||
|
|
||||||
内置 LuaJIT 运行时,支持:
|
内置 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
|
```lua
|
||||||
local buffer = ""
|
local buffer = ""
|
||||||
@@ -178,79 +77,99 @@ return {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
会话 API:`xserial.open()`、`xserial.list_ports()`、`xserial.sleep()`、`xserial.poll()`、`xserial.log()`、`session:on_data()`、`session:send()`
|
||||||
|
|
||||||
## GUI
|
|
||||||
|
|
||||||
### 面板
|
|
||||||
|
|
||||||
| 面板 | 功能 |
|
|
||||||
|------|------|
|
|
||||||
| **Sidebar** | Session 列表、创建/删除/编辑 session、连接切换 |
|
|
||||||
| **Config** | 传输参数、Pipeline 配置(分帧器 + 解码器类型及参数) |
|
|
||||||
| **Console** | 数据发送输入框,发送按钮 |
|
|
||||||
| **Text View** | 解码后的文本流,支持时间戳和方向标记 |
|
|
||||||
| **Hex View** | 十六进制字节展示,可配置分组和分隔符 |
|
|
||||||
| **Plot View** | 实时波形图,支持内嵌和浮动窗口两种模式 |
|
|
||||||
|
|
||||||
### 快捷键
|
### 快捷键
|
||||||
|
|
||||||
| 快捷键 | 操作 |
|
| 快捷键 | 操作 |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| `Ctrl+Tab` | 下一个 Tab |
|
| `Ctrl+N` | 新建会话 |
|
||||||
| `Ctrl+Shift+Tab` | 上一个 Tab |
|
| `Ctrl+E` | 编辑当前会话 |
|
||||||
| `Ctrl+T` | 切换到 Text 视图 |
|
| `Ctrl+W` | 删除当前会话 |
|
||||||
| `Ctrl+H` | 切换到 Hex 视图 |
|
|
||||||
| `Ctrl+P` | 切换到 Plot 视图 |
|
|
||||||
| `Ctrl+N` | 新建 Session |
|
|
||||||
| `Ctrl+E` | 编辑当前 Session |
|
|
||||||
| `Ctrl+W` | 删除当前 Session |
|
|
||||||
| `Ctrl+F5` | 切换连接 |
|
| `Ctrl+F5` | 切换连接 |
|
||||||
|
| `Ctrl+T/H/P` | 切换到文本 / 十六进制 / 波形视图 |
|
||||||
|
| `Ctrl+Tab` / `Ctrl+Shift+Tab` | 下一个 / 上一个标签页 |
|
||||||
| `Ctrl+L` | 清空输出 |
|
| `Ctrl+L` | 清空输出 |
|
||||||
| `Ctrl+,` | UI 设置 |
|
| `Ctrl+,` | UI 设置 |
|
||||||
| `Esc` | 关闭浮层 |
|
| `Esc` | 关闭浮层 |
|
||||||
|
|
||||||
### 状态持久化
|
---
|
||||||
|
|
||||||
GUI 状态(session 配置、活动 tab、显示选项)自动保存到:
|
## 快速开始
|
||||||
|
|
||||||
|
### 前置条件
|
||||||
|
|
||||||
|
- **Rust ≥ 1.85**
|
||||||
|
- **Linux**: `libudev-dev`(serialport 依赖)
|
||||||
|
- **所有平台**: C 编译器(mlua 从源码编译 LuaJIT)
|
||||||
|
|
||||||
|
### 构建与运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p xserial-gui
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --workspace # ~308 个测试
|
||||||
|
cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置文件位置
|
||||||
|
|
||||||
| 平台 | 路径 |
|
| 平台 | 路径 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| Linux | `$XDG_CONFIG_HOME/xserial/gui-state.json` 或 `~/.config/xserial/gui-state.json` |
|
| Linux | `$XDG_CONFIG_HOME/xserial/` 或 `~/.config/xserial/` |
|
||||||
| macOS | `~/Library/Application Support/xserial/gui-state.json` |
|
| macOS | `~/Library/Application Support/xserial/` |
|
||||||
| Windows | `%APPDATA%\xserial\gui-state.json` |
|
| Windows | `%APPDATA%\xserial\` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 开发工具
|
## 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ 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 | 传输层、分帧器、协议解码、管线 |
|
||||||
|
| `xserial-client` | library | 会话管理、事件分发、历史缓冲、Lua 运行时 |
|
||||||
|
| `xserial-gui` | binary | egui 桌面应用(主要入口) |
|
||||||
|
| `xserial-tui` | binary | ratatui 终端应用(占位) |
|
||||||
|
|
||||||
|
依赖方向: `core ← client ← {gui, tui}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
### Plot 测试服务器
|
### Plot 测试服务器
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 启动 TCP 波形数据源
|
|
||||||
python tools/test_plot.py --wire-format mixed --host 127.0.0.1 --port 8091
|
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
|
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`
|
GUI 中配置:Transport `TCP 127.0.0.1:8091`,Framer `MixedTextPlot`,Decoder `MixedTextPlot`。
|
||||||
|
|
||||||
### 性能分析
|
### 性能分析
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 开启性能日志
|
|
||||||
RUST_LOG=xserial_gui::perf=info XSERIAL_GUI_PROFILE=1 cargo run -p xserial-gui
|
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
|
XSERIAL_GUI_PROFILE_INTERVAL_MS=500 cargo run -p xserial-gui
|
||||||
```
|
```
|
||||||
|
|
||||||
日志输出: frame 耗时、事件 drain 耗时、text/hex/plot 渲染耗时、plot 点数统计
|
|
||||||
|
|
||||||
### 日志
|
### 日志
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -258,109 +177,17 @@ RUST_LOG=info cargo run -p xserial-gui
|
|||||||
RUST_LOG=xserial_gui=debug cargo run -p xserial-gui
|
RUST_LOG=xserial_gui=debug cargo run -p xserial-gui
|
||||||
```
|
```
|
||||||
|
|
||||||
使用 `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`,串口测试使用虚拟端口名),无外部依赖。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 目录结构
|
## 技术栈
|
||||||
|
|
||||||
```
|
- **运行时**: tokio (multi-thread)
|
||||||
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 参考头文件
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MixedTextPlot 协议
|
|
||||||
|
|
||||||
单连接上混合传输文本行和波形数据的线格式。
|
|
||||||
|
|
||||||
**文本帧**: 普通文本行,以 `\n` 分隔。
|
|
||||||
|
|
||||||
**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`
|
- **串口**: `tokio-serial` + `serialport`
|
||||||
- **分帧器特征**: `feed()`, `flush()`, `reset()`, `pending_len()` — 有状态字节流分帧
|
- **GUI**: egui + egui_plot
|
||||||
- **解码器特征**: `name()`, `decode(&[u8]) -> Option<DecodedData>` — 无状态帧解码
|
- **Lua**: mlua 0.11, LuaJIT (vendored), async/serde/send
|
||||||
- **Session 事件循环**: `tokio::select!` 多路复用命令、读取、重连定时器
|
- **事件**: `tokio::sync::broadcast` 多订阅者
|
||||||
- **事件分发**: `tokio::sync::broadcast` — 多订阅者
|
- **零 feature flags**,零 build script,零条件编译
|
||||||
- **Lua**: `mlua` 0.11, LuaJIT (vendored 编译), 支持 async/serde/send
|
|
||||||
- **无 feature flags**: 零条件编译,无 build script
|
## License
|
||||||
- **无 CI**: 本地自检 `cargo test --workspace` + `cargo clippy`
|
|
||||||
|
MIT
|
||||||
|
|||||||
16
examples/drone_plot.lua
Normal file
16
examples/drone_plot.lua
Normal file
@@ -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,
|
||||||
|
}
|
||||||
33
examples/drone_text.lua
Normal file
33
examples/drone_text.lua
Normal file
@@ -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,
|
||||||
|
}
|
||||||
102
tools/test_drone.py
Normal file
102
tools/test_drone.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user