Trace and Watch
Bug 5 in the broken faces lab was invisible. Nothing looked wrong in a photograph of the screen; the program was just too slow to notice a finger. You cannot find a bug like that by staring at the code, and you certainly cannot find it by guessing.
You have to measure.
Let's build an instrument
This lab turns my face into its own test equipment. Four numbers on screen, updated live, and suddenly a bug you could only feel becomes a bug you can read.
Four Numbers That Tell You What Is Really Happening
The heads-up display reports these, and the same numbers go to the Thonny shell once a second so you have a record you can scroll back through:
| Reading | What it means |
|---|---|
loops |
How many times the main loop has run since it started |
fps |
Loops per second — the real speed of your program |
A / B |
What each button pin reads right now (1 = up, 0 = pressed) |
hit |
How many presses the program actually managed to notice |
The panel takes the top and bottom strips of the circle and leaves the middle to the face, so the instruments never sit on top of what they are measuring.
The panel stays in the small font, and that is a decision, not an oversight. face.label()
draws in the 16×32 font on this kit — great for a one-word headline like "Bug 5", but a readout
like fps:120 hit:34 would run wide of the safe circle the moment fps hit three digits.
face.centered_text() stays in the 8×16 font, where the same string has room to grow:
1 2 3 4 5 6 7 8 9 10 | |
PANEL_HEIGHT is face.FONT_HEIGHT — 16 pixels, the actual height of an 8×16 glyph — not the 24
you would get by scaling the smartwatch kit's erase strip by 1.5×. A bitmap font is not a position:
multiplying its height by 1.5 would blank rows of face that no letter ever touches.
Two Switches, Two Ways to Break It
The lab ships with two flags at the top, both set to False. Each one breaks the program in a
completely different way and produces the same symptom.
1 2 3 4 5 6 7 8 9 | |
| Flag | What it does | Why fps collapses |
|---|---|---|
SLOW_MODE |
Adds a 300 ms sleep() per loop |
The program is asleep instead of working |
FULL_REDRAW |
Redraws the whole face every frame | The program is busy sending 259,200 bytes |
Identical symptom, unrelated causes. That is why you measure instead of guessing.
The Instrument Measures a Different Thing Here
One difference from the OLED version is worth noticing. There, the fps you measured was almost
entirely your code's speed, because show() cost the same 8 milliseconds no matter what.
Here, drawing is sending, so this fps number is dominated by how many pixels you chose to touch. The instrument measures a different thing on different hardware, which is a good thing to know about instruments in general.
That is also why this page does not print a specific fps figure anywhere. This panel is 360×360 — 129,600 pixels, 2.25 times the smartwatch kit's 57,600 — pushed by a different driver through a different chip, and nobody has run this exact program on this exact panel and written the answer down yet. The instrument is real. The reading is yours to take.
Counting Frames Between Two Clock Readings
Every game, every robot, and every video player measures its own speed exactly the way this lab does: count how many times you did the thing, then divide by how long it took. That is the whole technique.
Sample Program Code
Here's the excerpt that does the actual measuring — the main loop, unchanged in structure from the smartwatch kit, since only the pixel numbers around it are different:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | |
The full program is 26-trace-and-watch.py in the kit.
Here's the instrument running:

Both buttons read 1 in that picture, which is what "not pressed" looks like. The fps figure needs a full second of running before it has anything to report.
Watch the Buttons Change in Real Time
Hold button A down and watch A:1 become A:0. That single digit answers "is my wiring right?" faster than any amount of code reading, and it will save you an afternoon at some point.
Things to Try
- Run it as-is and note the fps. Then set
SLOW_MODE = Trueand note it again. Write both numbers down — that ratio is the bug, expressed as a number instead of a feeling. Nobody can hand you those two numbers; they belong to your board, your wiring, and your SPI baud rate. - Now leave
SLOW_MODEoff and setFULL_REDRAW = True. The program still never sleeps, and the fps still collapses. Two causes, one symptom. - With
SLOW_MODEon, tap button A ten times as fast as you can. Compare thehitcounter to ten. Every missing press was swallowed by asleep(). - Lower
SLOW_DELAY_MSuntil presses stop getting lost. The number you land on is roughly how long a human finger stays on a button — you just measured a person with a microcontroller. - Comment out
draw_panel()for ten seconds and watch fps jump. Two text strips are not free either, which is exactly the question the next lab answers. - Raise
config.BAUDRATEand run test 1 again. On a display with no frame buffer, the SPI wire is the program's real speed limit, so this one constant may move fps more than anything you write.config.pydocuments the exact ladder this board actually lands on — 8, 12, or 24 MHz, with nothing reachable in between — so find the highest speed that still draws cleanly and write down the fps at each one you try. That table is the first honest performance measurement anyone will have made of this panel.
References
- Five Broken Faces — bug 5, the invisible one this lab was built to catch
- Don't Block the Loop — the
ticks_ms()pattern the blink timer uses - Only Redraw What Changed — where measurement turns into optimization
- Trace and Watch on the 1.2" kit — the same instrument
on the smaller 240×240 screen, where
face.label()is already the small font this kit's panel had to switch to on purpose