#include #include #include "app.h" #include "ESPAsyncWebServer.h" #include "ArduinoJson.h" #include "settings.h" #include "device.h" #include "playlist.h" #include "image.h" #include "display.h" #include "datetime.h" #include "faceWeather.h" #include "faceCalendar.h" #include "download.h" AsyncWebServer server(80); void setupSettingsGet(); void setupSettingsPost(); void setupWifiScan(); void setupWifiConnect(); void setupCurrentImage(); void setupApiFace(); void setupApiUpdate(); void setupOTA(); //flag to use from web update to update display bool updateDisplayRequired = false; //flag to use from web update to reboot the ESP bool shouldReboot = false; // bmp void write16(AsyncResponseStream &f, uint16_t v); void write32(AsyncResponseStream &f, uint32_t v); uint8_t filldata2[] = {0x0, 0x23, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0}; // bmp void setupApp() { Serial.println("setup configure"); // @see https://github.com/me-no-dev/ESPAsyncWebServer // @see https://arduinojson.org/v6/assistant/ // serve static files server .serveStatic("/", SPIFFS, "/dist/") .setDefaultFile("index.html") //.setCacheControl("max-age=600") ; server.serveStatic("/fs/", SPIFFS, "/"); setupSettingsGet(); setupSettingsPost(); setupWifiScan(); setupWifiConnect(); setupCurrentImage(); setupApiFace(); setupApiUpdate(); setupOTA(); server.onNotFound([](AsyncWebServerRequest *request) { request->send(404); }); // TODO response server.on("/stats", HTTP_GET, [](AsyncWebServerRequest *request) { AsyncResponseStream *response = request->beginResponseStream("application/json"); DynamicJsonDocument doc(668); // https://arduinojson.org/v6/assistant/ doc["wifi"]["rssi"] = WiFi.RSSI(); doc["wifi"]["ssid"] = WiFi.SSID(); doc["wifi"]["connected"] = WiFi.isConnected(); doc["wifi"]["ip"] = WiFi.localIP().toString(); doc["wifi"]["mac"] = WiFi.macAddress(); doc["wifi"]["channel"] = WiFi.channel(); doc["wifi"]["dns"] = WiFi.dnsIP().toString(); doc["wifi"]["gateway"] = WiFi.gatewayIP().toString(); doc["device"]["id"] = DeviceId; doc["device"]["heap"] = ESP.getFreeHeap(); doc["device"]["bootCycle"] = deviceGetBootCount(); doc["device"]["screen"]["width"] = 640; doc["device"]["screen"]["height"] = 384; doc["device"]["fs"]["total"] = SPIFFS.totalBytes(); doc["device"]["fs"]["used"] = SPIFFS.usedBytes(); doc["device"]["fs"]["free"] = SPIFFS.totalBytes() - SPIFFS.usedBytes(); doc["device"]["time"] = time(NULL); doc["playlist"]["current"] = PlaylistGetCurrentFace(); doc["playlist"]["remaining"] = PlaylistGetRemainingTimeMs() / 1000; JsonArray capability = doc.createNestedArray("capability"); capability.add("png"); capability.add("wbmp"); doc["cloud"]["sleep"] = deviceGetSleepInterval(); serializeJson(doc, *response); request->send(response); }); // CORS //DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); server.begin(); Serial.println("setup configure - done"); } void loopApp() { if (shouldReboot) { Serial.println("Rebooting..."); delay(100); ESP.restart(); } if (updateDisplayRequired) { Serial.println("loop app update display"); // stop playlist to show the new image PlaylistResetTimer(); updateDisplayRequired = false; display.nextPage(); } } void setupSettingsGet() { server.on("/api/settings", HTTP_GET, [](AsyncWebServerRequest *request) { AsyncResponseStream *response = request->beginResponseStream("application/json"); DynamicJsonDocument root(1024); root["system"]["country"] = NVS.getString("system.country"); root["system"]["language"] = NVS.getString("system.language"); root["system"]["timezone"] = NVS.getString("system.timezone"); root["system"]["utc"] = NVS.getInt("system.utc"); root["system"]["dst"] = NVS.getInt("system.dst"); // gmtOffset_sec // daylightOffset_sec //root["device"]["angle"] = NVS.getInt("device.angle"); root["device"]["theme"] = NVS.getString("device.theme"); root["device"]["name"] = NVS.getString("device.name"); root["playlist"]["timer"] = NVS.getInt("playlist.timer"); root["weather"]["api"] = NVS.getString("weather.api"); root["weather"]["location"] = NVS.getInt("weather.loc"); root["weather"]["lang"] = NVS.getString("weather.lang"); root["weather"]["unit"] = NVS.getString("weather.unit"); //root["cloud"]["mode"] = NVS.getString("cloud.mode"); //root["cloud"]["url"] = NVS.getString("cloud.url"); //root["cloud"]["token"] = NVS.getString("cloud.token"); serializeJson(root, *response); request->send(response); }); } void setupSettingsPost() { server.on( "/api/settings", HTTP_PUT, [](AsyncWebServerRequest *request) { /* nothing and dont remove it */ }, NULL, [](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { DynamicJsonDocument doc(2048); DeserializationError error = deserializeJson(doc, data); if (error) { Serial.print(F("deserializeJson() failed with code ")); Serial.println(error.c_str()); request->send(404, "text/plain", ""); } else { JsonVariant system = doc["system"]; if (!system.isNull()) { NVS.setString("system.country", system["country"]); NVS.setString("system.language", system["language"]); NVS.setString("system.timezone", system["timezone"]); NVS.setInt("system.utc", system["utc"].as()); NVS.setInt("system.dst", system["dst"].as()); } JsonVariant device = doc["device"]; if (!device.isNull()) { NVS.setInt("device.angle", device["angle"].as()); NVS.setString("device.theme", device["theme"]); NVS.setString("device.name", device["name"]); } JsonVariant playlist = doc["playlist"]; if (!playlist.isNull()) { NVS.setInt("playlist.timer", playlist["timer"].as()); } JsonVariant weather = doc["weather"]; if (!doc["weather"].isNull()) { NVS.setString("weather.api", weather["api"]); NVS.setInt("weather.loc", weather["location"].as()); NVS.setString("weather.lang", weather["lang"]); NVS.setString("weather.unit", weather["unit"]); } /* NVS.setString("cloud.mode", doc["cloud"]["mode"]); NVS.setString("cloud.url", doc["cloud"]["url"]); NVS.setString("cloud.token", doc["cloud"]["token"]); */ NVS.commit(); request->send(200, "application/ld+json; charset=utf-8", "{}"); } }); } /** * get current screen */ void setupCurrentImage() { server.on("/current-image", HTTP_GET, [](AsyncWebServerRequest *request) { uint8_t *bitmap = display.getBuffer(); int16_t w = display.width(); int16_t h = display.height(); uint16_t depth = 1; uint32_t rowSizeCode = (w + 8 - depth) * depth / 8; // BMP rows are padded (if needed) to 4-byte boundary uint32_t rowSizeBMP = (w * depth / 8 + 3) & ~3; uint32_t headerSize = 40; uint32_t imageOffset = 62; uint32_t fileSize = imageOffset + h * rowSizeBMP; AsyncResponseStream *response = request->beginResponseStream("image/bmp", w * h / 8 + imageOffset); write16(*response, 0x4D42); // BMP signature write32(*response, fileSize); // fileSize write32(*response, 0); // creator bytes write32(*response, imageOffset); // image offset write32(*response, headerSize); // Header size write32(*response, w); // image width write32(*response, h); // image height write16(*response, 1); // # planes write16(*response, depth); // bits per pixel write32(*response, 0); // format uncompressed uint32_t j = 0; for (uint32_t i = 34; i < imageOffset; i++) { response->write(filldata2[j++]); // remaining header bytes } uint32_t rowidx = w * h / 8; for (uint16_t row = 0; row < h; row++) // for each line { rowidx -= rowSizeCode; uint32_t colidx; for (colidx = 0; colidx < rowSizeCode; colidx++) { uint8_t data = pgm_read_byte(&bitmap[rowidx + colidx]); response->write(data); } while (colidx++ < rowSizeBMP) { response->write(uint8_t(0)); // padding } } request->send(response); }); } /** * @todo */ void setupWifiScan() { //First request will return 0 results unless you start scan from somewhere else (loop/setup) //Do not request more often than 3-5 seconds server.on("/api/wifi/scan", HTTP_GET, [](AsyncWebServerRequest *request) { String json = "["; int n = WiFi.scanComplete(); if (n == -2) { WiFi.scanNetworks(true); } else if (n) { for (int i = 0; i < n; ++i) { if (i) { json += ","; } json += "{"; json += "\"rssi\":" + String(WiFi.RSSI(i)); json += ",\"ssid\":\"" + WiFi.SSID(i) + "\""; json += ",\"bssid\":\"" + WiFi.BSSIDstr(i) + "\""; json += ",\"channel\":" + String(WiFi.channel(i)); json += ",\"secure\":" + String(WiFi.encryptionType(i)); json += "}"; } WiFi.scanDelete(); if (WiFi.scanComplete() == -2) { WiFi.scanNetworks(true); } } json += "]"; request->send(200, "application/json", json); json = String(); }); } /** * @todo */ void setupWifiConnect() { server.on( "/api/wifi/connect", HTTP_POST, [](AsyncWebServerRequest *request) { /* nothing and dont remove it */ }, NULL, [](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { DynamicJsonDocument doc(1024); DeserializationError error = deserializeJson(doc, data); if (error) { Serial.print(F("deserializeJson() failed with code ")); Serial.println(error.c_str()); request->send(404, "text/plain", ""); } else { if (doc.containsKey("ssid")) { NVS.setString("wifi_ssid", doc["ssid"]); Serial.println(doc["ssid"].as()); } if (doc.containsKey("password")) { NVS.setString("wifi_password", doc["password"]); Serial.println(doc["password"].as()); } request->send(200, "application/ld+json; charset=utf-8", "{}"); ESP.restart(); } }); } static void handle_update_progress_cb(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { if (!index) { Serial.printf("UploadStart: %s\n", filename.c_str()); bool dither = strcmp(filename.c_str(), "dithering") == 0; ImageNew(0, 0, 0, 0, dither); PlaylistResetTimer(); } ImageWriteBuffer(data, len); if (final) { Serial.printf("UploadEnd: %s, %u B\n", filename.c_str(), index + len); ImageFlushBuffer(); updateDisplayRequired = true; } } void setupApiFace() { server.on( "/api/face", HTTP_POST, [](AsyncWebServerRequest *request) { AsyncResponseStream *response = request->beginResponseStream("application/json"); DynamicJsonDocument doc(117); // https://arduinojson.org/v6/assistant/ // todo doc["status"] = true; doc["image"]["format"] = "xxx"; doc["image"]["width"] = 0; doc["image"]["height"] = 0; //doc["jpg"]["comps"] = 0; /* Serial.println(JpegDec.width); Serial.println(JpegDec.height); Serial.print(F("Components :")); Serial.println(JpegDec.comps); Serial.print(F("MCU / row :")); Serial.println(JpegDec.MCUSPerRow); Serial.print(F("MCU / col :")); Serial.println(JpegDec.MCUSPerCol); Serial.print(F("Scan type :")); Serial.println(JpegDec.scanType); Serial.print(F("MCU width :")); Serial.println(JpegDec.MCUWidth); Serial.print(F("MCU height :")); Serial.println(JpegDec.MCUHeight); */ serializeJson(doc, *response); request->send(response); //request->send(200, "application/ld+json; charset=utf-8", "{}"); }, handle_update_progress_cb); } void setupApiUpdate() { server.on("/api/update", HTTP_GET, [](AsyncWebServerRequest *request) { if (request->hasParam("datetime")) { Serial.println("update datetime..."); updateDateTime(); } if (request->hasParam("weather")) { Serial.println("update weather data..."); updateWeatherData(); } if (request->hasParam("calendar")) { Serial.println("update calendar data..."); updateCalendarData(); } if (request->getParam("url") && request->hasParam("file")) { Serial.println("file..."); downloadFile(request->getParam("url")->value().c_str(), request->getParam("file")->value().c_str()); } request->send(200, "application/ld+json; charset=utf-8", "{}"); }); } void setupOTA() { // Simple Firmware Update Form /* server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request) { // TODO in die pwa auslagern request->send(200, "text/html", "
"); }); */ server.on( "/update", HTTP_POST, [](AsyncWebServerRequest *request) { shouldReboot = !Update.hasError(); AsyncWebServerResponse *response = request->beginResponse(200, "application/ld+json; charset=utf-8", shouldReboot ? "{\"success\": true}" : "{\"success\": false}"); response->addHeader("Connection", "close"); request->send(response); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { if (!index) { Serial.printf("Update Start: %s\n", filename.c_str()); // bool canBegin = Update.begin(contentLength, U_FLASH); // bool canBegin = Update.begin(contentLength, U_SPIFFS); if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { Update.printError(Serial); } } if (!Update.hasError()) { if (Update.write(data, len) != len) { Update.printError(Serial); } } if (final) { if (Update.end(true)) { Serial.printf("Update Success: %uB\n", index + len); } else { Update.printError(Serial); } } }); } void write16(AsyncResponseStream &f, uint16_t v) { f.write(uint8_t(v)); f.write(uint8_t(v >> 8)); } void write32(AsyncResponseStream &f, uint32_t v) { f.write(uint8_t(v)); f.write(uint8_t(v >> 8)); f.write(uint8_t(v >> 16)); f.write(uint8_t(v >> 24)); }