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
@@ -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) {