Pi IoT Academy
Phase 3 · Talk over MQTT

Phase 03

Talk over MQTT.

In Phase 2 you proved the bus works by piping a single message through. Now you’ll wire it up properly: a fake sensor on your laptop publishes a temperature reading every second, the API picks it up, and the Pi’s screen shows a live card that updates in real time.

~45–60 min Difficulty: 2 / 3 One laptop is enough Software lesson

How sensor data flows

Read this first.

Here is the path a single temperature reading will take:

data flow
laptop  ─[publish]─→  Mosquitto  ─[subscribe]─→  API  ─[WebSocket]─→  dashboard
sensor                broker                    (Pi)                  (browser)

Three jobs. Each one is small, each one is useful on its own:

1. Publisher
Anything that produces data and sends it. Your laptop today, a Micro:bit next week.
2. Broker
Mosquitto on the Pi. It doesn’t store data — it just routes messages from publishers to subscribers.
3. Subscriber(s)
The API listens; whenever a message arrives it pushes it to every browser viewing the dashboard.
Why this is clever

The publisher doesn’t know who’s listening. The dashboard doesn’t know which Micro:bit sent the data. Both only know the broker. That’s called decoupling — you can add a second sensor, or a third dashboard, without changing any of the others.

Open the dashboard

~2 minutes

From your laptop, open the dashboard:

browser
http://pi-iot-XX.local/

It should say “No sensor data yet.” with a connection pill in the top-right that reads connected. That’s the dashboard reporting that it has an open WebSocket to the API. It just hasn’t seen any sensor messages yet — because nothing is publishing.

If the pill says disconnected

The API isn’t reachable. SSH into the Pi and run docker compose ps — the api service should be Up. If not, check the logs: docker compose logs api.

Checkpoint

Look at a real message

~5 minutes

Before we send any messages, here’s the shape they’ll all use. Every reading published to the platform looks like this:

topic
classroom/sensor/microbit-test-01/temperature
payload (JSON)
{
  "ts": "2026-05-10T13:45:12.123Z",
  "source": "microbit-test-01",
  "schema": "v1",
  "data": {
    "value": 24.3,
    "unit": "celsius",
    "quality": "good"
  }
}

The topic is structured: <domain>/sensor/<device>/<measurement>. Every part is lowercase, every part is meaningful. The API subscribes to +/sensor/+/+, where + means “match any one segment” — so it picks up every sensor on every domain.

Why JSON, why schema: "v1"?

JSON is human-readable and trivially decoded everywhere. The schema field is a promise: “this message has the v1 shape”. When we change the shape later we’ll bump it to v2, and the API will be able to handle both.

Checkpoint

Run the test publisher

~10 minutes · on your laptop

The platform repo includes a small Python script that publishes fake temperature and humidity readings once a second. We’ll run it on your laptop — it talks to the Pi over Wi-Fi.

If you don’t already have the repo on your laptop, clone it. Then in a terminal:

terminal · on your laptop
cd /path/to/iot-platform
pip install aiomqtt
Use a virtual environment

If pip install complains about “externally-managed-environment”, your system Python is locked. Create an isolated one:

python3 -m venv .venv
source .venv/bin/activate
pip install aiomqtt

Now run the publisher, pointing it at your Pi:

terminal · on your laptop
python3 scripts/publish-test-sensor.py \
  --host pi-iot-XX.local \
  --password "<your MQTT password>"

You should see lines tick by:

expected output
→ connecting to pi-iot-XX.local:1883 as platform
→ publishing as microbit-test-01 on classroom/sensor/microbit-test-01/{temperature,humidity}
✓ connected. Publishing 1 Hz. Ctrl-C to stop.
  t=   0.0s  temperature= 22.05 °C  humidity= 60.4 %
  t=   1.0s  temperature= 22.18 °C  humidity= 60.5 %
  t=   2.0s  temperature= 21.93 °C  humidity= 60.6 %

Leave it running. Don’t close the terminal yet. We’ll come back to it.

“Connection refused” or “not authorised”?

Same fixes as Phase 2: check the password matches .env on the Pi, and that your laptop is on the same Wi-Fi.

Checkpoint

Watch the cards update

~5 minutes · the fun bit

Switch back to the dashboard tab. Within one second of starting the publisher, you should see two cards appear:

  • microbit-test-01 / temperature with a value like 22.1 °C.
  • microbit-test-01 / humidity with a value like 60.4 %.

Each card’s number should change every second — the value goes up and down a bit because the publisher’s fake signal is a slow sine wave plus noise. Below the value you’ll see “just now” or “1 s ago”, ticking up.

Try this:

  1. Open a second tab on the same dashboard URL. The same cards appear instantly. Both tabs are subscribed to the same WebSocket; both get the same updates.
  2. Run a second publisher in another terminal with a different device name:
    terminal 2 · laptop
    python3 scripts/publish-test-sensor.py \
      --host pi-iot-XX.local \
      --password "<your MQTT password>" \
      --device microbit-test-02

    Two more cards appear. The dashboard didn’t need to be told — it just received new sensor IDs from the API.

What you just demonstrated

The platform doesn’t hard-code which sensors exist. New ones appear when they show up. Old ones stick around showing their last value. This is how every IoT system you’ll ever build should behave.

Checkpoint

Peek at MQTT in Node-RED

~10 minutes

Node-RED is your X-ray machine for the bus. Let’s use it to see every message flying through the broker, in real time.

Open Node-RED:

browser
http://pi-iot-XX.local:1880/

Build a tiny three-node flow:

  1. From the left sidebar, drag an mqtt in node onto the canvas.
  2. Drag a debug node next to it.
  3. Connect the output of the mqtt node to the input of the debug node (click and drag from the right edge of one to the left edge of the other).
  4. Double-click the mqtt node:
    • Server: click the pencil and add a new server.
      • Server: mosquitto
      • Port: 1883
      • On the Security tab, set Username and Password to match your .env.
      • Click Add.
    • Topic: +/sensor/+/+
    • Output: a parsed JSON object
    • Click Done.
  5. Click the red Deploy button (top-right).
  6. On the right, click the bug-shaped tab to open the Debug pane.

You should see a new debug entry appear roughly every half-second — one per published topic. Click any of them to expand the JSON. Compare it to the shape from Step 2.

When you’ll really love this

Later in the course, when something doesn’t show up on the dashboard, this Node-RED flow tells you instantly whether the message reached the broker at all. If you see it here but not on the dashboard, the bug is in the API. If you don’t see it here, the bug is in the publisher.

Checkpoint

Stop the publisher — watch it go stale

~5 minutes

Go back to the publisher terminal and press Ctrl+C. The script stops; no more messages are sent.

Watch the dashboard. The cards keep their last value — that’s correct, the API still has the most recent reading in memory. After about 30 seconds, each card should grow a yellow STALE badge, and the timestamp underneath ticks up: “30 s ago”, “1 min ago”.

Restart the publisher. The badges disappear immediately as fresh readings arrive.

Why “stale” matters

A real classroom sensor might lose Wi-Fi, run out of battery, or just disconnect. Showing the last value is fine; showing it without telling the user it’s old is dangerous — teachers might think the room is still 22 °C when actually the sensor died an hour ago. Always surface freshness.

Checkpoint

Final verification

5 minutes

Tick everything below before declaring Phase 3 done. Phase 4 will assume readings make it from MQTT to the API reliably.

Phase 3 sign-off

Phase 3 complete?

You’ve got a real-time data pipeline. A reading made on a laptop at one end of the school can show up on a touchscreen at the other end in under a second. Next phase: stop losing the data when the Pi reboots.

Phase 4: Save your data  →

(The button unlocks when every box above is ticked.)