How to Re-initialize a Stuck ESP32 (in CircuitPython) (Shallow Thoughts)

Akkana's Musings on Open Source Computing and Technology, Science, and Nature.

Thu, 30 Apr 2026

How to Re-initialize a Stuck ESP32 (in CircuitPython)

I designed my particulate air quality sensor project around Adafruit's PyPortal. It uses a ESP32 coprocessor for networking.

Unfortunately, the ESP32 is a little flaky. It tends to lose track of the network after an hour or so:

ESP32 not responding
Traceback (most recent call last):
  File "code.py", line 182, in 
  File "adafruit_requests.py", line 725, in post
  File "adafruit_requests.py", line 649, in request
  File "adafruit_connection_manager.py", line 331, in get_socket
  File "adafruit_connection_manager.py", line 248, in _get_connected_socket
  File "adafruit_connection_manager.py", line 61, in connect
  File "adafruit_esp32spi/adafruit_esp32spi_socketpool.py", line 114, in connect
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 899, in socket_connect
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 801, in socket_open
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 422, in _send_command_get_response
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 378, in _wait_response_cmd
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 292, in _wait_for_ready
TimeoutError: ESP32 not responding

I tried re-initializing the network, but it didn't help: re-initializing always died with Timed out waiting for SPI char and SCK in use.

There are lots of people asking about this on the net, but I couldn't find a discussion that actually had a solution for how to re-initialize a stuck ESP32. So I asked Claude. I know, AI, eww ... but Claude seems to have access to CircuitPython code and discussions that Google doesn't index, so sometimes it's the best way to find out how to solve CircuitPython problems. It took a couple of iterations (each requiring a few hours of testing, since it typically takes an hour or so before the network stops working), but we got there. Here's what seems to work for me.

The Problem(s)

The first problem is that after esp.reset() and esp.disconnect(), the SPI bus and GPIO pins are still held by the old objects, and need to be deinitialized. But it's not just the esp and spi objects: it turned out the DigitalInOut objects for the ESP32 control pins also need to be deinitialized, not just the SPI bus.

The SCK in use error is busio.SPI refusing to re-acquire pins that are already claimed by the existing SPI object. CircuitPython tracks hardware peripheral ownership, and simply resetting the ESP32 firmware doesn't release the SAMD processor side's grip on those pins. Calling spi.deinit() explicitly releases them back to the system so busio.SPI() can reclaim them cleanly on the next call.

The Timed out waiting for SPI apparently happens because the old esp object's internal state is out of sync after the reset pulse — the SPI bus is still technically "open" on the host side, but the ESP32 has restarted, so the handshake never completes. Deinitializing in order (esp teardown → spi.deinit() → sleep → reinit) lets everything settle before attempting reconnection.

But sometimes while re-initializing those, the device hangs completely:

  File "code.py", line 198, in 
  File "adafruit_requests.py", line 725, in post
  File "adafruit_requests.py", line 649, in request
  File "adafruit_connection_manager.py", line 328, in get_socket
  File "adafruit_esp32spi/adafruit_esp32spi_socketpool.py", line 60, in getaddrinfo
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 752, in get_host_by_name
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 421, in _send_command_get_response
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 333, in _send_command

This is partly due to those DigitalInOut objects needing re-initializing, as I mentioned before, and partly because the ESP32 sometimes needs more time to fully come back after a reset.

Here's what ultimately worked:

requests = None

# All the objects that may need re-initializing:
esp = None
spi = None
esp32_cs = None
esp32_ready = None
esp32_reset = None

def init_network():
    """Initialize the esp32's network"""
    global requests, DATA_REPORTING_URL
    global esp, spi, esp32_cs, esp32_ready, esp32_reset
    try:
        print("====== Initializing Network =====")
        ssid = os.getenv("CIRCUITPY_WIFI_SSID")
        print("ssid:", ssid)
        password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
        if password:
            print("Read wi-fi password")

        # Secondary (SCK1) SPI used to connect to WiFi board on Arduino Nano Connect RP2040
        if "SCK1" in dir(board):
            spi = busio.SPI(board.SCK1, board.MOSI1, board.MISO1)
        else:
            spi = busio.SPI(board.SCK, board.MOSI, board.MISO)

        # On a board with pre-defined ESP32 Pins:
        esp32_cs = DigitalInOut(board.ESP_CS)
        esp32_ready = DigitalInOut(board.ESP_BUSY)
        esp32_reset = DigitalInOut(board.ESP_RESET)

        esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs,
                                               esp32_ready, esp32_reset)

        print("Waiting for ESP32 to be ready")
        MAX_WAIT = 10
        waited = 0
        while waited < MAX_WAIT:
            try:
                if esp.status is not None:
                    break
            except Exception as e:
                print("Exception while waiting for ESP32:", e)
            time.sleep(1)
            waited += 1
        if waited >= MAX_WAIT:
            print("Couldn't make ESP32 ready, bailing")
            sys.exit(0)

        pool = adafruit_connection_manager.get_radio_socketpool(esp)
        ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp)
        requests = adafruit_requests.Session(pool, ssl_context)

        print("Connecting to AP...")
        # Only retry this many times
        MAX_CONNECTION_RETRIES = 7
        num_tries = 0
        while not esp.is_connected:
            try:
                esp.connect_AP(ssid, password)
            except OSError as e:
                print("could not connect to AP, retrying: ", e)
                num_tries += 1
                if num_tries >= MAX_CONNECTION_RETRIES:
                    DATA_REPORTING_URL = None
                    print("Can't connect, won't do any network reporting")
                    break
        print("Connected to", esp.ap_info.ssid, "\tRSSI:", esp.ap_info.rssi,
              "with IP address", esp.ipv4_address)

    except Exception as e:
        print("Couldn't set up network:", e)

# Initialize the network the first time
init_network()

def deinit_network():
    """Reset the esp32 so it will be possible to re-initialize the network"""
    global esp, spi, esp32_cs, esp32_ready, esp32_reset
    try:
        if esp:
            esp.reset()
            esp.disconnect()
            esp = None
    except Exception as e:
        print("Error during esp teardown:", e)

    try:
        if spi:
            spi.deinit()
            spi = None
    except Exception as e:
        print("Error during SPI deinit:", e)

    try:
        if esp32_cs:
            esp32_cs.deinit()
            esp32_cs = None
        if esp32_ready:
            esp32_ready.deinit()
            esp32_ready = None
        if esp32_reset:
            esp32_reset.deinit()
            esp32_reset = None
    except Exception as e:
        print("Error during pin deinit:", e)

    time.sleep(2)


if __name__ == '__main__':
    while True:
        # ... call whatever fills in data

        # report_values
        if data and DATA_REPORTING_URL:
            try:
                print("Posting results ...", end=' ')
                r = requests.post(DATA_REPORTING_URL, json=data)
                if r.text.startswith('Error'):
                    print(r.text)
                else:
                    print("Posted data:", r.text)
            except Exception as e:
                # Sometimes there's a TimeoutError: ESP32 not responding
                print("******* Couldn't post results:", e)
                print("Trying to de-initialize network")
                deinit_network()
                print("Trying to re-initialize network")
                init_network()

With that code, the particle sensor has been running all day, about 10 hours so far, and has successfully reset itself nine times. Fingers crossed that continues!

Watchdog Reset

Claude also tipped me off that CircuitPython has a watchdog reset option that I hadn't seen mentioned anywhere:

from microcontroller import watchdog as w
from watchdog import WatchDogMode
w.timeout = 30  # seconds — set longer than your slowest normal loop iteration
w.mode = WatchDogMode.RESET

Then call w.feed() periodically to keep it from triggering, and if something goes wrong, like that freeze I saw in get_host_by_name(), the watchdog might save you. I haven't actually needed to add it yet (assuming the network de-init/re-init continues to work), but it's a good option to know about.

Tags: ,
[ 10:10 Apr 30, 2026    More hardware | permalink to this entry | ]

Comments via Disqus:

blog comments powered by Disqus