#include "zh_nrf24_gateway.h" #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "driver/spi_master.h" #include "driver/gpio.h" #include "esp_log.h" #include "esp_rom_sys.h" #include "nvs_flash.h" #include "nvs.h" ESP_EVENT_DEFINE_BASE(ZH_NRF_GATEWAY_EVENTS); static const char *TAG = "NRF_GW"; /* ================== nRF24 КОМАНДЫ / РЕГИСТРЫ ================== */ #define NRF_CMD_R_REGISTER 0x00 #define NRF_CMD_W_REGISTER 0x20 #define NRF_CMD_R_RX_PAYLOAD 0x61 #define NRF_CMD_W_TX_PAYLOAD 0xA0 #define NRF_CMD_FLUSH_TX 0xE1 #define NRF_CMD_FLUSH_RX 0xE2 #define NRF_CMD_NOP 0xFF #define NRF_CMD_ACTIVATE 0x50 #define NRF_CMD_R_RX_PL_WID 0x60 #define NRF_REG_CONFIG 0x00 #define NRF_REG_EN_AA 0x01 #define NRF_REG_EN_RXADDR 0x02 #define NRF_REG_SETUP_AW 0x03 #define NRF_REG_SETUP_RETR 0x04 #define NRF_REG_RF_CH 0x05 #define NRF_REG_RF_SETUP 0x06 #define NRF_REG_STATUS 0x07 #define NRF_REG_RX_ADDR_P0 0x0A #define NRF_REG_TX_ADDR 0x10 #define NRF_REG_FIFO_STATUS 0x17 #define NRF_REG_DYNPD 0x1C #define NRF_REG_FEATURE 0x1D #define NRF_CONFIG_MASK_RX_DR 0x40 #define NRF_CONFIG_MASK_TX_DS 0x20 #define NRF_CONFIG_MASK_MAX_RT 0x10 #define NRF_CONFIG_EN_CRC 0x08 #define NRF_CONFIG_PWR_UP 0x02 #define NRF_CONFIG_PRIM_RX 0x01 #define NRF_CONFIG_ALL_MASKED \ (NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_MASK_TX_DS | \ NRF_CONFIG_MASK_MAX_RT | NRF_CONFIG_EN_CRC) #define NRF_CONFIG_RX_UNMASKED \ (NRF_CONFIG_EN_CRC | NRF_CONFIG_PWR_UP | NRF_CONFIG_PRIM_RX) #define NRF_STATUS_RX_DR 0x40 #define NRF_STATUS_TX_DS 0x20 #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 ================== */ #define NVS_NAMESPACE "zh_gw" #define NVS_KEY_BINDINGS "bindings" #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}; /* ============================================================ * SPI / nRF24 HELPERS * ============================================================ */ static void nrf_csn_low(void) { gpio_set_level(s_ctx.cfg.pin_csn, 0); } static void nrf_csn_high(void) { gpio_set_level(s_ctx.cfg.pin_csn, 1); } static void nrf_ce_low(void) { gpio_set_level(s_ctx.cfg.pin_ce, 0); } static void nrf_ce_high(void) { gpio_set_level(s_ctx.cfg.pin_ce, 1); } static uint8_t nrf_read_register(uint8_t reg) { uint8_t tx[2] = {NRF_CMD_R_REGISTER | (reg & 0x1F), 0xFF}; uint8_t rx[2] = {0}; spi_transaction_t t = {.length = 16, .tx_buffer = tx, .rx_buffer = rx}; nrf_csn_low(); ESP_ERROR_CHECK(spi_device_transmit(s_ctx.spi, &t)); nrf_csn_high(); return rx[1]; } static void nrf_write_register(uint8_t reg, uint8_t val) { uint8_t tx[2] = {NRF_CMD_W_REGISTER | (reg & 0x1F), val}; spi_transaction_t t = {.length = 16, .tx_buffer = tx}; nrf_csn_low(); ESP_ERROR_CHECK(spi_device_transmit(s_ctx.spi, &t)); nrf_csn_high(); } static void nrf_write_buffer(uint8_t reg, const uint8_t *buf, uint8_t len) { uint8_t tx[32 + 1]; tx[0] = NRF_CMD_W_REGISTER | (reg & 0x1F); memcpy(&tx[1], buf, len); spi_transaction_t t = {.length = (len + 1) * 8, .tx_buffer = tx}; nrf_csn_low(); ESP_ERROR_CHECK(spi_device_transmit(s_ctx.spi, &t)); nrf_csn_high(); } static uint8_t nrf_send_command(uint8_t cmd) { uint8_t tx[1] = {cmd}; uint8_t rx[1] = {0}; spi_transaction_t t = {.length = 8, .tx_buffer = tx, .rx_buffer = rx}; nrf_csn_low(); ESP_ERROR_CHECK(spi_device_transmit(s_ctx.spi, &t)); nrf_csn_high(); return rx[0]; } static void nrf_read_payload(uint8_t *buf, uint8_t len) { uint8_t cmd = NRF_CMD_R_RX_PAYLOAD; nrf_csn_low(); spi_transaction_t t_cmd = {.length = 8, .tx_buffer = &cmd}; ESP_ERROR_CHECK(spi_device_transmit(s_ctx.spi, &t_cmd)); spi_transaction_t t_data = {.length = len * 8, .rx_buffer = buf}; ESP_ERROR_CHECK(spi_device_transmit(s_ctx.spi, &t_data)); nrf_csn_high(); } static uint8_t nrf_read_payload_width(void) { uint8_t tx[2] = {NRF_CMD_R_RX_PL_WID, 0xFF}; uint8_t rx[2] = {0}; spi_transaction_t t = {.length = 16, .tx_buffer = tx, .rx_buffer = rx}; nrf_csn_low(); ESP_ERROR_CHECK(spi_device_transmit(s_ctx.spi, &t)); nrf_csn_high(); return rx[1]; } static void nrf_activate(void) { uint8_t tx[2] = {NRF_CMD_ACTIVATE, 0x73}; spi_transaction_t t = {.length = 16, .tx_buffer = tx}; nrf_csn_low(); ESP_ERROR_CHECK(spi_device_transmit(s_ctx.spi, &t)); nrf_csn_high(); } /* ============================================================ * IRQ (GPIO ISR) * ============================================================ */ static void IRAM_ATTR nrf_irq_isr(void *arg) { BaseType_t woken = pdFALSE; xSemaphoreGiveFromISR(s_ctx.irq_sem, &woken); 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; gpio_config_t io_conf = { .pin_bit_mask = (1ULL << s_ctx.cfg.pin_irq), .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_NEGEDGE, }; esp_err_t err = gpio_config(&io_conf); if (err != ESP_OK) return err; err = gpio_install_isr_service(0); 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 BINDING TABLE * ============================================================ */ static esp_err_t bindings_load(void) { nvs_handle_t h; 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; } size_t size = sizeof(s_ctx.bindings); err = nvs_get_blob(h, NVS_KEY_BINDINGS, &s_ctx.bindings, &size); nvs_close(h); 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; } 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; } /* ============================================================ * BINDING TABLE PUBLIC API * ============================================================ */ 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; 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 }; nrf_ce_low(); nrf_write_register(NRF_REG_CONFIG, NRF_CONFIG_ALL_MASKED | NRF_CONFIG_PWR_UP); vTaskDelay(pdMS_TO_TICKS(2)); nrf_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT); nrf_send_command(NRF_CMD_FLUSH_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); spi_transaction_t t = { .length = (1 + ZH_NRF24_PAIRING_PACKET_SIZE) * 8, .tx_buffer = tx, }; nrf_csn_low(); ESP_ERROR_CHECK(spi_device_transmit(s_ctx.spi, &t)); nrf_csn_high(); nrf_ce_high(); esp_rom_delay_us(20); nrf_ce_low(); uint8_t status = 0; 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; vTaskDelay(pdMS_TO_TICKS(1)); } bool ok = (status & NRF_STATUS_TX_DS) != 0; nrf_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT); nrf_send_command(NRF_CMD_FLUSH_TX); 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; } /* ============================================================ * ОБРАБОТКА ПАКЕТОВ * ============================================================ */ 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]; 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); 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); return; } /* --- ACK delivered → binding is final. Notify the application. --- */ ESP_LOGI(TAG, "Pairing complete: id=%u type=%u", new_id, req_type); zh_nrf_gateway_binding_t paired = { .id = new_id, .type = req_type, }; esp_err_t post_err = esp_event_post(ZH_NRF_GATEWAY_EVENTS, ZH_NRF_GATEWAY_EVENT_SENSOR_PAIRED, &paired, sizeof(paired), pdMS_TO_TICKS(100)); if (post_err != ESP_OK) { ESP_LOGW(TAG, "esp_event_post(paired) failed: %s", esp_err_to_name(post_err)); } } static void handle_sensor_data(const uint8_t *buf, uint8_t len) { 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.battery = zh_nrf24_sensor_read_i16_le_at(buf, 4); uint8_t expected = zh_get_nrf24_sensor_payload_size(d.type); 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", len, expected, 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) { 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) { 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) { 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) { ESP_LOGW(TAG, "esp_event_post failed: %s", esp_err_to_name(err)); } } static bool process_one_packet(void) { uint8_t fifo = nrf_read_register(NRF_REG_FIFO_STATUS); if (fifo & NRF_FIFO_RX_EMPTY) return false; uint8_t width = nrf_read_payload_width(); 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); return true; } uint8_t buf[ZH_NRF24_MAX_PAYLOAD_SIZE] = {0}; nrf_read_payload(buf, width); nrf_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR); if (width == ZH_NRF24_PAIRING_PACKET_SIZE && buf[1] == ZH_NRF24_PAIRING_REQ_MAGIC) { handle_pairing_request(buf); } else { handle_sensor_data(buf, width); } return true; } static void process_all_pending(void) { while (process_one_packet()) { } } static void gateway_task(void *arg) { while (1) { if (xSemaphoreTake(s_ctx.irq_sem, pdMS_TO_TICKS(1000)) == pdTRUE) { process_all_pending(); } else { process_all_pending(); } } } /* ============================================================ * PUBLIC API * ============================================================ */ 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; 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; esp_err_t err = nvs_flash_init(); 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; 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)); } err = esp_event_loop_create_default(); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) return err; spi_bus_config_t buscfg = { .miso_io_num = s_ctx.cfg.pin_miso, .mosi_io_num = s_ctx.cfg.pin_mosi, .sclk_io_num = s_ctx.cfg.pin_sck, .quadwp_io_num = -1, .quadhd_io_num = -1, .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; spi_device_interface_config_t devcfg = { .clock_speed_hz = (int)s_ctx.cfg.spi_clock_hz, .mode = 0, .spics_io_num = s_ctx.cfg.pin_csn, .queue_size = 1, }; err = spi_bus_add_device(s_ctx.cfg.spi_host, &devcfg, &s_ctx.spi); if (err != ESP_OK) return err; 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; nrf_ce_low(); vTaskDelay(pdMS_TO_TICKS(100)); 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); 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); /* 250 kbps, 0 dBm */ uint8_t addr_bytes[3] = { (uint8_t)(s_ctx.cfg.address >> 0), (uint8_t)(s_ctx.cfg.address >> 8), (uint8_t)(s_ctx.cfg.address >> 16), }; nrf_write_buffer(NRF_REG_TX_ADDR, addr_bytes, 3); 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_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_CONFIG, NRF_CONFIG_RX_UNMASKED); vTaskDelay(pdMS_TO_TICKS(5)); nrf_ce_high(); vTaskDelay(pdMS_TO_TICKS(1)); err = nrf_irq_init(); 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, %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; BaseType_t r = xTaskCreatePinnedToCore(gateway_task, "nrf_gateway", 4096, NULL, 5, &s_ctx.task, tskNO_AFFINITY); if (r != pdPASS) { s_ctx.task = NULL; return ESP_ERR_NO_MEM; } ESP_LOGI(TAG, "Receiver task started"); return ESP_OK; }