An open-source, high-performance, and payload-agnostic TDMA (Time Division Multiple Access) scheduled MAC layer for ESP-NOW on ESP32-S3. Features low-latency deterministic communication with Adaptive Frequency Hopping (AFH) and NFC Out-Of-Band (OOB) provisioning.
- Time Division scheduled (TDMA): Master broadcasts a beacon every 10 ms (configurable). Up to 15 slaves transmit in assigned slots, completely eliminating collisions.
- Adaptive Frequency Hopping (AFH): Automatic real-time Packet Error Rate (PER) tracking per node. When interference is detected, the master schedules a synchronized hop across channels (1 -> 6 -> 11 -> 1).
- NFC Out-Of-Band Provisioning: Fast provisioning payloads designed to be written to ST25DV mailbox. Allows nodes to pair instantly with zero radio pollution.
- Payload Agnostic: The MAC layer treats the payload as an opaque byte buffer (
void *). You define your own structures. - Thread-safe & Lock-free: Uses a Single-Producer Single-Consumer (SPSC) ring buffer under the hood, ensuring that real-time sensor processing (e.g., IMU sampling) is never blocked by transmission.
Copy the esp_tdma_mac folder into your ESP-IDF project's components directory or specify it in your main/CMakeLists.txt.
Ensure your component requires esp_wifi and espressif/esp-now:
REQUIRES esp_wifi espressif__esp-nowRun idf.py menuconfig and navigate to ESP TDMA MAC Configuration:
Maximum number of slave nodes(Default: 15)Beacon broadcast interval(Default: 10 ms)Time slot step per node(Default: 700 µs)PER sliding window size(Default: 1000 packets)PER threshold to trigger AFH(Default: 5%)Maximum user payload size per packet(Default: 220 bytes)
#include "esp_tdma_mac.h"
#include "esp_log.h"
static const char *TAG = "gateway";
static void on_data_received(uint8_t node_id, uint32_t seq, uint8_t battery,
const void *payload, uint8_t payload_len) {
ESP_LOGI(TAG, "Received payload from node %d, seq=%lu, battery=%d%%, len=%d",
node_id, (unsigned long)seq, battery, payload_len);
// Cast payload back to your custom struct
// my_sensor_data_t *data = (my_sensor_data_t *)payload;
}
static void on_node_registered(uint8_t node_id, uint32_t fw_ver, bool upgrade_required) {
ESP_LOGI(TAG, "Node %d registered. Firmware version: %lu", node_id, (unsigned long)fw_ver);
}
void app_main(void) {
// Wi-Fi (STA mode) and ESP-NOW are automatically initialized by the component on startup.
// No manual Wi-Fi or ESP-NOW initialization boilerplate required.
esp_tdma_master_cfg_t cfg = {
.target_node_count = 1,
.on_data_received = on_data_received,
.on_node_registered = on_node_registered,
.on_state_changed = NULL
};
ESP_ERROR_CHECK(esp_tdma_master_init(&cfg));
ESP_ERROR_CHECK(esp_tdma_master_start());
}#include "esp_tdma_mac.h"
#include "esp_log.h"
static const char *TAG = "node";
void app_main(void) {
// Wi-Fi (STA mode) and ESP-NOW are automatically initialized by the component on startup.
// No manual Wi-Fi or ESP-NOW initialization boilerplate required.
esp_tdma_slave_cfg_t cfg = {
.on_state_changed = NULL
};
ESP_ERROR_CHECK(esp_tdma_slave_init(&cfg));
// Config received via NFC or loaded from NVS:
uint8_t node_id = 1;
uint8_t gateway_mac[6] = {0x28, 0x84, 0x85, 0x52, 0xC4, 0x0C};
uint8_t aes_lmk[16] = {0}; // LMK for encrypted ESP-NOW
uint32_t slot_offset_us = node_id * 700; // 700us offset
esp_tdma_slave_set_config(node_id, gateway_mac, aes_lmk, slot_offset_us);
ESP_ERROR_CHECK(esp_tdma_slave_start());
// In your sensor sampling task:
while (1) {
my_sensor_data_t data = read_sensor();
esp_tdma_slave_enqueue(&data, sizeof(data));
vTaskDelay(pdMS_TO_TICKS(10));
}
}This project is licensed under the Apache License 2.0 - see the LICENSE file for details.