主动扫描附近的 CM/MW/CG 系列中继盒,用户点击设备完成 BLE 连接;CM4 机型连接成功后跳转到「连接模式」让用户选 BLE-only 或 WiFi+云端。
lib/features/thermometer/presentation/pages/scan_page.dart(约 880 行,含调试子页 _DebugBleScanPage)deviceScanProvider(扫描结果流)、boosterProvider(family provider,调用形式为 boosterProvider(deviceId))(连接控制)、bleDeviceServiceProvider(已连设备查询)BleService.releaseScan / acquireScan(扫描优先级仲裁)、FlutterBluePlus(底层扫描)_DeviceRow、_ConcentricRadarPainter、_DebugBleScanPage(均在同文件内)/scan(ScanPage.routeName)lib/app.dart 的 MaterialApp.routes map顶部一行图标按钮(左侧返回、右侧刷新);下方居中是状态文案("Initializing..." 或 "Searching...")和一个动画脉动同心圆雷达 + 蓝牙图标。中央是滚动设备列表(带边框容器),未连接行显示蓝牙图标 + deviceId,右侧是通用信号图标;已连接行蓝牙图标变绿并显示 scanConnected 文案;正在连接行左侧显示转圈进度、右侧显示 scanConnecting 文案。空列表态由 _showEmptyHint 这个 15 秒计时器统一门控(不看 scanResult.hasValue——底层 scan stream 每 2 秒 reemit 空列表心跳,hasValue 会在第一轮真扫描完成前就翻 true,导致 4 秒就误显示"找不到"):开扫前(loading 状态)只渲染雷达 + "Initializing...",收到首笔扫描列表后翻转为 "Searching...";15 秒仍无设备时显示蓝牙图标 + 本地化 scanNoDevices 标题 + scanNoDevicesHint 提示 + scanAgain 按钮(ElevatedButton.icon,调 _triggerRescan);任何时刻发现设备立即切到设备列表。底部有 "No device found?" 蓝色链接(→ 设备指南)和橙色 "🔧 DEBUG: All BLE" 链接(→ 内部调试子页 _DebugBleScanPage,列出所有 BLE 广播)。
IconButton.onPressed(顶部 bar,第一个按钮)IconButton.onPressed
→ BleService.releaseScan(ScanPriority.userScan)
→ Navigator.pop(context)
IconButton.onPressedElevatedButton.onPressed两者都调 _triggerRescan。
_triggerRescan
→ ref.invalidate(deviceScanProvider)
→ autoDispose 销毁旧 stream
→ build 触发 ref.watch 重新订阅 → 新 scan stream 启动
FlutterBluePlus.startScan 重新发起;empty hint 计时器重置_DeviceRow.onTap(InkWell.onTap,_isConnecting 时禁用)InkWell.onTap
→ _connectToDevice(device)
→ 若 bleService.connectedDeviceIds 已含该 deviceId → Navigator.pop(直接返回)
→ setState(_isConnecting = true)
→ BleService.releaseScan(ScanPriority.userScan)
→ FlutterBluePlus.stopScan()
→ ref.read(boosterProvider(deviceId).notifier).connectAndReport()
→ 成功且是 CM4_*:Navigator.pushReplacementNamed('/connection-mode', arguments: deviceId)
→ 成功且不是 CM4:`DeviceGuidePage.showAfterDeviceAdd(context)`(TAPD #1003087 / 88e3ad7 起统一收尾——`pushNamedAndRemoveUntil('/dashboard')` 清栈 + `pushNamed('/device-guide')` 在顶部叠 [设备指南](/zh/03-客户端实现/04-界面解析/15-设备指南),guide pop 后落回 Dashboard)
→ 失败:根据 ConnectResult 类型显示对应 SnackBar 文案(unreachable / timeout / generic gattError)
BoosterNotifier(deviceId) 触发 BLE 连接 + AE05 订阅 + 心跳/锁刷新启动KnownDevicesService 持久化该设备(CM4 会再触发 connectionModeProvider 流)GestureDetector.onTap(底部蓝色文案)/device-guide(设备指南)RouteAware.didPopNext 恢复扫描GestureDetector.onTap(底部橙色文案)_DebugBleScanPage(不在 routes map,匿名 MaterialPageRoute)—— 列出所有 BLE 广播(包括非 CM/MW/CG 设备),用于现场排查GestureDetector.onTap
→ Navigator.push(MaterialPageRoute(builder: (_) => _DebugBleScanPage()))
_DebugBleScanPage 自启动一次原始 BLE 扫描(不带 service UUID 过滤);返回时停止_stopScan,停止时显示刷新图标调 _startScan),仅供调试build 通过 ref.watch(deviceScanProvider) 订阅扫描结果(AsyncValue<List<Booster>>);扫描每发现一个新设备就 emit 一次新值,列表自动重建。bleDeviceServiceProvider 用 ref.read(仅读已连 ID 集合做"已连"标记,不订阅)。
RouteAware 暂停模式(_paused 标志):扫描页订阅 rootRouteObserver;当被子路由覆盖时 didPushNext 设 _paused = true,build 跳过 ref.watch → autoDispose 关闭扫描 stream,省电。didPopNext 把 _paused 翻回 false 触发 rebuild → 重新订阅 → 新一轮扫描启动。这是 Scan 页和 Dashboard 等其他页交互最关键的内部机制。
_showEmptyHint 与 _emptyHintTimer 由 initState 启动一个 15 秒计时器(scan_page.dart:91):到时仍无设备就显示 hint;若 hint 已经显示且随后发现设备,则会清掉 hint 并取消计时器。_triggerRescan(:100)使用同样的 15 秒。
无显著差异。扫描的字面行为依赖 FlutterBluePlus,iOS/Android 两侧权限和后台行为差异由 BleService 屏蔽(详见 平台集成)。
_DebugBleScanPage 不应进生产构建:橙色 "DEBUG" 文案永久可见。建议根据 kReleaseMode 或 DevLogService.enabled gate。_emptyHintTimer 初始触发的延迟在 initState 里硬编码 15 秒(scan_page.dart:91);改成命名常量更清晰。