feat(reticulum): complete Sideband client runtime

This commit is contained in:
liu weikai committed 2026-07-16 23:17:50 +08:00
1 parent df725643c6
commit 14d277e2c4
97 files changed
+9620 -2011

No files matched your search

+91
View File
@@ -26,6 +26,9 @@ AppScreen* s_pending_exit = nullptr;
lv_timer_t* s_exit_timer = nullptr;
lv_timer_t* s_rebuild_timer = nullptr;
bool s_overlay_active = false;
AppScreen* s_interruption_app = nullptr;
AppScreen* s_interrupted_app = nullptr;
bool s_interrupted_was_menu = false;
#if defined(TRAIL_MATE_ESP_BOARD_T_DISPLAY_P4)
struct AppEdgeSwipeState
@@ -370,6 +373,10 @@ void set_default_group(lv_group_t* group)
void menu_show()
{
if (s_interruption_app != nullptr)
{
return;
}
show_menu_internal();
}
@@ -385,6 +392,10 @@ void ui_clear_active_app()
void ui_switch_to_app(AppScreen* app, lv_obj_t* parent)
{
if (s_interruption_app != nullptr && app != s_interruption_app)
{
return;
}
#if defined(ESP_PLATFORM)
ESP_LOGI(kTag,
"ui_switch_to_app from=%s to=%s parent=%d",
@@ -422,6 +433,10 @@ void ui_switch_to_app(AppScreen* app, lv_obj_t* parent)
void ui_exit_active_app(lv_obj_t* parent)
{
if (s_interruption_app != nullptr)
{
return;
}
if (s_active_app)
{
std::printf("[UI][Lifecycle] app exit name=%s reason=explicit\n",
@@ -432,8 +447,84 @@ void ui_exit_active_app(lv_obj_t* parent)
ui::menu_runtime::setScene(ui::menu_runtime::Scene::Menu);
}
bool ui_present_interruption_app(AppScreen* app, lv_obj_t* parent)
{
if (!app || !parent)
{
return false;
}
if (s_interruption_app != nullptr)
{
return s_interruption_app == app && s_active_app == app;
}
const bool exit_to_menu_was_pending =
s_pending_exit != nullptr && s_pending_exit == s_active_app;
s_interrupted_app = s_active_app;
s_interrupted_was_menu =
ui::menu_runtime::currentScene() == ui::menu_runtime::Scene::Menu ||
exit_to_menu_was_pending;
if (s_exit_timer)
{
lv_timer_del(s_exit_timer);
s_exit_timer = nullptr;
}
if (s_rebuild_timer)
{
lv_timer_del(s_rebuild_timer);
s_rebuild_timer = nullptr;
}
s_pending_exit = nullptr;
s_interruption_app = app;
ui_set_overlay_active(true);
ui::menu_layout::setMenuVisible(false);
ui_switch_to_app(app, parent);
return s_active_app == app;
}
void ui_dismiss_interruption_app(lv_obj_t* parent)
{
AppScreen* interruption = s_interruption_app;
if (!interruption)
{
return;
}
AppScreen* interrupted = s_interrupted_app;
const bool restore_menu = s_interrupted_was_menu || interrupted == nullptr;
s_interruption_app = nullptr;
s_interrupted_app = nullptr;
s_interrupted_was_menu = false;
ui_set_overlay_active(false);
if (s_active_app == interruption)
{
std::printf("[UI][Lifecycle] app exit name=%s reason=interruption_complete\n",
interruption->name());
interruption->exit(parent);
s_active_app = nullptr;
}
if (!restore_menu && interrupted)
{
ui::menu_layout::setMenuVisible(false);
ui_switch_to_app(interrupted, parent);
return;
}
show_menu_internal();
}
bool ui_is_interruption_app_active()
{
return s_interruption_app != nullptr;
}
void ui_request_exit_to_menu()
{
if (s_interruption_app != nullptr)
{
return;
}
AppScreen* app = s_active_app;
#if defined(ESP_PLATFORM)
ESP_LOGI(kTag, "ui_request_exit_to_menu app=%s", app ? app->name() : "(null)");
@@ -268,6 +268,8 @@ bool model_fits_location_overlay(lv_obj_t* viewport_root,
return ::ui::chat::MessageDeliveryState::Sent;
case chat::MessageStatus::Failed:
return ::ui::chat::MessageDeliveryState::Failed;
case chat::MessageStatus::Delivered:
return ::ui::chat::MessageDeliveryState::Delivered;
}
return ::ui::chat::MessageDeliveryState::Unknown;
}
@@ -28,6 +28,8 @@ constexpr size_t kMaxPrefixedSenderLen = 20;
return ::ui::chat::MessageDeliveryState::Sent;
case chat::MessageStatus::Failed:
return ::ui::chat::MessageDeliveryState::Failed;
case chat::MessageStatus::Delivered:
return ::ui::chat::MessageDeliveryState::Delivered;
}
return ::ui::chat::MessageDeliveryState::Unknown;
}
@@ -1210,23 +1210,6 @@ static chat::MeshActionResult persist_reticulum_contact_peer(
return g_contacts_state.chat_service->persistReticulumPeer(identity, favorite);
}
static void present_busy_overlay_now()
{
static bool s_presenting = false;
if (s_presenting)
{
return;
}
s_presenting = true;
if (lv_obj_t* top = lv_layer_top())
{
lv_obj_invalidate(top);
}
lv_timer_handler();
lv_refr_now(nullptr);
s_presenting = false;
}
class ScopedReticulumContactSaveOverlay
{
public:
@@ -1239,7 +1222,6 @@ class ScopedReticulumContactSaveOverlay
return;
}
::ui::widgets::busy_overlay::show("Saving contact...", detail);
present_busy_overlay_now();
}
~ScopedReticulumContactSaveOverlay()
@@ -1249,7 +1231,6 @@ class ScopedReticulumContactSaveOverlay
return;
}
::ui::widgets::busy_overlay::hide();
present_busy_overlay_now();
}
ScopedReticulumContactSaveOverlay(const ScopedReticulumContactSaveOverlay&) =
@@ -4463,23 +4444,6 @@ void refresh_ui()
}
}
g_contacts_state.list_items.clear();
if (g_contacts_state.virtual_top_spacer != nullptr)
{
if (lv_obj_is_valid(g_contacts_state.virtual_top_spacer))
{
lv_obj_del(g_contacts_state.virtual_top_spacer);
}
g_contacts_state.virtual_top_spacer = nullptr;
}
if (g_contacts_state.virtual_bottom_spacer != nullptr)
{
if (lv_obj_is_valid(g_contacts_state.virtual_bottom_spacer))
{
lv_obj_del(g_contacts_state.virtual_bottom_spacer);
}
g_contacts_state.virtual_bottom_spacer = nullptr;
}
// Choose list by mode (unchanged)
std::vector<chat::contacts::NodeInfo> broadcast_list;
std::vector<chat::contacts::NodeInfo> team_list;
@@ -4582,7 +4546,6 @@ void refresh_ui()
{
g_contacts_state.total_items += 1;
}
const bool use_virtual_window = false;
int target_scroll_y = same_render_context ? saved_scroll_y : 0;
if (target_scroll_y < 0)
{
@@ -4613,12 +4576,6 @@ void refresh_ui()
{
end_idx = static_cast<int>(current_list->size());
}
g_contacts_state.virtual_list_active = use_virtual_window;
g_contacts_state.list_window_start_index = use_virtual_window ? start_idx : 0;
g_contacts_state.list_window_end_index =
use_virtual_window ? end_idx : static_cast<int>(current_list->size());
g_contacts_state.list_scroll_y = target_scroll_y;
if (show_reticulum_group_add_item)
{
chat::contacts::NodeInfo add_node{};
@@ -148,13 +148,28 @@ static lv_obj_t* get_list_button(void* /*ctx*/, size_t index)
static int get_preferred_list_index(void* /*ctx*/)
{
const int selected = contacts::ui::g_contacts_state.selected_index;
if (contacts::ui::g_contacts_state.virtual_list_active)
if (selected < 0)
{
const int start = contacts::ui::g_contacts_state.list_window_start_index;
const int end = contacts::ui::g_contacts_state.list_window_end_index;
return selected >= start && selected < end ? selected - start : -1;
return -1;
}
return selected >= 0 ? selected : -1;
const auto& items = contacts::ui::g_contacts_state.list_items;
for (size_t i = 0; i < items.size(); ++i)
{
lv_obj_t* item = items[i];
if (item == nullptr || !lv_obj_is_valid(item))
{
continue;
}
const intptr_t item_index =
reinterpret_cast<intptr_t>(lv_obj_get_user_data(item));
if (item_index == selected)
{
return static_cast<int>(i);
}
}
return -1;
}
static lv_obj_t* get_list_back_button(void* /*ctx*/)
@@ -194,7 +194,9 @@ void create_filter_panel(lv_obj_t* parent)
style::apply_panel_side(g_contacts_state.filter_panel);
g_contacts_state.contacts_btn = create_filter_button(g_contacts_state.filter_panel, "Contacts");
g_contacts_state.nearby_btn = create_filter_button(g_contacts_state.filter_panel, "Nearby");
g_contacts_state.nearby_btn = create_filter_button(
g_contacts_state.filter_panel,
reticulum_profile ? "Announced" : "Nearby");
if (reticulum_profile)
{
@@ -7,6 +7,7 @@
#include "chat/infra/reticulum/reticulum_wire.h"
#include "chat/usecase/chat_service.h"
#include "chat/usecase/contact_service.h"
#include "platform/ui/reticulum_contact_projection_policy.h"
#include "platform/ui/reticulum_directory_runtime.h"
#include "platform/ui/reticulum_group_config_runtime.h"
#include "ui/app_runtime.h"
@@ -41,6 +42,7 @@ namespace
{
using contacts::ui::shell::Host;
namespace rtcontacts = ::platform::ui::reticulum_contacts;
namespace rtdir = ::platform::ui::reticulum_directory;
constexpr uint32_t kContactsRefreshIntervalMs = 2000;
@@ -365,7 +367,7 @@ chat::contacts::NodeInfo node_from_lxmf_address(
item.channel = 0xFF;
item.is_contact = as_contact || record.favorite;
item.protocol = chat::contacts::NodeProtocolType::Reticulum;
item.role = chat::contacts::NodeRoleType::Client;
item.role = chat::contacts::NodeRoleType::Unknown;
item.is_ignored = record.ignored;
item.has_public_key = true;
item.key_manually_verified = record.trusted;
@@ -476,18 +478,22 @@ void merge_reticulum_directory_projection()
for (std::size_t index = 0; index < count; ++index)
{
const auto& record = records[index];
if (!record.valid)
const rtcontacts::ProjectionBucket bucket =
rtcontacts::classify(record);
if (bucket == rtcontacts::ProjectionBucket::Hidden)
{
continue;
}
const chat::contacts::NodeInfo item =
node_from_lxmf_address(record, record.favorite);
if (record.favorite)
node_from_lxmf_address(
record,
bucket == rtcontacts::ProjectionBucket::Contact);
if (bucket == rtcontacts::ProjectionBucket::Contact)
{
upsert_reticulum_projection(g_contacts_state.contacts_list, item, true);
}
if (record.ignored)
else if (bucket == rtcontacts::ProjectionBucket::Ignored)
{
upsert_reticulum_projection(g_contacts_state.ignored_list, item, false);
}
@@ -652,10 +658,6 @@ void enter(const shell::Host* host, lv_obj_t* parent)
g_contacts_state.last_action_mode = ContactsMode::Contacts;
g_contacts_state.current_page = 0;
g_contacts_state.selected_index = -1;
g_contacts_state.virtual_list_active = false;
g_contacts_state.list_window_start_index = 0;
g_contacts_state.list_window_end_index = 0;
g_contacts_state.list_scroll_y = 0;
g_contacts_state.rendered_mode_valid = false;
g_contacts_state.rendered_search_query[0] = '\0';
g_contacts_state.contacts_data_signature = 0;
@@ -331,7 +331,18 @@ void copy_text(char* out, std::size_t out_len, const char* text)
{
return;
}
std::snprintf(out, out_len, "%s", text ? text : "");
if (!text)
{
out[0] = '\0';
return;
}
std::size_t copy_len = std::strlen(text);
if (copy_len >= out_len)
{
copy_len = out_len - 1U;
}
std::memmove(out, text, copy_len);
out[copy_len] = '\0';
}
void* network_page_alloc(std::size_t size)
@@ -1660,7 +1671,14 @@ void resolve_link_target(const char* target, char* out, std::size_t out_len)
char hash_text[kHashTextLen] = {};
if (extract_destination_text(g_state.current_address, hash_text, sizeof(hash_text)))
{
std::snprintf(out, out_len, "%s:%s", hash_text, target);
copy_text(out, out_len, hash_text);
std::size_t used = std::strlen(out);
if (used + 1U < out_len)
{
out[used++] = ':';
out[used] = '\0';
copy_text(out + used, out_len - used, target);
}
return;
}
}
@@ -25,8 +25,13 @@ void copy_bounded(char* out, std::size_t out_len, const char* text)
out[0] = '\0';
return;
}
std::strncpy(out, text, out_len - 1);
out[out_len - 1] = '\0';
std::size_t copy_len = std::strlen(text);
if (copy_len >= out_len)
{
copy_len = out_len - 1U;
}
std::memmove(out, text, copy_len);
out[copy_len] = '\0';
}
bool parse_hex_char(char c, std::uint8_t& out)
@@ -500,7 +500,9 @@ static void firmware_status_summary(const firmware_update_runtime::Status& statu
status.detail[0] != '\0' &&
std::strcmp(status.detail, message) != 0)
{
std::snprintf(out, out_len, "%s: %s", message, status.detail);
copy_bounded(out, out_len, message);
append_bounded(out, out_len, ": ");
append_bounded(out, out_len, status.detail);
return;
}
@@ -1310,8 +1312,6 @@ static void reset_mesh_settings()
app_ctx.getConfig().reticulumConfig().reticulum_wifi_auto_connect;
g_settings.rt_anonymous_peer =
app_ctx.getConfig().reticulumConfig().reticulum_anonymous_peer;
g_settings.rt_call_wire = static_cast<int>(
app_ctx.getConfig().reticulumConfig().reticulum_call_wire_profile);
g_settings.rt_location_requests =
app_ctx.getConfig().reticulumConfig().reticulum_allow_location_requests;
g_settings.mt_mqtt_enabled = app_ctx.getConfig().meshtastic_mqtt_enabled;
@@ -1618,8 +1618,6 @@ static void settings_load()
g_settings.rt_wifi_gateway_enabled = reticulum_cfg.reticulum_wifi_gateway_enabled;
g_settings.rt_wifi_auto_connect = reticulum_cfg.reticulum_wifi_auto_connect;
g_settings.rt_anonymous_peer = reticulum_cfg.reticulum_anonymous_peer;
g_settings.rt_call_wire =
static_cast<int>(reticulum_cfg.reticulum_call_wire_profile);
g_settings.rt_location_requests =
reticulum_cfg.reticulum_allow_location_requests;
copy_bounded(g_settings.rt_wifi_gateway_host,
@@ -2797,7 +2795,7 @@ static void build_number_options(char* out,
size_t used = 0;
for (int value = min_value; value <= max_value; ++value)
{
char item[8] = {};
char item[16] = {};
if (digits == 4)
{
std::snprintf(item, sizeof(item), "%04d", value);
@@ -3249,20 +3247,6 @@ static void on_option_clicked(lv_event_t* e)
app_ctx.applyMeshConfig();
rebuild_list = true;
}
if (id == settings::ui::SettingId::RtCallWire)
{
app::IAppFacade& app_ctx = app::appFacade();
const int clamped = payload->value == static_cast<int>(
chat::ReticulumCallWireProfile::MeshChatCallAudio)
? payload->value
: static_cast<int>(
chat::ReticulumCallWireProfile::SidebandLxst);
g_settings.rt_call_wire = clamped;
app_ctx.getConfig().reticulumConfig().reticulum_call_wire_profile =
static_cast<chat::ReticulumCallWireProfile>(clamped);
app_ctx.saveConfig();
app_ctx.applyMeshConfig();
}
if (id == settings::ui::SettingId::MeshProtocol)
{
app::IAppFacade& app_ctx = app::appFacade();
@@ -4410,10 +4394,6 @@ static const settings::ui::SettingOption kReticulumBearerOptions[] = {
{"LoRa", static_cast<int>(chat::ReticulumInterfacePolicy::LoRaOnly)},
{"Wi-Fi", static_cast<int>(chat::ReticulumInterfacePolicy::WifiGatewayOnly)},
};
static const settings::ui::SettingOption kReticulumCallWireOptions[] = {
{"Sideband", static_cast<int>(chat::ReticulumCallWireProfile::SidebandLxst)},
{"MeshChat", static_cast<int>(chat::ReticulumCallWireProfile::MeshChatCallAudio)},
};
static const settings::ui::SettingOption kChatContactAlertOptions[] = {
{"OFF", kChatContactAlertsNone},
{"Contacts Only", kChatContactAlertsContacts},
@@ -4603,7 +4583,6 @@ static settings::ui::SettingItem kMeshItems[] = {
{"Gateway Port", settings::ui::SettingType::Text, nullptr, 0, nullptr, nullptr, g_settings.rt_wifi_gateway_port, sizeof(g_settings.rt_wifi_gateway_port), false, "rt_wifi_port"},
{"Auto Wi-Fi", settings::ui::SettingType::Toggle, nullptr, 0, nullptr, &g_settings.rt_wifi_auto_connect, nullptr, 0, false, "rt_wifi_auto"},
{"Anonymous Peer", settings::ui::SettingType::Toggle, nullptr, 0, nullptr, &g_settings.rt_anonymous_peer, nullptr, 0, false, "rt_anonymous_peer"},
{"Call Protocol", settings::ui::SettingType::Enum, kReticulumCallWireOptions, sizeof(kReticulumCallWireOptions) / sizeof(kReticulumCallWireOptions[0]), &g_settings.rt_call_wire, nullptr, nullptr, 0, false, "rt_call_wire"},
{"Location Requests", settings::ui::SettingType::Toggle, nullptr, 0, nullptr, &g_settings.rt_location_requests, nullptr, 0, false, "rt_location_requests"},
};
@@ -70,7 +70,6 @@ constexpr SettingSpec kSpecs[] = {
{SettingId::RtWifiPort, "rt_wifi_port", DynamicOptionKind::None},
{SettingId::RtWifiAuto, "rt_wifi_auto", DynamicOptionKind::None},
{SettingId::RtAnonymousPeer, "rt_anonymous_peer", DynamicOptionKind::None},
{SettingId::RtCallWire, "rt_call_wire", DynamicOptionKind::None},
{SettingId::RtLocationRequests, "rt_location_requests", DynamicOptionKind::None},
{SettingId::NetUsePreset, "net_use_preset", DynamicOptionKind::None},
{SettingId::NetPreset, "net_preset", DynamicOptionKind::None},
@@ -276,7 +275,6 @@ bool is_reticulum_mesh(SettingId id)
case SettingId::RtWifiPort:
case SettingId::RtWifiAuto:
case SettingId::RtAnonymousPeer:
case SettingId::RtCallWire:
case SettingId::RtLocationRequests:
return true;
default:
File diff suppressed because it is too large. Load diff