fix(ui,bot): resolve remaining findings from the 2026-07-26 screen review

- DM/room unread badges could claim messages the ring no longer held
  (same class as the channel fix in 6470afaf, not covered by it):
  getDMUnread()/getDMUnreadTotal() now clamp to dmHistCountForContact(),
  and a new reconcileDMUnread() (called once per loop()) frees any
  _dm_unread_table slot whose ring occupancy has dropped to 0, so a
  17th sender isn't starved by stale entries. onContactRemoved() now
  also clears _dm_unread_table -- the one per-contact table it was
  missing.
- Shift didn't capitalise ł/ń/ź/ż (+ĺ/ľ/ň/ž): the Latin Extended-A
  case-pairing rule assumed a single parity for the whole block, but it
  flips around the unpaired codepoints ĸ/ʼn/Ÿ. Fixed with four
  sub-ranges, verified exhaustively over U+0100-U+017F.
- Triple-click could still toggle the buzzer while locked on
  PIN_USER_BTN/PIN_USER_BTN_ANA boards (joystick path already guarded
  this).
- millis() wraparound: 4 absolute comparisons in UITask.cpp (battery
  poll, auto-off, lock-wake, backlight) converted to the existing
  (int32_t)(millis()-deadline)>=0 idiom; MyMeshBot.h's DM-throttle
  eviction now picks the oldest slot by elapsed time instead of raw
  t_ms, which picked the wrong slot right after a rollover.
- Long-press bypassed checkDisplayOn() on all 5 call sites -- neither
  woke the display nor extended auto-off, and could deliver
  KEY_CONTEXT_MENU to the invisible screen. Moved the gate inside
  handleLongPress() itself instead of patching each site.
- CardKB's backspace/printable-insert branches didn't reset t9_cell,
  so typing right after a T9 cycle tap could get silently overwritten
  by a same-cell re-tap within the T9 timeout.
- buildContactList()'s counts[MAX_CONTACTS] was a 1400 B int array on
  the 4 KB loop() stack; values are bounded by DM_HIST_MAX (32), so
  now uint8_t.
- ACK table treated ack==0 as a wildcard: isAckPending(0) matched any
  free slot, and processAck() with an all-zero ACK matched the first
  free slot and returned its stale contact pointer. Both now skip/reject
  ack==0, and the matched slot's contact pointer is cleared alongside
  its ack hash.
- ensurePageOrderInit() could write one byte past page_order[13] when
  migrating a saved order with all 13 slots full and CLOCK last --
  guarded on insert_at < PAGE_ORDER_LEN.

Two findings from the same review were resolved as no-op decisions,
not code changes: !buzz over DM ignoring quiet hours is intentional
(the pull exemption is meant to cover the buzzer), and the offline
queue's full-queue drop-newest behaviour is upstream code, left alone.

Build-verified green on WioTrackerL1_companion_solo_dual (RAM 71.1%,
Flash 66.6%) and WioTrackerL1Eink_companion_solo_dual (RAM 73.0%,
Flash 67.9%).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-07-27 22:05:52 +02:00
co-authored by Claude Sonnet 5
parent 6470afaf56
commit 8e5f083e2e
9 changed files with 110 additions and 33 deletions
+11 -2
View File
@@ -462,8 +462,15 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) {
}
ContactInfo* MyMesh::processAck(const uint8_t *data) {
// 0 marks an empty/cleared table slot, not a real ACK -- a bare memcmp below
// would otherwise match the first free slot against an all-zero `data` (which
// *is* remotely reachable, ack_crc comes off the air) and misattribute the
// ACK to whatever contact that slot last belonged to.
static const uint8_t zero_ack[4] = {0, 0, 0, 0};
if (memcmp(data, zero_ack, 4) == 0) return checkConnectionsAck(data);
// see if matches any in a table
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) {
if (expected_ack_table[i].ack == 0) continue; // empty slot
if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient
out_frame[0] = PUSH_CODE_SEND_CONFIRMED;
memcpy(&out_frame[1], data, 4);
@@ -471,9 +478,11 @@ ContactInfo* MyMesh::processAck(const uint8_t *data) {
memcpy(&out_frame[5], &trip_time, 4);
_serial->writeFrame(out_frame, 9);
ContactInfo* contact = expected_ack_table[i].contact;
// NOTE: the same ACK can be received multiple times!
expected_ack_table[i].ack = 0; // clear expected hash, now that we have received ACK
return expected_ack_table[i].contact;
expected_ack_table[i].ack = 0; // clear expected hash, now that we have received ACK
expected_ack_table[i].contact = nullptr; // and the stale pointer along with it
return contact;
}
}
return checkConnectionsAck(data);