以下五条用户流程是 App 的主要叙事骨架。每条都串起 用户动作 → 页面 → Provider/Service → BLE 流量 → 硬件响应 → UI 状态切换 全链路,供 agent 和新人快速建立端到端的心智模型。
另一个当前代码里重要、且与这些流程直接衔接的真实启动行为是:冷启动读取已知设备后,App 会先调用 surfaceKnownDevices(),把持久化设备立刻以离线卡片显示到 Dashboard;随后才对 BLE 设备执行 seedReconnectQueue(...),并对 cloud-capable 设备启动 startManaging(..., bleOnly: false)。因此用户在首页通常会先看到完整设备列表,再等待 BLE/MQTT 把这些卡片逐步点亮。
若用户已登录,_initKnownDevices() 在 surfacing/seed 之后还会继续执行 _ensureAccountSession()(≤4 台手机登录位准入)、reconcileAccountRegion(ref) 与 syncAccountBoundDevices(ref),并启动 deviceBindingRealtimeProvider 等设备列表实时订阅,使本机舰队与账号其他手机收敛;这与仅依赖本地已知设备列表是并行发生的。
另一个当前代码里重要但本页尚未覆盖的真实启动流程是:进入这些主流程前,App 还会在首启时先展示 PermissionsIntroPage,之后按顺序执行缺权限提醒、系统蓝牙关闭提醒、通知/铃声音量过低提醒,以及 Android 后台权限引导。这些检查都在 main.dart 的启动后首帧链路里串行执行,会直接影响用户能否顺利进入后续配对与烹饪流程。另外,Android 上若 BackgroundKillWatchdog 判定这是一次“后台监控中被系统杀死后的冷启动”,且机型存在 OEM 自启动限制,App 还会在上述链路后追加一次 '/background-permission-renudge' 自启动再提醒;全部启动检查完成后,才会继续尝试展示首页的首次使用引导(home onboarding)。字节级细节(0x55AA 格式、锁刷新间隔、断开错误码 0x07、温度查表规则等)见 v4.2 BLE 协议规范 与 CM4 协议。
CM4 设备在连接成功后不会在这里直接结束添加流程;ScanPage 会先跳转到 ConnectionModePage(/connection-mode),再继续后续的连接模式 / WiFi 配网流程。只有非 CM4 设备才在这里直接完成添加。
此外,CM4 在进入这段添加子流程前,会先通过 deviceSetupProvider.begin(deviceId) 把设备标记为“in setup”,从而抑制刚连上时首批遥测触发的报警(例如“探针外温过低”)。这个标记会在添加完成、连接失败,或用户从连接模式页返回放弃时清除。
另一个当前代码里很重要、但本页尚未点出的事实是:CM4 的 WiFi 配网虽然目标是云连接,但整个 SSID/密码下发、重新加锁与 WIFI_STS 轮询过程仍走 BLE,因此配网期间手机蓝牙必须保持开启,否则流程会失败。
实际分支还更细:在 ConnectionModePage 里,用户若选择 WiFi,已登录用户直接进入 '/wifi-select',未登录用户先进入 '/sign-in';若选择蓝牙模式,则直接调用 DeviceGuidePage.showAfterDeviceAdd(context) 完成添加并展示操作指引。另一个当前代码里的真实分支是:iOS 设备在进入上述 WiFi 分支前,会先弹出本地网络权限确认框;只有用户确认后才调用 AppPermissions.requestCM4LocalNetwork() 并继续跳转,取消则停留在当前页。
ScanPageScanPage 通过 activeDeviceServiceProvider.scanForDevices() 开始 BLE 扫描;ScanPage 在 rootRouteObserver 上 subscribe,子页面 push 上来时主动暂停扫描以让出 BLE 优先权(避免和重连循环抢扫描槽)Booster 模型(名称 / 信号 / 电量),用户点击某台设备ScanPage 调用 service.connect(deviceId, trigger: 'user-select')AE30 → 解析特征 AE03(写)+ AE05(通知) → BleDeviceService._transportFor(deviceId) 根据 deviceId.boosterFamily 懒创建 LegacyBoosterTransport 或 Cm4BoosterTransport → transport 写出 family-specific 独占锁刷新(legacy 0x55B1 每 45s / CM4 ASCII CNT_0 每 45s——per Liang 2026-06-06 / c613148 起两家族同 cadence)CookingPage(deviceId, probeNumber)(路由参数 (String, ProbeNumber),见 app.dart:218-229)CookingPage 从已有会话 / 待定参数 / 探针默认值中恢复 target / 肉种 / 熟度CookingSessionsNotifier 创建 CookingSession(isActive=true);iOS 上 cookingLiveActivitySyncProvider 同步 start 一张 Live Activity 卡(锁屏 / 灵动岛),Android 静默 no-opalarmStateProvider 驱动——main.dart 里的 container.listen(connectedBoostersProvider, ... container.refresh(alarmStateProvider)) 让每次新遥测都触发重评;额外有 5s Timer.periodic 兜底 no-telemetry 场景(probe-disconnect 阈值无新包不会自然触发)AlarmType.targetReached → 顶部 AlarmBannerHost banner(不是全屏;full-screen WarningPage 仅留给 internalOverTemp / ambientOverTemp 这两类 safety alarm,见 app.dart:83-110)+ 响铃 + 震动0x07 断开 → 检查 _lastDockEventAt:
_dockShutoffWindow=1s) → 归类为「入仓关机」,发 DeviceStatus.boosterShuttingDown → 10 秒过渡(_dockShutoffUiDelay=10s) → DeviceStatus.boosterOff;首次重连延迟 8 秒(_dockShutoffReconnectDelay)gracePeriod=90s),UI 继续显示最后已知温度;前台始终 15 秒一次扫描尝试重连gracePeriod 结束;UI/报警层单独使用 _lostConnectionAfter = 60s 作为可见翻转阈值。也就是说,60 秒无遥测时就会翻成可见断连并触发 AlarmType.boosterDisconnected,而 BLE 层仍可继续处在 90 秒 silent retry 窗口内。GRACE_RECOVERED 日志、卡片无缝切回在线(用户甚至察觉不到);与之相应的 booster.lastConnectedAt 重新打点,alarm 评估器对此设备暂停 probe-disconnect 评估 60s(让残留 15B 静默自然刷新或被确认)若用户主动划掉/销毁 App 进程(AppLifecycleState.detached 或原生 onDestroy/onTaskRemoved 触发 _AppExit.run),会在 BLE/MQTT 关闭前先调用 cookingSessionsProvider.notifier.finalizeActiveForAppExit(),把当时所有 active 会话就地收尾并写入 Cook Log(“记到哪算哪”);这与中继盒断连后保持 active 不同,是进程退出时的显式结算路径。
0x55AA,_onAe05Rx 在 RX 时刻立即打点 _lastDockEventAt[deviceId](不在 handler 里打——0x07 断开可能在 ~65 ms 内到,先于 buffered handler)BleDeviceService._handleProbeStatus 将相应 Probe.isDocked = true;触发 AlarmType.probeDocked 单声+通知(per Liang 2026-04-30 #18 a)0x07 断开_lastDockEventAt 在 1s 内,归类为入仓关机:发 DeviceStatus.boosterShuttingDown → 等 10 秒过渡 → 发 DeviceStatus.boosterOff → 单声 AlarmType.boosterPoweredOff 通知(per Liang #18 — 期望行为也要明确确认)_transportFor 懒创建deviceId.hashCode.abs() % (heartbeatIntervalSeconds × 1000)(毫秒)相位错峰(ble_service.dart:1422),不会同步堵塞 BLE 命令队列CookingSession 为 (CM4_A, ProbeNumber.probe1) 创建CookingSession 为 (CM3_B, ProbeNumber.probe3) 创建AlarmKey = (deviceId, ProbeNumber?, AlarmType) 三元组键(alarm_service.dart:117,TAPD 2026-05-11 多探针回归后定型;老代码曾以 deviceId 单键导致 Probe 1 vs Probe 2 同事件被吞)startTime 重新排序。当前代码里还有一个与删除设备直接相关、但本页未覆盖的真实分支:当用户已登录账号后,App 会订阅账号维度的设备列表实时变更;如果同一账号在另一台手机上删除了某个设备,本机会通过 deviceBindingRealtimeProvider 收到 onDelete,并立即调用 forgetDevice(ref, name, unbind: false) 做完整本地拆除(注释写明需 full teardown,而非 bare removeDevice),而不是等到下次重启再同步。
实际代码里,删除设备前会先结束该设备的所有 active CookingSession;但写入 Cook Log 时会跳过 remoteStarted 的镜像会话,只归档本机拥有的会话。这样既避免未来重新添加同一 deviceId 时旧会话“复活”,也避免把另一台手机发起的会话重复写入账号级 Cook Log。
另一个当前代码里很重要、但本页尚未点出的事实是:显式删除和跨手机实时删除都会把该 deviceId 记入 _forgotten deny-list。这样即便删除后又收到 BLE 断连尾包、云端尾包,或 staleness 定时器的后续重算,刚删除的设备卡片也不会被异步数据重新“拉回” Dashboard。
此外,显式删除(Forget Device)时会同步断开该设备的云连接、清除本地 WiFi/cloud-configured 残留,并调用 _bindingSvc.unbind(deviceId) 解除账号绑定,避免设备在下次启动或账号同步时被当作已配置云设备重新拉回;而跨手机实时删除路径会传 unbind:false,因为服务端绑定行已先被另一台手机删掉,本机不会再重复调用 _bindingSvc.unbind(deviceId)。当前实现还会检查“上次设备”相关偏好:如果它正指向被删除的这台设备,就调用 BleDeviceService.clearLastBoosterIfMatches(deviceId) 清空该记录,并 invalidate(lastBoosterNameProvider),避免断连 banner 继续显示刚删除的设备名。
ConnectedBoostersNotifier.removeDevice(deviceId) 移除卡片;随后 removeFromReconnectQueue(deviceId) 清扫重连相关状态,deviceConnectionManagerProvider.stopManaging(deviceId) 停掉该设备的连接管理,最后才调用 bleService.disconnectDevice(deviceId) 断开这台设备的 BLE 连接removeFromReconnectQueue(deviceId) 实际只清理重连队列、尝试计数、入仓/关机相关时间戳、shutoff timer 与 PendingIntent 映射等“派生重连状态”;它不会在这里直接删除 _devices 或 _transports 条目。ConnectedBoostersNotifier.removeDevice(deviceId) 内部就会调用 _knownSvc.remove(deviceId) 从 SharedPreferences 删除已知设备;这不是后续单独再执行的一步。DEVICE_FORGOTTEN 事件字节级细节(0x55AA 格式、锁刷新间隔、断开错误码 0x07、温度查表规则、CM4 SET_RD / SET_01..04=ABCDEF 命令等)见:
本页只讲用户视角和 App 的反应。