本章介绍 App 的 in-app 诊断工具——最重要的是 DevLogService,它能产出 Liang 能直接 grep 的协议日志。
lib/core/services/dev_log_service.dart(757 行)—— 结构化日志、ring buffer、debug flagslib/core/services/dev_log_decoder.dart—— 日志解码(RX/TX 字节转可读文本)lib/core/services/mock_device_service.dart(354 行)—— Mock DeviceService 实现(UI 开发/演示用)lib/features/thermometer/presentation/widgets/dev_overlay.dart —— 开发者叠加层 UI(五 tab:ALL / APP / USER / RX / TX)DevLogService 的事件类型| 方法 | 事件范畴 | 举例 |
|---|---|---|
logAppEvent(deviceId, label, {details, seq}) |
App 级 | APP_START, RECONNECT_SEED, GRACE_EXPIRED, BLE_READY |
logParser(deviceId, label, {details, seq}) |
解析器 | PROBE_15B, UNSOLICITED_12B, DOCKED_8B, HEARTBEAT_2B, CM4_D_HEX, CM4_SETT== |
logCore(deviceId, label, {details, seq})(代码中无 logBle 方法;BLE 连接层事件经 logCore 记录,CONNECT_RESULT 等部分亦走 logAppEvent) |
BLE 连接层 | CONNECT_RESULT, DISCONNECT_REASON, CONNECT_GATE_ENTER |
logTrace(deviceId, label, {details, seq}) |
低层字节 | ONAE05RX, DOCK_STAMP_RX, WRITE_AE03 |
logPermission(phase, permissionName, {status, error}) |
权限 | UPFRONT_FLOW, SETTINGS_PROMPT, PERMANENTLY_DENIED(注:VOLUME_CHECK_* 实际由 logAppEvent 记录) |
logCore(deviceId, label, {details, seq}) |
核心生命周期 | APP_EXIT_CLEANUP, APP_VERSION, PLATFORM, ALARM_FRESH_CONNECT_RESET, SESSION_ALARM_EVAL / SESSION_ALARM_SKIP(TAPD #1003078 / c458c44 起的 session-alarm 决策诊断,state-change-gated), ALARM_EVAL_CONN(TAPD #1003095 / cd18bb1:alarm eval 全局 connState gate transition-gated,揭示 disconnected 整体早返是否吞掉其它设备 alarm), ALARM_OVERTEMP_EVAL(TAPD #1003095/#1003083 / cd18bb1:per-probe >= internalOverTempF 进入 trigger 前 once-per-episode 诊断,配合 PROBE_ROUTED 切分「未跑 / 跑了未 fire / trigger() 抑制」三态) |
logCrash(source, error, stack?, fatal?) |
未捕获错误 / 崩溃捕获 | CRASH(60fa696 / TAPD #1003152;main.dart 在 runApp 前接 FlutterError.onError(framework build/layout/paint)+ PlatformDispatcher.onError(root-zone 未捕获异步);与 logCore / logPermission 同档无条件写、不受 dev-mode 开关影响。落盘走 _addEntry(skipLiveFile: true) + _emergencySyncAppend 同步写盘(init 时缓存 _docsDirPath 到字段,崩溃 handler 不能再 await),保证就算 native crash / LMK 紧接着 tear-down 也能落一行;stack 顶 30 帧用 | 拼到 detail 单行不超长。只截 Dart 层——原生 SIGSEGV / OS low-memory kill 不进这两个 handler,崩溃报告里没有 CRASH 行本身就是「native cause 或 LMK reclaim、不是 Dart 异常」的诊断信号)⚠️ 7a1f7c3 起 PlatformDispatcher.onError 把 gotrue AuthException 列为可恢复类:仍 logCrash、但记 fatal: false 并 return true 吞掉(不再传播),仅此一条;其它 root-zone 异常仍 fatal: true 并 return false 与本 callout 描述一致传播。理由:SupabaseAuthService 内部 App-initiated 调用都已 catch + map AuthException,能溜到 root zone 的 AuthException 必是 supabase_flutter _handleIncomingLinks 那条 OAuth deep-link listener throw 的 SDK 内部异常,先前每次 Google sign-in 失败(如错误 client secret / 未授权 redirect URI)都打挂 App(field log 2026-06-15 5× 闪退)。注:这只止 crash、Google sign-in 仍依赖 Google Cloud console OAuth 配置(client secret / authorized redirect URI https://<ref>.supabase.co/auth/v1/callback)单独修复。 |
#<seq> 标签)问题:一个 AE05 字节到达 → 进 parser → dispatch → handler → emit telemetry → UI 更新,日志里会散落在 5 行不同事件。事后怎么串起来?
解决:每次 AE05 RX 调一次 DevLogService.nextPacketSeq() 拿递增序号,stamp 到 CM4Message 上。所有下游日志都带同一个 #<seq> 标签。
好处:grep #42 就能看完这个包的整条轨迹。
进入:设备列表页标题 "CulinaTech" 3 秒内连续点击 5 次(v4.1 规则 7)。
可见:DevOverlay 弹出,五个 tab:
每条事件带:
DateTime.now(),带毫秒)#<seq>(如果有 packet 关联)0x55B1 → "legacy LOCK-REFRESH 0x55B1")Per Liang 2026-05-14 / 33b4e62:
dev_overlay的颜色映射已修正为 V5 canonical:1=Black / 2=White / 3=Blue / 4=Yellow(旧错误的1=Blue / 3=Black / 4=Red已替换)。DevLogEntry自带 deviceId 列;ProbeNumber.colorName短形实际用于 Stats Panel 的探针列表显示(_StatsRow中p.number.colorName),而非日志条目的decoded字段(日志只展示decoded/hexDump,不存在details字段)。
culinatech_log_{时间戳}.txt(例:culinatech_log_20260518111444.txt)
一种格式(dev_log_service.dart 的 exportSnapshot):
culinatech_live.log.1、culinatech_live.log)以及当前内存快照有这个日志可以直接 grep 所有协议事件,比凭记忆还原问题高效得多。Liang 调 alarm-related TAPD(bg-test、targetReached fire-and-clear、fresh-connect reset 等)就是靠这套 log 定位时间线的——详见 告警与通知 §alarmStateProvider 的 keep-alive。
DevLogService 中通过以下布尔字段控制(无 debugFlags 聚合对象):
| Flag | 默认 | 控制什么 |
|---|---|---|
_enabled |
true (tester) / false (prod) | 开发者模式全局开关,控制大部分事件记录 |
_autoSave |
true | 是否自动写入持久化 live log 文件 |
_devVerboseTracing |
false | 低层字节 trace(极度嘈杂,对应 logTrace) |
持久化:通过 SharedPreferences 持久化,重启 App 会保留上次设置。
切换位置:DevOverlay 底部控制区(Auto-save / Verbose Tracing 开关)。
Provider:useMockServiceProvider(device_providers.dart:47,Settings 页的开发者开关)
activeDeviceServiceProvider 切到 MockDeviceService(lib/core/services/mock_device_service.dart,354 行),不走 BLE 直接产生虚假数据流BleDeviceService(real)用途:UI 开发、演示、集成测试 —— 不需要真中继盒也能看 App 工作。Mock 会模拟 CM4(4探针)与 CM1(1探针)多探针、温度上升曲线、入仓事件等。
不持久化:session-only,App 重启回 real。
AE05_SUBSCRIBEDCONNECT_RESULT(timeout? permission denied? CONNECT_GATE_ENTER/EXIT 看 _connectGate 是否在串行化等待)UPFRONT_FLOW 或 PERMANENTLY_DENIED 确认权限状态PROBE_15B(legacy)或 CM4_D_HEX(CM4)事件 → 看 intTempAdc, extTempAdc 原始值tempIntArray / tempExtArray(lib/core/protocol/temperature_lookup.dart)反查isInternalTempBelowRange / AboveRange 是否被设DISCONNECT_REASON_lastDockEventAt 是否被 stamped(应该在 0x07 的上一行附近,DOCK_STAMP_RX 在 RX 字节层打戳)RECONNECT_ATTEMPT 是否按节奏触发(前台 + 后台 <4h 始终 15s;后台 ≥4h 走 15/60s/5min;看 BG_CADENCE_TIERED_ENTER 确认 4h 门是否已开)GRACE_EXPIRED 是否打了——打了说明 90 秒宽限期已耗尽,进入 lostConnectionAlarmService._active[(deviceId, probe)] 是否包含预期类型(通过 alarmStateProvider 评估日志)Booster.isInGracePeriod —— 若为 true,probe-level 报警被冻结(booster-level 状态报警不冻结)_userDismissed 集合,确认没被误 dismiss——reset() 通常不清这个集合(V5 修复 6d2a35f 后 probe lowBattery 的 dismiss flag 也保留)ALARM_FRESH_CONNECT_RESET 是否触发过——offline → connected 转换会清整组 _active / _userDismissedconnectedBoostersProvider emit 时序 —— 每次新遥测应触发 container.refresh(alarmStateProvider)(main.dart 的 listener)invalidateSelf 不工作—— 评估器必须用 container.refresh,详见 告警与通知 §alarmStateProvider 的 keep-alive 与 TAPD 2026-05-13 / 099e442Timer.periodic 兜底应保证 no-telemetry 场景下 probe-disconnect 60s 阈值能 fire