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, inFile "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, inFile "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.
[ 10:10 Apr 30, 2026 More hardware | permalink to this entry | ]