mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 20:09:17 +00:00
joystick UI: DM retry, force-flood fallback, BLE mirror on outcome
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.
This commit is contained in:
@@ -695,6 +695,18 @@ ContactInfo *CompanionMesh::processAck(const uint8_t *data)
|
||||
return lookupContactByPubKey(ci.id.pub_key, PUB_KEY_SIZE);
|
||||
}
|
||||
}
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK)
|
||||
/* Joystick UI tracks its own pending DMs; let it match before falling
|
||||
* through to connection-keep-alive ACKs. The joystick handles delivery
|
||||
* indicator + BLE-mirror queueing internally — we just need the matched
|
||||
* contact pointer for BaseChatMesh's return-path-retry hook. */
|
||||
{
|
||||
uint8_t recipient_pubkey[6];
|
||||
if (ui_joystick_try_match_ack(ack_crc, recipient_pubkey)) {
|
||||
return lookupContactByPubKey(recipient_pubkey, 6);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return checkConnectionsAck(data);
|
||||
}
|
||||
|
||||
@@ -768,7 +780,7 @@ void CompanionMesh::queueContactMessage(const ContactInfo &contact, mesh::Packet
|
||||
}
|
||||
|
||||
void CompanionMesh::queueLocalSentContactMessage(const ContactInfo &contact,
|
||||
uint32_t timestamp, const char *text)
|
||||
uint32_t timestamp, const char *text, bool delivered)
|
||||
{
|
||||
if (!text) return;
|
||||
uint8_t frame[MAX_FRAME_SIZE];
|
||||
@@ -790,14 +802,15 @@ void CompanionMesh::queueLocalSentContactMessage(const ContactInfo &contact,
|
||||
i += 4;
|
||||
|
||||
/* DM wire-text has no sender prefix (the sender is the pubkey field).
|
||||
* For local-sent mirrors the phone app would otherwise show them as a
|
||||
* message *from* the contact, indistinguishable from incoming. Prepend
|
||||
* a visible marker so the user can see at a glance which side sent it.
|
||||
* "(>>) " matches the joystick UI's own sent indicator in addPreview(). */
|
||||
static const char kSentMarker[] = "(>>) ";
|
||||
size_t marker_len = sizeof(kSentMarker) - 1;
|
||||
* For local-sent mirrors the phone app would otherwise show them as
|
||||
* messages *from* the contact, indistinguishable from incoming. Prepend
|
||||
* a visible delivery indicator so the user can tell at a glance both
|
||||
* which side sent it and whether it was acked. */
|
||||
const char *marker = delivered ? "(>>\xe2\x9c\x93) " /* (>>✓) UTF-8 */
|
||||
: "(>>\xe2\x9c\x97) "; /* (>>✗) UTF-8 */
|
||||
size_t marker_len = strlen(marker);
|
||||
if (i + marker_len <= sizeof(frame)) {
|
||||
memcpy(&frame[i], kSentMarker, marker_len);
|
||||
memcpy(&frame[i], marker, marker_len);
|
||||
i += marker_len;
|
||||
}
|
||||
size_t text_len = strlen(text);
|
||||
@@ -807,7 +820,7 @@ void CompanionMesh::queueLocalSentContactMessage(const ContactInfo &contact,
|
||||
memcpy(&frame[i], text, text_len);
|
||||
i += text_len;
|
||||
|
||||
LOG_DBG("queueLocalSentContactMessage: frame_len=%d", i);
|
||||
LOG_DBG("queueLocalSentContactMessage: frame_len=%d delivered=%d", i, (int)delivered);
|
||||
queueOfflineMessage(frame, i);
|
||||
sendPush(PUSH_CODE_MSG_WAITING);
|
||||
}
|
||||
|
||||
@@ -193,13 +193,13 @@ public:
|
||||
|
||||
/**
|
||||
* Queue a locally-originated DM into the BLE offline queue and signal
|
||||
* MSG_WAITING. The frame uses path_len = OUT_PATH_SENT (0xFE) so an
|
||||
* updated phone app can render it as outbound. Older apps still see a
|
||||
* message text in their inbox (possibly mis-rendered as "0xFE hops"),
|
||||
* but the text content is correct so no info is lost.
|
||||
* MSG_WAITING. The frame uses path_len = OUT_PATH_SENT (0xFE) and the
|
||||
* body is prefixed with "(>>✓) " on delivery or "(>>✗) " on failure so
|
||||
* the phone app shows a visible outcome indicator without needing
|
||||
* protocol-level support.
|
||||
*/
|
||||
void queueLocalSentContactMessage(const ContactInfo &contact, uint32_t timestamp,
|
||||
const char *text);
|
||||
const char *text, bool delivered);
|
||||
|
||||
/**
|
||||
* Queue a locally-originated channel message into the BLE offline queue
|
||||
@@ -371,6 +371,10 @@ public:
|
||||
void clearJoystickPingTag() { _pending_joystick_ping_tag = 0; }
|
||||
void setJoystickAdminTag(uint32_t tag) { _pending_joystick_admin_tag = tag; }
|
||||
void clearJoystickAdminTag() { _pending_joystick_admin_tag = 0; }
|
||||
/* Force a contacts-flush schedule from outside (joystick UI clears a
|
||||
* stale out_path_len during 5th-attempt fallback flood and needs the
|
||||
* change to persist to /ext/contacts3). */
|
||||
void markContactsDirtyPublic() { markContactsDirty(); }
|
||||
private:
|
||||
#endif
|
||||
|
||||
|
||||
@@ -465,6 +465,10 @@ public:
|
||||
* when a BLE phone is connected and will sync them). */
|
||||
void addPreview(uint8_t path_len, const char *from_name, const char *msg,
|
||||
bool initially_read = false);
|
||||
/* Update the origin prefix of an existing sent-message entry to reflect
|
||||
* delivery outcome. Looks up by (timestamp, contact_name) — these are
|
||||
* unique-per-second for outgoing DMs. */
|
||||
void markSentEntryStatus(uint32_t ts, const char *contact_name, bool delivered);
|
||||
bool getStoredMessageForSourceAt(const char *source, uint32_t ts, const char *&out_msg) const;
|
||||
bool getLatestStoredMessageForSource(const char *source, const char *&out_msg, uint32_t *out_ts = nullptr) const;
|
||||
int getContactMsgCount(const char *contact_name) const;
|
||||
|
||||
@@ -225,6 +225,15 @@ extern "C" void ui_notify_radio_stats(uint32_t pkt_recv, uint32_t pkt_sent, uint
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" bool ui_joystick_try_match_ack(uint32_t ack, uint8_t out_pubkey[6])
|
||||
{
|
||||
if (!s_task) return false;
|
||||
ContactInfo *c = s_task->tryMatchPendingAck(ack);
|
||||
if (!c) return false;
|
||||
memcpy(out_pubkey, c->id.pub_key, 6);
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" void ui_set_battery(uint16_t mv, uint8_t /*pct*/)
|
||||
{
|
||||
if (s_task) {
|
||||
|
||||
@@ -105,6 +105,17 @@ void ui_signal_refresh(void);
|
||||
*/
|
||||
void ui_signal_tx(void);
|
||||
|
||||
/**
|
||||
* Try to match an incoming ACK against the joystick UI's pending-DM table.
|
||||
* On match: cancels the retry timer, marks the entry delivered, fires the
|
||||
* deferred BLE-app mirror with the success prefix, and writes the recipient's
|
||||
* 6-byte pubkey prefix to out_pubkey (caller uses it for BaseChatMesh's
|
||||
* return-path-retry housekeeping).
|
||||
*
|
||||
* @return true if matched (out_pubkey is filled), false otherwise
|
||||
*/
|
||||
bool ui_joystick_try_match_ack(uint32_t ack, uint8_t out_pubkey[6]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -83,6 +83,163 @@ void JoystickUITask::onDisplayStateChanged()
|
||||
_was_display_on = is_on;
|
||||
}
|
||||
|
||||
/* ===== Outgoing DM tracking ===== */
|
||||
|
||||
void JoystickUITask::pendingRetryTimerCb(struct k_timer *t)
|
||||
{
|
||||
/* ISR — mark slot ready for retry, wake main loop. */
|
||||
PendingSend *slot = (PendingSend *)k_timer_user_data_get(t);
|
||||
if (!slot || !slot->task) return;
|
||||
slot->retry_due = true;
|
||||
if (s_signal_fn) s_signal_fn();
|
||||
}
|
||||
|
||||
int JoystickUITask::allocPendingSendSlot()
|
||||
{
|
||||
for (int i = 0; i < MAX_PENDING_SENDS; i++) {
|
||||
if (!_pending_sends[i].active) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void JoystickUITask::doPendingSend(int slot_idx)
|
||||
{
|
||||
PendingSend &s = _pending_sends[slot_idx];
|
||||
ContactInfo *recipient = _mesh ? _mesh->lookupContactByPubKey(s.recipient_pubkey, PUB_KEY_SIZE) : nullptr;
|
||||
if (!recipient) {
|
||||
s.failed = true;
|
||||
completePendingSend(slot_idx);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Attempt 4 (5th, 0-indexed) — clear the saved path so this send forces
|
||||
* flood. The clear persists so future DMs to this contact also re-discover. */
|
||||
if (s.attempt == MAX_SEND_ATTEMPTS - 1) {
|
||||
recipient->out_path_len = OUT_PATH_UNKNOWN;
|
||||
if (CompanionMesh *cm = static_cast<CompanionMesh *>(_mesh)) {
|
||||
cm->markContactsDirtyPublic();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t expected_ack = 0, est_timeout = 0;
|
||||
int result = _mesh->sendMessage(*recipient, s.timestamp, s.attempt, s.text,
|
||||
expected_ack, est_timeout);
|
||||
if (result == 0) {
|
||||
/* sendMessage rejected (e.g. queue full) — back off and try again. */
|
||||
s.timeout_ms = 3000;
|
||||
schedulePendingRetry(slot_idx);
|
||||
return;
|
||||
}
|
||||
ui_signal_tx();
|
||||
s.expected_ack = expected_ack;
|
||||
/* Allow the ACK round-trip (2× est_timeout + small grace) before retrying. */
|
||||
s.timeout_ms = (est_timeout > 0) ? (est_timeout * 2 + 3000) : 8000;
|
||||
schedulePendingRetry(slot_idx);
|
||||
}
|
||||
|
||||
void JoystickUITask::schedulePendingRetry(int slot_idx)
|
||||
{
|
||||
PendingSend &s = _pending_sends[slot_idx];
|
||||
k_timer_stop(&s.retry_timer);
|
||||
k_timer_start(&s.retry_timer, K_MSEC(s.timeout_ms), K_NO_WAIT);
|
||||
}
|
||||
|
||||
void JoystickUITask::completePendingSend(int slot_idx)
|
||||
{
|
||||
PendingSend &s = _pending_sends[slot_idx];
|
||||
k_timer_stop(&s.retry_timer);
|
||||
bool delivered = s.delivered;
|
||||
|
||||
/* Update the on-device history entry (UnreadScreen) to reflect outcome. */
|
||||
if (_unread) {
|
||||
_unread->markSentEntryStatus(s.timestamp, s.recipient_name, delivered);
|
||||
}
|
||||
|
||||
/* Now (and only now) mirror to the BLE app with the outcome prefix. */
|
||||
ContactInfo *recipient = _mesh ? _mesh->lookupContactByPubKey(s.recipient_pubkey, PUB_KEY_SIZE) : nullptr;
|
||||
if (recipient) {
|
||||
if (CompanionMesh *cm = static_cast<CompanionMesh *>(_mesh)) {
|
||||
cm->queueLocalSentContactMessage(*recipient, s.timestamp, s.text, delivered);
|
||||
}
|
||||
}
|
||||
|
||||
s.active = false;
|
||||
s.retry_due = false;
|
||||
_next_refresh = 0;
|
||||
}
|
||||
|
||||
void JoystickUITask::processPendingRetries()
|
||||
{
|
||||
for (int i = 0; i < MAX_PENDING_SENDS; i++) {
|
||||
PendingSend &s = _pending_sends[i];
|
||||
if (!s.active || !s.retry_due) continue;
|
||||
s.retry_due = false;
|
||||
if (s.delivered) {
|
||||
completePendingSend(i);
|
||||
continue;
|
||||
}
|
||||
if (s.attempt + 1 >= MAX_SEND_ATTEMPTS) {
|
||||
/* Out of retries. */
|
||||
s.failed = true;
|
||||
completePendingSend(i);
|
||||
continue;
|
||||
}
|
||||
s.attempt++;
|
||||
doPendingSend(i);
|
||||
}
|
||||
}
|
||||
|
||||
bool JoystickUITask::startPendingDM(ContactInfo &recipient, uint32_t ts, const char *text)
|
||||
{
|
||||
if (!text || !_mesh) return false;
|
||||
int slot_idx = allocPendingSendSlot();
|
||||
if (slot_idx < 0) {
|
||||
showAlert("Send queue full", 1500);
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingSend &s = _pending_sends[slot_idx];
|
||||
s.active = true;
|
||||
s.retry_due = false;
|
||||
s.delivered = false;
|
||||
s.failed = false;
|
||||
memcpy(s.recipient_pubkey, recipient.id.pub_key, PUB_KEY_SIZE);
|
||||
strncpy(s.recipient_name, recipient.name, sizeof(s.recipient_name) - 1);
|
||||
s.recipient_name[sizeof(s.recipient_name) - 1] = '\0';
|
||||
s.timestamp = ts;
|
||||
size_t tlen = strlen(text);
|
||||
if (tlen > MAX_TEXT_LEN) tlen = MAX_TEXT_LEN;
|
||||
memcpy(s.text, text, tlen);
|
||||
s.text[tlen] = '\0';
|
||||
s.attempt = 0;
|
||||
s.expected_ack = 0;
|
||||
s.timeout_ms = 0;
|
||||
|
||||
/* Local history entry now (with pending marker — addPreview uses
|
||||
* OUT_PATH_SENT which becomes "(>>) ContactName:"; markSentEntryStatus()
|
||||
* rewrites the marker on outcome). */
|
||||
if (_unread) {
|
||||
_unread->addPreview(OUT_PATH_SENT, s.recipient_name, s.text, /*initially_read=*/true);
|
||||
}
|
||||
|
||||
doPendingSend(slot_idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
ContactInfo *JoystickUITask::tryMatchPendingAck(uint32_t ack)
|
||||
{
|
||||
if (ack == 0) return nullptr;
|
||||
for (int i = 0; i < MAX_PENDING_SENDS; i++) {
|
||||
PendingSend &s = _pending_sends[i];
|
||||
if (!s.active || s.expected_ack != ack) continue;
|
||||
s.delivered = true;
|
||||
ContactInfo *recipient = _mesh ? _mesh->lookupContactByPubKey(s.recipient_pubkey, PUB_KEY_SIZE) : nullptr;
|
||||
completePendingSend(i);
|
||||
return recipient;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool joystick_queue_initialized;
|
||||
|
||||
#define ENTER_LONG_PRESS_MS 500
|
||||
@@ -241,6 +398,16 @@ void JoystickUITask::begin(BaseChatMesh *mesh, mesh::ZephyrRTCClock *rtc, NodePr
|
||||
k_timer_user_data_set(&_lock_timer, this);
|
||||
scheduleLockTimer();
|
||||
|
||||
/* Pending-DM retry timers — one per slot, one-shot, callback signals
|
||||
* refresh and sets the slot's retry_due flag for the main loop. */
|
||||
for (int i = 0; i < MAX_PENDING_SENDS; i++) {
|
||||
_pending_sends[i].task = this;
|
||||
_pending_sends[i].active = false;
|
||||
_pending_sends[i].retry_due = false;
|
||||
k_timer_init(&_pending_sends[i].retry_timer, pendingRetryTimerCb, NULL);
|
||||
k_timer_user_data_set(&_pending_sends[i].retry_timer, &_pending_sends[i]);
|
||||
}
|
||||
|
||||
/* Init key queue */
|
||||
k_msgq_init(&_key_queue, _key_buf, sizeof(char), JOYSTICK_KEY_QUEUE_DEPTH);
|
||||
joystick_queue_initialized = true;
|
||||
@@ -486,6 +653,9 @@ void JoystickUITask::loop()
|
||||
* periodic timers it owns. */
|
||||
onDisplayStateChanged();
|
||||
|
||||
/* Pending-DM retry timers may have fired in ISR. */
|
||||
processPendingRetries();
|
||||
|
||||
/* Dequeue key events */
|
||||
char key = 0;
|
||||
if (k_msgq_get(&_key_queue, &key, K_NO_WAIT) == 0) {
|
||||
@@ -829,20 +999,10 @@ bool JoystickUITask::sendComposedMessage(const char *text)
|
||||
ContactInfo *recipient = _mesh->lookupContactByPubKey(_compose_contact_pubkey, PUB_KEY_SIZE);
|
||||
if (!recipient) return false;
|
||||
uint32_t ts = _rtc ? _rtc->getCurrentTimeUnique() : k_uptime_get_32();
|
||||
uint32_t expected_ack = 0, est_timeout = 0;
|
||||
int result = _mesh->sendMessage(*recipient, ts, 0, text, expected_ack, est_timeout);
|
||||
if (result == 0) return false;
|
||||
ui_signal_tx();
|
||||
if (_unread) {
|
||||
/* Sent message: in history but not "unread" (user sent it). */
|
||||
_unread->addPreview(OUT_PATH_SENT, _compose_contact_name, text,
|
||||
/*initially_read=*/true);
|
||||
}
|
||||
/* Notify BLE app so it can mirror the sent message in its UI. */
|
||||
if (CompanionMesh *cm = static_cast<CompanionMesh *>(_mesh)) {
|
||||
cm->queueLocalSentContactMessage(*recipient, ts, text);
|
||||
}
|
||||
return true;
|
||||
/* Goes through the pending-DM machinery: tracks expected_ack, retries
|
||||
* on no-ACK (up to MAX_SEND_ATTEMPTS, last attempt forces flood),
|
||||
* updates UnreadScreen + queues BLE-app mirror with outcome prefix. */
|
||||
return startPendingDM(*recipient, ts, text);
|
||||
}
|
||||
|
||||
if (_compose_channel_idx == -2) {
|
||||
|
||||
@@ -246,6 +246,43 @@ private:
|
||||
uint8_t _pending_ping_pubkey[4];
|
||||
uint32_t _pending_ping_sent_ms;
|
||||
|
||||
/* Outgoing DM tracking — retries on missing ACK, then forces flood as
|
||||
* a last-ditch attempt, then marks failed. The BLE-app mirror is
|
||||
* deferred until the outcome is known so the phone sees the message
|
||||
* exactly once with a delivery indicator in the body prefix. */
|
||||
static const int MAX_PENDING_SENDS = 4;
|
||||
static const uint8_t MAX_SEND_ATTEMPTS = 5; /* attempt 4 (0-indexed) forces flood */
|
||||
struct PendingSend {
|
||||
JoystickUITask *task; /* back-pointer for ISR dispatch */
|
||||
bool active;
|
||||
volatile bool retry_due; /* set by timer ISR, consumed in loop() */
|
||||
bool delivered;
|
||||
bool failed;
|
||||
uint8_t recipient_pubkey[PUB_KEY_SIZE];
|
||||
char recipient_name[32];
|
||||
uint32_t expected_ack;
|
||||
uint32_t timestamp;
|
||||
char text[MAX_TEXT_LEN + 1];
|
||||
uint8_t attempt;
|
||||
uint32_t timeout_ms;
|
||||
struct k_timer retry_timer;
|
||||
};
|
||||
PendingSend _pending_sends[MAX_PENDING_SENDS];
|
||||
static void pendingRetryTimerCb(struct k_timer *t);
|
||||
int allocPendingSendSlot();
|
||||
void schedulePendingRetry(int slot_idx);
|
||||
void doPendingSend(int slot_idx);
|
||||
void completePendingSend(int slot_idx);
|
||||
void processPendingRetries();
|
||||
public:
|
||||
/* Initiate a DM with retry tracking (called by sendComposedMessage). */
|
||||
bool startPendingDM(ContactInfo &recipient, uint32_t ts, const char *text);
|
||||
/* Try to match an arriving ACK to a pending send.
|
||||
* Returns the recipient ContactInfo* if matched (so BaseChatMesh can do
|
||||
* its return-path-retry housekeeping), nullptr otherwise. */
|
||||
ContactInfo *tryMatchPendingAck(uint32_t ack);
|
||||
private:
|
||||
|
||||
/* Discover signal cache, owned here, populated via onRepeaterDiscoverResp */
|
||||
static constexpr int DISCOVER_SIGNAL_TABLE_SIZE = 16;
|
||||
struct DiscoverSignal {
|
||||
|
||||
@@ -42,6 +42,22 @@ void UnreadScreen::onExit()
|
||||
k_timer_stop(&_preview_timer);
|
||||
}
|
||||
|
||||
void UnreadScreen::markSentEntryStatus(uint32_t ts, const char *contact_name, bool delivered)
|
||||
{
|
||||
if (!contact_name) return;
|
||||
for (int i = 0; i < MAX_UNREAD_MSGS; i++) {
|
||||
if (_entries[i].timestamp != ts) continue;
|
||||
/* Only touch entries that look like our own sent message to that
|
||||
* contact — origin currently starts with "(>>" (set by addPreview
|
||||
* when path_len == OUT_PATH_SENT). */
|
||||
if (strncmp(_entries[i].origin, "(>>", 3) != 0) continue;
|
||||
if (!strstr(_entries[i].origin, contact_name)) continue;
|
||||
snprintf(_entries[i].origin, sizeof(_entries[i].origin),
|
||||
"(>>%s) %s:", delivered ? "+" : "X", contact_name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void UnreadScreen::normalizeUnreadState()
|
||||
{
|
||||
if (_entry_count < 0) _entry_count = 0;
|
||||
|
||||
Reference in New Issue
Block a user