The first piece of turning these machines into connected endpoints. classroom-mqtt is one small service per room that subscribes, validates what arrives, and hands the work to commands that already exist. Nothing in the room depends on it. A broker that is down, a network that is missing, a machine with no broker configured at all — each is a service sitting quietly, not a classroom that fails. Nothing waits for it at boot and the shipped default is off, so every existing classroom upgrades into this release with messaging disabled until a broker exists. The room is the hostname, which is already how these machines are named. Topics carry no product name because the broker is the church's and these are just rooms: classroom/notify/all, classroom/notify/<room>, and a retained classroom/status/<room> so anyone asking later can be told which rooms are up without waiting for them to say so again. Notifications are events and are never retained; status is state and is. Stale messages cannot come back, which is the property that matters most and has two halves. The session is clean, so a broker never hands over a backlog from while the room was off. And retained notifications are refused on arrival, because otherwise one would replay into every room at every reboot forever. The honest limit is written down: MQTT only flags a retained message when it is replayed to a new subscription, so a retained publish delivered live is indistinguishable from an ordinary one and is shown once — which is right, since it is live at that moment, and the replay is the actual hazard. Delivery reuses the session-aware sender rather than a second copy of it. classroom-notify-test grew into the real thing, so it is now classroom-notify with the sample sender a wrapper over it, and its 36 tests came along. The broker password lives in device.conf, so that file stops being world readable: 0640 root:classroom, which everything that reads it already satisfies. Transport logic is kept free of any broker library so it can be tested without one — 40 checks on room identity, the topic contract and which payloads become notifications. Separately verified against a real mosquitto: connect, subscribe, retained status, per-room and all-room delivery, urgency, duplicate and expired and malformed messages ignored, the broker killed with the daemon surviving, reconnect, and a stale retained notification left on the broker not reaching the screen when a room boots. Co-Authored-By: Claude Opus 5 <[email protected]>
4.8 KiB
Classroom messaging
The rooms talk to the church over MQTT. classroom-mqtt is the link:
one small service per room that subscribes, validates what arrives, and
hands the work to the classroom commands that already exist.
Nothing in the room depends on it. A broker that is down, a network that is missing, a machine with no broker configured at all — each of those is a service sitting quietly, not a classroom that fails. The dock, the browser and the DVD player never ask whether it is connected, and nothing waits for it at boot.
The room is the machine
The room name is the hostname: nursery, k2, older. Lowercased,
domain stripped, and anything that is not a letter, digit, - or _
replaced, because it becomes a topic segment. ROOM= in device.conf
overrides it for a machine whose hostname cannot be changed.
Topics
No product name appears in any topic. The broker is the church's and these are just rooms.
| Topic | Retained | Direction | Meaning |
|---|---|---|---|
classroom/notify/all |
no | to the rooms | Show this in every room |
classroom/notify/<room> |
no | to the rooms | Show this in one room |
classroom/status/<room> |
yes | from the room | Whether that room is up |
Notifications are events, so they are never retained. Status is state,
so it is — published on connect, and set to online: false by the
broker's last will if the room drops off.
Notification payload
{
"id": "unique-id",
"title": "Sermon Starting",
"message": "The sermon has started in the sanctuary.",
"urgency": "normal",
"expires_at": 1780000000
}
Only title is required. urgency is low, normal or critical and
anything else becomes normal. expires_at is epoch seconds. id is
what deduplication works on; without one, a message is shown every time
it arrives.
Status payload
{"room": "nursery", "online": true, "version": "0.1.36", "since": 1780000000}
What gets dropped
Every one of these is ordinary traffic, logged and counted, never a reason to fall over:
- retained notifications (see below)
- payloads that are not JSON, or not a JSON object
- a missing, blank or non-text title
- a message or id that is not text
- an
expires_atthat is not a time, or is in the past - an id already seen — the last 512 are remembered
Stale messages cannot come back
This is the property that matters most, and it has two halves.
The subscription is clean. The client connects with a clean session, so a broker never hands over a backlog from while the room was off. Yesterday's message is not news.
Retained notifications are refused. If someone publishes a notification with the retain flag, the broker will replay it to every room that subscribes afterwards — at every reboot, forever. Those are dropped on arrival.
One honest limitation: MQTT only sets the retain flag on a message replayed to a new subscription. A retained publish delivered live to an already-connected room is indistinguishable from an ordinary one, and is shown once. That is the correct outcome — it is a live message at that moment — and the replay, which is the actual hazard, is blocked. Publishers should still not retain notifications.
When the broker goes away
The service stays up and paho retries with a backoff from 2 to 60 seconds. The disconnection is logged once, not once per attempt, so a broker down for a morning does not fill the journal. On reconnect the subscriptions are re-established and the retained status is republished.
Verified against a real broker: the daemon survives the broker being killed, reconnects when it returns, and delivers the next message normally.
Configuration
In /etc/ekklesia-classroom/device.conf:
MQTT_ENABLED=false
MQTT_HOST=
MQTT_PORT=1883
MQTT_USERNAME=
MQTT_PASSWORD=
With MQTT_ENABLED=false, or no host, the service logs one line and
exits. That is the shipped default: every existing classroom upgrades
into this release with messaging off until a broker exists.
Because the broker password lives here, device.conf is now 0640 root:classroom rather than world readable. Everything that reads it —
the dock, the notification daemon, the DVD commands, the updater — runs
as root or as the classroom user.
Trying it
mosquitto_pub -h <broker> -t classroom/notify/nursery \
-m '{"id":"t1","title":"Sermon Starting","message":"The sermon has started."}'
mosquitto_sub -h <broker> -t 'classroom/status/#' -v
journalctl -u ekklesia-classroom-mqtt -f
What this is for
Parent Call and Sanctuary Live View are next, and both need a room to be
reachable. The transport is deliberately separate from what it carries:
classroom_mqtt holds room identity, the topic contract and the rules
for an acceptable message, with no broker library in it at all, so those
decisions can be tested without one running.