mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +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
|
battery; anything that needs your position (the trail, live share, the
|
||||||
locator) keeps it on.
|
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]
|
> [!NOTE]
|
||||||
> **Wio Tracker L2:** the Diagnostics app, with a **Noise** tab that
|
> **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
|
> 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];
|
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:
|
// Human distance string. Metric: "850m" / "2.3km" / "140km". Imperial:
|
||||||
// "850ft" / "2.3mi" / "140mi" (feet below ~1000 ft, then miles). `spaced`
|
// "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.
|
// 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).
|
// the tag (it just looks like a coordinate).
|
||||||
#define LOCATION_MSG_TAG "[LOC]"
|
#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
|
// 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
|
// 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.
|
// 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
|
// Start the session afresh (next tick treats it as a new enable: re-announce
|
||||||
// + new duration clock).
|
// + new duration clock).
|
||||||
void restartSession() { _was_enabled = false; }
|
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
|
// Restart only the session's duration clock -- unlike restartSession(), no
|
||||||
// re-announce. For a changed "Stop after" length, where position hasn't
|
// 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).
|
// 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) {
|
if (_prefs->loc_share_target_type == 0) {
|
||||||
// Channel: sendGroupMessage prepends "<name>: ", so the payload already
|
// Channel: sendGroupMessage prepends "<name>: ", so the payload already
|
||||||
// names the sender — keep the [LOC] text bare.
|
// 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;
|
ChannelDetails ch;
|
||||||
if (!the_mesh.getChannel(_prefs->loc_share_channel_idx, ch)) return false;
|
if (!the_mesh.getChannel(_prefs->loc_share_channel_idx, ch)) return false;
|
||||||
return the_mesh.sendGroupMessage(rtc_clock.getCurrentTime(), ch.channel,
|
return the_mesh.sendGroupMessage(rtc_clock.getCurrentTime(), ch.channel,
|
||||||
@@ -120,8 +126,7 @@ public:
|
|||||||
ContactInfo* c = the_mesh.lookupContactByPubKey(_prefs->loc_share_dm_prefix,
|
ContactInfo* c = the_mesh.lookupContactByPubKey(_prefs->loc_share_dm_prefix,
|
||||||
NodePrefs::FAVOURITE_PREFIX_LEN);
|
NodePrefs::FAVOURITE_PREFIX_LEN);
|
||||||
if (!c) return false;
|
if (!c) return false;
|
||||||
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f %s",
|
geo::fmtLocShare(text, sizeof(text), lat, lon, the_mesh.getNodeName());
|
||||||
lat / 1e6, lon / 1e6, the_mesh.getNodeName());
|
|
||||||
uint32_t expected_ack = 0, est_timeout = 0;
|
uint32_t expected_ack = 0, est_timeout = 0;
|
||||||
return the_mesh.sendMessage(*c, rtc_clock.getCurrentTime(), 0, text, expected_ack, 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 {
|
void shareText(int i, char* out, size_t n) const {
|
||||||
if (i < 0 || i >= _store.count()) { out[0] = '\0'; return; }
|
if (i < 0 || i >= _store.count()) { out[0] = '\0'; return; }
|
||||||
const Waypoint& w = _store.at(i);
|
const Waypoint& w = _store.at(i);
|
||||||
double lat = w.lat_1e6 / 1000000.0, lon = w.lon_1e6 / 1000000.0;
|
int o = snprintf(out, n, WAYPOINT_MSG_TAG);
|
||||||
if (w.label[0]) snprintf(out, n, WAYPOINT_MSG_TAG "%.5f,%.5f %s", lat, lon, w.label);
|
geo::fmtLatLon(out + o, (int)n - o, w.lat_1e6, w.lon_1e6);
|
||||||
else snprintf(out, n, WAYPOINT_MSG_TAG "%.5f,%.5f", lat, lon);
|
if (w.label[0]) { size_t l = strlen(out); snprintf(out + l, n - l, " %s", w.label); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -309,10 +309,7 @@ void UITask::adminValuePopup() {
|
|||||||
if (f->kind == admin::K_ONOFF) {
|
if (f->kind == admin::K_ONOFF) {
|
||||||
lv_obj_t* r = centerRow(panel);
|
lv_obj_t* r = centerRow(panel);
|
||||||
label(r, "Off", THEME_FONT_BODY, theme::TEXT_MUTED);
|
label(r, "Off", THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||||
lv_obj_t* sw = lv_switch_create(r);
|
switchCreate(r, S.value != 0, onAdminValue, (void*)(uintptr_t)V_TOGGLE);
|
||||||
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);
|
|
||||||
label(r, "On", THEME_FONT_BODY, theme::TEXT_MUTED);
|
label(r, "On", THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||||
} else if (f->kind == admin::K_NUMBER) {
|
} else if (f->kind == admin::K_NUMBER) {
|
||||||
lv_obj_t* r = centerRow(panel);
|
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_add_event_cb(b, onBotPlaceholder, LV_EVENT_CLICKED, NULL);
|
||||||
lv_obj_center(label(b, ph, THEME_FONT_SMALL, theme::TEXT));
|
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 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) {
|
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);
|
const botcfg::Row& r = botcfg::row(s_tab, i);
|
||||||
uint8_t* fl = botcfg::flag(p, r.kind);
|
uint8_t* fl = botcfg::flag(p, r.kind);
|
||||||
if (fl && !botcfg::isScope(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;
|
continue;
|
||||||
}
|
}
|
||||||
uint16_t* cmd_off = botcfg::cmdOff(p, r.kind);
|
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)) {
|
if (uint16_t* off = botcfg::cmdOff(p, r.kind)) {
|
||||||
lv_obj_t* g = group(scrollList(navPopupPanel("Allowed commands", true)), nullptr);
|
lv_obj_t* g = group(scrollList(navPopupPanel("Allowed commands", true)), nullptr);
|
||||||
for (int i = 0; i < botcmd::count(botcfg::actionsOn(p, r.kind)); i++)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -186,19 +186,9 @@ void UITask::buildChannelEdit() {
|
|||||||
s_secret = chanField(body, "Passphrase");
|
s_secret = chanField(body, "Passphrase");
|
||||||
lv_textarea_set_max_length(s_secret, 32);
|
lv_textarea_set_max_length(s_secret, 32);
|
||||||
|
|
||||||
lv_obj_t* row = settingRow(body, "Hex key", "32 characters");
|
s_hex_sw = switchRow(body, "Hex key", "32 characters", false, onChanHex);
|
||||||
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);
|
|
||||||
|
|
||||||
lv_obj_t* save = lv_button_create(body);
|
primaryButton(body, LV_SYMBOL_OK " Save", onChanSave);
|
||||||
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);
|
|
||||||
s_status = noteLabel(body, "", THEME_FONT_SMALL, theme::FAIL);
|
s_status = noteLabel(body, "", THEME_FONT_SMALL, theme::FAIL);
|
||||||
|
|
||||||
if (edit) {
|
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_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_column(on, 8, 0);
|
||||||
lv_obj_set_style_pad_bottom(on, 4, 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);
|
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);
|
lv_obj_add_event_cb(sw, onAlarmSwitch, LV_EVENT_VALUE_CHANGED, NULL);
|
||||||
label(col, "Repeat", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
label(col, "Repeat", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||||
char opts[48];
|
char opts[48];
|
||||||
|
|||||||
@@ -366,14 +366,8 @@ void UITask::buildDiag() {
|
|||||||
if (s_tab == TAB_NOISE) {
|
if (s_tab == TAB_NOISE) {
|
||||||
noteLabel(s_list, "What the radio hears with nothing on air, on the mesh frequency and 1.1 MHz "
|
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.");
|
"either side. Takes about 12 seconds.");
|
||||||
lv_obj_t* b = lv_button_create(s_list);
|
lv_obj_t* b = primaryButton(s_list, s_noise_have ? LV_SYMBOL_REFRESH " Measure again" : LV_SYMBOL_PLAY " Measure", onNoiseRun);
|
||||||
lv_obj_set_size(b, LV_PCT(100), 40);
|
s_noise_status = lv_obj_get_child(b, 0);
|
||||||
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);
|
|
||||||
if (s_noise_have) {
|
if (s_noise_have) {
|
||||||
float mesh_f = _prefs ? _prefs->freq : 0;
|
float mesh_f = _prefs ? _prefs->freq : 0;
|
||||||
lv_obj_t* card = infoCard(s_list);
|
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 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(); }
|
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
|
// 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).
|
// satellite at its azimuth (N up) and elevation (overhead in the centre).
|
||||||
static void onGpsSkyDraw(lv_event_t* e) {
|
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);
|
if (g->utc_valid) snprintf(utc, sizeof(utc), "%02u:%02u:%02u", g->utc_h, g->utc_m, g->utc_s);
|
||||||
int32_t lat, lon;
|
int32_t lat, lon;
|
||||||
if (fix && _core->course.currentLocation(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));
|
locator(lat / 1e6, lon / 1e6, grid, sizeof(grid));
|
||||||
}
|
}
|
||||||
if (fix && g->alt_valid) snprintf(alt, sizeof(alt), "%.0f m", g->alt_m);
|
if (fix && g->alt_valid) snprintf(alt, sizeof(alt), "%.0f m", g->alt_m);
|
||||||
|
|||||||
@@ -388,13 +388,7 @@ void UITask::mapAreasPopup() {
|
|||||||
} else {
|
} else {
|
||||||
noteLabel(list, "No areas yet. Maps downloaded before this list are still on the card.");
|
noteLabel(list, "No areas yet. Maps downloaded before this list are still on the card.");
|
||||||
}
|
}
|
||||||
lv_obj_t* add = lv_button_create(list);
|
primaryButton(list, LV_SYMBOL_PLUS " New area", onNewArea);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::mapAreaPopup(int idx) {
|
void UITask::mapAreaPopup(int idx) {
|
||||||
|
|||||||
@@ -1292,7 +1292,8 @@ void UITask::mapZoom(int delta) {
|
|||||||
|
|
||||||
void UITask::mapCenterOnMe() {
|
void UITask::mapCenterOnMe() {
|
||||||
int32_t lat, lon;
|
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_follow = true;
|
||||||
_map_cx = mapview::lonToTileX(lon / 1e6, _map_z);
|
_map_cx = mapview::lonToTileX(lon / 1e6, _map_z);
|
||||||
_map_cy = mapview::latToTileY(lat / 1e6, _map_z) + mapCenterBias();
|
_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 lv_obj_t* s_dial = nullptr;
|
||||||
static int32_t s_dial_ang = 0; // shown: degrees clockwise from the dial's top
|
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_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 DIAL = 38;
|
||||||
static const int AHEAD_DEG = 12;
|
|
||||||
static const uint32_t DIAL_MS = 300;
|
static const uint32_t DIAL_MS = 300;
|
||||||
|
|
||||||
static void setDialAng(void* o, int32_t v) {
|
static void setDialAng(void* o, int32_t v) {
|
||||||
@@ -159,7 +158,7 @@ static void setDial(int deg, bool course) {
|
|||||||
deg = ((deg % 360) + 360) % 360;
|
deg = ((deg % 360) + 360) % 360;
|
||||||
const bool snap = course != s_dial_course || lv_obj_has_flag(s_dial, LV_OBJ_FLAG_HIDDEN);
|
const bool snap = course != s_dial_course || lv_obj_has_flag(s_dial, LV_OBJ_FLAG_HIDDEN);
|
||||||
s_dial_course = course;
|
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_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_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);
|
lv_anim_delete(s_dial, setDialAng);
|
||||||
@@ -517,7 +516,7 @@ void UITask::refreshNavBar() {
|
|||||||
navmap::setDial(course ? to - cog : to, course);
|
navmap::setDial(course ? to - cog : to, course);
|
||||||
// Walking: how far to turn (the dial shows it); standing: where the target lies.
|
// Walking: how far to turn (the dial shows it); standing: where the target lies.
|
||||||
if (course) {
|
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");
|
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 snprintf(what, sizeof(what), "turn %s %d\xC2\xB0", turn > 0 ? "right" : "left", turn > 0 ? turn : -turn);
|
||||||
} else {
|
} 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) {
|
void UITask::placeCard(lv_obj_t* parent, int32_t lat, int32_t lon) {
|
||||||
lv_obj_t* card = infoCard(parent);
|
lv_obj_t* card = infoCard(parent);
|
||||||
char t[40];
|
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);
|
infoRow(card, "Coordinates", t);
|
||||||
int32_t mlat, mlon;
|
int32_t mlat, mlon;
|
||||||
if (!_core->course.currentLocation(mlat, mlon)) return;
|
if (!_core->course.currentLocation(mlat, mlon)) return;
|
||||||
@@ -831,7 +830,7 @@ void UITask::navAddWaypoint(int32_t lat, int32_t lon) {
|
|||||||
|
|
||||||
void UITask::navMarkHere() {
|
void UITask::navMarkHere() {
|
||||||
int32_t lat, lon;
|
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);
|
uint16_t secs = NodePrefs::gpsAvgSecs(_prefs ? _prefs->gps_avg_idx : 0);
|
||||||
if (secs == 0) { navAddWaypoint(lat, lon); return; }
|
if (secs == 0) { navAddWaypoint(lat, lon); return; }
|
||||||
if (_core->waypoints.full()) { showToast(waypointsFull()); 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.
|
// What the map shows, right under the trail: used more than what follows.
|
||||||
lv_obj_t* g = group(list, "LAYERS");
|
lv_obj_t* g = group(list, "LAYERS");
|
||||||
choiceRow(g, "Map", "Topo, light, dark or satellite", mapview::LAYER_OPTS, mapview::s_layer, onMapLayer);
|
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);
|
switchRow(g, "Hiking trails", "Marked routes in their colours", lvport::trailsOn(), onTrails);
|
||||||
if (lvport::trailsOn()) lv_obj_add_state(tsw, LV_STATE_CHECKED);
|
|
||||||
lv_obj_add_event_cb(tsw, onTrails, LV_EVENT_VALUE_CHANGED, NULL);
|
|
||||||
schemaRow(g, SETTING(flow_show));
|
schemaRow(g, SETTING(flow_show));
|
||||||
#ifdef MAP_VECTOR // unfinished (LvglPort.h vectorOn)
|
#ifdef MAP_VECTOR // unfinished (LvglPort.h vectorOn)
|
||||||
lv_obj_t* vsw = switchRow(g, "Vector map (test)", "Drawn on the device: trails, contours", nullptr);
|
switchRow(g, "Vector map (test)", "Drawn on the device: trails, contours", lvport::vectorOn(), onVectorMap);
|
||||||
if (lvport::vectorOn()) lv_obj_add_state(vsw, LV_STATE_CHECKED);
|
|
||||||
lv_obj_add_event_cb(vsw, onVectorMap, LV_EVENT_VALUE_CHANGED, NULL);
|
|
||||||
char vr[48];
|
char vr[48];
|
||||||
int nv = mapview::s_vpacks.count();
|
int nv = mapview::s_vpacks.count();
|
||||||
snprintf(vr, sizeof(vr), nv ? "%d on the card" : "None - packs go in /vmap", nv);
|
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();
|
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);
|
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);
|
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);
|
switchRow(g, "Live tiles", "Load missing tiles over WiFi", lvport::liveTiles(), onLiveTiles);
|
||||||
if (lvport::liveTiles()) lv_obj_add_state(lsw, LV_STATE_CHECKED);
|
|
||||||
lv_obj_add_event_cb(lsw, onLiveTiles, LV_EVENT_VALUE_CHANGED, NULL);
|
|
||||||
|
|
||||||
|
|
||||||
refreshNavTools();
|
refreshNavTools();
|
||||||
@@ -1360,10 +1353,11 @@ void UITask::navToolAction(uint8_t act) {
|
|||||||
switch (act) {
|
switch (act) {
|
||||||
case navmap::TL_TRAIL_TOGGLE:
|
case navmap::TL_TRAIL_TOGGLE:
|
||||||
if (!tr.isActive()) {
|
if (!tr.isActive()) {
|
||||||
|
if (gpsAsked(GA_TRAIL)) return; // its popup in place of this one
|
||||||
tr.setActive(true);
|
tr.setActive(true);
|
||||||
int32_t lat, lon;
|
int32_t lat, lon;
|
||||||
if (_core->course.currentLocation(lat, lon)) showToast("Recording the trail");
|
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 {
|
} else {
|
||||||
tr.setActive(false);
|
tr.setActive(false);
|
||||||
showToast("Trail stopped");
|
showToast("Trail stopped");
|
||||||
@@ -1402,11 +1396,12 @@ void UITask::navToolAction(uint8_t act) {
|
|||||||
break;
|
break;
|
||||||
case navmap::TL_TRACKBACK: {
|
case navmap::TL_TRACKBACK: {
|
||||||
if (navmap::s_tb.active()) { navmap::s_tb.stop(); showToast("Track back stopped"); break; }
|
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;
|
int32_t lat = 0, lon = 0;
|
||||||
bool fix = _core->course.currentLocation(lat, lon);
|
bool fix = _core->course.currentLocation(lat, lon);
|
||||||
if (!navmap::s_tb.start(tr.store(), fix, lat, lon)) { showToast("No trail to walk back"); break; }
|
if (!navmap::s_tb.start(tr.store(), fix, lat, lon)) { showToast("No trail to walk back"); break; }
|
||||||
navmap::s_eta.reset();
|
navmap::s_eta.reset();
|
||||||
if (!fix) ensureGps();
|
if (!fix) ensureGps(GA_TRACKBACK); // says it's waiting
|
||||||
navClosePopup();
|
navClosePopup();
|
||||||
navFrameTarget();
|
navFrameTarget();
|
||||||
refreshNavBar();
|
refreshNavBar();
|
||||||
@@ -1421,24 +1416,24 @@ void UITask::navToolAction(uint8_t act) {
|
|||||||
}
|
}
|
||||||
case navmap::TL_SHARE_TOGGLE:
|
case navmap::TL_SHARE_TOGGLE:
|
||||||
if (_prefs->loc_share_enabled) {
|
if (_prefs->loc_share_enabled) {
|
||||||
_prefs->loc_share_enabled = 0;
|
_core->live_share.setEnabled(false);
|
||||||
showToast("Live share stopped");
|
showToast("Live share stopped");
|
||||||
} else {
|
} else {
|
||||||
_prefs->loc_share_enabled = 1;
|
if (gpsAsked(GA_SHARE)) return;
|
||||||
_core->live_share.restartSession();
|
_core->live_share.setEnabled(true);
|
||||||
int32_t lat, lon;
|
int32_t lat, lon;
|
||||||
if (_core->course.currentLocation(lat, lon)) showToast("Sharing your position");
|
if (_core->course.currentLocation(lat, lon)) showToast("Sharing your position");
|
||||||
else ensureGps();
|
else ensureGps(GA_SHARE); // says it's waiting
|
||||||
}
|
}
|
||||||
prefsSave();
|
prefsSave();
|
||||||
break;
|
break;
|
||||||
case navmap::TL_SHARE_ONCE: {
|
case navmap::TL_SHARE_ONCE: {
|
||||||
int32_t lat, lon;
|
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.
|
// 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; }
|
if (_prefs->loc_share_enabled && _core->live_share.send(lat, lon)) { showToast("Position sent"); break; }
|
||||||
char text[40];
|
char text[40];
|
||||||
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6);
|
geo::fmtLocShare(text, sizeof(text), lat, lon);
|
||||||
navClosePopup();
|
navClosePopup();
|
||||||
shareToMessage(text);
|
shareToMessage(text);
|
||||||
return;
|
return;
|
||||||
@@ -1520,7 +1515,7 @@ void UITask::navTargetsPopup() {
|
|||||||
const Waypoint& w = wp.at(i);
|
const Waypoint& w = wp.at(i);
|
||||||
char title[WAYPOINT_LABEL_LEN + 8], sub[40], d[12];
|
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(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* 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_t* more = lv_button_create(row); // the waypoint menu
|
||||||
lv_obj_set_size(more, 40, 34);
|
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
|
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.
|
// One of the node's "Can ask for" switches; user data = its TELEM_PERM_* bit.
|
||||||
static void onNodeTelemSwitch(lv_event_t* e) {
|
static void onNodeTelemSwitch(lv_event_t* e) {
|
||||||
ContactInfo* ci = the_mesh.lookupContactByPubKey(s_node.pub_key, PUB_KEY_SIZE);
|
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);
|
uint8_t perms = contactctl::telemetry(*ci);
|
||||||
for (const auto& t : T) {
|
for (const auto& t : T) {
|
||||||
lv_obj_t* sw = switchRow(g, t.text, t.sub, nullptr);
|
switchRow(g, t.text, t.sub, perms & t.bit, onNodeTelemSwitch, (void*)(uintptr_t)t.bit);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +404,7 @@ void UITask::refreshNode() {
|
|||||||
bearing = az;
|
bearing = az;
|
||||||
snprintf(dist, sizeof(dist), "%s %d\xC2\xB0 %s", d, az, geo::bearingCardinal(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) {
|
} else if (!_node_from_scan) {
|
||||||
snprintf(pos, sizeof(pos), "Not shared");
|
snprintf(pos, sizeof(pos), "Not shared");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,15 +198,8 @@ void UITask::buildOta() {
|
|||||||
lv_bar_set_range(_ota_bar, 0, 1000);
|
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::SURFACE), LV_PART_MAIN);
|
||||||
lv_obj_set_style_bg_color(_ota_bar, lv_color_hex(theme::ACCENT), LV_PART_INDICATOR);
|
lv_obj_set_style_bg_color(_ota_bar, lv_color_hex(theme::ACCENT), LV_PART_INDICATOR);
|
||||||
lv_obj_t* b = lv_button_create(body);
|
_ota_btn = primaryButton(body, "", onOtaButton);
|
||||||
lv_obj_set_size(b, LV_PCT(100), 40);
|
_ota_btn_lbl = lv_obj_get_child(_ota_btn, 0);
|
||||||
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);
|
|
||||||
groupNote(body, "From github.com/" OTA_REPO " releases, over WiFi. About a minute; keep the device "
|
groupNote(body, "From github.com/" OTA_REPO " releases, over WiFi. About a minute; keep the device "
|
||||||
"charged. Settings, contacts and messages are kept.");
|
"charged. Settings, contacts and messages are kept.");
|
||||||
refreshOta();
|
refreshOta();
|
||||||
|
|||||||
@@ -371,15 +371,18 @@ void UITask::quickPanelTile(int t, bool hold) {
|
|||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
case T_TRAIL:
|
case T_TRAIL:
|
||||||
_core->trail.setActive(!_core->trail.isActive());
|
case T_SHARE: {
|
||||||
if (_core->trail.isActive()) ensureGps(); // turns GPS on, or says it's waiting
|
|
||||||
break;
|
|
||||||
case T_SHARE:
|
|
||||||
if (!_prefs) break;
|
if (!_prefs) break;
|
||||||
_prefs->loc_share_enabled = !_prefs->loc_share_enabled;
|
const bool on = t == T_TRAIL ? _core->trail.isActive() : _prefs->loc_share_enabled;
|
||||||
if (_prefs->loc_share_enabled) { _core->live_share.restartSession(); ensureGps(); }
|
if (!on && _core->gpsAvailable() && !_core->gpsEnabled()) { // "GPS is off", over the screen
|
||||||
prefsSave();
|
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;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
refreshStatusBar();
|
refreshStatusBar();
|
||||||
quickPanelRefresh();
|
quickPanelRefresh();
|
||||||
|
|||||||
@@ -202,15 +202,11 @@ void UITask::advertSend(bool flood) {
|
|||||||
static void bluetoothRow(lv_obj_t* body) {
|
static void bluetoothRow(lv_obj_t* body) {
|
||||||
char sub[48];
|
char sub[48];
|
||||||
qview::bluetoothHint(sub, sizeof(sub));
|
qview::bluetoothHint(sub, sizeof(sub));
|
||||||
lv_obj_t* sw = switchRow(body, LV_SYMBOL_BLUETOOTH " Bluetooth", sub, nullptr);
|
lv_obj_t* sw = switchRow(body, LV_SYMBOL_BLUETOOTH " Bluetooth", sub, s_ui->isBluetoothEnabled(), qview::onBtSwitch);
|
||||||
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);
|
|
||||||
#ifdef BLE_PIN_CODE
|
#ifdef BLE_PIN_CODE
|
||||||
lv_obj_t* row = lv_obj_get_parent(sw);
|
switchRowOpens(sw, onBlePin);
|
||||||
rowPressable(row);
|
#else
|
||||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
(void)sw;
|
||||||
if (lv_event_get_target(e) == lv_event_get_current_target(e)) onBlePin(e); // not the switch
|
|
||||||
}, LV_EVENT_CLICKED, NULL);
|
|
||||||
#endif
|
#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;
|
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,
|
rowDropdown(settingRow(g, "TX power", p->tx_apc ? "Ceiling for auto power" : nullptr), s_opts, tx - TX_MIN, 0,
|
||||||
onRadioDropdown, R_TX);
|
onRadioDropdown, R_TX);
|
||||||
lv_obj_t* sw = switchRow(g, "Auto power", p->client_repeat ? "Off while repeating" : "Lowers power on good links", nullptr);
|
lv_obj_t* sw = switchRow(g, "Auto power", p->client_repeat ? "Off while repeating" : "Lowers power on good links",
|
||||||
if (p->tx_apc) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
p->tx_apc, onRadioSwitch, (void*)(uintptr_t)R_APC);
|
||||||
if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_DISABLED);
|
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) {
|
void UITask::radioSet(int which, int v) {
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ void UITask::buildRepeater() {
|
|||||||
NodePrefs* p = _prefs;
|
NodePrefs* p = _prefs;
|
||||||
|
|
||||||
lv_obj_t* g = group(body, nullptr);
|
lv_obj_t* g = group(body, nullptr);
|
||||||
lv_obj_t* sw = switchRow(g, "Repeater", p->client_repeat ? "Relaying for others" : "Relay others' messages", nullptr);
|
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);
|
||||||
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);
|
|
||||||
s_on_sub = rowSub(lv_obj_get_parent(sw));
|
s_on_sub = rowSub(lv_obj_get_parent(sw));
|
||||||
groupNote(body, "Uses more battery; auto power pauses.");
|
groupNote(body, "Uses more battery; auto power pauses.");
|
||||||
fill(&UITask::repeaterGroup, 1, 3); // the rest as the loop goes
|
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));
|
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) {
|
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));
|
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) ─────────────────────────
|
// ── Schema-driven settings (ui-core/SettingsSchema.h) ─────────────────────────
|
||||||
|
|
||||||
// Settings > Display: ui-new's Message alert as a switch -- the banner, or
|
// 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));
|
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) {
|
void UITask::msgBannerRow(lv_obj_t* card) {
|
||||||
lv_obj_t* sw = switchRow(card, "Message banner", "A new message drops in from the top", nullptr);
|
switchRow(card, "Message banner", "A new message drops in from the top", !_prefs || _prefs->msg_alert != NodePrefs::MSG_ALERT_OFF, onMsgBannerSwitch);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
void UITask::setMsgBanner(bool on) {
|
void UITask::setMsgBanner(bool on) {
|
||||||
if (!_prefs) return;
|
if (!_prefs) return;
|
||||||
@@ -161,9 +139,7 @@ void UITask::buildSchemaSettings() {
|
|||||||
schemaRow(g, SETTING(display_brightness));
|
schemaRow(g, SETTING(display_brightness));
|
||||||
schemaRow(g, SETTING(auto_off_secs));
|
schemaRow(g, SETTING(auto_off_secs));
|
||||||
schemaRow(g, SETTING(msg_wake));
|
schemaRow(g, SETTING(msg_wake));
|
||||||
lv_obj_t* tw = switchRow(g, "Tap to wake", "Off: only the top button wakes it", nullptr); // in NVS
|
switchRow(g, "Tap to wake", "Off: only the top button wakes it", _tap_wake, onTapWake);
|
||||||
if (_tap_wake) lv_obj_add_state(tw, LV_STATE_CHECKED);
|
|
||||||
lv_obj_add_event_cb(tw, onTapWake, LV_EVENT_VALUE_CHANGED, NULL);
|
|
||||||
g = group(body, "LOCK");
|
g = group(body, "LOCK");
|
||||||
schemaRow(g, SETTING(auto_lock));
|
schemaRow(g, SETTING(auto_lock));
|
||||||
listRow(g, "Screen PIN", pinSet() ? "On - asked when the screen wakes" : "Off", onPinSetup, NULL);
|
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) {
|
void UITask::ownPositionText(char* sub, size_t n) {
|
||||||
int32_t lat, lon;
|
int32_t lat, lon;
|
||||||
if (_core->gpsEnabled()) snprintf(sub, n, "From the GPS while it's on");
|
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");
|
else snprintf(sub, n, "Not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +206,7 @@ void UITask::ownPositionPick(int code) {
|
|||||||
if (code == OWNPOS_TYPE) {
|
if (code == OWNPOS_TYPE) {
|
||||||
int32_t lat, lon;
|
int32_t lat, lon;
|
||||||
char cur[32] = "";
|
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 };
|
TextEntry t = { "Fixed position", onOwnPosKb };
|
||||||
t.text = cur;
|
t.text = cur;
|
||||||
t.hint = "50.06143, 19.93658";
|
t.hint = "50.06143, 19.93658";
|
||||||
@@ -246,7 +222,7 @@ void UITask::ownPositionPick(int code) {
|
|||||||
const Waypoint& w = wp.at(i);
|
const Waypoint& w = wp.at(i);
|
||||||
char title[WAYPOINT_LABEL_LEN + 8], sub[40];
|
char title[WAYPOINT_LABEL_LEN + 8], sub[40];
|
||||||
snprintf(title, sizeof(title), UI_SYMBOL_FLAG " %s", w.label[0] ? w.label : "(unnamed)");
|
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));
|
listRow(list, title, sub, onOwnPosPick, (void*)(uintptr_t)(OWNPOS_WP0 + i));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -488,9 +464,7 @@ void UITask::schemaRow(lv_obj_t* card, int i) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!st.option) {
|
if (!st.option) {
|
||||||
lv_obj_t* sw = switchRow(card, st.label, st.hint, nullptr);
|
switchRow(card, st.label, st.hint, v, onSchemaSwitch, (void*)(uintptr_t)i);
|
||||||
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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The longest list, the time zone, is 27 options and 167 bytes: at 160
|
// 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");
|
g = group(body, "CONNECTIONS");
|
||||||
bluetoothRow(g);
|
bluetoothRow(g);
|
||||||
wifiRow(g);
|
wifiRow(g);
|
||||||
if (_core->gpsAvailable()) {
|
if (_core->gpsAvailable())
|
||||||
lv_obj_t* sw = switchRow(g, LV_SYMBOL_GPS " GPS", "Tap for satellites and options", nullptr);
|
switchRowOpens(switchRow(g, LV_SYMBOL_GPS " GPS", "Tap for satellites and options", _core->gpsEnabled(), onGpsSwitch),
|
||||||
if (_core->gpsEnabled()) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
onOpenGpsFromSettings);
|
||||||
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);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ void UITask::sosRow(int row) {
|
|||||||
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||||
ChannelDetails ch;
|
ChannelDetails ch;
|
||||||
if (!the_mesh.getChannel(i, ch) || !ch.name[0]) continue;
|
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++;
|
n++;
|
||||||
}
|
}
|
||||||
if (n == 0) label(list, "No channels.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
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(); }
|
void UITask::botSetGPS(bool on) { if (_core->setGpsEnabled(on)) refreshStatusBar(); }
|
||||||
|
|
||||||
bool UITask::ensureGps() {
|
bool UITask::ensureGps(GpsAsk what) {
|
||||||
int32_t lat, lon;
|
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;
|
if (_core->course.currentLocation(lat, lon)) return true;
|
||||||
showToast("Waiting for a GPS fix");
|
showToast("Waiting for a GPS fix");
|
||||||
return false;
|
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, "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, "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 (!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 (!strncmp(name, "compose@", 8)) { s_ui->simCompose(name + 8); return; }
|
||||||
if (!strcmp(name, "dm")) { s_ui->simDm(); return; }
|
if (!strcmp(name, "dm")) { s_ui->simDm(); return; }
|
||||||
if (!strcmp(name, "send")) { s_ui->simSend(); 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 pinPopup(bool channel, uint8_t ch_idx, const uint8_t* pub_key);
|
||||||
void pinTo(int slot);
|
void pinTo(int slot);
|
||||||
void setGps(bool on);
|
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).
|
// Kept for main.cpp's sim hooks (ui-new API).
|
||||||
void openContactDM(const ContactInfo& ci) { openDM(ci.id.pub_key); }
|
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);
|
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
|
// 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.
|
// (Theme.h), or back to the surface with TEXT. Every label under it follows.
|
||||||
static void textUnder(lv_obj_t* o, uint32_t col) {
|
static void textUnder(lv_obj_t* o, uint32_t col) {
|
||||||
@@ -1256,6 +1269,52 @@ static void prefsFlush() {
|
|||||||
the_mesh.savePrefs();
|
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
|
// 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
|
// 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
|
// 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
|
{ // the same switch as Settings' row
|
||||||
char sub[48];
|
char sub[48];
|
||||||
wifiHint(sub, sizeof(sub));
|
wifiHint(sub, sizeof(sub));
|
||||||
lv_obj_t* sw = switchRow(group(body, nullptr), LV_SYMBOL_WIFI " WiFi", sub, nullptr);
|
switchRow(group(body, nullptr), LV_SYMBOL_WIFI " WiFi", sub, lvport::wifiAllowed(), onWifiAllowed);
|
||||||
if (lvport::wifiAllowed()) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
|
||||||
lv_obj_add_event_cb(sw, onWifiAllowed, LV_EVENT_VALUE_CHANGED, NULL);
|
|
||||||
groupNote(body, "For map downloads, live map tiles and updates.");
|
groupNote(body, "For map downloads, live map tiles and updates.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,13 +127,7 @@ void UITask::buildWifi() {
|
|||||||
|
|
||||||
_wifi_pass = wifiField(body, "Password", true);
|
_wifi_pass = wifiField(body, "Password", true);
|
||||||
|
|
||||||
lv_obj_t* save = lv_button_create(body);
|
primaryButton(body, LV_SYMBOL_OK " Save", onWifiSave);
|
||||||
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);
|
|
||||||
_wifi_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
_wifi_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||||
|
|
||||||
// Keyboard over the bottom of the screen; the body shrinks above it while
|
// 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) {
|
static void wifiRow(lv_obj_t* body) {
|
||||||
char sub[48];
|
char sub[48];
|
||||||
wifiHint(sub, sizeof(sub));
|
wifiHint(sub, sizeof(sub));
|
||||||
lv_obj_t* sw = switchRow(body, LV_SYMBOL_WIFI " WiFi", sub, nullptr);
|
switchRowOpens(switchRow(body, LV_SYMBOL_WIFI " WiFi", sub, lvport::wifiAllowed(), onWifiAllowed),
|
||||||
if (lvport::wifiAllowed()) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
[](lv_event_t* e) { (void)e; s_ui->showWifi(); });
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::wifiSetAllowed(bool on) {
|
void UITask::wifiSetAllowed(bool on) {
|
||||||
|
|||||||
@@ -1614,6 +1614,7 @@ public:
|
|||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
display.drawTextCentered(display.width() / 2, content_y + step, "hibernating..."); // the panel's Hibernate row
|
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
|
if (slide >= 0) { // page turn: the page left behind slides out, this one in
|
||||||
display.slideCompose(_slide_dir * (int)(display.width() * slide + 0.5f));
|
display.slideCompose(_slide_dir * (int)(display.width() * slide + 0.5f));
|
||||||
anim_ms = 15;
|
anim_ms = 15;
|
||||||
@@ -1634,6 +1635,14 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool handleInput(char c) override {
|
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
|
// 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
|
// left column and RIGHT at the right column fall through to page nav so the
|
||||||
// user can still leave the page sideways.
|
// user can still leave the page sideways.
|
||||||
@@ -1759,6 +1768,12 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (c == KEY_CONTEXT_MENU && _page == HomePage::MAP) {
|
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();
|
_task->quickShareMyLocation();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "InfoKit.h"
|
#include "InfoKit.h"
|
||||||
#include "../NodePrefs.h"
|
#include "../NodePrefs.h"
|
||||||
#include "icons.h" // drawList (shared scrolling-list helper)
|
#include "icons.h" // drawList (shared scrolling-list helper)
|
||||||
|
#include "PopupMenu.h" // "GPS is off" before Auto share starts
|
||||||
|
|
||||||
class LiveShareScreen : public UIScreen {
|
class LiveShareScreen : public UIScreen {
|
||||||
UITask* _task;
|
UITask* _task;
|
||||||
@@ -18,6 +19,7 @@ class LiveShareScreen : public UIScreen {
|
|||||||
bool _dirty = false;
|
bool _dirty = false;
|
||||||
int _sel = 0;
|
int _sel = 0;
|
||||||
int _scroll = 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 };
|
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; };
|
struct Row { Kind kind; const char* label; };
|
||||||
@@ -39,7 +41,7 @@ class LiveShareScreen : public UIScreen {
|
|||||||
public:
|
public:
|
||||||
LiveShareScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {}
|
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).
|
// Resolve the configured target's display name (channel name or contact name).
|
||||||
void currentTargetName(char* buf, int n) {
|
void currentTargetName(char* buf, int n) {
|
||||||
@@ -112,6 +114,7 @@ public:
|
|||||||
int mqr = info::listRow(display, y, r.label, val, sel, reserve);
|
int mqr = info::listRow(display, y, r.label, val, sel, reserve);
|
||||||
if (mqr > 0) mq_delay = mqr;
|
if (mqr > 0) mq_delay = mqr;
|
||||||
});
|
});
|
||||||
|
if (_gps_ask.active) _gps_ask.render(display);
|
||||||
return (mq_delay > 0 && mq_delay < 500) ? mq_delay : 500;
|
return (mq_delay > 0 && mq_delay < 500) ? mq_delay : 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +127,14 @@ public:
|
|||||||
switch (rows(_sel).kind) {
|
switch (rows(_sel).kind) {
|
||||||
case K_TRACK: _prefs->track_shared_loc ^= 1; _dirty = true; break;
|
case K_TRACK: _prefs->track_shared_loc ^= 1; _dirty = true; break;
|
||||||
case K_AUTO:
|
case K_AUTO:
|
||||||
_prefs->loc_share_enabled ^= 1;
|
// With the GPS off nothing would go out: asked first, as Trail does.
|
||||||
if (_prefs->loc_share_enabled) _task->restartLocShareSession();
|
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;
|
_dirty = true; break;
|
||||||
case K_DURATION:
|
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);
|
_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 {
|
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) {
|
if (c == KEY_CANCEL) {
|
||||||
_task->savePrefsIfDirty(_dirty);
|
_task->savePrefsIfDirty(_dirty);
|
||||||
_task->leaveTool();
|
_task->leaveTool();
|
||||||
|
|||||||
@@ -14,8 +14,6 @@
|
|||||||
|
|
||||||
namespace navview {
|
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
|
// own_valid : is there a usable GPS fix for the device right now
|
||||||
// cog_valid : is there a stable course-over-ground (UITask::currentCourse)
|
// cog_valid : is there a stable course-over-ground (UITask::currentCourse)
|
||||||
// eta : optional closing-speed/ETA tracker; when supplied a third line is
|
// 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
|
int turn = 0; // -180..179 degrees to turn, right positive; with a course
|
||||||
if (cog_valid) turn = ((to_deg - cog_deg + 540) % 360) - 180;
|
if (cog_valid) turn = geo::turnDeg(to_deg, cog_deg);
|
||||||
const bool ahead = cog_valid && turn <= AHEAD_DEG && turn >= -AHEAD_DEG;
|
const bool ahead = cog_valid && geo::isAhead(turn);
|
||||||
|
|
||||||
// Distance — emphasised at size 2.
|
// Distance — emphasised at size 2.
|
||||||
d.setTextSize(2);
|
d.setTextSize(2);
|
||||||
|
|||||||
@@ -861,7 +861,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
|||||||
// row, unlabelled: then there are five rows either way.
|
// row, unlabelled: then there are five rows either way.
|
||||||
const bool one_pos = top + step * 5 + display.getLineHeight() > bottom;
|
const bool one_pos = top + step * 5 + display.getLineHeight() > bottom;
|
||||||
if (one_pos) {
|
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);
|
display.drawTextEllipsized(tx, top, w - 4, buf);
|
||||||
} else {
|
} else {
|
||||||
snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6);
|
snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6);
|
||||||
|
|||||||
@@ -964,7 +964,7 @@ public:
|
|||||||
} else if (sel == 0) { // Type in
|
} else if (sel == 0) { // Type in
|
||||||
int32_t lat, lon;
|
int32_t lat, lon;
|
||||||
char cur[32] = "";
|
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;
|
_edit_pos = true;
|
||||||
_kb->begin(cur, 31);
|
_kb->begin(cur, 31);
|
||||||
_kb->prompt = "Lat, lon";
|
_kb->prompt = "Lat, lon";
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class TrailScreen : public UIScreen {
|
|||||||
// correctly (settings rows cycle with LEFT/RIGHT; everything else is Enter).
|
// 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
|
// Live-share *config* lives in its own tool (Tools › Live Share); the map only
|
||||||
// keeps the one-shot "Share my pos" action.
|
// 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;
|
PopupMenu _action_menu;
|
||||||
uint8_t _menu_level = ML_MAIN;
|
uint8_t _menu_level = ML_MAIN;
|
||||||
uint8_t _act_map[16]; // max rows on any one level; pushAction guards the cap
|
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()
|
_menu_level = ML_MAIN; // popup already closed by handleInput()
|
||||||
return true;
|
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)
|
// Reset confirmation popup: same shape, but defaults to Cancel (row 1)
|
||||||
// since unlike the GPS prompt above, this one is destructive.
|
// since unlike the GPS prompt above, this one is destructive.
|
||||||
if (_menu_level == ML_CONFIRM_RESET) {
|
if (_menu_level == ML_CONFIRM_RESET) {
|
||||||
@@ -198,13 +203,16 @@ public:
|
|||||||
case ACT_MARK_AVG:
|
case ACT_MARK_AVG:
|
||||||
case ACT_AUTOSAVE:
|
case ACT_AUTOSAVE:
|
||||||
case ACT_AUTOPAUSE: return true;
|
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:
|
case ACT_TOGGLE:
|
||||||
// Starting a trail with GPS switched off logs nothing and just
|
// Starting a trail with GPS switched off logs nothing and just
|
||||||
// sits on "Waiting for GPS fix" forever -- prompt to enable it
|
// sits on "Waiting for GPS fix" forever -- prompt to enable it
|
||||||
// first (only on boards that actually have a toggleable GPS).
|
// first (only on boards that actually have a toggleable GPS).
|
||||||
if (!_store->isActive() && _task->hasGPS() && !_task->getGPSState()) {
|
if (!_store->isActive() && _task->hasGPS() && !_task->getGPSState()) {
|
||||||
buildGpsConfirmMenu();
|
buildGpsConfirmMenu(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
handleToggle();
|
handleToggle();
|
||||||
@@ -379,13 +387,14 @@ private:
|
|||||||
pushAction(ACT_SETTINGS, "Settings...");
|
pushAction(ACT_SETTINGS, "Settings...");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirmation shown when "Start tracking" is chosen with GPS off. Rows are
|
// Confirmation shown when "Start tracking" or "Share my pos" is chosen with
|
||||||
// plain (not ActionIds); handled by _menu_level in the SELECTED branch.
|
// GPS off. Rows are plain (not ActionIds); handled by _menu_level in the
|
||||||
void buildGpsConfirmMenu() {
|
// SELECTED branch.
|
||||||
_menu_level = ML_CONFIRM_GPS;
|
void buildGpsConfirmMenu(bool share) {
|
||||||
|
_menu_level = share ? ML_CONFIRM_GPS_SHARE : ML_CONFIRM_GPS;
|
||||||
_act_count = 0;
|
_act_count = 0;
|
||||||
_action_menu.begin("GPS is off", 2);
|
_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");
|
_action_menu.addItem("Cancel");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,16 +480,8 @@ private:
|
|||||||
p->gps_avg_idx = idx;
|
p->gps_avg_idx = idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One-shot manual share: build "[LOC]lat,lon" and hand it to the Messages
|
// One-shot manual share, as the home Map page's: UITask::quickShareMyLocation.
|
||||||
// screen, where the user picks a DM or channel recipient. (Auto live-share
|
void shareMyLocationNow() { _task->quickShareMyLocation(); }
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the average speed (or pace) in the globally-selected unit system.
|
// Render the average speed (or pace) in the globally-selected unit system.
|
||||||
void formatAvgPaceOrSpeed(char* buf, size_t n, NodePrefs* p) const {
|
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::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); }
|
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(); }
|
void UITask::restartLocShareClock() { _core->live_share.restartClock(); }
|
||||||
LiveTrackStore& UITask::liveTrack() { return _core->live_share.track(); }
|
LiveTrackStore& UITask::liveTrack() { return _core->live_share.track(); }
|
||||||
|
|
||||||
@@ -1923,7 +1922,7 @@ void UITask::quickShareMyLocation() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
char text[40];
|
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);
|
shareToMessage(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -366,7 +366,6 @@ public:
|
|||||||
void gotoLiveShareScreen();
|
void gotoLiveShareScreen();
|
||||||
// Live share session control (ui-core/LiveShareEngine.h): restart = new
|
// Live share session control (ui-core/LiveShareEngine.h): restart = new
|
||||||
// session (re-announce + fresh clock); clock-only = changed "Stop after".
|
// session (re-announce + fresh clock); clock-only = changed "Stop after".
|
||||||
void restartLocShareSession();
|
|
||||||
void restartLocShareClock();
|
void restartLocShareClock();
|
||||||
void gotoLocatorScreen();
|
void gotoLocatorScreen();
|
||||||
// Re-arm the locator state machine so the next evaluation initialises
|
// 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)
|
} else if (sel == 2) { // Send (share in a message)
|
||||||
if (wi >= 0 && wi < _task->waypoints().count()) {
|
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];
|
char text[80];
|
||||||
if (w.label[0]) snprintf(text, sizeof(text), WAYPOINT_MSG_TAG "%.5f,%.5f %s", lat, lon, w.label);
|
_task->core().waypoints.shareText(wi, text, sizeof(text));
|
||||||
else snprintf(text, sizeof(text), WAYPOINT_MSG_TAG "%.5f,%.5f", lat, lon);
|
|
||||||
_task->shareToMessage(text); // hands off to the Messages screen
|
_task->shareToMessage(text); // hands off to the Messages screen
|
||||||
}
|
}
|
||||||
} else { // Set as target
|
} else { // Set as target
|
||||||
|
|||||||
@@ -176,6 +176,33 @@ TEST(SosText, OnlyAnSosWithAPositionIsTakenForOne) {
|
|||||||
EXPECT_FALSE(geo::parseSos("SOSNA 50.06143,19.93658", lat, lon));
|
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) {
|
TEST(SosText, NoGpsIsRecognisedOnItsOwn) {
|
||||||
char t[64];
|
char t[64];
|
||||||
SosEngine::compose(t, sizeof(t), SosEngine::NOWHERE, 0, 0, nullptr, -1, "14:32", "");
|
SosEngine::compose(t, sizeof(t), SosEngine::NOWHERE, 0, 0, nullptr, -1, "14:32", "");
|
||||||
|
|||||||
Reference in New Issue
Block a user