Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@

- 新增 ZCode 插件清单与仓库市场定义,可从 `woooooooooolf/ser2mcp` 市场安装并自动加载 MCP 服务及两个 SKILL;现有 Reasonix / DSH 接入方式不受影响

### Changed

- 长文件传输在宿主并发能力未知时默认推荐根据估算与宿主超时显式设置 `max_duration_ms`;两个 SKILL 增加 5 步最小 happy path、BusyBox `stty raw -echo`、接收端就绪握手、大输出摘要对账和多串口实例提示

### Fixed

- 修复活动文件发送取消收尾期间,`uart_close` 已开始但排队的新写入/交换等普通 I/O 仍可能抢在最终释放端口前执行的竞态;关闭状态现在同步拒绝新的普通 I/O/配置,多个并发 close 等待者也会全部被唤醒,close 返回后端口不会被 `uart_write` 隐式重开

## [0.8.7] - 2026-08-18

### Added
Expand Down
6 changes: 5 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,14 @@ Important semantic boundaries:
- `uart_expect_send.newline` applies to `reply`. For a terminal reply, use `reply_mode="text"` with `newline="crlf"` instead of embedding the line ending in the reply text.
- By default, `uart_expect` consumes only through the end of the pattern. Follow it with `uart_read` when `pending=true` (equivalent to `buffered_bytes > 0`). `pending=false` is still only an instantaneous snapshot; continue waiting according to the device protocol when later output is required.
- Tools with arguments reject unknown fields instead of silently ignoring them. `buffer_size` can only be set by `uart_open`; close and reopen the port to change it.
- Multiple COM entries with the same USB serial may be separate UART instances exposed by one chip. Do not merge them by serial alone; confirm each port name and function.
- Once `uart_close` starts, newly queued ordinary I/O and configuration calls are rejected. `uart_write` never implicitly reopens a port after close; call `uart_open` explicitly before continuing.
- `overflow_delta > 0` means that ring-buffer data was overwritten, so the current read has a gap.
- The overflow fields from `uart_send_file` are return-time snapshots. Check the latest `overflow_total` with `uart_available` or `uart_read` afterward; zero is not final proof that no overflow occurred.
- `uart_send_file` blocks until it finishes by default. Optional `max_duration_ms` is an explicit automatic safeguard that returns `reason="duration_limit"`; normally, wait for the estimate-based transfer duration.
- `uart_send_file` blocks until it finishes by default. For long transfers or hosts with unknown concurrency support, explicitly set `max_duration_ms` from the estimate and host call timeout; expiry returns `reason="duration_limit"`.
- Before sending a file, have the peer emit a distinct readiness marker after applying its tty mode and immediately before entering the receiver; wait for it with `uart_expect`. Do not stream immediately after the receiver command's `uart_write` returns.
- `uart_send_file` returning `reason="completed"` only means that the server finished writing. Confirm end-to-end integrity with peer byte counts and a hash of the decoded content.
- For large output or file reconciliation, have the peer return only `wc -c` plus `sha256sum` (or `md5sum` when unavailable); do not pull the complete content into the agent context.

## Validation and Development

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,14 @@ Reasonix 与 ZCode 安装插件后会同时获得这两个 SKILL。Claude Code
- `uart_expect_send.newline` 作用于 `reply`;终端回复可传 `reply_mode="text"` 和 `newline="crlf"`,不需要把行尾嵌入 reply 文本
- `uart_expect` 默认只消费到 pattern 结尾;返回的 `pending=true`(等价于 `buffered_bytes > 0`)时补一次 `uart_read`。`pending=false` 仍只是瞬时快照;确实需要 pattern 后的未来输出时继续按协议等待
- 带参数工具的未知字段会报错,不再静默忽略;`buffer_size` 只能在 `uart_open` 时设置,需调整时先关闭再重新打开端口
- `uart_list_ports` 中相同 USB serial 的多个 COM 项可能是同一芯片的多个串口实例;不能只凭 serial 合并,应按端口名与功能确认
- `uart_close` 一经开始会拒绝排队的新普通 I/O/配置;返回后 `uart_write` 不会隐式重开端口,继续操作必须显式 `uart_open`
- `overflow_delta > 0` 表示环形缓冲已有数据被覆盖,当前读取结果存在缺口
- `uart_send_file` 的 overflow 是返回时快照;返回后用 `uart_available` / `uart_read` 再确认最新 `overflow_total`,0 不代表最终无溢出
- `uart_send_file` 默认同步阻塞至结束;可选 `max_duration_ms` 只在显式设置时自动止损并返回 `reason="duration_limit"`,通常仍应根据估算等待完成
- `uart_send_file` 默认同步阻塞至结束;长传输或宿主并发能力未知时,默认根据估算与宿主调用超时显式设置 `max_duration_ms`,到限返回 `reason="duration_limit"`
- 文件发送前应让对端在完成 tty 模式切换后、紧邻接收命令前输出独立就绪标记,`uart_expect` 命中后再发送;不要在接收命令的 `uart_write` 返回后立刻推流
- `uart_send_file` 的 `reason="completed"` 只表示服务器已完成写入;端到端完整性必须用对端长度和解码后哈希确认
- 大输出或文件对账让板端只返回 `wc -c` 与 `sha256sum`(不可用时 `md5sum`)摘要,不要把完整内容读进 Agent 上下文

## 验证与开发

Expand Down
32 changes: 23 additions & 9 deletions skills/ser2mcp-file-transfer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,43 @@ description: 使用 ser2mcp 经 UART/COM 串口发送本地文件或固件。用

# ser2mcp 文件传输

## 5 步最小 happy path

以下按长度接收的 Linux Shell 模板适合原始二进制;先把 `N`、路径、端口和时限替换为估算结果与实际值:

1. `uart_send_estimate {path: "C:/tmp/fw.bin", mode: "text", chunk_size: 256, baudrate: 115200}`。
2. `uart_expect {port: "COM3", data: "stty raw -echo; printf SER2MCP_%s RX_READY; dd bs=1 count=N of=/tmp/fw.bin", mode: "text", newline: "lf", pattern: "SER2MCP_RX_READY", pattern_mode: "text", match_scope: "new", read_mode: "text-escaped"}`,确认 tty 已切换且即将进入接收命令。
3. 长传输调用 `uart_send_file {port: "COM3", path: "C:/tmp/fw.bin", mode: "text", chunk_size: 256, max_duration_ms: ...}`;时限按下文规则由估算和宿主超时确定。
4. 接收结束后执行 `stty sane; wc -c /tmp/fw.bin; sha256sum /tmp/fw.bin`(无 `sha256sum` 时用 `md5sum`),只读取长度和哈希摘要并与源文件对账。
5. 调用 `uart_available {port: "COM3"}` 检查最终 `overflow_total`,然后 `uart_close {port: "COM3"}`。

## 必须遵守

- 先确认用户授权把指定本地文件发送到目标设备。`path` 可指向 ser2mcp 进程有权访问的任意普通文件,服务端不限制目录。
- 先调用 `uart_send_estimate`,向用户说明预计字节数和耗时,再调用一次 `uart_send_file`;不要循环调用 `uart_write`。
- `uart_send_file` 默认同步阻塞至结束;估算可接受时优先等待完成,不要把取消或短时限当作常规分段机制。
- `uart_send_file` 默认同步阻塞至结束。预计为长传输(例如至少 30 秒)或宿主并发能力未知时,默认显式设置 `max_duration_ms` 自动止损;不要把取消或短时限当作常规分段机制。
- 根据对端缓冲能力选择 `chunk_size`,不确定时从默认值 256 开始;无流控时宁小勿大。
- 把 `reason` 解释为服务器端结束状态,不解释为对端完整接收。
- 传输完成后在对端核对字节数和解码后哈希;只有对账一致才能确认端到端完整性。
- 大输出只读取板端 `wc -c` 与 `sha256sum`(不可用时 `md5sum`)结果,不要把接收文件或完整日志读进 Agent 上下文。
- `uart_send_file` 返回后立即调用 `uart_available` 或 `uart_read`,以最新 `overflow_total` 确认上行缓冲是否覆盖;发送返回中的 0 不是最终无溢出证明。
- ser2mcp 不主动发送 EOF。开始发送前先确定对端按长度结束,还是需要调用方另发 EOF。
- 准备对端时先让它在 tty 模式切换后、紧邻接收命令前输出独立的就绪标记,并用 `uart_expect` 命中后再发送文件;只调用 `uart_write` 后立刻发送会与板端执行 `stty`/进入接收命令竞态。

## 执行流程

1. 确认本地 `path`、目标 `port`、文件用途和用户授权范围。
2. 确定对端接收方式:
- 对端可按确定长度读取:优先用 `dd bs=1 count=N`,无需 EOF。
- 对端使用 icanon `cat`:可用 base64,并在发送后另发 `\x04` 结束输入。
- 对端需要原始二进制:先关闭 tty 字节转换,例如 Linux 使用 `stty raw`
- 对端需要原始二进制:先关闭 tty 字节转换和回显,例如 Linux 使用 `stty raw -echo`;BusyBox 的 `raw` 不保证关闭 echo
3. 调用估算:

```text
uart_send_estimate {path, mode?, chunk_size?, gap_ms?, baudrate?}
```

4. 准备对端接收,再调用一次:
4. 准备对端接收并等待它输出就绪标记,再调用一次:

```text
uart_send_file {port, path, mode?, chunk_size?, gap_ms?, max_duration_ms?}
Expand Down Expand Up @@ -58,11 +70,13 @@ description: 使用 ser2mcp 经 UART/COM 串口发送本地文件或固件。用
| `mode="base64"` | 连续编码整个文件,padding 仅在 EOF;每 76 字符换行,末尾补 `\n`。适合文本安全通道或 icanon 行缓冲。 |
| `chunk_size` | 原始文件分片大小,默认 256,范围 `1..=1 MiB`。应不大于对端可安全接收的缓冲;base64 输出约为原始数据的 1.34 倍并含换行。 |
| `gap_ms` | 分片间隔,默认 0,最大 60000。仅在设备处理能力低于串口持续输入速率时增加。 |
| `max_duration_ms` | 可选自动止损时限,默认不限制。仅在不能接受无限等待或调用预算明确时设置;达到后在检查点返回部分进度。 |
| `max_duration_ms` | 可选自动止损时限,默认不限制。长传输或宿主并发能力未知时默认显式设置;达到后在检查点返回部分进度。 |
| `baudrate` | 只用于估算,默认 115200;应与实际串口波特率一致。 |

理论参考:1 MiB @ 115200 时,text 下限约 91 秒,base64 约 123 秒,均未计 flush、调度和 `gap_ms` 开销。以 `uart_send_estimate` 的当前结果为准。

长传输的时限选择:先用 estimate 取得 `est_time_ms`。若宿主工具超时已知,先确保它大于完整传输估算;`max_duration_ms` 可从 `max(est_time_ms × 1.5, est_time_ms + 30000)` 起步,并至少给宿主保留 5–10 秒返回余量。若宿主超时容不下估算与余量,应先提高宿主超时或缩小任务,而不是设置一个必然截断的短时限;被截断的 base64 流不能假定可直接续传。

## 解释发送结果

- `reason="completed"`:服务器发送循环已把全部输出字节写入串口驱动;仍需对端对账。
Expand All @@ -75,27 +89,27 @@ description: 使用 ser2mcp 经 UART/COM 串口发送本地文件或固件。用
- `chunks`:已完成写入的输出分片数。
- `overflow_delta` / `overflow_total`:从发送开始到生成返回时,读线程已观察到的上行环形缓冲覆盖快照。串口驱动或线路中的尾部字节可能在返回后继续推高计数;即使返回 0,也要用随后的 `uart_available` / `uart_read` 获取最新 `overflow_total`。这些字段不是下行传输完整性证明。

发送期间服务端允许用 `uart_available` 查看 `send.active`、`sent_bytes`、`total_bytes`、`chunks` 和 `last_reason`,也允许 `uart_send_cancel` 请求取消;宿主必须支持并发调用,或在前一次会话/任务停止等待后仍能访问同一 ser2mcp 服务。严格串行且持续等待当前调用的宿主无法同时发出取消,此时依赖事前估算或显式 `max_duration_ms`。普通 I/O、配置和 expect 调用仍会等待全局 I/O 锁。
发送期间服务端允许用 `uart_available` 查看 `send.active`、`sent_bytes`、`total_bytes`、`chunks` 和 `last_reason`,也允许 `uart_send_cancel` 请求取消;宿主必须支持并发调用,或在前一次会话/任务停止等待后仍能访问同一 ser2mcp 服务。严格串行且持续等待当前调用的宿主无法同时发出取消,此时长传输必须依赖事前估算和显式 `max_duration_ms`。普通 I/O、配置和 expect 调用仍会等待全局 I/O 锁。

## 对端接收示例

Base64 写入 Linux 文件:

```text
uart_write {port, data: "stty -echo; cat > /tmp/f.b64", mode: "text", newline: "lf"}
uart_expect {port, data: "stty -echo; printf SER2MCP_%s RX_READY; cat > /tmp/f.b64", mode: "text", newline: "lf", pattern: "SER2MCP_RX_READY", pattern_mode: "text", match_scope: "new", read_mode: "text-escaped"}
uart_send_file {port, path: "C:/tmp/fw.bin", mode: "base64", chunk_size: 256}
uart_write {port, data: "04"}
uart_exchange {port, data: "wc -c /tmp/f.b64; base64 -d < /tmp/f.b64 | sha256sum", mode: "text", newline: "lf", read_mode: "text-escaped"}
uart_exchange {port, data: "stty sane; wc -c /tmp/f.b64; base64 -d < /tmp/f.b64 | sha256sum", mode: "text", newline: "lf", read_mode: "text-escaped"}
```

确认对端编码文件字节数等于 `sent_bytes`,解码后 SHA-256 等于源文件。

原始二进制按长度接收:

```text
uart_write {port, data: "stty raw; dd bs=1 count=65536 of=/tmp/f.bin", mode: "text", newline: "lf"}
uart_expect {port, data: "stty raw -echo; printf SER2MCP_%s RX_READY; dd bs=1 count=65536 of=/tmp/f.bin", mode: "text", newline: "lf", pattern: "SER2MCP_RX_READY", pattern_mode: "text", match_scope: "new", read_mode: "text-escaped"}
uart_send_file {port, path: "C:/tmp/f.bin", mode: "text", chunk_size: 1024}
uart_exchange {port, data: "stty sane; wc -c /tmp/f.bin; sha256sum /tmp/f.bin", mode: "text", newline: "lf", read_mode: "text-escaped"}
```

把 `count` 设为源文件精确字节数。默认 tty 的 IXON、ICRNL 等转换会破坏任意二进制;发送原始字节前确认已进入 raw 模式。启动接收命令时使用 `newline="lf"`,避免 `\r\n` 中残留的 `\n` 被文件接收程序读入。
把 `count` 设为源文件精确字节数。默认 tty 的 IXON、ICRNL 等转换会破坏任意二进制;发送原始字节前必须等到 raw/-echo 切换后的独立就绪标记,不能在接收命令 `uart_write` 返回后立刻发送。启动接收命令时使用 `newline="lf"`,避免 `\r\n` 中残留的 `\n` 被文件接收程序读入。对账只读取 `wc -c` 和哈希摘要;BusyBox 缺少 `sha256sum` 时可改用 `md5sum`,并与源文件的同算法结果比较
15 changes: 14 additions & 1 deletion skills/ser2mcp-usage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,27 @@ description: 通过 ser2mcp 的 uart_* MCP 工具操作 UART/COM 串口设备。

# ser2mcp 串口操作

## 5 步最小 happy path

以下 Linux Shell 示例可直接作为起点;把 `COM3`、波特率、命令、行尾和完成 pattern 换成目标设备的实际协议:

1. `uart_list_ports {}`,按端口名和用途选择目标,不只按 USB serial 合并。
2. `uart_open {port: "COM3", baudrate: 115200}`。
3. `uart_expect {port: "COM3", data: "printf 'SER2MCP_%s\\n' OK", mode: "text", newline: "lf", pattern: "SER2MCP_OK", pattern_mode: "text", match_scope: "new", read_mode: "text-escaped"}`。
4. 确认 `matched=true` 且 `overflow_delta=0`;仅当 `pending=true` 时补一次 `uart_read {port: "COM3", read_mode: "text-escaped"}`。
5. `uart_close {port: "COM3"}`。

## 必须遵守

- 按 `uart_list_ports → uart_open → 交互 → uart_close` 操作;重复打开同一端口前先关闭。
- `uart_list_ports` 中相同 USB serial 对应多个 COM 项时,可能是同一芯片暴露的多个串口实例;保留每个端口名并按实际功能逐一确认。
- 除 `uart_list_ports` 和 `uart_send_estimate` 外,调用时都传 `port`。
- 一次只发送一条命令,并用设备协议定义的响应特征判断完成;不要用 sleep 盲等。
- 把 `matched=true` 解释为“pattern 按所选原始字节/忽略 ANSI 语义在匹配范围内命中”,不要直接解释为当前事务成功。
- 终端命令和 `uart_expect_send.reply` 显式带行尾。通常用 `newline="crlf"`;已知设备只需 LF 时用 `lf`。
- 把 `reason="idle"` 解释为“字节流暂时静默”,不要解释为命令已完成。
- 检查每次读取结果的 `overflow_delta`;大于 0 表示数据已被覆盖,当前结果有缺口。
- 大输出优先让板端重定向到文件,再只读 `wc -c` 与 `sha256sum`(不可用时 `md5sum`)摘要对账;不要把完整内容读进 Agent 上下文。
- 大文件或固件使用 `ser2mcp-file-transfer`,不要循环调用 `uart_write`。

## 选择工具
Expand Down Expand Up @@ -105,6 +117,7 @@ uart_expect_send {port: "COM3", pattern: "Hit any key", pattern_mode: "text", re
- 需要显式结束标记且不能关闭回显:把标记拆开写在命令中,例如等待 `SLEEP-DONE-MARK` 时发送 `sleep 8; printf '%s%s\n' 'SLEEP-DONE-' 'MARK'`。回显不含连续的完整 pattern,实际输出才包含。
- 需要清除板端当前输入行:仅在确认 tty 为 icanon 时发送 `\x15`(Ctrl+U);需要中断当前命令时可发送 `\x03`(Ctrl+C)。`uart_clear` 只清宿主缓冲,不清板端状态。
- 输出缺失或设备拔出:调用 `uart_available` 检查 `read_error` 和 `overflow_total`。
- `uart_close` 已开始时,新的普通 I/O/配置会报错;`closed=true` 返回后端口保持关闭,`uart_write` 不会隐式重开,继续操作前必须显式 `uart_open`。

## 资源边界

Expand All @@ -113,4 +126,4 @@ uart_expect_send {port: "COM3", pattern: "Hit any key", pattern_mode: "text", re
- 所有带参数的工具都拒绝未知字段;拼写错误或把 `buffer_size` 传给 `uart_configure` 会返回参数错误
- read/exchange/expect `timeout_ms`:最大 `300000`
- expect pattern:编码后最大 `64 KiB`
- 普通 I/O、配置、expect 和 close 共享全局 I/O 锁;文件发送期间会排队。`uart_available` / `uart_clear` 不持有该锁;宿主允许并发或后续任务仍能访问同一服务时,`uart_send_cancel` 可请求取消。
- 普通 I/O、配置、expect 和 close 共享全局 I/O 锁;文件发送期间会排队。关闭一经开始,排队的新普通 I/O/配置会被拒绝。`uart_available` / `uart_clear` 不持有该锁;宿主允许并发或后续任务仍能访问同一服务时,`uart_send_cancel` 可请求取消。
Loading