App 启动后的主屏幕:以「中继盒外卡 + 探针子卡」为单位列出所有已配对设备的实时状态,提供入口到扫描、烹饪、烹饪日志、设置、帮助。
lib/features/thermometer/presentation/pages/dashboard_page.dart(1969 行,自包含 18+ 内部 widget)connectedBoostersProvider / connectionStateProvider / connectionModeProvider / cookingSessionsProvider / lastBoosterNameProvider(均在 lib/core/providers/device_providers.dart)BleDeviceService(重连 / 断开 / 设备清理)、DevLogService(用户操作埋点)/dashboard(DashboardPage.routeName)lib/app.dart —— initialRoute: DashboardPage.routeName + routes map(App 启动落点)顶部是一个标题 + 右侧 "+" 按钮的 header(45d9916 起 "+" 按钮挂 file-level _dashboardAddButtonKey: GlobalKey),下方紧跟一条连接状态横幅(仅在重连 / 刚重连 / 已断开三种非默认态下出现;_ConnectionBanner 的标题名来源 9d4b51a / TAPD #1003086 起两路分开——「Reconnecting to …」走 live BleDeviceService.reconnectQueue,「Connected to X」9d4b51a 起优先用 live in-memory BleDeviceService.justReconnectedDeviceId(scan-matched 的那台、_setState(justReconnected) 同帧 stamp),缺时才 fallback 到原 lastBoosterNameProvider——后者是单 cached 全局值、reconnect 时不会重读,在多设备同时断 + 用户先把 B 设备 pull-out 复连的场景下会命名错;fallback 路径保留只是兜底,正常路径不会经过它)。中央主区域是一列中继盒外卡(ListView.builder)—— 每张卡顶部一行显示连接圆点 / 设备 ID / ℹ 信息按钮 / 状态文字 / 电量百分比 / Booster RSSI 徽章 (legacy 仅)(per spec 2026-04-30 audit reply:≥-65 绿、-66 至 -85 橙、<-85 红,booster.rssi == null 时整块隐藏——首次 readRssi() 落地前的 ~10 秒。⚠️ 2483d1b / TAPD #1003131 起仅画信号格图标——原 ${rssi}dBm 数值文本删除,bars + 颜色已足够 at-a-glance 表达链路质量;精确 dBm 仍可在 ℹ 信息对话框看,详见下方 Action #5 ℹ 信息。⚠️ 4fa4487 / TAPD #1003173 起 CM4 header 不显示这块——legacy 的"信号格图标 + RSSI 颜色"原义对应单一 BLE 链路;CM4 的 BT 状态由下方 per-transport cluster 单独呈现,徽章 if 加 !booster.family.isCm4 闸把 CM4 行从这条 header 上拆走,避免与 WiFi 同行混淆)/ CM4 仅 per-transport cluster (Bluetooth | WiFi | gear)(2c8a636 / TAPD #1003173 起接进、5586d80 / TAPD #1003176 把 Bluetooth glyph 的 keying 从全局 connMode 改成 per-device 链路:Bluetooth glyph 读 isBleLinkUp = bleService.connectedDeviceIds.contains(deviceId)——真有 GATT 链路(含 90 s grace)就画信号格图标按 booster.rssi 颜色染(_rssiColor,缺 RSSI fallback 绿),没链路则画同一图标但白色 30% alpha("BT idle 可用")。⚠️ b056ec6 / TAPD #1003176(QA 黄小青 2026-06-05)起 glyph 由 Icons.bluetooth_connected / Icons.bluetooth 换成 Icons.signal_cellular_alt——与 legacy CM1/CM2/CM3 行上方的"信号格图标 + RSSI 颜色"徽章统一,但 keying 仍是 per-device isBleLinkUp(不是 RSSI 直读、也不是 connMode),dual-channel CM4 在 WiFi 模式 + BLE 还活时仍读绿。这覆盖 dual-channel CM4 的关键情形:WiFi 模式 + 蓝牙仍开时 BLE 链路真活就该绿,不能因 connMode == cloud 就一刀切灰——这是 TAPD #1003176 对 TAPD #1003173 "grey = on cloud" 读法的修正。⚠️ ae9524f / TAPD #1003222 起 fixed-shape Icons.signal_cellular_alt 替换为可变 1–4 信号格(per Liang 2026-06-16「BLE RSSI 按信号条显示」table,关 Liang 0615 build 的「未实现」flag):渲染走 _CellularBars 自绘 painter(16 px 对齐 WiFi glyph),档位由 BtLinkDisplayState 决定——枚举从 3-tier strong / weak / veryWeak / disconnected 改为 4-tier bars4 (> -60 dBm,极强) / bars3 (-70 ~ -60,良好) / bars2 (-80 ~ -70,中等) / bars1 (≤ -80,弱) / disconnected (断连),边界落到低档(-60 → 3 / -70 → 2 / -80 → 1);palette top 两档绿、bars2 橙、bars1 红、disconnected 每格 dim 30% alpha 白;rssi == null(首次 readRssi 在飞)乐观 fallback 满 4 格。共用 btLinkDisplayState({gattLinkUp, graceActive, deviceStatus, rssi}) resolver 让 CM4 与 legacy CM1/2/3 同档(先前 ticket observation 2–3 实证两族在 disconnect 中分歧:CM4 灰、CM1 卡 stale red)——本 commit 只升档位 + 渲染、resolver 共享路径自前面已合并不动。WiFi glyph (_WifiSignal) 自 4f26473(Kevin 2026-06-11)起无条件渲 booster.wifiStatus.firmwareValue 1–4 graded 白 bars(0 时画 dot + 斜线 dim 灰)——展示中继盒自己有没有 join WiFi 路由器(WIFI_STS,BLE 每 2 s auto-push、cloud 通道同步),与 App 当前走哪条传输无关。先前 isCloudConnected keying 把"booster joined WiFi"与"App 当前用 cloud"两个独立 fact 误捆——CM4 已 join WiFi 但手机优先走 in-range BLE 时 glyph 永远斜线 dim,与实际 WiFi 状态相反。_WifiSignal 自绘 fan(dot + 3 段圆弧)避免 Material WiFi glyph 高码位 tree-shake 不稳定;TAPD #1003131 起 Liang 2026-06-02 确认 WiFi 是 booster 级 fact——per-probe wifi_off glyph 与 _DeviceCardData.wifiBars 字段已删除。⚠️ 6c072c7 / TAPD #1003275(per Liang 2026-06-17「WIFI icon 不要消失」)起 WiFi glyph 自身脱钩 isOnline:先前 if (isOnline && booster.family.isCm4) 整块(glyph + gear)一起 hide,蓝牙关时(device → reconnecting/offline)WiFi 图标随整条 cluster 消失;新代码闸拆成 if (booster.family.isCm4) 渲 glyph、内部再 if (isOnline) 渲 gear——CM4 卡 WiFi glyph 永远在,isOnline 决定 bars 取值:isOnline ? booster.wifiStatus.firmwareValue : 0,offline 强制画 dot + 斜线 dim 灰(per Liang「WIFI 没连接时 灰色带斜杆」)。理由:offline 快照 copyWith 不重置 wifiStatus,stale 值不能伪装成 live 信号;只有 live 通道(BLE up / grace / cloud-live = isOnline)才允许读真实 WIFI_STS bars。Settings 齿轮仍 online-gated(其行需要 live 链路读 / 写,详见下方 §配置齿轮)。Cloud icon redefinition(app↔server)0ae9381 / Per Liang 2026-06-17 / Kevin Option B「先把功能做全」落地(同 ticket 后半,2026-06-06 BLE-primary drop-MQTT 省流改动被反转作 post-launch optimization):header cloud badge 改读新加 cloudServerConnectedProvider(StreamProvider<bool> 喂 MqttService.isConnected / connectionStateStream),蓝 = App 持 broker session / 灰 = 没持,不再 keying connMode == DeviceConnectionMode.cloud——后者 BLE 一起来就翻 grey,配对成功瞬间 badge 错误读灰(#1003275 报告的现象根因)。配套 DeviceConnectionManager 改 WiFi-configured 设备无论 _isBleUp 都 connect broker、_onBleStateChange 删 _mqtt.disconnect()、新加 _ensureBrokerSession() BLE-up 时幂等 (re)open,详见 MQTT 与云端 c613148 callout 末尾的 0ae9381 follow-up。同次 TAPD #1003176 dashboard 还把 _ConnectionBanner.suppressReconnecting 接通:固件在 WiFi 模式下连上 BLE 约 30 s 就主动 shed GATT 链路(CM4 用 MQTT 推遥测、BLE 仅作 transient handshake),导致设备永久挂在 BLE reconnect 队列、banner 永远显示 "Reconnecting to …" 罩在一台其实在云端正常工作的设备上面;先前逻辑在全局 connMode == cloud 下把 reconnect 队列减去 cloudConnectedIds(deviceStatus == connected 的 boosters),剩余为空时 suppressReconnectingBanner = true,banner 整体隐藏。⚠️ 875da30 / TAPD #1003164 req 2 起改 per-device 信号:全局 connMode 在每次 WiFi-mode 瞬时 BLE handshake 都会翻成 ble,原 #1003176 patch 只在 connMode 恰好读到 cloud 时生效,BT 真断时常常仍显示 "Reconnecting";新代码改读 ConnectedBoostersNotifier.cloudLiveDeviceIds——isCloudLive(lastFromCloud, silentFor, threshold = 75 s) predicate(visible-for-testing,4 个单测)按 notifier 自带的 _lastFromCloud + _lastSeen 在 75 s cloud-liveness 窗口内逐设备判定,仍在收云端帧的设备直接从 reconnect banner 移除,与全局 mode 解耦;BLE-only reconnect UX 不受影响(设备从未走过 cloud → _lastFromCloud=false → 不在 cloudLive 集合里 → 正常进 banner)。boosterDisconnected 报警本就 cloud-safe(gated 在 deviceStatus == lostConnection,75 s cloud grace 在 cloud live 时永远阻止该状态翻入)。⚠️ a87c314 / TAPD #1003247 起 disconnected 终态也接同一闸——875da30 的 suppressReconnecting 只罩重连中态,但 CM4 配网后 ~30 s shed GATT 链路、90 s BLE grace 过期后全局 connectionStateProvider 翻 disconnected、红色「Lost connection to booster」banner 仍然出现(field log 0613:GRACE_EXPIRED @102927 → connState=disconnected mode=cloud、MQTT_RX 整段 screenshot 期间连续流)。新加 visible-for-testing shouldHideLostBannerForCloud({shownDeviceIds, cloudLiveDeviceIds}) predicate(connection_display_state.dart,7 个单测,与 875da30 同源逐设备信号):只在 dashboard 显示的每一台 booster 都 cloud-live 时才 suppress——真断的 BLE-only booster 或多设备 fleet 里全 transport 掉线的那台仍 surface 红 banner;legacy CM1/2/3 无 cloud 通道、BLE-only banner UX 不动。_ConnectionBanner 新增 suppressDisconnected: bool 参数,dashboard build 时把 boosters.map((b) => b.deviceId) + cloudConnectedIds 喂 predicate 后传入。⚠️ ce8eb0d / TAPD #1003266 起 cloudConnectedIds 改 union cloudLiveDeviceIds 与新加的 DeviceConnectionManager.cloudActiveDeviceId——后者读 _mode == cloud ? _deviceId : null、_tryMqttFallover 顶端 _setMode(cloud) 先 emit 再 await _mqtt.connect(broker 失败再自校到 disconnected),让 dashboard 在 ~1.5 s MQTT cold-connect 期就把该设备算 cloud-reachable,避开 BT 关瞬间 cloudLiveDeviceIds 还 telemetry-empty、reconnect queue 内有该设备时 "Reconnecting…" / "Lost connection" banner 一闪的 #1003266 blip(先前 #1003176 让 BLE primary 时 MQTT 主动断,BT 关后必须 cold-connect ~1.5 s 才订阅、~2.3 s 才第一帧、整个 gap 内 telemetry-driven 闸没机会生效)。BT signal glyph 仍由 isBleLinkUp keying 正确变灰(radio 真断、#1003222 保留),灰 BT + cloud badge 现在的语义是「正经走 cloud」而不是 disconnect。待 Liang 确认固件:~20–30 s 后主动断 BLE GATT 是否 intended) / CM4 仅 配置齿轮(242ec8c / TAPD #1003131 part 4:WiFi 指示右侧再叠一个 Icons.settings 入口,tap 跳新 BoosterConfigurationsPage(/booster-config)做 per-device 设置——Connection Mode 底部 sheet 二选(内部走 DeviceConnectionManager.switchToBle / switchToCloud,不复用 ConnectionModePage 那条 onboarding 路径,避免 BLE 选项触发 showAfterDeviceAdd 把用户落回 fresh dashboard)/ WiFi Network → WifiSetupPage 重配 / Unbind 调共享 forgetDevice()(242ec8c 抽到 lib/core/services/device_actions.dart,与本页长按菜单同源——ghost-session safeguards 集中在一处)/ Display Mode 已激活(b4592c5 / TAPD #1003139,booster sw V09 / ESP V11 起 TEMP_INT / TEMP_EXT / TEMP_ALL,bottom-sheet 三选 Internal / External / Cycle,write-only 经 BleDeviceService.sendBoosterSettingTo 发出 + snackbar 确认(fc7e379 / TAPD #1003254 起:DISP 与 Buzzer RI_CNT 从原 raw sendCommandTo / sendCommandAsyncTo 改走 sendBoosterSettingTo,经 BoosterTransport.sendSetting 走 CM4 transport 的 BLE-or-cloud fallback,BLE 不在时经 /CM4/<id>/sett 推送送达——先前 WiFi 模式 + 蓝牙关时 buzzer / display 切换被 BLE-only 路径静默吞掉、永远不到中继盒);efd6ce8 / TAPD #1003204 起 sheet ✓ + row subtitle 也接通——新 boosterDisplayModeProvider(per-device SharedPreferences key display_mode_<deviceId>,与 Buzzer buzzerRingModeProvider 同 pattern,但无 verify query 可发)记录每次 pick + tap 同时 set() + 发线 + snackbar。⚠️ Per Liang 2026-06-10(97fe331 / TAPD #1003204 sub-correction)澄清:efd6ce8 wiki 写的「物理按键改不了 / App 是 sole writer」是错的——模式可在中继盒按键改 + 跨开关机保留 + 出厂默认 = TEMP_ALL(Cycle)。Provider 因此种子改为 displayCycle(state 类型 String? → String),fresh install / 同一账号第二台手机首次 pick 前 sheet ✓ 已亮 Cycle、row subtitle 显 Cycle;FW 已在开发 TEMP 查询命令,上线后按 RI_CNT=? pattern 接 on-connect read-back,否则物理按键改了 App 侧记录会 stale。详见 CM4 协议 §显示模式);Buzzer 已激活(7c141d7 / TAPD #1003203):Buzzer 行 tap 弹底部 sheet 5 选项(Continuous / 3 Beeps / 1 Minute / 5 Minutes / Mute——per Liang 2026-06-10:device-level Mute 由 RI_CNT=4 承接,不走 #1003138 A 字段),命中当前选项 green ✓;每次 tap 同时 buzzerRingModeProvider.set(N) 持久化 + 经 BleDeviceService.sendBoosterSettingTo 发 RI_CNT=N(fc7e379 / TAPD #1003254 起改走该接口走 BLE-or-cloud fallback,详见上方 Display Mode 同段说明),row subtitle 即刻反映。关键:booster 接收并保存 RI_CNT=N 但不应答任何 RI_CNT 帧(log 实证:每条 RI_CNT 都是 App→booster、零 reply,含 RI_CNT=?),所以 ✓ / subtitle 都直接读 buzzerRingModeProvider 本地持久化(SharedPreferences key buzzer_ring_mode_<deviceId>,默认 0=持续),跨机型一致——之前依赖 booster read-back 的设计在 Xiaomi 上 ✓ 隐形、Pixel 上 stale。⚠️ a5cf346 / TAPD #1003217 起 Mute 镜像写入 active cook:用户 pick Mute / 切回任一非 Mute mode 时,App 曾按该中继盒的 active CM4 cook 队列遍历调 transport.setProbeTarget(..., silent: <true|false>)(per-cook 间隔 250 ms 满足 AE03 同设备 ≥200 ms 间距)把每根探针的 SET A 字段翻到 silent / ring,让 booster LCD 在 already-armed 的 cook 上也立即渲染静音 / 斜线 icon;序列尾追发 RI_CNT=? verify。⚠️ 738fdeb / TAPD #1003233(Beta 2026-06-11)整体撤回该镜像——固件实证只接受 A∈{0,1}、任何 A=2 让整条 SET_<color>= 被丢弃(log SET_BU=214F25 无 SETT== ack 也无 LCD 更新),_sendBuzzerModeSequence 缩回纯 RI_CNT=N + 400 ms 间距 + RI_CNT=? verify 的 set + paced verify 两步;_activeSessionsForBuzzerRefresh 辅助 + CM4_BUZZER_MODE_TARGET_REFRESH 日志一并删除,Mute 自此纯走 booster-level RI_CNT=4、不再下传 per-cook。⚠️ c002576 / TAPD #1003273 起再缩到 RI_CNT=N 单条——booster 每收一条 RI_CNT 都 beep 一声,set + 400 ms 后 RI_CNT=? verify 让箱子响 "di----di"(WiFi 模式两条都 hit /sett、偶尔 MQTT message coalesce 才只响一声 "di");_sendBuzzerModeSequence helper 与 ble_device_service.dart import 整体删,pick 直接 sendBoosterSettingTo(deviceId, CM4Command.setRingCount(mode)) 一发一停,设备真相仍走 on-connect RI_CNT=? 查询 + onRingCountReported bridge、本地 optimistic pick 撑到那时,RI_CNT 是 App-only-editable、刚发的值即权威。详见 CM4 协议 §探针配置;Liang 2026-06-02 同次砍掉 C server region / E rename / F booster unit 行——region + unit 留在全局设置页、rename 因无固件 BLE-name 命令未做),下方是各探针子卡或一个状态提示面板("All Probes Docked" / "Move closer" / "Turn booster back on")。屏幕底部是三个等宽胶囊按钮:Cook Log / Help / Settings。Page 顶层 Stack 还会按需叠两层 overlay:开发者模式打开时右上角 DevOverlay;首次启动叠 HomeOnboardingOverlay(lib/features/onboarding/home_onboarding_overlay.dart,TAPD #1003076 / 45d9916)——homeOnboardingProvider 翻 true 时 dim 整页 + 走 _dashboardAddButtonKey 测的 "+" 按钮 rect 做 spotlight cut-out,tap-to-advance 两连 coach-mark(welcome 指 Help / Support → "Add Device" 提示「连接探针需从中继盒取出」),末 tap 调 markSeenAndHide() 写持久化 flag home_onboarding_seen_v1;触发点在 _AppInitializer 链尾,详见 01-平台集成 §权限请求策略 §First-launch 引导链尾段。
_DevModeTapTarget._handleTapDevLogService.enabled → 弹 SnackBar "Dev mode ON / OFF" 持续 2 秒GestureDetector.onTap (in _DevModeTapTarget.build)
→ _handleTap()
→ DevLogService.instance.toggleEnabled()
→ DevLogService.instance.logUserAction('DEV_MODE_ON' | 'DEV_MODE_OFF')
→ ScaffoldMessenger.showSnackBar(...)
DevLogService.enabled 翻转(持久至 App 重启);产生一条 user-action 日志事件;DevOverlay 显示/隐藏Navigator.pushNamed(context, '/scan'))TextButton(_ConnectionBanner 的 onRescanTap 回调)/scan 路由onTap / onPressed
→ Navigator.pushNamed(context, '/scan')
→ app.dart routes map 命中 ScanPage()
activeDeviceServiceProvider.scanForDevices() 开始 BLE 扫描_DeviceGroupFrame 外框 GestureDetector.onLongPress(中继盒头部)_DeviceCard 子卡 onLongPress(在线探针)两者都转发到同一 onLongPress 回调,最终调 _showDeviceOptions。注意:长按子卡删的是整台中继盒,不是单根探针——这是有意的(dashboard 不支持单探针删除),但 UX 上易误导。
l.dashboardDeleteTitle 标题 + l.dashboardDeleteContent 正文 + l.dashboardCancel / l.dashboardDeleteAction 两个按钮,6 语本地化,per Liang 2026-05-08;en 文案 "Are you sure you want to delete {deviceId}?")onLongPress
→ _showDeviceOptions(context, ref, booster)
→ HapticFeedback.mediumImpact()
→ showDialog<bool>(...)
TextButton.onPressed → Navigator.pop(ctx, false) → _showDeviceOptions.then 收到 false → 不做任何事TextButton.onPressed → Navigator.pop(ctx, true) → forgetDevice(ref, deviceId)forgetDevice
→ ConnectedBoostersNotifier.isForgotten(deviceId)(防重入)
→ DevLogService.logUserAction('FORGET_DEVICE')
→ CookingSessionsNotifier.endAllSessionsForDevice(deviceId, cancelled)(per TAPD 2026-05-18 / 5f3ebb9 — 扫所有该设备活跃会话、关掉 isActive 写 endTime + status=cancelled,返回 closed copies)
→ 把 closed copies 逐个 cookLogProvider.addSession 归档(保留历史 peak temp / duration / history)
→ DevLogService.instance.logCore(deviceId, 'FORGET_DEVICE_SESSIONS_ENDED', details: '${count} active session(s) cancelled + archived to cook log')(仅在有活跃会话被关时发)
→ ConnectedBoostersNotifier.removeDevice(deviceId)
→ BleDeviceService.removeFromReconnectQueue(deviceId)
→ BleDeviceService.disconnectDevice(deviceId)
→ BleDeviceService.clearLastBoosterIfMatches(deviceId)
→ ref.invalidate(lastBoosterNameProvider)
→ DevLogService.logCore('DEVICE_FORGOTTEN')
cookingSessionsProvider 上该 deviceId 的所有活跃会话被关掉(isActive=false + endTime + cancelled),同步 SharedPreferences active_cooking_sessions key 落盘(per TAPD 2026-05-18 / 5f3ebb9——原先这一步缺失,导致 mid-cook FORGET 之后 ghost session 仍在 memory + prefs 里 isActive=true,重新加回同 deviceId 时按 (deviceId, ProbeNumber) key 反向附着,timer 还从原 Start 计时、target 锁在 pre-forget 值,期间别的 phone 的烹饪静默被覆盖)cookLogProvider,用户依然能在 cook log 看到本机被中断的烹饪历史(peak temp / duration / 已加载的 history);iOS Live Activity 通过 cookingLiveActivitySyncProvider 的 active→inactive 扫描自动结束,不用显式调KnownDevicesService 从 SharedPreferences 删除该 deviceId_reconnectQueue 移除该 ID;派生状态全清(_deviceAttempts / _boosterOffDevices / _lastDockEventAt / _shutoffTimers)ConnectedBoostersNotifier._forgotten deny-list 加入 deviceId(防止迟到的遥测重新创建卡片)FORGET_DEVICE(user action)+ FORGET_DEVICE_SESSIONS_ENDED(per 5f3ebb9,仅在有活跃会话被关时发)+ DEVICE_FORGOTTEN(core lifecycle)_DeviceCard.onTap(在线探针子卡)_InactiveProbeCard.onTap(离线 / 入仓探针)两个回调最终都调用 _DeviceGroupFrame 的 onProbeTap 参数,进同一目的地。即使探针离线用户也能进入——烹饪页会显示 "Probe not connected to booster box"。
CookingPage(deviceId, probeNumber)GestureDetector.onTap
→ onProbeTap(probe) callback
→ navigateInstant(context, CookingPage(deviceId, probeNumber),
settings: RouteSettings(
name: CookingPage.routeName,
arguments: (deviceId, probeNumber)))
cookingSessionsProvider 在烹饪页恢复_DeviceGroupFrame 头部小 GestureDetector.onTap → _showVersionInfoDialog 显示中继盒固件版本 + Booster BT RSSI(booster.rssi dBm,每 10 秒采样一次,null 时显示 "—";6458c32 / TAPD #1003141 起 label 由原 "Booster RSSI" 改为 "Booster BT RSSI" 与新增的 WiFi RSSI 区分)+ 各连接探针固件版本;firmwareVersion == 0 显示 — (syncing) 而非 V0(后者会误导用户)。⚠️ CM4 only 多两行(6458c32 / TAPD #1003141):WiFi MAC——booster.wifiMac 来自 Ver=V08+MAC.. 应答(per Liang 2026-06-04,详见 CM4 协议 §Ver=V<hex>+MAC... 版本 + WiFi MAC 应答),连接末尾 ~300 ms 触发后到达、单次 emit 由 snapshotsEqual 比对 wifiMac 防 dedup,null 时显 "—";Booster WiFi RSSI——优先取 booster.wifiRssiDbm(next-FW 改进,per Liang 2026-06-04),未到位时按当前 wifiStatus bar 档位 fallback 显 Liang 给的 dBm 区间(4 bar -30 ~ -55 dBm / 3 bar -55 ~ -65 / 2 bar -65 ~ -75 / 1 bar -75 ~ -85 / disconnected 显 "-")。⚠️ decf29b / TAPD #1003141 0613 双修:(a) V13 固件实际 push 精确 RSSI——WIFI_STS=N 第二行追加 RSSI=<n>dB(如 WIFI_STS=3\n RSSI=-62dB),先前 _parseWifiStatus 用 int.tryParse(value.trim()) 直接吞整尾 → null、0615 log 实证每条 WIFI_STS push 都被静默丢(dispatch 事件 0 条),导致 BT 模式 row 永远卡 "-"、WiFi 模式只读到 stale bar 档区间。parser 改 regex 分别抓 bar bucket + 可选 RSSI=<n>dB、Booster.wifiRssiDbm 这才真的有值;_wifiRssiText 同时把 wifiStatus == disconnected 闸前置——任何 stale exact 值都不能在 WiFi 掉线后泄露(0613 req「WIFI 未连接时显 -」)。BoosterNotifier.snapshotsEqual 加 _wifiRssiDiffers ≥3 dBm 粗筛吸收 2 s push jitter、stable signal 不每 tick rebuild dashboard。(b) WiFi MAC 跨 transport 持久化——cloud 帧 wifiMac=null(NIC MAC 只走 BLE VER 回复)先前会覆盖 BLE captured 值、WiFi 模式 dialog 翻 "—";ConnectedBoostersNotifier 现把 knownMac = booster.wifiMac ?? existing?.wifiMac ?? _knownSvc.get(...).wifiMac 喂回 merged 再 dedup/store,并把首见的 wifiMac 写进 KnownDevice.wifiMac(JSON persist)——WiFi-only cold start(本次 session 从未 BLE 连过)也照样显,0613 req「WIFI MAC 在任意连接状态都应该提供且显示」闭合。下方分割线之后再渲染 Booster Pre-alarm(booster.preAlarmActive → "Enabled" / "Disabled")+ 每根探针的 Alarm Enabled/Disabled、Target、Target (HEX)、Target (°F ref)、Target (°C ref) 五行——per spec 2026-04-30 audit,全部从 probeAlarmConfigProvider((deviceId, probe.number)) 实时读,新 12B 到达就刷新;HEX 与 °F/°C ref 是中继盒预转换的诊断字段,权威 °C 还是来自 App 侧 LUT 反查的 targetTempC。GestureDetector.onTap
→ _showVersionInfo(context, booster)
→ showDialog<void>(barrierDismissible: true)
→ 渲染 booster.firmwareVersion + booster.rssi + 每个 isConnected 探针的 firmwareVersion
→ Consumer.builder 读 booster.preAlarmActive +
probeAlarmConfigProvider((deviceId, probe.number)) 渲染 Alarm /
Target / Target (HEX) / Target (°F ref) / Target (°C ref) 五行
GestureDetector.onTap → Navigator.of(ctx).pop()status == DeviceStatus.lostConnection && !isOnline 时替代探针卡显示;云端仍在线时不会出现)DeviceStatus.boosterOff 时显示,配 "Turn booster back on" hint 文案)两者都包了 GestureDetector(onTap: isReconnecting ? null : onRetryTap)——isReconnecting 时不可点(避免重复触发)。
GestureDetector.onTap
→ onRetryTap callback (in _buildContent ListView itemBuilder)
→ DevLogService.logUserAction('RETRY_CONNECT', deviceId: ...)
→ BleDeviceService.retryConnect(deviceId)
→ _reconnectQueue.add(deviceId)
→ 触发下次 BLE 扫描
reconnectQueue 包含该设备 → UI 翻成扫描状态GestureDetector.onTap → navigateInstant(context, const CookLogPage())CookLogPageGestureDetector.onTap
→ navigateInstant(context, const CookLogPage())
cookLogProvider 加载持久化的会话历史(cap 50 条)GestureDetector.onTap → Navigator.pushNamed(context, HelpCenterPage.routeName)HelpCenterPage(路由 /help 注册在 lib/app.dart)GestureDetector.onTap → Navigator.pushNamed(context, SettingsPage.routeName)SettingsPage_buildContent 通过 ref.watch 订阅 7 个 Provider,任一变化都重建:temperatureUnitProvider(°C/°F 切换)、connectedBoostersProvider(设备增减 / 探针数据更新)、cookingSessionsProvider(会话开始/结束等状态变化);列表计时显示由 _LiveCookTimer 的本地 Timer 单独刷新、connectionStateProvider(App 级 BLE 状态)、lastBoosterNameProvider(配对/忘记设备)、connectionModeProvider(BLE/Cloud/Disconnected 模式)、cloudServerConnectedProvider(App↔MQTT broker 会话状态)。
bleDeviceServiceProvider 用 ref.read,不是订阅——服务实例本身不变,dashboard 通过它调方法 + 读临时集合(reconnectQueue / gaveUpDevices / reconnectAttempts)。这些集合的变化不会自动触发 rebuild ——它们只在其他 Provider 发新值时被一并取一遍当前值。
List-view 计时器:由独立 _LiveCookTimer StatefulWidget 渲染,自己维持一个 Timer 而非依赖 cookingSessionsProvider 节奏 tick。773d908(TAPD #1003101)起从「1 Hz 自滴答 + 显示秒(CM1 HH:MM:SS / CM2 MM:SS)」改为「每整分钟边界唤醒一次 + 统一 Hh Mm / Mm」:_scheduleNextTick 计算 60 - (elapsed.inSeconds % 60) 秒后下次唤醒、setState 后重新对齐到下一个整分钟边界(每 tick 重新对齐避免起始 mid-minute 漂移),约 60× 少于 chip rebuild。_format(start) 仅按 elapsed.inHours / inMinutes % 60 渲染、丢掉秒;两个并发 cooking session(CM1 + CM2)现在永远同一格式、不再 CM1 显时分秒而 CM2 只显分秒。完整烹饪页 (cooking_page.dart) 的 timer 渲染不变——这里只动了 dashboard list view 显示粒度。
| 项 | iOS | Android |
|---|---|---|
| Haptic 反馈 | HapticFeedback.mediumImpact()(同样调用) |
同 |
| 系统返回键 | 不适用 | 按 Return → PopScope(canPop: false) 拦截 → 弹 AlertDialog(v4.2 §五规则 8 文案:"If you wish to close the app, your phone will stop monitoring the temperature and you will no longer receive any alarms."),"Cancel" 留页,"Close App" 调 SystemNavigator.pop() |
| BLE 适配器门控 | iOS CoreBluetooth 启动后前 ~500ms 报 CBManagerStateUnknown;dashboard 不直接调 BLE,只 watch Provider,不受影响 |
无此问题 |
dashboard_page.dart 1969 行:含 18+ 内部 widget 类,已在 仓库结构 标为重构候选——建议拆出 _DeviceCard / _DeviceGroupFrame / _ConnectionBanner 到独立文件。_DeviceCard.onLongPress 与 _DeviceGroupFrame 的外层 onLongPress 共用同一回调,长按某根探针实际删掉整台中继盒。功能正确(dashboard 不支持单探针删除),但视觉/语义不一致。_slowReconnectThreshold = 30 跨文件耦合:_DeviceGroupFrame 内部常量是 BleDeviceService._reconnectFastAttempts + _reconnectMediumAttempts(5 + 25,per spec 2026-04-28)的镜像;仅后台 ≥4 小时才会进入 slow 段(per spec 2026-04-29 follow-up),前台与后台 <4 小时始终 15 秒、永不进 slow。无自动同步机制——改 BLE 重连节奏时容易漏改这里(注释里已标 "if those constants move, update this")。