基于STM32的康养旅游生态农业互动系统

一、前言

1.1 项目开发背景

随着社会经济发展和人民生活水平提高,健康养生与休闲体验相结合的"康养旅游"模式日益受到青睐。游客不再满足于走马观花式的观光,而是渴望深度参与、学习知识并获得身心放松的沉浸式体验。生态农业因其绿色、健康、可持续的特质,成为康养旅游的理想载体。然而,传统的生态农场往往缺乏直观展示其生态价值和技术内涵的有效手段,游客参与度低,对现代化农业技术的认知有限,难以满足当下游客对互动性、科技感和信息透明度的需求。

同时,现代农业生产对精细化管理的要求不断提高。水肥资源的精准利用、环境因子的实时调控是保障作物健康生长、提升农产品品质和实现资源节约的关键。传统农业依赖人工经验判断和操作,存在效率低、响应慢、资源浪费等问题,亟需智能化手段进行升级。

物联网、嵌入式系统和云计算技术的快速发展为上述问题的解决提供了可能。将传感器网络、智能控制、移动互联网和多媒体交互技术应用于生态农业场景,可以构建一个集环境监测、智能调控、互动体验与数据管理于一体的综合平台。

本项目正是在此背景下提出的。旨在利用STM32微控制器作为核心处理单元,整合多种环境传感器(温湿度、光照、土壤湿度)、执行机构(喷灌、补光)、RFID身份识别、语音播报、WiFi通信及网络摄像头,构建一个服务于康养旅游场景的智能生态农业互动系统。该系统不仅能够实时采集并展示农田环境数据,实现灌溉和补光的自动化智能控制,降低运营成本,提高资源利用效率;更通过刷卡互动播种/浇水、多语种语音导览、远程监控与APP数据查看等功能,显著提升游客的参与感、趣味性和对生态农业科技的认知度,将农场转化为一个寓教于乐、充满科技魅力的康养旅游目的地。最终,所有数据汇聚至华为云平台,为农场的长周期精细化管理、作物生长模型优化及旅游服务改进提供坚实的数据支撑。

1.2 设计实现的功能

(1)通过信息化方式增强游客对生态农业的参与感与认知

(2)实时采集和展示农业环境数据

(3)游客扫码或刷卡即可操作,系统自动控制设备运行

(4)APP和后台系统支持远程监控与数据记录管理

1.3 项目硬件模块组成

(1)主控芯片:STM32F103RCT6

(2)温湿度传感器:DHT11

(3)光照传感器:BH1750

(4)土壤湿度传感器:电阻式模拟传感器

(5)刷卡模块:MFRC522 RFID模块

(6)继电器模块:用于控制补光灯与水泵

(7)音频模块:DFPlayer mini + 扬声器模块

(8)通信模块:ESP8266-WIFI模块

(9)监控摄像头:RTMP网络摄像头

(10)显示终端:5寸液晶触摸屏或LED屏

(11)电源模块:AC-DC 12V转换 + AMS1117稳压模块

1.4 设计意义

该系统的设计意义在于通过信息化技术提升生态农业旅游的互动体验与管理效率。游客能够实时查看农田环境数据并参与互动操作,如刷卡触发播种或浇水,增强对生态农业的认知与参与感。同时,系统自动调控喷灌和补光设备,优化作物生长环境,降低人工干预成本,提高资源利用效率。

远程监控与数据上传功能使管理人员可通过APP或后台实时掌握农田状态,长期记录的生长周期数据为农业研究提供科学依据。多语种语音播报系统则提升了国际化游客的体验包容性,使农业知识传递更直观易懂。

硬件整合上,STM32主控协调传感器、执行器与通信模块,构建了稳定可靠的数据采集与控制链路,为智慧农业在旅游场景的落地提供了可复用的技术框架。整套系统通过"体验+管理"的双向优化,推动生态农业与康养旅游的深度融合。

1.5 设计思路

本项目设计思路以STM32F103RCT6为主控芯片,整合多种硬件模块实现生态农业互动系统功能,确保实际需求高效落地。系统架构采用分层设计,STM32作为核心处理器,负责协调传感器数据采集、设备控制、通信交互和用户界面管理,通过实时多任务调度确保各功能模块并行运行,提升系统响应速度和可靠性。

环境数据采集与展示是系统基础,通过DHT11温湿度传感器、BH1750光照传感器和电阻式土壤湿度传感器实时监测农田参数,传感器数据经STM32的ADC和I2C接口读取并处理。采集的数据通过5寸液晶触摸屏或LED屏本地显示,同时利用ESP8266-WIFI模块上传至华为云平台,后台系统记录生长周期数据并支持APP远程访问,实现游客实时查看温湿度、光照和土壤湿度信息,增强农业认知体验。

智能控制功能基于预设阈值实现自动化操作,STM32分析传感器数据后,通过继电器模块驱动水泵执行喷灌或补光灯进行光照调节。例如,当土壤湿度低于阈值时自动启动喷灌系统,光照不足时触发补光,控制逻辑在本地执行以减少网络延迟,确保农田环境稳定,同时系统支持手动模式通过APP或大屏进行干预。

游客互动机制设计为简单易用的刷卡操作,MFRC522 RFID模块用于识别游客卡片,当游客刷卡时,STM32解析指令并触发相应动作,如通过继电器控制水泵模拟浇水或播种设备运行。互动过程在本地处理,避免依赖云服务,确保响应实时性,同时系统记录操作日志上传至华为云,便于后台管理游客参与数据。

监控与远程通信集成RTMP网络摄像头,摄像头独立运行并通过WiFi直接上传视频流至云平台,STM32通过ESP8266模块仅负责传输控制指令和元数据。所有环境数据及操作日志通过MQTT协议实时上传至华为云,后台系统进行存储和分析,支持管理员通过APP远程查看农田情况和历史记录,实现高效监控。

语音播报系统提升游客体验,DFPlayer mini音频模块连接扬声器,存储多语种音频文件。STM32根据系统事件(如刷卡成功或环境报警)触发语音播放,支持中文、英语等语言切换,播放逻辑基于本地SD卡存储,无需网络依赖,确保在农田现场快速响应,增强互动趣味性和可访问性。

1.6 框架图

以下是使用Mermaid语法优化后的系统框架图,采用分层结构和模块化设计:

lua 复制代码
Syntax error in graphmermaid version 8.8.3
ERROR: [Mermaid] Parse error on line 0:
...] direction TB A1[传感器系统]
----------------------^
Expecting 'SEMI', 'NEWLINE', 'EOF', 'AMP', 'START_LINK', 'LINK', got 'ALPHA'

优化特点:

  1. 分层结构清晰

    • 垂直排列5个主要层级
    • 每层内部采用水平布局
    • 使用不同颜色区分功能域
  2. 模块细节完善

    • 保留所有硬件型号和协议
    • 关键组件添加换行显示参数
    • 显示完整数据流向
  3. 交互路径明确

    • 用实线箭头表示数据流
    • 标注关键通信协议
    • 形成控制闭环
  4. 可视化增强

    • 模块采用圆角矩形
    • 重要接口特殊标注
    • 自动适应布局

建议将代码复制到Mermaid在线编辑器(如mermaid.live)中查看动态效果,可通过调整direction参数改变子图布局方向。如需增加更多技术细节或调整布局结构,可以进一步修改代码。

1.7 系统总体设计

本系统以STM32F103RCT6为主控制器,构建了一套集环境监测、智能控制、互动体验与远程管理于一体的生态农业平台。系统通过传感器网络实时采集农田环境数据:DHT11监测空气温湿度,BH1750检测光照强度,电阻式土壤湿度传感器获取土壤墒情。采集数据通过ESP8266 WiFi模块上传至华为云平台,同时本地显示于5寸液晶触摸屏或LED大屏,供游客直观查看。

智能控制部分通过继电器模块驱动喷灌水泵和补光灯。系统支持阈值自动控制(如土壤湿度低于设定值时自动喷灌)和游客互动模式。游客使用MFRC522 RFID模块刷卡后,可触发播种或浇水操作,STM32解析指令并控制相应设备执行。操作过程通过DFPlayer mini音频模块进行多语种语音播报反馈,增强互动体验。

远程监控由RTMP网络摄像头实现,实时视频流上传至云端,游客可通过APP远程查看农田实况。所有传感器数据及设备操作记录在华为云后台存储分析,形成作物生长周期数据库,支持历史数据查询与管理。

硬件供电采用AC-DC 12V转换模块,配合AMS1117稳压芯片提供3.3V/5V稳定电压。各模块通过UART、I2C、GPIO等接口与主控芯片协同工作,实现数据采集、设备控制、云端通信和人机交互的有机整合。

1.8 系统功能总结

功能类别 功能描述
数据监控 游客通过大屏或APP实时查看生态农田的温湿度、光照、土壤湿度数据。
智能控制 实现喷灌系统和补光系统的智能控制,自动或手动调节环境参数。
互动体验 游客通过刷卡触发播种或浇水操作,提升对农业的参与感和认知。
远程监控 设有监控摄像头,支持远程查看农田实时情况。
数据管理 环境数据实时上传至华为云,后台记录生长周期数据,支持APP和系统远程管理。
语音播报 支持多语种语音播报系统,增强游客体验。

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

环境数据采集模块负责实时监测农田环境参数,使用DHT11温湿度传感器获取空气温湿度,BH1750光照传感器采集光照强度,电阻式土壤湿度传感器检测土壤含水量。所有传感器数据通过STM32的ADC和I/O接口进行采集处理,为系统提供基础环境状态信息。

自动控制执行模块通过继电器组实现设备智能调控。当土壤湿度低于阈值时自动启动水泵喷灌,光照不足时触发补光灯系统。同时支持游客刷卡互动指令,通过STM32解析MFRC522 RFID模块信号后驱动相应继电器,执行播种或浇水操作。

游客互动认证模块基于MFRC522 RFID技术实现刷卡识别。游客使用授权卡片触发互动指令后,系统立即启动预设的播种/浇水流程,并通过语音模块给予操作反馈。互动数据同步记录至云端,确保操作可追溯。

多语种语音播报模块采用DFPlayer mini音频处理器配合扬声器,预存多语言音频文件。在设备状态变化、互动操作成功或系统警报时,自动根据设定语种播放对应语音提示,增强游客体验。

数据显示与远程监控模块通过5寸液晶触摸屏/LED屏本地展示实时环境数据,同时集成RTMP网络摄像头实现农田画面远程查看。游客可通过大屏或手机APP同步获取温湿度、光照等数据及监控画面。

云端通信模块通过ESP8266-WIFI连接华为云平台,定时上传传感器数据及设备状态。后台系统记录作物生长周期数据,支持历史数据查询与分析,管理人员可通过云端远程调整控制阈值或查看设备运行日志。

电源管理模块采用AC-DC 12V转换器供电,配合AMS1117稳压芯片输出5V/3.3V双路电压。为STM32主控、传感器组、继电器等模块提供稳定电力,确保系统在农田环境下持续运行。

主控核心模块以STM32F103RCT6为处理中枢,协调各硬件单元工作。负责传感器数据解析、设备控制逻辑判断、RFID指令处理、语音播放调度、屏幕显示刷新及云端通信协议转换,实现全系统功能集成与实时响应。

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

华为云官网: 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客户端软件模拟真实的设备来登录平台。测试与服务器通信是否正常。

MQTT软件下载地址【免费】: download.csdn.net/download/xi...

(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

三、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的中文官网: www.qt.io/zh-cn/

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

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

qt-opensource-windows-x86-5.12.6.exe 13-Nov-2019 07:28 3.7G Details

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

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

安装的时候,第一个复选框里勾选一个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文件,添加内容如下:

arduino 复制代码
#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();
}

代码解析:

arduino 复制代码
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 致命错误提示

示例代码:

scss 复制代码
qDebug("调试信息输出");
qWarning("一般警告信息输出");
qCritical("严重错误输出");
qFatal("致命错误输出");

qDebug输出的信息会打印到QT软件下边的输出面板。

在上节的HelloQt工程上加上调试输出代码,增加的main.cpp代码如下:

arduino 复制代码
#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的中文官网: www.qt.io/zh-cn/

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

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

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

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

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

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

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】开始设计界面

根据自己需求设计界面。

4.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

4.5 编译Windows上位机

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

4.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文件。

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

4.7 设备仿真调试

通过MQTT客户端模拟设备登录华为云服务器。进行设备联调,实现数据上传和下发测试。

五、代码设计

5.1 程序下载

也有视频教程:

讲解如何编译代码,下载STM32程序: www.bilibili.com/video/BV1Cw...

打STM32的keil工程,编译代码、然后,使用USB线将开发板的左边的USB口(串口1)与电脑的USB连接,打开程序下载软件下载程序。

具体下载过程看下面图:

打开程序下载软件:[软件就在资料包里的软件工具目录下]

5.2 程序正常运行效果

设备运行过程中会通过串口打印调试信息,我们可以通过串口打印了解程序是否正常。

程序下载之后,可以打开串口调试助手查看程序运行的状态信息。[软件就在资料包里的软件工具目录下]

5.3 取模软件的使用

显示屏上会显示中文,字母,数字等数据,可以使用下面的取模软件进行取模设置。

软件就在资料包里的软件工具目录下

打开软件之后:

5.4 上位机代码设计

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <chrono>
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <opencv2/opencv.hpp>

using json = nlohmann::json;
using namespace std;
using namespace cv;

// 全局配置
const string CLOUD_API = "https://huawei-cloud-api.com/iot-data";
const string CAMERA_RTMP = "rtmp://camera-stream.com/live";
const string API_KEY = "your_huawei_cloud_api_key";

// 传感器数据结构
struct SensorData {
    float temperature;
    float humidity;
    float light_intensity;
    float soil_moisture;
    string timestamp;
};

// 设备控制指令
enum DeviceCommand { IRRIGATION_ON, IRRIGATION_OFF, LIGHT_ON, LIGHT_OFF };

class CloudManager {
public:
    void uploadData(const SensorData& data) {
        json payload = {
            {"api_key", API_KEY},
            {"sensors", {
                {"temp", data.temperature},
                {"humidity", data.humidity},
                {"light", data.light_intensity},
                {"soil", data.soil_moisture}
            }},
            {"timestamp", data.timestamp}
        };

        CURL* curl = curl_easy_init();
        if (curl) {
            curl_easy_setopt(curl, CURLOPT_URL, CLOUD_API.c_str());
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.dump().c_str());
            
            struct curl_slist* headers = nullptr;
            headers = curl_slist_append(headers, "Content-Type: application/json");
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
            
            CURLcode res = curl_easy_perform(curl);
            if (res != CURLE_OK) {
                cerr << "Cloud upload failed: " << curl_easy_strerror(res) << endl;
            }
            curl_easy_cleanup(curl);
        }
    }

    void sendControlCommand(DeviceCommand cmd) {
        string command_str;
        switch (cmd) {
            case IRRIGATION_ON: command_str = "IRRIGATION_START"; break;
            case IRRIGATION_OFF: command_str = "IRRIGATION_STOP"; break;
            case LIGHT_ON: command_str = "LIGHT_ENABLE"; break;
            case LIGHT_OFF: command_str = "LIGHT_DISABLE"; break;
        }

        json payload = {{"api_key", API_KEY}, {"command", command_str}};
        
        CURL* curl = curl_easy_init();
        if (curl) {
            curl_easy_setopt(curl, CURLOPT_URL, (CLOUD_API + "/control").c_str());
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.dump().c_str());
            
            struct curl_slist* headers = nullptr;
            headers = curl_slist_append(headers, "Content-Type: application/json");
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
            
            curl_easy_perform(curl);
            curl_easy_cleanup(curl);
        }
    }
};

class CameraStream {
public:
    void startStream() {
        thread([this] {
            VideoCapture cap(CAMERA_RTMP);
            if (!cap.isOpened()) {
                cerr << "Error opening camera stream" << endl;
                return;
            }

            Mat frame;
            while (true) {
                cap >> frame;
                if (frame.empty()) break;
                
                // 实时显示(实际项目需用GUI框架渲染)
                imshow("Farm Monitoring", frame);
                if (waitKey(1) == 27) break; // ESC退出
            }
        }).detach();
    }
};

class RFIDManager {
public:
    void processCardEvent(const string& card_id) {
        cout << "RFID card scanned: " << card_id << endl;
        
        // 触发互动设备(播种/浇水)
        if (card_id.substr(0, 2) == "PL") {
            cout << "Starting planting sequence..." << endl;
            CloudManager().sendControlCommand(IRRIGATION_ON);
        } else if (card_id.substr(0, 2) == "WT") {
            cout << "Starting watering sequence..." << endl;
            CloudManager().sendControlCommand(LIGHT_ON);
        }
    }
};

class VoiceSystem {
public:
    void playMessage(const string& message, const string& lang = "en") {
        string tts_url = "https://tts-service.com/synthesize?lang=" + lang;
        json payload = {{"text", message}};

        CURL* curl = curl_easy_init();
        if (curl) {
            curl_easy_setopt(curl, CURLOPT_URL, tts_url.c_str());
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.dump().c_str());
            
            struct curl_slist* headers = nullptr;
            headers = curl_slist_append(headers, "Content-Type: application/json");
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
            
            // 音频处理伪代码
            // 1. 获取音频响应
            // 2. 通过扬声器播放
            cout << "Playing: [" << lang << "] " << message << endl;
            curl_easy_cleanup(curl);
        }
    }
};

int main() {
    // 初始化模块
    CloudManager cloud;
    CameraStream camera;
    RFIDManager rfid;
    VoiceSystem voice;

    camera.startStream();
    voice.playMessage("System initialized", "en");

    // 模拟传感器数据采集
    thread sensorThread([&cloud] {
        while (true) {
            SensorData data{
                25.5f + rand() % 5,   // 温度
                60.0f + rand() % 20,  // 湿度
                800.0f + rand() % 500, // 光照
                45.0f + rand() % 30,  // 土壤湿度
                to_string(time(nullptr)) // 时间戳
            };
            cloud.uploadData(data);
            this_thread::sleep_for(chrono::seconds(10));
        }
    });

    // 模拟RFID事件
    thread rfidThread([&rfid] {
        vector<string> cards = {"PL-001", "WT-002", "PL-003"};
        while (true) {
            rfid.processCardEvent(cards[rand() % cards.size()]);
            this_thread::sleep_for(chrono::seconds(15));
        }
    });

    // 主控制循环
    while (true) {
        // 此处添加用户界面交互逻辑
        // 例如:接收APP控制指令、自动控制策略等
        this_thread::sleep_for(chrono::seconds(1));
    }

    sensorThread.join();
    rfidThread.join();
    return 0;
}

代码说明

  1. 云服务通信 (CloudManager)

    • 使用libcurl实现HTTP POST请求
    • 上传传感器数据到华为云API
    • 发送设备控制命令(喷灌/补光)
  2. 视频监控 (CameraStream)

    • 基于OpenCV捕获RTMP视频流
    • 独立线程实时显示监控画面
  3. RFID交互 (RFIDManager)

    • 处理刷卡事件并触发相应操作
    • 不同卡号对应播种(PL)或浇水(WT)指令
  4. 语音系统 (VoiceSystem)

    • 支持多语种TTS服务集成
    • 通过HTTP请求生成语音播报
  5. 主程序逻辑

    • 独立线程处理传感器数据上传
    • RFID事件处理线程
    • 摄像头监控线程
    • 预留UI交互接口

关键依赖库

  • libcurl: HTTP通信
  • OpenCV: 视频流处理
  • nlohmann/json: JSON数据解析
  • C++11线程库: 多线程管理

5.5 STM32模块代码设计

bash 复制代码
当前项目使用的相关软件工具、模块源码已经上传到网盘:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink    
cpp 复制代码
#include "stm32f10x.h"

// 引脚定义
#define DHT11_PIN    GPIO_Pin_9   // PG9
#define DHT11_PORT   GPIOG
#define SOIL_ADC_CH ADC_Channel_0 // PA0
#define LIGHT_RELAY GPIO_Pin_10   // PG10
#define PUMP_RELAY  GPIO_Pin_11   // PG11
#define RFID_SS     GPIO_Pin_4    // PA4

// 函数声明
void SystemInit(void);
void GPIO_Config(void);
void USART_Config(void);
void SPI_Config(void);
void ADC_Config(void);
void TIM_Config(void);
uint8_t DHT11_Read(float *temp, float *hum);
uint16_t BH1750_Read(void);
uint16_t SoilMoisture_Read(void);
void RFID_Init(void);
uint8_t RFID_CheckCard(void);
void DFPlayer_Play(uint8_t track);
void ESP8266_SendData(char *data);

// 主函数
int main(void) {
    SystemInit();
    GPIO_Config();
    USART_Config();
    SPI_Config();
    ADC_Config();
    TIM_Config();
    RFID_Init();
    
    float temperature, humidity;
    uint16_t light, soil_moisture;
    
    while(1) {
        // 读取传感器数据
        DHT11_Read(&temperature, &humidity);
        light = BH1750_Read();
        soil_moisture = SoilMoisture_Read();
        
        // 控制逻辑
        if (light < 50) {
            GPIOG->ODR |= LIGHT_RELAY;  // 打开补光灯
        } else {
            GPIOG->ODR &= ~LIGHT_RELAY; // 关闭补光灯
        }
        
        if (RFID_CheckCard()) {
            GPIOG->ODR |= PUMP_RELAY;   // 刷卡触发浇水
            TIM2->CR1 |= TIM_CR1_CEN;   // 启动定时器(5秒后关闭)
            DFPlayer_Play(1);           // 播放浇水提示音
        }
        
        // 发送数据到云平台
        char buffer[128];
        sprintf(buffer, "Temp:%.1f,Humi:%.1f,Light:%d,Soil:%d", 
                temperature, humidity, light, soil_moisture);
        ESP8266_SendData(buffer);
    }
}

// 系统时钟初始化
void SystemInit(void) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPGEN | 
                   RCC_APB2ENR_AFIOEN | RCC_APB2ENR_ADC1EN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN | RCC_APB1ENR_SPI2EN;
    FLASH->ACR |= FLASH_ACR_LATENCY_2;
    RCC->CFGR |= RCC_CFGR_PLLMULL9;
    RCC->CR |= RCC_CR_PLLON;
    while(!(RCC->CR & RCC_CR_PLLRDY));
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}

// GPIO初始化
void GPIO_Config(void) {
    // DHT11 (PG9输入)
    GPIOG->CRH &= ~(0x0F << 4);
    GPIOG->CRH |= (0x04 << 4);  // 浮空输入
    
    // 继电器控制 (PG10/PG11输出)
    GPIOG->CRH &= ~(0xFF << 8);
    GPIOG->CRH |= (0x03 << 8) | (0x03 << 12); // 推挽输出
    
    // RFID片选 (PA4输出)
    GPIOA->CRL &= ~(0x0F << 16);
    GPIOA->CRL |= (0x03 << 16); // 推挽输出
    GPIOA->BSRR = RFID_SS;      // 初始高电平
}

// USART初始化 (USART2: ESP8266, USART3: DFPlayer)
void USART_Config(void) {
    // ESP8266 (USART2 PA2-TX, PA3-RX)
    GPIOA->CRL &= ~(0xFF << 8);
    GPIOA->CRL |= (0x0B << 8) | (0x04 << 12); // TX推挽复用, RX浮空输入
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
    USART2->BRR = 0x1D4C;       // 115200@72MHz
    USART2->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
    
    // DFPlayer (USART3 PB10-TX, PB11-RX)
    GPIOB->CRH &= ~(0xFF << 8);
    GPIOB->CRH |= (0x0B << 8) | (0x04 << 12);
    RCC->APB1ENR |= RCC_APB1ENR_USART3EN;
    USART3->BRR = 0x1D4C;       // 115200
    USART3->CR1 = USART_CR1_UE | USART_CR1_TE;
}

// SPI初始化 (RFID模块)
void SPI_Config(void) {
    // PA5-SCK, PA6-MISO, PA7-MOSI
    GPIOA->CRL &= ~(0xFFF << 20);
    GPIOA->CRL |= (0x0B << 20) | (0x04 << 24) | (0x0B << 28);
    RCC->APB1ENR |= RCC_APB1ENR_SPI1EN;
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_BR_1 | SPI_CR1_SSM | SPI_CR1_SSI;
    SPI1->CR1 |= SPI_CR1_SPE;
}

// ADC初始化 (土壤湿度)
void ADC_Config(void) {
    // PA0 模拟输入
    GPIOA->CRL &= ~(0x0F << 0);
    ADC1->SMPR2 = ADC_SMPR2_SMP0; // 1.5周期采样
    ADC1->SQR3 = SOIL_ADC_CH;      // 通道0
    ADC1->CR2 = ADC_CR2_ADON;
}

// 定时器配置 (继电器自动关闭)
void TIM_Config(void) {
    TIM2->PSC = 7200 - 1;        // 10KHz
    TIM2->ARR = 50000 - 1;       // 5秒定时
    TIM2->DIER = TIM_DIER_UIE;   // 更新中断
    NVIC_EnableIRQ(TIM2_IRQn);
}

// DHT11读取温湿度
uint8_t DHT11_Read(float *temp, float *hum) {
    uint8_t data[5] = {0};
    // ... (完整时序代码)
    *hum = data[0];
    *temp = data[2];
    return 1;
}

// BH1750光照强度读取
uint16_t BH1750_Read(void) {
    // I2C通信代码
    uint8_t buf[2];
    // ... (I2C读取流程)
    return (buf[0] << 8) | buf[1];
}

// 土壤湿度读取
uint16_t SoilMoisture_Read(void) {
    ADC1->CR2 |= ADC_CR2_ADON;
    while(!(ADC1->SR & ADC_SR_EOC));
    return ADC1->DR;
}

// RFID初始化
void RFID_Init(void) {
    // MFRC522初始化命令序列
    uint8_t init_cmds[] = {0x0F, 0x0D, 0x28, 0x00};
    for(int i=0; i<4; i++) {
        GPIOA->BRR = RFID_SS;  // 片选低
        SPI1->DR = init_cmds[i];
        while(!(SPI1->SR & SPI_SR_RXNE));
        GPIOA->BSRR = RFID_SS; // 片选高
    }
}

// RFID卡片检测
uint8_t RFID_CheckCard(void) {
    GPIOA->BRR = RFID_SS;
    SPI1->DR = 0x26;  // 检测命令
    while(!(SPI1->SR & SPI_SR_RXNE));
    uint8_t status = SPI1->DR;
    GPIOA->BSRR = RFID_SS;
    return (status == 0x40);
}

// DFPlayer播放语音
void DFPlayer_Play(uint8_t track) {
    uint8_t cmd[] = {0x7E, 0xFF, 0x06, 0x03, 0x00, track, 0x00, 0xEF};
    for(int i=0; i<8; i++) {
        while(!(USART3->SR & USART_SR_TXE));
        USART3->DR = cmd[i];
    }
}

// ESP8266发送数据
void ESP8266_SendData(char *data) {
    while(*data) {
        while(!(USART2->SR & USART_SR_TXE));
        USART2->DR = *data++;
    }
}

// 定时器中断处理 (关闭继电器)
void TIM2_IRQHandler(void) {
    if(TIM2->SR & TIM_SR_UIF) {
        TIM2->SR &= ~TIM_SR_UIF;
        GPIOG->BRR = PUMP_RELAY;  // 关闭水泵
        TIM2->CR1 &= ~TIM_CR1_CEN; // 停止定时器
    }
}

5.6 STM32项目核心代码

scss 复制代码
#include "stm32f10x.h"
#include "dht11.h"
#include "bh1750.h"
#include "mfrc522.h"
#include "esp8266.h"
#include "relay.h"
#include "dfplayer.h"
#include "lcd.h"
#include "adc.h"

// 传感器阈值定义
#define SOIL_MOISTURE_THRESHOLD 30    // 土壤湿度阈值(%)
#define LIGHT_INTENSITY_THRESHOLD 50  // 光照强度阈值(lux)
#define AUTO_CONTROL_INTERVAL 5000    // 自动控制间隔(ms)

// 全局变量声明
volatile uint32_t sysTick = 0;
uint8_t temperature = 0;
uint8_t humidity = 0;
uint16_t light = 0;
uint16_t soilMoisture = 0;
uint8_t rfidBuffer[16];

// 系统时钟初始化
void SysClock_Init(void) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN;
    SystemInit();
    SysTick_Config(SystemCoreClock / 1000);  // 1ms中断
}

// SysTick中断处理
void SysTick_Handler(void) {
    sysTick++;
}

// 延时函数
void Delay_ms(uint32_t ms) {
    uint32_t start = sysTick;
    while ((sysTick - start) < ms);
}

// 传感器数据采集
void Sensor_ReadAll(void) {
    DHT11_ReadData(&temperature, &humidity);
    light = BH1750_ReadLight();
    soilMoisture = ADC_ReadSoilMoisture();
}

// 自动控制系统
void Auto_Control(void) {
    // 土壤湿度低时开启水泵
    if (soilMoisture < SOIL_MOISTURE_THRESHOLD) {
        RELAY_WaterPump(ON);
        LCD_DisplayStatus("Auto Watering");
    } else {
        RELAY_WaterPump(OFF);
    }
    
    // 光照不足时开启补光灯
    if (light < LIGHT_INTENSITY_THRESHOLD) {
        RELAY_GrowLight(ON);
        LCD_DisplayStatus("Light ON");
    } else {
        RELAY_GrowLight(OFF);
    }
}

// RFID处理函数
void RFID_Process(void) {
    if (MFRC522_CheckCard(rfidBuffer) == 1) {
        // 卡类型判断 (示例: 卡号前两位判断)
        if (rfidBuffer[0] == 0x01) {  // 播种卡
            RELAY_Seeder(ON);
            Delay_ms(1000);
            RELAY_Seeder(OFF);
            DFPlayer_Play(1);  // 播放播种提示音
            LCD_DisplayStatus("Seeding");
        } 
        else if (rfidBuffer[0] == 0x02) {  // 浇水卡
            RELAY_WaterPump(ON);
            Delay_ms(3000);
            RELAY_WaterPump(OFF);
            DFPlayer_Play(2);  // 播放浇水提示音
            LCD_DisplayStatus("Manual Watering");
        }
    }
}

// 华为云数据上传
void Cloud_Upload(void) {
    char jsonBuf[128];
    sprintf(jsonBuf, "{"temp":%d,"humi":%d,"light":%d,"soil":%d}",
            temperature, humidity, light, soilMoisture);
    ESP8266_SendData(jsonBuf);
}

int main(void) {
    // 硬件初始化
    SysClock_Init();
    LCD_Init();
    DHT11_Init();
    BH1750_Init();
    ADC_Init();
    MFRC522_Init();
    ESP8266_Init();
    RELAY_Init();
    DFPlayer_Init();
    
    LCD_DisplayWelcome();  // 显示欢迎界面
    
    uint32_t lastControlTime = 0;
    uint32_t lastUploadTime = 0;
    
    while (1) {
        // 传感器数据采集
        Sensor_ReadAll();
        
        // LCD显示更新
        LCD_DisplayData(temperature, humidity, light, soilMoisture);
        
        // 自动控制逻辑(5秒执行一次)
        if ((sysTick - lastControlTime) >= AUTO_CONTROL_INTERVAL) {
            Auto_Control();
            lastControlTime = sysTick;
        }
        
        // 华为云数据上传(10秒上传一次)
        if ((sysTick - lastUploadTime) >= 10000) {
            Cloud_Upload();
            lastUploadTime = sysTick;
        }
        
        // RFID刷卡检测
        RFID_Process();
        
        // 系统延时
        Delay_ms(100);
    }
}

代码说明:

  1. 硬件初始化

    • 系统时钟配置
    • 外设初始化(LCD、传感器、RFID、WiFi模块等)
  2. 主循环功能

    • 实时采集环境数据(温湿度/光照/土壤湿度)
    • LCD显示最新数据
    • 每5秒执行自动控制逻辑(喷灌/补光)
    • 每10秒上传数据到华为云
    • 持续检测RFID刷卡事件
  3. 关键功能实现

    • 自动控制:根据阈值自动启停水泵和补光灯
    • RFID互动:识别不同类型卡片触发播种/浇水
    • 语音反馈:DFPlayer播放操作提示音
    • 数据上传:通过ESP8266发送JSON格式数据到云端
  4. 模块化设计

    • 传感器驱动封装在独立文件
    • 外设控制函数模块化
    • 状态信息实时显示在LCD

六、总结

本项目成功构建了一个集农业监测、游客互动与云端管理于一体的智能康养旅游系统。通过STM32主控芯片协调温湿度、光照及土壤传感器实时采集环境数据,结合RFID刷卡互动与继电器控制设备,实现了农田喷灌、补光的自动化管理,为游客提供了沉浸式的生态农业参与体验。

系统依托ESP8266模块将数据同步至华为云平台,确保生长周期信息的完整记录与分析,同时通过RTMP摄像头与多语种语音播报模块,强化了远程监控的便捷性与国际化服务能力。APP及大屏终端的应用进一步提升了数据可视化与远程操控的灵活性,有效达成了生态农业科普与智慧旅游体验的双重目标。

相关推荐
姑苏洛言4 小时前
编写产品需求文档:黄历日历小程序
前端·javascript·后端
姑苏洛言5 小时前
搭建一款结合传统黄历功能的日历小程序
前端·javascript·后端
你的人类朋友5 小时前
🍃认识一下boomi
后端
苏三说技术5 小时前
MySQL的三大日志
后端
豌豆花下猫5 小时前
让 Python 代码飙升330倍:从入门到精通的四种性能优化实践
后端·python·ai
南雨北斗6 小时前
TP6使用PHPMailer发送邮件
后端
你的人类朋友6 小时前
🤔什么时候用BFF架构?
前端·javascript·后端
争不过朝夕,又念着往昔7 小时前
Go语言反射机制详解
开发语言·后端·golang
绝无仅有9 小时前
企微审批对接错误与解决方案
后端·算法·架构
Super Rookie9 小时前
Spring Boot 企业项目技术选型
java·spring boot·后端