了解如何使用 ESP-MESH 网络协议,通过 ESP32 和 ESP8266 NodeMCU 开发板构建 Mesh 网络。ESP-MESH 允许多个设备(节点)在单个无线局域网下相互通信。ESP32 和 ESP8266 开发板均支持此功能。在本教程中,我们将向您展示如何使用 Arduino 内核开始使用 ESP-MESH。
本文涵盖以下主题:
- ESP-MESH 简介
- ESP-MESH Basic 示例(广播消息)
- 使用 ESP-MESH 交换传感器读数(广播)
目录
Arduino集成开发环境
如果您想使用 Arduino IDE 对 ESP32 和 ESP8266 开发板进行编程,您IDE应该安装 ESP32 或 ESP8266 插件。
ESP-MESH 简介
根据乐鑫文档:
“ESP-MESH 是一种建立在 Wi-Fi 协议之上的网络协议。ESP-MESH 允许分布在大型物理区域(室内和室外)的众多设备(称为节点)在单个 WLAN(无线局域网)下互连。
ESP-MESH 具有自组织和自修复功能,这意味着网络可以自主构建和维护。更多信息,请访问 ESP-MESH 官方文档。
传统Wi-Fi网络架构
在传统的 Wi-Fi 网络架构中,单个节点(接入点 – 通常是路由器)连接到所有其他节点(站点)。每个节点都可以使用接入点相互通信。但是,这仅限于接入点 Wi-Fi 覆盖范围。每个站点都必须在范围内才能直接连接到接入点。ESP-MESH 不会发生这种情况。
ESP-MESH 网络架构
使用 ESP-MESH,节点无需连接到中心节点。节点负责中继彼此的传输。这允许多个设备分布在较大的物理区域。节点可以自组织并动态地相互通信,以确保数据包到达其最终节点目的地。如果从网络中删除任何节点,它能够自组织以确保数据包到达目的地。
painlessMesh 库
painlessMesh 库允许我们以简单的方式使用 ESP8266 或/和 ESP32 开发板创建网状网络。
“painlessMesh是一个真正的自组织网络,这意味着不需要规划、中央控制器或路由器。任何包含 1 个或多个节点的系统都将自组织成功能齐全的网格。网格的最大大小(我们认为)受到堆中可分配给子连接缓冲区的内存量的限制,因此应该非常高。有关 painlessMesh 库的更多信息。
安装 painlessMesh 库
此库需要一些其他库依赖项。应该会弹出一个新窗口,要求您安装任何缺少的依赖项。选择“全部安装”。
如果未显示此窗口,则需要安装以下库依赖项:
如果您使用的是 PlatformIO,请将以下行添加到 platformio.ini 文件以添加库并更改监视器速度。
对于 ESP32:
monitor_speed = 115200
lib_deps = painlessmesh/painlessMesh @ ^1.4.5
ArduinoJson
arduinoUnity
TaskScheduler
AsyncTCP
对于ESP8266:
monitor_speed = 115200
lib_deps = painlessmesh/painlessMesh @ ^1.4.5
ArduinoJson
TaskScheduler
ESPAsyncTCP
ESP-MESH Basic 示例(广播消息)
要开始使用 ESP-MESH,我们首先会尝试使用该库的基本示例。此示例创建一个网状网络,其中所有板将消息广播到所有其他板。
我们用四块开发板(两块 ESP32 和两块 ESP8266)对这个示例进行了实验。您可以添加或删除看板。该代码与 ESP32 和 ESP8266 开发板兼容。
代码 – painlessMesh 库基本示例
将以下代码复制到 Arduino IDE(库示例中的代码)。该代码与 ESP32 和 ESP8266 开发板兼容。
/*
更多Arduino/ESP8266/ESP32等教程请访问: https://www.qutaojiao.com
This is a simple example that uses the painlessMesh library: https://github.com/gmag11/painlessMesh/blob/master/examples/basic/basic.ino
*/
#include "painlessMesh.h"
#define MESH_PREFIX "whateverYouLike"
#define MESH_PASSWORD "somethingSneaky"
#define MESH_PORT 5555
Scheduler userScheduler; // to control your personal task
painlessMesh mesh;
// User stub
void sendMessage() ; // Prototype so PlatformIO doesn't complain
Task taskSendMessage( TASK_SECOND * 1 , TASK_FOREVER, &sendMessage );
void sendMessage() {
String msg = "Hi from node1";
msg += mesh.getNodeId();
mesh.sendBroadcast( msg );
taskSendMessage.setInterval( random( TASK_SECOND * 1, TASK_SECOND * 5 ));
}
// Needed for painless library
void receivedCallback( uint32_t from, String &msg ) {
Serial.printf("startHere: Received from %u msg=%sn", from, msg.c_str());
}
void newConnectionCallback(uint32_t nodeId) {
Serial.printf("--> startHere: New Connection, nodeId = %un", nodeId);
}
void changedConnectionCallback() {
Serial.printf("Changed connectionsn");
}
void nodeTimeAdjustedCallback(int32_t offset) {
Serial.printf("Adjusted time %u. Offset = %dn", mesh.getNodeTime(),offset);
}
void setup() {
Serial.begin(115200);
//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on
mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages
mesh.init( MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT );
mesh.onReceive(&receivedCallback);
mesh.onNewConnection(&newConnectionCallback);
mesh.onChangedConnections(&changedConnectionCallback);
mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
userScheduler.addTask( taskSendMessage );
taskSendMessage.enable();
}
void loop() {
// it will run the user scheduler as well
mesh.update();
}
在上传代码之前,您可以设置MESH_PREFIX(就像 MESH 网络的名称)和MESH_PASSWORD变量(您可以将其设置为您喜欢的任何内容)。
然后,我们建议您更改每个板的以下行,以便轻松识别发送消息的节点。例如,对于节点 1,更改消息,如下所示:
String msg = "Hi from node 1 ";
代码的工作原理
首先包括 painlessMesh 库。
#include "painlessMesh.h"
网格详细信息
然后,添加网格详细信息。MESH_PREFIX是指网格的名称。您可以将其更改为您喜欢的任何内容。
#define MESH_PREFIX "whateverYouLike"
顾名思义,MESH_PASSWORD是网状密码。您可以将其更改为您喜欢的任何内容。
#define MESH_PASSWORD "somethingSneaky"
网格中的所有节点都应使用相同的MESH_PREFIX和MESH_PASSWORD。
MESH_PORT是指您希望运行网状服务器的 TCP 端口。默认值为 5555。
#define MESH_PORT 5555
调度
建议避免在网状网络代码中使用 delay()。为了维护网格,需要在后台执行一些任务。使用 delay() 将阻止这些任务的发生,并可能导致网格失去稳定性/分崩离析。
相反,建议使用 TaskScheduler 来运行在 painlessMesh 本身中使用的任务。
以下行创建一个名为 userScheduler 的新调度程序。
Scheduler userScheduler; // to control your personal task
painlessMesh
创建一个名为 mesh 的 painlessMesh 对象来处理网格网络。
taskSendMessage
创建一个名为 taskSendMessage 的任务,只要程序正在运行,它就负责每秒调用一次 sendMessage() 函数。
Task taskSendMessage(TASK_SECOND * 1 , TASK_FOREVER, &sendMessage);
sendMessage
sendMessage() 函数将消息发送到消息网络(广播)中的所有节点。
void sendMessage() {
String msg = "Hi from node 1";
msg += mesh.getNodeId();
mesh.sendBroadcast( msg );
taskSendMessage.setInterval(random(TASK_SECOND * 1, TASK_SECOND * 5));
}
该消息包含“Hi from node 1”文本,后跟电路板芯片 ID。
String msg = "Hi from node 1";
msg += mesh.getNodeId();
要广播消息,只需在网格对象上使用 sendBroadcast() 方法,并将要发送的消息 (msg) 作为参数传递。
mesh.sendBroadcast(msg);
每次发送新消息时,代码都会更改消息之间的间隔(一到五秒)。
taskSendMessage.setInterval(random(TASK_SECOND * 1, TASK_SECOND * 5));
receivedCallback
接下来,创建几个回调函数,当网格上发生特定事件时,将调用这些函数。
receivedCallback() 函数打印消息发件人 (from) 和消息内容 (msg.c_str())。
void receivedCallback( uint32_t from, String &msg ) {
Serial.printf("startHere: Received from %u msg=%sn", from, msg.c_str());
}
每当新节点加入网络时,newConnectionCallback() 函数就会运行。此函数仅打印新节点的芯片 ID。您可以修改该函数以执行任何其他任务。
void newConnectionCallback(uint32_t nodeId) {
Serial.printf("--> startHere: New Connection, nodeId = %un", nodeId);
}
每当网络上的连接发生变化时(当节点加入或离开网络时),changedConnectionCallback() 函数就会运行。
void changedConnectionCallback() {
Serial.printf("Changed connectionsn");
}
nodeTimeAdjustedCallback() 函数在网络调整时间时运行,以便所有节点同步。它打印偏移量。
void nodeTimeAdjustedCallback(int32_t offset) {
Serial.printf("Adjusted time %u. Offset = %dn", mesh.getNodeTime(),offset);
}
setup()
在 setup() 中,初始化串行监视器。
void setup() {
Serial.begin(115200);
选择所需的调试消息类型:
//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on
mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages
使用前面定义的详细信息初始化网格。
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
将所有回调函数分配给其对应的事件。
mesh.onReceive(&receivedCallback);
mesh.onNewConnection(&newConnectionCallback);
mesh.onChangedConnections(&changedConnectionCallback);
mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
最后,将 taskSendMessage 函数添加到 userScheduler。调度程序负责在正确的时间处理和运行任务。
userScheduler.addTask(taskSendMessage);
最后,启用 taskSendMessage,以便程序开始向网格发送消息。
taskSendMessage.enable();
要保持网格运行,请将 mesh.update() 添加到 loop() 中。
void loop() {
// it will run the user scheduler as well
mesh.update();
}
示范
将提供的代码上传到所有看板。不要忘记修改消息以轻松识别发送方节点
将主板连接到计算机后,打开与每个主板的串行连接。您可以使用串行监视器,也可以使用PuTTY等软件,并为所有板打开多个窗口。
您应该看到所有看板都收到彼此的消息。例如,这些是节点 1 接收的消息。它接收来自节点 2、3 和 4 的消息。
当网格发生变化时,您还应该看到其他消息:当板离开或加入网络时。
使用 ESP-MESH 交换传感器读数
在下一个示例中,我们将在 4 块板之间交换传感器读数(您可以使用不同数量的板)。每个板子都会收到其他板子的读数。
例如,我们将交换来自 BME280 传感器的传感器读数,但您可以使用任何其他传感器。
所需零件
以下是此示例所需的部件:
- 4 个 ESP 板(ESP32 或 ESP8266)
- 4 个 BME280
- 面包板
- 连接线
Arduino_JSON库
在此示例中,我们将以 JSON 格式交换传感器读数。
如果将 VS Code 与 PlatformIO 配合使用,请在 platformio.ini 文件中包含库,如下所示:
ESP32
monitor_speed = 115200
lib_deps = painlessmesh/painlessMesh @ ^1.4.5
ArduinoJson
arduinoUnity
AsyncTCP
TaskScheduler
adafruit/Adafruit Unified Sensor @ ^1.1.4
adafruit/Adafruit BME280 Library @ ^2.1.2
arduino-libraries/Arduino_JSON @ ^0.1.0
ESP8266
monitor_speed = 115200
lib_deps = painlessmesh/painlessMesh @ ^1.4.5
ArduinoJson
TaskScheduler
ESPAsyncTCP
adafruit/Adafruit Unified Sensor @ ^1.1.4
adafruit/Adafruit BME280 Library @ ^2.1.2
arduino-libraries/Arduino_JSON @ ^0.1.0
电路图
将 BME280 传感器连接到 ESP32 或ESP8266默认的 I2C 引脚,如下图所示。
ESP32
推荐阅读:ESP32 with BME280 Sensor using Arduino IDE(压力、温度、湿度)
ESP8266 NodeMCU
推荐阅读:使用 Arduino IDE ESP8266 BME280(压力、温度、湿度)
代码 – ESP-MESH 广播传感器读数
将以下代码上传到每个开发板。此代码读取当前温度、湿度和压力读数并将其广播到网状网络上的所有板。读数以 JSON 字符串的形式发送,该字符串还包含用于标识发送方板的节点号。
/*
更多Arduino/ESP8266/ESP32等教程请访问: https://www.qutaojiao.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include "painlessMesh.h"
#include <Arduino_JSON.h>
// MESH Details
#define MESH_PREFIX "RNTMESH" //name for your MESH
#define MESH_PASSWORD "MESHpassword" //password for your MESH
#define MESH_PORT 5555 //default port
//BME object on the default I2C pins
Adafruit_BME280 bme;
//Number for this node
int nodeNumber = 2;
//String to send to other nodes with sensor readings
String readings;
Scheduler userScheduler; // to control your personal task
painlessMesh mesh;
// User stub
void sendMessage() ; // Prototype so PlatformIO doesn't complain
String getReadings(); // Prototype for sending sensor readings
//Create tasks: to send messages and get readings;
Task taskSendMessage(TASK_SECOND * 5 , TASK_FOREVER, &sendMessage);
String getReadings () {
JSONVar jsonReadings;
jsonReadings["node"] = nodeNumber;
jsonReadings["temp"] = bme.readTemperature();
jsonReadings["hum"] = bme.readHumidity();
jsonReadings["pres"] = bme.readPressure()/100.0F;
readings = JSON.stringify(jsonReadings);
return readings;
}
void sendMessage () {
String msg = getReadings();
mesh.sendBroadcast(msg);
}
//Init BME280
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
// Needed for painless library
void receivedCallback( uint32_t from, String &msg ) {
Serial.printf("Received from %u msg=%sn", from, msg.c_str());
JSONVar myObject = JSON.parse(msg.c_str());
int node = myObject["node"];
double temp = myObject["temp"];
double hum = myObject["hum"];
double pres = myObject["pres"];
Serial.print("Node: ");
Serial.println(node);
Serial.print("Temperature: ");
Serial.print(temp);
Serial.println(" C");
Serial.print("Humidity: ");
Serial.print(hum);
Serial.println(" %");
Serial.print("Pressure: ");
Serial.print(pres);
Serial.println(" hpa");
}
void newConnectionCallback(uint32_t nodeId) {
Serial.printf("New Connection, nodeId = %un", nodeId);
}
void changedConnectionCallback() {
Serial.printf("Changed connectionsn");
}
void nodeTimeAdjustedCallback(int32_t offset) {
Serial.printf("Adjusted time %u. Offset = %dn", mesh.getNodeTime(),offset);
}
void setup() {
Serial.begin(115200);
initBME();
//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on
mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages
mesh.init( MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT );
mesh.onReceive(&receivedCallback);
mesh.onNewConnection(&newConnectionCallback);
mesh.onChangedConnections(&changedConnectionCallback);
mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
userScheduler.addTask(taskSendMessage);
taskSendMessage.enable();
}
void loop() {
// it will run the user scheduler as well
mesh.update();
}
该代码与 ESP32 和 ESP8266 开发板兼容。
代码的工作原理
继续阅读本部分,了解代码的工作原理。
库
首先包括所需的库:与 BME280 传感器接口的Adafruit_Sensor和Adafruit_BME280;用于处理网状网络的 painlessMesh 库和用于轻松创建和处理 JSON 字符串的 Arduino_JSON。
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include "painlessMesh.h"
#include <Arduino_JSON.h>
Mesh详情
在以下行中插入网格详细信息。
#define MESH_PREFIX "RNTMESH" //name for your MESH
#define MESH_PASSWORD "MESHpassword" //password for your MESH
#define MESH_PORT 5555 //default port
MESH_PREFIX是指网格的名称。您可以将其更改为您喜欢的任何内容。顾名思义,MESH_PASSWORD是网状密码。您可以将其更改为您喜欢的任何内容。网格中的所有节点都应使用相同的MESH_PREFIX和MESH_PASSWORD。
MESH_PORT是指您希望运行网状服务器的 TCP 端口。默认值为 5555。
BME280
在默认的 ESP32 或 ESP8266 引脚上创建一个名为 bme 的Adafruit_BME280对象。
Adafruit_BME280 bme;
在 nodeNumber 变量中,插入主板的节点号。每个板的编号必须不同。
int nodeNumber = 2;
readings 变量将用于保存要发送到其他板的读数。
String readings;
userScheduler
以下行创建一个名为 userScheduler 的新调度程序。
Scheduler userScheduler; // to control your personal task
painlessMesh
创建一个名为 mesh 的 painlessMesh 对象来处理网格网络。
创建任务
创建一个名为 taskSendMessage 的任务,只要程序正在运行,该任务就负责每 5 秒调用一次 sendMessage() 函数。
Task taskSendMessage(TASK_SECOND * 5 , TASK_FOREVER, &sendMessage);
getReadings()
getReadings() 函数从 BME280 传感器获取温度、湿度和压力读数,并连接所有信息,包括名为 jsonReadings 的 JSON 变量上的节点号。
JSONVar jsonReadings;
jsonReadings["node"] = nodeNumber;
jsonReadings["temp"] = bme.readTemperature();
jsonReadings["hum"] = bme.readHumidity();
jsonReadings["pres"] = bme.readPressure()/100.0F;
以下行显示了具有任意值的 jsonReadings 变量的结构。
{
"node":2,
"temperature":24.51,
"humidity":52.01,
"pressure":1005.21
}
然后使用 stringify() 方法将 jsonReadings 变量转换为 JSON 字符串,并保存在 readings 变量中。
readings = JSON.stringify(jsonReadings);
然后,该函数返回此变量。
return readings;
sendMessage
sendMessage() 函数将带有读数和节点号 (getReadings()) 的 JSON 字符串发送到网络中的所有节点(广播)。
void sendMessage () {
String msg = getReadings();
mesh.sendBroadcast(msg);
}
initBME
initBME() 函数初始化 BME280 传感器。
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
receivedCallback
接下来,创建几个回调函数,当网格上发生某些事件时,将调用这些函数。
receivedCallback() 函数打印消息发件人 (from) 和消息内容 (msg.c_str())。
void receivedCallback( uint32_t from, String &msg ) {
Serial.printf("startHere: Received from %u msg=%sn", from, msg.c_str());
该消息采用 JSON 格式,因此,我们可以按如下方式访问变量:
JSONVar myObject = JSON.parse(msg.c_str());
int node = myObject["node"];
double temp = myObject["temp"];
double hum = myObject["hum"];
double pres = myObject["pres"];
最后,在串行监视器上打印所有信息。
Serial.print("Node: ");
Serial.println(node);
Serial.print("Temperature: ");
Serial.print(temp);
Serial.println(" C");
Serial.print("Humidity: ");
Serial.print(hum);
Serial.println(" %");
Serial.print("Pressure: ");
Serial.print(pres);
Serial.println(" hpa");
每当新节点加入网络时,newConnectionCallback() 函数就会运行。此函数仅打印新节点的芯片 ID。您可以修改该函数以执行任何其他任务。
void newConnectionCallback(uint32_t nodeId) {
Serial.printf("--> startHere: New Connection, nodeId = %un", nodeId);
}
每当网络上的连接发生变化时(当节点加入或离开网络时),changedConnectionCallback() 函数就会运行。
void changedConnectionCallback() {
Serial.printf("Changed connectionsn");
}
nodeTimeAdjustedCallback() 函数在网络调整时间时运行,以便所有节点同步。它打印偏移量。
void nodeTimeAdjustedCallback(int32_t offset) {
Serial.printf("Adjusted time %u. Offset = %dn", mesh.getNodeTime(),offset);
}
setup()
在 setup() 中,初始化串行监视器。
void setup() {
Serial.begin(115200);
调用 initBME() 函数以初始化 BME280 传感器。
initBME();
选择所需的调试消息类型:
//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on
mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages
使用前面定义的详细信息初始化网格。
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
将所有回调函数分配给其对应的事件。
mesh.onReceive(&receivedCallback);
mesh.onNewConnection(&newConnectionCallback);
mesh.onChangedConnections(&changedConnectionCallback);
mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
最后,将 taskSendMessage 函数添加到 userScheduler。调度程序负责在正确的时间处理和运行任务。
userScheduler.addTask(taskSendMessage);
最后,启用 taskSendMessage,以便程序开始向网格发送消息。
taskSendMessage.enable();
要保持网格运行,请将 mesh.update() 添加到 loop() 中。
void loop() {
// it will run the user scheduler as well
mesh.update();
}
示范
将代码上传到所有开发板(每个开发板具有不同的节点编号)后,您应该看到每个开发板正在接收其他开发板的消息。
以下屏幕截图显示了节点 1 收到的消息。它接收来自节点 2、3 和 4 的传感器读数。
总结
希望您喜欢这篇 ESP-MESH 网络协议的快速介绍。