Files
hermes-bot/pi/display_test.py
kyleandClaude Sonnet 5.5 55d33ab490 Add hermes_face ROS package and verify the display on the Pi
The Pi (hermes-bot) is flashed, upgraded and running ROS 2 Jazzy. The ST7789
works through Blinka on lgpio: pass cs=None (the kernel owns CE0), and the glass
sits at y_offset=80 in the controller RAM for every rotation, since the driver
rotates in software. The mounted pose needs rotation=270.

hermes_face is a numpy port of the flat face from docs/face-prototype.html, with
phosphor and vector styles, plus a new `sleeping` pose for "no agent connected".
The renderer has no ROS or hardware imports. The tone map is a lookup table and
the glow runs at half resolution, so the face reaches ~28 fps on the panel at
24% of a core (20 fps) to 40% (30 fps) with the CPU at full clock.

CONTEXT.md now records the Pi, the cloud-init first-boot-only gotcha, and what
is left. pi/50-cloud-init.yaml is a gitignored Wi-Fi template.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
2026-10-04 20:35:33 -07:00

65 lines
2.3 KiB
Python

#!/usr/bin/env python3
"""Draw a test pattern on the BrainCraft HAT's ST7789 (240x240, SPI CE0, DC=GPIO25, BL=GPIO26).
display_test.py [ROTATION] one pattern, then a frame-rate probe
The driver rotates in software (PIL, counter-clockwise), and the panel's 80-row RAM offset belongs
to the glass, so y_offset=80 holds for every rotation. 270 is the mounted pose (microSD end up).
A correct picture has the frame complete on all four edges, no noise, the white triangle in the
top-left corner as you look at the panel in its mounted pose, and upright text.
"""
import sys
import time
import board
import digitalio
from adafruit_rgb_display import st7789
from PIL import Image, ImageDraw, ImageFont
W = H = 240
def make_display(rotation, x_offset, y_offset, baudrate=48_000_000):
return st7789.ST7789(
board.SPI(),
cs=None, # CE0 is driven by the kernel SPI driver; claiming GPIO8 fails with 'GPIO busy'
dc=digitalio.DigitalInOut(board.D25),
rst=None,
baudrate=baudrate,
width=W,
height=H,
x_offset=x_offset,
y_offset=y_offset,
rotation=rotation,
)
def pattern(label):
img = Image.new("RGB", (W, H), "black")
d = ImageDraw.Draw(img)
for i, c in enumerate(["red", "lime", "blue", "yellow", "cyan", "magenta"]):
d.rectangle([i * 40, 90, i * 40 + 39, 150], fill=c)
d.rectangle([0, 0, W - 1, H - 1], outline="white") # edge frame: shows offset/clipping
d.rectangle([3, 3, W - 4, H - 4], outline="gray")
d.polygon([(8, 8), (58, 8), (8, 58)], fill="white") # top-left marker: shows rotation
d.text((70, 12), label[0], fill="white", font=ImageFont.load_default(size=48))
d.text((70, 66), label[1], fill="white", font=ImageFont.load_default(size=14))
for x in range(0, W, 30):
d.line([(x, 160), (x, H - 8)], fill="gray")
for y in range(160, H - 8, 20):
d.line([(0, y), (W, y)], fill="gray")
return img
backlight = digitalio.DigitalInOut(board.D26)
backlight.switch_to_output(value=True)
rotation = int(sys.argv[1]) if len(sys.argv) > 1 else 270
disp = make_display(rotation, 0, 80)
img = pattern((str(rotation), f"rot={rotation} y_off=80"))
disp.image(img)
t0, n = time.monotonic(), 30
for _ in range(n):
disp.image(img)
print(f"pattern shown; full-frame push rate {n / (time.monotonic() - t0):.1f} fps at 48 MHz")