readerboard 0.2.0__tar.gz → 0.4.0__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.
Files changed (69) hide show
  1. {readerboard-0.2.0/readerboard.egg-info → readerboard-0.4.0}/PKG-INFO +187 -64
  2. {readerboard-0.2.0 → readerboard-0.4.0}/README.md +183 -62
  3. {readerboard-0.2.0 → readerboard-0.4.0}/pyproject.toml +36 -26
  4. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/__init__.py +1 -1
  5. readerboard-0.4.0/readerboard/__main__.py +171 -0
  6. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/api/app.py +49 -36
  7. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/api/deps.py +5 -14
  8. readerboard-0.4.0/readerboard/api/errors.py +40 -0
  9. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/api/models.py +60 -86
  10. readerboard-0.2.0/readerboard/api/routes_v2.py → readerboard-0.4.0/readerboard/api/routes.py +97 -15
  11. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/config.py +23 -34
  12. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/logging_setup.py +6 -4
  13. readerboard-0.4.0/readerboard/names.py +24 -0
  14. readerboard-0.4.0/readerboard/protocol/constants.py +772 -0
  15. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/protocol/frames.py +75 -8
  16. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/protocol/markup.py +45 -4
  17. readerboard-0.4.0/readerboard/protocol/replies.py +190 -0
  18. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/protocol/tokens.py +94 -30
  19. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/services/alerts.py +65 -9
  20. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/services/clock.py +66 -8
  21. readerboard-0.4.0/readerboard/services/commands.py +174 -0
  22. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/services/registry.py +69 -39
  23. readerboard-0.4.0/readerboard/sign/controller.py +491 -0
  24. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/sign/layout.py +9 -5
  25. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/sign/state.py +120 -7
  26. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/transport/base.py +13 -1
  27. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/transport/fake.py +25 -1
  28. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/transport/serial_link.py +81 -10
  29. {readerboard-0.2.0 → readerboard-0.4.0/readerboard.egg-info}/PKG-INFO +187 -64
  30. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard.egg-info/SOURCES.txt +12 -2
  31. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard.egg-info/requires.txt +1 -0
  32. {readerboard-0.2.0 → readerboard-0.4.0}/tests/test_alerts.py +73 -3
  33. readerboard-0.4.0/tests/test_api.py +739 -0
  34. {readerboard-0.2.0 → readerboard-0.4.0}/tests/test_clock.py +109 -6
  35. readerboard-0.4.0/tests/test_component_names.py +198 -0
  36. {readerboard-0.2.0 → readerboard-0.4.0}/tests/test_constant_values.py +218 -16
  37. readerboard-0.4.0/tests/test_controller.py +623 -0
  38. {readerboard-0.2.0 → readerboard-0.4.0}/tests/test_frames.py +50 -7
  39. readerboard-0.4.0/tests/test_launch_configurations.py +217 -0
  40. readerboard-0.4.0/tests/test_logging_setup.py +96 -0
  41. readerboard-0.4.0/tests/test_markup.py +246 -0
  42. readerboard-0.4.0/tests/test_open_docs.py +177 -0
  43. {readerboard-0.2.0 → readerboard-0.4.0}/tests/test_registry.py +104 -32
  44. readerboard-0.4.0/tests/test_replies.py +157 -0
  45. readerboard-0.4.0/tests/test_run_against_a_sign.py +276 -0
  46. readerboard-0.4.0/tests/test_run_with_simulator.py +89 -0
  47. readerboard-0.4.0/tests/test_state.py +335 -0
  48. readerboard-0.4.0/tests/test_tool_icons.py +73 -0
  49. {readerboard-0.2.0 → readerboard-0.4.0}/tests/test_transport.py +106 -1
  50. readerboard-0.2.0/readerboard/__main__.py +0 -57
  51. readerboard-0.2.0/readerboard/api/routes_simple.py +0 -145
  52. readerboard-0.2.0/readerboard/protocol/constants.py +0 -465
  53. readerboard-0.2.0/readerboard/services/commands.py +0 -83
  54. readerboard-0.2.0/readerboard/sign/controller.py +0 -278
  55. readerboard-0.2.0/tests/test_api.py +0 -531
  56. readerboard-0.2.0/tests/test_controller.py +0 -258
  57. readerboard-0.2.0/tests/test_markup.py +0 -101
  58. readerboard-0.2.0/tests/test_state.py +0 -171
  59. {readerboard-0.2.0 → readerboard-0.4.0}/LICENSE +0 -0
  60. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/api/__init__.py +0 -0
  61. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/protocol/__init__.py +0 -0
  62. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/py.typed +0 -0
  63. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/services/__init__.py +0 -0
  64. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/sign/__init__.py +0 -0
  65. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard/transport/__init__.py +0 -0
  66. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard.egg-info/dependency_links.txt +0 -0
  67. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard.egg-info/entry_points.txt +0 -0
  68. {readerboard-0.2.0 → readerboard-0.4.0}/readerboard.egg-info/top_level.txt +0 -0
  69. {readerboard-0.2.0 → readerboard-0.4.0}/setup.cfg +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: readerboard
3
- Version: 0.2.0
4
- Summary: An HTTP service for BetaBrite and Alpha protocol LED signs: several sources share one sign, with alerts, scheduling and clock sync
3
+ Version: 0.4.0
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
7
7
  Project-URL: Homepage, https://github.com/mjaksn/readerboard
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
20
20
  Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
23
24
  Classifier: Topic :: Home Automation
24
25
  Classifier: Topic :: System :: Hardware
25
26
  Classifier: Typing :: Typed
@@ -36,6 +37,7 @@ Provides-Extra: dev
36
37
  Requires-Dist: pytest<10,>=9.1.1; extra == "dev"
37
38
  Requires-Dist: pytest-asyncio<2,>=1.4.0; extra == "dev"
38
39
  Requires-Dist: httpx2<3,>=2.10.0; extra == "dev"
40
+ Requires-Dist: anyio<4.15,>=4.14.2; extra == "dev"
39
41
  Requires-Dist: ruff<0.17,>=0.16.2; extra == "dev"
40
42
  Requires-Dist: mypy<3,>=2.3.0; extra == "dev"
41
43
  Dynamic: license-file
@@ -65,15 +67,20 @@ until it is released, after which the rotation resumes.
65
67
  - **Alerts.** Take the display over, optionally with a deadline, then hand it back.
66
68
  - **It keeps the sign's clock right**, at startup, hourly, and whenever the link comes
67
69
  back. That last trigger is the one that matters: a sign returning from a power cut
68
- does so at no particular minute.
70
+ does so at no particular minute. The sign is set one minute fast on purpose: the
71
+ protocol has no seconds field, so a sign told the current minute reads behind for the
72
+ rest of it and never ahead, and a minute of lead puts the error on the side that reads
73
+ as a clock being a touch fast rather than most of a minute slow.
69
74
  - **It does not redraw the sign for nothing.** A write of bytes the sign already holds is
70
75
  suppressed, so a source re-sending an unchanged temperature does not make the display
71
76
  flicker.
72
- - **It survives restarts and outages.** The registered messages are persisted, and a
73
- write that arrives while the sign is unreachable is accepted and delivered when the
74
- link returns.
75
- - **Errors are errors.** A dead serial link is a 503, not an HTTP 200 with the word
76
- ERROR in the body.
77
+ - **It survives restarts and outages.** The registered messages are persisted and
78
+ pushed to the sign again whenever the link returns, so a restart or a power cut leaves
79
+ the rotation intact. A write that arrives while the sign is unreachable is refused with
80
+ a 503 rather than silently held, so the caller learns it did not land.
81
+ - **Errors are errors.** A dead serial link is a 503 and a message the sign cannot
82
+ render is a 400, each with the reason in the body. Nothing here reports a failure
83
+ under a 200.
77
84
 
78
85
  ## Requirements
79
86
 
@@ -120,6 +127,73 @@ other, and stops both on Ctrl+C. The simulator decodes each transmission, says
120
127
  what every byte of it means, and shows what the sign would be holding as a
121
128
  result. `tools/signsim/README.md` has the details.
122
129
 
130
+ ## Running it against a real sign
131
+
132
+ From a checkout, with the sign on a cable or on an Ethernet to RS-232 adapter:
133
+
134
+ ```
135
+ pip install -e ".[dev]"
136
+ pip install --require-hashes -r tools/apiclient/requirements.lock
137
+ python scripts/run_against_a_sign.py --serial-url socket://192.168.2.51:4001
138
+ ```
139
+
140
+ That starts the service and the client together, with no simulator. The service
141
+ comes up on <http://127.0.0.1:5001> with `/docs` beside it, the client comes up
142
+ pointed at that address, and the API key to paste into the client is printed in
143
+ the same window. `--no-client` leaves the client out. Ctrl+C stops everything,
144
+ and closing the client leaves the service running.
145
+
146
+ Both editors carry it as a launch configuration named "readerboard against the
147
+ real sign and the client". **The sign's address is an argument in those, not a
148
+ setting in a file**, so changing which sign is driven means editing the
149
+ Parameters field in PyCharm's run configuration dialog, or `args` in
150
+ `.vscode/launch.json`. They also pass `--api-port 5002`, so a second checkout of
151
+ this repository on the same machine can run beside them; the launcher checks
152
+ that port before it starts anything rather than letting the service bind, fail
153
+ and stop after the client has been pointed at whatever else answered.
154
+
155
+ ### Writing the address
156
+
157
+ It is a pyserial URL, and **there is no slash between the host and the port**.
158
+ `socket://192.168.2.51/:4001` looks close enough to right and is not: pyserial
159
+ answers it with a bare `TypeError` from deep inside a connection attempt, naming
160
+ neither the setting nor the value. The launcher checks the address before it
161
+ opens anything and says which part is wrong. The four forms are:
162
+
163
+ ```
164
+ socket://192.168.2.51:4001 an Ethernet to RS-232 adapter passing raw TCP
165
+ rfc2217://192.168.2.51:23 an adapter speaking the telnet serial protocol
166
+ COM3 a cable on Windows
167
+ /dev/ttyUSB0 a cable on Linux
168
+ ```
169
+
170
+ Most adapters pass raw TCP, so try `socket://` first. If the link opens but the
171
+ sign shows nothing or shows rubbish, and the adapter answers on port 23, it is
172
+ probably negotiating telnet rather than passing bytes through, and `rfc2217://`
173
+ is the form that speaks that.
174
+
175
+ ### The API key, and config.local.toml
176
+
177
+ The key is not an argument. A launch configuration is a tracked file and a
178
+ command line is a shell history, and anyone holding the key can write to the
179
+ sign. It lives in `config.local.toml` at the root of the checkout, which
180
+ `.gitignore` covers and which the launcher writes with a generated key the first
181
+ time it runs. Given no `--serial-url`, the address is read from there too.
182
+
183
+ ### The first run erases the sign
184
+
185
+ Writing a memory configuration erases every message on the sign, and the service
186
+ writes one whenever it has no record of the configuration already applied. The
187
+ first run against a sign this machine has never driven therefore erases it,
188
+ which is also the only way to allocate the files it then writes into. Every run
189
+ after that reads the record and leaves the sign alone.
190
+
191
+ That record is `.local-sign-state.json`, and it belongs to this launcher alone.
192
+ `scripts/run_with_simulator.py` deletes its own `.local-state.json` on every
193
+ launch, because the simulator starts empty every time and the service has to
194
+ reconfigure it. If the two shared one file, a simulator session would throw the
195
+ sign's record away and the next run against the sign would erase it.
196
+
123
197
  ## Installing it properly
124
198
 
125
199
  Two ways, which do the same job. Pick whichever suits the machine.
@@ -175,13 +249,15 @@ docker run ... --device /dev/ttyUSB0 --group-add 20 \
175
249
 
176
250
  ## Using it
177
251
 
178
- Every write needs an `X-API-Key` header. Reads and `GET /health` do not. In the
179
- Swagger UI at `/docs`, the **Authorize** button puts it in once for the whole page.
252
+ Every write needs an `X-API-Key` header, and so does `GET /sign/information`,
253
+ which asks the sign a question rather than reading the service's own record. The
254
+ service's other reads and `GET /health` do not. In the Swagger UI at `/docs`, the
255
+ **Authorize** button puts it in once for the whole page.
180
256
 
181
257
  Register a message:
182
258
 
183
259
  ```
184
- curl -X PUT http://localhost:5001/v2/messages/temperature \
260
+ curl -X PUT http://localhost:5001/messages/temperature \
185
261
  -H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
186
262
  -d '{"message": "<green>18.4<degree> <red><time>", "display_mode": "HOLD"}'
187
263
  ```
@@ -189,7 +265,7 @@ curl -X PUT http://localhost:5001/v2/messages/temperature \
189
265
  Register a second one and the sign rotates between them:
190
266
 
191
267
  ```
192
- curl -X PUT http://localhost:5001/v2/messages/doorbell \
268
+ curl -X PUT http://localhost:5001/messages/doorbell \
193
269
  -H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
194
270
  -d '{"message": "<amber>Someone at the door", "ttl_seconds": 300}'
195
271
  ```
@@ -197,46 +273,76 @@ curl -X PUT http://localhost:5001/v2/messages/doorbell \
197
273
  Take the sign over for thirty seconds:
198
274
 
199
275
  ```
200
- curl -X POST http://localhost:5001/v2/alerts \
276
+ curl -X POST http://localhost:5001/alerts \
201
277
  -H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
202
278
  -d '{"message": "<red><flash_on>SMOKE ALARM", "ttl_seconds": 30}'
203
279
  ```
204
280
 
205
- The full API, including every markup token and display mode, is at `/docs`.
281
+ Make a noise, which is worth pairing with an alert if the sign is somewhere nobody
282
+ is watching it:
283
+
284
+ ```
285
+ curl -X POST http://localhost:5001/sign/command \
286
+ -H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
287
+ -d '{"command": "SOUND", "parameter": "BEEPS"}'
288
+ ```
289
+
290
+ `BEEPS` is three short beeps and `TONE` is one continuous tone of about two seconds.
291
+ Those are the only two sounds there are: the sign has a fixed-pitch buzzer, so there
292
+ is no pitch or volume to choose.
293
+
294
+ Silence it with `{"command": "SPEAKER", "parameter": "OFF"}`, and turn it back on with
295
+ `ON`. That is a real mute: `SOUND` is still accepted and makes no noise. The setting
296
+ lives on the sign and survives a restart, so it is also the first thing to check if
297
+ `SOUND` ever seems to do nothing.
298
+
299
+ The full API is at `/docs`. Every markup token, display mode and control
300
+ command is listed by the `/enumerations` reads there, which answer at
301
+ request time rather than being frozen into the description.
206
302
 
207
303
  ### Writing messages
208
304
 
209
305
  A message is plain text plus tokens written as `<name>`: `<green>18.4<degree>` is a
210
- colour change, a number, and a degree symbol. `GET /v2/enumerations/markup-tokens` lists
306
+ colour change, a number, and a degree symbol. `GET /enumerations/markup-tokens` lists
211
307
  them all.
212
308
 
213
309
  Text is encoded against the sign's own character table rather than as UTF-8, so `café`
214
- displays correctly. A character the sign cannot render is rejected with a 400 on `/v2`,
215
- and replaced with `?` on the simpler endpoints described below.
310
+ displays correctly. A character the sign cannot render is rejected with a 400, as is an
311
+ unknown token: a write is told what the sign would have made of it rather than being
312
+ shown something it did not ask for.
216
313
 
217
- ## A simpler set of endpoints
314
+ ### Recovering a sign that has stopped responding
218
315
 
219
- Alongside `/v2` there is a smaller surface: `POST /Write/Message`,
220
- `POST /Write/ControlCommand`, and the `/Enumerations` reads.
316
+ A sign mounted out of reach can wedge: a stray bit corrupts what its decoder is
317
+ showing, it stops responding to writes, and there is no power switch within reach.
318
+ There are two recoveries, and they are not interchangeable. Try the gentle one first.
221
319
 
222
- These follow one convention that `/v2` does not. **Every response is HTTP 200**, with the
223
- outcome in the body:
320
+ **A soft reset restarts the sign and erases nothing.** The sign runs the same power-up
321
+ diagnostics it runs when you plug it in, then carries on showing what it was showing.
322
+ Its memory, its file table and its messages all survive; this was verified on the sign
323
+ by reading them back either side of a reset.
224
324
 
225
- ```json
226
- {"result": "OK", "result_message": "Message displayed on sign"}
227
325
  ```
326
+ curl -X POST http://localhost:5001/sign/command \
327
+ -H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
328
+ -d '{"command": "SOFT_RESET"}'
329
+ ```
330
+
331
+ The call waits out the diagnostics before answering, so a 204 means the sign is
332
+ listening again rather than that the bytes went out.
228
333
 
229
- That suits a client which finds branching on status codes awkward, such as a Home
230
- Assistant `rest_command` or a shell one-liner in a cron job. The exceptions are the
231
- requests that never reach the endpoint at all: a missing or wrong API key is a 401, a
232
- service with no API key configured is a 503, and a body that is not the shape the endpoint
233
- declares gets FastAPI's own 422. A caller the service will not talk to, and a body it
234
- cannot read, are not the same as a request that failed.
334
+ **`POST /sign/reboot` is the escalation, and it is destructive.** It clears the sign
335
+ outright, waits for it to restart, then re-pushes every message and the run sequence
336
+ from the service's own record, so the display still comes back to what it was.
337
+
338
+ ```
339
+ curl -X POST http://localhost:5001/sign/reboot -H 'X-API-Key: YOUR-KEY'
340
+ ```
235
341
 
236
- `POST /Write/Message` writes to one reserved slot, named `default`. It deliberately does
237
- not touch the sign's **priority** file, which by protocol suppresses every other message on
238
- the sign. Written to an ordinary slot it looks identical while it is the only message
239
- registered, and it shares the sign the moment anything else registers.
342
+ Reach for it only when a soft reset was not enough. The sign is blank for about ten
343
+ seconds while it resets. Neither is a way to clear messages: `DELETE /messages` does
344
+ that without resetting anything. The client fronts the reboot with a warning-coloured
345
+ confirmation for the same reason.
240
346
 
241
347
  ## Configuration
242
348
 
@@ -249,8 +355,11 @@ and usually absent, which is not an error. `READERBOARD_CONFIG_FILE` moves the f
249
355
  you want it somewhere other than the default.
250
356
 
251
357
  The sign's address is a full pyserial URL in `serial_url`: `socket://192.168.2.51:4001`
252
- for an Ethernet to RS-232 adapter, `/dev/ttyUSB0` for a cable plugged straight in, or
253
- `loop://` to run the service with no sign attached.
358
+ for an Ethernet to RS-232 adapter, `rfc2217://192.168.2.51:23` for one speaking the
359
+ telnet serial protocol, `/dev/ttyUSB0` or `COM3` for a cable plugged straight in, or
360
+ `loop://` to run the service with no sign attached. There is no slash between the host
361
+ and the port, and pyserial's answer to one that has a slash names neither the setting
362
+ nor the value.
254
363
 
255
364
  Two settings reallocate the sign's memory when changed, and **that erases every message
256
365
  on it**: `slot_count` and `slot_capacity`. The service will do it, and say so loudly in
@@ -259,12 +368,15 @@ the log, but they are not settings to fiddle with.
259
368
  ## Security
260
369
 
261
370
  An API key is required on every write, compared in constant time, and never logged.
262
- Reads and `GET /health` need none, so a monitor can watch the sign without holding a key
263
- that could write to it.
371
+ `GET /sign/information` needs one too: it is a read of the sign itself rather than of the
372
+ service, so it sends a question over the wire, holds the sign until the answer arrives,
373
+ and reports the hardware's firmware and how full its memory is. The service's own reads
374
+ and `GET /health` need none, so a monitor can watch the slots without holding a key that
375
+ could write to them.
264
376
 
265
377
  The key is declared to the API description as a security scheme, so the Swagger UI at
266
- `/docs` has an **Authorize** button: enter the key once and every write on the page
267
- carries it. It is the same `X-API-Key` header a client sends, so nothing about a script
378
+ `/docs` has an **Authorize** button: enter the key once and everything on the page that
379
+ needs it carries it. It is the same `X-API-Key` header a client sends, so nothing about a script
268
380
  or a Home Assistant `rest_command` changes.
269
381
 
270
382
  That page is configured to remember the key, so it survives a reload or a browser
@@ -321,35 +433,46 @@ claim. Read it before changing anything in `readerboard/protocol/`.
321
433
  `scripts/protocol_spike.py` settles the few questions the document cannot answer about
322
434
  this particular sign. It is destructive and refuses to run without `--confirm-erase`.
323
435
 
324
- `tools/signsim/` is a PySide6 stand-in for the sign, described above. Its own tests
325
- are collected by the `pytest` run here and need no Qt installed; the application does,
326
- and it is pinned separately so that nothing the service installs ever pulls Qt in.
327
-
328
- `scripts/run_with_simulator.py` starts the service and the simulator together. Both
329
- editors have it as a launch configuration, "API and sign simulator" in
330
- `.vscode/launch.json` and "API and simulator" in `.idea/runConfigurations/`, along with
331
- configurations for each half on its own.
436
+ `tools/signsim/` is the sign simulator, a PySide6 stand-in for the sign, described
437
+ above. `tools/apiclient/` is the client, a PySide6 application for calling the API by
438
+ hand: every endpoint, responses shown as text rather than JSON, and a vocabulary it
439
+ loads from the service rather than one compiled into it. The tests of both are
440
+ collected by the `pytest` run here and need no Qt installed; the applications do, and
441
+ each is pinned separately so that nothing the service installs ever pulls Qt in.
442
+
443
+ `scripts/run_with_simulator.py` starts the service and the simulator together, and
444
+ with `--with-client` the client as well, so the whole loop comes up from one command.
445
+ Both editors carry it as a launch configuration under the same name, "readerboard and
446
+ the sign simulator", in `.vscode/launch.json` and in `.idea/runConfigurations/`, beside
447
+ configurations for running the pieces separately. Both carry the three way one as
448
+ "readerboard, the sign simulator and the client" as well.
449
+
450
+ `scripts/run_against_a_sign.py` is the other one, for when the sign is real: the
451
+ service and the client, no simulator, and the sign's address passed as an argument so
452
+ that it can be edited in a run configuration dialog. Both editors carry it as
453
+ "readerboard against the real sign and the client". The section above has the rest,
454
+ including the one thing about it that is dangerous. The two launchers share their
455
+ process supervision through `scripts/_supervise.py` and differ in what each child is
456
+ given, which is the part that matters: the simulator launcher discards its state file
457
+ on every run and this one never discards anything.
458
+
459
+ Every one of those that starts the service sets `READERBOARD_OPEN_DOCS`, so `/docs`
460
+ opens in a browser once the port answers. The service does the waiting and the
461
+ opening, which is why it lands on the port actually bound rather than one repeated in
462
+ a launch file. The setting is off unless asked for, so an installed service opens
463
+ nothing.
332
464
 
333
465
  ## Licence
334
466
 
335
467
  MIT. See [LICENSE](https://github.com/mjaksn/readerboard/blob/main/LICENSE).
336
468
 
337
- One caveat, recorded because it is easy to miss.
338
- `readerboard/protocol/constants.py` is vendored from
339
- [jonathankoren/readerboard](https://github.com/jonathankoren/readerboard), and that
340
- repository carries no license file. No license is not the same as a permissive one: it
341
- means no copying permission has been granted at all. That module is therefore the one
342
- part of this project whose provenance is not cleanly MIT.
343
-
344
- In practice it is a table of byte values dictated by the protocol rather than authored
345
- expression, and `docs/protocol-notes.md` now cites the protocol document directly, so the
346
- table can be regenerated from the primary source if that ever needs settling properly.
347
-
348
469
  ## Credits
349
470
 
350
- `readerboard/protocol/constants.py` came, with thanks, from
351
- [jonathankoren/readerboard](https://github.com/jonathankoren/readerboard), with some
352
- corrections noted in the file.
471
+ The protocol is documented in the Alpha Sign Communications Protocol, form 9708-8061,
472
+ published by Adaptive Micro Systems. Every byte value in
473
+ `readerboard/protocol/constants.py` is transcribed from that document, and
474
+ `tests/test_constant_values.py` pins each one against it with a citation per assertion.
353
475
 
354
- The protocol itself is documented in the Alpha Sign Communications Protocol, form
355
- 9708-8061, published by Adaptive Micro Systems.
476
+ An earlier version of this project took that table from
477
+ [jonathankoren/readerboard](https://github.com/jonathankoren/readerboard), which is
478
+ recorded here with thanks even though no code from it remains.