Compare commits

...
17 Commits
Author SHA1 Message Date
yi-bot 578cd7fa20 docs: solidify HM42 firmware protocol 2026-08-03 03:42:59 +00:00
yi-bot 2919ebe312 fix: use HM42 vendor image upload session 2026-08-03 03:12:48 +00:00
yi-bot 092b104e3d fix: complete HM42 monochrome image planes 2026-08-03 02:40:00 +00:00
yi-bot 3db8295654 docs: record HM42 RXTX channel evidence 2026-08-03 02:02:33 +00:00
yi-bot da02bd31c6 fix: select RXTX channel by firmware family 2026-08-03 01:57:44 +00:00
agent 34dd6b4c52 docs: 项目级规则 review 打结——GOVERNANCE §2/§5 更新至 0.2.0-beta 现实、PROGRESS 快照标注、Task9 实机完成、DECISIONS 去重标注与留痕、README 依赖补 qrcode 2026-08-02 14:29:34 +00:00
agent faf8db4cbb docs: Gitea 远端上线记录(URL/认证/治理闭环) 2026-08-02 14:04:55 +00:00
agent 3ce4fc0f0a docs: GOVERNANCE §6 Git 远端协作治理 + DECISIONS 2026-08-01 2026-08-02 13:31:19 +00:00
chenweiandClaude 4ba51e8770 chore: gitignore 加 ._*(AppleDouble 垃圾,._llms.txt 已删)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 15:02:22 +00:00
chenweiandClaude a1041b9bad docs: 打结审计必修落地——DECISIONS 补方案C连接管理与双TransportError两条;CHANGELOG 常驻部署当日闭环+qrcode 修正行;llms.txt 目录补 mcp_windows_deploy/field-test-plan/superpowers;README dev 依赖补 pytest-asyncio;test_bridge docstring 指向 ppclock.bridge_server;部署清单标时点件
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 15:01:52 +00:00
chenweiandClaude a1ed2c2736 fix: qrcode 补入 dependencies(0.1.0 遗留,templates qrcode 模板硬依赖);requirements 冻结 mcp==1.29.0
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 15:01:17 +00:00
chenweiandClaude 8a9019a925 docs: 生产链路目视闭环——set_mode image0 单步复测上屏(用户确认)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 12:27:16 +00:00
chenweiandClaude 6a5ffda9fb docs: 生产接口实测六步全过 + connected=false 解析误报闭环
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 04:33:20 +00:00
chenweiandClaude 161c54ad5e docs: MCP.md 常驻部署节 + PROGRESS 0.2.0-beta release 与生产性测试记录
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 01:35:33 +00:00
chenweiandClaude 5130bb7aa2 docs: 测试数 207→208(与全量回归真值一致)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 01:05:58 +00:00
chenweiandClaude f975827971 docs: 0.2.0-beta 同步——CHANGELOG 复测与 beta 修正、测试数 195→207、DECISIONS 终审放行与依赖钉版留痕、Windows 部署清单 main+--allowed-hosts
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 01:02:11 +00:00
chenweiandClaude e9b919eeba chore: 版本 0.2.0b1 + mcp 地板抬至 >=1.23(transport_security 自 1.23 引入 + CVE-2025-66416)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 01:02:09 +00:00
27 changed files with 475 additions and 47 deletions

No files matched your search

+1
View File
@@ -6,3 +6,4 @@ __pycache__/
analysis/apk/jadx-out/
analysis/apk/native/
analysis/firmware/fw.asm
._*
+32 -1
View File
@@ -1,6 +1,23 @@
# CHANGELOG
## [0.2.0] — 2026-07-30
## [Unreleased] — 2026-08-03
- 修复同型号异固件设备的控制通道选择:`HM42_AIO_*` 自动使用 Web-BLE/APK
实际采用的 `1F1F`,已验证 `PP_da14585_4.2` 保持使用 `331F`。
- 本地 BLE 连接时读取 DIS 固件版本 `2A26`;若固件未知,则以 GATT 特征是否存在
在 `331F`/`1F1F` 间安全回退。
- write-only 通道不再强行订阅通知;设备状态新增 `firmware_revision` 与
`rxtx_uuid`,可直接审计当前固件档案与控制通道。
- HP 真机 `HM42_AIO_V1.0.9` 验证:直接 `1F1F` 模式命令由用户目视确认生效;
部署修复后 MCP 报告所选通道为 `00001f1f-...`,`set_mode(clock3)` 返回成功。
- 自动化测试:212 项通过。
## [治理] — 2026-08-02
项目级规则 review 打结(非产品变更):GOVERNANCE §2 目录结构/§5 工具链更新至 0.2.0-beta 现实(jadx→androguard、pyproject 为依赖准绳、交付物扩展为 SDK+CLI+bridge+MCP+examples)、PROGRESS 首日快照误标"最终状态"更正、详细计划 Task 9 标实机完成、DECISIONS 条目 18 标注被取代、README 依赖声明补 qrcode。
## [0.2.0-beta] — 2026-07-31
新增 **ppclock-mcp**:跑在蓝牙物理所在机器上的单层 MCP 服务器,agent 经 stdio 或
streamable HTTP 直驱墨水屏。
@@ -19,6 +36,20 @@ streamable HTTP 直驱墨水屏。
- 有意不暴露:OTA / 激活 / LUT / WiFi / 轮播(高危或本固件 no-op,走 SDK/CLI)
- 文档:`docs/sdk/MCP.md`(工具参考 + agent 挂载 + Windows 部署)
### beta 修正与复测(2026-07-31)
- **HTTP 复测通过**(熠管家,LAN 主机 192.168.61.56 → 192.168.61.35:8972):无 token 401 /
非法 Host 421 / 真实 LAN Host 200(serverInfo=ppclock)/ `device_status` 全链 ok;
详见 spec 末节(`docs/superpowers/specs/2026-07-30-ppclock-mcp-design.md`)
- **mcp 依赖地板抬至 `>=1.23`**:`transport_security`(`--allowed-hosts` 依赖的
DNS 重绑定防护)自 1.23 引入,且 1.23 起含 CVE-2025-66416 修复;钉 `mcp>=1.23,<2`
(2.0 将移除 FastMCP API)
- **`--allowed-hosts` 空串防护**:`--allowed-hosts ""` 逗号拆分得空列表,会启用防护并
连 localhost 族一并 421(自锁);现按 None 处理(保持 mcp 默认),含回归测试
- **qrcode 补入 dependencies**:0.1.0 遗留,`templates.py` qrcode 模板硬依赖
(`render_qrcode` 内 import qrcode),pyproject 现钉 `qrcode>=7`
- 常驻部署(Windows 服务化 + token 持久化)随熠管家进行,不在本发布内
(2026-07-31 当日已完成常驻部署并生产目视闭环,见 docs/sdk/MCP.md §常驻部署、PROGRESS 末节)
## [0.1.0] — 2026-07-30
首个 SDK 化发布。4.2寸墨水屏设备(DA14585 BLE,400×300 黑白红三色)的**完全离线**开发包:
+9 -1
View File
@@ -15,7 +15,7 @@
| 2026-07-28 | **RXTX 命令通道 = 331f(非 web 文档的 1f1f)** | 实机 GATT 诊断(bridge services op):1f1f(h=53) 仅 write 无 notify,写入 ATT-ACK 但设备不执行;331f(h=57) notify+write+read,经其实测 EFEF/模式/倒计时/停车牌全部生效 | ppclock 全栈切换(protocol.RXTX_CHAR_UUID=331f、BLETransport/BridgeTransport 写路径);保留 1f1f 为 legacy 常量 |
| 2026-07-28 | **设备 ID 反解参数序实机校正**:snprintf 实参序为 MAC5,MAC4(非固件代理读的 MAC4,MAC5) | 实机 ID '81233F3C267112' 仅按 MAC5 在前才能反解出广播地址 18:BC:5A:5D:BF:28(NVDS 反转序);按原序则掩码矛盾 | protocol.recover_mac_from_device_id 已修正并锁定实机向量测试;firmware-analysis §4.2 对应行以本条为准 |
| 2026-07-28 | 设备睡眠行为适配:广播窗口短(约 30-60s),连接态保持清醒 | 实测:空闲即停广播;单连接 batch 可在窗口内跑完全部命令 | 现场测试统一用 `ppclock batch` 单连接;唤醒守候循环(每 15-20s 重试)跨窗口执行 |
| 2026-07-29 | **SUOTA 状态码 8/16 = 会话信息码而非失败**(仅本固件) | 反汇编 MEM_DEV handler(0x07FCB6F8):type 0x13 命中 bit4 位测试 → 发状态 16 且 SPOTA 状态保持(0x07FCB770-0x07fcb772);type 0x12 → 状态 8 并清会话(0x07FCB780);空镜像探针与正式烧录均先收 16 后流程完好 | bridge ota 对 8/16 记录并继续,块确认只认 2;APK 错误表未覆盖 16,以固件证据为准 |
| 2026-07-29 | **SUOTA 状态码 8/16 = 会话信息码而非失败**(仅本固件)〔本条被 2026-07-29 第 5 条(状态码分两类)取代,保留溯源〕 | 反汇编 MEM_DEV handler(0x07FCB6F8):type 0x13 命中 bit4 位测试 → 发状态 16 且 SPOTA 状态保持(0x07FCB770-0x07fcb772);type 0x12 → 状态 8 并清会话(0x07FCB780);空镜像探针与正式烧录均先收 16 后流程完好 | bridge ota 对 8/16 记录并继续,块确认只认 2;APK 错误表未覆盖 16,以固件证据为准 |
| 2026-07-29 | bridge OTA 大请求需显式 StreamReader limit | asyncio 默认 64KiB 行限制,OTA 镜像 hex 请求 ~144KB 永不可达(三轮"静默停滞"根因);空镜像探针 0.6s 全通证明管线无恙 | build 9: start_server(limit=4MiB);教训:大载荷 RPC 协议设计必须考虑传输层帧限 |
| 2026-07-29 | **SUOTA 状态码分两类:会话信息码(8/16)与流程错误码**;16=MEM_DEV type 0x13 位测试应答(固件 0x07FCB770),状态保持 | 反汇编 MEM_DEV handler + 三轮烧录对照(16 恒在 MEM_DEV 后 ~1s 到达,块未受影响) | bridge 对 8/16 记录继续、块确认只认 2;APK 表未覆盖 16,固件证据为准 |
| 2026-07-29 | **SUOTA 状态 22/20 根因分层**:22=0x38000 产品头读电气失败(GPIO_MAP 引脚错);20=读成功但非 'pR'(引脚有效、0x38000 疑为空) | GPIO 探针(max_blocks=2 安全模式):A{00,00,06,05}→22、B{40,00,06,05}→22、C{05,06,00,40}→20;C=APK 整数 0x05060040 的 BE 字节,为唯一电气有效候选 | GPIO_MAP 采用 C(BE 序);剩余阻断=0x38000 产品头('pR')缺失/无效,见下条 |
@@ -24,3 +24,11 @@
| 2026-07-29 | **全量烧录实证安全性论证**:产品头校验失败时所有缓冲处理在写 flash 前中止(state[0x70] 恒 0),END 仅写 0xAA 于 [state[0x60]+2]=0x0002 而该字节原本就是 0xAA(无操作),复位后裸启原固件 | 反汇编 END 路径(0x07FCB7C6-0x07FCB7E4)与缓冲写路径(0x07FCB514 依赖中止的头部处理产物 state[0x74]) | 全量跑 OTA 的最坏结果=安全空转+复位;若中途校验通过则自然转正为真实烧录。用户授权下可执行 |
| 2026-07-29 | **用户明确授权全量烧录**(301 块+END+复位,gpio_map=BE 序 C),要求全程记录与 review | 用户原话:"授权 注意做好记录和review"(此前已授权"做一下审慎测试后刷机") | 执行范围:fw_tst.img(DIS 版本串 3 字符差异);留证:状态流日志 docs/suota-flash-log.jsonl;事后验证:scan/connect/DIS 读 |
| 2026-07-29 | **G4 固件可控路径选型:B(免线刷软件利用)**,放弃 UART 线刷恢复工厂布局(路径 A 文档保留备查) | 用户原话:"肯定是B";B 路径已具备:图像 flash 持久化写(0x2E000)、EF 激活 flash 写(0x3Fxxx)+ 离线 keygen、全部 BLE 功能可控、SUOTA 工具链就绪(工厂布局设备可直接复用) | 任务说明目标3 以 B 路径达标;A 路径(线刷 .bin + UART)作为可选增强留存 docs/firmware-layout.md |
| 2026-07-30 | MCP 连接管理选型**方案 C:按需连接 + 空闲保持 + 串行锁 + 掉线重连一次** | 三方拉扯:设备长睡眠(广播窗口仅 30-60s)vs agent 连续操作体验(每操作重连会撞睡眠窗口)vs 电池(常连使设备保持清醒耗电) | 首次操作守候连接(窗口覆盖 30-60s 广播),操作后保持连接、空闲 `--idle-timeout`(默认 300s)自动断开让设备睡眠;一把 asyncio.Lock 串行全部操作;操作中掉线重连重试一次再向上抛;退化配置:`--idle-timeout 0`(每操作即断,最省电)或大值(近常连,体验最佳);详见 docs/superpowers/specs/2026-07-30-ppclock-mcp-design.md |
| 2026-07-31 | 引入 mcp 依赖(ppclock-mcp extra)并钉 `>=1.23,<2` | 0.2.0-beta 新增 MCP 服务器层;GOVERNANCE 要求依赖引入留痕;mcp 2.0 移除 FastMCP/memory API | `<2` 防 2.0 API 断裂;`>=1.23` 因 `transport_security`(DNS 重绑定防护,`--allowed-hosts` 依赖)自 1.23 引入,且 1.23 起含 CVE-2025-66416 修复;本地与 Windows 实测均为 1.29.0 |
| 2026-07-31 | 0.2.0-beta 终审裁定放行项终裁:TRANSPORT_ERRORS 含 OSError 的理论变宽、token 非恒定时间比较、Image.open 惰性解码失败归入 INTERNAL | 终审提出的三处理论风险,需终裁留痕 | 均判低风险放行:OSError 变宽仅使极端平台错误归入 BLE_ERROR(语义可接受);token 比较在可信 LAN + Bearer 场景时序攻击不可行;Image.open 惰性解码异常有 INTERNAL 兜底且 message 带类型详情 |
| 2026-07-31 | `--allowed-hosts ""` 空串按 None 处理 | 空串逗号拆分得空列表,`TransportSecuritySettings(allowed_hosts=[])` 启用防护后连 localhost 族也 421(自锁) | main() 解析层 `_parse_allowed_hosts` 以 `or None` 归一,保持 mcp 默认 localhost 族;含回归测试 |
| 2026-07-31 | **双 TransportError 历史包袱在 MCP 层适配,SDK 零改动** | `transports.base.TransportError` 与 `transports.local.TransportError` 同名但互不继承(历史包袱);真实 bleak 写/读路径还裸抛 `BleakError` 不包装 | 改 SDK 会动公开语义、风险大于收益;MCP 层 `device_manager.py:19-26` 以 `TRANSPORT_ERRORS` 全家捕获兜底(base/local 两类 + BleakError + OSError + asyncio.TimeoutError),掉线判定不漏形态 |
| 2026-08-01 | 建立 Gitea 远端(chenwei/qianbian)并增补 Git 协作治理(GOVERNANCE §6) | 用户要求版本控制上云 + 后续治理迭代;此前仓库仅本地 | main 主干直提、分支短命、身份诚实不改写、推送前三项检查(净树/全绿/无 secret)、大文件仅原始输入随库。**远端落地**:origin=git@git.b.aeroprop.xyz:chenwei/qianbian.git(实例 git.b.aeroprop.xyz,展示域 git.banytech.com);认证=本机 ~/.ssh/id_ed25519(Gitea key 名 chenwei@bany.tech@openclaw-server);2026-08-02 全历史已推送(ls-remote 校验一致) |
| 2026-08-02 | 项目级规则 review 打结:8 项不一致修复(PROGRESS 首日快照误标"最终状态"、GOVERNANCE §2 目录/§5 工具过期、PLAN P0 jadx、详细计划 Task 9 终态、DECISIONS 条目 18/20 重复标注、README 依赖缺 qrcode) | 用户要求规则 review;项目已从 0.1.0 演进至 0.2.0-beta(SDK/MCP/bridge/实机验证),多文档口径落后于现实 | 规则文件必须与交付现实一致;不改写历史(快照保留标注、重复条目保留溯源),仅修正指向与口径;GOVERNANCE 变更按 §4 留痕本条 |
| 2026-08-03 | **RXTX 通道按固件家族选择**:`HM42_AIO_*` 使用写入型 `1F1F`;已验证 `PP_da14585_4.2` 继续使用 `331F`;未知固件优先 `331F` 并按 GATT 存在性回退 | HP 侧同型号异供应商设备报告 `HM42_AIO_V1.0.9`;原 SDK 写 `331F` 均返回成功但屏幕不变。GATT 枚举显示其 `1F10` 控制服务只有 write-only `1F1F`;直写 Web-BLE 序列 `e10202` + `e2` 后用户目视确认切至“日历+表” | ATT 写成功不等于设备执行。连接时读取 DIS `2A26` 并选型;仅在特征支持 notify/indicate 时订阅,避免 write-only HM42 通道误启通知。状态接口公开固件串和实际 RXTX UUID,便于运维审计 |
+28 -7
View File
@@ -18,25 +18,35 @@ qianbian/
├── PLAN.md # 总体规划与阶段验收标准(指向 docs/superpowers/plans/)
├── PROGRESS.md # 实时进度日志:每次工作会话结束必须更新
├── DECISIONS.md # 决策日志:每条决策含日期、背景、结论、理由
├── CHANGELOG.md # 产品变更记录(release 口径)
├── llms.txt # AI 消费向项目卡(随发布同步)
├── app/ # 原始输入物(APK、固件,只读,不得修改)
├── docs/ # 所有分析文档
│ ├── architecture.md # 软硬件架构总述
│ ├── protocol.md # BLE 协议规范(最终单一事实来源)
│ ├── firmware-analysis.md # 固件逆向笔记
│ └── superpowers/plans/ # 详细实现计划
│ ├── firmware-layout.md # 闪存布局(裸/工厂)与 SUOTA 门禁
│ ├── field-test-plan.md / field-test-result.md # 全链路测试计划与实测
│ ├── sdk/ # API.md(签名级)/ MCP.md(MCP 服务器)
│ └── superpowers/{plans,specs}/ # 详细实施计划与设计规格
├── analysis/ # 逆向中间产物(反编译输出、脚本、抓包等)
│ ├── apk/ # APK 反编译产物(不入库的大文件除外)
│ ├── firmware/ # 固件反汇编/提取产物
│ └── web/ # Web 上位机抓取产物
├── src/ppclock/ # 本地 CLI 上位机(交付物)
├── tools/ # 固件处理等辅助工具(交付物)
└── tests/ # CLI 与工具的自动化测试
├── src/ppclock/ # 交付物:SDK+CLI(PPClient/ppclock)
│ ├── transports/ # base/local(bleak)/bridge(RPC) 传输层
│ ├── firmware/ # 镜像解析重打包 + SUOTA
│ ├── bridge_server.py # ppclock-bridge 服务端
│ └── device_manager.py / mcp_server.py / mcp_tools.py # ppclock-mcp
├── tools/ # 辅助工具(fw 垫片/field_test/gpio_probe 等)
├── examples/ # 示例与测试夹具(jsonl 批量、测试图)
└── tests/ # 自动化测试(当前 208 项)
```
规则:
- `app/` 内原始文件**只读**;所有修改副本放 `analysis/`。
- 每个分析结论必须写明证据(文件偏移 / 代码位置 / 抓包数据),禁止无证据断言。
- 交付物只有两类:`src/ppclock`(CLI)与 `tools/`(固件工具)+ `docs/`(文档)。
- 交付物:`src/ppclock`(SDK+CLI+bridge+MCP)、`tools/`、`examples/`、`docs/`。
## 3. 验证门禁(每个阶段必须满足才算完成)
@@ -60,6 +70,17 @@ qianbian/
## 5. 工具与环境
- 分析环境:Python 3.12(venv 于 `.venv/`)、jadx、capstone、bleak。
- 依赖锁定:`requirements.txt` 固定版本;新增依赖需记录于 DECISIONS.md。
- 分析环境:Python 3.12(venv 于 `.venv/`)、androguard(APK)、capstone(固件反汇编)、bleak(BLE)。
(注:jadx 因下载策略未采用,APK 分析全程 androguard。)
- 依赖声明以 `pyproject.toml` 为准(runtime:bleak/pillow/qrcode;extras:dev/mcp);
`requirements.txt` 为其冻结镜像。新增/升级依赖需记录于 DECISIONS.md。
- 外部工具一律装在本机,不修改系统关键组件。
## 6. Git 与远端协作(2026-08 增补,DECISIONS 2026-08-01)
1. **远端**:Gitea 实例,chenwei 账户下同名仓库 `qianbian`(remote 名 `origin`)。URL 见 DECISIONS.md 2026-08-01 条目。
2. **分支策略**:`main` 为唯一主干,直接提交;实验性大改动用 `<topic>` 短分支,回合并即删。不使用 force push 改写 main 历史。
3. **提交身份**:agent 工作用 `agent <agent@local>`,人工工作用本人身份——不混用、不改写历史身份,保证溯源诚实。
4. **推送前检查**:工作树干净、`pytest` 全绿、无新增 secret(token/私钥/真实密码)。测试用口令(如 `pw123456`)允许但必须在测试/文档语境。
5. **大文件**:`app/` 原始输入(APK 9.3MB、固件 72KB)随库分发(项目自包含要求);其余 >1MB 产物须为文档/证据类文本,二进制中间产物一律 gitignore + 再生方法入库(analysis/README.md)。
6. **同步纪律**:每个工作会话开始先 `git pull --ff-only`;结束前提交并 `git push`。冲突时以远端为准 rebase 本地未提交改动,禁止 `--force`。
+1 -1
View File
@@ -7,7 +7,7 @@
| 阶段 | 目标 | 对应任务说明 | 门禁 |
|------|------|--------------|------|
| P0 工具链 | venv + jadx/androguard/capstone/bleak | — | 自检全 OK |
| P0 工具链 | venv + androguard/capstone/bleak | — | 自检全 OK |
| P1 三线逆向 | Web JS → APK → 固件互证,产出 architecture.md + protocol.md | 目标1 | **G1** 三线互证一致 |
| P2 CLI 上位机 | 协议层/图像管线/命令层/CLI,功能对齐 Web 上位机 | 目标2 | **G2** 测试全绿+JSON schema;**G3** 功能全覆盖 |
| P3 增强+固件可控 | 本地模板渲染/批量/调度;固件解析重打包+SUOTA | 目标2增强、目标3 | **G4** 镜像可改可重生成;**G5** 断网验收 |
+40 -3
View File
@@ -1,6 +1,10 @@
# 进度日志 (PROGRESS)
## 2026-07-24(最终状态)
> **最终状态以 2026-07-29 收官节与后续各节为准**。本节(2026-07-24)为首日快照,
> 其中 G3/G4 的"协议级/工具级"标注已被 2026-07-28/29 实机验证超越:G3 ✅实机(除 K1 停车牌待查)、
> G4 ✅路径B达标、测试 132→208、交付物扩展为 SDK+CLI+bridge+MCP。
## 2026-07-24(首日快照)
### 已完成(对照门禁)
@@ -220,8 +224,14 @@
- 三新模块:`device_manager.py`(方案 C:按需连接+空闲超时保持+串行锁+掉线重连重试一次)/ `mcp_tools.py`(13 高层工具+统一 ok/error 契约)/ `mcp_server.py`(stdio+streamable HTTP 双模,PPCLOCK_MCP_TOKEN 鉴权,非回环强制 token,--allowed-hosts Host 白名单)
- 规格 spec + 实施计划入库(docs/superpowers/specs|plans);207 测试全绿(新增 42)
- **终审关键修复**:全部 fake 曾只说英文 TransportError——真实 bleak 抛中文 `transports.local.TransportError`(与 base 同名互不继承)与未包装 BleakError;捕获面/错误码映射已按真实异常语义重写(类型优先、中英文消息次之)
- **Windows 实测**(熠管家,banyWinServer 192.168.61.35,C:\ppclock):stdio 全链路通过(scan→connect→set_time→upload_image 15000+15000→set_mode image0;远程无法目视上屏);HTTP LAN Host 421(mcp DNS 重绑定防护)已修为 --allowed-hosts,复测待回报
- 环境事实:mcp 钉 >=1.10,<2(2.0 移除 FastMCP/memory API;实测 1.29.0)
- **Windows 实测**(熠管家,banyWinServer 192.168.61.35,C:\ppclock):stdio 全链路通过(scan→connect→set_time→upload_image 15000+15000→set_mode image0;远程无法目视上屏);HTTP LAN Host 421(mcp DNS 重绑定防护)已修为 --allowed-hosts,复测已命中(401/421/200/device_status ok,2026-07-31,详见 spec 末节)
- 环境事实:mcp 钉 >=1.23,<2(2.0 移除 FastMCP/memory API;1.23 起有 transport_security + CVE-2025-66416 修复;实测 1.29.0)
### 2026-07-31:0.2.0-beta release + 常驻部署(生产性测试开始)
- **release**:节点审计 8 项必修落地(版本 7 处统一 0.2.0b1、mcp 地板 >=1.23、--allowed-hosts 空串自锁修复+回归、文档数 208、DECISIONS 留痕);tag `v0.2.0-beta` @ 5130bb7(旧 v0.2.0 删除);208 测试全绿
- **常驻部署**(熠管家,banyWinServer):SYSTEM 计划任务 `BanyTech-PPCLOCK-MCP`(AtStartup + 999 次/1 分钟恢复 + runner 5 秒子进程重启);token CSPRNG 生成 ACL 仅 SYSTEM/Administrators;日志 `C:\ProgramData\OpenClaw\ppclock-mcp\logs\`;验证:LAN 200/device_status ok/杀进程 5 秒自动拉起复 200;8971 与防火墙不变
- **运维记录**:`/home/cwmine/vps/host-win/maintain_logs/2026-07-31_ppclock-mcp-production-test-service.md`;运行问题经 Bridge task `ppclock-mcp-ops` 回报本 session 统一迭代
- 文档:docs/sdk/MCP.md(工具参考+部署)、README/llms.txt/API.md/CHANGELOG 同步;工具清单 tools/mcp_windows_deploy.md
### 会话时间线
@@ -232,3 +242,30 @@
- P1:Web JS 协议提取(子代理)→ APK 反编译互证(子代理)→ 固件逆向(子代理)
- P2:protocol/image/commands/transport/cli/templates/textbitmap 全 TDD 实现
- P3:batch/本地模板/ota/keygen 增强;fw 工具链;文档终稿
**生产接口实测**(2026-07-31,熠管家代跑,原始 JSON 入库):
- A 疑案闭环:「connected=false」为 OpenClaw 侧解析误读(读了顶层 connected/mac 而非 data.*,缺字段强转 false),非服务问题;原始 payload data.connected=true / data.mac=18:BC:5A:5D:BF:28 / connect_count=2
- B 授权六步全过(每步 HTTP 200/isError=false/ok=true):set_time tz=8 → upload_image slot0(bytes 15000+15000)→ set_mode image0 → countdown 2026-12-31 目标 → countdown_off(自回滚)→ device_status connected=true;屏显目视归用户
- 观察项:supervisor.log 09:16-09:18 曾现 exit -1 每 6 秒重启循环约 2 分钟(疑端口占用竞态,runner 加日志重定向后 09:20:50 起稳定至今)——运维关注,非代码缺陷结论待续观
- **目视闭环**(2026-07-31):首轮"屏幕未变"系夹具顺序缺陷(countdown(mode=clock) 覆盖 image0 可见态);单步 set_mode(image0) 复测,**用户目视确认 MCP-PROD-TEST 测试图上屏**——生产链路 agent→MCP→BLE→屏 全链目视闭环
### 2026-08-02:Gitea 远端上线(治理闭环)
- 熠管家在 Gitea(git.b.aeroprop.xyz)chenwei 账户下创建空仓库 qianbian(API 复核 empty=true)
- 本机认证:~/.ssh/id_ed25519 已在 Gitea 注册(key 名 chenwei@bany.tech@openclaw-server)
- origin 已添加并全历史推送(ls-remote HEAD 与本地一致:3ce4fc0)
- 治理落地:GOVERNANCE §6(remote/分支/身份/推送检查/大文件/同步纪律)+ DECISIONS 2026-08-01(含 URL 与认证方式)
- 推送前审计:secret 扫描干净(仅协议格式串与测试口令)、提交身份 agent/chenwei 两类诚实并存不改写
### 2026-08-03:HM42 异固件控制通道适配
- 新设备 DIS 实读:`DA14585 / HM42_AIO_V1.0.9 / Dialog Semi`,区别于参考设备
`PP_da14585_4.2 / 1.0.0.0-LE`。
- 原 SDK 的 `331F` 写操作虽有 ATT ACK,但屏幕无变化;GATT 枚举确认 HM42 的
`1F10` 控制服务仅暴露 write-only `1F1F`。
- 复刻 Web-BLE 模式序列直写 `1F1F`,用户目视确认切换为“日历+表”,完成协议证据闭环。
- SDK 已实现按 DIS 固件串选通道、GATT 存在性回退及条件通知订阅;MCP 状态公开
`firmware_revision`/`rxtx_uuid`。自动化测试 212 项全绿。
- HP 运行源已备份后部署并校验哈希,远端编译通过;MCP 实读
`HM42_AIO_V1.0.9 + 1F1F`,随后 `set_mode(clock3)` 返回 HTTP 200/ok。
- 最终屏显 `clock3` 的现场目视确认仍由用户回报;在目视前不把 MCP 返回值单独算作上屏成功。
+2 -2
View File
@@ -12,7 +12,7 @@ Python SDK(`PPClient`)+ CLI(`ppclock`)+ BLE 桥(`ppclock-bridge`),
```bash
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]" # 依赖:bleak、pillow(dev: pytest)
.venv/bin/pip install -e ".[dev]" # 依赖:bleak、pillow、qrcode(dev: pytest、pytest-asyncio)
```
运行要求:蓝牙适配器(本机 bleak)**或** 他机蓝牙经 `ppclock-bridge` 桥接。**不需要互联网**。
@@ -115,7 +115,7 @@ ppclock --json ota firmware.img --yes # SUOTA
## 开发
```bash
.venv/bin/python -m pytest tests/ -q # 195 测试
.venv/bin/python -m pytest tests/ -q # 208 测试
```
治理规则见 `GOVERNANCE.md`;变更记录 `CHANGELOG.md`;决策日志 `DECISIONS.md`。
+11 -2
View File
@@ -131,12 +131,21 @@ ASCII UTF-8 直发:`WIFI_SSID:{ssid};WIFI_PASS:{pass};CITY:{城市代码}`(
## 6. 标准会话流程
1. 扫描(namePrefix `NRF-`)→ 连接 → EPD service → RXTX service → 订阅两通道 notify。
1. 扫描(namePrefix `NRF-`)→ 连接 → 读取 DIS 固件版本 → 按固件选择 RXTX 特征。`PP_da14585_4.2` 使用 `331F`(notify/write/read);`HM42_AIO_*` 使用 `1F1F`(write-only,不订阅 notify)。首选特征不存在时才回退到另一特征。
2. (可选)`EFEF` 取设备 ID。
3. 对时:`dd…` + `e2`。
4. 操作:模式切换(+`e2`)/ 传图(`EA0{slot}` → 数据块×N → `01`/`AA`)/ 倒计时 / WiFi。
4. 操作:模式切换(+`e2`)/ 传图 / 倒计时 / WiFi。
5. 断线自动重连 ≤5 次。
### 6.1 图像协议的固件差异
| 固件家族 | RXTX | 400×300 单色平面 | EPD 图像会话 |
|---|---|---|---|
| `PP_da14585_4.2` / 未知 | `331F` | 黑白平面 | 旧序列:数据块 → `01`(小图 `AA`) |
| `HM42_AIO_*` | `1F1F` | 黑白 15,000B + 全零红色 15,000B | `8001` → `0000` → `020000` → 双平面数据 → `0101` → RXTX `e2` |
HM42 的双平面和会话帧均为必需条件:缺红色平面会出现红屏噪点,缺会话帧会出现黑白噪点。400×300 双平面在 Windows BLE 实机约需 8 分钟;调用方不得依赖跨网络的单个长 HTTP 连接,应在 BLE 主机本地执行并持久化结果后再轮询。
## 7. 本地化(去云)清单 —— 已全部解决
| 云依赖 | 本地化方案 | 状态 |
+2 -2
View File
@@ -1,4 +1,4 @@
# ppclock SDK · API 参考(v0.2.0)
# ppclock SDK · API 参考(v0.2.0-beta)
> 面向二次开发的签名级参考。协议细节见 `docs/protocol.md`,固件内部见 `docs/firmware-analysis.md`。MCP 服务器(ppclock-mcp)工具参考见 `docs/sdk/MCP.md`。
@@ -40,7 +40,7 @@ from ppclock import PPClient
| 方法 | 签名 | 说明 |
|---|---|---|
| `upload_image` | `async (source, slot=None, algo="atkinson", mono=False, size=(400,300), **adjust) -> dict` | source=路径或 PIL.Image;algo∈`none/floydsteinberg/atkinson/bayer/stucki/jarvis`;adjust∈`threshold/diffusion/brightness/contrast/saturation/rotate`;返回 `{"bytes_bw","bytes_red","small","slot"}`。小图(3500B/9000B)自动走 cod=04 |
| `upload_image` | `async (source, slot=None, algo="atkinson", mono=False, size=(400,300), **adjust) -> dict` | source=路径或 PIL.Image;algo∈`none/floydsteinberg/atkinson/bayer/stucki/jarvis`;adjust∈`threshold/diffusion/brightness/contrast/saturation/rotate`;返回 `{"bytes_bw","bytes_red","small","slot"}`。小图(3500B/9000B)自动走 cod=04;`HM42_AIO_*` 会自动补齐双平面并使用厂商图像会话 |
| `render_template` | `async (name, payload=None, slot=None, algo="atkinson") -> dict` | 6 模板本地渲染上传:`schedule/businesscard/memo/course/qrcode/custom`;payload 见 `src/ppclock/templates.py` docstring |
### 倒计时与文字
+17 -2
View File
@@ -1,4 +1,4 @@
# ppclock-mcp · MCP 服务器参考(v0.2.0)
# ppclock-mcp · MCP 服务器参考(v0.2.0-beta)
> 面向 agent 挂载与部署的工具级参考。SDK 签名见 `docs/sdk/API.md`,协议细节见 `docs/protocol.md`。
@@ -11,7 +11,7 @@ streamable HTTP 直接驱动 4.2" 墨水屏设备。
## 安装
```bash
pip install -e ".[mcp]" # 追加依赖:mcp>=1.10,<2
pip install -e ".[mcp]" # 追加依赖:mcp>=1.23,<2
```
要求:Python ≥ 3.10、本机蓝牙适配器(bleak)。服务器与被控设备在同一台蓝牙主机上;
@@ -127,6 +127,21 @@ setx PPCLOCK_MCP_TOKEN "<token>" # 或按服务方式注入环境变量
- 不动既有 **8971** bridge 与其他服务:8972 为独立端口、独立进程。
- `--mac` 建议显式指定设备地址,避免自动扫描误绑其他 `NRF-` 设备。
### 常驻部署(2026-07-31 生产性测试实例)
参考实现(banyWinServer 192.168.61.35,v0.2.0-beta,熠管家运维):
- **常驻方式**:SYSTEM 计划任务 `BanyTech-PPCLOCK-MCP`(AtStartup、最高权限、
执行时间不限、Task Scheduler 999 次/1 分钟恢复);runner 脚本另有 5 秒子进程重启。
- **token**:Windows CSPRNG 生成,存 `C:\ProgramData\OpenClaw\ppclock-mcp\token.txt`,
ACL 仅 SYSTEM/Administrators 可读;不入仓、不写日志。
- **日志**:同目录 `logs\`——`supervisor.log` 记版本/启动参数/退出,
stdout/stderr 按次落文件。
- **验证基线**:真实 LAN Host initialize 200、`device_status` ok;
杀 8972 进程后 5 秒自动拉起、initialize 复 200。
- agent 挂载:`http://192.168.61.35:8972/mcp` + `Authorization: Bearer <token>`。
- 运维记录原件:`/home/cwmine/vps/host-win/maintain_logs/2026-07-31_ppclock-mcp-production-test-service.md`。
## 不暴露的能力
以下 SDK 能力**有意不**注册为 MCP 工具:
@@ -130,9 +130,9 @@ apt-get install -y jadx 2>/dev/null || (cd /opt && curl -sL -o jadx.zip https://
- [x] Step 2: `--json` 输出 schema 固定(`{"ok":bool,"cmd":str,"data":...,"error":...}`),错误码文档化
- [x] Step 3: mock transport 的 CLI 集成测试全绿;提交;**G2 门禁检查**
### Task 9: 真机功能对齐验证 —— ⚠ 降级(无设备在 BLE 范围内,2026-07-24)
### Task 9: 真机功能对齐验证 —— ✅ 完成(2026-07-28/29 实机)
- [ ] Step 1: 对照 protocol.md 功能清单逐项在真机执行 CLI(每步记录结果与抓包)——**待设备到场执行**
- [x] Step 1: 对照 protocol.md 功能清单逐项在真机执行 CLI(每步记录结果与抓包)——**2026-07-28/29 经 bridge 实机完成**(六步全过+扩展 12 步+六抖动+六模板,记录 docs/field-test-result.md;K1 停车牌待查为唯一遗留)
- [x] Step 2(降级): 以 FakeTransport 逐帧断言 + 三线证据互证替代(tests/test_cli.py、test_commands.py);G3 以协议级达标记录于 PROGRESS.md
---
+6 -6
View File
@@ -1,6 +1,6 @@
# ppclock — 4.2寸墨水屏设备(DA14585 BLE)离线 SDK
> AI 消费向项目卡(llms.txt v0.2.0)。人用文档见 README.md;签名级 API 见 docs/sdk/API.md;MCP 服务器见 docs/sdk/MCP.md。
> AI 消费向项目卡(llms.txt v0.2.0-beta)。人用文档见 README.md;签名级 API 见 docs/sdk/API.md;MCP 服务器见 docs/sdk/MCP.md。
## 一句话
@@ -10,7 +10,7 @@ ppclock 是 4.2" 400×300 黑白红三色墨水屏设备(Dialog DA14585,广
```bash
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest tests/ -q # 195 全绿
.venv/bin/python -m pytest tests/ -q # 208 全绿
```
- 库:`from ppclock import PPClient`
@@ -42,10 +42,10 @@ MCP 挂载(agent 直挂 13 工具):`claude mcp add ppclock -- ppclock-mcp
## 目录
- `src/ppclock/`:client.py(门面)/ protocol / image_pipeline / templates / transports(base,local,bridge) / firmware(image,ota) / cli / bridge_server / ppclock-mcp(mcp_server 入口 / device_manager 连接管理 / mcp_tools 13 工具)
- `tools/`:fw_info/fw_pack(镜像工具)、field_test(真机引导测试)、flash_run、gpio_probe、rppclock.sh(守候重试)
- `docs/`:protocol.md(协议规范·唯一事实源)、sdk/API.md、sdk/MCP.md、architecture.md、firmware-analysis.md、firmware-layout.md、field-test-result.md
- `tools/`:fw_info/fw_pack(镜像工具)、field_test(真机引导测试)、flash_run、gpio_probe、rppclock.sh(守候重试)、mcp_windows_deploy.md(Windows 部署清单)
- `docs/`:protocol.md(协议规范·唯一事实源)、sdk/API.md、sdk/MCP.md、architecture.md、firmware-analysis.md、firmware-layout.md、field-test-plan.md、field-test-result.md、superpowers/(specs+plans)
- `analysis/`:三线逆向证据(web/apk/firmware)
- `tests/`:195 测试
- `tests/`:208 测试
## 设备事实(实测 NRF-5DBF28)
@@ -63,4 +63,4 @@ MCP 挂载(agent 直挂 13 工具):`claude mcp add ppclock -- ppclock-mcp
## 版本
0.2.0(CHANGELOG.md)。治理:GOVERNANCE.md(证据纪律/门禁);历史决策:DECISIONS.md。
0.2.0-beta(CHANGELOG.md)。治理:GOVERNANCE.md(证据纪律/门禁);历史决策:DECISIONS.md。
+5 -2
View File
@@ -4,17 +4,20 @@ build-backend = "setuptools.build_meta"
[project]
name = "ppclock"
version = "0.2.0"
version = "0.2.0b1"
description = "4.2寸墨水屏设备离线 CLI 上位机(DA14585 BLE,agent 友好)"
requires-python = ">=3.10"
dependencies = [
"bleak>=0.22",
"pillow>=10",
# 纯 Python 离线库,不违反零网络不变量;templates.py qrcode 模板硬依赖
"qrcode>=7",
]
[project.optional-dependencies]
dev = ["pytest>=8", "pytest-asyncio"]
mcp = ["mcp>=1.10,<2"]
# 地板 1.23:transport_security(--allowed-hosts 依赖)自 1.23 引入 + CVE-2025-66416 修复
mcp = ["mcp>=1.23,<2"]
[project.scripts]
ppclock = "ppclock.cli:main"
+2
View File
@@ -1,6 +1,8 @@
androguard==4.1.4
bleak==3.0.2
capstone==5.0.9
# MCP 服务器依赖;钉版理由见 DECISIONS 2026-07-31;本文件为 GOVERNANCE 指定的依赖冻结
mcp==1.29.0
pillow==12.3.0
pytest==9.1.1
pytest-asyncio==1.4.0
+1 -1
View File
@@ -16,7 +16,7 @@
- bridge_server:BLE 桥服务端(ppclock-bridge 命令)
- device_manager / mcp_tools / mcp_server:MCP 服务器(ppclock-mcp 命令)
"""
__version__ = "0.2.0"
__version__ = "0.2.0b1"
from .client import PPClient
from .transports.base import Transport, TransportError
+18
View File
@@ -19,6 +19,7 @@ from pathlib import Path
from . import commands as C
from . import image_pipeline as IP
from . import protocol as P
from .transports import bridge as _bridge_mod
from .transports import local as _local_mod
from .transports.base import TransportError # noqa: F401 (SDK 公开异常)
@@ -31,6 +32,18 @@ class PPClient:
def __init__(self, transport):
self._t = transport
@property
def transport_info(self) -> dict:
"""返回传输层公开诊断信息;bridge/fake 等未提供字段时为空。"""
info = {}
firmware_revision = getattr(self._t, "firmware_revision", None)
rxtx_uuid = getattr(self._t, "rxtx_uuid", None)
if firmware_revision:
info["firmware_revision"] = firmware_revision
if rxtx_uuid:
info["rxtx_uuid"] = rxtx_uuid
return info
# ---------- 生命周期 ----------
async def __aenter__(self):
@@ -105,6 +118,11 @@ class PPClient:
img = IP.Image.open(source) if isinstance(source, (str, Path)) else source
bw, red = IP.process_image(img, size=size, algorithm=algo,
tricolor=not mono, **adjust)
if red is None and P.requires_complete_image_planes(
getattr(self._t, "firmware_revision", None)):
# HM42 stores the two 400x300 planes as one sequential image. Its
# Web-BLE client sends an empty red plane even for monochrome input.
red = bytes(len(bw))
small = len(bw) in (3500, 9000)
await C.upload_image(self._t, bw, red, small=small, slot=slot)
return {"bytes_bw": len(bw), "bytes_red": len(red) if red else 0,
+14 -3
View File
@@ -34,10 +34,16 @@ async def clear(t, param: int = 0x00):
async def upload_image(t, bw: bytes, red: bytes | None, *, small: bool,
slot: int | None = None, block_delay: float = 0.0):
"""传图:可选槽位选择 → bw 平面块 → 红平面块 → 300ms → 01/AA。"""
"""传图;HM42_AIO 使用厂商 App 的 EPD 会话,其他固件保留旧协议。"""
hm42_session = P.uses_hm42_image_session(
getattr(t, "firmware_revision", None)
)
if slot is not None:
await t.write_rxtx(P.slot_select_frame(slot))
await t.delay(0.3)
if hm42_session:
for frame in P.hm42_image_begin_frames():
await t.write_epd(frame)
for frame in P.image_blocks(bw, small=small, red=False):
await t.write_epd(frame)
if block_delay:
@@ -47,8 +53,13 @@ async def upload_image(t, bw: bytes, red: bytes | None, *, small: bool,
await t.write_epd(frame)
if block_delay:
await t.delay(block_delay)
await t.delay(0.3)
await t.write_epd(P.end_frame(small=small))
if hm42_session:
await t.write_epd(P.hm42_image_finish_frame())
await t.delay(0.3)
await t.write_rxtx(P.simple_command("commit"))
else:
await t.delay(0.3)
await t.write_epd(P.end_frame(small=small))
async def countdown_set(t, d: datetime.date, mode: str, prefix: str | None = None,
+1
View File
@@ -67,6 +67,7 @@ class DeviceManager:
"connect_timeout": self._connect_timeout,
"idle_timeout": self._idle_timeout,
}
info.update(self._client.transport_info)
try:
info["device_id"] = await self._client.get_device_id()
except Exception: # noqa: BLE001 - 状态查询尽力而为
+49 -2
View File
@@ -8,13 +8,60 @@ from typing import Iterator
EPD_SERVICE_UUID = "13187b10-eba9-a3ba-044e-83d3217d9a38"
EPD_CHAR_UUID = "4b646063-6264-f3a7-8941-e65356ea82fe"
RXTX_SERVICE_UUID = "00001f10-0000-1000-8000-00805f9b34fb"
RXTX_CHAR_UUID = "0000331f-0000-1000-8000-00805f9b34fb" # 活命令通道(h=57, notify+write+read)
RXTX_CHAR_UUID_LEGACY = "00001f1f-0000-1000-8000-00805f9b34fb" # h=53 仅write,本设备不执行命令
RXTX_CHAR_UUID = "0000331f-0000-1000-8000-00805f9b34fb" # PP 4.2 活命令通道(h=57, notify+write+read)
RXTX_CHAR_UUID_LEGACY = "00001f1f-0000-1000-8000-00805f9b34fb" # HM42_AIO 活命令通道(h=53, write-only)
FIRMWARE_REVISION_UUID = "00002a26-0000-1000-8000-00805f9b34fb"
DEVICE_NAME_PREFIX = "NRF-"
BLOCK_PAYLOAD = 240 # my_step=480 hex 字符 = 240 数据字节
COMMIT = 0xE2
def select_rxtx_uuid(firmware_revision: str | None) -> str:
"""按固件家族选择控制特征。
PP 4.2 实机的 1F1F 虽可写但不执行控制命令,活通道是 331F;
HM42_AIO 实机则与 Web-BLE / 厂商 App 一致,仅执行 1F1F。
未知或读不到 DIS 版本时保留既有 331F 行为,避免回归已验证的 PP 设备。
"""
revision = (firmware_revision or "").strip().upper()
if revision.startswith("HM42_AIO"):
return RXTX_CHAR_UUID_LEGACY
return RXTX_CHAR_UUID
def requires_complete_image_planes(firmware_revision: str | None) -> bool:
"""Whether a nominally mono full-screen upload still needs both planes.
The HM42 Web-BLE implementation always sends the 400x300 black/white plane
followed by a red plane before opcode 01. Omitting the second plane leaves
HM42 rendering stale/uninitialised red data (observed as a red noisy screen).
"""
revision = (firmware_revision or "").strip().upper()
return revision.startswith("HM42_AIO")
def uses_hm42_image_session(firmware_revision: str | None) -> bool:
"""Whether image transfer needs the newer HM42 EPD session framing.
The vendor Android provider ``轻舞/时光`` (``d8.e``) wraps both image
planes with ``8001, 0000, 020000`` and closes with ``0101`` followed by
RXTX ``e2`` for a normal (non-calendar) image. The older Web-BLE path has
no such preamble and ends with the legacy one-byte EPD frame.
"""
revision = (firmware_revision or "").strip().upper()
return revision.startswith("HM42_AIO")
def hm42_image_begin_frames() -> tuple[bytes, ...]:
"""Vendor-app EPD image-session preamble for HM42_AIO firmware."""
return (bytes.fromhex("8001"), bytes.fromhex("0000"), bytes.fromhex("020000"))
def hm42_image_finish_frame() -> bytes:
"""Vendor-app EPD image-session terminator for HM42_AIO firmware."""
return bytes.fromhex("0101")
MODES = {
**{f"image{i}": bytes([0xE1, 0x00, 0x00 + i]) for i in range(4)},
"calendar1": bytes.fromhex("e10103"),
+36 -4
View File
@@ -35,6 +35,9 @@ class BLETransport:
self._client: BleakClient | None = None
self._id_buf = bytearray()
self._id_event = asyncio.Event()
self.firmware_revision: str | None = None
self.rxtx_uuid = P.RXTX_CHAR_UUID
self._notify_started = False
async def __aenter__(self):
await self.connect()
@@ -54,13 +57,37 @@ class BLETransport:
await self._client.connect()
except BleakError as e:
raise TransportError(f"连接失败: {e}") from e
await self._client.start_notify(P.RXTX_CHAR_UUID, self._on_notify)
try:
raw_revision = await self._client.read_gatt_char(P.FIRMWARE_REVISION_UUID)
self.firmware_revision = bytes(raw_revision).decode(
"utf-8", errors="replace").rstrip("\x00")
except Exception: # DIS 版本是兼容性提示,不应阻断基础连接
self.firmware_revision = None
preferred = P.select_rxtx_uuid(self.firmware_revision)
alternate = (
P.RXTX_CHAR_UUID_LEGACY
if preferred == P.RXTX_CHAR_UUID else P.RXTX_CHAR_UUID
)
selected = self._client.services.get_characteristic(preferred)
if selected is None:
selected = self._client.services.get_characteristic(alternate)
if selected is None:
raise TransportError("未找到 RXTX 控制特征 1F1F/331F")
self.rxtx_uuid = selected.uuid
properties = set(selected.properties)
if properties.intersection({"notify", "indicate"}):
await self._client.start_notify(selected, self._on_notify)
self._notify_started = True
async def close(self):
if self._client and self._client.is_connected:
try:
await self._client.stop_notify(P.RXTX_CHAR_UUID)
if self._notify_started:
await self._client.stop_notify(self.rxtx_uuid)
finally:
self._notify_started = False
await self._client.disconnect()
def _on_notify(self, _sender, data: bytearray):
@@ -72,12 +99,17 @@ class BLETransport:
await self._client.write_gatt_char(P.EPD_CHAR_UUID, data, response=response)
async def write_rxtx(self, data: bytes, response: bool = True):
await self._client.write_gatt_char(P.RXTX_CHAR_UUID, data, response=response)
await self._client.write_gatt_char(self.rxtx_uuid, data, response=response)
async def read_rxtx(self) -> bytes:
return bytes(await self._client.read_gatt_char(P.RXTX_CHAR_UUID))
return bytes(await self._client.read_gatt_char(self.rxtx_uuid))
async def request_device_id(self, timeout: float = 8.0) -> str:
if not self._notify_started:
raise TransportError(
f"固件 {self.firmware_revision or 'unknown'} 的控制特征不支持通知,"
"无法读取设备 ID"
)
self._id_buf.clear()
self._id_event.clear()
await self.write_rxtx(P.request_id_frame())
+1 -1
View File
@@ -9,7 +9,7 @@ from ppclock.transports import bridge
class FakeBridgeServer:
"""模拟 tools/bridge_server.py 的 JSONL 协议。"""
"""模拟 ppclock.bridge_server 的 JSONL 协议。"""
def __init__(self):
self.received = []
+36 -1
View File
@@ -7,10 +7,11 @@ from ppclock.client import PPClient
class FakeTransport:
def __init__(self):
def __init__(self, firmware_revision=None):
self.epd_writes = []
self.rxtx_writes = []
self.address = "AA:BB:CC:DD:EE:FF"
self.firmware_revision = firmware_revision
async def __aenter__(self):
return self
@@ -78,6 +79,40 @@ class TestCommands:
assert client._t.rxtx_writes[0] == bytes([0xEA, 0x00])
assert len(client._t.epd_writes) == 63 + 63 + 1
@pytest.mark.asyncio
async def test_upload_mono_non_hm42_sends_one_plane(self):
from PIL import Image
client = PPClient(FakeTransport("PP_da14585_4.2"))
result = await client.upload_image(
Image.new("RGB", (400, 300), "white"), slot=1,
algo="none", mono=True,
)
assert result["bytes_bw"] == 15000
assert result["bytes_red"] == 0
assert len(client._t.epd_writes) == 63 + 1
@pytest.mark.asyncio
async def test_upload_mono_hm42_appends_empty_red_plane(self):
from PIL import Image
client = PPClient(FakeTransport("HM42_AIO_V1.0.9"))
result = await client.upload_image(
Image.new("RGB", (400, 300), "white"), slot=1,
algo="none", mono=True,
)
assert result["bytes_bw"] == 15000
assert result["bytes_red"] == 15000
assert client._t.epd_writes[:3] == [
bytes.fromhex("8001"),
bytes.fromhex("0000"),
bytes.fromhex("020000"),
]
assert len(client._t.epd_writes) == 3 + 63 + 63 + 1
first_red = client._t.epd_writes[3 + 63]
assert first_red[:4] == bytes.fromhex("03000000")
assert set(first_red[4:]) == {0}
assert client._t.epd_writes[-1] == bytes.fromhex("0101")
assert client._t.rxtx_writes[-1] == bytes.fromhex("e2")
@pytest.mark.asyncio
async def test_render_template(self, client):
result = await client.render_template("custom", {"text": "你好"})
+19 -1
View File
@@ -7,10 +7,11 @@ from ppclock import commands as C
class FakeTransport:
def __init__(self, id_response="DEVICE12345678"):
def __init__(self, id_response="DEVICE12345678", firmware_revision=None):
self.epd_writes = []
self.rxtx_writes = []
self.id_response = id_response
self.firmware_revision = firmware_revision
self.delays = []
async def write_epd(self, data, response=True):
@@ -80,6 +81,23 @@ class TestUploadImage:
assert t.epd_writes[-1] == bytes([0xAA])
assert t.epd_writes[0][0] == 0x04
@pytest.mark.asyncio
async def test_hm42_uses_vendor_epd_session(self):
t = FakeTransport(firmware_revision="HM42_AIO_V1.0.9")
await C.upload_image(
t, bytes(15000), bytes(15000), small=False, slot=1
)
assert t.rxtx_writes == [bytes.fromhex("EA01"), bytes.fromhex("E2")]
assert t.epd_writes[:3] == [
bytes.fromhex("8001"),
bytes.fromhex("0000"),
bytes.fromhex("020000"),
]
assert t.epd_writes[3][:4] == bytes.fromhex("03FF0000")
assert t.epd_writes[66][:4] == bytes.fromhex("03000000")
assert t.epd_writes[-1] == bytes.fromhex("0101")
assert len(t.epd_writes) == 3 + 63 + 63 + 1
class TestOtherCommands:
@pytest.mark.asyncio
+4
View File
@@ -19,6 +19,8 @@ class FakeTransport:
# fail_next_write 触发时抛出的异常实例(默认 SDK 包装后的 TransportError;
# 真实硬件路径常是 bleak 裸抛的 BleakError,测试可替换)
self.fail_exc = TransportError("connection lost")
self.firmware_revision = "HM42_AIO_V1.0.9"
self.rxtx_uuid = "00001f1f-0000-1000-8000-00805f9b34fb"
async def __aenter__(self):
self.enters += 1
@@ -204,6 +206,8 @@ class TestStatus:
assert s["connect_timeout"] == 90.0
assert s["idle_timeout"] == 300.0
assert s["idle_seconds"] is None # 无先前活动;status 自身不得把读数归零
assert s["firmware_revision"] == "HM42_AIO_V1.0.9"
assert s["rxtx_uuid"].startswith("00001f1f")
await m.close()
@pytest.mark.asyncio
+23
View File
@@ -61,6 +61,29 @@ class TestSetTime:
class TestCommandFrames:
def test_rxtx_channel_by_firmware_family(self):
assert P.select_rxtx_uuid("HM42_AIO_V1.0.9") == P.RXTX_CHAR_UUID_LEGACY
assert P.select_rxtx_uuid("hm42_aio_v2") == P.RXTX_CHAR_UUID_LEGACY
assert P.select_rxtx_uuid("PP_da14585_4.2") == P.RXTX_CHAR_UUID
assert P.select_rxtx_uuid(None) == P.RXTX_CHAR_UUID
def test_complete_image_planes_by_firmware_family(self):
assert P.requires_complete_image_planes("HM42_AIO_V1.0.9") is True
assert P.requires_complete_image_planes("hm42_aio_v2") is True
assert P.requires_complete_image_planes("PP_da14585_4.2") is False
assert P.requires_complete_image_planes(None) is False
def test_hm42_image_session_by_firmware_family(self):
assert P.uses_hm42_image_session("HM42_AIO_V1.0.9") is True
assert P.uses_hm42_image_session("PP_da14585_4.2") is False
assert P.uses_hm42_image_session(None) is False
assert P.hm42_image_begin_frames() == (
bytes.fromhex("8001"),
bytes.fromhex("0000"),
bytes.fromhex("020000"),
)
assert P.hm42_image_finish_frame() == bytes.fromhex("0101")
def test_parking(self):
"""停车牌 "12" → EF1122(向量#4)"""
assert P.parking_frame("12") == bytes([0xEF, 0x11, 0x22])
+109
View File
@@ -0,0 +1,109 @@
"""本机 BLE 传输层的固件通道选择测试。"""
import pytest
from ppclock import protocol as P
from ppclock.transports import local
class FakeCharacteristic:
def __init__(self, uuid, properties):
self.uuid = uuid
self.properties = properties
class FakeServices:
def __init__(self):
self.characters = {
P.RXTX_CHAR_UUID_LEGACY: FakeCharacteristic(
P.RXTX_CHAR_UUID_LEGACY, ["write"]),
P.RXTX_CHAR_UUID: FakeCharacteristic(
P.RXTX_CHAR_UUID, ["read", "write", "notify"]),
}
def get_characteristic(self, uuid):
return self.characters.get(str(uuid).lower())
class FakeBleakClient:
firmware_revision = "PP_da14585_4.2"
fail_firmware_read = False
def __init__(self, address, timeout):
self.address = address
self.timeout = timeout
self.services = FakeServices()
self.is_connected = False
self.notifications = []
self.writes = []
async def connect(self):
self.is_connected = True
async def disconnect(self):
self.is_connected = False
async def read_gatt_char(self, uuid):
if str(uuid).lower() == P.FIRMWARE_REVISION_UUID:
if self.fail_firmware_read:
raise RuntimeError("DIS unavailable")
return self.firmware_revision.encode()
return b""
async def write_gatt_char(self, uuid, data, response=True):
target = uuid.uuid if hasattr(uuid, "uuid") else uuid
self.writes.append((target, bytes(data), response))
async def start_notify(self, uuid, callback):
target = uuid.uuid if hasattr(uuid, "uuid") else uuid
self.notifications.append(target)
async def stop_notify(self, uuid):
self.notifications.remove(uuid)
@pytest.mark.asyncio
async def test_hm42_selects_write_only_1f1f(monkeypatch):
class HM42Client(FakeBleakClient):
firmware_revision = "HM42_AIO_V1.0.9"
monkeypatch.setattr(local, "BleakClient", HM42Client)
transport = local.BLETransport("18:BC:5A:7E:BB:1E")
await transport.connect()
assert transport.firmware_revision == "HM42_AIO_V1.0.9"
assert transport.rxtx_uuid == P.RXTX_CHAR_UUID_LEGACY
assert not transport._notify_started
await transport.write_rxtx(b"\xe1\x02\x02")
assert transport._client.writes[-1][0] == P.RXTX_CHAR_UUID_LEGACY
with pytest.raises(local.TransportError, match="不支持通知"):
await transport.request_device_id()
await transport.close()
@pytest.mark.asyncio
async def test_pp42_keeps_331f_with_notifications(monkeypatch):
monkeypatch.setattr(local, "BleakClient", FakeBleakClient)
transport = local.BLETransport("18:BC:5A:5D:BF:28")
await transport.connect()
assert transport.rxtx_uuid == P.RXTX_CHAR_UUID
assert transport._notify_started
assert transport._client.notifications == [P.RXTX_CHAR_UUID]
await transport.write_rxtx(b"\xe2")
assert transport._client.writes[-1][0] == P.RXTX_CHAR_UUID
await transport.close()
@pytest.mark.asyncio
async def test_unknown_firmware_preserves_331f_default(monkeypatch):
class NoDISClient(FakeBleakClient):
fail_firmware_read = True
monkeypatch.setattr(local, "BleakClient", NoDISClient)
transport = local.BLETransport("AA:BB:CC:DD:EE:FF")
await transport.connect()
assert transport.firmware_revision is None
assert transport.rxtx_uuid == P.RXTX_CHAR_UUID
await transport.close()
+6 -3
View File
@@ -1,5 +1,7 @@
# ppclock-mcp · Windows 部署执行清单(熠管家协作版)
> 时点件(2026-07-30 测试期清单)。现行部署形态以 docs/sdk/MCP.md §常驻部署 为准。
目标:把 ppclock-mcp 部署到**蓝牙物理所在的 Windows 服务器**,跑通 stdio 与 HTTP 两种接入实测。
约束:**不动** 8971 bridge 与该机任何既有服务;8972 防火墙仅局域网放行;测试期手动启动,不注册系统服务。
@@ -19,7 +21,7 @@ python --version # 需 ≥3.10
```powershell
git clone <仓库地址> C:\ppclock # 或把 Linux 侧源码压缩包拷贝解压到 C:\ppclock
cd C:\ppclock
git checkout feat/ppclock-mcp # 分支合并 main 后此步改为 git checkout main
git checkout main # 对应 tag v0.2.0-beta
git log --oneline -1 # 记录 commit,回报用
```
@@ -36,7 +38,7 @@ python -m venv .venv
```powershell
.\.venv\Scripts\ppclock-mcp --help
```
预期:打印参数表,含 `--transport/--host/--port/--mac/--connect-timeout/--idle-timeout`。
预期:打印参数表,含 `--transport/--host/--port/--mac/--connect-timeout/--idle-timeout/--allowed-hosts`。
## 4. stdio 实测(本机 agent 直挂)
@@ -60,7 +62,8 @@ claude mcp add ppclock -- C:\ppclock\.venv\Scripts\ppclock-mcp.exe --mac <MAC>
```powershell
$env:PPCLOCK_MCP_TOKEN = "<随机长串>"
.\.venv\Scripts\ppclock-mcp --transport http --host 0.0.0.0 --port 8972 --mac <MAC>
.\.venv\Scripts\ppclock-mcp --transport http --host 0.0.0.0 --port 8972 --mac <MAC> `
--allowed-hosts "192.168.61.35:8972,localhost:8972,127.0.0.1:8972" # 换成本机局域网地址;缺了 LAN Host 请求被 DNS 重绑定防护 421
```
另开终端:
```powershell