From 32432ededf11e97d18cc64acd0197409c667be9e Mon Sep 17 00:00:00 2001 From: liu weikai Date: Thu, 23 Jul 2026 23:49:47 +0800 Subject: [PATCH] fix(runtime): unify network and direct messaging lifecycles --- .../infra/meshtastic/mt_protocol_helpers.h | 17 +- .../infra/meshtastic/mt_protocol_helpers.cpp | 8 - .../tests/test_meshtastic_protocol_policy.cpp | 11 + .../include/platform/ui/setting_sensitivity.h | 77 +++ .../include/platform/ui/wifi_access_runtime.h | 1 + .../include/platform/ui/wifi_runtime.h | 1 + .../core_team/src/usecase/team_service.cpp | 6 +- .../node_info/node_info_page_components.h | 8 + .../contacts/contacts_page_components.cpp | 19 +- .../extensions/extensions_page_runtime.cpp | 15 +- .../node_info/node_info_page_components.cpp | 78 +++ .../src/ui/widgets/map/map_viewport.cpp | 1 + .../infra/reticulum/reticulum_interfaces.h | 8 +- .../arduino_common/net/async_tcp_connector.h | 77 +++ .../include/ui/widgets/map/map_tiles.h | 8 + .../arduino_common/src/app_config_store.cpp | 9 +- .../chat/infra/mesh_mqtt_client_runtime.cpp | 152 +++--- .../src/chat/infra/meshtastic/mt_adapter.cpp | 29 +- .../infra/reticulum/reticulum_interfaces.cpp | 143 +++--- .../src/net/async_tcp_connector.cpp | 447 ++++++++++++++++++ .../platform_ui_firmware_update_runtime.cpp | 2 + .../src/platform_ui_wifi_access_runtime.cpp | 16 +- .../src/ui/widgets/map/map_tiles.cpp | 90 +++- .../platform/esp/common/settings_store_impl.h | 5 +- .../platform/esp/common/wifi_runtime_impl.h | 249 +++++----- .../src/platform_ui_wifi_runtime.cpp | 140 +----- .../common/include/ui/widgets/map/map_tiles.h | 1 + .../src/chat/linux_raw_lora_mesh_adapter.cpp | 10 +- .../common/src/ui/widgets/map/map_tiles.cpp | 5 + .../meshtastic/meshtastic_radio_adapter.cpp | 15 +- 30 files changed, 1116 insertions(+), 532 deletions(-) create mode 100644 modules/core_sys/include/platform/ui/setting_sensitivity.h create mode 100644 platform/esp/arduino_common/include/platform/esp/arduino_common/net/async_tcp_connector.h create mode 100644 platform/esp/arduino_common/src/net/async_tcp_connector.cpp diff --git a/modules/core_chat/include/chat/infra/meshtastic/mt_protocol_helpers.h b/modules/core_chat/include/chat/infra/meshtastic/mt_protocol_helpers.h index 590a3afb..5cb9736e 100644 --- a/modules/core_chat/include/chat/infra/meshtastic/mt_protocol_helpers.h +++ b/modules/core_chat/include/chat/infra/meshtastic/mt_protocol_helpers.h @@ -52,7 +52,22 @@ void fillDecodedPacketCommon(meshtastic_MeshPacket* packet, const meshtastic_Data& decoded, const PacketHeaderWire& header, chat::ChannelId channel_index); -bool allowPkiForPortnum(uint32_t portnum); +inline bool allowPkiForPortnum(uint32_t portnum) +{ + return portnum != meshtastic_PortNum_NODEINFO_APP && + portnum != meshtastic_PortNum_ROUTING_APP && + portnum != meshtastic_PortNum_POSITION_APP && + portnum != meshtastic_PortNum_TRACEROUTE_APP; +} + +inline bool requirePkiForDirectPort(uint32_t dest_node, uint32_t portnum) +{ + // The medium is not the audience: a direct packet can be relayed through + // LoRa and MQTT, but its confidentiality policy remains per-recipient. + constexpr uint32_t kBroadcastNode = 0xFFFFFFFFUL; + return dest_node != kBroadcastNode && allowPkiForPortnum(portnum); +} + uint32_t djb2HashText(const char* text); } // namespace meshtastic diff --git a/modules/core_chat/src/infra/meshtastic/mt_protocol_helpers.cpp b/modules/core_chat/src/infra/meshtastic/mt_protocol_helpers.cpp index f7f3389a..ff848534 100644 --- a/modules/core_chat/src/infra/meshtastic/mt_protocol_helpers.cpp +++ b/modules/core_chat/src/infra/meshtastic/mt_protocol_helpers.cpp @@ -422,14 +422,6 @@ void fillDecodedPacketCommon(meshtastic_MeshPacket* packet, packet->decoded = decoded; } -bool allowPkiForPortnum(uint32_t portnum) -{ - return portnum != meshtastic_PortNum_NODEINFO_APP && - portnum != meshtastic_PortNum_ROUTING_APP && - portnum != meshtastic_PortNum_POSITION_APP && - portnum != meshtastic_PortNum_TRACEROUTE_APP; -} - uint32_t djb2HashText(const char* text) { if (!text) diff --git a/modules/core_chat/tests/test_meshtastic_protocol_policy.cpp b/modules/core_chat/tests/test_meshtastic_protocol_policy.cpp index fd53fc0a..245a45db 100644 --- a/modules/core_chat/tests/test_meshtastic_protocol_policy.cpp +++ b/modules/core_chat/tests/test_meshtastic_protocol_policy.cpp @@ -1,4 +1,5 @@ #include "chat/infra/meshtastic/mt_mqtt_proxy_runtime.h" +#include "chat/infra/meshtastic/mt_protocol_helpers.h" #include "chat/runtime/meshtastic_protocol_policy.h" #include @@ -12,6 +13,7 @@ int main() using chat::meshtastic::mqttProxyRejectReasonName; using chat::meshtastic::mqttProxyRuntimeEnabled; using chat::meshtastic::MqttProxyRuntimeSettings; + using chat::meshtastic::requirePkiForDirectPort; using chat::meshtastic::resolveMqttProxyDownlinkChannel; using chat::meshtastic::shouldPublishToMqtt; using chat::meshtastic::validateMqttDecodedDownlinkPayload; @@ -33,6 +35,15 @@ int main() using chat::runtime::resolveMeshtasticPositionReplyPolicy; using chat::runtime::resolveMeshtasticTraceRouteReplyPolicy; + { + assert(requirePkiForDirectPort(0x12345678UL, + meshtastic_PortNum_TEXT_MESSAGE_APP)); + assert(!requirePkiForDirectPort(kMeshtasticBroadcastNode, + meshtastic_PortNum_TEXT_MESSAGE_APP)); + assert(!requirePkiForDirectPort(0x12345678UL, + meshtastic_PortNum_NODEINFO_APP)); + } + { const auto policy = resolveMeshtasticAppDataSendPolicy(0, true, true); assert(policy.wire_dest == kMeshtasticBroadcastNode); diff --git a/modules/core_sys/include/platform/ui/setting_sensitivity.h b/modules/core_sys/include/platform/ui/setting_sensitivity.h new file mode 100644 index 00000000..6aeca657 --- /dev/null +++ b/modules/core_sys/include/platform/ui/setting_sensitivity.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +namespace platform::ui::settings +{ + +enum class Sensitivity +{ + Public = 0, + Secret, +}; + +inline bool contains_case_insensitive(const char* text, const char* needle) +{ + if (!text || !needle || needle[0] == '\0') + { + return false; + } + const std::size_t text_len = std::strlen(text); + const std::size_t needle_len = std::strlen(needle); + if (needle_len > text_len) + { + return false; + } + for (std::size_t i = 0; i + needle_len <= text_len; ++i) + { + std::size_t j = 0; + for (; j < needle_len; ++j) + { + const auto lhs = static_cast(text[i + j]); + const auto rhs = static_cast(needle[j]); + if (std::tolower(lhs) != std::tolower(rhs)) + { + break; + } + } + if (j == needle_len) + { + return true; + } + } + return false; +} + +inline Sensitivity sensitivity_for_key(const char* key) +{ + static constexpr const char* kSecretMarkers[] = { + "password", + "passwd", + "pass", + "psk", + "private", + "secret", + "token", + "credential", + "_key", + }; + for (const char* marker : kSecretMarkers) + { + if (contains_case_insensitive(key, marker)) + { + return Sensitivity::Secret; + } + } + return Sensitivity::Public; +} + +inline const char* diagnostic_value(const char* key, const char* value) +{ + return sensitivity_for_key(key) == Sensitivity::Secret + ? "" + : (value ? value : ""); +} + +} // namespace platform::ui::settings diff --git a/modules/core_sys/include/platform/ui/wifi_access_runtime.h b/modules/core_sys/include/platform/ui/wifi_access_runtime.h index 7daa325e..3a3de257 100644 --- a/modules/core_sys/include/platform/ui/wifi_access_runtime.h +++ b/modules/core_sys/include/platform/ui/wifi_access_runtime.h @@ -61,6 +61,7 @@ enum class Decision : std::uint8_t OtaExclusive, CallExclusive, NonPreemptibleBusy, + ConnectDeferredForResources, }; enum class ExclusiveOwner : std::uint8_t diff --git a/modules/core_sys/include/platform/ui/wifi_runtime.h b/modules/core_sys/include/platform/ui/wifi_runtime.h index 8194be3e..469aee03 100644 --- a/modules/core_sys/include/platform/ui/wifi_runtime.h +++ b/modules/core_sys/include/platform/ui/wifi_runtime.h @@ -20,6 +20,7 @@ enum class ConnectionState : uint8_t Connecting, Connected, Error, + ResourceDeferred, }; struct Config diff --git a/modules/core_team/src/usecase/team_service.cpp b/modules/core_team/src/usecase/team_service.cpp index 06b9e0f4..8bbc1c9d 100644 --- a/modules/core_team/src/usecase/team_service.cpp +++ b/modules/core_team/src/usecase/team_service.cpp @@ -166,13 +166,11 @@ void logTeamTransferLeader(const team::proto::TeamTransferLeader& msg, const cha void logTeamKeyDist(const team::proto::TeamKeyDist& msg, const char* dir) { - std::string psk_hex = toHex(msg.channel_psk.data(), msg.channel_psk_len, msg.channel_psk_len); - TEAM_LOG("[TEAM] %s KeyDist team_id=%s key_id=%lu psk_len=%u psk_hex=%s\n", + TEAM_LOG("[TEAM] %s KeyDist team_id=%s key_id=%lu psk_len=%u psk=\n", dir, hexFromArray(msg.team_id).c_str(), static_cast(msg.key_id), - static_cast(msg.channel_psk_len), - psk_hex.c_str()); + static_cast(msg.channel_psk_len)); } void logTeamKeyRequest(const team::proto::TeamKeyRequest& msg, const char* dir) diff --git a/modules/ui_shared/include/ui/screens/node_info/node_info_page_components.h b/modules/ui_shared/include/ui/screens/node_info/node_info_page_components.h index 115ad85d..f8715254 100644 --- a/modules/ui_shared/include/ui/screens/node_info/node_info_page_components.h +++ b/modules/ui_shared/include/ui/screens/node_info/node_info_page_components.h @@ -55,6 +55,8 @@ struct NodeInfoWidgets lv_obj_t* zoom_status_label = nullptr; lv_obj_t* layer_btn = nullptr; lv_obj_t* layer_label = nullptr; + lv_obj_t* help_btn = nullptr; + lv_obj_t* help_label = nullptr; lv_obj_t* info_labels[kNodeInfoInfoLineCount]{}; ::ui::widgets::map::Widgets map_viewport{}; @@ -75,6 +77,12 @@ void destroy(); */ const NodeInfoWidgets& widgets(); +/** + * Register the complete NodeInfo interaction surface in navigation order. + * Callers no longer need to know which map controls are focusable. + */ +void bind_input_group(lv_group_t* group); + /** * @brief Update UI widgets with NodeInfo data. */ diff --git a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp index da55a56e..937b35d2 100644 --- a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp @@ -2517,9 +2517,9 @@ static void open_reticulum_node_info_screen(const chat::contacts::PeerDirectoryI { g_contacts_state.node_info_group = lv_group_create(); } - lv_group_remove_all_objs(g_contacts_state.node_info_group); g_contacts_state.node_info_prev_group = lv_group_get_default(); set_default_group(g_contacts_state.node_info_group); + node_info::ui::bind_input_group(g_contacts_state.node_info_group); if (s_reticulum_node_info_top_bar.back_btn) { lv_group_add_obj(g_contacts_state.node_info_group, s_reticulum_node_info_top_bar.back_btn); @@ -2637,17 +2637,22 @@ static void open_node_info_screen_for_node(uint32_t node_id) if (widgets.back_btn) { - lv_group_add_obj(g_contacts_state.node_info_group, widgets.back_btn); - lv_group_focus_obj(widgets.back_btn); lv_obj_add_event_cb(widgets.back_btn, on_node_info_back_clicked, LV_EVENT_CLICKED, nullptr); lv_obj_add_event_cb(widgets.back_btn, on_node_info_key, LV_EVENT_KEY, nullptr); CONTACTS_NODE_INFO_LOG("back button wired and focused back_btn=%p\n", widgets.back_btn); } - if (widgets.layer_btn) + lv_obj_t* node_info_controls[] = { + widgets.zoom_out_btn, + widgets.zoom_in_btn, + widgets.layer_btn, + widgets.help_btn, + }; + for (lv_obj_t* control : node_info_controls) { - lv_group_add_obj(g_contacts_state.node_info_group, widgets.layer_btn); - lv_obj_add_event_cb(widgets.layer_btn, on_node_info_key, LV_EVENT_KEY, nullptr); - CONTACTS_NODE_INFO_LOG("layer button added layer_btn=%p\n", widgets.layer_btn); + if (control) + { + lv_obj_add_event_cb(control, on_node_info_key, LV_EVENT_KEY, nullptr); + } } if (g_contacts_state.root) diff --git a/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp b/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp index 801c2571..7229857d 100644 --- a/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp +++ b/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp @@ -890,8 +890,19 @@ void on_connect_wifi_clicked(lv_event_t* event) return; } - ::ui::feedback::show_notice(::ui::i18n::tr("Wi-Fi connected"), 2000); - refresh_catalog_and_render(); + const platform::ui::wifi::Status wifi = platform::ui::wifi::status(); + if (wifi.connected) + { + ::ui::feedback::show_notice(::ui::i18n::tr("Wi-Fi connected"), 2000); + refresh_catalog_and_render(); + return; + } + + const char* text = + wifi.message[0] ? wifi.message : ::ui::i18n::tr("Wi-Fi connecting"); + set_status_text(text); + ::ui::feedback::show_notice(text, 2500); + update_top_bar_status(); } void show_message_body(const char* text) diff --git a/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp b/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp index 525c02d5..54112dd1 100644 --- a/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp @@ -13,6 +13,7 @@ #include "sys/clock.h" #include "ui/app_runtime.h" #include "ui/assets/fonts/font_utils.h" +#include "ui/components/shortcut_help_modal.h" #include "ui/localization.h" #include "ui/menu/dashboard/dashboard_style.h" #include "ui/page/page_profile.h" @@ -52,6 +53,8 @@ namespace map_viewport = ::ui::widgets::map; NodeInfoWidgets s_widgets; ::ui::widgets::TopBar s_top_bar; +::ui::components::shortcut_help_modal::State s_help_modal; +lv_group_t* s_input_group = nullptr; struct NodeInfoRuntimeState { @@ -70,6 +73,30 @@ struct NodeInfoRuntimeState NodeInfoRuntimeState s_state; +void on_help_button_clicked(lv_event_t*) +{ + using namespace ::ui::components::shortcut_help_modal; + if (is_open(s_help_modal)) + { + close(s_help_modal); + return; + } + static constexpr Row kRows[] = { + {"-", nullptr, "Zoom out"}, + {"+", nullptr, "Zoom in"}, + {"Touch", "Drag", "Pan the map"}, + {"Layer", nullptr, "Change offline map layer"}, + {"No tiles", nullptr, "Install offline map files on SD"}, + {"Back", nullptr, "Return to Contacts"}, + }; + Config config{}; + config.title = "Node Map Help"; + config.rows = kRows; + config.row_count = sizeof(kRows) / sizeof(kRows[0]); + config.restore_group = s_input_group; + (void)open(s_help_modal, s_widgets.root, config); +} + struct LayerPopupState { lv_obj_t* bg = nullptr; @@ -1582,6 +1609,20 @@ void position_overlay_widgets() { lv_obj_center(s_widgets.layer_label); } + apply_layer_button_style(s_widgets.help_btn, s_widgets.help_label, metrics.compact); + if (valid_obj(s_widgets.help_btn)) + { + lv_obj_set_size(s_widgets.help_btn, metrics.layer_w, metrics.layer_h); + lv_obj_set_pos(s_widgets.help_btn, + std::max(metrics.pad, + layer_x - metrics.layer_w - + metrics.zoom_gap), + layer_y); + } + if (valid_obj(s_widgets.help_label)) + { + lv_obj_center(s_widgets.help_label); + } log_scene_widgets("position_overlay_widgets"); } @@ -2091,6 +2132,9 @@ NodeInfoWidgets create(lv_obj_t* parent) s_widgets.layer_btn = lv_btn_create(s_widgets.map_stage); s_widgets.layer_label = lv_label_create(s_widgets.layer_btn); lv_label_set_text(s_widgets.layer_label, ::ui::i18n::tr("Layer")); + s_widgets.help_btn = lv_btn_create(s_widgets.map_stage); + s_widgets.help_label = lv_label_create(s_widgets.help_btn); + lv_label_set_text(s_widgets.help_label, ::ui::i18n::tr("Help")); apply_zoom_button_style(s_widgets.zoom_in_btn, s_widgets.zoom_in_label); apply_zoom_button_style(s_widgets.zoom_out_btn, s_widgets.zoom_out_label); apply_layer_button_style(s_widgets.layer_btn, s_widgets.layer_label, view_metrics().compact); @@ -2103,6 +2147,10 @@ NodeInfoWidgets create(lv_obj_t* parent) LV_EVENT_CLICKED, reinterpret_cast(static_cast(-1))); lv_obj_add_event_cb(s_widgets.layer_btn, on_layer_button_clicked, LV_EVENT_CLICKED, nullptr); + lv_obj_add_event_cb(s_widgets.help_btn, + on_help_button_clicked, + LV_EVENT_CLICKED, + nullptr); update_zoom_button_state(false); lv_obj_update_layout(s_widgets.root); @@ -2119,6 +2167,8 @@ void destroy() s_widgets.root, (s_widgets.root && lv_obj_is_valid(s_widgets.root)) ? 1 : 0); close_layer_popup(); + ::ui::components::shortcut_help_modal::close(s_help_modal); + s_input_group = nullptr; if (s_layer_popup.group) { lv_group_del(s_layer_popup.group); @@ -2143,6 +2193,34 @@ const NodeInfoWidgets& widgets() return s_widgets; } +void bind_input_group(lv_group_t* group) +{ + s_input_group = group; + if (!group) + { + return; + } + lv_group_remove_all_objs(group); + lv_obj_t* controls[] = { + s_widgets.back_btn, + s_widgets.zoom_out_btn, + s_widgets.zoom_in_btn, + s_widgets.layer_btn, + s_widgets.help_btn, + }; + for (lv_obj_t* control : controls) + { + if (control && lv_obj_is_valid(control)) + { + lv_group_add_obj(group, control); + } + } + if (s_widgets.back_btn && lv_obj_is_valid(s_widgets.back_btn)) + { + lv_group_focus_obj(s_widgets.back_btn); + } +} + void set_node_info(const chat::contacts::PeerDirectoryItem& node) { NODE_INFO_LOG("set_node_info begin\n"); diff --git a/modules/ui_shared/src/ui/widgets/map/map_viewport.cpp b/modules/ui_shared/src/ui/widgets/map/map_viewport.cpp index 31d34829..d72f0170 100644 --- a/modules/ui_shared/src/ui/widgets/map/map_viewport.cpp +++ b/modules/ui_shared/src/ui/widgets/map/map_viewport.cpp @@ -838,6 +838,7 @@ void destroy(Runtime& runtime) } cleanup_tiles(impl->tile_ctx); + release_tile_context(impl->tile_ctx); if (impl->widgets.root && lv_obj_is_valid(impl->widgets.root)) { diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/reticulum/reticulum_interfaces.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/reticulum/reticulum_interfaces.h index 5925cc89..7987be97 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/reticulum/reticulum_interfaces.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/reticulum/reticulum_interfaces.h @@ -17,6 +17,7 @@ #include #if __has_include() +#include "platform/esp/arduino_common/net/async_tcp_connector.h" #include #define TRAIL_MATE_RETICULUM_WIFI_CLIENT_AVAILABLE 1 #else @@ -132,7 +133,6 @@ class WifiGatewayReticulumInterface static constexpr uint8_t kHdlcEscape = 0x7D; static constexpr uint8_t kHdlcEscapeMask = 0x20; static constexpr uint32_t kReconnectIntervalMs = 10000; - static constexpr uint32_t kWifiConnectIntervalMs = 60000; static constexpr uint32_t kRxStatsLogIntervalMs = 5000; static constexpr int32_t kSocketConnectTimeoutMs = 5000; @@ -145,7 +145,6 @@ class WifiGatewayReticulumInterface bool socket_online_ = false; bool socket_open_pending_ = false; uint32_t last_reconnect_ms_ = 0; - uint32_t last_wifi_connect_ms_ = 0; uint32_t last_socket_read_ms_ = 0; bool hdlc_in_frame_ = false; bool hdlc_escape_ = false; @@ -166,13 +165,11 @@ class WifiGatewayReticulumInterface #if TRAIL_MATE_RETICULUM_WIFI_CLIENT_AVAILABLE WiFiClient client_; + platform::esp::arduino_common::net::AsyncTcpConnector connector_; #endif void stop(); bool connected() const; -#if TRAIL_MATE_RETICULUM_WIFI_CLIENT_AVAILABLE - bool resolveHost(IPAddress* out); -#endif void syncSocketState(); bool ensureSocket(); void readAvailable(); @@ -238,7 +235,6 @@ class AutoReticulumInterface uint8_t discovery_token_[reticulum::kFullHashSize] = {}; uint32_t last_announce_ms_ = 0; uint32_t last_socket_attempt_ms_ = 0; - uint32_t last_wifi_connect_ms_ = 0; std::array peers_{}; RxPacket rx_scratch_{}; sys::RingBuffer rx_queue_; diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/net/async_tcp_connector.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/net/async_tcp_connector.h new file mode 100644 index 00000000..b2053d00 --- /dev/null +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/net/async_tcp_connector.h @@ -0,0 +1,77 @@ +#pragma once + +#include + +namespace platform::esp::arduino_common::net +{ + +enum class TcpConnectPhase : std::uint8_t +{ + Idle = 0, + Resolving, + Connecting, + Connected, + Failed, +}; + +enum class TcpConnectFailure : std::uint8_t +{ + None = 0, + InvalidEndpoint, + ResolverBusy, + ResolveFailed, + SocketOpenFailed, + ConnectFailed, + TimedOut, + Cancelled, +}; + +struct TcpConnectStatus +{ + TcpConnectPhase phase = TcpConnectPhase::Idle; + TcpConnectFailure failure = TcpConnectFailure::None; + int detail = 0; +}; + +/** + * A bounded, task-free DNS + TCP connect state machine. + * + * DNS completion storage lives in a process-lifetime broker, so cancelling or + * destroying a connector cannot leave lwIP with a dangling callback pointer. + * poll() never waits: callers can safely invoke it from an LVGL/runtime tick. + */ +class AsyncTcpConnector final +{ + public: + AsyncTcpConnector() = default; + ~AsyncTcpConnector(); + + AsyncTcpConnector(const AsyncTcpConnector&) = delete; + AsyncTcpConnector& operator=(const AsyncTcpConnector&) = delete; + + bool start(const char* host, + std::uint16_t port, + std::uint32_t now_ms, + std::uint32_t timeout_ms); + TcpConnectStatus poll(std::uint32_t now_ms); + void cancel(); + + TcpConnectStatus status() const { return status_; } + bool pending() const; + int takeSocket(); + + private: + bool beginSocket(std::uint32_t ipv4_address, std::uint32_t now_ms); + void fail(TcpConnectFailure failure, int detail); + + TcpConnectStatus status_{}; + int socket_ = -1; + int dns_slot_ = -1; + std::uint32_t dns_generation_ = 0; + std::uint32_t deadline_ms_ = 0; + std::uint16_t port_ = 0; +}; + +const char* tcpConnectFailureName(TcpConnectFailure failure); + +} // namespace platform::esp::arduino_common::net diff --git a/platform/esp/arduino_common/include/ui/widgets/map/map_tiles.h b/platform/esp/arduino_common/include/ui/widgets/map/map_tiles.h index 2a5a347f..d83faf3c 100644 --- a/platform/esp/arduino_common/include/ui/widgets/map/map_tiles.h +++ b/platform/esp/arduino_common/include/ui/widgets/map/map_tiles.h @@ -89,6 +89,7 @@ struct TileContext ui::map_tiles::MapTileRenderQueue* render_queue; bool* has_map_data; // Global: any tile ever loaded bool* has_visible_map_data; // Viewport: current visible tiles have PNG + bool runtime_acquired = false; }; // Core tile functions - implemented in map_tiles.cpp @@ -213,4 +214,11 @@ void init_tile_context(TileContext& ctx, lv_obj_t* map_container, MapAnchor* anc */ void cleanup_tiles(TileContext& ctx); +/** + * Release the asynchronous tile service lease owned by this context. + * cleanup_tiles() intentionally keeps the lease because it is also used for + * ordinary viewport refreshes. + */ +void release_tile_context(TileContext& ctx); + #endif // MAP_TILES_H diff --git a/platform/esp/arduino_common/src/app_config_store.cpp b/platform/esp/arduino_common/src/app_config_store.cpp index f4963f38..217ac540 100644 --- a/platform/esp/arduino_common/src/app_config_store.cpp +++ b/platform/esp/arduino_common/src/app_config_store.cpp @@ -8,6 +8,7 @@ #include #include +#include "platform/ui/setting_sensitivity.h" #include "platform/ui/settings_store.h" namespace app @@ -290,7 +291,9 @@ String get_string_logged(Preferences& prefs, safe_label(key), exists ? "stored" : "default", static_cast(value.length()), - safe_label(value.c_str())); + ::platform::ui::settings::diagnostic_value( + key, + value.c_str())); } return value; } @@ -459,7 +462,9 @@ bool put_string_logged(Preferences& prefs, safe_label(key), static_cast(expected), bool_label(ok), - safe_value); + ::platform::ui::settings::diagnostic_value( + key, + safe_value)); } return ok; } diff --git a/platform/esp/arduino_common/src/chat/infra/mesh_mqtt_client_runtime.cpp b/platform/esp/arduino_common/src/chat/infra/mesh_mqtt_client_runtime.cpp index 3d5e342d..59aac4e0 100644 --- a/platform/esp/arduino_common/src/chat/infra/mesh_mqtt_client_runtime.cpp +++ b/platform/esp/arduino_common/src/chat/infra/mesh_mqtt_client_runtime.cpp @@ -19,9 +19,9 @@ #include #include #include -#include #include +#include "platform/esp/arduino_common/net/async_tcp_connector.h" #ifndef TRAIL_MATE_ENABLE_BLE #define TRAIL_MATE_ENABLE_BLE 0 #endif @@ -45,7 +45,6 @@ namespace constexpr uint16_t kDefaultMqttPort = 1883; constexpr uint16_t kMqttKeepaliveSeconds = 60; constexpr uint32_t kConfigRefreshMs = 5000; -constexpr uint32_t kWifiConnectIntervalMs = 30000; constexpr uint32_t kMqttReconnectIntervalMs = 15000; constexpr uint32_t kMqttPingIntervalMs = 30000; constexpr int32_t kMqttSocketConnectTimeoutMs = 5000; @@ -314,7 +313,6 @@ class PlainMqttRuntime bool logged_plaintext_forced_ = false; uint16_t packet_id_ = 1; uint32_t last_config_refresh_ms_ = 0; - uint32_t last_wifi_connect_ms_ = 0; uint32_t last_mqtt_reconnect_ms_ = 0; uint32_t last_io_ms_ = 0; char address_scratch_[80] = {}; @@ -336,6 +334,7 @@ class PlainMqttRuntime #if TRAIL_MATE_MESH_MQTT_HAS_WIFI_CLIENT WiFiClient client_; + platform::esp::arduino_common::net::AsyncTcpConnector connector_{}; #endif const char* protocolTag() const @@ -453,6 +452,9 @@ class PlainMqttRuntime const bool was_configured = config_.configured; config_ = next; have_config_ = true; +#if TRAIL_MATE_MESH_MQTT_HAS_WIFI_CLIENT + connector_.cancel(); +#endif logged_tls_unsupported_ = false; logged_plaintext_forced_ = false; stop("config"); @@ -674,14 +676,12 @@ class PlainMqttRuntime if (!status.enabled) { stop("wifi_disabled"); - last_wifi_connect_ms_ = 0; logWifiGate(status, WifiGateState::Disabled); return false; } if (!status.has_credentials) { stop("wifi_no_credentials"); - last_wifi_connect_ms_ = 0; logWifiGate(status, WifiGateState::NoCredentials); return false; } @@ -691,12 +691,6 @@ class PlainMqttRuntime return true; } - if (!elapsed(now_ms, last_wifi_connect_ms_, kWifiConnectIntervalMs)) - { - logWifiGate(status, WifiGateState::WaitingForConnection); - return false; - } - last_wifi_connect_ms_ = now_ms; std::printf("[%s][MQTT] requesting Wi-Fi access for MQTT ssid=%s\n", protocolTag(), status.ssid[0] ? status.ssid : ""); @@ -786,7 +780,10 @@ class PlainMqttRuntime { return true; } - if (!elapsed(now_ms, last_mqtt_reconnect_ms_, kMqttReconnectIntervalMs)) + if (!connector_.pending() && + connector_.status().phase != + platform::esp::arduino_common::net::TcpConnectPhase::Connected && + !elapsed(now_ms, last_mqtt_reconnect_ms_, kMqttReconnectIntervalMs)) { return false; } @@ -795,8 +792,6 @@ class PlainMqttRuntime return false; } - last_mqtt_reconnect_ms_ = now_ms; - resetConnectionState(); if (config_.host[0] == '\0') { return false; @@ -827,29 +822,62 @@ class PlainMqttRuntime return false; } - IPAddress remote_ip{}; - if (!resolveHost(&remote_ip)) + using platform::esp::arduino_common::net::TcpConnectPhase; + if (!connector_.pending() && + connector_.status().phase != TcpConnectPhase::Connected) { - return false; - } - - client_.stop(); - client_.setNoDelay(true); - std::printf("[%s][MQTT] broker connect host=%s port=%u client=%s\n", - protocolTag(), - config_.host, - static_cast(config_.port), - config_.client_id); - if (!client_.connect(remote_ip, config_.port, kMqttSocketConnectTimeoutMs)) - { - std::printf("[%s][MQTT] broker connect failed host=%s port=%u\n", + last_mqtt_reconnect_ms_ = now_ms; + resetConnectionState(); + client_.stop(); + std::printf("[%s][MQTT] broker connect start host=%s port=%u client=%s\n", protocolTag(), config_.host, - static_cast(config_.port)); - client_.stop(); + static_cast(config_.port), + config_.client_id); + if (!connector_.start(config_.host, + config_.port, + now_ms, + static_cast( + kMqttSocketConnectTimeoutMs))) + { + const auto status = connector_.status(); + std::printf("[%s][MQTT] broker connect failed host=%s port=%u stage=%s err=%d\n", + protocolTag(), + config_.host, + static_cast(config_.port), + platform::esp::arduino_common::net:: + tcpConnectFailureName(status.failure), + status.detail); + return false; + } + } + + const auto connect_status = connector_.poll(now_ms); + if (connect_status.phase == TcpConnectPhase::Resolving || + connect_status.phase == TcpConnectPhase::Connecting) + { + return false; + } + if (connect_status.phase != TcpConnectPhase::Connected) + { + std::printf("[%s][MQTT] broker connect failed host=%s port=%u stage=%s err=%d\n", + protocolTag(), + config_.host, + static_cast(config_.port), + platform::esp::arduino_common::net:: + tcpConnectFailureName(connect_status.failure), + connect_status.detail); + connector_.cancel(); return false; } + const int socket = connector_.takeSocket(); + if (socket < 0) + { + return false; + } + client_ = WiFiClient(socket); + client_.setNoDelay(true); if (!sendConnect()) { client_.stop(); @@ -861,70 +889,10 @@ class PlainMqttRuntime #endif } -#if TRAIL_MATE_MESH_MQTT_HAS_WIFI_CLIENT - bool resolveHost(IPAddress* out) - { - if (!out || config_.host[0] == '\0') - { - return false; - } - - IPAddress parsed{}; - if (parsed.fromString(config_.host)) - { - *out = parsed; - return true; - } - - addrinfo hints{}; - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; - - addrinfo* results = nullptr; - const int err = getaddrinfo(config_.host, nullptr, &hints, &results); - if (err != 0 || !results) - { - std::printf("[%s][MQTT] broker resolve failed host=%s err=%d\n", - protocolTag(), - config_.host, - err); - if (results) - { - freeaddrinfo(results); - } - return false; - } - - bool resolved = false; - for (addrinfo* item = results; item != nullptr; item = item->ai_next) - { - if (item->ai_family != AF_INET || !item->ai_addr) - { - continue; - } - - const auto* addr = reinterpret_cast(item->ai_addr); - uint8_t bytes[4] = {}; - std::memcpy(bytes, &addr->sin_addr.s_addr, sizeof(bytes)); - *out = IPAddress(bytes); - resolved = true; - break; - } - - freeaddrinfo(results); - if (!resolved) - { - std::printf("[%s][MQTT] broker resolve no_ipv4 host=%s\n", - protocolTag(), - config_.host); - } - return resolved; - } -#endif - void stop(const char* reason) { #if TRAIL_MATE_MESH_MQTT_HAS_WIFI_CLIENT + connector_.cancel(); if (client_.connected()) { std::printf("[%s][MQTT] broker stop reason=%s\n", diff --git a/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp b/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp index 1fc69627..2e5bb8fc 100644 --- a/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp +++ b/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp @@ -61,7 +61,6 @@ constexpr uint8_t kDefaultPskIndex = 1; constexpr uint8_t kBitfieldWantResponseMask = 0x02; constexpr uint32_t kBroadcastNodeId = 0xFFFFFFFFu; -using chat::meshtastic::allowPkiForPortnum; using chat::meshtastic::computeHopsAway; using chat::meshtastic::computeKeyVerificationHashes; using chat::meshtastic::decryptPkiAesCcm; @@ -71,6 +70,7 @@ using chat::meshtastic::hashSharedKey; using chat::meshtastic::initPkiNonce; using chat::meshtastic::makeEncryptedPacketFromWire; using chat::meshtastic::readPbString; +using chat::meshtastic::requirePkiForDirectPort; using chat::meshtastic::shouldSetAirWantAck; static const char* portName(uint32_t portnum) @@ -112,15 +112,6 @@ static const char* portName(uint32_t portnum) } } -bool shouldRequireDirectPki(uint8_t encrypt_mode, uint32_t dest_node, uint32_t portnum) -{ - // Match upstream Meshtastic: direct unicast app traffic uses PKI outside - // cleartext/Ham-style operation, and must not silently fall back to channel crypto. - return encrypt_mode != 0 && - dest_node != kBroadcastNodeId && - allowPkiForPortnum(portnum); -} - chat::delivery::SendFailureKind failureKindFromRoutingError( meshtastic_Routing_Error reason) { @@ -581,7 +572,7 @@ bool MtAdapter::sendAppDataNow(ChannelId channel, uint32_t portnum, const uint8_t* out_payload = data_buffer.data(); size_t out_len = data_size; bool use_pki = false; - if (shouldRequireDirectPki(encrypt_mode_, dest_node, portnum)) + if (requirePkiForDirectPort(dest_node, portnum)) { const bool have_dest_key = findPkiNodeKey(dest_node) != nullptr; if (!pki_ready_ || !have_dest_key) @@ -3123,7 +3114,7 @@ bool MtAdapter::sendPacket(const PendingSend& pending) const uint8_t* psk = nullptr; size_t psk_len = 0; bool use_pki = false; - if (shouldRequireDirectPki(encrypt_mode_, dest, pending.portnum)) + if (requirePkiForDirectPort(dest, pending.portnum)) { const bool have_dest_key = findPkiNodeKey(dest) != nullptr; if (!pki_ready_ || !have_dest_key) @@ -3517,20 +3508,14 @@ void MtAdapter::updateChannelKeys() computeChannelHash(secondary_name, (secondary_psk_len_ > 0) ? secondary_psk_ : nullptr, secondary_psk_len_); - std::string primary_psk_hex = toHex(primary_psk_, primary_psk_len_, primary_psk_len_); - std::string secondary_psk_hex = toHex(secondary_psk_, secondary_psk_len_, secondary_psk_len_); - if (primary_psk_hex.empty()) primary_psk_hex = "-"; - if (secondary_psk_hex.empty()) secondary_psk_hex = "-"; - LORA_LOG("[LORA] channel primary='%s' hash=0x%02X psk=%u hex=%s\n", + LORA_LOG("[LORA] channel primary='%s' hash=0x%02X psk=%u key=\n", primary_name, primary_channel_hash_, - (unsigned)primary_psk_len_, - primary_psk_hex.c_str()); - LORA_LOG("[LORA] channel secondary='%s' hash=0x%02X psk=%u hex=%s\n", + (unsigned)primary_psk_len_); + LORA_LOG("[LORA] channel secondary='%s' hash=0x%02X psk=%u key=\n", secondary_name, secondary_channel_hash_, - (unsigned)secondary_psk_len_, - secondary_psk_hex.c_str()); + (unsigned)secondary_psk_len_); } void MtAdapter::startRadioReceive() diff --git a/platform/esp/arduino_common/src/chat/infra/reticulum/reticulum_interfaces.cpp b/platform/esp/arduino_common/src/chat/infra/reticulum/reticulum_interfaces.cpp index f5248f44..72f9a632 100644 --- a/platform/esp/arduino_common/src/chat/infra/reticulum/reticulum_interfaces.cpp +++ b/platform/esp/arduino_common/src/chat/infra/reticulum/reticulum_interfaces.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include @@ -272,7 +271,6 @@ void WifiGatewayReticulumInterface::setTransportEnabled(bool enabled) { stop(); last_reconnect_ms_ = 0; - last_wifi_connect_ms_ = 0; } } @@ -437,6 +435,7 @@ bool WifiGatewayReticulumInterface::pollPacket(RxPacket* out) void WifiGatewayReticulumInterface::stop() { #if TRAIL_MATE_RETICULUM_WIFI_CLIENT_AVAILABLE + connector_.cancel(); client_.stop(); #elif TRAIL_MATE_RETICULUM_C6_TCP_AVAILABLE if (socket_online_ || socket_open_pending_) @@ -453,64 +452,6 @@ bool WifiGatewayReticulumInterface::connected() const return socket_online_; } -#if TRAIL_MATE_RETICULUM_WIFI_CLIENT_AVAILABLE -bool WifiGatewayReticulumInterface::resolveHost(IPAddress* out) -{ - if (!out || host_[0] == '\0') - { - return false; - } - - IPAddress parsed{}; - if (parsed.fromString(host_)) - { - *out = parsed; - return true; - } - - addrinfo hints{}; - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; - - addrinfo* results = nullptr; - const int err = getaddrinfo(host_, nullptr, &hints, &results); - if (err != 0 || !results) - { - Serial.printf("[Reticulum][IF][WiFi] gateway resolve failed host=%s err=%d\n", - host_, - err); - if (results) - { - freeaddrinfo(results); - } - return false; - } - - bool resolved = false; - for (addrinfo* item = results; item != nullptr; item = item->ai_next) - { - if (item->ai_family != AF_INET || !item->ai_addr) - { - continue; - } - - const auto* addr = reinterpret_cast(item->ai_addr); - uint8_t bytes[4] = {}; - std::memcpy(bytes, &addr->sin_addr.s_addr, sizeof(bytes)); - *out = IPAddress(bytes); - resolved = true; - break; - } - - freeaddrinfo(results); - if (!resolved) - { - Serial.printf("[Reticulum][IF][WiFi] gateway resolve no_ipv4 host=%s\n", host_); - } - return resolved; -} -#endif - void WifiGatewayReticulumInterface::syncSocketState() { #if TRAIL_MATE_RETICULUM_WIFI_CLIENT_AVAILABLE @@ -575,7 +516,16 @@ bool WifiGatewayReticulumInterface::ensureSocket() return false; #else const uint32_t now_ms = millis(); - if (last_reconnect_ms_ != 0 && + const bool socket_connect_pending = +#if TRAIL_MATE_RETICULUM_WIFI_CLIENT_AVAILABLE + connector_.pending(); +#elif TRAIL_MATE_RETICULUM_C6_TCP_AVAILABLE + socket_open_pending_; +#else + false; +#endif + if (!socket_connect_pending && + last_reconnect_ms_ != 0 && (now_ms - last_reconnect_ms_) < kReconnectIntervalMs) { return false; @@ -584,11 +534,8 @@ bool WifiGatewayReticulumInterface::ensureSocket() platform::ui::wifi::Status wifi_status = platform::ui::wifi::status(); if (!wifi_status.connected) { - if (auto_connect_wifi_ && - (last_wifi_connect_ms_ == 0 || - (now_ms - last_wifi_connect_ms_) >= kWifiConnectIntervalMs)) + if (auto_connect_wifi_) { - last_wifi_connect_ms_ = now_ms; Serial.printf("[Reticulum][IF][WiFi] requesting station before gateway host=%s:%u\n", host_, static_cast(port_)); @@ -636,29 +583,57 @@ bool WifiGatewayReticulumInterface::ensureSocket() } #if TRAIL_MATE_RETICULUM_WIFI_CLIENT_AVAILABLE - client_.stop(); - - IPAddress remote_ip{}; - if (!resolveHost(&remote_ip)) + using platform::esp::arduino_common::net::TcpConnectPhase; + if (!connector_.pending() && + connector_.status().phase != TcpConnectPhase::Connected) { - socket_online_ = false; client_.stop(); - return false; - } - - Serial.printf("[Reticulum][IF][WiFi] gateway connect host=%s port=%u\n", - host_, - static_cast(port_)); - if (!client_.connect(remote_ip, port_, kSocketConnectTimeoutMs)) - { - socket_online_ = false; - client_.stop(); - Serial.printf("[Reticulum][IF][WiFi] gateway connect failed host=%s port=%u\n", + Serial.printf("[Reticulum][IF][WiFi] gateway connect start host=%s port=%u\n", host_, static_cast(port_)); + if (!connector_.start(host_, + port_, + now_ms, + static_cast(kSocketConnectTimeoutMs))) + { + const auto status = connector_.status(); + Serial.printf("[Reticulum][IF][WiFi] gateway connect failed host=%s port=%u stage=%s err=%d\n", + host_, + static_cast(port_), + platform::esp::arduino_common::net:: + tcpConnectFailureName(status.failure), + status.detail); + return false; + } + } + + const auto connect_status = connector_.poll(now_ms); + if (connect_status.phase == TcpConnectPhase::Resolving || + connect_status.phase == TcpConnectPhase::Connecting) + { + socket_open_pending_ = true; + return true; + } + if (connect_status.phase != TcpConnectPhase::Connected) + { + connector_.cancel(); + socket_open_pending_ = false; + Serial.printf("[Reticulum][IF][WiFi] gateway connect failed host=%s port=%u stage=%s err=%d\n", + host_, + static_cast(port_), + platform::esp::arduino_common::net:: + tcpConnectFailureName(connect_status.failure), + connect_status.detail); return false; } + const int socket = connector_.takeSocket(); + if (socket < 0) + { + socket_open_pending_ = false; + return false; + } + client_ = WiFiClient(socket); client_.setNoDelay(true); socket_online_ = true; hdlc_in_frame_ = false; @@ -958,7 +933,6 @@ void AutoReticulumInterface::applyConfig( { stop(); last_socket_attempt_ms_ = 0; - last_wifi_connect_ms_ = 0; rx_queue_.clear(); } Serial.printf("[Reticulum][IF][Auto] enabled=%s group=%s discovery=%u data=%u available=%s\n", @@ -988,11 +962,8 @@ void AutoReticulumInterface::maintain() platform::ui::wifi::Status wifi_status = platform::ui::wifi::status(); const uint32_t now_ms = millis(); - if (!wifi_status.connected && auto_connect_wifi_ && - (last_wifi_connect_ms_ == 0 || - now_ms - last_wifi_connect_ms_ >= 60000U)) + if (!wifi_status.connected && auto_connect_wifi_) { - last_wifi_connect_ms_ = now_ms; platform::ui::wifi_access::Request request{}; request.client = platform::ui::wifi_access::Client::ReticulumGateway; request.kind = platform::ui::wifi_access::AccessKind::WifiConnect; diff --git a/platform/esp/arduino_common/src/net/async_tcp_connector.cpp b/platform/esp/arduino_common/src/net/async_tcp_connector.cpp new file mode 100644 index 00000000..7b386b3c --- /dev/null +++ b/platform/esp/arduino_common/src/net/async_tcp_connector.cpp @@ -0,0 +1,447 @@ +#include "platform/esp/arduino_common/net/async_tcp_connector.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace platform::esp::arduino_common::net +{ +namespace +{ + +constexpr std::size_t kDnsSlotCount = 4; + +struct DnsSlot +{ + bool in_use = false; + bool abandoned = false; + bool complete = false; + std::uint32_t generation = 0; + std::uint32_t address = 0; + int error = 0; +}; + +std::array s_dns_slots{}; +portMUX_TYPE s_dns_lock = portMUX_INITIALIZER_UNLOCKED; +std::uint32_t s_dns_generation = 1; + +void release_slot_locked(DnsSlot& slot) +{ + slot = {}; +} + +void dns_found(const char*, const ip_addr_t* address, void* arg) +{ + auto* slot = static_cast(arg); + if (!slot) + { + return; + } + + portENTER_CRITICAL(&s_dns_lock); + if (slot->in_use) + { + if (slot->abandoned) + { + release_slot_locked(*slot); + } + else + { + slot->complete = true; + if (address && IP_IS_V4(address)) + { + slot->address = ip4_addr_get_u32(ip_2_ip4(address)); + slot->error = 0; + } + else + { + slot->address = 0; + slot->error = ERR_VAL; + } + } + } + portEXIT_CRITICAL(&s_dns_lock); +} + +int claim_dns_slot(std::uint32_t& generation) +{ + int index = -1; + portENTER_CRITICAL(&s_dns_lock); + for (std::size_t i = 0; i < s_dns_slots.size(); ++i) + { + if (s_dns_slots[i].in_use) + { + continue; + } + ++s_dns_generation; + if (s_dns_generation == 0) + { + s_dns_generation = 1; + } + s_dns_slots[i] = {}; + s_dns_slots[i].in_use = true; + s_dns_slots[i].generation = s_dns_generation; + generation = s_dns_generation; + index = static_cast(i); + break; + } + portEXIT_CRITICAL(&s_dns_lock); + return index; +} + +void abandon_dns_slot(int index, std::uint32_t generation) +{ + if (index < 0 || static_cast(index) >= s_dns_slots.size()) + { + return; + } + portENTER_CRITICAL(&s_dns_lock); + DnsSlot& slot = s_dns_slots[static_cast(index)]; + if (slot.in_use && slot.generation == generation) + { + if (slot.complete) + { + release_slot_locked(slot); + } + else + { + slot.abandoned = true; + } + } + portEXIT_CRITICAL(&s_dns_lock); +} + +void release_dns_slot(int index, std::uint32_t generation) +{ + if (index < 0 || static_cast(index) >= s_dns_slots.size()) + { + return; + } + portENTER_CRITICAL(&s_dns_lock); + DnsSlot& slot = s_dns_slots[static_cast(index)]; + if (slot.in_use && slot.generation == generation) + { + release_slot_locked(slot); + } + portEXIT_CRITICAL(&s_dns_lock); +} + +bool take_dns_result(int index, + std::uint32_t generation, + bool& complete, + std::uint32_t& address, + int& error) +{ + complete = false; + if (index < 0 || static_cast(index) >= s_dns_slots.size()) + { + return false; + } + + bool valid = false; + portENTER_CRITICAL(&s_dns_lock); + DnsSlot& slot = s_dns_slots[static_cast(index)]; + if (slot.in_use && slot.generation == generation && !slot.abandoned) + { + valid = true; + complete = slot.complete; + if (complete) + { + address = slot.address; + error = slot.error; + release_slot_locked(slot); + } + } + portEXIT_CRITICAL(&s_dns_lock); + return valid; +} + +bool deadline_elapsed(std::uint32_t now_ms, std::uint32_t deadline_ms) +{ + return deadline_ms != 0 && + static_cast(now_ms - deadline_ms) >= 0; +} + +} // namespace + +AsyncTcpConnector::~AsyncTcpConnector() +{ + cancel(); +} + +bool AsyncTcpConnector::start(const char* host, + std::uint16_t port, + std::uint32_t now_ms, + std::uint32_t timeout_ms) +{ + cancel(); + if (!host || host[0] == '\0' || port == 0 || timeout_ms == 0) + { + fail(TcpConnectFailure::InvalidEndpoint, EINVAL); + return false; + } + + port_ = port; + deadline_ms_ = now_ms + timeout_ms; + + ip4_addr_t parsed{}; + if (ip4addr_aton(host, &parsed) != 0) + { + return beginSocket(ip4_addr_get_u32(&parsed), now_ms); + } + + dns_slot_ = claim_dns_slot(dns_generation_); + if (dns_slot_ < 0) + { + fail(TcpConnectFailure::ResolverBusy, ERR_MEM); + return false; + } + + ip_addr_t resolved{}; + const err_t err = dns_gethostbyname( + host, + &resolved, + dns_found, + &s_dns_slots[static_cast(dns_slot_)]); + if (err == ERR_OK) + { + // A cached answer completes synchronously and never calls dns_found(). + // Release the broker slot directly instead of marking it abandoned. + release_dns_slot(dns_slot_, dns_generation_); + dns_slot_ = -1; + if (!IP_IS_V4(&resolved)) + { + fail(TcpConnectFailure::ResolveFailed, ERR_VAL); + return false; + } + return beginSocket(ip4_addr_get_u32(ip_2_ip4(&resolved)), now_ms); + } + if (err != ERR_INPROGRESS) + { + // No callback is pending for an immediate resolver failure. + release_dns_slot(dns_slot_, dns_generation_); + dns_slot_ = -1; + fail(TcpConnectFailure::ResolveFailed, err); + return false; + } + + status_.phase = TcpConnectPhase::Resolving; + status_.failure = TcpConnectFailure::None; + status_.detail = 0; + return true; +} + +TcpConnectStatus AsyncTcpConnector::poll(std::uint32_t now_ms) +{ + if (status_.phase == TcpConnectPhase::Resolving) + { + if (deadline_elapsed(now_ms, deadline_ms_)) + { + abandon_dns_slot(dns_slot_, dns_generation_); + dns_slot_ = -1; + fail(TcpConnectFailure::TimedOut, ETIMEDOUT); + return status_; + } + + bool complete = false; + std::uint32_t address = 0; + int error = 0; + if (!take_dns_result(dns_slot_, + dns_generation_, + complete, + address, + error)) + { + dns_slot_ = -1; + fail(TcpConnectFailure::ResolveFailed, ERR_VAL); + return status_; + } + if (!complete) + { + return status_; + } + dns_slot_ = -1; + if (error != 0 || address == 0) + { + fail(TcpConnectFailure::ResolveFailed, error); + return status_; + } + (void)beginSocket(address, now_ms); + } + + if (status_.phase != TcpConnectPhase::Connecting) + { + return status_; + } + if (deadline_elapsed(now_ms, deadline_ms_)) + { + fail(TcpConnectFailure::TimedOut, ETIMEDOUT); + return status_; + } + + fd_set writable; + fd_set failed; + FD_ZERO(&writable); + FD_ZERO(&failed); + FD_SET(socket_, &writable); + FD_SET(socket_, &failed); + timeval timeout{}; + const int selected = + select(socket_ + 1, nullptr, &writable, &failed, &timeout); + if (selected < 0) + { + fail(TcpConnectFailure::ConnectFailed, errno); + return status_; + } + if (selected == 0) + { + return status_; + } + + int socket_error = 0; + socklen_t error_len = sizeof(socket_error); + if (getsockopt(socket_, + SOL_SOCKET, + SO_ERROR, + &socket_error, + &error_len) != 0 || + socket_error != 0 || + FD_ISSET(socket_, &failed)) + { + fail(TcpConnectFailure::ConnectFailed, + socket_error != 0 ? socket_error : errno); + return status_; + } + + status_.phase = TcpConnectPhase::Connected; + status_.failure = TcpConnectFailure::None; + status_.detail = 0; + return status_; +} + +void AsyncTcpConnector::cancel() +{ + if (dns_slot_ >= 0) + { + abandon_dns_slot(dns_slot_, dns_generation_); + } + dns_slot_ = -1; + dns_generation_ = 0; + if (socket_ >= 0) + { + close(socket_); + } + socket_ = -1; + port_ = 0; + deadline_ms_ = 0; + status_ = {}; +} + +bool AsyncTcpConnector::pending() const +{ + return status_.phase == TcpConnectPhase::Resolving || + status_.phase == TcpConnectPhase::Connecting; +} + +int AsyncTcpConnector::takeSocket() +{ + if (status_.phase != TcpConnectPhase::Connected || socket_ < 0) + { + return -1; + } + const int socket = socket_; + socket_ = -1; + status_ = {}; + deadline_ms_ = 0; + return socket; +} + +bool AsyncTcpConnector::beginSocket(std::uint32_t ipv4_address, + std::uint32_t) +{ + socket_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (socket_ < 0) + { + fail(TcpConnectFailure::SocketOpenFailed, errno); + return false; + } + + const int flags = fcntl(socket_, F_GETFL, 0); + if (flags < 0 || fcntl(socket_, F_SETFL, flags | O_NONBLOCK) < 0) + { + fail(TcpConnectFailure::SocketOpenFailed, errno); + return false; + } + + sockaddr_in endpoint{}; + endpoint.sin_family = AF_INET; + endpoint.sin_port = htons(port_); + endpoint.sin_addr.s_addr = ipv4_address; + const int result = + lwip_connect(socket_, + reinterpret_cast(&endpoint), + sizeof(endpoint)); + if (result == 0) + { + status_.phase = TcpConnectPhase::Connected; + status_.failure = TcpConnectFailure::None; + status_.detail = 0; + return true; + } + if (errno != EINPROGRESS) + { + fail(TcpConnectFailure::ConnectFailed, errno); + return false; + } + + status_.phase = TcpConnectPhase::Connecting; + status_.failure = TcpConnectFailure::None; + status_.detail = 0; + return true; +} + +void AsyncTcpConnector::fail(TcpConnectFailure failure, int detail) +{ + if (socket_ >= 0) + { + close(socket_); + } + socket_ = -1; + status_.phase = TcpConnectPhase::Failed; + status_.failure = failure; + status_.detail = detail; +} + +const char* tcpConnectFailureName(TcpConnectFailure failure) +{ + switch (failure) + { + case TcpConnectFailure::None: + return "none"; + case TcpConnectFailure::InvalidEndpoint: + return "invalid_endpoint"; + case TcpConnectFailure::ResolverBusy: + return "resolver_busy"; + case TcpConnectFailure::ResolveFailed: + return "resolve_failed"; + case TcpConnectFailure::SocketOpenFailed: + return "socket_open_failed"; + case TcpConnectFailure::ConnectFailed: + return "connect_failed"; + case TcpConnectFailure::TimedOut: + return "timed_out"; + case TcpConnectFailure::Cancelled: + return "cancelled"; + default: + return "unknown"; + } +} + +} // namespace platform::esp::arduino_common::net diff --git a/platform/esp/arduino_common/src/platform_ui_firmware_update_runtime.cpp b/platform/esp/arduino_common/src/platform_ui_firmware_update_runtime.cpp index a08baaa3..0bce5750 100644 --- a/platform/esp/arduino_common/src/platform_ui_firmware_update_runtime.cpp +++ b/platform/esp/arduino_common/src/platform_ui_firmware_update_runtime.cpp @@ -154,6 +154,8 @@ const char* wifi_state_name(::platform::ui::wifi::ConnectionState state) return "scanning"; case ::platform::ui::wifi::ConnectionState::Connecting: return "connecting"; + case ::platform::ui::wifi::ConnectionState::ResourceDeferred: + return "resource_deferred"; case ::platform::ui::wifi::ConnectionState::Connected: return "connected"; case ::platform::ui::wifi::ConnectionState::Error: diff --git a/platform/esp/arduino_common/src/platform_ui_wifi_access_runtime.cpp b/platform/esp/arduino_common/src/platform_ui_wifi_access_runtime.cpp index 687c9a90..6540b852 100644 --- a/platform/esp/arduino_common/src/platform_ui_wifi_access_runtime.cpp +++ b/platform/esp/arduino_common/src/platform_ui_wifi_access_runtime.cpp @@ -49,7 +49,7 @@ struct RuntimeState bool saw_screen_sample = false; bool last_sleeping = false; bool last_saver = false; - std::uint32_t last_connect_attempt_ms[kClientCount] = {}; + std::uint32_t last_connect_attempt_ms = 0; }; RuntimeState s_state{}; @@ -195,13 +195,13 @@ ScreenPhase sample_screen_phase(std::uint32_t now_ms) bool mark_connect_attempt(Client client, std::uint32_t now_ms) { - const std::size_t index = client_index(client); + (void)client; bool allowed = false; portENTER_CRITICAL(&s_lock); - const std::uint32_t last = s_state.last_connect_attempt_ms[index]; + const std::uint32_t last = s_state.last_connect_attempt_ms; if (last == 0 || (now_ms - last) >= kConnectBackoffMs) { - s_state.last_connect_attempt_ms[index] = now_ms; + s_state.last_connect_attempt_ms = now_ms; allowed = true; } portEXIT_CRITICAL(&s_lock); @@ -731,7 +731,11 @@ bool ensure_connected(const Request& request, Decision* out_decision) } else if (!::platform::ui::wifi::connect(nullptr)) { - decision = Decision::ConnectFailed; + decision = + ::platform::ui::wifi::status().state == + ::platform::ui::wifi::ConnectionState::ResourceDeferred + ? Decision::ConnectDeferredForResources + : Decision::ConnectFailed; } else { @@ -1157,6 +1161,8 @@ const char* decision_name(Decision decision) return "wifi_disconnected"; case Decision::ConnectDeferredForWake: return "connect_deferred_for_wake"; + case Decision::ConnectDeferredForResources: + return "connect_deferred_for_resources"; case Decision::ConnectBackoff: return "connect_backoff"; case Decision::ConnectFailed: diff --git a/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp b/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp index 2dafcfe1..f39029d9 100644 --- a/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp +++ b/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp @@ -971,6 +971,22 @@ class MapTileEventQueue final : public ui::map_tiles::IMapTileEventSink return ok; } + void clear() + { + if (!ensureMutex() || + xSemaphoreTake(mutex_, pdMS_TO_TICKS(20)) != pdTRUE) + { + return; + } + ui::map_tiles::MapTileAsyncEvent event{}; + while (queue_.pop(event)) + { + release_tile_payload(event); + event = {}; + } + xSemaphoreGive(mutex_); + } + private: bool ensureMutex() { @@ -1415,6 +1431,23 @@ ui::map_tiles::FilesystemMapTileSource& worker_tile_source() class MapTileAsyncHost final { public: + void acquire() + { + portENTER_CRITICAL(&lock_); + ++lease_count_; + portEXIT_CRITICAL(&lock_); + } + + void release() + { + portENTER_CRITICAL(&lock_); + if (lease_count_ > 0) + { + --lease_count_; + } + portEXIT_CRITICAL(&lock_); + } + bool request(const ui::map_tiles::MapTileRef& ref, uint32_t generation, ui::map_tiles::MapTileInteractionMode mode) @@ -1471,16 +1504,51 @@ class MapTileAsyncHost final vTaskDelay(kMapTileWorkerPostCommandYieldTicks); continue; } + bool idle = false; + portENTER_CRITICAL(&lock_); + if (lease_count_ == 0) + { + stopping_ = true; + idle = true; + } + portEXIT_CRITICAL(&lock_); + if (idle) + { + events_.clear(); + delete worker_; + worker_ = nullptr; + if (scratch_ != nullptr) + { + heap_caps_free(scratch_); + scratch_ = nullptr; + } + portENTER_CRITICAL(&lock_); + task_ = nullptr; + started_ = false; + stopping_ = false; + portEXIT_CRITICAL(&lock_); + std::printf("[GPS][MAP][worker] stopped reason=no_viewports\n"); + vTaskDelete(nullptr); + return; + } vTaskDelay(pdMS_TO_TICKS(5)); } } bool ensureStarted() { - if (started_) + portENTER_CRITICAL(&lock_); + const bool started = started_; + const bool stopping = stopping_; + portEXIT_CRITICAL(&lock_); + if (started) { return true; } + if (stopping) + { + return false; + } if (scratch_ == nullptr) { @@ -1533,7 +1601,9 @@ class MapTileAsyncHost final } return false; } + portENTER_CRITICAL(&lock_); started_ = true; + portEXIT_CRITICAL(&lock_); return true; } @@ -1549,8 +1619,11 @@ class MapTileAsyncHost final ui::map_tiles::MapTileRuntime runtime_{async_runtime_, state_machine_}; TaskHandle_t task_ = nullptr; bool started_ = false; + bool stopping_ = false; bool scratch_alloc_failed_logged_ = false; bool task_start_failed_logged_ = false; + std::size_t lease_count_ = 0; + portMUX_TYPE lock_ = portMUX_INITIALIZER_UNLOCKED; }; MapTileAsyncHost& map_tile_async_host() @@ -3603,6 +3676,11 @@ void init_tile_context(TileContext& ctx, lv_obj_t* map_container, MapAnchor* anc ui::map_tiles::MapTileRenderQueue* render_queue, bool* has_map_data, bool* has_visible_map_data) { + if (!ctx.runtime_acquired) + { + map_tile_async_host().acquire(); + ctx.runtime_acquired = true; + } ctx.map_container = map_container; ctx.anchor = anchor; ctx.tiles = tiles; @@ -3663,3 +3741,13 @@ void cleanup_tiles(TileContext& ctx) ctx.render_queue->clear(); } } + +void release_tile_context(TileContext& ctx) +{ + if (!ctx.runtime_acquired) + { + return; + } + ctx.runtime_acquired = false; + map_tile_async_host().release(); +} diff --git a/platform/esp/common/include/platform/esp/common/settings_store_impl.h b/platform/esp/common/include/platform/esp/common/settings_store_impl.h index 8ef62b23..fb2434db 100644 --- a/platform/esp/common/include/platform/esp/common/settings_store_impl.h +++ b/platform/esp/common/include/platform/esp/common/settings_store_impl.h @@ -1,5 +1,6 @@ #pragma once +#include "platform/ui/setting_sensitivity.h" #include "platform/ui/settings_store.h" #include @@ -318,7 +319,7 @@ bool put_string(const char* ns, const char* key, const char* value) safe_label(key), safe_label(storage_key), static_cast(std::strlen(value)), - safe_label(value), + ::platform::ui::settings::diagnostic_value(key, value), bool_label(ok)); return ok; } @@ -557,7 +558,7 @@ bool get_string(const char* ns, const char* key, std::string& out) safe_label(key), safe_label(storage_key), static_cast(out.size()), - safe_label(out.c_str())); + ::platform::ui::settings::diagnostic_value(key, out.c_str())); return true; } diff --git a/platform/esp/common/include/platform/esp/common/wifi_runtime_impl.h b/platform/esp/common/include/platform/esp/common/wifi_runtime_impl.h index 993155c9..92555d41 100644 --- a/platform/esp/common/include/platform/esp/common/wifi_runtime_impl.h +++ b/platform/esp/common/include/platform/esp/common/wifi_runtime_impl.h @@ -65,6 +65,7 @@ constexpr int kWifiRetryMgmtSbufNum = 6; constexpr int kWifiRetryRxMgmtBufNum = 5; constexpr std::size_t kWifiConnectMinInternalFreeBytes = 32U * 1024U; constexpr std::size_t kWifiConnectMinInternalLargestBlockBytes = 8U * 1024U; +constexpr std::size_t kWifiReconnectReserveBytes = 32U * 1024U; constexpr const char* kNetworkTimeSyncServer = "pool.ntp.org"; constexpr std::time_t kNetworkTimeMinValidEpochSeconds = 1577836800; // 2020-01-01 UTC constexpr uint32_t kNetworkTimeSyncTimeoutMs = 15000; @@ -90,6 +91,8 @@ struct RuntimeState bool profiles_cached = false; bool connected = false; bool connecting = false; + bool connect_deferred_for_resources = false; + bool intentional_disconnect_pending = false; bool scanning = false; int rssi = -127; Config config{}; @@ -105,6 +108,7 @@ struct RuntimeState esp_event_handler_instance_t ip_event_handler = nullptr; esp_timer_handle_t network_time_sync_timeout_timer = nullptr; TaskHandle_t network_time_apply_task = nullptr; + void* reconnect_memory_reserve = nullptr; }; RuntimeState s_runtime{}; @@ -142,13 +146,40 @@ void log_heap_snapshot(const char* stage) void set_status_message(const char* message); void cancel_network_time_sync(); +void release_reconnect_memory_reserve() +{ + if (!s_runtime.reconnect_memory_reserve) + { + return; + } + heap_caps_free(s_runtime.reconnect_memory_reserve); + s_runtime.reconnect_memory_reserve = nullptr; + log_heap_snapshot("released reconnect reserve"); +} + +void ensure_reconnect_memory_reserve() +{ + if (s_runtime.reconnect_memory_reserve) + { + return; + } + s_runtime.reconnect_memory_reserve = + heap_caps_malloc(kWifiReconnectReserveBytes, + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + std::printf("[WiFi][MEM] reconnect reserve bytes=%u acquired=%u\n", + static_cast(kWifiReconnectReserveBytes), + s_runtime.reconnect_memory_reserve ? 1U : 0U); +} + bool internal_memory_ready_for_wifi_connect(const char* stage) { + release_reconnect_memory_reserve(); const std::size_t ram_free = internal_free_bytes(); const std::size_t ram_largest = internal_largest_block_bytes(); if (ram_free >= kWifiConnectMinInternalFreeBytes && ram_largest >= kWifiConnectMinInternalLargestBlockBytes) { + s_runtime.connect_deferred_for_resources = false; return true; } @@ -158,7 +189,8 @@ bool internal_memory_ready_for_wifi_connect(const char* stage) static_cast(ram_largest), static_cast(kWifiConnectMinInternalFreeBytes), static_cast(kWifiConnectMinInternalLargestBlockBytes)); - set_status_message("Wi-Fi memory low"); + s_runtime.connect_deferred_for_resources = true; + set_status_message("Wi-Fi waiting for memory"); return false; } @@ -927,7 +959,9 @@ void wifi_event_handler(void*, { std::printf("[WiFi] sta connected\n"); } - s_runtime.connecting = false; + // Association is only an intermediate state. Keep the attempt + // active until DHCP produces GOT_IP or a disconnect event + // terminates it. break; } case WIFI_EVENT_STA_DISCONNECTED: @@ -949,7 +983,24 @@ void wifi_event_handler(void*, std::printf("[WiFi] sta disconnected ssid=%s\n", current_config().ssid[0] ? current_config().ssid : ""); } + if (s_runtime.intentional_disconnect_pending) + { + s_runtime.intentional_disconnect_pending = false; + std::printf("[WiFi] intentional disconnect completed\n"); + break; + } + + const bool attempt_failed = s_runtime.connecting; clear_connection_details(); + if (attempt_failed && s_runtime.profile_count > 1) + { + s_runtime.next_profile_index = + (s_runtime.next_profile_index + 1U) % + s_runtime.profile_count; + std::printf("[WiFi] connect event failed; next profile=%u/%u\n", + static_cast(s_runtime.next_profile_index + 1U), + static_cast(s_runtime.profile_count)); + } refresh_runtime_status_message(); break; } @@ -987,6 +1038,7 @@ void wifi_event_handler(void*, s_runtime.ssid[0] ? s_runtime.ssid : current_config().ssid, s_runtime.rssi); request_network_time_sync(); + ensure_reconnect_memory_reserve(); } } @@ -1244,6 +1296,7 @@ bool apply_enabled(bool enabled) #endif s_runtime.wifi_started = false; s_runtime.wifi_initialized = false; + release_reconnect_memory_reserve(); clear_connection_details(); restore_runtime_ble_after_wifi("Wi-Fi disabled"); set_status_message("Wi-Fi disabled"); @@ -1265,28 +1318,35 @@ bool apply_enabled(bool enabled) return true; } -static bool connect_single_profile(const Config& config) +enum class ConnectStartResult : uint8_t +{ + Started, + DeferredForResources, + Failed, +}; + +static ConnectStartResult connect_single_profile(const Config& config) { if (!config.enabled) { set_status_message("Enable Wi-Fi first"); - return false; + return ConnectStartResult::Failed; } if (!has_saved_credentials(config)) { set_status_message("SSID is not set"); - return false; + return ConnectStartResult::Failed; } if (!ensure_wifi_started()) { - return false; + return ConnectStartResult::Failed; } if (!internal_memory_ready_for_wifi_connect("before esp_wifi_connect")) { - return false; + return ConnectStartResult::DeferredForResources; } wifi_config_t wifi_config{}; @@ -1304,143 +1364,39 @@ static bool connect_single_profile(const Config& config) if (esp_wifi_set_config(WIFI_IF_STA, &wifi_config) != ESP_OK) { set_status_message("Set Wi-Fi config failed"); - return false; + return ConnectStartResult::Failed; } - cache_config(config); - clear_connection_details(); - s_runtime.connecting = true; - refresh_runtime_status_message(); - - if (esp_wifi_disconnect() != ESP_OK) + const bool replacing_active_attempt = + s_runtime.connected || s_runtime.connecting; + const esp_err_t disconnect_err = + replacing_active_attempt ? esp_wifi_disconnect() : ESP_ERR_WIFI_NOT_CONNECT; + s_runtime.intentional_disconnect_pending = + replacing_active_attempt && disconnect_err == ESP_OK; + if (disconnect_err != ESP_OK) { // Ignore disconnect failures before a fresh connect attempt. } + cache_config(config); + clear_connection_details(); + s_runtime.connect_deferred_for_resources = false; + s_runtime.connecting = true; + refresh_runtime_status_message(); if (esp_wifi_connect() != ESP_OK) { s_runtime.connecting = false; set_status_message("Wi-Fi connect failed"); - return false; + return ConnectStartResult::Failed; } - - constexpr int kConnectTimeoutMs = 15000; - constexpr int kPollMs = 100; - int waited_ms = 0; - while (waited_ms < kConnectTimeoutMs) - { - if (s_runtime.connected) - { - refresh_runtime_status_message(); - return true; - } - vTaskDelay(pdMS_TO_TICKS(kPollMs)); - waited_ms += kPollMs; - } - - s_runtime.connecting = false; - set_status_message("Wi-Fi connect timeout"); - return false; -} - -bool select_auto_profile_from_scan(std::size_t& out_index) -{ - out_index = s_runtime.next_profile_index; - if (s_runtime.profile_count == 0) - { - return false; - } - if (!ensure_wifi_started()) - { - return false; - } - if (!internal_memory_ready_for_wifi_connect("before auto scan")) - { - return false; - } - - s_runtime.scanning = true; - refresh_runtime_status_message(); - - wifi_scan_config_t scan_config{}; - const esp_err_t scan_err = esp_wifi_scan_start(&scan_config, true); - s_runtime.scanning = false; - if (scan_err != ESP_OK) - { - std::printf("[WiFi] auto scan skipped err=0x%x\n", - static_cast(scan_err)); - refresh_runtime_status_message(); - return false; - } - - uint16_t ap_count = 0; - if (esp_wifi_scan_get_ap_num(&ap_count) != ESP_OK || ap_count == 0) - { - std::printf("[WiFi] auto scan no networks profiles=%u\n", - static_cast(s_runtime.profile_count)); - refresh_runtime_status_message(); - return false; - } - - uint16_t record_count = std::min(ap_count, kWifiAutoScanMaxRecords); - for (wifi_ap_record_t& record : s_runtime.auto_scan_records) - { - record = wifi_ap_record_t{}; - } - if (esp_wifi_scan_get_ap_records(&record_count, s_runtime.auto_scan_records) != ESP_OK) - { - std::printf("[WiFi] auto scan read failed aps=%u\n", - static_cast(ap_count)); - refresh_runtime_status_message(); - return false; - } - - int best_index = -1; - int8_t best_rssi = -128; - for (uint16_t i = 0; i < record_count; ++i) - { - const wifi_ap_record_t& record = s_runtime.auto_scan_records[i]; - if (record.ssid[0] == 0) - { - continue; - } - const int index = profile_index_for_ssid(reinterpret_cast(record.ssid)); - if (index < 0) - { - continue; - } - if (best_index < 0 || record.rssi > best_rssi) - { - best_index = index; - best_rssi = record.rssi; - } - } - - refresh_runtime_status_message(); - if (best_index < 0) - { - std::printf("[WiFi] auto scan no saved match aps=%u read=%u profiles=%u\n", - static_cast(ap_count), - static_cast(record_count), - static_cast(s_runtime.profile_count)); - return false; - } - - out_index = static_cast(best_index); - s_runtime.next_profile_index = out_index; - std::printf("[WiFi] auto scan matched profile index=%u/%u ssid=%s rssi=%d aps=%u\n", - static_cast(out_index + 1U), - static_cast(s_runtime.profile_count), - s_runtime.profiles[out_index].ssid, - static_cast(best_rssi), - static_cast(record_count)); - return true; + return ConnectStartResult::Started; } bool connect(const Config* override_config) { if (override_config) { - return connect_single_profile(*override_config); + return connect_single_profile(*override_config) == + ConnectStartResult::Started; } Config config{}; @@ -1456,24 +1412,34 @@ bool connect(const Config* override_config) return false; } - std::size_t index = s_runtime.next_profile_index % s_runtime.profile_count; - (void)select_auto_profile_from_scan(index); + if (s_runtime.connected || s_runtime.connecting) + { + return true; + } + + // Automatic connection is event-driven. The old blocking scan and + // 15-second wait ran in the LVGL owner and made resource deferral look + // like a bad profile. + const std::size_t index = + s_runtime.next_profile_index % s_runtime.profile_count; Config candidate = s_runtime.profiles[index]; candidate.enabled = config.enabled; std::printf("[WiFi] auto connect profile index=%u/%u ssid=%s\n", static_cast(index + 1U), static_cast(s_runtime.profile_count), candidate.ssid); - const bool connected = connect_single_profile(candidate); - if (connected) + const ConnectStartResult result = connect_single_profile(candidate); + if (result == ConnectStartResult::Started) { - const bool saved = save_config(candidate); - s_runtime.next_profile_index = 0; - std::printf("[WiFi] auto connect profile selected ssid=%s saved=%u\n", - candidate.ssid, - saved ? 1U : 0U); return true; } + if (result == ConnectStartResult::DeferredForResources) + { + std::printf("[WiFi] auto connect deferred profile index=%u/%u reason=resources\n", + static_cast(index + 1U), + static_cast(s_runtime.profile_count)); + return false; + } s_runtime.next_profile_index = (index + 1U) % s_runtime.profile_count; std::printf("[WiFi] auto connect profile failed index=%u/%u next=%u\n", static_cast(index + 1U), @@ -1596,6 +1562,15 @@ Status status() return out; } + if (s_runtime.connect_deferred_for_resources) + { + out.state = ConnectionState::ResourceDeferred; + copy_bounded(out.message, + sizeof(out.message), + "Wi-Fi waiting for memory"); + return out; + } + if (s_runtime.connected) { out.state = ConnectionState::Connected; diff --git a/platform/esp/idf_common/src/platform_ui_wifi_runtime.cpp b/platform/esp/idf_common/src/platform_ui_wifi_runtime.cpp index 43509974..ee82edf5 100644 --- a/platform/esp/idf_common/src/platform_ui_wifi_runtime.cpp +++ b/platform/esp/idf_common/src/platform_ui_wifi_runtime.cpp @@ -37,8 +37,6 @@ constexpr uint32_t kWifiFeatureMask = TM_C6_FEATURE_WIFI_STA | TM_C6_FEATURE_WIF #if defined(TRAIL_MATE_WIFI_RUNTIME_C6_ASYNC_SCAN) constexpr uint32_t kC6ScanWaitTimeoutMs = 10000; constexpr uint32_t kC6ScanPollIntervalMs = 100; -constexpr uint32_t kC6ConnectWaitTimeoutMs = 15000; -constexpr uint32_t kC6ConnectPollIntervalMs = 100; #endif struct RuntimeState @@ -98,22 +96,6 @@ bool same_ssid(const Config& lhs, const Config& rhs) return std::strncmp(lhs.ssid, rhs.ssid, sizeof(lhs.ssid)) == 0; } -int profile_index_for_ssid(const char* ssid) -{ - if (!ssid || ssid[0] == '\0') - { - return -1; - } - for (std::size_t i = 0; i < s_runtime.profile_count; ++i) - { - if (std::strncmp(s_runtime.profiles[i].ssid, ssid, sizeof(s_runtime.profiles[i].ssid)) == 0) - { - return static_cast(i); - } - } - return -1; -} - void clear_profiles() { for (Config& profile : s_runtime.profiles) @@ -429,27 +411,6 @@ std::size_t copy_scan_results(ScanResult* out_results, return count; } -int best_profile_index_for_scan_results(const ScanResult* results, std::size_t count, int& out_rssi) -{ - int best_index = -1; - int best_rssi = -128; - for (std::size_t i = 0; i < count; ++i) - { - const int index = profile_index_for_ssid(results[i].ssid); - if (index < 0) - { - continue; - } - if (best_index < 0 || results[i].rssi > best_rssi) - { - best_index = index; - best_rssi = results[i].rssi; - } - } - out_rssi = best_rssi; - return best_index; -} - bool wait_for_c6_scan(ScanResult* out_results, std::size_t capacity, std::size_t& out_count) { out_count = 0; @@ -498,102 +459,6 @@ bool c6_wifi_ready_for_config(const c6::C6CompanionStatus& status, const Config& std::strncmp(status.wifi_ssid, config.ssid, sizeof(status.wifi_ssid)) == 0; } -bool wait_for_c6_connection(const Config& config, uint32_t baseline_event_count) -{ -#if defined(ESP_PLATFORM) && defined(TRAIL_MATE_WIFI_RUNTIME_C6_ASYNC_SCAN) - const TickType_t start_ticks = xTaskGetTickCount(); - const TickType_t timeout_ticks = pdMS_TO_TICKS(kC6ConnectWaitTimeoutMs); - - while ((xTaskGetTickCount() - start_ticks) < timeout_ticks) - { - const auto status = c6::get_c6_companion_status(); - if (!status.present) - { - std::printf("[WiFi][C6] connect wait failed reason=companion_unavailable\n"); - return false; - } - if (status.wifi_event_count != baseline_event_count && - c6_wifi_ready_for_config(status, config)) - { - char ip[16] = {}; - format_ipv4(status.wifi_ipv4_addr, ip, sizeof(ip)); - std::printf("[WiFi][C6] connected ssid=%s ip=%s events=%lu\n", - status.wifi_ssid[0] != '\0' ? status.wifi_ssid : config.ssid, - ip[0] != '\0' ? ip : "", - static_cast(status.wifi_event_count)); - return true; - } - if (status.wifi_event_count != baseline_event_count && status.wifi_error != TM_C6_OK) - { - std::printf("[WiFi][C6] connect wait failed reason=wifi_error error=%u events=%lu\n", - static_cast(status.wifi_error), - static_cast(status.wifi_event_count)); - return false; - } - vTaskDelay(pdMS_TO_TICKS(kC6ConnectPollIntervalMs)); - } - - const auto status = c6::get_c6_companion_status(); - std::printf("[WiFi][C6] connect wait timeout connected=%u ipv4=0x%08lX error=%u events=%lu\n", - status.wifi_connected ? 1U : 0U, - static_cast(status.wifi_ipv4_addr), - static_cast(status.wifi_error), - static_cast(status.wifi_event_count)); - return false; -#else - (void)config; - (void)baseline_event_count; - return false; -#endif -} - -bool select_auto_profile_from_scan(std::size_t& out_index) -{ - out_index = s_runtime.next_profile_index; - if (s_runtime.profile_count == 0 || !c6_present()) - { - return false; - } - - std::size_t result_count = - copy_scan_results(s_runtime.scan_results, kWifiProfileCapacity, c6::get_c6_companion_status()); - if (result_count == 0) - { - const bool sent = c6::c6_companion().sendWifiControl(make_control(c6::WifiCommand::Scan)); - if (!sent) - { - return false; - } -#if defined(TRAIL_MATE_WIFI_RUNTIME_C6_ASYNC_SCAN) - if (!wait_for_c6_scan(s_runtime.scan_results, kWifiProfileCapacity, result_count)) - { - return false; - } -#endif - } - - int best_rssi = -128; - const int best_index = - best_profile_index_for_scan_results(s_runtime.scan_results, result_count, best_rssi); - if (best_index < 0) - { - std::printf("[WiFi][C6] auto scan no saved match results=%u profiles=%u\n", - static_cast(result_count), - static_cast(s_runtime.profile_count)); - return false; - } - - out_index = static_cast(best_index); - s_runtime.next_profile_index = out_index; - std::printf("[WiFi][C6] auto scan matched profile index=%u/%u ssid=%s rssi=%d results=%u\n", - static_cast(out_index + 1U), - static_cast(s_runtime.profile_count), - s_runtime.profiles[out_index].ssid, - best_rssi, - static_cast(result_count)); - return true; -} - } // namespace bool is_supported() @@ -652,7 +517,6 @@ bool connect(const Config* override_config) if (!override_config && s_runtime.profile_count > 0) { std::size_t index = s_runtime.next_profile_index % s_runtime.profile_count; - (void)select_auto_profile_from_scan(index); config = s_runtime.profiles[index]; config.enabled = true; std::printf("[WiFi][C6] auto connect profile index=%u/%u ssid=%s\n", @@ -716,7 +580,9 @@ bool connect(const Config* override_config) after.wifi_connected ? 1U : 0U, static_cast(after.wifi_error), static_cast(after.wifi_event_count)); - return sent && wait_for_c6_connection(config, before.wifi_event_count); + // Connection completion is observed through status(); never wait for C6 + // events in the LVGL caller. + return sent; } void disconnect() diff --git a/platform/linux/common/include/ui/widgets/map/map_tiles.h b/platform/linux/common/include/ui/widgets/map/map_tiles.h index 7df2f653..5408c466 100644 --- a/platform/linux/common/include/ui/widgets/map/map_tiles.h +++ b/platform/linux/common/include/ui/widgets/map/map_tiles.h @@ -95,3 +95,4 @@ void init_tile_context(TileContext& ctx, bool* has_map_data, bool* has_visible_map_data); void cleanup_tiles(TileContext& ctx); +void release_tile_context(TileContext& ctx); diff --git a/platform/linux/common/src/chat/linux_raw_lora_mesh_adapter.cpp b/platform/linux/common/src/chat/linux_raw_lora_mesh_adapter.cpp index a17e5de4..2bed283e 100644 --- a/platform/linux/common/src/chat/linux_raw_lora_mesh_adapter.cpp +++ b/platform/linux/common/src/chat/linux_raw_lora_mesh_adapter.cpp @@ -72,14 +72,6 @@ std::uint32_t now_seconds() .count()); } -bool should_require_direct_pki(std::uint8_t encrypt_mode, - std::uint32_t dest_node, - std::uint32_t portnum) -{ - return encrypt_mode != 0 && dest_node != kBroadcastNodeId && - ::chat::meshtastic::allowPkiForPortnum(portnum); -} - bool secure_random_bytes(std::uint8_t* out, std::size_t len) { if (out == nullptr || len == 0) @@ -1737,7 +1729,7 @@ bool LinuxRawLoraMeshAdapter::sendMeshtasticPayload( bool air_want_ack = ::chat::meshtastic::shouldSetAirWantAck(dest, want_ack); - if (should_require_direct_pki(encrypt_mode_, dest, portnum)) + if (::chat::meshtastic::requirePkiForDirectPort(dest, portnum)) { const bool have_dest_key = node_public_keys_.find(dest) != node_public_keys_.end(); diff --git a/platform/linux/common/src/ui/widgets/map/map_tiles.cpp b/platform/linux/common/src/ui/widgets/map/map_tiles.cpp index 5d64a99e..dbb711de 100644 --- a/platform/linux/common/src/ui/widgets/map/map_tiles.cpp +++ b/platform/linux/common/src/ui/widgets/map/map_tiles.cpp @@ -1473,3 +1473,8 @@ void cleanup_tiles(TileContext& ctx) ctx.render_queue->clear(); } } + +void release_tile_context(TileContext&) +{ + // Linux tile loading has no dedicated worker lease. +} diff --git a/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp b/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp index c5f2562a..2f899cef 100644 --- a/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp +++ b/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp @@ -61,19 +61,13 @@ using ::chat::meshtastic::isZeroKey; using ::chat::meshtastic::keyVerificationStage; using ::chat::meshtastic::makeEncryptedPacketFromWire; using ::chat::meshtastic::readPbString; +using ::chat::meshtastic::requirePkiForDirectPort; using ::chat::meshtastic::shouldSetAirWantAck; using ::chat::meshtastic::toHex; void logMeshtasticRx(const char* format, ...); uint32_t nowSeconds(); -bool shouldRequireDirectPki(uint8_t encrypt_mode, ::chat::NodeId dest_node, uint32_t portnum) -{ - return encrypt_mode != 0 && - dest_node != kBroadcastNode && - allowPkiForPortnum(portnum); -} - ::chat::delivery::SendFailureKind failureKindFromRoutingError( meshtastic_Routing_Error reason) { @@ -507,9 +501,8 @@ bool MeshtasticRadioAdapter::sendTextWithId(::chat::ChannelId channel, const std out_channel = ::chat::ChannelId::PRIMARY; } - if (shouldRequireDirectPki(encrypt_mode_, - (peer == 0) ? kBroadcastNode : peer, - meshtastic_PortNum_TEXT_MESSAGE_APP)) + if (requirePkiForDirectPort((peer == 0) ? kBroadcastNode : peer, + meshtastic_PortNum_TEXT_MESSAGE_APP)) { const ::chat::MessageId packet_id = (forced_msg_id != 0) ? forced_msg_id : next_packet_id_; const bool ok = sendAppData(out_channel, @@ -665,7 +658,7 @@ bool MeshtasticRadioAdapter::sendAppData(::chat::ChannelId channel, uint32_t por bool use_pki = false; bool wire_want_ack = send_policy.wire_want_ack; bool track_ack = send_policy.track_ack; - if (shouldRequireDirectPki(encrypt_mode_, wire_dest, portnum)) + if (requirePkiForDirectPort(wire_dest, portnum)) { if (!pki_ready_ || !allowPkiForPortnum(portnum) || !hasPkiKey(wire_dest)) {