Rewrite README in Chinese and English, add drone examples
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
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__/
|
||||||
|
|||||||
134
AGENTS.md
134
AGENTS.md
@@ -1,134 +0,0 @@
|
|||||||
# Repository Guidelines
|
|
||||||
|
|
||||||
## Build Prerequisites
|
|
||||||
|
|
||||||
- **Linux**: `libudev-dev` (for `serialport`).
|
|
||||||
- **All platforms**: a C compiler (`cc`, `gcc`, `clang`, or MSVC) — `mlua` uses `features = ["vendored"]` which compiles LuaJIT from source at build time.
|
|
||||||
- **No pinned toolchain**: all crates use `edition = "2024"` (requires Rust ≥ 1.85). No `rust-toolchain.toml`; builds with whatever `rustc` is on `PATH`.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
`xserial` is a Rust workspace. The root `Cargo.toml` defines four crates in `crates/`, with dependencies flowing:
|
|
||||||
|
|
||||||
```
|
|
||||||
xserial-core (no internal deps — transport, framing, protocol, pipeline)
|
|
||||||
↑
|
|
||||||
xserial-client (depends on xserial-core — sessions, history, commands, Lua)
|
|
||||||
↑ ↑
|
|
||||||
xserial-gui xserial-tui
|
|
||||||
(both depend on xserial-core AND xserial-client)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Crate | Type | What goes here |
|
|
||||||
|-------|------|---------------|
|
|
||||||
| `xserial-core` | library (`src/lib.rs`) | `transport/`, `frame/`, `protocol/`, `pipeline.rs` — protocol and I/O. Integration tests: `tests/pipeline.rs`. |
|
|
||||||
| `xserial-client` | library (`src/lib.rs`) | `session.rs`, `manager.rs`, `config.rs`, `history.rs`, `lua/` — state management and Lua bindings. Integration tests: `tests/session_lifecycle.rs`, `tests/lua_tests.rs`. |
|
|
||||||
| `xserial-gui` | binary (`src/main.rs`) | egui/eframe app. Panels in `src/panels/`. Font helpers in `src/ui_fonts.rs`. Profiling in `src/perf.rs`. |
|
|
||||||
| `xserial-tui` | binary (`src/main.rs`) | ratatui/crossterm app. **Still a placeholder** — zero tests, not at feature parity with GUI. |
|
|
||||||
|
|
||||||
**Module boundaries**: transport/protocol → `xserial-core`. Session/Lua → `xserial-client`. UI state → `xserial-gui` or `xserial-tui`. Never put I/O logic in the UI crates, never put UI state in the client crate.
|
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo check --workspace # fast compile check (all crates)
|
|
||||||
cargo test --workspace # full suite (~266 tests)
|
|
||||||
cargo fmt --all # standard Rust formatting
|
|
||||||
cargo clippy --workspace --all-targets -- -D warnings # lint gate
|
|
||||||
```
|
|
||||||
|
|
||||||
**Run specific tests:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo test -p xserial-core -- tcp_line_text_utf8
|
|
||||||
cargo test -p xserial-client -- session_echo_send_and_read
|
|
||||||
cargo test -p xserial-core -- --nocapture # show test output
|
|
||||||
|
|
||||||
# Integration tests only
|
|
||||||
cargo test -p xserial-core --test pipeline
|
|
||||||
cargo test -p xserial-client --test session_lifecycle
|
|
||||||
cargo test -p xserial-client --test lua_tests
|
|
||||||
```
|
|
||||||
|
|
||||||
**Launch:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run -p xserial-gui
|
|
||||||
cargo run -p xserial-tui
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment & Configuration
|
|
||||||
|
|
||||||
### Tracing (all entrypoints)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RUST_LOG=info cargo run -p xserial-gui
|
|
||||||
RUST_LOG=xserial_gui::perf=debug cargo run -p xserial-gui
|
|
||||||
```
|
|
||||||
|
|
||||||
`tracing_subscriber::EnvFilter::from_default_env()` reads `RUST_LOG`. The workspace dep `tracing-subscriber` has `features = ["env-filter"]`.
|
|
||||||
|
|
||||||
### GUI 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 # custom interval (default 1s)
|
|
||||||
```
|
|
||||||
|
|
||||||
Profiling logs go to target `xserial_gui::perf`.
|
|
||||||
|
|
||||||
### State & Config File Paths (runtime, not compile-time)
|
|
||||||
|
|
||||||
Uses `env::consts::OS` + env vars — no `cfg(target_os)` gating for path resolution:
|
|
||||||
|
|
||||||
| Platform | Root config path |
|
|
||||||
|----------|-----------------|
|
|
||||||
| Linux | `$XDG_CONFIG_HOME/xserial/` or `~/.config/xserial/` |
|
|
||||||
| macOS | `~/Library/Application Support/xserial/` |
|
|
||||||
| Windows | `%APPDATA%\xserial\` |
|
|
||||||
|
|
||||||
State files: `gui-state.json`, `tui-state.json`, `font-settings.json`.
|
|
||||||
|
|
||||||
### GUI Plot Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
Configure in GUI: Transport `TCP 127.0.0.1:8091`, Framer `MixedTextPlot`, Decoder `MixedTextPlot`.
|
|
||||||
|
|
||||||
The wire format spec lives in `tools/xs_mixed_plot.h` (C header, reference only — not compiled into Rust).
|
|
||||||
|
|
||||||
## Coding Style
|
|
||||||
|
|
||||||
Standard Rust conventions (`cargo fmt`). No crate-level lint attrs, no `[lints]` table in any `Cargo.toml`. Names: `snake_case` for modules/files/functions/tests, `PascalCase` for types, `SCREAMING_SNAKE_CASE` for constants.
|
|
||||||
|
|
||||||
## Testing Guidelines
|
|
||||||
|
|
||||||
- **Inline unit tests** (`#[cfg(test)] mod tests`) next to implementation — 24 source files with `#[test]` or `#[tokio::test]`.
|
|
||||||
- **Integration tests** in `crates/<crate>/tests/*.rs` — 3 files covering pipeline, session lifecycle, and Lua bindings.
|
|
||||||
- **~266 tests total** — all active (zero `#[ignore]`). Single `#[should_panic]` in `frame/fixed.rs`.
|
|
||||||
- **All tests are self-contained**: TCP/UDP bind to `127.0.0.1:0` (OS-assigned port). No hardcoded ports. No external services. Serial tests use dummy port names and never open real hardware.
|
|
||||||
- **Async tests** use `#[tokio::test]` (no `async-std`). **No benchmarks** exist.
|
|
||||||
- Run `cargo test --workspace` before opening PRs. Add targeted regression tests for transport, framing, parsing, and session-state fixes.
|
|
||||||
|
|
||||||
## Known Quirks & Constraints
|
|
||||||
|
|
||||||
- **No CI** — no `.github/workflows` or other CI config. Agents must self-verify with `cargo test --workspace` and `cargo clippy --workspace --all-targets -- -D warnings`.
|
|
||||||
- **No feature flags** — zero `[features]` in any `Cargo.toml`. No `#[cfg(feature)]` in any source.
|
|
||||||
- **No build scripts** — zero `build.rs` files. No codegen. No `include!`/`include_str!`/`include_bytes!`.
|
|
||||||
- **All `unsafe` is test-only** — constructing noop wakers for poll-based unit tests in `transport/`. No non-test `unsafe` anywhere.
|
|
||||||
- **`xserial-tui` is a stub** — no tests, minimal implementation. GUI is the primary target.
|
|
||||||
- **Platform font directories** use `#[cfg(target_os)]` in `ui_fonts.rs` (three blocks: Windows, macOS, Linux/BSD).
|
|
||||||
- **`tokio` features differ by build**: `xserial-client` uses `["sync", "time"]` for production, `["full"]` for dev-dependencies (tests need net/IO).
|
|
||||||
|
|
||||||
## Commit & PR Guidelines
|
|
||||||
|
|
||||||
Short, imperative subjects (`Persist GUI font settings`), occasional `feat:` prefix for major additions. PRs: state which crate(s) changed, list validation commands, include screenshots/recordings for GUI/TUI changes. Explicitly call out protocol, transport, or Lua API compatibility impacts.
|
|
||||||
|
|
||||||
## Reference
|
|
||||||
|
|
||||||
- `tools/test_plot.py --help` — TCP plot-frame generator options.
|
|
||||||
- `tools/xs_mixed_plot.h` — wire format spec for MixedTextPlot (COBS-framed binary plot + text on one stream).
|
|
||||||
136
Cargo.lock
generated
136
Cargo.lock
generated
@@ -3262,6 +3262,74 @@ dependencies = [
|
|||||||
"futures-io",
|
"futures-io",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pipeview-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"mlua",
|
||||||
|
"pipeview-core",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pipeview-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"hex",
|
||||||
|
"nom 8.0.0",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serialport",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tokio-serial",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pipeview-gui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"eframe",
|
||||||
|
"egui",
|
||||||
|
"egui_plot",
|
||||||
|
"hex",
|
||||||
|
"pipeview-client",
|
||||||
|
"pipeview-core",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pipeview-tui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"crossterm 0.28.1",
|
||||||
|
"hex",
|
||||||
|
"image",
|
||||||
|
"mlua",
|
||||||
|
"pipeview-client",
|
||||||
|
"pipeview-core",
|
||||||
|
"ratatui",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-appender",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.33"
|
version = "0.3.33"
|
||||||
@@ -5750,74 +5818,6 @@ version = "0.8.28"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "xserial-client"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"mlua",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"tracing-subscriber",
|
|
||||||
"xserial-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "xserial-core"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"bytes",
|
|
||||||
"hex",
|
|
||||||
"nom 8.0.0",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serialport",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
|
||||||
"tokio-serial",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "xserial-gui"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"eframe",
|
|
||||||
"egui",
|
|
||||||
"egui_plot",
|
|
||||||
"hex",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"tracing-subscriber",
|
|
||||||
"xserial-client",
|
|
||||||
"xserial-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "xserial-tui"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"clap",
|
|
||||||
"crossterm 0.28.1",
|
|
||||||
"hex",
|
|
||||||
"image",
|
|
||||||
"mlua",
|
|
||||||
"ratatui",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"tracing-appender",
|
|
||||||
"tracing-subscriber",
|
|
||||||
"xserial-client",
|
|
||||||
"xserial-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/xserial-core",
|
"crates/pipeview-core",
|
||||||
"crates/xserial-client",
|
"crates/pipeview-client",
|
||||||
"crates/xserial-tui",
|
"crates/pipeview-tui",
|
||||||
"crates/xserial-gui",
|
"crates/pipeview-gui",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
652
README.md
652
README.md
@@ -1,366 +1,394 @@
|
|||||||
# xserial
|
# pipeview
|
||||||
|
|
||||||
**跨平台串口 / 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** — `pipeview.open()` 创建 Session、`session:on_data()` 事件回调、`session:send()` 发送数据
|
||||||
|
- **工具函数** — `pipeview.list_ports()`、`pipeview.sleep(ms)`、`pipeview.poll(limit)`、`pipeview.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/pipeview.git
|
||||||
|
cd pipeview
|
||||||
|
|
||||||
|
# 编译运行
|
||||||
|
cargo run -p pipeview-gui
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
cargo test --workspace # ~308 个测试
|
||||||
|
cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
### 发布构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build -p pipeview-gui --release
|
||||||
|
# 二进制位于 target/release/pipeview-gui (Linux/macOS)
|
||||||
|
# 或 target/release/pipeview-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
|
||||||
|
|
||||||
|
在 pipeview-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) │
|
│ pipeview-gui (egui) pipeview-tui (ratatui) │
|
||||||
│ 面板: sidebar, console, 占位实现 │
|
├──────────────────────────────────────────────────┤
|
||||||
│ hex_view, plot_view, │
|
│ pipeview-client │
|
||||||
│ config │
|
│ SessionManager · Session · Config · History │
|
||||||
├─────────────────────────────────────────────────────┤
|
│ Lua Runtime (mlua / LuaJIT) │
|
||||||
│ xserial-client │
|
├──────────────────────────────────────────────────┤
|
||||||
│ SessionManager · Session · Config · History │
|
│ pipeview-core │
|
||||||
│ Lua Runtime (mlua/LuaJIT) │
|
│ Transport ──▶ Frame ──▶ Protocol ──▶ Pipeline │
|
||||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
└──────────────────────────────────────────────────┘
|
||||||
│ │ 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 | 类型 | 职责 |
|
| Crate | 类型 | 职责 |
|
||||||
|-------|------|------|
|
|-------|------|------|
|
||||||
| `xserial-core` | library | 传输层、分帧器、协议解码器、Pipeline |
|
| `pipeview-core` | library | 传输层(Serial/TCP/UDP)、分帧器(Line/Fixed/Length/COBS/Mixed/Lua)、协议解码器(Text/Hex/Plot)、MultiPipeline |
|
||||||
| `xserial-client` | library | 会话管理、配置持久化、事件分发、历史缓冲、Lua 运行时 |
|
| `pipeview-client` | library | Session 生命周期管理、SessionManager、事件广播(tokio broadcast)、RingBuffer 历史、Lua 运行时及会话 API |
|
||||||
| `xserial-gui` | binary | egui 桌面应用(主要入口) |
|
| `pipeview-gui` | binary | egui 桌面应用,包含 sidebar/config/console/text/hex/plot 面板、键盘快捷键、字体管理、性能分析 |
|
||||||
| `xserial-tui` | binary | ratatui 终端应用(占位,未达功能对等) |
|
| `pipeview-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
|
||||||
|
→ ...
|
||||||
|
```
|
||||||
|
|
||||||
|
每条管线独立分帧、解码,互不干扰。只有成功解码的管线产生输出。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 构建
|
## 配置与持久化
|
||||||
|
|
||||||
**前置条件**:
|
GUI 状态自动保存,路径遵循各平台规范:
|
||||||
|
|
||||||
- **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、显示选项)自动保存到:
|
|
||||||
|
|
||||||
| 平台 | 路径 |
|
| 平台 | 路径 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| Linux | `$XDG_CONFIG_HOME/xserial/gui-state.json` 或 `~/.config/xserial/gui-state.json` |
|
| Linux | `$XDG_CONFIG_HOME/pipeview/gui-state.json` 或 `~/.config/pipeview/gui-state.json` |
|
||||||
| macOS | `~/Library/Application Support/xserial/gui-state.json` |
|
| macOS | `~/Library/Application Support/pipeview/gui-state.json` |
|
||||||
| Windows | `%APPDATA%\xserial\gui-state.json` |
|
| Windows | `%APPDATA%\pipeview\gui-state.json` |
|
||||||
|
|
||||||
|
持久化的内容包括:Session 配置(传输参数、管线设置)、日志开关及路径、活动标签页、显示选项。
|
||||||
|
|
||||||
|
日志文件默认保存在配置目录的 `logs/` 子目录下,文件命名格式为 `session_{id}_{timestamp}.log`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 开发工具
|
## 开发工具
|
||||||
|
|
||||||
### Plot 测试服务器
|
### 测试数据生成器
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 启动 TCP 波形数据源
|
# Plot 波形测试数据
|
||||||
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
|
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
|
```bash
|
||||||
# 开启性能日志
|
RUST_LOG=pipeview_gui::perf=info XSERIAL_GUI_PROFILE=1 cargo run -p pipeview-gui
|
||||||
RUST_LOG=xserial_gui::perf=info XSERIAL_GUI_PROFILE=1 cargo run -p xserial-gui
|
XSERIAL_GUI_PROFILE_INTERVAL_MS=500 cargo run -p pipeview-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
|
```bash
|
||||||
RUST_LOG=info cargo run -p xserial-gui
|
RUST_LOG=info cargo run -p pipeview-gui # 应用日志
|
||||||
RUST_LOG=xserial_gui=debug cargo run -p xserial-gui
|
RUST_LOG=pipeview_gui=debug cargo run -p pipeview-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/
|
crates/
|
||||||
xserial-core/ # 传输、分帧、协议解码、Pipeline
|
pipeview-core/ # 传输、分帧、协议
|
||||||
src/
|
pipeview-client/ # 会话管理、Lua 运行时
|
||||||
transport/ # Serial, TCP, UDP
|
pipeview-gui/ # egui 桌面应用
|
||||||
frame/ # Line, Fixed, Length, Cobs, MixedTextPlot
|
pipeview-tui/ # ratatui 终端应用
|
||||||
protocol/ # Text, Hex, Plot, MixedTextPlot
|
examples/ # Lua 脚本示例
|
||||||
pipeline.rs # MultiPipeline
|
drone_plot.lua # 飞控遥测波形解码器
|
||||||
xserial-client/ # 会话管理、Lua 运行时
|
drone_text.lua # 飞控遥测文本解码器
|
||||||
src/
|
tests/ # Lua 测试 fixture
|
||||||
session.rs # Session 事件循环
|
tools/ # 开发辅助工具
|
||||||
manager.rs # SessionManager
|
test_plot.py # 波形测试数据生成器
|
||||||
config.rs # SessionConfig, FramerConfig, DecoderConfig
|
test_drone.py # 飞控测试数据生成器
|
||||||
event.rs # DecodedEntry
|
test_plot_serial.c # C 串口波形测试客户端
|
||||||
history.rs # RingBuffer
|
test_text_serial.c # C 串口文本测试客户端
|
||||||
cmd.rs # SessionCmd
|
xs_mixed_plot.h # MixedTextPlot 协议参考头文件
|
||||||
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 协议
|
## 技术栈
|
||||||
|
|
||||||
单连接上混合传输文本行和波形数据的线格式。
|
- **运行时**: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 帧**:
|
MIT
|
||||||
```
|
|
||||||
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<DecodedData>` — 无状态帧解码
|
|
||||||
- **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`
|
|
||||||
|
|||||||
378
README_en.md
Normal file
378
README_en.md
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
# pipeview
|
||||||
|
|
||||||
|
**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** — `pipeview.open()` to create sessions, `session:on_data()` for event callbacks, `session:send()` to transmit data
|
||||||
|
- **Utilities** — `pipeview.list_ports()`, `pipeview.sleep(ms)`, `pipeview.poll(limit)`, `pipeview.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/pipeview.git
|
||||||
|
cd pipeview
|
||||||
|
|
||||||
|
cargo run -p pipeview-gui
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
cargo test --workspace # ~308 tests
|
||||||
|
cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build -p pipeview-gui --release
|
||||||
|
# Binary: target/release/pipeview-gui (Linux/macOS)
|
||||||
|
# target/release/pipeview-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 pipeview-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
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ pipeview-gui (egui) pipeview-tui (ratatui) │
|
||||||
|
├──────────────────────────────────────────────────┤
|
||||||
|
│ pipeview-client │
|
||||||
|
│ SessionManager · Session · Config · History │
|
||||||
|
│ Lua Runtime (mlua / LuaJIT) │
|
||||||
|
├──────────────────────────────────────────────────┤
|
||||||
|
│ pipeview-core │
|
||||||
|
│ Transport ──▶ Frame ──▶ Protocol ──▶ Pipeline │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| Crate | Type | Purpose |
|
||||||
|
|-------|------|---------|
|
||||||
|
| `pipeview-core` | library | Transport (Serial/TCP/UDP), framers (Line/Fixed/Length/COBS/Mixed/Lua), decoders (Text/Hex/Plot), MultiPipeline |
|
||||||
|
| `pipeview-client` | library | Session lifecycle, SessionManager, event broadcast (tokio broadcast), RingBuffer history, Lua runtime & session API |
|
||||||
|
| `pipeview-gui` | binary | egui desktop app: sidebar, config, console, text/hex/plot panels, keyboard shortcuts, font management, profiling |
|
||||||
|
| `pipeview-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/pipeview/gui-state.json` or `~/.config/pipeview/gui-state.json` |
|
||||||
|
| macOS | `~/Library/Application Support/pipeview/gui-state.json` |
|
||||||
|
| Windows | `%APPDATA%\pipeview\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=pipeview_gui::perf=info XSERIAL_GUI_PROFILE=1 cargo run -p pipeview-gui
|
||||||
|
XSERIAL_GUI_PROFILE_INTERVAL_MS=500 cargo run -p pipeview-gui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tracing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RUST_LOG=info cargo run -p pipeview-gui
|
||||||
|
RUST_LOG=pipeview_gui=debug cargo run -p pipeview-gui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
crates/
|
||||||
|
pipeview-core/ # Transport, framing, protocol
|
||||||
|
pipeview-client/ # Session management, Lua runtime
|
||||||
|
pipeview-gui/ # egui desktop application
|
||||||
|
pipeview-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
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xserial-client"
|
name = "pipeview-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
xserial-core = { path = "../xserial-core" }
|
pipeview-core = { path = "../pipeview-core" }
|
||||||
tokio = { workspace = true, features = ["sync", "time"] }
|
tokio = { workspace = true, features = ["sync", "time"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use xserial_core::frame::{
|
use pipeview_core::frame::{
|
||||||
Endian, Framer,
|
Endian, Framer,
|
||||||
cobs::CobsFramer,
|
cobs::CobsFramer,
|
||||||
fixed::FixedLengthFramer,
|
fixed::FixedLengthFramer,
|
||||||
@@ -7,14 +7,14 @@ use xserial_core::frame::{
|
|||||||
line::{LineConfig, LineFramer},
|
line::{LineConfig, LineFramer},
|
||||||
mixed::{MixedTextPlotConfig as MixedFramerConfig, MixedTextPlotFramer},
|
mixed::{MixedTextPlotConfig as MixedFramerConfig, MixedTextPlotFramer},
|
||||||
};
|
};
|
||||||
use xserial_core::protocol::{
|
use pipeview_core::protocol::{
|
||||||
ProtocolDecoder,
|
ProtocolDecoder,
|
||||||
hex::{HexConfig, HexDecoder},
|
hex::{HexConfig, HexDecoder},
|
||||||
mixed::{MixedTextPlotConfig as MixedDecoderConfig, MixedTextPlotDecoder},
|
mixed::{MixedTextPlotConfig as MixedDecoderConfig, MixedTextPlotDecoder},
|
||||||
plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType},
|
plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType},
|
||||||
text::{TextDecoder, TextEncoding},
|
text::{TextDecoder, TextEncoding},
|
||||||
};
|
};
|
||||||
use xserial_core::transport::TransportConfig;
|
use pipeview_core::transport::TransportConfig;
|
||||||
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::lua::codec::{LuaDecoder, LuaFramer};
|
use crate::lua::codec::{LuaDecoder, LuaFramer};
|
||||||
@@ -229,10 +229,10 @@ impl Default for SessionConfig {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use xserial_core::protocol::Endian;
|
use pipeview_core::protocol::Endian;
|
||||||
use xserial_core::protocol::plot::{PlotFormat, SampleType};
|
use pipeview_core::protocol::plot::{PlotFormat, SampleType};
|
||||||
use xserial_core::protocol::text::TextEncoding;
|
use pipeview_core::protocol::text::TextEncoding;
|
||||||
use xserial_core::transport::TransportConfig;
|
use pipeview_core::transport::TransportConfig;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn framer_line_serde_roundtrip() {
|
fn framer_line_serde_roundtrip() {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use xserial_core::protocol::DecodedData;
|
use pipeview_core::protocol::DecodedData;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DecodedEntry {
|
pub struct DecodedEntry {
|
||||||
@@ -3,9 +3,9 @@ use std::sync::Mutex;
|
|||||||
|
|
||||||
use mlua::{Function, Lua, RegistryKey, Table, Value};
|
use mlua::{Function, Lua, RegistryKey, Table, Value};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use xserial_core::frame::Framer;
|
use pipeview_core::frame::Framer;
|
||||||
use xserial_core::protocol::plot::{PlotFormat, PlotFrame, SampleType};
|
use pipeview_core::protocol::plot::{PlotFormat, PlotFrame, SampleType};
|
||||||
use xserial_core::protocol::{DecodedData, ProtocolDecoder};
|
use pipeview_core::protocol::{DecodedData, ProtocolDecoder};
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
@@ -297,7 +297,7 @@ mod tests {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_nanos();
|
.as_nanos();
|
||||||
let path = std::env::temp_dir().join(format!("xserial_{name}_{nonce}.lua"));
|
let path = std::env::temp_dir().join(format!("pipeview_{name}_{nonce}.lua"));
|
||||||
fs::write(&path, script).unwrap();
|
fs::write(&path, script).unwrap();
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
@@ -81,9 +81,9 @@ pub fn register(lua: &Lua) -> LuaResult<()> {
|
|||||||
|
|
||||||
pub fn register_with_manager(lua: &Lua, manager: SessionManager) -> LuaResult<()> {
|
pub fn register_with_manager(lua: &Lua, manager: SessionManager) -> LuaResult<()> {
|
||||||
let runtime = LuaRuntime::new(manager);
|
let runtime = LuaRuntime::new(manager);
|
||||||
let xserial = lua.create_table()?;
|
let pipeview = lua.create_table()?;
|
||||||
|
|
||||||
xserial.set("open", {
|
pipeview.set("open", {
|
||||||
let runtime = runtime.clone();
|
let runtime = runtime.clone();
|
||||||
lua.create_async_function(move |lua, config: Table| {
|
lua.create_async_function(move |lua, config: Table| {
|
||||||
let runtime = runtime.clone();
|
let runtime = runtime.clone();
|
||||||
@@ -96,15 +96,15 @@ pub fn register_with_manager(lua: &Lua, manager: SessionManager) -> LuaResult<()
|
|||||||
})?
|
})?
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
xserial.set(
|
pipeview.set(
|
||||||
"list_ports",
|
"list_ports",
|
||||||
lua.create_function(|_, ()| {
|
lua.create_function(|_, ()| {
|
||||||
let ports = xserial_core::transport::serial::SerialTransport::list_ports();
|
let ports = pipeview_core::transport::serial::SerialTransport::list_ports();
|
||||||
Ok(ports.into_iter().map(|p| p.port_name).collect::<Vec<_>>())
|
Ok(ports.into_iter().map(|p| p.port_name).collect::<Vec<_>>())
|
||||||
})?,
|
})?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
xserial.set("sleep", {
|
pipeview.set("sleep", {
|
||||||
let runtime = runtime.clone();
|
let runtime = runtime.clone();
|
||||||
lua.create_async_function(move |lua, ms: u64| {
|
lua.create_async_function(move |lua, ms: u64| {
|
||||||
let runtime = runtime.clone();
|
let runtime = runtime.clone();
|
||||||
@@ -116,7 +116,7 @@ pub fn register_with_manager(lua: &Lua, manager: SessionManager) -> LuaResult<()
|
|||||||
})?
|
})?
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
xserial.set("poll", {
|
pipeview.set("poll", {
|
||||||
let runtime = runtime.clone();
|
let runtime = runtime.clone();
|
||||||
lua.create_async_function(move |lua, limit_per_session: Option<usize>| {
|
lua.create_async_function(move |lua, limit_per_session: Option<usize>| {
|
||||||
let runtime = runtime.clone();
|
let runtime = runtime.clone();
|
||||||
@@ -124,7 +124,7 @@ pub fn register_with_manager(lua: &Lua, manager: SessionManager) -> LuaResult<()
|
|||||||
})?
|
})?
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
xserial.set(
|
pipeview.set(
|
||||||
"log",
|
"log",
|
||||||
lua.create_function(|_, msg: String| {
|
lua.create_function(|_, msg: String| {
|
||||||
tracing::info!("[lua] {}", msg);
|
tracing::info!("[lua] {}", msg);
|
||||||
@@ -132,7 +132,7 @@ pub fn register_with_manager(lua: &Lua, manager: SessionManager) -> LuaResult<()
|
|||||||
})?,
|
})?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
lua.globals().set("xserial", xserial)?;
|
lua.globals().set("pipeview", pipeview)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,9 +146,9 @@ mod tests {
|
|||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
register(&lua).unwrap();
|
register(&lua).unwrap();
|
||||||
|
|
||||||
let xserial: Table = lua.globals().get("xserial").unwrap();
|
let pipeview: Table = lua.globals().get("pipeview").unwrap();
|
||||||
for name in ["open", "list_ports", "sleep", "poll", "log"] {
|
for name in ["open", "list_ports", "sleep", "poll", "log"] {
|
||||||
let value: Value = xserial.get(name).unwrap();
|
let value: Value = pipeview.get(name).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(value, Value::Function(_)),
|
matches!(value, Value::Function(_)),
|
||||||
"{name} should be a function"
|
"{name} should be a function"
|
||||||
@@ -162,8 +162,8 @@ mod tests {
|
|||||||
let manager = SessionManager::new();
|
let manager = SessionManager::new();
|
||||||
register_with_manager(&lua, manager.clone()).unwrap();
|
register_with_manager(&lua, manager.clone()).unwrap();
|
||||||
|
|
||||||
let xserial: Table = lua.globals().get("xserial").unwrap();
|
let pipeview: Table = lua.globals().get("pipeview").unwrap();
|
||||||
let open: Value = xserial.get("open").unwrap();
|
let open: Value = pipeview.get("open").unwrap();
|
||||||
assert!(matches!(open, Value::Function(_)));
|
assert!(matches!(open, Value::Function(_)));
|
||||||
assert_eq!(manager.count(), 0);
|
assert_eq!(manager.count(), 0);
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ use std::time::Duration;
|
|||||||
use mlua::{Function, Lua, LuaSerdeExt, RegistryKey, UserData, UserDataMethods, Value, Variadic};
|
use mlua::{Function, Lua, LuaSerdeExt, RegistryKey, UserData, UserDataMethods, Value, Variadic};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use xserial_core::protocol::DecodedData;
|
use pipeview_core::protocol::DecodedData;
|
||||||
|
|
||||||
use crate::config::SessionConfig;
|
use crate::config::SessionConfig;
|
||||||
use crate::lua::LuaRuntime;
|
use crate::lua::LuaRuntime;
|
||||||
@@ -11,8 +11,8 @@ use tokio::sync::{Mutex, broadcast, mpsc};
|
|||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
use xserial_core::pipeline::{MultiPipeline, Pipeline};
|
use pipeview_core::pipeline::{MultiPipeline, Pipeline};
|
||||||
use xserial_core::transport::Connection;
|
use pipeview_core::transport::Connection;
|
||||||
|
|
||||||
use crate::cmd::SessionCmd;
|
use crate::cmd::SessionCmd;
|
||||||
use crate::config::SessionConfig;
|
use crate::config::SessionConfig;
|
||||||
@@ -499,7 +499,7 @@ async fn try_read(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_io_error(err: xserial_core::error::Error) -> std::io::Error {
|
fn to_io_error(err: pipeview_core::error::Error) -> std::io::Error {
|
||||||
io::Error::other(err.to_string())
|
io::Error::other(err.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,7 +510,7 @@ mod tests {
|
|||||||
|
|
||||||
fn tcp_config() -> SessionConfig {
|
fn tcp_config() -> SessionConfig {
|
||||||
SessionConfig {
|
SessionConfig {
|
||||||
transport: xserial_core::transport::TransportConfig::Tcp {
|
transport: pipeview_core::transport::TransportConfig::Tcp {
|
||||||
addr: "127.0.0.1:1".into(),
|
addr: "127.0.0.1:1".into(),
|
||||||
},
|
},
|
||||||
pipelines: vec![PipelineConfig {
|
pipelines: vec![PipelineConfig {
|
||||||
@@ -520,7 +520,7 @@ mod tests {
|
|||||||
max_line_len: 65536,
|
max_line_len: 65536,
|
||||||
},
|
},
|
||||||
decoder: DecoderConfig::Text {
|
decoder: DecoderConfig::Text {
|
||||||
encoding: xserial_core::protocol::text::TextEncoding::Utf8,
|
encoding: pipeview_core::protocol::text::TextEncoding::Utf8,
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -11,7 +11,7 @@ static INIT: Once = Once::new();
|
|||||||
fn init_tracing() {
|
fn init_tracing() {
|
||||||
INIT.call_once(|| {
|
INIT.call_once(|| {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter("xserial=trace")
|
.with_env_filter("pipeview=trace")
|
||||||
.try_init()
|
.try_init()
|
||||||
.ok();
|
.ok();
|
||||||
});
|
});
|
||||||
@@ -22,7 +22,7 @@ fn write_temp_lua(name: &str, script: &str) -> PathBuf {
|
|||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_nanos();
|
.as_nanos();
|
||||||
let path = std::env::temp_dir().join(format!("xserial_{name}_{nonce}.lua"));
|
let path = std::env::temp_dir().join(format!("pipeview_{name}_{nonce}.lua"));
|
||||||
fs::write(&path, script).unwrap();
|
fs::write(&path, script).unwrap();
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
@@ -47,18 +47,18 @@ fn hex_pipeline_lua() -> &'static str {
|
|||||||
"#
|
"#
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── xserial.sleep / xserial.log ──────────────────────────────────
|
// ── pipeview.sleep / pipeview.log ──────────────────────────────────
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn lua_sleep_and_log() {
|
async fn lua_sleep_and_log() {
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
xserial_client::lua::register(&lua).unwrap();
|
pipeview_client::lua::register(&lua).unwrap();
|
||||||
|
|
||||||
lua.load(
|
lua.load(
|
||||||
r#"
|
r#"
|
||||||
xserial.log("test start")
|
pipeview.log("test start")
|
||||||
xserial.sleep(50)
|
pipeview.sleep(50)
|
||||||
xserial.log("test end")
|
pipeview.log("test end")
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.exec_async()
|
.exec_async()
|
||||||
@@ -66,15 +66,15 @@ async fn lua_sleep_and_log() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── xserial.list_ports ───────────────────────────────────────────
|
// ── pipeview.list_ports ───────────────────────────────────────────
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn lua_list_ports() {
|
async fn lua_list_ports() {
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
xserial_client::lua::register(&lua).unwrap();
|
pipeview_client::lua::register(&lua).unwrap();
|
||||||
|
|
||||||
let result: Vec<String> = lua
|
let result: Vec<String> = lua
|
||||||
.load(r#"return xserial.list_ports()"#)
|
.load(r#"return pipeview.list_ports()"#)
|
||||||
.eval_async()
|
.eval_async()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -84,7 +84,7 @@ async fn lua_list_ports() {
|
|||||||
let _ = result;
|
let _ = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── xserial.open + session:send / session:read / session:close ────
|
// ── pipeview.open + session:send / session:read / session:close ────
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn lua_session_open_send_read_close() {
|
async fn lua_session_open_send_read_close() {
|
||||||
@@ -102,11 +102,11 @@ async fn lua_session_open_send_read_close() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
xserial_client::lua::register(&lua).unwrap();
|
pipeview_client::lua::register(&lua).unwrap();
|
||||||
|
|
||||||
let script = format!(
|
let script = format!(
|
||||||
r#"
|
r#"
|
||||||
local sess = xserial.open({{
|
local sess = pipeview.open({{
|
||||||
transport = {{ Tcp = {{ addr = "{}" }} }},
|
transport = {{ Tcp = {{ addr = "{}" }} }},
|
||||||
pipelines = {{{}}}
|
pipelines = {{{}}}
|
||||||
}})
|
}})
|
||||||
@@ -141,11 +141,11 @@ async fn lua_session_read_timeout() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
xserial_client::lua::register(&lua).unwrap();
|
pipeview_client::lua::register(&lua).unwrap();
|
||||||
|
|
||||||
let script = format!(
|
let script = format!(
|
||||||
r#"
|
r#"
|
||||||
local sess = xserial.open({{
|
local sess = pipeview.open({{
|
||||||
transport = {{ Tcp = {{ addr = "{}" }} }},
|
transport = {{ Tcp = {{ addr = "{}" }} }},
|
||||||
pipelines = {{{}}}
|
pipelines = {{{}}}
|
||||||
}})
|
}})
|
||||||
@@ -162,7 +162,7 @@ async fn lua_session_read_timeout() {
|
|||||||
lua.load(&script).exec_async().await.unwrap();
|
lua.load(&script).exec_async().await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── xserial.open with hex decoder ────────────────────────────────
|
// ── pipeview.open with hex decoder ────────────────────────────────
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn lua_session_hex_decoder() {
|
async fn lua_session_hex_decoder() {
|
||||||
@@ -176,11 +176,11 @@ async fn lua_session_hex_decoder() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
xserial_client::lua::register(&lua).unwrap();
|
pipeview_client::lua::register(&lua).unwrap();
|
||||||
|
|
||||||
let script = format!(
|
let script = format!(
|
||||||
r#"
|
r#"
|
||||||
local sess = xserial.open({{
|
local sess = pipeview.open({{
|
||||||
transport = {{ Tcp = {{ addr = "{}" }} }},
|
transport = {{ Tcp = {{ addr = "{}" }} }},
|
||||||
pipelines = {{{}}}
|
pipelines = {{{}}}
|
||||||
}})
|
}})
|
||||||
@@ -201,7 +201,7 @@ async fn lua_session_hex_decoder() {
|
|||||||
server.await.unwrap();
|
server.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── xserial.open with Lua framer + Lua decoder ───────────────────
|
// ── pipeview.open with Lua framer + Lua decoder ───────────────────
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn lua_session_custom_lua_pipeline() {
|
async fn lua_session_custom_lua_pipeline() {
|
||||||
@@ -259,11 +259,11 @@ async fn lua_session_custom_lua_pipeline() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
xserial_client::lua::register(&lua).unwrap();
|
pipeview_client::lua::register(&lua).unwrap();
|
||||||
|
|
||||||
let script = format!(
|
let script = format!(
|
||||||
r#"
|
r#"
|
||||||
local sess = xserial.open({{
|
local sess = pipeview.open({{
|
||||||
transport = {{ Tcp = {{ addr = "{}" }} }},
|
transport = {{ Tcp = {{ addr = "{}" }} }},
|
||||||
pipelines = {{
|
pipelines = {{
|
||||||
{{
|
{{
|
||||||
@@ -311,12 +311,12 @@ async fn lua_session_on_data_callback() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
xserial_client::lua::register(&lua).unwrap();
|
pipeview_client::lua::register(&lua).unwrap();
|
||||||
|
|
||||||
let script = format!(
|
let script = format!(
|
||||||
r#"
|
r#"
|
||||||
local received = {{}}
|
local received = {{}}
|
||||||
local sess = xserial.open({{
|
local sess = pipeview.open({{
|
||||||
transport = {{ Tcp = {{ addr = "{}" }} }},
|
transport = {{ Tcp = {{ addr = "{}" }} }},
|
||||||
pipelines = {{{}}}
|
pipelines = {{{}}}
|
||||||
}})
|
}})
|
||||||
@@ -327,8 +327,8 @@ async fn lua_session_on_data_callback() {
|
|||||||
|
|
||||||
for _ = 1, 50 do
|
for _ = 1, 50 do
|
||||||
if #received < 2 then
|
if #received < 2 then
|
||||||
xserial.poll(1)
|
pipeview.poll(1)
|
||||||
xserial.sleep(10)
|
pipeview.sleep(10)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -371,11 +371,11 @@ async fn lua_session_reconfigure() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
xserial_client::lua::register(&lua).unwrap();
|
pipeview_client::lua::register(&lua).unwrap();
|
||||||
|
|
||||||
let script = format!(
|
let script = format!(
|
||||||
r#"
|
r#"
|
||||||
local sess = xserial.open({{
|
local sess = pipeview.open({{
|
||||||
transport = {{ Tcp = {{ addr = "{}" }} }},
|
transport = {{ Tcp = {{ addr = "{}" }} }},
|
||||||
pipelines = {{{}}}
|
pipelines = {{{}}}
|
||||||
}})
|
}})
|
||||||
@@ -420,11 +420,11 @@ async fn lua_session_next_event_stream() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
xserial_client::lua::register(&lua).unwrap();
|
pipeview_client::lua::register(&lua).unwrap();
|
||||||
|
|
||||||
let script = format!(
|
let script = format!(
|
||||||
r#"
|
r#"
|
||||||
local sess = xserial.open({{
|
local sess = pipeview.open({{
|
||||||
transport = {{ Tcp = {{ addr = "{}" }} }},
|
transport = {{ Tcp = {{ addr = "{}" }} }},
|
||||||
pipelines = {{{}}}
|
pipelines = {{{}}}
|
||||||
}})
|
}})
|
||||||
@@ -450,7 +450,7 @@ async fn lua_session_next_event_stream() {
|
|||||||
server.await.unwrap();
|
server.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── session:off + xserial.poll ───────────────────────────────────
|
// ── session:off + pipeview.poll ───────────────────────────────────
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn lua_session_off_stops_future_callbacks() {
|
async fn lua_session_off_stops_future_callbacks() {
|
||||||
@@ -466,12 +466,12 @@ async fn lua_session_off_stops_future_callbacks() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
xserial_client::lua::register(&lua).unwrap();
|
pipeview_client::lua::register(&lua).unwrap();
|
||||||
|
|
||||||
let script = format!(
|
let script = format!(
|
||||||
r#"
|
r#"
|
||||||
local received = {{}}
|
local received = {{}}
|
||||||
local sess = xserial.open({{
|
local sess = pipeview.open({{
|
||||||
transport = {{ Tcp = {{ addr = "{}" }} }},
|
transport = {{ Tcp = {{ addr = "{}" }} }},
|
||||||
pipelines = {{{}}}
|
pipelines = {{{}}}
|
||||||
}})
|
}})
|
||||||
@@ -482,8 +482,8 @@ async fn lua_session_off_stops_future_callbacks() {
|
|||||||
|
|
||||||
for _ = 1, 50 do
|
for _ = 1, 50 do
|
||||||
if #received == 0 then
|
if #received == 0 then
|
||||||
xserial.poll(1)
|
pipeview.poll(1)
|
||||||
xserial.sleep(10)
|
pipeview.sleep(10)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -491,8 +491,8 @@ async fn lua_session_off_stops_future_callbacks() {
|
|||||||
assert(received[1] == "text:first")
|
assert(received[1] == "text:first")
|
||||||
assert(sess:off(token) == true)
|
assert(sess:off(token) == true)
|
||||||
|
|
||||||
xserial.sleep(250)
|
pipeview.sleep(250)
|
||||||
xserial.poll(10)
|
pipeview.poll(10)
|
||||||
|
|
||||||
assert(#received == 1, "callback should not fire after off")
|
assert(#received == 1, "callback should not fire after off")
|
||||||
assert(sess:off(token) == false)
|
assert(sess:off(token) == false)
|
||||||
@@ -3,11 +3,11 @@ use std::time::Duration;
|
|||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use xserial_client::SessionManager;
|
use pipeview_client::SessionManager;
|
||||||
use xserial_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig};
|
use pipeview_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig};
|
||||||
use xserial_client::session::{Session, SessionEvent};
|
use pipeview_client::session::{Session, SessionEvent};
|
||||||
use xserial_core::protocol::DecodedData;
|
use pipeview_core::protocol::DecodedData;
|
||||||
use xserial_core::transport::TransportConfig;
|
use pipeview_core::transport::TransportConfig;
|
||||||
|
|
||||||
fn tcp_config(addr: String) -> SessionConfig {
|
fn tcp_config(addr: String) -> SessionConfig {
|
||||||
SessionConfig {
|
SessionConfig {
|
||||||
@@ -19,7 +19,7 @@ fn tcp_config(addr: String) -> SessionConfig {
|
|||||||
max_line_len: 1024 * 1024,
|
max_line_len: 1024 * 1024,
|
||||||
},
|
},
|
||||||
decoder: DecoderConfig::Text {
|
decoder: DecoderConfig::Text {
|
||||||
encoding: xserial_core::protocol::text::TextEncoding::Utf8,
|
encoding: pipeview_core::protocol::text::TextEncoding::Utf8,
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
history_limit: 100,
|
history_limit: 100,
|
||||||
@@ -113,7 +113,7 @@ async fn session_multi_pipeline_text_and_hex() {
|
|||||||
uppercase: false,
|
uppercase: false,
|
||||||
separator: " ".into(),
|
separator: " ".into(),
|
||||||
bytes_per_group: 1,
|
bytes_per_group: 1,
|
||||||
endian: xserial_core::protocol::Endian::Big,
|
endian: pipeview_core::protocol::Endian::Big,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ async fn session_reconfigure_changes_pipeline() {
|
|||||||
uppercase: true,
|
uppercase: true,
|
||||||
separator: " ".into(),
|
separator: " ".into(),
|
||||||
bytes_per_group: 1,
|
bytes_per_group: 1,
|
||||||
endian: xserial_core::protocol::Endian::Big,
|
endian: pipeview_core::protocol::Endian::Big,
|
||||||
};
|
};
|
||||||
|
|
||||||
handle.reconfigure(new_cfg).await.unwrap();
|
handle.reconfigure(new_cfg).await.unwrap();
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xserial-core"
|
name = "pipeview-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
@@ -61,11 +61,11 @@ pub struct PipelineResult {
|
|||||||
/// Manages multiple [`Pipeline`]s, feeding the same byte stream to all of them.
|
/// Manages multiple [`Pipeline`]s, feeding the same byte stream to all of them.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use xserial_core::pipeline::{MultiPipeline, Pipeline};
|
/// use pipeview_core::pipeline::{MultiPipeline, Pipeline};
|
||||||
/// use xserial_core::frame::line::{LineFramer, LineConfig};
|
/// use pipeview_core::frame::line::{LineFramer, LineConfig};
|
||||||
/// use xserial_core::frame::fixed::FixedLengthFramer;
|
/// use pipeview_core::frame::fixed::FixedLengthFramer;
|
||||||
/// use xserial_core::protocol::text::{TextDecoder, TextEncoding};
|
/// use pipeview_core::protocol::text::{TextDecoder, TextEncoding};
|
||||||
/// use xserial_core::protocol::hex::{HexDecoder, HexConfig};
|
/// use pipeview_core::protocol::hex::{HexDecoder, HexConfig};
|
||||||
///
|
///
|
||||||
/// let mut mp = MultiPipeline::new();
|
/// let mut mp = MultiPipeline::new();
|
||||||
/// mp.add(
|
/// mp.add(
|
||||||
@@ -4,7 +4,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|||||||
use tokio::net::{TcpListener, UdpSocket};
|
use tokio::net::{TcpListener, UdpSocket};
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
|
||||||
use xserial_core::frame::{
|
use pipeview_core::frame::{
|
||||||
Endian, Framer,
|
Endian, Framer,
|
||||||
cobs::CobsFramer,
|
cobs::CobsFramer,
|
||||||
cobs::cobs_encode as raw_cobs_encode,
|
cobs::cobs_encode as raw_cobs_encode,
|
||||||
@@ -13,17 +13,17 @@ use xserial_core::frame::{
|
|||||||
line::{LineConfig, LineFramer},
|
line::{LineConfig, LineFramer},
|
||||||
mixed::{MixedTextPlotConfig as MixedFramerConfig, MixedTextPlotFramer},
|
mixed::{MixedTextPlotConfig as MixedFramerConfig, MixedTextPlotFramer},
|
||||||
};
|
};
|
||||||
use xserial_core::protocol::{
|
use pipeview_core::protocol::{
|
||||||
DecodedData, ProtocolDecoder,
|
DecodedData, ProtocolDecoder,
|
||||||
hex::{HexConfig, HexDecoder},
|
hex::{HexConfig, HexDecoder},
|
||||||
mixed::{MIXED_PLOT_ESCAPE, MIXED_PLOT_MARKER, MixedTextPlotConfig, MixedTextPlotDecoder},
|
mixed::{MIXED_PLOT_ESCAPE, MIXED_PLOT_MARKER, MixedTextPlotConfig, MixedTextPlotDecoder},
|
||||||
plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType},
|
plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType},
|
||||||
text::{TextDecoder, TextEncoding},
|
text::{TextDecoder, TextEncoding},
|
||||||
};
|
};
|
||||||
use xserial_core::transport::serial::{
|
use pipeview_core::transport::serial::{
|
||||||
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits,
|
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits,
|
||||||
};
|
};
|
||||||
use xserial_core::transport::{Connection, TransportConfig, TransportType};
|
use pipeview_core::transport::{Connection, TransportConfig, TransportType};
|
||||||
|
|
||||||
const TEST_TIMEOUT: Duration = Duration::from_secs(5);
|
const TEST_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xserial-gui"
|
name = "pipeview-gui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "xserial-gui"
|
name = "pipeview-gui"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
xserial-core = { path = "../xserial-core" }
|
pipeview-core = { path = "../pipeview-core" }
|
||||||
xserial-client = { path = "../xserial-client" }
|
pipeview-client = { path = "../pipeview-client" }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
egui = { workspace = true }
|
egui = { workspace = true }
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
@@ -10,11 +10,11 @@ use crate::perf::{DrainStats, GuiProfiler, GuiSnapshot};
|
|||||||
use crate::shortcuts::{self, default_bindings};
|
use crate::shortcuts::{self, default_bindings};
|
||||||
use crate::ui_fonts::{self, FontCandidate, FontChoice, UiFontSettings};
|
use crate::ui_fonts::{self, FontCandidate, FontChoice, UiFontSettings};
|
||||||
use egui::{Color32, Layout, Panel, Pos2, Rect, TextEdit, UiBuilder};
|
use egui::{Color32, Layout, Panel, Pos2, Rect, TextEdit, UiBuilder};
|
||||||
use xserial_client::SessionManager;
|
use pipeview_client::SessionManager;
|
||||||
use xserial_client::config::SessionConfig;
|
use pipeview_client::config::SessionConfig;
|
||||||
use xserial_client::session::SessionEvent;
|
use pipeview_client::session::SessionEvent;
|
||||||
use xserial_core::protocol::DecodedData;
|
use pipeview_core::protocol::DecodedData;
|
||||||
use xserial_core::transport::TransportConfig;
|
use pipeview_core::transport::TransportConfig;
|
||||||
|
|
||||||
const DATA_REPAINT_INTERVAL: Duration = Duration::from_millis(33);
|
const DATA_REPAINT_INTERVAL: Duration = Duration::from_millis(33);
|
||||||
|
|
||||||
@@ -851,7 +851,7 @@ impl XserialApp {
|
|||||||
fn render_top_bar(&mut self, ui: &mut egui::Ui) {
|
fn render_top_bar(&mut self, ui: &mut egui::Ui) {
|
||||||
Panel::top("top_bar").show_inside(ui, |ui| {
|
Panel::top("top_bar").show_inside(ui, |ui| {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
ui.heading("xserial");
|
ui.heading("pipeview");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
// ui.label(format!(
|
// ui.label(format!(
|
||||||
// "Fonts: {} + {} {:.1} pt",
|
// "Fonts: {} + {} {:.1} pt",
|
||||||
@@ -1434,8 +1434,8 @@ fn build_payload(tab: &SessionTab) -> Result<Option<Vec<u8>>, String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::transport_summary;
|
use super::transport_summary;
|
||||||
use xserial_core::transport::TransportConfig;
|
use pipeview_core::transport::TransportConfig;
|
||||||
use xserial_core::transport::serial::{
|
use pipeview_core::transport::serial::{
|
||||||
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits,
|
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use xserial_client::config::SessionConfig;
|
use pipeview_client::config::SessionConfig;
|
||||||
|
|
||||||
const GUI_STATE_FILE_NAME: &str = "gui-state.json";
|
const GUI_STATE_FILE_NAME: &str = "gui-state.json";
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ fn gui_state_path() -> PathBuf {
|
|||||||
gui_state_path_for_os_and_env(env::consts::OS, |key| env::var(key).ok())
|
gui_state_path_for_os_and_env(env::consts::OS, |key| env::var(key).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the xserial configuration directory (the parent of gui-state.json).
|
/// Returns the pipeview configuration directory (the parent of gui-state.json).
|
||||||
pub fn config_dir() -> PathBuf {
|
pub fn config_dir() -> PathBuf {
|
||||||
gui_state_path()
|
gui_state_path()
|
||||||
.parent()
|
.parent()
|
||||||
@@ -67,12 +67,12 @@ fn gui_state_path_for_os_and_env(os: &str, get_env: impl Fn(&str) -> Option<Stri
|
|||||||
"windows" => {
|
"windows" => {
|
||||||
if let Some(path) = get_env("APPDATA").filter(|path| !path.trim().is_empty()) {
|
if let Some(path) = get_env("APPDATA").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(path)
|
return PathBuf::from(path)
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(GUI_STATE_FILE_NAME);
|
.join(GUI_STATE_FILE_NAME);
|
||||||
}
|
}
|
||||||
if let Some(path) = get_env("LOCALAPPDATA").filter(|path| !path.trim().is_empty()) {
|
if let Some(path) = get_env("LOCALAPPDATA").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(path)
|
return PathBuf::from(path)
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(GUI_STATE_FILE_NAME);
|
.join(GUI_STATE_FILE_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,20 +81,20 @@ fn gui_state_path_for_os_and_env(os: &str, get_env: impl Fn(&str) -> Option<Stri
|
|||||||
return PathBuf::from(home)
|
return PathBuf::from(home)
|
||||||
.join("Library")
|
.join("Library")
|
||||||
.join("Application Support")
|
.join("Application Support")
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(GUI_STATE_FILE_NAME);
|
.join(GUI_STATE_FILE_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(path) = get_env("XDG_CONFIG_HOME").filter(|path| !path.trim().is_empty()) {
|
if let Some(path) = get_env("XDG_CONFIG_HOME").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(path)
|
return PathBuf::from(path)
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(GUI_STATE_FILE_NAME);
|
.join(GUI_STATE_FILE_NAME);
|
||||||
}
|
}
|
||||||
if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) {
|
if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(home)
|
return PathBuf::from(home)
|
||||||
.join(".config")
|
.join(".config")
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(GUI_STATE_FILE_NAME);
|
.join(GUI_STATE_FILE_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ const fn default_true() -> bool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use xserial_core::transport::TransportConfig;
|
use pipeview_core::transport::TransportConfig;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn windows_gui_state_path_uses_appdata() {
|
fn windows_gui_state_path_uses_appdata() {
|
||||||
@@ -135,7 +135,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
path,
|
path,
|
||||||
PathBuf::from(r"C:\Users\Test\AppData\Roaming")
|
PathBuf::from(r"C:\Users\Test\AppData\Roaming")
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(GUI_STATE_FILE_NAME)
|
.join(GUI_STATE_FILE_NAME)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ mod tests {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_nanos();
|
.as_nanos();
|
||||||
let dir = env::temp_dir().join(format!("xserial-gui-state-{unique}"));
|
let dir = env::temp_dir().join(format!("pipeview-gui-state-{unique}"));
|
||||||
let path = dir.join(GUI_STATE_FILE_NAME);
|
let path = dir.join(GUI_STATE_FILE_NAME);
|
||||||
let state = PersistedGuiState {
|
let state = PersistedGuiState {
|
||||||
sessions: vec![SessionConfig {
|
sessions: vec![SessionConfig {
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use egui_plot::PlotBounds;
|
use egui_plot::PlotBounds;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use xserial_client::RingBuffer;
|
use pipeview_client::RingBuffer;
|
||||||
use xserial_client::event::DecodedEntry;
|
use pipeview_client::event::DecodedEntry;
|
||||||
use xserial_core::protocol::DecodedData;
|
use pipeview_core::protocol::DecodedData;
|
||||||
use xserial_core::protocol::plot::{PlotFormat, PlotFrame};
|
use pipeview_core::protocol::plot::{PlotFormat, PlotFrame};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum LineDirection {
|
pub enum LineDirection {
|
||||||
@@ -716,9 +716,9 @@ impl PlotBuffer {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use xserial_client::event::DecodedEntry;
|
use pipeview_client::event::DecodedEntry;
|
||||||
use xserial_core::protocol::DecodedData;
|
use pipeview_core::protocol::DecodedData;
|
||||||
use xserial_core::protocol::plot::{PlotFrame, SampleType};
|
use pipeview_core::protocol::plot::{PlotFrame, SampleType};
|
||||||
|
|
||||||
fn plot_entry() -> DecodedEntry {
|
fn plot_entry() -> DecodedEntry {
|
||||||
DecodedEntry {
|
DecodedEntry {
|
||||||
@@ -6,8 +6,8 @@ use std::thread;
|
|||||||
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use xserial_client::event::DecodedEntry;
|
use pipeview_client::event::DecodedEntry;
|
||||||
use xserial_core::protocol::DecodedData;
|
use pipeview_core::protocol::DecodedData;
|
||||||
|
|
||||||
/// Manages background file logging via a dedicated writer thread.
|
/// Manages background file logging via a dedicated writer thread.
|
||||||
///
|
///
|
||||||
@@ -30,7 +30,7 @@ impl LogWriter {
|
|||||||
let mut writer = BufWriter::with_capacity(1024, file);
|
let mut writer = BufWriter::with_capacity(1024, file);
|
||||||
let (sender, receiver) = mpsc::channel::<String>();
|
let (sender, receiver) = mpsc::channel::<String>();
|
||||||
|
|
||||||
let thread_name = format!("xserial-log-{}", path.display());
|
let thread_name = format!("pipeview-log-{}", path.display());
|
||||||
thread::Builder::new().name(thread_name).spawn(move || {
|
thread::Builder::new().name(thread_name).spawn(move || {
|
||||||
while let Ok(line) = receiver.recv() {
|
while let Ok(line) = receiver.recv() {
|
||||||
if writeln!(writer, "{line}").is_err() {
|
if writeln!(writer, "{line}").is_err() {
|
||||||
@@ -7,7 +7,7 @@ mod perf;
|
|||||||
mod shortcuts;
|
mod shortcuts;
|
||||||
mod ui_fonts;
|
mod ui_fonts;
|
||||||
|
|
||||||
use xserial_client::SessionManager;
|
use pipeview_client::SessionManager;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
@@ -21,7 +21,7 @@ fn main() {
|
|||||||
let rx = mgr.subscribe();
|
let rx = mgr.subscribe();
|
||||||
|
|
||||||
let _ = eframe::run_native(
|
let _ = eframe::run_native(
|
||||||
"xserial",
|
"pipeview",
|
||||||
eframe::NativeOptions::default(),
|
eframe::NativeOptions::default(),
|
||||||
Box::new(|cc| Ok(Box::new(app::XserialApp::new(mgr, rx, cc.egui_ctx.clone())))),
|
Box::new(|cc| Ok(Box::new(app::XserialApp::new(mgr, rx, cc.egui_ctx.clone())))),
|
||||||
);
|
);
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use egui::{ComboBox, DragValue, ScrollArea, TextEdit, Ui};
|
use egui::{ComboBox, DragValue, ScrollArea, TextEdit, Ui};
|
||||||
use xserial_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig};
|
use pipeview_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig};
|
||||||
use xserial_core::protocol::Endian;
|
use pipeview_core::protocol::Endian;
|
||||||
use xserial_core::protocol::plot::{PlotFormat, SampleType};
|
use pipeview_core::protocol::plot::{PlotFormat, SampleType};
|
||||||
use xserial_core::protocol::text::TextEncoding;
|
use pipeview_core::protocol::text::TextEncoding;
|
||||||
use xserial_core::transport::TransportConfig;
|
use pipeview_core::transport::TransportConfig;
|
||||||
use xserial_core::transport::serial::{
|
use pipeview_core::transport::serial::{
|
||||||
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits, SerialTransport,
|
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits, SerialTransport,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ impl GuiProfiler {
|
|||||||
|
|
||||||
let repaints = self.repaint_counter.take();
|
let repaints = self.repaint_counter.take();
|
||||||
info!(
|
info!(
|
||||||
target: "xserial_gui::perf",
|
target: "pipeview_gui::perf",
|
||||||
frames = self.interval_stats.frames.count,
|
frames = self.interval_stats.frames.count,
|
||||||
frame_avg_ms = self.interval_stats.frames.avg_ms(),
|
frame_avg_ms = self.interval_stats.frames.avg_ms(),
|
||||||
frame_max_ms = self.interval_stats.frames.max_ms(),
|
frame_max_ms = self.interval_stats.frames.max_ms(),
|
||||||
@@ -358,12 +358,12 @@ fn font_settings_path_for_os_and_env(
|
|||||||
"windows" => {
|
"windows" => {
|
||||||
if let Some(path) = get_env("APPDATA").filter(|path| !path.trim().is_empty()) {
|
if let Some(path) = get_env("APPDATA").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(path)
|
return PathBuf::from(path)
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(FONT_SETTINGS_FILE_NAME);
|
.join(FONT_SETTINGS_FILE_NAME);
|
||||||
}
|
}
|
||||||
if let Some(path) = get_env("LOCALAPPDATA").filter(|path| !path.trim().is_empty()) {
|
if let Some(path) = get_env("LOCALAPPDATA").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(path)
|
return PathBuf::from(path)
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(FONT_SETTINGS_FILE_NAME);
|
.join(FONT_SETTINGS_FILE_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,20 +372,20 @@ fn font_settings_path_for_os_and_env(
|
|||||||
return PathBuf::from(home)
|
return PathBuf::from(home)
|
||||||
.join("Library")
|
.join("Library")
|
||||||
.join("Application Support")
|
.join("Application Support")
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(FONT_SETTINGS_FILE_NAME);
|
.join(FONT_SETTINGS_FILE_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(path) = get_env("XDG_CONFIG_HOME").filter(|path| !path.trim().is_empty()) {
|
if let Some(path) = get_env("XDG_CONFIG_HOME").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(path)
|
return PathBuf::from(path)
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(FONT_SETTINGS_FILE_NAME);
|
.join(FONT_SETTINGS_FILE_NAME);
|
||||||
}
|
}
|
||||||
if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) {
|
if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(home)
|
return PathBuf::from(home)
|
||||||
.join(".config")
|
.join(".config")
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(FONT_SETTINGS_FILE_NAME);
|
.join(FONT_SETTINGS_FILE_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -591,7 +591,7 @@ mod tests {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_nanos();
|
.as_nanos();
|
||||||
let dir = env::temp_dir().join(format!("xserial-gui-font-settings-{unique}"));
|
let dir = env::temp_dir().join(format!("pipeview-gui-font-settings-{unique}"));
|
||||||
let path = dir.join("gui-fonts.json");
|
let path = dir.join("gui-fonts.json");
|
||||||
let settings = UiFontSettings {
|
let settings = UiFontSettings {
|
||||||
primary_choice: FontChoice::System(String::from("primary")),
|
primary_choice: FontChoice::System(String::from("primary")),
|
||||||
@@ -622,7 +622,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
path,
|
path,
|
||||||
PathBuf::from("/tmp/xdg-config")
|
PathBuf::from("/tmp/xdg-config")
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(FONT_SETTINGS_FILE_NAME)
|
.join(FONT_SETTINGS_FILE_NAME)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -636,7 +636,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
path,
|
path,
|
||||||
PathBuf::from(r"C:\Users\Test\AppData\Roaming")
|
PathBuf::from(r"C:\Users\Test\AppData\Roaming")
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(FONT_SETTINGS_FILE_NAME)
|
.join(FONT_SETTINGS_FILE_NAME)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -652,7 +652,7 @@ mod tests {
|
|||||||
PathBuf::from("/Users/tester")
|
PathBuf::from("/Users/tester")
|
||||||
.join("Library")
|
.join("Library")
|
||||||
.join("Application Support")
|
.join("Application Support")
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(FONT_SETTINGS_FILE_NAME)
|
.join(FONT_SETTINGS_FILE_NAME)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xserial-tui"
|
name = "pipeview-tui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "xserial-tui"
|
name = "pipeview-tui"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
xserial-core = { path = "../xserial-core" }
|
pipeview-core = { path = "../pipeview-core" }
|
||||||
xserial-client = { path = "../xserial-client" }
|
pipeview-client = { path = "../pipeview-client" }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
@@ -3,12 +3,12 @@ use std::{io, time::Duration};
|
|||||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use ratatui::DefaultTerminal;
|
use ratatui::DefaultTerminal;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use xserial_client::{
|
use pipeview_client::{
|
||||||
DecoderConfig, FramerConfig, PipelineConfig, SessionConfig, SessionEvent, SessionId,
|
DecoderConfig, FramerConfig, PipelineConfig, SessionConfig, SessionEvent, SessionId,
|
||||||
SessionManager,
|
SessionManager,
|
||||||
};
|
};
|
||||||
use xserial_core::protocol::DecodedData;
|
use pipeview_core::protocol::DecodedData;
|
||||||
use xserial_core::transport::{
|
use pipeview_core::transport::{
|
||||||
TransportConfig, TransportType,
|
TransportConfig, TransportType,
|
||||||
serial::{SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits, SerialTransport},
|
serial::{SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits, SerialTransport},
|
||||||
};
|
};
|
||||||
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use xserial_client::SessionConfig;
|
use pipeview_client::SessionConfig;
|
||||||
|
|
||||||
const TUI_STATE_FILE_NAME: &str = "tui-state.json";
|
const TUI_STATE_FILE_NAME: &str = "tui-state.json";
|
||||||
|
|
||||||
@@ -49,12 +49,12 @@ fn state_path_for_os_and_env(os: &str, get_env: impl Fn(&str) -> Option<String>)
|
|||||||
"windows" => {
|
"windows" => {
|
||||||
if let Some(path) = get_env("APPDATA").filter(|path| !path.trim().is_empty()) {
|
if let Some(path) = get_env("APPDATA").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(path)
|
return PathBuf::from(path)
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(TUI_STATE_FILE_NAME);
|
.join(TUI_STATE_FILE_NAME);
|
||||||
}
|
}
|
||||||
if let Some(path) = get_env("LOCALAPPDATA").filter(|path| !path.trim().is_empty()) {
|
if let Some(path) = get_env("LOCALAPPDATA").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(path)
|
return PathBuf::from(path)
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(TUI_STATE_FILE_NAME);
|
.join(TUI_STATE_FILE_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,20 +63,20 @@ fn state_path_for_os_and_env(os: &str, get_env: impl Fn(&str) -> Option<String>)
|
|||||||
return PathBuf::from(home)
|
return PathBuf::from(home)
|
||||||
.join("Library")
|
.join("Library")
|
||||||
.join("Application Support")
|
.join("Application Support")
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(TUI_STATE_FILE_NAME);
|
.join(TUI_STATE_FILE_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(path) = get_env("XDG_CONFIG_HOME").filter(|path| !path.trim().is_empty()) {
|
if let Some(path) = get_env("XDG_CONFIG_HOME").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(path)
|
return PathBuf::from(path)
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(TUI_STATE_FILE_NAME);
|
.join(TUI_STATE_FILE_NAME);
|
||||||
}
|
}
|
||||||
if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) {
|
if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) {
|
||||||
return PathBuf::from(home)
|
return PathBuf::from(home)
|
||||||
.join(".config")
|
.join(".config")
|
||||||
.join("xserial")
|
.join("pipeview")
|
||||||
.join(TUI_STATE_FILE_NAME);
|
.join(TUI_STATE_FILE_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use xserial_client::{DecodedEntry, RingBuffer};
|
use pipeview_client::{DecodedEntry, RingBuffer};
|
||||||
use xserial_core::protocol::DecodedData;
|
use pipeview_core::protocol::DecodedData;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum LineDirection {
|
pub enum LineDirection {
|
||||||
@@ -10,7 +10,7 @@ use crossterm::{
|
|||||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
||||||
};
|
};
|
||||||
use ratatui::{Terminal, backend::CrosstermBackend};
|
use ratatui::{Terminal, backend::CrosstermBackend};
|
||||||
use xserial_client::SessionManager;
|
use pipeview_client::SessionManager;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
@@ -100,7 +100,7 @@ fn render_main(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
app.help_text(),
|
app.help_text(),
|
||||||
app.notice().unwrap_or("")
|
app.notice().unwrap_or("")
|
||||||
))
|
))
|
||||||
.block(Block::default().borders(Borders::ALL).title("xserial-tui"))
|
.block(Block::default().borders(Borders::ALL).title("pipeview-tui"))
|
||||||
.wrap(Wrap { trim: false }),
|
.wrap(Wrap { trim: false }),
|
||||||
area,
|
area,
|
||||||
);
|
);
|
||||||
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 pipeview 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()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""TCP server that sends plot waveform frames or text lines for xserial GUI."""
|
"""TCP server that sends plot waveform frames or text lines for pipeview GUI."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import math
|
import math
|
||||||
@@ -116,7 +116,7 @@ def is_disconnect_error(err: OSError) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(description="xserial plot test server")
|
parser = argparse.ArgumentParser(description="pipeview plot test server")
|
||||||
parser.add_argument("--host", default="127.0.0.1")
|
parser.add_argument("--host", default="127.0.0.1")
|
||||||
parser.add_argument("--port", type=int, default=8080)
|
parser.add_argument("--port", type=int, default=8080)
|
||||||
parser.add_argument("--channels", type=int, default=1)
|
parser.add_argument("--channels", type=int, default=1)
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ int main(int argc, char **argv) {
|
|||||||
double frame_interval = (double)args.samples_per_channel / args.rate;
|
double frame_interval = (double)args.samples_per_channel / args.rate;
|
||||||
double frame_rate = args.rate / (double)args.samples_per_channel;
|
double frame_rate = args.rate / (double)args.samples_per_channel;
|
||||||
|
|
||||||
printf("═══ xserial Serial Plot Generator ═══\n");
|
printf("═══ pipeview Serial Plot Generator ═══\n");
|
||||||
printf("串口: %s @ %d baud\n", args.port, args.baudrate);
|
printf("串口: %s @ %d baud\n", args.port, args.baudrate);
|
||||||
printf("通道: %u 格式: %s\n", args.channels, format_name);
|
printf("通道: %u 格式: %s\n", args.channels, format_name);
|
||||||
printf("频率: %.1f Hz 振幅: %.1f\n", args.freq, args.amp);
|
printf("频率: %.1f Hz 振幅: %.1f\n", args.freq, args.amp);
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ int main(int argc, char **argv) {
|
|||||||
|
|
||||||
double interval = 1.0 / args.rate;
|
double interval = 1.0 / args.rate;
|
||||||
|
|
||||||
printf("═══ xserial Plain Text Generator ═══\n");
|
printf("═══ pipeview Plain Text Generator ═══\n");
|
||||||
printf("串口: %s @ %d baud\n", args.port, args.baudrate);
|
printf("串口: %s @ %d baud\n", args.port, args.baudrate);
|
||||||
printf("通道: %u 振幅: %.1f 频率: %.1f Hz\n", args.channels, args.amp, args.freq);
|
printf("通道: %u 振幅: %.1f 频率: %.1f Hz\n", args.channels, args.amp, args.freq);
|
||||||
printf("速率: %.0f lines/sec (间隔 %.3f ms)\n", args.rate, interval * 1000.0);
|
printf("速率: %.0f lines/sec (间隔 %.3f ms)\n", args.rate, interval * 1000.0);
|
||||||
|
|||||||
Reference in New Issue
Block a user