Arduino Captive WiFi Portal with QR Code
Learn how to create a captive WiFi connection portal that's user friendly and features a QR code for your Arduino project. We're using an Heltec E290 esp32 with an integrated e-ink display but you can implement something similar for your device & display.

Make a Captive WiFi Portal Like This
To get started open up your Arduino IDE or if you like VScode you can use the community maintained version of the deprecated Arduino VScode extension like I do! It allows you to use all your other fancy extensions and Copilot which was definitely used to write some of this code. This tutorial will step through the functions used and details about them.
Start Here
Begin with just the boilerplate setup()
and loop()
functions. If you didn't know, the setup()
function runs once when the device starts up, and theloop()
function runs continually after the setup function completes. That's the basics!
sketch.ino
The Setup() Function
Within the setup()
function shown in the code below were going to do 3 things basically:
First we initialize the e-ink display settings. We're telling it to set the rotation of the device into a horizontal format, clear the screen and then use "fastmode" which allows for partial refreshes of the display. It's all explained here in the library's documentation for the display.
Next we check for saved WiFi credentials in SPIFFS with the
loadWiFiCredentials()
function. If there are saved credentials the script will try to connect to wifi.If the there's no WiFi then
startCaptivePortal()
is called which turns on access point mode, the DNS/web server, and generates a QR Code on the display.
sketch.ino
Load Wifi Credentials Function
Now, let's create the loadWiFiCredentials()
function we just used in the setup()
function. Using SPIFFS, it will look for the presence of a file named wifi.txt
which will contain the SSID and password.
sketch.ino
Start Captive Portal Function
Ok, now we can create the other function we called in setup()
, the startCaptivePortal()
function. This function starts the WiFi access point, configures the DNS server to redirect all domains to the devices IP, setups routes for the "captive" portal, and generates a QR code based on the WiFI URI standard.
sketch.ino
Handle Root Function
The startCaptivePortal()
function includes a handleRoot()
function called on lines 11 and 13. This function contains the (ugly string concatenated) HTML for the captive portal WiFi network selection view.
sketch.ino
Handle Setup Function
Not to be confused with the former setup()
function, the handleSetup()
function is fired after a user clicks the "Connect" button in the captive portal's root view.
It first attempts to connect to the new WiFi network and then will save the credentials using SPIFFS if a connection succeeds. If the connection fails the user is prompted to try again and directed back to the WiFi selection root view.
sketch.ino
Save WiFi Credentials Function
Used by the handleSetup()
function, the saveWiFiCredentials()
function will save your WiFi credentials to the file system. It saves it in the wifi.txt with the first line being the SSID and the second the password.
sketch.ino
Display Provisioning Screen Function
This displayProvisioningScreen()
function is called in the startCaptivePortal()
function and magically prints a QR code to the display using the ricmoo/QRCode library and a bit of hackery.
sketch.ino
And Finally The Loop Function
The loop()
function here runs the dnsServer and webServer for the captive portal if there is no WiFi connection.
sketch.ino
Imports and Globals
The imports and globals go at the very top of your sketch file. These are used to import the libraries, declare global variables, and constants.
Oh yeah and don't forget that you will also need to include the third party libraries we used. You can search for the two libraries here and install each:
"QRcode" by Richard Moore
"heltec-eink-modules" by Todd Herbert
sketch.ino
Bonus Points, Reset Button
Ok so you got this solution working but you want to reset the WiFi credentials huh? You could just use the "Erase All Flash Before Sketch Upload" option in your IDE's board configuration, but that's not very user friendly. It's better to have a physical way to reset it!
For this Heltec E290 board It's got a built in button at GPIO 21 so that's what i'm going to use. There are a few ways to detect a button press, you can use an interrupt, or just poll for it in the loop()
function, and you can also add a debounce if you need. But we don't need a debounce and we can use the simple polling method because this button can be pressed multiple times and will not have a negative effect.
First let's add the resetWiFiCredentials()
function that will actually do the removal of the credentials.
sketch.ino
Next modify your loop()
function to have this super simple pinMode()
init and digitalRead()
conditional that will fire the resetWifiCredentials()
function. Lines 10-14.
sketch.ino
Boom that should do it! Now you can press that button and a message will pop up notifying users of a WiFi reset.
Put it all Together
Ok when you put it all together you should get something like this sketch.ino file. TL;DR You can also just cut to the chase and copy this!
Once you get this uploaded to your device you should see the QR code view shown in the image above. If not check your console output to see if it ran into any errors compiling.
Scan the QR code with your phone and IOS or Android should open up the captive portal screen automatically!