mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(ui-lvgl): e-ink graphics in colour on the L2
Node detail: a card with a compass rose and the hop chain over the info; the bar keeps Message / Admin / Add, Ping and Navigate, the rest goes to a "More" group under the info (favourite toggled in place, delete by a second tap). Nodes: an arrow by each distance. A trail's height profile in Map tools and a saved trail's popup. Diagnostics > History: battery, noise, SNR and traffic charts. Clock > Calendar: today, the weekend and alarm days. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
// Clock tools (Home > Clock): alarm, countdown timer and stopwatch -- ui-new's
|
||||
// Tools > Clock on a touch screen. The alarm (NodePrefs alarm_*) and the
|
||||
// Clock tools (Home > Clock): alarm, countdown timer, stopwatch and this
|
||||
// month's calendar -- ui-new's Tools > Clock on a touch screen (the calendar
|
||||
// as ui-new's e-ink has it under the clock). The alarm (NodePrefs alarm_*) and the
|
||||
// countdown run in the Core's ClockEngine, so they fire on any screen and while
|
||||
// the display sleeps; the stopwatch is view state here, as in ui-new. Whatever
|
||||
// fires brings up a full-screen card with Dismiss (silent: the L2 speaker has
|
||||
@@ -10,7 +11,7 @@
|
||||
|
||||
namespace clockview {
|
||||
|
||||
enum : uint8_t { TAB_ALARM, TAB_TIMER, TAB_STOPWATCH, TAB_COUNT };
|
||||
enum : uint8_t { TAB_ALARM, TAB_TIMER, TAB_STOPWATCH, TAB_CALENDAR, TAB_COUNT };
|
||||
enum : uint8_t { ACT_START_STOP, ACT_RESET };
|
||||
|
||||
static uint8_t s_tab = TAB_ALARM;
|
||||
@@ -24,6 +25,63 @@ static lv_obj_t* s_ring = nullptr; // "Timer done" card on the top laye
|
||||
static lv_obj_t* s_ring_lbl = nullptr;
|
||||
static lv_obj_t* s_alarm_sw = nullptr; // follows a time change (setting it arms the alarm)
|
||||
|
||||
static struct tm s_cal_tm; // the day the calendar was drawn for (local)
|
||||
static uint32_t s_cal_marks = 0; // its alarm days, bit (day - 1)
|
||||
static lv_obj_t* s_cal = nullptr;
|
||||
|
||||
// The month as a grid, weeks from Monday: the weekday names, today in an
|
||||
// accent box, the alarm's days with an accent bar under them, the weekend
|
||||
// dimmer.
|
||||
static void onCalendarDraw(lv_event_t* e) {
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
||||
lv_layer_t* layer = lv_event_get_layer(e);
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords(o, &a);
|
||||
const struct tm& t = s_cal_tm;
|
||||
static const char* const NAMES[] = { "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" };
|
||||
const int W = a.x2 - a.x1 + 1, cw = W / 7, x0 = a.x1 + (W - 7 * cw) / 2;
|
||||
const int head = 16, rows = calmath::weeks(t), ch = (a.y2 - a.y1 + 1 - head) / rows;
|
||||
lv_draw_label_dsc_t td;
|
||||
lv_draw_label_dsc_init(&td);
|
||||
td.align = LV_TEXT_ALIGN_CENTER;
|
||||
td.font = THEME_FONT_SMALL;
|
||||
for (int c = 0; c < 7; c++) {
|
||||
td.color = lv_color_hex(theme::TEXT_MUTED);
|
||||
td.text = NAMES[c];
|
||||
lv_area_t la = { x0 + c * cw, a.y1, x0 + (c + 1) * cw - 1, a.y1 + head - 1 };
|
||||
lv_draw_label(layer, &td, &la);
|
||||
}
|
||||
td.font = THEME_FONT_BODY;
|
||||
const int fh = lv_font_get_line_height(td.font), col0 = calmath::firstCol(t), n = calmath::daysIn(t.tm_year, t.tm_mon);
|
||||
char buf[3];
|
||||
for (int day = 1; day <= n; day++) {
|
||||
const int k = col0 + day - 1, cx = x0 + (k % 7) * cw, cy = a.y1 + head + (k / 7) * ch;
|
||||
const bool today = day == t.tm_mday;
|
||||
if (today) {
|
||||
lv_draw_rect_dsc_t rd;
|
||||
lv_draw_rect_dsc_init(&rd);
|
||||
rd.radius = theme::RADIUS_SM;
|
||||
rd.bg_color = lv_color_hex(theme::ACCENT);
|
||||
const int bh = ch - 2 < fh + 2 ? ch - 2 : fh + 2, by = cy + (ch - bh) / 2;
|
||||
lv_area_t ra = { cx + 4, by, cx + cw - 5, by + bh - 1 };
|
||||
lv_draw_rect(layer, &rd, &ra);
|
||||
}
|
||||
snprintf(buf, sizeof(buf), "%d", day);
|
||||
td.text = buf;
|
||||
td.color = lv_color_hex(today ? theme::BG : k % 7 >= 5 ? theme::TEXT_MUTED : theme::TEXT);
|
||||
lv_area_t la = { cx, cy + (ch - fh) / 2, cx + cw - 1, cy + (ch + fh) / 2 };
|
||||
lv_draw_label(layer, &td, &la);
|
||||
if (s_cal_marks & (1UL << (day - 1))) { // under the number
|
||||
lv_draw_rect_dsc_t md;
|
||||
lv_draw_rect_dsc_init(&md);
|
||||
md.radius = 1;
|
||||
md.bg_color = lv_color_hex(today ? theme::BG : theme::ACCENT);
|
||||
lv_area_t ma = { cx + cw / 2 - 6, cy + ch - 2, cx + cw / 2 + 6, cy + ch - 1 };
|
||||
lv_draw_rect(layer, &md, &ma);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t swElapsed() { return s_sw_accum + (s_sw_running ? millis() - s_sw_start : 0); }
|
||||
|
||||
// Countdown: rounds up, so "00:01" shows until it actually fires.
|
||||
@@ -243,12 +301,11 @@ void UITask::buildClock() {
|
||||
lv_obj_t* body = newScreen("Clock", true);
|
||||
lv_obj_set_style_pad_row(body, 6, 0);
|
||||
lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLLABLE);
|
||||
s_big = s_go_lbl = s_alarm_sw = nullptr;
|
||||
s_big = s_go_lbl = s_alarm_sw = s_cal = nullptr;
|
||||
|
||||
// Tabs
|
||||
lv_obj_t* tabs = row(body);
|
||||
static const char* const NAMES[TAB_COUNT] = { LV_SYMBOL_BELL " Alarm", UI_SYMBOL_CLOCK " Timer",
|
||||
UI_SYMBOL_STOPWATCH " Stopwatch" };
|
||||
static const char* const NAMES[TAB_COUNT] = { "Alarm", "Timer", "Stopwatch", "Calendar" };
|
||||
for (int t = 0; t < TAB_COUNT; t++) {
|
||||
lv_obj_t* b = lv_button_create(tabs);
|
||||
lv_obj_set_height(b, 32);
|
||||
@@ -296,6 +353,29 @@ void UITask::buildClock() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_tab == TAB_CALENDAR) {
|
||||
struct tm t;
|
||||
bool synced = localTime(_prefs, t);
|
||||
if (!synced) {
|
||||
label(body, "The clock isn't set yet: no date to show.", THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||
return;
|
||||
}
|
||||
static const char* const LONG_MONTHS[] = { "January", "February", "March", "April", "May", "June", "July",
|
||||
"August", "September", "October", "November", "December" };
|
||||
char title[24]; // the month on the header's right: the grid gets the height
|
||||
snprintf(title, sizeof(title), "%s %d", LONG_MONTHS[t.tm_mon], t.tm_year + 1900);
|
||||
if (_header) lv_obj_align(label(_header, title, THEME_FONT_TITLE, theme::TEXT_MUTED), LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
s_cal_tm = t;
|
||||
s_cal_marks = calmath::alarmDays(t, _prefs->alarm_on, _prefs->alarm_repeat_mask, _prefs->alarm_hour, _prefs->alarm_min);
|
||||
s_cal = lv_obj_create(body);
|
||||
lv_obj_remove_style_all(s_cal);
|
||||
lv_obj_remove_flag(s_cal, LV_OBJ_FLAG_CLICKABLE);
|
||||
fillWidth(s_cal);
|
||||
lv_obj_set_flex_grow(s_cal, 1);
|
||||
lv_obj_add_event_cb(s_cal, onCalendarDraw, LV_EVENT_DRAW_MAIN_END, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_tab == TAB_TIMER) {
|
||||
s_shown_running = _core->clock.isTimerRunning();
|
||||
if (s_shown_running) {
|
||||
@@ -343,6 +423,14 @@ void UITask::buildClock() {
|
||||
// From loop() while SCR_CLOCK is shown: the running readouts.
|
||||
void UITask::refreshClock() {
|
||||
using namespace clockview;
|
||||
if (s_tab == TAB_CALENDAR) { // past midnight, or the alarm changed: a new day to box or mark
|
||||
struct tm t;
|
||||
if (s_cal && localTime(_prefs, t)
|
||||
&& (t.tm_mday != s_cal_tm.tm_mday || t.tm_mon != s_cal_tm.tm_mon
|
||||
|| calmath::alarmDays(t, _prefs->alarm_on, _prefs->alarm_repeat_mask, _prefs->alarm_hour, _prefs->alarm_min) != s_cal_marks))
|
||||
buildClock();
|
||||
return;
|
||||
}
|
||||
if (s_tab == TAB_TIMER) {
|
||||
bool running = _core->clock.isTimerRunning();
|
||||
if (running != s_shown_running) { buildClock(); return; } // started elsewhere / just fired
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
// Home > Diagnostics -- ui-new's Tools > Diagnostics. Tabs Live / System /
|
||||
// Font / Noise; the rows come from ui-core/Diagnostics.h. Live refreshes every second
|
||||
// and its header button resets the counters (after a confirm).
|
||||
// Home > Diagnostics -- ui-new's Tools > Diagnostics. Tabs Live / History /
|
||||
// System / Font / Noise; the rows come from ui-core/Diagnostics.h. Live
|
||||
// refreshes every second and its header button resets the counters (after a
|
||||
// confirm). History charts the last 8 hours (ui-core/StatusHistory.h, as on
|
||||
// e-ink): the battery, the noise floor, the SNR of the last packets, traffic.
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp after DeviceScreen.h.
|
||||
|
||||
@@ -11,7 +13,7 @@ extern RADIO_CLASS radio; // the variant's target.cpp (the noise sweep retunes
|
||||
|
||||
namespace diagview {
|
||||
|
||||
enum : uint8_t { TAB_LIVE, TAB_SYSTEM, TAB_FONT, TAB_NOISE, TAB_COUNT };
|
||||
enum : uint8_t { TAB_LIVE, TAB_HISTORY, TAB_SYSTEM, TAB_FONT, TAB_NOISE, TAB_COUNT };
|
||||
static uint8_t s_tab = TAB_LIVE; // kept across visits
|
||||
static lv_obj_t* s_list = nullptr;
|
||||
static const int EXTRA = 4; // GPS, power, last reset, last crash (L2 only)
|
||||
@@ -21,6 +23,118 @@ struct LiveVal { lv_obj_t* val; lv_obj_t* in; lv_obj_t* out; };
|
||||
static LiveVal s_vals[diag::MAX_ROWS + EXTRA];
|
||||
static int s_rows = 0;
|
||||
|
||||
// The history, a sample every 5 minutes from boot (UITask::sampleHistory()).
|
||||
static uicore::StatusHistory<96, 300000UL> s_hist;
|
||||
static uint32_t s_hist_shown = 0; // the history's version the charts show
|
||||
static lv_obj_t* s_ch_obj[4]; // History's charts and their values now
|
||||
static lv_obj_t* s_ch_now[4];
|
||||
|
||||
static uint32_t totalRecv() {
|
||||
uint32_t n = 0;
|
||||
for (uint8_t t = 0; t < 16; t++) n += the_mesh.getNumRecvByType(t);
|
||||
return n;
|
||||
}
|
||||
|
||||
// One chart's samples as drawn: a copy, so the draw callback needn't know
|
||||
// which ring (of which size) they came from.
|
||||
struct Chart { int16_t v[96]; int n, cap, min_span; bool bars; };
|
||||
static Chart s_charts[4];
|
||||
template <int N>
|
||||
static Chart* chartOf(int k, const uicore::History<N>& h, int min_span, bool bars) {
|
||||
Chart& c = s_charts[k];
|
||||
c.n = h.n; c.cap = N; c.min_span = min_span; c.bars = bars;
|
||||
for (int i = 0; i < h.n; i++) c.v[i] = h.at(i);
|
||||
return &c;
|
||||
}
|
||||
|
||||
// A history in its box, newest at the right: a line over a faint fill, or
|
||||
// bars from the bottom, scaled between the samples' own min and max (at
|
||||
// least min_span apart, so a steady reading stays flat).
|
||||
static void onChartDraw(lv_event_t* e) {
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
||||
const Chart& c = *(const Chart*)lv_event_get_user_data(e);
|
||||
lv_layer_t* layer = lv_event_get_layer(e);
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords(o, &a);
|
||||
const int x0 = a.x1 + 2, w = a.x2 - a.x1 - 4, y0 = a.y1 + 3, h = a.y2 - a.y1 - 5, base = a.y2 - 1;
|
||||
lv_draw_line_dsc_t ld;
|
||||
lv_draw_line_dsc_init(&ld);
|
||||
if (c.n < (c.bars ? 1 : 2)) { // nothing yet: a dotted baseline
|
||||
ld.color = lv_color_hex(theme::TEXT_MUTED);
|
||||
ld.width = 1; ld.dash_width = 2; ld.dash_gap = 4;
|
||||
ld.p1.x = x0; ld.p2.x = x0 + w; ld.p1.y = ld.p2.y = base - 1;
|
||||
lv_draw_line(layer, &ld);
|
||||
return;
|
||||
}
|
||||
int lo = c.bars ? 0 : c.v[0], hi = c.v[0];
|
||||
for (int i = 0; i < c.n; i++) { if (c.v[i] < lo) lo = c.v[i]; if (c.v[i] > hi) hi = c.v[i]; }
|
||||
if (hi - lo < c.min_span) {
|
||||
if (c.bars) hi = lo + c.min_span;
|
||||
else { const int mid = (hi + lo) / 2; lo = mid - c.min_span / 2; hi = lo + c.min_span; }
|
||||
}
|
||||
auto X = [&](int i) { return (int32_t)(x0 + w - (c.n - 1 - i) * w / (c.cap - 1)); };
|
||||
auto Y = [&](int v) { return (int32_t)(y0 + h - (v - lo) * h / (hi - lo)); };
|
||||
ld.color = lv_color_hex(theme::ACCENT);
|
||||
if (c.bars) { // a bar a sample, a pixel apart
|
||||
const int pitch = (w + c.cap / 2) / c.cap, bw = pitch > 2 ? pitch - 1 : 1;
|
||||
lv_draw_rect_dsc_t rd;
|
||||
lv_draw_rect_dsc_init(&rd);
|
||||
rd.bg_color = lv_color_hex(theme::ACCENT);
|
||||
rd.bg_opa = LV_OPA_70;
|
||||
for (int i = 0; i < c.n; i++) {
|
||||
if (c.v[i] <= 0) continue;
|
||||
lv_area_t r = { X(i) - bw + 1, Y(c.v[i]), X(i), base };
|
||||
lv_draw_rect(layer, &rd, &r);
|
||||
}
|
||||
return;
|
||||
}
|
||||
lv_draw_line_dsc_t fill;
|
||||
lv_draw_line_dsc_init(&fill);
|
||||
fill.color = lv_color_hex(theme::ACCENT);
|
||||
fill.opa = LV_OPA_20;
|
||||
fill.width = 1;
|
||||
ld.width = 2;
|
||||
ld.round_start = ld.round_end = 1;
|
||||
for (int i = 1; i < c.n; i++) {
|
||||
const int32_t xa = X(i - 1), xb = X(i), ya = Y(c.v[i - 1]), yb = Y(c.v[i]);
|
||||
for (int32_t x = (i == 1 ? xa : xa + 1); x <= xb; x++) {
|
||||
fill.p1.x = fill.p2.x = x;
|
||||
fill.p1.y = xb > xa ? ya + (yb - ya) * (x - xa) / (xb - xa) : yb;
|
||||
fill.p2.y = base;
|
||||
lv_draw_line(layer, &fill);
|
||||
}
|
||||
ld.p1.x = xa; ld.p1.y = ya; ld.p2.x = xb; ld.p2.y = yb;
|
||||
lv_draw_line(layer, &ld);
|
||||
}
|
||||
}
|
||||
|
||||
// A chart card: the title and the value now, the chart, the span under it.
|
||||
static void chartCard(lv_obj_t* parent, int k, const char* title, const char* from) {
|
||||
lv_obj_t* card = infoCard(parent);
|
||||
lv_obj_set_style_pad_ver(card, 6, 0);
|
||||
lv_obj_set_style_pad_row(card, 2, 0);
|
||||
lv_obj_t* r = lv_obj_create(card);
|
||||
lv_obj_remove_style_all(r);
|
||||
fillWidth(r);
|
||||
lv_obj_set_height(r, LV_SIZE_CONTENT);
|
||||
lv_obj_align(label(r, title, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_LEFT_MID, 0, 0);
|
||||
s_ch_now[k] = label(r, "", THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_align(s_ch_now[k], LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_t* ch = lv_obj_create(card);
|
||||
lv_obj_remove_style_all(ch);
|
||||
lv_obj_remove_flag(ch, LV_OBJ_FLAG_CLICKABLE);
|
||||
fillWidth(ch);
|
||||
lv_obj_set_height(ch, 48);
|
||||
lv_obj_add_event_cb(ch, onChartDraw, LV_EVENT_DRAW_MAIN_END, &s_charts[k]);
|
||||
s_ch_obj[k] = ch;
|
||||
lv_obj_t* ax = lv_obj_create(card);
|
||||
lv_obj_remove_style_all(ax);
|
||||
fillWidth(ax);
|
||||
lv_obj_set_height(ax, LV_SIZE_CONTENT);
|
||||
lv_obj_align(label(ax, from, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_align(label(ax, "now", THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
}
|
||||
|
||||
static void extraRow(diag::Row* rows, int& n, const char* lbl, const char* fmt, ...) {
|
||||
rows[n].label = lbl;
|
||||
va_list ap; va_start(ap, fmt);
|
||||
@@ -251,7 +365,7 @@ void UITask::buildDiag() {
|
||||
lv_obj_set_style_pad_row(body, 4, 0);
|
||||
if (s_tab == TAB_LIVE && _header) headerButton(_header, LV_SYMBOL_REFRESH " Reset", onDiagReset, 4, NULL);
|
||||
|
||||
static const char* TABS[] = { "Live", "System", "Font", "Noise", "" };
|
||||
static const char* TABS[] = { "Live", "History", "System", "Font", "Noise", "" };
|
||||
lv_obj_t* tabs = segmented(body, TABS, s_tab, lv_pct(100), 34);
|
||||
lv_obj_set_style_bg_color(tabs, lv_color_hex(theme::SURFACE), LV_PART_ITEMS);
|
||||
lv_obj_add_event_cb(tabs, onDiagTab, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
@@ -270,6 +384,14 @@ void UITask::buildDiag() {
|
||||
}
|
||||
|
||||
s_noise_status = nullptr;
|
||||
if (s_tab == TAB_HISTORY) {
|
||||
chartCard(s_list, 0, "Battery", "8 h ago");
|
||||
chartCard(s_list, 1, "Noise floor", "8 h ago");
|
||||
chartCard(s_list, 2, "SNR, last packets", "32 packets ago");
|
||||
chartCard(s_list, 3, "Packets heard, per 5 min", "8 h ago");
|
||||
fillHistory();
|
||||
return;
|
||||
}
|
||||
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.");
|
||||
@@ -365,8 +487,35 @@ void UITask::diagSection(int sec) {
|
||||
}
|
||||
}
|
||||
|
||||
// History's charts and values from the history as it is now.
|
||||
void UITask::fillHistory() {
|
||||
using namespace diagview;
|
||||
s_hist_shown = s_hist.version;
|
||||
chartOf(0, s_hist.batt, 20, false);
|
||||
chartOf(1, s_hist.noise, 6, false);
|
||||
chartOf(2, s_hist.snr, 20, false);
|
||||
chartOf(3, s_hist.traffic, 4, true);
|
||||
for (int k = 0; k < 4; k++) lv_obj_invalidate(s_ch_obj[k]);
|
||||
setTextFmt(s_ch_now[0], "%.2f V", (_batt_mv ? _batt_mv : getBattMilliVolts()) / 1000.0f);
|
||||
const int nf = (int)radio_driver.getNoiseFloor();
|
||||
if (nf) setTextFmt(s_ch_now[1], "%d dBm", nf); else setText(s_ch_now[1], "-");
|
||||
if (s_hist.snr.n) setTextFmt(s_ch_now[2], "%.1f dB", s_hist.snr.at(s_hist.snr.n - 1) / 4.0f); else setText(s_ch_now[2], "-");
|
||||
setTextFmt(s_ch_now[3], "%d", s_hist.traffic.n ? s_hist.traffic.at(s_hist.traffic.n - 1) : 0);
|
||||
}
|
||||
|
||||
// From loop(): the history's readings (it keeps its own pace).
|
||||
void UITask::sampleHistory() {
|
||||
using namespace diagview;
|
||||
s_hist.tick(millis(), (int)radio_driver.getNoiseFloor(), _batt_mv ? _batt_mv : getBattMilliVolts(),
|
||||
totalRecv(), radio_driver.getLastSNR());
|
||||
}
|
||||
|
||||
void UITask::refreshDiag() {
|
||||
using namespace diagview;
|
||||
if (_screen == SCR_DIAG && s_tab == TAB_HISTORY && !_nav_overlay && s_hist.version != s_hist_shown) {
|
||||
fillHistory(); // a new sample: the charts in place, the list stays where it was scrolled
|
||||
return;
|
||||
}
|
||||
if (_screen != SCR_DIAG || s_tab != TAB_LIVE || _nav_overlay) return;
|
||||
diag::Row rows[diag::MAX_ROWS + EXTRA];
|
||||
int n = allRows(rows, _core->gpsEnabled());
|
||||
@@ -381,6 +530,7 @@ void UITask::diagResetPopup() {
|
||||
|
||||
void UITask::diagReset() {
|
||||
diag::resetCounters();
|
||||
diagview::s_hist.countersReset();
|
||||
navClosePopup();
|
||||
refreshDiag();
|
||||
}
|
||||
|
||||
@@ -899,7 +899,7 @@ static void trailTitle(const char* name, char* out, size_t n) {
|
||||
|
||||
// Points, distance and recorded time of a saved trail, from its file.
|
||||
template <typename F>
|
||||
static bool trailSummary(F& io, int& points, float& meters, uint32_t& secs) {
|
||||
static bool trailSummary(F& io, int& points, float& meters, uint32_t& secs, trailprofile::Sampler* prof = nullptr) {
|
||||
uint16_t cnt = 0;
|
||||
uint32_t accum = 0;
|
||||
if (!persist::readHeader(io, TrailStore::SAVE_MAGIC, TrailStore::SAVE_VERSION, cnt, 1)) return false;
|
||||
@@ -910,6 +910,7 @@ static bool trailSummary(F& io, int& points, float& meters, uint32_t& secs) {
|
||||
if (io.read((uint8_t*)&p, sizeof(p)) != (int)sizeof(p)) break;
|
||||
if (points > 0 && !(p.flags & TRAIL_FLAG_SEG_START))
|
||||
meters += TrailStore::haversineMeters(prev.lat_1e6, prev.lon_1e6, p.lat_1e6, p.lon_1e6);
|
||||
if (prof) prof->feed(p);
|
||||
prev = p;
|
||||
points++;
|
||||
}
|
||||
@@ -963,6 +964,9 @@ void UITask::navToolsPopup() {
|
||||
|
||||
sectionTitle(list, "TRAIL");
|
||||
_nav_trail_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT);
|
||||
profileview::s_prof = trailprofile::Sampler(); // its height profile, as it is now
|
||||
profileview::s_prof.feedAll(_core->trail.store());
|
||||
profileChart(list, 72);
|
||||
lv_obj_t* r = buttonBar(list);
|
||||
_nav_trail_btn = toolButton(r, "", navmap::TL_TRAIL_TOGGLE, true);
|
||||
toolButton(r, LV_SYMBOL_SAVE " Save", navmap::TL_TRAIL_SAVE, false);
|
||||
@@ -1220,13 +1224,15 @@ void UITask::savedTrailPopup(int idx) {
|
||||
lv_obj_t* panel = navPopupPanel(title, false);
|
||||
|
||||
int pts = 0; float m = 0; uint32_t secs = 0; bool ok = false;
|
||||
trailprofile::Sampler& prof = profileview::s_prof;
|
||||
prof = trailprofile::Sampler();
|
||||
if (idx >= 0) {
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "%s/%s", navmap::TRAILS_DIR, navmap::s_st_names[idx]);
|
||||
if (FILE* f = fopen(path, "rb")) { navmap::FileRW io{ f }; ok = navmap::trailSummary(io, pts, m, secs); fclose(f); }
|
||||
if (FILE* f = fopen(path, "rb")) { navmap::FileRW io{ f }; ok = navmap::trailSummary(io, pts, m, secs, &prof); fclose(f); }
|
||||
} else if (DataStore* ds = the_mesh.getDataStore()) {
|
||||
File f = ds->openRead(TrailEngine::TRAIL_FILE);
|
||||
if (f) { ok = navmap::trailSummary(f, pts, m, secs); f.close(); }
|
||||
if (f) { ok = navmap::trailSummary(f, pts, m, secs, &prof); f.close(); }
|
||||
}
|
||||
char dist[12], dur[12], info[64];
|
||||
geo::fmtDist(dist, sizeof(dist), m / 1000.0f, _prefs && _prefs->units_imperial);
|
||||
@@ -1234,6 +1240,7 @@ void UITask::savedTrailPopup(int idx) {
|
||||
if (ok) snprintf(info, sizeof(info), "%s - %s - %d points", dist, dur, pts);
|
||||
else snprintf(info, sizeof(info), "Can't read this file");
|
||||
label(panel, info, THEME_FONT_BODY, theme::TEXT);
|
||||
if (ok) profileChart(panel, 60);
|
||||
if (!_core->trail.store().empty())
|
||||
label(panel, "Loading replaces the trail on the map.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_t* r = buttonBar(panel);
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#pragma once
|
||||
// Node detail's picture card, over its info rows: where the node is, on a
|
||||
// small compass rose (north up, an arrow towards it), and how a message gets
|
||||
// there, a chain from you to it with a ring per repeater on the way. Either half shows only when
|
||||
// there's something to draw (a position of both ends / a contact's path).
|
||||
// ui-new draws the same on e-ink (NearbyScreen.h).
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp before the node detail.
|
||||
|
||||
namespace nodeviz {
|
||||
|
||||
static const int ROSE_W = 64, ROSE_H = 76; // the ring, N over it
|
||||
static lv_obj_t* s_card = nullptr;
|
||||
static lv_obj_t* s_rose = nullptr;
|
||||
static lv_obj_t* s_path_col = nullptr;
|
||||
static lv_obj_t* s_chain = nullptr;
|
||||
static lv_obj_t* s_path_txt = nullptr;
|
||||
static int s_bearing = -1; // degrees, -1: no position
|
||||
static int s_hops = -1; // 0..63, 0xFF flood, -1: not a contact
|
||||
|
||||
static void onRoseDraw(lv_event_t* e) {
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
||||
lv_layer_t* layer = lv_event_get_layer(e);
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords(o, &a);
|
||||
const float r = ROSE_W / 2.0f - 8, cx = (a.x1 + a.x2) / 2.0f, cy = a.y2 - r - 3;
|
||||
|
||||
lv_draw_arc_dsc_t ad; // the ring
|
||||
lv_draw_arc_dsc_init(&ad);
|
||||
ad.center.x = (int32_t)cx; ad.center.y = (int32_t)cy;
|
||||
ad.radius = (uint16_t)r;
|
||||
ad.start_angle = 0; ad.end_angle = 360;
|
||||
ad.width = 1;
|
||||
ad.color = lv_color_hex(theme::TEXT_MUTED);
|
||||
lv_draw_arc(layer, &ad);
|
||||
|
||||
lv_draw_line_dsc_t ld; // a tick every 45 degrees, longer at the cardinals
|
||||
lv_draw_line_dsc_init(&ld);
|
||||
ld.color = lv_color_hex(theme::TEXT_MUTED);
|
||||
for (int deg = 0; deg < 360; deg += 45) {
|
||||
const float ang = deg * (float)M_PI / 180.0f, sx = sinf(ang), sy = -cosf(ang);
|
||||
const float len = deg % 90 == 0 ? 6 : 3;
|
||||
ld.width = deg % 90 == 0 ? 2 : 1;
|
||||
ld.p1.x = cx + sx * r; ld.p1.y = cy + sy * r;
|
||||
ld.p2.x = cx + sx * (r - len); ld.p2.y = cy + sy * (r - len);
|
||||
lv_draw_line(layer, &ld);
|
||||
}
|
||||
|
||||
lv_draw_label_dsc_t td; // N above the ring
|
||||
lv_draw_label_dsc_init(&td);
|
||||
td.align = LV_TEXT_ALIGN_CENTER;
|
||||
td.font = THEME_FONT_SMALL;
|
||||
td.color = lv_color_hex(theme::FAIL);
|
||||
td.text = "N";
|
||||
const int32_t h = lv_font_get_line_height(td.font);
|
||||
lv_area_t la = { (int32_t)cx - 10, (int32_t)(cy - r) - h - 1, (int32_t)cx + 10, (int32_t)(cy - r) - 1 };
|
||||
lv_draw_label(layer, &td, &la);
|
||||
|
||||
if (s_bearing < 0) return;
|
||||
const float ang = s_bearing * (float)M_PI / 180.0f, sx = sinf(ang), sy = -cosf(ang);
|
||||
const float tip = r - 4, base = tip - 12;
|
||||
ld.color = lv_color_hex(theme::ACCENT); // the arrow: a shaft from the centre, a head at the ring
|
||||
ld.width = 3;
|
||||
ld.round_start = ld.round_end = 1;
|
||||
ld.p1.x = cx - sx * (r * 0.35f); ld.p1.y = cy - sy * (r * 0.35f);
|
||||
ld.p2.x = cx + sx * base; ld.p2.y = cy + sy * base;
|
||||
lv_draw_line(layer, &ld);
|
||||
lv_draw_triangle_dsc_t tri;
|
||||
lv_draw_triangle_dsc_init(&tri);
|
||||
tri.color = lv_color_hex(theme::ACCENT);
|
||||
tri.opa = LV_OPA_COVER;
|
||||
tri.p[0].x = cx + sx * tip; tri.p[0].y = cy + sy * tip;
|
||||
tri.p[1].x = cx + sx * base + sy * 7; tri.p[1].y = cy + sy * base - sx * 7;
|
||||
tri.p[2].x = cx + sx * base - sy * 7; tri.p[2].y = cy + sy * base + sx * 7;
|
||||
lv_draw_triangle(layer, &tri);
|
||||
}
|
||||
|
||||
// You and the node filled in the accent, the repeaters between as rings,
|
||||
// dotted links; as many rings as fit, the count says the rest.
|
||||
static void onChainDraw(lv_event_t* e) {
|
||||
if (s_hops < 0 || s_hops == 0xFF) return;
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
||||
lv_layer_t* layer = lv_event_get_layer(e);
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords(o, &a);
|
||||
const int R = 6, PITCH = 26, w = a.x2 - a.x1 + 1, cy = (a.y1 + a.y2) / 2;
|
||||
const int cap = (w - 2 * R) / PITCH - 1;
|
||||
const int shown = s_hops > cap ? (cap < 1 ? 1 : cap) : s_hops, n = shown + 2;
|
||||
lv_draw_line_dsc_t ld;
|
||||
lv_draw_line_dsc_init(&ld);
|
||||
ld.color = lv_color_hex(theme::TEXT_MUTED);
|
||||
ld.width = 2;
|
||||
ld.dash_width = 3; ld.dash_gap = 3;
|
||||
ld.p1.x = a.x1 + R; ld.p1.y = cy;
|
||||
ld.p2.x = a.x1 + R + (n - 1) * PITCH; ld.p2.y = cy;
|
||||
lv_draw_line(layer, &ld);
|
||||
for (int k = 0; k < n; k++) {
|
||||
const bool end = k == 0 || k == n - 1;
|
||||
lv_draw_rect_dsc_t rd;
|
||||
lv_draw_rect_dsc_init(&rd);
|
||||
rd.radius = LV_RADIUS_CIRCLE;
|
||||
rd.bg_color = lv_color_hex(end ? theme::ACCENT : theme::SURFACE);
|
||||
rd.border_color = lv_color_hex(end ? theme::ACCENT : theme::TEXT_MUTED);
|
||||
rd.border_width = 2;
|
||||
const int x = a.x1 + R + k * PITCH;
|
||||
lv_area_t ra = { x - R, cy - R, x + R, cy + R };
|
||||
lv_draw_rect(layer, &rd, &ra);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace nodeviz
|
||||
|
||||
// The card, over the node's info rows. Filled by nodeVizSet().
|
||||
static void nodeVizBuild(lv_obj_t* parent) {
|
||||
using namespace nodeviz;
|
||||
s_card = lv_obj_create(parent);
|
||||
styleSurface(s_card, theme::SURFACE);
|
||||
lv_obj_remove_flag(s_card, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_remove_flag(s_card, LV_OBJ_FLAG_CLICKABLE);
|
||||
fillWidth(s_card);
|
||||
lv_obj_set_height(s_card, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_radius(s_card, theme::RADIUS, 0);
|
||||
lv_obj_set_style_pad_all(s_card, 6, 0);
|
||||
lv_obj_set_flex_flow(s_card, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(s_card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(s_card, 12, 0);
|
||||
|
||||
s_rose = lv_obj_create(s_card);
|
||||
lv_obj_remove_style_all(s_rose);
|
||||
lv_obj_remove_flag(s_rose, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(s_rose, ROSE_W, ROSE_H);
|
||||
lv_obj_add_event_cb(s_rose, onRoseDraw, LV_EVENT_DRAW_MAIN_END, NULL);
|
||||
|
||||
s_path_col = lv_obj_create(s_card);
|
||||
lv_obj_remove_style_all(s_path_col);
|
||||
lv_obj_remove_flag(s_path_col, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_width(s_path_col, 1);
|
||||
lv_obj_set_flex_grow(s_path_col, 1);
|
||||
lv_obj_set_height(s_path_col, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(s_path_col, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(s_path_col, 4, 0);
|
||||
label(s_path_col, "Path", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
s_chain = lv_obj_create(s_path_col);
|
||||
lv_obj_remove_style_all(s_chain);
|
||||
lv_obj_remove_flag(s_chain, LV_OBJ_FLAG_CLICKABLE);
|
||||
fillWidth(s_chain);
|
||||
lv_obj_set_height(s_chain, 16);
|
||||
lv_obj_add_event_cb(s_chain, onChainDraw, LV_EVENT_DRAW_MAIN_END, NULL);
|
||||
s_path_txt = label(s_path_col, "", THEME_FONT_BODY, theme::TEXT);
|
||||
}
|
||||
|
||||
// `bearing` in degrees or -1; `hops` 0..63, 0xFF (flood) or -1 (no contact).
|
||||
static void nodeVizSet(int bearing, int hops) {
|
||||
using namespace nodeviz;
|
||||
if (!s_card) return;
|
||||
if (bearing != s_bearing) { s_bearing = bearing; lv_obj_invalidate(s_rose); }
|
||||
if (hops != s_hops) {
|
||||
s_hops = hops;
|
||||
lv_obj_invalidate(s_chain);
|
||||
char t[24];
|
||||
if (hops == 0xFF) snprintf(t, sizeof(t), "Flood, no route yet");
|
||||
else if (hops == 0) snprintf(t, sizeof(t), "Direct");
|
||||
else snprintf(t, sizeof(t), "%d hop%s", hops, hops == 1 ? "" : "s");
|
||||
setText(s_path_txt, t);
|
||||
if (hops == 0xFF) lv_obj_add_flag(s_chain, LV_OBJ_FLAG_HIDDEN); else lv_obj_remove_flag(s_chain, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
if (bearing < 0) lv_obj_add_flag(s_rose, LV_OBJ_FLAG_HIDDEN); else lv_obj_remove_flag(s_rose, LV_OBJ_FLAG_HIDDEN);
|
||||
if (hops < 0) lv_obj_add_flag(s_path_col, LV_OBJ_FLAG_HIDDEN); else lv_obj_remove_flag(s_path_col, LV_OBJ_FLAG_HIDDEN);
|
||||
if (bearing < 0 && hops < 0) lv_obj_add_flag(s_card, LV_OBJ_FLAG_HIDDEN); else lv_obj_remove_flag(s_card, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
// A trail's height profile (ui-core/TrailProfile.h) as a chart: the height
|
||||
// over the distance, a line in the accent over a faint fill, the climb and
|
||||
// the highest and lowest points over it, top left. In the live trail's Map tools section and
|
||||
// a saved trail's popup. ui-new draws the same on e-ink and the OLED
|
||||
// (TrailScreen.h).
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp before NavMap.h.
|
||||
|
||||
namespace profileview {
|
||||
|
||||
static trailprofile::Sampler s_prof; // what the chart on screen shows
|
||||
|
||||
static void onDraw(lv_event_t* e) {
|
||||
const trailprofile::Sampler& p = s_prof;
|
||||
if (p.n < 2) return;
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
||||
lv_layer_t* layer = lv_event_get_layer(e);
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords(o, &a);
|
||||
int lo = p.st.lo, hi = p.st.hi;
|
||||
if (hi - lo < 20) { const int mid = (hi + lo) / 2; lo = mid - 10; hi = mid + 10; }
|
||||
const float total = p.total > 0 ? p.total : 1.0f;
|
||||
const int x0 = a.x1 + 2, w = a.x2 - a.x1 - 4, y0 = a.y1 + 20, h = a.y2 - y0 - 2, base = a.y2 - 1; // under the caption
|
||||
auto X = [&](int i) { return (int32_t)(x0 + p.dist[i] / total * w); };
|
||||
auto Y = [&](int i) { return (int32_t)(y0 + h - (p.alt[i] - lo) * h / (hi - lo)); };
|
||||
|
||||
lv_draw_line_dsc_t fill; // under the line, a column at a time (triangles would leave seams)
|
||||
lv_draw_line_dsc_init(&fill);
|
||||
fill.color = lv_color_hex(theme::ACCENT);
|
||||
fill.opa = LV_OPA_20;
|
||||
fill.width = 1;
|
||||
lv_draw_line_dsc_t ld;
|
||||
lv_draw_line_dsc_init(&ld);
|
||||
ld.color = lv_color_hex(theme::ACCENT);
|
||||
ld.width = 2;
|
||||
ld.round_start = ld.round_end = 1;
|
||||
for (int i = 1; i < p.n; i++) {
|
||||
const int32_t xa = X(i - 1), xb = X(i), ya = Y(i - 1), yb = Y(i);
|
||||
for (int32_t x = (i == 1 ? xa : xa + 1); x <= xb; x++) {
|
||||
fill.p1.x = fill.p2.x = x;
|
||||
fill.p1.y = xb > xa ? ya + (yb - ya) * (x - xa) / (xb - xa) : yb;
|
||||
fill.p2.y = base;
|
||||
lv_draw_line(layer, &fill);
|
||||
}
|
||||
ld.p1.x = xa; ld.p1.y = ya; ld.p2.x = xb; ld.p2.y = yb;
|
||||
lv_draw_line(layer, &ld);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace profileview
|
||||
|
||||
// The chart, `h` tall, into `parent`, for what s_prof holds; nothing when
|
||||
// the trail has no heights. Returns the chart, or null.
|
||||
static lv_obj_t* profileChart(lv_obj_t* parent, int h) {
|
||||
using namespace profileview;
|
||||
if (!s_prof.st.any || s_prof.n < 2) return nullptr;
|
||||
lv_obj_t* c = lv_obj_create(parent);
|
||||
styleSurface(c, theme::SURFACE);
|
||||
lv_obj_remove_flag(c, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_remove_flag(c, LV_OBJ_FLAG_CLICKABLE);
|
||||
fillWidth(c);
|
||||
lv_obj_set_height(c, h);
|
||||
lv_obj_set_style_radius(c, theme::RADIUS_SM, 0);
|
||||
lv_obj_add_event_cb(c, onDraw, LV_EVENT_DRAW_MAIN_END, NULL);
|
||||
char t[64];
|
||||
snprintf(t, sizeof(t), "Climb %d m - highest %d m - lowest %d m", s_prof.st.gain, s_prof.st.hi, s_prof.st.lo);
|
||||
lv_obj_align(label(c, t, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_TOP_LEFT, 6, 2);
|
||||
return c;
|
||||
}
|
||||
@@ -74,6 +74,9 @@ static bool s_chats_stale = false; // Messages: a refresh waits for a held row
|
||||
#include "../ui-core/SettingsSchema.h"
|
||||
#include "../ui-core/GpsAverager.h"
|
||||
#include "../ui-core/TrackBack.h"
|
||||
#include "../ui-core/TrailProfile.h"
|
||||
#include "../ui-core/StatusHistory.h"
|
||||
#include "../ui-core/CalendarMath.h"
|
||||
#include "../ui-core/RadioControl.h"
|
||||
#include "../ui-core/RepeaterControl.h"
|
||||
#include "../ui-core/Diagnostics.h"
|
||||
@@ -1620,6 +1623,7 @@ extern "C" lv_result_t __wrap_lv_obj_send_event(lv_obj_t* o, lv_event_code_t c,
|
||||
void UITask::loop() {
|
||||
pollConnection();
|
||||
drainCoreEvents();
|
||||
sampleHistory();
|
||||
|
||||
// USER (BOOT, side) button: Home's clock page; held and let go, mutes / unmutes, also with the
|
||||
// screen off, which it doesn't wake. WAKE (top) button: screen off / on.
|
||||
@@ -2607,6 +2611,8 @@ int UITask::fillPending() const {
|
||||
return n;
|
||||
}
|
||||
|
||||
#include "NodeViz.h" // the node detail's rose and path (newScreen() drops its card)
|
||||
|
||||
lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
|
||||
s_fill_n = 0; // what was still to be built goes with the screen
|
||||
_home_clock = _home_date = _home_unread = nullptr;
|
||||
@@ -2617,6 +2623,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
|
||||
_header = _body = nullptr;
|
||||
_nearby_list = _nearby_status = _nearby_sort_lbl = _nearby_chips = nullptr;
|
||||
_node_info = _node_ping = _node_delete_lbl = nullptr;
|
||||
nodeviz::s_card = nullptr;
|
||||
_scan_overlay = _scan_list = _scan_status = nullptr; // the popup went with the old screen
|
||||
_map_area = _map_marks = _map_me = _map_zoom_lbl = _map_hint = _map_dl_pill = _map_center_btn = nullptr;
|
||||
_dl_overlay = _dl_info = _dl_sub = _dl_start_lbl = _dl_job_row = _dl_job_lbl = nullptr;
|
||||
@@ -3602,6 +3609,27 @@ static void nearbyText(const NearbyModel::Entry& e, uint32_t now, bool imperial,
|
||||
t.right_col = e.is_live ? theme::OK : theme::TEXT_MUTED;
|
||||
}
|
||||
|
||||
// A small arrow towards the node, drawn in the value label's left padding:
|
||||
// its user data is the bearing + 1 (0: none).
|
||||
static const int ROW_ARROW_W = 14;
|
||||
static void onRowArrowDraw(lv_event_t* e) {
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
||||
const int b = (int)(intptr_t)lv_obj_get_user_data(o) - 1;
|
||||
if (b < 0) return;
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords(o, &a);
|
||||
const float cx = a.x1 + ROW_ARROW_W / 2 - 1, cy = (a.y1 + a.y2) / 2.0f;
|
||||
const float ang = b * (float)M_PI / 180.0f, sx = sinf(ang), sy = -cosf(ang);
|
||||
lv_draw_triangle_dsc_t tri;
|
||||
lv_draw_triangle_dsc_init(&tri);
|
||||
tri.color = lv_obj_get_style_text_color(o, LV_PART_MAIN);
|
||||
tri.opa = LV_OPA_COVER;
|
||||
tri.p[0].x = cx + sx * 6; tri.p[0].y = cy + sy * 6; // the tip
|
||||
tri.p[1].x = cx - sx * 5 + sy * 4; tri.p[1].y = cy - sy * 5 - sx * 4;
|
||||
tri.p[2].x = cx - sx * 5 - sy * 4; tri.p[2].y = cy - sy * 5 + sx * 4;
|
||||
lv_draw_triangle(lv_event_get_layer(e), &tri);
|
||||
}
|
||||
|
||||
void UITask::refreshNearbyList() {
|
||||
if (!_nearby_list) return;
|
||||
_nearby->refreshModel();
|
||||
@@ -3621,13 +3649,27 @@ void UITask::refreshNearbyList() {
|
||||
// order); the list stays where it was scrolled.
|
||||
static const vlist::Kind ROW = {
|
||||
theme::ROW_H,
|
||||
[](lv_obj_t* list) { return vRowValue(list, onNearbyRow); },
|
||||
[](lv_obj_t* list) {
|
||||
lv_obj_t* row = vRowValue(list, onNearbyRow);
|
||||
lv_obj_add_event_cb(lv_obj_get_child(row, 2), onRowArrowDraw, LV_EVENT_DRAW_MAIN_END, NULL);
|
||||
return row;
|
||||
},
|
||||
[](lv_obj_t* row, intptr_t i) {
|
||||
const NearbyModel::Entry& e = s_ui->_nearby->at((int)i);
|
||||
NearbyText t;
|
||||
nearbyText(e, rtc_clock.getCurrentTime(), s_ui->_prefs && s_ui->_prefs->units_imperial, t);
|
||||
rowSet(row, t.title, t.sub, e.fav ? theme::ACCENT : theme::TEXT);
|
||||
rowValue(row, t.right, t.right_col);
|
||||
// By the distance, an arrow towards it
|
||||
int b = -1;
|
||||
int32_t lat, lon;
|
||||
if (e.dist_km >= 0.0f && s_ui->_nearby->ownPosition(lat, lon)) b = geo::bearingDeg(lat, lon, e.lat_e6, e.lon_e6);
|
||||
lv_obj_t* v = lv_obj_get_child(row, 2);
|
||||
if ((intptr_t)lv_obj_get_user_data(v) != b + 1) {
|
||||
lv_obj_set_user_data(v, (void*)(intptr_t)(b + 1));
|
||||
lv_obj_set_style_pad_left(v, b >= 0 ? ROW_ARROW_W : 0, 0);
|
||||
lv_obj_invalidate(v);
|
||||
}
|
||||
},
|
||||
};
|
||||
vlist::begin(_nearby_list);
|
||||
@@ -3653,11 +3695,19 @@ void UITask::openNode(int row) {
|
||||
buildNode();
|
||||
}
|
||||
|
||||
static lv_obj_t* s_nd_fav = nullptr;
|
||||
static void nodeFavText() {
|
||||
if (s_nd_fav) lv_label_set_text(s_nd_fav, s_node.fav ? UI_SYMBOL_STAR " Remove from favourites" : UI_SYMBOL_STAR " Add to favourites");
|
||||
}
|
||||
|
||||
void UITask::buildNode() {
|
||||
const NearbyModel::Entry& e = s_node;
|
||||
lv_obj_t* body = newScreen(e.name[0] ? e.name : "(unknown)", true);
|
||||
|
||||
lv_obj_t* info = scrollList(body); // the info, scrolling above the actions at the bottom
|
||||
s_nd_fav = nullptr;
|
||||
nodeVizBuild(info);
|
||||
nodeviz::s_bearing = nodeviz::s_hops = -2; // nothing drawn yet: the first set fills both
|
||||
_node_info = infoCard(info);
|
||||
s_nd_type = infoRow(_node_info, "Type", "");
|
||||
s_nd_status = infoRow(_node_info, "Status", "");
|
||||
@@ -3668,26 +3718,26 @@ void UITask::buildNode() {
|
||||
s_nd_id = infoRow(_node_info, "ID", "");
|
||||
_node_ping = label(info, "", THEME_FONT_BODY, theme::ACCENT);
|
||||
|
||||
// The bar: what the node is for -- a message, its admin, adding it -- a
|
||||
// ping and the way there. The rest, less often, in a group under the info.
|
||||
lv_obj_t* acts = buttonBar(body);
|
||||
bool contact = e.contact_idx >= 0;
|
||||
bool admin = contact && (e.type == ADV_TYPE_REPEATER || e.type == ADV_TYPE_ROOM);
|
||||
bool pos = e.lat_e6 != 0 || e.lon_e6 != 0;
|
||||
struct Act { const char* icon; const char* text; uint8_t action; bool accent; } list[9];
|
||||
int n = 0;
|
||||
if (contact && e.type == ADV_TYPE_CHAT) list[n++] = { LV_SYMBOL_ENVELOPE, " Message", NODE_MSG, true };
|
||||
if (e.has_key) list[n++] = { LV_SYMBOL_LOOP, " Ping", NODE_PING, false };
|
||||
if (pos) list[n++] = { UI_SYMBOL_COMPASS, "", NODE_NAV, false };
|
||||
if (pos) list[n++] = { UI_SYMBOL_FLAG, "", NODE_WAYPOINT, false }; // save where it was seen
|
||||
if (contact) list[n++] = { UI_SYMBOL_STAR, admin ? "" : e.fav ? " Unfav" : " Fav", NODE_FAV, e.fav }; // lit while it is one
|
||||
if (admin) list[n++] = { LV_SYMBOL_SETTINGS, " Admin", NODE_ADMIN, false };
|
||||
if (!contact && e.has_key && !e.is_known) list[n++] = { LV_SYMBOL_PLUS, " Add", NODE_ADD, true };
|
||||
if (contact && e.has_key) list[n++] = { UI_SYMBOL_TACK, "", NODE_PIN, favslots::findContact(_prefs, e.pub_key) >= 0 };
|
||||
if (contact) list[n++] = { LV_SYMBOL_TRASH, "", NODE_DELETE, false };
|
||||
for (int i = 0; i < n; i++) { // five or more: icons only, so every button fits one row
|
||||
char t[24];
|
||||
snprintf(t, sizeof(t), "%s%s", list[i].icon, n >= 5 ? "" : list[i].text);
|
||||
lv_obj_t* b = barButton(acts, t, onNodeAction, list[i].action, list[i].accent);
|
||||
if (list[i].action == NODE_DELETE) _node_delete_lbl = lv_obj_get_child(b, 0);
|
||||
if (contact && e.type == ADV_TYPE_CHAT) barButton(acts, LV_SYMBOL_ENVELOPE " Message", onNodeAction, NODE_MSG, true);
|
||||
if (admin) barButton(acts, LV_SYMBOL_SETTINGS " Admin", onNodeAction, NODE_ADMIN, true);
|
||||
if (!contact && e.has_key && !e.is_known) barButton(acts, LV_SYMBOL_PLUS " Add", onNodeAction, NODE_ADD, true);
|
||||
if (e.has_key) barButton(acts, LV_SYMBOL_LOOP " Ping", onNodeAction, NODE_PING);
|
||||
if (pos) barButton(acts, UI_SYMBOL_COMPASS " Navigate", onNodeAction, NODE_NAV);
|
||||
|
||||
if (pos || contact) {
|
||||
lv_obj_t* g = group(info, "More");
|
||||
if (pos) actionRow(g, UI_SYMBOL_FLAG " Save as waypoint", onNodeAction, (void*)(uintptr_t)NODE_WAYPOINT);
|
||||
if (contact) s_nd_fav = actionRow(g, "", onNodeAction, (void*)(uintptr_t)NODE_FAV);
|
||||
nodeFavText();
|
||||
if (contact && e.has_key) actionRow(g, UI_SYMBOL_TACK " Pin to Home", onNodeAction, (void*)(uintptr_t)NODE_PIN);
|
||||
if (contact) _node_delete_lbl = actionRow(g, LV_SYMBOL_TRASH " Delete contact", onNodeAction,
|
||||
(void*)(uintptr_t)NODE_DELETE, theme::FAIL);
|
||||
}
|
||||
|
||||
refreshNode();
|
||||
@@ -3717,11 +3767,13 @@ void UITask::refreshNode() {
|
||||
if (e.fav) tag("favourite");
|
||||
if (status[0] >= 'a' && status[0] <= 'z') status[0] -= 'a' - 'A';
|
||||
int32_t lat, lon;
|
||||
int bearing = -1;
|
||||
if (e.lat_e6 != 0 || e.lon_e6 != 0) {
|
||||
if (_nearby->ownPosition(lat, lon) && e.dist_km >= 0.0f) {
|
||||
char d[16];
|
||||
geo::fmtDist(d, sizeof(d), e.dist_km, _prefs && _prefs->units_imperial);
|
||||
int az = geo::bearingDeg(lat, lon, e.lat_e6, e.lon_e6);
|
||||
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);
|
||||
@@ -3741,6 +3793,11 @@ void UITask::refreshNode() {
|
||||
infoSet(s_nd_heard, heard);
|
||||
infoSet(s_nd_signal, sig);
|
||||
infoSet(s_nd_id, id);
|
||||
ContactInfo ci;
|
||||
int hops = -1;
|
||||
if (!_node_from_scan && e.contact_idx >= 0 && the_mesh.getContactByIdx(e.contact_idx, ci))
|
||||
hops = ci.out_path_len == 0xFF ? 0xFF : (ci.out_path_len & 63);
|
||||
nodeVizSet(bearing, hops);
|
||||
|
||||
// Ping result (PingEngine releases its slot on reply; the view owns the timeout)
|
||||
if (_pinging) {
|
||||
@@ -3779,9 +3836,10 @@ void UITask::nodeAction(uint8_t action) {
|
||||
break;
|
||||
}
|
||||
case NODE_FAV:
|
||||
if (contactctl::setFavourite(e.pub_key, !e.fav)) {
|
||||
if (contactctl::setFavourite(e.pub_key, !e.fav)) { // in place: the list stays where it was scrolled
|
||||
e.fav = !e.fav;
|
||||
buildNode();
|
||||
nodeFavText();
|
||||
refreshNode();
|
||||
}
|
||||
break;
|
||||
case NODE_ADD:
|
||||
@@ -3819,10 +3877,7 @@ void UITask::nodeAction(uint8_t action) {
|
||||
pinPopup(false, 0, e.pub_key);
|
||||
break;
|
||||
case NODE_DELETE:
|
||||
if (!tapConfirmed(_node_delete_lbl, LV_SYMBOL_TRASH "?")) { // "?" fits an icon-only button
|
||||
showToast("Tap again to delete the contact", 2500);
|
||||
break;
|
||||
}
|
||||
if (!tapConfirmed(_node_delete_lbl, LV_SYMBOL_TRASH " Tap again to delete")) break;
|
||||
if (the_mesh.deleteContactByKey(e.pub_key)) {
|
||||
_screen = SCR_NEARBY;
|
||||
buildNearby();
|
||||
@@ -3833,6 +3888,7 @@ void UITask::nodeAction(uint8_t action) {
|
||||
|
||||
static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub, uint8_t* pref); // below
|
||||
#include "MapScreen.h"
|
||||
#include "ProfileView.h"
|
||||
#include "NavMap.h"
|
||||
#include "MapAreas.h"
|
||||
#include "MapRegions.h"
|
||||
@@ -4924,6 +4980,8 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
||||
if (!strcmp(name, "regions")) { s_ui->mapRegionsPopup(); return; }
|
||||
if (!strcmp(name, "region0")) { s_ui->mapRegionPopup(0); return; }
|
||||
if (!strcmp(name, "advert")) { s_ui->advertPopup(); return; }
|
||||
if (!strcmp(name, "trailsave")) { s_ui->navToolAction(navmap::TL_TRAIL_SAVE); return; } // Map tools > Save
|
||||
if (!strcmp(name, "savedtrail0")) { navmap::scanTrails(); s_ui->savedTrailPopup(0); return; }
|
||||
if (!strncmp(name, "trail@", 6)) { // "trail@name.trl" from /sdcard/trails: Load, as the saved-trail popup does
|
||||
navmap::scanTrails();
|
||||
for (int i = 0; i < navmap::s_st_n; i++)
|
||||
|
||||
@@ -432,6 +432,8 @@ private:
|
||||
void buildDiag();
|
||||
void diagSection(int sec);
|
||||
void refreshDiag();
|
||||
void sampleHistory(); // DiagScreen.h: History's readings, from loop()
|
||||
void fillHistory();
|
||||
void buildCompass();
|
||||
void refreshCompass();
|
||||
void favPickPopup(int slot);
|
||||
|
||||
Reference in New Issue
Block a user