Skip to content

fix: stop the 5 s boot wait, tell the client about a bad restore - #69

Merged
cjkas merged 1 commit into
mainfrom
fix/boot-clock-restore-logs
Oct 19, 2026
Merged

cjkas merged 1 commit into
mainfrom
fix/boot-clock-restore-logs

Conversation

@cjkas

@cjkas cjkas commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes from the issues in the 2026-10-18/19 serial logs (#68).

Boot clock. Each cold boot waited 5 s and then skipped the time zone.

 NTPSettings::apply()
-  configTime(0, 0, ntpServer)
-  if !getLocalTime(&dt)        # waits up to 5000 ms, before the network is up
-    return false               # ...and returns before TZ is set
-  setenv("TZ", posixZone)
+  configTzTime(posixZone, ntpServer)   # sets TZ, starts SNTP, doesn't wait
 NTPSettings::save()
-  (the same wait, on the web server task)
+  return apply()
 /setgeneral
-  if ntpServer || ntpServer
+  if ntpServer || posixZone

Restore. Today a bad file still reboots the device, and nobody is told.

 POST /restore (upload done)
+  if upload incomplete          -> 400 "The upload didn't complete…"   (was: no reply)
+  if !validateFile(shades.tmp)  -> delete it, 400 "This isn't a valid backup file…"
   queue restore_shades
 loop: restore_shades
-  restore(); reboot
+  ok = restore(); delete shades.tmp
+  if ok reboot, else log and stay up     # a failed restore changes nothing
 loop: load_shades (/updateShadeConfig)
+  delete shades.tmp afterwards
 restore dialog (index.js)
+  non-2xx reply -> show its desc

Backup header. The 6 extra bytes in that backup came from TRANS_REC_SIZE. It stayed at 74 when noiseDetection was added to the transceiver record, which is 80 bytes. So every backup since then has understated that record by 6 bytes.

-#define TRANS_REC_SIZE 74
+#define TRANS_REC_SIZE 80
 validate()
-  size sum inline; mismatch -> ESP_LOGE, carry on
+  expectedFileBytes(header)  (ConfigHeader.h, tested); mismatch -> ESP_LOGW, carry on
+  repeater record length checked -> ESP_LOGW only

The checks stay warnings on purpose. A strict size check would reject every existing backup. validate() also runs on shades.cfg at boot, and failing it there would start the device with no shades.

Logs. Commands for an unknown shade, tilt or group now log ... not found. The socket disconnect line says never pinged instead of last ping 0ms ago.

Docs. KNOWN_ISSUES.md marks these issues fixed, and corrects the "365 ms loop" entry with the real cause. The radio_tx task busy-waits each frame at priority 5 on loop()'s core; the fix would be sending with RMT.

Evidence

  • test_configheader, using the real backup's header:
    expectedFileBytes(header with trans 74) == 3647   # what validate() wanted
    expectedFileBytes(header with trans 80) == 3653   # the file's actual size
    version 10/13/14/19/21 only count their own records
    
  • Native tests: 407/407.
  • Local builds: esp32dev, esp32devdbg, esp32c3 and esp32s3. esp32c6 doesn't build locally, because pioarduino fails to install its Python dependencies.
  • The cause of the 6 bytes: the backup still on the device was split into records. Every record matched its declared size except the transceiver record (80 on disk, 74 in the header).
  • Not done yet: device checks.
    • Cold boot and soft reboot: no 5 s gap between File system mounted and Launching web server.
    • Restore a truncated backup: the dialog shows the error, the device stays up, shades.tmp is gone.
    • Restore a fresh backup: reboots, shades restored, no shades.tmp in the boot file list.
    • shadeCommand with shadeId=99: not found warning in the log.

Merge Danger

Door: two-way

Every change reverts cleanly. New backups declare an 80-byte transceiver record. Older firmware reads that record field by field and then skips to the record end, so a new backup still restores on older firmware.

Blast Radius: boot, restore

  • NTPSettings::apply() runs on every boot.
  • validate() runs on shades.cfg at every boot, but nothing that used to pass validation can fail it now.
  • The index.js change only reaches the device with a new file-system image.

🤖 Generated with Claude Code

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>
@cjkas
cjkas merged commit b74f382 into main Oct 19, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant