When makers search for stuff to do with raspberry pi, they usually land on retro-gaming consoles or media centers. But for embedded electronics and IoT, the real utility lies in the microcontroller-class boards. Blinking an LED is a day-one exercise; building a reliable, WiFi-connected environmental sensor node that publishes to an MQTT broker is a foundational IoT skill. This guide walks through building a robust temperature, humidity, and pressure monitor using the latest RP2350 silicon, complete with hardware decision frameworks, exact wiring, and production-grade MicroPython code.

The Decision Path: Which Board for Wi-Fi Sensor Nodes?

Before buying parts, you need to match the silicon to the job. Use this decision matrix to select the right board for your IoT sensor node. Default Pick: If you need a low-cost, dual-core microcontroller with Wi-Fi and native MicroPython support, terminate your search at the Raspberry Pi Pico 2 W.

Project Requirement Recommended Board Why?
Need full Linux OS, USB webcams, Docker containers, or local databases? Raspberry Pi 5 (8GB) Requires an OS and high RAM; overkill for simple sensor polling.
Need sub-milliamp deep sleep, battery-operated, coin-cell friendly? ESP32-C6 SuperMini Superior deep sleep current (~8µA) and native 802.15.4/Zigbee support.
Need dual-core 150MHz, robust MicroPython, Wi-Fi, under $10? Raspberry Pi Pico 2 W (RP2350) Best balance of processing headroom, security features, and ecosystem.
Need high-precision ADC (16-bit+) for analog sensor arrays? ESP32-S3 DevKitC Pico 2 W has 12-bit ADCs; ESP32-S3 or external ADCs are better for precision analog.

Parts List and Spec Sheet

This build assumes a 3.3V logic environment and a standard 2.4 GHz Wi-Fi network. Do not use 5V I2C breakouts without a logic level converter, or you will fry the RP2350 GPIO pins.

Component Exact Variant / Model Approx. Price Notes
Microcontroller Raspberry Pi Pico 2 W (RP2350) $7.00 Ensure it's the '2 W' with the Infineon CYW43439 Wi-Fi/BLE module.
Sensor Adafruit BME280 I2C/SPI Breakout (3659) $19.50 Includes onboard 3.3V regulator and 10kΩ I2C pull-ups. Generic clones ($5) often lack pull-ups.
Wiring 22 AWG Solid Core Hookup Wire $12.00/spool Pre-tinned copper. Stranded wire is prone to fraying on breadboards.
Resistors (If using generic sensor) 4.7kΩ 1/4W Carbon Film $0.10 Required for I2C SDA/SCL pull-ups if the breakout board lacks them.
Power Supply 5V 3A USB-C PD Power Adapter $10.00 Pico 2 W can spike to 300mA+ during Wi-Fi TX bursts. Use a quality PSU.

Pin Mapping and I2C Bus Physics

The BME280 communicates via I2C. While I2C only requires two wires (SDA and SCL), the physics of the bus demand attention to capacitance and pull-up resistors. The I2C specification limits bus capacitance to 400pF. Long jumper wires add parasitic capacitance; if your wires exceed 30cm, drop the I2C clock speed from 400kHz to 100kHz in software to prevent signal degradation.

Hardware Note: If you bought a $4 generic BME280 module from an online marketplace, check the back of the PCB. If you do not see three small surface-mount resistors near the VCC pin, you must add external 4.7kΩ pull-up resistors between 3.3V and both SDA and SCL. Without them, the bus will float, and the Pico will read garbage data or lock up.
Pico 2 W Pin GPIO Number BME280 Pin Function
Pin 4 GP4 SDI / SDA I2C Data (Serial Data)
Pin 5 GP5 SCK / SCL I2C Clock (Serial Clock)
Pin 36 3V3(OUT) VIN / VCC 3.3V Power Output (Max 300mA draw)
Pin 38 GND GND Common Ground

Complete MicroPython Code (Target: Pico 2 W)

This code targets the Raspberry Pi Pico 2 W running MicroPython v1.23.0 or newer (required for RP2350 support). It connects to Wi-Fi, initializes the I2C bus, reads the BME280, and publishes JSON-formatted telemetry to an MQTT broker. Save the BME280 driver library as bme280.py in your Pico root directory before running this main script.


# main.py - MQTT Environmental Node for Raspberry Pi Pico 2 W
import network
import time
import json
import ubinascii
from machine import Pin, I2C
import bme280  # Requires bme280.py driver in root directory
from umqtt.simple import MQTTClient

# --- PIN DEFINITIONS & CONFIG ---
I2C_SDA_PIN = 4
I2C_SCL_PIN = 5
I2C_FREQ = 400_000  # 400kHz Fast Mode

WIFI_SSID = 'YourNetworkSSID'
WIFI_PASS = 'YourNetworkPassword'

MQTT_BROKER = '192.168.1.100'
MQTT_PORT = 1883
CLIENT_ID = ubinascii.hexlify(machine.unique_id()).decode()
MQTT_TOPIC = f'home/environmental/{CLIENT_ID}'

# --- HARDWARE INITIALIZATION ---
led = Pin('LED', Pin.OUT)  # Pico W onboard LED
i2c = I2C(0, sda=Pin(I2C_SDA_PIN), scl=Pin(I2C_SCL_PIN), freq=I2C_FREQ)

def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    # Prevent Wi-Fi radio from sleeping to maintain MQTT keepalive
    wlan.config(pm = 0xa11140) 
    print(f'Connecting to {WIFI_SSID}...')
    wlan.connect(WIFI_SSID, WIFI_PASS)
    
    max_wait = 20
    while max_wait > 0:
        if wlan.status() < 0 or wlan.status() >= 3:
            break
        max_wait -= 1
        led.toggle()
        time.sleep(0.5)
        
    if wlan.status() != 3:
        raise RuntimeError(f'Wi-Fi connection failed, status: {wlan.status()}')
    
    led.value(1)
    ip = wlan.ifconfig()[0]
    print(f'Connected on {ip}')
    return ip

def get_sensor_data():
    # BME280 default I2C address is 0x76 or 0x77
    addresses = i2c.scan()
    if not addresses:
        raise OSError('[Errno 19] ENODEV: No I2C devices found on bus.')
    
    bme_addr = 0x76 if 0x76 in addresses else 0x77
    sensor = bme280.BME280(i2c=i2c, address=bme_addr)
    
    # Returns tuple: (temp_C, pressure_hPa, humidity_%)
    temp = float(sensor.temperature[:-1])  # Strip 'C' character
    press = float(sensor.pressure[:-3])    # Strip 'hPa' characters
    hum = float(sensor.humidity[:-1])      # Strip '%' character
    
    return {'temp': round(temp, 2), 'press': round(press, 2), 'hum': round(hum, 2)}

def main():
    try:
        connect_wifi()
        client = MQTTClient(CLIENT_ID, MQTT_BROKER, port=MQTT_PORT, keepalive=60)
        client.connect()
        print(f'Connected to MQTT Broker: {MQTT_BROKER}')
        
        while True:
            try:
                data = get_sensor_data()
                payload = json.dumps(data)
                # QoS 1 ensures broker acknowledges receipt
                client.publish(MQTT_TOPIC, payload, qos=1) 
                print(f'Published: {payload}')
                
                # Check for incoming MQTT messages (non-blocking)
                client.check_msg()
                time.sleep(30)
                
            except OSError as e:
                print(f'Sensor I2C Error: {e}')
                time.sleep(5) # Wait before retrying I2C
                
    except Exception as e:
        print(f'Fatal Error: {e}')
        led.value(0)
        # Implement watchdog reset in production to auto-recover
        machine.reset()

if __name__ == '__main__':
    main()

Debugging: First Three Checks and Exact Error Strings

Embedded IoT rarely works perfectly on the first compile. When the script crashes or fails to publish, do not guess. Follow this ranked troubleshooting path based on the exact MicroPython error strings.

The First Three Things to Check When It Fails

  1. Run an I2C Scan: Open the MicroPython REPL and run import machine; i2c = machine.I2C(0, sda=machine.Pin(4), scl=machine.Pin(5)); print(i2c.scan()). If it returns an empty list [], your hardware wiring or pull-up resistors are faulty. It should return [118] (0x76) or [119] (0x77).
  2. Verify Broker Reachability: From a PC on the same Wi-Fi network, open a terminal and run ping 192.168.1.100 (replace with your broker IP). If it times out, your MQTT service (e.g., Mosquitto) isn't running or the firewall is blocking port 1883.
  3. Check Wi-Fi RSSI: If the Pico connects but drops MQTT packets, the Wi-Fi signal might be too weak. Add wlan.status('rssi') to your code. Values below -75 dBm will cause intermittent ETIMEDOUT errors.

Exact Error Strings and Ranked Causes

Error: OSError: [Errno 19] ENODEV
Context: Thrown during i2c.scan() or when initializing the BME280 object.
Ranked Causes:
1. SDA and SCL wires are swapped (most common breadboard mistake).
2. Missing 4.7kΩ pull-up resistors on a generic breakout board.
3. The BME280 module is dead or operating at 5V logic, damaging the sensor's internal I2C transceiver.
Error: OSError: [Errno 110] ETIMEDOUT or MQTTException: [Errno 104] ECONNRESET
Context: Thrown during client.connect() or client.publish().
Ranked Causes:
1. The MQTT broker IP address is incorrect or the broker service crashed.
2. The router's AP Isolation (Client Isolation) feature is enabled, blocking local device-to-device traffic.
3. The Wi-Fi radio entered power-save mode. (Fixed in the code above via wlan.config(pm = 0xa11140)).
Error: RuntimeError: Wi-Fi connection failed, status: 1 (or status: 3 in some firmware versions indicating NO_AP_FOUND)
Context: Thrown during the Wi-Fi connection loop.
Ranked Causes:
1. SSID or Password typo (MicroPython Wi-Fi is case-sensitive and space-sensitive).
2. Attempting to connect to a 5 GHz Wi-Fi network. The Pico 2 W's CYW43439 chip is strictly 2.4 GHz (802.11b/g/n).
3. The router has reached its maximum DHCP client limit.

Extending and Simplifying the Build

Once the baseline node is stable, you can scale the complexity up or down depending on your deployment environment.

How to Simplify (No MQTT Broker Required)

If setting up a Mosquitto broker on a Raspberry Pi 5 or NAS feels like overkill, strip the MQTT layer and run a local TCP socket server. Replace the umqtt logic with MicroPython's socket library. The Pico will listen on port 8080; you can simply open a web browser on your phone, type the Pico's IP address, and read the raw JSON telemetry. This eliminates broker dependencies but sacrifices the ability to log historical data easily.

How to Extend (Home Assistant Integration)

To make this node instantly visible in Home Assistant without manual YAML configuration, implement MQTT Auto-Discovery. Before your main while loop, publish a retained configuration message to the homeassistant/sensor/{CLIENT_ID}/config topic. The payload should be a JSON object defining the device class, unit of measurement, and state topic. Home Assistant will automatically parse this and create dashboard entities for temperature, humidity, and pressure.

For off-grid deployments, integrate a 3.7V LiFePO4 cell and a TP4056 charging module. Safety Caveat: Never connect raw lithium cells directly to the Pico's VSYS pin without a proper Battery Management System (BMS) and charge controller. Use the RP2350's internal RTC (Real Time Clock) and the machine.deepsleep() function to wake the board every 15 minutes, transmit data, and return to a ~1mA sleep state, extending battery life from days to months.

For further reading on MicroPython network configurations and RP2350 specific features, consult the official MicroPython WLAN documentation and the Raspberry Pi Pico series hardware specs.