248 lines
7.6 KiB
C
248 lines
7.6 KiB
C
/**
|
|
* DreamStack ESP-NOW Transport — Implementation
|
|
*
|
|
* Handles ESP-NOW receive/send for binary signal frames,
|
|
* and UDP listener for IR JSON push.
|
|
*/
|
|
|
|
#include <string.h>
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "esp_log.h"
|
|
#include "esp_wifi.h"
|
|
#include "esp_now.h"
|
|
#include "lwip/sockets.h"
|
|
|
|
#include "ds_espnow.h"
|
|
|
|
static const char *TAG = "ds-espnow";
|
|
|
|
// ─── State ───
|
|
static ds_espnow_config_t s_config;
|
|
static uint8_t s_seq = 0;
|
|
static int s_udp_sock = -1;
|
|
static TaskHandle_t s_udp_task = NULL;
|
|
|
|
// ─── IR fragment reassembly ───
|
|
#define MAX_IR_SIZE 16384
|
|
static uint8_t s_ir_buf[MAX_IR_SIZE];
|
|
static size_t s_ir_len = 0;
|
|
static uint8_t s_frag_received = 0;
|
|
static uint8_t s_frag_total = 0;
|
|
static uint8_t s_frag_seq = 0xFF;
|
|
|
|
// ─── ESP-NOW Receive Callback ───
|
|
static void espnow_recv_cb(const esp_now_recv_info_t *recv_info,
|
|
const uint8_t *data, int len) {
|
|
if (len < 1) return;
|
|
|
|
uint8_t type = data[0];
|
|
|
|
switch (type) {
|
|
case DS_NOW_SIG:
|
|
if (len >= sizeof(ds_sig_frame_t)) {
|
|
const ds_sig_frame_t *f = (const ds_sig_frame_t *)data;
|
|
if (s_config.on_signal) {
|
|
s_config.on_signal(f->signal_id, f->value);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case DS_NOW_SIG_BATCH:
|
|
if (len >= sizeof(ds_sig_batch_t)) {
|
|
const ds_sig_batch_t *b = (const ds_sig_batch_t *)data;
|
|
const ds_sig_entry_t *entries = (const ds_sig_entry_t *)(data + sizeof(ds_sig_batch_t));
|
|
size_t expected = sizeof(ds_sig_batch_t) + b->count * sizeof(ds_sig_entry_t);
|
|
if (len >= expected && s_config.on_signal) {
|
|
for (int i = 0; i < b->count; i++) {
|
|
s_config.on_signal(entries[i].id, entries[i].val);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case DS_NOW_PING: {
|
|
// Respond with pong
|
|
ds_heartbeat_t pong = { .type = DS_NOW_PONG, .seq = data[1] };
|
|
esp_now_send(recv_info->src_addr, (const uint8_t *)&pong, sizeof(pong));
|
|
break;
|
|
}
|
|
|
|
case DS_NOW_PONG:
|
|
ESP_LOGD(TAG, "Pong received (seq=%d)", data[1]);
|
|
break;
|
|
|
|
default:
|
|
ESP_LOGW(TAG, "Unknown ESP-NOW frame type: 0x%02x", type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ─── ESP-NOW Send Callback ───
|
|
static void espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) {
|
|
if (status != ESP_NOW_SEND_SUCCESS) {
|
|
ESP_LOGW(TAG, "ESP-NOW send failed");
|
|
}
|
|
}
|
|
|
|
// ─── UDP Listener Task ───
|
|
// Receives IR JSON push and fragmented IR over UDP
|
|
static void udp_listener_task(void *arg) {
|
|
struct sockaddr_in addr = {
|
|
.sin_family = AF_INET,
|
|
.sin_port = htons(DS_UDP_PORT),
|
|
.sin_addr.s_addr = htonl(INADDR_ANY),
|
|
};
|
|
|
|
s_udp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
|
if (s_udp_sock < 0) {
|
|
ESP_LOGE(TAG, "Failed to create UDP socket");
|
|
vTaskDelete(NULL);
|
|
return;
|
|
}
|
|
|
|
if (bind(s_udp_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
|
ESP_LOGE(TAG, "Failed to bind UDP port %d", DS_UDP_PORT);
|
|
close(s_udp_sock);
|
|
s_udp_sock = -1;
|
|
vTaskDelete(NULL);
|
|
return;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "UDP listener on port %d", DS_UDP_PORT);
|
|
|
|
uint8_t buf[1500];
|
|
while (1) {
|
|
int len = recvfrom(s_udp_sock, buf, sizeof(buf), 0, NULL, NULL);
|
|
if (len < 4) continue;
|
|
|
|
// Check magic
|
|
if (buf[0] != 0xD5 || buf[1] != 0x7A) continue;
|
|
|
|
uint8_t frame_type = buf[2];
|
|
|
|
if (frame_type == DS_UDP_IR_PUSH) {
|
|
// Non-fragmented IR push
|
|
const ds_ir_push_t *hdr = (const ds_ir_push_t *)buf;
|
|
size_t json_len = hdr->length;
|
|
if (json_len <= len - sizeof(ds_ir_push_t) && json_len < MAX_IR_SIZE) {
|
|
memcpy(s_ir_buf, buf + sizeof(ds_ir_push_t), json_len);
|
|
s_ir_buf[json_len] = '\0';
|
|
ESP_LOGI(TAG, "IR push received (%zu bytes)", json_len);
|
|
if (s_config.on_ir_push) {
|
|
s_config.on_ir_push((const char *)s_ir_buf, json_len);
|
|
}
|
|
}
|
|
} else if (frame_type == DS_UDP_IR_FRAG) {
|
|
// Fragmented IR push
|
|
if (len < sizeof(ds_ir_frag_t)) continue;
|
|
const ds_ir_frag_t *frag = (const ds_ir_frag_t *)buf;
|
|
size_t payload_len = len - sizeof(ds_ir_frag_t);
|
|
const uint8_t *payload = buf + sizeof(ds_ir_frag_t);
|
|
|
|
// New fragment group?
|
|
if (frag->seq != s_frag_seq) {
|
|
s_frag_seq = frag->seq;
|
|
s_frag_received = 0;
|
|
s_frag_total = frag->frag_total;
|
|
s_ir_len = 0;
|
|
}
|
|
|
|
// Append fragment (assume ordered delivery)
|
|
if (s_ir_len + payload_len < MAX_IR_SIZE) {
|
|
memcpy(s_ir_buf + s_ir_len, payload, payload_len);
|
|
s_ir_len += payload_len;
|
|
s_frag_received++;
|
|
|
|
if (s_frag_received >= s_frag_total) {
|
|
s_ir_buf[s_ir_len] = '\0';
|
|
ESP_LOGI(TAG, "IR reassembled (%zu bytes, %d frags)",
|
|
s_ir_len, s_frag_total);
|
|
if (s_config.on_ir_push) {
|
|
s_config.on_ir_push((const char *)s_ir_buf, s_ir_len);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Public API ───
|
|
|
|
esp_err_t ds_espnow_init(const ds_espnow_config_t *config) {
|
|
s_config = *config;
|
|
if (s_config.channel == 0) s_config.channel = DS_ESPNOW_CHANNEL;
|
|
|
|
// Initialize ESP-NOW
|
|
esp_err_t ret = esp_now_init();
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "ESP-NOW init failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
esp_now_register_recv_cb(espnow_recv_cb);
|
|
esp_now_register_send_cb(espnow_send_cb);
|
|
|
|
// Add hub as peer
|
|
esp_now_peer_info_t peer = {
|
|
.channel = s_config.channel,
|
|
.ifidx = WIFI_IF_STA,
|
|
.encrypt = false,
|
|
};
|
|
memcpy(peer.peer_addr, s_config.hub_mac, 6);
|
|
ret = esp_now_add_peer(&peer);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGW(TAG, "Add peer failed (may already exist): %s", esp_err_to_name(ret));
|
|
}
|
|
|
|
ESP_LOGI(TAG, "ESP-NOW initialized (ch=%d, hub=%02x:%02x:%02x:%02x:%02x:%02x)",
|
|
s_config.channel,
|
|
s_config.hub_mac[0], s_config.hub_mac[1], s_config.hub_mac[2],
|
|
s_config.hub_mac[3], s_config.hub_mac[4], s_config.hub_mac[5]);
|
|
|
|
// Start UDP listener
|
|
xTaskCreate(udp_listener_task, "ds_udp", 4096, NULL, 5, &s_udp_task);
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t ds_espnow_send_action(uint8_t node_id, uint8_t action) {
|
|
ds_action_frame_t frame = {
|
|
.type = DS_NOW_ACTION,
|
|
.node_id = node_id,
|
|
.action = action,
|
|
.seq = s_seq++,
|
|
};
|
|
return esp_now_send(s_config.hub_mac, (const uint8_t *)&frame, sizeof(frame));
|
|
}
|
|
|
|
esp_err_t ds_espnow_send_touch(uint8_t node_id, uint8_t event,
|
|
uint16_t x, uint16_t y) {
|
|
ds_touch_now_t frame = {
|
|
.type = DS_NOW_TOUCH,
|
|
.node_id = node_id,
|
|
.event = event,
|
|
.seq = s_seq++,
|
|
.x = x,
|
|
.y = y,
|
|
};
|
|
return esp_now_send(s_config.hub_mac, (const uint8_t *)&frame, sizeof(frame));
|
|
}
|
|
|
|
esp_err_t ds_espnow_send_ping(void) {
|
|
ds_heartbeat_t ping = { .type = DS_NOW_PING, .seq = s_seq++ };
|
|
return esp_now_send(s_config.hub_mac, (const uint8_t *)&ping, sizeof(ping));
|
|
}
|
|
|
|
void ds_espnow_deinit(void) {
|
|
if (s_udp_task) {
|
|
vTaskDelete(s_udp_task);
|
|
s_udp_task = NULL;
|
|
}
|
|
if (s_udp_sock >= 0) {
|
|
close(s_udp_sock);
|
|
s_udp_sock = -1;
|
|
}
|
|
esp_now_deinit();
|
|
ESP_LOGI(TAG, "ESP-NOW deinitialized");
|
|
}
|