ESP-NOW

ESP-NOW is a great protocol when it comes to low latency applications because the accesspoint-less communication enables ultra fast message transport. The delay between the button press and the receiving of the sent message is as low as 200ms. This is great for smart home solutions where you immediately see the result (light/outlet goes on).

When using ESP-NOW with the Picoclicks you have to use an ESP based bridge which converts these messages into ones of another protocol. In the most cases an ESP-NOW to MQTT bridge is the best way to go with. Before talking about the bridge let's have a look at the senders code.

ESP-NOW sender

Here, the sender is the IOT-button itself. Once the Picoclick is pressed a message will be filled with informations and sent to its destination. Afterwards the Picoclick will deactivate itself again. A functional code can look like this:

#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <FastLED.h>
#include "config.h"

// ESPNOW packet structure.
// Can be modified but should be the same on the receivers side.
typedef struct struct_message {
  int id;
  int value;
  int battery_level;
  int single_tap_duration;
} struct_message;

typedef struct struct_message_recv {
    bool answer;
} struct_message_recv;

struct_message data;
struct_message_recv data_recv;

#define ESPNOW_ID 8888 // Random 4 digit number
uint8_t receiver_address[] = {0x10, 0x91, 0xA8, 0x32, 0x7B, 0x70}; // Mac address of the receiver.

bool espnow_answer_received = false;

void on_data_recv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  memcpy(&data_recv, incomingData, sizeof(data_recv));
  espnow_answer_received = true;
}

void setup(){
  pinMode(BUTTON_PIN, INPUT);
  pinMode(ADC_ENABLE_PIN, OUTPUT);
  pinMode(ADC_PIN, INPUT);
  analogReadResolution(12);
  digitalWrite(ADC_ENABLE_PIN, HIGH);

  btStop();
  WiFi.mode(WIFI_STA);

  FastLED.addLeds<APA102, APA102_SDI_PIN, APA102_CLK_PIN, BGR>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
  FastLED.setBrightness(160);
  delay(50);

  if(esp_now_init() != ESP_OK) {
    printf("Error initializing ESP-NOW\r\n");
    return;
  }

  set_fastled(CRGB::Blue);
  
  esp_now_peer_info_t peerInfo;

  memcpy(peerInfo.peer_addr, receiver_address, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
      
  if(esp_now_add_peer(&peerInfo) != ESP_OK){
    printf("Failed to add peer\r\n");
    return;
  }

  esp_now_register_recv_cb(on_data_recv);

  // Fill ESPNOW struct with values.
  data.id = ESPNOW_ID;
  data.value = 1;
  data.battery_level = int(get_battery_voltage());
  data.single_tap_duration = 1000;

  esp_now_send(receiver_address, (uint8_t *) &data, sizeof(data));

  // wait on espnow answer
  unsigned long t_wait_answer_start = millis();
  while(!espnow_answer_received && millis() <= t_wait_answer_start + 300){
    delayMicroseconds(1);
  }

  // This will reduce power consumption.
  WiFi.mode(WIFI_OFF);
  setCpuFrequencyMhz(10);

  CRGB col = espnow_answer_received ? CRGB::Green : CRGB::Red;
  set_fastled(col);
  delay(500);
  
  int counter = 0;
  while(digitalRead(BUTTON_PIN) == 1){
    set_fastled(counter % 2 == 0 ? CRGB::Blue : CRGB::Black, (counter+1) % 2 == 0 ? CRGB::Blue : CRGB::Black);
    delay(50);
    counter++;
  }

  set_fastled(CRGB::Blue);
  delay(500);

  // Add a loop which will wait as long as the button is pressed before entering deepsleep.
  // Once in deepsleep the USB console is not available anymore.
  esp_deep_sleep_start();
}

void loop() {
  
}

The code above does one more thing which ensures that the message delivery was succesfull. After it sends out the initial message with all the informations it waits for an answer form the receiver device for 300ms:

while(!espnow_answer_received && millis() <= t_wait_answer_start + 300){
    delayMicroseconds(1);
}

The espnow_answer_received flag will be set to true if the receive callback is executed, so once a message is received at the sender. If this isn't the case the while loop will be escaped after 300ms. To give the user a feedback the LEDs will light up in green or red depending on the receiving state.

CRGB col = espnow_answer_received ? CRGB::Green : CRGB::Red;

ESP-NOW receiver

The receiver can be any ESP based device but in this case I'm using a Picoclick as well. Once the button is pressed the receiver is powered on and will wait for a message from the sender. While waiting the LEDs will cycle through all colors.

Once a message is received the send_answer() function will be called. It adds an ESP-NOW peer with the received MAC address of the sender device, sends the answer message and will delete the peer afterwards.

As the receiver devices turns its LEDs green just like the sender, both devices will light up in green at almost the same time.

#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <FastLED.h>
#include "config.h"

// ESPNOW packet structure.
// Can be modified but should be the same on the receivers side.
typedef struct struct_message {
  int id;
  int value;
  int battery_level;
  int single_tap_duration;
} struct_message;

typedef struct struct_message_recv {
    bool answer;
} struct_message_recv;

struct_message data;
struct_message_recv data_answer;

#define ESPNOW_ID 8888 // Random 4 digit number
uint8_t receiver_address[] = {0x10, 0x91, 0xA8, 0x32, 0x7B, 0x70}; // Mac address of the receiver. 10:91:A8:32:7B:70

uint8_t temp_address[6];
uint8_t last_recv_address[6];

String mac;

bool need_answer = false;

void on_data_recv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  memcpy(&data, incomingData, sizeof(data));
  memcpy(temp_address, mac, 6);
  need_answer = true;
}

esp_now_peer_info_t peerInfo;

void send_answer(){
  memcpy(peerInfo.peer_addr, temp_address, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
      
  if(esp_now_add_peer(&peerInfo) != ESP_OK){
    printf("Failed to add peer\r\n");
    return;
  }

  data_answer.answer = true;

  esp_now_send(temp_address, (uint8_t *) &data_answer, sizeof(data_answer));
  if(esp_now_del_peer(temp_address) != ESP_OK){
    printf("Failed to delete peer\r\n");
    return;
  }
  memcpy(last_recv_address, temp_address, 6);
  memset(temp_address, 0, 6);

  set_fastled(CRGB::Green);
}

String mac_to_string(uint8_t *addr){
  String mac_str = String(addr[0], HEX) + ":" + String(addr[1], HEX) + ":" + String(addr[2], HEX) + ":"
    + String(addr[3], HEX) + ":" + String(addr[4], HEX) + ":" + String(addr[5], HEX);
  mac_str.toUpperCase();
  return mac_str;
}

void setup(){
  pinMode(BUTTON_PIN, INPUT);
  pinMode(ADC_ENABLE_PIN, OUTPUT);
  pinMode(ADC_PIN, INPUT);
  analogReadResolution(12);
  digitalWrite(ADC_ENABLE_PIN, HIGH);

  WiFi.mode(WIFI_STA);

  FastLED.addLeds<APA102, APA102_SDI_PIN, APA102_CLK_PIN, BGR>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
  FastLED.setBrightness(160);
  delay(50);

  if(esp_now_init() != ESP_OK) {
    printf("Error initializing ESP-NOW\r\n");
    return;
  }

  set_fastled(CRGB::Blue);

  esp_now_register_recv_cb(on_data_recv);

  mac = WiFi.macAddress();
  // printf("MAC %s\r\n", mac.c_str());

  delay(500);
}

unsigned long led_timer = millis();
int hue1 = 0, hue2 = 0;
int brightness = 255;

void loop() {
  if(digitalRead(BUTTON_PIN) == 1){
    set_fastled(CRGB::Red);
    delay(1000);
    esp_deep_sleep_start();
  }

  if(need_answer){
    need_answer = false;
    send_answer();
    led_timer += 1000;
  }

  if(millis() >= led_timer + 15){
    led_timer = millis();
    set_fastled(CHSV(hue1, 255, brightness), CHSV(hue2, 255, brightness));
    hue1 = (hue1 + 1)%255;
    hue2 = (hue1 + 127)%255;
  }
}

ESP-NOW to MQTT bridge

The following code is a ESP-NOW to MQTT bridge on a single device. To make things work, you need to change the channel of your router to the fixed channel 1. For further informations for that or the Home Assistant integration you should check out the video.

To run this code you need to add your WiFi credentials in the top section and your MQTT credentials in the reconnect function.

The code forwards all ESP-NOW messages as MQTT messages to be integrated into Home Assistant. The topic is generated by the identifier "node" and the four digit "ESPNOW_ID". A typical topic can look like this: home/node1234/value

#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <PubSubClient.h>
#include <time.h>

const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PWD";
const char* mqtt_server = "YOUR_MQTT_IP"; // Example: 192.168.1.4

WiFiClient espClient;
PubSubClient client(espClient);

String current_time_str = "";

const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 3600;
const int   daylightOffset_sec = 3600;
struct tm timeinfo;
int current_seconds = 0;

int32_t channel;

#define LED_PIN     15 // Wemos S2: 15, ThingPlusS2: 13

typedef struct struct_message {
    int id;
    int value;
    int battery_level;
    int single_tap_duration;
} struct_message;

typedef struct struct_message_recv {
    bool answer;
} struct_message_recv;

uint8_t temp_address[6];

bool need_answer = false;

struct_message data;
struct_message_recv data_answer;
bool new_data_received = false;
bool new_data_to_mqtt = false;
unsigned long t_new_data_received = 0;

#define N_SINGLETAP_SLOTS   10
bool reset_after_single_tap = false;
unsigned long t_reset_single_tap[N_SINGLETAP_SLOTS];
int reset_after_single_tap_id[N_SINGLETAP_SLOTS];
int single_tap_duration[N_SINGLETAP_SLOTS];


int battery_percentage[] = {4200, 4150, 4110, 4080, 4020, 3980, 3950, 3910, 3870, 3850, 3840, 3820, 3800, 3790, 3770, 3750, 3730, 3710, 3690, 3610, 3270};
int get_battery_percentage(float mv){
  int battery_mv = int(mv);
  int perc = 0;
  for(int i=0; i<=20; i++) if(battery_mv > battery_percentage[20-i]) perc+=5;
  return constrain(perc, 0, 100);
}

String get_formatted_time(){
  String t = "";
  if(timeinfo.tm_hour < 10) t += "0";
  t += timeinfo.tm_hour;
  t += ":";
  if(timeinfo.tm_min < 10) t += "0";
  t += timeinfo.tm_min;
  t += ":";
  if(timeinfo.tm_sec < 10) t += "0";
  t += timeinfo.tm_sec;
  return t;
}


void on_data_recv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  digitalWrite(LED_PIN, 1);
  memcpy(&data, incomingData, sizeof(data));
  memcpy(temp_address, mac, 6);
  need_answer = true;

  printf("\r\n");
  printf("Receivid packet from %X:%X:%X:%X:%X:%X\r\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  printf("Data - ID: %i, Value: %i, Battery: %i, TapDuration: %i\r\n", data.id, data.value, data.battery_level, data.single_tap_duration);
  printf("\r\n");

  if(data.single_tap_duration > 0){
    reset_after_single_tap = true;
    bool slot_found = false;
    for(int i=0; i<N_SINGLETAP_SLOTS; i++){
      if(single_tap_duration[i] == 0 && !slot_found){
        single_tap_duration[i] = data.single_tap_duration;
        reset_after_single_tap_id[i] = data.id;
        t_reset_single_tap[i] = millis();
        slot_found = true;
      }
    }
  }

  new_data_received = true;
  new_data_to_mqtt = true;
  t_new_data_received = millis();
}

esp_now_peer_info_t peerInfo;

void send_answer(){
  memcpy(peerInfo.peer_addr, temp_address, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
      
  if(esp_now_add_peer(&peerInfo) != ESP_OK){
    printf("Failed to add peer\r\n");
    return;
  }

  data_answer.answer = true;
  esp_now_send(temp_address, (uint8_t *) &data_answer, sizeof(data_answer));
  delay(10);
  if(esp_now_del_peer(temp_address) != ESP_OK){
    printf("Failed to delete peer\r\n");
    return;
  }
  memset(temp_address, 0, 6);
}
 

void callback(char* topic, byte* message, unsigned int length) {
  // digitalWrite(LED_PIN, 1);
  String messageTemp;
  
  for (int i = 0; i < length; i++) {
    messageTemp += (char)message[i];
  }
  // digitalWrite(LED_PIN, 0);
}

void reconnect() {
  while (!client.connected()) {
    digitalWrite(LED_PIN, 1);
    if (client.connect("MQTTClient_new", "YOUR_MQTT_USER", "YOUR_MQTT_PWD")) {
      client.subscribe("home/#");
      digitalWrite(LED_PIN, 0);
    } else {
      printf("Cannot connect to MQTT Server - Restarting in 5s!\r\n");
      delay(5000);
    }
  }
}

int32_t getWiFiChannel(const char *ssid) {
  if (int32_t n = WiFi.scanNetworks()) {
      for (uint8_t i=0; i<n; i++) {
          if (!strcmp(ssid, WiFi.SSID(i).c_str())) {
              return WiFi.channel(i);
          }
      }
  }
  return 0;
}

void setup() {
  pinMode(LED_PIN, OUTPUT);

  WiFi.mode(WIFI_AP_STA);

  channel = getWiFiChannel(ssid);
  esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
  printf("WiFi channel: %i\r\n", channel);

  printf("Connecting to: %s\r\n", ssid);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    digitalWrite(LED_PIN, 1);
    delay(150);
    printf(".\r\n");
    digitalWrite(LED_PIN, 0);
    delay(150);
  }

  printf("WiFi connected with IP: %s\r\n", WiFi.localIP().toString().c_str());

  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);

  client.setServer(mqtt_server, 1883);
  // client.setCallback(callback);

  if (esp_now_init() != ESP_OK) {
    printf("Error initializing ESP-NOW\r\n");
    return;
  }
  
  esp_now_register_recv_cb(on_data_recv);

  reconnect();

  String payload_mac = String(WiFi.macAddress());
  client.publish("home/s2macaddr", payload_mac.c_str());
}

unsigned long t_send_time_running = 0;

void loop() {
  if(need_answer){
    need_answer = false;
    send_answer();
  }

  if(new_data_received){
    if(new_data_to_mqtt){
      new_data_to_mqtt = false;

      String topic_value = "home/node" + String(data.id) + "/value";
      String topic_battery = "home/node" + String(data.id) + "/battery";
      String topic_batterylevel = "home/node" + String(data.id) + "/batterylevel";

      client.publish(topic_value.c_str(), String(data.value).c_str());
      client.publish(topic_battery.c_str(), String(data.battery_level).c_str());
      client.publish(topic_batterylevel.c_str(), String(get_battery_percentage(data.battery_level)).c_str());
    }

    if(millis() >= t_new_data_received + 1000){
      digitalWrite(LED_PIN, 0);
      new_data_received = false;
    }
  }

  if(reset_after_single_tap){
    bool any_reset_open = false;
    for(int i=0; i<N_SINGLETAP_SLOTS; i++){
      if(single_tap_duration[i] > 0){
        any_reset_open = true;
        if(millis() >= t_reset_single_tap[i] + single_tap_duration[i]){
          printf("Send zero\r\n");
          String topic_value = "home/node" + String(reset_after_single_tap_id[i]) + "/value";
          client.publish(topic_value.c_str(), "0");

          single_tap_duration[i] = 0;
          reset_after_single_tap_id[i] = 0;
        }
      }
    }
    if(!any_reset_open){
      reset_after_single_tap = false;
    }
  }

  unsigned int start = millis();
  if(!getLocalTime(&timeinfo)){
    printf("Failed to obtain time\r\n");
  }
  unsigned int stop = millis();

  if(current_seconds != timeinfo.tm_sec){
    current_seconds = timeinfo.tm_sec;
    printf("Time: %s\r\n", get_formatted_time());
    if(timeinfo.tm_sec == 0){
      client.publish("home/current_time", get_formatted_time().c_str());
    }
  }

  if (!client.connected()) {
    reconnect();
  }

  client.loop();
}

Last updated