fix: review pass - Discover's late replies dropped and its way back usable, the Gallery's newest first and faster, the network clock's toast

- Discover: a reply after its try timed out, was stopped or was overtaken
  went on to the normal handling and became the contact's route (and sent
  a path back); the last tags sent are kept and a late reply is dropped.
  Leaving Route on the L2 stops a discovery as it stops a trace.
- Discover: a route whose reply came back another way offers that way too
  (L2 There / Back, L1 a Back row), turned round into a route there, so a
  trace checks it; the Discover button no longer sits on its group.
- Screenshots: numbered one past the highest (one pass over the folder,
  not a stat per number), so one taken after a delete is still the newest.
- Gallery: the newest 300 kept whatever order the folder lists them in;
  BMP rows read in the file's order and several at a time; the thumbnail
  buffer sized to the page; the folder read once per visit; a failed
  open frees or restores the picture.
- NTP: the slot the router's server goes in emptied before joining, so a
  cached address isn't taken for it; "Clock set from the network" judged
  against the clock a tick before (SNTP has set the system time already).
- Sim: sim_scroll up stops at the top.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-29 16:30:19 +01:00
co-authored by Claude Opus 5.5
parent e154a4e721
commit 3a26a8bee7
11 changed files with 171 additions and 65 deletions
+6 -2
View File
@@ -94,7 +94,10 @@ on the map. On the **Discover** row, **Left / Right** sets 1, 3 or 5 tries
and **Enter** starts it (or stops it). Each try waits for its reply or its and **Enter** starts it (or stops it). Each try waits for its reply or its
timeout, then 20 seconds more, so the floods don't come all at once. Each timeout, then 20 seconds more, so the floods don't come all at once. Each
route found is listed under it with how many times it came; **Enter** on one route found is listed under it with how many times it came; **Enter** on one
puts it in the list, ready to **Trace** and save. A repeater you don't have puts it in the list, ready to **Trace** and save. When the reply came back
another way, a **Back** row follows it: that way turned round, as a route to
the node, so a trace checks it too (a trace goes there and back on the same
repeaters). A repeater you don't have
as a contact shows as its hash (`?8D`). Nothing is saved until you save it. as a contact shows as its hash (`?8D`). Nothing is saved until you save it.
Only a node that answers requests can be asked: a companion whose Only a node that answers requests can be asked: a companion whose
@@ -119,7 +122,8 @@ straight to the node. The route a node sends back usually replaces it again;
> **Discover** is under the route, in **Find the route**, with the number of > **Discover** is under the route, in **Find the route**, with the number of
> tries beside it and where it is (the try waiting for its reply, or the > tries beside it and where it is (the try waiting for its reply, or the
> seconds to the next one). Tap a route found to use it; the way back is under > seconds to the next one). Tap a route found to use it; the way back is under
> it. > it. When the way back is another one, **There** and **Back** put either in
> the list to trace.
## Your own contact ## Your own contact
+12 -4
View File
@@ -1079,6 +1079,8 @@ uint32_t MyMesh::sendPathDiscovery(const uint8_t* pub_key, DiscoverCallback cb,
uint32_t tag, est; uint32_t tag, est;
if (sendDiscoveryReq(*recipient, tag, est) == MSG_SEND_FAILED) return 0; if (sendDiscoveryReq(*recipient, tag, est) == MSG_SEND_FAILED) return 0;
_ui_discovery = tag; _ui_discovery = tag;
_ui_disc_sent[_ui_disc_next] = tag;
_ui_disc_next = (_ui_disc_next + 1) % UI_DISC_KEEP;
_discover_cb = cb; _discover_cb = cb;
if (est_timeout) *est_timeout = est; if (est_timeout) *est_timeout = est;
return tag; return tag;
@@ -1089,10 +1091,16 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
uint32_t tag; uint32_t tag;
memcpy(&tag, extra, 4); memcpy(&tag, extra, 4);
if (_ui_discovery && tag == _ui_discovery) { // this device's own (sendPathDiscovery()) // This device's own (sendPathDiscovery()): to the editor while it waits
_ui_discovery = 0; // for it; late (timed out, stopped, an earlier try's) only dropped.
if (_discover_cb && mesh::Packet::isValidPathLen(in_path_len) && mesh::Packet::isValidPathLen(out_path_len)) for (int k = 0; k < UI_DISC_KEEP; k++) {
_discover_cb(contact.id.pub_key, out_path, out_path_len, in_path, in_path_len); if (!tag || _ui_disc_sent[k] != tag) continue;
_ui_disc_sent[k] = 0;
if (tag == _ui_discovery) {
_ui_discovery = 0;
if (_discover_cb && mesh::Packet::isValidPathLen(in_path_len) && mesh::Packet::isValidPathLen(out_path_len))
_discover_cb(contact.id.pub_key, out_path, out_path_len, in_path, in_path_len);
}
return false; // DON'T send reciprocal path, nor take it: the user picks return false; // DON'T send reciprocal path, nor take it: the user picks
} }
+3
View File
@@ -679,6 +679,9 @@ private:
uint32_t pending_status; uint32_t pending_status;
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
uint32_t _ui_discovery = 0; // sendPathDiscovery()'s, answered to _discover_cb uint32_t _ui_discovery = 0; // sendPathDiscovery()'s, answered to _discover_cb
static const int UI_DISC_KEEP = 6;
uint32_t _ui_disc_sent[UI_DISC_KEEP] = {}; // the last ones sent: a late reply is dropped, not taken as the route
uint8_t _ui_disc_next = 0;
DiscoverCallback _discover_cb = nullptr; DiscoverCallback _discover_cb = nullptr;
int sendDiscoveryReq(ContactInfo& recipient, uint32_t& tag, uint32_t& est_timeout); int sendDiscoveryReq(ContactInfo& recipient, uint32_t& tag, uint32_t& est_timeout);
uint32_t pending_req; // pending _BINARY_REQ uint32_t pending_req; // pending _BINARY_REQ
+28 -9
View File
@@ -14,7 +14,8 @@
// save() -- the list becomes the contact's out_path // save() -- the list becomes the contact's out_path
// discover(tries) -- asks the node by flood which way it's reached (path // discover(tries) -- asks the node by flood which way it's reached (path
// discovery), `tries` times with a pause between; the // discovery), `tries` times with a pause between; the
// routes found, useFound() puts one in the list // routes found, useFound() puts one in the list -- the
// way there, or the reply's way back turned round
// //
// A TRACE is answered only by the node that hears the last retransmission, so // A TRACE is answered only by the node that hears the last retransmission, so
// a route is traced there and back (A B dest B A): every node of it has to // a route is traced there and back (A B dest B A): every node of it has to
@@ -299,30 +300,48 @@ public:
int foundCount() const { return _found_n; } int foundCount() const { return _found_n; }
const Found& found(int i) const { return _found[i]; } const Found& found(int i) const { return _found[i]; }
static int hopsOf(uint8_t len) { return len & 63; } static int hopsOf(uint8_t len) { return len & 63; }
// Route i's hops, "Name > Name" (hash "?3F" for one not a contact), cut to fit. // Route i's hops, "Name > Name" (hash "?3F" for one not a contact), cut to
void foundText(int i, bool back, char* out, size_t n) const { // fit. The way back as the reply came (from the node), or with `as_route`
// turned round, from here: as useFound(i, true) lists it.
void foundText(int i, bool back, char* out, size_t n, bool as_route = false) const {
const Found& f = _found[i]; const Found& f = _found[i];
const uint8_t len = back ? f.back_len : f.out_len; const uint8_t len = back ? f.back_len : f.out_len;
const uint8_t* p = back ? f.back : f.out; const uint8_t* p = back ? f.back : f.out;
const int hops = hopsOf(len), size = (len >> 6) + 1; const int hops = hopsOf(len), size = (len >> 6) + 1;
const int kept = hops < FOUND_HOPS ? hops : FOUND_HOPS;
const bool rev = back && as_route;
size_t o = 0; size_t o = 0;
out[0] = '\0'; out[0] = '\0';
if (hops == 0) { snprintf(out, n, "direct"); return; } if (hops == 0) { snprintf(out, n, "direct"); return; }
for (int h = 0; h < hops && h < FOUND_HOPS && o + 4 < n; h++) { for (int h = 0; h < kept && o + 4 < n; h++) {
char nm[NAME_LEN]; char nm[NAME_LEN];
hashName(&p[h * size], size, nm, sizeof(nm)); hashName(&p[(rev ? kept - 1 - h : h) * size], size, nm, sizeof(nm));
o += snprintf(out + o, n - o, "%s%s", h ? " > " : "", nm); o += snprintf(out + o, n - o, "%s%s", h ? " > " : "", nm);
} }
if (hops > FOUND_HOPS && o + 4 < n) snprintf(out + o, n - o, " ..."); if (hops > FOUND_HOPS && o + 4 < n) snprintf(out + o, n - o, " ...");
} }
// Route i as the list (its repeaters by key where they're contacts). // The reply's way back, turned round, is route i itself: one way both ways.
bool useFound(int i) { bool foundBackSame(int i) const {
const Found& f = _found[i];
if (f.back_len != f.out_len) return false;
const int hops = hopsOf(f.out_len) < FOUND_HOPS ? hopsOf(f.out_len) : FOUND_HOPS, size = (f.out_len >> 6) + 1;
for (int h = 0; h < hops; h++)
if (memcmp(&f.out[h * size], &f.back[(hops - 1 - h) * size], size) != 0) return false;
return true;
}
static int foundHops(const Found& f, bool back) { return hopsOf(back ? f.back_len : f.out_len); }
// Route i as the list (its repeaters by key where they're contacts): the
// way there, or the way back turned round -- a route there too, which a
// trace then checks out and back like any.
bool useFound(int i, bool back = false) {
if (i < 0 || i >= _found_n) return false; if (i < 0 || i >= _found_n) return false;
const Found& f = _found[i]; const Found& f = _found[i];
const int hops = hopsOf(f.out_len), size = (f.out_len >> 6) + 1; const uint8_t len = back ? f.back_len : f.out_len;
const uint8_t* p = back ? f.back : f.out;
const int hops = hopsOf(len), size = (len >> 6) + 1;
cancelTrace(); cancelTrace();
_n = 0; _n = 0;
for (int h = 0; h < hops && h < MAX_HOPS; h++) loadHash(&f.out[h * size], size); for (int h = 0; h < hops && h < MAX_HOPS; h++) loadHash(&p[(back ? hops - 1 - h : h) * size], size);
_too_long = hops > MAX_HOPS; _too_long = hops > MAX_HOPS;
_edited = true; _edited = true;
invalidate(); invalidate();
@@ -26,6 +26,7 @@ static PSRAM_BSS char s_names[SHOTS_MAX][16];
static int s_n = 0; static int s_n = 0;
static int s_page = 0; // the grid's page, from the newest static int s_page = 0; // the grid's page, from the newest
static uint8_t s_tab = T_SHOTS; static uint8_t s_tab = T_SHOTS;
static bool s_listed = false; // s_names read since the gallery opened (pages, tabs reuse it)
// The grid's thumbnails: one buffer per grid, freed with it (a rebuild makes // The grid's thumbnails: one buffer per grid, freed with it (a rebuild makes
// the new grid before the old one goes, so each frees only its own). // the new grid before the old one goes, so each frees only its own).
@@ -49,12 +50,17 @@ static const int SWIPE_PX = 40;
static void scan() { static void scan() {
s_n = 0; s_n = 0;
s_listed = true;
DIR* d = opendir(SHOTS_DIR); DIR* d = opendir(SHOTS_DIR);
if (!d) return; if (!d) return;
while (struct dirent* de = readdir(d)) { while (struct dirent* de = readdir(d)) {
size_t l = strlen(de->d_name); size_t l = strlen(de->d_name);
if (de->d_name[0] == '.' || l < 5 || l >= sizeof(s_names[0]) || strcasecmp(de->d_name + l - 4, ".bmp") != 0) continue; if (de->d_name[0] == '.' || l < 5 || l >= sizeof(s_names[0]) || strcasecmp(de->d_name + l - 4, ".bmp") != 0) continue;
if (s_n < SHOTS_MAX) snprintf(s_names[s_n++], sizeof(s_names[0]), "%s", de->d_name); if (s_n < SHOTS_MAX) { snprintf(s_names[s_n++], sizeof(s_names[0]), "%s", de->d_name); continue; }
// Full: the newest SHOTS_MAX stay, whatever order the folder lists them in.
int lo = 0;
for (int k = 1; k < s_n; k++) if (strcmp(s_names[k], s_names[lo]) < 0) lo = k;
if (strcmp(de->d_name, s_names[lo]) > 0) snprintf(s_names[lo], sizeof(s_names[0]), "%s", de->d_name);
} }
closedir(d); closedir(d);
// Newest first: scr_NNNN numbers count up. // Newest first: scr_NNNN numbers count up.
@@ -89,12 +95,17 @@ static void dscInit(lv_image_dsc_t& d, uint16_t* px, int w, int h) {
d.data = (const uint8_t*)px; d.data = (const uint8_t*)px;
} }
// An uncompressed 24-bit BMP, read a row at a time (top row = 0). static const int CHUNK_ROWS = 16; // rows read at once for the full view
// An uncompressed 24-bit BMP, read several rows at a time (top row = 0) in
// the file's order, so a seek only goes forward and each read is long.
struct Bmp { struct Bmp {
FILE* f = nullptr; FILE* f = nullptr;
int32_t w = 0, h = 0; int32_t w = 0, h = 0;
uint32_t off = 0, stride = 0; uint32_t off = 0, stride = 0;
bool bottom_up = true; bool bottom_up = true;
uint8_t* buf = nullptr; // CHUNK_ROWS rows
int ch = 1, c0 = 0, cn = 0; // rows read at a time; rows c0..c0+cn-1 in buf
bool open(const char* p) { bool open(const char* p) {
f = fopen(p, "rb"); f = fopen(p, "rb");
uint8_t hdr[54]; uint8_t hdr[54];
@@ -110,13 +121,26 @@ struct Bmp {
stride = ((uint32_t)w * 3 + 3) & ~3u; stride = ((uint32_t)w * 3 + 3) & ~3u;
return true; return true;
} }
bool row(int y, uint8_t* out) { // BGR // Rows lo..hi in buf (hi - lo < ch), with the next ones in the file's
if (fseek(f, (long)(off + (uint32_t)(bottom_up ? h - 1 - y : y) * stride), SEEK_SET) != 0) return false; // order: a bottom-up file is read from its end, upwards on the screen.
return fread(out, 1, (size_t)w * 3, f) == (size_t)w * 3; bool load(int lo, int hi) {
if (lo >= c0 && hi < c0 + cn) return true;
int a, z;
if (bottom_up) { z = hi; a = hi - ch + 1 < 0 ? 0 : hi - ch + 1; }
else { a = lo; z = lo + ch - 1 >= h ? h - 1 : lo + ch - 1; }
cn = 0;
if (fseek(f, (long)(off + (uint32_t)(bottom_up ? h - 1 - z : a) * stride), SEEK_SET) != 0) return false;
const size_t n = (size_t)(z - a + 1) * stride;
if (fread(buf, 1, n, f) < n - (stride - (uint32_t)w * 3)) return false; // the last row's padding may be missing
c0 = a; cn = z - a + 1;
return true;
}
const uint8_t* row(int y) const { // BGR, after load()
return buf + (size_t)(bottom_up ? c0 + cn - 1 - y : y - c0) * stride;
} }
~Bmp() { if (f) fclose(f); } ~Bmp() { if (f) fclose(f); }
}; };
static PSRAM_BSS uint8_t s_row[2][BMP_MAX_W * 3]; static PSRAM_BSS uint8_t s_rows[CHUNK_ROWS * ((BMP_MAX_W * 3 + 3) & ~3)];
// File `i` scaled into w x h: each pixel the mean of the 2x2 under its // File `i` scaled into w x h: each pixel the mean of the 2x2 under its
// spot (a thumbnail), or the one pixel there (the full view). // spot (a thumbnail), or the one pixel there (the full view).
@@ -125,11 +149,14 @@ static bool decode(int i, uint16_t* out, int w, int h, bool smooth) {
path(i, p, sizeof(p)); path(i, p, sizeof(p));
Bmp b; Bmp b;
if (!b.open(p)) return false; if (!b.open(p)) return false;
for (int ty = 0; ty < h; ty++) { b.buf = s_rows;
b.ch = smooth ? 2 : CHUNK_ROWS; // a thumbnail skips rows: just the pair it means
for (int k = 0; k < h; k++) {
const int ty = b.bottom_up ? h - 1 - k : k;
const int sy = ty * b.h / h, sy2 = smooth && sy + 1 < b.h ? sy + 1 : sy; const int sy = ty * b.h / h, sy2 = smooth && sy + 1 < b.h ? sy + 1 : sy;
if (!b.row(sy, s_row[0]) || (sy2 != sy && !b.row(sy2, s_row[1]))) return false; if (!b.load(sy, sy2)) return false;
const uint8_t* r1 = s_row[0]; const uint8_t* r1 = b.row(sy);
const uint8_t* r2 = sy2 != sy ? s_row[1] : s_row[0]; const uint8_t* r2 = b.row(sy2);
for (int tx = 0; tx < w; tx++) { for (int tx = 0; tx < w; tx++) {
const int sx = tx * b.w / w, sx2 = smooth && sx + 1 < b.w ? sx + 1 : sx; const int sx = tx * b.w / w, sx2 = smooth && sx + 1 < b.w ? sx + 1 : sx;
const uint8_t* a = r1 + sx * 3; const uint8_t* c = r1 + sx2 * 3; const uint8_t* a = r1 + sx * 3; const uint8_t* c = r1 + sx2 * 3;
@@ -184,6 +211,7 @@ static void onGalleryViewTouch(lv_event_t* e) {
void UITask::showGallery() { void UITask::showGallery() {
_screen = SCR_GALLERY; _screen = SCR_GALLERY;
gallery::s_page = 0; gallery::s_page = 0;
gallery::s_listed = false;
buildGallery(); buildGallery();
} }
@@ -230,7 +258,7 @@ void UITask::buildGallery() {
} }
if (!card) { noteLabel(list, "No SD card - screenshots are saved on it.", THEME_FONT_BODY); return; } if (!card) { noteLabel(list, "No SD card - screenshots are saved on it.", THEME_FONT_BODY); return; }
scan(); if (!s_listed) scan();
if (s_n == 0) { if (s_n == 0) {
noteLabel(list, "No screenshots yet. Hold the side button and press the top one to take one.", THEME_FONT_BODY); noteLabel(list, "No screenshots yet. Hold the side button and press the top one to take one.", THEME_FONT_BODY);
return; return;
@@ -246,7 +274,7 @@ void UITask::buildGallery() {
const int first = s_page * PAGE; const int first = s_page * PAGE;
const int cells = s_n - first < PAGE ? s_n - first : PAGE; const int cells = s_n - first < PAGE ? s_n - first : PAGE;
uint16_t* px = (uint16_t*)psramBuf<uint16_t>((size_t)PAGE * TW * TH); uint16_t* px = (uint16_t*)psramBuf<uint16_t>((size_t)cells * TW * TH);
if (!px) { noteLabel(list, "Out of memory", THEME_FONT_BODY); return; } if (!px) { noteLabel(list, "Out of memory", THEME_FONT_BODY); return; }
s_thumbs = px; s_thumbs = px;
s_cells = cells; s_cells = cells;
@@ -313,7 +341,13 @@ void UITask::galleryOpen(int i) {
if (!s_full) s_full = (uint16_t*)psramBuf<uint16_t>((size_t)SW * SH); if (!s_full) s_full = (uint16_t*)psramBuf<uint16_t>((size_t)SW * SH);
if (!s_full) { showToast("Out of memory"); return; } if (!s_full) { showToast("Out of memory"); return; }
if (s_view_img) lv_image_cache_drop(&s_fdsc); if (s_view_img) lv_image_cache_drop(&s_fdsc);
if (!decode(i, s_full, SW, SH, false)) { showToast("Can't read this file"); return; } if (!decode(i, s_full, SW, SH, false)) {
// The picture shown (if any) was half overwritten: back to it whole.
if (s_view) decode(s_cur, s_full, SW, SH, false);
else { free(s_full); s_full = nullptr; }
showToast("Can't read this file");
return;
}
s_cur = i; s_cur = i;
dscInit(s_fdsc, s_full, SW, SH); dscInit(s_fdsc, s_full, SW, SH);
if (!s_view) { if (!s_view) {
+3 -1
View File
@@ -625,7 +625,9 @@ static int netState() {
// itself; ntpTake() hands the time on to rtc_clock (a hardware RTC, if any). // itself; ntpTake() hands the time on to rtc_clock (a hardware RTC, if any).
static volatile uint32_t s_ntp_secs = 0; static volatile uint32_t s_ntp_secs = 0;
static char s_ntp_dhcp[16]; static char s_ntp_dhcp[16];
static void ntpAskDhcp() { esp_sntp_servermode_dhcp(true); } // Slot 0 emptied first: it keeps the address it last used (pool.ntp.org's,
// or the last network's router), which ntpStart() would take for this one's.
static void ntpAskDhcp() { esp_sntp_setserver(0, nullptr); esp_sntp_servermode_dhcp(true); }
static void ntpSynced(struct timeval* tv) { s_ntp_secs = (uint32_t)tv->tv_sec; } // the lwIP task static void ntpSynced(struct timeval* tv) { s_ntp_secs = (uint32_t)tv->tv_sec; } // the lwIP task
static void ntpStart() { static void ntpStart() {
if (esp_sntp_enabled()) esp_sntp_stop(); if (esp_sntp_enabled()) esp_sntp_stop();
+25 -10
View File
@@ -146,6 +146,7 @@ void UITask::buildRoute() {
_route_note = noteLabel(scroll, "", THEME_FONT_SMALL, theme::TEXT_MUTED); _route_note = noteLabel(scroll, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
_route_disc = flexBox(scroll, LV_FLEX_FLOW_COLUMN); _route_disc = flexBox(scroll, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(_route_disc, LV_PCT(100)); lv_obj_set_width(_route_disc, LV_PCT(100));
lv_obj_set_style_pad_row(_route_disc, theme::GAP, 0); // Discover's button off the group, as the list's rows
_route_disc_shown = 0xFFFFFFFF; _route_disc_shown = 0xFFFFFFFF;
lv_obj_t* bar = buttonBar(body); lv_obj_t* bar = buttonBar(body);
@@ -321,18 +322,29 @@ void UITask::routeDiscRebuild() {
lv_obj_t* l = groupLine(g, false); lv_obj_t* l = groupLine(g, false);
noteLabel(l, t, THEME_FONT_SMALL, R.discoverFailed() ? theme::FAIL : theme::TEXT_MUTED); noteLabel(l, t, THEME_FONT_SMALL, R.discoverFailed() ? theme::FAIL : theme::TEXT_MUTED);
} }
// Code i * 2 + 1: the way back, turned round into a route there to trace.
for (int i = 0; i < R.foundCount(); i++) { for (int i = 0; i < R.foundCount(); i++) {
const PathEditor::Found& f = R.found(i); const PathEditor::Found& f = R.found(i);
char route[96], back[64], sub[96]; char route[96], back[64], sub[128];
R.foundText(i, false, route, sizeof(route)); R.foundText(i, false, route, sizeof(route));
R.foundText(i, true, back, sizeof(back)); R.foundText(i, true, back, sizeof(back));
const int hops = PathEditor::hopsOf(f.out_len); const int hops = PathEditor::hopsOf(f.out_len);
snprintf(sub, sizeof(sub), "%d hop%s%s, found %u time%s. Back: %s", hops, hops == 1 ? "" : "s", const bool same = R.foundBackSame(i);
hops > PathEditor::MAX_HOPS ? " (too long to use)" : "", (unsigned)f.times, f.times == 1 ? "" : "s", back); int o = snprintf(sub, sizeof(sub), "%d hop%s%s, found %u time%s. ", hops, hops == 1 ? "" : "s",
lv_obj_t* r = groupLine(g, true); hops > PathEditor::MAX_HOPS ? " (too long to use)" : "", (unsigned)f.times, f.times == 1 ? "" : "s");
if (same) snprintf(sub + o, sizeof(sub) - o, "Back the same way");
else snprintf(sub + o, sizeof(sub) - o, "Back: %s", back);
lv_obj_t* r = groupLine(g, same);
groupText(r, route, sub); groupText(r, route, sub);
label(r, "Use", THEME_FONT_SMALL, theme::ACCENT); if (same) {
lv_obj_add_event_cb(r, onRouteFound, LV_EVENT_CLICKED, (void*)(uintptr_t)i); label(r, "Use", THEME_FONT_SMALL, theme::ACCENT);
lv_obj_add_event_cb(r, onRouteFound, LV_EVENT_CLICKED, (void*)(uintptr_t)(i * 2));
} else {
lv_obj_t* col = flexBox(r, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(col, 4, 0);
lv_obj_set_width(pillButton(col, "There", theme::SURFACE_2, onRouteFound, i * 2), 64);
lv_obj_set_width(pillButton(col, "Back", theme::SURFACE_2, onRouteFound, i * 2 + 1), 64);
}
} }
lv_obj_t* bar = buttonBar(_route_disc); lv_obj_t* bar = buttonBar(_route_disc);
barButton(bar, R.discovering() ? LV_SYMBOL_CLOSE " Stop" : LV_SYMBOL_REFRESH " Discover", onRouteDiscover, 0, !R.discovering()); barButton(bar, R.discovering() ? LV_SYMBOL_CLOSE " Stop" : LV_SYMBOL_REFRESH " Discover", onRouteDiscover, 0, !R.discovering());
@@ -350,11 +362,14 @@ void UITask::routeDiscTries(int sel) {
if (sel >= 0 && sel < 3) routeview::s_tries = sel; if (sel >= 0 && sel < 3) routeview::s_tries = sel;
} }
void UITask::routeUseFound(int i) { void UITask::routeUseFound(int code) {
PathEditor& R = _core->route; PathEditor& R = _core->route;
if (PathEditor::hopsOf(R.found(i).out_len) > PathEditor::MAX_HOPS) { showToast("Too long: up to 7 repeaters"); return; } const int i = code >> 1;
if (!R.useFound(i)) return; const bool back = code & 1;
showToast("In the list: Trace to check it, Save to use it"); if (i >= R.foundCount()) return;
if (PathEditor::foundHops(R.found(i), back) > PathEditor::MAX_HOPS) { showToast("Too long: up to 7 repeaters"); return; }
if (!R.useFound(i, back)) return;
showToast(back ? "The way back, turned round, in the list: Trace to check it" : "In the list: Trace to check it, Save to use it");
routeRebuild(); routeRebuild();
} }
+17 -10
View File
@@ -948,14 +948,19 @@ void UITask::takeScreenshot() {
if (!lvport::mountStorage()) { showToast("No SD card"); return; } if (!lvport::mountStorage()) { showToast("No SD card"); return; }
mkdir("/sdcard", 0777); // the sim's MEMFS may not have it yet (the card: fails, harmless) mkdir("/sdcard", 0777); // the sim's MEMFS may not have it yet (the card: fails, harmless)
mkdir("/sdcard/screenshots", 0777); mkdir("/sdcard/screenshots", 0777);
char path[48]; // One past the highest number: a deleted one's isn't reused, so the newest
struct stat st; // stays first in the Gallery (one pass over the folder, not a stat a file).
int n = 1; int n = 0;
for (; n <= 9999; n++) { if (DIR* d = opendir("/sdcard/screenshots")) {
snprintf(path, sizeof(path), "/sdcard/screenshots/scr_%04d.bmp", n); while (struct dirent* de = readdir(d)) {
if (stat(path, &st) != 0) break; int k;
if (sscanf(de->d_name, "scr_%4d", &k) == 1 && k > n) n = k;
}
closedir(d);
} }
if (n > 9999) { showToast("Screenshots folder full"); return; } if (++n > 9999) { showToast("Screenshots folder full"); return; }
char path[48];
snprintf(path, sizeof(path), "/sdcard/screenshots/scr_%04d.bmp", n);
int W, H; int W, H;
uint16_t* px = captureFrame(W, H); uint16_t* px = captureFrame(W, H);
if (!px) { showToast("Out of memory"); return; } if (!px) { showToast("Out of memory"); return; }
@@ -1249,8 +1254,9 @@ void UITask::back() {
case SCR_BOT: if (_nav_overlay) navClosePopup(); else showHome(); break; case SCR_BOT: if (_nav_overlay) navClosePopup(); else showHome(); break;
case SCR_DIAG: 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_ADMIN_PICK: showHome(); break;
case SCR_ROUTE: // back to the node; a trace in flight is dropped case SCR_ROUTE: // back to the node; a trace or discovery in flight is dropped
_core->route.cancelTrace(); _core->route.cancelTrace();
_core->route.stopDiscover();
_screen = SCR_NODE; _screen = SCR_NODE;
buildNode(); buildNode();
break; break;
@@ -1527,8 +1533,9 @@ extern "C" EMSCRIPTEN_KEEPALIVE int sim_scroll(int dy) {
dbgScrollable(lv_layer_top(), o, h); dbgScrollable(lv_layer_top(), o, h);
if (!o) dbgScrollable(lv_screen_active(), o, h); if (!o) dbgScrollable(lv_screen_active(), o, h);
if (!o) return -1; if (!o) return -1;
int left = lv_obj_get_scroll_bottom(o); int left = dy >= 0 ? lv_obj_get_scroll_bottom(o) : lv_obj_get_scroll_top(o); // up: no further than the top
lv_obj_scroll_by(o, 0, -(dy < left ? dy : left), LV_ANIM_OFF); const int step = dy >= 0 ? (dy < left ? dy : left) : (-dy < left ? dy : -left);
lv_obj_scroll_by(o, 0, -step, LV_ANIM_OFF);
return left; return left;
} }
#endif #endif
+1 -1
View File
@@ -131,7 +131,7 @@ public:
uint32_t routeDiscSig(); uint32_t routeDiscSig();
void routeDiscover(); void routeDiscover();
void routeDiscTries(int sel); void routeDiscTries(int sel);
void routeUseFound(int i); void routeUseFound(int code); // found route code >> 1; bit 0: its way back, turned round
void routeAct(uint8_t act); void routeAct(uint8_t act);
void routeOp(int code); void routeOp(int code);
void routeKeep(); void routeKeep();
@@ -323,9 +323,11 @@ void UITask::wifiSave() {
// From loop(), any screen, once a second: SNTP runs while WiFi is connected // From loop(), any screen, once a second: SNTP runs while WiFi is connected
// (Settings > WiFi, a map download, an update, the app over WiFi), and each // (Settings > WiFi, a map download, an update, the app over WiFi), and each
// time it answers the clock takes the time -- ahead or behind, as from GPS. // time it answers the clock takes the time -- ahead or behind, as from GPS.
// The first answer of a connection says so when it moved the clock. // The first answer of a connection says so when it moved the clock --
// judged against the clock a tick before: without a clock chip rtc_clock is
// the system time, which SNTP has set already by the time we hear of it.
void UITask::wifiClockTick() { void UITask::wifiClockTick() {
static uint32_t next_ms = 0; static uint32_t next_ms = 0, ref_wall = 0, ref_ms = 0;
static bool first = false; static bool first = false;
if ((int32_t)(millis() - next_ms) < 0) return; if ((int32_t)(millis() - next_ms) < 0) return;
next_ms = millis() + 1000; next_ms = millis() + 1000;
@@ -338,9 +340,11 @@ void UITask::wifiClockTick() {
const uint32_t t = lvport::ntpTake(); const uint32_t t = lvport::ntpTake();
if (t > 1700000000UL) { if (t > 1700000000UL) {
const uint32_t now = rtc_clock.getCurrentTime(); const uint32_t now = rtc_clock.getCurrentTime();
const bool moved = t > now + 1 || now > t + 1; if (t > now + 1 || now > t + 1) rtc_clock.setCurrentTime(t); // a clock chip: SNTP set only the system time
if (moved) rtc_clock.setCurrentTime(t); const uint32_t was = ref_wall + (millis() - ref_ms) / 1000;
if (first && moved && !_asleep) showToast("Clock set from the network"); if (first && (t > was + 2 || was > t + 2) && !_asleep) showToast("Clock set from the network");
first = false; first = false;
} }
ref_wall = rtc_clock.getCurrentTime();
ref_ms = millis();
} }
+20 -10
View File
@@ -24,8 +24,8 @@ class RouteView {
bool _seen_disc = false; // a discovery running at the last pass: its end said once bool _seen_disc = false; // a discovery running at the last pass: its end said once
uint8_t _tries = 1; // Discover: triesOf(_tries) tries uint8_t _tries = 1; // Discover: triesOf(_tries) tries
// R_FOUND before R_NODE: like the hops, not an action the cursor follows. // R_FOUND(_BACK) before R_NODE: like the hops, not an action the cursor follows.
enum Row : uint8_t { R_HOP, R_FOUND, R_NODE, R_ADD, R_TRACE, R_PROBE, R_TO_NODE, R_SAVE, R_KEEP, R_DISCOVER }; enum Row : uint8_t { R_HOP, R_FOUND, R_FOUND_BACK, R_NODE, R_ADD, R_TRACE, R_PROBE, R_TO_NODE, R_SAVE, R_KEEP, R_DISCOVER };
static int triesOf(int i) { return i == 0 ? 1 : i == 1 ? 3 : 5; } static int triesOf(int i) { return i == 0 ? 1 : i == 1 ? 3 : 5; }
PathEditor& R() { return _task->core().route; } PathEditor& R() { return _task->core().route; }
@@ -43,14 +43,17 @@ class RouteView {
out[k++] = R_SAVE; out[k++] = R_SAVE;
out[k++] = R_KEEP; out[k++] = R_KEEP;
out[k++] = R_DISCOVER; out[k++] = R_DISCOVER;
for (int i = 0; i < r.foundCount(); i++) out[k++] = R_FOUND; for (int i = 0; i < r.foundCount(); i++) { // a way back of its own: a row to use that too
out[k++] = R_FOUND;
if (!r.foundBackSame(i)) out[k++] = R_FOUND_BACK;
}
return k; return k;
} }
static const int ROWS_MAX = PathEditor::MAX_HOPS + 8 + PathEditor::FOUND_MAX; static const int ROWS_MAX = PathEditor::MAX_HOPS + 8 + 2 * PathEditor::FOUND_MAX;
// Which found route row `idx` is. // Which found route row `idx` (R_FOUND or the R_FOUND_BACK after it) is of.
static int foundIndex(const Row* kind, int idx) { static int foundIndex(const Row* kind, int idx) {
int f = 0; int f = -1;
for (int i = 0; i < idx; i++) if (kind[i] == R_FOUND) f++; for (int i = 0; i <= idx; i++) if (kind[i] == R_FOUND) f++;
return f; return f;
} }
@@ -192,6 +195,12 @@ public:
snprintf(right, sizeof(right), "%ux", (unsigned)r.found(f).times); snprintf(right, sizeof(right), "%ux", (unsigned)r.found(f).times);
break; break;
} }
case R_FOUND_BACK: { // the way the reply came, turned round as the list will have it
char route[40];
r.foundText(foundIndex(kind, idx), true, route, sizeof(route), true);
snprintf(left, sizeof(left), " Back %s", route);
break;
}
} }
const int rw = right[0] ? display.getTextWidth(right) + display.getCharWidth() : 0; const int rw = right[0] ? display.getTextWidth(right) + display.getCharWidth() : 0;
display.drawTextEllipsized(2, y, display.width() - reserve - 4 - rw, left, sel); display.drawTextEllipsized(2, y, display.width() - reserve - 4 - rw, left, sel);
@@ -260,10 +269,11 @@ public:
else if (!r.discover(triesOf(_tries))) _task->showAlert("Send failed", 1000); else if (!r.discover(triesOf(_tries))) _task->showAlert("Send failed", 1000);
else _seen_disc = true; else _seen_disc = true;
break; break;
case R_FOUND: { case R_FOUND: case R_FOUND_BACK: {
const int f = foundIndex(kind, _sel); const int f = foundIndex(kind, _sel);
if (PathEditor::hopsOf(r.found(f).out_len) > PathEditor::MAX_HOPS) { _task->showAlert("Too long", 1000); break; } const bool back = k == R_FOUND_BACK;
if (r.useFound(f)) { _task->showAlert("In list: Trace, Save", 1200); _sel = 0; } if (PathEditor::foundHops(r.found(f), back) > PathEditor::MAX_HOPS) { _task->showAlert("Too long", 1000); break; }
if (r.useFound(f, back)) { _task->showAlert(back ? "Way back in list" : "In list: Trace, Save", 1200); _sel = 0; }
break; break;
} }
} }