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]>
139 lines
4.8 KiB
Markdown
139 lines
4.8 KiB
Markdown
# 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
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{"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.
|