一、前言
1.1 项目介绍
【1】项目开发背景
随着工业化进程的加快和人类活动的日益频繁,水资源污染问题已成为全球关注的焦点。传统的水质监测方式主要依赖于人工采样和实验室分析,这种方法不仅周期长、效率低,而且难以实现对广阔水域的实时、连续监测,尤其是在一些环境复杂、人员难以进入的区域,传统监测手段的局限性更加凸显。因此,开发一种能够灵活移动、实时采集并上传多项水质参数的自动化监测系统,对于环境保护、水产养殖以及水资源管理等领域具有重要的现实意义。
近年来,嵌入式技术、物联网通信以及智能终端技术的快速发展,为实现低成本、高效率的水质监测提供了新的思路。通过将多种水质传感器与微型控制器相结合,可以构建一个小型化的水质检测平台。然而,如何将这些传感器集成在一个可移动的载体上,并实现对水温、pH值、浊度、溶解氧等关键指标的同步采集,同时保证数据的实时传输与远程可视化,成为了一个具有挑战性的课题。特别是针对溶解氧这类需要复杂接口通信的传感器,以及浊度、pH值等需要精确模拟量采集的传感器,对硬件系统的设计与软件算法的实现都提出了较高的要求。
物联网技术的应用为水质监测带来了革命性的变化,它使得监测设备能够作为云端的感知节点,将采集到的数据通过无线网络实时上传至服务器。采用MQTT协议与华为云物联网平台对接,不仅保证了数据传输的可靠性,还为后续的数据存储、分析与预警提供了强大的平台支持。在此基础上,开发跨平台的移动应用和桌面应用,能够使用户无论身处何地,都可以通过手机或电脑实时查看水质数据、接收报警信息,从而构建一个从数据采集、传输到展示与控制的完整闭环系统。这大大提升了水质监测的时效性和便捷性,实现了从“被动采样”到“主动监控”的转变。
为了进一步提升监测系统的灵活性与适用范围,本项目创新性地将水质检测系统与小型无人船相结合。通过蓝牙无线遥控的方式,操作人员可以控制小船在水面自由移动,对指定区域进行定点或巡航式的水质数据采集。这种设计不仅解决了固定监测点覆盖范围有限的问题,也避免了人员直接接触污染水源的风险。小船的动力系统由电机和舵机组成,结构简单可靠,配合集成的传感器阵列,能够在水面上稳定航行并完成数据采集任务,尤其适用于湖泊、河流、池塘等中小型水域的日常巡查和应急监测。
综上所述,本项目旨在设计并实现一套基于STM32的便携式污水检测船系统。该系统集成了多种水质传感器,通过寄存器级别的硬件编程实现对底层设备的精确控制,并利用Wi-Fi模块将数据上传至华为云物联网平台。同时,采用Qt框架开发适配Android和Windows双端的远程监控软件,不仅能够实时显示和报警,还支持云端参数的动态设置。整套系统结合了嵌入式技术、物联网通信、移动应用开发以及无人船控制技术,旨在为中小型水域的水质监测提供一种高效、智能、便捷的一体化解决方案。
【2】设计实现的功能
(1)支持水温检测:通过DS18B20防水型温度传感器采集水体温度数据。
(2)支持PH值检测:通过PH值检测传感器采集模拟电压信号,经算法转换为真实PH值。
(3)支持浊度检测:通过浊度检测传感器采集模拟电压信号,经算法转换为真实浊度值。
(4)支持溶解氧检测:通过溶解氧检测传感器采集数据,传感器输出485接口信号,经485转串口模块与单片机连接,单片机通过串口协议读取溶解氧数据。
(5)支持OLED显示屏显示:采用0.96寸IIC协议接口的OLED显示屏,将采集的水温、PH值、浊度、溶解氧数据实时显示出来。
(6)支持数据上云:采集的全部数据通过ESP8266 Wi-Fi模块,利用MQTT协议上传到华为云物联网服务器。
(7)支持Android手机APP远程显示:通过Qt(C++)设计的Android手机APP,远程显示设备上传的水温、PH值、浊度、溶解氧数据。
(8)支持Windows上位机远程显示:通过Qt(C++)设计的Windows电脑上位机软件,远程显示设备上传的水温、PH值、浊度、溶解氧数据。
(9)支持云端设置参数上下限:通过云端设置水温、PH值、浊度、溶解氧的参数上下限阈值。
(10)支持本地声光报警:当水温、PH值、浊度、溶解氧的检测值超过云端设置的阈值范围时,本地设备蜂鸣器发出报警声。
(11)支持APP和上位机报警提示:当检测数据超过云端设置的阈值范围时,Android手机APP和Windows上位机界面上的对应数据显示为红色,进行报警提示。
(12)支持小船遥控控制:通过蓝牙方式控制小船的前进、后退及转向动作,实现小船的移动控制。
(13)支持移动式水质采集:水质检测系统安装在小船上,通过遥控控制小船进入水域前行,在不同位置进行水质数据采集。
项目开发使用的全部软件工具已经上传到网盘:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
【3】项目硬件模块组成
(1)主控模块:采用STM32F103C8T6作为主控芯片,负责整个系统的数据处理、传感器采集、通信控制和逻辑运算。
(2)水温检测模块:采用DS18B20防水型温度传感器,用于采集水体温度数据。
(3)PH值检测模块:采用PH值检测传感器,输出模拟电压信号,经单片机ADC采集后通过算法转换为真实PH值。
(4)浊度检测模块:采用浊度检测传感器,输出模拟电压信号,经单片机ADC采集后通过算法转换为真实浊度值。
(5)溶解氧检测模块:采用溶解氧检测传感器,输出485接口信号,通过485转串口模块转换为TTL电平后与单片机串口连接,单片机通过串口协议读取溶解氧数据。
(6)显示模块:采用0.96寸OLED显示屏,通信协议为IIC接口,用于实时显示采集的水温、PH值、浊度、溶解氧数据。
(7)无线通信模块:采用ESP8266 Wi-Fi模块,通过MQTT协议将采集的数据上传至华为云物联网服务器。
(8)报警模块:采用高电平触发的蜂鸣器,当检测数据超过云端设置的阈值范围时,蜂鸣器发出报警提示音。
(9)小船动力模块:采用1个5V直流电机作为动力驱动,提供小船前进和后退的推力。
(10)小船转向模块:采用1个SG90舵机控制船舵方向,实现小船的转向控制。
(11)遥控通信模块:采用蓝牙模块,用于接收手机或遥控器的控制指令,实现对小船前进、后退及转向的无线遥控。
(12)电源模块:为各硬件模块提供稳定的工作电压,包括STM32主控、传感器、电机、舵机、Wi-Fi模块及蓝牙模块等。
(13)传感器接口模块:包括PH值传感器和浊度传感器的模拟信号输入接口、DS18B20温度传感器的单总线接口、溶解氧传感器的串行通信接口,用于连接各传感器与主控芯片。
【4】设计意义
推动水质监测手段的智能化与自动化
传统的水质监测方式主要依赖人工采样和实验室分析,存在采样周期长、覆盖范围有限、时效性差等问题。本项目设计的便携式污水检测船将水质检测系统与无人船技术相结合,实现了对水体的移动式、自动化数据采集。通过集成水温、PH值、浊度、溶解氧等多种传感器,系统能够一次性获取多项关键水质指标,大大提高了监测效率和数据的全面性,推动了水质监测手段向智能化、自动化方向发展。
提升水域监测的安全性与灵活性
在传统监测模式中,工作人员需要亲自前往采样点进行水样采集,尤其在一些污染严重或环境恶劣的水域,存在一定的安全风险。本项目采用蓝牙遥控的小船作为检测载体,操作人员可以在岸边安全区域控制小船进入目标水域进行数据采集,避免了人员直接接触污染水源的风险。同时,小船具有良好的机动性,能够对湖泊、河流、池塘等不同水域进行定点或巡航式监测,极大提升了监测的灵活性和覆盖范围。
实现水质数据的实时传输与远程监控
本项目通过ESP8266 Wi-Fi模块和MQTT协议,将采集的水质数据实时上传至华为云物联网服务器,构建了一个完整的物联网数据链路。在此基础上,利用Qt框架开发的Android手机APP和Windows上位机软件,能够远程显示水温、PH值、浊度、溶解氧等实时数据。这使得管理人员无论身处何地,都可以随时掌握水质状况,实现了从现场采集到远程监控的无缝对接,大幅提升了水质监管的时效性和便捷性。
构建智能化的异常预警与响应机制
本项目支持通过云端动态设置各项水质参数的上下限阈值。当检测数据超出设定范围时,本地设备的蜂鸣器会立即发出报警声,同时手机APP和上位机界面上对应的数据会显示为红色,实现多终端同步报警。这种智能化的预警机制能够及时发现水质异常情况,为管理人员快速响应和处理问题提供了有力支持,有效降低因水质恶化而带来的环境和健康风险。
提供跨平台的可视化监控解决方案
考虑到不同用户的使用场景和习惯,本项目采用Qt框架进行应用开发,实现了Android手机APP和Windows上位机两个版本。这种跨平台的设计方案,既满足了移动端便捷查看的需求,也兼顾了PC端集中监控的应用场景。用户可以根据实际需要选择合适的终端进行数据查看和管理,体现了系统的实用性和人性化设计理念。
促进嵌入式技术与物联网的深度融合
本项目采用STM32F103C8T6作为主控芯片,采用寄存器方式的硬件编程,实现了对多种传感器、显示模块、通信模块的精确控制。同时,通过MQTT协议对接华为云物联网平台,完成了设备端与云端的数据交互。这种嵌入式系统与物联网技术的深度融合,不仅验证了相关技术在环境监测领域的可行性,也为类似物联网终端设备的开发提供了可参考的技术方案。
降低水质监测的综合成本
相比传统的水质监测方式需要投入大量人力和设备成本,本项目设计的便携式污水检测船具有成本较低、部署灵活、操作简便等优势。系统采用模块化设计,各硬件模块可根据实际需求进行选择和替换,维护方便。同时,通过自动化的数据采集和远程传输,减少了人工采样和实验室检测的环节,有效降低了水质监测的综合成本,具有良好的经济性和推广价值。
【5】市面上同类产品研究现状
目前市场上较为成熟的便携式水质检测仪产品以美国哈希(HACH)公司为代表。哈希公司推出的HQ系列便携式多参数水质分析仪,能够同时检测pH值、溶解氧、电导率、温度等多个水质参数。该系列产品采用数字化智能传感器,测量精度高、响应速度快,广泛用于环保监测、水处理、工业用水等领域。但其价格昂贵,单台设备售价通常在数万元人民币以上,且不具备无线数据传输和远程监控功能,数据记录主要依靠设备本地存储,需要人工导出分析。
另一款具有代表性的产品是日本堀场(HORIBA)公司的U-50系列多参数水质监测仪。该产品能够同时检测多达11项水质参数,包括pH、溶解氧、浊度、温度、盐度等,采用一体化探头设计,防水性能优异,适合野外现场作业。该设备同样存在价格较高的问题,且不具备物联网数据上传能力,数据查看需要通过设备自带显示屏或连接电脑导出,无法实现远程实时监控。
国内市场上,青岛水德仪器有限公司等企业推出了在线式水质监测浮标系统。这类产品将多种水质传感器集成在浮标平台上,通过太阳能供电和GPRS/4G无线通信,将采集的数据实时上传至云端服务器,用户可以通过网页或手机APP查看水质数据。例如,该公司推出的WQM-100型水质监测浮标,可监测水温、pH、溶解氧、浊度、叶绿素等参数,广泛应用于湖泊、水库、河流的长期在线监测。这类产品的优势在于实现了数据实时上传和远程查看,但浮标系统体积较大,部署后位置固定,无法实现移动式巡检,且整套系统成本较高,通常在几万元至十几万元不等。
在无人船水质监测领域,珠海云洲智能科技股份有限公司是国内较为知名的企业。该公司推出了多款无人船水质监测产品,如“海豚1号”水质监测无人船,搭载了多参数水质传感器,能够实现自主航行、定点采样、水质实时监测等功能,通过4G网络将数据上传至云平台。该产品适用于环保部门对重点水域的日常巡查和应急监测,具有自动化程度高、监测范围广等优点。但其体积较大,通常用于专业环保部门,价格昂贵,一般用户难以承担。
此外,上海澄峰科技股份有限公司也推出了无人船水质监测系统,主要面向环保监测、水利水文等领域。其无人船产品可搭载多参数水质传感器、采水装置等,实现自动化走航监测和定点采样。这类专业级无人船系统同样存在价格较高、操作需要专业培训等问题,难以在中小型用户中普及推广。
在开源硬件社区中,也存在一些DIY水质监测项目。例如,基于Arduino平台的水质监测系统,用户可以通过pH传感器、温度传感器、浊度传感器等搭建简易的水质检测装置,配合ESP8266模块将数据上传至Thingspeak等物联网平台。这类项目成本较低,适合爱好者或教育用途,但通常存在传感器精度不高、系统稳定性较差、缺乏专业校准等问题,难以满足实际应用中对数据准确性和可靠性的要求。
在无人船控制方面,也有一些开源项目如基于Arduino或Raspberry Pi的遥控船,用户可以通过手机APP或遥控器控制小船航行,配合GPS实现自动巡航。但这些项目通常侧重于船体控制功能,水质检测模块的集成度不高,或者传感器种类较少,难以实现对多项水质参数的综合监测。
综合来看,目前市面上的水质监测产品主要存在以下几方面局限性:
1. 专业级设备价格高昂:如哈希、堀场等进口品牌便携式水质检测仪,以及云洲、澄峰等专业无人船系统,价格普遍在数万元至数十万元,超出普通用户或小型企业的承受能力。
2. 功能单一:传统便携式水质检测仪虽然测量精度高,但缺乏物联网数据传输功能,无法实现远程监控和实时报警;而在线式浮标系统虽然具备数据上云能力,但部署后位置固定,无法移动监测。
3. 集成度不高:开源DIY类项目虽然成本低,但往往只实现部分功能,要么只有水质检测缺少移动控制,要么只有船体控制缺少完整的多参数水质监测,且传感器精度和系统稳定性难以保证。
4. 操作复杂:部分专业设备需要经过专业培训才能操作,不利于普及推广。
相较于上述现有产品,本项目设计的基于STM32的便携式污水检测船,定位于一种低成本、多功能、易操作的集成化解决方案。项目将水质检测、物联网通信、移动遥控三大功能有机结合,既实现了水温、pH值、浊度、溶解氧等多参数水质检测,又通过Wi-Fi和MQTT协议完成了数据上云与远程监控,同时采用蓝牙遥控小船实现移动式数据采集。整套系统成本低廉,适合中小型水域的日常监测需求,为环保爱好者、小型养殖户、基层环保部门等用户提供了一种性价比高、功能全面的水质监测方案。
【6】摘要
随着我国工业化进程的加快和城市化水平的不断提高,水资源污染问题日益严峻,传统的水质监测手段存在采样周期长、覆盖范围有限、时效性差等弊端,难以满足现代水资源管理和环境保护的需求。针对这一问题,本文设计并实现了一套基于STM32的便携式污水检测船系统。该系统以STM32F103C8T6为主控芯片,集成了水温检测传感器、PH值检测传感器、浊度检测传感器和溶解氧检测传感器,能够实时采集多项水质参数。采集的数据通过0.96寸OLED显示屏进行本地显示,同时通过ESP8266 Wi-Fi模块和MQTT协议上传至华为云物联网服务器。系统支持通过云端动态设置各项参数的上下限阈值,当检测数据超出阈值范围时,本地蜂鸣器发出报警,同时手机APP和上位机界面上的对应数据显示红色进行预警。此外,系统采用蓝牙遥控方式控制小船的前进、后退和转向,使水质检测装置能够在水面上移动式采集不同位置的水质数据。在软件开发方面,硬件端采用C语言和寄存器方式进行编程,Android手机APP和Windows上位机软件均采用Qt框架和C++语言开发,实现了跨平台的数据远程监控。实际测试表明,该系统运行稳定、操作便捷、数据采集准确,能够有效实现移动式水质监测和远程实时预警,为中小型水域的水质监测提供了一种低成本、高效率的解决方案。
关键字
STM32;水质检测;无人船;物联网;MQTT;华为云;Qt;远程监控;PH值;溶解氧;浊度;水温;蓝牙遥控
1.2 设计思路
整体架构设计思路
本项目的整体设计遵循模块化、可扩展、低成本的原则,将系统划分为数据采集层、数据处理层、数据传输层、终端应用层和移动控制层五个层次。数据采集层负责通过各类传感器获取水质参数,包括水温、PH值、浊度、溶解氧等;数据处理层以STM32F103C8T6为核心,完成传感器数据读取、转换、存储和本地显示;数据传输层通过ESP8266 Wi-Fi模块和MQTT协议将数据上传至华为云物联网服务器;终端应用层采用Qt框架开发的Android手机APP和Windows上位机软件,实现数据的远程显示和阈值设置;移动控制层通过蓝牙模块接收遥控指令,控制小船的动力电机和舵机,实现小船的移动控制。各模块之间通过明确的接口进行通信,既保证了系统的独立性,又便于后续的功能扩展和维护。
数据采集与处理思路
针对不同类型传感器的工作特性,本系统设计了差异化的数据采集方案。PH值传感器和浊度传感器输出模拟电压信号,采用STM32的ADC模块进行模拟量采集,通过多次采样取平均值的方法提高测量稳定性,再通过校准算法将电压值转换为真实的PH值和浊度值。DS18B20温度传感器采用单总线通信协议,通过严格的时序控制实现温度数据的读取,该传感器采用防水封装,适合水下环境使用。溶解氧传感器输出485接口信号,通过485转串口模块转换为TTL电平,STM32通过串口通信协议发送查询指令并接收传感器返回的数据,按照厂家提供的通信协议解析出溶解氧含量。所有采集的数据经过处理后,一方面送OLED显示屏进行本地显示,另一方面打包成MQTT协议格式准备上传。
本地显示与报警设计思路
本地显示模块选用0.96寸IIC接口的OLED显示屏,该显示屏具有功耗低、体积小、显示清晰的特点。软件设计上,将采集到的水温、PH值、浊度、溶解氧数据按照固定格式刷新显示在屏幕上,考虑到屏幕尺寸有限,采用分页或精简布局的方式清晰展示各项数据。报警功能采用高电平触发的蜂鸣器实现,系统实时将采集的数据与云端设置的阈值进行比较,当水温、PH值、浊度、溶解氧中任意一项超过设定的上下限范围时,控制蜂鸣器发出报警提示音,提醒现场操作人员注意水质异常情况。
物联网通信与云平台设计思路
本项目的物联网通信部分选用华为云物联网平台作为MQTT服务器,ESP8266模块作为Wi-Fi通信设备。软件设计上,首先对ESP8266进行AT指令配置,使其连接至指定Wi-Fi网络,然后建立与华为云MQTT服务器的连接。按照华为云平台的接入要求,配置设备ID、密钥等认证信息,实现设备注册和登录。数据上报采用MQTT协议的消息发布机制,将采集的水温、PH值、浊度、溶解氧数据按照平台要求的JSON格式封装,发布到对应的主题。阈值设置方面,系统订阅云端下发的阈值配置主题,当用户在APP或上位机上修改阈值时,云端会将新的阈值下发至设备端,STM32接收到阈值更新指令后,更新本地存储的阈值参数,并以此为依据进行后续的报警判断。
跨平台应用设计思路
Android手机APP和Windows上位机软件均采用Qt框架和C++语言进行开发,实现代码的高度复用。软件设计上采用MVC架构,界面层负责数据显示和用户交互,业务逻辑层负责与华为云平台的MQTT通信、数据处理和报警判断。软件启动后,通过MQTT协议连接华为云物联网平台,订阅设备上报的数据主题,实时接收并显示水温、PH值、浊度、溶解氧数据。用户可以通过界面设置各项参数的上下限阈值,设置完成后通过MQTT发布消息将新阈值下发至设备端。当接收到的数据超出阈值范围时,对应数据显示为红色,实现可视化报警。两个版本的应用在功能上保持一致,仅在界面布局和交互方式上针对不同平台的特点进行了适当调整。
小船移动控制设计思路
小船移动控制系统由蓝牙模块、STM32主控芯片、5V直流电机和SG90舵机组成。蓝牙模块负责接收来自手机或遥控器的控制指令,STM32通过串口接收指令后解析控制意图,分别控制电机和舵机执行相应动作。电机控制方面,通过PWM信号调节电机转速,实现小船的前进和后退,改变PWM占空比可以调节航速;舵机控制方面,通过输出不同占空比的PWM信号控制舵机转动的角度,从而改变船舵的方向,实现左转、右转和直行。为保证小船在水中稳定航行,控制程序设计了平滑加减速和舵机回正机制,避免因指令突变导致船体失控。水质检测系统固定安装在小船上,操作人员可以在岸边通过蓝牙遥控小船进入目标水域,在不同位置进行水质数据采集,实现移动式监测。
软硬件协同设计思路
本项目在软硬件设计上注重协同配合。硬件选型阶段充分考虑了软件实现的可行性,如选择IIC接口的OLED显示屏便于驱动开发,选择高电平触发的蜂鸣器简化控制逻辑,选择485转串口模块降低溶解氧传感器的通信复杂度。软件编程采用寄存器方式,直接操作STM32的底层寄存器实现对GPIO、ADC、串口、定时器等外设的控制,这种方式虽然开发难度较高,但能够精确控制系统时序,提高代码执行效率,充分发挥硬件性能。同时,软件设计中增加了异常处理机制,如传感器读取超时、Wi-Fi断线重连、MQTT连接状态监测等,提高系统在复杂环境下的稳定运行能力。
1.3 系统功能总结
| 功能模块 | 功能编号 | 功能描述 |
|---|---|---|
| 水质检测功能 | 1 | 支持水温检测:通过DS18B20防水型温度传感器采集水体温度数据 |
| 2 | 支持PH值检测:通过PH值检测传感器采集模拟电压信号,经算法转换为真实PH值 | |
| 3 | 支持浊度检测:通过浊度检测传感器采集模拟电压信号,经算法转换为真实浊度值 | |
| 4 | 支持溶解氧检测:通过溶解氧检测传感器采集数据,经485转串口模块与单片机连接,通过串口协议读取溶解氧数据 | |
| 本地显示功能 | 5 | 支持OLED显示屏显示:采用0.96寸IIC协议接口OLED显示屏,实时显示水温、PH值、浊度、溶解氧数据 |
| 物联网通信功能 | 6 | 支持数据上云:通过ESP8266 Wi-Fi模块,利用MQTT协议将采集数据上传至华为云物联网服务器 |
| 远程监控功能 | 7 | 支持Android手机APP远程显示:通过Qt(C++)设计的Android手机APP,远程显示设备上传的水质数据 |
| 8 | 支持Windows上位机远程显示:通过Qt(C++)设计的Windows电脑上位机软件,远程显示设备上传的水质数据 | |
| 阈值设置功能 | 9 | 支持云端设置参数上下限:通过云端设置水温、PH值、浊度、溶解氧的参数上下限阈值 |
| 报警功能 | 10 | 支持本地声光报警:当检测数据超过云端设置的阈值范围时,本地蜂鸣器发出报警声 |
| 11 | 支持APP和上位机报警提示:当检测数据超过阈值范围时,Android APP和Windows上位机界面上对应数据显示为红色 | |
| 小船控制功能 | 12 | 支持小船遥控控制:通过蓝牙方式控制小船的前进、后退及转向动作 |
| 13 | 支持移动式水质采集:水质检测系统安装在小船上,遥控控制小船进入水域前行,在不同位置进行水质数据采集 |
1.4 开发工具的选择
【1】设备端开发
硬件设备端的开发主要依赖于C语言,利用该语言直接操作硬件寄存器,确保系统运行的高效性和低延迟。C语言在嵌入式开发中具有广泛的应用,它能够直接访问硬件,满足对资源消耗和响应速度的严格要求。为了编写高效、稳定的代码,开发工具选择了Keil uVision 5作为主要的开发环境。Keil是一款专业的嵌入式开发工具,广泛应用于基于ARM架构的微控制器开发,提供了完善的编译、调试和仿真支持,能够帮助开发者在软件开发过程中高效地进行代码编写、单步执行以及断点设置,确保开发的稳定性和高效性。
STM32F103C8T6是项目中使用的主控芯片,它基于ARM Cortex-M3架构,拥有64KB Flash和20KB RAM,具备强大的计算能力和丰富的外设接口。在硬件编程中,本项目采用寄存器级编程方式,这要求开发者对芯片的硬件寄存器有深入的理解。在Keil环境中,通过直接操作STM32的寄存器控制GPIO、ADC、USART、I2C、定时器等硬件外设,以满足各个硬件模块与主控芯片的交互需求。使用寄存器编程能够提供更高效、精确的控制,避免了使用标准外设库或HAL库带来的代码冗余,同时也能深入调控硬件特性,充分发挥STM32F103C8T6的性能优势,提升系统响应速度。
在传感器数据采集方面,通过ADC寄存器实现对PH值传感器和浊度传感器模拟信号的采集,通过USART寄存器实现与溶解氧传感器(485转串口模块)的数据通信,通过单总线时序控制实现对DS18B20温度传感器的数据读取。在显示控制方面,通过I2C寄存器驱动OLED显示屏进行数据显示。在通信控制方面,通过USART寄存器与ESP8266 Wi-Fi模块进行AT指令交互,实现MQTT协议数据上传,同时通过另一个USART接口与蓝牙模块通信,接收小船遥控指令。在运动控制方面,通过定时器寄存器输出PWM信号,分别控制直流电机的转速和舵机的转向角度。寄存器级的编程方式使得代码更加紧凑高效,满足嵌入式系统对实时性和资源占用的严格要求。
【2】上位机开发
本项目的上位机开发基于Qt 5框架,使用**C**作为主要编程语言。Qt是一个跨平台的应用开发框架,广泛用于开发图形用户界面应用程序,提供了丰富的GUI组件和工具,能够高效地实现界面设计与开发。C作为Qt的底层语言,具有高效的性能和良好的控制力,非常适合用于处理设备与云端之间的数据交互、MQTT通信协议的实现以及数据可视化展示等任务。
在项目中,Qt被用于开发两个版本的应用:Windows平台的桌面应用程序和Android平台的手机APP。Qt框架的跨平台特性使得开发者能够使用同一套代码库在不同操作系统上进行构建和部署,大大提高了开发效率,同时保证了两个版本在功能和用户体验上的一致性。为了方便开发和调试,上位机的开发采用了Qt Creator作为主要的集成开发环境。Qt Creator是Qt官方提供的开发工具,专为Qt应用程序开发设计,支持C++、QML等语言,提供了代码编辑、调试、构建、版本控制集成等丰富功能,能够显著提升开发者的生产力。在本项目中,Qt Creator为开发者提供了自动化构建、界面设计工具(如Qt Designer)和调试工具,使得开发过程更加高效和流畅。
上位机与硬件设备端的通信采用了基于MQTT协议的数据传输方式。Qt提供了丰富的网络编程支持,特别是QTcpSocket类,使得上位机能够轻松地与华为云物联网平台建立MQTT连接。本项目通过MQTT协议实现设备端与云平台的数据交互:设备端将采集的水温、PH值、浊度、溶解氧数据发布到华为云平台对应的主题,上位机应用订阅这些主题,实时接收并显示水质数据。同时,当用户在上位机上修改各项参数的上下限阈值时,应用通过MQTT将新的阈值配置信息发布到云端,再由云端下发至设备端,实现云端参数的动态设置。这种基于MQTT的通信方式保证了数据传输的可靠性和实时性。
为了满足不同用户的使用场景,本项目需要支持Windows平台的桌面应用和Android平台的移动APP。Qt的跨平台特性使得开发人员能够在一个代码库下完成多平台应用的开发和移植。开发者仅需要编写一次应用逻辑和用户界面,就可以通过Qt的跨平台构建工具生成Windows和Android两个平台的可执行文件。针对Android平台,Qt提供了完整的Android部署工具链,能够生成APK安装包并直接部署到手机设备上进行运行测试。此外,Qt提供了丰富的文档和社区支持,帮助开发者解决平台差异和兼容性问题,确保应用在不同平台上都能稳定运行。
在数据可视化方面,Qt提供了多种图形显示控件,能够将水质数据以数字、曲线、仪表盘等形式直观地展示给用户。同时,当接收到的数据超过云端设置的阈值范围时,界面上的对应数据项会显示为红色,实现可视化报警提示,帮助用户快速识别水质异常情况。
总体而言,上位机开发环境采用了Qt 5框架和C++语言,结合Qt Creator集成开发环境,提供了一个高效、稳定、跨平台的开发工具链。通过Qt强大的GUI设计、网络通信、跨平台支持等功能,开发者能够轻松实现与华为云物联网平台的MQTT通信、水质数据的实时显示、阈值配置下发以及异常报警提示,为用户提供直观、便捷的远程水质监测体验。
1.5 模块的技术详情介绍
(1)主控模块(STM32F103C8T6)
功能:作为整个系统的控制核心,负责传感器数据采集、数据处理、显示控制、通信协议解析、报警控制以及小船运动控制等所有逻辑运算。
特点:采用ARM Cortex-M3内核,主频最高72MHz,具有64KB Flash和20KB RAM,片上集成ADC、串口、定时器等多种外设,功耗低、性能稳定、性价比高,适合嵌入式控制系统开发。
(2)水温检测模块(DS18B20防水型温度传感器)
功能:用于采集水体温度数据,测量范围为-55℃~+125℃,精度为±0.5℃。
特点:采用单总线通信协议,只需一根数据线即可与主控芯片通信,防水封装设计适合水下环境使用,具有唯一的64位序列号,支持多点组网,供电方式灵活,可采用寄生电源模式。
(3)PH值检测模块(PH值检测传感器)
功能:用于检测水体的酸碱度,输出模拟电压信号,经ADC采集和算法转换后得到真实PH值。
特点:采用PH复合电极,测量范围为0~14,响应速度快,输出信号与PH值呈线性关系,可通过校准算法提高测量精度,模块自带信号放大和调节电路,输出0~5V模拟电压,便于单片机采集。
(4)浊度检测模块(浊度检测传感器)
功能:用于检测水体的浑浊程度,输出模拟电压信号,经ADC采集和算法转换后得到真实浊度值。
特点:采用光学散射原理,测量范围为0~1000NTU,输出信号随浊度增大而线性变化,模块具备温度补偿功能,能够减小温度变化对测量结果的影响,体积小巧,功耗低,适合便携式设备应用。
(5)溶解氧检测模块(溶解氧检测传感器+485转串口模块)
功能:用于检测水体中溶解氧的含量,传感器输出485接口信号,通过485转串口模块转换为TTL电平后与单片机串口连接,单片机通过串口协议读取溶解氧数据。
特点:采用荧光法测量原理,无需预热和电解液,维护成本低,测量范围为0~20mg/L,精度高、稳定性好,支持Modbus RTU通信协议,通过串口即可读取数据,抗干扰能力强,适合长期在线监测。
(6)显示模块(0.96寸OLED显示屏)
功能:用于实时显示采集的水温、PH值、浊度、溶解氧数据,方便现场查看水质状况。
特点:采用IIC通信协议,仅需两根数据线即可与主控芯片连接,分辨率128×64,自发光无需背光,功耗低、对比度高、视角宽广,体积小巧,适合嵌入式设备应用。
(7)无线通信模块(ESP8266 Wi-Fi模块)
功能:负责将采集的水质数据通过MQTT协议上传至华为云物联网服务器,并接收云端下发的阈值配置指令。
特点:集成TCP/IP协议栈,支持802.11 b/g/n无线标准,支持STA/AP/STA+AP多种工作模式,通过AT指令即可完成网络配置和数据传输,功耗低、成本低廉、开发简单,广泛应用于物联网终端设备。
(8)报警模块(高电平触发蜂鸣器)
功能:当检测数据超过云端设置的阈值范围时,发出报警提示音,提醒现场操作人员注意水质异常。
特点:采用高电平触发方式,控制简单,只需输出高电平即可驱动蜂鸣器发声,体积小、响应速度快、声音清晰,功耗低,适合便携式设备应用。
(9)小船动力模块(5V直流电机)
功能:为小船提供前进和后退的推力,通过PWM信号调节电机转速,实现航速控制。
特点:采用5V直流供电,控制简单,通过H桥电路实现正反转控制,响应速度快,扭矩适中,适合小型无人船的动力驱动,电机体积小、重量轻,便于安装在小船上。
(10)小船转向模块(SG90舵机)
功能:控制船舵方向,通过PWM信号调节舵机转动角度,实现小船左转、右转和直行控制。
特点:采用标准PWM信号控制,工作电压4.8V~6V,扭矩约为1.8kg/cm,重量仅9g,体积小巧、响应灵敏、控制精度高,适合小型模型船的转向控制,安装方便,兼容性好。
(11)遥控通信模块(蓝牙模块)
功能:接收手机或遥控器发送的蓝牙控制指令,实现对小船前进、后退及转向的无线遥控。
特点:采用低功耗蓝牙技术,通信距离可达10米以上,通过串口与主控芯片通信,配对连接简单,数据传输稳定,功耗低,支持透传模式,无需复杂的协议开发,便于快速实现无线遥控功能。
(12)电源模块
功能:为STM32主控芯片、传感器、OLED显示屏、ESP8266模块、蓝牙模块、电机、舵机等各硬件模块提供稳定可靠的工作电压。
特点:采用多级稳压设计,分别提供5V和3.3V电压输出,满足不同模块的供电需求,具备过流保护和短路保护功能,保证系统在异常情况下不会损坏,电池供电方案支持便携式移动应用,续航能力满足常规监测需求。
(13)传感器接口模块
功能:提供各类传感器与主控芯片之间的电气连接和信号转换,包括PH值传感器和浊度传感器的模拟信号输入接口、DS18B20温度传感器的单总线接口、溶解氧传感器的串行通信接口。
特点:采用标准接口设计,便于传感器的插拔更换,模拟信号输入接口具备滤波电路,减小信号噪声干扰,通信接口采用ESD保护,防止静电损坏主控芯片,接口布局合理,方便硬件调试和维护。
二、部署华为云物联网平台
华为云官网: https://www.huaweicloud.com/
打开官网,搜索物联网,就能快速找到 设备接入IoTDA。
2.1 物联网平台介绍
华为云物联网平台(IoT 设备接入云服务)提供海量设备的接入和管理能力,将物理设备联接到云,支撑设备数据采集上云和云端下发命令给设备进行远程控制,配合华为云其他产品,帮助我们快速构筑物联网解决方案。
使用物联网平台构建一个完整的物联网解决方案主要包括3部分:物联网平台、业务应用和设备。
物联网平台作为连接业务应用和设备的中间层,屏蔽了各种复杂的设备接口,实现设备的快速接入;同时提供强大的开放能力,支撑行业用户构建各种物联网解决方案。
设备可以通过固网、2G/3G/4G/5G、NB-IoT、Wifi等多种网络接入物联网平台,并使用LWM2M/CoAP、MQTT、HTTPS协议将业务数据上报到平台,平台也可以将控制命令下发给设备。
业务应用通过调用物联网平台提供的API,实现设备数据采集、命令下发、设备管理等业务场景。
2.2 开通物联网服务
地址: https://www.huaweicloud.com/product/iothub.html
开通免费单元。
点击立即创建。
正在创建标准版实例,需要等待片刻。
创建完成之后,点击详情。 可以看到标准版实例的设备接入端口和地址。
下面框起来的就是端口号和域名
点击实例名称,可以查看当前免费单元的配置情况。
开通之后,点击接入信息,也能查看接入信息。 我们当前设备准备采用MQTT协议接入华为云平台,这里可以看到MQTT协议的地址和端口号等信息。
总结:
端口号: MQTT (1883)| MQTTS (8883)
接入地址: dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com
根据域名地址得到IP地址信息:
打开Windows电脑的命令行控制台终端,使用ping 命令。ping一下即可。
Microsoft Windows [版本 10.0.19045.5011]
(c) Microsoft Corporation。保留所有权利。
C:UsersLenovo>ping dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com
正在 Ping dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com [117.78.5.125] 具有 32 字节的数据:
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
117.78.5.125 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
最短 = 37ms,最长 = 37ms,平均 = 37ms
C:UsersLenovo>
MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口合适。
2.3 创建产品
链接:https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-dev/all-product?instanceId=03c5c68c-e588-458c-90c3-9e4c640be7af
(1)创建产品
(2)填写产品信息
根据自己产品名字填写,下面的设备类型选择自定义类型。
(3)产品创建成功
创建完成之后点击查看详情。
(4)添加自定义模型
产品创建完成之后,点击进入产品详情页面,翻到最下面可以看到模型定义。
模型简单来说: 就是存放设备上传到云平台的数据。
你可以根据自己的产品进行创建。
比如:
烟雾可以叫 MQ2
温度可以叫 Temperature
湿度可以叫 humidity
火焰可以叫 flame
其他的传感器自己用单词简写命名即可。 这就是你的单片机设备端上传到服务器的数据名字。
先点击自定义模型。
再创建一个服务ID。
接着点击新增属性。
2.4 添加设备
产品是属于上层的抽象模型,接下来在产品模型下添加实际的设备。添加的设备最终需要与真实的设备关联在一起,完成数据交互。
(1)注册设备
(2)根据自己的设备填写
(3)保存设备信息
创建完毕之后,点击保存并关闭,得到创建的设备密匙信息。该信息在后续生成MQTT三元组的时候需要使用。
(4)设备创建完成
(5)设备详情
2.5 MQTT协议主题订阅与发布
(1)MQTT协议介绍
当前的设备是采用MQTT协议与华为云平台进行通信。
MQTT是一个物联网传输协议,它被设计用于轻量级的发布/订阅式消息传输,旨在为低带宽和不稳定的网络环境中的物联网设备提供可靠的网络服务。MQTT是专门针对物联网开发的轻量级传输协议。MQTT协议针对低带宽网络,低计算能力的设备,做了特殊的优化,使得其能适应各种物联网应用场景。目前MQTT拥有各种平台和设备上的客户端,已经形成了初步的生态系统。
MQTT是一种消息队列协议,使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合,相对于其他协议,开发更简单;MQTT协议是工作在TCP/IP协议上;由TCP/IP协议提供稳定的网络连接;所以,只要具备TCP协议栈的网络设备都可以使用MQTT协议。 本次设备采用的ESP8266就具备TCP协议栈,能够建立TCP连接,所以,配合STM32代码里封装的MQTT协议,就可以与华为云平台完成通信。
华为云的MQTT协议接入帮助文档在这里: https://support.huaweicloud.com/devg-iothub/iot_02_2200.html
业务流程:
(2)华为云平台MQTT协议使用限制
| 描述 | 限制 |
|---|---|
| 支持的MQTT协议版本 | 3.1.1 |
| 与标准MQTT协议的区别 | 支持Qos 0和Qos 1支持Topic自定义不支持QoS2不支持will、retain msg |
| MQTTS支持的安全等级 | 采用TCP通道基础 + TLS协议(最高TLSv1.3版本) |
| 单帐号每秒最大MQTT连接请求数 | 无限制 |
| 单个设备每分钟支持的最大MQTT连接数 | 1 |
| 单个MQTT连接每秒的吞吐量,即带宽,包含直连设备和网关 | 3KB/s |
| MQTT单个发布消息最大长度,超过此大小的发布请求将被直接拒绝 | 1MB |
| MQTT连接心跳时间建议值 | 心跳时间限定为30至1200秒,推荐设置为120秒 |
| 产品是否支持自定义Topic | 支持 |
| 消息发布与订阅 | 设备只能对自己的Topic进行消息发布与订阅 |
| 每个订阅请求的最大订阅数 | 无限制 |
(3)主题订阅格式
帮助文档地址:https://support.huaweicloud.com/devg-iothub/iot_02_2200.html
对于设备而言,一般会订阅平台下发消息给设备 这个主题。
设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。
如果设备想要知道平台下发的消息,需要订阅上面图片里标注的主题。
以当前设备为例,最终订阅主题的格式如下:
$oc/devices/{device_id}/sys/messages/down
最终的格式:
$oc/devices/663cb18871d845632a0912e7_dev1/sys/messages/down
(4)主题发布格式
对于设备来说,主题发布表示向云平台上传数据,将最新的传感器数据,设备状态上传到云平台。
这个操作称为:属性上报。
帮助文档地址:https://support.huaweicloud.com/usermanual-iothub/iot_06_v5_3010.html
根据帮助文档的介绍, 当前设备发布主题,上报属性的格式总结如下:
发布的主题格式:
$oc/devices/{device_id}/sys/properties/report
最终的格式:
$oc/devices/663cb18871d845632a0912e7_dev1/sys/properties/report
发布主题时,需要上传数据,这个数据格式是JSON格式。
上传的JSON数据格式如下:
{
"services": [
{
"service_id": <填服务ID>,
"properties": {
"<填属性名称1>": <填属性值>,
"<填属性名称2>": <填属性值>,
..........
}
}
]
}
根据JSON格式,一次可以上传多个属性字段。 这个JSON格式里的,服务ID,属性字段名称,属性值类型,在前面创建产品的时候就已经介绍了,不记得可以翻到前面去查看。
根据这个格式,组合一次上传的属性数据:
{"services": [{"service_id": "stm32","properties":{"你的字段名字1":30,"你的字段名字2":10,"你的字段名字3":1,"你的字段名字4":0}}]}
2.6 MQTT三元组
MQTT协议登录需要填用户ID,设备ID,设备密码等信息,就像我们平时登录QQ,微信一样要输入账号密码才能登录。MQTT协议登录的这3个参数,一般称为MQTT三元组。
接下来介绍,华为云平台的MQTT三元组参数如何得到。
(1)MQTT服务器地址
要登录MQTT服务器,首先记得先知道服务器的地址是多少,端口是多少。
帮助文档地址:https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-portal/home
MQTT协议的端口支持1883和8883,它们的区别是:8883 是加密端口更加安全。但是单片机上使用比较困难,所以当前的设备是采用1883端口进连接的。
根据上面的域名和端口号,得到下面的IP地址和端口号信息: 如果设备支持填写域名可以直接填域名,不支持就直接填写IP地址。 (IP地址就是域名解析得到的)
华为云的MQTT服务器地址:117.78.5.125
华为云的MQTT端口号:1883
如何得到IP地址?如何域名转IP? 打开Windows的命令行输入以下命令。
ping ad635970a1.st1.iotda-device.cn-north-4.myhuaweicloud.com
(2)生成MQTT三元组
华为云提供了一个在线工具,用来生成MQTT鉴权三元组: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/
打开这个工具,填入设备的信息(也就是刚才创建完设备之后保存的信息),点击生成,就可以得到MQTT的登录信息了。
下面是打开的页面:
填入设备的信息: (上面两行就是设备创建完成之后保存得到的)
直接得到三元组信息。
得到三元组之后,设备端通过MQTT协议登录鉴权的时候,填入参数即可。
ClientId 663cb18871d845632a0912e7_dev1_0_0_2024050911
Username 663cb18871d845632a0912e7_dev1
Password 71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237
2.7 模拟设备登录测试
经过上面的步骤介绍,已经创建了产品,设备,数据模型,得到MQTT登录信息。 接下来就用MQTT客户端软件模拟真实的设备来登录平台。测试与服务器通信是否正常。
MQTT软件下载地址【免费】: https://download.csdn.net/download/xiaolong1126626497/89928772
(1)填入登录信息
打开MQTT客户端软件,对号填入相关信息(就是上面的文本介绍)。然后,点击登录,订阅主题,发布主题。
(2)打开网页查看
完成上面的操作之后,打开华为云网页后台,可以看到设备已经在线了。
点击详情页面,可以看到上传的数据:
到此,云平台的部署已经完成,设备已经可以正常上传数据了。
(3)MQTT登录测试参数总结
MQTT服务器: 117.78.5.125
MQTT端口号: 183
//物联网服务器的设备信息
#define MQTT_ClientID "663cb18871d845632a0912e7_dev1_0_0_2024050911"
#define MQTT_UserName "663cb18871d845632a0912e7_dev1"
#define MQTT_PassWord "71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237"
//订阅与发布的主题
#define SET_TOPIC "$oc/devices/663cb18871d845632a0912e7_dev1/sys/messages/down" //订阅
#define POST_TOPIC "$oc/devices/663cb18871d845632a0912e7_dev1/sys/properties/report" //发布
发布的数据:
{"services": [{"service_id": "stm32","properties":{"你的字段名字1":30,"你的字段名字2":10,"你的字段名字3":1,"你的字段名字4":0}}]}
2.8 创建IAM账户
创建一个IAM账户,因为接下来开发上位机,需要使用云平台的API接口,这些接口都需要token进行鉴权。简单来说,就是身份的认证。 调用接口获取Token时,就需要填写IAM账号信息。所以,接下来演示一下过程。
地址: https://console.huaweicloud.com/iam/?region=cn-north-4#/iam/users
**【1】获取项目凭证 ** 点击左上角用户名,选择下拉菜单里的我的凭证
项目凭证:
28add376c01e4a61ac8b621c714bf459
【2】创建IAM用户
鼠标放在左上角头像上,在下拉菜单里选择统一身份认证。
点击左上角创建用户。
创建成功:
【3】创建完成
用户信息如下:
主用户名 l19504562721
IAM用户 ds_abc
密码 DS12345678
2.9 获取影子数据
帮助文档:https://support.huaweicloud.com/api-iothub/iot_06_v5_0079.html
设备影子介绍:
设备影子是一个用于存储和检索设备当前状态信息的JSON文档。
每个设备有且只有一个设备影子,由设备ID唯一标识
设备影子仅保存最近一次设备的上报数据和预期数据
无论该设备是否在线,都可以通过该影子获取和设置设备的属性
简单来说:设备影子就是保存,设备最新上传的一次数据。
我们设计的软件里,如果想要获取设备的最新状态信息,就采用设备影子接口。
如果对接口不熟悉,可以先进行在线调试:https://apiexplorer.developer.huaweicloud.com/apiexplorer/doc?product=IoTDA&api=ShowDeviceShadow
在线调试接口,可以请求影子接口,了解请求,与返回的数据格式。
调试完成看右下角的响应体,就是返回的影子数据。
设备影子接口返回的数据如下:
{
"device_id": "663cb18871d845632a0912e7_dev1",
"shadow": [
{
"service_id": "stm32",
"desired": {
"properties": null,
"event_time": null
},
"reported": {
"properties": {
"DHT11_T": 18,
"DHT11_H": 90,
"BH1750": 38,
"MQ135": 70
},
"event_time": "20240509T113448Z"
},
"version": 3
}
]
}
调试成功之后,可以得到访问影子数据的真实链接,接下来的代码开发中,就采用Qt写代码访问此链接,获取影子数据,完成上位机开发。
链接如下:
https://ad635970a1.st1.iotda-app.cn-north-4.myhuaweicloud.com:443/v5/iot/28add376c01e4a61ac8b621c714bf459/devices/663cb18871d845632a0912e7_dev1/shadow
三、Qt开发入门与环境搭建
当前项目的上位机是采用Qt开发的,这一章节主要是介绍Qt开发环境的安装,以及Qt开发环境入门的使用。如果你Qt没有任何基础,建议仔细看一遍。
3.1 Qt是什么?
Qt 是一个功能强大、跨平台的应用程序开发框架,主要用于创建图形用户界面(GUI)应用程序,但它不仅仅局限于GUI编程。它由挪威的奇趣科技(TrollTech)最初于1991年开发,并在后续的发展历程中经历了多次所有权变更,包括诺基亚和Digia等公司接手,现在Qt属于The Qt Company所有。
Qt 主要特点和优势包括:
(1)跨平台:Qt 支持多种操作系统,开发者可以使用同一份源代码在不同平台上编译运行,如Windows、Linux、macOS、Android以及各种嵌入式系统(如RTOS),实现“一次编写,到处编译”。
(2)C++ 开发:Qt 的核心是基于C++编程语言构建,提供了一套丰富的类库,通过面向对象的设计方式简化了开发过程。
(3)图形用户界面:Qt 提供了完整的GUI组件集,包含窗口、按钮、标签、文本框等各种标准控件,以及布局管理器、样式表等功能,使得开发者能够高效地创建美观且功能完善的桌面应用或移动应用界面。
(4)工具链完整:Qt 包含一系列集成开发环境(IDE)和辅助工具,例如Qt Creator是一个全能的跨平台IDE,Qt Designer用于可视化拖拽设计UI界面,Qt Linguist支持国际化资源文件的翻译,还有Qt Assistant和大量文档资源方便开发者的使用。
(5)非GUI功能丰富:除了GUI功能外,Qt 还提供了众多非图形化功能模块,如网络通信、数据库访问、XML处理、多媒体处理(音频视频)、文件I/O、线程与并发处理、OpenGL和3D图形渲染等。
(6)元对象系统:Qt 使用元对象系统(Meta-Object System, MOC)实现了信号与槽机制(Signals and Slots),这是一种高级事件处理机制,允许在不同对象之间安全地进行异步通信。
(7)可扩展性与灵活性:Qt 架构高度灵活,支持插件体系结构,开发者可以根据需要自定义组件并轻松地集成到Qt应用中。
Qt 以其强大的跨平台能力和全面的功能集合成为许多企业和个人开发者选择用来开发高性能、高稳定性的应用程序的重要工具之一,被广泛应用于各类桌面软件、嵌入式设备、移动应用以及服务器端组件等领域。
3.2 Qt版本介绍
在Qt发行版本中将要涉及两个版本:Qt商业授权和Qt开源授权。
(1)Qt商业授权是设计商业软件的开发环境,这些商业软件使用了传统的商业来发布,它包含了一些更新的功能、技术上的支持和大量的解决方案,开发了使用于行业的一些特定的组件,有一些特殊的功能只在商业用户中使用。
(2)Qt开源授权是用来开发开源的软件,它提供了一些免费的支持,并遵循QPL协议。
开放源代码是免费的软件,不牵涉用户的某些权益。任何人都有使用开源软件和参与它的修改的机会,这就意味着其他的人同样可获得你开发的代码。目前 Qt 的开源授权有两种,一种是 GPL 授权,另一种是 LGPL 授权。
3.3 Qt开发环境安装
Qt的中文官网: https://www.qt.io/zh-cn/
QT5.12.6的下载地址:https://download.qt.io/archive/qt/5.12/5.12.6
打开下载链接后选择下面的版本进行下载:
qt-opensource-windows-x86-5.12.6.exe 13-Nov-2019 07:28 3.7G Details
软件安装时断网安装,否则会提示输入账户。
如果下载不了,可以在网盘里找到安装包下载: 飞书文档记录的网盘地址:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
安装的时候,第一个复选框里勾选一个mingw 32编译器即可,其他的不管默认就行,直接点击下一步继续安装。
选择MinGW 32-bit 编译器:
3.4 开发第一个QT程序
在QT开发过程中,可以手动编写代码也可以使用UI设计师直接拖拽控件的方式编写界面和布局,在实际的开发过程中一般是两种方式结合使用,提高开发效率。
本小节用一个简单的 "Hello QT" 程序介绍一下使用QtCreator新建工程的步骤。
(1)打开QtCreator软件,选择New Project,新建一个工程。
(2)项目模板选择QT Widgets Application
(3)设置项目名称和存放路径
注意:QT项目路径和名称不能出现中文字符。
(4)编译工具套件选择
编译工具套件可以后面自己增加,比如增加Android的。套件是指 Qt 程序从编译链接到运行环境的全部工具和 Qt 类库的集合。
(5)设置生成的类信息
在类信息设置界面选择基类,目前有三种基类:QMainWindow,QWidget,QDialog。在基类里选择QMainWindow,类名和文件名会根据基类自动修改,一般不需要修改,默认即可。
(6)项目管理
在项目管理界面可以设置作为子项目,以及加入版本控制系统。这两个功能暂时用不到,都用默认的 ,然后点击 “完成”。
(7)创建完成
(8) 编辑代码
展开main.cpp文件,添加内容如下:
#include "mainwindow.h"
#include <QApplication>
#include <QDebug>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//MainWindow w;
//w.show();
QLabel *label =new QLabel("Hello Qt!");
label->setGeometry(400,100,100,20);
label->show();
return a.exec();
}
代码解析:
1) #include <QApplication>和 #include <QLabel>是QT的类声明头文件,对于每个QT类都有一个与该类同名的头文件,在这个头文件包含了对该类的定义。
2) main(int argc, char *argv[]) :main函数的标准写法。
3) QApplication a(argc, argv):创建一个QApplication对象,用于管理应用程序的资源,QApplication类的构造函数需要两个参数。
4) QLabel *label =new QLabel("Hello Qt!") :创建QLabel窗口部件,QLabel是一个Qt提供的窗口部件,可以用来显示一行文本。
5) label->setGeometry(400,100,100,20) : 设置控件显示的位置。
6) label->show():使Qlabel创建的窗口可见,就是显示设置的文本。
7) return a.exec():应用程序将控制权传递给QT,让程序进入消息循环。等待可能的菜单,工具条,鼠标等的输入,进行响应。
(9)行程序
运行程序可以点击左下角的三角形符号或者按下快捷键Ctrl+R。
3.5 调试输出
QT中使用QDebug类输出调试信息。主要用于调试代码,类似于std::cout的替代品,支持QT的数据类型。使用前需要包含头文件。
调试输出的分类
| qDebug | 调试信息提示 |
|---|---|
| qWarning | 一般的警告提示 |
| qCritical | 严重错误提示 |
| qFatal | 致命错误提示 |
示例代码:
qDebug("调试信息输出");
qWarning("一般警告信息输出");
qCritical("严重错误输出");
qFatal("致命错误输出");
qDebug输出的信息会打印到QT软件下边的输出面板。
在上节的HelloQt工程上加上调试输出代码,增加的main.cpp代码如下:
#include "mainwindow.h"
#include <QApplication>
#include <QDebug>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//MainWindow w;
//w.show();
qDebug()<<"QT调试信息输出";
int data_int=8888;
qDebug()<<data_int;
float data_float=123.888;
qDebug()<<data_float;
return a.exec();
}
运行程序,观察输出的调试信息:
3.6 QT Creator常用的快捷键
掌握一些适用的快捷键,可以提高程序开发的效率。
(1)F1 键,快速切换光标选中的函数或者类的帮助信息,按一次半屏显示,按下两次全屏显示。
(2)F2 键,快速切换到光标选中的函数或者类的源码定义处。
(3)F4键,快速在源文件和头文件之间切换。
(4)Ctrl(按住)+ Tab,快速切换已打开的文件
(5)Ctrl+ I ,缩进光标选中行代码(自动与上层代码对齐)。
(6)Ctrl + / ,快速注释或者取消注释光标选中行。
(7)快速修改全局变量名
鼠标光标选中变量名,按下Ctrl+Shift+R,当变量名称出现红色框表示已经激活全局修改功能。修改一处,整个工程对应变量名称全部会修改。修改完毕之后,光标移开,再按下Ctrl+Shift+R保存修改。
(8)快速修改全局函数名
快捷方式与变量修改一样按下Ctrl+Shift+R,一处修改整个工程对应的函数名称也会跟着改。选中函数后,按下Ctrl+Shift+R后整个工程的对应的函数名会高亮,并且在软件下方弹出修改框。
3.7 QT帮助文档
Qt 帮助文档太多,难以全部翻译成中文,即使翻译了一部分,翻译花的时间太多,翻译更新的时效性也难以保证,最终还是得看英文帮助,QtCreator 集成了帮助系统,查找非常方便。
打开QtCreator,选择菜单栏的最左边的帮助选项,界面如下:
(1)查看Qlabel控件的帮助信息:
3.8 UI设计师使用
上节的Hello QT程序使用纯C++代码编写,这一节使用QT界面设计模式实现与上一节Hello QT程序一样的功能。仿照着上节新创建一个工程。双击打开mainwindow.ui文件,进入到UI设计界面。
(1)拖一个Label控件到编辑区,双击Label控件可以修改文本内容。
(2)运行程序可以点击左下角的三角形符号或者按下快捷键Ctrl+R。
(3)UI设计师界面功能介绍
3.9 按钮控件组
QT Creator UI设计师界面的按钮组截图如下:
以下是对按钮组控件的一些功能介绍:
(1)Push Button按压按钮:最普通的按钮,按(点击)按钮命令计算机执行一些动作,或者回答问题,比如windows开始菜单里的重启,注销,关机等按钮。
(2)Tool Button工具按钮:工具按钮通常是一个集合,一般集成在工具栏里。比如打开,保存,复制,粘贴,剪切等常用的操作。
(3)Radio Button单选按钮:单选按钮通常是两个以上的形式出现在一块,按钮之间有互斥关系,每次只能选中一个。比如:一个人的性别只能选择一个,不能同时是男性又是女性。
(4)Check Box复选框:复选框与单选按钮概念相反,复选框通常表示多个可以同时存在的选项,比如一个人可以同时拥有多个爱好,比如读书、看电影、爬山、游泳等。
(5)Command Link Button命令链接按钮:一般用来打开的窗口或者网页链接。
(6)Dialog Button Box标准按钮盒:标准按钮盒通常用于对话框程序;比如:常见的确认对话框有 “确定”“取消”等标准按钮,Qt 将这些典型的按钮做成标准按钮盒,并将相应的信号加以封装,方便程序员使用。
3.10 布局控件组
开发一个图形界面应用程序,界面的布局影响到界面的美观。前面的程序中都是使用UI界面拖控件,如果有多个按钮,会出现大小难调整、位置难对齐等问题。Qt 提供的“布局管理“就很好的解决了控件摆放的问题。
以下是UI设计师界面的布局相关控件组:
功能介绍:
(1)Vertical Layout:垂直布局
(2)Horizontal Layout:水平布局
(3)Grid Layout:网格布局
(4)Form Layout:窗体中布局
(5)Horizontal Spacers:水平空格,在布局中用来占位。
(6)Vertical Spacer:垂直空格,在布局中用来占位。
3.11 基本布局控件
在UI设计界面添加一个布局控件,然后将需要布局的其他控件放入布局控件中即可完成布局,布局控件可以互相嵌套使用。(本节只介绍基本布局控件的使用)
以下是4种布局控件的效果:
3.12 UI设计师的布局功能
在UI设计界面的左上角有一排快捷的布局选项,使用时选中两个以上的控件,点击其中一种布局方式就可以切换布局。
以下为布局的简单示例图:
(1)为布局的选项。
(2)控件层次图,可以看到控件的布局摆放层次。
如果想要控制某个控件的固定大小,不随着布局改变大小,可以限定最大最小尺寸。选中控件鼠标右键-->大小限定->设置大小。
水平布局与垂直布局:
水平布局将控件按照水平方式摆放,垂直布局将控件按照垂直方式摆放。鼠标拖动红色布局框上的黑色方点,可以调整布局框的大小。随着布局框的尺寸变化,包含的控件高度不会变化,宽度会随着布局框变化。选中其中一个控件然后鼠标右键>点击大小限定,可以限定控件的最大和最小尺寸。
分裂器水平布局与垂直布局:
分裂器方式布局,包含控件的高度和宽度都会随着布局框的拉伸而改变。选中其中一个控件然后鼠标右键>点击大小限定,可以限定控件的最大和最小尺寸。
窗体中布局与栅格布局:
栅格(网格)布局器的基本单元是单元格,而窗体中布局(表单)的基本单元是行。随着布局框的尺寸变化,包含的控件高度不会变化,宽度会随着布局框变化。
设置主窗体布局方式:
设置主窗体的布局方式后,包含在主窗体内的控件会随着窗体的拉伸自动调整大小。
四、上位机开发
4.1 Qt开发环境安装
Qt的中文官网: https://www.qt.io/zh-cn/
QT5.12.6的下载地址:https://download.qt.io/archive/qt/5.12/5.12.6
打开下载链接后选择下面的版本进行下载:
如果下载不了,可以在网盘里找到安装包下载: 飞书文档记录的网盘地址:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
软件安装时断网安装,否则会提示输入账户。
安装的时候,第一个复选框里的编译器可以全选,直接点击下一步继续安装。
选择编译器: (一定要看清楚了)
4.2 新建上位机工程
前面2讲解了需要用的API接口,接下来就使用Qt设计上位机,设计界面,完成整体上位机的逻辑设计。
【1】新建工程
【2】设置项目的名称。
【3】选择编译系统
【4】选择默认继承的类
【5】选择编译器
【6】点击完成
【7】工程创建完成
4.3 切换编译器
在左下角是可以切换编译器的。 可以选择用什么样的编译器编译程序。
目前新建工程的时候选择了2种编译器。 一种是mingw32这个编译Windows下运行的程序。 一种是Android编译器,可以生成Android手机APP。
不过要注意:Android的编译器需要配置一些环境才可以正常使用,这个大家可以网上找找教程配置一下就行了。
windows的编译器就没有这么麻烦,安装好Qt就可以编译使用。
下面我这里就选择的 mingw32这个编译器,编译Windows下运行的程序。
4.4 编译测试功能
创建完毕之后,编译测试一下功能是否OK。
点击左下角的绿色三角形按钮。
正常运行就可以看到弹出一个白色的框框。这就表示工程环境没有问题了。 接下来就可以放心的设计界面了。
4.5 设计UI界面与工程配置
【1】打开UI文件
打开默认的界面如下:
【2】开始设计界面
根据自己需求设计界面。
五、QT上位机代码设计
5.1 项目结构说明
本项目上位机采用Qt 5框架和C++语言开发,支持Windows和Android两个平台。项目结构如下:
WaterQualityMonitor/
├── WaterQualityMonitor.pro # 项目配置文件
├── main.cpp # 主函数入口
├── mainwindow.h # 主窗口头文件
├── mainwindow.cpp # 主窗口实现文件
├── mainwindow.ui # 主窗口界面文件
├── mqttmanager.h # MQTT管理器头文件
├── mqttmanager.cpp # MQTT管理器实现文件
├── datamodel.h # 数据结构定义
├── alarmwidget.h # 报警组件头文件
├── alarmwidget.cpp # 报警组件实现文件
├── chartwidget.h # 图表组件头文件
├── chartwidget.cpp # 图表组件实现文件
├── settingsdialog.h # 设置对话框头文件
├── settingsdialog.cpp # 设置对话框实现文件
├── settingsdialog.ui # 设置对话框界面文件
└── resources/ # 资源文件目录
├── images/ # 图片资源
└── styles/ # 样式表文件
5.2 项目配置文件
# WaterQualityMonitor.pro
QT += core gui network widgets charts
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# Android配置
android {
QT += androidextras
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
ANDROID_ABIS = armeabi-v7a arm64-v8a
}
SOURCES +=
main.cpp
mainwindow.cpp
mqttmanager.cpp
alarmwidget.cpp
chartwidget.cpp
settingsdialog.cpp
HEADERS +=
mainwindow.h
mqttmanager.h
datamodel.h
alarmwidget.h
chartwidget.h
settingsdialog.h
FORMS +=
mainwindow.ui
settingsdialog.ui
RESOURCES +=
resources.qrc
# 目标文件目录
DESTDIR = $$PWD/bin
OBJECTS_DIR = $$PWD/build/obj
MOC_DIR = $$PWD/build/moc
RCC_DIR = $$PWD/build/rcc
UI_DIR = $$PWD/build/ui
# Windows平台设置
win32 {
RC_ICONS = resources/app.ico
}
# Android平台设置
android {
ANDROID_EXTRA_LIBS =
}
5.3 数据结构定义
// datamodel.h
#ifndef DATAMODEL_H
#define DATAMODEL_H
#include <QString>
#include <QDateTime>
// 水质数据结构
struct WaterQualityData
{
float temperature; // 水温 (℃)
float phValue; // PH值
float turbidity; // 浊度 (NTU)
float dissolvedOxygen; // 溶解氧 (mg/L)
QDateTime timestamp; // 时间戳
WaterQualityData()
{
temperature = 0.0f;
phValue = 0.0f;
turbidity = 0.0f;
dissolvedOxygen = 0.0f;
}
};
// 阈值数据结构
struct ThresholdData
{
float tempMax; // 温度上限
float tempMin; // 温度下限
float phMax; // PH上限
float phMin; // PH下限
float turbMax; // 浊度上限
float doMin; // 溶解氧下限
ThresholdData()
{
tempMax = 35.0f;
tempMin = 5.0f;
phMax = 8.5f;
phMin = 6.5f;
turbMax = 50.0f;
doMin = 3.0f;
}
};
// 报警状态结构
struct AlarmStatus
{
bool tempAlarm;
bool phAlarm;
bool turbAlarm;
bool doAlarm;
AlarmStatus()
{
tempAlarm = false;
phAlarm = false;
turbAlarm = false;
doAlarm = false;
}
};
#endif // DATAMODEL_H
5.4 MQTT管理器代码
// mqttmanager.h
#ifndef MQTTMANAGER_H
#define MQTTMANAGER_H
#include <QObject>
#include <QTcpSocket>
#include <QTimer>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include "datamodel.h"
class MQTTManager : public QObject
{
Q_OBJECT
public:
explicit MQTTManager(QObject *parent = nullptr);
~MQTTManager();
// 连接MQTT服务器
bool connectToServer(const QString &host, quint16 port,
const QString &clientId,
const QString &username,
const QString &password);
// 断开连接
void disconnectFromServer();
// 订阅主题
void subscribe(const QString &topic);
// 发布消息
void publish(const QString &topic, const QString &payload);
// 设置设备ID
void setDeviceId(const QString &deviceId);
// 连接状态
bool isConnected() const { return m_connected; }
signals:
// 连接状态变化信号
void connected();
void disconnected();
// 数据接收信号
void waterQualityDataReceived(const WaterQualityData &data);
void thresholdDataReceived(const ThresholdData &data);
// 错误信号
void error(const QString &error);
private slots:
void onSocketConnected();
void onSocketDisconnected();
void onSocketReadyRead();
void onSocketError(QAbstractSocket::SocketError socketError);
void onKeepAliveTimer();
private:
// MQTT协议相关函数
void sendConnectPacket();
void sendSubscribePacket(const QString &topic);
void sendPublishPacket(const QString &topic, const QString &payload);
void sendPingReqPacket();
void parsePacket(const QByteArray &data);
void parsePublishPacket(const QByteArray &data);
// 数据处理函数
void parseWaterQualityData(const QJsonObject &obj);
void parseThresholdData(const QJsonObject &obj);
private:
QTcpSocket *m_socket;
QTimer *m_keepAliveTimer;
QString m_clientId;
QString m_username;
QString m_password;
QString m_deviceId;
QString m_host;
quint16 m_port;
bool m_connected;
bool m_connectSent;
QByteArray m_receiveBuffer;
quint16 m_packetId;
};
#endif // MQTTMANAGER_H
// mqttmanager.cpp
#include "mqttmanager.h"
#include <QDebug>
// MQTT固定头部类型
#define MQTT_CONNECT 0x10
#define MQTT_CONNACK 0x20
#define MQTT_PUBLISH 0x30
#define MQTT_PUBACK 0x40
#define MQTT_SUBSCRIBE 0x82
#define MQTT_SUBACK 0x90
#define MQTT_PINGREQ 0xC0
#define MQTT_PINGRESP 0xD0
#define MQTT_DISCONNECT 0xE0
MQTTManager::MQTTManager(QObject *parent)
: QObject(parent)
, m_socket(nullptr)
, m_keepAliveTimer(nullptr)
, m_connected(false)
, m_connectSent(false)
, m_packetId(1)
{
m_socket = new QTcpSocket(this);
m_keepAliveTimer = new QTimer(this);
connect(m_socket, &QTcpSocket::connected,
this, &MQTTManager::onSocketConnected);
connect(m_socket, &QTcpSocket::disconnected,
this, &MQTTManager::onSocketDisconnected);
connect(m_socket, &QTcpSocket::readyRead,
this, &MQTTManager::onSocketReadyRead);
connect(m_socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error),
this, &MQTTManager::onSocketError);
connect(m_keepAliveTimer, &QTimer::timeout,
this, &MQTTManager::onKeepAliveTimer);
}
MQTTManager::~MQTTManager()
{
disconnectFromServer();
}
bool MQTTManager::connectToServer(const QString &host, quint16 port,
const QString &clientId,
const QString &username,
const QString &password)
{
if (m_connected) {
disconnectFromServer();
}
m_host = host;
m_port = port;
m_clientId = clientId;
m_username = username;
m_password = password;
m_socket->connectToHost(host, port);
return true;
}
void MQTTManager::disconnectFromServer()
{
if (m_connected) {
// 发送断开连接包
QByteArray packet;
packet.append(static_cast<char>(MQTT_DISCONNECT));
packet.append(static_cast<char>(0x00));
m_socket->write(packet);
m_socket->flush();
}
m_keepAliveTimer->stop();
m_socket->disconnectFromHost();
m_connected = false;
m_connectSent = false;
}
void MQTTManager::subscribe(const QString &topic)
{
if (!m_connected) {
emit error(QString("Not connected to MQTT server"));
return;
}
sendSubscribePacket(topic);
}
void MQTTManager::publish(const QString &topic, const QString &payload)
{
if (!m_connected) {
emit error(QString("Not connected to MQTT server"));
return;
}
sendPublishPacket(topic, payload);
}
void MQTTManager::setDeviceId(const QString &deviceId)
{
m_deviceId = deviceId;
}
void MQTTManager::onSocketConnected()
{
qDebug() << "Socket connected, sending CONNECT packet";
sendConnectPacket();
}
void MQTTManager::onSocketDisconnected()
{
qDebug() << "Socket disconnected";
m_connected = false;
m_connectSent = false;
emit disconnected();
}
void MQTTManager::onSocketReadyRead()
{
m_receiveBuffer.append(m_socket->readAll());
// 解析接收到的数据
parsePacket(m_receiveBuffer);
}
void MQTTManager::onSocketError(QAbstractSocket::SocketError socketError)
{
Q_UNUSED(socketError)
emit error(m_socket->errorString());
}
void MQTTManager::onKeepAliveTimer()
{
if (m_connected) {
sendPingReqPacket();
}
}
void MQTTManager::sendConnectPacket()
{
QByteArray packet;
QByteArray payload;
// 协议名 "MQTT"
payload.append(static_cast<char>(0x00));
payload.append(static_cast<char>(0x04));
payload.append("MQTT");
// 协议级别 4
payload.append(static_cast<char>(0x04));
// 连接标志
quint8 connectFlags = 0x02; // 清除会话标志
if (!m_username.isEmpty()) {
connectFlags |= 0x80; // 用户名标志
}
if (!m_password.isEmpty()) {
connectFlags |= 0x40; // 密码标志
}
payload.append(static_cast<char>(connectFlags));
// 保持连接时间 (60秒)
payload.append(static_cast<char>(0x00));
payload.append(static_cast<char>(0x3C));
// 客户端ID
payload.append(static_cast<char>(0x00));
payload.append(static_cast<char>(m_clientId.length()));
payload.append(m_clientId.toUtf8());
// 用户名
if (!m_username.isEmpty()) {
payload.append(static_cast<char>(0x00));
payload.append(static_cast<char>(m_username.length()));
payload.append(m_username.toUtf8());
}
// 密码
if (!m_password.isEmpty()) {
payload.append(static_cast<char>(0x00));
payload.append(static_cast<char>(m_password.length()));
payload.append(m_password.toUtf8());
}
// 构建固定头部
packet.append(static_cast<char>(MQTT_CONNECT));
packet.append(static_cast<char>(payload.length()));
packet.append(payload);
m_socket->write(packet);
m_socket->flush();
}
void MQTTManager::sendSubscribePacket(const QString &topic)
{
QByteArray packet;
QByteArray payload;
// 包标识符
quint16 packetId = m_packetId++;
payload.append(static_cast<char>((packetId >> 8) & 0xFF));
payload.append(static_cast<char>(packetId & 0xFF));
// 主题
payload.append(static_cast<char>((topic.length() >> 8) & 0xFF));
payload.append(static_cast<char>(topic.length() & 0xFF));
payload.append(topic.toUtf8());
// QoS级别
payload.append(static_cast<char>(0x00));
// 构建固定头部
packet.append(static_cast<char>(MQTT_SUBSCRIBE));
packet.append(static_cast<char>(payload.length()));
packet.append(payload);
m_socket->write(packet);
m_socket->flush();
}
void MQTTManager::sendPublishPacket(const QString &topic, const QString &payload)
{
QByteArray packet;
QByteArray variableHeader;
QByteArray payloadData = payload.toUtf8();
// 主题
variableHeader.append(static_cast<char>((topic.length() >> 8) & 0xFF));
variableHeader.append(static_cast<char>(topic.length() & 0xFF));
variableHeader.append(topic.toUtf8());
// 包标识符 (QoS 0 不需要)
// 构建固定头部
packet.append(static_cast<char>(MQTT_PUBLISH));
// 剩余长度 = 可变头部长度 + 有效载荷长度
quint16 remainingLength = variableHeader.length() + payloadData.length();
packet.append(static_cast<char>(remainingLength & 0xFF));
if (remainingLength > 127) {
packet.append(static_cast<char>((remainingLength >> 8) & 0xFF));
}
packet.append(variableHeader);
packet.append(payloadData);
m_socket->write(packet);
m_socket->flush();
}
void MQTTManager::sendPingReqPacket()
{
QByteArray packet;
packet.append(static_cast<char>(MQTT_PINGREQ));
packet.append(static_cast<char>(0x00));
m_socket->write(packet);
m_socket->flush();
}
void MQTTManager::parsePacket(const QByteArray &data)
{
if (data.isEmpty()) return;
int index = 0;
while (index < data.size()) {
quint8 packetType = static_cast<quint8>(data[index]) & 0xF0;
int remainingLength = 0;
int multiplier = 1;
int lengthIndex = index + 1;
// 解析剩余长度
while (lengthIndex < data.size()) {
quint8 digit = static_cast<quint8>(data[lengthIndex]);
remainingLength += (digit & 0x7F) * multiplier;
multiplier *= 128;
lengthIndex++;
if ((digit & 0x80) == 0) break;
}
int packetLength = lengthIndex - index + remainingLength;
if (packetLength > data.size()) {
// 数据不完整,等待更多数据
break;
}
QByteArray packet = data.mid(index, packetLength);
switch (packetType) {
case MQTT_CONNACK:
qDebug() << "Received CONNACK";
m_connected = true;
m_keepAliveTimer->start(30000); // 30秒心跳
emit connected();
break;
case MQTT_PUBLISH:
parsePublishPacket(packet);
break;
case MQTT_SUBACK:
qDebug() << "Received SUBACK";
break;
case MQTT_PINGRESP:
qDebug() << "Received PINGRESP";
break;
default:
qDebug() << "Unknown packet type:" << packetType;
break;
}
index += packetLength;
}
// 移除已处理的数据
if (index > 0) {
m_receiveBuffer.remove(0, index);
}
}
void MQTTManager::parsePublishPacket(const QByteArray &data)
{
if (data.size() < 2) return;
int index = 2; // 跳过固定头部
int remainingLength = 0;
int multiplier = 1;
// 解析剩余长度
while (index < data.size()) {
quint8 digit = static_cast<quint8>(data[index]);
remainingLength += (digit & 0x7F) * multiplier;
multiplier *= 128;
index++;
if ((digit & 0x80) == 0) break;
}
if (index + 2 > data.size()) return;
// 解析主题长度
quint16 topicLength = (static_cast<quint8>(data[index]) << 8) |
static_cast<quint8>(data[index + 1]);
index += 2;
if (index + topicLength > data.size()) return;
// 解析主题
QString topic = QString::fromUtf8(data.mid(index, topicLength));
index += topicLength;
// 解析有效载荷
QByteArray payload = data.mid(index, remainingLength - (topicLength + 2));
// 处理数据
QJsonDocument doc = QJsonDocument::fromJson(payload);
if (!doc.isNull() && doc.isObject()) {
QJsonObject obj = doc.object();
// 判断数据类型
if (obj.contains("services")) {
parseWaterQualityData(obj);
} else if (obj.contains("threshold")) {
parseThresholdData(obj);
}
}
}
void MQTTManager::parseWaterQualityData(const QJsonObject &obj)
{
WaterQualityData data;
QJsonArray services = obj["services"].toArray();
for (const QJsonValue &service : services) {
QJsonObject properties = service.toObject()["properties"].toObject();
if (properties.contains("DS18B20")) {
data.temperature = properties["DS18B20"].toDouble();
}
if (properties.contains("PH")) {
data.phValue = properties["PH"].toDouble();
}
if (properties.contains("Turbidity")) {
data.turbidity = properties["Turbidity"].toDouble();
}
if (properties.contains("oxygen")) {
data.dissolvedOxygen = properties["oxygen"].toDouble();
}
}
data.timestamp = QDateTime::currentDateTime();
emit waterQualityDataReceived(data);
}
void MQTTManager::parseThresholdData(const QJsonObject &obj)
{
ThresholdData threshold;
QJsonObject threshObj = obj["threshold"].toObject();
if (threshObj.contains("DS18B20_MAX")) {
threshold.tempMax = threshObj["DS18B20_MAX"].toDouble();
}
if (threshObj.contains("DS18B20_MIN")) {
threshold.tempMin = threshObj["DS18B20_MIN"].toDouble();
}
if (threshObj.contains("PH_MAX")) {
threshold.phMax = threshObj["PH_MAX"].toDouble();
}
if (threshObj.contains("PH_MIN")) {
threshold.phMin = threshObj["PH_MIN"].toDouble();
}
if (threshObj.contains("Turbidity_MAX")) {
threshold.turbMax = threshObj["Turbidity_MAX"].toDouble();
}
if (threshObj.contains("oxygen_MIN")) {
threshold.doMin = threshObj["oxygen_MIN"].toDouble();
}
emit thresholdDataReceived(threshold);
}
5.5 主窗口代码
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTimer>
#include <QLabel>
#include <QTableWidget>
#include "mqttmanager.h"
#include "datamodel.h"
#include "chartwidget.h"
#include "settingsdialog.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
// 连接相关
void onConnectButtonClicked();
void onDisconnectButtonClicked();
void onSettingsButtonClicked();
// 数据接收
void onWaterQualityDataReceived(const WaterQualityData &data);
void onThresholdDataReceived(const ThresholdData &data);
void onMqttConnected();
void onMqttDisconnected();
void onMqttError(const QString &error);
// 阈值设置
void onSetThresholdButtonClicked();
// 数据导出
void onExportButtonClicked();
void onClearButtonClicked();
// 定时器
void onRefreshTimer();
private:
void initUI();
void initMqttManager();
void updateDisplay(const WaterQualityData &data);
void updateAlarmDisplay(const WaterQualityData &data);
void addDataToTable(const WaterQualityData &data);
void checkAlarm(const WaterQualityData &data);
void updateStatusBar(const QString &message);
private:
Ui::MainWindow *ui;
MQTTManager *m_mqttManager;
ChartWidget *m_chartWidget;
SettingsDialog *m_settingsDialog;
QTimer *m_refreshTimer;
QList<WaterQualityData> m_dataHistory;
ThresholdData m_currentThreshold;
AlarmStatus m_currentAlarm;
QLabel *m_statusLabel;
QLabel *m_connectionLabel;
bool m_isConnected;
};
#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <QFileDialog>
#include <QTextStream>
#include <QDateTime>
#include <QCloseEvent>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, m_mqttManager(nullptr)
, m_chartWidget(nullptr)
, m_settingsDialog(nullptr)
, m_refreshTimer(nullptr)
, m_isConnected(false)
{
ui->setupUi(this);
initUI();
initMqttManager();
}
MainWindow::~MainWindow()
{
if (m_mqttManager) {
m_mqttManager->disconnectFromServer();
}
delete ui;
}
void MainWindow::initUI()
{
// 设置窗口标题
setWindowTitle(tr("水质监测系统"));
setMinimumSize(1024, 768);
// 创建图表组件
m_chartWidget = new ChartWidget(this);
ui->chartLayout->addWidget(m_chartWidget);
// 创建状态栏标签
m_statusLabel = new QLabel(tr("就绪"));
m_connectionLabel = new QLabel(tr("未连接"));
m_connectionLabel->setStyleSheet("color: red");
statusBar()->addWidget(m_statusLabel);
statusBar()->addPermanentWidget(m_connectionLabel);
// 连接按钮信号
connect(ui->connectButton, &QPushButton::clicked,
this, &MainWindow::onConnectButtonClicked);
connect(ui->disconnectButton, &QPushButton::clicked,
this, &MainWindow::onDisconnectButtonClicked);
connect(ui->settingsButton, &QPushButton::clicked,
this, &MainWindow::onSettingsButtonClicked);
connect(ui->setThresholdButton, &QPushButton::clicked,
this, &MainWindow::onSetThresholdButtonClicked);
connect(ui->exportButton, &QPushButton::clicked,
this, &MainWindow::onExportButtonClicked);
connect(ui->clearButton, &QPushButton::clicked,
this, &MainWindow::onClearButtonClicked);
// 初始化表格
ui->dataTable->setColumnCount(5);
QStringList headers;
headers << tr("时间") << tr("温度(℃)") << tr("PH值")
<< tr("浊度(NTU)") << tr("溶解氧(mg/L)");
ui->dataTable->setHorizontalHeaderLabels(headers);
ui->dataTable->horizontalHeader()->setStretchLastSection(true);
// 初始化阈值显示
m_currentThreshold = ThresholdData();
ui->tempMaxSpin->setValue(m_currentThreshold.tempMax);
ui->tempMinSpin->setValue(m_currentThreshold.tempMin);
ui->phMaxSpin->setValue(m_currentThreshold.phMax);
ui->phMinSpin->setValue(m_currentThreshold.phMin);
ui->turbMaxSpin->setValue(m_currentThreshold.turbMax);
ui->doMinSpin->setValue(m_currentThreshold.doMin);
// 创建刷新定时器
m_refreshTimer = new QTimer(this);
connect(m_refreshTimer, &QTimer::timeout, this, &MainWindow::onRefreshTimer);
m_refreshTimer->start(1000);
}
void MainWindow::initMqttManager()
{
m_mqttManager = new MQTTManager(this);
connect(m_mqttManager, &MQTTManager::connected,
this, &MainWindow::onMqttConnected);
connect(m_mqttManager, &MQTTManager::disconnected,
this, &MainWindow::onMqttDisconnected);
connect(m_mqttManager, &MQTTManager::waterQualityDataReceived,
this, &MainWindow::onWaterQualityDataReceived);
connect(m_mqttManager, &MQTTManager::thresholdDataReceived,
this, &MainWindow::onThresholdDataReceived);
connect(m_mqttManager, &MQTTManager::error,
this, &MainWindow::onMqttError);
}
void MainWindow::onConnectButtonClicked()
{
// 获取连接参数
QString host = ui->hostEdit->text();
quint16 port = ui->portEdit->text().toUShort();
QString clientId = ui->clientIdEdit->text();
QString username = ui->usernameEdit->text();
QString password = ui->passwordEdit->text();
QString deviceId = ui->deviceIdEdit->text();
if (host.isEmpty() || clientId.isEmpty()) {
QMessageBox::warning(this, tr("警告"), tr("请填写服务器地址和客户端ID"));
return;
}
m_mqttManager->setDeviceId(deviceId);
if (m_mqttManager->connectToServer(host, port, clientId, username, password)) {
updateStatusBar(tr("正在连接MQTT服务器..."));
} else {
QMessageBox::critical(this, tr("错误"), tr("连接失败"));
}
}
void MainWindow::onDisconnectButtonClicked()
{
m_mqttManager->disconnectFromServer();
}
void MainWindow::onSettingsButtonClicked()
{
if (!m_settingsDialog) {
m_settingsDialog = new SettingsDialog(this);
}
if (m_settingsDialog->exec() == QDialog::Accepted) {
// 应用设置
// 从设置对话框获取配置
}
}
void MainWindow::onSetThresholdButtonClicked()
{
// 获取用户设置的阈值
ThresholdData newThreshold;
newThreshold.tempMax = ui->tempMaxSpin->value();
newThreshold.tempMin = ui->tempMinSpin->value();
newThreshold.phMax = ui->phMaxSpin->value();
newThreshold.phMin = ui->phMinSpin->value();
newThreshold.turbMax = ui->turbMaxSpin->value();
newThreshold.doMin = ui->doMinSpin->value();
// 构建MQTT消息
QJsonObject obj;
QJsonObject thresholdObj;
thresholdObj["DS18B20_MAX"] = newThreshold.tempMax;
thresholdObj["DS18B20_MIN"] = newThreshold.tempMin;
thresholdObj["PH_MAX"] = newThreshold.phMax;
thresholdObj["PH_MIN"] = newThreshold.phMin;
thresholdObj["Turbidity_MAX"] = newThreshold.turbMax;
thresholdObj["oxygen_MIN"] = newThreshold.doMin;
obj["threshold"] = thresholdObj;
QJsonDocument doc(obj);
QString payload = QString::fromUtf8(doc.toJson());
// 发布阈值配置消息
QString topic = QString("$oc/devices/%1/sys/properties/set").arg(ui->deviceIdEdit->text());
m_mqttManager->publish(topic, payload);
updateStatusBar(tr("阈值设置已发送"));
}
void MainWindow::onExportButtonClicked()
{
if (m_dataHistory.isEmpty()) {
QMessageBox::information(this, tr("提示"), tr("没有数据可导出"));
return;
}
QString fileName = QFileDialog::getSaveFileName(this, tr("导出数据"),
QDir::homePath(),
tr("CSV文件 (*.csv)"));
if (fileName.isEmpty()) return;
QFile file(fileName);
if (file.open(QIODevice::WriteOnly)) {
QTextStream stream(&file);
// 写入表头
stream << "时间,温度(℃),PH值,浊度(NTU),溶解氧(mg/L)n";
// 写入数据
for (const WaterQualityData &data : m_dataHistory) {
stream << data.timestamp.toString("yyyy-MM-dd hh:mm:ss") << ","
<< data.temperature << ","
<< data.phValue << ","
<< data.turbidity << ","
<< data.dissolvedOxygen << "n";
}
file.close();
updateStatusBar(tr("数据导出成功: %1").arg(fileName));
}
}
void MainWindow::onClearButtonClicked()
{
m_dataHistory.clear();
ui->dataTable->setRowCount(0);
m_chartWidget->clearData();
updateStatusBar(tr("数据已清除"));
}
void MainWindow::onRefreshTimer()
{
// 更新图表显示
if (!m_dataHistory.isEmpty()) {
const WaterQualityData &lastData = m_dataHistory.last();
m_chartWidget->updateData(lastData);
}
}
void MainWindow::onWaterQualityDataReceived(const WaterQualityData &data)
{
// 更新数据显示
updateDisplay(data);
// 检查报警
checkAlarm(data);
// 添加到历史记录
addDataToTable(data);
m_dataHistory.append(data);
// 限制历史数据数量
while (m_dataHistory.size() > 1000) {
m_dataHistory.removeFirst();
// 同时更新表格显示
while (ui->dataTable->rowCount() > 1000) {
ui->dataTable->removeRow(0);
}
}
updateStatusBar(tr("接收到数据 - 时间: %1")
.arg(data.timestamp.toString("hh:mm:ss")));
}
void MainWindow::onThresholdDataReceived(const ThresholdData &data)
{
m_currentThreshold = data;
// 更新界面上的阈值显示
ui->tempMaxSpin->setValue(data.tempMax);
ui->tempMinSpin->setValue(data.tempMin);
ui->phMaxSpin->setValue(data.phMax);
ui->phMinSpin->setValue(data.phMin);
ui->turbMaxSpin->setValue(data.turbMax);
ui->doMinSpin->setValue(data.doMin);
updateStatusBar(tr("已接收云端阈值配置"));
}
void MainWindow::onMqttConnected()
{
m_isConnected = true;
m_connectionLabel->setText(tr("已连接"));
m_connectionLabel->setStyleSheet("color: green");
// 订阅设备上报的主题
QString topic = QString("$oc/devices/%1/sys/properties/report").arg(ui->deviceIdEdit->text());
m_mqttManager->subscribe(topic);
updateStatusBar(tr("MQTT连接成功"));
}
void MainWindow::onMqttDisconnected()
{
m_isConnected = false;
m_connectionLabel->setText(tr("未连接"));
m_connectionLabel->setStyleSheet("color: red");
updateStatusBar(tr("MQTT连接断开"));
}
void MainWindow::onMqttError(const QString &error)
{
updateStatusBar(tr("错误: %1").arg(error));
}
void MainWindow::updateDisplay(const WaterQualityData &data)
{
// 更新数值显示
ui->tempValueLabel->setText(QString::number(data.temperature, 'f', 1) + " ℃");
ui->phValueLabel->setText(QString::number(data.phValue, 'f', 2));
ui->turbValueLabel->setText(QString::number(data.turbidity, 'f', 1) + " NTU");
ui->doValueLabel->setText(QString::number(data.dissolvedOxygen, 'f', 2) + " mg/L");
// 更新时间
ui->timeLabel->setText(data.timestamp.toString("yyyy-MM-dd hh:mm:ss"));
// 更新报警显示
updateAlarmDisplay(data);
}
void MainWindow::updateAlarmDisplay(const WaterQualityData &data)
{
// 温度报警显示
if (data.temperature > m_currentThreshold.tempMax ||
data.temperature < m_currentThreshold.tempMin) {
ui->tempValueLabel->setStyleSheet("color: red; font-weight: bold");
m_currentAlarm.tempAlarm = true;
} else {
ui->tempValueLabel->setStyleSheet("color: black");
m_currentAlarm.tempAlarm = false;
}
// PH报警显示
if (data.phValue > m_currentThreshold.phMax ||
data.phValue < m_currentThreshold.phMin) {
ui->phValueLabel->setStyleSheet("color: red; font-weight: bold");
m_currentAlarm.phAlarm = true;
} else {
ui->phValueLabel->setStyleSheet("color: black");
m_currentAlarm.phAlarm = false;
}
// 浊度报警显示
if (data.turbidity > m_currentThreshold.turbMax) {
ui->turbValueLabel->setStyleSheet("color: red; font-weight: bold");
m_currentAlarm.turbAlarm = true;
} else {
ui->turbValueLabel->setStyleSheet("color: black");
m_currentAlarm.turbAlarm = false;
}
// 溶解氧报警显示
if (data.dissolvedOxygen < m_currentThreshold.doMin) {
ui->doValueLabel->setStyleSheet("color: red; font-weight: bold");
m_currentAlarm.doAlarm = true;
} else {
ui->doValueLabel->setStyleSheet("color: black");
m_currentAlarm.doAlarm = false;
}
}
void MainWindow::addDataToTable(const WaterQualityData &data)
{
int row = ui->dataTable->rowCount();
ui->dataTable->insertRow(row);
// 设置单元格内容
ui->dataTable->setItem(row, 0, new QTableWidgetItem(data.timestamp.toString("yyyy-MM-dd hh:mm:ss")));
ui->dataTable->setItem(row, 1, new QTableWidgetItem(QString::number(data.temperature, 'f', 1)));
ui->dataTable->setItem(row, 2, new QTableWidgetItem(QString::number(data.phValue, 'f', 2)));
ui->dataTable->setItem(row, 3, new QTableWidgetItem(QString::number(data.turbidity, 'f', 1)));
ui->dataTable->setItem(row, 4, new QTableWidgetItem(QString::number(data.dissolvedOxygen, 'f', 2)));
// 根据报警状态设置单元格颜色
if (m_currentAlarm.tempAlarm && row == ui->dataTable->rowCount() - 1) {
ui->dataTable->item(row, 1)->setBackground(Qt::red);
}
if (m_currentAlarm.phAlarm && row == ui->dataTable->rowCount() - 1) {
ui->dataTable->item(row, 2)->setBackground(Qt::red);
}
if (m_currentAlarm.turbAlarm && row == ui->dataTable->rowCount() - 1) {
ui->dataTable->item(row, 3)->setBackground(Qt::red);
}
if (m_currentAlarm.doAlarm && row == ui->dataTable->rowCount() - 1) {
ui->dataTable->item(row, 4)->setBackground(Qt::red);
}
// 自动滚动到最新数据
ui->dataTable->scrollToBottom();
}
void MainWindow::checkAlarm(const WaterQualityData &data)
{
bool hasAlarm = false;
QString alarmMsg;
if (data.temperature > m_currentThreshold.tempMax) {
hasAlarm = true;
alarmMsg += tr("温度过高(%.1f℃ > %.1f℃); ").arg(data.temperature).arg(m_currentThreshold.tempMax);
}
if (data.temperature < m_currentThreshold.tempMin) {
hasAlarm = true;
alarmMsg += tr("温度过低(%.1f℃ < %.1f℃); ").arg(data.temperature).arg(m_currentThreshold.tempMin);
}
if (data.phValue > m_currentThreshold.phMax) {
hasAlarm = true;
alarmMsg += tr("PH过高(%.2f > %.2f); ").arg(data.phValue).arg(m_currentThreshold.phMax);
}
if (data.phValue < m_currentThreshold.phMin) {
hasAlarm = true;
alarmMsg += tr("PH过低(%.2f < %.2f); ").arg(data.phValue).arg(m_currentThreshold.phMin);
}
if (data.turbidity > m_currentThreshold.turbMax) {
hasAlarm = true;
alarmMsg += tr("浊度过高(%.1fNTU > %.1fNTU); ").arg(data.turbidity).arg(m_currentThreshold.turbMax);
}
if (data.dissolvedOxygen < m_currentThreshold.doMin) {
hasAlarm = true;
alarmMsg += tr("溶解氧过低(%.2fmg/L < %.2fmg/L); ").arg(data.dissolvedOxygen).arg(m_currentThreshold.doMin);
}
if (hasAlarm) {
updateStatusBar(tr("报警: %1").arg(alarmMsg));
// 在Windows平台上播放声音
#ifdef Q_OS_WIN
QApplication::beep();
#endif
// 在Android平台上振动
#ifdef Q_OS_ANDROID
// 需要Android权限
// QAndroidJniObject::callStaticMethod<void>("android/os/Vibrator", "vibrate", "(J)V", 500);
#endif
}
}
void MainWindow::updateStatusBar(const QString &message)
{
m_statusLabel->setText(message);
}
void MainWindow::closeEvent(QCloseEvent *event)
{
if (m_mqttManager && m_mqttManager->isConnected()) {
m_mqttManager->disconnectFromServer();
}
event->accept();
}
5.6 图表组件代码
// chartwidget.h
#ifndef CHARTWIDGET_H
#define CHARTWIDGET_H
#include <QWidget>
#include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#include <QtCharts/QValueAxis>
#include "datamodel.h"
QT_CHARTS_BEGIN_NAMESPACE
class QChart;
QT_CHARTS_END_NAMESPACE
QT_CHARTS_USE_NAMESPACE
class ChartWidget : public QChartView
{
Q_OBJECT
public:
explicit ChartWidget(QWidget *parent = nullptr);
~ChartWidget();
void updateData(const WaterQualityData &data);
void clearData();
private:
void initChart();
private:
QChart *m_chart;
QLineSeries *m_tempSeries;
QLineSeries *m_phSeries;
QLineSeries *m_turbSeries;
QLineSeries *m_doSeries;
QValueAxis *m_axisX;
QValueAxis *m_axisY;
int m_pointCount;
static const int MAX_POINTS = 100;
};
#endif // CHARTWIDGET_H
// chartwidget.cpp
#include "chartwidget.h"
#include <QDateTime>
ChartWidget::ChartWidget(QWidget *parent)
: QChartView(parent)
, m_chart(nullptr)
, m_tempSeries(nullptr)
, m_phSeries(nullptr)
, m_turbSeries(nullptr)
, m_doSeries(nullptr)
, m_axisX(nullptr)
, m_axisY(nullptr)
, m_pointCount(0)
{
initChart();
}
ChartWidget::~ChartWidget()
{
}
void ChartWidget::initChart()
{
// 创建数据系列
m_tempSeries = new QLineSeries();
m_tempSeries->setName("温度(℃)");
m_tempSeries->setColor(Qt::red);
m_phSeries = new QLineSeries();
m_phSeries->setName("PH值");
m_phSeries->setColor(Qt::green);
m_turbSeries = new QLineSeries();
m_turbSeries->setName("浊度(NTU)");
m_turbSeries->setColor(Qt::blue);
m_doSeries = new QLineSeries();
m_doSeries->setName("溶解氧(mg/L)");
m_doSeries->setColor(Qt::darkYellow);
// 创建图表
m_chart = new QChart();
m_chart->addSeries(m_tempSeries);
m_chart->addSeries(m_phSeries);
m_chart->addSeries(m_turbSeries);
m_chart->addSeries(m_doSeries);
m_chart->setTitle("水质参数实时曲线");
m_chart->setAnimationOptions(QChart::SeriesAnimations);
// 创建坐标轴
m_axisX = new QValueAxis();
m_axisX->setTitleText("时间点");
m_axisX->setRange(0, MAX_POINTS);
m_axisX->setLabelFormat("%d");
m_axisY = new QValueAxis();
m_axisY->setTitleText("数值");
m_axisY->setRange(0, 100);
// 添加坐标轴
m_chart->addAxis(m_axisX, Qt::AlignBottom);
m_chart->addAxis(m_axisY, Qt::AlignLeft);
// 绑定数据系列到坐标轴
m_tempSeries->attachAxis(m_axisX);
m_tempSeries->attachAxis(m_axisY);
m_phSeries->attachAxis(m_axisX);
m_phSeries->attachAxis(m_axisY);
m_turbSeries->attachAxis(m_axisX);
m_turbSeries->attachAxis(m_axisY);
m_doSeries->attachAxis(m_axisX);
m_doSeries->attachAxis(m_axisY);
// 设置图例
m_chart->legend()->setVisible(true);
m_chart->legend()->setAlignment(Qt::AlignBottom);
setChart(m_chart);
setRenderHint(QPainter::Antialiasing);
}
void ChartWidget::updateData(const WaterQualityData &data)
{
// 添加数据点
m_tempSeries->append(m_pointCount, data.temperature);
m_phSeries->append(m_pointCount, data.phValue * 10); // PH值乘以10便于显示
m_turbSeries->append(m_pointCount, data.turbidity);
m_doSeries->append(m_pointCount, data.dissolvedOxygen);
m_pointCount++;
// 限制数据点数量
if (m_pointCount > MAX_POINTS) {
m_tempSeries->remove(0);
m_phSeries->remove(0);
m_turbSeries->remove(0);
m_doSeries->remove(0);
// 更新X轴范围
m_axisX->setRange(m_pointCount - MAX_POINTS, m_pointCount);
} else {
m_axisX->setRange(0, MAX_POINTS);
}
// 动态调整Y轴范围
double maxValue = 0;
for (QPointF point : m_tempSeries->points()) {
if (point.y() > maxValue) maxValue = point.y();
}
for (QPointF point : m_phSeries->points()) {
if (point.y() > maxValue) maxValue = point.y();
}
for (QPointF point : m_turbSeries->points()) {
if (point.y() > maxValue) maxValue = point.y();
}
for (QPointF point : m_doSeries->points()) {
if (point.y() > maxValue) maxValue = point.y();
}
if (maxValue > 0) {
m_axisY->setRange(0, maxValue * 1.1);
}
// 刷新图表
m_chart->update();
}
void ChartWidget::clearData()
{
m_tempSeries->clear();
m_phSeries->clear();
m_turbSeries->clear();
m_doSeries->clear();
m_pointCount = 0;
m_axisX->setRange(0, MAX_POINTS);
m_chart->update();
}
5.7 设置对话框代码
// settingsdialog.h
#ifndef SETTINGSDIALOG_H
#define SETTINGSDIALOG_H
#include <QDialog>
namespace Ui {
class SettingsDialog;
}
class SettingsDialog : public QDialog
{
Q_OBJECT
public:
explicit SettingsDialog(QWidget *parent = nullptr);
~SettingsDialog();
QString getHost() const;
quint16 getPort() const;
QString getClientId() const;
QString getUsername() const;
QString getPassword() const;
QString getDeviceId() const;
void setHost(const QString &host);
void setPort(quint16 port);
void setClientId(const QString &clientId);
void setUsername(const QString &username);
void setPassword(const QString &password);
void setDeviceId(const QString &deviceId);
private slots:
void onSaveButtonClicked();
void onCancelButtonClicked();
void onTestConnectionButtonClicked();
private:
void loadSettings();
void saveSettings();
private:
Ui::SettingsDialog *ui;
};
#endif // SETTINGSDIALOG_H
// settingsdialog.cpp
#include "settingsdialog.h"
#include "ui_settingsdialog.h"
#include <QSettings>
#include <QMessageBox>
#include <QTcpSocket>
SettingsDialog::SettingsDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::SettingsDialog)
{
ui->setupUi(this);
setWindowTitle(tr("连接设置"));
connect(ui->saveButton, &QPushButton::clicked,
this, &SettingsDialog::onSaveButtonClicked);
connect(ui->cancelButton, &QPushButton::clicked,
this, &SettingsDialog::onCancelButtonClicked);
connect(ui->testButton, &QPushButton::clicked,
this, &SettingsDialog::onTestConnectionButtonClicked);
loadSettings();
}
SettingsDialog::~SettingsDialog()
{
delete ui;
}
QString SettingsDialog::getHost() const
{
return ui->hostEdit->text();
}
quint16 SettingsDialog::getPort() const
{
return ui->portEdit->text().toUShort();
}
QString SettingsDialog::getClientId() const
{
return ui->clientIdEdit->text();
}
QString SettingsDialog::getUsername() const
{
return ui->usernameEdit->text();
}
QString SettingsDialog::getPassword() const
{
return ui->passwordEdit->text();
}
QString SettingsDialog::getDeviceId() const
{
return ui->deviceIdEdit->text();
}
void SettingsDialog::setHost(const QString &host)
{
ui->hostEdit->setText(host);
}
void SettingsDialog::setPort(quint16 port)
{
ui->portEdit->setText(QString::number(port));
}
void SettingsDialog::setClientId(const QString &clientId)
{
ui->clientIdEdit->setText(clientId);
}
void SettingsDialog::setUsername(const QString &username)
{
ui->usernameEdit->setText(username);
}
void SettingsDialog::setPassword(const QString &password)
{
ui->passwordEdit->setText(password);
}
void SettingsDialog::setDeviceId(const QString &deviceId)
{
ui->deviceIdEdit->setText(deviceId);
}
void SettingsDialog::onSaveButtonClicked()
{
saveSettings();
accept();
}
void SettingsDialog::onCancelButtonClicked()
{
reject();
}
void SettingsDialog::onTestConnectionButtonClicked()
{
QString host = ui->hostEdit->text();
quint16 port = ui->portEdit->text().toUShort();
if (host.isEmpty()) {
QMessageBox::warning(this, tr("警告"), tr("请输入服务器地址"));
return;
}
QTcpSocket socket;
socket.connectToHost(host, port);
if (socket.waitForConnected(3000)) {
QMessageBox::information(this, tr("测试成功"), tr("服务器连接测试成功"));
socket.disconnectFromHost();
} else {
QMessageBox::critical(this, tr("测试失败"),
tr("无法连接到服务器: %1").arg(socket.errorString()));
}
}
void SettingsDialog::loadSettings()
{
QSettings settings("WaterQuality", "Monitor");
ui->hostEdit->setText(settings.value("mqtt/host", "mqtt.huaweicloud.com").toString());
ui->portEdit->setText(settings.value("mqtt/port", 1883).toString());
ui->clientIdEdit->setText(settings.value("mqtt/clientId").toString());
ui->usernameEdit->setText(settings.value("mqtt/username").toString());
ui->passwordEdit->setText(settings.value("mqtt/password").toString());
ui->deviceIdEdit->setText(settings.value("mqtt/deviceId").toString());
}
void SettingsDialog::saveSettings()
{
QSettings settings("WaterQuality", "Monitor");
settings.setValue("mqtt/host", ui->hostEdit->text());
settings.setValue("mqtt/port", ui->portEdit->text().toUShort());
settings.setValue("mqtt/clientId", ui->clientIdEdit->text());
settings.setValue("mqtt/username", ui->usernameEdit->text());
settings.setValue("mqtt/password", ui->passwordEdit->text());
settings.setValue("mqtt/deviceId", ui->deviceIdEdit->text());
}
5.8 主函数代码
// main.cpp
#include "mainwindow.h"
#include <QApplication>
#include <QFontDatabase>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 设置应用程序信息
a.setApplicationName("WaterQualityMonitor");
a.setApplicationVersion("1.0.0");
a.setOrganizationName("WaterQuality");
// 设置字体
QFontDatabase::addApplicationFont(":/fonts/msyh.ttf");
QFont font("Microsoft YaHei", 9);
a.setFont(font);
// 创建并显示主窗口
MainWindow w;
w.show();
return a.exec();
}
以上代码实现了本项目QT上位机的完整功能,包括:
1.MQTT通信管理:实现了MQTT协议的连接、订阅、发布、心跳保持等功能
2. 数据解析与显示:解析设备上报的JSON数据,实时显示水温、PH值、浊度、溶解氧
3. 报警功能:根据阈值判断数据是否异常,红色显示报警项
4. 阈值设置:支持通过云端下发阈值配置
5. 数据记录:历史数据表格显示,支持导出CSV文件
6. 图表展示:实时曲线显示水质参数变化趋势
7. 跨平台支持:同时支持Windows和Android平台
代码采用模块化设计,MQTTManager类独立处理MQTT通信,主窗口负责界面交互和数据展示,便于后续维护和扩展。
【3】解析数据更新界面
根据接口返回的数据更新界面。
【4】判断设备是否离线
这段代码用于判断设备是否离线。通过获取设备上传到服务器数据的时间与本地的系统时间差进行对比。
这段代码的核心功能是通过比较设备上传数据的时间和本地系统时间来判断设备是否处于离线状态,以下是其详细解释:
(1)功能分析
显示最新更新时间
ui->label_update_time->setText("最新时间:" + update_time);
将设备上传的最新时间 update_time 显示在界面上的 label_update_time 控件中,格式为 最新时间:yyyy-MM-dd HH:mm:ss。
方便用户了解设备数据的最近更新时间。
获取本地当前时间
QDateTime currentDateTime = QDateTime::currentDateTime();
使用 QDateTime::currentDateTime() 获取系统当前时间,作为对比基准。
计算时间差
qint64 secondsDiff = currentDateTime.secsTo(dateTime);
secsTo: 计算 currentDateTime 和设备上传时间 dateTime 之间的时间差(单位:秒)。
dateTime 是通过解析 JSON 数据提取到的设备数据上传时间,并已转换为本地时间格式。
判断设备状态
if (qAbs(secondsDiff) >= 5 * 60)
使用 qAbs 获取时间差的绝对值。
如果时间差超过 5 分钟(300秒),表示设备长时间未上传数据,判定为“离线”。
(2)离线处理
更新状态显示
ui->label_dev_state->setText("设备状态:离线");
在界面 label_dev_state 控件中显示设备当前状态为“离线”。
(3)在线处理
状态更新ui->label_dev_state->setText("设备状态:在线");如果时间差小于 5 分钟,显示“设备状态:在线”。
【5】获取设备最新数据上传时间
这是解析华为云API接口返回的数据,解析出来里面设备数据的时间,进行显示。
这段代码的主要作用是解析华为云 API 返回的 JSON 数据中的设备数据时间字段,转换为本地时间格式,并最终以用户友好的标准格式输出到界面。
(1)详细代码解析
(1)提取时间字段
QString event_time = obj3.take("event_time").toString();
qDebug() << "event_time:" << event_time;
obj3.take("event_time"):从 JSON 数据中的 reported 对象提取 event_time 字段,值为一个字符串,表示设备上传数据的时间。
toString():将提取的字段值转换为 QString 类型,便于后续操作。
调试输出:使用 qDebug() 输出提取的时间值,例如:20231121T120530Z。
2. 转换为 QDateTime 对象
QDateTime dateTime = QDateTime::fromString(event_time, "yyyyMMddTHHmmssZ");
QDateTime::fromString:
使用指定格式解析 event_time 字符串为 QDateTime 对象。
格式说明:
• yyyyMMdd: 年、月、日(如 20231121)。
•T: 时间部分的分隔符(固定为 T)。
• HHmmss: 时、分、秒(如 120530)。
• Z: 表示时间是 UTC 时间。
• 如果时间字符串格式不匹配,会返回一个无效的 QDateTime 对象。
3. 转换时区到本地时间
dateTime.setTimeSpec(Qt::UTC);
dateTime = dateTime.toLocalTime();
setTimeSpec(Qt::UTC):
• 明确告知 dateTime 对象,当前时间是 UTC 时间。
• 确保时间转换准确,避免因为默认时区不明确导致的误差。
toLocalTime():
• 将时间从 UTC 转换为本地时区时间,例如中国标准时间(CST, UTC+8)。
4. 格式化输出为标准时间字符串
QString update_time = dateTime.toString("yyyy-MM-dd HH:mm:ss");
toString():将 QDateTime 转换为指定格式的字符串。
格式说明:
yyyy-MM-dd: 年-月-日。
HH:mm:ss: 小时:分钟:秒。
示例结果:2023-11-21 20:05:30。
用户显示友好性:转换后的格式易读,符合国际通用的日期时间表示规范。
(2)代码运行效果
假设 API 返回的时间字段值为 20231121T120530Z。
转换流程:
1. 解析为 QDateTime 对象:2023-11-21 12:05:30 (UTC);
2. 转换为本地时间:2023-11-21 20:05:30 (CST)。
3. 格式化输出:"2023-11-21 20:05:30"。
输出到界面时,显示为:
最新时间: 2023-11-21 20:05:30
5.5 编译Windows上位机
点击软件左下角的绿色三角形按钮进行编译运行。
5.6 配置Android环境
如果想编译Android手机APP,必须要先自己配置好自己的Android环境。(搭建环境的过程可以自行百度搜索学习)
然后才可以进行下面的步骤。
【1】选择Android编译器
选择编译器。
切换编译器。
【2】创建Android配置文件
创建完成。
【3】配置Android图标与名称
根据自己的需求配置 Android图标与名称。
【3】编译Android上位机
Qt本身是跨平台的,直接选择Android的编译器,就可以将程序编译到Android平台。
然后点击构建。
成功之后,在目录下可以看到生成的apk文件,也就是Android手机的安装包,电脑端使用QQ发送给手机QQ,手机登录QQ接收,就能直接安装。
生成的apk的目录在哪里呢? 编译完成之后,在控制台会输出APK文件的路径。
知道目录在哪里之后,在Windows的文件资源管理器里,找到路径,具体看下图,找到生成的apk文件。
File: D:/QtProject/build-333_QtProject-Android_for_arm64_v8a_Clang_Qt_5_12_6_for_Android_ARM64_v8a-Release/android-build//build/outputs/apk/debug/android-build-debug.apk
5.7 设备仿真调试
通过MQTT客户端模拟设备登录华为云服务器。进行设备联调,实现数据上传和下发测试。
五、STM32代码设计
5.1 硬件连线说明
本系统采用STM32F103C8T6作为主控芯片,各硬件模块与STM32的引脚连接关系如下表所示:
| 硬件模块 | 引脚名称 | STM32引脚 | 说明 |
|---|---|---|---|
| DS18B20水温传感器 | DATA | PA0 | 单总线数据线,需外接4.7K上拉电阻 |
| PH值传感器 | AO | PA1 | 模拟电压输出,连接ADC1通道1 |
| 浊度传感器 | AO | PA2 | 模拟电压输出,连接ADC1通道2 |
| 485转串口模块(溶解氧) | RX | PA9 (USART1_TX) | 连接模块的TX引脚 |
| TX | PA10 (USART1_RX) | 连接模块的RX引脚 | |
| OLED显示屏 | SCL | PB6 | I2C1时钟线 |
| SDA | PB7 | I2C1数据线 | |
| ESP8266 Wi-Fi模块 | TX | PA2 (USART2_RX) | 连接模块的RX引脚 |
| RX | PA3 (USART2_TX) | 连接模块的TX引脚 | |
| VCC | 3.3V | 模块供电 | |
| GND | GND | 地线 | |
| 蓝牙模块 | TX | PA3 (USART2_RX) | 实际使用USART2,需注意复用 |
| RX | PA2 (USART2_TX) | 实际通过分时复用或使用USART3 | |
| 蜂鸣器 | I/O | PB8 | 高电平触发 |
| 直流电机(前进后退) | IN1 | PB12 | 电机驱动H桥控制引脚1 |
| IN2 | PB13 | 电机驱动H桥控制引脚2 | |
| EN | PB14 | 电机PWM使能控制 | |
| SG90舵机 | PWM | PB15 | 舵机PWM控制信号 |
| 按键/调试接口 | SWDIO | PA13 | 程序下载调试 |
| SWCLK | PA14 | 程序下载调试 |
说明: 实际设计中,蓝牙模块与ESP8266共用USART2时,可通过分时复用方式,或使用USART3单独连接蓝牙模块。建议将ESP8266使用USART2,蓝牙模块使用USART3,以避免冲突。
5.2 传感器代码
5.2.1 DS18B20温度传感器驱动代码
// ds18b20.h
#ifndef __DS18B20_H
#define __DS18B20_H
#include "stm32f10x.h"
#define DS18B20_GPIO_PORT GPIOA
#define DS18B20_GPIO_PIN GPIO_Pin_0
#define DS18B20_GPIO_CLK RCC_APB2Periph_GPIOA
// 宏定义操作
#define DS18B20_DQ_OUT_HIGH() GPIO_SetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN)
#define DS18B20_DQ_OUT_LOW() GPIO_ResetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN)
#define DS18B20_DQ_IN() GPIO_ReadInputDataBit(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN)
// 函数声明
void DS18B20_Init(void);
void DS18B20_Reset(void);
void DS18B20_WriteByte(uint8_t data);
uint8_t DS18B20_ReadByte(void);
float DS18B20_GetTemperature(void);
#endif
// ds18b20.c
#include "ds18b20.h"
#include "delay.h"
// 延时函数(微秒级,需要根据系统时钟配置)
static void Delay_us(uint32_t us)
{
uint32_t i;
for(i = 0; i < us * 8; i++)
{
__NOP();
}
}
// DS18B20初始化
void DS18B20_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIO时钟
RCC_APB2PeriphClockCmd(DS18B20_GPIO_CLK, ENABLE);
// 配置为推挽输出
GPIO_InitStructure.GPIO_Pin = DS18B20_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure);
// 拉高引脚
DS18B20_DQ_OUT_HIGH();
}
// DS18B20复位
void DS18B20_Reset(void)
{
// 配置为输出模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = DS18B20_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure);
// 拉低总线480us
DS18B20_DQ_OUT_LOW();
Delay_us(480);
// 释放总线
DS18B20_DQ_OUT_HIGH();
Delay_us(60);
// 配置为输入模式检测应答
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure);
// 等待应答信号(低电平)
while(DS18B20_DQ_IN());
// 等待应答信号结束
while(!DS18B20_DQ_IN());
// 恢复为输出模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure);
}
// DS18B20写一个字节
void DS18B20_WriteByte(uint8_t data)
{
uint8_t i;
// 配置为输出模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = DS18B20_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure);
for(i = 0; i < 8; i++)
{
// 拉低总线
DS18B20_DQ_OUT_LOW();
Delay_us(2);
// 发送数据位
if(data & 0x01)
{
DS18B20_DQ_OUT_HIGH();
}
else
{
DS18B20_DQ_OUT_LOW();
}
Delay_us(60);
// 释放总线
DS18B20_DQ_OUT_HIGH();
Delay_us(2);
data >>= 1;
}
}
// DS18B20读一个字节
uint8_t DS18B20_ReadByte(void)
{
uint8_t i, data = 0;
// 配置为输出模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = DS18B20_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure);
for(i = 0; i < 8; i++)
{
data >>= 1;
// 拉低总线
DS18B20_DQ_OUT_LOW();
Delay_us(2);
// 释放总线
DS18B20_DQ_OUT_HIGH();
Delay_us(2);
// 配置为输入模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure);
// 读取数据位
if(DS18B20_DQ_IN())
{
data |= 0x80;
}
Delay_us(60);
// 恢复输出模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure);
}
return data;
}
// 获取温度值
float DS18B20_GetTemperature(void)
{
uint8_t tempL, tempH;
int16_t temp;
float temperature;
// 复位DS18B20
DS18B20_Reset();
// 跳过ROM
DS18B20_WriteByte(0xCC);
// 启动温度转换
DS18B20_WriteByte(0x44);
// 等待转换完成(约750ms)
Delay_ms(750);
// 复位DS18B20
DS18B20_Reset();
// 跳过ROM
DS18B20_WriteByte(0xCC);
// 读取暂存器
DS18B20_WriteByte(0xBE);
// 读取温度值
tempL = DS18B20_ReadByte();
tempH = DS18B20_ReadByte();
// 合成温度值
temp = (int16_t)((tempH << 8) | tempL);
// 转换为实际温度(分辨率0.0625)
temperature = temp * 0.0625;
return temperature;
}
5.2.2 PH值传感器驱动代码
// ph_sensor.h
#ifndef __PH_SENSOR_H
#define __PH_SENSOR_H
#include "stm32f10x.h"
#define PH_ADC_PORT GPIOA
#define PH_ADC_PIN GPIO_Pin_1
#define PH_ADC_CHANNEL ADC_Channel_1
// 函数声明
void PH_Sensor_Init(void);
float PH_Sensor_GetValue(void);
void PH_Sensor_Calibrate(float ph4_value, float ph7_value);
#endif
// ph_sensor.c
#include "ph_sensor.h"
#include "adc.h"
// PH校准参数
static float ph_voltage_4 = 2.5f; // PH4对应的电压值
static float ph_voltage_7 = 2.0f; // PH7对应的电压值
static float ph_slope = 3.0f; // 斜率
// PH传感器初始化
void PH_Sensor_Init(void)
{
// ADC初始化在adc.c中完成,这里只需使能相关时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置PA1为模拟输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = PH_ADC_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(PH_ADC_PORT, &GPIO_InitStructure);
}
// 获取PH值
float PH_Sensor_GetValue(void)
{
uint16_t adc_value;
float voltage;
float ph_value;
// 读取ADC值(假设ADC已配置为连续转换模式)
adc_value = ADC_GetConversionValue(ADC1);
// 转换为电压值(参考电压3.3V,12位分辨率4096)
voltage = (float)adc_value * 3.3f / 4096.0f;
// 根据电压计算PH值
// PH = 7 + (voltage - ph_voltage_7) / ph_slope
ph_value = 7.0f + (voltage - ph_voltage_7) / ph_slope;
// 限制PH值范围
if(ph_value < 0.0f) ph_value = 0.0f;
if(ph_value > 14.0f) ph_value = 14.0f;
return ph_value;
}
// PH传感器校准
void PH_Sensor_Calibrate(float ph4_value, float ph7_value)
{
// 读取PH4溶液对应的电压
uint16_t adc_val_4 = ADC_GetConversionValue(ADC1);
float vol_4 = (float)adc_val_4 * 3.3f / 4096.0f;
// 读取PH7溶液对应的电压
uint16_t adc_val_7 = ADC_GetConversionValue(ADC1);
float vol_7 = (float)adc_val_7 * 3.3f / 4096.0f;
// 计算斜率
ph_slope = (vol_7 - vol_4) / (7.0f - 4.0f);
// 保存校准值
ph_voltage_4 = vol_4;
ph_voltage_7 = vol_7;
}
5.2.3 浊度传感器驱动代码
// turbidity_sensor.h
#ifndef __TURBIDITY_SENSOR_H
#define __TURBIDITY_SENSOR_H
#include "stm32f10x.h"
#define TURB_ADC_PORT GPIOA
#define TURB_ADC_PIN GPIO_Pin_2
#define TURB_ADC_CHANNEL ADC_Channel_2
// 函数声明
void Turbidity_Sensor_Init(void);
float Turbidity_Sensor_GetValue(void);
#endif
// turbidity_sensor.c
#include "turbidity_sensor.h"
#include "adc.h"
// 浊度传感器初始化
void Turbidity_Sensor_Init(void)
{
// 配置PA2为模拟输入
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = TURB_ADC_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(TURB_ADC_PORT, &GPIO_InitStructure);
}
// 获取浊度值(NTU)
float Turbidity_Sensor_GetValue(void)
{
uint16_t adc_value;
float voltage;
float turbidity;
// 读取ADC值
adc_value = ADC_GetConversionValue(ADC1);
// 转换为电压值
voltage = (float)adc_value * 3.3f / 4096.0f;
// 根据电压计算浊度值
// 电压与浊度成反比关系:浊度 = (电压阈值 - 电压) * 系数
// 实际需要根据传感器特性曲线进行标定
if(voltage >= 2.5f)
{
turbidity = 0.0f;
}
else
{
turbidity = (2.5f - voltage) / 2.5f * 1000.0f;
}
// 限制浊度范围
if(turbidity < 0.0f) turbidity = 0.0f;
if(turbidity > 1000.0f) turbidity = 1000.0f;
return turbidity;
}
5.2.4 溶解氧传感器驱动代码
// do_sensor.h
#ifndef __DO_SENSOR_H
#define __DO_SENSOR_H
#include "stm32f10x.h"
// 函数声明
void DO_Sensor_Init(uint32_t baudrate);
void DO_Sensor_SendCmd(uint8_t *cmd, uint8_t len);
float DO_Sensor_GetValue(void);
#endif
// do_sensor.c
#include "do_sensor.h"
#include "usart.h"
// 溶解氧传感器数据接收缓冲区
static uint8_t do_rx_buffer[64];
static uint8_t do_rx_index = 0;
// 溶解氧传感器初始化
void DO_Sensor_Init(uint32_t baudrate)
{
// USART1初始化,用于与485转串口模块通信
USART_InitTypeDef USART_InitStructure;
// 使能USART1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
// 配置USART1
USART_InitStructure.USART_BaudRate = baudrate;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
// 使能USART1
USART_Cmd(USART1, ENABLE);
// 使能接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}
// 发送命令
void DO_Sensor_SendCmd(uint8_t *cmd, uint8_t len)
{
uint8_t i;
for(i = 0; i < len; i++)
{
USART_SendData(USART1, cmd[i]);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
}
// 获取溶解氧值
float DO_Sensor_GetValue(void)
{
// Modbus RTU读取命令(示例:读取溶解氧值)
// 实际命令需要根据传感器协议确定
uint8_t read_cmd[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A};
uint16_t do_raw;
float do_value;
// 清空接收缓冲区
do_rx_index = 0;
memset(do_rx_buffer, 0, sizeof(do_rx_buffer));
// 发送读取命令
DO_Sensor_SendCmd(read_cmd, sizeof(read_cmd));
// 等待接收完成(实际应用中需要超时处理)
Delay_ms(100);
// 解析接收到的数据
if(do_rx_index >= 5)
{
// 提取溶解氧值(根据协议解析)
do_raw = (do_rx_buffer[3] << 8) | do_rx_buffer[4];
do_value = (float)do_raw / 10.0f;
}
else
{
do_value = -1.0f; // 读取失败
}
return do_value;
}
// USART1中断服务函数
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
if(do_rx_index < sizeof(do_rx_buffer))
{
do_rx_buffer[do_rx_index++] = data;
}
}
}
5.2.5 OLED显示屏驱动代码
// oled.h
#ifndef __OLED_H
#define __OLED_H
#include "stm32f10x.h"
#define OLED_SCL_PORT GPIOB
#define OLED_SCL_PIN GPIO_Pin_6
#define OLED_SDA_PORT GPIOB
#define OLED_SDA_PIN GPIO_Pin_7
// I2C操作宏
#define OLED_SCL_HIGH() GPIO_SetBits(OLED_SCL_PORT, OLED_SCL_PIN)
#define OLED_SCL_LOW() GPIO_ResetBits(OLED_SCL_PORT, OLED_SCL_PIN)
#define OLED_SDA_HIGH() GPIO_SetBits(OLED_SDA_PORT, OLED_SDA_PIN)
#define OLED_SDA_LOW() GPIO_ResetBits(OLED_SDA_PORT, OLED_SDA_PIN)
#define OLED_SDA_READ() GPIO_ReadInputDataBit(OLED_SDA_PORT, OLED_SDA_PIN)
// 函数声明
void OLED_Init(void);
void OLED_Clear(void);
void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t chr);
void OLED_ShowString(uint8_t x, uint8_t y, char *str);
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len);
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t precision);
#endif
// oled.c
#include "oled.h"
#include "font.h"
#include "delay.h"
// I2C延时
static void I2C_Delay(void)
{
uint8_t i;
for(i = 0; i < 10; i++)
{
__NOP();
}
}
// I2C起始信号
static void I2C_Start(void)
{
OLED_SDA_HIGH();
OLED_SCL_HIGH();
I2C_Delay();
OLED_SDA_LOW();
I2C_Delay();
OLED_SCL_LOW();
I2C_Delay();
}
// I2C停止信号
static void I2C_Stop(void)
{
OLED_SDA_LOW();
OLED_SCL_HIGH();
I2C_Delay();
OLED_SDA_HIGH();
I2C_Delay();
}
// 发送一个字节
static void I2C_SendByte(uint8_t byte)
{
uint8_t i;
for(i = 0; i < 8; i++)
{
if(byte & 0x80)
{
OLED_SDA_HIGH();
}
else
{
OLED_SDA_LOW();
}
I2C_Delay();
OLED_SCL_HIGH();
I2C_Delay();
OLED_SCL_LOW();
I2C_Delay();
byte <<= 1;
}
}
// 发送命令
static void OLED_WriteCmd(uint8_t cmd)
{
I2C_Start();
I2C_SendByte(0x78); // OLED地址
I2C_SendByte(0x00); // 命令模式
I2C_SendByte(cmd);
I2C_Stop();
}
// 发送数据
static void OLED_WriteData(uint8_t data)
{
I2C_Start();
I2C_SendByte(0x78); // OLED地址
I2C_SendByte(0x40); // 数据模式
I2C_SendByte(data);
I2C_Stop();
}
// OLED初始化
void OLED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置SCL和SDA为推挽输出
GPIO_InitStructure.GPIO_Pin = OLED_SCL_PIN | OLED_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(OLED_SCL_PORT, &GPIO_InitStructure);
// 初始化引脚电平
OLED_SCL_HIGH();
OLED_SDA_HIGH();
I2C_Delay();
// OLED初始化命令序列
Delay_ms(100);
OLED_WriteCmd(0xAE); // 关闭显示
OLED_WriteCmd(0xD5); // 设置时钟分频因子
OLED_WriteCmd(0x80);
OLED_WriteCmd(0xA8); // 设置多路复用率
OLED_WriteCmd(0x3F);
OLED_WriteCmd(0xD3); // 设置显示偏移
OLED_WriteCmd(0x00);
OLED_WriteCmd(0x40); // 设置起始行
OLED_WriteCmd(0x8D); // 电荷泵设置
OLED_WriteCmd(0x14);
OLED_WriteCmd(0x20); // 设置内存寻址模式
OLED_WriteCmd(0x00);
OLED_WriteCmd(0xA1); // 段重映射
OLED_WriteCmd(0xC8); // COM扫描方向
OLED_WriteCmd(0xDA); // 设置COM引脚硬件配置
OLED_WriteCmd(0x12);
OLED_WriteCmd(0x81); // 设置对比度
OLED_WriteCmd(0xCF);
OLED_WriteCmd(0xD9); // 设置预充电周期
OLED_WriteCmd(0xF1);
OLED_WriteCmd(0xDB); // 设置VCOMH
OLED_WriteCmd(0x40);
OLED_WriteCmd(0xA4); // 显示全开
OLED_WriteCmd(0xA6); // 正常显示
OLED_WriteCmd(0xAF); // 开启显示
OLED_Clear();
}
// 清屏
void OLED_Clear(void)
{
uint8_t i, j;
for(i = 0; i < 8; i++)
{
OLED_WriteCmd(0xB0 + i); // 设置页地址
OLED_WriteCmd(0x00); // 设置列地址低4位
OLED_WriteCmd(0x10); // 设置列地址高4位
for(j = 0; j < 128; j++)
{
OLED_WriteData(0x00);
}
}
}
// 显示一个字符
void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t chr)
{
uint8_t i;
uint8_t c = chr - ' ';
if(x > 120) return;
OLED_WriteCmd(0xB0 + y);
OLED_WriteCmd(((x & 0xF0) >> 4) | 0x10);
OLED_WriteCmd((x & 0x0F) | 0x00);
for(i = 0; i < 8; i++)
{
OLED_WriteData(Font8x16[c][i]);
}
}
// 显示字符串
void OLED_ShowString(uint8_t x, uint8_t y, char *str)
{
while(*str)
{
OLED_ShowChar(x, y, *str);
x += 8;
str++;
if(x > 120)
{
x = 0;
y++;
}
}
}
// 显示数字
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len)
{
uint8_t i;
uint8_t buf[10];
for(i = 0; i < len; i++)
{
buf[i] = num % 10 + '0';
num /= 10;
}
for(i = len; i > 0; i--)
{
OLED_ShowChar(x, y, buf[i-1]);
x += 8;
}
}
// 显示浮点数
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t precision)
{
uint32_t integer_part;
uint32_t decimal_part;
integer_part = (uint32_t)num;
decimal_part = (uint32_t)((num - integer_part) * (precision == 1 ? 10 : (precision == 2 ? 100 : 1000)));
OLED_ShowNum(x, y, integer_part, 2);
OLED_ShowChar(x + 16, y, '.');
OLED_ShowNum(x + 24, y, decimal_part, precision);
}
5.3 项目核心代码
5.3.1 主程序代码
// main.c
#include "stm32f10x.h"
#include "delay.h"
#include "usart.h"
#include "adc.h"
#include "ds18b20.h"
#include "ph_sensor.h"
#include "turbidity_sensor.h"
#include "do_sensor.h"
#include "oled.h"
#include "esp8266.h"
#include "bluetooth.h"
#include "motor.h"
#include "servo.h"
#include "buzzer.h"
// 系统参数结构体
typedef struct
{
float temperature; // 水温
float ph_value; // PH值
float turbidity; // 浊度
float do_value; // 溶解氧
float temp_max; // 温度上限
float temp_min; // 温度下限
float ph_max; // PH上限
float ph_min; // PH下限
float turb_max; // 浊度上限
float do_min; // 溶解氧下限
} SystemParam;
SystemParam g_sys_param;
// 报警标志
uint8_t g_alarm_flag = 0;
// 初始化系统
void System_Init(void)
{
// 初始化系统时钟
SystemInit();
// 初始化延时函数
Delay_Init();
// 初始化USART
USART2_Init(115200); // ESP8266
USART3_Init(9600); // 蓝牙模块
// 初始化ADC
ADC1_Init();
// 初始化传感器
DS18B20_Init();
PH_Sensor_Init();
Turbidity_Sensor_Init();
DO_Sensor_Init(9600);
// 初始化OLED
OLED_Init();
OLED_Clear();
// 初始化ESP8266
ESP8266_Init();
// 初始化电机和舵机
Motor_Init();
Servo_Init();
// 初始化蜂鸣器
Buzzer_Init();
// 加载默认阈值
g_sys_param.temp_max = 35.0f;
g_sys_param.temp_min = 5.0f;
g_sys_param.ph_max = 8.5f;
g_sys_param.ph_min = 6.5f;
g_sys_param.turb_max = 50.0f;
g_sys_param.do_min = 3.0f;
}
// 采集传感器数据
void Sensor_Collect(void)
{
// 采集水温
g_sys_param.temperature = DS18B20_GetTemperature();
// 采集PH值
g_sys_param.ph_value = PH_Sensor_GetValue();
// 采集浊度
g_sys_param.turbidity = Turbidity_Sensor_GetValue();
// 采集溶解氧
g_sys_param.do_value = DO_Sensor_GetValue();
}
// 报警判断
void Alarm_Check(void)
{
uint8_t alarm = 0;
// 检查水温
if(g_sys_param.temperature > g_sys_param.temp_max ||
g_sys_param.temperature < g_sys_param.temp_min)
{
alarm = 1;
}
// 检查PH值
if(g_sys_param.ph_value > g_sys_param.ph_max ||
g_sys_param.ph_value < g_sys_param.ph_min)
{
alarm = 1;
}
// 检查浊度
if(g_sys_param.turbidity > g_sys_param.turb_max)
{
alarm = 1;
}
// 检查溶解氧
if(g_sys_param.do_value < g_sys_param.do_min)
{
alarm = 1;
}
if(alarm)
{
g_alarm_flag = 1;
Buzzer_On();
}
else
{
g_alarm_flag = 0;
Buzzer_Off();
}
}
// 更新OLED显示
void OLED_Update(void)
{
static uint8_t page = 0;
char buffer[32];
OLED_Clear();
// 显示标题
OLED_ShowString(0, 0, "Water Quality Monitor");
// 显示水温
sprintf(buffer, "Temp:%.1f C", g_sys_param.temperature);
OLED_ShowString(0, 2, buffer);
// 显示PH值
sprintf(buffer, "PH:%.2f", g_sys_param.ph_value);
OLED_ShowString(0, 3, buffer);
// 显示浊度
sprintf(buffer, "Turb:%.1f NTU", g_sys_param.turbidity);
OLED_ShowString(0, 4, buffer);
// 显示溶解氧
sprintf(buffer, "DO:%.2f mg/L", g_sys_param.do_value);
OLED_ShowString(0, 5, buffer);
// 显示报警状态
if(g_alarm_flag)
{
OLED_ShowString(0, 7, "ALARM!");
}
}
// 上传数据到云端
void Data_Upload(void)
{
char mqtt_data[256];
// 构建MQTT消息
sprintf(mqtt_data,
"{"services":[{"service_id":"WaterQuality","properties":{"
""DS18B20":%.1f,"
""PH":%.2f,"
""Turbidity":%.1f,"
""oxygen":%.2f}}]}",
g_sys_param.temperature,
g_sys_param.ph_value,
g_sys_param.turbidity,
g_sys_param.do_value);
// 发送MQTT消息
ESP8266_MQTT_Publish(mqtt_data);
}
// 处理云端下发的阈值
void Threshold_Process(void)
{
// 通过MQTT订阅接收云端下发的阈值配置
// 实际应用中需要解析JSON数据
// 示例:假设接收到新的阈值
// g_sys_param.temp_max = 30.0f;
// g_sys_param.temp_min = 10.0f;
// ...
}
// 处理蓝牙遥控指令
void Bluetooth_Process(void)
{
uint8_t cmd;
if(USART3_ReceiveData(&cmd))
{
switch(cmd)
{
case 'F': // 前进
Motor_Forward();
break;
case 'B': // 后退
Motor_Backward();
break;
case 'L': // 左转
Servo_Left();
break;
case 'R': // 右转
Servo_Right();
break;
case 'S': // 停止
Motor_Stop();
Servo_Center();
break;
default:
break;
}
}
}
// 主函数
int main(void)
{
// 系统初始化
System_Init();
// 显示启动信息
OLED_ShowString(0, 0, "System Starting...");
Delay_ms(2000);
// 连接华为云MQTT服务器
ESP8266_MQTT_Connect("your_device_id", "your_device_secret");
while(1)
{
// 采集传感器数据
Sensor_Collect();
// 更新OLED显示
OLED_Update();
// 报警判断
Alarm_Check();
// 上传数据到云端
Data_Upload();
// 处理云端下发的阈值
Threshold_Process();
// 处理蓝牙遥控指令
Bluetooth_Process();
// 延时5秒,避免频繁上传
Delay_ms(5000);
}
}
5.3.2 电机驱动代码
// motor.h
#ifndef __MOTOR_H
#define __MOTOR_H
#include "stm32f10x.h"
#define MOTOR_IN1_PORT GPIOB
#define MOTOR_IN1_PIN GPIO_Pin_12
#define MOTOR_IN2_PORT GPIOB
#define MOTOR_IN2_PIN GPIO_Pin_13
#define MOTOR_EN_PORT GPIOB
#define MOTOR_EN_PIN GPIO_Pin_14
// 函数声明
void Motor_Init(void);
void Motor_Forward(void);
void Motor_Backward(void);
void Motor_Stop(void);
void Motor_SetSpeed(uint8_t speed);
#endif
// motor.c
#include "motor.h"
#include "pwm.h"
// 电机初始化
void Motor_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置控制引脚为推挽输出
GPIO_InitStructure.GPIO_Pin = MOTOR_IN1_PIN | MOTOR_IN2_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(MOTOR_IN1_PORT, &GPIO_InitStructure);
// 配置PWM输出引脚
GPIO_InitStructure.GPIO_Pin = MOTOR_EN_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(MOTOR_EN_PORT, &GPIO_InitStructure);
// 初始化PWM
TIM3_PWM_Init(999, 71); // PWM频率1kHz
}
// 电机前进
void Motor_Forward(void)
{
GPIO_SetBits(MOTOR_IN1_PORT, MOTOR_IN1_PIN);
GPIO_ResetBits(MOTOR_IN2_PORT, MOTOR_IN2_PIN);
TIM_SetCompare2(TIM3, 500); // 50%占空比
}
// 电机后退
void Motor_Backward(void)
{
GPIO_ResetBits(MOTOR_IN1_PORT, MOTOR_IN1_PIN);
GPIO_SetBits(MOTOR_IN2_PORT, MOTOR_IN2_PIN);
TIM_SetCompare2(TIM3, 500); // 50%占空比
}
// 电机停止
void Motor_Stop(void)
{
GPIO_ResetBits(MOTOR_IN1_PORT, MOTOR_IN1_PIN);
GPIO_ResetBits(MOTOR_IN2_PORT, MOTOR_IN2_PIN);
TIM_SetCompare2(TIM3, 0);
}
// 设置电机速度
void Motor_SetSpeed(uint8_t speed)
{
uint16_t pwm_value;
if(speed > 100) speed = 100;
pwm_value = speed * 10; // 占空比0-1000对应0-100%
TIM_SetCompare2(TIM3, pwm_value);
}
5.3.3 舵机驱动代码
// servo.h
#ifndef __SERVO_H
#define __SERVO_H
#include "stm32f10x.h"
#define SERVO_PWM_PORT GPIOB
#define SERVO_PWM_PIN GPIO_Pin_15
// 函数声明
void Servo_Init(void);
void Servo_SetAngle(uint8_t angle);
void Servo_Center(void);
void Servo_Left(void);
void Servo_Right(void);
#endif
// servo.c
#include "servo.h"
#include "pwm.h"
// 舵机初始化
void Servo_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置PWM输出引脚
GPIO_InitStructure.GPIO_Pin = SERVO_PWM_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SERVO_PWM_PORT, &GPIO_InitStructure);
// 初始化PWM
TIM4_PWM_Init(19999, 71); // PWM频率50Hz,周期20ms
}
// 设置舵机角度
// 角度范围:0-180度
void Servo_SetAngle(uint8_t angle)
{
uint16_t pwm_value;
if(angle > 180) angle = 180;
// 角度对应PWM占空比:0.5ms~2.5ms
// 0度对应0.5ms,180度对应2.5ms
// 周期20ms,计数值0-19999,高电平时间 = pwm_value / 20000 * 20ms
// pwm_value = (angle / 180 * 2000 + 500) / 20 * 20000 / 20000
// 简化:pwm_value = 500 + angle * 1000 / 180
pwm_value = 500 + (uint32_t)angle * 1000 / 180;
TIM_SetCompare1(TIM4, pwm_value);
}
// 舵机回中(90度)
void Servo_Center(void)
{
Servo_SetAngle(90);
}
// 舵机左转
void Servo_Left(void)
{
Servo_SetAngle(45);
}
// 舵机右转
void Servo_Right(void)
{
Servo_SetAngle(135);
}
5.3.4 ESP8266 Wi-Fi模块驱动代码
// esp8266.h
#ifndef __ESP8266_H
#define __ESP8266_H
#include "stm32f10x.h"
// 函数声明
void ESP8266_Init(void);
void ESP8266_SendCmd(char *cmd);
uint8_t ESP8266_MQTT_Connect(char *device_id, char *device_secret);
void ESP8266_MQTT_Publish(char *data);
#endif
// esp8266.c
#include "esp8266.h"
#include "usart.h"
#include "delay.h"
// ESP8266初始化
void ESP8266_Init(void)
{
// USART2已初始化,用于与ESP8266通信
// 发送AT指令测试通信
ESP8266_SendCmd("ATrn");
Delay_ms(500);
// 设置Wi-Fi模式为STA模式
ESP8266_SendCmd("AT+CWMODE=1rn");
Delay_ms(500);
// 连接Wi-Fi热点
ESP8266_SendCmd("AT+CWJAP="SSID","PASSWORD"rn");
Delay_ms(5000);
// 查询IP地址
ESP8266_SendCmd("AT+CIFSRrn");
Delay_ms(500);
}
// 发送AT指令
void ESP8266_SendCmd(char *cmd)
{
while(*cmd)
{
USART_SendData(USART2, *cmd++);
while(USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
}
}
// 连接MQTT服务器
uint8_t ESP8266_MQTT_Connect(char *device_id, char *device_secret)
{
char cmd[256];
// 配置MQTT连接
sprintf(cmd, "AT+MQTTUSERCFG=0,1,"%s","%s","",0,0,""rn",
device_id, device_secret);
ESP8266_SendCmd(cmd);
Delay_ms(500);
// 连接MQTT服务器
ESP8266_SendCmd("AT+MQTTCONN=0,"mqtt.huaweicloud.com",1883,1rn");
Delay_ms(2000);
// 检查连接状态
ESP8266_SendCmd("AT+MQTTCONN?rn");
Delay_ms(500);
return 1;
}
// 发布MQTT消息
void ESP8266_MQTT_Publish(char *data)
{
char cmd[512];
sprintf(cmd, "AT+MQTTPUB=0,"$oc/devices/%s/sys/properties/report","%s",0,0rn",
"your_device_id", data);
ESP8266_SendCmd(cmd);
Delay_ms(500);
}
5.3.5 蜂鸣器驱动代码
// buzzer.h
#ifndef __BUZZER_H
#define __BUZZER_H
#include "stm32f10x.h"
#define BUZZER_PORT GPIOB
#define BUZZER_PIN GPIO_Pin_8
// 函数声明
void Buzzer_Init(void);
void Buzzer_On(void);
void Buzzer_Off(void);
void Buzzer_Alarm(uint16_t duration);
#endif
// buzzer.c
#include "buzzer.h"
#include "delay.h"
// 蜂鸣器初始化
void Buzzer_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置为推挽输出
GPIO_InitStructure.GPIO_Pin = BUZZER_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(BUZZER_PORT, &GPIO_InitStructure);
// 初始化为低电平
GPIO_ResetBits(BUZZER_PORT, BUZZER_PIN);
}
// 蜂鸣器开
void Buzzer_On(void)
{
GPIO_SetBits(BUZZER_PORT, BUZZER_PIN);
}
// 蜂鸣器关
void Buzzer_Off(void)
{
GPIO_ResetBits(BUZZER_PORT, BUZZER_PIN);
}
// 蜂鸣器报警(指定持续时间)
void Buzzer_Alarm(uint16_t duration)
{
Buzzer_On();
Delay_ms(duration);
Buzzer_Off();
}
5.4 程序下载
也有视频教程:
讲解如何编译代码,下载STM32程序: https://www.bilibili.com/video/BV1Cw4m1e7Yc
打STM32的keil工程,编译代码、然后,使用USB线将开发板的左边的USB口(串口1)与电脑的USB连接,打开程序下载软件下载程序。
具体下载过程看下面图:
打开程序下载软件:[软件就在资料包里的软件工具目录下]
5.5 程序正常运行效果
设备运行过程中会通过串口打印调试信息,我们可以通过串口打印了解程序是否正常。
程序下载之后,可以打开串口调试助手查看程序运行的状态信息。[软件就在资料包里的软件工具目录下]
5.6 取模软件的使用
显示屏上会显示中文,字母,数字等数据,可以使用下面的取模软件进行取模设置。
[软件就在资料包里的软件工具目录下]
打开软件之后:
六、总结
本文设计并实现了一套基于STM32的便携式污水检测船系统,该系统融合了水质检测技术、物联网通信技术、嵌入式控制技术以及无人船遥控技术,构建了一个集数据采集、本地显示、云端传输、远程监控、智能预警和移动控制于一体的综合性水质监测解决方案。
在硬件设计方面,系统以STM32F103C8T6为主控芯片,集成了水温检测传感器、PH值检测传感器、浊度检测传感器和溶解氧检测传感器,实现了对水体多项关键指标的实时采集。通过0.96寸OLED显示屏实现数据的本地可视化,采用ESP8266 Wi-Fi模块和MQTT协议将数据上传至华为云物联网服务器,完成了设备与云端的无缝对接。小船控制系统由5V直流电机和SG90舵机组成,通过蓝牙模块实现无线遥控,使水质检测装置能够在目标水域自由移动,进行多点位数据采集。
在软件设计方面,硬件端采用C语言和寄存器方式进行编程,精确控制STM32的各外设模块,实现了传感器数据读取、显示刷新、通信协议解析、报警控制等功能。Android手机APP和Windows上位机软件均采用Qt框架和C++语言开发,实现了代码的高度复用,两款应用通过MQTT协议与华为云平台交互,能够实时显示水质数据、设置参数阈值,并在数据异常时进行红色预警提示。系统支持云端动态设置水温、PH值、浊度、溶解氧的上下限阈值,当检测数据超出设定范围时,本地蜂鸣器发出报警,同时APP和上位机界面上的对应数据显示红色,实现了多终端同步预警。
经过实际测试,本系统运行稳定可靠,各项功能均达到预期设计目标。水温检测精度可达±0.5℃,PH值检测范围0~14,浊度检测范围0~1000NTU,溶解氧检测范围0~20mg/L,能够满足常规水质监测的需求。Wi-Fi通信在信号良好的环境下连接稳定,数据上报及时,云端数据接收正常。蓝牙遥控距离可达10米以上,小船前进、后退、转向动作响应灵敏,能够按照操作指令在目标水域移动并进行数据采集。Android手机APP和Windows上位机界面友好、操作便捷,数据刷新及时,报警功能正常。
本项目的创新之处主要体现在以下几个方面:一是将水质检测系统与无人船技术相结合,实现了移动式水质监测,解决了固定监测点覆盖范围有限的问题;二是采用MQTT协议对接华为云物联网平台,实现了水质数据的实时上传和远程监控;三是利用Qt框架开发跨平台应用,同时支持Android手机和Windows电脑,满足了不同使用场景的需求;四是支持云端动态设置参数阈值,实现了灵活的远程预警配置。
本系统的设计充分考虑了成本控制,选用性价比较高的硬件模块,整体成本远低于市面上同类专业设备,具有良好的经济性和推广价值。同时,系统采用模块化设计,各功能模块相对独立,便于后续的功能扩展和维护升级。
综上所述,本设计成功实现了基于STM32的便携式污水检测船的研制,该系统能够有效完成水质数据的移动采集、本地显示、云端上传、远程监控和智能预警,为中小型水域的水质监测提供了一种低成本、高效率、易操作的一体化解决方案。本项目的实施不仅验证了嵌入式技术与物联网技术在环境监测领域融合应用的可行性,也为后续相关研究和产品开发提供了有益的参考和借鉴。
261