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
+21
View File
@@ -87,6 +87,23 @@ With no reply, **Find break** traces to the first repeater, then the second,
and so on, and shows where it stops. If every repeater answers, the node and so on, and shows where it stops. If every repeater answers, the node
itself didn't. itself didn't.
**Discover** finds the route for you. It asks the node by flood, and the
reply says which repeaters the flood went through to reach it, and which the
reply took back. These come from the mesh itself, not from anyone's position
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
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
Settings › Privacy › **Ask status** lets you (**Everyone**, or **Allowed**
with Telem given to you), or a repeater or room you're logged in to (a guest
login is enough). Your own Ask status decides the same for others finding a
route to you. Not to be confused with **Discover scan** in the node options,
which looks for nodes in direct range.
**Save path** makes the list the contact's route; saved empty, messages go **Save path** makes the list the contact's route; saved empty, messages go
straight to the node. The route a node sends back usually replaces it again; straight to the node. The route a node sends back usually replaces it again;
**Keep path** stops that, and keeps the route after failed sends too. **Keep path** stops that, and keeps the route after failed sends too.
@@ -99,6 +116,10 @@ straight to the node. The route a node sends back usually replaces it again;
> the node to trace to it or stop at the last repeater. **Keep** is in the > the node to trace to it or stop at the last repeater. **Keep** is in the
> header, **Find where** appears next to a "No reply". The repeaters can also > header, **Find where** appears next to a "No reply". The repeaters can also
> be tapped on the map, in order (**Map**); **Undo** takes the last one out. > be tapped on the map, in order (**Map**); **Undo** takes the last one out.
> **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.
## Your own contact ## Your own contact
+11 -1
View File
@@ -30,10 +30,20 @@ older and newer one.
contains a location (see [Navigation](./navigation.md)). contains a location (see [Navigation](./navigation.md)).
- **Path** on a received message: the repeaters it came through. - **Path** on a received message: the repeaters it came through.
**Relayed by** on your own channel post: the repeaters heard repeating it. **Relayed by** on your own channel post: the repeaters heard repeating it.
- **Show route**: when the message has a route written in it, such as a
bot's `!path` reply (`3f:a1:c8`, see [Remote bot](./tools.md#remote-bot)).
It draws the route on a small map: you, each repeater numbered in order,
and the sender, with how many of the hops could be placed (a repeater
needs a position). In a direct message the route starts from you, because
it's the way your message went; on a channel it's someone else's, so it
doesn't. **Back** closes it.
> [!NOTE] > [!NOTE]
> **Wio Tracker L2:** hold a bubble. The card shows when it was sent, the hop > **Wio Tracker L2:** hold a bubble. The card shows when it was sent, the hop
> count, the path as a diagram, and **Reply** / **Set target**. > count, the path as a diagram, and **Reply** / **Set target**, plus **Route**
> when the message has a route in it: the nodes map opens with the route drawn
> and its hops numbered, and the names along the bottom; **Done** goes back to
> the conversation.
### How much is kept ### How much is kept
+4 -1
View File
@@ -68,7 +68,10 @@ nodes aren't added until you make room.
**Allowed** (the contacts you gave that permission: **Telem** in a node's **Allowed** (the contacts you gave that permission: **Telem** in a node's
options in [Nodes](./contacts.md#nearby-nodes), or the phone app) or options in [Nodes](./contacts.md#nearby-nodes), or the phone app) or
**Everyone**. Status is the battery; a request for location or **Everyone**. Status is the battery; a request for location or
sensors is answered only where status is allowed too. sensors is answered only where status is allowed too. Status also lets
others find a route to you (**Discover** in
[Path and trace](./contacts.md#path-and-trace), or the phone app's path
discovery).
- **BT PIN**: the six-digit Bluetooth pairing PIN. Leave it empty for a new - **BT PIN**: the six-digit Bluetooth pairing PIN. Leave it empty for a new
random one at each start, shown on screen. A new PIN is used from the next random one at each start, shown on screen. A new PIN is used from the next
start; the phone pairs again after that. start; the phone pairs again after that.
+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) { 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) { if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) {
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())
_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) if (tag == pending_discovery) { // check for matching response tag)
pending_discovery = 0; pending_discovery = 0;
@@ -2244,16 +2277,7 @@ void MyMesh::handleCmdFrame(size_t len) {
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) { if (recipient) {
uint32_t tag, est_timeout; uint32_t tag, est_timeout;
// 'Path Discovery' is just a special case of flood + Telemetry req int result = sendDiscoveryReq(*recipient, tag, 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;
if (result == MSG_SEND_FAILED) { if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL); writeErrFrame(ERR_CODE_TABLE_FULL);
} else { } else {
+12
View File
@@ -277,6 +277,15 @@ public:
bool setContactPinned(const uint8_t* pub_key, bool pin); // keep / release the stored route 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); 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); 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); void clearPingResult(uint32_t tag);
PingResult* getPingResult(uint32_t tag); PingResult* getPingResult(uint32_t tag);
PingCallback getPingCallback() const { return _ping_callback; } 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 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_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
DiscoverCallback _discover_cb = nullptr;
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
BaseSerialInterface *_serial; BaseSerialInterface *_serial;
Listener* _listener; Listener* _listener;
@@ -12,6 +12,9 @@
// ... repeater, to find the link where it stops // ... repeater, to find the link where it stops
// poll() -- advance a running trace (call every UI pass) // poll() -- advance a running trace (call every UI pass)
// 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
// 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 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
@@ -52,6 +55,9 @@ public:
ContactInfo* c = the_mesh.lookupContactByPubKey(pub_key, PUB_KEY_SIZE); ContactInfo* c = the_mesh.lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (!c) return false; if (!c) return false;
cancelTrace(); cancelTrace();
stopDiscover();
_found_n = 0;
_disc_done = _disc_answered = 0;
memcpy(_target_key, c->id.pub_key, PUB_KEY_SIZE); memcpy(_target_key, c->id.pub_key, PUB_KEY_SIZE);
snprintf(_target_name, sizeof(_target_name), "%s", c->name); snprintf(_target_name, sizeof(_target_name), "%s", c->name);
_target_repeats = c->type == ADV_TYPE_REPEATER; _target_repeats = c->type == ADV_TYPE_REPEATER;
@@ -195,6 +201,7 @@ public:
// Call every UI pass while a trace may be running. // Call every UI pass while a trace may be running.
void poll() { void poll() {
pollDiscover();
if (_state != RUNNING || !_ping) return; if (_state != RUNNING || !_ping) return;
if (!_ping->isActive()) { // the reply came (the engine released its slot), or it was dropped if (!_ping->isActive()) { // the reply came (the engine released its slot), or it was dropped
if (_ping->hopCount() > 0) { if (_ping->hopCount() > 0) {
@@ -245,6 +252,83 @@ public:
return _links > 0; 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 ──────────────────────────────────────────────────────────────────── // ── save ────────────────────────────────────────────────────────────────────
// The list as the contact's stored route; an empty list stores "direct" // 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 // (no repeaters, sent straight to the node). False when the hops share no
@@ -265,6 +349,86 @@ public:
} }
private: 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 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; uint8_t want = _prefs ? _prefs->path_hash_mode + 1 : 1;
if (want > 3) want = 3; if (want > 3) want = 3;
@@ -386,4 +550,9 @@ private:
int _links = 0; int _links = 0;
int16_t _out_x4[MAX_NODES]; int16_t _out_x4[MAX_NODES];
int16_t _back_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 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) 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 } // namespace routeview
static void onRouteAct(lv_event_t* e) { s_ui->routeAct((uint8_t)(uintptr_t)lv_event_get_user_data(e)); } 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 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 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. // 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) { 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); lv_obj_set_style_pad_row(_route_list, 4, 0);
_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);
lv_obj_set_width(_route_disc, LV_PCT(100));
_route_disc_shown = 0xFFFFFFFF;
lv_obj_t* bar = buttonBar(body); lv_obj_t* bar = buttonBar(body);
barButton(bar, LV_SYMBOL_PLUS " Add", onRouteAct, routeview::RT_ADD); 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_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); _route_save_btn = barButton(bar, LV_SYMBOL_SAVE " Save", onRouteAct, routeview::RT_SAVE);
routeRebuild(); routeRebuild();
routeDiscRebuild();
} }
// What the chain shows: the state, the links with a result, a probe's step. // 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() { void UITask::routePoll() {
_core->route.poll(); _core->route.poll();
if (routeSig() != _route_shown) routeRebuild(); 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) { void UITask::routeAct(uint8_t act) {
@@ -127,6 +127,11 @@ public:
void routeStatus(); void routeStatus();
uint32_t routeSig(); uint32_t routeSig();
void routePoll(); void routePoll();
void routeDiscRebuild();
uint32_t routeDiscSig();
void routeDiscover();
void routeDiscTries(int sel);
void routeUseFound(int i);
void routeAct(uint8_t act); void routeAct(uint8_t act);
void routeOp(int code); void routeOp(int code);
void routeKeep(); void routeKeep();
@@ -664,6 +669,8 @@ private:
lv_obj_t* _route_save_btn = nullptr; 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_probe_row = nullptr; // "Find where it stops", after a trace with no reply
lv_obj_t* _route_note = nullptr; 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) lv_obj_t* _route_keep_btn = nullptr; // header: Keep (accent while kept)
uint32_t _route_shown = 0xFFFFFFFF; // what routeRebuild() drew (routeSig()) uint32_t _route_shown = 0xFFFFFFFF; // what routeRebuild() drew (routeSig())
uint32_t _next_route_ms = 0; uint32_t _next_route_ms = 0;
+51 -5
View File
@@ -21,12 +21,16 @@ class RouteView {
int _menu_hop = 0; int _menu_hop = 0;
uint8_t _seen_state = PathEditor::IDLE; // a result's alert, once 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 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; } PathEditor& R() { return _task->core().route; }
// The rows: the hops, the node, then the actions (Find break only after a // 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) { int rows(Row* out) {
PathEditor& r = R(); PathEditor& r = R();
int k = 0; 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 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_SAVE;
out[k++] = R_KEEP; out[k++] = R_KEEP;
out[k++] = R_DISCOVER;
for (int i = 0; i < r.foundCount(); i++) out[k++] = R_FOUND;
return k; 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) { void openHopMenu(int i) {
_menu_hop = i; _menu_hop = i;
@@ -58,6 +70,13 @@ class RouteView {
// What a result means, said once when it comes. // What a result means, said once when it comes.
void announce() { void announce() {
PathEditor& r = R(); 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; if (r.state() == _seen_state) return;
_seen_state = r.state(); _seen_state = r.state();
char t[48]; char t[48];
@@ -99,6 +118,7 @@ public:
_menu.active = false; _menu.active = false;
_seen_state = PathEditor::IDLE; _seen_state = PathEditor::IDLE;
_sel_kind = 0xFF; _sel_kind = 0xFF;
_seen_disc = false;
return true; return true;
} }
// Back from picking a repeater: the cursor on Add, for the next one. // Back from picking a repeater: the cursor on Add, for the next one.
@@ -108,7 +128,7 @@ public:
_sel_kind = R_ADD; _sel_kind = R_ADD;
} }
void close() { void close() {
if (_task) R().cancelTrace(); if (_task) { R().cancelTrace(); R().stopDiscover(); }
_active = false; _active = false;
_menu.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_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_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_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; 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);
@@ -167,7 +201,7 @@ public:
} }
}); });
if (_menu.active) _menu.render(display); 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). // 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 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_TO_NODE) r.setToNode(!r.toNode());
else if (k == R_KEEP) toggleKeep(); else if (k == R_KEEP) toggleKeep();
else if (k == R_DISCOVER && !r.discovering()) _tries = (uint8_t)((_tries + 3 + dir) % 3);
return true; return true;
} }
if (c != KEY_ENTER && c != KEY_CONTEXT_MENU) 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); else _task->showAlert(r.count() ? "Path saved" : "Saved: direct", 1000);
break; break;
case R_KEEP: toggleKeep(); 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; return true;
} }