Repository navigation
Home
A standalone GPS lap timer for track days: a Waveshare ESP32-S3 board with a 600×450 panel, a microSD card, a QMI8658 IMU and a u-blox NEO-M9N receiver. It runs a session end to end — countdown, overrun, rest — records lap and G data to the card, and survives a power cycle.
This wiki holds orientation and the things that are easy to get wrong. The repository holds the authoritative detail; every link below points into it.
| Subsystem | State |
|---|---|
| Display, touch, gesture UI | working on hardware |
| microSD card, settings, session lifecycle | working, survives power cycle |
| IMU, gyro-tracked attitude, G recording | working |
| Session logging and Review | working |
| GNSS | built and host-tested, never met a receiver |
| Lap timing engine, lap state machine | built and host-tested, never seen a fix |
GNSS is the critical path. The whole chain is in the firmware and linked in — an I2C
transport, a UBX parser, a fix-quality gate, and a pipeline that counts every way a fix can
go missing — and the boot log confirms gnss: transport attached over i2c. But no part of
it has met a real receiver. The receiver and antenna are in hand; the Qwiic cables are
not. Until they arrive the device is a session timer that cannot time a lap.
Track geometry is further along than it was. Two definitions can arm the timing engine — Donington Park GP and National — against 22 in the UK pack that cannot, plus a road-loop test fixture. See the provenance gate below for why that number is small on purpose.
56 issues are open and 61 closed. The largest open group is M1 — Display board bring-up with 11, mostly board and UI work. M2 — GNSS bring-up holds only 4, which understates it: those four are the critical path, and most of what follows waits behind them.
make venv # builds .venv from the pinned requirements-dev.txt
make check # 45 host test targets, schema validation, repo hygienemake venv matters. The Makefile uses .venv/bin/python whenever it exists, so it does not
need activating, and installing the requirements into whatever python happens to resolve
to is a documented way to break the suite silently — see
the traps.
make firmware-container-build # pinned ESP-IDF v6.0.2 container, no local toolchain
make firmware-container-flash # flash the attached board
make firmware-capture-boot # capture a cold boot log
make uk-track-pack # build the UK circuit pack
make test-track-pack # build the functional test fixtures
make simulator-test # 72 host tests; the simulator itself is frozenThe simulator is no longer developed. It lags the device and cannot exercise the gesture-driven interaction model the firmware uses, so time spent on it does not buy confidence in the product. Its host tests are still run and still useful.
Each of these was discovered the hard way and cost real time.
This is the one step that can destroy a board. The Waveshare has two physically
identical SH1.0 4-pin sockets. The UART block's rear label reads UART, GND, 3v3, TX, RX.
The I2C block's pin order has never been reported. If its power ordering differs, a
board-to-board cable puts 3.3 V onto a data line. Read the label before plugging anything
in. Tracked in
#9, with the wiring it
affects on the Hardware page.
On USB-Serial-JTAG, RTS is wired to EN (reset) and DTR to GPIO0 (boot select). pyserial asserts both when it opens a port, so pulsing RTS alone releases reset with GPIO0 still held low: the chip comes up in the ROM download stub, which prints nothing at all. An empty capture then reads as "the output was discarded" when it means "the application never ran".
make firmware-capture-boot does it correctly. A genuine power cycle cannot be captured
this way at all — the USB device is inside the chip, so cutting power destroys the port the
listener is holding. An RTS reset is a chip reset and the handle survives it. Background in
#181.
The on-card layout is /sdcard/track-packs/<pack_id>/definitions/<track_id>.json, and
<pack_id> is a directory name the firmware does not interpret — it scans whatever
directories contain a definitions subdirectory and takes each definition's identity from
its filename.
So copying a new pack into a new directory does not replace the old one, it doubles it: the same track ids twice, against a 32-entry catalog. The surplus is reported as truncation and those circuits are simply absent from the driver's list. The only symptom is a wrong count on the selection screen.
device-captured is reserved for geometry captured on the device, so wiping a distributed
pack never deletes a capture.
jsonschema enforces a format keyword only when a checker for that format is installed,
and says nothing when one is not. Without rfc3339-validator, format: date-time is not
checked, so provenance.verified_utc — the field the track provenance gate reads — accepts
any string at all. The failure does not stop the validators running; it makes them agree
with you.
make check refuses an interpreter that cannot validate what the suite asserts. This is
why the versions are pinned and why make venv exists.
A pull request body containing the words "does not close #18" still closes #18 on
merge. GitHub's parser sees the keyword and the number and ignores the prose around them.
Use Refs #N unless the PR genuinely completes the issue — and an issue with hardware
acceptance criteria that software cannot satisfy is not complete, however finished the code
is.
Track definitions carry a geometry status on a four-rung ladder:
provisional → device_captured → independently_validated → physically_validated
Only the upper three arm the timing engine. A provisional circuit can be selected and
named on the dashboard, and the device will run a session timer, but it will not produce a
lap time.
That gate exists because the geometry was not merely unverified — it was wrong. The first circuit put through validation, Donington Park, had its start/finish gate on a way named Melbourne Loop (up): 82 m from the actual pit straight, with a heading 162° out. A car crossing the real line would never have triggered it. Checking the rest of the pack the same way found four more gates on the wrong feature, including a rallycross track and a different circuit on the same site.
So the gate was not protecting against imprecision. It was the only thing standing between unverified geometry and confident, wrong lap times.
Promotion by desk research requires two sources of distinct origin agreeing within 10 m, with a status and a date. Two readings of the same database are one source read twice. The pack build fails rather than shipping a promotion that does not meet this. Full process in docs/TRACK_DATABASE.md.
| Read this when | Document |
|---|---|
| Setting up to build, flash, or capture a boot log | DEVELOPMENT_SETUP.md |
| Submitting work, or wondering why a convention exists | CONTRIBUTING.md |
| Working on track geometry, packs, or promotion | TRACK_DATABASE.md |
| Contributing a circuit | TRACK_CONTRIBUTIONS.md |
| Touching GNSS, fix quality, or lap timing | GNSS_AND_LAP_TIMING.md |
| Changing screens or driver-facing text | NAVIGATION.md, UI_UX.md, UI_FOUNDATION.md |
| Wiring, power, antenna, or enclosure | Hardware on this wiki, then HARDWARE_INTEGRATION.md and HARDWARE_BOM.md |
| Understanding a structural decision | docs/decisions/ |
| Checking what is claimed to be tested | TEST_PLAN.md |
| Understanding the limits of a GPS lap timer | SAFETY_AND_LIMITATIONS.md |
| Blocked on | Issues |
|---|---|
| A Qwiic cable — satellite count climbing to a fix, the gap counter tripping on-device, a promoted circuit arming the engine | #178, #18, #139 |
| Driving a circuit — one lap time per lap, heading discrimination, lane position, a lap readable in Review | #184, #53 |
| Reading the board's label — whether GPIO43/44 are broken out, and the I2C socket's pin order | #181, #9 |
Two questions are recorded rather than answered, because both are behaviour choices rather than tidy-ups:
-
Should a provisional circuit be selectable at all?
ui::TrackSelectionController::confirmrefuses one and persists nothing, while the path that actually runs on the device persists it and runs timer-only, so the driver keeps the circuit name. The same question, answered two ways. #187 - Is a USB-UART adapter on UART0 still wanted? It is the only way to capture a boot following a genuine power cycle, which is now the sole case the scriptable capture cannot reach. #181
-
C++17,
snake_casefunctions,PascalCasetypes, explicit units in names (speed_mps,h_acc_m) — and say what a distance is measured between, not only its unit. A total width and a centreline-to-centreline separation are not comparable. -
Two clock domains are never compared:
measurement_time_nsis receiver time-of-week,arrival_monotonic_usis MCU monotonic. Lap events use GNSS time; MCU durations use monotonic time. - Bounded queues, bounded work per poll, and loss counted by cause. A drop that is counted is a diagnosis; a drop that is hidden is a mystery.
- The session countdown never depends on GNSS, SD, Wi-Fi or any optional subsystem. A timer that stops working because a receiver is unplugged is not a timer.
-
New tests belong in
make check, as host targets. Anything that does not genuinely need LVGL should run without it.