readerboard 0.1.2__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {readerboard-0.1.2/readerboard.egg-info → readerboard-0.1.3}/PKG-INFO +72 -5
- {readerboard-0.1.2 → readerboard-0.1.3}/README.md +70 -3
- {readerboard-0.1.2 → readerboard-0.1.3}/pyproject.toml +11 -2
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/__init__.py +1 -1
- {readerboard-0.1.2 → readerboard-0.1.3/readerboard.egg-info}/PKG-INFO +72 -5
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard.egg-info/requires.txt +1 -1
- {readerboard-0.1.2 → readerboard-0.1.3}/tests/test_api.py +10 -3
- {readerboard-0.1.2 → readerboard-0.1.3}/tests/test_markup.py +2 -2
- {readerboard-0.1.2 → readerboard-0.1.3}/LICENSE +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/__main__.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/api/__init__.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/api/app.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/api/deps.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/api/models.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/api/routes_simple.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/api/routes_v2.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/config.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/logging_setup.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/protocol/__init__.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/protocol/constants.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/protocol/frames.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/protocol/markup.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/protocol/tokens.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/py.typed +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/services/__init__.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/services/alerts.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/services/clock.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/services/commands.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/services/registry.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/sign/__init__.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/sign/controller.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/sign/layout.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/sign/state.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/transport/__init__.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/transport/base.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/transport/fake.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard/transport/serial_link.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard.egg-info/SOURCES.txt +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard.egg-info/dependency_links.txt +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard.egg-info/entry_points.txt +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/readerboard.egg-info/top_level.txt +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/setup.cfg +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/tests/test_alerts.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/tests/test_clock.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/tests/test_constant_values.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/tests/test_controller.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/tests/test_frames.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/tests/test_registry.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/tests/test_state.py +0 -0
- {readerboard-0.1.2 → readerboard-0.1.3}/tests/test_transport.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: readerboard
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: An HTTP service for BetaBrite and Alpha protocol LED signs: several sources share one sign, with alerts, scheduling and clock sync
|
|
5
5
|
Author: mjaksn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -27,7 +27,7 @@ Requires-Python: >=3.11
|
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
License-File: LICENSE
|
|
29
29
|
Requires-Dist: fastapi<0.142,>=0.141.1
|
|
30
|
-
Requires-Dist: uvicorn
|
|
30
|
+
Requires-Dist: uvicorn<0.53,>=0.52.1
|
|
31
31
|
Requires-Dist: pydantic<3,>=2.13.4
|
|
32
32
|
Requires-Dist: pydantic-settings<3,>=2.15.0
|
|
33
33
|
Requires-Dist: pyserial<4,>=3.5
|
|
@@ -45,6 +45,8 @@ Dynamic: license-file
|
|
|
45
45
|
[](https://github.com/mjaksn/readerboard/actions/workflows/ci.yml)
|
|
46
46
|
[](https://github.com/mjaksn/readerboard/actions/workflows/release.yml)
|
|
47
47
|
[](https://pypi.org/project/readerboard/)
|
|
48
|
+
[](https://github.com/mjaksn/readerboard/pkgs/container/readerboard)
|
|
49
|
+
[](https://hub.docker.com/r/mjaksn/readerboard)
|
|
48
50
|
[](https://github.com/mjaksn/readerboard/blob/main/LICENSE)
|
|
49
51
|
|
|
50
52
|
An HTTP service that drives a BetaBrite Classic sign, either through a serial cable or
|
|
@@ -78,13 +80,14 @@ until it is released, after which the rotation resumes.
|
|
|
78
80
|
- Python 3.11 or newer.
|
|
79
81
|
- A BetaBrite Classic, reachable either at a serial device such as `/dev/ttyUSB0` or over
|
|
80
82
|
the network through an Ethernet to RS-232 adapter at `socket://host:port`.
|
|
81
|
-
-
|
|
82
|
-
does; only
|
|
83
|
+
- Either a machine running systemd, for `scripts/install.sh`, or a container runtime, for
|
|
84
|
+
the published image. The service itself runs anywhere Python does; only the installer
|
|
85
|
+
is Linux specific.
|
|
83
86
|
|
|
84
87
|
## Try it without a sign
|
|
85
88
|
|
|
86
89
|
`loop://` is pyserial's loopback, so the service will start and serve its API with
|
|
87
|
-
nothing attached.
|
|
90
|
+
nothing attached. From a checkout:
|
|
88
91
|
|
|
89
92
|
```
|
|
90
93
|
pip install -e ".[dev]"
|
|
@@ -93,10 +96,22 @@ READERBOARD_SERIAL_URL=loop:// READERBOARD_API_KEY=dev-key \
|
|
|
93
96
|
python -m readerboard
|
|
94
97
|
```
|
|
95
98
|
|
|
99
|
+
Or without one:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
docker run --rm -p 5001:5001 \
|
|
103
|
+
-e READERBOARD_SERIAL_URL=loop:// -e READERBOARD_API_KEY=dev-key \
|
|
104
|
+
ghcr.io/mjaksn/readerboard:latest
|
|
105
|
+
```
|
|
106
|
+
|
|
96
107
|
Then open <http://127.0.0.1:5001/docs>.
|
|
97
108
|
|
|
98
109
|
## Installing it properly
|
|
99
110
|
|
|
111
|
+
Two ways, which do the same job. Pick whichever suits the machine.
|
|
112
|
+
|
|
113
|
+
### With systemd
|
|
114
|
+
|
|
100
115
|
```
|
|
101
116
|
sudo scripts/install.sh --serial-url socket://192.168.2.51:4001
|
|
102
117
|
```
|
|
@@ -110,6 +125,40 @@ again after pulling a new version: your config file and key are left alone.
|
|
|
110
125
|
your registered messages, so reinstalling puts the sign back as it was. Add `--purge` to
|
|
111
126
|
remove those too.
|
|
112
127
|
|
|
128
|
+
### With Docker
|
|
129
|
+
|
|
130
|
+
The image is published to both registries on every release, for `linux/amd64`,
|
|
131
|
+
`linux/arm64` and `linux/arm/v7`, so a Pi pulls the same tag an x86 server does.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
docker run -d --name readerboard --restart unless-stopped -p 5001:5001 \
|
|
135
|
+
-e READERBOARD_SERIAL_URL=socket://192.168.2.51:4001 \
|
|
136
|
+
-e READERBOARD_API_KEY=YOUR-KEY \
|
|
137
|
+
-v readerboard-state:/var/lib/readerboard \
|
|
138
|
+
ghcr.io/mjaksn/readerboard:latest
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`packaging/docker-compose.yml` is the same thing as a Compose file, with the settings
|
|
142
|
+
worth knowing about written out beside it.
|
|
143
|
+
|
|
144
|
+
The volume is what matters here. The registered messages are persisted to
|
|
145
|
+
`/var/lib/readerboard`, and without it the sign comes back empty after a restart rather
|
|
146
|
+
than putting back what was on it.
|
|
147
|
+
|
|
148
|
+
Every setting is available as an environment variable, so no config file is needed. Mount
|
|
149
|
+
one at `/etc/readerboard/config.toml` if you would rather have it, in the format
|
|
150
|
+
`packaging/config.example.toml` documents; the environment still wins over the file.
|
|
151
|
+
|
|
152
|
+
For a sign on a cable rather than on the network, the container needs the device passed
|
|
153
|
+
in and needs to be in the group that owns it. The group has to be given as a number,
|
|
154
|
+
because the container has no `/etc/group` entry for the host's `dialout`:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
stat -c '%G %g' /dev/ttyUSB0 # 20 on Debian and Raspberry Pi OS, 18 on Fedora
|
|
158
|
+
docker run ... --device /dev/ttyUSB0 --group-add 20 \
|
|
159
|
+
-e READERBOARD_SERIAL_URL=/dev/ttyUSB0 ...
|
|
160
|
+
```
|
|
161
|
+
|
|
113
162
|
## Using it
|
|
114
163
|
|
|
115
164
|
Every write needs an `X-API-Key` header. `GET /health` does not.
|
|
@@ -177,6 +226,11 @@ registered, and it shares the sign the moment anything else registers.
|
|
|
177
226
|
Settings come from `/etc/readerboard/config.toml`, overridden by environment variables
|
|
178
227
|
prefixed `READERBOARD_`. `packaging/config.example.toml` documents every one of them.
|
|
179
228
|
|
|
229
|
+
Every setting has both forms, and the container path relies on it: `slot_count` in the
|
|
230
|
+
file is `READERBOARD_SLOT_COUNT` in the environment. Under Docker the file is optional
|
|
231
|
+
and usually absent, which is not an error. `READERBOARD_CONFIG_FILE` moves the file if
|
|
232
|
+
you want it somewhere other than the default.
|
|
233
|
+
|
|
180
234
|
The sign's address is a full pyserial URL in `serial_url`: `socket://192.168.2.51:4001`
|
|
181
235
|
for an Ethernet to RS-232 adapter, `/dev/ttyUSB0` for a cable plugged straight in, or
|
|
182
236
|
`loop://` to run the service with no sign attached.
|
|
@@ -205,6 +259,19 @@ Sensible precautions remain sensible:
|
|
|
205
259
|
- The service runs as a dedicated system user under a hardened systemd unit, which is
|
|
206
260
|
worth keeping rather than running it as root for convenience.
|
|
207
261
|
|
|
262
|
+
Under Docker the same points apply, with different mechanisms:
|
|
263
|
+
|
|
264
|
+
- The image runs as an unprivileged user, UID and GID 10001, not as root. A bind-mounted
|
|
265
|
+
state directory has to be owned by that number on the host.
|
|
266
|
+
- An API key passed as an environment variable is visible to anyone who can run
|
|
267
|
+
`docker inspect` on the container, and to anything that reads the Compose file's
|
|
268
|
+
environment. Mounting a config file mode 0640 keeps it out of both.
|
|
269
|
+
- Bind the published port to the loopback address, `-p 127.0.0.1:5001:5001`, unless
|
|
270
|
+
clients on other machines need to reach it.
|
|
271
|
+
- Passing a serial device in with `--device` gives the container that device and nothing
|
|
272
|
+
else. It does not need `--privileged`, and giving it that would hand it every device on
|
|
273
|
+
the host.
|
|
274
|
+
|
|
208
275
|
## Development
|
|
209
276
|
|
|
210
277
|
```
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
[](https://github.com/mjaksn/readerboard/actions/workflows/ci.yml)
|
|
4
4
|
[](https://github.com/mjaksn/readerboard/actions/workflows/release.yml)
|
|
5
5
|
[](https://pypi.org/project/readerboard/)
|
|
6
|
+
[](https://github.com/mjaksn/readerboard/pkgs/container/readerboard)
|
|
7
|
+
[](https://hub.docker.com/r/mjaksn/readerboard)
|
|
6
8
|
[](https://github.com/mjaksn/readerboard/blob/main/LICENSE)
|
|
7
9
|
|
|
8
10
|
An HTTP service that drives a BetaBrite Classic sign, either through a serial cable or
|
|
@@ -36,13 +38,14 @@ until it is released, after which the rotation resumes.
|
|
|
36
38
|
- Python 3.11 or newer.
|
|
37
39
|
- A BetaBrite Classic, reachable either at a serial device such as `/dev/ttyUSB0` or over
|
|
38
40
|
the network through an Ethernet to RS-232 adapter at `socket://host:port`.
|
|
39
|
-
-
|
|
40
|
-
does; only
|
|
41
|
+
- Either a machine running systemd, for `scripts/install.sh`, or a container runtime, for
|
|
42
|
+
the published image. The service itself runs anywhere Python does; only the installer
|
|
43
|
+
is Linux specific.
|
|
41
44
|
|
|
42
45
|
## Try it without a sign
|
|
43
46
|
|
|
44
47
|
`loop://` is pyserial's loopback, so the service will start and serve its API with
|
|
45
|
-
nothing attached.
|
|
48
|
+
nothing attached. From a checkout:
|
|
46
49
|
|
|
47
50
|
```
|
|
48
51
|
pip install -e ".[dev]"
|
|
@@ -51,10 +54,22 @@ READERBOARD_SERIAL_URL=loop:// READERBOARD_API_KEY=dev-key \
|
|
|
51
54
|
python -m readerboard
|
|
52
55
|
```
|
|
53
56
|
|
|
57
|
+
Or without one:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
docker run --rm -p 5001:5001 \
|
|
61
|
+
-e READERBOARD_SERIAL_URL=loop:// -e READERBOARD_API_KEY=dev-key \
|
|
62
|
+
ghcr.io/mjaksn/readerboard:latest
|
|
63
|
+
```
|
|
64
|
+
|
|
54
65
|
Then open <http://127.0.0.1:5001/docs>.
|
|
55
66
|
|
|
56
67
|
## Installing it properly
|
|
57
68
|
|
|
69
|
+
Two ways, which do the same job. Pick whichever suits the machine.
|
|
70
|
+
|
|
71
|
+
### With systemd
|
|
72
|
+
|
|
58
73
|
```
|
|
59
74
|
sudo scripts/install.sh --serial-url socket://192.168.2.51:4001
|
|
60
75
|
```
|
|
@@ -68,6 +83,40 @@ again after pulling a new version: your config file and key are left alone.
|
|
|
68
83
|
your registered messages, so reinstalling puts the sign back as it was. Add `--purge` to
|
|
69
84
|
remove those too.
|
|
70
85
|
|
|
86
|
+
### With Docker
|
|
87
|
+
|
|
88
|
+
The image is published to both registries on every release, for `linux/amd64`,
|
|
89
|
+
`linux/arm64` and `linux/arm/v7`, so a Pi pulls the same tag an x86 server does.
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
docker run -d --name readerboard --restart unless-stopped -p 5001:5001 \
|
|
93
|
+
-e READERBOARD_SERIAL_URL=socket://192.168.2.51:4001 \
|
|
94
|
+
-e READERBOARD_API_KEY=YOUR-KEY \
|
|
95
|
+
-v readerboard-state:/var/lib/readerboard \
|
|
96
|
+
ghcr.io/mjaksn/readerboard:latest
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`packaging/docker-compose.yml` is the same thing as a Compose file, with the settings
|
|
100
|
+
worth knowing about written out beside it.
|
|
101
|
+
|
|
102
|
+
The volume is what matters here. The registered messages are persisted to
|
|
103
|
+
`/var/lib/readerboard`, and without it the sign comes back empty after a restart rather
|
|
104
|
+
than putting back what was on it.
|
|
105
|
+
|
|
106
|
+
Every setting is available as an environment variable, so no config file is needed. Mount
|
|
107
|
+
one at `/etc/readerboard/config.toml` if you would rather have it, in the format
|
|
108
|
+
`packaging/config.example.toml` documents; the environment still wins over the file.
|
|
109
|
+
|
|
110
|
+
For a sign on a cable rather than on the network, the container needs the device passed
|
|
111
|
+
in and needs to be in the group that owns it. The group has to be given as a number,
|
|
112
|
+
because the container has no `/etc/group` entry for the host's `dialout`:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
stat -c '%G %g' /dev/ttyUSB0 # 20 on Debian and Raspberry Pi OS, 18 on Fedora
|
|
116
|
+
docker run ... --device /dev/ttyUSB0 --group-add 20 \
|
|
117
|
+
-e READERBOARD_SERIAL_URL=/dev/ttyUSB0 ...
|
|
118
|
+
```
|
|
119
|
+
|
|
71
120
|
## Using it
|
|
72
121
|
|
|
73
122
|
Every write needs an `X-API-Key` header. `GET /health` does not.
|
|
@@ -135,6 +184,11 @@ registered, and it shares the sign the moment anything else registers.
|
|
|
135
184
|
Settings come from `/etc/readerboard/config.toml`, overridden by environment variables
|
|
136
185
|
prefixed `READERBOARD_`. `packaging/config.example.toml` documents every one of them.
|
|
137
186
|
|
|
187
|
+
Every setting has both forms, and the container path relies on it: `slot_count` in the
|
|
188
|
+
file is `READERBOARD_SLOT_COUNT` in the environment. Under Docker the file is optional
|
|
189
|
+
and usually absent, which is not an error. `READERBOARD_CONFIG_FILE` moves the file if
|
|
190
|
+
you want it somewhere other than the default.
|
|
191
|
+
|
|
138
192
|
The sign's address is a full pyserial URL in `serial_url`: `socket://192.168.2.51:4001`
|
|
139
193
|
for an Ethernet to RS-232 adapter, `/dev/ttyUSB0` for a cable plugged straight in, or
|
|
140
194
|
`loop://` to run the service with no sign attached.
|
|
@@ -163,6 +217,19 @@ Sensible precautions remain sensible:
|
|
|
163
217
|
- The service runs as a dedicated system user under a hardened systemd unit, which is
|
|
164
218
|
worth keeping rather than running it as root for convenience.
|
|
165
219
|
|
|
220
|
+
Under Docker the same points apply, with different mechanisms:
|
|
221
|
+
|
|
222
|
+
- The image runs as an unprivileged user, UID and GID 10001, not as root. A bind-mounted
|
|
223
|
+
state directory has to be owned by that number on the host.
|
|
224
|
+
- An API key passed as an environment variable is visible to anyone who can run
|
|
225
|
+
`docker inspect` on the container, and to anything that reads the Compose file's
|
|
226
|
+
environment. Mounting a config file mode 0640 keeps it out of both.
|
|
227
|
+
- Bind the published port to the loopback address, `-p 127.0.0.1:5001:5001`, unless
|
|
228
|
+
clients on other machines need to reach it.
|
|
229
|
+
- Passing a serial device in with `--device` gives the container that device and nothing
|
|
230
|
+
else. It does not need `--privileged`, and giving it that would hand it every device on
|
|
231
|
+
the host.
|
|
232
|
+
|
|
166
233
|
## Development
|
|
167
234
|
|
|
168
235
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "readerboard"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "An HTTP service for BetaBrite and Alpha protocol LED signs: several sources share one sign, with alerts, scheduling and clock sync"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -39,7 +39,16 @@ classifiers = [
|
|
|
39
39
|
# is allowed to live.
|
|
40
40
|
dependencies = [
|
|
41
41
|
"fastapi>=0.141.1,<0.142",
|
|
42
|
-
"
|
|
42
|
+
# Plain uvicorn, not the "standard" extra. That extra exists to make a busy
|
|
43
|
+
# server faster, and nothing here is busy: this one answers a handful of
|
|
44
|
+
# requests and then waits on a 9600 baud serial line behind a deliberate
|
|
45
|
+
# inter-packet delay, so a faster event loop and HTTP parser buy nothing
|
|
46
|
+
# measurable. What they cost is real, though. uvloop, httptools and PyYAML
|
|
47
|
+
# publish no 32-bit arm wheel, and building all three from source under
|
|
48
|
+
# emulation was eight minutes of the container image's build. The extra also
|
|
49
|
+
# brought in websockets, for a surface this API does not have, and watchfiles,
|
|
50
|
+
# for a --reload this never runs with.
|
|
51
|
+
"uvicorn>=0.52.1,<0.53",
|
|
43
52
|
"pydantic>=2.13.4,<3",
|
|
44
53
|
"pydantic-settings>=2.15.0,<3",
|
|
45
54
|
"pyserial>=3.5,<4",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: readerboard
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: An HTTP service for BetaBrite and Alpha protocol LED signs: several sources share one sign, with alerts, scheduling and clock sync
|
|
5
5
|
Author: mjaksn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -27,7 +27,7 @@ Requires-Python: >=3.11
|
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
License-File: LICENSE
|
|
29
29
|
Requires-Dist: fastapi<0.142,>=0.141.1
|
|
30
|
-
Requires-Dist: uvicorn
|
|
30
|
+
Requires-Dist: uvicorn<0.53,>=0.52.1
|
|
31
31
|
Requires-Dist: pydantic<3,>=2.13.4
|
|
32
32
|
Requires-Dist: pydantic-settings<3,>=2.15.0
|
|
33
33
|
Requires-Dist: pyserial<4,>=3.5
|
|
@@ -45,6 +45,8 @@ Dynamic: license-file
|
|
|
45
45
|
[](https://github.com/mjaksn/readerboard/actions/workflows/ci.yml)
|
|
46
46
|
[](https://github.com/mjaksn/readerboard/actions/workflows/release.yml)
|
|
47
47
|
[](https://pypi.org/project/readerboard/)
|
|
48
|
+
[](https://github.com/mjaksn/readerboard/pkgs/container/readerboard)
|
|
49
|
+
[](https://hub.docker.com/r/mjaksn/readerboard)
|
|
48
50
|
[](https://github.com/mjaksn/readerboard/blob/main/LICENSE)
|
|
49
51
|
|
|
50
52
|
An HTTP service that drives a BetaBrite Classic sign, either through a serial cable or
|
|
@@ -78,13 +80,14 @@ until it is released, after which the rotation resumes.
|
|
|
78
80
|
- Python 3.11 or newer.
|
|
79
81
|
- A BetaBrite Classic, reachable either at a serial device such as `/dev/ttyUSB0` or over
|
|
80
82
|
the network through an Ethernet to RS-232 adapter at `socket://host:port`.
|
|
81
|
-
-
|
|
82
|
-
does; only
|
|
83
|
+
- Either a machine running systemd, for `scripts/install.sh`, or a container runtime, for
|
|
84
|
+
the published image. The service itself runs anywhere Python does; only the installer
|
|
85
|
+
is Linux specific.
|
|
83
86
|
|
|
84
87
|
## Try it without a sign
|
|
85
88
|
|
|
86
89
|
`loop://` is pyserial's loopback, so the service will start and serve its API with
|
|
87
|
-
nothing attached.
|
|
90
|
+
nothing attached. From a checkout:
|
|
88
91
|
|
|
89
92
|
```
|
|
90
93
|
pip install -e ".[dev]"
|
|
@@ -93,10 +96,22 @@ READERBOARD_SERIAL_URL=loop:// READERBOARD_API_KEY=dev-key \
|
|
|
93
96
|
python -m readerboard
|
|
94
97
|
```
|
|
95
98
|
|
|
99
|
+
Or without one:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
docker run --rm -p 5001:5001 \
|
|
103
|
+
-e READERBOARD_SERIAL_URL=loop:// -e READERBOARD_API_KEY=dev-key \
|
|
104
|
+
ghcr.io/mjaksn/readerboard:latest
|
|
105
|
+
```
|
|
106
|
+
|
|
96
107
|
Then open <http://127.0.0.1:5001/docs>.
|
|
97
108
|
|
|
98
109
|
## Installing it properly
|
|
99
110
|
|
|
111
|
+
Two ways, which do the same job. Pick whichever suits the machine.
|
|
112
|
+
|
|
113
|
+
### With systemd
|
|
114
|
+
|
|
100
115
|
```
|
|
101
116
|
sudo scripts/install.sh --serial-url socket://192.168.2.51:4001
|
|
102
117
|
```
|
|
@@ -110,6 +125,40 @@ again after pulling a new version: your config file and key are left alone.
|
|
|
110
125
|
your registered messages, so reinstalling puts the sign back as it was. Add `--purge` to
|
|
111
126
|
remove those too.
|
|
112
127
|
|
|
128
|
+
### With Docker
|
|
129
|
+
|
|
130
|
+
The image is published to both registries on every release, for `linux/amd64`,
|
|
131
|
+
`linux/arm64` and `linux/arm/v7`, so a Pi pulls the same tag an x86 server does.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
docker run -d --name readerboard --restart unless-stopped -p 5001:5001 \
|
|
135
|
+
-e READERBOARD_SERIAL_URL=socket://192.168.2.51:4001 \
|
|
136
|
+
-e READERBOARD_API_KEY=YOUR-KEY \
|
|
137
|
+
-v readerboard-state:/var/lib/readerboard \
|
|
138
|
+
ghcr.io/mjaksn/readerboard:latest
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`packaging/docker-compose.yml` is the same thing as a Compose file, with the settings
|
|
142
|
+
worth knowing about written out beside it.
|
|
143
|
+
|
|
144
|
+
The volume is what matters here. The registered messages are persisted to
|
|
145
|
+
`/var/lib/readerboard`, and without it the sign comes back empty after a restart rather
|
|
146
|
+
than putting back what was on it.
|
|
147
|
+
|
|
148
|
+
Every setting is available as an environment variable, so no config file is needed. Mount
|
|
149
|
+
one at `/etc/readerboard/config.toml` if you would rather have it, in the format
|
|
150
|
+
`packaging/config.example.toml` documents; the environment still wins over the file.
|
|
151
|
+
|
|
152
|
+
For a sign on a cable rather than on the network, the container needs the device passed
|
|
153
|
+
in and needs to be in the group that owns it. The group has to be given as a number,
|
|
154
|
+
because the container has no `/etc/group` entry for the host's `dialout`:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
stat -c '%G %g' /dev/ttyUSB0 # 20 on Debian and Raspberry Pi OS, 18 on Fedora
|
|
158
|
+
docker run ... --device /dev/ttyUSB0 --group-add 20 \
|
|
159
|
+
-e READERBOARD_SERIAL_URL=/dev/ttyUSB0 ...
|
|
160
|
+
```
|
|
161
|
+
|
|
113
162
|
## Using it
|
|
114
163
|
|
|
115
164
|
Every write needs an `X-API-Key` header. `GET /health` does not.
|
|
@@ -177,6 +226,11 @@ registered, and it shares the sign the moment anything else registers.
|
|
|
177
226
|
Settings come from `/etc/readerboard/config.toml`, overridden by environment variables
|
|
178
227
|
prefixed `READERBOARD_`. `packaging/config.example.toml` documents every one of them.
|
|
179
228
|
|
|
229
|
+
Every setting has both forms, and the container path relies on it: `slot_count` in the
|
|
230
|
+
file is `READERBOARD_SLOT_COUNT` in the environment. Under Docker the file is optional
|
|
231
|
+
and usually absent, which is not an error. `READERBOARD_CONFIG_FILE` moves the file if
|
|
232
|
+
you want it somewhere other than the default.
|
|
233
|
+
|
|
180
234
|
The sign's address is a full pyserial URL in `serial_url`: `socket://192.168.2.51:4001`
|
|
181
235
|
for an Ethernet to RS-232 adapter, `/dev/ttyUSB0` for a cable plugged straight in, or
|
|
182
236
|
`loop://` to run the service with no sign attached.
|
|
@@ -205,6 +259,19 @@ Sensible precautions remain sensible:
|
|
|
205
259
|
- The service runs as a dedicated system user under a hardened systemd unit, which is
|
|
206
260
|
worth keeping rather than running it as root for convenience.
|
|
207
261
|
|
|
262
|
+
Under Docker the same points apply, with different mechanisms:
|
|
263
|
+
|
|
264
|
+
- The image runs as an unprivileged user, UID and GID 10001, not as root. A bind-mounted
|
|
265
|
+
state directory has to be owned by that number on the host.
|
|
266
|
+
- An API key passed as an environment variable is visible to anyone who can run
|
|
267
|
+
`docker inspect` on the container, and to anything that reads the Compose file's
|
|
268
|
+
environment. Mounting a config file mode 0640 keeps it out of both.
|
|
269
|
+
- Bind the published port to the loopback address, `-p 127.0.0.1:5001:5001`, unless
|
|
270
|
+
clients on other machines need to reach it.
|
|
271
|
+
- Passing a serial device in with `--device` gives the container that device and nothing
|
|
272
|
+
else. It does not need `--privileged`, and giving it that would hand it every device on
|
|
273
|
+
the host.
|
|
274
|
+
|
|
208
275
|
## Development
|
|
209
276
|
|
|
210
277
|
```
|
|
@@ -260,10 +260,17 @@ class TestUnreachableSign:
|
|
|
260
260
|
|
|
261
261
|
|
|
262
262
|
class TestTheSimpleEndpoints:
|
|
263
|
-
"""The
|
|
263
|
+
"""The payload shapes these endpoints accept, written out in full.
|
|
264
|
+
|
|
265
|
+
They exist for callers that post a fixed body to a fixed path, so the
|
|
266
|
+
bodies below are spelled out rather than built. A change that breaks one
|
|
267
|
+
of them breaks somebody's configuration file, which is exactly what these
|
|
268
|
+
tests are here to notice.
|
|
269
|
+
"""
|
|
264
270
|
|
|
265
271
|
def test_the_home_assistant_rest_command_payload(self, client):
|
|
266
|
-
#
|
|
272
|
+
# A temperature and the time, the common shape: a value from somewhere
|
|
273
|
+
# else, then <time> for the sign to fill in on its own.
|
|
267
274
|
response = client.post(
|
|
268
275
|
"/Write/Message",
|
|
269
276
|
json={
|
|
@@ -280,7 +287,7 @@ class TestTheSimpleEndpoints:
|
|
|
280
287
|
}
|
|
281
288
|
|
|
282
289
|
def test_the_cron_line_payload(self, client):
|
|
283
|
-
#
|
|
290
|
+
# SET_TIME takes the time as HHMM on a 24 hour clock.
|
|
284
291
|
response = client.post(
|
|
285
292
|
"/Write/ControlCommand",
|
|
286
293
|
json={"command": "SET_TIME", "parameter": "2359"},
|
|
@@ -25,8 +25,8 @@ def test_every_token_in_the_table_renders():
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def test_the_home_assistant_payload_renders():
|
|
28
|
-
#
|
|
29
|
-
#
|
|
28
|
+
# A temperature and the time, the common shape: a value from somewhere
|
|
29
|
+
# else, then <time> for the sign to fill in on its own.
|
|
30
30
|
rendered = render("<green>18.4<degree> <red><time>")
|
|
31
31
|
assert rendered == (
|
|
32
32
|
c.TEXT_COLOR_GREEN + b"18.4" + c.XC_DEGREES + b" " + c.TEXT_COLOR_RED + c.CURTIME_INSERT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|