REFERENCE · GOING FURTHER

Going Further with the ESP32

Four capabilities none of the 13 builds use, kept off the main path so day one stays simple.

Each of these is a real pattern you can drop into any project in this kit once the basics are second nature: deep sleep for battery life, Wi‑Fi resilience for a device that isn't tethered to a lab bench, on‑board storage that survives a reboot, and real timestamps instead of "milliseconds since boot".

1Deep sleep & power

Foundation 2 introduced deep sleep as a concept: shut almost everything down, wake on a timer, an external pin, or touch. Here's what actually using it looks like.

Sleep and wake

#include <esp_sleep.h>

RTC_DATA_ATTR int bootCount = 0;   // survives deep sleep, unlike a normal variable

void setup() {
  Serial.begin(115200);
  delay(500);

  bootCount++;
  Serial.printf("Boot number: %d\n", bootCount);
  Serial.printf("Wakeup cause: %d\n", esp_sleep_get_wakeup_cause());

  esp_sleep_enable_timer_wakeup(60 * 1000000ULL);   // wake in 60 seconds (microseconds)
  Serial.println("Going to sleep now...");
  esp_deep_sleep_start();   // never returns: the chip restarts from setup() on wake
}

void loop() {
  // never reached: setup() sleeps before loop() would run
}

RTC_DATA_ATTR places a variable in the small pocket of RAM that survives deep sleep (the RTC domain), the only place state can live across a sleep cycle: everything else, including every normal variable in loop(), resets. esp_sleep_get_wakeup_cause() tells you why the chip woke up (timer, external pin, touch, or a normal power‑on), which matters once a device wakes for more than one reason.

Waking on a GPIO instead of a timer

esp_sleep_enable_ext0_wakeup(GPIO_NUM_27, 1);   // wake when GPIO 27 goes HIGH

This is the PIR from Project 4, turned into a wake source instead of something you poll: the board sleeps at roughly 10 µA until motion actually happens, instead of burning power in loop() checking a pin that is usually LOW. ext0 wakeup only works on the ESP32's RTC‑capable pins, which is why GPIO 27 (also usable as ADC2 and touch T7) is the right choice here.

Why it matters

A project powered from a battery instead of USB lives on milliamp‑hours. Active current for this kit's projects runs tens of milliamps; deep sleep drops that to roughly 10 µA, thousands of times less. A device that sleeps between readings and wakes briefly to publish can run for weeks on a battery that would last hours running flat out.

2Wi-Fi resilience

Every Wi‑Fi project in this kit (5 onward) assumes the network is always there. Real deployments don't get that luxury: routers reboot, students walk out of range, access points get congested. None of the 13 builds handle this beyond a bare reconnect check, on purpose (it would be one more thing to debug on day one), but here's the pattern once you're ready.

Reacting to connection events

#include <WiFi.h>

void onWiFiEvent(WiFiEvent_t event) {
  switch (event) {
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
      Serial.println("[WiFi] Lost connection");
      break;
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      Serial.println("[WiFi] Connected, IP: " + WiFi.localIP().toString());
      break;
    default:
      break;
  }
}

void setup() {
  WiFi.onEvent(onWiFiEvent);
  WiFi.begin(ssid, password);
}

WiFi.onEvent() registers a callback the core calls the instant the radio's state changes, the same inversion‑of‑control idea as Project 11's MQTT callback(). It's how you find out a connection dropped immediately, instead of noticing next time you happen to check WiFi.status().

Reconnecting with backoff

Retrying WiFi.begin() every loop pass the instant a connection drops can make a struggling router's problem worse. Project 11's Going Further section shows the same backoff pattern applied to MQTT reconnects; it applies identically here:

unsigned long lastAttempt = 0;
unsigned long backoff = 2000;   // start at 2 s, cap at 30 s

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    if (millis() - lastAttempt > backoff) {
      lastAttempt = millis();
      WiFi.reconnect();
      backoff = min(backoff * 2, 30000UL);
    }
  } else {
    backoff = 2000;
  }
}

3Persistent storage (NVS)

Every sketch in this kit forgets everything on power loss: ledState, the last relay position, whatever a switch was set to. The ESP32 sets aside a small partition of flash for exactly this, called NVS (Non‑Volatile Storage), and the Arduino core wraps it in a library called Preferences.

#include <Preferences.h>

Preferences prefs;

void setup() {
  prefs.begin("my-app", false);            // a namespace, false = read/write
  int lastState = prefs.getInt("led", 0);  // 0 is the default if never saved
  digitalWrite(ledPin, lastState);
  prefs.end();
}

void saveState(int state) {
  prefs.begin("my-app", false);
  prefs.putInt("led", state);
  prefs.end();
}

Call saveState() whenever the value you care about changes (in Project 8's terms, right next to whatever sets ledState), and the board remembers it across a reboot or a power cut, the way Project 8's dashboard already remembers state across a browser refresh. Preferences stores simple key/value pairs (Int, Float, String, Bool, and a few others), which covers most of what a small device needs to remember: the last output state, a saved setting, a calibration value.

Flash has a write‑cycle limit (roughly 100,000 per sector). Writing on every sensor reading would wear it out in weeks; writing on every meaningful change (a button press, a command from the cloud) is the right granularity.

4NTP time

millis() only ever tells you how long the board has been running, not what time it actually is. For real timestamps, a project needs an outside source, which is exactly what NTP (Network Time Protocol) provides once the board has Wi‑Fi.

#include <WiFi.h>
#include <time.h>

void setup() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);

  configTime(0, 0, "pool.ntp.org");   // UTC (gmtOffset=0, daylightOffset=0)

  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    Serial.println(&timeinfo, "%Y-%m-%d %H:%M:%S");
  }
}

configTime() starts a background sync against a public NTP server (pool.ntp.org is a free, shared one), and getLocalTime() fills in a standard C struct tm once the sync succeeds, usually within a second or two. The two zero arguments are the UTC offset and daylight‑saving offset in seconds; leave them at 0 for UTC and adjust for a local zone if a project needs one.

Real timestamps are what turn "a reading" into "a reading at 14:32", which matters the moment two devices need to agree on ordering, or a log needs to survive being read a week later. Project 11's Ubidots dashboard already timestamps everything server‑side when it receives a publish; NTP is what lets the board itself know the same thing, useful for local logging (Project 10's OLED, or Serial) without a cloud round trip.

🎯 Knowledge Check

1. What happens when `esp_deep_sleep_start()` is called?

2. How do you reconnect to Wi-Fi with exponential backoff?

3. What does `Preferences` library provide?

4. What does `configTime(0, 0, "pool.ntp.org")` do?