在之前的文章中:https://blog.csdn.net/m0_50114967/article/details/127016395
已经简单地介绍了MQTT协议,对比于其它网络协议,MQTT协议在物联网的开发中,它的特点使它适用于大多数受限的环境。例如网络代价昂贵,带宽低、不可靠,在嵌入设备中运行,处理器和内存资源有限。
下面深入了解一下MQTT协议的特点和优势,下图是一个MQTT的概念图:
如图所示,MQTT基于一个MQTT服务器(MQTT Broker),所有设备或客户端都可以是一个发布设备同时也可以是一个订阅设备,所以,只要你的设备可以连接在同一个MQTT服务器,都可以给其它设备进行发布任务或接收其它设备发布的数据,实现一对多的消息发布,完美地解决设备或应用程序的耦合。
MQTT发布的消息有三种服务质量:
QoS0:最多一次,可能导至发布的消息丢失
QoS1:至少一次,可能导至发布的消息多次发布
QoS2:确保只有一次,保证消息到达对方并且只到达一次
QoS等级越高,系统消耗也越高,在应用时可以根据需求选择合适的QoS等级。
服务器与客户端通信时,当遇到异常或客户端心跳超时的情况,MQTT服务器会替客户端发布一个遗嘱消息。当然如果服务器收到来自客户端的断开的消息(如自主选择断开连接),则不会触发遗嘱消息的发送。 在重要的应用或设备上使用该标记,可以在发生网络故障或网络波动,设备在保持连接周期内未能通讯,连接被服务端关闭,设备意外掉电,设备尝试进行不被允许的操作而被服务端关闭连接,例如订阅自身权限以外的主题等异常时通知第三方。
客户端可以设置一个心跳时间间隔,客户端会周期性地给服务发送一个心跳请求(PINGREQ),服务器收到请求后会回复并响应心跳请求(PINGRESP)。如果客户端在发送心跳请求(PINGREQ)后,没有收到服务端的心跳响应(PINGRESP),那么客户端就会认为自己与服务端的连接已经被断开了。
更多MQTT的介绍可以到http://mqtt.p2hp.com/了解
在ESP32上使用MQTT协议,可以用的库比较多,这里选择pubsubclient ,该库可以找到的资料与文档说明都比较详细,唯一的不足是该库只能发布QoS0的消息和订阅Qos0和Qos1的消息。
该库的项目地址:https://github.com/knolleary/pubsubclient
API文档(英文)地址:https://pubsubclient.knolleary.net/api
创建一个完全配置的客户端实例。
参数:
server IPAddress, uint8_t[] 或 const char[] -服务器地址
port int - 要连接的端口
callback function* (可选) -一个指向消息回调函数的指针,消息到达客户端时调用该函数
client -使用的网络客户端,例如WiFiClient
stream Stream (可选) - 将接收到的消息写入的流
客户端连接到服务器
参数:
clientID const char[] - 连接到服务器时使用的客户端ID
Credentials - (可选)
username const char[] - 要使用的用户名。如果为NULL,则不使用用户名或密码
password const char[] - 要使用的密码。如果为NULL,表示不使用密码
Will - (可选)
willTopic const char[] - 遗嘱消息要使用的主题
willQoS int: 0,1 or 2 - 遗嘱消息将要使用的服务质量
willRetain boolean - 遗嘱是否应以保留标记公布
willMessage const char[] - 遗嘱消息的有效负载
cleanSession boolean (可选) - 是否连接clean-session
返回值:
false - 连接失败
true - 连接成功
检查客户端是否连接到服务器端。
返回
false - 未连接
true - 已连接
需要循环调用该函数,以便客户端用来处理传入消息并维护与服务器的连接。
返回值:
false - 客户端不在连接状态
true - 客户端处于连接状态
将消息发布到指定的主题。
参数:
topic const char[] - 要发布的主题
payload const char[], byte[] - 要发布的消息
length unsigned int (可选) - 有效载荷的长度。如果有效负载是byte[],则为必选项。
retained boolean (可选) - 是否保留消息
false - 不保留
true - 保留
返回:
false - 发布失败,要么连接丢失,要么消息长度超长
true - 发布成功
定阅发送到主题的消息
参数:
topic const char[] - 订阅的主题
qos int: 只能选择 0 或 1 (可选) - 订阅消息的服务质量
返回:
false - 订阅失败,要么连接丢失,要么消息超长
true - 订阅成功
pubsubclient这个库的头文件为PubSubClient.h,同时,该库需要使用一个网络客户端,这里直接使用WiFi库来创建一个网络客户端实例,所以同时要引入WiFi.h这个头文件。
同时,我们创建几个变量,来保存连接服务器所需要的数据,如服务器地址和连接端口。
#include <WiFi.h>
#include <PubSubClient.h>
const char* mqttServer = "broker.emqx.io"; //MQTT服务器地址,一个公共的免费MQTT服务器
const int mqttPort = 1883; //连接的端口
/***********************************************************************************
* 创建一个网络客户端,用来创建MQTT客户端实例
***********************************************************************************/
WiFiClient espClient;
/***********************************************************************************
* 创建一个完全配置的客户端实例。
* 参数
* mqttServer - 服务器地址
* mqttPort - 要连接的端口
* callback - 一个指向消息回调函数的指针,当消息到达此客户端创建的订阅时调用该函数
* espClient - 使用的网络客户端,例如WiFiClient
***********************************************************************************/
PubSubClient mqtt_client(mqttServer,mqttPort,callback,espClient);
客户端已经设置好,后面我们需要利用connect ()来连接服务器,正常情况下,连接服务器前,我们先确定ESP32是否已连接网络,因为这里我们是用WIFI来连接网络的,所以这里写一个函数来方便调用
/***********************************************************************************
* 函数:连接mqtt服务器
* 返回:
* 返回连接状态
***********************************************************************************/
boolean connect_MQTT(){
if(WiFi.status() == WL_CONNECTED){ //WIFI是否连接
if(mqtt_client.connect("ESP32Client")){ //连接服务器,客户端ID可以自定义
Serial.println("连接MQTT服务器成功"); //输出连接状态
mqtt_client.publish("ESP32", "ESP32已连接MQTT服务器"); //发布一个消息到ESP32主题
mqtt_client.subscribe("ESP32"); //订阅一个ESP32主题
}
}
return mqtt_client.connected(); //返回连接状态
}
当设备连接上服务器后,大多数情况下,我们会把设备的一些数据发布,比如ESP32的引脚状态,从传感器获取到的数据等。所以,我们还需要创建一个定时发布消息的函数。
/***********************************************************************************
* 函数:定时发布消息到指定主题
***********************************************************************************/
long lastSendTime = 0; //最后发送时间,该变量为全局变量
void regularPublish(){
long publishNow = millis(); //得到当前时间
if (publishNow - lastSendTime > 5000){ //当前时间-最后发送时间>5000时
lastSendTime = publishNow; //把最后发送时间设为当前时间
mqtt_client.publish("ESP32", "来自esp32定时发布的消息"); //发布一个消息到ESP32主题
}
}
除了发布消息,我们也需要ESP32响应其它客户端发布的消息,来控制ESP32或响应对方的指令,如从手机端或电脑发送一个指令来控制ESP32的引脚状态,从而实现多远端来实现远程控制。该函数设定为当ESP32收到一个第一个字符为1的消息后,设置ESP32的2号引脚设为高电平,而收到的字符为0时2号引脚设为低电平。
/***********************************************************************************
* 函数:回调函数,收到消息时的动作,参数是固定的
* 参数:
* char* topic:主题
* byte* payload:消息内容
* unsigned int length:消息内容长度
***********************************************************************************/
void callback(char* topic, byte* payload, unsigned int length) {
if(payload[0]=='1'){
digitalWrite(2,1); //2号引脚设为高电平
Serial.println("2号引脚设为高");
}else if(payload[0]=='0'){
Serial.println("2号引脚设为低");
digitalWrite(2,0); //2号引脚设为低电平
}else{
return;
}
}
该回调函数对应之前创建客户端实例里第三个参数。
PubSubClient mqtt_client(mqttServer,mqttPort,callback,espClient);
对于在线设备,我们需要考虑设备或网络异常的情况,让设备出现意外掉线等情况下自动可以重新连接服务器,这里用一个函数来调用之前连接MQTT服务器的函数,该函数在设备在线的情况下运行loop()函数,来接收已订阅的主题所收到的消息,如果设备出现异常,会每5秒调用之前连接MQTT服务器的函数来重新连接服务器。
/***********************************************************************************
* 函数:mqtt循环
* 如果连接断开,每5秒进行一次重连
***********************************************************************************/
long lastAttemptTime = 0; //该变量为全局变量,用来保存最后连接的时间
void mqtt_loop(){
if(!mqtt_client.connected()){ //如果未连接MQTT服务器
long now = millis(); //得到当前时间
if (now - lastAttemptTime > 5000){ //当前时间-最后连接时间>5000时
lastAttemptTime = now; //把最后连接时间设为当前时间
mqtt_client.setKeepAlive(60); //心跳时间设为60秒
if(connect_MQTT()){ //如果连接服务器成功
lastAttemptTime = 0; //最后连接时间设为0
}
}
}else{
regularPublish(); //定时发布消息
mqtt_client.loop(); //MQTT循环
}
}
ESP32_web_server_12_mqtt.ino
#include "ESPAsyncWebServer.h"
AsyncWebServer server(80); //创建一个服务器对象,WEB服务器端口:80
void setup() {
Serial.begin(9600); //串口波特率初始化
LittleFS_begin(); //LittleFS文件系统初始化
connect_NET(); //网络初始化
web_server(); //WEB服务器初始化
GPIO_begin(); //引脚初始化
connect_MQTT(); //连接MQTT服务器
}
void loop() {
DNS_request_loop(); //DNS服务请求处理
mqtt_loop(); //mqtt服务循环
}
mqtt_server.ino
#include <WiFi.h>
#include <PubSubClient.h>
/***********************************************************************************
* 库:https://github.com/knolleary/pubsubclient
* 该库只能发布QoS0消息,可以订阅QoS0或QoS1消息
* 最大消息,包括头,默认为256个字节,可以通过PubSubClient::setBufferSize(size)重新配置
* 心跳间隔默认为15秒,可以通过PubSubClient::setKeepAlive(keepAlive)重新配置
* 默认mqtt版本为3.1.1。
***********************************************************************************/
const char* mqttServer = "broker.emqx.io"; //MQTT服务器地址,一个公共的免费MQTT服务器
const int mqttPort = 1883; //连接的端口
const char* mqttUser = "yourMQTTuser"; //公共的MQTT服务器一般都不要求帐号登陆,这里可以不设置
const char* mqttPassword = "yourMQTTpassword"; //公共的MQTT服务器一般都不要求帐号登陆,这里可以不设置
long lastAttemptTime = 0; //最后更新时间
long lastSendTime = 0; //最后发送时间
/***********************************************************************************
* 创建一个网络客户端,用来创建MQTT客户端实例
***********************************************************************************/
WiFiClient espClient;
/***********************************************************************************
* 创建一个完全配置的客户端实例。
* 参数
* mqttServer - 服务器地址
* mqttPort - 要连接的端口
* callback - 一个指向消息回调函数的指针,当消息到达此客户端创建的订阅时调用该函数
* espClient - 使用的网络客户端,例如WiFiClient
***********************************************************************************/
PubSubClient mqtt_client(mqttServer,mqttPort,callback,espClient);
/***********************************************************************************
* 函数:定时发布消息到指定主题
***********************************************************************************/
void regularPublish(){
long publishNow = millis(); //得到当前时间
if (publishNow - lastSendTime > 5000){ //当前时间-最后发送时间>5000时
lastSendTime = publishNow; //把最后发送时间设为当前时间
mqtt_client.publish("ESP32", "来自esp32定时发布的消息"); //发布一个消息到ESP32主题
}
}
/***********************************************************************************
* 函数:回调函数,收到消息时的动作,参数是固定的
* 参数:
* char* topic:主题
* byte* payload:消息内容
* unsigned int length:消息内容长度
***********************************************************************************/
void callback(char* topic, byte* payload, unsigned int length) {
/*
String str;
payload[length]='\0'; //缓冲区最后加入结束字符
str = String((char *)payload); //转为字符串
Serial.print("Message arrived [");
Serial.print(topic); // 打印主题信息
Serial.print("] ");
Serial.print(str);
Serial.println();
Serial.print("收到的指令:");
Serial.println(payload[0]);
*/
if(payload[0]=='1'){
digitalWrite(2,1); //2号引脚设为高电平
Serial.println("2号引脚设为高");
}else if(payload[0]=='0'){
Serial.println("2号引脚设为低");
digitalWrite(2,0); //2号引脚设为低电平
}else{
return;
}
}
/***********************************************************************************
* 函数:连接mqtt服务器
* 返回:
* 返回连接状态
***********************************************************************************/
boolean connect_MQTT(){
if(WiFi.status() == WL_CONNECTED){ //WIFI是否连接
if(mqtt_client.connect("ESP32Client")){ //连接服务器,如果成功
Serial.println("连接MQTT服务器成功"); //输出连接状态
mqtt_client.publish("ESP32", "ESP32已连接MQTT服务器"); //发布一个消息到ESP32主题
mqtt_client.subscribe("ESP32"); //订阅一个ESP32主题
}
}
return mqtt_client.connected(); //返回连接状态
}
/***********************************************************************************
* 函数:mqtt循环
* 如果连接断开,每5秒进行一次重连
***********************************************************************************/
void mqtt_loop(){
if(!mqtt_client.connected()){ //如果未连接MQTT服务器
long now = millis(); //得到当前时间
if (now - lastAttemptTime > 5000){ //当前时间-最后尝试时间>5000时
lastAttemptTime = now; //把最后尝试时间设为当前时间
mqtt_client.setKeepAlive(60); //心跳时间设为60秒
if(connect_MQTT()){ //如果连接服务器成功
lastAttemptTime = 0; //最后尝试时间设为0
}
}
}else{
regularPublish(); //定时发布消息
mqtt_client.loop(); //MQTT循环
}
}
web_server.ino
#include "ESPAsyncWebServer.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
/***********************************************************************************
* 函数:引脚初始化
***********************************************************************************/
void GPIO_begin(){
pinMode(2, OUTPUT); //引脚2设置为输出模式
}
/***********************************************************************************
* 函数:响应按键回调函数
***********************************************************************************/
void GPIO_button(AsyncWebServerRequest *request){
int pin_state = digitalRead(2);
String state;
digitalWrite(2,(!pin_state)); //每次按下循环地改变引脚状态
if(digitalRead(2)){
state = "开";
}else{
state = "关";
}
request->send(200,"text/plain",state); //把状态发送回页面
Serial.print("引脚状态改变为:");
Serial.println(pin_state);
}
/**************************************************************************************
* 函数:字符串写入文件,文件如果存在,将被清零并新建,文件不存在,将新建该文件
* path: 文件的绝对路径
* str: 要写入的字符串
*************************************************************************************/
void str_write(String path, String str){
Serial.println("写入文件");
File wf = LittleFS.open(path,"w"); //以写入模式打开文件
if(!wf){ //如果无法打开文件
Serial.println("打开文件写入时错误"); //显示错误信息
return; //无法打开文件直接返回
}
wf.print(str); //字符串写入文件
wf.close(); //关闭文件
File rf = LittleFS.open(path,"r"); //以读取模式打开文件
Serial.print("FILE:");Serial.println(rf.readString()); //读取文件
rf.close(); //关闭文件
}
// /**********************************************************************************
// * 函数:把收到的POST数据格式化为JSON格式的字符串
// *********************************************************************************/
//String format_json(AsyncWebParameter* post_data , int len){
// String json_name = post_data->name().c_str(); //得到名称
// String json_value = post_data->value.c_str(); //得到值
// StaticJsonDocument<len> json_obj; //创建一个JSON对象
// json_obj[json_name] = json_value; //写入一个名称和值
// String json_str;
// serializeJson(wifi_json, wifi_json_str); //生成JOSN的字符串
// return json_str; //返回JOSN字符串
//}
/**********************************************************************************
* 函数:响应网站/setwifi目录的POST请求,收到请求后,运行get_WIFI_set_CALLback回调函数
* 获取并格式化收到的POST数据
*********************************************************************************/
void get_WIFI_set_CALLback(AsyncWebServerRequest *request){
Serial.println("收到设置WIFI按钮");
if(request->hasParam("wifiname",true)){
AsyncWebParameter* wifiname = request->getParam("wifiname",true); //获取POST数据
AsyncWebParameter* wifipassword = request->getParam("wifipassword",true); //获取POST数据
String wn = wifiname->name().c_str();
String wnv = wifiname->value().c_str();
String wp = wifipassword->name().c_str();
String wpv = wifipassword->value().c_str();
//把SSID和password写成一个JSON格式
StaticJsonDocument<200> wifi_json; //创建一个JSON对象,wifi_json
wifi_json[wn] = wnv; //写入一个建和值
wifi_json[wp] = wpv; //写入一个键和值
String wifi_json_str; //定义一个字符串变量
serializeJson(wifi_json, wifi_json_str); //生成JOSN的字符串
str_write("/WIFIConfig.conf",wifi_json_str); //字符串写入
}
}
/**********************************************************************************
* 函数:从文件path中读取字符串
* path: 文件的绝对路径
* return: 返回读取的字符串
*********************************************************************************/
String str_read(String path){
Serial.println("读取文件");
File rf = LittleFS.open(path,"r"); //以读取模式打开文件
if(!rf){ //如果无法打开文件
Serial.println("打开文件读取时错误"); //显示错误信息
return ""; //无法打开文件直接返回
}
String str = rf.readString(); //读取字符串
rf.close(); //关闭文件
return str;
}
/***************************************************************************************
* 函数:解析JSON字符串,从JSON字符串名称得到该值
* str: JSON字符串
* Name: JSON集合的名称
* return: 返回值的字符串
***************************************************************************************/
String analysis_json(String str, String Name){
DynamicJsonDocument doc(str.length()*2); //定义一个JSON对象
DeserializationError error = deserializeJson(doc, str); //尝试反序列数据,如果失败生成error错误码,成功时将字符串str生成一个JSON对象doc
if(error){ //如果反序列数据失败
Serial.print("JSON解析失败:"); //输出错误信息
Serial.println(error.f_str()); //输出错误信息
return ""; //返回
}
if(!doc.containsKey(Name)){ //键是否存在
Serial.println("不存在键");
return ""; //返回
}
return doc[Name].as<String>(); //返回读取到的字符串
}
/***********************************************************************************
* 函数:/WIFIConfig.conf文件中读取设置数据并连接WIFI
***********************************************************************************/
void wifi_connect(){
Serial.println("在conf文件中读取数据并连接WIFI");
String str = str_read("/WIFIConfig.conf"); //读取文件内容
String wifiname = analysis_json(str,"wifiname"); //解析WIFI名称
String wifipassword = analysis_json(str,"wifipassword"); //解析WIFI名称
connect_WIFI(wifiname, wifipassword); //连接WIFI
}
/***********************************************************************************
* web服务器初始化
***********************************************************************************/
void web_server(){
Serial.println("初始化WEB服务器");
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html"); //响应网站根目录的GET请求,返回文件index.html
server.on("/setwifi" ,HTTP_POST , get_WIFI_set_CALLback); //响应设置WIFI按钮的请求
server.on("/GPIO2", HTTP_GET, GPIO_button); //响应改变引脚按钮的请求
server.begin(); //初始化
}
/********************************************************************************
* LittleFS文件系统初始化
*********************************************************************************/
void LittleFS_begin(){
Serial.println();
Serial.println("初始化文件系统");
if(!LittleFS.begin(true)){
Serial.println("An Error has occurred while mounting LittleFS");
return;
}
}
wifi_connect.ino
/*WIFI连接*/
#include <WiFi.h>
#include <DNSServer.h>
DNSServer dnsserver;
/***********************************************************************************
* 函数:连接WIFI
* ssid: WIFI名称
* password: WIFI密码
* return: 连接成功返回true
***********************************************************************************/
void connect_WIFI(String ssid, String password){
WiFi.begin(ssid.c_str(), password.c_str()); //连接WIFI
Serial.print("连接WIFI");
//循环,10秒后连接不上跳出循环
int i = 0;
while(WiFi.status() != WL_CONNECTED){
Serial.print(".");
delay(500);
i++;
if(i>20){
Serial.println();
Serial.println("WIFI连接失败");
return;
}
}
Serial.println();
IPAddress local_IP = WiFi.localIP();
Serial.print("WIFI连接成功,本地IP地址:"); //连接成功提示
Serial.println(local_IP);
}
/***********************************************************************************
* 设置AP和STA共存模式,设置DNS服务器
***********************************************************************************/
void connect_NET(){
const byte DNS_PORT = 53; //DNS端口
const String url = "ESPAP.com"; //域名
IPAddress APIp(10,0,10,1); //AP IP
IPAddress APGateway(10,0,10,1); //AP网关
IPAddress APSubnetMask(255,255,255,0); //AP子网掩码
const char* APSsid = "esp32_AP"; //AP SSID
const char* APPassword = "12345678"; //AP wifi密码
wifi_connect(); //连接WIFI
WiFi.mode(WIFI_AP_STA); //打开AP和STA共存模式
WiFi.softAPConfig(APIp, APGateway, APSubnetMask); //设置AP的IP地址,网关和子网掩码
WiFi.softAP(APSsid, APPassword, 6); //设置AP模式的登陆名和密码
dnsserver.start(DNS_PORT, url, APIp); //设置DNS的端口、网址、和IP
Serial.print("AP模式IP地址为:");
Serial.println(WiFi.softAPIP());
}
/***********************************************************************************
* DNS处理请求的循环
***********************************************************************************/
void DNS_request_loop(){
dnsserver.processNextRequest();
}
以下文件需要用插件上传到SPIFFS文件系统
data\index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="mystyle.css">
<title>EPS32教程</title>
</head>
<body>
<div id="pal">
<form name="wifiset" onsubmit="return validateForm()" action="\setwifi" method="post" target="myframe">
<label for="wifiname">WIFI SSID</label>
<input name="wifiname" type="text" value="ESP32">
<label for="wifipassward">WIFI PASSWARD</label>
<input name="wifipassword" type="text" value="ESP32">
<input type='submit' value='设置WIFI'>
</form>
<iframe src="" width="200" height="200" frameborder="0" name="myframe" style="display:NONE" ></iframe>
<input type="button" value="控制GPIO" onclick="get_request('sw','/GPIO2')">
<p id="sw">关</p>
</div>
</body>
<script>
/******************************
表单验证,WIFI名称输入框为空时提示
******************************/
function validateForm(){
var wifiname=document.forms["wifiset"]["wifiname"].value; //得到name输入框的文字
//var wifipassword=document.forms["wifiset"]["password"].value;
if (wifiname==null || wifiname==""){ //如果输入框为空
alert("WIFI SSID必需输入"); //显示提示
return false;
}
}
/******************************
函数:向path发送GET请求,并得到后台send()方法发送过来的值,把得到的值写到ElementId所指向的元素
注意,发送GET请求后台的响应是必要的。
ElementId: 要得到数据的元素ID
path: 请求的路径
******************************/
function get_request(ElementId , path){
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { //如果请求就绪和状态已完成
document.getElementById(ElementId).innerHTML = xmlhttp.responseText; //将获取到的内容<send(200,"text/plain", String());>写到id为ElementId的元素中
}
},
xmlhttp.open("GET", path, true); //发送GET请求
xmlhttp.send();
}
/******************************
创建一个新元素
ParentElement , 父元素
Element , 要创建的元素
Attribute , 新元素的属性
AttributeValue , 新元素的属性值
text 新元素的文本节点
******************************/
function show_wifi_state(ParentElement , Element ,Attribute , AttributeValue , text){
var att = document.createAttribute(Attribute); //创建一个新的属性
att.value = AttributeValue; //属性值
var para=document.createElement(Element); //创建了一个新的元素
para.setAttributeNode(att); //把属性加入元素
var node=document.createTextNode(text); //创建文本节点
para.appendChild(node); //向元素追加文本节点
var element=document.getElementById(ParentElement); //查找到一个已有的父元素
element.appendChild(para); //向这个已存在的父元素追加新元素
}
</script>
</html>
data\mystyle.css
div{
background-color:red;
}
data\WIFIConfig.conf
该文件需要改动以符合你的WIFI设备,或以AP用手机登陆来生成该文件(生成需要重启设备),详细的操作方法可以找之前的文章:https://blog.csdn.net/m0_50114967/category_12014624.html
{"wifiname":"你的WIFI名称","wifipassword":"你的WIFI密码"}
以上代码上传到ESP32后,在WIFI配置正确的情况下,会自动连接到MQTT服务器(broker.emqx.io),上传前注意打开串口监视器,查看是否正确连接MQTT服务器。
连接成功后,这里我们需要别一个客户端来接收或给ESP32发送指令。
用如果电脑端,我们可以用MQTTX来把电脑做为一个客户端,下载地址:https://www.emqx.com/zh/try?product=MQTTX
下载和安装这个工具,运行后,我们新建一个连接
如上图设置好,点击右上角的连接。
连接成功后,我们给该连接添加一个订阅
设置好后点确定来订阅,这个时候,我们应该会每五秒收到一条从ESP32发布的消息
我们在ESP32的2号引脚上连接一个LED,来测试在电脑上发送一个指令来控制该LED的亮灭,同时如果控制成功,在串口监视器上也可以看到提示。
我们用MQTTX发送一个数字"1"或"0",在代码设计中,当ESP32收到时会分别把2号引脚设置为高电平或低电平
发布的内容可以为"1"或"0",发布其它内容ESP32也可以接收到,但不一定能控制引脚的状态
点击发送按钮后,我们观察2号脚上的LED或观察串口监视器,
如上图的状态,就说明我们已经可以通过电脑远端控制ESP32了。
如果有空,大家也可以在手机上下载可以使用MQTT的APP来测试控制ESP32。
以上就是ESP32利用MQTT协议与其它客户端互动的简单实现,结合之前的文章https://blog.csdn.net/m0_50114967/article/details/127016395内容,我们现在可以用WEB页面和MQTT协议来与ESP32进行互动。
在之后的文章里,将会在安全性、可控范围上继续完备这两种互动方式。
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h