After broadcasting a group message, wait 5s to see if any neighbor
repeated the flood and use the outcome to mark the on-device entry
and the BLE-app mirror.
- ContentionTracker gets extractDupeCount(hash): finds the tracked
entry, captures dupe_count, finalizes (folds into EMA, marks
inactive), returns the count or -1.
- BaseChatMesh::sendGroupMessage gains an optional out_hash param;
when set, the FNV-1a packet hash is also pre-registered with the
contention tracker so heard retransmits get counted (originated
floods weren't tracked before, only relays).
- Mesh::getContentionTracker() promoted to public so the UI can
query after the feedback window.
JoystickUITask grows a 4-slot pending-channel table with per-slot
k_timer (5s one-shot). startPendingChannel() broadcasts, adds the
local _ch_previews entry with path_len = OUT_PATH_SENT, and starts
the feedback timer. The timer ISR sets a feedback_due flag; the
loop's processPendingChannelFeedback() picks it up, calls
extractDupeCount(), and rewrites the preview's path_len to
OUT_PATH_SENT_HEARD (0xFD) or OUT_PATH_SENT_UNHEARD (0xFC) — which
formatHopCount renders as "sent+" / "sent?".
The deferred BLE-app mirror queues only on outcome with body prefix
"(>>✓) " (heard) or "(>>✗) " (not heard). queueLocalSentChannelMessage
gains a heard_repeat parameter for the selection.
Both channel send entry points (sendComposedMessage's channel branch
and sendChannelMessage) now route through startPendingChannel().
Outgoing DMs now go through a 4-slot pending-send table with per-slot
one-shot k_timer. On no-ACK the message retries (up to 5 attempts,
0-indexed); attempt 4 clears recipient.out_path_len + markContactsDirty
so the last try forces flood and future DMs re-discover the path.
ACK dispatch: CompanionMesh::processAck tries _ack_table first
(phone-initiated sends), then ui_joystick_try_match_ack() for joystick-
initiated sends, then falls through to connection-keepalive ACKs.
BLE-app mirror is now deferred until outcome is known. The body prefix
in the offline-queue frame is "(>>✓) " on delivery or "(>>✗) " on
failure, replacing the previous unconditional "(>>) ". UnreadScreen's
sent-entry origin prefix is updated in place via markSentEntryStatus()
to "(>>+) " / "(>>X) " (ASCII for the OLED font).
queueLocalSentContactMessage gains a 'delivered' parameter to pick the
prefix; markContactsDirtyPublic() exposes the lazy-write trigger so
the joystick's path-clear persists.
- Lock overlay: show battery % + unread count between title and unlock
sequence; ui_invalidate_battery_cache() on screen wake forces a fresh
ADC sample so the user sees current data immediately
- UnreadScreen::addPreview gains initially_read; received msgs pass
_ble_connected (don't count unread when phone is syncing); sent msgs
pass true (you sent it, you know it)
- CompanionMesh::queueLocalSent{Contact,Channel}Message + PUSH_CODE_MSG_WAITING
on wio-originated sends so a connected phone app sees them via the
normal offline-queue flow (path_len = OUT_PATH_SENT marker)
- OUT_PATH_SENT moved back from joystick_defs.h to ContactInfo.h
(now a wire-format value, not UI-only)
- #ifdef-gate _pending_joystick_{ping,admin}_tag fields + setters
under CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK (saves 8 bytes per
CompanionMesh instance on button-UI builds)
- gate logTx ui_notify_packet_sent() to joystick builds only;
was firing on every TX for any UI variant (dead code on button UI)
- drop redundant _pending_login manual set in CMD_SEND_LOGIN;
BaseChatMesh::sendLogin's onLoginSent hook owns it now, just
clear the other pending fields explicitly
- flood retransmit jitter now capped at min(2000ms, 6·airtime) instead of
fixed 2000ms — spreads tighter at SF7, unchanged at SF8
- reactive per-dupe backoff cap now min(2000ms, 12·airtime), keeps
semantic of "push past ~12 relay slots"
- contention ring 16 → 24 for 50-neighbor hilltops
- companions passively track heard floods (warms EMA without forwarding)
and spread their own TX by up to min(1000ms, 3·airtime), hopefully
fixing repeaters missing companion's first transmission
config cleanup:
- move BLE TX buffer bumps (ACL_TX=12 etc.) from zephcore_common.conf to
esp32_common.conf — the Espressif blob needs them, nRF doesn't, and
the bumps were overflowing nRF52840 RAM
- remove CONFIG_ZEPHCORE_MAX_CONTACTS=510 overrides from 5 nRF52840
companion boards; Kconfig default of 350 fits with comfortable margin
(wio prod: 91% → 79% RAM)
- RegionMap::findMatch(): fix 1-byte stack overflow in tmp[] with bounded memcpy
- Packet::readFrom(): add bounds checks before reading transport codes
- Mesh::sendFlood(): release packet on TRACE type and invalid hash_size early returns
- CompanionMesh: add cleanupSignState() called from BLE disconnect to free sign buffer
- ZephyrCompanionUSB: add 2s timeout for partial V3 frames to prevent parser stall
- ui_mesh_actions: replace volatile bool with Zephyr atomic_t for cross-thread state
- CommonCLI: wrap ~40 fs_read() calls in prefs_read() helper with truncation warning