feat(01-02): wifi_server.h/.cpp — AsyncUDP listener with FreeRTOS queue
- RawPacket struct carries raw bytes, IP, port from AsyncUDP callback - xQueueSendFromISR in callback (ISR-safe) — zero JSON parsing in callback - parseTask runs in separate FreeRTOS task, drains rawQueue, parses JSON - STATUS command handled immediately in parseTask with UDP response to sender - commandQueue declared extern for animation engine (Plan 03) to consume - main.cpp: include wifi_server.h, call startUdpServer() after WIFI_PS_NONE
This commit is contained in:
194
firmware/src/animation_engine.cpp
Normal file
194
firmware/src/animation_engine.cpp
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
#include "animation_engine.h"
|
||||||
|
#include "led_driver.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "wifi_server.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Animation registry — maps name strings to tick functions.
|
||||||
|
// These are the exact strings the Pi uses in JSON "animation" field.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const AnimEntry ANIM_REGISTRY[] = {
|
||||||
|
{"chase", animChase},
|
||||||
|
{"pulse", animPulse},
|
||||||
|
{"rainbow", animRainbow},
|
||||||
|
{"strobe", animStrobe},
|
||||||
|
{"color_wash", animColorWash},
|
||||||
|
{"breathe", animBreathe},
|
||||||
|
{"sparkle", animSparkle},
|
||||||
|
{"gradient_sweep", animGradientSweep},
|
||||||
|
};
|
||||||
|
const uint8_t ANIM_REGISTRY_SIZE = sizeof(ANIM_REGISTRY) / sizeof(ANIM_REGISTRY[0]);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Global state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
ZoneState zoneSchrank = {};
|
||||||
|
ZoneState zoneWand = {};
|
||||||
|
float g_brightness = BRIGHTNESS_CAP;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// W-channel auto-derive (per D-02): warm-white approximation from RGB.
|
||||||
|
// Returns min(r, g, b) / 2 so white only appears for near-neutral colors.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
uint8_t autoWhite(uint8_t r, uint8_t g, uint8_t b) {
|
||||||
|
return std::min(r, std::min(g, b)) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lookup helper: find AnimTickFn by name string in ANIM_REGISTRY.
|
||||||
|
// Returns nullptr if not found and logs unknown name to Serial.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static AnimTickFn findAnimation(const char* name) {
|
||||||
|
for (uint8_t i = 0; i < ANIM_REGISTRY_SIZE; i++) {
|
||||||
|
if (strcmp(ANIM_REGISTRY[i].name, name) == 0) {
|
||||||
|
return ANIM_REGISTRY[i].fn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Serial.printf("[anim] Unknown animation: '%s' — ignoring (D-07)\n", name);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Apply a command to zone(s).
|
||||||
|
// Handles ANIMATION, STOP, BRIGHTNESS for SCHRANK, WAND, or ALL zones.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static void applyCommandToZone(ZoneState& zone, const LedCommand& cmd) {
|
||||||
|
switch (cmd.type) {
|
||||||
|
case CmdType::STOP:
|
||||||
|
zone.active = false;
|
||||||
|
zone.currentAnim = nullptr;
|
||||||
|
zone.animName[0] = '\0';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CmdType::ANIMATION: {
|
||||||
|
AnimTickFn fn = findAnimation(cmd.animName);
|
||||||
|
if (fn) {
|
||||||
|
zone.currentAnim = fn;
|
||||||
|
zone.params = cmd.params;
|
||||||
|
zone.tick = 0;
|
||||||
|
zone.active = true;
|
||||||
|
strncpy(zone.animName, cmd.animName, sizeof(zone.animName) - 1);
|
||||||
|
zone.animName[sizeof(zone.animName) - 1] = '\0';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CmdType::BRIGHTNESS:
|
||||||
|
// brightness is 0–100 (percent); scale to [0, BRIGHTNESS_MAX]
|
||||||
|
g_brightness = std::max(0.0f,
|
||||||
|
std::min((cmd.brightness / 100.0f) * BRIGHTNESS_MAX, BRIGHTNESS_MAX));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// STATUS and UNKNOWN handled elsewhere or ignored
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void applyCommand(const LedCommand& cmd) {
|
||||||
|
switch (cmd.zone) {
|
||||||
|
case Zone::SCHRANK:
|
||||||
|
applyCommandToZone(zoneSchrank, cmd);
|
||||||
|
break;
|
||||||
|
case Zone::WAND:
|
||||||
|
applyCommandToZone(zoneWand, cmd);
|
||||||
|
break;
|
||||||
|
case Zone::ALL:
|
||||||
|
applyCommandToZone(zoneSchrank, cmd);
|
||||||
|
applyCommandToZone(zoneWand, cmd);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Serial.println("[anim] Unknown zone — ignoring command");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main animation task — runs at PRIORITY_ANIM_TICK, 50fps via vTaskDelayUntil.
|
||||||
|
// Drains commandQueue non-blocking, ticks active animations, shows strips.
|
||||||
|
// Uses vTaskDelayUntil (not vTaskDelay) for drift-free 50fps (per D-04).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static void animTickTask(void* pvParams) {
|
||||||
|
TickType_t lastWake = xTaskGetTickCount();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Drain commandQueue — process ALL pending commands before this frame
|
||||||
|
LedCommand cmd;
|
||||||
|
while (xQueueReceive(commandQueue, &cmd, 0) == pdTRUE) {
|
||||||
|
applyCommand(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tick Schrank (WS2801, RGB, 160 LEDs, isRGBW=false)
|
||||||
|
if (zoneSchrank.active && zoneSchrank.currentAnim) {
|
||||||
|
zoneSchrank.currentAnim(zoneSchrank.tick++, zoneSchrank.params,
|
||||||
|
LED_COUNT_SCHRANK, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tick Wand (SK6812, RGBW, 300 LEDs, isRGBW=true)
|
||||||
|
if (zoneWand.active && zoneWand.currentAnim) {
|
||||||
|
zoneWand.currentAnim(zoneWand.tick++, zoneWand.params,
|
||||||
|
LED_COUNT_WAND, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
showBothStrips();
|
||||||
|
|
||||||
|
// Drift-free delay: vTaskDelayUntil keeps exact 50fps regardless of tick duration
|
||||||
|
vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(1000 / ANIM_FPS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Initialize animation engine.
|
||||||
|
// Call from setup() after initStrips(), before WiFi connect (D-11).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void initAnimationEngine() {
|
||||||
|
// Default zone state: inactive, no animation
|
||||||
|
zoneSchrank = {};
|
||||||
|
zoneSchrank.active = false;
|
||||||
|
zoneSchrank.currentAnim = nullptr;
|
||||||
|
zoneSchrank.tick = 0;
|
||||||
|
|
||||||
|
zoneWand = {};
|
||||||
|
zoneWand.active = false;
|
||||||
|
zoneWand.currentAnim = nullptr;
|
||||||
|
zoneWand.tick = 0;
|
||||||
|
|
||||||
|
// Default global brightness: BRIGHTNESS_CAP (40%, per D-09)
|
||||||
|
g_brightness = BRIGHTNESS_CAP;
|
||||||
|
|
||||||
|
// Launch 50fps tick task
|
||||||
|
xTaskCreate(animTickTask, "anim", STACK_ANIM, NULL, PRIORITY_ANIM_TICK, NULL);
|
||||||
|
|
||||||
|
Serial.println("Animation engine: 8 animations registered, 50fps tick, brightness cap 40%");
|
||||||
|
|
||||||
|
// Startup breathing animation (D-11): visual confirmation firmware is alive.
|
||||||
|
// Set both zones to "breathe" at low brightness before WiFi connects.
|
||||||
|
g_brightness = 0.15f;
|
||||||
|
|
||||||
|
AnimParams breatheParams = {};
|
||||||
|
breatheParams.colorCount = 1;
|
||||||
|
breatheParams.colors[0][0] = 0; // R
|
||||||
|
breatheParams.colors[0][1] = 60; // G
|
||||||
|
breatheParams.colors[0][2] = 120; // B
|
||||||
|
breatheParams.colors[0][3] = 0; // W (auto-derived for Wand)
|
||||||
|
breatheParams.speed = 0.3f;
|
||||||
|
breatheParams.intensity = 1.0f;
|
||||||
|
breatheParams.direction = 0;
|
||||||
|
|
||||||
|
LedCommand startCmd = {};
|
||||||
|
startCmd.type = CmdType::ANIMATION;
|
||||||
|
startCmd.zone = Zone::ALL;
|
||||||
|
strncpy(startCmd.animName, "breathe", sizeof(startCmd.animName) - 1);
|
||||||
|
startCmd.params = breatheParams;
|
||||||
|
|
||||||
|
// Apply directly (commandQueue may not exist yet at init time)
|
||||||
|
applyCommandToZone(zoneSchrank, startCmd);
|
||||||
|
applyCommandToZone(zoneWand, startCmd);
|
||||||
|
|
||||||
|
Serial.println("Animation engine: startup breathing active (dim blue-white)");
|
||||||
|
}
|
||||||
35
firmware/src/animation_engine.h
Normal file
35
firmware/src/animation_engine.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "animations/animation_base.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Per-zone state machine
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct ZoneState {
|
||||||
|
AnimTickFn currentAnim; // nullptr = stopped
|
||||||
|
AnimParams params; // current animation parameters
|
||||||
|
uint32_t tick; // frame counter for this zone (reset on animation change)
|
||||||
|
bool active; // false = no animation running
|
||||||
|
char animName[32]; // current animation name (for status responses)
|
||||||
|
};
|
||||||
|
|
||||||
|
extern ZoneState zoneSchrank;
|
||||||
|
extern ZoneState zoneWand;
|
||||||
|
extern float g_brightness; // global brightness (0.0–BRIGHTNESS_MAX)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// W-channel auto-derive from RGB (per D-02).
|
||||||
|
// Used by animations when params.colors[i][3] == 0 on RGBW strips.
|
||||||
|
// Returns a warm-white approximation: min(r, g, b) / 2
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
uint8_t autoWhite(uint8_t r, uint8_t g, uint8_t b);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Initialize the animation engine:
|
||||||
|
// - Sets default ZoneState (active=false, tick=0)
|
||||||
|
// - Sets g_brightness = BRIGHTNESS_CAP (40%)
|
||||||
|
// - Launches 50fps animTickTask FreeRTOS task
|
||||||
|
// - Starts breathing animation on both zones as startup indicator (D-11)
|
||||||
|
// Call from setup() after initStrips() and before WiFi connect.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void initAnimationEngine();
|
||||||
40
firmware/src/animations/animation_base.h
Normal file
40
firmware/src/animations/animation_base.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "protocol.h" // AnimParams
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Animation tick function signature.
|
||||||
|
// tick: monotonically increasing frame counter (wraps at UINT32_MAX)
|
||||||
|
// params: color palette + speed/intensity/direction
|
||||||
|
// ledCount: number of LEDs in this strip
|
||||||
|
// isRGBW: true for SK6812 Wand (4 bytes/LED), false for WS2801 Schrank (3 bytes/LED)
|
||||||
|
//
|
||||||
|
// Animations write pixel colors directly to the global strip objects (stripWand /
|
||||||
|
// stripSchrank). They must NOT call Show() — the engine calls showBothStrips().
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
typedef void (*AnimTickFn)(uint32_t tick, const AnimParams& params,
|
||||||
|
uint16_t ledCount, bool isRGBW);
|
||||||
|
|
||||||
|
struct AnimEntry {
|
||||||
|
const char* name; // exact name string used in JSON commands (e.g. "chase")
|
||||||
|
AnimTickFn fn;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Forward-declare all 8 animation functions (implemented in animations/*.cpp)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void animChase (uint32_t tick, const AnimParams& p, uint16_t ledCount, bool isRGBW);
|
||||||
|
void animPulse (uint32_t tick, const AnimParams& p, uint16_t ledCount, bool isRGBW);
|
||||||
|
void animRainbow (uint32_t tick, const AnimParams& p, uint16_t ledCount, bool isRGBW);
|
||||||
|
void animStrobe (uint32_t tick, const AnimParams& p, uint16_t ledCount, bool isRGBW);
|
||||||
|
void animColorWash (uint32_t tick, const AnimParams& p, uint16_t ledCount, bool isRGBW);
|
||||||
|
void animBreathe (uint32_t tick, const AnimParams& p, uint16_t ledCount, bool isRGBW);
|
||||||
|
void animSparkle (uint32_t tick, const AnimParams& p, uint16_t ledCount, bool isRGBW);
|
||||||
|
void animGradientSweep(uint32_t tick, const AnimParams& p, uint16_t ledCount, bool isRGBW);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Animation registry — index by name at command dispatch time.
|
||||||
|
// Defined in animation_engine.cpp.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
extern const AnimEntry ANIM_REGISTRY[];
|
||||||
|
extern const uint8_t ANIM_REGISTRY_SIZE;
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "led_driver.h"
|
#include "led_driver.h"
|
||||||
#include "credentials.h"
|
#include "credentials.h"
|
||||||
|
#include "wifi_server.h"
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Color constants for startup indicators
|
// Color constants for startup indicators
|
||||||
@@ -72,11 +73,14 @@ void setup() {
|
|||||||
esp_wifi_set_ps(WIFI_PS_NONE);
|
esp_wifi_set_ps(WIFI_PS_NONE);
|
||||||
Serial.println("WIFI_PS_NONE set — RMT coexistence mode active");
|
Serial.println("WIFI_PS_NONE set — RMT coexistence mode active");
|
||||||
|
|
||||||
// 6. Print connection info
|
// 6. Start UDP server (must be after WiFi connect and WIFI_PS_NONE)
|
||||||
|
startUdpServer();
|
||||||
|
|
||||||
|
// 7. Print connection info
|
||||||
Serial.print("WiFi connected: ");
|
Serial.print("WiFi connected: ");
|
||||||
Serial.println(WiFi.localIP().toString());
|
Serial.println(WiFi.localIP().toString());
|
||||||
|
|
||||||
// 7. WiFi connected indicator: dim white
|
// 8. WiFi connected indicator: dim white
|
||||||
setAllWand(COLOR_WIFI_READY);
|
setAllWand(COLOR_WIFI_READY);
|
||||||
showBothStrips();
|
showBothStrips();
|
||||||
|
|
||||||
|
|||||||
109
firmware/src/wifi_server.cpp
Normal file
109
firmware/src/wifi_server.cpp
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#include "wifi_server.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "protocol.h"
|
||||||
|
#include <AsyncUDP.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/queue.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Constants
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static const size_t MAX_UDP_PACKET = 512; // max command payload bytes
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct RawPacket {
|
||||||
|
uint8_t data[512];
|
||||||
|
size_t len;
|
||||||
|
IPAddress ip;
|
||||||
|
uint16_t port;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Module state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static AsyncUDP udp;
|
||||||
|
static QueueHandle_t rawQueue; // raw bytes from callback → parse task
|
||||||
|
|
||||||
|
// External: animation engine reads from commandQueue (Plan 03)
|
||||||
|
QueueHandle_t commandQueue;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// parseTask — drains rawQueue, deserializes JSON, dispatches to commandQueue
|
||||||
|
// Runs in its own FreeRTOS task so JSON heap allocation never happens in the
|
||||||
|
// AsyncUDP callback (which runs in WiFi task context — watchdog risk).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static void parseTask(void* pvParams) {
|
||||||
|
RawPacket raw;
|
||||||
|
while (true) {
|
||||||
|
if (xQueueReceive(rawQueue, &raw, portMAX_DELAY)) {
|
||||||
|
LedCommand cmd;
|
||||||
|
cmd.senderIP = raw.ip;
|
||||||
|
cmd.senderPort = raw.port;
|
||||||
|
|
||||||
|
if (parseCommand(raw.data, raw.len, cmd)) {
|
||||||
|
// STATUS is handled immediately here — no animation engine needed
|
||||||
|
if (cmd.type == CmdType::STATUS) {
|
||||||
|
String resp = buildStatusResponse("unknown", "unknown", 40);
|
||||||
|
udp.writeTo(
|
||||||
|
(const uint8_t*)resp.c_str(),
|
||||||
|
resp.length(),
|
||||||
|
cmd.senderIP,
|
||||||
|
cmd.senderPort
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch to animation engine via commandQueue
|
||||||
|
// Use 10ms timeout to avoid stalling if queue is full
|
||||||
|
xQueueSend(commandQueue, &cmd, pdMS_TO_TICKS(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// startUdpServer — call after WiFi connected + WIFI_PS_NONE set
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void startUdpServer() {
|
||||||
|
// Create raw packet queue (ISR-safe, filled by AsyncUDP callback)
|
||||||
|
rawQueue = xQueueCreate(4, sizeof(RawPacket));
|
||||||
|
if (rawQueue == NULL) {
|
||||||
|
Serial.println("ERROR: Failed to create rawQueue");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create command queue (consumed by animation engine in Plan 03)
|
||||||
|
commandQueue = xQueueCreate(8, sizeof(LedCommand));
|
||||||
|
if (commandQueue == NULL) {
|
||||||
|
Serial.println("ERROR: Failed to create commandQueue");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register AsyncUDP packet handler
|
||||||
|
// IMPORTANT: Never parse JSON here — callback runs in WiFi task context.
|
||||||
|
// Heap allocation in callback causes watchdog resets (see RESEARCH.md Pattern 2).
|
||||||
|
if (udp.listen(UDP_PORT)) {
|
||||||
|
udp.onPacket([](AsyncUDPPacket packet) {
|
||||||
|
RawPacket raw;
|
||||||
|
raw.len = min(packet.length(), (size_t)MAX_UDP_PACKET);
|
||||||
|
memcpy(raw.data, packet.data(), raw.len);
|
||||||
|
raw.ip = packet.remoteIP();
|
||||||
|
raw.port = packet.remotePort();
|
||||||
|
// ISR-safe queue send (callback is in WiFi task context, not true ISR,
|
||||||
|
// but xQueueSendFromISR is safe to use here per ESP-IDF docs)
|
||||||
|
BaseType_t woken = pdFALSE;
|
||||||
|
xQueueSendFromISR(rawQueue, &raw, &woken);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Serial.println("ERROR: AsyncUDP listen failed on port " + String(UDP_PORT));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch parse task
|
||||||
|
xTaskCreate(parseTask, "parse", STACK_WIFI, NULL, PRIORITY_WIFI_RECV, NULL);
|
||||||
|
|
||||||
|
Serial.println("UDP server listening on port " + String(UDP_PORT));
|
||||||
|
}
|
||||||
18
firmware/src/wifi_server.h
Normal file
18
firmware/src/wifi_server.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/queue.h>
|
||||||
|
#include "protocol.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// commandQueue — parsed LedCommand structs dispatched to animation engine
|
||||||
|
// Declared here (extern), defined in wifi_server.cpp.
|
||||||
|
// Plan 03 (animation engine) consumes from this queue.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
extern QueueHandle_t commandQueue;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// startUdpServer — create queues, register AsyncUDP callback, launch parse task
|
||||||
|
// Must be called AFTER WiFi is connected and esp_wifi_set_ps(WIFI_PS_NONE) is set.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void startUdpServer();
|
||||||
Reference in New Issue
Block a user