• 正文
  • 相关推荐
申请入驻 产业图谱

基于Arduino uno设计的室内环境监控系统

11小时前
252
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

一、前言

1.1 项目开发背景

随着城市化进程不断加快以及居民生活水平持续提高,人们对居住环境质量的关注程度不断提升。室内环境作为人们学习、工作和生活的重要场所,其空气质量直接影响人体健康、生活舒适度以及工作效率。现代家庭、办公室、实验室等封闭空间中,由于装修材料释放、人员活动、空气流通不足以及外部污染物进入等因素,容易造成温湿度异常、颗粒物浓度升高、二氧化碳积聚以及甲醛超标等问题。因此,建立一种低成本、实时化、智能化的室内环境监控系统,成为当前智能家居与环境监测领域的重要研究方向。

传统室内环境检测方式大多依赖人工巡检或单一参数检测设备,存在检测项目少、实时性不足、数据无法长期存储以及缺乏远程管理能力等问题。当室内空气质量发生变化时,用户往往难以及时发现并采取有效措施,容易造成空气污染长期存在,影响人体健康。同时,普通检测设备通常缺少联网能力,无法实现历史数据分析、异常报警和跨终端查看,不利于智能化环境管理系统建设。

近年来,物联网技术嵌入式技术以及无线通信技术快速发展,使环境监测设备逐步向智能化、网络化方向演进。通过将传感器、微控制器无线通信模块以及云平台进行融合,可以实现环境数据实时采集、远程传输、云端存储和移动端展示,为用户提供更加直观、高效的环境监测服务。特别是在智能家居应用场景下,通过云平台和移动终端建立数据互联机制,能够突破传统本地监测限制,实现随时随地查看环境状态和接收异常告警。

Arduino Uno平台因其开发门槛低、资源丰富、硬件生态完善等特点,在教学实验、智能控制以及物联网应用开发领域得到广泛应用。基于Arduino Uno构建室内环境监控系统,能够充分利用其良好的外设扩展能力,实现多种环境传感器的数据采集与控制逻辑处理。同时结合ESP8266无线网络模块,可将设备采集的数据上传至OneNet物联网云平台,实现远程数据访问与设备在线管理。

本课题以Arduino Uno单片机作为核心控制单元,融合温湿度检测、PM2.5颗粒物检测、二氧化碳浓度检测以及甲醛检测等多种环境监测功能,通过OLED显示屏实现本地实时数据显示,并利用ESP8266完成无线联网,将环境数据上传至OneNet云平台进行存储与管理。同时基于Qt框架开发Android移动端与Windows上位机软件,实现跨平台数据可视化与远程监控。当环境参数超出设定范围时,系统能够通过蜂鸣器进行本地声光预警,并同步向云端上传报警状态,实现移动端告警提示,从而形成集数据采集、远程监测、异常预警于一体的室内环境智能监控方案,为家庭环境管理和物联网应用实践提供参考价值。

1.2 设计实现的功能

模块代码合集放在网盘:https://pan.quark.cn/s/6c4fac974120

(1)环境温湿度检测功能
系统采用SHT30温湿度传感器对室内环境温度和空气湿度进行实时采集。Arduino Uno通过IIC通信方式读取传感器输出数据,并完成数据解析处理,实现对当前环境温度、湿度参数的实时监测,为用户了解室内环境舒适度提供依据。

(2)空气PM2.5浓度检测功能
系统采用夏普PM2.5空气质量传感器对空气中的颗粒物浓度进行检测。Arduino Uno通过串口方式采集传感器输出数据,并结合数据换算算法获得实际PM2.5浓度值,实现对室内空气质量状态的实时监测。

(3)空气二氧化碳浓度检测功能
系统采用SGP30传感器对室内空气中的二氧化碳浓度进行检测。Arduino Uno通过IIC通信方式获取传感器采样结果,并进行数据处理,实现对环境中CO₂浓度变化情况的实时监控,为室内通风管理提供数据支持。

(4)空气甲醛浓度检测功能
系统采用MS1100VOC传感器对空气中的甲醛浓度进行检测。Arduino Uno通过ADC方式采集传感器模拟输出信号,并经过算法计算转换得到对应的甲醛浓度值,实现室内甲醛污染情况监测。

(5)OLED显示屏本地数据显示功能
系统配置0.96寸OLED显示屏用于数据显示。设备运行过程中,实时采集得到的温度、湿度、PM2.5、二氧化碳以及甲醛等参数会在显示屏上进行动态刷新显示,方便用户直接查看当前环境状态。

(6)环境数据无线联网与云平台上传功能
系统通过ESP8266无线模块连接WiFi热点,并配置为STA+TCP客户端模式,实现设备接入互联网。Arduino Uno将采集到的环境监测数据通过MQTT协议上传至OneNet物联网云平台,实现数据在线存储与远程访问。

(7)手机端与上位机远程监控功能
系统支持Android手机APP和Windows上位机远程访问功能。APP与上位机采用Qt进行开发,通过调用OneNet云平台提供的数据接口获取设备上传的数据,实现环境参数实时查看和远程监测。

(8)环境异常报警与云端告警功能
系统具备环境参数超限报警能力。当检测到温湿度、PM2.5、二氧化碳或甲醛等数据超过设定阈值时,本地有源蜂鸣器会自动发出声音报警。同时设备会将报警状态同步上传至OneNet云平台,并通过手机端实现报警提示功能,便于用户及时处理异常情况。

1.3 项目硬件模块组成

(1)Arduino Uno R3主控模块
系统采用Arduino Uno R3作为核心控制单元,负责完成各传感器数据采集、数据处理、显示控制、通信管理以及报警逻辑控制等功能。主控芯片通过串口、IIC、ADC以及数字IO接口与各功能模块进行连接,实现整个系统的数据协调与控制。

(2)SHT30温湿度检测模块
系统采用SHT30数字温湿度传感器完成环境温度与空气湿度参数采集。该模块通过IIC总线与Arduino Uno进行通信,实时获取室内环境温湿度数据,并将检测结果上传至显示终端及云平台。

(3)夏普PM2.5空气质量检测模块
系统采用夏普PM2.5传感器实现空气中颗粒物浓度检测。模块输出检测数据后,由Arduino Uno通过串口接口进行采集,并经过数据计算获得对应的PM2.5浓度值,用于环境空气质量分析。

(4)SGP30二氧化碳检测模块
系统采用SGP30传感器对空气中的二氧化碳浓度进行监测。模块通过IIC接口与Arduino Uno连接,实时输出环境CO₂浓度信息,为空气质量评估提供依据。

(5)MS1100VOC甲醛检测模块
系统采用MS1100VOC传感器完成空气中甲醛浓度检测。该模块输出模拟电压信号,Arduino Uno通过内部ADC接口采集模拟量数据,并利用换算算法得到实际甲醛浓度值。

(6)ESP8266无线通信模块
系统采用ESP8266作为无线联网模块,工作于STA+TCP客户端模式。模块通过串口与Arduino Uno进行数据交互,在连接WiFi热点后实现环境监测数据上传至OneNet云平台,同时支持远程数据访问。

(7)0.96寸OLED显示模块
系统采用0.96寸OLED显示屏作为本地数据显示终端,通过IIC通信方式与Arduino Uno连接。显示模块用于实时显示温度、湿度、PM2.5、二氧化碳、甲醛以及设备运行状态等信息。

(8)蜂鸣器报警模块
系统采用高电平触发式有源蜂鸣器作为本地报警装置。Arduino Uno通过数字IO输出高低电平信号驱动蜂鸣器工作,在环境参数超过设定阈值时发出声音提醒。

(9)USB供电模块
系统采用USB接口进行供电,为Arduino Uno主控板及各外围功能模块提供稳定工作电源,满足系统长期运行和联网监测需求。

1.4 系统框架图

1.5 运行流程图

1.6 硬件材料

【1】arduino uno r3 开发板

【2】OLED显示屏(IIC协议4针)

【3】WIFI模块

链接:https://item.taobao.com/item.htm?id=40511001950&mi_id=00009rn3BwTdBqYIpJRJ35YTxmj60kqzihzpKh0u5sR2Hyo&skuId=3833808544302&spm=tbpc.boughtlist.suborder_itemtitle.1.661d2e8dLsnZ9R

【4】PM2.5粉尘检测传感器

链接:https://item.taobao.com/item.htm?from=cart&id=601006937585&mi_id=0000NbfBy6kx8wYOlniCSiC6K2pON17irLyBl4JvRbL1Lo8&spm=a1z0d.6639537%2F202410.item.d601006937585.6cda7484pQ6Pf3&upStreamPrice=2880

【5】SHT30温湿度模块

链接: https://detail.tmall.com/item.htm?abbucket=8&id=624272456739&ns=1&skuId=4420709624009&spm=a21n57.1.item.4.5097523cBtlCji

【6】MS1100VOC

链接:https://item.taobao.com/item.htm?abbucket=9&id=597034760151&ns=1&pisk=f46rLpV7s8ePjtyId3JE39q-Xx9J9L4_U9TBxMjHVUYkPMxHLZbQ2Us3ywWeoMC72Ba-Y0BV7vM7y7InLp935Pw_CgnJpp46FhIug4K6Y22Brex0PmvUhPw_CiPk2ps05TTHlz-6fvvHZBADnEKe-YYh-IvDXEDkxYxnoo8vxpxHZYxmiHKiKYcl-x2DjHDoKYxhmKx6YpYh-pc7JBAZ3HduXsXkGTeISITGqUj5qtDyNbI2uYDP3_O6SRTqKvX2qiEvnyHaOFf9PCTGo-MyLixFuC1z7Y7FxGCHg9uuVNbcTwxAMyle76j56tOxxbjVLURGEIoLfKvyxwAFMPk64L12_T58vqjl5U5MeMiaPiRNgCKDiDlkF1s1pCXuUVLX6H721_zZUNjrX2K0qz6da2ceZnK25or4V1cSircs7-GK9Qgv0FZuqXhpZnK25orq9Xd76n8_quf..&priceTId=213e360317314825907796524edb95&spm=a21n57.1.item.50.331b523cH2yGEF&utparam=%7B%22aplus_abtest%22%3A%22cf44a5b6b34a4594046ab0ada302ce0c%22%7D&xxc=taobaoSearch

【7】SGP30模块

链接:https://item.taobao.com/item.htm?spm=a230r.1.14.16.2cd232b70AYMYG&id=573784216895&ns=1&abbucket=17#detail

二、OneNet平台搭建

2.1 OneNet平台介绍

OneNet是中国移动打造的物联网开放平台,为物联网设备与应用开发者提供稳定、可靠、可扩展的云端服务。该平台向下连接海量设备,支持设备接入、数据采集与远程控制;向上提供数据存储、分析与应用使能工具,帮助用户快速搭建各类物联网解决方案。作为国内领先的物联网PaaS平台,OneNet具备高并发处理能力和毫秒级消息响应能力,其分布式集群架构可保障99.95%的服务可用性。

在功能层面,OneNet支持MQTT、CoAP、LwM2M等多种主流物联网协议,并提供设备注册、认证、生命周期管理以及设备影子等功能。平台内嵌规则引擎,可实现数据流转与场景联动,同时配备了数据可视化大屏和时序数据库,便于用户进行数据分析和监控。安全机制方面,OneNet提供了设备身份认证、数据加密传输和访问控制策略,确保设备和数据的安全可靠。

在生态与开发支持方面,OneNet提供了多语言SDK(如Java、Python、C、Node.js等)、详细的开发者文档、设备模拟器以及活跃的开发者社区,帮助开发者和企业快速完成从设备端到应用端的集成。该平台已广泛应用于智慧城市、工业物联网、智慧家居、车联网等领域,例如智能路灯、设备远程监控、智能家电控制、车辆定位追踪等典型场景。随着5G、AI和边缘计算技术的发展,OneNet正在持续融合边缘计算能力与人工智能分析,进一步提升平台的实时性与数据价值,助力企业数字化转型

2.2 创建产品

(1)登录账户

官网:https://open.iot.10086.cn/

进来先登录账号。

(2)选择物联网开放平台

(3)创建产品

根据自己产品信息填写:

创建之后点击确定。

创建完成。

(4)产品ID

产品ID: Dy3FSnC24O

2.3 创建设备

产品是属性抽象模型,产品下面的设备就表示具体的硬件设备,需要与具体的硬件关联。产品下可以创建很多的设。

(1)添加设备

(2)填写设备信息

创建完成。

创建完成。

(3)查看设备详情

添加完成之后,点击查看详情,查看设备的详细信息。

这里的产品ID、设备ID、设备密匙非常有用。后续MQTT登录参数需要使用。这里记录一下。

产品ID:  Dy3FSnC24O
设备ID:  2605967662
设备密匙:b211dUEzY2pZdHdJMXhFS1FzTDBQbjYzdk1WMjRZZkg=
设备名称:dev1

2.4 添加数据流模板

(1)添加数据流模板

添加数据流模板。

(2)根据设备需求添加

data1  测试数据字段1
data2  测试数据字段2
data3  测试数据字段3
data4  测试数据字段4
data5  测试数据字段5

开始创建:

(3)添加完毕

这里可以看到订阅,发布主题的格式。

2.5 MQTT协议接入地址

当前智能鱼缸设备是采用MQTT协议与OneNet服务器进行通信。

MQTT物联网套件产品架构如下图所示:

接入地址说明:https://open.iot.10086.cn/doc/v5/develop/detail/248

在帮助文档页面,介绍了MQTT接入的地址和端口号。 当前设备是单片机,端口采用1883非加密端口。

地址与端口总结如下:

固定的IP地址: 183.230.40.96
固定的端口号: 1883

2.6 MQTT主题订阅与发布

MQTT协议是一种消息列队传输协议,采用订阅、发布机制,订阅者只接收自己已经订阅的数据,非订阅数据则不接收,既保证了必要的数据的交换,又避免了无效数据造成的储存与处理。因此在工业物联网中得到广泛的应用。

(1)主题订阅

主题订阅是设备订阅平台的消息,如果设备想知道平台下发的消息,就需要订阅主题。

帮助文档:  https://open.iot.10086.cn/doc/mqtt/book/device-develop/protocol.html

需要订阅什么数据,设备端按照下面的主题格式填写订阅即可。

如果想知道设备所有相关信息,直接订阅$sys/{产品ID}/{device-name}/#即可。 (其中的PID就是产品ID)

$sys/Dy3FSnC24O/dev1/#

(2)主题发布

主题发布: 就是设备向平台上传数据。

帮助文档地址:https://open.iot.10086.cn/doc/mqtt/book/example/datapoints.html

文档里介绍了数据点上传的格式:

根据当前设备,总结的格式如下:

发布主题: $sys/Dy3FSnC24O/dev1/dp/post/json

发布消息:
{"id":123,"dp":{"data1":[{"v":6.4}],"data2":[{"v":3.4}]}}

dp对象里面就是需要上传的数据点字段。 这个数据点的名字就是自己创建数据流模板的时候创建的。

数据点是标准的JSON结构:

{
  "id": 123,
  "dp": {
    "data1": [
      {
        "v": 6.4
      }
    ],
    "data2": [
      {
        "v": 3.4
      }
    ]
  }
}

2.7 MQTT三元组生成

设备登录OneNet采用的是MQTT协议,MQTT协议登录需要填写 登录信息:简称  MQTT三元组

(1)需要的参数

(2)密码生成规则

链接:https://open.iot.10086.cn/doc/v5/develop/detail/241

(3)编写生成密码的算法

这里算法代码采用Python编写。下面是生成密码的Python算法。

电脑先安装Python环境,然后将下面代码copy到文本里,文本后缀改为.py,就可以允许python代码了。

import base64
import hmac
from urllib.parse import quote
import time

# 中国移动官方文档给出的核心秘钥计算算法
def token(id,access_key):  
    version = '2018-10-31'
    res = 'products/%s' % id  # 通过产品ID访问产品API
    # 用户自定义token过期时间
    et = str(int(time.time()) + 63072000) # 设置为2年有效时间
    # 签名方法,支持md5、sha1、sha256
    method = 'sha1'
    # 对access_key进行decode
    key = base64.b64decode(access_key)
    # 计算sign
    org = et + 'n' + method + 'n' + res + 'n' + version
    sign_b = hmac.new(key=key, msg=org.encode(), digestmod=method)
    sign = base64.b64encode(sign_b.digest()).decode()
    # value 部分进行url编码,method/res/version值较为简单无需编码
    sign = quote(sign, safe='')
    res = quote(res, safe='')
    # token参数拼接
    token = 'version=%s&res=%s&et=%s&method=%s&sign=%s' % (version, res, et, method, sign)
    return token

username    = "Dy3FSnC24O"                                           # 产品ID
accesskey   = "c3BoeTdqM3hPQXI4Zm1HckdhZTJieEpyT1N0eGpXTDI="         # accessKey
password = token(username, accesskey)
print(password)

# 等待用户按下 Enter 键再退出
input("按 Enter 键退出程序...")

先安装好Python环境。

根据自己的实际修改以上的产品ID与accessKey

**将上面的文件保存为 xxx.py  文件,然后双击运行: ** 就得到了生成的key

(4)MQTT登录参数总结

MQTT协议登录时,需要输入3个参数: MQTT-设备ID,MQTT-设备名称,MQTT-密码。

对应OneNet的参数:

MQTT的设备ID  -----> 就是OneNet的设备名称

MQTT的设备名称-----> 就是OneNet的产品ID

MQTT的密码------------> 就是OneNet的密匙工具生成的密码

下面是对本次的设备做总结:

IP地址: 183.230.40.96
端口号: 1883
clientId: dev1
username: Dy3FSnC24O
password: version=2018-10-31&res=products%2FDy3FSnC24O&et=1827306543&method=sha1&sign=Yzr14cK2iyhIDhYSCXMf89h%2BeQk%3D
订阅主题:  $sys/Dy3FSnC24O/dev1/#


发布主题: $sys/Dy3FSnC24O/dev1/dp/post/json

发布消息:
{"id":123,"dp":{"data1":[{"v":6.4}],"data2":[{"v":3.4}]}}

2.8 MQTT工具登录测试

前面已经介绍了MQTT协议登录需要用到的参数,以及订阅主题、发布主题的格式,接下来我们通过MQTT工具模拟设备登录OneNet平台,完成数据交互测试。

简单来说: 就是用软件来模拟实际的硬件,登录onenet平台,上传数据,走一下这个流程。

(1)模拟设备登录

接下来根据软件的输入框提示,输入对应的参数,然后登录设备,订阅主题,发布主题即可完成一个流程的测试。

(2)登录OneNet控制台查看设备

在设备列表页面,可以看到设备已经在线了。

在设备详情页面可以看到设备模拟器刚才上传的数据。

在设备的数据流页面可以看到上传的数据。

(3)查看历史数据

切换成列表模式。可以查看历史数据。

点击数据名称,可以查看数据的历史数据,同时可以导出EXECL表格,还可以看历史数据变化波形。

到此,设备的云平台已经配置完毕。

2.9 上位机开发必备参数

开发上位机需要得到OneNet的一些参数,生成鉴权的Token。

查看AccessKey:

下面就可以查看了:

复制保存下来,后面需要使用:

fa6da5ee39964d9c9a168fec801b7dd1

链接地址:https://open.iot.10086.cn/console/summary

产品ID:Dy3FSnC24O
设备名称: dev1

AccessKey:
fa6da5ee39964d9c9a168fec801b7dd1

用户ID:477709

三、上位机开发

3.1 Qt开发环境安装

Qt的中文官网: https://www.qt.io/zh-cn/![image-20221207160550486](https://led-obs.obs.cn-north-1.myhuaweicloud.com/Blog/img/image-20221207160550486.png)

QT5.12.6的下载地址:https://download.qt.io/archive/qt/5.12/5.12.6

打开下载链接后选择下面的版本进行下载:

如果下载不了,可以在网盘里找到安装包下载:    飞书文档记录的网盘地址:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink

软件安装时断网安装,否则会提示输入账户。

安装的时候,第一个复选框里的编译器可以全选,直接点击下一步继续安装。

选择编译器: (一定要看清楚了)

3.2 新建上位机工程

前面2讲解了需要用的API接口,接下来就使用Qt设计上位机,设计界面,完成整体上位机的逻辑设计。

【1】新建工程

【2】设置项目的名称。

【3】选择编译系统

【4】选择默认继承的类

【5】选择编译器

【6】点击完成

【7】工程创建完成

3.3 切换编译器

在左下角是可以切换编译器的。 可以选择用什么样的编译器编译程序。

目前新建工程的时候选择了2种编译器。 一种是mingw32这个编译Windows下运行的程序。 一种是Android编译器,可以生成Android手机APP。

不过要注意:Android的编译器需要配置一些环境才可以正常使用,这个大家可以网上找找教程配置一下就行了。

windows的编译器就没有这么麻烦,安装好Qt就可以编译使用。

下面我这里就选择的 mingw32这个编译器,编译Windows下运行的程序。

3.4 编译测试功能

创建完毕之后,编译测试一下功能是否OK。

点击左下角的绿色三角形按钮

正常运行就可以看到弹出一个白色的框框。这就表示工程环境没有问题了。 接下来就可以放心的设计界面了。

3.5 设计UI界面与工程配置

【1】打开UI文件

打开默认的界面如下:

【2】开始设计界面

根据自己需求设计界面。

3.6 设计代码

widget.h 头文件

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTimer>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QTableWidget>
#include <QLabel>
#include <QPushButton>
#include <QGroupBox>
#include <QGridLayout>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QProgressBar>
#include <QMessageBox>
#include <QDateTime>
#include <QSoundEffect>
#include <QSettings>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    // 定时器槽函数
    void onRefreshTimer();
    
    // 网络请求槽函数
    void onNetworkReplyFinished(QNetworkReply *reply);
    
    // UI交互槽函数
    void onRefreshButtonClicked();
    void onSettingsButtonClicked();
    void onClearLogButtonClicked();
    
    // 数据解析槽函数
    void parseDeviceData(const QJsonObject &dataObj);
    void parseAlarmStatus(const QJsonObject &dataObj);
    
    // 报警处理槽函数
    void checkAlarmConditions();
    void showAlarmMessage(const QString &message);

private:
    Ui::Widget *ui;
    
    // 网络管理
    QNetworkAccessManager *networkManager;
    QNetworkRequest networkRequest;
    
    // 定时器
    QTimer *refreshTimer;
    
    // API配置
    QString apiKey;
    QString deviceId;
    QString productId;
    QString apiUrl;
    
    // 设备数据
    struct DeviceData {
        float temperature;
        float humidity;
        float pm25;
        int co2;
        int tvoc;
        float hcho;
        bool alarm;
        qint64 timestamp;
    } currentData;
    
    // 报警阈值
    struct AlarmThreshold {
        float tempMax;
        float tempMin;
        float humidMax;
        float humidMin;
        float pm25Max;
        int co2Max;
        float hchoMax;
    } thresholds;
    
    // UI组件指针
    QLabel *tempLabel;
    QLabel *humidLabel;
    QLabel *pm25Label;
    QLabel *co2Label;
    QLabel *tvocLabel;
    QLabel *hchoLabel;
    QLabel *alarmLabel;
    QLabel *statusLabel;
    QLabel *updateTimeLabel;
    
    QProgressBar *tempBar;
    QProgressBar *humidBar;
    QProgressBar *pm25Bar;
    QProgressBar *co2Bar;
    QProgressBar *hchoBar;
    
    QTableWidget *dataTable;
    QTextEdit *logTextEdit;
    
    QPushButton *refreshBtn;
    QPushButton *settingsBtn;
    QPushButton *clearLogBtn;
    
    // 报警音效
    QSoundEffect *alarmSound;
    
    // 函数
    void initUI();
    void initNetwork();
    void loadSettings();
    void saveSettings();
    void updateUI();
    void addLogMessage(const QString &msg, bool isError = false);
    void fetchDeviceData();
    QString formatDateTime(qint64 timestamp);
    void showSettingsDialog();
    
    // 报警状态
    bool previousAlarmState;
};
#endif // WIDGET_H

widget.cpp 实现文件

#include "widget.h"
#include "ui_widget.h"
#include <QJsonParseError>
#include <QUrl>
#include <QUrlQuery>
#include <QDebug>
#include <QInputDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLineEdit>
#include <QDoubleSpinBox>
#include <QSpinBox>
#include <QApplication>
#include <QScreen>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
    , networkManager(nullptr)
    , refreshTimer(nullptr)
    , previousAlarmState(false)
{
    ui->setupUi(this);
    
    // 初始化默认阈值
    thresholds.tempMax = 35.0;
    thresholds.tempMin = 10.0;
    thresholds.humidMax = 80.0;
    thresholds.humidMin = 30.0;
    thresholds.pm25Max = 75.0;
    thresholds.co2Max = 1000;
    thresholds.hchoMax = 0.08;
    
    // 初始化UI
    initUI();
    
    // 初始化网络
    initNetwork();
    
    // 加载设置
    loadSettings();
    
    // 初始化定时器
    refreshTimer = new QTimer(this);
    refreshTimer->setInterval(5000); // 5秒刷新一次
    connect(refreshTimer, &QTimer::timeout, this, &Widget::onRefreshTimer);
    
    // 初始化报警音效
    alarmSound = new QSoundEffect(this);
    alarmSound->setSource(QUrl::fromLocalFile(":/sounds/alarm.wav"));
    alarmSound->setLoopCount(QSoundEffect::Infinite);
    alarmSound->setVolume(0.5);
    
    // 添加日志
    addLogMessage("系统启动完成");
    addLogMessage("等待首次数据获取...");
    
    // 立即获取一次数据
    QTimer::singleShot(1000, this, &Widget::fetchDeviceData);
}

Widget::~Widget()
{
    saveSettings();
    delete ui;
}

void Widget::initUI()
{
    // 设置窗口标题和大小
    this->setWindowTitle("室内环境监控系统 v1.0");
    this->setMinimumSize(1000, 700);
    
    // 创建主布局
    QVBoxLayout *mainLayout = new QVBoxLayout(this);
    
    // ===== 顶部标题栏 =====
    QLabel *titleLabel = new QLabel("室内环境实时监控系统");
    titleLabel->setStyleSheet("font-size: 24px; font-weight: bold; color: #2c3e50; padding: 10px;");
    titleLabel->setAlignment(Qt::AlignCenter);
    mainLayout->addWidget(titleLabel);
    
    // ===== 数据显示区域 =====
    QGridLayout *dataLayout = new QGridLayout();
    
    // 创建各个数据卡片
    auto createDataCard = [&](const QString &title, const QString &unit, QLabel **label, QProgressBar **bar) {
        QGroupBox *groupBox = new QGroupBox(title);
        groupBox->setStyleSheet("QGroupBox { font-weight: bold; }");
        QVBoxLayout *layout = new QVBoxLayout(groupBox);
        
        *label = new QLabel("--");
        (*label)->setStyleSheet("font-size: 32px; font-weight: bold; color: #3498db;");
        (*label)->setAlignment(Qt::AlignCenter);
        layout->addWidget(*label);
        
        QLabel *unitLabel = new QLabel(unit);
        unitLabel->setStyleSheet("font-size: 14px; color: #7f8c8d;");
        unitLabel->setAlignment(Qt::AlignCenter);
        layout->addWidget(unitLabel);
        
        *bar = new QProgressBar();
        (*bar)->setRange(0, 100);
        (*bar)->setValue(0);
        (*bar)->setTextVisible(false);
        (*bar)->setStyleSheet("QProgressBar { height: 8px; border-radius: 4px; background-color: #ecf0f1; }"
                              "QProgressBar::chunk { background-color: #3498db; border-radius: 4px; }");
        layout->addWidget(*bar);
        
        return groupBox;
    };
    
    // 温度卡片
    dataLayout->addWidget(createDataCard("温度", "°C", &tempLabel, &tempBar), 0, 0);
    
    // 湿度卡片
    dataLayout->addWidget(createDataCard("湿度", "%", &humidLabel, &humidBar), 0, 1);
    
    // PM2.5卡片
    dataLayout->addWidget(createDataCard("PM2.5", "μg/m³", &pm25Label, &pm25Bar), 0, 2);
    
    // CO2卡片
    dataLayout->addWidget(createDataCard("CO₂", "ppm", &co2Label, &co2Bar), 1, 0);
    
    // TVOC卡片
    QGroupBox *tvocGroup = new QGroupBox("TVOC");
    tvocGroup->setStyleSheet("QGroupBox { font-weight: bold; }");
    QVBoxLayout *tvocLayout = new QVBoxLayout(tvocGroup);
    tvocLabel = new QLabel("--");
    tvocLabel->setStyleSheet("font-size: 32px; font-weight: bold; color: #3498db;");
    tvocLabel->setAlignment(Qt::AlignCenter);
    tvocLayout->addWidget(tvocLabel);
    QLabel *tvocUnitLabel = new QLabel("ppb");
    tvocUnitLabel->setStyleSheet("font-size: 14px; color: #7f8c8d;");
    tvocUnitLabel->setAlignment(Qt::AlignCenter);
    tvocLayout->addWidget(tvocUnitLabel);
    dataLayout->addWidget(tvocGroup, 1, 1);
    
    // 甲醛卡片
    dataLayout->addWidget(createDataCard("甲醛", "mg/m³", &hchoLabel, &hchoBar), 1, 2);
    
    mainLayout->addLayout(dataLayout);
    
    // ===== 报警状态显示 =====
    QHBoxLayout *alarmLayout = new QHBoxLayout();
    alarmLabel = new QLabel("✅ 系统运行正常");
    alarmLabel->setStyleSheet("font-size: 18px; font-weight: bold; padding: 10px; background-color: #27ae60; color: white; border-radius: 5px;");
    alarmLabel->setAlignment(Qt::AlignCenter);
    alarmLayout->addWidget(alarmLabel);
    mainLayout->addLayout(alarmLayout);
    
    // ===== 控制按钮区域 =====
    QHBoxLayout *buttonLayout = new QHBoxLayout();
    
    refreshBtn = new QPushButton(" 刷新数据");
    refreshBtn->setStyleSheet("QPushButton { padding: 8px 20px; background-color: #3498db; color: white; border: none; border-radius: 5px; font-size: 14px; }"
                              "QPushButton:hover { background-color: #2980b9; }");
    connect(refreshBtn, &QPushButton::clicked, this, &Widget::onRefreshButtonClicked);
    buttonLayout->addWidget(refreshBtn);
    
    settingsBtn = new QPushButton("⚙️ 设置");
    settingsBtn->setStyleSheet("QPushButton { padding: 8px 20px; background-color: #95a5a6; color: white; border: none; border-radius: 5px; font-size: 14px; }"
                               "QPushButton:hover { background-color: #7f8c8d; }");
    connect(settingsBtn, &QPushButton::clicked, this, &Widget::onSettingsButtonClicked);
    buttonLayout->addWidget(settingsBtn);
    
    clearLogBtn = new QPushButton("️ 清除日志");
    clearLogBtn->setStyleSheet("QPushButton { padding: 8px 20px; background-color: #e74c3c; color: white; border: none; border-radius: 5px; font-size: 14px; }"
                               "QPushButton:hover { background-color: #c0392b; }");
    connect(clearLogBtn, &QPushButton::clicked, this, &Widget::onClearLogButtonClicked);
    buttonLayout->addWidget(clearLogBtn);
    
    buttonLayout->addStretch();
    
    statusLabel = new QLabel("状态: 等待数据...");
    statusLabel->setStyleSheet("color: #7f8c8d;");
    buttonLayout->addWidget(statusLabel);
    
    updateTimeLabel = new QLabel("最后更新: --");
    updateTimeLabel->setStyleSheet("color: #7f8c8d;");
    buttonLayout->addWidget(updateTimeLabel);
    
    mainLayout->addLayout(buttonLayout);
    
    // ===== 数据表格 =====
    QGroupBox *tableGroup = new QGroupBox("历史数据记录");
    QVBoxLayout *tableLayout = new QVBoxLayout(tableGroup);
    
    dataTable = new QTableWidget();
    dataTable->setColumnCount(8);
    QStringList headers = {"时间", "温度(°C)", "湿度(%)", "PM2.5(μg/m³)", "CO₂(ppm)", "TVOC(ppb)", "甲醛(mg/m³)", "报警"};
    dataTable->setHorizontalHeaderLabels(headers);
    dataTable->horizontalHeader()->setStretchLastSection(true);
    dataTable->setEditTriggers(QTableWidget::NoEditTriggers);
    dataTable->setAlternatingRowColors(true);
    dataTable->setStyleSheet("QTableWidget::item { padding: 5px; }");
    tableLayout->addWidget(dataTable);
    
    mainLayout->addWidget(tableGroup);
    
    // ===== 日志显示 =====
    QGroupBox *logGroup = new QGroupBox("系统日志");
    QVBoxLayout *logLayout = new QVBoxLayout(logGroup);
    logTextEdit = new QTextEdit();
    logTextEdit->setReadOnly(true);
    logTextEdit->setMaximumHeight(120);
    logTextEdit->setStyleSheet("QTextEdit { background-color: #2c3e50; color: #ecf0f1; font-family: monospace; }");
    logLayout->addWidget(logTextEdit);
    mainLayout->addWidget(logGroup);
}

void Widget::initNetwork()
{
    networkManager = new QNetworkAccessManager(this);
    connect(networkManager, &QNetworkAccessManager::finished, 
            this, &Widget::onNetworkReplyFinished);
    
    // 设置默认API地址
    apiUrl = "https://api.heclouds.com/devices/";
}

void Widget::loadSettings()
{
    QSettings settings("IndoorMonitor", "Settings");
    
    apiKey = settings.value("apiKey", "").toString();
    deviceId = settings.value("deviceId", "").toString();
    productId = settings.value("productId", "").toString();
    
    thresholds.tempMax = settings.value("tempMax", 35.0).toFloat();
    thresholds.tempMin = settings.value("tempMin", 10.0).toFloat();
    thresholds.humidMax = settings.value("humidMax", 80.0).toFloat();
    thresholds.humidMin = settings.value("humidMin", 30.0).toFloat();
    thresholds.pm25Max = settings.value("pm25Max", 75.0).toFloat();
    thresholds.co2Max = settings.value("co2Max", 1000).toInt();
    thresholds.hchoMax = settings.value("hchoMax", 0.08).toFloat();
    
    if (!apiKey.isEmpty() && !deviceId.isEmpty()) {
        addLogMessage("已加载配置: 设备ID = " + deviceId);
        refreshTimer->start();
    } else {
        addLogMessage("请先配置API密钥和设备ID", true);
        // 自动打开设置对话框
        QTimer::singleShot(500, this, &Widget::onSettingsButtonClicked);
    }
}

void Widget::saveSettings()
{
    QSettings settings("IndoorMonitor", "Settings");
    
    settings.setValue("apiKey", apiKey);
    settings.setValue("deviceId", deviceId);
    settings.setValue("productId", productId);
    settings.setValue("tempMax", thresholds.tempMax);
    settings.setValue("tempMin", thresholds.tempMin);
    settings.setValue("humidMax", thresholds.humidMax);
    settings.setValue("humidMin", thresholds.humidMin);
    settings.setValue("pm25Max", thresholds.pm25Max);
    settings.setValue("co2Max", thresholds.co2Max);
    settings.setValue("hchoMax", thresholds.hchoMax);
}

void Widget::fetchDeviceData()
{
    if (apiKey.isEmpty() || deviceId.isEmpty()) {
        statusLabel->setText("状态: 请先配置API信息");
        return;
    }
    
    statusLabel->setText("状态: 正在获取数据...");
    
    // 构建OneNet API请求
    QString url = apiUrl + deviceId + "/datapoints";
    QUrl requestUrl(url);
    QUrlQuery query;
    query.addQueryItem("limit", "1");
    query.addQueryItem("order", "desc");
    requestUrl.setQuery(query);
    
    networkRequest.setUrl(requestUrl);
    networkRequest.setRawHeader("api-key", apiKey.toUtf8());
    
    networkManager->get(networkRequest);
}

void Widget::onNetworkReplyFinished(QNetworkReply *reply)
{
    if (reply->error() != QNetworkReply::NoError) {
        QString errorMsg = "网络错误: " + reply->errorString();
        addLogMessage(errorMsg, true);
        statusLabel->setText("状态: " + errorMsg);
        reply->deleteLater();
        return;
    }
    
    QByteArray responseData = reply->readAll();
    reply->deleteLater();
    
    // 解析JSON
    QJsonParseError parseError;
    QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &parseError);
    
    if (parseError.error != QJsonParseError::NoError) {
        addLogMessage("JSON解析错误: " + parseError.errorString(), true);
        return;
    }
    
    QJsonObject jsonObj = jsonDoc.object();
    int errno = jsonObj.value("errno").toInt();
    
    if (errno != 0) {
        QString errorMsg = jsonObj.value("error").toString();
        addLogMessage("API错误: " + errorMsg, true);
        statusLabel->setText("状态: API错误 - " + errorMsg);
        return;
    }
    
    QJsonObject dataObj = jsonObj.value("data").toObject();
    parseDeviceData(dataObj);
}

void Widget::parseDeviceData(const QJsonObject &dataObj)
{
    QJsonArray datastreams = dataObj.value("datastreams").toArray();
    
    if (datastreams.isEmpty()) {
        addLogMessage("没有数据点", true);
        statusLabel->setText("状态: 无数据");
        return;
    }
    
    // 解析各个数据流
    for (const QJsonValue &streamValue : datastreams) {
        QJsonObject streamObj = streamValue.toObject();
        QString streamId = streamObj.value("id").toString();
        QJsonArray datapoints = streamObj.value("datapoints").toArray();
        
        if (datapoints.isEmpty()) continue;
        
        QJsonObject pointObj = datapoints.first().toObject();
        QJsonValue valueObj = pointObj.value("value");
        qint64 timestamp = pointObj.value("at").toString().toLongLong();
        
        // 更新当前数据时间戳
        currentData.timestamp = timestamp;
        
        // 根据数据流ID解析数据
        if (streamId == "temperature") {
            currentData.temperature = valueObj.toDouble();
        } else if (streamId == "humidity") {
            currentData.humidity = valueObj.toDouble();
        } else if (streamId == "pm25") {
            currentData.pm25 = valueObj.toDouble();
        } else if (streamId == "co2") {
            currentData.co2 = valueObj.toInt();
        } else if (streamId == "tvoc") {
            currentData.tvoc = valueObj.toInt();
        } else if (streamId == "hcho") {
            currentData.hcho = valueObj.toDouble();
        } else if (streamId == "alarm") {
            currentData.alarm = valueObj.toBool();
        }
    }
    
    // 更新UI
    updateUI();
    
    // 检查报警
    checkAlarmConditions();
    
    // 添加数据到表格
    addDataToTable();
    
    // 更新状态
    statusLabel->setText("状态: 数据更新成功");
    updateTimeLabel->setText("最后更新: " + QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));
    addLogMessage("数据获取成功");
}

void Widget::updateUI()
{
    // 更新温度
    tempLabel->setText(QString::number(currentData.temperature, 'f', 1));
    int tempPercent = (currentData.temperature - 0) / 50.0 * 100;
    tempBar->setValue(qBound(0, tempPercent, 100));
    
    // 更新湿度
    humidLabel->setText(QString::number(currentData.humidity, 'f', 1));
    humidBar->setValue(qBound(0, (int)currentData.humidity, 100));
    
    // 更新PM2.5
    pm25Label->setText(QString::number(currentData.pm25, 'f', 1));
    int pm25Percent = qBound(0, (int)(currentData.pm25 / 200.0 * 100), 100);
    pm25Bar->setValue(pm25Percent);
    
    // 更新CO2
    co2Label->setText(QString::number(currentData.co2));
    int co2Percent = qBound(0, currentData.co2 / 2000 * 100, 100);
    co2Bar->setValue(co2Percent);
    
    // 更新TVOC
    tvocLabel->setText(QString::number(currentData.tvoc));
    
    // 更新甲醛
    hchoLabel->setText(QString::number(currentData.hcho, 'f', 3));
    int hchoPercent = qBound(0, (int)(currentData.hcho / 1.0 * 100), 100);
    hchoBar->setValue(hchoPercent);
    
    // 根据数值调整颜色
    auto setLabelColor = [&](QLabel *label, float value, float warning, float danger) {
        if (value > danger || value < 0) {
            label->setStyleSheet("font-size: 32px; font-weight: bold; color: #e74c3c;");
        } else if (value > warning) {
            label->setStyleSheet("font-size: 32px; font-weight: bold; color: #f39c12;");
        } else {
            label->setStyleSheet("font-size: 32px; font-weight: bold; color: #27ae60;");
        }
    };
    
    setLabelColor(tempLabel, currentData.temperature, 30, 35);
    setLabelColor(humidLabel, currentData.humidity, 70, 80);
    setLabelColor(pm25Label, currentData.pm25, 50, 75);
    setLabelColor(co2Label, currentData.co2, 800, 1000);
    setLabelColor(hchoLabel, currentData.hcho, 0.05, 0.08);
}

void Widget::checkAlarmConditions()
{
    bool alarmTriggered = false;
    QString alarmMessages;
    
    // 检查各项参数
    if (currentData.temperature > thresholds.tempMax) {
        alarmTriggered = true;
        alarmMessages += "温度过高(%.1f°C) ";
    }
    if (currentData.temperature < thresholds.tempMin) {
        alarmTriggered = true;
        alarmMessages += "温度过低(%.1f°C) ";
    }
    if (currentData.humidity > thresholds.humidMax) {
        alarmTriggered = true;
        alarmMessages += "湿度过高(%.1f%%) ";
    }
    if (currentData.humidity < thresholds.humidMin) {
        alarmTriggered = true;
        alarmMessages += "湿度过低(%.1f%%) ";
    }
    if (currentData.pm25 > thresholds.pm25Max) {
        alarmTriggered = true;
        alarmMessages += "PM2.5超标(%.1f μg/m³) ";
    }
    if (currentData.co2 > thresholds.co2Max) {
        alarmTriggered = true;
        alarmMessages += "CO₂超标(%d ppm) ";
    }
    if (currentData.hcho > thresholds.hchoMax) {
        alarmTriggered = true;
        alarmMessages += "甲醛超标(%.3f mg/m³) ";
    }
    
    // 更新报警状态
    currentData.alarm = alarmTriggered;
    
    // 更新报警显示
    if (alarmTriggered) {
        alarmLabel->setText(" 报警! " + alarmMessages);
        alarmLabel->setStyleSheet("font-size: 18px; font-weight: bold; padding: 10px; "
                                  "background-color: #e74c3c; color: white; border-radius: 5px; "
                                  "animation: blink 0.5s infinite;");
        
        // 播放报警音效
        if (!alarmSound->isPlaying()) {
            alarmSound->play();
        }
        
        // 弹出报警消息(仅在首次触发时)
        if (!previousAlarmState) {
            showAlarmMessage(alarmMessages);
        }
        
        addLogMessage("⚠️ 报警触发: " + alarmMessages, true);
    } else {
        alarmLabel->setText("✅ 系统运行正常");
        alarmLabel->setStyleSheet("font-size: 18px; font-weight: bold; padding: 10px; "
                                  "background-color: #27ae60; color: white; border-radius: 5px;");
        
        if (alarmSound->isPlaying()) {
            alarmSound->stop();
        }
        
        if (previousAlarmState) {
            addLogMessage("✅ 报警已解除");
        }
    }
    
    previousAlarmState = alarmTriggered;
}

void Widget::showAlarmMessage(const QString &message)
{
    QMessageBox::critical(this, "⚠️ 环境报警!", 
                         "检测到异常环境参数!nn" + message + 
                         "nn请立即采取措施!", 
                         QMessageBox::Ok);
}

void Widget::addDataToTable()
{
    int row = dataTable->rowCount();
    dataTable->insertRow(row);
    
    QString timeStr = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
    dataTable->setItem(row, 0, new QTableWidgetItem(timeStr));
    dataTable->setItem(row, 1, new QTableWidgetItem(QString::number(currentData.temperature, 'f', 1)));
    dataTable->setItem(row, 2, new QTableWidgetItem(QString::number(currentData.humidity, 'f', 1)));
    dataTable->setItem(row, 3, new QTableWidgetItem(QString::number(currentData.pm25, 'f', 1)));
    dataTable->setItem(row, 4, new QTableWidgetItem(QString::number(currentData.co2)));
    dataTable->setItem(row, 5, new QTableWidgetItem(QString::number(currentData.tvoc)));
    dataTable->setItem(row, 6, new QTableWidgetItem(QString::number(currentData.hcho, 'f', 3)));
    dataTable->setItem(row, 7, new QTableWidgetItem(currentData.alarm ? "⚠️ 报警" : "✅ 正常"));
    
    // 设置报警行颜色
    if (currentData.alarm) {
        for (int col = 0; col < dataTable->columnCount(); col++) {
            dataTable->item(row, col)->setBackground(QColor(255, 200, 200));
        }
    }
    
    // 滚动到最新数据
    dataTable->scrollToBottom();
    
    // 限制表格行数,防止内存溢出
    if (dataTable->rowCount() > 1000) {
        dataTable->removeRow(0);
    }
}

void Widget::addLogMessage(const QString &msg, bool isError)
{
    QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss");
    QString formattedMsg = QString("[%1] %2").arg(timestamp).arg(msg);
    
    if (isError) {
        formattedMsg = "<font color='#e74c3c'>" + formattedMsg + "</font>";
    } else {
        formattedMsg = "<font color='#ecf0f1'>" + formattedMsg + "</font>";
    }
    
    logTextEdit->append(formattedMsg);
    
    // 自动滚动到底部
    QTextCursor cursor = logTextEdit->textCursor();
    cursor.movePosition(QTextCursor::End);
    logTextEdit->setTextCursor(cursor);
}

QString Widget::formatDateTime(qint64 timestamp)
{
    if (timestamp == 0) return "--";
    QDateTime dt = QDateTime::fromSecsSinceEpoch(timestamp / 1000);
    return dt.toString("yyyy-MM-dd hh:mm:ss");
}

void Widget::showSettingsDialog()
{
    QDialog dialog(this);
    dialog.setWindowTitle("设置");
    dialog.setModal(true);
    dialog.setMinimumWidth(500);
    
    QFormLayout *formLayout = new QFormLayout(&dialog);
    
    // API配置
    QLineEdit *apiKeyEdit = new QLineEdit(apiKey);
    QLineEdit *deviceIdEdit = new QLineEdit(deviceId);
    QLineEdit *productIdEdit = new QLineEdit(productId);
    
    formLayout->addRow("API Key:", apiKeyEdit);
    formLayout->addRow("设备ID:", deviceIdEdit);
    formLayout->addRow("产品ID:", productIdEdit);
    
    // 阈值设置
    QDoubleSpinBox *tempMaxSpin = new QDoubleSpinBox();
    tempMaxSpin->setRange(0, 60);
    tempMaxSpin->setValue(thresholds.tempMax);
    tempMaxSpin->setSuffix(" °C");
    
    QDoubleSpinBox *tempMinSpin = new QDoubleSpinBox();
    tempMinSpin->setRange(-20, 50);
    tempMinSpin->setValue(thresholds.tempMin);
    tempMinSpin->setSuffix(" °C");
    
    QDoubleSpinBox *humidMaxSpin = new QDoubleSpinBox();
    humidMaxSpin->setRange(0, 100);
    humidMaxSpin->setValue(thresholds.humidMax);
    humidMaxSpin->setSuffix(" %");
    
    QDoubleSpinBox *humidMinSpin = new QDoubleSpinBox();
    humidMinSpin->setRange(0, 100);
    humidMinSpin->setValue(thresholds.humidMin);
    humidMinSpin->setSuffix(" %");
    
    QDoubleSpinBox *pm25Spin = new QDoubleSpinBox();
    pm25Spin->setRange(0, 500);
    pm25Spin->setValue(thresholds.pm25Max);
    pm25Spin->setSuffix(" μg/m³");
    
    QSpinBox *co2Spin = new QSpinBox();
    co2Spin->setRange(0, 5000);
    co2Spin->setValue(thresholds.co2Max);
    co2Spin->setSuffix(" ppm");
    
    QDoubleSpinBox *hchoSpin = new QDoubleSpinBox();
    hchoSpin->setRange(0, 1);
    hchoSpin->setSingleStep(0.001);
    hchoSpin->setDecimals(3);
    hchoSpin->setValue(thresholds.hchoMax);
    hchoSpin->setSuffix(" mg/m³");
    
    formLayout->addRow("温度上限:", tempMaxSpin);
    formLayout->addRow("温度下限:", tempMinSpin);
    formLayout->addRow("湿度上限:", humidMaxSpin);
    formLayout->addRow("湿度下限:", humidMinSpin);
    formLayout->addRow("PM2.5上限:", pm25Spin);
    formLayout->addRow("CO₂上限:", co2Spin);
    formLayout->addRow("甲醛上限:", hchoSpin);
    
    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
    connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
    connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    formLayout->addRow(buttonBox);
    
    if (dialog.exec() == QDialog::Accepted) {
        apiKey = apiKeyEdit->text();
        deviceId = deviceIdEdit->text();
        productId = productIdEdit->text();
        
        thresholds.tempMax = tempMaxSpin->value();
        thresholds.tempMin = tempMinSpin->value();
        thresholds.humidMax = humidMaxSpin->value();
        thresholds.humidMin = humidMinSpin->value();
        thresholds.pm25Max = pm25Spin->value();
        thresholds.co2Max = co2Spin->value();
        thresholds.hchoMax = hchoSpin->value();
        
        saveSettings();
        
        addLogMessage("设置已更新");
        
        if (!apiKey.isEmpty() && !deviceId.isEmpty()) {
            refreshTimer->start();
            fetchDeviceData();
        }
    }
}

// ==================== 槽函数 ====================

void Widget::onRefreshTimer()
{
    fetchDeviceData();
}

void Widget::onRefreshButtonClicked()
{
    addLogMessage("手动刷新数据");
    fetchDeviceData();
}

void Widget::onSettingsButtonClicked()
{
    showSettingsDialog();
}

void Widget::onClearLogButtonClicked()
{
    logTextEdit->clear();
    dataTable->setRowCount(0);
    addLogMessage("日志已清除");
}

// Android平台特殊处理
#ifdef Q_OS_ANDROID
#include <QAndroidJniEnvironment>
#include <QAndroidJniObject>

void Widget::showAlarmMessage(const QString &message)
{
    // Android平台使用Toast显示消息
    QAndroidJniObject javaString = QAndroidJniObject::fromString(message);
    QAndroidJniObject::callStaticMethod<void>("android/widget/Toast",
                                              "makeText",
                                              "(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;",
                                              QtAndroid::androidContext().object(),
                                              javaString.object(),
                                              ione);
    
    // 同时使用QMessageBox
    QMessageBox::critical(this, "⚠️ 环境报警!", 
                         "检测到异常环境参数!nn" + message + 
                         "nn请立即采取措施!", 
                         QMessageBox::Ok);
}
#endif

四、Arduino uno代码设计

4.1 下载安装开发环境

官网下载地址:https://www.arduino.cc/en/software/

双击安装。

软件启动中:

在启动过程中,会弹出一堆安装驱动的说明。全部点击确定就行了。

软件启动成功。

设置中文界面。

重新启动,就是中文界面了。

4.2 选择开发板

根据自己的板子型号进行选择。

这是我的开发板

将开发板连接电脑。

选择开发板的端口。

连接成功了。

将文件保存起来。编写代码,点击编译下载按钮就可以将代码下载到板子上就可以测试了。

下面代码是点亮2个LED灯。

void setup() {
  pinMode(13, OUTPUT);  //LED2
  pinMode(12, OUTPUT);  //LED1
}

void loop() {
  digitalWrite(13, HIGH);
  digitalWrite(12, HIGH);
  delay(500);

  digitalWrite(13, LOW);
  digitalWrite(12, LOW);
  delay(500);
}

4.3 开发板的引脚

开发板右边的013 就是数字口,D0D13.

开发板左边的A0~A5 是模拟口。

4.4 安装Adafruit GFX扩展库

搜索Adafruit GFX

    1. 1. 打开 Arduino IDE。2. 点击菜单栏 ‌**“工具” → “管理库”**‌。3. 在搜索框中输入 ‌**“Adafruit GFX”**‌。4. 找到 ‌**“Adafruit GFX Library”

‌ 并点击 ‌

    “安装”**‌。

安装成功。

4.5 安装Adafruit SSD1306扩展库

    • • 工具 → 管理库 → 搜索

SSD1306

    •  → 安装

Adafruit SSD1306

4.6 OLED显示测试

库安装好了,就可以写代码测试了。

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C    // 使用扫描到的地址

// 创建显示对象
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

void setup() {
  // 初始化 OLED
  if(!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    // 初始化失败,无限循环(可以通过串口查看)
    Serial.begin(9600);
    Serial.println("OLED 初始化失败!");
    for(;;);
  }
  
  display.clearDisplay();           // 清屏
  display.setTextSize(2);           // 字体大小
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(20, 20);        // 位置(x, y)
  display.println("hello");
  display.display();                // 显示到屏幕
}

void loop() {
  // 空循环,不需要重复显示
}

直接编译下载:

运行的效果。

4.7 项目硬件接线说明

硬件接线说明(Arduino Uno R3)

    1. 1. CoreY100P 4G模块
    1. RXD <--------> 引脚3(软串口RX)
    1. TXD <--------> 引脚2(软串口TX)
    1. GND <--------> GND
    1. VCC <--------> 5V2. 0.96寸IIC接口的OLED显示屏
    1. VCC <--------> 5V
    1. GND <--------> GND
    1. SCL <--------> A5(IIC时钟线)
    1. SDA <--------> A4(IIC数据线)3. GPS接线说明(ATGM336H-5N)
    1. GND <--------> GND
    1. VCC <--------> 5V
    1. GPS_TX <-----> 引脚4(软串口RX,接收GPS数据)
    1. (GPS_RX可不接,无需向GPS发送指令)4. MAX30102脉搏心率检测模块
    1. SCL <--------> A5
    1. SDA <--------> A4
    1. INT <--------> 引脚5(可选,用于数据就绪中断)
    1. VCC <--------> 3.3V
    1. GND <--------> GND5. MLX90614体温检测模块
    1. GND <--------> GND
    1. VCC <--------> 5V
    1. SDA <--------> A4
    1. SCL <--------> A56. MPU6050六轴陀螺仪
    1. VCC <--------> 5V
    1. GND <--------> GND
    1. SDA <--------> A4
    1. SCL <--------> A5
    1. MPU_INT <----> 未接
    1. MPU_AD0 <----> GND(地址为0x68)7. 蜂鸣器(有源高电平触发)
    1. GND <--------> GND
    1. VCC <--------> 5V
    1. OUT <--------> 引脚6(高电平蜂鸣器响)8. 板载LED灯(开发板自带,无需外接)
    1. LED1 --- 运行指示灯(可用引脚13)
    1. LED2 --- 数据上传指示灯(可用引脚12)
    1. LED3 --- 报警指示灯(可用引脚11)9. 按键接线(按下为低电平,需启用内部上拉或外接上拉电阻
    1. SW1(翻页) ---------> 引脚7
    1. SW2(清除报警) -----> 引脚8
    1. SW3(手动SOS报警) --> 引脚9
    1. SW4(报警模式开关) --> 引脚10
    1. (公共端均接GND)10. 光敏传感器:BH1750
    1. VCC <--------> 5V
    1. GND <--------> GND
    1. SCL <--------> A5
    1. SDA <--------> A411. 温湿度传感器:DHT11
    1. VCC <--------> 5V
    1. GND <--------> GND
  1. DATA <-------> 引脚14(A0,模拟口可作数字口使用)

引脚汇总:

    • IIC总线(共用):A4(SDA)、A5(SCL)• 4G模块软串口:引脚2(TX)、引脚3(RX)• GPS软串口:引脚4(RX)• MAX30102中断:引脚5• 蜂鸣器:引脚6• 按键:引脚7、8、9、10• 指示灯(可选):引脚11、12、13• DHT11:引脚14(A0)

4.8 代码设计

项目开发使用的全部软件工具、代码已经上传到网盘:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink

/*
 * 基于Arduino Uno的室内环境监控系统
 * 功能:温湿度(SHT30)、PM2.5(夏普)、CO2(SGP30)、甲醛(MS1100)
 * 显示:0.96寸OLED (I2C)
 * 通信:ESP8266 (串口) + OneNet云平台 (MQTT)
 * 报警:有源蜂鸣器
 */

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <SHT30.h>
#include <SGP30.h>

// ===================== 引脚定义 =====================
#define BUZZER_PIN     3      // 蜂鸣器控制引脚
#define MS1100_PIN     A0     // 甲醛传感器模拟输入
#define PM25_PIN       A1     // PM2.5传感器模拟输入(夏普GP2Y1010AU0F)

// ===================== OLED配置 =====================
#define SCREEN_WIDTH   128
#define SCREEN_HEIGHT  64
#define OLED_RESET     -1     // 复位引脚(-1表示共享复位)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ===================== 传感器对象 =====================
SHT30 sht30;                 // 温湿度传感器
SGP30 sgp30;                 // CO2和TVOC传感器

// ===================== 系统参数 =====================
// 报警阈值
#define TEMP_MAX        35.0   // 最高温度 (°C)
#define TEMP_MIN        10.0   // 最低温度 (°C)
#define HUMID_MAX       80.0   // 最高湿度 (%)
#define HUMID_MIN       30.0   // 最低湿度 (%)
#define PM25_MAX        75.0   // PM2.5最大值 (μg/m³)
#define CO2_MAX         1000   // CO2最大值 (ppm)
#define HCHO_MAX        0.08   // 甲醛最大值 (mg/m³)

// 数据上传间隔 (毫秒)
#define UPLOAD_INTERVAL 5000

// ===================== 全局变量 =====================
// 传感器数据
float temperature = 0.0;
float humidity = 0.0;
float pm25 = 0.0;
uint16_t co2 = 0;
uint16_t tvoc = 0;
float hcho = 0.0;

// 报警标志
bool alarmTemp = false;
bool alarmHumid = false;
bool alarmPM25 = false;
bool alarmCO2 = false;
bool alarmHCHO = false;
bool overallAlarm = false;

// 时间控制
unsigned long lastUploadTime = 0;
unsigned long lastSensorReadTime = 0;

// ESP8266串口通信缓冲区
String esp8266Buffer = "";
bool esp8266Ready = false;

// ===================== 函数声明 =====================
void readSHT30();
void readSGP30();
float readPM25();
float readMS1100();
void updateDisplay();
void checkAlarms();
void triggerAlarm(bool on);
void sendToESP8266(String command);
void processESP8266Response();
void connectWiFi();
void sendMQTTData();

// ===================== 初始化 =====================
void setup() {
  Serial.begin(9600);          // Arduino与PC通信
  Serial1.begin(115200);       // Arduino与ESP8266通信 (RX=0, TX=1)
  
  // 初始化引脚
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);
  
  // 初始化OLED
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("OLED初始化失败!"));
    while(1);
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println(F("系统启动中..."));
  display.display();
  
  // 初始化SHT30
  Wire.begin();
  if(!sht30.begin()) {
    Serial.println(F("SHT30初始化失败!"));
  }
  
  // 初始化SGP30
  if(!sgp30.begin()) {
    Serial.println(F("SGP30初始化失败!"));
  } else {
    // SGP30需要初始化基线
    sgp30.measureAirQuality();
  }
  
  // 配置ESP8266为STA+TCP客户端模式
  delay(2000);
  connectWiFi();
  
  delay(1000);
  display.clearDisplay();
  display.setCursor(0, 0);
  display.println(F("系统就绪"));
  display.display();
  delay(1000);
}

// ===================== 主循环 =====================
void loop() {
  unsigned long currentMillis = millis();
  
  // 每2秒读取一次传感器
  if(currentMillis - lastSensorReadTime >= 2000) {
    lastSensorReadTime = currentMillis;
    
    // 读取各传感器
    readSHT30();
    readSGP30();
    pm25 = readPM25();
    hcho = readMS1100();
    
    // 检查报警
    checkAlarms();
    
    // 更新OLED显示
    updateDisplay();
    
    // 串口输出调试信息
    Serial.print(F("T:")); Serial.print(temperature);
    Serial.print(F(" H:")); Serial.print(humidity);
    Serial.print(F(" PM:")); Serial.print(pm25);
    Serial.print(F(" CO2:")); Serial.print(co2);
    Serial.print(F(" HCHO:")); Serial.println(hcho);
  }
  
  // 每5秒上传一次数据到OneNet
  if(currentMillis - lastUploadTime >= UPLOAD_INTERVAL) {
    lastUploadTime = currentMillis;
    sendMQTTData();
  }
  
  // 处理ESP8266响应
  processESP8266Response();
  
  // 控制蜂鸣器(报警持续鸣响,无报警则关闭)
  triggerAlarm(overallAlarm);
}

// ===================== 传感器读取函数 =====================

// 读取SHT30温湿度
void readSHT30() {
  if(sht30.update()) {
    temperature = sht30.getTemperature();
    humidity = sht30.getHumidity();
  }
}

// 读取SGP30 CO2和TVOC
void readSGP30() {
  if(sgp30.measureAirQuality()) {
    co2 = sgp30.CO2;
    tvoc = sgp30.TVOC;
  }
}

// 读取PM2.5 (夏普GP2Y1010AU0F)
float readPM25() {
  // 夏普传感器: 开启内部LED
  pinMode(PM25_PIN, OUTPUT);
  digitalWrite(PM25_PIN, LOW);
  delayMicroseconds(280);
  
  // 读取ADC值
  int adcValue = analogRead(PM25_PIN);
  
  // 关闭LED
  digitalWrite(PM25_PIN, HIGH);
  delayMicroseconds(40);
  
  // 转换为电压 (mV)
  float voltage = adcValue * (5000.0 / 1023.0);
  
  // 根据夏普传感器特性曲线转换为粉尘浓度 (μg/m³)
  // 参考: 输出电压 vs 粉尘浓度关系
  if(voltage < 600) {
    return 0;
  } else {
    // 近似公式: 浓度 = (电压 - 600) / 5.0
    return (voltage - 600.0) / 5.0;
  }
}

// 读取MS1100 VOC传感器 (甲醛)
float readMS1100() {
  // 读取ADC值
  int adcValue = analogRead(MS1100_PIN);
  
  // 转换为电压 (V)
  float voltage = adcValue * (5.0 / 1023.0);
  
  // 传感器特性曲线: 0-5V对应0-1mg/m³甲醛 (根据实际传感器标定)
  // 注意: MS1100是VOC传感器,对甲醛灵敏度较低,此转换仅为近似
  float hchoValue = voltage * 0.2;  // 0-5V -> 0-1.0 mg/m³
  
  // 限制范围
  if(hchoValue < 0) hchoValue = 0;
  if(hchoValue > 1.0) hchoValue = 1.0;
  
  return hchoValue;
}

// ===================== OLED显示 =====================
void updateDisplay() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  
  // 第一行:温湿度
  display.setCursor(0, 0);
  display.print(F("T:")); display.print(temperature, 1); display.print((char)247); display.print("C ");
  display.print(F("H:")); display.print(humidity, 1); display.println("%");
  
  // 第二行:PM2.5
  display.setCursor(0, 16);
  display.print(F("PM2.5: ")); display.print(pm25, 1); display.println(" ug/m3");
  
  // 第三行:CO2
  display.setCursor(0, 32);
  display.print(F("CO2: ")); display.print(co2); display.println(" ppm");
  
  // 第四行:甲醛 + 报警状态
  display.setCursor(0, 48);
  display.print(F("HCHO: ")); display.print(hcho, 3); display.print(F(" mg"));
  
  if(overallAlarm) {
    display.print(F("  ALARM!"));
  }
  
  display.display();
}

// ===================== 报警检测 =====================
void checkAlarms() {
  alarmTemp = (temperature > TEMP_MAX || temperature < TEMP_MIN);
  alarmHumid = (humidity > HUMID_MAX || humidity < HUMID_MIN);
  alarmPM25 = (pm25 > PM25_MAX);
  alarmCO2 = (co2 > CO2_MAX);
  alarmHCHO = (hcho > HCHO_MAX);
  
  overallAlarm = alarmTemp || alarmHumid || alarmPM25 || alarmCO2 || alarmHCHO;
}

// ===================== 蜂鸣器控制 =====================
void triggerAlarm(bool on) {
  if(on) {
    digitalWrite(BUZZER_PIN, HIGH);
  } else {
    digitalWrite(BUZZER_PIN, LOW);
  }
}

// ===================== ESP8266通信 =====================

// 连接WiFi热点
void connectWiFi() {
  display.clearDisplay();
  display.setCursor(0, 0);
  display.println(F("连接WiFi..."));
  display.display();
  
  // 发送AT指令配置ESP8266
  sendToESP8266("AT+RSTrn");
  delay(3000);
  
  sendToESP8266("AT+CWMODE=1rn");  // STA模式
  delay(1000);
  
  // 替换为你的WiFi热点名称和密码
  String cmd = "AT+CWJAP="你的WiFi名称","你的WiFi密码"rn";
  sendToESP8266(cmd);
  delay(5000);
  
  // 查询IP地址
  sendToESP8266("AT+CIFSRrn");
  delay(1000);
  
  // 设置MQTT连接参数 (OneNet)
  // 实际MQTT连接在sendMQTTData中通过AT指令建立
  display.clearDisplay();
  display.setCursor(0, 0);
  display.println(F("WiFi已连接"));
  display.display();
}

// 发送数据到ESP8266 (MQTT上传到OneNet)
void sendMQTTData() {
  // 构建JSON数据负载
  String payload = "{";
  payload += ""temperature":" + String(temperature, 2) + ",";
  payload += ""humidity":" + String(humidity, 2) + ",";
  payload += ""pm25":" + String(pm25, 2) + ",";
  payload += ""co2":" + String(co2) + ",";
  payload += ""tvoc":" + String(tvoc) + ",";
  payload += ""hcho":" + String(hcho, 4) + ",";
  payload += ""alarm":" + String(overallAlarm ? 1 : 0);
  payload += "}";
  
  // 通过MQTT发布到OneNet
  // 注意: 需要替换为你的OneNet设备信息
  String topic = "$sys/你的产品ID/你的设备名称/thing/property/report";
  String mqttCmd = "AT+MQTTPUB=" + topic + "," + payload + ",0,0rn";
  
  sendToESP8266(mqttCmd);
  
  // 串口打印上传数据
  Serial.println(F("上传数据: " + payload));
}

// 发送AT指令到ESP8266
void sendToESP8266(String command) {
  Serial1.print(command);
  Serial.println(F("发送: " + command));
}

// 处理ESP8266响应
void processESP8266Response() {
  while(Serial1.available()) {
    char c = Serial1.read();
    esp8266Buffer += c;
    if(c == 'n') {
      // 处理一行响应
      if(esp8266Buffer.indexOf("OK") >= 0 || esp8266Buffer.indexOf("SEND OK") >= 0) {
        esp8266Ready = true;
      }
      Serial.print(F("ESP8266: "));
      Serial.print(esp8266Buffer);
      esp8266Buffer = "";
    }
  }
}

1. 传感器驱动

• SHT30:使用I2C协议读取温湿度•

SGP30:使用I2C协议读取CO2和TVOC•

夏普PM2.5:通过ADC读取模拟电压,转换为粉尘浓度•

MS1100:通过ADC读取模拟电压,转换为甲醛浓度(需实际标定)

2. 数据上传

    • 每5秒通过ESP8266以MQTT协议上传数据到OneNet云平台• 数据格式为JSON,包含所有传感器值和报警状态

3. 报警机制

    • 各参数超限时触发报警• 蜂鸣器持续鸣响• 报警状态随数据上传到云端

4. 注意事项

需要修改的配置

// WiFi配置
String cmd = "AT+CWJAP="你的WiFi名称","你的WiFi密码"rn";

// OneNet MQTT配置
String topic = "$sys/你的产品ID/你的设备名称/thing/property/report";

传感器校准

    • MS1100甲醛传感器的转换公式需要根据实际标定调整• PM2.5传感器的转换系数可能需要根据传感器型号微调

ESP8266固件
确保ESP8266烧录了支持MQTT的AT固件(安信可的MQTT固件)

引脚连接

    • SHT30/SGP30/OLED:I2C总线 (A4=SDA, A5=SCL)• ESP8266:串口1 (TX=1, RX=0)• 蜂鸣器:D3• MS1100:A0• PM2.5:A1

项目开发使用的全部软件工具已经上传到网盘:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink

相关推荐