diff --git a/src/MyMesh.cpp b/src/MyMesh.cpp index 692256c..e43926e 100644 --- a/src/MyMesh.cpp +++ b/src/MyMesh.cpp @@ -2200,6 +2200,26 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, } return; } + + // Deferred guest-login-then-request (uiSendRequestAfterGuestLogin): the touch + // UI sent only a blank-password LOGIN and is waiting on the LOGIN-OK before + // issuing the STATUS/TELEMETRY REQ. While armed we have sent no REQ to this + // contact, so any RESPONSE from it here is the login reply. On OK, fire the + // deferred REQ now — we're in the repeater's ACL and a direct path has been + // learned, so it lands first try. On a non-OK (fail) reply, just disarm and + // let the UI's reply deadline flip the window to "failed". + if (_ui_login_then && len > 4 && memcmp(&_ui_login_then, contact.id.pub_key, 4) == 0) { + const bool login_ok = (data[4] == RESP_SERVER_LOGIN_OK) + || (len > 5 && memcmp(&data[4], "OK", 2) == 0); + const UiReqKind k = _ui_login_then_kind; + cancelUIDeferredLogin(); + if (login_ok) { + ContactInfo& rc = const_cast(contact); + if (k == UiReqKind::Telemetry) sendTelemetryRequestForUI(rc); + else sendStatusPingForUI(rc); + } + return; // login frame consumed (OK fired the REQ; fail disarmed) + } #endif if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response diff --git a/src/MyMesh.h b/src/MyMesh.h index 7b49c59..48f2ec7 100644 --- a/src/MyMesh.h +++ b/src/MyMesh.h @@ -333,6 +333,31 @@ public: return sendTelemetryRequestForUI(recipient); } + /** Touch-UI manual STATUS/TELEMETRY request that DEFERS the REQ until the + * guest LOGIN is acknowledged. The chained helpers above fire LOGIN and REQ + * back-to-back, but a repeater drops a PAYLOAD_TYPE_REQ from a sender it + * hasn't ACL'd yet — and on first contact the ACL entry isn't committed by + * the time the REQ is processed, so the first request usually gets no reply + * (the user had to tap twice). This sends ONLY the blank-password LOGIN now + * and arms _ui_login_then; onContactResponse issues the REQ once the + * LOGIN-OK lands, by which point we're in the ACL and a direct out_path has + * been learned, so the REQ decrypts on the first try. + * Returns the LOGIN's MSG_SEND_* result — the UI shows "requesting…" on a + * successful send and arms its own reply deadline. Manual paths only; + * auto-poll keeps the immediate chained send above (a single arm slot can't + * serve its multi-node loop, and a missed poll just retries next interval). */ + int uiSendRequestAfterGuestLogin(ContactInfo& recipient, UiReqKind kind) { + uint32_t login_est = 0; + int r = sendLogin(recipient, "", login_est); + if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { + memcpy(&_ui_login_then, recipient.id.pub_key, 4); + _ui_login_then_kind = kind; + } else { + cancelUIDeferredLogin(); + } + return r; + } + /** Admin login for the touch UI repeater admin console. * Sends a sendLogin with the given password (empty = guest), records * pending_login so onContactResponse's existing login branch can route @@ -371,6 +396,13 @@ public: _ui_pending_kind = UiReqKind::None; _ui_pending_tag = 0; } + /** Disarm a deferred guest-login-then-request (see uiSendRequestAfterGuestLogin) + * so a late LOGIN-OK can't fire a REQ after the UI gave up. Kept separate from + * cancelUIPingPending() so a ping timeout never disarms a telemetry request. */ + void cancelUIDeferredLogin() { + _ui_login_then = 0; + _ui_login_then_kind = UiReqKind::None; + } /** True if a UI ping is still waiting on a reply. */ bool hasUIPingPending() const { return _ui_pending_status != 0; } @@ -896,6 +928,12 @@ private: * matching by pubkey alone misroutes the login OK as the REQ reply. * Compare tag in onContactResponse to keep the two streams separate. */ uint32_t _ui_pending_tag = 0; + /** Deferred guest-login-then-request arm: holds the first 4 bytes of the + * recipient pub_key (0 = none) plus which REQ to send, set by + * uiSendRequestAfterGuestLogin(). onContactResponse fires the REQ when the + * matching LOGIN-OK arrives, removing the ACL-commit race on the first try. */ + uint32_t _ui_login_then = 0; + UiReqKind _ui_login_then_kind = UiReqKind::None; BaseSerialInterface *_serial; AbstractUITask* _ui; diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 0dd0845..8fee236 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -32269,6 +32269,10 @@ static void telemetryPollSet(const uint8_t* key6, int interval_min) { } static void telemetryPollTick(uint32_t now_ms) { if (SD.cardType() == CARD_NONE) return; + // Hold auto-poll while a manual request is awaiting its reply — an auto-poll + // REQ would overwrite the manual request's pending tag and orphan its reply. + // It'll poll on the next interval (the manual pending window is short). + if (s_telem_manual_pending) return; telemetryPollLoad(); for (auto& e : s_telem_poll) { if (!e.used || (int32_t)(now_ms - e.next_ms) < 0) continue; @@ -32301,9 +32305,16 @@ static void telemWinApplyCb(lv_event_t* e) { // send, but drives the telemetry window's own pending/deadline state. static void telemetryRequestNow() { if (!g_lv.task) return; + // Single-flight: a manual request for this node is already awaiting its reply. + // Re-firing would send a fresh LOGIN+REQ and overwrite the in-flight pending + // tag, orphaning the first reply — just keep waiting on the one in progress. + if (s_telem_manual_pending) return; ContactInfo c; if (!telemetryFindContact(s_telem_node, &c)) { openTelemetryWindow(s_telem_node, s_telem_name, TELEM_FAILED); return; } - const int r = the_mesh.sendTelemetryRequestWithGuestLoginForUI(c); + // Send the guest LOGIN now and defer the telemetry REQ until the repeater + // acknowledges it (the REQ fires from onContactResponse on the LOGIN-OK, by + // which point we're ACL'd so it lands first try — no more "tap twice"). + const int r = the_mesh.uiSendRequestAfterGuestLogin(c, MyMesh::UiReqKind::Telemetry); s_telem_reading[0] = '\0'; if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { s_telem_manual_pending = true; @@ -35609,6 +35620,8 @@ void UITask::loop() { if (s_telem_manual_pending && s_telem_deadline_ms != 0 && now >= s_telem_deadline_ms) { s_telem_manual_pending = false; s_telem_deadline_ms = 0; + the_mesh.cancelUIDeferredLogin(); // drop a not-yet-acked LOGIN so a late OK can't fire a stale REQ + the_mesh.cancelUIPingPending(); // drop the pending reply tag if the REQ was already sent if (s_telemetry_root) openTelemetryWindow(s_telem_node, s_telem_name, TELEM_FAILED); } #endif @@ -36035,7 +36048,9 @@ void UITask::loop() { // worker). Cheap — a blink toggle, no animation. if (g_statusbar.async_icon) { const bool active = (g_mesh_req_ms != 0 && (uint32_t)(now - g_mesh_req_ms) < 1500u) - || (s_ui_ping_deadline_ms != 0) || s_los_busy; + || (s_ui_ping_deadline_ms != 0) + || (s_telem_manual_pending && s_telem_deadline_ms != 0) // manual telemetry req stays lit for its whole pending window, like a ping + || s_los_busy; static bool s_async_shown = false; static uint32_t s_async_blink = 0; if (active) {