Skip to content

Commit ff09280

Browse files
committed
Add configurable hardware startup splash
1 parent 7fce608 commit ff09280

8 files changed

Lines changed: 369 additions & 25 deletions

File tree

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Trackday or race session timer.
1111
* Added configuration prompts and a complete Ready-screen settings summary.
1212
* Increased the countdown font and added fixed-width timer digits to prevent movement.
1313
* Added a Ready-screen battery gauge with an external-power lightning indicator.
14-
* Expanded hardware-independent regression coverage to 82 tests.
14+
* Expanded hardware-independent regression coverage to 89 tests.
1515
### v3.3
1616
* Smooth proportional font rendering at native display resolution.
1717
* Centered typography and improved layout across timer, configuration, and diagnostic screens.
@@ -66,7 +66,9 @@ On the supported Waveshare board running MicroPython 1.21.0, five full live-scre
6666

6767
## Startup splash
6868

69-
At startup, the timer displays the supplied Caterham artwork on a black background sized for the 240x240 round display. The image is stored as a native `startup_splash.rgb565` framebuffer and loaded directly into the LCD's existing buffer, avoiding a second full-screen allocation on the RP2040. If the asset is absent or has the wrong size, the original text splash is shown instead.
69+
Startup now has two consecutive screens. First, the timer displays the supplied Caterham artwork on a black background sized for the 240x240 round display. The image is stored as a native `startup_splash.rgb565` framebuffer and loaded directly into the LCD's existing buffer, avoiding a second full-screen allocation on the RP2040. If the asset is absent or has the wrong size, the original text splash is shown instead.
70+
71+
The second screen uses high-contrast white text on black and identifies the hardware and runtime: board vendor and model, processor type, timer firmware, MicroPython version, and platform. Both screens default to two seconds. Their durations can be tuned independently in `params.json` with `STARTUP_SPLASH_DURATION_SEC` and `HARDWARE_SPLASH_DURATION_SEC`; zero skips the wait while still drawing that screen. Existing installations using `BOOT_DELAY_SEC` automatically apply that value to the first screen and use two seconds for the new hardware screen.
7072

7173
The original artwork and a device-layout preview are kept under `assets/`. To regenerate the runtime asset after changing the source image, install Pillow and run:
7274

@@ -142,7 +144,7 @@ The second command should identify an RP2040 MicroPython board.
142144
Run these commands from the repository root. Supporting files and font assets are copied first; `main.py` is installed last as the automatic entry point.
143145

144146
```sh
145-
mpremote connect auto fs cp battery.py configuration.py font_data.py font_renderer.py hardware.py launch.py lcd_1inch28.py live_display.py params.json qmi8658.py ready_screen.py settings.py splash.py timing.py touch_drive.py font_data*.bin startup_splash.rgb565 :
147+
mpremote connect auto fs cp battery.py configuration.py font_data.py font_renderer.py hardware.py hardware_splash.py launch.py lcd_1inch28.py live_display.py params.json qmi8658.py ready_screen.py settings.py splash.py timing.py touch_drive.py font_data*.bin startup_splash.rgb565 :
146148
mpremote connect auto fs cp main.py :
147149
mpremote connect auto reset
148150
```
@@ -157,7 +159,7 @@ When upgrading an existing device, omit that command so its saved track duration
157159

158160
### 4. Verify first boot
159161

160-
The display should show the Caterham v3.5 splash and then the green **Ready** screen. The serial console should report the loaded user parameters, `Success:Detected CST816T.`, and the touchscreen revision without a traceback.
162+
The display should show the Caterham v3.5 splash, the hardware-information splash, and then the green **Ready** screen. The serial console should report the loaded user parameters, `Success:Detected CST816T.`, and the touchscreen revision without a traceback.
161163

162164
If first boot fails:
163165

@@ -176,9 +178,9 @@ The QMI8658 IMU is optional unless a non-zero Launch Mode sensitivity is selecte
176178

177179
## Configuration files
178180

179-
Version 3.2 uses two separate configuration scopes:
181+
Version 3.5 uses two separate configuration scopes:
180182

181-
* `params.json` contains system-owned choices and display behavior: `DURATION_VALUES`, `LAUNCH_SENSE_VALUES`, `VERSION`, `DISPLAY_DELAY_REST`, `DISPLAY_DELAY_REST_COLOUR`, and `BOOT_DELAY_SEC`.
183+
* `params.json` contains system-owned choices and display behavior: `DURATION_VALUES`, `LAUNCH_SENSE_VALUES`, `VERSION`, `DISPLAY_DELAY_REST`, `DISPLAY_DELAY_REST_COLOUR`, `STARTUP_SPLASH_DURATION_SEC`, and `HARDWARE_SPLASH_DURATION_SEC`.
182184
* `user.json` contains the current user selections: `RACE_LENGTH` (track-session minutes), `REST_LENGTH` (pit-rest minutes), and `SENSITIVITY` (launch threshold; `0` disables Launch Mode).
183185

184186
Launch sensitivity is the filtered change in acceleration-vector magnitude from a 0.4-second stationary baseline, measured in g. This removes gravity and mounting orientation and handles acceleration on either side of every axis. Lower non-zero values are more sensitive. Detection requires three consecutive samples above the threshold; double-tap cancels the wait, and a 30-second timeout returns to the Ready screen. See `User Guide.md` for the practical meaning of every configured value.
@@ -193,4 +195,4 @@ Run the hardware-independent regression suite with:
193195
python -m unittest discover -s tests -v
194196
```
195197

196-
The suite uses fakes for time, touch gestures, display calls, filesystem operations, accelerometer samples, battery readings, and USB power state. Version 3.5 was additionally validated on the supported Waveshare board for boot, LCD/font rendering, CST816S touchscreen detection, QMI8658 initialization, saved settings, launch behavior, and the Ready-screen battery indicator.
198+
The suite uses fakes for time, touch gestures, display calls, filesystem operations, accelerometer samples, battery readings, and USB power state. Version 3.5 was additionally validated on the supported Waveshare board for both startup screens, LCD/font rendering, CST816S touchscreen detection, QMI8658 initialization, saved settings, launch behavior, and the Ready-screen battery indicator.

User Guide.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
## General / Sessions Use
44
The following describes general operation of both the ``Track Session`` and ``Rest in Pits Session`` timer.
55

6-
* Upon start up a boot splash will be shown for 2 seconds.
7-
* After which the ``Primary Screen`` will show ``Ready`` together with the saved track duration, rest duration, and effective Launch Mode state. A battery icon above `Ready` fills from left to right with estimated remaining charge. A lightning bolt through the battery means USB/external power is present. When the timer starts while connected to USB, the initial full fill represents powered status because this board cannot read the isolated battery cell until it runs from battery. ``Launch unavailable`` means the saved non-zero sensitivity could not be used because the IMU is unavailable; normal swipe-down timing still works. To start the ``Track Session`` or race, ``Swipe Down``.
6+
* Upon startup, the Caterham boot artwork is followed by a hardware-information screen showing the board, processor, firmware, operating system, and platform. Each screen is shown for two seconds by default. Maintainers can tune the waits independently with `STARTUP_SPLASH_DURATION_SEC` and `HARDWARE_SPLASH_DURATION_SEC` in `params.json`.
7+
* The ``Primary Screen`` will then show ``Ready`` together with the saved track duration, rest duration, and effective Launch Mode state. A battery icon above `Ready` fills from left to right with estimated remaining charge. A lightning bolt through the battery means USB/external power is present. When the timer starts while connected to USB, the initial full fill represents powered status because this board cannot read the isolated battery cell until it runs from battery. ``Launch unavailable`` means the saved non-zero sensitivity could not be used because the IMU is unavailable; normal swipe-down timing still works. To start the ``Track Session`` or race, ``Swipe Down``.
88
* After swiping down, ``Go`` will display briefly. If ``Launch Mode`` has been activated, ``Lights`` will be displayed while the timer measures a stationary baseline and waits for sufficient acceleration.
99
* While waiting in ``Launch Mode``, double-tap to cancel and return to the ``Primary Screen``. The wait also cancels automatically after 30 seconds.
1010
* Upon starting, the ``Track Session`` timer count down will be displayed, and immediately commence.

hardware_splash.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""Configurable startup sequence and compact hardware-information screen."""
2+
3+
import time
4+
5+
6+
BOARD_VENDOR = "Waveshare"
7+
BOARD_MODEL = "Touch LCD 1.28"
8+
UNKNOWN_VALUE = "Unknown"
9+
10+
HARDWARE_SPLASH_BACKGROUND = "black"
11+
HARDWARE_SPLASH_TEXT_COLOUR = "white"
12+
13+
14+
class DeviceDetails:
15+
"""Display-safe device metadata collected without requiring hardware."""
16+
17+
def __init__(
18+
self,
19+
firmware,
20+
cpu,
21+
os_name,
22+
os_version,
23+
platform,
24+
vendor=BOARD_VENDOR,
25+
model=BOARD_MODEL,
26+
):
27+
self.vendor = vendor
28+
self.model = model
29+
self.firmware = firmware
30+
self.cpu = cpu
31+
self.os_name = os_name
32+
self.os_version = os_version
33+
self.platform = platform
34+
35+
36+
def _runtime_value(source, attribute):
37+
try:
38+
value = getattr(source, attribute)
39+
except Exception:
40+
return UNKNOWN_VALUE
41+
if value is None:
42+
return UNKNOWN_VALUE
43+
value = str(value).strip()
44+
return value if value else UNKNOWN_VALUE
45+
46+
47+
def _short_value(value, maximum=18):
48+
value = str(value)
49+
if len(value) <= maximum:
50+
return value
51+
return value[: maximum - 1] + "."
52+
53+
54+
def _runtime_name(implementation):
55+
name = _runtime_value(implementation, "name")
56+
known_names = {
57+
"micropython": "MicroPython",
58+
"cpython": "CPython",
59+
}
60+
return known_names.get(name.lower(), _short_value(name))
61+
62+
63+
def _cpu_name(uname_info):
64+
machine = _runtime_value(uname_info, "machine")
65+
if "RP2040" in machine.upper():
66+
return "RP2040"
67+
return _short_value(machine, maximum=12)
68+
69+
70+
def collect_device_details(firmware_version, uname_info=None, implementation=None):
71+
"""Collect normalized details with safe fallbacks for missing fields."""
72+
if uname_info is None:
73+
try:
74+
try:
75+
import uos as os
76+
except ImportError:
77+
import os
78+
uname_info = os.uname()
79+
except Exception:
80+
uname_info = object()
81+
82+
if implementation is None:
83+
try:
84+
import sys
85+
implementation = sys.implementation
86+
except Exception:
87+
implementation = object()
88+
89+
return DeviceDetails(
90+
firmware=_short_value(firmware_version, maximum=10),
91+
cpu=_cpu_name(uname_info),
92+
os_name=_runtime_name(implementation),
93+
os_version=_short_value(
94+
_runtime_value(uname_info, "release"),
95+
maximum=10,
96+
),
97+
platform=_short_value(
98+
_runtime_value(uname_info, "sysname"),
99+
maximum=12,
100+
),
101+
)
102+
103+
104+
def hardware_splash_lines(details):
105+
"""Build a circular-display-safe hardware summary."""
106+
return [
107+
["Hardware", None, 24, 3, HARDWARE_SPLASH_TEXT_COLOUR],
108+
["Board " + details.vendor, None, 70, 1, HARDWARE_SPLASH_TEXT_COLOUR],
109+
["Model " + details.model, None, 94, 1, HARDWARE_SPLASH_TEXT_COLOUR],
110+
["Type " + details.cpu, None, 118, 1, HARDWARE_SPLASH_TEXT_COLOUR],
111+
["Firmware v" + details.firmware, None, 142, 1, HARDWARE_SPLASH_TEXT_COLOUR],
112+
[
113+
"OS " + details.os_name + " " + details.os_version,
114+
None,
115+
166,
116+
1,
117+
HARDWARE_SPLASH_TEXT_COLOUR,
118+
],
119+
["Platform " + details.platform, None, 190, 1, HARDWARE_SPLASH_TEXT_COLOUR],
120+
]
121+
122+
123+
def run_startup_screens(
124+
touch,
125+
lcd,
126+
firmware_version,
127+
startup_duration_sec,
128+
hardware_duration_sec,
129+
clock=time,
130+
details=None,
131+
):
132+
"""Show artwork, then hardware details, for their configured durations."""
133+
touch.BootScreen(lcd, version_number=firmware_version)
134+
clock.sleep(startup_duration_sec)
135+
136+
if details is None:
137+
details = collect_device_details(firmware_version)
138+
touch.ControlScreen(
139+
lcd,
140+
text_array=hardware_splash_lines(details),
141+
back_colour=HARDWARE_SPLASH_BACKGROUND,
142+
)
143+
clock.sleep(hardware_duration_sec)
144+
return details

main.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,28 +63,37 @@ def main():
6363
duration_values = system_params["DURATION_VALUES"]
6464
launch_sense_values = system_params["LAUNCH_SENSE_VALUES"]
6565
version = system_params["VERSION"]
66-
boot_delay_sec = system_params["BOOT_DELAY_SEC"]
66+
startup_splash_duration_sec = system_params["STARTUP_SPLASH_DURATION_SEC"]
67+
hardware_splash_duration_sec = system_params["HARDWARE_SPLASH_DURATION_SEC"]
6768
display_delay_rest = system_params["DISPLAY_DELAY_REST"]
6869
display_delay_rest_colour = system_params["DISPLAY_DELAY_REST_COLOUR"]
6970

7071
print("User Parameters: " + str(user_params))
7172

7273
# Display and touchscreen
74+
# Allocate the 115,200-byte framebuffer before importing optional startup
75+
# presentation code. On the RP2040, doing this in the opposite order can
76+
# fragment the heap enough to prevent the single large allocation.
7377
lcd = LCD_1inch28()
78+
from hardware_splash import run_startup_screens
7479
lcd.set_bl_pwm(65535)
7580
battery_monitor = BatteryMonitor()
7681
try:
7782
touch = initialize_with_retry(
7883
lambda: Touch_CST816T(mode=1, LCD=lcd),
7984
"CST816T",
8085
)
81-
touch.BootScreen(lcd, version_number=version)
86+
run_startup_screens(
87+
touch,
88+
lcd,
89+
firmware_version=version,
90+
startup_duration_sec=startup_splash_duration_sec,
91+
hardware_duration_sec=hardware_splash_duration_sec,
92+
)
8293
except PeripheralError as error:
8394
_show_touch_failure(lcd, error)
8495
return False
8596

86-
time.sleep(boot_delay_sec)
87-
8897
qmi8658, imu_error = _initialize_imu(user_params["SENSITIVITY"])
8998
if imu_error is not None:
9099
_show_imu_degraded(lcd, imu_error)

params.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"LAUNCH_SENSE_VALUES": [0, 0.5, 1, 1.25, 1.5, 1.75, 2, 2.5, 3.5, 4],
55
"VERSION": "3.5",
66
"DISPLAY_DELAY_REST_COLOUR": "blue",
7-
"BOOT_DELAY_SEC": 2
7+
"STARTUP_SPLASH_DURATION_SEC": 2,
8+
"HARDWARE_SPLASH_DURATION_SEC": 2
89
}

settings.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"LAUNCH_SENSE_VALUES": [0, 0.5, 1, 1.25, 1.5, 1.75, 2, 2.5, 3.5, 4],
1515
"VERSION": "3.5",
1616
"DISPLAY_DELAY_REST_COLOUR": "blue",
17-
"BOOT_DELAY_SEC": 2,
17+
"STARTUP_SPLASH_DURATION_SEC": 2,
18+
"HARDWARE_SPLASH_DURATION_SEC": 2,
1819
}
1920

2021
DEFAULT_USER_PARAMS = {
@@ -78,23 +79,36 @@ def validate_system_params(data):
7879
if not isinstance(data, dict):
7980
return _copy_params(DEFAULT_SYSTEM_PARAMS), False
8081

82+
source = dict(data)
83+
if (
84+
"STARTUP_SPLASH_DURATION_SEC" not in source
85+
and "BOOT_DELAY_SEC" in source
86+
):
87+
source["STARTUP_SPLASH_DURATION_SEC"] = source["BOOT_DELAY_SEC"]
88+
if "HARDWARE_SPLASH_DURATION_SEC" not in source:
89+
source["HARDWARE_SPLASH_DURATION_SEC"] = DEFAULT_SYSTEM_PARAMS[
90+
"HARDWARE_SPLASH_DURATION_SEC"
91+
]
92+
8193
valid = (
82-
_positive_int_list(data.get("DURATION_VALUES"))
83-
and _non_negative_number_list(data.get("LAUNCH_SENSE_VALUES"))
84-
and _is_number(data.get("DISPLAY_DELAY_REST"))
85-
and data.get("DISPLAY_DELAY_REST") >= 0
86-
and _is_number(data.get("BOOT_DELAY_SEC"))
87-
and data.get("BOOT_DELAY_SEC") >= 0
88-
and isinstance(data.get("VERSION"), str)
89-
and len(data.get("VERSION")) > 0
90-
and data.get("DISPLAY_DELAY_REST_COLOUR") in DISPLAY_COLOURS
94+
_positive_int_list(source.get("DURATION_VALUES"))
95+
and _non_negative_number_list(source.get("LAUNCH_SENSE_VALUES"))
96+
and _is_number(source.get("DISPLAY_DELAY_REST"))
97+
and source.get("DISPLAY_DELAY_REST") >= 0
98+
and _is_number(source.get("STARTUP_SPLASH_DURATION_SEC"))
99+
and source.get("STARTUP_SPLASH_DURATION_SEC") >= 0
100+
and _is_number(source.get("HARDWARE_SPLASH_DURATION_SEC"))
101+
and source.get("HARDWARE_SPLASH_DURATION_SEC") >= 0
102+
and isinstance(source.get("VERSION"), str)
103+
and len(source.get("VERSION")) > 0
104+
and source.get("DISPLAY_DELAY_REST_COLOUR") in DISPLAY_COLOURS
91105
)
92106
if not valid:
93107
return _copy_params(DEFAULT_SYSTEM_PARAMS), False
94108

95109
params = {}
96110
for key in DEFAULT_SYSTEM_PARAMS:
97-
value = data[key]
111+
value = source[key]
98112
params[key] = list(value) if isinstance(value, list) else value
99113
return params, True
100114

0 commit comments

Comments
 (0)