mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-07-29 06:49:43 +00:00
Pins microReticulum to the fork's pyxis-fixes-on-0.4.1 @ b582986 (three 0.4.1-graft regression fixes) and adds the pyxis-side pieces. microReticulum fixes (via the pin): - Identity::recall now accounts for the announce ratchet (matches validate_announce) — it read app_data 32 bytes early for ratchet-bearing announces (Sideband/Columba), garbling peer display names. - DestinationEntry caps persisted random_blobs to PERSIST_RANDOM_BLOBS (16); the set was never trimmed, growing path entries ~12B/re-announce past microStore's 1024B ceiling so put() rejected them (empty path table) — and under a raised ceiling bloated the FS until compaction crashed. - Transport mirrors learned paths into _path_table so path_table() (the UI announce list + T:PATHS) reflects them; the microStore migration stopped populating it. pyxis pieces: - -DUSTORE_DEFAULT_MAX_RECS=400 caps the live path-store record count (defense-in-depth). - AnnounceListScreen::refresh() defers to a main-loop tick(): the gather (path-table iteration + per-entry recall/app_data) runs off the LVGL lock, serialized with the main-loop path-table writes, instead of racing them on the LVGL task and blocking past the 5s lock timeout — which hung/crashed on open once the table was non-empty. Verified on device: display names correct, T:PATHS 0 -> 11, no rejections, no crashes, announce list opens cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
462 lines
16 KiB
C++
462 lines
16 KiB
C++
// Copyright (c) 2024 microReticulum contributors
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "AnnounceListScreen.h"
|
|
#include "Theme.h"
|
|
|
|
#ifdef ARDUINO
|
|
|
|
#include <microReticulum/Log.h>
|
|
#include "../LVGL/LVGLLock.h"
|
|
#include <microReticulum/Transport.h>
|
|
#include <microReticulum/Identity.h>
|
|
#include <microReticulum/Destination.h>
|
|
#include <microReticulum/Persistence/DestinationEntry.h> // RNS::Persistence::DestinationEntry (post-graft path)
|
|
#include <microReticulum/Utilities/OS.h>
|
|
#include "../LVGL/LVGLInit.h"
|
|
#include <MsgPack.h>
|
|
|
|
using namespace RNS;
|
|
|
|
namespace UI {
|
|
namespace LXMF {
|
|
|
|
AnnounceListScreen::AnnounceListScreen(lv_obj_t* parent)
|
|
: _screen(nullptr), _header(nullptr), _list(nullptr),
|
|
_btn_back(nullptr), _btn_refresh(nullptr), _btn_announce(nullptr), _empty_label(nullptr) {
|
|
LVGL_LOCK();
|
|
|
|
// Create screen object
|
|
if (parent) {
|
|
_screen = lv_obj_create(parent);
|
|
} else {
|
|
_screen = lv_obj_create(lv_scr_act());
|
|
}
|
|
|
|
lv_obj_set_size(_screen, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_bg_color(_screen, Theme::surface(), 0);
|
|
lv_obj_set_style_bg_opa(_screen, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_pad_all(_screen, 0, 0);
|
|
lv_obj_set_style_border_width(_screen, 0, 0);
|
|
lv_obj_set_style_radius(_screen, 0, 0);
|
|
|
|
// Create UI components
|
|
create_header();
|
|
create_list();
|
|
|
|
// Hide by default
|
|
hide();
|
|
|
|
TRACE("AnnounceListScreen created");
|
|
}
|
|
|
|
AnnounceListScreen::~AnnounceListScreen() {
|
|
LVGL_LOCK();
|
|
if (_screen) {
|
|
lv_obj_del(_screen);
|
|
}
|
|
}
|
|
|
|
void AnnounceListScreen::create_header() {
|
|
_header = lv_obj_create(_screen);
|
|
lv_obj_set_size(_header, LV_PCT(100), 36);
|
|
lv_obj_align(_header, LV_ALIGN_TOP_MID, 0, 0);
|
|
lv_obj_set_style_bg_color(_header, Theme::surfaceHeader(), 0);
|
|
lv_obj_set_style_border_width(_header, 0, 0);
|
|
lv_obj_set_style_radius(_header, 0, 0);
|
|
lv_obj_set_style_pad_all(_header, 0, 0);
|
|
|
|
// Back button
|
|
_btn_back = lv_btn_create(_header);
|
|
lv_obj_set_size(_btn_back, 50, 28);
|
|
lv_obj_align(_btn_back, LV_ALIGN_LEFT_MID, 2, 0);
|
|
lv_obj_set_style_bg_color(_btn_back, Theme::btnSecondary(), 0);
|
|
lv_obj_set_style_bg_color(_btn_back, Theme::btnSecondaryPressed(), LV_STATE_PRESSED);
|
|
lv_obj_add_event_cb(_btn_back, on_back_clicked, LV_EVENT_CLICKED, this);
|
|
|
|
lv_obj_t* label_back = lv_label_create(_btn_back);
|
|
lv_label_set_text(label_back, LV_SYMBOL_LEFT);
|
|
lv_obj_center(label_back);
|
|
lv_obj_set_style_text_color(label_back, Theme::textSecondary(), 0);
|
|
|
|
// Title
|
|
lv_obj_t* title = lv_label_create(_header);
|
|
lv_label_set_text(title, "Announces");
|
|
lv_obj_align(title, LV_ALIGN_LEFT_MID, 60, 0);
|
|
lv_obj_set_style_text_color(title, Theme::textPrimary(), 0);
|
|
lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0);
|
|
|
|
// Send Announce button (green)
|
|
_btn_announce = lv_btn_create(_header);
|
|
lv_obj_set_size(_btn_announce, 65, 28);
|
|
lv_obj_align(_btn_announce, LV_ALIGN_RIGHT_MID, -70, 0);
|
|
lv_obj_set_style_bg_color(_btn_announce, Theme::primary(), 0);
|
|
lv_obj_set_style_bg_color(_btn_announce, Theme::primaryPressed(), LV_STATE_PRESSED);
|
|
lv_obj_add_event_cb(_btn_announce, on_send_announce_clicked, LV_EVENT_CLICKED, this);
|
|
|
|
lv_obj_t* label_announce = lv_label_create(_btn_announce);
|
|
lv_label_set_text(label_announce, LV_SYMBOL_BELL); // Bell icon for announce
|
|
lv_obj_center(label_announce);
|
|
lv_obj_set_style_text_color(label_announce, Theme::textPrimary(), 0);
|
|
|
|
// Refresh button
|
|
_btn_refresh = lv_btn_create(_header);
|
|
lv_obj_set_size(_btn_refresh, 65, 28);
|
|
lv_obj_align(_btn_refresh, LV_ALIGN_RIGHT_MID, -2, 0);
|
|
lv_obj_set_style_bg_color(_btn_refresh, Theme::btnSecondary(), 0);
|
|
lv_obj_set_style_bg_color(_btn_refresh, Theme::btnSecondaryPressed(), LV_STATE_PRESSED);
|
|
lv_obj_add_event_cb(_btn_refresh, on_refresh_clicked, LV_EVENT_CLICKED, this);
|
|
|
|
lv_obj_t* label_refresh = lv_label_create(_btn_refresh);
|
|
lv_label_set_text(label_refresh, LV_SYMBOL_REFRESH);
|
|
lv_obj_center(label_refresh);
|
|
lv_obj_set_style_text_color(label_refresh, Theme::textPrimary(), 0);
|
|
}
|
|
|
|
void AnnounceListScreen::create_list() {
|
|
_list = lv_obj_create(_screen);
|
|
lv_obj_set_size(_list, LV_PCT(100), 204); // 240 - 36 (header)
|
|
lv_obj_align(_list, LV_ALIGN_TOP_MID, 0, 36);
|
|
lv_obj_set_style_pad_all(_list, 4, 0);
|
|
lv_obj_set_style_pad_gap(_list, 4, 0);
|
|
lv_obj_set_style_bg_color(_list, Theme::surface(), 0);
|
|
lv_obj_set_style_border_width(_list, 0, 0);
|
|
lv_obj_set_style_radius(_list, 0, 0);
|
|
lv_obj_set_flex_flow(_list, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(_list, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
}
|
|
|
|
void AnnounceListScreen::refresh() {
|
|
// Defer the real work to tick() on the main loop (UIManager::update). The
|
|
// gather (iterate the live path table + per-entry Identity::recall /
|
|
// recall_app_data) must NOT run on the LVGL task or hold the render lock:
|
|
// once the path table became non-empty it raced the main-loop path-table
|
|
// writes and blocked past LVGLLock's 5s timeout — hanging, then crashing, on
|
|
// open. refresh() is safe to call from any task; it just arms the tick.
|
|
_refresh_pending.store(true);
|
|
}
|
|
|
|
void AnnounceListScreen::tick() {
|
|
// Runs on the main loop (UIManager::update), so iterating Transport::path_table()
|
|
// and reading the identity cache is serialized with — never concurrent to — the
|
|
// main-loop announce processing that writes them (both run in the Arduino loop).
|
|
// No LVGL lock is held during the gather; only the final render briefly takes it.
|
|
if (!_refresh_pending.exchange(false)) {
|
|
return;
|
|
}
|
|
|
|
// GATHER (no LVGL lock) — build the item list off the render lock.
|
|
std::vector<AnnounceItem> items;
|
|
const auto& dest_table = Transport::path_table();
|
|
for (auto it = dest_table.begin(); it != dest_table.end(); ++it) {
|
|
const Bytes& dest_hash = it->first;
|
|
const RNS::Persistence::DestinationEntry& dest_entry = it->second;
|
|
|
|
// Only lxmf.delivery destinations with a known identity belong in the list.
|
|
Identity identity = Identity::recall(dest_hash);
|
|
if (!identity) {
|
|
continue;
|
|
}
|
|
Bytes expected_hash = Destination::hash(identity, "lxmf", "delivery");
|
|
if (dest_hash != expected_hash) {
|
|
continue;
|
|
}
|
|
|
|
AnnounceItem item;
|
|
item.destination_hash = dest_hash;
|
|
item.hash_display = truncate_hash(dest_hash);
|
|
item.hops = dest_entry._hops;
|
|
item.timestamp = dest_entry._timestamp;
|
|
item.timestamp_str = format_timestamp(dest_entry._timestamp);
|
|
item.has_path = true; // it's in the path table we're iterating — skip the store probe
|
|
Bytes app_data = Identity::recall_app_data(dest_hash);
|
|
if (app_data && app_data.size() > 0) {
|
|
item.display_name = parse_display_name(app_data);
|
|
}
|
|
items.push_back(item);
|
|
if (items.size() >= 64) break; // bound the gather
|
|
}
|
|
|
|
std::sort(items.begin(), items.end(),
|
|
[](const AnnounceItem& a, const AnnounceItem& b) {
|
|
return a.timestamp > b.timestamp;
|
|
});
|
|
|
|
{
|
|
char log_buf[64];
|
|
snprintf(log_buf, sizeof(log_buf), "Announce list: %zu lxmf.delivery destinations", items.size());
|
|
INFO(log_buf);
|
|
}
|
|
|
|
// RENDER (brief LVGL lock) — no store access here, capped item count.
|
|
LVGL_LOCK();
|
|
lv_obj_clean(_list);
|
|
_announce_containers.clear();
|
|
_dest_hash_pool.clear();
|
|
_empty_label = nullptr;
|
|
_announces = std::move(items);
|
|
|
|
if (_announces.empty()) {
|
|
show_empty_state();
|
|
} else {
|
|
const size_t MAX_DISPLAY = 20;
|
|
size_t display_count = std::min(_announces.size(), MAX_DISPLAY);
|
|
_dest_hash_pool.reserve(display_count);
|
|
_announce_containers.reserve(display_count);
|
|
size_t count = 0;
|
|
for (const auto& item : _announces) {
|
|
if (count >= MAX_DISPLAY) break;
|
|
create_announce_item(item);
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
void AnnounceListScreen::show_empty_state() {
|
|
_empty_label = lv_label_create(_list);
|
|
lv_label_set_text(_empty_label, "No announces yet\n\nWaiting for LXMF\ndestinations to announce...");
|
|
lv_obj_set_style_text_color(_empty_label, Theme::textMuted(), 0);
|
|
lv_obj_set_style_text_align(_empty_label, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_align(_empty_label, LV_ALIGN_CENTER, 0, 0);
|
|
}
|
|
|
|
void AnnounceListScreen::create_announce_item(const AnnounceItem& item) {
|
|
// Create container for announce item - compact 2-row layout matching ConversationListScreen
|
|
lv_obj_t* container = lv_obj_create(_list);
|
|
lv_obj_set_size(container, LV_PCT(100), 44);
|
|
lv_obj_set_style_bg_color(container, Theme::surfaceContainer(), 0);
|
|
lv_obj_set_style_bg_color(container, Theme::surfaceElevated(), LV_STATE_PRESSED);
|
|
lv_obj_set_style_border_width(container, 1, 0);
|
|
lv_obj_set_style_border_color(container, Theme::border(), 0);
|
|
lv_obj_set_style_radius(container, 6, 0);
|
|
lv_obj_set_style_pad_all(container, 0, 0);
|
|
lv_obj_add_flag(container, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_clear_flag(container, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
// Focus style for trackball navigation
|
|
lv_obj_set_style_border_color(container, Theme::info(), LV_STATE_FOCUSED);
|
|
lv_obj_set_style_border_width(container, 2, LV_STATE_FOCUSED);
|
|
lv_obj_set_style_bg_color(container, Theme::surfaceElevated(), LV_STATE_FOCUSED);
|
|
|
|
// Store destination hash in user data using pool (avoids per-item heap allocations)
|
|
_dest_hash_pool.push_back(item.destination_hash);
|
|
lv_obj_set_user_data(container, &_dest_hash_pool.back());
|
|
lv_obj_add_event_cb(container, on_announce_clicked, LV_EVENT_CLICKED, this);
|
|
|
|
// Track container for focus group management
|
|
_announce_containers.push_back(container);
|
|
|
|
// Row 1: Display name (if available) or destination hash
|
|
lv_obj_t* label_name = lv_label_create(container);
|
|
if (item.display_name.length() > 0) {
|
|
lv_label_set_text(label_name, item.display_name.c_str());
|
|
} else {
|
|
lv_label_set_text(label_name, item.hash_display.c_str());
|
|
}
|
|
lv_obj_align(label_name, LV_ALIGN_TOP_LEFT, 6, 4);
|
|
lv_obj_set_style_text_color(label_name, Theme::info(), 0);
|
|
lv_obj_set_style_text_font(label_name, &lv_font_montserrat_14, 0);
|
|
|
|
// Row 2: Hops info (left) + Timestamp (right)
|
|
lv_obj_t* label_hops = lv_label_create(container);
|
|
lv_label_set_text(label_hops, format_hops(item.hops).c_str());
|
|
lv_obj_align(label_hops, LV_ALIGN_BOTTOM_LEFT, 6, -4);
|
|
lv_obj_set_style_text_color(label_hops, Theme::textTertiary(), 0);
|
|
|
|
lv_obj_t* label_time = lv_label_create(container);
|
|
lv_label_set_text(label_time, item.timestamp_str.c_str());
|
|
lv_obj_align(label_time, LV_ALIGN_BOTTOM_RIGHT, -6, -4);
|
|
lv_obj_set_style_text_color(label_time, Theme::textMuted(), 0);
|
|
|
|
// Status indicator (green dot if has path) - on row 1, right side
|
|
if (item.has_path) {
|
|
lv_obj_t* status_dot = lv_obj_create(container);
|
|
lv_obj_set_size(status_dot, 8, 8);
|
|
lv_obj_align(status_dot, LV_ALIGN_TOP_RIGHT, -6, 8);
|
|
lv_obj_set_style_bg_color(status_dot, Theme::success(), 0);
|
|
lv_obj_set_style_radius(status_dot, LV_RADIUS_CIRCLE, 0);
|
|
lv_obj_set_style_border_width(status_dot, 0, 0);
|
|
lv_obj_set_style_pad_all(status_dot, 0, 0);
|
|
}
|
|
}
|
|
|
|
void AnnounceListScreen::set_announce_selected_callback(AnnounceSelectedCallback callback) {
|
|
_announce_selected_callback = callback;
|
|
}
|
|
|
|
void AnnounceListScreen::set_back_callback(BackCallback callback) {
|
|
_back_callback = callback;
|
|
}
|
|
|
|
void AnnounceListScreen::set_send_announce_callback(SendAnnounceCallback callback) {
|
|
_send_announce_callback = callback;
|
|
}
|
|
|
|
void AnnounceListScreen::show() {
|
|
LVGL_LOCK();
|
|
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_move_foreground(_screen);
|
|
|
|
// Add widgets to focus group for trackball navigation
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
// Add header buttons first
|
|
if (_btn_back) lv_group_add_obj(group, _btn_back);
|
|
if (_btn_announce) lv_group_add_obj(group, _btn_announce);
|
|
if (_btn_refresh) lv_group_add_obj(group, _btn_refresh);
|
|
|
|
// Add announce containers
|
|
for (lv_obj_t* container : _announce_containers) {
|
|
lv_group_add_obj(group, container);
|
|
}
|
|
|
|
// Focus on first announce if available, otherwise back button
|
|
if (!_announce_containers.empty()) {
|
|
lv_group_focus_obj(_announce_containers[0]);
|
|
} else if (_btn_back) {
|
|
lv_group_focus_obj(_btn_back);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AnnounceListScreen::hide() {
|
|
LVGL_LOCK();
|
|
// Remove from focus group when hiding
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
if (_btn_back) lv_group_remove_obj(_btn_back);
|
|
if (_btn_announce) lv_group_remove_obj(_btn_announce);
|
|
if (_btn_refresh) lv_group_remove_obj(_btn_refresh);
|
|
|
|
// Remove announce containers
|
|
for (lv_obj_t* container : _announce_containers) {
|
|
lv_group_remove_obj(container);
|
|
}
|
|
}
|
|
|
|
lv_obj_add_flag(_screen, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
lv_obj_t* AnnounceListScreen::get_object() {
|
|
return _screen;
|
|
}
|
|
|
|
void AnnounceListScreen::on_announce_clicked(lv_event_t* event) {
|
|
AnnounceListScreen* screen = (AnnounceListScreen*)lv_event_get_user_data(event);
|
|
lv_obj_t* target = lv_event_get_target(event);
|
|
|
|
Bytes* dest_hash = (Bytes*)lv_obj_get_user_data(target);
|
|
|
|
if (dest_hash && screen->_announce_selected_callback) {
|
|
screen->_announce_selected_callback(*dest_hash);
|
|
}
|
|
}
|
|
|
|
void AnnounceListScreen::on_back_clicked(lv_event_t* event) {
|
|
AnnounceListScreen* screen = (AnnounceListScreen*)lv_event_get_user_data(event);
|
|
|
|
if (screen->_back_callback) {
|
|
screen->_back_callback();
|
|
}
|
|
}
|
|
|
|
void AnnounceListScreen::on_refresh_clicked(lv_event_t* event) {
|
|
AnnounceListScreen* screen = (AnnounceListScreen*)lv_event_get_user_data(event);
|
|
screen->refresh();
|
|
}
|
|
|
|
void AnnounceListScreen::on_send_announce_clicked(lv_event_t* event) {
|
|
AnnounceListScreen* screen = (AnnounceListScreen*)lv_event_get_user_data(event);
|
|
if (screen->_send_announce_callback) {
|
|
screen->_send_announce_callback();
|
|
}
|
|
}
|
|
|
|
std::string AnnounceListScreen::format_timestamp(double timestamp) {
|
|
double now = Utilities::OS::time();
|
|
double diff = now - timestamp;
|
|
char buf[16];
|
|
|
|
if (diff < 60) {
|
|
return "Just now";
|
|
} else if (diff < 3600) {
|
|
int mins = (int)(diff / 60);
|
|
snprintf(buf, sizeof(buf), "%dm ago", mins);
|
|
return buf;
|
|
} else if (diff < 86400) {
|
|
int hours = (int)(diff / 3600);
|
|
snprintf(buf, sizeof(buf), "%dh ago", hours);
|
|
return buf;
|
|
} else {
|
|
int days = (int)(diff / 86400);
|
|
snprintf(buf, sizeof(buf), "%dd ago", days);
|
|
return buf;
|
|
}
|
|
}
|
|
|
|
std::string AnnounceListScreen::format_hops(uint8_t hops) {
|
|
if (hops == 0) {
|
|
return "Direct";
|
|
} else if (hops == 1) {
|
|
return "1 hop";
|
|
} else {
|
|
char buf[16];
|
|
snprintf(buf, sizeof(buf), "%u hops", hops);
|
|
return buf;
|
|
}
|
|
}
|
|
|
|
std::string AnnounceListScreen::truncate_hash(const Bytes& hash) {
|
|
return hash.toHex();
|
|
}
|
|
|
|
std::string AnnounceListScreen::parse_display_name(const Bytes& app_data) {
|
|
if (app_data.size() == 0) {
|
|
return std::string();
|
|
}
|
|
|
|
uint8_t first_byte = app_data.data()[0];
|
|
|
|
// Check for msgpack array format (LXMF 0.5.0+)
|
|
// fixarray: 0x90-0x9f (array with 0-15 elements)
|
|
// array16: 0xdc
|
|
if ((first_byte >= 0x90 && first_byte <= 0x9f) || first_byte == 0xdc) {
|
|
// Msgpack encoded: [display_name, stamp_cost, ...]
|
|
MsgPack::Unpacker unpacker;
|
|
unpacker.feed(app_data.data(), app_data.size());
|
|
|
|
// Get array size
|
|
MsgPack::arr_size_t arr_size;
|
|
if (!unpacker.deserialize(arr_size)) {
|
|
return std::string();
|
|
}
|
|
|
|
if (arr_size.size() < 1) {
|
|
return std::string();
|
|
}
|
|
|
|
// First element is display_name (can be nil or bytes)
|
|
if (unpacker.isNil()) {
|
|
unpacker.unpackNil();
|
|
return std::string();
|
|
}
|
|
|
|
MsgPack::bin_t<uint8_t> name_bin;
|
|
if (unpacker.deserialize(name_bin)) {
|
|
// Convert bytes to string
|
|
return std::string((const char*)name_bin.data(), name_bin.size());
|
|
}
|
|
|
|
return std::string();
|
|
} else {
|
|
// Original format: raw UTF-8 string
|
|
return app_data.toString();
|
|
}
|
|
}
|
|
|
|
} // namespace LXMF
|
|
} // namespace UI
|
|
|
|
#endif // ARDUINO
|