diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d550677..cb92a4f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -83,7 +83,7 @@ jobs: - board: esp32 addr_bootloader: 0x1000 chip: ESP32 - fqbn: esp32:esp32:esp32:FlashMode=qio,FlashFreq=80,DebugLevel=none + fqbn: esp32:esp32:esp32:PartitionScheme=default,FlashMode=qio,FlashFreq=80,DebugLevel=none # esp32:esp32:esp32wrover:PartitionScheme=default,FlashMode=qio,FlashFreq=80,UploadSpeed=921600,DebugLevel=none,EraseFlash=none name: ESP32 obname: SomfyController.onboard.esp32.bin @@ -105,7 +105,7 @@ jobs: - board: esp32s3 addr_bootloader: 0x0 chip: ESP32-S3 - fqbn: esp32:esp32:esp32s3:USBMode=hwcdc,CDCOnBoot=cdc + fqbn: esp32:esp32:esp32s3:JTAGAdapter=default,USBMode=hwcdc,CDCOnBoot=cdc,DebugLevel=none,FlashMode=qio,PartitionScheme=default # esp32:esp32:esp32s3:JTAGAdapter=default,PSRAM=disabled,FlashMode=qio,FlashSize=4M,LoopCore=1,EventsCore=1,USBMode=hwcdc,CDCOnBoot=cdc,MSCOnBoot=default,DFUOnBoot=default,UploadMode=default,PartitionScheme=default,CPUFreq=240,UploadSpeed=921600,DebugLevel=none,EraseFlash=none name: ESP32S3 fwname: SomfyController.ino.esp32s3.bin @@ -161,7 +161,7 @@ jobs: - name: Build ${{ matrix.name }} run: | mkdir -p build${{ matrix.name }} - arduino-cli compile --clean --output-dir build${{ matrix.name }} --fqbn ${{ matrix.fqbn }} --warnings default ./SomfyController + arduino-cli compile --clean --output-dir build${{ matrix.name }} --fqbn ${{ matrix.fqbn }} --warnings none ./SomfyController - name: ${{ matrix.name }} Image run: | diff --git a/ConfigSettings.h b/ConfigSettings.h index d313fa3..29c9fca 100644 --- a/ConfigSettings.h +++ b/ConfigSettings.h @@ -161,6 +161,7 @@ class ConfigSettings: BaseSettings { appver_t fwVersion; appver_t appVersion; bool ssdpBroadcast = true; + bool checkForUpdate = true; uint8_t status; IPSettings IP; WifiSettings WIFI; diff --git a/GitOTA.cpp b/GitOTA.cpp index 6b3ef70..1e73387 100644 --- a/GitOTA.cpp +++ b/GitOTA.cpp @@ -29,7 +29,6 @@ void GitRelease::setReleaseProperty(const char *key, const char *val) { else if(strcmp(key, "published_at") == 0) { //Serial.printf("Key:[%s] Value:[%s]\n", key, val); this->releaseDate = Timestamp::parseUTCTime(val); - //Serial.println(this->releaseDate); } } void GitRelease::setAssetProperty(const char *key, const char *val) { @@ -85,6 +84,7 @@ bool GitRelease::toJSON(JsonObject &obj) { int16_t GitRepo::getReleases(uint8_t num) { WiFiClientSecure sclient; sclient.setInsecure(); + sclient.setHandshakeTimeout(3); uint8_t ndx = 0; uint8_t count = min((uint8_t)GIT_MAX_RELEASES, num); char url[128]; @@ -241,8 +241,11 @@ bool GitRepo::toJSON(JsonObject &obj) { void GitUpdater::loop() { if(this->status == GIT_STATUS_READY) { - if(this->lastCheck == 0) this->lastCheck = millis(); - else if(this->lastCheck + 14400000 < millis() && !rebootDelay.reboot) { // 4 hours + //if(this->lastCheck == 0) + //this->lastCheck = millis(); + //else + if(settings.checkForUpdate && + (this->lastCheck + 14400000 < millis() || this->lastCheck == 0) && !rebootDelay.reboot) { // 4 hours this->checkForUpdate(); } } @@ -265,9 +268,9 @@ void GitUpdater::checkForUpdate() { Serial.println("Check github for updates..."); this->status = GIT_STATUS_CHECK; settings.printAvailHeap(); + this->lastCheck = millis(); if(this->checkInternet() == 0) { GitRepo repo; - this->lastCheck = millis(); this->updateAvailable = false; this->error = repo.getReleases(2); if(this->error == 0) { // Get 2 releases so we can filter our pre-releases @@ -320,6 +323,7 @@ int GitUpdater::checkInternet() { uint32_t t = millis(); WiFiClientSecure client; client.setInsecure(); + client.setHandshakeTimeout(3); HTTPClient *https = new HTTPClient(); https->setReuse(false); if(https->begin(client, "https://github.com/rstrouse/ESPSomfy-RTS")) { @@ -328,11 +332,11 @@ int GitUpdater::checkInternet() { int httpCode = https->sendRequest("HEAD"); if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY || httpCode == HTTP_CODE_FOUND) { err = 0; - Serial.printf("Check Internet Success: %ldms\n", millis() - t); + Serial.printf("Internet is Available: %ldms\n", millis() - t); } else { err = httpCode; - Serial.printf("Check Internet Error: %d: %ldms\n", err, millis() - t); + Serial.printf("Internet is Unavailable: %d: %ldms\n", err, millis() - t); } https->end(); } diff --git a/SomfyController.ino b/SomfyController.ino index 4ec293b..804c6fa 100644 --- a/SomfyController.ino +++ b/SomfyController.ino @@ -38,8 +38,6 @@ void setup() { } void loop() { - if(!rebootDelay.reboot) git.loop(); - // put your main code here, to run repeatedly: if(rebootDelay.reboot && millis() > rebootDelay.rebootTime) { Serial.print("Rebooting after "); @@ -55,6 +53,7 @@ void loop() { if(millis() - timing > 100) Serial.printf("Timing Somfy: %ldms\n", millis() - timing); timing = millis(); if(net.connected()) { + if(!rebootDelay.reboot) git.loop(); webServer.loop(); if(millis() - timing > 200) Serial.printf("Timing WebServer: %ldms\n", millis() - timing); timing = millis(); diff --git a/SomfyController.ino.esp32.bin b/SomfyController.ino.esp32.bin index 91eb7ed..ac342e1 100644 Binary files a/SomfyController.ino.esp32.bin and b/SomfyController.ino.esp32.bin differ diff --git a/SomfyController.ino.esp32s3.bin b/SomfyController.ino.esp32s3.bin index 60b45ae..1abf5c0 100644 Binary files a/SomfyController.ino.esp32s3.bin and b/SomfyController.ino.esp32s3.bin differ diff --git a/SomfyController.littlefs.bin b/SomfyController.littlefs.bin index 1cfc645..e32259e 100644 Binary files a/SomfyController.littlefs.bin and b/SomfyController.littlefs.bin differ diff --git a/Web.cpp b/Web.cpp index cab1d75..d0cabb0 100644 --- a/Web.cpp +++ b/Web.cpp @@ -22,7 +22,8 @@ extern MQTTClass mqtt; extern GitUpdater git; extern Network net; -#define WEB_MAX_RESPONSE 34768 +//#define WEB_MAX_RESPONSE 34768 +#define WEB_MAX_RESPONSE 8192 static char g_content[WEB_MAX_RESPONSE]; @@ -225,16 +226,94 @@ void Web::handleStreamFile(WebServer &server, const char *filename, const char * server.streamFile(file, encoding); file.close(); } +void Web::chunkRoomsResponse(WebServer &server, const char * elem) { + uint8_t ndx = 0; + if(elem && strlen(elem) > 0) { + sprintf(g_content, "\"%s\"", elem); + server.sendContent(g_content); + } + for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) { + if(somfy.rooms[i].roomId != 0) { + DynamicJsonDocument doc(512); + JsonObject obj = doc.to(); + somfy.rooms[i].toJSON(obj); + strcpy(g_content, ndx++ != 0 ? "," : "["); + serializeJson(doc, &g_content[strlen(g_content)], sizeof(g_content) - strlen(g_content)); + server.sendContent(g_content); + } + } + server.sendContent(ndx == 0 ? "[]" : "]"); +} +void Web::chunkShadesResponse(WebServer &server, const char * elem) { + uint8_t ndx = 0; + if(elem && strlen(elem) > 0) { + sprintf(g_content, "\"%s\"", elem); + server.sendContent(g_content); + } + for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { + if(somfy.shades[i].getShadeId() != 255) { + DynamicJsonDocument doc(1024); + JsonObject obj = doc.to(); + somfy.shades[i].toJSON(obj); + strcpy(g_content, ndx++ != 0 ? "," : "["); + serializeJson(doc, &g_content[strlen(g_content)], sizeof(g_content) - strlen(g_content)); + server.sendContent(g_content); + } + } + server.sendContent(ndx == 0 ? "[]" : "]"); +} +void Web::chunkGroupsResponse(WebServer &server, const char * elem) { + uint8_t ndx = 0; + if(elem && strlen(elem) > 0) { + sprintf(g_content, "\"%s\"", elem); + server.sendContent(g_content); + } + for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) { + if(somfy.groups[i].getGroupId() != 255) { + DynamicJsonDocument doc(1024); + JsonObject obj = doc.to(); + somfy.groups[i].toJSON(obj); + strcpy(g_content, ndx++ != 0 ? "," : "["); + serializeJson(doc, &g_content[strlen(g_content)], sizeof(g_content) - strlen(g_content)); + server.sendContent(g_content); + } + } + server.sendContent(ndx == 0 ? "[]" : "]"); +} void Web::handleController(WebServer &server) { webServer.sendCORSHeaders(server); if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } HTTPMethod method = server.method(); settings.printAvailHeap(); if (method == HTTP_POST || method == HTTP_GET) { - DynamicJsonDocument doc(min((uint32_t)WEB_MAX_RESPONSE, ESP.getMaxAllocHeap() - 512)); - somfy.toJSON(doc); - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + // Alright lets chunk our response. + snprintf(g_content, sizeof(g_content), "{\"maxRooms\":%d,\"maxShades\":%d,\"maxGroups\":%d,\"maxGroupedShades\":%d,\"maxLinkedRemotes\":%d,\"startingAddress\":%d,\"transceiver\":", + SOMFY_MAX_ROOMS, SOMFY_MAX_SHADES, SOMFY_MAX_GROUPS, SOMFY_MAX_GROUPED_SHADES, SOMFY_MAX_LINKED_REMOTES, somfy.startingAddress); + server.send_P(200, _encoding_json, g_content); + { + DynamicJsonDocument doc(1024); + JsonObject trans = doc.to(); + somfy.transceiver.toJSON(trans); + serializeJson(doc, g_content); + server.sendContent(g_content); + } + { + DynamicJsonDocument doc(512); + JsonObject fw = doc.to(); + git.toJSON(fw); + server.sendContent(",\"version\":"); + serializeJson(doc, g_content); + server.sendContent(g_content); + } + server.sendContent(",\"rooms\":"); + this->chunkRoomsResponse(server); + server.sendContent(",\"shades\":"); + this->chunkShadesResponse(server); + server.sendContent(",\"groups\":"); + this->chunkGroupsResponse(server); + server.sendContent("}"); + server.sendContent("", 0); } else server.send(404, _encoding_text, _response_404); } @@ -257,11 +336,10 @@ void Web::handleGetRooms(WebServer &server) { if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } HTTPMethod method = server.method(); if (method == HTTP_POST || method == HTTP_GET) { - DynamicJsonDocument doc(16384); - JsonArray arr = doc.to(); - somfy.toJSONRooms(arr); - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + server.send_P(200, _encoding_json, " "); + this->chunkRoomsResponse(server); + server.sendContent("", 0); } else server.send(404, _encoding_text, _response_404); } @@ -270,11 +348,10 @@ void Web::handleGetShades(WebServer &server) { if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } HTTPMethod method = server.method(); if (method == HTTP_POST || method == HTTP_GET) { - DynamicJsonDocument doc(16384); - JsonArray arr = doc.to(); - somfy.toJSONShades(arr); - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + server.send_P(200, _encoding_json, " "); + this->chunkShadesResponse(server); + server.sendContent("", 0); } else server.send(404, _encoding_text, _response_404); } @@ -283,11 +360,10 @@ void Web::handleGetGroups(WebServer &server) { if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } HTTPMethod method = server.method(); if (method == HTTP_POST || method == HTTP_GET) { - DynamicJsonDocument doc(16384); - JsonArray arr = doc.to(); - somfy.toJSONGroups(arr); - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + server.send_P(200, _encoding_json, " "); + this->chunkGroupsResponse(server); + server.sendContent("", 0); } else server.send(404, _encoding_text, _response_404); } @@ -713,27 +789,22 @@ void Web::handleDiscovery(WebServer &server) { HTTPMethod method = apiServer.method(); if (method == HTTP_POST || method == HTTP_GET) { Serial.println("Discovery Requested"); - DynamicJsonDocument doc(min((uint32_t)WEB_MAX_RESPONSE, ESP.getMaxAllocHeap() - 512)); - JsonObject obj = doc.to(); - obj["serverId"] = settings.serverId; - obj["version"] = settings.fwVersion.name; - obj["latest"] = git.latest.name; - obj["model"] = "ESPSomfyRTS"; - obj["hostname"] = settings.hostname; - obj["authType"] = static_cast(settings.Security.type); - obj["permissions"] = settings.Security.permissions; - obj["chipModel"] = settings.chipModel; - if(net.connType == conn_types::ethernet) obj["connType"] = "Ethernet"; - else if(net.connType == conn_types::wifi) obj["connType"] = "Wifi"; - else obj["connType"] = "Unknown"; - JsonArray arrRooms = obj.createNestedArray("rooms"); - somfy.toJSONRooms(arrRooms); - JsonArray arrShades = obj.createNestedArray("shades"); - somfy.toJSONShades(arrShades); - JsonArray arrGroups = obj.createNestedArray("groups"); - somfy.toJSONGroups(arrGroups); - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + // Alright lets chunk our response. + char connType[10] = "Unknown"; + if(net.connType == conn_types::ethernet) strcpy(connType, "Ethernet"); + else if(net.connType == conn_types::wifi) strcpy(connType, "Wifi"); + snprintf(g_content, sizeof(g_content), "{\"serverId\":\"%s\",\"version\":\"%s\",\"latest\":\"%s\",\"model\":\"%s\",\"hostname\":\"%s\",\"authType\":%d,\"permissions\":%d,\"chipModel\":\"%s\",\"connType:\":\"%s\"", + settings.serverId, settings.fwVersion.name, git.latest.name, "ESPSomfyRTS", settings.hostname, static_cast(settings.Security.type), settings.Security.permissions, settings.chipModel, connType); + server.send_P(200, _encoding_json, g_content); + server.sendContent(",\"rooms\":"); + this->chunkRoomsResponse(server); + server.sendContent(",\"shades\":"); + this->chunkShadesResponse(server); + server.sendContent(",\"groups\":"); + this->chunkGroupsResponse(server); + server.sendContent("}"); + server.sendContent("", 0); } else server.send(500, _encoding_text, "Invalid http method"); @@ -768,6 +839,7 @@ void Web::handleBackup(WebServer &server, bool attach) { if (!file) { Serial.println("Error opening shades.cfg"); server.send(500, _encoding_text, "shades.cfg"); + return; } server.streamFile(file, _encoding_text); file.close(); @@ -1988,34 +2060,22 @@ void Web::begin() { Serial.print("Scanned "); Serial.print(n); Serial.println(" networks"); - DynamicJsonDocument doc(16384); - JsonObject obj = doc.to(); - JsonObject connected = obj.createNestedObject("connected"); - connected["name"] = settings.WIFI.ssid; - connected["passphrase"] = settings.WIFI.passphrase; - connected["strength"] = WiFi.RSSI(); - connected["channel"] = WiFi.channel(); - JsonArray arr = obj.createNestedArray("accessPoints"); + // Ok we need to chunk this response as well. + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + // Alright lets chunk our response to this because we cannot allocate all that memory. + snprintf(g_content, sizeof(g_content), "{\"connected\":{\"name\":\"%s\",\"passphrase\":\"%s\",\"strength\":%d,\"channel\":%d},\"accessPoints\":[", + settings.WIFI.ssid, settings.WIFI.passphrase, WiFi.RSSI(), WiFi.channel()); + server.send_P(200, _encoding_json, g_content); + bool bFirst = true; for(int i = 0; i < n; ++i) { if(WiFi.SSID(i).length() == 0 || WiFi.RSSI(i) < -95) continue; // Ignore hidden and weak networks that we cannot connect to anyway. - JsonObject a = arr.createNestedObject(); - a["name"] = WiFi.SSID(i); - a["channel"] = WiFi.channel(i); - a["encryption"] = settings.WIFI.mapEncryptionType(WiFi.encryptionType(i)); - a["strength"] = WiFi.RSSI(i); - a["macAddress"] = WiFi.BSSIDstr(i); + snprintf(g_content, sizeof(g_content), "%s{\"name\":\"%s\",\"channel\":%d,\"encryption\":\"%s\",\"strength\":%d,\"macAddress\":\"%s\"}", + bFirst ? "" : ",", WiFi.SSID(i).c_str(), WiFi.channel(i), settings.WIFI.mapEncryptionType(WiFi.encryptionType(i)).c_str(), WiFi.RSSI(i), WiFi.BSSIDstr(i).c_str()); + server.sendContent(g_content); + bFirst = false; } - serializeJson(doc, g_content); - /* - String content = "{\"connected\": {\"name\":\"" + String(settings.WIFI.ssid) + "\",\"passphrase\":\"" + String(settings.WIFI.passphrase) + "\",\"strength\":" + WiFi.RSSI() + ",\"channel\":" + WiFi.channel() + "}, \"accessPoints\":["; - for (int i = 0; i < n; ++i) { - if (i != 0) content += ","; - content += "{\"name\":\"" + WiFi.SSID(i) + "\",\"channel\":" + WiFi.channel(i) + ",\"encryption\":\"" + settings.WIFI.mapEncryptionType(WiFi.encryptionType(i)) + "\",\"strength\":" + WiFi.RSSI(i) + ",\"macAddress\":\"" + WiFi.BSSIDstr(i) + "\"}"; - delay(10); - } - content += "]}"; - */ - server.send(statusCode, "application/json", g_content); + server.sendContent("]}"); + server.sendContent("", 0); }); server.on("/reboot", []() { webServer.handleReboot(server);}); server.on("/saveSecurity", []() { @@ -2380,7 +2440,6 @@ void Web::begin() { } } }); - server.on("/shadeSortOrder", []() { if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } DynamicJsonDocument doc(512); diff --git a/Web.h b/Web.h index b972cb1..d7bddaf 100644 --- a/Web.h +++ b/Web.h @@ -39,5 +39,10 @@ class Web { bool createAPIPinToken(const IPAddress ipAddress, const char *pin, char *token); bool createAPIPasswordToken(const IPAddress ipAddress, const char *username, const char *password, char *token); bool isAuthenticated(WebServer &server, bool cfg = false); + + void chunkRoomsResponse(WebServer &server, const char *elem = nullptr); + void chunkShadesResponse(WebServer &server, const char *elem = nullptr); + void chunkGroupsResponse(WebServer &server, const char *elem = nullptr); }; + #endif diff --git a/data/index.html b/data/index.html index 1b1a0be..059b439 100644 --- a/data/index.html +++ b/data/index.html @@ -3,11 +3,11 @@ - - - + + + - +
diff --git a/data/index.js b/data/index.js index 7592376..859abc1 100644 --- a/data/index.js +++ b/data/index.js @@ -273,7 +273,6 @@ function getJSON(url, cb) { function getJSONSync(url, cb) { let overlay = ui.waitMessage(document.getElementById('divContainer')); let xhr = new XMLHttpRequest(); - console.log({ get: url }); xhr.responseType = 'json'; xhr.onload = () => { let status = xhr.status; @@ -285,10 +284,12 @@ function getJSONSync(url, cb) { cb(xhr.response, null); } else { + console.log({ get: url, obj:xhr.response }); cb(null, xhr.response); } if (typeof overlay !== 'undefined') overlay.remove(); }; + xhr.onerror = (evt) => { let err = { htmlError: xhr.status || 500, @@ -1956,6 +1957,7 @@ class Somfy { this.setRoomsList(somfy.rooms); this.setShadesList(somfy.shades); this.setGroupsList(somfy.groups); + if (typeof somfy.version !== 'undefined') firmware.procFwStatus(somfy.version); } }); } @@ -2755,7 +2757,7 @@ class Somfy { while (sel.firstChild) sel.removeChild(sel.firstChild); let cm = document.getElementById('divContainer').getAttribute('data-chipmodel'); let pm = this.pinMaps.find(x => x.name === cm) || { name: '', maxPins: 39, inputs: [0, 1, 6, 7, 8, 9, 10, 11, 37, 38], outputs: [3, 6, 7, 8, 9, 10, 11, 34, 35, 36, 37, 38, 39] }; - console.log({ cm: cm, pm: pm }); + //console.log({ cm: cm, pm: pm }); for (let i = 0; i <= pm.maxPins; i++) { if (type.includes('in') && pm.inputs.includes(i)) continue; if (type.includes('out') && pm.outputs.includes(i)) continue;