Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .github/workflows/beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ jobs:
"SparkFun u-blox GNSS v3" \
"Seeed Arduino LSM6DS3" \
"ArxTypeTraits"
arduino-cli lib install --git-url https://github.com/TheAngryRaven/DovesLapTimer.git
# Beta builds track the lap-timer library's own BETA branch so the
# two beta channels move together (master/release pin a tag).
arduino-cli lib install --git-url https://github.com/TheAngryRaven/DovesLapTimer.git#BETA
pip install --user adafruit-nrfutil
- name: Compile
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/clang-tidy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ jobs:
BirdsEye/gps_time.cpp \
BirdsEye/gps_validation.cpp \
BirdsEye/dovex_header.cpp \
BirdsEye/filename_validator.cpp
BirdsEye/filename_validator.cpp \
BirdsEye/sd_access_policy.cpp \
BirdsEye/lap_format.cpp \
BirdsEye/tach_filter.cpp
8 changes: 8 additions & 0 deletions .github/workflows/compile-sketch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ jobs:
compile:
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.1.0' }}
# 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 @@ -52,6 +59,7 @@ jobs:
- name: Seeed Arduino LSM6DS3
- name: ArxTypeTraits
- source-url: https://github.com/TheAngryRaven/DovesLapTimer.git
version: ${{ env.LAPTIMER_REF }}
sketch-paths: |
- BirdsEye
cli-compile-flags: |
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ jobs:
"SparkFun u-blox GNSS v3" \
"Seeed Arduino LSM6DS3" \
"ArxTypeTraits"
arduino-cli lib install --git-url https://github.com/TheAngryRaven/DovesLapTimer.git
# Release builds pin the lap-timer library to a known-good tag —
# bump deliberately. (Beta builds track its BETA branch instead.)
arduino-cli lib install --git-url https://github.com/TheAngryRaven/DovesLapTimer.git#v4.1.0
pip install --user adafruit-nrfutil

- name: Compile
Expand Down
20 changes: 17 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,23 @@ the main loop, and the GPS library reads from that buffer. This is why
The BLE callbacks run in a **separate FreeRTOS task** from `loop()`, and
SdFat is not thread-safe. Every SD user (logging, replay, BLE transfer,
track parsing) must take a single mutex via `acquireSDAccess(mode)` /
`releaseSDAccess(mode)`. BLE commands that need the card defer their work
into `BLUETOOTH_LOOP()` (main-loop context) instead of touching SdFat from
the callback.
`releaseSDAccess(mode)`. Two layers make this sound:

1. **Atomic transitions.** `acquireSDAccess()` evaluates the grant rules
and commits the new owner inside a FreeRTOS critical section
(`taskENTER_CRITICAL`, BASEPRI-masked so the SoftDevice's radio
interrupts are untouched) — a plain check-then-set on the shared flag
would be a TOCTOU between the two tasks. The grant/deny decision table
itself (same-mode re-acquire is idempotent; the brief `TRACK_PARSE`
mode is preemptible as leak recovery) is the host-tested
`sd_access_policy` pure unit.
2. **Single-task SdFat.** *Every* BLE command that touches the card —
`LIST`/`GET`/`DELETE`, `TLIST`/`TGET`/`TPUT`/`TDEL`, settings, firmware
OTA — is parsed in the callback (filenames validated there, RAM only)
and executed by `BLUETOOTH_LOOP()` on the main loop. Nothing calls
SdFat from the Bluefruit callback task, so the filesystem only ever
has one task in it; directory listings hold the lock for the whole
walk, and `DELETE` refuses while a transfer is streaming.

### DOVEX crash safety
A `.dovex` file reserves the first 1 KB for session metadata (driver,
Expand Down
43 changes: 29 additions & 14 deletions BirdsEye/BirdsEye.ino
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,17 @@ volatile uint32_t tachLastPulseUs = 0;
volatile bool tachHavePeriod = false;

// Ring buffer: ISR writes pulse timestamps, TACH_LOOP reads and computes periods.
// Single-producer (ISR writes head), single-consumer (TACH_LOOP reads tail).
// 16 entries handles up to 20k RPM with 48ms of main-loop stall margin.
// Single-producer (ISR writes head), single-consumer (TACH_LOOP writes tail).
// The ISR checks full before publishing (one slot sacrificed so head==tail
// means empty) and drops + flags instead of lapping the consumer: SD GC
// stalls can block the main loop for 100 ms–2 s, far past what any sane
// ring size covers at racing RPM. tachRingTail is volatile because the ISR
// reads it for the full check.
static const uint8_t TACH_RING_SIZE = 16;
volatile uint32_t tachRingBuf[TACH_RING_SIZE];
volatile uint8_t tachRingHead = 0; // ISR write index (only ISR writes)
static uint8_t tachRingTail = 0; // Main-loop read index (only TACH_LOOP writes)
volatile uint8_t tachRingTail = 0; // Main-loop read index (only TACH_LOOP writes)
volatile bool tachRingOverflow = false; // ISR sets on drop; TACH_LOOP clears

// Tunable constants
static const float tachRevsPerPulse = 1.0f; // Wasted spark = 1 pulse/rev
Expand Down Expand Up @@ -380,16 +385,11 @@ File replayFile;

///////////////////////////////////////////
// SD CARD ACCESS STATE MANAGEMENT
// Prevents race conditions between logging, replay, and BLE file transfers
// Prevents race conditions between logging, replay, and BLE file transfers.
// The SD_ACCESS_* modes come from sd_functions.h (aliases of the host-tested
// sd_access_policy constants); transitions are made atomically by
// acquireSDAccess() / releaseSDAccess() in sd_functions.ino.
///////////////////////////////////////////
// Note: Using #define instead of enum to avoid Arduino preprocessor issues
// (Arduino generates function prototypes before seeing enum definitions)
#define SD_ACCESS_NONE 0 // SD card not in use by any subsystem
#define SD_ACCESS_LOGGING 1 // Data logging active (dataFile in use)
#define SD_ACCESS_REPLAY 2 // Replay mode active (replayFile in use)
#define SD_ACCESS_BLE_TRANSFER 3 // BLE file transfer active (bleCurrentFile in use)
#define SD_ACCESS_TRACK_PARSE 4 // Track file parsing (temporary, should release quickly)

volatile int currentSDAccess = SD_ACCESS_NONE;

// Replay function prototypes (must be after SdFat include for File type)
Expand Down Expand Up @@ -775,6 +775,16 @@ bool activeTimerSectorsConfigured() {
void trackDetectionLoop() {
if (trackDetected || !gpsData.fix || trackManifestCount == 0) return;

// Throttle the scan to 1 Hz. Each haversineDistanceMiles() is several
// software-emulated double libm calls (the M4F FPU is single-precision
// only); at the 200-entry manifest ceiling a full scan costs multiple
// milliseconds. gpsData.fix stays true BETWEEN PVT updates, so without
// this gate the scan ran every ~250 Hz loop iteration — collapsing the
// loop rate — for an answer that changes at driving pace.
static unsigned long lastManifestScan = 0;
if (millis() - lastManifestScan < 1000) return;
lastManifestScan = millis();

double bestDist = 999999.0;
int bestIndex = -1;

Expand Down Expand Up @@ -1066,11 +1076,16 @@ void enterSleepMode() {
digitalWrite(PIN_LSM6DS3TR_C_POWER, HIGH); // HIGH = disable power
}

// 5. Set state
// 5. Re-arm the tach RPM wake trigger and drop pre-sleep pulse state.
// The ISR stays attached — the NEXT valid pulse sets tachHavePeriod and
// wakes straight into race mode. Without this clear, the flag stays
// latched from the first pulse since boot and sleep instantly bounces.
TACH_SLEEP();

// 6. Set state
sleepModeActive = true;
sleepEnteredAt = millis();
sleepLastGpsWake = millis();
// Tach ISR stays attached -- RPM pulses will wake via tachHavePeriod
}

void exitSleepMode(bool rpmWake = false) {
Expand Down
Loading
Loading