V16 / 6df58cd3ed86
LilithLight.ino
Examples/Arduino/LilithLight/LilithLight.ino
Unchanged from the preceding versionINODirectivesKeywordsTypesFunctionsStringsValuesComments
// ESP32 DevKit, Arduino-ESP32 core 3.x, ArduinoJson 7.x.
// Common-cathode low-voltage RGB LED, one current-limiting resistor per colour.
// For a strip, use suitable external MOSFET drivers and a separate supply.
#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>
const char* WIFI_SSID = "REPLACE_WIFI_SSID";
const char* WIFI_PASSWORD = "REPLACE_WIFI_PASSWORD";
const char* API_KEY = "REPLACE_WITH_A_RANDOM_32_CHARACTER_KEY";
const char* DEVICE_ID = "living_room_light";
constexpr uint8_t RED_PIN = 25, GREEN_PIN = 26, BLUE_PIN = 27;
WebServer server(80);
bool powerOn = true;
int brightness = 100;
String colour = "#FFFFFF";
String lastId, lastPayload, lastResponse;
bool authorized() {
if (server.header("X-Lilith-Key") == API_KEY) return true;
server.send(401, "application/json", "{\"status\":\"unauthorized\"}");
return false;
}
bool validHex(const String& value, size_t length) {
if (value.length() != length) return false;
for (size_t i = 0; i < length; ++i) if (!isxdigit(value[i])) return false;
return true;
}
void errorResponse(int code, const char* status) {
JsonDocument doc; doc["status"] = status;
String body; serializeJson(doc, body); server.send(code, "application/json", body);
}
bool applyLight(bool enabled, int percent, const String& hex) {
unsigned long rgb = strtoul(hex.substring(1).c_str(), nullptr, 16);
unsigned int level = enabled ? percent : 0;
bool r = ledcWrite(RED_PIN, ((rgb >> 16) & 255) * level / 100);
bool g = ledcWrite(GREEN_PIN, ((rgb >> 8) & 255) * level / 100);
bool b = ledcWrite(BLUE_PIN, (rgb & 255) * level / 100);
return r && g && b;
}
String stateResponse(const char* status, const String& requestId = "") {
JsonDocument doc; doc["device"] = DEVICE_ID; doc["status"] = status;
if (requestId.length()) doc["requestId"] = requestId;
doc["state"]["power"] = powerOn;
doc["state"]["brightness"] = brightness;
doc["state"]["color"] = colour;
String body; serializeJson(doc, body); return body;
}
void readLight() {
if (!authorized()) return;
server.send(200, "application/json", stateResponse("state"));
}
void writeLight() {
if (!authorized()) return;
String body = server.arg("plain");
if (body.length() > 512) { errorResponse(413, "too_large"); return; }
JsonDocument doc;
if (deserializeJson(doc, body) || !doc.is<JsonObject>() || doc.size() != 4 ||
!doc["requestId"].is<const char*>() || !doc["device"].is<const char*>() ||
!doc["action"].is<const char*>() || !doc.containsKey("value")) {
errorResponse(400, "invalid_request"); return;
}
String id = doc["requestId"].as<String>();
String action = doc["action"].as<String>();
if (!validHex(id, 32) || doc["device"].as<String>() != DEVICE_ID) { errorResponse(400, "wrong_identity"); return; }
String serializedValue; serializeJson(doc["value"], serializedValue);
String payload = action + "|" + serializedValue;
if (id == lastId) {
if (payload != lastPayload) { errorResponse(409, "request_id_conflict"); return; }
server.send(200, "application/json", lastResponse); return;
}
bool nextPower = powerOn; int nextBrightness = brightness; String nextColour = colour;
if (action == "power" && doc["value"].is<bool>()) nextPower = doc["value"].as<bool>();
else if (action == "brightness" && doc["value"].is<int>() && doc["value"].as<int>() >= 0 && doc["value"].as<int>() <= 100) nextBrightness = doc["value"].as<int>();
else if (action == "color" && doc["value"].is<const char*>()) {
nextColour = doc["value"].as<String>();
if (nextColour.length() != 7 || nextColour[0] != '#' || !validHex(nextColour.substring(1), 6)) { errorResponse(400, "invalid_color"); return; }
nextColour.toUpperCase();
} else { errorResponse(400, "unsupported_action_or_value"); return; }
if (!applyLight(nextPower, nextBrightness, nextColour)) { errorResponse(500, "pwm_failed"); return; }
powerOn = nextPower; brightness = nextBrightness; colour = nextColour;
lastId = id; lastPayload = payload; lastResponse = stateResponse("applied", id);
server.send(200, "application/json", lastResponse);
}
void setup() {
Serial.begin(115200);
if (String(API_KEY).startsWith("REPLACE_") || String(WIFI_SSID).startsWith("REPLACE_")) {
Serial.println("Set WiFi credentials and a private API key before use.");
while (true) delay(1000);
}
if (!ledcAttach(RED_PIN, 5000, 8) || !ledcAttach(GREEN_PIN, 5000, 8) || !ledcAttach(BLUE_PIN, 5000, 8)) {
Serial.println("PWM initialization failed."); while (true) delay(1000);
}
applyLight(powerOn, brightness, colour);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.print("Lilith light IP: "); Serial.println(WiFi.localIP());
const char* headers[] = {"X-Lilith-Key"}; server.collectHeaders(headers, 1);
server.on("/api/light", HTTP_GET, readLight);
server.on("/api/light", HTTP_POST, writeLight);
server.onNotFound([]() { errorResponse(404, "not_found"); });
server.begin();
}
void loop() { server.handleClient(); }