Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
3a3fe91
Merge pull request #111 from TheAngryRaven/master
TheAngryRaven Jul 28, 2026
89cd540
SensorEgg PW-ADV v2: Temp2 page, DOVEX Temp2 column, -Ofast isnan fix
claude Aug 1, 2026
b3dba86
Merge pull request #112 from TheAngryRaven/claude/pw-adv-2-logger
TheAngryRaven Aug 1, 2026
1b9601a
docs: sprint mode (autocross) concept + cross-repo roadmap
claude Aug 2, 2026
cb018ec
Fix battery-sleep reboot loop: sample tach idle level before arming S…
claude Aug 2, 2026
4686121
Merge pull request #114 from TheAngryRaven/claude/sleep-mode-battery-…
TheAngryRaven Aug 2, 2026
69773b1
docs: fold real autocross cadence into sprint concept
claude Aug 2, 2026
0b172c0
docs: sprint concept — engine-on-between-runs confirmed, best run is …
claude Aug 2, 2026
59399d1
docs: sprint concept — DOVEX race_mode marker decided (CIRCUIT/SPRINT…
claude Aug 2, 2026
285e73a
docs: adopt numbered docs/plans structure (DovesDataViewer convention)
claude Aug 2, 2026
d143cc9
plan 0002: mode-by-folder, date-based courses, on-device course creat…
claude Aug 2, 2026
439f31a
plan 0002+0003: lock in tiebreak setting, ISO dates, 3s point hold, s…
claude Aug 2, 2026
100d717
plan 0002: run semantics decided - both lines always hot, start-cross…
claude Aug 2, 2026
ec8fa69
plan 0002: final decisions - race mode stays live between runs (*wait…
claude Aug 2, 2026
a737913
Merge pull request #115 from TheAngryRaven/claude/sprint-mode-race-ty…
TheAngryRaven Aug 2, 2026
ef75267
plan 0002: sprint_select pure unit + DOVEX race_mode trailing column
claude Aug 2, 2026
7b8c1fd
plan 0002: sprint mode core - /TRACKS/SPRINT, mode-by-folder, SprintT…
claude Aug 2, 2026
197e481
plan 0002: CHANGELOG + CLAUDE.md + README for sprint mode core
claude Aug 2, 2026
7e904af
plan 0002: pass -DDOVES_DISABLE_DEBUG in all firmware builds (OTA cap…
claude Aug 2, 2026
06a2915
ci: re-run against DovesLapTimer BETA with DOVES_DISABLE_DEBUG (post-…
claude Aug 2, 2026
54463fd
Merge pull request #116 from TheAngryRaven/claude/sprint-mode-race-ty…
TheAngryRaven Aug 2, 2026
10e417b
feat: FWDFU BLE command - reboot into UF2 mass-storage DFU (pre-updat…
claude Aug 2, 2026
6340984
plan 0004: SD-direct OTA staging spike doc (FWDFU pre-update + rework…
claude Aug 2, 2026
2656caa
Merge branch 'BETA' into claude/uf2-dfu-command-beta
TheAngryRaven Aug 2, 2026
8c31bbc
Merge pull request #118 from TheAngryRaven/claude/uf2-dfu-command-beta
TheAngryRaven Aug 2, 2026
b16c42e
perf: generate the crossing animation instead of storing 2 KB of bitmaps
claude Aug 2, 2026
31660bf
Merge pull request #119 from TheAngryRaven/claude/generate-crossing-p…
TheAngryRaven Aug 3, 2026
dda33f0
feat: BLE sprint-track sync opcodes (TSLIST/TSGET/TSPUT/TSDEL)
claude Aug 3, 2026
3cc4753
Merge pull request #120 from TheAngryRaven/claude/ble-sprint-track-sync
TheAngryRaven Aug 3, 2026
2e2f538
ci: pick the build channel from the source tree, not just the target
claude Aug 3, 2026
403033a
Merge pull request #121 from TheAngryRaven/claude/sprint-mode-race-ty…
TheAngryRaven Aug 3, 2026
d11d7d6
feat: raise the OTA image cap 320 KiB -> 408 KiB by splitting flash e…
claude Aug 3, 2026
4b9ddea
Merge pull request #122 from TheAngryRaven/claude/ota-flash-rebalance
TheAngryRaven Aug 3, 2026
f0e2178
docs(plan 0004): park SD-direct apply to end of project; DDV upgrade-…
claude Aug 3, 2026
0deb18f
Merge pull request #125 from TheAngryRaven/claude/ota-upgrade-path-notes
TheAngryRaven Aug 3, 2026
4192612
docs: fix the DOVEX header column names to match what is emitted
claude Aug 3, 2026
3d7f86a
Merge pull request #126 from TheAngryRaven/claude/dovex-header-doc-fix
TheAngryRaven Aug 3, 2026
5d0da0b
plan 0002: course-creator model and the firmware's first track-JSON w…
claude Aug 3, 2026
4c1ea53
plan 0002: write a walked course to the SD card
claude Aug 3, 2026
92b968f
plan 0002: Create Course on the main menu — five screens over the model
claude Aug 3, 2026
51e0e60
plan 0002: golden-test the course creator through the real menus
claude Aug 3, 2026
6712205
plan 0002: document the course creator and close out the sprint plan
claude Aug 3, 2026
a7a3152
Merge pull request #128 from TheAngryRaven/claude/sprint-run-log-pars…
TheAngryRaven Aug 4, 2026
cf9f028
fix: say which GPS milestone is outstanding on the status page
claude Aug 4, 2026
6cec29d
fix: say which GPS milestone is outstanding on the status page
claude Aug 4, 2026
037a692
Merge pull request #129 from TheAngryRaven/claude/gps-status-time-sync
TheAngryRaven Aug 4, 2026
c2457d2
Merge pull request #130 from TheAngryRaven/claude/sprint-run-log-pars…
TheAngryRaven Aug 4, 2026
a3282af
fix: don't let auto-race hijack the menu the instant you land on it
claude Aug 4, 2026
dd277e2
Merge pull request #131 from TheAngryRaven/claude/auto-race-menu-grace
TheAngryRaven Aug 5, 2026
e8d9a5b
fix: show the crossing animation only on racing pages
claude Aug 4, 2026
9ae6d46
Merge pull request #132 from TheAngryRaven/claude/crossing-overlay-ra…
TheAngryRaven Aug 5, 2026
a440cd8
feat(tach): true RPM from spark mode and cylinder count (plan 0003)
claude Aug 6, 2026
fb0023a
fix(tach): derive the pulse gap with integer math, not a rounding cast
claude Aug 6, 2026
1a1dbfd
Merge pull request #133 from TheAngryRaven/claude/rpm-spark-cylinders
TheAngryRaven Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ jobs:
run: |
arduino-cli compile \
--fqbn "${{ matrix.board.fqbn }}" \
--build-property "compiler.cpp.extra_flags=-D${{ matrix.board.define }} -DFIRMWARE_VERSION_OVERRIDE=${{ steps.ver.outputs.betaver }} -DSERIAL_BUFFER_SIZE=256 -DBIRDSEYE_ENABLE_SENSOREGG=1" \
--build-property "compiler.cpp.extra_flags=-D${{ matrix.board.define }} -DFIRMWARE_VERSION_OVERRIDE=${{ steps.ver.outputs.betaver }} -DSERIAL_BUFFER_SIZE=256 -DDOVES_DISABLE_DEBUG -DBIRDSEYE_ENABLE_SENSOREGG=1" \
--output-dir dist \
--warnings none \
BirdsEye
Expand Down
59 changes: 44 additions & 15 deletions .github/workflows/compile-sketch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,31 @@ jobs:
name: ${{ matrix.board.name }}
runs-on: ubuntu-latest
env:
# DovesLapTimer ref for this build: anything targeting (or running on)
# the BETA branch tracks the library's own BETA branch, so the two beta
# channels move together; everything else pins the known-good release
# tag (bump deliberately). base_ref is only set on pull_request events;
# ref_name covers push / workflow_dispatch.
LAPTIMER_REF: ${{ (github.base_ref == 'BETA' || github.ref_name == 'BETA') && 'BETA' || 'v4.2.0' }}
# Feature flags for this build, matched to the channel the code is
# headed for (see project.h). BETA enables the SensorEgg POC exactly
# as beta.yml does, so the flag-on build is compile-checked on the PR
# rather than first failing on the publish workflow. Everything else
# builds the master/release defaults (all flags off).
FEATURE_FLAGS: ${{ (github.base_ref == 'BETA' || github.ref_name == 'BETA') && '-DBIRDSEYE_ENABLE_SENSOREGG=1' || '' }}
# Channel this build belongs to. The channel follows the SOURCE tree
# being compiled, not just where it is headed:
# base_ref == BETA -> a feature PR into BETA (compiling BETA-era code)
# head_ref == BETA -> the long-lived BETA -> master integration PR
# (#113). Its source is still BETA's, so it must
# build with BETA's library + flags; without this
# it compiled BETA code against master's pins and
# failed on the sprint headers forever.
# ref_name == BETA -> push / workflow_dispatch on BETA itself
# Anything else is master/release. NOTE: merging BETA into master is
# what promotes the channel — at that point the fallbacks below must be
# bumped deliberately (a DovesLapTimer release tag carrying
# CrossingEngine/SprintTimer, plus a decision on the SensorEgg flag),
# here and in sim-build.yml / release.yml. (The condition is repeated
# rather than hoisted into its own env var because a job-level `env:`
# entry cannot reference its siblings.)
# DovesLapTimer ref: the beta channel tracks the library's own BETA
# branch so the two move together; everything else pins the known-good
# release tag.
LAPTIMER_REF: ${{ (github.base_ref == 'BETA' || github.head_ref == 'BETA' || github.ref_name == 'BETA') && 'BETA' || 'v4.2.0' }}
# Feature flags for this build (see project.h). BETA enables the
# SensorEgg POC exactly as beta.yml does, so the flag-on build is
# compile-checked on the PR rather than first failing on the publish
# workflow. Everything else builds the master/release defaults (off).
FEATURE_FLAGS: ${{ (github.base_ref == 'BETA' || github.head_ref == 'BETA' || github.ref_name == 'BETA') && '-DBIRDSEYE_ENABLE_SENSOREGG=1' || '' }}
# Build both XIAO nRF52840 variants. The Sense board has the onboard
# LSM6DS3 IMU; the plain board does not (accelerometer logging degrades
# gracefully). Same MCU/BLE/bootloader otherwise.
Expand Down Expand Up @@ -74,7 +87,7 @@ jobs:
# project.h static_asserts on it — keep all three workflows in sync.
cli-compile-flags: |
- --build-property
- compiler.cpp.extra_flags=-D${{ matrix.board.define }} -DSERIAL_BUFFER_SIZE=256 ${{ env.FEATURE_FLAGS }}
- compiler.cpp.extra_flags=-D${{ matrix.board.define }} -DSERIAL_BUFFER_SIZE=256 -DDOVES_DISABLE_DEBUG ${{ env.FEATURE_FLAGS }}
enable-deltas-report: true

# Hard flash-size gate. The XIAO nRF52840's app flash is shared
Expand Down Expand Up @@ -126,10 +139,21 @@ jobs:
# bootloader is exactly that size. The program's flash usage equals the
# app .bin that gets streamed/flashed, so gate it against that cap: warn
# as it approaches, fail if it crosses (the image would be un-OTA-able).
#
# There are TWO caps, because the cap lives in the *installed* firmware,
# not in the image being delivered:
# OTA_IMAGE_MAX_BYTES — this layout's cap. Hard failure past it.
# OTA_LEGACY_MAX_BYTES — the cap on units still running <= 3.0.x, which
# stage at the old 0xA4000. Those units reject a larger image with
# FWERR:SIZE and would need the USB UF2 path (FWDFU) instead, so
# crossing it is a warning: it means the fleet has split.
# Drop the legacy warning once every unit you care about has taken a
# build carrying the 0x8E000 layout.
- name: OTA image size gate
env:
# Keep in sync with FW_MAX_IMAGE_SIZE in BirdsEye/firmware_ota.ino.
OTA_IMAGE_MAX_BYTES: 327680 # 320 KiB
OTA_IMAGE_MAX_BYTES: 417792 # 408 KiB
OTA_LEGACY_MAX_BYTES: 327680 # 320 KiB — pre-3.1 staging layout
OTA_WARN_PERCENT: 90
run: |
report="$(find sketches-reports -name '*.json' | head -1)"
Expand All @@ -149,13 +173,18 @@ jobs:
pct="$(awk "BEGIN { printf \"%.1f\", ($flash_abs / $OTA_IMAGE_MAX_BYTES) * 100 }")"
echo "OTA image: ${flash_abs} / ${OTA_IMAGE_MAX_BYTES} bytes (${pct}% of the OTA cap)"
if [ "$flash_abs" -gt "$OTA_IMAGE_MAX_BYTES" ]; then
echo "::error::Image ${flash_abs} B exceeds the OTA cap ${OTA_IMAGE_MAX_BYTES} B — FWBEGIN would reject it (FWERR:SIZE). Shrink the image or raise FW_MAX_IMAGE_SIZE (which moves the staging base down, costing app space)."
echo "::error::Image ${flash_abs} B exceeds the OTA cap ${OTA_IMAGE_MAX_BYTES} B — FWBEGIN would reject it (FWERR:SIZE). Shrink the image, or finish plan 0004's SD-direct apply (which removes the staging region and the cap with it)."
exit 1
fi
warn="$(awk "BEGIN { print (($flash_abs / $OTA_IMAGE_MAX_BYTES) * 100 >= ${OTA_WARN_PERCENT}) ? 1 : 0 }")"
if [ "$warn" = "1" ]; then
echo "::warning::OTA image at ${pct}% of the ${OTA_IMAGE_MAX_BYTES} B cap (>= ${OTA_WARN_PERCENT}%) — getting close to un-OTA-able; plan to shrink or raise the cap."
fi
# Fleet-split check: an image past the legacy cap can no longer be
# installed by units still running the pre-3.1 staging layout.
if [ "$flash_abs" -gt "$OTA_LEGACY_MAX_BYTES" ]; then
echo "::warning::Image ${flash_abs} B exceeds the legacy ${OTA_LEGACY_MAX_BYTES} B cap — units still on <= 3.0.x will answer FWERR:SIZE and can only take this build over USB (FWDFU -> UF2). Ship a <= ${OTA_LEGACY_MAX_BYTES} B build first if any such unit still needs to migrate."
fi

- name: Upload size deltas report
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
run: |
arduino-cli compile \
--fqbn "${{ matrix.board.fqbn }}" \
--build-property "compiler.cpp.extra_flags=-D${{ matrix.board.define }} -DSERIAL_BUFFER_SIZE=256" \
--build-property "compiler.cpp.extra_flags=-D${{ matrix.board.define }} -DSERIAL_BUFFER_SIZE=256 -DDOVES_DISABLE_DEBUG" \
--output-dir dist \
--warnings none \
BirdsEye
Expand Down
39 changes: 27 additions & 12 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ own header first so declaration/definition drift is caught at compile
time.

The **pure units** (`haversine`, `gps_time`, `gps_validation`,
`dovex_header`, `filename_validator`) deliberately avoid Arduino headers.
`dovex_header`, `filename_validator`, `course_creator`, `track_json`, …)
deliberately avoid Arduino headers.
The *same* `.cpp` is compiled into both the firmware (Arduino picks up
`.cpp` files in the sketch folder) and the host test binary (CMake). There
is no copy-paste — the tests exercise the exact code that ships.
Expand Down Expand Up @@ -97,16 +98,25 @@ to the matching `*_LOOP()`.
remote-button notifications.
- **SensorEgg** (`sensoregg` + the `sensoregg_protocol` pure unit) —
wireless EGT proof of concept: a passive BLE observer receives the
DovesSensorEgg pod's `PW-ADV-1` advertising broadcasts (14-byte
manufacturer data: EGT + cold junction as int16 deci-degC, fault flags,
sequence counter) and feeds the `Temp1`/`Junction1` DOVEX columns and
the Temp1 race page. Observer + peripheral coexist natively on S140;
passive scanning never transmits, so the camera link is untouched.
Readings older than 1 s go NaN — never held across a dropout. Gated on
the `BIRDSEYE_ENABLE_SENSOREGG` build flag: on in the beta channel, off
in master/release, where the scanner and Temp1 page are compiled out,
BLE returns to lazy init, and the DOVEX `Temp1`/`Junction1` columns are
written as `nan` so the log format stays identical across channels.
DovesSensorEgg pod's `PW-ADV` advertising broadcasts, v1 (14-byte:
EGT + cold junction as int16 deci-degC, fault flags, sequence counter)
and v2 (16-byte: + aux intake-air thermistor, real battery percent),
and feeds the `Temp1`/`Junction1`/`Temp2` DOVEX columns and the
Temp1/Temp2 race pages. Observer + peripheral coexist natively on
S140; passive scanning never transmits, so the camera link is
untouched. Readings older than 1 s go NaN — never held across a
dropout. Gated on the `BIRDSEYE_ENABLE_SENSOREGG` build flag: on in
the beta channel, off in master/release, where the scanner and temp
pages are compiled out, BLE returns to lazy init, and the DOVEX
`Temp1`/`Junction1`/`Temp2` columns are written as `nan` so the log
format stays identical across channels.
- **Course creator** (`course_creator` + `track_json` pure units, glued
into the menu/pages/SD modules) — authors a track course on the device
by walking to each cone and holding for a 3 s GPS average. Autocross
venues re-lay their course every event, so the alternative was a laptop
in a paddock. No text is ever entered on-device: names come from the GPS
clock and are renamed later in the web app. This is also the firmware's
only track-JSON *writer* — everywhere else the format is read-only.
- **Replay** (`replay`) — instant DOVEX header replay.
- **Settings** (`settings`) — JSON key/value store on the SD card.
- **CourseManager** (external library) — owns course detection, sector
Expand Down Expand Up @@ -181,7 +191,12 @@ the last resort.
### Shutdown is System OFF, wake is a reboot
There is no power switch (deliberately — the next hardware revision drops
it), so "off" is nRF52 **System OFF** at ~µA with GPIO SENSE armed on the
tach pin and the three buttons, plus VBUS. Waking is a full chip reset:
tach pin and the three buttons, plus VBUS. The tach's SENSE polarity is
not hardcoded: the pickup circuit's output stage idles high or low
depending on the build, so shutdown samples the parked line and arms
SENSE for the opposite level (majority vote in the host-tested
`wake_cause` unit) — arming toward the idle level satisfied DETECT
immediately and battery sleep reboot-looped. Waking is a full chip reset:
`setup()` runs fresh, and the very first thing it does is read (then
clear) the sticky `RESETREAS` + GPIO `LATCH` registers to decode *why* it
booted (the host-tested `wake_cause` unit). An engine-start (tach) wake
Expand Down
Loading
Loading