ESP-NOW vs BLE Power Analysis for UTLP¶
ESP32-C6 Transport Selection
mlehaptics Project — December 2025
1. The Question¶
Should UTLP use BLE or ESP-NOW for time synchronization beacons?
Hybrid proposal: BLE for bonding/pairing (security), ESP-NOW for data (speed).
2. ESP32-C6 Power Specifications¶
2.1 Radio Power Draw¶
| Mode | Current | Notes |
|---|---|---|
| WiFi TX (802.11n HT20 MCS7) | ~130-150 mA | ESP-NOW uses this |
| WiFi TX (802.11b 1Mbps) | ~170-180 mA | Long-range mode |
| WiFi RX | ~95-100 mA | Listening |
| BLE TX (0 dBm) | ~17-20 mA | Standard power |
| BLE RX | ~17-20 mA | Scanning/connected |
| Light Sleep (WiFi connected) | ~800 μA | WiFi modem sleep |
| Light Sleep (BLE connected) | ~30 μA | BLE maintains conn |
| Deep Sleep | ~7 μA | RTC only |
2.2 Timing Characteristics¶
| Operation | Duration | Notes |
|---|---|---|
| ESP-NOW packet | ~250-400 μs | Fixed, deterministic |
| BLE connection event | ~2-3 ms | Minimum practical |
| BLE connection interval | 7.5-4000 ms | Negotiated |
| WiFi wake from light sleep | ~1-3 ms | To active TX |
| BLE wake from light sleep | ~2-5 ms | To connection event |
3. Energy Per Beacon Analysis¶
3.1 ESP-NOW Beacon¶
Scenario: 27-byte UTLP beacon via ESP-NOW
Transmission:
- Packet overhead: ~50 bytes (MAC header, FCS)
- Total frame: ~77 bytes at 24 Mbps (HT20 MCS0)
- Airtime: ~300 μs (including preamble, SIFS)
- TX current: 145 mA
Energy per TX:
E = I × t = 145 mA × 0.3 ms = 43.5 μJ
Wake overhead (from light sleep):
- Wake time: ~2 ms at ~50 mA average = 100 μJ
Total per beacon (with wake): ~144 μJ
Total per beacon (already awake): ~44 μJ
3.2 BLE GATT Notification¶
Scenario: 27-byte UTLP beacon via BLE notification
Connection event:
- Data packet + ACK
- Minimum event duration: ~2 ms (625μs × 3-4 slots)
- TX/RX current: 18 mA average
Energy per notification:
E = I × t = 18 mA × 2 ms = 36 μJ
Connection maintenance:
- Empty events to maintain connection
- At 100ms interval: 10 events/sec
- Overhead: 36 μJ × 9 = 324 μJ/sec (no-data events)
Total per second (100ms interval): ~360 μJ
Total per second (1000ms interval): ~72 μJ
3.3 BLE Advertising (Connectionless)¶
Scenario: 27-byte beacon in BLE advertising PDU
Advertising:
- 3 channels (37, 38, 39)
- ~1 ms per channel = 3 ms total
- TX current: 18 mA
Energy per advertisement:
E = I × t = 18 mA × 3 ms = 54 μJ
At 1 Hz beacon rate: 54 μJ/sec
At 10 Hz beacon rate: 540 μJ/sec
4. Comparison Summary¶
4.1 Energy Budget (1 beacon/second, from sleep)¶
| Transport | Energy/sec | Battery Life (500mAh) |
|---|---|---|
| ESP-NOW (sleep between) | ~144 μJ | ~3,470 hours |
| BLE Notification (100ms CI) | ~360 μJ | ~1,390 hours |
| BLE Notification (1000ms CI) | ~72 μJ | ~6,940 hours |
| BLE Advertising | ~54 μJ | ~9,260 hours |
Assumes ideal conditions, no other system power
4.2 Energy Budget (10 beacons/second)¶
| Transport | Energy/sec | Battery Life (500mAh) |
|---|---|---|
| ESP-NOW (stay awake) | ~440 μJ | ~1,136 hours |
| ESP-NOW (sleep between) | ~1,440 μJ | ~347 hours |
| BLE Notification (15ms CI) | ~2,400 μJ | ~208 hours |
| BLE Advertising | ~540 μJ | ~926 hours |
Key insight: At higher beacon rates, ESP-NOW becomes competitive because BLE connection interval overhead dominates.
5. Latency Analysis (Critical for UTLP)¶
5.1 Worst-Case Latency¶
| Transport | Worst Case | Jitter |
|---|---|---|
| ESP-NOW | <1 ms | ~100 μs |
| BLE Connected (100ms CI) | 100 ms | ±50 ms |
| BLE Connected (15ms CI) | 15 ms | ±7.5 ms |
| BLE Advertising | Adv interval | ±10 ms |
5.2 Impact on Time Sync¶
UTLP offset calculation:
offset = ((T2 - T1) + (T3 - T4)) / 2
Jitter in T1/T2/T3/T4 directly impacts offset accuracy.
ESP-NOW: ±100 μs jitter → ±100 μs offset uncertainty
BLE (100ms CI): ±50 ms jitter → ±50 ms offset uncertainty (!!!)
For ±30 μs sync target:
- ESP-NOW: Achievable
- BLE (long CI): Not achievable without tricks
This is why your current BLE implementation probably uses short connection intervals or careful slot alignment.
6. The Hybrid Architecture¶
6.1 Proposal¶
┌─────────────────────────────────────────────────────────────────┐
│ Hybrid Transport Stack │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Phase 1: Discovery & Bonding (BLE) │
│ ┌──────────┐ ┌──────────┐ │
│ │ Device A │◄─BLE───►│ Device B │ │
│ └──────────┘ Adv/ └──────────┘ │
│ Scan/ │
│ Bond │
│ │
│ Result: Shared encryption key (LTK from BLE bonding) │
│ │
│ Phase 2: Data Exchange (ESP-NOW) │
│ ┌──────────┐ ┌──────────┐ │
│ │ Device A │◄─ESP────►│ Device B │ │
│ └──────────┘ NOW └──────────┘ │
│ (encrypted with BLE-derived key) │
│ │
│ - Sub-ms latency │
│ - Deterministic timing │
│ - Broadcast capable │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 Security Model¶
ESP-NOW doesn't have built-in encryption, but you can use the BLE bond:
// During BLE bonding, extract the Long Term Key (LTK)
// This is the key BLE uses for encrypted reconnections
typedef struct {
uint8_t ltk[16]; // From BLE bonding
uint8_t peer_mac[6]; // ESP-NOW peer address
bool trusted; // Bond complete
} peer_credential_t;
// Derive ESP-NOW encryption key from BLE LTK
void derive_espnow_key(const peer_credential_t* cred, uint8_t* espnow_key) {
// HKDF or simple derivation
// espnow_key = HMAC-SHA256(LTK, "ESPNOW" || peer_mac)
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1);
mbedtls_md_hmac_starts(&ctx, cred->ltk, 16);
mbedtls_md_hmac_update(&ctx, (uint8_t*)"ESPNOW", 6);
mbedtls_md_hmac_update(&ctx, cred->peer_mac, 6);
mbedtls_md_hmac_finish(&ctx, espnow_key); // 32 bytes, use first 16
mbedtls_md_free(&ctx);
}
// ESP-NOW has built-in CCMP encryption if you set a PMK
esp_now_set_pmk(espnow_key); // Primary Master Key
esp_now_peer_info_t peer = {
.encrypt = true,
.lmk = {derived_local_key}, // Per-peer key
};
esp_now_add_peer(&peer);
6.3 Transport Switching Logic¶
typedef enum {
TRANSPORT_BLE_ONLY,
TRANSPORT_ESPNOW_ONLY,
TRANSPORT_HYBRID,
} transport_mode_t;
typedef struct {
transport_mode_t mode;
bool ble_bonded;
bool espnow_ready;
// BLE handles
uint16_t ble_conn_handle;
// ESP-NOW handles
uint8_t espnow_peer_mac[6];
uint8_t espnow_key[16];
} transport_state_t;
esp_err_t transport_send_beacon(const utlp_beacon_t* beacon) {
switch (transport.mode) {
case TRANSPORT_BLE_ONLY:
return ble_send_notification(beacon);
case TRANSPORT_ESPNOW_ONLY:
case TRANSPORT_HYBRID:
if (transport.espnow_ready) {
return espnow_send_encrypted(beacon);
}
// Fallback to BLE if ESP-NOW not ready
return ble_send_notification(beacon);
}
}
// State machine for hybrid mode
void transport_update(void) {
if (transport.mode == TRANSPORT_HYBRID) {
if (transport.ble_bonded && !transport.espnow_ready) {
// Bonding complete, set up ESP-NOW
derive_espnow_key(&credentials, transport.espnow_key);
espnow_init_peer(transport.espnow_peer_mac, transport.espnow_key);
transport.espnow_ready = true;
// Optionally disconnect BLE to save power
// Or keep it for PWA communication
}
}
}
7. ESP-NOW Implementation Details¶
7.1 Basic Setup¶
#include "esp_now.h"
#include "esp_wifi.h"
// ESP-NOW requires WiFi to be initialized (but not connected)
esp_err_t espnow_transport_init(void) {
// Initialize WiFi in station mode (required for ESP-NOW)
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
// Set channel to match BLE advertising channels' WiFi overlap
// Channel 1 (2412 MHz) is near BLE ch 37 (2402 MHz)
ESP_ERROR_CHECK(esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE));
// Initialize ESP-NOW
ESP_ERROR_CHECK(esp_now_init());
ESP_ERROR_CHECK(esp_now_register_recv_cb(espnow_recv_callback));
ESP_ERROR_CHECK(esp_now_register_send_cb(espnow_send_callback));
return ESP_OK;
}
// Receive callback - runs in WiFi task context
static void espnow_recv_callback(const esp_now_recv_info_t* info,
const uint8_t* data, int len) {
int64_t rx_time = esp_timer_get_time(); // Timestamp immediately
// Validate sender is bonded peer
if (memcmp(info->src_addr, trusted_peer_mac, 6) != 0) {
return; // Ignore unknown senders
}
// Process UTLP beacon
if (len >= sizeof(utlp_beacon_t)) {
utlp_beacon_t* beacon = (utlp_beacon_t*)data;
time_sync_process_beacon(beacon, rx_time);
}
}
7.2 Timestamping for UTLP¶
// ESP-NOW provides TX callback after transmission completes
// We can bracket the TX to get approximate timestamp
static volatile int64_t last_tx_time = 0;
static volatile bool tx_pending = false;
static void espnow_send_callback(const uint8_t* mac, esp_now_send_status_t status) {
last_tx_time = esp_timer_get_time();
tx_pending = false;
}
esp_err_t espnow_send_beacon_timed(const utlp_beacon_t* beacon, int64_t* tx_time) {
tx_pending = true;
// Capture time just before send
int64_t t_before = esp_timer_get_time();
esp_err_t ret = esp_now_send(peer_mac, (uint8_t*)beacon, sizeof(*beacon));
if (ret != ESP_OK) {
tx_pending = false;
return ret;
}
// Wait for TX complete callback (should be <1ms)
int timeout = 10; // 10ms max
while (tx_pending && timeout-- > 0) {
vTaskDelay(pdMS_TO_TICKS(1));
}
// TX time is approximately midpoint
*tx_time = (t_before + last_tx_time) / 2;
return ESP_OK;
}
7.3 Power Management¶
// ESP-NOW with aggressive power saving
void espnow_enter_power_save(void) {
// Enable WiFi modem sleep between transmissions
esp_wifi_set_ps(WIFI_PS_MAX_MODEM);
}
void espnow_prepare_tx(void) {
// Wake modem before transmission
esp_wifi_set_ps(WIFI_PS_NONE);
vTaskDelay(pdMS_TO_TICKS(2)); // Allow modem wake
}
// For maximum power saving, can stop WiFi entirely between bursts
void espnow_deep_sleep_between_sync(void) {
esp_wifi_stop(); // Radio off completely
// ... sleep ...
esp_wifi_start(); // Radio back on
// Note: ESP-NOW peer info is retained
}
8. Coexistence: BLE + ESP-NOW¶
8.1 Radio Sharing¶
ESP32-C6 has a single 2.4 GHz radio. BLE and WiFi share it via coexistence arbitration.
// Enable coexistence
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE);
// Options:
// ESP_COEX_PREFER_WIFI - WiFi gets priority (better for ESP-NOW)
// ESP_COEX_PREFER_BT - BLE gets priority
// ESP_COEX_PREFER_BALANCE - Time-division sharing
8.2 Practical Coexistence Strategy¶
Scenario: PWA connected via BLE, devices syncing via ESP-NOW
┌─────────────────────────────────────────────────────────────────┐
│ Time Division Strategy │
├─────────────────────────────────────────────────────────────────┤
│ │
│ BLE Connection Interval: 100ms │
│ │
│ ──┬────────────────────────────────────────────────────┬── │
│ │ 100ms │ │
│ ──┴────────────────────────────────────────────────────┴── │
│ ▲ ▲ │
│ │ │ │
│ BLE event BLE event │
│ (~3ms) (~3ms) │
│ │
│ ESP-NOW sync beacons fire between BLE events │
│ │
│ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ ────┴─┴───────┴─┴───────┴─┴───────┴─┴───────┴─┴──── │
│ ▲ ▲ ▲ ▲ ▲ │
│ ESP-NOW ESP-NOW ESP-NOW ESP-NOW ESP-NOW │
│ (~0.3ms) (~0.3ms) (~0.3ms) (~0.3ms) (~0.3ms) │
│ │
│ 97ms available for ESP-NOW between 3ms BLE events │
│ │
└─────────────────────────────────────────────────────────────────┘
8.3 Conflict Avoidance¶
// Check if BLE event is imminent before ESP-NOW TX
bool is_ble_event_soon(void) {
// If we know the connection interval and anchor point,
// we can predict when the next BLE event will occur
int64_t now = esp_timer_get_time();
int64_t next_ble_event = ble_anchor_point +
((now - ble_anchor_point) / ble_conn_interval + 1) * ble_conn_interval;
int64_t time_to_event = next_ble_event - now;
// Don't TX if BLE event is within 5ms
return (time_to_event < 5000);
}
esp_err_t espnow_send_if_clear(const utlp_beacon_t* beacon) {
if (ble_connected && is_ble_event_soon()) {
// Defer transmission
return ESP_ERR_TIMEOUT;
}
return espnow_send_beacon_timed(beacon, &tx_time);
}
9. Recommendation¶
9.1 For UTLP Bilateral Sync¶
Use hybrid approach:
| Function | Transport | Rationale |
|---|---|---|
| Device discovery | BLE Advertising | Standard, phone-compatible |
| PWA connection | BLE GATT | Required for web interface |
| Bonding/security | BLE SMP | Established security model |
| Time sync beacons | ESP-NOW | Deterministic latency |
| Pattern transfer | BLE GATT | Larger MTU, reliable |
| FTM ranging | WiFi 802.11mc | Hardware timestamping |
9.2 Expected Improvements¶
| Metric | BLE Only | Hybrid | Improvement |
|---|---|---|---|
| Sync latency jitter | ±10-50 ms | ±100 μs | 100-500x |
| Achievable offset | ±30 μs (best case) | ±10 μs | 3x |
| Beacon rate limit | ~30 Hz | ~100+ Hz | 3x+ |
| Multi-device broadcast | Not native | Native | Architecture win |
9.3 Implementation Priority¶
- Phase 1: Add ESP-NOW transport alongside BLE (A/B testing)
- Phase 2: Implement BLE→ESP-NOW key derivation
- Phase 3: Add coexistence timing coordination
- Phase 4: Default to ESP-NOW for sync, BLE for control
10. Gotchas¶
10.1 Channel Selection¶
ESP-NOW requires both devices on same WiFi channel. BLE doesn't care.
// Both devices must agree on channel
#define ESPNOW_CHANNEL 1 // Configure in both devices
// Or: exchange channel via BLE during bonding
10.2 MAC Address¶
ESP-NOW uses WiFi MAC, BLE uses BLE MAC. They're different!
uint8_t wifi_mac[6], ble_mac[6];
esp_read_mac(wifi_mac, ESP_MAC_WIFI_STA);
esp_read_mac(ble_mac, ESP_MAC_BT);
// During BLE bonding, exchange WiFi MACs for ESP-NOW peer setup
10.3 Wake Latency¶
WiFi modem takes ~2ms to wake from power save. Budget this into timing.
// If sync beacon needed immediately, keep modem awake
// If beacon can wait 2ms, use modem sleep for power saving
References¶
- ESP-IDF ESP-NOW Guide: https://docs.espressif.com/projects/esp-idf/en/latest/esp32c6/api-reference/network/esp_now.html
- ESP32-C6 Datasheet (power specifications): https://www.espressif.com/sites/default/files/documentation/esp32-c6_datasheet_en.pdf
- BLE/WiFi Coexistence: https://docs.espressif.com/projects/esp-idf/en/latest/esp32c6/api-guides/coexist.html
Analysis prepared for mlehaptics UTLP transport selection.