refactor: one copy of shared logic and widgets; GPS is asked for, not switched on silently

Duplicates found between the L1 and L2 frontends, and inside the L2 one:
- geo::fmtLocShare / fmtLatLon / turnDeg / isAhead (GeoUtils.h): the [LOC]
  text was built in five places, the turn angle and its "ahead" threshold in
  both navigations, coordinates by hand in nine. Tested.
- LiveShareEngine::setEnabled: live share was switched in three places.
- L1 waypoint share uses WaypointModel::shareText; Trail's "Share my pos"
  goes through UITask::quickShareMyLocation, so it now sends to a running
  live-share session as the home Map page does.
- L2 Widgets.h: switchCreate (one switch size, was four), switchRow with a
  state and callback (16 hand-wired copies), switchRowOpens (Bluetooth, WiFi,
  GPS), primaryButton (one height for Save / Measure / New area / Update).

GPS: with it off, starting the trail, live share or track back, sharing or
marking a position, or centring the map asks first ("GPS is off") on both
L1 and L2; Cancel leaves it undone. A share or a mark waits for a fresh fix
(a switched-off module may still report its last one), which also fixes the
L1 "Share my pos" sending a stale position. SOS still turns the GPS on itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-11-02 10:32:23 +01:00
co-authored by Claude Opus 5.5
parent 781a480aef
commit ada5b8e545
35 changed files with 301 additions and 208 deletions
+5
View File
@@ -277,6 +277,11 @@ Right** between them, **Up / Down** scrolls):
battery; anything that needs your position (the trail, live share, the
locator) keeps it on.
With the GPS switched off, starting the trail, live share or track back, or
sharing or marking your position, asks first (**GPS is off**): turning it on
there starts what you chose; a share or a mark waits for the first fix, as
the last one may be old. SOS doesn't ask: it turns the GPS on itself.
> [!NOTE]
> **Wio Tracker L2:** the Diagnostics app, with a **Noise** tab that
> measures the noise floor over time and history charts you can touch for the
+20
View File
@@ -45,6 +45,18 @@ static inline const char* bearingCardinal(int deg) {
return dirs[((deg + 22) % 360) / 45];
}
// How far to turn from course `cog` to bearing `to` (degrees): -180..179, right
// positive. Within AHEAD_DEG either way the target counts as straight on.
static const int AHEAD_DEG = 12;
static inline int turnDeg(int to, int cog) { return (((to - cog) % 360 + 540) % 360) - 180; }
static inline bool isAhead(int turn) { return turn <= AHEAD_DEG && turn >= -AHEAD_DEG; }
// "50.06143,19.93658": a coordinate as messages carry it, and parseLatLon reads
// back. `spaced` ("50.06143, 19.93658") to show it, or to edit it.
static inline void fmtLatLon(char* buf, int n, int32_t lat_1e6, int32_t lon_1e6, bool spaced = false) {
snprintf(buf, n, spaced ? "%.5f, %.5f" : "%.5f,%.5f", lat_1e6 / 1e6, lon_1e6 / 1e6);
}
// Human distance string. Metric: "850m" / "2.3km" / "140km". Imperial:
// "850ft" / "2.3mi" / "140mi" (feet below ~1000 ft, then miles). `spaced`
// puts a space before the unit ("850 m"), for value rows with room for it.
@@ -143,6 +155,14 @@ static inline bool parseLatLon(const char* text, int32_t& lat_1e6, int32_t& lon_
// the tag (it just looks like a coordinate).
#define LOCATION_MSG_TAG "[LOC]"
// "[LOC]lat,lon", then `name` when given: a DM has no sender in front of it.
static inline void fmtLocShare(char* buf, int n, int32_t lat_1e6, int32_t lon_1e6, const char* name = nullptr) {
int o = snprintf(buf, n, LOCATION_MSG_TAG);
if (o < 0 || o >= n) return;
fmtLatLon(buf + o, n - o, lat_1e6, lon_1e6);
if (name && name[0]) { size_t l = strlen(buf); snprintf(buf + l, n - l, " %s", name); }
}
// An SOS with a position ("SOS 50.06143,19.93658 ...", "SOS last ...", see
// ui-core/SosEngine.h): `text` without the sender's name in front. Only at the
// very start, so a message that mentions SOS isn't taken for one.
@@ -73,6 +73,12 @@ public:
// Start the session afresh (next tick treats it as a new enable: re-announce
// + new duration clock).
void restartSession() { _was_enabled = false; }
// Sharing on (a session of its own, announced at once) or off.
void setEnabled(bool on) {
if (!_prefs) return;
_prefs->loc_share_enabled = on ? 1 : 0;
if (on) restartSession();
}
// Restart only the session's duration clock -- unlike restartSession(), no
// re-announce. For a changed "Stop after" length, where position hasn't
// changed. No-op before a session has started (loop() sets the clock then).
@@ -108,7 +114,7 @@ public:
if (_prefs->loc_share_target_type == 0) {
// Channel: sendGroupMessage prepends "<name>: ", so the payload already
// names the sender — keep the [LOC] text bare.
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6);
geo::fmtLocShare(text, sizeof(text), lat, lon);
ChannelDetails ch;
if (!the_mesh.getChannel(_prefs->loc_share_channel_idx, ch)) return false;
return the_mesh.sendGroupMessage(rtc_clock.getCurrentTime(), ch.channel,
@@ -120,8 +126,7 @@ public:
ContactInfo* c = the_mesh.lookupContactByPubKey(_prefs->loc_share_dm_prefix,
NodePrefs::FAVOURITE_PREFIX_LEN);
if (!c) return false;
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f %s",
lat / 1e6, lon / 1e6, the_mesh.getNodeName());
geo::fmtLocShare(text, sizeof(text), lat, lon, the_mesh.getNodeName());
uint32_t expected_ack = 0, est_timeout = 0;
return the_mesh.sendMessage(*c, rtc_clock.getCurrentTime(), 0, text, expected_ack, est_timeout) > 0;
}
@@ -76,9 +76,9 @@ public:
void shareText(int i, char* out, size_t n) const {
if (i < 0 || i >= _store.count()) { out[0] = '\0'; return; }
const Waypoint& w = _store.at(i);
double lat = w.lat_1e6 / 1000000.0, lon = w.lon_1e6 / 1000000.0;
if (w.label[0]) snprintf(out, n, WAYPOINT_MSG_TAG "%.5f,%.5f %s", lat, lon, w.label);
else snprintf(out, n, WAYPOINT_MSG_TAG "%.5f,%.5f", lat, lon);
int o = snprintf(out, n, WAYPOINT_MSG_TAG);
geo::fmtLatLon(out + o, (int)n - o, w.lat_1e6, w.lon_1e6);
if (w.label[0]) { size_t l = strlen(out); snprintf(out + l, n - l, " %s", w.label); }
}
private:
@@ -309,10 +309,7 @@ void UITask::adminValuePopup() {
if (f->kind == admin::K_ONOFF) {
lv_obj_t* r = centerRow(panel);
label(r, "Off", THEME_FONT_BODY, theme::TEXT_MUTED);
lv_obj_t* sw = lv_switch_create(r);
lv_obj_set_size(sw, 56, 28);
if (S.value != 0) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, onAdminValue, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)V_TOGGLE);
switchCreate(r, S.value != 0, onAdminValue, (void*)(uintptr_t)V_TOGGLE);
label(r, "On", THEME_FONT_BODY, theme::TEXT_MUTED);
} else if (f->kind == admin::K_NUMBER) {
lv_obj_t* r = centerRow(panel);
+2 -8
View File
@@ -58,12 +58,6 @@ static void botChip(const char* ph, void* parent) {
lv_obj_add_event_cb(b, onBotPlaceholder, LV_EVENT_CLICKED, NULL);
lv_obj_center(label(b, ph, THEME_FONT_SMALL, theme::TEXT));
}
// A switchRow() for something that isn't a plain pref: `cb` gets `idx`.
static void switchRowIdx(lv_obj_t* parent, const char* text, const char* hint, bool on, lv_event_cb_t cb, int idx) {
lv_obj_t* sw = switchRow(parent, text, hint, nullptr);
if (on) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, cb, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)idx);
}
static void onOpenBot(lv_event_t* e) { (void)e; s_ui->showBot(); }
static lv_obj_t* botHourButton(lv_obj_t* parent, const char* text, uint8_t act, bool accent) {
@@ -148,7 +142,7 @@ void UITask::buildBot() {
const botcfg::Row& r = botcfg::row(s_tab, i);
uint8_t* fl = botcfg::flag(p, r.kind);
if (fl && !botcfg::isScope(r.kind)) {
switchRowIdx(g, r.label, r.hint, *fl, onBotSwitch, i);
switchRow(g, r.label, r.hint, *fl, onBotSwitch, (void*)(uintptr_t)i);
continue;
}
uint16_t* cmd_off = botcfg::cmdOff(p, r.kind);
@@ -255,7 +249,7 @@ void UITask::botRow(int row) {
if (uint16_t* off = botcfg::cmdOff(p, r.kind)) {
lv_obj_t* g = group(scrollList(navPopupPanel("Allowed commands", true)), nullptr);
for (int i = 0; i < botcmd::count(botcfg::actionsOn(p, r.kind)); i++)
switchRowIdx(g, botcmd::CMDS[i].label, botcmd::CMDS[i].hint, !(*off & botcmd::flag(i)), onBotCmd, i);
switchRow(g, botcmd::CMDS[i].label, botcmd::CMDS[i].hint, !(*off & botcmd::flag(i)), onBotCmd, (void*)(uintptr_t)i);
return;
}
@@ -186,19 +186,9 @@ void UITask::buildChannelEdit() {
s_secret = chanField(body, "Passphrase");
lv_textarea_set_max_length(s_secret, 32);
lv_obj_t* row = settingRow(body, "Hex key", "32 characters");
s_hex_sw = lv_switch_create(row);
lv_obj_set_size(s_hex_sw, 46, 24);
lv_obj_align(s_hex_sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
lv_obj_add_event_cb(s_hex_sw, onChanHex, LV_EVENT_VALUE_CHANGED, NULL);
s_hex_sw = switchRow(body, "Hex key", "32 characters", false, onChanHex);
lv_obj_t* save = lv_button_create(body);
lv_obj_set_size(save, LV_PCT(100), 38);
lv_obj_set_style_shadow_width(save, 0, 0);
lv_obj_set_style_radius(save, theme::RADIUS, 0);
lv_obj_add_event_cb(save, onChanSave, LV_EVENT_CLICKED, NULL);
lv_obj_center(label(save, LV_SYMBOL_OK " Save", THEME_FONT_BODY, theme::TEXT));
stylePrimary(save);
primaryButton(body, LV_SYMBOL_OK " Save", onChanSave);
s_status = noteLabel(body, "", THEME_FONT_SMALL, theme::FAIL);
if (edit) {
@@ -314,10 +314,8 @@ void UITask::buildClock() {
lv_obj_set_flex_align(on, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(on, 8, 0);
lv_obj_set_style_pad_bottom(on, 4, 0);
lv_obj_t* sw = s_alarm_sw = lv_switch_create(on);
lv_obj_t* sw = s_alarm_sw = switchCreate(on, _prefs->alarm_on, nullptr, nullptr);
label(on, "Alarm on", THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_obj_set_size(sw, 50, 26);
if (_prefs->alarm_on) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, onAlarmSwitch, LV_EVENT_VALUE_CHANGED, NULL);
label(col, "Repeat", THEME_FONT_SMALL, theme::TEXT_MUTED);
char opts[48];
@@ -366,14 +366,8 @@ void UITask::buildDiag() {
if (s_tab == TAB_NOISE) {
noteLabel(s_list, "What the radio hears with nothing on air, on the mesh frequency and 1.1 MHz "
"either side. Takes about 12 seconds.");
lv_obj_t* b = lv_button_create(s_list);
lv_obj_set_size(b, LV_PCT(100), 40);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_add_event_cb(b, onNoiseRun, LV_EVENT_CLICKED, NULL);
s_noise_status = label(b, s_noise_have ? LV_SYMBOL_REFRESH " Measure again" : LV_SYMBOL_PLAY " Measure", THEME_FONT_BODY, theme::TEXT);
lv_obj_center(s_noise_status);
stylePrimary(b);
lv_obj_t* b = primaryButton(s_list, s_noise_have ? LV_SYMBOL_REFRESH " Measure again" : LV_SYMBOL_PLAY " Measure", onNoiseRun);
s_noise_status = lv_obj_get_child(b, 0);
if (s_noise_have) {
float mesh_f = _prefs ? _prefs->freq : 0;
lv_obj_t* card = infoCard(s_list);
+44 -1
View File
@@ -99,6 +99,49 @@ static void onOpenGps(lv_event_t* e) { (void)e; s_ui->showGps(false); }
static void onOpenGpsFromSettings(lv_event_t* e) { (void)e; s_ui->showGps(true); }
static void onGpsTurnOn(lv_event_t* e) { (void)e; s_ui->setGps(true); s_ui->refreshGps(); }
// ── "GPS is off" ──────────────────────────────────────────────────────────────
// Something that needs the GPS while it's off asks first, as on the L1: Turn
// on does it, the close button leaves it undone.
static void onGpsAsk(lv_event_t* e) {
UITask::GpsAsk what = (UITask::GpsAsk)(uintptr_t)lv_event_get_user_data(e);
s_ui->navClosePopup();
s_ui->setGps(true);
s_ui->gpsAskDone(what);
}
bool UITask::gpsAsked(GpsAsk what) {
if (!_core->gpsAvailable() || _core->gpsEnabled()) return false;
static const char* const NEEDS[] = {
"The trail is recorded from the GPS.", "Live share sends the GPS position.", "Track back follows the GPS.",
"The map finds you by the GPS.", "This needs a GPS fix.",
};
lv_obj_t* panel = navPopupPanel("GPS is off", false);
noteLabel(panel, NEEDS[what]);
primaryButton(panel, LV_SYMBOL_GPS " Turn on GPS", onGpsAsk, (void*)(uintptr_t)what);
return true;
}
// Just turned on: what asked for it. A position needs a fresh fix first (its
// last one may be old), so a share or a mark is left for then.
void UITask::gpsAskDone(GpsAsk what) {
switch (what) {
case GA_TRAIL:
_core->trail.setActive(true);
showToast("GPS on, recording the trail");
break;
case GA_SHARE:
_core->live_share.setEnabled(true);
prefsSave();
showToast("GPS on, sharing your position");
break;
case GA_TRACKBACK: navToolAction(navmap::TL_TRACKBACK); break;
case GA_CENTRE: mapCenterOnMe(); break;
case GA_FIX: showToast("GPS on, waiting for a fix"); break;
}
refreshStatusBar();
}
// Sky plot: horizon ring, 30 / 60 degree rings, N-S / E-W lines, one dot per
// satellite at its azimuth (N up) and elevation (overhead in the centre).
static void onGpsSkyDraw(lv_event_t* e) {
@@ -480,7 +523,7 @@ void UITask::refreshGps() {
if (g->utc_valid) snprintf(utc, sizeof(utc), "%02u:%02u:%02u", g->utc_h, g->utc_m, g->utc_s);
int32_t lat, lon;
if (fix && _core->course.currentLocation(lat, lon)) {
snprintf(pos, sizeof(pos), "%.5f, %.5f", lat / 1e6, lon / 1e6);
geo::fmtLatLon(pos, sizeof(pos), lat, lon, true);
locator(lat / 1e6, lon / 1e6, grid, sizeof(grid));
}
if (fix && g->alt_valid) snprintf(alt, sizeof(alt), "%.0f m", g->alt_m);
+1 -7
View File
@@ -388,13 +388,7 @@ void UITask::mapAreasPopup() {
} else {
noteLabel(list, "No areas yet. Maps downloaded before this list are still on the card.");
}
lv_obj_t* add = lv_button_create(list);
lv_obj_set_size(add, LV_PCT(100), 38);
lv_obj_set_style_radius(add, theme::RADIUS, 0);
lv_obj_set_style_shadow_width(add, 0, 0);
lv_obj_add_event_cb(add, onNewArea, LV_EVENT_CLICKED, NULL);
lv_obj_center(label(add, LV_SYMBOL_PLUS " New area", THEME_FONT_BODY, theme::TEXT));
stylePrimary(add);
primaryButton(list, LV_SYMBOL_PLUS " New area", onNewArea);
}
void UITask::mapAreaPopup(int idx) {
+2 -1
View File
@@ -1292,7 +1292,8 @@ void UITask::mapZoom(int delta) {
void UITask::mapCenterOnMe() {
int32_t lat, lon;
if (!ensureGps() || !_core->course.currentLocation(lat, lon)) { _map_follow = true; followShown(_map_center_btn, true); return; } // centres once a fix comes
if (gpsAsked(GA_CENTRE)) return;
if (!ensureGps(GA_CENTRE) || !_core->course.currentLocation(lat, lon)) { _map_follow = true; followShown(_map_center_btn, true); return; } // centres once a fix comes
_map_follow = true;
_map_cx = mapview::lonToTileX(lon / 1e6, _map_z);
_map_cy = mapview::latToTileY(lat / 1e6, _map_z) + mapCenterBias();
+19 -24
View File
@@ -100,9 +100,8 @@ static void onTrailDraw(lv_event_t* e) {
static lv_obj_t* s_dial = nullptr;
static int32_t s_dial_ang = 0; // shown: degrees clockwise from the dial's top
static bool s_dial_course = false; // a course is known: the top is ahead, else north
static bool s_dial_ahead = false; // within AHEAD_DEG of straight on
static bool s_dial_ahead = false; // within geo::AHEAD_DEG of straight on
static const int DIAL = 38;
static const int AHEAD_DEG = 12;
static const uint32_t DIAL_MS = 300;
static void setDialAng(void* o, int32_t v) {
@@ -159,7 +158,7 @@ static void setDial(int deg, bool course) {
deg = ((deg % 360) + 360) % 360;
const bool snap = course != s_dial_course || lv_obj_has_flag(s_dial, LV_OBJ_FLAG_HIDDEN);
s_dial_course = course;
s_dial_ahead = course && (deg <= AHEAD_DEG || deg >= 360 - AHEAD_DEG);
s_dial_ahead = course && geo::isAhead(geo::turnDeg(deg, 0));
lv_obj_remove_flag(s_dial, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_style_border_color(s_dial, lv_color_hex(s_dial_ahead ? theme::OK : theme::SURFACE_2), 0);
lv_anim_delete(s_dial, setDialAng);
@@ -517,7 +516,7 @@ void UITask::refreshNavBar() {
navmap::setDial(course ? to - cog : to, course);
// Walking: how far to turn (the dial shows it); standing: where the target lies.
if (course) {
int turn = ((to - cog + 540) % 360) - 180; // -180..179, right is positive
int turn = geo::turnDeg(to, cog);
if (navmap::s_dial_ahead) snprintf(what, sizeof(what), "straight ahead");
else snprintf(what, sizeof(what), "turn %s %d\xC2\xB0", turn > 0 ? "right" : "left", turn > 0 ? turn : -turn);
} else {
@@ -732,7 +731,7 @@ static void navRowRight(lv_obj_t* row, const char* text, uint32_t col, int right
void UITask::placeCard(lv_obj_t* parent, int32_t lat, int32_t lon) {
lv_obj_t* card = infoCard(parent);
char t[40];
snprintf(t, sizeof(t), "%.5f, %.5f", lat / 1e6, lon / 1e6);
geo::fmtLatLon(t, sizeof(t), lat, lon, true);
infoRow(card, "Coordinates", t);
int32_t mlat, mlon;
if (!_core->course.currentLocation(mlat, mlon)) return;
@@ -831,7 +830,7 @@ void UITask::navAddWaypoint(int32_t lat, int32_t lon) {
void UITask::navMarkHere() {
int32_t lat, lon;
if (!ensureGps() || !_core->course.currentLocation(lat, lon)) return;
if (!ensureGps(GA_FIX) || !_core->course.currentLocation(lat, lon)) return;
uint16_t secs = NodePrefs::gpsAvgSecs(_prefs ? _prefs->gps_avg_idx : 0);
if (secs == 0) { navAddWaypoint(lat, lon); return; }
if (_core->waypoints.full()) { showToast(waypointsFull()); return; }
@@ -1030,14 +1029,10 @@ void UITask::navToolsPopup(bool back) {
// What the map shows, right under the trail: used more than what follows.
lv_obj_t* g = group(list, "LAYERS");
choiceRow(g, "Map", "Topo, light, dark or satellite", mapview::LAYER_OPTS, mapview::s_layer, onMapLayer);
lv_obj_t* tsw = switchRow(g, "Hiking trails", "Marked routes in their colours", nullptr);
if (lvport::trailsOn()) lv_obj_add_state(tsw, LV_STATE_CHECKED);
lv_obj_add_event_cb(tsw, onTrails, LV_EVENT_VALUE_CHANGED, NULL);
switchRow(g, "Hiking trails", "Marked routes in their colours", lvport::trailsOn(), onTrails);
schemaRow(g, SETTING(flow_show));
#ifdef MAP_VECTOR // unfinished (LvglPort.h vectorOn)
lv_obj_t* vsw = switchRow(g, "Vector map (test)", "Drawn on the device: trails, contours", nullptr);
if (lvport::vectorOn()) lv_obj_add_state(vsw, LV_STATE_CHECKED);
lv_obj_add_event_cb(vsw, onVectorMap, LV_EVENT_VALUE_CHANGED, NULL);
switchRow(g, "Vector map (test)", "Drawn on the device: trails, contours", lvport::vectorOn(), onVectorMap);
char vr[48];
int nv = mapview::s_vpacks.count();
snprintf(vr, sizeof(vr), nv ? "%d on the card" : "None - packs go in /vmap", nv);
@@ -1094,9 +1089,7 @@ void UITask::navToolsPopup(bool back) {
int na = mapview::s_areas.count();
snprintf(ar, sizeof(ar), na ? "%d on the card - new, rename, refresh, delete" : "None yet - pick one with a frame", na);
listRow(g, "Map areas", ar, onNavTool, (void*)(uintptr_t)navmap::TL_AREAS);
lv_obj_t* lsw = switchRow(g, "Live tiles", "Load missing tiles over WiFi", nullptr);
if (lvport::liveTiles()) lv_obj_add_state(lsw, LV_STATE_CHECKED);
lv_obj_add_event_cb(lsw, onLiveTiles, LV_EVENT_VALUE_CHANGED, NULL);
switchRow(g, "Live tiles", "Load missing tiles over WiFi", lvport::liveTiles(), onLiveTiles);
refreshNavTools();
@@ -1360,10 +1353,11 @@ void UITask::navToolAction(uint8_t act) {
switch (act) {
case navmap::TL_TRAIL_TOGGLE:
if (!tr.isActive()) {
if (gpsAsked(GA_TRAIL)) return; // its popup in place of this one
tr.setActive(true);
int32_t lat, lon;
if (_core->course.currentLocation(lat, lon)) showToast("Recording the trail");
else ensureGps(); // turns GPS on, or says it's waiting
else ensureGps(GA_TRAIL); // says it's waiting
} else {
tr.setActive(false);
showToast("Trail stopped");
@@ -1402,11 +1396,12 @@ void UITask::navToolAction(uint8_t act) {
break;
case navmap::TL_TRACKBACK: {
if (navmap::s_tb.active()) { navmap::s_tb.stop(); showToast("Track back stopped"); break; }
if (gpsAsked(GA_TRACKBACK)) return; // its popup in place of this one
int32_t lat = 0, lon = 0;
bool fix = _core->course.currentLocation(lat, lon);
if (!navmap::s_tb.start(tr.store(), fix, lat, lon)) { showToast("No trail to walk back"); break; }
navmap::s_eta.reset();
if (!fix) ensureGps();
if (!fix) ensureGps(GA_TRACKBACK); // says it's waiting
navClosePopup();
navFrameTarget();
refreshNavBar();
@@ -1421,24 +1416,24 @@ void UITask::navToolAction(uint8_t act) {
}
case navmap::TL_SHARE_TOGGLE:
if (_prefs->loc_share_enabled) {
_prefs->loc_share_enabled = 0;
_core->live_share.setEnabled(false);
showToast("Live share stopped");
} else {
_prefs->loc_share_enabled = 1;
_core->live_share.restartSession();
if (gpsAsked(GA_SHARE)) return;
_core->live_share.setEnabled(true);
int32_t lat, lon;
if (_core->course.currentLocation(lat, lon)) showToast("Sharing your position");
else ensureGps();
else ensureGps(GA_SHARE); // says it's waiting
}
prefsSave();
break;
case navmap::TL_SHARE_ONCE: {
int32_t lat, lon;
if (!ensureGps() || !_core->course.currentLocation(lat, lon)) break;
if (!ensureGps(GA_FIX) || !_core->course.currentLocation(lat, lon)) break;
// With a session on, straight to its target; otherwise pick a conversation.
if (_prefs->loc_share_enabled && _core->live_share.send(lat, lon)) { showToast("Position sent"); break; }
char text[40];
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6);
geo::fmtLocShare(text, sizeof(text), lat, lon);
navClosePopup();
shareToMessage(text);
return;
@@ -1520,7 +1515,7 @@ void UITask::navTargetsPopup() {
const Waypoint& w = wp.at(i);
char title[WAYPOINT_LABEL_LEN + 8], sub[40], d[12];
snprintf(title, sizeof(title), UI_SYMBOL_FLAG " %s", w.label[0] ? w.label : "(unnamed)");
snprintf(sub, sizeof(sub), "%.5f, %.5f", w.lat_1e6 / 1e6, w.lon_1e6 / 1e6);
geo::fmtLatLon(sub, sizeof(sub), w.lat_1e6, w.lon_1e6, true);
lv_obj_t* row = listRow(list, title, sub, onNavPick, (void*)(uintptr_t)navmap::code(navmap::T_WAYPOINT, i));
lv_obj_t* more = lv_button_create(row); // the waypoint menu
lv_obj_set_size(more, 40, 34);
+2 -6
View File
@@ -294,8 +294,6 @@ static void nodeFavText() {
if (s_nd_fav) lv_label_set_text(s_nd_fav, s_node.fav ? "Remove from favourites" : "Add to favourites"); // the star in its icon column
}
static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub, uint8_t* pref); // below
// One of the node's "Can ask for" switches; user data = its TELEM_PERM_* bit.
static void onNodeTelemSwitch(lv_event_t* e) {
ContactInfo* ci = the_mesh.lookupContactByPubKey(s_node.pub_key, PUB_KEY_SIZE);
@@ -366,9 +364,7 @@ void UITask::buildNode() {
};
uint8_t perms = contactctl::telemetry(*ci);
for (const auto& t : T) {
lv_obj_t* sw = switchRow(g, t.text, t.sub, nullptr);
if (perms & t.bit) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, onNodeTelemSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)t.bit);
switchRow(g, t.text, t.sub, perms & t.bit, onNodeTelemSwitch, (void*)(uintptr_t)t.bit);
}
}
@@ -408,7 +404,7 @@ void UITask::refreshNode() {
bearing = az;
snprintf(dist, sizeof(dist), "%s %d\xC2\xB0 %s", d, az, geo::bearingCardinal(az));
}
snprintf(pos, sizeof(pos), "%.5f, %.5f", e.lat_e6 / 1e6, e.lon_e6 / 1e6);
geo::fmtLatLon(pos, sizeof(pos), e.lat_e6, e.lon_e6, true);
} else if (!_node_from_scan) {
snprintf(pos, sizeof(pos), "Not shared");
}
+2 -9
View File
@@ -198,15 +198,8 @@ void UITask::buildOta() {
lv_bar_set_range(_ota_bar, 0, 1000);
lv_obj_set_style_bg_color(_ota_bar, lv_color_hex(theme::SURFACE), LV_PART_MAIN);
lv_obj_set_style_bg_color(_ota_bar, lv_color_hex(theme::ACCENT), LV_PART_INDICATOR);
lv_obj_t* b = lv_button_create(body);
lv_obj_set_size(b, LV_PCT(100), 40);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_add_event_cb(b, onOtaButton, LV_EVENT_CLICKED, NULL);
_ota_btn = b;
_ota_btn_lbl = label(b, "", THEME_FONT_BODY, theme::TEXT);
lv_obj_center(_ota_btn_lbl);
stylePrimary(b);
_ota_btn = primaryButton(body, "", onOtaButton);
_ota_btn_lbl = lv_obj_get_child(_ota_btn, 0);
groupNote(body, "From github.com/" OTA_REPO " releases, over WiFi. About a minute; keep the device "
"charged. Settings, contacts and messages are kept.");
refreshOta();
+10 -7
View File
@@ -371,15 +371,18 @@ void UITask::quickPanelTile(int t, bool hold) {
#endif
break;
case T_TRAIL:
_core->trail.setActive(!_core->trail.isActive());
if (_core->trail.isActive()) ensureGps(); // turns GPS on, or says it's waiting
break;
case T_SHARE:
case T_SHARE: {
if (!_prefs) break;
_prefs->loc_share_enabled = !_prefs->loc_share_enabled;
if (_prefs->loc_share_enabled) { _core->live_share.restartSession(); ensureGps(); }
prefsSave();
const bool on = t == T_TRAIL ? _core->trail.isActive() : _prefs->loc_share_enabled;
if (!on && _core->gpsAvailable() && !_core->gpsEnabled()) { // "GPS is off", over the screen
quickPanelClose(false);
gpsAsked(t == T_TRAIL ? GA_TRAIL : GA_SHARE);
return;
}
if (t == T_TRAIL) _core->trail.setActive(!on);
else { _core->live_share.setEnabled(!on); prefsSave(); }
break;
}
}
refreshStatusBar();
quickPanelRefresh();
@@ -202,15 +202,11 @@ void UITask::advertSend(bool flood) {
static void bluetoothRow(lv_obj_t* body) {
char sub[48];
qview::bluetoothHint(sub, sizeof(sub));
lv_obj_t* sw = switchRow(body, LV_SYMBOL_BLUETOOTH " Bluetooth", sub, nullptr);
if (s_ui->isBluetoothEnabled()) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, qview::onBtSwitch, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_t* sw = switchRow(body, LV_SYMBOL_BLUETOOTH " Bluetooth", sub, s_ui->isBluetoothEnabled(), qview::onBtSwitch);
#ifdef BLE_PIN_CODE
lv_obj_t* row = lv_obj_get_parent(sw);
rowPressable(row);
lv_obj_add_event_cb(row, [](lv_event_t* e) {
if (lv_event_get_target(e) == lv_event_get_current_target(e)) onBlePin(e); // not the switch
}, LV_EVENT_CLICKED, NULL);
switchRowOpens(sw, onBlePin);
#else
(void)sw;
#endif
}
@@ -111,10 +111,9 @@ void UITask::radioGroup(int i) {
int tx = p->tx_power_dbm < TX_MIN ? TX_MIN : p->tx_power_dbm > TX_MAX ? TX_MAX : p->tx_power_dbm;
rowDropdown(settingRow(g, "TX power", p->tx_apc ? "Ceiling for auto power" : nullptr), s_opts, tx - TX_MIN, 0,
onRadioDropdown, R_TX);
lv_obj_t* sw = switchRow(g, "Auto power", p->client_repeat ? "Off while repeating" : "Lowers power on good links", nullptr);
if (p->tx_apc) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_t* sw = switchRow(g, "Auto power", p->client_repeat ? "Off while repeating" : "Lowers power on good links",
p->tx_apc, onRadioSwitch, (void*)(uintptr_t)R_APC);
if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_DISABLED);
lv_obj_add_event_cb(sw, onRadioSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)R_APC);
}
void UITask::radioSet(int which, int v) {
@@ -54,9 +54,7 @@ void UITask::buildRepeater() {
NodePrefs* p = _prefs;
lv_obj_t* g = group(body, nullptr);
lv_obj_t* sw = switchRow(g, "Repeater", p->client_repeat ? "Relaying for others" : "Relay others' messages", nullptr);
if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, onRptSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)RP_ON);
lv_obj_t* sw = switchRow(g, "Repeater", p->client_repeat ? "Relaying for others" : "Relay others' messages", p->client_repeat, onRptSwitch, (void*)(uintptr_t)RP_ON);
s_on_sub = rowSub(lv_obj_get_parent(sw));
groupNote(body, "Uses more battery; auto power pauses.");
fill(&UITask::repeaterGroup, 1, 3); // the rest as the loop goes
@@ -12,30 +12,10 @@ static void onKeyboardAlphabet(lv_event_t* e) {
s_ui->setKeyboardAlphabets(choiceSelected(s_kb_main_dd), choiceSelected(s_kb_alt_dd));
}
// A preference toggle (0/1 byte in NodePrefs), saved on change.
static void onPrefSwitch(lv_event_t* e) {
uint8_t* pref = (uint8_t*)lv_event_get_user_data(e);
*pref = lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0;
prefsSave();
}
static void onGpsSwitch(lv_event_t* e) {
s_ui->setGps(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED));
}
// pref == nullptr: the caller wires the switch itself.
static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub, uint8_t* pref) {
lv_obj_t* row = settingRow(parent, text, sub, 236); // the hint clear of the switch
lv_obj_t* sw = lv_switch_create(row);
lv_obj_set_size(sw, 46, 24);
lv_obj_align(sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
if (pref) {
if (*pref) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, onPrefSwitch, LV_EVENT_VALUE_CHANGED, pref);
}
return sw;
}
// ── Schema-driven settings (ui-core/SettingsSchema.h) ─────────────────────────
// Settings > Display: ui-new's Message alert as a switch -- the banner, or
@@ -44,9 +24,7 @@ static void onMsgBannerSwitch(lv_event_t* e) {
s_ui->setMsgBanner(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED));
}
void UITask::msgBannerRow(lv_obj_t* card) {
lv_obj_t* sw = switchRow(card, "Message banner", "A new message drops in from the top", nullptr);
if (!_prefs || _prefs->msg_alert != NodePrefs::MSG_ALERT_OFF) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, onMsgBannerSwitch, LV_EVENT_VALUE_CHANGED, NULL);
switchRow(card, "Message banner", "A new message drops in from the top", !_prefs || _prefs->msg_alert != NodePrefs::MSG_ALERT_OFF, onMsgBannerSwitch);
}
void UITask::setMsgBanner(bool on) {
if (!_prefs) return;
@@ -161,9 +139,7 @@ void UITask::buildSchemaSettings() {
schemaRow(g, SETTING(display_brightness));
schemaRow(g, SETTING(auto_off_secs));
schemaRow(g, SETTING(msg_wake));
lv_obj_t* tw = switchRow(g, "Tap to wake", "Off: only the top button wakes it", nullptr); // in NVS
if (_tap_wake) lv_obj_add_state(tw, LV_STATE_CHECKED);
lv_obj_add_event_cb(tw, onTapWake, LV_EVENT_VALUE_CHANGED, NULL);
switchRow(g, "Tap to wake", "Off: only the top button wakes it", _tap_wake, onTapWake);
g = group(body, "LOCK");
schemaRow(g, SETTING(auto_lock));
listRow(g, "Screen PIN", pinSet() ? "On - asked when the screen wakes" : "Off", onPinSetup, NULL);
@@ -207,7 +183,7 @@ enum : int { OWNPOS_TYPE, OWNPOS_WAYPOINTS, OWNPOS_CLEAR, OWNPOS_WP0 = 100 };
void UITask::ownPositionText(char* sub, size_t n) {
int32_t lat, lon;
if (_core->gpsEnabled()) snprintf(sub, n, "From the GPS while it's on");
else if (_core->fixedPosition(lat, lon)) snprintf(sub, n, "%.5f, %.5f", lat / 1e6, lon / 1e6);
else if (_core->fixedPosition(lat, lon)) geo::fmtLatLon(sub, n, lat, lon, true);
else snprintf(sub, n, "Not set");
}
@@ -230,7 +206,7 @@ void UITask::ownPositionPick(int code) {
if (code == OWNPOS_TYPE) {
int32_t lat, lon;
char cur[32] = "";
if (_core->fixedPosition(lat, lon)) snprintf(cur, sizeof(cur), "%.5f, %.5f", lat / 1e6, lon / 1e6);
if (_core->fixedPosition(lat, lon)) geo::fmtLatLon(cur, sizeof(cur), lat, lon, true);
TextEntry t = { "Fixed position", onOwnPosKb };
t.text = cur;
t.hint = "50.06143, 19.93658";
@@ -246,7 +222,7 @@ void UITask::ownPositionPick(int code) {
const Waypoint& w = wp.at(i);
char title[WAYPOINT_LABEL_LEN + 8], sub[40];
snprintf(title, sizeof(title), UI_SYMBOL_FLAG " %s", w.label[0] ? w.label : "(unnamed)");
snprintf(sub, sizeof(sub), "%.5f, %.5f", w.lat_1e6 / 1e6, w.lon_1e6 / 1e6);
geo::fmtLatLon(sub, sizeof(sub), w.lat_1e6, w.lon_1e6, true);
listRow(list, title, sub, onOwnPosPick, (void*)(uintptr_t)(OWNPOS_WP0 + i));
}
return;
@@ -488,9 +464,7 @@ void UITask::schemaRow(lv_obj_t* card, int i) {
return;
}
if (!st.option) {
lv_obj_t* sw = switchRow(card, st.label, st.hint, nullptr);
if (v) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, onSchemaSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)i);
switchRow(card, st.label, st.hint, v, onSchemaSwitch, (void*)(uintptr_t)i);
return;
}
// The longest list, the time zone, is 27 options and 167 bytes: at 160
@@ -584,16 +558,9 @@ void UITask::settingsGroup(int i) {
g = group(body, "CONNECTIONS");
bluetoothRow(g);
wifiRow(g);
if (_core->gpsAvailable()) {
lv_obj_t* sw = switchRow(g, LV_SYMBOL_GPS " GPS", "Tap for satellites and options", nullptr);
if (_core->gpsEnabled()) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, onGpsSwitch, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_t* row = lv_obj_get_parent(sw);
rowPressable(row);
lv_obj_add_event_cb(row, [](lv_event_t* e) {
if (lv_event_get_target(e) == lv_event_get_current_target(e)) onOpenGpsFromSettings(e); // not the switch
}, LV_EVENT_CLICKED, NULL);
}
if (_core->gpsAvailable())
switchRowOpens(switchRow(g, LV_SYMBOL_GPS " GPS", "Tap for satellites and options", _core->gpsEnabled(), onGpsSwitch),
onOpenGpsFromSettings);
break;
case 2:
+1 -1
View File
@@ -352,7 +352,7 @@ void UITask::sosRow(int row) {
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
ChannelDetails ch;
if (!the_mesh.getChannel(i, ch) || !ch.name[0]) continue;
switchRowIdx(g, ch.name, nullptr, p->sos_ch_mask & (1ULL << i), onSosChannel, i);
switchRow(g, ch.name, nullptr, (p->sos_ch_mask >> i) & 1, onSosChannel, (void*)(uintptr_t)i);
n++;
}
if (n == 0) label(list, "No channels.", THEME_FONT_SMALL, theme::TEXT_MUTED);
+3 -2
View File
@@ -930,9 +930,9 @@ void UITask::setGps(bool on) {
void UITask::botSetGPS(bool on) { if (_core->setGpsEnabled(on)) refreshStatusBar(); }
bool UITask::ensureGps() {
bool UITask::ensureGps(GpsAsk what) {
int32_t lat, lon;
if (_core->gpsAvailable() && !_core->gpsEnabled()) { setGps(true); return false; } // off, its last fix still reads valid
if (gpsAsked(what)) return false; // off, its last fix still reads valid
if (_core->course.currentLocation(lat, lon)) return true;
showToast("Waiting for a GPS fix");
return false;
@@ -1516,6 +1516,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
if (!strncmp(name, "accent@", 7)) { theme::setAccent(atoi(name + 7)); return; } // "accent@4": Cyan
if (!strncmp(name, "map@", 4)) { s_ui->simMapAt(name + 4); return; } // "map@lat,lon,z"
if (!strncmp(name, "channel@", 8)) { s_ui->openChannel(atoi(name + 8)); return; }
if (!strcmp(name, "chanedit")) { s_ui->showChannelEdit(-1); return; } // a new channel
if (!strncmp(name, "compose@", 8)) { s_ui->simCompose(name + 8); return; }
if (!strcmp(name, "dm")) { s_ui->simDm(); return; }
if (!strcmp(name, "send")) { s_ui->simSend(); return; }
+6 -1
View File
@@ -404,7 +404,12 @@ public:
void pinPopup(bool channel, uint8_t ch_idx, const uint8_t* pub_key);
void pinTo(int slot);
void setGps(bool on);
bool ensureGps(); // true with a fix; else turns GPS on / says it's waiting
// What needs the GPS while it's off: asked first ("GPS is off"), then done
// once it's turned on (GpsScreen.h).
enum GpsAsk : uint8_t { GA_TRAIL, GA_SHARE, GA_TRACKBACK, GA_CENTRE, GA_FIX };
bool gpsAsked(GpsAsk what); // true: it was off, the question is up
bool ensureGps(GpsAsk what); // true with a fix; else asks, or says it's waiting
void gpsAskDone(GpsAsk what);
// Kept for main.cpp's sim hooks (ui-new API).
void openContactDM(const ContactInfo& ci) { openDM(ci.id.pub_key); }
@@ -1071,6 +1071,19 @@ static void stylePrimary(lv_obj_t* b) {
lv_obj_set_style_text_color(lv_obj_get_child(b, i), lv_color_hex(theme::ON_ACCENT), 0);
}
// The one main action under a form or a list, full width (Save, Measure ...).
// Its label is child 0.
static lv_obj_t* primaryButton(lv_obj_t* parent, const char* text, lv_event_cb_t cb, void* user = nullptr) {
lv_obj_t* b = lv_button_create(parent);
lv_obj_set_size(b, LV_PCT(100), 40);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, user);
lv_obj_center(label(b, text, THEME_FONT_BODY, theme::TEXT));
stylePrimary(b);
return b;
}
// Something selected / on (a tab, a chip, a row): SEL_BG with SEL_FG text
// (Theme.h), or back to the surface with TEXT. Every label under it follows.
static void textUnder(lv_obj_t* o, uint32_t col) {
@@ -1256,6 +1269,52 @@ static void prefsFlush() {
the_mesh.savePrefs();
}
// A switch, one size on every screen.
static lv_obj_t* switchCreate(lv_obj_t* parent, bool on, lv_event_cb_t cb, void* user) {
lv_obj_t* sw = lv_switch_create(parent);
lv_obj_set_size(sw, 46, 24);
if (on) lv_obj_add_state(sw, LV_STATE_CHECKED);
if (cb) lv_obj_add_event_cb(sw, cb, LV_EVENT_VALUE_CHANGED, user);
return sw;
}
// A preference toggle (0/1 byte in NodePrefs), saved on change.
static void onPrefSwitch(lv_event_t* e) {
uint8_t* pref = (uint8_t*)lv_event_get_user_data(e);
*pref = lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0;
prefsSave();
}
// A settings row with a switch on the right. pref == nullptr: the caller wires
// the switch itself.
static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub, uint8_t* pref) {
lv_obj_t* row = settingRow(parent, text, sub, 236); // the hint clear of the switch
lv_obj_t* sw = switchCreate(row, pref && *pref, pref ? onPrefSwitch : nullptr, pref);
lv_obj_align(sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
return sw;
}
// The same for a switch that isn't a plain pref: `cb` gets `user` on a change.
static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub, bool on, lv_event_cb_t cb,
void* user = nullptr) {
lv_obj_t* sw = switchRow(parent, text, sub, nullptr);
if (on) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, cb, LV_EVENT_VALUE_CHANGED, user);
return sw;
}
// A switch row whose text opens the details (`open`) while the switch only
// switches: Bluetooth, WiFi, GPS.
static void onSwitchRowTap(lv_event_t* e) {
if (lv_event_get_target(e) != lv_event_get_current_target(e)) return; // the switch
((lv_event_cb_t)lv_event_get_user_data(e))(e);
}
static void switchRowOpens(lv_obj_t* sw, lv_event_cb_t open) {
lv_obj_t* row = lv_obj_get_parent(sw);
rowPressable(row);
lv_obj_add_event_cb(row, onSwitchRowTap, LV_EVENT_CLICKED, (void*)open);
}
// The big clock in the boot splash's lettering, as ui-new draws it on the
// OLED and e-ink (ui-core/Lettering.h: block digits in 3 px strokes): HH:MM
// at SC x, the seconds after them at SC_SEC x on the same baseline. The
+4 -18
View File
@@ -72,9 +72,7 @@ void UITask::buildWifi() {
{ // the same switch as Settings' row
char sub[48];
wifiHint(sub, sizeof(sub));
lv_obj_t* sw = switchRow(group(body, nullptr), LV_SYMBOL_WIFI " WiFi", sub, nullptr);
if (lvport::wifiAllowed()) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, onWifiAllowed, LV_EVENT_VALUE_CHANGED, NULL);
switchRow(group(body, nullptr), LV_SYMBOL_WIFI " WiFi", sub, lvport::wifiAllowed(), onWifiAllowed);
groupNote(body, "For map downloads, live map tiles and updates.");
}
@@ -129,13 +127,7 @@ void UITask::buildWifi() {
_wifi_pass = wifiField(body, "Password", true);
lv_obj_t* save = lv_button_create(body);
lv_obj_set_size(save, LV_PCT(100), 38);
lv_obj_set_style_shadow_width(save, 0, 0);
lv_obj_set_style_radius(save, theme::RADIUS, 0);
lv_obj_add_event_cb(save, onWifiSave, LV_EVENT_CLICKED, NULL);
lv_obj_center(label(save, LV_SYMBOL_OK " Save", THEME_FONT_BODY, theme::TEXT));
stylePrimary(save);
primaryButton(body, LV_SYMBOL_OK " Save", onWifiSave);
_wifi_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
// Keyboard over the bottom of the screen; the body shrinks above it while
@@ -182,14 +174,8 @@ void UITask::wifiKeyboardHide() {
static void wifiRow(lv_obj_t* body) {
char sub[48];
wifiHint(sub, sizeof(sub));
lv_obj_t* sw = switchRow(body, LV_SYMBOL_WIFI " WiFi", sub, nullptr);
if (lvport::wifiAllowed()) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, onWifiAllowed, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_t* row = lv_obj_get_parent(sw);
rowPressable(row);
lv_obj_add_event_cb(row, [](lv_event_t* e) {
if (lv_event_get_target(e) == lv_event_get_current_target(e)) s_ui->showWifi(); // not the switch
}, LV_EVENT_CLICKED, NULL);
switchRowOpens(switchRow(body, LV_SYMBOL_WIFI " WiFi", sub, lvport::wifiAllowed(), onWifiAllowed),
[](lv_event_t* e) { (void)e; s_ui->showWifi(); });
}
void UITask::wifiSetAllowed(bool on) {
@@ -1614,6 +1614,7 @@ public:
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, content_y + step, "hibernating..."); // the panel's Hibernate row
}
if (_page == HomePage::MAP && _tile_menu.active) _tile_menu.render(display); // "GPS is off"
if (slide >= 0) { // page turn: the page left behind slides out, this one in
display.slideCompose(_slide_dir * (int)(display.width() * slide + 0.5f));
anim_ms = 15;
@@ -1634,6 +1635,14 @@ public:
}
bool handleInput(char c) override {
// Map page: "GPS is off" before a share (the tile menu, free on this page).
if (_page == HomePage::MAP && _tile_menu.active) {
if (_tile_menu.handleInput(c) == PopupMenu::SELECTED && _tile_menu.selectedIndex() == 0) {
_task->toggleGPS(false);
_task->showAlert("GPS on, share once it has a fix", 1500);
}
return true;
}
// Favourites grid claims joystick UP/DOWN and inner LEFT/RIGHT; LEFT at the
// left column and RIGHT at the right column fall through to page nav so the
// user can still leave the page sideways.
@@ -1759,6 +1768,12 @@ public:
return true;
}
if (c == KEY_CONTEXT_MENU && _page == HomePage::MAP) {
if (_task->hasGPS() && !_task->getGPSState()) { // its last fix may be stale: asked first
_tile_menu.begin("GPS is off", 2);
_tile_menu.addItem("Enable GPS");
_tile_menu.addItem("Cancel");
return true;
}
_task->quickShareMyLocation();
return true;
}
@@ -11,6 +11,7 @@
#include "InfoKit.h"
#include "../NodePrefs.h"
#include "icons.h" // drawList (shared scrolling-list helper)
#include "PopupMenu.h" // "GPS is off" before Auto share starts
class LiveShareScreen : public UIScreen {
UITask* _task;
@@ -18,6 +19,7 @@ class LiveShareScreen : public UIScreen {
bool _dirty = false;
int _sel = 0;
int _scroll = 0;
PopupMenu _gps_ask;
enum Kind : uint8_t { K_TRACK, K_AUTO, K_DURATION, K_TARGET, K_SCOPE, K_MOVE, K_GAP, K_HB };
struct Row { Kind kind; const char* label; };
@@ -39,7 +41,7 @@ class LiveShareScreen : public UIScreen {
public:
LiveShareScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {}
void onShow() override { _dirty = false; _sel = 0; _scroll = 0; }
void onShow() override { _dirty = false; _sel = 0; _scroll = 0; _gps_ask.active = false; }
// Resolve the configured target's display name (channel name or contact name).
void currentTargetName(char* buf, int n) {
@@ -112,6 +114,7 @@ public:
int mqr = info::listRow(display, y, r.label, val, sel, reserve);
if (mqr > 0) mq_delay = mqr;
});
if (_gps_ask.active) _gps_ask.render(display);
return (mq_delay > 0 && mq_delay < 500) ? mq_delay : 500;
}
@@ -124,8 +127,14 @@ public:
switch (rows(_sel).kind) {
case K_TRACK: _prefs->track_shared_loc ^= 1; _dirty = true; break;
case K_AUTO:
_prefs->loc_share_enabled ^= 1;
if (_prefs->loc_share_enabled) _task->restartLocShareSession();
// With the GPS off nothing would go out: asked first, as Trail does.
if (!_prefs->loc_share_enabled && _task->hasGPS() && !_task->getGPSState()) {
_gps_ask.begin("GPS is off", 2); // the action first, as Trail's: nothing is lost
_gps_ask.addItem("Enable GPS & share");
_gps_ask.addItem("Cancel");
return;
}
_task->core().live_share.setEnabled(!_prefs->loc_share_enabled);
_dirty = true; break;
case K_DURATION:
_prefs->loc_share_duration_idx = (uint8_t)((_prefs->loc_share_duration_idx + (dir >= 0 ? 1 : NodePrefs::LOC_SHARE_DURATION_COUNT - 1)) % NodePrefs::LOC_SHARE_DURATION_COUNT);
@@ -192,6 +201,15 @@ public:
}
bool handleInput(char c) override {
if (_gps_ask.active) {
if (_gps_ask.handleInput(c) == PopupMenu::SELECTED && _gps_ask.selectedIndex() == 0) {
_task->toggleGPS(false);
_task->core().live_share.setEnabled(true);
_dirty = true;
_task->showAlert("GPS on, sharing", 1200);
}
return true;
}
if (c == KEY_CANCEL) {
_task->savePrefsIfDirty(_dirty);
_task->leaveTool();
+2 -4
View File
@@ -14,8 +14,6 @@
namespace navview {
static const int AHEAD_DEG = 12; // within this of straight on: "ahead"
// own_valid : is there a usable GPS fix for the device right now
// cog_valid : is there a stable course-over-ground (UITask::currentCourse)
// eta : optional closing-speed/ETA tracker; when supplied a third line is
@@ -89,8 +87,8 @@ inline void draw(DisplayDriver& d,
}
}
int turn = 0; // -180..179 degrees to turn, right positive; with a course
if (cog_valid) turn = ((to_deg - cog_deg + 540) % 360) - 180;
const bool ahead = cog_valid && turn <= AHEAD_DEG && turn >= -AHEAD_DEG;
if (cog_valid) turn = geo::turnDeg(to_deg, cog_deg);
const bool ahead = cog_valid && geo::isAhead(turn);
// Distance — emphasised at size 2.
d.setTextSize(2);
@@ -861,7 +861,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
// row, unlabelled: then there are five rows either way.
const bool one_pos = top + step * 5 + display.getLineHeight() > bottom;
if (one_pos) {
snprintf(buf, sizeof(buf), "%.5f,%.5f", e.lat_e6 / 1e6, e.lon_e6 / 1e6);
geo::fmtLatLon(buf, sizeof(buf), e.lat_e6, e.lon_e6);
display.drawTextEllipsized(tx, top, w - 4, buf);
} else {
snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6);
@@ -964,7 +964,7 @@ public:
} else if (sel == 0) { // Type in
int32_t lat, lon;
char cur[32] = "";
if (_task->core().fixedPosition(lat, lon)) snprintf(cur, sizeof(cur), "%.5f, %.5f", lat / 1e6, lon / 1e6);
if (_task->core().fixedPosition(lat, lon)) geo::fmtLatLon(cur, sizeof(cur), lat, lon, true);
_edit_pos = true;
_kb->begin(cur, 31);
_kb->prompt = "Lat, lon";
+19 -18
View File
@@ -81,7 +81,7 @@ class TrailScreen : public UIScreen {
// correctly (settings rows cycle with LEFT/RIGHT; everything else is Enter).
// Live-share *config* lives in its own tool (Tools › Live Share); the map only
// keeps the one-shot "Share my pos" action.
enum MenuLevel { ML_MAIN, ML_FILE, ML_SETTINGS, ML_CONFIRM_GPS, ML_CONFIRM_RESET };
enum MenuLevel { ML_MAIN, ML_FILE, ML_SETTINGS, ML_CONFIRM_GPS, ML_CONFIRM_GPS_SHARE, ML_CONFIRM_RESET };
PopupMenu _action_menu;
uint8_t _menu_level = ML_MAIN;
uint8_t _act_map[16]; // max rows on any one level; pushAction guards the cap
@@ -179,6 +179,11 @@ public:
_menu_level = ML_MAIN; // popup already closed by handleInput()
return true;
}
if (_menu_level == ML_CONFIRM_GPS_SHARE) { // "Enable GPS": a fix takes a while, so no send yet
if (_action_menu.selectedIndex() == 0) { _task->toggleGPS(false); _task->showAlert("GPS on, share once it has a fix", 1500); }
_menu_level = ML_MAIN;
return true;
}
// Reset confirmation popup: same shape, but defaults to Cancel (row 1)
// since unlike the GPS prompt above, this one is destructive.
if (_menu_level == ML_CONFIRM_RESET) {
@@ -198,13 +203,16 @@ public:
case ACT_MARK_AVG:
case ACT_AUTOSAVE:
case ACT_AUTOPAUSE: return true;
case ACT_SHARE_NOW: shareMyLocationNow(); break;
case ACT_SHARE_NOW:
if (_task->hasGPS() && !_task->getGPSState()) { buildGpsConfirmMenu(true); return true; } // its last fix may be stale
shareMyLocationNow();
break;
case ACT_TOGGLE:
// Starting a trail with GPS switched off logs nothing and just
// sits on "Waiting for GPS fix" forever -- prompt to enable it
// first (only on boards that actually have a toggleable GPS).
if (!_store->isActive() && _task->hasGPS() && !_task->getGPSState()) {
buildGpsConfirmMenu();
buildGpsConfirmMenu(false);
return true;
}
handleToggle();
@@ -379,13 +387,14 @@ private:
pushAction(ACT_SETTINGS, "Settings...");
}
// Confirmation shown when "Start tracking" is chosen with GPS off. Rows are
// plain (not ActionIds); handled by _menu_level in the SELECTED branch.
void buildGpsConfirmMenu() {
_menu_level = ML_CONFIRM_GPS;
// Confirmation shown when "Start tracking" or "Share my pos" is chosen with
// GPS off. Rows are plain (not ActionIds); handled by _menu_level in the
// SELECTED branch.
void buildGpsConfirmMenu(bool share) {
_menu_level = share ? ML_CONFIRM_GPS_SHARE : ML_CONFIRM_GPS;
_act_count = 0;
_action_menu.begin("GPS is off", 2);
_action_menu.addItem("Enable GPS & start");
_action_menu.addItem(share ? "Enable GPS" : "Enable GPS & start");
_action_menu.addItem("Cancel");
}
@@ -471,16 +480,8 @@ private:
p->gps_avg_idx = idx;
}
// One-shot manual share: build "[LOC]lat,lon" and hand it to the Messages
// screen, where the user picks a DM or channel recipient. (Auto live-share
// config lives in Tools › Live Share.)
void shareMyLocationNow() {
int32_t lat, lon;
if (!_task->currentLocation(lat, lon)) { _task->showAlert("No GPS fix", 1000); return; }
char text[40];
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6);
_task->shareToMessage(text);
}
// One-shot manual share, as the home Map page's: UITask::quickShareMyLocation.
void shareMyLocationNow() { _task->quickShareMyLocation(); }
// Render the average speed (or pace) in the globally-selected unit system.
void formatAvgPaceOrSpeed(char* buf, size_t n, NodePrefs* p) const {
+1 -2
View File
@@ -1907,7 +1907,6 @@ bool UITask::currentCourse(int& deg_out) const { return _core->course.currentCou
bool UITask::currentLocation(int32_t& lat, int32_t& lon) const { return _core->course.currentLocation(lat, lon); }
bool UITask::sendLocationShare(int32_t lat, int32_t lon) { return _core->live_share.send(lat, lon); }
void UITask::restartLocShareSession() { _core->live_share.restartSession(); }
void UITask::restartLocShareClock() { _core->live_share.restartClock(); }
LiveTrackStore& UITask::liveTrack() { return _core->live_share.track(); }
@@ -1923,7 +1922,7 @@ void UITask::quickShareMyLocation() {
return;
}
char text[40];
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6);
geo::fmtLocShare(text, sizeof(text), lat, lon);
shareToMessage(text);
}
-1
View File
@@ -366,7 +366,6 @@ public:
void gotoLiveShareScreen();
// Live share session control (ui-core/LiveShareEngine.h): restart = new
// session (re-announce + fresh clock); clock-only = changed "Stop after".
void restartLocShareSession();
void restartLocShareClock();
void gotoLocatorScreen();
// Re-arm the locator state machine so the next evaluation initialises
@@ -388,11 +388,8 @@ public:
}
} else if (sel == 2) { // Send (share in a message)
if (wi >= 0 && wi < _task->waypoints().count()) {
const Waypoint& w = _task->waypoints().at(wi);
double lat = w.lat_1e6 / 1000000.0, lon = w.lon_1e6 / 1000000.0;
char text[80];
if (w.label[0]) snprintf(text, sizeof(text), WAYPOINT_MSG_TAG "%.5f,%.5f %s", lat, lon, w.label);
else snprintf(text, sizeof(text), WAYPOINT_MSG_TAG "%.5f,%.5f", lat, lon);
_task->core().waypoints.shareText(wi, text, sizeof(text));
_task->shareToMessage(text); // hands off to the Messages screen
}
} else { // Set as target
+27
View File
@@ -176,6 +176,33 @@ TEST(SosText, OnlyAnSosWithAPositionIsTakenForOne) {
EXPECT_FALSE(geo::parseSos("SOSNA 50.06143,19.93658", lat, lon));
}
// The [LOC] share every sender builds, and what the receiver reads back.
TEST(GeoText, LocationShareRoundTrips) {
char t[64];
geo::fmtLocShare(t, sizeof(t), 50061430, -19936580);
EXPECT_STREQ(t, "[LOC]50.06143,-19.93658");
geo::fmtLocShare(t, sizeof(t), 50061430, -19936580, "Hiker");
EXPECT_STREQ(t, "[LOC]50.06143,-19.93658 Hiker");
int32_t lat = 0, lon = 0;
ASSERT_TRUE(geo::parseLocShare(t, lat, lon));
EXPECT_EQ(lat, 50061430);
EXPECT_EQ(lon, -19936580);
geo::fmtLatLon(t, sizeof(t), 50061430, 19936580, true);
EXPECT_STREQ(t, "50.06143, 19.93658");
ASSERT_TRUE(geo::parseLatLon(t, lat, lon)); // an edited value reads back
EXPECT_EQ(lon, 19936580);
}
TEST(GeoText, TurnIsTheShortWayRound) {
EXPECT_EQ(geo::turnDeg(90, 0), 90);
EXPECT_EQ(geo::turnDeg(0, 90), -90);
EXPECT_EQ(geo::turnDeg(10, 350), 20);
EXPECT_EQ(geo::turnDeg(350, 10), -20);
EXPECT_EQ(geo::turnDeg(180, 0), -180);
EXPECT_TRUE(geo::isAhead(geo::turnDeg(5, 355)));
EXPECT_FALSE(geo::isAhead(geo::turnDeg(20, 0)));
}
TEST(SosText, NoGpsIsRecognisedOnItsOwn) {
char t[64];
SosEngine::compose(t, sizeof(t), SosEngine::NOWHERE, 0, 0, nullptr, -1, "14:32", "");