WiFi configuration captive portal for Raspberry Pi — button-triggered AP + web UI
Project description
pi-wifi-setup
WiFi configuration captive portal for Raspberry Pi.
On first boot (or button press) the device creates a hotspot — connect from your phone, pick a network, enter the password, done.
How it works
- On startup, if no WiFi credentials are saved, the Pi automatically raises a hotspot
hostapdcreates a virtualuap0AP interface alongside the existingwlan0dnsmasqhandles DHCP and redirects all DNS to the captive portal- User connects to the hotspot and is directed to
http://192.168.4.1 - The web UI lists nearby networks; user selects one and enters the password
- Credentials are saved via NetworkManager (
nmcli) and persist across reboots - AP shuts down, device resumes normal operation
- A physical button can trigger setup mode at any time to change the network
Requirements
System packages:
sudo apt install hostapd dnsmasq network-manager
sudo systemctl enable NetworkManager
sudo systemctl disable dnsmasq
Python: 3.9+
Hardware: Any Raspberry Pi with built-in WiFi:
| Model | Chip | AP mode |
|---|---|---|
| Pi Zero W | CYW43438 | ✓ |
| Pi Zero 2 W | CYW43438 | ✓ |
| Pi 3B / 3B+ | CYW43438 / CYW43455 | ✓ |
| Pi 4B | CYW43455 | ✓ |
| Pi 5 | CYW43455 | ✓ |
Pi Zero W / Zero 2 W note: some units require hostapd to run without the
driver=nl80211line.pi-wifi-setupdetects this automatically and retries without the driver directive if the first attempt fails.
Installation
pip install pi-wifi-setup
Button wiring
Connect a momentary push button between GPIO 17 (BCM) and GND.
No resistor needed — the internal pull-up is enabled in software.
3.3V ──────────────────┐
│ (internal pull-up via lgpio)
GPIO 17 (BCM) ──────────┤
│
[BUTTON]
│
GND ────────────────────┘
Physical pin reference (Pi 40-pin header):
| Signal | Physical Pin | BCM |
|---|---|---|
| GND | 9, 14, 20 … | — |
| GPIO 17 | Pin 11 | 17 |
You can use any free GPIO pin — pass it as button_gpio=<pin>.
Quick start
import threading
import time
from pi_wifi_setup import SetupMode
# --- your application ---
_stop_event = threading.Event()
def main_loop():
while not _stop_event.is_set():
print("Working...") # replace with your actual logic
time.sleep(1)
def on_pause():
"""Called before the setup AP starts — stop your app here."""
_stop_event.set()
def on_resume():
"""Called after a successful WiFi connection — restart your app here."""
_stop_event.clear()
threading.Thread(target=main_loop, daemon=True).start()
# --- WiFi setup ---
setup = SetupMode(
ap_ssid="MyDevice",
ap_password="setup1234", # WPA2 password, or None for open network
button_gpio=17, # BCM pin number (physical pin 11)
auto_on_no_credentials=True,
)
# Blocks until WiFi is configured if no credentials are saved.
# After that runs in the background, waiting for a button press.
setup.start(on_pause=on_pause, on_resume=on_resume)
# WiFi is ready — start your app
main_loop()
If your application does not need to pause during reconfiguration (e.g. it is stateless or handles reconnection itself), on_pause and on_resume can be omitted:
setup = SetupMode(ap_ssid="MyDevice", ap_password="setup1234")
setup.start() # blocks until WiFi configured, then continues
main_loop()
First boot (no credentials saved)
setup = SetupMode(ap_ssid="MyDevice", ap_password="setup1234")
setup.start() # blocks until connected, then continues
Already configured — button only
setup = SetupMode(
ap_ssid="MyDevice",
ap_password="setup1234",
auto_on_no_credentials=False,
)
setup.start(on_pause=app.pause, on_resume=app.resume)
Connect programmatically (no UI)
from pi_wifi_setup import WiFiConnector
wifi = WiFiConnector(interface="wlan0")
if not wifi.has_credentials():
wifi.connect(ssid="HomeNetwork", password="secret123", timeout=30)
status = wifi.get_status()
print(status["ip_address"])
API
SetupMode
| Parameter | Default | Description |
|---|---|---|
button_gpio |
17 |
BCM GPIO pin for the setup button |
ap_ssid |
"VendingSetup" |
Hotspot SSID |
ap_password |
None |
WPA2 password, None for open network |
long_press_seconds |
3.0 |
Hold duration to trigger setup |
wifi_interface |
"wlan0" |
Primary WiFi interface |
auto_on_no_credentials |
True |
Auto-start AP if no network is saved |
| Method | Description |
|---|---|
start(on_pause, on_resume) |
Start listener; blocks if auto AP is triggered |
run_blocking() |
Start AP immediately and block until connected |
stop() |
Stop button listener |
has_credentials() |
True if a WiFi connection is saved in NetworkManager |
WiFiConnector
| Method | Description |
|---|---|
has_credentials() |
Check for saved connections |
connect(ssid, password, timeout) |
Connect and persist credentials |
get_status() |
Return {connected, ssid, ip_address, wpa_state} |
Architecture
pi_wifi_setup/
├── setup_mode.py # Orchestrator
├── ap_manager.py # hostapd + dnsmasq + iptables
├── web_server.py # Flask captive portal
├── wifi_connector.py # nmcli wrapper
├── scanner.py # WiFi network scanner
├── button_handler.py # GPIO long-press detector (lgpio)
└── templates/
└── index.html # Web UI
License
MIT © 2026 Mikhail Shmarov
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pi_wifi_setup-1.0.0.tar.gz.
File metadata
- Download URL: pi_wifi_setup-1.0.0.tar.gz
- Upload date:
- Size: 16.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
093878153010672b2c5527393afb0e7c0ca91cc75883f586c6d7549c3fd8d627
|
|
| MD5 |
a26e60bf04416c89b66afa6022b6679c
|
|
| BLAKE2b-256 |
084d8e7ab3381128f62c66f1cd03aa1099089d71e1b632dfeb9a31606f4b649b
|
File details
Details for the file pi_wifi_setup-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pi_wifi_setup-1.0.0-py3-none-any.whl
- Upload date:
- Size: 16.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d5b81d62a1e7e7c029d9492720a7b6facc77f625e191b843c78ab2fa98c83966
|
|
| MD5 |
28067cd4372456f2df564ef422aac558
|
|
| BLAKE2b-256 |
6e050e192167152f438a5cfda1d17a63f66f02b18553008f907b4dce62ad8495
|