mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 23:08:11 +00:00
feat(units): global metric/imperial setting for all distance displays
Add a single "Units" choice (Settings › System: Metric / Imperial) that governs every distance and speed shown in Tools — Nearby Nodes, the Trail views, and the navigate-to-point screen. - NodePrefs: units_imperial (global) + trail_show_pace (Trail readout mode), schema bumped 0xC0DE0005 → 0006. The old combined trail_units_idx is kept for file-layout stability but no longer drives anything; upgraders default to metric + speed. - geo::fmtDist gains an imperial mode (ft below ~1000 ft, then miles). - NavView / Nearby / Trail (waypoint list, Summary distance, List deltas, map scale-bar label) all format through the global preference. - Trail's action-menu "Units" row becomes a Speed/Pace readout toggle; the km-vs-mi unit now follows the global setting instead of being one of four combined options. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user