mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
fix: stop the 5 s boot wait, tell the client about a bad restore
NTPSettings::apply() waited up to 5 s in getLocalTime() for a clock that can't be set before the network is up, then returned before it set TZ. Every cold boot was 5 s slower and ran without its time zone. It now calls configTzTime(), which sets the zone and starts SNTP without waiting, and save() goes through apply(). /setgeneral checked ntpServer twice, so a request that changed only the zone was ignored. A restore ignored the result of ShadeConfigFile::restore() and rebooted either way, so a bad file failed with no word to the user. The /restore reply now validates the file and answers 400, which the restore dialog shows, and an incomplete upload gets a 400 instead of no reply. A failed restore changes nothing, so loop() no longer reboots after one. /shades.tmp, a full copy of the backup with its rolling codes, is deleted after every restore and after /updateShadeConfig. The 6 extra bytes in the 2026-10-18 backup: TRANS_REC_SIZE stayed 74 when noiseDetection was added to the 80-byte transceiver record, so every backup since declares it short. It is 80 now. The size check stays a warning so those backups still restore, and the sum moves to expectedFileBytes() with tests. The repeater record length is checked as a warning only, since validate() also runs on shades.cfg at boot. Commands for an unknown shade, tilt or group now log a warning, and a socket that never pinged says so. KNOWN_ISSUES.md is updated, including the real cause of the ~365 ms loop stall (radio_tx busy-waits each frame on loop()'s core). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4b75e8fb08
commit
953d4dc6f7
11 changed files with 168 additions and 77 deletions
|
|
@ -13,7 +13,10 @@ static const char *TAG = "ConfigFile";
|
|||
#define SHADE_HDR_SIZE 76
|
||||
#define SHADE_REC_SIZE 276
|
||||
#define GROUP_REC_SIZE 200
|
||||
#define TRANS_REC_SIZE 74
|
||||
// writeTransRecord: enabled, 8 one-byte fields, 3 floats, txPower and noiseDetection.
|
||||
// It was left at 74 when noiseDetection was added, so files written before
|
||||
// the fix declare this record 6 bytes short.
|
||||
#define TRANS_REC_SIZE 80
|
||||
#define ROOM_REC_SIZE 29
|
||||
#define REPEATER_REC_SIZE 77
|
||||
|
||||
|
|
@ -436,20 +439,11 @@ bool ShadeConfigFile::validate() {
|
|||
return false;
|
||||
}
|
||||
|
||||
// We should know the file size based upon the record information in the header
|
||||
uint32_t fsize = this->header.length + (this->header.shadeRecordSize * this->header.shadeRecords);
|
||||
if(this->header.version > 10) fsize += (this->header.groupRecordSize * this->header.groupRecords);
|
||||
if(this->header.version >= 19) fsize += (this->header.roomRecordSize * this->header.roomRecords);
|
||||
if(this->header.version > 13) {
|
||||
fsize += (this->header.settingsRecordSize);
|
||||
fsize += (this->header.netRecordSize);
|
||||
fsize += (this->header.transRecordSize);
|
||||
}
|
||||
if(this->header.version >= 21) {
|
||||
fsize += (this->header.repeaterRecordSize * this->header.repeaterRecords);
|
||||
}
|
||||
// Only a warning: older backups declare the transceiver record 6 bytes
|
||||
// short (see TRANS_REC_SIZE), and the record checks below catch real damage.
|
||||
const uint32_t fsize = expectedFileBytes(this->header);
|
||||
if(this->file.size() != fsize) {
|
||||
ESP_LOGE(TAG, "File size is not correct should be %d and got %d", fsize, this->file.size());
|
||||
ESP_LOGW(TAG, "File is %u bytes but its header adds up to %u", (unsigned)this->file.size(), (unsigned)fsize);
|
||||
}
|
||||
// Next check to see if the records match the header length.
|
||||
uint8_t recs = 0;
|
||||
|
|
@ -499,12 +493,16 @@ bool ShadeConfigFile::validate() {
|
|||
if(this->header.version >= 21) {
|
||||
recs = 0;
|
||||
while(recs < this->header.repeaterRecords) {
|
||||
//uint32_t pos = this->file.position();
|
||||
// Only logged: this also runs on shades.cfg at boot, and failing it
|
||||
// there would start the device without its shades.
|
||||
uint32_t pos = this->file.position();
|
||||
if(!this->seekChar(CFG_REC_END)) {
|
||||
ESP_LOGE(TAG, "Failed to find the repeater record end %d", recs);
|
||||
}
|
||||
else if(this->file.position() - pos != this->header.repeaterRecordSize) {
|
||||
ESP_LOGW(TAG, "Repeater record length is %d and should be %d", this->file.position() - pos, this->header.repeaterRecordSize);
|
||||
}
|
||||
recs++;
|
||||
|
||||
}
|
||||
}
|
||||
this->file.seek(startPos, SeekSet);
|
||||
|
|
@ -519,6 +517,14 @@ bool ShadeConfigFile::load(SomfyShadeController *s, const char *filename) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
bool ShadeConfigFile::validateFile(const char *filename) {
|
||||
if(!LittleFS.exists(filename)) return false;
|
||||
ShadeConfigFile file;
|
||||
file.begin(filename, true);
|
||||
const bool valid = file.validate();
|
||||
file.end();
|
||||
return valid;
|
||||
}
|
||||
bool ShadeConfigFile::restore(SomfyShadeController *s, const char *filename, restore_options_t &opts) {
|
||||
ShadeConfigFile file;
|
||||
if(file.begin(filename, true)) {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ class ShadeConfigFile : public ConfigFile {
|
|||
static bool exists();
|
||||
static bool load(SomfyShadeController *somfy, const char *filename = "/shades.cfg");
|
||||
static bool restore(SomfyShadeController *somfy, const char *filename, restore_options_t &opts);
|
||||
// Whether the file would pass the checks restore() starts with. Only
|
||||
// reads the file, so any task can call it.
|
||||
static bool validateFile(const char *filename);
|
||||
bool begin(const char *filename, bool readOnly = false);
|
||||
bool begin(bool readOnly = false);
|
||||
bool save(SomfyShadeController *somfy);
|
||||
|
|
|
|||
|
|
@ -30,4 +30,16 @@ inline uint32_t shadeModelBytes(const config_header_t &header) {
|
|||
+ static_cast<uint32_t>(header.shadeRecords) * header.shadeRecordSize
|
||||
+ static_cast<uint32_t>(header.groupRecords) * header.groupRecordSize;
|
||||
}
|
||||
// The file size the header adds up to, counting only the records its version
|
||||
// has. Backups written before the transceiver record's size was corrected
|
||||
// declare it 6 bytes short, so a mismatch alone doesn't make a file invalid.
|
||||
inline uint32_t expectedFileBytes(const config_header_t &header) {
|
||||
uint32_t bytes = static_cast<uint32_t>(header.length)
|
||||
+ static_cast<uint32_t>(header.shadeRecords) * header.shadeRecordSize;
|
||||
if(header.version > 10) bytes += static_cast<uint32_t>(header.groupRecords) * header.groupRecordSize;
|
||||
if(header.version >= 19) bytes += static_cast<uint32_t>(header.roomRecords) * header.roomRecordSize;
|
||||
if(header.version > 13) bytes += header.settingsRecordSize + header.netRecordSize + header.transRecordSize;
|
||||
if(header.version >= 21) bytes += static_cast<uint32_t>(header.repeaterRecords) * header.repeaterRecordSize;
|
||||
return bytes;
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -375,11 +375,7 @@ bool NTPSettings::save() {
|
|||
pref.putString("ntpServer", this->ntpServer);
|
||||
pref.putString("posixZone", this->posixZone);
|
||||
pref.end();
|
||||
struct tm dt;
|
||||
configTime(0, 0, this->ntpServer);
|
||||
if(!getLocalTime(&dt)) return false;
|
||||
setenv("TZ", this->posixZone, 1);
|
||||
return true;
|
||||
return this->apply();
|
||||
}
|
||||
bool NTPSettings::load() {
|
||||
Preferences pref;
|
||||
|
|
@ -402,11 +398,11 @@ bool NTPSettings::toJSON(JsonObject &obj) {
|
|||
obj["posixZone"] = this->posixZone;
|
||||
return true;
|
||||
}
|
||||
bool NTPSettings::apply() {
|
||||
struct tm dt;
|
||||
configTime(0, 0, this->ntpServer);
|
||||
if(!getLocalTime(&dt)) return false;
|
||||
setenv("TZ", this->posixZone, 1);
|
||||
bool NTPSettings::apply() {
|
||||
// Sets the zone and starts SNTP, which sets the clock once the network is
|
||||
// up. Waiting here with getLocalTime() held up boot by 5 s before the
|
||||
// network existed, and then returned before the zone was set.
|
||||
configTzTime(this->posixZone, this->ntpServer);
|
||||
return true;
|
||||
}
|
||||
IPSettings::IPSettings() {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/queue.h>
|
||||
#include <LittleFS.h>
|
||||
#include "esp_log.h"
|
||||
#include "LoopQueue.h"
|
||||
#include "ConfigFile.h"
|
||||
|
|
@ -55,7 +56,15 @@ void LoopQueue::drain() {
|
|||
break;
|
||||
case loop_item_type_t::restore_shades: {
|
||||
restore_options_t opts = restoreOptions(item.restoreParts);
|
||||
ShadeConfigFile::restore(&somfy, "/shades.tmp", opts);
|
||||
const bool restored = ShadeConfigFile::restore(&somfy, "/shades.tmp", opts);
|
||||
// It holds the whole backup, rolling codes included, and has no use now.
|
||||
LittleFS.remove("/shades.tmp");
|
||||
// A failed restore stops before it changes anything, so there is
|
||||
// nothing to reboot for.
|
||||
if(!restored) {
|
||||
ESP_LOGE(TAG, "Restore failed, keeping the current settings");
|
||||
break;
|
||||
}
|
||||
ESP_LOGI(TAG, "Rebooting ESP for restored settings...");
|
||||
rebootDelay.reboot = true;
|
||||
rebootDelay.rebootTime = millis() + 1000;
|
||||
|
|
@ -63,6 +72,7 @@ void LoopQueue::drain() {
|
|||
}
|
||||
case loop_item_type_t::load_shades:
|
||||
somfy.loadShadesFile("/shades.tmp");
|
||||
LittleFS.remove("/shades.tmp");
|
||||
break;
|
||||
case loop_item_type_t::snapshot_fill:
|
||||
snapshotSlots.fill(item.slot, fillSnapshot);
|
||||
|
|
|
|||
|
|
@ -441,7 +441,10 @@ void Web::handleShadeCommand(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}"));
|
||||
else {
|
||||
ESP_LOGW(TAG, "handleShadeCommand shade=%u not found", req.id);
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}"));
|
||||
}
|
||||
}
|
||||
void Web::handleGroupCommand(AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
|
|
@ -474,7 +477,10 @@ void Web::handleGroupCommand(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group with the specified id not found.\"}"));
|
||||
else {
|
||||
ESP_LOGW(TAG, "handleGroupCommand group=%u not found", req.id);
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group with the specified id not found.\"}"));
|
||||
}
|
||||
}
|
||||
void Web::handleTiltCommand(AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
|
|
@ -505,7 +511,10 @@ void Web::handleTiltCommand(AsyncWebServerRequest *request, JsonVariant &json) {
|
|||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}"));
|
||||
else {
|
||||
ESP_LOGW(TAG, "handleTiltCommand shade=%u not found", req.id);
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}"));
|
||||
}
|
||||
}
|
||||
void Web::handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
|
|
|
|||
|
|
@ -257,6 +257,13 @@ void registerWebUiRoutes(AsyncWebServer &server) {
|
|||
ESP_LOGD(TAG, "No restore options sent. Using defaults...");
|
||||
opts.shades = true;
|
||||
}
|
||||
// Checked here so the client hears about a bad file. It only reads
|
||||
// the file; the shades are still changed on loop() alone.
|
||||
if(!ShadeConfigFile::validateFile("/shades.tmp")) {
|
||||
LittleFS.remove("/shades.tmp");
|
||||
request->send(400, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"This isn't a valid backup file, so nothing was restored.\"}"));
|
||||
return;
|
||||
}
|
||||
// The main loop restores, since it owns the shades and their rolling
|
||||
// codes, and then reboots.
|
||||
loop_item_t item;
|
||||
|
|
@ -265,6 +272,7 @@ void registerWebUiRoutes(AsyncWebServer &server) {
|
|||
if(!queueLoopItem(request, item)) return;
|
||||
request->send(200, _encoding_json, "{\"status\":\"Success\",\"desc\":\"Restoring Shade settings\"}");
|
||||
}
|
||||
else request->send(400, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"The upload didn't complete, so nothing was restored.\"}"));
|
||||
},
|
||||
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
esp_task_wdt_reset();
|
||||
|
|
@ -759,7 +767,7 @@ void registerWebUiRoutes(AsyncWebServer &server) {
|
|||
if(settings.checkForUpdate != checkForUpdate) git.emitUpdateCheck();
|
||||
if(!obj["hostname"].isNull()) net.updateHostname();
|
||||
}
|
||||
if(!obj["ntpServer"].isNull() || !obj["ntpServer"].isNull()) {
|
||||
if(!obj["ntpServer"].isNull() || !obj["posixZone"].isNull()) {
|
||||
settings.NTP.fromJSON(obj);
|
||||
settings.NTP.save();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,8 +200,13 @@ void SocketEmitter::wsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client
|
|||
{
|
||||
uint8_t slot = mapClientId(asyncId);
|
||||
if(slot != 255) {
|
||||
ESP_LOGI(TAG, "Socket [%lu] Disconnected (slot %u) after %lums, last ping %lums ago", asyncId, slot,
|
||||
(unsigned long)(millis() - slots[slot].connectedAt), slots[slot].lastPing ? (unsigned long)(millis() - slots[slot].lastPing) : 0UL);
|
||||
// Browsers don't ping, so "never" is normal for them.
|
||||
if(slots[slot].lastPing)
|
||||
ESP_LOGI(TAG, "Socket [%lu] Disconnected (slot %u) after %lums, last ping %lums ago", asyncId, slot,
|
||||
(unsigned long)(millis() - slots[slot].connectedAt), (unsigned long)(millis() - slots[slot].lastPing));
|
||||
else
|
||||
ESP_LOGI(TAG, "Socket [%lu] Disconnected (slot %u) after %lums, never pinged", asyncId, slot,
|
||||
(unsigned long)(millis() - slots[slot].connectedAt));
|
||||
for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++)
|
||||
sockEmit.rooms[i].leave(slot);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue