mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 10:51:57 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", "");
|
||||
|
||||
Reference in New Issue
Block a user