# 品牌 → 型号 → 协议能力矩阵

> **doc-id** `analysis/protocol-matrix` · **层** 结论（速查表，权威）· **状态** 权威，档案发现的错回写本文 · **建档** 2026-09-20 · **修订** 2026-09-26
> 文档系统 [`docs/README.md`](../README.md) · `docs/evidence/*` 是本文的证据层，不留两份互相矛盾的表

> 数据来源：对三个官方 APK 的静态逆向（jadx 1.5.6 + 资源反编译），全部结论标注 `path:line`。
> 分析日期 2026-09-20。样本：`TUWIN.apk` 1.6.4、`XTU GO_8.4.3_243`、`idGoLive_1.3.6.apk`。
> 语料：`_work/tuwin_src/`、`_work/xtu_src/`（+ `_work/xtu_res/`、`_work/xtu_bad/`）、`_work/idgo_live_src/`。
> 背景与架构启示见 [APK 逆向分析报告](apk-overview.md)，本文只回答四件事：**有哪些型号 / 怎么识别 / 怎么连 / 怎么通讯 / 有哪些设置项**。

---

## 0. 四品牌总览

| | TUWIN 1.6.4 | XTU GO 8.4.3 | idGoLive 1.3.6 | SJCAM Zone 6.7.3.15 |
|---|---|---|---|---|
| 包名 | `com.tuwinsmart.tuwin` | `com.gku.xtugo` | `com.id221.golive`（Java 命名空间 `com.icatch.golive`） | `org.jght.sjcam.zone` |
| 方案商 | 自研（Clean Architecture） | gku（按 SoC 分包） | iCatchTek（`com.icatchtek.*`），**白标公版 App** | 方案商自有（`org.jght.*`），**一个 App 五套 SoC 通道** |
| 型号维度 | 按**机型**枚举（4 个） | 按**芯片**分派（3 套），机型 18 个 | 按**探测结果**分派，四层：PTP 会话成败 → ICMP 应答者 → `getSSID` 成败 → 相机名黑名单。**无「机型→能力」注册表，但有「机型名→跳过写时区」黑名单** | 按**网关 IP + 相机自报 model 串**分派五通道；三套互相不兼容的型号词表（`SJCAM*` / `<芯片>-<型号>` / 裸串） |
| 型号数 | 4 | 17 运动相机 + 1 行车记录仪 | 无正式表；SSID 图标表反推 ≈ 14 个自有型号，另有 `CameraProperties.java:1476` 的 **33 项产品名黑名单**（含 5 个贴牌名）；清单侧唯一「机型表」是 `device_filter.xml` 的 2 条 USB VID/PID | 约 40 个型号串（§7.1），官方选择列表 26 项 |
| 控制通道 | HTTP REST / HTTP CGI / TCP 推送 | HTTP CGI / JSON-over-TCP / 72B 二进制帧 | **PTP over IP（原生）** + HTTP `?custom=1&cmd=` + `/app/` JSON + XML-over-TCP:3333 + BLE JSON + UDP 组播配网 + PTP-over-USB 三种传输（共 **11 条**，见 §3.4） | HTTP `?custom=1&cmd=`（Novatek）· HTTP `/cgi-bin/hisnet/*.cgi`（海思）· HTTP `:8082/api/` JSON（全志）· **JSON-over-TCP 7878**（Ambarella）· PTP（iCatch，不实现） |
| 图传 | RTSP（8080 / 554 / 动态） | RTSP `:554/livestream/12`（老海思另有 `/11` 与 HTTP 变体） | **PTP 原生拉流**（参数串 `H264?W=&H=&FPS=&BR=` **由固件返回**）；副机 RTSP；直播上行 RTMP | RTSP `:554/xxx.mp4`（Novatek，拍照模式退化为 `:8192` HTTP）· RTSP `:554/livestream/12`（海思）· RTSP `:8554/ch01`（全志）· RTSP `/live`（Ambarella） |
| 配网入口 | 仅 WiFi 直连 | WiFi 直连 + 扫码 + 蓝牙 | WiFi 直连 + 蓝牙 + USB + 组播 AES `simpleConfig` + 手机开热点（**无 QR 扫码配网、无 Wi-Fi Direct**） | WiFi 直连（`isSjWifi` 前缀表）· 扫码 |
| 设置项来源 | 设备下发（菜单 XML / param 枚举） | 设备下发（capability 协商；**仅老海思方言在 APK 里有一张 PreferenceScreen 静态表**） | **PTP 设备属性表（`0x50xx` / `0xD6xx` / `0xD7xx` / `0xD8xx` + 高位能力码，57 个码点）** + `getparamitems` + `cmd=3031` 菜单树（App 只认 4 个 `Cmd`） | 设备下发：Novatek `cmd=3014` 的 `<Cmd>/<Status>` 字典 · 海思 `getmedia/getsetting` 的 `var item/value` 平行数组 · 全志 `cmd=3031` 的 `Menu[].MenuList` · Ambarella `msg_id=3/9` |
| 默认网关 IP | `192.168.25.1` / `192.168.169.1` / `192.168.0.1` | `192.168.0.1` | `192.168.1.1` / `192.168.1.254` / `192.168.169.1` | `192.168.1.254`（Ly）· `192.168.0.1`（hisnet）· `192.168.10.1`/`192.168.100.1`（全志）· `192.168.42.1`（Amba） |
| 默认 WiFi 密码 | `12345678` | `12345678` | `1234567890`（10 位，另有一处 `boi_2020` 开发残留） | 全志 `12345`；其余未在 APK 中发现写死口令 |
| minSdk / targetSdk | 24 / 36 | 23 / 35 | 28 / 35 | 24 / 35（versionCode 10061） |
| 通道鉴权 | `seed` 随机数握手（仅预览） | 无（激活码 `setactivate` 另说） | **HTTP 完全无鉴权**；PTP 凭据槽传空串 | HTTP 无鉴权；Amba 会话 `msg_id=257` 换 token（明文） |
| 下载续传 | `?offset=`，禁 `Range`，受固件版本门限 | 仅 Ambarella 支持 offset；CGI 家族的 8080 分支是 append 追加 | 标准 HTTP `Range: bytes=N-`（PTP 侧另有 native `ptp_getpartialobject`） | 标准 HTTP（Amba 侧另有 8787 数据 socket 的分块通道，我们未实现） |


**一句话结论**：四家都把「设置项」做成了**设备自描述、运行时协商**，APK 里根本没有完整的参数表；而「型号 → 协议」的映射，只有 TUWIN 是显式枚举的，XTU 完全靠相机自报的 `type` 字段分派，idGoLive 更是把主协议整条塞进 native `libcontrol.so` 的 PTP 栈里。这直接决定了我们的插件必须**以探测结果路由，而不是以型号名路由**。


---

## 1. TUWIN（`com.tuwinsmart.tuwin`）

TUWIN 是唯一把「机型 → 协议」写成代码的品牌：`DeviceType` 枚举 4 个常量，每个绑一份 `DeviceProtocolProfile`（型号白名单 + 探测端点 + 端口/路径白名单）。
证据：`core/domain/model/DeviceType.java:14-18`、`core/network/DefaultDeviceProtocolProfileRegistry.java:19`。

### 1.1 型号清单

| DeviceType | code | 显示名 | 产品类别 | 协议型号串 `expectedModels` | 判定芯片 |
|---|---|---|---|---|---|
| `RIDE3PRO` | `"RIDE3PRO"` | Ride3 Pro | Riding Recorder（骑行记录仪） | `{"R3Pro"}` | 未标注 |
| `RIDE6` | `"RIDE6"` | Ride6 | Riding Recorder | `{"R6"}` | 未标注 |
| `RIDE5` | `"RIDE5"` | Ride5 | Riding Recorder | `{}` → **不做型号校验**，走 `Ride5IdentityPolicy` | Hi3516CV610（身份串 `HI3516CV610-S-FV-CARRECORDER`） |
| `M3` | `"M3"` | M3 | Action Camera（运动相机） | `{"M3", "v57_sport_cam_ezgui"}` | v57 平台（`ezgui` 固件线） |

- 显示名/类别来自资源：`ride3pro`/`ride5`/`ride6`/`device_m3`、`riding_recorder`/`thumb_camera`。
- 适配器只按 `DeviceType` 注册（`DeviceAdapterRegistry.java:24-39`），**没有子型号表**；同一 `DeviceType` 下的硬件差异由固件上报的 `hwver`/`boardversion` 承载（仅 RIDE6 把 `hwver` 记为 boardVersion，`Ride3ProSessionConnector.java:1907-1911`）。
- 未发现自行车灯 / 头盔 / 其它产品线（对 `Bike|Tail|Cyc|Dashcam|尾灯` 全量 grep 为空）。

### 1.2 识别方式

**(a) WiFi SSID —— 两套规则，用途不同，必须都实现。**

扫描推断用锚定正则（`core/util/ConnectUtil.java:33-38`）：

| 常量 | 字面正则 |
|---|---|
| `M3_SSID_REGEX` | `^TUWIN[-_]M3_[A-Za-z0-9]+$` |
| `RIDE3PRO_SSID_REGEX` | `^TUWIN_R3P_[A-Za-z0-9]+$` |
| `RIDE6_SSID_REGEX` | `^TUWIN_R6_[A-Za-z0-9]+$` |
| `RIDE5_SSID_REGEX` | `^TUWIN_R5_[A-Za-z0-9]+$` |
| `RIDE5_PRO_SSID_REGEX` | `^TUWIN_R5P_[A-Za-z0-9]+$` |
| `RIDE5_COMMON_SSID_REGEX` | `^TUWIN_[A-Za-z0-9]{6,}$` |

匹配顺序即优先级：**M3 → RIDE3PRO → RIDE6 → RIDE5**（`ConnectUtil.java:128-147`）。RIDE5 放最后是因为 `RIDE5_COMMON_SSID_REGEX` 是兜底式，会把 `TUWIN_R3P_xxx` 也吞掉。返回 `null` 表示不是本品牌设备。

系统连接用 `PatternMatcher`（**非锚定**前缀，`DeviceWifiNetworkRequestFactory.java:68-85`，flags = `CASE_INSENSITIVE|UNICODE_CASE`）：

| DeviceType | PatternMatcher 串 |
|---|---|
| M3 | `TUWIN[-_]M3_[A-Za-z0-9]+` |
| RIDE3PRO | `TUWIN_R3P_[A-Za-z0-9]+` |
| RIDE6 | `TUWIN_R6_[A-Za-z0-9]+` |
| RIDE5 | `TUWIN_[A-Za-z0-9]{6,}` |

SSID 预处理：`trim()` + 去成对双引号（`normalizeSsid`，`ConnectUtil.java:212-225`）。泛化判定：`isConnectedTUWIN()` = `ssid.startsWith("TUWIN") || 缓存列表包含`（`:125`）。

**(b) 型号字段 —— 探测响应里读哪个 key。**

| 机型 | 响应对象 | 读取的 key | 证据 |
|---|---|---|---|
| RIDE3PRO / RIDE6 | `Ride3ProDeviceInfo` | **`model`**（另有 `uuid`/`swver`/`hwver`/`mac`/`ssid`/`pwd`/`soc`/`features`） | `ride3pro/Ride3ProDeviceInfo.java:18-42`；`Ride3ProSessionConnector.java:1904-1915` |
| M3 | `M3ProductInfoResponse{result,info}` | **`model`**（另有 `company`/`soc`/`sp`），且要求 `result == 0` | `dto/m3/M3ProductInfoResponse.java:74-76,87` |
| RIDE5 | CGI 文本解析出的 `Map` | **`model`，回退 `product`**；`devid`=序列号、`softversion`=固件、`boardversion`=板号 | `Ride5SessionConnector$connectUserInitiated$verified$1.java:136-153` |

`acceptsModel()` 语义（`DeviceProtocolProfile.java:183-206`）：trim 后空串 → false；`expectedModels` 非空 → **大小写不敏感全等**；`expectedModels` 为空 → **一律 true**（所以 RIDE5 不在此把关）。

RIDE5 另有 `Ride5IdentityPolicy.accepts(model, product)`：字面量 `"DBG"`、含 `"R5"`/`"RIDE5"` 的取反分支、以及 `product=="GNR" && model=="HI3516CV610-S-FV-CARRECORDER"` 特例。⚠️ 该方法 jadx 控制流反编译异常，**分支方向属 INFERRED，字面量 CONFIRMED**；接入前需真机抓一次 `getdeviceattr.cgi` 定死。

**(c) 其它标识**：无蓝牙（清单无 BT 权限，包内零 `Bluetooth|GATT`）。MAC 只用于 RIDE5 的 `deviceIdFromBssid`（去分隔符大写、必须匹配 `[0-9A-F]{12}`、黑名单 `020000000000/000000000000/FFFFFFFFFFFF`，`Ride5IdentityPolicy.java:22-37`）。连接后一致性以 **BSSID 优先**（`ConnectUtil.java:170-176`）。

**(d) 探测端点（profile 声明 = 实际调用顺序）**

| 机型 | 声明的 bootstrap | 运行时实际 |
|---|---|---|
| M3 | HTTP:80 `/app/getproductinfo` | 一次调用即定身份 |
| RIDE3PRO / RIDE6 | HTTP:80 `/api/device/status` | **两次**：先 `device/status`，再 `device/info`（身份取自后者，端点证据记前者） |
| RIDE5 | HTTP:80 `/cgi-bin/hisnet/getdeviceattr.cgi` | 一次 CGI，随后过 `Ride5IdentityPolicy` |

身份不符 → `DeviceIdentityMismatchException("Unexpected device identity: expected=…, actual=…, model=…")`（`DeviceIdentityVerifier.java:149-153`）。

### 1.3 连接方式

| 机型 | 默认网关 IP | 控制端口 | 媒体端口 |
|---|---|---|---|
| RIDE3PRO / RIDE6 | `192.168.25.1` | 80 | **8080**（`Ride3ProDeviceAdapterSession.RTSP_PREVIEW_PORT`） |
| M3 | `192.168.169.1` | 80 | RTSP 默认 554，实际从设备返回的 RTSP URI 取；**推送 socket 端口 = `/app/getmediainfo` 响应的 `port`**（校验 `1 ≤ port < 65536`，非法即整个 open 失败，`M3DynamicEndpointPolicy.java:59-62`、`:78`）。`getproductinfo` 的 `sp` 键客户端解析了但**零消费者**，与端口无关 |
| RIDE5 | `192.168.0.1` | 80（CGI） | profile 里 `allowedMediaPorts` 为空（字段层面的事实），但端口**有常量**：直播 RTSP `RTSP_LIVE_PORT = 554`、反向回调 TCP `CALLBACK_PORT = 9002`（`Ride5DeviceAdapterSession.java:53,57`；两处都挂了 `@Deprecated`，仍在 `:250`/`:251` 用来签发 `EndpointGrant`） |

证据：`core/device/DeviceHostPolicy.java:35-48`、`M3DynamicEndpointPolicy.java:21,59-66`、`DefaultDeviceProtocolProfileRegistry.java:19`。

- **只有 WiFi 直连一条路**：`WifiNetworkSpecifier` + `requestNetwork`，**必须带 WPA2 密码**（空密码直接抛 `IllegalArgumentException("Device Wi-Fi passphrase is required")`）；密码是常量 `DEFAULT_WIFI_PASSWORD = "12345678"`（`ConnectedDevice.java:22`），用户从不输入 SSID。
- 两种连接模式（`DeviceWifiConnectRequest.isExact()`）：给了确切 SSID 或 BSSID → `setSsid()+setBssid()`；否则 → `setSsidPattern()`。
- **未发现**：手动输 IP、扫码配网、蓝牙配网。
- 进程级 `bindProcessToNetwork` **不是主路径**，只在「平台不肯给 SSID」时作为探测兜底（`TemporaryProbeBindingLease.java:42-54`，要求 `permit.ssidReadable == false` 且有 routeRevision）；常态是把 OkHttp/Retrofit 绑到 `Network` 对象上。
- **连接工程数值（我们超时/重试默认值的参照，CONFIRMED）**：`WifiNetworkSpecifier` 要求 **SDK ≥ 29**（低于抛 `DeviceWifiRequestFailure.UNSUPPORTED_ANDROID_VERSION`，`DeviceNetworkController.java:272`）；路由 lease 替换等待 **3000 ms**、替换轮询 **50 ms**（`DeviceNetworkController.java:62`、`:61`）；主动 Wi-Fi 扫描等待 **8000 ms**（`DeviceWifiScanner.java:42` `DEFAULT_SCAN_TIMEOUT_MS`）。
- 断线恢复：`RecoverableDeviceControlChannel.recoverControlChannel()` + 路由版本变更时 `invalidateForRoute()` 级联重建（`DeviceSessionManager.java:353-368`、`:251-280`）。

### 1.4 通讯方式

| 机型 | 传输 | 路径前缀 | 命令形态 |
|---|---|---|---|
| RIDE3PRO / RIDE6 | HTTP:80 | `/api/` | REST，`Ride3ProApiResponse<T>` 包裹；**客户端自设三层串行**（L1 `Ride3ProRequestScheduler` 优先级队列 + 单 worker = 主路径；L2 `Ride3ProHttpRequestQueue` Mutex 兜底；L3 `Ride3ProHttpSerialDispatcher` `maxRequests=1` **只服务 Glide**）+ `retryOnConnectionFailure(false)`。⚠️「设备不接受并发」静态无证据 |
| M3 | HTTP:80 + **TCP 推送** | `/app/` | 泛化 `param` 协议 + 设备单向 JSON 推送 |
| RIDE5 | HTTP:80 | `/cgi-bin/hisnet/` | CGI，响应是**裸文本**不是 JSON |

**Ride3Pro/Ride6 端点**（`ride3pro/Ride3ProApiService.java`）：鉴权 `GET /api/authdevice?seed=<long>`（`/api/rtspstatus?seed=<long>` **只声明、零业务调用点**，不算鉴权步骤）；设备 `device/status`、`device/info`、`reboot`；拍摄 `capture`（**同样只声明无调用点**，官方「拍照」是播放器截 RTSP 帧，`DeviceDetailActivity.java:1202-1240`）、`record/start|stop`、`setmode?mode=`；设置 `menu/xml`、`menu/getparameter?id=`、`menu/setparameter?id=&value=`；回放 `playback/dayinfo?date=`、`filecount`、`filelist?mode=&file_index=`、`thumbnail?file_index=`、`rawdata`、`start|pause|stop|delete|settime|setspeed`；存储 `sd/info`、`system/formatsd`；时间 `vendor/send-time?year&month&day&hour&minute&second`；固件 `POST /api/firmware/upload?md5sum=&model=&hw=&sw=`。

**M3 HTTP 端点全表**（`m3/M3ApiService.java:47-134`，全部 `@GET`，前缀 `/app/`）：
`deletefile?file=` · `getparamitems?param=` · `getparamvalue?param=` · `getbatteryinfo` · `getcurmode` · `getdeviceattr` · `getfilelist` · `getgravitydirection` · `getmediainfo` · `getproductinfo` · `getrecduration` · `getsdinfo` · `getthumbnail?file=` · `reset` · `sdformat` · `setting?param=` · `mode?param=` · `setparamvalue?param=&value=` · `playback?param=` · `setsystime?date=` · `settimezone?` · `setwifi?wifissid=&wifipwd=` · `downloadFile(@Url)` · `POST /upload/{savepath}/{filesize}`

**M3 TCP 推送通道 —— 三个反直觉事实（会直接影响我们的实现）**：
1. **没有帧分隔符。** 线上解码器是 `LegacyReadChunkFrameDecoder`——「一次 read 到一个非空 chunk 就是一帧」（`LegacyReadChunkFrameDecoder.java:18-21`，`reset()` 是空实现）；活解码器由 `M3DeviceAdapter.java:103`（默认实参）→ `:110-121`（方法引用 `M3FrameDecoderKt.createDefaultM3FrameDecoder`）→ `M3FrameDecoderKt.java:10-12` 确定。`DelimitedFrameDecoder` 与 `UnconfiguredFrameDecoder` **都是零构造点的死类**，分隔符字节在整个语料里从未被赋值（`DelimitedFrameDecoder.java:20,30,31`，默认 maxFrame 64KB / maxBuffer 256KB —— **运行时不生效**）。单次读上限是 `maxReadBytes = 4096`（`DefaultM3SocketConnector.java:86`）。副作用：跨 read 的长 JSON 会被切成两帧，第二帧进 `Unknown`。
2. **端口不是常量**，从 `/app/getmediainfo` 响应里解析（`M3DynamicEndpointPolicy.java:59-62`、`:78`，调用链在 `M3DeviceAdapter.java:920-922`），校验 `1 ≤ port < 65536` 且无默认值——非法端口整个 open 直接失败，host 强制等于控制 host。
3. **单向推送，App 从不往这个 socket 写请求**：`SessionSocket` 实现跨 `DefaultM3SocketConnector.java:343-409`，但接口 `M3SessionSocket.java:12-18` 只有 `getFailures`/`getMessages`/`isOpen`/`close`，且全 `core/device/` + `data/source/socket/` 目录 `getOutputStream` **零命中**；无心跳/保活。连不上时只把 `M3_SOCKET_EVENTS` 从能力集去掉，会话照建（`M3DeviceAdapter.java:311-322`、`:332-338`）。

推送报文（UTF-8，trim 后单个 JSON 对象，`M3JsonMessageDecoder.java`）：信封 = 顶层 `msgid`(string) + 顶层 `info`(object) + **顶层第三个键的字面量静态不可证**（jadx 把它折叠成了 `DeepLinkPresetEvent.UMEventParamTime`，该类不在反编译产物里；按 Umeng 埋点惯例**疑为** `"time"`、类型 long，**不是 CONFIRMED**）。两个边界：**空帧 → `decode` 直接 `return null`**（既不产消息也不报错，`:33-37`）；**非法/半截 JSON → `Unknown(原始串)`，不上抛**（`:125-131`）。

| `msgid` | `info` 字段 | 语义 | App 侧消费方 |
|---|---|---|---|
| `battery` | `capacity`,`charge`（默认 0） | 电量 | 有 |
| `rec` | `value`（`==1` 录制中） | 录制状态 | 有 |
| `rec_time` | `recConsumTime`,`recFileTime` | 录制时长 | 有 |
| `sd` | `status`（**默认 99**） | SD 卡状态 | 有 |
| `cap` | `value` | 拍照 | 有 |
| `mic` | `value`（`==1` 开） | 麦克风 | **无**——被解码但没有任何 observer，麦克风状态实际靠 HTTP |
| `countdown_time` | `value` | 倒计时 | 有 |
| 其它 | — | `Unknown(rawMessage)`，不抛异常 | **无** |

**Ride5 CGI 全表**（`ride5/Ride5CgiApi.java:16-92`）—— ⚠️ **每个 query key 字面上带前导连字符**（`-workmode`、`-type`、`-value`），用会规范化 key 的 URL builder 会静默丢掉：
`checkconnect(-ip)` · `checkupgradepktinfo(-model,-softversion,-pktlen)` · `deletefile(-name)` · `sdcommand.cgi?&-format` · `getbatterycapacity` · `getcamnum` · `getcamparam(-workmode,-type)` · `getcamparamcapability(-workmode,-type)` · `getcommparam(-type)` · `getcommparamcapability(-type)` · `getdeviceattr` · `getdircapability` · `getdirfilecount(-dir)` · `getdirfilelist(-dir,-start,-end)` · `getfileinfo(-name)` · `getsdstatus` · `getwifi` · `getworkstate` · `client(-operation,-ip)` · `reset` · `setcamparam(-workmode,-type,-value)` · `setcommparam(-type,-value)` · `setsystime(-time)` · `setwifi(-wifissid,-wifikey)` · `getcamchnl(-camid)` · `workmodecmd(-cmd)`

**RTSP 预览**：R3Pro/R6 = `rtsp://<host>:8080/?action=stream`；M3 = 从 `getmediainfo` 返回的 URI 解析（scheme 必须 rtsp、**禁止 userinfo 凭据**、缺省端口 554、host 强制同控制端）；Ride5 = `rtsp://<host>:554/livestream/1`——⚠️ 端口**不来自设备属性**：`RTSP_LIVE_PORT` 是客户端 `companion object` 常量（554），先写进 `EndpointGrant`，起流时再由 `Ride5DeviceMediaPort.java:105-110` 从 grant 的 `allowedPorts.singleOrNull()` 取出来拼 URL；**设备不参与协商**，Ride5 的 CGI 里没有任何端口字段。

**seed 握手**：`seed = kotlin.random.Random.nextLong(Long.MAX_VALUE)` 的**明文非负 Long，无哈希无签名**（`Ride3ProPlaybackRepositoryImpl.java:1049`、`Ride3ProStreamRepositoryImpl.java:239`）。实际顺序：**设备认证（`authdevice`，`result` 非 0 且非 -2 才算失败）→ 同步设备时间（`vendor/send-time`，失败仅 `Timber.w`）→ `markReady` → 切换预览模式（`setmode?mode=0`，失败硬抛）→ 起播**（`Ride3ProStreamRepositoryImpl.java:239-267`、`:307-321`、`:380-392`）。UI 只有三个步骤串（`ride3pro_preview_step_device_auth` / `_switch_mode` / `_sync_time`），**不存在 rtspstatus 步骤**。「seed 让相机 RTSP 服务端授权本次连接」是**推断**：客户端没有任何把 seed 交给播放器的通路，RTSP URL 不含 seed，auth 响应的 `info` 声明为 `Any?` 且不解析。握手缓存 `Ride3ProPreviewHandshakeCache` 以 `(deviceIdentity, sessionId, routeRevision)` 为键，`consume()` 是**只读探测（peek）**：命中且未过期时 `return true` **不删除**，只有过期分支才 `remove`（`:199-211`），有效期 `VALIDITY_DURATION_MS = 600000`（**10 分钟**，单调时钟 `System.nanoTime()/1e6`）→ 预览与回放各自都能复用同一次握手。**复现成「一次性」会让每次起流多打一次 auth + send-time。**

**能力集（`DeviceCapabilities.names: Set<String>`）**：
- M3：`M3_HTTP_CONTROL`、`M3_MEDIA_ENDPOINT`、`M3_SOCKET_EVENTS`
- RIDE5：`RIDE5_HTTP_CGI`、`RIDE5_CALLBACK_9002`、`RIDE5_RTSP_LIVE`
- RIDE3PRO/RIDE6：**运行时拼接** `"<typeCode>_HTTP_CONTROL" | "_STATUS" | "_RTSP_PREVIEW"` → 实际是 `R3Pro_*` / `R6_*`（`Ride3ProDeviceAdapterSession.java:384-388`）。⚠️ interop 端不能写死，必须复刻拼接规则。

### 1.5 设置项

**结构结论（限定范围）**：Ride3Pro/Ride6 的**取值集合**（`<Value>` 列表）确实不在 APK 里，必须运行时向设备取；但「**哪些项存在**、**哪些项可写**、**分区顺序**、**英文→中文映射**」这四张表 APK 里是**写死可枚举**的。M3 的参数名表（22 键）与 Ride5 的 `-type` 词表更是**完全静态**，不受该结论约束。

**(a) Ride3Pro/Ride6 —— 设备下发菜单 XML**（文件名 `WIFI_MENU.xml`，客户端硬拒 `<!DOCTYPE`，`Ride3ProMenuXmlParser.java:33-35`）。⚠️ **取菜单的通道不是 HTTP**：`GET /api/menu/xml` 只是接口声明与「无会话 executor」时的兜底路径，有会话时官方把同一个 `getMenuXml` 改写成**裸 socket** 读同一份菜单（`Ride3ProSessionApiService.java:1498-1520` → `Ride3ProSessionTransport.java:373-380` → `readMenuXmlRaw`，`:538-543` jadx 反编译失败但局部名含 `socket`；建连用 `MENU_CONNECT_TIMEOUT_MILLIS`，`:657`），数值上限四个常量均 `@Deprecated` 且仍是活路径：`5000` / `524288` / `8192` / `30000`（`:65-74`）；**socket 上到底写什么请求静态无解**。Schema 全是**子元素、无属性**，解析器只读这 6 个名字：

```
<Category>            任意深度，文档序
├── <Name>            分区显示名
└── <Setting>         仅直接子元素，有序
    ├── <Name>        设置项显示名
    ├── <ID>          不作请求 id，但被兼容策略**数值化比较**用来认项（见下）
    ├── <Type>        原文自由字符串，客户端按整数解析（见下）
    ├── <Default>     默认值（不是「当前值」）
    └── <Value>*      仅直接子元素
        ├── <Name>    选项显示名
        └── <ID>      选项值
```

- **`<Type>` 被 `ride3ProSettingTypeCode()` 宽容解析成整数**（`0x` 前缀按 16 进制、否则 10 进制，`Ride3ProSettingsRuntimeMapperKt.java:17-35`），驱动 5 种渲染分支（`Ride3ProSettingsRuntimeMapper.java:53-94`）：`0`/`3` = 按 `<Value>` 反查选中项、`1`/`4` = 动作项（label `Execute`，且**单独再发一次 `getparameter?id=<requestId>`**，`Ride6SettingsProtocolClient.java:361,390,449` 的 `setOf(1, 4)`）、`2` = 原值直显；解析不出来时退回「原值 → `<Default>` → 空串」。
- **当前值不在 XML 里**：先 `getparameter?id=all` 批量读，再对 `Type ∈ {1,4}` 的项逐条补读。
- ⚠️ 但**不能读成「客户端完全不用 `<ID>`」**：`Ride6SettingsCompatibilityPolicy` 把 `<ID>` 做**数值化比较**来识别两项——`isFactoryReset` = `requestId=="factory_reset"` **或** id 数值 == `0x0000208`（`:17,:41-44`）、`isWifiPassword` = `requestId=="wifi_passwd"` **或** name==`Password` **或** id 数值 == `0x0000301`（`:19,:36-39`）；`factory_reset` 成功后还会把缓存的 WiFi 密码复位（`:46-56`）。

**没有** min/max/step、没有只读位、没有隐藏位——这些都在 App 侧策略类里（`Ride3ProSettingsMenuPolicy`、`Ride6SettingsCompatibilityPolicy`、`Ride6SettingsLocalizer`）。

⚠️ **最关键的互操作坑**：`getparameter`/`setparameter` 用的 `id` **不是** XML 里的 `<ID>`，而是由「分区名 + 项名」经 `normalize()` 再查表推导出来的（`Ride6ParameterRequestIds.java:40-161`）。`normalize()` **只做一次小写化**：`trim()` → `toLowerCase(Locale.ROOT)` → 逐字符 `Character.isLetterOrDigit` 过滤（`:163-184`）；**没有「先大写」这一步**，多做一次 `uppercase` 会让 `Motion Detection` → `motiondetection` 这类键全部对不上。全部可枚举 id（触发条件逐 `case` 核对，`←` 后括号 = 允许的分区名，`""` = 空分区）：

| 线上传输 id | 触发条件（归一化设置名 ← 归一化分区名） |
|---|---|
| `all` | 不参与 `forSetting`，只用于批量读 `getparameter?id=all` |
| `factory_reset` | `defaultsetting`←(`""`/`system`)、`factoryreset`←**非空**任意分区 |
| `record_exposure` | `exposure`←(`""`/`record`)、`recordexposure`←**无条件** |
| `record_loop_recording` | `looprecording`←(`""`/`record`)、`recordlooprecording`←**无条件** |
| `record_resolution` | `resolution`←(`""`/`record`)、`recordresolution`←**无条件** |
| `system_frequency` | `frequency`←(`""`/`system`)、`systemfrequency`←**无条件** |
| `system_version` | `version`←(`""`/`system`)、`systemversion`←**无条件** |
| `wifi_name` | `wifiname`←(`""`/`wifi`)、`ssid`←**非空**任意分区（**不是** `system`） |
| `wifi_passwd` | `wifipasswd`←(`""`/`wifi`)、`password`←**非空**任意分区（**不是** `system`） |

未命中 → `null`（`default:` 分支 `:158-159`）。**除这 9 个之外，R3Pro/R6 没有别的 id**（HDR/白平衡/慢动作等只存在于设备下发的 XML 里）。⚠️ 推不出 id 时 `update()` 直接抛 `IllegalStateException("R6 setting does not have a protocol request ID: …")`（`Ride6SettingsProtocolClient.java:248-251`）——即设备 XML 里任何推不出 id 的项**在官方 App 里既读不到也写不了**，这正是静态兜底菜单 31 项中只有 9 项能上线的原因。

**(b) M3 —— 显式参数名表**（`core/util/M3SettingNameMapper.java:17`，共 22 个）：
`rec_resolution`、`anti_shake`、`ev`、`wb`、`light_fre`、`denoise`、`audio_switch`、`external_mic`、`auto_poweroff`、`screen_standby`、`osd`、`image_size`、`image_quality`、`continue_shot`、`shot_timelapse_time`、`shot_timelapse_rate`、`delay_shot`、`shutter`、`rec_split_duration`、`timelapse_rate`、`timelapse_time`、`multiple`

- 枚举选项：`GET /app/getparamitems?param=all_<modeId>`（`"all_" + 整数模式 id`，`M3DeviceRepositoryImpl.java:2406`）；**读值不走 `getparamvalue`**——`/app/getparamvalue` 被 `getAllParamValues`(`M3ApiService.java:57-58`) 与 `getParamValue`(`:84-85`) 两个方法声明，但**全仓零调用方**；当前值实际来自 `getparamitems` 响应里的 `cur_index`，UI 用 `index.indexOf(curIndex)` 反查下标（`M3SettingsAdapter.java:482-490`）。把 `getparamvalue` 当既有实现去复现属**过度实现**。写值 `setparamvalue?param=&value=`。
- `rec_resolution` **随模式变化**，对 `currentModeId` 特判（`M3DeviceSettingsViewModel.java:166`）。
- UI 策略：`generalSettingNames = {auto_poweroff, screen_standby, osd}`；`hiddenSettingNames = {external_mic, logo}`（`M3SettingsMenuPolicy.java:21-22`）。
- **取值域无法静态枚举**，全部来自 `getparamitems`。

**(c) Ride5「更多设置」——词表已找到，不是未找到**。两条通道：`getcamparam.cgi?-workmode=&-type=` / `setcamparam.cgi?-workmode=&-type=&-value=`（`pType=3`，非 comm 参数）与 `get/setcommparam.cgi?-type=&-value=`（`isCommParam=true`）。`-workmode` 在该页**写死为 `"NORM_REC"`**（`MoreSettingsActivity.java:86`）。`-type` 静态可确定 **9 个**（`MoreSettingsCapabilityPolicy.java:18` 的 8 条 + WiFi 模式那条）：

| `-type` | 通道 | 成对取值 |
|---|---|---|
| `AUDIO` | get/setcamparam | `"1"` / `"0"` |
| `FLIP` | get/setcamparam | `"ON"` / `"OFF"` |
| `MIRROR` | get/setcamparam | `"ON"` / `"OFF"` |
| `MEDIAMODE`（分辨率） | get/setcamparam，能力集走 `getcamparamcapability.cgi` | 能力集逗号分隔项原文，兜底 `{"1080P+1080P"}` |
| `Rec_Split_Time`（分段时长） | 同上 | `MIN` ↔ 中文「分钟」互转，兜底 `{1,3,5 分钟}` |
| `GSR_SENSITIVITY` | get/setcommparam | `"MIDDLE"` / `"OFF"` |
| `GSR_PARKING` | get/setcommparam | `"HIGH"` / `"OFF"` |
| `VCAPVQE`（智能降风噪） | get/setcommparam | `"ON"` / `"OFF"` |
| `WIFI_DEAULT_STATE` | get/setcommparam | `"OFF"` / `"ON"` / `"ALWAYSON"` |

⚠️ 最后一条的**源码拼写就是 `WIFI_DEAULT_STATE`（少一个 `F`）**，`MoreSettingsActivity.java:87`，互操作时必须原样复刻。仍**未知**的只有 `workmodecmd.cgi?-cmd=` 的词表（观测到的字面量只有 `"stop"` 与 `"trigger"`）。

**(d) 概念 → 各机型 key 对照（注意刻意避免不了的撞名）**

| 概念 | RIDE3PRO/RIDE6 | M3 | RIDE5 |
|---|---|---|---|
| 分辨率 | `record_resolution` | `rec_resolution`（模式相关） | `-type=MEDIAMODE`（能力集走 `getcamparamcapability.cgi`） |
| 循环/分段 | `record_loop_recording` | `rec_split_duration` | `-type=Rec_Split_Time`（写值时把中文「分钟」反替换回 `MIN` 后缀） |
| 曝光 | `record_exposure` | `ev` | — |
| 白平衡 | 仅菜单 XML（**推不出 id ⇒ 只读显示**） | `wb` | — |
| HDR | **无 id** | **无 name** | — |
| 抗频闪 | `system_frequency` | `light_fre` | — |
| 音频/外接麦 | **可达 UI 层**仅菜单 XML；代码层存在一个从预览页到不了的 Ride3Pro/Ride6 分支（保留 `{AUDIO, MEDIAMODE, Rec_Split_Time}` 三条 CGI 队列） | `audio_switch`、`external_mic`(隐藏) | `-type=AUDIO`（`"1"`/`"0"`） |
| OSD / 自动关机 / 息屏 | 仅菜单 XML（同「白平衡」原因） | `osd`、`auto_poweroff`、`screen_standby` | — |
| 防抖 | 仅菜单 XML（同「白平衡」原因） | `anti_shake` | — |
| 照片尺寸/质量 | 仅菜单 XML（同「白平衡」原因） | `image_size`、`image_quality` | — |
| 延时摄影 | 仅菜单 XML（同「白平衡」原因） | `timelapse_rate/time`、`shot_timelapse_rate/time` | — |
| 快门/连拍/延迟/降噪/变焦 | 仅菜单 XML（同「白平衡」原因） | `shutter`、`continue_shot`、`delay_shot`、`denoise`、`multiple` | — |
| Logo 水印 | 仅菜单 XML（同「白平衡」原因） | `logo`（隐藏） | — |
| WiFi 名/密码 | `wifi_name`/`wifi_passwd` | `setwifi?wifissid=&wifipwd=` | `setwifi?-wifissid=&-wifikey=` |
| 恢复出厂 | `factory_reset` | `GET /app/reset` | `reset.cgi` |
| 对时 | `vendor/send-time?year&…&second` | `setsystime?date=` + `settimezone` | `setsystime?-time=` |
| 固件 | `POST /api/firmware/upload?md5sum&model&hw&sw` | `POST /upload/{savepath}/{filesize}` | `checkupgradepktinfo.cgi?-model=&-softversion=&-pktlen=`（**分包检查**，不等于上传通道本身；`Ride5CgiApi.java:19-20`） |
| 紧急录像 | 无 | 无 | `workmodecmd.cgi?-cmd=trigger` |
| 哨兵/停车监控 | 无 | 无 | `-type=GSR_PARKING`（`"HIGH"`/`"OFF"`） |
| GSR 灵敏度 | 无 | 无 | `-type=GSR_SENSITIVITY`（`"MIDDLE"`/`"OFF"`） |
| 智能降风噪 | 无 | 无 | `-type=VCAPVQE`（`"ON"`/`"OFF"`） |
| WiFi 模式三态 | 无 | 无 | `-type=WIFI_DEAULT_STATE`（`"OFF"`/`"ON"`/`"ALWAYSON"`，⚠️ 拼写原样复刻） |
| 上下/左右翻转 | 无 | 无 | `-type=FLIP` / `-type=MIRROR`（各 `"ON"`/`"OFF"`） |

> ⚠️ 「仅菜单 XML」的**原因要说清**：不是「没有这一项」，而是静态兜底菜单（`Ride3ProSettingsMenuPolicy.java:18-20` 的 31 项，含 `White Balance`=`0x00000108`、`Sequence`=`0x00000103`、`Anti-Shaking`=`0x00000106`…）里**确实有**，只是 `forSetting()` 推不出线上 id ⇒ 写操作必然抛 `IllegalStateException`。互操作时这些项应视为**只读显示**。
> 又：Ride3Pro/Ride6 状态回报里的 `hdr_chk_cnt`（`Ride3ProDeviceStatusInfo.java:26-27`）是「录像写入异常」判定字段，**不是**可写的 HDR 设置项，两者不可互推。
> 上表后 7 行属 Ride5/M3 专属页 `MoreSettingsActivity`；该页对 RIDE3PRO/RIDE6 **恒不可达**（`DeviceDetailSettingsEntryRoutePolicy.java:44-58` 直接返回 `DEDICATED_RIDE3PRO`），所以「Ride3Pro/Ride6 有 CGI 音频」这类功能官方 App 自己走不到。

### 1.6 文件下载与断点续传

- 回放条目以**整数 `file_index`** 为主键（不是文件名）：`filelist?mode=&file_index=` → `thumbnail?file_index=` / `rawdata` / `start` / `delete`。
- ⚠️ **设备侧下载不用 HTTP `Range`。** `ResumePolicy` 四态：`Disabled` / `HttpRange` / `DeviceOffset` / `DeviceQueryOffset`；其中 `HttpRange` **显式只允许 Internet 源**（云/OTA），设备源必须走 `DeviceOffset`（`ResumePolicyRegistry.java:25-55`）。
- 该结论的**三条硬证据**（此前只有策略表、没有线上证据）：① `TransferRequest.java:163-164` 对设备传输使用 `ResumePolicy.HttpRange` 时直接 `throw IllegalArgumentException("Device transfer requires an explicit device offset policy")`；② 全语料里字面 `"Range"` 只出现 1 处，且是日志脱敏白名单（`DiagnosticHttpLogPolicy.java:134`），`StreamingTransferEngine.java:1019` 读的是响应头 `Content-Range`；③ 设备下载的请求构造 `DownloadFileExecution.java:123` 是 `new Request.Builder().url(strWithOffset).build()`——**不加任何 header**，续传位置只拼进 URL。
- 设备续传的真实线上形态是 **query 参数**：`GET /api/playback/rawdata?...&offset=<bytes>`（`Ride3ProPlaybackDownloadResumePolicy.java:233-238`）。
- **续传受固件门限**：低于 `v1.20260708.101448` 直接拒绝并记 `reason=legacy_firmware`（`DownloadUtil.java:1602-1606`）。
- 恢复前置条件：`checkpoint.offset > 0` 且 `== 本地文件长度` 且 `sourceFingerprint` 匹配；响应 `Content-Length` 表示**剩余字节**而非全文件（`DownloadFileExecution.java:202`）；重试上限 5。

---

## 2. XTU GO（`com.gku.xtugo`）

XTU 按 **SoC 芯片**分三套协议栈，且**APK 里不存在「型号 → 芯片」表**——分派键是相机自报的 `type` 字段。

### 2.1 型号清单（18 个）

| # | 显示名 | 代码里的字面量 | 平台归属 | 置信度 |
|---|---|---|---|---|
| 1 | XTU X1 | `XTUX1` | CGI 家族 | 型号 CONFIRMED / 芯片仅到「非 Ambarella」 |
| 2 | XTU X2 | `XTUX2` | CGI 家族 | 同上 |
| 3 | XTU S2 | `XTUS2` | CGI 家族 | 同上 |
| 4 | XTU S5K | `XTUS5k` | CGI 家族 | 同上 |
| 5 | XTU S3 | `XTUS3`（不在选择列表） | CGI 家族 | 同上 |
| 6 | XTU S3PRO | `XTUS3Pro` | CGI 家族 | 同上 |
| 7 | XTU MAXPRO | `XTUMaxPro` | CGI 家族 | 同上 |
| 8 | XTU MAX2 | `XTUMax2` | CGI 家族 | 同上 |
| 9 | XTU MAX3 | `XTUMAX3` | CGI 家族 | 同上 |
| 10 | XTU S6 | `XTUS6` | CGI 家族 | 同上 |
| 11 | XTU S6PRO | `XTUS6Pro` | **Ambarella** | CONFIRMED（`AmbaPreviewModel.java:91` 判 `contains("XTUS6Pro")`） |
| 12 | XTU R1 | `XTUR1` | 未知 | INFERRED/低（仅与 S6Pro 共用图标素材，素材相同 ≠ SoC 相同） |
| 13 | XTU X3 | `XTUX3` | CGI 家族 | 同 1 |
| 14 | XTU Mini1 | `XTUMini1` | **iCatch 行车记录仪，不是运动相机** | CONFIRMED（`isDashCamera()` + `ICatchCamera`） |
| 15 | XTU S7 | `XTUS7` | **Ambarella** | CONFIRMED（`BTHeartbeatManage.java:18` 要求 `type∈{H75N,CV75} && name endsWith "S7"`） |
| 16 | XTU S7PRO | `XTU S7PRO` ← **带空格** | CGI 家族（**非 Ambarella**） | CONFIRMED（只在 `getdeviceattr.cgi` 成功后匹配） |
| 17 | XTU S7PRO MAX | `XTU S7PRO MAX` ← **带空格** | CGI 家族（非 Ambarella） | CONFIRMED（同上） |
| 18 | XTU MINI2PRO | `XTUMINI2PRO`/`XTUMINI2Pro` | **Ambarella** | CONFIRMED（Amba socket 回调内匹配） |

**命名坑（CONFIRMED）**：只有 S7Pro / S7ProMax 带空格，其余不带（`XTUS7` vs `XTU S7PRO`）；Ambarella 上报的名字可能带 `-` 后缀，App 用 `name.split("-")[0]` 剥掉（`HomeActivity.java:748,1765`）。

**支持直播的子集（5 个）**：S7PRO MAX、S7PRO、S7、R1、MINI2PRO（`SelectLiveDeviceActivity.java:110-114`）。

**证据来源（四份列表，彼此并不完全一致 —— 「18」是并集，不是任何一张原表）**：`DeviceAddNewActivity.java:270-286`（**17 项**，含 `Mini1`/`R1`/`MINI2PRO`/`S7PRO MAX`，**无 S3**）、`DeviceAddActivity.java:282-295`（**14 项**旧 UI，无 S3/R1/MINI2PRO/S7PRO MAX）、`CameraInfoUtils.java:11-61`（名字→图标，**17 个 `XTU*` token** + 一条 `XTU GO` 是 App 名；含 `XTUS3`、**不含 Mini1**）、`ReportFeedbackFragment.java:212-228`（**17 项**，含 `Mini1`、**无 S3**，且剥掉 `XTU ` 前缀）、`FragmentConnectedCamera.java:292-391`、`ScanAddRecycleAdapter.java:81-128`、`xtu_res/.../strings.xml:2398-2415`。⇒ Mini1 只出现在选择/反馈页，S3 只出现在图标表；两表的 **token 大小写互不相同**（`XTUMax2` vs `XTUMAX3` vs `XTUS5k` vs `XTU S7PRO`），匹配时不能只按一张表的字面量。

### 2.2 识别方式

**(a) 运行时决策树（CONFIRMED，逐行）**

```
NetworkDeviceUtils.onAvailable()                                HomeActivity:558
 └ guessDeviceIP()                                              :1853-1857
    ├ ip == 192.168.0.1 → 运动相机                               :562
    ├ ip == 192.168.1.1 → iCatch 行车记录仪                       :562-565
    └ 其它           → 不是相机，resetBT()                        :570-575
 STEP 1（先 HTTP 探测）GET http://<ip>/cgi-bin/hi3510/getdeviceattr.cgi   :1708-1710
        OkHttp，connect/write/read 超时各 5000ms，retryOnConnectionFailure=false
        解析 `var K = "V";\r\n` 键值对（HttpRequest.getMap:354-407）
        map 非空 → CGI 家族；再由自报 type 决定 SigmaStar / 老海思   :1711-1772
        map 空/null → STEP 2                                      :1774-1781
 STEP 2（再 socket）AmbaConnector.startSession() → TCP <ip>:7878   :1777-1779
        msg_id 257 start_session → token
        msg_id 261 set_clnt_info {type:"TCP", param:<手机IP>}
        msg_id 11  get_device_info → CameraInfors                 :787,663-756
```

⚠️ **顺序是「CGI 优先、Ambarella 兜底」**，与直觉相反。

**(b) 芯片分派表（唯一真实的路由键）**

| `type` 值 | 平台 | 分派点 |
|---|---|---|
| `SSC8838` / `SSC8826` / `SSC8838C` | SigmaStar → `Sigmastart*Model` | `SettingModelManage.java:17-18`、`PlaybackModelManage.java:23-24`、`PreviewModelManage.java:20,28` |
| `H75N`（常量 `SSConstant.AMBA`）/ `CV75` | Ambarella → `Amba*Model` | 上述 + `FastSettingModelManage.java:16-17` |
| 其它（`Hi`/`117`/`34220`/`""`） | 老海思 → `Haisi*Model` | 上述 else 分支 |

常量定义：`sigmastar/SSConstant.java:5-13`。`type` 永远来自设备：Ambarella 路径 `cameraInfoBean.getType()`，CGI 路径 `getdeviceattr.cgi` 的 `type` 键。

**(c) SSID / 蓝牙名规则**

| 规则 | 状态 | 证据 |
|---|---|---|
| SSID = `<型号名>_<后缀>`；App 用 `name + "_" + ssid.split("_")[1]` 重新命名 | CONFIRMED（两平台一致） | `HomeActivity.java:1803-1810`、`:1830-1836` |
| SSID 可含**多个**下划线；**最后**一段是固定后缀，前面是可改名部分 | CONFIRMED | `AmbaDeviceSettingPresenter.java:214-227`、`:201` |
| 改 SSID 必须保持长度完全不变 | CONFIRMED | `:193-195`、`strings.xml:2394-2395` |
| 判定「这是相机 AP」的唯一启发式 = `ssid.contains("XTU")` | CONFIRMED | `PopupDialogActivity.java:199`、`DeviceAddDeviceListFragment.java:315`、`DeviceAddWaveFragment.java:493` |
| **不存在** `SSID_PREFIX` 常量、不存在 SSID 正则 | NOT FOUND | 已按 `ssid`/`Pattern.compile`/`getSsid`/`String.format("XTU` 全量搜索 |
| BLE 名过滤 `name.contains("XTU") \|\| contains("GKU")` | CONFIRMED | `DeviceAddActivity.java:125`、`DeviceAddWaveFragment.java:418` |
| 「是否 BLE 配网设备」= `name.toUpperCase().startsWith("XTU_")` | CONFIRMED | `BLEConnectUtils.java:472-474` |
| 真实 BLE 名样例 `XTU_S7Pro_Max_f9be5a` | CONFIRMED（日志字面量） | `BluetoothLive.java:70` |
| 图标匹配规则 ⇒ 名字形如 `XTU_<型号token>_<6位>`：去 `_`、丢末尾 6 字符、忽略大小写比 `XTU<model>` | CONFIRMED | `ScanAddRecycleAdapter.java:77-128` |
| GATT service `0000180a-…8000-00805f9b34fb` / char `00008888-…` / CCCD `00002902-…`，MTU 请求 512 | CONFIRMED | `BluetoothLive.java:23-25`、`BLEConnectUtils.java:541,555,560` |
| MAC / OUI 规则 | NOT FOUND | 只有 Wake-on-LAN 用到 MAC |

⇒ **WiFi SSID 与 BLE 名是两套规则，不能合成一个「前缀常量」**：SSID 侧只有 `contains("XTU")` 子串判定（全树没有任何 `Pattern.compile` 用于 SSID，也不存在 `SSID_PREFIX`），`toUpperCase().startsWith("XTU_")` 只出现在 BLE 侧（`BLEConnectUtils.java:472-474`）。自研的 `CameraProfile` 里这两条要各留一个谓词。

**(d) `getdeviceattr.cgi` 返回键（设备自述）**：`name`、`region`(默认 `G`)、`pcbrevision`(默认 `V1.0`)、`serialnum`、**`type`**、`networkstatus`、`softversion`、`hardversion`（值为 `"NewAPP"` 时翻转 `CameraParameters.IsNewAPP`）、`bluetoothrxversion`、`bluetoothtxoneversion`、`bluetoothtxtwoversion`、`startdate`、`runtimes`、`model`(int)、`timeout`。证据 `HttpRequest.java:107-183`。
`getdevcapabilities.cgi` → 单键 `devcapabilities`（子串匹配，只测 `"standby"`、`"workmode"`）；`getcapability.cgi?-workmode=N&-type=M` → 键 `capability`（逗号分隔选项表）。

### 2.3 连接方式

三个入口（`DeviceAddNewActivity.java:108-143`）：**扫码**（标准 `WIFI:S:…;P:…;` 配网码，华为 ScanKit）、**蓝牙**（`R00x` 配对/PIN）、**手动**（只是跳 `android.settings.WIFI_SETTINGS`，让用户自己连，默认密码 `12345678`）。型号卡片列表点开的是**通用三步 WiFi 引导**，所有型号一样 —— 说明官方自己也没做型号差异化连接。

| Android 版本 | 机制 | 证据 |
|---|---|---|
| ≥ Q | `requestNetwork(... WifiNetworkSpecifier.setSsid().setWpa2Passphrase())` + `removeCapability(12)` | `NetworkDeviceUtils.java:403-410` |
| < Q | `WifiConfiguration`/`addNetwork` | `:405-406`；`sigmastar/wifi/beforeQ/WifiConnector.java` |
| BLE-live 路径 | `WifiNetworkSuggestion.addNetworkSuggestions` 再 `requestNetwork(ssidPattern)` | `WIFIUtils.java:42-47` |
| 路由绑定 | `bindProcessToNetworkNoNet()`；所有 socket / OkHttp 用 `getSocketFactory()` | `HomeActivity.java:1640`、`AmbaConnector.java:146`、`HttpRequest.java:49-51` |

**端口表（CONFIRMED，完整）**

| 平台 | 用途 | 端口 | 证据 |
|---|---|---|---|
| Ambarella | JSON 命令 socket | **7878** | `AmbaConstant.java:5` |
| Ambarella | 文件/缩略图数据 socket | **8787** | `AmbaConstant.java:6`、`SocketAmbaDownload` |
| 全部 | RTSP 预览 | **554** `rtsp://<ip>:554/livestream/12` | `SSCommandUtil.java:50`、`HaisiCommandUtil.java:50`、`AmbaPreviewActivity.java:972` |
| 海思老 `DV` | **预览的第二条 URL（HTTP）** | **80** `http://<ip>:80/12?trans=tcp&action=play&media=video_data` | `DV.java:331-342` |
| 海思 / SigmaStar | CGI 控制 | **80** | `Setting.java:387` |
| 海思 / SigmaStar | 二进制文件/语言/固件/RTMP | **8080** | `SSConnectDevice.java:57`、`DownLoadFileUtils.java:31-32`、`FirmwareClientThread.java:64` |
| 海思 **+ SigmaStar 共用** | **App 侧** `ServerSocket`（相机回拨） | **5678** | 真实监听者是 `HisiSocketServer.java:19`（端口）、`:70`、`:76`（`accept`），由海思/SigmaStar **共用**的老预览页无条件启动（`HisiActionCameraPreviewActivity.java:307`，页面选择逻辑 `:265-266`）；同端口的 `MessageService.java:19,32` 那份实现是**清单幽灵**——全树无 `startService(MessageService/KeepAliveService)`，只在清单注册（`re/xtugo/components.tsv:194-195`） |

**码流开关 `/12` vs `/11`（CONFIRMED，别写成"全平台都可切"）**：`/11`（大码流）**只存在于老海思 `DV`** —— `DV.getVideoRtspURL()`（`DV.java:319-329`）与 `getVideoHttpURL()`（`:331-342`）按 `isPreviewBigBitRate()`（`:119`）在 `11`/`12` 间切换；**SigmaStar 与 Ambarella 永远只有 `/12`**（`SSCommandUtil.java:50`、`HaisiCommandUtil.java:50`、`AmbaPreviewActivity.java:972` 三处都是字面量，无分支）。RTSP 传输协议按**在用**的播放层 `VideoTextureView.java:639-641` 决定：非 Ambarella → `rtsp_transport=tcp` + `framedrop=5`，Ambarella → `udp` + `framedrop=100`（`:643-645`）；`SSVideoView.java:603-607` 那份 udp/prefer_tcp 分支属**死代码**（`SSVideoView` 全树只被自己的 layout `activity_s_s_video_remote_play.xml` 引用，无 Activity 使用）。

**默认 IP 常量位有 6 处，不是一处**：`CameraParameters.java:37` 的 `baseIp`、孪生且类名拼错的 `CameraParmeras.java:37` 的 `baseIp`、`SSConstant.java:13` 的 `SS_IP`（⚠️ **非 final**，存在被改写的可能）、`DV.java:66` 的 `defaultIP`、`FwUpload.java:16` 的 `HOST`、`IcatchCaseKt.java:9` 的 `API_HOST_ICATCH = "192.168.1.1"`（iCatch 记录仪专用）。**分工要说清**：SigmaStar/海思的 **CGI 面不硬编码** —— `DeviceHttpUtils.get()` 用探测到的 `NetworkDeviceUtils.getIpDevice()` 拼绝对 URL（`DeviceHttpUtils.java:14-15`）；硬编码 `192.168.0.1` 只出现在 **8080**（`SSConnectDevice.java:57`、`FirmwareClientThread.java:64`、`sigmastar/.../DownLoadFileUtils.java:31-32` 与其海思孪生 `:34-35`）与 **RTSP/缩略图/下载/播放 URL**（`SSCommandUtil.java:50`、`SSFileInfoBean.java:83,87,115,155,158,160` 用 `SSConstant.SS_IP`）⇒ 相机换 IP 时这几处会静默失效，自研必须把**探测到的 IP 统一注入 base URL**，不能沿用官方的两套来源。

**USB 不是运动相机通道**：`libusb_transport.so` 属 iCatchTek SDK，只被 `com/gku/dashcam/icatch/` 加载 → 只服务 Mini1 记录仪。

**BLE 命令表（`BLEConnectUtils.java`）**

| 命令 | 载荷 | 含义 | 行 |
|---|---|---|---|
| `R001` / `R001_<pin>` | | 发现/查询，老配对路径 | :886,:184 |
| `R002_<pin>` | | 打开/索取 SSID+PWD | :722,:755,:192 |
| `R003_<pin>` | | 配对 | :177,:268,:696,:880 |
| `R004` | | 取消配对 | :376 |
| `R006` | `ssid:<s>;pwd:<p>;` | 让相机加入指定热点 | :275-280 |
| `R007` | `live_type:%s;res:%d;fps:%d;bitrate:%d;rtmp_url:%s;` | 起 RTMP 直播 | :282-287 |
| `R008_<pin>` | | 停直播 | :295 |
| `R009` | 回 `R009_cap:0x<NN>`，**bit1(`&2`) = 支持直播** | 能力查询，超时 1000ms | :204-229,:783-819 |

**BLE 分包规范（可直接照抄的 spec）**：`CHUNK_SIZE=100`；保留前缀 `pin:<code>;`；包头 `%s%03d001%s%s` = `cmd + 总包数(3位) + 001 + "pin:<code>;" + md5(整个载荷)`；包体 `%s%03d%03d%s%s`（序号从 **002** 起）；一次 `writeCharacteristic` 一包，靠 `onCharacteristicWrite` 推进。通知回复格式 `key:value,key:value,…`，已确认键：`Status`、`Pin`、`KEY`、`SSID`、`PWD`、`WiFi_Status`（`Status=="1"` 通过）。PIN 持久化键 `"bt_pin"+btDeviceName`。

⚠️ **所谓「蓝牙心跳」不是蓝牙**：`MSG_ID_BLUETOOTH_HEARTBEAT = 268435492` 是 **Ambarella TCP** 报文，1000ms 一次，且只在 `type∈{H75N,CV75}` **且** `name endsWith "S7"` 且设置页在前台时发（`BTHeartbeatManage.java:16-21`、`SSDeviceSettingActivity.java:64-72`）。SigmaStar 心跳模型为 `null`。`MSG_ID_HEART_BEAT=268435467` 声明了但**零调用点**。

### 2.4 通讯方式

**(a) Ambarella（`type` = `H75N`/`CV75`）—— 裸 JSON over TCP**

- TCP `<ip>:7878`，`connect(…,5000)`，命令 `soTimeout=5000`，轮询未决数据时降到 **10ms**。
- **请求 = 原始 Gson JSON 字节，无分隔符、无长度前缀、无换行**（`outputStream.write(str.getBytes())`），例：`{"msg_id":268435463,"token":123,"workmode":"System","name":"Resolution","value":"4K30"}`。
- **响应分帧靠「现在能不能解析」**：每次读 64KiB 追加到 StringBuilder，`GsonUtils.isValidJson()` 通过才算一帧；再拿 `msg_id` 与在途请求比对，**不一致就当推送消息派发**（`onPushMsgReceive`）。
- 同一时刻只允许一条命令在途（`LinkedHashMap mapMsgQueue` + `synchronized`，重复命令自动 cancel）；超时重发 3 次。
- 会话：257 拿到的 `token` 在后续每条请求的 `token` 字段回显；258 stop session 后关 socket。
- 文件/缩略图字节走**另一条 TCP 8787**。
- `rval != 0` 即错误；已映射：`-444 LINK_REFUSE`→ev137、`-4 ERR_INVALID_TOKEN`→ev129、`-1 ERROR_UNKNOW`→ev136、`-34 NO_SUPPORT_PARAM`/`-33 NO_SD`/`-18 CARD_PROTECTED`/`-17 NO_SPACE`→ev135、`"Read timed out"`→ev128。

**请求/响应 bean 的 JSON 键**：`CmdRequestBean{msg_id,token}`、`CmdResponseBean{msg_id,rval}`；扩展键 `param`、`workmode`、`name`、`value`、`start`/`end`、`offset`/`fetch_size`、`size`/`md5sum`/`sent_size`、`type`(恒为 `"TCP"`)、`ssid`/`pwd`、`status`。

**msg_id 全表（运动相机侧最完整的一份）**

*0x00 通用*：1 GET_SETTING · 2 SET_SETTING · 3 GET_ALL · 4 FORMAT_SD(`param:"C:"`) · 5 GET_SPACE · 6 GET_NUM_FILES · 7 NOTIFICATION · 8 BURN_FW · 9 GET_OPTIONS · **11 GET_DEVICE_INFO** · 12 POWER_MANAGE · 13 BATTERY_LEVEL · 14 ZOOM · 15 ZOOM_INFO · 16 SET_BITRATE

*0x01 会话*：**257 START_SESSION** · 258 STOP_SESSION · 259 RESETVF(弃用) · 260 STOP_VF(弃用) · **261 SET_CLNT_INFO**(`type:"TCP"`,`param:<手机IP>`) · 1793 QUERY_SESSION_HOLDER

*0x02 录像*：**513 RECORD_START** · **514 RECORD_STOP** · 515 RECORD_TIME · 516 FORCE_SPLIT
*0x03 拍照*：**769 TAKE_PHOTO** · 770 STOP_PHOTO
*0x04 媒体*：**1025 GET_THUMB**(`param:<path>`,`type`→`{path,size}`，字节走 8787；**必须先发 261**) · 1026 GET_MEDIAINFO(`thumb_file`,`duration`,`resolution`,`size`,`date`,`media_type`) · 1027 SET_ATTRIBUTE
*0x05 文件系统*：1281 DEL · 1282 LS · 1283 CD · 1284 PWD · **1285 GET_FILE**(`param`,`offset`,`fetch_size`) · 1286 PUT_FILE(`param`,`size`,`md5sum`,`offset`，目标 `/tmp/SD0/<name>`) · 1287 CANCEL_XFER
*0x06 WiFi*：1537 RESTART · 1538 SET_WIFI(`ssid`,`pwd`) · **1539 GET_WIFI**(→`ssid`,`pwd`) · 1540/1541 STOP/START · 1542 GET_WIFI_STATUS

*0x100000xx 现代命令空间*：

| msg_id | 名称 | 请求 | 响应键 |
|---|---|---|---|
| 268435457 | GET_ALL_WORKMODE | – | `var photo`、`var video` |
| 268435458 | GET_CUR_WORKMODE | – | `var workmode` |
| 268435459 | SET_CUR_WORKMODE | `param:<mode>` | `param`("success") |
| **268435460** | **GET_PRIMARY_MENU_ITEM** | `workmode` | `var item`(CSV)、`var cur`(CSV) |
| **268435461** | **GET_SECOND_MENU_ITEM** | `workmode`,`name` | `var item`(CSV)、`var cur` |
| 268435462 | GET_CUR_PARAMETER | – | **声明了但从不发送**（用 461） |
| **268435463** | **SET_CUR_PARAMETER** | `workmode`,`name`,`value` | `param`,`value` |
| 268435464 | GET_FILETYPE_COUNT | – | – |
| 268435465 | GET_FILELIST_INFO | – | – |
| 268435466 | GET_SYSTEM_WORK_STATE | – | `var mode`,`state`,`event`,`pasttime` |
| 268435467 | HEART_BEAT | – | **零调用点** |
| 268435468–471 | GPS_STATE / APP_CONNECT_STATE / CAMERA_STATE / SET_CAMERA_STATE | – | 均无调用点 |
| 268435474 | SYNC_SYSTEM_TIME | `param:yyyyMMddHHmmss` | ev61 |
| 268435475 | GET_SD_STATE | – | `sdstatus`,`sdfreespace`,`sdtotalspace` |
| 268435476 | SD_FORMAT | – | 与 4 重复 |
| 268435477 | RESET_FACTORY | – | ev66 |
| 268435478/479 | GET/SET_WIFI_INFO | – | 无调用点 |
| 268435480 | GET_BATTERY_INFO | – | **`var capacity=`**（键名自带尾随 `=`）、`var charge`、`var ac` |
| 268435482 | GET_FILE_COUNT | – | `var count` |
| **268435483** | GET_FILE_LIST | `start`,`end` | `filelist`（逗号分隔路径） |
| 268435484 | GET_FILE_INFO | `param:<path>` | `param` |
| **268435485** | GET_FILELIST_INFO_IOS | `start`,`end` | `fileinfolist`，记录间 `,`、字段间 `;`，固定序 `fileName;createTime;timeLong;size` |
| 268435486 | DELETE_FILE | `param:<path>` | ev62 |
| 268435487 | DELETE_ALL_FILE | `param:<int fileType>` | ev63 |
| 268435488 | QS_ADD_TIME | `param:5` | `param` |
| 268435489 | GET_ROTATE | – | `var rotation` |
| 268435490 | SET_STREAM_DATA | `param:"ssid:…;pwd:…;live_type:…;res:…;fps:…;rtmp_url:…;"` | – |
| 268435492 | BLUETOOTH_HEARTBEAT | – | 见 2.3 |
| 268435505/506 | GET/SET_ACTIVATE | `status`（`"2"`试用 `"1"`已激活） | `status`,`version`,`number`,`macaddr` |
| 268435507/508 | GET / PAIR_AND_GET 蓝牙发射器 | – | `BluetoothDevOne`、`BluetoothDevTwo` |

⚠️ **纠正**：`amba_control_video_param`、`amba_setting_page_mode`、`amba_control_photo_param` 等是 **Android Intent extra 键 / 资源名，不是协议命令**。真正的 Ambarella workmode/参数名只有 `"System"` 加设备下发的项名。

⚠️ **`pasttime` 的单位：不要写成「半秒」**（App 侧**零换算**）：`SSResponseParse.java:490`、`:505`、`:525` 与 `AmbaCmdModel.java:570`（`optInt("var pasttime")`）都是原样塞进状态对象再显示。⇒ 正确表述是「**App 把 `pasttime` 原样当秒显示**，若固件按半秒递增上报就会差 2 倍」，真机对照列入 §6。

**(b) 海思 / SigmaStar —— HTTP CGI `/cgi-bin/hi3510/*.cgi`**

响应统一是 JS 风格 `var <key> = "<value>";\r\n`（解析器 `SSResponseParse.java:532-565`，`HEAD_VAR="var "`、`MID_EQUAL_MARK="=\"`、`TAIL_SEMICOLON="\";\r\n"`）；`getfilelistinfoios.cgi` 例外，返回 **JSON 数组**；成功哨兵 `"Success"`。

SigmaStar 命令（`SSCommandUtil.java`）：`getdeviceattr` · `getcurallinfo` · `getallworkmode`(→`var video`/`var photo`) · `getcurworkmode`/`setcurworkmode?-workmode=` · `getprimarymenuitem?-workmode=` · `getsecondmenuitem?-workmode=&-name=` · **`getcurparameter?-workmode=&-name=`(SS 独有)** · `setcurparameter?-workmode=&-name=&-value=` · `getfilelist?-start=&-end=&-type=Photo|Video|<str>` · `getfiletypecount`(→`imagecount`,`videocount`,`eventcount`) · `getfilelistinfoios?-start=&-end=` · `getfileinfo?-name=` · **`getdirname`(SS 独有)** · **`getcurrotation`(SS 独有)** · `deletefile?-name=` · `getbatterycapacity`(→`capacity`,`charge`,`ac`) · `getsdstate` · `getwifi`/`setwifi?&-wifissid=&-wifikey=` · `sdcommand?-format&-partition=1` · `reset` · `setsystime?-time=` · `record?-cmd=start|stop` · `photo?-type=&-cmd=start|stop` · `exitquickrec` · `getcamerastatus`/`setcamerastatus?-status=` · `record?-cmd=Storie|Quick Stories`（名字以 `S7PRO`/`S7PRO MAX` 结尾用 `Storie`，否则 `Quick Stories`）

老海思差异（`HaisiCommandUtil.java` + `hisilicon/dv/biz/Setting.java:29-551`）：用 `getsecondmenuitem` 代 `getcurparameter`；`getfilecount` 代 `getfiletypecount`；无 `getdirname`/`getcurrotation`；另有 `getvideoinfo/setvideoinfo?-resolution=&-fps=`、`getviewfield/setviewfield?-fov=`(**只 150 或 170**)、`getlooprecord/setlooprecord?-enable=0|1`、`getflip/setflip?-enable=`、`getburstinfo/setburstinfo?-time=<0..30>&-count=<0..30>`、`gettimelapseinfo`、`gettimerinfo`、`getrecordtimelapse`、`getbootaction/setbootaction?-action=idle|record|timelapse`、`getaudioencode`、`getvideonorm/setvideonorm?-videonorm=NTSC|PAL`、`setsystime?-time=%04d%02d%02d%02d%02d%02d`、`getspotmeter`、`gettimeosd`、`getledstate`、`getbuzzer`、`setwifista.cgi?&-ssid=&-key=`(STA 模式)/裸 `setwifista.cgi?`(回 AP)、`getautoshutdown?-time=`、`getscreenautosleep?-time=`、`getscreenbrightness?-brightness=`、`getpoweronuimode?-uimode=`、`wifisleep`、`getbitrate?-bitrate=`、`getwifichannel`、`getdevcapabilities`、`getworkmode/setworkmode?-workmode=<int>`、`getcapability?-workmode=&-type=`、`getparameter/setparameter?-workmode=&-type=&-value=`、`deleteallfiles.cgi?`、`upgrade.cgi`、`getactivateinfo.cgi`/`setactivateinfo.cgi?-status=%d&-version=%s`/`settrial.cgi`

动作命令表 `Command.java:9-31`（`cgiFilesParams[]` 下标）：0/1 record start/stop · 2 photo · 3 photoburst · 4/5 phototimelapse start/stop · 6/7 phototimer start/stop · 8/9 continuous start/stop · 10 exitquickrec · 11–20 `record2.cgi?&-type=common|loop|timelapse|recsnap|slow&-cmd=start|stop`。响应包在 `SvrFuncResult` JSON 里，`substring(15, lastIndexOf('"'))` 取值，魔数串 `"sd is not ready"` / `"sd is full"`。`IsNewAPP` 时 `getexecuteCommand` 重映射：`cmd<10` 原样，否则**偶数→1、奇数→0**。

**(c) 端口 8080 的二进制帧（海思/SigmaStar 的数据面）**

⚠️ **有两份同名 `RegisterDeviceToServer`，字段布局相同但用途不同**：活路径是 `com/gku/actioncam/hisilicon/dv/ui/data/model/RegisterDeviceToServer.java`（被 SigmaStar 与海思的预览/下载/固件三条链路 import：`SSConnectDevice.java:13`、`sigmastar/.../DownLoadFileUtils.java:9`、`FirmwareClientThread.java:9`）；`com/hisi/hisiFW/RegisterDeviceToServer.java` 只服务 `FwUpload` 的 8080 上传。

**上行 72 字节小端、无分隔符**（`RegisterDeviceToServer.java:12-23`；`toLH` `:44-46`）：`[0..3] int32 cmd` · `[4..7] int32 length` · `[8..71] 64 字节路径槽`（`StringtoLH()` 就是 `str.getBytes()`，**平台默认字符集**、Android 上即 UTF-8，`:48-50`）。固件变体 **200 字节**：同 8 字节头 + 64B 路径 + **偏移 72 处 128B MD5**（`:25-38`）。

- ⚠️ **`[4..7]` 语义在两代实现里不同**：老实现写的是 Java `String.length()`（**字符数**，`SSConnectDevice.java:65`、`sigmastar/.../DownLoadFileUtils.java:73`），新实现 `module_camera/hisi/SocketHisiFile.java:111-119` 写 **UTF-8 字节数**且用 `Math.min(bytes.length, 64)` 截断路径；老实现 `System.arraycopy` 不截断 ⇒ **路径 ≥65 字节直接 `ArrayIndexOutOfBoundsException`**。ASCII 路径下两者等价，复现按字节数写。固件路径下该字段是**文件字节数**（`FirmwareClientThread.java:78`）。
- **cmd 取值（CONFIRMED）**：`0` = 取文件（`SSConnectDevice.java:63`，如 `/tmp/FL0/language.json`）、`2` = 推固件（`FirmwareClientThread.java:76`）、`4` = `MSG_TYPE.RECV_RTMP`（枚举 `GET_FILE,GET_FAILED,RECV_FILE,RECV_FAILED,RECV_RTMP` 的序号，Ambarella 直播页用它把 RTMP 参数推给相机，`amba/ui/stream/TCPClient.java:63-66`，同一条 8080）。
- **200B 固件帧的握手与 MD5 坑**：MD5 由 `new BigInteger(1, digest).toString(16)` 生成（`FirmwareClientThread.java:183-197`）⇒ **前导 0 会被吃掉、长度可能不是 32**；发完 200B 后 App 等相机回一行**以文件名开头**的数据才开始 10240B 分块写文件（`:100-118`、`:121-171`）。
- **下行 = 72B 头 + 裸文件流**（无分帧、无 CRC）：4B cmd → 4B 长度 → 64B 路径回显 → 循环读裸流（`hisilicon/dv/ui/data/connect/DownLoadFileUtils.java:308-351`，`bytesToInt` 是小端 `:360-362`）。⚠️ 长度字段**必须按 uint32 LE 读**；老代码的负数修正 `if (i<0) { j = ((i>>>1)<<1); if (i%2!=0) j++; }`（`:326-330`，SigmaStar 孪生版 `:184-191`）**是 bug**——结果仍是负数，>2 GiB 时拿不到正确长度，只有 `SocketHisiFile.java:131` 的 `getInt() & 4294967295L` 读对了。
- ⚠️ **同一端口两种响应格式并存**：语言表请求（cmd=0）的响应**不按 72B 头读**，而是 `BufferedReader.readLine()` 循环拼文本再 `subStr("[", "]")` 截 JSON 数组（`SSConnectDevice.java:59`、`:70-97`、`:113`）。

⚠️ **`sigmastar/data/connect/ClientThread.java` 是空壳**（`run(){}`），SigmaStar 控制面**就是纯 HTTP CGI**，不存在「Socket 上的 JSON 协议」。

### 2.5 设置项

**结构结论（对我们最重要）：三个平台的设置项都无法从 APK 枚举，全部是能力协商出来的。**

| 平台 | 菜单发现 | 取值 | 写入 |
|---|---|---|---|
| Ambarella | `268435460 {workmode:"System"}` → `var item`/`var cur`(CSV)；逐项 `268435461 {workmode,name}` → 选项 CSV | 设备 | `268435463 {workmode,name,value}` |
| SigmaStar/海思 | `getprimarymenuitem?-workmode=System` → `item`,`cur`；`getsecondmenuitem`/`getcurparameter` → `item` CSV + `value` | 设备 | `setcurparameter?-workmode=&-name=&-value=` |
| 老海思 | `getdevcapabilities` + 按 `(workmode,type)` 的 `getcapability`/`getparameter` | 设备 | `setparameter?-workmode=&-type=&-value=` |

- **标签翻译**：Ambarella 从设备取 `/tmp/FL0/language.json`（经 8787 下载）再 `LanguageUtil.replaceLocaleString()`；SigmaStar 经 8080 帧取 language.json/xml，`type` 决定 json 还是 xml（`HomeActivity.java:1751-1755`）。
- **CSV 空值坑**：连续 `,,` 先替换成 `,-,` 再把 `-` 当空串（`AmbaDeviceSettingPresenter.java:229-240`）。⚠️ SigmaStar 侧也有同一套补齐，但**两个重载行为不同**：`SSResponseParse.parseGetPrimaryMenuItem` 的 `String` 版会按 `item`/`cur` 的段数差补 `,-` 再比对（`SSResponseParse.java:428-441`），`Response` 版**不补差额**、段数不等就整表返回 `null`（`:389-395`）⇒ 同一份固件在两条代码路径下可能一条出菜单、一条出空。
- **UI 类型推断**：选项 ≤2 且值为 `ON`/`OFF` → 开关，否则列表（`AmbaSettingUtil.java:24-40`）。

APK 里唯一硬编码的字面参数名：

| 项名 | workmode | 取值域 | 证据 |
|---|---|---|---|
| `System` | (workmode) | – | `AmbaDeviceSettingPresenter.java:30` |
| `Resolution` | 视频模式 | 设备给；**灰掉** `720P100,720P200,1080P100,1080P200,720P120,720P240,1080P120,1080P240` | `AmbaSettingUtil.java:13` 等 |
| `FLIP` | `System` | ON/OFF | `HomeActivity.java:765` |
| `Time Zone` | `System` | `"%+d"` 小时偏移 | `AmbaCmdModel.java:306-312` |
| `Self-Timer` | `Timing Photo` | 设备 | `AmbaWorkSettingPresenter.java:190` |
| `Time Lapse` | `Timelapse Photo`/`Timelapse Video` | 设备 | `:187` |
| `Number` | `Burst Photo` | 设备 | `:193` |
| `Duration` | `Quick Stories` | 设备 | `:196` |
| `ImageStabilize` | 各模式 | 设备 | `AmbaSettingUtil.java:14-16` |
| `EIS` / `Gyro EIS` / `LDC` | 各模式 | ON/OFF；老海思强制 `EIS=OFF`；SigmaStar 上 EIS 与 LDC 互斥；120p 隐藏 `EIS` | `HaisiSettingModel.java:130-131`、`SigmastartSettingModel.java:125-141` |
| `WiFi Frequency` | `System` | 仅海思特判 | `HaisiSettingModel.java:112` |
| `supportSwitchType`（**12 个真实协议项名**） | `System` | ON/OFF | `Inversion Mode, Date Stamp, Brand Stamp, Key Tone, Cap Tone, Power Tone, Remote Control, Face Detection, Grid, Auto Sync Time, Voice Control, Time Stamp`（`SSDeviceSettingActivity.java:52`） |
| workmode 名（字面量） | | `Normal Photo, Timing Photo, Burst Photo, Lapse Photo, Timelapse Photo, Raw Photo, Long Exposure, Normal Video, Timelapse Video, Slow Motion, Car Looping, Video and Photo, Quick Stories, Quick Video, Under Water, Time Stretch, Lapse Burst, Manual Recsnap, Loop Video, Night Photo, Night Timelapse Photo` | `SSExchangeWorkMode.java:4-20` |

**唯一一张「APK 里就写死」的设置表是老方言（海思 `dv`）的 PreferenceScreen**：`res/xml/preferences.xml` 定义骨架、`dv/biz/Common.java` 定义 `KEY_*`、`dv/biz/Setting.java` 定义读写命令，其选项只引到 **8 个 `@array/…`**（`video_mode`、`boot_action`、`screen_auto_sleep`、`video_resolution_*_ntsc` 共 4 对 entries/values）。`arrays.xml` 实测 **28 个 `<array>`**、**零个 `<string-array>`**，其中相机相关的 4 组是 `video_mode`(NTSC/PAL)、`boot_action`(`idle,record,recordlapse,recordloop,recordslow`)、`screen_auto_sleep`(`OFF,1min,3min,5min`→`0,1,3,5`)、`video_resolution` NTSC(`720P/30,720P/60,720P/240,1080P/30,1080P/60,1080P/120,4K2K/30`→`720P_30,…,4K2K_30`)；两份 PAL 变体（`video_resolution_entries_pal`/`values_pal`）**只有 `R` 常量、全树无使用点**。→ 期望中的分辨率/HDR/帧率大表**不在资源里，在设备上**。

⚠️ **`boot_action` 的数组与代码常量不自洽**：`arrays.xml:28-33` 的 values 是 `idle,record,recordlapse,recordloop,recordslow`，而 `Common.java:8-10` 只有 `BOOT_ACTION_IDLE="idle"`、`BOOT_ACTION_RECORD="record"`、`BOOT_ACTION_TIMELAPSE="timelapse"` 三个常量 ⇒ 后三个取值在 App 侧没有常量支撑、只能按字符串原样发。**照抄字面量，不要"顺手改正"成 `timelapse`。**

老海思能力矩阵 `getcapability.cgi?-workmode=W&-type=T`（`DV.java:148-212`；type：0 resolution,1 time,2 scene/interval,3 rate,4 photo-res,5 video-res,6 mode,7 loop type）：

| workmode | 请求的 type |
|---|---|
| 0 PHOTO_SINGLE | 0,2 |
| 1 PHOTO_TIMER | 0,1,2 |
| 2 PHOTO_RAW | 0 |
| 10 MULTI_BURST | 0,1（当前值不在列表时自动纠正为 `values[0]`） |
| 11 MULTI_TIMELAPSE | 0,2 |
| 12 MULTI_CONTINUOUS | 0,3 |
| 20 VIDEO_NORMAL | 0 |
| 21 VIDEO_LOOP | 0,7 |
| 22 VIDEO_TIMELAPSE | 0,1 |
| 23 VIDEO_PHOTO | `deviceAttr.name` 含 `"3559"` → 6,1；否则 → 5,4,2,3 |
| 24 VIDEO_SLOW | 0 |
| 25/26 | 声明了，无 capability 调用 |

### 2.6 回放与下载

| 平台 | 列表 | 元数据 | 缩略图 | 下载 | 续传 |
|---|---|---|---|---|---|
| Ambarella | `268435483{start,end}`→`filelist` CSV，或 `268435485`→`fileinfolist` | `268435484{param}`；`268435482`→`var count`；`1026` media-info | `1025{param,type}` → 7878 给 `{path,size}`，字节走 **8787**；**必须先发 261** | `1285{param,offset,fetch_size}` → 7878 给 `{path,size}`，流走 **8787** | **有，offset 式**：`offset = 已存在文件.length()`，`FileOutputStream(path, append)` |
| SigmaStar | `getfilelist?-start=&-end=&-type=Photo\|Video\|Event\|Normal` | `getfileinfo?-name=`→`path,size,create,time`；`getfiletypecount`；`getdirname` | `http://192.168.0.1/thumb<originPath>` | `http://192.168.0.1<originPath>`；低码流代理 `…-s.MP4` | **无 Range 续传**：<2 GiB 走纯 HTTP GET（`SSDownloadUtil.java:152-159`），**≥2 GiB 才切 8080 帧**（`SSDownloadUtil.java:85-88`，阈值常量 `IjkMediaMeta.java:60` = 2147483648，`.LRV` 例外）；8080 分支用 `new FileOutputStream(file, true)` **append 追加**（`sigmastar/…/DownLoadFileUtils.java:235`）——**不查已落盘字节、不发 offset** ⇒ 重复下载会追加重复内容 |
| 老海思 | `getfilelist?&-start=&-end=`（`;` 分隔，扩展名 `mp4,lrv,dng,png,jpg`）或 `getfilelistinfoios?&-start=&-end=`(JSON) | `getfileinfo.cgi?&-name=` | `http://<ip>/<path>` 且后缀换 **`.THM`** | `http://<ip>/<path>` 或 8080 帧 `cmd=0` | **无 Range**；8080 分支与 SigmaStar 是同一套「append 追加、不发 offset」实现（`hisilicon/dv/ui/data/connect/DownLoadFileUtils.java:378` 的 `new FileOutputStream(file, true)`）——旧写法「`new FileOutputStream(p)` 无 append」不成立 |

路径/后缀代数（`SSFileInfoBean.java:75-158`、`HiDefine.java:30-33`）：Ambarella 远端根 `/tmp/SD0/…`（`fileSize<1000` 时从 HTTP URL 剥掉，`getPlayPath()` 恒剥）；`FILE_SUFFIX_THM=".THM"`、`FILE_SUFIX_LRV=".LRV"`、`.MP4`、`.JPG`；MP4→`-s.MP4`(SS) 或 `.LRV`(Amba/老海思)；本地名 `<fileSize>_<fileName>`。分块：SS 10240、Amba 数据 65536、老海思 发 1024/收 1MiB。Amba 接收放弃条件：连续 30 次 100ms `available()==0`（≈3s）或 4 次读异常。

---

## 3. idGoLive（APK 包 `com.id221.golive`，Java 命名空间 `com.icatch.golive`）

> ⚠️ 包名易错点：`AndroidManifest.xml:7` 的 package 是 **`com.id221.golive`**（FileProvider authority 也跟着它），而所有 Java 类在 **`com.icatch.golive`** 命名空间下。两者不一致，别只记一个。

**这是三家里最特殊的一个：它的主相机通道不是自研协议，而是 PTP / PTP-over-IP（libgphoto2 的 `ptp.c` 派生 + iCatch 厂商扩展），整条协议栈在 `libcontrol.so` 里，Java 侧只有 JNI 壳。**
CONFIRMED：`libcontrol.so` 内含 `ptp_ptpip_connect/sendreq/senddata/getresp/getdata/close`、`ptp_ptpip_event_check/event_wait`、`ptp_opensession`、`ptp_getobject/objectinfo/thumb/handles`、`ptp_mtp_getobjectquickview`、`ptp_formatstore`、`ptp_deleteobject`、`ptp_chdk_*`（Canon CHDK 残留 ⇒ 确为 libgphoto2 血统），以及 iCatch 扩展 `icatch_get_dev_prop_desc/value`、`icatch_set_dev_prop_desc/value`。
Java 侧开关：`JCameraConfig.enablePTPIP/disablePTPIP/enableSocketIO/enablePtpReconnection/setConnectionCheckParam`。

含义：**「协议即插件」的抽象必须能容纳 PTP 这种「属性表 + 对象表 + 事件」模型**，不能只按「HTTP 命令 / TCP 命令」两类设计。

**品牌属性也不同**：TUWIN、XTU 是自有品牌 App，idGoLive 是 **iCatch 的白标/公版 App**，服务 id221.com / idalbum.com 渠道下的一堆 ODM 贴牌子（Victure、Crosstour、Niceboy…）。⚠️ 但「它完全没有型号表」**只成立一半**：**没有「机型 → 能力」注册表**（`CameraType`/`CameraAddType` 里没有任何品牌或芯片枚举），**却有一条 33 项的「机型名 → 禁用某功能」黑名单**（`CameraProperties.java:1476`，命中就不写 `0xD83E` 时区）。它选协议靠的是**四层判定**：① `MyCamera.connect(true)` 的 PTP 会话成败（`192.168.1.1` 的 PTP 机**不参与 ping**）、② ICMP 应答者（只打 `.254`/`.169.1`，`ConnectDeviceManager.java:266`）、③ 各 handler `getSSID()` 成败、④ 相机名黑名单。我们的按型号插件表仍是比它更好的设计，但要留「无型号、纯探测」的兜底路由，并且**得能表达「按设备名跳过个别命令」这一层**。

### 3.1 型号清单

**没有添加设备/型号选择界面。** `AddNewCamFragment` 只给四个连接入口按钮：配对 / WiFi 连相机 / USB 连相机 / WiFi 自动连接（`ui/Fragment/AddNewCamFragment.java:116-119`）。`CameraType = {UNDEFIND 0, USB 1, WIFI 2, OLD_WIFI 3}`，`CameraAddType = {DEFAULT 0, WIFI_AUTO 1, WIFI_MANUAL 2, USB 3, BT 4}`。`arrays.xml` 里**零个 `<string-array>`**，`assets/` 只有 `dexopt/` 和 4 个 HTML，`res/drawable*` 里没有按型号的产品图。

型号名散落在三处硬编码里：

**(a) SSID 子串 → 产品图标**（`ui/adapter/CameraSlotAdapter.java:120-163`，`cameraPhoto` 这个 byte[] 列被复用成型号标签）

| 字面量 | 匹配方式 | 图标 |
|---|---|---|
| `zeeyu` | startsWith | `icon_device_zeeho` |
| `DC1_`、`_DC1Pro_`、`_C1_PRO_` | contains | `icon_device_dc1` |
| `_C5Pro_` | contains | `icon_device` |
| `_C5_` | contains | `icon_device_c5` |
| `_C5S_` | contains | `icon_device_c5s` |
| `_BC1_` | contains | `icon_device_bc1` |
| `_BC2_`、`_V1_PRO_` | contains | `icon_device_bc2` |
| `_C6_` | contains | `icon_device_c6` |
| `_C3Plus_` | contains | `icon_device_c3p` |
| `_C2_` | contains | `icon_device_c2` |
| `_C3_`、`_C1_` | contains | `icon_device_c3` |
| 其它 | — | `icon_device_default` |

⇒ 可推出的型号族：**C1 / C1 Pro / C2 / C3 / C3 Plus / C5 / C5 Pro / C5S / C6 / DC1 / DC1 Pro / BC1 / BC2 / V1 Pro**（+ `zeeyu` 一个第三方品牌前缀）。注意下划线是 SSID 里的分隔符，所以这些串**只在 SSID 中段匹配得上**。

**(b) 精确型号标签等值比较**（`CameraSlotAdapter.java:103-116`）：`C5Pro`、`BC2`、`C6`、`C3Plus`。

**(c) SDK 上报的 `getCameraName()` 产品串** —— 最有价值的一张表，因为它是**命令兼容性黑名单**的键（`Arrays.asList(...)` 在 `SdkApi/CameraProperties.java:1476`，判定点 `:1478`，命中即 `setCameraDateTimeZone()` **直接 `return false`**、不写 `0xD83E`（`CAMERA_DATE_TIMEZONE`）；方法体 `:1472-1481`）：

```
A86, A87, GA200, GA300, GA320, GA400, GA420, SF430, SF530, CT9300, DV19,
AC9000, DiCam880, DiCam870, DiCam80C, X7.2-LMXX72, X9.2-LMXX92, DC790, K10,
Victure/AC920, Victure/AC940, Crosstour/CT9900, Niceboy, Model : YDOL3,
Action Cam, Action Cam - 078, Action Cam - 258, Action Cam - 278,
Action Cam - 317, Action Cam - 386, Action Cam - 458, Action Camera, V40
```

另有：水印特判 `contains("BC2") || contains("C6")`（`PreviewPresenter.java:1619`）、固件门限 `startsWith("BC2")||startsWith("C6")` 且 `endsWith("CN")`（`PreviewActivity.java:710`）、`contains("DC1Pro")`（`OptionSetting.java:492`）。

- 黑名单的**匹配语义**：`Arrays.asList(...).contains(cameraName)` 是 **List 全等**判定，**不是前缀也不是子串**（`CameraProperties.java:1478`），比的是 PTP `0xD831 CAMERA_NAME` 读回的原样字符串（`CameraFixedInfo.getCameraName()`）⇒ `Action Cam - 079` 这种不在表里的名字照样会写时区。复现时**同名黑名单要照抄**，并把「跳过写时区」当成一条固件兼容策略。⚠️ 名单实测 **33** 个字面量（逐个点过 `:1476` 的 `Arrays.asList` 实参），08 档案标称的「34 项」是误计。
- ⚠️ **白标证据链在这里**：`Victure/AC920`、`Victure/AC940`、`Crosstour/CT9900`、`Niceboy`、`Model : YDOL3` 这些**贴牌产品名直接写在本 APK 源码里** ⇒ 旧说法「贴牌名不在本 APK 内」作废；识别方式是**运行时读名比对**，不是编译期品牌分支。拼写是 **`Crosstour`**（不是资料里常见的 `Croslyour`）。

⚠️ `"old_camera"` 是写进 `cameraPhoto` 列的**遗留/非-iCatch 标记**（`CameraType.OLD_WIFI_CAMERA`），**不是型号名，别当型号映射**。

**(d) 真正的「型号分派」= 按可达 IP 选 handler**（`net/ConnectDeviceManager.java`）

| Handler | `getCurrentICName()` | 目标 IP | 选中位置 |
|---|---|---|---|
| iCatch SDK（`MyCamera`） | — | `192.168.1.1` | **最先尝试**，`MyCamera.java:151` |
| `LyIC` | `"LyIntegratedCircuit"` | `192.168.1.254` | `:49→54→89` |
| `QzIC` | `"QzIntegratedCircuit"` | `192.168.169.1` | `:63→68→136` |
| `LyCmdWithYzIC` | `"QzIntegratedCircuit"`（复制粘贴 bug） | `192.168.1.254` + `/app/*` | 定义了但 `initDevice` **从不调用** |

`Ly*` ≈ Novatek/Linking 风格 `?custom=1&cmd=NNNN` 家族（旁证：下载 URL 里出现 `/Novatek/`），`Qz` ≈ 第二家 IC，两者共用 `/app/getproductinfo` + `/app/getdeviceattr` JSON 面。

### 3.2 识别方式

- **不存在 SSID 白名单/前缀集/正则**（已按 `SSID_PREFIX`、对 SSID 变量的 `startsWith`/`contains`、形似 SSID 的字面量全量搜索确认缺失）。仅有的两个 pattern 是刻意宽松的 `PatternMatcher`（type 3 = `PATTERN_PREFIX`）用于 WifiSpecifier，**不是识别器**：`"[\u4E00-\u9FA5a-zA-z0-9@_-]+.*"`（任意中文/字母/数字/`@_-`）与 `"[a-zA-z0-9@]+.*"`。
- 设备的"身份"就是**手机当前连着的 SSID 原文**（`MWifiManager.getSsid()` 去引号后直接当显示名，兜底字面量 `"unknown"`）。
- **mDNS/NSD：完全没有**（无 `NsdManager`、无 `_http._tcp`、清单无 `<tech-list>`）。
- **蓝牙**：设备名判定是**精确等值单个全局名** `getName().equals("iCatchBT")`（`ICatchBluetoothAssist.java:130`）；SPP/RFCOMM UUID `00001101-0000-1000-8000-00805F9B34FB`；iCatch BLE 特征 `e44b82fb-f3a6-4c72-ab3f-bf94abfd9930`（读写同一个）；HM-10 兜底特征 `0000ffe1-…`；CCCD `00002902-…`。**不声明 service UUID**，靠遍历 `getServices()/getCharacteristics()` 字符串匹配。BLE 与 SPP 由全局 `AppInfo.isBLE` 切换。
- **MAC/OUI 规则：未找到。**
- **USB**：代码常量 `USBHost_Feature.java:28,32` 是 `USB_VENDOR_ID = 10925`（= **`0x2AAD`**，旧版此处写 `0x2A0D` 是错的）/ `USB_PRODUCT_ID = 25427`（`0x6353`），另可比 deviceClass/subclass/protocol。**清单侧唯一带字面 VID/PID 的「机型表」是 `res/xml/device_filter.xml`（只有 2 条 `<usb-device>`）**：`vendor-id="10925"`+`product-id="25859"`(`0x2AAD:0x6503`)、`vendor-id="16716"`+`product-id="25971"`(`0x414C:0x6573`)。⇒ **VID 一致、PID 与代码常量不一致**，白名单以 `device_filter.xml` 为准，`USBHost_Feature` 的两个常量是另一条匹配路径。
- **确认设备的探测链（CONFIRMED，顺序）**：并发两个 ICMP `ping -c 3 -w 3`（`ConnectDeviceManager.java:266`）打 `192.168.1.254` 与 `192.168.169.1`；ping 只是可达性闸门，**真正确认靠各 handler 的 `getSSID()`**：

| Handler | 确认请求 | 响应解析 |
|---|---|---|
| `LyIC` | `http://192.168.1.254/?custom=1&cmd=3029` | **XML**，取 tag `SSID`、`PASSPHRASE` |
| `QzIC` | `http://192.168.169.1/app/getdeviceattr` | **JSON** `info.ssid` |
| `LyCmdWithYzIC` | `http://192.168.1.254/app/getdeviceattr` | JSON `info.ssid`，空则回落 `LyIC` |

  另有第二条非 HTTP 探测：`LySocketClient().connectAndReceive("192.168.1.254", 3333)` —— 这是 App 自有代码里**唯一的端口字面量**。
- 型号串载体：SDK 路径 `CameraFixedInfo.getCameraName()`；HTTP 路径 `/app/getproductinfo`（具体 JSON key 未确认）；云端固件表字段 `product,name,region,version,download`。

### 3.3 连接方式

- **拓扑固定：相机 = AP，手机 = STA。没有 Wi-Fi Direct**（`WifiP2p` 零命中）、**没有扫码配网**、没有 NFC、活路径里没有 `WifiNetworkSuggestion`。
- ⚠️ 「无扫码」要说准：包里**有** zxing 与两套 QR 生成类（`com/icatch/golive/utils/QRCode.java:18-81`、`com/icatchtek/baseutil/qrcode/{QRCode,SetupQRCode}.java`），但 `QRCode` 只被用在**分享链接生成二维码**上（`PreviewPresenter.java:1918` 的 `shared_url_qrcode`），全树**没有任何 QR 解码入口**（`QRCodeReader`/`DecodeHintType` 在 `com/icatch*/` 下零命中），`SetupQRCode` 也无包外调用者 ⇒ 「无扫码**配网**」成立，而不是「无二维码」。
- **配网/连接入口 = WiFi 自动连接 / WiFi 手动 / USB / 蓝牙** 四条（`AddNewCamFragment`）；此外还有两条**不走 `AddNewCamFragment` 的旁路**：①组播 AES 配网 `simpleConfig`（§3.6），②**手机开热点让相机连**（`WifiApActivity` + `activity_wifi_ap.xml:24,50` 的默认 `test01` / `1234567890` 与 no/wpa/wpa2 三态）。⇒ 精确表述：**无 QR，有 BT 配网 + 组播配网 + OS 级直连 + 手机热点四条替代路径**。
- **路径 A（Android 10+，`wifi_auto_connect`）**：`WifiNetworkSpecifier` + `setSsidPattern(宽松 glob)` + `setWpa2Passphrase`，**用户只输密码、从不选 SSID**，密码框预填 `AppInfo.default_pwd = "1234567890"`（注意与 TUWIN/XTU 的 `12345678` 不同，是 10 位）。关键在 `onAvailable → cm.bindProcessToNetwork(network)`（`WifiNetworkSpecifierUtil.java:139`），因此 `ApiCaller` 必须把 OkHttp 走 `PanoramaApp.getContext().getWifiNetwork().getSocketFactory()`（`ApiCaller.java:148`）。错误码：`onUnavailable→onError(0)`、`onPasswordError→onError(-1)`（触发重填密码对话框）。
- **路径 B（老式 `WifiManager.addNetwork` + `enableNetwork`，`WifiCheck.java:216,218`）**：只被**蓝牙配对流程**使用；Android 10+ 上是 no-op ⇒ 整个 BT 配网路径实际只在 API 28–29 可用（与清单缺 `BLUETOOTH_CONNECT` 一致）。
- **蓝牙配网载荷字段（你要的 spec）**：`ICatchWifiInformation.getWifiSSID()` / `getWifiPassword()` / `getWifiEncType()`（int，选 WEP/WPA/无），取自 `AppInfo.iCatchBluetoothClient.getSystemControl().getWifiInformation()`；同通道还有 `getSystemControl().enableWifi()` 唤醒相机的 AP。连上后轮询 `wifiCheck.isWifiConnected(ssid)`，15 次超时。
- **端到端顺序**：选入口 → (可选 BT 前置：扫→验 `iCatchBT`→连→`enableWifi()`→拿 ssid/pwd/encType→`addNetwork`→轮询) → WiFi join（A 或 B）→ `CameraManager.createCamera(WIFI_CAMERA, ssid, ip, …)` → `MyCamera.connect()`：由当前 WifiInfo 反算本机地址 `WifiAPUtil.getLocalIPAddressFromWifiInfo()` 并构造 `ICatchINETTransport(ipAddress, localIpAddress)` → `prepareTransport()` → `commandSession.prepareSession(transport, enablePTPIP)` + `panoramaSession.prepareSession(transport)` → **SDK 失败则回落** `ConnectDeviceManager.initDevice()` 的 HTTP IC 路径 → 连上后按 `hasFuction(CAMERA_DATE_TIMEZONE/CAMERA_DATE)` 对时 → `startHeartbeatPackage()`（**周期按 handler 分档**：无 handler 兜底 30000 ms（`ConnectDeviceManager.java:205`）、`LyIC` 10000 ms（`LyIC.java:225`）、`QzIC`/`LyCmdWithYzIC` 4000 ms（`QzIC.java:239`、`LyCmdWithYzIC.java:188`））。
- **明文流量全局放行**：`network_security_config.xml` 里 `cleartextTrafficPermitted="true"`。

### 3.4 传输通道清单（**11 条并存**；旧版「8 条」把 USB 的三种传输并成了一条，且漏了 RTMP 上行与组播配网）

| # | 传输 | 寻址 | 证据 |
|---|---|---|---|
| T1 | **PTP/IP（原生 TCP）**——主控制+文件通道，cmd/data/event 三条 socket | `ICatchINETTransport(ip, localIp)`，App 硬编码 **`192.168.1.1`**（`AppInfo.java:51`，SP 可覆盖，`LaunchPresenter.java:620`）；Java 层不出现端口 | `ICatchINETTransport.java:8-29`、`ICatchCameraSession.java:91`（`prepareSession_Jni(sid, ip, "", "")`）、`LocalSession.java:97,106` |
| T2 | **PTP over USB —— UVC BULK** | `prepareSession_Jni(sid, **true**, -1, -1, -1)`；transport 由 `USBHost_Feature` 的 UsbDevice/UsbDeviceConnection 构造 | `ICatchCameraSession.java:95`、`MyCamera.java:135-146`、`ICatchUVCBulkTransport.java:12` |
| T3 | **PTP over USB —— UVC ISO** | `prepareSession_Jni(sid, **false**, vid, pid, fd)` → `initUvc_Jni(vid, pid, "", fd)` | `ICatchCameraSession.java:100`、`ICatchUVCIsoTransport.java:53-54` |
| T4 | **PTP over USB —— MSC/SCSI（Bulk-Only + SCSI）** | 同一个 `prepareSession_Jni(sid, true, -1, -1, -1)`；`USB_TRANSPORT_ERROR_TRY_AGAIN = -200` → `IchTryAgainException` | `ICatchCameraSession.java:107`、`ICatchUsbScsiTransport.java:14-15,54` |
| T5 | **HTTP，Novatek 风格命令集**（`LyIC`） | `http://192.168.1.254/?custom=1&cmd=NNNN&par=N&str=…`；心跳 `cmd=8888` 每 **10000 ms** | `RequestInfo.java:8-66`、`LyIC.java:225` |
| T6 | **HTTP，`/app/*` + `/api/*` JSON 记录仪集**（`QzIC`） | `http://192.168.169.1/app/*`、`http://192.168.1.254/app/*`；心跳 `/app/getparamvalue?param=rec` 每 **4000 ms**；列表分页首屏 `0..99`、**步长 101** | `M1RequestInfo.java:23`、`LyWyzRequestInfo.java:25`、`QzIC.java:239`、`QzIC.java:25-26`、`QzIC.java:284-286` |
| T7 | **裸 TCP + XML（相机主动推，单向）** | `connect("192.168.1.254", 3333)`，连接超时 **5000 ms**、读缓冲 **1024 B**；**只在 `LyIC` 存在**，随 T5 建立 | `net/LySocketClient.java:123-128`；host/port 字面量 `net/LyIC.java:289` |
| T8 | **UDP 组播**：发现（Java 侧只收）**+ `simpleConfig` 配网（native 侧收发，AES-128-CBC）** | 组 **`234.168.168.168`**、端口 **`5002`**、256B 数据报、`soTimeout=200`、轮询 sleep **50 ms**、配网总超时 **180 s** | `CoreMulticast.java:15-16,25,30,54-60`、`ICatchCameraAssistImpl.java:41,74,223` |
| T9 | **BLE GATT + JSON / SPP 文本帧** | Android GATT 或 RFCOMM，NUL 结尾；二者由全局 `AppInfo.isBLE` 二选一 | `bluetooth/core/ICatchCoreBluetoothCommand.java:14-38` |
| T10 | **RTSP**（仅记录仪/副机/远端回放） | `rtsp://192.168.1.254`、`rtsp://192.168.169.1`、`rtsp://192.168.1.1<filePath>`；IC 只回**裸 host** | `ConnectDeviceManager.java:173,199`、`QzIC.java:165`、`LyIC.java:168,220`、`RemoteDeviceFilesFragmentPresenter.java:308` |
| T11 | **RTMP 推流（直播上行）** | Google Live API 返回的 ingestion 地址 `rtmp://a.rtmp.youtube.com/…`；泛化推流在 native（`JStreamPublish`） | `Function/live/google/CreateBroadcast.java:69,73`、`ICatchStreamPublish.java:12` |

⚠️ **第 12 条「存在但未启用」**：`enableSocketIO()`/`disableSocketIO()`（`ICatchCameraConfigImpl.java:49-66`）在厂商包内**零调用点**（全树只有接口声明与 JNI 包装）⇒ iCatch 的另一套 socketIO 私有二进制通道只是 SDK 能力，官方 App 没接。
⚠️ **`.254` 这一个 IP 上并存两套 profile**：T5 的 `?custom=1&cmd=`（XML）与 T6 的 `/app/` JSON（由 `LyWyzRequestInfo` + `LyCmdWithYzIC` 实现），但 `initDevice()` **从不调用** `try2connectLywyzIC` ⇒ 官方只会选中 Novatek 那套。复现时两套都实现，按 `getdeviceattr` 是否回 JSON 分流。

**分帧方式各不同**（再次证明分帧必须可插拔）：

- T7 XML：分隔符 **`</Function>`**，1024 字节读进 StringBuilder 累积，命中即整段派发（`LySocketClient.java:48-61`）；回包按 tag 取 `Cmd`/`Status`/`Value`/`SSID`/`PASSPHRASE`。
- T1 PTP：属性载荷是 **JSON 对象**，例：属性 `58884` 解成 `{cardStatus,years,months,days,hours,minutes,seconds}`（`ICatchCamVideoRecordStatus.java:18-26`）。
- 流参数是 **key=value 命令行式串**：`"H264?W=<w>&H=<h>&FPS=<fps>&BR=<br>"`（`ICatchH264StreamParam.java:66-71`）、`"MJPG?W=&H=&Q=&BR="`（`ICatchCustomerMJPGStreamParam.java:60-65`）。⚠️ 这串**字面量不在 APK 里**（20 个 arm64 `.so` 全量搜 `H264?` 零命中），是**固件返回**、App 只解析：`StreamInfoConvert.java:8-34`（无 `FPS` 字段时兜底 **fps=30**），且 `ConvertTools.resolutionConvert`（`:80-95`）会**按高度改写帧率**：`H=720`→`FPS=15`、`H=1080`→`FPS=10`。不复刻这两处改写，我方预览帧率会偏高。
- 文件对象 JSON 键：`fileHandle,filePath,fileName,fileDate,fileType,fileSize,fileWidth,fileHeight,frameRate,fileProtection,fileDuration`（`ICatchFile.java:107-121`）。
- 视频格式 JSON 键：`streamNo,streamType,mineType`（**协议里就拼错了**）`,codec,videoW,videoH,bitrate,frameRate,durationUs,maxInputSize`（`ICatchVideoFormat.java:37-48`）。
- T5/T6 HTTP：**无鉴权**，query 里 `custom=1&cmd=<int>&par=<int|list>&str=<string>`，支持批量并行列表 `?custom=1&cmd=2001,8010,3001&str=0,0,10`（`LyWyzRequestInfo.java:6`）。

### 3.5 命令目录

**(a) HTTP `?custom=1&cmd=`（主机 `192.168.1.254`，`net/RequestInfo.java`）**

| cmd | 含义 | 参数 | 行 |
|---|---|---|---|
| 1001 | 拍照 | – | :8 |
| 1002 | 照片尺寸 | `par` 0=12M,1=10M,2=8M,3=5M | :9-12 |
| 2001 | 录像开始/停止 | `par=1` 开始 / `0` 停止 | :47-48 |
| 2002 | 视频尺寸 | `par=<自定义>` | :46 |
| 2003 | 循环录像时长 | `par=<自定义>` | :14 |
| 2004 | WDR | `par=1/0` | :65-66 |
| 2008 | 时间水印 | `par=1/0` | :62-63 |
| 2011 | G-sensor 灵敏度 | 0=off,1=low,2=med,3=high,4=max | :19-23 |
| 2015 | **实时预览开关** | `par=1` 开 / `0` 关 | :44-45 |
| 2016 | 查询已录时长 | – | :49 |
| 2017 / 2018 | 触发 RAW→JPEG / 取编码结果 | – | :64,:31 |
| 3001 | **模式切换** | 0=photo,1=movie,2=playback | :41-43 |
| 3003 / 3004 | 设 SSID / 密码 | `str=` | :59,:58 |
| 3005 / 3006 | 设日期 `yyyyMMdd` / 时间 `HHmmss` | `str=` | :57,:60 |
| 3010 | **格式化 SD** | `par=1` | :18 |
| 3011 | 系统复位 | – | :61 |
| 3012 | 取固件版本 | – | :33 |
| 3014 | 查当前状态 | – | :50 |
| 3015 | **文件列表** | 可加 `&str=REC` | :17 |
| 3016 / 8888 | 心跳 | – | :36,:35 |
| 3017 | 剩余空间 | – | :27 |
| 3018 | 重启 WiFi/重连 | – | :53 |
| 3021 | **保存/提交菜单设置** | 载荷未找到 | :56 |
| 3024 | SD 卡状态 | – | :25 |
| 3029 | 取 SSID | – | :32 |
| 3030 | 录像尺寸容量 | – | :30 |
| 3031 | **查询菜单树** | `str=all` | :51 |
| 3044 | 镜头旋转 | `par=1/0` | :37-38 |
| 4002 | **缩略图**（拼在文件 URL 后） | `?custom=1&cmd=4002` | `entity/FileInfo.java:55` |
| 4003 / 4004 | 删单文件 `str=<path>` / 全删 | – | :16,:15 |
| 8010 / 8011 / 8012 / 8013 | 麦克风路由 / 录音开关 / 耳机状态 / 电量 | – | :39-40,:54-55,:28,:24 |

响应码：`codeSuccess=200`、`codeFailure=404`、`codeGetSSID=20001`（`:5-7`）。
⚠️ **`cmd=3031&str=all` 的菜单树 App 并不全收**：返回的是 `<Item><Cmd/><Index/><Id/></Item>` 三元组，而 `XmlParseUtil.java:35-53` **只认 `Cmd ∈ {2003, 2011, 8010, 8011}`**（分别进 map / 三个 list），其余条目**整条丢弃** ⇒ 新固件加设置项不会自动出现在官方 UI，自研也不能假设「设备报了就能用」。设置改完是**累积后一条 `cmd=3021`（`RequestInfo.java:56` 的 `urlSaveMenuSettings`，调用点 `LdeviceSettingActivity.java:638`、`:1010`）统一提交**，不是逐项写。
`192.168.169.1` + `/api/` 变体另有：`api/setdeviceinfo/?custom=1&cmd=1100&par=0`（拍照）、`cmd=1008&par=0|1`（语言 zh|en）、`cmd=3033&par=1`、`cmd=4016&par=1`（起保活）、`api/getdeviceinfo/?custom=1&cmd=2002|2004|3031`（`M1RequestInfo.java`）。

**(b) `/app/*` + `/api/*` 记录仪 JSON 集**

`getfilelist?folder=loop|event|emr&start=N&end=M` · `getparamvalue?param=all|rec|rec_resolution` · `getparamitems?param=all|rec_resolution` · `setparamvalue?param=<k>&value=<v>` · `setparamvalue?param=switchcam&value=0|1`（前后镜头） · `getdeviceattr`（SSID+密码） · `setwifi` / `wifireboot` / `reset` / `sdformat` · `playback?param=enter|exit` · `setting?param=enter|exit` · `enterrecorder` / `exitrecorder` · `getthumbnail?file=<path>` · `getmediainfo` · `getsdinfo` · `getrecduration` · `capability` · `getproductinfo` · `snapshot` · `deletefile?file=<p>` · `setsystime?date=`

响应形态：`{result:0, info:[{folder, files:[{name,duration,size,createtime,createtimestr,type}]}]}`、`{result:0, info:{items:[…], index:[…], value:…}}`。
⚠️ 静态只出现 3 个参数名（`rec`、`rec_resolution`、`switchcam`），**完整键表必须运行时用 `getparamitems?param=all` 发现**。

**(c) BLE GATT JSON 命令（`ICatchCoreBluetoothCommand.java:14-38`）**

| 常量 | 载荷 |
|---|---|
| `BT_CMD_BT_REQ_INFORM` | `{"mode":"bt","action":"info"}` |
| `BT_CMD_BT_REQ_INFORM_NAME_PWD` | `{"mode":"bt","action":"info","name":"","pwd":""}` |
| `BT_CMD_BT_SET_INFORM` | `{"mode":"bt","action":"set"}` |
| `BT_CMD_WIFI_REQ_ENABLE` | `{"mode":"wifi","action":"enable","type":"ap"}` |
| `BT_CMD_WIFI_REQ_DISABLE` | `{"mode":"wifi","action":"disable"}` |
| `BT_CMD_WIFI_REQ_INFORM` | `{"mode":"wifi","action":"info"}` |
| `BT_CMD_WIFI_REQ_INFORM_ESSID_PWD_IP` | `{"mode":"wifi","action":"info","essid":"","pwd":"","ipaddr":""}` |
| `BT_CMD_WIFI_SET_INFORM` | `{"mode":"wifi","action":"set"}` |
| `BT_CMD_EVENT_KEY_*` | `{"mode":"event","action":"key","type":"<t>"}`，t ∈ `s1`(半按),`s2`(拍摄),`del`,`down`,`up`,`left`,`right`,`menu`,`mode`,`set`,`tele`,`wide` |
| `BT_CMD_SYSTEM_POWER_DOWN/_HIBER` | `{"mode":"system","action":"power","type":"down"\|"hiber"}` |
| 回复 | **key=value**：`err=0` 无错 / `err=1` |
| 分帧 | `BT_CMD_TAIL = '\0'`，NUL 结尾 |

**(d) PTP 设备属性 ID —— idGoLive 的「设置项」真身**

⚠️ **属性空间不是一段，而是「四段 + 高位能力码」共 57 个码点**：`0x50xx`（标准 PTP DPC，10 个）、**`0xD6xx`（能力/查询段，6 个，此前被整段漏掉）**、`0xD7xx`（28 个）、`0xD8xx`（10 个）、高位 `0xE604`/`0xEE00`/`0xFFFF`（3 个）。下表十进制列全部取自 `data/PropertyId/PropertyId.java`（`:4-55`）与 `control/customer/type/ICatchCamProperty.java`（`:5-23`），**十六进制按十进制重算**（旧版这里有一批错位，如把 55072 写成 `0xD700`、把 55358 写成 `0xD81E`）。

| 属性 | 十进制(十六进制) | 显示名 | 取值 |
|---|---|---|---|
| `SCREEN_SAVER` | 55072 (`0xD720`) | Screen saver | 设备下发 |
| `AUTO_POWER_OFF` | 55073 (`0xD721`) | Auto power off | 同上 |
| `POWER_ON_AUTO_RECORD` | 55074 (`0xD722`) | 未解析 | – |
| `EXPOSURE_COMPENSATION` | 55075 (`0xD723`) | Exposure compensation | ⚠️ 位标志见 (e) 末：bit31 符号、**bit30 = 「除以 10」**、数值 = `i & 0x00FFFFFF` |
| `IMAGE_STABILIZATION` | 55076 (`0xD724`) | 未解析 | – |
| `VIDEO_FILE_LENGTH` | 55077 (`0xD725`) | Video file length | – |
| `FAST_MOTION_MOVIE` | 55078 (`0xD726`) | Fast motion movie | – |
| `WIND_NOISE_REDUCTION` | 55079 (`0xD727`) | 未解析 | – |
| `MULTIPLE_CAMERA_SENSOR` | 55081 (`0xD729`) | – | – |
| `CAPTURE_IN_VIDEO_RECORD` | 55082 (`0xD72A`) | – | – |
| `GET_NUMBER_OF_SENSORS` | 55083 (`0xD72B`) | – | 读 |
| `DEFALUT_TO_PREVIEW`(拼写如此) | 55084 (`0xD72C`) | – | – |
| `CAMERA_SWITCH` | 55091 (`0xD733`) | 前/后镜头 | 0=front,1=back |
| `CAMERA_CONNECT_CHANGE` | 55201 (`0xD7A1`) | – | 只写（通知固件连接状态变化） |
| `UP_SIDE` | 55216 (`0xD7B0`) | Flip image 180° | 0=off,1=on |
| `RESET_DEVICE` | 55217 (`0xD7B1`) | Reset Device | 写 `0` 触发 |
| `SD_SPACE` / `SD_SPACE_REMAIN` | 55218 / 55219 (`0xD7B2`/`0xD7B3`) | 只读 | – |
| `MICROPHONE_RECEPTION` | 55220 (`0xD7B4`) | "Mic Recoding"(拼写如此) | 0=headphone,1=unit |
| `BLUETOOTH_AUDIO_RECORDING` | 55221 (`0xD7B5`) | – | 0=off,1=on |
| `CAMERA_IMAGE_SWITCHING` | 55222 (`0xD7B6`) | – | 0=off,1=on |
| `VIDEO_WARNING_QUERY` | 55224 (`0xD7B8`) | Emergency videos | – |
| `G_SENSOR` | 55228 (`0xD7BC`) | Crash Sensitivity | **写入值 = 列表下标 + 1**；固件升级前也会读它 |
| `CAPTURE_DELAY_MODE` | 55280 (`0xD7F0`) | – | – |
| **`0xD7FB` 重载** | 55291 | `AP_MODE_TO_STA_MODE` = `NOTIFY_FW_TO_SHARE_MODE` = `TIMELAPSE_VIDEO_SIZE_LIST_MASK` **三合一** | `setPropertyValue(55291, i)`；⚠️ 旧文档写成 `0xD7DB`(=55259)，那个值**全仓不存在** |
| `VIDEO_SIZE_FLOW` / `VIDEO_RECORDING_TIME` / `SUPPORT_PREVIEW` | 55292 / 55293 / 55295 (`0xD7FC`/`0xD7FD`/`0xD7FF`) | 读 | 预览能力门 = `0xD7FF` |
| `CAMERA_NAME` | 55345 (`0xD831`) | 字符串，读写 | **贴牌识别就靠它**：读回的名字拿去比 (a) 里的 33 项黑名单 |
| `CAMERA_PASSWORD_NEW` | 55346 (`0xD832`) | 字符串 | – |
| `ESSID_NAME`/`STA_MODE_SSID`、`ESSID_PASSWORD`/`STA_MODE_PASSWORD` | 55348 / 55349 (`0xD834`/`0xD835`) | 字符串 | ⚠️ **各挂两个常量名**，同一码点 |
| `SERVICE_ESSID` / `SERVICE_PASSWORD` | 55350 / 55351 (`0xD836`/`0xD837`) | AP 模式 WiFi | – |
| `CAMERA_ESSID` / `CAMERA_PASSWORD` | 55356 / 55357 (`0xD83C`/`0xD83D`) | 字符串 | – |
| `CAMERA_DATE_TIMEZONE` | 55358 (`0xD83E`) | 时区 | 串 = `SimpleDateFormat("Z")`，如 `+0800`；⚠️ **33 项相机名黑名单命中就不写这一条** |
| `CAMERA_PB_LIMIT_NUMBER` | 55359 (`0xD83F`) | Limit of Auto download Size | – |
| `CAMERA_DATE` | 20497 (`0x5011`) | 对时 | 串格式 `yyyyMMdd'N'HHmmss".0"`，例 `20230415N133022.0` |
| `TIMELAPSE_MODE` | 60928 (`0xEE00`) | – | 0=still,1=video |

**标准 PTP DPC 也在用**：`BATTERY_LEVEL=0x5001`(20481)、`IMAGE_SIZE=0x5003`(20483)、`WHITE_BALANCE=0x5005`(20485)、`CAMERA_DATE=0x5011`(20497)、`CAPTURE_DELAY=0x5012`(20498)、`DIGITAL_ZOOM=0x5016`(20502)、`TIMELAPSE_STILL=0x501B`(20507)、`BURST_NUMBER=0x5018`(20504)、`PRODUCT_NAME=0x501E`(20510)、`FW_VERSION=0x501F`(20511)。
**`0xD6xx` 能力段（旧文档误记成 `0xD5Exx`，实际不存在）**：`VIDEO_SIZE=0xD605`(54789)、`LIGHT_FREQUENCY=0xD606`(54790)、`DATE_STAMP=0xD607`(54791)、`TIMELAPSE_VIDEO=0xD611`(54801)、`UPSIDE_DOWN=0xD614`(54804)、`SLOW_MOTION=0xD615`(54805)。另有 `MOVIE_REC=0xE604`(58884)、`UNDEFINED=0xFFFF`(65535)。

**通用逃生门（插件层必须暴露）**：`getProperty(int,byte[])` / `setProperty(int,byte[],int)` / `getPropertyValue(int[,int])` / `setPropertyValue(int,int[,int])` / `getStringPropertyValue` / `setStringPropertyValue` / `getByteArrayPropertyValue` / `setByteArrayPropertyValue` —— 即**任意 16 位 PTP DPC + 子属性索引**，类型分 int / string / bytes（`ICatchCameraProperty.java:19-41,61,97,107-121`）。

**(e) 取值域（索引 → 协议值 → 显示）**

| 设置 | 取值域 |
|---|---|
| 白平衡 `0x5005` | 1=Auto,2=Daylight,3=Cloudy,4=Fluorescent,5=Incandescent,65471=undef |
| 抗频闪 `0xD606` | 0=50Hz,1=60Hz,2=Auto,255=undef |
| 日期水印 `0xD607` | 1=off,2=日期,3=日期+时间,255=undef |
| 连拍 `0x5018` | 0=HS(持续),1=off,2=3,3=5,4=10,5=7,6=15,7=30,65535=undef；App 侧 `burst_values` 只给 `1,3,5,10` |
| 视频尺寸 `0xD605` | 码值表：1=720p30,2=720p60,3=1080p30,4=1080p60,5=720p120,6=1440p30,7=960p60,8=VGA120,9=QVGA240,10=FULL30,16=640×360@240,17=4K15,18=4K30,19/20=2K7@25,21/22=1280p30/60,25=640p15,26=HD15,255=undef；**但 `getSupportedVideoSizes()` 返回 String** |
| 曝光补偿 `0xD723` | ⚠️ **位打包**：bit31(`Integer.MIN_VALUE`) = 符号（置位则显示 `EV -`）、**bit30(`0x40000000`) = 「数值要除以 10」标志**、有效数值 = `i & 0x00FFFFFF`（`ConvertTools.java:56-61`）。漏掉这条位标志 ⇒ EV 直接差 10 倍 |
| 照片尺寸 `0x5003` | 0=VGA,2=2M,3=3M,5=5M,6=6M,8=8M,10=10M,255=undef；同样有 String 版 |
| 相机模式 | 1=VIDEO_OFF,2=SHARED,3=CAMERA,4=IDLE,7=TIMELAPSE_STILL,8=TIMELAPSE_VIDEO,9/10=对应 OFF,17=VIDEO_ON,42=VIDEO,43=TIMELAPSE,65471=undef |
| 预览模式 | 1=still,2=video,3=timelapse-still,4=timelapse-video |
| WiFi 模式 | 1=AP,2=Station,3=Ethernet,65535=undef |
| 文件过滤 | 17=video,18=image,19=media,255=all,33/34/35=紧急录像/照片/媒体；排序 1=升,2=降；sensor 0=all,1=front,2=back |
| 延迟拍摄间隔 | **App 域 = 字面分钟** 2,5,10,15,20,30,60,65535=不限,0=off；**SDK 域 = 枚举** 1=off,2=1S,3=5S,4=10S,5=20S,6=30S,7=1M,8=5M,9=10M,10=30M,11=1HR —— ⚠️ 两套域不兼容 |
| 快门延迟 `0x5012` | **毫秒**：0,2000,10000,65535 |
| 能力位 `checkCameraCapabilities(int)` | `1`=APP_DEFAULT_TO_PLAYBACK, `2`=NEW_PAGINATION_GET_FILE, `4`=TIMELAPSE_USING_MS_VALUE |

**(f) 事件（设备→App，`ICatchCamEventID.java:5-47`）**
`FILE_ADDED=1`、`FILE_REMOVED=2`、`FILE_INFO_CHANGED=3`、`SDCARD_FULL=17`、`SDCARD_ERROR=18`、`SDCARD_REMOVED=19`、`SDCARD_IN=20`、`SDCARD_INFO_CHANGED=21`、`VIDEO_ON=33`、`VIDEO_OFF=34`、`CAPTURE_COMPLETE=35`、`BATTERY_LEVEL_CHANGED=36`、`DEVICE_INFO_CHANGED=49`、`WB/CAPTURE_DELAY/IMAGE_SIZE/VIDEO_SIZE/LIGHT_FREQ/BURST _PROP_CHANGED=50..55`、`CONNECTION_DISCONNECTED/INIT_SUCCEED/INIT_FAILED=74,75,76`、`TIMELAPSE_STOP=81`、`CAPTURE_START=82`、`DEVICE_SCAN_ADD=85`、`FW_UPDATE_CHECK/COMPLETED/POWEROFF/CHKSUMERR/NG=96..100`、`VIDREC_TIME_CHANGE=101`、`FILE_DOWNLOAD=103`、`VIDEO_THUMB_READY/DONE=104,105`、`VIDEO_TRIM_DONE=106`、`SD_CARD_OUT/IN/ERR/LOCKED/MEMORY_FULL=128..132`、`INSUFFICIENT_DISK_SPACE=133`、`SD_CARD_SPEED_TOO_SLOW=134`、`UNDEFINED=255`。
事件对象携带 `sessionID,eventID,intValue1..3,doubleValue1..3,stringValue1..3,fileValue1`。

### 3.6 鉴权、心跳与固件升级

- **发现**：`CommandSession.startDeviceScan()` → 原生扫描 + UDP 组播 `234.168.168.168:5002`；命中通过事件 `DEVICE_SCAN_ADD=85` 上报。⚠️ **组播有两套实现**：Java 侧 `CoreMulticast`（`prepare()/receive()`，**只收**，用于发现）与 native 侧 `simpleconfig`（**收+发**，是配网协议，见下条）。⚠️ 清单里声明的是拼错的 `CHANGE_WIFI_MULTICAST_STAT`（少尾字母 `E`，`AndroidManifest.xml:39`），真正的 `CHANGE_WIFI_MULTICAST_STATE` **从未声明**，而 SDK 会 `createMulticastLock("multicast.test").acquire()`（`CoreMulticast.java:21-22`）⇒ 该锁是否真生效**静态不可证**（Android 的 `MulticastLock` 一般要求 `CHANGE_WIFI_MULTICAST_STATE`），组播发现在真机上可能压根不通，见 §6。
- **IC 判定的失败计数是共享的**：两个独立 ping 共用一个 `failure_connect_count`，累到 2 就发 `404`（`ConnectDeviceManager.java:28,57,71`）——一个 bug，别照抄。成功判据是 HTTP JSON `code==200` 且 `data.area_support` 存在。
- **配网密钥硬编码（组播 `simpleConfig` 通道）**：`simpleConfig(ctx, essid, pwd, ip, gw)` 默认 `timeoutSec=180`（`ICatchCameraAssistImpl.java:222-223`），不传 key 的重载直接套 `__default_key`（`:227-228`）；key 是 16 字节有符号字面量 `{33,126,26,22,40,-34,-46,-89,-85,-25,-123,-120,9,-54,128,60}` = `21 7E 1A 16 28 DE CE A7 AB E7 85 88 09 CA 80 3C`（`:41`），收包轮询 sleep **50 ms**（`:74`）。⇒ 这是一条**独立于连接流程的配网通道**（AES-CBC-128 在 native）。
- **鉴权**：HTTP 通道**完全没有**（无 Authorization/Cookie/token/nonce，按缺失确证，`ApiCaller.java:95-137`）——AP 网段内任意设备可被完全控制。PTP 会话 `prepareSession(sessionID, ip, "", "")` 传两个空串，即 SDK 留了凭据槽但未用（`ICatchCameraSession.java:91`）。
- **心跳**：HTTP `cmd=8888`、`cmd=3016`、`/app/getparamvalue?param=rec`、`api/setdeviceinfo/?custom=1&cmd=4016&par=1`；周期 = `ic.getHeartbeatPeriod()` —— **`LyIC` 10000 ms（`LyIC.java:225`）/ `QzIC` 与 `LyCmdWithYzIC` 4000 ms（`QzIC.java:239`、`LyCmdWithYzIC.java:188`）/ 无 handler 兜底 30000 ms（`ConnectDeviceManager.java:205`）。链路健康另由 `setConnectionCheckParam(connectionCheckTimes=4, connectionCheckInterval=4, 0)` 驱动（`AppInfo.java:57-58`、`CommandSession.java:120`）；⚠️ **第三个实参在 SDK 里被丢弃**——`setConnectionCheckParam(int i, double d, int i2)` 把 `i2` 又传成了 `i`（`ICatchCameraConfigImpl.java:84-85`），别继承这个 bug。
- **固件升级**：`updateFw(session, path)` 原生；App 把文件命名为 **`SPHOST.BRN`** 放到设备上 **`/idGoLive/Resoure/`**（拼写如此），再 `notifyUpdateFw()`，靠事件 96–100 回报。云端固件清单来自 `https://idcam.oss-cn-shenzhen.aliyuncs.com/Firmware/firmware.json`，字段 `product,name,region,version,download`。
- **现代权限全缺**：无 `BLUETOOTH_SCAN`/`BLUETOOTH_CONNECT`、无 `NEARBY_WIFI_DEVICES`、无 `FOREGROUND_SERVICE`、无 `POST_NOTIFICATIONS`，但 `targetSdk=35` ⇒ 蓝牙配对流程在 Android 12+ 上是坏的（推断，依据是清单只声明了 legacy `BLUETOOTH`/`BLUETOOTH_ADMIN`）。`minSdk=28`。

### 3.7 实时预览

**主相机（PTP 路径）不是 RTSP 也不是 MJPEG-over-HTTP**，而是同一 PTP 会话内的原生拉流：`ICatchIPancamPreview.start(ICatchStreamParam[, audio][, streamNo])` / `.stop()` / `.isStreamingOn()` / `getStreamControl()/getStreamPublish()/getThumbnail(...)`（`pancam/customer/ICatchIPancamPreview.java:20-44`）。
参数串即编解码协商：`H264?W=&H=&FPS=&BR=`（codec=41，默认 1920×960@30）、`MJPG?W=&H=&Q=&BR=`（codec=64，默认 q=50）、JPEG（codec=64）；帧在 App 侧由 `H264DecoderThread` / `MjpgDecoderThread` 解码。⚠️ **这些串是固件返回的、不是 APK 里的字面量**（20 个 arm64 `.so` 全量搜 `H264?` 零命中），App 解析后还会**改写帧率**（`StreamInfoConvert.java:8-34` 兜底 fps=30；`ConvertTools.java:80-95` 按高度降到 720→15、1080→10，详见 §3.4 分帧小节）。
握手顺序：`prepareCommandSession()` → `getPlaybackClient()/getControlClient()` → `changePreviewMode(int)` → `start(param)`；能力门 = `SUPPORT_PREVIEW` **55295（`0xD7FF`，旧版误写 `0xD7DF`）**（`CameraProperties.java:1379`）。
**拉流的实际 socket/端口在原生层，Java 与 .so 字符串里都找不到。**

**记录仪/副机走 RTSP，无鉴权**：默认 `rtsp://192.168.1.254`，Qz 系 `rtsp://192.168.169.1`；且**必须先 HTTP `?custom=1&cmd=2015&par=1` 打开预览**，`par=0` 关闭（`RequestInfo.java:44-45`、`LyIC.ready2CamLive:332-344`）。⚠️ **三个 IC 类返回的是裸 `rtsp://<ip>`——无路径、无端口**（`LyIC.java:168,220`、`QzIC.java:165,234`、`LyCmdWithYzIC.java:164,183`），`ic == null` 时兜底也是 `rtsp://192.168.1.254`（`ConnectDeviceManager.java:173,199`）⇒ **路径由播放器侧补，官方代码里没有完整 URL**，列为 §3.9 待抓包项。远端文件回放另拼一次：`"rtsp://192.168.1.1" + filePath`（`RemoteDeviceFilesFragmentPresenter.java:308`）。

### 3.8 文件传输

- **PTP**：`getFileCount()` / `listFiles(filter[,start,count])` / `setFileListAttribute(...)`，分页受能力位 `NEW_PAGINATION_GET_FILE=2` 控制；条目 = `ICatchFile`（`fileType`：**1=照片,2=视频**，由 `PbDownloadManager.java:127-129` 分支确证）；`getThumbnail(ICatchFile)→ICatchFrameBuffer`、`getQuickview(ICatchFile)`（原生 `ptp_mtp_getobjectquickview`）；专用批量通道 `openFileTransChannel()/closeFileTransChannel()/cancelFileDownload()`。设备路径形如 `A:/DCIM/100GOPRO/…`。
- **HTTP 映射**：`FileInfo.java:40-43` 把 `A:` 前缀直接替换成 `http://192.168.1.254`；反斜杠路径则 `furl = "http://192.168.1.254/Novatek/" + <父目录> + "/" + <文件>`，`turl = furl + "?custom=1&cmd=4002"`（`:53-55`）；`/app/` 系 `turl = <host> + "/app/getthumbnail?file=" + path`。
- **断点续传：有，标准 HTTP `Range`** —— `header("RANGE", "bytes=" + mAlreadyDownLength + "-")`，GET 与 POST 续传都带（`OkHttpDownUtil.java:119,242`；header 名是**大写 `RANGE`**，HTTP 头大小写不敏感 ⇒ 无功能影响，但 `:242` 把 Range 挂在 POST 上服务端多半会忽略），首次是普通 GET/POST，重试改派到 "Renewal" 变体（`:308-319`）。
- **PTP 侧还有一条独立的续传机制**：native `libcontrol.so` 导出 `ptp_getpartialobject`（arm64 与 armeabi-v7a 两个 ABI 都有，`_work/re/idgolive/natives-symbols.tsv:7301`）⇒ 设备源续传语义**不止 HTTP 一种**，插件层要按 profile 选。
  ⚠️ 这与 TUWIN（禁用 Range、用 `?offset=`）和 XTU（只有 Ambarella 支持 offset）**三家三种续传语义**，必须做成能力开关。

### 3.9 需要抓包补齐（静态无法确定）

1. PTP/IP 的**实际端口**（cmd/data/event 三条）。⚠️ 修正：**`3195/3196` 在本包里没有任何依据**（Java、`.so` 字符串、`.text` 的 `MOVZ` 立即数扫描全部零命中，端口是运行期算出来的）⇒ 首选假设是 **`15740`（PTP-over-IP 规范默认端口）**，`3195/3196` 只作为次选待验假设；`ptpip/init_cmd` / `init_cmd_ack` / `init_event` / `oprequest` 四个阶段名在 native 日志串里是实证的（三通道握手存在）。
2. 预览帧流的 socket/端口（原生）。
3. UDP 组播发现报文的语法（Java 只 `new String(data)` 打日志）；native `simpleconfig` 的字段顺序与 `checkpattern`（AES key 已知）。
4. `/app/getparamitems` 的完整键表（运行时发现）。
5. BLE GATT service/characteristic UUID（在 `bluetooth/core/jni` + 原生）。
6. `cmd=3031`/`3021` 的菜单 ID → 属性 ID 映射（设备返回）。
7. **副机 RTSP 的完整 URL**：三个 IC 类只回裸 `rtsp://<ip>`，路径/端口由谁补、补什么，静态看不到。


---

## 4. 对自研插件层的硬约束

1. **以探测结果路由，不以型号名路由。** XTU 根本没有「型号→协议」表，分派键是设备自报的 `type`；TUWIN 的 `RIDE5.expectedModels` 是空集。所以 `CameraProfile` 的匹配条件必须是 **(SSID 模式 ∧ 探测端点命中 ∧ 响应字段谓词)**，型号名只作为展示与统计维度。
2. **传输层至少六种**：HTTP-REST(JSON)、HTTP-CGI(`var K="V";` 文本)、裸 JSON-over-TCP(无长度前缀，靠"能否解析成 JSON"分帧)、二进制定长帧(72B/200B 小端)、**PTP container over TCP/IP 与 PTP over USB（UVC bulk / UVC isoc / MSC-SCSI 三种传输）**、**UDP 组播（发现 + AES 配网）**。外加 RTSP、RTMP 上行，与「设备回拨手机」的反向 socket（XTU 的 5678 由 `HisiSocketServer` 监听，**海思与 SigmaStar 共用**）。
3. **分帧策略要可插拔**：TUWIN M3 用 read-chunk-as-frame，XTU Ambarella 用 valid-JSON-accumulation，XTU 8080 用定长头。写死任一种都会在某品牌上崩。
4. **设置项必须走「设备自描述 + 运行时枚举」**，且要能承载三种发现协议：菜单 XML（TUWIN R3Pro/R6）、`primarymenuitem/secondmenuitem` CSV（XTU）、`getcapability(workmode,type)` 矩阵（老海思）、`getparamitems?param=all_<mode>`（M3）。UI 类型靠「选项数 ≤2 且值为 ON/OFF ⇒ 开关」这类启发式推断。
5. **参数键跨品牌不通用**，且同品牌内也不统一（`record_resolution` vs `rec_resolution` vs `Resolution`；`wifi_passwd` vs `wifipwd` vs `-wifikey`）。内部规范名 + 每 profile 一张映射表，别指望一个 key 空间。
6. **Ride5 的 CGI query key 带字面前导 `-`**，Retrofit/HttpUrl 之类会保留但很多 builder 会丢；必须用能原样输出 key 的构造器。
7. **TUWIN 的 `id` 是从菜单名推导的**（`normalize()` + 查表），不是 XML 里的 `<ID>`。复刻错这张表 → 所有 setparameter 静默失效。
8. **能力名对 R3Pro/R6 是拼接出来的**（`<code>_HTTP_CONTROL` 等），不能写死字符串。
9. **续传语义分裂**：TUWIN 设备源用 `?offset=` 且受固件版本门限、明确禁用 `Range`；XTU 只有 Ambarella 支持 offset 续传，CGI 家族完全没有。**默认按「不支持续传」实现，能力开关打开。**
10. **Ambarella 取缩略图/文件前必须先发 `261 set_clnt_info`**，否则设备不认后续 8787 数据连接。
11. **命名陷阱清单**：`XTU S7PRO`/`XTU S7PRO MAX` 带空格而其余不带；Amba 上报名要 `split("-")[0]`；`var capacity=` 这个键名自带尾随 `=`；CSV 里 `,,` 要先变成 `,-,` 再解释为空；idGoLive 协议里 `mineType` 拼错、`DEFALUT_TO_PREVIEW` 拼错、固件目录 `/idGoLive/Resoure/` 拼错 —— **照抄字面量，不要"顺手改正"**。
12. **命令层要能表达 PTP 模型**：idGoLive 的主相机是「设备属性表（16 位 DPC + 子属性索引，int/string/bytes 三型）+ 对象表（handle + ObjectInfoPropList）+ 事件队列」。所以 `CameraProtocol` 不能只有 `send(command, params)`，必须再支持 `getProperty(id, type)` / `setProperty(id, value)` / `listObjects(filter, page)` / `events(): Flow<Event>` 这一组原语，否则 iCatch 系接不进来。
13. **一条命令通道 ≠ 一个设备**。idGoLive 一台设备上并存 **11 条**通道（PTP/IP 控制+文件、PTP over USB ×3、HTTP `?custom=1&cmd=`、`/app/`+`/api/` JSON、XML-TCP:3333、UDP 组播发现+配网、BLE/SPP、RTSP、RTMP 上行；另有一条 `enableSocketIO` 是 SDK 提供但 App 未接的**第 12 条**），XTU 的 Ambarella 是「7878 命令 + 8787 数据」双 socket，海思/SigmaStar 还有 8080 二进制帧与 5678 回拨。插件的会话对象要允许**多路并发连接、各自独立分帧与生命周期**。
14. **安全上不能假设任何鉴权**。idGoLive 的 HTTP 通道零鉴权（按缺失确证），XTU 的 CGI 也是明文无 token，只有 TUWIN 有 `seed` 握手（且 seed 是明文随机数、无校验）。我们连上相机 AP 后就是全权控制；反过来说，**任何"设备已认证"的乐观假设都是错的**，UI 上要把"当前连的是哪台/哪个 IP"显示清楚。
15. **别把"厂商 App 有型号表"当成前提，但也别当成完全没有。** idGoLive 是 iCatch 的白标公版 App：**没有「机型 → 能力」注册表**（`CameraType`/`CameraAddType` 无品牌、无芯片枚举），但**有**一条 33 项的「相机名 → 跳过写时区」黑名单（`CameraProperties.java:1476`，命中即 `:1480` `return false`，不写 `0xD83E`）。选 handler 也**不是**「对三个 IP 并发 ping」：ping 只打 `192.168.1.254` 与 `192.168.169.1`（`ConnectDeviceManager.java:266`），**`192.168.1.1` 的 PTP 机不参与 ping**，由 `MyCamera.connect(true)` 的成败决定；两路 ping 还**共享一个失败计数器**（`:33-37`，累到 2 就整体 404）。⇒ 准确的模型是**四层判定**：PTP 会话成败 → ICMP 应答者 → `getSSID()` 成败 → 相机名黑名单。我们做「型号 → Profile」是比官方更好的设计，但要留一条**「无型号、纯探测」的兜底路由**，且 profile 里要有「按设备名跳过单条命令」这一层，否则遇到 idGoLive 这类贴牌长尾就接不进去。
16. **命令兼容性黑名单要能表达**：同一协议下，某些型号要跳过/改写个别命令（idGoLive 按 `getCameraName()` 与 33 项名单**全等比对**跳过写 `0xD83E` 时区，注意不是前缀匹配；XTU 按 `name endsWith "S7PRO"` 改 `record?-cmd=Storie`；TUWIN 按固件版本门限决定能否续传）。Profile 结构里要有 `命令级 override / 门限` 这一层，不能只有"支持哪些命令"的粗粒度能力集。



## 5. 现有代码需要据此修正的点

| 位置 | 现状 | 应改为 |
|---|---|---|
| `core/model/Models.kt:23-24` 注释 | 「TUWIN M3 – `/app/` HTTP + **delimiter-framed** TCP socket」 | M3 实际是 **read-chunk-as-frame**，`DelimitedFrameDecoder` 在官方包里是死代码 |
| `core/model/Models.kt` 的 `DevicePlatform` 枚举 | 只有 Hisilicon / TUWIN_REST / TUWIN_M3 等 HTTP+TCP 形态 | 需要新增 **PTP/IP** 一档（iCatch 系），并让协议接口带 `getProperty/setProperty/listObjects/events` 原语（见第 4 节第 12 条） |
| `core/net/DeviceDiscovery.kt:20-24` | 候选 IP `192.168.0.1/1.1/42.1/16.1/25.1` | 补 **`192.168.169.1`**（TUWIN M3 与 idGoLive Qz 系共用）、**`192.168.1.254`**（idGoLive Ly/Yz 系）；`192.168.1.1` 有双重身份 —— XTU 侧是 Mini1 行车记录仪，idGoLive 侧是 PTP 主相机，**不能只按 IP 判品牌**，必须配合探测通道 |
| 品牌识别 | 靠 IP 探测 | 加 TUWIN SSID 正则表（`^TUWIN[-_]M3_…` 等 6 条 + 匹配优先级）与 XTU 的 `contains("XTU")` + BLE `XTU_` 前缀；idGoLive 另加 UDP 组播 `234.168.168.168:5002` 发现 |
| 型号表 | 注释提到 "XTU X7 Pro" | XTU 官方 18 型号清单里**没有 X7 Pro**；有 `XTU S7PRO`、`XTU S7PRO MAX`。以设备自报 `name`/`type` 为准；且「18」是**四张列表的并集**（`S3` 只在 `CameraInfoUtils` 图标表、`Mini1` 只在添加页与反馈页），不能当成任何一张原表 |
| base URL 注入 | 混用「探测 IP」与「常量 IP」 | XTU 官方自己就是**两套来源**：CGI 用探测 IP（`DeviceHttpUtils.java:14-15`）、8080/RTSP/缩略图/下载用硬编码 `192.168.0.1`（`SSConnectDevice.java:57`、`SSCommandUtil.java:50`、`SSFileInfoBean.java:83`）。自研要**统一用探测到的 IP 注入 base URL**，否则遇到换网段的机型的会局部失效 |
| 预览帧率 | 直接照发设备返回的 `FPS` | idGoLive 官方会在 App 侧改写：`ConvertTools.java:80-95`（720→15、1080→10）与 `StreamInfoConvert.java:8-34`（无 `FPS` 字段时兜底 30）。不复刻 ⇒ 我方预览帧率比官方高 |
| 下载续传 | 单一策略 | 三家语义互不兼容（TUWIN `?offset=` 且禁 Range / XTU 仅 Ambarella offset、CGI 家族 8080 是 append 追加 / idGoLive 标准 `Range: bytes=N-` + native `ptp_getpartialobject`），必须做成**每 profile 一个能力开关** |


## 6. 仍需真机验证（静态无法确定）

1. TUWIN RIDE5 的**媒体端口协商**：profile 里 `allowedMediaPorts` 为空，但代码常量确证为 RTSP `554` + 反向回调 `9002`（`Ride5DeviceAdapterSession.java:53,57`，两处 grant 签发 `:250`/`:251`）⇒ 待验的是**设备侧实际监听端口是否等于这两个常量**，以及 `Ride5IdentityPolicy` 的分支方向（jadx 反编译异常）。
2. TUWIN M3 事件 socket 的**心跳与跨 read 分帧率**：端口来源已确证是 `/app/getmediainfo` 的 `port`（`M3DynamicEndpointPolicy.java:59-62`、`:78`），`getproductinfo` 的 `sp` 零消费者 ⇒ 只剩「官方无分隔符实现下，长 JSON 被切成两帧的实际发生率」与「设备是否发保活帧」两件事要抓包。
3. XTU 各 CGI 家族型号的**具体芯片归属**（X1/X2/X3/S2/S3/S5K/S3PRO/MAX2/MAX3/MAXPRO/S6 到底是 SSC88xx 还是老海思）——只能连上真机读 `getdeviceattr.cgi` 的 `type`。
4. XTU R1 的平台（目前只有共用图标这一条弱证据；`CameraInfoUtils.java:47-49` 与 `ScanAddRecycleAdapter.java:117` 都只画图不判平台）。
5. 各机型的 `capability` / `menu xml` 实际内容（设置项全集只能从设备取）。
6. 5678 回拨的**报文协议与相机侧触发条件**：谁在听已确证 —— `HisiSocketServer.java:19,70,76`，由海思/SigmaStar 共用的预览页无条件 `start()`（`HisiActionCameraPreviewActivity.java:307`），`MessageService`/`KeepAliveService` 是清单幽灵；未知的仍是「相机在什么条件下回拨、回拨什么字节」。
7. **idGoLive 的 PTP/IP 端口**（cmd/data/event 三条）与 PTP 拉流 socket 的端口。⚠️ 假设排序已改：**`15740`（PTP-over-IP 规范默认）为第一候选**，`3195/3196` 在本包里**零证据**、只作次选；`.text` 的 203 条 `MOVZ` 立即数无一落在端口区间，端口是运行期算出来的。
8. idGoLive 的 UDP 组播报文语法（`234.168.168.168:5002`，含 native `simpleconfig` 的字段顺序与 `checkpattern`）；以及那条拼错的 `CHANGE_WIFI_MULTICAST_STAT` 权限是否让 `MulticastLock` 失效——**静态不能判**（08 里「锁不需要权限」的说法与本仓库对 Android 行为的理解冲突，且它把正字写成了不存在的 `CHANGE_WIFI_MULTICAST_CAST`），故以真机验证为准。
9. idGoLive 的 `/app/getproductinfo` 响应里承载型号的确切 JSON key，以及 `cmd=3031&str=all` 返回的菜单树结构（App 侧只认 `Cmd ∈ {2003,2011,8010,8011}` 已确证，`XmlParseUtil.java:35-53`；未知的是被丢弃的那些 `Cmd` 到底是什么）。
10. idGoLive 的 BLE GATT service UUID（Java 里没有，在 `bluetooth/core/jni` + 原生）。
11. XTU 的 `pasttime` 单位（App 零换算、原样按秒显示；`AmbaCmdModel.java:570` 上报是否按半秒递增只能真机对照）。
12. XTU `SSConstant.SS_IP` **非 final**（`SSConstant.java:13`）→ 是否存在运行期改写路径；若不存在，换 IP 的机型在 8080/RTSP/缩略图三处必然失效。

---

---

## 7. SJCAM（山狗，`org.jght.sjcam.zone` 6.7.3.15）

SJCAM 是**多 SoC 品牌**：一个 App 里并排五套相机通道，按**网关 IP + 相机自报 model 串**分派（`factory/JFCamera.java:205-224`）。全部证据与逐条行号在 [SJCAM 档案](../evidence/sjcam.md)。

### 7.1 型号词表（三套写法并存，不能混用）

| 家族 | 词表形态 | 例 |
|---|---|---|
| Ambarella | `SJCAM<型号>` | `SJCAMSJ8PRO`、`SJCAMSJ9PRO`（SJ9 Strike）、`SJCAMSJ10PRO`、`SJCAMSJ10ProDualSCR`、`SJCAMSJ8KPRO`、`SJCAMSJ7STAR` |
| Novatek（Ly） | `<芯片号>-<型号>` | `660-SJ10X`、`683-SJ9`（SJ9 Max）、`683-SJ8+`（SJ8 Plus）、`658-SJ8AIR`、`580-SJ20_580`、`580-SJ6Pro_580`、`580-C300`、`580-C200Pro`、`580-C110+`、`675-C100+`、`672-C100`、`660-C200`、`660-SJ4000X`、`655-SJ4000WIFI`、`660-SJ5000X`、`660-M20`、`658-M20Air`、`670-A50`、`a10`（小写裸串） |
| 海思 / 全志 / iCatch | 裸串 | `Hi3559V200-DV-IMX458`（SJ10 MAX）· `V536-CDR`（SJ10_A）· `"ICatch"` |

⚠️ 同名两代机分属不同家族：**SJ9 有 `683-SJ9`（Novatek）与 `SJCAMSJ9PRO`（Ambarella）两个串**，SJ8 Pro 与 SJ8 Plus 亦然。

### 7.2 识别与分派

| 判据 | 值 | 证据 |
|---|---|---|
| SSID 前缀 | `SJ` / `C100` / `C200` / `A10` / `A20` / `A30` / `M20`（大写后 `startsWith`） | `view/WifiBottomPopup.java:252-254` |
| 网关 IP | `192.168.42.1`→Amba · `192.168.1.254`→Ly · `192.168.100.1`→全志（老） · 其它→非相机 | `factory/JFCamera.java:205-224`、`utils/NetUtils.java:209-215` |
| 特例 | 网关 `192.168.1.1`/`192.168.101.1` **且** SSID 含 `SJ6`/`C200` → 按 Ly | `factory/JFCamera.java:199-203` |
| 全志出厂密码 | `12345` | `camera/allwinner/AllwinnerCamera.java:52` |

**官方没有「型号 → 协议」注册表**；控制页按 model 串 + 固件版本进（`ConnectCameraFragment.java:1096-1113`），所以我们的插件同样以**探测结果**路由，model 串只用于「认领资格」。

### 7.3 四套控制通道（第 5 套 iCatch PTP 我们不实现）

| 通道 | 基址 / 端口 | 命令 | 响应 |
|---|---|---|---|
| Ly（Novatek） | `http://192.168.1.254/?custom=1` | `&cmd=<id>[&par=][&str=]`，`cmd`→`par`→`str` 顺序；id 全表见档案 §4.1 | `<Function><Cmd>/<Status>/<Value>/<Total>/<Free>` 伪 XML |
| hisnet（海思 SJ10 MAX） | `http://192.168.0.1/cgi-bin/hisnet/` | `getdeviceattr/getworkmode/setworkmode/getworkstate/getbatterystate/getsdstatus/getfilelist/getfileinfo/deletefile/deleteallfiles/sdcommand/getsetting/setsetting/getmedia/setmedia/getitem/getallmode/getlang/setlang/getsystime/setsystime/client/sendclickkey/reset/poweroff` | `var key = "value";` 文本 |
| 全志（SJ10_A） | `http://192.168.10.1:8082/api/` | 读 `getdeviceinfo`、写 `setdeviceinfo`；`cmd`：`RECORD=1100` `CAPTURE=1101` `MODE_CHANGE=1110` `GET_VERSION=2001` `RECORD_STATE=2005` `GET_MODEL=3030` `GET_MODEL_LIST=3031` | JSON（含固件自带拼写 `RecodStatus`） |
| Ambarella | TCP `192.168.42.1:7878` | `{"msg_id":N,"token":T,…}`：`257` 开会话 · `1/2/3/9` 设置 · `4` 格式化 · `11` 设备信息 · `12` 关机 · `13` 电量 · `513/514` 录像 · `515` 录像时长 · `769` 拍照 · `1281` 删文件 · `483/485` 文件表 | JSON，单值在 `param` |

事件通道（**我们一律用轮询代替**，与官方也混用轮询一致）：Ly TCP 3333（`<Cmd>3020/2020` 推送，`</Function>` 聚包）· hisnet **相机回连手机 TCP 9000**（需先 `client.cgi?&-operation=register&-ip=<手机IP>`）· Amba `msg_id=7` 通知。

### 7.4 我们侧的实现（`composeApp/.../brand/sjcam`）

一个插件四个通道，注册顺序 `ly → hisnet → allwinner → amba`，每个 probe 只认领自己家的报文（Ly 认 SJCAM 型号串，hisnet 认 SJ10 MAX 型号串，全志认 `device_name`+`software`，Amba 认会话成功且 model 含 `SJCAM`）。

| 能力 | Ly | hisnet | 全志 | Amba |
|---|---|---|---|---|
| 状态 / 电量 / 卡 | ✅（`3019/3017/3016/2016`） | ✅（含 `recording`，`var running`） | ⚠️ 仅录像状态（无电量命令） | ✅ 电量 + 录像时长 |
| 设置读写 | ✅（`3014` 字典，id 即 `cmd` 号） | ✅（媒体页 / 设备页分开） | ✅（`Menu` 已解析成选项） | ✅（`msg_id=3/2`） |
| 快门 / 录像 | ✅（`1001` / `2001&par=`） | ⚠️ 仅 `KEY_MENU` 点击，**录像命令 APK 里不存在** | ✅（`1101` / `1100`） | ✅（`769` / `513`·`514`） |
| 文件浏览 | ✅（抓 `/DCIM/<类型>/` 的 HTML 目录） | ✅（`getfilelist` 平行数组） | ❌ 官方走 sqlite 索引 `sunxi.db`，未实现 | ✅（`485`/`483`，下载走 HTTP 镜像 `/tmp/SD0`→`/SD`） |
| 缩略图 | ✅ 照片用原图 | ❌ | ❌ | ❌（8787 数据 socket 未实现） |
| 图传 | RTSP `:554/xxx.mp4`，拍照模式 `:8192` | RTSP `:554/livestream/12` | RTSP `:8554/ch01` | RTSP `/live` |

未实现且**有意不做**的：iCatch PTP 通道（native 栈，与 `brand/icatch` 同一取舍）、三条推送通道、Amba 8787 数据 socket、全志的 sqlite 文件浏览、OTA（三家通道都有升级端点，但包格式未知）。

## 变更记录（2026-09-22 全量复现档案回写）

| 处 | 改了什么 | 依据（`docs/evidence/`） |
|---|---|---|
| §0 总览表「型号维度 / 型号数」两行 | idGoLive 的分派键由「可达 IP / 方案 IC」改写成**四层判定**（PTP 会话成败 → ICMP 应答者 → `getSSID` 成败 → 相机名黑名单）；「无型号注册表」收窄为「无『机型→能力』注册表，但有 33 项『机型名→跳过写时区』黑名单（含 5 个贴牌名）」，并补清单侧唯一机型表 = `device_filter.xml` 的 2 条 USB VID/PID | `docs/evidence/idgolive §2`、`§2.1`、`§9`「无型号注册表」行 |
| §0「控制通道」行 | 通道计数落实为 **11 条**并指向 §3.4；UDP 组播补「配网」用途 | `docs/evidence/idgolive §3` |
| §0「图传」行 | idGoLive 的 `H264?W=&H=&FPS=&BR=` 标为**固件返回**（APK 内零字面量），补「直播上行 RTMP」；XTU 侧老海思补 `/11` 与 HTTP 变体 | `docs/evidence/idgolive §6.2`、`§9` 预览行；`docs/evidence/xtugo §11`「预览起流」行 |
| §0「配网入口」行 | 「无扫码」收窄为「无 QR 扫码**配网**」（包里有 zxing 与两套 QR 生成类，只用于分享链接出码）；补组播 AES `simpleConfig` 与「手机开热点让相机连」两条旁路 | `docs/evidence/idgolive §9`「无扫码配网、无 Wi-Fi Direct」行、`docs/evidence/idgolive 附录D（UI 与端点判定） §2.3` |
| §0「设置项来源」行 | idGoLive 属性空间从「`0xD7xx`/`0xD5xx`」改为 **57 个码点**（`0x50xx`/`0xD6xx`/`0xD7xx`/`0xD8xx` + 高位能力码），并注明 App 只认 4 个 `Cmd`；XTU 侧补限定「只有老海思方言在 APK 里有一张 PreferenceScreen 静态表」 | `docs/evidence/idgolive 附录A（PTP 属性表） §0`、`docs/evidence/idgolive §9` 属性表行；`docs/evidence/xtugo §11`「APK 内有参数表」行 |
| §0「下载续传」行 | XTU 补「CGI 家族的 8080 分支是 append 追加」；idGoLive 补 native `ptp_getpartialobject` 这条独立于 HTTP 的续传机制 | `docs/evidence/xtugo 附录C（SigmaStar 与播放层） §C.9` #11 ②③；`docs/evidence/idgolive §9` Range 行 |
| §1.3 端点表 M3 行 | 推送 socket 端口来源从 `info.sp` 改判为 `/app/getmediainfo` 响应的 `port`（校验 `1 ≤ port < 65536`、非法即整个 open 失败）；`getproductinfo` 的 `sp` 降为「解析了但零消费者」 | `docs/evidence/tuwin 附录一（协议面） §8.1` C-4（+ 本文 §4.2） |
| §1.3 端点表 RIDE5 行 | 「媒体 socket 端口未找到常量」改判：`allowedMediaPorts` 为空只是字段事实，常量确实存在 —— 直播 RTSP `554` + 反向回调 TCP `9002`，两处 `@Deprecated` 但仍用于签发 `EndpointGrant` | `docs/evidence/tuwin 附录一（协议面） §8.1` C-5（+ 本文 §7.1） |
| §1.3 连接 bullet | 新增连接工程数值一组：`WifiNetworkSpecifier` 要求 SDK ≥ 29、路由 lease 替换等待 3000 ms、替换轮询 50 ms、主动 Wi-Fi 扫描 8000 ms，标为自研超时/重试默认值的参照 | `docs/evidence/tuwin 附录三（传输·OTA·联网） §8.3`（+ 本文 §1.8） |
| §1.4 表格 RIDE3PRO/RIDE6 行 | 「命令必须串行」降级为**客户端自设三层串行**（L1 `Ride3ProRequestScheduler` 主路径 / L2 `Ride3ProHttpRequestQueue` Mutex 兜底 / L3 `Ride3ProHttpSerialDispatcher` 只服务 Glide）+ `retryOnConnectionFailure(false)`；⚠️ 标注「设备不接受并发」静态无证据 | `docs/evidence/tuwin 附录一（协议面） §8.1` C-7、`§8.2` D-4（+ 本文 §6.1–§6.3） |
| §1.4 Ride3Pro/Ride6 端点串 | `rtspstatus` 与 `capture` 都改标「只声明、零业务调用点」，`rtspstatus` 不再计入鉴权链；官方「拍照」是播放器截 RTSP 帧 | `docs/evidence/tuwin 附录一（协议面） §8.1` C-1、`§8.2` D-3；`docs/evidence/tuwin 附录二（设置与操作） §6` 末段 |
| §1.4 M3 TCP 三个反直觉事实 | 事实 1 补活解码器的确定链、`UnconfiguredFrameDecoder` 同为死类、单次读上限 4096 与跨 read 切帧副作用；事实 2 端口证据改指 `M3DynamicEndpointPolicy`、补「无默认值」；事实 3 的「无写路径」证据换成 `M3SessionSocket` 接口 + `getOutputStream` 零命中，并补「连不上只摘掉 `M3_SOCKET_EVENTS`、会话照建」 | `docs/evidence/tuwin 附录一（协议面） §8.1` C-4/C-11/C-12、`§8.2` D-2/D-8（+ 本文 §4.1/§4.4/§4.8） |
| §1.4 推送信封与 `msgid` 表 | 顶层第三个键 `time` 从确证降级为**静态不可证**（jadx 折叠成 `DeepLinkPresetEvent.UMEventParamTime`）；补两个边界「空帧 → `return null`」「非法/半截 JSON → `Unknown` 不上抛」；表加「App 侧消费方」列，`mic` 与 `Unknown` 标**无消费方** | `docs/evidence/tuwin 附录一（协议面） §8.1` C-8/C-9/C-10（+ 本文 §4.6） |
| §1.4 RTSP 预览段 Ride5 句 | 「端口从设备属性 `RTSP_LIVE_PORT` 读」改判为「客户端常量 554 → 写进 `EndpointGrant` → 起流时从 grant 取出拼 URL，设备不参与协商」 | `docs/evidence/tuwin 附录一（协议面） §8.1` C-6 |
| §1.4 seed 段 | 标题从「seed 鉴权」改「seed 握手」；顺序改为 authdevice（失败判据 `result`）→ send-time（失败仅告警）→ `markReady` → setmode → 起播，删 rtspstatus；`consume()` 从「一次性」改为**只读探测（peek）、10 分钟有效**；「seed 让 RTSP 服务端授权」降级为推断 | `docs/evidence/tuwin 附录一（协议面） §8.1` C-1/C-2/C-3、`§8.3` E-1/E-2 |
| §1.5 结构结论 | 「完整参数表不在 APK 里」限定范围：只有 R3Pro/R6 的**取值集合**在设备侧，「哪些项存在 / 哪些项可写 / 分区顺序 / 英文→中文映射」四张表 APK 里写死可枚举；M3 的 22 键与 Ride5 的 `-type` 词表完全静态 | `docs/evidence/tuwin 附录二（设置与操作） §6` #14（表见本文 §1.2.1–§1.2.3） |
| §1.5 (a) 菜单 XML 段 | 补「有会话时取菜单走**裸 socket**，HTTP `/api/menu/xml` 只是接口声明与兜底」及四个 `@Deprecated` 的 `MENU_*` 常量（5000 / 524288 / 8192 / 30000），socket 上写什么标静态无解；Schema 三行改判：`<ID>` 被**数值化比较**用来认项、`<Type>` 按整数解析、`<Default>` 是默认值不是当前值 | `docs/evidence/tuwin 附录一（协议面） §8.3` E-4/E-5（+ 本文 §5.1）、`docs/evidence/tuwin 附录二（设置与操作） §6` #1/#2 |
| §1.5 (a) 新增三条 | `<Type>` 的 5 种渲染分支与 `Type ∈ {1,4}` 需单独补读、当前值来自 `getparameter?id=all`、`Ride6SettingsCompatibilityPolicy` 用 `<ID>` 认 `factory_reset`/`wifi_passwd` 并在复位后清缓存密码 | `docs/evidence/tuwin 附录二（设置与操作） §6` #1/#2（+ 本文 §1.1、§1.2.2） |
| §1.5 (a) `id` 推导段与 9 行表 | 删掉 `normalize()` 里错误的「先大写 ROOT」一步（实际只做一次 `toLowerCase(Locale.ROOT)`）；9 个 id 的触发条件逐 `case` 重写（`wifi_name`/`wifi_passwd` 的分区是空分区或 `wifi` 而**非** `system`，`ssid`/`password` 走「非空任意分区」，`all` 不参与 `forSetting`）；补「推不出 id 时 `update()` 直接抛 `IllegalStateException`」 | `docs/evidence/tuwin 附录二（设置与操作） §6` #3/#4/#5（表见本文 §1.2.2 的 `Ride6ParameterRequestIds`） |
| §1.5 (b) M3 | 删掉「读值走 `getparamvalue`」：该端点的两个方法声明全仓零调用方，当前值实际来自 `getparamitems` 响应里的 `cur_index` + `indexOf` 反查，照抄属**过度实现** | `docs/evidence/tuwin 附录二（设置与操作） §6` #6（+ 本文 §1.4） |
| §1.5 (c) Ride5 | 「`-type`/`-workmode` 词表未找到」被推翻：`-workmode` 在该页写死 `NORM_REC`，`-type` 静态可确定 9 个并逐条给出成对取值（含源码拼错的 `WIFI_DEAULT_STATE`）；仍未知的只剩 `workmodecmd` 词表 | `docs/evidence/tuwin 附录二（设置与操作） §6` #7（全表见本文 §1.6） |
| §1.5 (d) 概念对照表 | Ride5 列由「词表未知 / —」填成 `MEDIAMODE`、`Rec_Split_Time` 等实值；「仅菜单 XML」各行统一注明真因是「推不出 id ⇒ 只读显示」；音频行分「可达 UI 层 / 代码层」两说；固件行精确到三个 query key 并标「分包检查 ≠ 上传通道」；新增 7 行 Ride5 专属项（紧急录像、停车监控、GSR 灵敏度、降风噪、WiFi 三态、上下/左右翻转） | `docs/evidence/tuwin 附录二（设置与操作） §6` #8/#9/#10/#12/#13（+ 本文 §1.6） |
| §1.5 (d) 表下三条注 | 新增：静态兜底菜单 31 项才是「仅菜单 XML」的原因、`hdr_chk_cnt` 是状态字段不可与 HDR 设置互推、该专属页对 RIDE3PRO/RIDE6 **恒不可达** | `docs/evidence/tuwin 附录二（设置与操作） §6` #9/#10/#11 |
| §1.6 文件下载与断点续传 | 「设备源禁 Range」补三条硬证据：`TransferRequest` 对设备传输用 `HttpRange` 直接抛、全语料字面 `Range` 只 1 处且属日志脱敏白名单、设备下载请求构造不加任何 header | `docs/evidence/tuwin 附录三（传输·OTA·联网） §8.3`（+ 本文 §2.3） |
| §2.1 证据来源行 | 「四份独立列表交叉一致」改判为**四份列表彼此不一致**：添加页 17 项（无 S3）、旧 UI 14 项、图标表 17 个 `XTU*` token（含 `XTUS3`、不含 Mini1）、反馈页 17 项（含 Mini1、无 S3）⇒「18」是并集不是任何一张原表，且 token 大小写互不相同不能只按一张表的字面量匹配 | `docs/evidence/xtugo §2.1`、`§11`「18 型号清单」行 |
| §2.2 识别方式 | 新增结论：**SSID 与 BLE 名是两套规则** —— SSID 侧只有 `contains("XTU")` 子串判定（全树无 SSID 正则、无 `SSID_PREFIX`），`toUpperCase().startsWith("XTU_")` 只出现在 BLE 侧；自研 `CameraProfile` 要各留一个谓词 | `docs/evidence/xtugo §2.4`、`§11`「SSID 前缀正则」行 |
| §2.3 端口表 | RTSP 行补 `HaisiCommandUtil` 证据；新增「海思老 `DV` 预览的第二条 URL（HTTP 80）」一行；5678 行改判 —— 真实监听者是 `HisiSocketServer` 且由海思/SigmaStar **共用**的预览页无条件启动，`MessageService`/`KeepAliveService` 是清单幽灵 | `docs/evidence/xtugo 附录C（SigmaStar 与播放层） §C.9` #3、`docs/evidence/xtugo §11`「预览起流」行 |
| §2.3 码流开关段（新增） | `/11` 大码流限定为**只存在于老海思 `DV`**（`isPreviewBigBitRate()` 在 `11`/`12` 间切），SigmaStar 与 Ambarella 恒 `/12`；RTSP 传输协议按**在用**的 `VideoTextureView` 判定，`SSVideoView` 的 udp/prefer_tcp 分支标死代码 | `docs/evidence/xtugo §11`「预览起流」行、`docs/evidence/xtugo 附录C（SigmaStar 与播放层） §C.9` #6/#7（+ 本文 §C.5.3/§C.5.4） |
| §2.3 默认 IP 段（新增） | 「一处默认 IP」改为 **6 个常量位**（含孪生且类名拼错的 `CameraParmeras`）；并拆清分工：CGI 面用探测到的 IP，硬编码 `192.168.0.1` 只在 8080 与 RTSP/缩略图/下载 URL，且 `SS_IP` 非 final ⇒ 换网段机型会局部静默失效，自研要统一注入 base URL | `docs/evidence/xtugo §2.5`、`§11`「相机默认地址常量」行、`docs/evidence/xtugo 附录C（SigmaStar 与播放层） §C.9` #8 |
| §2.4 `pasttime` 段（新增） | 「单位半秒」改判：App 侧**零换算**、原样当秒显示，是否差 2 倍只能真机对照（已列入 §6 #11） | `docs/evidence/xtugo §11`「`pasttime` 单位半秒」行、`docs/evidence/xtugo 附录C（SigmaStar 与播放层） §C.7.2` |
| §2.4 (c) 端口 8080 二进制帧 | 整段重写：同名 `RegisterDeviceToServer` **有两份**、活路径是 `dv.ui.data.model` 那版；`[4..7]` 语义两代不同（老实现写字符数且不截断 ⇒ 长路径抛越界，新实现写 UTF-8 字节数）；cmd 补 `2 = 推固件`、`4 = 把 RTMP 参数推给相机`；补 200 B 帧的 MD5 前导零坑与「等相机回一行才分块写」；下行改「72 B 头 + 裸流」并明确长度按 uint32 LE 读、老代码的负数修正是 bug；补语言表用文本行读的例外 | `docs/evidence/xtugo 附录C（SigmaStar 与播放层） §C.9` #4/#5/#9、本文 §C.3.2–§C.3.5 |
| §2.4 设置项 CSV 行 | 补「SigmaStar 侧同一套空值补齐的**两个重载行为不同**」：`String` 版按段数差补 `,-`，`Response` 版不补、段数不等即整表返回 null ⇒ 同一固件两条路径一条出菜单一条出空 | `docs/evidence/xtugo 附录C（SigmaStar 与播放层） §C.9` #10、本文 §C.7.1 |
| §2.5 设置项资源段 | 新增「APK 里唯一写死的设置表是老方言（海思 `dv`）的 PreferenceScreen」及其三个来源文件、选项只引到 4 对 `@array`；`arrays.xml` 计数 26 改 **28**、两份 PAL 变体改标「只有 `R` 常量、全树无使用点」；新增 `boot_action` 数组与 `Common.BOOT_ACTION_*` **不自洽**的照抄告警 | `docs/evidence/xtugo §4.3`（同档案 §11 写的「6 个 `R.array`」少计了一对，其 §4.3 表实为 4 对 8 个）、`docs/evidence/xtugo 附录D（操作复现表） §6.3` |
| §2.6 回放与下载表 | SigmaStar 续传格由「未找到」改成实值：<2 GiB 纯 HTTP GET、≥2 GiB 才切 8080，8080 分支是 `FileOutputStream(file, true)` **append 追加、不发 offset** ⇒ 重复下载会追加重复内容；老海思格删掉「`new FileOutputStream(p)` 无 append」的旧写法（不成立，与 SigmaStar 同一套） | `docs/evidence/xtugo 附录C（SigmaStar 与播放层） §C.9` #11 ②③、本文 §C.6.3 |
| §3 开头品牌属性段 | 「它没有型号注册表、靠哪个 IP 应答选协议」拆成两层：**无**「机型→能力」注册表（`CameraType`/`CameraAddType` 无品牌与芯片枚举），**有** 33 项相机名黑名单；选协议改述为**四层判定**并点明「`192.168.1.1` 的 PTP 机不参与 ping」 | `docs/evidence/idgolive §2`、`§2.2`、`§9` 对应两行 |
| §3.1 (c) 黑名单标题行 | 常量行号 `:1475` 纠正为 `:1476`，并写清命中后果是 `setCameraDateTimeZone()` 直接 `return false`、不写 `0xD83E`（判定点与方法体行号一并给出） | `docs/evidence/idgolive §2`、`§2.1` |
| §3.1 新增两条 | ①黑名单是 `List.contains()` **全等**比对，不是前缀也不是子串，比的是 PTP `0xD831` 读回的原样名 ⇒ 表外名字照样写时区；②白标证据链：5 个贴牌产品名直接写在本 APK 源码里，旧说法「贴牌名不在本 APK 内」作废，拼写是 `Crosstour` | `docs/evidence/idgolive §2.1`、`§9`「无型号注册表」行 |
| §3.2 USB 行 | 代码常量 VID 的十六进制纠正为 `0x2AAD`（旧写 `0x2A0D` 错）；补清单侧唯一机型表 `device_filter.xml` 的 2 条 `<usb-device>`，并说明「VID 一致、PID 与代码常量不一致，白名单以清单为准」 | `docs/evidence/idgolive §1.4` 末行、`§9`「新发现」⑦（08 那两处把 10925 写成 `0x2AAB`，按十进制换算应为 `0x2AAD`） |
| §3.3 连接方式 | 「无扫码」改为「无 QR 扫码**配网**」并给出 zxing/两套 QR 生成类的真实用途与零解码入口证据；配网入口四条之外补两条旁路（组播 AES `simpleConfig`、手机开热点）；端到端顺序里的心跳从「默认 30000 ms / QzIC 4000 ms」改为**按 handler 分档** 30000 / 10000 / 4000 | `docs/evidence/idgolive §9`「无扫码配网」行、本文 §3 #9/#10、§3.1 心跳周期行、§6.1 |
| §3.4 通道清单 | 「8 条并存」改为 **11 条**：USB 拆成 UVC BULK / UVC ISO / MSC-SCSI 三条并给出各自的 `prepareSession_Jni` 实参形状，新增 RTMP 上行，组播改标「发现（Java 只收）+ `simpleConfig` 配网（native 收发）」；表内补心跳与超时数值、`/app/` 分页步长 101、T1 的 `192.168.1.1` 常量位与 SP 可覆盖 | `docs/evidence/idgolive §3`、`§3.1`、`§9`「8 条传输通道」行 |
| §3.4 表下两条注（新增） | ①第 12 条 `enableSocketIO` **存在但零调用点**；②`.254` 上并存两套 profile（`?custom=1&cmd=` XML 与 `/app/` JSON），但 `try2connectLywyzIC` 从不被调 ⇒ 官方只选中 Novatek 那套 | `docs/evidence/idgolive §3` 末段、`§9`「192.168.1.254 两套 API」行 |
| §3.4 分帧方式 | T5/T6/T7 编号随表重排；补「`H264?…` 串**字面量不在 APK 里**（20 个 arm64 `.so` 全量搜零命中）、由固件返回」，以及 App 的两处改写：无 `FPS` 字段兜底 30、按高度降帧（720→15、1080→10） | `docs/evidence/idgolive 附录B（HTTP 与 TCP 命令） §7`、`docs/evidence/idgolive §6.2`、`§9` 预览行 |
| §3.5 (b) HTTP 命令段（新增） | `cmd=3031&str=all` 返回的三元组 App **只认 `Cmd` ∈ {2003, 2011, 8010, 8011}**、其余整条丢弃 ⇒ 新固件加项不会自动进 UI；设置改完是**累积后一条 `cmd=3021` 统一提交**，不是逐项写 | `docs/evidence/idgolive §5`、`docs/evidence/idgolive 附录B（HTTP 与 TCP 命令） §2` 的 3031/3021 两行 |
| §3.5 (c) BLE 命令表 | 修表：`BT_CMD_SYSTEM_POWER_DOWN/_HIBER` 行取值里 `down` 与 `hiber` 之间的裸竖线转义成 `\|`，该两列不再被这一行切成三列 | 纯排版修正：该行结论本轮未改，08 侧也未覆盖 `BT_CMD_*` 载荷表（只记了载荷是 NUL 结尾的 JSON 文本帧，见 `docs/evidence/idgolive §3` 第 10 条通道） |
| §3.5 (d) 属性表引言（新增） | 属性空间改为「四段 + 高位能力码 = **57 个码点**」，`0xD6xx` 段（6 个）此前整段漏掉；并声明十六进制全部按十进制重算（旧表一批错位） | `docs/evidence/idgolive 附录A（PTP 属性表） §0`、`§1`、`§2`，`docs/evidence/idgolive §9` 属性段行 |
| §3.5 (d) 属性表逐行 | 全表十六进制重算（`0xD700`→`0xD720`、`0xD790`→`0xD7B0`、`0xD811`→`0xD831`、`0xD81E`→`0xD83E` 等）；三重载码点由 `0xD7DB` 纠正为 **`0xD7FB`(55291)** 并注明旧值全仓不存在；`0xD834`/`0xD835` 标「各挂两个常量名」；补 `0xD72B`、`SUPPORT_PREVIEW`=`0xD7FF`、`0xD723` 的位标志指引、`0xD83E` 的黑名单豁免；`0xD5xx` 一批纠正为 `0xD6xx` | `docs/evidence/idgolive 附录A（PTP 属性表） §1`、`§2`，`docs/evidence/idgolive §9` 前两行 |
| §3.5 (e) 取值表 | 抗频闪/日期水印/视频尺寸三行的属性号从 `0xD5E6`/`0xD5E7`/`0xD5E5` 纠正为 `0xD606`/`0xD607`/`0xD605`；新增曝光补偿 `0xD723` 的**位打包**行（bit31 符号、bit30 = 除以 10、数值 `i & 0x00FFFFFF`） | `docs/evidence/idgolive 附录A（PTP 属性表） §1`、`§2`、`§3`、`docs/evidence/idgolive §9`「新发现」⑧ |
| §3.1 (c) / §3.5 (d) 附带纠正 | 两处按语料复核后改对：① 相机名黑名单计数 **34 → 33**（正文六处），`CameraProperties.java:1476` 的 `Arrays.asList` 实参数一遍只有 33 个字面量；② `PropertyId.java` 的常量区间确认为 `:5-55`（第 4 行是 `public class PropertyId {`，首个常量 `AP_MODE_TO_STA_MODE = 55291` 在第 5 行）；`docs/evidence/idgolive` 的 PTP 附录原先写 `:4-55`，已一并改回 | 语料 `_work/idgo_live_src/sources/com/icatch/golive/SdkApi/CameraProperties.java:1476`、`data/PropertyId/PropertyId.java:4`；① 与 `docs/evidence/idgolive §2.1`（标称「34 项」，其列出的字面量实为 33 个）不符，② 与该档案 附录 A §0 一致 |
| §3.6 鉴权与心跳 | 组播改为「两套实现（Java 只收 + native 收发）」，`MulticastLock` 的影响从「推断锁被拒绝」改成**静态不可证**；`simpleConfig` 签名与 key 的来源行号重写（含不传 key 的重载、16 字节有符号字面量、50 ms 轮询）；心跳补三档周期与 `connectionCheck` 第三个实参被 SDK 丢弃的 bug | `docs/evidence/idgolive §3`、`§3.1`、`§9`「UDP 组播」「新发现」⑥两行、`docs/evidence/idgolive 附录B（HTTP 与 TCP 命令） §5.1` |
| §3.7 实时预览 | 补「参数串由固件返回、App 还会降帧」；能力门 `SUPPORT_PREVIEW` 由 `0xD7DF` 纠正为 **`0xD7FF`(55295)**；副机 RTSP 明确三个 IC 类只回**裸 `rtsp://<ip>`**（无路径无端口）、路径由播放器侧补，并新增 §3.9 #7 与远端回放另拼一次 `"rtsp://192.168.1.1" + filePath` | `docs/evidence/idgolive §6.2`、`§9` 预览行与「副机才用 RTSP」行 |
| §3.8 文件传输 | `RANGE` 标为**大写** header 名且同一 header 被挂在 POST 上（大小写不敏感、无功能影响）；新增 PTP 侧独立续传 `ptp_getpartialobject`（两个 ABI 都有导出） | `docs/evidence/idgolive §9`「续传」行、本文 §3.1 断点续传行 |
| §3.9 待抓包清单 | 第 1 条重写：`3195/3196` 在本包里**零依据**（Java/`.so` 字符串/`.text` 立即数三类扫描全零命中），首选假设改 `15740`，并记 native 日志实证的四阶段名；第 3 条补 native `simpleconfig` 字段顺序；新增第 7 条「副机 RTSP 完整 URL」 | `docs/evidence/idgolive 附录C（原生层与 PTP-IP） §3.2`、`§3.5`，`docs/evidence/idgolive §8`、`§9` PTP/IP 端口行 |
| §4 #2 传输层 | 「至少四种」改为**至少六种**（补 PTP container over TCP/IP 与 PTP over USB 三种传输、UDP 组播），RTMP 上行列入；5678 反向 socket 纠正为「`HisiSocketServer` 监听、海思与 SigmaStar 共用」 | `docs/evidence/idgolive §3`、`docs/evidence/xtugo 附录C（SigmaStar 与播放层） §C.9` #3 |
| §4 #13 多通道 | 「idGoLive 同一设备 8 条通道」改为 **11 条**并列全，另注 `enableSocketIO` 是第 12 条「SDK 提供但 App 未接」 | `docs/evidence/idgolive §3` 与其末段 |
| §4 #15 / #16 | #15 从「根本没有型号表」改为「无能力注册表、**但有** 33 项名单」，并把「对三个 IP 并发 ping」纠正为「只 ping `.254`/`.169.1`，两路 ping **共享一个失败计数器**」，模型落成四层判定；#16 补「名单是全等比对、不是前缀匹配」 | `docs/evidence/idgolive §2`、`§2.3`、`§9` 对应两行 |
| §5 修正点表 | 「型号表」行补「18 是四张列表的并集」；新增两行 —— **base URL 注入**（官方自己 CGI 用探测 IP、8080/RTSP/缩略图用常量 IP，自研要统一）与**预览帧率**（官方在 App 侧改写，不复刻我方帧率偏高）；「下载续传」行补 XTU 8080 的 append 语义与 idGoLive 的 `ptp_getpartialobject` | `docs/evidence/xtugo 附录C（SigmaStar 与播放层） §C.9` #8、`docs/evidence/idgolive §6.2`、`§9` 预览行与 Range 行、`docs/evidence/xtugo §2.5` |
| §6 待真机验证 | #1/#2 从「端口来源未知」改为「常量已确证、只剩设备侧实际监听与跨 read 切帧率/心跳待验」；#4 补 R1 的两处图标证据；#6 改为「谁在听已确证、只剩相机侧触发条件与报文」；#7 假设排序改 `15740` 优先；#8 明确**静态不能判**（不采纳 08「锁不需要权限」的断言，它连正字都写成不存在的 `CHANGE_WIFI_MULTICAST_CAST`）；#9 补「App 只认 4 个 `Cmd` 已确证」；新增 #11 `pasttime` 单位、#12 `SS_IP` 非 final 的运行期改写路径 | `docs/evidence/tuwin 附录一（协议面） §8.1` C-4/C-5；`docs/evidence/xtugo §11`、`§10`；`docs/evidence/idgolive §8`、`§9` 组播行、`docs/evidence/idgolive 附录C（原生层与 PTP-IP） §3.5` |

