




ATS3089P 离线日志:USB CDC 与 BLE 无线导出
更新日期:2026-10-23。目标工程:`application/bt_watch`,目标板:`ats3089p_dev_watch`。
本文说明离线日志的采集、循环存储、稳定快照和两种传输方式,并给出 PC 操作、BLE 二进制协议与开发入口。原有 USB CDC 导出继续使用;新增 BLE 导出复用同一份离线日志,PC 不需要 USB 数据线或 COM 口。已有 USB U 盘快照方式也继续保留。
这里的"离线"指设备平时自行保存日志,导出不依赖互联网。BLE 传输的是此前采集到的文本快照,并不是通过蓝牙实时转发每一行 `printk`。
- 功能范围与选择
| 方式 | 连接条件 | PC 入口 | 特点 |
| --- | --- | --- | --- |
| USB CDC 有线 | USB 数据线,设备枚举为 CDC COM 口 | `offline_log_cli.py --mode cdc` | 沿用现有命令和文本帧协议,可明确指定 COM |
| BLE 无线 | PC BLE 适配器、驱动正常,设备正在广播且可连接 | `offline_log_ble.py export` | 新增独立 GATT 服务,以通知分包和累计 ACK 传输 |
| USB MSC 快照 | 已进入 U 盘模式,存在有效 `OFFLOG.TXT/OFFLOG.CRC` | `offline_log_cli.py --mode usb` | 保留原有方式;读取的是进入 U 盘时生成的快照 |
三条路径共享存储与快照资源。同一时刻只执行一个导出任务,重复请求返回忙;不为 BLE 再分配一份 512 KB 快照,也不增加一套导出线程。USB CDC 旧工具和已有使用方法无需修改。
BLE `export` 导出当前完整日志快照;新增 `start` 明确清除旧记录并开始一次新的采集。没有目录枚举、指定日期下载、断点续传或自动重传。导出本身不删除 Flash 中的历史日志,只有主动执行 `start` 或 UART `app log clear` 才清除本功能的旧记录。
- 设备如何保存日志
2.1 采集与循环存储
```text
设备中的 printk / printf / 控制台输出
│ 短临界区复制,不在打印处等待 Flash
▼
64 KB RAM 队列
│ 后台线程:足够一条记录,或等待约 5 秒
▼
NAND 后半区 512 KB 循环日志
│ 导出请求:提交请求时已有的队列数据
▼
按记录序号复制为 PSRAM 稳定快照
├── USB CDC:长度正文 + CRC32 结束帧
├── BLE:BEGIN / DATA / END + 累计 ACK
└── USB MSC:OFFLOG.TXT / OFFLOG.CRC
```
日志区域来自原有 `runtimelog` 分区,不改变录音 FAT 分区容量:
| 区域 | 本板地址 | 容量 | 用途 |
| --- | --- | --- | --- |
| SDK ACTLOG | `0x4D00000` | 512 KB | 保留 SDK 原有运行日志 |
| 本功能离线文本区 | `0x4D80000` | 512 KB | 原始文本循环记录 |
| 原录音 FAT | `0x4E00000` 起 | 沿用工程配置 | 录音文件与 MSC 导出快照 |
离线区共 256 条记录,每条 2048 字节,包括 32 字节头和最多 2016 字节正文。纯日志最大约 504 KB;导出时追加 `offline-log snapshot` 状态尾部,整个快照最多 512 KB。保留时长由日志量决定,不是固定保存几天。
记录头含格式版本、长度、记录序号、启动编号、运行毫秒和 CRC32。后台写入并刷新、读回检查后,才从 RAM 队列消费原文。区满后覆盖最旧记录。重启扫描验证记录,跳过坏页,不因为单页损坏就清空全部历史。
队列满时丢弃新来的日志字节,并增加 `dropped`;Flash 写入失败会增加 `errors`,保留待写内容并限速重试。USB 与 BLE 导出均不会修复此前已丢失的日志,可通过快照尾部和 `app log status` 判断采集完整性。
2026-10-23 本轮优化把 RAM 队列从 16 KB 增加到 64 KB,以容纳启动和突发调试输出。后台存储线程因唤醒 NAND 产生的 `spinand power on`、SPI 时钟等输出也纳入采集;这类自身输出不会单独触发"写日志又产生日志"的无限刷盘。队列容量仍然有限,持续输出超过存储能力时仍应检查 `dropped`,不能仅凭传输 CRC 正确判断采集完整。
2.2 为什么需要稳定快照
BLE 比 Flash 读取慢,传输期间设备还会持续打印。若直接边读循环区边发送,发送到后面时前面的页可能已被覆盖,导出文件就会混合不同时间点的数据。
本实现先把请求时已接收的队列数据提交到 Flash,在同一个存储线程中暂时停止本模块的 Flash 写入,按记录序号读取并验证,把正文复制到已有的 512 KB PSRAM `snapshot`。复制结束即恢复正常存储;接下来的 USB/BLE 传输只读取这份快照。
"冻结"只覆盖快照复制阶段,不会让 Flash 在整个 BLE 发送过程中停止保存。打印采集仍可入 RAM 队列;快照复制期间队列也有容量上限。快照之后的新日志留待下次导出,当前传输的总长度和 CRC32 保持不变。
快照尾部的 `boot`、`uptime_ms`、`enabled`、`stored_bytes`、`dropped`、`errors`、`last_error` 与正文一起参加导出 CRC 校验。电脑文件名中的时间来自 PC,不表示设备已经具有正确的实时时钟。
2.3 每行时间戳与采集会话
v5 固件在**采集时**为新日志的每个非空行添加时间。校时后使用与电脑相同的本地钟面格式,例如:
```text
18:30:01.168 app wake ui
18:30:01.187 Thread 0x2ffb9d40 ui_service run time 13 ms
18:30:01.209 viewI346: view 94 defocused
```
`HH:MM:SS.mmm` 表示本地时钟的时、分、秒、毫秒,小时范围为 00~23。设备记录校时基准和当时的 64 位单调运行时间,此后按运行时间增量计算采集时刻,避免把稍后的 Flash 提交时间或 BLE 到达电脑时间写成日志时间。USB CDC、BLE、MSC 导出共用同一份正文,因此时间戳一致;导出通路不会重新给每行打时间。
设备启动时会只读检查有效 RTC,存在可信日期时间时用其初始化日志时钟;不会为了日志时间而修改系统 RTC。需要按当前电脑时间校准时,推荐 BLE `start`:同一连接先同步 PC **本地时间**,收到校时成功后才清旧并开启采集。也可以独立执行 `sync-time`,只校时、不删除记录。PC 时区决定同步的钟面值,例如 Windows 设置为中国标准时间时,采集行显示北京时间。
普通串口助手显示的 `18:30:01.237` 是 **PC 接收软件按收到的数据批次添加**的时钟,未由设备原样发送。截图中多行都标为 `.237`,而设备逐行采集的时间之间仍可相差几毫秒。校时能统一钟面基准,但串口排队、PC 接收批次和 BLE 校时传输延迟仍会造成毫秒差异,不能保证每行与串口助手后加的时间完全相同,也不能声称离线文件复制了 PC 已有的接收时间戳。对比时应同时检查业务内容与设备采集间隔,区分设备采集时间和 PC 接收时间。
没有有效 RTC 且尚未校时时,明确保留 `+HH:MM:SS.mmm` 运行时间,避免把开机后的第 9 分钟伪装为实际钟面时间;该格式小时不按天回绕。启动标记 `=== BOOT ... ===` 和会话标记 `=== CAPTURE START boot=... ===` 中的启动编号用于区分启动。旧的无时间戳文本或 `+uptime` 原文已经落盘,不会在 `export` 时追溯改写;更新固件后先 `start`,再复现问题,才能得到本轮完整的本地时间记录。
`start` 串行清理本离线区 Flash 和旧 RAM 队列,再开启采集;完成确认后才复现问题。它不清理录音 FAT、SDK 原有日志区域或电脑上已导出的 TXT。普通 `export` 只生成快照,可重复导出并保留采集历史。
队列满后会在恢复采集时追加 `offline-log GAP dropped_bytes=...`,提示该处发生原文字节丢弃,避免把前后断裂的日志误读为连续原文。v5 状态和快照尾部使用 `timestamp=per_line clock_synced=...`:`clock_synced` 标明当前是否已有时钟基准,只描述当前状态,不表示旧历史行已经补齐时间。UART 状态还提供 `clock_source`:0 为无基准,1 为 PC/手工校时,2 为有效 RTC 初始化。v4 增加的 `ram_capacity=65536`、`suppressed` 和 `self_captured` 继续保留:`suppressed` 是清理/开始新采集屏障期间主动跳过的原文字节数,`self_captured` 是已接收的存储线程自身原文字节数。`dropped/errors/self_captured` 在启动或最近一次清理/新会话后重新统计;`suppressed` 可用于说明清理操作本身的采集边界。
2.4 异常与断电的边界
原有异常尾部恢复机制继续使用:故障现场尽量把文本保存到独立 4 KB SRAM bank,避免在异常处理里等待 NAND 或文件锁;再次启动且 SRAM 内容仍有效时恢复到日志队列。正常关机/重启会限时提交队列。
突然断电仍可能丢失尚未提交的 RAM 日志。异常 SRAM 也不保证跨彻底断电保存。导出的 TXT 是运行文本与尽力恢复的异常尾部,不包含完整二进制内存转储。详细存储恢复机制可结合 OFFLINE_LOG_README.md(OFFLINE_LOG_README.md) 阅读;旧文中的按功能命名固件是历史产物,本版构建位置以本文为准。
- 固件配置、构建与文件位置
本板配置包含:
```ini
CONFIG_APP_OFFLINE_LOG=y
CONFIG_APP_OFFLINE_LOG_BLE=y
```
`APP_OFFLINE_LOG_BLE` 依赖 `APP_OFFLINE_LOG`、`BT_MANAGER` 与 `BT_BLE`;日志采集还沿用 `APP_OFFLINE_LOG` 原有的 NAND、ACTLOG、文件系统、Shell、NVRAM、唤醒锁等依赖。不要仅添加宏、却关闭其底层依赖。关闭 BLE 子功能后,USB CDC 与离线采集仍可按原配置使用。
在 `\zephyr` 执行完整构建与打包:
```powershell
cd D:\Master\6project\ATS3089C_to_ATS3089P\\zephyr
python -B build.py -n -b ats3089p_dev_watch -a bt_watch
```
固件统一写入原目录:
```text
application/bt_watch/outdir/ats3089p_dev_watch/_firmware/
ats3089p_dev_watch_YYMMDD.fw
ats3089p_dev_watch_YYMMDD_debug.fw
ota_firmware.bin
```
例如 2026-10-23 对应 `ats3089p_dev_watch_261023.fw`。不新建 BLE 日志专用固件目录。只有编译链接成功并运行完整打包流程后,以上 `.fw` 才会更新;仅执行 `ninja` 不等于已经重新打包。
需要烧录包含新 GATT 服务的固件。旧固件即使支持 BLE 录音或 USB 日志导出,也不能仅靠安装新 PC 工具实现本协议。
- PC 使用 BLE 导出
4.1 安装依赖与工作目录
PC 使用 Python 3.8 或以上和 Bleak。SDK 自带 Python 3.8 也可以使用,建议通过同一个解释器执行安装和运行命令:
```powershell
cd D:\Master\6project\ATS3089C_to_ATS3089P\
python -m pip install -r .\application\bt_watch\tools\offline_log\requirements_ble.txt
$blelog = '.\application\bt_watch\tools\offline_log\offline_log_ble.py'
python $blelog --help
```
依赖文件固定范围为 `bleak>=0.22.3,<0.23`,兼容工程使用的 Python 3.8。BLE 方式不依赖 `pyserial`,不要求连接 USB 数据线。安装依赖需要网络,正常扫描与导出不需要网络。确认 PC 蓝牙开启、开发板已开机且 BLE 广播正常;关闭正在独占开发板 BLE 连接的录音工具、手机 App 等程序。
4.2 扫描与连接
当前开发板广播名称为 **`zkBLE`**,`export` 默认按此完整名称筛选,无需填写地址即可导出:
```powershell
python $blelog export --output-dir D:\logs
```
也可以显式指定相同名称,或者在更换广播名后指定新名称:
```powershell
python $blelog export --name zkBLE --output-dir D:\logs
```
需要检查广播或排查连接时,可以扫描全部设备,也可以仅显示名称完全匹配 `zkBLE` 的设备:
```powershell
python $blelog scan
python $blelog scan --name zkBLE
```
`scan` 默认显示所有扫描结果,`scan --name` 按完整名称过滤。扫描不强制要求广播包里携带日志服务 UUID。广播空间有限,"日志UUID广播=否 / 未携带"不等于设备没有服务,实际连接后才检查 GATT。
同名设备超过一块时,工具拒绝猜测目标。请关闭其它同名板,或使用扫描得到的实际地址固定目标:
```powershell
python $blelog export --address AA:BB:CC:DD:EE:FF --output-dir D:\logs
```
`AA:BB:CC:DD:EE:FF` 是示例,需要替换为本机扫描得到的地址。显式 `--address` 会覆盖默认的 `zkBLE` 名称筛选,不要求该地址对应设备仍叫此名称。设备名称取决于固件广播配置,修改过名称时使用 `--name` 指定新名称。
名称筛选、连接检查与旧配对修复均在 PC 工具侧执行。设备已经运行支持本文 BLE 日志服务的固件时,更新这些功能只需更新 Python 工具,无需重新打包或烧录固件。
工具自动完成发现、连接、检查服务、订阅通知、创建非零请求编号、发送 START、接收并确认窗口、验证长度与 CRC、保存文件以及释放连接。只执行 `scan` 不会建立导出连接,也不会生成文件。
目录含空格或中文时使用双引号:
```powershell
python $blelog export --output-dir "D:\客户反馈\设备 A"
```
接收先写同目录临时 `.part` 文件。只有完整收齐、END 校验通过后才原子发布正式 TXT,并保证不覆盖已有文件。失败、断开或取消不会把半截文件作为成功结果。按 `Ctrl+C` 可取消,工具尽力向设备发送 CANCEL 并清理临时文件。
默认正式文件名沿用 USB 工具的 `日志_YYYYMMDD_HHMMSS_微秒.txt`;也可通过 `--filename` 指定。已有同名文件时明确失败,不覆盖旧问题记录。可选参数如下:
| 参数 | 默认值 | 功能 |
| --- | --- | --- |
| `export/start/sync-time/status/repair --name` | `zkBLE` | 按完整广播名称选择目标,可省略;与显式 `--address` 互斥 |
| `export/start/sync-time/status/repair --address` | 不指定 | 使用实际扫描地址,覆盖默认名称筛选 |
| `scan --name` | 不过滤名称 | 仅显示完整名称匹配的扫描结果,例如 `zkBLE` |
| `--output-dir` | 当前目录下 `logs` | PC 保存目录,可自动创建 |
| `--filename` | 时间戳 TXT | 指定输出文件名,拒绝覆盖 |
| `--scan-timeout` | 8 秒 | 扫描时间,六个子命令均支持 |
| `--connect-timeout` | 20 秒 | 每次连接与 GATT 服务发现等待时间,`export/start/sync-time/status/repair` 支持 |
| `export --timeout` | 10 秒 | 通知停顿与控制写等待;初始快照阶段至少等待 20 秒 |
| `start --timeout` | 30 秒 | 等待校时、清旧并开启新采集的最终通知;清旧发送后超时不重发 |
| `start --no-sync-time` | 不指定 | 跳过 PC 校时,兼容只支持采集清旧的 v4 固件;不会把 v4 运行时间转成钟面时间 |
| `--log-file` | 不额外保存 | 可选 UTF-8 连接诊断日志路径,六个子命令均支持 |
保留连接诊断信息的例子:
```powershell
python $blelog export --output-dir D:\logs --log-file D:\logs\ble_log_diagnostic.txt
```
设备等待 ACK 固定为 10 秒,建议保留 PC 默认超时;单纯把 PC 超时加大不会改变板端 ACK 策略。进度显示 100% 只表示正文已收齐,仍需最后 END 和 CRC 验证,看到"导出成功"才算完成。正常退出码为 0,错误为 1,用户 `Ctrl+C` 取消为 130。
4.3 连接检查与旧配对修复
需要先检查连接而不导出日志时执行:
```powershell
python $blelog status
```
`status` 默认选择 `zkBLE`,建立连接并发现日志 GATT 服务,确认 CONTROL 支持 Write、DATA 支持 Notify,然后断开。它不发送 START,不创建日志快照,也不生成导出 TXT。"检查成功"表示连接与 GATT 特征就绪,尚不能代替完整导出的长度/CRC 验证。
如果已经能扫描到 `zkBLE`,但连接报 `Could not get GATT services: Unreachable`,并确认存在 Windows 旧配对与板端密钥不一致,可以显式运行修复:
```powershell
python $blelog repair
python $blelog export --output-dir D:\logs
```
`repair` 适用于 Windows,默认按 `zkBLE` 精确选择唯一目标,只清理**选定这块板**在 Windows 中的旧配对关联。选定后锁定原地址,重新扫描同一块设备,再连接并验证日志 GATT 服务及两个特征;不会改选另一个同名设备,也不会发送 START 或导出文件。随后运行 `export` 才开始日志传输。
多块同名板同时开机时,先关闭其它板,或用 `--address` 指定目标。修复前关闭其它正在连接这块板的 BLE 程序。普通 `export/status` 不会自动解除配对;删除旧关联只由显式 `repair` 发起,避免普通导出悄悄改动 Windows 配对状态。
信号较弱或移除旧关联后首次扫描暂未发现设备时,先靠近开发板,再适当延长这一轮扫描:
```powershell
python $blelog scan --name zkBLE --scan-timeout 15
python $blelog export --output-dir D:\logs --scan-timeout 15
```
本次排查曾在 8 秒扫描中未发现,下一次扫描发现 `zkBLE`、RSSI 约 -85 dBm。它说明扫描结果可能随信号和时机变化,不应把一次未发现误判为新固件没有广播;默认扫描时间仍为 8 秒。
连接与服务发现阶段最多尝试 2 次,每次失败会释放连接后再试。日志业务的 START 不自动重发;请求发出后若导出失败,应先处理原因,再重新运行一次导出。连接阶段重试不能修复失配的旧密钥,不能只靠不断重试绕过旧配对问题。
4.4 从当前开始抓取:先 start,再复现,再 export
需要排查一次新问题时,在 `` 运行:
```powershell
$blelog = ".\application\bt_watch\tools\offline_log\offline_log_ble.py"
同一连接先按PC本地时钟校时,成功后清旧并开启采集;默认按zkBLE连接
python $blelog start --scan-timeout 15
看到开始采集成功后,再操作设备、切换 MLP/CNN 或复现待定位问题
普通 UART 可执行:app wake model mlp / app wake model cnn / app wake on
复现完成后导出本轮记录;此命令不会再次清空记录
python $blelog export --output-dir D:\logs --scan-timeout 15
```
`start` 默认先在当前连接写入最新 PC 本地时间,收到匹配的 `CLOCK_SYNCED` 后才发送 `CAPTURE_START`。校时失败、旧固件拒绝或连接中断时不发送清空命令,保留原有记录。之后设备在清理完成、采集已开启后返回 `CAPTURE_STARTED`,PC 不把单纯的 ATT 写响应当作完成。`start` 适合**准备复现之前**执行;已经出现且希望保留的问题应直接 `export`。请求一旦发送,工具不会因为超时或断线自动重发清除命令;若未收到最终确认,先检查设备状态,避免不清楚设备是否已清理时反复执行。
只校时、继续保留现有日志,或为旧 v4 固件跳过校时的完整命令如下:
```powershell
v5:只同步PC本地时间,不清理Flash/RAM,不开启或关闭采集
python $blelog sync-time --scan-timeout 15
v4兼容:清旧并开始采集,时间仍为v4的+uptime
python $blelog start --no-sync-time --scan-timeout 15
```
独立 `sync-time` 只影响校时完成后采集的新行,已有正文原样保留。普通 `export` 不会校时,不清理记录,也不会根据导出时刻重新计算过去每行的时间。
本轮从 UART 控制也可以:
```text
app log time 18 30 00 000
app log start
app log status
```
`app log time HH MM SS mmm` 是没有 BLE 校时条件时的日内手工校时,范围为 00~23 时、00~59 分/秒、000~999 毫秒,省略毫秒时为 0;应填写执行当时的本地时间,人工发送会有延迟。它沿用已知日期,未有日期基准时采用占位日期,因此仅手工设置时分秒不能说明日历日期准确。`app log time` 不带参数时只查询。`app log start` 本身不会从 UART 接收软件自动取得电脑时钟。接着复现并通过任一通路导出。已有 `app log clear` 继续保留;它清理历史但保持原采集开关,不能代替"明确清理并开启"的 `start`。`app log on` 只打开采集并保留历史,也不能代替 `start`。
4.5 首次使用建议
-
在普通 UART 执行 `app log status`,确认 `ready=1 enabled=1`。
-
正常操作设备,制造几行容易识别的日志;需要主动提交时执行 `app log flush`。
-
直接执行 `python blelog export --output-dir D:\\logs\`;需要先检查设备时使用 \`python blelog scan --name zkBLE`。
-
打开导出的 TXT,检查设备启动标记、预期业务日志及末尾 `offline-log snapshot`。
-
再按第 5 节用 USB CDC 导出,验证两条路径都可用。两次请求生成于不同时间,正文和 CRC 可以不同,不应强求逐字节相同。
BLE 传输期间原有日志保存继续运行。快照之后产生的业务与连接状态日志出现在下一次快照中;当前导出的内容止于生成快照的时点。
- USB CDC 有线方式继续使用
5.1 PC 命令
在 `` 目录执行:
```powershell
python -m pip install pyserial
python .\application\bt_watch\tools\offline_log\offline_log_cli.py --mode cdc --port COM14 --output-dir D:\logs
```
`COM14` 替换为本机实际 CDC 端口,运行前关闭占用该端口的串口助手。普通 UART 调试口与 USB CDC COM 口是不同通道,不要将外接 UART 转接板端口误当成 CDC。CDC 工具配置 115200、8N1,DTR 不是必需条件。
沿用自动模式或 PowerShell 入口也可以:
```powershell
python .\application\bt_watch\tools\offline_log\offline_log_cli.py --mode auto --output-dir D:\logs
powershell -NoProfile -ExecutionPolicy Bypass -File .\application\bt_watch\tools\offline_log\export_logs.ps1 -Mode cdc -Port COM14 -OutputDir D:\logs
```
`auto` 保留原有 U 盘兼容策略:发现有效已有快照时读取它;没有有效 U 盘时再等待 CDC。需要明确取得本次 CDC 快照,使用 `--mode cdc`。USB U 盘读取入口为:
```powershell
python .\application\bt_watch\tools\offline_log\offline_log_cli.py --mode usb --drive E:\ --output-dir D:\logs
```
已有 U 盘快照反映进入 MSC 时的日志,读取文件不会刷新它。需要新快照时退出、重新进入 U 盘,或使用 CDC/BLE 新建一次快照。
5.2 普通 UART 控制指令
| 指令 | 用途 |
| --- | --- |
| `app log status` | 查询采集状态、容量、丢弃量、错误与当前导出状态 |
| `app log time HH MM SS mmm` | 日内手工校准离线日志时钟,不清历史,不修改系统 RTC |
| `app log on` / `app log off` | 开启/关闭普通日志采集保存;历史日志仍保留 |
| `app log start` | 串行清除本离线区 Flash 与旧 RAM 日志,并开启一轮新的采集 |
| `app log flush` | 提交已排队内容,正常最多等待约 3 秒 |
| `app log export id` | 经已开启的 USB CDC 发送快照;**不是 BLE START 指令** |
| `app log cancel id` | 取消当前导出;指定编号时只取消匹配请求 |
| `app log save` | 在板端持有 FAT 时生成 `OFFLOG.TXT/OFFLOG.CRC` |
| `app log usb id` | 经 CDC 接收请求,生成带编号快照,然后切换 U 盘 |
| `app log clear` | 清理本功能持久日志和命令前已排队内容;会丢失历史问题记录 |
| `app usbserial auto on` | 恢复 USB 插线自动检测,并保存开机偏好 |
| `app usbserial on` / `app usbserial status` | 手动开启 CDC / 查询 CDC 状态 |
BLE 无线 START 由 PC GATT 控制特征发送,不需要先执行 `app log export`,也不需要开启 CDC。UART 的 `app log cancel` 可用于研发紧急停止共用导出任务。
`status` 的 `export=1` 表示共用导出任务正在执行,`id` 为请求编号,`export_error` 为上次任务结果。采集相关 `ready`、`enabled`、`queued`、`dropped`、`errors`、`last_error` 仍沿用原含义。不要仅凭旧的采集 `version` 字段判断 BLE 服务是否存在,应以当前固件和 GATT 服务检查结果为准。
5.3 原 USB CDC 协议
PC 发送十进制非零编号:
```text
app log export <id>\r\n
```
设备返回:
```text
LOG,BEGIN,<id>,offline_log.txt,<总字节数>\r\n
<严格等于总字节数的原始文本>
\r\nLOG,END,<id>,<总字节数>,<8位大写CRC32>\r\n
```
开始前失败返回 `LOG,ERROR,<id>,<负错误码>\r\n`。取消为 `app log cancel <id>\r\n`。BEGIN 前允许普通控制台输出,PC 依据请求编号选择本次响应;BEGIN 后按长度读取正文,正文中出现 `LOG,END` 不会被误当结束帧。
USB MSC 的 `OFFLOG.CRC` 是 ASCII 单行,结尾必须为 CRLF:
```text
OFFLOG,1,<总字节数>,<8位大写CRC32>\r\n
OFFLOG,2,<id>,<总字节数>,<8位大写CRC32>\r\n
```
第一种用于普通已有快照,第二种用于由 `app log usb <id>` 自动生成的请求快照。切盘前 CDC 返回 `LOG,USB,<id>,READY\r\n`;PC 在切盘后只接受匹配编号的第二种清单。BLE 新协议没有更改上述旧格式。
- BLE GATT 服务与协议
6.1 独立服务
| 项目 | 完整 UUID | 属性/用途 |
| --- | --- | --- |
| 离线日志 Service | `7b2e1100-8e7f-4d3a-9c61-2a3089c00001` | 独立主服务 |
| CONTROL | `7b2e1101-8e7f-4d3a-9c61-2a3089c00001` | Write,PC 发送 START/CANCEL/ACK/CAPTURE_START/SYNC_CLOCK,使用带响应写 |
| DATA | `7b2e1102-8e7f-4d3a-9c61-2a3089c00001` | Notify,设备发送 BEGIN/DATA/END/ERROR/CAPTURE_STARTED/CLOCK_SYNCED;客户端先启用 CCC |
它与现有 BLE 录音、文字回传等业务服务并存,没有把日志数据混入音频数据流。PC 必须先完成 DATA 通知订阅,再发送 START。控制写的 ATT Write Response 仅说明 GATT 写入得到响应,不代表整份日志已导出;真正完成以 END 及 PC 校验为准。
同步拒绝控制请求时使用 ATT 错误:资源忙映射为 `INSUFFICIENT_RESOURCES`,其它协议/状态错误映射为 `VALUE_NOT_ALLOWED`;长写/Prepare Write 不支持。此时不保证收到 ERROR 通知。只有任务已经接收后发生的异步错误,才由工作线程尽力发送第 6.4 节的 ERROR。客户端应同时处理控制写异常与 ERROR 通知。
6.2 公共帧头:12 字节
所有多字节整数使用**小端序**。协议没有 C 字符串结尾,也不进行 UTF-8 转义。Python 对应:
```python
HEADER = struct.Struct("<2sBBII")
magic, version, op_or_kind, request_id, offset
```
| 字节偏移 | 长度 | 类型 | 字段 | 含义 |
| --- | --- | --- | --- | --- |
| 0 | 2 | bytes | magic | 固定 ASCII `OL`,十六进制 `4F 4C` |
| 2 | 1 | u8 | version | 固定 `1` |
| 3 | 1 | u8 | op/kind | CONTROL 中是操作码,DATA 中是通知类型 |
| 4 | 4 | u32 | request_id | 本次请求编号,范围 `1..0xFFFFFFFF`,0 无效 |
| 8 | 4 | u32 | offset | DATA/ACK/END 表示正文**字节**位置,其它控制帧必须为 0 |
编号绑定从 START 到 END/ERROR 的整个会话,用于拒绝旧包、错误 ACK 或其它请求。文本可以包含任意字节,位置与长度不按中文字符数计算。
6.3 CONTROL 帧:12 字节头与可选校时负载
| op | 名称 | offset | 行为 |
| --- | --- | --- | --- |
| `1` | START | 必须为 `0` | 申请本次完整快照;先检查连接、CCC 与任务互斥 |
| `2` | CANCEL | 必须为 `0` | 取消匹配编号的当前导出 |
| `3` | ACK | 累计成功接收的正文长度 | BEGIN 后先发送 ACK(0),每个发送窗口后确认已连续接收的位置 |
| `4` | CAPTURE_START | 必须为 `0` | 清除旧 Flash/RAM 记录并开启新采集;完成后发 CAPTURE_STARTED,失败发 ERROR |
| `5` | SYNC_CLOCK | 必须为 `0` | 12 字节头后附 `uint64 local_civil_epoch_ms`,总长 20 字节;成功发 CLOCK_SYNCED,不清旧 |
例如开始编号 1 的导出,CONTROL 写入以下 12 字节:
```text
4F 4C 01 01 01 00 00 00 00 00 00 00
```
不支持通过 START 的 offset 设置续传位置。ACK 必须对应当前编号和设备正在等待的累计位置,不能用"随便递增一个序号"代替累计字节位置。
`START`(op=1)是**导出快照**,`CAPTURE_START`(op=4)是**清旧并开始新采集**,二者不能混用。清旧操作与导出共享互斥作业槽,忙时拒绝。op=4 仍为固定 12 字节,无附加正文;例如编号 1 为 `4F 4C 01 04 01 00 00 00 00 00 00 00`。已发送的清旧请求不自动重试。
`SYNC_CLOCK`(op=5)只用于校准本模块的日志时钟,不写系统 RTC,也不改变采集开关。除 op=5 外,所有控制帧仍为固定 12 字节;op=5 必须恰为 20 字节,校时值使用小端 `uint64`。`local_civil_epoch_ms` 定义为"Unix UTC 毫秒 + 该时刻 PC 本地 UTC 偏移毫秒",即把 PC 本地年月日时分秒编码到整数中,**不是未经偏移的 UTC 时间戳**。板端不再叠加时区,直接从该本地时间值计算钟面和日期;有效校时范围为 2000-01-01(含)至 2100-01-01(不含),越界返回错误。
例如中国标准时间固定 UTC+08:00,PC 的 Unix UTC 毫秒为 `U` 时,发送 `U + 28800000`;其他地区使用该时刻 PC 实际时区偏移。PC 在扫描、连接、订阅均完成后,于控制写之前重新取当前时间,避免把扫描前的旧时刻写到板端。该方案不是往返延迟补偿协议,剩余一次控制写传输延迟不能视为已被消除。协议版本字节仍为 1,通过新增操作码扩展;"v5"指固件功能版本,不是把帧头 version 改为 5。
默认 `start` 的控制顺序为 `SYNC_CLOCK(id_a)` → 匹配的 `CLOCK_SYNCED(id_a)` → `CAPTURE_START(id_b)` → 匹配的 `CAPTURE_STARTED(id_b)`,两个阶段使用独立非零请求编号。第一阶段失败时不得发送第二阶段,避免为了校时失败而意外清除已有问题记录。
6.4 DATA 通知帧
| kind | 名称 | 头中 offset | 头后负载 | 总长度 |
| --- | --- | --- | --- | --- |
| `1` | BEGIN | `0` | `u32 total_bytes` + `u32 crc32` | 固定 20 字节 |
| `2` | DATA | 当前包正文的起始字节位置 | 原始正文,长度 1..244 字节 | 13..256 字节 |
| `3` | END | `total_bytes` | `u32 total_bytes` + `u32 crc32` | 固定 20 字节 |
| `4` | ERROR | 错误发生时的协议位置,不能当成功长度 | `int32 error`,负 SDK errno | 固定 16 字节 |
| `5` | CAPTURE_STARTED | 必须为 `0` | 无 | 固定 12 字节;清旧并开启采集已完成 |
| `6` | CLOCK_SYNCED | 必须为 `0` | 无 | 固定 12 字节;匹配 SYNC_CLOCK 的校时已生效 |
BEGIN 声明快照总长度和预期 CRC,END 再次声明相同元信息。正文总长度不能超过 512 KB。DATA 不另带长度字段,正文长度等于该条 Notify 长度减去 12;**一条 GATT Notify 对应一帧**,不要把多个通知随意拼接后按串口行解析。
CAPTURE_STARTED 必须对应本次 CAPTURE_START 的非零编号;它不是导出完成 END,没有文件长度或 CRC,也不需要 ACK。只有收到匹配的 CAPTURE_STARTED,PC 才打印开始新采集成功。旧固件不认识 op=4,会拒绝请求,不能用 PC 端删除文件来模拟板端清理。
CLOCK_SYNCED 同样必须对应本次 SYNC_CLOCK 的非零编号,固定 12 字节且 offset 为 0,没有正文长度、CRC 或 ACK 阶段。旧 v4 固件不认识 op=5,会拒绝校时;新 PC 工具不会把这当成清旧完成,需明确 `start --no-sync-time` 才能继续使用 v4 的采集命令。旧 v4 固件支持 export 和 CAPTURE_START,早于 v4 的固件是否支持清旧要另行确认。
CRC 为 IEEE CRC32,与 PC `zlib.crc32` 的结果一致,初始值为 0,计算对象是全部正文原始字节,包括快照尾部,排除 BLE 头和 BEGIN/END 元数据。校验向量:ASCII `123456789` 的 CRC 为 `0xCBF43926`,空字节串为 0。
例如 `total_bytes=9`、CRC 为 `0xCBF43926` 时,元信息在链路上是:
```text
09 00 00 00 26 39 F4 CB
```
6.5 MTU 与分包
ATT Notify 可用值长度为 `MTU - 3`,每帧还要扣除本协议 12 字节头:
```text
每个 DATA 正文上限 = min(244, ATT_MTU - 15)
```
| ATT MTU | Notify 值上限 | 单个 DATA 正文上限 |
| --- | --- | --- |
| 23 | 20 字节 | 8 字节 |
| 64 | 61 字节 | 49 字节 |
| 247 | 244 字节 | 232 字节 |
| 259 或更大 | 至少 256 字节 | 244 字节 |
本协议支持最低 MTU 23,BEGIN/END 20 字节恰好能放入最小通知。不要把"MTU=247"理解为"正文固定 244 字节",因为协议帧头也占空间。实际 DATA 还可能为了对齐窗口末尾而比上限短。
6.6 窗口累计 ACK
设备采用 **1024 字节正文窗口、停等累计 ACK**:
-
发送 BEGIN 后停下,等待 PC 的 ACK(0)。这样 PC 有机会校验元信息、创建文件后再允许发送。
-
从 offset=0 开始发送多个 DATA,直到该窗口累计发送 1024 字节,或达到正文末尾。
-
发送完窗口后停下,PC 确认全部连续数据已接收并写入临时文件,再发送 ACK(当前累计正文长度)。
-
收到匹配 ACK 才发送下一窗口。最后一个不足 1024 字节的窗口也必须 ACK。
-
最后窗口已确认后发送 END。PC 验证 END 的编号、长度和 CRC 后发布文件。
单次等待 ACK 超过 10 秒时,设备中止本次会话并释放资源。BLE 链路自身有链路层重传,但 Notify 没有本协议要求的应用层处理确认;累计 ACK 用于限制设备领先 PC 的数据量,也能发现客户端遗漏、解析失败或来不及消费。
对上一次已接受的 ACK 重复确认是幂等操作,不会额外增加发送额度;不匹配当前等待位置的超前 ACK、错误编号/来源会被拒绝。当前窗口机制不包含超时重发和断点续传;遗漏窗口不能通过伪造 ACK 跳过,应当失败后重新导出。ACK 表示 PC 应用已接受该连续窗口,不承诺 PC 突然断电后其操作系统缓存已全部物理落盘。
6.7 完整时序
```mermaid
sequenceDiagram
participant PC as PC Bleak 工具
participant GATT as 设备 BLE 服务
participant Worker as 共用导出线程
participant Store as 离线存储线程
PC->>GATT: 连接、发现服务、订阅 DATA
PC->>GATT: CONTROL START(id, offset=0)
GATT->>Worker: 非阻塞提交导出任务
Worker->>Store: 提交已有队列并创建稳定快照
Store-->>Worker: snapshot / total / CRC,恢复日常保存
Worker-->>PC: BEGIN(id, total, CRC)
PC->>GATT: ACK(id, 0)
loop 每 1024 字节,末窗口可不足
Worker-->>PC: 多个 DATA(id, offset, payload)
PC->>PC: 连续性校验并写入 .part
PC->>GATT: ACK(id, 累计接收字节数)
end
Worker-->>PC: END(id, total, CRC)
PC->>PC: 校验总长度及 CRC,发布 TXT
PC->>GATT: 断开连接
```
- 固件内部流程与资源管理
7.1 BLE 回调只处理轻量协议
GATT CONTROL 写回调检查长度、魔数、版本、操作码、编号和 offset,START 仅申请共用任务,ACK 只更新确认位置并唤醒等待者,CANCEL 只设置取消标志。回调不扫描 Flash、不生成大文件、不等待整个导出完成,也不分配新的 512 KB 缓冲。
长操作在原有 `offline_log_export.c` 工作线程运行,复用原 2048 字节线程栈与 512 KB PSRAM 快照。BLE 仅增加单帧及小型会话状态。主线程在蓝牙引擎就绪后初始化服务,避免从蓝牙接收回调执行长时间初始化。
7.2 互斥、取消与保活
-
共用任务状态串行化 CDC、USB 自动切盘和 BLE 导出。快照锁保护同一份快照,不能让另一请求在前一请求发送时覆盖正文。
-
BLE 传输不需要切换 USB 模式,不通过 FAT 文件转发;USB 原有模式所有权和 MSC 文件安全限制继续生效。
-
断开连接、关闭通知、用户取消、ACK 超时或底层发送失败时,导出退出并记录错误。正常完成和所有失败路径均释放锁及唤醒锁。
-
导出期间持有 `FULL_WAKE_LOCK`,防止快照或发送阶段进入深睡造成通信中断;完成后恢复正常功耗策略。它不等于保证无限制并发音频任务下的 BLE 吞吐。
USB 自动插线监听原有待机功耗行为未由 BLE 改变。无需随时插线导出时,可按原说明关闭 `app usbserial auto`;这与是否支持 BLE 导出是不同配置。
7.3 PC 如何拒绝不完整文件
PC 按协议状态机处理通知,并检查:
| 检查 | 解决的问题 |
| --- | --- |
| magic/version/帧长度/类型 | 拒绝非本协议或畸形帧 |
| 当前 request_id | 防止旧请求、其它请求污染本次文件 |
| BEGIN 总长度上限 | 防止异常设备声明无限长度 |
| 每个 DATA 的 offset 必须等于已收长度 | 发现丢包、重复包、乱序或重叠 |
| DATA 不得超过 BEGIN 总长度 | 防止越界正文 |
| END 总长度及 CRC 必须与 BEGIN 一致 | 防止元信息变化或提前结束 |
| 收齐正文后的实际 CRC32 | 发现内容损坏 |
| 临时文件 + 校验后原子发布、不覆盖已有文件 | 不把失败、取消、半截内容当成成功文件 |
原始文本按字节保存,不在校验前进行换行转换、去颜色符或中文重新编码。需要清理 ANSI 控制符时,应在完整导出后另做后处理,并保留原文件用于问题分析。
- 排查与实现记录
| 现象/问题 | 原因与处理 |
| --- | --- |
| 想直接从 BLE 回调读 Flash 发完整文件 | 会阻塞蓝牙接收线程;本实现把工作提交到已有导出线程 |
| 传到一半正文变化、CRC 不一致 | 直接读取循环区无法保证一致;先建稳定快照再传输 |
| 快照时暂停所有日志直到 BLE 发完 | 慢链路会长时间停止保存;本实现只在复制时冻结 Flash,发送阶段恢复日常存储 |
| 两个工具同时导出、数据互相覆盖 | CDC/BLE/自动切盘共用资源;使用统一忙状态与快照锁,忙时等待前一任务结束再重试 |
| MTU=23 时无法导出 | 不能假定大 MTU;按 `min(244, MTU-15)` 分包,最小 MTU 正文每包 8 字节 |
| PC 处理慢、接收堆积 | 使用 1024 字节累计 ACK 窗口限制设备领先量;超时中止,避免无限等待 |
| START 已写入但没有日志 | 先确认已订阅 DATA;ATT Write Response 不代表 BEGIN/END;检查 UART 中的导出错误 |
| BEGIN 后设备不继续发 DATA | PC 必须先 ACK(0);使用随附工具会自动发送 |
| 最后收齐 DATA 却没有 END | 最后不足 1024 字节的窗口也需要累计 ACK;检查最后 ACK 是否为 total |
| `-EBUSY` | 有其它导出或快照操作占用资源;结束/取消当前任务再试,不要循环并发 START |
| `-ENOTCONN` | BLE 已断开、通知未启用,或 CDC 未启用;按实际使用的传输方式检查连接 |
| `-ETIMEDOUT` | ACK/传输等待超时;检查信号、PC 是否阻塞、其它连接程序是否干扰后重新导出 |
| `-ECANCELED` | 用户、断线处理或 UART 发起取消;本次文件不作为成功发布 |
| `-EIO` / `-ENOSPC` | 检查 Flash 读取/校验、快照容量及 `app log status`,不要只反复修改 BLE 参数 |
| 能扫描但报找不到 1100/1101/1102 | 实际运行旧固件,或 Windows 缓存了旧 GATT 表;确认烧录后关闭其它 BLE 程序,断开重连,必要时在 Windows 设置移除这块板的旧配对并重启板子重新扫描 |
| Windows `Could not get GATT services: Unreachable` | 扫描成功不代表 GATT 已建立;本轮已确认 Windows 旧配对与板端密钥不匹配,按第 4.3 节显式 `repair` 后再导出,具体证据见下方记录 |
| USB CDC COM 打不开 | 关闭占用串口的程序,检查设备管理器;不要把 UART 口当 CDC |
| USB COM 消失但出现盘符 | 设备切换到 MSC,属于 USB 类别切换;使用 `--mode usb --drive` 读取快照 |
| 导出 TXT 里的日志较少或有缺口 | 检查采集开关、循环覆盖、`dropped`、`errors`;导出只能传输已采集到的内容 |
| 程序找不到 `offline_log_ble.py` | 命令执行目录不对;先进入 ``,使用本文完整相对路径 |
错误负载使用**板端 SDK errno 数值**,PC 不应直接用 Windows/Linux 本机 `errno` 表硬译所有值。开发时优先结合板端源码的 `-EBUSY` 等符号及 UART 状态定位。
8.1 本轮 `Unreachable` 问题定位记录
2026-10-23,PC 可以扫描到 `zkBLE`,但执行 `export` 在 GATT 服务发现阶段报 `Could not get GATT services: Unreachable`。此时尚未发送日志 START,问题发生在日志协议运行之前。
定位证据如下:
| 证据 | 含义 |
| --- | --- |
| Windows 查询该设备为 `pairing=True` | PC 保留了此前的配对关联 |
| 按日志服务 UUID 发现与全量服务发现均报 `Unreachable` | 故障不由本工具按服务筛选造成,仍停留在 GATT 连接/发现层 |
| 工程根目录 `log_26_10_22.txt` 第 83232 行:`RX: 04 3e 0d 05 ...` | 控制器上报 LE Long Term Key Request(长期密钥请求) |
| 第 83233 行:`TX: 01 1b 20 02 ...` | 设备发送 HCI `0x201B`,即 LE Long Term Key Request Negative Reply,拒绝提供所需密钥 |
| 第 83260、83265 行:断开原因 `0x13` | 对端主动终止连接,随后 Windows 无法继续完成 GATT 服务发现 |
上述证据说明 Windows 仍尝试使用旧配对关系恢复加密连接,而板端无法提供相应长期密钥,两端配对状态不一致。`0x13` 单独只能表示对端断开,需结合前面的长期密钥请求与负回复定位;不能仅看到它就判断射频质量或日志分包出错。
处理方式是显式清除选定设备在 Windows 中的旧配对,再锁定同一地址重新扫描、连接和验证服务。新增 `repair` 将这几个步骤串起来,`status` 提供独立连接检查。普通导出保留配对状态。此轮只更新 PC 工具,BLE 日志 UUID、帧格式与固件均无需更改。
本机已实际执行 `repair --name zkBLE`:16:36:55 清理旧配对,16:37:07 完成日志 CONTROL/DATA 两个特征检查,协商 ATT MTU=247。随后按默认名称运行导出,16:41:11 显示导出成功并以 0 退出,得到 284456 字节 TXT、CRC32 `809D253C`,独立读取文件重新计算 CRC 也一致。诊断日志为 `D:\logs\ble_log_repair_261023.txt` 与 `D:\logs\ble_log_export_261023.txt`,完整记录见第 10 节。
本次实现没有强制配对、认证或应用层加密;CRC32 用于完整性检查,不提供保密性或身份认证。它沿用当前工程 BLE 访问策略。需要面向产品增加访问控制时,应在 GATT 权限、配对/绑定策略和应用授权层完善,不能把 CRC 当作认证。
8.2 2026-10-23:串口与离线内容差异、旧日志和时间戳
对比文件为工程根目录 `log_26_10_22.txt` 与 `D:\logs\日志_20261023_164716_276967.txt`。应对齐同一次业务操作,而不是直接比较两份文件的全量行数:真实串口文件可能跨多次运行,离线区则按容量循环覆盖,并保留以前的采集历史。
| 证据 | 分析与本轮处理 |
| --- | --- |
| 真实文件第 84868 行、离线文件第 9439 行均为 `app wake model mlp`,随后切到 CNN | 这是一段可准确对齐的共同操作;去除串口助手时间和 ANSI 后,共同业务输出一致 |
| 从该 MLP 指令到下一次 `app wake on`,真实 37 个非空行,离线 29 个 | 7 行实际缺失为 `spinand power on`、两条 SPI3 频率、delaychain、sectors、4xIO、power-on cost;另 1 行差异是 UART 提示符和下一行合并,不是丢了识别结果 |
| 真实文件 16:46:26.309~16:46:26.364 有上述 NAND 唤醒输出,离线同处缺失 | 旧 `offline_log_capture()` 排除整个存储线程自身打印。NAND 恰由离线存储线程唤醒,因此这些驱动日志也被排除;新实现接收这些文本,并避免自身打印单独引发循环写盘 |
| 离线末尾 `stored_bytes=356501 dropped=5907 errors=0 last_error=0` | CRC 正确只证明保存的正文完整送达;`dropped=5907` 证明此前 RAM 队列满时已丢过 5907 字节。增大队列改善突发容量,已经丢掉的旧数据无法在导出时补回 |
| 离线 9935 行、356636 字节,其中 5160 行为 TX/RX 或十六进制续行,254796 字节,约 71.4% | 日志被大量蓝牙包转储占用;这些不全是新的业务事件 |
| 存在 1389 条包含 `4f 4c 01 02` 的导出 DATA 帧;前次 BEGIN 中有 `28 57 04 00 3c 25 9d 80` | 小端解码为前次文件 284456 字节、CRC `809D253C`。说明前一次导出的旧正文又通过 HCI 调试转储记录回离线区,造成历史日志放大 |
| 串口助手有 `16:46:22.275`,离线只有原文字节 | 此时钟由 PC 接收工具添加;当时旧固件正文没有采集行时间戳。v4 增加逐行运行时间,v5 再按第 2.3 节增加 PC/RTC 校时后的本地钟面时间,均不伪造历史采集时间 |
定位方法:先用 `app wake model mlp/cnn`、一条独特的 NTC 数值或 HIT 序号对齐,再剥除 PC 接收时间和 ANSI 后逐行比较;检查尾部 `dropped/errors` 判断采集侧损失。不能把"BLE 导出 CRC 通过"解释为"与串口接收到的所有字节完全一致"。
实际复测步骤见第 4.4 节:先执行 `start` 清旧并开启,看到成功后操作设备,最后 `export`。这样可以减少旧会话对故障定位的干扰。BLE 和 CDC 使用相同的采集与快照,时间戳、历史清理和采集修复对两条路径同时生效;旧文件不重新加工为看似有真实时间的新文件。
本轮同时在 `hci_data_log_debug()` 中过滤**离线日志协议自身**的十六进制打印。服务注册后取得本次实际分配的 CONTROL/DATA ATT 句柄,过滤器动态绑定这些句柄,不硬编码 GATT 表地址。只有 CID 为 ATT 固定通道 4、方向对应 WRITE/NOTIFY、目标句柄匹配,且 `OL` v1 的请求编号、类型和声明长度均合法时,才跳过 `stack_print_hex`。完整 ACL 包和带有完整 OL 头的合法 ACL 首片都可识别,后者使用 L2CAP 声明长度检查帧形状;不能把任意负载中恰好出现的 `OL` 文本当作传输帧。非匹配的业务数据、连接错误和原有 SNOOP 保存继续工作。这样导出的旧正文不会再因为日志传输本身被重复写回离线区;不是关闭整机蓝牙日志。v5 过滤器已用真实 C 实现执行 976 项断言回归,涵盖新增校时帧、动态句柄、合法首片及其它业务不误滤。
8.3 2026-10-23:运行时间改为本地钟面时间
用户进一步对齐 `app wake ui` 操作:串口接收软件显示 `18:30:01.168`,v4 离线文件显示 `+00:09:24.203`。业务文字一致,差别来自时间基准:前者是 PC 本地接收时钟,后者是设备启动后的单调运行时间,不是记录错误或 BLE 导出时补错时间。
v5 增加 PC 本地时间同步、有效 RTC 只读初始化和 UART 日内手工校时。推荐更新固件后执行默认 BLE `start`,收到校时及新采集成功,再复现;新行采用 `HH:MM:SS.mmm`,同样可由 USB CDC 导出。不校时且 RTC 无效时继续使用带 `+` 的运行时间,便于明确辨别。
截图中串口接收软件把多行统一标为 `18:30:01.237`,但它们在设备端分几毫秒陆续产生。v5 对齐钟面,保留逐行真实采集间隔,不把一批串口接收的同一时间复制给所有行。旧离线文件不能反推当时 PC 各接收批次的时间,重新导出也不会改写。比较时先对齐业务行,再区分"设备采集时刻"和"PC 收到批次的时刻",不能用逐毫秒完全相同作为校时正确的判据。
- 开发入口与扩展方法
| 路径 | 职责 |
| --- | --- |
| `src/main/offline_log.c/.h` | 64 KB 队列、行时间戳、NAND 记录、循环覆盖、刷盘、采集会话与稳定快照 |
| `src/main/offline_log_clock.c` | 启动时只读取得有效 RTC 日期时间,初始化日志时钟;不写系统 RTC |
| `src/main/offline_log_panic.c/.h` | 异常 SRAM 尾部与启动恢复 |
| `src/main/offline_log_export.c/.h` | 共用快照、导出工作线程、CDC/BLE/切盘任务、取消与状态 |
| `src/main/ble_offline_log.c/.h` | 独立 GATT 服务、协议检查、通知分包、窗口 ACK 与断开处理 |
| `src/main/system_app_main.c` | 蓝牙引擎就绪后的服务初始化 |
| `src/main/system_app_shell.c` | 普通 UART/CDC 的 `app log`,原 USB 模式切换助手 |
| `framework/bluetooth/bt_stack/src/hci_data_log.c`、`offline_log_hci_filter.h` | 仅过滤日志协议自身合法 ATT 帧的文本转储,防止导出正文递归写回 |
| `src/main/CMakeLists.txt`、`Kconfig`、`boards/ats3089p_dev_watch.conf` | 组件编译与配置依赖 |
| `tools/offline_log/offline_log_ble.py` | BLE 扫描、连接检查、显式旧配对修复、新采集 start、导出 CLI、通知解析、ACK 与文件保存 |
| `tools/offline_log/requirements_ble.txt` | BLE 工具依赖范围 |
| `tools/offline_log/offline_log_protocol.py` | 原 USB CDC/MSC 导出与长度/CRC/发布实现 |
| `tools/offline_log/offline_log_cli.py` | 保留原 USB 命令行入口 |
| `tools/offline_log/README.md` | 原 USB 工具详细说明 |
新增重要 C 代码按工程约定用 `//zk+` / `//zk+-` 标注,Python、Kconfig、CMake 使用 `zk+` / `zk+-`,注释说明关键流程与修改原因。
后续开发手机 App 或其它 PC 客户端,按第 6 节实现以下步骤即可:发现指定服务、订阅 DATA、生成非零 id、START、验证 BEGIN、ACK(0)、按 offset 接收 DATA、每 1024 字节及末窗口发送累计 ACK、验证 END 和实际 CRC、最后发布文件。不要把 BLE 传输当作以 `\n` 分隔的串口文本协议。
若修改窗口大小、协议字段、ACK 语义或帧长度,需要同时修改固件与 PC,并考虑升级版本号。只修改 UUID 会破坏既有客户端,不能用它代替协议版本管理。若扩展断点续传,需要定义快照身份与生命周期,不能仅允许非零 START offset:设备重启或重建快照后,相同 offset 未必对应同一内容。
- 验证方法与当前状态
软件测试应覆盖:正常导出、最小 MTU 分包、满窗口和不足窗口、空正文、错误 id/offset/长度/CRC、提前 END、设备 ERROR、ACK 超时、断线、取消、临时文件清理与同名文件防覆盖。USB 原有回归需要继续运行,确认保留的有线导出没有退化。
2026-10-23 本轮 v5 软件回归记录:
| 验证项 | 结果 | 覆盖范围 |
| --- | --- | --- |
| Python 3.8 离线日志工具测试 | 133 项通过,独立复跑 2.403 秒 | 原 USB/GUI、BLE 协议与文件、连接/修复、新采集、PC 本地校时及应答、默认先校时再清旧、校时失败不清旧和旧版兼容 |
| BLE 模拟协议与文件回归 | 包含在上述 133 项中 | 512 KB、MTU 23、空正文、跨线程通知、累计 ACK、错误 CRC/编号/顺序、超时、断线、取消及防覆盖 |
| 日志存储真实 C 回归 | 1044 项断言通过 | PC/RTC 时钟基准、无时钟时 +uptime、跨午夜、本地行时间、64 KB 队列、GAP、自身采集、防循环刷盘、新会话清理及存储故障 |
| RTC 接入真实 C 回归 | 60 项断言通过 | 真实 `offline_log_clock.c`、日志核心与 SDK `rtc_lib.c`;RTC 启用 46 项、无 RTC 7 项、无设备名 7 项,覆盖无设备、读错、非法日期、闰日、毫秒、本地输出及 PC 优先 |
| 异常日志真实 C 回归 | 4747 项断言通过 | SRAM 异常尾部、恢复、队列交接及原有故障边界 |
| 原 CDC/MSC 导出真实 C 回归 | 467 项断言通过 | 原帧格式、取消、USB 错误、FAT 所有权及锁释放 |
| BLE 服务真实 C 回归 | 2019 项断言通过 | 20 字节校时帧/12 字节成功通知、参数拒绝、MTU 23/247、ACK、取消/断线/超时、新采集及动态句柄绑定 |
| 共用导出线程 BLE 分支回归 | 491 项断言通过 | 校时与导出/采集作业互斥、时钟更新及成功通知、错误资源释放、同一快照/CRC、新采集及取消 |
| HCI 自身传输过滤器真实 C 回归 | 976 项断言通过 | 新校时控制/通知、动态 CONTROL/DATA 句柄、完整包和合法 ACL 首片、OL 参数与其它业务不误滤 |
| C 回归合计 | 9804 项断言通过 | 上述七组 C 回归合计,不计入 Python 用例数 |
| v5 固件完整构建及打包 | `build.py` 最终退出码 0 | 完整链接和日期固件/OTA/raw 已更新;打包 `app.bin` 含 v5/clock_synced,ELF/map 含 RTC 初始化、时钟更新与 BLE 完成路径 |
以上 v5 软件测试不等于板端已实测。v4 打包后用户反馈串口与离线业务正文已经对齐,并提供了运行时间戳截图,详见第 8.3 节;本轮 v5 **尚未烧录到实际开发板**,本地时间校准需在烧录后单独确认。下文 16:41 导出是历史排查记录,不作为 v5 校时的实测结论。
2026-10-23 18:55:19~18:55:29 当前 v5 打包产物已更新到统一 `application/bt_watch/outdir/ats3089p_dev_watch/_firmware` 目录,沿用日期文件名,没有新建功能专用目录:
| 文件 | 字节数 | CRC32(IEEE) |
| --- | ---: | --- |
| `ats3089p_dev_watch_261023.fw` | 24,493,568 | `A08C7319` |
| `ats3089p_dev_watch_261023_debug.fw` | 10,060,288 | `1DB95297` |
| `ota_firmware.bin` | 1,488,384 | `3F17FD0C` |
| `ats3089p_dev_watch_261023_raw.bin` / `ats3089p_dev_watch_261023_raw_debug.bin` | 各 16,777,216 | `72AFB3AD` |
v5 最终链接结果:FLASH 使用 2,391,896 字节(33.97%),SRAM 使用 924,928 字节(99.15%,剩余 7936 字节),PSRAM 使用 33,516,704 字节(99.89%),SHARE_RAM 使用 20,948 / 20,991 字节(99.80%)。64 KB 采集队列位于**内部 SRAM**;已有 512 KB 稳定快照仍位于 PSRAM,BLE 复用原快照和线程,没有新增快照副本或独立线程栈。以上校验值对应本次 v5 打包,后续构建会变化;本次未向开发板烧写。
同目录的 `ats3089p_dev_watch_261023_manifest.json` 与 `ats3089p_dev_watch_261023_SHA256.txt` 记录固件、调试包、raw 镜像及 OTA 的大小和校验值。
在 `` 运行工具回归:
```powershell
python -B -m unittest discover -s application/bt_watch/tools/offline_log -v
python -B application/bt_watch/tests/offline_log/run_tests.py
python -B application/bt_watch/tests/offline_log/run_clock_tests.py
python -B application/bt_watch/tests/offline_log/run_panic_tests.py
python -B application/bt_watch/tests/offline_log/run_export_tests.py
python -B application/bt_watch/tests/offline_log_ble/run_tests.py
python -B application/bt_watch/tests/offline_log_ble/run_export_tests.py
python -B application/bt_watch/tests/offline_log_ble/run_hci_filter_tests.py
```
前一阶段已在实际设备复现连接故障,完成旧配对修复并成功导出完整日志。正文从 BEGIN/0% 到 100% 用时约 119.319 秒,有效吞吐约 2384 字节/秒。这是当时 MTU 247、较弱信号场景的实测值,不代表其它距离、适配器或并行业务下的速度。连接修复阶段只更新 PC 工具;本轮时间戳、采集和新会话优化包含 C 固件修改,需要更新固件。
前一阶段完整导出文件与独立复核结果:
| 项目 | 结果 |
| --- | --- |
| 文件 | `D:\logs\日志_20261023_163906_726880.txt` |
| CLI 完成时间 | 2026-10-23 16:41:11,退出码 0 |
| 实际长度 | 284456 字节 |
| CRC32(IEEE) | `809D253C`,使用独立 `read_bytes()` + `zlib.crc32` 复核一致 |
| SHA256 | `04fde1dfc4923f21d1dcfb5ee24ecc45266f117162aae1de889dc1b57f663b29` |
| 文本检查 | 含 `offline-log snapshot` 状态尾部 |
| 修复诊断日志 | `D:\logs\ble_log_repair_261023.txt` |
| 导出诊断日志 | `D:\logs\ble_log_export_261023.txt` |
自动化测试包含取消、断线、512 KB 等故障与边界用例;v4 当时记录的硬件取消、中途断开、满 512 KB 日志和 USB CDC 专项回归仍需各自板端复测。本轮 v5 的校时也需要单独记录实机结果。连接距离、多业务并发稳定性及其它信号条件下的吞吐需分别测量,不能由历史成功导出推断全部通过。
建议板端验证记录:
| 项目 | 应检查的结果 |
| --- | --- |
| 新服务发现 | PC 可发现 1100 服务及 1101/1102 特征 |
| 新采集 start | 先保存旧问题,再 start,收到匹配编号 CAPTURE_STARTED;新导出不再混入旧会话 |
| 时间戳与漏采回归 | 默认 start 先校时,clock_synced=1;新行显示本地钟面,MLP/CNN 操作及 NAND 唤醒输出可对齐;无有效 RTC 且未校时时显示 +uptime,检查 dropped/errors |
| 独立校时与旧版兼容 | sync-time 保留旧行与采集开关,只影响后续;校时失败不清旧;v4 可明确 start --no-sync-time |
| BLE 导出 | 出现 BEGIN、进度、END;TXT 内容可读,CRC 通过 |
| USB CDC 回归 | 原 COM 命令仍可完成并校验导出 |
| 双客户端竞争 | 第二请求忙时拒绝,第一请求内容不变 |
| 用户取消 | PC 无成功文件,设备任务退出,随后可重新导出 |
| 中途断开 | 设备释放导出状态与保活锁,可重连重试 |
| 长/满日志 | 按实际字节数校验,记录 MTU、总字节数、耗时及环境 |
| 导出同时产生日志 | 当前快照稳定,新日志可在下次导出中找到 |
| 小 MTU | 实测或模拟 MTU 23,不越过 Notify 最大值 |
BLE 耗时随 MTU、连接间隔、PC 适配器、射频环境、ACK 往返和并行业务变化。测量时使用"正文总字节数 ÷ 实际传输秒数"记录有效吞吐,不把理论物理层速率当作应用日志导出速度。