762 lines
24 KiB
C
762 lines
24 KiB
C
#include "zh_nrf24_gateway.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#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"
|
|
|
|
/* 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};
|
|
|
|
/* ============================================================
|
|
* 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; /* empty table on first run */
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* ============================================================
|
|
* 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;
|
|
|
|
/* 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
|
|
};
|
|
|
|
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);
|
|
} else {
|
|
ESP_LOGI(TAG, "Pairing complete: id=%u type=%u", new_id, req_type);
|
|
}
|
|
}
|
|
|
|
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;
|
|
} |