Raspberry Pi Pico W MQTT: Publish Sensor Data and Receive Commands

A web server on the Pico W works well inside your home network, but it does not scale: every client must know the Pico's IP address and poll it. MQTT turns this around. Devices connect out to a central broker, publish messages on named topics, and subscribe to the topics they care about. A dashboard, a phone app, a Node-RED flow or another microcontroller can all exchange data through the broker without knowing anything about each other. In this tutorial your Pico W will publish its temperature every ten seconds and accept LED commands over MQTT, with a last-will message and automatic reconnection.

MQTT Concepts You Need

TermMeaningExample in this project
BrokerServer that receives and forwards all messagestest.mosquitto.org on port 1883, or your own Mosquitto
TopicSlash-separated name for a message streamplearn/pico1/temp
PublishSend a message to a topicPico sends 31.2 to the temp topic
SubscribeAsk the broker for messages on a topicPico listens on plearn/pico1/led/set
Wildcards+ matches one level, # matches the restplearn/# receives everything
RetainBroker stores the last message for new subscribersStatus online is retained
Last willMessage the broker publishes if the client vanishesStatus becomes offline
QoS 0 / 1At most once / at least once deliveryQoS 0 used here for simplicity

Setup

  1. Flash the Pico W MicroPython firmware and create secrets.py with SSID and PASSWORD, as in the web-server tutorial.
  2. Check that the MQTT client is available by typing from umqtt.simple import MQTTClient in the REPL. If it fails, connect to Wi-Fi and run import mip then mip.install('umqtt.simple'), or install micropython-umqtt.simple from Thonny's Manage packages.
  3. On your PC, install the Mosquitto command-line clients (mosquitto_pub and mosquitto_sub) or a graphical tool such as MQTT Explorer for testing.

The MQTT Client Program

main.py - Pico W publishes chip temperature, obeys LED commands, uses last will and reconnects
# main.py - Pico W MQTT node: publish temperature, subscribe to LED commands
import network
import time
import ubinascii
import machine
from machine import Pin, ADC
from umqtt.simple import MQTTClient
import secrets

BROKER = 'test.mosquitto.org'
PORT = 1883
CLIENT_ID = b'pico-' + ubinascii.hexlify(machine.unique_id())
BASE = b'plearn/pico1/'                 # change to something unique
T_TEMP = BASE + b'temp'
T_STATUS = BASE + b'status'
T_LED_SET = BASE + b'led/set'
T_LED_STATE = BASE + b'led/state'
PUBLISH_EVERY_MS = 10_000

led = Pin('LED', Pin.OUT)
temp_adc = ADC(4)

def chip_temp():
    total = sum(temp_adc.read_u16() for _ in range(32)) / 32
    v = total * 3.3 / 65535
    return 27 - (v - 0.706) / 0.001721

def wifi_connect():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        wlan.connect(secrets.SSID, secrets.PASSWORD)
        for _ in range(40):              # wait up to 20 s
            if wlan.isconnected():
                break
            time.sleep(0.5)
    if not wlan.isconnected():
        raise RuntimeError('Wi-Fi not connected')
    print('IP:', wlan.ifconfig()[0])

def on_message(topic, msg):
    # Called from check_msg() when a subscribed message arrives
    print('RX', topic, msg)
    if topic == T_LED_SET:
        cmd = msg.strip().lower()
        if cmd in (b'on', b'1'):
            led.on()
        elif cmd in (b'off', b'0'):
            led.off()
        elif cmd == b'toggle':
            led.toggle()
        client.publish(T_LED_STATE, b'on' if led.value() else b'off', retain=True)

def mqtt_connect():
    c = MQTTClient(CLIENT_ID, BROKER, port=PORT, keepalive=60)
    c.set_callback(on_message)
    # If the Pico disappears, the broker publishes 'offline' for us
    c.set_last_will(T_STATUS, b'offline', retain=True)
    c.connect()
    c.publish(T_STATUS, b'online', retain=True)
    c.subscribe(T_LED_SET)
    print('MQTT connected as', CLIENT_ID)
    return c

wifi_connect()
client = mqtt_connect()
last_pub = time.ticks_ms() - PUBLISH_EVERY_MS
failures = 0

while True:
    try:
        client.check_msg()               # non-blocking: handle any incoming message
        if time.ticks_diff(time.ticks_ms(), last_pub) >= PUBLISH_EVERY_MS:
            last_pub = time.ticks_ms()
            payload = '{:.1f}'.format(chip_temp())
            client.publish(T_TEMP, payload)
            print('TX', T_TEMP, payload)
        failures = 0
        time.sleep_ms(100)
    except OSError as e:
        # Network or broker problem: wait, then rebuild the connection
        failures += 1
        print('Connection lost:', e, 'attempt', failures)
        if failures > 5:
            machine.reset()              # last resort: clean restart
        time.sleep(5)
        try:
            wifi_connect()
            client = mqtt_connect()
        except OSError as e2:
            print('Reconnect failed:', e2)
        except RuntimeError as e3:
            print(e3)

Testing from Your Computer

Run the program on the Pico W, then open two terminals on your PC. In the first, subscribe to everything under your prefix; -v prints the topic with each message. In the second, send LED commands.

Terminal 1:
mosquitto_sub -h test.mosquitto.org -t "plearn/pico1/#" -v
plearn/pico1/status online
plearn/pico1/temp 30.8
plearn/pico1/temp 30.9

Terminal 2:
mosquitto_pub -h test.mosquitto.org -t "plearn/pico1/led/set" -m on
mosquitto_pub -h test.mosquitto.org -t "plearn/pico1/led/set" -m toggle

The LED reacts within a fraction of a second, and Terminal 1 shows the new led/state. Now pull the Pico's USB cable. After the keepalive period expires (about one and a half times the 60 s keepalive), Terminal 1 prints plearn/pico1/status offline: that is the last will at work.

Design Notes

  • The client ID is built from machine.unique_id(), so two boards running the same code never kick each other off the broker. Brokers disconnect an existing client when another connects with the same ID.
  • Topics and payloads are bytes (b'...'); umqtt.simple works with bytes throughout.
  • check_msg() returns immediately if nothing is waiting, which lets the loop publish on a schedule. wait_msg() would block until a message arrives.
  • Publishing every 10 s keeps the connection active within the 60 s keepalive. If you publish less often, call client.ping() periodically.
  • Retained status and led/state messages mean a dashboard that connects later still sees the current state immediately.

Topic and Payload Design

This example sends one plain number per topic, which is easy to read in a terminal and easy to graph in tools such as Node-RED. When a node has several sensors, many projects instead publish one JSON object, for example {"temp": 30.8, "hum": 61}, built with MicroPython's json.dumps(). Whichever style you pick, keep a consistent hierarchy such as site/device/measurement, use lowercase names, and keep command topics (led/set) separate from state topics (led/state). That separation prevents a device from reacting to its own state messages and makes wildcard subscriptions predictable.

Troubleshooting

  • MQTTException: 5: the broker refused authorisation. Pass user= and password= to MQTTClient if your broker requires them.
  • OSError from connect() right after Wi-Fi comes up: often the broker name could not be resolved or port 1883 is blocked by the network (some college and office networks block it). Try a phone hotspot or your own broker's IP address.
  • Commands not received: the topic strings differ by a character or case. MQTT topics are case-sensitive.
  • Frequent reconnects: a weak Wi-Fi signal. Move closer to the router or reduce publish rate.

Summary

With umqtt.simple your Pico W became a proper IoT node: it publishes readings to a broker, reacts to commands on a subscribed topic, reports online and offline status through retained messages and a last will, and recovers from network errors. From here you can connect the same topics to Node-RED, Home Assistant or a cloud MQTT service.