diff --git a/docs/solo/contacts.md b/docs/solo/contacts.md index 8fe1dcc6..17fcd28b 100644 --- a/docs/solo/contacts.md +++ b/docs/solo/contacts.md @@ -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 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 -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. 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 > 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 -> it. +> it. When the way back is another one, **There** and **Back** put either in +> the list to trace. ## Your own contact diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b968c560..2b1b1abb 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1079,6 +1079,8 @@ uint32_t MyMesh::sendPathDiscovery(const uint8_t* pub_key, DiscoverCallback cb, uint32_t tag, est; if (sendDiscoveryReq(*recipient, tag, est) == MSG_SEND_FAILED) return 0; _ui_discovery = tag; + _ui_disc_sent[_ui_disc_next] = tag; + _ui_disc_next = (_ui_disc_next + 1) % UI_DISC_KEEP; _discover_cb = cb; if (est_timeout) *est_timeout = est; return tag; @@ -1089,10 +1091,16 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i uint32_t tag; memcpy(&tag, extra, 4); - if (_ui_discovery && tag == _ui_discovery) { // this device's own (sendPathDiscovery()) - _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); + // This device's own (sendPathDiscovery()): to the editor while it waits + // for it; late (timed out, stopped, an earlier try's) only dropped. + for (int k = 0; k < UI_DISC_KEEP; k++) { + 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 } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 81379963..2cfcc442 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -679,6 +679,9 @@ private: uint32_t pending_status; uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ 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; int sendDiscoveryReq(ContactInfo& recipient, uint32_t& tag, uint32_t& est_timeout); uint32_t pending_req; // pending _BINARY_REQ diff --git a/examples/companion_radio/ui-core/PathEditor.h b/examples/companion_radio/ui-core/PathEditor.h index 04536eb6..8528a477 100644 --- a/examples/companion_radio/ui-core/PathEditor.h +++ b/examples/companion_radio/ui-core/PathEditor.h @@ -14,7 +14,8 @@ // save() -- the list becomes the contact's out_path // discover(tries) -- asks the node by flood which way it's reached (path // 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 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; } const Found& found(int i) const { return _found[i]; } 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. - void foundText(int i, bool back, char* out, size_t n) const { + // Route i's hops, "Name > Name" (hash "?3F" for one not a contact), cut to + // 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 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; + const int kept = hops < FOUND_HOPS ? hops : FOUND_HOPS; + const bool rev = back && as_route; size_t o = 0; out[0] = '\0'; 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]; - 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); } 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). - bool useFound(int i) { + // The reply's way back, turned round, is route i itself: one way both ways. + 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; 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(); _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; _edited = true; invalidate(); diff --git a/examples/companion_radio/ui-lvgl/GalleryScreen.h b/examples/companion_radio/ui-lvgl/GalleryScreen.h index 5526c000..6090e71f 100644 --- a/examples/companion_radio/ui-lvgl/GalleryScreen.h +++ b/examples/companion_radio/ui-lvgl/GalleryScreen.h @@ -26,6 +26,7 @@ static PSRAM_BSS char s_names[SHOTS_MAX][16]; static int s_n = 0; static int s_page = 0; // the grid's page, from the newest 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 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() { s_n = 0; + s_listed = true; DIR* d = opendir(SHOTS_DIR); if (!d) return; while (struct dirent* de = readdir(d)) { 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 (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); // 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; } -// 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 { FILE* f = nullptr; int32_t w = 0, h = 0; uint32_t off = 0, stride = 0; 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) { f = fopen(p, "rb"); uint8_t hdr[54]; @@ -110,13 +121,26 @@ struct Bmp { stride = ((uint32_t)w * 3 + 3) & ~3u; return true; } - bool row(int y, uint8_t* out) { // BGR - if (fseek(f, (long)(off + (uint32_t)(bottom_up ? h - 1 - y : y) * stride), SEEK_SET) != 0) return false; - return fread(out, 1, (size_t)w * 3, f) == (size_t)w * 3; + // Rows lo..hi in buf (hi - lo < ch), with the next ones in the file's + // order: a bottom-up file is read from its end, upwards on the screen. + 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); } }; -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 // 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)); Bmp b; 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; - if (!b.row(sy, s_row[0]) || (sy2 != sy && !b.row(sy2, s_row[1]))) return false; - const uint8_t* r1 = s_row[0]; - const uint8_t* r2 = sy2 != sy ? s_row[1] : s_row[0]; + if (!b.load(sy, sy2)) return false; + const uint8_t* r1 = b.row(sy); + const uint8_t* r2 = b.row(sy2); for (int tx = 0; tx < w; tx++) { 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; @@ -184,6 +211,7 @@ static void onGalleryViewTouch(lv_event_t* e) { void UITask::showGallery() { _screen = SCR_GALLERY; gallery::s_page = 0; + gallery::s_listed = false; buildGallery(); } @@ -230,7 +258,7 @@ void UITask::buildGallery() { } 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) { noteLabel(list, "No screenshots yet. Hold the side button and press the top one to take one.", THEME_FONT_BODY); return; @@ -246,7 +274,7 @@ void UITask::buildGallery() { const int first = s_page * PAGE; const int cells = s_n - first < PAGE ? s_n - first : PAGE; - uint16_t* px = (uint16_t*)psramBuf((size_t)PAGE * TW * TH); + uint16_t* px = (uint16_t*)psramBuf((size_t)cells * TW * TH); if (!px) { noteLabel(list, "Out of memory", THEME_FONT_BODY); return; } s_thumbs = px; s_cells = cells; @@ -313,7 +341,13 @@ void UITask::galleryOpen(int i) { if (!s_full) s_full = (uint16_t*)psramBuf((size_t)SW * SH); if (!s_full) { showToast("Out of memory"); return; } 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; dscInit(s_fdsc, s_full, SW, SH); if (!s_view) { diff --git a/examples/companion_radio/ui-lvgl/LvglPort.h b/examples/companion_radio/ui-lvgl/LvglPort.h index bbb77a3b..3138a4a9 100644 --- a/examples/companion_radio/ui-lvgl/LvglPort.h +++ b/examples/companion_radio/ui-lvgl/LvglPort.h @@ -625,7 +625,9 @@ static int netState() { // itself; ntpTake() hands the time on to rtc_clock (a hardware RTC, if any). static volatile uint32_t s_ntp_secs = 0; 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 ntpStart() { if (esp_sntp_enabled()) esp_sntp_stop(); diff --git a/examples/companion_radio/ui-lvgl/RouteScreen.h b/examples/companion_radio/ui-lvgl/RouteScreen.h index d87b4779..0afa4fe4 100644 --- a/examples/companion_radio/ui-lvgl/RouteScreen.h +++ b/examples/companion_radio/ui-lvgl/RouteScreen.h @@ -146,6 +146,7 @@ void UITask::buildRoute() { _route_note = noteLabel(scroll, "", THEME_FONT_SMALL, theme::TEXT_MUTED); _route_disc = flexBox(scroll, LV_FLEX_FLOW_COLUMN); 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; lv_obj_t* bar = buttonBar(body); @@ -321,18 +322,29 @@ void UITask::routeDiscRebuild() { lv_obj_t* l = groupLine(g, false); 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++) { 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, true, back, sizeof(back)); 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", - hops > PathEditor::MAX_HOPS ? " (too long to use)" : "", (unsigned)f.times, f.times == 1 ? "" : "s", back); - lv_obj_t* r = groupLine(g, true); + const bool same = R.foundBackSame(i); + int o = snprintf(sub, sizeof(sub), "%d hop%s%s, found %u time%s. ", hops, hops == 1 ? "" : "s", + 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); - label(r, "Use", THEME_FONT_SMALL, theme::ACCENT); - lv_obj_add_event_cb(r, onRouteFound, LV_EVENT_CLICKED, (void*)(uintptr_t)i); + if (same) { + 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); 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; } -void UITask::routeUseFound(int i) { +void UITask::routeUseFound(int code) { PathEditor& R = _core->route; - if (PathEditor::hopsOf(R.found(i).out_len) > PathEditor::MAX_HOPS) { showToast("Too long: up to 7 repeaters"); return; } - if (!R.useFound(i)) return; - showToast("In the list: Trace to check it, Save to use it"); + const int i = code >> 1; + const bool back = code & 1; + 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(); } diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index af16216f..e05c123a 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -948,14 +948,19 @@ void UITask::takeScreenshot() { 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/screenshots", 0777); - char path[48]; - struct stat st; - int n = 1; - for (; n <= 9999; n++) { - snprintf(path, sizeof(path), "/sdcard/screenshots/scr_%04d.bmp", n); - if (stat(path, &st) != 0) break; + // One past the highest number: a deleted one's isn't reused, so the newest + // stays first in the Gallery (one pass over the folder, not a stat a file). + int n = 0; + if (DIR* d = opendir("/sdcard/screenshots")) { + while (struct dirent* de = readdir(d)) { + 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; uint16_t* px = captureFrame(W, H); 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_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 + case SCR_ROUTE: // back to the node; a trace or discovery in flight is dropped _core->route.cancelTrace(); + _core->route.stopDiscover(); _screen = SCR_NODE; buildNode(); break; @@ -1527,8 +1533,9 @@ extern "C" EMSCRIPTEN_KEEPALIVE int sim_scroll(int dy) { dbgScrollable(lv_layer_top(), o, h); if (!o) dbgScrollable(lv_screen_active(), o, h); if (!o) return -1; - int left = lv_obj_get_scroll_bottom(o); - lv_obj_scroll_by(o, 0, -(dy < left ? dy : left), LV_ANIM_OFF); + int left = dy >= 0 ? lv_obj_get_scroll_bottom(o) : lv_obj_get_scroll_top(o); // up: no further than the top + 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; } #endif diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index a38d63b6..83de446f 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -131,7 +131,7 @@ public: uint32_t routeDiscSig(); void routeDiscover(); 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 routeOp(int code); void routeKeep(); diff --git a/examples/companion_radio/ui-lvgl/WifiScreen.h b/examples/companion_radio/ui-lvgl/WifiScreen.h index 5ee443b1..d3f929fe 100644 --- a/examples/companion_radio/ui-lvgl/WifiScreen.h +++ b/examples/companion_radio/ui-lvgl/WifiScreen.h @@ -323,9 +323,11 @@ void UITask::wifiSave() { // 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 // 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() { - static uint32_t next_ms = 0; + static uint32_t next_ms = 0, ref_wall = 0, ref_ms = 0; static bool first = false; if ((int32_t)(millis() - next_ms) < 0) return; next_ms = millis() + 1000; @@ -338,9 +340,11 @@ void UITask::wifiClockTick() { const uint32_t t = lvport::ntpTake(); if (t > 1700000000UL) { const uint32_t now = rtc_clock.getCurrentTime(); - const bool moved = t > now + 1 || now > t + 1; - if (moved) rtc_clock.setCurrentTime(t); - if (first && moved && !_asleep) showToast("Clock set from the network"); + if (t > now + 1 || now > t + 1) rtc_clock.setCurrentTime(t); // a clock chip: SNTP set only the system time + const uint32_t was = ref_wall + (millis() - ref_ms) / 1000; + if (first && (t > was + 2 || was > t + 2) && !_asleep) showToast("Clock set from the network"); first = false; } + ref_wall = rtc_clock.getCurrentTime(); + ref_ms = millis(); } diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index 56ada9e5..ba470491 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -24,8 +24,8 @@ class RouteView { bool _seen_disc = false; // a discovery running at the last pass: its end said once uint8_t _tries = 1; // Discover: triesOf(_tries) tries - // R_FOUND 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 }; + // R_FOUND(_BACK) before R_NODE: like the hops, not an action the cursor follows. + 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; } PathEditor& R() { return _task->core().route; } @@ -43,14 +43,17 @@ class RouteView { out[k++] = R_SAVE; out[k++] = R_KEEP; 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; } - static const int ROWS_MAX = PathEditor::MAX_HOPS + 8 + PathEditor::FOUND_MAX; - // Which found route row `idx` is. + static const int ROWS_MAX = PathEditor::MAX_HOPS + 8 + 2 * PathEditor::FOUND_MAX; + // Which found route row `idx` (R_FOUND or the R_FOUND_BACK after it) is of. static int foundIndex(const Row* kind, int idx) { - int f = 0; - for (int i = 0; i < idx; i++) if (kind[i] == R_FOUND) f++; + int f = -1; + for (int i = 0; i <= idx; i++) if (kind[i] == R_FOUND) f++; return f; } @@ -192,6 +195,12 @@ public: snprintf(right, sizeof(right), "%ux", (unsigned)r.found(f).times); 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; 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 _seen_disc = true; break; - case R_FOUND: { + case R_FOUND: case R_FOUND_BACK: { const int f = foundIndex(kind, _sel); - if (PathEditor::hopsOf(r.found(f).out_len) > PathEditor::MAX_HOPS) { _task->showAlert("Too long", 1000); break; } - if (r.useFound(f)) { _task->showAlert("In list: Trace, Save", 1200); _sel = 0; } + const bool back = k == R_FOUND_BACK; + 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; } }