The Verdict: Choosing the Pico 2 W for Wireless Control
If you are building a Wi-Fi connected relay node in 2026, the Raspberry Pi Pico 2 W (featuring the RP2350 chip, 4MB flash, and the Infineon CYW43439 Wi-Fi/Bluetooth module) is the definitive default pick. It outperforms the older RP2040-based Pico 1 W in deep-sleep current and processing headroom, while avoiding the ADC non-linearity quirks and higher idle power draw of the ESP32-C3.
Below is the decision matrix to confirm this is the right board for your specific build:
| Board Variant | Core / Flash | Wireless | Best Use Case |
|---|---|---|---|
| Pico 1 W | RP2040 / 2MB | Wi-Fi 4 / BT | Legacy replacements, extreme budget constraints. |
| Pico 2 | RP2350 / 4MB | None | High-speed data logging, offline motor control. |
| Pico 2 W | RP2350 / 4MB | Wi-Fi 4 / BT 5.2 | Smart home nodes, MQTT sensors, IoT relays. |
| ESP32-C3 | RISC-V / 4MB | Wi-Fi 4 / BT 5 | High-temp environments, existing ESP-IDF codebases. |
Default Pick: For any new Home Assistant or MQTT-based relay project requiring local failsafes and low standby power, buy the Pico 2 W.
Hardware BOM and RP2350 Pin Mapping
The code and wiring below target the Raspberry Pi Pico 2 W (RP2350, 4MB Flash, Arm Cortex-M33 mode). Do not use the RISC-V Hazard3 core for this specific build, as the Arduino-Pico Wi-Fi libraries are currently most stable on the M33 core.
Parts List
- MCU: Raspberry Pi Pico 2 W (with pre-soldered headers).
- Relay Module: 5V 10A Opto-isolated relay (Songle SRD-05VDC-SL-C). Must be opto-isolated to protect the 3.3V logic.
- Display: 0.96" I2C OLED, SSD1306 driver, 128x64 pixels (3.3V tolerant variant).
- Power: 12V-to-5V buck converter (LM2596 or similar) if powering from a 12V alarm/HVAC bus, or a standard 5V 2A USB-C supply.
Pin Mapping Table
| Function | Pico 2 W Pin (GP) | Notes & Constraints |
|---|---|---|
| I2C SDA (OLED) | GP4 (Pin 6) | Default I2C0 SDA. 3.3V logic only. |
| I2C SCL (OLED) | GP5 (Pin 7) | Default I2C0 SCL. Use 4.7k pull-ups if not on module. |
| Relay Control | GP16 (Pin 21) | Drives opto-isolator LED. Active LOW on most modules. |
| Local Override Button | GP17 (Pin 22) | Internal pull-up enabled in code. |
The Infineon CYW43439 Wi-Fi chip on the Pico 2 W communicates with the RP2350 via a dedicated internal SPI bus. GP23, GP24, GP25, and GP29 are permanently consumed by the Wi-Fi chip. Do not attempt to use these pins for external SPI displays, sensors, or GPIO, or your Wi-Fi will silently fail to initialize.
Wiring Procedure and Power Isolation
When switching inductive loads (like solenoid valves or contactor coils) with a microcontroller, back-EMF will destroy your silicon if you aren't careful. Follow this sequence:
- Isolate the Logic: Connect the Pico 2 W GP16 to the signal input of the opto-isolated relay module. Do not share the 5V relay coil power with the Pico's 3.3V or 5V VBUS pins.
- Wire the I2C Bus: Connect GP4 to SDA and GP5 to SCL. If your OLED module lacks onboard pull-up resistors, solder 4.7kΩ resistors from SDA and SCL to the 3.3V (Pin 36) rail.
- Local Override: Wire a momentary pushbutton between GP17 and GND. The code will use the RP2350's internal pull-up resistor.
- Flyback Protection: Ensure your relay module has a flyback diode across the coil. If you are wiring a bare relay, place a 1N4007 diode in reverse parallel across the coil terminals.
Complete C++ Firmware with Error Handling
This sketch uses the Arduino-Pico core. It connects to Wi-Fi, subscribes to an MQTT command topic, updates a local OLED, and includes a hardware watchdog timer (WDT) to recover from CYW43439 lockups.
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <hardware/watchdog.h>
// --- PIN DEFINITIONS ---
#define RELAY_PIN 16
#define BUTTON_PIN 17
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
// --- NETWORK CREDENTIALS ---
const char* ssid = "YOUR_2.4GHZ_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.50";
const char* mqtt_topic_cmd = "home/relay/pico2w/set";
const char* mqtt_topic_state = "home/relay/pico2w/state";
WiFiClient espClient;
PubSubClient client(espClient);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
bool relayState = false;
unsigned long lastReconnectAttempt = 0;
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, HIGH); // Active LOW relay, start OFF
pinMode(BUTTON_PIN, INPUT_PULLUP);
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Halt if display fails
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("Pico 2 W Booting...");
display.display();
WiFi.begin(ssid, password);
// Enable Hardware Watchdog (8 seconds timeout)
watchdog_enable(8000, 1);
}
void loop() {
watchdog_update(); // Feed the dog
if (WiFi.status() != WL_CONNECTED) {
handleWiFiReconnect();
}
if (!client.connected()) {
handleMQTTReconnect();
}
client.loop();
// Local button override
if (digitalRead(BUTTON_PIN) == LOW) {
delay(50); // Debounce
if (digitalRead(BUTTON_PIN) == LOW) {
toggleRelay(!relayState);
while(digitalRead(BUTTON_PIN) == LOW); // Wait for release
}
}
}
void toggleRelay(bool state) {
relayState = state;
digitalWrite(RELAY_PIN, relayState ? LOW : HIGH);
client.publish(mqtt_topic_state, relayState ? "ON" : "OFF", true);
updateDisplay();
}
void callback(char* topic, byte* payload, unsigned int length) {
String msg = "";
for (int i = 0; i < length; i++) msg += (char)payload[i];
if (msg == "ON") toggleRelay(true);
else if (msg == "OFF") toggleRelay(false);
}
void handleWiFiReconnect() {
display.clearDisplay();
display.setCursor(0,0);
display.print("WiFi Drop. Status: ");
display.println(WiFi.status());
display.display();
WiFi.reconnect();
delay(2000);
}
boolean handleMQTTReconnect() {
if (millis() - lastReconnectAttempt > 5000) {
lastReconnectAttempt = millis();
String clientId = "Pico2W_" + String(random(0xffff), HEX);
if (client.connect(clientId.c_str(), "home/status", 0, true, "offline")) {
client.subscribe(mqtt_topic_cmd);
client.publish("home/status", "Pico 2 W Online", true);
updateDisplay();
} else {
Serial.print("MQTT failed, rc=");
Serial.println(client.state());
}
}
return client.connected();
}
void updateDisplay() {
display.clearDisplay();
display.setCursor(0,0);
display.println("MQTT Relay Node");
display.print("IP: "); display.println(WiFi.localIP());
display.print("State: "); display.println(relayState ? "ON" : "OFF");
display.display();
}
Debugging: Exact Error Strings and the First Three Checks
The CYW43439 Wi-Fi chip is highly capable but unforgiving of RF environment and power issues. If your serial monitor fills with errors, perform these first three checks before touching the code:
- Verify the 2.4GHz Band: The CYW43439 does not support 5GHz or 6GHz Wi-Fi. If your router uses a unified SSID for both bands, force the Pico to connect by temporarily disabling 5GHz on your router, or create a dedicated 2.4GHz IoT SSID.
- Check 3.3V Logic on I2C: If the OLED is blank but the relay clicks, you likely fried the RP2350's GP4/GP5 pins by connecting a 5V I2C display without a logic level shifter. Measure the SDA/SCL pins with a multimeter; they should read ~3.3V when idle.
- Inspect the RF Shield: If Wi-Fi connects only when the board is inches from the router, check if your enclosure has metallic paint or if a ground plane is blocking the PCB antenna. The Pico 2 W antenna is on the end of the board; keep it clear of copper pours.
Ranked Causes for Exact Error Strings
Error String: WiFi.status() returned 1 (WL_NO_SSID_AVAIL)
- Cause 1 (Most Likely): SSID string in code has a typo, or router is set to hide the SSID (the Pico Arduino core struggles with hidden SSIDs on initial connect).
- Cause 2: Router is enforcing WPA3-Only. The CYW43439 supports WPA3, but the Arduino-Pico
WiFi.himplementation sometimes fails the handshake. Set router to WPA2/WPA3 Transitional.
Error String: MQTT connect failed, rc=-2
According to the PubSubClient API documentation, rc=-2 means the network connection failed. This is rarely an MQTT broker issue; it means the Pico dropped Wi-Fi right before the TCP handshake.
- Cause 1: Wi-Fi dropped due to a brownout. When the relay coil engages, it pulls a spike of current. If sharing a weak 5V USB supply, the RP2350 brownouts and the CYW43439 resets. Use a dedicated buck converter or a large capacitor (1000µF) on the 5V rail.
- Cause 2: Broker IP address changed or firewall is blocking port 1883. Ping the broker IP from another device on the same VLAN.
Error String: SSD1306 allocation failed (Halt in setup)
- Cause 1: Incorrect I2C address. Run an I2C scanner sketch. Some 0.96" OLEDs use
0x3Dinstead of0x3C. - Cause 2: Missing pull-up resistors on SDA/SCL lines causing the
Wirelibrary to hang during initialization.
Extending vs. Simplifying the Build
Once the baseline firmware is stable on your bench, you need to decide how to adapt it for permanent deployment.
How to Extend (The Smart Home Route)
If you are integrating this into Home Assistant, do not manually configure YAML files. Extend the C++ code to publish an MQTT Discovery JSON payload to the homeassistant/switch/pico2w/config topic on boot. This forces Home Assistant to automatically create the switch entity, complete with availability tracking tied to the Last Will and Testament (LWT) message configured in the client.connect() function.
How to Simplify (The Production Route)
For a node stuffed inside a junction box or DIN-rail enclosure, the OLED is a liability. It draws ~20mA continuously and introduces I2C bus lockup risks in high-EMI environments (like near VFDs or heavy contactors). To simplify:
- Remove the
Adafruit_SSD1306library and alldisplay.*calls to free up ~15KB of flash and reduce RAM fragmentation. - Replace the visual feedback with a single 3mm LED on GP15 to blink the Wi-Fi status.
- Rely entirely on the
watchdog_update()and MQTT LWT messages to monitor node health remotely.






