In previous lessons, we learned about two WiFi modes:
In real projects, we often need to combine the advantages of both modes:
| Requirement | STA Mode | AP Mode |
|---|---|---|
| Access internet data | ✅ Connects to router, accesses internet | ❌ Cannot access internet |
| Local control | ❌ Phone must share router | ✅ Phone connects directly to Arduino |
| Works without router | ❌ Requires router | ✅ Standalone operation |
| NTP time sync | ✅ Can sync | ❌ Cannot sync |
Core concept: On startup, use STA mode to connect to the router and fetch network time, then switch to AP mode to create a hotspot for phone control. This way, we can both access internet data and work independently!
Simple analogy:
NTP (Network Time Protocol) is a protocol for synchronizing the time of computers across a network. By connecting to an NTP server on the internet, Arduino can obtain the current accurate time.
This project uses the Chinese National Time Service Center's NTP server cn.ntp.org.cn with the timezone set to UTC+8 (Beijing time).
The UNO R4 WiFi board cannot use STA and AP modes simultaneously — it can only operate in one mode at a time. So we need to:
After switching to AP mode, Arduino can no longer access the internet for time updates, so we need a local timekeeping solution:
millis() function returns the number of milliseconds elapsed since Arduino booted up| Function | Description | Purpose in This Project |
|---|---|---|
WiFi.disconnect() |
Disconnects current WiFi connection | Clears previous state |
WiFi.begin(ssid, pass) |
Starts STA mode, connects to router | Connects Arduino as a client to WiFi |
WiFi.status() |
Checks WiFi connection status | Confirms successful router connection |
WiFi.end() |
Completely shuts down WiFi | Releases resources, prepares for mode switch |
| Function | Description | Purpose in This Project |
|---|---|---|
WiFi.beginAP(ssid, pass) |
Starts AP mode, creates WiFi hotspot | Turns Arduino into a WiFi hotspot |
WiFi.localIP() |
Gets the device's IP address | Checks the default IP in AP mode (usually 192.168.4.1) |
| Function | Description | Purpose in This Project |
|---|---|---|
WiFiUDP ntpUDP |
Creates UDP object | NTP communication requires UDP protocol |
NTPClient timeClient(...) |
Creates NTP client | Configures NTP server and timezone |
timeClient.begin() |
Starts NTP client | Begins NTP communication |
timeClient.update() |
Gets time from NTP server | Syncs network time |
timeClient.getEpochTime() |
Gets Unix timestamp | Gets current seconds count |
timeClient.end() |
Stops NTP client | Releases NTP resources |
| Function | Description | Purpose in This Project |
|---|---|---|
millis() |
Returns milliseconds since boot | Checks if 1 second has elapsed |
currentUnixTime++ |
Increments timestamp by 1 | Implements per-second timekeeping |
| Function | Description | Purpose in This Project |
|---|---|---|
WiFiServer server(80) |
Creates web server on port 80 | Listens for HTTP requests |
server.begin() |
Starts web server | Begins accepting client connections |
server.accept() |
Accepts new client connection | Gets client object |
client.readStringUntil('\r') |
Reads client's HTTP request | Parses control commands |
client.println() |
Sends data to client | Returns HTML webpage |
client.stop() |
Closes client connection | Completes communication |
HTTP requests consist of multiple lines of text; the first line tells the server what the client wants. This project uses two types of requests:
| Request Path | Meaning | Corresponding Action |
|---|---|---|
GET /H |
"High" - high voltage | Turn LED on |
GET /L |
"Low" - low voltage | Turn LED off |
// STA mode settings (connecting to router)
const char* sta_ssid = "ZNP"; // Change to your WiFi name
const char* sta_password = "12345678"; // Change to your WiFi password
// AP mode settings (creating hotspot)
const char* ap_ssid = "R4_AP"; // Hotspot name, customizable
const char* ap_password = "12345678"; // Hotspot password, min 8 chars
Notes:
sta_ssid and sta_password must be changed to your home router's WiFi name and passwordcn.ntp.org.cn#include <WiFiS3.h> // Main WiFi library for UNO R4 WiFi
#include <WiFiUdp.h> // UDP protocol library (required by NTP)
#include <NTPClient.h> // NTP client library
// STA and AP WiFi configuration
const char* sta_ssid = "ZNP"; // Router name
const char* sta_password = "12345678"; // Router password
const char* ap_ssid = "R4_AP"; // Hotspot name
const char* ap_password = "12345678"; // Hotspot password
WiFiServer server(80); // Create web server on port 80
WiFiUDP ntpUDP; // Create UDP object
NTPClient timeClient(ntpUDP, "cn.ntp.org.cn", 8 * 3600); // Configure NTP client (UTC+8)
int led = LED_BUILTIN; // On-board LED pin
// Time variables
unsigned long currentUnixTime = 0; // Current timestamp (seconds)
unsigned long lastUpdateTime = 0; // Last update time (milliseconds)
Line-by-line explanation:
#include <WiFiS3.h>: Includes the main WiFi library containing AP/STA modes, web server, and other WiFi features#include <WiFiUdp.h>: Includes the UDP library; NTP protocol uses UDP for communication#include <NTPClient.h>: Includes the NTP client library for fetching network timeNTPClient timeClient(ntpUDP, "cn.ntp.org.cn", 8 * 3600): Creates an NTP client, specifying the server address and timezone offset (UTC+8 = 8 hours = 28800 seconds)currentUnixTime: Stores the current Unix timestamp (seconds since January 1, 1970)lastUpdateTime: Records the millis() value at the last time updatesetup() - Initializationvoid setup() {
Serial.begin(115200);
while (!Serial);
delay(1000);
pinMode(led, OUTPUT);
digitalWrite(led, LOW);
Serial.println("=== Boot: Get Time → Switch to AP ===");
getNetTime();
startAPMode();
server.begin();
lastUpdateTime = millis();
}
Step breakdown:
getNetTime() to connect to router via STA mode and sync NTP timestartAPMode() to create a WiFi hotspotserver.begin() to start the HTTP service, and record the millis() timestamp as the software timekeeping starting pointgetNetTime() - Get Network Time via STA Modevoid getNetTime() {
WiFi.disconnect();
delay(500);
WiFi.begin(sta_ssid, sta_password);
Serial.print("Connecting to router: ");
int timeout = 0;
while (WiFi.status() != WL_CONNECTED && timeout < 40) {
delay(500);
Serial.print(".");
timeout++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n✅ WiFi connected");
timeClient.begin();
for (int i = 0; i < 3; i++) {
if (timeClient.update()) {
currentUnixTime = timeClient.getEpochTime();
Serial.print("✅ Time synced: ");
Serial.println(timeClient.getFormattedTime());
break;
}
delay(2000);
}
timeClient.end();
} else {
Serial.println("\n❌ WiFi connection failed, using default time 12:00:00");
currentUnixTime = 12 * 3600;
}
WiFi.end();
delay(1000);
}
Step breakdown:
Step 1: Connect to router
WiFi.disconnect(); // Clear previous connections
WiFi.begin(sta_ssid, sta_password); // Connect to router
Step 2: Wait for connection
while (WiFi.status() != WL_CONNECTED && timeout < 40) {
delay(500); // Check every 0.5 seconds
timeout++; // Maximum wait: 20 seconds (40 × 0.5s)
}
Timeout mechanism: waits at most 20 seconds to avoid infinite waiting.
Step 3: Get network time
timeClient.begin(); // Start NTP client
timeClient.update(); // Request time from NTP server
currentUnixTime = timeClient.getEpochTime(); // Get timestamp
Retries up to 3 times with 2-second intervals.
Step 4: Handle failure
// If connection fails, use default time
currentUnixTime = 12 * 3600; // 12:00:00 noon
Step 5: Disconnect STA connection
timeClient.end(); // Stop NTP client
WiFi.end(); // Completely shut down WiFi, release resources
WiFi.end() completely closes the WiFi module, preparing for AP mode startup.
startAPMode() - Start AP Modevoid startAPMode() {
Serial.println("\n==================================");
Serial.println("📶 AP mode started");
Serial.println("Connect to WiFi: R4_AP");
Serial.println("Visit in browser: 192.168.4.1");
Serial.println("==================================\n");
WiFi.beginAP(ap_ssid, ap_password); // Create WiFi hotspot
delay(3000); // Wait 3 seconds for initialization
Serial.print("AP IP: ");
Serial.println(WiFi.localIP()); // Print IP address in AP mode
}
After startup, nearby phones/computers can detect the "R4_AP" WiFi hotspot. The default IP is 192.168.4.1.
loop() - Main Loopvoid loop() {
// === Core 1: Automatic timekeeping ===
if (millis() - lastUpdateTime >= 1000) {
lastUpdateTime = millis();
currentUnixTime++;
int h = (currentUnixTime % 86400L) / 3600;
int m = (currentUnixTime % 3600) / 60;
int s = currentUnixTime % 60;
Serial.print("⏰ Time: ");
Serial.print(h < 10 ? "0" : ""); Serial.print(h);
Serial.print(":");
Serial.print(m < 10 ? "0" : ""); Serial.print(m);
Serial.print(":");
Serial.print(s < 10 ? "0" : ""); Serial.println(s);
}
// === Core 2: AP mode web page LED control ===
WiFiClient client = server.accept();
if (!client) return;
String req = client.readStringUntil('\r');
client.flush();
if (req.indexOf("GET /H") != -1) {
digitalWrite(led, HIGH);
}
if (req.indexOf("GET /L") != -1) {
digitalWrite(led, LOW);
}
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println();
client.println("<!DOCTYPE html><html>");
client.println("<head><title>UNO R4 AP LED Control</title></head>");
client.println("<body style='text-align:center; margin-top:50px;'>");
client.println("<h1 style='color:blue;'>UNO R4 AP LED Control</h1>");
client.println("<a href='/H' style='font-size:22px; margin:20px;'>LED ON</a>");
client.println("<a href='/L' style='font-size:22px; margin:20px;'>LED OFF</a>");
client.println("</body></html>");
client.stop();
}
Step breakdown:
Core 1: Automatic timekeeping
if (millis() - lastUpdateTime >= 1000) { // Check if 1 second has passed
lastUpdateTime = millis(); // Update reference time
currentUnixTime++; // Increment timestamp by 1 second
// Calculate hours, minutes, seconds from timestamp
int h = (currentUnixTime % 86400L) / 3600; // Hours (0-23)
int m = (currentUnixTime % 3600) / 60; // Minutes (0-59)
int s = currentUnixTime % 60; // Seconds (0-59)
}
millis() returns the number of milliseconds since Arduino booted. By checking whether the difference is ≥ 1000 milliseconds, it updates the time once per second.
Timestamp to hours/minutes/seconds conversion formula:
h = (timestamp % 86400) / 3600: Get total seconds of the day, divide by 3600 to get hoursm = (timestamp % 3600) / 60: Get seconds of the current hour, divide by 60 to get minutess = timestamp % 60: Get seconds of the current minuteCore 2: Web page LED control
Same as the pure AP mode project:
server.accept() waits for client connectionreadStringUntil('\r') reads HTTP request/H and /L to control LEDclient.stop() closes connectionArduino boots up
↓
┌─────────────────────────┐
│ STA mode connects │
│ to router (gets time) │
└─────────────────────────┘
↓
┌─────────────────────────┐
│ Disconnect STA, │
│ start AP mode │
│ (creates WiFi hotspot)│
└─────────────────────────┘
↓
┌─────────────────────────┐
│ Start web server │
└─────────────────────────┘
↓
┌─────────────────────────┐
│ Main loop() │
│ ├─ Software timekeeping│
│ │ (+1 per second) │
│ └─ Handle HTTP requests│
└─────────────────────────┘
↓
Phone connects to "R4_AP" → Visit 192.168.4.1 → Control LED
Note: The serial monitor baud rate needs to be set to 115200 (not the default 9600).
On startup (time sync successful):
=== Boot: Get Time → Switch to AP ===
Connecting to router: ....
✅ WiFi connected
✅ Time synced: Thu Aug 04 14:30:25 2026
==================================
📶 AP mode started
Connect to WiFi: R4_AP
Visit in browser: 192.168.4.1
==================================
AP IP: 192.168.4.1
⏰ Time: 14:30:26
⏰ Time: 14:30:27
⏰ Time: 14:30:28
...
On startup (time sync failed):
=== Boot: Get Time → Switch to AP ===
Connecting to router: ........................................
❌ WiFi connection failed, using default time 12:00:00
==================================
📶 AP mode started
Connect to WiFi: R4_AP
Visit in browser: 192.168.4.1
==================================
AP IP: 192.168.4.1
⏰ Time: 12:00:01
⏰ Time: 12:00:02
...
NTPClient, install the version by Fabrice WeinbergSTA_and_AP.inosta_ssid to your home router's WiFi namesta_password to your home router's WiFi passwordap_ssid and ap_password as needed192.168.4.1Q1: The serial monitor shows garbled characters?
A: Make sure the serial monitor baud rate is set to 115200 (the code uses Serial.begin(115200)).
Q2: Can't connect to router, "WiFi connection failed" message?
A: Please check:
sta_ssid and sta_password are correctQ3: Phone can't find the "R4_AP" hotspot?
A: Please check:
Q4: The time is not accurate?
A: Possible reasons:
Q5: After the phone connects to the hotspot, is Arduino's time still accurate?
A: The phone connects to the AP hotspot and will not affect Arduino's software timekeeping. The time will continue ticking.
Q6: Can the phone access the internet after connecting?
A: No. In AP mode, Arduino only provides local network connectivity; the phone cannot access the internet through it.
Q7: Why can't STA and AP modes be used simultaneously?
A: The UNO R4 WiFi hardware only supports operating in one mode at a time. This is a hardware limitation, so we use the strategy of "first STA to get time, then AP to provide services."
Q8: After WiFi connection fails, will the time always display 12:00:00?
A: No. When the connection fails, the default time 12:00:00 is used, but then millis() continues timekeeping, so the time will start ticking from 12:00:00.
Q9: Why is the output time a few seconds slower than the actual time?
A: There are two main reasons:
1. Mode switching delay: After obtaining the network time, Arduino needs to close WiFi (WiFi.end()) and then start AP mode (WiFi.beginAP()). This process takes several seconds, creating a brief gap between timestamp acquisition and actual timekeeping start.
2. Local timekeeping accuracy: Arduino uses the millis() function for local timekeeping, which relies on the board's RC oscillator (internal clock). Its accuracy is not as good as a real-time clock (RTC) chip. Over time, accumulated errors may occur, typically ranging from a few seconds to tens of seconds per day.
💡 Extended knowledge:
millis()returns the number of milliseconds since Arduino booted. Each time throughloop(), the program checks ifmillis() - lastUpdateTime >= 1000(i.e., whether one second has passed). If the condition is met, the timestamp is incremented by 1. This "polling check" approach, while simple, does not block the program, allowing LED control and timekeeping to happen simultaneously.
| Problem | Possible Cause | Solution |
|---|---|---|
| Garbled serial | Wrong baud rate | Change to 115200 |
| Can't connect WiFi | Wrong password/5GHz | Check password, use 2.4GHz |
| Time sync fails | Unstable network | Move closer to router, retry |
| Can't find hotspot | Code not uploaded | Re-upload code |
| Webpage won't open | Wrong IP address | Use 192.168.4.1 |
| LED not responding | Wrong pin/browser cache | Refresh page, verify pin |
const char* sta_ssid = "YourWiFi"; // Change to your WiFi
const char* sta_password = "yourPassword"; // Change to your WiFi password
const char* ap_ssid = "MyArduino"; // Custom hotspot name
const char* ap_password = "secret1234"; // Custom password
// Change to a different timezone, e.g., Tokyo, Japan (UTC+9)
NTPClient timeClient(ntpUDP, "ntp.jst.mfeed.ad.jp", 9 * 3600);
// Change to New York, USA (UTC-4, note the negative sign)
NTPClient timeClient(ntpUDP, "time.google.com", -4 * 3600);
// Use a different default time if WiFi connection fails
currentUnixTime = 8 * 3600 + 30 * 60; // 08:30:00
// Calculate date info (converting from Unix timestamp)
void printDateTime(unsigned long timestamp) {
// Unix timestamp to year/month/day/hour/minute/second conversion is complex
// Simplified version: shows only hours/minutes/seconds
int h = (timestamp % 86400L) / 3600;
int m = (timestamp % 3600) / 60;
int s = timestamp % 60;
Serial.printf("%02d:%02d:%02d\n", h, m, s);
}
// Add time display in HTML
client.print("<p>Current Time: ");
client.print(h < 10 ? "0" : ""); client.print(h);
client.print(":");
client.print(m < 10 ? "0" : ""); client.print(m);
client.print(":");
client.println(s < 10 ? "0" : ""); client.println(s);
client.println("</p>");
if (req.indexOf("GET /B") != -1) { // Blinking mode
for (int i = 0; i < 5; i++) {
digitalWrite(led, HIGH);
delay(200);
digitalWrite(led, LOW);
delay(200);
}
}
// Re-sync time every hour
unsigned long lastSyncTime = 0;
void loop() {
if (millis() - lastSyncTime > 3600000) { // 3600 seconds = 1 hour
lastSyncTime = millis();
resyncTime(); // Re-sync time
}
// ... existing code
}
// Make LED blink rapidly when connection fails
if (WiFi.status() != WL_CONNECTED) {
for (int i = 0; i < 10; i++) {
digitalWrite(led, !digitalRead(led));
delay(100);
}
}
Create a dynamic clock webpage:
<!DOCTYPE html>
<html>
<head>
<title>UNO R4 Clock</title>
<script>
// Use JavaScript to implement dynamic clock
// Refresh page every second or use AJAX to update time
</script>
</head>
<body>
<h1 id="clock">00:00:00</h1>
</body>
</html>
Trigger actions (ringing, turning on lights) at specified times:
if (h == 7 && m == 30 && s == 0) { // Every day at 7:30
digitalWrite(led, HIGH); // Turn on light
}
Connect a DHT11/DHT22 sensor and display temperature and humidity on the webpage:
client.print("<p>Temperature: ");
client.print(temperature);
client.println("°C</p>");
client.print("<p>Humidity: ");
client.print(humidity);
client.println("%</p>");
Allow multiple users to connect simultaneously:
while (true) {
WiFiClient client = server.available();
if (!client) break;
// Handle each client
}
Save time and sensor data to an SD card or send to a cloud platform via WiFi:
// Save to SD card
// Send to IoT platform
int led1 = 9;
int led2 = 10;
if (req.indexOf("GET /H1") != -1) digitalWrite(led1, HIGH);
if (req.indexOf("GET /L1") != -1) digitalWrite(led1, LOW);
if (req.indexOf("GET /H2") != -1) digitalWrite(led2, HIGH);
if (req.indexOf("GET /L2") != -1) digitalWrite(led2, LOW);
Periodically switch between STA and AP modes to maintain time accuracy while continuously providing control services:
void loop() {
// Switch modes every hour to sync time
if (needResync()) {
switchToSTA();
getNetTime();
switchToAP();
}
// ...
}