☰
基于物联网的作业场所粉尘危害监测预警系统设计与实现
2026/10/9 13:13:28 网站建设 项目流程

简介:基于物联网的作业场所粉尘危害监测预警系统是一套面向物联网、计算机、自动化等专业课程设计或毕业设计的完整项目资源,包含系统源码、前端页面与文档说明。项目立足煤矿、建筑工地等典型作业场所的呼吸性粉尘监测需求,运用物联网感知、数据库与软件工程方法,实现粉尘浓度的实时采集、传输和预警,能够帮助学习者掌握物联网系统设计的基本流程与工程开发技能。资源包共952个文件、约5.06MB,其中图片素材与前端代码占据主体,包括453个png图片、276个js脚本、82个css样式和60个html页面,另有json配置与md说明文档,便于直接部署和二次修改。目前已有181人学习,代码均测试运行成功,答辩平均分达到96分。下载后可结合文档说明快速理解系统架构与模块分工,在现有基础上扩展功能或调整页面,也可作为同类课程设计、毕业设计的参考模板。

1. 基于物联网的作业场所粉尘危害监测预警系统:它到底解决了什么

这套资源是一套完整的基于物联网的作业场所粉尘危害监测预警系统课程设计包,源代码和文档都在里面。拆包后的第一印象:它没有把粉尘监测做成只会显示数字的大屏,而是围绕呼吸性粉尘浓度把采集、传输、入库、预警、历史追溯串成了一条可闭环验证的链路。煤矿、建筑工地、生产车间的粉尘一旦超标,系统能按阈值触发分级预警,而不是等人走到大屏前才发现问题。

如果你是物联网、计科、自动化相关专业的学生,或者刚入门 IoT 开发的从业者,这份资源可以直接拿来当课设/毕设骨架:先照文档跑通,再改阈值、换传感器、加页面。下面按“下载后第一周能跑通”的标准拆一遍。

2. 物联网粉尘监测的架构选型:为什么先定数据链路再写代码

2.1 粉尘监测为什么不建议做成单机设备

如果只是买一台粉尘仪放现场,问题很明显:数据只在设备本地,人在办公室看不到;报警靠设备自身蜂鸣器,超过阈值只能吓跑附近的人,无法通知远端值班员;历史浓度没有数据库,后面要做职业健康评估时根本拿不出趋势。既然题目是“基于物联网”,核心就是把现场浓度变成一条线上可流动的数据,让粉尘仪不再是信息孤岛。这也是我拆这类项目时习惯先画数据链路、再动代码的原因。

一套课程设计如果一开始就把所有细节铺开,很容易卡在传感器接线或者页面样式上。正确顺序是:先确认数据从哪来、经谁转发、落到哪个表、最终被哪个页面消费。链路通了,后面的阈值判断和展示都是增量工作。

2.2 感知层、传输层、平台层、应用层的选型

常见做法是把系统拆成四层,每一层只解决一个明确问题。下面这张选型表基本覆盖了同类课设最常见的实现方式:

层次职责本资源常见实现选型理由
感知层采集粉尘浓度、温湿度粉尘传感器(例如 GP2Y1010 或 SDS011)加温湿度传感器成本低、有现成驱动,课程设计够用
传输层把采集值上传到服务端STM32/ESP8266 通过 WiFi 或串口转 4G,走 MQTT 协议MQTT 报文轻量,后端订阅主题就能解耦设备数量
平台层接收、解码、入库、判断阈值Spring Boot / SSM 后端,MyBatis 操作 MySQL生态成熟,接口写起来快,答辩容易解释
应用层实时曲线、设备状态、预警记录Layui 静态页面 + Ajax/WebSocket资源里有 layui.css、skin.css 这些静态文件,基本可以判断前端走的是 Layui 这一套

这四层不是各自独立写死的。我在实际项目里非常看重“接口契约”:设备端只负责把 JSON 报文发出来,后端只负责按约定字段解析。只要字段名一致,换传感器、换网关都不需要动后端。

2.3 数据链路:一个 MQTT 主题就是一条业务边界

先定链路的具体做法:设备上报 JSON,MQTT 主题按设备区分,后端订阅同一组主题。下面是课程设计里最常见的报文格式,也是我建议你先照着模拟的格式:

{ "deviceId": "D001", "timestamp": "2025-03-10 08:30:00", "pm25": 45.2, "pm10": 78.6, "breathableDust": 1.8, "temperature": 22.5, "humidity": 55.0 }

字段里的breathableDust是呼吸性粉尘浓度,这个值是预警判断的核心。pm25和pm10可以当作参考指标展示。timestamp最好由设备生成,不要用后端接收时间代替,否则弱网环境下上报延迟会直接影响历史曲线。

如果资源里的单片机固件不是 Python 而是 STM32 C 代码,同样按这个 JSON 拼字符串,后端解析器只要兼容这个结构即可。这个约定是整个项目最先要确认的东西,我称之为“接口契约”。后端代码里对应的订阅逻辑一般是这样的:

@Bean public MessageHandler mqttHandler() { return (topic, message) -> { String deviceId = topic.replace("dust/", "").replace("/data", ""); DustData data = JSON.parseObject(message.toString(), DustData.class); dustService.saveAndCheck(data); }; }

这里的topic形如dust/D001/data,deviceId从主题里提取,消息体转成 DO 后入库并触发阈值判断。需要注意:replace处理只适用于主题结构固定为dust/{设备ID}/data的情况,如果你把主题改成dust/{设备ID}/upload,这里的字符串处理也要同步改。用正则提取更稳妥,但课程设计里简单替换反而容易讲清楚。

2.4 为什么用 MySQL 加定时任务做历史统计

实时预警是 OLTP,历史趋势和班组统计是 OLAP。课程设计不用上大数据,MySQL 就够了。建议在后端加一个定时任务,每小时或每天把原始表聚合一次,把均值、峰值、超标次数存到 summary 表。前端画曲线时直接查 summary,不用回放原始表,页面加载会快很多。

INSERT INTO dust_summary(hour, avg_pm25, max_pm25, exceed_count) SELECT DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00'), AVG(pm25), MAX(pm25), SUM(CASE WHEN pm25 > 75 THEN 1 ELSE 0 END) FROM dust_raw WHERE create_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR) GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00');

这个 SQL 的要点是DATE_FORMAT把记录归到整点小时,CASE WHEN统计超标次数。阈值 75 只是示例,你应当把它替换成自己的预警阈值。定时任务可以用 Spring 的@Scheduled,cron 表达式设为0 0 * * * ?,也就是每小时整点执行一次。这样原始表只保留近期明细,历史报表走聚合表,存储压力和查询速度都能兼顾。

3. 把源码跑起来:数据库初始化、配置项修改与前后端联调

3.1 解压后先看清目录结构

拿到压缩包后不要急着启动,先扫一遍目录。我一般会确认这几个部分是否存在:

dust-monitor/ ├── docs/ # 文档说明、答辩PPT、设计报告 ├── sql/ # 数据库初始化脚本 ├── backend/ # Spring Boot 或 SSM 后端源码 ├── web/ # Layui 前端静态资源 └── simulator/ # 模拟数据脚本,如果没有就自己写

资源里实际文件可能略有出入,但核心一定是“后端 + 前端 + SQL”。如果目录里没有simulator,我建议自己补一个模拟器,后面联调会轻松很多。前端静态文件里像skin.css、content.css这些是 Layui 皮肤和布局样式,不是核心业务逻辑,不用每一个都去改。

3.2 初始化数据库:先建库再建表,字符集不要用默认

粉尘数据入库最怕中文乱码,尤其是报警描述字段。建库时一定要指定字符集,否则后面改表很痛苦:

CREATE DATABASE dust_monitor CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE dust_monitor; CREATE TABLE dust_raw ( id INT AUTO_INCREMENT PRIMARY KEY, device_id VARCHAR(32) NOT NULL, pm25 DECIMAL(6,2), pm10 DECIMAL(6,2), breathable_dust DECIMAL(6,2), temperature DECIMAL(4,1), humidity DECIMAL(4,1), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_device_time(device_id, create_time) ) ENGINE=InnoDB;

utf8mb4比utf8多覆盖了更多字符,像报警描述里的特殊符号也不会出问题。DECIMAL(6,2)表示最多 4 位整数加 2 位小数,足够表达粉尘浓度。索引idx_device_time用来加快按设备和时间范围查询,历史曲线就靠这个索引。

再建一张报警记录表:

CREATE TABLE alarm_log ( id INT AUTO_INCREMENT PRIMARY KEY, device_id VARCHAR(32), alarm_level TINYINT, alarm_value DECIMAL(6,2), alarm_desc VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;

alarm_level用数字表示等级,后端在展示时映射成“关注/预警/报警”。这样比直接存字符串占空间小,查询排序也更方便。文档里的 SQL 可能还有用户表、设备表、配置表,上面两张表是跑通链路的最小集合,先保证它们能创建成功。

3.3 改配置并启动后端:application.yml 里最容易改错的是时区和密码

后端启动前,先打开配置文件。Spring Boot 项目一般是application.yml或application.properties,重点看这几项:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/dust_monitor?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mqtt: broker: tcp://127.0.0.1:1883 topic: dust/+/data clientId: dust-backend

serverTimezone=Asia/Shanghai不能省,否则时间差 8 小时。密码改成你自己的数据库密码。mqtt.topic用的是通配符+,匹配任意设备 ID。如果远端 MQTT 服务不是本机,broker地址也要改。

启动命令很简单:

mvn clean package -DskipTests java -jar target/dust-monitor.jar

如果资源里没有 Maven 工程,而是普通 JavaWeb 项目,就把打包步骤换成 IDE 里点 Run Tomcat。启动后看到“Started”日志且没有异常,说明后端已经起来了。此时可以先用浏览器访问http://localhost:8080,如果直接能看到接口文档或空页面,基本正常。

3.4 启动前端静态页面:Nginx 指向打包目录

资源里的 Layui 页面是静态资源,可以直接用 Nginx 托管。这里有一个常见前后端分离配置:

server { listen 80; root /opt/dust-monitor/web; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } }

location /api/会把前端发起的接口请求转发到后端 8080 端口。Layui 的表格、日期、滑块都是插件化组件,改起来比原生 JS 省事,这也是很多课设选它的原因。如果你不想装 Nginx,可以直接把web目录放到后端resources/static下,让 Spring Boot 托管,访问路径保持/index.html即可。

启动 Nginx 后,浏览器打开http://localhost,能看到实时曲线页面说明前端没有问题。此时前后端还没有数据,下一步用模拟器推送数据。

3.5 模拟器联调:没有硬件也能看到曲线

联调阶段不需要真实粉尘仪,我一般用 Python 模拟器发 MQTT 数据。先确保本地已经装好 MQTT Broker,并启动服务:

mosquitto_sub -t 'dust/#' -v

这个命令用来监听所有粉尘主题,能看到实际消息是否到达 Broker。然后运行下面的模拟器:

import paho.mqtt.client as mqtt import json import time import random client = mqtt.Client(client_id="simulator-001") client.connect("127.0.0.1", 1883, 60) while True: payload = { "deviceId": "D001", "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "pm25": 30 + random.randint(0, 50), "pm10": 50 + random.randint(0, 80), "breathableDust": 1.0 + random.random(), "temperature": 22.0, "humidity": 50.0 } client.publish("dust/D001/data", json.dumps(payload)) print("published", payload) time.sleep(5)

client_id在同一 Broker 里不能重复,否则后启动的连接会把前面的踢掉。发布周期 5 秒一次,刷新页面就能看到曲线跳动。如果页面没有反应,先看mosquitto_sub是否收到消息,再看后端日志是否入库。这个排查顺序可以解决大半联调问题。

4. 预警阈值这样设才有效:分级判断、参数下发与历史回溯

4.1 阈值模型:不是只有一个报警开关

很多入门项目会把报警写成if (value > 100) alarm,答辩时很容易被问倒。实际作业场所更关心“快到限值了”和“已经超过限值”两个状态,所以我会把判断改成四级:正常、关注、预警、报警。判断逻辑放后端统一处理,前端只根据返回状态切换标签颜色。

public String checkLevel(DustData d) { double v = d.getBreathableDust(); double warn = config.getWarnThreshold(); // 预警阈值 double alert = config.getAlertThreshold(); // 报警阈值 if (v >= alert) return "ALERT"; if (v >= warn) return "WARNING"; if (v >= warn * 0.8) return "ATTENTION"; return "NORMAL"; }

这里的warn * 0.8是“关注阈值”,也可以放在配置表里单独配置。课程设计阶段不需要做得多精确,但分级逻辑一定要讲清楚:当浓度涨到预警阈值的 80% 时,系统先提醒;超过预警阈值后,页面变为醒目状态;超过报警阈值后,触发声光或短信联动。

需要注意:Double和浮点数比较时,如果精度要求高,建议用BigDecimal。粉尘数据在数据库里是DECIMAL,如果 Java 端用double接收,换算后可能出现 1.9999 这种值。课程设计一般影响不大,但如果你要写进报告,我建议统一用BigDecimal做阈值比较。

4.2 阈值配置:放配置表而不是硬编码

硬编码最大的问题是每次调阈值都要重新编译打包。我通常会把阈值放进数据库配置表,前端留一个管理页面,演示时直接改数值,不用重启后端。

INSERT INTO sys_config(config_key, config_value, remark) VALUES ('warn.threshold', '1.5', '呼吸性粉尘预警阈值 mg/m3'), ('alert.threshold', '2.0', '呼吸性粉尘报警阈值 mg/m3');

后端启动时加载这张配置表到内存,修改后通过接口刷新。前端用 Layui 的滑块组件调用更新接口,页面上的“预警阈值”和“报警阈值”可以动态调整。这样演示时你可以先设一个低值,模拟粉尘浓度轻松超过阈值,直观展示报警效果。

除了阈值本身,报警恢复机制也要写清楚。如果报警后浓度降到正常值,系统不能立刻恢复,否则会出现“报警恢复报警恢复”的抖动。常见做法是维持一个状态机,报警持续触发超过一定时间才转换状态:

if (currentLevel.equals("ALERT") && nextLevel.equals("NORMAL")) { if (normalSince == null) { normalSince = now; } else if (now - normalSince > 5 * 60 * 1000) { return "NORMAL"; } return "ALERT"; }

这里的normalSince记录首次低于报警阈值的时间,5 分钟内如果没有再次超标,才恢复为正常。这就是“滞后恢复”或“消抖”的思路。实现不需要 Redis,用一个ConcurrentHashMap<deviceId, Long>存时间戳就够了。

4.3 历史回溯:SQL 聚合验证预警真的触发了

预警系统不能只展示“刚才报警了”,还要能回答“这周哪台设备报了几次”。用报警记录表做聚合查询,几行 SQL 就能生成答辩要用的统计报表:

SELECT device_id, DATE(create_time) AS d, SUM(alarm_level >= 3) AS alert_count, MAX(alarm_value) AS max_value FROM alarm_log WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY device_id, DATE(create_time) ORDER BY d DESC;

SUM(alarm_level >= 3)在 MySQL 里会把布尔表达式当成 0/1 求和,也就是统计“报警”级别的次数。MAX(alarm_value)用来找当天峰值。这个查询结果可以直接贴进文档,比截图更能说明系统真的把超标事件留痕了。

如果还要做月度趋势,就把DATE(create_time)改成DATE_FORMAT(create_time, '%Y-%m'),把INTERVAL 7 DAY改成INTERVAL 1 MONTH。粒度变粗之后,前端曲线依然可以保持逐日刷新,因为明细表和聚合表分离,互不阻塞。

5. 复现这套系统常踩的 5 个坑:现象、原因与处理办法

5.1 数据库中文全变成问号

现象:前端页面中文正常,但历史记录和报警描述在数据库里全是??。

原因:建库时没指定字符集,或者 JDBC 连接没带characterEncoding=utf8。MySQL 默认字符集在某些环境下是latin1,中文字节存不进去。

解决:重新建库,明确使用utf8mb4;连接字符串加上useUnicode=true&characterEncoding=utf8。如果已经有数据,先执行ALTER TABLE dust_raw CONVERT TO CHARACTER SET utf8mb4;,然后重启后端重新写入。

5.2 MQTT 能连接但收不到数据

现象:模拟器发布成功,mosquitto_sub也能看到消息,但后端日志里没有入库记录。

原因:主题不匹配,或者客户端 ID 冲突。例如后端订阅dust/+/data,设备发布dust/D001/data/upload,通配符+匹配的是单层,/upload多余一层就收不到。

解决:先用mosquitto_sub -t '#' -v查看所有主题,确认设备实际发布到哪里;把后端订阅改成dust/#,或者统一主题格式。客户端 ID 加随机后缀,比如simulator-001改成simulator-002,避免互踢。

5.3 前端打开正常但所有接口 404

现象:Layui 页面样式都在,轮询接口报 404,后端控制台没有收到请求。

原因:前端请求地址和后端 context-path 不一致,或者 Nginx 里没有代理/api。很多课设前端直接把请求写死为http://localhost:8080/api/list,但部署后端口或路径变了。

解决:前端全部改成相对路径/api/...,由 Nginx 统一转发;检查后端server.servlet.context-path,如果配置了/dust,前端请求要同步加前缀。后端启动后在浏览器直接访问接口地址,确认能不能返回 JSON,能返回再排查前端。

5.4 传感器数值一直是 0

现象:页面能刷新,但曲线是一条直线,数值恒为 0。

原因:模拟器没跑,或者 JSON 字段名对不上。比如后端解析的是breathableDust,设备发送的是breath_dust,解析器拿到默认值 0。更早期的传感器模块需要预热,一上电就读也是 0。

解决:先用模拟器发送固定值breathableDust: 1.8,排除前端和后端问题;检查设备上报代码里的 key 是否和接口契约一致;真实传感器至少预热 1 分钟,前几条数据直接丢弃。

5.5 端口被占用导致后端启动失败

现象:启动日志报Port 8080 was already in use.,后端进程退出。

原因:本机已有服务占用 8080,或者上次启动的 Java 进程没关掉。在 Windows 上很常见,Linux 上可能是别的开发者服务占用了端口。

解决:

lsof -i:8080 # Linux/macOS netstat -ano | findstr 8080 # Windows kill -9 <pid>

如果你不想动现有进程,直接把server.port改成18080,前端代理同步修改即可。注意改端口后重启后端,不要只改配置文件不重启。

6. 进阶验证:用模拟器把预警链路完整跑一遍并留下证据

6.1 用模拟器替代真实硬件做闭环演示

如果手头没有真实粉尘传感器,完全可以用模拟器完成从采集到预警的闭环。我建议的验证流程是:清空原始表和报警表,启动后端和前端,先发送一组正常值,再发送一组超过报警阈值的值,最后发送恢复值。这样每一步的状态变化都清晰可见。

import paho.mqtt.client as mqtt import json import time client = mqtt.Client(client_id="demo-sim-001") client.connect("127.0.0.1", 1883, 60) def send(breathable): payload = { "deviceId": "D001", "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "pm25": 30.0, "pm10": 50.0, "breathableDust": breathable, "temperature": 22.0, "humidity": 50.0 } client.publish("dust/D001/data", json.dumps(payload)) print("sent", breathable) send(0.8) time.sleep(2) send(2.5) # 超过报警阈值,触发 ALERT time.sleep(2) send(0.5) # 恢复正常

这段脚本里,2.5超过我们配置的报警阈值2.0,页面应该出现明显告警标识,alarm_log表出现一条报警记录。最后一条0.5会触发恢复逻辑,但因为消抖时间没到,状态可能继续保持报警,这是正常现象。

6.2 存档验证证据的方法

如果是要交课设或答辩,验证过程最好留下证据链。我的做法是:每执行一步就截图,包括数据库里新增的报警记录、前端页面状态、历史曲线。把截图按时间顺序放进项目文档,标题写成“XX 时刻模拟器推送超过阈值数据,系统产生报警记录”,答辩老师不需要自己翻日志就能看懂。

更严谨一点,可以同时打开浏览器开发者工具,截取接口请求和响应的 JSON。这样能证明前端展示的数据确实来自后端接口,而不是写死的假数据。

6.3 再往前一步:加一条短信或声光报警

如果基础闭环已经跑通,我建议在报警逻辑里加一个“联动动作”的接口,不一定真发短信,但把接口留出来:

if (level.equals("ALERT")) { alarmService.sendSms(device.getOwnerPhone(), "粉尘浓度达到 " + value + " mg/m3,请立即检查"); gpioService.high(); // 驱动继电器或蜂鸣器 }

这里的sendSms可以先用日志代替,演示时打印一句话;gpioService.high()也不是必须接硬件,可以在前端页面模拟一个指示灯。这个扩展点能让你的项目从“能看曲线”变成“能响应事件”,答辩分数往往在这里拉开。

这套资源我拆到最后一件事:验证不是最后做,而是每改一个参数就做一次。从那以后,我每次拿到类似的物联网课程设计,都会强制走一遍冷启动流程——清库、重置设备、重新上报、把浓度从正常推到报警、确认记录入库、截图存档。这五步看起来简单,却救了我很多次答辩前翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询