Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
#include "WebApi.h"
#include <WiFi.h>
#include <Preferences.h>
#include <Logger.h>
#include "WebAssets.h"
#include "diag/ScreenshotRoute.h"
#include "led/LEDManager.h"
#include "ui/UIManager.h"
#include "ui/ui.h"
#include "MqttManager.h"
// Defined in main.cpp — set true to schedule a deferred restart from a handler
// (the loop performs the actual ESP.restart() after a short delay).
extern bool g_restartRequested;
// Minimal JSON string escaper for user-entered values (broker host/user/password).
static String jsonEscape(const String& in) {
String out;
out.reserve(in.length() + 4);
for (size_t i = 0; i < in.length(); i++) {
char c = in[i];
if (c == '"' || c == '\\') {
out += '\\';
out += c;
} else if (c == '\n') {
out += "\\n";
} else if (c == '\r') {
out += "\\r";
} else if (c == '\t') {
out += "\\t";
} else {
out += c;
}
}
return out;
}
namespace {
// Streams one embedded asset. AsyncProgmemResponse keeps only the pointer, so
// nothing is copied out of flash into RAM.
void sendAsset(AsyncWebServerRequest* request, const WebAssets::Asset* asset) {
if (!asset) {
request->send(404, "text/plain", "Not found");
return;
}
AsyncWebServerResponse* response =
request->beginResponse(200, asset->contentType, asset->data, asset->len);
if (asset->gzip) {
response->addHeader("Content-Encoding", "gzip");
}
request->send(response);
}
} // namespace
void WebApi::registerRoutes(AsyncWebServer* server) {
Logger.debug("=== WebApi: Setting up routes ===");
// NOTE: WiFi setup routes (/setup, /factory-reset, /get-networks, /save-wifi)
// are handled by the WiFiSetupManager library, not here.
server->on("/test", HTTP_GET, [](AsyncWebServerRequest* request) {
request->send(200, "text/plain", "Server is working! IP: " + WiFi.localIP().toString());
});
// LED Configuration page — served by the SPA. We send index.html and
// let the client-side router show the LedConfig view based on
// window.location.pathname.
server->on("/led-config", HTTP_GET, [](AsyncWebServerRequest* request) {
sendAsset(request, WebAssets::find("/index.html"));
});
server->on("/get-led-config", HTTP_GET, [](AsyncWebServerRequest* request) {
Preferences ledPrefs;
ledPrefs.begin("led-config", true); // Read-only
bool hasLedSettings = ledPrefs.isKey("num_strips");
int numStrips = ledPrefs.getInt("num_strips", 0);
int ledsPerStrip = ledPrefs.getInt("leds_per_strip", 0);
int totalLeds = numStrips * ledsPerStrip;
ledPrefs.end();
String json = "{";
json += "\"hasLedSettings\":" + String(hasLedSettings ? "true" : "false") + ",";
json += "\"numStrips\":" + String(numStrips) + ",";
json += "\"ledsPerStrip\":" + String(ledsPerStrip) + ",";
json += "\"totalLeds\":" + String(totalLeds);
json += "}";
request->send(200, "application/json", json);
});
server->on("/save-led-config", HTTP_POST, [](AsyncWebServerRequest* request) {
int numStrips = 0;
int ledsPerStrip = 0;
if (request->hasParam("num_strips", true)) {
numStrips = request->getParam("num_strips", true)->value().toInt();
}
if (request->hasParam("leds_per_strip", true)) {
ledsPerStrip = request->getParam("leds_per_strip", true)->value().toInt();
}
if (numStrips > 0 && ledsPerStrip > 0) {
Preferences ledPrefs;
ledPrefs.begin("led-config", false);
ledPrefs.putInt("num_strips", numStrips);
ledPrefs.putInt("leds_per_strip", ledsPerStrip);
ledPrefs.end();
Logger.info("Saved LED config: %d strips, %d LEDs per strip", numStrips, ledsPerStrip);
request->send(200, "application/json", "{\"ok\":true}");
// Schedule restart — the web client shows the "Restarting..." view.
g_restartRequested = true;
} else {
request->send(400, "application/json", "{\"ok\":false,\"error\":\"Invalid LED configuration\"}");
}
});
// Settings: idle screensaver (enable + idle timeout). Applied live, no restart.
server->on("/get-settings", HTTP_GET, [](AsyncWebServerRequest* request) {
bool enabled = g_uiManager ? g_uiManager->isScreensaverEnabled() : true;
uint32_t idleSec = g_uiManager ? g_uiManager->getScreensaverIdleMs() / 1000 : 30;
// MQTT / Home Assistant broker config (defaults when the manager isn't up,
// e.g. captive-portal setup mode).
bool mqttEnabled = g_mqttManager ? g_mqttManager->isEnabled() : false;
String mqttHost = g_mqttManager ? g_mqttManager->host() : String("homeassistant.local");
uint16_t mqttPort = g_mqttManager ? g_mqttManager->port() : 1883;
String mqttUser = g_mqttManager ? g_mqttManager->user() : String("");
String mqttPrefix = g_mqttManager ? g_mqttManager->prefix() : String("rackglow");
// Password is write-only: never sent back. Expose only whether one is set so
// the UI can show a "leave blank to keep" affordance.
bool mqttHasPass = g_mqttManager ? (g_mqttManager->pass().length() > 0) : false;
// Scene list comes from the device so the picker can't drift out of step
// with what is actually registered in ScenePlayer.
String scenes = "[";
const int sceneCount = g_uiManager ? g_uiManager->screensaverSceneCount() : 0;
for (int i = 0; i < sceneCount; i++) {
if (i) scenes += ",";
scenes += "\"" + jsonEscape(g_uiManager->screensaverSceneName(i)) + "\"";
}
scenes += "]";
const int sceneSel = g_uiManager ? g_uiManager->getScreensaverScene() : 0;
String json = "{";
json += "\"screensaverEnabled\":" + String(enabled ? "true" : "false") + ",";
json += "\"screensaverTimeoutSec\":" + String(idleSec) + ",";
json += "\"screensaverScene\":" + String(sceneSel) + ",";
json += "\"screensaverScenes\":" + scenes + ",";
json += "\"mqttEnabled\":" + String(mqttEnabled ? "true" : "false") + ",";
json += "\"mqttHost\":\"" + jsonEscape(mqttHost) + "\",";
json += "\"mqttPort\":" + String(mqttPort) + ",";
json += "\"mqttUser\":\"" + jsonEscape(mqttUser) + "\",";
json += "\"mqttHasPass\":" + String(mqttHasPass ? "true" : "false") + ",";
json += "\"mqttPrefix\":\"" + jsonEscape(mqttPrefix) + "\"";
json += "}";
request->send(200, "application/json", json);
});
server->on("/save-settings", HTTP_POST, [](AsyncWebServerRequest* request) {
if (!g_uiManager) {
request->send(503, "application/json", "{\"ok\":false,\"error\":\"UI not ready\"}");
return;
}
// Default to the current values so a partial form doesn't clobber them.
bool enabled = g_uiManager->isScreensaverEnabled();
uint32_t idleSec = g_uiManager->getScreensaverIdleMs() / 1000;
if (request->hasParam("screensaver_enabled", true)) {
String v = request->getParam("screensaver_enabled", true)->value();
enabled = (v == "true" || v == "1" || v == "on");
}
if (request->hasParam("screensaver_timeout_sec", true)) {
idleSec = request->getParam("screensaver_timeout_sec", true)->value().toInt();
}
g_uiManager->setScreensaverConfig(enabled, idleSec * 1000);
if (request->hasParam("screensaver_scene", true)) {
g_uiManager->setScreensaverScene(
request->getParam("screensaver_scene", true)->value().toInt());
}
// MQTT / Home Assistant broker config (applied live via a deferred restart
// inside MqttManager). Only touched when the form submits the mqtt fields.
if (g_mqttManager && request->hasParam("mqtt_enabled", true)) {
bool mqttEnabled = false;
String v = request->getParam("mqtt_enabled", true)->value();
mqttEnabled = (v == "true" || v == "1" || v == "on");
String host = g_mqttManager->host();
uint16_t port = g_mqttManager->port();
String user = g_mqttManager->user();
String pass = g_mqttManager->pass();
String prefix = g_mqttManager->prefix();
if (request->hasParam("mqtt_host", true)) host = request->getParam("mqtt_host", true)->value();
if (request->hasParam("mqtt_port", true)) port = request->getParam("mqtt_port", true)->value().toInt();
if (request->hasParam("mqtt_user", true)) user = request->getParam("mqtt_user", true)->value();
if (request->hasParam("mqtt_prefix", true)) prefix = request->getParam("mqtt_prefix", true)->value();
// Password is write-only: a blank field means "keep the stored one", so we
// only overwrite when the user actually typed a new password.
if (request->hasParam("mqtt_pass", true)) {
String p = request->getParam("mqtt_pass", true)->value();
if (p.length() > 0) pass = p;
}
g_mqttManager->applyConfig(mqttEnabled, host, port, user, pass, prefix);
}
request->send(200, "application/json", "{\"ok\":true}");
});
// Clear saved LED state (for testing first-boot experience)
server->on("/clear-led-state", HTTP_POST, [](AsyncWebServerRequest* request) {
if (g_ledManager) {
g_ledManager->clearSavedState();
}
Logger.info("LED state preferences cleared via web UI");
request->send(200, "application/json", "{\"ok\":true}");
});
// Legacy get-message endpoint (mostly unused)
server->on("/get-message", HTTP_GET, [](AsyncWebServerRequest* request) {
request->send(200, "application/json", "");
});
// Screenshot endpoint for help docs. Registered here so it lands before the
// static wildcard below.
ScreenshotRoute::registerRoutes(server);
// The Vue bundle is linked into the firmware (see include/WebAssets.h), so
// it is served from flash rather than a filesystem. One route per asset
// instead of a wildcard on purpose: an unmatched URL has to keep falling
// through to the WiFi library's onNotFound, which redirects to /setup and is
// what makes the captive portal work. WebApi's routes are registered in
// captive mode too.
for (size_t i = 0; i < WebAssets::count(); ++i) {
const WebAssets::Asset* asset = WebAssets::at(i);
server->on(asset->path, HTTP_GET, [asset](AsyncWebServerRequest* request) {
sendAsset(request, asset);
});
}
Logger.debug("WebApi: %u embedded assets served from flash",
(unsigned)WebAssets::count());
Logger.debug("=== WebApi: Routes setup complete ===");
}
96336ed42This file is pinned to the selected release. Download the original to open it in your design tool.