Skip to content

AirLift support for wifi, socketpool, and ssl modules#10786

Draft
dhalbert wants to merge 15 commits intoadafruit:mainfrom
dhalbert:wifi-airlift
Draft

AirLift support for wifi, socketpool, and ssl modules#10786
dhalbert wants to merge 15 commits intoadafruit:mainfrom
dhalbert:wifi-airlift

Conversation

@dhalbert
Copy link
Collaborator

Implement wifi/socketpool/ssl for Airlift (NINA-FW) co-processors.

Starting this as a draft, for testing purposes. Tested with Adafruit NINA-FW 3.4.0.

  • Web workflow not yet implemented.
  • HTTP and HTTPS client work. UDP client and server work.
  • HTTP server gets an error after a minute or so of continuous polling, alternating GET_STATE_TCP_CMD and AVAIL_DATA_TCP_CMD. The failure is that the ready line from the AirLift is in the wrong state. This may be some failure or race condition on the AirLift side. I have some other ideas for investigating this as well. Adding some delays did not help. Error is repeated:
Traceback (most recent call last):
  File "adafruit_httpserver/server.py", line 482, in poll
TimeoutError: timeout waiting for ready False
  • Main implementation is in devices/airlift.
  • wifi.radio.init_airlift() added to specify pins to use.
  • CIRCUITPY_{WIFI,SSL,SOCKETPOOL} further refined as CIRCUITPY_*_NATIVE and CIRCUITPY_*_AIRLIFT. Compiled guards changed appropriately.
  • Fixed common_hal_socketpool_socket_bind() return type from size_t to int.
  • Fixed some socketpool function names which were not named canonically.
  • Added common_hal_wifi_radio_get_version(().
  • common_hal_socketpool_socket_recv_into() and socketpool_socket_recv_into() buffer args were const and should not have been.
  • Use -flto=auto instead of -flto=jobserver to fix spurious message about jobserver.
  • Simplify arg validation in Freeverb,c (found while consolidating some error messages).
  • Check for deinited SPI in SPI try_lock().
  • Add some internal IPv4 address conversion functions in IPv4Address. Simplify some conversion code.
  • Be more precise with uses of "host", "address", and "port" in socket documentaiton.
  • Add an elem_print_helper() routines in shared_bindings/util.c to make printing objects easier. Used to give more info when printing a Network object.
  • Add port_malloc_check() and port_realloc_check() routines. These are not used now, but were added when it looked like I needed to do port mallocs for talking to the AirLift.

There were a lot more commits with some dead ends and snapshot commits but I rebased to only a few.

get/set hostname

power mgmt, ip config; compiles

wip

compiles

SSID connection working

fix authmode in scanned results

getaddrinfo

fix incorrect const in socket recv API

wip: progress on socket ops

wip

wip; removing heap ops; still need to do sock ops

HTTP fetches work

wip ssl

HTTPS fetching works

wip: manage nina client and server

UDP working both ways
@dhalbert dhalbert marked this pull request as draft January 27, 2026 17:10
@dhalbert
Copy link
Collaborator Author

dhalbert commented Feb 2, 2026

Some test code I have been running on a PyPortal, which is basically like the typical WiFi test program you find in the guides.
Update the NINA-FW firmware on the AirLift co-processor to whatever is latest (currently 3.3.0).

import os
import board
from digitalio import DigitalInOut
import displayio
import ipaddress
import ssl
import wifi
import socketpool
import adafruit_requests
import sys
import time

displayio.release_displays()

# URLs to fetch from
HTTP_TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html"
HTTPS_TEXT_URL = "https://www.example.org"
HTTPS_TEXT_URL = "https://www.adafruit.com/api/quotes.php"
JSON_QUOTES_URL = "https://www.adafruit.com/api/quotes.php"
JSON_STARS_URL = "https://api.github.com/repos/adafruit/circuitpython"

print("wifi AirLift test")

wifi.radio.init_airlift(
    board.SPI(),
    DigitalInOut(board.ESP_CS),
    DigitalInOut(board.ESP_BUSY),
    DigitalInOut(board.ESP_RESET)
    )

#### device info test
mac_address = ':'.join(f"{i:02x}" for i in wifi.radio.mac_address)
print(f"MAC address: {mac_address}")
print("AirLift Firmware version:", wifi.radio.version)
print(f"Hostname: {wifi.radio.hostname}")
wifi.radio.hostname = "another-name"
print(f"New hostname: {wifi.radio.hostname}")
print(f"{wifi.radio.ipv4_dns=}")

#### WiFi scan test
print("Available WiFi networks:")
for network in wifi.radio.start_scanning_networks():
    print(network)
wifi.radio.stop_scanning_networks()

#### Connect and info test
print(f"Connecting to {os.getenv('CIRCUITPY_WIFI_SSID')}")
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}")
print(f"My IP address: {wifi.radio.ipv4_address}")
print(f"{wifi.radio.ap_info=}")

#### Ping test
ping_ip = ipaddress.IPv4Address("8.8.8.8")
ping = wifi.radio.ping(ip=ping_ip)

if ping is None:
    print("Couldn't ping 'google.com' successfully")
else:
    # convert s to ms
    print(f"Pinging 'google.com' took: {ping * 1000} ms")

pool = socketpool.SocketPool(wifi.radio)
print(f"{pool.getaddrinfo("adafruit.com", 80)=}")

#### UDP test
# address = ("192.168.1.83", 12000)
# udp_socket = pool.socket(pool.AF_INET, pool.SOCK_DGRAM)
# udp_socket.settimeout(1.0)

# buf = bytearray(1024)
# for _ in range(3):
#     print("UDP loop")
#     message = str(time.time()).encode("utf-8")
#     udp_socket.sendto(message, address)
#     try:
#         # Get echoed message
#         length, server = udp_socket.recvfrom_into(buf)
#         print("Echo: ", buf[0:length], "from", server)
#     except Exception as e:
#         print("Catching", type(e), e, "no reponse")
#     time.sleep(2)
# udp_socket.close()


requests = adafruit_requests.Session(pool, ssl.create_default_context())

#### HTTP test
start = time.monotonic()
print(f"Fetching text from {HTTP_TEXT_URL}")
response = requests.get(HTTP_TEXT_URL)
print("-" * 80)
print(response.text)
print("-" * 40)
response.close()
print(f"fetch: {time.monotonic() -start} secs")
print()

#### HTTPS test
start = time.monotonic()
print(f"Fetching text from {HTTPS_TEXT_URL}")
response = requests.get(HTTPS_TEXT_URL)
print("-" * 80)
print(response.text)
print("-" * 40)
response.close()
print(f"fetch: {time.monotonic() -start} secs")
print()

#### HTTPS JSON test
# print(f"Fetching json from {JSON_QUOTES_URL}")
# response = requests.get(JSON_QUOTES_URL)
# print("-" * 80)
# print(response.json())
# print("-" * 40)

# print()

# print(f"Fetching and parsing json from {JSON_STARS_URL}")
# response = requests.get(JSON_STARS_URL)
# print("-" * 80)
# print(f"CircuitPython GitHub Stars: {response.json()['stargazers_count']}")
# print("-" * 40)

print("Done")

A UDP server to run on a host computer for testing:

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind(("", 12000))

while True:
    message, address = server_socket.recvfrom(1024)
    print("Received:, message)

@bablokb
Copy link

bablokb commented Feb 2, 2026

I have an iperf3 implementation which you might want to test with: https://github.com/bablokb/circuitpython-iperf

@dhalbert
Copy link
Collaborator Author

dhalbert commented Feb 2, 2026

@anecdata @RetiredWizard @Neradoc may be of interest to you

@RetiredWizard
Copy link

I either don't understand frozen modules or I built this wrong. I was assuming that all the libraries included in the frozen directory would be available without having to install an mpy version in the lib folder. I built the Makerfabs S3 TFT board but the connection_manager library isn't seen unless I put the file on the board.

When I did put the connection_manager and requests library on the board the SSL library wasn't available. I was able to fix this by adding CIRCUITPY_SSL_NATIVE = 1 to the makerfabs mpconfigboard.mk file but I'm guessing the change should actually be in the port version of mpconfigboard.mk. Looking through that file the only time the SSL_NATIVE parameter is set is when it is set to 0 for the P4 chip. I'm thinking it should default to 1 for all the WiFi enable espressif chips?

Once I made the parameter change WiFi seemed to work fine on the makerfabs board. I'll do a little more testing though. After the makerfabs, I'll try a couple RP2xx0 board boards.

Where there any particular boards or functionality you thought I should be looking at?

@dhalbert
Copy link
Collaborator Author

dhalbert commented Feb 3, 2026

@RetiredWizard Sorry! I forgot I only turned this on for SAMD51J20 boards. It is not '_NATIVE, it is CIRCUITPY_WIFI_AIRLIFT and what it turns on. It does not fit right now on SAMD51J19 boards, but I should see if I can make it fit. I will also see about some other boards like RP2350 and RP2040 without a CYW43.

@RetiredWizard
Copy link

Ah ok, I think the only thing I have that comes close is the matrix portal M4 (j19). let me know if/when you want me to run some tests on that. I can also check other WiFi boards just to make sure this update doesn't cause issues. Just let me know how I can help 😁

Copy link
Member

Choose a reason for hiding this comment

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

What are these frozen changes for? (Just a first glance at this. I haven't looked at the devices code yet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was meant to synchronize with main re frozen modules. I'm not sure what happened. I'll remove them.

@bablokb
Copy link

bablokb commented Feb 4, 2026

Since Airlift isn't compatible with ethernet, does this imply that the Wiznet Eth-breakouts can't be used anymore?

@dhalbert
Copy link
Collaborator Author

dhalbert commented Feb 4, 2026

Since Airlift isn't compatible with ethernet, does this imply that the Wiznet Eth-breakouts can't be used anymore?

I'll have to figure this out -- the problem is that two versions of ssl are needed. If the board doesn't provide AirLift support, then there's no conflict.

This is why this is a draft :) .

I could change the airlift support ssl to airlift_ssl, and one could do import airlift_ssl as ssl most of the time. I wonder if the other airlift modules should be similarly renamed, so that most of the time you would do:

import airlift_wifi as wifi
import airlift_socketpool as socketpool
import airlift_ssl as ssl

But then adafruit_connection_manager needs to know about this too.

Or perhaps ssl can decide what to do at run time, based on the socketpool it is given. socketpool would also need to do run-time checking on the Radio it was given.

@anecdata
Copy link
Member

anecdata commented Feb 4, 2026

Or perhaps ssl can decide what to do at run time, based on the socketpool it is given. socketpool would also need to do run-time checking on the Radio it was given.

This sounds like core code taking on some of what connection_manager does, which seems fine, though the distinct naming may allow more run-time configurability?

A small consideration: using two radio types on a project. I suspect wifi/airlift + ethernet would be more in demand than wifi + airlift. Or at least the ability to use airlift or ethernet on a native wifi board (that may have been chosen for its other features).

@RetiredWizard
Copy link

RetiredWizard commented Feb 5, 2026

I loaded the matrix_portal_m4 artifact and it partially worked with my programs. I'm not sure, but I think connection manager may need to be updated as it looks like it decided to try and use the adafruit_esp32spi library in at least one case:

This was the fault of my driver program, never mind....
>> wifi_weather.wifi_weather()
  File "adafruit_esp32spi/adafruit_esp32spi_socketpool.py", line 126, in connect

I also noticed that if the SCK pin was accessed after it was in use (and presumably not deinit'd) any subsequent Ctrl-D resulted in a safemode crash: This was fixed by updating the Airlift firmware....

Adafruit CircuitPython 10.1.0-beta.1-31-g7decafea35 on 2026-02-05; Adafruit Matrix Portal M4 with samd51j19
>>> import getdate
Enter your current timezone offset (enter for default): 
Connecting to AP...
Connected to twilightzone!
My IP address is 10.0.0.236
Attempting to set Date/TimeTrying NTP....
Time and Date successfully set
>>> getdate.getdate()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "getdate.py", line 23, in getdate
  File "/lib/pydos_wifi.py", line 120, in connect
ValueError: SCK in use
>>> 

[16:23:15.926] Disconnected
[16:23:16.927] Warning: Could not open tty device (No such file or directory)
[16:23:16.927] Waiting for tty device..
[16:23:17.929] Connected
Auto-reload is off.
Running in safe mode! Not running saved code.

You are in safe mode because:
CircuitPython core code crashed hard. Whoops!
Hard fault: memory access or instruction error.
Please file an issue with your program at github.com/adafruit/circuitpython/issues.
Press reset to exit safe mode.

Here are the results from running your test code:

Click to expand Adafruit CircuitPython 10.1.0-beta.1-31-g7decafea35 on 2026-02-05; Adafruit Matrix Portal M4 with samd51j19 >>> import dantest wifi AirLift test >cmd GET_MACADDR_CMD (22) >>param #0 (length: 1) --x ff --d 255 --c "�"

cmd SET_HOSTNAME (16)

param #0 (length: 14) --x 41 69 72 4c 69 66 74 2d 38 36 45 36 39 34 --d 65 105 114 76 105 102 116 45 56 54 69 54 57 52 --c "AirLift-86E694"
<response #0 (length 1) --x 01 --d 1 --c ""

cmd GET_MACADDR_CMD (22)

param #0 (length: 1) --x ff --d 255 --c "�"
<response #0 (length 6) --x 88 9a 06 86 e6 94 --d 136 154 6 134 230 148 --c "����"

MAC address: 88:9a:06:86:e6:94

cmd GET_FW_VERSION_CMD (37)
<response #0 (length 6) --x 31 2e 32 2e 32 00 --d 49 46 50 46 50 0 --c "1.2.2"

AirLift Firmware version: 1.2.2
Hostname: AirLift-86E694

cmd SET_HOSTNAME (16)

param #0 (length: 12) --x 61 6e 6f 74 68 65 72 2d 6e 61 6d 65 --d 97 110 111 116 104 101 114 45 110 97 109 101 --c "another-name"
<response #0 (length 1) --x 01 --d 1 --c ""

New hostname: another-name

cmd GET_DNS_CONFIG_CMD (1e)

param #0 (length: 1) --x ff --d 255 --c "�"
{wifi.radio.ipv4_dns=}
Traceback (most recent call last):
File "dantest.py", line 45, in
RuntimeError: Error response to AirLift command
Available WiFi networks:
cmd START_SCAN_NETWORKS (36)
<response #0 (length 1) --x 01 --d 1 --c ""

cmd SCAN_NETWORKS (27)
<response #0 (length 12) --x 74 77 69 6c 69 67 68 74 7a 6f 6e 65 --d 116 119 105 108 105 103 104 116 122 111 110 101 --c "twilightzone"

<response #1 (length 18) --x 74 77 69 6c 69 67 68 74 7a 6f 6e 65 2d 67 75 65 73 74 --d 116 119 105 108 105 103 104 116 122 111 110 101 45 103 117 101 115 116 --c "twilightzone-guest"

<response #2 (length 14) --x 56 65 72 69 7a 6f 6e 5f 58 37 42 37 4a 51 --d 86 101 114 105 122 111 110 95 88 55 66 55 74 81 --c "Verizon_X7B7JQ"

<response #3 (length 14) --x 56 65 72 69 7a 6f 6e 5f 58 37 42 37 4a 51 --d 86 101 114 105 122 111 110 95 88 55 66 55 74 81 --c "Verizon_X7B7JQ"

<response #4 (length 4) --x 48 6f 6d 65 --d 72 111 109 101 --c "Home"

cmd GET_IDX_BSSID_CMD (3c)

param #0 (length: 1) --x 00 --d 0 --c ""
<response #0 (length 6) --x e0 30 d1 36 5d 48 --d 224 48 209 54 93 72 --c "�0�6]H"

cmd GET_IDX_RSSI_CMD (32)

param #0 (length: 1) --x 00 --d 0 --c ""
<response #0 (length 4) --x bd ff ff ff --d 189 255 255 255 --c "����"

cmd GET_IDX_CHAN_CMD (3d)

param #0 (length: 1) --x 00 --d 0 --c ""
<response #0 (length 1) --x 0b --d 11 --c "
"

cmd GET_IDX_ENCT_CMD (33)

param #0 (length: 1) --x 00 --d 0 --c ""
<response #0 (length 1) --x 04 --d 4 --c ""

cmd GET_IDX_BSSID_CMD (3c)

param #0 (length: 1) --x 01 --d 1 --c ""
<response #0 (length 6) --x e1 30 d1 36 5d 4a --d 225 48 209 54 93 74 --c "�0�6]J"

cmd GET_IDX_RSSI_CMD (32)

param #0 (length: 1) --x 01 --d 1 --c ""
<response #0 (length 4) --x bd ff ff ff --d 189 255 255 255 --c "����"

cmd GET_IDX_CHAN_CMD (3d)

param #0 (length: 1) --x 01 --d 1 --c ""
<response #0 (length 1) --x 0b --d 11 --c "
"

cmd GET_IDX_ENCT_CMD (33)

param #0 (length: 1) --x 01 --d 1 --c ""
<response #0 (length 1) --x 04 --d 4 --c ""

cmd GET_IDX_BSSID_CMD (3c)

param #0 (length: 1) --x 02 --d 2 --c ""
<response #0 (length 6) --x 6d 29 d7 c5 bd 3c --d 109 41 215 197 189 60 --c "m)�Ž<"

cmd GET_IDX_RSSI_CMD (32)

param #0 (length: 1) --x 02 --d 2 --c ""
<response #0 (length 4) --x a9 ff ff ff --d 169 255 255 255 --c "����"

cmd GET_IDX_CHAN_CMD (3d)

param #0 (length: 1) --x 02 --d 2 --c ""
<response #0 (length 1) --x 01 --d 1 --c ""

cmd GET_IDX_ENCT_CMD (33)

param #0 (length: 1) --x 02 --d 2 --c ""
<response #0 (length 1) --x 04 --d 4 --c ""

cmd GET_IDX_BSSID_CMD (3c)

param #0 (length: 1) --x 03 --d 3 --c ""
<response #0 (length 6) --x 3d f1 f6 c5 bd 3c --d 61 241 246 197 189 60 --c "=��Ž<"

cmd GET_IDX_RSSI_CMD (32)

param #0 (length: 1) --x 03 --d 3 --c ""
<response #0 (length 4) --x a9 ff ff ff --d 169 255 255 255 --c "����"

cmd GET_IDX_CHAN_CMD (3d)

param #0 (length: 1) --x 03 --d 3 --c ""
<response #0 (length 1) --x 01 --d 1 --c ""

cmd GET_IDX_ENCT_CMD (33)

param #0 (length: 1) --x 03 --d 3 --c ""
<response #0 (length 1) --x 04 --d 4 --c ""

cmd GET_IDX_BSSID_CMD (3c)

param #0 (length: 1) --x 04 --d 4 --c ""
<response #0 (length 6) --x 0a 00 3c 3b 22 14 --d 10 0 60 59 34 20 --c "
<;""

cmd GET_IDX_RSSI_CMD (32)

param #0 (length: 1) --x 04 --d 4 --c ""
<response #0 (length 4) --x a6 ff ff ff --d 166 255 255 255 --c "����"

cmd GET_IDX_CHAN_CMD (3d)

param #0 (length: 1) --x 04 --d 4 --c ""
<response #0 (length 1) --x 01 --d 1 --c ""

cmd GET_IDX_ENCT_CMD (33)

param #0 (length: 1) --x 04 --d 4 --c ""
<response #0 (length 1) --x 04 --d 4 --c ""

Network(ssid='twilightzone', bssid=b'\xe00\xd16]H', rssi=-67, channel=11, country='', authmode=(wifi.AuthMode.PSK, wifi.AuthMode.WPA, wifi.AuthMode.WPA2))
Network(ssid='twilightzone-guest', bssid=b'\xe10\xd16]J', rssi=-67, channel=11, country='', authmode=(wifi.AuthMode.PSK, wifi.AuthMode.WPA, wifi.AuthMode.WPA2))
Network(ssid='Verizon_X7B7JQ', bssid=b'm)\xd7\xc5\xbd<', rssi=-87, channel=1, country='', authmode=(wifi.AuthMode.PSK, wifi.AuthMode.WPA, wifi.AuthMode.WPA2))
Network(ssid='Verizon_X7B7JQ', bssid=b'=\xf1\xf6\xc5\xbd<', rssi=-87, channel=1, country='', authmode=(wifi.AuthMode.PSK, wifi.AuthMode.WPA, wifi.AuthMode.WPA2))
Network(ssid='Home', bssid=b'\n\x00<;"\x14', rssi=-90, channel=1, country='', authmode=(wifi.AuthMode.PSK, wifi.AuthMode.WPA, wifi.AuthMode.WPA2))
Connecting to twilightzone

cmd SET_PASSPHRASE_CMD (11)

param #0 (length: 12) --x 74 77 69 6c 69 67 68 74 7a 6f 6e 65 --d 116 119 105 108 105 103 104 116 122 111 110 101 --c "twilightzone"
param #1 (length: 10) --x 36 39 36 64 30 63 31 37 30 31 --d 54 57 54 100 48 99 49 55 48 49 --c "MySecretPassword(not really:)"
<response #0 (length 1) --x 01 --d 1 --c ""

cmd GET_CONN_STATUS_CMD (20)
<response #0 (length 1) --x 01 --d 1 --c ""

Traceback (most recent call last):
File "", line 1, in
File "dantest.py", line 59, in
ConnectionError: No network with that ssid

I did wrap one of the early dns calls in a try/except block and printed out the traceback so the test would run a little further.

@dhalbert
Copy link
Collaborator Author

dhalbert commented Feb 5, 2026

Thanks for the testing. In my own testing on Fruit Jam, I found that a first HTTP request works, and a second one fails -- it doesn't fetch the last chunk of data from the host. I have not yet debugged this. It worked fine on SAMD. I'm not sure if this is an ESP32 vs ESP32-C6 difference or a SAMD vs RP2xxx difference.

@RetiredWizard
Copy link

I'm looking my comment over and besides the mis-diagnosed connection_manager issue, it looks like I need to figure out how to update the airlift firmware on this device as well. I'll take a look at that and retest. Sorry for the noise....

@justmobilize
Copy link

Doing some random testing, noticed ntp doesn't work:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "adafruit_ntp.py", line 123, in datetime
  File "adafruit_ntp.py", line 91, in _update_time_sync
OSError: [Errno 9] EBADF

line 91 is:

sock.recv_into(self._packet)

I also noticed I needed to call pool.getaddrinfo multiple times. I got:

TimeoutError: timeout waiting for ready False

then

RuntimeError: Error response to AirLift command

and then it worked

@RetiredWizard
Copy link

RetiredWizard commented Feb 5, 2026

After updating the airlift firmware, I'm no longer seeing the safe mode crashes and the DNS error I needed to try/except around in your test program no longer fails. The test program still ends at the same point but the message is slightly different:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "dantest.py", line 59, in <module>
ConnectionError: Failed

After my changes to the test script this is line 59:
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))

@justmobilize
Copy link

Regarding connection manager, as long as it has a way of knowing which radio it is, it can import things correctly.

I actually have a dual wifi project that's connected to two different SSIDs (esp32s3 + airlift)

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.

6 participants