readerboard
An HTTP service that drives a BetaBrite Classic sign, either through a serial cable or through an Ethernet to RS-232 adapter.
Several sources can share the sign at once. Each registers a named slot, and the sign rotates through the registered slots by itself. An alert takes the whole display over until it is released, after which the rotation resumes.
What it does
- Many messages, one sign. Home Assistant can own
temperaturewhile a doorbell automation ownsdoorbell, without either knowing about the other. - The sign does the rotating. Each message lives in its own sign file and the sign cycles them on its own, so rotation costs no serial traffic at all.
- Alerts. Take the display over, optionally with a deadline, then hand it back.
- It keeps the sign's clock right, at startup, hourly, and whenever the link comes back. That last trigger is the one that matters: a sign returning from a power cut does so at no particular minute.
- It does not redraw the sign for nothing. A write of bytes the sign already holds is suppressed, so a source re-sending an unchanged temperature does not make the display flicker.
- It survives restarts and outages. The registered messages are persisted, and a write that arrives while the sign is unreachable is accepted and delivered when the link returns.
- Errors are errors. A dead serial link is a 503, not an HTTP 200 with the word ERROR in the body.
Requirements
- Python 3.11 or newer.
- A BetaBrite Classic, reachable either at a serial device such as
/dev/ttyUSB0or over the network through an Ethernet to RS-232 adapter atsocket://host:port. - For the installer, a machine running systemd. The service itself runs anywhere Python
does; only
scripts/install.shis Linux specific.
Try it without a sign
loop:// is pyserial's loopback, so the service will start and serve its API with
nothing attached.
pip install -e ".[dev]"
READERBOARD_SERIAL_URL=loop:// READERBOARD_API_KEY=dev-key \
READERBOARD_STATE_PATH=./state.json \
python -m readerboard
Then open http://127.0.0.1:5001/docs.
Installing it properly
sudo scripts/install.sh --serial-url socket://192.168.2.51:4001
This creates a readerboard system user, builds a virtual environment in
/opt/readerboard, writes /etc/readerboard/config.toml with a freshly generated API key,
and enables the readerboard service. It prints the key once, and it is safe to run
again after pulling a new version: your config file and key are left alone.
sudo scripts/uninstall.sh removes the service and the program but keeps your config and
your registered messages, so reinstalling puts the sign back as it was. Add --purge to
remove those too.
Using it
Every write needs an X-API-Key header. GET /health does not.
Register a message:
curl -X PUT http://localhost:5001/v2/messages/temperature \
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
-d '{"message": "<green>18.4<degree> <red><time>", "display_mode": "HOLD"}'
Register a second one and the sign rotates between them:
curl -X PUT http://localhost:5001/v2/messages/doorbell \
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
-d '{"message": "<amber>Someone at the door", "ttl_seconds": 300}'
Take the sign over for thirty seconds:
curl -X POST http://localhost:5001/v2/alerts \
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
-d '{"message": "<red><flash_on>SMOKE ALARM", "ttl_seconds": 30}'
The full API, including every markup token and display mode, is at /docs.
Writing messages
A message is plain text plus tokens written as <name>: <green>18.4<degree> is a
colour change, a number, and a degree symbol. GET /v2/enumerations/markup-tokens lists
them all.
Text is encoded against the sign's own character table rather than as UTF-8, so café
displays correctly. A character the sign cannot render is rejected with a 400 on /v2,
and replaced with ? on the simpler endpoints described below.
A simpler set of endpoints
Alongside /v2 there is a smaller surface: POST /Write/Message,
POST /Write/ControlCommand, and the /Enumerations reads.
These follow one convention that /v2 does not. Every response is HTTP 200, with the
outcome in the body:
{"result": "OK", "result_message": "Message displayed on sign"}
That suits a client which finds branching on status codes awkward, such as a Home
Assistant rest_command or a shell one-liner in a cron job. The single exception is a
missing or wrong API key, which is a 401: a caller the service will not talk to is not the
same as a request that failed.
POST /Write/Message writes to one reserved slot, named default. It deliberately does
not touch the sign's priority file, which by protocol suppresses every other message on
the sign. Written to an ordinary slot it looks identical while it is the only message
registered, and it shares the sign the moment anything else registers.
Configuration
Settings come from /etc/readerboard/config.toml, overridden by environment variables
prefixed READERBOARD_. packaging/config.example.toml documents every one of them.
The sign's address is a full pyserial URL in serial_url: socket://192.168.2.51:4001
for an Ethernet to RS-232 adapter, /dev/ttyUSB0 for a cable plugged straight in, or
loop:// to run the service with no sign attached.
Two settings reallocate the sign's memory when changed, and that erases every message
on it: slot_count and slot_capacity. The service will do it, and say so loudly in
the log, but they are not settings to fiddle with.
Security
An API key is required on every write, compared in constant time, and never logged.
Message content reaches the sign as protocol bytes, so it is worth knowing what a client holding the key can do. The markup renderer emits bytes only for tokens it recognises and for characters in the sign's own table, so arbitrary control sequences cannot be injected through a message. What the holder of a key can do is display anything they like on your wall and set the sign's clock. There is nothing beyond the sign to reach: the service opens one serial link and touches nothing else.
Sensible precautions remain sensible:
- Do not expose the service to the internet.
- Keep
/etc/readerboard/config.tomlmode 0640. Anyone who can read it can write to the sign. - Give the key only to clients you trust, and prefer a firewall allow-list on top.
- The service runs as a dedicated system user under a hardened systemd unit, which is worth keeping rather than running it as root for convenience.
Development
pip install -e ".[dev]"
pytest
ruff check .
mypy readerboard
No sign is needed. The tests run against a capturing fake transport and against
pyserial's loop:// URL, so the real serial code path is exercised without hardware.
docs/protocol-notes.md records what the Alpha protocol actually says about the memory
configuration, the run sequence and the priority file, with the quotations that back each
claim. Read it before changing anything in readerboard/protocol/.
scripts/protocol_spike.py settles the few questions the document cannot answer about
this particular sign. It is destructive and refuses to run without --confirm-erase.
Licence
MIT. See LICENSE.md.
One caveat, recorded because it is easy to miss.
readerboard/protocol/constants.py is vendored from
jonathankoren/readerboard, and that
repository carries no license file. No license is not the same as a permissive one: it
means no copying permission has been granted at all. That module is therefore the one
part of this project whose provenance is not cleanly MIT.
In practice it is a table of byte values dictated by the protocol rather than authored
expression, and docs/protocol-notes.md now cites the protocol document directly, so the
table can be regenerated from the primary source if that ever needs settling properly.
Credits
readerboard/protocol/constants.py came, with thanks, from
jonathankoren/readerboard, with some
corrections noted in the file.
The protocol itself is documented in the Alpha Sign Communications Protocol, form 9708-8061, published by Adaptive Micro Systems.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file readerboard-0.1.2.tar.gz.
File metadata
- Download URL: readerboard-0.1.2.tar.gz
- Upload date:
- Size: 81.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3cead997ab05db6fc8253334b1d014c6c3a448e2b050f0d35fb3bcfc3a6e9041
|
|
| MD5 |
e5c5a1407996888163c87ac58535a2a7
|
|
| BLAKE2b-256 |
26d4939f1f35cdf7cbbfe42b61adb075a277651e08298937b9db82326975b9b4
|
Provenance
The following attestation bundles were made for readerboard-0.1.2.tar.gz:
Publisher:
release.yml on mjaksn/readerboard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
readerboard-0.1.2.tar.gz -
Subject digest:
3cead997ab05db6fc8253334b1d014c6c3a448e2b050f0d35fb3bcfc3a6e9041 - Sigstore transparency entry: 2596968772
- Sigstore integration time:
-
Permalink:
mjaksn/readerboard@58d9260e3b6f7b1f53a4c386523a33625f3b852b -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/mjaksn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@58d9260e3b6f7b1f53a4c386523a33625f3b852b -
Trigger Event:
push
-
Statement type:
File details
Details for the file readerboard-0.1.2-py3-none-any.whl.
File metadata
- Download URL: readerboard-0.1.2-py3-none-any.whl
- Upload date:
- Size: 64.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
03b29a989b14f3df6b6fd38f573fd907e68104e96ac14bc425b1ccc06c2fb4fb
|
|
| MD5 |
a44d93357c7de8df7015289bc11da2e0
|
|
| BLAKE2b-256 |
87dea0fc5e064a5f6badbb2ee08a8a51337764602cb621e68547abc5cdcf1a88
|
Provenance
The following attestation bundles were made for readerboard-0.1.2-py3-none-any.whl:
Publisher:
release.yml on mjaksn/readerboard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
readerboard-0.1.2-py3-none-any.whl -
Subject digest:
03b29a989b14f3df6b6fd38f573fd907e68104e96ac14bc425b1ccc06c2fb4fb - Sigstore transparency entry: 2596968797
- Sigstore integration time:
-
Permalink:
mjaksn/readerboard@58d9260e3b6f7b1f53a4c386523a33625f3b852b -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/mjaksn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@58d9260e3b6f7b1f53a4c386523a33625f3b852b -
Trigger Event:
push
-
Statement type: