Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ Trackday or race session timer.
* Added configuration prompts and a complete Ready-screen settings summary.
* Increased the countdown font and added fixed-width timer digits to prevent movement.
* Added a Ready-screen battery gauge with an external-power lightning indicator.
* Expanded hardware-independent regression coverage to 82 tests.
* Expanded hardware-independent regression coverage to 89 tests.
### v3.3
* Smooth proportional font rendering at native display resolution.
* Centered typography and improved layout across timer, configuration, and diagnostic screens.
Expand DownExpand Up@@ -66,7 +66,9 @@ On the supported Waveshare board running MicroPython 1.21.0, five full live-scre

## Startup splash

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.
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.

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.

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:

Expand DownExpand Up@@ -142,7 +144,7 @@ The second command should identify an RP2040 MicroPython board.
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.

```sh
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 :
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 :
mpremote connect auto fs cp main.py :
mpremote connect auto reset
```
Expand All@@ -157,7 +159,7 @@ When upgrading an existing device, omit that command so its saved track duration

### 4. Verify first boot

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.
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.

If first boot fails:

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

## Configuration files

Version 3.2 uses two separate configuration scopes:
Version 3.5 uses two separate configuration scopes:

* `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`.
* `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`.
* `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).

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.
Expand All@@ -193,4 +195,4 @@ Run the hardware-independent regression suite with:
python -m unittest discover -s tests -v
```

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.
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.
4 changes: 2 additions & 2 deletions User Guide.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,8 @@
## General / Sessions Use
The following describes general operation of both the ``Track Session`` and ``Rest in Pits Session`` timer.

* Upon start up a boot splash will be shown for 2 seconds.
* 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``.
* 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`.
* 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``.
* 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.
* While waiting in ``Launch Mode``, double-tap to cancel and return to the ``Primary Screen``. The wait also cancels automatically after 30 seconds.
* Upon starting, the ``Track Session`` timer count down will be displayed, and immediately commence.
Expand Down
144 changes: 144 additions & 0 deletions hardware_splash.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
"""Configurable startup sequence and compact hardware-information screen."""

import time


BOARD_VENDOR = "Waveshare"
BOARD_MODEL = "Touch LCD 1.28"
UNKNOWN_VALUE = "Unknown"

HARDWARE_SPLASH_BACKGROUND = "black"
HARDWARE_SPLASH_TEXT_COLOUR = "white"


class DeviceDetails:
"""Display-safe device metadata collected without requiring hardware."""

def __init__(
self,
firmware,
cpu,
os_name,
os_version,
platform,
vendor=BOARD_VENDOR,
model=BOARD_MODEL,
):
self.vendor = vendor
self.model = model
self.firmware = firmware
self.cpu = cpu
self.os_name = os_name
self.os_version = os_version
self.platform = platform


def _runtime_value(source, attribute):
try:
value = getattr(source, attribute)
except Exception:
return UNKNOWN_VALUE
if value is None:
return UNKNOWN_VALUE
value = str(value).strip()
return value if value else UNKNOWN_VALUE


def _short_value(value, maximum=18):
value = str(value)
if len(value) <= maximum:
return value
return value[: maximum - 1] + "."


def _runtime_name(implementation):
name = _runtime_value(implementation, "name")
known_names = {
"micropython": "MicroPython",
"cpython": "CPython",
}
return known_names.get(name.lower(), _short_value(name))


def _cpu_name(uname_info):
machine = _runtime_value(uname_info, "machine")
if "RP2040" in machine.upper():
return "RP2040"
return _short_value(machine, maximum=12)


def collect_device_details(firmware_version, uname_info=None, implementation=None):
"""Collect normalized details with safe fallbacks for missing fields."""
if uname_info is None:
try:
try:
import uos as os
except ImportError:
import os
uname_info = os.uname()
except Exception:
uname_info = object()

if implementation is None:
try:
import sys
implementation = sys.implementation
except Exception:
implementation = object()

return DeviceDetails(
firmware=_short_value(firmware_version, maximum=10),
cpu=_cpu_name(uname_info),
os_name=_runtime_name(implementation),
os_version=_short_value(
_runtime_value(uname_info, "release"),
maximum=10,
),
platform=_short_value(
_runtime_value(uname_info, "sysname"),
maximum=12,
),
)


def hardware_splash_lines(details):
"""Build a circular-display-safe hardware summary."""
return [
["Hardware", None, 24, 3, HARDWARE_SPLASH_TEXT_COLOUR],
["Board " + details.vendor, None, 70, 1, HARDWARE_SPLASH_TEXT_COLOUR],
["Model " + details.model, None, 94, 1, HARDWARE_SPLASH_TEXT_COLOUR],
["Type " + details.cpu, None, 118, 1, HARDWARE_SPLASH_TEXT_COLOUR],
["Firmware v" + details.firmware, None, 142, 1, HARDWARE_SPLASH_TEXT_COLOUR],
[
"OS " + details.os_name + " " + details.os_version,
None,
166,
1,
HARDWARE_SPLASH_TEXT_COLOUR,
],
["Platform " + details.platform, None, 190, 1, HARDWARE_SPLASH_TEXT_COLOUR],
]


def run_startup_screens(
touch,
lcd,
firmware_version,
startup_duration_sec,
hardware_duration_sec,
clock=time,
details=None,
):
"""Show artwork, then hardware details, for their configured durations."""
touch.BootScreen(lcd, version_number=firmware_version)
clock.sleep(startup_duration_sec)

if details is None:
details = collect_device_details(firmware_version)
touch.ControlScreen(
lcd,
text_array=hardware_splash_lines(details),
back_colour=HARDWARE_SPLASH_BACKGROUND,
)
clock.sleep(hardware_duration_sec)
return details
17 changes: 13 additions & 4 deletions main.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,28 +63,37 @@ def main():
duration_values = system_params["DURATION_VALUES"]
launch_sense_values = system_params["LAUNCH_SENSE_VALUES"]
version = system_params["VERSION"]
boot_delay_sec = system_params["BOOT_DELAY_SEC"]
startup_splash_duration_sec = system_params["STARTUP_SPLASH_DURATION_SEC"]
hardware_splash_duration_sec = system_params["HARDWARE_SPLASH_DURATION_SEC"]
display_delay_rest = system_params["DISPLAY_DELAY_REST"]
display_delay_rest_colour = system_params["DISPLAY_DELAY_REST_COLOUR"]

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

# Display and touchscreen
# Allocate the 115,200-byte framebuffer before importing optional startup
# presentation code. On the RP2040, doing this in the opposite order can
# fragment the heap enough to prevent the single large allocation.
lcd = LCD_1inch28()
from hardware_splash import run_startup_screens
lcd.set_bl_pwm(65535)
battery_monitor = BatteryMonitor()
try:
touch = initialize_with_retry(
lambda: Touch_CST816T(mode=1, LCD=lcd),
"CST816T",
)
touch.BootScreen(lcd, version_number=version)
run_startup_screens(
touch,
lcd,
firmware_version=version,
startup_duration_sec=startup_splash_duration_sec,
hardware_duration_sec=hardware_splash_duration_sec,
)
except PeripheralError as error:
_show_touch_failure(lcd, error)
return False

time.sleep(boot_delay_sec)

qmi8658, imu_error = _initialize_imu(user_params["SENSITIVITY"])
if imu_error is not None:
_show_imu_degraded(lcd, imu_error)
Expand Down
3 changes: 2 additions & 1 deletion params.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,5 +4,6 @@
"LAUNCH_SENSE_VALUES": [0, 0.5, 1, 1.25, 1.5, 1.75, 2, 2.5, 3.5, 4],
"VERSION": "3.5",
"DISPLAY_DELAY_REST_COLOUR": "blue",
"BOOT_DELAY_SEC": 2
"STARTUP_SPLASH_DURATION_SEC": 2,
"HARDWARE_SPLASH_DURATION_SEC": 2
}
36 changes: 25 additions & 11 deletions settings.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,8 @@
"LAUNCH_SENSE_VALUES": [0, 0.5, 1, 1.25, 1.5, 1.75, 2, 2.5, 3.5, 4],
"VERSION": "3.5",
"DISPLAY_DELAY_REST_COLOUR": "blue",
"BOOT_DELAY_SEC": 2,
"STARTUP_SPLASH_DURATION_SEC": 2,
"HARDWARE_SPLASH_DURATION_SEC": 2,
}

DEFAULT_USER_PARAMS = {
Expand DownExpand Up@@ -78,23 +79,36 @@ def validate_system_params(data):
if not isinstance(data, dict):
return _copy_params(DEFAULT_SYSTEM_PARAMS), False

source = dict(data)
if (
"STARTUP_SPLASH_DURATION_SEC" not in source
and "BOOT_DELAY_SEC" in source
):
source["STARTUP_SPLASH_DURATION_SEC"] = source["BOOT_DELAY_SEC"]
if "HARDWARE_SPLASH_DURATION_SEC" not in source:
source["HARDWARE_SPLASH_DURATION_SEC"] = DEFAULT_SYSTEM_PARAMS[
"HARDWARE_SPLASH_DURATION_SEC"
]

valid = (
_positive_int_list(data.get("DURATION_VALUES"))
and _non_negative_number_list(data.get("LAUNCH_SENSE_VALUES"))
and _is_number(data.get("DISPLAY_DELAY_REST"))
and data.get("DISPLAY_DELAY_REST") >= 0
and _is_number(data.get("BOOT_DELAY_SEC"))
and data.get("BOOT_DELAY_SEC") >= 0
and isinstance(data.get("VERSION"), str)
and len(data.get("VERSION")) > 0
and data.get("DISPLAY_DELAY_REST_COLOUR") in DISPLAY_COLOURS
_positive_int_list(source.get("DURATION_VALUES"))
and _non_negative_number_list(source.get("LAUNCH_SENSE_VALUES"))
and _is_number(source.get("DISPLAY_DELAY_REST"))
and source.get("DISPLAY_DELAY_REST") >= 0
and _is_number(source.get("STARTUP_SPLASH_DURATION_SEC"))
and source.get("STARTUP_SPLASH_DURATION_SEC") >= 0
and _is_number(source.get("HARDWARE_SPLASH_DURATION_SEC"))
and source.get("HARDWARE_SPLASH_DURATION_SEC") >= 0
and isinstance(source.get("VERSION"), str)
and len(source.get("VERSION")) > 0
and source.get("DISPLAY_DELAY_REST_COLOUR") in DISPLAY_COLOURS
)
if not valid:
return _copy_params(DEFAULT_SYSTEM_PARAMS), False

params = {}
for key in DEFAULT_SYSTEM_PARAMS:
value = data[key]
value = source[key]
params[key] = list(value) if isinstance(value, list) else value
return params, True

Expand Down
Loading
Loading