# WiFi Notes: IdeaSpark ESP32 + Orbi

Hard-won lessons from debugging WiFi on the IdeaSpark ESP32 1.9" ST7789 board.
Kept here so the next debugging session starts with answers, not questions.

---

## The board is 2.4 GHz only

The ESP32-D0WD chip (verified via esptool: `Chip type: ESP32-D0WD revision v1.0`)
supports only 802.11 b/g/n on 2.4 GHz. It cannot see or connect to 5 GHz networks.

**On Orbi (and most mesh routers) this is a trap.** The default SSID (e.g. `ORBI02`)
is a "Smart Connect" SSID that broadcasts on both bands simultaneously. The ESP32 can
see and scan it because the 2.4 GHz radio is part of that SSID — but the router's band
steering logic may reject or redirect association attempts from capable-looking devices,
causing repeated `status=5/connection_lost` or `status=4/connect_failed` without ever
fully connecting.

Creating a separate 2.4 GHz-only SSID in the Orbi admin panel is the clean fix.
Alternatively, disabling band steering for this device's MAC address works too.

---

## Serial status codes and what they actually mean

The firmware logs `status=N/name` on every reconnect attempt. Reference:

| Code | Name | What it means in practice |
|---:|---|---|
| 0 | idle | Driver is transitioning — check again in a moment |
| 1 | no_ssid | SSID not found in scan — wrong name, wrong band, or AP too far |
| 3 | connected | Fully connected with IP |
| 4 | connect_failed | AP rejected the association — wrong password, MAC filter, or band steering |
| 5 | connection_lost | Associated briefly then dropped — DHCP timing, band steering, or marginal signal |
| 6 | disconnected | Not associated — scan found SSID but couldn't start handshake |
| 254 | driver_not_ready | Radio powered down — seen after `WiFi.disconnect(true)` |
| 255 | no_shield | Driver not yet initialised — transient on cold boot |

**status=1 (no_ssid)** almost always means the wrong SSID, not a range problem — if you
can see other networks, you're in range.

**status=5 (connection_lost) recurring** is the Orbi band-steering symptom. The device
gets through the 802.11 handshake but the router drops it before DHCP completes.

**status=254 (driver_not_ready)** on reconnect attempt 1 after a failed `connectWiFi()`
was a firmware bug: `WiFi.disconnect(true)` powers down the radio and leaves it in
state 254. Fixed by using `WiFi.disconnect(false)` at the end of `connectWiFi()` so the
radio stays powered between setup() failure and the first loop reconnect attempt.

---

## DHCP reliability: reserve a static IP

The most reliable long-term fix is a DHCP reservation in the router, so the ESP32
always gets the same IP and DHCP race conditions are eliminated.

**ESP32 MAC address: `10:52:1C:7B:11:C4`**

In Orbi admin (`http://orbilogin.com`): Advanced → Setup → LAN Setup → Address
Reservation → Add. Pick any free IP in your subnet and bind it to the MAC above.

Once the lease is reserved you can optionally hardcode it in `config.h` to skip DHCP
entirely (faster connect, zero DHCP dependency):

```cpp
// Optional: skip DHCP by setting a static IP.
// Leave all four as INADDR_NONE to use DHCP (default).
#define STATIC_IP      "192.168.x.y"   // your reserved address
#define STATIC_GATEWAY "192.168.x.1"
#define STATIC_SUBNET  "255.255.255.0"
#define STATIC_DNS     "8.8.8.8"
```

Then call `WiFi.config(...)` before `WiFi.begin()` in `beginWiFiAttempt()`.

---

## The firmware's reconnect strategy

The WiFi section of `codexbar_esp32.ino` implements a layered reconnect strategy.
Here is why each non-obvious piece exists:

### `WiFi.setAutoReconnect(false)`
The ESP32 SDK's built-in auto-reconnect races with explicit `WiFi.begin()` calls and
emits "sta is connecting, cannot set config" errors. We own all reconnects explicitly.

### `WiFi.disconnect(true)` + `WiFi.mode(WIFI_OFF)` → `WiFi.mode(WIFI_STA)`
Full radio reset sequence before every join attempt. Without the `WIFI_OFF` → `WIFI_STA`
cycle, stale association state from the previous attempt can prevent the new `begin()`
from starting a clean scan.

### `WiFi.disconnect(false)` at the end of `connectWiFi()`
If the initial connect in `setup()` fails, we want the radio to stay powered so the loop
reconnect starts from a normal disconnected state. Using `disconnect(true)` here would
power down the radio to state 254, and the driver-wait in `connectWiFi()` only ran on
the next call — burning an extra settle cycle before the first reconnect attempt.

### Driver-wait for status 254 and 255
Both `WL_NO_SHIELD` (255) and `driver_not_ready` (254) indicate the radio is still
powering up. We wait up to 2 s for either to clear before starting `beginWiFiAttempt()`.

### Exponential backoff: 5 → 10 → 20 → 40 → 60 s (cap)
Short initial delays recover quickly after a brief AP hiccup (router rebooting, DHCP
stutter). The 60 s cap prevents the device spinning constantly when the AP is genuinely
down for an extended period.

### Radio power-cycle every 5 failures
A full `WIFI_OFF` → settle → `WIFI_STA` cycle at the OS level, not just a driver reset.
Clears state the driver-level disconnect cannot reach. Logs as `[WiFi] Power-cycling radio...`

### Soft reboot after 30 failures (`esp_restart()`)
After ~30 minutes of consecutive failures a full MCU reset clears any persistent WiFi
driver state that radio power-cycling alone cannot fix. The reboot logs as:
```
[WiFi] 30 consecutive failures — rebooting to clear driver state
```
and the next boot shows `Reset reason: 3/software`. High threshold ensures a normal
AP outage never triggers this.

### `justConnectedMs` fast-retry window
Immediately after WiFi reconnects, the device retries `/usage` every 5 s for up to
60 s. This recovers data quickly when the Mac companion wakes slightly after the ESP32
reconnects, without waiting the full 5-minute poll interval.

---

## Reusing this logic in other ESP32 projects

The reconnect strategy above is general-purpose for any ESP32 Arduino project over
a home WiFi network. The key pieces to copy:

1. `beginWiFiAttempt(bool resetRadio)` — full radio teardown + begin
2. `settleWiFiAfterConnect()` — wait for valid IP before first HTTP call
3. `connectWiFi()` — setup-time connect with driver-wait
4. Loop reconnect block — backoff, power-cycle, reboot threshold
5. `wifiStatusName()` — human-readable status for serial logs

The `#define` tuning knobs (`WIFI_ATTEMPT_TIMEOUT_MS`, `WIFI_RETRY_SECS_MIN/MAX`,
`WIFI_RADIO_RESET_EVERY`, `WIFI_REBOOT_AFTER_FAILURES`) can be adjusted per project
without touching the logic.
