# BLE 协议规范(4.2寸墨水屏设备 · 实现标准) > 状态:**单一事实来源**。实现 `src/ppclock` 以本文件为准。 > 证据来源:`analysis/web/protocol-raw.md`(Web JS)+ `analysis/apk/apk-analysis.md`(APK 反编译互证)。 > **互证结论**:GATT UUID、NRF- 前缀、240B 分块、ff/00 平面码、dd 时间帧、e2 收尾、e100/e101/e102 模式族、倒计时 04/0n/990/ff00/ffff 帧、6 种抖动(Bayer 矩阵逐值一致)、红色判定、灰度系数 —— 三线一致。 > Web 独有(APK 未实现):WiFi 配置、EFEF 取设备 ID、2300/2301、14/15、e3/e4/e5、E6、FB、e901/EA000、EA0x —— 本规范以 Web 证据为准。 > APK 独有:`$IMG(0,0)`、8000/8001 图像起始帧(属扫地/轻舞固件家族分叉,本设备不适用)。 ## 1. 设备与 GATT | 项 | 值 | |---|---| | 广播名前缀 | `NRF-` | | EPD Service | `13187b10-eba9-a3ba-044e-83d3217d9a38` | | EPD Characteristic(图像+刷屏,write+notify) | `4b646063-6264-f3a7-8941-e65356ea82fe` | | RXTX Service | `00001f10-0000-1000-8000-00805f9b34fb` | | RXTX Characteristic(命令,write+read+notify) | `00001f1f-0000-1000-8000-00805f9b34fb` | | `0000221f-...` | 预留,未使用 | 连接顺序:先 EPD service → 再 RXTX;写均用 write-with-response(`triggerRxTxCmd2` 例外,without-response)。 ## 2. 屏幕参数 | 参数 | 值 | |---|---| | 分辨率 | 400 × 300 | | 色彩 | 黑/白/红 三色(亦可切黑白模式) | | 像素打包 | 1bpp,MSB first,行主序(y:0→300, x:0→400) | | 平面 | bw 平面 15000B + 红平面 15000B(红仅三色模式) | | bw 位值 | R,G,B 任一>0 → 1(白),全 0 → 0(黑)⚠即"非黑即白" | | 红平面位值 | R>0,G=0,B=0 → 1(红) | | 日历小图 | 280×100(3500B)或 400×180(9000B),同 1bpp | ## 3. 帧格式 ### 3.1 图像数据块(EPD 通道) ``` [cod:1B][code:1B][offset:2B BE][payload:≤240B] ``` | 字段 | 值 | |---|---| | cod | `03`=整图;`04`=日历小图(数据总长 3500B 或 9000B 时) | | code | `ff`=bw 平面;`00`=红平面 | | offset | 本块在平面内的字节偏移,大端 | | payload | ≤240 字节;按 offset 顺序连发,块间无应用层 ACK | 结束:全部块发完延时 ≥300ms → 整图发 `01`;日历小图发 `AA`。 ### 3.2 刷屏(EPD 通道) `00{param}` 后接 `01`。 ### 3.3 时间设置(RXTX 通道) ``` dd [unix:4B BE][year:2B BE][month:1B][day:1B][week:1B] → 随后发 e2 ``` - unix = 本地时区修正秒(`now_utc + tz`);week:周一=1 … 周日=7。 - 测试向量:2026-07-24 15:30:00 (UTC+8) 周五 → `dd 6982a7f0 07ea 07 18 05`(unix=1785475800=0x6982A7D8 ⚠按实现时实际值校验,此处仅示意格式)→ 待真机/互证校准。 ### 3.4 倒计时(RXTX 通道) - 日期:`ff00{YYYY}{MM}{DD}`(时钟界面)/ `ffff{YYYY}{MM}{DD}`(日历界面);YYYY=4 位十进制 ASCII。 - 前缀文字位图(时钟界面,16px 字格):≤4 字符(中文=1,英文=0.5)单包 `0{charCount}{位图hex}`;>4 拆两包:`04{前128B}` 然后 `0{charCount}{剩余}`。 - 前缀文字位图(日历界面,24px 字格):每字符一包 `990{索引}{72B}`,包间隔 1s。 - 位图打包:1bpp MSB first 行主序。 - 收尾 `E2`;关闭倒计时 `FA` → 1s → `E2`。 - 日历中文字设置:`990*` → `FA` → `e10103` → `E2`。 ### 3.5 激活与设备 ID(RXTX 通道) 1. 请求 ID:发 `EFEF` → notify 收 **14 字节**(可打印即 ASCII,否则 hex 表示)。 2. 激活码:原流程 `GET http://4638e813e4.qicp.vip:8889/key_generator.php?id={用户ID}&key={设备ID}` → 返回 ≥32 hex(正则 `激活码1:([0-9A-F]{32,})`)。⚠算法在服务端,本地化方案见 §7。 3. 下发:`EF{激活码hex大写}` → 400ms → `e2`。 ### 3.6 WiFi 配置(RXTX 通道) ASCII UTF-8 直发:`WIFI_SSID:{ssid};WIFI_PASS:{pass};CITY:{城市代码}`(城市代码为天气代码,如 `101010100`)。 ## 4. 命令 opcode 总表(RXTX 通道,除注明外) | 命令 | 含义 | |---|---| | `e2` | 提交/刷新(多数命令收尾) | | `dd…` | 设置时间(§3.3) | | `2300` / `2301` | 三色模式 / 黑白模式 | | `14` | 12/24 小时制切换 | | `15` | 时钟颜色切换 | | `e3` | 反色模式 | | `e4` | 时钟字体切换 | | `e5` | 旋转 180° | | `e10000`–`e10003` | 图片模式:显示图一~图四 | | `e10103` / `e10104` / `e10105` | 日历模式一/二/三 | | `e10201` / `e10202` / `e10203` | 时钟模式一/二/三 | | `E6{01-0f}` / `E6{10-f0}` | 红 / 黑 LUT 校准(越大越深;0 无效) | | `FB01{HH}{MM}` / `FB00{HH}{MM}` | 开启 / 关闭休眠时段 | | `e901{n}{01\|00}` | 轮播间隔 n(01=分钟,00=小时) | | `EA0{0-3}` | 上传目标槽位(图一~图四) | | `EA000{0-3}` | 轮播数量 1–4(值=n-1) | | `EF{hex}` | 激活码下发 | | `EF{数字逐位双写}` | 停车牌号码("12"→`EF1122`) | | `EFEF` | 请求设备 ID | | `FA` | 关闭倒计时 | | `ff00…` / `ffff…` / `990…` / `04…` / `0…` | 倒计时(§3.4) | | `00{xx}`+`01`(EPD) | 刷屏 | | `01`(EPD) | 整图传输结束刷新 | | `AA`(EPD) | 小图传输结束 | 模式切换命令(`e1xx`/`23xx`/`14`/`15`/`e3`-`e5`)发后 300ms 补 `e2`(setri 模式)。 ## 5. 抖动算法参数(图像管线) | 算法 | 参数 | |---|---| | none | 阈值化,threshold 默认 125 | | floydsteinberg | 误差核 7/16,3/16,5/16,1/16 | | atkinson | 6 邻域 ×1/8 | | bayer | 4×4 矩阵 [[15,135,45,165],[195,75,225,105],[60,180,30,150],[240,120,210,90]] | | stucki | /42 核 | | jarvis | /48 核 | | bwr_* | 同上 6 种三色版;红色判定 `r>g*1.5 && r>b*1.5 && r>threshold` | 预处理:亮度 ±255·v/100;对比度 (c+100)/100(128 中点);饱和度=灰度插值(0.299/0.587/0.114);diffusion 0–100/100 误差缩放。 ## 6. 标准会话流程 1. 扫描(namePrefix `NRF-`)→ 连接 → EPD service → RXTX service → 订阅两通道 notify。 2. (可选)`EFEF` 取设备 ID。 3. 对时:`dd…` + `e2`。 4. 操作:模式切换(+`e2`)/ 传图(`EA0{slot}` → 数据块×N → `01`/`AA`)/ 倒计时 / WiFi。 5. 断线自动重连 ≤5 次。 ## 7. 本地化(去云)清单 | 云依赖 | 本地化方案 | |---|---| | key_generator.php 激活码 | 分析固件校验逻辑(见 docs/firmware-analysis.md),本地实现算法或固件补丁绕过 ⚠待固件分析 | | file_down.php 固件下载 | 固件已本地存档 app/;SUOTA 由 `ppclock ota` 实现(§9) | | qbsg.top 自助激活 | 同上激活方案 | | 天气(CITY 代码) | 本地化=不依赖天气;CLI 提供 `wifi-config` 透传 | | qrcodejs CDN(模板用) | CLI 用本地 qrcode 库 | ## 9. SUOTA 固件升级(证据:APK §e) | 项 | 值 | |---|---| | Service | `0000fef5-0000-1000-8000-00805f9b34fb` | | SERV_STATUS(notify) | `64b4e8b5-0de5-401b-a21d-acc8db3b913a` | | MEM_DEV | `8082caa8-41a6-4021-91c6-56f9b954cc34` | | GPIO_MAP | `724249f0-5ec3-4b5f-8804-42345af08651` | | PATCH_DATA(自定义映射) | `457871e8-d516-4ca1-9116-57d0b17b9cb2` | | PATCH_LEN(自定义映射) | `9d84b9a3-000c-49d8-9183-855b673fda31` | 流程:MEM_DEV←`{00,00,00,13}`(SPI Flash)→ GPIO_MAP←`{00,00,06,05}` → 镜像+尾 1B XOR 校验 → 240B 块(PATCH_LEN 2B LE + 20B 无响应写块 + 等 SERV_STATUS==2)→ MEM_DEV←`{00,00,00,FE}` → 等 ==2 → `{00,00,00,FD}` 重启。 状态码:2=成功,3-9/17-22/0xFFFF/0xFFFE=各类错误(见 ota.py STATUS_ERRORS)。 ## 8. 测试向量(实现前必须写入 tests/) > 来源:analysis/web/protocol-raw.md 各节;标 ⚠ 的待 APK/固件互证确认。 1. setTime 布局(§3.3)⚠ 2. 图像块帧头:400×300 bw 平面首块 → `03 ff 0000` + 240B ⚠ 3. 小图判定:3500B → cod=`04`;15000B → cod=`03` 4. 停车牌 "12" → `EF1122` 5. 轮播 5 分钟 → `e9010501` 6. 槽位 3 上传前置 → `EA02`;轮播数量 4 → `EA0003` 7. 停车牌/激活/取 ID 的 `EF` 前缀区分:`EFEF`=取ID、双写数字=车牌、≥32hex=激活