Author:teacherXue
一、什么是MQTT
定义
MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。——来自于百度百科。
特点
MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控制设备通讯而设计的协议,它具有以下主要的几项特性:
使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合;
对负载内容屏蔽的消息传输;
使用 TCP/IP 提供网络连接;
有三种消息发布服务质量;
小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量;
使用 Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。
基本工作结构
通过百度百科的解释,我们可以看到其运行模式非常适合物联网场景中的大量不同的物联网终端和其他应用端的数据交互需求。而运行该协议服务的服务器就称之为MQTT服务器。
其基本工作结构如下图(不考虑终端用户鉴权资源隔离等情况下),MCU硬件端(MCU端可以是任何具备智能网关功能的设备,ESP芯片、开发板、手机、甚至于PC)可以任意数量接入平台,发送任意传感器数据,并接受相关控制指令。同时遵循本协议开发的应用端可以自动匹配所接入的mcu以及相应的传感器和控制方式。平台结构图如下:
![](https://i-blog.csdnimg.cn/blog_migrate/fbe242ec570c855cba3f373264e32a22.png)
相关角色
1)MCU
微控制器单元,为具备互联网连接能力,以上报传感器数据为主要任务目标的,任意类型物联网接入端,如鸿蒙平台、esp平台、手机平台等。
2)物联网服务器
部署于公网之上的,提供mqtt消息的订阅和用户角色鉴权服务的平台,不进行存储,以获得最高的服务性能。
3)应用端
通过mqtt协议,获得相应mcu端数据,并根据业务的具体需要,提供数据的展示交互控制能力,可以是java、c#、python任意类型开发语言,业务逻辑由应用端实现。
二、mqtt消息模板和规范
大量的设备和应用都要通过MQTT消息来传输数据。那么我们需要保证数据结构的一致性,才可以保证其良好的通用性。如果使用第三方的MQTT物联网平台,则其本身对消息的协议标准还有具体要求。
本例中采用自己部署在公网上的MQTT服务器,因此协议的重点放在产品模板规定上,并适配了具有不同权限和应用的订阅地址进行隔离,有需要的同学可以参考标准订阅方式。
mqtt服务器
如何构建服务器不是本章的重点,个人使用的是Eclipse Mosquitto。推荐使用cedalo综合平台,它包括一个Eclipse Mosquitto物联网服务,一个cedalo基于web的图形化管理web终端,以及EclipseStreamsheets数据可视化定制平台。其采用docker方式进行部署非常的便捷。
其安装也比较简单,重点是官方文档非常详细,提供windows、macos、树莓派、linux多种平台的安装教程。官网地址https://cedalo.com/ ,文档地址https://docs.cedalo.com/mosquitto/installation/。有需要的同学可以自己部署服务,但公网部署就要自己想办法了。
本次实验消息订阅规范
本文中公网服务器地址为实训中对学校提供,不对外无偿使用。以下协议规范考虑到了多学校、多班级、多组的共同实训,网络上的同学们需根据自己的情况修改。
1) 物联网服务器连接
访问地址:xue1024.XXXX.cn
端口:1883
通信协议标准:mqtt5.0、3.0、2.0
2)以下账户为实训院校账户
MCU用户名:院校缩写_年级_mcu
应用端用户名:院校缩写_年级_stu
密码:XXXXXXXX
3)订阅地址
消息,按不同组织、用图、产品线通过订阅地址进行区分,并按账户角色进行访问权限隔离,基础格式为:
MCU发送消息订阅地址(数据消息获得订阅与此相同):iss/行业/产品/平台/data/芯片ID(进行用户区分时,为iss/行业/产品/平台/data/用户token/芯片ID)。
应用端控制指令发送地址(MCU端控制指令订阅与此相同):iss/行业/产品/平台/order/芯片ID(进行用户区分时,为iss/行业/产品/平台/order/用户token/芯片ID)。
4)某院校实训平台订阅地址使用如下(有实训需求的院校可以和我联系):
MCU数据发布地址:iss/lot/院校缩写_年级/mcu/data/两位组号/MCU_ID/
控制指令订阅地址:iss/lot/院校缩写_年级/mcu/order/两位组号/MCU_ID/
MCU端发送数据消息协议结构
此协议定义传输的数据如何封装和解析的规范,遵循本协议的MCU端和应用端可自动任意数量和传感器类型适配,开发者如使用自己的规范标准,需统MCU和应用端适配,消息长度超过部分MCU端消息默认长度,需对应修改。
协议规范标注 |
|
不含注释版本 |
|
协议项说明
协议名 | 作用 | 值列表 |
protocol | 当前协议版本,为解析依据 | 1.0 |
chip_id | MCU唯一识别编号,为MAC地址,同时为MCU端的连接ID | Iss_十六进制 |
chip_type | //mcu型号,业务处理备用 |
|
product_line | 所属产品线,用以区分场景 | 智慧农业、智慧家居、智慧园区… |
timestamp | 数据发送时间戳,long型 |
|
public_ip | MCU接入时的公网IP地址,可以大数据做地域分析。应用端可以根据地域查询地域公共信息。 |
|
private_ip | MCU接入局域网IP地址,应用端如需管理设备时使用。 |
|
user_name | 用户名,应用端做进一步鉴权使用 |
|
user_token | 用户访问令牌 |
|
sensor_list[ ] | MCU接驳传感器列表,应用端获得MCU所挂载的传感器集合 | 传感器 |
{ sensor_list }number | 传感器在MCU上管理的序号,MCU端唯一。 |
|
{ sensor_list }type | 传感器型号,应用端做界面构建时的类型判断。 | 例如lamp(灯)、led(led灯)、T(温度)、H(湿度)、PW(电源)、RGB(三色全彩灯)、LUX(光照)、GAS(气体)、curtain(窗帘)、motor(电机)、lamp_n(不可调光),voice(声音),flame(火焰),smoke(烟雾),PIR(红外遥控器),buzzer(蜂鸣器),body(人体),knob(旋钮控制器),water(水位)、soil(土壤)、hm(距离)、valve(阀门控制),具体取值参考设备名称对应表。 |
{ sensor_list }name | 传感器名字,应用端做界面构建时控制类型判断 |
|
{ sensor_list }message | 返回消息备用 | 字符长度小于等于50 |
{ sensor_list }data | 传感器数据值,用于返回检测数据 | data_vals子对象集合 |
{ sensor_list }.{data}data_vals[ ] | 数据列表,单一传感器有多个值统一控制时, | 例如灯光R、G、B、亮度。颜色名称、亮度等 |
{sensor_list}.{data}.[data_vals] data_name | 传感器数据对应名称 | 例如可控灯的亮度、色温。具体取值参考控制参数名称对应列表。 |
{ sensor_list }.{data}.[data_vals]data_val | 传感器对应数据值。 |
|
{ sensor_list }.{data}.[data_vals]max_val | 该数据的上阈值 |
|
{ sensor_list }.{data}.[data_vals]min_val | 该数据的下阈值 |
|
{ sensor_list }.{data}data_type | 传感器数据类型,是否可读可控 | r w rw |
{ sensor_list }.{data}effective_time | 数据发送后是否有有效时间 | 毫秒为单位的长整形 |
{ sensor_list }.{data}data_state | 该传感器此数据项状态 | 开、关、不可控,具体取值参考控制参数名称对应列表。 |
{ sensor_list }.{ tasks }[ ] | 当前传感器任务集合,集合方式数量任意,任务分单次任务和周期重复任务。此处为微控制器回传数据,任务列表作为应用端解析显示使用,无此需求的可以不用回传。 | 任务项集合 |
{ sensor_list }.{ tasks }[ ] task_type | 任务类型 | 任务类型,单次任务\周期任务\延迟任务,single/repeat/delay,具体取值参考控制参数名称对应列表。 |
{ sensor_list }.{ tasks }[ ] task_number | 任务编号,当传感器唯一整数 |
|
{ sensor_list }.{ tasks }[ ] task_name | 任务名称 |
|
{ sensor_list }.{ tasks }[ ] task_state | 任务状态 | 运行中、停止中、失效的(执行端因故未能运行),run/stop/invalid,具体取值参考控制参数名称对应列表。 |
{ sensor_list }.{ tasks }[ ] delay_time | 任务状态为延时,延期执行时间,单位毫秒,也可以作为任务执行时长 | 毫秒为单位的长整形 |
{ sensor_list }.{ tasks }[ ] week_day[ ] | 任务状态为repeat时,覆盖每周几 ,全部则是每天 ,单次和延期任务时无此项目 | 0~6序列,代表周日到周六。 0,1,2,3,4,5,6 |
{ sensor_list }.{ tasks }[ ] start_time | 任务开始时间,长整形,当前日期零时长整形到具体时间的差值,如时间段任务设置,需增加开始和结束两项任务. |
|
{ sensor_list }.{ tasks }[ ] single_time | 单次任务预定执行时间,长整形,如时间段任务设置,需增加开始和结束两项任务 |
|
{ sensor_list }.{ tasks }[ ] exec_order[ ] | 任务指令,具体被控制的属性和值 | 控制项:控制值 |
{ sensor_list }.{ tasks }[ ] exec_order[ ] order_name | 控制项名称, | 如转速、预定角度、亮度,具体取值参考控制参数名称对应列表。 |
{ sensor_list }.{ tasks }[ ] exec_order[ ] order_val | 控制值 |
|
传感器名称和数据对应表
以下规范为传感器数据特性,非产品本身,产品可以是多个传感器控制器的封装。MCU端提报数据时按此协议规范,包括传感器数据和控制器当前状态数据。开发人员可根据自身平台设备进行扩充定义,如命名方式尊循该规范可以获得更好的适配性(on/off no/yes 为布尔型)。
以下规范为传感器数据特性,非产品本身,产品可以是多个传感器控制器的封装。MCU端提报数据时按此协议规范,包括传感器数据和控制器当前状态数据。开发人员可根据自身平台设备进行扩充定义,如命名方式尊循该规范可以获得更好的适配性(on/off no/yes 为布尔型)。
智能家居类别
| 名称 | 设备类型 | 值类型 | 控制参数说明 |
灯光 控制类 | lamp_no | 不可调光灯光 | 开关状态 | data_vals[{"data_name": “state” ,"data_val":“on/off”}] |
lamp_ctl | 可调光 | 开关状态 百分比亮度值 | data_vals[ {"data_name": “state” ,"data_val": “on/off”}, {"data_name": “extent” ,"data_val": “0~100”} ] | |
lamp_cct | 色温可调灯光 | 开关状态 百分比亮度值 百分比色温值 | data_vals[ {"data_name": “state” ,"data_val": “on/off”}, {"data_name": “extent” ,"data_val": “0~100”}, {"data_name": “cct_val” ,"data_val": “0~100”} ] | |
lamp_rgb | 全彩可调灯光 | 开关状态 百分比亮度值 百分比色温值 颜色名称/ 红色通道 绿色通道 蓝色通道/ H色调通道 S饱和度通道 V明度通道 | data_vals[ {"data_name": “state” ,"data_val": “on/off”}, {"data_name": “extent” ,"data_val": “0~100”}, {"data_name": “cct_val” ,"data_val": “0~100”}, {"data_name": “color”,"data_val": “red/blue/golden…标准颜色名称”}, {"data_name": “r_val” ,"data_val": “0~255”}, {"data_name": “g_val” ,"data_val": “0~255”}, {"data_name": “b_val” ,"data_val": “0~255”}, {"data_name": “h_val” ,"data_val": “0~255”}, {"data_name": “s_val” ,"data_val": “0~255”}, {"data_name": “v_val” ,"data_val": “0~255”} ] | |
lamp_pro | 程控灯光 | 由对应设备开发人员自行定义控制项目 |
| |
开关伺服 控制类 | pw_sw | 电源开关 | 开关状态 | data_vals[{"data_name": “state” ,"data_val": “on/off”}] |
knob_ctl | 旋转开关 | 开关状态 开关幅度 | data_vals[ {"data_name": “state” ,"data_val": “on/off”}, {"data_name": “extent” ,"data_val": “0~100”} ] | |
valve_ctl | 可控阀门 | 阀门打开程度 | data_vals[{"data_name": “state” ,"data_val":“0~100”}] | |
curtain_ctl | 可控窗帘 | 窗帘打开幅度 | data_vals[{"data_val": “0~100”}] | |
jalousie_ctl | 可控百叶窗帘 | 窗帘打开幅度 百叶方向 百叶偏转幅度 | data_vals[ {"data_name": “extent” ,"data_val": “0~100”}, {"data_name": “direction” ,"data_val": “up/down”}, {"data_name": “angle” ,"data_val": “0~100”} ] | |
window_ctl | 可控窗户 | 窗户打开幅度 | data_vals[{"data_val": “0~100”}] | |
buzzer | 蜂鸣器 | 蜂鸣状态 | data_vals[{"data_val": “on/off”}] | |
servo | 舵机执行器 | 舵机角度 | data_vals[ {"data_val": “浮点数”} ] | |
step_motor | 步进电机 | 当前步数 目标步数 速度 | data_vals[ {"data_name": “val” ,"data_val": “整数}, {"data_name": “target” ,"data_val": “整数}, {"data_name": “speed” ,"data_val": “整数”} ] | |
door_ctl | 可控门 | 门打开幅度 | data_vals[{"data_val": “0~100”}] | |
数据传感器类 | temp | 温度传感器 | 温度值 | data_vals[{"data_val": “有符号浮点数”}] |
hum | 湿度传感器 | 湿度值 | data_vals[{"data_val": “有符号浮点数”}] | |
th | 温湿度传感器 | 温度值 湿度值 | data_vals[ {"data_name": “t_val” ,"data_val": “有符号浮点数”}, {"data_name": “h_val” ,"data_val": “有符号浮点数”} ] | |
lx | 光照强度传感器 | 照度值 | data_vals[ {"data_val": “整数”}] | |
voice | 声音传感器 | 声响响应 模拟量值 | data_vals[ {"data_name": “rps” ,"data_val": “no/yes”}, {"data_name": “val” ,"data_val": “浮点数”} ] | |
smoke | 烟雾传感器 | 烟雾响应 模拟量值 | data_vals[ {"data_name": “rps” ,"data_val": “no/yes”}, {"data_name": “val” ,"data_val": “浮点数”} ] | |
body | 人体传感器 | 人体响应 | data_vals[{"data_val": “no/yes”}] | |
flame | 火焰传感器 | 火焰响应 模拟量值 | data_vals[{"data_name": “rps” ,"data_val": “no/yes”}, {"data_name": “val” ,"data_val": “浮点数”} ] | |
water | 水传感器 | 浸润响应 模拟量值 | data_vals[{"data_name": “rps” ,"data_val": “no/yes”}, {"data_name": “val” ,"data_val": “浮点数”} ] | |
water_lv | 水位传感器 | 水位值 | data_vals[{"data_val": “有符号浮点数”}] | |
raindrop | 雨滴传感器 | 下雨状态 模拟量值 | data_vals[ {"data_name": “rps” ,"data_val": “no/yes”}, {"data_name": “val” ,"data_val": “浮点数”} ] | |
hpa | 气压传感器 | 气压值 | data_vals[{"data_val": “有符号浮点数”}] | |
fuelgas | 燃气传感器 | 燃气报警 模拟量值 | data_vals[ {"data_name": “rps” ,"data_val": “no/yes”}, {"data_name": “val” ,"data_val": “浮点数”} ] | |
soil | 土壤传感器 | 土壤湿度响应 模拟量值 | data_vals[ {"data_name": “rps” ,"data_val": “no/yes”}, {"data_name": “val” ,"data_val": “浮点数”} ] | |
hm | 距离传感器 | 距离响应状态 距离值 距离单位 | data_vals[ {"data_name": “rps” ,"data_val": “no/yes”}, {"data_name": “val” ,"data_val": “浮点数”}, {"data_name": “unit” ,"data_val": “value”} ] | |
voltage | 电压传感器 | 电压值 | data_vals[ {"data_val": “浮点数”} ] | |
current | 电流传感器 | 电流值 | data_vals[ {"data_val": “浮点数”} ] |
应用端指令发送协议结构
协议规范 |
{ //芯片ID "chip_id":value, //发送时间戳 "timestamp":value, //指令列表 "task_list":[ //指令1 { //要控制的外设编号,和MCU端外设匹配 "number":value, //控制项和控制值,处理时要先判断是否有值,再进行处理 "控制项1": value }, //指令2 { //要控制的外设编号,和MCU端外设匹配 "number": value, //控制项和控制值,处理时要先判断是否有值,再进行处理 "控制项1": value, //可以有多个控制项,控制项参考传感器数据定义协议 "控制项2": value } ]} |
案例 |
{ "chip_id": "xm_00749E03", "timestamp": 1677856857, "task_list": [{ "number": 1, "state": true }, { "number": 2, "state": true, "extent": 30 } ] } |
三、ESP8266实现MQTT消息发送
下面我们将按照预定标准,使用之前章节构建的平台进行mqtt数据的发送。此实验网络中上的小伙伴需自行解决MQTT服务器问题,可以自己安装。
创建项目
1)新建项目Lot_mqtt_json_test_v2.0,我们将在上一章节项目上修改。
2)配置串口波特率115200
3)需要通过扩展库管理器安装如下扩展库
DHT sensor library
ArduinoJson
TaskScheduler
JC_Button
NTPClient
PubSubClient:MQTT订阅扩展库,本例中数据较大,初始化时需定义缓冲大小。
4)在项目lib文件夹本地安装库的方式安装以下扩展库:
WiFiManager:如果无高需求,也可以采用上面扩展库安装方式。
代码解析
1)在上一章节项目中增加头文件导入mqtt消息库,以及网络时间服务的NTPClient,以及所需的UDP库
#include <Arduino.h>
#include <JC_Button.h>
#include <WiFiManager.h>
// 导入多任务库
#include <TaskScheduler.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
// 导入NTP网络时间服务扩展库
#include <NTPClient.h>
// 导入UDP扩展库,
#include <WiFiUdp.h>
// json对象处理类,6.x 和5.x不一样 ,这里用的是6.19
#include <ArduinoJson.h>
#include <PubSubClient.h> // mqtt消息订阅库
2)声明mqtt消息服务所需的参数
// mqtt服务器连接信息
// xue1024.tpddns.cn
const char *mqttServer = "xue1024.XXXXX.cn"; // 个人mqtt服务器
const int mqttPort = 1883; // mqtt端口号
const char *mqttUser = "院校缩写_年级_mcu"; // 用户名
const char *mqttPassword = "你的密码"; // 密码
3)准备存储订阅发送和订阅接收的消息地址缓冲
// 传感器数据订阅地址,mcu向里面发数据,客户端去里面取数据
char outTopic[50]; // 订阅发送地址
// 控制指令订阅地址,mcu订阅,客户端发送
char inTopic[50]; // 订阅接收地址缓存
4)创建新增扩展库对象
// mqtt连接对象
WiFiClient espClient;
PubSubClient client(espClient);
// 创建UDP对象
WiFiUDP ntpUDP;
//NTP时间服务器对象
NTPClient timeClient(ntpUDP, "ntp.aliyun.com");
5)函数前置声明,其中callback是mqtt接收到消息后执行的回调函数,reconnect为保持mqtt连接的方法,publishMQTT为发送消息的方法。
// 前置函数声明
void getTH();
void getKnob();
void getJson();
void callback(char *topic, byte *payload, unsigned int length); // mqtt回调函数
void reconnect(); // mqtt保持连接
void publishMQTT(); // 发送mqtt数据
6)上一章节的getJson不在需要任务调度,由消息发送时调度生成即可。
Task t3(5000, TASK_FOREVER, &publishMQTT); // 任务名称t3,间隔5秒,一直执行。
7)Begin方法中增加ntp时间服务器对象的初始化,并调整中国地区的时区差,标准时区+8。
// 初始化NTP时间服务器连接
timeClient.begin();
// 设置时区偏差,中国地区偏差+8
// GMT +1 = 3600
// GMT +8 = 28800
// GMT -1 = -3600
// GMT 0 = 0
timeClient.setTimeOffset(28800);
8)Setup方法内增加订阅消息地址的完善,修改mqtt消息大小和连接超时时间。
//-------------- 订阅地址(xm替换为组号)--------------
sprintf(inTopic, "iss/lot/xust_19/mcu/order/两位组号/%s/", chipId);
sprintf(outTopic, "iss/lot/xust_19/mcu/data/两位组号/%s/", chipId);
Serial.printf("inTopic:%s\n", inTopic);
Serial.printf("outTopic:%s\n", outTopic);
// mqtt连接
client.setServer(mqttServer, mqttPort);
client.setBufferSize(2048);//设置mqtt消息传输包的大小
client.setSocketTimeout(60);//设置mqtt连接超时
client.setCallback(callback);//接收到消息后的回调处理方法
//-----------------------------------------------
9) 保持mqtt连接的方法,首次连接时也是此方法
// mqtt保持连接方法
void reconnect()
{
// Loop until we're reconnected
while (!client.connected())
{
Serial.print("Attempting MQTT connection...");
// Attempt to connect
// chipId 在这里是客户端连接的sessionid,同账户名下,不能相同,这里用芯片id
String clientId = String(chipId)+"-"+String(random(0xffff), HEX);
if (client.connect(clientId.c_str(), mqttUser, mqttPassword))
{
Serial.println("connected...");
}
else
{
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);//如果失败延迟五秒后连接
}
}
}
10) 发布mqtt消息的方法
// 发布mqtt消息
void publishMQTT()
{
getJson(); // 生成要发送的json数据
//保持连接
if (!client.connected())
{
reconnect();
}
client.loop();
// 发送mqtt数据
client.publish(outTopic, messageBuff, true);
// Serial.println(messageBuff);
//client.subscribe(inTopic);
//关闭连接
client.endPublish();
// 发送完成后清除缓存数据
memset(messageBuff, 0, 1024);
}
11) 完整代码
#include <Arduino.h>
#include <JC_Button.h>
#include <WiFiManager.h>
// 导入多任务库
#include <TaskScheduler.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
// 导入NTP网络时间服务扩展库
#include <NTPClient.h>
// 导入UDP扩展库,
#include <WiFiUdp.h>
// json对象处理类,6.x 和5.x不一样 ,这里用的是6.19
#include <ArduinoJson.h>
#include <PubSubClient.h> // mqtt消息订阅库
#define LED_1 D5
#define LED_2 D6
#define BUTTON_PIN D4
#define DHTPIN D3 // DHT温湿度传感器引脚
#define analogInPin A0 // 模拟输入引脚A0
#define DHTTYPE DHT22 // 声明DHT传感器类型
DHT dht(DHTPIN, DHTTYPE); // 创建DHT对象
Scheduler runner; // 任务调度器对象
Button myBtn(BUTTON_PIN); // 按钮对象
// 温湿度值全局保存
float t = 0.0;
float h = 0.0;
// led灯光状态
bool led1state = false;
bool led2state = false;
// 旋钮值和灯光亮度的保存
unsigned int knobValue = 0;
unsigned int dutyCycle = 0;
// 存放芯片ID的缓冲
char chipId[10];
// 消息发送缓冲,json最后字符串方式传输
char messageBuff[1024];
// mqtt服务器连接信息
// xue1024.tpddns.cn
const char *mqttServer = "xue1024.tpddns.cn"; // 个人mqtt服务器
const int mqttPort = 1883; // mqtt端口号
const char *mqttUser = "shixun_admin"; // 用户名
const char *mqttPassword = "teacherxue"; // 密码
// long mqtt_interval = 2000;
// long mqtt_comiit = 0;
// 传感器数据订阅地址,mcu向里面发数据,客户端去里面取数据
char outTopic[50]; // 发送地址
// 控制指令订阅地址,mcu订阅,客户端发送
char inTopic[50]; // 订阅地址缓存
// mqtt连接对象
WiFiClient espClient;
PubSubClient client(espClient);
// 创建UDP对象
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntp.aliyun.com");
// 前置函数声明
void getTH();
void getKnob();
void getJson();
void callback(char *topic, byte *payload, unsigned int length); // mqtt回调函数
void reconnect(); // mqtt保持连接
void publishMQTT(); // 发送mqtt数据
Task t1(2000, TASK_FOREVER, &getTH); // 任务名称t1,间隔2秒一直执行.
Task t2(30, TASK_FOREVER, &getKnob); // 任务名称t2,间隔30毫秒,一直执行。
Task t3(5000, TASK_FOREVER, &publishMQTT); // 任务名称t3,间隔5秒,一直执行。
// Task t3(3000, TASK_FOREVER, &getJson);
void setup()
{
Serial.begin(115200);
pinMode(LED_1, OUTPUT);
pinMode(LED_2, OUTPUT);
// led1为关灯
digitalWrite(LED_1, LOW);
// led2使用pwm调光
analogWrite(LED_2, 0);
dht.begin(); // DHT传感器对象工作
myBtn.begin(); // button按钮运作
WiFiManager wm; // wifi管理对象,配网用
bool res;
// 拼接芯片的hostname
sprintf(chipId, "xm_%08X", ESP.getChipId());
res = wm.autoConnect(chipId, "12345678"); // 密码认证模式的AP
if (!res)
{
Serial.println("Failed to connect");
// ESP.restart();
}
else
{
// if you get here you have connected to the WiFi
WiFi.setHostname(chipId); // 从模式后设置设备名
Serial.println("connected...yeey :)");
}
// 初始化NTP时间服务器连接
timeClient.begin();
// 设置时区偏差,中国地区偏差+8
// GMT +1 = 3600
// GMT +8 = 28800
// GMT -1 = -3600
// GMT 0 = 0
timeClient.setTimeOffset(28800);
// 初始化任务调度器,规划任务链
runner.init();
runner.addTask(t1);
runner.addTask(t2);
runner.addTask(t3);
t1.enable();
t2.enable();
t3.enable();
//-------------- 订阅地址(xm替换为组号)-------------------
sprintf(inTopic, "iss/lot/xust_19/mcu/order/xm/%s/", chipId);
sprintf(outTopic, "iss/lot/xust_19/mcu/data/xm/%s/", chipId);
Serial.printf("inTopic:%s\n", inTopic);
Serial.printf("outTopic:%s\n", outTopic);
// mqtt连接
client.setServer(mqttServer, mqttPort);
client.setBufferSize(2048);//设置mqtt消息传输包的大小
client.setSocketTimeout(60);//设置mqtt连接超时
client.setCallback(callback);
//-----------------------------------------------
}
void loop()
{
runner.execute(); // 执行任务
// 处理按钮事件
myBtn.read();
if (myBtn.wasPressed())
{
led1state = !led1state;
}
digitalWrite(LED_1, led1state);
analogWrite(LED_2, dutyCycle); // 根据状态控灯
}
// 获得温湿度
void getTH()
{
h = dht.readHumidity();
// Read temperature as Celsius (the default)
t = dht.readTemperature();
}
// 旋钮操作
void getKnob()
{
// 关灯时不做任何调整
// if (!led2state)
// {
// return;
// }
knobValue = analogRead(analogInPin);
dutyCycle = map(knobValue, 15, 1008, 0, 255);
}
// 获得json封装的数据
void getJson()
{
//------------封装json对象传递----------------------------
/* 申明一个大小为1K的DynamicJsonDocument对象JSON_Buffer,
用于存储反序列化后的(即由字符串转换成JSON格式的)JSON报文,
*/
// DynamicJsonDocument doc(2048);
// 创建json对象
StaticJsonDocument<1024> doc;
// 创建json根节点对象
JsonObject root = doc.to<JsonObject>();
// root节点下创建子节点并赋值
root["protocol"] = "1.0";
root["chip_id"] = chipId;
root["chip_type"] = "ESP8266-12E";
root["product_line"] = "xust_19_teacher";
root["private_ip"] = WiFi.localIP();
root["public_ip"] = "";
timeClient.update();
root["timestamp"] = timeClient.getEpochTime();
// 创建json对象集合,存放该mcu节点下的所有传感器列表
JsonArray sensors = root.createNestedArray("sensor_list");
// 集合节点,创建子节点对象
// 1.不可调光主灯
JsonObject s1 = sensors.createNestedObject();
s1["number"] = 1;
s1["type"] = "amp_no";
s1["name"] = "厨房灯";
JsonObject s1_data = s1.createNestedObject("data");
JsonArray s1_data_ls = s1_data.createNestedArray("data_vals");
JsonObject s1_data1 = s1_data_ls.createNestedObject();
s1_data1["data_name"] = "state";
s1_data1["data_val"] = led1state;
// 2.可调光主灯
JsonObject s2 = sensors.createNestedObject();
s2["number"] = 2;
s2["type"] = "lamp_ctl";
s2["name"] = "主卧灯";
JsonObject s2_data = s2.createNestedObject("data");
JsonArray s2_data_ls = s2_data.createNestedArray("data_vals");
JsonObject s2_data1 = s2_data_ls.createNestedObject();
s2_data1["data_name"] = "state";
s2_data1["data_val"] = led2state;
JsonObject s2_data2 = s2_data_ls.createNestedObject();
s2_data2["data_name"] = "extent";
s2_data2["data_val"] = dutyCycle;
// 3.温湿度传感器
JsonObject s3 = sensors.createNestedObject();
s3["number"] = 3;
s3["type"] = "th";
s3["name"] = "温湿度";
JsonObject s3_data = s3.createNestedObject("data");
JsonArray s3_data_ls = s3_data.createNestedArray("data_vals");
JsonObject s3_data1 = s3_data_ls.createNestedObject();
s3_data1["data_name"] = "t_val";
s3_data1["data_val"] = t;
JsonObject s3_data2 = s3_data_ls.createNestedObject();
s3_data2["data_name"] = "h_val";
s3_data2["data_val"] = h;
}
// mqtt保持连接方法
void reconnect()
{
// Loop until we're reconnected
while (!client.connected())
{
Serial.print("Attempting MQTT connection...");
// Attempt to connect
// chipId 在这里是客户端连接的sessionid,同账户名下,不能相同,这里用芯片,固定配置下写死即可uc_01_keting_01
String clientId = String(chipId)+"-"+String(random(0xffff), HEX);
if (client.connect(clientId.c_str(), mqttUser, mqttPassword))
{
Serial.println("connected...");
}
else
{
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);
}
}
}
// 发布mqtt消息
void publishMQTT()
{
getJson(); // 生成要发送的json数据
if (!client.connected())
{
reconnect();
}
client.loop();
// 发送mqtt数据
client.publish(outTopic, messageBuff, true);
Serial.print("messageBuff:");
Serial.println(messageBuff);
//关闭连接
client.endPublish();
// 发送完成后清除缓存数据
memset(messageBuff, 0, 1024);
}
// mqtt回调函数_这里未做进一步处理,下一章节进行解析
void callback(char *topic, byte *payload, unsigned int length)
{
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
payload[length] = '\0'; // 追加字符串结束字符
for (int i = 0; i < length; i++)
{
Serial.print((char)payload[i]);
}
Serial.print("\n");
Serial.println();
}
补全说明
本例重点为常规数据的发送,无紧急需求,所以采取发送时才建立连接,像ESP01芯片单独部署的温湿度传感器5分钟上报一次数据已经足够。下一章节中接收指令时,如果要求有即时性的命令,改为保持连接。
本例中未处理控制指令,LED2没有对APP控制做出兼容。下一章节将完整实现远程和本地的旋钮都可以控制LED2的亮度。