Skip to content

Commit fd4e6bd

Browse files
Merge pull request #78 from ctrmint/feature/issue-77-average-lap-time-setting
Add persistent average lap-time setting
2 parents 98cc683 + ecf8852 commit fd4e6bd

7 files changed

Lines changed: 232 additions & 10 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ Trackday or race session timer.
1414
* Moved the Ready-screen dim level into `params.json` as the validated `AUTO_DIM_PERCENT` system setting, defaulting to 25%.
1515
* Added a hardware Bill of Materials identifying the cased Waveshare RP2040-Touch-LCD-1.28-B reference variant.
1616
* Moved the device payload into `firmware/` and added a layout-aware deployment tool so the repository stays organized while MicroPython still receives a flat filesystem.
17-
* Expanded hardware-independent regression coverage to 189 tests.
17+
* Added a persistent average lap-time setting with separate minute and second editing for future lap features.
18+
* Expanded hardware-independent regression coverage to 195 tests.
1819

1920
## Version 4.2
2021
### v4.2.0
@@ -132,7 +133,7 @@ Press and continuously hold the touchscreen for five seconds from the Timer Read
132133

133134
* **Timer Mode** retains the existing track, rest, and Launch Mode workflow. During a track session, a baseline-corrected value such as `MAX 1.23 g` appears in a compact, clearly spaced line above the countdown. The peak resets for each track session and remains visible through overrun. `MAX --` indicates that acceleration data is unavailable; timing and the stop gesture continue normally. After a double-tap stop, an interactive review gives actual duration, overrun, total maximum G, acceleration, braking, left G, right G, and the completion reason their own high-visibility screens. Swipe left to advance and right to go back; cool-down begins only after swiping left from the eighth and final page. The review is held in RAM only and is not retained after leaving it. Rest sessions do not show maximum G.
134135
* **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.
135-
* **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. **Auto-Dim** is an independent On/Off choice: when enabled, the Ready screen reduces to 25% brightness after 10 seconds without motion and returns immediately to the saved brightness when motion is detected. It never dims menus, configuration, Launch Mode, active timing, review, rest, or G Mode. 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, disabled Auto-Dim, 20-minute track/rest sessions, and disabled Launch Mode.
136+
* **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. **Auto-Dim** is an independent On/Off choice: when enabled, the Ready screen reduces to 25% brightness after 10 seconds without motion and returns immediately to the saved brightness when motion is detected. **Avg Lap Time** stores the current circuit's expected lap duration from `00:00` to `59:59`; minutes and seconds are edited separately, and `00:00` means not configured. The value is reserved for later lap functionality and does not alter current sessions. Auto-Dim never dims menus, configuration, Launch Mode, active timing, review, rest, or G Mode. In every case, touch gestures remain relative to the text on screen. Swipe up saves a preview; swipe down cancels and restores the previous value. **Restore defaults** requires confirmation, then restores Timer Mode, 100% brightness, fixed 0° rotation, disabled Auto-Dim, an unset `00:00` average lap time, 20-minute track/rest sessions, and disabled Launch Mode.
136137

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

@@ -246,13 +247,13 @@ The QMI8658 IMU is optional unless a non-zero Launch Mode sensitivity, G Mode, A
246247
Version 4.3.0 uses two separate configuration scopes:
247248

248249
* `firmware/params.json` is the repository source for 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`, `MODE_MENU_HOLD_SEC`, and `AUTO_DIM_PERCENT` (an integer from 1 to 100, default 25).
249-
* `firmware/user.json` is the optional fresh-install example. The device-root `user.json` contains the current 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`, `DISPLAY_ROTATION_DEG` (`auto` or the fixed clockwise device mounting angle `0`, `90`, `180`, or `270`), and `AUTO_DIM_ENABLED` (`true` or `false`).
250+
* `firmware/user.json` is the optional fresh-install example. The device-root `user.json` contains the current 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`, `DISPLAY_ROTATION_DEG` (`auto` or the fixed clockwise device mounting angle `0`, `90`, `180`, or `270`), `AUTO_DIM_ENABLED` (`true` or `false`), and `AVG_LAP_TIME_SECONDS` (integer `0` to `3599`; `0` means not configured).
250251

251252
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 the [User Guide](docs/USER_GUIDE.md) for the practical meaning of every configured value.
252253

253254
Directional summary labels use a dashboard mounting convention: the screen faces the driver, the screen-normal axis represents acceleration/braking, and the viewer-horizontal axis represents left/right. Fixed and automatic quarter-turn display rotations are applied to the lateral mapping. Mounting the board with its screen facing away from the driver reverses the longitudinal labels.
254255

255-
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, 100% brightness, 0° rotation, and disabled Auto-Dim defaults.
256+
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, 100% brightness, 0° rotation, disabled Auto-Dim, and an unset average lap-time default.
256257

257258
## Host-side tests
258259

@@ -262,4 +263,4 @@ Run the hardware-independent regression suite with:
262263
python -m unittest discover -s tests -t . -v
263264
```
264265

265-
The suite uses fakes for time, continuous holds, touch gestures, automatic and fixed display rotation, Ready-screen inactivity dimming and motion wake-up, 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.
266+
The suite uses fakes for time, continuous holds, touch gestures, automatic and fixed display rotation, Ready-screen inactivity dimming and motion wake-up, average-lap-time editing and migration, 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.

docs/USER_GUIDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ The following describes general operation of both the ``Track Session`` and ``Re
1818
* 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``.
1919

2020
## Configuration / Setup
21-
Track duration, rest duration, launch sensitivity, operating mode, brightness, display rotation, and Auto-Dim state 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.
21+
Track duration, rest duration, launch sensitivity, operating mode, brightness, display rotation, Auto-Dim state, and average lap time 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.
2222

2323
### Operating Mode Menu
2424

@@ -48,7 +48,8 @@ Choose ``Settings`` from the operating-mode menu.
4848
* ``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.
4949
* ``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.
5050
* ``Auto-Dim`` offers ``Off`` and ``On``. When On, 10 continuous seconds without motion reduces only the Timer Ready screen to the level configured by `AUTO_DIM_PERCENT` in `params.json` (25% by default; valid values are integer percentages from 1 to 100). Moving the device immediately restores the saved brightness and restarts the inactivity interval. Leaving Ready for a menu, configuration, Launch Mode, an active session, review, rest, or G Mode always restores normal saved brightness. The setting defaults to Off and does not change the saved Brightness percentage. If the IMU is unavailable, the timer shows a warning and safely remains at normal brightness.
51-
* ``Restore defaults`` requires an explicit confirmation. Confirming restores Timer Mode, 100% brightness, 0° rotation, disabled Auto-Dim, 20-minute track and rest sessions, and disabled Launch Mode. Cancelling changes nothing.
51+
* ``Avg Lap Time`` stores the expected lap duration for the current circuit. The first editor stage sets minutes from `00` to `59`; swipe Up to continue, then set seconds from `00` to `59` and swipe Up again to save. Swipe Left or Right to change the active component. Swipe Down from either stage to cancel the complete edit without writing it. `00:00` means not configured. This value is saved for later lap features and does not currently change Timer Mode, the Ready screen, session review, or G Mode.
52+
* ``Restore defaults`` requires an explicit confirmation. Confirming restores Timer Mode, 100% brightness, 0° rotation, disabled Auto-Dim, an unset `00:00` average lap time, 20-minute track and rest sessions, and disabled Launch Mode. Cancelling changes nothing.
5253
* Choose ``Back`` or swipe down to return to the operating-mode menu, then select or cancel back to an operating mode.
5354

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

firmware/operating_modes.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
BRIGHTNESS_VALUES,
66
DEFAULT_USER_PARAMS,
77
DISPLAY_ROTATION_VALUES,
8+
MAX_AVG_LAP_TIME_SECONDS,
89
OPERATING_MODES,
910
persist_setting,
1011
restore_user_defaults,
@@ -25,6 +26,7 @@
2526
("Brightness", "brightness"),
2627
("Rotation", "rotation"),
2728
("Auto-Dim", "auto_dim"),
29+
("Avg Lap Time", "avg_lap_time"),
2830
("Restore defaults", "restore"),
2931
("Back", "back"),
3032
)
@@ -193,6 +195,75 @@ def draw():
193195
return original, False
194196

195197

198+
def format_avg_lap_time(total_seconds):
199+
"""Format a validated canonical duration for the settings display."""
200+
bounded = max(0, min(MAX_AVG_LAP_TIME_SECONDS, int(total_seconds)))
201+
minutes, seconds = divmod(bounded, 60)
202+
return "{:02d}:{:02d}".format(minutes, seconds)
203+
204+
205+
def avg_lap_time_lines(total_seconds, component):
206+
"""Build the two-stage average-lap-time editor."""
207+
setting_minutes = component == "minutes"
208+
return [
209+
["Avg Lap Time", None, 30, 2, "white"],
210+
[format_avg_lap_time(total_seconds), None, 78, 4, "white"],
211+
[
212+
"Set minutes" if setting_minutes else "Set seconds",
213+
None,
214+
143,
215+
1,
216+
"white",
217+
],
218+
["L/R: change", None, 180, 1, "white"],
219+
["UP: next" if setting_minutes else "UP: save", None, 202, 1, "white"],
220+
["DOWN: cancel", None, 220, 1, "white"],
221+
]
222+
223+
224+
def select_avg_lap_time(touch, lcd, current):
225+
"""Edit minutes then seconds and return ``(total_seconds, save)``."""
226+
if (
227+
not isinstance(current, int)
228+
or isinstance(current, bool)
229+
or current < 0
230+
or current > MAX_AVG_LAP_TIME_SECONDS
231+
):
232+
current = DEFAULT_USER_PARAMS["AVG_LAP_TIME_SECONDS"]
233+
original = current
234+
minutes, seconds = divmod(current, 60)
235+
component = "minutes"
236+
237+
def value():
238+
return (minutes * 60) + seconds
239+
240+
def draw():
241+
touch.ControlScreen(
242+
lcd,
243+
text_array=avg_lap_time_lines(value(), component),
244+
back_colour="black",
245+
)
246+
247+
draw()
248+
while True:
249+
gesture = touch.GetGesture(lcd)
250+
if gesture in ("left", "right"):
251+
delta = -1 if gesture == "left" else 1
252+
if component == "minutes":
253+
minutes = (minutes + delta) % 60
254+
else:
255+
seconds = (seconds + delta) % 60
256+
draw()
257+
elif gesture == "up":
258+
if component == "minutes":
259+
component = "seconds"
260+
draw()
261+
else:
262+
return value(), True
263+
elif gesture == "down":
264+
return original, False
265+
266+
196267
def apply_rotation(lcd, touch, degrees, auto_rotation=None):
197268
"""Apply one mount angle to rendering and directional gestures."""
198269
if degrees == AUTO_ROTATION:
@@ -386,6 +457,24 @@ def _run_settings(
386457
if saved:
387458
user_params = updated
388459

460+
elif action == "avg_lap_time":
461+
previous = user_params["AVG_LAP_TIME_SECONDS"]
462+
selected, should_save = select_avg_lap_time(
463+
touch,
464+
lcd,
465+
previous,
466+
)
467+
if not should_save:
468+
continue
469+
updated, saved = persist_setting(
470+
user_file,
471+
user_params,
472+
"AVG_LAP_TIME_SECONDS",
473+
selected,
474+
)
475+
if saved:
476+
user_params = updated
477+
389478
elif action == "restore" and confirm_restore_defaults(touch, lcd):
390479
defaults, saved = restore_user_defaults(user_file)
391480
if saved:

firmware/settings.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828
"BRIGHTNESS_PERCENT": 100,
2929
"DISPLAY_ROTATION_DEG": 0,
3030
"AUTO_DIM_ENABLED": False,
31+
"AVG_LAP_TIME_SECONDS": 0,
3132
}
3233

3334
OPERATING_MODES = ("timer", "g")
3435
BRIGHTNESS_VALUES = (25, 50, 75, 100)
3536
DISPLAY_ROTATION_VALUES = (0, 90, 180, 270, "auto")
37+
MAX_AVG_LAP_TIME_SECONDS = (60 * 60) - 1
3638

3739
LEGACY_USER_KEYS = {
3840
"TRACK_LENGTH": "RACE_LENGTH",
@@ -180,6 +182,16 @@ def normalize_user_params(data, system_params=None):
180182
DEFAULT_USER_PARAMS["AUTO_DIM_ENABLED"],
181183
)
182184
auto_dim_invalid = not isinstance(auto_dim_enabled, bool)
185+
avg_lap_time_seconds = migrated.get(
186+
"AVG_LAP_TIME_SECONDS",
187+
DEFAULT_USER_PARAMS["AVG_LAP_TIME_SECONDS"],
188+
)
189+
avg_lap_time_invalid = (
190+
not isinstance(avg_lap_time_seconds, int)
191+
or isinstance(avg_lap_time_seconds, bool)
192+
or avg_lap_time_seconds < 0
193+
or avg_lap_time_seconds > MAX_AVG_LAP_TIME_SECONDS
194+
)
183195

184196
if (
185197
not isinstance(race_length, int)
@@ -210,6 +222,8 @@ def normalize_user_params(data, system_params=None):
210222
display_rotation = DEFAULT_USER_PARAMS["DISPLAY_ROTATION_DEG"]
211223
if auto_dim_invalid:
212224
auto_dim_enabled = DEFAULT_USER_PARAMS["AUTO_DIM_ENABLED"]
225+
if avg_lap_time_invalid:
226+
avg_lap_time_seconds = DEFAULT_USER_PARAMS["AVG_LAP_TIME_SECONDS"]
213227

214228
normalized = {
215229
"SENSITIVITY": sensitivity,
@@ -219,8 +233,12 @@ def normalize_user_params(data, system_params=None):
219233
"BRIGHTNESS_PERCENT": brightness_percent,
220234
"DISPLAY_ROTATION_DEG": display_rotation,
221235
"AUTO_DIM_ENABLED": auto_dim_enabled,
236+
"AVG_LAP_TIME_SECONDS": avg_lap_time_seconds,
222237
}
223-
return normalized, auto_dim_invalid or normalized != source
238+
return (
239+
normalized,
240+
auto_dim_invalid or avg_lap_time_invalid or normalized != source,
241+
)
224242

225243

226244
def _remove_if_exists(path):

firmware/user.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"OPERATING_MODE": "timer",
66
"BRIGHTNESS_PERCENT": 100,
77
"DISPLAY_ROTATION_DEG": 0,
8-
"AUTO_DIM_ENABLED": false
8+
"AUTO_DIM_ENABLED": false,
9+
"AVG_LAP_TIME_SECONDS": 0
910
}

0 commit comments

Comments
 (0)