#include "web.h" #include #include #include #include #include #include #include #include #include #include "AsyncJson.h" #include "LittleFS.h" #include "SPIFFSEditor.h" #include "commstructs.h" #include "language.h" #include "leds.h" #include "newproto.h" #include "ota.h" #include "serialap.h" #include "settings.h" #include "storage.h" #include "system.h" #include "tag_db.h" #include "udp.h" #include "wifimanager.h" #ifdef HAS_EXT_FLASHER #include "webflasher.h" #endif extern uint8_t data_to_send[]; AsyncWebServer server(80); AsyncWebSocket ws("/ws"); WifiManager wm; SemaphoreHandle_t wsMutex; uint32_t lastssidscan = 0; void wsLog(const String &text) { StaticJsonDocument<250> doc; doc["logMsg"] = text; if (wsMutex) xSemaphoreTake(wsMutex, portMAX_DELAY); ws.textAll(doc.as()); if (wsMutex) xSemaphoreGive(wsMutex); } void wsErr(const String &text) { StaticJsonDocument<250> doc; doc["errMsg"] = text; if (wsMutex) xSemaphoreTake(wsMutex, portMAX_DELAY); ws.textAll(doc.as()); if (wsMutex) xSemaphoreGive(wsMutex); } size_t dbSize() { size_t size = tagDB.size() * sizeof(tagRecord); for (auto &tag : tagDB) { if (tag->data) { size += tag->len; } size += tag->modeConfigJson.length(); } return size; } void wsSendSysteminfo() { DynamicJsonDocument doc(250); JsonObject sys = doc.createNestedObject("sys"); time_t now; time(&now); static int freeSpaceLastRun = 0; static size_t tagDBsize = 0; static uint64_t freeSpace = Storage.freeSpace(); sys["currtime"] = now; sys["heap"] = ESP.getFreeHeap(); sys["recordcount"] = tagDBsize; sys["dbsize"] = dbSize(); if (millis() - freeSpaceLastRun > 30000 || freeSpaceLastRun == 0) { freeSpace = Storage.freeSpace(); tagDBsize = tagDB.size(); freeSpaceLastRun = millis(); } sys["littlefsfree"] = freeSpace; #if BOARD_HAS_PSRAM sys["psfree"] = ESP.getFreePsram(); #endif sys["apstate"] = apInfo.state; sys["runstate"] = config.runStatus; sys["rssi"] = WiFi.RSSI(); sys["wifistatus"] = WiFi.status(); sys["wifissid"] = WiFi.SSID(); sys["uptime"] = esp_timer_get_time() / 1000000; static uint8_t day = 0; struct tm timeinfo; localtime_r(&now, &timeinfo); if (day != timeinfo.tm_mday) { day = timeinfo.tm_mday; char timeBuffer[80]; strftime(timeBuffer, sizeof(timeBuffer), languageDateFormat[0].c_str(), &timeinfo); setVarDB("ap_date", timeBuffer); } setVarDB("ap_ip", WiFi.localIP().toString()); #ifdef HAS_SUBGHZ String ApChanString = String(apInfo.channel); if(apInfo.hasSubGhz) { ApChanString += ", SubGhz "; if(apInfo.SubGhzChannel == 0) { ApChanString += "disabled"; } else { ApChanString += "Ch " + String(apInfo.SubGhzChannel); } } setVarDB("ap_ch", ApChanString); #else setVarDB("ap_ch", String(apInfo.channel)); #endif // reboot once at night if (timeinfo.tm_hour == 4 && timeinfo.tm_min == 0 && millis() > 2 * 3600 * 1000) { logLine("Nightly reboot"); wsErr("REBOOTING"); delay(100); ws.enable(false); refreshAllPending(); saveDB("/current/tagDB.json"); ws.closeAll(); delay(100); ESP.restart(); } static uint32_t tagcounttimer = 0; if (millis() - tagcounttimer > 60000 || tagcounttimer == 0) { uint32_t timeoutcount = 0; uint32_t tagcount = getTagCount(timeoutcount); char result[40]; if (timeoutcount > 0) { snprintf(result, sizeof(result), "%lu/%lu, %lu timeout", tagcount, tagDB.size(), timeoutcount); } else { snprintf(result, sizeof(result), "%lu / %lu", tagcount, tagDB.size()); } setVarDB("ap_tagcount", result); #ifdef HAS_RGB_LED if (timeoutcount > 0) { if (apInfo.state == AP_STATE_ONLINE && apInfo.isOnline == true) rgbIdleColor = CRGB::DarkBlue; } else { if (apInfo.state == AP_STATE_ONLINE && apInfo.isOnline == true) rgbIdleColor = CRGB::Green; } #endif tagcounttimer = millis(); } xSemaphoreTake(wsMutex, portMAX_DELAY); ws.textAll(doc.as()); xSemaphoreGive(wsMutex); } void wsSendTaginfo(const uint8_t *mac, uint8_t syncMode) { if (syncMode != SYNC_DELETE) { String json = ""; json = tagDBtoJson(mac); xSemaphoreTake(wsMutex, portMAX_DELAY); ws.textAll(json); xSemaphoreGive(wsMutex); } if (syncMode > SYNC_NOSYNC) { const tagRecord *taginfo = tagRecord::findByMAC(mac); if (taginfo != nullptr) { if (taginfo->contentMode != 12 || syncMode == SYNC_DELETE) { UDPcomm udpsync; struct TagInfo taginfoitem; memcpy(taginfoitem.mac, taginfo->mac, sizeof(taginfoitem.mac)); taginfoitem.syncMode = syncMode; taginfoitem.contentMode = taginfo->contentMode; if (syncMode == SYNC_USERCFG) { strncpy(taginfoitem.alias, taginfo->alias.c_str(), sizeof(taginfoitem.alias) - 1); taginfoitem.alias[sizeof(taginfoitem.alias) - 1] = '\0'; taginfoitem.nextupdate = taginfo->nextupdate; } if (syncMode == SYNC_TAGSTATUS) { taginfoitem.lastseen = taginfo->lastseen; taginfoitem.nextupdate = taginfo->nextupdate; taginfoitem.pendingCount = taginfo->pendingCount; taginfoitem.expectedNextCheckin = taginfo->expectedNextCheckin; taginfoitem.hwType = taginfo->hwType; taginfoitem.wakeupReason = taginfo->wakeupReason; taginfoitem.capabilities = taginfo->capabilities; taginfoitem.pendingIdle = taginfo->pendingIdle; } udpsync.netTaginfo(&taginfoitem); } } } } void wsSendAPitem(struct APlist *apitem) { DynamicJsonDocument doc(250); JsonObject ap = doc.createNestedObject("apitem"); char version_str[6]; sprintf(version_str, "%04X", apitem->version); ap["ip"] = ((IPAddress)apitem->src).toString(); ap["alias"] = apitem->alias; ap["count"] = apitem->tagCount; ap["channel"] = apitem->channelId; ap["version"] = version_str; if (wsMutex) xSemaphoreTake(wsMutex, portMAX_DELAY); ws.textAll(doc.as()); if (wsMutex) xSemaphoreGive(wsMutex); } void wsSerial(const String &text) { wsSerial(text, String("")); } void wsSerial(const String &text, const String &color) { StaticJsonDocument<250> doc; doc["console"] = text; if (!color.isEmpty()) doc["color"] = color; Serial.println(text); if (wsMutex) xSemaphoreTake(wsMutex, portMAX_DELAY); ws.textAll(doc.as()); if (wsMutex) xSemaphoreGive(wsMutex); } uint8_t wsClientCount() { return ws.count(); } void init_web() { wsMutex = xSemaphoreCreateMutex(); WiFi.mode(WIFI_STA); WiFi.setTxPower(static_cast(config.wifiPower)); wm.connectToWifi(); server.addHandler(new SPIFFSEditor(*contentFS)); server.addHandler(&ws); server.on("/reboot", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200, "text/plain", "OK Reboot"); logLine("Reboot request by user"); wsErr("REBOOTING"); delay(100); ws.enable(false); refreshAllPending(); saveDB("/current/tagDB.json"); ws.closeAll(); delay(100); ESP.restart(); }); server.serveStatic("/current", *contentFS, "/current/").setCacheControl("max-age=604800"); server.serveStatic("/tagtypes", *contentFS, "/tagtypes/").setCacheControl("max-age=300"); server.on( "/imgupload", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }, doImageUpload); server.on("/jsonupload", HTTP_POST, doJsonUpload); server.on("/get_db", HTTP_GET, [](AsyncWebServerRequest *request) { String json = ""; if (request->hasParam("mac")) { String dst = request->getParam("mac")->value(); uint8_t mac[8]; if (hex2mac(dst, mac)) { json = tagDBtoJson(mac); } else { json = "{\"error\": \"malformatted parameter\"}"; } } else { uint8_t startPos = 0; if (request->hasParam("pos")) { startPos = atoi(request->getParam("pos")->value().c_str()); } json = tagDBtoJson(nullptr, startPos); } request->send(200, "application/json", json); }); server.on("/getdata", HTTP_GET, [](AsyncWebServerRequest *request) { if (request->hasParam("mac")) { String dst = request->getParam("mac")->value(); uint8_t mac[8]; if (hex2mac(dst, mac)) { tagRecord *taginfo = tagRecord::findByMAC(mac); if (taginfo != nullptr) { if (request->hasParam("md5")) { uint8_t md5[8]; if (hex2mac(request->getParam("md5")->value(), md5)) { PendingItem *queueItem = getQueueItem(mac, *reinterpret_cast(md5)); if (queueItem == nullptr) { Serial.println("getQueueItem: no queue item"); request->send(404, "text/plain", "File not found"); return; } if (queueItem->data == nullptr) { fs::File file = contentFS->open(queueItem->filename); if (file) { queueItem->data = getDataForFile(file); Serial.println("Reading file " + String(queueItem->filename)); file.close(); } else { request->send(404, "text/plain", "File not found"); return; } } request->send_P(200, "application/octet-stream", queueItem->data, queueItem->len); return; } } else { // older version without queue if (taginfo->data == nullptr) { fs::File file = contentFS->open(taginfo->filename); if (!file) { request->send(404, "text/plain", "File not found"); return; } taginfo->data = getDataForFile(file); file.close(); } request->send_P(200, "application/octet-stream", taginfo->data, taginfo->len); return; } } } } request->send(400, "text/plain", "No data available"); }); server.on("/save_cfg", HTTP_POST, [](AsyncWebServerRequest *request) { if (request->hasParam("mac", true)) { String dst = request->getParam("mac", true)->value(); uint8_t mac[8]; if (hex2mac(dst, mac)) { tagRecord *taginfo = tagRecord::findByMAC(mac); if (taginfo != nullptr) { uint16_t newContentMode = atoi(request->getParam("contentmode", true)->value().c_str()); if (newContentMode != taginfo->contentMode && (newContentMode == 5 || newContentMode == 17 || newContentMode == 18)) { // temporary content, restore after sending pushTagInfo(taginfo); } taginfo->alias = request->getParam("alias", true)->value(); taginfo->modeConfigJson = request->getParam("modecfgjson", true)->value(); taginfo->contentMode = newContentMode; taginfo->nextupdate = 0; if (request->hasParam("rotate", true)) { taginfo->rotate = atoi(request->getParam("rotate", true)->value().c_str()); } if (request->hasParam("lut", true)) { taginfo->lut = atoi(request->getParam("lut", true)->value().c_str()); } if (request->hasParam("invert", true)) { taginfo->invert = atoi(request->getParam("invert", true)->value().c_str()); } wsSendTaginfo(mac, SYNC_USERCFG); // saveDB("/current/tagDB.json"); request->send(200, "text/plain", "Ok, saved"); } else { request->send(200, "text/plain", "Error while saving: mac not found"); } } } request->send(200, "text/plain", "Ok, saved"); }); server.on("/tag_cmd", HTTP_POST, [](AsyncWebServerRequest *request) { if (request->hasParam("mac", true) && request->hasParam("cmd", true)) { uint8_t mac[8]; if (hex2mac(request->getParam("mac", true)->value(), mac)) { tagRecord *taginfo = tagRecord::findByMAC(mac); if (taginfo != nullptr) { const char *cmdValue = request->getParam("cmd", true)->value().c_str(); if (strcmp(cmdValue, "del") == 0) { wsSendTaginfo(mac, SYNC_DELETE); deleteRecord(mac); } if (strcmp(cmdValue, "purge") == 0) { time_t now; time(&now); for (int c = tagDB.size() - 1; c >= 0; --c) { tagRecord *tag = tagDB.at(c); if (tag->expectedNextCheckin == 3216153600 || tag->lastseen < now - 24 * 3600 || now > tag->expectedNextCheckin + 600) { wsSendTaginfo(tag->mac, SYNC_DELETE); deleteRecord(tag->mac); } } } if (strcmp(cmdValue, "clear") == 0) { clearPending(taginfo); while (dequeueItem(mac)) { }; taginfo->pendingCount = countQueueItem(mac); wsSendTaginfo(mac, SYNC_TAGSTATUS); } if (strcmp(cmdValue, "refresh") == 0) { updateContent(mac); } if (strcmp(cmdValue, "reboot") == 0) { sendTagCommand(mac, CMD_DO_REBOOT, !taginfo->isExternal); } if (strcmp(cmdValue, "scan") == 0) { sendTagCommand(mac, CMD_DO_SCAN, !taginfo->isExternal); } if (strcmp(cmdValue, "reset") == 0) { sendTagCommand(mac, CMD_DO_RESET_SETTINGS, !taginfo->isExternal); } if (strcmp(cmdValue, "deepsleep") == 0) { sendTagCommand(mac, CMD_DO_DEEPSLEEP, !taginfo->isExternal); taginfo->pendingIdle = 9999; wsSendTaginfo(mac, SYNC_TAGSTATUS); } if (strcmp(cmdValue, "ledflash") == 0) { struct ledFlash flashData = {0}; flashData.mode = 1; flashData.flashDuration = 8; flashData.color1 = 0x3C; // green flashData.color2 = 0xE4; // red flashData.color3 = 0x03; // blue flashData.flashCount1 = 3; flashData.flashCount2 = 3; flashData.flashCount3 = 3; flashData.delay1 = 10; flashData.delay2 = 10; flashData.delay3 = 10; flashData.flashSpeed1 = 1; flashData.flashSpeed2 = 5; flashData.flashSpeed3 = 10; flashData.repeats = 2; const uint8_t *payload = reinterpret_cast(&flashData); sendTagCommand(mac, CMD_DO_LEDFLASH, !taginfo->isExternal, payload); } if (strcmp(cmdValue, "ledflash_long") == 0) { struct ledFlash flashData = {0}; flashData.mode = 1; flashData.flashDuration = 1; flashData.color1 = 0xE4; // red flashData.flashCount1 = 3; flashData.flashSpeed1 = 3; flashData.delay1 = 50; flashData.repeats = 60; const uint8_t *payload = reinterpret_cast(&flashData); sendTagCommand(mac, CMD_DO_LEDFLASH, !taginfo->isExternal, payload); } if (strcmp(cmdValue, "ledflash_stop") == 0) { struct ledFlash flashData = {0}; flashData.mode = 0; const uint8_t *payload = reinterpret_cast(&flashData); sendTagCommand(mac, CMD_DO_LEDFLASH, !taginfo->isExternal, payload); } request->send(200, "text/plain", "Ok, done"); } else { request->send(400, "text/plain", "Error: mac not found"); } } } else { request->send(500, "text/plain", "param error"); } }); server.on("/led_flash", HTTP_GET, [](AsyncWebServerRequest *request) { // color picker: https://roger-random.github.io/RGB332_color_wheel_three.js/ // http GET to /led_flash?mac=000000000000&pattern=000000000000000000000000 // see https://github.com/jjwbruijn/OpenEPaperLink/wiki/Led-control if (request->hasParam("mac")) { String dst = request->getParam("mac")->value(); uint8_t mac[8]; if (hex2mac(dst, mac)) { tagRecord *taginfo = tagRecord::findByMAC(mac); if (taginfo != nullptr) { uint8_t payload[12] = {0}; if (request->hasParam("pattern")) { if (sscanf(request->getParam("pattern")->value().c_str(), "%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx", &payload[0], &payload[1], &payload[2], &payload[3], &payload[4], &payload[5], &payload[6], &payload[7], &payload[8], &payload[9], &payload[10], &payload[11]) != 12) { request->send(400, "text/plain", "Error: expects 12 hex bytes in pattern"); return; } } sendTagCommand(mac, CMD_DO_LEDFLASH, !taginfo->isExternal, payload); request->send(200, "text/plain", "ok, request transmitted"); return; } } } request->send(400, "text/plain", "parameters are missing"); }); server.on("/get_ap_config", HTTP_GET, [](AsyncWebServerRequest *request) { UDPcomm udpsync; udpsync.getAPList(); AsyncResponseStream *response = request->beginResponseStream("application/json"); response->print("{"); #ifdef C6_OTA_FLASHING response->print("\"C6\": \"1\", "); #else response->print("\"C6\": \"0\", "); #endif #ifdef SAVE_SPACE response->print("\"savespace\": \"1\", "); #else response->print("\"savespace\": \"0\", "); #endif #ifdef HAS_EXT_FLASHER response->print("\"hasFlasher\": \"1\", "); #else response->print("\"hasFlasher\": \"0\", "); #endif #ifdef HAS_BLE_WRITER response->print("\"hasBLE\": \"1\", "); #else response->print("\"hasBLE\": \"0\", "); #endif #ifdef HAS_SUBGHZ response->print("\"hasSubGhz\": \"" + String(apInfo.hasSubGhz) + "\","); #else response->print("\"hasSubGhz\": \"0\", "); #endif response->print("\"apstate\": \"" + String(apInfo.state) + "\""); File configFile = contentFS->open("/current/apconfig.json", "r"); if (configFile) { response->print(", "); configFile.seek(1); const size_t bufferSize = 64; uint8_t buffer[bufferSize]; while (configFile.available()) { size_t bytesRead = configFile.read(buffer, bufferSize); response->write(buffer, bytesRead); } configFile.close(); } else { response->print("}"); } request->send(response); }); server.on("/save_apcfg", HTTP_POST, [](AsyncWebServerRequest *request) { if (request->hasParam("alias", true)) { String aliasValue = request->getParam("alias", true)->value(); size_t aliasLength = aliasValue.length(); if (aliasLength > 31) aliasLength = 31; aliasValue.toCharArray(config.alias, aliasLength + 1); config.alias[aliasLength] = '\0'; } if (request->hasParam("channel", true)) { config.channel = static_cast(request->getParam("channel", true)->value().toInt()); } if (request->hasParam("subghzchannel", true)) { config.subghzchannel = static_cast(request->getParam("subghzchannel", true)->value().toInt()); } if (request->hasParam("led", true)) { config.led = static_cast(request->getParam("led", true)->value().toInt()); updateBrightnessFromConfig(); } if (request->hasParam("tft", true)) { config.tft = static_cast(request->getParam("tft", true)->value().toInt()); updateBrightnessFromConfig(); } if (request->hasParam("language", true)) { config.language = static_cast(request->getParam("language", true)->value().toInt()); updateLanguageFromConfig(); } if (request->hasParam("maxsleep", true)) { config.maxsleep = static_cast(request->getParam("maxsleep", true)->value().toInt()); } if (request->hasParam("stopsleep", true)) { config.stopsleep = static_cast(request->getParam("stopsleep", true)->value().toInt()); } if (request->hasParam("preview", true)) { config.preview = static_cast(request->getParam("preview", true)->value().toInt()); } if (request->hasParam("lock", true)) { config.lock = static_cast(request->getParam("lock", true)->value().toInt()); } if (request->hasParam("ble", true)) { config.ble = static_cast(request->getParam("ble", true)->value().toInt()); } if (request->hasParam("sleeptime1", true)) { config.sleepTime1 = static_cast(request->getParam("sleeptime1", true)->value().toInt()); config.sleepTime2 = static_cast(request->getParam("sleeptime2", true)->value().toInt()); } if (request->hasParam("wifipower", true)) { config.wifiPower = static_cast(request->getParam("wifipower", true)->value().toInt()); WiFi.setTxPower(static_cast(config.wifiPower)); } if (request->hasParam("timezone", true)) { strncpy(config.timeZone, request->getParam("timezone", true)->value().c_str(), sizeof(config.timeZone) - 1); config.timeZone[sizeof(config.timeZone) - 1] = '\0'; setenv("TZ", config.timeZone, 1); tzset(); } if (request->hasParam("repo", true)) { config.repo = request->getParam("repo", true)->value(); } if (request->hasParam("env", true)) { config.env = request->getParam("env", true)->value(); } saveAPconfig(); setAPchannel(); request->send(200, "text/plain", "Ok, saved"); }); server.on("/set_var", HTTP_POST, [](AsyncWebServerRequest *request) { if (request->hasParam("key", true) && request->hasParam("val", true)) { std::string key = request->getParam("key", true)->value().c_str(); String val = request->getParam("val", true)->value(); Serial.printf("set key %s value %s\n", key.c_str(), val); setVarDB(key, val); request->send(200, "text/plain", "Ok, saved"); } else { request->send(500, "text/plain", "param error"); } }); server.on("/set_vars", HTTP_POST, [](AsyncWebServerRequest *request) { if (request->hasParam("json", true)) { DynamicJsonDocument jsonDocument(2048); DeserializationError error = deserializeJson(jsonDocument, request->getParam("json", true)->value()); if (error) { request->send(400, "text/plain", "Failed to parse JSON"); return; } for (JsonPair kv : jsonDocument.as()) { std::string key = kv.key().c_str(); String val = kv.value().as(); Serial.printf("set key %s value %s\n", key.c_str(), val); setVarDB(key, val); } request->send(200, "text/plain", "JSON uploaded and processed"); } else { request->send(400, "text/plain", "No 'json' parameter found in request"); } }); // setup server.on("/setup", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(*contentFS, "/www/setup.html"); }); server.on("/get_wifi_config", HTTP_GET, [](AsyncWebServerRequest *request) { Preferences preferences; AsyncResponseStream *response = request->beginResponseStream("application/json"); StaticJsonDocument<250> doc; preferences.begin("wifi", false); const char *keys[] = {"ssid", "pw", "ip", "mask", "gw", "dns"}; const size_t numKeys = sizeof(keys) / sizeof(keys[0]); for (size_t i = 0; i < numKeys; i++) { doc[keys[i]] = preferences.getString(keys[i], ""); } doc["mac"] = WiFi.macAddress(); serializeJson(doc, *response); request->send(response); }); server.on("/get_ssid_list", HTTP_GET, [](AsyncWebServerRequest *request) { AsyncResponseStream *response = request->beginResponseStream("application/json"); DynamicJsonDocument doc(5000); doc["scanstatus"] = WiFi.scanComplete(); JsonArray networks = doc.createNestedArray("networks"); for (int i = 0; i < (WiFi.scanComplete() > 50 ? 50 : WiFi.scanComplete()); ++i) { if (WiFi.SSID(i) != "") { JsonObject network = networks.createNestedObject(); network["ssid"] = WiFi.SSID(i); network["ch"] = WiFi.channel(i); network["rssi"] = WiFi.RSSI(i); network["enc"] = WiFi.encryptionType(i); } } if (WiFi.scanComplete() != -1 && (WiFi.scanComplete() == -2 || millis() - lastssidscan > 30000)) { WiFi.scanDelete(); Serial.println("start scanning"); WiFi.scanNetworks(true, true); lastssidscan = millis(); } serializeJson(doc, *response); request->send(response); }); AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler("/save_wifi_config", [](AsyncWebServerRequest *request, JsonVariant &json) { const JsonObject &jsonObj = json.as(); Preferences preferences; preferences.begin("wifi", false); const char *keys[] = {"ssid", "pw", "ip", "mask", "gw", "dns"}; const size_t numKeys = sizeof(keys) / sizeof(keys[0]); for (size_t i = 0; i < numKeys; i++) { String key = keys[i]; if (jsonObj.containsKey(key)) { preferences.putString(key.c_str(), jsonObj[key].as()); } } preferences.end(); Serial.println("config saved"); request->send(200, "text/plain", "Ok, saved"); ws.enable(false); if (jsonObj["ssid"].as() == "factory") { preferences.begin("wifi", false); preferences.putString("ssid", ""); preferences.putString("pw", ""); preferences.end(); contentFS->remove("/AP_FW_Pack.bin"); contentFS->remove("/OpenEPaperLink_esp32_C6.bin"); contentFS->remove("/bootloader.bin"); contentFS->remove("/partition-table.bin"); contentFS->remove("/update_actions.json"); contentFS->remove("/log.txt"); contentFS->remove("/current/tagDB.json"); delay(100); ESP.restart(); } else { refreshAllPending(); saveDB("/current/tagDB.json"); } ws.closeAll(); delay(100); ESP.restart(); }); server.addHandler(handler); // end of setup server.on("/backup_db", HTTP_GET, [](AsyncWebServerRequest *request) { saveDB("/current/tagDB.json"); request->send(*contentFS, "/current/tagDB.json", String(), true); }); server.on( "/restore_db", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }, dotagDBUpload); // OTA related calls server.on("/sysinfo", HTTP_GET, handleSysinfoRequest); server.on("/check_file", HTTP_GET, handleCheckFile); server.on("/rollback", HTTP_POST, handleRollback); server.on("/update_c6", HTTP_POST, handleUpdateC6); server.on("/update_actions", HTTP_POST, handleUpdateActions); server.on("/update_ota", HTTP_POST, [](AsyncWebServerRequest *request) { handleUpdateOTA(request); }); server.on( "/littlefs_put", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }, handleLittleFSUpload); #ifdef HAS_EXT_FLASHER // Flasher related calls ws.onEvent([](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { if (type == WS_EVT_DATA) handleWSdata(data, len, client); }); #endif server.onNotFound([](AsyncWebServerRequest *request) { if (request->url() == "/" || request->url() == "index.htm") { request->send(200, "text/html", "index.html not found. Did you forget to upload the littlefs partition?"); return; } request->send(404); }); server.serveStatic("/", *contentFS, "/www/").setDefaultFile("index.html"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "content-type"); server.begin(); } #define UPLOAD_BUFFER_SIZE 32768 struct UploadInfo { String filename; uint8_t buffer[UPLOAD_BUFFER_SIZE]; size_t bufferSize; }; void doImageUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { String uploadfilename; if (!index) { if (config.runStatus != RUNSTATUS_RUN) { request->send(409, "text/plain", "Come back later"); return; } if (!request->hasParam("mac", true)) { request->send(400, "text/plain", "parameters incomplete"); return; } logLine("http imageUpload " + uploadfilename); uploadfilename = request->getParam("mac", true)->value() + "_" + String(millis()) + ".jpg"; File file = contentFS->open("/temp/" + uploadfilename, "w"); file.close(); Serial.println("upload started " + uploadfilename); UploadInfo *uploadInfo = new UploadInfo{uploadfilename, {}, 0}; request->_tempObject = (void *)uploadInfo; } UploadInfo *uploadInfo = static_cast(request->_tempObject); if (uploadInfo != nullptr) { uploadfilename = uploadInfo->filename; if (len) { if (uploadInfo->bufferSize + len <= UPLOAD_BUFFER_SIZE) { memcpy(&uploadInfo->buffer[uploadInfo->bufferSize], data, len); uploadInfo->bufferSize += len; } else { xSemaphoreTake(fsMutex, portMAX_DELAY); File file = contentFS->open("/temp/" + uploadfilename, "a"); if (file) { file.write(uploadInfo->buffer, uploadInfo->bufferSize); file.close(); uploadInfo->bufferSize = 0; } else { logLine("Failed to open file for appending: " + uploadfilename); } xSemaphoreGive(fsMutex); memcpy(uploadInfo->buffer, data, len); uploadInfo->bufferSize = len; } } if (final) { if (uploadInfo->bufferSize > 0) { xSemaphoreTake(fsMutex, portMAX_DELAY); File file = contentFS->open("/temp/" + uploadfilename, "a"); if (file) { file.write(uploadInfo->buffer, uploadInfo->bufferSize); file.close(); } else { logLine("Failed to open file for appending: " + uploadfilename); } xSemaphoreGive(fsMutex); request->_tempObject = nullptr; delete uploadInfo; } if (request->hasParam("mac", true)) { String dst = request->getParam("mac", true)->value(); uint8_t mac[8]; if (hex2mac(dst, mac)) { tagRecord *taginfo = tagRecord::findByMAC(mac); if (taginfo != nullptr) { uint8_t dither = 1; if (request->hasParam("dither", true)) { dither = request->getParam("dither", true)->value().toInt(); } uint32_t ttl = 0; if (request->hasParam("ttl", true)) { ttl = request->getParam("ttl", true)->value().toInt(); } uint8_t preload = 0; uint8_t preloadlut = 0; uint8_t preloadtype = 0; if (request->hasParam("preloadtype", true)) { preload = 1; preloadtype = request->getParam("preloadtype", true)->value().toInt(); if (request->hasParam("preloadlut", true)) { preloadlut = request->getParam("preloadlut", true)->value().toInt(); } } taginfo->modeConfigJson = "{\"filename\":\"/temp/" + uploadfilename + "\",\"timetolive\":\"" + String(ttl) + "\",\"dither\":\"" + String(dither) + "\",\"delete\":\"1\", \"preload\":\"" + String(preload) + "\", \"preload_lut\":\"" + String(preloadlut) + "\", \"preload_type\":\"" + String(preloadtype) + "\"}"; if (request->hasParam("contentmode", true)) { taginfo->contentMode = request->getParam("contentmode", true)->value().toInt(); } else { taginfo->contentMode = 24; } Serial.println("upload finished " + uploadfilename); taginfo->nextupdate = 0; wsSendTaginfo(mac, SYNC_USERCFG); request->send(200, "text/plain", "Ok, saved"); } else { request->send(400, "text/plain", "mac not found"); } } } } } } void doJsonUpload(AsyncWebServerRequest *request) { if (config.runStatus != RUNSTATUS_RUN) { request->send(409, "text/plain", "come back later"); return; } if (request->hasParam("mac", true) && request->hasParam("json", true)) { String dst = request->getParam("mac", true)->value(); uint8_t mac[8]; if (hex2mac(dst, mac)) { xSemaphoreTake(fsMutex, portMAX_DELAY); File file = LittleFS.open("/current/" + dst + ".json", "w"); if (!file) { request->send(400, "text/plain", "Failed to create file"); xSemaphoreGive(fsMutex); return; } file.print(request->getParam("json", true)->value()); file.close(); xSemaphoreGive(fsMutex); tagRecord *taginfo = tagRecord::findByMAC(mac); if (taginfo != nullptr) { uint32_t ttl = 0; if (request->hasParam("ttl", true)) { ttl = request->getParam("ttl", true)->value().toInt(); } taginfo->modeConfigJson = "{\"filename\":\"/current/" + dst + ".json\",\"interval\":\"" + String(ttl) + "\"}"; taginfo->contentMode = 19; taginfo->nextupdate = 0; wsSendTaginfo(mac, SYNC_USERCFG); request->send(200, "text/plain", "Ok, saved"); } else { request->send(400, "text/plain", "mac not found in tagDB"); } } return; } request->send(400, "text/plain", "Missing parameters"); } void dotagDBUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { if (!index) { logLine("restore tagDB"); xSemaphoreTake(fsMutex, portMAX_DELAY); request->_tempFile = contentFS->open("/current/tagDBrestored.json", "w"); } if (len) { request->_tempFile.write(data, len); } if (final) { request->_tempFile.close(); xSemaphoreGive(fsMutex); destroyDB(); loadDB("/current/tagDBrestored.json"); request->send(200, "text/plain", "Ok, restored."); } }