Learning Goals 5 min
Your ESP has WiFi but no idea what time it is. millis() counts from boot, not from midnight. NTP (Network Time Protocol) gives any internet-connected device the current real-world time, accurate to milliseconds, for free, from public servers. No RTC chip needed. By the end of this lesson you will:
- Use the built-in
configTime()on ESP cores to sync to public NTP pool servers. - Format the current local time as a human-readable string ("Sun 2026-05-26 19:30:00") using standard C
time.hfunctions. - Set up a time zone and daylight saving rules with POSIX TZ strings — the same format Linux servers use.
Warm-Up 10 min
No new wiring. Just the ESP + USB. We'll use the WiFi connection from yesterday.
Why NTP instead of an RTC chip
| Approach | Pros | Cons |
|---|---|---|
| RTC chip (DS3231) | Works offline, battery-backed, < 1 min drift / year | Extra hardware, ~£3, needs setting once via a button or computer |
| NTP over WiFi | Free, accurate, no hardware, automatic time-zone handling | Requires internet, takes ~1 s to sync at boot |
For a WiFi-connected device, NTP wins. For an offline data logger, RTC. Some pro projects use both: RTC as the always-available clock, NTP to correct RTC drift weekly.
How NTP works (very briefly)
The device sends a UDP packet to a public NTP server asking "what time is it?". The server replies with its current time (in UTC, seconds since 1900-01-01). The device's library applies a small adjustment for the round-trip time and uses the result to set the internal clock. Accuracy of ~10 ms over the public internet is typical.
New Concept · configTime + C time functions 25 min
The one-line setup
#include <time.h>
configTime(0, 0, "pool.ntp.org", "time.nist.gov");Three arguments: GMT offset in seconds, daylight saving offset in seconds, then up to three NTP server hostnames. pool.ntp.org is the global community-run pool; for school use it's perfect.
The first call kicks off background sync. It doesn't block — your code returns immediately. After ~1 s the system clock will have a valid time.
Wait for sync
time_t now = time(nullptr);
while (now < 24 * 3600) {
delay(200);
now = time(nullptr);
Serial.print(".");
}
Serial.println(" synced");Before sync, time() returns small numbers (seconds since boot, basically). We wait until it crosses 24 hours' worth of seconds — well past any reasonable boot count, well below the real Unix timestamp (which is > 1.7 billion as of 2024).
Get the current time in pieces
time_t now = time(nullptr);
struct tm tmNow;
localtime_r(&now, &tmNow);
Serial.printf("%04d-%02d-%02d %02d:%02d:%02d\n",
tmNow.tm_year + 1900,
tmNow.tm_mon + 1,
tmNow.tm_mday,
tmNow.tm_hour,
tmNow.tm_min,
tmNow.tm_sec);localtime_r converts a Unix timestamp to a broken-down struct tm. Notice tm_year is years-since-1900 and tm_mon is 0-indexed — both classic C gotchas.
Time zones with POSIX TZ strings
If you live in a UTC+8 zone with no daylight saving (Malaysia, Singapore, China), set:
setenv("TZ", "MYT-8", 1);
tzset();The format is NAME[offset_with_inverted_sign][DST_NAME[,DST_RULES]]. So MYT-8 = "name MYT, offset minus -8 hours from UTC" = UTC+8. (Yes, the sign is backwards from how you'd say it — that's the POSIX convention.)
For a zone with DST (e.g. London):
setenv("TZ", "GMT0BST,M3.5.0/1,M10.5.0/2", 1);
tzset();Reads: "GMT in winter, BST in summer; DST starts on the last Sunday of March at 1 AM and ends on the last Sunday of October at 2 AM." The full reference is the POSIX TZ spec — Google the format for your zone.
Format with strftime
char buf[32];
strftime(buf, sizeof(buf), "%a %Y-%m-%d %H:%M:%S", &tmNow);
Serial.println(buf); // "Sun 2026-05-26 19:30:00"strftime is the standard C function for date-time formatting. Format codes:
| Code | Meaning |
|---|---|
%Y | 4-digit year |
%m | Month (01–12) |
%d | Day of month (01–31) |
%H / %M / %S | Hour (00–23) / minute / second |
%a / %A | Weekday short / long ("Sun" / "Sunday") |
%Z | Time zone abbreviation |
Drift and re-sync
The ESP's internal crystal drifts by ~10–50 ppm — a few seconds per day. The system automatically re-syncs every ~1 hour by default (configurable). Good enough for any school project.
Worked Example · Wall-clock display 25 min
Step 1 — sketch with time
// L03-35 · NTP wall clock — print local time every second
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#include <time.h>
const char* SSID = "YourNetwork";
const char* PASSWORD = "YourPassword";
// Pick your timezone. Examples:
// "MYT-8" = Malaysia/Singapore (UTC+8)
// "WIB-7" = Indonesia western (UTC+7)
// "JST-9" = Japan (UTC+9)
// "GMT0BST,M3.5.0/1,M10.5.0/2" = UK (with DST)
// "EST5EDT,M3.2.0,M11.1.0" = US Eastern (with DST)
// "UTC0" = UTC (no offset)
const char* TZ = "MYT-8";
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
Serial.print("# WiFi");
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println();
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
setenv("TZ", TZ, 1);
tzset();
Serial.print("# Syncing");
time_t now = time(nullptr);
while (now < 24 * 3600) {
delay(200);
Serial.print(".");
now = time(nullptr);
}
Serial.println(" done");
}
void loop() {
time_t now = time(nullptr);
struct tm tmNow;
localtime_r(&now, &tmNow);
char buf[40];
strftime(buf, sizeof(buf), "%a %Y-%m-%d %H:%M:%S %Z", &tmNow);
Serial.println(buf);
delay(1000);
}Step 2 — upload, watch the sync
You'll see something like:
# WiFi.... # Syncing..... done Sun 2026-05-26 19:30:01 MYT Sun 2026-05-26 19:30:02 MYT Sun 2026-05-26 19:30:03 MYT
The first time after "done" is now in your local time zone. Compare to your phone — should match within ~1 s.
Step 3 — combine with the smart lamp
Open yesterday's L03-34 sketch. Add:
- Include
time.h. - After
WiFi.begin:configTime(0, 0, "pool.ntp.org"),setenv("TZ", "MYT-8", 1),tzset(). - In
loop(), every minute check the current hour. If it's 19:00 and the lamp is off, turn it on. If it's 23:00 and on, turn it off.
unsigned long lastSchedCheck = 0;
void checkSchedule() {
if (millis() - lastSchedCheck < 60000) return;
lastSchedCheck = millis();
time_t now = time(nullptr);
if (now < 24 * 3600) return; // not yet synced
struct tm tmNow;
localtime_r(&now, &tmNow);
// 19:00 -> on; 23:00 -> off
if (tmNow.tm_hour == 19 && tmNow.tm_min == 0 && !lamp.on) {
lamp.on = true; applyLamp();
Serial.println("# Scheduled ON");
}
if (tmNow.tm_hour == 23 && tmNow.tm_min == 0 && lamp.on) {
lamp.on = false; applyLamp();
Serial.println("# Scheduled OFF");
}
}Add checkSchedule() at the top of loop(). Your smart lamp now has a real daily schedule.
Step 4 — add "current time" to the dashboard page
Expose the time via the existing /state.json endpoint:
json += ",\"time\":\"";
char buf[20];
strftime(buf, sizeof(buf), "%H:%M:%S", &tmNow);
json += buf;
json += "\"";JS side: read j.time and display it. Your phone's page now shows the ESP's current local time, ticking every second.
Step 5 — verify accuracy
Open your phone's clock app. Open the ESP's page. They should agree to the second. If not, check the TZ string — that's the usual culprit.
Try It Yourself 15 min
Goal: Change the format to 12-hour with AM/PM. Use the %I and %p strftime codes.
Hint
strftime(buf, sizeof(buf), "%a %I:%M:%S %p", &tmNow);
// e.g. "Sun 07:30:45 PM"Goal: Make the schedule configurable via two HTTP endpoints: POST /sched/on-hour?h=19 and POST /sched/off-hour?h=23. Store in EEPROM so the schedule survives reboot.
Hint
Persist onHour and offHour in EEPROM addresses 4 and 5. Read on boot; update on POST. The phone page can have two small <input type="number"> fields that POST to those endpoints.
Goal: Detect day of the week and use a different schedule for weekends. The lamp turns on at 19:00 weekdays, 21:00 weekends.
Hint
tmNow.tm_wday = 0 (Sunday) to 6 (Saturday). For weekdays, check 1..5. For weekend, 0 || 6.
int targetHour = (tmNow.tm_wday == 0 || tmNow.tm_wday == 6) ? 21 : 19;
if (tmNow.tm_hour == targetHour && tmNow.tm_min == 0 && !lamp.on) {
...
}Mini-Challenge · Add a clock face to the lamp page 10 min
- Add a big clock display at the top of the page (HH:MM, large font).
- Show today's date underneath in a smaller font ("Sun, 26 May 2026").
- If a schedule is enabled, show "Next on: 19:00 (4h 12m from now)" below the clock.
- All driven by the existing /state.json endpoint, extended with a "nextSched" field.
The lamp page now feels like a real smart-home app — clock, schedule, controls, all on one screen. From here, the work to ship it as a commercial product is mostly the packaging, the manufacturing and the legal side.
Recap 5 min
NTP gives any internet-connected device the real time, accurate to ms, for free. configTime(0, 0, "pool.ntp.org") kicks off background sync; time(nullptr) returns Unix seconds; localtime_r + strftime turns that into a human-friendly string. TZ strings handle zones and DST. The classic gotcha: years are since 1900, months are 0-indexed. Tomorrow we use the connection in the outbound direction — sending notifications and triggers to external services with webhooks.
- NTP (Network Time Protocol)
- UDP-based protocol for synchronising clocks across a network.
pool.ntp.orgis the global community pool. - UTC (Coordinated Universal Time)
- The global reference time, independent of zones. Sometimes called GMT.
- Unix timestamp
- Seconds since 1970-01-01 00:00 UTC. The standard way to store a moment in time on Unix-like systems.
- POSIX TZ string
- A compact format for specifying a time zone, including DST rules.
setenv("TZ", "...", 1)+tzset()sets it. configTime- ESP-specific helper that initialises NTP sync. Background sync happens automatically; first call may take ~1 s to land.
time(nullptr)- Returns current Unix timestamp. Standard C; works the same on Linux, ESP, etc.
localtime_r- Converts a Unix timestamp to a
struct tmin the current local time zone. Reentrant (the_rvariant); thread-safe. strftime- Standard C function for formatting a
struct tmas a string. Uses%Y %m %d-style format codes. - RTC chip
- Real-Time Clock — a dedicated battery-backed chip (e.g. DS3231) that keeps time without WiFi. Used when offline operation matters.
- 2038 problem
- 32-bit signed Unix timestamps overflow on 19 January 2038. The ESP cores currently use 32-bit time, so products shipping today have to plan for it.
Homework 5 min
- Save the NTP wall clock as
ntp-clock.ino. - Verify your TZ string by comparing the ESP's time to your phone. If it's off by hours, the TZ is wrong.
- Update yesterday's smart lamp with a daily schedule. Show a friend.
- Read ahead to ARD-L03-36 (Webhooks 101). Tomorrow the ESP sends notifications.
Bring back next class:
- Saved NTP sketch.
- Lamp with schedule, working.