Stage 5 of 6 · Lab · about 25 minutes, plus the ladder
The Pi listens
mosquitto_sub proves messages flow; Python makes them mean something. The Pi-side client is paho-mqtt, the Eclipse Foundation's reference library, and its shape will feel familiar on arrival: you hand it callbacks and start a loop, when_pressed for the network age. This is the page where Week 6's quiet promise about objects and callbacks gets kept in full.
Install: the venv habit pays again
paho-mqtt comes from PyPI, and Week 4 already taught the only correct way onto a modern Pi, inside the project's virtual environment, never sudo pip:
you@lastname-pi:~/iot-labs $ source .venv/bin/activate (.venv) you@lastname-pi:~/iot-labs $ pip install paho-mqtt
paho-mqtt: the callback API
A minimal, complete subscriber, current (2.x) API:
# monitor_min.py: print every light reading as it arrives import paho.mqtt.client as mqtt BROKER = "localhost" # the broker is this very Pi TOPIC = "bench/s07/light" def on_connect(client, userdata, flags, reason_code, properties): print("Connected:", reason_code) client.subscribe(TOPIC) # subscribe HERE, not outside: see below def on_message(client, userdata, msg): pct = float(msg.payload.decode()) print(f"{msg.topic}: {pct:.1f} %") client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) client.on_connect = on_connect client.on_message = on_message client.connect(BROKER, 1883) client.loop_forever()
Read it as the event-driven program it is, then keep four facts:
loop_forever()is the engine. Callbacks fire only while a network loop runs; a script that assigns callbacks and exits hears nothing, this week's version of Week 4's "the program must stay alive forwhen_pressedto matter".- Subscribe inside
on_connect. Subscriptions die with the connection; paho reconnects automatically after drops, andon_connectre-fires each time, re-planting the subscription. Subscribed outside, a monitor survives its first disconnection as a zombie: connected, silent. - Payloads arrive as bytes.
msg.payload.decode()mirrors the node's.encode(): the two ends of one contract, and forgetting it printsb'42.3', this week's most popular bug. - The version flag matters.
CallbackAPIVersion.VERSION2selects the current callback signatures. Most tutorials you will find online predate it (1.x:on_connect(client, userdata, flags, rc)), and pasting their code raises signature errors; the troubleshoot table reads that symptom.
LightMonitor: the Week 6 promise, kept
Week 6 closed by promising that "Week 9's MQTT client is an object whose callbacks you set", and that state plus behaviour would keep moving in together. Here is that sentence as code, the monitor refactored the way Week 6 refactored the reaction game: state (last_pct, a reading counter) in attributes, behaviour in methods, and, the new move, bound methods handed over as callbacks:
# lightmonitor.py: a monitor with memory import paho.mqtt.client as mqtt class LightMonitor: """Follows one station's light topic; remembers the latest reading.""" def __init__(self, broker, station): self.topic = f"bench/{station}/light" self.last_pct = None self.count = 0 self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) self.client.on_connect = self.on_connect # bound methods as callbacks: self.client.on_message = self.on_message # self travels with the function self.client.connect(broker, 1883) def on_connect(self, client, userdata, flags, reason_code, properties): client.subscribe(self.topic) def on_message(self, client, userdata, msg): self.last_pct = float(msg.payload.decode()) self.count += 1 self.react() def react(self): print(f"[{self.count:4d}] {self.topic}: {self.last_pct:.1f} %") def run(self): self.client.loop_forever() if __name__ == "__main__": LightMonitor("localhost", "s07").run()
self.on_message is a bound method: the object rides along inside it, so when paho calls it with a message, the method reaches self.last_pct without paho knowing objects are involved at all, the exact mechanism of Week 6's NightLight (button.when_pressed = self.press), now carrying network events. The design earns its keep immediately: every ladder rung below is a subclass-or-extend of this class, mostly by overriding react(), and Week 10's controller is this class with a control law where the print is.
The ladder
Rungs in order; each is demonstrable on its own. Rung 3 changes the system's shape, worth the picture first:
- Status, properly. On the node, with
STATUS = b"bench/s07/status": beforeconnect(), lodgeset_last_will(STATUS, b"offline", retain=True), and giveMQTTClientan explicitkeepalive=30; just after connecting, publishb"online", retained, to the same topic. Demonstrate the full theory: subscribe tostatusfrom a fresh terminal (the retainedonlinearrives instantly), then pull the node's USB and time how long untilofflineappears, about a keep-alive and a half, exactly as the theory page priced it. - The dark alert: first distributed behaviour. Wire Week 4's LED circuit on the Pi's breadboard (GPIO17, 330 Ω, the rules still apply) and override
react(): below 20 %,self.led.on(), else off, gpiozero and paho in one class. Shade the sensor on one machine; a light answers on another. That is sense → network → decide → actuate, the full IoT arc, in about six lines of subclass. - The command channel. Pi side: publish
"led:on"to the station'scmdtopic,self.client.publish(CMD, "led:on"), from a tiny keyboard loop (or on the dark threshold). Node side:set_callback(handle_cmd)thensubscribe(CMD)on the client, andclient.check_msg()added to the publish loop, the non-blocking "anything for me?" that fits a once-a-second rhythm;handle_cmdparsesled:on/led:off/duty:30000and drives the Week 8 PWM LED. The bench is now bidirectional, and remote actuation, the half of IoT that makes safety engineers careful, is yours, on a supervised LAN, with the security section's words ringing. - Structured payloads. Publish
{"pct": 42.3, "raw": 1876, "seq": 1041}instead:json.dumps(...).encode()on the node (MicroPython's module answers tojson; older firmware saysujson),json.loads(msg.payload)in the monitor. One topic, several fields, self-describing: the format Week 11's dashboard will want, and the moment Week 8's "plain number now, structure later" promise matures. - Stretch: memory or breadth. Either a
LightLogger(LightMonitor)whosereact()appendstimestamp,pctto a CSV (Week 10's tuning sessions will thank you for the data), or a wildcard monitor onbench/+/lightthat keepslast_pctper station in a dictionary, your first taste of one consumer following a whole fleet.
Checklist for this stage
Check yourself
A subscriber assigns on_message, calls connect(), and ends. It never prints. Why?
loop_forever() (or a started background loop). Assigning callbacks is wiring; the loop is electricity, Week 4's pause lesson at network scale.A monitor works all afternoon, survives a broker restart, and is silent ever after, though status shows it connected. Diagnose.
on_connect. Paho auto-reconnected after the restart, but subscriptions die with the old connection, and nothing re-planted them. Subscribing inside on_connect makes every (re)connection re-subscribe, which is the whole argument for putting it there.What exactly travels when you write self.client.on_message = self.on_message, and why does it fulfil Week 6's promise?
self attached, reaching the object's state (last_pct, count). It is button.when_pressed = self.press with the button replaced by a network, state and behaviour moved in together, as promised.Rung 1's offline took about 45 seconds to appear after the USB pull, with keepalive=30. Account for the delay.
Why does the node use check_msg() in its loop rather than wait_msg()?
wait_msg() blocks until a message arrives, which would stop the sensor loop dead between commands. check_msg() is the non-blocking peek: handle a command if one is waiting, otherwise carry on reading and publishing, the single-loop version of doing two jobs.Name the two topics and two payload directions that make rung 3 "bidirectional", and what never happens between the machines.
bench/s07/light (node publishes, Pi subscribes); commands down on bench/s07/cmd (Pi publishes, node subscribes). What never happens: a direct connection, neither machine knows the other's address; both speak only to the broker.