Access point mode
The board becomes the network, so a phone can reach it before it has any Wi-Fi credentials at all. It is how every commercial device gets configured, and it fails in three ways station mode never does.
Three things it does not do
Give it a name a person recognises
ESP_1A2B is the default and it is useless — a customer looking at a list of
networks has no idea which one is yours. Use the product name and, if there
might be several, the last four digits of the MAC: Greenhouse setup A3F2.
AP and station together
WIFI_AP_STA runs both. It is the right mode during provisioning: the phone
stays on the setup page while the board tests the credentials, so you can tell
it why the password failed instead of just disappearing.
It is the wrong mode permanently. One radio serves both, throughput halves, and the access point is dragged onto whatever channel your router picked.
The code
An access point plus a DNS server that answers every name with the board's own address. That second part is what makes the setup page appear by itself instead of waiting to be typed.
#include <WiFi.h>
#include <DNSServer.h>
#include <ESPAsyncWebServer.h>
DNSServer dns;
AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_AP);
WiFi.softAP("Greenhouse setup", "12345678", 1, false, 8);
Serial.println(WiFi.softAPIP()); // 192.168.4.1 by default
dns.start(53, "*", WiFi.softAPIP()); // every name -> us
server.on("/", HTTP_GET, [](AsyncWebServerRequest *r) {
r->send(200, "text/html",
"<form method=post action=/save>"
"<input name=ssid><input name=pass type=password>"
"<button>Save</button></form>");
});
server.onNotFound([](AsyncWebServerRequest *r) {
r->redirect("/"); // the captive portal nudge
});
server.begin();
}
void loop() { dns.processNextRequest(); }Android and iOS both check for internet access after joining. Without the DNS trick they decide the network is useless and quietly switch back to mobile data.
Bringing the access point up is three lines. The scan is worth including - showing the networks the board can actually see removes the most common setup failure, which is a mistyped name.
import network
ap = network.WLAN(network.AP_IF)
ap.config(essid='Greenhouse setup', password='12345678',
authmode=network.AUTH_WPA2_PSK, max_clients=8)
ap.active(True)
print(ap.ifconfig()[0]) # 192.168.4.1
sta = network.WLAN(network.STA_IF)
sta.active(True)
for ssid, *_ in sta.scan():
print(ssid.decode()) # offer these, do not make them type itThe board and a phone can be on the AP at the same time as the board is joined to your router, but there is only one radio, so both share whatever channel the router chose.
When it does not work
It tested for internet access, found none, and switched back to mobile data. A captive portal - a DNS server answering everything with your IP - is what keeps it there.
max_connection defaults to four. Raise it in softAP. Ten is the hardware ceiling and each client costs RAM.
In AP+STA mode there is one radio and one channel. When the station side associates, the access point follows the router's channel - anything already connected to it is dropped.
WPA2 requires at least eight characters. Fewer than that and softAP silently creates an open network instead.
Both halves at the same time, on one radio — which is the constraint that decides the order your code has to do it in.
Both at once: AP and station →Edit this page — content/esp32/access-point-mode.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.