# 墨水屏 Web 上位机 BLE 协议提取报告(原始证据) > 来源:analysis/web/(ble.js 4408 行 / index.html 1309 行 / moban.js 943 行 / timedjs.js 714 行 / timedjs2.js 323 行 / switch2.js 54 行 / time.js 21 行 / hex.js 15 行) > 设备:4.2寸 400x300 黑白红三色墨水屏,DA14585,设备名前缀 `NRF-` ## 1. GATT 结构 | 项 | 值 | 证据 | |---|---|---| | 设备名过滤 | `namePrefix: 'NRF-'` | ble.js:4052-4054 | | optionalServices | `0000221f-...34fb`、`00001f10-...34fb`、`13187b10-eba9-a3ba-044e-83d3217d9a38` | ble.js:4054 | | EPD Service | `13187b10-eba9-a3ba-044e-83d3217d9a38` | ble.js:4163 | | EPD Characteristic | `4b646063-6264-f3a7-8941-e65356ea82fe` | ble.js:4165 | | RXTX Service | `00001f10-0000-1000-8000-00805f9b34fb` | ble.js:4101 | | RXTX Characteristic | `00001f1f-0000-1000-8000-00805f9b34fb` | ble.js:4103 | | `0000221f` | 仅声明未使用 | ble.js:4054 | - epdCharacteristic:图像数据块 + 刷屏命令(`00xx`/`01`/`AA`),writeValueWithResponse(ble.js:38-44),订阅 notify(ble.js:4167-4205)。 - rxtxCharacteristic:全部控制命令(时间/模式/WiFi/激活/倒计时/模板槽位),writeValueWithResponse + 100ms 后 read(ble.js:50-71);另有 writeValueWithoutResponse(ble.js:84-90);notify 是设备ID 14 字节应答主通道(ble.js:4121-4137)。 - 连接顺序:preConnect()(ble.js:4045)→ connect()(ble.js:4158)先 EPD 后 RXTX(ble.js:4207);断线重连≤5次(ble.js:30-36)。 ## 2. 命令协议 ### 2.1 发送封装 | 函数 | 行为 | 证据 | |---|---|---| | sendCommand | EPD writeValueWithResponse | ble.js:38-44 | | rxTxSendCommand | RXTX write+100ms read | ble.js:50-75 | | rxTxSendCommand2 | RXTX writeWithoutResponse | ble.js:84-90 | | triggerEpdCmd | hex→EPD | ble.js:3971-3974 | | triggerRxTxCmd | hex→RXTX | ble.js:3963-3966 | | triggerRxTxCmd2 | hex→RXTX(无响应) | ble.js:3967-3970 | | setri | RXTX 发 cmd,300ms 后补 `e2` | ble.js:3956-3962 | | clearScreen | EPD 发 `00{cmd}` 再 `01` | ble.js:45-49 | | triggerEpdRed_lut | RXTX `E6{cmd}`(0 直接返回) | ble.js:3975-3980 | | triggerEpdJH | RXTX `EF{cmd}`,500ms 后 `e2` | ble.js:3981-3987 | | triggerEpdCHEPAI | 字符双写+`EF` 前缀,500ms 后 `e2` | ble.js:3988-3998 | | triggerRxTxCmd_LED | `cmd+note+color+HH+MM` | ble.js:3999-4009 | | triggerRxTxCmd_XM | `cmd+HH+MM` | ble.js:4010-4020 | | triggerRxTxCmd_LB | `e901`+hex(n)+cmd | ble.js:4022-4038 | ### 2.2 Opcode 一览 **EPD 通道:** | Opcode | 含义 | 证据 | |---|---|---| | `00{xx}` | 刷屏第一步 | ble.js:47 | | `01` | 刷屏执行/整图结束刷新 | ble.js:48,160,230 | | `AA` | 日历小图传输结束 | ble.js:159 | | `03{ff\|00}{offset:2B BE}{data}` | 整图数据块(≤240B/块) | ble.js:98,104 | | `04{ff\|00}{offset:2B BE}{data}` | 日历小图数据块(hex 总长 7000/18000 时) | ble.js:99,104 | **RXTX 通道:** | Opcode | 含义 | 证据 | |---|---|---| | `e2` | 提交/强制刷新(多数命令收尾) | ble.js:256,3961 等 | | `dd{unix:4B}{year:2B}{month:1B}{day:1B}{week:1B}` | 设置时间 | ble.js:251-257 | | `2300`/`2301` | 三色/黑白屏模式 | index.html:840-841 | | `14` | 12/24 小时制切换 | index.html:845 | | `15` | 时钟颜色切换 | index.html:881 | | `e3` | 反色模式 | index.html:878 | | `e4` | 时钟字体切换 | index.html:880 | | `e5` | 旋转180° | index.html:886 | | `e10000`..`e10003` | 图片模式 图一~图四 | index.html:868-873 | | `e10103`/`e10104`/`e10105` | 日历模式一/二/三 | index.html:860-862 | | `e10201`/`e10202`/`e10203` | 时钟模式一/二/三 | index.html:853-855 | | `E6{lut}` | 红/黑 LUT 校准:01–0f 设红、10–f0 设黑 | ble.js:3978; index.html:885-887 | | `FB01{HH}{MM}` / `FB00{HH}{MM}` | 开/关休眠时段 | index.html:897-898; ble.js:4017 | | `e901{n:hex}{01\|00}` | 图片轮播间隔(01=分钟 00=小时) | ble.js:4035 | | `EA0{slot}` | 上传选槽位 EA00–EA03 | ble.js:213,237 | | `EA000{n-1}` | 轮播数量 1–4 | ble.js:241-249 | | `EF{激活码hex}` | 激活设备 | ble.js:3891,3983 | | `EF{逐位双写号码}` | 停车牌("12"→`EF1122`) | ble.js:3990-3995 | | `EFEF` | 请求设备 ID(14B notify 应答) | ble.js:3924,4123 | | `FA` | 关闭倒计时 | timedjs2.js:142,308 | | `ff00{year}{MM}{dd}` | 时钟倒计时日期(year=4位十进制ASCII) | timedjs2.js:250-253 | | `ffff{year}{MM}{dd}` | 日历倒计时日期 | timedjs2.js:245-248 | | `04{128B hex}` + `0{charCount}{余hex}` | 时钟倒计时前缀文字位图(>4字符拆两包) | timedjs2.js:206-217 | | `0{charCount}{位图hex}` | 同上 ≤4字符单包 | timedjs2.js:218-224 | | `990{j}{72B hex}` | 日历倒计时文字位图(每字符一包,24x24/8) | timedjs2.js:229-233,300-304 | | ASCII `WIFI_SSID:..;WIFI_PASS:..;CITY:..` | WiFi+天气城市配置(UTF-8 直发) | ble.js:3839-3863 | | `{cmd}{note}{color}{HH}{MM}` | LED 变体(遗留) | ble.js:4000-4008 | ## 3. 图像传输协议(sendBufferData ble.js:92-107) 1. 分块:`my_step=480` hex 字符 = 240 数据字节/块(ble.js:9,100)。 2. 帧格式(ble.js:104):`cod`(1B: 03整图/04小图) + `code`(1B: ff=bw平面 00=红平面) + offset(2B 大端,=字节序号) + ≤240B 数据。块总长 244B。 3. cod 判定(ble.js:98-99):hex 长度 7000(=3500B=280x100/8) 或 18000(=9000B=400x180/8) 用 `04`,否则 `03`。 4. 无应用层 ACK,顺序 writeWithResponse(ble.js:102-106)。 5. 结束:delay(300) → 整图发 `01`;日历小图(宽280 或 高180)发 `AA`(ble.js:158-160)。 上传函数差异: | 函数 | 前置 | 打包 | 结束 | |---|---|---|---| | upload_image | 无 | 296宽→canvas2bytes_bw;250/212宽→bw;其他→canvas2bytes 双平面 | 01/AA | | upload_image_moban(bat) | RXTX `EA0{bat}` | 296宽→bw,否则双平面 | 01 | | upload_image_lb(cmd) | RXTX `EA0{cmd}` | 同 upload_image | 同 | | upload_image_lb_num(cmd) | RXTX `EA000{cmd-1}` | 不传图 | 无 | 三色 = 两次发送:bw 平面(code=ff) + 红平面(code=00)(ble.js:154-155,219-222)。 ## 4. 像素打包(timedjs.js) | 函数 | 扫描 | 位序 | 用途 | 证据 | |---|---|---|---|---| | canvas2bytes | 行主序 y0→H,x0→W | MSB first,8px/B | 400x300/800x480/280x100/400x180 | timedjs.js:564-584 | | canvas2bytes_bw | 列主序 x 从 W-1→0,y0→H | MSB first | 2.9寸296x128、2.13寸 | timedjs.js:586-611 | | canvas2bytes_to | 转置 | MSB first | 未调用 | timedjs.js:612-632 | 位值:bw 平面 R,G,B 全>0→1(白) 否则 0(黑);bwr 平面 R>0,G=0,B=0→1(红)(timedjs.js:573-576,599-601)。 400x300 → 每平面 15000B。红色判定:`r>g*1.5 && r>b*1.5 && r>threshold`(timedjs.js:255 等)。 抖动算法(dithering() 分发 timedjs.js:513-563): - 黑白:none/floydsteinberg(7,3,5,1 /16)/atkinson(1/8×6)/bayer(4x4 [[15,135,45,165],[195,75,225,105],[60,180,30,150],[240,120,210,90]])/stucki(/42)/jarvis(/48) - 黑白红:bwr_ 前缀同名 6 种,默认 bwr_atkinson - threshold 默认 125;diffusion 0–100/100 误差缩放 - bwrPalette=黑白红(timedjs.js:504-508) - 预处理 applyImageAdjustments:亮度/对比度/饱和度(timedjs.js:2-38),灰度 0.299/0.587/0.114 ## 5. 时钟/日历/倒计时 setTime(ble.js:251-257)RXTX 连发 `dd…` + `e2`: | 偏移 | 长度 | 含义 | |---|---|---| | 0 | 1B | `dd` | | 1 | 4B | unix 秒(本地时区修正 Date.now()/1000 - tzOffset*60,ble.js:4222) | | 5 | 2B | 年(大端) | | 7 | 1B | 月 1–12 | | 8 | 1B | 日 | | 9 | 1B | 星期(getDay()||7:周一=1…周日=7) | 倒计时: - 时钟界面 gridSize=16:>4 字符(中文=1 英文=0.5)拆两包 `04`+128B / `0{n}`+余;≤4 单包 `0{n}`(timedjs2.js:206-224) - 日历界面 gridSize=24:每字符一包 `990{j}`+72B,间隔1s(timedjs2.js:228-235) - 日期帧 `ff00{year}{MM}{dd}`(时钟)/`ffff...`(日历),year=4位十进制ASCII(timedjs2.js:244-254) - 收尾 `E2`;关闭 `FA`+1s+`E2`(timedjs2.js:141-145) - upload_rili:`990*`→`FA`→`e10103`→`E2`(timedjs2.js:308-312) ## 6. 模板系统(moban.js) 6 模板渲染到 #unifiedCanvas(#epdwh 选 400x300/800x480/296x128),无 BLE 代码;上传走 upload_image_moban(0)→`EA00`+图块+`01`(index.html:1243)。 | 模板 | data-template | 渲染 | |---|---|---| | 日程 schedule | renderPresetTemplate | moban.js:140-145,369-467 | | 名片 businesscard | 同上 | moban.js:146-151 | | 备忘录 memo | 同上 | moban.js:152-157 | | 课程表 course | renderCourseTable(默认5天x4节,3–7天、2–9节) | moban.js:29-63,165-317 | | 二维码 qrcode | renderQRCodeMode(qrcodejs CorrectLevel.M) | moban.js:81-113,501-570 | | 自定义文本 customText | renderCustomMode(自动换行) | moban.js:572-693 | 红字语法 `【…】`(parseColoredText moban.js:470-498);反色模式 7 种(moban.js:122-137)。 ## 7. 激活机制 - API:`http://4638e813e4.qicp.vip:8889/key_generator.php`(ble.js:3866) - fullAutoActivate(ble.js:3932-3955): 1. 读 #fixedId(用户固定ID,UI 隐藏 index.html:930-935) 2. getDeviceIdFromScreen:RXTX 发 `EFEF`,8s 超时,notify 累计 14 字节(EXPECTED_ID_BYTES=14 ble.js:4123),可打印转 ASCII 否则 hex(ble.js:4121-4137) 3. requestActivationCode:GET `?id={fixedId}&key={deviceId}`(ble.js:3879-3886) 4. extractActivationCode:正则 `激活码1:([0-9A-F]{32,})` / `激活码:(…)` / 任意≥32hex(ble.js:3869-3876) 5. sendActivationToDevice:`EF`+大写激活码,400ms 后 `e2`(ble.js:3889-3902) - 算法在服务端 key_generator.php,客户端无实现。自助站 qbsg.top,作者微信 2549294317。 - 固件下载同 host:`http://4638e813e4.qicp.vip:8889/GuJian/file_down.php`(index.html:836),型号列表在服务端。 ## 8. 高级命令 index.html:919-920:#cmdcmd 任意 hex → triggerRxTxCmd 直发 RXTX。 hex.js:2-15:hexToBytes/bytesToHex/intToHex(大端 pad)。 ## 9. 变体与其他 - LED=灯板变体(遗留);XM=休眠(FB00/FB01);LB=轮播(e901/EA000);CHEPAI=停车牌;JH=激活;lut=LUT 校准 E6 - 设备名前缀仅 `NRF-`(ble.js:4053) - chinaCities 城市→天气代码表 ble.js:259 起约3580行,用于 WIFI 配置 CITY 字段 - 死代码:SendCommand(i 未初始化)、upload_image_lbs(未定义)