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
36 changes: 26 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# Track Session Timer - v3.5
# Track Session Timer - v4.0.0
Trackday or race session timer.

# Change log
## Version 4.0
### v4.0.0 [current]
* Added a hold-to-open operating-mode menu with persistent Timer and G modes.
* Added a responsive graphical live/peak G meter with a high-visibility peak arc.
* Added saved display-brightness control and confirmed restoration of defaults.
* Expanded hardware-independent regression coverage to 116 tests.
## Version 3.0
### v3.5 [current]
### v3.5
* Added the Caterham startup splash with a safe text fallback.
* Made Launch Mode orientation-independent, filtered, cancellable, and timeout-safe.
* Added controlled touch/IMU failure handling and normal-timer degraded operation.
Expand Down Expand Up @@ -77,9 +83,19 @@ python tools/convert_splash.py assets/startup_splash.gif startup_splash.rgb565 \
--preview assets/startup_splash_preview.png
```

## Operating modes and settings

Press and continuously hold the touchscreen for five seconds from the Timer Ready screen or G Mode to open the operating-mode menu. Releasing early cancels the hold. For safety, the menu cannot interrupt a running track/rest session or the Launch Mode wait. In menus, swipe left/right to choose, swipe up to select, and swipe down to cancel. The selected operating mode persists across restarts.

* **Timer Mode** retains the existing track, rest, and Launch Mode workflow.
* **G Mode** calibrates the stationary QMI8658 baseline, then presents a responsive graphical round G meter rather than numeric telemetry. The green filled marker and short trail show the current filtered acceleration vector at the LCD's display-limited refresh rate. The red hollow marker records the maximum vector, while the red perimeter arc shows peak magnitude relative to the 4 g visual scale. Double-tap resets the trail and peak. Hold for five seconds to return to the mode menu.
* **Settings** provides 25%, 50%, 75%, and 100% brightness choices with immediate preview. Swipe up saves; swipe down cancels and restores the previous brightness. **Restore defaults** requires confirmation, then restores Timer Mode, 100% brightness, 20-minute track/rest sessions, and disabled Launch Mode.

If the IMU is unavailable in G Mode, the firmware shows an actionable message and safely returns to Timer Mode. The timer remains usable.

## Supported hardware

Version 3.5 supports the integrated [Waveshare RP2040-Touch-LCD-1.28](https://www.waveshare.com/product/rp2040-touch-lcd-1.28.htm). This board combines the RP2040, GC9A01A 240x240 LCD, CST816S touchscreen, and QMI8658 IMU used by the firmware. The standalone 1.28-inch Touch LCD connected to a separate Raspberry Pi Pico uses a different pin map and is not currently supported.
Version 4.0.0 supports the integrated [Waveshare RP2040-Touch-LCD-1.28](https://www.waveshare.com/product/rp2040-touch-lcd-1.28.htm). This board combines the RP2040, GC9A01A 240x240 LCD, CST816S touchscreen, and QMI8658 IMU used by the firmware. The standalone 1.28-inch Touch LCD connected to a separate Raspberry Pi Pico uses a different pin map and is not currently supported.

### Onboard pin map

Expand Down Expand Up @@ -144,7 +160,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 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 application.py battery.py configuration.py font_data.py font_renderer.py g_meter.py hardware.py hardware_splash.py hold_detector.py launch.py lcd_1inch28.py live_display.py operating_modes.py params.json qmi8658.py ready_screen.py settings.py splash.py timer_mode.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 @@ -159,7 +175,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, 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.
The display should show the Caterham v4.0.0 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 @@ -178,14 +194,14 @@ The QMI8658 IMU is optional unless a non-zero Launch Mode sensitivity is selecte

## Configuration files

Version 3.5 uses two separate configuration scopes:
Version 4.0.0 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`, `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).
* `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`, `HARDWARE_SPLASH_DURATION_SEC`, and `MODE_MENU_HOLD_SEC`.
* `user.json` contains the current user selections: `RACE_LENGTH` (track-session minutes), `REST_LENGTH` (pit-rest minutes), `SENSITIVITY` (launch threshold; `0` disables Launch Mode), `OPERATING_MODE` (`timer` or `g`), and `BRIGHTNESS_PERCENT`.

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.

The firmware has built-in system and user defaults. Missing, malformed, or unsupported user values are replaced with safe defaults and saved using the canonical keys above. Existing `TRACK_LENGTH`, `TRACK_SESSION_LENGTH`, and `REST_SESSION_LENGTH` user keys are migrated automatically.
The firmware has built-in system and user defaults. Missing, malformed, or unsupported user values are replaced with safe defaults and saved using the canonical keys above. Existing `TRACK_LENGTH`, `TRACK_SESSION_LENGTH`, and `REST_SESSION_LENGTH` user keys are migrated automatically, while older files gain Timer Mode and 100% brightness defaults.

## Host-side tests

Expand All @@ -195,4 +211,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 both startup screens, LCD/font rendering, CST816S touchscreen detection, QMI8658 initialization, saved settings, launch behavior, and the Ready-screen battery indicator.
The suite uses fakes for time, continuous holds, touch gestures, mode/settings navigation, graphical G vectors, display calls, filesystem operations, accelerometer samples, battery readings, and USB power state. Version 4.0.0 was additionally validated on the supported Waveshare board for both startup screens, Timer and G Mode boots, native G-meter rendering, LCD/font rendering, CST816S touch-state detection, QMI8658 sampling, saved settings, launch behavior, and the Ready-screen battery indicator.
35 changes: 32 additions & 3 deletions User Guide.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# User Guide - v3.5
# User Guide - v4.0.0

## General / Sessions Use
The following describes general operation of both the ``Track Session`` and ``Rest in Pits Session`` timer.

* 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``.
* 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``. Hold the screen continuously for five seconds to open the operating-mode menu.
* 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 All @@ -16,7 +16,36 @@ The following describes general operation of both the ``Track Session`` and ``Re
* Once the ``Rest in Pits Session`` is complete, the timer will return to the ``Primary screen``. The ``Rest in Pits Session`` can be terminated with a ``Double Tap``.

## Configuration / Setup
Track duration, rest duration, and launch sensitivity are saved to `user.json` when changed. These settings persist across restarts and power loss. If the file is missing, damaged, or contains unsupported values, the timer restores safe defaults and rewrites the file using the canonical v3.2 setting names.
Track duration, rest duration, launch sensitivity, operating mode, and brightness are saved to `user.json` when changed. These settings persist across restarts and power loss. If the file is missing, damaged, or contains unsupported values, the timer restores safe defaults and rewrites the file using the canonical setting names.

### Operating Mode Menu

From the Timer ``Ready`` screen or G Mode, press and continuously hold the touchscreen for five seconds. Releasing before five seconds cancels entry. The menu is intentionally unavailable during track/rest timing or the Launch Mode wait.

* Swipe ``Left`` or ``Right`` to choose ``Timer Mode``, ``G Mode``, or ``Settings``.
* Swipe ``UP`` to select the displayed choice.
* Swipe ``DOWN`` to cancel and return to the previously active mode.
* Timer Mode and G Mode selections are saved and used on the next restart. Settings returns to the menu rather than becoming an operating mode.

#### G Mode

Keep the device still while ``Calibrating`` is displayed. The screen then becomes a graphical G meter:

* The green filled marker is the current filtered acceleration vector; its short trail shows recent direction of travel.
* The red hollow marker is the maximum vector since G Mode began or was reset.
* The red outer arc represents maximum magnitude against the meter's 4 g visual scale.
* ``Double Tap`` clears the peak and trail.
* Hold the screen for five seconds to reopen the operating-mode menu.

The graph removes the stationary gravity/mounting baseline and does not rely on small numeric telemetry. If the IMU is unavailable, the timer shows an error and returns safely to Timer Mode.

#### Device Settings

Choose ``Settings`` from the operating-mode menu.

* ``Brightness`` offers 25%, 50%, 75%, and 100%. Left/right previews each level immediately, ``Swipe UP`` saves, and ``Swipe DOWN`` cancels and restores the previous level.
* ``Restore defaults`` requires an explicit confirmation. Confirming restores Timer Mode, 100% brightness, 20-minute track and rest sessions, and disabled Launch Mode. Cancelling changes nothing.
* Choose ``Back`` or swipe down to return to the operating-mode menu, then select or cancel back to an operating mode.

### Session Duration
It is possible to change the duration of both the ``Track Session`` and the ``Rest in Pits``.
Expand Down
209 changes: 209 additions & 0 deletions application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"""Memory-aware operating-mode dispatcher loaded after LCD allocation."""

import gc
import sys
import time


PARAMS_FILE = "params.json"
USER_FILE = "user.json"
MODE_TIMER = "timer"
MODE_G = "g"

TIMER_MODULES = (
"timer_mode",
"battery",
"configuration",
"hold_detector",
"launch",
"live_display",
"ready_screen",
"timing",
)


def _unload_modules(names):
for name in names:
sys.modules.pop(name, None)
gc.collect()


def _apply_brightness(lcd, percent):
lcd.set_bl_pwm(int(round(65535 * int(percent) / 100)))


def _show_touch_failure(lcd, error):
from hardware import show_hardware_message

print("Timer stopped: {}".format(error))
show_hardware_message(
lcd,
"Touch error",
["Touch not detected", "Check board / I2C", "Restart timer"],
)


def _show_imu_degraded(lcd, error):
from hardware import show_hardware_message

print("Normal timing remains available: {}".format(error))
show_hardware_message(
lcd,
"Launch disabled",
["IMU not available", "Normal timer works", "Check board / I2C"],
background=lcd.brown,
)


def _show_g_mode_unavailable(lcd, error):
from hardware import show_hardware_message

print("G Mode unavailable: {}".format(error))
show_hardware_message(
lcd,
"G Mode unavailable",
["IMU not available", "Returning to Timer", "Check board / I2C"],
background=lcd.brown,
)


def _initialize_imu(sensitivity):
from hardware import initialize_optional_imu
from qmi8658 import QMI8658

return initialize_optional_imu(sensitivity, QMI8658)


def _open_mode_menu(touch, lcd, user_params):
from operating_modes import configure_operating_mode

result = configure_operating_mode(
touch,
lcd,
user_params,
USER_FILE,
)
del configure_operating_mode
_unload_modules(("operating_modes",))
return result


def _persist_timer_fallback(user_params):
from settings import persist_setting

return persist_setting(
USER_FILE,
user_params,
"OPERATING_MODE",
MODE_TIMER,
)[0]


def run_application(lcd):
"""Initialize shared hardware and dispatch one active feature at a time."""
from hardware import PeripheralError, initialize_with_retry
from hardware_splash import run_startup_screens
from settings import load_configuration
from touch_drive import Touch_CST816T

system_params, user_params = load_configuration(PARAMS_FILE, USER_FILE)
print("User Parameters: " + str(user_params))
_apply_brightness(lcd, user_params["BRIGHTNESS_PERCENT"])

try:
touch = initialize_with_retry(
lambda: Touch_CST816T(mode=1, LCD=lcd),
"CST816T",
)
run_startup_screens(
touch,
lcd,
firmware_version=system_params["VERSION"],
startup_duration_sec=system_params["STARTUP_SPLASH_DURATION_SEC"],
hardware_duration_sec=system_params["HARDWARE_SPLASH_DURATION_SEC"],
)
except PeripheralError as error:
_show_touch_failure(lcd, error)
return False

del run_startup_screens
_unload_modules(("hardware_splash", "splash"))

active_mode = user_params["OPERATING_MODE"]
imu_requirement = user_params["SENSITIVITY"]
if active_mode == MODE_G and imu_requirement <= 0:
imu_requirement = 1
qmi8658, imu_error = _initialize_imu(imu_requirement)
if imu_error is not None:
if active_mode == MODE_G:
_show_g_mode_unavailable(lcd, imu_error)
user_params = _persist_timer_fallback(user_params)
active_mode = MODE_TIMER
else:
_show_imu_degraded(lcd, imu_error)
time.sleep(2)

while True:
if active_mode == MODE_TIMER:
from timer_mode import run_timer_mode

try:
user_params, qmi8658 = run_timer_mode(
lcd,
touch,
user_params,
system_params,
qmi8658,
_initialize_imu,
_show_imu_degraded,
)
except PeripheralError as error:
_show_touch_failure(lcd, error)
return False
del run_timer_mode
_unload_modules(TIMER_MODULES)
user_params, active_mode = _open_mode_menu(
touch,
lcd,
user_params,
)
continue

if qmi8658 is None:
qmi8658, imu_error = _initialize_imu(1)
if imu_error is not None:
_show_g_mode_unavailable(lcd, imu_error)
time.sleep(2)
user_params = _persist_timer_fallback(user_params)
active_mode = MODE_TIMER
continue

from g_meter import run_g_mode

try:
run_g_mode(
qmi8658,
touch,
lcd,
hold_seconds=system_params["MODE_MENU_HOLD_SEC"],
)
except PeripheralError as error:
del run_g_mode
_unload_modules(("g_meter", "hold_detector"))
if error.peripheral == "CST816T":
_show_touch_failure(lcd, error)
return False
qmi8658 = None
_show_g_mode_unavailable(lcd, error)
time.sleep(2)
user_params = _persist_timer_fallback(user_params)
active_mode = MODE_TIMER
continue

del run_g_mode
_unload_modules(("g_meter", "hold_detector"))
user_params, active_mode = _open_mode_menu(
touch,
lcd,
user_params,
)
Loading
Loading