feat(route): path editor with out-and-back trace and a kept route; map markers as one drawn layer

Core: PathEditor (UiCore::route) loads, edits, traces and saves the route to
a contact; MyMesh::sendTrace over an explicit route with per-hop SNRs,
setContactPath / setContactPinned. A route can be kept
(CONTACT_FLAG_ROUTE_PINNED, ContactInfo::flags bit 4): a path heard from the
node doesn't replace it and the last DM resend doesn't drop it.

ui-lvgl: Route screen (node detail), repeater picker by distance, picking
hops by tapping the nodes map (numbered badges, line, Undo / Done). The
map's markers are one drawn layer with hit-testing and name decluttering
instead of three objects per node.

ui-new: Path / trace view in the Nodes action menu; Add reuses the list
filtered to repeaters.

First cut: built for L2 LVGL, L1 OLED and L1 e-ink, tried on L2 only.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-13 10:18:05 +02:00
co-authored by Claude Sonnet 5.5
parent c180814960
commit 140ea7c008
17 changed files with 1306 additions and 76 deletions
+377 -52
View File
@@ -160,13 +160,37 @@ static void fillAroundMap(lv_event_t* e) {
if (a.x1 <= a.x2 && a.y1 <= a.y2) lv_draw_rect(layer, &d, &a);
}
// Markers, remembered so layout can reposition them. `idx` is the Nearby row
// Markers: one drawn layer, not an object per marker (64 nodes were ~190
// objects to lay out and invalidate on every pan). `idx` is the Nearby row
// (MK_NODE), the waypoint index (MK_WAYPOINT) or the live-share slot (MK_LIVE).
// layoutMapMarks() fills the placement (dot centre, name pill) from the view;
// the layer's draw callback paints it; a tap is hit-tested by hitMark().
enum : uint8_t { MK_NODE, MK_WAYPOINT, MK_LIVE };
struct Mark { int32_t lat_e6, lon_e6; int idx; uint8_t kind; uint32_t col, text_hash; lv_obj_t* obj; lv_obj_t* dot; };
static const int MARK_NAME = 31;
struct Mark {
int32_t lat_e6, lon_e6; int idx; uint8_t kind; uint32_t col, text_hash;
char name[MARK_NAME + 1];
int16_t tw; // the name's width in px
int16_t x, y; // dot centre, layer px (valid when vis)
int16_t lx, ly, lw, lh; // name pill, layer px (lw 0: no room for it)
uint8_t order; // 1.. while it is a hop of the route being picked, else 0
bool vis; // inside the view
};
// What was drawn for a slot last time, to repaint only what changed.
struct MarkSig { int16_t x, y, lx, ly, lw; uint32_t col, hash; uint8_t order; bool vis; };
static const int MAX_MARKS = NearbyModel::MAX_NEARBY > WaypointStore::CAPACITY + LiveTrackStore::CAPACITY
? NearbyModel::MAX_NEARBY : WaypointStore::CAPACITY + LiveTrackStore::CAPACITY; // Nearby map, or waypoints + live shares
static Mark* s_marks = psramBuf<Mark>(MAX_MARKS);
static MarkSig* s_sigs = psramBuf<MarkSig>(MAX_MARKS);
static int s_sig_n = 0;
static lv_obj_t* s_mark_layer = nullptr;
// The route being picked (Route > Map): its points as lat/lon, and in layer px
// from the last layout, drawn as a line under the markers.
static const int ROUTE_PTS = 10;
static int32_t s_route_lat[ROUTE_PTS], s_route_lon[ROUTE_PTS];
static int s_route_n = 0;
static lv_point_precise_t s_route_px[ROUTE_PTS];
static bool s_route_on = false;
static int s_mark_count = 0;
static int s_mark_prev = 0; // during a rebuild: how many the last one left, reused where unchanged
static int s_drag = 0; // px moved in the current press: a drag isn't a tap
@@ -235,6 +259,100 @@ static bool prefetchOne(double left, double top, int w, int h, int z) {
}
static const int MARK_D = 12; // marker dot diameter
static const int MARK_LABELS_MAX = 20; // names shown at once: the rest stay dots
// LV_EVENT_DRAW_MAIN of the marker layer: the name pills, then the dots over them.
static void drawMarks(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);
if (s_route_on && s_route_n >= 2) {
lv_draw_line_dsc_t ln;
lv_draw_line_dsc_init(&ln);
ln.color = lv_color_hex(theme::ACCENT);
ln.width = 3;
ln.round_start = ln.round_end = 1;
for (int i = 0; i + 1 < s_route_n; i++) {
ln.p1 = { s_route_px[i].x + a.x1, s_route_px[i].y + a.y1 };
ln.p2 = { s_route_px[i + 1].x + a.x1, s_route_px[i + 1].y + a.y1 };
lv_draw_line(layer, &ln);
}
}
lv_draw_rect_dsc_t pill;
lv_draw_rect_dsc_init(&pill);
pill.bg_color = lv_color_hex(theme::BG);
pill.bg_opa = LV_OPA_COVER;
pill.radius = 4;
lv_draw_label_dsc_t ld;
lv_draw_label_dsc_init(&ld);
ld.font = THEME_FONT_SMALL;
ld.color = lv_color_hex(theme::TEXT);
for (int k = 0; k < s_mark_count; k++) {
const Mark& m = s_marks[k];
if (!m.vis || !m.lw) continue;
lv_area_t pa = { a.x1 + m.lx, a.y1 + m.ly, a.x1 + m.lx + m.lw - 1, a.y1 + m.ly + m.lh - 1 };
lv_draw_rect(layer, &pill, &pa);
lv_area_t ta = { pa.x1 + 7, pa.y1 + 1, pa.x2 - 7, pa.y2 - 1 };
ld.text = m.name;
lv_draw_label(layer, &ld, &ta);
}
lv_draw_rect_dsc_t dot;
lv_draw_rect_dsc_init(&dot);
dot.bg_opa = LV_OPA_COVER;
dot.radius = LV_RADIUS_CIRCLE;
dot.border_color = lv_color_hex(theme::BG);
dot.border_opa = LV_OPA_COVER;
dot.border_width = 2;
const int half = MARK_D / 2;
for (int k = 0; k < s_mark_count; k++) {
const Mark& m = s_marks[k];
if (!m.vis) continue;
dot.bg_color = lv_color_hex(m.col);
lv_area_t da = { a.x1 + m.x - half, a.y1 + m.y - half, a.x1 + m.x + half - 1, a.y1 + m.y + half - 1 };
lv_draw_rect(layer, &dot, &da);
}
// Hop numbers of the route being picked: a badge at the dot's top right.
lv_draw_rect_dsc_t badge;
lv_draw_rect_dsc_init(&badge);
badge.bg_color = lv_color_hex(theme::ACCENT);
badge.bg_opa = LV_OPA_COVER;
badge.radius = LV_RADIUS_CIRCLE;
lv_draw_label_dsc_t bl;
lv_draw_label_dsc_init(&bl);
bl.font = THEME_FONT_SMALL;
bl.color = lv_color_hex(theme::BG);
bl.align = LV_TEXT_ALIGN_CENTER;
for (int k = 0; k < s_mark_count; k++) {
const Mark& m = s_marks[k];
if (!m.vis || !m.order) continue;
lv_area_t ba = { a.x1 + m.x + 1, a.y1 + m.y - 17, a.x1 + m.x + 15, a.y1 + m.y - 3 };
lv_draw_rect(layer, &badge, &ba);
char t[4];
snprintf(t, sizeof(t), "%d", m.order);
bl.text = t;
bl.text_local = true; // t is on this stack
lv_area_t ta = { ba.x1, ba.y1 + 0, ba.x2, ba.y2 };
lv_draw_label(layer, &bl, &ta);
}
}
// The marker at a point of the map area (px relative to it): a name pill under
// the point, else the nearest dot within a finger's reach; -1 if none.
static int hitMark(int px, int py) {
for (int k = 0; k < s_mark_count; k++) {
const Mark& m = s_marks[k];
if (m.vis && m.lw && px >= m.lx - 3 && px < m.lx + m.lw + 3 && py >= m.ly - 3 && py < m.ly + m.lh + 3) return k;
}
int best = -1, best_d = 18 * 18 + 1;
for (int k = 0; k < s_mark_count; k++) {
const Mark& m = s_marks[k];
if (!m.vis) continue;
int d = (m.x - px) * (m.x - px) + (m.y - py) * (m.y - py);
if (d < best_d) { best_d = d; best = k; }
}
return best;
}
} // namespace mapview
@@ -280,9 +398,14 @@ static void onDlResume(lv_event_t* e) { (void)e; s_ui->mapDownloadResume(); }
static void onDlDiscard(lv_event_t* e) { (void)e; s_ui->mapDownloadDiscard(); }
static void onNavTools(lv_event_t* e); // NavMap.h
static void onMapCredits(lv_event_t* e) { (void)e; s_ui->showToast(creditText(mapview::s_provider->attribution()), 4000); }
static void onMapMarker(lv_event_t* e) {
if (mapview::s_drag > 8) return; // the press was a pan that ended on a marker
s_ui->mapOpenMarker((int)(uintptr_t)lv_event_get_user_data(e));
static void onMapClick(lv_event_t* e) {
if (mapview::s_drag > 8) return; // the press was a pan
lv_point_t p;
lv_indev_get_point(lv_indev_active(), &p);
lv_area_t a;
lv_obj_get_coords((lv_obj_t*)lv_event_get_target(e), &a);
int k = mapview::hitMark(p.x - a.x1, p.y - a.y1);
if (k >= 0) s_ui->mapOpenMarker(k);
}
// The centre-on-me button in the accent colour while the map follows you.
@@ -394,6 +517,7 @@ void UITask::buildMap() {
lv_obj_add_event_cb(body, onMapPress, LV_EVENT_PRESSED, NULL);
lv_obj_add_event_cb(body, onMapPress, LV_EVENT_PRESSING, NULL);
lv_obj_add_event_cb(body, onMapLongPress, LV_EVENT_LONG_PRESSED, NULL);
lv_obj_add_event_cb(body, onMapClick, LV_EVENT_CLICKED, NULL); // a marker, hit-tested
_map_area = body;
for (int i = 0; i < mapview::GRID_COLS * mapview::GRID_ROWS; i++) {
@@ -431,6 +555,14 @@ void UITask::buildMap() {
lv_obj_remove_flag(_map_me, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_flag(_map_me, LV_OBJ_FLAG_HIDDEN);
mapview::s_mark_layer = lv_obj_create(_map_marks); // markers over the trail, target and own position
lv_obj_remove_style_all(mapview::s_mark_layer);
lv_obj_set_size(mapview::s_mark_layer, LV_PCT(100), LV_PCT(100));
lv_obj_remove_flag(mapview::s_mark_layer, LV_OBJ_FLAG_CLICKABLE);
lv_obj_remove_flag(mapview::s_mark_layer, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(mapview::s_mark_layer, mapview::drawMarks, LV_EVENT_DRAW_MAIN, NULL);
mapview::s_sig_n = 0;
areaBuildLayer(body); // download frame / area outlines, over the tiles (MapAreas.h)
// Controls
@@ -499,6 +631,8 @@ void UITask::buildMap() {
lv_obj_align(_map_hint, LV_ALIGN_TOP_MID, 0, 50); // clear of own position, mid-map
lv_obj_add_flag(_map_hint, LV_OBJ_FLAG_HIDDEN);
if (_map_nav) buildNavControls(body);
_route_bar = _route_lbl = nullptr;
if (_route_pick) buildRouteBar(body);
layoutNow(body);
mapview::s_mark_count = 0; // the previous markers went with the previous screen
@@ -587,14 +721,7 @@ void UITask::layoutMap() {
lv_obj_add_flag(_map_me, LV_OBJ_FLAG_HIDDEN);
}
followShown(_map_center_btn, _map_follow);
const int half = mapview::MARK_D / 2;
for (int k = 0; k < mapview::s_mark_count; k++) {
const mapview::Mark& m = mapview::s_marks[k];
int dy = lv_obj_get_y(m.dot); // the dot within the marker (vertically centred on the name)
double x = mapview::lonToTileX(m.lon_e6 / 1e6, _map_z) * mapview::TILE_PX - left;
double y = mapview::latToTileY(m.lat_e6 / 1e6, _map_z) * mapview::TILE_PX - top;
lv_obj_set_pos(m.obj, (int)lround(x) - half, (int)lround(y) - half - dy); // the dot's centre on the spot
}
layoutMapMarks(left, top, w, h);
if (_map_nav) layoutNav();
if (over) setTextFmt(_map_zoom_lbl, "z%d (map z%d)", _map_z, _map_z - over); // magnified
@@ -696,9 +823,7 @@ void UITask::rebuildMapMarkers() {
mapview::s_mark_count = 0;
if (_map_nav) rebuildNavMarkers();
if (_map_nodes || !_map_nav) rebuildNodeMarkers();
for (int k = mapview::s_mark_count; k < mapview::s_mark_prev; k++) lv_obj_delete(mapview::s_marks[k].obj);
mapview::s_mark_prev = 0;
layoutNow(_map_marks); // so layoutMap() knows where each dot sits in its marker
}
void UITask::rebuildNodeMarkers() {
@@ -712,48 +837,142 @@ void UITask::rebuildNodeMarkers() {
}
}
// One clickable object per marker: a dot plus the name beside it. The object
// is placed so the dot's centre is on the spot, from where layout actually put
// the dot inside it (Mark::dot) -- the name may be taller than the dot. The
// markers are rebuilt every few seconds: one the same as the last time in its
// slot is kept as it is, so an unchanged map isn't redrawn under it.
// A marker is a record: where it is, its colour and name. Placement and
// painting are layoutMapMarks() and mapview::drawMarks(). The name's width is
// measured here, not per pan: a slot whose name is unchanged keeps it.
void UITask::addMapMark(uint8_t kind, int idx, int32_t lat_e6, int32_t lon_e6, uint32_t col, const char* text) {
if (!_map_marks || mapview::s_mark_count >= mapview::MAX_MARKS) return;
uint32_t h = 2166136261u; // FNV-1a of the name
for (const char* c = text; *c; c++) h = (h ^ (uint8_t)*c) * 16777619u;
int k = mapview::s_mark_count;
if (k < mapview::s_mark_prev) {
mapview::Mark& old = mapview::s_marks[k];
if (old.kind == kind && old.idx == idx && old.lat_e6 == lat_e6 && old.lon_e6 == lon_e6 && old.col == col && old.text_hash == h) {
mapview::s_mark_count++;
return;
}
lv_obj_delete(old.obj);
int k = mapview::s_mark_count++;
mapview::Mark& mk = mapview::s_marks[k];
bool same_name = k < mapview::s_mark_prev && mk.text_hash == h;
mk.lat_e6 = lat_e6; mk.lon_e6 = lon_e6; mk.idx = idx; mk.kind = kind; mk.col = col; mk.text_hash = h;
mk.vis = false; mk.lw = 0; mk.order = 0;
if (same_name) return;
size_t n = strlen(text);
if (n > (size_t)mapview::MARK_NAME) { // cut at a character boundary
n = mapview::MARK_NAME;
while (n > 0 && ((uint8_t)text[n] & 0xC0) == 0x80) n--;
}
lv_obj_t* m = lv_obj_create(_map_marks);
lv_obj_remove_style_all(m);
lv_obj_set_size(m, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(m, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(m, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(m, 3, 0);
lv_obj_add_flag(m, LV_OBJ_FLAG_EVENT_BUBBLE); // a drag starting here still pans
lv_obj_add_event_cb(m, onMapMarker, LV_EVENT_CLICKED, (void*)(uintptr_t)mapview::s_mark_count);
lv_obj_t* dot = lv_obj_create(m);
lv_obj_remove_style_all(dot);
lv_obj_set_size(dot, mapview::MARK_D, mapview::MARK_D);
lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(dot, lv_color_hex(col), 0);
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(dot, lv_color_hex(theme::BG), 0);
lv_obj_set_style_border_width(dot, 2, 0);
lv_obj_remove_flag(dot, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t* name = mapPill(m, text);
lv_obj_remove_flag(name, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_pad_ver(name, 1, 0); // a marker's name stays small
lv_obj_set_style_radius(name, 4, 0);
memcpy(mk.name, text, n);
mk.name[n] = '\0';
lv_point_t sz;
lv_text_get_size(&sz, mk.name, THEME_FONT_SMALL, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE);
mk.tw = (int16_t)sz.x;
}
mapview::Mark& mk = mapview::s_marks[mapview::s_mark_count++];
mk.lat_e6 = lat_e6; mk.lon_e6 = lon_e6; mk.idx = idx; mk.kind = kind; mk.col = col; mk.text_hash = h; mk.obj = m; mk.dot = dot;
// Places the markers for the view: a dot on each spot inside it, and the name
// beside it (right, left, above, below) where it covers neither the map's
// controls, nor another name, nor another dot -- in marker order, so
// waypoints and live shares keep their names before the many nodes. Only the
// markers that changed since the last layout are repainted: a rebuild that
// changed nothing leaves the tiles under them alone.
void UITask::layoutMapMarks(double left, double top, int w, int h) {
using namespace mapview;
if (!s_mark_layer) return;
const int half = MARK_D / 2;
const int lh = lv_font_get_line_height(THEME_FONT_SMALL) + 2;
struct Box { int16_t x1, y1, x2, y2; };
static Box used[MAX_MARKS + 8];
int nu = 0;
const int bottom = _map_nav ? navmap::BAR_H : 0;
const int16_t W = (int16_t)w, H = (int16_t)(h - bottom);
used[nu++] = { 0, 0, 50, 144 }; // button columns
used[nu++] = { (int16_t)(W - 50), 0, W, 144 };
used[nu++] = { 0, 0, W, 36 }; // the going pills
used[nu++] = { 0, (int16_t)(H - 34), 180, (int16_t)h }; // credit / scale
used[nu++] = { (int16_t)(W - 50), (int16_t)(H - 50), W, (int16_t)h }; // centre button
if (bottom) used[nu++] = { 0, H, W, (int16_t)h };
for (int k = 0; k < s_mark_count; k++) {
Mark& m = s_marks[k];
double x = lonToTileX(m.lon_e6 / 1e6, _map_z) * TILE_PX - left;
double y = latToTileY(m.lat_e6 / 1e6, _map_z) * TILE_PX - top;
m.vis = x > -half - 4 && x < w + half + 4 && y > -half - 4 && y < h + half + 4;
m.lw = 0;
m.order = 0;
if (_route_pick && m.vis && m.kind == MK_NODE && m.idx < _nearby->count()) {
const NearbyModel::Entry& ne = _nearby->at(m.idx);
int at = ne.has_key ? _core->route.indexOf(ne.pub_key) : -1;
if (at >= 0) m.order = (uint8_t)(at + 1);
}
if (!m.vis) continue;
m.x = (int16_t)lround(x);
m.y = (int16_t)lround(y);
used[nu++] = { (int16_t)(m.x - half), (int16_t)(m.y - half), (int16_t)(m.x + half), (int16_t)(m.y + half) };
}
auto clearOf = [&](int x1, int y1, int x2, int y2) {
if (x1 < 2 || y1 < 2 || x2 > w - 2 || y2 > h - 2) return false;
for (int i = 0; i < nu; i++)
if (x1 <= used[i].x2 && x2 >= used[i].x1 && y1 <= used[i].y2 && y2 >= used[i].y1) return false;
return true;
};
int named = 0;
for (int k = 0; k < s_mark_count && named < MARK_LABELS_MAX; k++) {
Mark& m = s_marks[k];
if (!m.vis || !m.name[0]) continue;
int lw = m.tw + 14;
const struct { int x, y; } at[4] = {
{ m.x + half + 3, m.y - lh / 2 }, { m.x - half - 3 - lw, m.y - lh / 2 },
{ m.x - lw / 2, m.y - half - 2 - lh }, { m.x - lw / 2, m.y + half + 2 } };
for (const auto& c : at) {
if (!clearOf(c.x, c.y, c.x + lw - 1, c.y + lh - 1)) continue;
m.lx = (int16_t)c.x; m.ly = (int16_t)c.y; m.lw = (int16_t)lw; m.lh = (int16_t)lh;
used[nu++] = { m.lx, m.ly, (int16_t)(m.lx + lw - 1), (int16_t)(m.ly + lh - 1) };
named++;
break;
}
}
// What changed since the last layout, repainted (a pan changes all: the layer at once).
lv_area_t la;
lv_obj_get_coords(s_mark_layer, &la);
auto bbox = [&](int x, int y, int lx, int ly, int lw, lv_area_t& a) {
int x1 = x - half, y1 = y - half, x2 = x + half, y2 = y + half;
if (lw) { x1 = min(x1, lx); y1 = min(y1, ly); x2 = max(x2, lx + lw); y2 = max(y2, ly + lh); }
a = { la.x1 + x1 - 1, la.y1 + y1 - 1, la.x1 + x2 + 1, la.y1 + y2 + 1 };
};
s_route_on = _route_pick;
if (_route_pick) { // the route's points, in px (any distance away: the line is clipped by the layer)
for (int i = 0; i < s_route_n; i++) {
double rx = lonToTileX(s_route_lon[i] / 1e6, _map_z) * TILE_PX - left;
double ry = latToTileY(s_route_lat[i] / 1e6, _map_z) * TILE_PX - top;
s_route_px[i] = { (lv_value_precise_t)lround(max(-30000.0, min(30000.0, rx))), (lv_value_precise_t)lround(max(-30000.0, min(30000.0, ry))) };
}
lv_obj_invalidate(s_mark_layer); // the line may have moved: a pan repaints it anyway
}
int changed = 0;
const int n = max(s_mark_count, s_sig_n);
for (int k = 0; k < n; k++) {
bool have = k < s_mark_count, had = k < s_sig_n;
const Mark* m = have ? &s_marks[k] : nullptr;
MarkSig* g = had ? &s_sigs[k] : nullptr;
bool now_vis = have && m->vis, was_vis = had && g->vis;
bool same = now_vis == was_vis && (!now_vis || (m->x == g->x && m->y == g->y && m->lx == g->lx && m->ly == g->ly &&
m->lw == g->lw && m->col == g->col && m->text_hash == g->hash && m->order == g->order));
if (!same) changed++;
}
if (changed > 6) {
lv_obj_invalidate(s_mark_layer);
} else if (changed) {
for (int k = 0; k < n; k++) {
bool have = k < s_mark_count, had = k < s_sig_n;
const Mark* m = have ? &s_marks[k] : nullptr;
MarkSig* g = had ? &s_sigs[k] : nullptr;
bool now_vis = have && m->vis, was_vis = had && g->vis;
if (now_vis == was_vis && (!now_vis || (m->x == g->x && m->y == g->y && m->lx == g->lx && m->ly == g->ly &&
m->lw == g->lw && m->col == g->col && m->text_hash == g->hash && m->order == g->order))) continue;
lv_area_t a;
if (was_vis) { bbox(g->x, g->y, g->lx, g->ly, g->lw, a); lv_obj_invalidate_area(s_mark_layer, &a); }
if (now_vis) { bbox(m->x, m->y, m->lx, m->ly, m->lw, a); lv_obj_invalidate_area(s_mark_layer, &a); }
}
}
for (int k = 0; k < s_mark_count; k++) {
const Mark& m = s_marks[k];
s_sigs[k] = { m.x, m.y, m.lx, m.ly, m.lw, m.col, m.text_hash, m.order, m.vis };
}
s_sig_n = s_mark_count;
}
void UITask::mapPan(int dx, int dy) {
@@ -787,6 +1006,13 @@ void UITask::mapCenterOnMe() {
void UITask::mapOpenMarker(int idx) {
if (idx < 0 || idx >= mapview::s_mark_count) return;
const mapview::Mark& m = mapview::s_marks[idx];
if (_route_pick) { // Route > Map: a repeater tapped in (or out again)
if (m.kind != mapview::MK_NODE || m.idx >= _nearby->count()) return;
const NearbyModel::Entry& e = _nearby->at(m.idx);
if (!e.has_key) { showToast("Pick repeaters"); return; }
routeMapTap(e.pub_key, e.type);
return;
}
if (m.kind == mapview::MK_WAYPOINT) { navWaypointMenu(m.idx); return; }
if (m.kind == mapview::MK_LIVE) { navPick(navmap::code(navmap::T_LIVE, m.idx)); return; }
openNode(m.idx);
@@ -804,7 +1030,106 @@ void UITask::mapPointToLatLon(int x, int y, int32_t& lat_e6, int32_t& lon_e6) co
}
void UITask::mapLongPress(int x, int y) {
if (_map_nav) navDropAt(x, y);
if (_map_nav && !_route_pick) navDropAt(x, y);
}
// ── Picking a route (Route > Map) ─────────────────────────────────────────────
static void onRouteUndo(lv_event_t* e) { (void)e; s_ui->routeMapUndo(); }
static void onRouteDone(lv_event_t* e) { (void)e; s_ui->routeMapDone(); }
// A bar over the nav bar: the route so far, Undo (last hop out) and Done.
void UITask::buildRouteBar(lv_obj_t* body) {
_route_bar = lv_obj_create(body);
lv_obj_set_size(_route_bar, LV_PCT(100), navmap::BAR_H);
lv_obj_align(_route_bar, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_remove_flag(_route_bar, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_radius(_route_bar, 0, 0);
lv_obj_set_style_bg_color(_route_bar, lv_color_hex(theme::BG), 0);
lv_obj_set_style_bg_opa(_route_bar, LV_OPA_COVER, 0);
lv_obj_set_style_border_side(_route_bar, LV_BORDER_SIDE_TOP, 0);
lv_obj_set_style_border_color(_route_bar, lv_color_hex(theme::ACCENT), 0);
lv_obj_set_style_border_width(_route_bar, 2, 0);
lv_obj_set_style_pad_all(_route_bar, 4, 0);
_route_lbl = label(_route_bar, "", THEME_FONT_SMALL, theme::TEXT);
lv_label_set_long_mode(_route_lbl, LV_LABEL_LONG_WRAP);
lv_obj_set_width(_route_lbl, 150);
lv_obj_align(_route_lbl, LV_ALIGN_LEFT_MID, 2, 0);
lv_obj_t* done = lv_button_create(_route_bar);
lv_obj_set_size(done, 70, 34);
lv_obj_align(done, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_shadow_width(done, 0, 0);
lv_obj_set_style_radius(done, theme::RADIUS, 0);
lv_obj_add_event_cb(done, onRouteDone, LV_EVENT_CLICKED, NULL);
lv_obj_center(label(done, LV_SYMBOL_OK " Done", THEME_FONT_SMALL, theme::TEXT));
stylePrimary(done);
lv_obj_t* undo = lv_button_create(_route_bar);
lv_obj_set_size(undo, 64, 34);
lv_obj_align(undo, LV_ALIGN_RIGHT_MID, -76, 0);
lv_obj_set_style_shadow_width(undo, 0, 0);
lv_obj_set_style_radius(undo, theme::RADIUS, 0);
lv_obj_set_style_bg_color(undo, lv_color_hex(theme::SURFACE), 0);
lv_obj_add_event_cb(undo, onRouteUndo, LV_EVENT_CLICKED, NULL);
lv_obj_center(label(undo, LV_SYMBOL_CLOSE " Undo", THEME_FONT_SMALL, theme::TEXT));
routeMapRefresh();
}
void UITask::routeMapUndo() {
_core->route.removeLast();
routeMapRefresh();
}
// The route changed: its points for the line, the text, the markers.
void UITask::routeMapRefresh() {
using namespace mapview;
PathEditor& R = _core->route;
s_route_n = 0;
auto add = [&](int32_t lat, int32_t lon) {
if ((lat != 0 || lon != 0) && s_route_n < ROUTE_PTS) { s_route_lat[s_route_n] = lat; s_route_lon[s_route_n] = lon; s_route_n++; }
};
int32_t mlat, mlon;
if (_core->course.currentLocation(mlat, mlon)) add(mlat, mlon);
for (int i = 0; i < R.count(); i++) {
if (R.hop(i).known < 4) continue;
ContactInfo* c = the_mesh.lookupContactByPubKey(R.hop(i).key, 4);
if (c) add(c->gps_lat, c->gps_lon);
}
ContactInfo* d = the_mesh.lookupContactByPubKey(R.targetKey(), PUB_KEY_SIZE);
if (d) add(d->gps_lat, d->gps_lon);
if (_route_lbl) {
char t[96];
int o = snprintf(t, sizeof(t), "%d hop%s: ", R.count(), R.count() == 1 ? "" : "s");
for (int i = 0; i < R.count() && o < (int)sizeof(t) - 8; i++)
o += snprintf(t + o, sizeof(t) - o, "%s%.10s", i ? " > " : "", R.hop(i).name);
if (R.count() == 0) snprintf(t, sizeof(t), "Tap the repeaters in order, towards %.12s", R.targetName());
lv_label_set_text(_route_lbl, t);
}
if (_map_area) layoutMap();
}
// Frames this device and the node, so the hops between them are on screen.
void UITask::routeMapFrame() {
using namespace mapview;
routeMapRefresh();
if (s_route_n == 0 || !_map_area) return;
int32_t la0 = s_route_lat[0], la1 = s_route_lat[0], lo0 = s_route_lon[0], lo1 = s_route_lon[0];
for (int i = 1; i < s_route_n; i++) {
la0 = min(la0, s_route_lat[i]); la1 = max(la1, s_route_lat[i]);
lo0 = min(lo0, s_route_lon[i]); lo1 = max(lo1, s_route_lon[i]);
}
int w = lv_obj_get_width(_map_area), h = lv_obj_get_height(_map_area) - navmap::BAR_H;
int z = MAX_Z;
for (; z > MIN_Z; z--) {
double dx = fabs(lonToTileX(lo1 / 1e6, z) - lonToTileX(lo0 / 1e6, z)) * TILE_PX;
double dy = fabs(latToTileY(la0 / 1e6, z) - latToTileY(la1 / 1e6, z)) * TILE_PX;
if (dx < w * 0.7 && dy < h * 0.6) break;
}
_map_z = z;
_map_follow = false;
_map_cx = lonToTileX((lo0 + lo1) / 2e6, z);
_map_cy = latToTileY((la0 + la1) / 2e6, z) + mapCenterBias();
layoutMap();
}
+2 -3
View File
@@ -343,10 +343,9 @@ void UITask::layoutNav() {
lv_obj_set_pos(navmap::s_target_ring, tx - navmap::RING_D / 2, ty - navmap::RING_D / 2);
lv_obj_remove_flag(navmap::s_target_ring, LV_OBJ_FLAG_HIDDEN);
bool marked = false; // a waypoint / live marker already sits on the target
const int half = mapview::MARK_D / 2;
for (int k = 0; k < mapview::s_mark_count && !marked; k++) {
lv_obj_t* m = mapview::s_marks[k].obj;
marked = abs(lv_obj_get_x(m) + half - tx) <= 3 && abs(lv_obj_get_y(m) + lv_obj_get_y(mapview::s_marks[k].dot) + half - ty) <= 3;
const mapview::Mark& m = mapview::s_marks[k];
marked = m.vis && abs(m.x - tx) <= 3 && abs(m.y - ty) <= 3;
}
if (marked) lv_obj_add_flag(navmap::s_target_dot, LV_OBJ_FLAG_HIDDEN);
else lv_obj_remove_flag(navmap::s_target_dot, LV_OBJ_FLAG_HIDDEN);
+5 -1
View File
@@ -12,7 +12,7 @@ static NearbyModel::Entry s_node; // the node open in SCR_NODE (a copy: the li
// Its info card's values (UITask::_node_info is the card).
static lv_obj_t *s_nd_type, *s_nd_status, *s_nd_dist, *s_nd_pos, *s_nd_heard, *s_nd_signal, *s_nd_id;
enum : uint8_t { NODE_MSG, NODE_PING, NODE_FAV, NODE_ADD, NODE_DELETE, NODE_NAV, NODE_ADMIN, NODE_WAYPOINT, NODE_PIN,
NODE_RESET_PATH, NODE_SHARE };
NODE_RESET_PATH, NODE_SHARE, NODE_ROUTE };
static void onNearbyChip(lv_event_t* e) { s_ui->setNearbyFilter((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
static void onNearbySort(lv_event_t* e) { (void)e; s_ui->toggleNearbySort(); }
@@ -358,6 +358,7 @@ void UITask::buildNode() {
nodeFavText();
if (contact && e.has_key) actionRow(g, UI_SYMBOL_TACK " Pin to Home", onNodeAction, (void*)(uintptr_t)NODE_PIN);
ContactInfo* ci = (contact && e.has_key) ? the_mesh.lookupContactByPubKey(e.pub_key, PUB_KEY_SIZE) : nullptr;
if (ci) actionRow(g, UI_SYMBOL_ROUTE " Route and trace", onNodeAction, (void*)(uintptr_t)NODE_ROUTE);
if (ci && contactctl::hasPath(*ci))
_node_path_lbl = actionRow(g, LV_SYMBOL_REFRESH " Reset path", onNodeAction, (void*)(uintptr_t)NODE_RESET_PATH);
if (ci) actionRow(g, LV_SYMBOL_UPLOAD " Share with nodes in range", onNodeAction, (void*)(uintptr_t)NODE_SHARE);
@@ -524,6 +525,9 @@ void UITask::nodeAction(uint8_t action) {
refreshNode();
}
break;
case NODE_ROUTE:
showRoute(e.pub_key);
break;
case NODE_SHARE:
showToast(contactctl::share(e.pub_key) ? "Sent to nodes in range" : "No advert saved to send");
break;
@@ -0,0 +1,302 @@
#pragma once
// Route: the repeaters between this node and one contact, edited, traced and
// saved (node detail > Route). The list, the out-and-back trace and the save
// are the UI Core's PathEditor (`_core->route`); ui-new's Path screen is a
// view over the same. This draws the rows, a picker of repeaters by distance,
// and -- from the map -- picking the hops by tapping them (MapScreen.h,
// `_route_pick`).
//
// Single-TU fragment: included by ui-lvgl/UITask.cpp after AdminScreen.h.
namespace routeview {
enum : uint8_t { RT_ADD, RT_MAP, RT_TRACE, RT_SAVE, RT_PIN };
enum : uint8_t { OP_UP, OP_DOWN, OP_DEL };
static const int PICK_MAX = 96;
struct Pick { uint16_t idx; float km; }; // raw contact index, distance (<0: no position)
static Pick* s_pick = psramBuf<Pick>(PICK_MAX);
static int s_pick_n = 0;
static int cmpPick(const void* a, const void* b) {
float x = ((const Pick*)a)->km, y = ((const Pick*)b)->km;
if ((x < 0) != (y < 0)) return x < 0 ? 1 : -1; // those with no position last
return x < y ? -1 : x > y ? 1 : 0;
}
// A hop's text from an SNR x4.
static void snrText(char* out, size_t n, int16_t snr_x4) { snprintf(out, n, "%.1f", snr_x4 / 4.0f); }
} // namespace routeview
static void onRouteAct(lv_event_t* e) { s_ui->routeAct((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
static void onRouteOp(lv_event_t* e) { s_ui->routeOp((int)(uintptr_t)lv_event_get_user_data(e)); }
static void onRoutePickRow(lv_event_t* e) { s_ui->routePick((int)vlist::arg(e)); }
// A small square button on a hop's card: user data = op << 8 | hop index.
static lv_obj_t* routeSquare(lv_obj_t* card, const char* sym, int code) {
lv_obj_t* b = lv_button_create(card);
lv_obj_set_size(b, 30, 30);
lv_obj_set_style_pad_all(b, 0, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0);
lv_obj_add_event_cb(b, onRouteOp, LV_EVENT_CLICKED, (void*)(uintptr_t)code);
lv_obj_center(label(b, sym, THEME_FONT_SMALL, theme::TEXT));
return b;
}
// One row of the route: a number, the name and what the trace heard.
static lv_obj_t* routeCard(lv_obj_t* list, const char* num, const char* name, const char* sub, uint32_t col) {
lv_obj_t* c = lv_obj_create(list);
styleSurface(c, theme::SURFACE);
lv_obj_remove_flag(c, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(c, LV_PCT(100), 44);
lv_obj_set_style_radius(c, theme::RADIUS, 0);
lv_obj_set_style_pad_hor(c, 8, 0);
lv_obj_set_style_pad_ver(c, 0, 0);
lv_obj_set_flex_flow(c, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(c, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(c, 6, 0);
lv_obj_t* n = label(c, num, THEME_FONT_BODY, theme::ACCENT);
lv_obj_set_width(n, 18);
lv_obj_t* box = lv_obj_create(c);
lv_obj_remove_style_all(box);
lv_obj_set_height(box, LV_SIZE_CONTENT);
lv_obj_set_flex_grow(box, 1);
lv_obj_set_flex_flow(box, LV_FLEX_FLOW_COLUMN);
lv_obj_t* nm = label(box, name, THEME_FONT_BODY, col);
lv_label_set_long_mode(nm, LV_LABEL_LONG_DOT);
lv_obj_set_width(nm, LV_PCT(100));
if (sub && sub[0]) {
lv_obj_t* s = label(box, sub, THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(s, LV_LABEL_LONG_DOT);
lv_obj_set_width(s, LV_PCT(100));
}
return c;
}
// Node detail > Route.
void UITask::showRoute(const uint8_t* pub_key) {
if (!_core->route.open(pub_key)) { showToast("Not a contact"); return; }
_route_pick = false;
_screen = SCR_ROUTE;
buildRoute();
}
void UITask::buildRoute() {
lv_obj_t* body = newScreen("Route", true);
_route_status = noteLabel(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
_route_list = scrollList(body);
lv_obj_t* bar = buttonBar(body);
barButton(bar, LV_SYMBOL_PLUS " Add", onRouteAct, routeview::RT_ADD);
barButton(bar, UI_SYMBOL_MAP " Map", onRouteAct, routeview::RT_MAP);
_route_trace_btn = barButton(bar, LV_SYMBOL_LOOP " Trace", onRouteAct, routeview::RT_TRACE, true);
barButton(bar, LV_SYMBOL_OK " Save", onRouteAct, routeview::RT_SAVE);
_route_pin_btn = barButton(bar, UI_SYMBOL_TACK " Keep", onRouteAct, routeview::RT_PIN, _core->route.pinned());
_route_shown_state = 255;
routeRebuild();
}
// The rows and the line above them, from the editor's state.
void UITask::routeRebuild() {
if (!_route_list || !_route_status) return;
PathEditor& R = _core->route;
lv_obj_clean(_route_list);
const int n = R.count();
const bool done = R.state() == PathEditor::DONE;
char sub[48], a[12], b[12];
int16_t back = 0;
sub[0] = '\0';
if (done && R.snrBackX4(back)) { routeview::snrText(a, sizeof(a), back); snprintf(sub, sizeof(sub), "" LV_SYMBOL_LEFT " %s dB", a); }
routeCard(_route_list, "", "This device", sub, theme::TEXT_MUTED);
for (int i = 0; i < n; i++) {
int16_t out = 0, ret = 0;
sub[0] = '\0';
if (done && R.hopSnrX4(i, out)) {
routeview::snrText(a, sizeof(a), out);
if (R.hopSnrX4(2 * n - i, ret)) {
routeview::snrText(b, sizeof(b), ret);
snprintf(sub, sizeof(sub), "" LV_SYMBOL_RIGHT " %s " LV_SYMBOL_LEFT " %s dB", a, b);
} else {
snprintf(sub, sizeof(sub), "" LV_SYMBOL_RIGHT " %s dB", a);
}
} else if (R.hop(i).known < 4) {
snprintf(sub, sizeof(sub), "not a known contact");
}
char num[4];
snprintf(num, sizeof(num), "%d", i + 1);
lv_obj_t* c = routeCard(_route_list, num, R.hop(i).name, sub, theme::TEXT);
if (i > 0) routeSquare(c, LV_SYMBOL_UP, routeview::OP_UP << 8 | i);
if (i < n - 1) routeSquare(c, LV_SYMBOL_DOWN, routeview::OP_DOWN << 8 | i);
routeSquare(c, LV_SYMBOL_CLOSE, routeview::OP_DEL << 8 | i);
}
int16_t out = 0;
sub[0] = '\0';
if (done && R.hopSnrX4(n, out)) { routeview::snrText(a, sizeof(a), out); snprintf(sub, sizeof(sub), "" LV_SYMBOL_RIGHT " %s dB", a); }
routeCard(_route_list, "", R.targetName()[0] ? R.targetName() : "(unnamed)", sub, theme::ACCENT);
if (n == 0)
noteLabel(_route_list, R.hasStoredPath()
? "Direct: no repeaters between. Save with repeaters to route through them."
: "No route stored, so messages are flooded. Add the repeaters the node should be reached through.",
THEME_FONT_SMALL, theme::TEXT_MUTED);
routeStatus();
}
void UITask::routeStatus() {
PathEditor& R = _core->route;
char t[96];
switch (R.state()) {
case PathEditor::RUNNING: snprintf(t, sizeof(t), "Tracing %d hops, out and back...", R.traceEntries()); break;
case PathEditor::DONE: {
int16_t worst = 0;
if (R.worstOutX4(worst)) snprintf(t, sizeof(t), "Heard back in %lu ms - weakest hop %.1f dB", (unsigned long)R.rttMs(), worst / 4.0f);
else snprintf(t, sizeof(t), "Heard back in %lu ms", (unsigned long)R.rttMs());
break;
}
case PathEditor::TIMEOUT: snprintf(t, sizeof(t), "No reply: a hop didn't answer, or its answer wasn't heard here"); break;
case PathEditor::FAILED: snprintf(t, sizeof(t), "Can't trace: a repeater has no key known (add it from contacts)"); break;
default:
snprintf(t, sizeof(t), "Route to %s: %d repeater%s%s", R.targetName(), R.count(), R.count() == 1 ? "" : "s",
R.pinned() ? " (kept)" : "");
}
setText(_route_status, t);
if (_route_trace_btn) setText(lv_obj_get_child(_route_trace_btn, 0), R.state() == PathEditor::RUNNING ? LV_SYMBOL_CLOSE " Stop" : LV_SYMBOL_LOOP " Trace");
_route_shown_state = R.state();
}
// From loop() on SCR_ROUTE: a running trace's reply or timeout.
void UITask::routePoll() {
PathEditor& R = _core->route;
R.poll();
if (R.state() != _route_shown_state) routeRebuild();
}
void UITask::routeAct(uint8_t act) {
PathEditor& R = _core->route;
switch (act) {
case routeview::RT_ADD:
R.cancelTrace();
showRoutePick();
break;
case routeview::RT_MAP:
R.cancelTrace();
_route_pick = true;
openMap(false); // the nodes map: repeaters are tapped into the route
routeMapFrame();
break;
case routeview::RT_TRACE:
if (R.state() == PathEditor::RUNNING) { R.cancelTrace(); routeRebuild(); break; }
if (R.trace()) {
_next_route_ms = millis() + 200;
routeRebuild();
} else if (R.state() == PathEditor::FAILED) {
routeRebuild();
} else {
showToast("Busy, try again");
}
break;
case routeview::RT_PIN: // kept: not replaced by a path heard, nor dropped after a failed send
R.setPinned(!R.pinned());
barButtonOn(_route_pin_btn, R.pinned());
showToast(R.pinned() ? (R.hasStoredPath() ? "Route kept" : "Kept once you save it") : "Route is learned again");
break;
case routeview::RT_SAVE:
if (R.count() == 0) { showToast("Add a repeater first (Reset path floods)"); break; }
showToast(R.save() ? "Route saved" : "Can't save this route");
routeRebuild();
break;
}
}
void UITask::routeOp(int code) {
PathEditor& R = _core->route;
int i = code & 0xFF, op = code >> 8;
if (op == routeview::OP_UP) R.move(i, -1);
else if (op == routeview::OP_DOWN) R.move(i, +1);
else if (op == routeview::OP_DEL) R.remove(i);
routeRebuild();
}
// ── Picker: repeaters by distance ─────────────────────────────────────────────
void UITask::showRoutePick() {
_screen = SCR_ROUTE_PICK;
buildRoutePick();
}
void UITask::buildRoutePick() {
lv_obj_t* body = newScreen("Add repeater", true);
vlist::attach(body);
vlist::begin(body);
int32_t mlat, mlon;
bool own = _core->course.currentLocation(mlat, mlon);
routeview::s_pick_n = 0;
for (int i = 0; i < the_mesh.getNumContacts() && routeview::s_pick_n < routeview::PICK_MAX; i++) {
ContactInfo c;
if (!the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, c) || c.type != ADV_TYPE_REPEATER) continue;
bool has_pos = c.gps_lat != 0 || c.gps_lon != 0;
routeview::s_pick[routeview::s_pick_n++] = { (uint16_t)(MAX_ANON_CONTACTS + i),
own && has_pos ? geo::haversineKm(mlat, mlon, c.gps_lat, c.gps_lon) : -1.0f };
}
qsort(routeview::s_pick, routeview::s_pick_n, sizeof(routeview::Pick), routeview::cmpPick);
static const vlist::Kind ROW = {
theme::ROW_H,
[](lv_obj_t* list) { return vRow(list, onRoutePickRow, NULL); },
[](lv_obj_t* row, intptr_t i) {
ContactInfo c;
if (!the_mesh.getContactByIdx(routeview::s_pick[i].idx, c)) { rowSet(row, "?", NULL); return; }
char sub[40] = "";
float km = routeview::s_pick[i].km;
if (km >= 0) geo::fmtDist(sub, sizeof(sub), km, (s_ui->_prefs && s_ui->_prefs->units_imperial));
else snprintf(sub, sizeof(sub), "no position");
int at = s_ui->_core->route.indexOf(c.id.pub_key);
if (at >= 0) { char t[48]; snprintf(t, sizeof(t), "%s - hop %d", sub, at + 1); rowSet(row, c.name, t); }
else rowSet(row, c.name, sub);
},
};
for (int i = 0; i < routeview::s_pick_n; i++) vlist::add(body, &ROW, i);
if (!routeview::s_pick_n)
vlist::own(body, noteLabel(body, "No repeaters yet. They show up here once their advert is heard.", THEME_FONT_BODY, theme::TEXT_MUTED));
vlist::end(body);
}
void UITask::routePick(int row) {
if (row < 0 || row >= routeview::s_pick_n) return;
ContactInfo c;
if (!the_mesh.getContactByIdx(routeview::s_pick[row].idx, c)) return;
PathEditor& R = _core->route;
if (R.full()) { showToast("The route is full"); return; }
if (!R.addRepeater(c.id.pub_key)) { showToast("That repeater is already the last hop"); return; }
_screen = SCR_ROUTE;
_nav_back = true;
buildRoute();
_nav_back = false;
}
// ── From the map ──────────────────────────────────────────────────────────────
// A tap on a repeater of the nodes map while the route is picked: in, or out
// again if it is already a hop.
void UITask::routeMapTap(const uint8_t* pub_key, uint8_t type) {
PathEditor& R = _core->route;
if (type != ADV_TYPE_REPEATER) { showToast("Pick repeaters"); return; }
int at = R.indexOf(pub_key);
if (at >= 0) R.remove(at);
else if (R.full()) { showToast("The route is full"); return; }
else if (!R.addRepeater(pub_key)) { showToast("That repeater is already the last hop"); return; }
routeMapRefresh();
}
void UITask::routeMapDone() {
_route_pick = false;
_screen = SCR_ROUTE;
_nav_back = true;
buildRoute();
_nav_back = false;
}
@@ -344,6 +344,10 @@ void UITask::loop() {
if (_screen == SCR_WIFI) pollWifiScan();
if (_screen == SCR_STORAGE) pollStorage();
if (_screen == SCR_ADMIN) adminPoll();
if (_screen == SCR_ROUTE && (int32_t)(millis() - _next_route_ms) >= 0) {
_next_route_ms = millis() + 200;
routePoll();
}
roomPoll();
if (_screen == SCR_CLOCK && (int32_t)(millis() - _next_clock_ms) >= 0) {
_next_clock_ms = millis() + 100; // stopwatch tenths
@@ -1085,6 +1089,12 @@ void UITask::back() {
case SCR_BOT: if (_nav_overlay) navClosePopup(); else showHome(); break;
case SCR_DIAG: if (_nav_overlay) navClosePopup(); else showHome(); break;
case SCR_ADMIN_PICK: showHome(); break;
case SCR_ROUTE: // back to the node; a trace in flight is dropped
_core->route.cancelTrace();
_screen = SCR_NODE;
buildNode();
break;
case SCR_ROUTE_PICK: _screen = SCR_ROUTE; buildRoute(); break;
case SCR_OTA: otaLeave(); break;
case SCR_COMPASS: showHome(); break;
case SCR_BATT: showSchemaSettings(PG_POWER); break;
@@ -1141,6 +1151,7 @@ void UITask::back() {
if (_node_from_scan) showScanPopup();
break;
case SCR_MAP:
if (_route_pick) { routeMapDone(); break; }
if (_nav_overlay) navClosePopup();
else if (areaSelecting()) areaSelectEnd();
else if (_dl_overlay) mapDownloadClose();
@@ -1194,6 +1205,7 @@ static void onOpenSchemaPage(lv_event_t* e); // Radio > Advanced
#include "WifiScreen.h"
#include "ChannelScreen.h"
#include "AdminScreen.h"
#include "RouteScreen.h"
#include "BotScreen.h"
#include "ThreadView.h"
+28 -1
View File
@@ -112,6 +112,23 @@ public:
void mapCenterOnMe();
void mapPan(int dx, int dy);
void mapOpenMarker(int idx);
// Node detail > Route (RouteScreen.h) and picking its hops on the map
void showRoute(const uint8_t* pub_key);
void buildRoute();
void routeRebuild();
void routeStatus();
void routePoll();
void routeAct(uint8_t act);
void routeOp(int code);
void showRoutePick();
void buildRoutePick();
void routePick(int row);
void routeMapTap(const uint8_t* pub_key, uint8_t type);
void routeMapDone();
void routeMapRefresh();
void routeMapUndo();
void routeMapFrame();
void buildRouteBar(lv_obj_t* body);
void mapDownloadPopup();
// Map areas (MapAreas.h)
void areaBuildLayer(lv_obj_t* body);
@@ -347,7 +364,7 @@ public:
void openAdminFor(const ContactInfo& ci, bool from_picker) { (void)from_picker; openAdmin(ci.id.pub_key); }
private:
enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI, SCR_SETTINGS_NAV, SCR_CLOCK, SCR_RADIO, SCR_CHANNEL_EDIT, SCR_ADMIN, SCR_BOT, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY, SCR_QUICK, SCR_ADMIN_PICK, SCR_OTA, SCR_STORAGE, SCR_GPS, SCR_BATT };
enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI, SCR_SETTINGS_NAV, SCR_CLOCK, SCR_RADIO, SCR_CHANNEL_EDIT, SCR_ADMIN, SCR_BOT, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY, SCR_QUICK, SCR_ADMIN_PICK, SCR_OTA, SCR_STORAGE, SCR_GPS, SCR_BATT, SCR_ROUTE, SCR_ROUTE_PICK };
void buildStatusBar();
void refreshStatusBar();
@@ -407,6 +424,7 @@ private:
void refreshScanPopup();
void buildMap();
void layoutMap();
void layoutMapMarks(double left, double top, int w, int h);
void mapLoop();
void rebuildMapMarkers();
void rebuildNodeMarkers();
@@ -610,6 +628,15 @@ private:
lv_obj_t* _node_delete_lbl = nullptr;
lv_obj_t* _node_path_lbl = nullptr;
bool _node_from_map = false;
lv_obj_t* _route_status = nullptr; // Route screen (RouteScreen.h)
lv_obj_t* _route_list = nullptr;
lv_obj_t* _route_trace_btn = nullptr;
lv_obj_t* _route_pin_btn = nullptr;
uint8_t _route_shown_state = 255;
uint32_t _next_route_ms = 0;
bool _route_pick = false; // the nodes map taps repeaters into the route
lv_obj_t* _route_bar = nullptr;
lv_obj_t* _route_lbl = nullptr;
// Map (SCR_MAP): view centre in fractional tile coords at zoom _map_z
double _map_cx = 0, _map_cy = 0;