Browse Source

ui and settings fixes

pull/2568/head
Christos Themelis 1 month ago
parent
commit
0a38fd48f7
  1. 124
      examples/simple_secure_chat_ui/main.cpp
  2. 455
      examples/simple_secure_chat_ui/uiManager.cpp
  3. 18
      include/uiManager.h

124
examples/simple_secure_chat_ui/main.cpp

@ -280,57 +280,110 @@ void msgstore_append_public(uint32_t ts, const char* sender, bool sent, const ch
#endif
}
// Returns true if a DM file line is valid; fills ts, dir, text.
static bool parse_dm_line(const String& line, uint32_t& ts, char& dir, String& text) {
int p1 = line.indexOf('|');
int p2 = line.indexOf('|', p1 + 1);
if (p1 < 1 || p2 < p1 + 2) return false;
ts = (uint32_t) line.substring(0, p1).toInt();
if (ts == 0) return false;
dir = line.charAt(p1 + 1);
if (dir != '>' && dir != '<') return false;
text = line.substring(p2 + 1);
text.trim();
return text.length() > 0;
}
void msgstore_load_dm(const uint8_t* pub_key) {
#if defined(ESP32)
// LVGL heap is 48 KB, shared with all UI widgets. Each bubble needs ~4 LVGL
// objects (~600 B). Limit history to the last 20 messages to avoid OOM.
static const int MAX_DISPLAY = 20;
char path[24];
msgstore_dm_path(path, sizeof(path), pub_key);
if (!SPIFFS.exists(path)) return;
// Pass 1 — count valid lines so we know how many to skip
File f = SPIFFS.open(path, "r");
if (!f) return;
int total = 0;
while (f.available()) {
String line = f.readStringUntil('\n');
int p1 = line.indexOf('|');
int p2 = line.indexOf('|', p1 + 1);
if (p1 < 1 || p2 < p1 + 2) continue;
uint32_t ts = (uint32_t) line.substring(0, p1).toInt();
if (ts == 0) continue; // skip unsync'd entries
char dir = line.charAt(p1 + 1);
if (dir != '>' && dir != '<') continue; // skip malformed direction
String text = line.substring(p2 + 1);
text.trim();
if (text.length() == 0) continue; // skip empty messages
uint32_t ts; char dir; String text;
if (parse_dm_line(line, ts, dir, text)) total++;
}
f.close();
// Pass 2 — skip oldest, render only the last MAX_DISPLAY valid entries
f = SPIFFS.open(path, "r");
if (!f) return;
int skip = (total > MAX_DISPLAY) ? total - MAX_DISPLAY : 0;
int seen = 0;
while (f.available()) {
String line = f.readStringUntil('\n');
uint32_t ts; char dir; String text;
if (!parse_dm_line(line, ts, dir, text)) continue;
if (seen++ < skip) continue;
char time_buf[16];
format_time(ts, time_buf, sizeof(time_buf));
uiManager->addPrivateChatBubble(time_buf, text.c_str(), dir == '>');
uiManager->addPrivateChatBubble(time_buf, text.c_str(), dir == '>', false);
}
f.close();
uiManager->scrollPrivateChatToBottom();
#endif
}
void msgstore_load_public() {
#if defined(ESP32)
static const int MAX_DISPLAY = 20;
if (!SPIFFS.exists(MSGSTORE_PUBLIC_PATH)) return;
// Pass 1 — count valid lines
File f = SPIFFS.open(MSGSTORE_PUBLIC_PATH, "r");
if (!f) return;
int total = 0;
while (f.available()) {
String line = f.readStringUntil('\n');
int p1 = line.indexOf('|');
int p2 = line.indexOf('|', p1 + 1);
int p3 = line.indexOf('|', p2 + 1);
if (p1 < 1 || p2 < p1 + 2 || p3 < p2 + 2) continue;
uint32_t ts = (uint32_t) line.substring(0, p1).toInt();
if ((uint32_t)line.substring(0, p1).toInt() == 0) continue;
char dir = line.charAt(p2 + 1);
if (dir != '>' && dir != '<') continue;
String text = line.substring(p3 + 1); text.trim();
if (text.length() == 0) continue;
total++;
}
f.close();
// Pass 2 — skip oldest, render last MAX_DISPLAY
f = SPIFFS.open(MSGSTORE_PUBLIC_PATH, "r");
if (!f) return;
int skip = (total > MAX_DISPLAY) ? total - MAX_DISPLAY : 0;
int seen = 0;
while (f.available()) {
String line = f.readStringUntil('\n');
int p1 = line.indexOf('|');
int p2 = line.indexOf('|', p1 + 1);
int p3 = line.indexOf('|', p2 + 1);
if (p1 < 1 || p2 < p1 + 2 || p3 < p2 + 2) continue;
uint32_t ts = (uint32_t)line.substring(0, p1).toInt();
if (ts == 0) continue;
String sender = line.substring(p1 + 1, p2);
char dir = line.charAt(p2 + 1);
if (dir != '>' && dir != '<') continue;
String text = line.substring(p3 + 1);
text.trim();
String text = line.substring(p3 + 1); text.trim();
if (text.length() == 0) continue;
if (seen++ < skip) continue;
char time_buf[16];
format_time(ts, time_buf, sizeof(time_buf));
uiManager->addChatBubble(time_buf, sender.c_str(), text.c_str(), dir == '>');
uiManager->addChatBubble(time_buf, sender.c_str(), text.c_str(), dir == '>', false);
}
f.close();
uiManager->scrollPublicChatToBottom();
#endif
}
@ -342,7 +395,10 @@ struct NodePrefs { // persisted to file
double node_lat, node_lon;
float freq;
uint8_t tx_power_dbm;
uint8_t unused[3];
uint8_t sf; // was unused[0]
uint8_t cr; // was unused[1]
uint8_t _pad; // was unused[2]
float bw; // new field (zeros from old 60-byte files → defaults applied in begin())
};
#ifndef FIRMWARE_BUILD_DATE
@ -615,6 +671,9 @@ public:
#endif
_prefs.freq = LORA_FREQ;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.bw = LORA_BW;
_prefs.sf = LORA_SF;
_prefs.cr = LORA_CR;
command[0] = 0;
curr_recipient = NULL;
@ -624,6 +683,9 @@ public:
const char* getRole() { return FIRMWARE_ROLE; }
const char* getNodeName() { return _prefs.node_name; }
float getFreqPref() const { return _prefs.freq; }
float getBwPref() const { return _prefs.bw; }
uint8_t getSfPref() const { return _prefs.sf; }
uint8_t getCrPref() const { return _prefs.cr; }
uint8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }
void begin(FILESYSTEM& fs) {
@ -668,6 +730,10 @@ public:
file.close();
}
}
// Apply defaults for fields that were zero in old saved files
if (_prefs.bw == 0.0f) _prefs.bw = LORA_BW;
if (_prefs.sf == 0) _prefs.sf = LORA_SF;
if (_prefs.cr == 0) _prefs.cr = LORA_CR;
loadContacts();
_public = addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
@ -831,6 +897,18 @@ public:
_prefs.freq = atof(&config[5]);
savePrefs();
Serial.println(" OK - reboot to apply");
} else if (memcmp(config, "bw ", 3) == 0) {
_prefs.bw = atof(&config[3]);
savePrefs();
Serial.println(" OK - reboot to apply");
} else if (memcmp(config, "sf ", 3) == 0) {
_prefs.sf = (uint8_t)atoi(&config[3]);
savePrefs();
Serial.println(" OK - reboot to apply");
} else if (memcmp(config, "cr ", 3) == 0) {
_prefs.cr = (uint8_t)atoi(&config[3]);
savePrefs();
Serial.println(" OK - reboot to apply");
} else {
Serial.printf(" ERROR: unknown config: %s\n", config);
}
@ -1038,19 +1116,25 @@ void initializeMesh() {
#endif
Serial.println("[mesh] the_mesh.begin() done");
float freq = the_mesh.getFreqPref();
float freq = the_mesh.getFreqPref();
float bw = the_mesh.getBwPref();
uint8_t sf = the_mesh.getSfPref();
uint8_t cr = the_mesh.getCrPref();
// Always use the build-defined TX power; saved prefs may contain a stale
// value from a previous firmware version with a different default.
uint8_t txpwr = LORA_TX_POWER;
Serial.printf("[mesh] Radio params: freq=%.3f MHz BW=%.1f SF=%d CR=%d TX=%d dBm\n",
freq, (float)LORA_BW, (int)LORA_SF, (int)LORA_CR, (int)txpwr);
radio_set_params(freq, LORA_BW, LORA_SF, LORA_CR);
freq, bw, (int)sf, (int)cr, (int)txpwr);
radio_set_params(freq, bw, sf, cr);
radio_set_tx_power(txpwr);
the_mesh.showWelcome();
uiManager->populateSettings(the_mesh.getNodeName(),
the_mesh.getFreqPref(),
the_mesh.getBwPref(),
the_mesh.getSfPref(),
the_mesh.getCrPref(),
the_mesh.getTxPowerPref(),
the_mesh.getFirmwareVer(),
the_mesh.getBuildDate());
@ -1064,7 +1148,9 @@ void initializeMesh() {
the_mesh.getFreqPref());
uiManager->setMyNodeName(the_mesh.getNodeName());
uiManager->beginPublicHistoryLoad();
msgstore_load_public();
uiManager->endPublicHistoryLoad();
Serial.println("[mesh] Sending self-advert...");
the_mesh.sendSelfAdvert(1200);

455
examples/simple_secure_chat_ui/uiManager.cpp

@ -168,7 +168,7 @@ void UIManager::updateInfo(const char *str, uint32_t color) {
// lv_obj_set_style_text_color(ui_ValueLastUpdate, lv_color_hex(color), LV_PART_MAIN | LV_STATE_DEFAULT);
}
void UIManager::addChatBubble(const char *time_str, const char *sender, const char *msg,bool is_self)
void UIManager::addChatBubble(const char *time_str, const char *sender, const char *msg, bool is_self, bool do_scroll)
{
// Remove oldest
if (chat_count >= MAX_CHAT_MESSAGES) {
@ -251,10 +251,10 @@ void UIManager::addChatBubble(const char *time_str, const char *sender, const ch
chat_items[chat_count++] = row;
lv_obj_scroll_to_view(row, LV_ANIM_ON);
if (do_scroll) lv_obj_scroll_to_view(row, LV_ANIM_ON);
}
void UIManager::addPrivateChatBubble(const char *time_str, const char *msg, bool is_self) {
void UIManager::addPrivateChatBubble(const char *time_str, const char *msg, bool is_self, bool do_scroll) {
lv_obj_set_style_pad_bottom(ui_ContactMessages, 20, 0);
@ -315,7 +315,25 @@ void UIManager::addPrivateChatBubble(const char *time_str, const char *msg, bool
lv_obj_set_style_text_color(lbl_time, lv_color_hex(0x808080), 0);
lv_obj_set_style_text_font(lbl_time, &lv_font_arial_14, 0);
lv_obj_scroll_to_view(row, LV_ANIM_OFF);
if (do_scroll) lv_obj_scroll_to_view(row, LV_ANIM_OFF);
}
void UIManager::scrollPrivateChatToBottom() {
if (ui_ContactMessages)
lv_obj_scroll_to_y(ui_ContactMessages, LV_COORD_MAX, LV_ANIM_OFF);
}
void UIManager::scrollPublicChatToBottom() {
if (ui_ChannelMessages)
lv_obj_scroll_to_y(ui_ChannelMessages, LV_COORD_MAX, LV_ANIM_OFF);
}
void UIManager::beginPublicHistoryLoad() {
if (ui_ChannelMessages) lv_obj_set_layout(ui_ChannelMessages, 0);
}
void UIManager::endPublicHistoryLoad() {
if (ui_ChannelMessages) lv_obj_set_flex_flow(ui_ChannelMessages, LV_FLEX_FLOW_COLUMN);
}
void UIManager::getInitials(const char *name, char *out) {
@ -409,7 +427,15 @@ void UIManager::handleContactClick(lv_event_t *e)
snprintf(cmd, sizeof(cmd), "to %s", currentContactName);
handleCommand(cmd);
// Disable flex layout before bulk-loading history messages.
// Without this, each lv_obj_create() child triggers lv_obj_update_layout()
// which recalculates ALL children's positions → O(n²) → watchdog timeout.
// lv_obj_set_flex_flow() below re-enables flex and does ONE O(n) pass.
if (ui_ContactMessages) lv_obj_set_layout(ui_ContactMessages, 0);
msgstore_load_dm(currentContactPubKey);
if (ui_ContactMessages) lv_obj_set_flex_flow(ui_ContactMessages, LV_FLEX_FLOW_COLUMN);
}
void UIManager::addContactToUI(ContactInfo c)
@ -552,12 +578,27 @@ void UIManager::addContactToUI(ContactInfo c)
void UIManager::updateContactLastSeen(const uint8_t* pub_key, uint32_t lastmod)
{
// Called from Core 1 (mesh task). LVGL is NOT thread-safe, so we must NOT
// call any lv_* write functions here. We only update the data field (atomic
// 32-bit write on ESP32). The LVGL timer (Core 0) will refresh the label.
UIContactInfo* uic = findContactByPubKey(ui_Contacts, pub_key);
if (!uic || !uic->label_lastseen) return;
uic->info.lastmod = lastmod;
char buf[32];
formatLastSeen(lastmod, buf, sizeof(buf));
lv_label_set_text(uic->label_lastseen, buf);
if (!uic) return;
uic->info.lastmod = lastmod; // atomic on ESP32, safe without mutex
}
// Called by an LVGL timer on Core 0 — safe to call lv_* here.
void UIManager::refreshLastSeenLabels()
{
if (!ui_Contacts) return;
uint32_t cnt = lv_obj_get_child_cnt(ui_Contacts);
for (uint32_t i = 0; i < cnt; i++) {
lv_obj_t* row = lv_obj_get_child(ui_Contacts, i);
UIContactInfo* uic = (UIContactInfo*) lv_obj_get_user_data(row);
if (!uic || !uic->label_lastseen) continue;
char buf[32];
formatLastSeen(uic->info.lastmod, buf, sizeof(buf));
lv_label_set_text(uic->label_lastseen, buf);
}
}
void UIManager::onShowKeyboard()
@ -791,12 +832,36 @@ static void s_onSettingsSaveClick(lv_event_t *e)
if(self) self->onSettingsSaveClick(e);
}
static void s_onPresetChange(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->onPresetChange(lv_dropdown_get_selected(lv_event_get_target(e)));
}
void UIManager::onPresetChange(uint16_t idx)
{
if (idx == 0) { // Wide band
if (ui_SettingsFreq) lv_textarea_set_text(ui_SettingsFreq, "869.525");
if (ui_SettingsBw) lv_textarea_set_text(ui_SettingsBw, "250");
if (ui_SettingsSf) lv_textarea_set_text(ui_SettingsSf, "11");
if (ui_SettingsCr) lv_textarea_set_text(ui_SettingsCr, "5");
} else { // Narrow band
if (ui_SettingsFreq) lv_textarea_set_text(ui_SettingsFreq, "869.618");
if (ui_SettingsBw) lv_textarea_set_text(ui_SettingsBw, "62.5");
if (ui_SettingsSf) lv_textarea_set_text(ui_SettingsSf, "8");
if (ui_SettingsCr) lv_textarea_set_text(ui_SettingsCr, "8");
}
}
void UIManager::onSettingsSaveClick(lv_event_t* e)
{
char cmd[96];
const char* name = ui_SettingsName ? lv_textarea_get_text(ui_SettingsName) : "";
const char* freq = ui_SettingsFreq ? lv_textarea_get_text(ui_SettingsFreq) : "";
const char* bw = ui_SettingsBw ? lv_textarea_get_text(ui_SettingsBw) : "";
const char* sf = ui_SettingsSf ? lv_textarea_get_text(ui_SettingsSf) : "";
const char* cr = ui_SettingsCr ? lv_textarea_get_text(ui_SettingsCr) : "";
const char* tx = ui_SettingsTx ? lv_textarea_get_text(ui_SettingsTx) : "";
if (name && name[0]) {
@ -807,6 +872,18 @@ void UIManager::onSettingsSaveClick(lv_event_t* e)
snprintf(cmd, sizeof(cmd), "set freq %s", freq);
handleCommand(cmd);
}
if (bw && bw[0]) {
snprintf(cmd, sizeof(cmd), "set bw %s", bw);
handleCommand(cmd);
}
if (sf && sf[0]) {
snprintf(cmd, sizeof(cmd), "set sf %s", sf);
handleCommand(cmd);
}
if (cr && cr[0]) {
snprintf(cmd, sizeof(cmd), "set cr %s", cr);
handleCommand(cmd);
}
if (tx && tx[0]) {
snprintf(cmd, sizeof(cmd), "set tx %s", tx);
handleCommand(cmd);
@ -827,8 +904,8 @@ void UIManager::onSettingsSaveClick(lv_event_t* e)
ESP.restart();
}
void UIManager::populateSettings(const char* name, float freq, uint8_t tx_power,
const char* fw_ver, const char* build_date)
void UIManager::populateSettings(const char* name, float freq, float bw, uint8_t sf, uint8_t cr,
uint8_t tx_power, const char* fw_ver, const char* build_date)
{
if (ui_SettingsName && name) lv_textarea_set_text(ui_SettingsName, name);
@ -837,18 +914,37 @@ void UIManager::populateSettings(const char* name, float freq, uint8_t tx_power,
snprintf(buf, sizeof(buf), "%.3f", freq);
lv_textarea_set_text(ui_SettingsFreq, buf);
}
if (ui_SettingsBw) {
char buf[16];
snprintf(buf, sizeof(buf), "%.1f", bw);
lv_textarea_set_text(ui_SettingsBw, buf);
}
if (ui_SettingsSf) {
char buf[8];
snprintf(buf, sizeof(buf), "%u", (unsigned)sf);
lv_textarea_set_text(ui_SettingsSf, buf);
}
if (ui_SettingsCr) {
char buf[8];
snprintf(buf, sizeof(buf), "%u", (unsigned)cr);
lv_textarea_set_text(ui_SettingsCr, buf);
}
if (ui_SettingsTx) {
char buf[8];
snprintf(buf, sizeof(buf), "%u", (unsigned)tx_power);
lv_textarea_set_text(ui_SettingsTx, buf);
}
if (ui_SettingsFw && fw_ver && build_date) {
char buf[64];
snprintf(buf, sizeof(buf), "%s (%s)", fw_ver, build_date);
lv_label_set_text(ui_SettingsFw, buf);
}
// Select the matching preset (does not fire the VALUE_CHANGED event)
if (ui_SettingsPreset) {
bool isNarrow = (fabsf(freq - 869.618f) < 0.01f && bw < 100.0f && sf == 8 && cr == 8);
lv_dropdown_set_selected(ui_SettingsPreset, isNarrow ? 1 : 0);
}
}
void UIManager::populateHome(const char* name, const char* pub_key_hex,
@ -954,7 +1050,6 @@ void UIManager::ui_Screen1_screen_init(void)
lv_obj_clear_flag(ui_TabPageHome, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(ui_TabPageContacts, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(ui_TabPageChannels, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(ui_TabPageSettings, LV_OBJ_FLAG_SCROLLABLE);
// Node name (large)
ui_HomeNodeName = LvLabel(ui_TabPageHome)
@ -1228,101 +1323,264 @@ void UIManager::ui_Screen1_screen_init(void)
.font(&lv_font_arial_18);
lv_obj_center(iu_SendLabel);
// ---- Settings tab ----
const int FIELD_W = 380;
const int LABEL_FONT_SIZE_HACK = 0; // placeholder
// ---- Settings tab (scrollable, horizontal label+input rows) ----
lv_obj_add_flag(ui_TabPageSettings, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_scrollbar_mode(ui_TabPageSettings, LV_SCROLLBAR_MODE_AUTO);
// Device name
LvLabel(ui_TabPageSettings)
#if defined(LANG_GR)
.text("Όνομα συσκευής")
#else
.text("Device name")
#endif
.font(&lv_font_arial_20)
.textColor(0xCCCCCC)
.align(LV_ALIGN_CENTER)
.position(0, -200);
// Layout constants: label on left, input on right, same row Y
// Label: width=175, center at x=-115 from tab center → left=40, right=212
// Input: width=240, center at x=100 from tab center → left=220, right=460
const lv_coord_t LBL_X = -115;
const lv_coord_t LBL_W = 175;
const lv_coord_t INP_X = 100;
const lv_coord_t INP_W = 240;
const lv_coord_t INP_H = 40;
ui_SettingsName = LvTextArea(ui_TabPageSettings)
.size(FIELD_W, 40)
.align(LV_ALIGN_CENTER)
.position(0, -170)
.oneLine(true)
.font(&lv_font_arial_20)
.bgColor(0x111111)
.textColor(0xFFFFFF)
.borderColor(0x444444)
.borderWidth(1)
.radius(6)
.onFocus(s_onSettingsInputFocus, this)
.raw();
lv_coord_t rowY = -195;
// Frequency
LvLabel(ui_TabPageSettings)
#if defined(LANG_GR)
.text("Συχνότητα LoRa (MHz)")
#else
.text("LoRa frequency (MHz)")
#endif
.font(&lv_font_arial_20)
.textColor(0xCCCCCC)
.align(LV_ALIGN_CENTER)
.position(0, -120);
// ---- Row 1: Device name ----
{
lv_obj_t* lbl = LvLabel(ui_TabPageSettings)
#if defined(LANG_GR)
.text("Όνομα")
#else
.text("Device name")
#endif
.font(&lv_font_arial_20)
.textColor(0xCCCCCC)
.width(LBL_W)
.align(LV_ALIGN_CENTER)
.position(LBL_X, rowY)
.raw();
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_RIGHT, 0);
ui_SettingsName = LvTextArea(ui_TabPageSettings)
.size(INP_W, INP_H)
.align(LV_ALIGN_CENTER)
.position(INP_X, rowY)
.oneLine(true)
.font(&lv_font_arial_20)
.bgColor(0x111111)
.textColor(0xFFFFFF)
.borderColor(0x444444)
.borderWidth(1)
.radius(6)
.onFocus(s_onSettingsInputFocus, this)
.raw();
}
rowY += 50;
ui_SettingsFreq = LvTextArea(ui_TabPageSettings)
.size(FIELD_W, 40)
.align(LV_ALIGN_CENTER)
.position(0, -90)
.oneLine(true)
.font(&lv_font_arial_20)
.bgColor(0x111111)
.textColor(0xFFFFFF)
.borderColor(0x444444)
.borderWidth(1)
.radius(6)
.onFocus(s_onSettingsInputFocus, this)
.raw();
// ---- Row 2: Preset dropdown ----
{
lv_obj_t* lbl = LvLabel(ui_TabPageSettings)
.text("Preset")
.font(&lv_font_arial_20)
.textColor(0xCCCCCC)
.width(LBL_W)
.align(LV_ALIGN_CENTER)
.position(LBL_X, rowY)
.raw();
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_RIGHT, 0);
ui_SettingsPreset = LvDropdown(ui_TabPageSettings)
.options("Wide band\nNarrow band")
.width(INP_W)
.align(LV_ALIGN_CENTER)
.position(INP_X, rowY)
.raw();
lv_obj_set_height(ui_SettingsPreset, INP_H);
lv_obj_set_style_bg_color(ui_SettingsPreset, lv_color_hex(0x111111), LV_PART_MAIN);
lv_obj_set_style_text_color(ui_SettingsPreset, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_style_border_color(ui_SettingsPreset, lv_color_hex(0x444444), LV_PART_MAIN);
lv_obj_set_style_border_width(ui_SettingsPreset, 1, LV_PART_MAIN);
lv_obj_set_style_text_font(ui_SettingsPreset, &lv_font_arial_20, LV_PART_MAIN);
lv_obj_t* ddList = lv_dropdown_get_list(ui_SettingsPreset);
if (ddList) {
lv_obj_set_style_bg_color(ddList, lv_color_hex(0x222222), 0);
lv_obj_set_style_text_color(ddList, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_text_font(ddList, &lv_font_arial_20, 0);
lv_obj_set_style_border_color(ddList, lv_color_hex(0x444444), 0);
}
lv_obj_add_event_cb(ui_SettingsPreset, s_onPresetChange, LV_EVENT_VALUE_CHANGED, this);
}
rowY += 50;
// TX power
LvLabel(ui_TabPageSettings)
#if defined(LANG_GR)
.text("Ισχύς εκπομπής (dBm)")
#else
.text("TX power (dBm)")
#endif
.font(&lv_font_arial_20)
.textColor(0xCCCCCC)
.align(LV_ALIGN_CENTER)
.position(0, -40);
// ---- Row 3: Frequency ----
{
lv_obj_t* lbl = LvLabel(ui_TabPageSettings)
#if defined(LANG_GR)
.text("Συχνότητα (MHz)")
#else
.text("Frequency (MHz)")
#endif
.font(&lv_font_arial_20)
.textColor(0xCCCCCC)
.width(LBL_W)
.align(LV_ALIGN_CENTER)
.position(LBL_X, rowY)
.raw();
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_RIGHT, 0);
ui_SettingsFreq = LvTextArea(ui_TabPageSettings)
.size(INP_W, INP_H)
.align(LV_ALIGN_CENTER)
.position(INP_X, rowY)
.oneLine(true)
.font(&lv_font_arial_20)
.bgColor(0x111111)
.textColor(0xFFFFFF)
.borderColor(0x444444)
.borderWidth(1)
.radius(6)
.onFocus(s_onSettingsInputFocus, this)
.raw();
}
rowY += 50;
ui_SettingsTx = LvTextArea(ui_TabPageSettings)
.size(FIELD_W, 40)
.align(LV_ALIGN_CENTER)
.position(0, -10)
.oneLine(true)
.font(&lv_font_arial_20)
.bgColor(0x111111)
.textColor(0xFFFFFF)
.borderColor(0x444444)
.borderWidth(1)
.radius(6)
.onFocus(s_onSettingsInputFocus, this)
.raw();
// ---- Row 4: Bandwidth ----
{
lv_obj_t* lbl = LvLabel(ui_TabPageSettings)
#if defined(LANG_GR)
.text("Bandwidth (kHz)")
#else
.text("Bandwidth (kHz)")
#endif
.font(&lv_font_arial_20)
.textColor(0xCCCCCC)
.width(LBL_W)
.align(LV_ALIGN_CENTER)
.position(LBL_X, rowY)
.raw();
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_RIGHT, 0);
ui_SettingsBw = LvTextArea(ui_TabPageSettings)
.size(INP_W, INP_H)
.align(LV_ALIGN_CENTER)
.position(INP_X, rowY)
.oneLine(true)
.font(&lv_font_arial_20)
.bgColor(0x111111)
.textColor(0xFFFFFF)
.borderColor(0x444444)
.borderWidth(1)
.radius(6)
.onFocus(s_onSettingsInputFocus, this)
.raw();
}
rowY += 50;
// ---- Row 5: Spreading Factor ----
{
lv_obj_t* lbl = LvLabel(ui_TabPageSettings)
#if defined(LANG_GR)
.text("Spreading Factor")
#else
.text("Spreading Factor")
#endif
.font(&lv_font_arial_20)
.textColor(0xCCCCCC)
.width(LBL_W)
.align(LV_ALIGN_CENTER)
.position(LBL_X, rowY)
.raw();
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_RIGHT, 0);
ui_SettingsSf = LvTextArea(ui_TabPageSettings)
.size(INP_W, INP_H)
.align(LV_ALIGN_CENTER)
.position(INP_X, rowY)
.oneLine(true)
.font(&lv_font_arial_20)
.bgColor(0x111111)
.textColor(0xFFFFFF)
.borderColor(0x444444)
.borderWidth(1)
.radius(6)
.onFocus(s_onSettingsInputFocus, this)
.raw();
}
rowY += 50;
// ---- Row 6: Coding Rate ----
{
lv_obj_t* lbl = LvLabel(ui_TabPageSettings)
#if defined(LANG_GR)
.text("Coding Rate")
#else
.text("Coding Rate")
#endif
.font(&lv_font_arial_20)
.textColor(0xCCCCCC)
.width(LBL_W)
.align(LV_ALIGN_CENTER)
.position(LBL_X, rowY)
.raw();
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_RIGHT, 0);
ui_SettingsCr = LvTextArea(ui_TabPageSettings)
.size(INP_W, INP_H)
.align(LV_ALIGN_CENTER)
.position(INP_X, rowY)
.oneLine(true)
.font(&lv_font_arial_20)
.bgColor(0x111111)
.textColor(0xFFFFFF)
.borderColor(0x444444)
.borderWidth(1)
.radius(6)
.onFocus(s_onSettingsInputFocus, this)
.raw();
}
rowY += 50;
// Firmware (read-only)
// ---- Row 7: TX power ----
{
lv_obj_t* lbl = LvLabel(ui_TabPageSettings)
#if defined(LANG_GR)
.text("Ισχύς (dBm)")
#else
.text("TX power (dBm)")
#endif
.font(&lv_font_arial_20)
.textColor(0xCCCCCC)
.width(LBL_W)
.align(LV_ALIGN_CENTER)
.position(LBL_X, rowY)
.raw();
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_RIGHT, 0);
ui_SettingsTx = LvTextArea(ui_TabPageSettings)
.size(INP_W, INP_H)
.align(LV_ALIGN_CENTER)
.position(INP_X, rowY)
.oneLine(true)
.font(&lv_font_arial_20)
.bgColor(0x111111)
.textColor(0xFFFFFF)
.borderColor(0x444444)
.borderWidth(1)
.radius(6)
.onFocus(s_onSettingsInputFocus, this)
.raw();
}
rowY += 43;
// Firmware (read-only, centered)
ui_SettingsFw = LvLabel(ui_TabPageSettings)
.text("...")
.font(&lv_font_arial_18)
.textColor(0x888888)
.align(LV_ALIGN_CENTER)
.position(0, 40);
.position(0, rowY);
rowY += 42;
// Save button
ui_SettingsSaveBtn = LvButton(ui_TabPageSettings)
.size(220, 50)
.size(270, 50)
.align(LV_ALIGN_CENTER)
.position(0, 90)
.position(0, rowY)
.bgColor(0x2E7D32)
.onClick(s_onSettingsSaveClick, this)
.raw();
@ -1337,6 +1595,7 @@ void UIManager::ui_Screen1_screen_init(void)
.textColor(0xFFFFFF)
.raw();
lv_obj_center(saveLabel);
rowY += 56;
// Status label
ui_SettingsStatus = LvLabel(ui_TabPageSettings)
@ -1344,13 +1603,21 @@ void UIManager::ui_Screen1_screen_init(void)
.font(&lv_font_arial_18)
.textColor(0xFFC107)
.align(LV_ALIGN_CENTER)
.position(0, 145);
.position(0, rowY);
ui_Keyboard = LvKeyboard(lv_layer_top())
.size(480, 200)
.align(LV_ALIGN_BOTTOM_MID)
.show(false)
.onEvent(s_onKeyboardEvent, this);
// Refresh last-seen labels from Core 0 (LVGL task) every 30 s.
// updateContactLastSeen() only writes the data (Core 1, atomic), this
// timer does the actual lv_label_set_text() safely on Core 0.
lv_timer_create([](lv_timer_t* t) {
UIManager* self = (UIManager*) t->user_data;
self->refreshLastSeenLabels();
}, 30000, this);
}
void UIManager::setNightMode(bool night) {

18
include/uiManager.h

@ -84,7 +84,11 @@ class UIManager {
lv_obj_t* ui_AdvertiseBtn = nullptr;
lv_obj_t* ui_SettingsName = nullptr;
lv_obj_t* ui_SettingsPreset = nullptr;
lv_obj_t* ui_SettingsFreq = nullptr;
lv_obj_t* ui_SettingsBw = nullptr;
lv_obj_t* ui_SettingsSf = nullptr;
lv_obj_t* ui_SettingsCr = nullptr;
lv_obj_t* ui_SettingsTx = nullptr;
lv_obj_t* ui_SettingsFw = nullptr;
lv_obj_t* ui_SettingsSaveBtn = nullptr;
@ -109,8 +113,9 @@ class UIManager {
void onKeyboardEvent(lv_event_t* e);
void scroll_begin_event(lv_event_t* e);
void populateSettings(const char* name, float freq, uint8_t tx_power,
const char* fw_ver, const char* build_date);
void onPresetChange(uint16_t idx);
void populateSettings(const char* name, float freq, float bw, uint8_t sf, uint8_t cr,
uint8_t tx_power, const char* fw_ver, const char* build_date);
void populateHome(const char* name, const char* pub_key_hex,
int contact_count, float freq);
void setMyNodeName(const char* name);
@ -124,10 +129,15 @@ class UIManager {
void updateInfo(const char *str, uint32_t color);
void clearDateTime();
void updateValues();
void addPrivateChatBubble(const char *time_str, const char *msg, bool is_self);
void addChatBubble(const char *time_str, const char *sender, const char *msg,bool is_self);
void addPrivateChatBubble(const char *time_str, const char *msg, bool is_self, bool do_scroll = true);
void addChatBubble(const char *time_str, const char *sender, const char *msg, bool is_self, bool do_scroll = true);
void scrollPrivateChatToBottom();
void scrollPublicChatToBottom();
void beginPublicHistoryLoad(); // disable flex before bulk load
void endPublicHistoryLoad(); // re-enable flex after bulk load
void addContactToUI(ContactInfo c);
void updateContactLastSeen(const uint8_t* pub_key, uint32_t lastmod);
void refreshLastSeenLabels();
void handleContactClick(lv_event_t *e);
void setNightMode(bool night);
};

Loading…
Cancel
Save