CulinaTech App 是一款 Flutter/Dart 编写的智能温度计应用,通过 BLE 与「中继盒」(booster)通讯,中继盒再无线读取探针温度。主打场景:烹饪过程中实时监测温度、达到目标温度报警、多设备并行监控。
CG_ 前缀)由 deviceTypeLabel 识别但当前文档未深入ProbeAddress / ProbeNumber.colorName,V5 spec 2026-05-14)boosterProvider.family 以 deviceId 为键,每台独立| 领域 | 组件 | 版本 |
|---|---|---|
| 语言/框架 | Flutter + Dart | SDK >=3.3.0 <4.0.0 |
| 状态管理 | Riverpod (flutter_riverpod) |
^2.5.1(以 family 键区分多设备) |
| BLE | flutter_blue_plus | ^1.35.3 |
| 云端 | MQTT (mqtt_client) |
^10.5.1(仅 CM4 WiFi 机型使用) |
| UI | Material Design(深色主题)+ google_fonts ^6.1.0 + flutter_svg ^2.2.4 | — |
| 图表 | fl_chart | ^0.68.0 |
| 持久化 | shared_preferences ^2.2.2 + path_provider ^2.1.2 |
— |
| 报警 | flutter_ringtone_player ^4.0.0+3 + flutter_local_notifications ^18.0.0 |
— |
| 权限 | permission_handler |
^11.3.0 |
| 音量读取 | flutter_volume_controller ^1.3.3(Android 读 notification + ring;启动检查 <50% 弹提醒) |
— |
| iOS 实时活动 | live_activities ^2.4.3(iOS 16.1+;Android 静默 no-op) |
— |
| 设备/网络信息 | package_info_plus ^8.0.0 + network_info_plus ^5.0.0 + uuid ^4.3.3 |
— |
| 分享/导出 | share_plus ^12.0.0 + url_launcher ^6.2.5 |
— |
| 国际化 | intl + ARB |
6 种语言:en / de / es / fr / it / zh |
ios/Podfile:2);Live Activities 需要 iOS 16.1+minSdk = flutter.minSdkVersion,即当前 Flutter 默认值(API 21 / Android 5.0);MainActivity 重写了 onDestroy 通过 MethodChannel culinatech.app/lifecycle 通知 Dart 层做 BLE + MQTT 清理kIsWeb 分支禁用 BLE/通知,但 Web 不是发布目标lib/
├── main.dart # 入口:启动顺序锁定、权限预请求、Provider 容器、生命周期钩子(625 行)
├── app.dart # MaterialApp、命名路由、rootNavigatorKey、安全告警全屏推送(165 行)
├── core/ # 跨业务共享层
│ ├── models/ # Booster / Probe / CookingSession / ConnectionDisplayState
│ ├── protocol/ # 协议层:cm4_protocol / legacy_protocol / 数据解析 / LUT
│ ├── transport/ # 传输层抽象(2026-05-01 拆分落地):booster_transport + legacy/cm4 impls + booster_family
│ ├── providers/ # device_providers.dart(2096 行,所有 Riverpod 状态集中此处)
│ ├── services/ # 单例服务:BleService / BleDeviceService / MqttService / AlarmService / NotificationService …
│ ├── theme/ # 深色主题、颜色常量
│ └── utils/ # anonymous_id、log_timestamp、meat_localization、navigation_utils
├── features/thermometer/ # 业务层
│ ├── data/ # datasources/(BLE / 云端 / Mock)+ repositories/
│ ├── domain/ # entities/(DeviceType / ProbeReading / ThermometerDevice / ThermometerSnapshot)+ repositories/
│ └── presentation/ # 16 个页面 + 共享控件 + Provider + 控制器
└── l10n/ # ARB 源文件 + 自动生成的本地化代码
16 个屏的
.dart文件中,13 个在app.dart的命名路由表中注册(12 个静态、9 个走onGenerateRoute传参),help_detail_page.dart/cook_log_detail_page.dart等直接 pushMaterialPageRoute。详见仓库结构。
lib/core/transport/booster_transport.dart:抽象接口(connect / setProbeTarget / setUnit / muteAlarm / queryStatus),数据流为 Stream<CM4Message>lib/core/transport/booster_family.dart:enum BoosterFamily { legacy, cm4 } + 独占锁刷新元数据(legacy 45s 0x55B1 / CM4 45s CNT_0——per Liang 2026-06-06 / TAPD #1003176 起两家族同 cadence 同机制;c613148 之前 CM4 是 20s 且文档为"无独占锁",那是错的)+ MTU 决策(仅 CM4 协商 512)lib/core/transport/legacy_booster_transport.dart / cm4_booster_transport.dart:family-specific 实现BleDeviceService._transportFor(deviceId) 按 deviceId.boosterFamily 懒创建并缓存;调用层不感知 family 具体类型详见 04-规划与跟进/02-传输层重构 跟踪页。
独占锁机制对所有家族适用(per Liang 2026-06-06 / TAPD #1003176 更正先前 "CM4 无独占锁" 假设)。CM1/CM2/CM3 每 45 秒发送 0x55B1 刷新独占锁,60 秒 TTL 到期后其他手机可抢占(Liang 2026-04-20 锁定不变,见 legacy_protocol.dart:25-44)。CM4 也有同样的独占锁——发 ASCII CNT_0 每 45 秒一次(c613148 之前是 20s),同 60 s TTL;仅 wire 字节与 legacy 0x55B1 不同(CM4 不接受二进制锁命令)。WiFi 模式下 BLE 链路也必须持续刷锁——否则中继盒释放锁、约 20–30 s 后主动 shed GATT 链路。详见 CM4 协议 §心跳。
断线宽限期:90 秒(ble_device_service.dart:388 gracePeriod = 90s)。非入仓引发的断开,前 90 秒 UI 仍显示最后温度,后台静默重连;超时才显示"已断开"。设计动机:legacy 60s lock TTL + 30s 留给一次连接尝试。
入仓触发关机:收到 0x55AA 后 1 秒内若再收到 0x07 断开(_dockShutoffWindow=1s,ble_device_service.dart:589),分类为"入仓关机",跳过宽限期,UI 显示 10 秒 boosterShuttingDown 过渡态(_dockShutoffUiDelay=10s)再切到 boosterOff。首次重连延迟 8 秒(_dockShutoffReconnectDelay)匹配中继盒断电+复位时间。
多设备彼此独立:每台中继盒有独立的 BLE 连接、独立的心跳/锁刷新、独立的重连失败计数器(_deviceAttempts[deviceId])。UI 层 boosterProvider.family 以 deviceId 为键。锁刷新写按 deviceId.hashCode 相位错峰(ble_service.dart:1354),避免多设备同步写堵塞 BLE 命令队列。
温度查表权威 + 内温 HI 阈值 = 101 °C:App 必须从 15B 包的字节 3-4 / 5-6 自己查 tempIntArray / tempExtArray(已抽到 lib/core/protocol/temperature_lookup.dart,199 行),不要信任 12B 包的字节 7-8(中继盒预转换值,已确认不权威,见 v4.2 §Q20)。内温显示阈值经 Liang 2026-04-29 v5 §15 修订收紧到 101 °C:超过即显示"HI"、值钳制为 101 °C;alarm 触发阈值则是 100 °C(AlarmThresholds.internalOverTempF=212),HI 钳位再晚一度。HI 报警走 WarningPage 全屏推送——在 app.dart 的 ref.listen(alarmStateProvider) 里只针对 AlarmType.internalOverTemp / ambientOverTemp 这两类 safety alarm 推全屏,其它类型用顶部 banner (AlarmBannerHost)。
探针断连判断:探针 20 秒 15B 沉默 → 探针卡片置灰 + Probe.isConnected=false(_probeDropoutThreshold=20s,ble_device_service.dart:400,老 10s 阈值因 12-15s 正常 jitter 误报已上调)。60 秒 15B 沉默 → 触发 AlarmType.probeDisconnected 报警弹窗(per Liang 2026-04-28)。任意 15B 重置计时器。报警评估器在 device_providers.dart 里——此为载荷于陈旧烹饪场景下的安全报警,未经 Liang 重新确认前不要改阈值或抑制窗口。
中继盒断连判断:连续 60 秒 12B & 15B & 2B 全部沉默 → DeviceStatus.lostConnection。AND 条件由单一时间戳 _DeviceState.lastBoosterData 隐式实现——任何一种包到达都重置时间戳,60 秒无更新即三种全沉默(ble_device_service.dart:3649-3656,v4 时为 15 秒,Liang 2026-04-30 audit reply 提高到 60 秒)。判定连接是否可建立用相反的判据:扫描期间发现该设备广播任意 15B / 12B / 2B 即可发起连接。
扫描和重连节奏(v4 §6 升级后,per Liang 2026-04-29 follow-up):前台 始终 15 秒主动扫描,不衰减;后台前 4 小时同前台节奏;后台 4 小时后才进入分档退避——第 1-5 次每 15 秒、第 6-30 次每 60 秒、第 31 次以后每 5 分钟。多设备扫描集合:每次扫描构建所有断开-但-保留的 known devices 集合,任意命中即算成功,避免轮询时错过 B 设备。入仓引发的断线也进入重连循环——用户重开中继盒后下次扫描窗口即可命中。每个设备独立的失败计数器(_deviceAttempts),连接成功后清零;用户长按"忘记设备"后该设备的状态全部清除。详见 02-重连与宽限期。
| Tier | 类型(优先级 0=最高) | 触发 |
|---|---|---|
| S(连续大鸣) | ambientOverTemp(0) / internalOverTemp(1) |
外温 ≥527°F / 内温 ≥100°C |
| B(单声+通知) | probeDisconnected(2) |
15B 沉默 60s,链路仍活 |
| C(烹饪流) | targetReached(3) / earlyWarning2(4) / earlyWarning1(5) |
达标 / -5°C / -10°C |
| D(信息) | lowBattery(6) |
探针电量 ≤20% |
| A(单声+通知) | boosterDisconnected(7) / boosterPoweredOff(8) |
中继盒 60s 沉默 / 入仓关机 |
| D | probeDocked(9) |
探针入仓 |
| D | boosterLowBattery(10) |
中继盒电量 ≤20%;≤10% 时每 10 min 重复 |
| D | internalUnderTemp(11) / ambientUnderTemp(12) |
内温 <0°C / 外温 <40°C |
AlarmKey = (deviceId, ProbeNumber?, AlarmType):booster-level 报警的 probe 为 null。详见 告警与通知。
SET_RD、SET_01..04=ABCDEF、USUS=N、CNT_0 等),cloud doc 是权威,Beta 是渠道。