Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/tdeck-feature-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Status labels:
| Lock screen | Functional | clock, battery, network icons, notification card | Add per-network badges once MeshCore is active. |
| Home launcher | Partial | filtered app grid, Developer Mode hides Terminal by default, Messages unread counter badge, scanned local apps flow across paged 4x2 Home screens | V0.95: add full app launch/runtime integration; run hardware visual regression for badge layout. |
| Unified inbox | Functional/Partial | Messages tabs, filters, unread highlighting, per-thread badges, mute indicator, channel tab | MeshCore filter is gated; finish hardware responsiveness pass. |
| Conversation view | Functional/Partial | compose, in-place draft text refresh, scroll-preserving chat rebuilds, bubbles, status colors, resend long-press, persisted sent-DM delivery metadata | Stock-device ACK/retry interop and hardware chat-log latency still need validation. |
| Conversation view | Functional/Partial | compose, in-place draft text refresh, scroll-preserving chat rebuilds, bubbles, status colors, resend long-press, persisted sent-DM delivery metadata, and display-safe chat rendering for malformed UTF-8/common emoji aliases | Stock-device ACK/retry interop, hardware chat-log latency, and broader visual emoji coverage still need validation. |
| Meshtastic manager | Functional/Partial | identity card, virtualized node list, channels tab, separate USB and BLE companion toggles | Emergency channel row is disabled; BLE companion has Android connect/send/receive proof but still needs reconnect/disconnect soak. |
| MeshCore manager | Prototype/Partial | "Coming soon" unless gate is flipped; deeper screen exists behind gate | Do not enable until MeshCore message path works. |
| Contacts/detail | Functional/Partial | virtualized contacts list, add contact, messageable role check, bounded trace diagnostic for contact detail | MeshCore contacts locked; hardware long-list scroll and Trace-on-device need validation. |
Expand Down
5 changes: 4 additions & 1 deletion docs/tdeck-firmware-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ These maintainer-provided beta labels are the canonical near-term sequence. The
| V0.6 | MeshCore public chat and split airtime config | In progress - public chat send/receive hardware-verified; split-airtime serial dwell/switch smoke passed on COM8; packet-loss, latency, and real dual-network traffic soak still open |
| V0.7 | MeshCore DMs and private chats | ✅ Encrypted DMs (X25519 ECDH + AES) send/receive hardware-verified against a real MeshCore peer |
| V0.8 | MeshCore USB companion and MeshCore BLE companion | 🚧 Protocol foundation drafted; USB/BLE implementation still planned and not external-app compatible yet |
| V0.9 | Code review, optimization, and emoji polish | ⬜ Not started |
| V0.9 | Code review, optimization, and emoji polish | In progress - display-safe chat text and emoji aliases implemented in UI rendering |
| V0.95 | Basic app SDK and infrastructure; Home UI supports adding apps and multiple home screens | 🚧 Local manifest scanner, Home paging, and detail shell started; runtime/catalog still TODO |
| V0.96 | Upgraded Wi-Fi password storage | ✅ Implemented on T-Deck hardware: credentials use ESP32 NVS, legacy `wifi.cfg` migrates/removes, and diagnostics do not print passwords |

Expand Down Expand Up @@ -232,6 +232,9 @@ Deliverables:
for Data, POSITION, and TELEMETRY decoders.
- Clean up dead demo data and stale comments that no longer match product state.
- Add basic emoji rendering/input support appropriate for the T-Deck screen and memory budget.
Initial UI slice implemented: Messages, conversation bubbles, draft text, and
lock-screen notifications now render through a bounded display-safe text path
that aliases common emoji and folds malformed UTF-8 before LVGL measures text.
- Re-run hardware dogfood tests on Meshtastic-only, MeshCore-only, and split-airtime modes.

Exit criteria:
Expand Down
49 changes: 49 additions & 0 deletions sim/main_sim.c
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,14 @@ static int shots(const char *dir)
pump(60);
snprintf(path, sizeof path, "%s/17-convo-draft.bmp", dir);
write_bmp(path); printf("wrote %s\n", path);
{
const char safe_draft[] = "ETA 12 " "\xF0\x9F\x94\xA5" " near waypoint, bring " "\xF0\x9F\x94\x8B" " pack";
snprintf(S.draft, sizeof S.draft, "%s", safe_draft);
lz_convo_draft_refresh();
pump(60);
snprintf(path, sizeof path, "%s/17b-convo-safe-draft.bmp", dir);
write_bmp(path); printf("wrote %s\n", path);
}
lz_ui_key(LZ_K_ENTER, 0);
pump(60);
snprintf(path, sizeof path, "%s/18-convo-sent.bmp", dir);
Expand Down Expand Up @@ -635,6 +643,47 @@ static int codec_selftest(void)
/* 1. default LongFast channel hash must be 0x08 */
CHECK(mt_channel_hash() == 0x08, "channel hash(LongFast,defaultPSK) == 0x08");

/* UI chat labels only receive display-safe text: protocol/storage bytes are
* left alone, but invalid UTF-8 and unsupported glyphs are folded before
* LVGL measures or renders them. */
{
char out[48];
lz_text_safe_copy(out, sizeof out, "plain text", LZ_TEXT_SAFE_LINE);
CHECK(strcmp(out, "plain text") == 0, "display text leaves ASCII unchanged");
lz_text_safe_copy(out, sizeof out, "one\ntwo\tthree", LZ_TEXT_SAFE_LINE);
CHECK(strcmp(out, "one two three") == 0, "display line text folds controls");
lz_text_safe_copy(out, sizeof out, "one\ntwo", LZ_TEXT_SAFE_BLOCK);
CHECK(strcmp(out, "one\ntwo") == 0, "display block text preserves newlines");
lz_text_safe_copy(out, sizeof out,
"rx " "\xF0\x9F\x94\xA5" " " "\xE2\x9C\x85",
LZ_TEXT_SAFE_LINE);
CHECK(strcmp(out, "rx fire OK") == 0, "display text aliases common emoji");
lz_text_safe_copy(out, sizeof out, "copy " "\xE2\x80\x94" " 73",
LZ_TEXT_SAFE_LINE);
CHECK(strcmp(out, "copy - 73") == 0, "display text aliases punctuation");
{
const char bad[] = { 'b', 'a', 'd', ' ', (char)0xF0, (char)0x28,
(char)0x8C, (char)0x28, 0 };
bool ok = true;
lz_text_safe_copy(out, sizeof out, bad, LZ_TEXT_SAFE_LINE);
for(size_t i = 0; out[i]; i++) {
unsigned char c = (unsigned char)out[i];
if(c >= 0x80u || c < 32u || c == 127u) ok = false;
}
CHECK(ok && strstr(out, "?") != NULL,
"display text replaces malformed UTF-8");
}
{
char small[5];
size_t n = lz_text_safe_copy(small, sizeof small, "abcdef",
LZ_TEXT_SAFE_LINE);
CHECK(n == 4 && strcmp(small, "abcd") == 0,
"display text stays nul-terminated under cap");
}
lz_text_safe_copy(out, sizeof out, NULL, LZ_TEXT_SAFE_LINE);
CHECK(out[0] == 0, "display text accepts null input");
}

/* 2. text frame round-trip: build -> parse header -> decrypt -> decode */
const char *msg = "hello mesh, this is limitlezzOS";
uint32_t from = 0x7c3af1d0, to = 0xa1b2c3d4, id = 0x12345678;
Expand Down
8 changes: 6 additions & 2 deletions src/ui/screens/scr_lock_home.c
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,18 @@ void lz_scr_lock(lv_obj_t *root)
lv_obj_set_flex_align(hrow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(hrow, 6, 0);
lz_icon(hrow, LZ_I_FORUM, &lz_icons_14, LZ_MINT);
lv_obj_t *nm = lz_text(hrow, g_notif_t->name, LZ_F_BODY, LZ_TEXT);
char safe_name[48];
char safe_last[96];
lz_text_safe_copy(safe_name, sizeof safe_name, g_notif_t->name, LZ_TEXT_SAFE_LINE);
lz_text_safe_copy(safe_last, sizeof safe_last, g_notif_t->last_text, LZ_TEXT_SAFE_LINE);
lv_obj_t *nm = lz_text(hrow, safe_name, LZ_F_BODY, LZ_TEXT);
lv_obj_set_flex_grow(nm, 1);
lv_label_set_long_mode(nm, LV_LABEL_LONG_DOT);
if(g_notif_t->unread > 1) {
char cnt[8]; snprintf(cnt, sizeof cnt, "%d", g_notif_t->unread);
lz_text(hrow, cnt, LZ_F_SMALL, LZ_MINT);
}
lv_obj_t *snip = lz_text(card, g_notif_t->last_text, LZ_F_SMALL, lv_color_hex(0xCFD4DA));
lv_obj_t *snip = lz_text(card, safe_last, LZ_F_SMALL, lv_color_hex(0xCFD4DA));
lv_obj_set_width(snip, lv_pct(100));
lv_label_set_long_mode(snip, LV_LABEL_LONG_DOT);
lz_on_click(card, notif_tap);
Expand Down
50 changes: 28 additions & 22 deletions src/ui/screens/scr_messages.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,14 @@ static void draft_label_text(const lz_thread_rt *t, char *out, size_t cap, bool
bool has = S.draft[0] != 0;
if(has_draft) *has_draft = has;
if(!has) {
snprintf(out, cap, "Message %s...", t ? t->name : "thread");
char name[40];
lz_text_safe_copy(name, sizeof name, t ? t->name : "thread", LZ_TEXT_SAFE_LINE);
snprintf(out, cap, "Message %s...", name);
return;
}

snprintf(out, cap, "%s", S.draft);
int maxw = LZ_W - 80; /* usable px inside the pill */
lv_point_t sz;
lv_txt_get_size(&sz, S.draft, LZ_F_SMALL, 0, 0, LV_COORD_MAX, 0);
if(sz.x <= maxw) return;

int start = 0;
while(S.draft[start]) {
lv_txt_get_size(&sz, S.draft + start, LZ_F_SMALL, 0, 0, LV_COORD_MAX, 0);
if(sz.x <= maxw - 12) break; /* leave room for the ellipsis */
start++;
}
snprintf(out, cap, "...%s", S.draft + start);
lz_text_safe_tail_fit(out, cap, S.draft, LZ_F_SMALL, maxw, LZ_TEXT_SAFE_LINE);
}

bool lz_convo_draft_refresh(void)
Expand Down Expand Up @@ -232,6 +223,10 @@ void lz_scr_messages(lv_obj_t *root)
for(int i = 0; i < vis_thread_count; i++) {
lz_thread_rt *t = vis_threads[i];
char ago[8]; lz_fmt_ago(t->last_ts, ago, sizeof ago);
char tname[48];
char last_text[96];
lz_text_safe_copy(tname, sizeof tname, t->name, LZ_TEXT_SAFE_LINE);
lz_text_safe_copy(last_text, sizeof last_text, t->last_text, LZ_TEXT_SAFE_LINE);
bool unread = t->unread && !t->muted;
lv_obj_t *row = lz_row(body, i == S.focus);
lv_obj_set_style_radius(row, 11, 0);
Expand All @@ -244,7 +239,7 @@ void lz_scr_messages(lv_obj_t *root)
lv_obj_t *avwrap = lz_box(row);
lv_obj_set_size(avwrap, 34, 34);
lv_obj_t *av = lz_dot(avwrap, 33, lz_av_color(t->net));
char initial[2] = { t->name[0], 0 };
char initial[2] = { (tname[0] && tname[0] != ' ') ? tname[0] : '?', 0 };
lv_obj_t *ini = lz_text(av, initial, LZ_F_BODY, lv_color_white());
lv_obj_center(ini);
lv_obj_t *ring = lz_dot(avwrap, 13, LZ_SCREEN_BG);
Expand All @@ -264,7 +259,7 @@ void lz_scr_messages(lv_obj_t *root)
lv_obj_set_height(top, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(top, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(top, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *name = lz_text(top, t->name, LZ_F_BODY, unread ? lv_color_hex(0xF2F4F6) : LZ_TEXT);
lv_obj_t *name = lz_text(top, tname, LZ_F_BODY, unread ? lv_color_hex(0xF2F4F6) : LZ_TEXT);
lv_label_set_long_mode(name, LV_LABEL_LONG_DOT);
lz_text(top, ago, LZ_F_SMALL, LZ_TEXT_META);

Expand All @@ -273,7 +268,7 @@ void lz_scr_messages(lv_obj_t *root)
lv_obj_set_height(bot, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(bot, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(bot, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *snip = lz_text(bot, t->last_text, LZ_F_SMALL,
lv_obj_t *snip = lz_text(bot, last_text, LZ_F_SMALL,
unread ? lv_color_hex(0xCFD4DA) : lv_color_hex(0x838A93));
lv_label_set_long_mode(snip, LV_LABEL_LONG_DOT);
lv_obj_set_flex_grow(snip, 1);
Expand Down Expand Up @@ -331,6 +326,10 @@ void lz_scr_messages(lv_obj_t *root)
for(int i = 0; i < vis_thread_count; i++) {
lz_thread_rt *t = vis_threads[i];
char ago[8]; lz_fmt_ago(t->last_ts, ago, sizeof ago);
char tname[48];
char last_text[96];
lz_text_safe_copy(tname, sizeof tname, t->name, LZ_TEXT_SAFE_LINE);
lz_text_safe_copy(last_text, sizeof last_text, t->last_text, LZ_TEXT_SAFE_LINE);
lv_obj_t *row = lz_row(body, i == S.focus);
lv_obj_set_style_radius(row, 11, 0);
lv_obj_add_event_cb(row, mute_longpress_cb, LV_EVENT_LONG_PRESSED, t); /* hold = silence */
Expand All @@ -355,10 +354,10 @@ void lz_scr_messages(lv_obj_t *root)
lv_obj_set_height(top, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(top, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(top, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lz_text(top, t->name, LZ_F_BODY, LZ_TEXT);
lz_text(top, tname, LZ_F_BODY, LZ_TEXT);
lz_text(top, t->last_ts ? ago : "", LZ_F_SMALL, LZ_TEXT_META);
lz_text(colm, "Primary - broadcast to everyone", LZ_F_SMALL, lv_color_hex(0x838A93));
lv_obj_t *last = lz_text(colm, t->last_text[0] ? t->last_text : "Tap to open the channel",
lv_obj_t *last = lz_text(colm, last_text[0] ? last_text : "Tap to open the channel",
LZ_F_SMALL, LZ_TEXT_VALUE);
lv_label_set_long_mode(last, LV_LABEL_LONG_DOT);
lv_obj_set_width(last, lv_pct(100));
Expand All @@ -380,6 +379,12 @@ void lz_scr_convo(lv_obj_t *root)
if(!t) t = lz_svc_thread_at(0);
if(!t) return;
lv_color_t net = lz_net_color(t->net);
char tname[48];
char taddr[24];
char tpath[20];
lz_text_safe_copy(tname, sizeof tname, t->name, LZ_TEXT_SAFE_LINE);
lz_text_safe_copy(taddr, sizeof taddr, t->addr, LZ_TEXT_SAFE_LINE);
lz_text_safe_copy(tpath, sizeof tpath, t->path, LZ_TEXT_SAFE_LINE);
lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN);

/* nav bar: name + network tag */
Expand All @@ -396,7 +401,7 @@ void lz_scr_convo(lv_obj_t *root)
lv_obj_set_size(backhit, 64, LZ_NAVBAR_H);
lv_obj_set_pos(backhit, 0, 0);
lz_on_click(backhit, lz_back);
lv_obj_t *name = lz_text(bar, t->name, LZ_F_BODY, lv_color_hex(0xF2F4F6));
lv_obj_t *name = lz_text(bar, tname, LZ_F_BODY, lv_color_hex(0xF2F4F6));
lv_obj_align(name, LV_ALIGN_TOP_MID, 0, 3);
lv_obj_t *sub = lz_box(bar);
lv_obj_set_size(sub, LV_SIZE_CONTENT, 10);
Expand All @@ -405,7 +410,7 @@ void lz_scr_convo(lv_obj_t *root)
lv_obj_set_style_pad_column(sub, 4, 0);
lz_dot(sub, 5, net);
lz_text(sub, lz_net_name(t->net), LZ_F_SMALL, net);
char pathb[24]; snprintf(pathb, sizeof pathb, "- %s", t->path);
char pathb[24]; snprintf(pathb, sizeof pathb, "- %s", tpath);
lz_text(sub, pathb, LZ_F_SMALL, LZ_TEXT_META);
lv_obj_align(sub, LV_ALIGN_BOTTOM_MID, 0, -1);

Expand All @@ -428,7 +433,7 @@ void lz_scr_convo(lv_obj_t *root)
lz_nav_set_scroll(body);

char cap[64];
snprintf(cap, sizeof cap, "Encrypted - %s - %s", lz_net_name(t->net), t->addr);
snprintf(cap, sizeof cap, "Encrypted - %s - %s", lz_net_name(t->net), taddr);
lv_obj_t *caption = lz_text(body, cap, LZ_F_SMALL, lv_color_hex(0x5E656E));
lv_obj_set_width(caption, lv_pct(100));
lv_obj_set_style_text_align(caption, LV_TEXT_ALIGN_CENTER, 0);
Expand All @@ -437,7 +442,8 @@ void lz_scr_convo(lv_obj_t *root)
int total = lz_svc_tail(&msgs);
for(int i = 0; i < total; i++) {
bool self = msgs[i].self;
const char *txt = msgs[i].text;
char txt[sizeof msgs[i].text + 32];
lz_text_safe_copy(txt, sizeof txt, msgs[i].text, LZ_TEXT_SAFE_BLOCK);

/* row spanning the width; justify the bubble cluster right for self
* (outgoing) and left for incoming — the standard messenger layout */
Expand Down
Loading