This module provides ESP-NOW-based device-to-device communication for the plaipin-device project. ESP-NOW enables fast, low-latency messaging between ESP32 devices without requiring WiFi AP connection or pairing.
- Fast Communication: ~10ms latency for local messaging
- Connectionless: No AP required, works in STA mode
- Low Power: Minimal overhead compared to WiFi/BLE
- Coexistence: Works alongside WiFi and BLE
- Structured Messages: Type-safe protocol with multiple message types
- Peer Management: Automatic peer tracking and pairing
- Flexible: Support for pairing, text messages, metadata, heartbeats
┌──────────────────────────────────────────────┐
│ EspNowManager (Singleton) │
├──────────────────────────────────────────────┤
│ • Initialize ESP-NOW stack │
│ • Manage peer list │
│ • Send/receive messages │
│ • Callback dispatch │
└──────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ EspNowProtocol │
├──────────────────────────────────────────────┤
│ • Message serialization/deserialization │
│ • Message validation │
│ • Protocol version management │
└──────────────────────────────────────────────┘
Sent to initiate pairing with another device.
Payload:
- Device UUID
- Device name
- AgentMail inbox ID
- Current RSSI
Response to a pairing request (accept/reject).
Payload:
- Device UUID
- Device name
- AgentMail inbox ID
- Accepted (boolean)
Plain text message for LLM processing or user display.
Payload:
- Sender UUID
- Sender name
- Message text (up to 150 chars)
Update device information for paired devices.
Payload:
- Device UUID
- Device name
- AgentMail inbox ID
Periodic keep-alive and status message.
Payload:
- Uptime in seconds
- Current RSSI
- Battery level (0-100)
Request to remove pairing.
Payload:
- Device UUID
- Reason code
#include "espnow/espnow_manager.h"
using namespace espnow;
// Initialize WiFi first (ESP-NOW requires WiFi to be initialized)
// ... WiFi initialization code ...
// Initialize ESP-NOW
auto& manager = EspNowManager::GetInstance();
if (!manager.Initialize()) {
ESP_LOGE(TAG, "Failed to initialize ESP-NOW");
return;
}
ESP_LOGI(TAG, "ESP-NOW initialized. Local MAC: %s",
manager.GetLocalMacString().c_str());// Get device info
std::string device_uuid = board.GetUuid();
std::string device_name = "PlaiPin-" + device_uuid.substr(0, 6);
uint8_t peer_mac[6] = {0x24, 0x0A, 0xC4, 0x12, 0x34, 0x56};
// Send pairing request
manager.SendPairingRequest(
peer_mac,
device_uuid,
device_name,
"[email protected]",
-45 // RSSI
);
// Send text message
manager.SendTextMessage(
peer_mac,
device_uuid,
device_name,
"Hello from nearby device!"
);
// Broadcast to all paired peers
manager.BroadcastTextMessage(
device_uuid,
device_name,
"Broadcasting to all paired devices"
);// Handle pairing requests
manager.OnPairingRequest([](const uint8_t* mac, const PairingRequestPayload& payload) {
ESP_LOGI(TAG, "Pairing request from %s (%s)",
payload.device_name,
EspNowManager::MacToString(mac).c_str());
// Auto-accept or prompt user
if (should_accept_pairing) {
manager.SendPairingResponse(mac, my_uuid, my_name, my_inbox, true);
}
});
// Handle text messages
manager.OnTextMessage([](const uint8_t* mac, const TextMessagePayload& payload) {
ESP_LOGI(TAG, "Message from %s: %s",
payload.sender_name,
payload.message_text);
// Process message (e.g., inject into LLM)
Application::GetInstance().ProcessTextMessage(
payload.sender_name,
payload.message_text
);
});
// Handle pairing responses
manager.OnPairingResponse([](const uint8_t* mac, const PairingResponsePayload& payload) {
if (payload.accepted) {
ESP_LOGI(TAG, "Pairing accepted by %s", payload.device_name);
// Store peer info in NVS
} else {
ESP_LOGW(TAG, "Pairing rejected by %s", payload.device_name);
}
});// Get all paired devices
auto paired_devices = manager.GetPairedDevices();
for (const auto& peer : paired_devices) {
ESP_LOGI(TAG, "Peer: %s (%s)",
peer.name.c_str(),
EspNowManager::MacToString(peer.mac).c_str());
}
// Check if peer exists
if (manager.HasPeer(peer_mac)) {
ESP_LOGI(TAG, "Peer is in list");
}
// Get specific peer info
PeerDevice peer;
if (manager.GetPeerInfo(peer_mac, peer)) {
ESP_LOGI(TAG, "Peer RSSI: %d", peer.rssi);
}
// Remove peer
manager.RemovePeer(peer_mac);
// Clear all peers
manager.ClearAllPeers();Enable ESP-NOW in menuconfig:
plaipin-device Configuration
└─> ESP-NOW Configuration
├─> [*] Enable ESP-NOW Communication
├─> [*] Enable Automatic Pairing
├─> (10) Pairing Duration (seconds)
├─> (-50) Proximity RSSI Threshold (dBm)
├─> [*] Enable ESP-NOW Text Messaging
├─> [*] Inject ESP-NOW Messages as Virtual STT
├─> (10) Maximum Number of Paired Peers
├─> (60) Heartbeat Interval (seconds)
└─> [ ] Enable Message Encryption
# ESP-NOW Configuration
CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM=7Enable ESP-NOW test mode in menuconfig:
plaipin-device Configuration
└─> Test Modes
└─> [*] Enable ESP-NOW Test Mode
Build and flash:
idf.py build flash monitorThe test mode will:
- Initialize WiFi and ESP-NOW
- Listen for pairing requests (auto-accept)
- Send test messages every 15 seconds to paired devices
- Display status every 10 seconds
- Log all received messages
- Flash both devices with ESP-NOW test mode enabled
- Power on both devices
- Devices will automatically discover and pair (if using BLE proximity detection)
- Or manually trigger pairing by sending a pairing request from one device
- Observe message exchange in serial logs
The ESP-NOW module is designed to work alongside the BLE proximity module:
// In proximity detection callback:
proximity_manager.OnSustainedProximity([](const BleDevice& device) {
// Device has been close for 10+ seconds
ESP_LOGI(TAG, "Device %s close for 10 seconds, initiating pairing",
device.name);
// Trigger ESP-NOW pairing
auto& espnow = EspNowManager::GetInstance();
espnow.SendPairingRequest(
device.address,
board.GetUuid(),
board.GetName(),
settings.GetString("agentmail_inbox"),
device.rssi
);
});| Metric | Value |
|---|---|
| Max Message Size | 250 bytes |
| Typical Latency | 10-50 ms |
| Max Range | 200+ meters (line of sight) |
| Max Peers | 20 (encrypted) |
| Bandwidth | ~1 Mbps |
| Power Consumption | ~100-150 mA (active) |
ESP-NOW shares the 2.4GHz radio with WiFi and BLE:
- WiFi Impact: Minimal (<5% throughput reduction)
- BLE Impact: No noticeable impact on BLE scanning/advertising
- Time-Division Multiplexing: Handled automatically by ESP-IDF
- Recommendation: Use ESP-NOW for bursts, not continuous streaming
- Same Channel: All devices must be on the same WiFi channel
- 2.4GHz Only: No 5GHz support
- Message Size: 250 bytes max per message
- Range: Limited by WiFi range and obstacles
- No Acknowledgment Guarantee: Use heartbeats or application-level ACKs if needed
Error: esp_now_init() returned ESP_ERR_ESPNOW_NOT_INIT
Solution: WiFi must be initialized first. Call esp_wifi_start() before manager.Initialize().
Check:
- Both devices on same WiFi channel
- Devices within range (~50m indoors)
- Peer is added:
manager.HasPeer(mac) - Callbacks registered before sending
Error: esp_now_add_peer() returned ESP_ERR_ESPNOW_FULL
Solution: Increase CONFIG_ESPNOW_MAX_PEERS or remove unused peers.
espnow_protocol.h/cc- Message protocol and serializationespnow_manager.h/cc- ESP-NOW management and peer handlingespnow_test.h/cc- Test mode implementationREADME.md- This file
Same as plaipin-device project.