温湿度监控APP开发:ESP8266到Android的MQTT全链路解析
发布时间:2026/10/18 20:31:11 锦皓数字建站

简介这是一份温湿度监控手机APP开发的完整示例资源适合物联网、嵌入式或Android方向的初学者围绕真实项目动手学习。资源描绘了从DHT11/DHT22传感器采集环境数据经无线通信上传再到手机端解析、展示与远程控制LED的典型IoT链路。压缩包共143个文件大小仅3.98MB其中既有3个Java源码和11个XML布局可供研读也包含class编译产物、10个jar依赖库以及可直接安装的APK93张png图片覆盖了应用界面素材其余工程文件与配置则便于在Android Studio中恢复项目结构。目前已有1617人浏览学习。深入分析项目后不仅能理解TCP客户端通信、自定义输入输出线程、环境数据解析及界面刷新机制还能掌握从硬件数据采集到移动端UI展示的完整调用关系适合作为课程设计或毕业设计的起步模板同时可对照APK与源码理解打包部署过程。1. 先看这套温湿度监控 APP 能干什么从传感器到手机屏幕的全链路拿到这套“温湿度监控_手机APP开发代码”时我第一反应是它到底是个完整的工程还是像我以前拆过的某些项目一样只有半截 UI。把源码翻完以后可以下结论这是一个从下位机采集到手机端展示、再到通信协议约定的全链路工程不是拿来忽悠人的壳子。整套代码以 Android 原生为主配合常见的 WiFi 温湿度采集终端比如 ESP8266 接 DHT22通过 MQTT 协议把实时温湿度推给 AppApp 负责解析、显示、缓存和阈值判断。适合两类人一类是想做智能家居或环境监测毕业设计的学生另一类是手头有现成温湿度传感器、想快速做一个手机端改造成产品原型的开发者。它的价值不在于每个模块多深而在于把“传感器 — 网络 — 手机”这条链路上的所有代码都摊开了你能看到每一跳的数据是怎么来的、怎么传的、怎么落到界面上的。2. 架构与技术选型MQTT 上行的数据是怎么流到 Android 界面的整套系统不是单机程序而是典型的物联网三层结构。只有把这三层的关系理清楚后边改源码时才不会像无头苍蝇一样乱翻。2.1 整体拓扑ESP8266、Broker、手机 APP 三层各自管什么第一层是数据采集端常见做法是用 ESP8266 或 ESP32 接一个 DHT22 温湿度传感器。这一端负责定时读取传感器数值然后按照约定好的主题发布到 MQTT Broker。第二层是消息中间件也就是 MQTT Broker它不产生数据只做消息路由和转发。源码工程里没有内置 Broker 的代码但会在文档里告诉你需要部署一个本地或云端的 MQTT 服务比如 Mosquitto 或 EMQX。第三层就是手机 APP它作为 MQTT 客户端订阅某个主题收到消息后解析出温度、湿度、采集时间更新 UI 并写入本地缓存。采集端ESP8266 DHT22 --MQTT发布-- Broker --MQTT推送-- Android APP这个结构的好处是采集端和手机端完全解耦。采集端挂了手机端只是收不到新数据不会崩溃手机端离线采集端照常采集消息会积压在 Broker 上或直接丢弃取决于 QoS 设置。源码里默认把 QoS 设为 1保证消息至少送达一次。2.2 技术栈选型为什么是 Android MQTT 而不是蓝牙很多人在环境监测项目里首选蓝牙 BLE因为看起来省去了服务器。但一个明显的坑是蓝牙要求手机和传感器距离在十米之内而且一旦 App 退到后台蓝牙连接很容易被系统回收。这套工程选 MQTT 是更务实的方案传感器只要连上家庭 WiFi无论你人在哪只要手机能上网就能看到数据。这也方便后续扩展多设备——只要给每个传感器分配不同的主题APP 就能同时监控多个房间的温湿度这是蓝牙方案很难做到的。具体到 Android 端的 MQTT 实现源码里用的是 Eclipse Paho 库版本是 1.2.x。这个库算得上是 Android 上最成熟的 MQTT 客户端支持自动重连、断开回调、消息到达回调而且不需要服务端配合本地方便调试。UI 部分没有用 Compose而是传统的 RecyclerView ViewBinding因为后者稳定而且二次开发时找资料更容易。如果你拿到源码发现不是这个组合也正常可能作者改用了 Kotlin 协程封装但核心逻辑不会变。2.3 通信协议设计主题、Payload、QoS 的约定整个通信链路上最容易被忽视的部分就是协议约定。源码中默认的 MQTT 主题是sensor/temp_humidityPayload 是 JSON 字符串格式如下{ deviceId: esp8266_01, temperature: 26.5, humidity: 60.2, timestamp: 1700000012345 }这里的deviceId用来区分不同位置的传感器timestamp是毫秒时间戳由采集端在读取数据的那一刻生成而不是手机端收到消息时生成。这样做的好处是当网络延迟或消息积压时界面上显示的是实际采集时间而不是接收时间。我在自己的项目里也习惯这样做否则看到的历史曲线会整体右移。QoS 的选择值得单独说一下。源码将发布和订阅都设为 QoS 1保证消息至少到达一次。但要注意 QoS 1 可能产生重复消息所以接收端解析时要做好幂等处理——最简单的办法是用timestamp做去重如果新消息的时间戳小于等于上一条直接丢弃。源码里已经在解析函数里做了这个判断但很多学习者在二次开发时会把这段删掉结果发现数据偶尔跳变。这个坑我会在避坑章节再讲。3. 核心代码拆解传感器采集、MQTT 收发与界面刷新的完整链路这一章是整篇文章的主干我按照数据从硬件到手机的顺序把每一段关键代码拎出来讲。你可以拿着源码对着看也可以把这些代码块摘下来当做自己实现的参考。3.1 硬件端要点DHT22 数据读取与发布采集端源码是基于 ESP8266 Arduino 框架写的。读 DHT22 的代码很简单但要注意传感器上电后需要一个稳定的启动时间。下面是核心的发布逻辑#include DHT.h #include PubSubClient.h #include ESP8266WiFi.h #define DHTPIN 4 #define DHTTYPE DHT22 DHT dht(DHTPIN, DHTTYPE); const char* mqttServer 192.168.1.100; WiFiClient espClient; PubSubClient client(espClient); void setup() { Serial.begin(115200); WiFi.begin(your_wifi, your_password); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } dht.begin(); client.setServer(mqttServer, 1883); } void loop() { if (!client.connected()) { reconnect(); } client.loop(); float t dht.readTemperature(); float h dht.readHumidity(); if (isnan(t) || isnan(h)) { Serial.println(Failed to read from DHT sensor); return; } String payload String({\deviceId\:\esp8266_01\,\temperature\:) String(t, 1) String(,\humidity\:) String(h, 1) String(,\timestamp\:) String(millis()) String(}); client.publish(sensor/temp_humidity, payload.c_str()); delay(5000); } void reconnect() { while (!client.connected()) { if (client.connect(ESP8266Client)) { Serial.println(MQTT connected); } else { delay(2000); } } }这段代码里有两个容易翻车的点。第一个是millis()在这里返回的是设备上电以来的毫秒数而不是真实时间戳。如果采集端没有做 NTP 校时APP 端拿到的时间戳语义就不对。我一般会改用time(nullptr) * 1000前提是 ESP8266 通过configTime获取了网络时间。第二个是String(t, 1)表示保留一位小数这个精度用于室温监控足够但如果你想做更精细的实验室环境监测建议改成String(t, 2)。3.2 Android 端 MQTT 客户端连接、订阅、回调Android 端的核心类是MqttHelper它封装了 Paho 的MqttAsyncClient。这一层做的事比较多建立连接参数、设置回调、订阅主题以及断线重连。下面是精简后的连接方法public void connect(String brokerUrl, String clientId) { MqttConnectOptions options new MqttConnectOptions(); options.setCleanSession(false); options.setConnectionTimeout(10); options.setKeepAliveInterval(30); options.setAutomaticReconnect(true); mqttClient new MqttAsyncClient(brokerUrl, clientId, new MemoryPersistence()); mqttClient.setCallback(new MqttCallback() { Override public void messageArrived(String topic, MqttMessage message) { String json new String(message.getPayload()); processSensorData(json); } Override public void connectionLost(Throwable cause) { Log.e(TAG, connection lost: cause.getMessage()); } Override public void deliveryComplete(IMqttDeliveryToken token) {} }); try { mqttClient.connect(options); mqttClient.subscribe(sensor/temp_humidity, 1); } catch (MqttException e) { e.printStackTrace(); } }有几个参数值得展开。setCleanSession(false)表示客户端断开重连后Broker 会保留该 session 的订阅关系否则重连后还要重新走一遍订阅流程。setKeepAliveInterval(30)表示客户端每 30 秒发送一次 PINGREQ如果 Broker 在 90 秒内收不到任何报文就会踢掉这个连接。这些参数在弱网环境下特别重要我见过太多人默认参数不改结果手机锁屏一会儿就掉线实际上把 KeepAlive 调短、打开自动重连就能解决。3.3 温湿度数据解析与本地缓存收到消息后不能直接往界面丢因为 MQTT 消息是字节数组要先转字符串再解析 JSON。源码里用org.json.JSONObject解析没有引入 Gson目的是减少依赖。解析的同时做了两类处理一是时间戳去重二是写本地缓存。private void processSensorData(String json) { try { JSONObject obj new JSONObject(json); long ts obj.optLong(timestamp); if (ts lastTimestamp) { Log.d(TAG, stale message, drop it); return; } lastTimestamp ts; SensorData data new SensorData(); data.deviceId obj.getString(deviceId); data.temperature obj.getDouble(temperature); data.humidity obj.getDouble(humidity); data.timestamp ts; saveToCache(data); runOnUiThread(() - updateUI(data)); } catch (JSONException e) { Log.e(TAG, parse error: json, e); } }lastTimestamp是一个成员变量起到去重的作用。注意这里用的是而不是因为如果两条消息时间戳相同但数值不同说明发生了重复投递后一条不应该被展示。保存缓存用的是 DataStore相对 SharedPreferences 来说更现代支持异步读写不会阻塞主线程。如果你拿到的源码里用的是 SharedPreferences也不必惊讶功能上是等价的。3.4 界面更新列表展示与实时曲线UI 部分由两个界面组成主界面显示当前温湿度和最近 20 条记录另一个界面用自定义 View 绘制实时曲线。列表用的是 RecyclerView因为数据量不大没有做分页直接全量刷新。关键代码在TrendChartView里它接收一个ListSensorData在onDraw方法中绘制折线Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (dataList.size() 2) return; float maxTemp 40f, minTemp -10f; float chartHeight getHeight() - paddingTop - paddingBottom; float chartWidth getWidth() - paddingLeft - paddingRight; Paint linePaint new Paint(); linePaint.setStrokeWidth(6f); linePaint.setAntiAlias(true); Path path new Path(); for (int i 0; i dataList.size(); i) { float x paddingLeft i * chartWidth / (dataList.size() - 1); float normalized (float) (dataList.get(i).temperature - minTemp) / (maxTemp - minTemp); float y paddingTop chartHeight * (1 - normalized); if (i 0) path.moveTo(x, y); else path.lineTo(x, y); } canvas.drawPath(path, linePaint); }这里maxTemp和minTemp写死是 40 和 -10如果用于工业环境需要改成动态计算数据列表中的最大值和最小值否则曲线会被压扁。我在二次开发章节会给出更合理的做法。曲线 View 本身没有处理点击事件想看具体数值还是要靠列表。4. 从零跑起来工程导入、Broker 配置与整机联调代码看得再多不如自己跑一遍。这一章我把从拿到源码到看到数据的全过程捋一遍包括环境准备和最常见的连接失败排查。4.1 准备工具和依赖跑通这套工程需要准备以下环境Android Studio版本建议 2023.1 以上用来打开 APP 工程JDK 11 或 JDK 17MQTT Broker本地调试装一个 Mosquitto 就行Windows 和 Linux 都有安装包一块 ESP8266 或 ESP32 开发板一个 DHT22 模块串口调试工具用来刷硬件端固件第一次打开工程时Gradle 会同步下载依赖。如果网络不好Paho 库可能拉不下来我一般会把仓库源换成国内镜像。如果源码里自带 Gradle wrapper直接用gradlew命令构建更稳妥避免本机 Gradle 版本不一致导致翻车。4.2 导入工程并修改连接参数App 端的连接参数集中在Config.java里通常是这样的结构public class Config { public static final String MQTT_BROKER_URL tcp://192.168.1.100:1883; public static final String MQTT_CLIENT_ID android_app; public static final String MQTT_TOPIC sensor/temp_humidity; }你需要把MQTT_BROKER_URL改成你电脑或云服务器的 IP。注意如果是模拟器调试不能用localhost或127.0.0.1因为那是指模拟器自身要用10.0.2.2才能映射到宿主机。如果你用真机必须确保手机和 Broker 在同一局域网下并且防火墙放行了 1883 端口。硬件端代码里的mqttServer和 WiFi 账号密码也要改。这里有一个小细节ESP8266 的reconnect()函数里client.connect(ESP8266Client)这个 clientId 如果和别的设备重复后连的会把先连的踢下线。如果你同时跑了两个采集板记得改成不同的 ID。4.3 联调验证先看日志再看曲线一切配置完成后烧录硬件端然后按顺序做三件事在电脑上启动 Mosquitto并开启日志输出打开手机 APP观察日志中是否出现Mqtt connected和订阅成功的提示给传感器哈一口气观察温度值是否短暂上升并出现在 APP 列表里如果列表一直没有数据先不要急着改代码。直接在电脑上用 Mosquitto 订阅同一主题mosquitto_sub -h localhost -t sensor/temp_humidity -v如果这个命令能看到 JSON 数据说明链路中采集端和 Broker 都正常问题一定出在手机端连接参数或订阅逻辑上。如果连这里都没有数据那问题就出在硬件端去检查串口输出看看是不是 DHT22 读到了nan。这套排查思路能帮你避开 80% 的联调问题。5. 避坑与排查温湿度数据不准、掉线、乱码的常见原因这个项目的代码本身不复杂但跑起来之后的小问题非常多。我根据自己的实际测试和学员反馈总结了下面四个高频坑每条都是“现象 → 原因 → 解决”的结构。5.1 传感器读数为 0 或固定值现象APP 上温度湿度常年显示 0或者卡在一个不可能的值上。原因DHT22 的杜邦线接触不良或者数据线过长导致信号衰减。DHT22 对时序要求比较高尤其是数据线超过 20 厘米时读到的数据容易出现全 0 或者固定值。解决先看串口监视器确认硬件端是否打印Failed to read from DHT sensor。如果是把传感器拔下来重新插紧缩短数据线长度。还有一种情况是传感器型号搞错了代码里定义的是DHTTYPE DHT22但你实际接的是 DHT11那读出来的值当然不对。换成 DHT11 对应的宏定义即可。5.2 APP 收不到消息但服务端正常现象用mosquitto_sub能收到数据但手机 APP 就是没反应。原因最常见的是客户端 id 冲突或订阅主题不一致。另一个容易被忽略的原因是setCleanSession(false)与 Broker 上的残留 session 冲突导致客户端连接后没有重新订阅。解决在代码里临时把setCleanSession(false)改成true如果数据能收到说明是 session 的问题。彻底解决的方法是 Broker 端踢掉旧 session或者在代码中每次连接成功后都显式调用subscribe不要依赖 session 恢复。5.3 界面显示 NaN 或乱码现象列表里温度显示NaN湿度显示乱码或者明显的符号错乱。原因硬件端发送的 JSON 里temperature字段为非数字通常是 DHT 读取失败后强转字符串造成的。另外数据精度格式不一致也会导致解析异常。解决硬件端在发布前先判断isnan(t)或isnan(h)如果非法直接跳过这次发布。APP 端解析时不要用getDouble硬取先用isNaN判断一下。乱码问题则要考虑MqttMessage默认是否按 UTF-8 解码如果硬件端发送的是 UTF-8 的 JSON一般不会乱码如果出现乱码就在new String(message.getPayload())里明确写StandardCharsets.UTF_8。5.4 时间长了自动掉线现象APP 刚启动时一切正常放了一个小时之后再打开发现已经断线且没有自动重连。原因手机系统在后台挂起时网络状态变化导致 TCP 连接被系统销毁但 Paho 客户端的自动重连没有触发或者触发了但因为网络还没就绪而失败。解决在 Activity 的onResume方法里检查MqttAsyncClient.isConnected()如果为 false 就手动重连。我一般会在重连前先关闭旧客户端再重新创建连接避免连接句柄失效的问题。另外不要用深度定制的省电模式这会延迟推送接收。6. 二次开发增加数据存储、告警推送与多设备管理的思路跑通之后这个项目只能算“能用”距离“好用”还有一段路。这里我讲三个最实用的改造方向每个都给出可落地的实现思路。6.1 替换数据源与扩展传感器如果你不想用 DHT22想换成 SHT30 或 BME280只需要改硬件端的读取代码和初始化部分APP 端的数据结构可以完全不变。因为通信协议里的temperature和humidity字段是通用的。如果你要加一个 PM2.5 传感器就需要在 JSON 里增加一个字段并在 APP 端解析时同步修改。建议在SensorData类里统一维护字段避免在 Activity 里直接散落getJSONObject的代码。6.2 把数据落到 SQLite 并导出 CSV当前源码用的是 DataStore 缓存它适合保存最近几十条记录但没法做历史查询。改造方案是引入 Room 数据库建一张sensor_record表字段包括device_id、temperature、humidity、timestamp。在processSensorData里每次收到消息就异步写入一条记录。查询界面用LiveData监听数据库变化自动刷新曲线。导出 CSV 可以用一个简单的FileWriter把查询结果一行行写入文件然后用系统分享发出去。这个功能在做数据汇报时非常实用。6.3 增加阈值告警与通知目前的界面只负责展示没有用户主动告警。改造时可以在设置界面加两个输入框温度上限、湿度下限。保存后用 SharedPreferences 或 DataStore 存起来每次数据刷新时在processSensorData里做一次判断if (data.temperature tempThreshold) { sendNotification(温度告警, 当前温度 data.temperature °C); } if (data.humidity humidityThreshold) { sendNotification(湿度告警, 当前湿度 data.humidity %); }sendNotification用 NotificationManager 创建一条通知即可。需要注意 Android 13 以上要求POST_NOTIFICATIONS运行时权限记得在 Activity 里申请否则你会发现通知发不出来这个坑我在自己的项目里踩过几次。告警逻辑放到processSensorData里而不是放到适配器里这是为了确保不管界面是否在前台判断逻辑都能执行。这套源码最让我满意的地方就是它的协议设计和模块边界硬件端只负责采集和上报APP 端只负责解析和展示Broker 在中间做中转。这种职责划分让二次开发变得很轻松你可以在不碰其他模块的情况下替换任何一环。从那以后我每次做物联网 Demo都强制自己先把主题和 Payload 设计清楚再开始写代码事实证明这能省下大量联调时擦屁股的时间。希望这套温湿度监控 APP 的拆解思路能帮到你也欢迎你在这个基础上做出自己的版本。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。