QS08 Recorder BLE Communication Protocol
Document Overview
| Item | Content |
|---|---|
| Version | V1.0 (2026-07-10) |
| Project Scope | `E:\codex\record` |
| Communication Roles | Mini Program as Central / Host; Recorder as Peripheral / Slave |
| Basis | Vendor document Recorder and App Communication Protocol (20260313) plus CB08 live sniffing and download validation |
| Validation Scope | Connection, battery level, file list, WAV download, frame CRC, byte order, and output file structure |
Protocol Summary
- The frame header LEN and CRC fields are both little-endian.
- In the file list, count / time / size are big-endian.
- CRC uses CRC-16/XMODEM.
- The full file download request frame for command 2-2 is 36 bytes.
- CB08 requires the entire 36-byte frame in a single GATT write. Splitting it into `20 + 16` bytes reliably returns “file does not exist”.
1. Scope and Conventions
This protocol is used by a WeChat Mini Program to control the CB08 recorder over BLE, receive real-time audio, read the device file list, and import recordings from the device as WAV or OPUS files.
If a multi-byte field is explicitly marked LE, parse it as little-endian. Integer fields in the file list structure use BE according to live packet captures. Unless otherwise stated, all command IDs are expressed in decimal.
| Term | Description |
|---|---|
| App / Mini Program | BLE Central. Actively scans, connects, and sends commands. |
| Dev / Recorder | BLE Peripheral. Provides the AE20 service and sends data back through Notify. |
| LE | Little Endian. Low byte first. |
| BE | Big Endian. High byte first. |
| Single GATT Write | One complete characteristic value written in a single `writeBLECharacteristicValue` call. |
2. BLE Transport Layer
| Object | UUID | Property | Direction / Usage |
|---|---|---|---|
| Service | `0xAE20` | Primary Service | Recorder business service |
| Characteristic | `0xAE21` | `WRITE_WITHOUT_RESPONSE` | App → Dev, sends protocol frames |
| Characteristic | `0xAE22` | `NOTIFY` | Dev → App, control responses, audio, list, and file data |
| Characteristic | `0xAE23` | `NOTIFY` | Dev → App, physical key events and recording status |
After a successful connection, AE22 must be subscribed first. AE23 carries key and recording-state messages, and should also be subscribed. AE22 and AE23 should use independent frame buffers to avoid byte interleaving between notification channels.
Android may actively negotiate MTU with `setBLEMTU`. On iOS, MTU is negotiated by the system. Regular commands may use `MTU - 3` as payload size, but the file import request 2-2 must follow the live-device single-write constraint described in Section 7.
3. Generic Frame Format
| Offset | Length | Field | Byte Order | Description |
|---|---|---|---|---|
| 0 | 1B | MAGIC | - | Fixed `0x5A` |
| 1 | 1B | SEQ | - | Packet sequence number, loops from `0` to `255` |
| 2 | 2B | CRC | LE | CRC-16/XMODEM over `LEN(2B) + DATA` |
| 4 | 2B | LEN | LE | Actual byte count of `DATA` |
| 6 | LEN | DATA | Depends on command | `[TYPE:1B][CMD:1B][PARAMS…]`; ACK may contain only `TYPE` |
3.1 CRC-16/XMODEM Parameters
| Parameter | Value |
|---|---|
| Polynomial | `0x1021` |
| Initial value | `0x0000` |
| RefIn / RefOut | `false / false` |
| XorOut | `0x0000` |
| Standard test vector | ASCII `123456789` → `0x31C3` |
CRC input must contain the two raw LEN bytes from the frame header followed by DATA. It must not include `MAGIC`, `SEQ`, or the `CRC` field itself.
3.2 DATA Types
| TYPE | Name | Content |
|---|---|---|
| 0 | Control Commands | Time, battery, capacity, firmware, authorization code |
| 1 | Real-Time Audio / Transcription | Start, audio data, stop, pause/resume, device state |
| 2 | File Operations | List, import, data, finish, delete, terminate |
| 3 | Key / Recording Control | Physical keys, app control, state, gain |
In the vendor document, Section 4 once described `TYPE=3` as ACK, while Section 7 defined it as key commands. Implementation should follow the Section 7 command table. If DATA contains only a single TYPE byte, treat it as ACK.
4. Control Commands (TYPE=0)
| CMD | Direction | Name | Parameters / Response |
|---|---|---|---|
| 0 | App → Dev | Sync Time | `year:2B LE + month/day/hour/minute/second`, each of the latter fields is 1B, total 7B |
| 1 | App → Dev | Get Capacity | None |
| 2 | Dev → App | Capacity Response | `remain:4B LE + total:4B LE`; vendor text marks the unit as 8 KB, current implementation displays it as 1 KB |
| 3 | App → Dev | Get Battery | None |
| 4 | Dev → App | Battery Response | `1B`: `0-100`; `110` means charging |
| 10 | App → Dev | Get Firmware Version | None |
| 11 | Dev → App | Firmware Version Response | `6B` ASCII, for example `V1.0.0` |
| 12 | App → Dev | Get Authorization Code | None |
| 13 | Dev → App | Authorization Code Response | Authorization code byte string |
5. Real-Time Audio Commands (TYPE=1)
| CMD | Direction | Name | Parameters / Response |
|---|---|---|---|
| 0 | App → Dev | Start Real-Time Transcription | None |
| 0 | Dev → App | Current Recording Filename | Sent before the device starts pushing audio |
| 1 | Dev → App | Real-Time Audio Data | Compressed audio byte stream; current model uses OPUS-family encoding |
| 2 | App → Dev | Stop Real-Time Transcription | None |
| 3 | App → Dev | Pause / Resume | `1B`: `0 = resume`, `1 = pause` |
| 4 | Dev → App | Device State | `1B`: `0 = resume`, `1 = pause`, `2 = stop` |
Real-time audio data may be saved directly as backup data. Actual speech-to-text requires backend OPUS decoding/transcoding plus streaming ASR. Permanent cloud service keys must not be written into the Mini Program.
6. File Commands (TYPE=2)
| CMD | Direction | Name | Parameters / Response |
|---|---|---|---|
| 0 | App → Dev | Get File List | None |
| 1 | Dev → App | File List Data | `count:4B BE + N x 28B entries` |
| 2 | App → Dev | Request File Import | `offset:4B LE + filename:24B` |
| 3 | Dev → App | Import Started | Actual imported filename |
| 4 | Dev → App | File Data | Audio file byte chunks |
| 5 | Dev → App | Import Finished | `1B` status code: `0 = done`, `1 = not found`, `2 = offset too large`, `3 = other stop` |
| 7 | App → Dev | Terminate Import | None |
| 8 | App → Dev | Delete Single File | Same 28B entry format as file list |
| 9 | App → Dev | Delete All Files | None |
| 10 | Dev → App | Delete All Response | `1B`: `0 = success`, `1 = fail`; some old firmware may not send this |
| 11 | Dev → App | Terminate Import Response | None |
| 12 | App → Dev | Segment Import | `start:4B LE + end:4B LE + filename` |
| 13 | Dev → App | Delete Single Response | `1B`: `0 = success`, `1 = fail`; some old firmware may not send this |
| 18 | Dev → App | File List Complete | `1B`: `0` means complete |
7. File List and Download Flow
7.1 File List Structure (CMD 2-1)
| Field | Length | Byte Order | Description |
|---|---|---|---|
| count | 4B | BE | Number of file entries in this frame, not the total number of all device files |
| time | 4B | BE | Recording duration in seconds; some firmware may use an absolute timestamp |
| size | 4B | BE | Compressed file size stored on the device, in bytes |
| name | 20B | UTF-8 / ASCII | Fixed-length field padded with NUL; long filenames truncate the extension |
The device may send the file list over multiple `CMD=1` frames. The app should accumulate all `N` records from each frame and return the full list after receiving `CMD=18`. To remain compatible with older firmware that does not send `CMD=18`, use an idle timeout of about 1.2 seconds after the final list frame.
A typical filename such as `note20260710-162938.opus` is 24 bytes long, while the file list field is only 20 bytes. The list therefore actually returns `note20260710-162938.` and the full extension must be reconstructed during download.
7.2 Standard Download Request (CMD 2-2)
| Parameter | Length | Byte Order | Example |
|---|---|---|---|
| offset | 4B | LE | First download request uses `00 00 00 00` |
| filename | 24B | ASCII / UTF-8 | `note20260710-162938.wav + 00` |
Mandatory Constraint: CMD 2-2 Must Be Written as One Complete Frame
The complete protocol frame is 36 bytes. Live testing on CB08 devices A and B confirmed the following:
- A single 36-byte GATT write succeeds.
- If the application layer splits the frame into `20B + 16B`, the device parses the filename incorrectly and returns `CMD=5 / code=1` (file does not exist).
Do not let a generic packet splitter fragment the `2-2` frame.
Download Procedure
- Step 1: Read the actual MTU
After connection, `getBLEMTU` may be used to obtain `ATT_MTU`. Regular commands may use `MTU-3`, but `2-2` still requires the forced single-write rule above.
- Step 2: Rebuild the target filename
Prefer `base.wav` so the recorder outputs a standard WAV file. If that fails, try `base.opus` and finally the raw truncated name from the file list. The `filename` field is fixed at 24 bytes and must be NUL-padded.
- Step 3: Send the complete request
Build `TYPE=2`, `CMD=2`, `offset=0`, and the 24-byte filename, then send the full frame including the 6-byte generic header in a single write to `AE21`.
- Step 4: Receive file data
After receiving `CMD=3`, create the transfer session and keep appending the body bytes from `CMD=4`. Each valid data chunk may refresh the idle timeout.
- Step 5: Handle the finish code
When `CMD=5` is received:
- `code=0`: write the file to disk.
- `code=1`: try the next candidate filename.
- `code=2`: reset offset.
- `code=3`: stop and report the transfer as interrupted or stopped.
7.3 Successful Request Frame Example
Real transmitted request frame for a successful download of `note20260710-162938.wav` captured on 2026-07-10:
5a 03 9e 20 1e 00 02 02 00 00 00 00 6e 6f 74 65 32 30 32 36 30 37 31 30 2d 31 36 32 39 33 38 2e 77 61 76 00
| Segment | Parsed Meaning |
|---|---|
| `5a` | MAGIC |
| `03` | SEQ = 3 |
| `9e 20` | CRC = `0x209E` stored in LE |
| `1e 00` | LEN = 30 |
| `02 02` | TYPE = 2, CMD = 2 |
| `00 00 00 00` | offset = 0 |
| Remaining 24B | filename = `note20260710-162938.wav` with trailing NUL |
7.4 WAV Output Validation
| Check Item | Live Result |
|---|---|
| Device / Address | `CB08 / D1:A1:C7:00:02:F2` |
| File List | 24 items |
| Requested File | `note20260710-162938.wav` |
| IMPORT_END | `code=0` |
| Received Bytes | `38,444B` |
| RIFF Declared Length | `38,444B`, matches actual length |
| Audio Parameters | PCM, 16 kHz, 16-bit, mono, 1.2 seconds |
A valid WAV file should satisfy `bytes[0:4] = RIFF` and `bytes[8:12] = WAVE`. The `size` field in the file list is the compressed size on the device, not the transcoded WAV size. WAV progress may be estimated by `duration x 32000B/s + 44`.
8. Key and Recording Control (TYPE=3)
| CMD | Name | Parameters / Response |
|---|---|---|
| 1 / 2 | Start Recording / Start Result | Result `1B`: `1 = success`, `2 = fail` |
| 3 / 4 | Save Recording / Save Result | Result `1B`: `1 = success`, `2 = fail` |
| 5 / 6 | Pause Recording / Pause Result | Result `1B`: `1 = success`, `2 = fail` |
| 7 / 8 | Resume Recording / Resume Result | Result `1B`: `1 = success`, `2 = fail` |
| 19 / 20 | Get / Response Recording Status | `1 = recording`, `2 = idle`, `3 = paused` |
| 21 / 22 | Get / Response Recording Duration | `duration:2B LE + currentSize:4B LE` |
| 23 / 24 | Get / Response Current Filename | Filename byte string |
| 25 / 26 | Get / Response Gain | `1 = low`, `2 = medium`, `3 = high` |
| 27 / 28 | Set Gain / Set Result | Set value `1-3`; result `0 = success`, `1 = fail` |
`CMD 1 / 3 / 5 / 7` may also be triggered by physical buttons and reported through `AE23`. Implementations should distinguish active command responses from device-generated events based on the characteristic source and the current session state.
9. Parsing, Timeout, and Compatibility Strategy
| Scenario | Recommended Handling |
|---|---|
| BLE Notify Fragmentation | Reassemble across notifications according to LEN. One notification may contain half a frame or multiple frames. |
| Concurrent AE22 / AE23 | Use independent `FrameParser` buffers and dispatch parsed frames through a unified layer. |
| CRC Error | After byte order is locked, discard corrupted frames and log the raw hex. |
| Unknown Byte Order | Probe LEN/CRC by sending a battery query after connection; once CRC matches, lock the byte order. |
| File List Without CMD=18 | After receiving list data, use an idle timeout of about 1.2 seconds and return the accumulated list. |
| File Download Without Data | After about 12 seconds of idle time, treat it as timeout. If `received = 0`, try another candidate filename. |
| Transfer Interrupted After Partial Data | Do not automatically switch filename. Report failure or support resume via offset. |
| User-Initiated Cancel | Send `CMD=7`, clear timers and promises, and ignore cancel-style popups. |
| Same-Name Local Files | Add `time / size / sequence` to the local cache path to avoid overwriting. |
10. Mini Program Implementation Mapping
| File | Responsibility |
|---|---|
| `utils/crc16.js` | CRC-16/XMODEM implementation |
| `utils/protocol.js` | Command constants, frame builder, stream parser, and field decoding |
| `utils/ble.js` | Scan, connect, MTU, notify subscription, AE21 write, and forced single-write for `2-2` |
| `utils/recorder.js` | High-level request/response, file list assembly, download session, and event handling |
| `pages/scan` | Discover and connect to recorder devices |
| `pages/files` | List, download, playback, export, and delete |
| `pages/transcribe` | Real-time audio, waveform, timer, pause/resume, and ASR text |
| `test/integration/ble_e2e.py` | End-to-end validation script over local Bluetooth |
11. Integration Acceptance Checklist
| Check Item | Pass Criteria |
|---|---|
| Scan | CB08 is found and the advertising payload includes service `AE20` |
| Connect | `AE21 / AE22` are discovered and AE22 Notify is enabled successfully |
| Battery | After request `0-3`, response `0-4` is received and the value is reasonable |
| File List | After `2-0`, `2-1` frames accumulate and the count matches the original app |
| Download Request | `2-2` full 36-byte frame is written once and the device returns `2-3` instead of directly `2-5/code1` |
| File Data | `2-4` frames continue arriving and end with `2-5/code0` |
| WAV | `RIFF/WAVE` header is correct, declared length equals actual length, and the file is playable |
| Disconnect | Download promise ends, timers are released, and the UI returns to disconnected state |
Appendix A. Key Status Codes
| Location | Value | Meaning |
|---|---|---|
| Battery `0-4` | `110` | Charging; UI may show `100%` or `Charging` |
| Real-Time State `1-4` | `0 / 1 / 2` | Resume / Pause / Stop |
| Import Finish `2-5` | `0 / 1 / 2 / 3` | Done / File not found / Offset too large / Other stop |
| Recording Status `3-20` | `1 / 2 / 3` | Recording / Not recording / Paused |
| Gain `3-26` | `1 / 2 / 3` | Low / Medium / High |
Safety Note
File deletion commands are destructive. End-to-end validation should perform read and download only by default, and should not execute `CMD=8 / CMD=9` unless validated on dedicated test recordings first. Before release, confirm that the big-endian 28-byte file entry matches the target firmware exactly.
评论