基于STM32+华为云IOT设计的智能垃圾桶

一、项目介绍

在商业街、小吃街和景区等人流密集的场所,垃圾桶的及时清理对于提供良好的游客体验至关重要。然而,传统的垃圾桶清理方式通常是定时或定期进行,无法根据实际情况进行及时响应,导致垃圾桶溢满,影响环境卫生,给游客带来不便和不满。

为了解决这一问题,本项目基于STM32F103ZET6主控芯片和华为云物联网平台,设计了一套智能垃圾桶管理系统。该系统通过NBIOT-BC26模块连接到华为云物联网平台,实现了垃圾桶数据的实时采集和上传。

在本地,垃圾桶通过多种传感器进行数据采集。使用DHT11模块实时监测环境温度和湿度,以了解垃圾桶所处环境的状态。采用中科微电子出品的GPS模块,通过串口输出GPS数据,实现垃圾桶的定位功能。垃圾桶口还配备了红外传感器,用于检测垃圾桶是否已满。

通过NBIOT-BC26模块,采集到的数据被实时上传到华为云物联网平台。在保洁人员管理中心,开发了一个数据大屏,采用Qt开发,运行在Windows系统下。数据大屏展示了该区域内垃圾桶的详细情况,包括环境温度、湿度、GPS定位和垃圾桶的满溢状态。

当垃圾桶满了时,上位机会实时发送短信通知保洁人员进行清理,并提供垃圾桶的位置信息,以便保洁人员快速响应并进行清理操作。

通过这套智能垃圾桶管理系统,垃圾桶的清理可以根据实际情况进行及时调度,提高了垃圾桶的使用效率,改善了环境卫生状况,提升了游客的体验感。同时,保洁人员能够更加高效地管理垃圾桶,提升工作效率,减少资源浪费。整个系统的设计在提供一个智能、高效的垃圾桶管理解决方案,为公共场所的环境卫生管理带来便利和改进。

image-20230728140353572

image-20230728140410093

image-20230728141958810

二、设计思路总结

2.1 硬件选型

【1】主控芯片:STM32F103ZET6

  • STM32F103系列是意法半导体(STMicroelectronics)推出的低功耗、高性能的32位ARM Cortex-M3微控制器系列。选择STM32F103ZET6作为主控芯片,是因为它具有较高的计算能力和丰富的外设接口,能够满足项目的需求。

【2】通信模块:NBIOT-BC26

  • NBIOT-BC26是一种窄带物联网(NB-IoT)通信模块,支持低功耗、广覆盖、远距离的物联网通信。它能够将垃圾桶采集到的数据通过NB-IoT网络上传到云平台,实现实时监测和远程管理。

【3】传感器模块:

  • DHT11模块:用于采集环境温度和湿度数据。DHT11是一种低成本、数字式温湿度传感器,具有简单的接口和良好的性能,适用于本项目的温湿度监测需求。
  • GPS模块:用于实现垃圾桶的定位功能。选择中科微电子出品的GPS模块,通过串口输出GPS数据,能够准确获取垃圾桶的位置信息。
  • 红外传感器:用于检测垃圾桶是否已满。红外传感器能够通过红外线的反射来判断垃圾桶内是否有垃圾,从而判断垃圾桶的满溢状态。

【5】数据大屏:采用Qt开发,运行在Windows系统下。数据大屏通过图形界面展示垃圾桶的详细情况,包括环境温度、湿度、GPS定位和垃圾桶的满溢状态。

项目的硬件选型包括主控芯片、通信模块、传感器模块、红外传感器和数据大屏。这些硬件组件相互配合,实现了智能垃圾桶管理系统的功能,包括数据采集、通信传输、定位功能和信息展示。

2.2 硬件设计

【1】主控芯片选择:
作为整个系统的核心,选择了STM32F103ZET6作为主控芯片。该芯片具有较高的计算能力和丰富的外设接口,能够满足项目的需求。主控芯片负责与各个硬件模块进行通信和数据处理,同时控制通信模块的数据传输。

【2】通信模块选择:
为了实现垃圾桶数据的实时采集和上传,选择了NBIOT-BC26通信模块。NBIOT-BC26支持窄带物联网通信,具有低功耗、广覆盖和远距离传输的特点,能够将采集到的数据通过NB-IoT网络上传到华为云物联网平台。

【3】传感器模块选择:

  • DHT11模块用于采集环境温度和湿度数据。DHT11是一种低成本、数字式温湿度传感器,通过数字信号输出温湿度数值,具有简单的接口和良好的性能,适用于本项目的温湿度监测需求。
  • GPS模块用于实现垃圾桶的定位功能。选择中科微电子出品的GPS模块,通过串口输出GPS数据,能够准确获取垃圾桶的位置信息。
  • 红外传感器设计:
    为了检测垃圾桶是否已满,采用红外传感器。红外传感器能够通过红外线的反射来判断垃圾桶内是否有垃圾,从而判断垃圾桶的满溢状态。红外传感器与主控芯片相连,通过数字输入口接收传感器的信号,并进行处理判断。

【4】数据大屏设计:
数据大屏采用Qt开发,运行在Windows系统下。通过图形界面展示垃圾桶的详细情况,包括环境温度、湿度、GPS定位和垃圾桶的满溢状态。主控芯片通过与数据大屏的通信接口实时传输数据,数据大屏根据接收到的数据进行展示。

整个硬件设计思路是将各个硬件模块与主控芯片相连接,通过主控芯片的控制和数据处理,实现数据的采集、通信传输、定位功能和信息展示。通过合理选择硬件组件,并进行适当的连接和接口设计,实现了智能垃圾桶管理系统的功能。

2.3 软件设计

【1】系统架构设计:
软件设计的第一步是确定系统的整体架构。根据项目需求,可以采用分层架构设计,将系统划分为应用层、业务逻辑层和驱动层。应用层负责与用户交互,业务逻辑层处理具体的业务逻辑,驱动层与硬件模块进行通信和控制。

【2】硬件驱动设计:
针对每个硬件模块,需要编写相应的驱动程序。主控芯片与通信模块、传感器模块和红外传感器进行通信,通过串口、I2C、SPI等接口与它们进行数据交互。每个硬件模块的驱动程序应包括初始化、数据采集和控制等功能。

【3】数据处理与逻辑控制:
主控芯片负责接收来自各个硬件模块的数据,并进行处理和逻辑控制。例如,从DHT11传感器读取温湿度数据后,可以进行数据的校验和转换,然后根据设定的阈值判断是否需要进行烘干操作。同时,主控芯片还负责控制红外传感器进行垃圾桶满溢状态的检测。

【4】通信与数据上传:
通过NBIOT-BC26通信模块,将采集到的数据通过NB-IoT网络上传到华为云物联网平台。主控芯片与通信模块进行通信,将需要上传的数据打包成相应的格式,并通过串口等接口发送给通信模块,实现数据的上传。

【5】用户界面设计:
软件还需要设计用户界面,以便用户可以直观地查看垃圾桶的状态和数据信息。可以使用Qt等工具进行界面设计,展示环境温度、湿度、GPS定位和垃圾桶的满溢状态等信息。用户界面与主控芯片进行通信,接收数据并进行展示。

整个软件设计思路是基于系统架构设计,通过硬件驱动、数据处理与逻辑控制、通信与数据上传以及用户界面设计等模块的开发,实现智能垃圾桶管理系统的功能。软件设计需要与硬件设计相结合,保证数据的采集、处理、传输和展示的协调运作。

2.4 系统交互流程

【1】用户打开智能垃圾桶管理系统的应用程序。

【2】系统初始化:

  • 系统进行硬件初始化,包括主控芯片、通信模块、传感器模块和红外传感器的初始化。
  • 确保通信模块连接到NB-IoT网络,并与华为云物联网平台建立通信连接。

【3】环境监测:

  • 系统开始监测环境温度和湿度,并获取垃圾桶的定位信息。主控芯片通过DHT11传感器获取温湿度数据,通过GPS模块获取垃圾桶的位置信息。

【4】数据处理与逻辑控制:

  • 主控芯片对采集到的数据进行处理和逻辑控制。可以根据温度和湿度数据判断垃圾桶是否需要进行清理,并通过红外传感器检测垃圾桶的满溢状态。

【5】数据上传:

  • 主控芯片将处理后的数据通过通信模块上传到华为云物联网平台。数据可以包括环境温度、湿度、GPS定位和垃圾桶的满溢状态等信息。
  • 上传的数据可以以JSON等格式进行打包,通过NB-IoT网络传输到华为云物联网平台。

【6】数据展示:

  • 用户界面接收从主控芯片传输过来的数据,并进行展示。用户可以在界面上查看环境温度、湿度、GPS定位和垃圾桶的满溢状态等信息。
  • 数据展示可以通过图表、文字、图像等形式进行呈现,以便用户直观地了解系统的状态和数据信息。

整个系统的交互流程涉及到硬件模块的数据采集、主控芯片的数据处理与逻辑控制、通信模块的数据上传以及用户界面的数据展示和用户交互。通过这些步骤,用户可以方便地监测和管理智能垃圾桶的状态和数据信息。

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

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

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

image-20221204193824815

3.1 物联网平台介绍

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

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

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

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

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

img

3.2 开通物联网服务

地址: https://www.huaweicloud.com/product/iothub.html

image-20221204194233414

进来默认会提示开通标准版,在2023的1月1号年之后没有基础版了。

image-20230313171311582

image-20230313171325183

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

image-20230321154943337

总结:

端口号:   MQTT (1883)| MQTTS (8883)   
接入地址: 7445c6bcd3.st1.iotda-app.cn-north-4.myhuaweicloud.com

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

Microsoft Windows [版本 10.0.19044.2728]
(c) Microsoft Corporation。保留所有权利。

C:\Users\11266>ping 7445c6bcd3.st1.iotda-device.cn-north-4.myhuaweicloud.com

正在 Ping 7445c6bcd3.st1.iotda-device.cn-north-4.myhuaweicloud.com [117.78.5.125] 具有 32 字节的数据:
来自 117.78.5.125 的回复: 字节=32 时间=42ms TTL=30
来自 117.78.5.125 的回复: 字节=32 时间=35ms TTL=30
来自 117.78.5.125 的回复: 字节=32 时间=36ms TTL=30
来自 117.78.5.125 的回复: 字节=32 时间=36ms TTL=30

117.78.5.125 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 35ms,最长 = 42ms,平均 = 37ms

C:\Users\11266>

image-20230321161044723

MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口比较合适。 接下来的ESP8266就采用1883端口连接华为云物联网平台。

3.3 创建产品

(1)创建产品

点击产品页,再点击左上角创建产品。

image-20230313171503547

(2)填写产品信息

根据自己产品名字填写。

image-20230728135430775

(3)产品创建成功

image-20230728135444283

(4)添加自定义模型

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

先点击自定义模型。

image-20230313171815860

再创建一个服务ID。

image-20230321155733514

接着点击新增属性。

image-20230321155756735

3.4 添加设备

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

(1)注册设备

image-20230313173158883

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

image-20230728135539727

(3)保存设备信息

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

image-20230728135557050

(4) 设备创建完成

image-20230728135620427

3.5 MQTT协议主题订阅与发布

(1)MQTT协议介绍

当前的设备是采用MQTT协议与华为云平台进行通信。

MQTT是一个物联网传输协议,它被设计用于轻量级的发布/订阅式消息传输,在为低带宽和不稳定的网络环境中的物联网设备提供可靠的网络服务。MQTT是专门针对物联网开发的轻量级传输协议。MQTT协议针对低带宽网络,低计算能力的设备,做了特殊的优化,使得其能适应各种物联网应用场景。目前MQTT拥有各种平台和设备上的客户端,已经形成了初步的生态系统。

MQTT是一种消息队列协议,使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合,相对于其他协议,开发更简单;MQTT协议是工作在TCP/IP协议上;由TCP/IP协议提供稳定的网络连接;所以,只要具备TCP协议栈的网络设备都可以使用MQTT协议。 本次设备采用的ESP8266就具备TCP协议栈,能够建立TCP连接,所以,配合STM32代码里封装的MQTT协议,就可以与华为云平台完成通信。

华为云的MQTT协议接入帮助文档在这里: https://support.huaweicloud.com/devg-iothub/iot_02_2200.html

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

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

image-20221207153310037

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

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

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

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

最终的格式:
$oc/devices/6419627e40773741f9fbdac7_dev1/sys/messages/down

(4)主题发布格式

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

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

帮助文档地址:https://support.huaweicloud.com/usermanual-iothub/iot_06_v5_3010.html

image-20221207153637391

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

发布的主题格式:
$oc/devices/{device_id}/sys/properties/report
 
最终的格式:
$oc/devices/6419627e40773741f9fbdac7_dev1/sys/properties/report
发布主题时,需要上传数据,这个数据格式是JSON格式。

上传的JSON数据格式如下:

{
  "services": [
    {
      "service_id": <填服务ID>,
      "properties": {
        "<填属性名称1>": <填属性值>,
        "<填属性名称2>": <填属性值>,
        ..........
      }
    }
  ]
}
根据JSON格式,一次可以上传多个属性字段。 这个JSON格式里的,服务ID,属性字段名称,属性值类型,在前面创建产品的时候就已经介绍了,不记得可以翻到前面去查看。

根据这个格式,组合一次上传的属性数据:
{"services": [{"service_id": "stm32","properties":{"DS18B20":18,"motor_water":1,"motor_oxygen":1,"temp_max":10,"water_hp":130,"motor_food":0,"time_food":0,"oxygen_food":3}}]}

3.6 MQTT三元组

MQTT协议登录需要填用户ID,设备ID,设备密码等信息,就像我们平时登录QQ,微信一样要输入账号密码才能登录。MQTT协议登录的这3个参数,一般称为MQTT三元组。

接下来介绍,华为云平台的MQTT三元组参数如何得到。

(1)MQTT服务器地址

要登录MQTT服务器,首先记得先知道服务器的地址是多少,端口是多少。

帮助文档地址:https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-portal/home

image-20230302135718882

MQTT协议的端口支持1883和8883,它们的区别是:8883 是加密端口更加安全。但是单片机上使用比较困难,所以当前的设备是采用1883端口进连接的。

根据上面的域名和端口号,得到下面的IP地址和端口号信息: 如果设备支持填写域名可以直接填域名,不支持就直接填写IP地址。 (IP地址就是域名解析得到的)

华为云的MQTT服务器地址:114.116.232.138
域名:7445c6bcd3.st1.iotda-device.cn-north-4.myhuaweicloud.com
华为云的MQTT端口号:1883

(2)生成MQTT三元组

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

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

下面是打开的页面:

image-20221207154917230

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

直接得到三元组信息。

image-20230321160708924

image-20230321160718302

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

ClientId 6419627e40773741f9fbdac7_dev1_0_0_2023032108
Username 6419627e40773741f9fbdac7_dev1
Password 861ac9e6a579d36888b2aaf97714be7af6c77017b017162884592bd68b086a6e

3.7 模拟设备登录测试

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

(1)填入登录信息

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

image-20230321161132971

(2)打开网页查看

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

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

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

四、上位机开发

为了方便查看设备上传的数据,对设备进行远程控制,接下来利用Qt开发一款Android和windows系统的上位机。

使用华为云平台提供的API接口获取设备上传的数据,也可以给设备下发指令,控制设备。

为了方便查看设备上传的数据,对设备进行远程控制,接下来利用Qt开发一款Android和windows系统的上位机。

使用华为云平台提供的API接口获取设备上传的数据,也可以给设备下发指令,控制设备。

4.1 Qt开发环境安装

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

image-20221207160606892

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

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

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

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

安装的时候,第一个复选框里勾选一个mingw 32编译器即可,其他的不管默认就行,直接点击下一步继续安装。

image-20221203151742653

说明: 我这里只是介绍PC端的环境搭建(这个比较简单)。 Android的开发环境比较麻烦,可以去我的博客里看详细文章。

选择MinGW 32-bit 编译器:

image-20221203151750344

4.2 创建IAM账户

创建一个IAM账户,因为接下来开发上位机,需要使用云平台的API接口,这些接口都需要token进行鉴权。简单来说,就是身份的认证。 调用接口获取Token时,就需要填写IAM账号信息。所以,接下来演示一下过程。

地址: https://console.huaweicloud.com/iam/?region=cn-north-4#/iam/users

获取Token时,除了AIM账号外,还需要项目凭证:

faa0973835ab409ab48182e2590f4ad3

image-20230321161348409

image-20230321161414487

鼠标点击自己昵称,点击统一身份认证。

image-20230321161433209

点击左上角创建用户

image-20230321161450557

image-20221207161209880

image-20221207161308917

image-20221207161327200

创建成功:

image-20221212174359962

image-20221212174412097

image-20230321161557848

4.3 获取影子数据

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

设备影子介绍:

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

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

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

如果对接口不熟悉,可以先进行在线调试:https://apiexplorer.developer.huaweicloud.com/apiexplorer/doc?product=IoTDA&api=ShowDeviceShadow

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

image-20230321161636567

image-20230321161701419

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

{
    
    
 "device_id": "6419627e40773741f9fbdac7_dev1",
 "shadow": [
  {
    
    
   "service_id": "stm32",
   "desired": {
    
    
    "properties": null,
    "event_time": null
   },
   "reported": {
    
    
    "properties": {
    
    
     "DS18B20": 18,
     "motor_water": 1,
     "motor_oxygen": 1,
     "temp_max": 10,
     "water_hp": 130,
     "motor_food": 0,
     "time_food": 0,
     "oxygen_food": 3
    },
    "event_time": "20230321T081126Z"
   },
   "version": 0
  }
 ]
}

4.4 修改设备属性

地址: https://support.huaweicloud.com/api-iothub/iot_06_v5_0034.html

接口说明

设备的产品模型中定义了物联网平台可向设备下发的属性,应用服务器可调用此接口向指定设备下发属性。平台负责将属性以同步方式发送给设备,并将设备执行属性结果同步返回。

修改设备属性的接口,可以让服务器给设备下发指令,如果需要控制设备。

在线调试地址:

https://apiexplorer.developer.huaweicloud.com/apiexplorer/doc?product=IoTDA&api=UpdateProperties

修改设备属性是属于同步命令,需要设备在线才可以进行调试,先使用MQTT客户端登录服务器,模拟设备上线。

然后进行调试,测试数据远程下发给设备。

【1】利用MQTT客户端先登录设备 (这是同步命令,必须在线才能调试)

image-20230321161923007

【2】点击调试

image-20230321161905033

{
    
    "services":{
    
    "temp_max":100}}

【4】可以看到,MQTT客户端软件上已经收到了服务器下发的消息

image-20230313175819901

由于是同步命令,服务器必须要收到设备的响应才能顺利完成一个流程,设备响应了服务器才能确定数据下发成功。

image-20230321161941584

MQTT设备端如何响应呢?

设备响应格式说明:https://support.huaweicloud.com/api-iothub/iot_06_v5_3008.html

image-20221203163532648

下面进行实操:

当服务器通过在线调试,发送指令下来之后,客户端将请求ID复制下来,添加到发布主题的格式里,再回复回去,服务器收到了响应,一次属性修改就完美完成了。

image-20230321162053263

就是成功的状态:

image-20230321162026282

**下面是请求的总结: ** (响应服务器的修改设备属性请求)

上报主题的格式:$oc/devices/{
    
    device_id}/sys/properties/set/response/request_id=

$oc/devices/6419627e40773741f9fbdac7_dev1/sys/properties/set/response/request_id=

响应的数据:
{
    
    "result_code": 0,"result_desc": "success"}

4.5 设计上位机

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

【1】新建Qt工程

image-20230302144331541

选择工程路径,放在英文路径下。

image-20230321162532573

image-20230313180428670

image-20230313180451177

image-20230313180504518

image-20230321162620141

创建完毕。

新建Android的模板:

image-20230321162657975

image-20230321162731010

image-20230321162741791

image-20230321162812486

【2】界面设计

image-20230728140933190

【4】代码设计:配置参数读取与保存

/*
功能: 保存数据到文件
*/
void Widget::SaveDataToFile(QString text)
{
    
    
    /*保存数据到文件,方便下次加载*/
    QString file;
    file=QCoreApplication::applicationDirPath()+"/"+ConfigFile;
    QFile filesrc(file);
    filesrc.open(QIODevice::WriteOnly);
    QDataStream out(&filesrc);
    out << text;  //序列化写字符串
    filesrc.flush();
    filesrc.close();
}


/*
功能: 从文件读取数据
*/
QString Widget::ReadDataFile(void)
{
    
    
    //读取配置文件
    QString text,data;
    text=QCoreApplication::applicationDirPath()+"/"+ConfigFile;

    //判断文件是否存在
    if(QFile::exists(text))
    {
    
    
        QFile filenew(text);
        filenew.open(QIODevice::ReadOnly);
        QDataStream in(&filenew); // 从文件读取序列化数据
        in >> data; //提取写入的数据
        filenew.close();
    }
    return data; //返回值读取的值
}

【3】代码设计:云端数据解析

//解析反馈结果
void Widget::replyFinished(QNetworkReply *reply)
{
    
    
    QString displayInfo;

    int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();

    //读取所有数据
    QByteArray replyData = reply->readAll();

    qDebug()<<"状态码:"<<statusCode;
    qDebug()<<"反馈的数据:"<<QString(replyData);

    //更新token
    if(function_select==3)
    {
    
    
        displayInfo="token 更新失败.";
        //读取HTTP响应头的数据
        QList<QNetworkReply::RawHeaderPair> RawHeader=reply->rawHeaderPairs();
        qDebug()<<"HTTP响应头数量:"<<RawHeader.size();
        for(int i=0;i<RawHeader.size();i++)
        {
    
    
            QString first=RawHeader.at(i).first;
            QString second=RawHeader.at(i).second;
            if(first=="X-Subject-Token")
            {
    
    
                Token=second.toUtf8();
                displayInfo="token 更新成功.";

                //保存到文件
                SaveDataToFile(Token);
                break;
            }
        }
        QMessageBox::information(this,"提示",displayInfo,QMessageBox::Ok,QMessageBox::Ok);
        return;
    }

    //判断状态码
    if(200 != statusCode)
    {
    
    
        //解析数据
        QJsonParseError json_error;
        QJsonDocument document = QJsonDocument::fromJson(replyData, &json_error);
        if(json_error.error == QJsonParseError::NoError)
        {
    
    
            //判断是否是对象,然后开始解析数据
            if(document.isObject())
            {
    
    
                QString error_str="";
                QJsonObject obj = document.object();
                QString error_code;
                //解析错误代码
                if(obj.contains("error_code"))
                {
    
    
                    error_code=obj.take("error_code").toString();
                    error_str+="错误代码:";
                    error_str+=error_code;
                    error_str+="\n";
                }
                if(obj.contains("error_msg"))
                {
    
    
                    error_str+="错误消息:";
                    error_str+=obj.take("error_msg").toString();
                    error_str+="\n";
                }

                //显示错误代码
                QMessageBox::information(this,"提示",error_str,QMessageBox::Ok,QMessageBox::Ok);
            }
         }
        return;
    }

    //设置属性
    if(function_select==12 || function_select==13)
    {
    
    
        //解析数据
        QJsonParseError json_error;
        QJsonDocument document = QJsonDocument::fromJson(replyData, &json_error);
        if(json_error.error == QJsonParseError::NoError)
        {
    
    
            //判断是否是对象,然后开始解析数据
            if(document.isObject())
            {
    
    
                QJsonObject obj = document.object();
                if(obj.contains("response"))
                {
    
    
                    QJsonObject obj1=obj.take("response").toObject();
                    int val=0;
                    QString success;
                    if(obj1.contains("result_code"))
                    {
    
    
                         val=obj1.take("result_code").toInt();
                    }
                    if(obj1.contains("result_desc"))
                    {
    
    
                         success=obj1.take("result_desc").toString();
                    }

                    if(val==0 && success =="success")
                    {
    
    
                        //显示状态
                        QMessageBox::information(this,"提示","远程命令操作完成.",QMessageBox::Ok,QMessageBox::Ok);
                        return;
                    }
                    else
                    {
    
    
                        //显示状态
                        QMessageBox::information(this,"提示","设备未正确回应.请检查设备网络.",QMessageBox::Ok,QMessageBox::Ok);
                        return;
                    }
                }
            }
         }
    }

    //查询设备属性
    if(function_select==0)
    {
    
    
        //解析数据
        QJsonParseError json_error;
        QJsonDocument document = QJsonDocument::fromJson(replyData, &json_error);
        if(json_error.error == QJsonParseError::NoError)
        {
    
    
            //判断是否是对象,然后开始解析数据
            if(document.isObject())
            {
    
    
                QJsonObject obj = document.object();
                if(obj.contains("shadow"))
                {
    
    
                    QJsonArray array=obj.take("shadow").toArray();
                    for(int i=0;i<array.size();i++)
                    {
    
    
                        QJsonObject obj2=array.at(i).toObject();
                        if(obj2.contains("reported"))
                        {
    
    
                            QJsonObject obj3=obj2.take("reported").toObject();


                            if(obj3.contains("properties"))
                            {
    
    
                                QJsonObject properties=obj3.take("properties").toObject();

                                qDebug()<<"开始解析数据....";
                            }
                        }
                    }
                }
            }
         }
        return;
    }
}

五、代码实现

5.1 BC26连接云平台实现代码

下面是使用STM32F103ZET6和BC26连接华为云物联网平台实现MQTT设备登录、主题订阅和主题发布的实现代码:

#include "stdio.h"
#include "string.h"
#include "stdlib.h"

// 定义华为云物联网平台的服务器地址、端口号、设备ID和设备密码
#define MQTT_SERVER "mqtt://xxxxxx.iotplatform.com"  // 请替换为实际的服务器地址
#define MQTT_PORT 1883  // 请根据实际情况修改端口号
#define DEVICE_ID "your_device_id"  // 请替换为实际的设备ID
#define DEVICE_PASSWORD "your_device_password"  // 请替换为实际的设备密码

// 定义MQTT相关的参数
#define MQTT_CLIENT_ID "your_client_id"  // 请替换为实际的客户端ID
#define MQTT_TOPIC "your_topic"  // 请替换为实际的主题

// 定义MQTT消息接收回调函数
void mqtt_message_received(char *topic, char *payload) {
    
    
    printf("Received message on topic: %s\n", topic);
    printf("Payload: %s\n", payload);
}

// 建立MQTT连接
void mqtt_connect() {
    
    
    // 连接到华为云物联网平台的MQTT服务器
    // 这里使用的是MQTT的QoS 1级别
    // 请根据实际情况修改QoS级别和其他参数
    char command[256];
    sprintf(command, "AT+QMTCFG=\"aliauth\",0,%d,\"%s\",\"%s\"", MQTT_PORT, DEVICE_ID, DEVICE_PASSWORD);
    printf("Sending command: %s\n", command);
    // 发送AT指令连接到MQTT服务器
    // ...

    // 订阅主题
    sprintf(command, "AT+QMTSUB=0,1,\"%s\",1", MQTT_TOPIC);
    printf("Sending command: %s\n", command);
    // 发送AT指令订阅主题
    // ...
}

// 发布MQTT消息
void mqtt_publish(char *payload) {
    
    
    // 发布消息到指定的主题
    char command[256];
    sprintf(command, "AT+QMTPUB=0,0,0,0,\"%s\"", MQTT_TOPIC);
    printf("Sending command: %s\n", command);
    // 发送AT指令设置发布的主题
    // ...

    sprintf(command, "AT+QMTPUB=0,1,0,0,%d", strlen(payload));
    printf("Sending command: %s\n", command);
    // 发送AT指令设置消息的长度
    // ...

    printf("Sending payload: %s\n", payload);
    // 发送消息的内容
    // ...

    // 等待MQTT服务器返回发布结果
    // ...
}

int main() {
    
    
    // 初始化串口和其他硬件模块
    // ...

    // 连接到华为云物联网平台的MQTT服务器
    mqtt_connect();

    // 进入主循环
    while (1) {
    
    
        // 处理其他任务
        // ...

        // 检查是否有需要发布的消息
        // 如果有,调用mqtt_publish函数发布消息
        // ...

        // 检查是否有接收到的MQTT消息
        // 如果有,调用mqtt_message_received函数处理消息
        // ...
    }

    return 0;
}

5.2 BC26模块的MQTT协议指令

BC26模块是一款支持NB-IoT通信技术的物联网模块,可以通过AT指令与外部设备进行通信和控制。

下面是BC26模块与MQTT协议相关的一些常用AT指令及其功能:

【1】AT+QMTOPEN:打开MQTT客户端连接。

  • 功能:通过该指令连接到MQTT服务器。
  • 参数:服务器地址、端口号、用户名和密码等。
  • 示例:AT+QMTOPEN=0,“mqtt://xxxxxx.iotplatform.com”,1883

【2】AT+QMTCLOSE:关闭MQTT客户端连接。

  • 功能:通过该指令关闭与MQTT服务器的连接。
  • 参数:无。
  • 示例:AT+QMTCLOSE=0

【3】AT+QMTCONN:建立MQTT连接。

  • 功能:通过该指令建立与MQTT服务器的连接。
  • 参数:客户端ID、用户名、密码等。
  • 示例:AT+QMTCONN=0,“your_client_id”,“your_username”,“your_password”

【4】AT+QMTDISC:断开MQTT连接。

  • 功能:通过该指令断开与MQTT服务器的连接。
  • 参数:无。
  • 示例:AT+QMTDISC=0

【5】AT+QMTSUB:订阅MQTT主题。

  • 功能:通过该指令订阅指定的MQTT主题。
  • 参数:主题、QoS级别等。
  • 示例:AT+QMTSUB=0,1,“your_topic”,1

【6】AT+QMTUNS:取消订阅MQTT主题。

  • 功能:通过该指令取消订阅指定的MQTT主题。
  • 参数:主题。
  • 示例:AT+QMTUNS=0,1,“your_topic”

【7】AT+QMTPUB:发布MQTT消息。

  • 功能:通过该指令发布消息到指定的MQTT主题。
  • 参数:主题、消息内容、QoS级别等。
  • 示例:AT+QMTPUB=0,0,0,0,“your_topic”

【8】AT+QMTRECV:接收MQTT消息。

  • 功能:通过该指令接收从MQTT服务器接收到的消息。
  • 参数:无。
  • 示例:AT+QMTRECV=0

这些是BC26模块中与MQTT协议相关的一些常用AT指令。

5.3 读取DHT11传感器的温湿度数据

以下是使用STM32F103ZET6读取DHT11传感器的温湿度数据的实现代码:

#include "stm32f10x.h"
#include "dht11.h"

int main(void)
{
    
    
    // 初始化DHT11传感器
    DHT11_Init();

    while (1)
    {
    
    
        // 读取DHT11传感器的温湿度数据
        DHT11_Result result = DHT11_Read();

        if (result.status == DHT11_OK)
        {
    
    
            // 温度数据
            uint8_t temperature = result.temperature;
            // 湿度数据
            uint8_t humidity = result.humidity;

            // 在这里进行温湿度数据的处理和使用
            // ...

            // 延时一段时间后再次读取
            DelayMs(2000);
        }
        else
        {
    
    
            // 读取失败,可以进行相应的错误处理
            // ...
        }
    }
}

在主函数中,通过循环不断读取DHT11传感器的温湿度数据。如果读取成功,可以从result结构体中获取温度和湿度数据,并进行相应的处理。如果读取失败,可以根据需要进行错误处理。

5.4 DHT11.c和DHT11.h代码

dht11.h:

#ifndef DHT11_H
#define DHT11_H

#include "stm32f10x.h"

typedef struct
{
    
    
    uint8_t status;      // 读取状态,0表示成功,其他表示失败
    uint8_t humidity;    // 湿度值
    uint8_t temperature; // 温度值
} DHT11_Result;

void DHT11_Init(void);
DHT11_Result DHT11_Read(void);

#endif

dht11.c:

#include "dht11.h"

#define DHT11_PORT GPIOA
#define DHT11_PIN GPIO_Pin_0

static void DHT11_Delay(uint32_t us)
{
    
    
    uint32_t count = us * 8;
    while (count--)
    {
    
    
        __NOP();
    }
}

static void DHT11_SetOutput(void)
{
    
    
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = DHT11_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DHT11_PORT, &GPIO_InitStructure);
}

static void DHT11_SetInput(void)
{
    
    
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = DHT11_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(DHT11_PORT, &GPIO_InitStructure);
}

static uint8_t DHT11_ReadByte(void)
{
    
    
    uint8_t byte = 0;
    for (uint8_t i = 0; i < 8; i++)
    {
    
    
        while (!GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN))
        {
    
    
            // 等待低电平结束
        }
        DHT11_Delay(30);
        if (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN))
        {
    
    
            byte |= (1 << (7 - i));
        }
        while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN))
        {
    
    
            // 等待高电平结束
        }
    }
    return byte;
}

void DHT11_Init(void)
{
    
    
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    GPIO_InitStructure.GPIO_Pin = DHT11_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DHT11_PORT, &GPIO_InitStructure);

    GPIO_SetBits(DHT11_PORT, DHT11_PIN);
}

DHT11_Result DHT11_Read(void)
{
    
    
    DHT11_Result result;
    result.status = 1;

    DHT11_SetOutput();
    GPIO_ResetBits(DHT11_PORT, DHT11_PIN);
    DHT11_Delay(18000);
    GPIO_SetBits(DHT11_PORT, DHT11_PIN);
    DHT11_Delay(20);
    DHT11_SetInput();

    if (!GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN))
    {
    
    
        while (!GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN))
        {
    
    
            // 等待低电平结束
        }
        while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN))
        {
    
    
            // 等待高电平结束
        }

        uint8_t data[5];
        for (uint8_t i = 0; i < 5; i++)
        {
    
    
            data[i] = DHT11_ReadByte();
        }

        uint8_t sum = data[0] + data[1] + data[2] + data[3];
        if (sum == data[4])
        {
    
    
            result.status = 0;
            result.humidity = data[0];
            result.temperature = data[2];
        }
    }

    return result;
}

dht11.h文件定义了DHT11传感器的初始化函数DHT11_Init()和读取函数DHT11_Read(),以及DHT11_Result结构体用于存储读取结果。

dht11.c文件实现了DHT11传感器的初始化和读取函数。在初始化函数中,配置了DHT11引脚的GPIO模式和速度。在读取函数中,通过发送开始信号和接收数据的方式读取DHT11传感器的温湿度数据,并进行校验。

5.5 GPS数据解析

在STM32F103ZET6上通过串口2读取GPS模块返回的定位数据并解析经纬度和定位状态。

#include "stm32f10x.h"
#include <stdio.h>
#include <string.h>

// 定义串口2接收缓冲区大小
#define RX_BUFFER_SIZE 256

// 定义GPS数据解析状态
typedef enum {
    
    
    GPS_STATE_IDLE,        // 空闲状态
    GPS_STATE_RECEIVING,   // 接收中状态
    GPS_STATE_COMPLETE     // 接收完成状态
} GPS_State;

// 定义接收缓冲区和接收状态变量
char rxBuffer[RX_BUFFER_SIZE];
volatile uint16_t rxIndex = 0;
volatile GPS_State gpsState = GPS_STATE_IDLE;

// 处理接收到的GPS数据
void processGPSData() {
    
    
    // 在这里进行GPS数据解析和处理
    // 解析经纬度和定位状态等信息
    // 根据需要进行相应的操作或显示
    // 例如,打印经纬度和定位状态
    printf("Latitude: %s\n", latitude);
    printf("Longitude: %s\n", longitude);
    printf("Position Fix Status: %s\n", positionStatus);
}

// 串口2接收中断处理函数
void USART2_IRQHandler(void) {
    
    
    if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) {
    
    
        char data = USART_ReceiveData(USART2);
        
        // 接收到回车换行符表示一条完整的GPS数据
        if (data == '\n') {
    
    
            rxBuffer[rxIndex] = '\0';
            rxIndex = 0;
            gpsState = GPS_STATE_COMPLETE;
        } else {
    
    
            // 将接收到的数据存储到缓冲区中
            rxBuffer[rxIndex] = data;
            rxIndex++;
            
            // 接收缓冲区溢出时进行处理
            if (rxIndex >= RX_BUFFER_SIZE) {
    
    
                rxIndex = 0;
                gpsState = GPS_STATE_IDLE;
            }
        }
    }
}

int main(void) {
    
    
    // 初始化串口2和GPIO引脚
    // 设置串口2的波特率、数据位、停止位等参数
    
    // 使能串口2接收中断
    USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
    NVIC_EnableIRQ(USART2_IRQn);
    
    while (1) {
    
    
        // 如果接收到完整的GPS数据
        if (gpsState == GPS_STATE_COMPLETE) {
    
    
            // 处理接收到的GPS数据
            processGPSData();
            
            // 处理完成后,重置接收状态为IDLE
            gpsState = GPS_STATE_IDLE;
        }
    }
}

代码中的串口初始化和中断处理部分是基于标准库的使用方式。

猜你喜欢

转载自blog.csdn.net/xiaolong1126626497/article/details/132991011