diff --git a/examples/companion_radio/Trail.h b/examples/companion_radio/Trail.h index db8dd5af..5d3add51 100644 --- a/examples/companion_radio/Trail.h +++ b/examples/companion_radio/Trail.h @@ -13,36 +13,46 @@ struct TrailPoint { int32_t lat_1e6; int32_t lon_1e6; uint32_t ts; // epoch seconds (RTC) + uint8_t flags; // bit 0 = SEG_START (don't draw a line from the previous point) }; +static const uint8_t TRAIL_FLAG_SEG_START = 0x01; + class TrailStore { public: static const int CAPACITY = 256; - // Interval options (seconds) and their settings labels. Index 0 default = 1 min. - static const uint8_t INTERVAL_COUNT = 4; + // Interval options (seconds) and their settings labels. Index 0 default = 30 s, + // a reasonable middle for walking/cycling without burning GPS frames. + static const uint8_t INTERVAL_COUNT = 6; static uint16_t intervalSecs(uint8_t idx) { - static const uint16_t OPTS[INTERVAL_COUNT] = { 60, 30, 300, 900 }; + static const uint16_t OPTS[INTERVAL_COUNT] = { 30, 10, 20, 60, 300, 900 }; return OPTS[idx < INTERVAL_COUNT ? idx : 0]; } static const char* intervalLabel(uint8_t idx) { - static const char* L[INTERVAL_COUNT] = { "1 min", "30 s", "5 min", "15 min" }; + static const char* L[INTERVAL_COUNT] = { "30 s", "10 s", "20 s", "1 min", "5 min", "15 min" }; return L[idx < INTERVAL_COUNT ? idx : 0]; } // Min-delta (metres) gates samples too close to the previous one. - static const uint8_t MIN_DELTA_COUNT = 3; + // Default 5 m: keeps walking jitter out, dense enough for a visible trail. + static const uint8_t MIN_DELTA_COUNT = 4; static uint16_t minDeltaMeters(uint8_t idx) { - static const uint16_t OPTS[MIN_DELTA_COUNT] = { 25, 5, 100 }; + static const uint16_t OPTS[MIN_DELTA_COUNT] = { 5, 10, 25, 100 }; return OPTS[idx < MIN_DELTA_COUNT ? idx : 0]; } static const char* minDeltaLabel(uint8_t idx) { - static const char* L[MIN_DELTA_COUNT] = { "25 m", "5 m", "100 m" }; + static const char* L[MIN_DELTA_COUNT] = { "5 m", "10 m", "25 m", "100 m" }; return L[idx < MIN_DELTA_COUNT ? idx : 0]; } bool isActive() const { return _active; } - void setActive(bool a) { _active = a; } + void setActive(bool a) { + // Re-arming after a stop marks the next addPoint as a segment start, so + // the renderer doesn't draw a straight line through the dead time. + if (_active && !a) _pending_seg_break = true; + _active = a; + } int count() const { return _count; } bool empty() const { return _count == 0; } @@ -52,14 +62,19 @@ public: const TrailPoint& first() const { return at(0); } const TrailPoint& last() const { return at(_count - 1); } - void clear() { _head = 0; _count = 0; } + void clear() { _head = 0; _count = 0; _pending_seg_break = false; } // Returns true if the point was stored (passed the min-delta gate). + // First point of the ring and the first point after a stop/start cycle + // get flagged TRAIL_FLAG_SEG_START so the map renderer breaks the line. bool addPoint(int32_t lat_1e6, int32_t lon_1e6, uint32_t ts, uint16_t min_delta_m) { - if (_count > 0) { + if (_count > 0 && !_pending_seg_break) { float d = haversineMeters(last().lat_1e6, last().lon_1e6, lat_1e6, lon_1e6); if (d < (float)min_delta_m) return false; } + uint8_t flags = (_count == 0 || _pending_seg_break) ? TRAIL_FLAG_SEG_START : 0; + _pending_seg_break = false; + int pos; if (_count < CAPACITY) { pos = (_head + _count) % CAPACITY; @@ -71,34 +86,38 @@ public: _buf[pos].lat_1e6 = lat_1e6; _buf[pos].lon_1e6 = lon_1e6; _buf[pos].ts = ts; + _buf[pos].flags = flags; return true; } - // Sum of pairwise Haversine deltas across the whole ring. + // Sum of pairwise Haversine deltas across the whole ring, skipping segment + // boundaries (a SEG_START point isn't reached from its predecessor). uint32_t totalDistanceMeters() const { float d = 0; for (int i = 1; i < _count; i++) { + if (at(i).flags & TRAIL_FLAG_SEG_START) continue; d += haversineMeters(at(i - 1).lat_1e6, at(i - 1).lon_1e6, at(i).lat_1e6, at(i).lon_1e6); } return (uint32_t)d; } - // Seconds between the first and last sample (0 if fewer than 2 points). - uint32_t elapsedSeconds() const { - if (_count < 2) return 0; - return last().ts - first().ts; + // Seconds between the first sample and either the most recent sample (when + // stopped) or the current RTC time passed in by the caller (when active). + // Using `now_ts` while active lets the UI advance the displayed time + // smoothly even when samples land on the floor of the min-delta gate. + uint32_t elapsedSeconds(uint32_t now_ts = 0) const { + if (_count == 0) return 0; + uint32_t start = first().ts; + uint32_t end = (_active && now_ts > start) ? now_ts : last().ts; + return (end > start) ? (end - start) : 0; } - // Speed in km/h from the last pair of samples; 0 if only one point. - uint16_t currentSpeedKmh() const { - if (_count < 2) return 0; - const auto& a = at(_count - 2); - const auto& b = last(); - float dt = (float)(b.ts - a.ts); - if (dt <= 0) return 0; - float d = haversineMeters(a.lat_1e6, a.lon_1e6, b.lat_1e6, b.lon_1e6); - return (uint16_t)(d / dt * 3.6f); + // Average speed in km/h = total distance / elapsed time. + uint16_t avgSpeedKmh(uint32_t now_ts = 0) const { + uint32_t es = elapsedSeconds(now_ts); + if (es == 0) return 0; + return (uint16_t)((float)totalDistanceMeters() / (float)es * 3.6f); } // Compute bounding box across all points. Returns false if empty. @@ -137,4 +156,5 @@ private: int _head = 0; int _count = 0; bool _active = false; + bool _pending_seg_break = false; // next addPoint flags itself SEG_START }; diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 7c93cf5a..942f37b2 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -81,13 +81,17 @@ private: break; } case 3: { - uint32_t es = _store->elapsedSeconds(); - snprintf(buf, n, "Time: %lu:%02lu", - (unsigned long)(es / 3600), (unsigned long)((es % 3600) / 60)); + uint32_t es = _store->elapsedSeconds((uint32_t)rtc_clock.getCurrentTime()); + // Below 1 h show m:ss so the seconds counter updates visibly each refresh. + if (es < 3600) snprintf(buf, n, "Time: %lu:%02lu", + (unsigned long)(es / 60), (unsigned long)(es % 60)); + else snprintf(buf, n, "Time: %lu:%02lu", + (unsigned long)(es / 3600), (unsigned long)((es % 3600) / 60)); break; } case 4: - snprintf(buf, n, "Speed: %u km/h", (unsigned)_store->currentSpeedKmh()); + snprintf(buf, n, "Avg speed: %u km/h", + (unsigned)_store->avgSpeedKmh((uint32_t)rtc_clock.getCurrentTime())); break; default: buf[0] = '\0'; @@ -181,12 +185,20 @@ private: py = off_y + (int)dy; }; + // Draw connected segments. A point flagged SEG_START starts a new segment; + // its predecessor is not joined to it. Single-point segments still get a + // visible pixel. int x0, y0; project(_store->at(0), x0, y0); + display.fillRect(x0, y0, 1, 1); for (int i = 1; i < _store->count(); i++) { int x1, y1; project(_store->at(i), x1, y1); - drawLine(display, x0, y0, x1, y1); + if (_store->at(i).flags & TRAIL_FLAG_SEG_START) { + display.fillRect(x1, y1, 1, 1); // mark the segment's first point + } else { + drawLine(display, x0, y0, x1, y1); + } x0 = x1; y0 = y1; }