Skip to content

wifi: allow 5 GHz channels in start_ap() - #11374

Open
mikeysklar wants to merge 1 commit into
adafruit:mainfrom
mikeysklar:wifi-ap-5ghz
Open

wifi: allow 5 GHz channels in start_ap()#11374
mikeysklar wants to merge 1 commit into
adafruit:mainfrom
mikeysklar:wifi-ap-5ghz

Conversation

@mikeysklar

Copy link
Copy Markdown
Collaborator

What

start_ap(channel=) accepts 1 to 165. Ports raise ValueError for channels the radio or its country setting rejects.

Why

Fixes #11338. The 1 to 13 clamp in shared-bindings made a 5 GHz access point impossible on the ESP32-C5.

Port Change
shared-bindings range 1..13 -> 1..165, docstring
espressif check esp_wifi_set_config; ESP_ERR_INVALID_ARG -> ValueError: Invalid channel; undo AP mode if this call turned it on
raspberrypi CYW43 is 2.4 GHz only; keeps its bound at 13 with channel must be <= 13
zephyr-cp untouched, it ignores the channel today

No new translatable strings; Invalid %q is reused.

Hardware tested

Board CircuitPython Host
ESP32-C5-DevKitC-1-N8R8 10.4.0-alpha.1, main at 42af95d plus this change Linux
Raspberry Pi Pico 2 W same same
Adafruit Metro ESP32-S2 same same

Not tested: Pico W, zephyr-cp boards.

How I tested it

REPL lines below are condensed from scripted runs; outputs are verbatim.

ESP32-C5, start_ap("x", pw, channel=ch) over the full 5 GHz list, station state varied:

Station state 2.4 GHz accepted 5 GHz accepted
disconnected (IDF default country "01") 1-11 36-64
associated on ch 5, AP country US 1-11 none
associated on ch 149, AP country US none 36-165

Seen from a dual-band Linux client (nmcli dev wifi list), BSSID is the C5's AP MAC:

c5ap149  10:BD:A3:DF:20:15  chan 149  5745 MHz  WPA1 WPA2
c5ap36   10:BD:A3:DF:20:15  chan 36   5180 MHz  WPA1 WPA2

Rejected values:

>>> wifi.radio.start_ap("x", "12345678", channel=0)
ValueError: channel must be 1-165
>>> wifi.radio.start_ap("x", "12345678", channel=37)
ValueError: Invalid channel
>>> wifi.radio.ap_active
False

A rejected start_ap leaves a running AP alone and does not leave a half-started one:

>>> wifi.radio.start_ap("keep6", "12345678", channel=6); wifi.radio.ap_active
True
>>> wifi.radio.start_ap("bad", "12345678", channel=149)
ValueError: Invalid channel
>>> wifi.radio.ap_active
True
>>> wifi.radio.stop_ap(); wifi.radio.start_ap("bad", "12345678", channel=149)
ValueError: Invalid channel
>>> wifi.radio.ap_active
False

Metro ESP32-S2 (2.4 GHz only), the IDF rejects 5 GHz itself:

>>> wifi.radio.start_ap("x", "12345678", channel=36)
ValueError: Invalid channel
>>> wifi.radio.start_ap("x", "12345678", channel=6); wifi.radio.ap_active
True

Pico 2 W:

>>> wifi.radio.start_ap("x", "12345678", channel=36)
ValueError: channel must be <= 13
>>> wifi.radio.start_ap("x", "12345678", channel=1); wifi.radio.ap_active
True

A Metro ESP32-S2 scan then saw pico1 on channel 1 with the Pico's AP MAC.

The ordering from the issue comment also works on the C5: enabled = False, start_ap(channel=36), set mac_address_ap, enabled = True gives an active AP with the new MAC.

Try it

code.py for an ESP32-C5. Keeps the AP up, since WiFi started from the REPL is reset when the session ends.

import os, time, wifi

SSID = os.getenv("CIRCUITPY_WIFI_SSID")
PW = os.getenv("CIRCUITPY_WIFI_PASSWORD")

# Channel 36-64 work with no station (world-safe country).
# 100-165 need the station associated on 5 GHz to a US access point.
wifi.radio.stop_ap()
wifi.radio.stop_station()
wifi.radio.start_ap("c5ap36", "12345678", channel=36)
print("ap_active", wifi.radio.ap_active, "mac", wifi.radio.mac_address_ap.hex())
time.sleep(30)  # scan for it from another device

net = None
for n in wifi.radio.start_scanning_networks(start_channel=36, stop_channel=165):
    if n.ssid == SSID:
        net = n
        break
wifi.radio.stop_scanning_networks()
wifi.radio.connect(network=net, password=PW)
print("station ch", wifi.radio.ap_info.channel, "country", wifi.radio.ap_info.country)
wifi.radio.start_ap("c5ap149", "12345678", channel=149)
print("ap_active", wifi.radio.ap_active)
while True:
    time.sleep(1)

Scope

anecdata's POC on #11339 saw an invalid channel silently remapped by the IDF; here it raises instead.

wifi.Monitor (#11339) is a separate PR. A country setting, which decides which 5 GHz channels the IDF allows, is not part of this.

AI assistance

Claude drafted the change and the test scripts. I reviewed the diff, ran every test on the boards above, and checked the IDF headers for the country and band rules.

🤖 Generated with Claude Code

The channel was clamped to 1..13, so dual-band radios could not run a
5 GHz access point. Accept 1..165, matching start_scanning_networks(),
and let each port reject what its radio does not support:

- espressif: raise on esp_wifi_set_config() failure. An invalid channel
  raises ValueError. If this call turned AP mode on, turn it off again.
- raspberrypi: CYW43 is 2.4 GHz only; keep its bound at 13.

The IDF further limits 5 GHz by country and station band. With the
default world-safe country only 36-64 are allowed while disconnected.

Fixes adafruit#11338

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@anecdata

anecdata commented Sep 14, 2026

Copy link
Copy Markdown
Member

This is great, looks like you've got it! I'll do some testing after the artifacts are done.

Did you happen to look at the issue with custom AP MAC address on 5GHz... just curious if I missed some possible way of doing it (perhaps via some restructuring of our core code), or if it is an upstream issue. Definitely not an immediate issue.

@mikeysklar

Copy link
Copy Markdown
Collaborator Author

Yes. Tested on the ESP32-C5-DevKitC-1-N8R8 with this PR: radio off, start_ap on any channel, set mac_address_ap, radio on, associate to a US AP on 5 GHz, then start_ap(channel=149). A Linux client sees the AP on 149 with BSSID 1A:22:33:44:55:66. The IDF keeps the MAC across esp_wifi_start, so band mode order does not matter. Note it is stored in NVS and survives power cycles.

@anecdata

anecdata commented Sep 14, 2026

Copy link
Copy Markdown
Member

OK, I'll look at it again. It theoretically shouldn't need to associate as a station (a known wifi network may not be available).

One other thing... maybe should be its own issue. iirc when AP was first implemented, there wasn't a supported WPA2-WPA3 transitional authmode (or I think no WPA3 AP support at all ...edit: not at all #4650 (comment)).

Now there is support for WPA3, but we don't have a transitional that I can come up with, and [wifi.AuthMode.WPA2, wifi.AuthMode.WPA3, wifi.AuthMode.PSK] gives ValueError: Invalid authmode. Not sure why, it's just OR-ing bits afaict.

This works for WPA-WPA2 transitional: authmode=[wifi.AuthMode.WPA, wifi.AuthMode.WPA2, wifi.AuthMode.PSK].

edit: ah, WPA3 doesn't seem to work?

>>> AUTHMODE = [wifi.AuthMode.WPA3, wifi.AuthMode.PSK]
>>> wifi.radio.start_ap("Bob", "YourUncle", channel=48, authmode=AUTHMODE)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Invalid authmode

I guess it never got added into the common-hal check:

void common_hal_wifi_radio_start_ap(wifi_radio_obj_t *self, uint8_t *ssid, size_t ssid_len, uint8_t *password, size_t password_len, uint8_t channel, uint32_t authmode, uint8_t max_connections) {
set_mode_ap(self, true);
uint8_t esp_authmode = 0;
switch (authmode) {
case AUTHMODE_OPEN:
esp_authmode = WIFI_AUTH_OPEN;
break;
case AUTHMODE_WPA | AUTHMODE_PSK:
esp_authmode = WIFI_AUTH_WPA_PSK;
break;
case AUTHMODE_WPA2 | AUTHMODE_PSK:
esp_authmode = WIFI_AUTH_WPA2_PSK;
break;
case AUTHMODE_WPA | AUTHMODE_WPA2 | AUTHMODE_PSK:
esp_authmode = WIFI_AUTH_WPA_WPA2_PSK;
break;
default:
mp_arg_error_invalid(MP_QSTR_authmode);
break;
}

@mikeysklar

Copy link
Copy Markdown
Collaborator Author

On 149: that is the IDF default country. It ships as world-safe "01", which allows only 36-64 on 5 GHz until the station picks up a country from the AP it joins. CircuitPython never sets a country. A country setting is the fix and I would take it as a separate PR.

On WPA3: agreed, its own issue. The espressif start_ap switch only maps OPEN, WPA/PSK, WPA2/PSK and WPA/WPA2/PSK, so WPA3 and WPA2/WPA3 transitional never reach the IDF. If you file it I can pick it up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add 5GHz capability to wifi Access Point (AP)

2 participants