From 253a2168aeb729f088b8f03d1c1c4361e29f2679 Mon Sep 17 00:00:00 2001 From: Alexey Zholtikov Date: Sat, 31 Oct 2026 09:11:59 +0300 Subject: [PATCH] wip: --- include/zh_nrf24_gateway.h | 137 ++++++---- zh_nrf24_gateway.c | 499 +++++++++++++++++++++++++------------ 2 files changed, 427 insertions(+), 209 deletions(-) diff --git a/include/zh_nrf24_gateway.h b/include/zh_nrf24_gateway.h index 694c8e3..5634c4c 100644 --- a/include/zh_nrf24_gateway.h +++ b/include/zh_nrf24_gateway.h @@ -4,15 +4,23 @@ * nRF24L01+ gateway for ESP-IDF. * * Receives RF sensor telemetry and posts it as events on the default - * ESP event loop (base ZH_NRF_GATEWAY_EVENTS, id ZH_NRF_GATEWAY_EVENT_SENSOR_DATA). + * ESP event loop. Pairing is fully internal: the gateway allocates + * per-type device IDs, persists the binding table in NVS and sends + * ACK packets. No pairing event reaches the event loop. * - * Pairing is fully internal to the component: it allocates device IDs, - * persists the next-id counter in NVS and sends ACK packets. No pairing - * event is posted to the event loop. + * Binding table semantics: + * - each binding is a pair {id, type}; + * - the same id may be reused across different types; + * - on pairing, the gateway allocates the smallest free id in 1..254 + * for the requested type; + * - deleting a binding makes its id available to the next pairing + * request of the same type. */ #pragma once #include +#include +#include #include "esp_err.h" #include "esp_event.h" @@ -23,68 +31,99 @@ extern "C" { #endif -/** - * @brief Event base for gateway events. - * - * Event data for ZH_NRF_GATEWAY_EVENT_SENSOR_DATA is a copy of - * zh_nrf24_sensor_transmitted_data_t. - */ +/** Max entries in the binding table (RAM and NVS). */ +#define ZH_NRF_GATEWAY_MAX_BINDINGS 254 + ESP_EVENT_DECLARE_BASE(ZH_NRF_GATEWAY_EVENTS); -/** - * @brief Gateway event IDs. - */ typedef enum { /** Sensor telemetry received (data: zh_nrf24_sensor_transmitted_data_t) */ ZH_NRF_GATEWAY_EVENT_SENSOR_DATA, } zh_nrf_gateway_event_t; -/** - * @brief Gateway configuration. - * - * Filled by the application before calling zh_nrf_gateway_init(). All pins - * refer to the ESP32 GPIO numbers; the nRF24L01+ module is connected over - * the selected SPI host. CE, CSN and IRQ are managed by the component. - */ typedef struct { - spi_host_device_t spi_host; /*!< SPI host (SPI2_HOST / SPI3_HOST) */ - int pin_sck; /*!< SCK GPIO */ - int pin_miso; /*!< MISO GPIO */ - int pin_mosi; /*!< MOSI GPIO */ - int pin_csn; /*!< CSN GPIO (chip select, active low) */ - int pin_ce; /*!< CE GPIO (chip enable, active high) */ - int pin_irq; /*!< IRQ GPIO (open-drain, active low) */ - uint8_t channel; /*!< RF channel (0..125) */ - uint32_t address; /*!< 24-bit pipe address, transmitted as LE bytes */ - uint32_t spi_clock_hz; /*!< SPI clock, 0 → default 4 MHz */ + spi_host_device_t spi_host; + int pin_sck; + int pin_miso; + int pin_mosi; + int pin_csn; + int pin_ce; + int pin_irq; + uint8_t channel; + uint32_t address; + uint32_t spi_clock_hz; } zh_nrf_gateway_config_t; -/** - * @brief Initialize the gateway. - * - * Configures SPI, the IRQ GPIO with falling-edge interrupt, the nRF24L01+ - * transceiver (DPL enabled, RX mode) and prepares NVS and the default - * event loop if they are not already initialized. - * - * @param[in] config Gateway configuration (must not be NULL) - * - * @return ESP_OK on success, or an ESP-IDF error code - */ +/** A single {id, type} binding. */ +typedef struct +{ + uint8_t id; /**< 1..254, 0xFF = unassigned */ + uint8_t type; /**< zh_nrf24_sensor_type_t */ +} zh_nrf_gateway_binding_t; + esp_err_t zh_nrf_gateway_init(const zh_nrf_gateway_config_t *config); +esp_err_t zh_nrf_gateway_start(void); + +/* ===================== BINDING TABLE API ===================== */ /** - * @brief Start the receiver task. + * @brief Copy current bindings into @p buf. * - * The task blocks on the IRQ semaphore (or polls the FIFO as a fallback - * once per second) and dispatches received packets. Sensor telemetry is - * posted to the default event loop as ZH_NRF_GATEWAY_EVENT_SENSOR_DATA. - * Pairing packets are handled internally and never reach the event loop. - * - * @return ESP_OK on success + * Thread-safe. Returns number of entries copied (≤ max_count and ≤ + * current table size). Use with zh_nrf_gateway_bindings_count() to + * decide buffer size. */ -esp_err_t zh_nrf_gateway_start(void); +size_t zh_nrf_gateway_bindings_get(zh_nrf_gateway_binding_t *buf, size_t max_count); + +/** @brief Current number of bindings in the table. */ +size_t zh_nrf_gateway_bindings_count(void); + +/** + * @brief Check whether a {id, type} binding exists. + * + * @return true if the binding is present in the table + */ +bool zh_nrf_gateway_binding_exists(uint8_t id, uint8_t type); + +/** + * @brief Delete a {id, type} binding. + * + * Composite key: both @p id AND @p type must match an existing entry. + * If only one of them matches, nothing is deleted and ESP_ERR_NOT_FOUND + * is returned. The id becomes immediately available for the next + * pairing request of the same type. The change is persisted to NVS. + * + * @return ESP_OK on success + * @return ESP_ERR_NOT_FOUND no such {id, type} pair in the table + * @return ESP_ERR_INVALID_ARG id out of 1..254, or unknown type + * @return ESP_ERR_INVALID_STATE gateway not initialized + * @return other NVS error + */ +esp_err_t zh_nrf_gateway_binding_delete(uint8_t id, uint8_t type); + +/** + * @brief Delete every binding whose id equals @p id (any type). + * + * Use when the same physical device changed its type and the old + * binding must be dropped regardless of the type field. + * + * @return Number of bindings deleted (≥ 0), or negative ESP error code + */ +int zh_nrf_gateway_bindings_delete_by_id(uint8_t id); + +/** + * @brief Delete every binding whose type equals @p type (any id). + * + * Use for mass-forget of one sensor category. + * + * @return Number of bindings deleted (≥ 0), or negative ESP error code + */ +int zh_nrf_gateway_bindings_delete_by_type(uint8_t type); + +/** @brief Remove all bindings (persisted). */ +esp_err_t zh_nrf_gateway_binding_clear_all(void); #ifdef __cplusplus } diff --git a/zh_nrf24_gateway.c b/zh_nrf24_gateway.c index 86e35f0..0ed3bdd 100644 --- a/zh_nrf24_gateway.c +++ b/zh_nrf24_gateway.c @@ -17,7 +17,7 @@ ESP_EVENT_DEFINE_BASE(ZH_NRF_GATEWAY_EVENTS); static const char *TAG = "NRF_GW"; -/* ================== nRF24 КОМАНДЫ ================== */ +/* ================== nRF24 КОМАНДЫ / РЕГИСТРЫ ================== */ #define NRF_CMD_R_REGISTER 0x00 #define NRF_CMD_W_REGISTER 0x20 #define NRF_CMD_R_RX_PAYLOAD 0x61 @@ -28,7 +28,6 @@ static const char *TAG = "NRF_GW"; #define NRF_CMD_ACTIVATE 0x50 #define NRF_CMD_R_RX_PL_WID 0x60 -/* ================== nRF24 РЕГИСТРЫ ================== */ #define NRF_REG_CONFIG 0x00 #define NRF_REG_EN_AA 0x01 #define NRF_REG_EN_RXADDR 0x02 @@ -43,7 +42,6 @@ static const char *TAG = "NRF_GW"; #define NRF_REG_DYNPD 0x1C #define NRF_REG_FEATURE 0x1D -/* ================== nRF24 БИТЫ ================== */ #define NRF_CONFIG_MASK_RX_DR 0x40 #define NRF_CONFIG_MASK_TX_DS 0x20 #define NRF_CONFIG_MASK_MAX_RT 0x10 @@ -63,21 +61,32 @@ static const char *TAG = "NRF_GW"; #define NRF_STATUS_MAX_RT 0x10 #define NRF_FIFO_RX_EMPTY 0x01 - #define NRF_FEATURE_EN_DPL 0x04 #define NRF_DYNPD_DPL_P0 0x01 -/* NVS namespace and key */ +/* ================== NVS ================== */ #define NVS_NAMESPACE "zh_gw" -#define NVS_KEY_NEXT_ID "next_id" +#define NVS_KEY_BINDINGS "bindings" + +/* Valid id range for allocation */ +#define BINDING_ID_MIN 1 +#define BINDING_ID_MAX 254 /* ================== СОСТОЯНИЕ КОМПОНЕНТА ================== */ +typedef struct __attribute__((packed)) +{ + uint16_t count; + zh_nrf_gateway_binding_t items[ZH_NRF_GATEWAY_MAX_BINDINGS]; +} bindings_blob_t; + static struct { zh_nrf_gateway_config_t cfg; spi_device_handle_t spi; SemaphoreHandle_t irq_sem; + SemaphoreHandle_t bindings_mutex; TaskHandle_t task; + bindings_blob_t bindings; bool initialized; } s_ctx = {0}; @@ -169,87 +178,281 @@ static void IRAM_ATTR nrf_irq_isr(void *arg) { BaseType_t woken = pdFALSE; xSemaphoreGiveFromISR(s_ctx.irq_sem, &woken); - if (woken) - portYIELD_FROM_ISR(); + if (woken) portYIELD_FROM_ISR(); } static esp_err_t nrf_irq_init(void) { s_ctx.irq_sem = xSemaphoreCreateBinary(); - if (!s_ctx.irq_sem) - return ESP_ERR_NO_MEM; + if (!s_ctx.irq_sem) return ESP_ERR_NO_MEM; gpio_config_t io_conf = { .pin_bit_mask = (1ULL << s_ctx.cfg.pin_irq), - .mode = GPIO_MODE_INPUT, - .pull_up_en = GPIO_PULLUP_ENABLE, + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_NEGEDGE, + .intr_type = GPIO_INTR_NEGEDGE, }; esp_err_t err = gpio_config(&io_conf); - if (err != ESP_OK) - return err; + if (err != ESP_OK) return err; err = gpio_install_isr_service(0); - if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) - return err; + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) return err; return gpio_isr_handler_add(s_ctx.cfg.pin_irq, nrf_irq_isr, NULL); } /* ============================================================ - * NVS: ВЫДАЧА ID + * NVS BINDING TABLE * ============================================================ */ -static uint8_t allocate_device_id(void) + +static esp_err_t bindings_load(void) { nvs_handle_t h; - uint8_t id = 0; - - if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h) != ESP_OK) - { - ESP_LOGW(TAG, "NVS open failed, using fallback id=1"); - return 1; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h); + if (err != ESP_OK) { + memset(&s_ctx.bindings, 0, sizeof(s_ctx.bindings)); + return ESP_OK; /* empty table on first run */ } - if (nvs_get_u8(h, NVS_KEY_NEXT_ID, &id) != ESP_OK || id == 0 || id == 0xFF) - { - id = 1; - } - - uint8_t next = id + 1; - if (next == 0 || next == 0xFF) - next = 1; - - nvs_set_u8(h, NVS_KEY_NEXT_ID, next); - nvs_commit(h); + size_t size = sizeof(s_ctx.bindings); + err = nvs_get_blob(h, NVS_KEY_BINDINGS, &s_ctx.bindings, &size); nvs_close(h); - ESP_LOGI(TAG, "Pairing: issued device_id=%u (next=%u)", id, next); - return id; + if (err == ESP_ERR_NVS_NOT_FOUND) { + memset(&s_ctx.bindings, 0, sizeof(s_ctx.bindings)); + ESP_LOGI(TAG, "Bindings: empty (first run)"); + return ESP_OK; + } + if (err != ESP_OK) { + memset(&s_ctx.bindings, 0, sizeof(s_ctx.bindings)); + return err; + } + + if (s_ctx.bindings.count > ZH_NRF_GATEWAY_MAX_BINDINGS) { + ESP_LOGW(TAG, "Bindings: corrupt count %u, resetting", s_ctx.bindings.count); + memset(&s_ctx.bindings, 0, sizeof(s_ctx.bindings)); + return ESP_OK; + } + + ESP_LOGI(TAG, "Bindings: loaded %u entries", s_ctx.bindings.count); + return ESP_OK; +} + +static esp_err_t bindings_save(void) +{ + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h); + if (err != ESP_OK) return err; + + size_t size = sizeof(s_ctx.bindings.count) + + s_ctx.bindings.count * sizeof(zh_nrf_gateway_binding_t); + err = nvs_set_blob(h, NVS_KEY_BINDINGS, &s_ctx.bindings, size); + if (err == ESP_OK) err = nvs_commit(h); + nvs_close(h); + return err; +} + +/* Find first free id in [BINDING_ID_MIN, BINDING_ID_MAX] for @p type. */ +static uint8_t bindings_alloc_id(uint8_t type) +{ + for (uint16_t id = BINDING_ID_MIN; id <= BINDING_ID_MAX; id++) { + bool used = false; + for (uint16_t i = 0; i < s_ctx.bindings.count; i++) { + if (s_ctx.bindings.items[i].id == id && + s_ctx.bindings.items[i].type == type) { + used = true; + break; + } + } + if (!used) return (uint8_t)id; + } + return 0; +} + +static esp_err_t bindings_add(uint8_t id, uint8_t type) +{ + if (s_ctx.bindings.count >= ZH_NRF_GATEWAY_MAX_BINDINGS) { + return ESP_ERR_NO_MEM; + } + s_ctx.bindings.items[s_ctx.bindings.count].id = id; + s_ctx.bindings.items[s_ctx.bindings.count].type = type; + s_ctx.bindings.count++; + return bindings_save(); +} + +static esp_err_t bindings_delete(uint8_t id, uint8_t type) +{ + for (uint16_t i = 0; i < s_ctx.bindings.count; i++) { + if (s_ctx.bindings.items[i].id == id && + s_ctx.bindings.items[i].type == type) { + memmove(&s_ctx.bindings.items[i], + &s_ctx.bindings.items[i + 1], + (s_ctx.bindings.count - i - 1) * sizeof(zh_nrf_gateway_binding_t)); + s_ctx.bindings.count--; + return bindings_save(); + } + } + return ESP_ERR_NOT_FOUND; } /* ============================================================ - * ОТПРАВКА PAIRING-ACK + * BINDING TABLE PUBLIC API * ============================================================ */ -static void send_pairing_ack(uint8_t req_type, uint16_t token, uint8_t new_id) + +size_t zh_nrf_gateway_bindings_get(zh_nrf_gateway_binding_t *buf, size_t max_count) +{ + if (!buf || max_count == 0 || !s_ctx.bindings_mutex) return 0; + xSemaphoreTake(s_ctx.bindings_mutex, portMAX_DELAY); + size_t n = s_ctx.bindings.count < max_count ? s_ctx.bindings.count : max_count; + memcpy(buf, s_ctx.bindings.items, n * sizeof(zh_nrf_gateway_binding_t)); + xSemaphoreGive(s_ctx.bindings_mutex); + return n; +} + +size_t zh_nrf_gateway_bindings_count(void) +{ + if (!s_ctx.bindings_mutex) return 0; + xSemaphoreTake(s_ctx.bindings_mutex, portMAX_DELAY); + size_t n = s_ctx.bindings.count; + xSemaphoreGive(s_ctx.bindings_mutex); + return n; +} + +bool zh_nrf_gateway_binding_exists(uint8_t id, uint8_t type) +{ + if (!s_ctx.initialized || !s_ctx.bindings_mutex) return false; + xSemaphoreTake(s_ctx.bindings_mutex, portMAX_DELAY); + bool found = false; + for (uint16_t i = 0; i < s_ctx.bindings.count; i++) { + if (s_ctx.bindings.items[i].id == id && + s_ctx.bindings.items[i].type == type) { + found = true; + break; + } + } + xSemaphoreGive(s_ctx.bindings_mutex); + return found; +} + +esp_err_t zh_nrf_gateway_binding_delete(uint8_t id, uint8_t type) +{ + if (!s_ctx.initialized) return ESP_ERR_INVALID_STATE; + + /* Argument validation */ + if (id == 0 || id == 0xFF) return ESP_ERR_INVALID_ARG; + if (zh_get_nrf24_sensor_payload_size(type) == 0) return ESP_ERR_INVALID_ARG; + + xSemaphoreTake(s_ctx.bindings_mutex, portMAX_DELAY); + esp_err_t err = bindings_delete(id, type); + xSemaphoreGive(s_ctx.bindings_mutex); + + if (err == ESP_OK) { + ESP_LOGI(TAG, "Binding deleted: id=%u type=%u (%s)", + id, type, zh_get_nrf24_sensor_type_name((zh_nrf24_sensor_type_t)type)); + } else if (err == ESP_ERR_NOT_FOUND) { + ESP_LOGW(TAG, "Binding not found: id=%u type=%u (%s)", + id, type, zh_get_nrf24_sensor_type_name((zh_nrf24_sensor_type_t)type)); + } else { + ESP_LOGE(TAG, "Binding delete failed (id=%u type=%u): %s", + id, type, esp_err_to_name(err)); + } + return err; +} + +int zh_nrf_gateway_bindings_delete_by_id(uint8_t id) +{ + if (!s_ctx.initialized) return -ESP_ERR_INVALID_STATE; + if (id == 0 || id == 0xFF) return -ESP_ERR_INVALID_ARG; + + xSemaphoreTake(s_ctx.bindings_mutex, portMAX_DELAY); + int deleted = 0; + for (uint16_t i = 0; i < s_ctx.bindings.count; ) { + if (s_ctx.bindings.items[i].id == id) { + memmove(&s_ctx.bindings.items[i], + &s_ctx.bindings.items[i + 1], + (s_ctx.bindings.count - i - 1) * sizeof(zh_nrf_gateway_binding_t)); + s_ctx.bindings.count--; + deleted++; + } else { + i++; + } + } + esp_err_t err = (deleted > 0) ? bindings_save() : ESP_OK; + xSemaphoreGive(s_ctx.bindings_mutex); + + if (err != ESP_OK) return -(int)err; + if (deleted > 0) { + ESP_LOGI(TAG, "Deleted %d binding(s) with id=%u", deleted, id); + } else { + ESP_LOGW(TAG, "No bindings with id=%u", id); + } + return deleted; +} + +int zh_nrf_gateway_bindings_delete_by_type(uint8_t type) +{ + if (!s_ctx.initialized) return -ESP_ERR_INVALID_STATE; + if (zh_get_nrf24_sensor_payload_size(type) == 0) return -ESP_ERR_INVALID_ARG; + + xSemaphoreTake(s_ctx.bindings_mutex, portMAX_DELAY); + int deleted = 0; + for (uint16_t i = 0; i < s_ctx.bindings.count; ) { + if (s_ctx.bindings.items[i].type == type) { + memmove(&s_ctx.bindings.items[i], + &s_ctx.bindings.items[i + 1], + (s_ctx.bindings.count - i - 1) * sizeof(zh_nrf_gateway_binding_t)); + s_ctx.bindings.count--; + deleted++; + } else { + i++; + } + } + esp_err_t err = (deleted > 0) ? bindings_save() : ESP_OK; + xSemaphoreGive(s_ctx.bindings_mutex); + + if (err != ESP_OK) return -(int)err; + if (deleted > 0) { + ESP_LOGI(TAG, "Deleted %d binding(s) of type=%u (%s)", + deleted, type, zh_get_nrf24_sensor_type_name((zh_nrf24_sensor_type_t)type)); + } else { + ESP_LOGW(TAG, "No bindings of type=%u (%s)", + type, zh_get_nrf24_sensor_type_name((zh_nrf24_sensor_type_t)type)); + } + return deleted; +} + +esp_err_t zh_nrf_gateway_binding_clear_all(void) +{ + if (!s_ctx.initialized) return ESP_ERR_INVALID_STATE; + xSemaphoreTake(s_ctx.bindings_mutex, portMAX_DELAY); + s_ctx.bindings.count = 0; + esp_err_t err = bindings_save(); + xSemaphoreGive(s_ctx.bindings_mutex); + if (err == ESP_OK) ESP_LOGI(TAG, "All bindings cleared"); + return err; +} + +/* ============================================================ + * PAIRING ACK + * ============================================================ */ +static bool send_pairing_ack(uint8_t req_type, uint16_t token, uint8_t new_id) { uint8_t pkt[ZH_NRF24_PAIRING_PACKET_SIZE] = { req_type, ZH_NRF24_PAIRING_ACK_MAGIC, (uint8_t)(token >> 8), (uint8_t)(token & 0xFF), - new_id}; + new_id + }; - /* 1. Заглушить ВСЕ IRQ + перейти в PTX standby */ nrf_ce_low(); nrf_write_register(NRF_REG_CONFIG, NRF_CONFIG_ALL_MASKED | NRF_CONFIG_PWR_UP); vTaskDelay(pdMS_TO_TICKS(2)); - /* 2. Сбросить только TX-флаги */ nrf_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT); nrf_send_command(NRF_CMD_FLUSH_TX); - /* 3. Записать payload и запустить TX */ uint8_t tx[1 + ZH_NRF24_PAIRING_PACKET_SIZE]; tx[0] = NRF_CMD_W_TX_PAYLOAD; memcpy(&tx[1], pkt, ZH_NRF24_PAIRING_PACKET_SIZE); @@ -265,145 +468,147 @@ static void send_pairing_ack(uint8_t req_type, uint16_t token, uint8_t new_id) esp_rom_delay_us(20); nrf_ce_low(); - /* 4. Опрос TX_DS / MAX_RT */ uint8_t status = 0; - for (int i = 0; i < 100; i++) - { + for (int i = 0; i < 100; i++) { status = nrf_send_command(NRF_CMD_NOP); - if (status & (NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT)) - break; + if (status & (NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT)) break; vTaskDelay(pdMS_TO_TICKS(1)); } - if (status & NRF_STATUS_TX_DS) - { - ESP_LOGI(TAG, "Pairing ACK sent (type=%u, id=%u)", req_type, new_id); - } - else - { - ESP_LOGW(TAG, "Pairing ACK NOT delivered (status=0x%02X)", status); - } + bool ok = (status & NRF_STATUS_TX_DS) != 0; - /* 5. Сбросить TX-флаги */ nrf_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT); nrf_send_command(NRF_CMD_FLUSH_TX); - /* 6. Возврат в RX, снимаем маску */ nrf_write_register(NRF_REG_CONFIG, NRF_CONFIG_RX_UNMASKED); vTaskDelay(pdMS_TO_TICKS(5)); nrf_ce_high(); vTaskDelay(pdMS_TO_TICKS(1)); + + return ok; } /* ============================================================ * ОБРАБОТКА ПАКЕТОВ * ============================================================ */ -/* Pairing request: {type, 0xA5, token_hi, token_lo, 0}. Internal only. */ static void handle_pairing_request(const uint8_t *data) { - uint8_t req_type = data[0]; - uint16_t token = ((uint16_t)data[2] << 8) | data[3]; + uint8_t req_type = data[0]; + uint16_t token = ((uint16_t)data[2] << 8) | data[3]; ESP_LOGI(TAG, "Pairing request: type=%u (%s), token=0x%04X", req_type, zh_get_nrf24_sensor_type_name((zh_nrf24_sensor_type_t)req_type), token); - uint8_t new_id = allocate_device_id(); - send_pairing_ack(req_type, token, new_id); + xSemaphoreTake(s_ctx.bindings_mutex, portMAX_DELAY); + + uint8_t new_id = bindings_alloc_id(req_type); + if (new_id == 0) { + xSemaphoreGive(s_ctx.bindings_mutex); + ESP_LOGW(TAG, "Pairing: no free id for type %u (all %d used)", + req_type, BINDING_ID_MAX); + return; + } + + esp_err_t err = bindings_add(new_id, req_type); + if (err != ESP_OK) { + if (s_ctx.bindings.count > 0 && + s_ctx.bindings.items[s_ctx.bindings.count - 1].id == new_id && + s_ctx.bindings.items[s_ctx.bindings.count - 1].type == req_type) { + s_ctx.bindings.count--; + } + xSemaphoreGive(s_ctx.bindings_mutex); + ESP_LOGE(TAG, "Pairing: NVS save failed (%s), aborting", esp_err_to_name(err)); + return; + } + + xSemaphoreGive(s_ctx.bindings_mutex); + ESP_LOGI(TAG, "Pairing: allocated id=%u for type=%u", new_id, req_type); + + if (!send_pairing_ack(req_type, token, new_id)) { + ESP_LOGW(TAG, "Pairing: ACK not delivered, removing binding id=%u type=%u", + new_id, req_type); + xSemaphoreTake(s_ctx.bindings_mutex, portMAX_DELAY); + bindings_delete(new_id, req_type); + xSemaphoreGive(s_ctx.bindings_mutex); + } else { + ESP_LOGI(TAG, "Pairing complete: id=%u type=%u", new_id, req_type); + } } -/* Data packet: parse and post to event loop. */ static void handle_sensor_data(const uint8_t *buf, uint8_t len) { - if (len < ZH_NRF24_DATA_HEADER_SIZE) - { + if (len < ZH_NRF24_DATA_HEADER_SIZE) { ESP_LOGW(TAG, "Too short data packet (%u)", len); return; } zh_nrf24_sensor_transmitted_data_t d = {0}; - d.id = zh_nrf24_sensor_read_i16_le_at(buf, 0); - d.type = zh_nrf24_sensor_read_i16_le_at(buf, 2); + d.id = zh_nrf24_sensor_read_i16_le_at(buf, 0); + d.type = zh_nrf24_sensor_read_i16_le_at(buf, 2); d.battery = zh_nrf24_sensor_read_i16_le_at(buf, 4); uint8_t expected = zh_get_nrf24_sensor_payload_size(d.type); - if (expected == 0) - { + if (expected == 0) { ESP_LOGW(TAG, "Unknown sensor type %d, dropping", d.type); return; } - if (len != expected) - { - ESP_LOGW(TAG, "Length %u != expected %u for type %d (%s)", - len, expected, d.type, - zh_get_nrf24_sensor_type_name((zh_nrf24_sensor_type_t)d.type)); + if (len != expected) { + ESP_LOGW(TAG, "Length %u != expected %u for type %d", + len, expected, d.type); } - switch (d.type) - { + switch (d.type) { case ZH_NRF24_SENSOR_TOUCH_SWITCH: break; - case ZH_NRF24_SENSOR_WATER_LEAKAGE: case ZH_NRF24_SENSOR_OPEN_CLOSE: case ZH_NRF24_SENSOR_PLANT_HUMIDITY: if (len >= ZH_NRF24_DATA_HEADER_SIZE + ZH_NRF24_DATA_FIELD_SIZE) d.value_1 = zh_nrf24_sensor_read_i16_le_at(buf, 6); break; - case ZH_NRF24_SENSOR_BMP280: - if (len >= ZH_NRF24_DATA_HEADER_SIZE + 2 * ZH_NRF24_DATA_FIELD_SIZE) - { + if (len >= ZH_NRF24_DATA_HEADER_SIZE + 2 * ZH_NRF24_DATA_FIELD_SIZE) { d.value_1 = zh_nrf24_sensor_read_i16_le_at(buf, 6); d.value_2 = zh_nrf24_sensor_read_i16_le_at(buf, 8); } break; - case ZH_NRF24_SENSOR_BME280: - if (len >= ZH_NRF24_DATA_HEADER_SIZE + 3 * ZH_NRF24_DATA_FIELD_SIZE) - { + if (len >= ZH_NRF24_DATA_HEADER_SIZE + 3 * ZH_NRF24_DATA_FIELD_SIZE) { d.value_1 = zh_nrf24_sensor_read_i16_le_at(buf, 6); d.value_2 = zh_nrf24_sensor_read_i16_le_at(buf, 8); d.value_3 = zh_nrf24_sensor_read_i16_le_at(buf, 10); } break; - case ZH_NRF24_SENSOR_BME680: - if (len >= ZH_NRF24_DATA_HEADER_SIZE + 4 * ZH_NRF24_DATA_FIELD_SIZE) - { + if (len >= ZH_NRF24_DATA_HEADER_SIZE + 4 * ZH_NRF24_DATA_FIELD_SIZE) { d.value_1 = zh_nrf24_sensor_read_i16_le_at(buf, 6); d.value_2 = zh_nrf24_sensor_read_i16_le_at(buf, 8); d.value_3 = zh_nrf24_sensor_read_i16_le_at(buf, 10); d.value_4 = zh_nrf24_sensor_read_i16_le_at(buf, 12); } break; - default: break; } esp_err_t err = esp_event_post(ZH_NRF_GATEWAY_EVENTS, ZH_NRF_GATEWAY_EVENT_SENSOR_DATA, - &d, sizeof(d), - pdMS_TO_TICKS(100)); - if (err != ESP_OK) - { + &d, sizeof(d), pdMS_TO_TICKS(100)); + if (err != ESP_OK) { ESP_LOGW(TAG, "esp_event_post failed: %s", esp_err_to_name(err)); } } -/* One packet from RX FIFO. Returns false if FIFO is empty. */ static bool process_one_packet(void) { uint8_t fifo = nrf_read_register(NRF_REG_FIFO_STATUS); - if (fifo & NRF_FIFO_RX_EMPTY) - return false; + if (fifo & NRF_FIFO_RX_EMPTY) return false; uint8_t width = nrf_read_payload_width(); - if (width > ZH_NRF24_MAX_PAYLOAD_SIZE) - { + if (width > ZH_NRF24_MAX_PAYLOAD_SIZE) { ESP_LOGW(TAG, "Corrupt payload width %u, FLUSH_RX", width); nrf_send_command(NRF_CMD_FLUSH_RX); nrf_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR); @@ -414,13 +619,9 @@ static bool process_one_packet(void) nrf_read_payload(buf, width); nrf_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR); - /* Pairing: 5 bytes, magic @ offset 1. Data: >=6 bytes, id @ offset 0. */ - if (width == ZH_NRF24_PAIRING_PACKET_SIZE && buf[1] == ZH_NRF24_PAIRING_REQ_MAGIC) - { + if (width == ZH_NRF24_PAIRING_PACKET_SIZE && buf[1] == ZH_NRF24_PAIRING_REQ_MAGIC) { handle_pairing_request(buf); - } - else - { + } else { handle_sensor_data(buf, width); } return true; @@ -428,25 +629,15 @@ static bool process_one_packet(void) static void process_all_pending(void) { - while (process_one_packet()) - { - } + while (process_one_packet()) { } } -/* ============================================================ - * RECEIVER TASK - * ============================================================ */ static void gateway_task(void *arg) { - while (1) - { - if (xSemaphoreTake(s_ctx.irq_sem, pdMS_TO_TICKS(1000)) == pdTRUE) - { + while (1) { + if (xSemaphoreTake(s_ctx.irq_sem, pdMS_TO_TICKS(1000)) == pdTRUE) { process_all_pending(); - } - else - { - /* Fallback polling once per second */ + } else { process_all_pending(); } } @@ -457,34 +648,33 @@ static void gateway_task(void *arg) * ============================================================ */ esp_err_t zh_nrf_gateway_init(const zh_nrf_gateway_config_t *config) { - if (!config) - return ESP_ERR_INVALID_ARG; - if (s_ctx.initialized) - return ESP_ERR_INVALID_STATE; + if (!config) return ESP_ERR_INVALID_ARG; + if (s_ctx.initialized) return ESP_ERR_INVALID_STATE; s_ctx.cfg = *config; - if (s_ctx.cfg.spi_clock_hz == 0) - s_ctx.cfg.spi_clock_hz = 4 * 1000 * 1000; - if (s_ctx.cfg.channel > 125) - s_ctx.cfg.channel = 120; + if (s_ctx.cfg.spi_clock_hz == 0) s_ctx.cfg.spi_clock_hz = 4 * 1000 * 1000; + if (s_ctx.cfg.channel > 125) s_ctx.cfg.channel = 120; - /* NVS - init if needed */ esp_err_t err = nvs_flash_init(); - if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) - { + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_LOGW(TAG, "NVS flash needs erase, erasing"); ESP_ERROR_CHECK(nvs_flash_erase()); err = nvs_flash_init(); } - if (err != ESP_OK) - return err; + if (err != ESP_OK) return err; + + s_ctx.bindings_mutex = xSemaphoreCreateMutex(); + if (!s_ctx.bindings_mutex) return ESP_ERR_NO_MEM; + + err = bindings_load(); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Bindings load failed (%s), starting empty", esp_err_to_name(err)); + memset(&s_ctx.bindings, 0, sizeof(s_ctx.bindings)); + } - /* Default event loop - create if needed */ err = esp_event_loop_create_default(); - if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) - return err; + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) return err; - /* SPI bus */ spi_bus_config_t buscfg = { .miso_io_num = s_ctx.cfg.pin_miso, .mosi_io_num = s_ctx.cfg.pin_mosi, @@ -494,8 +684,7 @@ esp_err_t zh_nrf_gateway_init(const zh_nrf_gateway_config_t *config) .max_transfer_sz = ZH_NRF24_MAX_PAYLOAD_SIZE, }; err = spi_bus_initialize(s_ctx.cfg.spi_host, &buscfg, SPI_DMA_CH_AUTO); - if (err != ESP_OK) - return err; + if (err != ESP_OK) return err; spi_device_interface_config_t devcfg = { .clock_speed_hz = (int)s_ctx.cfg.spi_clock_hz, @@ -504,29 +693,25 @@ esp_err_t zh_nrf_gateway_init(const zh_nrf_gateway_config_t *config) .queue_size = 1, }; err = spi_bus_add_device(s_ctx.cfg.spi_host, &devcfg, &s_ctx.spi); - if (err != ESP_OK) - return err; + if (err != ESP_OK) return err; - /* CE pin */ gpio_config_t ce_conf = { .pin_bit_mask = (1ULL << s_ctx.cfg.pin_ce), .mode = GPIO_MODE_OUTPUT, }; err = gpio_config(&ce_conf); - if (err != ESP_OK) - return err; + if (err != ESP_OK) return err; nrf_ce_low(); vTaskDelay(pdMS_TO_TICKS(100)); - /* nRF24 config */ nrf_write_register(NRF_REG_CONFIG, 0x00); nrf_write_register(NRF_REG_EN_AA, 0x01); nrf_write_register(NRF_REG_EN_RXADDR, 0x01); - nrf_write_register(NRF_REG_SETUP_AW, 0x01); /* 3-byte address */ + nrf_write_register(NRF_REG_SETUP_AW, 0x01); nrf_write_register(NRF_REG_SETUP_RETR, 0xFF); nrf_write_register(NRF_REG_RF_CH, s_ctx.cfg.channel); - nrf_write_register(NRF_REG_RF_SETUP, 0x26); /* 1 Mbps, 0 dBm */ + nrf_write_register(NRF_REG_RF_SETUP, 0x26); /* 250 kbps, 0 dBm */ uint8_t addr_bytes[3] = { (uint8_t)(s_ctx.cfg.address >> 0), @@ -537,44 +722,38 @@ esp_err_t zh_nrf_gateway_init(const zh_nrf_gateway_config_t *config) nrf_write_buffer(NRF_REG_RX_ADDR_P0, addr_bytes, 3); nrf_write_register(NRF_REG_STATUS, - NRF_STATUS_RX_DR | NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT); + NRF_STATUS_RX_DR | NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT); - /* Enable Dynamic Payload Length */ nrf_activate(); nrf_write_register(NRF_REG_FEATURE, NRF_FEATURE_EN_DPL); - nrf_write_register(NRF_REG_DYNPD, NRF_DYNPD_DPL_P0); + nrf_write_register(NRF_REG_DYNPD, NRF_DYNPD_DPL_P0); - /* Enter RX */ nrf_write_register(NRF_REG_CONFIG, NRF_CONFIG_RX_UNMASKED); vTaskDelay(pdMS_TO_TICKS(5)); nrf_ce_high(); vTaskDelay(pdMS_TO_TICKS(1)); - /* IRQ GPIO */ err = nrf_irq_init(); - if (err != ESP_OK) - return err; + if (err != ESP_OK) return err; xSemaphoreTake(s_ctx.irq_sem, 0); s_ctx.initialized = true; - ESP_LOGI(TAG, "Initialized: ch=%u addr=0x%06X DPL on, RX mode", - s_ctx.cfg.channel, (unsigned)s_ctx.cfg.address & 0xFFFFFF); + ESP_LOGI(TAG, "Initialized: ch=%u addr=0x%06X, %u bindings in NVS", + s_ctx.cfg.channel, (unsigned)(s_ctx.cfg.address & 0xFFFFFF), + s_ctx.bindings.count); return ESP_OK; } esp_err_t zh_nrf_gateway_start(void) { - if (!s_ctx.initialized) - return ESP_ERR_INVALID_STATE; - if (s_ctx.task) - return ESP_OK; + if (!s_ctx.initialized) return ESP_ERR_INVALID_STATE; + if (s_ctx.task) return ESP_OK; BaseType_t r = xTaskCreatePinnedToCore(gateway_task, "nrf_gateway", 4096, NULL, 5, &s_ctx.task, tskNO_AFFINITY); - if (r != pdPASS) - { + if (r != pdPASS) { s_ctx.task = NULL; return ESP_ERR_NO_MEM; }