touch: free the UI request slot after 45 s without a reply

The status, telemetry, neighbour and region requests share one single-flight
slot in MyMesh. The telemetry auto-poll and the Home sensor widget take it
without a deadline of their own, so one unanswered request held it until
reboot. The repeater admin app then queued every status, neighbours and
access-list request behind it and sent nothing: logged in, blank status,
Refresh does nothing (reported on Discord against beta_99).

uiRequestTick() now expires the slot and the deferred guest login 45 s after
they were taken; every sender with its own deadline gives up well before.
The web build's stand-in gets the same expiry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaj SchittecatandClaude Opus 5.5 committed 2026-10-26 11:37:38 +01:00
1 parent df7cb4092d
commit cec1431c7a
3 files changed
+24

No files matched your search

+17
View File
@@ -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.
+6
View File
@@ -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;
}
+1
View File
@@ -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;