TCP scaling, GUI pooling, dev mode, protocol improvements

- TCP: wider drain budgets, TCP_NODELAY, no flush, PSRAM buffers, queue announces until hub ID
- GUI: object pool Messages/Contacts screens, partial MessageView status, targeted tab refresh
- LVGL throttled to 5fps when dimmed, non-blocking RSSI monitor, bounded I2S writes
- LXMF batch drain, BLE frame mutex, LoRa interrupt-driven RX, name cache cap 300
- Developer mode in settings: custom radio params (freq/txp/SF/BW/CR/preamble) behind warning
This commit is contained in:
DeFiDude committed 2026-03-15 12:25:29 -06:00
1 parent 02d7dedcce
commit fc91f8214e
30 files changed
+604 -238

No files matched your search

+18 -12
View File
@@ -60,22 +60,28 @@ void LvTabBar::cycleTab(int direction) {
void LvTabBar::setUnreadCount(int tab, int count) {
if (tab < 0 || tab >= TAB_COUNT) return;
if (_unread[tab] == count) return; // No change
_unread[tab] = count;
refreshTabs();
refreshTab(tab);
}
void LvTabBar::refreshTab(int idx) {
if (idx < 0 || idx >= TAB_COUNT || !_tabs[idx]) return;
bool active = (idx == _activeTab);
lv_obj_set_style_text_color(_tabs[idx],
lv_color_hex(active ? Theme::TAB_ACTIVE : Theme::TAB_INACTIVE), 0);
char buf[24];
if (_unread[idx] > 0) {
snprintf(buf, sizeof(buf), "%s(%d)", TAB_NAMES[idx], _unread[idx]);
} else {
snprintf(buf, sizeof(buf), "%s", TAB_NAMES[idx]);
}
lv_label_set_text(_tabs[idx], buf);
}
void LvTabBar::refreshTabs() {
for (int i = 0; i < TAB_COUNT; i++) {
bool active = (i == _activeTab);
lv_obj_set_style_text_color(_tabs[i],
lv_color_hex(active ? Theme::TAB_ACTIVE : Theme::TAB_INACTIVE), 0);
char buf[24];
if (_unread[i] > 0) {
snprintf(buf, sizeof(buf), "%s(%d)", TAB_NAMES[i], _unread[i]);
} else {
snprintf(buf, sizeof(buf), "%s", TAB_NAMES[i]);
}
lv_label_set_text(_tabs[i], buf);
refreshTab(i);
}
}
+1
View File
@@ -21,6 +21,7 @@ public:
private:
void refreshTabs();
void refreshTab(int idx);
lv_obj_t* _bar = nullptr;
lv_obj_t* _tabs[TAB_COUNT] = {};
+67 -34
View File
@@ -28,6 +28,31 @@ void LvContactsScreen::createUI(lv_obj_t* parent) {
lv_obj_set_layout(_list, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(_list, LV_FLEX_FLOW_COLUMN);
// Pre-allocate row pool
const lv_font_t* font = &lv_font_montserrat_14;
for (int i = 0; i < ROW_POOL_SIZE; i++) {
lv_obj_t* row = lv_obj_create(_list);
lv_obj_set_size(row, Theme::CONTENT_W, 28);
lv_obj_set_style_bg_color(row, lv_color_hex(Theme::BG), 0);
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
lv_obj_set_style_border_width(row, 1, 0);
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_style_radius(row, 0, 0);
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN);
lv_obj_t* lbl = lv_label_create(row);
lv_obj_set_style_text_font(lbl, font, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_text(lbl, "");
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 8, 0);
_poolRows[i] = row;
_poolNameLabels[i] = lbl;
}
_lastContactCount = -1;
rebuildList();
}
@@ -35,6 +60,7 @@ void LvContactsScreen::createUI(lv_obj_t* parent) {
void LvContactsScreen::onEnter() {
_lastContactCount = -1;
_selectedIdx = 0;
_viewportStart = 0;
rebuildList();
}
@@ -48,20 +74,13 @@ void LvContactsScreen::refreshUI() {
}
void LvContactsScreen::updateSelection(int oldIdx, int newIdx) {
if (oldIdx >= 0 && oldIdx < (int)_rows.size()) {
lv_obj_set_style_bg_color(_rows[oldIdx], lv_color_hex(Theme::BG), 0);
}
if (newIdx >= 0 && newIdx < (int)_rows.size()) {
lv_obj_set_style_bg_color(_rows[newIdx], lv_color_hex(Theme::SELECTION_BG), 0);
lv_obj_scroll_to_view(_rows[newIdx], LV_ANIM_OFF);
}
syncVisibleRows();
}
void LvContactsScreen::rebuildList() {
if (!_am || !_list) return;
_rows.clear();
_contactIndices.clear();
lv_obj_clean(_list);
const auto& nodes = _am->nodes();
for (int i = 0; i < (int)nodes.size(); i++) {
@@ -73,6 +92,7 @@ void LvContactsScreen::rebuildList() {
if (count == 0) {
lv_obj_clear_flag(_lblEmpty, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_list, LV_OBJ_FLAG_HIDDEN);
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
return;
}
@@ -82,32 +102,47 @@ void LvContactsScreen::rebuildList() {
if (_selectedIdx >= count) _selectedIdx = count - 1;
if (_selectedIdx < 0) _selectedIdx = 0;
const lv_font_t* font = &lv_font_montserrat_14;
syncVisibleRows();
}
for (int i = 0; i < count; i++) {
const auto& node = nodes[_contactIndices[i]];
bool selected = (i == _selectedIdx);
void LvContactsScreen::syncVisibleRows() {
if (!_am || !_list) return;
int count = (int)_contactIndices.size();
const auto& nodes = _am->nodes();
lv_obj_t* row = lv_obj_create(_list);
lv_obj_set_size(row, Theme::CONTENT_W, 28);
lv_obj_set_style_bg_color(row, lv_color_hex(
selected ? Theme::SELECTION_BG : Theme::BG), 0);
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
lv_obj_set_style_border_width(row, 1, 0);
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_style_radius(row, 0, 0);
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
if (count == 0) {
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
return;
}
// Display name only
lv_obj_t* lbl = lv_label_create(row);
lv_obj_set_style_text_font(lbl, font, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_text(lbl, node.name.c_str());
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 8, 0);
// Compute viewport centered on selection
int halfPool = ROW_POOL_SIZE / 2;
_viewportStart = _selectedIdx - halfPool;
if (_viewportStart < 0) _viewportStart = 0;
if (_viewportStart + ROW_POOL_SIZE > count) {
_viewportStart = count - ROW_POOL_SIZE;
if (_viewportStart < 0) _viewportStart = 0;
}
_rows.push_back(row);
for (int i = 0; i < ROW_POOL_SIZE; i++) {
int contactIdx = _viewportStart + i;
if (contactIdx >= count) {
lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
continue;
}
lv_obj_clear_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
int nodeIdx = _contactIndices[contactIdx];
if (nodeIdx < 0 || nodeIdx >= (int)nodes.size()) {
lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
continue;
}
const auto& node = nodes[nodeIdx];
bool isSelected = (contactIdx == _selectedIdx);
lv_obj_set_style_bg_color(_poolRows[i], lv_color_hex(
isSelected ? Theme::SELECTION_BG : Theme::BG), 0);
lv_label_set_text(_poolNameLabels[i], node.name.c_str());
}
}
@@ -148,17 +183,15 @@ bool LvContactsScreen::handleKey(const KeyEvent& event) {
if (event.up) {
if (_selectedIdx > 0) {
int prev = _selectedIdx;
_selectedIdx--;
updateSelection(prev, _selectedIdx);
syncVisibleRows();
}
return true;
}
if (event.down) {
if (_selectedIdx < count - 1) {
int prev = _selectedIdx;
_selectedIdx++;
updateSelection(prev, _selectedIdx);
syncVisibleRows();
}
return true;
}
+7
View File
@@ -25,6 +25,7 @@ public:
private:
void rebuildList();
void syncVisibleRows();
void updateSelection(int oldIdx, int newIdx);
AnnounceManager* _am = nullptr;
@@ -33,9 +34,15 @@ private:
bool _confirmDelete = false;
int _lastContactCount = -1;
int _selectedIdx = 0;
int _viewportStart = 0;
std::vector<int> _contactIndices; // Maps row -> node index in _am->nodes()
lv_obj_t* _list = nullptr;
lv_obj_t* _lblEmpty = nullptr;
std::vector<lv_obj_t*> _rows;
// Object pool
static constexpr int ROW_POOL_SIZE = 10;
lv_obj_t* _poolRows[ROW_POOL_SIZE] = {};
lv_obj_t* _poolNameLabels[ROW_POOL_SIZE] = {};
};
+39 -3
View File
@@ -111,14 +111,14 @@ void LvMessageView::onEnter() {
_lxmf->markRead(_peerHex);
// Update unread badge on Messages tab
if (_ui) _ui->lvTabBar().setUnreadCount(LvTabBar::TAB_MSGS, _lxmf->unreadCount());
// Register status callback — update cached message status in-place
// Register status callback — partial update without full rebuild
std::string peer = _peerHex;
_lxmf->setStatusCallback([this, peer](const std::string& peerHex, double, LXMFStatus newStatus) {
if (peerHex != peer) return;
for (int i = _cachedMsgs.size() - 1; i >= 0; i--) {
if (!_cachedMsgs[i].incoming && _cachedMsgs[i].status == LXMFStatus::QUEUED) {
_cachedMsgs[i].status = newStatus;
rebuildMessages();
updateMessageStatus(i, newStatus);
break;
}
}
@@ -174,6 +174,8 @@ void LvMessageView::rebuildMessages() {
_lastMsgCount = (int)_cachedMsgs.size();
_lastRefreshMs = millis();
lv_obj_clean(_msgScroll);
_statusLabels.clear();
_textLabels.clear();
const lv_font_t* font = &lv_font_montserrat_12;
int maxBubbleW = Theme::CONTENT_W * 3 / 4;
@@ -229,7 +231,7 @@ void LvMessageView::rebuildMessages() {
lv_obj_set_width(lbl, maxBubbleW - 14);
lv_label_set_text(lbl, msg.content.c_str());
// Status indicator for outgoing
// Status indicator for outgoing (tracked for partial updates)
if (!msg.incoming) {
const char* ind = "~";
uint32_t indColor = Theme::MUTED;
@@ -243,6 +245,11 @@ void LvMessageView::rebuildMessages() {
lv_obj_set_style_text_color(statusLbl, lv_color_hex(indColor), 0);
lv_label_set_text(statusLbl, ind);
lv_obj_align(statusLbl, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
_statusLabels.push_back(statusLbl);
_textLabels.push_back(lbl);
} else {
_statusLabels.push_back(nullptr);
_textLabels.push_back(nullptr);
}
// Timestamp below bubble
@@ -269,6 +276,35 @@ void LvMessageView::rebuildMessages() {
lv_obj_scroll_to_y(_msgScroll, LV_COORD_MAX, LV_ANIM_OFF);
}
void LvMessageView::updateMessageStatus(int msgIdx, LXMFStatus status) {
if (msgIdx < 0 || msgIdx >= (int)_statusLabels.size()) return;
lv_obj_t* statusLbl = _statusLabels[msgIdx];
lv_obj_t* textLbl = _textLabels[msgIdx];
if (!statusLbl) return; // Incoming message, no status label
// Update status indicator
const char* ind = "~";
uint32_t indColor = Theme::MUTED;
if (status == LXMFStatus::SENT || status == LXMFStatus::DELIVERED) {
ind = "*"; indColor = Theme::ACCENT;
} else if (status == LXMFStatus::FAILED) {
ind = "!"; indColor = Theme::ERROR_CLR;
}
lv_obj_set_style_text_color(statusLbl, lv_color_hex(indColor), 0);
lv_label_set_text(statusLbl, ind);
// Update text color to match status
if (textLbl) {
uint32_t textColor = Theme::PRIMARY;
if (status == LXMFStatus::QUEUED || status == LXMFStatus::SENDING) {
textColor = Theme::WARNING_CLR;
} else if (status == LXMFStatus::FAILED) {
textColor = Theme::ERROR_CLR;
}
lv_obj_set_style_text_color(textLbl, lv_color_hex(textColor), 0);
}
}
void LvMessageView::sendCurrentMessage() {
if (!_lxmf || _peerHex.empty() || _inputText.empty()) return;
+6
View File
@@ -43,6 +43,8 @@ private:
unsigned long _lastRefreshMs = 0;
std::vector<LXMFMessage> _cachedMsgs;
void updateMessageStatus(int msgIdx, LXMFStatus status);
// LVGL widgets
lv_obj_t* _header = nullptr;
lv_obj_t* _lblHeader = nullptr;
@@ -51,5 +53,9 @@ private:
lv_obj_t* _textarea = nullptr;
lv_obj_t* _btnSend = nullptr;
// Per-message status labels for partial updates (avoids full rebuild)
std::vector<lv_obj_t*> _statusLabels;
std::vector<lv_obj_t*> _textLabels;
static constexpr unsigned long REFRESH_INTERVAL_MS = 2000; // Check for new messages every 2s
};
+119 -77
View File
@@ -32,6 +32,62 @@ void LvMessagesScreen::createUI(lv_obj_t* parent) {
lv_obj_set_layout(_list, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(_list, LV_FLEX_FLOW_COLUMN);
// Pre-allocate row pool (following LvNodesScreen pattern)
const lv_font_t* nameFont = &lv_font_montserrat_14;
const lv_font_t* smallFont = &lv_font_montserrat_12;
for (int i = 0; i < ROW_POOL_SIZE; i++) {
lv_obj_t* row = lv_obj_create(_list);
lv_obj_set_size(row, Theme::CONTENT_W, 38);
lv_obj_set_style_bg_color(row, lv_color_hex(Theme::BG), 0);
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
lv_obj_set_style_border_width(row, 1, 0);
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_style_radius(row, 0, 0);
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN);
// Unread dot
lv_obj_t* dot = lv_obj_create(row);
lv_obj_set_size(dot, 6, 6);
lv_obj_set_style_radius(dot, 3, 0);
lv_obj_set_style_bg_color(dot, lv_color_hex(Theme::PRIMARY), 0);
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(dot, 0, 0);
lv_obj_set_style_pad_all(dot, 0, 0);
lv_obj_set_pos(dot, 5, 8);
lv_obj_add_flag(dot, LV_OBJ_FLAG_HIDDEN);
// Name label (line 1)
lv_obj_t* nameLbl = lv_label_create(row);
lv_obj_set_style_text_font(nameLbl, nameFont, 0);
lv_obj_set_style_text_color(nameLbl, lv_color_hex(Theme::PRIMARY), 0);
lv_label_set_text(nameLbl, "");
lv_obj_align(nameLbl, LV_ALIGN_TOP_LEFT, 14, 2);
// Time label (line 1, right)
lv_obj_t* timeLbl = lv_label_create(row);
lv_obj_set_style_text_font(timeLbl, &lv_font_montserrat_10, 0);
lv_obj_set_style_text_color(timeLbl, lv_color_hex(Theme::MUTED), 0);
lv_label_set_text(timeLbl, "");
lv_obj_align(timeLbl, LV_ALIGN_TOP_RIGHT, -4, 4);
// Preview label (line 2)
lv_obj_t* prevLbl = lv_label_create(row);
lv_obj_set_style_text_font(prevLbl, smallFont, 0);
lv_obj_set_style_text_color(prevLbl, lv_color_hex(Theme::MUTED), 0);
lv_label_set_text(prevLbl, "");
lv_obj_align(prevLbl, LV_ALIGN_BOTTOM_LEFT, 14, -4);
_poolRows[i] = row;
_poolDots[i] = dot;
_poolNameLabels[i] = nameLbl;
_poolTimeLabels[i] = timeLbl;
_poolPreviewLabels[i] = prevLbl;
}
_lastConvCount = -1;
rebuildList();
}
@@ -39,6 +95,7 @@ void LvMessagesScreen::createUI(lv_obj_t* parent) {
void LvMessagesScreen::onEnter() {
_lastConvCount = -1;
_selectedIdx = 0;
_viewportStart = 0;
rebuildList();
}
@@ -51,15 +108,9 @@ void LvMessagesScreen::refreshUI() {
}
}
// Update only the selection highlight without rebuilding widgets
// Update only the selection highlight via pool sync
void LvMessagesScreen::updateSelection(int oldIdx, int newIdx) {
if (oldIdx >= 0 && oldIdx < (int)_rows.size()) {
lv_obj_set_style_bg_color(_rows[oldIdx], lv_color_hex(Theme::BG), 0);
}
if (newIdx >= 0 && newIdx < (int)_rows.size()) {
lv_obj_set_style_bg_color(_rows[newIdx], lv_color_hex(Theme::SELECTION_BG), 0);
lv_obj_scroll_to_view(_rows[newIdx], LV_ANIM_OFF);
}
syncVisibleRows();
}
void LvMessagesScreen::rebuildList() {
@@ -69,30 +120,21 @@ void LvMessagesScreen::rebuildList() {
int count = (int)convs.size();
_lastConvCount = count;
_lastUnreadTotal = _lxmf->unreadCount();
_rows.clear();
_sortedPeers.clear();
lv_obj_clean(_list);
_sortedConvs.clear();
if (count == 0) {
lv_obj_clear_flag(_lblEmpty, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_list, LV_OBJ_FLAG_HIDDEN);
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
return;
}
lv_obj_add_flag(_lblEmpty, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(_list, LV_OBJ_FLAG_HIDDEN);
// Build conversation info for sorting by most recent
struct ConvInfo {
std::string peerHex;
double lastTs = 0;
std::string preview;
bool hasUnread = false;
};
std::vector<ConvInfo> sorted;
sorted.reserve(count);
// Build sorted conversation info
_sortedConvs.reserve(count);
for (int i = 0; i < count; i++) {
ConvInfo ci;
ci.peerHex = convs[i];
@@ -102,85 +144,85 @@ void LvMessagesScreen::rebuildList() {
ci.preview = s->lastPreview;
ci.hasUnread = s->unreadCount > 0;
}
sorted.push_back(ci);
// Resolve display name
std::string peerName;
if (_am) peerName = _am->lookupName(ci.peerHex);
ci.displayName = !peerName.empty() ? peerName.substr(0, 15) : ci.peerHex.substr(0, 12);
_sortedConvs.push_back(ci);
}
std::sort(sorted.begin(), sorted.end(), [](const ConvInfo& a, const ConvInfo& b) {
std::sort(_sortedConvs.begin(), _sortedConvs.end(), [](const ConvInfo& a, const ConvInfo& b) {
return a.lastTs > b.lastTs;
});
for (auto& ci : sorted) _sortedPeers.push_back(ci.peerHex);
for (auto& ci : _sortedConvs) _sortedPeers.push_back(ci.peerHex);
if (_selectedIdx >= count) _selectedIdx = count - 1;
if (_selectedIdx < 0) _selectedIdx = 0;
const lv_font_t* nameFont = &lv_font_montserrat_14;
const lv_font_t* smallFont = &lv_font_montserrat_12;
syncVisibleRows();
}
for (int i = 0; i < (int)sorted.size(); i++) {
const auto& ci = sorted[i];
void LvMessagesScreen::syncVisibleRows() {
if (!_list) return;
int count = (int)_sortedConvs.size();
lv_obj_t* row = lv_obj_create(_list);
lv_obj_set_size(row, Theme::CONTENT_W, 38);
lv_obj_set_style_bg_color(row, lv_color_hex(
i == _selectedIdx ? Theme::SELECTION_BG : Theme::BG), 0);
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
lv_obj_set_style_border_width(row, 1, 0);
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_style_radius(row, 0, 0);
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
if (count == 0) {
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
return;
}
// Green dot for unread
if (ci.hasUnread) {
lv_obj_t* dot = lv_obj_create(row);
lv_obj_set_size(dot, 6, 6);
lv_obj_set_style_radius(dot, 3, 0);
lv_obj_set_style_bg_color(dot, lv_color_hex(Theme::PRIMARY), 0);
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(dot, 0, 0);
lv_obj_set_style_pad_all(dot, 0, 0);
lv_obj_set_pos(dot, 5, 8);
// Compute viewport centered on selection
int halfPool = ROW_POOL_SIZE / 2;
_viewportStart = _selectedIdx - halfPool;
if (_viewportStart < 0) _viewportStart = 0;
if (_viewportStart + ROW_POOL_SIZE > count) {
_viewportStart = count - ROW_POOL_SIZE;
if (_viewportStart < 0) _viewportStart = 0;
}
for (int i = 0; i < ROW_POOL_SIZE; i++) {
int convIdx = _viewportStart + i;
if (convIdx >= count) {
lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
continue;
}
// Peer name (line 1)
std::string peerName;
if (_am) peerName = _am->lookupName(ci.peerHex);
std::string displayName = !peerName.empty() ?
peerName.substr(0, 15) : ci.peerHex.substr(0, 12);
lv_obj_clear_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
const auto& ci = _sortedConvs[convIdx];
bool isSelected = (convIdx == _selectedIdx);
lv_obj_t* nameLbl = lv_label_create(row);
lv_obj_set_style_text_font(nameLbl, nameFont, 0);
lv_obj_set_style_text_color(nameLbl, lv_color_hex(Theme::PRIMARY), 0);
lv_label_set_text(nameLbl, displayName.c_str());
lv_obj_align(nameLbl, LV_ALIGN_TOP_LEFT, 14, 2);
// Selection highlight
lv_obj_set_style_bg_color(_poolRows[i], lv_color_hex(
isSelected ? Theme::SELECTION_BG : Theme::BG), 0);
// Timestamp (line 1, right)
// Unread dot
if (ci.hasUnread) {
lv_obj_clear_flag(_poolDots[i], LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(_poolDots[i], LV_OBJ_FLAG_HIDDEN);
}
// Name
lv_label_set_text(_poolNameLabels[i], ci.displayName.c_str());
// Time
if (ci.lastTs > 1700000000) {
time_t t = (time_t)ci.lastTs;
struct tm* tm = localtime(&t);
if (tm) {
char timeBuf[8];
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tm->tm_hour, tm->tm_min);
lv_obj_t* timeLbl = lv_label_create(row);
lv_obj_set_style_text_font(timeLbl, &lv_font_montserrat_10, 0);
lv_obj_set_style_text_color(timeLbl, lv_color_hex(Theme::MUTED), 0);
lv_label_set_text(timeLbl, timeBuf);
lv_obj_align(timeLbl, LV_ALIGN_TOP_RIGHT, -4, 4);
lv_label_set_text(_poolTimeLabels[i], timeBuf);
} else {
lv_label_set_text(_poolTimeLabels[i], "");
}
} else {
lv_label_set_text(_poolTimeLabels[i], "");
}
// Preview (line 2)
if (!ci.preview.empty()) {
lv_obj_t* prevLbl = lv_label_create(row);
lv_obj_set_style_text_font(prevLbl, smallFont, 0);
lv_obj_set_style_text_color(prevLbl, lv_color_hex(Theme::MUTED), 0);
lv_label_set_text(prevLbl, ci.preview.c_str());
lv_obj_align(prevLbl, LV_ALIGN_BOTTOM_LEFT, 14, -4);
}
_rows.push_back(row);
// Preview
lv_label_set_text(_poolPreviewLabels[i], ci.preview.c_str());
}
}
@@ -276,7 +318,7 @@ bool LvMessagesScreen::handleKey(const KeyEvent& event) {
if (_selectedIdx > 0) {
int prev = _selectedIdx;
_selectedIdx--;
updateSelection(prev, _selectedIdx);
syncVisibleRows();
}
return true;
}
@@ -284,7 +326,7 @@ bool LvMessagesScreen::handleKey(const KeyEvent& event) {
if (_selectedIdx < count - 1) {
int prev = _selectedIdx;
_selectedIdx++;
updateSelection(prev, _selectedIdx);
syncVisibleRows();
}
return true;
}
+20
View File
@@ -27,6 +27,7 @@ public:
private:
void rebuildList();
void syncVisibleRows();
void updateSelection(int oldIdx, int newIdx);
LXMFManager* _lxmf = nullptr;
@@ -36,6 +37,7 @@ private:
int _lastConvCount = -1;
int _lastUnreadTotal = 0;
int _selectedIdx = 0;
int _viewportStart = 0;
std::vector<std::string> _sortedPeers;
enum LongPressState { LP_NONE, LP_MENU, LP_CONFIRM_DELETE };
LongPressState _lpState = LP_NONE;
@@ -44,4 +46,22 @@ private:
lv_obj_t* _list = nullptr;
lv_obj_t* _lblEmpty = nullptr;
std::vector<lv_obj_t*> _rows;
// Object pool for conversation rows
static constexpr int ROW_POOL_SIZE = 10;
lv_obj_t* _poolRows[ROW_POOL_SIZE] = {};
lv_obj_t* _poolNameLabels[ROW_POOL_SIZE] = {};
lv_obj_t* _poolPreviewLabels[ROW_POOL_SIZE] = {};
lv_obj_t* _poolTimeLabels[ROW_POOL_SIZE] = {};
lv_obj_t* _poolDots[ROW_POOL_SIZE] = {};
// Cached sorted conversation data
struct ConvInfo {
std::string peerHex;
double lastTs = 0;
std::string preview;
std::string displayName;
bool hasUnread = false;
};
std::vector<ConvInfo> _sortedConvs;
};
+76 -42
View File
@@ -165,6 +165,39 @@ void LvSettingsScreen::buildItems() {
_items.push_back(newId);
idx++;
}
{
SettingItem devModeItem;
devModeItem.label = "Developer Mode";
devModeItem.type = SettingType::ACTION;
devModeItem.formatter = [&s](int) { return s.devMode ? String("ON") : String("OFF"); };
devModeItem.action = [this, &s]() {
if (s.devMode) {
// Already on — just turn it off
s.devMode = false;
_confirmingDevMode = false;
applyAndSave();
buildItems();
enterCategory(_categoryIdx);
if (_ui) _ui->lvStatusBar().showToast("Developer mode OFF", 1200);
return;
}
if (!_confirmingDevMode) {
_confirmingDevMode = true;
if (_ui) _ui->lvStatusBar().showToast("WARNING: KNOW YOUR LAWS! Enter=Enable", 5000);
rebuildItemList();
return;
}
// Confirmed
_confirmingDevMode = false;
s.devMode = true;
applyAndSave();
buildItems();
enterCategory(_categoryIdx);
if (_ui) _ui->lvStatusBar().showToast("Developer mode ON", 1200);
};
_items.push_back(devModeItem);
idx++;
}
_categories.push_back({"Device", devStart, idx - devStart,
[&s]() { return s.displayName.isEmpty() ? String("(unnamed)") : s.displayName; }});
@@ -202,48 +235,47 @@ void LvSettingsScreen::buildItems() {
_items.push_back(presetItem);
idx++;
}
_items.push_back({"Frequency", SettingType::ENUM_CHOICE,
[&s]() {
if (s.loraFrequency <= 868000000) return 0;
if (s.loraFrequency <= 906000000) return 1;
if (s.loraFrequency <= 915000000) return 2;
return 3;
},
[&s](int v) {
static const uint32_t freqs[] = {868000000, 906000000, 915000000, 923000000};
s.loraFrequency = freqs[constrain(v, 0, 3)];
},
nullptr, 0, 3, 1, {"868 MHz", "906 MHz", "915 MHz", "923 MHz"}});
idx++;
_items.push_back({"TX Power", SettingType::INTEGER,
[&s]() { return s.loraTxPower; }, [&s](int v) { s.loraTxPower = v; },
[](int v) { return String(v) + " dBm"; }, -9, 22, 1});
idx++;
_items.push_back({"Spread Factor", SettingType::INTEGER,
[&s]() { return s.loraSF; }, [&s](int v) { s.loraSF = v; },
[](int v) { return String("SF") + String(v); }, 5, 12, 1});
idx++;
_items.push_back({"Bandwidth", SettingType::ENUM_CHOICE,
[&s]() {
if (s.loraBW <= 62500) return 0;
if (s.loraBW <= 125000) return 1;
if (s.loraBW <= 250000) return 2;
return 3;
},
[&s](int v) {
static const uint32_t bws[] = {62500, 125000, 250000, 500000};
s.loraBW = bws[constrain(v, 0, 3)];
},
nullptr, 0, 3, 1, {"62.5k", "125k", "250k", "500k"}});
idx++;
_items.push_back({"Coding Rate", SettingType::INTEGER,
[&s]() { return s.loraCR; }, [&s](int v) { s.loraCR = v; },
[](int v) { return String("4/") + String(v); }, 5, 8, 1});
idx++;
_items.push_back({"Preamble", SettingType::INTEGER,
[&s]() { return (int)s.loraPreamble; }, [&s](int v) { s.loraPreamble = v; },
[](int v) { return String(v); }, 6, 65, 1});
idx++;
// Custom radio parameters — only visible in Developer Mode
if (s.devMode) {
_items.push_back({"Frequency", SettingType::INTEGER,
[&s]() { return (int)(s.loraFrequency / 1000); },
[&s](int v) { s.loraFrequency = (uint32_t)v * 1000; },
[](int v) -> String {
char buf[16]; snprintf(buf, sizeof(buf), "%d.%03d MHz", v / 1000, v % 1000);
return String(buf);
},
137000, 1020000, 125});
idx++;
_items.push_back({"TX Power", SettingType::INTEGER,
[&s]() { return s.loraTxPower; }, [&s](int v) { s.loraTxPower = v; },
[](int v) { return String(v) + " dBm"; }, -9, 22, 1});
idx++;
_items.push_back({"Spread Factor", SettingType::INTEGER,
[&s]() { return s.loraSF; }, [&s](int v) { s.loraSF = v; },
[](int v) { return String("SF") + String(v); }, 5, 12, 1});
idx++;
_items.push_back({"Bandwidth", SettingType::ENUM_CHOICE,
[&s]() {
if (s.loraBW <= 62500) return 0;
if (s.loraBW <= 125000) return 1;
if (s.loraBW <= 250000) return 2;
return 3;
},
[&s](int v) {
static const uint32_t bws[] = {62500, 125000, 250000, 500000};
s.loraBW = bws[constrain(v, 0, 3)];
},
nullptr, 0, 3, 1, {"62.5k", "125k", "250k", "500k"}});
idx++;
_items.push_back({"Coding Rate", SettingType::INTEGER,
[&s]() { return s.loraCR; }, [&s](int v) { s.loraCR = v; },
[](int v) { return String("4/") + String(v); }, 5, 8, 1});
idx++;
_items.push_back({"Preamble", SettingType::INTEGER,
[&s]() { return (int)s.loraPreamble; }, [&s](int v) { s.loraPreamble = v; },
[](int v) { return String(v); }, 6, 65, 1});
idx++;
}
_categories.push_back({"Radio", radioStart, idx - radioStart,
[this]() { int p = detectPreset(); return (p >= 0) ? String(LV_PRESETS[p].name) : String("Custom"); }});
@@ -590,6 +622,7 @@ void LvSettingsScreen::onEnter() {
_editing = false;
_textEditing = false;
_confirmingReset = false;
_confirmingDevMode = false;
rebuildCategoryList();
}
@@ -901,6 +934,7 @@ void LvSettingsScreen::exitToCategories() {
_editing = false;
_textEditing = false;
_confirmingReset = false;
_confirmingDevMode = false;
rebuildCategoryList();
}
+1
View File
@@ -130,6 +130,7 @@ private:
bool _textEditing = false;
String _editText;
bool _confirmingReset = false;
bool _confirmingDevMode = false;
// WiFi picker
std::vector<WiFiInterface::ScanResult> _wifiResults;