Skip to content

ESP-NOW COMMUNICATION

INTRODUCTION

ESP-NOW is a low-power, low-latency wireless communication protocol provided by Espressif, suitable for ESP8266 and ESP32 series chips. It allows direct point-to-point communication between devices without the need for a Wi-Fi router, enabling a more efficient communication method. It is based on the IEEE 802.11 protocol but omits the handshake and connection processes of traditional Wi-Fi communication.

ESP-NOW Features: - Low Power Consumption: ESP-NOW is designed for low-power applications, making it suitable for battery-powered devices, extending their lifespan.

  • Low Latency: ESP-NOW provides fast data transmission, suitable for latency-sensitive application scenarios.

  • Point-to-Point Communication: Devices can communicate directly with each other without the need for a Wi-Fi router, simplifying the network architecture.

  • Multi-Device Support: ESP-NOW supports communication between multiple devices, with support for up to 20 peer devices.

ESP-NOW COMMUNICATION STEPS

The basic steps for ESP-NOW communication are as follows:

  1. Initialize the ESP-NOW protocol stack.

  2. Configure peer device information, including MAC address and encryption key (if needed).

  3. Register send and receive callback functions to handle data transmission.

  4. Send data to the specified peer device.

  5. Handle received data in the receive callback function.

FIRST ESP-NOW COMMUNICATION EXAMPLE

Based on the AIoTNode-CPP-MORE version, divided into sender and receiver parts. Modifications are limited to the main.cpp file.

TX - SENDER

/**
 * @file main.cpp
 * @author SHUAIWEN CUI (SHUAIWEN001@e.ntu.edu.sg)
 * @brief ESP-NOW Transmitter Example - Simple and Easy to Understand Tutorial Version
 * @version 1.0
 * @date 2025-10-22
 * 
 * @copyright Copyright (c) 2024
 * 
 */

/* DEPENDENCIES */
// ESP Basic Libraries
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_chip_info.h"
#include "esp_psram.h"
#include "esp_flash.h"
#include "esp_log.h"
#include <string.h>

// FreeRTOS
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// ESP-NOW Related (ESP-NOW is based on WiFi, so these headers are needed)
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "esp_now.h"

// BSP Drivers
#include "node_led.h"
#include "node_exit.h"
#include "node_spi.h"
#include "node_lcd.h"
#include "node_timer.h"
#include "node_rtc.h"
#include "node_sdcard.h"

#ifdef __cplusplus
extern "C" {
#endif

/* ========== Global Variables ========== */
const char *TAG = "ESP_NOW_TX";

// Receiver MAC address (6 bytes)
// 0xFF means broadcast address, all ESP-NOW devices can receive
// To target a specific device, replace with the target device's MAC address
static uint8_t receiver_mac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// Send counter (used to identify each sent data)
static int send_count = 0;

// Receive counter (records the number of received messages)
static int recv_count = 0;

/* ========== Data Structure Definition ========== */
// Define the data structure we want to send
// This structure contains all the data to be transmitted
typedef struct {
    int counter;           // Send sequence number
    char text[30];        // Text message (max 30 characters)
    int value1;           // Value 1 (example)
    int value2;           // Value 2 (example)
} message_data_t;

/* ========== Callback Function: Notification after Send Completion ========== */
// When data transmission completes (success or failure), ESP-NOW will automatically call this function
// Note: ESP-IDF v6.0 changed the callback function signature, now uses wifi_tx_info_t structure
void send_callback(const wifi_tx_info_t *info, esp_now_send_status_t status)
{
    // status == ESP_NOW_SEND_SUCCESS means send successful
    if (status == ESP_NOW_SEND_SUCCESS)
    {
        ESP_LOGI(TAG, "✓ Send Success");
    }
    else
    {
        ESP_LOGE(TAG, "✗ Send Failed");
    }
}

/* ========== Callback Function: Data Received ========== */
// When ESP-NOW data is received, ESP-NOW will automatically call this function
// Note: ESP-IDF v6.0 changed the callback function signature, now uses esp_now_recv_info structure
void recv_callback(const esp_now_recv_info *recv_info, const uint8_t *data, int len)
{
    recv_count++;  // Increment receive counter

    // Get sender's MAC address from recv_info structure
    const uint8_t *mac_addr = recv_info->src_addr;

    // Print sender's MAC address
    ESP_LOGI(TAG, "Data received! From: %02X:%02X:%02X:%02X:%02X:%02X, Length: %d",
             mac_addr[0], mac_addr[1], mac_addr[2], 
             mac_addr[3], mac_addr[4], mac_addr[5], len);

    // If data length matches our message structure, parse it
    if (len == sizeof(message_data_t))
    {
        message_data_t *msg = (message_data_t *)data;
        ESP_LOGI(TAG, "Message content: counter=%d, text=%s, value1=%d, value2=%d",
                 msg->counter, msg->text, msg->value1, msg->value2);

        // Receive info is already displayed in the send task above, no need to display separately here
        // If need to update receive count, can update when send task executes next time
    }
}

/* ========== ESP-NOW Initialization Function ========== */
// This function is responsible for initializing WiFi and ESP-NOW protocol
esp_err_t espnow_init(void)
{
    esp_err_t ret = ESP_OK;

    // Step 1: Initialize network interface (ESP-NOW needs this)
    ESP_LOGI(TAG, "Step 1: Initialize network interface...");
    ret = esp_netif_init();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Network interface initialization failed");
        return ret;
    }

    // Step 2: Create event loop (ESP-NOW needs this to handle events)
    ESP_LOGI(TAG, "Step 2: Create event loop...");
    ret = esp_event_loop_create_default();
    if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
        ESP_LOGE(TAG, "Event loop creation failed");
        return ret;
    }

    // Step 3: Initialize WiFi (ESP-NOW is based on WiFi protocol)
    ESP_LOGI(TAG, "Step 3: Initialize WiFi...");
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ret = esp_wifi_init(&cfg);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "WiFi initialization failed");
        return ret;
    }

    // Step 4: Set WiFi to Station mode (client mode)
    // Note: ESP-NOW does not need to connect to router, only WiFi needs to be started
    ret = esp_wifi_set_storage(WIFI_STORAGE_RAM);
    ret = esp_wifi_set_mode(WIFI_MODE_STA);  // Station mode
    ret = esp_wifi_start();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "WiFi start failed");
        return ret;
    }

    // Step 5: Set WiFi channel (transmitter and receiver must be on the same channel)
    ret = esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Channel setting failed");
        return ret;
    }

    // Step 6: Initialize ESP-NOW protocol
    ESP_LOGI(TAG, "Step 4: Initialize ESP-NOW...");
    ret = esp_now_init();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "ESP-NOW initialization failed");
        return ret;
    }

    // Step 7: Register send callback function (will call send_callback after send completes)
    ESP_LOGI(TAG, "Step 5: Register send callback...");
    ret = esp_now_register_send_cb(send_callback);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Register send callback failed");
        return ret;
    }

    // Step 8: Register receive callback function (will call recv_callback when data is received)
    ESP_LOGI(TAG, "Step 6: Register receive callback...");
    ret = esp_now_register_recv_cb(recv_callback);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Register receive callback failed");
        return ret;
    }

    // Step 9: Add receiver peer (tell ESP-NOW where to send data)
    ESP_LOGI(TAG, "Step 7: Add receiver peer...");
    esp_now_peer_info_t peer_info;
    memset(&peer_info, 0, sizeof(peer_info));  // Clear structure
    memcpy(peer_info.peer_addr, receiver_mac, 6);  // Copy MAC address
    peer_info.channel = 1;        // Channel (must match WiFi channel)
    peer_info.ifidx = WIFI_IF_STA; // Use Station interface
    peer_info.encrypt = false;    // No encryption (simplified version)

    ret = esp_now_add_peer(&peer_info);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Add receiver peer failed");
        return ret;
    }

    // Get and print local MAC address
    uint8_t my_mac[6];
    esp_wifi_get_mac(WIFI_IF_STA, my_mac);
    ESP_LOGI(TAG, "✓ ESP-NOW initialization successful!");
    ESP_LOGI(TAG, "Local MAC address: %02X:%02X:%02X:%02X:%02X:%02X",
             my_mac[0], my_mac[1], my_mac[2], my_mac[3], my_mac[4], my_mac[5]);
    ESP_LOGI(TAG, "Target MAC address: %02X:%02X:%02X:%02X:%02X:%02X",
             receiver_mac[0], receiver_mac[1], receiver_mac[2], 
             receiver_mac[3], receiver_mac[4], receiver_mac[5]);

    return ESP_OK;
}

/* ========== Send Task Function ========== */
// This function runs in an independent FreeRTOS task, periodically sending data
void send_task(void *pvParameters)
{
    message_data_t message;  // Data to send
    char display_buf[80];     // LCD display buffer

    ESP_LOGI(TAG, "Send task started...");

    while (1)  // Infinite loop
    {
        // 1. Prepare data to send
        send_count++;  // Increment counter
        message.counter = send_count;
        snprintf(message.text, sizeof(message.text), "Hello #%d", send_count);
        message.value1 = send_count * 10;
        message.value2 = send_count * 20;

        // 2. Send data (asynchronous send, function returns immediately)
        // esp_now_send() does not wait after sending, result is notified via callback function
        esp_err_t ret = esp_now_send(receiver_mac, (uint8_t *)&message, sizeof(message));

        if (ret == ESP_OK)
        {
            // Simple display: only show 3 lines, sufficient spacing, unified 16-pixel font
            lcd_clear(WHITE);

            // Line 1: Title (Y=0, occupies 0-16)
            lcd_show_string(0, 0, 160, 16, 16, (char*)"ESP-NOW TX", GREEN);

            // Line 2: Send info (Y=24, 8-pixel spacing, occupies 24-40)
            snprintf(display_buf, sizeof(display_buf), "Send #%d", send_count);
            lcd_show_string(0, 24, 160, 16, 16, display_buf, BLACK);

            // Line 3: Receive info (Y=48, 8-pixel spacing, occupies 48-64)
            snprintf(display_buf, sizeof(display_buf), "Recv #%d", recv_count);
            lcd_show_string(0, 48, 160, 16, 16, display_buf, BLACK);

            ESP_LOGI(TAG, "Send data #%d: %s", send_count, message.text);
        }
        else
        {
            // Send failed
            ESP_LOGE(TAG, "Send failed: %s", esp_err_to_name(ret));
            lcd_clear(WHITE);
            lcd_show_string(0, 16, 160, 16, 12, (char*)"Send Failed!", RED);
        }

        // 4. Toggle LED state (visual feedback)
        led_toggle();

        // 5. Wait 2 seconds before sending again
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

/* ========== Main Function ========== */
void app_main(void)
{
    esp_err_t ret;
    uint32_t flash_size;
    esp_chip_info_t chip_info;

    // ========== Part 1: System Initialization ==========
    ESP_LOGI(TAG, "========== System Initialization ==========");

    // Initialize NVS (Non-Volatile Storage)
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }

    // Get chip information (for debugging)
    esp_flash_get_size(NULL, &flash_size);
    esp_chip_info(&chip_info);
    ESP_LOGI(TAG, "CPU cores: %d", chip_info.cores);
    ESP_LOGI(TAG, "Flash size: %ld MB", flash_size / (1024 * 1024));
    ESP_LOGI(TAG, "PSRAM size: %d bytes", esp_psram_get_size());

    // ========== Part 2: Hardware Initialization ==========
    ESP_LOGI(TAG, "========== Hardware Initialization ==========");

    led_init();    // Initialize LED
    exit_init();   // Initialize external interrupt
    spi2_init();   // Initialize SPI interface
    lcd_init();    // Initialize LCD display

    // Clear screen and show initialization message
    lcd_clear(WHITE);
    lcd_show_string(0, 0, 160, 16, 16, (char*)"Initializing...", BLUE);

    // Note: SD card initialization is skipped here because ESP-NOW doesn't need SD card
    // If SD card is needed, uncomment below
    /*
    while (sd_card_init())
    {
        lcd_show_string(0, 0, 200, 16, 16, (char*)"SD Card Error!", RED);
        vTaskDelay(500);
    }
    */

    // ========== Part 3: ESP-NOW Initialization ==========
    ESP_LOGI(TAG, "========== ESP-NOW Initialization ==========");

    lcd_fill(0, 0, 160, 16, WHITE);
    lcd_show_string(0, 0, 160, 16, 12, (char*)"Init ESP-NOW...", BLUE);

    ret = espnow_init();
    if (ret != ESP_OK)
    {
        // Initialization failed, show error on LCD and stop
        lcd_clear(WHITE);
        lcd_show_string(0, 0, 160, 16, 12, (char*)"ESP-NOW Init", RED);
        lcd_show_string(0, 16, 160, 16, 12, (char*)"Failed!", RED);
        ESP_LOGE(TAG, "ESP-NOW initialization failed, program stopped");
        while (1) {
            vTaskDelay(1000);  // Infinite wait
        }
    }

    // Initialization successful
    lcd_clear(WHITE);
    lcd_show_string(0, 0, 160, 16, 12, (char*)"ESP-NOW Ready!", GREEN);
    vTaskDelay(pdMS_TO_TICKS(1000));  // Wait 1 second for user to see the message

    // ========== Part 4: Create Send Task ==========
    ESP_LOGI(TAG, "========== Create Send Task ==========");

    // Create a FreeRTOS task to periodically send data
    // Parameter description:
    // - send_task: task function
    // - "send_task": task name
    // - 4096: task stack size (bytes)
    // - NULL: parameters passed to task
    // - 5: task priority (higher number = higher priority)
    // - NULL: task handle (no need to return)
    xTaskCreate(send_task, "send_task", 4096, NULL, 5, NULL);

    ESP_LOGI(TAG, "✓ Program started! Begin sending data...");

    // Main task can continue doing other things, or go to sleep
    while (1)
    {
        // Main task does nothing now, just waits
        // Actual send work is done by send_task
        vTaskDelay(pdMS_TO_TICKS(5000));  // Print every 5 seconds
        ESP_LOGI(TAG, "Main task running... (Send:%d, Recv:%d)", send_count, recv_count);
    }
}

#ifdef __cplusplus
}
#endif

RX - RECEIVER

/**
 * @file main.cpp
 * @author SHUAIWEN CUI (SHUAIWEN001@e.ntu.edu.sg)
 * @brief ESP-NOW Receiver Example - Easy to understand tutorial version (ESP-IDF v6.0)
 * @version 1.0
 * @date 2025-10-22
 * 
 * @copyright Copyright (c) 2024
 * 
 */

/* DEPENDENCIES */
// ESP Core Libraries
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_chip_info.h"
#include "esp_psram.h"
#include "esp_flash.h"
#include "esp_log.h"
#include <string.h>

// FreeRTOS
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// ESP-NOW related (ESP-NOW is based on WiFi, so these headers are needed)
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "esp_now.h"

// BSP Drivers
#include "node_led.h"
#include "node_exit.h"
#include "node_spi.h"
#include "node_lcd.h"
#include "node_timer.h"
#include "node_rtc.h"
#include "node_sdcard.h"

#ifdef __cplusplus
extern "C" {
#endif

/* ========== Data Structure Definition ========== */
// Define the data structure we will receive
// This structure contains all the data to be transmitted
typedef struct {
    int counter;           // Sequence number from sender
    char text[30];        // Text message (max 30 characters)
    int value1;           // Value 1 (example)
    int value2;           // Value 2 (example)
} message_data_t;

/* ========== Global Variables ========== */
const char *TAG = "ESP_NOW_RX";

// Receive counter (records the number of received messages)
static int recv_count = 0;

// Last received message (for LCD display)
static message_data_t last_message;
static bool message_received = false;

/* ========== Callback Function: Send Completion Notification ========== */
// Receivers usually don't need send callback, but keep this function in case reply is needed
// Note: ESP-IDF v6.0 changed the callback function signature, now using wifi_tx_info_t structure
void send_callback(const wifi_tx_info_t *info, esp_now_send_status_t status)
{
    // In receiver mode, this callback is usually not used
    // If you need to reply to the sender, you can use this callback
    if (status == ESP_NOW_SEND_SUCCESS)
    {
        ESP_LOGI(TAG, "✓ Reply sent successfully");
    }
    else
    {
        ESP_LOGE(TAG, "✗ Reply send failed");
    }
}

/* ========== Callback Function: Data Received ========== */
// This function is automatically called by ESP-NOW when data is received
// Note: ESP-IDF v6.0 changed the callback function signature, now using esp_now_recv_info structure
void recv_callback(const esp_now_recv_info *recv_info, const uint8_t *data, int len)
{
    recv_count++;  // Increment receive counter

    // Get sender's MAC address from recv_info structure
    const uint8_t *mac_addr = recv_info->src_addr;

    // Print sender's MAC address
    ESP_LOGI(TAG, "✓ Data received! From: %02X:%02X:%02X:%02X:%02X:%02X, Length: %d",
             mac_addr[0], mac_addr[1], mac_addr[2], 
             mac_addr[3], mac_addr[4], mac_addr[5], len);

    // If data length matches our message structure, parse it
    if (len == sizeof(message_data_t))
    {
        message_data_t *msg = (message_data_t *)data;
        ESP_LOGI(TAG, "Message content: counter=%d, text=%s, value1=%d, value2=%d",
                 msg->counter, msg->text, msg->value1, msg->value2);

        // Save the last received message
        memcpy(&last_message, msg, sizeof(message_data_t));
        message_received = true;

        // Toggle LED to indicate data received
        led_toggle();
    }
    else
    {
        ESP_LOGW(TAG, "Data length mismatch! Expected: %d, Actual: %d", sizeof(message_data_t), len);
    }
}

/* ========== ESP-NOW Initialization Function ========== */
// This function initializes WiFi and ESP-NOW protocol
esp_err_t espnow_init(void)
{
    esp_err_t ret = ESP_OK;

    // Step 1: Initialize network interface (required for ESP-NOW)
    ESP_LOGI(TAG, "Step 1: Initializing network interface...");
    ret = esp_netif_init();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Network interface initialization failed");
        return ret;
    }

    // Step 2: Create event loop (required for ESP-NOW to handle events)
    ESP_LOGI(TAG, "Step 2: Creating event loop...");
    ret = esp_event_loop_create_default();
    if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
        ESP_LOGE(TAG, "Event loop creation failed");
        return ret;
    }

    // Step 3: Initialize WiFi (ESP-NOW is based on WiFi protocol)
    ESP_LOGI(TAG, "Step 3: Initializing WiFi...");
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ret = esp_wifi_init(&cfg);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "WiFi initialization failed");
        return ret;
    }

    // Step 4: Set WiFi to Station mode (client mode)
    // Note: ESP-NOW doesn't need to connect to router, just start WiFi
    ret = esp_wifi_set_storage(WIFI_STORAGE_RAM);
    ret = esp_wifi_set_mode(WIFI_MODE_STA);  // Station mode
    ret = esp_wifi_start();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "WiFi start failed");
        return ret;
    }

    // Step 5: Set WiFi channel (sender and receiver must be on the same channel)
    ret = esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Channel setting failed");
        return ret;
    }

    // Step 6: Initialize ESP-NOW protocol
    ESP_LOGI(TAG, "Step 6: Initializing ESP-NOW...");
    ret = esp_now_init();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "ESP-NOW initialization failed");
        return ret;
    }

    // Step 7: Register receive callback function (called when data is received)
    ESP_LOGI(TAG, "Step 7: Registering receive callback...");
    ret = esp_now_register_recv_cb(recv_callback);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Receive callback registration failed");
        return ret;
    }

    // Step 8: Optional - Register send callback (if you need to reply to sender)
    ESP_LOGI(TAG, "Step 8: Registering send callback...");
    ret = esp_now_register_send_cb(send_callback);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Send callback registration failed");
        return ret;
    }

    // Note: Receiver doesn't need to add peer, because receiver automatically receives all data sent to it
    // If you need to reply after receiving, you can dynamically add sender's peer when data is received

    // Get and print local MAC address
    uint8_t my_mac[6];
    esp_wifi_get_mac(WIFI_IF_STA, my_mac);
    ESP_LOGI(TAG, "✓ ESP-NOW receiver initialization successful!");
    ESP_LOGI(TAG, "Local MAC address: %02X:%02X:%02X:%02X:%02X:%02X",
             my_mac[0], my_mac[1], my_mac[2], my_mac[3], my_mac[4], my_mac[5]);
    ESP_LOGI(TAG, "Waiting for data...");

    return ESP_OK;
}

/* ========== Display Update Task Function ========== */
// This function runs in an independent FreeRTOS task, periodically updating LCD display
void display_task(void *pvParameters)
{
    char display_buf[80];     // LCD display buffer

    ESP_LOGI(TAG, "Display task started...");

    while (1)  // Infinite loop
    {
        // Update LCD display
        lcd_clear(WHITE);

        // Line 1: Title (Y=0, occupies 0-16)
        lcd_show_string(0, 0, 160, 16, 16, (char*)"ESP-NOW RX", GREEN);

        // Line 2: Receive count (Y=24, 8px spacing, occupies 24-40)
        snprintf(display_buf, sizeof(display_buf), "Recv #%d", recv_count);
        lcd_show_string(0, 24, 160, 16, 16, display_buf, BLACK);

        // If message received, display message content
        if (message_received)
        {
            // Line 3: Message text (Y=48, 8px spacing, occupies 48-64)
            snprintf(display_buf, sizeof(display_buf), "Text: %s", last_message.text);
            lcd_show_string(0, 48, 160, 16, 16, display_buf, BLUE);

            // Line 4: Counter value (Y=72, 8px spacing, occupies 72-88)
            snprintf(display_buf, sizeof(display_buf), "Cnt: %d", last_message.counter);
            lcd_show_string(0, 72, 160, 16, 16, display_buf, BLACK);
        }
        else
        {
            // Line 3: Waiting for message (Y=48, 8px spacing, occupies 48-64)
            lcd_show_string(0, 48, 160, 16, 16, (char*)"Waiting...", BLACK);
        }

        // Wait 500ms before updating display
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

/* ========== Main Function ========== */
void app_main(void)
{
    esp_err_t ret;
    uint32_t flash_size;
    esp_chip_info_t chip_info;

    // ========== Part 1: System Initialization ==========
    ESP_LOGI(TAG, "========== System Initialization ==========");

    // Initialize NVS (Non-Volatile Storage)
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }

    // Get chip information (for debugging)
    esp_flash_get_size(NULL, &flash_size);
    esp_chip_info(&chip_info);
    ESP_LOGI(TAG, "CPU cores: %d", chip_info.cores);
    ESP_LOGI(TAG, "Flash size: %ld MB", flash_size / (1024 * 1024));
    ESP_LOGI(TAG, "PSRAM size: %d bytes", esp_psram_get_size());

    // ========== Part 2: Hardware Initialization ==========
    ESP_LOGI(TAG, "========== Hardware Initialization ==========");

    led_init();    // Initialize LED
    exit_init();   // Initialize external interrupt
    spi2_init();   // Initialize SPI interface
    lcd_init();    // Initialize LCD display

    // Clear screen and display initialization message
    lcd_clear(WHITE);
    lcd_show_string(0, 0, 160, 16, 16, (char*)"Initializing...", BLUE);

    // Note: SD card initialization is skipped here because ESP-NOW doesn't need SD card
    // If SD card is needed, uncomment the following
    /*
    while (sd_card_init())
    {
        lcd_show_string(0, 0, 200, 16, 16, (char*)"SD Card Error!", RED);
        vTaskDelay(500);
    }
    */

    // ========== Part 3: ESP-NOW Initialization ==========
    ESP_LOGI(TAG, "========== ESP-NOW Initialization ==========");

    lcd_fill(0, 0, 160, 16, WHITE);
    lcd_show_string(0, 0, 160, 16, 12, (char*)"Init ESP-NOW...", BLUE);

    ret = espnow_init();
    if (ret != ESP_OK)
    {
        // Initialization failed, display error on LCD and stop
        lcd_clear(WHITE);
        lcd_show_string(0, 0, 160, 16, 12, (char*)"ESP-NOW Init", RED);
        lcd_show_string(0, 16, 160, 16, 12, (char*)"Failed!", RED);
        ESP_LOGE(TAG, "ESP-NOW initialization failed, program stopped");
        while (1) {
            vTaskDelay(1000);  // Infinite wait
        }
    }

    // Initialization successful
    lcd_clear(WHITE);
    lcd_show_string(0, 0, 160, 16, 12, (char*)"ESP-NOW Ready!", GREEN);
    vTaskDelay(pdMS_TO_TICKS(1000));  // Wait 1 second for user to see the message

    // ========== Part 4: Create Display Update Task ==========
    ESP_LOGI(TAG, "========== Create Display Update Task ==========");

    // Create a FreeRTOS task to periodically update LCD display
    // Parameter description:
    // - display_task: Task function
    // - "display_task": Task name
    // - 4096: Task stack size (bytes)
    // - NULL: Parameters passed to task
    // - 5: Task priority (higher number = higher priority)
    // - NULL: Task handle (not needed here)
    xTaskCreate(display_task, "display_task", 4096, NULL, 5, NULL);

    ESP_LOGI(TAG, "✓ Program startup complete! Waiting for data...");

    // Main task can continue doing other things or go to sleep
    while (1)
    {
        // Main task does nothing now, just waits
        // Actual receive work is done by ESP-NOW receive callback
        // Display updates are done by display_task
        vTaskDelay(pdMS_TO_TICKS(5000));  // Print every 5 seconds
        ESP_LOGI(TAG, "Main task running... (Receive count: %d)", recv_count);
    }
}

#ifdef __cplusplus
}
#endif

DEMO