Wireless (Wi-Fi/Bluetooth)¶
Pi boards (Zero 2 W, 4, 5) have integrated Wi-Fi and Bluetooth radios; virt/generic
targets have none. Both radios are off by default on every board — a freshly flashed or
factory-reset device has no wireless daemon running and both radios soft-blocked. The USB link
stays the primary, always-on transport regardless of whether you turn wireless on.
Security note. The trusted-link model of this product (no authentication on the control API) is scoped to the USB link. Turning on Wi-Fi widens the network surface: any host reachable over that Wi-Fi network can reach the same unauthenticated API a USB host can. This is an explicit choice you're making by enabling the radio — treat the Wi-Fi network you attach the device to accordingly (e.g. a network you control, not an open guest network).
Power note (Pi Zero 2 W). Wi-Fi transmit bursts draw noticeably more current than idle. On a Zero 2 W powered from a marginal USB 2.0 host port, this can brown out the board. If you enable Wi-Fi on a Zero 2 W, verify it on the actual host port you intend to deploy on, or use an externally-powered setup.
Check whether your board supports wireless¶
Look at the wifi/bluetooth capability flags — true on pi02w/pi4/pi5, false on
virt/generic targets. On a board without the capability, every /wifi/*//bluetooth/* call
answers 503 wifi_unsupported/503 bluetooth_unsupported — there's nothing to configure.
Enabling flow¶
flowchart TD
A["Radio soft-blocked,<br/>no daemon running (default)"] -->|"PATCH /config wifi.enabled=true<br/>+ wifi.country set"| B["displayd unblocks rfkill,<br/>starts wpa_supplicant"]
B --> C{"mode"}
C -->|"station"| D["POST /wifi/connect<br/>or configured networks auto-associate"]
C -->|"ap"| E["POST /wifi/ap {active:true}"]
D --> F["wifi_changed event:<br/>ssid, signal, address"]
E --> G["wifi_changed event:<br/>AP up, clients"]
Bluetooth follows the same shape: bluetooth.enabled: true unblocks rfkill and starts
bluetoothd; POST /bluetooth/power {"powered": true} and POST /bluetooth/scan drive
discovery and pairing.
Enable Wi-Fi (station mode)¶
A country code is required before any Wi-Fi radio can be enabled — there is no default, and enabling without one is rejected outright:
curl -X PATCH http://10.99.0.1/api/v1/config \
-H 'Content-Type: application/json' \
-d '{"wifi": {"enabled": true, "mode": "station", "country": "FR"}}'
Applies live — wpa_supplicant starts immediately, no reboot needed.
Web UI: Wi-Fi page — enable toggle, country selector.
Scan for networks¶
Synchronous, takes a couple of seconds; returns the visible networks (ssid, bssid,
signal_dbm, band, security). Fails with 409 conflict while the device is in AP mode.
Save networks to connect to automatically¶
curl -X PUT http://10.99.0.1/api/v1/wifi/networks \
-H 'Content-Type: application/json' \
-d '[{"ssid": "shop-floor", "psk": "correct horse battery staple", "priority": 10}]'
This is persistent — it replaces the configured network list in /data/config.json (same as
PATCH /config on wifi.networks) and a running supplicant is reconfigured live to match.
Connect now¶
curl -X POST http://10.99.0.1/api/v1/wifi/connect \
-H 'Content-Type: application/json' \
-d '{"ssid": "shop-floor"}'
Returns 200 once the association attempt starts; watch the wifi_changed event (see
Events) for the outcome (connected with an address, or failed). You can also
pass an inline {"network": {...}} instead of ssid to connect to a network without saving it
to the persisted list — useful for a one-off connection.
Web UI: Wi-Fi page — scan results list, Connect per network, saved-network list.
Enable Wi-Fi (access point mode)¶
curl -X PATCH http://10.99.0.1/api/v1/config \
-H 'Content-Type: application/json' \
-d '{"wifi": {"enabled": true, "mode": "ap", "country": "FR", "ap": {"ssid": "mcd-front-desk", "psk": "changeme123", "band": "2.4", "channel": 0}}}'
or bring an AP up as a one-off runtime action without changing the persisted mode:
curl -X POST http://10.99.0.1/api/v1/wifi/ap \
-H 'Content-Type: application/json' \
-d '{"active": true, "ap": {"ssid": "mcd-front-desk", "psk": "changeme123"}}'
Notes:
- The AP is WPA2-PSK only (no WPA3/SAE AP mode).
bandis the string"2.4"or"5"—"2.4"is the default and the only option valid on every board (the Zero 2 W's radio is 2.4 GHz-only; requesting"5"there is rejected).channel: 0(the default) lets the device pick a fixed non-DFS channel automatically rather than doing real automatic channel selection.POST /wifi/ap {"active": false}returns to whatever the persisted configuration says (station mode, if that's what's configured) without touching the savedwifi.mode/wifi.apvalues.
Web UI: Wi-Fi page — AP mode toggle, SSID/password fields.
Bluetooth¶
curl -X PATCH http://10.99.0.1/api/v1/config \
-H 'Content-Type: application/json' \
-d '{"bluetooth": {"enabled": true, "discoverable": true}}'
Applies live. Then:
curl -X POST http://10.99.0.1/api/v1/bluetooth/scan -d '{"active": true}'
curl http://10.99.0.1/api/v1/bluetooth/devices
curl -X POST http://10.99.0.1/api/v1/bluetooth/pair -d '{"address": "AA:BB:CC:DD:EE:FF"}'
Pairing uses Just-Works (no PIN/passkey prompt — there's nowhere on a kiosk display to show
or type one); confirmations are auto-accepted. A paired device is trusted for future
reconnection without re-pairing. Pairing keys persist across reboots (survive under /data);
POST /bluetooth/remove {"address": "..."} unpairs.
Web UI: Bluetooth page — power toggle, discoverable toggle, scan/pair/remove controls.
Turning a radio back off¶
curl -X PATCH http://10.99.0.1/api/v1/config -d '{"wifi": {"enabled": false}}'
curl -X PATCH http://10.99.0.1/api/v1/config -d '{"bluetooth": {"enabled": false}}'
Stops the corresponding daemon and re-blocks the radio via rfkill. The USB gadget link and the control API over it are never affected by enabling or disabling either radio.
Error codes you'll see if wireless is off¶
| Code | Meaning |
|---|---|
503 wifi_unsupported / 503 bluetooth_unsupported |
This board has no such radio (e.g. virt) — nothing to configure. |
503 wifi_disabled / 503 bluetooth_disabled |
The board has the radio, but the config block isn't enabled yet. |
400 bad_request on enabling Wi-Fi |
Usually a missing/invalid wifi.country. |
409 conflict |
Scanning while in AP mode, or an operation that conflicts with the current radio state. |
There is no "empty/read-only" degraded response for either radio — a gated surface always answers one of the codes above rather than silently returning empty data, so you can tell "not supported" from "not enabled" from "actually configured but nothing found".
Hardware validation status¶
Wireless is defined and implemented against the full board matrix (Pi Zero 2 W, Pi 4, Pi 5), but
as with any hardware-facing capability, the acceptance criteria that require real radios and a
real over-the-air pairing/association (station association, AP mode joined by a phone,
Bluetooth pairing surviving a reboot, coexistence of BT pairing with an active Wi-Fi AP) can only
be verified on physical boards, not from a successful build. The virt target only exercises the
capability-gating logic (the 503 wifi_unsupported/bluetooth_unsupported responses) since it
has no radio at all. If you're evaluating wireless for a deployment, budget time for your own
on-site validation on the specific board and USB power setup you intend to ship, rather than
assuming the recipes above are hardware-proven for your environment.
See also¶
- Configuration — general
PATCH /configsemantics. - Configuration reference — full
wifi.*/bluetooth.*field list. - Events — subscribing to
wifi_changed/bluetooth_changed.