Files
immich-frame/src/web_server.cpp

339 lines
12 KiB
C++
Raw Normal View History

#include "web_server.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <Update.h>
#include <WiFi.h>
#include "immich_client.h"
#include "power_manager.h"
#include "wifi_manager.h"
#include "config.h"
void AppWebServer::begin(SettingsManager& settings, ImmichClient& immich,
PowerManager& power, WiFiManager& wifi) {
_settings = &settings;
_immich = &immich;
_power = &power;
_wifi = &wifi;
if (!LittleFS.begin(true)) {
Serial.println("[web] LittleFS mount failed");
}
if (_wifi->isAPMode()) {
setupCaptivePortal();
} else {
setupAPIRoutes();
setupStaticFiles();
setupOTA();
}
_server.begin();
Serial.printf("[web] Server started (mode: %s)\n",
_wifi->isAPMode() ? "AP/captive" : "LAN");
}
void AppWebServer::setActionCallback(std::function<void(const String& action)> cb) {
_actionCb = cb;
}
bool AppWebServer::isOTAInProgress() {
return _otaInProgress;
}
void AppWebServer::setupCaptivePortal() {
// Serve setup page for all requests (captive portal behavior)
_server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
request->send(LittleFS, "/setup.html", "text/html");
});
// Kick off initial async WiFi scan
WiFi.scanNetworks(true);
// Handle WiFi scan (non-blocking — returns cached results or "scanning" status)
_server.on("/api/wifi/scan", HTTP_GET, [](AsyncWebServerRequest* request) {
int n = WiFi.scanComplete();
if (n == WIFI_SCAN_RUNNING) {
// Scan in progress — tell client to retry
request->send(200, "application/json", "{\"scanning\":true}");
return;
}
if (n == WIFI_SCAN_FAILED) {
// No scan started or failed — kick one off
WiFi.scanNetworks(true);
request->send(200, "application/json", "{\"scanning\":true}");
return;
}
// Scan complete — return results
JsonDocument doc;
JsonArray arr = doc.to<JsonArray>();
for (int i = 0; i < n; i++) {
JsonObject net = arr.add<JsonObject>();
net["ssid"] = WiFi.SSID(i);
net["rssi"] = WiFi.RSSI(i);
net["secure"] = WiFi.encryptionType(i) != WIFI_AUTH_OPEN;
}
String response;
serializeJson(doc, response);
// Start a fresh scan for next request
WiFi.scanDelete();
WiFi.scanNetworks(true);
request->send(200, "application/json", response);
});
// Handle setup submission
_server.on("/api/setup", HTTP_POST, [](AsyncWebServerRequest* request) {
request->send(200);
}, nullptr, [this](AsyncWebServerRequest* request, uint8_t* data, size_t len,
size_t index, size_t total) {
if (index + len == total) {
handlePostSetup(request, data, len);
}
});
// Captive portal redirect for all other paths
_server.onNotFound([](AsyncWebServerRequest* request) {
request->redirect("/");
});
}
void AppWebServer::setupAPIRoutes() {
_server.on("/api/status", HTTP_GET,
[this](AsyncWebServerRequest* req) { handleGetStatus(req); });
_server.on("/api/albums", HTTP_GET,
[this](AsyncWebServerRequest* req) { handleGetAlbums(req); });
_server.on("/api/albums/select", HTTP_POST,
[](AsyncWebServerRequest* req) { req->send(200); },
nullptr,
[this](AsyncWebServerRequest* req, uint8_t* data, size_t len,
size_t index, size_t total) {
if (index + len == total) handlePostAlbumsSelect(req, data, len);
});
_server.on("/api/settings", HTTP_GET,
[this](AsyncWebServerRequest* req) { handleGetSettings(req); });
_server.on("/api/settings", HTTP_POST,
[](AsyncWebServerRequest* req) { req->send(200); },
nullptr,
[this](AsyncWebServerRequest* req, uint8_t* data, size_t len,
size_t index, size_t total) {
if (index + len == total) handlePostSettings(req, data, len);
});
// Action endpoints
_server.on("/api/action/next", HTTP_POST,
[this](AsyncWebServerRequest* req) {
if (_actionCb) _actionCb("next");
req->send(200, "application/json", "{\"ok\":true}");
});
_server.on("/api/action/random", HTTP_POST,
[this](AsyncWebServerRequest* req) {
if (_actionCb) _actionCb("random");
req->send(200, "application/json", "{\"ok\":true}");
});
_server.on("/api/action/pause", HTTP_POST,
[this](AsyncWebServerRequest* req) {
if (_actionCb) _actionCb("pause");
req->send(200, "application/json", "{\"ok\":true}");
});
_server.on("/api/action/play", HTTP_POST,
[this](AsyncWebServerRequest* req) {
if (_actionCb) _actionCb("play");
req->send(200, "application/json", "{\"ok\":true}");
});
_server.on("/api/action/sleep", HTTP_POST,
[this](AsyncWebServerRequest* req) {
req->send(200, "application/json", "{\"ok\":true}");
delay(100);
_power->enterDeepSleep();
});
// Test Immich connectivity
_server.on("/api/test-connection", HTTP_POST,
[this](AsyncWebServerRequest* req) {
Settings s = _settings->get();
Serial.printf("[web] Testing connection to: %s (key: %s...)\n",
s.immich_url.c_str(),
s.immich_key.substring(0, 8).c_str());
// Reinitialize immich client with current settings
_immich->begin(s.immich_url, s.immich_key);
auto albums = _immich->fetchAlbums();
JsonDocument doc;
if (!albums.empty()) {
doc["ok"] = true;
doc["msg"] = String("Connected! Found ") + String(albums.size()) + " albums.";
} else {
doc["ok"] = false;
doc["msg"] = "Failed to connect. Check URL and API key.";
}
String response;
serializeJson(doc, response);
req->send(200, "application/json", response);
});
_server.on("/api/action/reboot", HTTP_POST,
[](AsyncWebServerRequest* req) {
req->send(200, "application/json", "{\"ok\":true,\"msg\":\"Rebooting...\"}");
delay(500);
ESP.restart();
});
}
void AppWebServer::setupStaticFiles() {
_server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
}
void AppWebServer::setupOTA() {
_server.on("/api/firmware", HTTP_POST,
[this](AsyncWebServerRequest* request) {
_otaInProgress = false;
bool success = !Update.hasError();
request->send(200, "application/json",
success ? "{\"ok\":true,\"msg\":\"Rebooting...\"}"
: "{\"ok\":false,\"msg\":\"Update failed\"}");
if (success) {
delay(500);
ESP.restart();
}
},
[this](AsyncWebServerRequest* request, const String& filename,
size_t index, uint8_t* data, size_t len, bool final) {
if (index == 0) {
_otaInProgress = true;
Serial.printf("[web] OTA start: %s\n", filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) {
Serial.println("[web] OTA begin failed");
}
}
if (Update.isRunning()) {
Update.write(data, len);
}
if (final) {
if (Update.end(true)) {
Serial.printf("[web] OTA complete: %u bytes\n", index + len);
} else {
Serial.println("[web] OTA finalize failed");
Update.printError(Serial);
}
}
});
}
void AppWebServer::handleGetStatus(AsyncWebServerRequest* request) {
JsonDocument doc;
doc["battery"] = _power->getBatteryPercent();
doc["charging"] = _power->isCharging();
doc["wifi_rssi"] = _wifi->getRSSI();
doc["ip"] = _wifi->getIP();
doc["uptime"] = millis() / 1000;
doc["free_heap"] = ESP.getFreeHeap();
doc["free_psram"] = ESP.getFreePsram();
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
}
void AppWebServer::handleGetAlbums(AsyncWebServerRequest* request) {
auto albums = _immich->fetchAlbums();
JsonDocument doc;
JsonArray arr = doc.to<JsonArray>();
for (auto& album : albums) {
JsonObject obj = arr.add<JsonObject>();
obj["id"] = album.id;
obj["title"] = album.title;
obj["assetCount"] = album.assetCount;
}
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
}
void AppWebServer::handlePostAlbumsSelect(AsyncWebServerRequest* request,
uint8_t* data, size_t len) {
String body = String((char*)data).substring(0, len);
JsonDocument doc;
if (deserializeJson(doc, body)) {
request->send(400, "application/json", "{\"error\":\"Invalid JSON\"}");
return;
}
String albumsJson;
serializeJson(doc["album_ids"], albumsJson);
_settings->saveField("albums_json", albumsJson.c_str());
request->send(200, "application/json", "{\"ok\":true}");
}
void AppWebServer::handleGetSettings(AsyncWebServerRequest* request) {
Settings s = _settings->get();
JsonDocument doc;
doc["interval_min"] = s.interval_min;
doc["cycle_mode"] = static_cast<uint8_t>(s.cycle_mode);
doc["img_quality"] = static_cast<uint8_t>(s.img_quality);
doc["meta_flags"] = s.meta_flags;
doc["meta_pos"] = static_cast<uint8_t>(s.meta_pos);
doc["led_brightness"] = s.led_brightness;
doc["immich_url"] = s.immich_url;
doc["immich_key"] = s.immich_key;
doc["albums_json"] = s.albums_json;
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
}
void AppWebServer::handlePostSettings(AsyncWebServerRequest* request,
uint8_t* data, size_t len) {
String body = String((char*)data).substring(0, len);
JsonDocument doc;
if (deserializeJson(doc, body)) {
request->send(400, "application/json", "{\"error\":\"Invalid JSON\"}");
return;
}
Settings s = _settings->get();
if (!doc["interval_min"].isNull()) s.interval_min = doc["interval_min"];
if (!doc["cycle_mode"].isNull()) s.cycle_mode = static_cast<CycleMode>((uint8_t)doc["cycle_mode"]);
if (!doc["img_quality"].isNull()) s.img_quality = static_cast<ImageQuality>((uint8_t)doc["img_quality"]);
if (!doc["meta_flags"].isNull()) s.meta_flags = doc["meta_flags"];
if (!doc["meta_pos"].isNull()) s.meta_pos = static_cast<MetaPosition>((uint8_t)doc["meta_pos"]);
if (!doc["led_brightness"].isNull()) s.led_brightness = doc["led_brightness"];
if (!doc["immich_url"].isNull()) s.immich_url = doc["immich_url"].as<String>();
if (!doc["immich_key"].isNull()) s.immich_key = doc["immich_key"].as<String>();
_settings->save(s);
request->send(200, "application/json", "{\"ok\":true}");
}
void AppWebServer::handlePostSetup(AsyncWebServerRequest* request,
uint8_t* data, size_t len) {
String body = String((char*)data).substring(0, len);
JsonDocument doc;
if (deserializeJson(doc, body)) {
request->send(400, "application/json", "{\"error\":\"Invalid JSON\"}");
return;
}
Settings s = _settings->get();
s.wifi_ssid = doc["wifi_ssid"].as<String>();
s.wifi_pass = doc["wifi_pass"].as<String>();
s.immich_url = doc["immich_url"] | DEFAULT_IMMICH_URL;
s.immich_key = doc["immich_key"].as<String>();
_settings->save(s);
request->send(200, "application/json", "{\"ok\":true,\"msg\":\"Rebooting...\"}");
delay(1000);
ESP.restart();
}