# YHY02 BLE 录音胸牌 — 项目状态

> **2026-08-18 更新**：Windows 验证已完成，结论见文末 §10。
> 方案 C（设备 WiFi 直传服务器）已被厂商否掉，Android App 是必需的。

> 交接自 2026-08-18 的 macOS session。接手方**零上下文**，本文自包含。
> 上一段工作全部在 macOS 上完成（BLE 实测 + 厂商工具反编译），
> 现在需要在 Windows 上跑厂商的桌面工具，验证剩下的关键问题。

---

## 1. 背景与目标

**公司**：PropertyLab（吉隆坡）。**仓库**：`petav3`（Laravel + Inertia + Vue 3）。

**目标**：用悦航益（Yuehangyi，简称 yhy）的 **YHY02 BLE 录音胸牌**，替代现在生产在跑的
**doway（dowayai）通话录音**链路。

**为什么换**：
- doway 现在是 6 跳：胸牌 →BLE→ doway 官方 App →HTTP→ 阿里云 OSS + dowayai 云 →我们 60s 轮询→ petav3
- 换成 YHY02 后是 3 跳：胸牌 →BLE→ 我们的 App →HTTPS→ petav3
- 少两跳厂商云，客户对话不再经第三方；且省掉 doway 的 SDK 费用
- doway 三个硬伤 YHY02 都解决了：**没有删除命令**（存储满了没法回收）、**没有断点续传**、**没有用户绑定**

**重要**：YHY02 的厂商**就是我们现有 WiFi showroom 胸牌的同一家（悦航益）**。
我们跟他们已有合作关系（petaV2 里有 `YHY_APP_SECRET` 等凭据），要文档/SDK 可以直接开口。

---

## 2. 已经实测确定的事实 —— 不要重新验证

设备：**`YHY02_5BA8E1`**
- SN = MAC = `C8478C5BA8E1`（广播里 `0x01` + 6 字节 MAC，MAC 即 SN）
- 广播 manufacturer data：company id `0x05F0`，7 字节 `01 c8 47 8c 5b a8 e1`
- macOS 上的 CoreBluetooth 地址：`FFDF7529-B59F-937C-A658-B240417D50AB`（Windows 上会是别的形式）
- 固件 **1.2.4**，电量 100%，eMMC **29.12 GB 总 / 29.12 GB 剩**（几乎全空）

**GATT 实测**（`FEE0` 主服务）：

| 特征 | 实机属性 | 用途 |
|---|---|---|
| FEE1 | `write, read` ← **没有 notify** | 设备信息 0x0020 |
| FEE2 | `write, read` ← **文档未记载但存在** | 录音状态（厂商 DLL 有 `ParseFEE2RecordingStatus`） |
| FEE3 | `write, read, notify` | 录音控制 0x0022 + 状态主动通知 |
| FEE4 | `notify` | 实时 AAC 音频流 0x0024 |
| FEE5 | `write, read, notify` | 本地控制 0x0030/0032/0033；**FEE6 命令的 ACK 也走这里** |
| FEE6 | `write, read, notify` | 历史文件同步 0x0040/0041/0043 |

**协商 MTU = 512**（文档写"默认 20"，实测远大于此，不是瓶颈）

**帧头 9 字节**（实测与官方 Wi-Fi+BLE 规范一致）：
```
[0]   Flag        0x01 = 设备→APP    0x11 = APP→设备
[1]   Encoding    0 = AAC   1 = LC3   2 = Opus     ← 实机恒为 0x00
[2:4] CMD         BE UInt16
[4:6] DataLength  BE UInt16
[6]   Sequence    UInt8
[7]   CRC8        可选（厂商自己的工具都会 "CRC mismatch (non-fatal)"，填 0 即可）
[8]   Padding     0
[9:]  payload
```

**音频（这条最重要）**：
- 实时流是 **AAC-LC / ADTS 封装 / 16000 Hz / 单声道 / ≈32.4 kbps**
- **不是 Opus**。之前那份 `bt-protocol-0808.pdf` 写 opus，是因为 opus 只是三种可选编码之一；
  当前固件默认输出 AAC，编码类型写在**每一帧的第 2 个字节**里
- FEE4 帧总长 = `9 + DataLength`，AAC 数据从偏移 9 开始（`ff f1 60 40`），**没有 status 字节**
  （规范 §2.3.3 说有，实测 347 帧证明没有；那句话疑似从 §2.3.5.2 文件传输那节复制来的）
- 实测 347 帧**零丢包、零 ADTS 长度不符**；剥掉 9 字节头首尾相接就是合法 `.aac`，
  `ffprobe` 判定 `aac / LC / 16000Hz / 1ch / 32429bps`，`ffmpeg` 能直接解出人声

**体积（按实测 32.4 kbps）**：10 分钟 ≈ 2.32 MB，50 分钟 ≈ 11.6 MB

**行为**：
- FEE4 **只在录音时推流**，1× 实时速率（边录边传，不是事后下载）
- 按录音键时 FEE3 主动推 `01 [录音中0/1] [2字节未知]`
  （观察值：`01 00 0000` 未录音 / `01 01 0009` 录音中9秒 / `01 01 0000` 刚开始）
- **AAC 在 Gemini 和 Deepgram 的官方支持格式里，不需要任何转码**

---

## 3. 两份厂商文档 —— 只信第二份

| 文件 | 状态 |
|---|---|
| `bt-protocol-0808.pdf` | ❌ **和这台机器完全不符**。GATT 写 FFF3/FFFC/FFF9/FFF0，帧头有 `aimt-0`(`61696d742d30`) 魔数，广播 14 字节。三项全错，描述的是别的产品 |
| `Recording Card Wi-Fi+BLE – Protocol Specification.pdf` | ✅ **这份才对**。逐条对上，包括广播 MAC 格式、设备名规则、FEE0~FEE6、9 字节帧头 |

**第二份文档已确认的三处错误**（可以反馈厂商）：
1. §2.1 把 **FEE1 标成 "Write / Notify"，实机只有 `write, read`** —— 照文档写会在 `start_notify(FEE1)` 直接抛 `CBATTErrorDomain Code=6`。设备信息要"写 0x0020 → 再 read FEE1"
2. §2.1 **漏了 FEE2**（实机存在，厂商自己的代码在解它）
3. §2.3.3 说 FEE4 帧 = `9 + 1(status) + DataLength`，**实测是 `9 + DataLength`，无 status 字节**

另有一处待确认：sync.json 示例 `fsize=133754, time=120` 算下来 8.9 kbps，
和实时流实测 32 kbps 差 3.6 倍 —— **存储文件是不是用更低码率？**

---

## 4. 厂商桌面工具反编译发现（关键）

工具：`~/Desktop/YHY BLE/Yuehanie.Tools.Desktop.exe`
- 173 MB **.NET 单文件自包含**（bundle v6.0，298 个内嵌文件）
- 已用 Python 解出 bundle，提取到 `~/ble-probe/unbundle/`：
  - `Yuehanie.Tools.dll`（628 KB，**主逻辑**）
  - `SharpJaad.AAC.dll`（AAC 解码库 —— 旁证音频就是 AAC）

**从 DLL 字符串挖出的、文档完全没写的命令**：

| 方法名 | 推测作用 |
|---|---|
| **`CmdHttpConfig`** | **配置 HTTP 上传** |
| **`CmdWsControl`** | **WebSocket 控制** |
| **`CmdWifiConfig`** + `BuildWifiSsidPayload` / `BuildWifiPasswordPayload` | **给设备配 WiFi（STA 模式，不是 SoftAP）** |
| **`CmdWifiUploadTime`** + `ApplyWifiUploadTime` | **定时 WiFi 上传** |
| `CmdDeleteFolder` / `CmdOta` / `CmdGetWifiName` | 删整个目录 / OTA / 读 WiFi 名 |
| 观察到但未命名的码：`0x0023`、`0x0034` | ? |

**🔴 这批命令凑在一起意味着：设备可能能"连门店 WiFi → 定时 HTTP 直传到我们服务器"，
完全不需要手机 App。** 如果成立，等于复刻我们现有 yhy WiFi 胸牌那条链路，
而 petav3 里 `/webhooks/yhy/push` 的接收端**已经在生产跑着**。
Android 端那一大块工作量可能根本不用做。

**另外两条实现级提示**：
- DLL 有 `FindAdtsFrames` / `FixAdtsFrameLength` / `IsValidFrameStart` / `ParseMultiFrameFileData`
  —— **厂商自己也要修复 ADTS 帧长和重新对齐帧边界**。实时流我们实测是干净的，
  但**历史文件那条路径大概率需要这套修复逻辑**
- `0x0033` 文档叫"本地录音存储开关"，DLL 里叫 `CmdSyncOnRecord`/`SetSyncOnRecordAsync`
  （录音时自动同步）—— 语义不一致，需向厂商确认

---

## 5. Windows 上要做的事（本次核心任务）

### 任务 A（最高优先）— 跑起厂商工具，重点看 WiFi / HTTP 配置

把 `Yuehanie.Tools.Desktop.exe` 跑起来连上胸牌，**逐个界面翻**，重点找：

1. **有没有"配置 WiFi"的入口**（填 SSID + 密码，让设备连门店路由器）
2. **有没有"上传地址 / 服务器地址 / HTTP 配置"的入口**
3. **有没有"定时上传"的设置**
4. 如果有 → **这是本次最大的收获**，意味着可以走"设备直传服务器"，不用做 Android App

同时观察：
- SoftAP + FTP 那条路（`0x0030` 开热点 → 连 `YHY02_5BA8E1` / 密码 `12345678` → FTP `192.168.1.1:21` admin/admin）实际速度多少
- 历史文件列表（sync.json）长什么样，真实文件名 / 大小 / 时长 / 码率
- 拉一个历史文件下来，`ffprobe` 看**存储文件的真实码率**（回答上面那个 8.9k vs 32k 的疑问）

**建议开 Wireshark + Windows BLE 抓包，或看工具自己的日志窗口** ——
DLL 里有大量日志字符串（`sync.json cmd=`、`ACK: status=`、`BLE 0x0041` 等），
说明工具会把帧交互打出来。**那些日志直接就是协议的真实样例。**

### 任务 B — 把 macOS 上没跑完的 probe4 跑掉

macOS 端写了 4 个只读探针（在 `~/ble-probe/`，Windows 上需要重写或从 Mac 拷）：

| 脚本 | 作用 | 状态 |
|---|---|---|
| `probe.py` | 扫描 + GATT 全量 + MTU | ✅ 已跑，结论见 §2 |
| `probe2.py` | 读所有可读特征 + 被动监听 notify | ✅ 已跑 |
| `probe3.py` | 抓 FEE4 实时流落盘成 .aac | ✅ 已跑，产物 `~/ble-probe/capture.aac`（22 秒真实人声） |
| `probe4.py` | `0x0020` 设备信息 → `0x0040` sync.json → `0x0041` 拉文件 | ⬜ **未跑完**（FEE1 notify 的 bug 已修，但还没重跑） |

`probe4.py` 已经修好了两处：按实机属性过滤 notify 订阅（不再盲信文档）、
FEE1 改成"写命令 → read 回响应"。**Windows 上可以用厂商工具替代它**，
或者装 `pip install bleak` 重跑（bleak 跨平台，Windows 走 WinRT 后端）。

### 任务 C（可选）— IL 反编译，挖 WiFi/HTTP 命令的确切字节格式

如果任务 A 确认了 WiFi/HTTP 直传可行，就需要确切的载荷格式。
Windows 上直接用 **ILSpy**（GUI，免费）打开 `~/ble-probe/unbundle/Yuehanie.Tools.dll`，
看这几个方法的实现：
```
BuildWifiSsidPayload   BuildWifiPasswordPayload   BuildWifiOperationPayload
BuildWifiUploadTimePayload   CmdHttpConfig 相关   CmdWsControl 相关
GetCharacteristicUuidForCmd  ← 这个直接给出 命令码→特征 的完整映射表
```
`GetCharacteristicUuidForCmd` 是最值钱的一个 —— 一次拿到全部命令码。

**安全边界（沿用）**：探测时只发读取类命令。
`0x0043` 删除、`0x0032` 格式化 eMMC、`0x0030` 开热点、OTA —— 不要随便发。

---

## 6. petav3 侧的集成方案（已设计完，未动一行代码）

**结论：管道后半段完全复用，分界线比想象中靠前。**

现有 doway 轮询命令 `app/Console/Commands/PollDowayaiRecordings.php` 拆开看：
```
遍历 TYPE_DOWAYAI devices          ← 换：上传时带 device_sn
DowayaiClient::listRecordings()    ← 换：手机端 BLE/FTP 取文件
existingExternalIds() 去重          ← 换：服务端 unique 索引兜底
DowayaiIngestPolicy::skipReason()  ← 保留（同样 <60s 过滤）
DowayaiClient::downloadAudio()     ← 换：手机端上传
━━━━━━━━━━ 分界线 ━━━━━━━━━━
MediaService::store()              ← 原样
createFromDowayai()                ← 只加一个 source 常量
ProcessCallRecording::dispatch()   ← 原样
ClickEventRecordingMatcher         ← 原样（按 admin_id + called_at 时间窗匹配，
                                      不依赖电话号码，所以换设备不影响客户配对）
```

**改动清单（零 migration）**：
| # | 改动 |
|---|---|
| A1 | 移植 agent API：`~/petaV2/routes/api-agent.php` + `Src\People\User implements JWTSubject`（petav3 已有 `tymon/jwt-auth` 依赖和 `config/jwt.php`，只是没接线） |
| A2 | `Device::TYPE_BADGE_BLE = 3`（`type` 是 tinyint，`secret` 已 nullable） |
| A3 | Devices 模块去 doway 化：`DevicesController` 4 处 + `StoreRequest:44,75` + `DeviceFormModal.vue:28` —— 把 `isDowayai` 改成「这个类型需不需要凭据」 |
| A4 | `CallRecording::SOURCE_BADGE_BLE = 6`（1–5 已占满） |
| A5 | **修 `CallImportMapper::dedupeKey()`**（`src/Call/Support/CallImportMapper.php:96`）—— 目前只有 `SOURCE_DOWAYAI` 走 `external_id` 分支，BLE 没有 caller/callee 会退化成 `hash(source:::calledAt)`，**两台胸牌同秒开录就撞唯一索引** |
| A6 | `POST /api/agent/v1/call-recordings` —— body 就是把分界线以下那段搬过来 + **SN 归属校验**（`Device::adminIdFor(TYPE_BADGE_BLE,$sn) === auth()->user()->admin?->id`，否则 A 销售能传 B 的录音） |
| A7 | Feature test（造假 multipart + 假 aac，不碰硬件） |
| A8 | nginx `client_max_body_size` + PHP `upload_max_filesize` ≥ 20M |

**幂等键**：`external_id = "{device_sn}:{file_name}"`
（`external_id` 是 varchar(100)，够；新设备文件名 `20250625/111642.aac` 不含设备标识，**必须拼 SN**）

**A1 和 A5 就算最后不上这个设备也是净收益** —— agent app 迟早要从 petaV2 切到 petav3。

---

## 7. 文件位置（macOS 端，需要的话从 Mac 拷）

```
~/Desktop/YHY BLE/
    Recording Card Wi-Fi+BLE – Protocol Specification.pdf   ← 正确的文档
    bt-protocol-0808.pdf                                     ← 错的，别看
    Yuehanie.Tools.Desktop.exe                               ← 厂商工具（173MB）

~/ble-probe/
    probe.py probe2.py probe3.py probe4.py    ← 只读探针（bleak，跨平台）
    capture.aac / capture.mp3                  ← 抓下来的 22 秒真实录音
    unbundle/Yuehanie.Tools.dll                ← 反编译目标（628KB）
    unbundle/SharpJaad.AAC.dll
```

petav3 相关（git 里）：
```
app/Console/Commands/PollDowayaiRecordings.php    ← 要替换的主体（284行）
app/Helpers/Calls/DowayaiClient.php               ← 要替换（275行）
src/Call/Support/DowayaiIngestPolicy.php          ← 保留复用
src/Call/Support/CallImportMapper.php:96          ← 必改（dedupeKey）
src/Call/Services/ClickEventRecordingMatcher.php  ← 保留，一行不改
src/Device/Device.php:25                          ← 加 TYPE_BADGE_BLE
src/Call/CallRecording.php:32                     ← 加 SOURCE_BADGE_BLE
```

---

## 8. 未决问题（按优先级）

1. **🔴 设备能不能 WiFi STA + HTTP 直传我们服务器？**（决定要不要做 Android App —— 本次 Windows 任务的核心）
2. **🟠 存储文件的真实码率**（sync.json 示例算出 8.9 kbps vs 实时流实测 32 kbps，差 3.6 倍）
3. **🟠 历史文件拉取的实测速率**（BLE 和 FTP 各是多少 kB/s）
4. **🟡 `0x0023` / `0x0034` 是什么命令**
5. **🟡 `0x0033` 到底是"本地存储开关"还是"录音时自动同步"**（文档和 DLL 说法不一致）
6. **🟡 设备是否只允许单个 BLE 客户端连接**（doway FW920 踩过：官方 App 占着就连不上）
7. **🟡 FEE3 状态载荷后两字节是什么**（疑似已录秒数）

---

## 9. 用户偏好（沿用）

- **中文回复**，不用 emoji（除非明确要）
- 大改动先出 plan 再动手，不要直接改代码
- `git add <具体文件>`，**绝不** `git add -A`
- commit / PR **不要任何 AI 署名**
- 本地 `.env` 绝不指向生产
- 不要起后台 dev server（内存会炸）


---

## 10. Windows 验证结果（2026-08-18，已完成）

原始记录：`~/Desktop/YHY BLE/WINDOWS-VALIDATION-2026-08-18.md`

### 🔴 方案 C 死了 —— Android App 是必需的

厂商书面回复：

> 这个 WIFI 连接只是建立一个从录音卡片到手机的 FTP 快传，不是通常我们用的那种连接路由的 WIFI 设置。

- 设备 WiFi 是 **SoftAP**，不是 STA。手机是 FTP 客户端，设备是局域网 FTP 服务端
- 手机连设备热点时通常上不了互联网 → WiFi/FTP 只解决"设备→手机"，不解决"设备→云"
- `0x0026 CmdHttpConfig` 只有常量定义：**无 payload builder、无调用点、无 URL 字段、无 UI**
- `WebSocketServiceStub` 是模拟实现，不是真网络客户端
- 结论：服务器地址 / HTTPS / 鉴权 / 断点续传 / 重试**全部由 Android App 负责**

**最终链路**：
```
YHY02 ──BLE 实时 AAC──> Android App ──HTTPS──> petav3
   └──SoftAP + FTP 历史文件──┘
```

### 已解决的悬案（§8 的编号）

| # | 结论 |
|---|---|
| 1 | ❌ 设备不能 WiFi 直传，见上 |
| 2 | ✅ **存储文件码率 32,288 bps ≈ 实时流 32.4 kbps**。文档示例算出的 8.9 kbps 是假数据 |
| 3 | ✅ **BLE 历史下载 12.38 kB/s = 音频实时产生速度的 3.1 倍**。FTP 速度仍未测 |
| 4 | ✅ `0x0023`=WiFi STA 配置(FEE2)、`0x0034`=WiFi 上传周期(FEE5) |
| 5 | ✅ `0x0033` = "录音时同步保存到 EMMC"(FEE5)，**布尔反转：`00`=开 `01`=关** |
| 7 | ✅ FEE3 末两字节 = **大端"已录秒数"**（实测 `01 01 0E27` → `01 01 0E2D`，间隔 6 秒） |

### 🔧 必须写进 Android 实现的坑

1. **必须用 GATT Write With Response**。Write Without Response 会漏掉传输握手/数据（macOS 端的 probe4.py 原本就是错的，已修）
2. **两条路径的帧长规则不同**：
   - FEE4 实时流：`总长 = 9 + DataLength`（无 status 字节）
   - FEE6 历史文件：`总长 = 9 + 1 + DataLength`（`payload[0]` 是 status，不计入 DataLength）
   - 这印证了规范 §2.3.3 那句话确实是从 §2.3.5.2 串过来的
3. **历史文件提取结果是 `00 + AAC + 00`**，首尾各去掉 1 字节后与索引大小完全一致
4. `0x0033` 的 GUI 状态**不可信** —— 设备信息解析里硬编码为"开启"，没读真实值
5. `0x0035`（读 WiFi 名）**固件 1.2.4 不响应**
6. Windows Bleak/WinRT 报 `MTU=23`，但 FEE4 实际收到的是完整 AAC 帧，不是 20 字节碎片 —— **WinRT 的 MTU 读数不可信，不要据此设计分帧**

### 实机数据

- 工具 `Yuehanie.Tools.Desktop.exe` v1.0.0 未签名；首次启动被 `ZoneId=3` 拦，需 `Unblock-File`
- 窗口标题 `YHY-BLE-WIFI-TOOLBOX`；GUI 有：设备信息 / 录音控制 / AP 模式 / 录音时同步保存 / 格式化 / BLE 历史文件 / FTP / 本地文件 / 通讯日志
- **GUI 里没有 WiFi STA SSID/密码、HTTP URL、上传服务器、定时上传任何控件**（代码里有属性和命令，但 `MainView` 没绑定 → 未完成/遗留代码）
- 录音停止后 EMMC 已用量 1.2 MB → 17.7 MB，**确认录音确实写本地存储**
- sync.json 解析出 5 条，UI 显示 3 个 AAC：
  ```
  C8478C5BA8E1_20260814_020735.aac    960 KB
  C8478C5BA8E1_20260814_021223.aac   15.2 MB
  C8478C5BA8E1_20260818_131505.aac  731,499 B
  ```
  注意：**下载后的文件名带 SN**，但设备上的路径是 `20260818/131505.aac`（folder 8 字节 + filename 10 字节）
- 完整下载 `20260818/131505.aac`：1,788 个 FEE6 帧，净耗时 57.691s，**12.38 kB/s**
- 清洁后：2,857 个 ADTS 帧、181.24 秒、AAC-LC/16000Hz/单声道/32,288 bps，ffmpeg 全量解码无错
- SHA-256 `2A3C7F42F3CBA8E4DFE7DC086F759B902C73FF085468DDEBE68763260532D786`

### 仍未验证

- FTP 实际下载速度
- 设备是否只允许单个 BLE 客户端连接
- Android 各品牌手机的热点切换行为（连设备热点 → 断开 → 恢复移动网络）

### 建议实施顺序（Windows session 给的，我认同）

1. Android：BLE 扫描 / 连接 / FEE3 状态监听 / FEE4 实时 AAC 接收
2. AAC 先可靠落盘，后台任务 HTTPS 上传 petav3，用 SN + 文件名 + 校验值保幂等
3. 断线重连、序号缺口检测、未完成任务恢复（防退后台/网络切换丢录音）
4. SoftAP + FTP 历史补传：下完校验 → 断热点 → 恢复互联网 → 上传
5. 服务端校验登录用户对设备 SN 的归属 + 限制大小/格式/重复导入
6. FTP 性能和各品牌热点切换真机专项测试
