This commit is contained in:
aZholtikov committed 2026-10-31 09:11:59 +03:00
1 parent 98fb08cd6f
commit 253a2168ae
2 files changed
+427 -209

No files matched your search

+88 -49
View File
@@ -4,15 +4,23 @@
* nRF24L01+ gateway for ESP-IDF. * nRF24L01+ gateway for ESP-IDF.
* *
* Receives RF sensor telemetry and posts it as events on the default * 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, * Binding table semantics:
* persists the next-id counter in NVS and sends ACK packets. No pairing * - each binding is a pair {id, type};
* event is posted to the event loop. * - 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 #pragma once
#include <stdint.h> #include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include "esp_err.h" #include "esp_err.h"
#include "esp_event.h" #include "esp_event.h"
@@ -23,68 +31,99 @@
extern "C" { extern "C" {
#endif #endif
/** /** Max entries in the binding table (RAM and NVS). */
* @brief Event base for gateway events. #define ZH_NRF_GATEWAY_MAX_BINDINGS 254
*
* Event data for ZH_NRF_GATEWAY_EVENT_SENSOR_DATA is a copy of
* zh_nrf24_sensor_transmitted_data_t.
*/
ESP_EVENT_DECLARE_BASE(ZH_NRF_GATEWAY_EVENTS); ESP_EVENT_DECLARE_BASE(ZH_NRF_GATEWAY_EVENTS);
/**
* @brief Gateway event IDs.
*/
typedef enum typedef enum
{ {
/** Sensor telemetry received (data: zh_nrf24_sensor_transmitted_data_t) */ /** Sensor telemetry received (data: zh_nrf24_sensor_transmitted_data_t) */
ZH_NRF_GATEWAY_EVENT_SENSOR_DATA, ZH_NRF_GATEWAY_EVENT_SENSOR_DATA,
} zh_nrf_gateway_event_t; } 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 typedef struct
{ {
spi_host_device_t spi_host; /*!< SPI host (SPI2_HOST / SPI3_HOST) */ spi_host_device_t spi_host;
int pin_sck; /*!< SCK GPIO */ int pin_sck;
int pin_miso; /*!< MISO GPIO */ int pin_miso;
int pin_mosi; /*!< MOSI GPIO */ int pin_mosi;
int pin_csn; /*!< CSN GPIO (chip select, active low) */ int pin_csn;
int pin_ce; /*!< CE GPIO (chip enable, active high) */ int pin_ce;
int pin_irq; /*!< IRQ GPIO (open-drain, active low) */ int pin_irq;
uint8_t channel; /*!< RF channel (0..125) */ uint8_t channel;
uint32_t address; /*!< 24-bit pipe address, transmitted as LE bytes */ uint32_t address;
uint32_t spi_clock_hz; /*!< SPI clock, 0 → default 4 MHz */ uint32_t spi_clock_hz;
} zh_nrf_gateway_config_t; } zh_nrf_gateway_config_t;
/** /** A single {id, type} binding. */
* @brief Initialize the gateway. typedef struct
* {
* Configures SPI, the IRQ GPIO with falling-edge interrupt, the nRF24L01+ uint8_t id; /**< 1..254, 0xFF = unassigned */
* transceiver (DPL enabled, RX mode) and prepares NVS and the default uint8_t type; /**< zh_nrf24_sensor_type_t */
* event loop if they are not already initialized. } zh_nrf_gateway_binding_t;
*
* @param[in] config Gateway configuration (must not be NULL)
*
* @return ESP_OK on success, or an ESP-IDF error code
*/
esp_err_t zh_nrf_gateway_init(const zh_nrf_gateway_config_t *config); 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 * Thread-safe. Returns number of entries copied (≤ max_count and ≤
* once per second) and dispatches received packets. Sensor telemetry is * current table size). Use with zh_nrf_gateway_bindings_count() to
* posted to the default event loop as ZH_NRF_GATEWAY_EVENT_SENSOR_DATA. * decide buffer size.
* Pairing packets are handled internally and never reach the event loop.
*
* @return ESP_OK on success
*/ */
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 #ifdef __cplusplus
} }
+339 -160
View File
@@ -17,7 +17,7 @@ ESP_EVENT_DEFINE_BASE(ZH_NRF_GATEWAY_EVENTS);
static const char *TAG = "NRF_GW"; static const char *TAG = "NRF_GW";
/* ================== nRF24 КОМАНДЫ ================== */ /* ================== nRF24 КОМАНДЫ / РЕГИСТРЫ ================== */
#define NRF_CMD_R_REGISTER 0x00 #define NRF_CMD_R_REGISTER 0x00
#define NRF_CMD_W_REGISTER 0x20 #define NRF_CMD_W_REGISTER 0x20
#define NRF_CMD_R_RX_PAYLOAD 0x61 #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_ACTIVATE 0x50
#define NRF_CMD_R_RX_PL_WID 0x60 #define NRF_CMD_R_RX_PL_WID 0x60
/* ================== nRF24 РЕГИСТРЫ ================== */
#define NRF_REG_CONFIG 0x00 #define NRF_REG_CONFIG 0x00
#define NRF_REG_EN_AA 0x01 #define NRF_REG_EN_AA 0x01
#define NRF_REG_EN_RXADDR 0x02 #define NRF_REG_EN_RXADDR 0x02
@@ -43,7 +42,6 @@ static const char *TAG = "NRF_GW";
#define NRF_REG_DYNPD 0x1C #define NRF_REG_DYNPD 0x1C
#define NRF_REG_FEATURE 0x1D #define NRF_REG_FEATURE 0x1D
/* ================== nRF24 БИТЫ ================== */
#define NRF_CONFIG_MASK_RX_DR 0x40 #define NRF_CONFIG_MASK_RX_DR 0x40
#define NRF_CONFIG_MASK_TX_DS 0x20 #define NRF_CONFIG_MASK_TX_DS 0x20
#define NRF_CONFIG_MASK_MAX_RT 0x10 #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_STATUS_MAX_RT 0x10
#define NRF_FIFO_RX_EMPTY 0x01 #define NRF_FIFO_RX_EMPTY 0x01
#define NRF_FEATURE_EN_DPL 0x04 #define NRF_FEATURE_EN_DPL 0x04
#define NRF_DYNPD_DPL_P0 0x01 #define NRF_DYNPD_DPL_P0 0x01
/* NVS namespace and key */ /* ================== NVS ================== */
#define NVS_NAMESPACE "zh_gw" #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 static struct
{ {
zh_nrf_gateway_config_t cfg; zh_nrf_gateway_config_t cfg;
spi_device_handle_t spi; spi_device_handle_t spi;
SemaphoreHandle_t irq_sem; SemaphoreHandle_t irq_sem;
SemaphoreHandle_t bindings_mutex;
TaskHandle_t task; TaskHandle_t task;
bindings_blob_t bindings;
bool initialized; bool initialized;
} s_ctx = {0}; } s_ctx = {0};
@@ -169,87 +178,281 @@ static void IRAM_ATTR nrf_irq_isr(void *arg)
{ {
BaseType_t woken = pdFALSE; BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(s_ctx.irq_sem, &woken); xSemaphoreGiveFromISR(s_ctx.irq_sem, &woken);
if (woken) if (woken) portYIELD_FROM_ISR();
portYIELD_FROM_ISR();
} }
static esp_err_t nrf_irq_init(void) static esp_err_t nrf_irq_init(void)
{ {
s_ctx.irq_sem = xSemaphoreCreateBinary(); s_ctx.irq_sem = xSemaphoreCreateBinary();
if (!s_ctx.irq_sem) if (!s_ctx.irq_sem) return ESP_ERR_NO_MEM;
return ESP_ERR_NO_MEM;
gpio_config_t io_conf = { gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << s_ctx.cfg.pin_irq), .pin_bit_mask = (1ULL << s_ctx.cfg.pin_irq),
.mode = GPIO_MODE_INPUT, .mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE, .pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE, .intr_type = GPIO_INTR_NEGEDGE,
}; };
esp_err_t err = gpio_config(&io_conf); esp_err_t err = gpio_config(&io_conf);
if (err != ESP_OK) if (err != ESP_OK) return err;
return err;
err = gpio_install_isr_service(0); err = gpio_install_isr_service(0);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) return err;
return err;
return gpio_isr_handler_add(s_ctx.cfg.pin_irq, nrf_irq_isr, NULL); 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; nvs_handle_t h;
uint8_t id = 0; esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h);
if (err != ESP_OK) {
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h) != ESP_OK) memset(&s_ctx.bindings, 0, sizeof(s_ctx.bindings));
{ return ESP_OK; /* empty table on first run */
ESP_LOGW(TAG, "NVS open failed, using fallback id=1");
return 1;
} }
if (nvs_get_u8(h, NVS_KEY_NEXT_ID, &id) != ESP_OK || id == 0 || id == 0xFF) size_t size = sizeof(s_ctx.bindings);
{ err = nvs_get_blob(h, NVS_KEY_BINDINGS, &s_ctx.bindings, &size);
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);
nvs_close(h); nvs_close(h);
ESP_LOGI(TAG, "Pairing: issued device_id=%u (next=%u)", id, next); if (err == ESP_ERR_NVS_NOT_FOUND) {
return id; 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] = { uint8_t pkt[ZH_NRF24_PAIRING_PACKET_SIZE] = {
req_type, req_type,
ZH_NRF24_PAIRING_ACK_MAGIC, ZH_NRF24_PAIRING_ACK_MAGIC,
(uint8_t)(token >> 8), (uint8_t)(token >> 8),
(uint8_t)(token & 0xFF), (uint8_t)(token & 0xFF),
new_id}; new_id
};
/* 1. Заглушить ВСЕ IRQ + перейти в PTX standby */
nrf_ce_low(); nrf_ce_low();
nrf_write_register(NRF_REG_CONFIG, NRF_CONFIG_ALL_MASKED | NRF_CONFIG_PWR_UP); nrf_write_register(NRF_REG_CONFIG, NRF_CONFIG_ALL_MASKED | NRF_CONFIG_PWR_UP);
vTaskDelay(pdMS_TO_TICKS(2)); vTaskDelay(pdMS_TO_TICKS(2));
/* 2. Сбросить только TX-флаги */
nrf_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT); nrf_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT);
nrf_send_command(NRF_CMD_FLUSH_TX); nrf_send_command(NRF_CMD_FLUSH_TX);
/* 3. Записать payload и запустить TX */
uint8_t tx[1 + ZH_NRF24_PAIRING_PACKET_SIZE]; uint8_t tx[1 + ZH_NRF24_PAIRING_PACKET_SIZE];
tx[0] = NRF_CMD_W_TX_PAYLOAD; tx[0] = NRF_CMD_W_TX_PAYLOAD;
memcpy(&tx[1], pkt, ZH_NRF24_PAIRING_PACKET_SIZE); 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); esp_rom_delay_us(20);
nrf_ce_low(); nrf_ce_low();
/* 4. Опрос TX_DS / MAX_RT */
uint8_t status = 0; 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); status = nrf_send_command(NRF_CMD_NOP);
if (status & (NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT)) if (status & (NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT)) break;
break;
vTaskDelay(pdMS_TO_TICKS(1)); vTaskDelay(pdMS_TO_TICKS(1));
} }
if (status & NRF_STATUS_TX_DS) bool ok = (status & NRF_STATUS_TX_DS) != 0;
{
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);
}
/* 5. Сбросить TX-флаги */
nrf_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT); nrf_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT);
nrf_send_command(NRF_CMD_FLUSH_TX); nrf_send_command(NRF_CMD_FLUSH_TX);
/* 6. Возврат в RX, снимаем маску */
nrf_write_register(NRF_REG_CONFIG, NRF_CONFIG_RX_UNMASKED); nrf_write_register(NRF_REG_CONFIG, NRF_CONFIG_RX_UNMASKED);
vTaskDelay(pdMS_TO_TICKS(5)); vTaskDelay(pdMS_TO_TICKS(5));
nrf_ce_high(); nrf_ce_high();
vTaskDelay(pdMS_TO_TICKS(1)); 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) static void handle_pairing_request(const uint8_t *data)
{ {
uint8_t req_type = data[0]; uint8_t req_type = data[0];
uint16_t token = ((uint16_t)data[2] << 8) | data[3]; uint16_t token = ((uint16_t)data[2] << 8) | data[3];
ESP_LOGI(TAG, "Pairing request: type=%u (%s), token=0x%04X", ESP_LOGI(TAG, "Pairing request: type=%u (%s), token=0x%04X",
req_type, req_type,
zh_get_nrf24_sensor_type_name((zh_nrf24_sensor_type_t)req_type), zh_get_nrf24_sensor_type_name((zh_nrf24_sensor_type_t)req_type),
token); token);
uint8_t new_id = allocate_device_id(); xSemaphoreTake(s_ctx.bindings_mutex, portMAX_DELAY);
send_pairing_ack(req_type, token, new_id);
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) 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); ESP_LOGW(TAG, "Too short data packet (%u)", len);
return; return;
} }
zh_nrf24_sensor_transmitted_data_t d = {0}; zh_nrf24_sensor_transmitted_data_t d = {0};
d.id = zh_nrf24_sensor_read_i16_le_at(buf, 0); d.id = zh_nrf24_sensor_read_i16_le_at(buf, 0);
d.type = zh_nrf24_sensor_read_i16_le_at(buf, 2); d.type = zh_nrf24_sensor_read_i16_le_at(buf, 2);
d.battery = zh_nrf24_sensor_read_i16_le_at(buf, 4); d.battery = zh_nrf24_sensor_read_i16_le_at(buf, 4);
uint8_t expected = zh_get_nrf24_sensor_payload_size(d.type); 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); ESP_LOGW(TAG, "Unknown sensor type %d, dropping", d.type);
return; return;
} }
if (len != expected) if (len != expected) {
{ ESP_LOGW(TAG, "Length %u != expected %u for type %d",
ESP_LOGW(TAG, "Length %u != expected %u for type %d (%s)", len, expected, d.type);
len, expected, d.type,
zh_get_nrf24_sensor_type_name((zh_nrf24_sensor_type_t)d.type));
} }
switch (d.type) switch (d.type) {
{
case ZH_NRF24_SENSOR_TOUCH_SWITCH: case ZH_NRF24_SENSOR_TOUCH_SWITCH:
break; break;
case ZH_NRF24_SENSOR_WATER_LEAKAGE: case ZH_NRF24_SENSOR_WATER_LEAKAGE:
case ZH_NRF24_SENSOR_OPEN_CLOSE: case ZH_NRF24_SENSOR_OPEN_CLOSE:
case ZH_NRF24_SENSOR_PLANT_HUMIDITY: case ZH_NRF24_SENSOR_PLANT_HUMIDITY:
if (len >= ZH_NRF24_DATA_HEADER_SIZE + ZH_NRF24_DATA_FIELD_SIZE) if (len >= ZH_NRF24_DATA_HEADER_SIZE + ZH_NRF24_DATA_FIELD_SIZE)
d.value_1 = zh_nrf24_sensor_read_i16_le_at(buf, 6); d.value_1 = zh_nrf24_sensor_read_i16_le_at(buf, 6);
break; break;
case ZH_NRF24_SENSOR_BMP280: 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_1 = zh_nrf24_sensor_read_i16_le_at(buf, 6);
d.value_2 = zh_nrf24_sensor_read_i16_le_at(buf, 8); d.value_2 = zh_nrf24_sensor_read_i16_le_at(buf, 8);
} }
break; break;
case ZH_NRF24_SENSOR_BME280: 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_1 = zh_nrf24_sensor_read_i16_le_at(buf, 6);
d.value_2 = zh_nrf24_sensor_read_i16_le_at(buf, 8); 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_3 = zh_nrf24_sensor_read_i16_le_at(buf, 10);
} }
break; break;
case ZH_NRF24_SENSOR_BME680: 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_1 = zh_nrf24_sensor_read_i16_le_at(buf, 6);
d.value_2 = zh_nrf24_sensor_read_i16_le_at(buf, 8); 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_3 = zh_nrf24_sensor_read_i16_le_at(buf, 10);
d.value_4 = zh_nrf24_sensor_read_i16_le_at(buf, 12); d.value_4 = zh_nrf24_sensor_read_i16_le_at(buf, 12);
} }
break; break;
default: default:
break; break;
} }
esp_err_t err = esp_event_post(ZH_NRF_GATEWAY_EVENTS, esp_err_t err = esp_event_post(ZH_NRF_GATEWAY_EVENTS,
ZH_NRF_GATEWAY_EVENT_SENSOR_DATA, ZH_NRF_GATEWAY_EVENT_SENSOR_DATA,
&d, sizeof(d), &d, sizeof(d), pdMS_TO_TICKS(100));
pdMS_TO_TICKS(100)); if (err != ESP_OK) {
if (err != ESP_OK)
{
ESP_LOGW(TAG, "esp_event_post failed: %s", esp_err_to_name(err)); 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) static bool process_one_packet(void)
{ {
uint8_t fifo = nrf_read_register(NRF_REG_FIFO_STATUS); uint8_t fifo = nrf_read_register(NRF_REG_FIFO_STATUS);
if (fifo & NRF_FIFO_RX_EMPTY) if (fifo & NRF_FIFO_RX_EMPTY) return false;
return false;
uint8_t width = nrf_read_payload_width(); 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); ESP_LOGW(TAG, "Corrupt payload width %u, FLUSH_RX", width);
nrf_send_command(NRF_CMD_FLUSH_RX); nrf_send_command(NRF_CMD_FLUSH_RX);
nrf_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR); 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_read_payload(buf, width);
nrf_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR); 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); handle_pairing_request(buf);
} } else {
else
{
handle_sensor_data(buf, width); handle_sensor_data(buf, width);
} }
return true; return true;
@@ -428,25 +629,15 @@ static bool process_one_packet(void)
static void process_all_pending(void) static void process_all_pending(void)
{ {
while (process_one_packet()) while (process_one_packet()) { }
{
}
} }
/* ============================================================
* RECEIVER TASK
* ============================================================ */
static void gateway_task(void *arg) static void gateway_task(void *arg)
{ {
while (1) while (1) {
{ if (xSemaphoreTake(s_ctx.irq_sem, pdMS_TO_TICKS(1000)) == pdTRUE) {
if (xSemaphoreTake(s_ctx.irq_sem, pdMS_TO_TICKS(1000)) == pdTRUE)
{
process_all_pending(); process_all_pending();
} } else {
else
{
/* Fallback polling once per second */
process_all_pending(); 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) esp_err_t zh_nrf_gateway_init(const zh_nrf_gateway_config_t *config)
{ {
if (!config) if (!config) return ESP_ERR_INVALID_ARG;
return ESP_ERR_INVALID_ARG; if (s_ctx.initialized) return ESP_ERR_INVALID_STATE;
if (s_ctx.initialized)
return ESP_ERR_INVALID_STATE;
s_ctx.cfg = *config; s_ctx.cfg = *config;
if (s_ctx.cfg.spi_clock_hz == 0) if (s_ctx.cfg.spi_clock_hz == 0) s_ctx.cfg.spi_clock_hz = 4 * 1000 * 1000;
s_ctx.cfg.spi_clock_hz = 4 * 1000 * 1000; if (s_ctx.cfg.channel > 125) s_ctx.cfg.channel = 120;
if (s_ctx.cfg.channel > 125)
s_ctx.cfg.channel = 120;
/* NVS - init if needed */
esp_err_t err = nvs_flash_init(); 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_LOGW(TAG, "NVS flash needs erase, erasing");
ESP_ERROR_CHECK(nvs_flash_erase()); ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init(); err = nvs_flash_init();
} }
if (err != ESP_OK) if (err != ESP_OK) return err;
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(); err = esp_event_loop_create_default();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) return err;
return err;
/* SPI bus */
spi_bus_config_t buscfg = { spi_bus_config_t buscfg = {
.miso_io_num = s_ctx.cfg.pin_miso, .miso_io_num = s_ctx.cfg.pin_miso,
.mosi_io_num = s_ctx.cfg.pin_mosi, .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, .max_transfer_sz = ZH_NRF24_MAX_PAYLOAD_SIZE,
}; };
err = spi_bus_initialize(s_ctx.cfg.spi_host, &buscfg, SPI_DMA_CH_AUTO); err = spi_bus_initialize(s_ctx.cfg.spi_host, &buscfg, SPI_DMA_CH_AUTO);
if (err != ESP_OK) if (err != ESP_OK) return err;
return err;
spi_device_interface_config_t devcfg = { spi_device_interface_config_t devcfg = {
.clock_speed_hz = (int)s_ctx.cfg.spi_clock_hz, .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, .queue_size = 1,
}; };
err = spi_bus_add_device(s_ctx.cfg.spi_host, &devcfg, &s_ctx.spi); err = spi_bus_add_device(s_ctx.cfg.spi_host, &devcfg, &s_ctx.spi);
if (err != ESP_OK) if (err != ESP_OK) return err;
return err;
/* CE pin */
gpio_config_t ce_conf = { gpio_config_t ce_conf = {
.pin_bit_mask = (1ULL << s_ctx.cfg.pin_ce), .pin_bit_mask = (1ULL << s_ctx.cfg.pin_ce),
.mode = GPIO_MODE_OUTPUT, .mode = GPIO_MODE_OUTPUT,
}; };
err = gpio_config(&ce_conf); err = gpio_config(&ce_conf);
if (err != ESP_OK) if (err != ESP_OK) return err;
return err;
nrf_ce_low(); nrf_ce_low();
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
/* nRF24 config */
nrf_write_register(NRF_REG_CONFIG, 0x00); nrf_write_register(NRF_REG_CONFIG, 0x00);
nrf_write_register(NRF_REG_EN_AA, 0x01); nrf_write_register(NRF_REG_EN_AA, 0x01);
nrf_write_register(NRF_REG_EN_RXADDR, 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_SETUP_RETR, 0xFF);
nrf_write_register(NRF_REG_RF_CH, s_ctx.cfg.channel); 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 addr_bytes[3] = {
(uint8_t)(s_ctx.cfg.address >> 0), (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_buffer(NRF_REG_RX_ADDR_P0, addr_bytes, 3);
nrf_write_register(NRF_REG_STATUS, 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_activate();
nrf_write_register(NRF_REG_FEATURE, NRF_FEATURE_EN_DPL); 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); nrf_write_register(NRF_REG_CONFIG, NRF_CONFIG_RX_UNMASKED);
vTaskDelay(pdMS_TO_TICKS(5)); vTaskDelay(pdMS_TO_TICKS(5));
nrf_ce_high(); nrf_ce_high();
vTaskDelay(pdMS_TO_TICKS(1)); vTaskDelay(pdMS_TO_TICKS(1));
/* IRQ GPIO */
err = nrf_irq_init(); err = nrf_irq_init();
if (err != ESP_OK) if (err != ESP_OK) return err;
return err;
xSemaphoreTake(s_ctx.irq_sem, 0); xSemaphoreTake(s_ctx.irq_sem, 0);
s_ctx.initialized = true; s_ctx.initialized = true;
ESP_LOGI(TAG, "Initialized: ch=%u addr=0x%06X DPL on, RX mode", 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.cfg.channel, (unsigned)(s_ctx.cfg.address & 0xFFFFFF),
s_ctx.bindings.count);
return ESP_OK; return ESP_OK;
} }
esp_err_t zh_nrf_gateway_start(void) esp_err_t zh_nrf_gateway_start(void)
{ {
if (!s_ctx.initialized) if (!s_ctx.initialized) return ESP_ERR_INVALID_STATE;
return ESP_ERR_INVALID_STATE; if (s_ctx.task) return ESP_OK;
if (s_ctx.task)
return ESP_OK;
BaseType_t r = xTaskCreatePinnedToCore(gateway_task, "nrf_gateway", BaseType_t r = xTaskCreatePinnedToCore(gateway_task, "nrf_gateway",
4096, NULL, 5, 4096, NULL, 5,
&s_ctx.task, tskNO_AFFINITY); &s_ctx.task, tskNO_AFFINITY);
if (r != pdPASS) if (r != pdPASS) {
{
s_ctx.task = NULL; s_ctx.task = NULL;
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} }