基于华为云设计的智能水产养殖监控系统

一、前言

1.1 项目开发背景

随着水产养殖业的规模化发展,传统依赖人工经验的养殖模式已难以满足现代精细化管理的需求。水质参数波动、设备控制滞后及病害预防困难等问题,直接影响养殖效益与生物安全性。水温、pH值、溶解氧等关键指标的实时监控是保障水产品健康生长的核心,但人工监测不仅效率低下,且无法实现全天候异常预警。

华为云物联网平台的引入为这一问题提供了技术突破口。通过云边端协同架构,系统可实现对多源传感器数据的实时采集与远程传输,结合云端大数据分析能力,为养殖决策提供科学依据。水下影像采集功能进一步扩展了监控维度,通过视觉数据辅助判断鱼类活动状态与摄食情况,形成多维度的养殖环境感知体系。

本系统通过STM32主控制器整合传感器网络,依托ESP32-CAM实现影像数字化,最终经华为云物联网平台进行数据融合与指令下发。QT上位机提供可视化交互界面,使养殖人员能够实时掌握水质趋势与设备运行状态,既降低了人工劳动强度,又显著提升了养殖过程的精准性与抗风险能力,符合现代智慧农业的发展方向。

实物部署模型:

硬件开发模型:

1.2 设计实现的功能

(1)使用DS18B20防水温度传感器实时监测水体温度 (2)使用PH-4502C pH传感器实时监测水体pH值 (3)使用DO溶解氧传感器模块实时监测溶解氧含量 (4)使用STM32F103C8T6主控制器自动控制增氧机、投饵机等设备运行 (5)使用ESP32-CAM模块采集水下鱼类活动影像 (6)通过QT上位机显示水质参数变化曲线和设备运行状态 (7)采用华为云物联网服务器进行数据通信和存储

1.3 项目硬件模块组成

(1) STM32F103C8T6最小系统核心板作为主控制器

(2) DS18B20防水温度传感器监测水温

(3) PH-4502C pH传感器监测酸碱度

(4) DO溶解氧传感器模块监测溶氧量

(5) ESP32-CAM模块实现水下影像采集

(6) 洞洞板焊接信号处理电路,杜邦线连接传感器

1.4 设计意义

该智能水产养殖监控系统通过集成多种传感器和华为云物联网服务器,实现了对水产养殖环境的全面数字化管理,显著提升了养殖过程的精细化和自动化水平。系统能够实时监测水温、pH值和溶解氧含量等关键水质参数,确保养殖环境始终处于 optimal 状态,从而有效预防因水质突变导致的鱼类应激反应或死亡,提高养殖成活率和产量。

自动控制增氧机和投饵机等设备的功能,减少了传统养殖中依赖人工操作的误差和延迟,实现了资源的精准投放和能源的高效利用。这不仅降低了人力成本,还通过智能化调节避免了过度投饵或增氧不足的问题,促进了养殖业的可持续发展。

水下摄像头采集鱼类活动影像,为养殖者提供了直观的鱼类行为观察手段,有助于早期发现异常情况如疾病或拥挤,并及时采取干预措施。这种视觉监控补充了传感器数据,增强了系统的综合监测能力,提升了养殖管理的全面性和可靠性。

QT上位机显示水质参数变化曲线和设备运行状态,使得养殖者能够通过图形化界面轻松跟踪历史数据和实时状态,辅助决策制定。这种数据可视化工具简化了管理流程,提高了操作便捷性,特别适合非专业用户使用,推动了智能养殖技术的普及。

采用华为云物联网服务器作为数据中枢,实现了数据的远程存储、分析和访问,支持多终端监控和警报功能。这增强了系统的可扩展性和可靠性,便于未来集成大数据分析或人工智能算法,为养殖业提供长期的数据驱动优化建议。

硬件组成基于STM32F103C8T6核心板和常见传感器模块,通过洞洞板焊接和杜邦线连接,体现了设计的实用性和成本效益。这种选择降低了系统部署和维护的门槛,使其更适合中小型养殖场应用,推动了智能技术在水产领域的实际落地。

1.5 设计思路

基于华为云物联网服务器,设计一个智能水产养殖监控系统,以STM32F103C8T6最小系统核心板作为主控制器,负责整体系统的协调与数据处理。该系统旨在实现水产养殖环境的实时监控和自动化控制,通过传感器采集关键水质参数,并利用云平台进行数据管理和远程访问。

传感器数据采集部分使用DS18B20防水温度传感器监测水温,PH-4502C pH传感器监测酸碱度,以及DO溶解氧传感器模块监测溶氧量。这些传感器通过洞洞板焊接的信号处理电路进行接口适配和信号调理,使用杜邦线连接至STM32的GPIO或ADC引脚,STM32定期轮询读取传感器数据,并进行初步校准和滤波处理,以确保数据的准确性和稳定性。

设备控制功能由STM32实现,根据传感器读取的水质参数与预设阈值进行比较,自动控制增氧机和投饵机等设备的运行。例如,当溶解氧含量低于设定值时,STM32输出控制信号启动增氧机;类似地,基于时间或条件触发投饵机操作,从而优化养殖环境并减少人工干预。

影像采集通过ESP32-CAM模块完成,该模块配备水下摄像头,用于采集鱼类活动影像。ESP32-CAM独立处理图像数据,并通过WiFi将视频流或抓拍图像传输至华为云物联网服务器,便于远程查看和分析鱼类行为,增强监控的全面性。

数据上传与云集成部分,STM32将处理后的传感器数据通过串口或网络模块(如集成ESP32的WiFi功能)发送至华为云物联网服务器,实现数据的远程存储和实时更新。华为云服务器提供数据管理、设备管理和API接口,支持后续的数据分析和告警功能。

上位机显示采用QT开发的应用程序,该程序从华为云服务器获取实时数据和设备状态,以图形化界面显示水质参数(如温度、pH值、溶解氧)的变化曲线,同时展示增氧机和投饵机的运行状态,帮助用户直观监控养殖情况并进行历史数据回顾。

1.6 框架图

go 复制代码
Syntax error in graphmermaid version 8.8.3

1.7 系统总体设计

系统总体设计基于华为云物联网平台,旨在实现智能水产养殖的实时监控与自动控制。系统以STM32F103C8T6最小系统核心板作为主控制器,负责协调数据采集、设备控制和通信任务。传感器部分包括DS18B20防水温度传感器用于监测水温,PH-4502C pH传感器监测酸碱度,以及DO溶解氧传感器模块监测溶氧量,这些传感器通过洞洞板焊接的信号处理电路进行信号调理,并使用杜邦线连接到主控制器,确保数据准确性和稳定性。

影像采集由ESP32-CAM模块独立处理,该模块部署于水下环境,直接捕获鱼类活动影像,并通过Wi-Fi将视频流传输到华为云物联网服务器,实现远程监控。主控制器STM32定期读取传感器数据,并进行初步处理,如数据滤波和校准,以确保监测参数的可靠性。

自动控制功能通过主控制器实现,根据预设阈值(如溶解氧低于临界值)自动触发增氧机或投饵机等设备的运行。控制逻辑基于实时传感器数据,确保养殖环境的优化,同时通过数字输出端口连接继电器模块来控制外部设备,提升系统的自动化水平。

数据通信部分,主控制器将处理后的传感器数据通过串口或Wi-Fi模块(如集成ESP8266)上传至华为云物联网服务器,利用华为云提供的MQTT协议进行数据传输,实现云端存储和远程访问。华为云服务器作为数据中枢,支持数据分析和告警功能,为系统提供可靠的云基础。

QT上位机软件运行于用户端PC,通过API接口从华为云服务器获取实时数据和设备状态,并以图形化界面显示水质参数(如温度、pH、溶解氧)的变化曲线,同时展示设备运行状态,方便用户进行监控和决策。整个系统设计注重实用性和扩展性,确保养殖过程的高效管理。

1.8 系统功能总结

功能描述 实现方式
实时监测水体温度 使用DS18B20防水温度传感器,通过STM32F103C8T6处理数据
实时监测pH值 使用PH-4502C pH传感器,通过STM32F103C8T6处理数据
实时监测溶解氧含量 使用DO溶解氧传感器模块,通过STM32F103C8T6处理数据
自动控制增氧机、投饵机等设备运行 STM32F103C8T6输出控制信号,通过洞洞板焊接信号处理电路驱动设备
水下摄像头采集鱼类活动影像 使用ESP32-CAM模块进行影像采集
QT上位机显示水质参数变化曲线和设备运行状态 QT应用程序从华为云物联网服务器获取数据并实现可视化显示
数据上传和通信 使用华为云物联网服务器进行数据存储和远程通信

1.9 设计的各个功能模块描述

主控制器模块采用STM32F103C8T6最小系统核心板,作为整个系统的核心处理单元,负责协调数据采集、设备控制和通信任务。它通过读取传感器数据、执行逻辑判断,并发送指令来控制外部设备,同时处理与华为云服务器的数据交互。

温度监测模块使用DS18B20防水温度传感器,该传感器能够实时监测水体温度,并通过单总线数字信号输出。传感器数据被主控制器采集和处理,确保水温测量的准确性和稳定性,适用于水产养殖环境。

pH监测模块基于PH-4502C pH传感器,用于检测水体的酸碱度。该传感器输出模拟信号,通过洞洞板焊接的信号处理电路进行调理和转换,最终由主控制器的ADC模块读取数据,以实现pH值的实时监控。

溶解氧监测模块采用专门的DO溶解氧传感器模块,监测水体中的溶氧含量。传感器输出信号经过信号处理电路优化后,由主控制器采集和分析,帮助维持养殖水体的氧气水平,支持自动控制决策。

影像采集模块利用ESP32-CAM模块实现水下鱼类活动影像的捕获。该模块集成摄像头和WiFi功能,能够拍摄视频或图像,并通过网络传输数据到云端或本地,供后续分析和显示。

设备控制模块由主控制器驱动,根据传感器数据自动操作增氧机、投饵机等外部设备。主控制器输出控制信号,通过继电器或驱动电路切换设备运行状态,确保养殖环境的优化管理。

云通信模块将系统连接到华为云物联网服务器,实现数据的远程传输和存储。主控制器通过网络模块(如ESP32的WiFi功能)上传传感器读数、设备状态和影像数据,支持云端监控和数据分析。

上位机显示模块基于QT开发,运行在PC端,用于可视化显示水质参数(如温度、pH、溶解氧)的变化曲线和设备运行状态。该软件从华为云服务器或直接通过网络接收数据,提供用户友好的监控界面。

信号处理电路模块采用洞洞板焊接设计,用于连接和调理各类传感器信号。通过杜邦线将传感器与主控制器互联,该电路确保信号稳定性和抗干扰能力,提升数据采集的可靠性。

二、部署华为云物联网平台

华为云官网: www.huaweicloud.com/

打开官网,搜索物联网,就能快速找到 设备接入IoTDA

2.1 物联网平台介绍

华为云物联网平台(IoT 设备接入云服务)提供海量设备的接入和管理能力,将物理设备联接到云,支撑设备数据采集上云和云端下发命令给设备进行远程控制,配合华为云其他产品,帮助我们快速构筑物联网解决方案。

使用物联网平台构建一个完整的物联网解决方案主要包括3部分:物联网平台、业务应用和设备。

物联网平台作为连接业务应用和设备的中间层,屏蔽了各种复杂的设备接口,实现设备的快速接入;同时提供强大的开放能力,支撑行业用户构建各种物联网解决方案。

设备可以通过固网、2G/3G/4G/5G、NB-IoT、Wifi等多种网络接入物联网平台,并使用LWM2M/CoAP、MQTT、HTTPS协议将业务数据上报到平台,平台也可以将控制命令下发给设备。

业务应用通过调用物联网平台提供的API,实现设备数据采集、命令下发、设备管理等业务场景。

2.2 开通物联网服务

地址: www.huaweicloud.com/product/iot...

开通免费单元。

点击立即创建

正在创建标准版实例,需要等待片刻。

创建完成之后,点击详情。 可以看到标准版实例的设备接入端口和地址。

下面框起来的就是端口号域名

点击实例名称,可以查看当前免费单元的配置情况。

开通之后,点击接入信息,也能查看接入信息。 我们当前设备准备采用MQTT协议接入华为云平台,这里可以看到MQTT协议的地址和端口号等信息。

总结:

scss 复制代码
端口号:   MQTT (1883)| MQTTS (8883)    
接入地址: dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com

根据域名地址得到IP地址信息:

打开Windows电脑的命令行控制台终端,使用ping 命令。ping一下即可。

ini 复制代码
Microsoft Windows [版本 10.0.19045.5011]
(c) Microsoft Corporation。保留所有权利。
​
C:\Users\Lenovo>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:\Users\Lenovo>
​

MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口合适

2.3 创建产品

链接:console.huaweicloud.com/iotdm/?regi...

(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协议接入帮助文档在这里: support.huaweicloud.com/devg-iothub...

业务流程:

(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)主题订阅格式

帮助文档地址:support.huaweicloud.com/devg-iothub...

对于设备而言,一般会订阅平台下发消息给设备 这个主题。

设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。

如果设备想要知道平台下发的消息,需要订阅上面图片里标注的主题。

bash 复制代码
以当前设备为例,最终订阅主题的格式如下:
$oc/devices/{device_id}/sys/messages/down
    
最终的格式:
$oc/devices/663cb18871d845632a0912e7_dev1/sys/messages/down

(4)主题发布格式

对于设备来说,主题发布表示向云平台上传数据,将最新的传感器数据,设备状态上传到云平台。

这个操作称为:属性上报。

帮助文档地址:support.huaweicloud.com/usermanual-...

根据帮助文档的介绍, 当前设备发布主题,上报属性的格式总结如下:

bash 复制代码
发布的主题格式:
$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服务器,首先记得先知道服务器的地址是多少,端口是多少。

帮助文档地址:console.huaweicloud.com/iotdm/?regi...

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鉴权三元组: 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客户端软件模拟真实的设备来登录平台。测试与服务器通信是否正常。

(1)填入登录信息

打开MQTT客户端软件,对号填入相关信息(就是上面的文本介绍)。然后,点击登录,订阅主题,发布主题。

(2)打开网页查看

完成上面的操作之后,打开华为云网页后台,可以看到设备已经在线了。

点击详情页面,可以看到上传的数据:

到此,云平台的部署已经完成,设备已经可以正常上传数据了。

(3)MQTT登录测试参数总结

arduino 复制代码
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账号信息。所以,接下来演示一下过程。

地址: console.huaweicloud.com/iam/?region...

【1】获取项目凭证 点击左上角用户名,选择下拉菜单里的我的凭证

项目凭证:

复制代码
28add376c01e4a61ac8b621c714bf459

【2】创建IAM用户

鼠标放在左上角头像上,在下拉菜单里选择统一身份认证

点击左上角创建用户

创建成功:

【3】创建完成

用户信息如下:

复制代码
主用户名  l19504562721
IAM用户  ds_abc
密码     DS12345678

2.9 获取影子数据

帮助文档:support.huaweicloud.com/api-iothub/...

设备影子介绍:

javascript 复制代码
设备影子是一个用于存储和检索设备当前状态信息的JSON文档。
每个设备有且只有一个设备影子,由设备ID唯一标识
设备影子仅保存最近一次设备的上报数据和预期数据
无论该设备是否在线,都可以通过该影子获取和设置设备的属性

简单来说:设备影子就是保存,设备最新上传的一次数据。

我们设计的软件里,如果想要获取设备的最新状态信息,就采用设备影子接口。

如果对接口不熟悉,可以先进行在线调试:apiexplorer.developer.huaweicloud.com/apiexplorer...

在线调试接口,可以请求影子接口,了解请求,与返回的数据格式。

调试完成看右下角的响应体,就是返回的影子数据。

设备影子接口返回的数据如下:

json 复制代码
{
 "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写代码访问此链接,获取影子数据,完成上位机开发。

链接如下:

bash 复制代码
https://ad635970a1.st1.iotda-app.cn-north-4.myhuaweicloud.com:443/v5/iot/28add376c01e4a61ac8b621c714bf459/devices/663cb18871d845632a0912e7_dev1/shadow

三、上位机开发

3.1 Qt开发环境安装

Qt的中文官网: www.qt.io/zh-cn/

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

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

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

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

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

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 设计代码

【1】获取token

调用华为云的API都需要填token参数,先看帮助文章,了解如何获取token。

帮助文档:support.huaweicloud.com/api-iam/iam...

根据帮助文档,写完成下面代码编写:

这段代码的功能是通过华为云IAM服务获取Token,以便后续调用华为云API时使用。以下是代码的详细功能解释:


1. 设置功能标识
ini 复制代码
function_select = 3;
  • function_select是一个标识变量,用于区分当前请求的功能类型。这里设置为3,表示当前请求是获取Token。

2. 构造请求URL
ini 复制代码
QString requestUrl;
QNetworkRequest request;

// 设置请求地址
QUrl url;

// 获取token请求地址
requestUrl = QString("https://iam.%1.myhuaweicloud.com/v3/auth/tokens")
             .arg(SERVER_ID);
  • 构造获取Token的请求URL,URL格式为:https://iam.{SERVER_ID}.myhuaweicloud.com/v3/auth/tokens
  • SERVER_ID是华为云服务器的区域ID(如cn-north-1),通过QStringarg方法动态替换到URL中。

3. 设置请求头
less 复制代码
// 设置数据提交格式
request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json;charset=UTF-8"));
  • 设置HTTP请求头,指定请求体的数据格式为application/json;charset=UTF-8,表示发送的数据是JSON格式。

4. 设置请求URL
scss 复制代码
// 构造请求
url.setUrl(requestUrl);
request.setUrl(url);
  • 将构造好的URL设置到QUrl对象中,并将其绑定到QNetworkRequest对象。

5. 构造请求体
python 复制代码
QString text = QString("{"auth":{"identity":{"methods":["password"],"password":"
"{"user":{"domain": {"
""name":"%1"},"name": "%2","password": "%3"}}},"
""scope":{"project":{"name":"%4"}}}}")
        .arg(MAIN_USER)
        .arg(IAM_USER)
        .arg(IAM_PASSWORD)
        .arg(SERVER_ID);
  • 构造JSON格式的请求体,用于向华为云IAM服务请求Token。请求体包含以下字段:

    • auth:认证信息。

      • identity:身份信息。

        • methods:认证方法,这里使用密码认证(password)。

        • password:密码认证的具体信息。

          • user:用户信息。

            • domain:用户所属的域名。

              • name:域名名称(MAIN_USER)。
            • name:用户名(IAM_USER)。

            • password:用户密码(IAM_PASSWORD)。

      • scope:请求的范围。

        • project:项目信息。

          • name:项目名称(SERVER_ID)。
  • 使用QStringarg方法动态替换请求体中的变量(如MAIN_USERIAM_USER等)。


6. 发送HTTP POST请求
scss 复制代码
// 发送请求
manager->post(request, text.toUtf8());
  • 使用QNetworkAccessManagerpost方法发送HTTP POST请求。
  • request是构造好的请求对象,text.toUtf8()是将请求体转换为UTF-8编码的字节数组。

7. 总结

这段代码的核心功能是:

  1. 构造获取Token的HTTP请求:包括请求URL、请求头和请求体。
  2. 发送请求 :通过QNetworkAccessManager发送POST请求,向华为云IAM服务请求Token。
  3. Token的作用:获取到的Token将用于后续调用华为云API时的身份验证。

通过这段代码,QT上位机能够获取华为云的Token,为后续的设备数据查询、控制等操作提供身份验证支持。

【2】获取影子数据

前面章节介绍了影子数据获取接口。下面是对应编写的代码:

这段代码的功能是向华为云IoT平台查询设备的属性信息(设备状态)。以下是对代码的详细功能含义解释:

代码功能含义解释:

(1)function_select = 0;

  • 这行代码设置function_select为0,表示当前操作是查询设备属性。这个变量用于标识不同的操作,可以帮助后续根据不同的操作类型执行不同的处理逻辑。

(2)QString requestUrl; QNetworkRequest request;

  • requestUrl:用于存储请求的URL地址,后续将构造一个用于查询设备属性的URL。
  • request:用来封装HTTP请求的对象,包含请求的所有信息,包括请求头、URL等。

(3)QUrl url;

  • url:用于存储并处理请求的URL对象,确保请求使用正确的地址。

(4)构造请求URL:

scss 复制代码
requestUrl = QString("https://%1:443/v5/iot/%2/devices/%3/shadow")
             .arg(IP_ADDR)
             .arg(PROJECT_ID)
             .arg(device_id);

这行代码构建了一个URL,用于查询设备的状态(属性)。URL包括了:

IP_ADDR:华为云IoT平台的IP地址或域名。

PROJECT_ID:项目的ID,用于区分不同的项目。

device_id:设备的唯一标识符,用于查询指定设备的属性。

:443指定使用HTTPS协议(端口443)进行安全通信。

最终构造出的URL形如:https://<IP_ADDR>:443/v5/iot/<PROJECT_ID>/devices/<device_id>/shadow,这是查询设备状态的API接口。

(1)request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json"));

  • 设置请求头的内容类型为application/json,表明请求体中的数据格式是JSON。

(2)request.setRawHeader("X-Auth-Token", Token);

  • 设置请求头中的X-Auth-Token字段,传递身份验证令牌(Token)。这个令牌用于验证请求的合法性,确保只有授权的用户可以查询设备的状态。

(3)url.setUrl(requestUrl);

  • 将前面构建好的请求URL赋值给url对象,确保后续的请求使用正确的URL。

(4)request.setUrl(url);

  • url对象设置到request对象中,准备发送请求。

(5)manager->get(request);

  • 使用QNetworkAccessManagerget方法发送GET请求,查询设备的属性。request中包含了URL、请求头以及Token等信息,服务器接收到请求后将返回设备的属性信息(如设备状态、属性值等)。

代码整体功能:

该代码实现了通过华为云IoT平台的API查询设备的属性信息。具体步骤包括:

  1. 构造查询设备属性的API请求URL。
  2. 设置请求头,指定数据格式为JSON,并传递Token进行身份验证。
  3. 使用QNetworkAccessManager发送GET请求,向服务器请求设备的状态数据。
  4. 服务器将返回设备的属性数据,供后续处理。

总结:

这段代码的功能是向华为云IoT平台查询指定设备的属性信息,并通过GET请求将设备的状态返回给客户端。通过Token进行身份验证,确保请求的合法性。

【3】解析数据更新界面

根据接口返回的数据更新界面。

【4】判断设备是否离线

这段代码用于判断设备是否离线。通过获取设备上传到服务器数据的时间与本地的系统时间差进行对比。

这段代码的核心功能是通过比较设备上传数据的时间和本地系统时间来判断设备是否处于离线状态,以下是其详细解释:


(1)功能分析

显示最新更新时间

rust 复制代码
ui->label_update_time->setText("最新时间:" + update_time);

将设备上传的最新时间 update_time 显示在界面上的 label_update_time 控件中,格式为 最新时间:yyyy-MM-dd HH:mm:ss

方便用户了解设备数据的最近更新时间。

获取本地当前时间

ini 复制代码
QDateTime currentDateTime = QDateTime::currentDateTime();

使用 QDateTime::currentDateTime() 获取系统当前时间,作为对比基准。

计算时间差

ini 复制代码
qint64 secondsDiff = currentDateTime.secsTo(dateTime);

secsTo: 计算 currentDateTime 和设备上传时间 dateTime 之间的时间差(单位:秒)。

dateTime 是通过解析 JSON 数据提取到的设备数据上传时间,并已转换为本地时间格式。

判断设备状态

less 复制代码
if (qAbs(secondsDiff) >= 5 * 60)

使用 qAbs 获取时间差的绝对值。

如果时间差超过 5 分钟(300秒) ,表示设备长时间未上传数据,判定为"离线"。


(2)离线处理

更新状态显示

rust 复制代码
ui->label_dev_state->setText("设备状态:离线");

在界面 label_dev_state 控件中显示设备当前状态为"离线"。


(3)在线处理

状态更新ui->label_dev_state->setText("设备状态:在线");如果时间差小于 5 分钟,显示"设备状态:在线"。

【5】获取设备最新数据上传时间

这是解析华为云API接口返回的数据,解析出来里面设备数据的时间,进行显示。

这段代码的主要作用是解析华为云 API 返回的 JSON 数据中的设备数据时间字段,转换为本地时间格式,并最终以用户友好的标准格式输出到界面。


(1)详细代码解析

(1)提取时间字段

ini 复制代码
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 对象

ini 复制代码
QDateTime dateTime = QDateTime::fromString(event_time, "yyyyMMddTHHmmssZ");

QDateTime::fromString:

使用指定格式解析 event_time 字符串为 QDateTime 对象。

格式说明:

  • yyyyMMdd: 年、月、日(如 20231121)。
  • T: 时间部分的分隔符(固定为 T)。
  • HHmmss: 时、分、秒(如 120530)。
  • Z: 表示时间是 UTC 时间。
  • 如果时间字符串格式不匹配,会返回一个无效的 QDateTime 对象。

3. 转换时区到本地时间

ini 复制代码
dateTime.setTimeSpec(Qt::UTC);
dateTime = dateTime.toLocalTime();

setTimeSpec(Qt::UTC):

  • 明确告知 dateTime 对象,当前时间是 UTC 时间。
  • 确保时间转换准确,避免因为默认时区不明确导致的误差。

toLocalTime():

  • 将时间从 UTC 转换为本地时区时间,例如中国标准时间(CST, UTC+8)。

4. 格式化输出为标准时间字符串

ini 复制代码
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"

输出到界面时,显示为:

makefile 复制代码
最新时间: 2023-11-21 20:05:30

3.5 编译Windows上位机

点击软件左下角的绿色三角形按钮进行编译运行。

3.6 上位机代码设计

以下是基于QT和C++开发的上位机代码设计,用于智能水产养殖监控系统。代码包括主窗口类、MQTT客户端集成、图表显示和数据更新逻辑。项目使用QT Charts和QT Mqtt模块。

项目文件结构:

  • SmartAquaculture.pro:QT项目配置文件
  • main.cpp:应用程序入口点
  • mainwindow.h:主窗口头文件
  • mainwindow.cpp:主窗口实现文件
  • mainwindow.ui:UI设计文件(通过QT Designer创建,这里用代码描述大致UI)

代码实现:

1. SmartAquaculture.pro
cpp 复制代码
QT       += core gui charts mqtt

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++17

SOURCES += \
    main.cpp \
    mainwindow.cpp

HEADERS += \
    mainwindow.h

FORMS += \
    mainwindow.ui

# Additional settings
DEFINES += QT_DEPRECATED_WARNINGS

# Default rules for deployment
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
2. main.cpp
cpp 复制代码
#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}
3. mainwindow.h
cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QtMqtt/QMqttClient>
#include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#include <QValueAxis>
#include <QLabel>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private slots:
    void onConnected();
    void onMessageReceived(const QByteArray &message, const QMqttTopicName &topic);

private:
    Ui::MainWindow *ui;
    QMqttClient *m_client;
    QChart *m_chart;
    QLineSeries *m_tempSeries;
    QLineSeries *m_phSeries;
    QLineSeries *m_doSeries;
    QValueAxis *m_axisX;
    QValueAxis *m_axisYTemp;
    QValueAxis *m_axisYPh;
    QValueAxis *m_axisYDo;
    int m_timeCount;
};
#endif // MAINWINDOW_H
4. mainwindow.cpp
cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDateTime>
#include <QMessageBox>
#include <QVBoxLayout>
#include <QHBoxLayout>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , m_timeCount(0)
{
    ui->setupUi(this);

    // Initialize chart
    m_chart = new QChart();
    m_tempSeries = new QLineSeries();
    m_tempSeries->setName("Temperature");
    m_phSeries = new QLineSeries();
    m_phSeries->setName("pH");
    m_doSeries = new QLineSeries();
    m_doSeries->setName("Dissolved Oxygen");

    m_chart->addSeries(m_tempSeries);
    m_chart->addSeries(m_phSeries);
    m_chart->addSeries(m_doSeries);

    m_axisX = new QValueAxis();
    m_axisX->setRange(0, 100);
    m_axisX->setLabelFormat("%d");
    m_axisX->setTitleText("Time (s)");

    m_axisYTemp = new QValueAxis();
    m_axisYTemp->setRange(0, 50);
    m_axisYTemp->setTitleText("Temperature (°C)");

    m_axisYPh = new QValueAxis();
    m_axisYPh->setRange(0, 14);
    m_axisYPh->setTitleText("pH");

    m_axisYDo = new QValueAxis();
    m_axisYDo->setRange(0, 20);
    m_axisYDo->setTitleText("DO (mg/L)");

    m_chart->addAxis(m_axisX, Qt::AlignBottom);
    m_chart->addAxis(m_axisYTemp, Qt::AlignLeft);
    m_chart->addAxis(m_axisYPh, Qt::AlignRight);
    m_chart->addAxis(m_axisYDo, Qt::AlignRight);

    m_tempSeries->attachAxis(m_axisX);
    m_tempSeries->attachAxis(m_axisYTemp);
    m_phSeries->attachAxis(m_axisX);
    m_phSeries->attachAxis(m_axisYPh);
    m_doSeries->attachAxis(m_axisX);
    m_doSeries->attachAxis(m_axisYDo);

    ui->chartView->setChart(m_chart);
    ui->chartView->setRenderHint(QPainter::Antialiasing);

    // Initialize MQTT client
    m_client = new QMqttClient(this);
    m_client->setHostname("your_huawei_cloud_iot_host"); // Replace with actual Huawei云IoT host
    m_client->setPort(1883); // Use 8883 for SSL
    // Set credentials if required (e.g., device ID and secret)
    // m_client->setUsername("username");
    // m_client->setPassword("password");

    connect(m_client, &QMqttClient::connected, this, &MainWindow::onConnected);
    connect(m_client, &QMqttClient::messageReceived, this, &MainWindow::onMessageReceived);

    m_client->connectToHost();
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::onConnected()
{
    // Subscribe to topics for sensors and devices
    m_client->subscribe("sensors/temperature");
    m_client->subscribe("sensors/ph");
    m_client->subscribe("sensors/do");
    m_client->subscribe("devices/status");
}

void MainWindow::onMessageReceived(const QByteArray &message, const QMqttTopicName &topic)
{
    QString topicName = topic.name();
    QString data = QString::fromUtf8(message);

    if (topicName == "sensors/temperature") {
        double temp = data.toDouble();
        ui->labelTemp->setText(QString("Temperature: %1 °C").arg(temp));
        m_tempSeries->append(m_timeCount, temp);
    } else if (topicName == "sensors/ph") {
        double ph = data.toDouble();
        ui->labelPh->setText(QString("pH: %1").arg(ph));
        m_phSeries->append(m_timeCount, ph);
    } else if (topicName == "sensors/do") {
        double doVal = data.toDouble();
        ui->labelDO->setText(QString("DO: %1 mg/L").arg(doVal));
        m_doSeries->append(m_timeCount, doVal);
    } else if (topicName == "devices/status") {
        // Parse device status message; assumed format: "aerator:on,feeder:off"
        if (data.contains("aerator:on")) {
            ui->labelAerator->setText("Aerator: On");
        } else {
            ui->labelAerator->setText("Aerator: Off");
        }
        if (data.contains("feeder:on")) {
            ui->labelFeeder->setText("Feeder: On");
        } else {
            ui->labelFeeder->setText("Feeder: Off");
        }
    }

    m_timeCount++;
    if (m_timeCount > 100) {
        // Scroll the chart view
        m_axisX->setRange(m_timeCount - 100, m_timeCount);
    }
    m_chart->update(); // Refresh chart
}
5. mainwindow.ui(大致描述):

通过QT Designer创建UI,包含以下元素:

  • chartView:QChartView对象,用于显示图表。
  • labelTemp:QLabel,显示当前温度值。
  • labelPh:QLabel,显示当前pH值。
  • labelDO:QLabel,显示当前溶解氧值。
  • labelAerator:QLabel,显示增氧机状态。
  • labelFeeder:QLabel,显示投饵机状态。

UI布局使用垂直和水平布局管理器组织这些元素。

使用说明:

  1. 确保QT安装时包含了Charts和Mqtt模块。
  2. 替换MQTT连接参数(如主机名、端口、用户名和密码)为实际的华为云IoT设备凭证。
  3. 编译并运行项目。
  4. 上位机将连接到华为云IoT服务器,订阅主题,并实时更新数据和图表。

此代码提供了基本功能,可根据实际需求扩展错误处理、数据持久化等功能。

3.7 STM32模块代码设计

cpp 复制代码
#include <stdint.h>

// Register definitions for STM32F103
#define PERIPH_BASE 0x40000000UL
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)

#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
#define ADC1_BASE (APB2PERIPH_BASE + 0x2400)
#define USART1_BASE (APB2PERIPH_BASE + 0x3800)

#define RCC_APB2ENR (*(volatile uint32_t*)(RCC_BASE + 0x18))
#define GPIOA_CRL (*(volatile uint32_t*)(GPIOA_BASE + 0x00))
#define GPIOA_CRH (*(volatile uint32_t*)(GPIOA_BASE + 0x04))
#define GPIOA_IDR (*(volatile uint32_t*)(GPIOA_BASE + 0x08))
#define GPIOA_ODR (*(volatile uint32_t*)(GPIOA_BASE + 0x0C))
#define GPIOB_CRL (*(volatile uint32_t*)(GPIOB_BASE + 0x00))
#define GPIOB_ODR (*(volatile uint32_t*)(GPIOB_BASE + 0x0C))
#define ADC1_SR (*(volatile uint32_t*)(ADC1_BASE + 0x00))
#define ADC1_CR1 (*(volatile uint32_t*)(ADC1_BASE + 0x04))
#define ADC1_CR2 (*(volatile uint32_t*)(ADC1_BASE + 0x08))
#define ADC1_SMPR2 (*(volatile uint32_t*)(ADC1_BASE + 0x10))
#define ADC1_SQR3 (*(volatile uint32_t*)(ADC1_BASE + 0x34))
#define ADC1_DR (*(volatile uint32_t*)(ADC1_BASE + 0x4C))
#define USART1_SR (*(volatile uint32_t*)(USART1_BASE + 0x00))
#define USART1_DR (*(volatile uint32_t*)(USART1_BASE + 0x04))
#define USART1_BRR (*(volatile uint32_t*)(USART1_BASE + 0x08))
#define USART1_CR1 (*(volatile uint32_t*)(USART1_BASE + 0x0C))

// Bit definitions for RCC
#define RCC_APB2ENR_IOPAEN (1 << 2)
#define RCC_APB2ENR_IOPBEN (1 << 3)
#define RCC_APB2ENR_ADC1EN (1 << 9)
#define RCC_APB2ENR_USART1EN (1 << 14)

// ADC channels
#define ADC_CHANNEL_PH 1  // PA1 for pH sensor
#define ADC_CHANNEL_DO 2  // PA2 for DO sensor

// Device control pins
#define OXYGENATOR_PIN 0  // PB0
#define FEEDER_PIN 1      // PB1

// DS18B20 on PA0
#define DS18B20_PIN 0

// UART settings
#define BAUD_RATE 9600
#define CLOCK_FREQ 72000000UL  // Assume 72MHz system clock

// Simple delay functions
void delay_us(uint32_t us) {
    for (volatile uint32_t i = 0; i < us * 8; i++); // Approximate delay
}

void delay_ms(uint32_t ms) {
    for (volatile uint32_t i = 0; i < ms * 1000; i++); // Approximate delay
}

// 1-Wire functions for DS18B20
void onewire_init(void) {
    // Set PA0 as open-drain output
    GPIOA_CRL &= ~(0xF << (4 * DS18B20_PIN));
    GPIOA_CRL |= (0x7 << (4 * DS18B20_PIN)); // Output open-drain, 50MHz
}

void onewire_low(void) {
    GPIOA_ODR &= ~(1 << DS18B20_PIN);
}

void onewire_high(void) {
    GPIOA_ODR |= (1 << DS18B20_PIN);
}

uint8_t onewire_read_bit(void) {
    uint8_t bit = 0;
    onewire_low();
    delay_us(2);
    onewire_high();
    delay_us(10);
    if (GPIOA_IDR & (1 << DS18B20_PIN)) bit = 1;
    delay_us(50);
    return bit;
}

void onewire_write_bit(uint8_t bit) {
    if (bit) {
        onewire_low();
        delay_us(10);
        onewire_high();
        delay_us(50);
    } else {
        onewire_low();
        delay_us(50);
        onewire_high();
        delay_us(10);
    }
}

uint8_t onewire_reset(void) {
    uint8_t presence = 0;
    onewire_low();
    delay_us(480);
    onewire_high();
    delay_us(70);
    if (!(GPIOA_IDR & (1 << DS18B20_PIN))) presence = 1;
    delay_us(410);
    return presence;
}

void onewire_write_byte(uint8_t byte) {
    for (int i = 0; i < 8; i++) {
        onewire_write_bit(byte & 0x01);
        byte >>= 1;
    }
}

uint8_t onewire_read_byte(void) {
    uint8 byte = 0;
    for (int i = 0; i < 8; i++) {
        byte >>= 1;
        if (onewire_read_bit()) byte |= 0x80;
    }
    return byte;
}

float read_ds18b20(void) {
    if (!onewire_reset()) return -1000.0; // Error
    onewire_write_byte(0xCC); // Skip ROM
    onewire_write_byte(0x44); // Convert T
    delay_ms(750); // Wait for conversion
    if (!onewire_reset()) return -1000.0;
    onewire_write_byte(0xCC); // Skip ROM
    onewire_write_byte(0xBE); // Read Scratchpad
    uint8_t temp_l = onewire_read_byte();
    uint8_t temp_h = onewire_read_byte();
    int16_t temp = (temp_h << 8) | temp_l;
    return temp * 0.0625; // Convert to Celsius
}

// ADC functions
void adc_init(void) {
    // Enable ADC1 clock
    RCC_APB2ENR |= RCC_APB2ENR_ADC1EN;
    // Configure PA1 and PA2 as analog inputs
    GPIOA_CRL &= ~(0xF << (4 * 1) | 0xF << (4 * 2));
    GPIOA_CRL |= (0x0 << (4 * 1) | 0x0 << (4 * 2)); // Analog mode
    // ADC configuration
    ADC1_CR2 = 0; // Disable ADC during config
    ADC1_CR1 = 0; // Single conversion, 12-bit
    ADC1_SMPR2 = (0x7 << (3 * ADC_CHANNEL_PH)) | (0x7 << (3 * ADC_CHANNEL_DO)); // 239.5 cycles sampling time
    ADC1_CR2 |= (1 << 0); // Enable ADC
    delay_ms(1); // Wait for ADC to stabilize
}

uint16_t read_adc(uint8 channel) {
    ADC1_SQR3 = channel; // Set channel
    ADC1_CR2 |= (1 << 22); // Start conversion
    while (!(ADC1_SR & (1 << 1))); // Wait for conversion complete
    return ADC1_DR; // Read data
}

// UART functions
void uart_init(void) {
    // Enable USART1 clock
    RCC_APB2ENR |= RCC_APB2ENR_USART1EN;
    // Configure PA9 as alternate function push-pull (TX), PA10 as input floating (RX)
    GPIOA_CRH &= ~(0xF << (4 * (9 - 8)) | 0xF << (4 * (10 - 8)));
    GPIOA_CRH |= (0xB << (4 * (9 - 8)) | 0x4 << (4 * (10 - 8))); // AFPP for PA9, input floating for PA10
    // Set baud rate
    USART1_BRR = CLOCK_FREQ / BAUD_RATE;
    // Enable USART1, TX and RX
    USART1_CR1 = (1 << 13) | (1 << 3) | (1 << 2); // UE, TE, RE
}

void uart_send_char(char c) {
    while (!(USART1_SR & (1 << 7))); // Wait for TXE
    USART1_DR = c;
}

void uart_send_string(const char *str) {
    while (*str) {
        uart_send_char(*str++);
    }
}

// Device control functions
void devices_init(void) {
    // Set PB0 and PB1 as output for control
    GPIOB_CRL &= ~(0xF << (4 * OXYGENATOR_PIN) | 0xF << (4 * FEEDER_PIN));
    GPIOB_CRL |= (0x3 << (4 * OXYGENATOR_PIN) | 0x3 << (4 * FEEDER_PIN)); // Output push-pull, 50MHz
    GPIOB_ODR &= ~((1 << OXYGENATOR_PIN) | (1 << FEEDER_PIN)); // Initially off
}

void control_oxygenator(uint8_t state) {
    if (state) GPIOB_ODR |= (1 << OXYGENATOR_PIN);
    else GPIOB_ODR &= ~(1 << OXYGENATOR_PIN);
}

void control_feeder(uint8_t state) {
    if (state) GPIOB_ODR |= (1 << FEEDER_PIN);
    else GPIOB_ODR &= ~(1 << FEEDER_PIN);
}

// Main function
int main(void) {
    // Enable clocks
    RCC_APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;
    
    // Initialize peripherals
    onewire_init();
    adc_init();
    uart_init();
    devices_init();
    
    float temperature, ph_value, do_value;
    uint16_t adc_ph, adc_do;
    
    while (1) {
        // Read sensors
        temperature = read_ds18b20();
        adc_ph = read_adc(ADC_CHANNEL_PH);
        adc_do = read_adc(ADC_CHANNEL_DO);
        
        // Convert ADC to actual values (example conversions, adjust based on calibration)
        ph_value = (adc_ph / 4095.0) * 14.0; // Assuming 0-3.3V corresponds to 0-14 pH
        do_value = (adc_do / 4095.0) * 20.0; // Assuming 0-3.3V corresponds to 0-20 mg/L
        
        // Control devices based on thresholds (example thresholds)
        if (do_value < 5.0) control_oxygenator(1); // Turn on oxygenator if DO low
        else control_oxygenator(0);
        
        // Example: control feeder based on time or other logic, here simple on/off
        // In real application, use timer or external trigger
        control_feeder(0); // Off for now, add logic as needed
        
        // Send data via UART to ESP32-CAM for cloud upload
        char buffer[100];
        sprintf(buffer, "T:%.2f,PH:%.2f,DO:%.2f\r\n", temperature, ph_value, do_value);
        uart_send_string(buffer);
        
        delay_ms(5000); // Read every 5 seconds
    }
}

3.8 STM32项目核心代码

cpp 复制代码
#include <stdint.h>

// Register definitions for STM32F103
#define RCC_BASE        0x40021000
#define GPIOA_BASE      0x40010800
#define GPIOB_BASE      0x40010C00
#define ADC1_BASE       0x40012400
#define USART1_BASE     0x40013800

#define RCC_APB2ENR     (*(volatile uint32_t *)(RCC_BASE + 0x18))
#define GPIOA_CRL       (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_CRH       (*(volatile uint32_t *)(GPIOA_BASE + 0x04))
#define GPIOA_IDR       (*(volatile uint32_t *)(GPIOA_BASE + 0x08))
#define GPIOA_ODR       (*(volatile uint32_t *)(GPIOA_BASE + 0x0C))
#define GPIOB_CRL       (*(volatile uint32_t *)(GPIOB_BASE + 0x00))
#define GPIOB_ODR       (*(volatile uint32_t *)(GPIOB_BASE + 0x0C))
#define ADC1_SR         (*(volatile uint32_t *)(ADC1_BASE + 0x00))
#define ADC1_CR1        (*(volatile uint32_t *)(ADC1_BASE + 0x04))
#define ADC1_CR2        (*(volatile uint32_t *)(ADC1_BASE + 0x08))
#define ADC1_SMPR2      (*(volatile uint32_t *)(ADC1_BASE + 0x10))
#define ADC1_SQR1       (*(volatile uint32_t *)(ADC1_BASE + 0x2C))
#define ADC1_SQR3       (*(volatile uint32_t *)(ADC1_BASE + 0x34))
#define ADC1_DR         (*(volatile uint32_t *)(ADC1_BASE + 0x4C))
#define USART1_SR       (*(volatile uint32_t *)(USART1_BASE + 0x00))
#define USART1_DR       (*(volatile uint32_t *)(USART1_BASE + 0x04))
#define USART1_BRR      (*(volatile uint32_t *)(USART1_BASE + 0x08))
#define USART1_CR1      (*(volatile uint32_t *)(USART1_BASE + 0x0C))

// Bit definitions
#define RCC_APB2ENR_IOPAEN    (1 << 2)
#define RCC_APB2ENR_IOPBEN    (1 << 3)
#define RCC_APB2ENR_ADC1EN    (1 << 9)
#define RCC_APB2ENR_USART1EN  (1 << 14)
#define ADC_CR2_ADON          (1 << 0)
#define ADC_CR2_SWSTART       (1 << 22)
#define USART_CR1_UE          (1 << 13)
#define USART_CR1_TE          (1 << 3)
#define USART_CR1_RE          (1 << 2)
#define USART_SR_TXE          (1 << 7)
#define USART_SR_RXNE         (1 << 5)

// Pin definitions
#define AERATOR_PIN   0  // PB0
#define FEEDER_PIN    1  // PB1

// Assume external functions for sensors (already implemented)
extern float DS18B20_ReadTemp(void);  // Returns temperature in Celsius
extern void DS18B20_Init(void);       // Initializes DS18B20 sensor

// Function prototypes
void GPIO_Init(void);
void ADC_Init(void);
void USART_Init(void);
void USART_SendChar(char c);
void USART_SendString(const char *str);
uint16_t ADC_Read(uint8_t channel);
void delay_ms(uint32_t ms);

int main(void) {
    // Initialize system clock (assumed to be 72MHz, configured elsewhere)
    GPIO_Init();
    ADC_Init();
    USART_Init();
    DS18B20_Init();  // Initialize DS18B20 sensor

    float temperature, pH_value, do_value;
    uint16_t pH_raw, do_raw;
    uint32_t feed_counter = 0;

    while(1) {
        // Read sensors
        temperature = DS18B20_ReadTemp();
        pH_raw = ADC_Read(1);  // Channel 1 for PA1 (pH sensor)
        do_raw = ADC_Read(2);  // Channel 2 for PA2 (DO sensor)

        // Convert ADC values to actual values (example calibration, adjust as needed)
        pH_value = (pH_raw * 3.3f / 4096.0f) * 3.0f + 4.0f;  // Example: scale and offset for pH
        do_value = (do_raw * 3.3f / 4096.0f) * 10.0f;        // Example: scale for DO (mg/L)

        // Control aerator based on DO value
        if (do_value < 5.0f) {  Threshold for low dissolved oxygen
            GPIOB_ODR |= (1 << AERATOR_PIN);  // Turn on aerator
        } else {
            GPIOB_ODR &= ~(1 << AERATOR_PIN); // Turn off aerator
        }

        // Control feeder based on time (example: feed every 60 seconds)
        feed_counter++;
        if (feed_counter >= 60000) {  // Assuming loop delay of 1ms, so 60000 loops = 60 seconds
            GPIOB_ODR |= (1 << FEEDER_PIN);  // Turn on feeder
            delay_ms(1000);                  // Feed for 1 second
            GPIOB_ODR &= ~(1 << FEEDER_PIN); // Turn off feeder
            feed_counter = 0;
        }

        // Send data to ESP32 via USART for uploading to Huawei Cloud
        char buffer[64];
        sprintf(buffer, "%.2f,%.2f,%.2f\r\n", temperature, pH_value, do_value);
        USART_SendString(buffer);

        delay_ms(1000);  // Delay for 1 second between readings
    }
}

void GPIO_Init(void) {
    // Enable clocks for GPIOA and GPIOB
    RCC_APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;

    // Configure PA0 for DS18B20 (input with pull-up, assuming one-wire)
    GPIOA_CRL &= ~(0xF << 0);  // Clear CNF0 and MODE0
    GPIOA_CRL |= (0x8 << 0);   Input pull-up/pull-down
    GPIOA_ODR |= (1 << 0);     Set pull-up

    // Configure PA1 and PA2 as analog inputs for pH and DO sensors
    GPIOA_CRL &= ~(0xF << 4);  // For PA1
    GPIOA_CRL |= (0x0 << 4);   Analog mode
    GPIOA_CRL &= ~(0xF << 8);  // For PA2
    GPIOA_CRL |= (0x0 << 8);   Analog mode

    // Configure PB0 and PB1 as output for relays (push-pull, 50MHz)
    GPIOB_CRL &= ~(0xF << 0);  // For PB0
    GPIOB_CRL |= (0x3 << 0);   Output push-pull, 50MHz
    GPIOB_CRL &= ~(0xF << 4);  // For PB1
    GPIOB_CRL |= (0x3 << 4);   Output push-pull, 50MHz

    // Initially turn off relays
    GPIOB_ODR &= ~(1 << AERATOR_PIN);
    GPIOB_ODR &= ~(1 << FEEDER_PIN);
}

void ADC_Init(void) {
    // Enable ADC1 clock
    RCC_APB2ENR |= RCC_APB2ENR_ADC1EN;

    // Configure ADC1: single conversion mode, right alignment
    ADC1_CR2 = 0;
    ADC1_CR2 |= ADC_CR2_ADON;  Enable ADC

    // Set sample time for channels 1 and 2 to 239.5 cycles (long sample time for accuracy)
    ADC1_SMPR2 &= ~(0x7 << 3);  // Clear SMP1 (channel 1)
    ADC1_SMPR2 |= (0x7 << 3);   Set to 239.5 cycles
    ADC1_SMPR2 &= ~(0x7 << 6);  // Clear SMP2 (channel 2)
    ADC1_SMPR2 |= (0x7 << 6);   Set to 239.5 cycles
}

uint16_t ADC_Read(uint8_t channel) {
    // Configure channel for single conversion
    ADC1_SQR3 = channel;  Set first conversion in sequence

    // Start conversion
    ADC1_CR2 |= ADC_CR2_SWSTART;

    // Wait for conversion complete
    while (!(ADC1_SR & (1 << 1)));  Wait for EOC flag

    return ADC1_DR;  Read converted value
}

void USART_Init(void) {
    // Enable USART1 clock
    RCC_APB2ENR |= RCC_APB2ENR_USART1EN;

    // Configure PA9 as alternate function push-pull (TX)
    GPIOA_CRH &= ~(0xF << 4);  // Clear bits for PA9
    GPIOA_CRH |= (0xB << 4);   AF output push-pull, 50MHz

    // Configure PA10 as input floating (RX)
    GPIOA_CRH &= ~(0xF << 8);  // Clear bits for PA10
    GPIOA_CRH |= (0x4 << 8);   Input floating

    // Set baud rate to 9600 (assuming 72MHz clock)
    USART1_BRR = (468 << 4) | (12 & 0xF);  USARTDIV = 468.75

    // Enable USART, transmitter, and receiver
    USART1_CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}

void USART_SendChar(char c) {
    while (!(USART1_SR & USART_SR_TXE));  Wait for TX buffer empty
    USART1_DR = c;
}

void USART_SendString(const char *str) {
    while (*str) {
        USART_SendChar(*str++);
    }
}

void delay_ms(uint32_t ms) {
    for (uint32_t i = 0; i < ms * 7200; i++);  Crude delay for 72MHz system
}

四、总结

本系统基于华为云物联网平台,设计并实现了一套智能水产养殖监控系统,旨在通过实时数据采集和设备自动控制,提升养殖管理的效率和可靠性。系统集成多种传感器和硬件组件,实现了对水体关键参数的持续监测与智能响应。

硬件方面,以STM32F103C8T6核心板作为主控制器,协调DS18B20温度传感器、PH-4502C pH传感器和DO溶解氧传感器模块的数据采集,确保水温、酸碱度和溶氧量的准确测量。ESP32-CAM模块负责水下影像捕获,而信号处理电路通过洞洞板焊接和杜邦线连接,保障了传感器与控制器之间的稳定通信。

软件和云集成部分,QT上位机提供直观的界面,显示水质参数变化曲线和设备运行状态,便于用户实时监控。华为云物联网服务器作为后端支撑,处理数据存储、分析和远程控制指令,实现了增氧机和投饵机等设备的自动化运行,增强了系统的可扩展性和远程管理能力。

整体而言,该系统结合了嵌入式硬件、传感器技术和云计算,为水产养殖提供了高效、可靠的智能化解决方案,有助于降低人工成本、优化养殖环境,并促进可持续发展。

相关推荐
天天摸鱼的java工程师5 小时前
别再只会 new 了!八年老炮带你看透对象创建的 5 层真相
java·后端
洛阳泰山5 小时前
MaxKB4j智能体平台 Docker Compose 快速部署教程
java·人工智能·后端
AAA修煤气灶刘哥5 小时前
10 分钟吃透!同步异步不绕弯,MQ+RabbitMQ 入门到实操
后端·spring cloud·rabbitmq
努力的小郑6 小时前
MySQL索引(一):从数据结构到存储引擎的实现
后端·mysql
倚栏听风雨6 小时前
MapStruct
后端
盖世英雄酱581366 小时前
深入探索 Java 栈
java·后端
IT果果日记6 小时前
Flink+Dinky实现UDF自定义函数
大数据·后端·flink
华仔啊6 小时前
Java异常处理别再瞎搞了!阿里大神总结的 9 种最佳实践,让你的代码更健壮!
java·后端
武子康7 小时前
大数据-87 Spark 实现圆周率计算与共同好友分析:Scala 实战案例
大数据·后端·spark