针对一台中继盒上某根探针的实时温度页面:屏幕中央巨大的数字显示当前温度,下方面板提供 START/STOP、目标温度、计时器、温度图、肉种/熟度调整 4-5 个入口;连接丢失时下方面板会置灰禁用;主显并非一律显示最后已知温度,recentlyLost/reconnecting 才显示灰阶最后读数,docked 或真正断连时显示红字提示。
lib/features/thermometer/presentation/pages/cooking_page.dart(1894 行)boosterProvider.family、probeByNumberProvider、cookingSessionsProvider、pendingCookParamsProvider.family、alarmServiceProvider、earlyWarnLevelProvider、temperatureUnitProvider(页面上的连接状态已从全局 connectionStateProvider 废弃,改为由 boosterProvider 的 isBleConnected / deviceStatus 派生,并参考 connectedBoostersProvider.cloudLiveDeviceIds 与 deviceConnectionManagerProvider.cloudActiveDeviceId 判断云端可达性)BleDeviceService.setProbeTarget(transport-split 后的统一入口;legacy 走 LegacyBoosterTransport.setProbeTarget 写 0x55 [color] [unit] [temp] [MAC],CM4 走 Cm4BoosterTransport.setProbeTarget 写 ASCII SET_<color>=ABCDEF(SET_BL/WH/BU/YE,per cloud doc 2026-05-19 / 6e5dfb7),详见 CM4 协议 §探针配置)(deviceId, probeNumber)(路由 arguments)/cooking(CookingPage.routeName)lib/app.dart 的 onGenerateRoute(line 98),需要 (String deviceId, ProbeNumber probeNumber) arguments顶部 _TopBar:返回箭头 + 居中设备名 + 两个 glass badge(探针电量 / 中继盒电量),断连时多一行 _ConnectionLostBanner。低电量 inline banner 已删(99cf247 / TAPD #1003093,Liang 2026-05-28)——原 _LowBatteryBanner(≤20% 红/橙 banner)+ 底部 _maybeWarnLowBattery snackbar(20/10/5/3% 阈值)+ _lastWarnedBatteryThreshold 状态机一并退役,与 告警与通知 改造同行:probe lowBattery 现复用共享的居中卡片 alarm popup(与所有 non-safety 报警一致),at-a-glance 留持久电量百分比 chip(顶部 glass badge)。中央占据大半屏的是巨大温度数字(76pt 粗体白);超下界显示蓝色 "LO",超上界显示橙色 "HI",BLE 链路真断时(displayState ∈ {docked, disconnected},per spec 2026-04-29 review extension:覆盖入仓 0x55AA、入仓后中继盒自动关机、电池耗尽、用户主动关机、BLE 断 + 90 秒宽限期满五种"手机已不再与中继盒通讯"的情况)切换到红字 "Booster box not connected to smartphone";宽限期内(recentlyLost)与用户手动重连中(reconnecting)保留 _ConnectionLostDisplay 显示 "last known" 灰阶温度(per Liang 早期 grace-UX 指令),无探针数据时红字 "Probe not reaching booster / Move the booster closer to the probe"(per spec 2026-04-30:可操作建议替换原描述性 "Probe not connected to booster box")。底部 _BottomPanel 是 6 格胶囊磁贴(分两行):目标温度(display-only,不可点,per TAPD 2026-05-19 / c31ebee——之前点 Target 会弹 QuickTargetSheet,现已撤销,改目标走 Adjust / Meat Presets 的 canonical 入口)、环境温度(Ambient)、计时器(点开 START/STOP)、温度图(点开 push 子页)、Adjust(推到预设页)、Meat(推到肉类预设)。底部面板在断连时整体置灰且不可点(AnimatedOpacity + IgnorePointer)。
_TopBar 的 IconButton.onPressed → Navigator.pop(context)Per TAPD 2026-05-19 / c31ebee:目标温度磁贴改为display-only——onTargetTap 传 null,_GlassTile 不挂手势 handler、无 ripple、无任何 tap 动作。原 _showTargetSheet helper 与 quick_target_sheet.dart import 一并从 cooking_page.dart 删掉;_BottomPanel.onTargetTap 字段类型放宽到 VoidCallback? 让 null 通过类型检查(其它字段签名不变)。改目标的 canonical 路径是下面 Action 5 (Adjust 磁贴) 与 Action 6 (Meat 磁贴)——Liang 想要的是 Target tile 只渲读数,bottom-sheet quick-picker 在测试中让用户误以为 read-out 自身可编辑、属反向 UX。QuickTargetSheet widget 自身仍存在,当前用于 meat_presets_page.dart 的自定义槽位编辑流程 _openCustomPicker,不是 protein preset tile 点击路径。
_BottomPanel.onTimerTap 或 _StartCookingTile.onTap → _toggleSessiononTap → _toggleSession
→ notifier.activeSession(deviceId, pn) 取活跃会话
→ 有活跃:DevLogService.logUserAction('SESSION_STOP') → _finishSession(cancelled)
→ notifier.endSession(deviceId, pn, cancelled)
→ cookLogProvider.addSession(finalized.copyWith(isActive: false, endTime, status))
→ 无活跃:DevLogService.logUserAction('SESSION_START') → notifier.startSession(...)
→ _clearPendingParams() (session 内已存)
→ _pushTargetToDevice()
CookingSession(isActive=true) 加入 cookingSessionsProvider;pendingCookParams 清空;目标温度推到设备cancelled,写入 cookLog(FIFO cap 50 见 12-待解决问题)_BottomPanel.onGraphTap → _openGraph_TemperatureGraphPage(同一文件内的私有 widget,不是路由),显示历史温度曲线 + 目标线GestureDetector.onTap → navigateInstant(context, _TemperatureGraphPage(tempHistory, targetTempF, deviceId, probeNumber))
IconButton.onPressed → Navigator.pop),其他都是被动渲染(fl_chart)probeByNumberProvider 让新温度点实时加入l.graphTitle、图例 l.graphLegendProbe / l.graphLegendTarget(temp)、空图提示 l.graphWaitingForTelemetry(无点)/ l.graphCollectingData(仅 1 点)。_TempGraphPainter 在 paint 时拿不到 BuildContext,所以两条 hint 由 _TemperatureGraphPage.build 在外层取本地化字符串后通过构造器 waitingHint / collectingHint 参数 plumb 进 painter。 ⚠️ dcf2699(TAPD #1003115)后只剩 4 条:图表改为单样本直接画 dot(points.length == 1 分支),l.graphCollectingData + _TempGraphPainter.collectingHint 参数一并从 6 语 ARB 与 painter 拆除,仅在 points.isEmpty 时画 l.graphWaitingForTelemetry。同次接 _TemperatureGraphPage.build 改为:active session 在时优先用 cookSessionRecorderProvider 写入的 session.history 渲染整条曲线(page-independent recorder,详见 §状态变化时如何更新)、pre-cook fallback 到本页 _tempHistory、再追加 probe.internalTemp 一个 now 点;本页 _recordTemperature 加 5 s heartbeat(值未变 + now - last ≥ 5s 时也补一点),稳态探针不再卡在「Collecting data…」34 分钟(实测 log)。⚠️ 1c26e22 / TAPD #1003126 改 idle fallback 链:无 active session 时不再立即落到 _tempHistory,而是先看新增的 lastCookSessionProvider.family((deviceId, ProbeNumber))——newest-first 扫 cookLogProvider 找该探针最近一条已结束 cook(任何 status,包括 auto-stop-on-target-reached / cancelled / disconnected),有则冻结渲染那条 session.history 整曲线;只有该探针从未烹饪过才回退到本页 _tempHistory。Target / early-warn 用 historical 那条 cook 自己保存的 targetTemp / earlyWarnLevel,legend 与 painter 共用(与显示的曲线对齐),冻结历史时不追加 live now 点(避免拖一条尾巴到当前 idle 温让人误以为 cook 还在跑)。Cook log 跨 auto-finalize / 退页 / App 重启都持久化,所以这条 frozen 曲线一直在屏上直到用户按 START——按 Start 时 active 翻非空、新 cook 的 session.history 立刻接管(初始为空),同时 _toggleSession 启动分支主动 _tempHistory.clear() + _lastRecordedTemp = -1,防止旧 preview 点在中央 recorder 写第一个样本之前的短窗里 flash 出来。时间轴 tick 标签(30s / 2m05s)SI 符号未本地化,Liang 没标,留作 follow-up(要做时按同样的构造器 pattern 走)。_BottomPanel.onAdjustTap → _navigateToAdjustnavigateInstant<Map<String, dynamic>>(PresetPage(initialTargetTempF, initialMeat))
→ 返回结果 Map:{ targetTempF, meatType }
→ setState 更新 _targetTempF / _meatType
→ _savePendingParams + _syncTargetToSession + alarmServiceProvider.reset
_BottomPanel.onMeatTap → _navigateToMeatPresetscooking_page._navigateToMeatPresets 把 widget.deviceId + widget.probeNumber 透到 MeatPresetsPage / PresetPage,chip 在 grid 内 tap 即时 commit;回 cooking 页时调 _syncEarlyWarnFromGlobal 从 earlyWarnLevelProvider 拉回本页 _earlyWarnLevel、再为命名路由 caller 补写 per-probe stores,避免后续 _savePendingParams 用 stale 快照覆盖已 commit 值。_navigateToMeatPresets → await navigateInstant(
MeatPresetsPage(family, deviceId, probeNumber))
→ _syncEarlyWarnFromGlobal() // 兜底回拉 chip tap 已 commit 的 level
→ pick: {meatType, targetTempF, isCustom} // earlyWarnLevel key 已在 8513d11 移除
→ await navigateInstant(
PresetPage(initialMeat, initialTargetTempF, family, deviceId, probeNumber))
→ _syncEarlyWarnFromGlobal() // 嵌套 Presets&Goals 内 chip 改值也要兜底
→ result: {targetTempF, meatType?, isCustom?}
→ setState 更新 _targetTempF / _meatType / _isCustomPreset
→ _savePendingParams + _syncTargetToSession + alarmServiceProvider.reset
earlyWarnLevelProvider 本地 seed 状态变更 + active session 的 earlyWarnLevel 落盘(per Liang TAPD #1003091 / 33b4e62 — alarm evaluator 自此读 per-session 值而非全局 provider,避免并发 (device, probe) 会话之间阈值串扰;下方 2026-05-09 spec note 末段"同步到 earlyWarnLevelProvider 让 evaluator 立即可用"在 33b4e62 后仅作 UI seed 同步,evaluator 不再消费)。⚠️ 8513d11 / TAPD #1003234 起 chip 落盘改在 grid 内 _setEarlyWarnLevel 完成(每次 tap 写 global provider + 有 context 时同时写 per-probe pending + 当前 session),cooking 页本侧只通过 _syncEarlyWarnFromGlobal 把本页 _earlyWarnLevel 与命名路由 caller 缺的 per-probe stores 拉齐——保证后续 _savePendingParams 不会用 stale 快照覆盖。Per Liang 2026-05-09(TAPD):earlyWarnLevel 必须持久化到 CookingSession 自身。之前 5°/10° 早期警告偏移有三处内存副本(page-local _earlyWarnLevel、global earlyWarnLevelProvider、pendingCookParamsProvider)但都不跨进程重启,会话对象本身又没带这个字段,所以中继盒断连 ~10 分钟后 Android 后台 OOM kill 掉进程、用户回到烹饪页时 earlyWarnLevelProvider 重置为 0、_restoreCookState 走 active-session 分支早返不恢复,alarm evaluator 读到 0 后剩余烹饪时间永久不再触发 earlyWarning1/2,肉类预设页 toggle 也显示为 off。修复:CookingSession 增 earlyWarnLevel 字段(fromJson 缺键 fallback 0 兼容旧持久化会话),_toggleSession 启动会话时把当前 _earlyWarnLevel 写入 session,肉类预设页返回新 level 时由 _navigateToMeatPresets 调 cookingSessionsProvider.notifier.updateEarlyWarnLevel(deviceId, probeNumber, level) 落盘,_restoreCookState active-session 分支早返之前先把 session 上的 level 写回 _earlyWarnLevel 并同步到 earlyWarnLevelProvider 让 evaluator 立即可用。
_ConnectionLostBanner.onBackToScan → Navigator.pop(context)_buildContent 通过 ref.watch 订阅多个 Provider:temperatureUnitProvider、boosterProvider(deviceId)、cookingSessionsProvider、probeByNumberProvider((deviceId, pn))、probeSensorFlagsProvider((deviceId, pn))(连接状态已改为从 boosterProvider 的 isBleConnected / deviceStatus 派生,不再 watch 全局 connectionStateProvider) —— 任一变化触发整页重建。
ref.listen 在 :362-371 监听 pendingCookParamsProvider.targetTempF:当中继盒侧推送的目标温度(例:用户按了中继盒物理按钮、或 12B 的 preset alarm 与 App 不一致触发的 reconciliation——TAPD #5;reconciler 通过 BleDeviceService.onProbeTargetReconciled 回调写入此 provider)变化超过 0.5°F 时,_targetTempF 同步更新。0.5°F tolerance 防止 echo App 自己刚写的值。
每次新温度数据通过 _recordTemperature 只入栈本地 _tempHistory(cap 300 点,仅服务本页的实时迷你图表)。会话 history 不再由本页写入——b58946 / TAPD #1003099 起搬到独立的 page-independent cookSessionRecorderProvider(device_providers.dart 内、app.dart watch 保活),从 connectedBoostersProvider 监听遥测、对每个 active session 调 CookingSessionsNotifier.recordSample(...) 落盘,hybrid throttle(≤1 sample / 5 s、≥0.5 °F 或 30 s heartbeat、cap 500 点、超出按 2× decimation 保全 span 而非掐头),同时把 ambient temp 真实写入(之前 cooking-page-coupled addReading hardcoded 0)。这样多探针场景下用户没打开过的探针(CM2 White 等)也能录到完整曲线、烹饪日志详情页画线不缺段。
_timerTick 是一个 ValueNotifier<int>,在 initState 中由 Timer.periodic 每秒递增其 value,驱动计时器通过 ValueListenableBuilder 局部重建显示,而不触发整页 setState。
无显著差异。HapticFeedback.heavyImpact() 在低电量警告时双端都触发。
cooking_page.dart 1894 行:建议拆 _BottomPanel / _TopBar / _TemperatureGraphPage / _GlassTile 等到独立 widget 文件,与 仓库结构 的重构候选一致。c072acf) 接通调用点(_pushTargetToDevice CM4 分支调 Cm4BoosterTransport.setProbeTarget → CM4Command.configureProbe);a3c9cf7 + Beta 2026-05-11 spec 收口编码——CD 槽是大写十六进制 00–FF(鸡 165°F → A5),守卫放宽到 > 0xFF;6e5dfb7 反向恢复 SET_BL/WH/BU/YE 色名前缀(cloud doc 2026-05-19 权威 + Liang 2026-05-16 HW 实测),详见 CM4 协议 §Q1 / §SET 前缀历史。_tempHistory 与 session history 双轨:本地数组(cap 300)+ session.history 都在记录,可能产生不一致 🔴 需要确认(语义是否有意——本地图表 vs 持久化历史)cookSessionRecorderProvider 写入(page-independent,详见 状态变化时如何更新),本地 _tempHistory(cap 300)仅服务本页的实时迷你图表、与持久化历史故意分离:本地图表追求 1 Hz 全粒度可视、session history 走 hybrid throttle 节省存储(≤1 sample / 5 s + ≥0.5 °F / 30 s heartbeat + cap 500 / 2× decimation)。两者数据点密度本就不同,不构成「不一致」bug。:263-266 硬编码英文 'Probe battery at 3% — replace now' 等);其他文案都通过 l.cookingXXX。_maybeWarnLowBattery + _LowBatteryBanner + _lastWarnedBatteryThreshold 状态机,与冗余的 in-page banner / bottom snackbar 一并退役;probe lowBattery 改走共享居中卡片 alarm popup(详见 告警与通知 §报警类型与优先级 表 lowBattery 行),at-a-glance 留持久电量 chip。i18n 问题随源头消失、不再需要单独处理。_pushTargetToDevice byte-identical dedup:本页持 _lastPushedConfig: String? 签名('<pn>|<tempByte>|<useCelsius>|<cm4WireE>|<cm4WireF>'),同一 config 重复 push 时记 SET_TARGET_DEDUP core log 后早返、跳过 transport 层发线、避免 Save→Start 时中继盒物理屏多 flash 一次。Build 里 ref.listen(boosterProvider(deviceId).select(isBleConnected)) 在 false 时清 signature——真断连(已过 90 s grace 由 isBleConnected 落定)后下次 reconnect 强制重 push。App 侧 alarm evaluator 独立于此(看 cachedTargets / session.target,不读 _lastPushedConfig),skip 不抑制任何 alarm。⚠️ 28c930e / TAPD #1003124 追加 cook-end 清 signature 路径:再加一个 ref.listen<bool>(cookingSessionsProvider.select((all) => all.any((s) => s.deviceId == deviceId && s.probeNumber == pn && s.isActive))),当 active 从 true 翻 false(用户取消 / session-complete 状态机收尾——62621923 / TAPD #1003117 Layer B 起 target-reached 不再立即 finalize、改 latch 节点 + arm trailing 录制窗,active 翻 false 由 4-条件状态机推进至 session-complete 时才发生,详见 状态模型 §烹饪会话)时把 _lastPushedConfig = null。原因:完成的 cook 触发中继盒报警后固件自身 disarm,下次同屏 Start 即使 config byte-identical 也必须重发才能 re-arm 中继盒;之前 dedup 直接 skip,仅 App 侧响、中继盒静默——TAPD #1003124 报告的"同探针二次烹饪中继盒不响"症状。Save → 首次 Start 路径不受影响(中间没有 session ENDS,dedup 仍 suppress 冗余重 flash)。