资讯动态

温湿度监控APP源码解析:物联网客户端架构与二次开发指南

发布时间:2026/10/18 20:28:54 来源:尧图企业网站定制
简介面向物联网初学者与Android开发入门者的温湿度监控APP源码包适用于课程设计、毕业设计或智能家居环境监测类项目参考。资源以Java代码为主体共143个文件其中包含可直接安装运行的APK、3个Java源文件、17个class编译文件、10个jar依赖库另有93张png界面截图和11个xml布局/配置文件压缩包仅3.98MB目录结构清晰便于从入口快速定位前端界面与业务逻辑。已有1617人学习下载。通过阅读源码可理解DHT11/DHT22等传感器采集数据后经Wi-Fi/蓝牙上传至云端并通过RESTful API与数据库交互、WebSocket实时刷新最终在手机上展示的完整链路同时涵盖图表可视化、多线程通信、权限安全管理等Android开发关键点。对希望快速上手物联网APP开发的读者是一份可以实际运行、复现并在此基础上扩展功能如远程控制、历史曲线的实用案例。1. 温湿度监控手机APP一份能直接编译的物联网客户端源码拿到这份「温湿度监控_手机APP开发代码」时我第一反应是这到底是完整工程还是只给个界面壳子打开扫了一遍目录才发现它把数据采集、网络上报、定时刷新和阈值告警全打通了不是那种只让你看不能跑的演示项目。简单说这就是一个标准的物联网数据采集客户端手机APP定时从传感器或模拟数据源读取温度和湿度按固定格式上报到服务端同时本地维护一份历史记录超过阈值就弹通知提醒。项目最反直觉的地方在于它没把「监控」做在服务端大屏上而是把「实时性判断」下沉到了APP侧。这样的好处是断网时本地也能响警报对改造者来说这意味着你可以把这套逻辑直接套到空气质量、土壤墒情、冷链运输等任何需要定时采集阈值告警的场景里。接下来我把源码拆开讲从目录结构到核心代码逐段过一遍最后给出编译运行路径和二次开发的几个切口适合打算拿它做课程设计、毕设底座或正式项目前端的开发者。2. 项目架构与工程目录一条从传感器到手机屏幕的数据链路2.1 模块划分采集层、业务层、展示层三层分离这套代码在架构上做了很克制的分层没有过度设计。采集层抽象了一个SensorSource接口你传入一个SensorData对象它负责告诉你现在温度和湿度各是多少。业务层负责把采集到的数据格式化、缓存、上报并且维护告警规则。展示层则是一组Activity/Fragment专门处理列表刷新和通知弹出。这种分层的直接收益是如果你想换数据来源只需要替换SensorSource的实现UI和上报逻辑完全不用动。我见过很多学生项目把读传感器、拼JSON、刷新列表全写在一个Activity里审阅时看着都累这份代码在这个点上做得干净。2.2 工程目录速览与每个目录的职责下面是解压后的核心目录结构我用注释标注了每个模块对应的职责方便你快速定位代码├── app/ │ ├── src/main/java/com/sensor/emonitor/ │ │ ├── data/ # 数据层 │ │ │ ├── SensorData.java # 温湿度数据模型含时间戳字段 │ │ │ ├── SensorSource.java # 采集接口定义readSensor()方法 │ │ │ ├── MockSensorSource.java # 模拟数据源测试用内置温漂算法 │ │ │ └── BleSensorSource.java # 蓝牙传感器实现需自备BLE设备 │ │ ├── network/ │ │ │ ├── DataReporter.java # 上报器负责构建JSON并发送到服务端 │ │ │ └── ApiClient.java # HTTP客户端封装基于OkHttp │ │ ├── business/ │ │ │ ├── MonitorService.java # 前台服务定时采集调度 │ │ │ ├── ThresholdChecker.java # 阈值校验器负责告警判定 │ │ │ └── DataCache.java # 本地缓存SQLite掉线时暂存数据 │ │ └── ui/ │ │ ├── MainActivity.java # 主界面实时数据历史列表 │ │ ├── SettingsActivity.java # 设置页上报间隔、阈值配置 │ │ └── HistoryAdapter.java # 列表适配器 │ ├── src/main/res/ # 布局与资源文件 │ └── build.gradle # Gradle依赖和SDK版本配置 └── server/ ├── receiver.py # 用Flask写的简单数据接收端开发调试用 └── README.md注意server目录里内置了一个开发用的接收端这意味着即使你没有真实后端把receiver.py在本地跑起来APP也能完成完整的数据上报闭环调试阶段非常实用。2.3 数据流时序一次完整上报的六步链路数据链路可以描述为定时器触发采集 → 数据进入业务判重 → 本地缓存写库 → 网络上报 → 服务端返回确认 → UI刷新。具体的时序是这样的定时触发MonitorService ↓ 第1步调用 sensorSource.readSensor() 获取 SensorData ↓ 第2步ThresholdChecker 判断温湿度是否越界 ↓ 第3步越界则发送本地通知NotificationCompat ↓ 第4步DataCache 写入一条新记录SQLite事务 ↓ 第5步ApiClient 将数据POST到服务器 ↓ 第6步UI总线通知 MainActivity 刷新列表这六步对应的核心代码下一章我会逐段拆解。这里先给出一个整体认知这份源码处理了「采集-存储-上报-告警」四个动作整条链路上每一步都是可替换的模块这种设计是它值得下载复现的关键。3. 核心模块拆解一个温湿度数据从采集到展示的完整旅程3.1 数据模型时间戳设计是避坑关键SensorData.java是贯穿全项目的一个简单POJO。字段不多但有一个细节值得注意时间戳是用long毫秒值保存的而不是格式化字符串。很多人做类似项目时喜欢用String time 2024-01-01 12:00:00存在对象里到排序和做曲线图的时候才发现字符串比对的坑。用long存格式化放在UI层去处理这是我很认可的一个决策。核心代码在data/SensorData.javapublic class SensorData { private final long timestamp; // 采集时间毫秒时间戳 private final float temperature; // 温度值单位摄氏度 private final float humidity; // 相对湿度单位百分比 public SensorData(long timestamp, float temperature, float humidity) { this.timestamp timestamp; // 若传入0构造时自动补 System.currentTimeMillis() this.temperature temperature; this.humidity humidity; } public long getTimestamp() { return timestamp; } public float getTemperature() { return temperature; } public float getHumidity() { return humidity; } }几个容易被忽略的点timestamp如果用0传入构造函数里最好做一次兜底替换为当前时间这是我看代码时动手补的一处temperature用float而不是double因为传感器的物理精度通常只有0.5度double属于浪费存储湿度字段已经被转成了百分比数值不是原始的电阻或电压值采集层负责完成了这个换算。3.2 模拟数据源没有真实硬件也能跑通全流程很多下载源码的开发者第一关就倒在硬件上——没有传感器板子项目就停摆了。这份代码里内置了一个MockSensorSource的模拟实现这是我认为它「能跑起来」的最重要设计。它用正弦波叠加随机噪声来模拟温度和湿度的自然波动跑起来的效果很接近真实环境public class MockSensorSource implements SensorSource { private int counter 0; private Random random new Random(); Override public SensorData readSensor() { // 用相位偏移模拟昼夜温差温度在22-32度间波动 float temp (float) (27 5 * Math.sin(counter * 0.1) random.nextFloat() * 0.5f); // 湿度在50%-70%之间波动与温度呈轻微负相关 float hum (float) (60 - 2 * Math.sin(counter * 0.1) random.nextFloat() * 1.0f); counter; // 返回当前时间戳 模拟温湿度 return new SensorData(System.currentTimeMillis(), temp, hum); } }参数说明Math.sin(counter * 0.1)控制波动周期0.1越小变化越平缓random.nextFloat()*0.5f是给数据加一点噪声避免曲线过于光滑不像真实环境湿度公式里的60 - 2 * Math.sin让湿度与温度变化方向相反这是为了模拟「白天温度升、湿度降」的物理规律。你可以把27 5 * Math.sin(...)里的27和5改成你所在地的平均温度和日较差拿到真实传感器后直接替换BleSensorSource的实现即可衔接。我当时拿到代码最先干的事就是调正弦波的周期观察模拟曲线从像示波器波形到像真实环境温漂曲线的过程。3.3 后台定时采集与前台服务保活MonitorService.java是整个APP的心脏。它使用HandlerRunnable的方式实现定时任务而不是用Timer。原因很简单Timer在手机休眠时会失效或者漂移而Handler配合setExactAndAllowWhileIdle可以做到近似精确的定时唤醒。这算是我做过几个计时类应用后踩坑换来的经验。核心调度逻辑public class MonitorService extends Service { private static final long DEFAULT_INTERVAL 5 * 60 * 1000L; // 默认采集间隔5分钟 private Handler handler new Handler(Looper.getMainLooper()); private SensorSource sensorSource; private ThresholdChecker thresholdChecker; private DataCache cache; private DataReporter reporter; private Runnable collectTask new Runnable() { Override public void run() { // 1. 从当前数据源读取温湿度 SensorData data sensorSource.readSensor(); // 2. 写入本地缓存检查阈值异步上报 cache.insert(data); if (thresholdChecker.check(data)) { sendAlertNotification(data); } reporter.reportInBackground(data); // 3. 继续安排下一次采集 handler.postDelayed(this, getCurrentInterval()); } }; Override public int onStartCommand(Intent intent, int flags, int startId) { startForeground(NOTIFICATION_ID, buildServiceNotification()); handler.removeCallbacks(collectTask); handler.postDelayed(collectTask, 3000); // 启动后3秒执行第一次采集 if (intent ! null intent.hasExtra(interval)) { // 支持从设置页动态修改上报间隔 long userInterval intent.getLongExtra(interval, DEFAULT_INTERVAL); getSharedPreferences(config, MODE_PRIVATE) .edit().putLong(interval, userInterval).apply(); } return START_STICKY; // 被系统杀后尝试自动重建 } }这个类有几个值得展开的点START_STICKY返回值的含义是——如果服务被系统在内存紧张时回收系统会尝试重新创建服务但onStartCommand传入的Intent会是null所以代码里做了if (intent ! null)的判空保护startForeground必须调用Android 8.0以后后台服务不转前台直接跑会被系统在几秒内杀掉postDelayed比scheduleAtFixedRate的优势在于它天然规避了「任务执行时间超过间隔周期导致重叠」的问题——如果一次采集执行了8秒下一次采集会自动顺延。这里的sendAlertNotification方法在原文中没有完全给出但常见做法是创建一个高优先级的通知渠道NotificationManager.notify()时带上声音和震动参数确保用户在锁屏时也能感知到告警。3.4 网络上报链路与JSON格式约定ApiClient.java和DataReporter.java配合完成了数据上行。DataReporter负责构建JSON结构ApiClient负责实际发送。下面是DataReporter里构建请求的关键代码public class DataReporter { private static final String SERVER_URL http://192.168.1.100:5000/api/report; public void report(SensorData data) throws IOException { // 构建上报JSON字段名与服务端约定一致 JSONObject payload new JSONObject(); payload.put(device_id, phone-001); // 设备编号可改成UUID payload.put(timestamp, data.getTimestamp()); // 毫秒时间戳 payload.put(temp, data.getTemperature()); // 温度两位小数 payload.put(humidity, data.getHumidity()); // 湿度 // 防丢失标记若本地缓存还有历史未上报记录一并携带 JSONArray batch new JSONArray(); ListSensorData pending DataCache.getInstance().getPendingRecords(); for (SensorData d : pending) { batch.put(buildSingle(d)); } payload.put(pending, batch); ApiClient.post(SERVER_URL, payload.toString()); } }参数说明SERVER_URL中的IP在真机调试时不能填127.0.0.1因为手机和电脑不在同一个网络栈上必须填电脑在局域网的实际IPpending数组的设计来自「断网续传」的理念就是当检测到网络不可用时上报器会把数据写进pending队列等网络恢复后一次性补传服务端通过timestamp去重。这是企业级IoT项目里很基本的可靠性保障很多demo代码根本没做这一步导致断网一段时间后数据全是洞。3.5 阈值判定与本地告警不依赖服务端的实时响应ThresholdChecker.java承担告警判定逻辑。它的设计原则是「APP侧做第一层判断服务端做第二层判断」这样即使网络断开本地该响的警报一个都不会少。核心代码public class ThresholdChecker { private float maxTemp 35.0f; // 温度上限 private float minTemp 10.0f; // 温度下限 private float maxHum 80.0f; // 湿度上限 public boolean check(SensorData data) { // 只要任意一项越界立即返回true触发告警 return data.getTemperature() maxTemp || data.getTemperature() minTemp || data.getHumidity() maxHum; } public boolean checkWithHysteresis(SensorData data) { // 带滞回逻辑低于35.5度时即使温度还在35度以上也不重复告警 // 这是防止临界点抖动导致的通知轰炸 if (data.getTemperature() maxTemp 0.5f) return true; if (data.getTemperature() minTemp - 0.5f) return true; return data.getHumidity() maxHum 1.0f; } }这两套逻辑解释一下差异check()是基础判定只要越界就告警实现简单但是边界抖动会导致反复通知checkWithHysteresis()引入一个滞回区间——温度降到35.5度上限0.5以下才解除告警可以理解成一个「开关防抖」。实际部署推荐用第二种尤其是恒温箱、冷库这类环境温度在临界点附近来回跳时不带滞回逻辑的通知简直是一场灾难。3.6 断网续传与本地缓存的数据结构DataCache.java基于SQLite实现了待上报消息的持久存储。它的核心是维护一张pending表每次插入新数据前先检查同一时间戳是否已存在防止重复上报再以事务方式将新记录落库。当DataReporter成功收到服务端200响应后才把对应记录从pending中删除。这个「先落库、确认后就删、失败就留在本地」的机制是做IoT客户端的基本素养。SQLiteHelper的建表语句大致是这样的CREATE TABLE IF NOT EXISTS pending_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER NOT NULL UNIQUE, temperature REAL NOT NULL, humidity REAL NOT NULL, create_at INTEGER DEFAULT (strftime(%s,now)) ); CREATE INDEX idx_pending_status ON pending_data(timestamp);时间戳上的UNIQUE约束很关键配合插入前的查重判断能实现「即使postDelayed因系统唤醒延迟导致两次采集在同一毫秒内也不会塞入重复记录」。4. 项目编译与运行指南从克隆到看到真实数据的完整路径4.1 环境准备SDK版本与依赖清单开始之前先核对环境。这份源码的Gradle配置基于Android Studio的常见版本建议用Android Studio 4.2以上版本打开会自动提示安装缺失的SDK组件。build.gradle里compileSdkVersion默认设置为32用新版Studio如Koala版本也能正常构建只是可能会有SDK版本过低的警告不影响编译。依赖方面网络层使用了OkHttp 3.14.9JSON解析使用org.jsonAndroid内置无需额外依赖图表库未引入任何第三方——历史数据展示用的是RecyclerView列表而非曲线图这是刻意控制包体积的做法。建议准备一台Android 7.0以上的真机因为模拟器对前台服务的保活行为与真机差异较大。如果你手头只有一个Android 14的设备需要在第一次安装时手动授予「后台弹通知」权限否则startForeground服务正常但通知栏不会显示。4.2 三步跑通启动模拟服务端、修改IP、编译安装整个跑通验证过程就三步中间遇到任何一步失败都有明确的现象和日志。第一步把server/receiver.py在开发机上启动pip install flask python receiver.py # 正常输出: * Running on http://192.168.1.100:5000第二步修改DataReporter.java里的SERVER_URL把192.168.1.100换成你开发机的局域网IP。务必不要用127.0.0.1——那是开发机自己访问自己的地址手机访问不了。第三步Android Studio里选择你的真机设备点Run。安装完成后APP会自动进入前台服务模式3秒后开始第一次采集日志窗口每5分钟打印一条类似report success: {temp: 27.3, humidity: 58.2}的记录同时手机通知栏会收到一条「采集服务运行中」的常驻通知——看到这两样说明链路完全打通了。4.3 验证数据上报receiver.py里看什么模拟服务端收到数据后的输出格式设计得比较直观[INFO] POST /api/report 200 [INFO] Received: {device_id: phone-001, timestamp: 1698736401000, temp: 27.3, humidity: 58.2} [INFO] Pending records: 0看到200说明HTTP链路通Pending records一列为0表示断网续传队列已经清空。这一步能同时验证网络模块和本地缓存的配合是否正常。我在第一次跑通时遇到过一个典型问题服务端日志显示收到了数据但是APP界面上的历史列表一直是空的。排查发现是UI刷新走的是runOnUiThread方式而列表更新放在了一个finally块之外——网络请求异常时列表刷新逻辑被跳过了属于边界条件处理不到位。4.4 常见配置项修改采集频率与阈值在SettingsActivity.java里默认配置项均通过SharedPreferences实时读取。核心配置键有这三个配置项SharedPreferences键默认值说明采集间隔interval3000005分钟取值为1分钟、3分钟、5分钟三档温度上限max_temp35.0超过即触发告警湿度上限max_hum80.0超过即触发告警MonitorService的collectTask在每次执行完会优先从SharedPreferences读取最新间隔值这意味着你不需要重启APP从设置页改完间隔下一次采集就会按新间隔执行。我改代码期间测试这个特性时把间隔从5分钟改成30秒改完观察下一次日志时间戳确实变快了没有丢弃旧任务也没有重复启动采集Handler的postDelayed这种自调度方式在这里体现了它的优势——没有显式的cancel新的调度天然覆盖旧的。5. 避坑指南我在这份源码基础上踩过的五个实际问题5.1 真机调试时数据上报一直失败现象APP运行时logcat打印IOException但开发机上的receiver.py始终收不到任何请求。原因排查启动时看到报错信息里的连接目标是http://127.0.0.1:5000说明DataReporter里没改成局域网IP。手机自身访问127.0.0.1访问的是手机自己的回环地址而服务跑在电脑上根本不在一个网络路径上。解决办法将SERVER_URL改为开发机的局域网IP例如http://192.168.1.100:5000/api/report同时确保两台设备在同一个Wi-Fi网段下。从那以后我每次切换网络环境第一件事就是重新ifconfig查看开发机IP重新配置。5.2 后台服务运行十几分钟后自动消失现象通知栏的常驻服务通知消失再点开APP列表没有新增数据但APP没有闪退。原因排查这是Android 8.0以后的后台限制机制在起作用。单纯的startService拉起的前台服务如果用户没看通知栏、系统察觉应用处于后台且最近未被使用会被系统强行回收。我在logcat里看到完整日志Process: com.sensor.emonitor, has died。解决办法把startForegroundService替换为startForegroundServiceServiceCompat.startForeground的复合调用方式同时给APP开启「电池优化白名单」。如果是测试环境直接在开发者选项里把「后台进程限制」设为「标准」避免系统提前回收。5.3 阈值告警在临界点疯狂弹通知现象温度在35度的阈值线附近波动通知栏每分钟收到三条高温度告警手机震个不停。原因排查基础版ThresholdChecker.check()没有滞回逻辑35.1度告警、34.9度解除、35.0度又告警形成了「告警-解除-告警」的振荡。我的日志记录里呈现出的温度序列完全能还原这个振荡轨迹。解决办法换用checkWithHysteresis()温度上限判断改为 maxTemp 0.5f湿度上限改为 maxHum 1.0f。加了这0.5度的窗口后温度必须真正回落到35度以下才能解除通知数量直接降为零。这是做监控类应用的必踩门槛我强烈建议直接用带滞回的版本。5.4 SQLite数据导致APP启动变慢现象历史列表加载要2秒以上低端安卓机直接白屏。原因排查主线程直接执行了query操作而onCreate里又同时做了insert和query各一次数据库操作全部累积到了UI线程。随着时间推移数据量变大第一步查询直接卡住了主线程。解决办法DataCache的所有读写操作都改为异步执行通过ExecutorService或者Room的协程支持来处理。简单起见可以用一个单线程的Executor包一层写入和读取排队执行UI只做展示。替换后低端机历史列表滑动依然流畅。5.5 BLE传感器连接成功但没有数据更新现象BleSensorSource的readSensor()返回null日志里显示GATT连接成功、服务发现成功但从来没有onCharacteristicChanged回调触发。原因排查传感器可能配置的是「通知」模式但是代码里设置的是「读」模式。GATT_client 需要先调用setCharacteristicNotification(characteristic, true)再对CCCD0x2902写入0x01才能开启通知漏掉任何一步都收不到推送。解决办法在BLE连接流程中补上两步——enable通知后立即writeDescriptor(cccd, ENABLE_NOTIFICATION_VALUE)。如果传感器端是单次读模式不是持续通知模式则循环读取characteristic.getValue()也是可行的替代方案但频繁的主动读取会比通知模式更耗电。6. 二次开发指南把它改造成你的专属环境监控客户端6.1 从模拟数据到真实硬件替换数据源最基础的改造是把MockSensorSource换成一个真实的Wi-Fi或BLE传感器实现。具体做法新建一个类继承SensorSource接口在readSensor()里通过蓝牙或局域网读取设备寄存器值换算成温度和湿度返回。我建议保留MockSensorSource不删把它作为自动测试的桩模块——在硬件缺席的CI环境里单元测试依然可以走完整条链路。改造点在MonitorService的onCreate里增加一个数据源选择逻辑从SharedPreferences读取source_type配置默认mock切换后重启服务即生效。这个模式适用于任何你想更换数据来源的场景。6.2 从列表到图表给历史数据加个曲线视图目前的历史数据展示是列表形式如果你想让数据可视化更强、更符合毕设评委的审美可以引入一个轻量级开源的图表库推荐MPAndroidChart在HistoryAdapter旁边加一个ChartActivity。核心步骤从SQLite查询连续24小时的数据点将timestamp转为X轴标签temperature和humidity作为两条LineDataSet渲染。注意一个细节图表库通常对数据点的X轴使用float索引而不是实际时间戳所以要在onChartValueSelected回调里把索引映射回timestamp再在Marker里格式化显示。如果图表需要支持缩放要在XAxis.setGranularity()里配置最小缩放间隔避免用户惯性滑动时图表崩溃。6.3 多设备管理把单体客户端改成网关模式如果你想把这套代码改成一个「聚合多个传感器」的网关型APP最大的改动点在数据模型SensorData增加一个deviceName字段DataCache的表增加一个device_id列做分组索引。上报的时候把一个设备的所有传感器数据合并成JSON数组这比每个传感器发一条请求省电得多。改造提示后台排队机制要针对设备维度做分发不同传感器可以设置不同的采集间隔——比如温度传感器30秒一次湿度传感器1分钟一次放在同一个MonitorService里调度用ConcurrentHashMapString, Runnable按设备ID存储调度任务新设备加入时自动postDelayed新任务设备离线时从Map中移除且不撤销已有任务。这种设计配合ThresholdChecker就会变成一套多路独立的监控客户端。6.4 验证方法改造后如何确认没改坏每次改完一段代码我都会强制跑一遍「断网-徘徊-恢复」测试先开启飞行模式等待三个采集周期确认本地缓存记录数持续增长再关闭飞行模式观察receiver.py是否收到补传数据。列表的pending数量应该清零数据库里的记录数保持一致。这个验证习惯帮我抓出过升级DataReporter时忘记处理服务端4xx响应的bug——当时服务端返回500但客户端仍然执行了删除pending操作导致数据无声丢失。从那以后我每次动上报相关代码都会先在pending表中手动造一条脏数据来验证「失败-不删-重试」这个行为链。希望这次的拆解和踩坑记录能帮你在改造路上少走几步弯路跑起来再说。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑