A pico w battery mqtt sensor only works if the board spends almost all of its life asleep and only a few seconds awake. In this tutorial you will build a MicroPython sensor node that wakes on a timer, reads a sensor and its own battery voltage, publishes one QoS 1 message to an MQTT broker, shuts the radio down and goes back to lightsleep. You will also learn which design choices actually move battery life and which ones are wasted effort.
Why Battery Power Is Hard on the Pico W
The Pico W was designed as a cheap, flexible microcontroller board, not as an ultra-low-power sensor platform. The RP2040 itself is efficient enough, but three things work against you on batteries:
- The CYW43439 Wi-Fi chip draws far more current while associating and transmitting than the microcontroller does while computing. Every second the radio is on costs more than minutes of sleep.
- The on-board buck-boost regulator has a quiescent current that never goes away, even when the RP2040 sleeps.
- Sleep modes in MicroPython are limited compared with chips like the ESP32.
machine.lightsleep()stops the clocks and keeps RAM, which is useful, but the board still draws noticeably more than a purpose-built low-power node.
The practical consequence is simple: battery life is dominated by how often you wake and how long the Wi-Fi connection takes. Optimising the Python code that reads a sensor makes almost no difference. Cutting a 30-second publish interval to 5 minutes, or shaving the connect time from eight seconds to three, makes a large one.
If you have not yet connected a Pico W to MQTT at all, start with the Raspberry Pi Pico W MQTT guide and come back here once a basic publish works.
The Duty Cycle Design
Every wake-up follows the same short script. Keeping it strictly linear (no background tasks, no long-lived connection) is what makes it predictable.
- Wake from lightsleep (or from a cold boot after a power cut).
- Read the battery voltage before turning on Wi-Fi.
- Read the sensor.
- Turn on Wi-Fi with a static configuration and a hard timeout.
- Connect to the broker, publish one message with QoS 1 and wait for the acknowledgement.
- Disconnect cleanly, turn the radio off and deinitialise it.
- Lightsleep for the configured interval, then loop.
| Design choice | Effect on battery life | Recommendation |
|---|---|---|
| Publish interval | Largest single factor | 5 to 15 minutes for room climate data |
| Wi-Fi connect time | Second largest factor | Strong signal, WPA2, short timeout |
| Keep MQTT connected while sleeping | Radio stays partly active | Never on batteries |
| QoS 1 publish | One extra round trip | Worth it, you know the message arrived |
Lightsleep vs busy time.sleep() |
Lightsleep stops clocks | Always lightsleep between cycles |
| LED blinking for status | Small but constant drain | Disable in production |
| External timer chip (TPL5110 or similar) | Cuts board power completely | Best option for multi-month life |
Hardware and Wiring
You need a Pico W (or Pico 2 W), a sensor and a power source on VSYS. A common setup is three AA or AAA cells in series feeding VSYS directly, or a single LiPo cell through a protection board. Do not feed more than the VSYS limit in the official datasheet, and never connect batteries to VBUS while USB is plugged in without a diode.
For the sensor, any I2C part with a low standby current works. This example uses a BME280 on I2C0 (GP4 = SDA, GP5 = SCL), but the duty-cycle logic is independent of the sensor.
The Pico W can measure its own supply voltage on ADC3, which is wired to VSYS through a 1:3 divider. On the Pico W that pin (GP29) is shared with the Wi-Fi chip's SPI clock, so you must read it while the radio is off and with GP25 driven high. That is why the voltage reading happens first in the cycle.
Prepare MicroPython and the MQTT Library
Flash a current MicroPython build for the Pico W from the official Raspberry Pi documentation, then install the umqtt.simple library and a BME280 driver with mpremote:
pip install mpremote
mpremote mip install umqtt.simple
mpremote cp bme280.py :bme280.py
Create a secrets.py on the board so credentials stay out of the main script:
# secrets.py
WIFI_SSID = "your-ssid"
WIFI_PASSWORD = "your-wifi-password"
MQTT_HOST = "broker.connectmq.com"
MQTT_PORT = 1883
MQTT_CLIENT_ID = "pico-bat-7f3a91" # must be non-empty and unique
MQTT_USER = None # set YOUR_USERNAME for a private broker
MQTT_PASSWORD = None # set YOUR_PASSWORD for a private broker
TOPIC = "connectmq/demo/7f3a91/garden/climate"
INTERVAL_S = 600 # 10 minutes between readings
The public test broker at broker.connectmq.com accepts anonymous connections with any non-empty client ID. Everything published there is readable by anyone, so use a random topic prefix and never send personal data. The free public MQTT broker guide lists its limits.
The Complete Battery MQTT Script
Save this as main.py. It runs automatically on every boot, and because the loop uses lightsleep instead of a reset, RAM state such as the cycle counter survives between wake-ups.
# main.py - battery-powered Pico W MQTT sensor
import time
import json
import network
import machine
from machine import Pin, ADC, I2C
from umqtt.simple import MQTTClient
import bme280
import secrets
led = Pin("LED", Pin.OUT)
led.off()
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=100_000)
cycle = 0
def read_vsys():
"""Read VSYS on the Pico W. Must run while the Wi-Fi chip is off."""
Pin(25, Pin.OUT).value(1) # deselect the CYW43 SPI bus
Pin(29, Pin.IN, None) # release the shared pin for the ADC
adc = ADC(3)
total = 0
for _ in range(8):
total += adc.read_u16()
time.sleep_ms(2)
raw = total / 8
return round(raw * 3.3 * 3 / 65535, 3)
def read_sensor():
sensor = bme280.BME280(i2c=i2c)
t, p, h = sensor.read_compensated_data()
# The driver returns hundredths of a degree, Pa*256 and %RH*1024
return round(t / 100, 2), round(h / 1024, 1), round(p / 25600, 1)
def wifi_up(timeout_ms=10_000):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(secrets.WIFI_SSID, secrets.WIFI_PASSWORD)
start = time.ticks_ms()
while not wlan.isconnected():
if time.ticks_diff(time.ticks_ms(), start) > timeout_ms:
return wlan, False
time.sleep_ms(100)
return wlan, True
def wifi_down(wlan):
try:
wlan.disconnect()
except OSError:
pass
wlan.active(False)
wlan.deinit()
def publish(payload):
client = MQTTClient(
secrets.MQTT_CLIENT_ID,
secrets.MQTT_HOST,
port=secrets.MQTT_PORT,
user=secrets.MQTT_USER,
password=secrets.MQTT_PASSWORD,
keepalive=30,
)
client.connect()
# QoS 1: publish() blocks until the broker returns PUBACK
client.publish(secrets.TOPIC, payload, qos=1)
client.disconnect()
while True:
cycle += 1
vbat = read_vsys()
try:
temp_c, rh, hpa = read_sensor()
except OSError:
temp_c, rh, hpa = None, None, None
awake_start = time.ticks_ms()
wlan, ok = wifi_up()
if ok:
msg = json.dumps({
"t": temp_c, "rh": rh, "hpa": hpa,
"vbat": vbat, "cycle": cycle,
"rssi": wlan.status("rssi"),
"awake_ms": time.ticks_diff(time.ticks_ms(), awake_start),
})
try:
publish(msg)
except OSError as exc:
print("MQTT failed:", exc)
wifi_down(wlan)
# Back off harder when the battery is low to protect the cells
sleep_s = secrets.INTERVAL_S if vbat > 3.3 else secrets.INTERVAL_S * 3
machine.lightsleep(sleep_s * 1000)
A few details are deliberate. The script reports awake_ms and rssi in every message so you can see, from the broker side, whether a node is spending too long connecting. The low-battery branch triples the interval rather than stopping entirely, which buys time to swap cells without losing the node. And deinit() after active(False) makes sure the CYW43 driver releases the radio before the board sleeps.
Test the Pico W Battery MQTT Messages
Before running on cells, test on USB with a short interval such as 30 seconds. Subscribe from your laptop:
mosquitto_sub -h broker.connectmq.com -p 1883 -i laptop-7f3a91 \
-t 'connectmq/demo/7f3a91/#' -v
You should see one JSON message per cycle. Watch the awake_ms field. On a healthy network it settles into a narrow, repeatable range. If it varies wildly, the access point is the problem, not your code.
Note that when the Pico W sleeps over USB, the serial REPL disconnects. That is normal. To regain control, unplug, hold BOOTSEL if needed, or use mpremote immediately after power-up before main.py enters its first sleep. A useful trick is to add a two-second window at the very top of main.py that exits to the REPL if a button on a GPIO pin is held.
Squeezing More Battery Life Out of the Pico W
Once the basic cycle works, these refinements give the best return:
- Lengthen the interval first. Halving the number of wake-ups roughly halves the radio energy, which is the biggest consumer.
- Batch readings. Read the sensor every minute during lightsleep, keep the values in a list, and publish them all in one message every ten minutes. You pay for Wi-Fi once instead of ten times.
- Improve the Wi-Fi path. Place the node where the signal is strong. A weak signal means retries, slower data rates and longer awake time.
- Use an external power timer. A timer chip that cuts power to the whole board and reapplies it on a schedule removes regulator and sleep currents entirely. In that design the script runs once from cold boot, publishes, then signals the timer to switch off. You lose RAM between cycles, so store counters in flash sparingly or let the broker hold state through retained messages.
- Disable anything that blinks. Status LEDs on the Pico and on sensor breakout boards draw current all the time. Remove or cut the power LED on breakouts where you can.
Production: Move to Private Credentials and TLS
The public broker is ideal for development, but a deployed sensor should use your own credentials. On a managed MQTT broker such as ConnectMQ, you create a broker in the portal and receive a generated username, password and client ID. Put them into secrets.py as YOUR_USERNAME, YOUR_PASSWORD and YOUR_CLIENT_ID. The client ID is bound to the issued credentials, so each node needs the one generated for it.
TLS on port 8883 adds a handshake to every wake-up, which costs extra awake time and therefore battery. For sensors on a trusted home network some people accept plain TCP; for anything leaving a building, encryption is worth the cost. The Pico W MQTT over TLS tutorial shows the SSL context setup for MicroPython. Measure awake_ms before and after so you know the real impact on your node.
Retained messages are useful here too. If you publish the last reading with the retain flag, a dashboard that opens hours later still shows the most recent value immediately instead of waiting up to ten minutes for the next wake-up. ConnectMQ supports retained messages on all plans, and its portal dashboard and live topic explorer let you watch each node's messages arrive without writing a subscriber.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
vbat reads near zero or random |
ADC read while Wi-Fi active | Read VSYS before wlan.active(True) |
| Node publishes once then stops | Wi-Fi not deinitialised before lightsleep | Call active(False) and deinit() |
OSError: -2 on connect |
DNS failed or Wi-Fi not fully up | Check isconnected() and retry once |
awake_ms keeps growing |
Weak signal or busy access point | Move the node or the AP |
| Battery dies far sooner than expected | LED, sensor standby, or regulator drain | Remove LEDs, check sensor sleep mode, add a power timer |
Get Started with ConnectMQ
When your Pico W prototype is ready to leave the bench, create a private broker in the ConnectMQ portal at app.connectmq.com. The free Serverless plan needs no credit card, supports QoS 1 and up to 25 connections, which comfortably covers a small fleet of battery sensors. For more Pi projects, the Raspberry Pi MQTT tutorial collects the full series.
Frequently Asked Questions
How long will a Pico W last on batteries with MQTT?
It depends mostly on the publish interval, Wi-Fi connect time and the cells you use, so measure your own node rather than trusting a single figure. Logging awake_ms and vbat over a few days gives you a reliable trend to extrapolate from.
Can the Pico W stay connected to MQTT while sleeping?
Not in a battery-friendly way. Keeping the MQTT session alive requires the radio to stay associated, which drains cells quickly, so reconnect on every wake-up instead.
Should I use QoS 0 or QoS 1 for a battery sensor?
QoS 1 is usually the better choice because publish() waits for the broker's acknowledgement before you turn the radio off. The extra round trip is small compared with the cost of joining Wi-Fi.
Is machine.deepsleep better than lightsleep on the Pico W?
On the RP2040 MicroPython port the difference is modest, and deepsleep resets the board so you lose RAM state. For the longest life, an external timer that removes power completely beats both.



