Files
Classroom_Desktop/docs/messaging.md
T
ekklesiaandClaude Opus 5 dfd42f6fdf feat: give the rooms a line to the church broker
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]>
2026-09-06 14:41:49 -05:00

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_at that 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.