Files
qianbian/analysis/apk/apk-analysis.md
T

228 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# qianbian Android APK 逆向分析报告(client-qbsg_v1.1.0.apk)
> 分析方法:androguard 4.1.4 静态分析(DEX 字符串表 + xref + DAD 反编译目标类);native 库 strings/导出符号粗扫。
> 对照基线:analysis/web/protocol-raw.md(下称"Web 报告")。
> 分析产物:analysis/apk/{classes.txt, classes-qbsg.txt, strings-clean.txt, xrefs-key-strings.txt, decomp/*.java, native/libapp.so, native/libeditor.so}
## 0. APK 基本信息
| 项 | 值 | 证据 |
|---|---|---|
| 包名 | `io.github.bszapp.dzjq42` | AndroidManifest |
| 版本 | v1.1.0-20260612-a2c9488 (versionCode 4) | AndroidManifest |
| minSdk / targetSdk | 24 / 37 | AndroidManifest |
| 主 Activity | `qbsg.top.MainActivity` | AndroidManifest |
| 权限 | BLUETOOTH/ADMIN/SCAN/CONNECT、ACCESS_FINE_LOCATION、INTERNET、**REQUEST_INSTALL_PACKAGES** | AndroidManifest |
| native 库 | arm64-v8a: libapp.so (1.5MB)、libeditor.so (3.8MB)、libandroidx.graphics.path.so | APK lib/ |
| 技术栈 | Kotlin Compose Multiplatform;共享逻辑经 R8 混淆(`La0/*`、`Lz7/*` 等);`qbsg.top.*` 为保留名的 Android 平台层;libapp.so/libeditor.so 为 **Rust** 编译(含 sha2 0.10.9、aes 0.8.4、rust-secp256k1 0.11 crate 路径字符串) | classes.txt、libapp.so strings |
应用结构要点:
- `qbsg.top.ble.BleManager/BleSession`:Android GATT 平台层(未混淆)。
- `e8.o`(抽象 EpdManager)两个实现:`c8.g`(固件家族名"平平/扫地",`j()` 返回)与 `d8.f`("轻舞/时光")——**APK 显式支持至少两个固件家族**,协议细节有差异(见 §3/§4)。
- `z7.b0`:独立 SUOTA 管理器(扫描→连接→SPOTA 全流程,20.6 万字符反编译代码)。
- `a8.g`:经已有会话的 OTA 通道(GattOtaChannel)。
- `qbsg.top.ble.Simulated*`:整套模拟设备框架("NRF-模拟设备"演示模式,SimulatedBleKt.buildSimulatedProfiles 引用 "NRF-")。
- `qbsg.top.template.editor.LibEditor`:JNI 桥到 libeditor.so(Rust 模板编辑器,导出 create/renderPng/exportBitmap/createElement 等)。
## a. GATT UUID 对照
证据:`qbsg.top.ble.BleManagerKt.<clinit>`(decomp/qbsg_top_ble_BleManagerKt.java:11-21)。
| UUID | 用途 | 与 Web 一致? |
|---|---|---|
| `13187b10-eba9-a3ba-044e-83d3217d9a38` | EPD_SERVICE_UUID | ✅ 一致 |
| `4b646063-6264-f3a7-8941-e65356ea82fe` | EPD_CHAR_UUID | ✅ 一致 |
| `00001f10-0000-1000-8000-00805f9b34fb` | TX_SERVICE_UUID(RXTX) | ✅ 一致 |
| `00001f1f-0000-1000-8000-00805f9b34fb` | TX_CHAR_UUID | ✅ 一致 |
| `00002902-...` | CCCD | Web 未显式用(Web BLE 隐式) |
| `00002A24-...` / `00002A26-...` | MODEL_NUMBER / FIRMWARE_REVISION(DIS) | **Web 端没有**(APK 连接后读型号+固件版本,BleManager$startDeviceInfoRead$1.java:47-56) |
**Web 端没有、APK 新增的 UUID**(全部用于 OTA,详见 §e):
| UUID | 角色 |
|---|---|
| `0000fef5-0000-1000-8000-00805f9b34fb` | Dialog SPOTA 服务(z7_b0.java:666 等) |
| `64b4e8b5-0de5-401b-a21d-acc8db3b913a` | SPOTA_SERV_STATUS(z7_b0.java:768) |
| `8082caa8-41a6-4021-91c6-56f9b954cc34` | SPOTA_MEM_DEV(z7_b0.java:679) |
| `724249f0-5ec3-4b5f-8804-42345af08651` | SPOTA_GPIO_MAP(z7_b0.java:677) |
| `457871e8-d516-4ca1-9116-57d0b17b9cb2` | **本固件的 SPOTA_PATCH_DATA**(z7_b0.java:665/1029 错误文案"SPOTA_PATCH_DATA未找到"对应此 UUID;注意这是 Dialog 标准里的 PATCH_LEN UUID,厂商做了自定义映射) |
| `9d84b9a3-000c-49d8-9183-855b673fda31` | **本固件的 SPOTA_PATCH_LEN**(z7_b0.java:670/1023;非标准 UUID) |
| `42c3dfdd-77be-4d9c-8454-8f875267fb3b` | SPOTA_MEM_INFO(兼容探测,z7_b0.java:772、a8_g.java:255) |
| `61c8849c-f639-4765-946e-5c3419bebb2a` / `b7de1eea-823d-43bb-a3af-c4903dfce23c` | OTA 兼容探测用备选特征(z7_b0.java:769-770、a8_g.java 头部) |
| `0000221f-...`(Web 声明未用) | APK strings 中**未出现** |
| `258EAFA5-E914-47DA-95CA-C5AB0DC85B11` | 仅字符串表存在,无代码 xref |
设备名过滤:代码扫描不过滤(`startScan(0, ...)` 无 ScanFilter,BleManager.java:761),UI 层按 `name.startsWith("NRF-")` 过滤(a9_b0.java:102;SUOTA 扫描同,a9_v2.java:190)。与 Web `namePrefix:'NRF-'` 一致。
连接参数:连接后 `requestMtu(512)`(BleManager$startDeviceInfoRead$1.java:80),`onMtuChanged` 更新会话 negotiatedMtu(默认 23);`BleSession.getMaxPayloadBytes() = max(20, mtu-3)`(BleSession.java:162-170)。connectGatt 用 TRANSPORT_LE(2)、autoConnect=false(BleManager.java:619)。
## b. 命令集对照
APK 命令发送模型与 Web 不同:**命令列表执行器** `e8.j`——把 `["e10201","TIME","e2"]` 这类列表逐条经 RXTX `sendCommand` 发出,`"TIME"` 占位符运行时替换为 `e8.o.m()` 构造的 `dd…` 时间命令(e8_j.java:111-114)。`sendCommand` 按 MTU-3 分块、互斥锁串行、10s 超时(BleSession.java:187-258)。
| 命令 | APK 证据 | 与 Web 一致? |
|---|---|---|
| `dd{unix:4B}{year:2B}{mo}{d}{week}` 设置时间 | `e8.o.m()`(e8_o.java:57-73):unix 秒+时区偏移、4B/2B/1B/1B/1B 大端 hex | ✅ 完全一致(Web ble.js:251-257) |
| `e2` 提交 | 所有命令列表收尾(c8_g.java:63 等) | ✅ |
| 模式切换 e100/e101/e102 系列 | 动态构造:行 0/1/2 → 前缀 `e102`/`e101`/`e100`(c8_g.java:39-54、s() in d8_f.java:135-150);`e1020{n+1}` 时钟、`e1010{n+3}` 日历、`e10000` 图片(c8_g.java:137-146) | ✅ 覆盖 Web 的 e10000-03/e10103-05/e10201-03 |
| 停车牌 `ef`+号码 | `c8.g.d()`:**逐字符双写**("12"→`ef1122`,c8_g.java:72-91);`d8.f.d()`:**逐字符转 hex 拼接**(Integer.toString(char,16),d8_f.java:168-189)——两固件家族格式不同!均校验 11 位数字 | ⚠️ Web 只有双写式;d8/f 的 hex 拼接式 Web 没有 |
| 倒计时文字位图 `04`/`0{n}`/`990{j}` | `f8.a.c()`(f8_a.java:70-162):gridSize 16 → ≤4 字 `0{n}` 单包、5-8 字 `04`+前段 / `0{n}`+128B 后段;gridSize 24 → 每字 `990{j}`+72B;位图 MSB-first | ✅ 与 Web timedjs2 一致 |
| 倒计时日期 `ff00`/`ffff`+`%04d%02d%02d` | `f8.a.b()`(f8_a.java:58-68):gridSize≠24→`ff00`,24→`ffff`,年为 4 位十进制 ASCII | ✅ 一致 |
| `FA` 关倒计时 | 字符串表存在(`fa`),命令列表中未直接出现(UI 文案有"关闭") | ≈ |
| 全刷/其他设置(e3/e4/e5/e6/14/15/2300/2301/FB/e901/EA0x) | **DEX 字符串与 xref 中均无**(xref 验证:'2300'/'2301'/'fb01'/'e901'/'EA00'/'EFEF' → 0 引用) | ❌ **APK 不实现**(三色/黑白切换、12/24h、颜色、反色、字体、旋转、LUT 校准 E6、休眠时段 FB、轮播 e901/EA000、模板槽位 EA0x 均为 Web 独有) |
| `EFEF` 请求设备 ID | **无**(xref=0)。APK 不读设备 14B ID,改用 DIS 2A24/2A26 + MAC 识别设备 | ❌ Web 独有 |
| WiFi 配置 `WIFI_SSID:...` | **无**(strings 无 WIFI_SSID,UI 无 WiFi 入口) | ❌ Web 独有 |
| 激活 `EF{code}` 发设备 | **无**(APK 激活是 App 本地的,见 §d) | ❌ 机制完全不同 |
| `$IMG(0,0)`(hex `24494d4728302c3029`) | c8/f 图片上传末尾经 RXTX 发送(c8_f.java:186) | ❌ **APK 独有**,Web 无此命令 |
| 上传开始 `8000`/`8001`、`0000`、`020000`,结束 `0101` | 见 §c | ❌ 帧格式与 Web 不同(见差异节) |
## c. 图像传输与像素处理
### 发送流程(EPD 通道,`BleSession.sendEpdBytes`,带写完成回调、互斥锁、5s 超时)
`c8.f`("平平/扫地"固件,c8_f.java:137-200):
1. EPD `{80,00}`(`8000`)
2. EPD `{00,00}`(`0000`)
3. EPD `{02,00,00}`(`020000`)
4. 数据帧循环:`[0x03, plane, offHi, offLo, ≤240B]`,plane:BW 平面 `0xFF`、红平面 `0x00`,offset 大端(c8_f.java:246-270)
5. EPD `{01,01}`(`0101`)
6. RXTX `$IMG(0,0)` → RXTX `e2`
`d8.e`("轻舞/时光"固件,d8_e.java:92-170):
1. EPD `8001` → `0000` → `020000`
2. 同样的 0x03 数据帧(240B/块,ff=BW、00=红)
3. EPD `0101` → delay(300ms) → RXTX `e2`(普通)或 `aa`(**isCalendar 预设为真时**,d8_e.java:150-167,g8.c.f=isCalendar)
与 Web 对照:分块 240B、平面码 ff/00、offset 大端 ✅ 一致;**帧头恒为 0x03,APK 没有 Web 的 0x04 小图帧**;首尾帧不同(Web:`00{cmd}` 开头 / `01` 或 `AA` 结尾;APK:`80xx`+`0000`+`020000` 开头 / `0101` 结尾 + RXTX `e2`/`aa`)。三色 = 两个平面依次发送 ✅。
### 像素打包(与 Web 完全同源)
- `a.b.h()`(a_b.java:866-914):行主序 MSB-first 8px/B;BW 平面 `R>0&&G>0&&B>0→1`,红平面 `R>0&&G==0&&B==0→1` —— = Web canvas2bytes。
- `a.b.i()`(a_b.java:916-963):列主序 x 从 W-1→0 —— = Web canvas2bytes_bw(296/250/212 宽屏用)。
- `d8.f.o()`(d8_f.java:39-96):d8/f 家族自带打包:宽 212/250/296 走列主序否则行主序;BW 平面判定为 **`R>0→1`**(d8_f.java:119-133,不含 G/B 检查;对抖动后的纯色调色板等价,但红像素在 BW 平面也会置 1,与 a.b.h 不同)。
- 红色判定 `g8.f.e()`:`r > g*1.5 && r > b*1.5 && r > threshold`(g8_f.java:45-53,float 1.5)—— = Web timedjs.js:255。
- 灰度 `g8.f.d()`:0.299/0.587/0.114(g8_f.java:40-43)。
- Bayer 4x4 矩阵 [[15,135,45,165],[195,75,225,105],[60,180,30,150],[240,120,210,90]](g8_f.java:5-8)—— 与 Web 逐值一致。
### 抖动算法
`g8.a` 枚举:NONE / FLOYD_STEINBERG / **ATKINSON(默认,g8.a.d)** / BAYER / STUCKI / JARVIS(g8_a.java:10-16)——与 Web 同 6 种;默认 Atkinson 与 Web 默认 bwr_atkinson 对应。配置 `g8.d`(ImageConfig):亮度/对比度/饱和度/diffusion/threshold(默认128)/旋转角度/scaleMode/isThreeColor(g8_d.java:229-250)。
预设 `g8.c` = FirmwareImageUploadPreset(screenSize, uploadWidthPx, uploadHeightPx, useRotatedScan, singleChannel, isCalendar)——**上传参数由固件型号预设驱动**,默认 400x300(g8_d.java:102-130)。
倒计时文字位图渲染 `f8.a.a()`:Android Canvas 绘制(DEFAULT_BOLD、fakeBold、抗锯齿、居中),按阈值二值化(f8_a.java:10-56)。
## d. 激活流程 ★(Web 端没有的关键信息)
**APK 的激活完全是 App 本地校验,算法在 libapp.so(Rust)里。**
JNI 接口 `qbsg.top.activation.ActivationManager`(decomp/qbsg_top_activation_ActivationManager.java):
```java
static { System.loadLibrary("app"); }
public final native boolean nativeActivate(String code);
public final native boolean nativeChecksumOk(String code);
public final native String nativeDecryptSegment(String hex);
public final native String nativeGetDeviceId();
public final native void nativeInit(Object ctx);
```
libapp.so 导出符号(readelf .dynstr):
- `Java_qbsg_top_activation_ActivationManager_native{Init,GetDeviceId,ChecksumOk,Activate,DecryptSegment}`
- `Java_io_github_bszapp_dzjq42_utils_SignatureUtils_getNativeSignature`(签名防伪校验)
- 字符串证据:引用 `android/provider/Settings$Secure`、`android_id`、`getContentResolver()`(**设备 ID 源自 ANDROID_ID**);rust-secp256k1(`pubkey != NULL`、`SECP256K1_FLAGS_BIT_COMPRESSION`)、sha2-0.10.9、aes-0.8.4 crate;运行时错误串 `[not activated]`、`[decrypt error: too short]`。
调用链(MainActivity.onCreate,qbsg_top_MainActivity.java:19-35):
1. `nativeInit(context)` → `nativeGetDeviceId()` 写入 StateFlow(UI 展示设备 ID)。
2. 读 SharedPreferences `activation_code`;非空且 `nativeChecksumOk(code)` 为真 → `nativeActivate(code)` → 置"已激活"全局标志(`w7.a.h.b.c`)。
3. `y7.a.a(code, fromQr)`(y7_a.java:21-61):手动/扫码激活入口,成功则持久化 `activation_code`;失败提示"激活码无效或不属于本设备"。
4. 扫码激活 `y7.a.b(bytes)`(y7_a.java:63-110):QR 原始字节 ≥71、前 4 字节 ASCII `"qbsg"`,取**最后 67 字节**作为激活码字符串 → `nativeActivate`。→ **激活码为 67 字符**。
5. `s0.u0` case 13(s0_u0.java:82-99):`nativeDecryptSegment` 仅在"已激活且处于特定模式"时可用——用于解密 §e 的两段密文 UUID。
密码学形态推断(基于 crate 组合,未做指令级逆):secp256k1(很可能 ECDSA/Schnorr 公钥验证,私钥在签发方)+ SHA-256 + AES(DecryptSegment 用)。"不属于本设备"说明激活码与 `nativeGetDeviceId()`(ANDROID_ID 派生)绑定。**这取代了 Web 的服务端 key_generator.php 模式**:Web 发 `EF`+激活码给设备固件校验;APK 在 App 侧本地校验,不给设备发任何激活命令。
另有**服务端激活 API**(面向"换固件"付费场景,与本地码并行):
- `POST https://qbsg.top/api/activation/activate`,JSON `{token, provider_id, firmware_id, mac: "mac1,mac2,…"}`(a8_f.java:529-545,`k8.h.c` = okhttp POST application/json,k8_f.java)。
- `POST https://qbsg.top/api/activation/check`,body 为设备 ID 字符串(a9_c2.java:62/80/98)。
- `POST https://qbsg.top/api/activation/firmware-list`(b9_g0.java:127)。
- 文案佐证:"Switching firmware requires paid activation."、"以下 %s 台设备未找到激活码"、"BatchActivationFirmwareOption(id=…"、"Batch firmware upgrades and sequential commands"(批量固件升级+顺序命令)。
## e. 固件 OTA / SUOTA ★(Web 端完全没有)
APK 有完整 OTA 体系,两条路径同一协议核心:
### 路径 1:独立 SUOTA 管理器 `z7.b0`(扫描 NRF- 设备→专连升级,支持批量/并发 Semaphore)
流程(z7_b0.java:655-1720,超时:连接 30s、写 8s、状态等待 60s):
1. 连接 + 服务发现,必须存在服务 `0000fef5`(否则"设备不支持SUOTA服务")。
2. 兼容探测 SERV_STATUS `64b4e8b5`;另探测 `42c3dfdd`/`b7de1eea`/`61c8849c` 备选特征。
3. `requestMtu(23)`(默认;若已协商更高则用 min(20, mtu-3) 作写块),`requestHighPriority()`。
4. SERV_STATUS 开 notify(CCCD 写 `{01,00}`,z7.k0.a)。
5. 写 MEM_DEV `8082caa8` ← `g(318767104)` = 4B 小端 `{00,00,00,13}`(mem type 0x13=SPI Flash;`z7.b0.c()` 恒返回该值,z7_b0.java:426-430)。
6. 写 GPIO_MAP `724249f0` ← `g(84280064)` = `{00,00,06,05}`(对应 UploadConfig 默认 misoGpio=5, mosiGpio=6, csGpio=3, sckGpio=0, i2cAddress=0x50,z7_o0.java toString)。
7. 镜像封装 `z7.h0`(z7_h0.java:7-34):**固件字节末尾追加 1 字节全镜像 XOR 校验**;块长 b = max(240, chunk)(默认 240B),写块长 c = MTU-3(默认 20B)。
8. 每块:写 PATCH_LEN `9d84b9a3` ← `f(blockLen)`(2B 小端)→ 分 20B 无响应写 PATCH_DATA `457871e8` → 等 SERV_STATUS notify == 2,否则报"块N失败: …"。
9. 全部块后写 MEM_DEV ← `g(-33554432)` = `{00,00,00,FE}`(END_SIGNAL)→ 等最终 SERV_STATUS==2(设备侧 XOR/CRC 校验,失败报"CRC校验失败")。
10. 写 MEM_DEV ← `g(-50331648)` = `{00,00,00,FD}`(重启设备)。
SERV_STATUS 错误码表(z7_k0.java:8-28,中文文案):2=操作成功 3=强制退出SPOTA 4=Patch Data CRC失败 5=长度与PATCH_LEN不符 6=外部存储写失败 7=内部存储不足 8=无效存储器类型 9=应用层错误 17=无效镜像Bank 18=无效镜像头部 19=无效镜像大小 20=无效产品头部 21=相同镜像错误 22=外部存储读失败 65535=通信错误 65534=设备不支持SUOTA。
### 路径 2:会话内 OTA `a8.g.f(byte[] firmware, String, GattOtaChannel)`(a8_g.java:94-…)
同一协议核心(fef5 服务探测、MTU 默认 23→20B 块、z7.h0 240B 块封装、PATCH_LEN/PATCH_DATA、SERV_STATUS 轮询),经 `BleManager$AndroidGattOtaChannel` 在已连接会话上执行。
### 固件来源与目录
- `POST https://qbsg.top/api/firmware/catalog` → 固件目录(b9_g0.java:118);`POST /api/activation/firmware-list` → 已购固件列表(b9_g0.java:127)。
- 目录项字段:firmwareId / firmwareFileName / firmwareCrc / firmwareTotalBytes / downloadUrl(strings-clean.txt:2247-2253、21021-21023;b1_m 同构解析逻辑)。
- 下载 `k8.h.b(url,…)` 流式写本地缓存(a8_f.java:396-435),`.img` 后缀(strings:3035)。**OTA 用固件从 qbsg.top 服务器下载,与 Web 端 4638e813e4.qicp.vip:8889/GuJian/file_down.php 不同源**。
- OTA 前还有 FirmwareImageUploadPreset 驱动的"升级画面"图片上传(g8.c)。
### 激活门控的加密 UUID
`z7.b0` 构造时(z7_b0.java:50-51)调 `s0.u0(13)` → `ActivationManager.nativeDecryptSegment` 解密两段 128-hex 密文:
- `a232499efa74dd64…d01d` → `this.e`
- `3fd29fea11187d48…7791` → `this.f`
字段初始为全零 UUID(`00000000-0000-0000-8000-000000000000`),**未激活时解密返回空串**(s0_u0.java:89-97 的门控逻辑)。两段明文应为 UUID(类静态块编译了 UUID 正则)。这是反盗版设计:部分 OTA 能力(大概率是某自定义服务/特征 UUID 或厂商私有权鉴)只对已激活 App 可用。
### App 自更新(与设备固件无关)
`POST https://qbsg.top/api/check-update`,body `{machine: <设备ID>, version: 4, platform: "android"}` → `{isLatest, latestVersionCode, latestVersionName, downloadUrl, updates:[{versionName,changelog}]}`(b1_m.java:215-245);下载 APK 后借 FileProvider + REQUEST_INSTALL_PACKAGES 安装("下载完成,点击安装")。
## f. 网络依赖清单(本地化去云清单)
DEX 中全部 URL(strings-clean.txt 提取,**无 mspppclock.tech、无 4638e813e4.qicp.vip**——Web 端两个服务器在 APK 均不存在):
| 端点 | 用途 | 证据 |
|---|---|---|
| `https://qbsg.top/api/activation/activate` | 固件激活(付费换固件) | a8_f.java:541 |
| `https://qbsg.top/api/activation/check` | 激活状态查询 | a9_c2.java |
| `https://qbsg.top/api/activation/firmware-list` | 已购固件列表 | b9_g0.java:127 |
| `https://qbsg.top/api/firmware/catalog` | 固件目录 | b9_g0.java:118 |
| `https://qbsg.top/api/check-update` | App 自更新 | b1_m.java:217 |
| `https://qbsg.top/api/fonts` | 云端字体 | strings |
| `https://qbsg.top/api/pictures`(+`/thumbnail?name=`)、`https://qbsg.top/Pictures/` | 云图库 | strings |
| `https://qbsg.top/api/templates/community/{list,download,downloaded,like,unlike}`、`/api/templates/{submit,my-list,preview,delete,tags,tmp-upload?token=}` | 模板社区 | strings |
| `https://qbsg.top/api/user/{info,logout}`、`https://qbsg.top/#/loginonly`、`/#/me?logintoken=` | 账号体系(AccountManager、网页登录 token) | strings、AccountScreen$submitToken |
| 营销外链 | QQ 群 280372025、拼多多店铺 mall_id=214155601、闲鱼 userid=355198804 | strings |
HTTP 栈:okhttp3,`k8.h`(POST JSON `k8.h.c`、下载 `k8.h.b`)。Web 端协议报告的激活站 `key_generator.php` 与固件站 `file_down.php` 在 APK 中均无迹。
## g. 与 Web 端差异汇总
| # | 差异 | 说明 |
|---|---|---|
| 1 | **激活机制根本不同** | Web:设备 ID(EFEF 14B)→qicp.vip key_generator.php→`EF{码}` 发设备;APK:App 本地 libapp.so(Rust, secp256k1+SHA-256+AES) 校验 67 字符码(QR: "qbsg"+67B),绑定 ANDROID_ID,不发任何激活命令给设备 |
| 2 | **OTA 仅 APK 有** | 完整 Dialog SPOTA/SUOTA(fef5 服务、240B 块+20B 写块、XOR 尾字节、FE/FD 收尾),固件来自 qbsg.top;Web 无任何 OTA |
| 3 | **图像首尾帧不同** | Web:`00{cmd}` 起、`01`/`AA`(EPD) 止;APK:`8000/8001`+`0000`+`020000` 起、`0101`(EPD)+`e2`/`aa`(RXTX) 止,c8/g 家族还发 ASCII `$IMG(0,0)`;APK 恒用 0x03 帧头(无 Web 的 0x04 小图帧) |
| 4 | **Web 独有的设备功能** | WiFi 配置、EFEF 设备 ID、2300/2301、14/15、e3/e4/e5、E6 LUT、FB 休眠、e901/EA000 轮播、EA0x 槽位——APK 全部未实现(APK 定位是"图片/模板/倒计时上传 + 固件升级"工具) |
| 5 | **APK 独有的协议细节** | 连接后 requestMtu(512);读 DIS 2A24/2A26 型号与固件版本;双固件家族(平平/扫地 vs 轻舞/时光)打包与停车牌格式分叉;`aa` 按 isCalendar 预设走 RXTX;SUOTA PATCH_LEN/DATA 使用非标准 UUID 映射(9d84b9a3 / 457871e8) |
| 6 | **服务器完全不同** | Web:4638e813e4.qicp.vip:8889(激活+固件);APK:qbsg.top 全家桶 API(激活/固件/字体/图库/模板社区/账号/自更新) |
| 7 | 一致项 | EPD/RXTX UUID、NRF- 前缀、240B 分块、ff/00 平面码、dd 时间帧、e2 收尾、e100/e101/e102 模式族、倒计时 04/0n/990/ff00/ffff 帧、6 种抖动(Bayer 矩阵逐值一致)、红色判定 r>1.5g&&r>1.5b、灰度系数 |
## 附:关键文件位置
- 反编译源码:analysis/apk/decomp/(BleManagerKt=UUID 常量、BleSession=发送层、e8_o/e8_j=命令执行器、c8_g/d8_f=模式管理器、c8_f/d8_e=图像上传、g8_f/a_b=像素与抖动、f8_a=倒计时、z7_b0/z7_h0/z7_k0=SUOTA、a8_g/a8_f=OTA 与会话、b9_g0=固件目录、b1_m=自更新、y7_a=激活入口、s0_u0=解密门控)
- native 库:analysis/apk/native/libapp.so(激活算法,Rust)、libeditor.so(模板编辑器)
- 字符串表:analysis/apk/strings-clean.txt(28367 条);关键字符串 xref:analysis/apk/xrefs-key-strings.txt