From ad2b2d99ae1d322ad660be93b0e1b4fcc6814b3a Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:19:07 -0500 Subject: [PATCH 1/2] Examples: appdev_touch_calibrate finds a board's touch_rotation_table --- examples/README.md | 1 + examples/appdev_touch_calibrate.py | 185 +++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 examples/appdev_touch_calibrate.py diff --git a/examples/README.md b/examples/README.md index a78ce61f..7df4e5a4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,6 +6,7 @@ Each one says in its docstring what it needs and where it has been run. | Example | What it shows | |---|---| | [appdev_simpletest.py](appdev_simpletest.py) | A timer, a tap and a key driving a box on the screen | +| [appdev_touch_calibrate.py](appdev_touch_calibrate.py) | Finding the touch_rotation_table for a board_config by touching four corners | | [audiodev_simpletest.py](audiodev_simpletest.py) | Four notes from synthio through the board's speaker | | [bledev_simpletest.py](bledev_simpletest.py) | The Bluetooth LE devices advertising nearby, on a board or a laptop | | [bledev_central_simpletest.py](bledev_central_simpletest.py) | Reading a Bluetooth LE counter and writing it a message | diff --git a/examples/appdev_touch_calibrate.py b/examples/appdev_touch_calibrate.py new file mode 100644 index 00000000..b80cf385 --- /dev/null +++ b/examples/appdev_touch_calibrate.py @@ -0,0 +1,185 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Find the touch_rotation_table for a board's touch screen. + +Turns the display through its four rotations and asks you to touch a box +in each corner in turn. From where your touches land it works out how the +touch screen's axes line up with the display at each rotation, and prints +the touch_rotation_table to put in board_config.py. + +Needs: a board_config with a display_drv and a touch screen, which a +PyDevices board package provides, and pygraphics. With demo set to True +it leaves the touch driver's table alone and only walks through the steps. + +Run it: + mpremote run appdev_touch_calibrate.py + +Tested on: CPython 3.12 on Linux, in an SDL window, as far as the first +corner's box. +""" + +from board_config import display_drv +import board_config +import appdev + +app = appdev.App(board_config) +import appdev +from pygraphics import round_rect, text16 + +# Change this to match your setup. +demo = False + +FG_COLOR = -1 # white +BG_COLOR = 0 # black + +text = "Touch here" +text_width = len(text) * 8 + +SWAP_XY = 0b001 +REVERSE_X = 0b010 +REVERSE_Y = 0b100 + +_ZONE_MASKS = { + (0, 1, 2, 3): 0b0, + (1, 0, 3, 2): REVERSE_X, + (2, 3, 0, 1): REVERSE_Y, + (3, 2, 1, 0): REVERSE_X | REVERSE_Y, + (0, 2, 1, 3): SWAP_XY, + (2, 0, 3, 1): SWAP_XY | REVERSE_X, + (1, 3, 0, 2): SWAP_XY | REVERSE_Y, + (3, 1, 2, 0): SWAP_XY | REVERSE_X | REVERSE_Y, +} + + +def set_rotation_table(table): + if display_drv.touch_device is not None: + if display_drv.touch_device.type == appdev.POINTER: + display_drv.touch_device.rotation_table = table + + +def _draw_target(x, y, half_width, half_height): + round_rect( + display_drv, + x * half_width + 10, + y * half_height + 10, + half_width - 20, + half_height - 20, + 10, + FG_COLOR, + True, + ) + text16( + display_drv, + text, + x * half_width + ((half_width - text_width) // 2), + y * half_height + ((half_height - 8) // 2), + BG_COLOR, + ) + + +def _clear_target(x, y, half_width, half_height): + display_drv.fill_rect( + x * half_width, + y * half_height, + half_width - 1, + half_height - 1, + BG_COLOR, + ) + + +def _report(touch_rotation_table): + if not demo: + set_rotation_table(touch_rotation_table) + print("Set the `touch_rotation_table` in board_config.py to the following:") + else: + print("Demo complete.") + out_text = f"touch_rotation_table = {tuple(touch_rotation_table)}" + print(" ", out_text, "\n") + text16( + display_drv, + out_text, + (display_drv.width - len(out_text) * 8) // 2, + (display_drv.height - 8) // 2, + FG_COLOR, + ) + + +# Callback-driven state machine (one path for sync and async). +_st = { + "rotation": 0, + "x": 0, + "y": 0, + "touched_zones": [], + "touch_rotation_table": [], + "half_width": 0, + "half_height": 0, + "done": False, +} + + +def _show_current_target(): + _draw_target(_st["x"], _st["y"], _st["half_width"], _st["half_height"]) + + +def _advance_or_finish(): + _st["x"] += 1 + if _st["x"] > 1: + _st["x"] = 0 + _st["y"] += 1 + if _st["y"] > 1: + # finished one rotation + mask = _ZONE_MASKS.get(tuple(_st["touched_zones"])) + if mask is None: + print("Invalid touch sequence. Starting over...\n") + _start_over() + return + _st["touch_rotation_table"].append(mask) + print(f"rotation={_st['rotation']} {mask=} ({mask:#05b})\n") + _st["rotation"] += 90 + if _st["rotation"] >= 360: + _report(_st["touch_rotation_table"]) + _st["done"] = True + return + _st["touched_zones"] = [] + _st["x"] = 0 + _st["y"] = 0 + display_drv.rotation = _st["rotation"] + _st["half_width"] = display_drv.width // 2 + _st["half_height"] = display_drv.height // 2 + display_drv.fill_rect(0, 0, display_drv.width - 1, display_drv.height - 1, BG_COLOR) + _show_current_target() + + +def _start_over(): + _st["rotation"] = 0 + _st["x"] = 0 + _st["y"] = 0 + _st["touched_zones"] = [] + _st["touch_rotation_table"] = [] + display_drv.rotation = 0 + _st["half_width"] = display_drv.width // 2 + _st["half_height"] = display_drv.height // 2 + display_drv.fill_rect(0, 0, display_drv.width - 1, display_drv.height - 1, BG_COLOR) + print("Touch the rectangle in each corner for 4 rotations.\n") + _show_current_target() + + +def _on_click(e): + if _st["done"] or e.button != 1: + return + touched_point = e.pos + zone = (touched_point[1] // _st["half_height"]) * 2 + ( + touched_point[0] // _st["half_width"] + ) + _st["touched_zones"].append(zone) + print(f"{touched_point=} in {zone=}") + _clear_target(_st["x"], _st["y"], _st["half_width"], _st["half_height"]) + _advance_or_finish() + + +if not demo: + set_rotation_table((0, 0, 0, 0)) + +_start_over() +app.on(app.events.MOUSEBUTTONDOWN, _on_click) From 6f9b5246bb40256875e09347507427445e4541db Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:20:48 -0500 Subject: [PATCH 2/2] Examples: palettes_demo and joystick_list_select, from pydevices-examples --- examples/README.md | 2 +- palettes/examples/README.md | 10 ++ palettes/examples/palettes_demo.py | 166 +++++++++++++++++++++ pdwidgets/examples/README.md | 10 ++ pdwidgets/examples/joystick_list_select.py | 57 +++++++ 5 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 palettes/examples/README.md create mode 100644 palettes/examples/palettes_demo.py create mode 100644 pdwidgets/examples/README.md create mode 100644 pdwidgets/examples/joystick_list_select.py diff --git a/examples/README.md b/examples/README.md index 7df4e5a4..a74f55fe 100644 --- a/examples/README.md +++ b/examples/README.md @@ -28,6 +28,6 @@ Each one says in its docstring what it needs and where it has been run. The ones that come in pairs, such as a central and a peripheral, are two ends of one link: run each on its own board, or put a phone or a laptop at -the other end. appkit, hmilink and midiports keep theirs in their own `examples/` folders. +the other end. appkit, hmilink, midiports, palettes and pdwidgets keep theirs in their own `examples/` folders. The full apps are in [pydevices-examples](https://github.com/PyDevices/pydevices-examples). diff --git a/palettes/examples/README.md b/palettes/examples/README.md new file mode 100644 index 00000000..df64189e --- /dev/null +++ b/palettes/examples/README.md @@ -0,0 +1,10 @@ +# Examples + +Each example says in its docstring what it needs and where it has been run. + +| Example | What it shows | +|---|---| +| [palettes_demo.py](palettes_demo.py) | The wheel, cube and Material Design palettes, one after another | + +Full apps that use palettes are in +[pydevices-examples](https://github.com/PyDevices/pydevices-examples). diff --git a/palettes/examples/palettes_demo.py b/palettes/examples/palettes_demo.py new file mode 100644 index 00000000..240bba9b --- /dev/null +++ b/palettes/examples/palettes_demo.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Walk through three palettes: a colour wheel, a cube and Material. + +Paints the screen in bands from the wheel palette, then scrolls through +every colour of a 5x5x5 cube palette with its name and number, then shows +the Material Design palette, and starts over. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, appdev and pygraphics, and palettes. + +Run it from pydevices' palettes folder: + python examples/palettes_demo.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +import board_config +import appdev + +app = appdev.App(board_config) +from pygraphics import FrameBuffer, RGB565 +from palettes import get_palette + +if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) +else: + needs_swap = False + +display_drv.rotation = 0 + +MODES = ("wheel", "cube", "material") +# Brief hold after a static material draw before advancing. +MATERIAL_HOLD_TICKS = 60 + + +def _setup(): + st = { + "mode_idx": 0, + "mode": None, + # wheel + "wheel_colors": None, + "wheel_ci": 0, + "wheel_i": 0, + "wheel_lh": 2, + # cube + "cube_palette": None, + "cube_entries": None, + "cube_fb": None, + "cube_ba": None, + "cube_idx": 0, + "cube_y": 0, + "cube_scroll": 0, + "cube_lh": 20, + "cube_last": 0, + # material + "mat_hold": 0, + "mat_drawn": False, + } + + def enter_mode(mode): + st["mode"] = mode + display_drv.vscsad(0) + display_drv.fill(0) + if mode == "wheel": + palette = get_palette(name="wheel", swapped=needs_swap, length=256, saturation=1.0) + st["wheel_colors"] = list(palette) + st["wheel_ci"] = 0 + st["wheel_i"] = 0 + elif mode == "cube": + palette = get_palette(name="cube", size=5, color_depth=16, swapped=needs_swap) + lh = st["cube_lh"] + bpp = display_drv.color_depth // 8 + ba = bytearray(display_drv.width * lh * bpp) + st["cube_palette"] = palette + st["cube_entries"] = list(enumerate(palette)) + st["cube_ba"] = ba + st["cube_fb"] = FrameBuffer(ba, display_drv.width, lh, RGB565) + st["cube_idx"] = 0 + st["cube_y"] = 0 + st["cube_scroll"] = 0 + st["cube_last"] = display_drv.height - lh + else: + st["mat_drawn"] = False + st["mat_hold"] = 0 + + def advance(): + st["mode_idx"] = (st["mode_idx"] + 1) % len(MODES) + enter_mode(MODES[st["mode_idx"]]) + + def poll_wheel(): + colors = st["wheel_colors"] + n = len(colors) + lh = st["wheel_lh"] + if st["wheel_i"] >= display_drv.height: + # One full screen painted — advance mode. + advance() + return + display_drv.fill_rect( + 0, + st["wheel_i"] % display_drv.height, + display_drv.width, + lh, + colors[st["wheel_ci"]], + ) + st["wheel_i"] += lh + st["wheel_ci"] = (st["wheel_ci"] + 1) % n + + def poll_cube(): + entries = st["cube_entries"] + n = len(entries) + palette = st["cube_palette"] + lh = st["cube_lh"] + index, color = entries[st["cube_idx"]] + if st["cube_y"] - st["cube_scroll"] - st["cube_last"] > 0: + st["cube_scroll"] = (st["cube_y"] - st["cube_last"]) % display_drv.height + display_drv.vscsad(st["cube_scroll"]) + name = f"{index} - {palette.color_name(index)}" + text_color = palette.WHITE if palette.brightness(index) < 0.4 else palette.BLACK # noqa: PLR2004 + st["cube_fb"].fill(color) + st["cube_fb"].text16(name, 2, 2, text_color) + display_drv.blit_rect( + st["cube_ba"], 0, st["cube_y"] % display_drv.height, display_drv.width, lh + ) + st["cube_y"] += lh + st["cube_idx"] = (st["cube_idx"] + 1) % n + # One full walk of the cube palette (and enough rows to fill a screen). + if st["cube_idx"] == 0 and st["cube_y"] >= display_drv.height: + advance() + + def poll_material(): + if not st["mat_drawn"]: + palette = get_palette(name="material_design", color_depth=16, swapped=needs_swap) + for i, color in enumerate(palette): + display_drv.fill_rect(0, i, display_drv.width, 1, color) + st["mat_drawn"] = True + st["mat_hold"] = 0 + return + st["mat_hold"] += 1 + if st["mat_hold"] >= MATERIAL_HOLD_TICKS: + advance() + + def poll(): + mode = st["mode"] + if mode == "wheel": + poll_wheel() + elif mode == "cube": + poll_cube() + else: + poll_material() + return False + + enter_mode(MODES[0]) + return poll + + +poll = _setup() + + +def _tick(_=None): + poll() + + +app.every(_tick, period=1) diff --git a/pdwidgets/examples/README.md b/pdwidgets/examples/README.md new file mode 100644 index 00000000..28a927aa --- /dev/null +++ b/pdwidgets/examples/README.md @@ -0,0 +1,10 @@ +# Examples + +Each example says in its docstring what it needs and where it has been run. + +| Example | What it shows | +|---|---| +| [joystick_list_select.py](joystick_list_select.py) | A list whose highlight a joystick or D-pad moves | + +Full apps that use pdwidgets are in +[pydevices-examples](https://github.com/PyDevices/pydevices-examples). diff --git a/pdwidgets/examples/joystick_list_select.py b/pdwidgets/examples/joystick_list_select.py new file mode 100644 index 00000000..4c1f92cb --- /dev/null +++ b/pdwidgets/examples/joystick_list_select.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Choose from a list with a joystick or a D-pad. + +Shows a pdwidgets list of three items and moves the highlight up and down +when the joystick's hat or D-pad moves. + +Needs: a board_config with a display_drv and a joystick, which a PyDevices +board package or pydevices-desktop provides, appdev and pdwidgets. + +Run it from pydevices' pdwidgets folder: + python examples/joystick_list_select.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +import board_config +import appdev +import pdwidgets as pd + +app = appdev.App(board_config) + +display = pd.Display(board_config.display_drv, app,40) +screen = pd.Screen(display, visible=False) + +top, bottom, main = screen.top, screen.bottom, screen.main + +pd.Label(top,value="Select an Item") + +list_view = pd.ListView(main, w=int(main.width * .9), h=int(main.height * .9), align=pd.ALIGN.CENTER) + +items = [pd.Label(list_view,value=i) for i in ["Item 1", "Item 2", "Item 3"]] +selected = 0 +items[selected].bg = list_view.color_theme.secondary + +screen.visible = True + + + +def select_item(up: bool): + global selected + if up and selected > 0: + selected -= 1 + elif not up and selected < len(items) - 1: + selected += 1 + for i in items: + i.bg = list_view.color_theme.secondary if i == items[selected] else list_view.color_theme.transparent + list_view.invalidate() + +def joystick_callback(event): + if event.type == pd.events.JOYHATMOTION: + if event.hat == 0: + if event.value[1] != 0: + select_item(event.value[1] > 0) + +app.on(pd.events.JOYHATMOTION, joystick_callback)