diff --git a/.gitignore b/.gitignore index 4ac97ad..74d8896 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ # Compiled binaries in tools/ tools/test_plot_c tools/test_text_c + +# Python +__pycache__/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 3953624..0000000 --- a/AGENTS.md +++ /dev/null @@ -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//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). diff --git a/Cargo.lock b/Cargo.lock index f057efe..0cf7aea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3262,6 +3262,74 @@ dependencies = [ "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]] name = "pkg-config" version = "0.3.33" @@ -5750,74 +5818,6 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 6ba7d72..b11d33b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [workspace] resolver = "2" members = [ - "crates/xserial-core", - "crates/xserial-client", - "crates/xserial-tui", - "crates/xserial-gui", + "crates/pipeview-core", + "crates/pipeview-client", + "crates/pipeview-tui", + "crates/pipeview-gui", ] [workspace.dependencies] diff --git a/README.md b/README.md index b26bc6a..4dede8f 100644 --- a/README.md +++ b/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) │ -│ 面板: sidebar, console, 占位实现 │ -│ hex_view, plot_view, │ -│ config │ -├─────────────────────────────────────────────────────┤ -│ xserial-client │ -│ SessionManager · Session · Config · History │ -│ Lua Runtime (mlua/LuaJIT) │ -│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ -│ │ LuaFramer│ │LuaDecoder│ │ Lua Session API │ │ -│ └──────────┘ └──────────┘ └──────────────────┘ │ -├─────────────────────────────────────────────────────┤ -│ xserial-core │ -│ Transport ──▶ Frame ──▶ Protocol ──▶ Pipeline │ -│ (Serial, (Line, (Text, Hex, (MultiPipeline) │ -│ TCP, UDP) Fixed, Plot, │ -│ Length, MixedTextPlot) │ -│ Cobs, │ -│ MixedTextPlot) │ -└─────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────┐ +│ pipeview-gui (egui) pipeview-tui (ratatui) │ +├──────────────────────────────────────────────────┤ +│ pipeview-client │ +│ SessionManager · Session · Config · History │ +│ Lua Runtime (mlua / LuaJIT) │ +├──────────────────────────────────────────────────┤ +│ pipeview-core │ +│ Transport ──▶ Frame ──▶ Protocol ──▶ Pipeline │ +└──────────────────────────────────────────────────┘ ``` | Crate | 类型 | 职责 | |-------|------|------| -| `xserial-core` | library | 传输层、分帧器、协议解码器、Pipeline | -| `xserial-client` | library | 会话管理、配置持久化、事件分发、历史缓冲、Lua 运行时 | -| `xserial-gui` | binary | egui 桌面应用(主要入口) | -| `xserial-tui` | binary | ratatui 终端应用(占位,未达功能对等) | +| `pipeview-core` | library | 传输层(Serial/TCP/UDP)、分帧器(Line/Fixed/Length/COBS/Mixed/Lua)、协议解码器(Text/Hex/Plot)、MultiPipeline | +| `pipeview-client` | library | Session 生命周期管理、SessionManager、事件广播(tokio broadcast)、RingBuffer 历史、Lua 运行时及会话 API | +| `pipeview-gui` | binary | egui 桌面应用,包含 sidebar/config/console/text/hex/plot 面板、键盘快捷键、字体管理、性能分析 | +| `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 + → ... +``` + +每条管线独立分帧、解码,互不干扰。只有成功解码的管线产生输出。 --- -## 构建 +## 配置与持久化 -**前置条件**: - -- **Rust ≥ 1.85**(workspace 使用 `edition = "2024"`) -- **Linux**: `libudev-dev`(`serialport` 依赖) -- **所有平台**: C 编译器(`mlua` 使用 `vendored` 特性从源码编译 LuaJIT) - -```bash -# 编译检查 -cargo check --workspace - -# 运行测试(~266 个) -cargo test --workspace - -# 格式化 + Clippy -cargo fmt --all -cargo clippy --workspace --all-targets -- -D warnings -``` - ---- - -## 运行 - -### GUI(主要入口) - -```bash -cargo run -p xserial-gui -``` - -### TUI(占位实现) - -```bash -cargo run -p xserial-tui -``` - ---- - -## 功能 - -### 传输层 - -| 类型 | 配置项 | -|------|--------| -| **Serial** | 端口名、波特率、数据位 (5/6/7/8)、校验位 (None/Odd/Even)、停止位 (1/2)、流控 (None/Software/Hardware)、DTR、RTS | -| **TCP** | 目标地址 `host:port` | -| **UDP** | 绑定地址 `host:port`,可选远端地址 | - -DTR/RTS 控制仅在 Serial 连接时可用。 - -### 分帧器 - -| 分帧器 | 说明 | 配置 | -|--------|------|------| -| **Line** | 按换行符 `\n` 分割,可选去除 `\r` | `strip_cr`, `max_line_len` | -| **Fixed** | 固定字节数为一帧 | `frame_len` | -| **Length** | 长度前缀协议帧 | `len_bytes` (1/2/4/8), `endian`, `length_includes_self`, `max_payload` | -| **Cobs** | [COBS](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) 编码,`0x00` 分隔 | `max_frame` | -| **MixedTextPlot** | 单连接混合文本行 + COBS plot 帧 | `strip_cr`, `max_line_len`, `max_plot_frame` | -| **Lua** | 用户自定义 Lua 脚本 | `script_path` | - -### 协议解码器 - -| 解码器 | 输出 | 配置 | -|--------|------|------| -| **Text** | UTF-8 / Latin1 文本 | `encoding` | -| **Hex** | 十六进制字符串 | `uppercase`, `separator`, `bytes_per_group`, `endian` | -| **Plot** | 数值波形数据 | `sample_type` (i8–f64), `endian`, `channels`, `format` (Interleaved / Block / XY) | -| **MixedTextPlot** | 混合流(文本 + 波形) | `encoding` | -| **Lua** | 用户自定义解码 | `script_path` | - -#### Plot 采样格式 - -| 格式 | 说明 | -|------|------| -| **Interleaved** | 多通道交叉排列: `[ch0_s0, ch1_s0, ch0_s1, ch1_s1, …]` | -| **Block** | 按通道分块: `[ch0_s0, ch0_s1, …, ch1_s0, ch1_s1, …]` | -| **XY** | 2 通道,交替 x/y: `[x0, y0, x1, y1, …]` | - -支持的采样类型: `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `f32`, `f64` - -### Pipeline - -每个 Session 可配置多个 Pipeline。同一个字节流被送入所有 Pipeline,每个 Pipeline 独立分帧、解码,互不干扰。例如: - -- Pipeline "text": LineFramer → TextDecoder → 文本视图 -- Pipeline "hex": FixedFramer(16) → HexDecoder → 十六进制视图 -- Pipeline "plot": FixedFramer(256) → PlotDecoder → 波形视图 - -当数据到达时,只有成功解码的 Pipeline 产生输出。 - -### 会话管理 - -- **多会话**: 同时运行多个独立 session -- **连接控制**: Connect / Disconnect / Reconnect -- **自动重连**: 断线后每秒自动重试 -- **动态重配置**: 运行时修改分帧器/解码器配置 -- **数据发送**: 通过 Console 面板发送原始字节到连接 -- **历史缓冲**: 环形缓冲区(默认 10,000 条),可配置上限 -- **状态持久化**: Session 配置自动保存到 `gui-state.json` - -### Lua 脚本扩展 - -内置 LuaJIT 运行时,支持: - -- **自定义分帧器**: 实现 `feed(bytes)`, `flush()`, `reset()`, `pending_len()` 四个函数 -- **自定义解码器**: 实现 `decode(frame)` 函数,返回 `{kind="text"|"hex"|"binary"|"plot", data=…}` -- **会话 API**: `xserial.open(config)` 创建 session,支持 `on_data(callback)` 事件回调 -- **辅助函数**: `xserial.list_ports()`, `xserial.sleep(ms)`, `xserial.poll(limit)`, `xserial.log(msg)` - -示例 Lua Line Framer: - -```lua -local buffer = "" -return { - feed = function(bytes) - buffer = buffer .. bytes - local frames = {} - while true do - local i = buffer:find("\n", 1, true) - if not i then break end - frames[#frames + 1] = buffer:sub(1, i - 1) - buffer = buffer:sub(i + 1) - end - return frames - end, - flush = function() - if #buffer == 0 then return nil end - local frame = buffer; buffer = ""; return frame - end, - reset = function() buffer = "" end, - pending_len = function() return #buffer end, -} -``` - ---- - -## GUI - -### 面板 - -| 面板 | 功能 | -|------|------| -| **Sidebar** | Session 列表、创建/删除/编辑 session、连接切换 | -| **Config** | 传输参数、Pipeline 配置(分帧器 + 解码器类型及参数) | -| **Console** | 数据发送输入框,发送按钮 | -| **Text View** | 解码后的文本流,支持时间戳和方向标记 | -| **Hex View** | 十六进制字节展示,可配置分组和分隔符 | -| **Plot View** | 实时波形图,支持内嵌和浮动窗口两种模式 | - -### 快捷键 - -| 快捷键 | 操作 | -|--------|------| -| `Ctrl+Tab` | 下一个 Tab | -| `Ctrl+Shift+Tab` | 上一个 Tab | -| `Ctrl+T` | 切换到 Text 视图 | -| `Ctrl+H` | 切换到 Hex 视图 | -| `Ctrl+P` | 切换到 Plot 视图 | -| `Ctrl+N` | 新建 Session | -| `Ctrl+E` | 编辑当前 Session | -| `Ctrl+W` | 删除当前 Session | -| `Ctrl+F5` | 切换连接 | -| `Ctrl+L` | 清空输出 | -| `Ctrl+,` | UI 设置 | -| `Esc` | 关闭浮层 | - -### 状态持久化 - -GUI 状态(session 配置、活动 tab、显示选项)自动保存到: +GUI 状态自动保存,路径遵循各平台规范: | 平台 | 路径 | |------|------| -| Linux | `$XDG_CONFIG_HOME/xserial/gui-state.json` 或 `~/.config/xserial/gui-state.json` | -| macOS | `~/Library/Application Support/xserial/gui-state.json` | -| Windows | `%APPDATA%\xserial\gui-state.json` | +| Linux | `$XDG_CONFIG_HOME/pipeview/gui-state.json` 或 `~/.config/pipeview/gui-state.json` | +| macOS | `~/Library/Application Support/pipeview/gui-state.json` | +| Windows | `%APPDATA%\pipeview\gui-state.json` | + +持久化的内容包括:Session 配置(传输参数、管线设置)、日志开关及路径、活动标签页、显示选项。 + +日志文件默认保存在配置目录的 `logs/` 子目录下,文件命名格式为 `session_{id}_{timestamp}.log`。 --- ## 开发工具 -### Plot 测试服务器 +### 测试数据生成器 ```bash -# 启动 TCP 波形数据源 +# Plot 波形测试数据 python tools/test_plot.py --wire-format mixed --host 127.0.0.1 --port 8091 - -# XY 格式,2 通道 python tools/test_plot.py --wire-format mixed --format xy --channels 2 - -# 固定长度 raw plot 帧 python tools/test_plot.py --wire-format raw --channels 2 --framelen 256 -``` -GUI 中配置: Transport `TCP 127.0.0.1:8091`, Framer `MixedTextPlot`, Decoder `MixedTextPlot` +# 无人机遥测测试数据 +python tools/test_drone.py --rate 10 --port 8092 +``` ### 性能分析 ```bash -# 开启性能日志 -RUST_LOG=xserial_gui::perf=info XSERIAL_GUI_PROFILE=1 cargo run -p xserial-gui - -# 自定义统计间隔(默认 1000ms) -XSERIAL_GUI_PROFILE_INTERVAL_MS=500 cargo run -p xserial-gui +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 ``` -日志输出: frame 耗时、事件 drain 耗时、text/hex/plot 渲染耗时、plot 点数统计 +输出每帧的耗时、事件 drain 耗时、text/hex/plot 渲染耗时、plot 点数统计。 ### 日志 ```bash -RUST_LOG=info cargo run -p xserial-gui -RUST_LOG=xserial_gui=debug cargo run -p xserial-gui +RUST_LOG=info cargo run -p pipeview-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/ - xserial-core/ # 传输、分帧、协议解码、Pipeline - src/ - transport/ # Serial, TCP, UDP - frame/ # Line, Fixed, Length, Cobs, MixedTextPlot - protocol/ # Text, Hex, Plot, MixedTextPlot - pipeline.rs # MultiPipeline - xserial-client/ # 会话管理、Lua 运行时 - src/ - session.rs # Session 事件循环 - manager.rs # SessionManager - config.rs # SessionConfig, FramerConfig, DecoderConfig - event.rs # DecodedEntry - history.rs # RingBuffer - cmd.rs # SessionCmd - lua/ # Lua 运行时、自定义 codec、会话 API - xserial-gui/ # egui 桌面应用 - src/ - main.rs - app.rs # XserialApp 主控制器 - panels/ # sidebar, config, console, hex_view, plot_view - shortcuts.rs # 键盘快捷键 - ui_fonts.rs # 字体加载 - perf.rs # 性能分析 - app_state.rs # 状态持久化 - buffers.rs # UI 缓冲管理 - xserial-tui/ # ratatui 终端应用 (占位) -tests/ # Lua 测试 fixture - lua_line_framer.lua - lua_text_decoder.lua -tools/ # 开发辅助工具 - test_plot.py # TCP 波形测试服务器 - test_plot_serial.c # C 串口 plot 测试客户端 - test_text_serial.c # C 串口文本测试客户端 - xs_mixed_plot.h # MixedTextPlot 线格式 C 参考头文件 + pipeview-core/ # 传输、分帧、协议 + pipeview-client/ # 会话管理、Lua 运行时 + pipeview-gui/ # egui 桌面应用 + pipeview-tui/ # ratatui 终端应用 +examples/ # Lua 脚本示例 + drone_plot.lua # 飞控遥测波形解码器 + drone_text.lua # 飞控遥测文本解码器 +tests/ # Lua 测试 fixture +tools/ # 开发辅助工具 + test_plot.py # 波形测试数据生成器 + test_drone.py # 飞控测试数据生成器 + test_plot_serial.c # C 串口波形测试客户端 + test_text_serial.c # C 串口文本测试客户端 + xs_mixed_plot.h # MixedTextPlot 协议参考头文件 ``` --- -## MixedTextPlot 协议 +## 技术栈 -单连接上混合传输文本行和波形数据的线格式。 +- **运行时**:tokio (multi-thread) +- **串口**:`tokio-serial` + `serialport` +- **GUI**:egui + egui_plot +- **Lua**:mlua 0.11, LuaJIT (vendored 编译), async/serde/send +- **事件分发**:`tokio::sync::broadcast`(多订阅者) +- **零 feature flags**、零 build script、零条件编译 -**文本帧**: 普通文本行,以 `\n` 分隔。 +## License -**Plot 帧**: -``` -0x1E 'P' | COBS(plot_packet) | 0x00 -``` - -**Plot Packet** (13 字节头 + payload): -``` -'X' 'P' (magic) -version:u8 (固定 1) -format:u8 (0=Interleaved, 1=Block, 2=XY) -sample_type:u8 (F32=8) -endian:u8 (0=Little) -channels:u8 -samples_per_channel:u16le -payload_len:u32le -payload:bytes (channels × samples_per_channel × sample_byte_size) -``` - -参考实现: `tools/xs_mixed_plot.h`(C header) - ---- - -## 技术细节 - -- **异步运行时**: tokio (multi-thread) -- **串口**: `tokio-serial` + `serialport` -- **分帧器特征**: `feed()`, `flush()`, `reset()`, `pending_len()` — 有状态字节流分帧 -- **解码器特征**: `name()`, `decode(&[u8]) -> Option` — 无状态帧解码 -- **Session 事件循环**: `tokio::select!` 多路复用命令、读取、重连定时器 -- **事件分发**: `tokio::sync::broadcast` — 多订阅者 -- **Lua**: `mlua` 0.11, LuaJIT (vendored 编译), 支持 async/serde/send -- **无 feature flags**: 零条件编译,无 build script -- **无 CI**: 本地自检 `cargo test --workspace` + `cargo clippy` +MIT diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000..205d63c --- /dev/null +++ b/README_en.md @@ -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 diff --git a/crates/xserial-client/Cargo.toml b/crates/pipeview-client/Cargo.toml similarity index 84% rename from crates/xserial-client/Cargo.toml rename to crates/pipeview-client/Cargo.toml index e29fb84..0a6047e 100644 --- a/crates/xserial-client/Cargo.toml +++ b/crates/pipeview-client/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "xserial-client" +name = "pipeview-client" version = "0.1.0" edition = "2024" [dependencies] -xserial-core = { path = "../xserial-core" } +pipeview-core = { path = "../pipeview-core" } tokio = { workspace = true, features = ["sync", "time"] } tracing = { workspace = true } serde = { workspace = true } diff --git a/crates/xserial-client/src/cmd.rs b/crates/pipeview-client/src/cmd.rs similarity index 100% rename from crates/xserial-client/src/cmd.rs rename to crates/pipeview-client/src/cmd.rs diff --git a/crates/xserial-client/src/config.rs b/crates/pipeview-client/src/config.rs similarity index 97% rename from crates/xserial-client/src/config.rs rename to crates/pipeview-client/src/config.rs index 25ca50f..8057e21 100644 --- a/crates/xserial-client/src/config.rs +++ b/crates/pipeview-client/src/config.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use xserial_core::frame::{ +use pipeview_core::frame::{ Endian, Framer, cobs::CobsFramer, fixed::FixedLengthFramer, @@ -7,14 +7,14 @@ use xserial_core::frame::{ line::{LineConfig, LineFramer}, mixed::{MixedTextPlotConfig as MixedFramerConfig, MixedTextPlotFramer}, }; -use xserial_core::protocol::{ +use pipeview_core::protocol::{ ProtocolDecoder, hex::{HexConfig, HexDecoder}, mixed::{MixedTextPlotConfig as MixedDecoderConfig, MixedTextPlotDecoder}, plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType}, text::{TextDecoder, TextEncoding}, }; -use xserial_core::transport::TransportConfig; +use pipeview_core::transport::TransportConfig; use crate::error::Result; use crate::lua::codec::{LuaDecoder, LuaFramer}; @@ -229,10 +229,10 @@ impl Default for SessionConfig { #[cfg(test)] mod tests { use super::*; - use xserial_core::protocol::Endian; - use xserial_core::protocol::plot::{PlotFormat, SampleType}; - use xserial_core::protocol::text::TextEncoding; - use xserial_core::transport::TransportConfig; + use pipeview_core::protocol::Endian; + use pipeview_core::protocol::plot::{PlotFormat, SampleType}; + use pipeview_core::protocol::text::TextEncoding; + use pipeview_core::transport::TransportConfig; #[test] fn framer_line_serde_roundtrip() { diff --git a/crates/xserial-client/src/error.rs b/crates/pipeview-client/src/error.rs similarity index 100% rename from crates/xserial-client/src/error.rs rename to crates/pipeview-client/src/error.rs diff --git a/crates/xserial-client/src/event.rs b/crates/pipeview-client/src/event.rs similarity index 72% rename from crates/xserial-client/src/event.rs rename to crates/pipeview-client/src/event.rs index 85fa07b..1e0ca91 100644 --- a/crates/xserial-client/src/event.rs +++ b/crates/pipeview-client/src/event.rs @@ -1,4 +1,4 @@ -use xserial_core::protocol::DecodedData; +use pipeview_core::protocol::DecodedData; #[derive(Debug, Clone)] pub struct DecodedEntry { diff --git a/crates/xserial-client/src/history.rs b/crates/pipeview-client/src/history.rs similarity index 100% rename from crates/xserial-client/src/history.rs rename to crates/pipeview-client/src/history.rs diff --git a/crates/xserial-client/src/lib.rs b/crates/pipeview-client/src/lib.rs similarity index 100% rename from crates/xserial-client/src/lib.rs rename to crates/pipeview-client/src/lib.rs diff --git a/crates/xserial-client/src/lua/codec.rs b/crates/pipeview-client/src/lua/codec.rs similarity index 98% rename from crates/xserial-client/src/lua/codec.rs rename to crates/pipeview-client/src/lua/codec.rs index dc7e1c4..255924e 100644 --- a/crates/xserial-client/src/lua/codec.rs +++ b/crates/pipeview-client/src/lua/codec.rs @@ -3,9 +3,9 @@ use std::sync::Mutex; use mlua::{Function, Lua, RegistryKey, Table, Value}; use tracing::warn; -use xserial_core::frame::Framer; -use xserial_core::protocol::plot::{PlotFormat, PlotFrame, SampleType}; -use xserial_core::protocol::{DecodedData, ProtocolDecoder}; +use pipeview_core::frame::Framer; +use pipeview_core::protocol::plot::{PlotFormat, PlotFrame, SampleType}; +use pipeview_core::protocol::{DecodedData, ProtocolDecoder}; use crate::error::{Error, Result}; @@ -297,7 +297,7 @@ mod tests { .duration_since(UNIX_EPOCH) .unwrap() .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(); path } diff --git a/crates/xserial-client/src/lua/mod.rs b/crates/pipeview-client/src/lua/mod.rs similarity index 88% rename from crates/xserial-client/src/lua/mod.rs rename to crates/pipeview-client/src/lua/mod.rs index 3be1979..52b55e3 100644 --- a/crates/xserial-client/src/lua/mod.rs +++ b/crates/pipeview-client/src/lua/mod.rs @@ -81,9 +81,9 @@ pub fn register(lua: &Lua) -> LuaResult<()> { pub fn register_with_manager(lua: &Lua, manager: SessionManager) -> LuaResult<()> { 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(); lua.create_async_function(move |lua, config: Table| { let runtime = runtime.clone(); @@ -96,15 +96,15 @@ pub fn register_with_manager(lua: &Lua, manager: SessionManager) -> LuaResult<() })? })?; - xserial.set( + pipeview.set( "list_ports", 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::>()) })?, )?; - xserial.set("sleep", { + pipeview.set("sleep", { let runtime = runtime.clone(); lua.create_async_function(move |lua, ms: u64| { 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(); lua.create_async_function(move |lua, limit_per_session: Option| { let runtime = runtime.clone(); @@ -124,7 +124,7 @@ pub fn register_with_manager(lua: &Lua, manager: SessionManager) -> LuaResult<() })? })?; - xserial.set( + pipeview.set( "log", lua.create_function(|_, msg: String| { 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(()) } @@ -146,9 +146,9 @@ mod tests { let lua = Lua::new(); 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"] { - let value: Value = xserial.get(name).unwrap(); + let value: Value = pipeview.get(name).unwrap(); assert!( matches!(value, Value::Function(_)), "{name} should be a function" @@ -162,8 +162,8 @@ mod tests { let manager = SessionManager::new(); register_with_manager(&lua, manager.clone()).unwrap(); - let xserial: Table = lua.globals().get("xserial").unwrap(); - let open: Value = xserial.get("open").unwrap(); + let pipeview: Table = lua.globals().get("pipeview").unwrap(); + let open: Value = pipeview.get("open").unwrap(); assert!(matches!(open, Value::Function(_))); assert_eq!(manager.count(), 0); } diff --git a/crates/xserial-client/src/lua/session_api.rs b/crates/pipeview-client/src/lua/session_api.rs similarity index 99% rename from crates/xserial-client/src/lua/session_api.rs rename to crates/pipeview-client/src/lua/session_api.rs index 092083e..e7234f1 100644 --- a/crates/xserial-client/src/lua/session_api.rs +++ b/crates/pipeview-client/src/lua/session_api.rs @@ -8,7 +8,7 @@ use std::time::Duration; use mlua::{Function, Lua, LuaSerdeExt, RegistryKey, UserData, UserDataMethods, Value, Variadic}; use tokio::sync::mpsc; use tokio::task::JoinHandle; -use xserial_core::protocol::DecodedData; +use pipeview_core::protocol::DecodedData; use crate::config::SessionConfig; use crate::lua::LuaRuntime; diff --git a/crates/xserial-client/src/manager.rs b/crates/pipeview-client/src/manager.rs similarity index 100% rename from crates/xserial-client/src/manager.rs rename to crates/pipeview-client/src/manager.rs diff --git a/crates/xserial-client/src/session.rs b/crates/pipeview-client/src/session.rs similarity index 98% rename from crates/xserial-client/src/session.rs rename to crates/pipeview-client/src/session.rs index c0907fe..df1ab93 100644 --- a/crates/xserial-client/src/session.rs +++ b/crates/pipeview-client/src/session.rs @@ -11,8 +11,8 @@ use tokio::sync::{Mutex, broadcast, mpsc}; use tokio::task::JoinHandle; use tracing::{debug, error, info, warn}; -use xserial_core::pipeline::{MultiPipeline, Pipeline}; -use xserial_core::transport::Connection; +use pipeview_core::pipeline::{MultiPipeline, Pipeline}; +use pipeview_core::transport::Connection; use crate::cmd::SessionCmd; 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()) } @@ -510,7 +510,7 @@ mod tests { fn tcp_config() -> SessionConfig { SessionConfig { - transport: xserial_core::transport::TransportConfig::Tcp { + transport: pipeview_core::transport::TransportConfig::Tcp { addr: "127.0.0.1:1".into(), }, pipelines: vec![PipelineConfig { @@ -520,7 +520,7 @@ mod tests { max_line_len: 65536, }, decoder: DecoderConfig::Text { - encoding: xserial_core::protocol::text::TextEncoding::Utf8, + encoding: pipeview_core::protocol::text::TextEncoding::Utf8, }, }], ..Default::default() diff --git a/crates/xserial-client/tests/lua_tests.rs b/crates/pipeview-client/tests/lua_tests.rs similarity index 87% rename from crates/xserial-client/tests/lua_tests.rs rename to crates/pipeview-client/tests/lua_tests.rs index 73cb349..6340356 100644 --- a/crates/xserial-client/tests/lua_tests.rs +++ b/crates/pipeview-client/tests/lua_tests.rs @@ -11,7 +11,7 @@ static INIT: Once = Once::new(); fn init_tracing() { INIT.call_once(|| { tracing_subscriber::fmt() - .with_env_filter("xserial=trace") + .with_env_filter("pipeview=trace") .try_init() .ok(); }); @@ -22,7 +22,7 @@ fn write_temp_lua(name: &str, script: &str) -> PathBuf { .duration_since(std::time::UNIX_EPOCH) .unwrap() .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(); path } @@ -47,18 +47,18 @@ fn hex_pipeline_lua() -> &'static str { "# } -// ── xserial.sleep / xserial.log ────────────────────────────────── +// ── pipeview.sleep / pipeview.log ────────────────────────────────── #[tokio::test] async fn lua_sleep_and_log() { let lua = Lua::new(); - xserial_client::lua::register(&lua).unwrap(); + pipeview_client::lua::register(&lua).unwrap(); lua.load( r#" - xserial.log("test start") - xserial.sleep(50) - xserial.log("test end") + pipeview.log("test start") + pipeview.sleep(50) + pipeview.log("test end") "#, ) .exec_async() @@ -66,15 +66,15 @@ async fn lua_sleep_and_log() { .unwrap(); } -// ── xserial.list_ports ─────────────────────────────────────────── +// ── pipeview.list_ports ─────────────────────────────────────────── #[tokio::test] async fn lua_list_ports() { let lua = Lua::new(); - xserial_client::lua::register(&lua).unwrap(); + pipeview_client::lua::register(&lua).unwrap(); let result: Vec = lua - .load(r#"return xserial.list_ports()"#) + .load(r#"return pipeview.list_ports()"#) .eval_async() .await .unwrap(); @@ -84,7 +84,7 @@ async fn lua_list_ports() { let _ = result; } -// ── xserial.open + session:send / session:read / session:close ──── +// ── pipeview.open + session:send / session:read / session:close ──── #[tokio::test] async fn lua_session_open_send_read_close() { @@ -102,11 +102,11 @@ async fn lua_session_open_send_read_close() { }); let lua = Lua::new(); - xserial_client::lua::register(&lua).unwrap(); + pipeview_client::lua::register(&lua).unwrap(); let script = format!( r#" - local sess = xserial.open({{ + local sess = pipeview.open({{ transport = {{ Tcp = {{ addr = "{}" }} }}, pipelines = {{{}}} }}) @@ -141,11 +141,11 @@ async fn lua_session_read_timeout() { }); let lua = Lua::new(); - xserial_client::lua::register(&lua).unwrap(); + pipeview_client::lua::register(&lua).unwrap(); let script = format!( r#" - local sess = xserial.open({{ + local sess = pipeview.open({{ transport = {{ Tcp = {{ addr = "{}" }} }}, pipelines = {{{}}} }}) @@ -162,7 +162,7 @@ async fn lua_session_read_timeout() { lua.load(&script).exec_async().await.unwrap(); } -// ── xserial.open with hex decoder ──────────────────────────────── +// ── pipeview.open with hex decoder ──────────────────────────────── #[tokio::test] async fn lua_session_hex_decoder() { @@ -176,11 +176,11 @@ async fn lua_session_hex_decoder() { }); let lua = Lua::new(); - xserial_client::lua::register(&lua).unwrap(); + pipeview_client::lua::register(&lua).unwrap(); let script = format!( r#" - local sess = xserial.open({{ + local sess = pipeview.open({{ transport = {{ Tcp = {{ addr = "{}" }} }}, pipelines = {{{}}} }}) @@ -201,7 +201,7 @@ async fn lua_session_hex_decoder() { server.await.unwrap(); } -// ── xserial.open with Lua framer + Lua decoder ─────────────────── +// ── pipeview.open with Lua framer + Lua decoder ─────────────────── #[tokio::test] async fn lua_session_custom_lua_pipeline() { @@ -259,11 +259,11 @@ async fn lua_session_custom_lua_pipeline() { }); let lua = Lua::new(); - xserial_client::lua::register(&lua).unwrap(); + pipeview_client::lua::register(&lua).unwrap(); let script = format!( r#" - local sess = xserial.open({{ + local sess = pipeview.open({{ transport = {{ Tcp = {{ addr = "{}" }} }}, pipelines = {{ {{ @@ -311,12 +311,12 @@ async fn lua_session_on_data_callback() { }); let lua = Lua::new(); - xserial_client::lua::register(&lua).unwrap(); + pipeview_client::lua::register(&lua).unwrap(); let script = format!( r#" local received = {{}} - local sess = xserial.open({{ + local sess = pipeview.open({{ transport = {{ Tcp = {{ addr = "{}" }} }}, pipelines = {{{}}} }}) @@ -327,8 +327,8 @@ async fn lua_session_on_data_callback() { for _ = 1, 50 do if #received < 2 then - xserial.poll(1) - xserial.sleep(10) + pipeview.poll(1) + pipeview.sleep(10) end end @@ -371,11 +371,11 @@ async fn lua_session_reconfigure() { }); let lua = Lua::new(); - xserial_client::lua::register(&lua).unwrap(); + pipeview_client::lua::register(&lua).unwrap(); let script = format!( r#" - local sess = xserial.open({{ + local sess = pipeview.open({{ transport = {{ Tcp = {{ addr = "{}" }} }}, pipelines = {{{}}} }}) @@ -420,11 +420,11 @@ async fn lua_session_next_event_stream() { }); let lua = Lua::new(); - xserial_client::lua::register(&lua).unwrap(); + pipeview_client::lua::register(&lua).unwrap(); let script = format!( r#" - local sess = xserial.open({{ + local sess = pipeview.open({{ transport = {{ Tcp = {{ addr = "{}" }} }}, pipelines = {{{}}} }}) @@ -450,7 +450,7 @@ async fn lua_session_next_event_stream() { server.await.unwrap(); } -// ── session:off + xserial.poll ─────────────────────────────────── +// ── session:off + pipeview.poll ─────────────────────────────────── #[tokio::test] async fn lua_session_off_stops_future_callbacks() { @@ -466,12 +466,12 @@ async fn lua_session_off_stops_future_callbacks() { }); let lua = Lua::new(); - xserial_client::lua::register(&lua).unwrap(); + pipeview_client::lua::register(&lua).unwrap(); let script = format!( r#" local received = {{}} - local sess = xserial.open({{ + local sess = pipeview.open({{ transport = {{ Tcp = {{ addr = "{}" }} }}, pipelines = {{{}}} }}) @@ -482,8 +482,8 @@ async fn lua_session_off_stops_future_callbacks() { for _ = 1, 50 do if #received == 0 then - xserial.poll(1) - xserial.sleep(10) + pipeview.poll(1) + pipeview.sleep(10) end end @@ -491,8 +491,8 @@ async fn lua_session_off_stops_future_callbacks() { assert(received[1] == "text:first") assert(sess:off(token) == true) - xserial.sleep(250) - xserial.poll(10) + pipeview.sleep(250) + pipeview.poll(10) assert(#received == 1, "callback should not fire after off") assert(sess:off(token) == false) diff --git a/crates/xserial-client/tests/session_lifecycle.rs b/crates/pipeview-client/tests/session_lifecycle.rs similarity index 95% rename from crates/xserial-client/tests/session_lifecycle.rs rename to crates/pipeview-client/tests/session_lifecycle.rs index c3879b9..a5717e4 100644 --- a/crates/xserial-client/tests/session_lifecycle.rs +++ b/crates/pipeview-client/tests/session_lifecycle.rs @@ -3,11 +3,11 @@ use std::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener; -use xserial_client::SessionManager; -use xserial_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig}; -use xserial_client::session::{Session, SessionEvent}; -use xserial_core::protocol::DecodedData; -use xserial_core::transport::TransportConfig; +use pipeview_client::SessionManager; +use pipeview_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig}; +use pipeview_client::session::{Session, SessionEvent}; +use pipeview_core::protocol::DecodedData; +use pipeview_core::transport::TransportConfig; fn tcp_config(addr: String) -> SessionConfig { SessionConfig { @@ -19,7 +19,7 @@ fn tcp_config(addr: String) -> SessionConfig { max_line_len: 1024 * 1024, }, decoder: DecoderConfig::Text { - encoding: xserial_core::protocol::text::TextEncoding::Utf8, + encoding: pipeview_core::protocol::text::TextEncoding::Utf8, }, }], history_limit: 100, @@ -113,7 +113,7 @@ async fn session_multi_pipeline_text_and_hex() { uppercase: false, separator: " ".into(), 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, separator: " ".into(), bytes_per_group: 1, - endian: xserial_core::protocol::Endian::Big, + endian: pipeview_core::protocol::Endian::Big, }; handle.reconfigure(new_cfg).await.unwrap(); diff --git a/crates/xserial-core/Cargo.toml b/crates/pipeview-core/Cargo.toml similarity index 94% rename from crates/xserial-core/Cargo.toml rename to crates/pipeview-core/Cargo.toml index 9943771..48f040b 100644 --- a/crates/xserial-core/Cargo.toml +++ b/crates/pipeview-core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "xserial-core" +name = "pipeview-core" version = "0.1.0" edition = "2024" diff --git a/crates/xserial-core/src/error.rs b/crates/pipeview-core/src/error.rs similarity index 100% rename from crates/xserial-core/src/error.rs rename to crates/pipeview-core/src/error.rs diff --git a/crates/xserial-core/src/frame/cobs.rs b/crates/pipeview-core/src/frame/cobs.rs similarity index 100% rename from crates/xserial-core/src/frame/cobs.rs rename to crates/pipeview-core/src/frame/cobs.rs diff --git a/crates/xserial-core/src/frame/fixed.rs b/crates/pipeview-core/src/frame/fixed.rs similarity index 100% rename from crates/xserial-core/src/frame/fixed.rs rename to crates/pipeview-core/src/frame/fixed.rs diff --git a/crates/xserial-core/src/frame/length.rs b/crates/pipeview-core/src/frame/length.rs similarity index 100% rename from crates/xserial-core/src/frame/length.rs rename to crates/pipeview-core/src/frame/length.rs diff --git a/crates/xserial-core/src/frame/line.rs b/crates/pipeview-core/src/frame/line.rs similarity index 100% rename from crates/xserial-core/src/frame/line.rs rename to crates/pipeview-core/src/frame/line.rs diff --git a/crates/xserial-core/src/frame/mixed.rs b/crates/pipeview-core/src/frame/mixed.rs similarity index 100% rename from crates/xserial-core/src/frame/mixed.rs rename to crates/pipeview-core/src/frame/mixed.rs diff --git a/crates/xserial-core/src/frame/mod.rs b/crates/pipeview-core/src/frame/mod.rs similarity index 100% rename from crates/xserial-core/src/frame/mod.rs rename to crates/pipeview-core/src/frame/mod.rs diff --git a/crates/xserial-core/src/lib.rs b/crates/pipeview-core/src/lib.rs similarity index 100% rename from crates/xserial-core/src/lib.rs rename to crates/pipeview-core/src/lib.rs diff --git a/crates/xserial-core/src/pipeline.rs b/crates/pipeview-core/src/pipeline.rs similarity index 97% rename from crates/xserial-core/src/pipeline.rs rename to crates/pipeview-core/src/pipeline.rs index c90e370..07eb304 100644 --- a/crates/xserial-core/src/pipeline.rs +++ b/crates/pipeview-core/src/pipeline.rs @@ -61,11 +61,11 @@ pub struct PipelineResult { /// Manages multiple [`Pipeline`]s, feeding the same byte stream to all of them. /// /// ``` -/// use xserial_core::pipeline::{MultiPipeline, Pipeline}; -/// use xserial_core::frame::line::{LineFramer, LineConfig}; -/// use xserial_core::frame::fixed::FixedLengthFramer; -/// use xserial_core::protocol::text::{TextDecoder, TextEncoding}; -/// use xserial_core::protocol::hex::{HexDecoder, HexConfig}; +/// use pipeview_core::pipeline::{MultiPipeline, Pipeline}; +/// use pipeview_core::frame::line::{LineFramer, LineConfig}; +/// use pipeview_core::frame::fixed::FixedLengthFramer; +/// use pipeview_core::protocol::text::{TextDecoder, TextEncoding}; +/// use pipeview_core::protocol::hex::{HexDecoder, HexConfig}; /// /// let mut mp = MultiPipeline::new(); /// mp.add( diff --git a/crates/xserial-core/src/protocol/hex.rs b/crates/pipeview-core/src/protocol/hex.rs similarity index 100% rename from crates/xserial-core/src/protocol/hex.rs rename to crates/pipeview-core/src/protocol/hex.rs diff --git a/crates/xserial-core/src/protocol/mixed.rs b/crates/pipeview-core/src/protocol/mixed.rs similarity index 100% rename from crates/xserial-core/src/protocol/mixed.rs rename to crates/pipeview-core/src/protocol/mixed.rs diff --git a/crates/xserial-core/src/protocol/mod.rs b/crates/pipeview-core/src/protocol/mod.rs similarity index 100% rename from crates/xserial-core/src/protocol/mod.rs rename to crates/pipeview-core/src/protocol/mod.rs diff --git a/crates/xserial-core/src/protocol/plot.rs b/crates/pipeview-core/src/protocol/plot.rs similarity index 100% rename from crates/xserial-core/src/protocol/plot.rs rename to crates/pipeview-core/src/protocol/plot.rs diff --git a/crates/xserial-core/src/protocol/text.rs b/crates/pipeview-core/src/protocol/text.rs similarity index 100% rename from crates/xserial-core/src/protocol/text.rs rename to crates/pipeview-core/src/protocol/text.rs diff --git a/crates/xserial-core/src/transport/mod.rs b/crates/pipeview-core/src/transport/mod.rs similarity index 100% rename from crates/xserial-core/src/transport/mod.rs rename to crates/pipeview-core/src/transport/mod.rs diff --git a/crates/xserial-core/src/transport/serial.rs b/crates/pipeview-core/src/transport/serial.rs similarity index 100% rename from crates/xserial-core/src/transport/serial.rs rename to crates/pipeview-core/src/transport/serial.rs diff --git a/crates/xserial-core/src/transport/tcp.rs b/crates/pipeview-core/src/transport/tcp.rs similarity index 100% rename from crates/xserial-core/src/transport/tcp.rs rename to crates/pipeview-core/src/transport/tcp.rs diff --git a/crates/xserial-core/src/transport/udp.rs b/crates/pipeview-core/src/transport/udp.rs similarity index 100% rename from crates/xserial-core/src/transport/udp.rs rename to crates/pipeview-core/src/transport/udp.rs diff --git a/crates/xserial-core/tests/pipeline.rs b/crates/pipeview-core/tests/pipeline.rs similarity index 99% rename from crates/xserial-core/tests/pipeline.rs rename to crates/pipeview-core/tests/pipeline.rs index cd5e112..54fbcb5 100644 --- a/crates/xserial-core/tests/pipeline.rs +++ b/crates/pipeview-core/tests/pipeline.rs @@ -4,7 +4,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, UdpSocket}; use tokio::time::timeout; -use xserial_core::frame::{ +use pipeview_core::frame::{ Endian, Framer, cobs::CobsFramer, cobs::cobs_encode as raw_cobs_encode, @@ -13,17 +13,17 @@ use xserial_core::frame::{ line::{LineConfig, LineFramer}, mixed::{MixedTextPlotConfig as MixedFramerConfig, MixedTextPlotFramer}, }; -use xserial_core::protocol::{ +use pipeview_core::protocol::{ DecodedData, ProtocolDecoder, hex::{HexConfig, HexDecoder}, mixed::{MIXED_PLOT_ESCAPE, MIXED_PLOT_MARKER, MixedTextPlotConfig, MixedTextPlotDecoder}, plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType}, text::{TextDecoder, TextEncoding}, }; -use xserial_core::transport::serial::{ +use pipeview_core::transport::serial::{ 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); diff --git a/crates/xserial-gui/Cargo.toml b/crates/pipeview-gui/Cargo.toml similarity index 71% rename from crates/xserial-gui/Cargo.toml rename to crates/pipeview-gui/Cargo.toml index 1eedc2d..1b2307d 100644 --- a/crates/xserial-gui/Cargo.toml +++ b/crates/pipeview-gui/Cargo.toml @@ -1,15 +1,15 @@ [package] -name = "xserial-gui" +name = "pipeview-gui" version = "0.1.0" edition = "2024" [[bin]] -name = "xserial-gui" +name = "pipeview-gui" path = "src/main.rs" [dependencies] -xserial-core = { path = "../xserial-core" } -xserial-client = { path = "../xserial-client" } +pipeview-core = { path = "../pipeview-core" } +pipeview-client = { path = "../pipeview-client" } tokio = { workspace = true } egui = { workspace = true } eframe = { workspace = true } diff --git a/crates/xserial-gui/src/app.rs b/crates/pipeview-gui/src/app.rs similarity index 99% rename from crates/xserial-gui/src/app.rs rename to crates/pipeview-gui/src/app.rs index 073e2e9..b1ae20a 100644 --- a/crates/xserial-gui/src/app.rs +++ b/crates/pipeview-gui/src/app.rs @@ -10,11 +10,11 @@ use crate::perf::{DrainStats, GuiProfiler, GuiSnapshot}; use crate::shortcuts::{self, default_bindings}; use crate::ui_fonts::{self, FontCandidate, FontChoice, UiFontSettings}; use egui::{Color32, Layout, Panel, Pos2, Rect, TextEdit, UiBuilder}; -use xserial_client::SessionManager; -use xserial_client::config::SessionConfig; -use xserial_client::session::SessionEvent; -use xserial_core::protocol::DecodedData; -use xserial_core::transport::TransportConfig; +use pipeview_client::SessionManager; +use pipeview_client::config::SessionConfig; +use pipeview_client::session::SessionEvent; +use pipeview_core::protocol::DecodedData; +use pipeview_core::transport::TransportConfig; 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) { Panel::top("top_bar").show_inside(ui, |ui| { ui.horizontal_wrapped(|ui| { - ui.heading("xserial"); + ui.heading("pipeview"); ui.separator(); // ui.label(format!( // "Fonts: {} + {} {:.1} pt", @@ -1434,8 +1434,8 @@ fn build_payload(tab: &SessionTab) -> Result>, String> { #[cfg(test)] mod tests { use super::transport_summary; - use xserial_core::transport::TransportConfig; - use xserial_core::transport::serial::{ + use pipeview_core::transport::TransportConfig; + use pipeview_core::transport::serial::{ SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits, }; diff --git a/crates/xserial-gui/src/app_state.rs b/crates/pipeview-gui/src/app_state.rs similarity index 91% rename from crates/xserial-gui/src/app_state.rs rename to crates/pipeview-gui/src/app_state.rs index 415b19f..2052697 100644 --- a/crates/xserial-gui/src/app_state.rs +++ b/crates/pipeview-gui/src/app_state.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use tracing::warn; -use xserial_client::config::SessionConfig; +use pipeview_client::config::SessionConfig; 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()) } -/// 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 { gui_state_path() .parent() @@ -67,12 +67,12 @@ fn gui_state_path_for_os_and_env(os: &str, get_env: impl Fn(&str) -> Option { if let Some(path) = get_env("APPDATA").filter(|path| !path.trim().is_empty()) { return PathBuf::from(path) - .join("xserial") + .join("pipeview") .join(GUI_STATE_FILE_NAME); } if let Some(path) = get_env("LOCALAPPDATA").filter(|path| !path.trim().is_empty()) { return PathBuf::from(path) - .join("xserial") + .join("pipeview") .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 { if let Some(path) = get_env("XDG_CONFIG_HOME").filter(|path| !path.trim().is_empty()) { return PathBuf::from(path) - .join("xserial") + .join("pipeview") .join(GUI_STATE_FILE_NAME); } if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) { return PathBuf::from(home) .join(".config") - .join("xserial") + .join("pipeview") .join(GUI_STATE_FILE_NAME); } } @@ -124,7 +124,7 @@ const fn default_true() -> bool { mod tests { use super::*; use std::time::{SystemTime, UNIX_EPOCH}; - use xserial_core::transport::TransportConfig; + use pipeview_core::transport::TransportConfig; #[test] fn windows_gui_state_path_uses_appdata() { @@ -135,7 +135,7 @@ mod tests { assert_eq!( path, PathBuf::from(r"C:\Users\Test\AppData\Roaming") - .join("xserial") + .join("pipeview") .join(GUI_STATE_FILE_NAME) ); } @@ -146,7 +146,7 @@ mod tests { .duration_since(UNIX_EPOCH) .unwrap() .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 state = PersistedGuiState { sessions: vec![SessionConfig { diff --git a/crates/xserial-gui/src/buffers.rs b/crates/pipeview-gui/src/buffers.rs similarity index 98% rename from crates/xserial-gui/src/buffers.rs rename to crates/pipeview-gui/src/buffers.rs index d5d2c64..fad79cc 100644 --- a/crates/xserial-gui/src/buffers.rs +++ b/crates/pipeview-gui/src/buffers.rs @@ -1,10 +1,10 @@ use egui_plot::PlotBounds; use std::collections::VecDeque; use std::time::{Duration, Instant}; -use xserial_client::RingBuffer; -use xserial_client::event::DecodedEntry; -use xserial_core::protocol::DecodedData; -use xserial_core::protocol::plot::{PlotFormat, PlotFrame}; +use pipeview_client::RingBuffer; +use pipeview_client::event::DecodedEntry; +use pipeview_core::protocol::DecodedData; +use pipeview_core::protocol::plot::{PlotFormat, PlotFrame}; #[derive(Clone)] pub enum LineDirection { @@ -716,9 +716,9 @@ impl PlotBuffer { #[cfg(test)] mod tests { use super::*; - use xserial_client::event::DecodedEntry; - use xserial_core::protocol::DecodedData; - use xserial_core::protocol::plot::{PlotFrame, SampleType}; + use pipeview_client::event::DecodedEntry; + use pipeview_core::protocol::DecodedData; + use pipeview_core::protocol::plot::{PlotFrame, SampleType}; fn plot_entry() -> DecodedEntry { DecodedEntry { diff --git a/crates/xserial-gui/src/logging.rs b/crates/pipeview-gui/src/logging.rs similarity index 95% rename from crates/xserial-gui/src/logging.rs rename to crates/pipeview-gui/src/logging.rs index 33f23d3..bee79b8 100644 --- a/crates/xserial-gui/src/logging.rs +++ b/crates/pipeview-gui/src/logging.rs @@ -6,8 +6,8 @@ use std::thread; use std::time::{SystemTime, UNIX_EPOCH}; -use xserial_client::event::DecodedEntry; -use xserial_core::protocol::DecodedData; +use pipeview_client::event::DecodedEntry; +use pipeview_core::protocol::DecodedData; /// Manages background file logging via a dedicated writer thread. /// @@ -30,7 +30,7 @@ impl LogWriter { let mut writer = BufWriter::with_capacity(1024, file); let (sender, receiver) = mpsc::channel::(); - let thread_name = format!("xserial-log-{}", path.display()); + let thread_name = format!("pipeview-log-{}", path.display()); thread::Builder::new().name(thread_name).spawn(move || { while let Ok(line) = receiver.recv() { if writeln!(writer, "{line}").is_err() { diff --git a/crates/xserial-gui/src/main.rs b/crates/pipeview-gui/src/main.rs similarity index 91% rename from crates/xserial-gui/src/main.rs rename to crates/pipeview-gui/src/main.rs index 4d01558..662f812 100644 --- a/crates/xserial-gui/src/main.rs +++ b/crates/pipeview-gui/src/main.rs @@ -7,7 +7,7 @@ mod perf; mod shortcuts; mod ui_fonts; -use xserial_client::SessionManager; +use pipeview_client::SessionManager; fn main() { tracing_subscriber::fmt() @@ -21,7 +21,7 @@ fn main() { let rx = mgr.subscribe(); let _ = eframe::run_native( - "xserial", + "pipeview", eframe::NativeOptions::default(), Box::new(|cc| Ok(Box::new(app::XserialApp::new(mgr, rx, cc.egui_ctx.clone())))), ); diff --git a/crates/xserial-gui/src/panels/config.rs b/crates/pipeview-gui/src/panels/config.rs similarity index 99% rename from crates/xserial-gui/src/panels/config.rs rename to crates/pipeview-gui/src/panels/config.rs index b387e63..706a4c7 100644 --- a/crates/xserial-gui/src/panels/config.rs +++ b/crates/pipeview-gui/src/panels/config.rs @@ -1,10 +1,10 @@ use egui::{ComboBox, DragValue, ScrollArea, TextEdit, Ui}; -use xserial_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig}; -use xserial_core::protocol::Endian; -use xserial_core::protocol::plot::{PlotFormat, SampleType}; -use xserial_core::protocol::text::TextEncoding; -use xserial_core::transport::TransportConfig; -use xserial_core::transport::serial::{ +use pipeview_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig}; +use pipeview_core::protocol::Endian; +use pipeview_core::protocol::plot::{PlotFormat, SampleType}; +use pipeview_core::protocol::text::TextEncoding; +use pipeview_core::transport::TransportConfig; +use pipeview_core::transport::serial::{ SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits, SerialTransport, }; diff --git a/crates/xserial-gui/src/panels/console.rs b/crates/pipeview-gui/src/panels/console.rs similarity index 100% rename from crates/xserial-gui/src/panels/console.rs rename to crates/pipeview-gui/src/panels/console.rs diff --git a/crates/xserial-gui/src/panels/hex_view.rs b/crates/pipeview-gui/src/panels/hex_view.rs similarity index 100% rename from crates/xserial-gui/src/panels/hex_view.rs rename to crates/pipeview-gui/src/panels/hex_view.rs diff --git a/crates/xserial-gui/src/panels/mod.rs b/crates/pipeview-gui/src/panels/mod.rs similarity index 100% rename from crates/xserial-gui/src/panels/mod.rs rename to crates/pipeview-gui/src/panels/mod.rs diff --git a/crates/xserial-gui/src/panels/plot_view.rs b/crates/pipeview-gui/src/panels/plot_view.rs similarity index 100% rename from crates/xserial-gui/src/panels/plot_view.rs rename to crates/pipeview-gui/src/panels/plot_view.rs diff --git a/crates/xserial-gui/src/panels/sidebar.rs b/crates/pipeview-gui/src/panels/sidebar.rs similarity index 100% rename from crates/xserial-gui/src/panels/sidebar.rs rename to crates/pipeview-gui/src/panels/sidebar.rs diff --git a/crates/xserial-gui/src/perf.rs b/crates/pipeview-gui/src/perf.rs similarity index 99% rename from crates/xserial-gui/src/perf.rs rename to crates/pipeview-gui/src/perf.rs index b1f1609..67242ff 100644 --- a/crates/xserial-gui/src/perf.rs +++ b/crates/pipeview-gui/src/perf.rs @@ -202,7 +202,7 @@ impl GuiProfiler { let repaints = self.repaint_counter.take(); info!( - target: "xserial_gui::perf", + target: "pipeview_gui::perf", frames = self.interval_stats.frames.count, frame_avg_ms = self.interval_stats.frames.avg_ms(), frame_max_ms = self.interval_stats.frames.max_ms(), diff --git a/crates/xserial-gui/src/shortcuts.rs b/crates/pipeview-gui/src/shortcuts.rs similarity index 100% rename from crates/xserial-gui/src/shortcuts.rs rename to crates/pipeview-gui/src/shortcuts.rs diff --git a/crates/xserial-gui/src/ui_fonts.rs b/crates/pipeview-gui/src/ui_fonts.rs similarity index 98% rename from crates/xserial-gui/src/ui_fonts.rs rename to crates/pipeview-gui/src/ui_fonts.rs index db66106..42419d4 100644 --- a/crates/xserial-gui/src/ui_fonts.rs +++ b/crates/pipeview-gui/src/ui_fonts.rs @@ -358,12 +358,12 @@ fn font_settings_path_for_os_and_env( "windows" => { if let Some(path) = get_env("APPDATA").filter(|path| !path.trim().is_empty()) { return PathBuf::from(path) - .join("xserial") + .join("pipeview") .join(FONT_SETTINGS_FILE_NAME); } if let Some(path) = get_env("LOCALAPPDATA").filter(|path| !path.trim().is_empty()) { return PathBuf::from(path) - .join("xserial") + .join("pipeview") .join(FONT_SETTINGS_FILE_NAME); } } @@ -372,20 +372,20 @@ fn font_settings_path_for_os_and_env( return PathBuf::from(home) .join("Library") .join("Application Support") - .join("xserial") + .join("pipeview") .join(FONT_SETTINGS_FILE_NAME); } } _ => { if let Some(path) = get_env("XDG_CONFIG_HOME").filter(|path| !path.trim().is_empty()) { return PathBuf::from(path) - .join("xserial") + .join("pipeview") .join(FONT_SETTINGS_FILE_NAME); } if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) { return PathBuf::from(home) .join(".config") - .join("xserial") + .join("pipeview") .join(FONT_SETTINGS_FILE_NAME); } } @@ -591,7 +591,7 @@ mod tests { .duration_since(UNIX_EPOCH) .unwrap() .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 settings = UiFontSettings { primary_choice: FontChoice::System(String::from("primary")), @@ -622,7 +622,7 @@ mod tests { assert_eq!( path, PathBuf::from("/tmp/xdg-config") - .join("xserial") + .join("pipeview") .join(FONT_SETTINGS_FILE_NAME) ); } @@ -636,7 +636,7 @@ mod tests { assert_eq!( path, PathBuf::from(r"C:\Users\Test\AppData\Roaming") - .join("xserial") + .join("pipeview") .join(FONT_SETTINGS_FILE_NAME) ); } @@ -652,7 +652,7 @@ mod tests { PathBuf::from("/Users/tester") .join("Library") .join("Application Support") - .join("xserial") + .join("pipeview") .join(FONT_SETTINGS_FILE_NAME) ); } diff --git a/crates/xserial-tui/Cargo.toml b/crates/pipeview-tui/Cargo.toml similarity index 77% rename from crates/xserial-tui/Cargo.toml rename to crates/pipeview-tui/Cargo.toml index 563aa77..5ac8c7e 100644 --- a/crates/xserial-tui/Cargo.toml +++ b/crates/pipeview-tui/Cargo.toml @@ -1,15 +1,15 @@ [package] -name = "xserial-tui" +name = "pipeview-tui" version = "0.1.0" edition = "2024" [[bin]] -name = "xserial-tui" +name = "pipeview-tui" path = "src/main.rs" [dependencies] -xserial-core = { path = "../xserial-core" } -xserial-client = { path = "../xserial-client" } +pipeview-core = { path = "../pipeview-core" } +pipeview-client = { path = "../pipeview-client" } tokio = { workspace = true } ratatui = { workspace = true } crossterm = { workspace = true } diff --git a/crates/xserial-tui/src/app.rs b/crates/pipeview-tui/src/app.rs similarity index 99% rename from crates/xserial-tui/src/app.rs rename to crates/pipeview-tui/src/app.rs index a6ba7cb..575e695 100644 --- a/crates/xserial-tui/src/app.rs +++ b/crates/pipeview-tui/src/app.rs @@ -3,12 +3,12 @@ use std::{io, time::Duration}; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use ratatui::DefaultTerminal; use tokio::sync::broadcast; -use xserial_client::{ +use pipeview_client::{ DecoderConfig, FramerConfig, PipelineConfig, SessionConfig, SessionEvent, SessionId, SessionManager, }; -use xserial_core::protocol::DecodedData; -use xserial_core::transport::{ +use pipeview_core::protocol::DecodedData; +use pipeview_core::transport::{ TransportConfig, TransportType, serial::{SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits, SerialTransport}, }; diff --git a/crates/xserial-tui/src/app_state.rs b/crates/pipeview-tui/src/app_state.rs similarity index 93% rename from crates/xserial-tui/src/app_state.rs rename to crates/pipeview-tui/src/app_state.rs index 3b86561..e5769a9 100644 --- a/crates/xserial-tui/src/app_state.rs +++ b/crates/pipeview-tui/src/app_state.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use tracing::warn; -use xserial_client::SessionConfig; +use pipeview_client::SessionConfig; 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) "windows" => { if let Some(path) = get_env("APPDATA").filter(|path| !path.trim().is_empty()) { return PathBuf::from(path) - .join("xserial") + .join("pipeview") .join(TUI_STATE_FILE_NAME); } if let Some(path) = get_env("LOCALAPPDATA").filter(|path| !path.trim().is_empty()) { return PathBuf::from(path) - .join("xserial") + .join("pipeview") .join(TUI_STATE_FILE_NAME); } } @@ -63,20 +63,20 @@ fn state_path_for_os_and_env(os: &str, get_env: impl Fn(&str) -> Option) return PathBuf::from(home) .join("Library") .join("Application Support") - .join("xserial") + .join("pipeview") .join(TUI_STATE_FILE_NAME); } } _ => { if let Some(path) = get_env("XDG_CONFIG_HOME").filter(|path| !path.trim().is_empty()) { return PathBuf::from(path) - .join("xserial") + .join("pipeview") .join(TUI_STATE_FILE_NAME); } if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) { return PathBuf::from(home) .join(".config") - .join("xserial") + .join("pipeview") .join(TUI_STATE_FILE_NAME); } } diff --git a/crates/xserial-tui/src/buffers.rs b/crates/pipeview-tui/src/buffers.rs similarity index 97% rename from crates/xserial-tui/src/buffers.rs rename to crates/pipeview-tui/src/buffers.rs index a7046e6..cc66fb1 100644 --- a/crates/xserial-tui/src/buffers.rs +++ b/crates/pipeview-tui/src/buffers.rs @@ -1,7 +1,7 @@ use std::time::{Duration, Instant}; -use xserial_client::{DecodedEntry, RingBuffer}; -use xserial_core::protocol::DecodedData; +use pipeview_client::{DecodedEntry, RingBuffer}; +use pipeview_core::protocol::DecodedData; #[derive(Clone, Copy, PartialEq, Eq)] pub enum LineDirection { diff --git a/crates/xserial-tui/src/main.rs b/crates/pipeview-tui/src/main.rs similarity index 96% rename from crates/xserial-tui/src/main.rs rename to crates/pipeview-tui/src/main.rs index 8a61c87..33f19f1 100644 --- a/crates/xserial-tui/src/main.rs +++ b/crates/pipeview-tui/src/main.rs @@ -10,7 +10,7 @@ use crossterm::{ terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; use ratatui::{Terminal, backend::CrosstermBackend}; -use xserial_client::SessionManager; +use pipeview_client::SessionManager; #[tokio::main] async fn main() -> io::Result<()> { diff --git a/crates/xserial-tui/src/ui.rs b/crates/pipeview-tui/src/ui.rs similarity index 99% rename from crates/xserial-tui/src/ui.rs rename to crates/pipeview-tui/src/ui.rs index 596e609..00bf81e 100644 --- a/crates/xserial-tui/src/ui.rs +++ b/crates/pipeview-tui/src/ui.rs @@ -100,7 +100,7 @@ fn render_main(frame: &mut Frame, app: &App, area: Rect) { app.help_text(), 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 }), area, ); diff --git a/examples/drone_plot.lua b/examples/drone_plot.lua new file mode 100644 index 0000000..0af1ecb --- /dev/null +++ b/examples/drone_plot.lua @@ -0,0 +1,16 @@ +return { + decode = function(frame) + if #frame == 0 then return nil end + + local gyro_str = frame:match("Gyro:([%d%.%-]+),([%d%.%-]+),([%d%.%-]+)") + if not gyro_str then return nil end + + local gz, gy, gx = frame:match("Gyro:([%d%.%-]+),([%d%.%-]+),([%d%.%-]+)") + return { + kind = "plot", + channels = { { tonumber(gz) }, { tonumber(gy) }, { tonumber(gx) } }, + sample_type = "F64", + format = "Block", + } + end, +} diff --git a/examples/drone_text.lua b/examples/drone_text.lua new file mode 100644 index 0000000..ca90f5c --- /dev/null +++ b/examples/drone_text.lua @@ -0,0 +1,33 @@ +local LABELS = { + AHRS = "Quaternion", + YPR = "Yaw/Pitch/Roll", + Gyro = "Gyro (deg/s)", + RC = "RC Channels", + M = "Motors", + L = "L", + F = "F", + C = "C", +} + +return { + decode = function(frame) + if #frame == 0 then return nil end + + local lines = {} + for segment in frame:gmatch("[^|]+") do + local colon = segment:find(":") + if colon then + local key = segment:sub(1, colon - 1) + local val = segment:sub(colon + 1) + local label = LABELS[key] or key + lines[#lines + 1] = string.format("%-18s %s", label .. ":", val) + end + end + lines[#lines + 1] = string.rep("-", 40) + + return { + kind = "text", + data = table.concat(lines, "\n"), + } + end, +} diff --git a/tools/test_drone.py b/tools/test_drone.py new file mode 100644 index 0000000..e703510 --- /dev/null +++ b/tools/test_drone.py @@ -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() diff --git a/tools/test_plot.py b/tools/test_plot.py index 5847431..4524670 100755 --- a/tools/test_plot.py +++ b/tools/test_plot.py @@ -1,5 +1,5 @@ #!/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 math @@ -116,7 +116,7 @@ def is_disconnect_error(err: OSError) -> bool: 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("--port", type=int, default=8080) parser.add_argument("--channels", type=int, default=1) diff --git a/tools/test_plot_serial.c b/tools/test_plot_serial.c index 86a7117..2fb7e24 100644 --- a/tools/test_plot_serial.c +++ b/tools/test_plot_serial.c @@ -429,7 +429,7 @@ int main(int argc, char **argv) { double frame_interval = (double)args.samples_per_channel / args.rate; 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("通道: %u 格式: %s\n", args.channels, format_name); printf("频率: %.1f Hz 振幅: %.1f\n", args.freq, args.amp); diff --git a/tools/test_text_serial.c b/tools/test_text_serial.c index a9d82e1..1878aae 100644 --- a/tools/test_text_serial.c +++ b/tools/test_text_serial.c @@ -208,7 +208,7 @@ int main(int argc, char **argv) { 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("通道: %u 振幅: %.1f 频率: %.1f Hz\n", args.channels, args.amp, args.freq); printf("速率: %.0f lines/sec (间隔 %.3f ms)\n", args.rate, interval * 1000.0);