基于STM32单片机设计的矿山环境作业安全监测系统

描述

一、前言

1.1 项目介绍

项目设计里用到的全部工具软件和文档源码,都可以在这里下载。

https://pan.quark.cn/s/145a9b3f7f53

【1】项目开发背景

矿山环境作业安全监测系统的开发背景主要源于对矿井作业环境中潜在危险因素的有效监控需求。矿山作为重要的资源开采场所,其工作环境往往存在诸多安全隐患,如瓦斯爆炸、粉尘超标等,这些因素不仅威胁着矿工的生命安全,还可能导致严重的经济损失和社会影响。因此,建立一个能够实时监测矿井内环境状况,并能在危险发生前及时预警的安全监测系统显得尤为重要。

随着物联网技术的发展,利用先进的传感器技术与无线通信技术相结合,可以实现对矿山环境的全方位监控。本项目选择以STM32F103RCT6单片机作为核心控制器,因其具备高性能、低功耗的特点,非常适合用于此类环境下的数据采集与控制任务。通过集成DHT11温湿度传感器、MQ5气体传感器、PM2.5传感器等,系统能够实时获取环境数据,并依据预设的阈值进行判断,从而采取相应的措施,比如启动通风装置降低瓦斯浓度或通过喷淋系统减少空气中的颗粒物含量。

此外,为了使矿山管理人员能够远程监控矿井内的实际情况,本项目还将通过BC26(NBIOT)模块将收集到的数据上传至华为云物联网平台,实现了数据的云端存储与分析。同时,借助移动应用技术,开发了一款APP,以便于工作人员随时查看环境参数及接收警报信息,进一步增强了系统的实用性和灵活性。

本项目的开发提供一套高效、可靠的矿山环境作业安全监测解决方案,通过技术手段提升矿山安全管理效率,保障矿工的人身安全,促进矿山行业的可持续发展。

STM32

【2】设计实现的功能

(1) 本项目设计的核心是以STM32F103RCT6单片机作为主控单元,负责整个系统的协调控制,实现对矿山环境各项关键参数的监测与管理。

(2) 采用DHT11温湿度传感器进行环境温度和湿度的实时采集,一旦检测到的数值超出安全范围,则通过蜂鸣器发出警报信号。

(3) 使用MQ5气体传感器监测瓦斯浓度,当浓度达到预设阈值时,系统将通过控制继电器启动风扇,以稀释瓦斯浓度。

(4) 配备PM2.5传感器用以检测空气中颗粒物的浓度,当浓度超标时,激活雾化喷淋系统以降低灰尘含量。

(5) 选用OLED显示屏作为人机交互界面,实时显示由各传感器采集到的环境数据。

(6) 利用BC26(NBIOT)模块将现场采集到的数据上传至华为云物联网平台,便于远程监控和数据分析。

(7) 实现自动模式功能,系统能够按照预先设定的阈值自动监测环境参数,并在必要时触发警报或执行相应控制动作,如启动风扇或喷淋系统。

(8) 提供手动模式功能,允许用户通过按键直接控制风扇和雾化降尘设备的开关状态,并且开发了基于Qt框架的Android平台手机APP,以便于远程控制这些设备的运行状态。

(9) 设计中考虑了系统的稳定供电方案,采用5V 2A的外部稳压电源为系统供电。

(10) 风扇和雾化降尘设备均采用5V电源供电,并通过继电器模块实现开关控制。

(11) OLED显示屏采用SPI协议进行数据传输,以确保信息显示的准确性和实时性。

【3】项目硬件模块组成

(1) 控制核心模块:STM32F103RCT6单片机最小系统模块,作为整个监测系统的控制中心。

(2) 温湿度采集模块:DHT11温湿度传感器,用于实时检测环境的温度和湿度。

(3) 气体检测模块:MQ5气体传感器,用于监测环境中的瓦斯浓度。

(4) 颗粒物检测模块:PM2.5传感器,用于检测空气中悬浮颗粒物的浓度。

(5) 显示模块:0.96寸OLED显示屏,采用SPI协议连接至控制核心,显示各项环境参数。

(6) 报警模块:蜂鸣器,当检测到环境参数异常时发出声音警报。

(7) 执行机构控制模块:继电器模块,用于控制风扇和雾化降尘设备的开关状态。

(8) 通风设备:风扇,由继电器控制,用于降低瓦斯浓度。

(9) 降尘设备:雾化喷淋系统,同样由继电器控制,用于减少空气中颗粒物含量。

(10) 远程通信模块:BC26(NBIOT)模块,负责将采集到的数据通过窄带物联网技术上传至云端。

(11) 电源供应模块:5V 2A外部稳压电源,为整个系统提供稳定的电力支持。

(12) 操作接口:按键模块,允许用户手动控制设备的开启与关闭。

(13) 移动终端交互模块:基于Qt开发的Android平台手机APP,实现远程监控和控制功能。

【4】需求总结

项目名称:基于STM32单片机设计的矿山环境作业安全监测系统
1、本次设计以 STM32F103RCT6 单片机最小系统模块作为系统控制核心,确定各种传感器模块选型,完成系统硬件结构设计。
2、采用 DHT11 温湿度采集模块进行环境温湿度检测。当传感器检测到环境值超过控制系统设定的阀值参数时,可触发蜂鸣器报警。
3、采用 MQ5气体传感器来检测环境的瓦斯浓度。当瓦斯浓度达到阈值时,控制系统控制继电器开关模块动作,实现风扇自动控制功能。
4、采用 PM2.5 传感器检测环境中的颗粒物,超过阈值触发报警,可打开雾化模块进行喷淋降低。
5、采用 OLED 显示屏作为显示模块显示实时数据。
6、将采集到的环境信息通过BC26(NBIOT)模块将数据上传到华为云物联网平台。
7、自动模式功能
根据预设的阈值设定,监测环境参数,并在超过阈值时触发警报和控制设备使用定时器进行周期性的环境参数监测。
设计逻辑判断程序,根据环境参数触发不同的处理动作。
8、手动模式功能
实现按键功能,根据用户的操作控制风扇和雾化降尘设备的开关状态。
开发手机 APP,使用Qt作为 Android 平台的开发工具
实现与BC26(NBIOT)模块的通信和数据显示功能。
在手机 APP 上实现远程控制风扇和雾化降尘设备的开关功能以及显示实时监测到的环境参数和警报信息
9、供电采用 5V 2A外部稳压电源
10、风扇和雾化降尘设备采用5V加湿器模块,通过继电器控制开关。
11OLED显示屏采用SPI协议的0.96OLED显示屏

1.2 设计思路

设计思路的核心是围绕提高矿山作业环境的安全性展开,考虑到矿山环境复杂多变的特点,本项目构建一个能够实时监测并有效应对潜在危险因素的自动化系统。该系统的设计从硬件选型到软件架构都遵循了模块化和易维护的原则,确保了系统的可靠性和扩展性。

在硬件层面,选择了性能稳定且易于编程的STM32F103RCT6单片机作为中央处理器,这是因为STM32系列芯片拥有丰富的外设接口,能够方便地与各种传感器和执行机构进行通信。同时,考虑到矿山环境的特殊性,传感器的选择上优先考虑了可靠性与准确性,如DHT11用于温湿度监测,MQ5用于瓦斯浓度检测,PM2.5传感器则用于颗粒物浓度测量。此外,为了实现环境参数的直观展示,选用了OLED显示屏作为人机交互界面,并通过继电器模块来控制风扇和雾化喷淋系统,以应对不同的紧急情况。

软件方面,系统的设计着重于逻辑清晰的程序架构,通过编写高效的算法来处理来自不同传感器的数据,并依据预设的安全阈值进行逻辑判断。当环境参数超出正常范围时,系统会自动触发相应的警报机制,并启动相应的应急措施,例如启动通风设备降低瓦斯浓度或启用喷淋系统减少粉尘。此外,为了便于远程监控,系统集成了BC26(NBIOT)模块,能够将采集到的数据上传至华为云物联网平台,同时开发了配套的手机应用程序,使得管理者能够随时随地查看环境状况,并进行远程控制。

整体而言,该项目的设计思路充分结合了现代物联网技术和传统矿山安全管理的需求,力求通过智能化手段提升矿山作业的安全水平,减少事故发生的可能性,保障矿山工作的顺利进行。

1.3 系统功能总结

功能类别描述
环境监测实时采集矿山环境的温度、湿度、瓦斯浓度、颗粒物浓度等数据。
自动警报当环境参数超过预设安全阈值时,自动触发蜂鸣器警报。
自动控制达到特定阈值时,自动控制风扇和雾化喷淋系统,以降低瓦斯浓度和颗粒物含量。
数据显示OLED显示屏实时显示采集到的各种环境参数。
数据上传通过BC26(NBIOT)模块将环境数据上传至华为云物联网平台,便于远程监控和数据分析。
定时监测使用定时器进行周期性的环境参数监测,确保数据的连续性和及时性。
手动控制用户可以通过按键手动控制风扇和雾化降尘设备的开关状态。
远程控制开发了基于Qt框架的Android平台手机APP,实现远程控制风扇和雾化降尘设备的开关功能。
数据可视化在手机APP上显示实时监测到的环境参数和警报信息。
稳定供电采用5V 2A外部稳压电源为系统提供稳定的电力支持。
设备控制风扇和雾化降尘设备采用5V电源供电,并通过继电器模块实现开关控制。
人机交互OLED显示屏采用SPI协议,保证信息显示的准确性和实时性。

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是有点不适应界面的。

STM32

【2】上位机开发

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

STM32

STM32

1.5 模块的技术详情介绍

【1】BC26-NBIOT模块

BC26-NBIOT模块是一款专为窄带物联网(Narrow Band Internet of Things, NB-IoT)设计的无线通信模块,适用于低功耗广域网络(LPWAN)的应用场景。该模块主要针对物联网市场的需求而开发,尤其适用于那些需要长距离通信、低功耗、低成本和高容量的应用场合,如智能城市、环境监测、智能家居等领域。

BC26-NBIOT模块具备良好的网络覆盖能力,能够在较远的距离内保持稳定的通信连接,这对于矿山环境作业安全监测系统来说至关重要。由于矿山内部结构复杂,通信条件苛刻,传统的无线通信技术可能难以满足要求,而NB-IoT技术凭借其优秀的穿透能力和低功耗特性,可以在这种环境下实现可靠的通信。

BC26-NBIOT模块支持全球主流运营商的NB-IoT频段,这意味着它可以无缝接入不同的网络环境,为用户提供灵活的部署选项。这使得矿山监测系统不仅可以在国内使用,也可以在全球范围内实施,增强了系统的通用性和适用性。

在能耗方面,BC26-NBIOT模块设计有低功耗模式,可以在不活跃期间大幅降低功耗,这对于延长电池寿命或减少系统整体功耗非常重要。尤其是在矿山这样的环境中,由于电源可能不是随时可得,低功耗特性就显得尤为关键。

模块还提供了丰富的接口,包括UART、GPIO、PWM等,便于与其他传感器或执行器进行连接和数据交换。这使得开发者可以根据具体的应用需求,灵活地构建起复杂的物联网系统。同时,BC26-NBIOT模块通常支持AT命令集,这简化了开发过程,使得开发人员能够更快地上手进行开发工作。

BC26-NBIOT模块以其卓越的通信性能、广泛的兼容性和低功耗特性,成为矿山环境作业安全监测系统中的理想选择,能够有效地支持数据的远程传输和系统的远程管理,提高了矿山作业的安全性和管理效率。

【2】DHT11温湿度模块

DHT11温湿度模块是一种经济实惠且广泛使用的数字温湿度传感器,它集成了温度和湿度感应元件以及一个信号转换电路。这款模块因其简单易用、成本低廉而被众多DIY爱好者和专业开发者所青睐,在智能家居、气象站、农业自动化等多种应用场景中都有广泛的应用。

DHT11模块的核心是由一个NTC热敏电阻和一个湿度敏感电容组成的复合传感器。NTC热敏电阻用于检测环境温度的变化,而湿度敏感电容则用于检测空气中的水分含量。这些原始数据经过内部电路的处理后,通过单线串行接口输出给外部微控制器。这种集成化的处理方式大大简化了传感器的使用,使得开发人员无需关心内部的具体实现细节。

在硬件接口方面,DHT11模块通常配备四个引脚,分别是电源正极(VCC)、电源地(GND)、信号输出(DATA)和预留的空引脚。其中,VCC引脚提供工作电压,通常为3.3V到5V之间;GND引脚接地;DATA引脚则是用于与外部微控制器进行数据通信的串行接口。为了保证数据传输的稳定性,通常会在DATA引脚与GND之间接一个上拉电阻。

在软件层面上,DHT11模块的操作相对简单,它遵循一种特定的通信协议。当微控制器想要读取温湿度数据时,需要向DHT11发送一个启动信号,然后等待DHT11回应一个确认信号。之后,DHT11会依次发送湿度整数部分、湿度小数部分、温度整数部分、温度小数部分以及一个校验位。开发人员只需要编写简单的函数来发送启动信号,并接收和解析返回的数据即可。

DHT11模块具有价格优势和易于使用的特性,它的精度不高,湿度测量范围为20%RH至90%RH,精度±5%RH;温度测量范围为0℃至50℃,精度±2℃。

【3】PM2.5粉尘模块

PM2.5粉尘模块是一种专门用于检测空气中细颗粒物(Particulate Matter 2.5,简称PM2.5)浓度的传感器。PM2.5是指直径小于或等于2.5微米的颗粒物,这类颗粒物因为体积小、面积大、活性强,容易携带污染物,对人体健康尤其是呼吸系统有着较大的危害。因此,监测PM2.5浓度对于环境保护和个人健康具有重要意义。

PM2.5粉尘模块通常基于光散射原理工作。当空气中的颗粒物通过传感器时,内置的光源(通常是红外LED)会照射这些颗粒物,导致光的散射。传感器内部装有一个光电二极管,用来接收散射光,并将其转换成电信号。通过分析这些电信号的强度,就可以估算出空气中PM2.5颗粒物的浓度。这种检测方法简单、快速,适用于各种便携式或固定式的空气质量监测设备。

市场上常见的PM2.5粉尘模块如PMS5003、SDS011等,它们通常具备较小的尺寸和较低的功耗,适合集成到各种物联网设备中。这些模块一般都提供标准的串行通信接口(如TTL UART),可以直接与微控制器(如STM32系列)相连,进行数据的读取和处理。此外,一些高级模块还支持I2C或SPI接口,提供更多的配置选项和更高的数据传输速率。

在硬件设计上,PM2.5粉尘模块内部集成了气流通道、光源、光接收器以及信号处理电路。为了保证测量结果的准确性,模块内部通常设有风机来确保空气能够均匀流动并通过传感器区域。此外,为了防止外界干扰,传感器通常会配备有防尘网或过滤器,以保护内部元件不受污染。

从软件角度来看,使用PM2.5粉尘模块相对简单。开发人员只需要按照模块提供的数据手册编写相应的驱动程序,就能实现对模块的初始化和数据读取。大多数模块都会提供一整套的通信协议,其中包括了如何发送查询命令以及如何解析返回的数据格式。例如,一些模块会以ASCII码形式返回数据,包含PM2.5、PM10等不同粒径颗粒物的浓度值,以及其他辅助信息如温度、湿度等。

值得注意的是,虽然PM2.5粉尘模块在一定程度上能够提供准确的颗粒物浓度数据,但在实际应用中,还需要考虑诸如环境温度、湿度等因素对测量结果的影响。此外,为了确保数据的长期稳定性和准确性,定期对传感器进行校准也是非常必要的。

PM2.5粉尘模块作为一种有效的颗粒物浓度监测工具,已经广泛应用于家庭、办公室、工厂等各种环境下的空气质量监测系统中,为人们提供了便捷的方式来监控和改善生活环境质量。

二、BC26-NBIOT模块调试过程

2.1 模块调试接线

STM32

STM32

STM32

2.2 测试模块

第一步接上之后,串口调试助手选择波特率为115200,勾选软件上的发送新行选项。发送AT过去,正常模块会返回OK

只有收到了OK,才表示模块工作正常。

STM32

2.3 上电初始化操作

1】查询模块是否正常
AT

OK


【2】获取卡号,查询卡是否插好
AT+CIMI

460041052911195

OK


【3】激活网络
AT+CGATT=1

OK


【4】获取网络激活状态
AT+CGATT?

+CGATT: 1

OK


【5】查询网络质量
AT+CSQ

+CSQ: 26,0

OK

【6】 检查网络状态
AT+CEREG=?   //
+CEREG: 0,1 //找网成功
OK

2.4 开启GPS定位

如果需要使用GPS定位就开,不需要使用就不用管。

使用GPS定位还需要将模块上的GPS天线接好,否则也是没有信号的。

官方文档:

STM32

1】激活GPS,要等一段时间
AT+QGNSSC=1

OK


【2】查询激活状态,1表示成功激活
AT+QGNSSC?

+QGNSSC: 1

OK


【3】获取一次GPS定位语句
AT+QGNSSRD="NMEA/RMC"
+QGNSSRD: $GNRMC,120715.00,A,3150.78179,N,11711.93433,E,0.000,,310818,,,A,V*19
OK

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

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

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

STM32

3.1 物联网平台介绍

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

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

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

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

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

STM32

3.2 开通物联网服务

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

STM32

点击立即创建

STM32

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

STM32

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

STM32

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

STM32

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

STM32

总结:

端口号:   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:Users11266 >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:Users11266 >

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

3.3 创建产品

(1)创建产品

STM32

(2)填写产品信息

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

STM32

(3)产品创建成功

STM32

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

STM32

(4)添加自定义模型

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

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

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

比如:

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

先点击自定义模型。

STM32

再创建一个服务ID。

STM32

接着点击新增属性。

STM32

STM32

3.4 添加设备

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

(1)注册设备

STM32

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

STM32

(3)保存设备信息

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

STM32

(4)设备创建完成

STM32

(5)设备详情

STM32

STM32

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

STM32

业务流程:

STM32

(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

STM32

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

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

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

以当前设备为例,最终订阅主题的格式如下:
$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

STM32

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

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

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

STM32

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

STM32

(2)生成MQTT三元组

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

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

下面是打开的页面:

STM32

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

直接得到三元组信息。

STM32

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

ClientId  663cb18871d845632a0912e7_dev1_0_0_2024050911
Username  663cb18871d845632a0912e7_dev1
Password  71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237

3.7 模拟设备登录测试

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

(1)填入登录信息

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

STM32

(2)打开网页查看

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

STM32

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

STM32

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

(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}}]}

3.8 创建IAM账户

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

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

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

STM32

STM32

项目凭证:

28add376c01e4a61ac8b621c714bf459

【2】创建IAM用户

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

STM32

点击左上角创建用户

STM32

STM32

STM32

STM32

创建成功:

STM32

【3】创建完成

STM32

用户信息如下:

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

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

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

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

STM32

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

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

STM32

链接如下:

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

四、上位机开发

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

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

4.1 Qt开发环境安装

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

STM32

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编译器即可,其他的不管默认就行,直接点击下一步继续安装。

STM32

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

STM32

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

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

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

4.2 新建上位机工程

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

【1】新建工程

STM32

【2】设置项目的名称。

STM32

【3】选择编译系统

STM32

【4】选择默认继承的类

STM32

【5】选择编译器

STM32

【6】点击完成

STM32

【7】工程创建完成

STM32

4.3 设计UI界面与工程配置

【1】打开UI文件

STM32

打开默认的界面如下:

STM32

【2】开始设计界面

根据自己需求设计界面。

4.5 编译Windows上位机

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

STM32

编译之后的效果:

STM32

4.6 配置Android环境

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

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

【1】选择Android编译器

STM32

STM32

【2】创建Android配置文件

STM32

STM32

STM32

创建完成。

【3】配置Android图标与名称

STM32

【3】编译Android上位机

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

然后点击构建。

STM32

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

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

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

STM32

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代码开发

5.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, verbosen");
	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 testn");
		}
		else if (strcmp(argv[count], "--subscribe_failure_test") == 0 || strcmp(argv[count], "-s") == 0)
		{
			options.run_subscribe_failure_test = 1;
			printf("Running subscribe failure testn");
		}
		else if (strcmp(argv[count], "--connection") == 0)
		{
			if (++count < argc)
			{
				options.connection = argv[count];
				printf("Setting connection to %sn", options.connection);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--clientid1") == 0)
		{
			if (++count < argc)
			{
				options.clientid1 = argv[count];
				printf("Setting clientid1 to %sn", options.clientid1);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--clientid2") == 0)
		{
			if (++count < argc)
			{
				options.clientid2 = argv[count];
				printf("Setting clientid2 to %sn", options.clientid2);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--username") == 0)
		{
			if (++count < argc)
			{
				options.username = argv[count];
				printf("Setting username to %sn", options.username);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--password") == 0)
		{
			if (++count < argc)
			{
				options.password = argv[count];
				printf("Setting password to %sn", options.password);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--MQTTversion") == 0)
		{
			if (++count < argc)
			{
				options.MQTTVersion = atoi(argv[count]);
				printf("Setting MQTT version to %dn", options.MQTTVersion);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--iterations") == 0)
		{
			if (++count < argc)
			{
				options.iterations = atoi(argv[count]);
				printf("Setting iterations to %dn", options.iterations);
			}
			else
				usage();
		}
		else if (strcmp(argv[count], "--verbose") == 0)
		{
			options.verbose = 1;
			printf("nSetting verbose onn");
		}
		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("%sn", 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: %sn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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 %dn", 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");
}

5.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();
}

5.3 PM2.5与MQ5采集代码

#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;
}

5.4 DHT11温湿度采集代码

#include "dht11.h"
#include "delay.h"
//IO方向设置
#define DHT11_IO_IN()  {GPIOG- >CRH&=0XFFFF0FFF;GPIOG- >CRH|=8< < 12;}
#define DHT11_IO_OUT() {GPIOG- >CRH&=0XFFFF0FFF;GPIOG- >CRH|=3< < 12;}
////IO操作函数											   
#define	DHT11_DQ_OUT PGout(11) //数据端口	PG11 
#define	DHT11_DQ_IN  PGin(11)  //数据端口	PG11


u8 DHT11_Init(void);		//初始化DHT11
u8 DHT11_Read_Data(u8 *temp,u8 *humi);//读取温湿度
u8 DHT11_Read_Byte(void);	//读出一个字节
u8 DHT11_Read_Bit(void);	//读出一个位
u8 DHT11_Check(void);		//检测是否存在DHT11
void DHT11_Rst(void);		//复位DHT11    


//复位DHT11
void DHT11_Rst(void)	   
{                 
	DHT11_IO_OUT(); 	//SET OUTPUT
    DHT11_DQ_OUT=0; 	//拉低DQ
    delay_ms(20);    	//拉低至少18ms
    DHT11_DQ_OUT=1; 	//DQ=1 
	delay_us(30);     	//主机拉高20~40us
}
//等待DHT11的回应
//返回1:未检测到DHT11的存在
//返回0:存在
u8 DHT11_Check(void) 	   
{   
	u8 retry=0;
	DHT11_IO_IN();//SET INPUT	 
    while (DHT11_DQ_IN&&retry< 100)//DHT11会拉低40~80us
	{
		retry++;
		delay_us(1);
	};	 
	if(retry >=100)return 1;
	else retry=0;
    while (!DHT11_DQ_IN&&retry< 100)//DHT11拉低后会再次拉高40~80us
	{
		retry++;
		delay_us(1);
	};
	if(retry >=100)return 1;	    
	return 0;
}
//从DHT11读取一个位
//返回值:1/0
u8 DHT11_Read_Bit(void) 			 
{
 	u8 retry=0;
	while(DHT11_DQ_IN&&retry< 100)//等待变为低电平
	{
		retry++;
		delay_us(1);
	}
	retry=0;
	while(!DHT11_DQ_IN&&retry< 100)//等待变高电平
	{
		retry++;
		delay_us(1);
	}
	delay_us(40);//等待40us
	if(DHT11_DQ_IN)return 1;
	else return 0;		   
}
//从DHT11读取一个字节
//返回值:读到的数据
u8 DHT11_Read_Byte(void)    
{        
    u8 i,dat;
    dat=0;
	for (i=0;i< 8;i++) 
	{
   		dat< <=1; 
	    dat|=DHT11_Read_Bit();
    }						    
    return dat;
}
//从DHT11读取一次数据
//temp:温度值(范围:0~50°)
//humi:湿度值(范围:20%~90%)
//返回值:0,正常;1,读取失败
u8 DHT11_Read_Data(u8 *temp,u8 *humi)    
{        
 	u8 buf[5];
	u8 i;
	DHT11_Rst();
	if(DHT11_Check()==0)
	{
		for(i=0;i< 5;i++)//读取40位数据
		{
			buf[i]=DHT11_Read_Byte();
		}
		if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4])
		{
			*humi=buf[0];
			*temp=buf[2];
		}
	}else return 1;
	return 0;	    
}
//初始化DHT11的IO口 DQ 同时检测DHT11的存在
//返回1:不存在
//返回0:存在    	 
u8 DHT11_Init(void)
{
	RCC- >APB2ENR|=1< < 8;    //使能PORTG口时钟 
	GPIOG- >CRH&=0XFFFF0FFF;//PORTG.11 推挽输出
	GPIOG- >CRH|=0X00003000;
	GPIOG- >ODR|=1< < 11;      //输出1				    
	DHT11_Rst();
	return DHT11_Check();
}

六、总结

该项目开发一种基于STM32单片机的矿山环境作业安全监测系统,以提高矿山作业的安全性和效率。系统集成了多种传感器,包括DHT11温湿度传感器、MQ5气体传感器和PM2.5传感器,用于实时监测矿山环境中的关键参数,如温度、湿度、瓦斯浓度和颗粒物含量。通过这些传感器,系统能够及时发现潜在的安全隐患,并采取必要的预防措施。

在硬件设计方面,系统采用了STM32F103RCT6单片机作为核心控制器,配合蜂鸣器、继电器模块、OLED显示屏等组件,形成了一个完整的监测与控制系统。当检测到的环境参数超过预设的安全阈值时,系统能够自动触发报警,并通过控制风扇和雾化喷淋系统来降低瓦斯浓度和颗粒物含量,从而保障矿工的生命安全。

此外,为了实现远程监控与管理,项目还引入了BC26(NBIOT)模块,将环境数据上传至华为云物联网平台,并开发了一款基于Qt框架的Android平台手机应用程序。该应用程序不仅能够实时显示环境参数,还能接收警报信息,并允许用户远程控制风扇和雾化降尘设备的开关状态,极大地提升了系统的实用性和灵活性。

该矿山环境作业安全监测系统通过集成先进的传感技术和物联网技术,实现了对矿山环境的全方位监控,能够在危险发生之前提供预警,并采取有效的防护措施,对于提升矿山作业的安全管理水平具有重要意义。

审核编辑 黄宇

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分