mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 02:41:57 +00:00
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:
@@ -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
|
||||
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
|
||||
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.
|
||||
@@ -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
|
||||
> 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.
|
||||
> **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
|
||||
|
||||
|
||||
+11
-1
@@ -30,10 +30,20 @@ older and newer one.
|
||||
contains a location (see [Navigation](./navigation.md)).
|
||||
- **Path** on a received message: the repeaters it came through.
|
||||
**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]
|
||||
> **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
|
||||
|
||||
|
||||
@@ -68,7 +68,10 @@ nodes aren't added until you make room.
|
||||
**Allowed** (the contacts you gave that permission: **Telem** in a node's
|
||||
options in [Nodes](./contacts.md#nearby-nodes), or the phone app) 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
|
||||
random one at each start, shown on screen. A new PIN is used from the next
|
||||
start; the phone pairs again after that.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user