The Raspberry Pi Pico W pairs the dual-core RP2040 microcontroller with an Infineon CYW43439 WiFi/Bluetooth chip, creating a $6 powerhouse for IoT sensor nodes. Unlike the ESP32, the Pico W separates the WiFi radio from the main MCU, which introduces unique power-management and debugging quirks. This guide walks through building a reliable MQTT environmental monitor, targeting the original Raspberry Pi Pico W (RP2040, 2MB QSPI flash) running MicroPython v1.23.0+. We will bypass the common CYW43439 firmware crashes and deliver production-ready code with robust error handling.
Project Overview and Hardware Requirements
Time to Build: 45 minutes
Target Board: Raspberry Pi Pico W (RP2040) with header pins soldered
Before writing code, ensure you have the exact hardware variants listed below. Substituting the Pico W with the newer Pico 2 W (RP2350) will require different MicroPython firmware builds and slightly altered WiFi initialization routines.
- MCU: Raspberry Pi Pico W (RP2040, CYW43439 WiFi)
- Sensor: Internal RP2040 Temperature Sensor (ADC Channel 4) for baseline testing
- Status Indicator: 5mm Red LED (2.0V forward voltage) + 330Ω through-hole resistor
- Wiring: 22 AWG solid-core jumper wires, 400-tie-point breadboard
- Software: Thonny IDE, MicroPython v1.23.0 UF2 firmware,
umqtt.simplelibrary
Pi Pico W vs ESP32-C3 for Low-Power IoT Nodes
Choosing the right silicon dictates your battery life and firmware architecture. The Pico W is exceptional for processing, but its WiFi implementation carries a power penalty compared to integrated SoCs. Below is a data-dense comparison of popular 2026 IoT boards to help you decide if the Pico W fits your power budget.
| Board Variant | MCU Core / Speed | WiFi Implementation | Deep Sleep Current | Typical 2026 Price |
|---|---|---|---|---|
| Raspberry Pi Pico W | Dual Cortex-M0+ @ 133MHz | External CYW43439 via SPI | ~1.2 mA (System) / ~20 mA (WiFi Idle) | $6.00 |
| ESP32-C3 SuperMini | Single RISC-V @ 160MHz | Integrated 2.4GHz WiFi/BLE | ~5 µA (True Deep Sleep) | $3.50 |
| ESP8266 NodeMCU v3 | Tensilica L106 @ 80MHz | Integrated 2.4GHz WiFi | ~20 µA (Deep Sleep) | $4.00 |
| Raspberry Pi Pico 2 W | Dual Cortex-M33 / RISC-V | External CYW43439 via SPI | ~1.0 mA (System) | $7.00 |
The Deep Sleep Caveat: The Pico W cannot achieve true microamp deep sleep while keeping the WiFi radio state alive. The CYW43439 chip draws continuous current unless you explicitly cut its power via the WL_GPIO pins, which drops the network connection. If your project requires waking up every 10 minutes, sending data, and sleeping on a coin cell, choose the ESP32-C3. If you need continuous MQTT streaming and complex local data logging on USB power, the Pico W is superior.
Pin Mapping and Physical Wiring
A frequent trap for beginners is attempting to control the Pico W’s onboard LED using Pin(25). On the original Pico (without WiFi), GPIO25 drives the LED. On the Pico W, the LED is routed through the CYW43439 WiFi chip and is accessed via WL_GPIO0. MicroPython abstracts this, but external wiring requires standard GPIO pins.
| Pico W Pin | Function | Connected To | Notes |
|---|---|---|---|
| GP15 (Pin 20) | External Status LED | 330Ω Resistor → LED Anode | Indicates MQTT connection state |
| GND (Pin 23) | Ground Reference | LED Cathode | Common ground for external components |
| WL_GPIO0 | Onboard LED | Internal (No wiring needed) | Accessed via Pin('LED') in MicroPython |
| VSYS (Pin 39) | Input Power | Battery / 5V Source | Feed 2.7V to 5.5V here for standalone use |
Wiring Steps:
- Insert the Pico W into the breadboard, ensuring the USB port faces the edge.
- Connect the 330Ω resistor to GP15 (Physical Pin 20).
- Connect the other end of the resistor to the anode (long leg) of the 5mm LED.
- Connect the cathode (short leg) of the LED to the breadboard ground rail.
- Jumper the ground rail to any Pico W GND pin (e.g., Physical Pin 23).
MicroPython MQTT Code with Error Handling
The following script targets the Raspberry Pi Pico W. It connects to a 2.4GHz WPA2 WiFi network, reads the internal RP2040 temperature sensor, and publishes the data to an MQTT broker. It includes explicit timeout handling for the CYW43439 WiFi association and error catching for MQTT socket drops.
Prerequisite: Install the umqtt.simple library via Thonny's package manager (Tools → Manage Packages) before running.
import network
import time
import machine
from umqtt.simple import MQTTClient
# --- PIN & CONFIG DEFINITIONS ---
EXT_LED = machine.Pin(15, machine.Pin.OUT)
ONBOARD_LED = machine.Pin('LED', machine.Pin.OUT)
ADC_TEMP = machine.ADC(4) # Internal temperature sensor on channel 4
WIFI_SSID = 'Your_2.4GHz_Network'
WIFI_PASS = 'Your_WiFi_Password'
MQTT_BROKER = '192.168.1.50' # Use IP, not hostname, to avoid DNS timeouts
MQTT_TOPIC = b'pico_w/sensor/temp'
CLIENT_ID = 'pico_node_01'
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
# Disable WiFi power save to prevent CYW43439 firmware crashes on some routers
wlan.config(pm = 0xa11140)
if not wlan.isconnected():
print('Connecting to WiFi...')
wlan.connect(WIFI_SSID, WIFI_PASS)
timeout = 15
while not wlan.isconnected() and timeout > 0:
ONBOARD_LED.toggle()
time.sleep(0.5)
timeout -= 1
if wlan.isconnected():
print('WiFi connected:', wlan.ifconfig())
ONBOARD_LED.value(1)
return True
else:
print('WiFi connection failed. Status:', wlan.status())
ONBOARD_LED.value(0)
return False
def read_internal_temp():
# RP2040 internal temp formula from datasheet
reading = ADC_TEMP.read_u16() * 3.3 / (65535)
temperature = 27 - (reading - 0.706) / 0.001721
return round(temperature, 2)
def main():
if not connect_wifi():
machine.reset() # Reboot if WiFi fails entirely
client = MQTTClient(CLIENT_ID, MQTT_BROKER, keepalive=60)
try:
client.connect()
print('Connected to MQTT Broker')
EXT_LED.value(1)
except OSError as e:
print('MQTT Connection Failed:', e)
machine.reset()
while True:
try:
temp = read_internal_temp()
msg = f'{{"temp_c": {temp}}}'
client.publish(MQTT_TOPIC, msg)
print(f'Published: {msg}')
# Brief blink to indicate successful publish
EXT_LED.value(0)
time.sleep(0.1)
EXT_LED.value(1)
time.sleep(10)
except OSError as e:
# Catches EHOSTUNREACH or broken pipe if router drops client
print('MQTT Publish Error:', e)
EXT_LED.value(0)
print('Attempting WiFi reconnect...')
if connect_wifi():
try:
client = MQTTClient(CLIENT_ID, MQTT_BROKER, keepalive=60)
client.connect()
except:
machine.reset()
else:
machine.reset()
if __name__ == '__main__':
main()
Debugging: 'OSError: [Errno 113] EHOSTUNREACH' and Connection Failures
The CYW43439 chip on the Pico W is notoriously sensitive to router configurations and power fluctuations. The most common fatal error you will encounter in the Thonny console is:
OSError: [Errno 113] EHOSTUNREACH
or occasionally
OSError: [Errno -2] ENOENT(if using a hostname instead of an IP address)
This error means the Pico W's TCP stack cannot route packets to the MQTT broker. It usually occurs 10 to 30 minutes into runtime, not at boot.
Ranked Causes for EHOSTUNREACH
- CYW43439 Power-Save Disconnect (Most Likely): The WiFi chip enters a low-power state and fails to wake properly when the router sends a beacon. We mitigated this in the code using
wlan.config(pm = 0xa11140), which disables the 802.11 power-save polling. - WPA3 / PMF Incompatibility: The Pico W's WiFi firmware struggles with WPA3 transition modes and Management Frame Protection (PMF). If your router enforces PMF, the Pico W will randomly drop and fail to reassociate.
- DNS Resolution Timeout: If you passed a domain name (e.g.,
mqtt.myhouse.local) to theMQTTClient, the MicroPython DNS resolver will occasionally fail and throwENOENT.
The First Three Things to Check When It Fails
When your node drops offline, execute these three checks before rewriting your code:
- Check
wlan.status(): Add a print statement in your exception block. Ifwlan.status()returns1(STAT_IDLE) or2(STAT_CONNECTING) instead of3(STAT_GOT_IP), the radio dropped. You must reset the interface, not just the MQTT client. - Ping the Broker IP from a PC: Ensure the MQTT broker (e.g., Mosquitto on a Raspberry Pi) hasn't crashed or been blocked by a local firewall update. Verify the PC and Pico W are on the exact same VLAN/Subnet.
- Verify Router 2.4GHz Settings: Log into your router and ensure the 2.4GHz band is set to WPA2-Personal (AES only). Disable WPA3, disable PMF (802.11w), and lock the channel width to 20MHz to prevent CYW43439 RF desensitization.
Extending and Simplifying the Build
Once the baseline internal temperature node is stable, you can adapt the hardware to fit your specific deployment environment.
How to Extend the Node (Adding I2C and Solar Power)
To make this a true environmental station, wire a Bosch BME280 sensor to GP4 (SDA) and GP5 (SCL). You will need to import a BME280 MicroPython driver and replace the read_internal_temp() function with I2C read calls. For power, add a 3.7V 18650 LiPo cell wired through a TP4056 charging module and an HT7333 LDO to drop the voltage to a clean 3.3V for the VSYS pin.
How to Simplify the Build (Local Logging)
If you do not have an MQTT broker set up and want to skip the networking layer entirely, strip the network and umqtt imports. Use Thonny's built-in Plotter feature. Simply format your print statements as print("Temp:", temperature), open the Thonny plotter (View → Plotter), and the IDE will automatically graph the serial output in real-time over USB. This is the fastest way to validate sensor logic before introducing WiFi complexity.
For deeper hardware specifications, always refer to the official Pico W datasheet and the MicroPython network.WLAN documentation to verify pinouts and firmware flags for your specific board revision.






