144 lines
5 KiB
C
144 lines
5 KiB
C
|
|
/**
|
||
|
|
* DreamStack ESP-NOW Transport — Ultra-Low Latency Binary Protocol
|
||
|
|
*
|
||
|
|
* Sub-1ms signal delivery over ESP-NOW (WiFi direct, no router).
|
||
|
|
* Binary packed frames instead of JSON for minimal overhead.
|
||
|
|
*
|
||
|
|
* Transport strategy:
|
||
|
|
* ESP-NOW: real-time signals + events (<1ms, ≤250 bytes)
|
||
|
|
* UDP: initial IR push + large payloads (~2ms, ≤1472 bytes)
|
||
|
|
*/
|
||
|
|
|
||
|
|
#pragma once
|
||
|
|
#include <stdint.h>
|
||
|
|
#include <stdbool.h>
|
||
|
|
#include "esp_now.h"
|
||
|
|
|
||
|
|
// ─── ESP-NOW Frame Types ───
|
||
|
|
#define DS_NOW_SIG 0x20 // Single signal update (hub → panel)
|
||
|
|
#define DS_NOW_SIG_BATCH 0x21 // Batch signal update (hub → panel)
|
||
|
|
#define DS_NOW_TOUCH 0x30 // Touch event (panel → hub)
|
||
|
|
#define DS_NOW_ACTION 0x31 // Button/widget action (panel → hub)
|
||
|
|
#define DS_NOW_PING 0xFE // Heartbeat (bidirectional)
|
||
|
|
#define DS_NOW_PONG 0xFD // Heartbeat response
|
||
|
|
|
||
|
|
// ─── UDP Frame Types ───
|
||
|
|
#define DS_UDP_IR_PUSH 0x40 // Full IR JSON push (hub → panel)
|
||
|
|
#define DS_UDP_IR_FRAG 0x41 // IR fragment for payloads > MTU
|
||
|
|
#define DS_UDP_DISCOVER 0x42 // Panel discovery broadcast
|
||
|
|
|
||
|
|
// ─── UDP Port ───
|
||
|
|
#define DS_UDP_PORT 9200
|
||
|
|
|
||
|
|
// ─── ESP-NOW Channel ───
|
||
|
|
#define DS_ESPNOW_CHANNEL 1
|
||
|
|
|
||
|
|
// ─── Max ESP-NOW payload ───
|
||
|
|
#define DS_ESPNOW_MAX_DATA 250
|
||
|
|
|
||
|
|
// ─── Signal Update Frame (7 bytes) ───
|
||
|
|
// Hub → Panel: update a single signal value
|
||
|
|
typedef struct __attribute__((packed)) {
|
||
|
|
uint8_t type; // DS_NOW_SIG
|
||
|
|
uint16_t signal_id; // which signal (0-65535)
|
||
|
|
int32_t value; // new value
|
||
|
|
} ds_sig_frame_t;
|
||
|
|
|
||
|
|
// ─── Signal Batch Frame (3 + 6*N bytes) ───
|
||
|
|
// Hub → Panel: update multiple signals at once
|
||
|
|
typedef struct __attribute__((packed)) {
|
||
|
|
uint16_t id;
|
||
|
|
int32_t val;
|
||
|
|
} ds_sig_entry_t;
|
||
|
|
|
||
|
|
typedef struct __attribute__((packed)) {
|
||
|
|
uint8_t type; // DS_NOW_SIG_BATCH
|
||
|
|
uint8_t count; // number of signals (max ~40 in 250B)
|
||
|
|
uint8_t seq; // sequence number (wrapping u8)
|
||
|
|
// followed by `count` ds_sig_entry_t entries
|
||
|
|
} ds_sig_batch_t;
|
||
|
|
|
||
|
|
// ─── Touch Event Frame (8 bytes) ───
|
||
|
|
// Panel → Hub: touch on the display
|
||
|
|
typedef struct __attribute__((packed)) {
|
||
|
|
uint8_t type; // DS_NOW_TOUCH
|
||
|
|
uint8_t node_id; // which UI node (from IR)
|
||
|
|
uint8_t event; // 0=click, 1=long_press, 2=release, 3=drag
|
||
|
|
uint8_t seq; // sequence number
|
||
|
|
uint16_t x; // touch X coordinate
|
||
|
|
uint16_t y; // touch Y coordinate
|
||
|
|
} ds_touch_now_t;
|
||
|
|
|
||
|
|
// ─── Action Event Frame (4 bytes) ───
|
||
|
|
// Panel → Hub: widget action (button click, toggle, etc.)
|
||
|
|
typedef struct __attribute__((packed)) {
|
||
|
|
uint8_t type; // DS_NOW_ACTION
|
||
|
|
uint8_t node_id; // which widget
|
||
|
|
uint8_t action; // 0=click, 1=toggle, 2=slide_change
|
||
|
|
uint8_t seq; // sequence number
|
||
|
|
} ds_action_frame_t;
|
||
|
|
|
||
|
|
// ─── Heartbeat Frame (2 bytes) ───
|
||
|
|
typedef struct __attribute__((packed)) {
|
||
|
|
uint8_t type; // DS_NOW_PING or DS_NOW_PONG
|
||
|
|
uint8_t seq; // echo back on pong
|
||
|
|
} ds_heartbeat_t;
|
||
|
|
|
||
|
|
// ─── UDP IR Push Header (4 bytes + payload) ───
|
||
|
|
typedef struct __attribute__((packed)) {
|
||
|
|
uint8_t magic[2]; // 0xD5, 0x7A
|
||
|
|
uint16_t length; // JSON payload length
|
||
|
|
// followed by `length` bytes of IR JSON
|
||
|
|
} ds_ir_push_t;
|
||
|
|
|
||
|
|
// ─── UDP IR Fragment Header (6 bytes + payload) ───
|
||
|
|
typedef struct __attribute__((packed)) {
|
||
|
|
uint8_t magic[2]; // 0xD5, 0x7A
|
||
|
|
uint8_t type; // DS_UDP_IR_FRAG
|
||
|
|
uint8_t frag_id; // fragment index (0-based)
|
||
|
|
uint8_t frag_total; // total fragments
|
||
|
|
uint8_t seq; // group sequence
|
||
|
|
// followed by fragment data (up to 1466 bytes)
|
||
|
|
} ds_ir_frag_t;
|
||
|
|
|
||
|
|
// ─── Callbacks ───
|
||
|
|
typedef void (*ds_signal_cb_t)(uint16_t signal_id, int32_t value);
|
||
|
|
typedef void (*ds_ir_cb_t)(const char *ir_json, size_t length);
|
||
|
|
|
||
|
|
// ─── Configuration ───
|
||
|
|
typedef struct {
|
||
|
|
uint8_t hub_mac[6]; // Hub MAC address (set to FF:FF:FF:FF:FF:FF for broadcast)
|
||
|
|
uint8_t channel; // WiFi channel (default: DS_ESPNOW_CHANNEL)
|
||
|
|
ds_signal_cb_t on_signal; // Called when a signal update arrives
|
||
|
|
ds_ir_cb_t on_ir_push; // Called when a full IR JSON arrives
|
||
|
|
} ds_espnow_config_t;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize ESP-NOW transport.
|
||
|
|
* Sets up ESP-NOW, registers peer, starts UDP listener.
|
||
|
|
* WiFi must be initialized first (STA or AP mode, no connection needed).
|
||
|
|
*/
|
||
|
|
esp_err_t ds_espnow_init(const ds_espnow_config_t *config);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Send an action event to the hub (panel → hub).
|
||
|
|
* Encodes as ds_action_frame_t and sends via ESP-NOW.
|
||
|
|
*/
|
||
|
|
esp_err_t ds_espnow_send_action(uint8_t node_id, uint8_t action);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Send a touch event to the hub (panel → hub).
|
||
|
|
*/
|
||
|
|
esp_err_t ds_espnow_send_touch(uint8_t node_id, uint8_t event,
|
||
|
|
uint16_t x, uint16_t y);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Send a heartbeat ping.
|
||
|
|
*/
|
||
|
|
esp_err_t ds_espnow_send_ping(void);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Deinitialize ESP-NOW transport.
|
||
|
|
*/
|
||
|
|
void ds_espnow_deinit(void);
|