基于炬芯ATS308X的BLE 无线方式离线日志传输导出功能实现

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`。

  1. 功能范围与选择

| 方式 | 连接条件 | 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` 才清除本功能的旧记录。

  1. 设备如何保存日志

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) 阅读;旧文中的按功能命名固件是历史产物,本版构建位置以本文为准。

  1. 固件配置、构建与文件位置

本板配置包含:

```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 工具实现本协议。

  1. 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 首次使用建议

  1. 在普通 UART 执行 `app log status`,确认 `ready=1 enabled=1`。

  2. 正常操作设备,制造几行容易识别的日志;需要主动提交时执行 `app log flush`。

  3. 直接执行 `python blelog export --output-dir D:\\logs\`;需要先检查设备时使用 \`python blelog scan --name zkBLE`。

  4. 打开导出的 TXT,检查设备启动标记、预期业务日志及末尾 `offline-log snapshot`。

  5. 再按第 5 节用 USB CDC 导出,验证两条路径都可用。两次请求生成于不同时间,正文和 CRC 可以不同,不应强求逐字节相同。

BLE 传输期间原有日志保存继续运行。快照之后产生的业务与连接状态日志出现在下一次快照中;当前导出的内容止于生成快照的时点。

  1. 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 新协议没有更改上述旧格式。

  1. 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**:

  1. 发送 BEGIN 后停下,等待 PC 的 ACK(0)。这样 PC 有机会校验元信息、创建文件后再允许发送。

  2. 从 offset=0 开始发送多个 DATA,直到该窗口累计发送 1024 字节,或达到正文末尾。

  3. 发送完窗口后停下,PC 确认全部连续数据已接收并写入临时文件,再发送 ACK(当前累计正文长度)。

  4. 收到匹配 ACK 才发送下一窗口。最后一个不足 1024 字节的窗口也必须 ACK。

  5. 最后窗口已确认后发送 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: 断开连接

```

  1. 固件内部流程与资源管理

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 控制符时,应在完整导出后另做后处理,并保留原文件用于问题分析。

  1. 排查与实现记录

| 现象/问题 | 原因与处理 |

| --- | --- |

| 想直接从 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 收到批次的时刻",不能用逐毫秒完全相同作为校时正确的判据。

  1. 开发入口与扩展方法

| 路径 | 职责 |

| --- | --- |

| `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 未必对应同一内容。

  1. 验证方法与当前状态

软件测试应覆盖:正常导出、最小 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 往返和并行业务变化。测量时使用"正文总字节数 ÷ 实际传输秒数"记录有效吞吐,不把理论物理层速率当作应用日志导出速度。

相关推荐
gz927cool1 小时前
在 Harness 上搭建 Agent-Native 应用:可迁移经验
服务器·人工智能·ai·系统架构
kkkkkkkkkk_Z1 小时前
新手自学嵌入式 | 学习日记: Linux驱动进阶 —— Platform总线与I2C子系统实战
linux·运维·学习
Android系统攻城狮1 小时前
Linux Gstreamer深度解析之gst_audio_aggregator_set_sink_caps调用流程与实战(九十一)
linux·运维·服务器·gstreamer音视频·音视频进阶
‎ദ്ദിᵔ.˛.ᵔ₎1 小时前
Linux线程:this指针在pthread中的解决方案
linux
denlaku1 小时前
JDK 20 新特性详解
java
tedcloud1231 小时前
Docker Compose 部署 Memos:从安装到备份恢复
linux·开发语言·jvm·后端·docker·容器·rust
木井巳1 小时前
【JavaEE】SpringBoot 统一功能处理: 拦截器、统一返回与全局异常
java·spring boot·后端·spring·java-ee
SL-staff1 小时前
JVS-Rules金融风控实践:四大原生能力的技术实现与可落地配置指南
java·决策树·规则引擎·函数计算·金融风控·jvs-rules·数据源抽象
qq_377572771 小时前
typora install linux ubuntu
linux·ubuntu