readerboard 0.6.0__tar.gz → 0.7.1__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.6.0/readerboard.egg-info → readerboard-0.7.1}/PKG-INFO +99 -12
- {readerboard-0.6.0 → readerboard-0.7.1}/README.md +96 -9
- {readerboard-0.6.0 → readerboard-0.7.1}/pyproject.toml +17 -3
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/__init__.py +1 -1
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/api/app.py +79 -15
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/api/errors.py +24 -1
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/api/models.py +47 -2
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/api/routes.py +65 -6
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/config.py +58 -21
- readerboard-0.7.1/readerboard/icons.py +520 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/protocol/constants.py +76 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/protocol/frames.py +224 -6
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/protocol/markup.py +116 -10
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/protocol/replies.py +82 -4
- readerboard-0.7.1/readerboard/services/alerts.py +647 -0
- readerboard-0.7.1/readerboard/services/registry.py +1834 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/sign/controller.py +31 -2
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/sign/layout.py +73 -14
- readerboard-0.7.1/readerboard/sign/pool.py +165 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/sign/state.py +83 -9
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/transport/serial_link.py +63 -0
- {readerboard-0.6.0 → readerboard-0.7.1/readerboard.egg-info}/PKG-INFO +99 -12
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard.egg-info/SOURCES.txt +5 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard.egg-info/requires.txt +2 -2
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_alerts.py +52 -1
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_api.py +263 -10
- readerboard-0.7.1/tests/test_config.py +107 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_constant_values.py +85 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_frames.py +163 -3
- readerboard-0.7.1/tests/test_icons.py +141 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_markup.py +86 -0
- readerboard-0.7.1/tests/test_pictures.py +1671 -0
- readerboard-0.7.1/tests/test_pool.py +192 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_registry.py +141 -4
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_run_against_a_sign.py +102 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_run_with_simulator.py +47 -3
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_state.py +13 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_transport.py +101 -1
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_variables.py +9 -4
- readerboard-0.6.0/readerboard/services/alerts.py +0 -247
- readerboard-0.6.0/readerboard/services/registry.py +0 -952
- readerboard-0.6.0/tests/test_config.py +0 -47
- {readerboard-0.6.0 → readerboard-0.7.1}/LICENSE +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/__main__.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/api/__init__.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/api/deps.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/logging_setup.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/names.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/protocol/__init__.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/protocol/tokens.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/py.typed +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/services/__init__.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/services/clock.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/services/commands.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/sign/__init__.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/transport/__init__.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/transport/base.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard/transport/fake.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard.egg-info/dependency_links.txt +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard.egg-info/entry_points.txt +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/readerboard.egg-info/top_level.txt +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/setup.cfg +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_clock.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_component_names.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_controller.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_launch_configurations.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_logging_setup.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_open_docs.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_replies.py +0 -0
- {readerboard-0.6.0 → readerboard-0.7.1}/tests/test_tool_icons.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: readerboard
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: An HTTP service for BetaBrite and Alpha protocol LED signs: several sources share one sign, with alerts, expiring messages and clock sync
|
|
5
5
|
Author: mjaksn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -28,13 +28,13 @@ Requires-Python: >=3.11
|
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
License-File: LICENSE
|
|
30
30
|
Requires-Dist: fastapi<0.142,>=0.141.1
|
|
31
|
-
Requires-Dist: uvicorn<0.
|
|
31
|
+
Requires-Dist: uvicorn<0.54,>=0.52.1
|
|
32
32
|
Requires-Dist: pydantic<3,>=2.13.4
|
|
33
33
|
Requires-Dist: pydantic-settings<3,>=2.15.0
|
|
34
34
|
Requires-Dist: pyserial<4,>=3.5
|
|
35
35
|
Requires-Dist: tzdata>=2026.3; sys_platform == "win32"
|
|
36
36
|
Provides-Extra: dev
|
|
37
|
-
Requires-Dist: pytest<
|
|
37
|
+
Requires-Dist: pytest<9.2,>=9.1.1; extra == "dev"
|
|
38
38
|
Requires-Dist: pytest-asyncio<2,>=1.4.0; extra == "dev"
|
|
39
39
|
Requires-Dist: httpx2<3,>=2.10.0; extra == "dev"
|
|
40
40
|
Requires-Dist: anyio<4.15,>=4.14.2; extra == "dev"
|
|
@@ -73,6 +73,8 @@ the rotation resumes.
|
|
|
73
73
|
time it draws the message, with no blank and no restart. One variable can appear in
|
|
74
74
|
any number of messages, and a value that stops arriving can be made to go stale.
|
|
75
75
|
- **Alerts.** Take the display over, optionally with a deadline, then hand it back.
|
|
76
|
+
A caller that must not overwrite somebody else's alert can ask to be refused
|
|
77
|
+
instead.
|
|
76
78
|
- **It keeps the sign's clock right**, at startup, hourly, and whenever the link comes
|
|
77
79
|
back. That last trigger is the one that matters: a sign returning from a power cut
|
|
78
80
|
does so at no particular minute. The sign is set one minute fast on purpose: the
|
|
@@ -149,8 +151,8 @@ python scripts/run_against_a_sign.py --serial-url socket://192.168.2.51:4001
|
|
|
149
151
|
|
|
150
152
|
That starts the service and the client together, with no simulator. The service
|
|
151
153
|
comes up on <http://127.0.0.1:5001> with `/docs` beside it, the client comes up
|
|
152
|
-
pointed at that address
|
|
153
|
-
the same window. `--no-client` leaves the client out. Ctrl+C stops everything,
|
|
154
|
+
pointed at that address with the API key already in its box, and the key is
|
|
155
|
+
printed in the same window for anything else that needs it. `--no-client` leaves the client out. Ctrl+C stops everything,
|
|
154
156
|
and closing the client leaves the service running.
|
|
155
157
|
|
|
156
158
|
Both editors carry it as a launch configuration named "readerboard against the
|
|
@@ -182,6 +184,22 @@ sign shows nothing or shows rubbish, and the adapter answers on port 23, it is
|
|
|
182
184
|
probably negotiating telnet rather than passing bytes through, and `rfc2217://`
|
|
183
185
|
is the form that speaks that.
|
|
184
186
|
|
|
187
|
+
An adapter that drops off the network says nothing as it goes. The service asks
|
|
188
|
+
the operating system to give a network link up about a minute after the adapter
|
|
189
|
+
stops answering: on Linux, a minute after the first write it did not
|
|
190
|
+
acknowledge, or, if nothing was written, about a minute after it last answered,
|
|
191
|
+
found by keepalive probes. A platform that does not take one of these options
|
|
192
|
+
keeps its own default for it, which can be much longer. The service itself only
|
|
193
|
+
finds out at its first read or write after the link has gone, and `GET /health`
|
|
194
|
+
shows `"connected": false` from then on. A sign with a variable updated every
|
|
195
|
+
minute is written to that often. One left alone is written to at the refresh,
|
|
196
|
+
every `refresh_interval_seconds`, which is 15 minutes by default, and a refresh
|
|
197
|
+
that lands before the link has gone is buffered like any other write, so on a
|
|
198
|
+
quiet sign `link.connected` can stay true for a refresh interval and a minute
|
|
199
|
+
after the adapter has gone. It is still worth a monitor watching, and needs no
|
|
200
|
+
key, but a ping of the adapter answers within a minute however quiet the sign
|
|
201
|
+
is, and is the better alarm.
|
|
202
|
+
|
|
185
203
|
### The API key, and config.local.toml
|
|
186
204
|
|
|
187
205
|
The key is not an argument. A launch configuration is a tracked file and a
|
|
@@ -223,6 +241,19 @@ again after pulling a new version: your config file and key are left alone.
|
|
|
223
241
|
your registered messages, so reinstalling puts the sign back as it was. Add `--purge` to
|
|
224
242
|
remove those too.
|
|
225
243
|
|
|
244
|
+
The unit restarts the service whenever it stops, and gives up after ten failed starts in
|
|
245
|
+
five minutes. Almost nothing here fails permanently, which is what makes the exceptions
|
|
246
|
+
worth stopping for: a memory pool too big for the sign fails identically every time, and
|
|
247
|
+
each attempt puts a read on the wire that stalls a scrolling message. `systemctl status
|
|
248
|
+
readerboard` says which failure it was. Once the configuration is fixed, clearing the
|
|
249
|
+
give-up and starting it again are two commands, because `reset-failed` clears the counter
|
|
250
|
+
and leaves the unit stopped:
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
sudo systemctl reset-failed readerboard
|
|
254
|
+
sudo systemctl start readerboard
|
|
255
|
+
```
|
|
256
|
+
|
|
226
257
|
### With Docker
|
|
227
258
|
|
|
228
259
|
The image is published to both registries on every release, for `linux/amd64`,
|
|
@@ -433,6 +464,40 @@ actions:
|
|
|
433
464
|
value: "{{ states('sensor.outside_temperature') | round(0) | int }}"
|
|
434
465
|
```
|
|
435
466
|
|
|
467
|
+
### Icons
|
|
468
|
+
|
|
469
|
+
A message can draw one of 148 built-in bitmaps where a tag sits. `<icon:sun> FINE`
|
|
470
|
+
puts a sun in front of the word, and `<icon:lock:red>` retints an icon that is drawn
|
|
471
|
+
in a single ink; the colour words are the colour tokens' own, down to `dimred` and
|
|
472
|
+
`dimgreen`, so a message that can say `<red>` needs no second spelling. An icon
|
|
473
|
+
drawn in its own colours, such as the sun, takes no tint and asking for one is
|
|
474
|
+
refused rather than ignored. `GET /enumerations/icons` lists every icon with its
|
|
475
|
+
group, its width and whether it takes a tint.
|
|
476
|
+
|
|
477
|
+
Icons are off until `picture_count` is set, because each one on the sign needs a
|
|
478
|
+
picture file of its own and allocating those reallocates the sign's memory, which
|
|
479
|
+
erases every message on it. Set it once, alongside the other pool settings, and
|
|
480
|
+
16 is a comfortable number.
|
|
481
|
+
|
|
482
|
+
Four things are worth knowing, and the first is the one that decides how to use them:
|
|
483
|
+
|
|
484
|
+
- **An icon is not a live value.** Every write to a picture file blanks the display
|
|
485
|
+
and restarts a scrolling message. A weather slot stepping from sun to cloud to rain
|
|
486
|
+
pays that each time it lands on an icon the sign is not already holding. Something
|
|
487
|
+
that changes every minute belongs in a variable, which costs no blank at all.
|
|
488
|
+
- **The pool is smaller than the library, and that is the design.** A picture file is
|
|
489
|
+
claimed by whichever icon a message calls, and kept after its last caller goes, so a
|
|
490
|
+
source alternating between two icons costs nothing after the first write. When every
|
|
491
|
+
file is holding an icon something still calls and a new one is asked for, the write
|
|
492
|
+
is refused with a 409. `GET /health` reports pictures used against pictures total,
|
|
493
|
+
and a full pool is the resting state rather than a warning.
|
|
494
|
+
- **A tint makes a second picture.** `<icon:check:green>` and `<icon:check:red>` are
|
|
495
|
+
two bitmaps and take two files.
|
|
496
|
+
- **An icon can be parted from its word.** A line too wide for the display breaks onto
|
|
497
|
+
a second page in HOLD, and the last word can arrive there without the icon labelling
|
|
498
|
+
it. The service cannot warn about this: it would have to know the width of the sign's
|
|
499
|
+
proportional font.
|
|
500
|
+
|
|
436
501
|
### Recovering a sign that has stopped responding
|
|
437
502
|
|
|
438
503
|
A sign mounted out of reach can wedge: a stray bit corrupts what its decoder is
|
|
@@ -483,11 +548,32 @@ telnet serial protocol, `/dev/ttyUSB0` or `COM3` for a cable plugged straight in
|
|
|
483
548
|
and the port, and pyserial's answer to one that has a slash names neither the setting
|
|
484
549
|
nor the value.
|
|
485
550
|
|
|
486
|
-
|
|
487
|
-
on it**: `slot_count`, `slot_capacity`, `variable_count
|
|
488
|
-
service will do it, and say so loudly in the log, but they are not
|
|
489
|
-
with. With `variable_count` at 0, `variable_capacity` allocates
|
|
490
|
-
alone reallocates nothing either.
|
|
551
|
+
Five settings reallocate the sign's memory when changed, and **that erases every message
|
|
552
|
+
on it**: `slot_count`, `slot_capacity`, `variable_count`, `variable_capacity` and
|
|
553
|
+
`picture_count`. The service will do it, and say so loudly in the log, but they are not
|
|
554
|
+
settings to fiddle with. With `variable_count` at 0, `variable_capacity` allocates
|
|
555
|
+
nothing, so changing it alone reallocates nothing either. `picture_count` starts at 0,
|
|
556
|
+
which switches icons off, so turning them on is one deliberate erase.
|
|
557
|
+
|
|
558
|
+
All five come out of one memory pool, which a BetaBrite Classic reported as 5482 bytes,
|
|
559
|
+
and each file costs thirteen bytes beyond its own size. The defaults take 2518 of that,
|
|
560
|
+
and each icon takes 69 on top, so the defaults with sixteen icons take 3622.
|
|
561
|
+
|
|
562
|
+
That 5482 is a ceiling, and it is checked in two places that do different jobs. A
|
|
563
|
+
configuration bigger than it is refused when the settings are read, on any machine,
|
|
564
|
+
whether or not a sign is attached; that is the ceiling, and no sign can raise it. Then,
|
|
565
|
+
on a start that is about to reallocate the sign's memory and only then, the service asks
|
|
566
|
+
the sign for its own figure, and a sign reporting less than 5482 is believed. So the
|
|
567
|
+
second check can lower the limit and never raise it. A sign with a bigger pool than this
|
|
568
|
+
hardware's would need `ASSUMED_SIGN_MEMORY_POOL` in `readerboard/sign/pool.py` raised
|
|
569
|
+
before it could use the extra.
|
|
570
|
+
|
|
571
|
+
A sign that answers and does not have the room stops the service starting, with a message
|
|
572
|
+
naming what was configured, what it needs and what there is; that is the one failure that
|
|
573
|
+
does stop it, because the alternative is erasing every message on the sign to write a pool
|
|
574
|
+
that could never work. A sign that says nothing does not stop anything. `POST /sign/reboot`
|
|
575
|
+
asks the same question before it clears the sign, and answers 409 rather than erasing it,
|
|
576
|
+
except when the sign is too wedged to answer, which is the case that endpoint exists for.
|
|
491
577
|
|
|
492
578
|
## Security
|
|
493
579
|
|
|
@@ -568,8 +654,9 @@ each is pinned separately so that nothing the service installs ever pulls Qt in.
|
|
|
568
654
|
with `--with-client` the client as well, so the whole loop comes up from one command.
|
|
569
655
|
Both editors carry it as a launch configuration under the same name, "readerboard and
|
|
570
656
|
the sign simulator", in `.vscode/launch.json` and in `.idea/runConfigurations/`, beside
|
|
571
|
-
configurations for running the pieces separately
|
|
572
|
-
"readerboard
|
|
657
|
+
configurations for running the pieces separately, the client among them as
|
|
658
|
+
"readerboard client". Both carry the three way one as "readerboard, the sign simulator
|
|
659
|
+
and the client" as well.
|
|
573
660
|
|
|
574
661
|
`scripts/run_against_a_sign.py` is the other one, for when the sign is real: the
|
|
575
662
|
service and the client, no simulator, and the sign's address passed as an argument so
|
|
@@ -29,6 +29,8 @@ the rotation resumes.
|
|
|
29
29
|
time it draws the message, with no blank and no restart. One variable can appear in
|
|
30
30
|
any number of messages, and a value that stops arriving can be made to go stale.
|
|
31
31
|
- **Alerts.** Take the display over, optionally with a deadline, then hand it back.
|
|
32
|
+
A caller that must not overwrite somebody else's alert can ask to be refused
|
|
33
|
+
instead.
|
|
32
34
|
- **It keeps the sign's clock right**, at startup, hourly, and whenever the link comes
|
|
33
35
|
back. That last trigger is the one that matters: a sign returning from a power cut
|
|
34
36
|
does so at no particular minute. The sign is set one minute fast on purpose: the
|
|
@@ -105,8 +107,8 @@ python scripts/run_against_a_sign.py --serial-url socket://192.168.2.51:4001
|
|
|
105
107
|
|
|
106
108
|
That starts the service and the client together, with no simulator. The service
|
|
107
109
|
comes up on <http://127.0.0.1:5001> with `/docs` beside it, the client comes up
|
|
108
|
-
pointed at that address
|
|
109
|
-
the same window. `--no-client` leaves the client out. Ctrl+C stops everything,
|
|
110
|
+
pointed at that address with the API key already in its box, and the key is
|
|
111
|
+
printed in the same window for anything else that needs it. `--no-client` leaves the client out. Ctrl+C stops everything,
|
|
110
112
|
and closing the client leaves the service running.
|
|
111
113
|
|
|
112
114
|
Both editors carry it as a launch configuration named "readerboard against the
|
|
@@ -138,6 +140,22 @@ sign shows nothing or shows rubbish, and the adapter answers on port 23, it is
|
|
|
138
140
|
probably negotiating telnet rather than passing bytes through, and `rfc2217://`
|
|
139
141
|
is the form that speaks that.
|
|
140
142
|
|
|
143
|
+
An adapter that drops off the network says nothing as it goes. The service asks
|
|
144
|
+
the operating system to give a network link up about a minute after the adapter
|
|
145
|
+
stops answering: on Linux, a minute after the first write it did not
|
|
146
|
+
acknowledge, or, if nothing was written, about a minute after it last answered,
|
|
147
|
+
found by keepalive probes. A platform that does not take one of these options
|
|
148
|
+
keeps its own default for it, which can be much longer. The service itself only
|
|
149
|
+
finds out at its first read or write after the link has gone, and `GET /health`
|
|
150
|
+
shows `"connected": false` from then on. A sign with a variable updated every
|
|
151
|
+
minute is written to that often. One left alone is written to at the refresh,
|
|
152
|
+
every `refresh_interval_seconds`, which is 15 minutes by default, and a refresh
|
|
153
|
+
that lands before the link has gone is buffered like any other write, so on a
|
|
154
|
+
quiet sign `link.connected` can stay true for a refresh interval and a minute
|
|
155
|
+
after the adapter has gone. It is still worth a monitor watching, and needs no
|
|
156
|
+
key, but a ping of the adapter answers within a minute however quiet the sign
|
|
157
|
+
is, and is the better alarm.
|
|
158
|
+
|
|
141
159
|
### The API key, and config.local.toml
|
|
142
160
|
|
|
143
161
|
The key is not an argument. A launch configuration is a tracked file and a
|
|
@@ -179,6 +197,19 @@ again after pulling a new version: your config file and key are left alone.
|
|
|
179
197
|
your registered messages, so reinstalling puts the sign back as it was. Add `--purge` to
|
|
180
198
|
remove those too.
|
|
181
199
|
|
|
200
|
+
The unit restarts the service whenever it stops, and gives up after ten failed starts in
|
|
201
|
+
five minutes. Almost nothing here fails permanently, which is what makes the exceptions
|
|
202
|
+
worth stopping for: a memory pool too big for the sign fails identically every time, and
|
|
203
|
+
each attempt puts a read on the wire that stalls a scrolling message. `systemctl status
|
|
204
|
+
readerboard` says which failure it was. Once the configuration is fixed, clearing the
|
|
205
|
+
give-up and starting it again are two commands, because `reset-failed` clears the counter
|
|
206
|
+
and leaves the unit stopped:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
sudo systemctl reset-failed readerboard
|
|
210
|
+
sudo systemctl start readerboard
|
|
211
|
+
```
|
|
212
|
+
|
|
182
213
|
### With Docker
|
|
183
214
|
|
|
184
215
|
The image is published to both registries on every release, for `linux/amd64`,
|
|
@@ -389,6 +420,40 @@ actions:
|
|
|
389
420
|
value: "{{ states('sensor.outside_temperature') | round(0) | int }}"
|
|
390
421
|
```
|
|
391
422
|
|
|
423
|
+
### Icons
|
|
424
|
+
|
|
425
|
+
A message can draw one of 148 built-in bitmaps where a tag sits. `<icon:sun> FINE`
|
|
426
|
+
puts a sun in front of the word, and `<icon:lock:red>` retints an icon that is drawn
|
|
427
|
+
in a single ink; the colour words are the colour tokens' own, down to `dimred` and
|
|
428
|
+
`dimgreen`, so a message that can say `<red>` needs no second spelling. An icon
|
|
429
|
+
drawn in its own colours, such as the sun, takes no tint and asking for one is
|
|
430
|
+
refused rather than ignored. `GET /enumerations/icons` lists every icon with its
|
|
431
|
+
group, its width and whether it takes a tint.
|
|
432
|
+
|
|
433
|
+
Icons are off until `picture_count` is set, because each one on the sign needs a
|
|
434
|
+
picture file of its own and allocating those reallocates the sign's memory, which
|
|
435
|
+
erases every message on it. Set it once, alongside the other pool settings, and
|
|
436
|
+
16 is a comfortable number.
|
|
437
|
+
|
|
438
|
+
Four things are worth knowing, and the first is the one that decides how to use them:
|
|
439
|
+
|
|
440
|
+
- **An icon is not a live value.** Every write to a picture file blanks the display
|
|
441
|
+
and restarts a scrolling message. A weather slot stepping from sun to cloud to rain
|
|
442
|
+
pays that each time it lands on an icon the sign is not already holding. Something
|
|
443
|
+
that changes every minute belongs in a variable, which costs no blank at all.
|
|
444
|
+
- **The pool is smaller than the library, and that is the design.** A picture file is
|
|
445
|
+
claimed by whichever icon a message calls, and kept after its last caller goes, so a
|
|
446
|
+
source alternating between two icons costs nothing after the first write. When every
|
|
447
|
+
file is holding an icon something still calls and a new one is asked for, the write
|
|
448
|
+
is refused with a 409. `GET /health` reports pictures used against pictures total,
|
|
449
|
+
and a full pool is the resting state rather than a warning.
|
|
450
|
+
- **A tint makes a second picture.** `<icon:check:green>` and `<icon:check:red>` are
|
|
451
|
+
two bitmaps and take two files.
|
|
452
|
+
- **An icon can be parted from its word.** A line too wide for the display breaks onto
|
|
453
|
+
a second page in HOLD, and the last word can arrive there without the icon labelling
|
|
454
|
+
it. The service cannot warn about this: it would have to know the width of the sign's
|
|
455
|
+
proportional font.
|
|
456
|
+
|
|
392
457
|
### Recovering a sign that has stopped responding
|
|
393
458
|
|
|
394
459
|
A sign mounted out of reach can wedge: a stray bit corrupts what its decoder is
|
|
@@ -439,11 +504,32 @@ telnet serial protocol, `/dev/ttyUSB0` or `COM3` for a cable plugged straight in
|
|
|
439
504
|
and the port, and pyserial's answer to one that has a slash names neither the setting
|
|
440
505
|
nor the value.
|
|
441
506
|
|
|
442
|
-
|
|
443
|
-
on it**: `slot_count`, `slot_capacity`, `variable_count
|
|
444
|
-
service will do it, and say so loudly in the log, but they are not
|
|
445
|
-
with. With `variable_count` at 0, `variable_capacity` allocates
|
|
446
|
-
alone reallocates nothing either.
|
|
507
|
+
Five settings reallocate the sign's memory when changed, and **that erases every message
|
|
508
|
+
on it**: `slot_count`, `slot_capacity`, `variable_count`, `variable_capacity` and
|
|
509
|
+
`picture_count`. The service will do it, and say so loudly in the log, but they are not
|
|
510
|
+
settings to fiddle with. With `variable_count` at 0, `variable_capacity` allocates
|
|
511
|
+
nothing, so changing it alone reallocates nothing either. `picture_count` starts at 0,
|
|
512
|
+
which switches icons off, so turning them on is one deliberate erase.
|
|
513
|
+
|
|
514
|
+
All five come out of one memory pool, which a BetaBrite Classic reported as 5482 bytes,
|
|
515
|
+
and each file costs thirteen bytes beyond its own size. The defaults take 2518 of that,
|
|
516
|
+
and each icon takes 69 on top, so the defaults with sixteen icons take 3622.
|
|
517
|
+
|
|
518
|
+
That 5482 is a ceiling, and it is checked in two places that do different jobs. A
|
|
519
|
+
configuration bigger than it is refused when the settings are read, on any machine,
|
|
520
|
+
whether or not a sign is attached; that is the ceiling, and no sign can raise it. Then,
|
|
521
|
+
on a start that is about to reallocate the sign's memory and only then, the service asks
|
|
522
|
+
the sign for its own figure, and a sign reporting less than 5482 is believed. So the
|
|
523
|
+
second check can lower the limit and never raise it. A sign with a bigger pool than this
|
|
524
|
+
hardware's would need `ASSUMED_SIGN_MEMORY_POOL` in `readerboard/sign/pool.py` raised
|
|
525
|
+
before it could use the extra.
|
|
526
|
+
|
|
527
|
+
A sign that answers and does not have the room stops the service starting, with a message
|
|
528
|
+
naming what was configured, what it needs and what there is; that is the one failure that
|
|
529
|
+
does stop it, because the alternative is erasing every message on the sign to write a pool
|
|
530
|
+
that could never work. A sign that says nothing does not stop anything. `POST /sign/reboot`
|
|
531
|
+
asks the same question before it clears the sign, and answers 409 rather than erasing it,
|
|
532
|
+
except when the sign is too wedged to answer, which is the case that endpoint exists for.
|
|
447
533
|
|
|
448
534
|
## Security
|
|
449
535
|
|
|
@@ -524,8 +610,9 @@ each is pinned separately so that nothing the service installs ever pulls Qt in.
|
|
|
524
610
|
with `--with-client` the client as well, so the whole loop comes up from one command.
|
|
525
611
|
Both editors carry it as a launch configuration under the same name, "readerboard and
|
|
526
612
|
the sign simulator", in `.vscode/launch.json` and in `.idea/runConfigurations/`, beside
|
|
527
|
-
configurations for running the pieces separately
|
|
528
|
-
"readerboard
|
|
613
|
+
configurations for running the pieces separately, the client among them as
|
|
614
|
+
"readerboard client". Both carry the three way one as "readerboard, the sign simulator
|
|
615
|
+
and the client" as well.
|
|
529
616
|
|
|
530
617
|
`scripts/run_against_a_sign.py` is the other one, for when the sign is real: the
|
|
531
618
|
service and the client, no simulator, and the sign's address passed as an argument so
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "readerboard"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.1"
|
|
8
8
|
description = "An HTTP service for BetaBrite and Alpha protocol LED signs: several sources share one sign, with alerts, expiring messages and clock sync"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -52,7 +52,7 @@ dependencies = [
|
|
|
52
52
|
# watchfiles, which only makes --reload quicker. That flag is for working on
|
|
53
53
|
# the service rather than for running one, and uvicorn polls the tree
|
|
54
54
|
# without it.
|
|
55
|
-
"uvicorn>=0.52.1,<0.
|
|
55
|
+
"uvicorn>=0.52.1,<0.54",
|
|
56
56
|
"pydantic>=2.13.4,<3",
|
|
57
57
|
"pydantic-settings>=2.15.0,<3",
|
|
58
58
|
"pyserial>=3.5,<4",
|
|
@@ -63,7 +63,12 @@ dependencies = [
|
|
|
63
63
|
|
|
64
64
|
[project.optional-dependencies]
|
|
65
65
|
dev = [
|
|
66
|
-
|
|
66
|
+
# pytest 9.2 renamed CallSpec2 and deprecated the old name, which the anyio
|
|
67
|
+
# held below 4.15 further down still imports in its pytest plugin. That is
|
|
68
|
+
# the same collection error again, raised as the plugin loads, so no test
|
|
69
|
+
# runs at all. anyio 4.16 copes with the rename, so this ceiling comes off
|
|
70
|
+
# when the anyio one does.
|
|
71
|
+
"pytest>=9.1.1,<9.2",
|
|
67
72
|
"pytest-asyncio>=1.4.0,<2",
|
|
68
73
|
# starlette's TestClient moved to httpx2; plain httpx now raises a
|
|
69
74
|
# deprecation warning, which this suite is configured to treat as an error.
|
|
@@ -175,6 +180,15 @@ files = ["readerboard"]
|
|
|
175
180
|
strict = true
|
|
176
181
|
warn_unreachable = true
|
|
177
182
|
enable_error_code = ["redundant-expr", "truthy-bool", "ignore-without-code"]
|
|
183
|
+
# `files` above still names only the service, so checking a tool is a separate
|
|
184
|
+
# invocation, `mypy tools/signsim/signsim` or `mypy tools/apiclient/apiclient`.
|
|
185
|
+
# Naming both tool roots here means one of those resolves the tool's own
|
|
186
|
+
# package (`import signsim`, `import apiclient`) on its own, no MYPYPATH
|
|
187
|
+
# needed by hand or in CI. The directory-level invocations above already found
|
|
188
|
+
# their root by crawling up from the package's own `__init__.py`; what this
|
|
189
|
+
# actually buys is the narrower case that crawl finds nothing for, such as
|
|
190
|
+
# `mypy` on a single file under a tool's `tests/`, which has none.
|
|
191
|
+
mypy_path = ["tools/signsim", "tools/apiclient"]
|
|
178
192
|
|
|
179
193
|
[[tool.mypy.overrides]]
|
|
180
194
|
# pyserial ships no type information.
|
|
@@ -9,12 +9,18 @@ Nothing here fails to start because the sign is unreachable. A service that
|
|
|
9
9
|
refused to boot with the sign unplugged would need someone to notice and restart
|
|
10
10
|
it once the sign came back, which is precisely the situation it exists to
|
|
11
11
|
survive.
|
|
12
|
+
|
|
13
|
+
There is one thing that does stop it, and it is not that. A sign that is
|
|
14
|
+
answering and has less memory than the configured pool needs is a configuration
|
|
15
|
+
somebody has to change, and going ahead would erase every message on the sign to
|
|
16
|
+
write a pool that cannot work. See readerboard.sign.pool.
|
|
12
17
|
"""
|
|
13
18
|
|
|
14
19
|
from __future__ import annotations
|
|
15
20
|
|
|
16
21
|
import asyncio
|
|
17
22
|
import contextlib
|
|
23
|
+
import functools
|
|
18
24
|
import logging
|
|
19
25
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
20
26
|
|
|
@@ -29,6 +35,7 @@ from readerboard.config import Settings
|
|
|
29
35
|
from readerboard.services.alerts import AlertService
|
|
30
36
|
from readerboard.services.clock import ClockService
|
|
31
37
|
from readerboard.services.registry import SlotRegistry
|
|
38
|
+
from readerboard.sign import pool
|
|
32
39
|
from readerboard.sign.controller import SignController
|
|
33
40
|
from readerboard.sign.layout import Layout
|
|
34
41
|
from readerboard.sign.state import StateStore
|
|
@@ -51,6 +58,13 @@ A **variable** is a value a slot's message or an alert calls with `<var:name>`.
|
|
|
51
58
|
Changing it rewrites only the variable, so the sign shows the new value without
|
|
52
59
|
blanking or restarting what calls it.
|
|
53
60
|
|
|
61
|
+
An **icon** is one of the built-in bitmaps, drawn where `<icon:name>` sits in a
|
|
62
|
+
message or an alert. `GET /enumerations/icons` lists them. There is a fixed pool
|
|
63
|
+
of picture files on the sign and it is far smaller than the library, so a file is
|
|
64
|
+
claimed by whichever icon a message calls and kept until another icon needs it.
|
|
65
|
+
Icons are off until `picture_count` is raised, because raising it reallocates the
|
|
66
|
+
sign's memory and clears it.
|
|
67
|
+
|
|
54
68
|
Every write needs an `X-API-Key` header, and so does `GET /sign/information`,
|
|
55
69
|
which reads the sign rather than the service: it puts a question on the wire and
|
|
56
70
|
holds the sign until the answer comes back. The service's own reads and
|
|
@@ -59,14 +73,17 @@ holds the sign until the answer comes back. The service's own reads and
|
|
|
59
73
|
|
|
60
74
|
A failure is reported by the status code, with the reason in a `detail` field:
|
|
61
75
|
400 for a command the sign does not have, a parameter it will not accept, a
|
|
62
|
-
message or value too long for its file, markup the sign cannot render
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
when
|
|
67
|
-
something
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
message or value too long for its file, markup the sign cannot render, a call to
|
|
77
|
+
a variable that does not exist, an icon nobody has, a colour asked for on an icon
|
|
78
|
+
drawn in fixed colours, or an icon called while icons are switched off, 401 for a
|
|
79
|
+
missing or wrong `X-API-Key`, 404 for a slot or variable that does not exist, 409
|
|
80
|
+
when every slot, every variable or every picture file is already in use, a
|
|
81
|
+
variable something still calls is deleted, or an alert is raised with
|
|
82
|
+
`fail_if_active` while one is already up, 503 when the sign is unreachable,
|
|
83
|
+
stops partway through an answer or answers with something the service cannot
|
|
84
|
+
read, or no API key is configured at all, 500 for something the service has no
|
|
85
|
+
code for, and 422 for a body that is not the shape the endpoint declares, which
|
|
86
|
+
includes a display mode the sign does not have.
|
|
70
87
|
"""
|
|
71
88
|
|
|
72
89
|
|
|
@@ -81,6 +98,26 @@ def build_transport(settings: Settings) -> Transport:
|
|
|
81
98
|
)
|
|
82
99
|
|
|
83
100
|
|
|
101
|
+
async def _refresh_and_reassert(registry: SlotRegistry, alerts: AlertService) -> None:
|
|
102
|
+
"""Push everything to the sign again, and make sure any alert is back on it.
|
|
103
|
+
|
|
104
|
+
Both of the places that re-push go through here, the timer below and the
|
|
105
|
+
reconnect hook, because both are answering the same question: the sign may
|
|
106
|
+
have been power cycled and nothing the controller believes about it holds.
|
|
107
|
+
|
|
108
|
+
The refresh puts the slots back, and it puts the alert back too when it had
|
|
109
|
+
to hand the sign over to write a picture. It says which, and that is why
|
|
110
|
+
this asks rather than always re-asserting: re-asserting an alert that is
|
|
111
|
+
already back on the priority file restarts it on the display for nothing,
|
|
112
|
+
which is a visible flicker every refresh interval for as long as the alert
|
|
113
|
+
is up. In the case where the refresh did not touch the priority file, which
|
|
114
|
+
is a service with no icons, the re-assert is the only thing that would put
|
|
115
|
+
the alert on a sign that came back blank.
|
|
116
|
+
"""
|
|
117
|
+
if not await registry.refresh():
|
|
118
|
+
await alerts.reassert()
|
|
119
|
+
|
|
120
|
+
|
|
84
121
|
async def _refresh_loop(app: FastAPI, interval: float) -> None:
|
|
85
122
|
"""Push everything to the sign again, periodically.
|
|
86
123
|
|
|
@@ -93,12 +130,7 @@ async def _refresh_loop(app: FastAPI, interval: float) -> None:
|
|
|
93
130
|
while True:
|
|
94
131
|
await asyncio.sleep(interval)
|
|
95
132
|
try:
|
|
96
|
-
await registry
|
|
97
|
-
# The refresh puts the slots back, but an alert lives in the
|
|
98
|
-
# priority file, which the registry does not touch. Without this a
|
|
99
|
-
# sign power cycled mid-alert would stay blank until the alert's
|
|
100
|
-
# deadline, and an alert with no deadline would stay blank for good.
|
|
101
|
-
await alerts.reassert()
|
|
133
|
+
await _refresh_and_reassert(registry, alerts)
|
|
102
134
|
except TransportError as err:
|
|
103
135
|
logger.debug("periodic refresh skipped, sign unreachable: %s", err)
|
|
104
136
|
except Exception:
|
|
@@ -149,12 +181,19 @@ def create_app(settings: Settings | None = None, transport: Transport | None = N
|
|
|
149
181
|
settings.slot_capacity,
|
|
150
182
|
settings.variable_count,
|
|
151
183
|
settings.variable_capacity,
|
|
184
|
+
settings.picture_count,
|
|
152
185
|
)
|
|
153
186
|
alerts = AlertService(controller, store, state)
|
|
154
187
|
registry = SlotRegistry(controller, layout, store, state)
|
|
155
188
|
# An alert calling a variable is rendered by the registry, under its
|
|
156
189
|
# lock, so the variable cannot be deleted while the alert calls it.
|
|
157
190
|
alerts.set_rendering(registry.rendering)
|
|
191
|
+
# And the other way: the registry hands the sign back before it writes a
|
|
192
|
+
# picture, because the sign will not take one while a priority message
|
|
193
|
+
# is running. Both directions are wired here rather than either service
|
|
194
|
+
# reaching for the other, and the lock order is what the two of them
|
|
195
|
+
# have to agree on: the registry's first, then this one's.
|
|
196
|
+
registry.set_priority_hold(alerts.lifted)
|
|
158
197
|
clock = ClockService(
|
|
159
198
|
controller,
|
|
160
199
|
interval_seconds=settings.clock_sync_interval_seconds,
|
|
@@ -169,6 +208,23 @@ def create_app(settings: Settings | None = None, transport: Transport | None = N
|
|
|
169
208
|
|
|
170
209
|
await controller.start()
|
|
171
210
|
|
|
211
|
+
# Only when a reconfiguration is due, which is the one moment the pool
|
|
212
|
+
# can be got wrong and the one moment the sign is about to be erased. An
|
|
213
|
+
# ordinary restart asks the sign nothing, costs the display nothing, and
|
|
214
|
+
# behaves exactly as it did before this check existed. Nothing here can
|
|
215
|
+
# bring on a reallocation either: the only thing it can do is stop one.
|
|
216
|
+
if layout.needs_reconfiguration(state.layout):
|
|
217
|
+
budget = await pool.measure(controller, fallback=pool.ASSUMED_SIGN_MEMORY_POOL)
|
|
218
|
+
try:
|
|
219
|
+
pool.check_fits(layout, budget)
|
|
220
|
+
except pool.PoolTooLarge as err:
|
|
221
|
+
# Logged as well as raised, because the traceback uvicorn prints
|
|
222
|
+
# around a failed startup buries the one sentence that says what
|
|
223
|
+
# to do about it.
|
|
224
|
+
logger.error("refusing to start: %s", err)
|
|
225
|
+
await controller.stop()
|
|
226
|
+
raise
|
|
227
|
+
|
|
172
228
|
# The sign may be unreachable, and that is not a reason to refuse to
|
|
173
229
|
# start. What is put back below happens again on the next reconnect.
|
|
174
230
|
try:
|
|
@@ -201,7 +257,12 @@ def create_app(settings: Settings | None = None, transport: Transport | None = N
|
|
|
201
257
|
controller.on_reconnect(clock.sync_quietly)
|
|
202
258
|
# A link that just came back may be in front of a sign that was power
|
|
203
259
|
# cycled, so nothing the controller believes about its contents holds.
|
|
204
|
-
|
|
260
|
+
# The alert goes back on with everything else: it was only the slots
|
|
261
|
+
# before, so a reconnect to a sign that had lost an alert left the
|
|
262
|
+
# priority file empty until the next tick of the timer above.
|
|
263
|
+
controller.on_reconnect(
|
|
264
|
+
functools.partial(_refresh_and_reassert, registry, alerts)
|
|
265
|
+
)
|
|
205
266
|
|
|
206
267
|
if settings.clock_sync_enabled:
|
|
207
268
|
await clock.start()
|
|
@@ -251,6 +312,7 @@ def create_app(settings: Settings | None = None, transport: Transport | None = N
|
|
|
251
312
|
|
|
252
313
|
used, total = registry.occupancy
|
|
253
314
|
variables_used, variables_total = registry.variable_occupancy
|
|
315
|
+
pictures_used, pictures_total = registry.picture_occupancy
|
|
254
316
|
return HealthResponse(
|
|
255
317
|
status="ok" if controller.is_connected else "degraded",
|
|
256
318
|
version=__version__,
|
|
@@ -266,6 +328,8 @@ def create_app(settings: Settings | None = None, transport: Transport | None = N
|
|
|
266
328
|
slots_total=total,
|
|
267
329
|
variables_used=variables_used,
|
|
268
330
|
variables_total=variables_total,
|
|
331
|
+
pictures_used=pictures_used,
|
|
332
|
+
pictures_total=pictures_total,
|
|
269
333
|
sign_in_sync=registry.in_sync,
|
|
270
334
|
alert_active=alerts.active is not None,
|
|
271
335
|
clock_last_synced_at=clock.last_sync_at,
|
|
@@ -13,13 +13,16 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from fastapi import status
|
|
15
15
|
|
|
16
|
+
from readerboard.icons import IconError
|
|
16
17
|
from readerboard.protocol.frames import ProtocolError
|
|
17
18
|
from readerboard.protocol.markup import MarkupError
|
|
18
19
|
from readerboard.protocol.replies import ReplyError
|
|
19
20
|
from readerboard.services import commands
|
|
20
|
-
from readerboard.services.alerts import AlertTooLong
|
|
21
|
+
from readerboard.services.alerts import AlertAlreadyActive, AlertTooLong
|
|
21
22
|
from readerboard.services.registry import (
|
|
23
|
+
IconsDisabled,
|
|
22
24
|
MessageTooLong,
|
|
25
|
+
PicturePoolFull,
|
|
23
26
|
UnknownSlot,
|
|
24
27
|
UnknownVariable,
|
|
25
28
|
VariableInUse,
|
|
@@ -27,6 +30,7 @@ from readerboard.services.registry import (
|
|
|
27
30
|
VariableTooLong,
|
|
28
31
|
)
|
|
29
32
|
from readerboard.sign.layout import LayoutFull
|
|
33
|
+
from readerboard.sign.pool import PoolTooLarge
|
|
30
34
|
from readerboard.transport.base import TransportError
|
|
31
35
|
|
|
32
36
|
STATUS_FOR_ERROR: tuple[tuple[type[Exception], int], ...] = (
|
|
@@ -35,16 +39,35 @@ STATUS_FOR_ERROR: tuple[tuple[type[Exception], int], ...] = (
|
|
|
35
39
|
(MessageTooLong, status.HTTP_400_BAD_REQUEST),
|
|
36
40
|
(VariableTooLong, status.HTTP_400_BAD_REQUEST),
|
|
37
41
|
(VariablesDisabled, status.HTTP_400_BAD_REQUEST),
|
|
42
|
+
(IconsDisabled, status.HTTP_400_BAD_REQUEST),
|
|
43
|
+
# An icon nobody has, or a tint on one drawn in fixed colours. The library
|
|
44
|
+
# raises these rather than the renderer, because the renderer is not told
|
|
45
|
+
# which icons exist; mapping the one base they share is what keeps a
|
|
46
|
+
# misspelled icon name a 400 rather than a 500.
|
|
47
|
+
(IconError, status.HTTP_400_BAD_REQUEST),
|
|
38
48
|
(AlertTooLong, status.HTTP_400_BAD_REQUEST),
|
|
39
49
|
(commands.UnknownCommand, status.HTTP_400_BAD_REQUEST),
|
|
40
50
|
(commands.BadParameter, status.HTTP_400_BAD_REQUEST),
|
|
41
51
|
(UnknownSlot, status.HTTP_404_NOT_FOUND),
|
|
42
52
|
(UnknownVariable, status.HTTP_404_NOT_FOUND),
|
|
43
53
|
(LayoutFull, status.HTTP_409_CONFLICT),
|
|
54
|
+
# Only POST /sign/reboot can raise this: the sign has less memory than the
|
|
55
|
+
# configuration needs, found by asking it just before the reboot would have
|
|
56
|
+
# erased it. The state of the hardware rather than anything wrong with the
|
|
57
|
+
# request, which is what makes it a conflict, and nothing was written.
|
|
58
|
+
(PoolTooLarge, status.HTTP_409_CONFLICT),
|
|
44
59
|
# Deleting a variable a message still calls would leave that message
|
|
45
60
|
# calling a file the next variable could be given. Not the caller's
|
|
46
61
|
# request being malformed, which is what makes it a conflict.
|
|
47
62
|
(VariableInUse, status.HTTP_409_CONFLICT),
|
|
63
|
+
# Every picture file is holding an icon something still calls. Like a full
|
|
64
|
+
# slot pool and unlike a bad icon name, this is the state of the sign rather
|
|
65
|
+
# than anything wrong with the request.
|
|
66
|
+
(PicturePoolFull, status.HTTP_409_CONFLICT),
|
|
67
|
+
# An alert is already up and the caller asked to be refused rather than
|
|
68
|
+
# replace it. The same request would have been accepted a moment earlier, so
|
|
69
|
+
# it is the state of the sign that decides this and not the body.
|
|
70
|
+
(AlertAlreadyActive, status.HTTP_409_CONFLICT),
|
|
48
71
|
(TransportError, status.HTTP_503_SERVICE_UNAVAILABLE),
|
|
49
72
|
# A sign that answers with something unreadable is as unusable as one
|
|
50
73
|
# that does not answer, and neither is the caller's doing. The route
|