diff --git a/src/MyMesh.h b/src/MyMesh.h index d1564257..a75d7b9e 100644 --- a/src/MyMesh.h +++ b/src/MyMesh.h @@ -381,6 +381,7 @@ public: memcpy(&_ui_pending_status, recipient.id.pub_key, 4); _ui_pending_kind = UiReqKind::Status; _ui_pending_tag = tag; // request tag, reflected by the repeater + _ui_pending_at = millis(); } return r; } @@ -491,6 +492,7 @@ public: 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; + _ui_login_then_at = millis(); } else { cancelUIDeferredLogin(); } @@ -591,6 +593,15 @@ public: * the request a moment after a fallback login was accepted. */ void uiRequestTick() { const uint32_t now = millis(); + // The slot is single-flight and every UI request waits for it, but not every + // sender keeps a deadline (telemetry auto-poll, the Home sensor widget). One + // unanswered request then held it until reboot: the admin app's status, + // neighbours and access list never went out. Nothing waits longer than 45 s. + if (_ui_pending_status && (uint32_t)(now - _ui_pending_at) >= UI_REQ_SLOT_MAX_MS) cancelUIPingPending(); + if (_ui_login_then && (uint32_t)(now - _ui_login_then_at) >= UI_REQ_SLOT_MAX_MS) { + _ui_login_then = 0; + _ui_login_then_kind = UiReqKind::None; + } if (_ui_fallback_at && (int32_t)(now - _ui_fallback_at) >= 0) { _ui_fallback_at = 0; if (_ui_pending_status && memcmp(&_ui_pending_status, _ui_fallback_key, 4) == 0) { @@ -1005,6 +1016,7 @@ public: memcpy(&_ui_pending_status, recipient.id.pub_key, 4); _ui_pending_kind = UiReqKind::Telemetry; _ui_pending_tag = tag; // request tag, reflected by the repeater + _ui_pending_at = millis(); } return r; } @@ -1023,6 +1035,7 @@ public: memcpy(&_ui_pending_status, recipient.id.pub_key, 4); _ui_pending_kind = UiReqKind::Binary; _ui_pending_tag = tag; + _ui_pending_at = millis(); } return r; } @@ -1062,6 +1075,7 @@ public: memcpy(&_ui_pending_status, pub_key, 4); _ui_pending_kind = UiReqKind::Regions; _ui_pending_tag = tag; + _ui_pending_at = millis(); } return result; } @@ -1452,6 +1466,8 @@ 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; + uint32_t _ui_pending_at = 0; // millis() when the slot was taken (uiRequestTick expiry) + static constexpr uint32_t UI_REQ_SLOT_MAX_MS = 45000; UiLoginInfo _ui_last_login = {}; // uiLastLoginInfo() /** 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 @@ -1459,6 +1475,7 @@ private: * 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; + uint32_t _ui_login_then_at = 0; // Direct-first UI requests (#683): fall back to the guest login at _ui_fallback_at // if the reply has not come; after a fallback LOGIN-OK the REQ waits until // _ui_deferred_at. 0 = not armed. diff --git a/web/mesh/WebMesh.cpp b/web/mesh/WebMesh.cpp index 044a81e5..124a3d66 100644 --- a/web/mesh/WebMesh.cpp +++ b/web/mesh/WebMesh.cpp @@ -149,6 +149,8 @@ void MyMesh::begin(MeshBackend* backend) { void MyMesh::loop() { const uint32_t now = millis(); + // src/MyMesh.h uiRequestTick: a request nobody answered frees the slot after 45 s. + if (_ui_pending_kind != UiReqKind::None && (uint32_t)(now - _ui_pending_at) >= 45000u) cancelUIPingPending(); if (_backend) _backend->loop(now); if (now - _pos_sync_ms >= 5000) { _pos_sync_ms = now; syncAdvertPosition(now); } } @@ -776,6 +778,7 @@ int MyMesh::uiSendRequestAfterGuestLogin(ContactInfo& recipient, UiReqKind kind) if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { _ui_pending_kind = kind; memcpy(_ui_pending_key, recipient.id.pub_key, PUB_KEY_SIZE); + _ui_pending_at = millis(); } return r; } @@ -787,6 +790,7 @@ int MyMesh::uiStraightRequest(ContactInfo& recipient, UiReqKind kind) { if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { _ui_pending_kind = kind; memcpy(_ui_pending_key, recipient.id.pub_key, PUB_KEY_SIZE); + _ui_pending_at = millis(); } return r; } @@ -798,6 +802,7 @@ int MyMesh::uiSendBinaryRequest(ContactInfo& recipient, const uint8_t* payload, if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { _ui_pending_kind = UiReqKind::Binary; memcpy(_ui_pending_key, recipient.id.pub_key, PUB_KEY_SIZE); + _ui_pending_at = millis(); } return r; } @@ -821,6 +826,7 @@ int MyMesh::sendRegionsRequestForUI(const uint8_t pub_key[PUB_KEY_SIZE], uint32_ if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { _ui_pending_kind = UiReqKind::Regions; memcpy(_ui_pending_key, pub_key, PUB_KEY_SIZE); + _ui_pending_at = millis(); } return r; } diff --git a/web/mesh/WebMesh.h b/web/mesh/WebMesh.h index a2343296..d1e65c90 100644 --- a/web/mesh/WebMesh.h +++ b/web/mesh/WebMesh.h @@ -357,6 +357,7 @@ private: UiReqKind _ui_pending_kind = UiReqKind::None; UiLoginInfo _ui_last_login = {}; uint8_t _ui_pending_key[PUB_KEY_SIZE] = {0}; + uint32_t _ui_pending_at = 0; uint32_t _ui_trace_tag = 0; uint32_t _n_sent_flood = 0, _n_sent_direct = 0, _n_recv_flood = 0, _n_recv_direct = 0;