Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wifi.radio Access Point modes #4650

Merged
merged 20 commits into from
May 5, 2021
Merged

wifi.radio Access Point modes #4650

merged 20 commits into from
May 5, 2021

Conversation

anecdata
Copy link
Member

@anecdata anecdata commented Apr 23, 2021

Rough early implementation based on comments in #4246...

Current Assumptions: [outdated]

  • Station (STA) is the baseline default mode. Current users expect to be able to scan without any additional API calls to start up the station (scan requires STA or STA+AP mode; scan does not require connection to an AP). ping requires connection to an AP just like any other socket-based activity.
  • I don't think we want AP by default. Most users want STA. AP broadcasts its beacon constantly. AP adds security considerations for users.
  • I don't think there is any harm to STA by default, or STA in the form of STA+AP mode. There is no external effect from STA without user action. ESP32-S2 users aren't required to exercise STA by using scan, ping, or other radio activity.
  • Access Point (AP) is started by user code to create STA+AP mode.
  • Single event handler due to STA and AP events sharing the WIFI_EVENT_ base.
  • Single radio object with separate STA and AP configs and netifs
  • Like STA, AP netif is created at wifi init, and destroyed at reset

Current Status: [outdated]

  • AP has params ssid and password only (needs channel, authmode, etc.)
  • wifi.radio.start_ap(ssid, pw) starts the AP, and the AP is then visible to other devices on the network due to broadcasting beacon frames
  • wifi.radio.get_mac_address_ap returns the proper AP MAC address (STA MAC + 1)
  • wifi.radio.enabled = boolean_value toggles the wifi (AP & STA) between started and stopped; modes are retained, connections are dropped

Open Questions:

  • API...
  • Implementation...
  • Testing...
  • Futures: light sleep considerations, WIFI_EVENT_AP_STA[DIS]CONNECTED, WIFI_EVENT_AP_PROBEREQRECVED,...

Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me! I do like the idea of treating sta and ap modes independently. So you would be able to do AP only mode if you only start the AP.

shared-bindings/wifi/Radio.c Outdated Show resolved Hide resolved
@anecdata
Copy link
Member Author

anecdata commented Apr 23, 2021

Just a note: I can't think of any material downsides to using STA+AP mode for achieving AP mode. There is some resource load, I don't know how significant, a data structure or two. But STA+AP can be used just like AP-only, by just not invoking any STA functionality (scan, ping, connect, etc.)

The trick about AP-only mode is that STA currently starts by default, so if we want to allow AP-only as opposed to STA+AP, there are two ways I can think of:

  1. Don't start STA by default (don't start anything by default). This breaks existing code since it requires a new API call to start STA, for example to do an unconnected scan or more involved STA activity.
  2. Continue to start STA by default, but provide APIs for turning it on and off, and provide an API for turning AP off also, so that both modes can be started and stopped independently, and they can co-exist (STA+AP) if desired.

Both options add some coordination complexity that I need to investigate.

BTW, I haven't actually gotten a separate station connected to the AP yet, hopefully will get some time this weekend.

Addendum:

I think there's a 3rd way to handle STA, without an explicit new start_station() user API needed at the top of code.py: start_station() before anything that needs STA, but don't automatically (re-)start it in wifi.radio.set_enabled (which is also called from wifi init). start_scanning_networks() and connect() already do this, but ping() doesn't (ping actually does need some rudimentary checks, it gets OSError 0 if wifi isn't enabled/started).

Bottom line: I could use some architectural direction on the API / how we want these modes to behave for users, particularly on startup.

Addendum 2:

The nomenclature is quickly getting out of hand too - functions and variables. I've been adding _ap to things to differentiate them from the originals (STA, but not designated so). Do we want to try to normalize the names throughout to designate if they are STA or AP? But, at the CP user API level, that would be breaking so I suspect we don't want that. I guess another option is to add an optional mode parameter for these functions, with STA mode as default. A good example of this is:
common_hal_wifi_radio_get_ipv4_address(wifi_radio_obj_t *self)
and
common_hal_wifi_radio_get_ipv4_address_ap(wifi_radio_obj_t *self)

Addendum 3 (from the future):

Went with Option 2 above - full independence with auto-STA at init.

@anecdata
Copy link
Member Author

anecdata commented Apr 24, 2021

Connection to AP (open wifi) working now:

W (6142) wifi: got ip
I (6212) wifi:mode : sta (7c:df:re:da:ct:8c) + softAP (7c:df:re:da:ct:8d)
I (6212) wifi:Total power save buffer number: 8
I (6212) wifi:Init max length of beacon: 752/752
I (6212) wifi:Init max length of beacon: 752/752
W (6222) wifi: ap start
I (6222) wifi:Total power save buffer number: 8
W (6222) wifi: ap stop
W (6232) wifi: ap start
W (6232) radio: ssid=Bob
W (6232) radio: password=
W (6242) radio: max_connection=4
I (78692) wifi:new:<1,0>, old:<1,0>, ap:<1,1>, sta:<1,0>, prof:6
I (78692) wifi:station: 8c:85:90:re:da:ct join, AID=1, bgn, 20
W (78692) wifi: ap sta connected
I (78782) esp_netif_lwip: DHCP server assigned IP to a station, IP is: 192.168.4.3

CircuitPython STA+AP SSID "Bob" running a UDP socket server:

Self AP IP 192.168.4.1
Create UDP Server socket
Received bytearray(b'Hello, world') 12 bytes from ('192.168.4.3', 59548)
Sent bytearray(b'Hello, world') 12 bytes to ('192.168.4.3', 59548)

macOS wifi STA connected to CircuitPython AP SSID "Bob", Ethernet off, running CPython UDP socket client:

Create UDP Client Socket
Sent 12 bytes
Received bytearray(b'Hello, world') 12 bytes from ('192.168.4.1', 5000)

I think this is doing what I think it's doing. Don't worry, I won't spam every little test result :p but this was the first successful connection to the AP, with socket stuff running on top and working fine. Pleasantly surprised.

Addendum:

Other authmodes should be fine, tested WIFI_AUTH_WPA2_PSK successfully.

Note that the ESP32-S2 can operate on only one channel at a time. If STA is connected, AP will use the same channel. If STA is not connected, AP can choose its channel. Needs more testing, only once I think I saw a connected station getting stopped when starting an AP with a different channel. But trying it more, the AP just uses the same channel as the STA.

@anecdata
Copy link
Member Author

anecdata commented Apr 25, 2021

Current status:

STA starts up by default in __init__.c common_hal_wifi_init().

STA and AP can be independently started and stopped, generally with no disruption to the other.

wifi mode is retained through transition from wifi.radio.enabled = False (wifi stop) to wifi.radio.enabled = True (wifi start).

There are four wifi modes:

  • Station
  • AP
  • Station + AP
  • NULL (turns off both Station and AP, but wifi remains init'ed

There's more fleshing out to do... additional AP params in start_ap(), getting more data about the AP besides just MAC and IP address, getting info about connected stations, etc.

@anecdata anecdata added the espressif applies to multiple Espressif chips label Apr 25, 2021
@anecdata
Copy link
Member Author

anecdata commented Apr 25, 2021

Latest commit allows sequences like this:

>>> wifi.radio.enabled = False
>>> wifi.radio.stop_station()
>>> wifi.radio.stop_ap()
>>> wifi.radio.enabled = True
>>> # wifi is started, but neither STA nor AP are now active
>>> 
>>> wifi.radio.enabled = False
>>> wifi.radio.start_station()
>>> wifi.radio.enabled = True
>>> # wifi is started, STA is now active and can be used

@anecdata
Copy link
Member Author

anecdata commented Apr 26, 2021

A couple of comments on authmode:

  • Not all of the advertised authmodes are supported (notably WPA3 modes), perhaps related to:
  • "Currently, PMF is supported only in Station mode." (Protected Management Frames)
  • Supported modes are OPEN, WPA_PSK, WPA_WPA2_PSK, WPA2_PSK
  • WPA2_PSK shows up to some external stations as WPA/WPA2 Personal, perhaps because it's (PSK/AES,TKIP/TKIP) rather than (PSK/AES/AES)

@anecdata
Copy link
Member Author

anecdata commented Apr 26, 2021

I think this is ready to come out of draft. I had little idea what I was getting into, and I don't know what I don't know. It needs careful review and test, I'm sure others will find novel ways to exercise the code ;-).

An oddity... there's nothing in the Espressif NETIF or WiFi APIs to get the IP address of connected stations. Maybe I'm missing something obvious. It is possible to get their MAC address and an aid counter, so you could de-auth them (not implemented here). DHCP is behind the scenes, although the DEBUG logging does put the assigned IP address out there:

I (248822) esp_netif_handlers: sta ip: 192.168.6.183, mask: 255.255.252.0, gw: 192.168.4.1

Probably not a big deal when the AP is also the "server" in the IP sense since clients usually need to be directed to the server anyway, bit if it's say a UDP client, it would need to know the IP address of the UDP server running on a connected Station. The AP has default IP address 192.168.4.1 and DHCP seems to count up from there, but I wouldn't count on that. You could ping and add some protocol to figure it out at the app level, but surely there's a better way?

Function/variable naming is a little out of control for mirror AP/Station items, suggestions welcome.

I can't help think that the mode change functions could be more efficient than a bunch of if statements. I could do bitwise operations on the wifi_mode_t enum with WIFI_MODE_AP and the like, but Espressif could pull the rug out by changing the numbering.

@anecdata anecdata marked this pull request as ready for review April 26, 2021 21:24
@jposada202020
Copy link
Collaborator

@anecdata I could help with some testing, this is intended to work with the ESP32S2 and Pyportal?
Do you have some simpletest code to share? Or do I use the stubs?

@tannewt
Copy link
Member

tannewt commented Apr 26, 2021

I think this is ready to come out of draft. I had little idea what I was getting into, and I don't know what I don't know. It needs careful review and test, I'm sure others will find novel ways to exercise the code ;-).

I'm happy to merge this quickly. It'll end up in 7.x which will need lots of testing anyway. Getting folks to try it out is the easiest way to find the issues. :-)

@anecdata
Copy link
Member Author

anecdata commented Apr 26, 2021

jposada202020 Thank you! I have no idea about stubs. I can post some code (I'll edit this comment in a couple of minutes). The API is pretty simple. ESP32-S2-only as the Access Point. Anything can be a wifi station to it.

The first step to testing is to start the AP, then connect some external station to it. That doesn't actually do anthing though, so as a second step I've been running some TCP or UDP stuff over that connection... that's a little more work to set up, but the same as on any other pair of devices.

Some basic annotated code:

# ESP32-S2 wifi radio SoftAP changes:
#
# the key concept is `mode`, one of: Station (STA), Access Point (AP), STA+AP, (NONE)
# before now, ESP32-S2 wifi was always in Station mode, ready to scan or connect to an AP
# now Station and AP can be independently controlled, and the ESP32-S2 can be either, both, or neither
# note that AP mode isn't a router; it's typically going to be an IP server endpoint to IP clients
#
# mode is also independent of `enabled` state, which turns wifi on or off
# mode can be changed when wifi is enabled or not enabled
#
# the maximum number of connected stations is currently 4

import wifi
import ipaddress
import socketpool
import time

from secrets import secrets

# at this point, the ESP32-S2 is running as a station (init default), and if desired can connect to any AP

# the following two lines are not new APIs, but they are useful to test in combination with mode changes:
wifi.radio.enabled = False  # turns wifi off, mode is retained or can be changed while not enabled
wifi.radio.enabled = True  # turns wifi back on
print("Wi-Fi Enabled?", wifi.radio.enabled)

print(dir(wifi.radio))  # useful reference

print("Stopping the (default) station...")
wifi.radio.stop_station()  # now the device is in NONE mode, neither Station nor Access Point
# print("(Re-)Starting the station...")
# wifi.radio.start_station()  # would restart the station and later you would have both Station and AP running

# start the AP, `channel` of your choosing, `authmode` of your choosing:
# The Access Point IP address will be 192.168.4.1
# ...if that collides with your LAN, you may need to isolate the external station from your LAN
print("Starting AP...")
wifi.radio.start_ap("Bob", "YourUncle", channel=6, authmode=wifi.radio.WPA2)

# connect from some client(s), check their interfaces to verify the wi-fi is connected, channel, authmode,...


# Fudge an HTTP response back to a browser on a connected wifi station
HOST = ""  # see below
PORT = 80
TIMEOUT = None
BACKLOG = 2
MAXBUF = 1024

# get some sockets
pool = socketpool.SocketPool(wifi.radio)

print("AP IP Address:", wifi.radio.ipv4_address_ap)
print("      Gateway:", wifi.radio.ipv4_gateway_ap)
print("       Subnet:", wifi.radio.ipv4_subnet_ap)
HOST = str(wifi.radio.ipv4_address_ap)

print("Create TCP Server socket", (HOST, PORT))
s = pool.socket(pool.AF_INET, pool.SOCK_STREAM)
s.settimeout(TIMEOUT)

s.bind((HOST, PORT))
s.listen(BACKLOG)
print("Listening")

inbuf = bytearray(MAXBUF)
while True:
    print("Accepting connections")
    conn, addr = s.accept()
    conn.settimeout(TIMEOUT)
    print("Accepted from", addr)

    size = conn.recv_into(inbuf, MAXBUF)
    print("Received", size, "bytes")
    print(inbuf[:size])

    outbuf = b"HTTP/1.0 200 OK\r\n" + \
             b"Connection: close\r\n" + \
             b"\r\n" + \
             b"<html>Hello, world!<pre>" + \
             inbuf[:size] + \
             b"</pre></html>"

    conn.send(outbuf)
    print("Sent", len(outbuf), "bytes")

    conn.close()


# the rest is exercise left for the reader, could try some socket stuff
# (some examples at <https://github.com/anecdata/Socket>)
# or anything else that's convenient to set up.

print("Stopping the AP...")
wifi.radio.stop_ap()  # close down the shop
# </fin>

You should be able to do virtually any sequences of:

  • wifi.radio.enabled = True/False
  • wifi.radio.start/stop_station()
  • wifi.radio.start/stop_ap()
  • wifi.radio.connect()
  • scanning the network
  • (ping is quirky, may or may not work depending on other states)

Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code structure looks good to me. One comment about the security enum values. Looks good otherwise.

shared-bindings/wifi/Radio.c Outdated Show resolved Hide resolved
@jposada202020
Copy link
Collaborator

Preliminary testing done in

Adafruit CircuitPython 7.0.0-alpha.1-853-gcbe1aa74f on 2021-04-26; Adafruit Metro ESP32S2 with ESP32S2

Test Code

>>> import wifi
>>> import ipaddress
>>> import socketpool
>>> import time
>>> wifi.radio.enabled = True
>>> wifi.radio.start_ap("Bob", "YourUncle", channel=6, authmode=wifi.radio.WPA2_PSK)

Result

Could connect from a Samsung S9+ Phone
image

@jposada202020
Copy link
Collaborator

@anecdata I could try different stuff but it has been a while since I do AP/Internet stuff, even with the help of your repo code... last time I was using flask, so you could put a date on that :)

@anecdata
Copy link
Member Author

anecdata commented Apr 26, 2021

@jposada202020 Looks good, thanks! "without internet" lol, I was wondering how hard it would be to write a router or at least a packet forwarder, or if it's even possible in this environment.

It would be equally useful to get confirmation that these changes didn't break various folks' existing station scan and connect code. If you have existing wifi stuff that still works fine on this version, that would be a good sign :-)

@anecdata
Copy link
Member Author

anecdata commented Apr 27, 2021

I updated the code above to make it a little more interesting. Run it, then connect to the new AP and try in a browser to go to:
http://192.168.4.1
It's not fully to spec, but it should work in most browsers. It's a TCP socket server with barely enough HTTP to fake out a browser.
Running something over the AP connection just confirms that the AP is functional and not just pretending ;-)

@jposada202020
Copy link
Collaborator

jposada202020 commented Apr 27, 2021

Ok will try. Thanks for the code. :)
image

@anecdata There you go!

@anecdata anecdata marked this pull request as draft April 27, 2021 18:31
@anecdata anecdata marked this pull request as ready for review April 28, 2021 21:25
@anecdata anecdata changed the title wifi.radio Access Point (STA+AP) mode wifi.radio Access Point modes Apr 30, 2021
@microdev1
Copy link
Collaborator

AuthMode can now be set by:

import wifi
from wifi import AuthMode
wifi.radio.start_ap("ssid", "password", authmode=[AuthMode.WPA, AuthMode.WPA2, AuthMode.PSK])

@microdev1 microdev1 linked an issue Apr 30, 2021 that may be closed by this pull request
@anecdata
Copy link
Member Author

anecdata commented Apr 30, 2021

@tannewt Some of the authmodes are not currently supported for AP... should we remove them or comment them out in start_ap(), or raise an exception? Currently, they will appear to be successful, but there will be no AP.

@microdev1
Copy link
Collaborator

microdev1 commented Apr 30, 2021

Some of the authmodes are not currently supported for AP.

An exception should be raised as that is port specific. From the idf docs, I see only WEP being unsupported in soft-ap.

@anecdata
Copy link
Member Author

From the idf docs, I see only WEP being unsupported in soft-ap.

I would have thought so too, but the WPA3 modes fail with the same error in the DEBUG console as with WEP. And we also haven't implemented Enterprise. So the only working AP authmodes currently are: WPA, WPA2, WPA-WPA2, and Open.

@microdev1
Copy link
Collaborator

So the only working AP authmodes currently are: WPA, WPA2, WPA-WPA2, and Open.

Yup! just checked, only these authmodes are supported. Thanks for pointing that out.

@tannewt
Copy link
Member

tannewt commented Apr 30, 2021

Some of the authmodes are not currently supported for AP.

An exception should be raised as that is port specific. From the idf docs, I see only WEP being unsupported in soft-ap.

100% agree

@anecdata
Copy link
Member Author

I can add the exception in start_ap(), may take me a couple of days, I'm in the process of moving my office and my computer will be going down for an indeterminate length of time :-(. If anyone else feels like doing it, feel free.

@anecdata
Copy link
Member Author

anecdata commented Apr 30, 2021

oh, you did it already @microdev1 , nice! are we ready to merge then?

Tested most valid and invalid combinations of SSID, password, and AuthMode. Behaved as expected.

@microdev1 microdev1 requested a review from tannewt May 5, 2021 02:52
{ MP_ROM_QSTR(MP_QSTR_stop_station), MP_ROM_PTR(&wifi_radio_stop_station_obj) },
{ MP_ROM_QSTR(MP_QSTR_stop_ap), MP_ROM_PTR(&wifi_radio_stop_ap_obj) },
{ MP_ROM_QSTR(MP_QSTR_start_ap), MP_ROM_PTR(&wifi_radio_start_ap_obj) },

{ MP_ROM_QSTR(MP_QSTR_connect), MP_ROM_PTR(&wifi_radio_connect_obj) },
// { MP_ROM_QSTR(MP_QSTR_connect_to_enterprise), MP_ROM_PTR(&wifi_radio_connect_to_enterprise_obj) },

{ MP_ROM_QSTR(MP_QSTR_ap_info), MP_ROM_PTR(&wifi_radio_ap_info_obj) },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ap_info is a bit confusing with the new api, should this be re-named?

Copy link
Member Author

@anecdata anecdata May 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I mentioned earlier that naming is an issue. There's a natural confusion between AP-things related to the AP an ESP32-S2 Station is connected to, vs. the ESP32-S2 as an AP. Changing it would break existing code, but now's the time if we're going to do that. Maybe connected_ap_info or remote_ap_info? Whatever we do, RTD and examples could be beefed up to clarify usage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do this in a follow up PR too.

Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me! Thank you!

@tannewt tannewt merged commit 30c7aa8 into adafruit:main May 5, 2021
@anecdata anecdata deleted the ap branch May 6, 2021 21:39
@ZHNscl
Copy link

ZHNscl commented May 9, 2021

Hello.I would like to ask how to open the AP module of IOTS2 module?Has this module been released yet?

@anecdata
Copy link
Member Author

anecdata commented May 9, 2021

It's part of the wifi module on ESP32-S2 builds since this merge (7.0.0 alpha builds). You can find "Absolute Newest" builds for your board here: https://circuitpython.org/downloads. Documentation is here: https://circuitpython.readthedocs.io/en/latest/shared-bindings/wifi/index.html#wifi.radio. If you have further questions, I'd suggest asking on Adafruit's #help-with-circuitpython Discord channel: http://adafru.it/discord

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement espressif applies to multiple Espressif chips network
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ESP32S2: implement API for Access Point mode
5 participants