What is verified
Compiles clean under -Wall -Wextra
at 57% of flash and 37% of RAM, 57% because 0.4.2 moved to a
single 3.00 MB app partition rather than the pair of 1.88 MB slots an
over-the-air update needs, which nothing here uses. Several fixes are verified at the symbol level: the linked
image was diffed symbol by symbol to prove a refactor changed nothing, and the overrides were
checked with nm and objdump rather than
assumed. 59 host check scripts run on every release. The
thirty-nine that count their cases assert 379,416 between them: the
WPA handshake classifier, the Fast Pair frames and probe, the Flipper .sub
reader, the AirTag parser, the advertisement walker, the rotating-tail detector, the OUI
signatures against the IEEE registry, the surveillance matching primitives and filter, and the
logo scale. The other nineteen arrive as a verdict rather than a count: that no two
pins collide, that every menu table is fully initialised, that nothing drawn runs off the
panel, that no source file carries a control byte, and that the
documentation's own numbers still match the source. All of that on a host, but as of 0.4.1 the receive side is
no longer only on a host: a second board running
PueoBeacon transmits what each detector looks for, and
every one of them has been shown to receive it.
Two results came off a board rather than off an
argument. The key-frame reserve was measured under traffic that
dropped 1,387 of 4,268 frames, 32 per cent, with four
EAPOL messages arriving consecutively and every one reaching the card; the
resulting files were then checked at byte level against Wireshark's own WPA
reference capture rather than with the parser they are a fixture for. And a
Flipper .sub has been imported, stored and read back
on the panel as 433.92 MHz, 24 bits, 0x123456,
which is what the parser's own transcription says that file is. That one
matters because it is the first time the C++ parser and the Python model
behind 61,453 assertions have agreed on a real file on real hardware rather
than only on each other.
What ran on hardware
Pueo ran on a real
board for the first time on 2026-09-20. Display, bitmap rendering,
the menus, touch, the WiFi scanner, the packet monitor, Surveillance and the new tracker hunt
all work. Nothing is wired to it yet, so every transmit claim above stands exactly
where it did, including the GPIO 1 handover, still a
prediction.
The drone detector’s own alert was raised on the board against the beacon
transmitting Remote ID, mark and fade and notes and all. It had been drawing
Hunt’s owl as its feature mark since 0.3.9. A borrowed mark that nothing
would ever fail on, and that told anyone looking at the screen the wrong thing
about what they had opened.
Stealth Mode was walked through on the board at
0.4.4: every feature that transmits refused and named itself,
every feature that listens carried on. That is a check of the gates rather than of the
radios. It says the firmware declines to call the transmit paths, not that a
spectrum analyser watched the antenna go quiet. The gates themselves are enumerated by a
host check, so the list cannot shrink without failing it.
The
receive side is no longer in that bracket. As of
0.4.1 a second board runs
PueoBeacon, which transmits every signal this firmware
looks for, and all six detection paths have been shown to work against it: Surveillance on
WiFi OUI, on a BLE company and service pair, and on a BLE name; Drones on WiFi Beacon and
on BLE; Hunt's picker and gauge; and Fast Pair on both payload shapes, which is the
distinction that parser exists to draw.
None of it was in doubt on paper and two thirds of it had never moved a
byte off a radio.
Building the beacon
found six faults in one evening, and it is worth saying that two of them were in the beacon
itself. A WiFi transmit path that was silently refused, and five decoys sharing one
MAC, which made a vehicle match vanish into a smart-glasses row. The other four were here:
three list screens that only flicker once something is actually in range, and a tracker
picker that never repainted when a tracker arrived. Every one of them needed a transmitter
to exist before it could be seen.
A day on hardware found
more than a month of reading had. Three features were unreachable
(Surveillance, Fast Pair and Hunt) because a submenu selection is dispatched by two
hand-maintained chains, one for the buttons and one for the touch bar, and the touch one
stopped after the first entry on that page. They then had
no visible way out: the exit is the centre slot and those three
labelled it with an empty string. And the bar they did label was
painted and then wiped by the same setup, because
redraw(true) starts with a fillScreen. Each of the three hid the
next.
The BLE sniffer
rebooted the board. Its log shifts as many lines as the screen can
hold and its buffer holds sixteen; on a 320×480 panel that zone holds thirty-one, so
fifteen assignments landed past the end of the array and onto the members after it. One of
those is the scan pointer. Found by decoding the panic rather than by reading for it: the
backtrace named the function, the disassembly named the offset, and the offset named the
member. It was already two past the end on the 2.8″, where they fell in padding.
The board was a 3.5″
ESP32-3248S035R, Sunton's, 55.50 × 101.50
× 5.80 mm, ST7796 rather than ILI9341, 320×480 rather than
240×320, and an XPT2046 on the display's own SPI bus rather than the separate touch
bus the 2.8″ used. It became a supported target rather than a surprise, with
its own flash image beside the 2.8″ one, and in the
end the only one, since it is the panel anybody has actually run.
Every feature screen was
still drawing into the top-left 240×320 of it. The menu grid had been made
panel-aware; nothing else had, and the dimensions were not in one place to fix but in
221, declarations, status-bar icon rows laid out from
x=240, and coordinates in drawing calls. They read one pair of constants now, and a check
holds the line.
Two things the panel
split did not reach, both of which shipped in the first cut of 0.3.4 and neither of which
announced itself. TFT_eSPI reads its configuration from a copy inside the
installed library, so the 2.8″ image was built with the
3.5″'s driver, size and backlight pin, the exact bug the split existed to fix,
still in the artifact claiming to fix it. Caught by rebuilding the archive at a different
path: one image reproduced and the other did not. And the touch bus flag was set for both
boards, which would have driven touch through the display's SPI on the panel whose
controller is not there.
The datasheet then
disagreed with the pin map, and the datasheet was for the wrong
board. It said GPIO 4 was an audio amplifier's enable on the 3.5″ rather than
the RGB LED's red channel, and the NRF24's chip select was on it, an amplifier
keyed at SPI rates. The select was moved: 4 on the 2.8″, 25 on the 3.5″, where
the pin is free because that board does not need it for touch.
On
2026-09-23 the pin was driven on a board here and read. GPIO 4 is the
LED's red channel on this board too (4 red, 16 blue, 17 green, 22 nothing),
and the amplifier was lcdwiki's, on a part this is not built on. The chip select stays on
25 because 25 is free and the map is published, but it was dodging a hazard that is not
there. Nothing was soldered either time, which is the only reason either version cost
nothing.
Two findings were not board-specific. The
touch calibrator recorded a point and advanced without waiting for the
finger to lift, so one press satisfied all four targets in milliseconds and returned
four nearly identical readings. Its endpoint arithmetic also used
min() and max() across the corner pairs,
which assumes raw counts rise with the screen coordinate and skews the result on any panel
where they fall. Both would have been just as wrong on the 2.8″ board, and neither was
reachable from a host check, which is the argument for this section existing.