Browse Source

UI updates

pull/2568/head
Christos Themelis 1 month ago
parent
commit
86862860f5
  1. 1
      .gitignore
  2. 121
      examples/simple_secure_chat_ui/main.cpp
  3. 15
      examples/simple_secure_chat_ui/messageStore.h
  4. 581
      examples/simple_secure_chat_ui/uiManager.cpp
  5. 44
      include/uiManager.h
  6. 4
      variants/elecrow_espnow/platformio.ini

1
.gitignore

@ -16,3 +16,4 @@ cmake-*
compile_commands.json
.venv/
venv/
examples/simple_secure_chat_ui/UI_PLAN.md

121
examples/simple_secure_chat_ui/main.cpp

@ -21,13 +21,13 @@
#define FIRMWARE_VER_TEXT "v2 (build: 4 Feb 2025)"
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#define LORA_FREQ 869.525
#endif
#ifndef LORA_BW
#define LORA_BW 250
#endif
#ifndef LORA_SF
#define LORA_SF 10
#define LORA_SF 11
#endif
#ifndef LORA_CR
#define LORA_CR 5
@ -59,6 +59,7 @@
#include "../include/uiManager.h"
#include "../include/uiTasks.h"
#include "uiTouch.h"
#include "messageStore.h"
#define TAG "main"
@ -232,6 +233,95 @@ static uint32_t _atoi(const char* sp) {
/* -------------------------------------------------------------------------------------- */
static void msgstore_dm_path(char* out, size_t len, const uint8_t* pub_key) {
snprintf(out, len, "/m_%02x%02x%02x%02x.log",
pub_key[0], pub_key[1], pub_key[2], pub_key[3]);
}
static const char* MSGSTORE_PUBLIC_PATH = "/m_public.log";
static void msgstore_rotate_if_needed(const char* path) {
#if defined(ESP32)
if (!SPIFFS.exists(path)) return;
File f = SPIFFS.open(path, "r");
if (!f) return;
size_t sz = f.size();
f.close();
if (sz > MSGSTORE_MAX_BYTES) SPIFFS.remove(path);
#endif
}
void msgstore_append_dm(const uint8_t* pub_key, uint32_t ts, bool sent, const char* text) {
#if defined(ESP32)
char path[24];
msgstore_dm_path(path, sizeof(path), pub_key);
msgstore_rotate_if_needed(path);
File f = SPIFFS.open(path, FILE_APPEND);
if (!f) return;
f.printf("%u|%c|%s\n", (unsigned)ts, sent ? '>' : '<', text ? text : "");
f.close();
#endif
}
void msgstore_append_public(uint32_t ts, const char* sender, bool sent, const char* text) {
#if defined(ESP32)
msgstore_rotate_if_needed(MSGSTORE_PUBLIC_PATH);
File f = SPIFFS.open(MSGSTORE_PUBLIC_PATH, FILE_APPEND);
if (!f) return;
f.printf("%u|%s|%c|%s\n", (unsigned)ts,
sender ? sender : "?", sent ? '>' : '<', text ? text : "");
f.close();
#endif
}
void msgstore_load_dm(const uint8_t* pub_key) {
#if defined(ESP32)
char path[24];
msgstore_dm_path(path, sizeof(path), pub_key);
if (!SPIFFS.exists(path)) return;
File f = SPIFFS.open(path, "r");
if (!f) return;
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();
char dir = line.charAt(p1 + 1);
String text = line.substring(p2 + 1);
char time_buf[16];
format_time(ts, time_buf, sizeof(time_buf));
uiManager->addPrivateChatBubble(time_buf, text.c_str(), dir == '>');
}
f.close();
#endif
}
void msgstore_load_public() {
#if defined(ESP32)
if (!SPIFFS.exists(MSGSTORE_PUBLIC_PATH)) return;
File f = SPIFFS.open(MSGSTORE_PUBLIC_PATH, "r");
if (!f) return;
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();
String sender = line.substring(p1 + 1, p2);
char dir = line.charAt(p2 + 1);
String text = line.substring(p3 + 1);
char time_buf[16];
format_time(ts, time_buf, sizeof(time_buf));
uiManager->addChatBubble(time_buf, sender.c_str(), text.c_str(), dir == '>');
}
f.close();
#endif
}
/* -------------------------------------------------------------------------------------- */
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
@ -405,7 +495,8 @@ protected:
Serial.printf(" Got ACK! (round trip: %d millis)\n", _ms->getMillis() - last_msg_sent);
// NOTE: the same ACK can be received multiple times!
expected_ack_crc = 0; // reset our expected hash, now that we have received ACK
return NULL; // TODO: really should return ContactInfo pointer
if (uiManager) uiManager->setSendStatus(1);
return NULL; // TODO: really should return ContactInfo pointer
}
//uint32_t crc;
@ -425,9 +516,8 @@ protected:
char time_buf[16];
format_time(sender_timestamp, time_buf, sizeof(time_buf));
bool is_self = (strcmp(from.name, _prefs.node_name) == 0);
uiManager->addPrivateChatBubble(time_buf, text, is_self);
uiManager->routeIncomingDM(from.id.pub_key, from.name, time_buf, text);
msgstore_append_dm(from.id.pub_key, sender_timestamp, false, text);
}
void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override {
@ -463,6 +553,7 @@ protected:
bool is_self = (strcmp(sender, _prefs.node_name) == 0);
uiManager->addChatBubble(time_buf, sender, msg, is_self);
msgstore_append_public(timestamp, sender, is_self, msg);
}
uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) override {
@ -486,6 +577,7 @@ protected:
void onSendTimeout() override {
Serial.println(" ERROR: timed out, no ACK.");
if (uiManager) uiManager->setSendStatus(2);
}
public:
@ -875,6 +967,23 @@ void initializeMesh() {
the_mesh.showWelcome();
uiManager->populateSettings(the_mesh.getNodeName(),
the_mesh.getFreqPref(),
the_mesh.getTxPowerPref(),
the_mesh.getFirmwareVer(),
the_mesh.getBuildDate());
char pk_hex[17];
mesh::Utils::toHex(pk_hex, the_mesh.self_id.pub_key, 8);
pk_hex[16] = 0;
uiManager->populateHome(the_mesh.getNodeName(),
pk_hex,
the_mesh.getNumContacts(),
the_mesh.getFreqPref());
uiManager->setMyNodeName(the_mesh.getNodeName());
msgstore_load_public();
Serial.println("[mesh] Sending self-advert...");
the_mesh.sendSelfAdvert(1200);
Serial.println("[mesh] Advert queued (1200ms delay)");

15
examples/simple_secure_chat_ui/messageStore.h

@ -0,0 +1,15 @@
#pragma once
#include <stdint.h>
// Append-only per-contact / public chat persistence on SPIFFS.
// Each DM contact gets a file keyed by the first 4 bytes of the public key.
// When a file exceeds MSGSTORE_MAX_BYTES it is dropped (oldest history lost).
#define MSGSTORE_MAX_BYTES 16384
void msgstore_append_dm(const uint8_t* pub_key, uint32_t ts, bool sent, const char* text);
void msgstore_append_public(uint32_t ts, const char* sender, bool sent, const char* text);
// Replays stored history into the UI (calls uiManager->addPrivateChatBubble / addChatBubble).
void msgstore_load_dm(const uint8_t* pub_key);
void msgstore_load_public();

581
examples/simple_secure_chat_ui/uiManager.cpp

@ -5,6 +5,7 @@
#include "uiVars.h"
#include "uiManager.h"
#include "messageStore.h"
#include "../src/fonts/fonts.h"
@ -23,7 +24,39 @@ const char *UIManager::months[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
#define TAG "UIManager"
//extern void handleCommand(char *msg);
extern void handleCommand(char *msg);
namespace {
struct UIContactInfo {
ContactInfo info;
lv_obj_t* badge = nullptr;
lv_obj_t* badge_label = nullptr;
uint32_t unread = 0;
};
UIContactInfo* findContactByPubKey(lv_obj_t* list, const uint8_t* pub_key) {
if (!list) return nullptr;
uint32_t cnt = lv_obj_get_child_cnt(list);
for (uint32_t i = 0; i < cnt; i++) {
lv_obj_t* row = lv_obj_get_child(list, i);
UIContactInfo* uic = (UIContactInfo*) lv_obj_get_user_data(row);
if (uic && memcmp(uic->info.id.pub_key, pub_key, 32) == 0) return uic;
}
return nullptr;
}
void updateBadge(UIContactInfo* uic) {
if (!uic || !uic->badge || !uic->badge_label) return;
if (uic->unread == 0) {
lv_obj_add_flag(uic->badge, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_clear_flag(uic->badge, LV_OBJ_FLAG_HIDDEN);
char buf[8];
snprintf(buf, sizeof(buf), "%u", (unsigned)uic->unread);
lv_label_set_text(uic->badge_label, buf);
}
}
} // namespace
UIManager::UIManager() {
@ -351,9 +384,35 @@ static void onContactClick(lv_event_t *e)
void UIManager::handleContactClick(lv_event_t *e)
{
lv_obj_t *row = lv_event_get_target(e);
ContactInfo *c = (ContactInfo*) lv_obj_get_user_data(row);
UIContactInfo *uic = (UIContactInfo*) lv_obj_get_user_data(row);
if (!uic) return;
ContactInfo* c = &uic->info;
Serial.printf("Clicked: %s\n", c->name);
strncpy(currentContactName, c->name, sizeof(currentContactName) - 1);
currentContactName[sizeof(currentContactName) - 1] = 0;
memcpy(currentContactPubKey, c->id.pub_key, sizeof(currentContactPubKey));
hasCurrentContact = true;
if (ui_ContactName) {
lv_label_set_text(ui_ContactName, currentContactName);
}
if (ui_ContactMessages) {
lv_obj_clean(ui_ContactMessages);
}
uic->unread = 0;
updateBadge(uic);
setSendStatus(-1);
char cmd[64];
snprintf(cmd, sizeof(cmd), "to %s", currentContactName);
handleCommand(cmd);
msgstore_load_dm(currentContactPubKey);
}
void UIManager::addContactToUI(ContactInfo c)
@ -382,8 +441,9 @@ void UIManager::addContactToUI(ContactInfo c)
lv_obj_set_style_border_color(btn, lv_color_hex(0x222222), 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(0x111111), LV_STATE_PRESSED);
// Store ContactInfo
auto* store = new ContactInfo(c);
// Store wrapper (ContactInfo + badge state)
auto* store = new UIContactInfo();
store->info = c;
lv_obj_set_user_data(btn, store);
lv_obj_add_event_cb(btn, onContactClick, LV_EVENT_CLICKED, this);
@ -459,6 +519,31 @@ void UIManager::addContactToUI(ContactInfo c)
.textColor(0x888888)
.wrap(false);
// ============================
// Unread badge (top-right corner of row)
// ============================
lv_obj_t* badge = LvObj(btn)
.size(22, 22)
.position(ROW_W - 26, 4)
.radius(LV_RADIUS_CIRCLE)
.bgColor(0xE53935)
.border(0)
.scrollable(false)
.raw();
lv_obj_add_flag(badge, LV_OBJ_FLAG_HIDDEN);
LvObj(badge, true).clickable(false);
lv_obj_t* badge_label = LvLabel(badge)
.text("0")
.font(&lv_font_arial_14)
.textColor(0xFFFFFF)
.align(LV_ALIGN_CENTER)
.raw();
LvObj(badge_label, true).clickable(false);
store->badge = badge;
store->badge_label = badge_label;
// ============================
// Disable child clicks
// ============================
@ -470,16 +555,16 @@ void UIManager::onShowKeyboard()
{
LvKeyboard(ui_Keyboard, true).show(true);
LvObj(ui_DimOverlay, true).clickable(true);
LvObj(ui_ChannelInput, true).positionY(channelInputBaseKeybOnY);
LvObj(ui_SendBtn, true).positionY(channelInputBaseKeybOnY);
if (activeInput) LvObj(activeInput, true).positionY(channelInputBaseKeybOnY);
if (activeSendBtn) LvObj(activeSendBtn, true).positionY(channelInputBaseKeybOnY);
}
void UIManager::onHideKeyboard()
{
LvKeyboard(ui_Keyboard, true).show(false);
LvObj(ui_DimOverlay, true).clickable(false);
LvObj(ui_ChannelInput, true).positionY(channelInputBaseY);
LvObj(ui_SendBtn, true).positionY(channelInputBaseY);
if (activeInput) LvObj(activeInput, true).positionY(activeInputBaseY);
if (activeSendBtn) LvObj(activeSendBtn, true).positionY(activeInputBaseY);
}
static void s_onRestartClick(lv_event_t *e)
@ -487,6 +572,18 @@ static void s_onRestartClick(lv_event_t *e)
ESP.restart();
}
static void s_onAdvertiseClick(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->onAdvertiseClick(e);
}
void UIManager::onAdvertiseClick(lv_event_t* e)
{
char cmd[16] = "advert";
handleCommand(cmd);
}
static void s_onChannelInputFocus(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
@ -498,10 +595,73 @@ void UIManager::onChannelInputFocus(lv_event_t* e)
lv_obj_t* ta = lv_event_get_target(e);
if(!ui_Keyboard || !ta) return;
activeInput = ui_ChannelInput;
activeSendBtn = ui_SendBtn;
activeInputBaseY = channelInputBaseY;
lv_keyboard_set_textarea(ui_Keyboard, ta);
onShowKeyboard();
}
static void s_onContactInputFocus(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->onContactInputFocus(e);
}
void UIManager::onContactInputFocus(lv_event_t* e)
{
lv_obj_t* ta = lv_event_get_target(e);
if(!ui_Keyboard || !ta) return;
activeInput = ui_ContactInput;
activeSendBtn = ui_ContactSendBtn;
activeInputBaseY = channelInputBaseY;
lv_keyboard_set_textarea(ui_Keyboard, ta);
onShowKeyboard();
}
static void s_onContactSendClick(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->onContactSendClick(e);
}
void UIManager::onContactSendClick(lv_event_t* e)
{
if (!hasCurrentContact) {
Serial.println("[ui] no contact selected — DM ignored");
return;
}
const char* msg = lv_textarea_get_text(ui_ContactInput);
if (msg == nullptr || msg[0] == '\0') return;
char msgCopy[200];
strncpy(msgCopy, msg, sizeof(msgCopy) - 1);
msgCopy[sizeof(msgCopy) - 1] = 0;
lv_textarea_set_text(ui_ContactInput, "");
char cmd[256];
snprintf(cmd, sizeof(cmd), "send %s", msgCopy);
handleCommand(cmd);
char time_buf[16];
time_t now = time(NULL);
struct tm t;
localtime_r(&now, &t);
snprintf(time_buf, sizeof(time_buf), "%02d:%02d", t.tm_hour, t.tm_min);
addPrivateChatBubble(time_buf, msgCopy, true);
msgstore_append_dm(currentContactPubKey, (uint32_t)now, true, msgCopy);
setSendStatus(0);
onHideKeyboard();
}
static void s_onSendClick(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
@ -522,7 +682,7 @@ void UIManager::onSendClick(lv_event_t* e)
lv_textarea_set_text(ui_ChannelInput, "");
snprintf(fullMessage, sizeof(fullMessage), "public %s", msgCopy);
//handleCommand(fullMessage);
handleCommand(fullMessage);
char time_buf[16];
time_t now = time(NULL);
@ -530,11 +690,175 @@ void UIManager::onSendClick(lv_event_t* e)
localtime_r(&now, &t);
sprintf(time_buf, "%02d:%02d:%02d\n", t.tm_hour, t.tm_min, t.tm_sec);
addChatBubble(time_buf, "Me", msgCopy, true);
const char* sender = (myNodeName[0] != 0) ? myNodeName : "Me";
addChatBubble(time_buf, sender, msgCopy, true);
msgstore_append_public((uint32_t)now, sender, true, msgCopy);
onHideKeyboard();
}
void UIManager::setMyNodeName(const char* name) {
if (!name) { myNodeName[0] = 0; return; }
strncpy(myNodeName, name, sizeof(myNodeName) - 1);
myNodeName[sizeof(myNodeName) - 1] = 0;
}
void UIManager::routeIncomingDM(const uint8_t* from_pub_key, const char* from_name,
const char* time_str, const char* text)
{
if (hasCurrentContact && memcmp(from_pub_key, currentContactPubKey, 32) == 0) {
addPrivateChatBubble(time_str, text, false);
return;
}
UIContactInfo* uic = findContactByPubKey(ui_Contacts, from_pub_key);
if (uic) {
uic->unread++;
updateBadge(uic);
}
}
void UIManager::setSendStatus(int state)
{
if (!ui_ContactStatus) return;
switch (state) {
case 0:
lv_label_set_text(ui_ContactStatus,
#if defined(LANG_GR)
"Αποστολή...");
#else
"Sending...");
#endif
lv_obj_set_style_text_color(ui_ContactStatus, lv_color_hex(0xFFC107), 0);
break;
case 1:
lv_label_set_text(ui_ContactStatus, LV_SYMBOL_OK
#if defined(LANG_GR)
" Παραδόθηκε");
#else
" Delivered");
#endif
lv_obj_set_style_text_color(ui_ContactStatus, lv_color_hex(0x4CAF50), 0);
break;
case 2:
lv_label_set_text(ui_ContactStatus, LV_SYMBOL_CLOSE
#if defined(LANG_GR)
" Δεν έγινε ack");
#else
" No ack");
#endif
lv_obj_set_style_text_color(ui_ContactStatus, lv_color_hex(0xE53935), 0);
break;
default:
lv_label_set_text(ui_ContactStatus, "");
break;
}
}
static void s_onSettingsInputFocus(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->onSettingsInputFocus(e);
}
void UIManager::onSettingsInputFocus(lv_event_t* e)
{
lv_obj_t* ta = lv_event_get_target(e);
if(!ui_Keyboard || !ta) return;
// Settings inputs do not slide up — keyboard floats over the form.
activeInput = nullptr;
activeSendBtn = nullptr;
lv_keyboard_set_textarea(ui_Keyboard, ta);
LvKeyboard(ui_Keyboard, true).show(true);
LvObj(ui_DimOverlay, true).clickable(true);
}
static void s_onSettingsSaveClick(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->onSettingsSaveClick(e);
}
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* tx = ui_SettingsTx ? lv_textarea_get_text(ui_SettingsTx) : "";
if (name && name[0]) {
snprintf(cmd, sizeof(cmd), "set name %s", name);
handleCommand(cmd);
}
if (freq && freq[0]) {
snprintf(cmd, sizeof(cmd), "set freq %s", freq);
handleCommand(cmd);
}
if (tx && tx[0]) {
snprintf(cmd, sizeof(cmd), "set tx %s", tx);
handleCommand(cmd);
}
if (ui_SettingsStatus) {
#if defined(LANG_GR)
lv_label_set_text(ui_SettingsStatus, "Αποθηκεύτηκε. Επανεκκίνηση...");
#else
lv_label_set_text(ui_SettingsStatus, "Saved. Restarting...");
#endif
}
onHideKeyboard();
Serial.flush();
delay(800);
ESP.restart();
}
void UIManager::populateSettings(const char* name, float freq, uint8_t tx_power,
const char* fw_ver, const char* build_date)
{
if (ui_SettingsName && name) lv_textarea_set_text(ui_SettingsName, name);
if (ui_SettingsFreq) {
char buf[16];
snprintf(buf, sizeof(buf), "%.3f", freq);
lv_textarea_set_text(ui_SettingsFreq, 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);
}
}
void UIManager::populateHome(const char* name, const char* pub_key_hex,
int contact_count, float freq)
{
if (ui_HomeNodeName && name) lv_label_set_text(ui_HomeNodeName, name);
if (ui_HomePubKey && pub_key_hex) lv_label_set_text(ui_HomePubKey, pub_key_hex);
if (ui_HomeInfo) {
char buf[64];
#if defined(LANG_GR)
snprintf(buf, sizeof(buf), "%.3f MHz · %d επαφές", freq, contact_count);
#else
snprintf(buf, sizeof(buf), "%.3f MHz · %d contacts", freq, contact_count);
#endif
lv_label_set_text(ui_HomeInfo, buf);
}
}
static void s_onKeyboardEvent(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
@ -549,8 +873,8 @@ void UIManager::onKeyboardEvent(lv_event_t* e)
{
LvKeyboard(ui_Keyboard, true).show(false);
LvObj(ui_ChannelInput, true).positionY(channelInputBaseY);
LvObj(ui_SendBtn, true).positionY(channelInputBaseY);
if (activeInput) LvObj(activeInput, true).positionY(activeInputBaseY);
if (activeSendBtn) LvObj(activeSendBtn, true).positionY(activeInputBaseY);
LvObj(ui_DimOverlay, true)
.bgOpa(0)
@ -623,6 +947,36 @@ void UIManager::ui_Screen1_screen_init(void)
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)
.text("...")
.width(LV_SIZE_CONTENT)
.height(LV_SIZE_CONTENT)
.font(&lv_font_arial_26)
.textColor(0xFFFFFF)
.align(LV_ALIGN_CENTER)
.position(0, -200);
// Public key (short hex)
ui_HomePubKey = LvLabel(ui_TabPageHome)
.text("")
.width(LV_SIZE_CONTENT)
.height(LV_SIZE_CONTENT)
.font(&lv_font_arial_18)
.textColor(0x888888)
.align(LV_ALIGN_CENTER)
.position(0, -170);
// Freq + contacts count
ui_HomeInfo = LvLabel(ui_TabPageHome)
.text("")
.width(LV_SIZE_CONTENT)
.height(LV_SIZE_CONTENT)
.font(&lv_font_arial_20)
.textColor(0xAAAAAA)
.align(LV_ALIGN_CENTER)
.position(0, -140);
ui_ValueDate = LvLabel(ui_TabPageHome)
.text("--- --/--/----")
.width(LV_SIZE_CONTENT)
@ -631,8 +985,7 @@ void UIManager::ui_Screen1_screen_init(void)
.textColor(0xFFFFFF)
.opa(255)
.align(LV_ALIGN_CENTER)
.position(0, -165);
.position(0, -85);
ui_ValueTime = LvLabel(ui_TabPageHome)
.text("--:--")
@ -642,12 +995,32 @@ void UIManager::ui_Screen1_screen_init(void)
.textColor(0xFFFFFF)
.opa(255)
.align(LV_ALIGN_CENTER)
.position(0, -100);
.position(0, -25);
// Advertise button
ui_AdvertiseBtn = LvButton(ui_TabPageHome)
.size(220, 56)
.align(LV_ALIGN_CENTER)
.position(0, 55)
.bgColor(0x1565C0)
.onClick(s_onAdvertiseClick, this)
.raw();
lv_obj_t* ui_AdvertiseLabel = LvLabel(ui_AdvertiseBtn)
#if defined(LANG_GR)
.text(LV_SYMBOL_WIFI " Διαφήμιση")
#else
.text(LV_SYMBOL_WIFI " Advertise")
#endif
.font(&lv_font_arial_22)
.textColor(0xFFFFFF)
.raw();
lv_obj_center(ui_AdvertiseLabel);
lv_obj_t* ui_RestartBtn = LvButton(ui_TabPageHome)
.size(200, 56)
.size(220, 56)
.align(LV_ALIGN_CENTER)
.position(0, 60)
.position(0, 130)
.bgColor(0xC62828)
.onClick(s_onRestartClick, nullptr)
.raw();
@ -679,11 +1052,20 @@ void UIManager::ui_Screen1_screen_init(void)
lv_obj_set_style_bg_opa(ui_Contacts, LV_OPA_TRANSP, LV_PART_ITEMS);
lv_obj_set_style_border_width(ui_Contacts, 0, LV_PART_ITEMS);
ui_ContactName = LvLabel(ui_TabPageContacts)
.text("")
.width(300)
.height(LV_SIZE_CONTENT)
.font(&lv_font_arial_22)
.textColor(0xFFFFFF)
.align(LV_ALIGN_CENTER)
.position(80, -195);
ui_ContactMessages = LvList(ui_TabPageContacts)
.width(310)
.height(400)
.height(290)
.align(LV_ALIGN_CENTER)
.position(80, 0)
.position(80, -40)
.transparent()
.raw();
@ -695,6 +1077,49 @@ void UIManager::ui_Screen1_screen_init(void)
//lv_obj_set_scrollbar_mode(ui_ContactMessages, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_bg_opa(ui_ContactMessages, LV_OPA_TRANSP, LV_PART_ITEMS);
lv_obj_set_style_border_width(ui_ContactMessages, 0, LV_PART_ITEMS);
ui_ContactInput = LvTextArea(ui_TabPageContacts)
.size(230, 40)
.align(LV_ALIGN_CENTER)
.position(40, channelInputBaseY)
.oneLine(true)
#if defined(LANG_EN)
.placeholder("Write message...")
#elif defined(LANG_GR)
.placeholder("Γράψε μήνυμα...")
#endif
.font(&lv_font_arial_20)
.bgColor(0x111111)
.textColor(0xFFFFFF)
.borderColor(0x444444)
.borderWidth(1)
.radius(6)
.onFocus(s_onContactInputFocus, this)
.raw();
ui_ContactSendBtn = LvButton(ui_TabPageContacts)
.size(70, 42)
.align(LV_ALIGN_CENTER)
.position(195, channelInputBaseY)
.bgColor(0x3A7AFE)
.onClick(s_onContactSendClick, this)
.raw();
ui_ContactSendLabel = LvLabel(ui_ContactSendBtn)
#if defined(LANG_EN)
.text("Send")
#elif defined(LANG_GR)
.text("Αποστολή")
#endif
.font(&lv_font_arial_18);
lv_obj_center(ui_ContactSendLabel);
ui_ContactStatus = LvLabel(ui_TabPageContacts)
.text("")
.font(&lv_font_arial_14)
.textColor(0xAAAAAA)
.align(LV_ALIGN_CENTER)
.position(80, channelInputBaseY + 30);
// LvObj(ui_TabPageContacts)
// .size(2, 400)
@ -788,6 +1213,124 @@ 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
// 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);
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();
// 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);
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();
// 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);
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();
// Firmware (read-only)
ui_SettingsFw = LvLabel(ui_TabPageSettings)
.text("...")
.font(&lv_font_arial_18)
.textColor(0x888888)
.align(LV_ALIGN_CENTER)
.position(0, 40);
// Save button
ui_SettingsSaveBtn = LvButton(ui_TabPageSettings)
.size(220, 50)
.align(LV_ALIGN_CENTER)
.position(0, 90)
.bgColor(0x2E7D32)
.onClick(s_onSettingsSaveClick, this)
.raw();
lv_obj_t* saveLabel = LvLabel(ui_SettingsSaveBtn)
#if defined(LANG_GR)
.text(LV_SYMBOL_SAVE " Αποθήκευση & Επανεκκίνηση")
#else
.text(LV_SYMBOL_SAVE " Save & Restart")
#endif
.font(&lv_font_arial_18)
.textColor(0xFFFFFF)
.raw();
lv_obj_center(saveLabel);
// Status label
ui_SettingsStatus = LvLabel(ui_TabPageSettings)
.text("")
.font(&lv_font_arial_18)
.textColor(0xFFC107)
.align(LV_ALIGN_CENTER)
.position(0, 145);
ui_Keyboard = LvKeyboard(lv_layer_top())
.size(480, 200)
.align(LV_ALIGN_BOTTOM_MID)

44
include/uiManager.h

@ -66,15 +66,59 @@ class UIManager {
lv_obj_t* iu_SendLabel;
lv_obj_t* ui_ChannelDivider;
lv_obj_t* ui_ContactName = nullptr;
lv_obj_t* ui_ContactInput = nullptr;
lv_obj_t* ui_ContactSendBtn = nullptr;
lv_obj_t* ui_ContactSendLabel = nullptr;
char currentContactName[32] = {0};
uint8_t currentContactPubKey[32] = {0};
bool hasCurrentContact = false;
char myNodeName[32] = {0};
lv_obj_t* ui_ContactStatus = nullptr;
lv_obj_t* ui_HomeNodeName = nullptr;
lv_obj_t* ui_HomePubKey = nullptr;
lv_obj_t* ui_HomeInfo = nullptr;
lv_obj_t* ui_AdvertiseBtn = nullptr;
lv_obj_t* ui_SettingsName = nullptr;
lv_obj_t* ui_SettingsFreq = nullptr;
lv_obj_t* ui_SettingsTx = nullptr;
lv_obj_t* ui_SettingsFw = nullptr;
lv_obj_t* ui_SettingsSaveBtn = nullptr;
lv_obj_t* ui_SettingsStatus = nullptr;
// Whichever input/send button last got focus — used by keyboard show/hide
lv_obj_t* activeInput = nullptr;
lv_obj_t* activeSendBtn = nullptr;
int activeInputBaseY = 0;
public:
UIManager();
void onChannelInputFocus(lv_event_t* e);
void onContactInputFocus(lv_event_t* e);
void onContactSendClick(lv_event_t* e);
void onSettingsInputFocus(lv_event_t* e);
void onSettingsSaveClick(lv_event_t* e);
void onAdvertiseClick(lv_event_t* e);
void onDimOverlayClick(lv_event_t* e);
void onSendClick(lv_event_t* e);
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 populateHome(const char* name, const char* pub_key_hex,
int contact_count, float freq);
void setMyNodeName(const char* name);
// Phase 6
void routeIncomingDM(const uint8_t* from_pub_key, const char* from_name,
const char* time_str, const char* text);
void setSendStatus(int state); // 0=sending, 1=delivered, 2=failed, -1=clear
void updateDateTime(const struct tm timeinfo);
void updateInfo(const char *str, uint32_t color);
void clearDateTime();

4
variants/elecrow_espnow/platformio.ini

@ -7,6 +7,10 @@ board = esp32-s3-devkitc-1-myboard
build_flags =
${esp32_base.build_flags}
-I variants/Elecrow_espnow
-D LORA_FREQ=869.525
-D LORA_BW=250.0
-D LORA_SF=11
-D LORA_CR=5
-D PIN_BOARD_SDA=-1
-D PIN_BOARD_SCL=-1
-D PIN_USER_BTN=0

Loading…
Cancel
Save