设置中心:温度单位/语言/服务器区域偏好、连接设备的固件版本展示、文档与支持入口、WiFi 配置入口(仅 CM4)、开发者工具(Mock 切换、MQTT 测试)、品牌 footer。
lib/features/thermometer/presentation/pages/settings_page.dart(782 行)temperatureUnitProvider、appLanguageProvider、regionServiceProvider、connectionManagerProvider、connectionModeProvider、connectedBoostersProvider、useMockServiceProvidergetWifiSsid / clearWifiConfig(lib/core/services/...)、DeviceConnectionManager.switchToBle(manager.provisionWifi(mock no-op)→ bleDeviceServiceProvider.sendCommandAsyncTo(deviceId, CM4Command.setWifiSsid/setWifiPassword)cm4_protocol import + SSID/密码 controllers 一并去掉,配网只走 add-device wizard 与下方 §WiFi 的 Set up / Change WiFi → /wifi-select)SettingsContent(可复用片段,正在被 SettingsPage Scaffold 包裹)/settings(SettingsPage.routeName)lib/app.dart 静态 routes mapAppBar 标题 "Settings"。下方滚动若干分组:Account(c9fcea5 / TAPD #1003149 起,已登录时单一 chevron tile,title settingsAccount,trailing 显 accountUsernameFor(user) ellipsis + chevron;tap 跳 AccountPage (/account) 做 Username / Email Address / Change Password / Delete Account 四行 per-account 操作。⚠️ c5b09e4 / TAPD #1003149 follow-up 起未登录态同样在 settingsAccount section header 下渲染一个 Sign In chevron tile(Icons.login + l.authSignInTitle,tap Navigator.pushNamed(SignInLandingPage.routeName) 进 account-only sign-in 漏斗、deviceId=null),漏斗成功 finishAuthFunnel(context, null) 只 popUntil auth 路由返回本页,本节立刻翻为已登录 Account chevron。先前未登录用户只能从 WiFi add-device wizard 进 sign-in、本页 settingsAccount 区段整段隐藏,是 #1003149 Liang flag 的入口可达性缺口)、Connection / Provision(曾在 Account 后渲染 BLE 状态行 + 内嵌 WiFi 配网卡片含 SSID/密码 + "Provision device" 按钮) 2650709 / per Liang 2026-06-12 起整段移除——BLE 状态行与 dashboard 的 per-device signal glyph 重复(TAPD #1003222),内嵌配网卡又与 add-device wizard 和下方 §WiFi 的 Set Up / Change WiFi 重复(→ /wifi-select);Account 后直接接 Preferences(温度单位 toggle、Notifications chevron 行(28c930e / TAPD #1003096 起 Switch 已撤换为跳转 OS 系统通知设置的 chevron tile,详见下方 Action #4)、Language 下拉、Server Region 下拉带 "Auto" tag)、Device Info(仅 connected:Booster 固件版本 + 各连接探针固件版本)、About(App version、Operation Guide、Help Center、Support 三个 chevron tile)、WiFi(仅 CM4:Network 状态 + Cloud 状态 + Set up/Change WiFi、可选 Forget)、Developer(Mock BLE switch + Test MQTT chevron tile)、Branding footer。
Navigator.pop(context) → 回到 DashboardSettingsContent._ssidController / _passwordController / 整张 Wi-Fi Provisioning 卡片 / _SectionHeader(l.settingsConnection) BLE 状态行连同 cm4_protocol import 一并删掉。配网入口只剩两条:(a) 扫描配对 → 连接模式 → WiFi 配置 的 add-device wizard;(b) 本页下方 §WiFi 区块 Set up / Change WiFi tile → /wifi-select(详见 Action #8)。9 个 connection-section 的 l10n key(settingsConnection / settingsBleStatus / settingsWifiProvisioning / settingsSsid / settingsPassword / settingsProvisionDevice / settingsProvisionNoBooster / settingsSsidRequired / settingsCredentialsSent)按 commit body 留在 ARB 里 inert、暂未引用。
_TempUnitToggle.onChanged_TempUnitToggle.onChanged(v)
→ DevLogService.logUserAction('UNIT_TOGGLE', details: 'switched to °C/°F')
→ temperatureUnitProvider.notifier.setCelsius() / setFahrenheit()
transport.setUnit(...) → 0x55AB 把中继盒物理屏单位也推过去——但有 firmware-version 门,详见下方 ⚠️ callout。lib/core/theme/app_theme.dart 的 formatTempF:°F 分支用 tempF.truncate(),°C 分支保留 toStringAsFixed(0)。⚠️ 299be98 / TAPD #1003186:°F wire byte 与显示对齐——之前 cooking_page._pushTargetToDevice 用 _targetTempF.round() 编 wire byte,与上面 °F 去尾规则不一致:Lamb Medium 段中点 (134+145)/2 = 139.5°F UI 显「139」、wire 写 140,中继盒物理屏跟 wire 显「140」(同样症状 130→131、159→160、168→169)。Per Liang 2026-04-28 同一规则:wire 改 .truncate() 后中继盒回显与 App 一致;°C 路径不受影响(HEX→°C 查表本身即整数,两侧 round 同结果)。Legacy re-arm 路径读 cachedTargets 整数值自动 inherit 此修复,SESSION_START dev-log 同步。⚠️ legacy 全家族跳过 0x55AB wire 写入(per Liang 2026-05-19 → 2026-05-20 扩展,3e3af70 → 0feadf6):App 现在对所有 legacy 中继盒(CM1 / CM2 全版本 / CM3 / MW3–5 别名)都跳过 0x55AB 单位切换 wire 写入;dev log 记
UNIT_SYNC_SKIPPED。两层原因叠加——(1) CM2 fw byte <0x20(Liang hex 命名的 V19 及更早)有 target-byte-not-converted bug(CM2_0DA6 fw byte0x18/ "V18" 测试:53 °C 切 °F 屏显 53 °F 而非 127 °F,App 之前还会从次条 12B ADOPT 错值;3e3af70 已收口此分支);(2) per Liang 2026-05-20 follow-up 到 QA "切单位中继盒响铃 4/6 次" beep 工单:固件设计上每收一帧 0x55AB 就按探针槽×2 各响一声 ack-beep(CM2 4 声 / CM3 6 声),不是 App 行为,没有比"不发该帧"更安静的方案——0feadf6 把 skip 扩到全 legacy。Skip 后:temperatureUnitProvider仍立即翻,App 端渲染 / 报警阈值 / 通知文案下一帧就用新单位;_DeviceState.cachedTargets全程保持 canonical °F、不受单位切换影响,所以 ALARM_SYNC_CHECK 仍与 booster 12B 报告一致;中继盒物理屏单位保持原状,直到用户主动 SET_TARGET(0x55AF/B0/B2,byte-2 unit flag 被固件作为全局显示单位采纳,详见 状态模型 §Booster.useCelsius 与 ec99c9b post-MUTE rearm 同根 firmware 侧效应)—— 实际上"用户切单位 → 用户启动一次烹饪"通常发生在同一分钟,desync 窗口很短。CM4 路径不跳:ASCIISET_F/SET_C用不同的固件 ack pattern(没有 per-probe ring chain),并且 WiFi-only 操作期在用户主动 SET_TARGET 之前可能依赖屏幕显示单位与 App 一致。⚠️ 084c4ad / TAPD #1003257 起_broadcastUnit广播范围 union 上 cloud-live 设备:先前_broadcastUnit只 iterate_bleService.connectedDeviceIds——WiFi 模式 + 蓝牙关时该集合为空、dev log 记UNIT_ROUTE routing 0 device(s)、CM4 物理屏切单位从此不到(field log 实证)。TemperatureUnitNotifier现接cloudLiveDeviceIds: Set<String> Function()注入 callback(在temperatureUnitProviderfactory 里ref.read(connectedBoostersProvider.notifier).cloudLiveDeviceIds);_broadcastUnit把它与connectedDeviceIdsunion 后再喂给setUnit(),让每台设备都经各自 transport 的 BLE-or-cloud fallback 路由(CM4 cloud 经/CM4/<id>/sett,legacy 跳setUnit内部仍按上面规则),WiFi 模式 parity 与 BLE 模式一致。同时撤回了 411afed 引入的 5 s ALARM_SYNC_SUPPRESSED 窗口 +_rePushTargetsAfterUnitSwitch250 ms-paced 重推扫描(不发 wire 命令就没有 reinforcement 需求)。Firmware byte convention:Liang hex 命名("V20" = byte0x20),App parser + dev log 渲染同一字节按十进制(byte0x28→ "V40"),不影响 wire 解析。
⚠️ 28c930e / TAPD #1003096:从 in-app Switch 改为跳 OS 系统通知设置。Notifications 通知开关交还给 OS——in-app 端不再保留 on/off 状态,单纯做"打开系统通知设置"的入口。
GestureDetector.onTap → _openNotificationSettings();Android 走 BackgroundPermissionsService.openNotificationSettings() → method channel → BackgroundPermissionsHelper.openNotificationSettings() → Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + EXTRA_APP_PACKAGE(API 26+ 标准 action,MIUI/Samsung 等 OEM 都识别,无需 OEM-specific 组件;解析失败 fallback 到 ACTION_APPLICATION_DETAILS_SETTINGS 应用详情页,通知一栏一 tap 进去),native 端 dev log 记 OPEN_NOTIFICATION_SETTINGS;iOS 走 permission_handler.openAppSettings()(App 设置页第一行就是 Notifications)notificationsEnabledProvider 现状:NotificationsEnabledNotifier 移除了 set(bool) 写入路径;init() 强制 state = true 并主动 prefs.remove(_notificationsPrefKey) 删掉历史持久化键,避免曾经 in-app 关过的用户被永久静音;alarmStateProvider 评估时仍 read 一次此 provider(告警与通知 §Settings → Notifications toggle gate 描述的 11 类非 safety alarm continue 早返 gate 代码保留),但 provider 恒为 true,等于无 gate——保留 wiring 只是为未来若要回归 in-app mute 不用重新接线。Safety alarm internalOverTemp / ambientOverTemp 由 OS / fullScreenIntent / Critical Alerts 路径独立直通,不受系统通知开关 gate 影响_SelectionField.onTap → _showSelectionSheet<String>(...)(99cf247 / TAPD #1003100:之前是 DropdownButton 弹角落 cramped overlay + 小 tap target,改为 slide-up showModalBottomSheet 列出 6 选项、命中当前 locale 行带 ✓)_SelectionField.onTap → _showSelectionSheet<String>(
context: ..., title: l.settingsLanguage, current: language,
options: [(value: 'English'/'Français'/'Deutsch'/'Italiano'/'Español'/'中文', label: 同名)])
→ 选中 → Navigator.pop(opt.value)
→ 回到 page: ref.read(appLanguageProvider.notifier).setLanguage(picked)
→ MaterialApp 的 locale 切换 → 全 App 重建为新语言
_SelectionField.onTap → _showSelectionSheet<ServerRegion>(...)(99cf247 / TAPD #1003100:与 Language 一并从 DropdownButton 迁到 slide-up sheet)regionService.setRegion(picked) → 持久化用户覆盖;下次 MQTT 连接走新 region 的 broker_SelectionField.onTap → _showSelectionSheet<ServerRegion>(
context: ..., title: l.settingsServerRegion, current: currentRegion,
options: [(value: ServerRegion.china/us/eu, label: l.settingsRegionChina/US/EU)])
→ 选中 → Navigator.pop(opt.value)
→ 回到 page: regionSvc.setRegion(picked) → SharedPreferences 写入 mqtt_server_region
→ setState(()) (触发显示更新)
Navigator.pushNamed(context, DeviceGuidePage.routeName)Navigator.pushNamed(context, HelpCenterPage.routeName)Navigator.pushNamed(context, SupportPage.routeName)GestureDetector.onTap → Navigator.pushNamed(context, '/wifi-select', arguments: booster.deviceId)connectedBoostersProvider.firstOrNull 的 deviceId;代码未额外筛选“当前第一个已连接的 CM4”。_wifiConfigured 切换GestureDetector.onTapGestureDetector.onTap
→ clearWifiConfig(booster.deviceId) → SharedPreferences 删除 WiFi 配置
→ DeviceConnectionManager.switchToBle() → MQTT 断开,回 BLE 模式
→ _loadWifiConfig() → 刷新本页 _wifiConfigured 状态
→ ScaffoldMessenger.showSnackBar('settingsWifiForgotten')
/wifi-setupSwitch.onChanged: (_) => ref.read(useMockServiceProvider.notifier).toggle()activeDeviceServiceProvider 切换实现;session-only(不持久化),App 重启回真实GestureDetector.onTap → Navigator.pushNamed(context, '/mqtt-test') → 打开 MQTT 测试watch 大量 Provider:温度单位、语言、连接管理器(含 snapshot)、区域服务、连接模式、已连设备、Mock 开关。设置页是个「映射 Provider 到 UI」的偏好编辑器,几乎所有交互都是直接读写 Provider 完成。
_WiFiSection 自己持有局部 state(_loading / _wifiConfigured / _ssid),通过 _loadWifiConfig 在 initState + Forget 后异步重新加载。
无显著差异 —— network_info_plus / permission_handler 已在底层服务层屏蔽。
_notificationsEnabled 状态,没有实际启用/关闭 NotificationService 🔴 需要确认(是否是 UI placeholder)notificationsEnabledProvider(SharedPreferences notifications_enabled),off 时 告警与通知 §本地通知 的 background notification dispatch 抑制非 safety 报警 11 类。28c930e / 同 TAPD #1003096 二次迭代:in-app Switch 整体撤换为跳 OS 系统通知设置的 chevron tile(详见 §Action #4),NotificationsEnabledNotifier.set 删除、init() 强制 state = true 并清掉持久化键,让 OS 单独 gate 通知可见性;alarm 评估侧的 notificationsEnabled gate 代码保留(恒 true 路径)以便未来若要回归 in-app mute。package_info_plus 取以避免发版漂移。