diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index f1d32150..001ad55d 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -317,6 +317,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no rd(&_prefs.trail_units_idx, sizeof(_prefs.trail_units_idx)); rd(&_prefs.ch_fav_bitmask, sizeof(_prefs.ch_fav_bitmask)); rd(&_prefs.ch_fav_only, sizeof(_prefs.ch_fav_only)); + rd(&_prefs.units_imperial, sizeof(_prefs.units_imperial)); + rd(&_prefs.trail_show_pace, sizeof(_prefs.trail_show_pace)); + // Both are booleans: an older file leaves stray bytes here (the old tail + // sentinel gets partly consumed), so clamp anything other than 0/1 to 0. + if (_prefs.units_imperial > 1) _prefs.units_imperial = 0; + if (_prefs.trail_show_pace > 1) _prefs.trail_show_pace = 0; // Schema sentinel: bumped on layout changes. Mismatch means an older file // (or a different schema); rd() already zero-inits any fields not present, @@ -340,6 +346,9 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no if (sentinel == 0xC0DE0003) { _prefs.trail_units_idx = 0; } + // 0xC0DE0005 → 0xC0DE0006: units_imperial + trail_show_pace replaced the + // combined trail_units_idx. The new bytes overlap the old tail sentinel and + // are already clamped to 0 above, so upgraders default to metric + speed. } file.close(); @@ -428,6 +437,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.trail_units_idx, sizeof(_prefs.trail_units_idx)); file.write((uint8_t *)&_prefs.ch_fav_bitmask, sizeof(_prefs.ch_fav_bitmask)); file.write((uint8_t *)&_prefs.ch_fav_only, sizeof(_prefs.ch_fav_only)); + file.write((uint8_t *)&_prefs.units_imperial, sizeof(_prefs.units_imperial)); + file.write((uint8_t *)&_prefs.trail_show_pace, sizeof(_prefs.trail_show_pace)); // Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL; diff --git a/examples/companion_radio/GeoUtils.h b/examples/companion_radio/GeoUtils.h index 9cf95803..4e525ef5 100644 --- a/examples/companion_radio/GeoUtils.h +++ b/examples/companion_radio/GeoUtils.h @@ -42,11 +42,20 @@ static inline const char* bearingCardinal(int deg) { return dirs[((deg + 22) % 360) / 45]; } -// Human distance string: "850m" / "2.3km" / "140km". -static inline void fmtDist(char* buf, int n, float km) { - if (km < 1.0f) snprintf(buf, n, "%dm", (int)(km * 1000 + 0.5f)); - else if (km < 100.0f) snprintf(buf, n, "%.1fkm", km); - else snprintf(buf, n, "%dkm", (int)(km + 0.5f)); +// Human distance string. Metric: "850m" / "2.3km" / "140km". Imperial: +// "850ft" / "2.3mi" / "140mi" (feet below ~1000 ft, then miles). +static inline void fmtDist(char* buf, int n, float km, bool imperial) { + if (imperial) { + float ft = km * 3280.84f; + if (ft < 1000.0f) { snprintf(buf, n, "%dft", (int)(ft + 0.5f)); return; } + float mi = km * 0.621371f; + if (mi < 100.0f) snprintf(buf, n, "%.1fmi", mi); + else snprintf(buf, n, "%dmi", (int)(mi + 0.5f)); + } else { + if (km < 1.0f) snprintf(buf, n, "%dm", (int)(km * 1000 + 0.5f)); + else if (km < 100.0f) snprintf(buf, n, "%.1fkm", km); + else snprintf(buf, n, "%dkm", (int)(km + 0.5f)); + } } } // namespace geo diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 1538c901..3ddf9eb9 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -103,16 +103,23 @@ struct NodePrefs { // persisted to file // not a persisted preference. uint8_t trail_interval_idx; // reserved — sampling cadence is now fixed at TrailStore::SAMPLING_SECS uint8_t trail_min_delta_idx; // indexes TrailStore::minDeltaMeters (5/10/25/100 m) - uint8_t trail_units_idx; // indexes TrailStore::unitLabel (km/h, mph, min/km, min/mi) + uint8_t trail_units_idx; // legacy: old combined speed/pace+unit index (km/h, mph, min/km, min/mi) uint64_t ch_fav_bitmask; // bit i = channel i is marked as favourite uint8_t ch_fav_only; // 0=show all channels (default), 1=show favourites only + // Global measurement system for every distance/speed shown in the UI + // (Nearby, Trail, navigate-to-point). 0=metric (default), 1=imperial. + uint8_t units_imperial; + // Trail Summary readout: 0=speed (km/h or mph), 1=pace (min/km or min/mi). + // The km-vs-mi choice now comes from units_imperial, so this is just the mode. + uint8_t trail_show_pace; + // Tail sentinel written at the end of /new_prefs. Bump the low byte when // adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so // older saves are detected on load and skipped (zero-init defaults kept). // High 24 bits identify the file format; low byte is the schema revision. - static const uint32_t SCHEMA_SENTINEL = 0xC0DE0005; + static const uint32_t SCHEMA_SENTINEL = 0xC0DE0006; // Bit-index for each home page. Used by page_order (entries store bit+1) and // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage diff --git a/examples/companion_radio/ui-new/NavView.h b/examples/companion_radio/ui-new/NavView.h index 967c8c24..b3e9549e 100644 --- a/examples/companion_radio/ui-new/NavView.h +++ b/examples/companion_radio/ui-new/NavView.h @@ -18,7 +18,8 @@ inline void draw(DisplayDriver& d, bool own_valid, int32_t own_lat, int32_t own_lon, int32_t tgt_lat, int32_t tgt_lon, const char* label, - bool cog_valid, int cog_deg) { + bool cog_valid, int cog_deg, + bool imperial) { const int cx = d.width() / 2; const int hdr = d.headerH(); @@ -40,7 +41,7 @@ inline void draw(DisplayDriver& d, int to_deg = geo::bearingDeg(own_lat, own_lon, tgt_lat, tgt_lon); float dist_km = geo::haversineKm(own_lat, own_lon, tgt_lat, tgt_lon); char dist[12]; - geo::fmtDist(dist, sizeof(dist), dist_km); + geo::fmtDist(dist, sizeof(dist), dist_km, imperial); const int step = d.lineStep(); int y = hdr + 2; diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index 714fc01f..f94fb21e 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -85,6 +85,12 @@ class NearbyScreen : public UIScreen { // Geographic helpers live in GeoUtils.h (geo::) — shared with Waypoints / // trail course-over-ground. Call sites use geo:: directly. + // Global metric/imperial preference for distance display. + bool useImperial() const { + NodePrefs* p = _task ? _task->getNodePrefs() : nullptr; + return p && p->units_imperial; + } + static void fmtAge(char* buf, int n, uint32_t lastmod) { uint32_t now = rtc_clock.getCurrentTime(); if (now < lastmod || lastmod == 0) { snprintf(buf, n, "unknown"); return; } @@ -366,7 +372,7 @@ class NearbyScreen : public UIScreen { if (e.dist_km >= 0.0f) { char dist[12]; - geo::fmtDist(dist, sizeof(dist), e.dist_km); + geo::fmtDist(dist, sizeof(dist), e.dist_km, useImperial()); int az = geo::bearingDeg(_own_lat, _own_lon, e.lat_e6, e.lon_e6); snprintf(buf, sizeof(buf), "Dist: %s %s", dist, geo::bearingCardinal(az)); } else { @@ -582,7 +588,7 @@ public: const Entry& e = _entries[_sel]; int cog; bool cogv = _task->currentCourse(cog); navview::draw(display, _own_gps, _own_lat, _own_lon, - e.lat_e6, e.lon_e6, e.name, cogv, cog); + e.lat_e6, e.lon_e6, e.name, cogv, cog, useImperial()); return 1000; } @@ -622,7 +628,7 @@ public: display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT); char dist[10]; - if (e.dist_km >= 0.0f) geo::fmtDist(dist, sizeof(dist), e.dist_km); + if (e.dist_km >= 0.0f) geo::fmtDist(dist, sizeof(dist), e.dist_km, useImperial()); else strncpy(dist, "?GPS", sizeof(dist)); display.setCursor(dist_col, y); display.print(dist); diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index dd609e98..6246bf22 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -56,6 +56,7 @@ class SettingsScreen : public UIScreen { SECTION_SYSTEM, TIMEZONE, LOW_BAT, + UNITS, // Contacts section SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER, // Messages section @@ -486,6 +487,10 @@ class SettingsScreen : public UIScreen { display.print("LowBat"); display.setCursor(display.valCol(), y); display.print(LOW_BAT_LABELS[lowBatIndex()]); + } else if (item == UNITS) { + display.print("Units"); + display.setCursor(display.valCol(), y); + display.print((p && p->units_imperial) ? "Imperial" : "Metric"); } else if (item == BATT_DISPLAY) { display.print("BattDisp"); display.setCursor(display.valCol(), y); @@ -732,6 +737,11 @@ public: if (left) idx = (idx + LOW_BAT_COUNT - 1) % LOW_BAT_COUNT; if (left || right) { p->low_batt_mv = LOW_BAT_OPTS[idx]; _dirty = true; return true; } } + if (_selected == UNITS && p && (left || right || enter)) { + p->units_imperial ^= 1; + _dirty = true; + return true; + } if (_selected == BATT_DISPLAY && p) { int idx = p->batt_display_mode < BATT_DISPLAY_COUNT ? p->batt_display_mode : 0; if (right) idx = (idx + 1) % BATT_DISPLAY_COUNT; diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 58c35fdd..c7cddf48 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -289,8 +289,10 @@ private: NodePrefs* p = _task->getNodePrefs(); snprintf(_act_min_dist_label, sizeof(_act_min_dist_label), "Min dist: %s", TrailStore::minDeltaLabel(p ? p->trail_min_delta_idx : 0)); + // Unit system (km/mi) is the global Settings choice; here we only toggle + // what the Summary readout shows — speed or pace. snprintf(_act_units_label, sizeof(_act_units_label), - "Units: %s", TrailStore::unitLabel(p ? p->trail_units_idx : 0)); + "Readout: %s", (p && p->trail_show_pace) ? "Pace" : "Speed"); snprintf(_act_grid_label, sizeof(_act_grid_label), "Grid: %s", _map_grid ? "ON" : "OFF"); snprintf(_act_toggle_label, sizeof(_act_toggle_label), @@ -353,6 +355,12 @@ private: bool ownPos(int32_t& lat, int32_t& lon) const { return _task->currentLocation(lat, lon); } + // Global metric/imperial preference for distance display. + bool useImperial() const { + NodePrefs* p = _task->getNodePrefs(); + return p && p->units_imperial; + } + void handleMarkHere() { int32_t lat, lon; if (!ownPos(lat, lon)) { _task->showAlert("No GPS fix", 1000); return; } @@ -414,7 +422,7 @@ private: char dist[12] = ""; int bw = 0; if (have) { - geo::fmtDist(dist, sizeof(dist), geo::haversineKm(mylat, mylon, tlat, tlon)); + geo::fmtDist(dist, sizeof(dist), geo::haversineKm(mylat, mylon, tlat, tlon), useImperial()); bw = display.getTextWidth(dist) + 2; } char nm[24]; @@ -430,7 +438,7 @@ private: if (!rowTarget(_wp_sel, tlat, tlon, label)) { _wp_mode = WP_LIST; return; } int32_t mylat, mylon; bool have = ownPos(mylat, mylon); int cog; bool cogv = _task->currentCourse(cog); - navview::draw(display, have, mylat, mylon, tlat, tlon, label, cogv, cog); + navview::draw(display, have, mylat, mylon, tlat, tlon, label, cogv, cog, useImperial()); } // Resolve a combined-list row to a nav target. Row 0 is the trail start when @@ -467,40 +475,32 @@ private: : (idx + TrailStore::MIN_DELTA_COUNT - 1) % TrailStore::MIN_DELTA_COUNT; p->trail_min_delta_idx = idx; } - void cycleUnits(NodePrefs* p, int dir) { - uint8_t idx = p->trail_units_idx; - if (idx >= TrailStore::UNITS_COUNT) idx = 0; - idx = (dir > 0) ? (idx + 1) % TrailStore::UNITS_COUNT - : (idx + TrailStore::UNITS_COUNT - 1) % TrailStore::UNITS_COUNT; - p->trail_units_idx = idx; - } + // The trail "Readout" row toggles speed vs pace; the metric/imperial unit + // comes from the global Settings preference, so this is a plain on/off flip. + void cycleUnits(NodePrefs* p, int /*dir*/) { p->trail_show_pace ^= 1; } - // Render the average speed (or pace) in the user-selected units. + // Render the average speed (or pace) in the globally-selected unit system. void formatAvgPaceOrSpeed(char* buf, size_t n, NodePrefs* p) const { - uint8_t u = p ? p->trail_units_idx : 0; - if (u >= TrailStore::UNITS_COUNT) u = 0; - uint16_t kmh = _store->avgSpeedKmh(); - if (u == TrailStore::UNITS_KMH) { - snprintf(buf, n, "Avg: %u km/h", (unsigned)kmh); - } else if (u == TrailStore::UNITS_MPH) { - unsigned mph = (unsigned)((float)kmh * 0.621371f + 0.5f); - snprintf(buf, n, "Avg: %u mph", mph); - } else { - uint32_t d_m = _store->totalDistanceMeters(); - uint32_t es_s = _store->elapsedSeconds(); - const char* unit = (u == TrailStore::UNITS_PACE_KM) ? "/km" : "/mi"; - if (d_m == 0 || es_s == 0) { - snprintf(buf, n, "Pace: --:-- %s", unit); - } else { - float dist_unit = (u == TrailStore::UNITS_PACE_KM) ? (d_m / 1000.0f) - : (d_m / 1609.344f); - if (dist_unit <= 0) { snprintf(buf, n, "Pace: --:-- %s", unit); return; } - float pace_min = (es_s / 60.0f) / dist_unit; - unsigned pm = (unsigned)pace_min; - unsigned ps = (unsigned)((pace_min - pm) * 60.0f); - snprintf(buf, n, "Pace: %u:%02u %s", pm, ps, unit); - } + bool imperial = p && p->units_imperial; + bool pace = p && p->trail_show_pace; + uint16_t kmh = _store->avgSpeedKmh(); + if (!pace) { + if (imperial) snprintf(buf, n, "Avg: %u mph", (unsigned)((float)kmh * 0.621371f + 0.5f)); + else snprintf(buf, n, "Avg: %u km/h", (unsigned)kmh); + return; } + uint32_t d_m = _store->totalDistanceMeters(); + uint32_t es_s = _store->elapsedSeconds(); + const char* unit = imperial ? "/mi" : "/km"; + float dist_unit = imperial ? (d_m / 1609.344f) : (d_m / 1000.0f); + if (d_m == 0 || es_s == 0 || dist_unit <= 0) { + snprintf(buf, n, "Pace: --:-- %s", unit); + return; + } + float pace_min = (es_s / 60.0f) / dist_unit; + unsigned pm = (unsigned)pace_min; + unsigned ps = (unsigned)((pace_min - pm) * 60.0f); + snprintf(buf, n, "Pace: %u:%02u %s", pm, ps, unit); } void summaryItem(int i, char* buf, size_t n) const { @@ -518,9 +518,9 @@ private: break; case 2: { uint32_t d = _store->totalDistanceMeters(); - if (d < 1000) snprintf(buf, n, "Dist: %lu m", (unsigned long)d); - else snprintf(buf, n, "Dist: %lu.%02lu km", - (unsigned long)(d / 1000), (unsigned long)((d % 1000) / 10)); + char ds[12]; + geo::fmtDist(ds, sizeof(ds), d / 1000.0f, useImperial()); + snprintf(buf, n, "Dist: %s", ds); break; } case 3: { @@ -608,8 +608,9 @@ private: const TrailPoint& prev = _store->at(idx - 1); float d = TrailStore::haversineMeters(prev.lat_1e6, prev.lon_1e6, pt.lat_1e6, pt.lon_1e6); - if (d < 1000.0f) snprintf(dist, sizeof(dist), "+%d m", (int)d); - else snprintf(dist, sizeof(dist), "+%.1f km", d / 1000.0f); + char ds[10]; + geo::fmtDist(ds, sizeof(ds), d / 1000.0f, useImperial()); + snprintf(dist, sizeof(dist), "+%s", ds); } char row[28]; @@ -834,9 +835,18 @@ private: display.fillRect(x, y, 1, 1); }; + // Grid spacing stays metric (the STEPS are round metres); the label is + // converted to the user's unit so the scale bar matches the rest of the UI. char lbl[12]; - if (grid_m < 1000) snprintf(lbl, sizeof(lbl), "%um", (unsigned)grid_m); - else snprintf(lbl, sizeof(lbl), "%ukm", (unsigned)(grid_m / 1000)); + if (useImperial()) { + float ft = grid_m * 3.28084f; + if (ft < 5280.0f) snprintf(lbl, sizeof(lbl), "%uft", (unsigned)(ft + 0.5f)); + else snprintf(lbl, sizeof(lbl), "%umi", (unsigned)(grid_m / 1609.344f + 0.5f)); + } else if (grid_m < 1000) { + snprintf(lbl, sizeof(lbl), "%um", (unsigned)grid_m); + } else { + snprintf(lbl, sizeof(lbl), "%ukm", (unsigned)(grid_m / 1000)); + } int lh = display.getLineHeight(); int cw = display.getCharWidth();