feat(route): Discover - path discovery from the device, 1/3/5 tries 20 s apart, the routes found to use in the route editor

MyMesh::sendPathDiscovery() shares the request with the app's
CMD_SEND_PATH_DISCOVERY_REQ and hands the reply (the way there and back)
to the UI; PathEditor keeps up to 4 distinct routes with how often each
came. L2: Find the route under Route; L1: Discover row in Path / trace.
Docs: Discover, message routes on the map, Ask status and discovery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-26 23:09:23 +01:00
co-authored by Claude Opus 5.5
parent f4ad1bd643
commit cce2798d2d
9 changed files with 400 additions and 17 deletions
+34 -10
View File
@@ -1058,11 +1058,44 @@ namespace {
};
}
// 'Path Discovery' is just a special case of flood + Telemetry req: the
// reply comes back as a path return carrying both ways (onContactPathRecv()).
int MyMesh::sendDiscoveryReq(ContactInfo& recipient, uint32_t& tag, uint32_t& est_timeout) {
uint8_t req_data[9];
req_data[0] = REQ_TYPE_GET_TELEMETRY_DATA;
req_data[1] = ~(TELEM_PERM_BASE); // NEW: inverse permissions mask (ie. we only want BASE telemetry)
memset(&req_data[2], 0, 3); // reserved
getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique
auto save = recipient.out_path_len; // temporarily force sendRequest() to flood
recipient.out_path_len = OUT_PATH_UNKNOWN;
int result = sendRequest(recipient, req_data, sizeof(req_data), tag, est_timeout);
recipient.out_path_len = save;
return result;
}
uint32_t MyMesh::sendPathDiscovery(const uint8_t* pub_key, DiscoverCallback cb, uint32_t* est_timeout) {
ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (!recipient) return 0;
uint32_t tag, est;
if (sendDiscoveryReq(*recipient, tag, est) == MSG_SEND_FAILED) return 0;
_ui_discovery = tag;
_discover_cb = cb;
if (est_timeout) *est_timeout = est;
return tag;
}
bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) {
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);
return false; // DON'T send reciprocal path, nor take it: the user picks
}
if (tag == pending_discovery) { // check for matching response tag)
pending_discovery = 0;
@@ -2244,16 +2277,7 @@ void MyMesh::handleCmdFrame(size_t len) {
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
uint32_t tag, est_timeout;
// 'Path Discovery' is just a special case of flood + Telemetry req
uint8_t req_data[9];
req_data[0] = REQ_TYPE_GET_TELEMETRY_DATA;
req_data[1] = ~(TELEM_PERM_BASE); // NEW: inverse permissions mask (ie. we only want BASE telemetry)
memset(&req_data[2], 0, 3); // reserved
getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique
auto save = recipient->out_path_len; // temporarily force sendRequest() to flood
recipient->out_path_len = OUT_PATH_UNKNOWN;
int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout);
recipient->out_path_len = save;
int result = sendDiscoveryReq(*recipient, tag, est_timeout);
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
+12
View File
@@ -277,6 +277,15 @@ public:
bool setContactPinned(const uint8_t* pub_key, bool pin); // keep / release the stored route
bool setContactPath(const uint8_t* pub_key, const uint8_t* path, uint8_t hash_size, uint8_t hops, bool pin = false);
void setPingCallback(PingCallback cb, void* arg);
// Path discovery from this device's UI (PathEditor::discover()): the node is
// asked by flood, and its reply brings the way the flood reached it (out)
// and the way the reply came back (in), as encoded path lengths and hashes,
// to the callback. The tag, 0 when not sent. One at a time; the app's own
// discovery (CMD_SEND_PATH_DISCOVERY_REQ) is answered to the app as before.
typedef void (*DiscoverCallback)(const uint8_t* pub_key, const uint8_t* out_path, uint8_t out_path_len,
const uint8_t* in_path, uint8_t in_path_len);
uint32_t sendPathDiscovery(const uint8_t* pub_key, DiscoverCallback cb, uint32_t* est_timeout = nullptr);
void cancelPathDiscovery() { _ui_discovery = 0; }
void clearPingResult(uint32_t tag);
PingResult* getPingResult(uint32_t tag);
PingCallback getPingCallback() const { return _ping_callback; }
@@ -669,6 +678,9 @@ private:
uint32_t ui_pending_admin_reply; // pub_key prefix of the contact AdminScreen's sendAdminCommand() is awaiting a CLI reply from
uint32_t pending_status;
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
uint32_t _ui_discovery = 0; // sendPathDiscovery()'s, answered to _discover_cb
DiscoverCallback _discover_cb = nullptr;
int sendDiscoveryReq(ContactInfo& recipient, uint32_t& tag, uint32_t& est_timeout);
uint32_t pending_req; // pending _BINARY_REQ
BaseSerialInterface *_serial;
Listener* _listener;
@@ -12,6 +12,9 @@
// ... repeater, to find the link where it stops
// poll() -- advance a running trace (call every UI pass)
// 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
//
// 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
@@ -52,6 +55,9 @@ public:
ContactInfo* c = the_mesh.lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (!c) return false;
cancelTrace();
stopDiscover();
_found_n = 0;
_disc_done = _disc_answered = 0;
memcpy(_target_key, c->id.pub_key, PUB_KEY_SIZE);
snprintf(_target_name, sizeof(_target_name), "%s", c->name);
_target_repeats = c->type == ADV_TYPE_REPEATER;
@@ -195,6 +201,7 @@ public:
// Call every UI pass while a trace may be running.
void poll() {
pollDiscover();
if (_state != RUNNING || !_ping) return;
if (!_ping->isActive()) { // the reply came (the engine released its slot), or it was dropped
if (_ping->hopCount() > 0) {
@@ -245,6 +252,83 @@ public:
return _links > 0;
}
// ── discover ────────────────────────────────────────────────────────────────
// The node is asked by flood (MyMesh::sendPathDiscovery()); its reply says
// which repeaters the flood went through to reach it, and which the reply
// took back -- learned from the mesh, not from anyone's position. A flood
// goes everywhere, so several tries wait for each other's reply (or its
// timeout) and then DISCOVER_GAP_MS more. Each different route is kept
// with how many times it came; the fastest is not always the best, so the
// user picks one (useFound()) and can trace it.
static const int FOUND_MAX = 4;
static const int FOUND_HOPS = 8; // hops kept of a route (one longer than the list holds: "too long")
static const uint32_t DISCOVER_GAP_MS = 20000;
struct Found {
uint8_t out_len, back_len; // encoded path lengths (hop count | (hash size - 1) << 6)
uint8_t out[FOUND_HOPS * 3], back[FOUND_HOPS * 3];
uint8_t times;
};
bool discover(int tries) {
if (tries < 1) tries = 1;
stopDiscover();
_found_n = 0;
_disc_tries = (uint8_t)tries;
_disc_done = _disc_answered = 0;
return discoverSend();
}
void stopDiscover() {
if (_disc_waiting) the_mesh.cancelPathDiscovery();
_disc_waiting = false;
_disc_tries = 0;
}
bool discovering() const { return _disc_tries > 0 && _disc_done < _disc_tries; }
bool discoverWaiting() const { return _disc_waiting; } // a try is out, its reply awaited
int discoverTry() const { return _disc_done + (_disc_waiting ? 1 : 0); } // 1-based while waiting
int discoverTries() const { return _disc_tries; }
int discoverDone() const { return _disc_done; }
int discoverAnswered() const { return _disc_answered; }
bool discoverFailed() const { return _disc_failed; }
// Seconds to the next try, between tries.
int discoverNextIn() const {
if (!discovering() || _disc_waiting) return 0;
int32_t ms = (int32_t)(_disc_at - millis());
return ms > 0 ? (ms + 999) / 1000 : 0;
}
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 {
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;
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++) {
char nm[NAME_LEN];
hashName(&p[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) {
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;
cancelTrace();
_n = 0;
for (int h = 0; h < hops && h < MAX_HOPS; h++) loadHash(&f.out[h * size], size);
_too_long = hops > MAX_HOPS;
_edited = true;
invalidate();
return true;
}
// ── save ────────────────────────────────────────────────────────────────────
// The list as the contact's stored route; an empty list stores "direct"
// (no repeaters, sent straight to the node). False when the hops share no
@@ -265,6 +349,86 @@ public:
}
private:
// The editor a discovery reply goes to (one editor, one discovery at a time).
static PathEditor*& discOwner() { static PathEditor* p = nullptr; return p; }
bool discoverSend() {
uint32_t est = 0;
discOwner() = this;
_disc_failed = false;
if (!the_mesh.sendPathDiscovery(_target_key, onDiscovered, &est)) {
_disc_failed = true;
_disc_tries = 0;
return false;
}
_disc_waiting = true;
uint32_t wait = est + 4000; // the flood there and the reply's flood back
if (wait < 8000) wait = 8000;
_disc_at = millis() + wait;
return true;
}
void pollDiscover() {
if (_disc_tries == 0) return;
if (_disc_waiting) {
if ((int32_t)(millis() - _disc_at) < 0) return;
the_mesh.cancelPathDiscovery(); // no reply in time
tryOver();
} else if (_disc_done < _disc_tries && (int32_t)(millis() - _disc_at) >= 0) {
discoverSend();
}
}
// A try answered or timed out: the next one after the pause.
void tryOver() {
_disc_waiting = false;
_disc_done++;
_disc_at = millis() + DISCOVER_GAP_MS;
}
static void onDiscovered(const uint8_t* key, const uint8_t* out, uint8_t out_len, const uint8_t* back, uint8_t back_len) {
PathEditor* e = discOwner();
if (!e || !e->_disc_waiting || memcmp(key, e->_target_key, PUB_KEY_SIZE) != 0) return;
e->record(out, out_len, back, back_len);
e->_disc_answered++;
e->tryOver();
}
// A route found: another time for one already found (the latest way back
// with it), else a new one while there's room.
void record(const uint8_t* out, uint8_t out_len, const uint8_t* back, uint8_t back_len) {
const int osize = (out_len >> 6) + 1, bsize = (back_len >> 6) + 1;
if (osize > 3 || bsize > 3) return;
const int obytes = (hopsOf(out_len) < FOUND_HOPS ? hopsOf(out_len) : FOUND_HOPS) * osize;
const int bbytes = (hopsOf(back_len) < FOUND_HOPS ? hopsOf(back_len) : FOUND_HOPS) * bsize;
Found* f = nullptr;
for (int i = 0; i < _found_n && !f; i++)
if (_found[i].out_len == out_len && memcmp(_found[i].out, out, obytes) == 0) f = &_found[i];
if (f) {
if (f->times < 255) f->times++;
} else {
if (_found_n >= FOUND_MAX) return;
f = &_found[_found_n++];
f->out_len = out_len;
memcpy(f->out, out, obytes);
f->times = 1;
}
f->back_len = back_len;
memcpy(f->back, back, bbytes);
}
// A hash's repeater by name, else "?3F".
static void hashName(const uint8_t* hash, uint8_t size, char* out, size_t n) {
for (int idx = MAX_ANON_CONTACTS; ; idx++) {
ContactInfo c;
if (!the_mesh.getContactByIdx(idx, c)) break;
if (c.type == ADV_TYPE_REPEATER && c.id.isHashMatch(hash, size)) { snprintf(out, n, "%s", c.name); return; }
}
char hex[9] = {0};
for (uint8_t k = 0; k < size && k < 4; k++) snprintf(hex + k * 2, 3, "%02X", hash[k]);
snprintf(out, n, "?%s", hex);
}
uint8_t saveWidth() const { // 1..3 bytes of a stored path, the hash mode narrowed to what every hop has
uint8_t want = _prefs ? _prefs->path_hash_mode + 1 : 1;
if (want > 3) want = 3;
@@ -386,4 +550,9 @@ private:
int _links = 0;
int16_t _out_x4[MAX_NODES];
int16_t _back_x4[MAX_NODES];
Found _found[FOUND_MAX];
int _found_n = 0;
uint8_t _disc_tries = 0, _disc_done = 0, _disc_answered = 0;
bool _disc_waiting = false, _disc_failed = false;
uint32_t _disc_at = 0; // waiting: the reply's deadline; between tries: the next one's time
};
@@ -39,6 +39,11 @@ static void linkText(char* out, size_t n, int16_t out_x4, int16_t back_x4) {
static int s_sel = -1; // the hop showing its move / remove buttons
static const int ROW_TARGET = 0xFE; // a row's tap code: the node (trace to it or not)
// Discover: how many tries (TRIES[s_tries]), kept while the device runs.
static const char* TRIES_MAP[] = { "1", "3", "5", "" };
static const int TRIES[] = { 1, 3, 5 };
static int s_tries = 1;
} // namespace routeview
static void onRouteAct(lv_event_t* e) { s_ui->routeAct((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
@@ -47,6 +52,11 @@ static void onRoutePickRow(lv_event_t* e) { s_ui->routePick((int)vlist::arg(e));
static void onRouteRow(lv_event_t* e) { s_ui->routeOp(routeview::OP_SEL << 8 | (int)(uintptr_t)lv_event_get_user_data(e)); }
static void onRouteKeep(lv_event_t* e) { (void)e; s_ui->routeAct(routeview::RT_PIN); }
static void onRouteDiscover(lv_event_t* e) { (void)e; s_ui->routeDiscover(); }
static void onRouteFound(lv_event_t* e) { s_ui->routeUseFound((int)(uintptr_t)lv_event_get_user_data(e)); }
static void onRouteTries(lv_event_t* e) {
s_ui->routeDiscTries((int)lv_buttonmatrix_get_selected_button((lv_obj_t*)lv_event_get_target(e)));
}
// A small square button on a hop's row: user data = op << 8 | hop index.
static lv_obj_t* routeSquare(lv_obj_t* row, const char* sym, int code) {
@@ -134,6 +144,9 @@ void UITask::buildRoute() {
lv_obj_set_style_pad_row(_route_list, 4, 0);
_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));
_route_disc_shown = 0xFFFFFFFF;
lv_obj_t* bar = buttonBar(body);
barButton(bar, LV_SYMBOL_PLUS " Add", onRouteAct, routeview::RT_ADD);
@@ -141,6 +154,7 @@ void UITask::buildRoute() {
_route_trace_btn = barButton(bar, LV_SYMBOL_LOOP " Trace", onRouteAct, routeview::RT_TRACE, true);
_route_save_btn = barButton(bar, LV_SYMBOL_SAVE " Save", onRouteAct, routeview::RT_SAVE);
routeRebuild();
routeDiscRebuild();
}
// What the chain shows: the state, the links with a result, a probe's step.
@@ -265,6 +279,83 @@ void UITask::routeStatus() {
void UITask::routePoll() {
_core->route.poll();
if (routeSig() != _route_shown) routeRebuild();
if (routeDiscSig() != _route_disc_shown) routeDiscRebuild();
}
// ── Discover ──────────────────────────────────────────────────────────────────
uint32_t UITask::routeDiscSig() {
PathEditor& R = _core->route;
return (uint32_t)R.discovering() | (uint32_t)R.discoverWaiting() << 1 | (uint32_t)R.discoverDone() << 2 |
(uint32_t)R.discoverAnswered() << 6 | (uint32_t)R.foundCount() << 10 | (uint32_t)R.discoverNextIn() << 13 |
(uint32_t)R.discoverFailed() << 20 | (uint32_t)routeview::s_tries << 21 | (uint32_t)(R.discoverTries() > 0) << 23;
}
// Under the chain: how many tries and Discover (Stop while it runs), where
// it is, then each route found -- its repeaters, how often it came, the way
// back -- a tap putting it in the list above.
void UITask::routeDiscRebuild() {
if (!_route_disc) return;
using namespace routeview;
PathEditor& R = _core->route;
lv_obj_clean(_route_disc);
lv_obj_t* g = group(_route_disc, "FIND THE ROUTE");
lv_obj_t* row = groupLine(g, false);
groupText(row, "Discover", "Ask the node which way it's reached: a flood each try, 20 s apart");
lv_obj_t* seg = segmented(row, TRIES_MAP, s_tries, 84, 30);
lv_obj_add_event_cb(seg, onRouteTries, LV_EVENT_VALUE_CHANGED, NULL);
char t[160];
t[0] = '\0';
if (R.discoverFailed()) snprintf(t, sizeof(t), "Couldn't send the request");
else if (R.discoverWaiting())
snprintf(t, sizeof(t), "Try %d of %d: waiting for the reply...", R.discoverTry(), R.discoverTries());
else if (R.discovering())
snprintf(t, sizeof(t), "%d of %d answered, next try in %d s", R.discoverAnswered(), R.discoverDone(), R.discoverNextIn());
else if (R.discoverTries() > 0 && R.discoverAnswered())
snprintf(t, sizeof(t), "%d of %d answered", R.discoverAnswered(), R.discoverDone());
else if (R.discoverTries() > 0)
snprintf(t, sizeof(t), "No answer to %d tr%s. A companion answers only if its Settings > Privacy > Status lets you, a repeater after a login",
R.discoverDone(), R.discoverDone() == 1 ? "y" : "ies");
if (t[0]) {
lv_obj_t* l = groupLine(g, false);
noteLabel(l, t, THEME_FONT_SMALL, R.discoverFailed() ? theme::FAIL : theme::TEXT_MUTED);
}
for (int i = 0; i < R.foundCount(); i++) {
const PathEditor::Found& f = R.found(i);
char route[96], back[64], sub[96];
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);
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);
}
lv_obj_t* bar = buttonBar(_route_disc);
barButton(bar, R.discovering() ? LV_SYMBOL_CLOSE " Stop" : LV_SYMBOL_REFRESH " Discover", onRouteDiscover, 0, !R.discovering());
_route_disc_shown = routeDiscSig();
}
void UITask::routeDiscover() {
PathEditor& R = _core->route;
if (R.discovering()) R.stopDiscover();
else R.discover(routeview::TRIES[routeview::s_tries]);
routeDiscRebuild();
}
void UITask::routeDiscTries(int sel) {
if (sel >= 0 && sel < 3) routeview::s_tries = sel;
}
void UITask::routeUseFound(int i) {
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");
routeRebuild();
}
void UITask::routeAct(uint8_t act) {
@@ -127,6 +127,11 @@ public:
void routeStatus();
uint32_t routeSig();
void routePoll();
void routeDiscRebuild();
uint32_t routeDiscSig();
void routeDiscover();
void routeDiscTries(int sel);
void routeUseFound(int i);
void routeAct(uint8_t act);
void routeOp(int code);
void routeKeep();
@@ -664,6 +669,8 @@ private:
lv_obj_t* _route_save_btn = nullptr;
lv_obj_t* _route_probe_row = nullptr; // "Find where it stops", after a trace with no reply
lv_obj_t* _route_note = nullptr;
lv_obj_t* _route_disc = nullptr; // Discover: the tries, its progress, the routes found
uint32_t _route_disc_shown = 0xFFFFFFFF; // what routeDiscRebuild() drew (routeDiscSig())
lv_obj_t* _route_keep_btn = nullptr; // header: Keep (accent while kept)
uint32_t _route_shown = 0xFFFFFFFF; // what routeRebuild() drew (routeSig())
uint32_t _next_route_ms = 0;
+51 -5
View File
@@ -21,12 +21,16 @@ class RouteView {
int _menu_hop = 0;
uint8_t _seen_state = PathEditor::IDLE; // a result's alert, once
uint8_t _sel_kind = 0xFF; // the action row under the cursor, kept as rows come and go
bool _seen_disc = false; // a discovery running at the last pass: its end said once
uint8_t _tries = 1; // Discover: triesOf(_tries) tries
enum Row : uint8_t { R_HOP, R_NODE, R_ADD, R_TRACE, R_PROBE, R_TO_NODE, R_SAVE, R_KEEP };
// 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 };
static int triesOf(int i) { return i == 0 ? 1 : i == 1 ? 3 : 5; }
PathEditor& R() { return _task->core().route; }
// The rows: the hops, the node, then the actions (Find break only after a
// trace with no reply).
// trace with no reply), Discover and the routes it found.
int rows(Row* out) {
PathEditor& r = R();
int k = 0;
@@ -38,9 +42,17 @@ class RouteView {
if (r.count() > 0) out[k++] = R_TO_NODE; // with no repeaters the trace is to the node
out[k++] = R_SAVE;
out[k++] = R_KEEP;
out[k++] = R_DISCOVER;
for (int i = 0; i < r.foundCount(); i++) out[k++] = R_FOUND;
return k;
}
static const int ROWS_MAX = PathEditor::MAX_HOPS + 7;
static const int ROWS_MAX = PathEditor::MAX_HOPS + 8 + PathEditor::FOUND_MAX;
// Which found route row `idx` is.
static int foundIndex(const Row* kind, int idx) {
int f = 0;
for (int i = 0; i < idx; i++) if (kind[i] == R_FOUND) f++;
return f;
}
void openHopMenu(int i) {
_menu_hop = i;
@@ -58,6 +70,13 @@ class RouteView {
// What a result means, said once when it comes.
void announce() {
PathEditor& r = R();
if (_seen_disc && !r.discovering() && r.discoverTries() > 0) { // a discovery just ended
char t[32];
if (r.discoverAnswered()) snprintf(t, sizeof(t), "%d/%d answered", r.discoverAnswered(), r.discoverDone());
else snprintf(t, sizeof(t), "No answer");
_task->showAlert(t, 1500);
}
_seen_disc = r.discovering();
if (r.state() == _seen_state) return;
_seen_state = r.state();
char t[48];
@@ -99,6 +118,7 @@ public:
_menu.active = false;
_seen_state = PathEditor::IDLE;
_sel_kind = 0xFF;
_seen_disc = false;
return true;
}
// Back from picking a repeater: the cursor on Add, for the next one.
@@ -108,7 +128,7 @@ public:
_sel_kind = R_ADD;
}
void close() {
if (_task) R().cancelTrace();
if (_task) { R().cancelTrace(); R().stopDiscover(); }
_active = false;
_menu.active = false;
}
@@ -158,6 +178,20 @@ public:
case R_TO_NODE: snprintf(left, sizeof(left), "To node"); snprintf(right, sizeof(right), "%s", r.toNode() ? "ON" : "OFF"); break;
case R_SAVE: snprintf(left, sizeof(left), "%s", n ? "Save path" : "Save direct"); break;
case R_KEEP: snprintf(left, sizeof(left), "Keep path"); snprintf(right, sizeof(right), "%s", r.pinned() ? "ON" : "OFF"); break;
case R_DISCOVER:
if (r.discoverWaiting()) snprintf(left, sizeof(left), "Discover %d/%d...", r.discoverTry(), r.discoverTries());
else if (r.discovering()) snprintf(left, sizeof(left), "Next try %ds", r.discoverNextIn());
else snprintf(left, sizeof(left), "Discover");
snprintf(right, sizeof(right), "%s", r.discovering() ? "Stop" : _tries == 0 ? "1 try" : _tries == 1 ? "3 tries" : "5 tries");
break;
case R_FOUND: {
const int f = foundIndex(kind, idx);
char route[40];
r.foundText(f, false, route, sizeof(route));
snprintf(left, sizeof(left), "Use %s", route);
snprintf(right, sizeof(right), "%ux", (unsigned)r.found(f).times);
break;
}
}
const int rw = right[0] ? display.getTextWidth(right) + display.getCharWidth() : 0;
display.drawTextEllipsized(2, y, display.width() - reserve - 4 - rw, left, sel);
@@ -167,7 +201,7 @@ public:
}
});
if (_menu.active) _menu.render(display);
return r.state() == PathEditor::RUNNING ? 100 : 2000;
return r.state() == PathEditor::RUNNING ? 100 : r.discovering() ? 500 : 2000;
}
// Returns true: the key was used (the view is modal).
@@ -196,6 +230,7 @@ public:
if (k == R_HOP) { if (r.move(_sel, dir)) _sel += dir; } // reorder in place
else if (k == R_TO_NODE) r.setToNode(!r.toNode());
else if (k == R_KEEP) toggleKeep();
else if (k == R_DISCOVER && !r.discovering()) _tries = (uint8_t)((_tries + 3 + dir) % 3);
return true;
}
if (c != KEY_ENTER && c != KEY_CONTEXT_MENU) return true;
@@ -220,6 +255,17 @@ public:
else _task->showAlert(r.count() ? "Path saved" : "Saved: direct", 1000);
break;
case R_KEEP: toggleKeep(); break;
case R_DISCOVER:
if (r.discovering()) r.stopDiscover();
else if (!r.discover(triesOf(_tries))) _task->showAlert("Send failed", 1000);
else _seen_disc = true;
break;
case R_FOUND: {
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; }
break;
}
}
return true;
}