← All source files

V18 / 8d896b8a86e5

LilithLight.ino

Examples/Arduino/LilithLight/LilithLight.ino

Unchanged from the preceding version
INODirectivesKeywordsTypesFunctionsStringsValuesComments
// 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(); }