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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ python tools/generate_font.py /path/to/Montserrat-SemiBold.otf font_data.py

The generated font data is distributed under the SIL Open Font License 1.1 in `FONT_LICENSE.txt`.

## Live display refresh

Track and rest sessions poll stop gestures every 50 ms while comparing the complete visible frame (remaining time, elapsed time, font size, background, and text colour) with the previous frame. The 115,200-byte framebuffer is transferred only when a displayed second or warning state changes, normally reducing continuous redraws to one per second. Touch-controller mode changes are also cached, so an unchanged gesture mode does not generate repeated I2C writes.

On the supported Waveshare board running MicroPython 1.21.0, five full live-screen redraws measured 56.2–65.4 ms. Input is therefore checked within 50 ms between redraws and within approximately 115 ms in the worst case when a gesture arrives immediately before a redraw. Five consecutive frames produced only the two register writes needed for the initial gesture-mode configuration and no rewrites on later frames.

## 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.
Expand Down Expand Up @@ -107,7 +113,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 configuration.py font_data.py font_renderer.py launch.py lcd_1inch28.py params.json qmi8658.py settings.py splash.py timing.py touch_drive.py font_data*.bin startup_splash.rgb565 :
mpremote connect auto fs cp configuration.py font_data.py font_renderer.py launch.py lcd_1inch28.py live_display.py params.json qmi8658.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 Down
94 changes: 94 additions & 0 deletions live_display.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Throttled live-session display updates with responsive input polling."""

import time

from timing import secs_to_mins_secs


LIVE_LOOP_DELAY_SEC = 0.05


def _visible_times(session, now):
elapsed_seconds = max(0, int(now - session.start_time))
remaining_seconds = max(0, int(session.duration_secs) - elapsed_seconds)
return (
secs_to_mins_secs(remaining_seconds),
secs_to_mins_secs(elapsed_seconds),
)


def track_live_frame(session, now, lcd):
"""Return all values visible on the track-session screen."""
remaining, elapsed = _visible_times(session, now)
if now >= session.end_time:
return ("00:00", elapsed, 6, lcd.red, lcd.black)

if now < session.last_15:
return (remaining, elapsed, 6, None, None)
if now < session.last_5:
return (remaining, elapsed, 6, lcd.salmon, lcd.black)
return (remaining, elapsed, 6, lcd.lilac, None)


def rest_live_frame(session, now, lcd):
"""Return visible rest-session values, or ``None`` when it is complete."""
if now >= session.end_time:
return None
remaining, elapsed = _visible_times(session, now)
return (
remaining,
elapsed,
6,
lcd.blue,
None,
)


def draw_live_frame(touch, lcd, frame):
"""Render one frame tuple produced by a live-frame builder."""
remaining, elapsed, text_size, background, text_colour = frame
touch.LiveScreen(
lcd,
textsize_rem=text_size,
backColour=background,
textColour=text_colour,
elapsed=elapsed,
remaining=remaining,
)


def run_live_display(
session,
frame_builder,
draw_frame,
stop_check,
clock=time,
loop_delay_sec=LIVE_LOOP_DELAY_SEC,
):
"""Poll input at a bounded rate and redraw only when visible state changes."""
if loop_delay_sec <= 0:
raise ValueError("loop_delay_sec must be positive")

previous_frame = None
first_frame = True
redraw_count = 0

while session.live is True:
if stop_check():
session.live = False
break

frame = frame_builder(clock.time())
if frame is None:
session.live = False
break

if first_frame or frame != previous_frame:
draw_frame(frame)
previous_frame = frame
first_frame = False
redraw_count += 1

clock.sleep(loop_delay_sec)

return redraw_count
63 changes: 19 additions & 44 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
from configuration import set_sensitivity, set_session
from launch import accel_launch
from lcd_1inch28 import LCD_1inch28
from live_display import (
draw_live_frame,
rest_live_frame,
run_live_display,
track_live_frame,
)
from qmi8658 import QMI8658
from settings import load_configuration, persist_setting
from timing import SessionTracker, secs_to_mins_secs
from timing import SessionTracker
from touch_drive import Touch_CST816T


Expand Down Expand Up @@ -117,35 +123,12 @@ def main():

track_session.start_session()

while track_session.live is True:
now = time.time()
remaining = secs_to_mins_secs(track_session.end_time - now)
elapsed = secs_to_mins_secs(now - track_session.start_time)

if touch.StopGesture(lcd):
track_session.live = False

if now < track_session.end_time:
if now < track_session.last_15:
touch.LiveScreen(
lcd, textsize_rem=6, backColour=None, textColour=None,
elapsed=elapsed, remaining=remaining,
)
elif now < track_session.last_5:
touch.LiveScreen(
lcd, textsize_rem=6, backColour=lcd.salmon, textColour=lcd.black,
elapsed=elapsed, remaining=remaining,
)
else:
touch.LiveScreen(
lcd, textsize_rem=6, backColour=lcd.lilac, textColour=None,
elapsed=elapsed, remaining=remaining,
)
else:
touch.LiveScreen(
lcd, textsize_rem=6, backColour=lcd.red, textColour=lcd.black,
elapsed=elapsed, remaining="00:00",
)
run_live_display(
track_session,
frame_builder=lambda now: track_live_frame(track_session, now, lcd),
draw_frame=lambda frame: draw_live_frame(touch, lcd, frame),
stop_check=lambda: touch.StopGesture(lcd),
)

touch.ControlScreen(
lcd,
Expand All @@ -155,20 +138,12 @@ def main():
time.sleep(display_delay_rest)

rest_session.start_session(debug=True)
while rest_session.live is True:
now = time.time()
remaining = secs_to_mins_secs(rest_session.end_time - now)
elapsed = secs_to_mins_secs(now - rest_session.start_time)

if touch.ClearGesture(lcd):
rest_session.live = False
if now < rest_session.end_time:
touch.LiveScreen(
lcd, textsize_rem=6, backColour=lcd.blue, textColour=None,
elapsed=elapsed, remaining=remaining,
)
else:
rest_session.live = False
run_live_display(
rest_session,
frame_builder=lambda now: rest_live_frame(rest_session, now, lcd),
draw_frame=lambda frame: draw_live_frame(touch, lcd, frame),
stop_check=lambda: touch.ClearGesture(lcd),
)


if __name__ == "__main__":
Expand Down
133 changes: 133 additions & 0 deletions tests/test_live_display.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import unittest

from live_display import rest_live_frame, run_live_display, track_live_frame
from timing import SessionTracker


class FakeClock:
def __init__(self, now=0):
self.now = now
self.sleep_calls = []

def time(self):
return self.now

def sleep(self, delay):
self.sleep_calls.append(delay)
self.now += delay


class FakeLCD:
red = 1
black = 2
salmon = 3
lilac = 4
blue = 5


class LiveDisplayTests(unittest.TestCase):
def test_simulated_session_redraws_at_most_once_per_visible_second(self):
clock = FakeClock()
lcd = FakeLCD()
session = SessionTracker(duration_mins=0.1, clock=clock.time)
session.start_session()
frames = []
input_checks = {"count": 0}

def stop_check():
input_checks["count"] += 1
return False

redraws = run_live_display(
session,
frame_builder=lambda now: rest_live_frame(session, now, lcd),
draw_frame=frames.append,
stop_check=stop_check,
clock=clock,
)

self.assertEqual(int(session.duration_secs), redraws)
self.assertEqual(redraws, len(frames))
self.assertGreaterEqual(input_checks["count"], 100)
self.assertTrue(all(delay == 0.05 for delay in clock.sleep_calls))
self.assertFalse(session.live)

def test_input_is_checked_between_unchanged_frames(self):
clock = FakeClock()
session = SessionTracker(duration_mins=1, clock=clock.time)
session.start_session()
frames = []
input_checks = {"count": 0}

def stop_check():
input_checks["count"] += 1
return input_checks["count"] == 4

redraws = run_live_display(
session,
frame_builder=lambda now: ("same", "same", 1, 1, 1),
draw_frame=frames.append,
stop_check=stop_check,
clock=clock,
)

self.assertEqual(1, redraws)
self.assertEqual(4, input_checks["count"])
self.assertAlmostEqual(0.15, clock.now)
self.assertFalse(session.live)

def test_warning_state_change_redraws_even_when_text_is_unchanged(self):
clock = FakeClock()
session = SessionTracker(duration_mins=1, clock=clock.time)
session.start_session()
frames = []

def frame_builder(now):
colour = "green" if now < 0.1 else "warning"
return ("00:59", "00:00", 6, colour, "white")

def stop_check():
return clock.now >= 0.2

redraws = run_live_display(
session,
frame_builder=frame_builder,
draw_frame=frames.append,
stop_check=stop_check,
clock=clock,
)

self.assertEqual(2, redraws)
self.assertEqual("green", frames[0][3])
self.assertEqual("warning", frames[1][3])

def test_track_frames_include_warning_and_overrun_state(self):
lcd = FakeLCD()
session = SessionTracker(duration_mins=10, clock=lambda: 100)
session.start_session()

running = track_live_frame(session, 609, lcd)
last_15 = track_live_frame(session, 610, lcd)
last_5 = track_live_frame(session, 670, lcd)
overrun = track_live_frame(session, 700, lcd)

self.assertEqual((None, None), running[3:])
self.assertEqual((lcd.salmon, lcd.black), last_15[3:])
self.assertEqual((lcd.lilac, None), last_5[3:])
self.assertEqual("00:00", overrun[0])
self.assertEqual((lcd.red, lcd.black), overrun[3:])

def test_loop_delay_must_be_bounded(self):
session = SessionTracker(duration_mins=1, live=True)
with self.assertRaises(ValueError):
run_live_display(
session,
frame_builder=lambda now: None,
draw_frame=lambda frame: None,
stop_check=lambda: False,
loop_delay_sec=0,
)


if __name__ == "__main__":
unittest.main()
Loading
Loading