- 工具表:scan_devices 返回形状 data.devices=[{name,address,rssi}];
device_status 注明触发连接守候且刷新空闲计时(周期轮询阻止 idle 断开)
- 启动参数表新增 --allowed-hosts;Windows 部署段同步(含 421 说明)
- 连接行为节:重连覆盖面改为传输层异常全家
- spec 末尾加「实测与终审偏离记录」:5 码契约(DEVICE_DISCONNECTED 未 emit)、
mac 解析顺序修正(启动 --mac > 自动扫描记住,工具面无 mac 参数)、
异常面适配、熠管家 Windows 实测结论与 421 修复
Co-Authored-By: Claude <noreply@anthropic.com>
148 lines
7.6 KiB
Markdown
148 lines
7.6 KiB
Markdown
# ppclock-mcp 设计:AI-native MCP 服务器(单层部署)
|
||
|
||
日期:2026-07-30 · 状态:已批准(用户免审,直接进入实现)
|
||
|
||
## 目标
|
||
|
||
把 ppclock SDK 的设备能力包装成 **MCP 服务器**,直接部署在蓝牙物理所在的
|
||
Windows 服务器上(单层,不经 bridge),供各 agent 平台调用。
|
||
|
||
- 传图、不同状态显示、时间核对等高层能力,AI-native 工具语义
|
||
- Windows 先行实测;Linux 保持代码兼容(bleak 跨平台),暂不实测
|
||
- Windows 部署与联调由 worker 与熠管家协作完成,用户不参与过程
|
||
|
||
## 架构
|
||
|
||
```
|
||
agent 平台(Claude Code/Desktop/其他)
|
||
│ stdio(本机 agent) 或 streamable HTTP(局域网 agent,bearer token)
|
||
▼
|
||
ppclock-mcp(运行在蓝牙所在服务器,单层,无 bridge)
|
||
├─ mcp_server.py FastMCP 装配:工具注册、传输启动、HTTP 鉴权、CLI 参数
|
||
├─ device_manager.py 连接管理器(方案 C)
|
||
└─ tools.py 高层工具薄包装:参数校验 + 统一输出契约
|
||
▼
|
||
ppclock SDK(via_local,bleak;Windows=WinRT / Linux=BlueZ)→ BLE → NRF- 设备
|
||
```
|
||
|
||
**SDK 零改动**:MCP 层只是 SDK 的消费者。
|
||
|
||
### 连接管理(方案 C:按需连接 + 空闲保持)
|
||
|
||
- 一把 `asyncio.Lock` 串行所有设备操作(BLE 适配器独占,先到先得)
|
||
- 首次操作守候连接:扫描/连接超时默认 90s(覆盖设备 30–60s 广播窗口)
|
||
- 操作完成后保持连接;空闲超过 `--idle-timeout`(默认 300s)自动断开,
|
||
设备恢复睡眠省电
|
||
- 操作中连接丢失 → 自动重连并重试**一次**,再失败才报错
|
||
- 配置退化:`--idle-timeout 0` = 每次独立连接;很大值 ≈ 常驻连接
|
||
|
||
### 传输与鉴权
|
||
|
||
- 官方 `mcp` Python 包(FastMCP),一份实现双模:`--transport stdio|http`
|
||
- HTTP = streamable HTTP,默认 `--host 127.0.0.1 --port 8972`(避开 bridge 8971)
|
||
- 鉴权:环境变量 `PPCLOCK_MCP_TOKEN`;绑非回环地址时**必须**设置,
|
||
请求头 `Authorization: Bearer <token>`,不符即 401
|
||
- stdio 模式无鉴权(进程隔离即边界)
|
||
|
||
### 安装形态
|
||
|
||
- 本仓库内:`pyproject.toml` 增加 optional extra `mcp = ["mcp>=1.2"]`
|
||
- 新入口脚本:`ppclock-mcp = ppclock.mcp_server:main`
|
||
- 基础 SDK 依赖不增加
|
||
|
||
## 工具面(13 个)
|
||
|
||
| 工具 | 参数 | 说明 |
|
||
|---|---|---|
|
||
| `scan_devices` | `timeout=5.0` | 扫描 NRF- 设备 → `[{name,address,rssi}]` |
|
||
| `device_status` | — | 连接状态/绑定 MAC/设备 ID(连接中时)/空闲秒数 |
|
||
| `set_time` | `tz=8.0` | 对时(默认北京) |
|
||
| `set_mode` | `mode` | clock1-3/calendar1-3/image0-3/tricolor/mono |
|
||
| `toggle` | `name` | invert/font/rotate180/hour_format/clock_color |
|
||
| `upload_image` | `source, slot=0, algo="atkinson", mono=false, **adjust` | source = 服务器本地路径 **或** base64(可带 data URI 前缀);返回 `{bytes_bw,bytes_red,small,slot}` |
|
||
| `render_template` | `name, payload, slot=0, algo="atkinson"` | 6 模板本地渲染上传 |
|
||
| `countdown` | `date, mode="clock", prefix=None` | date 为 ISO `YYYY-MM-DD` |
|
||
| `countdown_off` | — | 关闭倒计时 |
|
||
| `calendar_text` | `text` | 日历中文文字并切日历模式一 |
|
||
| `set_sleep` | `on, start_h, end_h` | 休眠时段(0–23) |
|
||
| `clear_screen` | — | 刷屏 |
|
||
| `raw_send` | `data_hex, channel="alt"` | 逃生舱直发(hex 字符串) |
|
||
|
||
不暴露:OTA、激活、LUT、WiFi、轮播(高危或本固件 no-op)。
|
||
|
||
### 输出契约(与 CLI `--json` 一致)
|
||
|
||
成功 `{"ok": true, "tool": <name>, "data": {...}}`;
|
||
失败 `{"ok": false, "tool": <name>, "error": {"code", "message", "hint"}}`。
|
||
|
||
错误码小集合:`INVALID_PARAM` / `DEVICE_NOT_FOUND` / `CONNECT_TIMEOUT` /
|
||
`BLE_ERROR` / `DEVICE_DISCONNECTED` / `INTERNAL`。`DEVICE_NOT_FOUND` 与
|
||
`CONNECT_TIMEOUT` 的 hint 说明设备睡眠与守候窗口机制。
|
||
|
||
## 数据流(典型)
|
||
|
||
1. agent 调 `upload_image(base64…)` → tools 校验/解码 → manager 取锁
|
||
2. 无连接 → `PPClient.via_local(mac)` 守候连接(≤90s)
|
||
3. SDK 图像管线(裁剪 400×300→抖动→双平面打包)→ BLE 分块上传 → 返回字节统计
|
||
4. 刷新空闲计时器;锁释放;空闲 300s 后自动断开
|
||
|
||
mac 解析顺序:工具参数 > 启动 `--mac` > 自动扫描首个 NRF-(并记住)。
|
||
|
||
## 测试
|
||
|
||
- **单元**(无硬件):
|
||
- device_manager:fake transport 注入,覆盖串行锁、空闲超时断开、
|
||
掉线重连重试一次、守候超时错误
|
||
- tools:参数校验、base64/路径两种图源、错误契约形状
|
||
- **MCP 协议层**:FastMCP 内存 ClientSession 端到端调工具(fake transport)
|
||
- **Windows 实测**(熠管家协助):scan → set_time → upload_image → set_mode
|
||
目视确认上屏;HTTP 模式 token 鉴权正/反例;stdio 模式本机 agent 挂载
|
||
- **Linux**:仅保证导入与单元测试通过(bleak 分支兼容),不做硬件测试
|
||
|
||
## 部署(Windows,熠管家协作)
|
||
|
||
1. 服务器准备 Python ≥3.10 venv;源码部署(git clone 或拷贝)
|
||
2. `pip install -e ".[mcp]"`(注意 bleak WinRT 后端,无需额外系统组件)
|
||
3. 设 `PPCLOCK_MCP_TOKEN`;启动
|
||
`ppclock-mcp --transport http --host 0.0.0.0 --port 8972 --mac AA:BB:…`
|
||
4. 防火墙放行 8972(仅局域网);**不动**既有服务与 8971 bridge
|
||
5. 测试期手动启动即可;稳定后再议是否注册为 Windows 服务(超出本期范围)
|
||
|
||
熠管家协作边界:worker 提供部署脚本/步骤与验证清单;熠管家提供 Windows
|
||
访问与执行;用户不参与。
|
||
|
||
## 文档与版本
|
||
|
||
- 版本 0.2.0;CHANGELOG、README(MCP 段)、llms.txt、docs/sdk/API.md 同步
|
||
- 新增 docs/sdk/MCP.md:工具参考 + agent 挂载示例(stdio/HTTP 双例)
|
||
|
||
## 明确不做(YAGNI)
|
||
|
||
- 图片 URL 拉取(保持离线;agent 可自行下载后走 base64)
|
||
- 多设备并发(单适配器串行已够;mac 参数可切换设备)
|
||
- MCP resources/prompts(本期只 tools)
|
||
- Windows 服务化、Linux 硬件实测(后续迭代)
|
||
|
||
## 实测与终审偏离记录(2026-07-30)
|
||
|
||
实现与终审后对原设计的偏离,逐条留痕:
|
||
|
||
- **错误码实为 5 码**:`DEVICE_DISCONNECTED` 未 emit——操作中途掉线被
|
||
DeviceManager 的"重连+重试一次"吸收,重试仍败按 `BLE_ERROR` 上报;工具面
|
||
始终只看到最终失败,无法区分"中途掉过线"。契约保留 5 码:
|
||
`INVALID_PARAM` / `DEVICE_NOT_FOUND` / `CONNECT_TIMEOUT` / `BLE_ERROR` /
|
||
`INTERNAL`。
|
||
- **mac 解析顺序修正**:实际为"启动 `--mac` > 自动扫描首个 `NRF-` 前缀并记住",
|
||
工具面无 mac 参数(原设计的"工具参数 > …"未实现;多设备切换靠重启服务器
|
||
换 `--mac`,归入 YAGNI 多设备并发)。
|
||
- **异常面适配真实硬件**:全部测试 fake 原来只抛英文 `transports.base.
|
||
TransportError`;真实 bleak 路径抛的是中文 `transports.local.TransportError`
|
||
(与 base 同名但互不继承)与**未包装**的 `BleakError`(如 `Not connected`)。
|
||
终审后 MCP 层捕获面扩为异常全家(两个 TransportError + BleakError + OSError
|
||
+ asyncio.TimeoutError),错误码按类型优先、消息(中英文)次之映射。
|
||
- **2026-07-30 熠管家 Windows 实测**:stdio 全链路通过(scan → connect →
|
||
set_time → upload_image 15000+15000 字节 → set_mode image0;远程无法目视
|
||
确认上屏)。HTTP 模式 LAN Host 被 mcp DNS 重绑定防护 421——已修:新增
|
||
`--allowed-hosts`(逗号分隔 Host 白名单,`TransportSecuritySettings`
|
||
实现,mcp 1.29.0 实测 API 与设计一致),Windows 部署文档同步更新。
|