A redo of the console’s look, starting with the boot screen. Each iteration adds a section here with screenshots and what changed. Every picture is the real drawing code running in the emulator (pixel-for-pixel the Pico’s 240×240 panel, shown at 2×). Comment on any line in the pull request, or edit the change requests list at the bottom straight in GitHub.
What came before: the v1 walkthrough (the approval screens, iterations 1–3) and its four boot screen options. The 2-of-3 “Quorum” triangle there is where this one starts.
You asked for a home that fits how PicoQuorum works now: requests pop up, and when there’s nothing to sign, something calmer, turning into a screensaver with useful information. The grid of tiles is gone.
Nothing waits for this key: the status screen.
A request arrives: the queue list, as before. It replaces the status screen until nothing waits for this key.
A quiet minute: the screensaver. The backlight dims to 15%, and every 8 seconds the screen shows the next card, each a few pixels off from the last:
Every card comes from what the console already knows, so the screensaver makes no extra
requests. Any key wakes it and does nothing else (the first press only turns the screen on). A
transaction for this key wakes it straight to the queue. QUORUM_SAVER_S sets the quiet time (0
turns it off), and QUORUM_UTC_OFFSET sets the clock’s time zone. The browser console dims its
screen the same way.
Keys on the status screen:
| Waveshare, 240×240 | Build 1, 320×240 | |
|---|---|---|
| Ready | ![]() |
![]() |
| The second Safe in focus | ![]() |
![]() |
| The dial past the Safes | ![]() |
![]() |
| Your passkey, not an owner yet | ![]() |
![]() |
| Queued for others (you’ve signed) | ![]() |
![]() |
| A request waiting: the list | ![]() |
![]() |
| Screensaver: the time | ![]() |
![]() |
| Screensaver: a Safe | ![]() |
![]() |
| Screensaver: this key | ![]() |
![]() |
| Screensaver: the last approval | ![]() |
![]() |
| Screensaver: the release | ![]() |
![]() |
These are drawn by the real firmware in the emulator (node tools/board/shots_home.mjs). The
approvals and the release in them are made up.
Not done, and why:
Questions for you:
You asked for a home you can browse when nothing needs signing, with the queue list only when a request comes in.
Nothing waits for this key: a grid of tiles.
recent.json on the board, and every push keeps it. It starts empty
and fills as you approve.A request arrives: the queue list, exactly as before. Its first row is the waiting transaction, and A opens it.
Keys:
The 1.8” panel keeps the list for now.
| Waveshare, 240×240 | Build 1, 320×240 | |
|---|---|---|
| The grid | ![]() |
![]() |
| The chip in focus | ![]() |
![]() |
| The dial’s footer | ![]() |
![]() |
| Recent | ![]() |
![]() |
| The Chip pages from the tile (B comes back here) | ![]() |
![]() |
| A request waiting: the list | ![]() |
![]() |
These are drawn by the real firmware in the emulator (node tools/board/shots_home.mjs). The
Recent entries in the shots are made up; on the boards Recent starts empty. The Device tile’s
“test” and “emulator” are the emulator’s build and network names.
Questions for you:
You asked to see the loaded chip’s blockie when the console first loads.
| Waveshare, 240×240 | Build 1, 320×240 |
|---|---|
![]() |
![]() |
These are drawn by the real firmware in the emulator, with TM1’s owner address
0x47c0…8f35 and its board name, chip 3. node test-vectors/run-bootmark.mjs checks every frame
of the animation, with the row arriving at 700 ms, against a fresh paint on all three panels.
You said the diamond fading in and flying to the corner didn’t work. You asked to go back to the original idea, drawn the light way: the green triangle becomes the top of the Ethereum logo, which spins until the whole logo is there. Then PicoQuorum, with a small version of the mark to its left, fades in over the logo, which keeps turning. And no pauses: as quick as it can go.
Update, same day: on the board it ran fast and smooth until the name came up, then stuttered and froze. That was the console starting up behind it (below), so the diamond now slows to a stop on the logo’s pose as the name fades in, rather than freezing mid-turn.

![]() |
![]() |
| 1.5 s: the quorum. Three signed, the triangle filling. The signatures now come 0.3 s apart (they were 0.5 s), with no holds. | 1.9 s: no beat. The filled triangle is the diamond’s front face seen head-on. It brightens and starts to turn. |
![]() |
![]() |
| 2.0 s: it turns. The signers ride its corners and shrink away, and the two who didn’t sign fade out. | 2.2 s: the green face goes round the side as the camera rises to the logo’s 30°. |
![]() |
![]() |
| 2.5 s: all Ethereum blue, and the lower pyramid rises in. | 2.8 s: the logo, spinning. |
![]() |
![]() |
| 3.3 s: it slows, grows and dims, and the small mark and PicoQuorum fade in over it. | 4.5 s: the tagline, and the logo at rest on its own pose behind. |
The small mark is the boot screen in miniature: five owners, three signed in green, two amber, their triangle.
On every panel (this is BOOT_ART = "still", the end of it drawn at once):

main.py’s imports (1.1 s), and the chip checks.
With the Trust M those were 6.7 s, all without a frame. Two pure-Python P-256 verifies of its
factory certificate and key binding took 4.7 s, and the owner address (two keccaks) took 0.7 s.
Both are now worked out once and remembered by a hash of exactly the bytes they’re computed from
(tmproof.txt, owners.txt). The same chip gives the same answer at once, and any change works
them out again. The console is up about 7.8 s after power-on, down from about 13 s.
log.txt has a boot ms: line each boot with where the time went.Questions for you:
The orbit redraws and sends the whole panel every frame: 100–130 ms a frame on the Pico (8–10 fps), of which about 50 ms is just the 150 KB going over SPI to the 2.8”. The new default is the orbit’s end mark on its own, drawn as flat shapes: five owners pop in, three sign (green, a ring going out), their triangle closes and fills from the base, the two left out stay amber, and PicoQuorum fades in under it, then a tagline. The real boot status is the bottom line, as before.

Why it’s smooth: each frame sends only what changed since the last one: a dot’s square, an edge as far as it has got, the rows the green rose through, the name while it fades, the status line when it changes. Across the animation that’s about 6% of the bytes whole frames would take; the busiest frame is 26–30 KB and 17 of 131 frames send nothing. The drawing is a dozen framebuf calls per rectangle, in C. Only the first frame paints the whole panel (about the cost of one orbit frame). It lasts 3.8 s after a 0.3 s black lead-in (the orbit: 9.6 s after 4 s of black).
![]() |
![]() |
![]() |
![]() |
![]() |
It fits every panel we have, including the 1.8” (which had only the checklist; no room there for a tagline):

Taglines (firmware/taglines.py): one per boot on the mark,
cycling on the orbit. secrets.BOOT_TAGLINES picks "serious" (the default), "fun", "all" or
"none". The serious ones are claims about the console, so each has to stay true of the firmware
(the file says which code backs which claim). The fun ones are films and shows, cyberpunk and
the demoscene. Each fits 220 px; the test checks.

secrets.BOOT_ART picks the boot screen: "mark" (the default), "still" (the finished mark at
once, then only the status line changes), "orbit" (the full animation above) or "checklist".
node test-vectors/run-bootmark.mjs checks that after every frame the panel equals the whole
scene painted afresh, on all three panels, and prints the bytes sent.
You asked:
Why it was jerky: to give each boot step time on screen, the animation’s clock stopped before each signature. Anything mid-motion froze: a signing ring halfway out, a line partway drawn, the green triangle dead still before it lurched into the turn. The orbit now has its own clock that never stops. Between steps the waiting owners keep breathing, and every move eases in and out.

This is the lite GIF (2026-09-28): the panel’s own 240 px, 10 frames a second and 48 colours, 0.2 MB, for slower viewers. The full one, 360 px at 20 frames a second (1.1 MB), is here.
![]() |
![]() |
| Signing: each ring now spreads out and fades smoothly (0.8 s), and each line eases in. | Green fills from the bottom, with a bright surface line, as the two who didn’t sign fade. |
![]() |
![]() |
| Filling up: 1.1 s, easing in and out. | Full, with a soft glow that swells and fades instead of a flash. |
![]() |
![]() |
| It turns straight out of the glow: one face of the pyramid, blue coming round. | The green face turns away, the inverted pyramid rises. |
![]() |
![]() |
| The Ethereum diamond, still spinning; it never stops now. | Behind the name: it grows, centres and dims, and PicoQuorum comes up in front, with the quorum mark (five owners, three signed, their green triangle). |
![]() |
![]() |
| Taglines cycle under the name, about 2 s each… | …and the diamond keeps turning behind, slowly, for as long as the screen is up. |
Questions for you:
You said the green triangle’s timing felt awkward. Frame by frame, three things were off:
Now:

![]() |
![]() |
| The quorum closes. | Green pours in from the tip, with a bright surface line, in a third of a second. |
![]() |
![]() |
| A flash, then a beat: it holds, solid green, for about a second. | It turns, and it’s one face of a pyramid. The face coming round is Ethereum blue. |
![]() |
![]() |
| The green face turns away as the camera pulls back… | …and goes out of sight. Only blue is left, and the diamond settles as before. |
The first half (the orbit, the status line, the signatures) and the ending are iteration 4’s. The loading screen is 0.7 s longer (12.4 s) for the beat.
Question for you: is the beat the right length? About 1 s now (the pour, the flash and the hold).
You asked:

![]() |
![]() |
| Starting up. Five owners far out in space. The orbit now takes 3.2 s and 2.4 turns (was 2.2 s and 1.4). | Joining WiFi while they swing round to face you. |
![]() |
![]() |
| WiFi connected: the top signs, and turns green. Each step’s line slides up and out as the next slides in. | Chip verified: the bottom left signs, with a green line to the top. |
![]() |
![]() |
| The Safe found: the bottom right signs and the triangle closes. “Family Safe · 3 of 5”. | The triangle turns solid green. The two who didn’t sign fade away. |
![]() |
![]() |
| Loading the queue: the pyramid turns, and its green blends into Ethereum’s blue… | …and the inverted pyramid rises underneath. |
![]() |
|
| Ready. The diamond lands on the logo’s pose, with the name above the last line. |
Questions for you:
You picked the orbit, without the circle. The orbit’s path is gone, and so is the faint circle the owners landed on. There are only five dots, then only the triangle of the three who sign. The pictures for this iteration were replaced by iteration 4’s, which builds on it.
You asked: the star could read as a pentagram. Drop it, and have the five owners spin in a circle or in space, then carry on to the top and the two bottom ones and the Ethereum diamond.
There are two options. Both end with the owners exactly where the star had them. From there it’s iteration 1 unchanged, minus the star’s lines: the three sign, the triangle closes, and the diamond spins.
| A. Ring | B. Orbit |
|---|---|
![]() |
![]() |
| Five owners spin round a faint circle, each trailing a comet tail of light. They slow down and stop with one at the top. Flat and simple, like a dial settling. | Five owners orbit in space: the circle starts small and tipped back like a planet’s ring, with a few stars drifting behind. It swings round to face you and grows as it slows, and the stars fade out as it lands. |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Both land here, and the signing starts:
![]() |
![]() |
| Landed. Five amber owners on a faint circle, no lines between them. | Quorum. The top and the two bottom ones sign; only their triangle is drawn. The circle fades out with the two who didn’t sign. |
Questions for you (answered: the orbit, just dots):

emu/sketches/ethboot.py, 150 frames at the firmware’s 50 ms tick (7.5 s, then the last frame
holds). tools/emu run ethboot plays it in the emulator; A replays it.
![]() |
![]() |
| Five owners, ready. A pen draws the star in one stroke, and each owner appears as it gets there. All five are amber, which means they can sign but haven’t. The ones still waiting breathe slowly. | Signing. The top signs, then the left leg. Each one turns Ethereum blue and sends out a ring, and a blue line joins it to the top. |
![]() |
![]() |
| Quorum. The right leg signs and the base closes. That’s the tall triangle made by the star’s top and legs (36° at the tip). The two owners who didn’t sign start to fade. | The triangle fills in. It’s about to become a solid face. |
![]() |
![]() |
| It’s a pyramid, seen head-on. The filled triangle is exactly one face of a square pyramid. The three signers sit on its tip and its two front corners. | The camera pulls back and rises, and the pyramid starts to turn. The signers ride their corners, then shrink away. |
![]() |
![]() |
| The inverted pyramid rises underneath, fading in from dark blue. | The Ethereum diamond, spinning on its axis. The gap between the two halves stays an even band at every angle. |
![]() |
|
| It slows to a stop with a corner toward you: the logo’s own pose. Then the name comes up. |
framebuf.poly (which runs in C), back faces dropped, far faces first. That’s at
most four triangles a frame, so it should cost about what the v1 Quorum option did on the board
(54 ms a frame, most of it the 46 ms push to the panel). Not timed on the chip yet.warn) means ready to sign. Signed is Ethereum’s periwinkle
(#627EEA) lifted to (122, 146, 255) so it reads on near-black. The diamond is shaded from dark
indigo to pale lavender, lit from the upper left.framebuf.poly doesn’t anti-alias. At 2× in the GIF the steps show; on the
1.3″ screen (about 260 pixels per inch) they’re small. A cheap fix is a 1-pixel edge in an
in-between colour.This is a proposal; none of it is wired up yet. Today firmware/splash.py draws the boot checklist.
| Boot step | What it shows (status line in quotes) |
|---|---|
| Screen up | the five owners spin in and land, all amber: “Starting up”, “Joining WiFi” |
| WiFi joined | the top signs: “WiFi connected”, then “Checking the chip” |
| The chip answers with its key | the bottom left signs: “Chip verified”, then “Finding the Safe” |
| The Safe found, through the service | the bottom right signs, the triangle closes and turns green: “Family Safe · 3 of 5” |
| Loading the queue | the diamond spins, as long as it takes: “Loading the queue” |
| Ready | it lands on the logo: “Ready”, and the home screen follows |
main.py: tools/emu ship ethboot. I haven’t
timed it on the chip yet.Today (v1, iteration 2): Atkinson Hyperlegible Next and Mono, seven sizes, rendered on the laptop
by tools/fonts/gen.py and drawn by the Pico with 16 levels of anti-aliasing. It was picked for
telling 0/O and 1/l/I apart, which matters on a device for comparing addresses. So:
gen.py can render any of them. I’d put these side by side on the real
screen:
1 of 2 ● Alice ○ Bob ○ You) becomes a small star or polygon of the
Safe’s owners.tools/emu run ethboot # play it in the emulator; A = next option
tools/bootgifs --sketch ethboot --stills 8,16,26 orbit # an option's GIF and stills
tools/bootgifs --sketch ethboot --lite orbit # plus orbit-lite.gif: 240 px, 10 a second
tools/emu ship ethboot # a one-off run on the Pico
tools/bootgifs now takes --sketch, --out and --stills. It also builds each GIF’s palette
from frames across the whole animation instead of only the last one. That kept the amber here, and
it gave the v1 hash-rain GIF back the blue it had lost.
Edit this list on GitHub: add a line, tick one off, or write under it. Each iteration starts here.