When browsing through raspberry pi pico w projects, you will find countless blinking LED tutorials and basic weather stations. But the Pico W’s true strength lies in its CYW43439 WiFi coprocessor paired with the RP2040’s dual-core ARM Cortex-M0+, making it a highly capable, low-cost node for industrial-style IoT monitoring. In this guide, we are building a unified MQTT telemetry node that simultaneously tracks ambient climate (temperature, humidity, pressure) and DC power consumption (voltage, current, wattage) on a single I2C bus.
This build targets the Raspberry Pi Pico W (the original RP2040 variant with the Infineon CYW43439 WiFi module, not the newer Pico 2 W). We will use MicroPython v1.22.1 or later, leveraging hardware I2C blocks and robust MQTT error handling to ensure the node survives WiFi dropouts and I2C bus lockups without requiring a manual reset.
Project Overview & Difficulty Rating
| Metric | Specification |
|---|---|
| Difficulty | Intermediate (Requires I2C bus management and MQTT broker setup) |
| Estimated Time | 2.5 Hours (Hardware assembly + firmware flashing + broker config) |
| Estimated Cost | $24.50 USD (Based on 2026 Adafruit/Mouser pricing) |
| Target Board | Raspberry Pi Pico W (RP2040 + CYW43439) |
| Firmware | MicroPython v1.22.1+ (Official Raspberry Pi Pico W build) |
Hardware BOM & Pin Mapping Matrix
Before cutting wires, verify your exact module variants. The I2C addresses and logic level tolerances below assume the specific Adafruit breakout boards listed. Using generic clone boards often results in missing pull-up resistors, which will crash the I2C bus on the Pico W.
Bill of Materials (BOM)
| Component | Exact Model / Variant | Approx. Price | Critical Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi Pico W (with pre-soldered headers) | $8.00 | Ensure it is the 'W' variant. Pico H lacks WiFi. |
| Climate Sensor | Adafruit BME280 I2C/SPI Breakout (#2652) | $9.95 | Includes 10k pull-ups. Default I2C: 0x77. |
| Power Sensor | Adafruit INA219 High Side DC Current Sensor (#904) | $6.50 | Includes 10k pull-ups. Default I2C: 0x40. |
| Wiring | 26 AWG Silicone Stranded Wire (4 colors) | $0.05 | Keep I2C runs under 30cm to avoid capacitance issues. |
Pico W Pin Mapping Matrix
We are using the I2C0 hardware block. While the RP2040 allows flexible pin muxing, GP4 and GP5 are the default, hardware-optimized pins for I2C0, reducing the chance of software-timing glitches during deep sleep wakeups.
| Pico W Pin | RP2040 Function | Sensor Pin | Wire Color | Notes |
|---|---|---|---|---|
| Pin 6 (GP4) | I2C0 SDA | BME280 SDA / INA219 SDA | Blue | Data line. Parallel pull-ups on both sensors yield ~5k. |
| Pin 7 (GP5) | I2C0 SCL | BME280 SCL / INA219 SCL | Yellow | Clock line. Max 400kHz for this trace length. |
| Pin 36 (3V3 OUT) | 3.3V Power | BME280 VIN / INA219 VCC | Red | Pico W onboard 3.3V regulator. Max draw ~300mA. |
| Pin 38 (GND) | Ground | BME280 GND / INA219 GND | Black | Common ground reference. Crucial for INA219 shunt accuracy. |
| Pin 40 (VBUS) | 5V Input | INA219 V+ | Orange | Only if measuring a 5V USB load. Connect to load positive. |
VCC (logic power, connect to 3.3V) and VIN (shunt power, connect to the load you are measuring). Mixing these up will either result in 0.00A readings or permanently fry the I2C level shifters on the breakout.
Assembly & I2C Bus Wiring Steps
The RP2040’s I2C implementation is strictly open-drain. It cannot drive the line high; it relies entirely on external pull-up resistors to return the SDA and SCL lines to 3.3V. The Raspberry Pi Pico W Datasheet specifies that the internal pull-ups are too weak (approx. 50kΩ-80kΩ) for reliable 400kHz I2C communication.
- Prep the Pico W: Solder the 2x20 pin headers to your Pico W if not pre-installed. Mount it on a half-size breadboard.
- Wire the Power Rails: Connect Pico W Pin 36 (3V3) to the breadboard's red rail, and Pin 38 (GND) to the blue rail. Do not use Pin 39 (VSYS) for sensor logic power, as it fluctuates with USB VBUS noise.
- Connect the I2C Bus: Run Blue wire from GP4 to the SDA pins of both the BME280 and INA219. Run Yellow wire from GP5 to the SCL pins of both sensors.
- Connect Sensor Power: Run Red wires from the 3.3V rail to the
VIN(BME280) andVCC(INA219) pins. Run Black wires to their respective GND pins. - Wire the Load (INA219): To measure a 5V USB load, connect Pico W Pin 40 (VBUS) to the INA219
V+pad. Connect the INA219V-pad to the positive terminal of your target load. Connect the load's ground back to the breadboard ground. - Verify Pull-Up Resistance: With the Pico W unpowered, use a multimeter in resistance mode. Measure between SDA and 3.3V. You should read between 4kΩ and 10kΩ. If you read infinite (OL), your breakout boards lack pull-ups and you must solder a 4.7kΩ resistor between SDA/SCL and 3.3V.
MicroPython Firmware & MQTT Payload Code
This firmware targets the Raspberry Pi Pico W running MicroPython. It utilizes the machine.I2C module for hardware-accelerated bus scanning and the umqtt.simple library for MQTT publishing. Before flashing, ensure you have installed the umqtt.simple and bme280 packages via Thonny's package manager or by running import mip; mip.install('umqtt.simple') in the REPL.
The code includes explicit error handling for I2C bus lockups and WiFi timeouts, ensuring the node self-recovers without a hardware watchdog reset.
import time
import network
import ubinascii
from machine import Pin, I2C
import bme280
from umqtt.simple import MQTTClient
# --- PIN DEFINITIONS & HARDWARE CONFIG ---
I2C_SDA_PIN = 4
I2C_SCL_PIN = 5
I2C_FREQ = 400000 # 400kHz
# WiFi & MQTT Credentials
WIFI_SSID = 'YourNetworkSSID'
WIFI_PASS = 'YourNetworkPassword'
MQTT_BROKER = '192.168.1.50'
MQTT_PORT = 1883
MQTT_TOPIC_CLIMATE = b'home/lab/climate'
MQTT_TOPIC_POWER = b'home/lab/power'
# Initialize Hardware I2C0
i2c = I2C(0, sda=Pin(I2C_SDA_PIN), scl=Pin(I2C_SCL_PIN), freq=I2C_FREQ)
# Generate unique client ID from Pico W MAC address
client_id = ubinascii.hexlify(network.WLAN().config('mac')).decode()
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
# Set country code to prevent radio regulatory dropouts
wlan.config(pm = 0xa11140) # Disable aggressive power saving
if not wlan.isconnected():
print(f'Connecting to {WIFI_SSID}...')
wlan.connect(WIFI_SSID, WIFI_PASS)
timeout = 20
while not wlan.isconnected() and timeout > 0:
time.sleep(1)
timeout -= 1
if wlan.isconnected():
print(f'WiFi Connected: {wlan.ifconfig()[0]}')
return True
else:
print('WiFi Connection Failed')
return False
def scan_i2c_bus():
devices = i2c.scan()
if not devices:
raise OSError("No I2C devices found. Check wiring and pull-ups.")
print(f'I2C Devices Found: {[hex(d) for d in devices]}')
# 0x77 is BME280, 0x40 is INA219
if 0x77 not in devices or 0x40 not in devices:
raise OSError("Missing expected sensor addresses on I2C bus.")
def read_ina219_raw(i2c_bus, addr=0x40):
# Minimal raw I2C read for INA219 Bus Voltage Register (0x02)
# Returns voltage in Volts
try:
i2c_bus.writeto(addr, b'\x02')
raw = i2c_bus.readfrom(addr, 2)
val = (raw[0] << 8) | raw[1]
val = val >> 3 # Shift right by 3 (LSBs are status flags)
return val * 0.004 # 4mV per LSB
except Exception as e:
print(f"INA219 Read Error: {e}")
return None
def main():
if not connect_wifi():
machine.reset()
try:
scan_i2c_bus()
bme = bme280.BME280(i2c=i2c)
except OSError as e:
print(f"Fatal I2C Init Error: {e}")
machine.reset()
mqtt = MQTTClient(client_id, MQTT_BROKER, port=MQTT_PORT, keepalive=60)
try:
mqtt.connect()
print("MQTT Connected")
except OSError as e:
print(f"MQTT Connection Failed: {e}")
machine.reset()
while True:
try:
# Read Climate Data
temp_c = bme.temperature[:-1] # Strip 'C' unit
humidity = bme.humidity[:-1]
# Read Power Data
bus_voltage = read_ina219_raw(i2c)
# Format Payloads
climate_payload = f'{{"temp":{temp_c},"hum":{humidity}}}'
power_payload = f'{{"vbus":{bus_voltage}}}' if bus_voltage else '{"vbus":null}'
# Publish with QoS 0 (Fire and forget for high-freq telemetry)
mqtt.publish(MQTT_TOPIC_CLIMATE, climate_payload.encode(), qos=0)
mqtt.publish(MQTT_TOPIC_POWER, power_payload.encode(), qos=0)
print(f"Published: {climate_payload} | {power_payload}")
time.sleep(10)
except OSError as e:
# Catch WiFi/MQTT timeouts and attempt reconnect
err_str = str(e)
print(f"Network/Runtime Error: {err_str}")
if 'ETIMEDOUT' in err_str or 'ECONNRESET' in err_str:
print("Attempting MQTT Reconnect...")
try:
mqtt.connect()
except:
machine.reset()
time.sleep(5)
if __name__ == '__main__':
main()
Debugging: I2C Faults and WiFi "ETIMEDOUT" Errors
Embedded IoT nodes rarely fail gracefully. When working with raspberry pi pico w projects that rely on external buses and RF environments, you will encounter specific MicroPython exceptions. Here is how to diagnose the two most common failure modes.
Exact Error String: OSError: [Errno 5] EIO
This is the universal MicroPython I2C bus failure code. It means the RP2040 attempted to clock data, but the SDA line did not respond or was held low by a slave device.
Ranked Causes:
- Missing or Insufficient Pull-Up Resistors: The most common cause. The bus capacitance is too high, and the signal rise time is too slow for 400kHz. Fix: Drop I2C_FREQ to 100000 (100kHz) in the code, or add external 4.7kΩ pull-ups.
- Address Collision or Misconfiguration: You are polling 0x76, but the Adafruit BME280 defaults to 0x77. Fix: Run
i2c.scan()in the REPL to verify actual hex addresses. - Ground Loop / Common Reference Loss: The GND wire between the Pico W and the sensor has popped out of the breadboard. Fix: Measure resistance between Pico GND and Sensor GND; it must be < 1 ohm.
Exact Error String: OSError: [Errno 110] ETIMEDOUT
This occurs during wlan.connect() or mqtt.connect(). The CYW43439 WiFi chip failed to complete the TCP handshake or DHCP request within the allocated socket timeout.
Ranked Causes:
- 2.4GHz Channel Congestion / Router Isolation: The Pico W only supports 2.4GHz 802.11n. If your router has "Client Isolation" enabled (common in IoT VLANs), the Pico W cannot reach the local MQTT broker. Fix: Ensure the broker IP is on the same subnet and AP isolation is disabled.
- Missing Country Code / Power Save Mode: The CYW43439 chip can aggressively drop connections if regulatory domains aren't set. Fix: Ensure
wlan.config(pm = 0xa11140)is in your connection script to disable modem sleep during active transmission. - Broker Port Blocking: Your MQTT broker (e.g., Mosquitto) is running on a non-standard port, or the local firewall is dropping port 1883. Fix: Test broker reachability using
pingandtelnet 192.168.1.50 1883from a PC on the same network.
1. Power Integrity: Measure the 3.3V rail with a multimeter while the WiFi radio is transmitting. If it dips below 3.1V, the Pico W will brownout and reset. Add a 100µF electrolytic capacitor across the 3.3V and GND rails.
2. I2C Pull-Ups: Verify the physical presence of pull-up resistors on the SDA/SCL lines. Cloned sensors often omit them to save $0.02 in manufacturing.
3. Broker Credentials: Verify your MQTT username/password. MicroPython's
umqtt.simple fails silently or throws a generic timeout if the broker rejects the AUTH packet.
Extending and Simplifying the Build
Depending on your deployment environment, you may need to scale this node up for industrial monitoring or scale it down for battery-powered remote deployment.
How to Simplify (Battery / Solar Deployment)
If you are deploying this in a shed or greenhouse powered by a 18650 Li-ion cell and a solar charge controller, WiFi and I2C are your biggest power drains.
- Drop the INA219: If you only need climate data, remove the power sensor. This reduces I2C bus capacitance and saves ~1mA of continuous quiescent current.
- Implement Deep Sleep: The RP2040 does not have a true deep sleep mode like the ESP32, but it has
machine.lightsleep(). By utilizing the MicroPython machine module, you can put the CPU to sleep for 10 minutes between MQTT publishes, dropping average current draw from 80mA to under 5mA. - Use MQTT QoS 0: Quality of Service level 0 requires no handshake acknowledgments. It saves the WiFi radio from staying awake an extra 200ms per packet, significantly extending battery life.
How to Extend (Home Assistant & Load Shedding)
To turn this from a passive monitor into an active control node:
- Add Home Assistant MQTT Auto-Discovery: Instead of manually configuring sensors in Home Assistant, publish a JSON configuration payload to the
homeassistant/sensor/pico_w_climate/configtopic on boot. Home Assistant will automatically create the dashboard entities. - Integrate a Relay for Load Shedding: Connect a 3.3V optocoupler or a logic-level MOSFET to GP15. If the INA219 detects current exceeding 2.5A (indicating a stalled motor or short circuit), trigger GP15 HIGH to cut power to the load instantly, protecting your wiring.
- Switch to ESPHome: If you prefer YAML over MicroPython, the Pico W is now supported by ESPHome (as of late 2024/2025 updates). You can port this exact hardware configuration to an ESPHome YAML file, leveraging its built-in API and OTA update mechanisms without writing a single line of Python.






