|
| 1 | +# DovesLapTimer - Project Guide |
| 2 | + |
| 3 | +> **IMPORTANT**: Always keep this CLAUDE.md file updated when making changes to the codebase. |
| 4 | +> When adding new features, modifying APIs, fixing bugs, or changing architecture, update the |
| 5 | +> relevant sections below so future sessions don't waste tokens re-exploring the project. |
| 6 | +
|
| 7 | +## Project Overview |
| 8 | + |
| 9 | +**What**: GPS-based lap timing Arduino library for go-kart / racing applications. |
| 10 | +**Author**: Michael Champagne (crimsondove) |
| 11 | +**Version**: 3.1.1 |
| 12 | +**Repo**: https://github.com/TheAngryRaven/DovesLapTimer |
| 13 | +**License**: GPL v3 |
| 14 | +**Dependency**: ArxTypeTraits (auto-included by Arduino Library Manager) |
| 15 | + |
| 16 | +The library does NOT interface with GPS hardware directly. You feed it coordinates and time, |
| 17 | +it handles crossing detection, lap timing, sector splits, distance tracking, and pace comparison. |
| 18 | + |
| 19 | +## Directory Structure |
| 20 | + |
| 21 | +``` |
| 22 | +DovesLapTimer/ |
| 23 | +├── src/ |
| 24 | +│ ├── DovesLapTimer.h # Header - class definition, structs, public API (535 lines) |
| 25 | +│ └── DovesLapTimer.cpp # Implementation - all logic (929 lines) |
| 26 | +├── examples/ |
| 27 | +│ ├── basic_oled_example/ # Full working example with OLED + uBlox GPS |
| 28 | +│ │ ├── basic_oled_example.ino |
| 29 | +│ │ ├── display_config.h # OLED init (SSD1306 or SH110X) |
| 30 | +│ │ ├── gps_config.h # uBlox binary config commands |
| 31 | +│ │ └── images.h # Bitmap data for UI |
| 32 | +│ ├── sector_timing_example/ # Demonstrates 3-sector timing |
| 33 | +│ │ └── sector_timing_example.ino |
| 34 | +│ └── real_track_data_debug/ # Replays real NMEA data (no GPS needed) |
| 35 | +│ ├── real_track_data_debug.ino |
| 36 | +│ ├── gps_race_data_2laps.h |
| 37 | +│ ├── gps_race_data_lap.h |
| 38 | +│ ├── gps_race_data_long_lap.h |
| 39 | +│ └── gps_race_data_praga_laps.h |
| 40 | +├── wokwi/ # Wokwi simulator custom GPS chip |
| 41 | +│ ├── gps-fake.chip.c |
| 42 | +│ └── gps-fake.chip.json |
| 43 | +├── images/ # Diagrams for HELPME.md |
| 44 | +├── library.properties # Arduino library metadata |
| 45 | +├── README.md # User-facing docs and API reference |
| 46 | +├── HELPME.md # Technical deep-dive on crossing detection algorithm |
| 47 | +├── LICENSE # GPL v3 (conflicts with library.properties MIT claim) |
| 48 | +└── .github/FUNDING.yml |
| 49 | +``` |
| 50 | + |
| 51 | +## Architecture & Key Concepts |
| 52 | + |
| 53 | +### Core Class: `DovesLapTimer` |
| 54 | + |
| 55 | +**Constructor**: `DovesLapTimer(double crossingThresholdMeters = 7, Stream *debugSerial = NULL)` |
| 56 | + |
| 57 | +### Main Loop Flow |
| 58 | +1. User calls `updateCurrentTime(millisecondsSinceMidnight)` with GPS time |
| 59 | +2. User calls `loop(lat, lng, altMeters, speedKnots)` on each GPS fix |
| 60 | +3. `loop()` updates odometer (haversine3D), speed, then checks all crossing lines |
| 61 | +4. `checkStartFinish()` and `checkSectorLine()` handle detection + interpolation |
| 62 | + |
| 63 | +### Crossing Detection Algorithm (the hard part) |
| 64 | +- Uses **hypotenuse-based threshold** (not simple distance-to-line) |
| 65 | +- Calculates hypotenuse from `crossingLineWidth` and `crossingThresholdMeters` |
| 66 | +- Checks driver distance to EACH endpoint of crossing line vs hypotenuse |
| 67 | +- When inside threshold: buffers GPS points in `crossingPointBuffer` |
| 68 | +- When exiting threshold: calls `interpolateCrossingPoint()` to find exact crossing |
| 69 | + |
| 70 | +### Interpolation Methods |
| 71 | +- **Linear** (default, `forceLinear = true`): distance+speed weighted interpolation |
| 72 | +- **Catmull-Rom spline**: uses 4 control points, falls back to linear if insufficient points |
| 73 | +- Known issue: Catmull-Rom has a bug (noted in basic_oled_example setup comment) |
| 74 | + |
| 75 | +### Sector Timing |
| 76 | +- 3 sectors: Start/Finish -> Sector2 -> Sector3 -> Start/Finish |
| 77 | +- Requires both sector 2 and sector 3 lines to be configured |
| 78 | +- Validates crossing order (rejects out-of-order crossings) |
| 79 | +- Tracks best sector times and lap numbers independently |
| 80 | + |
| 81 | +### Memory Management |
| 82 | +- Circular buffer `crossingPointBuffer` sized by available RAM: |
| 83 | + - `>3000 bytes RAM`: 100 entries |
| 84 | + - `<=3000 bytes RAM`: 25 entries |
| 85 | +- Buffer is shared between all line crossings (start/finish + sectors) |
| 86 | +- Mutual exclusion: only one line can be "crossing" at a time |
| 87 | + |
| 88 | +## Public API Quick Reference |
| 89 | + |
| 90 | +### Setup Methods |
| 91 | +| Method | Description | |
| 92 | +|--------|-------------| |
| 93 | +| `setStartFinishLine(aLat, aLng, bLat, bLng)` | Define start/finish line | |
| 94 | +| `setSector2Line(aLat, aLng, bLat, bLng)` | Define sector 2 split line | |
| 95 | +| `setSector3Line(aLat, aLng, bLat, bLng)` | Define sector 3 split line | |
| 96 | +| `updateCurrentTime(millisSinceMidnight)` | Set GPS time (call before loop) | |
| 97 | +| `forceLinearInterpolation()` | Use linear interpolation (default) | |
| 98 | +| `forceCatmullRomInterpolation()` | Use Catmull-Rom spline (has known issues) | |
| 99 | +| `reset()` | Reset all state to zero | |
| 100 | + |
| 101 | +### Timing Getters |
| 102 | +| Method | Returns | |
| 103 | +|--------|---------| |
| 104 | +| `getCurrentLapTime()` | Current lap elapsed ms | |
| 105 | +| `getLastLapTime()` | Last completed lap ms | |
| 106 | +| `getBestLapTime()` | Best lap ms | |
| 107 | +| `getCurrentLapSector1Time()` | Current lap S1 ms | |
| 108 | +| `getCurrentLapSector2Time()` | Current lap S2 ms | |
| 109 | +| `getCurrentLapSector3Time()` | Current lap S3 ms | |
| 110 | +| `getBestSector1Time()` | Best S1 ms (any lap) | |
| 111 | +| `getBestSector2Time()` | Best S2 ms (any lap) | |
| 112 | +| `getBestSector3Time()` | Best S3 ms (any lap) | |
| 113 | +| `getOptimalLapTime()` | Sum of best sectors | |
| 114 | + |
| 115 | +### State/Distance Getters |
| 116 | +| Method | Returns | |
| 117 | +|--------|---------| |
| 118 | +| `getRaceStarted()` | True after first line crossing | |
| 119 | +| `getCrossing()` | True while inside crossing zone | |
| 120 | +| `getLaps()` | Completed lap count | |
| 121 | +| `getBestLapNumber()` | Lap # of best time | |
| 122 | +| `getCurrentSector()` | 0=not started, 1/2/3 | |
| 123 | +| `areSectorLinesConfigured()` | True if both sector lines set | |
| 124 | +| `getCurrentLapDistance()` | Current lap meters | |
| 125 | +| `getLastLapDistance()` | Last lap meters | |
| 126 | +| `getBestLapDistance()` | Best lap meters | |
| 127 | +| `getTotalDistanceTraveled()` | Odometer meters | |
| 128 | +| `getPaceDifference()` | Pace delta vs best lap | |
| 129 | + |
| 130 | +### Public Utility Methods (also used by examples for display) |
| 131 | +| Method | Description | |
| 132 | +|--------|-------------| |
| 133 | +| `insideLineThreshold(...)` | Check if point is in crossing detection zone | |
| 134 | +| `pointLineSegmentDistance(...)` | Distance from point to line segment (meters) | |
| 135 | +| `haversine(lat1, lon1, lat2, lon2)` | Great-circle distance (meters) | |
| 136 | +| `haversine3D(...)` | 3D distance including altitude | |
| 137 | +| `isObtuseTriangle(...)` | Triangle type check | |
| 138 | +| `pointOnSideOfLine(...)` | Which side of line a point is on | |
| 139 | + |
| 140 | +## Known Issues & TODOs (from code comments) |
| 141 | + |
| 142 | +1. ~~**Catmull-Rom interpolation bug**~~: Fixed - spline now only used for lat/lng (not time/odometer), and previous GPS fix inserted as pre-crossing control point |
| 143 | +2. **Altitude messing up distance**: `loop()` line 27 has TODO: "I think alt is messing up, investigate more... maybe flag?" |
| 144 | +3. **Early abort bug**: checkStartFinish line 148-149 TODO about aborting early causing re-check immediately |
| 145 | +4. **`checkStartFinish` portability**: Line 106 TODO to make more portable for split timing |
| 146 | +5. ~~**License mismatch**~~: Resolved - GPL v3, library.properties updated |
| 147 | +6. ~~**Header comment outdated**~~: Fixed - updated to mention 3-sector timing |
| 148 | +7. ~~**No keywords.txt**~~: Added |
| 149 | +8. ~~**No .gitignore**~~: Added |
| 150 | + |
| 151 | +## Supported Hardware |
| 152 | + |
| 153 | +- **MCU**: Seeed XIAO nRF52840 (recommended), Arduino Mega+ (minimum) |
| 154 | +- **GPS**: Matek SAM-M10Q (25Hz), Matek SAM-M8Q (18Hz) - uBlox modules |
| 155 | +- **Display**: 128x64 I2C OLED (SSD1306 or SH110X) |
| 156 | +- **GPS Library**: Adafruit_GPS |
| 157 | +- **Display Library**: Adafruit GFX + Adafruit_SSD1306 or Adafruit_SH110x |
| 158 | + |
| 159 | +## Testing |
| 160 | + |
| 161 | +Use the `real_track_data_debug` example to replay real NMEA data without GPS hardware. |
| 162 | +Track data from Orlando Kart Center is included. Compare results against MyLaps timing. |
| 163 | + |
| 164 | +## Cross-Reference: Related Repos |
| 165 | + |
| 166 | +(Update this section as the other two repos are set up) |
| 167 | +- DovesLapTimer (this repo) - Core timing library |
| 168 | +- [Add other repos here as they are created] |
0 commit comments