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: 11 additions & 5 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,10 +95,16 @@ Press and continuously hold the touchscreen for five seconds from the Timer Read

* **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. It also supports 0°, 90°, 180°, and 270° clockwise mounting angles: the LCD counter-rotates its output and touch gestures remain relative to the text on screen. Swipe up saves a preview; swipe down cancels and restores the previous brightness or orientation. **Restore defaults** requires confirmation, then restores Timer Mode, 100% brightness, 0° rotation, 20-minute track/rest sessions, and disabled Launch Mode.
* **Settings** provides 25%, 50%, 75%, and 100% brightness choices with immediate preview. Rotation offers **Auto** plus fixed 0°, 90°, 180°, and 270° clockwise mounting angles. Auto uses the onboard IMU to keep the display upright as the device turns; fixed choices continue to work without the IMU. In every case, touch gestures remain relative to the text on screen. Swipe up saves a preview; swipe down cancels and restores the previous brightness or orientation. **Restore defaults** requires confirmation, then restores Timer Mode, 100% brightness, fixed 0° rotation, 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.

### Automatic orientation

Auto rotation samples the QMI8658 at a bounded 10 Hz and filters the gravity vector before selecting one of the four display orientations. A new angle must remain stable for 300 ms and clearly dominate the adjacent axis, preventing rapid changes near 45° boundaries or during short acceleration spikes. The LCD controller and touch-direction mapping change together, and the current framebuffer is redrawn without allocating another 240×240 buffer. Active track/rest timing is neither reset nor paused.

An accelerometer cannot determine rotation around gravity when the screen is nearly horizontal. In that position Auto deliberately retains the last reliable angle until the device is upright enough to resolve again. Detected angles remain in RAM to avoid flash wear; `user.json` stores only the `auto` selection. If the IMU is missing or later fails, Auto freezes safely at its last angle and Timer Mode remains available. Choose a fixed angle to operate without automatic sensing.

## Supported hardware

Version 4.1.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.
Expand DownExpand Up@@ -166,7 +172,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 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 orientation.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 application.py auto_rotation.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 orientation.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 DownExpand Up@@ -196,14 +202,14 @@ If first boot fails:

The touchscreen is required for safe operation. A transient touchscreen I2C failure is retried three times at 100 ms intervals; an unexpected chip ID is not retried. If detection still fails, the firmware shows an actionable error, logs the detailed cause over serial, and stops before using an incomplete touch object.

The QMI8658 IMU is optional unless a non-zero Launch Mode sensitivityis selected. It is not initialized when Launch Mode is off. Transient initialization failures receive the same three attempts, while an unexpected chip ID fails immediately. If initialization or a launch-time sample fails, Launch Mode is disabled for the current run and the standard swipe-down timer remains available. The saved sensitivity is retained so the firmware can retry after a restart or the next Launch Mode configuration change.
The QMI8658 IMU is optional unless a non-zero Launch Mode sensitivity, G Mode, or Auto rotation is selected. It is not initialized when none of those features needs it. Transient initialization failures receive three attempts, while an unexpected chip ID fails immediately. If initialization or a runtime sample fails, sensor-dependent behavior degrades safely and the standard timer remains available. Auto retains its last reliable orientation, and manual rotation choices remain usable. Saved choices are retained so the firmware can retry after a restart.

## Configuration files

Version 4.1.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`, `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`), `BRIGHTNESS_PERCENT`, and `DISPLAY_ROTATION_DEG` (clockwise device mounting angle: `0`, `90`, `180`, or `270`).
* `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`), `BRIGHTNESS_PERCENT`, and `DISPLAY_ROTATION_DEG` (`auto` or the fixed clockwise device mounting angle `0`, `90`, `180`, or `270`).

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

The suite uses fakes for time, continuous holds, touch gestures, all four display rotations, 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.
The suite uses fakes for time, continuous holds, touch gestures, automatic and fixed display rotation, gravity filtering/hysteresis, 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.
6 changes: 5 additions & 1 deletion User Guide.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,10 +44,14 @@ The graph removes the stationary gravity/mounting baseline and does not rely on
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.
* ``Rotation`` supports mounting the device at 0°, 90°, 180°, or 270°. The value is the physical device rotation clockwise from its original 0° position. Left/right previews the orientation immediately; the text and directional gestures rotate together, so swipes stay relative to the displayed instructions. ``Swipe UP`` saves, while ``Swipe DOWN`` safely restores the previous orientation.
* ``Rotation`` offers ``Auto`` followed by fixed 0°, 90°, 180°, and 270° choices. Fixed values are the physical device rotation clockwise from its original 0° position. ``Auto`` uses the QMI8658 gravity reading to keep the screen upright and shows the currently detected angle during preview. Left/right previews immediately; text and directional gestures rotate together, so swipes stay relative to the displayed instructions. ``Swipe UP`` saves, while ``Swipe DOWN`` safely restores the previous orientation.
* ``Restore defaults`` requires an explicit confirmation. Confirming restores Timer Mode, 100% brightness, 0° rotation, 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.

Auto rotation continues through Ready, configuration, active track/rest timing, Launch Mode, G Mode, menus, and timed information screens. A turn must remain clear and stable for about 0.3 seconds before the display changes, which prevents flicker from road vibration or positions near a diagonal. Changing orientation does not reset or pause a session, and detected angles are not written repeatedly to flash.

When the display is nearly horizontal, gravity points mostly through the screen and cannot identify which edge is physically upward. Auto therefore keeps the last reliable orientation until the display is upright enough again. If the IMU is unavailable, the preview reports ``IMU unavailable`` and Auto retains a safe fixed angle; Timer Mode and all four manual rotation choices continue to work.

### Session Duration
It is possible to change the duration of both the ``Track Session`` and the ``Rest in Pits``.

Expand Down
105 changes: 89 additions & 16 deletions application.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@

import gc
import sys
import time


PARAMS_FILE = "params.json"
Expand All@@ -12,7 +11,6 @@

TIMER_MODULES = (
"timer_mode",
"battery",
"configuration",
"hold_detector",
"launch",
Expand DownExpand Up@@ -55,6 +53,21 @@ def _show_imu_degraded(lcd, error):
)


def _show_auto_rotation_degraded(lcd, error, launch_disabled=False):
from hardware import show_hardware_message

print("Automatic orientation unavailable: {}".format(error))
middle_line = (
"Launch also disabled" if launch_disabled else "Normal timer works"
)
show_hardware_message(
lcd,
"Auto rotate paused",
["IMU not available", middle_line, "Use fixed rotation"],
background=lcd.brown,
)


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

Expand All@@ -74,18 +87,19 @@ def _initialize_imu(sensitivity):
return initialize_optional_imu(sensitivity, QMI8658)


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

result = configure_operating_mode(
touch,
lcd,
user_params,
USER_FILE,
auto_rotation=auto_rotation,
)
del configure_operating_mode
_unload_modules(("operating_modes",))
return result
return result[0], result[1], auto_rotation.sensor


def _persist_timer_fallback(user_params):
Expand All@@ -101,31 +115,67 @@ def _persist_timer_fallback(user_params):

def run_application(lcd):
"""Initialize shared hardware and dispatch one active feature at a time."""
from auto_rotation import AUTO_ROTATION, AutoRotationController
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

# Import this small Timer dependency before automatic orientation begins
# producing filtered sample objects. Its module allocation otherwise lands
# in the RP2040 heap's most fragmented startup phase.
from battery import BatteryMonitor
del BatteryMonitor

system_params, user_params = load_configuration(PARAMS_FILE, USER_FILE)
print("User Parameters: " + str(user_params))
lcd.set_rotation(user_params["DISPLAY_ROTATION_DEG"])
rotation_setting = user_params["DISPLAY_ROTATION_DEG"]
initial_rotation = 0 if rotation_setting == AUTO_ROTATION else rotation_setting
lcd.set_rotation(initial_rotation)
_apply_brightness(lcd, user_params["BRIGHTNESS_PERCENT"])

try:
touch = initialize_with_retry(
lambda: Touch_CST816T(
mode=1,
LCD=lcd,
rotation=user_params["DISPLAY_ROTATION_DEG"],
rotation=initial_rotation,
),
"CST816T",
)

active_mode = user_params["OPERATING_MODE"]
imu_requirement = user_params["SENSITIVITY"]
if (
active_mode == MODE_G
or rotation_setting == AUTO_ROTATION
) and imu_requirement <= 0:
imu_requirement = 1
qmi8658, imu_error = _initialize_imu(imu_requirement)

auto_rotation = AutoRotationController(
qmi8658,
lcd,
touch,
initial_rotation=initial_rotation,
sensor_factory=lambda: _initialize_imu(1),
sensor_error=imu_error,
)
touch.Set_Auto_Rotation(auto_rotation)
if rotation_setting == AUTO_ROTATION:
auto_rotation.enable(initialize=False)
auto_rotation.prime()
if not auto_rotation.available:
qmi8658 = None
imu_error = auto_rotation.error

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"],
wait=touch.Wait,
)
except PeripheralError as error:
_show_touch_failure(lcd, error)
Expand All@@ -134,19 +184,36 @@ def run_application(lcd):
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)
# Release one-time startup imports before Timer Mode loads its dependency
# set. On the RP2040 these references are enough to decide whether the next
# small module allocation can fit beside the framebuffer.
del AutoRotationController
del AUTO_ROTATION
del initialize_with_retry
del load_configuration
del Touch_CST816T
del initial_rotation
del imu_requirement
gc.collect()

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
elif rotation_setting == AUTO_ROTATION:
_show_auto_rotation_degraded(
lcd,
imu_error,
launch_disabled=user_params["SENSITIVITY"] > 0,
)
else:
_show_imu_degraded(lcd, imu_error)
time.sleep(2)
touch.Wait(lcd, 2)

del rotation_setting
del imu_error
gc.collect()

while True:
if active_mode == MODE_TIMER:
Expand All@@ -161,27 +228,31 @@ def run_application(lcd):
qmi8658,
_initialize_imu,
_show_imu_degraded,
auto_rotation,
)
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(
user_params, active_mode, qmi8658 = _open_mode_menu(
touch,
lcd,
user_params,
auto_rotation,
)
continue

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

from g_meter import run_g_mode

Expand All@@ -199,16 +270,18 @@ def run_application(lcd):
_show_touch_failure(lcd, error)
return False
qmi8658 = None
auto_rotation.set_sensor(None, error)
_show_g_mode_unavailable(lcd, error)
time.sleep(2)
touch.Wait(lcd, 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(
user_params, active_mode, qmi8658 = _open_mode_menu(
touch,
lcd,
user_params,
auto_rotation,
)
Loading
Loading