# BLE Recorder File Protocol Draft 版本:V0.1 本文档定义 APP 通过 BLE 同步设备 TF 卡内容的私有协议草案,覆盖 TF 信息查询、文件列表、文件信息、文件传输、删除文件、格式化和录音状态上报。 ## BLE GATT 建议使用一个自定义 Service,两个 Characteristic。 | Characteristic | 方向 | 属性 | 用途 | | --- | --- | --- | --- | | CMD_RX | APP -> Device | Write 或 Write Without Response | APP 下发命令、传输配置、取消 | | CMD_TX | Device -> APP | Notify | 设备返回响应、连续文件数据 | 当前工程实现使用 16-bit UUID: | 项目 | UUID | | --- | --- | | Service | `0xFDA5` | | CMD_TX | `0xFDA6` | | CMD_RX | `0xFDA7` | 文件传输使用 Notify,不使用 Indicate。Indicate 每包都需要 ATT 确认,吞吐会明显下降。 ## 基础约定 - 所有多字节整数均为 little-endian。 - 所有结构体均为 packed。 - 每个 BLE value 均以 `ble_rec_pkt_hdr_t` 开头。 - 请求和响应不再用 `type` 区分,由 BLE 方向和 `cmd` 决定。 - 文件查询、传输、删除固定在 TF 卡 `\REC\` 目录下。 - APP 只传文件名,不传路径,例如 `MIC0016.OGG`。 - 第一版文件名固定使用 8.3 短文件名,建议大写 ASCII,例如 `MIC0016.OGG`。 - 所有变长 `name[]` 均必须带字符串结束符 `\0`。 - `name_len` 表示 `name[]` 总长度,包含结尾 `\0`。例如 `MIC0016.OGG` 的 `name_len = 12`。 - 接收方应同时校验 `name_len` 和结尾 `\0`,两者任一异常都按参数错误处理。 - `name_len` 最小值为 2,最后一个字节必须是 `0x00`,前面的文件名内容不能包含 `\` 或 `/`。 - 协议版本不在线协商,APP 和固件按项目版本配套发布。后续新增字段尽量只在结构体末尾扩展。 结构体说明: - 文档中的 packed 结构体表示 BLE 线格式,不要求 APP 或固件直接对收包 buffer 做结构体指针强转。 - 解析 `u16/u32` 时应按 little-endian 从字节流读取,避免非对齐访问问题。 - `hdr.len` 表示 payload 长度,即 `BLE value 总长度 - sizeof(ble_rec_pkt_hdr_t)`。 - 变长数组所在结构体的实际长度需要按字段长度计算,不能使用 `sizeof()` 直接得到完整包长。 示例宏: ```c #ifndef PACKED #define PACKED __attribute__((packed)) #endif ``` ## 包头 包头保留 `magic/cmd/seq/len`,用于快速识别协议包、匹配请求响应、检查 payload 长度。 ```c #define BLE_REC_PKT_MAGIC 0xA5 typedef struct PACKED { u8 magic; // BLE_REC_PKT_MAGIC u8 cmd; // BLE_REC_CMD_xxx u8 seq; // 发送方独立递增序号 u16 len; // payload 长度,不包含本包头 } ble_rec_pkt_hdr_t; ``` 约定: - APP -> Device 和 Device -> APP 两个方向的 `seq` 独立递增。 - `seq` 为 u8,递增到 0xFF 后回绕到 0x00。 - 设备响应不回填 APP 请求的 `seq`,而是使用设备侧自己的响应 `seq`。 - 第一版协议按单事务处理:APP 发出请求后,等待对应 `cmd` 的设备响应,再发下一个普通请求。 - 录音状态变化时的 `REC_INFO` 和录音停止后紧随的 `FILE_LIST` 是设备主动 Notify,不对应 APP 请求。 - 文件传输 DATA 使用设备侧连续递增 `seq`,数据顺序和缺口主要由 `file_ofs` 判断。 - `len` 必须等于 BLE value 长度减 `sizeof(ble_rec_pkt_hdr_t)`。 - 收到 `magic` 不匹配或 `len` 不匹配的包,直接丢弃。 - 普通命令建议 APP 使用 3 秒超时;超时后可重新发送当前命令。 ## MTU 和数据长度 BLE ATT Notify/Write 的应用 value 最大长度通常为: ```text ble_value_max = att_mtu - 3 ``` 协议本身不再协商 `max_payload`。设备端从 BLE 栈获取当前 MTU 后自行计算单包最大 data 长度。 文件 DATA 包最大数据长度: ```text data_max = att_mtu - 3 - sizeof(ble_rec_pkt_hdr_t) - sizeof(action_cmd) - sizeof(file_ofs) - sizeof(control) - sizeof(sum) data_max = att_mtu - 16 ``` 例:`att_mtu = 247` 时,`data_max = 231`。 设备实现时可再取 `min(data_max, 512)`,避免文件系统读缓存过大。 ## 命令总览 | Cmd | 名称 | 说明 | 第一版建议 | | ---: | --- | --- | --- | | 0x01 | REC_INFO | 查询 TF 信息和录音状态;录音状态变化时设备主动上报 | 必做 | | 0x02 | FILE_LIST | 获取完整文件列表 | 必做 | | 0x03 | FILE_INFO | 查询指定文件信息 | 可选 | | 0x04 | FILE_TRANSFER | 文件传输,设备连续 Notify | 必做 | | 0x05 | FILE_DELETE | 删除文件 | 第二步 | | 0x06 | REC_FORMAT | 格式化 TF 卡 | 第二步 | | 0x07 | RTC_SYNC | APP 同步设备 RTC 时间 | 必做 | 说明: - `FILE_INFO` 不是首版必需,因为 `FILE_LIST` 已返回文件大小和时间;保留它是为了 APP 删除/下载前单独刷新文件状态。 - 删除和格式化属于危险操作,建议等列表和传输稳定后再接。 ## 状态码 响应结构体中的 `status` 使用以下取值。 ```c enum { BLE_REC_ST_OK = 0x00, BLE_REC_ST_PARAM_ERR = 0x01, BLE_REC_ST_BUSY = 0x02, BLE_REC_ST_NO_CARD = 0x03, BLE_REC_ST_FS_ERR = 0x04, BLE_REC_ST_FILE_NOT_FOUND = 0x05, BLE_REC_ST_ACCESS_DENIED = 0x06, BLE_REC_ST_UNSUPPORTED = 0x07, BLE_REC_ST_NO_SPACE = 0x08, BLE_REC_ST_CANCELLED = 0x09, }; ``` ## 0x01 REC_INFO 查询 TF 卡信息和录音状态。容量单位为 KB;录音开始、停止时,设备也使用此响应结构主动 Notify。 ```c #define BLE_REC_CMD_INFO 0x01 typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_INFO, len = 0 } ble_rec_info_req_t; typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_INFO u8 status; // BLE_REC_ST_xxx u8 fs_type; // 1:FAT12 / 2:FAT16 / 3:FAT32 / 4:exFAT u32 total_kb; // TF 总容量,单位 KB u32 used_kb; // 已使用容量,单位 KB u8 rec_state; // 0:已停止 / 1:录音中 } ble_rec_info_rsp_t; ``` `u32` KB 单位最大可表示约 4TB,足够覆盖当前 128MB TF 卡和后续常见容量。 `used_kb` 建议按 `total_kb - free_kb` 计算。如果当前文件系统接口暂时无法快速获取剩余空间,可以后续补实现,但协议字段保持不变。 `rec_state` 始终有效。如果 `status != BLE_REC_ST_OK`,APP 仍可更新录音状态,其余信息字段按 0 处理。 录音状态变化时,设备主动发送 `REC_INFO`,`status = BLE_REC_ST_OK`。其中容量信息取最近一次成功查询的结果,APP 应以 `rec_state` 为主要关注字段。录音停止状态发送成功后,设备主动发送完整 `FILE_LIST`,用于刷新新生成的录音文件。 ## 0x02 FILE_LIST 获取 `\REC\` 目录下的完整文件列表。APP 只发一次请求,设备固定返回所有文件条目;如果一包放不下,设备连续 Notify 多个 FILE_LIST 响应包,直到 `eof = 1`。 文件顺序由 FAT 目录项扫描顺序决定,不保证按文件名或时间排序。APP 如需排序,建议本地排序。 ```c #define BLE_REC_CMD_FILE_LIST 0x02 typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_FILE_LIST, len = 0 } ble_rec_file_list_req_t; typedef struct PACKED { u32 size; // 文件大小 u32 ftime; // FAT 时间,格式同 fs_get_ftime() u8 name_len; // name[] 总长度,包含结尾 '\0' u8 name[]; // 文件名,变长,以 '\0' 结尾 } ble_rec_file_item_t; typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_FILE_LIST u8 status; // BLE_REC_ST_xxx u8 item_count; // 本包 item 数量 u8 eof; // 1: 所有文件列表返回结束 u8 items[]; // ble_rec_file_item_t 数组,逐项变长解析 } ble_rec_file_list_rsp_t; ``` 建议: - 默认固定扫描 `\REC\` 目录,过滤隐藏/系统文件。 - 如果 `\REC\` 目录不存在,返回 `status = BLE_REC_ST_OK`、`item_count = 0`、`eof = 1`。 - 如果 TF 卡不存在,返回 `status = BLE_REC_ST_NO_CARD`、`item_count = 0`、`eof = 1`。 - 设备每个响应包尽量填满当前 MTU 可容纳的 item。 - 单个 `ble_rec_file_item_t` 不跨 BLE 包;如果当前剩余空间放不下完整 item,则放到下一个 FILE_LIST 响应包。 - 每个 item 的实际长度为 `sizeof(size) + sizeof(ftime) + sizeof(name_len) + name_len`,其中 `name_len` 包含结尾 `\0`。 - APP 解析时应同时校验 `hdr.len` 和 `item_count`,避免异常包导致越界解析。 - 如果文件列表需要多包返回,除最后一包外 `eof = 0`,最后一包 `eof = 1`。 - 删除或新增文件后,APP 重新发送一次 FILE_LIST 获取完整列表。 ## 0x03 FILE_INFO 查询指定文件信息。该命令可选,首版可以先不实现。 ```c #define BLE_REC_CMD_FILE_INFO 0x03 typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_FILE_INFO u8 name_len; // name[] 总长度,包含结尾 '\0' u8 name[]; // \REC\ 目录下的文件名,以 '\0' 结尾 } ble_rec_file_info_req_t; typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_FILE_INFO u8 status; // BLE_REC_ST_xxx u32 size; // 文件大小 u32 ftime; // FAT 时间 } ble_rec_file_info_rsp_t; ``` 如果 `status != BLE_REC_ST_OK`,APP 只处理 `status` 字段,其余字段按 0 处理。 ## 0x04 FILE_TRANSFER 文件传输使用一个命令,通过 `action_cmd` 区分配置、数据、取消。 核心策略: - APP 发一次 SETUP。 - 设备返回 SETUP_RSP 后连续 Notify DATA。 - APP 不逐包请求数据。 - 设备同一时间只允许一个文件传输,DATA/CANCEL 默认作用于当前传输。 - 如果 APP 发现 offset 缺口,发送 CANCEL,然后重新 SETUP,从期望 offset 继续。 ### 结构体 ```c #define BLE_REC_CMD_FILE_TRANSFER 0x04 enum { BLE_REC_TRANS_SETUP = 0x01, // APP -> Device,设置传输配置 BLE_REC_TRANS_SETUP_RSP = 0x02, // Device -> APP,配置响应 BLE_REC_TRANS_DATA = 0x03, // Device -> APP,文件数据 BLE_REC_TRANS_CANCEL = 0x04, // 双向,取消/中断传输 }; enum { BLE_REC_DATA_CONTINUE = 0x00, // 继续 BLE_REC_DATA_END = 0x01, // 本包后传输结束 }; typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_FILE_TRANSFER u8 action_cmd; // BLE_REC_TRANS_xxx union { /* BLE_REC_TRANS_SETUP: APP -> Device */ struct PACKED { u32 start_ofs; // 起始偏移,断点续传时使用 u8 name_len; // name[] 总长度,包含结尾 '\0' u8 name[]; // \REC\ 目录下的文件名,以 '\0' 结尾 } setup; /* BLE_REC_TRANS_SETUP_RSP: Device -> APP */ struct PACKED { u8 status; // BLE_REC_ST_xxx u32 file_size; // 文件总大小 u32 start_ofs; // 实际起始偏移 u32 ftime; // FAT 时间 u16 data_len; // 设备实际采用的单包 data 长度 } setup_rsp; /* BLE_REC_TRANS_DATA: Device -> APP */ struct PACKED { u32 file_ofs; // 本包 data 在文件中的偏移 u8 control; // BLE_REC_DATA_CONTINUE / BLE_REC_DATA_END u16 sum; // 本包校验和,计算时 sum 字段按 0 处理 u8 data[]; // 文件数据 } data; /* BLE_REC_TRANS_CANCEL: 双向 */ struct PACKED { u8 reason; // 0:用户取消 / 1:offset异常 / 2:超时 / 3:设备错误 } cancel; }; } ble_rec_file_transfer_t; ``` SETUP_RSP 说明: - 如果 `status != BLE_REC_ST_OK`,APP 只处理 `status` 字段,其余字段按 0 处理。 - 如果 `status == BLE_REC_ST_OK`,APP 保存 `file_size/start_ofs/data_len`,后续 DATA/CANCEL 都默认属于当前传输。 - 当前传输结束、取消、超时或 BLE 断开后,设备清空当前传输状态;断点续传需要重新 SETUP。 DATA 实际数据长度: ```text data_actual_len = hdr.len - sizeof(action_cmd) - sizeof(file_ofs) - sizeof(control) - sizeof(sum) data_actual_len = hdr.len - 8 ``` APP 必须使用 `data_actual_len` 解析 `data[]`,不要使用 SETUP_RSP 里的 `data_len` 作为每包固定长度。最后一包通常会小于 `data_len`。 ### DATA 校验和 `sum` 是一个轻量 sanity check,不用于替代 BLE 链路层 CRC。 建议算法: ```c u16 sum = 0; for (i = 0; i < packet_len; i++) { sum += packet[i]; } ``` 校验范围为整个 BLE value,即从 `hdr.magic` 到 `data[]` 最后一个字节。计算时 `sum` 字段两个字节按 0 处理。APP 收到 DATA 后按同样方式计算,结果一致才写入本地文件。 如果实测 CPU 开销影响吞吐,可以后续约定 `sum = 0` 表示不启用 DATA 校验和;第一版建议先保留,方便调试。 ### 设备发送策略 - 如果 `\REC\` 目录不存在或目标文件不存在,SETUP_RSP 返回 `BLE_REC_ST_FILE_NOT_FOUND`。 - SETUP 成功后,设备尽快连续发送 DATA。 - 能发就发,直到 BLE TX 队列满、文件结束、收到 CANCEL 或文件系统错误。 - 如果传输过程中发生文件系统错误,设备发送 `BLE_REC_TRANS_CANCEL`,`reason = 3`。 - BLE TX 队列满时不要忙等,等 notify complete/tx credit 回调后继续发送。 - 每次从 TF 读取的长度不超过 `data_len`,复用一个静态缓冲。 - 长文件传输循环中定期喂狗。 - 最后一包 DATA 的 `control = BLE_REC_DATA_END`。如果文件大小刚好按 data_len 对齐,可以发送一个 0 字节 END 包。 ### APP 接收策略 - 按 `file_ofs` 写入本地文件。 - 如果收到重复 DATA,可按 offset 去重。 - 如果 `file_ofs` 不等于期望 offset,发送 CANCEL,然后重新 SETUP,从期望 offset 继续。 - 如果 APP 超过 5 秒没有收到新的 DATA 或 CANCEL,可认为传输超时,重新 SETUP 从已保存 offset 续传。 ## 0x05 FILE_DELETE 删除指定文件。 ```c #define BLE_REC_CMD_FILE_DELETE 0x05 #define BLE_REC_DELETE_CONFIRM 0x44454C21 // "DEL!" typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_FILE_DELETE u32 confirm; // BLE_REC_DELETE_CONFIRM u8 name_len; // name[] 总长度,包含结尾 '\0' u8 name[]; // \REC\ 目录下的文件名,以 '\0' 结尾 } ble_rec_file_delete_req_t; typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_FILE_DELETE u8 status; // BLE_REC_ST_xxx } ble_rec_file_delete_rsp_t; ``` 安全要求: - 录音中返回 `BLE_REC_ST_BUSY`。 - 文件传输中返回 `BLE_REC_ST_BUSY`,或要求 APP 先 CANCEL。 - 如果 `\REC\` 目录不存在或目标文件不存在,返回 `BLE_REC_ST_FILE_NOT_FOUND`。 - APP UI 侧必须二次确认。 ## 0x06 REC_FORMAT 格式化 TF 卡。 ```c #define BLE_REC_CMD_FORMAT 0x06 #define BLE_REC_FORMAT_CONFIRM 0x464D5421 // "FMT!" typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_FORMAT u32 confirm; // BLE_REC_FORMAT_CONFIRM u8 fs_type; // 0: 自动 } ble_rec_format_req_t; typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_FORMAT u8 status; // BLE_REC_ST_xxx } ble_rec_format_rsp_t; ``` 安全要求: - 录音中返回 `BLE_REC_ST_BUSY`。 - 文件传输中返回 `BLE_REC_ST_BUSY`。 - 低电量返回 `BLE_REC_ST_ACCESS_DENIED`。 - TF 卡不存在返回 `BLE_REC_ST_NO_CARD`。 - APP UI 侧必须二次确认。 ## 0x07 RTC_SYNC APP 将本地时间同步到设备 RTC。设备后续创建录音文件时,使用 RTC 时间生成 FAT 创建时间。 ```c #define BLE_REC_CMD_RTC_SYNC 0x07 typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_RTC_SYNC u16 year; // 1980~2106 u8 month; // 1~12 u8 day; // 1~31,设备按年月校验 u8 hour; // 0~23 u8 minute; // 0~59 u8 second; // 0~59 } ble_rec_rtc_sync_req_t; typedef struct PACKED { ble_rec_pkt_hdr_t hdr; // cmd = BLE_REC_CMD_RTC_SYNC u8 status; // BLE_REC_ST_xxx } ble_rec_rtc_sync_rsp_t; ``` 说明: - 时间按设备本地时间理解,不做时区字段协商。 - 建议 APP 在 BLE 连接成功后主动同步一次 RTC,再查询/下载文件。 - 如果设备 RTC 未同步或时间无效,录音文件创建时间会回退到旧的递增时间策略,避免写入非法 FAT 时间。 ## 典型流程 ### 枚举文件 1. APP 使能 CMD_TX Notify。 2. APP -> `RTC_SYNC`。 3. APP -> `REC_INFO`。 4. APP -> `FILE_LIST`。 5. 设备连续返回 FILE_LIST 响应包,APP 收集 item,直到 `eof = 1`。 ### 下载文件,最高速度模式 1. APP -> `FILE_TRANSFER / SETUP`,`start_ofs = 0`。 2. 设备 -> `FILE_TRANSFER / SETUP_RSP`。 3. 设备连续 Notify `FILE_TRANSFER / DATA`。 4. APP 按 offset 写文件。 5. APP 收到 `control = BLE_REC_DATA_END` 后,确认本地文件大小等于 `file_size`。 ### 断点续传 1. APP 本地已有 `saved_offset`。 2. APP -> `FILE_TRANSFER / SETUP`,`start_ofs = saved_offset`。 3. 设备从该 offset 开始连续发送 DATA。 ### 删除文件 1. APP UI 二次确认。 2. APP -> `FILE_DELETE(confirm = "DEL!")`。 3. 设备返回 OK 后,APP 重新 `FILE_LIST`。 ### 格式化 1. APP UI 二次确认。 2. APP -> `REC_FORMAT(confirm = "FMT!")`。 3. 成功后 APP 重新 `REC_INFO` 和 `FILE_LIST`。 ## 第一版实现建议 第一版建议只实现: - `REC_INFO` - `RTC_SYNC` - `FILE_LIST` - `FILE_TRANSFER` 之后再补: - `FILE_INFO` - `FILE_DELETE` - `REC_FORMAT` 这样可以先把 APP 浏览和高速下载主链路跑通,再加危险操作。