mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-03-30 13:45:38 +00:00
Split T-Deck firmware from microReticulum examples/lxmf_tdeck/ into its own repo. microReticulum is consumed as a git submodule dependency pinned to feat/t-deck. All include paths updated from relative symlinks to bare includes resolved via library build flags. Both tdeck (NimBLE) and tdeck-bluedroid environments compile successfully. Licensed under AGPLv3. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
652 lines
19 KiB
C++
652 lines
19 KiB
C++
// Copyright (c) 2024 microReticulum contributors
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "UIManager.h"
|
|
|
|
#ifdef ARDUINO
|
|
|
|
#include <lvgl.h>
|
|
#include <Preferences.h>
|
|
#include "Log.h"
|
|
#include "tone/Tone.h"
|
|
#include "../LVGL/LVGLLock.h"
|
|
|
|
using namespace RNS;
|
|
|
|
// NVS keys for propagation settings
|
|
static const char* NVS_NAMESPACE = "propagation";
|
|
static const char* KEY_AUTO_SELECT = "auto_select";
|
|
static const char* KEY_NODE_HASH = "node_hash";
|
|
|
|
namespace UI {
|
|
namespace LXMF {
|
|
|
|
UIManager::UIManager(Reticulum& reticulum, ::LXMF::LXMRouter& router, ::LXMF::MessageStore& store)
|
|
: _reticulum(reticulum), _router(router), _store(store),
|
|
_current_screen(SCREEN_CONVERSATION_LIST),
|
|
_conversation_list_screen(nullptr),
|
|
_chat_screen(nullptr),
|
|
_compose_screen(nullptr),
|
|
_announce_list_screen(nullptr),
|
|
_status_screen(nullptr),
|
|
_qr_screen(nullptr),
|
|
_settings_screen(nullptr),
|
|
_propagation_nodes_screen(nullptr),
|
|
_propagation_manager(nullptr),
|
|
_ble_interface(nullptr),
|
|
_initialized(false) {
|
|
}
|
|
|
|
UIManager::~UIManager() {
|
|
if (_conversation_list_screen) {
|
|
delete _conversation_list_screen;
|
|
}
|
|
if (_chat_screen) {
|
|
delete _chat_screen;
|
|
}
|
|
if (_compose_screen) {
|
|
delete _compose_screen;
|
|
}
|
|
if (_announce_list_screen) {
|
|
delete _announce_list_screen;
|
|
}
|
|
if (_status_screen) {
|
|
delete _status_screen;
|
|
}
|
|
if (_qr_screen) {
|
|
delete _qr_screen;
|
|
}
|
|
if (_settings_screen) {
|
|
delete _settings_screen;
|
|
}
|
|
if (_propagation_nodes_screen) {
|
|
delete _propagation_nodes_screen;
|
|
}
|
|
}
|
|
|
|
bool UIManager::init() {
|
|
LVGL_LOCK();
|
|
if (_initialized) {
|
|
return true;
|
|
}
|
|
|
|
INFO("Initializing UIManager");
|
|
|
|
// Create all screens
|
|
_conversation_list_screen = new ConversationListScreen();
|
|
_chat_screen = new ChatScreen();
|
|
_compose_screen = new ComposeScreen();
|
|
_announce_list_screen = new AnnounceListScreen();
|
|
_status_screen = new StatusScreen();
|
|
_qr_screen = new QRScreen();
|
|
_settings_screen = new SettingsScreen();
|
|
_propagation_nodes_screen = new PropagationNodesScreen();
|
|
|
|
// Set up callbacks for conversation list screen
|
|
_conversation_list_screen->set_conversation_selected_callback(
|
|
[this](const Bytes& peer_hash) { on_conversation_selected(peer_hash); }
|
|
);
|
|
|
|
_conversation_list_screen->set_compose_callback(
|
|
[this]() { on_new_message(); }
|
|
);
|
|
|
|
_conversation_list_screen->set_sync_callback(
|
|
[this]() { on_propagation_sync(); }
|
|
);
|
|
|
|
_conversation_list_screen->set_settings_callback(
|
|
[this]() { show_settings(); }
|
|
);
|
|
|
|
_conversation_list_screen->set_announces_callback(
|
|
[this]() { show_announces(); }
|
|
);
|
|
|
|
// Set up callbacks for chat screen
|
|
_chat_screen->set_back_callback(
|
|
[this]() { on_back_to_conversation_list(); }
|
|
);
|
|
|
|
_chat_screen->set_send_message_callback(
|
|
[this](const String& content) { on_send_message_from_chat(content); }
|
|
);
|
|
|
|
// Set up callbacks for compose screen
|
|
_compose_screen->set_cancel_callback(
|
|
[this]() { on_cancel_compose(); }
|
|
);
|
|
|
|
_compose_screen->set_send_callback(
|
|
[this](const Bytes& dest_hash, const String& message) {
|
|
on_send_message_from_compose(dest_hash, message);
|
|
}
|
|
);
|
|
|
|
// Set up callbacks for announce list screen
|
|
_announce_list_screen->set_announce_selected_callback(
|
|
[this](const Bytes& dest_hash) { on_announce_selected(dest_hash); }
|
|
);
|
|
|
|
_announce_list_screen->set_back_callback(
|
|
[this]() { on_back_from_announces(); }
|
|
);
|
|
|
|
_announce_list_screen->set_send_announce_callback(
|
|
[this]() {
|
|
INFO("Sending LXMF announce");
|
|
_router.announce();
|
|
}
|
|
);
|
|
|
|
// Set up callbacks for status screen
|
|
_status_screen->set_back_callback(
|
|
[this]() { on_back_from_status(); }
|
|
);
|
|
|
|
_status_screen->set_share_callback(
|
|
[this]() { on_share_from_status(); }
|
|
);
|
|
|
|
// Set up callbacks for QR screen
|
|
_qr_screen->set_back_callback(
|
|
[this]() { on_back_from_qr(); }
|
|
);
|
|
|
|
// Set up callbacks for settings screen
|
|
_settings_screen->set_back_callback(
|
|
[this]() { on_back_from_settings(); }
|
|
);
|
|
|
|
_settings_screen->set_propagation_nodes_callback(
|
|
[this]() { show_propagation_nodes(); }
|
|
);
|
|
|
|
// Set up callbacks for propagation nodes screen
|
|
_propagation_nodes_screen->set_back_callback(
|
|
[this]() { on_back_from_propagation_nodes(); }
|
|
);
|
|
|
|
_propagation_nodes_screen->set_node_selected_callback(
|
|
[this](const Bytes& node_hash) { on_propagation_node_selected(node_hash); }
|
|
);
|
|
|
|
_propagation_nodes_screen->set_auto_select_changed_callback(
|
|
[this](bool enabled) { on_propagation_auto_select_changed(enabled); }
|
|
);
|
|
|
|
_propagation_nodes_screen->set_sync_callback(
|
|
[this]() { on_propagation_sync(); }
|
|
);
|
|
|
|
// Load settings from NVS
|
|
_settings_screen->load_settings();
|
|
|
|
// Set identity and LXMF address on settings screen
|
|
_settings_screen->set_identity_hash(_router.identity().hash());
|
|
_settings_screen->set_lxmf_address(_router.delivery_destination().hash());
|
|
|
|
// Set up callback for status button in conversation list
|
|
_conversation_list_screen->set_status_callback(
|
|
[this]() { show_status(); }
|
|
);
|
|
|
|
// Set identity hash and LXMF address on status screen
|
|
_status_screen->set_identity_hash(_router.identity().hash());
|
|
_status_screen->set_lxmf_address(_router.delivery_destination().hash());
|
|
|
|
// Set identity and LXMF address on QR screen
|
|
_qr_screen->set_identity(_router.identity());
|
|
_qr_screen->set_lxmf_address(_router.delivery_destination().hash());
|
|
|
|
// Register LXMF delivery callback
|
|
_router.register_delivery_callback(
|
|
[this](::LXMF::LXMessage& message) { on_message_received(message); }
|
|
);
|
|
|
|
// Load conversations and show conversation list
|
|
_conversation_list_screen->load_conversations(_store);
|
|
show_conversation_list();
|
|
|
|
_initialized = true;
|
|
INFO("UIManager initialized");
|
|
|
|
return true;
|
|
}
|
|
|
|
void UIManager::update() {
|
|
LVGL_LOCK();
|
|
// Process outbound LXMF messages
|
|
_router.process_outbound();
|
|
|
|
// Process inbound LXMF messages
|
|
_router.process_inbound();
|
|
|
|
// Update status indicators (WiFi/battery) on conversation list
|
|
static uint32_t last_status_update = 0;
|
|
uint32_t now = millis();
|
|
if (now - last_status_update > 3000) { // Update every 3 seconds
|
|
last_status_update = now;
|
|
if (_conversation_list_screen) {
|
|
_conversation_list_screen->update_status();
|
|
}
|
|
// Update status screen if visible
|
|
if (_current_screen == SCREEN_STATUS && _status_screen) {
|
|
_status_screen->refresh();
|
|
}
|
|
}
|
|
}
|
|
|
|
void UIManager::show_conversation_list() {
|
|
LVGL_LOCK();
|
|
INFO("Showing conversation list");
|
|
|
|
_conversation_list_screen->refresh();
|
|
_conversation_list_screen->show();
|
|
_chat_screen->hide();
|
|
_compose_screen->hide();
|
|
_announce_list_screen->hide();
|
|
_status_screen->hide();
|
|
_settings_screen->hide();
|
|
_propagation_nodes_screen->hide();
|
|
|
|
_current_screen = SCREEN_CONVERSATION_LIST;
|
|
}
|
|
|
|
void UIManager::show_chat(const Bytes& peer_hash) {
|
|
LVGL_LOCK();
|
|
std::string hash_hex = peer_hash.toHex().substr(0, 8);
|
|
std::string msg = "Showing chat with peer " + hash_hex + "...";
|
|
INFO(msg.c_str());
|
|
|
|
_current_peer_hash = peer_hash;
|
|
|
|
_chat_screen->load_conversation(peer_hash, _store);
|
|
_chat_screen->show();
|
|
_conversation_list_screen->hide();
|
|
_compose_screen->hide();
|
|
_announce_list_screen->hide();
|
|
_status_screen->hide();
|
|
_settings_screen->hide();
|
|
_propagation_nodes_screen->hide();
|
|
|
|
_current_screen = SCREEN_CHAT;
|
|
}
|
|
|
|
void UIManager::show_compose() {
|
|
LVGL_LOCK();
|
|
INFO("Showing compose screen");
|
|
|
|
_compose_screen->clear();
|
|
_compose_screen->show();
|
|
_conversation_list_screen->hide();
|
|
_chat_screen->hide();
|
|
_announce_list_screen->hide();
|
|
_status_screen->hide();
|
|
_settings_screen->hide();
|
|
_propagation_nodes_screen->hide();
|
|
|
|
_current_screen = SCREEN_COMPOSE;
|
|
}
|
|
|
|
void UIManager::show_announces() {
|
|
LVGL_LOCK();
|
|
INFO("Showing announces screen");
|
|
|
|
_announce_list_screen->refresh();
|
|
_announce_list_screen->show();
|
|
_conversation_list_screen->hide();
|
|
_chat_screen->hide();
|
|
_compose_screen->hide();
|
|
_status_screen->hide();
|
|
_settings_screen->hide();
|
|
_propagation_nodes_screen->hide();
|
|
|
|
_current_screen = SCREEN_ANNOUNCES;
|
|
}
|
|
|
|
void UIManager::show_status() {
|
|
LVGL_LOCK();
|
|
INFO("Showing status screen");
|
|
|
|
_status_screen->refresh();
|
|
_status_screen->show();
|
|
_conversation_list_screen->hide();
|
|
_chat_screen->hide();
|
|
_compose_screen->hide();
|
|
_announce_list_screen->hide();
|
|
_settings_screen->hide();
|
|
_propagation_nodes_screen->hide();
|
|
|
|
_current_screen = SCREEN_STATUS;
|
|
}
|
|
|
|
void UIManager::on_conversation_selected(const Bytes& peer_hash) {
|
|
show_chat(peer_hash);
|
|
}
|
|
|
|
void UIManager::on_new_message() {
|
|
show_compose();
|
|
}
|
|
|
|
void UIManager::show_settings() {
|
|
LVGL_LOCK();
|
|
INFO("Showing settings screen");
|
|
|
|
_settings_screen->refresh();
|
|
_settings_screen->show();
|
|
_conversation_list_screen->hide();
|
|
_chat_screen->hide();
|
|
_compose_screen->hide();
|
|
_announce_list_screen->hide();
|
|
_status_screen->hide();
|
|
_propagation_nodes_screen->hide();
|
|
|
|
_current_screen = SCREEN_SETTINGS;
|
|
}
|
|
|
|
void UIManager::show_propagation_nodes() {
|
|
LVGL_LOCK();
|
|
INFO("Showing propagation nodes screen");
|
|
|
|
if (_propagation_manager) {
|
|
// Load settings from NVS
|
|
Preferences prefs;
|
|
prefs.begin(NVS_NAMESPACE, true); // read-only
|
|
bool auto_select = prefs.getBool(KEY_AUTO_SELECT, true);
|
|
|
|
Bytes selected_hash;
|
|
size_t hash_len = prefs.getBytesLength(KEY_NODE_HASH);
|
|
if (hash_len > 0 && hash_len <= 32) {
|
|
uint8_t buf[32];
|
|
prefs.getBytes(KEY_NODE_HASH, buf, hash_len);
|
|
selected_hash = Bytes(buf, hash_len);
|
|
}
|
|
prefs.end();
|
|
|
|
// If not auto-select and we have a saved hash, use it
|
|
if (!auto_select && selected_hash.size() > 0) {
|
|
_router.set_outbound_propagation_node(selected_hash);
|
|
}
|
|
|
|
_propagation_nodes_screen->load_nodes(*_propagation_manager, selected_hash, auto_select);
|
|
}
|
|
|
|
_propagation_nodes_screen->show();
|
|
_conversation_list_screen->hide();
|
|
_chat_screen->hide();
|
|
_compose_screen->hide();
|
|
_announce_list_screen->hide();
|
|
_status_screen->hide();
|
|
_settings_screen->hide();
|
|
|
|
_current_screen = SCREEN_PROPAGATION_NODES;
|
|
}
|
|
|
|
void UIManager::set_propagation_node_manager(::LXMF::PropagationNodeManager* manager) {
|
|
_propagation_manager = manager;
|
|
}
|
|
|
|
void UIManager::set_lora_interface(Interface* iface) {
|
|
if (_conversation_list_screen) {
|
|
_conversation_list_screen->set_lora_interface(iface);
|
|
}
|
|
}
|
|
|
|
void UIManager::set_ble_interface(Interface* iface) {
|
|
_ble_interface = iface;
|
|
if (_conversation_list_screen) {
|
|
_conversation_list_screen->set_ble_interface(iface);
|
|
}
|
|
}
|
|
|
|
void UIManager::set_gps(TinyGPSPlus* gps) {
|
|
if (_conversation_list_screen) {
|
|
_conversation_list_screen->set_gps(gps);
|
|
}
|
|
}
|
|
|
|
void UIManager::on_back_to_conversation_list() {
|
|
show_conversation_list();
|
|
}
|
|
|
|
void UIManager::on_send_message_from_chat(const String& content) {
|
|
send_message(_current_peer_hash, content);
|
|
}
|
|
|
|
void UIManager::on_send_message_from_compose(const Bytes& dest_hash, const String& message) {
|
|
send_message(dest_hash, message);
|
|
|
|
// Switch to chat screen for this conversation
|
|
show_chat(dest_hash);
|
|
}
|
|
|
|
void UIManager::on_cancel_compose() {
|
|
show_conversation_list();
|
|
}
|
|
|
|
void UIManager::on_announce_selected(const Bytes& dest_hash) {
|
|
std::string hash_hex = dest_hash.toHex().substr(0, 8);
|
|
std::string msg = "Announce selected: " + hash_hex + "...";
|
|
INFO(msg.c_str());
|
|
|
|
// Go directly to chat screen with this destination
|
|
show_chat(dest_hash);
|
|
}
|
|
|
|
void UIManager::on_back_from_announces() {
|
|
show_conversation_list();
|
|
}
|
|
|
|
void UIManager::on_back_from_status() {
|
|
show_conversation_list();
|
|
}
|
|
|
|
void UIManager::on_share_from_status() {
|
|
LVGL_LOCK();
|
|
_status_screen->hide();
|
|
_qr_screen->show();
|
|
_current_screen = SCREEN_QR;
|
|
}
|
|
|
|
void UIManager::on_back_from_qr() {
|
|
LVGL_LOCK();
|
|
_qr_screen->hide();
|
|
_status_screen->show();
|
|
_current_screen = SCREEN_STATUS;
|
|
}
|
|
|
|
void UIManager::on_back_from_settings() {
|
|
show_conversation_list();
|
|
}
|
|
|
|
void UIManager::on_back_from_propagation_nodes() {
|
|
show_settings();
|
|
}
|
|
|
|
void UIManager::on_propagation_node_selected(const Bytes& node_hash) {
|
|
std::string hash_hex = node_hash.toHex().substr(0, 16);
|
|
std::string msg = "Propagation node selected: " + hash_hex + "...";
|
|
INFO(msg.c_str());
|
|
|
|
// Set the node in the router
|
|
_router.set_outbound_propagation_node(node_hash);
|
|
|
|
// Save to NVS
|
|
Preferences prefs;
|
|
prefs.begin(NVS_NAMESPACE, false);
|
|
prefs.putBool(KEY_AUTO_SELECT, false);
|
|
prefs.putBytes(KEY_NODE_HASH, node_hash.data(), node_hash.size());
|
|
prefs.end();
|
|
DEBUG("Propagation node saved to NVS");
|
|
}
|
|
|
|
void UIManager::on_propagation_auto_select_changed(bool enabled) {
|
|
std::string msg = "Propagation auto-select changed: ";
|
|
msg += enabled ? "enabled" : "disabled";
|
|
INFO(msg.c_str());
|
|
|
|
if (enabled) {
|
|
// Clear manual selection, router will use best node
|
|
_router.set_outbound_propagation_node(Bytes());
|
|
}
|
|
|
|
// Save to NVS
|
|
Preferences prefs;
|
|
prefs.begin(NVS_NAMESPACE, false);
|
|
prefs.putBool(KEY_AUTO_SELECT, enabled);
|
|
if (enabled) {
|
|
prefs.remove(KEY_NODE_HASH); // Clear saved node when auto-select enabled
|
|
}
|
|
prefs.end();
|
|
DEBUG("Propagation auto-select saved to NVS");
|
|
}
|
|
|
|
void UIManager::on_propagation_sync() {
|
|
INFO("Requesting messages from propagation node");
|
|
_router.request_messages_from_propagation_node();
|
|
}
|
|
|
|
void UIManager::set_rns_status(bool connected, const String& server_name) {
|
|
if (_status_screen) {
|
|
_status_screen->set_rns_status(connected, server_name);
|
|
}
|
|
}
|
|
|
|
void UIManager::send_message(const Bytes& dest_hash, const String& content) {
|
|
std::string hash_hex = dest_hash.toHex().substr(0, 8);
|
|
std::string msg = "Sending message to " + hash_hex + "...";
|
|
INFO(msg.c_str());
|
|
|
|
// Get our source destination (needed for signing)
|
|
Destination source = _router.delivery_destination();
|
|
|
|
// Create message content
|
|
Bytes content_bytes((const uint8_t*)content.c_str(), content.length());
|
|
Bytes title; // Empty title
|
|
|
|
// Look up destination identity
|
|
Identity dest_identity = Identity::recall(dest_hash);
|
|
|
|
// Create destination object - either real or placeholder
|
|
Destination destination(Type::NONE);
|
|
if (dest_identity) {
|
|
destination = Destination(dest_identity, Type::Destination::OUT, Type::Destination::SINGLE, "lxmf", "delivery");
|
|
INFO(" Destination identity known");
|
|
} else {
|
|
WARNING(" Destination identity not known, message may fail until peer announces");
|
|
}
|
|
|
|
// Create message with destination and source objects
|
|
// Source is needed for signing
|
|
::LXMF::LXMessage message(destination, source, content_bytes, title);
|
|
|
|
// If destination identity was unknown, manually set the destination hash
|
|
if (!dest_identity) {
|
|
message.destination_hash(dest_hash);
|
|
DEBUG(" Set destination hash manually");
|
|
}
|
|
|
|
// Pack the message to generate hash and signature before saving
|
|
message.pack();
|
|
|
|
// Add to UI immediately (optimistic update)
|
|
if (_current_screen == SCREEN_CHAT && _current_peer_hash == dest_hash) {
|
|
_chat_screen->add_message(message, true);
|
|
}
|
|
|
|
// Save to store (now has valid hash from pack())
|
|
_store.save_message(message);
|
|
|
|
// Queue for sending (pack already called, will use cached packed data)
|
|
_router.handle_outbound(message);
|
|
|
|
INFO(" Message queued for delivery");
|
|
}
|
|
|
|
void UIManager::on_message_received(::LXMF::LXMessage& message) {
|
|
LVGL_LOCK();
|
|
std::string source_hex = message.source_hash().toHex().substr(0, 8);
|
|
std::string msg = "Message received from " + source_hex + "...";
|
|
INFO(msg.c_str());
|
|
|
|
// Save to store
|
|
_store.save_message(message);
|
|
|
|
// Update UI if we're viewing this conversation
|
|
bool viewing_this_chat = (_current_screen == SCREEN_CHAT && _current_peer_hash == message.source_hash());
|
|
if (viewing_this_chat) {
|
|
_chat_screen->add_message(message, false);
|
|
}
|
|
|
|
// Play notification sound if enabled and not viewing this conversation
|
|
if (_settings_screen) {
|
|
const auto& settings = _settings_screen->get_settings();
|
|
if (settings.notification_sound && !viewing_this_chat) {
|
|
Notification::tone_play(1000, 100, settings.notification_volume); // 1kHz beep, 100ms
|
|
}
|
|
}
|
|
|
|
// Update conversation list unread count
|
|
// TODO: Track unread counts
|
|
_conversation_list_screen->refresh();
|
|
|
|
INFO(" Message processed");
|
|
}
|
|
|
|
void UIManager::on_message_delivered(::LXMF::LXMessage& message) {
|
|
LVGL_LOCK();
|
|
std::string hash_hex = message.hash().toHex().substr(0, 8);
|
|
std::string msg = "Message delivered: " + hash_hex + "...";
|
|
INFO(msg.c_str());
|
|
|
|
// Update UI if we're viewing this conversation
|
|
if (_current_screen == SCREEN_CHAT && _current_peer_hash == message.destination_hash()) {
|
|
_chat_screen->update_message_status(message.hash(), true);
|
|
}
|
|
}
|
|
|
|
void UIManager::on_message_failed(::LXMF::LXMessage& message) {
|
|
LVGL_LOCK();
|
|
std::string hash_hex = message.hash().toHex().substr(0, 8);
|
|
std::string msg = "Message delivery failed: " + hash_hex + "...";
|
|
WARNING(msg.c_str());
|
|
|
|
// Update UI if we're viewing this conversation
|
|
if (_current_screen == SCREEN_CHAT && _current_peer_hash == message.destination_hash()) {
|
|
_chat_screen->update_message_status(message.hash(), false);
|
|
}
|
|
}
|
|
|
|
void UIManager::refresh_current_screen() {
|
|
LVGL_LOCK();
|
|
switch (_current_screen) {
|
|
case SCREEN_CONVERSATION_LIST:
|
|
_conversation_list_screen->refresh();
|
|
break;
|
|
case SCREEN_CHAT:
|
|
_chat_screen->refresh();
|
|
break;
|
|
case SCREEN_COMPOSE:
|
|
// No refresh needed
|
|
break;
|
|
case SCREEN_ANNOUNCES:
|
|
_announce_list_screen->refresh();
|
|
break;
|
|
case SCREEN_STATUS:
|
|
_status_screen->refresh();
|
|
break;
|
|
case SCREEN_SETTINGS:
|
|
_settings_screen->refresh();
|
|
break;
|
|
case SCREEN_PROPAGATION_NODES:
|
|
_propagation_nodes_screen->refresh();
|
|
break;
|
|
}
|
|
}
|
|
|
|
} // namespace LXMF
|
|
} // namespace UI
|
|
|
|
#endif // ARDUINO
|