feat(01-02): protocol.h/.cpp — JSON parsing into LedCommand
- LedCommand, AnimParams, Zone, CmdType structs with all fields per D-05/D-06 - parseCommand(): validates v:1 version, routes schrank/wand/all zones, parses cmd/animation - RGBW colors array parsed from params.colors; defaults to single [255,255,255,255] - buildStatusResponse(): serializes current state to JSON for STATUS cmd reply - Errors logged to Serial, no UDP response on bad commands (per D-07)
This commit is contained in:
148
firmware/src/protocol.cpp
Normal file
148
firmware/src/protocol.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
#include "protocol.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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;
|
||||
}
|
||||
65
firmware/src/protocol.h
Normal file
65
firmware/src/protocol.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <Arduino.h>
|
||||
#include <IPAddress.h>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command types — determined by JSON "cmd" field or presence of "animation"
|
||||
// Per D-06: stop, brightness, status commands; animation command via "animation" field
|
||||
// ---------------------------------------------------------------------------
|
||||
enum class CmdType : uint8_t {
|
||||
ANIMATION = 0, // "animation" field present — run named animation
|
||||
STOP = 1, // {"cmd":"stop","zone":...}
|
||||
BRIGHTNESS = 2, // {"cmd":"brightness","zone":...,"value":0-100}
|
||||
STATUS = 3, // {"cmd":"status"} — respond with JSON
|
||||
UNKNOWN = 255
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Animation parameters — per D-01, D-04
|
||||
// ---------------------------------------------------------------------------
|
||||
struct AnimParams {
|
||||
uint8_t colors[8][4]; // up to 8 RGBW colors [R,G,B,W]
|
||||
uint8_t colorCount; // how many colors[] entries are valid
|
||||
float speed; // 0.0–1.0 (default 0.5)
|
||||
float intensity; // 0.0–1.0 (default 1.0)
|
||||
uint8_t direction; // 0=forward, 1=reverse
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Zone selector — per D-05: schrank, wand, all
|
||||
// ---------------------------------------------------------------------------
|
||||
enum class Zone : uint8_t {
|
||||
SCHRANK = 0,
|
||||
WAND = 1,
|
||||
ALL = 2,
|
||||
UNKNOWN = 255
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parsed command struct — what commandQueue carries to animation engine
|
||||
// ---------------------------------------------------------------------------
|
||||
struct LedCommand {
|
||||
CmdType type;
|
||||
Zone zone;
|
||||
char animName[32]; // animation name string (e.g. "chase"), null-terminated
|
||||
AnimParams params;
|
||||
uint8_t brightness; // 0–100, only valid for BRIGHTNESS cmd
|
||||
IPAddress senderIP; // for STATUS response
|
||||
uint16_t senderPort;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parses raw JSON bytes into LedCommand.
|
||||
// Returns false if malformed, version mismatch, or missing required fields.
|
||||
// Errors are logged to Serial; no UDP error response is sent (per D-07).
|
||||
// ---------------------------------------------------------------------------
|
||||
bool parseCommand(const uint8_t* data, size_t len, LedCommand& out);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Builds a JSON status response string (for STATUS cmd reply).
|
||||
// Returns JSON: {"v":1,"status":"ok","wand":"chase","schrank":"breathe","brightness":40}
|
||||
// Caller provides current animation names and brightness.
|
||||
// ---------------------------------------------------------------------------
|
||||
String buildStatusResponse(const char* wandAnim, const char* schrankAnim, uint8_t brightness);
|
||||
Reference in New Issue
Block a user