基于华为云设计的智能宠物喂养管理系统

2 阅读47分钟

一、前言

1.1 项目开发背景

随着现代生活节奏的加快,宠物主人往往因工作繁忙、旅行或日常事务无法确保宠物的定时定量喂养,这可能导致宠物健康问题,如肥胖或营养不良。传统喂养方式依赖人工操作,容易出现疏忽或不一致,尤其对于独居宠物或长时间外出的家庭,喂养管理成为一大挑战。此外,余粮监测不足可能导致粮食浪费或断粮风险,影响宠物的福祉。

物联网和智能家居技术的快速发展为解决这些问题提供了新途径。通过自动化设备和云平台集成,可以实现远程监控和智能控制,提升宠物护理的便捷性和可靠性。华为云物联网服务器作为可靠的基础设施,支持设备连接、数据存储和实时通信,使得开发智能宠物喂养系统成为可能,并确保系统的稳定性和可扩展性。

本项目基于华为云设计智能宠物喂养管理系统,旨在通过硬件和软件结合,实现精准的自动喂养、余粮预警和进食行为监控。系统利用STM32微控制器作为核心,协调步进电机控制投放机构、称重传感器监测余量,以及摄像头模块捕捉进食画面,并通过Wi-Fi模块将数据传输至华为云平台,实现远程管理和数据分析。

硬件组成包括STM32F103C8T6最小系统板、28BYJ-48步进电机、HX711称重传感器、OV2640摄像头模块和ESP8266-01S Wi-Fi模块,通过洞洞板焊接和杜邦线连接构建可靠电路。软件方面,QT上位机提供用户友好界面,用于设置喂养计划、查看记录和状态,从而形成一个完整的智能喂养解决方案,提升宠物主人的生活质量和宠物的健康水平。

最终的实物产品:

image-20250902161730341

设计的模块模型:

image-20250902161835945

1.2 设计实现的功能

(1) 定时定量自动投放宠物粮食

(2) 称重传感器监测余粮量并预警

(3) 摄像头监控宠物进食行为

(4) QT上位机设置喂养计划、查看进食记录和余粮状态

(5) 服务器采用华为云物联网服务器

1.3 项目硬件模块组成

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

(2)28BYJ-48步进电机控制粮食投放机构

(3)HX711称重传感器监测粮食余量

(4)OV2640摄像头模块监控进食情况

(5)ESP8266-01S Wi-Fi模块实现华为云数据传输

(6)洞洞板焊接驱动电路,杜邦线连接各功能模块

1.4 设计意义

基于华为云设计的智能宠物喂养管理系统通过自动化技术显著提升了宠物喂养的便利性和可靠性。系统能够根据预设计划定时定量投放粮食,确保宠物获得规律饮食,减少主人的日常负担,尤其适用于工作繁忙或经常外出的宠物主人,避免因疏忽导致的喂养不均问题。

称重传感器实时监测余粮量并在不足时发出预警,这一功能有效防止了粮食耗尽的情况,保障宠物持续获得食物供应。预警机制通过华为云物联网服务器及时通知用户,使得主人能够远程响应并补充粮食,提升了系统的应急处理能力。

摄像头监控宠物进食行为提供了额外的健康管理维度。通过观察进食情况,主人可以及时发现宠物的异常行为或食欲变化,这可能预示着健康问题,从而促进早期干预和兽医咨询,增强宠物的福祉。

QT上位机界面允许用户直观地设置喂养计划、查看历史记录和当前状态,结合华为云的数据存储和处理能力,实现了数据的集中管理和远程访问。这种集成使得系统不仅智能,而且用户友好,支持多设备访问,方便主人随时随地管理宠物喂养。

硬件组成采用常见的STM32控制器、步进电机、传感器和Wi-Fi模块,通过洞洞板焊接和杜邦线连接,体现了成本效益和可定制性。华为云服务器的使用确保了数据的安全性和可扩展性,为未来功能升级奠定了基础,同时保持了系统的稳定性和可靠性。

1.5 设计思路

设计思路基于华为云智能宠物喂养管理系统的功能需求和硬件组成,首先以STM32F103C8T6最小系统核心板作为主控制器,负责整体系统的协调和控制。该控制器通过编程实现逻辑处理,包括接收来自上位机的指令、处理传感器数据以及驱动执行机构。系统通过洞洞板焊接驱动电路,并使用杜邦线连接各模块,确保硬件之间的可靠通信和电源管理。

定时定量自动投放宠物粮食的功能通过28BYJ-48步进电机实现,该电机由STM32控制,通过驱动电路精确控制旋转角度和速度,从而推动投放机构释放预定量的粮食。STM32根据上位机设置的喂养计划,定时触发电机动作,并通过算法确保投放量的准确性,避免过量或不足。

余粮量监测和预警功能依赖HX711称重传感器,该传感器实时测量粮食容器的重量,并将数据传送至STM32。STM32处理这些数据,计算剩余粮食量,并在余量低于阈值时通过Wi-Fi模块发送预警信息到华为云服务器,从而提醒用户及时补充粮食。

摄像头监控部分采用OV2640模块,用于捕捉宠物进食行为的图像或视频。OV2640通过接口与STM32连接,STM32控制摄像头的启停和图像采集,并将数据暂存或直接通过Wi-Fi传输。这使得用户可以通过上位机查看实时监控或历史记录,辅助分析宠物健康状况。

数据传输和云集成通过ESP8266-01S Wi-Fi模块实现,该模块与STM32串口通信,负责将传感器数据、预警信息和摄像头数据上传到华为云物联网服务器。同时,它接收来自云端的指令,如下发新的喂养计划,确保系统与远程服务的双向交互。

上位机交互基于QT开发的应用,允许用户设置喂养计划、查看进食记录和余粮状态。QT应用通过网络与华为云服务器通信,获取实时数据并显示给用户,同时支持用户输入指令,这些指令经云服务器转发至STM32,实现远程控制和管理。整个设计注重实际可行性和稳定性,确保各模块协同工作。

1.6 框架图

Syntax error in graphmermaid version 8.8.3

1.7 系统总体设计

智能宠物喂养管理系统旨在通过自动化技术实现宠物的定时定量喂养,并集成云平台进行远程监控和管理。系统采用华为云物联网服务器作为后端,支持数据存储和远程访问,确保用户能够实时掌握宠物喂养情况。

系统以STM32F103C8T6最小系统核心板作为主控制器,负责协调所有硬件模块的工作。主控制器处理来自传感器的输入数据,控制执行机构,并管理网络通信,确保系统整体运行稳定可靠。

粮食投放机构由28BYJ-48步进电机驱动,主控制器根据用户预设的喂养计划控制电机旋转,实现定时定量自动投放宠物粮食。投放过程精确可控,避免浪费或过量喂养。

HX711称重传感器用于监测粮食余量,实时数据被主控制器读取和处理。当余量低于预设阈值时,系统会生成预警信号,并通过云平台通知用户,及时提醒补充粮食。

OV2640摄像头模块监控宠物进食行为,捕捉视频或图像数据。这些数据可以通过网络传输到华为云服务器,供用户远程查看和分析宠物进食情况,增强监控的直观性。

ESP8266-01S Wi-Fi模块负责实现与华为云物联网服务器的数据传输。主控制器通过Wi-Fi模块发送传感器数据、接收控制命令,并同步喂养计划,确保系统与云端的实时交互。

QT上位机应用程序提供用户界面,允许用户设置喂养计划、查看进食记录和余粮状态。数据通过华为云服务器与硬件系统交互,实现远程管理和历史数据查询。

系统硬件通过洞洞板焊接驱动电路,并使用杜邦线连接各功能模块,包括主控制器、步进电机、称重传感器、摄像头和Wi-Fi模块。这种连接方式确保电气稳定性和模块间的可靠通信。

1.8 系统功能总结

功能描述实现方式
定时定量自动投放宠物粮食STM32F103C8T6控制28BYJ-48步进电机执行投放机构
监测余粮量并预警HX711称重传感器采集数据,经ESP8266上传华为云,QT上位机显示预警信息
监控宠物进食行为OV2640摄像头模块捕获视频,数据可本地存储或通过云处理
设置喂养计划、查看进食记录和余粮状态QT上位机应用通过Wi-Fi与系统交互,数据同步至华为云
云数据传输与服务器集成ESP8266-01S Wi-Fi模块连接华为云物联网服务器,实现数据上传和远程管理

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

STM32F103C8T6最小系统核心板作为整个智能宠物喂养管理系统的中央处理单元,负责协调和控制所有外围模块。它通过编程实现定时定量投放逻辑,处理来自传感器的数据,并执行与上位机和云服务器的通信协议,确保系统稳定运行。

28BYJ-48步进电机用于驱动粮食投放机构,通过精确控制步进电机的旋转角度和速度,实现定时和定量的粮食释放。电机由STM32通过驱动电路(如ULN2003)控制,确保投放过程准确可靠,满足喂养计划的需求。

HX711称重传感器模块安装在粮食容器底部,用于实时监测粮食余量。它将重量数据转换为数字信号供STM32读取,当余量低于预设阈值时,系统会触发预警机制,并通过Wi-Fi模块上传状态到云服务器,确保用户及时获知余粮情况。

OV2640摄像头模块用于监控宠物进食行为,捕捉图像或视频流。该模块通过串口或并行接口与STM32连接,数据可以本地处理或通过Wi-Fi传输到华为云服务器,供QT上位机查看和分析进食记录,增强监控功能。

ESP8266-01S Wi-Fi模块实现STM32与华为云物联网服务器之间的无线数据传输。它负责将传感器数据(如余粮状态和投放记录)上传到云平台,同时接收来自云端的喂养计划指令,确保远程监控和控制功能的实现。

洞洞板用于焊接各模块的驱动电路和接口电路,提供稳定的电源管理和信号调理。杜邦线则用于灵活连接STM32核心板与各功能模块,确保硬件组件之间的可靠电气连接,简化系统搭建和维护过程。

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

image-20221204193824815

2.1 物联网平台介绍

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

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

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

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

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

img

2.2 物联网服务

image-20241028135834377

开通免费单元。

image-20241028135935457

点击立即创建

image-20240117134653452

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

image-20241028140048811

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

image-20241028140129102

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

image-20241028140229696

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

image-20241028140331523

image-20241028140428663

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

image-20241028140511105

总结:

端口号:   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:\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 创建产品

(1)创建产品

image-20241028141601305

(2)填写产品信息

根据自己产品名字填写,下面的设备类型选择自定义类型。

image-20240612094809689

(3)产品创建成功

image-20240612095148945

创建完成之后点击查看详情。

image-20240612095134263

(4)添加自定义模型

产品创建完成之后,点击进入产品详情页面,翻到最下面可以看到模型定义。

模型简单来说: 就是存放设备上传到云平台的数据。

你可以根据自己的产品进行创建。

比如:

烟雾可以叫  MQ2
温度可以叫  Temperature
湿度可以叫  humidity
火焰可以叫  flame
其他的传感器自己用单词简写命名即可。 这就是你的单片机设备端上传到服务器的数据名字。

先点击自定义模型。

image-20240612095517900

再创建一个服务ID。

image-20240612095542749

接着点击新增属性。

image-20240612095648815

image-20240612095711898

2.4 添加设备

产品是属于上层的抽象模型,接下来在产品模型下添加实际的设备。添加的设备最终需要与真实的设备关联在一起,完成数据交互。

(1)注册设备

image-20240425181935561

(2)根据自己的设备填写

image-20240612100115167

(3)保存设备信息

创建完毕之后,点击保存并关闭,得到创建的设备密匙信息。该信息在后续生成MQTT三元组的时候需要使用。

image-20240612100128061

(4)设备创建完成

image-20240612100147232

(5)设备详情

image-20240612100202960

image-20240612100217236

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…

img

业务流程:

img

(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…

image-20221207153310037

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

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

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

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

(4)主题发布格式

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

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

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

image-20221207153637391

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

发布的主题格式:
$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…

image-20240509193207359

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

image-20240425182610048

(2)生成MQTT三元组

华为云提供了一个在线工具,用来生成MQTT鉴权三元组: iot-tool.obs-website.cn-north-4.myhuaweicloud.com/

打开这个工具,填入设备的信息(也就是刚才创建完设备之后保存的信息),点击生成,就可以得到MQTT的登录信息了。

下面是打开的页面:

image-20240425183025893

填入设备的信息: (上面两行就是设备创建完成之后保存得到的)

直接得到三元组信息。

image-20240509193310020

得到三元组之后,设备端通过MQTT协议登录鉴权的时候,填入参数即可。

ClientId  663cb18871d845632a0912e7_dev1_0_0_2024050911
Username  663cb18871d845632a0912e7_dev1
Password  71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237

2.7 模拟设备登录测试

经过上面的步骤介绍,已经创建了产品,设备,数据模型,得到MQTT登录信息。 接下来就用MQTT客户端软件模拟真实的设备来登录平台。测试与服务器通信是否正常。

(1)填入登录信息

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

image-20240509193457358

(2)打开网页查看

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

image-20240612100508790

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

image-20240612100529581

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

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

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

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

image-20240509193646253

image-20240509193701262

项目凭证:

28add376c01e4a61ac8b621c714bf459

【2】创建IAM用户

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

image-20240509193729078

点击左上角创建用户

image-20240509193744287

image-20240314153208692

image-20240314153228359

image-20240314153258229

创建成功:

image-20240314153315444

【3】创建完成

image-20240509193828289

用户信息如下:

主用户名  l19504562721
IAM用户  ds_abc
密码     DS12345678

2.9 获取影子数据

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

设备影子介绍:

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

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

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

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

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

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

image-20240509194152229

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

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

image-20240509194214716

链接如下:

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

三、上位机代码设计

3.1 Qt开发环境安装

image-20221207160606892

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

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

image-20221203151742653

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

image-20241028152725134

3.2 新建上位机工程

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

【1】新建工程

image-20240117144052547

【2】设置项目的名称。

image-20250420200347498

【3】选择编译系统

image-20240117144239681

【4】选择默认继承的类

image-20240117144302275

【5】选择编译器

image-20241028153603487

【6】点击完成

image-20240117144354252

【7】工程创建完成

image-20250420200411303

3.3 切换编译器

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

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

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

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

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

image-20250420200424965

3.4 编译测试功能

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

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

image-20250420200442769

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

image-20250420200457319

3.5 设计UI界面与工程配置

【1】打开UI文件

image-20250420200514220

打开默认的界面如下:

image-20250420200526194

【2】开始设计界面

根据自己需求设计界面。

3.6 设计代码

【1】获取token

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

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

image-20221207175635181

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

image-20250402143229329

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


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

2. 构造请求URL
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. 设置请求头
// 设置数据提交格式
request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json;charset=UTF-8"));
  • 设置HTTP请求头,指定请求体的数据格式为application/json;charset=UTF-8,表示发送的数据是JSON格式。

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

5. 构造请求体
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请求
// 发送请求
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】获取影子数据

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

image-20250402143256287

这段代码的功能是向华为云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:

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】判断设备是否离线

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

image-20250402143349492

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


(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接口返回的数据,解析出来里面设备数据的时间,进行显示。

image-20250402143424863

这段代码的主要作用是解析华为云 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

3.5 编译Windows上位机

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

image-20250402144112519

3.6 上位机代码设计

// main.cpp
#include <QApplication>
#include "mainwindow.h"

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

#include <QMainWindow>
#include <QMqttClient>
#include <QTableWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QTimeEdit>
#include <QSpinBox>
#include <QTextEdit>

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private slots:
    void onConnectButtonClicked();
    void onDisconnectButtonClicked();
    void onSetPlanButtonClicked();
    void onMessageReceived(const QByteArray &message, const QMqttTopicName &topic);
    void updateStatus();

private:
    void setupUI();
    void connectToCloud();
    void publishMessage(const QString &topic, const QString &message);
    void subscribeToTopics();

    QMqttClient *m_client;
    QLineEdit *m_serverEdit;
    QLineEdit *m_clientIdEdit;
    QLineEdit *m_usernameEdit;
    QLineEdit *m_passwordEdit;
    QPushButton *m_connectButton;
    QPushButton *m_disconnectButton;
    QTimeEdit *m_timeEdit;
    QSpinBox *m_amountSpin;
    QPushButton *m_setPlanButton;
    QTableWidget *m_recordTable;
    QLabel *m_foodStatusLabel;
    QTextEdit *m_logText;
};

#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
#include <QGridLayout>
#include <QGroupBox>
#include <QHeaderView>
#include <QMessageBox>
#include <QDateTime>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), m_client(new QMqttClient(this))
{
    setupUI();
    connect(m_client, &QMqttClient::messageReceived, this, &MainWindow::onMessageReceived);
    connect(m_client, &QMqttClient::stateChanged, this, &MainWindow::updateStatus);
}

MainWindow::~MainWindow()
{
    if (m_client->state() == QMqttClient::Connected)
        m_client->disconnectFromHost();
}

void MainWindow::setupUI()
{
    QWidget *centralWidget = new QWidget(this);
    setCentralWidget(centralWidget);
    QGridLayout *layout = new QGridLayout(centralWidget);

    // Connection group
    QGroupBox *connectionGroup = new QGroupBox("华为云连接", this);
    QGridLayout *connectionLayout = new QGridLayout(connectionGroup);
    m_serverEdit = new QLineEdit("iot-mqtts.cn-north-4.myhuaweicloud.com", this);
    m_clientIdEdit = new QLineEdit("pet_feeder_upper", this);
    m_usernameEdit = new QLineEdit("your_username", this);
    m_passwordEdit = new QLineEdit("your_password", this);
    m_passwordEdit->setEchoMode(QLineEdit::Password);
    m_connectButton = new QPushButton("连接", this);
    m_disconnectButton = new QPushButton("断开", this);
    m_disconnectButton->setEnabled(false);

    connectionLayout->addWidget(new QLabel("服务器:"), 0, 0);
    connectionLayout->addWidget(m_serverEdit, 0, 1);
    connectionLayout->addWidget(new QLabel("客户端ID:"), 1, 0);
    connectionLayout->addWidget(m_clientIdEdit, 1, 1);
    connectionLayout->addWidget(new QLabel("用户名:"), 2, 0);
    connectionLayout->addWidget(m_usernameEdit, 2, 1);
    connectionLayout->addWidget(new QLabel("密码:"), 3, 0);
    connectionLayout->addWidget(m_passwordEdit, 3, 1);
    connectionLayout->addWidget(m_connectButton, 4, 0);
    connectionLayout->addWidget(m_disconnectButton, 4, 1);

    // Feeding plan group
    QGroupBox *planGroup = new QGroupBox("喂养计划", this);
    QGridLayout *planLayout = new QGridLayout(planGroup);
    m_timeEdit = new QTimeEdit(this);
    m_timeEdit->setTime(QTime::currentTime());
    m_amountSpin = new QSpinBox(this);
    m_amountSpin->setRange(1, 1000);
    m_amountSpin->setSuffix(" g");
    m_setPlanButton = new QPushButton("设置计划", this);

    planLayout->addWidget(new QLabel("时间:"), 0, 0);
    planLayout->addWidget(m_timeEdit, 0, 1);
    planLayout->addWidget(new QLabel("食物量:"), 1, 0);
    planLayout->addWidget(m_amountSpin, 1, 1);
    planLayout->addWidget(m_setPlanButton, 2, 0, 1, 2);

    // Status and records group
    QGroupBox *statusGroup = new QGroupBox("状态和记录", this);
    QGridLayout *statusLayout = new QGridLayout(statusGroup);
    m_foodStatusLabel = new QLabel("余粮: 未知", this);
    m_recordTable = new QTableWidget(0, 3, this);
    m_recordTable->setHorizontalHeaderLabels(QStringList() << "时间" << "食物量" << "状态");
    m_recordTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    m_logText = new QTextEdit(this);
    m_logText->setReadOnly(true);

    statusLayout->addWidget(m_foodStatusLabel, 0, 0);
    statusLayout->addWidget(new QLabel("进食记录:"), 1, 0);
    statusLayout->addWidget(m_recordTable, 2, 0);
    statusLayout->addWidget(new QLabel("日志:"), 3, 0);
    statusLayout->addWidget(m_logText, 4, 0);

    layout->addWidget(connectionGroup, 0, 0);
    layout->addWidget(planGroup, 1, 0);
    layout->addWidget(statusGroup, 2, 0);

    connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::onConnectButtonClicked);
    connect(m_disconnectButton, &QPushButton::clicked, this, &MainWindow::onDisconnectButtonClicked);
    connect(m_setPlanButton, &QPushButton::clicked, this, &MainWindow::onSetPlanButtonClicked);
}

void MainWindow::onConnectButtonClicked()
{
    connectToCloud();
}

void MainWindow::onDisconnectButtonClicked()
{
    if (m_client->state() == QMqttClient::Connected)
        m_client->disconnectFromHost();
}

void MainWindow::connectToCloud()
{
    m_client->setHostname(m_serverEdit->text());
    m_client->setPort(8883); // Huawei Cloud MQTT SSL port
    m_client->setClientId(m_clientIdEdit->text());
    m_client->setUsername(m_usernameEdit->text());
    m_client->setPassword(m_passwordEdit->text());

    // Enable SSL (assuming Huawei Cloud requires SSL)
    QSslConfiguration sslConfig;
    sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); // For testing, adjust for production
    m_client->setTransport(QLatin1String("ssl"), sslConfig);

    m_client->connectToHost();
}

void MainWindow::subscribeToTopics()
{
    if (m_client->state() == QMqttClient::Connected)
    {
        auto subscription = m_client->subscribe(QLatin1String("pet/feeder/status"));
        if (!subscription)
            m_logText->append("订阅失败");
        else
            m_logText->append("订阅成功: pet/feeder/status");
    }
}

void MainWindow::publishMessage(const QString &topic, const QString &message)
{
    if (m_client->state() == QMqttClient::Connected)
    {
        if (m_client->publish(topic, message.toUtf8()) == -1)
            m_logText->append("发布失败");
        else
            m_logText->append("发布成功: " + message);
    }
    else
    {
        m_logText->append("未连接,无法发布");
    }
}

void MainWindow::onSetPlanButtonClicked()
{
    QTime time = m_timeEdit->time();
    int amount = m_amountSpin->value();
    QString message = QString("{"time": "%1", "amount": %2}").arg(time.toString("hh:mm")).arg(amount);
    publishMessage(QLatin1String("pet/feeder/plan"), message);
}

void MainWindow::onMessageReceived(const QByteArray &message, const QMqttTopicName &topic)
{
    QString msg = QString::fromUtf8(message);
    m_logText->append("收到消息: " + topic.name() + " - " + msg);

    if (topic.name() == "pet/feeder/status")
    {
        // Parse JSON message (simplified)
        if (msg.contains("food_weight"))
        {
            // Extract food weight and update label
            m_foodStatusLabel->setText("余粮: " + msg + " g");
        }
        else if (msg.contains("eating_record"))
        {
            // Add to record table
            int row = m_recordTable->rowCount();
            m_recordTable->insertRow(row);
            m_recordTable->setItem(row, 0, new QTableWidgetItem(QDateTime::currentDateTime().toString()));
            m_recordTable->setItem(row, 1, new QTableWidgetItem(msg)); // Simplified
            m_recordTable->setItem(row, 2, new QTableWidgetItem("完成"));
        }
    }
}

void MainWindow::updateStatus()
{
    switch (m_client->state())
    {
    case QMqttClient::Connected:
        m_connectButton->setEnabled(false);
        m_disconnectButton->setEnabled(true);
        m_logText->append("已连接到华为云");
        subscribeToTopics();
        break;
    case QMqttClient::Disconnected:
        m_connectButton->setEnabled(true);
        m_disconnectButton->setEnabled(false);
        m_logText->append("断开连接");
        break;
    default:
        break;
    }
}

3.7 STM32模块代码设计

#include <stdint.h>

// Define STM32F103C8T6 register addresses (based on Cortex-M3)
#define RCC_BASE        0x40021000
#define GPIOA_BASE      0x40010800
#define GPIOB_BASE      0x40010C00
#define USART1_BASE     0x40013800
#define TIM2_BASE       0x40000000

// RCC registers
#define RCC_APB2ENR     *(volatile uint32_t *)(RCC_BASE + 0x18)
#define RCC_APB1ENR     *(volatile uint32_t *)(RCC_BASE + 0x1C)

// GPIO registers
#define GPIOA_CRL       *(volatile uint32_t *)(GPIOA_BASE + 0x00)
#define GPIOA_CRH       *(volatile uint32_t *)(GPIOA_BASE + 0x04)
#define GPIOA_ODR       *(volatile uint32_t *)(GPIOA_BASE + 0x0C)
#define GPIOB_CRL       *(volatile uint32_t *)(GPIOB_BASE + 0x00)
#define GPIOB_CRH       *(volatile uint32_t *)(GPIOB_BASE + 0x04)
#define GPIOB_ODR       *(volatile uint32_t *)(GPIOB_BASE + 0x0C)
#define GPIOB_IDR       *(volatile uint32_t *)(GPIOB_BASE + 0x08)

// USART1 registers
#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)

// TIM2 registers
#define TIM2_CR1        *(volatile uint32_t *)(TIM2_BASE + 0x00)
#define TIM2_PSC        *(volatile uint32_t *)(TIM2_BASE + 0x28)
#define TIM2_ARR        *(volatile uint32_t *)(TIM2_BASE + 0x2C)
#define TIM2_CNT        *(volatile uint32_t *)(TIM2_BASE + 0x24)
#define TIM2_SR         *(volatile uint32_t *)(TIM2_BASE + 0x10)

// Pin definitions for modules
// Stepper motor (28BYJ-48) connected to GPIOA pins 0-3
#define MOTOR_PIN0      (1 << 0)
#define MOTOR_PIN1      (1 << 1)
#define MOTOR_PIN2      (1 << 2)
#define MOTOR_PIN3      (1 << 3)

// HX711 connected to GPIOB pins 0 (DT) and 1 (SCK)
#define HX711_DT_PIN    (1 << 0)
#define HX711_SCK_PIN   (1 << 1)

// OV2640 I2C pins: SDA on GPIOB pin 7, SCL on GPIOB pin 6
#define OV2640_SDA_PIN  (1 << 7)
#define OV2640_SCL_PIN  (1 << 6)

// ESP8266 connected to USART1 (TX: GPIOA pin 9, RX: GPIOA pin 10)

// Function prototypes
void SystemInit(void);
void GPIO_Init(void);
void USART1_Init(void);
void TIM2_Init(void);
void StepperMotor_Step(uint8_t step);
void HX711_Init(void);
int32_t HX711_Read(void);
void OV2640_Init(void);
void ESP8266_SendCommand(const char *cmd);
void USART1_SendChar(char c);
void USART1_SendString(const char *str);
char USART1_ReceiveChar(void);

// Stepper motor sequence for 28BYJ-48 (half-step)
const uint8_t stepper_seq[] = {0x09, 0x08, 0x0C, 0x04, 0x06, 0x02, 0x03, 0x01};
uint8_t stepper_index = 0;

// HX711 variables
int32_t hx711_offset = 0;
float hx711_scale = 1.0; // Calibration factor

int main(void) {
    SystemInit();
    GPIO_Init();
    USART1_Init();
    TIM2_Init();
    HX711_Init();
    OV2640_Init();

    // Calibrate HX711 (tare)
    hx711_offset = HX711_Read();

    while (1) {
        // Example: Check weight and control motor
        int32 weight = HX711_Read() - hx711_offset;
        float weight_grams = weight / hx711_scale;

        if (weight_grams < 100) { // If less than 100g, trigger warning
            // Send warning to Huawei Cloud via ESP8266
            ESP8266_SendCommand("AT+CIPSEND=0,50\r\n");
            // Send data format, e.g., JSON: {"weight": weight_grams, "warning": "low_food"}
            // Wait for response and handle
        }

        // Example feeding schedule: every 12 hours, rotate motor
        // Use TIM2 for timing or simple delay in loop
        for (int i = 0; i < 512; i++) { // Rotate 360 degrees approx
            StepperMotor_Step(stepper_index);
            stepper_index = (stepper_index + 1) % 8;
            for (volatile int d = 0; d < 1000; d++); // Simple delay
        }

        // Monitor with OV2640 (simplified: just init, actual image capture not implemented due to complexity)
        // Send image data via ESP8266 if needed
    }
}

void SystemInit(void) {
    // Enable clocks for GPIOA, GPIOB, USART1, TIM2
    RCC_APB2ENR |= (1 << 2) | (1 << 3) | (1 << 14); // GPIOA, GPIOB, USART1 clocks
    RCC_APB1ENR |= (1 << 0); // TIM2 clock
}

void GPIO_Init(void) {
    // GPIOA for motor and USART1
    GPIOA_CRL &= ~0xFFFF; // Clear PA0-3 for motor output
    GPIOA_CRL |= 0x1111; // PA0-3 as output push-pull, 10MHz
    GPIOA_CRH &= ~0xFF00; // Clear PA9 (TX) and PA10 (RX) for USART1
    GPIOA_CRH |= 0x4B00; // PA9 as AF push-pull, PA10 as input floating

    // GPIOB for HX711 and OV2640 I2C
    GPIOB_CRL &= ~0x0000000F; // Clear PB0 and PB1
    GPIOB_CRL |= 0x00000008; // PB0 as input floating (DT), PB1 as output push-pull (SCK)
    GPIOB_CRH &= ~0xFF000000; // Clear PB6 and PB7
    GPIOB_CRH |= 0x33000000; // PB6 and PB7 as output open-drain for I2C (simulated)
}

void USART1_Init(void) {
    USART1_BRR = 0x1D4; // 9600 baud at 8MHz PCLK2 (adjust based on system clock)
    USART1_CR1 |= (1 << 13) | (1 << 3) | (1 << 2); // UE, TE, RE
}

void TIM2_Init(void) {
    TIM2_PSC = 7999; // Prescaler for 1ms tick (assuming 8MHz clock)
    TIM2_ARR = 999; // Auto-reload for 1s interval
    TIM2_CR1 |= (1 << 0); // Enable TIM2
}

void StepperMotor_Step(uint8_t step) {
    GPIOA_ODR = (GPIOA_ODR & ~0x0F) | stepper_seq[step];
}

void HX711_Init(void) {
    GPIOB_ODR &= ~HX711_SCK_PIN; // Set SCK low
    // Wait for DT to become low indicating ready
    while (GPIOB_IDR & HX711_DT_PIN);
}

int32_t HX711_Read(void) {
    int32_t value = 0;
    GPIOB_ODR &= ~HX711_SCK_PIN; // Ensure SCK low

    while (GPIOB_IDR & HX711_DT_PIN); // Wait for DT low

    for (uint8_t i = 0; i < 24; i++) {
        GPIOB_ODR |= HX711_SCK_PIN; // Pulse SCK high
        for (volatile int d = 0; d < 10; d++); // Short delay
        value = (value << 1) | ((GPIOB_IDR & HX711_DT_PIN) ? 1 : 0);
        GPIOB_ODR &= ~HX711_SCK_PIN; // SCK low
        for (volatile int d = 0; d < 10; d++);
    }

    // Set gain to 128 (one more pulse)
    GPIOB_ODR |= HX711_SCK_PIN;
    for (volatile int d = 0; d < 10; d++);
    GPIOB_ODR &= ~HX711_SCK_PIN;

    // Convert to signed 32-bit
    if (value & 0x800000) {
        value |= 0xFF000000;
    }
    return value;
}

void OV2640_Init(void) {
    // Simulated I2C initialization for OV2640
    // Typically, we'd send I2C commands to configure camera, but simplified here
    GPIOB_ODR &= ~OV2640_SCL_PIN;
    GPIOB_ODR &= ~OV2640_SDA_PIN;
    // Add I2C start condition and write commands if needed
    // This is a placeholder; actual implementation requires full I2C protocol
}

void ESP8266_SendCommand(const char *cmd) {
    USART1_SendString(cmd);
}

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

void USART1_SendString(const char *str) {
    while (*str) {
        USART1_SendChar(*str++);
    }
}

char USART1_ReceiveChar(void) {
    while (!(USART1_SR & (1 << 5))); // Wait for RXNE
    return USART1_DR;
}

3.8 STM32项目核心代码

#include "stm32f10x.h"
#include "stdio.h"
#include "string.h"
#include "stepper_motor.h"
#include "hx711.h"
#include "camera.h"
#include "wifi.h"

// 定义系统时钟频率
#define SYSTEM_CLOCK_FREQ 72000000  // 72MHz

// 定义喂养计划结构
typedef struct {
    uint8_t hour;
    uint8_t minute;
    float amount;  // 投放量,单位克
} FeedingPlan;

// 全局变量
volatile uint32_t systemTime = 0;  // 系统时间,单位毫秒
FeedingPlan currentPlan = {8, 0, 50.0};  // 默认喂养计划:8:00, 50克
float currentWeight = 0.0;  // 当前余粮重量
uint8_t lowWeightWarning = 0;  // 低粮预警标志

// SysTick中断处理函数
void SysTick_Handler(void) {
    systemTime++;
}

// 初始化系统时钟
void SystemClock_Init(void) {
    // 启用外部晶振
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY));

    // 配置PLL:HSE输入,9倍频,输出72MHz
    RCC->CFGR |= RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9;
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY));

    // 切换系统时钟到PLL
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);

    // 配置AHB、APB1、APB2分频
    RCC->CFGR |= RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE1_DIV2 | RCC_CFGR_PPRE2_DIV1;
}

// 初始化SysTick定时器
void SysTick_Init(void) {
    SysTick->LOAD = (SYSTEM_CLOCK_FREQ / 1000) - 1;  // 1ms中断
    SysTick->VAL = 0;
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
}

// 初始化GPIO
void GPIO_Init(void) {
    // 启用GPIOA、GPIOB、GPIOC时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN;

    // 配置步进电机引脚(假设使用GPIOA的PIN0-PIN3)
    GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0 | GPIO_CRL_MODE1 | GPIO_CRL_CNF1 |
                   GPIO_CRL_MODE2 | GPIO_CRL_CNF2 | GPIO_CRL_MODE3 | GPIO_CRL_CNF3);
    GPIOA->CRL |= (GPIO_CRL_MODE0_0 | GPIO_CRL_MODE1_0 | GPIO_CRL_MODE2_0 | GPIO_CRL_MODE3_0);  // 推挽输出,10MHz

    // 配置HX711引脚(假设使用GPIOB的PIN0和PIN1)
    GPIOB->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0 | GPIO_CRL_MODE1 | GPIO_CRL_CNF1);
    GPIOB->CRL |= (GPIO_CRL_CNF0_1 | GPIO_CRL_CNF1_1);  // PIN0输入上拉,PIN1推挽输出
    GPIOB->ODR |= GPIO_ODR_ODR0;  // 上拉

    // 配置摄像头引脚(假设使用GPIOC的PIN0-PIN7用于数据,其他用于控制)
    // 简化处理,具体配置在camera模块中
}

// 初始化USART用于Wi-Fi通信(假设使用USART1,GPIOA的PIN9和PIN10)
void USART1_Init(void) {
    // 启用USART1时钟
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

    // 配置GPIOA PIN9为推挽复用输出(TX),PIN10为浮空输入(RX)
    GPIOA->CRH &= ~(GPIO_CRH_MODE9 | GPIO_CRH_CNF9 | GPIO_CRH_MODE10 | GPIO_CRH_CNF10);
    GPIOA->CRH |= (GPIO_CRH_MODE9_0 | GPIO_CRH_CNF9_1 | GPIO_CRH_CNF10_0);

    // 配置USART1:115200波特率,8数据位,无校验,1停止位
    USART1->BRR = 0x1D4C;  // 72MHz / 115200 ≈ 0x1D4C
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}

// 主函数
int main(void) {
    // 初始化系统
    SystemClock_Init();
    SysTick_Init();
    GPIO_Init();
    USART1_Init();
    
    // 初始化模块
    StepperMotor_Init();
    HX711_Init();
    Camera_Init();
    WiFi_Init();
    
    // 连接Wi-Fi和华为云
    WiFi_Connect("SSID", "password");  // 假设SSID和密码已配置
    Cloud_Connect();  // 连接华为云物联网服务器
    
    uint32_t lastFeedTime = 0;
    uint32_t lastWeightCheckTime = 0;
    uint32_t lastCameraCheckTime = 0;
    uint32_t lastCloudUpdateTime = 0;
    
    while (1) {
        uint32_t currentTime = systemTime;
        
        // 检查是否到达喂养时间
        if ((currentTime - lastFeedTime) >= (currentPlan.hour * 3600000 + currentPlan.minute * 60000)) {
            // 投放粮食
            StepperMotor_Control(currentPlan.amount);  // 假设函数根据量控制步数
            lastFeedTime = currentTime;
            
            // 记录喂养事件
            Cloud_SendData("Feeding occurred");  // 发送到云
        }
        
        // 每隔30秒检查余粮量
        if ((currentTime - lastWeightCheckTime) >= 30000) {
            currentWeight = HX711_ReadWeight();
            lastWeightCheckTime = currentTime;
            
            // 如果余粮低于阈值(假设100克),预警
            if (currentWeight < 100.0 && !lowWeightWarning) {
                lowWeightWarning = 1;
                Cloud_SendData("Low food warning");
            } else if (currentWeight >= 100.0) {
                lowWeightWarning = 0;
            }
        }
        
        // 每隔60秒检查摄像头(简化处理)
        if ((currentTime - lastCameraCheckTime) >= 60000) {
            Camera_Capture();  // 捕获图像,可能存储或发送
            lastCameraCheckTime = currentTime;
        }
        
        // 每隔10秒更新云状态
        if ((currentTime - lastCloudUpdateTime) >= 10000) {
            char statusMsg[50];
            sprintf(statusMsg, "Weight: %.2f g", currentWeight);
            Cloud_SendData(statusMsg);
            lastCloudUpdateTime = currentTime;
        }
        
        // 处理云命令(如下发喂养计划)
        if (Cloud_DataAvailable()) {
            char command[50];
            Cloud_ReceiveData(command, sizeof(command));
            // 解析命令并更新currentPlan(简化)
            // 例如,命令格式:"SET_PLAN hh:mm amount"
            // 这里省略解析代码
        }
    }
}

四、总结

基于华为云设计的智能宠物喂养管理系统成功实现了宠物喂养的智能化和远程化管理,通过集成多种传感器和执行器,系统能够自动定时定量投放粮食,确保宠物饮食的规律性和健康性。称重传感器实时监测余粮量,并在不足时及时预警,避免了断粮风险,而摄像头模块则允许用户远程观察宠物进食行为,增强互动性和安全性。

该系统以STM32F103C8T6最小系统核心板作为主控制器,协调步进电机驱动粮食投放机构,并通过HX711称重传感器和OV2640摄像头模块采集数据,硬件连接采用洞洞板焊接和杜邦线,确保了稳定性和灵活性。ESP8266-01S Wi-Fi模块将数据无缝传输至华为云物联网服务器,实现云端数据存储和分析,用户可通过QT上位机轻松设置喂养计划、查看记录和状态,提升了使用的便捷性和可扩展性。

总体而言,这一系统不仅提高了宠物喂养的效率和可靠性,还通过华为云的强大支持,实现了数据的远程监控和管理,为宠物主人提供了智能化、一体化的解决方案,未来可进一步扩展功能,如健康数据分析或移动端集成,以更好地满足现代养宠需求。