简介:智慧水务物联网系统是一套面向供水管理场景的完整应用项目,基于智能水表(含NB-IoT水表)、智能消火栓、智能阀门、数据采集终端(RTU/PLC)及前置传感器,实现监测点数据的采集与统一管理。适用于毕业设计、课程设计、工程实训、大创及各类学科竞赛,也可供物联网、自动化、给排水等相关专业学生作为项目复刻与二次扩展的参考。
资源包共2000个文件,以JavaScript(1022个)、CSS(470个)、HTML(371个)等前端页面与业务逻辑文件为主体,辅以JSON/XML配置、SQL数据库脚本及Python辅助脚本,包体大小61.66MB,目录结构清晰便于定位查阅。项目源码经严格测试运行正常,可直接复现复刻;内置README等说明文档,基础较好的使用者还可在此基础上扩展更多水务监测与智能管理功能。目前已有88人学习浏览,适合需要完整可运行项目参考的开发者与在校学生。
1. 智慧水务物联网系统到底要从哪一层写起
智慧水务物联网系统的项目价值不在“抄表”,而在于把读表这件事从“人养表”变成“链路养数”。一个完整可答辩的系统,至少要覆盖这么一段:表具上报用水数据、云端接入、入库、算费、展示。这段链路牵涉的设备是NB-IoT水表(也包括LoRa、Cat.1等智能表),但这类表具不是买的现成API,而是自己定上报格式、自己接IoT平台、自己写解析服务。做毕设课设的人最容易卡住的地方是:抄表数据发到哪、用什么格式、平台怎么收、水费怎么算清楚。顺着这条链路把每一层的参数选对,项目本身就能作为“完整物联网应用”交付。
2. 智能水表接入:NB-IoT表具的数据从哪来、怎么上报
2.1 三种数据源:真实NB表、开发板模拟、MQTT模拟器
先搞清楚你要驱动的设备是什么。做毕设课设时,用真实商用NB-IoT水表的风险在于:出厂协议不统一(抄表周期、报文结构、厂商私有字段都不一样),而且需要运营商资费卡和专用调试工具。更实际的做法是下面三种里选一种:
| 数据源 | 硬件成本 | 协议可控制程度 | 适合场景 |
|---|---|---|---|
| 真实商用NB-IoT水表 | 高,需要运营商卡 | 低,以厂商协议为准 | 有厂商资料和售后支持的项目 |
| 开发板+NB-IoT模组(BC26/NB86-G/SIM7000C) | 中,模组+天线+资费卡 | 高,可以自己定义报文 | 想展示真实无线链路的毕设、竞赛 |
| 纯MQTT客户端模拟器 | 零 | 最高,随时改字段 | 把重点放在后端和可视化,网络不可靠 |
如果选开发板方案,常见做法是拿STM32或ESP32的板子接串口转NB模组,模组里跑运营商开放的物模型或自定义JSON。选MQTT模拟器则直接用电脑上的mosquitto_pub脚本就能模拟几十块表同时上报。建议先把模拟器跑通,再决定要不要花预算买真实模组,因为后端链路完全一样。
2.2 自定义上行报文:日冻结、时点用量与阀门状态
真实水表上报的数据按业务用途可以分为三类:日冻结,每天固定时刻记录累计读数,用于日结算;时点用量,每小时或每半小时的水量脉冲,用于画用水曲线;事件类,电池低电压、磁干扰、阀门状态变化。把这三种合并成一条JSON消息是够用的:
{ "deviceId": "WM20240015", "reportTime": "2024-06-12 23:50:00", "seq": 1287, "meterStatus": "normal", "valveState": "open", "totalM3": 1024.356, "hourM3": 0.045, "dailyM3": 1.238, "batteryV": 3.68, "signalRsrp": -89 }字段说明:deviceId是水表在系统中的唯一编号,注意不要用MAC地址直接暴露;seq是上报序号,NB网络存在数据重传,用这个字段去重;totalM3是累计用水量,单位直接用“立方米”;hourM3是本次距上次上报之间的用量;dailyM3是当日累计;signalRsrp是NB信号强度,取值范围-44到-140,值越大信号越好。
上报频率的设计一般这样:日冻结每天1次放在夜间23:50左右,时点数据1小时1次,事件数据触发即报。密集抄表项目可以压缩到15分钟1次,但NB-IoT是按次数计费的,上报越频繁流量费用越高,毕设如果自己掏钱建议1天1次日冻结加几次时点数据就够用。
2.3 用MQTT模拟器发出第一条真实报文
mosquitto_pub -h 127.0.0.1 -p 1883 \ -t water/meter/up/WM20240015 \ -m '{"deviceId":"WM20240015","reportTime":"2024-06-12 23:50:00","seq":1287,"meterStatus":"normal","valveState":"open","totalM3":1024.356,"hourM3":0.045,"dailyM3":1.238,"batteryV":3.68,"signalRsrp":-89}' \ -q 1逻辑说明:这条命令把自定义报文发布到topicwater/meter/up/{deviceId}。-q 1表示至少送达一次,服务端收到重复消息时靠seq去重。真实设备上报频率低,用while循环加sleep就能模拟几十块表轮番上报,不需要多线程程序。
提示:如果电脑上没装mosquitto客户端,用
docker run --rm -it eclipse-mosquitto mosquitto_pub ...也能跑同一套命令,只是127.0.0.1要换成宿主机实际可达的地址。
要把真实模组接入时,模组内部已经封装了NB入网和数据PDU发送,应用层只要把上述JSON按模组的AT指令格式填入AT+QMTCFG和AT+QMTPUB相关指令即可。核心参数有三个:APN接入点名称(运营商提供)、MQTT broker地址和端口、上报周期,这三个参数都放在设备配置项里分开管理,不写死在代码里。
3. 平台侧接入:MQTT消息解析与数据落库
3.1 自建EMQX还是直接用运营商IoT平台
项目标题里带“物联网平台”的常见坑是:报名用OneNET、电信AEP,答辩时却发现自己只用了人家平台的折线图,底层消息流完全是黑盒。毕设里更可控的方式是自建EMQX,再考虑是否对接运营商平台。自建broker理由有三个:调试MQTT消息能看到每条pub/sub消息体;设备鉴权、topic权限都能自己控制;数据直接从broker回流到自己的业务库,不依赖第三方平台提供的“数据流”功能。
| 接入方式 | 调试可视性 | 二次开发成本 | 适合场景 |
|---|---|---|---|
| 自建EMQX | 高,Dashboard可逐条查消息 | 低,MQTT协议直接对接 | 毕设/课设,需要展示完整链路 |
| 运营商平台(OneNET/AEP) | 中,依赖平台日志和转储任务 | 高,要适配平台物模型 | 有真实NB表且运营商已有行业模板 |
| 云厂商物联网套件 | 中,控制台功能全 | 中,计费项复杂 | 团队已用云账号,后续要扩展设备量 |
EMQX用docker跑起来的最简配置:
version: '3' services: emqx: image: emqx/emqx:5.8.0 container_name: water-emqx ports: - "1883:1883" - "18083:18083" environment: EMQX_DASHBOARD__DEFAULT_PASSWORD: "Water@123456" EMQX_AUTHENTICATION__1__MECHANISM: password_based EMQX_AUTHENTICATION__1__BACKEND: internal volumes: - emqx-data:/opt/emqx/data volumes: emqx-data:表具通过1883端口接入,18083是管理控制台端口。鉴权用internal模式,在控制台里创建用户,用户名建议按设备类型建组:water_meter一个用户共用来连接,设备编号放进topic里做权限控制;安全要求高的话每个设备一个独立用户名,成本是设备多时维护量大。
3.2 Spring Boot里收消息:从回调到业务服务
服务端负责三件事:接收消息、校验合法性、写库。用Spring Integration MQTT做这一步比裸写Paho回调更清晰些。mqttClientFactory里要配broker地址、clientId和username,clientId每次启动要带随机后缀,避免两个实例共用同一个clientId导致MQTT会话互相挤掉。
@Bean public MessageProducer inboundMqtt() { MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter( "water-server-client-" + System.currentTimeMillis(), mqttClientFactory(), "water/meter/up/#"); adapter.setQos(1); adapter.setOutputChannel(mqttInputChannel()); return adapter; } @Bean public IntegrationFlow mqttFlow() { return IntegrationFlow.from(mqttInputChannel()) .handle("meterMessageHandler", "handleMessage") .get(); }处理器里做四步:解析JSON、校验deviceId、幂等去重、落库。
@Component("meterMessageHandler") public class MeterMessageHandler { private final MeterReadingRepo repo; public void handleMessage(String payload) { MeterReading reading = MeterReading.parse(payload); if (reading == null || !deviceExists(reading.getDeviceId())) { log.warn("非法上报: {}", payload); return; } // (deviceId, seq) 已存在则视为重复帧 if (repo.existsByDeviceIdAndSeq(reading.getDeviceId(), reading.getSeq())) { return; } repo.save(reading); } }去重逻辑的依据是:NB网络在弱信号场景下会重传应用层PDU,模拟器里肉眼看不出来,真机上重复帧比例能到百分之几。用(deviceId, seq)做联合唯一索引,比程序里先查后插更可靠,并发时数据库会直接拒绝第二笔插入。
3.3 表结构:关系数据与时序数据分开存
水表和用户档案、账单放在MySQL关系库里;用水读数如果每15分钟一条、坚持一年,单表能到数万行。属性字段多、查询维度杂,建议直接用时序表承载,避免和业务表相互干扰。
CREATE DATABASE IF NOT EXISTS water_meter DEFAULT CHARACTER SET utf8mb4; CREATE TABLE device ( device_id VARCHAR(32) PRIMARY KEY, group_id VARCHAR(16) NOT NULL, gateway_id VARCHAR(32), install_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE meter_reading ( id BIGINT AUTO_INCREMENT PRIMARY KEY, device_id VARCHAR(32) NOT NULL, seq INT UNSIGNED NOT NULL, report_time DATETIME NOT NULL, meter_status VARCHAR(16), valve_state VARCHAR(8), total_m3 DECIMAL(10,3) NOT NULL, hour_m3 DECIMAL(8,3) NOT NULL, daily_m3 DECIMAL(8,3) NOT NULL, battery_v DECIMAL(4,2), signal_rsrp SMALLINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_device_seq (device_id, seq), KEY idx_report_time (report_time) ) ENGINE=InnoDB;三个关键设计点:total_m3用了DECIMAL(10,3),10位整数3位小数能存到千万立方米级别,浮点类型在累计读数上会出现精度漂移;联合唯一索引用数据库兜底处理重复帧;report_time上建普通索引用于查询小时曲线。这个表不要按设备分区,一个毕设也就几万行,按日分区收益为负。
4. 应用层联动:用水曲线、阶梯计价与远程阀门
4.1 下行控制:远程阀门与命令状态
下行命令前先把Topic命名统一。上行链路用water/meter/up/{deviceId},下行命令用water/meter/down/{deviceId},控制结果回报用water/meter/ack/{deviceId}。三个Topic分开后,EMQX的Dashboard里能直接看到命令是否发出去、表具响应是否回来,排查“命令发出去了但用户没反应”时能少猜半天。远程阀门实现方式是平台向water/meter/down/{deviceId}发命令,表具收到后执行并回一条状态帧:
{ "requestId": "R20240612001", "cmd": "CLOSE_VALVE", "timeoutSec": 30, "deviceId": "WM20240015" }设备在线时立刻返回结果;离线时消息会堆积在broker里,但重传几次后还收不到确认就不太建议继续等。为避免命令丢失后用户侧完全无提示,平台侧保存命令记录表,status字段有PENDING、SUCCESS、TIMEOUT三种,后端定时扫描超过timeoutSec的PENDING任务并标记TIMEOUT,前端据此提示“设备无响应”。
4.2 阶梯水价:把边界条件做成配置表
阶梯计价最怕把边界写死在Java if里,换一档价就要改代码重新发版。建一张价目表,把阶梯区间和单价都放进去:
| 阶梯 | 月用水量区间(m³) | 单价(元/m³) |
|---|---|---|
| 第一档 | (0, 12] | 2.80 |
| 第二档 | (12, 30] | 4.10 |
| 第三档 | (30, +∞) | 6.90 |
对应建表SQL:
CREATE TABLE tier_price ( tier_no TINYINT PRIMARY KEY, upper_m3 DECIMAL(10,2) NULL, unit_price DECIMAL(6,3) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO tier_price (tier_no, upper_m3, unit_price) VALUES (1, 12.00, 2.80), (2, 30.00, 4.10), (3, NULL , 6.90);结算时按月用量流水计算,第一阶梯0到12立方米2.80元每立方米,第二阶梯12到30立方米4.10元,第三阶梯30立方米以上6.90元。Java侧计算注意边界:用量正好等于12.000立方米时按第二阶梯的起算点计,别用>,要用>=时把上限值理解成“本阶梯可包含的最大值”,这样每个阶梯的区间才是闭合的。
4.3 一小时用水曲线与折线图参数
展示层画一天用水曲线时,后端接口按小时分组聚合:
SELECT DATE_FORMAT(report_time, '%H:00') AS hour_point, SUM(hour_m3) AS total_hour FROM meter_reading WHERE device_id = 'WM20240015' AND report_time >= '2024-06-12 00:00:00' AND report_time < '2024-06-13 00:00:00' GROUP BY DATE_FORMAT(report_time, '%Y-%m-%d %H') ORDER BY hour_point;前端把小时点填到x轴、total_hour填到y轴即可。要留意的坑是数据缺失:NB表具夜间可能整小时不上报,这类小时没有数据行,GROUP BY结果里不会出现那一条,前端需要自己补零,否则曲线在缺数时段会断开。补零逻辑放在后端做,返回数组长度固定为24。
在线状态判定不依赖设备心跳,直接看“最近一条report_time距今的时间差”:超过设定阈值(一般设上报周期的3倍)就判为离线,前端的设备列表状态用这条规则轮询。注意延迟上报的帧到达时,report_time还是设备本地时间,入库前要做时区统一,后端统一按+08:00处理,否则夜间上报的数据被错记到另一天。
5. 竞赛与毕设评审视角的三个参数陷阱与验证技巧
5.1 重传、重复消息和数据库唯一键
评审演示时最常被问到“设备断网重连后数据会不会丢或重复”。验证方法:先把设备断网,发3条消息,再恢复网络,把这3条重发一遍,看库里是否只多出3条。uk_device_seq唯一索引的存在让重复帧直接落库失败,但失败信息要写日志,别静默处理,否则答辩时说不出“系统怎么发现重复”。配合前面existsByDeviceIdAndSeq的预检查,重复帧连SQL都不会执行到。
5.2 上报时间、时区和累计读数小数位
三条容易丢分的点:时区问题(设备上报本地时间还是UTC)、累计读数小数位(NB表具部分型号上报整数对应0.001立方米)、上报序号重置(换电池后seq可能从1重新开始,联合唯一键要做成(device_id, seq, report_time))。建议在原型里就把report_time统一成UTC存储,展示层再转本地时区,避免跨天账单算错。
5.3 数据完整性的最小验证脚本
用一个shell循环模拟10块表、每块表每小时上报一篇,跑24小时,之后用一条SQL对比每个设备的SUM(hour_m3)和首末total_m3差值的偏差。偏差超过0.001立方米说明有消息丢失或解析BUG。这类验证脚本不是答辩材料,但能让项目在演示前自己先暴露问题,比演示时当场失败强得多。最后把上报序号seq的剩余价值利用起来:在device表里加一个last_seq字段,每次入库后由服务端回写,下次上报时若seq <= last_seq直接拒绝。这个字段是整个系统从“数据能上来”走向“数据可信”的关键一步,生产成本基本为零,答辩评分上很直观。
本文还有配套的精品资源,点击获取