#include "protocol.h" #include // --------------------------------------------------------------------------- // parseCommand — deserialize raw JSON bytes into a LedCommand struct // // Protocol spec (D-05 through D-08): // - Every command must include {"v":1, ...} version field // - zone field is mandatory ("schrank", "wand", "all") — except STATUS // - Either "animation" field or "cmd" field must be present // - Unknown extra fields are silently ignored (ArduinoJson default) // - Errors: logged to Serial, return false, no UDP response sent // --------------------------------------------------------------------------- bool parseCommand(const uint8_t* data, size_t len, LedCommand& out) { JsonDocument doc; DeserializationError err = deserializeJson(doc, data, len); if (err) { Serial.print("JSON parse error: "); Serial.println(err.c_str()); return false; } // Check protocol version (D-08) if ((doc["v"] | 0) != 1) { Serial.println("Unknown protocol version — expected v:1"); return false; } // Determine zone (mandatory for all commands except STATUS) const char* zoneStr = doc["zone"] | ""; if (strcmp(zoneStr, "schrank") == 0) { out.zone = Zone::SCHRANK; } else if (strcmp(zoneStr, "wand") == 0) { out.zone = Zone::WAND; } else if (strcmp(zoneStr, "all") == 0) { out.zone = Zone::ALL; } else { // Zone may be absent for STATUS commands — handle below out.zone = Zone::UNKNOWN; } // Determine command type from "cmd" field or "animation" field (D-06) const char* cmdStr = doc["cmd"] | ""; if (strcmp(cmdStr, "stop") == 0) { // STOP command — zone required if (out.zone == Zone::UNKNOWN) { Serial.println("STOP command missing valid zone field"); return false; } out.type = CmdType::STOP; return true; } if (strcmp(cmdStr, "brightness") == 0) { // BRIGHTNESS command — zone required, value 0-60 (D-09) if (out.zone == Zone::UNKNOWN) { Serial.println("BRIGHTNESS command missing valid zone field"); return false; } out.type = CmdType::BRIGHTNESS; uint8_t val = doc["value"] | 40; // Clamp to firmware-enforced max (D-09: max 60) out.brightness = (val > 60) ? 60 : val; return true; } if (strcmp(cmdStr, "status") == 0) { // STATUS command — zone not required, sender IP/port already set by caller out.type = CmdType::STATUS; out.zone = Zone::ALL; // Status responds for all zones return true; } // Check for ANIMATION command (presence of "animation" field) const char* animStr = doc["animation"] | ""; if (strlen(animStr) > 0) { // Animation command — zone required (D-05) if (out.zone == Zone::UNKNOWN) { Serial.println("ANIMATION command missing valid zone field"); return false; } out.type = CmdType::ANIMATION; // Copy animation name (max 31 chars + null terminator) strncpy(out.animName, animStr, 31); out.animName[31] = '\0'; // Parse params object (D-01, D-04) JsonObject params = doc["params"]; // Speed, intensity, direction with sensible defaults out.params.speed = params["speed"] | 0.5f; out.params.intensity = params["intensity"] | 1.0f; out.params.direction = params["direction"] | (uint8_t)0; // Parse colors array — each element is [R, G, B, W] JsonArray colorsArr = params["colors"]; out.params.colorCount = 0; if (colorsArr) { for (JsonArray color : colorsArr) { if (out.params.colorCount >= 8) break; uint8_t i = out.params.colorCount; out.params.colors[i][0] = color[0] | (uint8_t)0; // R out.params.colors[i][1] = color[1] | (uint8_t)0; // G out.params.colors[i][2] = color[2] | (uint8_t)0; // B out.params.colors[i][3] = color[3] | (uint8_t)0; // W out.params.colorCount++; } } // Default to single warm white if no colors provided if (out.params.colorCount == 0) { out.params.colors[0][0] = 255; // R out.params.colors[0][1] = 255; // G out.params.colors[0][2] = 255; // B out.params.colors[0][3] = 255; // W out.params.colorCount = 1; } return true; } // Neither a known "cmd" nor an "animation" field Serial.println("No cmd or animation field in command — dropping packet"); return false; } // --------------------------------------------------------------------------- // buildStatusResponse — serialize current state as JSON for STATUS reply // // Returns: {"v":1,"status":"ok","wand":"chase","schrank":"breathe","brightness":40} // --------------------------------------------------------------------------- String buildStatusResponse(const char* wandAnim, const char* schrankAnim, uint8_t brightness) { JsonDocument doc; doc["v"] = 1; doc["status"] = "ok"; doc["wand"] = wandAnim; doc["schrank"] = schrankAnim; doc["brightness"] = brightness; String out; serializeJson(doc, out); return out; }