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
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -27,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).
185 changes: 185 additions & 0 deletions examples/appdev_touch_calibrate.py
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 10 additions & 0 deletions palettes/examples/README.md
Original file line number Diff line number Diff line change
@@ -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).
166 changes: 166 additions & 0 deletions palettes/examples/palettes_demo.py
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 10 additions & 0 deletions pdwidgets/examples/README.md
Original file line number Diff line number Diff line change
@@ -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).
Loading
Loading