From 2b90d3891f6f59c3bdb33d8cb89c140fdf3ed4f9 Mon Sep 17 00:00:00 2001 From: agent Date: Thu, 30 Jul 2026 14:58:18 +0000 Subject: [PATCH] =?UTF-8?q?docs+release:=20CHANGELOG/llms.txt/SDK=20README?= =?UTF-8?q?/API.md=EF=BC=8C0.1.0=20=E5=80=99=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 29 +++++++++ README.md | 155 ++++++++++++++++++++---------------------------- docs/sdk/API.md | 101 +++++++++++++++++++++++++++++++ llms.txt | 63 ++++++++++++++++++++ 4 files changed, 258 insertions(+), 90 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/sdk/API.md create mode 100644 llms.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..27bdaaf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# CHANGELOG + +## [0.1.0] — 2026-07-30 + +首个 SDK 化发布。4.2寸墨水屏设备(DA14585 BLE,400×300 黑白红三色)的**完全离线**开发包: +协议三线逆向(Web JS/APK/固件反汇编)+ 实机互证,全部功能经真机验证。 + +### SDK +- **`PPClient` 统一门面**:`via_local`(本机 bleak)/ `via_bridge(host, mac)`(经 bridge 远程 BLE)/ `via_uri` / `scan_devices` +- 设备操作全集:对时(含 `tz` 时区偏移)、模式/切换、传图(6 抖动+调整+槽位)、本地 6 模板渲染上传、倒计时(时钟/日历/前缀/日历中文字)、休眠、停车牌、轮播、LUT、WiFi 透传、raw 直发、SUOTA OTA +- **离线激活**:`activate_auto`——EFEF 取设备 ID → MAC 反解 → 本地 keygen(破解厂商云激活,实机验证) +- `transports`(base/local/bridge)与 `firmware`(image 解析重打包/ota)分包;`Transport` 协议开放自定义传输 +- **BLE bridge 一等公民**:`ppclock-bridge` 命令(bleak 单文件服务端,Windows/Linux 原生运行),JSONL/TCP RPC,解决本机无蓝牙或 USB/IP 透传断流场景 + +### CLI(同一能力面) +`ppclock` 17 子命令 + `--json` 机器契约 + `batch` JSONL 单连接批量(agent 编排友好)+ `--via bridge://` / `PPCLOCK_VIA` + +### 固件工具 +镜像解析/修改/重打包('pQ' 头 + zlib CRC32,字节级往返一致);SUOTA 客户端(fef5 自定义 UUID 映射,实机推至产品头门禁);工厂线刷布局已解析(pP 引导+双 bank+pR 产品头,docs/firmware-layout.md) + +### 测试与文档 +- 165 自动化测试全绿(协议/图像/命令/CLI/bridge/keygen/ota/fw/client 八个域) +- AI-native 文档:`docs/sdk/API.md` + `llms.txt` +- 逆向证据链:analysis/(web/apk/firmware 三域)+ docs/(架构/协议/固件分析/布局/实测记录) + +### 已知限制 +- 停车牌命令设备端不显示(厂商固件家族渲染差异,K1 待查) +- 本机设备为裸闪存布局(无 'pR' 产品头),SUOTA 止于设备内部门禁;线刷恢复布局路径已成文备查 +- 轮播/WiFi 配置在本固件版本为 no-op(固件证据) diff --git a/README.md b/README.md index 8639abe..2fcbec7 100644 --- a/README.md +++ b/README.md @@ -1,129 +1,104 @@ -# ppclock — 4.2寸墨水屏设备离线 CLI 上位机 +# ppclock — 4.2寸墨水屏设备离线 SDK 与 CLI -替代厂商 Web 上位机(mspppclock.tech)与 Android App 的**完全离线**命令行工具, -面向 agent 编排设计:所有命令支持 `--json` 机器可读输出,`batch` 子命令支持 JSONL 批量执行。 +替代厂商 Web 上位机(mspppclock.tech)与 Android App 的**完全离线**开发包: +Python SDK(`PPClient`)+ CLI(`ppclock`)+ BLE 桥(`ppclock-bridge`), +全部功能经真机验证(协议三线逆向 + 实机互证)。 设备:4.2" 400×300 黑/白/红三色墨水屏,Dialog DA14585 BLE SoC(广播名前缀 `NRF-`)。 +> AI 消费向项目卡见 [`llms.txt`](llms.txt);签名级 API 见 [`docs/sdk/API.md`](docs/sdk/API.md)。 + ## 安装 ```bash python3 -m venv .venv -.venv/bin/pip install -e . # 依赖:bleak、pillow(离线 wheel 即可,运行零网络) -.venv/bin/ppclock --help +.venv/bin/pip install -e ".[dev]" # 依赖:bleak、pillow(dev: pytest) ``` -运行环境要求:Linux + 蓝牙适配器(bleak/D-Bus)。**不需要互联网**。 +运行要求:蓝牙适配器(本机 bleak)**或** 他机蓝牙经 `ppclock-bridge` 桥接。**不需要互联网**。 -## 快速开始 +## SDK 用法(二次开发) + +```python +from ppclock import PPClient + +async with PPClient.via_local() as dev: # 本机蓝牙 + await dev.set_time(tz=8) # 北京时间 + await dev.set_mode("clock1") + +async with PPClient.via_bridge("192.168.61.35:8971") as dev: # 经 bridge + await dev.upload_image("photo.jpg", slot=0) # 传图(6 种抖动可选) + await dev.render_template("custom", {"text": "你好【红字】"}) + await dev.countdown(date(2026, 12, 31), prefix="目标") + await dev.activate_auto() # 离线激活(无需厂商服务器) +``` + +设备操作全集:对时(tz)/模式/切换/传图/6 模板/倒计时/休眠/停车牌/轮播/LUT/WiFi/raw/OTA, +见 [`docs/sdk/API.md`](docs/sdk/API.md)。 + +## CLI 用法 ```bash -ppclock scan # 扫描设备(NRF- 前缀) -ppclock --mac AA:BB:CC:DD:EE:FF time # 对时(免扫描直连) -ppclock mode clock1 # 时钟模式一 -ppclock image photo.jpg --slot 0 # 传图到图一(默认 atkinson 抖动+三色) +ppclock scan # 扫描设备(NRF- 前缀) +ppclock --mac AA:BB:CC:DD:EE:FF time --tz 8 # 对时(北京时间) +ppclock image photo.jpg --slot 0 # 传图到图一 ppclock template custom --data '{"text":"你好【世界】"}' -ppclock countdown 2026-12-31 --prefix "目标" # 倒计时 -ppclock batch job.jsonl --json # 批量执行(单连接) +ppclock countdown 2026-12-31 --prefix 目标 +ppclock batch job.jsonl --json # 批量执行(单连接) ``` -## 命令一览 +- 全命令支持 `--json` 机器可读输出(`{"ok","cmd","data"|"error"}`,退出码 0/1/2/3) +- `batch`:JSONL 批量(`{"argv":[...]}` 每行一条,单连接执行,agent 编排友好) -| 命令 | 功能 | 对应 Web 上位机功能 | -|------|------|---------------------| -| `scan` | 扫描设备 | 搜索设备 | -| `time [ISO]` | 对时 | 设置时间 | -| `mode NAME` | clock1-3/calendar1-3/image0-3/tricolor/mono | 时钟/日历/图片模式、三色/黑白 | -| `toggle NAME` | invert/font/rotate180/hour_format/clock_color | 反色/字体/旋转/12-24h/颜色 | -| `clear` | 刷屏 | 刷屏 | -| `image PATH` | 传图(6 种抖动+亮度/对比度/饱和度/旋转/缩放+槽位) | 4.2传图/上传图片 | -| `template NAME` | 本地渲染 6 模板并上传 | 模板系统 | -| `countdown` | 倒计时日期/前缀文字/日历中文字/关闭 | 倒计时 | -| `sleep` | 休眠时段 | 休眠 | -| `parking NUM` | 停车牌 | 停车牌 | -| `rotation` | 轮播数量/间隔 | 轮播 | -| `lut HEX` | 红/黑 LUT 校准 | LUT | -| `activate` | 激活码下发/读取设备 ID | 激活 | -| `wifi` | WiFi+城市配置 | WiFi 配置 | -| `raw HEX` | 高级命令直发 | 高级命令 | -| `batch FILE` | JSONL 批量(单连接) | —(增强) | +## BLE bridge(蓝牙在他机时) -## JSON 输出契约(`--json`) +本机无蓝牙(或 USB/IP 透传断流)时,把 BLE 桥接到蓝牙物理所在机: -成功:`{"ok": true, "cmd": "", "data": {...}}` -失败:`{"ok": false, "cmd": "", "error": ""}` -退出码:0 成功 / 1 参数错误 / 2 传输(BLE)错误 / 3 其他错误。 +```bash +# 蓝牙所在机(Windows/Linux,仅需 bleak) +pip install bleak +ppclock-bridge --host 0.0.0.0 --port 8971 # 或 python -m ppclock.bridge_server -## batch JSONL 格式 - -每行一条命令:`{"argv": ["mode", "clock1"]}`,输出 `{"ok":true,"cmd":"batch","data":{"results":[...]}}`。 -整个批量在**一次 BLE 连接**内完成;`--stop-on-error` 遇错即停。 - -## 定时推送(增强) - -CLI 全部为单次执行语义,调度交给系统 cron: - -```cron -# 每小时整点更新倒计时画面 -0 * * * * /path/.venv/bin/ppclock --mac AA:BB:CC:DD:EE:FF countdown 2026-12-31 --prefix 目标 --json >> /var/log/ppclock.log 2>&1 +# 使用方(全部 SDK/CLI 透明可用) +ppclock --via bridge://192.168.61.35:8971 --json scan +export PPCLOCK_VIA=bridge://192.168.61.35:8971 ``` -## 离线说明 +协议:JSONL over TCP v1(`ping/scan/connect/write/read/request_device_id/ota/services/close`), +仅限可信 LAN(无鉴权)。详见 `docs/sdk/API.md` §Bridge RPC。 -- 激活:原厂流程依赖云端 `key_generator.php`。本工具的本地化方案见 - `docs/firmware-analysis.md`(激活校验分析)与 `docs/protocol.md` §7。 -- 模板渲染、二维码生成(本地 qrcode 库)、抖动算法全部本地实现,无任何 CDN/服务器依赖。 -- 天气(CITY 代码)属原厂云功能,本地化范围外;`wifi` 命令仅透传配置。 +## 固件工具 + +```bash +.venv/bin/python tools/fw_info.py app/firmware-PP_da14585_4.2_CH.img # 镜像头解析 +.venv/bin/python tools/fw_pack.py body.bin out.img # 修改后重打包(CRC 重算) +ppclock --json ota firmware.img # 预检(不烧录) +ppclock --json ota firmware.img --yes # SUOTA 烧录(高危,需授权) +``` + +本机设备为裸闪存布局(无产品头)→ SUOTA 止于设备内部门禁;工厂布局与线刷恢复路径见 +[`docs/firmware-layout.md`](docs/firmware-layout.md)。 ## 真机全链路测试 -设备到场后执行(计划:`docs/field-test-plan.md`): - ```bash .venv/bin/python tools/field_test.py # 引导式:自动判定+目视确认 -.venv/bin/python tools/field_test.py --ota # 含高危固件烧录阶段(默认跳过) ``` 结果自动写入 `docs/field-test-result.md`。 -## BLE bridge(蓝牙在他机时) +## 离线说明 -当本机无蓝牙(或蓝牙经 USB/IP 透传不稳定)时,把 BLE 操作桥接到蓝牙物理所在机: - -**服务端**(蓝牙所在机,如 Windows host-win;单文件,仅依赖 bleak): - -```bash -pip install bleak -python tools/bridge_server.py --host 0.0.0.0 --port 8971 -``` - -**客户端**(本机,全部 CLI 命令透明可用): - -```bash -ppclock --via bridge://192.168.61.35:8971 --json scan -export PPCLOCK_VIA=bridge://192.168.61.35:8971 # 或环境变量一次设定 -ppclock --json image photo.jpg --slot 0 -PPCLOCK_VIA=bridge://192.168.61.35 .venv/bin/python tools/field_test.py -``` - -协议:JSONL over TCP(`ping/scan/connect/write/read/request_device_id/ota/close`), -OTA 进度以事件流回。仅限可信 LAN 使用(无鉴权)。 - -## 本机蓝牙经 USB/IP 透传的已知问题 - -大 MTU GATT 流量会触发断流(详见 DECISIONS.md 2026-07-28)。保留缓解: -`/sys/bus/usb/devices/1-1/power/control=on`(禁用 runtime PM)。重启后失效,固化: - -```bash -echo 'ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="8087", ATTR{idProduct}=="0029", ATTR{power/control}="on"' | sudo tee /etc/udev/rules.d/99-ax200-pm.rules -``` - -根治请用上面的 bridge 模式。 +- **激活**:原厂流程依赖云端 `key_generator.php`;本 SDK 用固件逆向出的本地 keygen + (`activate_auto`:EFEF 取 ID → MAC 反解 → 本地算码),实机验证。 +- 模板渲染、二维码(本地 qrcode 库)、六种抖动全部本地实现,无 CDN/服务器依赖。 +- 天气(CITY 代码)属原厂云功能,本地化范围外;`wifi` 命令仅透传(且本固件 no-op)。 ## 开发 ```bash -.venv/bin/python -m pytest tests/ -q # 150 测试 +.venv/bin/python -m pytest tests/ -q # 165 测试 ``` -治理规则见 `GOVERNANCE.md`;计划见 `PLAN.md`;协议规范见 `docs/protocol.md`。 +治理规则见 `GOVERNANCE.md`;变更记录 `CHANGELOG.md`;决策日志 `DECISIONS.md`。 +协议规范(唯一事实源):`docs/protocol.md`;固件内部:`docs/firmware-analysis.md`。 diff --git a/docs/sdk/API.md b/docs/sdk/API.md new file mode 100644 index 0000000..3b803c3 --- /dev/null +++ b/docs/sdk/API.md @@ -0,0 +1,101 @@ +# ppclock SDK · API 参考(v0.1.0) + +> 面向二次开发的签名级参考。协议细节见 `docs/protocol.md`,固件内部见 `docs/firmware-analysis.md`。 + +## 安装与入口 + +```bash +pip install -e . # 依赖:bleak, pillow +ppclock --help # CLI +ppclock-bridge --host 0.0.0.0 --port 8971 # BLE 桥服务端 +``` + +```python +from ppclock import PPClient +``` + +## PPClient(统一门面,async) + +### 工厂与扫描 + +| 方法 | 签名 | 说明 | +|---|---|---| +| `via_local` | `(mac: str|None=None, timeout=10.0) -> PPClient` | 本机 BLE(bleak);mac 省略自动扫描 `NRF-` 前缀 | +| `via_bridge` | `(host: str, mac=None, timeout=10.0, port=None) -> PPClient` | 经 bridge 远程 BLE;`host` 可含 `:port`(默认 8971) | +| `via_uri` | `(uri: str, mac=None, timeout=10.0) -> PPClient` | `"local"` 或 `"bridge://host[:port]"` | +| `scan_devices` | `async (uri="local", timeout=5.0) -> list[dict]` | `[{name, address, rssi}]`,按 uri 选择本机/桥扫描 | + +生命周期:`async with PPClient.via_*(...) as dev:` —— 进入连接、退出断开。 + +### 时钟与显示 + +| 方法 | 签名 | 说明 | +|---|---|---| +| `set_time` | `async (dt=None, tz: float|None=None)` | 对时;`tz=8` 为北京时(dt 缺省时生效) | +| `set_mode` | `async (mode: str)` | `clock1-3`/`calendar1-3`/`image0-3`/`tricolor`/`mono` | +| `toggle` | `async (name: str)` | `invert`/`font`/`rotate180`/`hour_format`/`clock_color` | +| `clear` | `async ()` | 刷屏(EPD 00+01) | + +### 图像 + +| 方法 | 签名 | 说明 | +|---|---|---| +| `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 | +| `render_template` | `async (name, payload=None, slot=None, algo="atkinson") -> dict` | 6 模板本地渲染上传:`schedule/businesscard/memo/course/qrcode/custom`;payload 见 `src/ppclock/templates.py` docstring | + +### 倒计时与文字 + +| 方法 | 签名 | 说明 | +|---|---|---| +| `countdown` | `async (d: date, mode="clock", prefix=None)` | mode=`clock`/`calendar`;prefix 时钟≤8 单位、日历≤4 字(中文=1,其他=0.5) | +| `countdown_off` | `async ()` | 关闭倒计时 | +| `calendar_text` | `async (text: str)` | 设置日历中文字并切日历模式一(upload_rili 序列) | + +### 其他命令 + +| 方法 | 签名 | 说明 | +|---|---|---| +| `parking` | `async (number: str)` | 停车牌(纯数字;⚠ 本固件不显示,K1 待查) | +| `sleep` | `async (on: bool, start_h: int, end_h: int)` | 休眠时段(0-23 时) | +| `rotation` | `async (count=None, interval=None, unit="min")` | 轮播数量 1-4 / 间隔(⚠ 本固件 no-op) | +| `lut` | `async (value: int)` | LUT 校准:0x01-0f 红、0x10-f0 黑 | +| `wifi` | `async (ssid, password, city="")` | WiFi 透传(⚠ 本固件 no-op) | +| `raw` | `async (data: bytes, channel="rxtx")` | 高级直发;channel=`rxtx`/`epd`/`alt`(alt=331f) | + +### 激活与固件 + +| 方法 | 签名 | 说明 | +|---|---|---| +| `activate` | `async (code_hex: str)` | 下发激活码(6 字节 hex) | +| `activate_auto` | `async () -> dict` | **离线激活**:EFEF→反解 MAC→keygen→下发;返回 `{device_id, mac, code}` | +| `get_device_id` | `async () -> str` | 14 hex 设备 ID | +| `ota` | `async (image: bytes, on_progress=None) -> dict` | SUOTA 升级(高危,需调用方先完成授权);⚠ 裸布局设备止于产品头门禁(docs/firmware-layout.md) | + +## 子包速查 + +| 模块 | 关键导出 | +|---|---| +| `ppclock.protocol` | GATT UUID 常量、全部帧构造函数、`keygen_code`/`recover_mac_from_device_id`(纯函数) | +| `ppclock.image_pipeline` | `process_image/adjust_pixels/dither_pixels/pack_plane`、`ALGORITHMS` | +| `ppclock.templates` | `render_template(name, payload)` | +| `ppclock.transports.base` | `Transport` 协议(自定义传输实现规范)、`TransportError` | +| `ppclock.transports.local` | `BLETransport`、`scan` | +| `ppclock.transports.bridge` | `BridgeTransport`、`scan`、`parse_via`、`BridgeError` | +| `ppclock.firmware.image` | `parse_image/unpack_body/pack_image`('pQ' 镜像) | +| `ppclock.firmware.ota` | `run_ota/package_image/xor_checksum/STATUS_ERRORS` | +| `ppclock.bridge_server` | `run()`(`ppclock-bridge` 入口) | + +## Bridge RPC 协议(v1,JSONL/TCP,默认 8971) + +请求 `{"id":N,"op":...}` → 响应 `{"id":N,"ok":bool,"data":...,"error":...}`;OTA 期间穿插 `{"event":"ota_stage|ota_block_sent|ota_progress",...}`。 + +ops:`ping / scan{timeout} / connect{address,timeout,subscribe} / write{char,data,response} / read{char} / request_device_id{timeout} / ota{image,gpio_map?,max_blocks?} / services / close` + +char 映射:`epd`=图像通道、`rxtx`=1f1f(遗留)、`alt`=331f(活命令通道)、`dis_model/dis_fw`、`spota_*`(SUOTA 五写两读)。 + +## 不变量(使用约束) + +1. 命令写一律走 **331f(alt)**;1f1f 仅 ATT-ACK 不执行(实机证据)。 +2. 图像块 244B(`03|04`+`ff|00`+2B BE 偏移),整图结束 `01`、小图结束 `AA`。 +3. 时间/日期中「十进制直拼」字段为 BCD(休眠小时、倒计时月日),`dd` 帧内为真 hex——勿混。 +4. 离线:除 BLE/LAN bridge 外无任何网络依赖。 diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..f266d20 --- /dev/null +++ b/llms.txt @@ -0,0 +1,63 @@ +# ppclock — 4.2寸墨水屏设备(DA14585 BLE)离线 SDK + +> AI 消费向项目卡(llms.txt v0.1.0)。人用文档见 README.md;签名级 API 见 docs/sdk/API.md。 + +## 一句话 + +ppclock 是 4.2" 400×300 黑白红三色墨水屏设备(Dialog DA14585,广播名 `NRF-` 前缀)的**完全离线** Python SDK 与 CLI:BLE 协议三线逆向(Web JS/APK/固件反汇编)+ 真机互证,覆盖厂商全部功能 + 离线激活 + SUOTA 工具链 + BLE bridge 远程蓝牙。 + +## 安装与运行 + +```bash +python3 -m venv .venv && .venv/bin/pip install -e ".[dev]" +.venv/bin/python -m pytest tests/ -q # 165 全绿 +``` + +- 库:`from ppclock import PPClient` +- CLI:`ppclock [--json] [--mac X] [--via bridge://host[:port]] ` +- BLE 桥:`ppclock-bridge --host 0.0.0.0 --port 8971`(在他机蓝牙物理所在机运行,本机经 `--via` 透明使用) + +## 快速示例 + +```python +async with PPClient.via_bridge("192.168.61.35:8971", mac="18:BC:5A:5D:BF:28") as dev: + await dev.set_time(tz=8) # 北京时间 + await dev.upload_image("photo.jpg", slot=0) # 传图(默认 atkinson 三色抖动) + await dev.set_mode("image0") # 显示图一 + await dev.render_template("custom", {"text": "你好【红字】"}) + await dev.activate_auto() # 离线激活(破解厂商云) +``` + +## 架构(关键不变量) + +1. **命令走 331f(alt 通道)**:1f1f 只 ATT-ACK 不执行(实机证据,勿用)。 +2. 图像:400×300,1bpp MSB-first 行主序双平面(bw 15000B + red 15000B);块 244B(`03|04`+`ff|00`+2B BE 偏移);整图 `01` 结束、小图 `AA`。 +3. BCD 字段(休眠小时/倒计时月日)与真 hex 字段(dd 时间帧)严格区分。 +4. 离线激活:`code[i] = ror2(((MAC[i]<<1)&0xFF)^0xEF) % 101`;MAC 由 EFEF 设备 ID(14hex,参数序 MAC5 在前)无损反解。 +5. 零网络依赖(除本机 BLE / LAN bridge)。 + +## 目录 + +- `src/ppclock/`:client.py(门面)/ protocol / image_pipeline / templates / transports(base,local,bridge) / firmware(image,ota) / cli / bridge_server +- `tools/`:fw_info/fw_pack(镜像工具)、field_test(真机引导测试)、flash_run、gpio_probe、rppclock.sh(守候重试) +- `docs/`:protocol.md(协议规范·唯一事实源)、sdk/API.md、architecture.md、firmware-analysis.md、firmware-layout.md、field-test-result.md +- `analysis/`:三线逆向证据(web/apk/firmware) +- `tests/`:165 测试 + +## 设备事实(实测 NRF-5DBF28) + +- GATT:EPD svc `13187b10-…`/char `4b646063-…`(h=48,notify+write);RXTX svc `00001f10`;**活命令 char `0000331f`(h=57)**;`00001f1f`(h=53,write-only,不执行);SUOTA svc `0000fef5`(自定义 6 特征)。 +- 闪存:本机裸布局(app@0x0,无 'pP' 引导、0x38000 无 'pR' 产品头)→ SUOTA 止于产品头门禁(状态 20);工厂布局与恢复路径见 docs/firmware-layout.md。 +- 睡眠:广播窗口短(约 30-60s),连接态保持清醒;批量操作用单连接(PPClient 或 `ppclock batch`)。 +- no-op 命令(本固件):轮播 e9xx/EA0x、WiFi 配置。 +- 停车牌 EF:命令被收但不显示(K1 待查)。 + +## 扩展点 + +- 自定义传输:实现 `ppclock.transports.base.Transport`(鸭子类型协议),注入 `PPClient(transport)`。 +- 新模板:`ppclock.templates.RENDERERS[name] = fn(payload) -> PIL.Image`。 +- bridge 协议:docs/sdk/API.md §Bridge RPC。 + +## 版本 + +0.1.0(CHANGELOG.md)。治理:GOVERNANCE.md(证据纪律/门禁);历史决策:DECISIONS.md。