简介:这是一份面向物联网(IoT)开发者与Android初学者的温湿度监控手机APP完整源码包,覆盖DHT11/DHT22传感器数据采集、单片机处理、无线传输,到手机端实时展示与LED控制的完整链路。压缩包共143个文件,体量约3.98MB,内部含有93张PNG界面素材、17个class编译结果、11个XML布局与项目配置、10个JAR第三方依赖库、3个Java核心源码、1个可直接安装的APK调试包,以及工程配置文件等,结构清晰可导入Android Studio运行。代码中详细演示了Android前端布局与图表可视化(MPAndroidChart)、后端RESTful API设计、MySQL/SQLite数据库存储、WebSocket实时刷新、HTTPS/OAuth2.0安全通信,并涉及SPI/I2C/MQTT等物联网协议的实际协作;同时保留LED开关控制与实时温湿度刷新等功能逻辑。目前已有1615人浏览学习,特别适合需要参考完整IoT+移动端项目架构的在校学生、软件开发者及竞赛团队。无论是学习硬件接入,还是练习APP工程化开发,都能从中找到对应参考。 做个温湿度监控手机APP的开发项目,是我去年冬天被家里暖气逼出来的决定。客厅温度一直上不去,体感冷了才去摸暖气片,完全抓瞎。我先后买过两个几十块的温湿度计,只能站在跟前看数字,夜里温度最低是多少、湿度变化曲线根本查不到,更别说在手机APP上远程回看了。后来索性自己动手,从传感器到服务器再到手机端,完整写了一套温湿度监控系统,把整套开发代码沉淀了下来。今天把这套东西完整复盘一遍,覆盖硬件采集、HTTP上报、Flutter APP展示、联调排错的全过程。适合刚接触物联网开发、想自己掌握数据而不是被厂商生态绑定的朋友,也适合拿来做课程设计或毕业设计的项目骨架。
1. 为什么自己搭一套温湿度监控系统而不是买现成设备
1.1 现成温湿度计的两类典型痛点
不到一百块的温湿度计,绝大多数是本地LCD显示,数据存在设备内部,只能看当前值,不能远程访问,也没有历史曲线。贵一点的智能温湿度计,虽然能连手机APP,但走的是厂商自己的云,数据格式不公开,API更不开放,你想把温湿度数据接到自己的服务端或者和家里的智能家居联动,基本只能靠逆向和抓包,维护成本高,随时还可能被厂商停服变成砖头。
我自己的需求很明确:家里多个房间放采集节点,数据必须落到自己的服务器上,手机APP能看实时值、24小时曲线、最低最高温湿度,后面还想加告警推送。这些需求用市面成品很难同时满足,自己写一套反而是最稳妥的。算下来单个节点成本大概五十块以内,开发板加传感器,比一个联网温湿度计还便宜。
1.2 自制系统的能力边界和整体数据流
自己开发的好处不是省钱,而是数据链路的每一环都可以控制。硬件端想换传感器就换传感器,服务端想加接口就加接口,APP想改UI就改UI。当然代价也很明确:你需要同时碰嵌入式、服务端和移动端三个方向,知识面要求比较宽。如果你只是想要一个能用的温湿度计,直接买成品;如果你想做的是“一套可扩展的环境监控系统”,那自己写代码这条路值得走。
整套系统的数据流非常朴素:温湿度传感器采集数据,交给ESP32单片机做简单处理,然后通过WiFi以HTTP POST的方式上报到轻量服务端,服务端把数据存进数据库并暴露查询接口,手机APP定时拉取接口数据,渲染成实时卡片和趋势曲线。这个链路里没有复杂消息队列,也没有微服务,个人项目最重要是先把链路跑通,再去谈架构升级。
1.3 先定接口再写代码,这条经验救了我
我一开始犯了个典型的错误:先把ESP32端代码写完,再回头写服务端,最后才写APP,结果三端的字段命名各写各的,联调时改来改去浪费了大半天。后来我总结出一个固定流程:先定义接口文档,哪怕只是写在记事本里的几行JSON示例,然后服务端先按这个接口mock返回,APP端同时开发,最后再让硬件端对接真实接口。三步并行但契约先行,联调效率高非常多。
接口只需要两个:一个是POST /api/sensor,用于ESP32上报温湿度;一个是GET /api/latest?device=xxx,用于APP查询某个节点的最新数据。上报的JSON格式如下:
{ "device": "living_room", "temp": 23.5, "humi": 45.2 }服务端返回{"code":0}表示成功。这里device字段用来区分不同房间,后面扩展多节点部署时全靠它。
2. 硬件端采集与上报:把温湿度变成手机APP能读的数据
2.1 传感器选型:SHT30比DHT22省心太多
温湿度传感器我前后试过DHT22和SHT30,两者的差别在实际使用中非常明显。
| 项目 | DHT22 | SHT30 |
|---|---|---|
| 温度精度 | ±0.5℃ | ±0.2℃ |
| 湿度精度 | ±2%~5% RH | ±2% RH |
| 接口 | 单总线,时序敏感 | I2C,稳定 |
| 采样间隔要求 | 建议大于2秒 | 可连续读取 |
| 价格 | 约10元 | 约15~20元 |
| 代码复杂度 | 中,需要时序库 | 低,直接读寄存器 |
DHT22价格便宜但单总线协议对时序要求高,换一个开发板或者线长一点就容易读取出错,湿度受干扰也比较明显。SHT30是I2C接口,接线就三根线(VCC、GND、SDA、SCL加一根也行),代码稳定得多。如果预算允许,我建议直接上SHT30,省下来的调试时间远超那几块钱差价。
接线方面,ESP32的默认I2C引脚是GPIO21对应SDA、GPIO22对应SCL,SHT30模块的VCC接3.3V,GND接GND。注意模块上的上拉电阻一般已经集成,不需要额外处理。
2.2 采样逻辑:别把原始读数直接上报
传感器刚上电时读数不稳定,尤其是湿度,前十几秒可能跳得离谱。我实测过SHT30刚通电时温度比正常值高两三度,因为传感器自身通电后会轻微发热,PCB板子测温需要时间稳定。所以采集代码里不能上电就读,要等几秒让传感器稳定,然后连续采样多次取平均值或中位值。
我的做法是连续读5次、每次间隔100毫秒,去掉最大值和最小值,剩下三个取平均。这样处理之后读数平稳很多,基本不需要额外做软件滤波。如果你对精度要求高,可以在代码里留一个校准偏移量,比如tempOffset = -0.3,把已知的系统偏差在最终读数里修正掉。
2.3 ESP32采集上报代码参考
下面是ESP32端基于Arduino框架的采集和上报代码,我用的传感器库是Adafruit SHT31,Wifi和HTTPClient用ESP32内置库:
#include <WiFi.h> #include <HTTPClient.h> #include <ArduinoJson.h> #include <Wire.h> #include "Adafruit_SHT31.h" Adafruit_SHT31 sht30; const char* ssid = "your_wifi"; const char* password = "your_password"; const char* serverUrl = "http://192.168.1.100:8080/api/sensor"; const char* deviceName = "living_room"; float readStableTemp() { float values[5]; for (int i = 0; i < 5; i++) { values[i] = sht30.readTemperature(); delay(100); } // 冒泡排掉最大最小值,剩下取平均 for (int i = 0; i < 4; i++) { for (int j = 0; j < 4 - i; j++) { if (values[j] > values[j + 1]) { float tmp = values[j]; values[j] = values[j + 1]; values[j + 1] = tmp; } } } return (values[1] + values[2] + values[3]) / 3.0; } void uploadSensorData(float temp, float humi) { HTTPClient http; http.begin(serverUrl); http.addHeader("Content-Type", "application/json"); StaticJsonDocument<128> doc; doc["device"] = deviceName; doc["temp"] = temp; doc["humi"] = humi; char payload[128]; serializeJson(doc, payload); int httpCode = http.POST(payload); if (httpCode != 200) { Serial.printf("upload failed, code=%d\n", httpCode); } http.end(); } void setup() { Serial.begin(115200); Wire.begin(21, 22); sht30.begin(0x44); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } delay(3000); // 上电稳定时间 } void loop() { float t = readStableTemp(); float h = sht30.readHumidity(); uploadSensorData(t, h); delay(30000); // 30秒上报一次,够用了 }这里有几个细节说一下。delay(3000)放在WiFi连接成功之后,是为了让传感器从供电波动中恢复稳定。上报周期30秒,对家庭环境足够了,太频繁反而会让服务端数据库迅速膨胀。如果你放在电池供电的场景,上报周期可以拉长到5分钟甚至更长,配合ESP32的深度睡眠,续航能到几个月。
2.4 为什么选HTTP轮询而不是MQTT
方案选型的时候我犹豫过MQTT。MQTT是物联网设备通信的主流协议,发布订阅模式,实时性很高,服务端能即时感知设备状态。但实际考虑下来,个人项目的节点数量有限,设备上报频率又低,用HTTP POST简单直接,调试也方便,出问题用curl就能模拟设备端测试。MQTT需要额外搭建或依赖Broker,还要处理主题设计、遗嘱消息、QoS等级这些概念,时间成本会明显增加。
APP端同理,我用的是定时轮询接口,每30秒或60秒拉一次最新数据。家庭环境下一分钟内的延迟完全感知不到,没必要为了“实时”引入长连接和推送通道,徒增复杂度。如果你的项目要求湿度突变时手机立刻弹告警,那再用MQTT加WebSocket也不迟,但一开始就把链路做简单,永远是个人项目的第一原则。
3. 手机APP端:从拉取数据到绘制温湿度曲线
3.1 技术栈选择:我直接用Flutter跨平台方案
到了手机APP这一层,摆在面前的无非是Android原生、iOS原生、Flutter、uni-app这几个方向。我最后选了Flutter,原因很简单:一套Dart代码同时覆盖Android和iOS,温湿度曲线用fl_chart库就能画,不需要我在Java和Swift之间切换。而且Flutter的热重载对UI迭代很友好,改完布局马上能看到效果,调试体验比原生好不少。
如果你只是自己用,只装Android包,那用Kotlin写原生也可以。但考虑到这套监控系统后面想分享给家人用,iOS也得有安装包,Flutter是时间成本最低的选择。我用的开发环境是Flutter 3.x稳定版,IDE用VS Code,Android侧targetSdkVersion设的是33。要注意Flutter版本和Android Gradle插件的版本匹配,Flutter升级之后如果不跟着升级gradle配置,很容易在构建阶段报错。
3.2 数据层:Dio网络请求加本地缓存
APP的数据层我用Dio做网络请求。Dio拦截器里统一设置超时时间,避免弱网环境下请求长时间卡住。定义了一个SensorApi类集中管理接口调用,代码结构清晰,后面加接口也方便:
import 'package:dio/dio.dart'; class SensorApi { static final Dio _dio = Dio(BaseOptions( baseUrl: 'http://192.168.1.100:8080', connectTimeout: Duration(seconds: 5), receiveTimeout: Duration(seconds: 5), )); static Future<Map<String, dynamic>> fetchLatest(String device) async { final resp = await _dio.get( '/api/latest', queryParameters: {'device': device}, ); return resp.data as Map<String, dynamic>; } }这个接口返回的JSON长这样:
{ "code": 0, "data": { "device": "living_room", "temp": 23.5, "humi": 45.2, "time": "2025-01-06 22:30:00" } }数据拿到之后不能直接扔,我在APP里用shared_preferences把最近一次成功请求的数据缓存到本地。为什么要缓存?实际场景里手机可能短暂断网,或者服务端短暂不可用,有了缓存,APP打开时先渲染上一次的数据,再静默刷新,用户感知是页面永远有内容,而不是白屏或者一个错误提示。这个体验细节在物联网APP里很重要,因为设备端数据本来就有时间连续性,展示旧一秒的数据没有风险。
3.3 首页实时卡片和24小时曲线的实现
首页UI不需要花哨,核心是两大块:一块是当前温湿度大数字卡片,一块是24小时趋势曲线。卡片部分我直接用了一个Container加圆角背景,温度用大字显示,湿度排在下面,代码不复杂就不占篇幅了。关键是趋势曲线,我用fl_chart的LineChart来画。
曲线部分的核心是把接口返回的时间点转成FlSpot坐标。x轴用时间戳(epoch秒),y轴用温度值或湿度值。温度曲线和湿度曲线的数值范围差很多,温度可能只在15到30之间波动,湿度最高能到80,如果共用左Y轴,温度变化会被压成一条平线。我处理方式是两条数据线分别配左右两个Y轴,温度用左轴,湿度用右轴,这样两条曲线都能看清趋势。
LineChart( LineChartData( lineBarsData: [ LineChartBarData( spots: tempPoints, // List<FlSpot>,x为时间戳,y为温度 color: Color(0xFFFF9800), isCurved: true, dotData: FlDotData(show: false), barWidth: 2, ), LineChartBarData( spots: humiPoints, // List<FlSpot>,x为时间戳,y为湿度 color: Color(0xFF03A9F4), isCurved: true, dotData: FlDotData(show: false), barWidth: 2, ), ], titlesData: FlTitlesData( leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true)), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: true)), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: true)), ), ), )曲线做出来后有一个要处理的问题:如果历史数据量很大,比如一个月的数据全画上去,FlSpot列表可能有几千个点,线形图绘制会有明显卡顿。我的方案是只请求最近24小时的数据,APP端拿到后再做降采样,比如把5分钟的数据聚合成一个点,这样24小时最多显示两百多个点,手机上画起来毫无压力。
3.4 后台刷新:冷启动之外最重要的体验
APP拉数据的时机我做了两层。第一层是页面可见时立刻拉一次,覆盖用户切回APP的场景;第二层是定时器后台刷新,每60秒请求一次最新数据。这个频率对家庭监控足够,数据曲线基本是连续的。
Android上实现定时刷新有个坑:进程可能被系统回收,尤其是国产ROM的后台清理策略很激进。我用的是前台Service加Flutter的Timer.periodic组合方式。Service通过startForeground创建一个常驻通知,让系统知道这个APP正在干活,降低被回收的概率。如果你不想做常驻通知,也可以退一步接受“打开APP时刷新一次”的体验,看你自己的取舍。iOS侧因为后台限制更严格,我的妥协方案是:每次切入前台时从服务端补拉缺失时间段的历史数据,而不是试图在后台保持长连接,这在实际使用中完全够用。
4. 联调阶段反复踩过的坑与定位过程
4.1 Android 9之后默认禁止明文HTTP请求
第一个坑来得特别快。APP端代码写完连上服务端,一请求就直接抛异常,错误信息指向cleartext traffic。这是因为targetSdkVersion 28及以上,Android默认禁止HTTP明文流量,只允许HTTPS。我本地开发环境用的是局域网IP加8080端口,没有配HTTPS,自然被拦。
定位方法很简单:抓日志看到CLEARTEXT communication to ... not permitted by network security policy,基本就是这个问题。修复方法有两层,最省事的是在AndroidManifest.xml的application节点加一行:
<application android:usesCleartextTraffic="true" ... >这样整个APP都允许明文HTTP请求。如果要更安全,可以用networkSecurityConfig只允许特定域名走明文,其他域名强制HTTPS。我的建议是内网测试阶段先用usesCleartextTraffic跑通链路,后期上正式环境换成HTTPS,再把这一行去掉。
4.2 手机锁屏后数据不更新,曲线出现断层
跑通基础功能后我发现一个问题:手机亮屏时数据正常刷新,一锁屏过几分钟再打开,曲线中间有一段空白。排查下来是Android的Doze省电模式在起作用。屏幕关闭后系统会限制网络访问和CPU调度,定时器被延迟甚至暂停,网络请求频率大幅降低,这不是APP代码的bug,而是系统的正常策略。
解决方案我试过两种。最简单的是把刷新逻辑放到前台Service里,同时给APP加电池优化白名单权限,引导用户去系统设置里把APP设为“不受限制”。实测这个组合在多数手机上能让后台刷新稳定运行。更彻底的方案是用WorkManager做周期性任务,但系统对周期任务的最小间隔做了限制,最短也要15分钟,对温湿度监控来说粒度太粗,所以我最终采用了前台Service方案。如果你对实时性没这么敏感,WorkManager其实更省电也更规范。
4.3 传感器读数跳变,温度持续偏高
硬件端也有一个让我排查了很久的问题:同一个房间里,SHT30读到的温度比水银温度计高两度左右,湿度偶尔还会突然跳高再恢复。刚开始我怀疑是传感器坏了,换了一个新的还是同样现象。
后来把传感器拿下来裸奔测试才找到原因:我把传感器直接放在了ESP32开发板的塑料外壳里,板子上的稳压芯片和WiFi模组工作时发热,壳内空气不流通,热量全被传感器吸收了,导致读数虚高。湿度跳变则是空气流通不畅加上传感器表面可能残留助焊剂导致的。解决办法很朴素:把传感器引出来,用杜邦线连接到板子外部,放在通风处,不要贴着任何发热元件。改成外接之后,温度读数稳定在合理范围,湿度跳变也消失了。
4.4 时间戳时区不一致导致曲线整体偏移
服务端上线后,APP曲线出现了一个很隐蔽的问题:温度曲线整体平移了8个小时。我一开始以为是数据延迟,后来发现是ESP32上报时自己生成了时间戳,设备里用的是UTC时间,APP端直接渲染出来没有转时区,自然差了一个时区。
定位清楚后就简单了,处理原则是“设备端不生成业务时间,服务端统一打点”。ESP32上报时只发设备和数据,服务端收到后取当前服务器时间作为这条记录的时间戳。这样无论设备端时钟是否准确、无论设备在哪个时区,数据的时间轴都统一和服务端一致。温湿度监控这种低频数据场景完全不需要设备端带时间戳,越是简单的方案越不容易出错。
4.5 真机调试常见的网络访问问题
最后说一个调试环境的小坑。我用Android模拟器调试时,APP里访问服务端的地址不能写127.0.0.1,因为模拟器里的127.0.0.1指向的是模拟器自己,要访问宿主机必须写10.0.2.2。真机调试时又换了新问题:真机通过USB连接电脑,虽然能装APK,但手机的网络和电脑不在同一个网段,直接访问电脑IP根本不通。
我的解决办法是真机开启USB网络调试,通过ADB把手机端口转发到电脑端口:
adb reverse tcp:8080 tcp:8080执行之后,手机APP里访问http://127.0.0.1:8080就能连接到电脑上跑的服务端。这个技巧对局域网环境不稳定或者公司WiFi开了AP隔离的情况特别管用,不用折腾网络配置,USB插上就能联调。
5. 实测效果、优化方向与项目心得
5.1 整套系统的实测数据
我在家里部署了三个采集节点,客厅、卧室、阳台各一个,跑了整整两周。用标准的温湿度计做参照,SHT30的温度偏差基本在±0.2℃以内,湿度偏差在±5%以内,精度完全可以接受。三个节点上报成功率在99%以上,偶尔一两次失败也是因为路由器重启。APP冷启动到显示曲线大概两秒,主要耗时在服务端查询和网络传输,页面加载时先出缓存数据再刷新,体感上几乎没有等待。
功耗方面,ESP32以30秒为周期上报,实测平均电流在90mA左右,长期插USB供电没什么问题。如果改成电池供电,我会把上报周期调成5分钟,并且让ESP32在两次上报之间进入深度睡眠,这样理论待机电流能降到几十微安,一节18650电池用几个月不难。
5.2 继续优化可以做的几件事
这套系统的代码结构留好了扩展位,后面有几个明显可以升级的方向。一是告警推送,当某个房间温度超过设定阈值或者湿度低于某个值,服务端调用服务器酱或邮件接口推送到手机。二是历史数据整理,目前数据落在数据库里,时间长了会变大,可以写个定时任务只保留每小时的一条聚合数据,原始数据留一个月。三是多用户支持,如果家人也要看数据,加一个简单的token鉴权就够了。
关于低代码平台,我也顺手提一句自己的感受:如果你只是想要一个演示用的原型,低代码拖拽生成一个温湿度大屏确实快;但真到了生产环境,数据的格式、采集频率、告警逻辑、设备管理这些细节,还得靠正经代码一条条写清楚。低代码不是不能用,而是边界要清晰,原型验证可以拿它省时间,长期维护的监控系统我不建议押在上面。
5.3 写代码之外的一点个人体会
这套项目做完,我最深的感受是:真正耗时间的不是某个端的具体代码,而是把数据链路上每一环的“想当然”填平。硬件端的传感器发热、APP端的Android明文流量限制、服务端的时间戳统一,每一个问题单拎出来都不难,但串联起来就是开发周期的大头。希望这份复盘能帮你绕开这些坑,让你把时间花在真正有意思的事情上,比如把监控数据变成保护家人舒适生活的实用工具。
本文还有配套的精品资源,点击获取