Skip to content

Commit 9302a47

Browse files
TheAngryRavenclaude
andcommitted
Fix Catmull-Rom interpolation bugs and clean up crossing point search
- Fix Bug B: Use Catmull-Rom only for lat/lng, linear for time/odometer to prevent spline overshoot producing garbage unsigned long values - Fix Bug C: Store previous GPS fix snapshot and insert as pre-crossing buffer entry, giving Catmull-Rom its p0 control point on fast passes - Clean up interpolateCrossingPoint: rename misleading "best" variables to "crossing", remove dead comparison (break makes it single-match), deduplicate t/distA/distB computation across interpolation branches - Update CLAUDE.md known issues to reflect Catmull-Rom fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e7db1f4 commit 9302a47

3 files changed

Lines changed: 94 additions & 69 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ DovesLapTimer/
139139

140140
## Known Issues & TODOs (from code comments)
141141

142-
1. **Catmull-Rom interpolation bug**: basic_oled_example forces linear with comment "CURRENTLY REQUIRED: BUG IN CATMULROM INTERPOLATION METHOD"
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
143143
2. **Altitude messing up distance**: `loop()` line 27 has TODO: "I think alt is messing up, investigate more... maybe flag?"
144144
3. **Early abort bug**: checkStartFinish line 148-149 TODO about aborting early causing re-check immediately
145145
4. **`checkStartFinish` portability**: Line 106 TODO to make more portable for split timing

src/DovesLapTimer.cpp

Lines changed: 85 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ int DovesLapTimer::loop(double currentLat, double currentLng, float currentAltit
102102
}
103103
}
104104

105+
// Save current fix as previous for next iteration's Catmull-Rom pre-crossing point
106+
prevFixLat = currentLat;
107+
prevFixLng = currentLng;
108+
prevFixTime = millisecondsSinceMidnight;
109+
prevFixOdometer = totalDistanceTraveled;
110+
prevFixSpeedKmh = currentSpeedkmh;
111+
hasPrevFix = true;
112+
105113
return nearAnyLine ? 0 : -1;
106114
}
107115

@@ -256,9 +264,18 @@ bool DovesLapTimer::checkStartFinish(double currentLat, double currentLng) {
256264
debugln();
257265
debugln(F("we are possibly crossing"));
258266
crossing = true;
259-
// crossingStartedLineSide = pointOnSideOfLine(currentLat, currentLng, startFinishPointALat, startFinishPointALng, startFinishPointBLat, startFinishPointBLng);
260267

261-
// Capture this first point - it's important context for Catmull-Rom interpolation
268+
// Insert previous GPS fix as pre-crossing point (gives Catmull-Rom its p0 control point)
269+
if (hasPrevFix) {
270+
crossingPointBuffer[crossingPointBufferIndex].lat = prevFixLat;
271+
crossingPointBuffer[crossingPointBufferIndex].lng = prevFixLng;
272+
crossingPointBuffer[crossingPointBufferIndex].time = prevFixTime;
273+
crossingPointBuffer[crossingPointBufferIndex].odometer = prevFixOdometer;
274+
crossingPointBuffer[crossingPointBufferIndex].speedKmh = prevFixSpeedKmh;
275+
crossingPointBufferIndex = (crossingPointBufferIndex + 1) % crossingPointBufferSize;
276+
}
277+
278+
// Capture current point
262279
crossingPointBuffer[crossingPointBufferIndex].lat = currentLat;
263280
crossingPointBuffer[crossingPointBufferIndex].lng = currentLng;
264281
crossingPointBuffer[crossingPointBufferIndex].time = millisecondsSinceMidnight;
@@ -449,16 +466,15 @@ double DovesLapTimer::catmullRom(double p0, double p1, double p2, double p3, dou
449466
void DovesLapTimer::interpolateCrossingPoint(double& crossingLat, double& crossingLng, unsigned long& crossingTime, double& crossingOdometer, double pointALat, double pointALng, double pointBLat, double pointBLng) {
450467
int numPoints = crossingPointBufferFull ? crossingPointBufferSize : crossingPointBufferIndex;
451468

452-
// Variables to store the best pair of points
453-
int bestIndexA = -1;
454-
int bestIndexB = -1;
455-
double bestSumDistances = INFINITY;
469+
// Find the first pair of consecutive buffer points on opposite sides of the crossing line.
470+
// In a normal pass there is exactly one such pair - the two GPS fixes that straddle the line.
471+
int crossingIndexA = -1;
472+
int crossingIndexB = -1;
473+
double crossingSumDistances = INFINITY;
456474

457-
// Iterate through the crossingPointBuffer, comparing the sum of distances from the start/finish line of each pair of consecutive points
458475
for (int i = 0; i < numPoints - 1; i++) {
459476
double distA = pointLineSegmentDistance(crossingPointBuffer[i].lat, crossingPointBuffer[i].lng, pointALat, pointALng, pointBLat, pointBLng);
460477
double distB = pointLineSegmentDistance(crossingPointBuffer[i + 1].lat, crossingPointBuffer[i + 1].lng, pointALat, pointALng, pointBLat, pointBLng);
461-
double sumDistances = distA + distB;
462478

463479
int sideA = pointOnSideOfLine(crossingPointBuffer[i].lat, crossingPointBuffer[i].lng, pointALat, pointALng, pointBLat, pointBLng);
464480
int sideB = pointOnSideOfLine(crossingPointBuffer[i + 1].lat, crossingPointBuffer[i + 1].lng, pointALat, pointALng, pointBLat, pointBLng);
@@ -474,71 +490,64 @@ void DovesLapTimer::interpolateCrossingPoint(double& crossingLat, double& crossi
474490
debug(F(" sideB: "));
475491
debug(sideB);
476492
debug(F(" sum: "));
477-
debugln(sumDistances, 2);
478-
479-
// Update the best pair of points if the current pair has a smaller sum of distances and the points are on opposite sides of the line
480-
// todo: investigate if accuracy "on line" is good enough to avoid interpolation
481-
if (sumDistances < bestSumDistances && sideA != sideB) {
482-
debug(F("new best sum: "));
483-
debugln(sumDistances, 2);
484-
bestSumDistances = sumDistances;
485-
bestIndexA = i;
486-
bestIndexB = i + 1;
487-
// this seems safe, as soon as we cross the line, no two points can be closer...
493+
debugln(distA + distB, 2);
494+
495+
// First pair on opposite sides of the line = the crossing pair
496+
if (sideA != sideB) {
497+
crossingIndexA = i;
498+
crossingIndexB = i + 1;
499+
crossingSumDistances = distA + distB;
500+
debug(F("crossing pair found, sum: "));
501+
debugln(crossingSumDistances, 2);
488502
break;
489503
}
490504
}
491-
debug(F("bestSumDistances: "));
492-
debugln(bestSumDistances);
505+
debug(F("crossingSumDistances: "));
506+
debugln(crossingSumDistances);
493507

494-
// Make sure we found a valid pair of points
495-
if (bestSumDistances < crossingThresholdMeters && bestIndexA != -1 && bestIndexB != -1) {
508+
// Validate: the crossing pair must exist and be close enough to the line
509+
if (crossingSumDistances < crossingThresholdMeters && crossingIndexA != -1 && crossingIndexB != -1) {
496510
debugln(F("~~~ VALID CROSSING ~~~"));
497511

512+
// Compute the interpolation factor (t) from distances and speeds at the crossing pair
513+
double distA = pointLineSegmentDistance(crossingPointBuffer[crossingIndexA].lat, crossingPointBuffer[crossingIndexA].lng, pointALat, pointALng, pointBLat, pointBLng);
514+
double distB = pointLineSegmentDistance(crossingPointBuffer[crossingIndexB].lat, crossingPointBuffer[crossingIndexB].lng, pointALat, pointALng, pointBLat, pointBLng);
515+
double t = interpolateWeight(distA, distB, crossingPointBuffer[crossingIndexA].speedKmh, crossingPointBuffer[crossingIndexB].speedKmh);
516+
517+
// Time and odometer are always interpolated linearly (monotonic values that
518+
// should not overshoot), regardless of the interpolation mode for position.
519+
double deltaOdometer = crossingPointBuffer[crossingIndexB].odometer - crossingPointBuffer[crossingIndexA].odometer;
520+
double deltaTime = crossingPointBuffer[crossingIndexB].time - crossingPointBuffer[crossingIndexA].time;
521+
crossingOdometer = crossingPointBuffer[crossingIndexA].odometer + t * deltaOdometer;
522+
crossingTime = crossingPointBuffer[crossingIndexA].time + t * deltaTime;
523+
498524
if (forceLinear) {
499-
// Linear interpolation
500-
double distA = pointLineSegmentDistance(crossingPointBuffer[bestIndexA].lat, crossingPointBuffer[bestIndexA].lng, pointALat, pointALng, pointBLat, pointBLng);
501-
double distB = pointLineSegmentDistance(crossingPointBuffer[bestIndexB].lat, crossingPointBuffer[bestIndexB].lng, pointALat, pointALng, pointBLat, pointBLng);
502-
double t = interpolateWeight(distA, distB, crossingPointBuffer[bestIndexA].speedKmh, crossingPointBuffer[bestIndexB].speedKmh);
503-
504-
double deltaLat = crossingPointBuffer[bestIndexB].lat - crossingPointBuffer[bestIndexA].lat;
505-
double deltaLon = crossingPointBuffer[bestIndexB].lng - crossingPointBuffer[bestIndexA].lng;
506-
double deltaOdometer = crossingPointBuffer[bestIndexB].odometer - crossingPointBuffer[bestIndexA].odometer;
507-
double deltaTime = crossingPointBuffer[bestIndexB].time - crossingPointBuffer[bestIndexA].time;
508-
509-
crossingLat = crossingPointBuffer[bestIndexA].lat + t * deltaLat;
510-
crossingLng = crossingPointBuffer[bestIndexA].lng + t * deltaLon;
511-
crossingOdometer = crossingPointBuffer[bestIndexA].odometer + t * deltaOdometer;
512-
crossingTime = crossingPointBuffer[bestIndexA].time + t * deltaTime;
525+
// Linear interpolation for position
526+
double deltaLat = crossingPointBuffer[crossingIndexB].lat - crossingPointBuffer[crossingIndexA].lat;
527+
double deltaLon = crossingPointBuffer[crossingIndexB].lng - crossingPointBuffer[crossingIndexA].lng;
528+
529+
crossingLat = crossingPointBuffer[crossingIndexA].lat + t * deltaLat;
530+
crossingLng = crossingPointBuffer[crossingIndexA].lng + t * deltaLon;
513531
} else {
514-
// Catmull-Rom spline interpolation requires 4 control points:
515-
// index0 (before A), index1 (A), index2 (B), index3 (after B)
516-
// Check bounds: we need bestIndexA >= 1 and bestIndexB <= numPoints - 2
517-
bool canUseCatmullRom = (bestIndexA >= 1) && (bestIndexB <= numPoints - 2);
532+
// Catmull-Rom spline interpolation for position only.
533+
// Requires 4 control points: p0 (before A), p1 (A), p2 (B), p3 (after B)
534+
bool canUseCatmullRom = (crossingIndexA >= 1) && (crossingIndexB <= numPoints - 2);
518535

519536
if (!canUseCatmullRom) {
520-
// Not enough points for Catmull-Rom, fall back to linear interpolation
537+
// Not enough points for Catmull-Rom, fall back to linear
521538
debugln(F("Catmull-Rom: insufficient control points, using linear fallback"));
522539

523-
double distA = pointLineSegmentDistance(crossingPointBuffer[bestIndexA].lat, crossingPointBuffer[bestIndexA].lng, pointALat, pointALng, pointBLat, pointBLng);
524-
double distB = pointLineSegmentDistance(crossingPointBuffer[bestIndexB].lat, crossingPointBuffer[bestIndexB].lng, pointALat, pointALng, pointBLat, pointBLng);
525-
double t = interpolateWeight(distA, distB, crossingPointBuffer[bestIndexA].speedKmh, crossingPointBuffer[bestIndexB].speedKmh);
526-
527-
double deltaLat = crossingPointBuffer[bestIndexB].lat - crossingPointBuffer[bestIndexA].lat;
528-
double deltaLon = crossingPointBuffer[bestIndexB].lng - crossingPointBuffer[bestIndexA].lng;
529-
double deltaOdometer = crossingPointBuffer[bestIndexB].odometer - crossingPointBuffer[bestIndexA].odometer;
530-
double deltaTime = crossingPointBuffer[bestIndexB].time - crossingPointBuffer[bestIndexA].time;
540+
double deltaLat = crossingPointBuffer[crossingIndexB].lat - crossingPointBuffer[crossingIndexA].lat;
541+
double deltaLon = crossingPointBuffer[crossingIndexB].lng - crossingPointBuffer[crossingIndexA].lng;
531542

532-
crossingLat = crossingPointBuffer[bestIndexA].lat + t * deltaLat;
533-
crossingLng = crossingPointBuffer[bestIndexA].lng + t * deltaLon;
534-
crossingOdometer = crossingPointBuffer[bestIndexA].odometer + t * deltaOdometer;
535-
crossingTime = crossingPointBuffer[bestIndexA].time + t * deltaTime;
543+
crossingLat = crossingPointBuffer[crossingIndexA].lat + t * deltaLat;
544+
crossingLng = crossingPointBuffer[crossingIndexA].lng + t * deltaLon;
536545
} else {
537546
// We have 4 valid control points for Catmull-Rom
538-
int index0 = bestIndexA - 1;
539-
int index1 = bestIndexA;
540-
int index2 = bestIndexB;
541-
int index3 = bestIndexB + 1;
547+
int index0 = crossingIndexA - 1;
548+
int index1 = crossingIndexA;
549+
int index2 = crossingIndexB;
550+
int index3 = crossingIndexB + 1;
542551

543552
debugln(F("Catmull-Rom: using spline interpolation"));
544553
debug(F(" indices: "));
@@ -550,22 +559,14 @@ void DovesLapTimer::interpolateCrossingPoint(double& crossingLat, double& crossi
550559
debug(F(", "));
551560
debugln(index3);
552561

553-
// Compute the interpolation factor based on distance and speed
554-
double distA = pointLineSegmentDistance(crossingPointBuffer[index1].lat, crossingPointBuffer[index1].lng, pointALat, pointALng, pointBLat, pointBLng);
555-
double distB = pointLineSegmentDistance(crossingPointBuffer[index2].lat, crossingPointBuffer[index2].lng, pointALat, pointALng, pointBLat, pointBLng);
556-
double t = interpolateWeight(distA, distB, crossingPointBuffer[index1].speedKmh, crossingPointBuffer[index2].speedKmh);
557-
558-
// Perform Catmull-Rom spline interpolation for latitude, longitude, time, and odometer
562+
// Catmull-Rom for lat/lng only - spline smoothing helps with curved paths
559563
crossingLat = catmullRom(crossingPointBuffer[index0].lat, crossingPointBuffer[index1].lat, crossingPointBuffer[index2].lat, crossingPointBuffer[index3].lat, t);
560564
crossingLng = catmullRom(crossingPointBuffer[index0].lng, crossingPointBuffer[index1].lng, crossingPointBuffer[index2].lng, crossingPointBuffer[index3].lng, t);
561-
crossingTime = catmullRom(crossingPointBuffer[index0].time, crossingPointBuffer[index1].time, crossingPointBuffer[index2].time, crossingPointBuffer[index3].time, t);
562-
crossingOdometer = catmullRom(crossingPointBuffer[index0].odometer, crossingPointBuffer[index1].odometer, crossingPointBuffer[index2].odometer, crossingPointBuffer[index3].odometer, t);
563565
}
564566
}
565567
} else {
566568
debugln(F("~~~ INVALID CROSSING ~~~ INVALID CROSSING ~~~ INVALID CROSSING ~~~ INVALID CROSSING ~~~"));
567569
}
568-
return;
569570
}
570571

571572
/////////// sector timing helper methods
@@ -736,7 +737,17 @@ bool DovesLapTimer::checkSectorLine(double currentLat, double currentLng, double
736737
debugln(F(" crossing zone"));
737738
crossingFlag = true;
738739

739-
// Capture this first point - it's important context for Catmull-Rom interpolation
740+
// Insert previous GPS fix as pre-crossing point (gives Catmull-Rom its p0 control point)
741+
if (hasPrevFix) {
742+
crossingPointBuffer[crossingPointBufferIndex].lat = prevFixLat;
743+
crossingPointBuffer[crossingPointBufferIndex].lng = prevFixLng;
744+
crossingPointBuffer[crossingPointBufferIndex].time = prevFixTime;
745+
crossingPointBuffer[crossingPointBufferIndex].odometer = prevFixOdometer;
746+
crossingPointBuffer[crossingPointBufferIndex].speedKmh = prevFixSpeedKmh;
747+
crossingPointBufferIndex = (crossingPointBufferIndex + 1) % crossingPointBufferSize;
748+
}
749+
750+
// Capture current point
740751
crossingPointBuffer[crossingPointBufferIndex].lat = currentLat;
741752
crossingPointBuffer[crossingPointBufferIndex].lng = currentLng;
742753
crossingPointBuffer[crossingPointBufferIndex].time = millisecondsSinceMidnight;
@@ -789,6 +800,12 @@ void DovesLapTimer::reset() {
789800
positionPrevLng = 0;
790801
positionPrevAlt = 0;
791802
firstPositionReceived = false;
803+
prevFixLat = 0;
804+
prevFixLng = 0;
805+
prevFixTime = 0;
806+
prevFixOdometer = 0;
807+
prevFixSpeedKmh = 0;
808+
hasPrevFix = false;
792809

793810
// Reset the crossingPointBuffer index and full status
794811
crossing = false;

src/DovesLapTimer.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,14 @@ class DovesLapTimer {
496496
double positionPrevLng = 0.00;
497497
bool firstPositionReceived = false; // Explicit flag for first GPS fix detection
498498

499+
// Previous GPS fix snapshot (used as Catmull-Rom pre-crossing control point)
500+
double prevFixLat = 0;
501+
double prevFixLng = 0;
502+
unsigned long prevFixTime = 0;
503+
float prevFixOdometer = 0;
504+
float prevFixSpeedKmh = 0;
505+
bool hasPrevFix = false;
506+
499507
double startFinishPointALat;
500508
double startFinishPointALng;
501509
double startFinishPointBLat;

0 commit comments

Comments
 (0)