Skip to content

WiFiController.isConnected() reboot-loops after a mid-session wifi drop instead of just retrying #10

Description

@Ryan4n6

What's happening

The moon-phase Canvas clockface (cw-cf-0x07) connects to wifi fine at boot,
draws its splash screen, then goes dark/unreachable minutes later with no
trace on the network (no DHCP lease, no ARP entry, no mDNS, port 80/3232
closed everywhere on a full subnet sweep). The owner (Kristina) reports the
panel "rebooting" repeatedly, which is what tipped this off.

Root cause

firmware/lib/cw-commons/WiFiController.h, isConnected():

bool isConnected()
{
  if (improvSerial.isConnected()) {
    elapsedTimeOffline = 0;
    return true;
  } else {
    if (elapsedTimeOffline == 0 && !connectionSucessfulOnce)
      elapsedTimeOffline = millis();

    if ((millis() - elapsedTimeOffline) > 1000 * 60 * 5)  // restart if offline 5min
      StatusController::getInstance()->forceRestart();

    return false;
  }
}

elapsedTimeOffline is only ever set to millis() when
!connectionSucessfulOnce — i.e. only on the very first disconnect, before
any successful connection has ever happened. Once connectionSucessfulOnce
flips true (first successful wifi join), it is never set again on
subsequent drops, and stays pinned at 0 for the rest of the device's
uptime (it's only reset back to 0, never re-armed to millis(), on
reconnect).

So the "restart after 5 minutes offline" check silently becomes
millis() - 0 > 5min — i.e. total device uptime, not time-since-drop.
Once the device has been up longer than 5 minutes, any wifi drop after that
point causes forceRestart() on essentially the next disconnected loop
iteration, not after a real 5-minute grace period.

Impact

On a marginal/flaky wifi signal, this turns "briefly drop and reconnect"
into a hard crash-loop: boot → connect just long enough to run setup()
drop → immediate restart → repeat. Each connected window can be too short
for OTA, the local settings webserver (CWWebServer.h, port 80), or even a
DHCP/ARP scan to catch it. From the outside the device looks completely
absent from the network, even though it's actively cycling.

Suggested fix

Track elapsed-time-offline per disconnect, not gated on
connectionSucessfulOnce:

bool isConnected()
{
  if (improvSerial.isConnected()) {
    elapsedTimeOffline = 0;
    return true;
  } else {
    if (elapsedTimeOffline == 0)
      elapsedTimeOffline = millis();

    if ((millis() - elapsedTimeOffline) > 1000 * 60 * 5)
      StatusController::getInstance()->forceRestart();

    return false;
  }
}

i.e. drop the && !connectionSucessfulOnce guard entirely, so the timer
arms on every transition into the disconnected state, not just the first
one. This gives a real 5-minute grace period on every drop instead of only
the first.

Checklist

  • Confirm the crash-loop theory by physically observing the panel for
    60s+ after a fresh power cycle (reboot / blackout pattern visible?)
  • Apply the isConnected() fix above
  • Also fix the unrelated but adjacent bug this session hit: Canvas
    clockface (cw-cf-0x07) crashed on boot from a 32KB static
    DynamicJsonDocument faulting static init — already shrunk as a
    live patch, not yet committed/merged
  • Flash via USB (OTA unreliable while the device is actively
    crash-looping) and confirm stable reconnect behavior on a forced
    wifi drop
  • Once stable, set canvasServer / canvasFile via the local
    settings page (/, port 80) to point at the moon-canvas Cloudflare
    Worker so the moon-phase clockface actually renders instead of
    showing "Params weren't set"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions