diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index 09b6d1a..b91d573 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -767,6 +767,71 @@ void CompanionMesh::queueContactMessage(const ContactInfo &contact, mesh::Packet queueOfflineMessage(frame, i); } +void CompanionMesh::queueLocalSentContactMessage(const ContactInfo &contact, + uint32_t timestamp, const char *text) +{ + if (!text) return; + uint8_t frame[MAX_FRAME_SIZE]; + int i = 0; + + if (_app_target_ver >= 3) { + frame[i++] = PACKET_CONTACT_MSG_V3; + frame[i++] = 0; // SNR (local origin, no incoming packet) + frame[i++] = 0; // reserved1 + frame[i++] = 0; // reserved2 + } else { + frame[i++] = PACKET_CONTACT_MSG_RECV; + } + memcpy(&frame[i], contact.id.pub_key, 6); + i += 6; + frame[i++] = OUT_PATH_SENT; // mark as locally-originated + frame[i++] = TXT_TYPE_PLAIN; + put_le32(&frame[i], timestamp); + i += 4; + size_t text_len = strlen(text); + if (i + text_len > sizeof(frame)) { + text_len = sizeof(frame) - i; + } + memcpy(&frame[i], text, text_len); + i += text_len; + + LOG_DBG("queueLocalSentContactMessage: frame_len=%d", i); + queueOfflineMessage(frame, i); + sendPush(PUSH_CODE_MSG_WAITING); +} + +void CompanionMesh::queueLocalSentChannelMessage(uint8_t channel_idx, + uint32_t timestamp, const char *text) +{ + if (!text) return; + uint8_t frame[MAX_FRAME_SIZE]; + int i = 0; + + if (_app_target_ver >= 3) { + frame[i++] = PACKET_CHANNEL_MSG_V3; + frame[i++] = 0; // SNR + frame[i++] = 0; // reserved1 + frame[i++] = 0; // reserved2 + } else { + frame[i++] = PACKET_CHANNEL_MSG_RECV; + } + frame[i++] = channel_idx; + frame[i++] = OUT_PATH_SENT; + frame[i++] = TXT_TYPE_PLAIN; + put_le32(&frame[i], timestamp); + i += 4; + size_t text_len = strlen(text); + if (i + text_len > sizeof(frame)) { + text_len = sizeof(frame) - i; + } + memcpy(&frame[i], text, text_len); + i += text_len; + + LOG_DBG("queueLocalSentChannelMessage: frame_len=%d channel_idx=%d", i, channel_idx); + queueOfflineMessage(frame, i); + sendPush(PUSH_CODE_MSG_WAITING); +} + void CompanionMesh::onCommandDataRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) { diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index a04af8d..c6dffda 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -191,6 +191,23 @@ public: */ const AdvertPath *findAdvertPath(const uint8_t *pubkey_prefix, int prefix_len); + /** + * 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. + */ + void queueLocalSentContactMessage(const ContactInfo &contact, uint32_t timestamp, + const char *text); + + /** + * Queue a locally-originated channel message into the BLE offline queue + * and signal MSG_WAITING. See queueLocalSentContactMessage(). + */ + void queueLocalSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, + const char *text); + /* DataStoreHost interface */ bool onContactLoaded(const ContactInfo &c) override; bool getContactForSave(uint32_t idx, ContactInfo &c) override; diff --git a/zephcore/helpers/ContactInfo.h b/zephcore/helpers/ContactInfo.h index 73d8745..9dd4f60 100644 --- a/zephcore/helpers/ContactInfo.h +++ b/zephcore/helpers/ContactInfo.h @@ -9,6 +9,11 @@ #include #define OUT_PATH_UNKNOWN 0xFF +/* Marker used in BLE offline-queue frames to indicate the message was sent + * by THIS device (not received). Also reused by the joystick UI's local + * message-preview structs for the same purpose. Not a routing value — + * never appears in over-the-air packet path_len fields. */ +#define OUT_PATH_SENT 0xFE struct ContactInfo { mesh::Identity id; diff --git a/zephcore/helpers/ui-joystick/joystick_defs.h b/zephcore/helpers/ui-joystick/joystick_defs.h index b7a2bea..59b7b5f 100644 --- a/zephcore/helpers/ui-joystick/joystick_defs.h +++ b/zephcore/helpers/ui-joystick/joystick_defs.h @@ -47,9 +47,3 @@ /* List view constants */ #define UI_RECENT_LIST_SIZE 4 /* max items visible in a scrollable list */ - -/* UI display marker: this MsgEntry/preview slot represents a message - * WE SENT (vs received). Used only inside the joystick UI's stored-message - * structs — never assigned to ContactInfo::out_path_len, never serialized, - * never sent over the air. */ -#define OUT_PATH_SENT 0xFE diff --git a/zephcore/helpers/ui-joystick/joystick_screens.h b/zephcore/helpers/ui-joystick/joystick_screens.h index 2c726c3..63a70ab 100644 --- a/zephcore/helpers/ui-joystick/joystick_screens.h +++ b/zephcore/helpers/ui-joystick/joystick_screens.h @@ -402,7 +402,11 @@ public: void activatePreview(bool transient, uint32_t timeout_ms = 10000); int getUnreadCount(); int getStoredMsgCount() const; - void addPreview(uint8_t path_len, const char *from_name, const char *msg); + /* @param initially_read true → store in history without counting as unread + * (used for sent messages, and for received messages + * 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); 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; diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp index f7cbd0a..f67ba23 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp @@ -388,6 +388,27 @@ void JoystickUITask::renderLockOverlay() mc_display_text((w - title_w) / 2, 6, title, false); mc_display_hline(2, 6 + fh + 2, w - 4); + /* Battery + unread (cached values; refreshed on screen wake via the + * normal render-path ui_refresh_battery() gate, so locked-and-asleep + * doesn't burn power). */ + char batt_buf[16]; + if (_cached_batt_mv > 0) { + int pct = ((int)_cached_batt_mv - kBattMinMv) * 100 / (kBattMaxMv - kBattMinMv); + if (pct < 0) pct = 0; + if (pct > 100) pct = 100; + snprintf(batt_buf, sizeof(batt_buf), "Batt: %d%%", pct); + } else { + snprintf(batt_buf, sizeof(batt_buf), "Batt: --"); + } + int batt_w = (int)strlen(batt_buf) * fw; + mc_display_text((w - batt_w) / 2, 6 + fh + 6, batt_buf, false); + + char unread_buf[16]; + int unread = (_unread) ? static_cast(_unread)->getUnreadCount() : 0; + snprintf(unread_buf, sizeof(unread_buf), "Unread: %d", unread); + int unread_w = (int)strlen(unread_buf) * fw; + mc_display_text((w - unread_w) / 2, 6 + fh + 6 + fh + 2, unread_buf, false); + /* Three step unlock sequence */ const char *toks[3] = { "Back", "OK", "Back" }; const int gap = 6; @@ -435,7 +456,10 @@ void JoystickUITask::loop() char key = 0; if (k_msgq_get(&_key_queue, &key, K_NO_WAIT) == 0) { if (!_display.isOn()) { - /* Wake display, consume the key press */ + /* Wake display, consume the key press. Invalidate the battery + * cache so the upcoming render samples a fresh value — matters + * for the lock screen, which the user is about to see. */ + ui_invalidate_battery_cache(); _display.turnOn(); /* mc_display_on() already calls mc_display_reset_auto_off() */ _auto_off = now + _screen_off_ms; _next_refresh = 0; @@ -628,7 +652,10 @@ void JoystickUITask::newMsg(uint8_t path_len, const char *from_name, const char { _msgcount = msgcount; if (_unread) { - static_cast(_unread)->addPreview(path_len, from_name, text); + /* When BLE phone is connected it pulls offline queue and marks read; + * keep the message in our local history but don't count as unread. */ + static_cast(_unread)->addPreview(path_len, from_name, text, + /*initially_read=*/_ble_connected); } /* Wake display if wake on msg is enabled and no BLE peer is connected */ if (_wake_on_msg && !_ble_connected && !_display.isOn()) { @@ -661,7 +688,9 @@ void JoystickUITask::newChannelMsg(const char *channel_name, const char *text, if (_unread) { char ch_from[48]; snprintf(ch_from, sizeof(ch_from), "#%s", channel_name ? channel_name : "?"); - static_cast(_unread)->addPreview(path_len, ch_from, text); + /* BLE-connected → phone reads via offline queue, don't count as unread. */ + static_cast(_unread)->addPreview(path_len, ch_from, text, + /*initially_read=*/_ble_connected); } if (_wake_on_msg && !_ble_connected && !_display.isOn()) { _display.turnOn(); @@ -784,7 +813,13 @@ bool JoystickUITask::sendComposedMessage(const char *text) if (result == 0) return false; ui_signal_tx(); if (_unread) { - static_cast(_unread)->addPreview(OUT_PATH_SENT, _compose_contact_name, text); + /* Sent message: in history but not "unread" (user sent it). */ + static_cast(_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(_mesh)) { + cm->queueLocalSentContactMessage(*recipient, ts, text); } return true; } @@ -816,6 +851,10 @@ bool JoystickUITask::sendComposedMessage(const char *text) p.timestamp = ts; p.path_len = OUT_PATH_SENT; if (_ch_preview_count < JOYSTICK_OFFLINE_QUEUE_SIZE) _ch_preview_count++; + /* Notify BLE app so it can mirror the sent message in its UI. */ + if (CompanionMesh *cm = static_cast(_mesh)) { + cm->queueLocalSentChannelMessage((uint8_t)_compose_channel_idx, ts, text); + } } return ok; } @@ -831,7 +870,13 @@ bool JoystickUITask::sendChannelMessage(const char *text) uint32_t ts = _rtc ? _rtc->getCurrentTimeUnique() : k_uptime_get_32(); bool ok = _mesh->sendGroupMessage(ts, ch.channel, _prefs ? _prefs->node_name : "", text, (int)strlen(text)); - if (ok) ui_signal_tx(); + if (ok) { + ui_signal_tx(); + /* Notify BLE app so it can mirror the sent message in its UI. */ + if (CompanionMesh *cm = static_cast(_mesh)) { + cm->queueLocalSentChannelMessage((uint8_t)_compose_channel_idx, ts, text); + } + } return ok; } diff --git a/zephcore/helpers/ui-joystick/screens/messaging.cpp b/zephcore/helpers/ui-joystick/screens/messaging.cpp index d193f03..81d450c 100644 --- a/zephcore/helpers/ui-joystick/screens/messaging.cpp +++ b/zephcore/helpers/ui-joystick/screens/messaging.cpp @@ -81,7 +81,8 @@ const UnreadScreen::MsgEntry *UnreadScreen::getByListIndex(int idx) const return nullptr; } -void UnreadScreen::addPreview(uint8_t path_len, const char *from_name, const char *msg) +void UnreadScreen::addPreview(uint8_t path_len, const char *from_name, const char *msg, + bool initially_read) { normalizeUnreadState(); _head_index = (_head_index + 1) % MAX_UNREAD_MSGS; @@ -92,8 +93,8 @@ void UnreadScreen::addPreview(uint8_t path_len, const char *from_name, const cha } MsgEntry &entry = _entries[_head_index]; entry.timestamp = _rtc->getCurrentTime(); - entry.read = false; - if (_visible_unread_count < MAX_UNREAD_MSGS) _visible_unread_count++; + entry.read = initially_read; + if (!initially_read && _visible_unread_count < MAX_UNREAD_MSGS) _visible_unread_count++; const char *safe_from = (from_name && from_name[0]) ? from_name : "unknown"; const char *safe_msg = msg ? msg : ""; diff --git a/zephcore/helpers/ui/ui_common.c b/zephcore/helpers/ui/ui_common.c index 1db0f4c..6872933 100644 --- a/zephcore/helpers/ui/ui_common.c +++ b/zephcore/helpers/ui/ui_common.c @@ -224,3 +224,12 @@ void ui_refresh_battery(void) s_batt_last_read_ms = k_uptime_get_32(); s_batt_ever_read = true; } + +/* Forget the freshness timestamp so the next ui_refresh_battery() call is + * guaranteed to sample the ADC. Use when entering a state where a fresh + * reading matters (e.g. just woke the screen from sleep). */ +void ui_invalidate_battery_cache(void) +{ + s_batt_ever_read = false; + s_batt_last_read_ms = 0; +} diff --git a/zephcore/helpers/ui/ui_task.h b/zephcore/helpers/ui/ui_task.h index 056ccb7..58a3b85 100644 --- a/zephcore/helpers/ui/ui_task.h +++ b/zephcore/helpers/ui/ui_task.h @@ -182,6 +182,14 @@ void ui_set_battery_provider(uint16_t (*provider)(void)); */ void ui_refresh_battery(void); +/** + * Drop the battery-refresh freshness timestamp. The next + * ui_refresh_battery() call is guaranteed to sample the ADC. + * Use when waking the display from sleep so the user sees a current + * reading immediately instead of a possibly-stale cached value. + */ +void ui_invalidate_battery_cache(void); + /** * Notify UI of a received contact message. * Rich UIs display the text and sender; simpler ones forward to