twobells 0.1.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.
- twobells-0.1.0/LICENSE +21 -0
- twobells-0.1.0/PKG-INFO +228 -0
- twobells-0.1.0/README.md +194 -0
- twobells-0.1.0/pyproject.toml +97 -0
- twobells-0.1.0/setup.cfg +4 -0
- twobells-0.1.0/src/twobells/__init__.py +1 -0
- twobells-0.1.0/src/twobells/__main__.py +8 -0
- twobells-0.1.0/src/twobells/board/__init__.py +1 -0
- twobells-0.1.0/src/twobells/board/app.py +581 -0
- twobells-0.1.0/src/twobells/board/displays/__init__.py +1 -0
- twobells-0.1.0/src/twobells/board/displays/display.py +94 -0
- twobells-0.1.0/src/twobells/board/displays/screen.py +150 -0
- twobells-0.1.0/src/twobells/board/displays/waveshare.py +239 -0
- twobells-0.1.0/src/twobells/board/dotfont.py +187 -0
- twobells-0.1.0/src/twobells/board/fonts/Inter-ExtraBold.ttf +0 -0
- twobells-0.1.0/src/twobells/board/fonts/Inter-Regular.ttf +0 -0
- twobells-0.1.0/src/twobells/board/fonts/LICENSE +92 -0
- twobells-0.1.0/src/twobells/board/fonts/SHA256SUMS +3 -0
- twobells-0.1.0/src/twobells/board/panel.py +84 -0
- twobells-0.1.0/src/twobells/board/positions.py +149 -0
- twobells-0.1.0/src/twobells/board/render.py +852 -0
- twobells-0.1.0/src/twobells/board/route.py +131 -0
- twobells-0.1.0/src/twobells/board/scroll.py +137 -0
- twobells-0.1.0/src/twobells/board/sign.py +592 -0
- twobells-0.1.0/src/twobells/board/state.py +105 -0
- twobells-0.1.0/src/twobells/board/themes.py +63 -0
- twobells-0.1.0/src/twobells/board/view.py +268 -0
- twobells-0.1.0/src/twobells/cli/__init__.py +1 -0
- twobells-0.1.0/src/twobells/cli/common.py +94 -0
- twobells-0.1.0/src/twobells/cli/dev.py +266 -0
- twobells-0.1.0/src/twobells/cli/main.py +196 -0
- twobells-0.1.0/src/twobells/cli/run.py +283 -0
- twobells-0.1.0/src/twobells/cli/serve.py +439 -0
- twobells-0.1.0/src/twobells/cli/setup.py +114 -0
- twobells-0.1.0/src/twobells/cli/timetable.py +55 -0
- twobells-0.1.0/src/twobells/core/__init__.py +1 -0
- twobells-0.1.0/src/twobells/core/budget.py +34 -0
- twobells-0.1.0/src/twobells/core/errors.py +29 -0
- twobells-0.1.0/src/twobells/core/files.py +80 -0
- twobells-0.1.0/src/twobells/core/model.py +164 -0
- twobells-0.1.0/src/twobells/core/net.py +265 -0
- twobells-0.1.0/src/twobells/core/osgb.py +118 -0
- twobells-0.1.0/src/twobells/core/polling.py +80 -0
- twobells-0.1.0/src/twobells/core/safexml.py +130 -0
- twobells-0.1.0/src/twobells/core/walk.py +43 -0
- twobells-0.1.0/src/twobells/core/words.py +54 -0
- twobells-0.1.0/src/twobells/dashboard/__init__.py +1 -0
- twobells-0.1.0/src/twobells/dashboard/area.py +164 -0
- twobells-0.1.0/src/twobells/dashboard/collector.py +621 -0
- twobells-0.1.0/src/twobells/dashboard/config.py +389 -0
- twobells-0.1.0/src/twobells/dashboard/enrich.py +257 -0
- twobells-0.1.0/src/twobells/dashboard/history.py +440 -0
- twobells-0.1.0/src/twobells/dashboard/snapshot.py +732 -0
- twobells-0.1.0/src/twobells/dashboard_static/alerts.js +136 -0
- twobells-0.1.0/src/twobells/dashboard_static/api.js +129 -0
- twobells-0.1.0/src/twobells/dashboard_static/app.css +1234 -0
- twobells-0.1.0/src/twobells/dashboard_static/area.js +372 -0
- twobells-0.1.0/src/twobells/dashboard_static/board.js +507 -0
- twobells-0.1.0/src/twobells/dashboard_static/disruptions.js +312 -0
- twobells-0.1.0/src/twobells/dashboard_static/dom.js +176 -0
- twobells-0.1.0/src/twobells/dashboard_static/extras.js +89 -0
- twobells-0.1.0/src/twobells/dashboard_static/fonts/DepartureboardDots.woff2 +0 -0
- twobells-0.1.0/src/twobells/dashboard_static/fonts/LICENSE +28 -0
- twobells-0.1.0/src/twobells/dashboard_static/fonts/SHA256SUMS +2 -0
- twobells-0.1.0/src/twobells/dashboard_static/header.js +76 -0
- twobells-0.1.0/src/twobells/dashboard_static/hero.js +203 -0
- twobells-0.1.0/src/twobells/dashboard_static/history.js +69 -0
- twobells-0.1.0/src/twobells/dashboard_static/icons/apple-touch-icon.png +0 -0
- twobells-0.1.0/src/twobells/dashboard_static/icons/icon-192.png +0 -0
- twobells-0.1.0/src/twobells/dashboard_static/icons/icon-512.png +0 -0
- twobells-0.1.0/src/twobells/dashboard_static/icons/icon.svg +10 -0
- twobells-0.1.0/src/twobells/dashboard_static/icons.js +65 -0
- twobells-0.1.0/src/twobells/dashboard_static/index.html +180 -0
- twobells-0.1.0/src/twobells/dashboard_static/kiosk.js +89 -0
- twobells-0.1.0/src/twobells/dashboard_static/live.js +105 -0
- twobells-0.1.0/src/twobells/dashboard_static/main.js +173 -0
- twobells-0.1.0/src/twobells/dashboard_static/manifest.webmanifest +16 -0
- twobells-0.1.0/src/twobells/dashboard_static/map.js +204 -0
- twobells-0.1.0/src/twobells/dashboard_static/sheet.js +129 -0
- twobells-0.1.0/src/twobells/dashboard_static/strip.js +105 -0
- twobells-0.1.0/src/twobells/dashboard_static/theme.js +16 -0
- twobells-0.1.0/src/twobells/dashboard_static/time.js +76 -0
- twobells-0.1.0/src/twobells/dashboard_static/train.js +153 -0
- twobells-0.1.0/src/twobells/dashboard_static/vendor/inter-4.1/InterVariable.woff2 +0 -0
- twobells-0.1.0/src/twobells/dashboard_static/vendor/inter-4.1/LICENSE +92 -0
- twobells-0.1.0/src/twobells/dashboard_static/vendor/inter-4.1/SHA256SUMS +2 -0
- twobells-0.1.0/src/twobells/dashboard_static/vendor/leaflet-1.9.4/LICENSE +26 -0
- twobells-0.1.0/src/twobells/dashboard_static/vendor/leaflet-1.9.4/SHA256SUMS +3 -0
- twobells-0.1.0/src/twobells/dashboard_static/vendor/leaflet-1.9.4/leaflet-src.esm.js +14419 -0
- twobells-0.1.0/src/twobells/dashboard_static/vendor/leaflet-1.9.4/leaflet.css +661 -0
- twobells-0.1.0/src/twobells/dashboard_static/weather.js +181 -0
- twobells-0.1.0/src/twobells/data/gb_stations.csv +2644 -0
- twobells-0.1.0/src/twobells/data/naptan_area_grid.csv +3248 -0
- twobells-0.1.0/src/twobells/emulator/__init__.py +1 -0
- twobells-0.1.0/src/twobells/emulator/emulation.py +156 -0
- twobells-0.1.0/src/twobells/emulator/emulator.html +392 -0
- twobells-0.1.0/src/twobells/emulator/fake.py +313 -0
- twobells-0.1.0/src/twobells/emulator/server.py +175 -0
- twobells-0.1.0/src/twobells/fetch/__init__.py +1 -0
- twobells-0.1.0/src/twobells/fetch/handoff.py +352 -0
- twobells-0.1.0/src/twobells/fetch/service.py +209 -0
- twobells-0.1.0/src/twobells/settings/__init__.py +1 -0
- twobells-0.1.0/src/twobells/settings/config.py +617 -0
- twobells-0.1.0/src/twobells/settings/installs.py +99 -0
- twobells-0.1.0/src/twobells/settings/keyresults.py +135 -0
- twobells-0.1.0/src/twobells/settings/overlay.py +283 -0
- twobells-0.1.0/src/twobells/settings/schema.py +661 -0
- twobells-0.1.0/src/twobells/settings/store.py +312 -0
- twobells-0.1.0/src/twobells/setup/__init__.py +1 -0
- twobells-0.1.0/src/twobells/setup/api.py +1037 -0
- twobells-0.1.0/src/twobells/setup/discover.py +696 -0
- twobells-0.1.0/src/twobells/setup/keytest.py +420 -0
- twobells-0.1.0/src/twobells/setup/lookups.py +665 -0
- twobells-0.1.0/src/twobells/setup/spool/__init__.py +83 -0
- twobells-0.1.0/src/twobells/setup/spool/checks.py +346 -0
- twobells-0.1.0/src/twobells/setup/spool/folder.py +143 -0
- twobells-0.1.0/src/twobells/setup/spool/runner.py +256 -0
- twobells-0.1.0/src/twobells/setup/spool/worker.py +393 -0
- twobells-0.1.0/src/twobells/sources/__init__.py +1 -0
- twobells-0.1.0/src/twobells/sources/bods.py +167 -0
- twobells-0.1.0/src/twobells/sources/bodsbus.py +262 -0
- twobells-0.1.0/src/twobells/sources/darwin.py +761 -0
- twobells-0.1.0/src/twobells/sources/disruptions.py +701 -0
- twobells-0.1.0/src/twobells/sources/holidays.py +280 -0
- twobells-0.1.0/src/twobells/sources/livebus.py +422 -0
- twobells-0.1.0/src/twobells/sources/transportapi.py +169 -0
- twobells-0.1.0/src/twobells/sources/weather.py +367 -0
- twobells-0.1.0/src/twobells/timetable/__init__.py +1 -0
- twobells-0.1.0/src/twobells/timetable/build.py +1491 -0
- twobells-0.1.0/src/twobells/timetable/model.py +432 -0
- twobells-0.1.0/src/twobells/timetable/roads.py +276 -0
- twobells-0.1.0/src/twobells/timetable/routesetup.py +417 -0
- twobells-0.1.0/src/twobells/version.py +50 -0
- twobells-0.1.0/src/twobells/web/__init__.py +1 -0
- twobells-0.1.0/src/twobells/web/auth.py +919 -0
- twobells-0.1.0/src/twobells/web/formpages.py +606 -0
- twobells-0.1.0/src/twobells/web/groupform.py +769 -0
- twobells-0.1.0/src/twobells/web/icons.py +53 -0
- twobells-0.1.0/src/twobells/web/keyspage.py +375 -0
- twobells-0.1.0/src/twobells/web/keystate.py +147 -0
- twobells-0.1.0/src/twobells/web/netguard.py +122 -0
- twobells-0.1.0/src/twobells/web/overview.py +555 -0
- twobells-0.1.0/src/twobells/web/places.py +76 -0
- twobells-0.1.0/src/twobells/web/server.py +532 -0
- twobells-0.1.0/src/twobells/web/settings.js +180 -0
- twobells-0.1.0/src/twobells/web/settingsite.py +616 -0
- twobells-0.1.0/src/twobells/web/status.py +69 -0
- twobells-0.1.0/src/twobells/web/web.css +384 -0
- twobells-0.1.0/src/twobells/web/windowrows.py +252 -0
- twobells-0.1.0/src/twobells/wizard_static/api.js +62 -0
- twobells-0.1.0/src/twobells/wizard_static/finish.js +197 -0
- twobells-0.1.0/src/twobells/wizard_static/home.js +120 -0
- twobells-0.1.0/src/twobells/wizard_static/index.html +241 -0
- twobells-0.1.0/src/twobells/wizard_static/keys.js +151 -0
- twobells-0.1.0/src/twobells/wizard_static/maps.js +105 -0
- twobells-0.1.0/src/twobells/wizard_static/roads.js +99 -0
- twobells-0.1.0/src/twobells/wizard_static/say.js +50 -0
- twobells-0.1.0/src/twobells/wizard_static/state.js +51 -0
- twobells-0.1.0/src/twobells/wizard_static/station.js +145 -0
- twobells-0.1.0/src/twobells/wizard_static/steps.js +80 -0
- twobells-0.1.0/src/twobells/wizard_static/stops.js +165 -0
- twobells-0.1.0/src/twobells/wizard_static/views.js +43 -0
- twobells-0.1.0/src/twobells/wizard_static/walk.js +121 -0
- twobells-0.1.0/src/twobells/wizard_static/wizard.css +335 -0
- twobells-0.1.0/src/twobells/wizard_static/wizard.js +144 -0
- twobells-0.1.0/src/twobells.egg-info/PKG-INFO +228 -0
- twobells-0.1.0/src/twobells.egg-info/SOURCES.txt +171 -0
- twobells-0.1.0/src/twobells.egg-info/dependency_links.txt +1 -0
- twobells-0.1.0/src/twobells.egg-info/entry_points.txt +2 -0
- twobells-0.1.0/src/twobells.egg-info/requires.txt +7 -0
- twobells-0.1.0/src/twobells.egg-info/top_level.txt +1 -0
- twobells-0.1.0/tests/test_deploy.py +1211 -0
- twobells-0.1.0/tests/test_version.py +76 -0
twobells-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Albert Cornelissen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
twobells-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: twobells
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Two Bells: know when to leave. Train and bus departures for Great Britain, on an e-paper or HDMI board on a Raspberry Pi and on a web dashboard, installed on a Pi, in Docker or on a desktop
|
|
5
|
+
Author: Albert Cornelissen
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/acornelissen/twobells
|
|
8
|
+
Project-URL: Source, https://github.com/acornelissen/twobells
|
|
9
|
+
Project-URL: Issues, https://github.com/acornelissen/twobells/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/acornelissen/twobells/blob/main/docs/user-guide.md
|
|
11
|
+
Keywords: departures,trains,buses,national-rail,bods,e-paper,raspberry-pi,dashboard
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Home Automation
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: Pillow>=10.1
|
|
29
|
+
Provides-Extra: pi
|
|
30
|
+
Requires-Dist: inky>=2.0; extra == "pi"
|
|
31
|
+
Provides-Extra: screen
|
|
32
|
+
Requires-Dist: pygame>=2.5; extra == "screen"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# Two Bells
|
|
36
|
+
|
|
37
|
+
**Know when to leave.** Live trains and buses for your station and stops in Great Britain, with the walk already worked out: a departure board by the door and a dashboard on your phone, from one small app you run at home.
|
|
38
|
+
|
|
39
|
+
[](https://github.com/acornelissen/twobells/actions/workflows/ci.yml)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
|
|
42
|
+
<picture>
|
|
43
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/dashboard-dark.png">
|
|
44
|
+
<img alt="The dashboard on a Glossop morning: Leave now for the 07:09 341 bus, then the 07:12 train, with the weather, a disruption at your stop, and the train and bus departures" src="docs/images/dashboard-light.png">
|
|
45
|
+
</picture>
|
|
46
|
+
|
|
47
|
+
One install is one app with two views of the same departures:
|
|
48
|
+
|
|
49
|
+
- **The board:** a Raspberry Pi with a four-colour Inky pHAT (250x122). The app also draws for a Waveshare 10.85" e-Paper (G) (1360x480), a Waveshare 7.9" HDMI screen (1280x400, on its side) and an 11.3" HDMI bar screen (1920x440); those are tested in software but not yet on the hardware.
|
|
50
|
+
- **The dashboard:** a web page for a phone or computer at home, with the next 10 trains and buses, calling points, bus strips and maps, the weather, disruptions and punctuality history.
|
|
51
|
+
|
|
52
|
+
A Pi can run either view or both; Docker and a desktop run the dashboard. Each install has one settings site, at `/settings` on port 8090, with one password and a setup wizard that finds your station, stops, walk times and bus companies from your postcode. The examples use Glossop, with trains to Manchester and buses from two nearby stops (`examples/glossop.toml`).
|
|
53
|
+
|
|
54
|
+
## Why it's worth a place by your door
|
|
55
|
+
|
|
56
|
+
- **It tells you when to leave, not just when it goes.** Every train and bus comes with your walk to its stop, so the page says *Leave now*, *Leave in 4 min* or *Go now*, and can remind you a minute before.
|
|
57
|
+
- **Live trains, properly.** National Rail Darwin gives the platform, coaches, delays and their reasons, every calling point, and where the train is right now.
|
|
58
|
+
- **Live buses, for free, all day.** Worked out the way [bustimes.org](https://bustimes.org) does it, from the BODS timetable plus live bus positions. A strip under each bus shows how many stops away it is.
|
|
59
|
+
- **The things that change your plan.** Disruptions on your journeys, rain on its way, a new timetable starting soon, and which trains are *often late*.
|
|
60
|
+
- **Yours, at home.** It runs on a Pi, a NAS or a laptop on your own network, with free API keys and no cloud account. The Pi's web server never connects to the internet, and releases are signed.
|
|
61
|
+
- **Easy to read for everyone.** It is built to WCAG 2.2 AA, for the keyboard and screen readers, and always says what it means in words. Colour only backs the words up.
|
|
62
|
+
|
|
63
|
+
## The dashboard
|
|
64
|
+
|
|
65
|
+
The next trains and buses side by side, each with its walk, its countdown and where it is now:
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
Open a train for its whole journey, or the area map for every bus on your lines:
|
|
70
|
+
|
|
71
|
+
<p>
|
|
72
|
+
<img width="40%" alt="The 07:12 to Manchester Piccadilly: expected 07:19 after a broken down train, leave by 07:09, arriving 07:55, and its journey with the train at Hadfield" src="docs/images/dashboard-train.png">
|
|
73
|
+
<img width="59%" alt="The area map of Glossop with three live buses on their routes past Henry Street and St Marys Road, and the same buses listed in words" src="docs/images/dashboard-map.png">
|
|
74
|
+
</p>
|
|
75
|
+
|
|
76
|
+
Where there is room, the line runs across the page with each train placed on it:
|
|
77
|
+
|
|
78
|
+

|
|
79
|
+
|
|
80
|
+
Light, dark, or the amber Board look, at any size from a phone to a wall tablet (`/?kiosk`):
|
|
81
|
+
|
|
82
|
+

|
|
83
|
+
|
|
84
|
+
## The board
|
|
85
|
+
|
|
86
|
+
An e-paper board needs no attention: it redraws only when what it shows changes. It comes in two looks: `sign`, an amber dot-matrix station board whose font was drawn for this project one pixel per LED, and `plain`, black on white with a red accent.
|
|
87
|
+
|
|
88
|
+

|
|
89
|
+
|
|
90
|
+
On the 10.85" e-paper there is room for platforms (as the app draws it; not yet run on the panel itself):
|
|
91
|
+
|
|
92
|
+

|
|
93
|
+
|
|
94
|
+
An HDMI screen redraws every second, with a running clock and countdowns, and a page showing where your next bus is (again as the app draws it, not yet tried on a real screen):
|
|
95
|
+
|
|
96
|
+

|
|
97
|
+
|
|
98
|
+
## Set up in a few minutes
|
|
99
|
+
|
|
100
|
+
No config files to write. Give the setup wizard your postcode or address: it finds your nearest stations and bus stops, works out the walk to each along real streets, finds your bus companies and builds their timetable.
|
|
101
|
+
|
|
102
|
+
<p>
|
|
103
|
+
<img width="49%" alt="Setup step 2: the five nearest stations with their distances, Glossop chosen, and trains filtered to those calling at Manchester (MAN)" src="docs/images/setup-station.png">
|
|
104
|
+
<img width="49%" alt="Setup step 4: walk times to Glossop station and two bus stops, each worked out along streets and paths" src="docs/images/setup-walk.png">
|
|
105
|
+
</p>
|
|
106
|
+
|
|
107
|
+
Everything else is on the settings site, in plain words, behind one password:
|
|
108
|
+
|
|
109
|
+
<img width="49%" alt="The settings site's Dashboard page: position strips on or off, and quiet hours by day" src="docs/images/settings-dashboard.png">
|
|
110
|
+
|
|
111
|
+
## What it covers
|
|
112
|
+
|
|
113
|
+
New here? The [user guide](docs/user-guide.md) explains what everything on the board and the dashboard means, how to install it, how to use the settings, and how to look after it. [Getting your keys](docs/keys.md) covers the free keys it needs.
|
|
114
|
+
|
|
115
|
+
- **Where it works:** trains anywhere in Great Britain (National Rail Darwin). Bus times and positions from BODS cover England only. The rain warning works anywhere. Bank holidays follow England and Wales, Scotland or Northern Ireland.
|
|
116
|
+
- **Trains:** National Rail Darwin, via the Rail Data Marketplace. Free.
|
|
117
|
+
- **Buses:** worked out the way [bustimes.org](https://bustimes.org) does, from the DfT's Bus Open Data Service (BODS) timetable for your stops plus live bus positions, free and checked all day. On the board, TransportAPI (free plan: 30 hits a day) can stand in while there is no usable timetable, or be the only source.
|
|
118
|
+
- **E-paper** takes about 20 s to refresh, so the board redraws only when what it shows changes, and never more than once every 2 minutes. An HDMI screen redraws every second, with a running clock, countdowns and a page showing where the next bus is.
|
|
119
|
+
- **Two looks** for the board: `sign`, an amber dot-matrix station board (its font drawn for this project at one pixel per LED), and `plain`, black on white with a red accent.
|
|
120
|
+
- **Meaning is always in text** (`On time`, `Exp 07:49`, `Delayed`, `Cancelled`, `No report`, `sch`, `Not updating`). Colour only reinforces it, so both views work for colour-blind viewers, and the board works on a mono panel.
|
|
121
|
+
|
|
122
|
+
## Install
|
|
123
|
+
|
|
124
|
+
- **Raspberry Pi** (the board, and the dashboard if you like): download a release tarball and its `SHA256SUMS`, check it with `sha256sum --check --ignore-missing SHA256SUMS`, unpack it and run `sudo ./deploy/pi/install.sh`. Nothing is piped from the internet into `sudo bash`. Then open `http://<pi name>.local:8090/settings`. `sudo ./deploy/pi/upgrade.sh VERSION` moves it to another release, checked the same way. See [On a Raspberry Pi](docs/user-guide.md#on-a-raspberry-pi).
|
|
125
|
+
- **Docker** (a NAS, say; the dashboard): copy the folder, copy `deploy/secrets.env.example` to `secrets.env`, and `docker compose up -d --build`. The container is `twobells`, keeping its data in the `twobells-data` volume. See [Docker](docs/user-guide.md#9-docker). From the first release, `deploy/docker/docker-compose.ghcr.yml` runs the published, signed image instead; [Docker on other NASes and on Linux](docs/docker.md) covers QNAP, Unraid, TrueNAS SCALE, and Docker or Podman.
|
|
126
|
+
- **Desktop** (a Mac or Linux laptop; the dashboard): `uv tool install twobells` or `pipx install twobells` (from PyPI from the first release), then `twobells serve`. `deploy/desktop` has a launchd agent and a systemd user service to start it at login. See [Desktop](docs/user-guide.md#10-desktop).
|
|
127
|
+
|
|
128
|
+
No `config.toml` is needed: the setup wizard saves your settings in the install's store. `examples/config.example.toml` lists everything that can be set by hand.
|
|
129
|
+
|
|
130
|
+
The settings password is claimed with a code shown on the board, or put in the log where there is no board. Or set it from the command line; in Docker:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
docker exec -it twobells python -m twobells serve --set-password --state-dir /data
|
|
134
|
+
docker exec -it twobells python -m twobells serve --reset-code --state-dir /data
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
## Releases
|
|
139
|
+
|
|
140
|
+
CI runs ruff, the tests on Python 3.11, 3.12 and 3.13, and builds the wheel and the container (amd64, arm64) on every push and pull request. A tag `vX.Y.Z` matching `pyproject.toml`'s version publishes a release: the package on PyPI, a signed container image on GHCR (`ghcr.io/acornelissen/twobells`), and a source tarball for the Pi with `SHA256SUMS`, signed with Sigstore. `twobells --version` prints the installed version. See `.github/workflows/release.yml` for how to verify the signatures.
|
|
141
|
+
|
|
142
|
+
## Develop on your laptop
|
|
143
|
+
|
|
144
|
+
With [mise](https://mise.jdx.dev) (it pins python, ruff, node and uv in `mise.toml`):
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
mise install # the pinned tools
|
|
148
|
+
mise run setup # .venv with the package and its dev tools, as uv.lock pins them
|
|
149
|
+
mise run test
|
|
150
|
+
mise run lint # ruff
|
|
151
|
+
mise run typecheck # pyright
|
|
152
|
+
mise run demo # sample board to preview.png, no keys
|
|
153
|
+
mise run emulate # the browser panel emulator on sample data
|
|
154
|
+
mise run serve # the dashboard and settings, with ./config.toml and ./secrets.env
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) lists the checks a change must pass.
|
|
158
|
+
|
|
159
|
+
Without mise: `uv sync && uv run pytest`.
|
|
160
|
+
|
|
161
|
+
Then, with the venv active (`. .venv/bin/activate`, or any shell where mise is activated):
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
twobells demo --png preview.png # sample data, no keys
|
|
165
|
+
twobells demo --png big.png --display waveshare-10in85g # or hdmi-1280x400, hdmi-1920x440
|
|
166
|
+
twobells run --png preview.png --config config.toml --secrets secrets.env
|
|
167
|
+
twobells serve --config config.toml --secrets secrets.env # the dashboard and settings
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Emulator
|
|
171
|
+
|
|
172
|
+
A browser page that draws the panel at true proportions and logs every tick, so you can see when the board redraws and when it leaves the panel alone. E-paper panels show in e-paper colours and play the slow colour refresh; the HDMI screen swaps frames at once and ticks every second.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
twobells emulate --demo # fake data, 1 simulated minute every 2 s
|
|
176
|
+
twobells emulate --fake-train \
|
|
177
|
+
--config config.toml --secrets secrets.env # fake trains, live buses
|
|
178
|
+
twobells emulate --config config.toml --secrets secrets.env # all live
|
|
179
|
+
twobells emulate --demo --display hdmi-1280x400 # another panel
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
It opens <http://127.0.0.1:8780/>, or the next free port (`--port` picks another; `--no-browser` leaves the browser alone). Live modes emulate the `display` in your config; `--display` overrides it. Choose realistic (20 s), quick or instant refresh, and the scale (1× to 4× for the pHAT, ½× or 1× for the bigger panels); 1× is the panel's real pixel count. `--tick SECONDS` sets the tick (e-paper: 2 in the demo, 60 live; the screen: 1), and `--accent red` or `yellow` the demo's colour. The demo drops the bus feed from 07:30 to 07:52 simulated time, so the "Not updating" badge shows up.
|
|
183
|
+
|
|
184
|
+
On start the emulator fetches every source once, even outside its windows, so the board fills straight away; after that it polls only inside the windows. Bus times from the BODS timetable cost nothing. Where TransportAPI gives them (`source = "transportapi"`, or standing in while there is no timetable), a live start costs one hit per bus stop; the daily budget still applies, and when it is spent the first fetch is skipped too. `twobells run` does the same on its very first run, when there is no saved state yet. Live modes keep state in the store, so the TransportAPI budget holds across restarts.
|
|
185
|
+
|
|
186
|
+
## The dashboard's API
|
|
187
|
+
|
|
188
|
+
The server and the dashboard page talk through a read-only JSON and SSE API, specified in [docs/dashboard-api.md](docs/dashboard-api.md); the page is vanilla JS modules with no build step, under a strict CSP.
|
|
189
|
+
|
|
190
|
+
## Layout
|
|
191
|
+
|
|
192
|
+
One package, `src/twobells`, a folder per part of the app. Each folder's `__init__.py` says what it holds.
|
|
193
|
+
|
|
194
|
+
| Folder | Role |
|
|
195
|
+
|---|---|
|
|
196
|
+
| `cli/` | The commands, a module each (`run`, `serve`, `timetable`, `setup`, and `dev` for `demo`, `emulate`, `probe` and `route`), and the parser in `cli/main.py` (see the [user guide](docs/user-guide.md#11-commands)). `__main__.py` makes it `python -m twobells` |
|
|
197
|
+
| `core/` | Shared building blocks: the provider-neutral `Departure` and lateness maths (`model.py`), when to poll (`polling.py`), the daily hit cap (`budget.py`), walk times (`walk.py`), the HTTPS helper (`net.py`), files written whole or not at all, and lock files (`files.py`), XML that refuses a DOCTYPE (`safexml.py`), grid references (`osgb.py`), and short, key-free error messages (`errors.py`) |
|
|
198
|
+
| `settings/` | The store, the one folder an install saves in, layered over `config.toml` (`store.py`); every saved setting, by group and settings page, and the board's defaults (`schema.py`); loading them into the board's sources (`config.py`); the kinds of install and what differs between them (`installs.py`); saving a settings page's or the wizard's changes (`overlay.py`); bringing the old dashboard's folder in, once (`migrate.py`) |
|
|
199
|
+
| `sources/` | Where departures and the rest come from, with HTTP injected so parsing is tested with fixtures: Darwin (`darwin.py`), the BODS bus source (`bodsbus.py`: timetable plus live positions, with TransportAPI standing in) and its matching (`livebus.py`), BODS positions (`bods.py`), TransportAPI (`transportapi.py`), disruptions (`disruptions.py`), weather (`weather.py`) and bank holidays (`holidays.py`) |
|
|
200
|
+
| `timetable/` | The BODS bus timetable: built from TransXChange for your stops (`build.py`), kept between builds (`model.py`), the roads between stops for the dashboard's maps (`roads.py`), and route strips for the screen (`routesetup.py`, the `route` command) |
|
|
201
|
+
| `board/` | The board view: one tick, redrawn only on change (`app.py`), departures to rows (`view.py`), the two looks (`themes.py`, `sign.py` and `dotfont.py`, `render.py` with Inter from `fonts/`), panel sizes (`panel.py`), route strips (`route.py`), state between runs (`state.py`) |
|
|
202
|
+
| `board/displays/` | Inky or PNG output (`display.py`), the Waveshare 10.85" e-Paper (G) driver (`waveshare.py`), and the HDMI screen with pygame (`screen.py`) |
|
|
203
|
+
| `dashboard/` | The dashboard view: its settings (`config.py`), pollers (`collector.py`), the versioned snapshot the page shows (`snapshot.py`), row extras (`enrich.py`), the area map (`area.py`), punctuality history in SQLite (`history.py`) |
|
|
204
|
+
| `web/` | The one web server on port 8090 (`server.py`): the dashboard's page, files and API, and the settings site under `/settings` (`settingsite.py`, the Overview's `overview.py` and the names it shows from `places.py`, a `groupform.py` form per page, `formpages.py`, `web.css`, `icons.py`, and `settings.js`, which marks unsaved changes), sign-in and the home-network checks (`auth.py`), reading the board's state for the Overview (`status.py`) |
|
|
205
|
+
| `setup/` | First-run setup: the wizard's JSON API (`api.py`), finding home, stations, stops and walk times (`lookups.py`), finding the bus companies and building their timetable (`discover.py`), key tests (`keytest.py`) |
|
|
206
|
+
| `setup/spool/` | On a Pi, the wizard's internet work handed from the web server to the fetch service through files in memory: the web server's side (`runner.py`), the fetch service's (`worker.py`), the folder (`folder.py`) and the checks on what each side writes (`checks.py`) |
|
|
207
|
+
| `fetch/` | The Pi's outbound-only fetch service (`service.py`, the `fetch` command), and the dashboard's snapshot handed to the web server as a file (`handoff.py`) |
|
|
208
|
+
| `emulator/` | The browser panel emulator (`server.py`, `emulator.html`), its demo and live wiring (`emulation.py`), and the fake sources and clock (`fake.py`) |
|
|
209
|
+
| `dashboard_static/` | The dashboard's page: HTML, CSS, JS modules, vendored Leaflet 1.9.4 |
|
|
210
|
+
| `wizard_static/` | The setup wizard's page: HTML, CSS, a JS module per step. It sits beside `dashboard_static/` because it uses the dashboard's `dom.js`, `icons.js`, Leaflet and Inter by relative path, which must resolve the same on disk as when served |
|
|
211
|
+
| `data/` | Stations and NaPTAN areas for setup, built by `tools/build_setup_data.py` |
|
|
212
|
+
| `version.py` | The installed version, for `--version` |
|
|
213
|
+
|
|
214
|
+
Tests mirror the package: `tests/<folder>/test_<module>.py`, with `tests/test_deploy.py` keeping the deploy files and their docs in step. `deploy/` holds the Pi's units and scripts (`pi/`), the published image's compose file and the image's pinned requirements (`docker/`), the desktop's login files (`desktop/`), the Unraid template (`unraid/`) and `secrets.env.example`; `Dockerfile` and `docker-compose.yml` stay at the top, where `docker compose` looks.
|
|
215
|
+
|
|
216
|
+
To add a provider, implement `fetch() -> list[Departure]` with a `name` and add it to `settings/config.py`.
|
|
217
|
+
|
|
218
|
+
## Security
|
|
219
|
+
|
|
220
|
+
Found a vulnerability? Please report it privately, as [SECURITY.md](SECURITY.md) says.
|
|
221
|
+
|
|
222
|
+
## Licence
|
|
223
|
+
|
|
224
|
+
MIT: see [LICENSE](LICENSE). The data the app shows comes under its providers' own terms.
|
|
225
|
+
|
|
226
|
+
## Data credits
|
|
227
|
+
|
|
228
|
+
See [NOTICE.md](NOTICE.md) for every data source and licence.
|
twobells-0.1.0/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Two Bells
|
|
2
|
+
|
|
3
|
+
**Know when to leave.** Live trains and buses for your station and stops in Great Britain, with the walk already worked out: a departure board by the door and a dashboard on your phone, from one small app you run at home.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/acornelissen/twobells/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
<picture>
|
|
9
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/dashboard-dark.png">
|
|
10
|
+
<img alt="The dashboard on a Glossop morning: Leave now for the 07:09 341 bus, then the 07:12 train, with the weather, a disruption at your stop, and the train and bus departures" src="docs/images/dashboard-light.png">
|
|
11
|
+
</picture>
|
|
12
|
+
|
|
13
|
+
One install is one app with two views of the same departures:
|
|
14
|
+
|
|
15
|
+
- **The board:** a Raspberry Pi with a four-colour Inky pHAT (250x122). The app also draws for a Waveshare 10.85" e-Paper (G) (1360x480), a Waveshare 7.9" HDMI screen (1280x400, on its side) and an 11.3" HDMI bar screen (1920x440); those are tested in software but not yet on the hardware.
|
|
16
|
+
- **The dashboard:** a web page for a phone or computer at home, with the next 10 trains and buses, calling points, bus strips and maps, the weather, disruptions and punctuality history.
|
|
17
|
+
|
|
18
|
+
A Pi can run either view or both; Docker and a desktop run the dashboard. Each install has one settings site, at `/settings` on port 8090, with one password and a setup wizard that finds your station, stops, walk times and bus companies from your postcode. The examples use Glossop, with trains to Manchester and buses from two nearby stops (`examples/glossop.toml`).
|
|
19
|
+
|
|
20
|
+
## Why it's worth a place by your door
|
|
21
|
+
|
|
22
|
+
- **It tells you when to leave, not just when it goes.** Every train and bus comes with your walk to its stop, so the page says *Leave now*, *Leave in 4 min* or *Go now*, and can remind you a minute before.
|
|
23
|
+
- **Live trains, properly.** National Rail Darwin gives the platform, coaches, delays and their reasons, every calling point, and where the train is right now.
|
|
24
|
+
- **Live buses, for free, all day.** Worked out the way [bustimes.org](https://bustimes.org) does it, from the BODS timetable plus live bus positions. A strip under each bus shows how many stops away it is.
|
|
25
|
+
- **The things that change your plan.** Disruptions on your journeys, rain on its way, a new timetable starting soon, and which trains are *often late*.
|
|
26
|
+
- **Yours, at home.** It runs on a Pi, a NAS or a laptop on your own network, with free API keys and no cloud account. The Pi's web server never connects to the internet, and releases are signed.
|
|
27
|
+
- **Easy to read for everyone.** It is built to WCAG 2.2 AA, for the keyboard and screen readers, and always says what it means in words. Colour only backs the words up.
|
|
28
|
+
|
|
29
|
+
## The dashboard
|
|
30
|
+
|
|
31
|
+
The next trains and buses side by side, each with its walk, its countdown and where it is now:
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
Open a train for its whole journey, or the area map for every bus on your lines:
|
|
36
|
+
|
|
37
|
+
<p>
|
|
38
|
+
<img width="40%" alt="The 07:12 to Manchester Piccadilly: expected 07:19 after a broken down train, leave by 07:09, arriving 07:55, and its journey with the train at Hadfield" src="docs/images/dashboard-train.png">
|
|
39
|
+
<img width="59%" alt="The area map of Glossop with three live buses on their routes past Henry Street and St Marys Road, and the same buses listed in words" src="docs/images/dashboard-map.png">
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
Where there is room, the line runs across the page with each train placed on it:
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
Light, dark, or the amber Board look, at any size from a phone to a wall tablet (`/?kiosk`):
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
## The board
|
|
51
|
+
|
|
52
|
+
An e-paper board needs no attention: it redraws only when what it shows changes. It comes in two looks: `sign`, an amber dot-matrix station board whose font was drawn for this project one pixel per LED, and `plain`, black on white with a red accent.
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
On the 10.85" e-paper there is room for platforms (as the app draws it; not yet run on the panel itself):
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
An HDMI screen redraws every second, with a running clock and countdowns, and a page showing where your next bus is (again as the app draws it, not yet tried on a real screen):
|
|
61
|
+
|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
## Set up in a few minutes
|
|
65
|
+
|
|
66
|
+
No config files to write. Give the setup wizard your postcode or address: it finds your nearest stations and bus stops, works out the walk to each along real streets, finds your bus companies and builds their timetable.
|
|
67
|
+
|
|
68
|
+
<p>
|
|
69
|
+
<img width="49%" alt="Setup step 2: the five nearest stations with their distances, Glossop chosen, and trains filtered to those calling at Manchester (MAN)" src="docs/images/setup-station.png">
|
|
70
|
+
<img width="49%" alt="Setup step 4: walk times to Glossop station and two bus stops, each worked out along streets and paths" src="docs/images/setup-walk.png">
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
Everything else is on the settings site, in plain words, behind one password:
|
|
74
|
+
|
|
75
|
+
<img width="49%" alt="The settings site's Dashboard page: position strips on or off, and quiet hours by day" src="docs/images/settings-dashboard.png">
|
|
76
|
+
|
|
77
|
+
## What it covers
|
|
78
|
+
|
|
79
|
+
New here? The [user guide](docs/user-guide.md) explains what everything on the board and the dashboard means, how to install it, how to use the settings, and how to look after it. [Getting your keys](docs/keys.md) covers the free keys it needs.
|
|
80
|
+
|
|
81
|
+
- **Where it works:** trains anywhere in Great Britain (National Rail Darwin). Bus times and positions from BODS cover England only. The rain warning works anywhere. Bank holidays follow England and Wales, Scotland or Northern Ireland.
|
|
82
|
+
- **Trains:** National Rail Darwin, via the Rail Data Marketplace. Free.
|
|
83
|
+
- **Buses:** worked out the way [bustimes.org](https://bustimes.org) does, from the DfT's Bus Open Data Service (BODS) timetable for your stops plus live bus positions, free and checked all day. On the board, TransportAPI (free plan: 30 hits a day) can stand in while there is no usable timetable, or be the only source.
|
|
84
|
+
- **E-paper** takes about 20 s to refresh, so the board redraws only when what it shows changes, and never more than once every 2 minutes. An HDMI screen redraws every second, with a running clock, countdowns and a page showing where the next bus is.
|
|
85
|
+
- **Two looks** for the board: `sign`, an amber dot-matrix station board (its font drawn for this project at one pixel per LED), and `plain`, black on white with a red accent.
|
|
86
|
+
- **Meaning is always in text** (`On time`, `Exp 07:49`, `Delayed`, `Cancelled`, `No report`, `sch`, `Not updating`). Colour only reinforces it, so both views work for colour-blind viewers, and the board works on a mono panel.
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
- **Raspberry Pi** (the board, and the dashboard if you like): download a release tarball and its `SHA256SUMS`, check it with `sha256sum --check --ignore-missing SHA256SUMS`, unpack it and run `sudo ./deploy/pi/install.sh`. Nothing is piped from the internet into `sudo bash`. Then open `http://<pi name>.local:8090/settings`. `sudo ./deploy/pi/upgrade.sh VERSION` moves it to another release, checked the same way. See [On a Raspberry Pi](docs/user-guide.md#on-a-raspberry-pi).
|
|
91
|
+
- **Docker** (a NAS, say; the dashboard): copy the folder, copy `deploy/secrets.env.example` to `secrets.env`, and `docker compose up -d --build`. The container is `twobells`, keeping its data in the `twobells-data` volume. See [Docker](docs/user-guide.md#9-docker). From the first release, `deploy/docker/docker-compose.ghcr.yml` runs the published, signed image instead; [Docker on other NASes and on Linux](docs/docker.md) covers QNAP, Unraid, TrueNAS SCALE, and Docker or Podman.
|
|
92
|
+
- **Desktop** (a Mac or Linux laptop; the dashboard): `uv tool install twobells` or `pipx install twobells` (from PyPI from the first release), then `twobells serve`. `deploy/desktop` has a launchd agent and a systemd user service to start it at login. See [Desktop](docs/user-guide.md#10-desktop).
|
|
93
|
+
|
|
94
|
+
No `config.toml` is needed: the setup wizard saves your settings in the install's store. `examples/config.example.toml` lists everything that can be set by hand.
|
|
95
|
+
|
|
96
|
+
The settings password is claimed with a code shown on the board, or put in the log where there is no board. Or set it from the command line; in Docker:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
docker exec -it twobells python -m twobells serve --set-password --state-dir /data
|
|
100
|
+
docker exec -it twobells python -m twobells serve --reset-code --state-dir /data
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## Releases
|
|
105
|
+
|
|
106
|
+
CI runs ruff, the tests on Python 3.11, 3.12 and 3.13, and builds the wheel and the container (amd64, arm64) on every push and pull request. A tag `vX.Y.Z` matching `pyproject.toml`'s version publishes a release: the package on PyPI, a signed container image on GHCR (`ghcr.io/acornelissen/twobells`), and a source tarball for the Pi with `SHA256SUMS`, signed with Sigstore. `twobells --version` prints the installed version. See `.github/workflows/release.yml` for how to verify the signatures.
|
|
107
|
+
|
|
108
|
+
## Develop on your laptop
|
|
109
|
+
|
|
110
|
+
With [mise](https://mise.jdx.dev) (it pins python, ruff, node and uv in `mise.toml`):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
mise install # the pinned tools
|
|
114
|
+
mise run setup # .venv with the package and its dev tools, as uv.lock pins them
|
|
115
|
+
mise run test
|
|
116
|
+
mise run lint # ruff
|
|
117
|
+
mise run typecheck # pyright
|
|
118
|
+
mise run demo # sample board to preview.png, no keys
|
|
119
|
+
mise run emulate # the browser panel emulator on sample data
|
|
120
|
+
mise run serve # the dashboard and settings, with ./config.toml and ./secrets.env
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) lists the checks a change must pass.
|
|
124
|
+
|
|
125
|
+
Without mise: `uv sync && uv run pytest`.
|
|
126
|
+
|
|
127
|
+
Then, with the venv active (`. .venv/bin/activate`, or any shell where mise is activated):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
twobells demo --png preview.png # sample data, no keys
|
|
131
|
+
twobells demo --png big.png --display waveshare-10in85g # or hdmi-1280x400, hdmi-1920x440
|
|
132
|
+
twobells run --png preview.png --config config.toml --secrets secrets.env
|
|
133
|
+
twobells serve --config config.toml --secrets secrets.env # the dashboard and settings
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Emulator
|
|
137
|
+
|
|
138
|
+
A browser page that draws the panel at true proportions and logs every tick, so you can see when the board redraws and when it leaves the panel alone. E-paper panels show in e-paper colours and play the slow colour refresh; the HDMI screen swaps frames at once and ticks every second.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
twobells emulate --demo # fake data, 1 simulated minute every 2 s
|
|
142
|
+
twobells emulate --fake-train \
|
|
143
|
+
--config config.toml --secrets secrets.env # fake trains, live buses
|
|
144
|
+
twobells emulate --config config.toml --secrets secrets.env # all live
|
|
145
|
+
twobells emulate --demo --display hdmi-1280x400 # another panel
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
It opens <http://127.0.0.1:8780/>, or the next free port (`--port` picks another; `--no-browser` leaves the browser alone). Live modes emulate the `display` in your config; `--display` overrides it. Choose realistic (20 s), quick or instant refresh, and the scale (1× to 4× for the pHAT, ½× or 1× for the bigger panels); 1× is the panel's real pixel count. `--tick SECONDS` sets the tick (e-paper: 2 in the demo, 60 live; the screen: 1), and `--accent red` or `yellow` the demo's colour. The demo drops the bus feed from 07:30 to 07:52 simulated time, so the "Not updating" badge shows up.
|
|
149
|
+
|
|
150
|
+
On start the emulator fetches every source once, even outside its windows, so the board fills straight away; after that it polls only inside the windows. Bus times from the BODS timetable cost nothing. Where TransportAPI gives them (`source = "transportapi"`, or standing in while there is no timetable), a live start costs one hit per bus stop; the daily budget still applies, and when it is spent the first fetch is skipped too. `twobells run` does the same on its very first run, when there is no saved state yet. Live modes keep state in the store, so the TransportAPI budget holds across restarts.
|
|
151
|
+
|
|
152
|
+
## The dashboard's API
|
|
153
|
+
|
|
154
|
+
The server and the dashboard page talk through a read-only JSON and SSE API, specified in [docs/dashboard-api.md](docs/dashboard-api.md); the page is vanilla JS modules with no build step, under a strict CSP.
|
|
155
|
+
|
|
156
|
+
## Layout
|
|
157
|
+
|
|
158
|
+
One package, `src/twobells`, a folder per part of the app. Each folder's `__init__.py` says what it holds.
|
|
159
|
+
|
|
160
|
+
| Folder | Role |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `cli/` | The commands, a module each (`run`, `serve`, `timetable`, `setup`, and `dev` for `demo`, `emulate`, `probe` and `route`), and the parser in `cli/main.py` (see the [user guide](docs/user-guide.md#11-commands)). `__main__.py` makes it `python -m twobells` |
|
|
163
|
+
| `core/` | Shared building blocks: the provider-neutral `Departure` and lateness maths (`model.py`), when to poll (`polling.py`), the daily hit cap (`budget.py`), walk times (`walk.py`), the HTTPS helper (`net.py`), files written whole or not at all, and lock files (`files.py`), XML that refuses a DOCTYPE (`safexml.py`), grid references (`osgb.py`), and short, key-free error messages (`errors.py`) |
|
|
164
|
+
| `settings/` | The store, the one folder an install saves in, layered over `config.toml` (`store.py`); every saved setting, by group and settings page, and the board's defaults (`schema.py`); loading them into the board's sources (`config.py`); the kinds of install and what differs between them (`installs.py`); saving a settings page's or the wizard's changes (`overlay.py`); bringing the old dashboard's folder in, once (`migrate.py`) |
|
|
165
|
+
| `sources/` | Where departures and the rest come from, with HTTP injected so parsing is tested with fixtures: Darwin (`darwin.py`), the BODS bus source (`bodsbus.py`: timetable plus live positions, with TransportAPI standing in) and its matching (`livebus.py`), BODS positions (`bods.py`), TransportAPI (`transportapi.py`), disruptions (`disruptions.py`), weather (`weather.py`) and bank holidays (`holidays.py`) |
|
|
166
|
+
| `timetable/` | The BODS bus timetable: built from TransXChange for your stops (`build.py`), kept between builds (`model.py`), the roads between stops for the dashboard's maps (`roads.py`), and route strips for the screen (`routesetup.py`, the `route` command) |
|
|
167
|
+
| `board/` | The board view: one tick, redrawn only on change (`app.py`), departures to rows (`view.py`), the two looks (`themes.py`, `sign.py` and `dotfont.py`, `render.py` with Inter from `fonts/`), panel sizes (`panel.py`), route strips (`route.py`), state between runs (`state.py`) |
|
|
168
|
+
| `board/displays/` | Inky or PNG output (`display.py`), the Waveshare 10.85" e-Paper (G) driver (`waveshare.py`), and the HDMI screen with pygame (`screen.py`) |
|
|
169
|
+
| `dashboard/` | The dashboard view: its settings (`config.py`), pollers (`collector.py`), the versioned snapshot the page shows (`snapshot.py`), row extras (`enrich.py`), the area map (`area.py`), punctuality history in SQLite (`history.py`) |
|
|
170
|
+
| `web/` | The one web server on port 8090 (`server.py`): the dashboard's page, files and API, and the settings site under `/settings` (`settingsite.py`, the Overview's `overview.py` and the names it shows from `places.py`, a `groupform.py` form per page, `formpages.py`, `web.css`, `icons.py`, and `settings.js`, which marks unsaved changes), sign-in and the home-network checks (`auth.py`), reading the board's state for the Overview (`status.py`) |
|
|
171
|
+
| `setup/` | First-run setup: the wizard's JSON API (`api.py`), finding home, stations, stops and walk times (`lookups.py`), finding the bus companies and building their timetable (`discover.py`), key tests (`keytest.py`) |
|
|
172
|
+
| `setup/spool/` | On a Pi, the wizard's internet work handed from the web server to the fetch service through files in memory: the web server's side (`runner.py`), the fetch service's (`worker.py`), the folder (`folder.py`) and the checks on what each side writes (`checks.py`) |
|
|
173
|
+
| `fetch/` | The Pi's outbound-only fetch service (`service.py`, the `fetch` command), and the dashboard's snapshot handed to the web server as a file (`handoff.py`) |
|
|
174
|
+
| `emulator/` | The browser panel emulator (`server.py`, `emulator.html`), its demo and live wiring (`emulation.py`), and the fake sources and clock (`fake.py`) |
|
|
175
|
+
| `dashboard_static/` | The dashboard's page: HTML, CSS, JS modules, vendored Leaflet 1.9.4 |
|
|
176
|
+
| `wizard_static/` | The setup wizard's page: HTML, CSS, a JS module per step. It sits beside `dashboard_static/` because it uses the dashboard's `dom.js`, `icons.js`, Leaflet and Inter by relative path, which must resolve the same on disk as when served |
|
|
177
|
+
| `data/` | Stations and NaPTAN areas for setup, built by `tools/build_setup_data.py` |
|
|
178
|
+
| `version.py` | The installed version, for `--version` |
|
|
179
|
+
|
|
180
|
+
Tests mirror the package: `tests/<folder>/test_<module>.py`, with `tests/test_deploy.py` keeping the deploy files and their docs in step. `deploy/` holds the Pi's units and scripts (`pi/`), the published image's compose file and the image's pinned requirements (`docker/`), the desktop's login files (`desktop/`), the Unraid template (`unraid/`) and `secrets.env.example`; `Dockerfile` and `docker-compose.yml` stay at the top, where `docker compose` looks.
|
|
181
|
+
|
|
182
|
+
To add a provider, implement `fetch() -> list[Departure]` with a `name` and add it to `settings/config.py`.
|
|
183
|
+
|
|
184
|
+
## Security
|
|
185
|
+
|
|
186
|
+
Found a vulnerability? Please report it privately, as [SECURITY.md](SECURITY.md) says.
|
|
187
|
+
|
|
188
|
+
## Licence
|
|
189
|
+
|
|
190
|
+
MIT: see [LICENSE](LICENSE). The data the app shows comes under its providers' own terms.
|
|
191
|
+
|
|
192
|
+
## Data credits
|
|
193
|
+
|
|
194
|
+
See [NOTICE.md](NOTICE.md) for every data source and licence.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "twobells"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Two Bells: know when to leave. Train and bus departures for Great Britain, on an e-paper or HDMI board on a Raspberry Pi and on a web dashboard, installed on a Pi, in Docker or on a desktop"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Albert Cornelissen" }]
|
|
14
|
+
keywords = ["departures", "trains", "buses", "national-rail", "bods", "e-paper", "raspberry-pi", "dashboard"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Web Environment",
|
|
18
|
+
"Intended Audience :: End Users/Desktop",
|
|
19
|
+
"Natural Language :: English",
|
|
20
|
+
"Operating System :: MacOS",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Home Automation",
|
|
28
|
+
"Topic :: Utilities",
|
|
29
|
+
]
|
|
30
|
+
dependencies = ["Pillow>=10.1"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/acornelissen/twobells"
|
|
34
|
+
Source = "https://github.com/acornelissen/twobells"
|
|
35
|
+
Issues = "https://github.com/acornelissen/twobells/issues"
|
|
36
|
+
Documentation = "https://github.com/acornelissen/twobells/blob/main/docs/user-guide.md"
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
pi = ["inky>=2.0"]
|
|
40
|
+
screen = ["pygame>=2.5"]
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
# The tools for working on the board (uv sync installs them; uv.lock pins them).
|
|
44
|
+
# pygame is here for the screen's tests; fonttools builds the vendored fonts
|
|
45
|
+
# (tools/build_fonts.py) and the Board look's dot-matrix face (tools/build_dotfont.py).
|
|
46
|
+
dev = [
|
|
47
|
+
"pytest>=8",
|
|
48
|
+
"pygame>=2.5",
|
|
49
|
+
"fonttools[woff]>=4.55",
|
|
50
|
+
"ruff==0.16.10",
|
|
51
|
+
"pyright==1.1.406",
|
|
52
|
+
"pytest-xdist>=3.6",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[project.scripts]
|
|
56
|
+
twobells = "twobells.cli.main:main"
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.packages.find]
|
|
59
|
+
where = ["src"]
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.package-data]
|
|
62
|
+
"twobells" = ["data/*.csv", "dashboard_static/**/*", "wizard_static/*"]
|
|
63
|
+
"twobells.board" = ["fonts/*"]
|
|
64
|
+
"twobells.emulator" = ["*.html"]
|
|
65
|
+
"twobells.web" = ["*.css", "*.js"]
|
|
66
|
+
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
testpaths = ["tests"]
|
|
69
|
+
pythonpath = ["src"]
|
|
70
|
+
|
|
71
|
+
[tool.ruff]
|
|
72
|
+
line-length = 120
|
|
73
|
+
target-version = "py311"
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint]
|
|
76
|
+
select = ["E", "F", "W", "B", "A", "N", "BLE", "I", "UP", "SIM", "C4", "RUF", "S"]
|
|
77
|
+
ignore = [
|
|
78
|
+
"N818", # exception names without an Error suffix are public API; renaming them is churn
|
|
79
|
+
"RUF001", # typographic quotes and symbols in user-facing text are deliberate
|
|
80
|
+
"RUF059", # unused names in unpacking say what each part is
|
|
81
|
+
"UP042", # a StrEnum's str() differs from a (str, Enum)'s; not worth the behaviour change
|
|
82
|
+
"SIM108", # an if/else block often reads better than a long or nested conditional expression
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint.per-file-ignores]
|
|
86
|
+
# Tests keep long fixture strings and asserts on one line, and use `l` for a
|
|
87
|
+
# live row in short comprehensions.
|
|
88
|
+
# Tests assert, use made-up keys and passwords, and bind and fetch freely; their
|
|
89
|
+
# regexes are full of braces, which % keeps readable where an f-string can't.
|
|
90
|
+
"tests/**" = ["E501", "E741", "S", "UP031"]
|
|
91
|
+
|
|
92
|
+
[tool.pyright]
|
|
93
|
+
include = ["src"]
|
|
94
|
+
typeCheckingMode = "basic"
|
|
95
|
+
pythonVersion = "3.11"
|
|
96
|
+
venvPath = "."
|
|
97
|
+
venv = ".venv"
|
twobells-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Train and bus departures for GB, on an e-paper or HDMI board and a web dashboard."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The board view: what it shows and how each look draws it."""
|