networking and communications

This week, I got my two boards from the previous weeks to communicate with each other. To do this, I set up a small network. In order to understand the process step by step, I first started by sending a simple piece of code from one board to the other. Once that worked, I moved on to a more advanced test: I connected a pulse sensor to the first board and sent the data to the second board, which then displayed the readings on an LCD screen.

For this setup, I used the following components:

 - 2 Xiao ESP32-C6 boards

 -  1 pulse sensor (KY-031)

 - 1 LCD screen

 - 2 USB-C cables

With this configuration, I was able to successfully establish basic wireless communication between the two boards and display live sensor data on the screen.

ESP-NOW

ESP-NOW is a wireless communication protocol created by Espressif (the company behind the popular ESP32 and ESP8266 chips). It allows fast, direct, and low-power communication between ESP devices — without needing a traditional Wi-Fi router. How does ESP-NOW work?

Connectionless: Unlike normal Wi-Fi, ESP-NOW doesn’t require a handshake or maintained connection. One device can send data to another as soon as it knows the other’s MAC address.

MAC address based: Each device communicates by targeting the receiver’s unique MAC address.

Pairing: Devices must be "paired" before they can communicate. This means exchanging MAC addresses and registering each other once during setup.

Roles: Devices can act as senders, receivers — or both at the same time.

Small payloads: You can send up to 250 bytes per message, which is more than enough for sensor values or commands.

Low latency: Because there's no need for a full Wi-Fi connection, the response time is super quick.

Optional encryption: ESP-NOW supports AES encryption, so if you want secure communication, you can add a key exchange during pairing.

Advantages of ESP-NOW

- No Wi-Fi router required

- Very fast data transmission

- Low power consumption (great for battery-powered projects)

- Simple to implement

- Decent range (similar to standard Wi-Fi)

Limitations

- Small data size (max. 250 bytes per packet)

- No automatic routing like in mesh networks

- Mostly limited to ESP32 and ESP8266 devices

Typical use cases include sensor networks, remote control systems, or small home automation projects — exactly the kind of stuff I’m working on right now.

For me, ESP-NOW has been the ideal solution to create a fast and direct communication channel between my boards, without the hassle of setting up complex networks. If you’re looking for a simple way to connect multiple ESP devices, you should definitely give ESP-NOW a try.

receiver

This is the receiver

sender

And this is the transmitter.

Before You Start

Before getting started, make sure you’ve added all the necessary libraries to your project. Without them, the code won’t compile correctly. I included the following libraries in my setup:

esp_now.h           // This enables the use of ESP-NOW communication functions.
WiFi.h               // Required by ESP-NOW for wireless functionality.
LiquidCrystal_I2C.h // Used to control the LCD display.
LiquidCrystal_I2C lcd(0x27, 16, 2); // Initializes the LCD screen (address 0x27, 16 columns, 2 rows).

Note: If you’re not sure how to add libraries in the Arduino IDE, I explained this in detail back in Week 8. Feel free to take another look if you need a quick reminder!

Step 1: Basic Communication – Random Numbers

In the first step, I wanted to test basic communication between my two ESP32 boards. To do that, I set up a simple example: one board sends random numbers at regular intervals, and the other board receives and displays them via the serial monitor.

This helped me verify that the ESP-NOW connection was working properly before moving on to more complex data like sensor readings. Code Section

randomvalue


1.1: The Receiver – “Listener” Board

The following code runs on the ESP32 board that receives the messages. I like to think of it as the “listener” board, since it’s always waiting for incoming data. Note: If you’re trying this yourself, make sure you register the sender board’s MAC address correctly — either hardcoded in the receiver’s code or dynamically during pairing. Without the sender’s MAC address, the receiver won’t be able to get any data.

#include <esp_now.h>
#include <WiFi.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);

// Structure for the received data
typedef struct struct_message {
    int sensorValue;
} struct_message;

// Create an instance of the data structure
struct_message myData;

// Callback function that is called when data is received
void OnDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *incomingData, int len) {
  struct struct_message receivedData;
  memcpy(&receivedData, incomingData, sizeof(receivedData));
  Serial.print("Bytes received: ");
  Serial.println(len);
  Serial.print("Sensor value received: ");
  Serial.println(receivedData.sensorValue);
  Serial.print("Sender MAC address: ");
  for (int i = 0; i < 6; i++) {
    if (recv_info->src_addr[i] < 16) {
      Serial.print("0");
    }
    Serial.print(recv_info->src_addr[i], HEX);
    if (i < 5) {
      Serial.print(":");
    }
  }
  Serial.println();

  // Display the received sensor value on the LCD
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Sensor Value:");
  lcd.setCursor(0, 1);
  lcd.print(receivedData.sensorValue);
}

void setup() {
  Serial.begin(115200);
  Serial.println("ESP-NOW Receiver");

  // Initialize the LCD
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("Receiving..."); // Initial message on the LCD

  // Set the ESP in Wi-Fi station mode
  WiFi.mode(WIFI_STA);

  // Initialize ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Register the callback function for data reception
  esp_now_register_recv_cb(OnDataRecv);
}

void loop() {
  delay(100);
}

1.2: “Send Simple Random Numbers” (The Sender)

This code runs on the ESP32 that sends the random numbers. Think of it as the “speaker” board.

#include <esp_now.h> // Include the ESP-NOW library for wireless communication
#include <WiFi.h>    // Include the Wi-Fi library for basic Wi-Fi functionalities

// MAC address of the receiver ESP32 (C6) - REPLACE WITH YOUR RECEIVER'S MAC ADDRESS!
uint8_t receiverAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // Placeholder

// Structure for the data to be sent
typedef struct struct_message {
    int sensorValue; // Integer variable to store the sensor value
} struct_message;

// Create an instance of the data structure
struct_message myData;

// Callback function that is called when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
    Serial.print("Transmission status: ");
    Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Successful" : "Failed");
}

void setup() {
    Serial.begin(115200); // Initialize serial communication at a baud rate of 115200
    Serial.println("ESP-NOW Sender");

    // Set the ESP in Wi-Fi station mode
    WiFi.mode(WIFI_STA);

    // Initialize ESP-NOW
    if (esp_now_init() != ESP_OK) {
        Serial.println("Error initializing ESP-NOW");
        return;
    }

    // Register the callback function for the send status
    esp_now_register_send_cb(OnDataSent);

    // Add the peer (receiver)
    esp_now_peer_info_t peerInfo;
    memcpy(peerInfo.peer_addr, receiverAddress, 6); // Copy the receiver's MAC address
    peerInfo.channel = 0;  // Must be the same channel on both sender and receiver
    peerInfo.encrypt = false; // Encryption is disabled for simplicity

    // Check if the peer already exists
    esp_err_t addStatus = esp_now_add_peer(&peerInfo);
    if (addStatus == ESP_OK) {
        Serial.println("Peer added successfully");
    } else if (addStatus == ESP_ERR_ESPNOW_NOT_FOUND) {
        Serial.println("Error adding peer: Peer not found");
    } else if (addStatus == ESP_ERR_ESPNOW_EXIST) {
        Serial.println("Peer already exists");
    } else {
        Serial.printf("Error adding peer: %d\n", addStatus);
    }
}

void loop() {
    // Read the actual sensor value here
    myData.sensorValue = random(0, 100); // Placeholder for the sensor value
    Serial.print("Sending sensor value: ");
    Serial.println(myData.sensorValue);

    // Send the data
    esp_err_t result = esp_now_send(receiverAddress, (uint8_t *) &myData, sizeof(myData));
    if (result != ESP_OK) {
        Serial.println("Error sending the data");
        Serial.println(esp_err_to_name(result));
    }

    delay(1000); // Send the value once per second (can be adjusted)
}

Step 2: Sending Heartbeat Data and Displaying It as a Live Graph

In the second step, I expanded the functionality. Instead of sending random numbers, the sender board now transmits data from a heartbeat sensor (KY-031).
The receiver board processes this data and displays it on the screen as a kind of continuous graph to visualize the heartbeat in real time.

coderandom


2.1: Sending Heartbeat Data (The Sender)

This code runs on the ESP32 that reads the heartbeat sensor and sends the data.

#include <esp_now.h> //include esp_now.h
#include <WiFi.h>    //include WiFi.h

// MAC address of the receiver ESP32 (C6) - REPLACE WITH YOUR RECEIVER'S MAC ADDRESS!
uint8_t receiverAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // Placeholder

// Structure for the data to be sent
typedef struct struct_message {
    int sensorValue;
} struct_message;

// Create an instance of the data structure
struct struct_message myData;

// Define the pin connected to the KY-031 heartbeat sensor
const int KY_031 = 0; // Assuming D0 is GPIO0 on your ESP32 C& board. Check your board's pinout.

// Callback function that is called when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("Transmission status: ");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Successful" : "Failed");
}

void setup() {
  Serial.begin(115200);
  Serial.println("ESP-NOW Sender with KY-031 Heartbeat Sensor");

  // Set the ESP in Wi-Fi station mode
  WiFi.mode(WIFI_STA);

  // Initialize ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Register the callback function for the send status
  esp_now_register_send_cb(OnDataSent);

  // Add the peer (receiver)
  esp_now_peer_info_t peerInfo;
  memcpy(peerInfo.peer_addr, receiverAddress, 6);
  peerInfo.channel = 0; // Must be the same on both devices
  peerInfo.encrypt = false;

  // Check if the peer already exists
  esp_err_t addStatus = esp_now_add_peer(&peerInfo);
  if (addStatus == ESP_OK) {
    Serial.println("Peer added successfully");
  } else if (addStatus == ESP_ERR_ESPNOW_NOT_FOUND) {
    Serial.println("Error adding peer: Peer not found");
  } else if (addStatus == ESP_ERR_ESPNOW_EXIST) {
    Serial.println("Peer already exists");
  } else {
    Serial.printf("Error adding peer: %d\n", addStatus);
  }

  // Initialize the heartbeat sensor pin as an input
  pinMode(KY_031, INPUT);
}

void loop() {
  // Read the value from the heartbeat sensor
  int sensorValue = digitalRead(KY_031);
  myData.sensorValue = sensorValue;
  Serial.print("Sending heartbeat value: ");
  Serial.println(myData.sensorValue);

  // Send the data
  esp_err_t result = esp_now_send(receiverAddress, (uint8_t *) &myData, sizeof(myData));
  if (result != ESP_OK) {
    Serial.println("Error sending the data");
    Serial.println(esp_err_to_name(result));
  }

  delay(50); // Same delay
}

2.2: Displaying a Live Graph (The Receiver)

This code runs on the ESP32 that receives the heartbeat data and displays it on the screen as a live graph.

#include <esp_now.h> //include esp_now.h
#include <WiFi.h>    //include WiFi.h
#include <LiquidCrystal_I2C.h> //include LiquidCrystal_I2C.h
#include <string.h> // For memcpy include string.h

// Define the I2C address and dimensions of the LCD
LiquidCrystal_I2C lcd(0x27, 16, 2); // Adjust address if needed

// Structure for the received data
typedef struct struct_message {
    int sensorValue;
} struct_message;

// Create an instance of the data structure
struct struct_message myData;

// Buffer to store the heartbeat waveform (16 characters + null terminator)
char heartbeatBuffer[17] = ""; // Initialize with '_'

// Callback function that is called when data is received
void OnDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *incomingData, int len) {
  struct struct_message receivedData;
  memcpy(&receivedData, incomingData, sizeof(receivedData));
  Serial.print("Bytes received: ");
  Serial.println(len);
  Serial.print("Heartbeat value received: ");
  Serial.println(receivedData.sensorValue);
  Serial.print("Sender MAC address: ");
  for (int i = 0; i < 6; i++) {
    if (recv_info->src_addr[i] < 16) {
      Serial.print("0");
    }
    Serial.print(recv_info->src_addr[i], HEX);
    if (i < 5) {
      Serial.print(":");
    }
  }
  Serial.println();

  // Shift the buffer to the left by one position
  memmove(heartbeatBuffer, heartbeatBuffer + 1, sizeof(heartbeatBuffer) - 2); // -2 to leave space for the new char and null terminator

  // Add the new heartbeat value to the end of the buffer
  if (receivedData.sensorValue == 0) {
    heartbeatBuffer[15] = '_';
  } else if (receivedData.sensorValue == 1) {
    heartbeatBuffer[15] = '^';
  }
  heartbeatBuffer[16] = '\0'; // Ensure null termination

  // Display the heartbeat waveform on the second line of the LCD
  lcd.setCursor(0, 1);
  lcd.print(heartbeatBuffer);
}

void setup() {
  Serial.begin(115200);
  Serial.println("ESP-NOW Receiver for Heartbeat");

  // Initialize the LCD
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("Heartbeat:");
  lcd.setCursor(0, 1);
  lcd.print(heartbeatBuffer); // Initialize the second line with underscores

  // Set the ESP in Wi-Fi station mode
  WiFi.mode(WIFI_STA);

  // Initialize ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Register the callback function for data reception
  esp_now_register_recv_cb(OnDataRecv);
}

void loop() {
  delay(100); // Keep the receiver running
}

If you need a good holder for the pulse sensor on your finger, feel free to use this 3D print file and get yourself some Velcro strap.

fingerring fingerring2

Group Assignment

In our group assignment, I learned how to connect two Xiao boards using I2C. Compared to my previous experience with ESP-NOW, I2C is much easier to set up. However, it always requires a wired connection. While I2C is great for fast and simple communication, a wireless solution like ESP-NOW is more suitable for long-term projects where cables would be impractical.

Click here for our group assignment