DovesLapTimer 4.3.0
GPS-based lap timing Arduino library — go-karts to race cars
Loading...
Searching...
No Matches
DovesLapTimer.h
Go to the documentation of this file.
1
9#ifndef _DOVES_LAP_TIMER_H
10#define _DOVES_LAP_TIMER_H
11
12#include "ArxTypeTraits.h"
13#include "GeoMath.h"
14#include "CrossingEngine.h"
15using TRITYPE = double;
16
17// On classic AVR (Mega, Uno) `double` is a 32-bit float (~7 significant
18// digits). At real-world latitudes/longitudes that quantizes position to
19// roughly 0.2-0.75 m and pushes per-fix haversine half-angle differences
20// below float precision: lap *counting* still works (the 7 m crossing
21// threshold absorbs it) but odometer increments and crossing interpolation
22// carry large relative error. The full advertised precision requires a
23// target with true 64-bit double (nRF52840, ESP32, SAMD51, RP2040, ...).
24// See README "Hardware" notes.
25#if defined(__SIZEOF_DOUBLE__) && (__SIZEOF_DOUBLE__ < 8)
26#warning "DovesLapTimer: 'double' is only 32 bits on this target (classic AVR). Lap counting works but GPS math runs degraded - distances and interpolated crossing times will be noticeably less accurate. Use a 64-bit-double MCU (e.g. XIAO nRF52840) for full precision."
27#endif
28
29// CROSSING_LINE_SIDE_* now live in CrossingEngine.h (included above).
30
31// Course detection constants
32#define COURSE_DETECT_SPEED_THRESHOLD_MPH 20
33#define COURSE_DETECT_WAYPOINT_PROXIMITY_METERS 10.0
34#define COURSE_DETECT_MIN_DISTANCE_METERS 200.0
35#define COURSE_DETECT_DISTANCE_TOLERANCE_PCT 0.25
36#define COURSE_DETECT_MAX_REJECTIONS 3
37// Completed proximity passes (full laps back at the waypoint) that matched
38// no configured course length before CourseManager falls back to Lap
39// Anything. Without this, a wrong/missing course config left detection in
40// WAYPOINT_SET forever while the WaypointLapTimer's laps were never surfaced.
41#define COURSE_DETECT_MAX_NO_MATCH_PASSES 3
42// Distance failsafe: if detection is still incomplete after driving
43// factor x (longest configured course length), or the floor below for very
44// short courses, fall back to Lap Anything. Catches the "never returns to
45// within 10m of the waypoint" hang that pass counting can't see.
46#define COURSE_DETECT_FALLBACK_DISTANCE_FACTOR 4.0
47#define COURSE_DETECT_FALLBACK_MIN_METERS 2000.0
48#define METERS_TO_FEET GEOMATH_METERS_TO_FEET
49
50// Waypoint lap timer constants
51#define WAYPOINT_LAP_MIN_DISTANCE_METERS 100.0
52#define WAYPOINT_LAP_PROXIMITY_METERS 30.0
53
54// Direction detection
55#define DIR_UNKNOWN 0
56#define DIR_FORWARD 1
57#define DIR_REVERSE 2
58
59// Course detection states
60#define DETECT_STATE_IDLE 0
61#define DETECT_STATE_WAITING_FOR_SPEED 1
62#define DETECT_STATE_WAYPOINT_SET 2
63#define DETECT_STATE_CANDIDATES_READY 3
64#define DETECT_STATE_DETECTED 4
65
66// Maximum courses supported
67#define MAX_COURSES 8
68
69// DOVES_MILLIS_PER_DAY, timeSinceMidnightDelta(), CROSSING_MAX_FIX_GAP_MS,
70// CROSSING_PAIR_SPACING_FACTOR, LineDetectResult, and
71// crossingPointBufferEntry now live in CrossingEngine.h (included above).
72
73// GPS input validation: a single-fix jump beyond this is treated as a glitch
74// and dropped; after GPS_JUMP_REACCEPT_COUNT consecutive far fixes the new
75// position is accepted as real (signal re-acquisition) and the position is
76// re-seeded without crediting the gap to the odometer.
77#define GPS_MAX_PLAUSIBLE_JUMP_METERS 500.0
78#define GPS_JUMP_REACCEPT_COUNT 3
79
81 int direction; // DIR_UNKNOWN, DIR_FORWARD, DIR_REVERSE
83 // Per-lap window: timestamps of physical S2/S3 crossings since the last
84 // start/finish. Direction is decided only when BOTH are present, by their
85 // temporal order — independent of the lap calculation's sector output.
86 unsigned long lapS2CrossingTime;
87 unsigned long lapS3CrossingTime;
88
92 void reset() {
94 raceSeen = false;
97 }
98 void onLineCrossing(int sectorNumber, unsigned long crossingTime);
99 bool isReverse() const { return direction == DIR_REVERSE; }
100 bool isResolved() const { return direction != DIR_UNKNOWN; }
101};
102
104public:
105 DovesLapTimer(double crossingThresholdMeters = 7, Stream *debugSerial = NULL);
106
118 int loop(double currentLat, double currentLng, float currentAltitudeMeters, float currentSpeedKnots);
119
121
133 bool isObtuseTriangle(double lat1, double lon1, double lat2, double lon2, double lat3, double lon3);
134
149 bool insideLineThreshold(double driverLat, double driverLon, double crossingPointALat, double crossingPointALon, double crossingPointBLat, double crossingPointBLon);
150
165 int pointOnSideOfLine(double driverLat, double driverLng, double pointALat, double pointALng, double pointBLat, double pointBLng);
184 double pointLineSegmentDistance(double pointX, double pointY, double startX, double startY, double endX, double endY);
200 double haversine(double lat1, double lon1, double lat2, double lon2);
216 double haversine3D(double prevLat, double prevLng, double prevAlt, double currentLat, double currentLng, double currentAlt);
217
219
223 void reset();
232 void setStartFinishLine(double pointALat, double pointALng, double pointBLat, double pointBLng);
241 void setSector2Line(double pointALat, double pointALng, double pointBLat, double pointBLng);
250 void setSector3Line(double pointALat, double pointALng, double pointBLat, double pointBLng);
256 void updateCurrentTime(unsigned long currentTimeMilliseconds);
278 bool getRaceStarted() const;
284 bool getCrossing() const;
290 unsigned long getCurrentLapStartTime() const;
296 unsigned long getCurrentLapTime() const;
302 unsigned long getLastLapTime() const;
308 unsigned long getBestLapTime() const;
314 float getCurrentLapOdometerStart() const;
320 float getCurrentLapDistance() const;
326 float getLastLapDistance() const;
332 float getBestLapDistance() const;
338 float getTotalDistanceTraveled() const;
344 int getBestLapNumber() const;
350 int getLaps() const;
362 float getPaceDifference() const;
368 float getCurrentSpeedKmh() const;
374 float getCurrentSpeedMph() const;
375
376
378 // Sector timing methods
379
385 unsigned long getBestSector1Time() const;
391 unsigned long getBestSector2Time() const;
397 unsigned long getBestSector3Time() const;
403 unsigned long getCurrentLapSector1Time() const;
409 unsigned long getCurrentLapSector2Time() const;
415 unsigned long getCurrentLapSector3Time() const;
421 unsigned long getOptimalLapTime() const;
427 int getBestSector1LapNumber() const;
433 int getBestSector2LapNumber() const;
439 int getBestSector3LapNumber() const;
445 int getCurrentSector() const;
451 bool areSectorLinesConfigured() const;
464 unsigned int getRejectedCrossingCount() const;
473 bool isStartFinishLineConfigured() const;
474
476 // Direction detection methods
477
483 int getDirection() const;
489 bool isDirectionResolved() const;
490
491private:
492#ifdef DOVES_DISABLE_DEBUG
493 // Compile-time debug kill switch — see CrossingEngine.h for the rationale.
494 template<typename... Args> void debug_print(Args&&...) {}
495 template<typename... Args> void debug_println(Args&&...) {}
496#else
497 template<typename... Args>
498 void debug_print(Args&&... args) {
499 if(_serial) {
500 _serial->print(std::forward<Args>(args)...);
501 }
502 }
503 template<typename... Args>
504 void debug_println(Args&&... args) {
505 if(_serial) {
506 _serial->println(std::forward<Args>(args)...);
507 }
508 }
509#endif
510
524 bool checkStartFinish(double currentLat, double currentLng);
538 bool checkSectorLine(double currentLat, double currentLng, double pointALat, double pointALng, double pointBLat, double pointBLng, bool& crossingFlag, int sectorNumber);
553 LineDetectResult _detectLineCrossing(
554 double currentLat, double currentLng,
555 double pointALat, double pointALng,
556 double pointBLat, double pointBLng,
557 bool& crossingFlag,
558 int lineLabel,
559 double& outLat, double& outLng,
560 unsigned long& outTime,
561 double& outOdometer);
568 void handleLineCrossing(unsigned long crossingTime, int sectorNumber);
572 void updateBestSectors();
573
574 Stream *_serial;
575 DirectionDetector _directionDetector;
576 // The shared crossing pipeline (buffer + interpolation). ONE engine for
577 // all three lines — the historical shared-buffer design, kept so the
578 // by-value CourseManager timer array doesn't triple in size. loop()
579 // enforces the resulting only-one-line-crossing-at-a-time exclusion.
580 CrossingEngine _crossingEngine;
581
582 unsigned long millisecondsSinceMidnight = 0;
583 // Timing variables
584 double crossingThresholdMeters;
585 bool raceStarted = false;
586 bool crossing = false;
587 unsigned long currentLapStartTime = 0;
588 unsigned long lastLapTime = 0;
589 unsigned long bestLapTime = 0;
590 float currentLapOdometerStart = 0.0;
591 float lastLapDistance = 0.0;
592 float bestLapDistance = 0.0;
593 float currentSpeedkmh = 0.0;
594 int bestLapNumber = 0;
595 int laps = 0;
596
597 // Sector timing state
598 int currentSector = 0; // 0=not started, 1/2/3=in sector
599 unsigned long currentSectorStartTime = 0;
600 bool crossingSector2 = false;
601 bool crossingSector3 = false;
602
603 // Current lap sector times (reset each lap)
604 unsigned long currentLapSector1Time = 0;
605 unsigned long currentLapSector2Time = 0;
606 unsigned long currentLapSector3Time = 0;
607
608 // Best sector times (persistent across laps)
609 unsigned long bestSector1Time = 0;
610 unsigned long bestSector2Time = 0;
611 unsigned long bestSector3Time = 0;
612
613 // Lap numbers that achieved best sectors
614 int bestSector1LapNumber = 0;
615 int bestSector2LapNumber = 0;
616 int bestSector3LapNumber = 0;
617
618 float totalDistanceTraveled = 0;
619 float positionPrevAlt = 0.00;
620 double positionPrevLat = 0.00;
621 double positionPrevLng = 0.00;
622 bool firstPositionReceived = false; // Explicit flag for first GPS fix detection
623
624 // Previous GPS fix snapshot (used as Catmull-Rom pre-crossing control point)
625 crossingPointBufferEntry prevFix = {0, 0, 0, 0, 0};
626 bool hasPrevFix = false;
627
628 double startFinishPointALat = 0.0;
629 double startFinishPointALng = 0.0;
630 double startFinishPointBLat = 0.0;
631 double startFinishPointBLng = 0.0;
632
633 // Sector 2 line coordinates
634 double sector2PointALat = 0.0;
635 double sector2PointALng = 0.0;
636 double sector2PointBLat = 0.0;
637 double sector2PointBLng = 0.0;
638
639 // Sector 3 line coordinates
640 double sector3PointALat = 0.0;
641 double sector3PointALng = 0.0;
642 double sector3PointBLat = 0.0;
643 double sector3PointBLng = 0.0;
644
645 // Line configuration flags — loop() skips detection for unconfigured lines
646 bool startFinishLineConfigured = false;
647 bool sector2LineConfigured = false;
648 bool sector3LineConfigured = false;
649
650 // Consecutive fixes rejected for jumping > GPS_MAX_PLAUSIBLE_JUMP_METERS
651 int consecutiveJumpCount = 0;
652};
653
654#endif
LineDetectResult
Definition CrossingEngine.h:67
double TRITYPE
Definition DovesLapTimer.h:15
#define DIR_REVERSE
Definition DovesLapTimer.h:57
#define DIR_UNKNOWN
Definition DovesLapTimer.h:55
const double crossingPointBLat
Definition basic_oled_example.ino:21
const double crossingPointALat
Definition basic_oled_example.ino:19
void loop()
Definition basic_oled_example.ino:116
Definition CrossingEngine.h:81
Definition DovesLapTimer.h:103
unsigned long getBestSector2Time() const
Gets the best sector 2 time.
Definition DovesLapTimer.cpp:614
unsigned long getLastLapTime() const
Gets the last lap time.
Definition DovesLapTimer.cpp:557
unsigned long getCurrentLapStartTime() const
Gets the current lap start time.
Definition DovesLapTimer.cpp:549
unsigned int getRejectedCrossingCount() const
Number of crossing-zone exits whose interpolation was rejected.
Definition DovesLapTimer.cpp:654
double pointLineSegmentDistance(double pointX, double pointY, double startX, double startY, double endX, double endY)
Calculate the shortest distance between a point and a line segment.
Definition DovesLapTimer.cpp:254
void updateCurrentTime(unsigned long currentTimeMilliseconds)
Updates the current GPS time since midnight.
Definition DovesLapTimer.cpp:534
unsigned long getOptimalLapTime() const
Gets the optimal lap time calculated from best sector times.
Definition DovesLapTimer.cpp:629
float getTotalDistanceTraveled() const
Gets the total distance traveled.
Definition DovesLapTimer.cpp:575
void forceLinearInterpolation()
forces linear interpolation when checking crossing line
Definition DovesLapTimer.cpp:537
bool isStartFinishLineConfigured() const
Checks if the start/finish line is configured.
Definition DovesLapTimer.cpp:651
float getCurrentSpeedMph() const
Gets the current speed in miles per hour.
Definition DovesLapTimer.cpp:605
float getLastLapDistance() const
Gets the last lap distance.
Definition DovesLapTimer.cpp:569
int getBestSector1LapNumber() const
Gets the lap number that achieved the best sector 1 time.
Definition DovesLapTimer.cpp:636
unsigned long getCurrentLapTime() const
Gets the current lap time.
Definition DovesLapTimer.cpp:552
float getPaceDifference() const
Calculates the pace difference between the current lap and the best lap.
Definition DovesLapTimer.cpp:584
bool getRaceStarted() const
Gets the race started status (passed the line one time).
Definition DovesLapTimer.cpp:543
int getDirection() const
Gets the detected driving direction.
Definition DovesLapTimer.cpp:660
unsigned long getCurrentLapSector2Time() const
Gets the current lap sector 2 time.
Definition DovesLapTimer.cpp:623
void reset()
Reset all parameters back to 0.
Definition DovesLapTimer.cpp:442
void setSector2Line(double pointALat, double pointALng, double pointBLat, double pointBLng)
Sets the sector 2 line using two points (A and B).
Definition DovesLapTimer.cpp:514
int getBestSector3LapNumber() const
Gets the lap number that achieved the best sector 3 time.
Definition DovesLapTimer.cpp:642
bool isObtuseTriangle(double lat1, double lon1, double lat2, double lon2, double lat3, double lon3)
Checks if the triangle formed by the given coordinates is an obtuse triangle.
Definition DovesLapTimer.cpp:220
bool insideLineThreshold(double driverLat, double driverLon, double crossingPointALat, double crossingPointALon, double crossingPointBLat, double crossingPointBLon)
Check if a driver is within a threshold distance of the line formed by the two crossing points.
Definition DovesLapTimer.cpp:216
bool getCrossing() const
Gets the crossing status.
Definition DovesLapTimer.cpp:546
float getCurrentSpeedKmh() const
Gets the current speed in kilometers per hour.
Definition DovesLapTimer.cpp:602
int pointOnSideOfLine(double driverLat, double driverLng, double pointALat, double pointALng, double pointBLat, double pointBLng)
Determines which side of a line a driver is on.
Definition DovesLapTimer.cpp:250
unsigned long getBestSector1Time() const
Gets the best sector 1 time.
Definition DovesLapTimer.cpp:611
void setSector3Line(double pointALat, double pointALng, double pointBLat, double pointBLng)
Sets the sector 3 line using two points (A and B).
Definition DovesLapTimer.cpp:524
int getBestLapNumber() const
Gets the best lap number.
Definition DovesLapTimer.cpp:578
unsigned long getBestSector3Time() const
Gets the best sector 3 time.
Definition DovesLapTimer.cpp:617
float getCurrentLapOdometerStart() const
Gets the current lap odometer start.
Definition DovesLapTimer.cpp:563
unsigned long getCurrentLapSector1Time() const
Gets the current lap sector 1 time.
Definition DovesLapTimer.cpp:620
void setStartFinishLine(double pointALat, double pointALng, double pointBLat, double pointBLng)
Sets the start/finish line using two points (A and B).
Definition DovesLapTimer.cpp:504
bool isDirectionResolved() const
Checks if the driving direction has been resolved.
Definition DovesLapTimer.cpp:663
unsigned long getBestLapTime() const
Gets the best lap time.
Definition DovesLapTimer.cpp:560
void forceCatmullRomInterpolation()
Forces Catmull-Rom spline interpolation when checking crossing line.
Definition DovesLapTimer.cpp:540
float getBestLapDistance() const
Gets the best lap distance.
Definition DovesLapTimer.cpp:572
float getCurrentLapDistance() const
Gets the current lap distance.
Definition DovesLapTimer.cpp:566
double haversine3D(double prevLat, double prevLng, double prevAlt, double currentLat, double currentLng, double currentAlt)
Calculates the distance between two GPS points, including altitude difference.
Definition DovesLapTimer.cpp:262
unsigned long getCurrentLapSector3Time() const
Gets the current lap sector 3 time.
Definition DovesLapTimer.cpp:626
bool areSectorLinesConfigured() const
Checks if sector lines are configured.
Definition DovesLapTimer.cpp:648
int getLaps() const
Gets the total number of laps completed.
Definition DovesLapTimer.cpp:581
int getBestSector2LapNumber() const
Gets the lap number that achieved the best sector 2 time.
Definition DovesLapTimer.cpp:639
double haversine(double lat1, double lon1, double lat2, double lon2)
Calculates the great-circle distance between two points on the Earth's surface using the Haversine fo...
Definition DovesLapTimer.cpp:258
int getCurrentSector() const
Gets the current sector the driver is in.
Definition DovesLapTimer.cpp:645
Definition DovesLapTimer.h:80
DirectionDetector()
Definition DovesLapTimer.h:89
void reset()
Definition DovesLapTimer.h:92
unsigned long lapS2CrossingTime
Definition DovesLapTimer.h:86
void onLineCrossing(int sectorNumber, unsigned long crossingTime)
Definition DovesLapTimer.cpp:268
bool isReverse() const
Definition DovesLapTimer.h:99
bool raceSeen
Definition DovesLapTimer.h:82
unsigned long lapS3CrossingTime
Definition DovesLapTimer.h:87
int direction
Definition DovesLapTimer.h:81
bool isResolved() const
Definition DovesLapTimer.h:100
Definition CrossingEngine.h:73