基于STM32设计的农作物生长管理系统【玩转华为云】

举报
DS小龙哥 发表于 2024/10/31 21:36:51 2024/10/31
【摘要】 本项目致力于构建一套基于STM32F103RCT6微控制器和NBIOT-BC26通信模块的农作物生长管理系统。系统的核心功能在于实时监测农田环境中的关键参数,例如土壤含水量、环境光照强度、温度与湿度,并据此实现自动化管理。具体而言,系统能够依据预设的阈值自动控制补水,同时允许用户通过本地按钮或远程控制的方式进行手动补水。此外,系统还能根据环境亮度的变化自动调节补光灯的亮度,以保证植物获得适宜的光照


一、前言

1.1 项目介绍

【1】项目开发背景

随着现代农业技术的发展,智能农业逐渐成为提高农作物产量和品质的关键手段之一。传统的农业生产方式依赖于人工经验,这种方式不仅效率低下,而且难以应对气候变化带来的挑战。特别是在水资源管理和光照控制方面,传统方法往往无法提供精确的控制,这直接影响到了农作物的生长周期和最终产量。

在此背景下,基于现代物联网技术的农作物生长管理系统应运而生。这类系统能够通过各种传感器实时监测农田环境,如土壤湿度、光照强度、空气温度和湿度等,并根据这些数据自动调整灌溉、照明等设施的工作状态。这种智能化的管理不仅大大减轻了农民的劳动强度,而且通过优化资源利用,提高了农作物的生产效率。

本项目提出了一种基于STM32微控制器和NBIOT通信技术的农作物生长管理系统设计方案。通过将先进的微处理器技术与窄带物联网(NBIOT)结合,本系统能够在无人干预的情况下,实现对农田环境的精确控制。系统还具备远程监控和控制功能,用户可以通过手机应用程序随时查看农田的实时数据,并根据需要调整设备的工作模式。

随着科技的进步和人们对食品安全重视程度的提高,智能农业管理系统的需求日益增长。本项目的开发不仅有助于推动农业向更加科学化、精准化的方向发展,同时也为实现可持续农业生产和保障粮食安全提供了技术支持。通过本系统的应用,可以预见未来的农业生产将更加高效、环保,同时也为农民带来了更大的经济效益和社会效益。

image-20240904170032732

华为云IoT平台是华为提供的一站式物联网(Internet of Things, IoT)服务解决方案,帮助企业和开发者快速构建安全可靠的物联网应用。该平台基于云计算技术,集成了设备接入、数据处理、业务逻辑定制、数据分析等核心功能,支持大规模设备的连接与管理,以及高效的数据交换与处理。

在设备接入方面,华为云IoT平台提供了丰富的协议支持,包括MQTT、CoAP、HTTP等标准协议,同时也支持自定义协议,确保不同类型的设备能够轻松接入云端。通过安全认证机制,如设备证书、密钥等方式,保障了设备与平台之间的通信安全。

对于数据处理,华为云IoT平台具备强大的规则引擎,可以对收集到的设备数据进行实时处理和分析,根据预设的条件触发相应的动作,比如将处理后的数据转发至其他系统或调用API接口实现业务联动。此外,平台还支持数据存储,方便用户对历史数据进行查询和分析。

在业务逻辑定制上,华为云IoT平台允许用户根据实际需求灵活配置业务流程,比如设置告警阈值、定义设备间的互动模式等,极大地提高了应用开发的灵活性和效率。同时,平台也提供了丰富的API接口,便于第三方系统的集成和扩展。

安全性是华为云IoT平台设计的重要考量之一。除了前面提到的设备接入安全措施外,平台还采用了多重防护策略,如数据加密传输、访问控制、安全审计等,确保整个物联网系统的稳定运行和用户数据的安全。

华为云IoT平台还注重用户体验,提供了直观易用的管理界面,让非专业技术人员也能轻松完成设备管理和监控工作。无论是小规模的试验项目还是大规模的商业部署,华为云IoT平台都能提供全面的支持和服务,助力客户加速物联网应用的落地实施。

【2】设计实现的功能

(1)实时检测土壤含水量,并根据预设的阈值自动判断是否需要补水,进而实现自动化的灌溉控制。

(2)除了自动补水功能外,系统还提供了本地按钮控制补水选项,同时支持通过远程控制手段进行手动补水操作,增加了补水控制的灵活性。

(3)系统支持植物补光灯亮度的PWM自动调节,可以根据环境亮度的变化动态调整灯光亮度,以确保植物获得最佳光照条件。

(4)环境温度与湿度检测功能也是系统的重要组成部分,当检测到空气温度或湿度超出设定的安全范围时,系统会触发相应的警报机制。

(5)本地OLED屏幕显示当前环境的各项参数,如温度、湿度、土壤含水量以及光线照射强度,方便用户随时查看环境状态。

(6)为了及时响应异常情况,系统配备了蜂鸣器报警功能。一旦发现温度、湿度或土壤含水量不符合预设的健康范围,系统会通过蜂鸣器发出声音报警。这些阈值可以通过手机应用程序进行设定。

(7)为了避免由于温度突变引起的误操作,系统采用了适当的控制方法,在设定温度发生大变动时延迟系统的反应调节时间,确保系统的稳定性和准确性。

(8)通过NBIOT-BC26模块,整个设备可以连接至华为云IoT物联网平台,并采用MQTT协议上传数据。基于此,设计了Android手机APP,以便用户能够远程查看设备上传的数据,并实现远程控制设备的功能,如设置设备的温度阈值等。

(9)采用Qt框架开发Android手机应用程序



【3】项目硬件模块组成

(1)主控芯片:采用STM32F103RCT6作为核心处理器,负责处理系统中所有的逻辑运算和控制任务。

(2)环境光照强度检测模块:使用BH1750传感器来测量环境光照强度,为补光灯的亮度调节提供数据支持。

(3)环境温湿度检测模块:采用DHT11传感器,用于监测环境中的温度和湿度,确保农作物处于适宜的生长环境中。

(4)OLED显示屏模块:选用0.96寸的SPI协议OLED显示屏,用于显示当前环境参数,如温度、湿度、土壤含水量及光线强度等信息。

(5)补光灯模块:使用白色LED灯作为植物补光灯,通过PWM技术调节亮度,以适应不同环境下的光照需求。

(6)声音报警模块:采用蜂鸣器作为报警装置,当环境参数超出预设阈值时,通过蜂鸣器发出声音警报。

(7)土壤湿度检测模块:使用带有ADC模拟量接口的土壤湿度检测传感器,用于实时监测土壤含水量,确保适时补水。

(8)网络通信模块:采用NBIOT-BC26模块,实现设备与华为云IoT物联网平台之间的数据传输,通过MQTT协议上传数据。

(9)补水控制模块:使用继电器驱动5V抽水电机,根据土壤湿度传感器的反馈信息自动或手动控制补水过程。

(10)供电模块:采用USB线5V供电方式,为整个系统提供稳定的电力供应,简化了设备的安装和使用流程。


【4】需求总结

项目名字: 基于STM32+NBIOT设计的农作物生长管理系统

实现功能:

1. 实时检测土壤含水量,根据设置的阀值判断实现自动补水
2. 本地可用按钮控制补水、远程手动控制补水
3. 支持植物补光灯亮度PWM自动调节,可根据环境亮度调节灯光亮度
4. 支持检测环境温度、湿度,当空气温度、湿度超过设定值范围时会报警。
5. 本地0LED屏幕显示当前环境温度、湿度、土壤含水量、光线照射强度等参数。
6. 支持蜂鸣器报警提醒,当温度、湿度、土壤含水量阀值不符合设定值时通过蜂鸣器发出声音报警(阀值可通过手机APP设定)
7.采用合适的控制方法,当设定温度发生大突变时,为了防止误设置,延迟系统的反应调节时间。
8. 整个设备会通过NBIOT-BC26连接华为云IOT物联网平台,通过MQTT协议上传数据到物联网云平台,再设计Android手机APP实现远程显示设备上传的数据,同时可以远程控制设备,设置设备温度阀值等等。
9. 采用Qt(C++)设计Android手机APP,实现数据远程监测显示和远程控制。

硬件选型:

1. 主控芯片选择STM32F103RCT6
2. 环境光照强度检测采用BH1750
3. 环境温湿度检测采用DHT11
4. OLED显示屏采用0.96寸SPI协议显示屏
5. 补光灯采用白色LED灯
6. 声音报警采用蜂鸣器
7. 土壤湿度检测采用ADC模拟量接口的土壤湿度检测传感器
8. 联网采用NBIOT-BC26模块
9. 植物补水采用继电器驱动5V抽水电机抽水进行补水。
10.供电电源:采用USB线-5V供电。


1.2 设计思路

本项目的设计思路围绕着实现一个高度自动化、智能化的农作物生长管理系统展开,通过现代电子技术与物联网技术的融合,为农作物提供最佳的生长环境。首先,系统的核心是基于STM32F103RCT6微控制器,这是因为该芯片具备高性能、低功耗的特点,非常适合用于需要实时处理大量数据的应用场景。通过集成多种传感器,系统能够实时采集环境数据,如土壤湿度、光照强度、温度和湿度等,为后续的决策和控制提供依据。

在环境监测方面,系统选用了BH1750光照强度传感器和DHT11温湿度传感器。BH1750能够精确测量光照强度,从而支持植物补光灯的PWM自动调节功能;而DHT11则用于监测空气中的温度和湿度,当这些参数超出预设的安全范围时,系统会通过蜂鸣器报警,提醒用户采取措施。系统配置了土壤湿度检测传感器,它可以实时检测土壤的含水量,并根据设定的阈值自动控制补水,确保植物获得充足的水分。

为了使用户能够直观地了解环境状态,系统配备了一块0.96寸的OLED显示屏,通过SPI协议与主控芯片相连,实时显示各项环境参数。这样,即使在没有智能手机的情况下,用户也能通过显示屏掌握当前的环境状况。

在联网功能上,系统采用了NBIOT-BC26模块,通过窄带物联网技术实现设备与华为云IoT物联网平台的连接。借助MQTT协议,系统能够将采集到的数据上传至云端,用户可以通过Android手机APP实时查看数据,并远程控制设备。为了进一步增强用户体验,项目还计划使用Qt框架开发Android版的应用程序,以便于用户更方便地进行远程监控和管理。

在控制策略上,系统特别考虑到了温度突变可能引发的误操作问题。为此,设计了适当的延时控制机制,即当设定温度发生较大变化时,系统不会立即作出反应,而是经过一段时间的延迟后再进行调节,从而避免了因温度波动导致的误动作。

在供电方面,系统选择了简单且可靠的USB线5V供电方式,不仅便于安装和维护,也确保了系统的稳定性。通过这一系列的设计思路,本项目打造一个高效、可靠且易于使用的农作物生长管理系统,助力现代农业的智能化转型。


1.3 系统功能总结

功能类别 描述
实时土壤含水量检测与自动补水 系统能够实时检测土壤含水量,并根据预设阈值自动判断是否需要启动补水机制。
本地与远程补水控制 用户可以通过本地按钮手动控制补水,也可以通过远程手段(如手机APP)进行手动补水控制。
补光灯亮度自动调节 根据环境亮度变化,系统能够自动调节补光灯的亮度,以满足植物生长所需的光照条件。
环境温湿度检测与报警 通过传感器检测环境温度和湿度,当检测到的数值超出设定范围时,系统会触发报警机制。
环境参数本地显示 OLED屏幕实时显示当前环境的温度、湿度、土壤含水量及光线照射强度等参数。
蜂鸣器声音报警 当温度、湿度或土壤含水量不符合预设的健康范围时,系统通过蜂鸣器发出声音报警。
温度突变延迟控制 针对温度突变的情况,系统采用了延迟反应机制,以防止因温度快速变化而导致的误操作。
数据上传与远程监控 通过NBIOT-BC26模块将数据上传至华为云IoT平台,并通过MQTT协议传输数据,支持远程监控。
远程控制与设置 用户可以通过设计的Android手机APP实现远程控制设备,包括查看数据、控制设备和设置温度阈值等功能。


1.4 开发工具的选择

【1】设备端开发

STM32的编程语言选择C语言,C语言执行效率高,大学里主学的C语言,C语言编译出来的可执行文件最接近于机器码,汇编语言执行效率最高,但是汇编的移植性比较差,目前在一些操作系统内核里还有一些低配的单片机使用的较多,平常的单片机编程还是以C语言为主。C语言的执行效率仅次于汇编,语法理解简单、代码通用性强,也支持跨平台,在嵌入式底层、单片机编程里用的非常多,当前的设计就是采用C语言开发。

开发工具选择Keil,keil是一家世界领先的嵌入式微控制器软件开发商,在2015年,keil被ARM公司收购。因为当前芯片选择的是STM32F103系列,STMF103是属于ARM公司的芯片构架、Cortex-M3内核系列的芯片,所以使用Kile来开发STM32是有先天优势的,而keil在各大高校使用的也非常多,很多教科书里都是以keil来教学,开发51单片机、STM32单片机等等。目前作为MCU芯片开发的软件也不只是keil一家独大,IAR在MCU微处理器开发领域里也使用的非常多,IAR扩展性更强,也支持STM32开发,也支持其他芯片,比如:CC2530,51单片机的开发。从软件的使用上来讲,IAR比keil更加简洁,功能相对少一些。如果之前使用过keil,而且使用频率较多,已经习惯再使用IAR是有点不适应界面的。

image-20221210225339928


【2】上位机开发

上位机的开发选择Qt框架,编程语言采用C++;Qt是一个1991年由Qt Company开发的跨平台C++图形用户界面应用程序开发框架。它既可以开发GUI程序,也可用于开发非GUI程序,比如控制台工具和服务器。Qt是面向对象的框架,使用特殊的代码生成扩展(称为元对象编译器(Meta Object Compiler, moc))以及一些宏,Qt很容易扩展,并且允许真正地组件编程。Qt能轻松创建具有原生C++性能的连接设备、用户界面(UI)和应用程序。它功能强大且结构紧凑,拥有直观的工具和库。

image-20230218001243591

image-20230218001219105






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

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

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

image-20221204193824815


2.1 物联网平台介绍

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

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

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

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

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

img



2.2 开通物联网服务

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

image-20221204194233414


点击立即创建

image-20240117134653452


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

image-20240117134729401


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

image-20240425180759670



在上面也能看到 免费单元的限制。

image-20240425180817704



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

image-20240425180845461


总结:

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

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

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

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

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

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

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

C:\Users\11266>

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


2.3 创建产品

(1)创建产品

image-20230109164412041


(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协议接入帮助文档在这里: 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/663cb18871d845632a0912e7_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/663cb18871d845632a0912e7_dev1/sys/properties/report
发布主题时,需要上传数据,这个数据格式是JSON格式。

上传的JSON数据格式如下:

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

根据这个格式,组合一次上传的属性数据:
{"services": [{"service_id": "stm32","properties":{"DHT11_T":30,"DHT11_H":10,"BH1750":1,"MQ135":0}}]}


2.6 MQTT三元组

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

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


(1)MQTT服务器地址

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

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

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鉴权三元组: https://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":{"DHT11_T":30,"DHT11_H":10,"BH1750":1,"MQ135":0}}]}


2.8 创建IAM账户

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

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


【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 获取影子数据

帮助文档: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-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





三、上位机开发

为了方便查看设备上传的数据,接下来利用Qt开发一款Android手机APP 和 Windows上位机。

使用华为云平台提供的API接口获取设备上传的数据,进行可视化显示,以及远程控制设备。

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

或者去网盘里下载:https://pan.quark.cn/s/145a9b3f7f53


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

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

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

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

image-20221203151742653


选择MinGW 32-bit 编译器: (一定要看清楚了)

image-20221203151750344


说明: 我这里只是介绍PC端,也就是Windows系统下的Qt环境搭建。 Android的开发环境比较麻烦,如果想学习Android开发,想编译Android程序的APP,需要自己去搭建Android环境。


也可以看下面这篇文章,不过这个文章是在Qt开发专栏里付费的,需要订阅专栏才可以看。 如果不想付费看,也可以自行找其他教程,自己搭建好必须的环境就行了

Android环境搭建的博客链接: https://blog.csdn.net/xiaolong1126626497/article/details/117254453


3.2 新建上位机工程

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


【1】新建工程

image-20240117144052547


【2】设置项目的名称。

image-20240509195711965



【3】选择编译系统

image-20240117144239681


【4】选择默认继承的类

image-20240117144302275


【5】选择编译器

image-20240314162137170



【6】点击完成

image-20240117144354252


【7】工程创建完成

image-20230421094133333



3.3 设计UI界面与工程配置

【1】打开UI文件

image-20230421094815236


打开默认的界面如下:

image-20240425194845233



【2】开始设计界面

根据自己需求设计界面。

image-20240904160948734



3.5 编译Windows上位机

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

image-20240509202031739

编译之后的效果:

image-20240903163952415



3.6 配置Android环境

如果想编译Android手机APP,必须要先自己配置好自己的Android环境。(搭建环境的过程可以自行百度搜索学习)


然后才可以进行下面的步骤。


【1】选择Android编译器

image-20240425232651515


image-20240509202408776


【2】创建Android配置文件


image-20240117144604025


image-20240117144635052


image-20240117144652014


创建完成。

【3】配置Android图标与名称

image-20240612100947190


【3】编译Android上位机

Qt本身是跨平台的,直接选择Android的编译器,就可以将程序编译到Android平台。

然后点击构建。

image-20240509202534407


成功之后,在目录下可以看到生成的apk文件,也就是Android手机的安装包,电脑端使用QQ发送给手机QQ,手机登录QQ接收,就能直接安装。


生成的apk的目录在哪里呢? 编译完成之后,在控制台会输出APK文件的路径。

知道目录在哪里之后,在Windows的文件资源管理器里,找到路径,具体看下图,找到生成的apk文件。

image-20240509202712295


D:/linux-share-dir/QT/build-app_Huawei_Eco_tracking-Android_for_arm64_v8a_Clang_Qt_5_12_6_for_Android_ARM64_v8a-Release/android-build//build/outputs/apk/debug/android-build-debug.apk


四、STM32代码开发

4.1 MQTT协议设计

#include "MQTTClient.h"
#include <string.h>
#include <stdlib.h>

#if !defined(_WINDOWS)
	#include <sys/time.h>
  	#include <sys/socket.h>
	#include <unistd.h>
  	#include <errno.h>
#else
#include <winsock2.h>
#include <ws2tcpip.h>
#define MAXHOSTNAMELEN 256
#define EAGAIN WSAEWOULDBLOCK
#define EINTR WSAEINTR
#define EINPROGRESS WSAEINPROGRESS
#define EWOULDBLOCK WSAEWOULDBLOCK
#define ENOTCONN WSAENOTCONN
#define ECONNRESET WSAECONNRESET
#endif

#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))


char* topics[] =  {"TopicA", "TopicA/B", "Topic/C", "TopicA/C", "/TopicA"};
char* wildtopics[] = {"TopicA/+", "+/C", "#", "/#", "/+", "+/+", "TopicA/#"};
char* nosubscribe_topics[] = {"nosubscribe",};

struct Options
{
	char* connection;         /**< connection to system under test. */
	char* clientid1;
	char* clientid2;
	char* username;
	char* password;
	int verbose;
	int MQTTVersion;
	int iterations;
	int run_dollar_topics_test;
	int run_subscribe_failure_test;
} options =
{
	"tcp://localhost:1883",
	"myclientid",
	"myclientid2",
	NULL,
	NULL,
	0,
	MQTTVERSION_3_1_1,
	1,
	0,
	0,
};


void usage(void)
{
	printf("options:\n  connection, clientid1, clientid2, username, password, MQTTversion, iterations, verbose\n");
	exit(EXIT_FAILURE);
}

void getopts(int argc, char** argv)
{
	int count = 1;
	
	while (count < argc)
	{
		if (strcmp(argv[count], "--dollar_topics_test") == 0 || strcmp(argv[count], "--$") == 0)
		{
			options.run_dollar_topics_test = 1;
			printf("Running $ topics test\n");
		}
		else if (strcmp(argv[count], "--subscribe_failure_test") == 0 || strcmp(argv[count], "-s") == 0)
		{
			options.run_subscribe_failure_test = 1;
			printf("Running subscribe failure test\n");
		}
		else if (strcmp(argv[count], "--connection") == 0)
		{
			if (++count < argc)
			{
				options.connection = argv[count];
				printf("Setting connection to %s\n", options.connection);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--clientid1") == 0)
		{
			if (++count < argc)
			{
				options.clientid1 = argv[count];
				printf("Setting clientid1 to %s\n", options.clientid1);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--clientid2") == 0)
		{
			if (++count < argc)
			{
				options.clientid2 = argv[count];
				printf("Setting clientid2 to %s\n", options.clientid2);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--username") == 0)
		{
			if (++count < argc)
			{
				options.username = argv[count];
				printf("Setting username to %s\n", options.username);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--password") == 0)
		{
			if (++count < argc)
			{
				options.password = argv[count];
				printf("Setting password to %s\n", options.password);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--MQTTversion") == 0)
		{
			if (++count < argc)
			{
				options.MQTTVersion = atoi(argv[count]);
				printf("Setting MQTT version to %d\n", options.MQTTVersion);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--iterations") == 0)
		{
			if (++count < argc)
			{
				options.iterations = atoi(argv[count]);
				printf("Setting iterations to %d\n", options.iterations);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--verbose") == 0)
		{
			options.verbose = 1;
			printf("\nSetting verbose on\n");
		}
		count++;
	}
}


#if defined(_WIN32) || defined(_WINDOWS)
#define msleep Sleep
#define START_TIME_TYPE DWORD
static DWORD start_time = 0;
START_TIME_TYPE start_clock(void)
{
	return GetTickCount();
}
#elif defined(AIX)
#define mqsleep sleep
#define START_TIME_TYPE struct timespec
START_TIME_TYPE start_clock(void)
{
	static struct timespec start;
	clock_gettime(CLOCK_REALTIME, &start);
	return start;
}
#else
#define msleep(A) usleep(A*1000)
#define START_TIME_TYPE struct timeval
/* TODO - unused - remove? static struct timeval start_time; */
START_TIME_TYPE start_clock(void)
{
	struct timeval start_time;
	gettimeofday(&start_time, NULL);
	return start_time;
}
#endif

#define LOGA_DEBUG 0
#define LOGA_INFO 1
#include <stdarg.h>
#include <time.h>
#include <sys/timeb.h>
void MyLog(int LOGA_level, char* format, ...)
{
	static char msg_buf[256];
	va_list args;
#if defined(_WIN32) || defined(_WINDOWS)
	struct timeb ts;
#else
	struct timeval ts;
#endif
	struct tm timeinfo;

	if (LOGA_level == LOGA_DEBUG && options.verbose == 0)
	  return;

#if defined(_WIN32) || defined(_WINDOWS)
	ftime(&ts);
	localtime_s(&timeinfo, &ts.time);
#else
	gettimeofday(&ts, NULL);
	localtime_r(&ts.tv_sec, &timeinfo);
#endif
	strftime(msg_buf, 80, "%Y%m%d %H%M%S", &timeinfo);

#if defined(_WIN32) || defined(_WINDOWS)
	sprintf(&msg_buf[strlen(msg_buf)], ".%.3hu ", ts.millitm);
#else
	sprintf(&msg_buf[strlen(msg_buf)], ".%.3lu ", ts.tv_usec / 1000L);
#endif

	va_start(args, format);
	vsnprintf(&msg_buf[strlen(msg_buf)], sizeof(msg_buf) - strlen(msg_buf), format, args);
	va_end(args);

	printf("%s\n", msg_buf);
	fflush(stdout);
}

int tests = 0;
int failures = 0;


void myassert(char* filename, int lineno, char* description, int value, char* format, ...)
{
	++tests;
	if (!value)
	{
		int count;
		va_list args;

		++failures;
		printf("Assertion failed, file %s, line %d, description: %s\n", filename, lineno, description);

		va_start(args, format);
		count = vprintf(format, args);
		va_end(args);
		if (count)
			printf("\n");

		//cur_output += sprintf(cur_output, "<failure type=\"%s\">file %s, line %d </failure>\n", 
        //               description, filename, lineno);
	}
    else
    	MyLog(LOGA_DEBUG, "Assertion succeeded, file %s, line %d, description: %s", filename, lineno, description);  
}


#define assert(a, b, c, d) myassert(__FILE__, __LINE__, a, b, c, d)
#define assert1(a, b, c, d, e) myassert(__FILE__, __LINE__, a, b, c, d, e)

typedef struct
{
	char* topicName;
	int topicLen;
	MQTTClient_message* m;
} messageStruct;

messageStruct messagesArrived[1000];
int messageCount = 0;

int messageArrived(void* context, char* topicName, int topicLen, MQTTClient_message* m)
{
	messagesArrived[messageCount].topicName = topicName;
	messagesArrived[messageCount].topicLen = topicLen;
	messagesArrived[messageCount++].m = m;
	MyLog(LOGA_DEBUG, "Callback: %d message received on topic %s is %.*s.",
					messageCount, topicName, m->payloadlen, (char*)(m->payload));
	return 1;
}


void clearMessages(void)
{
	int i;

	for (i = 0; i < messageCount; ++i)
	{
		MQTTClient_free(messagesArrived[i].topicName);
		MQTTClient_freeMessage(&messagesArrived[i].m);
	}
	messageCount = 0;
}

void cleanup(void)
{
	// clean all client state
	char* clientids[] = {options.clientid1, options.clientid2};
	int i, rc;
	MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
	MQTTClient aclient;

	MyLog(LOGA_INFO, "Cleaning up");

	opts.keepAliveInterval = 20;
	opts.cleansession = 1;
	opts.username = options.username;
	opts.password = options.password;
	opts.MQTTVersion = options.MQTTVersion;
	
	for (i = 0; i < 2; ++i)
	{
		rc = MQTTClient_create(&aclient, options.connection, clientids[i], MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
		assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

		rc = MQTTClient_connect(aclient, &opts);
		assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

		rc = MQTTClient_disconnect(aclient, 100);
		assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

		MQTTClient_destroy(&aclient);
	}

	// clean retained messages 
	rc = MQTTClient_create(&aclient, options.connection, options.clientid1, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_setCallbacks(aclient, NULL, NULL, messageArrived, NULL);
	assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_subscribe(aclient, "#", 0);
	assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	msleep(2000); // wait for all retained messages to arrive

	rc = MQTTClient_unsubscribe(aclient, "#");
	assert("Good rc from unsubscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	for (i = 0; i < messageCount; ++i)
	{
		if (messagesArrived[i].m->retained)
		{
			MyLog(LOGA_INFO, "Deleting retained message for topic %s", (char*)messagesArrived[i].topicName);
			rc = MQTTClient_publish(aclient, messagesArrived[i].topicName, 0, "", 0, 1, NULL);
			assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);
		}
	}

	rc = MQTTClient_disconnect(aclient, 100);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	MQTTClient_destroy(&aclient);

	clearMessages();

	MyLog(LOGA_INFO, "Finished cleaning up");
}

 
int basic_test(void)
{
	int i, rc;
	MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
	MQTTClient aclient;

	MyLog(LOGA_INFO, "Starting basic test");

	tests = failures = 0;

	opts.keepAliveInterval = 20;
	opts.cleansession = 1;
	opts.username = options.username;
	opts.password = options.password;
	opts.MQTTVersion = options.MQTTVersion;

	rc = MQTTClient_create(&aclient, options.connection, options.clientid1, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_setCallbacks(aclient, NULL, NULL, messageArrived, NULL);
	assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_disconnect(aclient, 100);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_subscribe(aclient, topics[0], 0);
	assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(aclient, topics[0], 5, "qos 0", 0, 0, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(aclient, topics[0], 5, "qos 1", 1, 0, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(aclient, topics[0], 5, "qos 2", 2, 0, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	msleep(1000);

	rc = MQTTClient_disconnect(aclient, 10000);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	assert("3 Messages received", messageCount == 3, "messageCount was %d", messageCount);
	clearMessages();

	/*opts.MQTTVersion = MQTTVERSION_3_1;
	rc = MQTTClient_connect(aclient, &opts); // should fail - wrong protocol version
	assert("Bad rc from connect", rc == MQTTCLIENT_FAILURE, "rc was %d", rc);*/
	
	MQTTClient_destroy(&aclient);

	MyLog(LOGA_INFO, "Basic test %s", (failures == 0) ?  "succeeded" : "failed");
	return failures;
}



int offline_message_queueing_test(void)
{
	int i, rc;
	MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
	MQTTClient aclient;
	MQTTClient bclient;

	MyLog(LOGA_INFO, "Offline message queueing test");

	tests = failures = 0;

	opts.keepAliveInterval = 20;
	opts.cleansession = 0;
	opts.username = options.username;
	opts.password = options.password;
	opts.MQTTVersion = options.MQTTVersion;

	rc = MQTTClient_create(&aclient, options.connection, options.clientid1, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_setCallbacks(aclient, NULL, NULL, messageArrived, NULL);
	assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_subscribe(aclient, wildtopics[5], 2);
	assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_disconnect(aclient, 100);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_create(&bclient, options.connection, options.clientid2, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	opts.cleansession = 1;
	rc = MQTTClient_connect(bclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(bclient, topics[1], 5, "qos 0", 0, 0, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(bclient, topics[2], 5, "qos 1", 1, 0, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);	

	rc = MQTTClient_publish(bclient, topics[3], 5, "qos 2", 2, 0, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	msleep(2000);

	rc = MQTTClient_disconnect(bclient, 100);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	MQTTClient_destroy(&bclient);

	opts.cleansession = 0;
	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

    msleep(1000);  // receive the queued messages

	rc = MQTTClient_disconnect(aclient, 100);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	MQTTClient_destroy(&aclient);

	assert("2 or 3 messages received", messageCount == 3 || messageCount == 2, "messageCount was %d", messageCount);

	MyLog(LOGA_INFO, "This server %s queueing QoS 0 messages for offline clients", (messageCount == 3) ? "is" : "is not");

	clearMessages();

	MyLog(LOGA_INFO, "Offline message queueing test %s", (failures == 0) ?  "succeeded" : "failed");
	return failures;
}


int retained_message_test(void)
{ 
	int i, rc;
	MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
	MQTTClient aclient;

	MyLog(LOGA_INFO, "Retained message test");

	tests = failures = 0;

	opts.keepAliveInterval = 20;
	opts.cleansession = 1;
	opts.username = options.username;
	opts.password = options.password;
	opts.MQTTVersion = options.MQTTVersion;

	assert("0 messages received", messageCount == 0, "messageCount was %d", messageCount);
  
    // set retained messages
	rc = MQTTClient_create(&aclient, options.connection, options.clientid1, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_setCallbacks(aclient, NULL, NULL, messageArrived, NULL);
	assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(aclient, topics[1], 5, "qos 0", 0, 1, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(aclient, topics[2], 5, "qos 1", 1, 1, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);	

	rc = MQTTClient_publish(aclient, topics[3], 5, "qos 2", 2, 1, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

    msleep(1000);

	rc = MQTTClient_subscribe(aclient, wildtopics[5], 2);
	assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

    msleep(2000);

	rc = MQTTClient_disconnect(aclient, 100);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	assert("3 messages received", messageCount == 3, "messageCount was %d", messageCount);

	for (i = 0; i < messageCount; ++i)
	{
		assert("messages should be retained", messagesArrived[i].m->retained, "retained was %d", 
			messagesArrived[i].m->retained);
		MQTTClient_free(messagesArrived[i].topicName);
		MQTTClient_freeMessage(&messagesArrived[i].m);
	}
	messageCount = 0;

    // clear retained messages
	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(aclient, topics[1], 0, "", 0, 1, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(aclient, topics[2], 0, "", 1, 1, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);	

	rc = MQTTClient_publish(aclient, topics[3], 0, "", 2, 1, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

    msleep(200); // wait for QoS 2 exchange to be completed
	rc = MQTTClient_subscribe(aclient, wildtopics[5], 2);
	assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

    msleep(200);

	rc = MQTTClient_disconnect(aclient, 100);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	assert("0 messages received", messageCount == 0, "messageCount was %d", messageCount);

	MQTTClient_destroy(&aclient);

	MyLog(LOGA_INFO, "Retained message test %s", (failures == 0) ?  "succeeded" : "failed");
	return failures;
}

#define SOCKET_ERROR -1

int test6_socket_error(char* aString, int sock)
{
#if defined(_WIN32)
	int errno;
#endif

#if defined(_WIN32)
	errno = WSAGetLastError();
#endif
	if (errno != EINTR && errno != EAGAIN && errno != EINPROGRESS && errno != EWOULDBLOCK)
	{
		if (strcmp(aString, "shutdown") != 0 || (errno != ENOTCONN && errno != ECONNRESET))
			printf("Socket error %d in %s for socket %d", errno, aString, sock);
	}
	return errno;
}

int test6_socket_close(int socket)
{
	int rc;

#if defined(_WIN32)
	if (shutdown(socket, SD_BOTH) == SOCKET_ERROR)
		test6_socket_error("shutdown", socket);
	if ((rc = closesocket(socket)) == SOCKET_ERROR)
		test6_socket_error("close", socket);
#else
	if (shutdown(socket, SHUT_RDWR) == SOCKET_ERROR)
		test6_socket_error("shutdown", socket);
	if ((rc = close(socket)) == SOCKET_ERROR)
		test6_socket_error("close", socket);
#endif
	return rc;
}

typedef struct
{
	int socket;
	time_t lastContact;
#if defined(OPENSSL)
	SSL* ssl;
	SSL_CTX* ctx;
#endif
} networkHandles;


typedef struct
{
	char* clientID;					/**< the string id of the client */
	char* username;					/**< MQTT v3.1 user name */
	char* password;					/**< MQTT v3.1 password */
	unsigned int cleansession : 1;	/**< MQTT clean session flag */
	unsigned int connected : 1;		/**< whether it is currently connected */
	unsigned int good : 1; 			/**< if we have an error on the socket we turn this off */
	unsigned int ping_outstanding : 1;
	int connect_state : 4;
	networkHandles net;
/* ... */
} Clients;


typedef struct
{
	char* serverURI;
	Clients* c;
	MQTTClient_connectionLost* cl;
	MQTTClient_messageArrived* ma;
	MQTTClient_deliveryComplete* dc;
	void* context;

	int connect_sem;
	int rc; /* getsockopt return code in connect */
	int connack_sem;
	int suback_sem;
	int unsuback_sem;
	void* pack;
} MQTTClients;

 
int will_message_test(void)
{
	int i, rc, count = 0;
	MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
	MQTTClient_willOptions wopts =  MQTTClient_willOptions_initializer;
	MQTTClient aclient, bclient;

	MyLog(LOGA_INFO, "Will message test");

	tests = failures = 0;

	opts.keepAliveInterval = 2;
	opts.cleansession = 1;
	opts.username = options.username;
	opts.password = options.password;
	opts.MQTTVersion = options.MQTTVersion;

	opts.will = &wopts;
	opts.will->message = "client not disconnected";
	opts.will->qos = 1;
	opts.will->retained = 0;
	opts.will->topicName = topics[2];

	rc = MQTTClient_create(&aclient, options.connection, options.clientid1, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_create(&bclient, options.connection, options.clientid2, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_setCallbacks(bclient, NULL, NULL, messageArrived, NULL);
	assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	opts.keepAliveInterval = 20;
	opts.will = NULL;
	rc = MQTTClient_connect(bclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_subscribe(bclient, topics[2], 2);
	assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

    msleep(100);

   	test6_socket_close(((MQTTClients*)aclient)->c->net.socket); 

	while (messageCount == 0 && ++count < 10)
    	msleep(1000);

	rc = MQTTClient_disconnect(bclient, 100);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	MQTTClient_destroy(&bclient);

	assert("will message received", messageCount == 1, "messageCount was %d", messageCount);

	rc = MQTTClient_disconnect(aclient, 100);

	MQTTClient_destroy(&aclient);

	MyLog(LOGA_INFO, "Will message test %s", (failures == 0) ?  "succeeded" : "failed");
	return failures;
}


int overlapping_subscriptions_test(void)
{
  /* overlapping subscriptions. When there is more than one matching subscription for the same client for a topic,
   the server may send back one message with the highest QoS of any matching subscription, or one message for
   each subscription with a matching QoS. */

	int i, rc;
	MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
	MQTTClient aclient;
	char* topicList[] = {wildtopics[6], wildtopics[0]};
	int qosList[] = {2, 1};

	MyLog(LOGA_INFO, "Starting overlapping subscriptions test");

	clearMessages();
	tests = failures = 0;

	opts.keepAliveInterval = 20;
	opts.cleansession = 1;
	opts.username = options.username;
	opts.password = options.password;
	opts.MQTTVersion = options.MQTTVersion;

	rc = MQTTClient_create(&aclient, options.connection, options.clientid1, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_setCallbacks(aclient, NULL, NULL, messageArrived, NULL);
	assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_subscribeMany(aclient, 2, topicList, qosList);
	assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(aclient, topics[3], strlen("overlapping topic filters") + 1, 
                "overlapping topic filters", 2, 0, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

    msleep(1000);

	assert("1 or 2 messages received", messageCount == 1 || messageCount == 2, "messageCount was %d", messageCount);

    if (messageCount == 1)
	{
		MyLog(LOGA_INFO, "This server is publishing one message for all matching overlapping subscriptions, not one for each.");
		assert("QoS should be 2", messagesArrived[0].m->qos == 2, "QoS was %d", messagesArrived[0].m->qos);
	}
    else
	{
		MyLog(LOGA_INFO, "This server is publishing one message per each matching overlapping subscription.");
		assert1("QoSs should be 1 and 2", 
			(messagesArrived[0].m->qos == 2 && messagesArrived[1].m->qos == 1) ||
      		(messagesArrived[0].m->qos == 1 && messagesArrived[1].m->qos == 2),
		"QoSs were %d %d", messagesArrived[0].m->qos, messagesArrived[1].m->qos);
	}

	rc = MQTTClient_disconnect(aclient, 100);

	MQTTClient_destroy(&aclient);

	MyLog(LOGA_INFO, "Overlapping subscription test %s", (failures == 0) ?  "succeeded" : "failed");
	return failures;
}


int keepalive_test(void)
{
	/* keepalive processing.  We should be kicked off by the server if we don't send or receive any data, and don't send
	any pings either. */

	int i, rc, count = 0;
	MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
	MQTTClient_willOptions wopts =  MQTTClient_willOptions_initializer;
	MQTTClient aclient, bclient;

	MyLog(LOGA_INFO, "Starting keepalive test");

	tests = failures = 0;
	clearMessages();

	opts.cleansession = 1;
	opts.username = options.username;
	opts.password = options.password;
	opts.MQTTVersion = options.MQTTVersion;

	opts.will = &wopts;
	opts.will->message = "keepalive expiry";
	opts.will->qos = 1;
	opts.will->retained = 0;
	opts.will->topicName = topics[4];

	opts.keepAliveInterval = 20;
	rc = MQTTClient_create(&bclient, options.connection, options.clientid2, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_setCallbacks(bclient, NULL, NULL, messageArrived, NULL);
	assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_connect(bclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_subscribe(bclient, topics[4], 2);
	assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	opts.keepAliveInterval = 2;
	rc = MQTTClient_create(&aclient, options.connection, options.clientid1, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	while (messageCount == 0 && ++count < 20)
    	msleep(1000);

	rc = MQTTClient_disconnect(bclient, 100);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	assert("Should have will message", messageCount == 1, "messageCount was %d", messageCount);

	rc = MQTTClient_disconnect(aclient, 100);

	MQTTClient_destroy(&aclient);

	MyLog(LOGA_INFO, "Keepalive test %s", (failures == 0) ?  "succeeded" : "failed");
	return failures;
}



int redelivery_on_reconnect_test(void)
{
	/* redelivery on reconnect. When a QoS 1 or 2 exchange has not been completed, the server should retry the 
	 appropriate MQTT packets */

	int i, rc, count = 0;
	MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
	MQTTClient aclient;

	MyLog(LOGA_INFO, "Starting redelivery on reconnect test");

	tests = failures = 0;
	clearMessages();

	opts.keepAliveInterval = 0;
	opts.cleansession = 0;
	opts.username = options.username;
	opts.password = options.password;
	opts.MQTTVersion = options.MQTTVersion;

	rc = MQTTClient_create(&aclient, options.connection, options.clientid1, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_subscribe(aclient, wildtopics[6], 2);
	assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);
	MQTTClient_yield();

    // no background processing because no callback has been set
	rc = MQTTClient_publish(aclient, topics[1], 6, "qos 1", 2, 0, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(aclient, topics[3], 6, "qos 2", 2, 0, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_disconnect(aclient, 0);

	assert("No messages should have been received yet", messageCount == 0, "messageCount was %d", messageCount);

	rc = MQTTClient_setCallbacks(aclient, NULL, NULL, messageArrived, NULL);
	assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	while (messageCount < 2 && ++count < 5)
		msleep(1000);

	assert("Should have 2 messages", messageCount == 2, "messageCount was %d", messageCount);

	rc = MQTTClient_disconnect(aclient, 100);

	MQTTClient_destroy(&aclient);

	MyLog(LOGA_INFO, "Redelivery on reconnect test %s", (failures == 0) ?  "succeeded" : "failed");
	return failures;
}



int zero_length_clientid_test(void)
{
	int i, rc, count = 0;
	MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
	MQTTClient aclient;

	MyLog(LOGA_INFO, "Starting zero length clientid test");

	tests = failures = 0;
	clearMessages();

	opts.keepAliveInterval = 0;
	opts.cleansession = 0;
	opts.username = options.username;
	opts.password = options.password;
	opts.MQTTVersion = options.MQTTVersion;

	rc = MQTTClient_create(&aclient, options.connection, "", MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("rc 2 from connect", rc == 2, "rc was %d", rc); // this should always fail

	opts.cleansession = 1;
	rc = MQTTClient_connect(aclient, &opts);
	assert("Connack rc should be 0 or 2", rc == MQTTCLIENT_SUCCESS || rc == 2, "rc was %d", rc);

	MyLog(LOGA_INFO, "This server %s support zero length clientids", (rc == 2) ? "does not" : "does");

	if (rc == MQTTCLIENT_SUCCESS)
		rc = MQTTClient_disconnect(aclient, 100);

	MQTTClient_destroy(&aclient);

	MyLog(LOGA_INFO, "Zero length clientid test %s", (failures == 0) ?  "succeeded" : "failed");
	return failures;
}


int dollar_topics_test(void)
{
  /* $ topics. The specification says that a topic filter which starts with a wildcard does not match topic names that
   	begin with a $.  Publishing to a topic which starts with a $ may not be allowed on some servers (which is entirely valid),
   	so this test will not work and should be omitted in that case.
	*/
	int i, rc, count = 0;
	MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
	MQTTClient aclient;
	char dollartopic[20];

	MyLog(LOGA_INFO, "Starting $ topics test");

	sprintf(dollartopic, "$%s", topics[1]);
  
    clearMessages();

	opts.keepAliveInterval = 5;
	opts.cleansession = 1;
	opts.username = options.username;
	opts.password = options.password;
	opts.MQTTVersion = options.MQTTVersion;

	rc = MQTTClient_create(&aclient, options.connection, options.clientid1, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_setCallbacks(aclient, NULL, NULL, messageArrived, NULL);
	assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_subscribe(aclient, wildtopics[5], 2);
	assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

    msleep(1000); // wait for any retained messages, hopefully
    clearMessages();

	rc = MQTTClient_publish(aclient, topics[1], 20, "not sent to dollar topic", 1, 0, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_publish(aclient, dollartopic, 20, "sent to dollar topic", 1, 0, NULL);
	assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

    msleep(1000);
	assert("Should have 1 message", messageCount == 1, "messageCount was %d", messageCount);

	rc = MQTTClient_disconnect(aclient, 100);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	MQTTClient_destroy(&aclient);

	MyLog(LOGA_INFO, "$ topics test %s", (failures == 0) ?  "succeeded" : "failed");
	return failures;
}


int subscribe_failure_test(void)
{
  /* Subscribe failure.  A new feature of MQTT 3.1.1 is the ability to send back negative reponses to subscribe
   requests.  One way of doing this is to subscribe to a topic which is not allowed to be subscribed to.
  */
	int i, rc, count = 0;
	MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
	MQTTClient aclient;
	int subqos = 2;

	MyLog(LOGA_INFO, "Starting subscribe failure test");
  
    clearMessages();

	opts.keepAliveInterval = 5;
	opts.cleansession = 1;
	opts.username = options.username;
	opts.password = options.password;
	opts.MQTTVersion = options.MQTTVersion;

	rc = MQTTClient_create(&aclient, options.connection, options.clientid1, MQTTCLIENT_PERSISTENCE_DEFAULT, NULL);
	assert("good rc from create",  rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc);

	rc = MQTTClient_setCallbacks(aclient, NULL, NULL, messageArrived, NULL);
	assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	rc = MQTTClient_connect(aclient, &opts);
	assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);
 
	rc = MQTTClient_subscribeMany(aclient, 1, &nosubscribe_topics[0], &subqos);
	assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);
	assert("0x80 rc from subscribe", subqos == 0x80, "subqos was %d", subqos);

	rc = MQTTClient_disconnect(aclient, 100);
	assert("Disconnect successful", rc == MQTTCLIENT_SUCCESS, "rc was %d", rc);

	MQTTClient_destroy(&aclient);

	MyLog(LOGA_INFO, "Subscribe failure test %s", (failures == 0) ?  "succeeded" : "failed");
	return failures;
}


int main(int argc, char** argv)
{
	int i;
	int all_failures = 0;

	getopts(argc, argv);

	for (i = 0; i < options.iterations; ++i)
	{
		cleanup();
		all_failures += basic_test() + 
						offline_message_queueing_test() +
		                retained_message_test() +
		                will_message_test() +
		                overlapping_subscriptions_test() +
		                keepalive_test() +
						redelivery_on_reconnect_test() + 
						zero_length_clientid_test();

		if (options.run_dollar_topics_test)
			all_failures += dollar_topics_test();
		
		if (options.run_subscribe_failure_test)
			all_failures += subscribe_failure_test();
	}

	MyLog(LOGA_INFO, "Test suite %s", (all_failures == 0) ?  "succeeded" : "failed");
}


4.2 OLED显示屏驱动代码

#include "oled.h"
#include "stdlib.h"
#include "oledfont.h"  	 
#include "delay.h"

//OLED模式设置
//0: 4线串行模式  (模块的BS1,BS2均接GND)
//1: 并行8080模式 (模块的BS1,BS2均接VCC)
#define OLED_MODE 	1 
		    						  
//---------------------------OLED端口定义--------------------------  					   
#define OLED_CS  PDout(6)
#define OLED_RST PGout(15) 	
#define OLED_RS  PDout(3)
#define OLED_WR  PGout(14)		  
#define OLED_RD  PGout(13)	   
//PC0~7,作为数据线
#define DATAOUT(x) GPIOC->ODR=(GPIOC->ODR&0xff00)|(x&0x00FF); //输出
  
//使用4线串行接口时使用 
#define OLED_SCLK PCout(0)
#define OLED_SDIN PCout(1)
		     
#define OLED_CMD  0	//写命令
#define OLED_DATA 1	//写数据
//OLED控制用函数
void OLED_WR_Byte(u8 dat,u8 cmd);	    
void OLED_Display_On(void);
void OLED_Display_Off(void);
void OLED_Refresh_Gram(void);  		    
void OLED_Init(void);
void OLED_Clear(void);
void OLED_DrawPoint(u8 x,u8 y,u8 t);
void OLED_Fill(u8 x1,u8 y1,u8 x2,u8 y2,u8 dot);
void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size,u8 mode);
void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size);
void OLED_ShowString(u8 x,u8 y,const u8 *p,u8 size);	



//OLED的显存
//存放格式如下.
//[0]0 1 2 3 ... 127	
//[1]0 1 2 3 ... 127	
//[2]0 1 2 3 ... 127	
//[3]0 1 2 3 ... 127	
//[4]0 1 2 3 ... 127	
//[5]0 1 2 3 ... 127	
//[6]0 1 2 3 ... 127	
//[7]0 1 2 3 ... 127 		   
u8 OLED_GRAM[128][8];	 

//更新显存到LCD		 
void OLED_Refresh_Gram(void)
{
	u8 i,n;		    
	for(i=0;i<8;i++)  
	{  
		OLED_WR_Byte (0xb0+i,OLED_CMD);    //设置页地址(0~7)
		OLED_WR_Byte (0x00,OLED_CMD);      //设置显示位置—列低地址
		OLED_WR_Byte (0x10,OLED_CMD);      //设置显示位置—列高地址   
		for(n=0;n<128;n++)OLED_WR_Byte(OLED_GRAM[n][i],OLED_DATA); 
	}   
}
#if OLED_MODE==1	//8080并口 
//向SSD1306写入一个字节。
//dat:要写入的数据/命令
//cmd:数据/命令标志 0,表示命令;1,表示数据;
void OLED_WR_Byte(u8 dat,u8 cmd)
{
	DATAOUT(dat);	    
 	OLED_RS=cmd;
	OLED_CS=0;	   
	OLED_WR=0;	 
	OLED_WR=1;
	OLED_CS=1;	  
	OLED_RS=1;	 
} 	    	    
#else
//向SSD1306写入一个字节。
//dat:要写入的数据/命令
//cmd:数据/命令标志 0,表示命令;1,表示数据;
void OLED_WR_Byte(u8 dat,u8 cmd)
{	
	u8 i;			  
	OLED_RS=cmd; //写命令 
	OLED_CS=0;		  
	for(i=0;i<8;i++)
	{			  
		OLED_SCLK=0;
		if(dat&0x80)OLED_SDIN=1;
		else OLED_SDIN=0;
		OLED_SCLK=1;
		dat<<=1;   
	}				 
	OLED_CS=1;		  
	OLED_RS=1;   	  
} 
#endif
	  	  
//开启OLED显示    
void OLED_Display_On(void)
{
	OLED_WR_Byte(0X8D,OLED_CMD);  //SET DCDC命令
	OLED_WR_Byte(0X14,OLED_CMD);  //DCDC ON
	OLED_WR_Byte(0XAF,OLED_CMD);  //DISPLAY ON
}
//关闭OLED显示     
void OLED_Display_Off(void)
{
	OLED_WR_Byte(0X8D,OLED_CMD);  //SET DCDC命令
	OLED_WR_Byte(0X10,OLED_CMD);  //DCDC OFF
	OLED_WR_Byte(0XAE,OLED_CMD);  //DISPLAY OFF
}		   			 
//清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!	  
void OLED_Clear(void)  
{  
	u8 i,n;  
	for(i=0;i<8;i++)for(n=0;n<128;n++)OLED_GRAM[n][i]=0X00;  
	OLED_Refresh_Gram();//更新显示
}
//画点 
//x:0~127
//y:0~63
//t:1 填充 0,清空				   
void OLED_DrawPoint(u8 x,u8 y,u8 t)
{
	u8 pos,bx,temp=0;
	if(x>127||y>63)return;//超出范围了.
	pos=7-y/8;
	bx=y%8;
	temp=1<<(7-bx);
	if(t)OLED_GRAM[x][pos]|=temp;
	else OLED_GRAM[x][pos]&=~temp;	    
}
//x1,y1,x2,y2 填充区域的对角坐标
//确保x1<=x2;y1<=y2 0<=x1<=127 0<=y1<=63	 	 
//dot:0,清空;1,填充	  
void OLED_Fill(u8 x1,u8 y1,u8 x2,u8 y2,u8 dot)  
{  
	u8 x,y;  
	for(x=x1;x<=x2;x++)
	{
		for(y=y1;y<=y2;y++)OLED_DrawPoint(x,y,dot);
	}													    
	OLED_Refresh_Gram();//更新显示
}
//在指定位置显示一个字符,包括部分字符
//x:0~127
//y:0~63
//mode:0,反白显示;1,正常显示				 
//size:选择字体 12/16/24
void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size,u8 mode)
{      			    
	u8 temp,t,t1;
	u8 y0=y;
	u8 csize=(size/8+((size%8)?1:0))*(size/2);		//得到字体一个字符对应点阵集所占的字节数
	chr=chr-' ';//得到偏移后的值		 
    for(t=0;t<csize;t++)
    {   
		if(size==12)temp=asc2_1206[chr][t]; 	 	//调用1206字体
		else if(size==16)temp=asc2_1608[chr][t];	//调用1608字体
		else if(size==24)temp=asc2_2412[chr][t];	//调用2412字体
		else return;								//没有的字库
        for(t1=0;t1<8;t1++)
		{
			if(temp&0x80)OLED_DrawPoint(x,y,mode);
			else OLED_DrawPoint(x,y,!mode);
			temp<<=1;
			y++;
			if((y-y0)==size)
			{
				y=y0;
				x++;
				break;
			}
		}  	 
    }          
}
//m^n函数
u32 mypow(u8 m,u8 n)
{
	u32 result=1;	 
	while(n--)result*=m;    
	return result;
}				  
//显示2个数字
//x,y :起点坐标	 
//len :数字的位数
//size:字体大小
//mode:模式	0,填充模式;1,叠加模式
//num:数值(0~4294967295);	 		  
void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size)
{         	
	u8 t,temp;
	u8 enshow=0;						   
	for(t=0;t<len;t++)
	{
		temp=(num/mypow(10,len-t-1))%10;
		if(enshow==0&&t<(len-1))
		{
			if(temp==0)
			{
				OLED_ShowChar(x+(size/2)*t,y,' ',size,1);
				continue;
			}else enshow=1; 
		 	 
		}
	 	OLED_ShowChar(x+(size/2)*t,y,temp+'0',size,1); 
	}
} 
//显示字符串
//x,y:起点坐标  
//size:字体大小 
//*p:字符串起始地址 
void OLED_ShowString(u8 x,u8 y,const u8 *p,u8 size)
{	
    while((*p<='~')&&(*p>=' '))//判断是不是非法字符!
    {       
        if(x>(128-(size/2))){x=0;y+=size;}
        if(y>(64-size)){y=x=0;OLED_Clear();}
        OLED_ShowChar(x,y,*p,size,1);	 
        x+=size/2;
        p++;
    }  
	
}	
//初始化SSD1306					    
void OLED_Init(void)
{ 	 	 			 	 				 
	RCC->APB2ENR|=1<<4;    //使能PORTC时钟 
	RCC->APB2ENR|=1<<5;    //使能PORTD时钟 
	RCC->APB2ENR|=1<<8;    //使能PORTG时钟

  	GPIOD->CRL&=0XF0FF0FFF;//PD3,6 推挽输出 
  	GPIOD->CRL|=0X03003000;	 
  	GPIOD->ODR|=1<<3;
  	GPIOD->ODR|=1<<6;	    
#if OLED_MODE==1			//8080并口模式 
	GPIOC->CRL=0X33333333; 	//PC0~7 OUT
	GPIOC->ODR|=0X00FF;
		
  	GPIOG->CRH&=0X000FFFFF;	//PG13,14,15 OUT
  	GPIOG->CRH|=0X33300000;	 
	GPIOG->ODR|=7<<13; 
#else						//4线SPI模式
	GPIOC->CRL&=0XFFFFFF00; //PC0,1 OUT
	GPIOC->CRL|=0X00000033;	    
 	GPIOC->ODR|=3<<0;

 	GPIOG->CRH&=0X0FFFFFFF;	//RST	   
 	GPIOG->CRH|=0X30000000;	 
	GPIOG->ODR|=1<<15;
#endif									  
	OLED_CS=1;
	OLED_RS=1;	 
	
	OLED_RST=0;
	delay_ms(100);
	OLED_RST=1; 
					  
	OLED_WR_Byte(0xAE,OLED_CMD); //关闭显示
	OLED_WR_Byte(0xD5,OLED_CMD); //设置时钟分频因子,震荡频率
	OLED_WR_Byte(80,OLED_CMD);   //[3:0],分频因子;[7:4],震荡频率
	OLED_WR_Byte(0xA8,OLED_CMD); //设置驱动路数
	OLED_WR_Byte(0X3F,OLED_CMD); //默认0X3F(1/64) 
	OLED_WR_Byte(0xD3,OLED_CMD); //设置显示偏移
	OLED_WR_Byte(0X00,OLED_CMD); //默认为0

	OLED_WR_Byte(0x40,OLED_CMD); //设置显示开始行 [5:0],行数.
													    
	OLED_WR_Byte(0x8D,OLED_CMD); //电荷泵设置
	OLED_WR_Byte(0x14,OLED_CMD); //bit2,开启/关闭
	OLED_WR_Byte(0x20,OLED_CMD); //设置内存地址模式
	OLED_WR_Byte(0x02,OLED_CMD); //[1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10;
	OLED_WR_Byte(0xA1,OLED_CMD); //段重定义设置,bit0:0,0->0;1,0->127;
	OLED_WR_Byte(0xC0,OLED_CMD); //设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数
	OLED_WR_Byte(0xDA,OLED_CMD); //设置COM硬件引脚配置
	OLED_WR_Byte(0x12,OLED_CMD); //[5:4]配置
		 
	OLED_WR_Byte(0x81,OLED_CMD); //对比度设置
	OLED_WR_Byte(0xEF,OLED_CMD); //1~255;默认0X7F (亮度设置,越大越亮)
	OLED_WR_Byte(0xD9,OLED_CMD); //设置预充电周期
	OLED_WR_Byte(0xf1,OLED_CMD); //[3:0],PHASE 1;[7:4],PHASE 2;
	OLED_WR_Byte(0xDB,OLED_CMD); //设置VCOMH 电压倍率
	OLED_WR_Byte(0x30,OLED_CMD); //[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc;

	OLED_WR_Byte(0xA4,OLED_CMD); //全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏)
	OLED_WR_Byte(0xA6,OLED_CMD); //设置显示方式;bit0:1,反相显示;0,正常显示	    						   
	OLED_WR_Byte(0xAF,OLED_CMD); //开启显示	 
	OLED_Clear();
}  


4.3 土壤湿度采集代码

#include "adc.h"
#include "delay.h"					   
   
//初始化ADC1
//这里我们仅以规则通道为例
//我们默认仅开启通道1																	   
void  Adc_Init(void)
{    
	//先初始化IO口
 	RCC->APB2ENR|=1<<2;    //使能PORTA口时钟 
	GPIOA->CRL&=0XFFFFFF0F;//PA1 anolog输入 
	RCC->APB2ENR|=1<<9;    //ADC1时钟使能	  
	RCC->APB2RSTR|=1<<9;   //ADC1复位
	RCC->APB2RSTR&=~(1<<9);//复位结束	    
	RCC->CFGR&=~(3<<14);   //分频因子清零	
	//SYSCLK/DIV2=12M ADC时钟设置为12M,ADC最大时钟不能超过14M!
	//否则将导致ADC准确度下降! 
	RCC->CFGR|=2<<14;      	 
	ADC1->CR1&=0XF0FFFF;   //工作模式清零
	ADC1->CR1|=0<<16;      //独立工作模式  
	ADC1->CR1&=~(1<<8);    //非扫描模式	  
	ADC1->CR2&=~(1<<1);    //单次转换模式
	ADC1->CR2&=~(7<<17);	   
	ADC1->CR2|=7<<17;	   //软件控制转换  
	ADC1->CR2|=1<<20;      //使用用外部触发(SWSTART)!!!	必须使用一个事件来触发
	ADC1->CR2&=~(1<<11);   //右对齐	 
	ADC1->SQR1&=~(0XF<<20);
	ADC1->SQR1|=0<<20;     //1个转换在规则序列中 也就是只转换规则序列1 			   
	//设置通道1的采样时间
	ADC1->SMPR2&=~(3*1);   //通道1采样时间清空	  
 	ADC1->SMPR2|=7<<(3*1); //通道1  239.5周期,提高采样时间可以提高精确度	 
	ADC1->CR2|=1<<0;	   //开启AD转换器	 
	ADC1->CR2|=1<<3;       //使能复位校准  
	while(ADC1->CR2&1<<3); //等待校准结束 			 
    //该位由软件设置并由硬件清除。在校准寄存器被初始化后该位将被清除。 		 
	ADC1->CR2|=1<<2;        //开启AD校准	   
	while(ADC1->CR2&1<<2);  //等待校准结束
	//该位由软件设置以开始校准,并在校准结束时由硬件清除  
}				  
//获得ADC1某个通道的值
//ch:通道值 0~16
//返回值:转换结果
u16 Get_Adc(u8 ch)   
{
	//设置转换序列	  		 
	ADC1->SQR3&=0XFFFFFFE0;//规则序列1 通道ch
	ADC1->SQR3|=ch;		  			    
	ADC1->CR2|=1<<22;       //启动规则转换通道 
	while(!(ADC1->SR&1<<1));//等待转换结束	 	   
	return ADC1->DR;		//返回adc值	
}
//获取通道ch的转换值,取times次,然后平均 
//ch:通道编号
//times:获取次数
//返回值:通道ch的times次转换结果平均值
u16 Get_Adc_Average(u8 ch,u8 times)
{
	u32 temp_val=0;
	u8 t;
	for(t=0;t<times;t++)
	{
		temp_val+=Get_Adc(ch);
		delay_ms(5);
	}
	return temp_val/times;
} 


4.4 定时器配置代码

#include "timer.h"
#include "led.h"

//定时器3中断服务程序	 
void TIM3_IRQHandler(void)
{ 		    		  			    
	if(TIM3->SR&0X0001)//溢出中断
	{
		LED1=!LED1;			    				   				     	    	
	}				   
	TIM3->SR&=~(1<<0);//清除中断标志位 	    
}
//通用定时器3中断初始化
//这里时钟选择为APB1的2倍,而APB1为36M
//arr:自动重装值。
//psc:时钟预分频数
//这里使用的是定时器3!
void TIM3_Int_Init(u16 arr,u16 psc)
{
	RCC->APB1ENR|=1<<1;	//TIM3时钟使能    
 	TIM3->ARR=arr;  	//设定计数器自动重装值//刚好1ms    
	TIM3->PSC=psc;  	//预分频器7200,得到10Khz的计数时钟		  
	TIM3->DIER|=1<<0;   //允许更新中断	  
	TIM3->CR1|=0x01;    //使能定时器3
  	MY_NVIC_Init(1,3,TIM3_IRQn,2);//抢占1,子优先级3,组2									 
}
//TIM3 PWM部分初始化 
//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
void TIM3_PWM_Init(u16 arr,u16 psc)
{		 					 
	//此部分需手动修改IO口设置
	RCC->APB1ENR|=1<<1; 	//TIM3时钟使能    
	RCC->APB2ENR|=1<<3;    	//使能PORTB时钟	
	GPIOB->CRL&=0XFF0FFFFF;	//PB5输出
	GPIOB->CRL|=0X00B00000;	//复用功能输出 	  	 
	   
	RCC->APB2ENR|=1<<0;     //开启辅助时钟	   
	AFIO->MAPR&=0XFFFFF3FF; //清除MAPR的[11:10]
	AFIO->MAPR|=1<<11;      //部分重映像,TIM3_CH2->PB5

	TIM3->ARR=arr;			//设定计数器自动重装值 
	TIM3->PSC=psc;			//预分频器不分频
	
	TIM3->CCMR1|=7<<12;  	//CH2 PWM2模式		 
	TIM3->CCMR1|=1<<11; 	//CH2预装载使能	   
	TIM3->CCER|=1<<4;   	//OC2 输出使能	   
	TIM3->CR1=0x0080;   	//ARPE使能 
	TIM3->CR1|=0x01;    	//使能定时器3 											  
}  	 
//定时器5通道1输入捕获配置
//arr:自动重装值
//psc:时钟预分频数
void TIM5_Cap_Init(u16 arr,u16 psc)
{		 
	RCC->APB1ENR|=1<<3;   	//TIM5 时钟使能 
	RCC->APB2ENR|=1<<2;    	//使能PORTA时钟  
	 
	GPIOA->CRL&=0XFFFFFFF0;	//PA0 清除之前设置  
	GPIOA->CRL|=0X00000008;	//PA0 输入   
	GPIOA->ODR|=0<<0;		//PA0 下拉
	  
 	TIM5->ARR=arr;  		//设定计数器自动重装值   
	TIM5->PSC=psc;  		//预分频器 

	TIM5->CCMR1|=1<<0;		//CC1S=01 	选择输入端 IC1映射到TI1上
 	TIM5->CCMR1|=0<<4; 		//IC1F=0000 配置输入滤波器 不滤波
 	TIM5->CCMR1|=0<<10; 	//IC2PS=00 	配置输入分频,不分频 

	TIM5->CCER|=0<<1; 		//CC1P=0	上升沿捕获
	TIM5->CCER|=1<<0; 		//CC1E=1 	允许捕获计数器的值到捕获寄存器中

	TIM5->DIER|=1<<1;   	//允许捕获中断				
	TIM5->DIER|=1<<0;   	//允许更新中断	
	TIM5->CR1|=0x01;    	//使能定时器2
	MY_NVIC_Init(2,0,TIM5_IRQn,2);//抢占2,子优先级0,组2	   
}

//捕获状态
//[7]:0,没有成功的捕获;1,成功捕获到一次.
//[6]:0,还没捕获到高电平;1,已经捕获到高电平了.
//[5:0]:捕获高电平后溢出的次数
u8  TIM5CH1_CAPTURE_STA=0;	//输入捕获状态		    				
u16	TIM5CH1_CAPTURE_VAL;	//输入捕获值
//定时器5中断服务程序	 
void TIM5_IRQHandler(void)
{ 		    
	u16 tsr;
	tsr=TIM5->SR;
 	if((TIM5CH1_CAPTURE_STA&0X80)==0)//还未成功捕获	
	{
		if(tsr&0X01)//溢出
		{	    
			if(TIM5CH1_CAPTURE_STA&0X40)//已经捕获到高电平了
			{
				if((TIM5CH1_CAPTURE_STA&0X3F)==0X3F)//高电平太长了
				{
					TIM5CH1_CAPTURE_STA|=0X80;//标记成功捕获了一次
					TIM5CH1_CAPTURE_VAL=0XFFFF;
				}else TIM5CH1_CAPTURE_STA++;
			}	 
		}
		if(tsr&0x02)//捕获1发生捕获事件
		{	
			if(TIM5CH1_CAPTURE_STA&0X40)		//捕获到一个下降沿 		
			{	  			
				TIM5CH1_CAPTURE_STA|=0X80;		//标记成功捕获到一次高电平脉宽
			    TIM5CH1_CAPTURE_VAL=TIM5->CCR1;	//获取当前的捕获值.
	 			TIM5->CCER&=~(1<<1);			//CC1P=0 设置为上升沿捕获
			}else  								//还未开始,第一次捕获上升沿
			{
				TIM5CH1_CAPTURE_STA=0;			//清空
				TIM5CH1_CAPTURE_VAL=0;
				TIM5CH1_CAPTURE_STA|=0X40;		//标记捕获到了上升沿
	 			TIM5->CNT=0;					//计数器清空
	 			TIM5->CCER|=1<<1; 				//CC1P=1 设置为下降沿捕获
			}		    
		}			     	    					   
 	}
	TIM5->SR=0;//清除中断标志位 	    
}



五、总结

本项目致力于构建一套基于STM32F103RCT6微控制器和NBIOT-BC26通信模块的农作物生长管理系统。系统的核心功能在于实时监测农田环境中的关键参数,例如土壤含水量、环境光照强度、温度与湿度,并据此实现自动化管理。具体而言,系统能够依据预设的阈值自动控制补水,同时允许用户通过本地按钮或远程控制的方式进行手动补水。此外,系统还能根据环境亮度的变化自动调节补光灯的亮度,以保证植物获得适宜的光照条件。

为了保障作物生长环境的安全性,系统配置了环境温湿度传感器(DHT11),并在检测到温度或湿度超出预设范围时触发蜂鸣器报警。这些阈值可以通过手机应用程序进行设定,使得系统更加灵活易用。系统还配备了一块0.96寸的OLED显示屏,用于实时显示环境温度、湿度、土壤含水量以及光照强度等重要参数,使用户能够直观地了解当前的环境状况。

考虑到突然的温度变化可能导致误操作,系统采用了适当的控制策略,在设定温度出现较大波动时延迟系统的反应调节时间,从而避免不必要的误动作。整个设备通过NBIOT-BC26模块连接至华为云IoT平台,并采用MQTT协议上传数据,使得用户能够借助微信小程序远程监控设备状态并进行控制,包括调整设备的温度阈值等设置。

项目利用Qt框架开发了一个Android版的应用程序,进一步增强了系统的远程监控与控制能力。该应用程序不仅可以显示远程监测数据,还可以让用户随时随地控制设备的各项功能。供电方面,系统选择了简便且易于获取的USB线5V供电方案,方便用户安装与使用。

该项目通过集成先进的传感器技术和物联网应用,为现代化农业生产提供了一套高效、便捷且智能化的解决方案。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。