joystick UI: lock-screen info, BLE-aware unread, mirror local sends

- 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)
This commit is contained in:
liquidraver committed 2026-05-21 09:37:24 +02:00
1 parent 18a67e679d
commit 3de179f9bc
9 files changed
+163 -15

No files matched your search

+65
View File
@@ -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)
{
+17
View File
@@ -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;
+5
View File
@@ -9,6 +9,11 @@
#include <mesh/MeshCore.h>
#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;
@@ -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
@@ -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;
@@ -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<UnreadScreen *>(_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<UnreadScreen *>(_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<UnreadScreen *>(_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<UnreadScreen *>(_unread)->addPreview(path_len, ch_from, text);
/* BLE-connected → phone reads via offline queue, don't count as unread. */
static_cast<UnreadScreen *>(_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<UnreadScreen *>(_unread)->addPreview(OUT_PATH_SENT, _compose_contact_name, text);
/* Sent message: in history but not "unread" (user sent it). */
static_cast<UnreadScreen *>(_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;
}
@@ -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<CompanionMesh *>(_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<CompanionMesh *>(_mesh)) {
cm->queueLocalSentChannelMessage((uint8_t)_compose_channel_idx, ts, text);
}
}
return ok;
}
@@ -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 : "";
+9
View File
@@ -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;
}
+8
View File
@@ -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