前言
关于esp-now的介绍视频建议看官方https://www.bilibili.com/video/BV1gf4y1H7iA
esp-now的官方例程代码仓库在https://github.com/espressif/esp-now
本章着重介绍自己的理解,请大家批判的接受
代码整理
我在分析demo的时候极其不习惯使用官方的工程,我会自己整理一个比较“纯净”的工程利于我自己的学习研究,当然这个只是自己的个人习惯。
这里我把demo变成如下结构:
.
├── components
│ ├── control
│ │ ├── include
│ │ └── src
│ ├── espnow
│ │ ├── include
│ │ └── src
│ ├── led
│ │ ├── include
│ │ └── src
│ └── utils
│ ├── include
│ └── src
└── main
除了led,是在esp-now\examples\dev_kits\common_components\led,其它都是在esp-now\components里面。这样搞好后,我们的外层CMakeLists.txt就可以回归到我们最常用的
cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(main)
整个文件夹显得非常的舒服
源码注释
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_utils.h"
#include "espnow.h"
#include "espnow_ctrl.h"
#include "driver/gpio.h"
#include "driver/rmt.h"
#include "led_strip.h"
// 这里为了兼容C3,C3的18、19用于usb所以最好不占用
#ifdef CONFIG_IDF_TARGET_ESP32C3
#define BOOT_KEY_GPIIO GPIO_NUM_9
#define CONFIG_LED_STRIP_GPIO GPIO_NUM_8
#else
#define BOOT_KEY_GPIIO GPIO_NUM_0
#define CONFIG_LED_STRIP_GPIO GPIO_NUM_18
#endif
// 绑定设备的时候需要长按的时间
#define BOOT_KEY_PRESS_TIMEOUT_BIND 1000
// 解绑设备的时候需要长按的时间
#define BOOT_KEY_PRESS_TIMEOUT_UNBIND 5000
// log用的标签
static const char *TAG = "main";
// gpio中断消息
static xQueueHandle gpio_evt_queue = NULL;
// led中断消息
static led_strip_t *g_strip_handle = NULL;
/**
* @brief wifi初始化
*
*/
static void wifi_init()
{
ESP_ERROR_CHECK(esp_netif_init()); // 初始化底层 TCP/IP 协议栈。
esp_event_loop_create_default(); // 创建默认事件循环,用于接收处理wifi相关事件
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); // 使用默认参数配置wifi
ESP_ERROR_CHECK(esp_wifi_init(&cfg)); // 将配置丢进去,初始化wifi
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); // 设置wifi模式为station模式
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));// 设置wifi存储位置(存在ram里意味着。断电不保存)
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));// 设置当前 WiFi 省电类型(这里为不省电)
ESP_ERROR_CHECK(esp_wifi_start());// 开启wifi
}
/**
* @brief gpio中断服务函数,用于第一时间发送中断消息给处理任务去处理
*
*/
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
uint32_t gpio_num = (uint32_t) arg;
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
/**
* @brief 按键处理任务
*
*/
static void initiator_key_task(void *arg)
{
ESP_LOGI(TAG, "Initiator key handle task is running");
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_ANYEDGE // 任意边沿中断
.mode = GPIO_MODE_INPUT, // 输入模式
.pin_bit_mask = BIT(BOOT_KEY_GPIIO), // 配置成gpio0
.pull_up_en = true, // 上拉输入
};
gpio_config(&io_conf);// gpio初始化
gpio_install_isr_service(0);// 安装中断驱动
gpio_isr_handler_add(BOOT_KEY_GPIIO, gpio_isr_handler, (void *) BOOT_KEY_GPIIO);// 给引脚绑定中断服务函数
uint32_t gpio_num; // 用于接收gpio中断的消息
uint32_t last_press_timetamp = UINT32_MAX; // 记录最后按下的时间
gpio_evt_queue = xQueueCreate(1, sizeof(uint32_t)); // 创建gpio队列
bool status = 0; // 记录goio的状态
// 下面是按钮逻辑处理
while (xQueueReceive(gpio_evt_queue, &gpio_num, portMAX_DELAY)) {
bool key_level = gpio_get_level(gpio_num);
vTaskDelay(pdMS_TO_TICKS(10));
// 经过10ms查询按钮电平是确定按钮不是处于抖动状态,这一步是为了消抖
if (key_level != gpio_get_level(gpio_num)) {
continue;
}
// 当按钮处于松开且最后一次按下的时间小于当前时间时
if (key_level == 1 && last_press_timetamp < esp_timer_get_time() / 1000) {
// 从按下到松开的时间小于按钮按钮绑定时间时发送开关灯指令
// 按钮按下的时间大于绑定时间小于解绑时间时发送绑定指令
// 按钮按下的时间大于解绑的时间时发送解绑指令
if (esp_timer_get_time() / 1000 - last_press_timetamp < BOOT_KEY_PRESS_TIMEOUT_BIND) {
ESP_LOGI(TAG, "espnow_ctrl_initiator_send, status: %d", status);
// 第一个参数时发送者属性(按钮1),第二个时响应者的属性,第三个是数据
espnow_ctrl_initiator_send(ESPNOW_ATTRIBUTE_KEY_1, ESPNOW_ATTRIBUTE_POWER, status);
status = !status;
} else if (esp_timer_get_time() / 1000 - last_press_timetamp < BOOT_KEY_PRESS_TIMEOUT_UNBIND) {
ESP_LOGI(TAG, "espnow_ctrl_initiator_bind");
espnow_ctrl_initiator_bind(ESPNOW_ATTRIBUTE_KEY_1, true);
} else {
ESP_LOGI(TAG, "espnow_ctrl_initiator_unbind");
espnow_ctrl_initiator_bind(ESPNOW_ATTRIBUTE_KEY_1, false);
}
}
// 当按钮按下的时候记录时间,否则设置成最大时间(最大相当于不记录)
last_press_timetamp = (key_level == 0) ? esp_timer_get_time() / 1000 : UINT32_MAX;
}
ESP_LOGW(TAG, "Initiator key task is exit");// 能到这里说明代码跑飞了
vTaskDelete(NULL); // 删除自己的线程
}
/**
* @brief 灯响应任务
*
*/
static void responder_light_task(void *arg)
{
// 配置默认的rmt,用于驱动ws2812
rmt_config_t config = RMT_DEFAULT_CONFIG_TX(CONFIG_LED_STRIP_GPIO, RMT_CHANNEL_0);
// 将计数器时钟设置为40M
config.clk_div = 2;
// 导入rmt配置,初始化rmt
ESP_ERROR_CHECK(rmt_config(&config));
// 安装rmt驱动
ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, 0));
// 配置默认的ws2812
led_strip_config_t strip_config = LED_STRIP_DEFAULT_CONFIG(1, (led_strip_dev_t)config.channel);
// 导入配置,初始化ws2812
g_strip_handle = led_strip_new_rmt_ws2812(&strip_config);
// 清除灯带颜色 (设置成白色)
g_strip_handle->set_pixel(g_strip_handle, 0, 255, 255, 255);
// 刷新灯带状态
ESP_ERROR_CHECK(g_strip_handle->refresh(g_strip_handle, 100));
// 用于接收发送者信息
espnow_attribute_t initiator_attribute;
// 用于接收响应信息
espnow_attribute_t responder_attribute;
// 用于接收响应值
uint32_t responder_value;
bool status = true;
uint8_t red = 255;
uint8_t green = 255;
uint8_t blue = 255;
// 设置接收响应绑定参数,第一个是响应超时,第二个是响应最低信号强度,第三个是回调
espnow_ctrl_responder_bind(30 * 1000, -55, NULL);
// 循环接收信号
while (espnow_ctrl_responder_recv(&initiator_attribute, &responder_attribute, &responder_value) == ESP_OK) {
ESP_LOGI(TAG, "espnow_ctrl_responder_recv, initiator_attribute: %d, responder_attribute: %d, value: %d",
initiator_attribute, responder_attribute, responder_value);
// 解析接收信号
switch (responder_attribute) {
// 这个用来开关led
case ESPNOW_ATTRIBUTE_POWER:
status = responder_value;
break;
// 这个信号用来设置led r的值
case ESPNOW_ATTRIBUTE_RED:
red = responder_value;
break;
// 这个信号用来设置led g的值
case ESPNOW_ATTRIBUTE_BLUE:
blue = responder_value;
break;
// 这个信号用来设置led b的值
case ESPNOW_ATTRIBUTE_GREEN:
green = responder_value;
break;
default:
break;
}
// 实现接收信号
if (status) {
g_strip_handle->set_pixel(g_strip_handle, 0, red, blue, green);
g_strip_handle->refresh(g_strip_handle, 100);
} else {
g_strip_handle->clear(g_strip_handle, 100);
}
}
ESP_LOGW(TAG, "Responder light task is exit"); // 除了跑飞,不可能跑到这里
vTaskDelete(NULL); // 删除此线程
}
/**
* @brief 这里是事件回调函数,还记得之前有设置一个默认的事件循环函数吧,后面有绑定事件,这里用来处理
* 这里的事件都是系统产生的,不用我们自己写,只要做处理就ok
*/
static void espnow_event_handler(void* handler_args, esp_event_base_t base, int32_t id, void* event_data)
{
// 如果不是espnow的事件,就不做处理
if(base != ESP_EVENT_ESPNOW) {
return ;
}
// 处理相应事件
switch (id) {
// 绑定的事件处理
case ESP_EVENT_ESPNOW_CTRL_BIND: {
espnow_ctrl_bind_info_t *info = (espnow_ctrl_bind_info_t *)event_data;
ESP_LOGI(TAG, "band, uuid: " MACSTR ", initiator_type: %d", MAC2STR(info->mac), info->initiator_attribute);
g_strip_handle->set_pixel(g_strip_handle, 0, 0x0, 255, 0x0); //亮绿灯
ESP_ERROR_CHECK(g_strip_handle->refresh(g_strip_handle, 100));
break;
}
// 解绑的事件处理
case ESP_EVENT_ESPNOW_CTRL_UNBIND: {
espnow_ctrl_bind_info_t *info = (espnow_ctrl_bind_info_t *)event_data;
ESP_LOGI(TAG, "unband, uuid: " MACSTR ", initiator_type: %d", MAC2STR(info->mac), info->initiator_attribute);
g_strip_handle->set_pixel(g_strip_handle, 0, 255, 0x0, 0x00);//亮红灯
ESP_ERROR_CHECK(g_strip_handle->refresh(g_strip_handle, 100));
break;
}
default:
break;
}
}
void app_main(void)
{
esp_log_level_set("*", ESP_LOG_INFO); // 为给定标签设置日志级别
esp_storage_init(); // 打开nvs,不然wifi会报错
wifi_init(); // wifi 初始化
espnow_config_t espnow_config = ESPNOW_INIT_CONFIG_DEFAULT(); // 默认参数配置espnow
espnow_init(&espnow_config); // 加载参数并初始化
esp_event_handler_register(ESP_EVENT_ESPNOW, ESP_EVENT_ANY_ID, espnow_event_handler, NULL); // 绑定回调函数到事件处理
// 开始按钮任务
xTaskCreate(initiator_key_task, "initiator_key_task", 4 * 1024, NULL, tskIDLE_PRIORITY + 1, NULL);
// 开始rgb led灯任务
xTaskCreate(responder_light_task, "responder_light_task", 4 * 1024, NULL, tskIDLE_PRIORITY + 1, NULL);
}
这里做了简单的解析,其主要功能是通过下载好的两个设备可以通过按钮绑定并且可以通过按钮控制绑定好的led亮灭。如何使用可以参照官方的readme:https://github.com/espressif/esp-now/blob/master/examples/control/README.md
吐槽一点
本篇代码其实乍一看有个bug,用来保存按下的时间是使用uint32_t去声明,其最大值也是UINT32_MAX,但是esp_timer_get_time()的时间是启动后的时间(以微秒为单位)是int64_t 类型。这就有可能在一定时间后很长一段时间内last_press_timetamp < esp_timer_get_time() / 1000变为恒等的式子且last_press_timetamp = (key_level == 0) ? esp_timer_get_time() / 1000 : UINT32_MAX;这里的结果会溢出。不知道他们怎么想的。。。
总结
总结一下esp_now在这里所出现的用法
// esp-now是基于wifi的所以wifi得开启
esp_storage_init();
wifi_init();
// 配置与初始化esp_now的相关参数
espnow_config_t espnow_config = ESPNOW_INIT_CONFIG_DEFAULT();
espnow_init(&espnow_config);
// 绑定回调用于相关的事件处理
esp_event_handler_register(ESP_EVENT_ESPNOW, ESP_EVENT_ANY_ID, espnow_event_handler, NULL);
// 绑定
espnow_ctrl_initiator_bind(ESPNOW_ATTRIBUTE_KEY_1, true);
// 解绑
espnow_ctrl_initiator_bind(ESPNOW_ATTRIBUTE_KEY_1, false);
// 设置接收参数
espnow_ctrl_responder_bind(30 * 1000, -55, NULL);
// 发送信息
espnow_ctrl_initiator_send(ESPNOW_ATTRIBUTE_KEY_1, ESPNOW_ATTRIBUTE_POWER, status);
// 接收espnow信息
espnow_ctrl_responder_recv(&initiator_attribute, &responder_attribute, &responder_value)
这些是基于他们写好的一些库来实现的。如果想要自定义传输内容,就有必要分析一下components里面的内容,这里咱们放到下篇再说。