statsbadge-railclock 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.
@@ -0,0 +1,16 @@
1
+ name: CI
2
+
3
+ # Lint, build and test against statsbadge from source, by statsbadge's own rules. The steps
4
+ # are statsbadge's, so every extension is checked the same way.
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ pull_request:
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ check:
16
+ uses: pimoroni/statsbadge/.github/workflows/extension.yml@main
@@ -0,0 +1,38 @@
1
+ name: Publish
2
+
3
+ # Trusted publishing, so PyPI trusts this workflow over OIDC and no API token is stored
4
+ # here. Configure it once at pypi.org/manage/project/statsbadge-railclock/settings/publishing/
5
+ # with this repository, this filename and the `pypi` environment. The build is
6
+ # statsbadge's; the publish has to stay here, since PyPI trusts a file by name.
7
+ on:
8
+ release:
9
+ types: [published]
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ build:
17
+ uses: pimoroni/statsbadge/.github/workflows/extension-build.yml@main
18
+
19
+ publish:
20
+ name: Publish to PyPI
21
+ needs: build
22
+ runs-on: ubuntu-latest
23
+ environment: pypi
24
+ permissions:
25
+ id-token: write # for trusted publishing
26
+
27
+ steps:
28
+ - uses: actions/download-artifact@v8
29
+ with:
30
+ name: dist
31
+ path: dist
32
+
33
+ # setup-uv publishes exact versions only, no major tag
34
+ - uses: astral-sh/setup-uv@v10.2.0
35
+ with:
36
+ version: "0.12.18"
37
+
38
+ - run: uv publish --trusted-publishing always dist/*
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ .ruff_cache/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vincent Willcox
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.
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.5
2
+ Name: statsbadge-railclock
3
+ Version: 0.1.0
4
+ Summary: A Rail Clock page for statsbadge: the time inside two rings of sweeping chevrons
5
+ Project-URL: Homepage, https://github.com/vwillcox/statsbadge-railclock
6
+ Project-URL: Repository, https://github.com/vwillcox/statsbadge-railclock
7
+ Project-URL: Issues, https://github.com/vwillcox/statsbadge-railclock/issues
8
+ Author: Vincent Willcox
9
+ License-Expression: MIT AND OFL-1.1
10
+ License-File: LICENSE
11
+ License-File: src/statsbadge_railclock/OFL-Inter.txt
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: statsbadge>=3.0.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # statsbadge-railclock
17
+
18
+ A Rail Clock page for [statsbadge](https://github.com/pimoroni/statsbadge) on the Tufty
19
+ 2350.
20
+
21
+ ![The Rail Clock on a Tufty 2350, one minute sped up four times](https://raw.githubusercontent.com/vwillcox/statsbadge-railclock/main/shots/railclock.gif)
22
+
23
+ The time sits inside two red rings, and each ring carries one chevron of the double arrow.
24
+ The outer chevron goes round clockwise and the inner one anticlockwise, each taking a
25
+ minute per turn. They meet as the double arrow at the top on the minute, and again at the
26
+ bottom on the half minute.
27
+
28
+ The design follows Network Rail's Rail Clock (Design Bridge and Partners, 2025), as drawn
29
+ by the clock tile of my FireTVDeparture departure board:
30
+
31
+ - **Geometry:** rings of radius 82 and 74.5 in a 200-unit square, with a stroke of 3. The
32
+ chevron arms leave the ring at 27°. The face takes 88% of the page.
33
+ - **Colours:** red `#d0202f` and time `#ececec`, on a `#0b0f14` tile with a `#182230`
34
+ border.
35
+ - **Time:** `HH:MM` in Inter Bold, with Inter's tabular digits and the raised colon it uses
36
+ between digits.
37
+
38
+ The badge keeps time off its own clock, set from the computer's, so the chevrons sweep
39
+ smoothly and keep going if the computer goes away. Until the first time arrives, the page
40
+ reads `--:--`.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ statsbadge ext add railclock
46
+ statsbadge install # badge on USB: puts the page on it
47
+ ```
48
+
49
+ Or type `railclock` into the install box on the config page's **Extensions** tab, then
50
+ run `statsbadge install` with the badge on USB. Then add a **Rail Clock** page. It has no
51
+ settings.
52
+
53
+ ## The font
54
+
55
+ `railclock_digits.af` is made from Inter, which is under the SIL Open Font License 1.1
56
+ (see `src/statsbadge_railclock/OFL-Inter.txt`). To rebuild it from `InterVariable.ttf`:
57
+
58
+ 1. Fix it at weight 700 and optical size 32 with `fontTools.varLib.instancer`.
59
+ 2. Point the digits at their tabular forms (`zero.tf` to `nine.tf`) and `:` at
60
+ `colon.case.tf`.
61
+ 3. Pack it with statsbadge's `tools/make_text_font.py`:
62
+
63
+ ```bash
64
+ uv run --group fonts python tools/make_text_font.py InterRailClock.ttf --wide \
65
+ --chars "0123456789:-H" --out railclock_digits.af
66
+ ```
67
+
68
+ ## Notes
69
+
70
+ This project is not affiliated with or endorsed by Network Rail or National Rail. The
71
+ double arrow symbol is a registered trademark.
72
+
73
+ The code is MIT licensed; the font is OFL-1.1.
@@ -0,0 +1,58 @@
1
+ # statsbadge-railclock
2
+
3
+ A Rail Clock page for [statsbadge](https://github.com/pimoroni/statsbadge) on the Tufty
4
+ 2350.
5
+
6
+ ![The Rail Clock on a Tufty 2350, one minute sped up four times](https://raw.githubusercontent.com/vwillcox/statsbadge-railclock/main/shots/railclock.gif)
7
+
8
+ The time sits inside two red rings, and each ring carries one chevron of the double arrow.
9
+ The outer chevron goes round clockwise and the inner one anticlockwise, each taking a
10
+ minute per turn. They meet as the double arrow at the top on the minute, and again at the
11
+ bottom on the half minute.
12
+
13
+ The design follows Network Rail's Rail Clock (Design Bridge and Partners, 2025), as drawn
14
+ by the clock tile of my FireTVDeparture departure board:
15
+
16
+ - **Geometry:** rings of radius 82 and 74.5 in a 200-unit square, with a stroke of 3. The
17
+ chevron arms leave the ring at 27°. The face takes 88% of the page.
18
+ - **Colours:** red `#d0202f` and time `#ececec`, on a `#0b0f14` tile with a `#182230`
19
+ border.
20
+ - **Time:** `HH:MM` in Inter Bold, with Inter's tabular digits and the raised colon it uses
21
+ between digits.
22
+
23
+ The badge keeps time off its own clock, set from the computer's, so the chevrons sweep
24
+ smoothly and keep going if the computer goes away. Until the first time arrives, the page
25
+ reads `--:--`.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ statsbadge ext add railclock
31
+ statsbadge install # badge on USB: puts the page on it
32
+ ```
33
+
34
+ Or type `railclock` into the install box on the config page's **Extensions** tab, then
35
+ run `statsbadge install` with the badge on USB. Then add a **Rail Clock** page. It has no
36
+ settings.
37
+
38
+ ## The font
39
+
40
+ `railclock_digits.af` is made from Inter, which is under the SIL Open Font License 1.1
41
+ (see `src/statsbadge_railclock/OFL-Inter.txt`). To rebuild it from `InterVariable.ttf`:
42
+
43
+ 1. Fix it at weight 700 and optical size 32 with `fontTools.varLib.instancer`.
44
+ 2. Point the digits at their tabular forms (`zero.tf` to `nine.tf`) and `:` at
45
+ `colon.case.tf`.
46
+ 3. Pack it with statsbadge's `tools/make_text_font.py`:
47
+
48
+ ```bash
49
+ uv run --group fonts python tools/make_text_font.py InterRailClock.ttf --wide \
50
+ --chars "0123456789:-H" --out railclock_digits.af
51
+ ```
52
+
53
+ ## Notes
54
+
55
+ This project is not affiliated with or endorsed by Network Rail or National Rail. The
56
+ double arrow symbol is a registered trademark.
57
+
58
+ The code is MIT licensed; the font is OFL-1.1.
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "statsbadge-railclock"
3
+ dynamic = ["version"]
4
+ description = "A Rail Clock page for statsbadge: the time inside two rings of sweeping chevrons"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Vincent Willcox" },
8
+ ]
9
+ license = "MIT AND OFL-1.1"
10
+ license-files = ["LICENSE", "src/statsbadge_railclock/OFL-Inter.txt"]
11
+ requires-python = ">=3.11"
12
+ dependencies = ["statsbadge>=3.0.0"]
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/vwillcox/statsbadge-railclock"
16
+ Repository = "https://github.com/vwillcox/statsbadge-railclock"
17
+ Issues = "https://github.com/vwillcox/statsbadge-railclock/issues"
18
+
19
+ # The whole extension mechanism is this one entry point:
20
+ [project.entry-points."statsbadge.sources"]
21
+ railclock = "statsbadge_railclock:RailClock"
22
+
23
+ # The tag is the version.
24
+ [tool.hatch.version]
25
+ source = "uv-dynamic-versioning"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/statsbadge_railclock"]
29
+
30
+ [build-system]
31
+ requires = ["hatchling", "uv-dynamic-versioning"]
32
+ build-backend = "hatchling.build"
@@ -0,0 +1,92 @@
1
+ Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+ -----------------------------------------------------------
8
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
9
+ -----------------------------------------------------------
10
+
11
+ PREAMBLE
12
+ The goals of the Open Font License (OFL) are to stimulate worldwide
13
+ development of collaborative font projects, to support the font creation
14
+ efforts of academic and linguistic communities, and to provide a free and
15
+ open framework in which fonts may be shared and improved in partnership
16
+ with others.
17
+
18
+ The OFL allows the licensed fonts to be used, studied, modified and
19
+ redistributed freely as long as they are not sold by themselves. The
20
+ fonts, including any derivative works, can be bundled, embedded,
21
+ redistributed and/or sold with any software provided that any reserved
22
+ names are not used by derivative works. The fonts and derivatives,
23
+ however, cannot be released under any other type of license. The
24
+ requirement for fonts to remain under this license does not apply
25
+ to any document created using the fonts or their derivatives.
26
+
27
+ DEFINITIONS
28
+ "Font Software" refers to the set of files released by the Copyright
29
+ Holder(s) under this license and clearly marked as such. This may
30
+ include source files, build scripts and documentation.
31
+
32
+ "Reserved Font Name" refers to any names specified as such after the
33
+ copyright statement(s).
34
+
35
+ "Original Version" refers to the collection of Font Software components as
36
+ distributed by the Copyright Holder(s).
37
+
38
+ "Modified Version" refers to any derivative made by adding to, deleting,
39
+ or substituting -- in part or in whole -- any of the components of the
40
+ Original Version, by changing formats or by porting the Font Software to a
41
+ new environment.
42
+
43
+ "Author" refers to any designer, engineer, programmer, technical
44
+ writer or other person who contributed to the Font Software.
45
+
46
+ PERMISSION AND CONDITIONS
47
+ Permission is hereby granted, free of charge, to any person obtaining
48
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
49
+ redistribute, and sell modified and unmodified copies of the Font
50
+ Software, subject to the following conditions:
51
+
52
+ 1) Neither the Font Software nor any of its individual components,
53
+ in Original or Modified Versions, may be sold by itself.
54
+
55
+ 2) Original or Modified Versions of the Font Software may be bundled,
56
+ redistributed and/or sold with any software, provided that each copy
57
+ contains the above copyright notice and this license. These can be
58
+ included either as stand-alone text files, human-readable headers or
59
+ in the appropriate machine-readable metadata fields within text or
60
+ binary files as long as those fields can be easily viewed by the user.
61
+
62
+ 3) No Modified Version of the Font Software may use the Reserved Font
63
+ Name(s) unless explicit written permission is granted by the corresponding
64
+ Copyright Holder. This restriction only applies to the primary font name as
65
+ presented to the users.
66
+
67
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
68
+ Software shall not be used to promote, endorse or advertise any
69
+ Modified Version, except to acknowledge the contribution(s) of the
70
+ Copyright Holder(s) and the Author(s) or with their explicit written
71
+ permission.
72
+
73
+ 5) The Font Software, modified or unmodified, in part or in whole,
74
+ must be distributed entirely under this license, and must not be
75
+ distributed under any other license. The requirement for fonts to
76
+ remain under this license does not apply to any document created
77
+ using the Font Software.
78
+
79
+ TERMINATION
80
+ This license becomes null and void if any of the above conditions are
81
+ not met.
82
+
83
+ DISCLAIMER
84
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
85
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
86
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
87
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
88
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
89
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
90
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
91
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
92
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,43 @@
1
+ """The Rail Clock as a statsbadge page.
2
+
3
+ The time inside two red rings, whose chevrons go round a turn a minute in opposite
4
+ directions and meet as the double arrow on the minute and the half minute. The page is
5
+ drawn on the badge, which keeps time off the host's clock; all this side sends is that.
6
+ """
7
+
8
+ import os
9
+ import time
10
+
11
+ from statsbadge.sources.base import Source
12
+
13
+ HERE = os.path.dirname(os.path.abspath(__file__))
14
+
15
+
16
+ class RailClock(Source):
17
+ name = "railclock"
18
+ label = "Rail Clock"
19
+ provides = ("railclock",)
20
+
21
+ # The badge-side code, pushed by `statsbadge install` and imported by the app.
22
+ badge_module = os.path.join(HERE, "badge", "railclock.py")
23
+
24
+ # Inter at weight 700 and optical size 32: its tabular digits, the colon it raises
25
+ # between digits, and a dash for the placeholder, packed --wide.
26
+ badge_assets = (os.path.join(HERE, "badge", "railclock_digits.af"),)
27
+
28
+ badge_page = {
29
+ "kind": "railclock",
30
+ "title": "Rail Clock",
31
+ "fields": [],
32
+ "slots": {},
33
+ }
34
+
35
+ @classmethod
36
+ def available(cls, _config=None):
37
+ return True
38
+
39
+ def sample(self, frame, dt):
40
+ """Send the host's local time, which the badge sets its own clock from."""
41
+ now = time.localtime()
42
+ frame["railclock"] = {"hour": now.tm_hour, "minute": now.tm_min,
43
+ "seconds": now.tm_sec}
@@ -0,0 +1,216 @@
1
+ """The badge side of statsbadge-railclock: the Rail Clock, as a page.
2
+
3
+ After Network Rail's Rail Clock (Design Bridge and Partners, 2025), as the FireTVDeparture
4
+ departure board draws it in a tile: two red rings, each carrying one chevron of the double
5
+ arrow, with the time inside them. The outer chevron goes round clockwise and the inner one
6
+ anticlockwise, a turn a minute each. They meet as the double arrow at the top on the
7
+ minute, and again at the bottom on the half minute.
8
+
9
+ The geometry is the board's, in its 200 unit square, scaled to the tile.
10
+ """
11
+
12
+ import machine
13
+ import math
14
+ import time
15
+
16
+ import draw
17
+ import look
18
+ import pages
19
+
20
+ DIGITS_FONT, DIGITS_FILE = "railclock", "railclock_digits.af"
21
+ # Where `statsbadge install` puts this extension, should __file__ not say.
22
+ EXT_HOME = "/system/apps/stats/ext/railclock/"
23
+
24
+ # The board's stylesheet: the tile in a card's border, the red, the time.
25
+ TILE = (11, 15, 20)
26
+ EDGE = (24, 34, 48)
27
+ RED = (208, 32, 47)
28
+ TIME = (236, 236, 236)
29
+
30
+ # renderRailClock(), in a 200 unit square.
31
+ OUTER = 82.0
32
+ INNER = 74.5
33
+ STROKE = 3.0
34
+ GAP = OUTER - INNER
35
+ MID = (OUTER + INNER) / 2.0
36
+ # The arms leave the ring at 27 degrees, as in Gerry Barney's logo.
37
+ ARM = math.radians(27.0)
38
+ BACK = GAP / math.tan(ARM)
39
+ HALF = STROKE / 2.0 / math.sin(ARM)
40
+ SPAN = math.degrees(BACK / MID)
41
+ # The time: 34 units, Inter Bold, whose capitals stand 1490 of 2048. The packed font's
42
+ # stand 81/128 of the size it is drawn at.
43
+ TIME_UNITS = 34.0
44
+ CAP_RATIO = (1490.0 / 2048.0) / (81.0 / 128.0)
45
+
46
+ # The tile is the page band, with a little of the page showing round it as between the
47
+ # board's cards, and the face is 88% of it.
48
+ INSET = 4
49
+ RADIUS = 8
50
+ FACE = 0.88
51
+
52
+ # A PCF85063A drifts a second or two a day.
53
+ RESYNC_S = 30
54
+ # A gap between draws longer than this is the page coming back on screen, so the frame
55
+ # it is handed may be a host's last, from whenever it went quiet.
56
+ AWAY_MS = 2000
57
+
58
+ _pens = None
59
+ _built = None
60
+ _tried_font = False
61
+
62
+ _synced = False
63
+ _seen_seq = None
64
+ _drawn_at = None
65
+ _phase_second = None
66
+ _phase_at = 0
67
+
68
+
69
+ def _beside(name):
70
+ """Return a file shipped alongside this module, where the installer put both."""
71
+ here = globals().get("__file__") or ""
72
+ return here.rsplit("/", 1)[0] + "/" + name if "/" in here else EXT_HOME + name
73
+
74
+
75
+ def _font():
76
+ global _tried_font
77
+ if not _tried_font:
78
+ _tried_font = True
79
+ draw.add_font(DIGITS_FONT, _beside(DIGITS_FILE), EXT_HOME + DIGITS_FILE)
80
+ return DIGITS_FONT if draw.has_font(DIGITS_FONT) else draw.TEXT
81
+
82
+
83
+ def _colours():
84
+ """Return the pens, made once: a label is cached against its pen."""
85
+ global _pens
86
+ if _pens is None:
87
+ _pens = {"tile": color.rgb(*TILE), "edge": color.rgb(*EDGE),
88
+ "red": color.rgb(*RED), "time": color.rgb(*TIME)}
89
+ return _pens
90
+
91
+
92
+ def _point(radius, degrees):
93
+ """Return a point `radius` from the centre, `degrees` clockwise from twelve."""
94
+ angle = math.radians(degrees)
95
+ return vec2(radius * math.sin(angle), -radius * math.cos(angle))
96
+
97
+
98
+ def _chevron(scale, ring, tip, direction):
99
+ """Return one chevron about the origin: railClockChevron(), bent onto the rings.
100
+
101
+ Built along the direction of travel and across the rings, so the arm crossing the gap
102
+ ends on the other ring's centreline and the opposite arm mirrors it outward.
103
+ """
104
+ local = ((HALF, 0.0), (HALF - BACK, GAP), (-HALF - BACK, GAP), (-HALF, 0.0),
105
+ (-HALF - BACK, -GAP), (HALF - BACK, -GAP))
106
+ return shape.custom([_point((ring + across) * scale,
107
+ tip + direction * math.degrees(along / MID))
108
+ for along, across in local])
109
+
110
+
111
+ def _geometry():
112
+ """Return the tile, the face's centre and scale, and the two chevrons, built once."""
113
+ global _built
114
+ if _built is None:
115
+ top = look.BODY_TOP + INSET // 2
116
+ tile = (INSET, top, look.W - INSET * 2, look.BODY_H - INSET)
117
+ centre = (tile[0] + tile[2] / 2.0, tile[1] + tile[3] / 2.0)
118
+ scale = min(tile[2], tile[3]) * FACE / 200.0
119
+ # Offset by half the chevron's span, so the two crossing arms coincide as the
120
+ # logo whenever the chevrons meet.
121
+ outer = _chevron(scale, OUTER, SPAN / 2.0, 1)
122
+ inner = _chevron(scale, INNER, -SPAN / 2.0, -1)
123
+ _built = (tile, centre, scale, outer, inner)
124
+ return _built
125
+
126
+
127
+ def _seconds():
128
+ """Return the second of the minute off the badge's clock, with its fraction."""
129
+ global _phase_second, _phase_at
130
+ whole = time.localtime()[5]
131
+ now = time.ticks_ms()
132
+ if whole != _phase_second:
133
+ _phase_second = whole
134
+ _phase_at = now
135
+ return whole + min(1.0, time.ticks_diff(now, _phase_at) / 1000.0)
136
+
137
+
138
+ def _take(frame):
139
+ """Set the badge's clock from the host's, when this frame is a fresh reading.
140
+
141
+ Fresh is a frame seen to arrive while the page is up: the last frame of a host that
142
+ has gone away carries the time it went. A page coming on screen with nothing set yet
143
+ takes what it is handed, as the time it has is no better.
144
+ """
145
+ global _seen_seq, _drawn_at
146
+ now = time.ticks_ms()
147
+ back = _drawn_at is None or time.ticks_diff(now, _drawn_at) > AWAY_MS
148
+ _drawn_at = now
149
+ seq = frame.get("seq")
150
+ if back:
151
+ _seen_seq = seq
152
+ if not _synced:
153
+ _resync(frame.get("railclock") or {})
154
+ return
155
+ if seq != _seen_seq:
156
+ _seen_seq = seq
157
+ _resync(frame.get("railclock") or {})
158
+
159
+
160
+ def _resync(clock):
161
+ """Set the badge's clock from a reading: the first, then only past RESYNC_S."""
162
+ global _synced
163
+ hour, minute, second = clock.get("hour"), clock.get("minute"), clock.get("seconds")
164
+ if hour is None or minute is None or second is None:
165
+ return
166
+ parts = time.localtime()
167
+ drift = (hour * 3600 + minute * 60 + second
168
+ - (parts[3] * 3600 + parts[4] * 60 + parts[5]) + 43200) % 86400 - 43200
169
+ if _synced and -RESYNC_S <= drift <= RESYNC_S:
170
+ return
171
+ machine.RTC().datetime((parts[0], parts[1], parts[2], parts[6],
172
+ hour, minute, second, 0))
173
+ _synced = True
174
+
175
+
176
+ def render(_page, frame, _history, _theme):
177
+ pens = _colours()
178
+ font = _font()
179
+ tile, centre, scale, outer, inner = _geometry()
180
+ _take(frame)
181
+
182
+ # The tile: the board's card, on the page.
183
+ screen.pen = pens["edge"]
184
+ screen.shape(shape.rounded_rectangle(rect(*tile), RADIUS))
185
+ screen.pen = pens["tile"]
186
+ screen.shape(shape.rounded_rectangle(
187
+ rect(tile[0] + 1, tile[1] + 1, tile[2] - 2, tile[3] - 2), RADIUS - 1))
188
+
189
+ # The rings, always there.
190
+ screen.pen = pens["red"]
191
+ half = STROKE * scale / 2.0
192
+ for ring in (OUTER, INNER):
193
+ screen.shape(shape.arc(vec2(*centre), ring * scale - half, ring * scale + half,
194
+ 0, 360))
195
+
196
+ # The chevrons, a turn a minute each way, off the badge's own clock.
197
+ turned = _seconds() * 6.0
198
+ outer.transform = mat3().translate(*centre).rotate(turned)
199
+ inner.transform = mat3().translate(*centre).rotate(-turned)
200
+ screen.shape(outer)
201
+ screen.shape(inner)
202
+
203
+ # The time, its capitals centred on the face, as the board's central baseline puts
204
+ # them. The board shows --:-- until its clock has the time.
205
+ if _synced:
206
+ hour, minute = time.localtime()[3:5]
207
+ shown = f"{hour:02d}:{minute:02d}"
208
+ else:
209
+ shown = "--:--"
210
+ size = int(TIME_UNITS * scale * CAP_RATIO + 0.5)
211
+ ink = size * draw.CAP
212
+ y = int(centre[1] - ink / 2.0 - (size - ink) + 0.5)
213
+ draw.blit_label(shown, size, pens["time"], int(centre[0]), y, align=1, name=font)
214
+
215
+
216
+ pages.register("railclock", render, api=1, animated=True)
@@ -0,0 +1,47 @@
1
+ """Checks run by statsbadge's extension workflow: `python tests/test_railclock.py`."""
2
+
3
+ import ast
4
+ import os
5
+
6
+ from statsbadge import badge_api
7
+
8
+ from statsbadge_railclock import RailClock
9
+
10
+
11
+ def test_sends_the_host_time():
12
+ frame = {}
13
+ RailClock({}).sample(frame, 1.0)
14
+ clock = frame["railclock"]
15
+ assert 0 <= clock["hour"] < 24
16
+ assert 0 <= clock["minute"] < 60
17
+ assert 0 <= clock["seconds"] < 62
18
+
19
+
20
+ def test_ships_what_the_badge_needs():
21
+ assert RailClock.available()
22
+ for path in (RailClock.badge_module, *RailClock.badge_assets):
23
+ assert os.path.isfile(path), path
24
+ assert os.path.getsize(path) > 0, path
25
+
26
+
27
+ def test_badge_module_keeps_to_the_badge_api():
28
+ """Only what statsbadge publishes for badge modules, at its current version."""
29
+ source = open(RailClock.badge_module, encoding="utf-8").read()
30
+ assert f"api={badge_api.VERSION}" in source
31
+ for node in ast.walk(ast.parse(source)):
32
+ if not (isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name)):
33
+ continue
34
+ if node.value.id in badge_api.NAMES:
35
+ assert node.attr in badge_api.NAMES[node.value.id], (
36
+ f"{node.value.id}.{node.attr} is not in badge_api")
37
+ imported = {alias.name for node in ast.walk(ast.parse(source))
38
+ if isinstance(node, ast.Import) for alias in node.names}
39
+ app_modules = {"deck", "app", "net", "splash", "setup"}
40
+ assert not imported & app_modules, imported & app_modules
41
+
42
+
43
+ if __name__ == "__main__":
44
+ for name, test in list(globals().items()):
45
+ if name.startswith("test_") and callable(test):
46
+ test()
47
+ print("ok", name)