物联网温湿度监控APP实战:MQTT、离线缓存与阈值告警
简介面向Android初学者与物联网应用开发者这份温湿度监控手机APP开发代码围绕传感器数据采集、无线通信与移动端展示展开可用于理解环境监测类应用的完整实现路径。压缩包共143个文件、约3.98MB以93张PNG界面与资源图、17个class编译文件、11个XML布局与配置、10个JAR依赖库和3个Java源文件为主并附APK安装包、dex及工程属性文件便于对照源码、资源与可运行产物。已有1619人学习适合作为课程设计、毕业设计或IoT入门项目的参考素材。读者可从中梳理Android端界面搭建、网络请求与线程处理、温湿度数据展示以及LED开关指令下发等环节结合编译产物和APK验证效果快速建立从传感器数据到手机端呈现的整体认知并积累调试与工程组织经验。1. 从一颗探头到一部手机温湿度监控手机APP真正要解决的问题接过一个档案室的改造需求对方开口第一句话并非“给我做个后台”而是“我人不在楼里掏手机就能看见库里现在几度、几成湿”。这句朴素的话其实已经把温湿度监控手机APP的全部难点抖了出来探头读数怎么稳定采上来、数据怎么一路走到手机、手机断网那段时间谁来兜底、超了阈值又由谁把人叫醒。这套方案适合三类人要交毕设或课设的学生、想给自己场地搭一套监控的从业者、手里已经有采集板只差一个手机端的开发者。下面按需求、选型、架构、实现、避坑、验收的顺序走一遍代码和字段尽量做到能直接抄改。2. 需求拆解与技术选型采样频率、通信链路和端侧框架怎么定动手写第一行代码之前我一般会先把需求拆成三张纸一张写清楚要测什么、测多密、留多久一张画数据从探头到手机要经过几跳一张列手机端必须提供的三个界面。这三张纸定下来后面选型基本就是填空题不用纠结。2.1 三个真实场景决定采集端形态温湿度监控听起来是一个需求实际落到场地差别很大。机房、档案库房这类场景温湿度变化慢5 分钟一个点足够但要求长期在线、掉线要补传果蔬保鲜、菌菇房这类场景湿度和通风强相关30 秒一个点才看得出趋势而且采集点分散、走线困难实验室恒温箱这类场景采集点只有一两个但精度要求高往往要 SHT 系列而不是几块钱的 DHT。场景不同采集端形态完全不同第一类适合“市电供电 Wi-Fi 常连”第二类适合“电池 低功耗定时唤醒”第三类适合“USB 直连 上位机轮询”。采样周期不能凭感觉定。一个 30 秒周期的单点设备一天的原始记录是 2880 条一条记录按 40 字节算约 115 KB一年下来 40 MB 出头手机端完全存得住改成 5 秒周期数据量直接翻六倍一年 250 MB本地缓存策略就必须配合滚动清理。所以我通常的做法是在需求阶段就把“周期 × 点数 × 保留天数”算成一张小表写进方案书避免开发到一半才发现本地库撑爆。下面这段是数据点模型字段名故意压短是为了在 MQTT 报文里省流量// 一条温湿度采样点的最小数据模型 class ThSample { final String deviceId; // 设备唯一编号建议用芯片ID而非随机UUID final int ts; // 毫秒时间戳统一由服务端校准后下发 final double temp; // 温度单位 ℃保留两位小数 final double humi; // 相对湿度单位 %RH final int quality; // 质量位0正常 1传感器异常 2补传数据 const ThSample({ required this.deviceId, required this.ts, required this.temp, required this.humi, this.quality 0, }); factory ThSample.fromJson(MapString, dynamic j) ThSample( deviceId: j[dev] as String, ts: (j[ts] as num).toInt(), temp: (j[t] as num).toDouble(), humi: (j[h] as num).toDouble(), quality: (j[q] as int?) ?? 0, ); MapString, dynamic toJson() {dev: deviceId, ts: ts, t: temp, h: humi, q: quality}; }quality这一位看起来多余但它是后面所有脏数据处理的抓手。传感器读失败时不要抛异常、不要塞 0而是标一个质量位照常上报服务端和 APP 都按质量位过滤。这个约定一旦定死曲线突然掉到谷底这种玄学问题会少掉一大半。ts用毫秒整数而不是格式化字符串是因为排序、去重、区间查询都靠它字符串时间在这三件事上都要额外转换。2.2 通信链路的三条路线与取舍设备到手机的数据通路常见三种走法没有绝对优劣只有场景匹配。第一种是设备直接对手机走 BLE。优点是不依赖路由器、不依赖外网演示和答辩场景最稳缺点是一次只能连一台手机手机离开范围就断历史数据要等重新连上才补。适合单点、近距离、以现场查看为主的场景。第二种是设备走局域网手机和采集端在同一个路由器下用 MQTT 或 HTTP 直连。优点是延迟低、不花流量费、数据不出内网缺点是手机一旦切到移动网络就看不见了跨网段还要处理设备发现。适合固定场地、有稳定 Wi-Fi 的机房和库房。第三种是设备上报到一台中转服务手机也连这台服务。优点是随时随地能看、多台手机共享同一份数据、告警可以服务端推送缺点是多了一层要维护服务端一挂全挂。真正要交付给非技术用户长期使用的方案我基本都选这条代价是必须把断线重连和离线补传做扎实。选型时还有个容易忽略的细节MQTT 主题怎么设计。我一般用三段式上行dev/{deviceId}/up下行dev/{deviceId}/cmd服务端广播app/{userId}/notify。设备只订阅自己的 cmd 主题手机只订阅自己 userId 的 notify 加上关注的 dev 上行主题。这样设备端的订阅表固定权限也好收不会出现一台设备收到别人指令的情况。QoS 上数据上报用 QoS1允许重复但不容许丢重复正好用(dev_id, ts)唯一索引做幂等指令下发用 QoS1 加请求 id 回执避免开关动作丢了没人知道。2.3 手机端技术栈与数据模型端侧框架的选择主要看交付时间和目标平台。只做安卓、要求后台常驻和通知稳定原生 Kotlin 最省心要同时交安卓和 iOS 两套Flutter 或 uni-app 更划算。这个项目里我倾向 Flutter原因是图表库成熟、串口和蓝牙插件齐全、一份代码两套壳答辩时演示机不用挑。如果课程要求必须体现 Android 四大组件那就老老实实上 Kotlin别为了炫技选型后期联调会很痛苦。不管选哪个框架APP 内部的数据分层建议固定成三层网络层只负责连接和收发原始报文仓储层负责把报文转成模型并落库界面层只订阅仓储层暴露的流。很多同学把 MQTT 客户端直接塞进页面里页面一销毁连接就断切回来数据全丢这类翻车在答辩现场特别常见。分层的意义就是把连接的生命周期从页面手里收回来交给一个全局单例管理。依赖清单通常就这几样MQTT 客户端如mqtt_client、本地数据库sqflite、图表fl_chart、本地通知flutter_local_notifications、状态管理Provider 或 Riverpod够用就行别上全家桶。第三方库版本不要盲目追新锁定在项目初始化时能跑通的版本写进pubspec.yaml并提交锁文件这是最便宜的后悔药。还有一项常被跳过的工作本地数据保留策略。我的默认配置是原始点保留 7 天、每小时聚合值保留 90 天、每天聚合值保留 2 年。聚合表的存在让“看一年趋势”这种需求不用扫几十万行原始数据查询基本在百毫秒级返回。这套策略在需求文档里写清楚验收时对方问“存一年会不会卡”你能直接翻到那一页。3. 总体架构设计四层结构与一次数据的完整旅程架构图不用画得多漂亮关键是把“一次采样从产生到显示”这条路走通每一跳的职责边界写死出问题时能立刻定位是哪一层的事。我习惯把这套系统切成采集层、传输层、服务层、应用层四段下面逐段说清楚它们各自管什么、不管什么。3.1 四层结构与数据流向采集层是探头加主控负责定时读取、做一次量程校验、打上质量位、带上时间戳发出去。它不管存储也不管告警判断读失败就标质量位继续发下一条绝不阻塞。传输层是链路本身Wi-Fi 加 MQTT 是主流组合。这一层只保证“尽力送达”重连、心跳、遗嘱消息在这里配置。设备端设 LWT 遗嘱主题dev/{deviceId}/status意外掉线时 broker 自动发 offline手机端据此把设备标灰比用户盯着不动的曲线猜要体面得多。服务层是可选的但一旦涉及多用户、历史回溯、远程推送就绕不开还会顺带把设备时间校准这件事接过去。服务端收到上报后做三件事按(dev_id, ts)幂等入库、按阈值规则判断是否需要推送、把校准后的时间戳回给设备。应用层是手机 APP负责展示、查询、告警确认和本地缓存。它不信任任何一条数据的时间展示前先用服务端对齐过的时间戳排序。数据流向是单向的探头 → 主控 → broker → 服务端 → APP。告警确认是唯一反向动作走dev/{deviceId}/cmd或者服务端接口别让 APP 直接连设备那会把权限模型搞乱。3.2 协议字段与数据表设计字段设计的原则是“上行短、下行清”。上行报文控制在 128 字节内能省一字节省一字节下行指令则可以啰嗦带上请求 id、超时时间、期望状态方便做幂等和重试。下面这张是本地缓存表APP 端和服务端都可以照这个结构落-- 本地缓存表只存最近 7 天原始点滚动删除 CREATE TABLE th_sample ( id INTEGER PRIMARY KEY AUTOINCREMENT, dev_id TEXT NOT NULL, ts INTEGER NOT NULL, -- 服务端对齐后的毫秒时间戳 temp REAL NOT NULL, humi REAL NOT NULL, quality INTEGER NOT NULL DEFAULT 0, synced INTEGER NOT NULL DEFAULT 0 -- 0未上报 1已上报 ); CREATE UNIQUE INDEX uk_dev_ts ON th_sample(dev_id, ts); -- 幂等去重的关键 CREATE INDEX idx_ts ON th_sample(ts); -- 会话表记录每次连接用于判断设备在线状态 CREATE TABLE dev_session ( dev_id TEXT PRIMARY KEY, last_seen INTEGER NOT NULL, -- 最后一次收到报文的时间 rssi INTEGER, -- 信号强度弱信号是掉线高发原因 fw_ver TEXT -- 固件版本方便远程排查 );uk_dev_ts这个唯一索引是整个设计里最值钱的一行。设备补传、网络重发、服务端重推都会造成重复有了它插入时用INSERT OR IGNORE直接吞掉重复行不用写任何去重逻辑。synced字段是给离线补传用的APP 在无网时把数据写本地标 0恢复网络后按时间顺序批量上报成功一条改一条。字段类型上有个小坑温湿度用REAL而不是整数放大一百倍存是因为 SQLite 的 REAL 是 8 字节双精度精度完全够而放大存储会在聚合计算时多一步除法容易漏。湿度上限 100温度范围按探头的实际量程约束落库前做一次区间校验超出范围的直接标质量位别让它污染统计。3.3 连接状态机与离线缓存策略移动端最不可靠的就是网络所以连接必须有明确的状态机不能靠“连上了就用、断了就崩”。我一般定义五个状态空闲、连接中、已连接、重连中、已断开。连接中超过 8 秒没收到 CONNACK 就转重连中已连接状态下心跳 30 秒一次连续两次没响应转重连重连采用指数退避1 秒、2 秒、4 秒、8 秒封顶 30 秒避免弱网下疯狂重试把电量吸干。离线缓存的关键是“写入优先于上报”。采样到达时先写本地库标未同步再尝试发送发送成功才更新标志。这样即使进程被杀数据也还在库里下次启动扫一遍synced 0的记录补齐就行。补传时要限速一次取 50 条、间隔 200 毫秒别一口气把几百条怼上去服务端和 broker 都吃不消。设备端的时间校准放在这里说。很多采集板没有 RTC重启后时间是 1970 年上报的时间戳全是乱的曲线画出来像心电图。稳妥的做法是设备启动后先通过 SNTP 对时拿到时间再开始上报服务端收到时间戳明显偏离的报文比如和服务器时间差超过 5 分钟统一用服务器时间覆盖并打标记。这个逻辑写在服务层比指望每个固件都做对要现实得多。4. 手机APP核心模块实现实时曲线、历史查询与阈值告警界面清单通常就三屏设备列表、实时详情、历史与告警。屏不在多在于每屏都有明确的一件事。下面按接入、实时、历史、告警四块讲实现代码都是按能跑起来的最小骨架写的缺的部分补上就成。4.1 设备接入与配网流程配网是新手最容易卡住的一步。常见两条路智能配网手机把 Wi-Fi 账号密码编码后广播设备抓包解析和热点配网设备开热点手机连上去填表单。课程设计和毕设我建议直接上热点配网协议简单、成功率接近百分之百、答辩现场演示不尴尬。智能配网虽然体验好但不同手机对组播和广播的限制不一致现场翻车概率高。配网流程固定成四步设备进入配网模式并亮灯、手机连上设备热点、提交 Wi-Fi 凭据和设备名、设备连上路由后向 broker 发一条上线报文APP 收到上线报文即视为配网成功。第四步的等待给 60 秒超时超时提示检查密码或信号。设备编号在出厂时就烧进芯片不要让用户在 APP 里手输输错一位后面全乱。配对成功后把 deviceId 存进本地偏好下次启动直接尝试静默连接连不上再引导用户检查。这套流程写下来不到两百行但把“用户第一次用”的路径堵死了大半。4.2 实时数据订阅与曲线渲染实时曲线是这套 APP 的门面也是最容易卡的地方。核心做法只有一条连接只建一次数据流广播出去页面只订阅、不持有连接。// 订阅实时数据一次订阅多处消费避免每个页面各连一条链路 class RealtimeService { final MqttClient _client; final _controller StreamControllerThSample.broadcast(); StreamThSample get stream _controller.stream; RealtimeService(this._client, String deviceId) { _client.updates!.listen((events) { for (final e in events) { if (e.topic ! dev/$deviceId/up) continue; final s ThSample.fromJson(jsonDecode(e.payload as String)); if (s.quality ! 0) continue; // 脏数据不进曲线 _controller.add(s); } }); } void dispose() _controller.close(); }broadcast()是关键普通 Stream 只允许一个监听者第二个页面订阅会直接抛异常。quality ! 0的过滤放在入口处是为了让曲线组件永远只处理干净数据别在渲染层写 if。曲线渲染有三个参数必须调点数上限、刷新频率、X 轴跨度。一次展示最多保留 200 个点超了从头裁掉界面刷新按 500 毫秒合并一次采样再快也别每来一条就重绘X 轴按最近 1 小时、6 小时、24 小时三档切换。温度湿度的 Y 轴不要共用各自独立刻度否则湿度那条线会把温度压成一条平线。还有一点图表库默认会在数据变化时重建整个 widget几百个点时就明显掉帧。做法是把数据列表换成不可变对象比较新旧列表的identical结果再决定是否重绘这一招能把帧率从个位数拉回流畅。4.3 历史数据分页查询与本地缓存历史查询的坑在于“一次要全部”。用户点开 24 小时视图你一口气查 2880 条低端机上直接卡三秒。稳妥做法是分页加聚合两条腿走路看趋势用聚合表看细节用分页原始表。-- 原始点分页按时间倒序每页 100 条 SELECT ts, temp, humi, quality FROM th_sample WHERE dev_id ? AND ts BETWEEN ? AND ? ORDER BY ts DESC LIMIT ? OFFSET ?; -- 按小时聚合趋势图专用查询量从千级降到几十 SELECT (ts / 3600000) * 3600000 AS hour_ts, AVG(temp) AS avg_t, MAX(temp) AS max_t, MIN(temp) AS min_t, AVG(humi) AS avg_h, MAX(humi) AS max_h FROM th_sample WHERE dev_id ? AND ts BETWEEN ? AND ? AND quality 0 GROUP BY hour_ts ORDER BY hour_ts ASC;聚合语句里quality 0必须加否则一条传感器异常记录就能把最大值拉到离谱的位置。(ts / 3600000) * 3600000是整数除法取整到小时比用日期函数快得多。分页参数LIMIT 100 OFFSET n在数据量大时越翻越慢超过一万条建议换成基于游标的写法WHERE ts 上一页最后一条的 ts ORDER BY ts DESC LIMIT 100走索引稳定在毫秒级。本地缓存和远程查询要有个优先级约定先查本地本地命中且时间范围完整就直接返回不完整再拉远程并合并。这样断网时历史仍可看联网时也不会重复拉同一段数据。4.4 阈值告警与本地通知告警最容易出的问题是刷屏。温度刚过阈值上下浮动 0.2 度一晚上推三十条通知用户第一件事就是把通知关了等于告警系统失效。解决办法是加回差和持续时间连续几个点确认才算真告警。// 连续 3 个采样点超阈值才触发回落需要连续 2 个点避免临界抖动刷屏 class ThresholdGuard { final double high; final double hysteresis; // 回差建议取 0.5 度或 3%RH int _hit 0; bool _alarming false; ThresholdGuard({required this.high, this.hysteresis 0.5}); /// 返回 true 表示本次应发出告警 bool feed(double v) { if (!_alarming) { _hit v high ? _hit 1 : 0; if (_hit 3) { _alarming true; return true; } } else { _hit v high - hysteresis ? _hit 1 : 0; if (_hit 2) { _alarming false; _hit 0; } } return false; } }hysteresis就是回差触发值和恢复值拉开一段距离物理上对应“温度确实降下来了”而不是“抖了一下”。三个点确认意味着 30 秒周期下大约 90 秒才报这个延迟对温湿度场景完全可接受比误报有价值得多。通知通道单独建一个别和聊天类通知混在一起否则用户静音了别的消息会连告警一起漏掉。安卓上要申请通知权限并建高优先级通道iOS 上要在推送 payload 里声明中断级别。本地通知和远程推送同时存在时用消息 id 去重避免同一条告警弹两次。告警确认动作要落到库里记下谁在什么时候确认的验收时这是能拿出来的证据。5. 避坑与排查联调阶段最容易翻车的五件事项目能不能按时交往往不取决于功能写得多全而取决于这几个坑有没有提前躲开。下面五条都是我在类似项目里真实见过的按“现象、原因、解决”的格式写遇到时直接对号。5.1 曲线一打开就卡滑动掉帧现象实时详情页进入三秒后开始卡左右滑动明显掉帧低端机上一分钟能卡死。原因每收到一条采样就调用setState重建整个页面图表组件带着几百个点全量重绘另外每个页面各自起了一个 MQTT 连接三个页面就是三条链路回调互相打架。解决连接收敛成全局单例数据用广播流分发界面刷新做 500 毫秒节流用定时器合并批量更新图表数据列表保持不可变用长度和首尾时间戳做一次快速比较没变化就不重绘。改完之后同样的机器上帧率稳定在 55 以上。5.2 数据里偶尔冒出一个 0 或者 85现象曲线偶尔出现一个贴底的尖刺温度显示 0℃ 或湿度 85%下一条又恢复正常。原因传感器读取时序不对DHT 类探头两次读取间隔小于两秒会直接返回上一次的缓存值甚至错误码代码里没判空就当成有效值写进了库有的固件读失败返回 0被当成真实温度。解决读失败时不要写 0而是标质量位照常上报固件里加最小读取间隔并做 CRC 校验APP 端和聚合查询都过滤quality ! 0。另外在协议里约定好量程温度超过 -40 到 80、湿度超过 0 到 100 的一律判为异常。5.3 切后台再回来设备一直显示离线现象手机锁屏十分钟解锁后 APP 显示设备离线实际上设备一直在发数据杀掉 APP 重开又正常。原因系统在后台挂起了网络线程MQTT 心跳停了broker 判定客户端超时后踢掉连接APP 回到前台没有主动检查连接状态还拿着一个已经死掉的客户端对象。解决监听应用生命周期回到前台先检查client.connectionStatus不是已连接就走一次重连把心跳间隔调到 30 秒以内并开启自动重连服务端侧用last_seen判断在线不要只依赖 broker 的会话状态。设备在线状态在界面上用颜色加文字双标识别只用一个小圆点。5.4 历史数据顺序混乱同一条记录出现两次现象历史列表里时间跳来跳去偶尔能看到两条一模一样的数据统计总数比预期多。原因设备重启后时间归零上报的时间戳比之前的还小网络重发和设备补传造成重复而表里没有唯一约束。解决设备启动先对时服务端收到偏差超过 5 分钟的报文用服务器时间覆盖建(dev_id, ts)唯一索引写入用INSERT OR IGNORE查询统一按ts排序不依赖插入顺序。5.5 阈值设了 6058 度就开始报现象阈值明明设的 60℃58℃ 的时候通知就来了用户以为系统有毛病。原因比较的时候拿错了字段温度告警用了湿度值或者单位不统一传感器返回华氏度而阈值按摄氏度填。这种错误在多人协作时特别常见。解决单位在模型层就固定死字段名带单位后缀tempC、humiPct比较前做一次显式转换阈值配置里也标明单位界面上显示“60 ℃”而不是裸数字。上线前拿一支温度计对着吹热风做一次端到端验证。6. 验收与答辩怎么证明这套监控是准的、稳的、能交付的功能跑通只算及格验收看的是三件事数据准不准、链路稳不稳、异常有没有被记录。我带过的项目里最后一周基本都花在这三件事上而不是加新功能。先说准。拿一支经过校准的温湿度计放在探头旁边静置二十分钟同一时刻读数差值控制在温度 ±0.5℃、湿度 ±3%RH 以内算合格。这个测试要做三组常温常湿、开空调降温、人为加湿覆盖量程两端。数据记进表格作为验收证据。再说稳。拔掉路由器电源五分钟再插回观察 APP 是否自动重连、数据是否补齐、曲线有没有断点把手机切飞行模式十分钟再恢复检查本地缓存的synced 0记录是否全部上报成功连续跑 72 小时统计丢点率超过 1% 就要回头查心跳和重连参数。最后说异常留痕。日志里要能查到每次连接、断开、重连、告警触发与确认的时间点这是答辩时唯一能证明“系统真的跑过”的东西。验收项方法合格标准读数准确性与校准仪表同点静置对比温度 ±0.5℃湿度 ±3%RH断网补传断网 10 分钟后恢复补传成功率 100%曲线无断点长时间稳定性连续运行 72 小时丢点率低于 1%无崩溃告警有效性手动升温越阈值90 秒内触发不重复推送历史查询性能查 30 天聚合曲线首屏 1 秒内出图答辩时被问得最多的三个问题提前准备好答案采样周期为什么是 30 秒而不是 5 秒回答时把数据量估算和传感器响应时间一起说断网期间的数据怎么保证不丢回答写入优先于上报、synced标志和补传限速告警为什么会延迟回答回差和连续点确认是为了防抖并给出误报率的对比数据。这三个问题答得出来基本就稳了。从那以后我每次做这类项目都会在写第一行业务代码之前先把验收表列出来边开发边填到交付那天表格填满心里才踏实。希望帮到你。本文还有配套的精品资源点击获取