From 92d5cc10a0699212bdd24891ff5431dc4e92c22a Mon Sep 17 00:00:00 2001 From: mrsqr Date: Sun, 9 Aug 2026 23:43:04 +0100 Subject: [PATCH] Add visible configuration prompts --- User Guide.md | 6 ++--- configuration.py | 36 ++++++++++++++++++++---------- font_renderer.py | 31 ++++++++++++++++++++++---- tests/test_configuration.py | 44 ++++++++++++++++++++++++++++++++++++- tests/test_font_renderer.py | 13 +++++++++++ 5 files changed, 111 insertions(+), 19 deletions(-) diff --git a/User Guide.md b/User Guide.md index 23153a4..7ed232d 100644 --- a/User Guide.md +++ b/User Guide.md @@ -22,14 +22,14 @@ Track duration, rest duration, and launch sensitivity are saved to `user.json` w It is possible to change the duration of both the ``Track Session`` and the ``Rest in Pits``. * From the ``Primary Screen``, ``Swipe Left`` to edit the ``Track Session`` and ``Swipe Right`` to edit the ``Rest in Pits Session``. -* Once in either edit modes, use a ``Swipe Right`` to increment the duration and a ``Swipe Left`` to decrement the value. Note duration values are predefined as [1, 5, 10, 15, 20, 25, 30, 40, 50, 60] minutes. -* When the desired duration value is shown ``Swipe Up`` to save and return to the ``Primary Screen`` +* Once in either edit mode, follow the on-screen prompts: ``Swipe Right`` to increment the duration and ``Swipe Left`` to decrement the value. Note duration values are predefined as [1, 5, 10, 15, 20, 25, 30, 40, 50, 60] minutes. +* When the desired duration value is shown, follow the ``Swipe UP: save`` prompt to save and return to the ``Primary Screen``. ### Launch Mode ``Launch mode`` is disabled by default. ``Launch mode`` can be enabled by defining a ``Launch Sensitivity`` value above zero. * From the ``Primary Screen``, ``Swipe Up`` to edit ``Launch Sensitivity``. -* Swipe ``Left`` or ``Right`` to select an appropriate value greater than zero. +* Follow the on-screen prompts and swipe ``Left`` or ``Right`` to select an appropriate value greater than zero. * Swipe ``UP`` to save and enable ``Launch mode``. * Select `0` and swipe ``UP`` to save and disable ``Launch mode``. diff --git a/configuration.py b/configuration.py index d31fc9b..14def32 100644 --- a/configuration.py +++ b/configuration.py @@ -1,6 +1,16 @@ """Touch-driven configuration editors with injected display dependencies.""" +CONFIGURATION_PROMPTS = ( + ("Swipe L/R to change", None, 205, 1, "black"), + ("Swipe UP: save", None, 222, 1, "black"), +) + + +def _with_prompts(text_array): + return text_array + list(CONFIGURATION_PROMPTS) + + def _selected_index(values, current): if not values: raise ValueError("At least one selectable value is required") @@ -17,12 +27,14 @@ def set_sensitivity(LCD=None, Touch=None, sensitivity_values=None, sensitivity=0 index = _selected_index(values, sensitivity) def draw(): - text_array = [ - [str(values[index]), None, 80, 5, "white"], - ["Launch", None, 145, 2, "black"], - ["Sensitivity", None, 175, 2, "black"], - [operation, None, 35, 2, "black"], - ] + text_array = _with_prompts( + [ + [str(values[index]), None, 80, 5, "white"], + ["Launch", None, 145, 2, "black"], + ["Sensitivity", None, 175, 2, "black"], + [operation, None, 35, 2, "black"], + ] + ) Touch.ControlScreen(LCD, text_array=text_array, back_colour=back_colour) draw() @@ -46,11 +58,13 @@ def set_session(LCD=None, Touch=None, session=None, session_values=None, index = _selected_index(values, current) def draw(): - text_array = [ - [str(values[index]), None, 90, 5, "white"], - [session_name, None, 180, 2, "black"], - [operation, None, 35, 2, "black"], - ] + text_array = _with_prompts( + [ + [str(values[index]), None, 90, 5, "white"], + [session_name, None, 180, 2, "black"], + [operation, None, 35, 2, "black"], + ] + ) Touch.ControlScreen(LCD, text_array=text_array, back_colour=back_colour) draw() diff --git a/font_renderer.py b/font_renderer.py index 1c283c7..07eb7cb 100644 --- a/font_renderer.py +++ b/font_renderer.py @@ -70,7 +70,16 @@ def _draw_scanlines(surface, bitmap, width, height, x, y, color): surface.hline(x + run_start, y + row, width - run_start, color) -def _blit_glyph(surface, bitmap, width, height, x, y, palette): +def _blit_glyph( + surface, + bitmap, + width, + height, + x, + y, + palette, + transparent_key, +): glyph_buffer = bytearray(bitmap) # Generated rows are padded to whole bytes and store the left-most pixel in # bit 7. MicroPython needs an explicit padded stride and MONO_HLSB for that @@ -79,7 +88,7 @@ def _blit_glyph(surface, bitmap, width, height, x, y, palette): glyph = framebuf.FrameBuffer( glyph_buffer, width, height, framebuf.MONO_HLSB, stride, ) - surface.blit(glyph, x, y, 0, palette) + surface.blit(glyph, x, y, transparent_key, palette) def draw_text(surface, text, x, y, size, color): @@ -93,10 +102,15 @@ def draw_text(surface, text, x, y, size, color): palette = None palette_buffer = None + transparent_key = None if use_blitter: + # A distinct palette value is required for transparency. Using black + # as the key also discards black foreground glyphs after palette + # mapping, which made configuration labels and prompts invisible. + transparent_key = 1 if color != 1 else 2 palette_buffer = bytearray(4) palette = framebuf.FrameBuffer(palette_buffer, 2, 1, framebuf.RGB565) - palette.pixel(0, 0, 0) + palette.pixel(0, 0, transparent_key) palette.pixel(1, 0, color) with open(BITMAP_FILES[size_index], "rb") as bitmap_file: @@ -110,7 +124,16 @@ def draw_text(surface, text, x, y, size, color): raise OSError("Font bitmap is missing or truncated") if use_blitter: - _blit_glyph(surface, bitmap, width, height, cursor, int(y), palette) + _blit_glyph( + surface, + bitmap, + width, + height, + cursor, + int(y), + palette, + transparent_key, + ) else: _draw_scanlines(surface, bitmap, width, height, cursor, int(y), color) cursor += width diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 4f3b84e..b3de0a7 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1,7 +1,9 @@ +import math import unittest from types import SimpleNamespace -from configuration import set_sensitivity, set_session +from configuration import CONFIGURATION_PROMPTS, set_sensitivity, set_session +from font_renderer import measure_text, pixel_height class FakeTouch: @@ -20,6 +22,46 @@ class ConfigurationEditorTests(unittest.TestCase): duration_values = [1, 5, 10, 15, 20, 25, 30, 40, 50, 60] sensitivity_values = [0, 0.5, 1, 1.25, 1.5, 1.75, 2, 2.5, 3.5, 4] + def assert_prompts_shown(self, touch): + expected = list(CONFIGURATION_PROMPTS) + for _, text_array, _ in touch.screens: + self.assertEqual(expected, text_array[-len(expected):]) + + def test_prompts_fit_inside_the_round_display(self): + display_radius = 120 + for text, _, y_position, size, _ in CONFIGURATION_PROMPTS: + text_center_y = y_position + (pixel_height(size) / 2) + distance_from_center = text_center_y - display_radius + visible_width = 2 * math.sqrt( + (display_radius ** 2) - (distance_from_center ** 2) + ) + self.assertLessEqual(measure_text(text, size), visible_width) + + def test_session_prompts_are_shown_on_every_redraw(self): + touch = FakeTouch(["right", "up"]) + set_session( + LCD=object(), + Touch=touch, + session=SimpleNamespace(duration_mins=self.duration_values[0]), + session_values=self.duration_values, + session_name="Track", + ) + + self.assertEqual(2, len(touch.screens)) + self.assert_prompts_shown(touch) + + def test_sensitivity_prompts_are_shown_on_every_redraw(self): + touch = FakeTouch(["left", "up"]) + set_sensitivity( + LCD=object(), + Touch=touch, + sensitivity_values=self.sensitivity_values, + sensitivity=self.sensitivity_values[0], + ) + + self.assertEqual(2, len(touch.screens)) + self.assert_prompts_shown(touch) + def test_session_immediate_save_preserves_every_allowed_value(self): for current in self.duration_values: with self.subTest(current=current): diff --git a/tests/test_font_renderer.py b/tests/test_font_renderer.py index 41803d6..a5d95b5 100644 --- a/tests/test_font_renderer.py +++ b/tests/test_font_renderer.py @@ -105,6 +105,19 @@ def test_hardware_blit_uses_msb_first_bits_and_padded_stride(self): self.assertEqual(((glyph_width + 7) // 8) * 8, glyph_call[4]) self.assertEqual(1, len(surface.blits)) + def test_hardware_blit_does_not_treat_black_text_as_transparent(self): + fake_framebuf = FakeFrameBufferModule() + surface = FakeBlitSurface() + original_framebuf = font_renderer.framebuf + font_renderer.framebuf = fake_framebuf + try: + draw_text(surface, "Config", 3, 4, 1, 0) + finally: + font_renderer.framebuf = original_framebuf + + self.assertTrue(surface.blits) + self.assertTrue(all(blit[3] != 0 for blit in surface.blits)) + def test_centering_uses_measured_width(self): surface = FakeSurface() x, width = draw_centered(surface, "Ready", 20, 5, 1)