From 9d2e5d1a2e3f3e84822f0c8d1e380c326849c2fb Mon Sep 17 00:00:00 2001 From: liu weikai Date: Sat, 18 Jul 2026 16:31:35 +0800 Subject: [PATCH] Refactor Reticulum runtime and delivery state Split LXMF runtime ownership into focused peer, path, link, propagation, and delivery helpers while keeping the adapter as the coordination layer. Make chat delivery refs and send-result events protocol-aware across Reticulum, Meshtastic, and MeshCore, including Sent/Delivered/Failed semantics, UI badges, retry lookup, and store persistence. Fix Meshtastic MQTT private replies to publish encrypted direct packets and cover the new delivery behavior with smoke tests. --- .../src/esp32_lvgl_idf_app_facade_runtime.cpp | 40 +- apps/linux_sim_shell/CMakeLists.txt | 31 +- .../src/nrf52_node_app_facade_runtime.cpp | 23 +- .../esp_idf/ESP_IDF_COMPONENT_SOURCES.cmake | 4 + .../src/chat_message_mapper.cpp | 1 + .../tests/test_chat_message_mapper.cpp | 6 + .../delivery/chat_delivery_event_projector.h | 13 - .../chat/delivery/chat_delivery_types.h | 17 +- .../chat/delivery/chat_message_ledger.h | 35 +- .../include/chat/domain/chat_model.h | 5 + .../include/chat/infra/store/ram_store.h | 6 + .../include/chat/ports/i_chat_store.h | 18 + .../include/chat/usecase/chat_service.h | 19 +- .../chat_delivery_message_projection.cpp | 6 +- .../src/delivery/chat_message_ledger.cpp | 138 +- modules/core_chat/src/domain/chat_model.cpp | 56 + .../core_chat/src/infra/store/ram_store.cpp | 51 + .../core_chat/src/usecase/chat_service.cpp | 123 +- .../test_chat_delivery_message_projection.cpp | 4 + ...t_chat_delivery_send_result_projection.cpp | 13 +- .../tests/test_chat_message_ledger.cpp | 119 +- .../tests/test_meshtastic_protocol_policy.cpp | 3 + modules/core_sys/include/app/app_facades.h | 18 + .../chat_delivery_event_projection_adapter.h | 9 +- .../ui_chat_runtime/chat_ui_refresh_sink.h | 5 +- .../src/chat_delivery_action_port_adapter.cpp | 1 + ...chat_delivery_event_projection_adapter.cpp | 30 +- .../src/chat_page_runtime_event_pump.cpp | 11 +- ...test_chat_delivery_action_port_adapter.cpp | 21 +- ...chat_delivery_event_projection_adapter.cpp | 89 +- .../ui_presentation/chat/chat_message_ref.h | 1 + .../chat/chat_conversation_components.h | 6 +- .../ui/screens/chat/chat_ui_controller.h | 5 +- .../chat_conversation_components_watch.h | 6 +- .../chat/chat_conversation_components.cpp | 18 +- .../src/ui/screens/chat/chat_page_runtime.cpp | 64 +- .../ui/screens/chat/chat_ui_controller.cpp | 14 +- .../chat_conversation_components_watch.cpp | 15 +- .../tests/test_chat_presentation_source.cpp | 47 +- .../chat/infra/lxmf/lxmf_adapter.h | 48 +- .../infra/lxmf/lxmf_delivery_attempt_ledger.h | 171 ++ .../chat/infra/lxmf/lxmf_delivery_notifier.h | 30 + .../chat/infra/lxmf/lxmf_delivery_planner.h | 53 + .../chat/infra/lxmf/lxmf_delivery_runtime.h | 4 +- .../chat/infra/lxmf/lxmf_link_manager.h | 141 +- .../infra/lxmf/lxmf_lxst_telephony_client.h | 27 +- .../infra/lxmf/lxmf_network_page_client.h | 27 +- .../chat/infra/lxmf/lxmf_packet_router.h | 23 + .../chat/infra/lxmf/lxmf_path_manager.h | 45 +- .../chat/infra/lxmf/lxmf_peer_directory.h | 92 + .../chat/infra/lxmf/lxmf_ping_service.h | 6 +- .../chat/infra/lxmf/lxmf_propagation_client.h | 13 +- .../lxmf/lxmf_propagation_service_runtime.h | 8 +- .../chat/infra/lxmf/lxmf_resource_runtime.h | 9 +- .../chat/infra/lxmf/lxmf_runtime_state.h | 101 +- .../chat/infra/lxmf/lxmf_transport_runtime.h | 17 - .../chat/infra/meshtastic/mt_adapter.h | 6 + .../chat/infra/store/sd_store.h | 6 + .../arduino_common/include/sys/event_bus.h | 33 +- .../src/app_event_runtime_support.cpp | 8 +- .../src/app_runtime_support.cpp | 18 +- .../src/chat/infra/lxmf/lxmf_adapter.cpp | 1799 +++++++---------- .../src/chat/infra/lxmf/lxmf_adapter_call.cpp | 55 +- .../infra/lxmf/lxmf_announce_ingestor.cpp | 41 +- .../lxmf/lxmf_delivery_attempt_ledger.cpp | 376 ++++ .../infra/lxmf/lxmf_delivery_notifier.cpp | 59 + .../chat/infra/lxmf/lxmf_delivery_planner.cpp | 68 + .../chat/infra/lxmf/lxmf_delivery_runtime.cpp | 2 +- .../src/chat/infra/lxmf/lxmf_link_manager.cpp | 274 ++- .../infra/lxmf/lxmf_lxst_telephony_client.cpp | 100 + .../infra/lxmf/lxmf_network_page_client.cpp | 68 + .../chat/infra/lxmf/lxmf_packet_router.cpp | 50 + .../src/chat/infra/lxmf/lxmf_path_manager.cpp | 139 +- .../chat/infra/lxmf/lxmf_peer_directory.cpp | 358 ++++ .../infra/lxmf/lxmf_propagation_client.cpp | 86 +- .../chat/infra/lxmf/lxmf_resource_runtime.cpp | 8 +- .../infra/lxmf/lxmf_transport_runtime.cpp | 106 - .../chat/infra/mesh_mqtt_client_runtime.cpp | 6 +- .../chat/infra/meshcore/meshcore_adapter.cpp | 23 +- .../src/chat/infra/meshtastic/mt_adapter.cpp | 120 +- .../src/chat/infra/store/sd_store.cpp | 153 ++ .../esp/radio/meshtastic_radio_adapter.cpp | 14 +- .../include/chat/linux_sqlite_chat_store.h | 6 + platform/linux/common/include/sys/event_bus.h | 22 +- .../common/src/app/linux_app_services.cpp | 17 +- .../src/chat/linux_sqlite_chat_store.cpp | 84 + .../chat/infra/store/internal_fs_store.h | 6 + .../nrf52/arduino_common/sys/event_bus.h | 22 +- .../infra/meshcore/meshcore_radio_adapter.cpp | 11 +- .../meshtastic/meshtastic_radio_adapter.cpp | 45 +- .../chat/infra/store/internal_fs_store.cpp | 63 + .../test_reticulum_runtime_state_contract.cpp | 737 ++++++- 92 files changed, 5280 insertions(+), 1615 deletions(-) create mode 100644 platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_attempt_ledger.h create mode 100644 platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_notifier.h create mode 100644 platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_planner.h create mode 100644 platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_peer_directory.h create mode 100644 platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_attempt_ledger.cpp create mode 100644 platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_notifier.cpp create mode 100644 platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_planner.cpp create mode 100644 platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_peer_directory.cpp diff --git a/apps/esp32_lvgl/src/esp32_lvgl_idf_app_facade_runtime.cpp b/apps/esp32_lvgl/src/esp32_lvgl_idf_app_facade_runtime.cpp index 73a4b111..42ead3bf 100644 --- a/apps/esp32_lvgl/src/esp32_lvgl_idf_app_facade_runtime.cpp +++ b/apps/esp32_lvgl/src/esp32_lvgl_idf_app_facade_runtime.cpp @@ -5,6 +5,9 @@ #include "app/app_facade_access.h" #include "app/app_facades.h" #include "board/BoardBase.h" +#include "chat/delivery/chat_delivery_event_port.h" +#include "chat/delivery/chat_delivery_event_projector.h" +#include "chat/delivery/chat_delivery_read_model.h" #include "chat/infra/contact_store_core.h" #include "chat/infra/mesh_peer_directory_core.h" #include "chat/infra/mesh_protocol_utils.h" @@ -1234,6 +1237,7 @@ class IdfAppFacadeRuntime final : public app::IAppFacade return false; } chat_service_.reset(new chat::ChatService(chat_model_, meshAdapter(), *chat_store_)); + chat_service_->setDeliveryEventPort(&delivery_event_port_); chat_service_->setActiveProtocol(config_.mesh_protocol); chat_service_->switchChannel(config_.chat_channel == 1 ? chat::ChannelId::SECONDARY : chat::ChannelId::PRIMARY); @@ -1410,6 +1414,19 @@ class IdfAppFacadeRuntime final : public app::IAppFacade chat::IMeshAdapter* getMeshAdapter() override { return &meshAdapter(); } const chat::IMeshAdapter* getMeshAdapter() const override { return &meshAdapter(); } chat::NodeId getSelfNodeId() const override { return meshAdapter().getNodeId(); } + chat::delivery::ChatDeliveryReadModel* getChatDeliveryReadModel() override + { + return &delivery_read_model_; + } + const chat::delivery::ChatDeliveryReadModel* + getChatDeliveryReadModel() const override + { + return &delivery_read_model_; + } + chat::delivery::IChatDeliveryEventPort* getChatDeliveryEventPort() override + { + return &delivery_event_port_; + } team::TeamController* getTeamController() override { return team_controller_.get(); } team::TeamPairingService* getTeamPairing() override { return team_pairing_service_.get(); } @@ -1755,8 +1772,22 @@ class IdfAppFacadeRuntime final : public app::IAppFacade case sys::EventType::ChatSendResult: { auto* result_event = static_cast(event); - chat_service_->handleSendResult(result_event->msg_id, - result_event->status); + if (result_event->has_protocol) + { + chat_service_->handleSendResultForProtocol( + result_event->msg_id, + result_event->protocol, + result_event->status, + result_event->timestamp, + result_event->failure); + } + else + { + chat_service_->handleSendResult(result_event->msg_id, + result_event->status, + result_event->timestamp, + result_event->failure); + } return false; } case sys::EventType::NodeInfoUpdate: @@ -1853,6 +1884,11 @@ class IdfAppFacadeRuntime final : public app::IAppFacade IdfNullMeshAdapter null_mesh_adapter_{}; chat::MeshAdapterRouter mesh_router_{}; chat::IMeshAdapter* mesh_adapter_ = &null_mesh_adapter_; + chat::delivery::ChatDeliveryReadModel delivery_read_model_{}; + chat::delivery::ChatDeliveryEventProjector delivery_projector_{ + delivery_read_model_}; + chat::delivery::ProjectingChatDeliveryEventPort delivery_event_port_{ + delivery_projector_}; std::unique_ptr chat_service_{}; std::unique_ptr team_crypto_{}; std::unique_ptr team_event_sink_{}; diff --git a/apps/linux_sim_shell/CMakeLists.txt b/apps/linux_sim_shell/CMakeLists.txt index 6c47b629..776243cc 100644 --- a/apps/linux_sim_shell/CMakeLists.txt +++ b/apps/linux_sim_shell/CMakeLists.txt @@ -104,11 +104,12 @@ if(BUILD_TESTING) "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/usecase/chat_service.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_message_ledger.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_outbox_service.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_message_projection.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_send_result_projection.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_event_projector.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/mesh_protocol_utils.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/store/ram_store.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_read_model.cpp" - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_event_projector.cpp" - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_message_projection.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/usecase/contact_service.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_sys/src/sys/clock.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/ui_shared/src/ui/presentation_sources/chat_presentation_source.cpp" @@ -160,6 +161,10 @@ if(BUILD_TESTING) "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/usecase/chat_service.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_message_ledger.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_outbox_service.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_message_projection.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_send_result_projection.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_event_projector.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_read_model.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/mesh_protocol_utils.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/store/ram_store.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_sys/src/sys/clock.cpp") @@ -182,6 +187,10 @@ if(BUILD_TESTING) "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/usecase/chat_service.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_message_ledger.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_outbox_service.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_message_projection.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_send_result_projection.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_event_projector.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_read_model.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/mesh_protocol_utils.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/store/ram_store.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_sys/src/sys/clock.cpp") @@ -234,7 +243,11 @@ if(BUILD_TESTING) "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/domain/chat_model.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/store/ram_store.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_message_ledger.cpp" - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_outbox_service.cpp") + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_outbox_service.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_message_projection.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_send_result_projection.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_event_projector.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_read_model.cpp") target_include_directories(trailmate_chat_message_ledger_smoke PRIVATE "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/include") @@ -444,9 +457,16 @@ if(BUILD_TESTING) add_executable(trailmate_reticulum_runtime_state_contract_smoke "${TRAIL_MATE_REPO_ROOT}/tests/reticulum_conformance/test_reticulum_runtime_state_contract.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_send_result_projection.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_outbox_service.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/lxmf/lxmf_wire.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/reticulum/lxst_call_state_machine.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/reticulum/lxst_telephony_wire.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/reticulum/reticulum_wire.cpp" "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_destination_registry.cpp" + "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_attempt_ledger.cpp" + "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_notifier.cpp" + "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_planner.cpp" "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_runtime.cpp" "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_link_manager.cpp" "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_link_runtime.cpp" @@ -454,6 +474,7 @@ if(BUILD_TESTING) "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_network_page_client.cpp" "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_packet_router.cpp" "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_path_manager.cpp" + "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_peer_directory.cpp" "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_ping_service.cpp" "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_propagation_client.cpp" "${TRAIL_MATE_REPO_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_propagation_runtime.cpp" @@ -843,6 +864,10 @@ if(BUILD_TESTING) "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/usecase/chat_service.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_message_ledger.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_outbox_service.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_message_projection.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_send_result_projection.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_event_projector.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/delivery/chat_delivery_read_model.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/mesh_protocol_utils.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_team/src/protocol/team_chat.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_team/src/protocol/team_wire.cpp" diff --git a/apps/nrf52_node/src/nrf52_node_app_facade_runtime.cpp b/apps/nrf52_node/src/nrf52_node_app_facade_runtime.cpp index e43817e5..a9218663 100644 --- a/apps/nrf52_node/src/nrf52_node_app_facade_runtime.cpp +++ b/apps/nrf52_node/src/nrf52_node_app_facade_runtime.cpp @@ -876,11 +876,30 @@ void AppFacadeRuntime::dispatchPendingEvents(std::size_t max_events) { auto* result = static_cast(event); const chat::ChatMessage* message = - chat_service_ ? chat_service_->getMessage(result->msg_id) : nullptr; + chat_service_ + ? (result->has_protocol + ? chat_service_->getMessageForProtocol(result->msg_id, + result->protocol) + : chat_service_->getMessage(result->msg_id)) + : nullptr; if (chat_service_ && message) { const bool local_outgoing = message->from == 0; - chat_service_->handleSendResult(result->msg_id, result->status); + if (result->has_protocol) + { + chat_service_->handleSendResultForProtocol(result->msg_id, + result->protocol, + result->status, + result->timestamp, + result->failure); + } + else + { + chat_service_->handleSendResult(result->msg_id, + result->status, + result->timestamp, + result->failure); + } if (local_outgoing) { pending_chat_send_result_feedback_ = true; diff --git a/builds/esp_idf/ESP_IDF_COMPONENT_SOURCES.cmake b/builds/esp_idf/ESP_IDF_COMPONENT_SOURCES.cmake index c1f80590..a13f5f5c 100644 --- a/builds/esp_idf/ESP_IDF_COMPONENT_SOURCES.cmake +++ b/builds/esp_idf/ESP_IDF_COMPONENT_SOURCES.cmake @@ -447,6 +447,9 @@ set(TRAILMATE_ESP_IDF_PLATFORM_COMMON_SOURCES "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter.cpp" "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter_call.cpp" "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_announce_ingestor.cpp" + "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_attempt_ledger.cpp" + "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_notifier.cpp" + "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_planner.cpp" "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_runtime.cpp" "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_destination_registry.cpp" "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_identity.cpp" @@ -456,6 +459,7 @@ set(TRAILMATE_ESP_IDF_PLATFORM_COMMON_SOURCES "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_network_page_client.cpp" "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_packet_router.cpp" "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_path_manager.cpp" + "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_peer_directory.cpp" "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_ping_service.cpp" "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_propagation_client.cpp" "${TRAILMATE_ROOT}/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_propagation_runtime.cpp" diff --git a/modules/chat_presentation_adapters/src/chat_message_mapper.cpp b/modules/chat_presentation_adapters/src/chat_message_mapper.cpp index 7cece556..a10b2bd5 100644 --- a/modules/chat_presentation_adapters/src/chat_message_mapper.cpp +++ b/modules/chat_presentation_adapters/src/chat_message_mapper.cpp @@ -63,6 +63,7 @@ ui::chat::MessageRef toUiMessageRef(const chat::ChatMessage& message) } out.protocol_id = message.msg_id; + out.protocol = static_cast(message.protocol); return out; } diff --git a/modules/chat_presentation_adapters/tests/test_chat_message_mapper.cpp b/modules/chat_presentation_adapters/tests/test_chat_message_mapper.cpp index e4c2d4b1..782232b6 100644 --- a/modules/chat_presentation_adapters/tests/test_chat_message_mapper.cpp +++ b/modules/chat_presentation_adapters/tests/test_chat_message_mapper.cpp @@ -55,6 +55,8 @@ void incomingMessageMapsToRemoteStoredRef() assert(ref.origin == ui::chat::MessageOrigin::RemoteStored); assert(ref.protocol_id == 77); + assert(ref.protocol == + static_cast(chat::MeshProtocol::Meshtastic)); assert(ref.local_id == 0); assert(ref.nonce_or_seq == 0); assert(ref.isValid()); @@ -72,6 +74,8 @@ void queuedMessageMapsToLocalPendingRef() assert(ref.origin == ui::chat::MessageOrigin::LocalPending); assert(ref.protocol_id == 88); + assert(ref.protocol == + static_cast(chat::MeshProtocol::Meshtastic)); assert(ref.isValid()); } @@ -87,6 +91,8 @@ void storedLocalMessageMapsToLocalStoredRef() assert(ref.origin == ui::chat::MessageOrigin::LocalStored); assert(ref.protocol_id == 99); + assert(ref.protocol == + static_cast(chat::MeshProtocol::Meshtastic)); assert(ref.isValid()); } diff --git a/modules/core_chat/include/chat/delivery/chat_delivery_event_projector.h b/modules/core_chat/include/chat/delivery/chat_delivery_event_projector.h index 0b385f15..5804154b 100644 --- a/modules/core_chat/include/chat/delivery/chat_delivery_event_projector.h +++ b/modules/core_chat/include/chat/delivery/chat_delivery_event_projector.h @@ -5,19 +5,6 @@ namespace chat::delivery { -enum class SendFailureKind : uint8_t -{ - None, - PeerKeyMissing, - ChannelKeyMissing, - LocalIdentityMissing, - RadioSendFailed, - AckTimeout, - UnsupportedProtocol, - Rejected, - Unknown, -}; - class ChatDeliveryEventProjector { public: diff --git a/modules/core_chat/include/chat/delivery/chat_delivery_types.h b/modules/core_chat/include/chat/delivery/chat_delivery_types.h index 0eca7b95..b44221f2 100644 --- a/modules/core_chat/include/chat/delivery/chat_delivery_types.h +++ b/modules/core_chat/include/chat/delivery/chat_delivery_types.h @@ -29,11 +29,25 @@ enum class DeliveryFailureKind : uint8_t Unknown, }; +enum class SendFailureKind : uint8_t +{ + None, + PeerKeyMissing, + ChannelKeyMissing, + LocalIdentityMissing, + RadioSendFailed, + AckTimeout, + UnsupportedProtocol, + Rejected, + Unknown, +}; + struct ChatDeliveryRef { uint64_t local_id = 0; uint32_t protocol_id = 0; uint32_t nonce_or_seq = 0; + uint8_t protocol = 0; bool isValid() const { @@ -44,7 +58,8 @@ struct ChatDeliveryRef { return local_id == other.local_id && protocol_id == other.protocol_id && - nonce_or_seq == other.nonce_or_seq; + nonce_or_seq == other.nonce_or_seq && + protocol == other.protocol; } }; diff --git a/modules/core_chat/include/chat/delivery/chat_message_ledger.h b/modules/core_chat/include/chat/delivery/chat_message_ledger.h index c787b257..bd7b0b43 100644 --- a/modules/core_chat/include/chat/delivery/chat_message_ledger.h +++ b/modules/core_chat/include/chat/delivery/chat_message_ledger.h @@ -1,5 +1,6 @@ #pragma once +#include "chat/delivery/chat_delivery_event_port.h" #include "chat/domain/chat_model.h" #include "chat/ports/i_chat_store.h" @@ -11,20 +12,50 @@ class ChatMessageLedger final public: ChatMessageLedger(ChatModel& model, IChatStore& store); - void recordOutbound(const ChatMessage& message, bool model_enabled); + void setDeliveryEventPort(IChatDeliveryEventPort* delivery_event_port); + + void recordOutbound(const ChatMessage& message, + bool model_enabled, + SendFailureKind failure = SendFailureKind::Unknown); bool applyOutboundStatus(MessageId msg_id, MessageStatus status, - bool model_enabled); + bool model_enabled, + uint32_t timestamp_ms = 0, + SendFailureKind failure = + SendFailureKind::Unknown); + bool applyOutboundStatusForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status, + bool model_enabled, + uint32_t timestamp_ms = 0, + SendFailureKind failure = + SendFailureKind::Unknown); bool markRetryQueued(MessageId msg_id, bool model_enabled); + bool markRetryQueuedForProtocol(MessageId msg_id, + MeshProtocol protocol, + bool model_enabled); private: bool lookupMessage(MessageId msg_id, ChatMessage& out) const; + bool lookupMessageForProtocol(MessageId msg_id, + MeshProtocol protocol, + ChatMessage& out) const; bool writeStatus(MessageId msg_id, MessageStatus status, bool model_enabled); + bool writeStatusForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status, + bool model_enabled); + void publishDeliveryEvent(const ChatMessage& message, + MessageStatus status, + uint32_t timestamp_ms = 0, + SendFailureKind failure = + SendFailureKind::Unknown); ChatModel& model_; IChatStore& store_; + IChatDeliveryEventPort* delivery_event_port_ = nullptr; }; } // namespace chat::delivery diff --git a/modules/core_chat/include/chat/domain/chat_model.h b/modules/core_chat/include/chat/domain/chat_model.h index f9857ae9..b49097bd 100644 --- a/modules/core_chat/include/chat/domain/chat_model.h +++ b/modules/core_chat/include/chat/domain/chat_model.h @@ -30,6 +30,9 @@ class ChatModel void onIncoming(const ChatMessage& msg); void onSendQueued(const ChatMessage& msg); bool updateMessageStatus(MessageId msg_id, MessageStatus status); + bool updateMessageStatusForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status); /** * @brief Handle send result @@ -56,6 +59,8 @@ class ChatModel * @brief Get message by ID */ const ChatMessage* getMessage(MessageId msg_id) const; + const ChatMessage* getMessageForProtocol(MessageId msg_id, + MeshProtocol protocol) const; /** * @brief Clear all conversations and failed messages diff --git a/modules/core_chat/include/chat/infra/store/ram_store.h b/modules/core_chat/include/chat/infra/store/ram_store.h index 2f410f51..40da403d 100644 --- a/modules/core_chat/include/chat/infra/store/ram_store.h +++ b/modules/core_chat/include/chat/infra/store/ram_store.h @@ -39,7 +39,13 @@ class RamStore : public IChatStore void clearConversation(const ConversationId& conv) override; void clearAll() override; bool updateMessageStatus(MessageId msg_id, MessageStatus status) override; + bool updateMessageStatusForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status) override; bool getMessage(MessageId msg_id, ChatMessage* out) const override; + bool getMessageForProtocol(MessageId msg_id, + MeshProtocol protocol, + ChatMessage* out) const override; bool hasReticulumLxmfMessageHash(const uint8_t* lxmf_hash) const override; private: diff --git a/modules/core_chat/include/chat/ports/i_chat_store.h b/modules/core_chat/include/chat/ports/i_chat_store.h index cc62ec19..e8fe5fcc 100644 --- a/modules/core_chat/include/chat/ports/i_chat_store.h +++ b/modules/core_chat/include/chat/ports/i_chat_store.h @@ -129,6 +129,15 @@ class IChatStore * @return true if updated */ virtual bool updateMessageStatus(MessageId msg_id, MessageStatus status) = 0; + virtual bool updateMessageStatusForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status) + { + (void)msg_id; + (void)protocol; + (void)status; + return false; + } /** * @brief Look up a stored message by message ID @@ -137,6 +146,15 @@ class IChatStore * @return true if found */ virtual bool getMessage(MessageId msg_id, ChatMessage* out) const = 0; + virtual bool getMessageForProtocol(MessageId msg_id, + MeshProtocol protocol, + ChatMessage* out) const + { + (void)msg_id; + (void)protocol; + (void)out; + return false; + } /** * @brief Check whether an LXMF message hash has already been stored. diff --git a/modules/core_chat/include/chat/usecase/chat_service.h b/modules/core_chat/include/chat/usecase/chat_service.h index 77134706..0abc2985 100644 --- a/modules/core_chat/include/chat/usecase/chat_service.h +++ b/modules/core_chat/include/chat/usecase/chat_service.h @@ -9,6 +9,7 @@ #include "../domain/chat_types.h" #include "../ports/i_chat_store.h" #include "../ports/i_mesh_adapter.h" +#include "chat/delivery/chat_delivery_event_port.h" #include "chat/delivery/chat_message_ledger.h" #include #include @@ -107,6 +108,7 @@ class ChatService * @return true if queued for resend */ bool resendFailed(MessageId msg_id); + bool resendFailedForProtocol(MessageId msg_id, MeshProtocol protocol); /** * @brief Get recent messages for a conversation @@ -166,12 +168,27 @@ class ChatService * Queued, Sent, Delivered, and Failed are accepted. Delivered is terminal; * an earlier failure may still be superseded by a later valid proof. */ - void handleSendResult(MessageId msg_id, MessageStatus status); + void handleSendResult(MessageId msg_id, + MessageStatus status, + uint32_t timestamp_ms = 0, + delivery::SendFailureKind failure = + delivery::SendFailureKind::Unknown); + void handleSendResultForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status, + uint32_t timestamp_ms = 0, + delivery::SendFailureKind failure = + delivery::SendFailureKind::Unknown); /** * @brief Get message by ID (for UI send status) */ const ChatMessage* getMessage(MessageId msg_id) const; + const ChatMessage* getMessageForProtocol(MessageId msg_id, + MeshProtocol protocol) const; + + void setDeliveryEventPort( + delivery::IChatDeliveryEventPort* delivery_event_port); void setActiveProtocol(MeshProtocol protocol) { diff --git a/modules/core_chat/src/delivery/chat_delivery_message_projection.cpp b/modules/core_chat/src/delivery/chat_delivery_message_projection.cpp index d4a420f1..231b8738 100644 --- a/modules/core_chat/src/delivery/chat_delivery_message_projection.cpp +++ b/modules/core_chat/src/delivery/chat_delivery_message_projection.cpp @@ -35,7 +35,11 @@ DeliveryFailureKind mapFailure(chat::MessageStatus status) ChatDeliveryRef toDeliveryRef(const chat::ChatMessage& message) { ChatDeliveryRef ref{}; - ref.protocol_id = message.msg_id; + if (message.msg_id != 0) + { + ref.protocol_id = message.msg_id; + ref.protocol = static_cast(message.protocol); + } return ref; } diff --git a/modules/core_chat/src/delivery/chat_message_ledger.cpp b/modules/core_chat/src/delivery/chat_message_ledger.cpp index 6caa0106..6b4b909a 100644 --- a/modules/core_chat/src/delivery/chat_message_ledger.cpp +++ b/modules/core_chat/src/delivery/chat_message_ledger.cpp @@ -1,5 +1,7 @@ #include "chat/delivery/chat_message_ledger.h" +#include "chat/delivery/chat_delivery_message_projection.h" +#include "chat/delivery/chat_delivery_send_result_projection.h" #include "chat/delivery/chat_outbox_service.h" namespace chat::delivery @@ -10,8 +12,15 @@ ChatMessageLedger::ChatMessageLedger(ChatModel& model, IChatStore& store) { } +void ChatMessageLedger::setDeliveryEventPort( + IChatDeliveryEventPort* delivery_event_port) +{ + delivery_event_port_ = delivery_event_port; +} + void ChatMessageLedger::recordOutbound(const ChatMessage& message, - bool model_enabled) + bool model_enabled, + SendFailureKind failure) { if (model_enabled) { @@ -22,11 +31,17 @@ void ChatMessageLedger::recordOutbound(const ChatMessage& message, } } store_.append(message); + if (ChatOutboxService::isOutboundStatusUpdate(message.status)) + { + publishDeliveryEvent(message, message.status, 0, failure); + } } bool ChatMessageLedger::applyOutboundStatus(MessageId msg_id, MessageStatus status, - bool model_enabled) + bool model_enabled, + uint32_t timestamp_ms, + SendFailureKind failure) { ChatMessage current{}; if (!lookupMessage(msg_id, current)) @@ -37,7 +52,36 @@ bool ChatMessageLedger::applyOutboundStatus(MessageId msg_id, { return false; } - return writeStatus(msg_id, status, model_enabled); + if (!writeStatus(msg_id, status, model_enabled)) + { + return false; + } + publishDeliveryEvent(current, status, timestamp_ms, failure); + return true; +} + +bool ChatMessageLedger::applyOutboundStatusForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status, + bool model_enabled, + uint32_t timestamp_ms, + SendFailureKind failure) +{ + ChatMessage current{}; + if (!lookupMessageForProtocol(msg_id, protocol, current)) + { + return false; + } + if (!ChatOutboxService::shouldApplyStatus(¤t, status)) + { + return false; + } + if (!writeStatusForProtocol(msg_id, protocol, status, model_enabled)) + { + return false; + } + publishDeliveryEvent(current, status, timestamp_ms, failure); + return true; } bool ChatMessageLedger::markRetryQueued(MessageId msg_id, bool model_enabled) @@ -51,7 +95,36 @@ bool ChatMessageLedger::markRetryQueued(MessageId msg_id, bool model_enabled) { return false; } - return writeStatus(msg_id, MessageStatus::Queued, model_enabled); + if (!writeStatus(msg_id, MessageStatus::Queued, model_enabled)) + { + return false; + } + publishDeliveryEvent(current, MessageStatus::Queued); + return true; +} + +bool ChatMessageLedger::markRetryQueuedForProtocol(MessageId msg_id, + MeshProtocol protocol, + bool model_enabled) +{ + ChatMessage current{}; + if (!lookupMessageForProtocol(msg_id, protocol, current)) + { + return false; + } + if (current.from != 0 || current.status != MessageStatus::Failed) + { + return false; + } + if (!writeStatusForProtocol(msg_id, + protocol, + MessageStatus::Queued, + model_enabled)) + { + return false; + } + publishDeliveryEvent(current, MessageStatus::Queued); + return true; } bool ChatMessageLedger::lookupMessage(MessageId msg_id, ChatMessage& out) const @@ -68,6 +141,23 @@ bool ChatMessageLedger::lookupMessage(MessageId msg_id, ChatMessage& out) const return store_.getMessage(msg_id, &out); } +bool ChatMessageLedger::lookupMessageForProtocol(MessageId msg_id, + MeshProtocol protocol, + ChatMessage& out) const +{ + if (msg_id == 0) + { + return false; + } + if (const ChatMessage* message = + model_.getMessageForProtocol(msg_id, protocol)) + { + out = *message; + return true; + } + return store_.getMessageForProtocol(msg_id, protocol, &out); +} + bool ChatMessageLedger::writeStatus(MessageId msg_id, MessageStatus status, bool model_enabled) @@ -84,4 +174,44 @@ bool ChatMessageLedger::writeStatus(MessageId msg_id, return store_.updateMessageStatus(msg_id, status) || updated; } +bool ChatMessageLedger::writeStatusForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status, + bool model_enabled) +{ + if (msg_id == 0) + { + return false; + } + bool updated = false; + if (model_enabled) + { + updated = + model_.updateMessageStatusForProtocol(msg_id, protocol, status); + } + return store_.updateMessageStatusForProtocol(msg_id, protocol, status) || + updated; +} + +void ChatMessageLedger::publishDeliveryEvent(const ChatMessage& message, + MessageStatus status, + uint32_t timestamp_ms, + SendFailureKind failure) +{ + if (delivery_event_port_ == nullptr || message.msg_id == 0 || + !ChatOutboxService::isOutboundStatusUpdate(status)) + { + return; + } + + delivery_event_port_->publishDeliveryEvent( + makeChatSendResultDeliveryEvent( + toDeliveryRef(message), + ChatOutboxService::toDeliveryState(status), + status == MessageStatus::Failed + ? failure + : ChatOutboxService::failureForStatus(status), + timestamp_ms)); +} + } // namespace chat::delivery diff --git a/modules/core_chat/src/domain/chat_model.cpp b/modules/core_chat/src/domain/chat_model.cpp index 6906a902..6f5fbe76 100644 --- a/modules/core_chat/src/domain/chat_model.cpp +++ b/modules/core_chat/src/domain/chat_model.cpp @@ -73,6 +73,44 @@ bool ChatModel::updateMessageStatus(MessageId msg_id, MessageStatus status) return false; } +bool ChatModel::updateMessageStatusForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status) +{ + for (auto& pair : conversations_) + { + ConversationData& data = pair.second; + for (size_t i = 0; i < data.messages.size(); i++) + { + ChatMessage* msg = &data.messages[i].message; + if (!msg || msg->msg_id != msg_id || msg->protocol != protocol) + { + continue; + } + msg->status = status; + failed_messages_.erase( + std::remove_if(failed_messages_.begin(), + failed_messages_.end(), + [msg_id, protocol](const ChatMessage& failed) + { + return failed.msg_id == msg_id && + failed.protocol == protocol; + }), + failed_messages_.end()); + if (status == MessageStatus::Failed) + { + if (failed_messages_.size() >= MAX_FAILED_MESSAGES) + { + failed_messages_.erase(failed_messages_.begin()); + } + failed_messages_.push_back(*msg); + } + return true; + } + } + return false; +} + void ChatModel::onSendResult(MessageId msg_id, bool ok) { (void)updateMessageStatus(msg_id, ok ? MessageStatus::Sent : MessageStatus::Failed); @@ -136,6 +174,24 @@ const ChatMessage* ChatModel::getMessage(MessageId msg_id) const return nullptr; } +const ChatMessage* ChatModel::getMessageForProtocol(MessageId msg_id, + MeshProtocol protocol) const +{ + for (const auto& pair : conversations_) + { + const ConversationData& data = pair.second; + for (size_t i = 0; i < data.messages.size(); i++) + { + const ChatMessage* msg = &data.messages[i].message; + if (msg && msg->msg_id == msg_id && msg->protocol == protocol) + { + return msg; + } + } + } + return nullptr; +} + void ChatModel::clearAll() { conversations_.clear(); diff --git a/modules/core_chat/src/infra/store/ram_store.cpp b/modules/core_chat/src/infra/store/ram_store.cpp index 4907943d..be512633 100644 --- a/modules/core_chat/src/infra/store/ram_store.cpp +++ b/modules/core_chat/src/infra/store/ram_store.cpp @@ -191,6 +191,28 @@ bool RamStore::updateMessageStatus(MessageId msg_id, MessageStatus status) return false; } +bool RamStore::updateMessageStatusForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status) +{ + if (msg_id == 0) return false; + for (auto& pair : conversations_) + { + ConversationStorage& storage = pair.second; + size_t count = storage.messages.size(); + for (size_t i = 0; i < count; ++i) + { + ChatMessage* msg = &storage.messages[i].message; + if (!msg) continue; + if (msg->msg_id != msg_id || msg->protocol != protocol) continue; + if (msg->from != 0) continue; // only update outgoing messages + msg->status = status; + return true; + } + } + return false; +} + bool RamStore::getMessage(MessageId msg_id, ChatMessage* out) const { if (msg_id == 0) @@ -217,6 +239,35 @@ bool RamStore::getMessage(MessageId msg_id, ChatMessage* out) const return false; } +bool RamStore::getMessageForProtocol(MessageId msg_id, + MeshProtocol protocol, + ChatMessage* out) const +{ + if (msg_id == 0) + { + return false; + } + + for (const auto& pair : conversations_) + { + const ConversationStorage& storage = pair.second; + for (const auto& entry : storage.messages) + { + if (entry.message.msg_id != msg_id || + entry.message.protocol != protocol) + { + continue; + } + if (out) + { + *out = entry.message; + } + return true; + } + } + return false; +} + bool RamStore::hasReticulumLxmfMessageHash(const uint8_t* lxmf_hash) const { if (!lxmf_hash || isAllZeroKeyBytes(lxmf_hash, kReticulumLxmfHashSize)) diff --git a/modules/core_chat/src/usecase/chat_service.cpp b/modules/core_chat/src/usecase/chat_service.cpp index 0770bda8..136a1d67 100644 --- a/modules/core_chat/src/usecase/chat_service.cpp +++ b/modules/core_chat/src/usecase/chat_service.cpp @@ -98,6 +98,38 @@ const char* failure_name(MeshOperationFailure failure) return "unknown"; } +delivery::SendFailureKind delivery_failure_from_mesh_operation( + MeshOperationFailure failure) +{ + switch (failure) + { + case MeshOperationFailure::None: + return delivery::SendFailureKind::None; + case MeshOperationFailure::PeerKeyMissing: + return delivery::SendFailureKind::PeerKeyMissing; + case MeshOperationFailure::ChannelKeyMissing: + return delivery::SendFailureKind::ChannelKeyMissing; + case MeshOperationFailure::LocalIdentityMissing: + return delivery::SendFailureKind::LocalIdentityMissing; + case MeshOperationFailure::Unsupported: + return delivery::SendFailureKind::UnsupportedProtocol; + case MeshOperationFailure::NotReady: + case MeshOperationFailure::TxDisabled: + case MeshOperationFailure::RadioOffline: + case MeshOperationFailure::DutyCycleLimited: + case MeshOperationFailure::EncodeFailed: + case MeshOperationFailure::CryptoFailed: + case MeshOperationFailure::RadioTxFailed: + return delivery::SendFailureKind::RadioSendFailed; + case MeshOperationFailure::InvalidInput: + case MeshOperationFailure::Busy: + return delivery::SendFailureKind::Rejected; + case MeshOperationFailure::Unknown: + return delivery::SendFailureKind::Unknown; + } + return delivery::SendFailureKind::Unknown; +} + const char* protocol_name(MeshProtocol protocol) { return infra::isValidMeshProtocol(protocol) ? infra::meshProtocolName(protocol) @@ -321,7 +353,11 @@ MeshSendResult ChatService::sendTextResolvedDetailed( msg.reticulum_identity = result.reticulum_identity; msg.status = result.ok ? MessageStatus::Queued : MessageStatus::Failed; - message_ledger_.recordOutbound(msg, model_enabled_); + message_ledger_.recordOutbound( + msg, + model_enabled_, + result.ok ? delivery::SendFailureKind::None + : delivery_failure_from_mesh_operation(result.failure)); CHAT_SERVICE_DIAG_LOG("[ChatService][TX] stored msg=%lu status=%u peer=%08lX dest=%s text=\"%s\"\n", static_cast(msg.msg_id), static_cast(msg.status), @@ -451,7 +487,51 @@ bool ChatService::resendFailed(MessageId msg_id) return false; } - return message_ledger_.markRetryQueued(msg.msg_id, model_enabled_); + return message_ledger_.markRetryQueuedForProtocol(msg.msg_id, + msg.protocol, + model_enabled_); +} + +bool ChatService::resendFailedForProtocol(MessageId msg_id, + MeshProtocol protocol) +{ + ChatMessage msg; + if (const ChatMessage* model_msg = + model_.getMessageForProtocol(msg_id, protocol)) + { + msg = *model_msg; + } + else if (!store_.getMessageForProtocol(msg_id, protocol, &msg)) + { + return false; + } + + if (msg.status != MessageStatus::Failed || msg.protocol != active_protocol_) + { + return false; + } + + const bool resend_reticulum_destination = + msg.protocol == MeshProtocol::Reticulum && + hasReticulumDestinationIdentity(msg.reticulum_identity); + const MeshSendResult result = + resend_reticulum_destination + ? adapter_.sendTextToReticulumDestination(msg.channel, + msg.text, + msg.msg_id, + msg.reticulum_identity) + : adapter_.sendTextDetailed(msg.channel, + msg.text, + msg.msg_id, + msg.peer); + if (!result.ok || result.msg_id != msg.msg_id) + { + return false; + } + + return message_ledger_.markRetryQueuedForProtocol(msg.msg_id, + msg.protocol, + model_enabled_); } std::vector ChatService::getRecentMessages(const ConversationId& conv, size_t limit) const @@ -822,9 +902,29 @@ void ChatService::handleSendResult(MessageId msg_id, bool ok) ok ? MessageStatus::Sent : MessageStatus::Failed); } -void ChatService::handleSendResult(MessageId msg_id, MessageStatus status) +void ChatService::handleSendResult(MessageId msg_id, + MessageStatus status, + uint32_t timestamp_ms, + delivery::SendFailureKind failure) { - (void)message_ledger_.applyOutboundStatus(msg_id, status, model_enabled_); + (void)message_ledger_.applyOutboundStatus( + msg_id, status, model_enabled_, timestamp_ms, failure); +} + +void ChatService::handleSendResultForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status, + uint32_t timestamp_ms, + delivery::SendFailureKind failure) +{ + (void)message_ledger_.applyOutboundStatusForProtocol( + msg_id, protocol, status, model_enabled_, timestamp_ms, failure); +} + +void ChatService::setDeliveryEventPort( + delivery::IChatDeliveryEventPort* delivery_event_port) +{ + message_ledger_.setDeliveryEventPort(delivery_event_port); } const ChatMessage* ChatService::getMessage(MessageId msg_id) const @@ -840,6 +940,21 @@ const ChatMessage* ChatService::getMessage(MessageId msg_id) const return nullptr; } +const ChatMessage* ChatService::getMessageForProtocol( + MessageId msg_id, + MeshProtocol protocol) const +{ + if (const ChatMessage* msg = model_.getMessageForProtocol(msg_id, protocol)) + { + return msg; + } + if (store_.getMessageForProtocol(msg_id, protocol, &store_lookup_cache_)) + { + return &store_lookup_cache_; + } + return nullptr; +} + void ChatService::setModelEnabled(bool enabled) { if (model_enabled_ == enabled) diff --git a/modules/core_chat/tests/test_chat_delivery_message_projection.cpp b/modules/core_chat/tests/test_chat_delivery_message_projection.cpp index 9376593c..b6070974 100644 --- a/modules/core_chat/tests/test_chat_delivery_message_projection.cpp +++ b/modules/core_chat/tests/test_chat_delivery_message_projection.cpp @@ -21,6 +21,8 @@ int main() chat::delivery::toDeliveryRecord(message(chat::MessageStatus::Queued, 10), 100); assert(queued.ref.protocol_id == 10); + assert(queued.ref.protocol == + static_cast(chat::MeshProtocol::Meshtastic)); assert(queued.state == chat::delivery::DeliveryState::Queued); assert(queued.failure == chat::delivery::DeliveryFailureKind::None); assert(queued.updated_at_ms == 100); @@ -28,6 +30,8 @@ int main() const auto sent = chat::delivery::toDeliveryRecord(message(chat::MessageStatus::Sent, 11)); assert(sent.state == chat::delivery::DeliveryState::Sent); + assert(sent.ref.protocol == + static_cast(chat::MeshProtocol::Meshtastic)); assert(sent.failure == chat::delivery::DeliveryFailureKind::None); const auto delivered = chat::delivery::toDeliveryRecord( diff --git a/modules/core_chat/tests/test_chat_delivery_send_result_projection.cpp b/modules/core_chat/tests/test_chat_delivery_send_result_projection.cpp index 73d7b47d..b77d6da4 100644 --- a/modules/core_chat/tests/test_chat_delivery_send_result_projection.cpp +++ b/modules/core_chat/tests/test_chat_delivery_send_result_projection.cpp @@ -1,4 +1,5 @@ #include "chat/delivery/chat_delivery_send_result_projection.h" +#include "chat/domain/chat_types.h" #include @@ -8,6 +9,7 @@ int main() ChatDeliveryRef ref{}; ref.protocol_id = 10; + ref.protocol = static_cast(chat::MeshProtocol::Meshtastic); ChatDeliveryEvent event = makeChatSendResultDeliveryEvent( ref, @@ -15,6 +17,8 @@ int main() SendFailureKind::PeerKeyMissing, 111); assert(event.ref.protocol_id == 10); + assert(event.ref.protocol == + static_cast(chat::MeshProtocol::Meshtastic)); assert(event.state == DeliveryState::Sent); assert(event.failure == SendFailureKind::None); assert(event.timestamp_ms == 111); @@ -61,8 +65,15 @@ int main() assert(event.failure == SendFailureKind::RadioSendFailed); assert(event.timestamp_ms == 333); - event = makeAckTimeoutDeliveryEvent(ChatDeliveryRef{0, 12, 0}, 444); + event = makeAckTimeoutDeliveryEvent( + ChatDeliveryRef{0, + 12, + 0, + static_cast(chat::MeshProtocol::MeshCore)}, + 444); assert(event.ref.protocol_id == 12); + assert(event.ref.protocol == + static_cast(chat::MeshProtocol::MeshCore)); assert(event.state == DeliveryState::Failed); assert(event.failure == SendFailureKind::AckTimeout); assert(event.timestamp_ms == 444); diff --git a/modules/core_chat/tests/test_chat_message_ledger.cpp b/modules/core_chat/tests/test_chat_message_ledger.cpp index 42c01968..66625190 100644 --- a/modules/core_chat/tests/test_chat_message_ledger.cpp +++ b/modules/core_chat/tests/test_chat_message_ledger.cpp @@ -6,6 +6,21 @@ namespace { +class RecordingDeliveryEventPort final + : public chat::delivery::IChatDeliveryEventPort +{ + public: + void publishDeliveryEvent( + const chat::delivery::ChatDeliveryEvent& event) override + { + last = event; + ++count; + } + + chat::delivery::ChatDeliveryEvent last{}; + int count = 0; +}; + chat::ChatMessage outgoing(chat::MessageId id, chat::MessageStatus status) { chat::ChatMessage message; @@ -19,6 +34,16 @@ chat::ChatMessage outgoing(chat::MessageId id, chat::MessageStatus status) return message; } +chat::ChatMessage outgoing_with_protocol(chat::MessageId id, + chat::MeshProtocol protocol, + chat::NodeId peer) +{ + chat::ChatMessage message = outgoing(id, chat::MessageStatus::Queued); + message.protocol = protocol; + message.peer = peer; + return message; +} + chat::ChatMessage incoming(chat::MessageId id) { chat::ChatMessage message = outgoing(id, chat::MessageStatus::Incoming); @@ -33,45 +58,83 @@ int main() chat::ChatModel model; chat::RamStore store; chat::delivery::ChatMessageLedger ledger(model, store); + RecordingDeliveryEventPort delivery_events{}; + ledger.setDeliveryEventPort(&delivery_events); ledger.recordOutbound(outgoing(100, chat::MessageStatus::Queued), true); const chat::ChatMessage* model_message = model.getMessage(100); assert(model_message != nullptr); assert(model_message->status == chat::MessageStatus::Queued); + assert(delivery_events.count == 1); + assert(delivery_events.last.ref.protocol_id == 100); + assert(delivery_events.last.ref.protocol == + static_cast(chat::MeshProtocol::Meshtastic)); + assert(delivery_events.last.state == chat::delivery::DeliveryState::Queued); chat::ChatMessage stored{}; assert(store.getMessage(100, &stored)); assert(stored.status == chat::MessageStatus::Queued); - assert(ledger.applyOutboundStatus(100, chat::MessageStatus::Sent, true)); + assert(ledger.applyOutboundStatus(100, + chat::MessageStatus::Sent, + true, + 1234)); assert(model.getMessage(100)->status == chat::MessageStatus::Sent); assert(store.getMessage(100, &stored)); assert(stored.status == chat::MessageStatus::Sent); + assert(delivery_events.count == 2); + assert(delivery_events.last.ref.protocol_id == 100); + assert(delivery_events.last.ref.protocol == + static_cast(chat::MeshProtocol::Meshtastic)); + assert(delivery_events.last.state == chat::delivery::DeliveryState::Sent); + assert(delivery_events.last.timestamp_ms == 1234); assert(!ledger.applyOutboundStatus(100, chat::MessageStatus::Queued, true)); + assert(delivery_events.count == 2); assert(model.getMessage(100)->status == chat::MessageStatus::Sent); assert(!ledger.applyOutboundStatus(100, chat::MessageStatus::Failed, true)); + assert(delivery_events.count == 2); assert(model.getMessage(100)->status == chat::MessageStatus::Sent); assert(ledger.applyOutboundStatus(100, chat::MessageStatus::Delivered, true)); assert(model.getMessage(100)->status == chat::MessageStatus::Delivered); + assert(delivery_events.count == 3); + assert(delivery_events.last.state == + chat::delivery::DeliveryState::Delivered); assert(!ledger.applyOutboundStatus(100, chat::MessageStatus::Failed, true)); + assert(delivery_events.count == 3); assert(model.getMessage(100)->status == chat::MessageStatus::Delivered); - ledger.recordOutbound(outgoing(200, chat::MessageStatus::Failed), true); + ledger.recordOutbound(outgoing(200, chat::MessageStatus::Failed), + true, + chat::delivery::SendFailureKind::PeerKeyMissing); assert(model.getMessage(200)->status == chat::MessageStatus::Failed); + assert(delivery_events.count == 4); + assert(delivery_events.last.ref.protocol_id == 200); + assert(delivery_events.last.ref.protocol == + static_cast(chat::MeshProtocol::Meshtastic)); + assert(delivery_events.last.state == chat::delivery::DeliveryState::Failed); + assert(delivery_events.last.failure == + chat::delivery::SendFailureKind::PeerKeyMissing); assert(!ledger.applyOutboundStatus(200, chat::MessageStatus::Queued, true)); assert(!ledger.applyOutboundStatus(200, chat::MessageStatus::Sent, true)); + assert(delivery_events.count == 4); assert(model.getMessage(200)->status == chat::MessageStatus::Failed); assert(ledger.markRetryQueued(200, true)); assert(model.getMessage(200)->status == chat::MessageStatus::Queued); + assert(delivery_events.count == 5); + assert(delivery_events.last.ref.protocol_id == 200); + assert(delivery_events.last.ref.protocol == + static_cast(chat::MeshProtocol::Meshtastic)); + assert(delivery_events.last.state == chat::delivery::DeliveryState::Queued); model.onIncoming(incoming(300)); store.append(incoming(300)); assert(!ledger.applyOutboundStatus(300, chat::MessageStatus::Sent, true)); assert(!ledger.markRetryQueued(300, true)); + assert(delivery_events.count == 5); chat::ChatModel store_only_model; chat::RamStore store_only_store; @@ -87,5 +150,57 @@ int main() assert(store_only_store.getMessage(400, &stored)); assert(stored.status == chat::MessageStatus::Sent); + chat::ChatModel collision_model; + chat::RamStore collision_store; + chat::delivery::ChatMessageLedger collision_ledger(collision_model, + collision_store); + collision_ledger.recordOutbound( + outgoing_with_protocol(500, + chat::MeshProtocol::Meshtastic, + 0x01020304), + true); + collision_ledger.recordOutbound( + outgoing_with_protocol(500, + chat::MeshProtocol::Reticulum, + 0x05060708), + true); + assert(collision_ledger.applyOutboundStatusForProtocol( + 500, + chat::MeshProtocol::Reticulum, + chat::MessageStatus::Delivered, + true)); + assert(collision_ledger.applyOutboundStatusForProtocol( + 500, + chat::MeshProtocol::Meshtastic, + chat::MessageStatus::Failed, + true, + 2222, + chat::delivery::SendFailureKind::AckTimeout)); + const chat::ChatMessage* meshtastic_collision = + collision_model.getMessageForProtocol(500, + chat::MeshProtocol::Meshtastic); + const chat::ChatMessage* reticulum_collision = + collision_model.getMessageForProtocol(500, + chat::MeshProtocol::Reticulum); + assert(meshtastic_collision != nullptr); + assert(reticulum_collision != nullptr); + assert(meshtastic_collision->status == chat::MessageStatus::Failed); + assert(reticulum_collision->status == chat::MessageStatus::Delivered); + assert(collision_ledger.markRetryQueuedForProtocol( + 500, + chat::MeshProtocol::Meshtastic, + true)); + assert(meshtastic_collision->status == chat::MessageStatus::Queued); + assert(collision_store.getMessageForProtocol( + 500, + chat::MeshProtocol::Meshtastic, + &stored)); + assert(stored.status == chat::MessageStatus::Queued); + assert(collision_store.getMessageForProtocol( + 500, + chat::MeshProtocol::Reticulum, + &stored)); + assert(stored.status == chat::MessageStatus::Delivered); + return 0; } diff --git a/modules/core_chat/tests/test_meshtastic_protocol_policy.cpp b/modules/core_chat/tests/test_meshtastic_protocol_policy.cpp index cfe20eee..54a8f32b 100644 --- a/modules/core_chat/tests/test_meshtastic_protocol_policy.cpp +++ b/modules/core_chat/tests/test_meshtastic_protocol_policy.cpp @@ -184,6 +184,9 @@ int main() MqttProxyRejectReason::None); settings.encryption_enabled = false; + assert(validateMqttProxyPublish(settings, chat::ChannelId::PRIMARY, false, true) == + MqttProxyRejectReason::None); + assert(shouldPublishToMqtt(settings, chat::ChannelId::PRIMARY, false, true)); assert(validateMqttDecodedDownlinkPayload(settings, true, meshtastic_PortNum_ADMIN_APP) == diff --git a/modules/core_sys/include/app/app_facades.h b/modules/core_sys/include/app/app_facades.h index e731f782..e22b0e8f 100644 --- a/modules/core_sys/include/app/app_facades.h +++ b/modules/core_sys/include/app/app_facades.h @@ -22,6 +22,11 @@ namespace chat { class ChatService; class IMeshAdapter; +namespace delivery +{ +class ChatDeliveryReadModel; +class IChatDeliveryEventPort; +} // namespace delivery namespace contacts { class ContactService; @@ -72,6 +77,19 @@ class IAppMessagingFacade virtual chat::IMeshAdapter* getMeshAdapter() = 0; virtual const chat::IMeshAdapter* getMeshAdapter() const = 0; virtual chat::NodeId getSelfNodeId() const = 0; + virtual chat::delivery::ChatDeliveryReadModel* getChatDeliveryReadModel() + { + return nullptr; + } + virtual const chat::delivery::ChatDeliveryReadModel* + getChatDeliveryReadModel() const + { + return nullptr; + } + virtual chat::delivery::IChatDeliveryEventPort* getChatDeliveryEventPort() + { + return nullptr; + } }; class IAppTeamFacade diff --git a/modules/ui_chat_runtime/include/ui_chat_runtime/chat_delivery_event_projection_adapter.h b/modules/ui_chat_runtime/include/ui_chat_runtime/chat_delivery_event_projection_adapter.h index a84b8e63..690d752b 100644 --- a/modules/ui_chat_runtime/include/ui_chat_runtime/chat_delivery_event_projection_adapter.h +++ b/modules/ui_chat_runtime/include/ui_chat_runtime/chat_delivery_event_projection_adapter.h @@ -20,11 +20,18 @@ class ChatDeliveryEventProjectionAdapter void onChatSendResult(::chat::MessageId msg_id, ::chat::MessageStatus status, - uint32_t timestamp_ms = 0); + uint32_t timestamp_ms = 0, + ::chat::delivery::SendFailureKind failure = + ::chat::delivery::SendFailureKind::Unknown, + bool has_protocol = false, + ::chat::MeshProtocol protocol = + ::chat::MeshProtocol::Meshtastic); void onAckTimeout(::chat::MessageId msg_id, uint32_t timestamp_ms = 0); private: bool publishSendResult(::chat::MessageId msg_id, + bool has_protocol, + ::chat::MeshProtocol protocol, ::chat::delivery::DeliveryState state, ::chat::delivery::SendFailureKind failure, uint32_t timestamp_ms); diff --git a/modules/ui_chat_runtime/include/ui_chat_runtime/chat_ui_refresh_sink.h b/modules/ui_chat_runtime/include/ui_chat_runtime/chat_ui_refresh_sink.h index 91701408..1e912e4b 100644 --- a/modules/ui_chat_runtime/include/ui_chat_runtime/chat_ui_refresh_sink.h +++ b/modules/ui_chat_runtime/include/ui_chat_runtime/chat_ui_refresh_sink.h @@ -12,7 +12,10 @@ class IChatUiRefreshSink virtual ~IChatUiRefreshSink() = default; virtual void onRuntimeMessageArrived(chat::MessageId msg_id) = 0; - virtual void onRuntimeSendResult(chat::MessageId msg_id) = 0; + virtual void onRuntimeSendResult( + chat::MessageId msg_id, + bool has_protocol = false, + chat::MeshProtocol protocol = chat::MeshProtocol::Meshtastic) = 0; virtual void onRuntimeUnreadChanged() = 0; virtual void showKeyVerification( diff --git a/modules/ui_chat_runtime/src/chat_delivery_action_port_adapter.cpp b/modules/ui_chat_runtime/src/chat_delivery_action_port_adapter.cpp index 649338f0..d1b6f56a 100644 --- a/modules/ui_chat_runtime/src/chat_delivery_action_port_adapter.cpp +++ b/modules/ui_chat_runtime/src/chat_delivery_action_port_adapter.cpp @@ -51,6 +51,7 @@ ChatDeliveryActionResult ChatDeliveryActionPortAdapter::clearFailure( out.local_id = ref.local_id; out.protocol_id = ref.protocol_id; out.nonce_or_seq = ref.nonce_or_seq; + out.protocol = ref.protocol; return out; } diff --git a/modules/ui_chat_runtime/src/chat_delivery_event_projection_adapter.cpp b/modules/ui_chat_runtime/src/chat_delivery_event_projection_adapter.cpp index 0a1f8e01..5bb67103 100644 --- a/modules/ui_chat_runtime/src/chat_delivery_event_projection_adapter.cpp +++ b/modules/ui_chat_runtime/src/chat_delivery_event_projection_adapter.cpp @@ -19,7 +19,10 @@ ChatDeliveryEventProjectionAdapter::ChatDeliveryEventProjectionAdapter( void ChatDeliveryEventProjectionAdapter::onChatSendResult( ::chat::MessageId msg_id, ::chat::MessageStatus status, - uint32_t timestamp_ms) + uint32_t timestamp_ms, + ::chat::delivery::SendFailureKind failure, + bool has_protocol, + ::chat::MeshProtocol protocol) { if (!::chat::delivery::ChatOutboxService::isOutboundStatusUpdate(status)) { @@ -27,6 +30,10 @@ void ChatDeliveryEventProjectionAdapter::onChatSendResult( } const ::chat::ChatMessage* message = chat_service_.getMessage(msg_id); + if (has_protocol) + { + message = chat_service_.getMessageForProtocol(msg_id, protocol); + } if (!::chat::delivery::ChatOutboxService::shouldApplyStatus(message, status)) { @@ -34,9 +41,16 @@ void ChatDeliveryEventProjectionAdapter::onChatSendResult( } const auto state = ::chat::delivery::ChatOutboxService::toDeliveryState(status); - const auto failure = - ::chat::delivery::ChatOutboxService::failureForStatus(status); - (void)publishSendResult(msg_id, state, failure, timestamp_ms); + if (status != ::chat::MessageStatus::Failed) + { + failure = ::chat::delivery::SendFailureKind::None; + } + (void)publishSendResult(msg_id, + has_protocol, + protocol, + state, + failure, + timestamp_ms); } void ChatDeliveryEventProjectionAdapter::onAckTimeout( @@ -45,6 +59,8 @@ void ChatDeliveryEventProjectionAdapter::onAckTimeout( { (void)publishSendResult( msg_id, + false, + ::chat::MeshProtocol::Meshtastic, ::chat::delivery::DeliveryState::Failed, ::chat::delivery::SendFailureKind::AckTimeout, timestamp_ms); @@ -52,6 +68,8 @@ void ChatDeliveryEventProjectionAdapter::onAckTimeout( bool ChatDeliveryEventProjectionAdapter::publishSendResult( ::chat::MessageId msg_id, + bool has_protocol, + ::chat::MeshProtocol protocol, ::chat::delivery::DeliveryState state, ::chat::delivery::SendFailureKind failure, uint32_t timestamp_ms) @@ -61,7 +79,9 @@ bool ChatDeliveryEventProjectionAdapter::publishSendResult( return false; } - const ::chat::ChatMessage* message = chat_service_.getMessage(msg_id); + const ::chat::ChatMessage* message = + has_protocol ? chat_service_.getMessageForProtocol(msg_id, protocol) + : chat_service_.getMessage(msg_id); if (message == nullptr) { return false; diff --git a/modules/ui_chat_runtime/src/chat_page_runtime_event_pump.cpp b/modules/ui_chat_runtime/src/chat_page_runtime_event_pump.cpp index 04aaba4a..71e7c7e3 100644 --- a/modules/ui_chat_runtime/src/chat_page_runtime_event_pump.cpp +++ b/modules/ui_chat_runtime/src/chat_page_runtime_event_pump.cpp @@ -100,11 +100,18 @@ void ChatPageRuntimeEventPump::handleChatSendResult( if (delivery_adapter_ != nullptr) { delivery_adapter_->onChatSendResult( - event.msg_id, event.status, event.timestamp); + event.msg_id, + event.status, + event.timestamp, + event.failure, + event.has_protocol, + event.protocol); } if (ui_ != nullptr) { - ui_->onRuntimeSendResult(event.msg_id); + ui_->onRuntimeSendResult(event.msg_id, + event.has_protocol, + event.protocol); } } diff --git a/modules/ui_chat_runtime/tests/test_chat_delivery_action_port_adapter.cpp b/modules/ui_chat_runtime/tests/test_chat_delivery_action_port_adapter.cpp index dcfa5152..dc196aa0 100644 --- a/modules/ui_chat_runtime/tests/test_chat_delivery_action_port_adapter.cpp +++ b/modules/ui_chat_runtime/tests/test_chat_delivery_action_port_adapter.cpp @@ -11,6 +11,15 @@ ui::chat::MessageRef messageRef(uint32_t id) ui::chat::MessageRef out{}; out.origin = ui::chat::MessageOrigin::LocalStored; out.protocol_id = id; + out.protocol = static_cast(chat::MeshProtocol::Meshtastic); + return out; +} + +chat::delivery::ChatDeliveryRef deliveryRef(uint32_t id) +{ + chat::delivery::ChatDeliveryRef out{}; + out.protocol_id = id; + out.protocol = static_cast(chat::MeshProtocol::Meshtastic); return out; } @@ -21,7 +30,7 @@ chat::delivery::ChatDeliveryRecord deliveryRecord( chat::delivery::DeliveryFailureKind::None) { chat::delivery::ChatDeliveryRecord out{}; - out.ref.protocol_id = id; + out.ref = deliveryRef(id); out.state = state; out.failure = failure; return out; @@ -56,6 +65,8 @@ int main() assert(mapped.local_id == 0); assert(mapped.protocol_id == 700); assert(mapped.nonce_or_seq == 0); + assert(mapped.protocol == + static_cast(chat::MeshProtocol::Meshtastic)); ui::chat::MessageRef invalid{}; auto result = adapter.clearFailure(invalid); @@ -68,18 +79,18 @@ int main() result = adapter.clearFailure(messageRef(701)); assert(result.ok); ChatDeliveryRecord found{}; - assert(!read_model.find(ChatDeliveryRef{0, 701, 0}, found)); + assert(!read_model.find(deliveryRef(701), found)); assert(read_model.upsert(deliveryRecord(702, DeliveryState::Queued))); result = adapter.cancelPending(messageRef(702)); assert(result.ok); - assert(!read_model.find(ChatDeliveryRef{0, 702, 0}, found)); + assert(!read_model.find(deliveryRef(702), found)); assert(read_model.upsert(deliveryRecord(703, DeliveryState::Sent))); result = adapter.cancelPending(messageRef(703)); assert(!result.ok); assert(result.failure == ChatDeliveryActionFailure::NotRetryable); - assert(read_model.find(ChatDeliveryRef{0, 703, 0}, found)); + assert(read_model.find(deliveryRef(703), found)); result = adapter.retryMessage(messageRef(704)); assert(!result.ok); @@ -92,7 +103,7 @@ int main() result = retrying_adapter.retryMessage(messageRef(705)); assert(result.ok); assert(retry_port.call_count == 1); - const ChatDeliveryRef expected_retry_ref{0, 705, 0}; + const ChatDeliveryRef expected_retry_ref = deliveryRef(705); assert(retry_port.last_ref == expected_retry_ref); result = retrying_adapter.handleMessageAction( diff --git a/modules/ui_chat_runtime/tests/test_chat_delivery_event_projection_adapter.cpp b/modules/ui_chat_runtime/tests/test_chat_delivery_event_projection_adapter.cpp index 9863fc6b..1ba222d4 100644 --- a/modules/ui_chat_runtime/tests/test_chat_delivery_event_projection_adapter.cpp +++ b/modules/ui_chat_runtime/tests/test_chat_delivery_event_projection_adapter.cpp @@ -10,6 +10,15 @@ namespace { +::chat::delivery::ChatDeliveryRef refFor(::chat::MessageId id, + ::chat::MeshProtocol protocol = + ::chat::MeshProtocol::Meshtastic) +{ + ::chat::delivery::ChatDeliveryRef ref{}; + ref.protocol_id = id; + ref.protocol = static_cast(protocol); + return ref; +} class FakeMeshAdapter final : public ::chat::IMeshAdapter { @@ -71,8 +80,7 @@ int main() sent_id, ::chat::MessageStatus::Queued, 1200); ::chat::delivery::ChatDeliveryRecord record{}; - assert(read_model.find(::chat::delivery::ChatDeliveryRef{0, sent_id, 0}, - record)); + assert(read_model.find(refFor(sent_id), record)); assert(record.state == ::chat::delivery::DeliveryState::Queued); assert(record.failure == ::chat::delivery::DeliveryFailureKind::None); assert(record.updated_at_ms == 1200); @@ -81,23 +89,20 @@ int main() projection_adapter.onChatSendResult( sent_id, ::chat::MessageStatus::Sent, 1234); - assert(read_model.find(::chat::delivery::ChatDeliveryRef{0, sent_id, 0}, - record)); + assert(read_model.find(refFor(sent_id), record)); assert(record.state == ::chat::delivery::DeliveryState::Sent); assert(record.failure == ::chat::delivery::DeliveryFailureKind::None); assert(record.updated_at_ms == 1234); service.handleSendResult(sent_id, ::chat::MessageStatus::Delivered); projection_adapter.onChatSendResult( sent_id, ::chat::MessageStatus::Delivered, 1250); - assert(read_model.find(::chat::delivery::ChatDeliveryRef{0, sent_id, 0}, - record)); + assert(read_model.find(refFor(sent_id), record)); assert(record.state == ::chat::delivery::DeliveryState::Delivered); assert(record.failure == ::chat::delivery::DeliveryFailureKind::None); assert(record.updated_at_ms == 1250); projection_adapter.onChatSendResult( sent_id, ::chat::MessageStatus::Failed, 1300); - assert(read_model.find(::chat::delivery::ChatDeliveryRef{0, sent_id, 0}, - record)); + assert(read_model.find(refFor(sent_id), record)); assert(record.state == ::chat::delivery::DeliveryState::Delivered); assert(record.failure == ::chat::delivery::DeliveryFailureKind::None); assert(record.updated_at_ms == 1250); @@ -110,28 +115,82 @@ int main() projection_adapter.onChatSendResult( failed_id, ::chat::MessageStatus::Failed, 2345); - assert(read_model.find(::chat::delivery::ChatDeliveryRef{0, failed_id, 0}, - record)); + assert(read_model.find(refFor(failed_id), record)); assert(record.state == ::chat::delivery::DeliveryState::Failed); assert(record.failure == ::chat::delivery::DeliveryFailureKind::Unknown); assert(record.updated_at_ms == 2345); + const auto ack_failed_id = + service.sendText(::chat::ChannelId::PRIMARY, "ackfail", 0); + assert(ack_failed_id == 702); + service.handleSendResult(ack_failed_id, + ::chat::MessageStatus::Failed, + 0, + ::chat::delivery::SendFailureKind::AckTimeout); + projection_adapter.onChatSendResult( + ack_failed_id, + ::chat::MessageStatus::Failed, + 2400, + ::chat::delivery::SendFailureKind::AckTimeout, + true, + ::chat::MeshProtocol::Meshtastic); + assert(read_model.find(refFor(ack_failed_id), record)); + assert(record.state == ::chat::delivery::DeliveryState::Failed); + assert(record.failure == ::chat::delivery::DeliveryFailureKind::AckTimeout); + const auto timeout_id = service.sendText(::chat::ChannelId::PRIMARY, "timeout", 0); - assert(timeout_id == 702); + assert(timeout_id == 703); projection_adapter.onAckTimeout(timeout_id, 3456); - assert(read_model.find(::chat::delivery::ChatDeliveryRef{0, timeout_id, 0}, - record)); + assert(read_model.find(refFor(timeout_id), record)); assert(record.state == ::chat::delivery::DeliveryState::Failed); assert(record.failure == ::chat::delivery::DeliveryFailureKind::AckTimeout); assert(record.updated_at_ms == 3456); projection_adapter.onAckTimeout(0, 4567); - assert(read_model.size() == 3); + assert(read_model.size() == 4); + + adapter.next_id = 800; + service.setActiveProtocol(::chat::MeshProtocol::Meshtastic); + const auto mt_collision_id = + service.sendText(::chat::ChannelId::PRIMARY, "mt collision", 0); + assert(mt_collision_id == 800); + + adapter.next_id = 800; + service.setActiveProtocol(::chat::MeshProtocol::MeshCore); + const auto mc_collision_id = + service.sendText(::chat::ChannelId::PRIMARY, "mc collision", 0); + assert(mc_collision_id == 800); + + projection_adapter.onChatSendResult( + mc_collision_id, + ::chat::MessageStatus::Delivered, + 5100, + ::chat::delivery::SendFailureKind::None, + true, + ::chat::MeshProtocol::MeshCore); + projection_adapter.onChatSendResult( + mt_collision_id, + ::chat::MessageStatus::Failed, + 5200, + ::chat::delivery::SendFailureKind::AckTimeout, + true, + ::chat::MeshProtocol::Meshtastic); + + assert(read_model.find( + refFor(800, ::chat::MeshProtocol::MeshCore), + record)); + assert(record.state == ::chat::delivery::DeliveryState::Delivered); + assert(record.failure == ::chat::delivery::DeliveryFailureKind::None); + assert(read_model.find( + refFor(800, ::chat::MeshProtocol::Meshtastic), + record)); + assert(record.state == ::chat::delivery::DeliveryState::Failed); + assert(record.failure == ::chat::delivery::DeliveryFailureKind::AckTimeout); projection_adapter.onChatSendResult( 9999, ::chat::MessageStatus::Failed, 0); - assert(read_model.size() == 3); + assert(read_model.size() == 6); return 0; } diff --git a/modules/ui_presentation/include/ui_presentation/chat/chat_message_ref.h b/modules/ui_presentation/include/ui_presentation/chat/chat_message_ref.h index ae9c51ba..748c9eac 100644 --- a/modules/ui_presentation/include/ui_presentation/chat/chat_message_ref.h +++ b/modules/ui_presentation/include/ui_presentation/chat/chat_message_ref.h @@ -21,6 +21,7 @@ struct MessageRef uint64_t local_id = 0; uint32_t protocol_id = 0; uint32_t nonce_or_seq = 0; + uint8_t protocol = 0; bool isValid() const { diff --git a/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h b/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h index 28c16355..d4995acc 100644 --- a/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h +++ b/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h @@ -54,7 +54,11 @@ class ChatConversationScreen void clearMessages(); void scrollToTop(); void scrollToBottom(); - bool updateMessageStatus(chat::MessageId msg_id, chat::MessageStatus status); + bool updateMessageStatus( + chat::MessageId msg_id, + chat::MessageStatus status, + bool has_protocol = false, + chat::MeshProtocol protocol = chat::MeshProtocol::Meshtastic); void setActionCallback(void (*cb)(ActionIntent intent, void*), void* user_data); void setMessageActionCallback( diff --git a/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h b/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h index 753345e9..a48b6053 100644 --- a/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h +++ b/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h @@ -89,7 +89,10 @@ class UiController : public IChatUiRefreshSink void handleMessageListAction(ChatMessageListScreen::ActionIntent intent, const chat::ConversationId& conv); void onRuntimeMessageArrived(chat::MessageId msg_id) override; - void onRuntimeSendResult(chat::MessageId msg_id) override; + void onRuntimeSendResult( + chat::MessageId msg_id, + bool has_protocol = false, + chat::MeshProtocol protocol = chat::MeshProtocol::Meshtastic) override; void onRuntimeUnreadChanged() override; void showKeyVerification( const ::ui::key_verification::KeyVerificationSnapshot& snapshot) override; diff --git a/modules/ui_shared/include/ui/screens/chat_watch/chat_conversation_components_watch.h b/modules/ui_shared/include/ui/screens/chat_watch/chat_conversation_components_watch.h index f1ea0549..d77bca12 100644 --- a/modules/ui_shared/include/ui/screens/chat_watch/chat_conversation_components_watch.h +++ b/modules/ui_shared/include/ui/screens/chat_watch/chat_conversation_components_watch.h @@ -36,7 +36,11 @@ class ChatConversationScreen void addMessage(const ::ui::chat::MessageRow& row); void clearMessages(); void scrollToBottom(); - bool updateMessageStatus(chat::MessageId msg_id, chat::MessageStatus status); + bool updateMessageStatus( + chat::MessageId msg_id, + chat::MessageStatus status, + bool has_protocol = false, + chat::MeshProtocol protocol = chat::MeshProtocol::Meshtastic); void setActionCallback(void (*cb)(ActionIntent intent, void*), void* user_data); void setMessageActionCallback( diff --git a/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp b/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp index fe626edc..ac9198f1 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp @@ -281,8 +281,9 @@ const char* delivery_status_text_key(::ui::chat::MessageDeliveryState delivery) case ::ui::chat::MessageDeliveryState::Queued: return "Queued"; case ::ui::chat::MessageDeliveryState::Sending: - case ::ui::chat::MessageDeliveryState::Sent: return "Sending..."; + case ::ui::chat::MessageDeliveryState::Sent: + return "Sent"; case ::ui::chat::MessageDeliveryState::Delivered: return "Delivered"; case ::ui::chat::MessageDeliveryState::Failed: @@ -351,8 +352,15 @@ void update_delivery_status_chip(lv_obj_t* status_label, } bool message_ref_matches_id(const ::ui::chat::MessageRef& ref, - chat::MessageId msg_id) + chat::MessageId msg_id, + bool has_protocol, + chat::MeshProtocol protocol) { + if (has_protocol && ref.protocol != 0 && + ref.protocol != static_cast(protocol)) + { + return false; + } if (ref.protocol_id != 0) { return ref.protocol_id == msg_id; @@ -740,7 +748,9 @@ void ChatConversationScreen::scrollToBottom() } bool ChatConversationScreen::updateMessageStatus(const chat::MessageId msg_id, - const chat::MessageStatus status) + const chat::MessageStatus status, + bool has_protocol, + chat::MeshProtocol protocol) { if (!guard_ || !guard_->alive || msg_id == 0) { @@ -749,7 +759,7 @@ bool ChatConversationScreen::updateMessageStatus(const chat::MessageId msg_id, for (auto& item : messages_) { - if (!message_ref_matches_id(item.ref, msg_id)) + if (!message_ref_matches_id(item.ref, msg_id, has_protocol, protocol)) { continue; } diff --git a/modules/ui_shared/src/ui/screens/chat/chat_page_runtime.cpp b/modules/ui_shared/src/ui/screens/chat/chat_page_runtime.cpp index 86460b4e..be525ac9 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_page_runtime.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_page_runtime.cpp @@ -49,7 +49,13 @@ class ChatServiceRetryChatMessagePort final ::chat::delivery::ChatDeliveryActionFailure::InvalidRef); } - const ::chat::ChatMessage* message = chat_service_.getMessage(msg_id); + const bool has_protocol = ref.protocol != 0; + const auto protocol = + static_cast<::chat::MeshProtocol>(ref.protocol); + const ::chat::ChatMessage* message = + has_protocol ? chat_service_.getMessageForProtocol(msg_id, + protocol) + : chat_service_.getMessage(msg_id); if (message == nullptr) { return ::chat::delivery::ChatDeliveryActionResult::fail( @@ -61,7 +67,11 @@ class ChatServiceRetryChatMessagePort final ::chat::delivery::ChatDeliveryActionFailure::NotRetryable); } - if (!chat_service_.resendFailed(msg_id)) + const bool resent = + has_protocol ? chat_service_.resendFailedForProtocol(msg_id, + protocol) + : chat_service_.resendFailed(msg_id); + if (!resent) { return ::chat::delivery::ChatDeliveryActionResult::fail( ::chat::delivery::ChatDeliveryActionFailure::Rejected); @@ -256,29 +266,43 @@ void enter(const shell::Host* host, lv_obj_t* parent) ? chat::ChannelId::SECONDARY : chat::ChannelId::PRIMARY; auto& chat_service = app::messagingFacade().getChatService(); - s_delivery_read_model = - std::unique_ptr<::chat::delivery::ChatDeliveryReadModel>( - new ::chat::delivery::ChatDeliveryReadModel()); - s_delivery_projector = - std::unique_ptr<::chat::delivery::ChatDeliveryEventProjector>( - new ::chat::delivery::ChatDeliveryEventProjector( - *s_delivery_read_model)); - s_delivery_event_port = - std::unique_ptr<::chat::delivery::ProjectingChatDeliveryEventPort>( - new ::chat::delivery::ProjectingChatDeliveryEventPort( - *s_delivery_projector)); - s_delivery_event_adapter = - std::unique_ptr<::ui_chat_runtime::ChatDeliveryEventProjectionAdapter>( - new ::ui_chat_runtime::ChatDeliveryEventProjectionAdapter( - chat_service, - *s_delivery_event_port)); + ::chat::delivery::ChatDeliveryReadModel* delivery_read_model = + app::messagingFacade().getChatDeliveryReadModel(); + ::chat::delivery::IChatDeliveryEventPort* delivery_event_port = + app::messagingFacade().getChatDeliveryEventPort(); + const bool using_facade_delivery = + delivery_read_model != nullptr && delivery_event_port != nullptr; + if (!using_facade_delivery) + { + s_delivery_read_model = + std::unique_ptr<::chat::delivery::ChatDeliveryReadModel>( + new ::chat::delivery::ChatDeliveryReadModel()); + s_delivery_projector = + std::unique_ptr<::chat::delivery::ChatDeliveryEventProjector>( + new ::chat::delivery::ChatDeliveryEventProjector( + *s_delivery_read_model)); + s_delivery_event_port = + std::unique_ptr<::chat::delivery::ProjectingChatDeliveryEventPort>( + new ::chat::delivery::ProjectingChatDeliveryEventPort( + *s_delivery_projector)); + delivery_read_model = s_delivery_read_model.get(); + delivery_event_port = s_delivery_event_port.get(); + } + if (!using_facade_delivery) + { + s_delivery_event_adapter = + std::unique_ptr<::ui_chat_runtime::ChatDeliveryEventProjectionAdapter>( + new ::ui_chat_runtime::ChatDeliveryEventProjectionAdapter( + chat_service, + *delivery_event_port)); + } s_delivery_retry_port = std::unique_ptr( new ChatServiceRetryChatMessagePort(chat_service)); s_delivery_action_service = std::unique_ptr<::chat::delivery::ChatDeliveryActionService>( new ::chat::delivery::ChatDeliveryActionService( - *s_delivery_read_model, + *delivery_read_model, s_delivery_retry_port.get())); s_delivery_action_adapter = std::unique_ptr<::ui_chat_runtime::ChatDeliveryActionPortAdapter>( @@ -288,7 +312,7 @@ void enter(const shell::Host* host, lv_obj_t* parent) new ::ui::presentation_sources::ChatPresentationSource( chat_service, &app::messagingFacade().getContactService(), - s_delivery_read_model.get(), + delivery_read_model, app::messagingFacade().getMeshAdapter())); s_chat_sink = std::unique_ptr<::ui::presentation_sources::RuntimeChatActionSink>( new ::ui::presentation_sources::RuntimeChatActionSink(chat_service)); diff --git a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp index 8d7389b0..a14b72f7 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp @@ -864,12 +864,20 @@ void UiController::onRuntimeMessageArrived(chat::MessageId msg_id) refreshUnreadCounts(false); } -void UiController::onRuntimeSendResult(chat::MessageId msg_id) +void UiController::onRuntimeSendResult(chat::MessageId msg_id, + bool has_protocol, + chat::MeshProtocol protocol) { if (state_ == State::Conversation && conversation_) { - const ChatMessage* msg = service_.getMessage(msg_id); - if (!msg || !conversation_->updateMessageStatus(msg_id, msg->status)) + const ChatMessage* msg = + has_protocol ? service_.getMessageForProtocol(msg_id, protocol) + : service_.getMessage(msg_id); + if (!msg || + !conversation_->updateMessageStatus(msg_id, + msg->status, + has_protocol, + protocol)) { reloadConversationView(); } diff --git a/modules/ui_shared/src/ui/screens/chat_watch/chat_conversation_components_watch.cpp b/modules/ui_shared/src/ui/screens/chat_watch/chat_conversation_components_watch.cpp index bac95bac..f26609d5 100644 --- a/modules/ui_shared/src/ui/screens/chat_watch/chat_conversation_components_watch.cpp +++ b/modules/ui_shared/src/ui/screens/chat_watch/chat_conversation_components_watch.cpp @@ -35,8 +35,15 @@ constexpr size_t kMaxPrefixedSenderLen = 20; } bool message_ref_matches_id(const ::ui::chat::MessageRef& ref, - chat::MessageId msg_id) + chat::MessageId msg_id, + bool has_protocol, + chat::MeshProtocol protocol) { + if (has_protocol && ref.protocol != 0 && + ref.protocol != static_cast(protocol)) + { + return false; + } if (ref.protocol_id != 0) { return ref.protocol_id == msg_id; @@ -346,7 +353,9 @@ void ChatConversationScreen::scrollToBottom() } bool ChatConversationScreen::updateMessageStatus(const chat::MessageId msg_id, - const chat::MessageStatus status) + const chat::MessageStatus status, + bool has_protocol, + chat::MeshProtocol protocol) { if (!guard_ || !guard_->alive || msg_id == 0) { @@ -355,7 +364,7 @@ bool ChatConversationScreen::updateMessageStatus(const chat::MessageId msg_id, for (auto& item : messages_) { - if (!message_ref_matches_id(item.ref, msg_id)) + if (!message_ref_matches_id(item.ref, msg_id, has_protocol, protocol)) { continue; } diff --git a/modules/ui_shared/tests/test_chat_presentation_source.cpp b/modules/ui_shared/tests/test_chat_presentation_source.cpp index 604654d7..99cdc3ef 100644 --- a/modules/ui_shared/tests/test_chat_presentation_source.cpp +++ b/modules/ui_shared/tests/test_chat_presentation_source.cpp @@ -356,6 +356,21 @@ class PagingStore final : public ::chat::IChatStore return false; } + bool updateMessageStatusForProtocol(::chat::MessageId msg_id, + ::chat::MeshProtocol protocol, + ::chat::MessageStatus status) override + { + for (auto& msg : messages_) + { + if (msg.msg_id == msg_id && msg.protocol == protocol) + { + msg.status = status; + return true; + } + } + return false; + } + bool getMessage(::chat::MessageId msg_id, ::chat::ChatMessage* out) const override { @@ -373,6 +388,24 @@ class PagingStore final : public ::chat::IChatStore return false; } + bool getMessageForProtocol(::chat::MessageId msg_id, + ::chat::MeshProtocol protocol, + ::chat::ChatMessage* out) const override + { + for (const auto& msg : messages_) + { + if (msg.msg_id == msg_id && msg.protocol == protocol) + { + if (out) + { + *out = msg; + } + return true; + } + } + return false; + } + private: std::vector<::chat::ChatMessage> messages_; int unread_ = 0; @@ -516,6 +549,8 @@ int main() ::chat::delivery::ChatDeliveryRecord delivered{}; delivered.ref.protocol_id = 100; + delivered.ref.protocol = + static_cast(::chat::MeshProtocol::Meshtastic); delivered.state = ::chat::delivery::DeliveryState::Delivered; delivered.failure = ::chat::delivery::DeliveryFailureKind::None; assert(delivery_read_model.upsert(delivered)); @@ -574,7 +609,11 @@ int main() assert(radio_offline_send.failure == ui::UiActionFailure::RadioOffline); mesh.fail_returns_msg_id = true; assert(delivery_read_model.upsert(::chat::delivery::toFailedDeliveryRecord( - ::chat::delivery::ChatDeliveryRef{0, 101, 0}, + ::chat::delivery::ChatDeliveryRef{ + 0, + 101, + 0, + static_cast(::chat::MeshProtocol::Meshtastic)}, ::chat::delivery::SendFailureKind::PeerKeyMissing))); assert(source.buildChatWorkspaceSnapshot(request, snapshot)); assert(snapshot.message_count == 2); @@ -583,7 +622,11 @@ int main() ui::chat::MessageFailureKind::PeerKeyMissing); assert(delivery_read_model.upsert(::chat::delivery::toFailedDeliveryRecord( - ::chat::delivery::ChatDeliveryRef{0, 101, 0}, + ::chat::delivery::ChatDeliveryRef{ + 0, + 101, + 0, + static_cast(::chat::MeshProtocol::Meshtastic)}, ::chat::delivery::SendFailureKind::ChannelKeyMissing))); assert(source.buildChatWorkspaceSnapshot(request, snapshot)); assert(snapshot.message_count == 2); diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_adapter.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_adapter.h index 02914530..8bef6355 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_adapter.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_adapter.h @@ -11,6 +11,9 @@ #include "chat/ports/i_mesh_adapter.h" #include "chat/ports/i_mesh_peer_directory.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_announce_ingestor.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_attempt_ledger.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_notifier.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_planner.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_destination_registry.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_identity.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_link_manager.h" @@ -18,6 +21,7 @@ #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_network_page_client.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_packet_router.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_path_manager.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_peer_directory.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_ping_service.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_client.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_stamp_runtime.h" @@ -167,7 +171,6 @@ class LxmfAdapter : public IMeshAdapter reticulum::interfaces::ReticulumInterfaceSet interfaces_; uint32_t network_config_generation_ = 0; - IMeshPeerDirectory* peer_directory_ = nullptr; reticulum::interfaces::RxPacket rx_packet_scratch_{}; uint8_t announce_tx_signed_scratch_[reticulum::kReticulumMtu] = {}; uint8_t announce_tx_payload_scratch_[reticulum::kReticulumMtu] = {}; @@ -181,6 +184,7 @@ class LxmfAdapter : public IMeshAdapter ::chat::infra::IncomingDataQueue data_receive_queue_; runtime::DestinationRegistry destination_registry_; runtime::PathManager path_manager_; + runtime::DeliveryAttemptLedger delivery_attempt_ledger_; runtime::LinkManager link_manager_; runtime::AnnounceIngestor announce_ingestor_; runtime::ReticulumPacketRouter packet_router_; @@ -188,6 +192,8 @@ class LxmfAdapter : public IMeshAdapter runtime::NetworkPageClient network_page_client_; runtime::PropagationClient propagation_client_; runtime::LxstTelephonyClient lxst_telephony_client_; + runtime::PeerDirectoryService peer_directory_service_; + runtime::LxmfDeliveryNotifier delivery_notifier_; std::string user_long_name_; std::string user_short_name_; uint32_t last_announce_ms_ = 0; @@ -216,6 +222,16 @@ class LxmfAdapter : public IMeshAdapter uint8_t link_request_payload_scratch_[reticulum::kReticulumMtu] = {}; uint8_t link_request_packet_scratch_[reticulum::kReticulumMtu] = {}; uint8_t link_request_routed_scratch_[reticulum::kReticulumMtu] = {}; + uint8_t path_request_packet_scratch_[reticulum::kReticulumMtu] = {}; + uint8_t proof_packet_scratch_[reticulum::kReticulumMtu] = {}; + uint8_t routed_packet_scratch_[reticulum::kReticulumMtu] = {}; + uint8_t forward_packet_scratch_[reticulum::kReticulumMtu] = {}; + uint8_t lxmf_tx_packet_scratch_[reticulum::kReticulumMtu] = {}; + uint8_t encrypted_payload_scratch_[reticulum::kReticulumMtu] = {}; + uint8_t link_wire_payload_scratch_[reticulum::kReticulumMtu] = {}; + uint8_t link_packet_scratch_[reticulum::kReticulumMtu] = {}; + uint8_t resource_advertisement_scratch_[reticulum::kReticulumMtu] = {}; + uint8_t resource_hashmap_update_scratch_[reticulum::kReticulumMtu] = {}; std::size_t link_request_packet_len_ = 0; uint32_t last_peer_projection_ms_ = 0; uint32_t next_app_packet_id_ = 1; @@ -266,6 +282,8 @@ class LxmfAdapter : public IMeshAdapter const reticulum::ParsedPacket& packet); bool maybeForwardLinkPacket(const uint8_t* raw_packet, size_t raw_len, const reticulum::ParsedPacket& packet); + bool sendForwardPlan(const reticulum::ParsedPacket& packet, + const runtime::PacketForwardPlan& plan); bool handleLocalLinkPacket( const uint8_t* raw_packet, size_t raw_len, const reticulum::ParsedPacket& packet, @@ -312,6 +330,10 @@ class LxmfAdapter : public IMeshAdapter size_t* inout_len); bool queueReadyPropagationUpload(PendingPropagationUpload& upload, const PropagationPeerState& node); + MessageId propagationUploadMessageId( + const PendingPropagationUpload& upload); + MessageId takePropagationUploadMessageId( + const PendingPropagationUpload& upload); bool sendPropagationSyncRequest(LinkSession& session, PropagationSyncStage next_stage, const runtime::PropagationIdList* wants, @@ -348,38 +370,15 @@ class LxmfAdapter : public IMeshAdapter const reticulum::ParsedPacket& packet, reticulum::interfaces::InterfaceKind ingress_interface) const; bool rebroadcastAnnounce(const PathEntry& path, const reticulum::ParsedPacket& packet); - bool isDuplicatePacket(const uint8_t packet_hash[reticulum::kFullHashSize]); - void rememberPacket(const uint8_t packet_hash[reticulum::kFullHashSize]); - void rememberReversePath(const uint8_t proof_hash[reticulum::kTruncatedHashSize], - reticulum::interfaces::InterfaceId interface_id, - uint8_t expected_hops); - ReverseEntry* findReversePath(const uint8_t proof_hash[reticulum::kTruncatedHashSize]); - PendingPathRequest* findPendingPathRequest( - const uint8_t destination_hash[reticulum::kTruncatedHashSize]); - const PendingPathRequest* findPendingPathRequest( - const uint8_t destination_hash[reticulum::kTruncatedHashSize]) const; - void notePendingPathRequest(const uint8_t destination_hash[reticulum::kTruncatedHashSize], - uint32_t now_ms); - void resolvePendingPathRequest(const uint8_t destination_hash[reticulum::kTruncatedHashSize]); void cullTransportState(); void cullLinkSessions(); - PathEntry& upsertPath(const uint8_t destination_hash[reticulum::kTruncatedHashSize]); - const PathEntry* findPath(const uint8_t destination_hash[reticulum::kTruncatedHashSize]) const; - LinkRelayEntry& upsertLinkRelay(const uint8_t link_id[reticulum::kTruncatedHashSize]); - LinkRelayEntry* findLinkRelay(const uint8_t link_id[reticulum::kTruncatedHashSize]); LinkSession* findLinkSession(const uint8_t link_id[reticulum::kTruncatedHashSize]); LinkSession* findActiveLinkSessionByDestination(const uint8_t destination_hash[reticulum::kTruncatedHashSize], LocalDestinationKind kind); - PeerInfo* findPeerByNodeId(NodeId node_id); - const PeerInfo* findPeerByDestinationHash(const uint8_t hash[reticulum::kTruncatedHashSize]) const; - const PeerInfo* findPeerByIdentityHash(const uint8_t hash[reticulum::kTruncatedHashSize]) const; const ReticulumGroupDestinationConfig* findConfiguredGroupDestination( const uint8_t hash[reticulum::kTruncatedHashSize]) const; bool isConfiguredGroupDestination( const ReticulumPeerIdentity& destination) const; - PeerInfo& upsertPeer(const uint8_t destination_hash[reticulum::kTruncatedHashSize]); - PeerInfo* upsertPeerFromDirectoryRecord(const MeshPeerRecord& record, - bool queue_update); PeerInfo* findOrLoadPeerByNodeId(NodeId node_id); PeerInfo* findOrLoadPeerByDestinationHash( const uint8_t destination_hash[reticulum::kTruncatedHashSize]); @@ -482,7 +481,6 @@ class LxmfAdapter : public IMeshAdapter void closeLinkSession(LinkSession& session, LinkCloseReason reason = LinkCloseReason::LocalClose); void flushDeferredLinkPayloads(LinkSession& session); - void expirePath(const uint8_t destination_hash[reticulum::kTruncatedHashSize]); bool handleLinkDataPacket(LinkSession& session, const uint8_t* raw_packet, size_t raw_len, const reticulum::ParsedPacket& packet); diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_attempt_ledger.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_attempt_ledger.h new file mode 100644 index 00000000..07f64b2a --- /dev/null +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_attempt_ledger.h @@ -0,0 +1,171 @@ +/** + * @file lxmf_delivery_attempt_ledger.h + * @brief Reticulum/LXMF outbound delivery attempt and proof receipt owner. + */ + +#pragma once + +#include "chat/domain/chat_types.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_identity.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_memory.h" + +#include + +namespace chat::lxmf::runtime +{ + +enum class DeliveryAttemptKind : uint8_t +{ + DirectPacket = 0, + LinkPacket = 1, + LinkResource = 2, + Propagation = 3, +}; + +struct DeliveryAttemptReceipt +{ + uint8_t packet_hash[reticulum::kFullHashSize] = {}; + uint8_t proof_hash[reticulum::kTruncatedHashSize] = {}; + uint8_t link_id[reticulum::kTruncatedHashSize] = {}; + uint8_t destination_hash[reticulum::kTruncatedHashSize] = {}; + uint8_t peer_sig_pub[LxmfIdentity::kSigPubKeySize] = {}; + MessageId message_id = 0; + uint32_t created_ms = 0; + DeliveryAttemptKind kind = DeliveryAttemptKind::DirectPacket; +}; + +class DeliveryAttemptLedger +{ + public: + DeliveryAttemptLedger() = default; + DeliveryAttemptLedger(const DeliveryAttemptLedger&) = delete; + DeliveryAttemptLedger& operator=(const DeliveryAttemptLedger&) = delete; + DeliveryAttemptLedger(DeliveryAttemptLedger&&) = delete; + DeliveryAttemptLedger& operator=(DeliveryAttemptLedger&&) = delete; + + void noteDirectPacketReceipt( + const uint8_t packet_hash[reticulum::kFullHashSize], + const uint8_t destination_hash[reticulum::kTruncatedHashSize], + const uint8_t peer_sig_pub[LxmfIdentity::kSigPubKeySize], + MessageId message_id, + uint32_t now_ms, + std::size_t max_receipts); + void noteLinkPacketReceipt( + const uint8_t packet_hash[reticulum::kFullHashSize], + const uint8_t link_id[reticulum::kTruncatedHashSize], + MessageId message_id, + uint32_t now_ms, + std::size_t max_receipts); + void noteLinkResourceReceipt( + const uint8_t resource_hash[reticulum::kFullHashSize], + const uint8_t link_id[reticulum::kTruncatedHashSize], + MessageId message_id, + uint32_t now_ms, + std::size_t max_receipts); + void notePropagationReceipt( + const uint8_t transient_id[reticulum::kFullHashSize], + MessageId message_id, + uint32_t now_ms, + std::size_t max_receipts); + + DeliveryAttemptReceipt* findReceiptByProofHash( + const uint8_t proof_hash[reticulum::kTruncatedHashSize]); + void removeReceiptByProofHash( + const uint8_t proof_hash[reticulum::kTruncatedHashSize]); + DeliveryAttemptReceipt* findLinkPacketReceipt( + const uint8_t link_id[reticulum::kTruncatedHashSize], + const uint8_t packet_hash[reticulum::kFullHashSize]); + void removeLinkPacketReceipt( + const uint8_t link_id[reticulum::kTruncatedHashSize], + const uint8_t packet_hash[reticulum::kFullHashSize]); + DeliveryAttemptReceipt* findLinkResourceReceipt( + const uint8_t link_id[reticulum::kTruncatedHashSize], + const uint8_t resource_hash[reticulum::kFullHashSize]); + void removeLinkResourceReceipt( + const uint8_t link_id[reticulum::kTruncatedHashSize], + const uint8_t resource_hash[reticulum::kFullHashSize]); + DeliveryAttemptReceipt* findPropagationReceipt( + const uint8_t transient_id[reticulum::kFullHashSize]); + void removePropagationReceipt( + const uint8_t transient_id[reticulum::kFullHashSize]); + + template + void forEachReceipt(Fn&& fn) const + { + for (const auto& receipt : receipts_) + { + fn(receipt); + } + } + + template + void takeReceiptsForLink( + const uint8_t link_id[reticulum::kTruncatedHashSize], + Fn&& fn) + { + if (!link_id) + { + return; + } + for (auto it = receipts_.begin(); it != receipts_.end();) + { + if (std::memcmp(it->link_id, link_id, sizeof(it->link_id)) == 0) + { + const DeliveryAttemptReceipt receipt = *it; + it = receipts_.erase(it); + fn(receipt); + } + else + { + ++it; + } + } + } + + template + void takeExpiredReceipts(DeliveryAttemptKind kind, + uint32_t now_ms, + uint32_t receipt_ttl_ms, + Fn&& fn) + { + if (receipt_ttl_ms == 0) + { + return; + } + for (auto it = receipts_.begin(); it != receipts_.end();) + { + if (it->kind == kind && + (it->created_ms == 0 || + now_ms - it->created_ms > receipt_ttl_ms)) + { + const DeliveryAttemptReceipt receipt = *it; + it = receipts_.erase(it); + fn(receipt); + } + else + { + ++it; + } + } + } + + void cull(DeliveryAttemptKind kind, + uint32_t now_ms, + uint32_t receipt_ttl_ms, + std::size_t max_receipts); + void clear(); + std::size_t size() const; + + private: + using ReceiptVector = + std::vector>; + + void trimOldestReceipts(DeliveryAttemptKind kind, + std::size_t max_receipts, + bool reserve_slot); + + ReceiptVector receipts_; +}; + +} // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_notifier.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_notifier.h new file mode 100644 index 00000000..7e429167 --- /dev/null +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_notifier.h @@ -0,0 +1,30 @@ +/** + * @file lxmf_delivery_notifier.h + * @brief Reticulum/LXMF outbound delivery status publisher. + */ + +#pragma once + +#include "chat/domain/chat_types.h" + +namespace chat::lxmf::runtime +{ + +class LxmfDeliveryNotifier +{ + public: + LxmfDeliveryNotifier() = default; + LxmfDeliveryNotifier(const LxmfDeliveryNotifier&) = delete; + LxmfDeliveryNotifier& operator=(const LxmfDeliveryNotifier&) = delete; + LxmfDeliveryNotifier(LxmfDeliveryNotifier&&) = delete; + LxmfDeliveryNotifier& operator=(LxmfDeliveryNotifier&&) = delete; + + void publish(MessageId message_id, MessageStatus status) const; + void publish(MessageId message_id, bool success) const; + void queued(MessageId message_id) const; + void sent(MessageId message_id) const; + void delivered(MessageId message_id) const; + void failed(MessageId message_id) const; +}; + +} // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_planner.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_planner.h new file mode 100644 index 00000000..d505e6ed --- /dev/null +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_planner.h @@ -0,0 +1,53 @@ +/** + * @file lxmf_delivery_planner.h + * @brief Reticulum/LXMF outbound delivery route decision owner. + */ + +#pragma once + +#include "chat/domain/reticulum_network_config.h" + +namespace chat::lxmf::runtime +{ + +enum class OutboundDeliveryPath : uint8_t +{ + None = 0, + Link, + Opportunistic, + DeferredLink, + Propagation, +}; + +struct OutboundDeliveryPlanInput +{ + bool has_active_link = false; + bool peer_has_usable_ratchet = false; + bool propagation_enabled = false; + chat::reticulum::LxmfDeliveryPreference propagation_preference = + chat::reticulum::LxmfDeliveryPreference::Automatic; + bool propagation_peer_available = false; +}; + +struct OutboundDeliveryPlan +{ + OutboundDeliveryPath path = OutboundDeliveryPath::None; + bool propagation_only = false; + bool propagation_first = false; + bool may_fallback_to_link = true; +}; + +class ReticulumDeliveryPlanner +{ + public: + ReticulumDeliveryPlanner() = default; + ReticulumDeliveryPlanner(const ReticulumDeliveryPlanner&) = delete; + ReticulumDeliveryPlanner& operator=(const ReticulumDeliveryPlanner&) = delete; + ReticulumDeliveryPlanner(ReticulumDeliveryPlanner&&) = delete; + ReticulumDeliveryPlanner& operator=(ReticulumDeliveryPlanner&&) = delete; + + static OutboundDeliveryPlan plan(const OutboundDeliveryPlanInput& input); + static const char* pathName(OutboundDeliveryPath path); +}; + +} // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_runtime.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_runtime.h index 532ee129..fc3cfda0 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_runtime.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_runtime.h @@ -7,11 +7,11 @@ #include "chat/domain/chat_types.h" #include "chat/infra/lxmf/lxmf_wire.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_memory.h" #include #include #include -#include namespace chat::lxmf::runtime { @@ -49,7 +49,7 @@ struct LxmfMaterialisedText struct LxmfMaterialisedAppData { MeshIncomingData incoming{}; - std::vector payload; + RuntimeByteBuffer payload; }; struct LxmfVerifiedDelivery diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_link_manager.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_link_manager.h index aa4adc5f..2501fc0c 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_link_manager.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_link_manager.h @@ -11,6 +11,29 @@ namespace chat::lxmf::runtime { +struct LinkSessionSpec +{ + const uint8_t* link_id = nullptr; + const uint8_t* remote_destination_hash = nullptr; + const uint8_t* remote_identity_hash = nullptr; + const uint8_t* local_sig_pub = nullptr; + const uint8_t* peer_enc_pub = nullptr; + const uint8_t* peer_link_sig_pub = nullptr; + const uint8_t* peer_identity_sig_pub = nullptr; + uint32_t now_ms = 0; + uint32_t keepalive_interval_ms = 15000; + uint32_t stale_timeout_ms = 30000; + uint16_t mtu = reticulum::kReticulumMtu; + uint16_t mdu = reticulum::kReticulumMdu; + uint8_t interface_id = 0; + uint8_t expected_hops = 0; + LocalDestinationKind destination = LocalDestinationKind::Delivery; + LinkState state = LinkState::Pending; + bool initiator = false; + bool remote_identity_known = false; + bool validated = false; +}; + class LinkManager { public: @@ -32,15 +55,52 @@ class LinkManager const uint8_t destination_hash[reticulum::kTruncatedHashSize], LocalDestinationKind kind); - LinkSession* appendSession(std::size_t max_link_sessions); - LinkSession* appendSessionPreserving( + LinkSession* openSession(std::size_t max_link_sessions, + const LinkSessionSpec& spec); + LinkSession* openSessionPreserving( std::size_t max_link_sessions, + const LinkSessionSpec& spec, const uint8_t preserve_link_id[reticulum::kTruncatedHashSize]); - void discardLastSession(); + bool discardSession(LinkSession& session); bool closeSession(LinkSession& session, LinkCloseReason reason, uint32_t now_ms); + LinkPendingRequest* queuePendingRequest(LinkSession& session, + const uint8_t* request_id, + std::size_t request_id_len, + uint32_t created_ms, + bool awaiting_resource); + LinkPendingRequest* findPendingRequest(LinkSession& session, + const uint8_t* request_id, + std::size_t request_id_len); + const LinkPendingRequest* findPendingRequest( + const LinkSession& session, + const uint8_t* request_id, + std::size_t request_id_len) const; + bool markPendingResponseReady(LinkSession& session, + const uint8_t* request_id, + std::size_t request_id_len, + const uint8_t* response_data, + std::size_t response_len, + bool data_is_nil); + bool erasePendingRequest(LinkSession& session, + const LinkPendingRequest& request); + std::size_t pendingRequestCount(const LinkSession& session) const; + DeferredLinkPayload* appendDeferredPayload( + LinkSession& session, + DeferredLinkPayload&& payload); + const DeferredLinkPayload* firstDeferredPayload( + const LinkSession& session) const; + bool popFirstDeferredPayload(LinkSession& session); + std::size_t deferredPayloadCount(const LinkSession& session) const; + void touchInbound(LinkSession& session, uint32_t now_ms); + void touchOutbound(LinkSession& session, uint32_t now_ms); + void noteKeepaliveSent(LinkSession& session, uint32_t now_ms); + void markSessionValidatedActive(LinkSession& session, + float rtt_s, + uint32_t keepalive_interval_ms); + bool reactivateSessionIfStale(LinkSession& session); void cullSessionTables(LinkSession& session, uint32_t now_ms, const LinkRuntimeLimits& limits); @@ -133,36 +193,20 @@ class LinkManager LinkResourceTransfer& resource, const uint8_t expected_proof[reticulum::kFullHashSize], uint32_t now_ms); - uint32_t takeResourceMessageId(LinkResourceTransfer& resource); void touchResource(LinkResourceTransfer& resource, uint32_t now_ms); template - void takeTrackedOutgoingResourceMessageIds(LinkSession& session, Fn&& fn) + void forEachExpiredOutgoingResource(const LinkSession& session, + uint32_t now_ms, + uint32_t ttl_ms, + Fn&& fn) const { - for (auto& resource : session.outgoing_resources) + for (const auto& resource : session.outgoing_resources) { - if (resource.message_id != 0) + if (resource.last_activity_ms == 0 || + now_ms - resource.last_activity_ms > ttl_ms) { - fn(resource.message_id); - resource.message_id = 0; - } - } - } - - template - void takeExpiredOutgoingResourceMessageIds(LinkSession& session, - uint32_t now_ms, - uint32_t ttl_ms, - Fn&& fn) - { - for (auto& resource : session.outgoing_resources) - { - if (resource.message_id != 0 && - (resource.last_activity_ms == 0 || - now_ms - resource.last_activity_ms > ttl_ms)) - { - fn(resource.message_id); - resource.message_id = 0; + fn(resource); } } } @@ -197,7 +241,50 @@ class LinkManager } } + template + LinkPendingRequest* findPendingRequestIf(LinkSession& session, + Predicate&& predicate) + { + for (auto& request : session.pending_requests) + { + if (predicate(request)) + { + return &request; + } + } + return nullptr; + } + + template + const LinkPendingRequest* findPendingRequestIf( + const LinkSession& session, + Predicate&& predicate) const + { + for (const auto& request : session.pending_requests) + { + if (predicate(request)) + { + return &request; + } + } + return nullptr; + } + + template + void forEachDeferredPayload(const LinkSession& session, Fn&& fn) const + { + for (const auto& payload : session.deferred_payloads) + { + fn(payload); + } + } + private: + LinkSession* appendSession(std::size_t max_link_sessions); + LinkSession* appendSessionPreserving( + std::size_t max_link_sessions, + const uint8_t preserve_link_id[reticulum::kTruncatedHashSize]); + void initialiseSession(LinkSession& session, const LinkSessionSpec& spec); bool ensureCapacity(std::size_t max_link_sessions, const uint8_t preserve_link_id[reticulum::kTruncatedHashSize]); diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_lxst_telephony_client.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_lxst_telephony_client.h index 55151e76..6fc8ab21 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_lxst_telephony_client.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_lxst_telephony_client.h @@ -5,7 +5,7 @@ #pragma once -#include "chat/infra/lxmf/lxmf_wire.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_runtime_state.h" #include @@ -25,6 +25,31 @@ class LxstTelephonyClient const uint8_t* scratch() const; std::size_t scratchCapacity() const; + bool isCallSession(const LinkSession& session) const; + bool isSidebandSession(const LinkSession& session) const; + bool runtimeStarted(const LinkSession& session) const; + void markRuntimeStarted(LinkSession& session, bool started); + void beginCallerSession(LinkSession& session, + ReticulumCallWireProfile wire_profile, + uint16_t profile, + uint32_t now_ms); + void beginSidebandCalleeSession(LinkSession& session, + uint16_t profile, + uint32_t now_ms); + uint16_t profile(const LinkSession& session) const; + reticulum::lxst::call::Phase phase(const LinkSession& session) const; + const reticulum::lxst::call::State& state( + const LinkSession& session) const; + bool phaseTimedOut(const LinkSession& session, uint32_t now_ms) const; + reticulum::lxst::call::Transition dispatch( + LinkSession& session, + const reticulum::lxst::call::Event& event, + uint32_t now_ms, + reticulum::lxst::call::Phase* out_previous_phase); + bool encodeSignal(uint16_t signal, + uint8_t** out_payload, + std::size_t* out_len); + private: uint8_t scratch_[reticulum::kReticulumMtu] = {}; }; diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_network_page_client.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_network_page_client.h index b1da3cf7..ec5731ce 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_network_page_client.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_network_page_client.h @@ -6,6 +6,7 @@ #pragma once #include "chat/infra/lxmf/lxmf_wire.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_memory.h" #include #include @@ -27,6 +28,10 @@ struct PendingNomadPageRequest bool request_sent = false; }; +using PendingNomadPageRequestList = + std::vector>; + enum class NetworkPageQueueResult { Queued, @@ -64,6 +69,26 @@ class NetworkPageClient const uint8_t destination_hash[reticulum::kTruncatedHashSize], const uint8_t* request_id, std::size_t request_id_len); + bool attemptDue(const PendingNomadPageRequest& request, + uint32_t now_ms, + uint32_t retry_interval_ms) const; + bool pathRequestDue(const PendingNomadPageRequest& request, + uint32_t now_ms, + uint32_t retry_interval_ms) const; + uint32_t lastAttemptAge(const PendingNomadPageRequest& request, + uint32_t now_ms) const; + void noteAttempt(PendingNomadPageRequest& request, uint32_t now_ms); + void notePathRequest(PendingNomadPageRequest& request, + bool sent, + uint32_t now_ms); + void noteLinkStart(PendingNomadPageRequest& request, + bool sent, + uint32_t now_ms, + bool accumulate_success); + bool noteRequestPacketSent(PendingNomadPageRequest& request, + const uint8_t* request_id, + std::size_t request_id_len, + uint32_t now_ms); template void forEach(Fn&& fn) @@ -84,7 +109,7 @@ class NetworkPageClient } private: - std::vector pending_; + PendingNomadPageRequestList pending_; }; } // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_packet_router.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_packet_router.h index 2efc4b12..331ced68 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_packet_router.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_packet_router.h @@ -6,6 +6,7 @@ #pragma once #include "chat/infra/lxmf/lxmf_wire.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_runtime_state.h" namespace chat::lxmf::runtime { @@ -19,10 +20,32 @@ enum class PacketRoute LinkOrTransport, }; +enum class PacketForwardHeader : uint8_t +{ + None = 0, + Header1Broadcast = 1, + Header2Transport = 2, +}; + +struct PacketForwardPlan +{ + bool forward = false; + PacketForwardHeader header = PacketForwardHeader::None; + uint8_t interface_id = 0; + uint8_t next_hop_transport[reticulum::kTruncatedHashSize] = {}; + uint8_t hops = 0; +}; + class ReticulumPacketRouter { public: PacketRoute route(const reticulum::ParsedPacket& packet) const; + PacketForwardPlan planPathForward(const PathEntry& path, + uint8_t packet_hops) const; + PacketForwardPlan planLinkRelayForward( + const LinkRelayEntry& relay, + uint8_t ingress_interface_id, + uint8_t packet_hops) const; }; } // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_path_manager.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_path_manager.h index 6ea64939..2f43edf9 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_path_manager.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_path_manager.h @@ -47,6 +47,19 @@ class PathManager std::size_t max_pending_path_requests); void resolvePendingPathRequest( const uint8_t destination_hash[reticulum::kTruncatedHashSize]); + bool pendingPathRequestCoolingDown( + const uint8_t destination_hash[reticulum::kTruncatedHashSize], + uint32_t now_ms, + uint32_t retry_interval_ms) const; + bool shouldRequestPeerPath(const PeerInfo& peer, + uint32_t now_ms, + uint32_t now_s, + uint32_t pending_request_ttl_ms, + uint32_t min_request_interval_ms, + uint32_t path_ttl_ms, + uint32_t refresh_age_s) const; + void notePeerPathRequest(PeerInfo& peer, uint32_t now_ms) const; + void resetPeerPathRequest(PeerInfo& peer) const; void notePendingPingReceipt( const uint8_t packet_hash[reticulum::kFullHashSize], @@ -59,21 +72,20 @@ class PathManager void removePendingPingReceipt( const uint8_t proof_hash[reticulum::kTruncatedHashSize]); - void notePendingDeliveryReceipt( - const uint8_t packet_hash[reticulum::kFullHashSize], - const uint8_t destination_hash[reticulum::kTruncatedHashSize], - const uint8_t peer_sig_pub[LxmfIdentity::kSigPubKeySize], - MessageId message_id, - uint32_t now_ms, - std::size_t max_pending_delivery_receipts); - PendingDeliveryReceipt* findPendingDeliveryReceipt( - const uint8_t proof_hash[reticulum::kTruncatedHashSize]); - void removePendingDeliveryReceipt( - const uint8_t proof_hash[reticulum::kTruncatedHashSize]); - PathEntry& upsertPath( const uint8_t destination_hash[reticulum::kTruncatedHashSize], std::size_t max_paths); + PathEntry* observeAnnouncePath( + const uint8_t destination_hash[reticulum::kTruncatedHashSize], + uint8_t hops, + const uint8_t random_hash[10], + uint32_t now_ms, + uint32_t now_s, + uint8_t ingress_interface_id, + const uint8_t* next_hop_transport, + const uint8_t* raw_packet, + std::size_t raw_len, + std::size_t max_paths); const PathEntry* findPath( const uint8_t destination_hash[reticulum::kTruncatedHashSize], uint32_t now_ms, @@ -114,15 +126,6 @@ class PathManager } } - template - void forEachPendingDeliveryReceipt(Fn&& fn) const - { - for (const auto& receipt : transport_.pending_delivery_receipts) - { - fn(receipt); - } - } - private: TransportRuntime transport_; }; diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_peer_directory.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_peer_directory.h new file mode 100644 index 00000000..f1ecbeb0 --- /dev/null +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_peer_directory.h @@ -0,0 +1,92 @@ +/** + * @file lxmf_peer_directory.h + * @brief Reticulum peer directory application service. + */ + +#pragma once + +#include "chat/ports/i_mesh_peer_directory.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_destination_registry.h" + +#include +#include + +namespace chat::lxmf::runtime +{ + +struct PeerDirectoryLoadResult +{ + PeerInfo* peer = nullptr; + MeshPeerDirectoryStatus status = MeshPeerDirectoryStatus::success(); + bool loaded_from_directory = false; +}; + +struct PeerDirectoryWriteResult +{ + MeshPeerDirectoryStatus record_status = MeshPeerDirectoryStatus::success(); + MeshPeerDirectoryStatus flags_status = MeshPeerDirectoryStatus::success(); + bool flags_attempted = false; + + bool succeeded() const + { + return record_status.succeeded() && flags_status.succeeded(); + } +}; + +struct PeerDirectoryLoadRecentResult +{ + MeshPeerDirectoryStatus status = MeshPeerDirectoryStatus::success(); + std::size_t scanned = 0; + std::size_t loaded = 0; +}; + +ReticulumPeerIdentity reticulumIdentityForPeer(const PeerInfo& peer); + +class PeerDirectoryService +{ + public: + explicit PeerDirectoryService(IMeshPeerDirectory* directory = nullptr); + PeerDirectoryService(const PeerDirectoryService&) = delete; + PeerDirectoryService& operator=(const PeerDirectoryService&) = delete; + PeerDirectoryService(PeerDirectoryService&&) = delete; + PeerDirectoryService& operator=(PeerDirectoryService&&) = delete; + + void setDirectory(IMeshPeerDirectory* directory); + bool hasDirectory() const; + + PeerInfo* applyRecord(DestinationRegistry& registry, + const MeshPeerRecord& record, + uint32_t now_s) const; + + PeerDirectoryLoadResult findOrLoadByNodeId( + DestinationRegistry& registry, + NodeId node_id, + uint32_t now_s) const; + + PeerDirectoryLoadResult findOrLoadByDestinationHash( + DestinationRegistry& registry, + const uint8_t destination_hash[reticulum::kTruncatedHashSize], + uint32_t now_s) const; + + MeshActionResult persistPeerAddressNow(const PeerInfo& peer, + bool favorite, + uint32_t now_s) const; + + PeerDirectoryWriteResult recordPeer(const PeerInfo& peer, + MeshPeerSource source, + bool update_favorite, + bool favorite, + uint32_t now_s) const; + + PeerDirectoryLoadRecentResult loadRecent(DestinationRegistry& registry, + MeshPeerRecord* scratch, + std::size_t scratch_count, + NodeId* out_loaded_nodes, + std::size_t max_loaded_nodes, + uint32_t now_s) const; + + private: + IMeshPeerDirectory* directory_ = nullptr; +}; + +} // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_ping_service.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_ping_service.h index 8ec1a1bd..0369f6e9 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_ping_service.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_ping_service.h @@ -6,6 +6,7 @@ #pragma once #include "chat/infra/lxmf/lxmf_wire.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_memory.h" #include #include @@ -22,6 +23,9 @@ struct PendingPingRequest uint32_t last_send_attempt_ms = 0; }; +using PendingPingRequestList = + std::vector>; + enum class PendingPingQueueResult { Queued, @@ -107,7 +111,7 @@ class PingService } private: - std::vector pending_; + PendingPingRequestList pending_; }; } // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_client.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_client.h index 058b5864..dfa3a657 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_client.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_client.h @@ -51,9 +51,18 @@ class PropagationClient PendingPropagationUpload* firstPendingUpload(); const PendingPropagationUpload* firstPendingUpload() const; bool removeFirstPendingUpload(); + void markUploadWaitingForNode(PendingPropagationUpload& upload); + bool bindUploadNode(PendingPropagationUpload& upload, + const PropagationPeerState& node); + bool beginUploadStamp(PendingPropagationUpload& upload); + bool completeUploadStamp( + PendingPropagationUpload& upload, + const uint8_t stamp[reticulum::kFullHashSize]); + void markUploadFailed(PendingPropagationUpload& upload); + void markUploadQueuedToLink(PendingPropagationUpload& upload); void markExpiredUploads(uint32_t now_ms, uint32_t ttl_ms); - std::vector takeFailedUploads(); - std::vector takeAllPendingUploads(); + PendingPropagationUploadList takeFailedUploads(); + PendingPropagationUploadList takeAllPendingUploads(); void resetStampingUploads(); void resetForDisabled(); diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_service_runtime.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_service_runtime.h index cfb1b93c..e4befaa8 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_service_runtime.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_service_runtime.h @@ -6,11 +6,11 @@ #pragma once #include "chat/infra/lxmf/lxmf_wire.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_memory.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_runtime.h" #include #include -#include namespace chat::lxmf::runtime { @@ -72,6 +72,10 @@ struct PropagationMessageAcceptance ResourcePayloadBuffer local_delivery_payload; }; +using PropagationMessageAcceptanceList = + std::vector>; + struct PropagationBatchContext { bool offer_validated = false; @@ -93,7 +97,7 @@ struct PropagationBatchAcceptance { bool remote_propagation_hash_known = false; uint8_t remote_propagation_hash[reticulum::kTruncatedHashSize] = {}; - std::vector messages; + PropagationMessageAcceptanceList messages; }; void propagationServicePathHash( diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_resource_runtime.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_resource_runtime.h index aa7a447d..6d5f1d6d 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_resource_runtime.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_resource_runtime.h @@ -10,7 +10,6 @@ #include #include #include -#include namespace chat::lxmf::runtime { @@ -27,7 +26,7 @@ struct ResourceWindowRequest bool valid = false; bool needs_more_hashmap = false; std::array last_known_hash = {}; - std::vector> requested_hashes; + RuntimeMapHashList requested_hashes; }; enum class ResourceAssemblyResult : uint8_t @@ -38,12 +37,12 @@ enum class ResourceAssemblyResult : uint8_t }; LinkResourceTransfer* findLinkResource( - std::vector& resources, + LinkResourceTransferList& resources, const uint8_t resource_hash[reticulum::kFullHashSize]); const LinkResourceTransfer* findLinkResource( - const std::vector& resources, + const LinkResourceTransferList& resources, const uint8_t resource_hash[reticulum::kFullHashSize]); -bool eraseLinkResourceByHash(std::vector& resources, +bool eraseLinkResourceByHash(LinkResourceTransferList& resources, const uint8_t resource_hash[reticulum::kFullHashSize]); LinkResourceAssembly* findLinkResourceAssembly( diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_runtime_state.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_runtime_state.h index e7e2c5ff..918f2ac9 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_runtime_state.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_runtime_state.h @@ -54,12 +54,17 @@ struct PathEntry bool direct = false; }; +using PathEntryList = std::vector>; + struct PacketFilterEntry { uint8_t packet_hash[reticulum::kFullHashSize] = {}; uint32_t seen_ms = 0; }; +using PacketFilterEntryList = + std::vector>; + struct ReverseEntry { uint8_t proof_hash[reticulum::kTruncatedHashSize] = {}; @@ -68,6 +73,9 @@ struct ReverseEntry uint32_t created_ms = 0; }; +using ReverseEntryList = + std::vector>; + struct PendingPathRequest { uint8_t destination_hash[reticulum::kTruncatedHashSize] = {}; @@ -77,6 +85,9 @@ struct PendingPathRequest bool resolved = false; }; +using PendingPathRequestList = + std::vector>; + struct PendingPingReceipt { uint8_t packet_hash[reticulum::kFullHashSize] = {}; @@ -86,15 +97,8 @@ struct PendingPingReceipt uint32_t created_ms = 0; }; -struct PendingDeliveryReceipt -{ - uint8_t packet_hash[reticulum::kFullHashSize] = {}; - uint8_t proof_hash[reticulum::kTruncatedHashSize] = {}; - uint8_t destination_hash[reticulum::kTruncatedHashSize] = {}; - uint8_t peer_sig_pub[LxmfIdentity::kSigPubKeySize] = {}; - MessageId message_id = 0; - uint32_t created_ms = 0; -}; +using PendingPingReceiptList = + std::vector>; struct LinkRelayEntry { @@ -106,6 +110,9 @@ struct LinkRelayEntry uint32_t last_seen_ms = 0; }; +using LinkRelayEntryList = + std::vector>; + enum class LocalDestinationKind : uint8_t { Delivery = 0, @@ -141,6 +148,9 @@ struct LinkPendingRequest ResourcePayloadBuffer response; }; +using LinkPendingRequestList = + std::vector>; + struct DeferredLinkPayload { ResourcePayloadBuffer payload; @@ -149,12 +159,8 @@ struct DeferredLinkPayload uint8_t resource_flags = 0; }; -struct LinkPacketReceipt -{ - uint8_t packet_hash[reticulum::kFullHashSize] = {}; - uint32_t message_id = 0; - uint32_t created_ms = 0; -}; +using DeferredLinkPayloadList = + std::vector>; struct LinkResourceTransfer { @@ -178,7 +184,6 @@ struct LinkResourceTransfer uint32_t total_segments = 1; uint32_t created_ms = 0; uint32_t last_activity_ms = 0; - uint32_t message_id = 0; uint8_t flags = 0; bool incoming = true; bool encrypted = false; @@ -191,6 +196,9 @@ struct LinkResourceTransfer int32_t consecutive_complete_index = -1; }; +using LinkResourceTransferList = + std::vector>; + struct LinkResourceAssembly { uint8_t original_hash[reticulum::kFullHashSize] = {}; @@ -202,6 +210,9 @@ struct LinkResourceAssembly uint8_t flags = 0; }; +using LinkResourceAssemblyList = + std::vector>; + struct LinkSession { uint8_t link_id[reticulum::kTruncatedHashSize] = {}; @@ -239,14 +250,15 @@ struct LinkSession LinkState state = LinkState::Pending; LinkCloseReason close_reason = LinkCloseReason::None; bool propagation_offer_validated = false; - std::vector pending_requests; - std::vector deferred_payloads; - std::vector pending_packet_receipts; - std::vector incoming_resources; - std::vector incoming_resource_assemblies; - std::vector outgoing_resources; + LinkPendingRequestList pending_requests; + DeferredLinkPayloadList deferred_payloads; + LinkResourceTransferList incoming_resources; + LinkResourceAssemblyList incoming_resource_assemblies; + LinkResourceTransferList outgoing_resources; }; +using LinkSessionList = std::vector>; + struct PropagationEntry { uint8_t transient_id[reticulum::kFullHashSize] = {}; @@ -256,6 +268,9 @@ struct PropagationEntry uint32_t served_count = 0; }; +using PropagationEntryList = + std::vector>; + struct PropagationTransientEntry { uint8_t transient_id[reticulum::kFullHashSize] = {}; @@ -263,6 +278,10 @@ struct PropagationTransientEntry bool delivered = false; }; +using PropagationTransientEntryList = + std::vector>; + enum class PropagationUploadState : uint8_t { WaitingNode = 0, @@ -281,12 +300,14 @@ struct PendingPropagationUpload uint8_t transient_id[reticulum::kFullHashSize] = {}; ResourcePayloadBuffer transient_data; uint32_t created_ms = 0; - uint32_t message_id = 0; uint8_t stamp_cost = 0; PropagationUploadState state = PropagationUploadState::WaitingNode; - bool track_user_message = false; }; +using PendingPropagationUploadList = + std::vector>; + enum class PropagationDeliveryCommitState : uint8_t { AwaitingPersistence = 0, @@ -302,6 +323,10 @@ struct PendingPropagationDelivery PropagationDeliveryCommitState::AwaitingPersistence; }; +using PendingPropagationDeliveryList = + std::vector>; + enum class PropagationSyncStage : uint8_t { Idle = 0, @@ -337,29 +362,31 @@ struct PropagationPeerState bool node_active = false; }; +using PropagationPeerStateList = + std::vector>; + struct TransportRuntime { - std::vector paths; - std::vector packet_filter; - std::vector reverse_table; - std::vector pending_path_requests; - std::vector pending_ping_receipts; - std::vector pending_delivery_receipts; - std::vector link_relays; + PathEntryList paths; + PacketFilterEntryList packet_filter; + ReverseEntryList reverse_table; + PendingPathRequestList pending_path_requests; + PendingPingReceiptList pending_ping_receipts; + LinkRelayEntryList link_relays; }; struct LinkRuntime { - std::vector sessions; + LinkSessionList sessions; }; struct PropagationRuntime { - std::vector entries; - std::vector transients; - std::vector peers; - std::vector pending_uploads; - std::vector pending_deliveries; + PropagationEntryList entries; + PropagationTransientEntryList transients; + PropagationPeerStateList peers; + PendingPropagationUploadList pending_uploads; + PendingPropagationDeliveryList pending_deliveries; PropagationIdList sync_wants; PropagationIdList sync_haves; uint8_t active_node_hash[reticulum::kTruncatedHashSize] = {}; diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_transport_runtime.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_transport_runtime.h index d7a4fac5..cdf51467 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_transport_runtime.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/lxmf/lxmf_transport_runtime.h @@ -27,8 +27,6 @@ struct TransportRuntimeLimits std::size_t max_pending_ping_receipts = 0; uint32_t path_ttl_ms = 0; uint32_t pending_ping_receipt_ttl_ms = 0; - std::size_t max_pending_delivery_receipts = 0; - uint32_t pending_delivery_receipt_ttl_ms = 0; }; enum class PathAnnounceDecision : uint8_t @@ -99,21 +97,6 @@ void removePendingPingReceipt( TransportRuntime& transport, const uint8_t proof_hash[reticulum::kTruncatedHashSize]); -void notePendingDeliveryReceipt( - TransportRuntime& transport, - const uint8_t packet_hash[reticulum::kFullHashSize], - const uint8_t destination_hash[reticulum::kTruncatedHashSize], - const uint8_t peer_sig_pub[LxmfIdentity::kSigPubKeySize], - MessageId message_id, - uint32_t now_ms, - std::size_t max_pending_delivery_receipts); -PendingDeliveryReceipt* findPendingDeliveryReceipt( - TransportRuntime& transport, - const uint8_t proof_hash[reticulum::kTruncatedHashSize]); -void removePendingDeliveryReceipt( - TransportRuntime& transport, - const uint8_t proof_hash[reticulum::kTruncatedHashSize]); - PathEntry& upsertPath(TransportRuntime& transport, const uint8_t destination_hash[reticulum::kTruncatedHashSize], std::size_t max_paths); diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h index 0187e444..040d4d45 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h @@ -289,6 +289,7 @@ class MtAdapter : public chat::IMeshAdapter NodeId node_id = 0; ChannelId last_channel = ChannelId::PRIMARY; bool has_last_channel = false; + bool last_seen_via_mqtt = false; uint32_t nodeinfo_reply_ms = 0; uint32_t last_touch_ms = 0; }; @@ -376,6 +377,11 @@ class MtAdapter : public chat::IMeshAdapter void eraseNodeRuntime(uint32_t node_id); bool getNodeLastChannel(uint32_t node_id, ChannelId* out) const; void rememberNodeLastChannel(uint32_t node_id, ChannelId channel, uint32_t now_ms); + void rememberNodeRuntimeRx(uint32_t node_id, + ChannelId channel, + bool via_mqtt, + uint32_t now_ms); + bool nodeLastSeenViaMqtt(uint32_t node_id) const; uint32_t getNodeInfoReplyMs(uint32_t node_id) const; void setNodeInfoReplyMs(uint32_t node_id, uint32_t now_ms); bool sendRoutingAck(uint32_t dest, uint32_t request_id, uint8_t channel_hash, diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/sd_store.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/sd_store.h index d952e892..7b6984c1 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/sd_store.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/sd_store.h @@ -48,7 +48,13 @@ class SdStore final : public IChatStore void clearConversation(const ConversationId& conv) override; void clearAll() override; bool updateMessageStatus(MessageId msg_id, MessageStatus status) override; + bool updateMessageStatusForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status) override; bool getMessage(MessageId msg_id, ChatMessage* out) const override; + bool getMessageForProtocol(MessageId msg_id, + MeshProtocol protocol, + ChatMessage* out) const override; bool hasReticulumLxmfMessageHash(const uint8_t* lxmf_hash) const override; void flush() override; diff --git a/platform/esp/arduino_common/include/sys/event_bus.h b/platform/esp/arduino_common/include/sys/event_bus.h index e9d90dc4..af440045 100644 --- a/platform/esp/arduino_common/include/sys/event_bus.h +++ b/platform/esp/arduino_common/include/sys/event_bus.h @@ -12,6 +12,7 @@ #include #include +#include "chat/delivery/chat_delivery_types.h" #include "chat/domain/chat_types.h" #include "chat/domain/contact_types.h" #include "team/domain/team_events.h" @@ -120,15 +121,35 @@ struct ChatSendResultEvent : public Event uint32_t msg_id; bool success; chat::MessageStatus status; + chat::delivery::SendFailureKind failure = + chat::delivery::SendFailureKind::None; + bool has_protocol = false; + chat::MeshProtocol protocol = chat::MeshProtocol::Meshtastic; - ChatSendResultEvent(uint32_t id, bool ok) - : Event(EventType::ChatSendResult), msg_id(id), success(ok), - status(ok ? chat::MessageStatus::Sent : chat::MessageStatus::Failed) {} - - ChatSendResultEvent(uint32_t id, chat::MessageStatus result_status) + ChatSendResultEvent(uint32_t id, + chat::MessageStatus result_status, + chat::MeshProtocol source_protocol) : Event(EventType::ChatSendResult), msg_id(id), success(result_status != chat::MessageStatus::Failed), - status(result_status) {} + status(result_status), + failure(result_status == chat::MessageStatus::Failed + ? chat::delivery::SendFailureKind::Unknown + : chat::delivery::SendFailureKind::None), + has_protocol(true), + protocol(source_protocol) {} + + ChatSendResultEvent(uint32_t id, + chat::MessageStatus result_status, + chat::MeshProtocol source_protocol, + chat::delivery::SendFailureKind failure_kind) + : Event(EventType::ChatSendResult), msg_id(id), + success(result_status != chat::MessageStatus::Failed), + status(result_status), + failure(result_status == chat::MessageStatus::Failed + ? failure_kind + : chat::delivery::SendFailureKind::None), + has_protocol(true), + protocol(source_protocol) {} }; enum class ReticulumPingResult : uint8_t diff --git a/platform/esp/arduino_common/src/app_event_runtime_support.cpp b/platform/esp/arduino_common/src/app_event_runtime_support.cpp index f8d041fa..9dc71253 100644 --- a/platform/esp/arduino_common/src/app_event_runtime_support.cpp +++ b/platform/esp/arduino_common/src/app_event_runtime_support.cpp @@ -169,10 +169,16 @@ void handleTeamChatNotification(app::IAppFacade& app_context, const sys::TeamCha void handleChatSendResultFeedback(app::IAppFacade& app_context, const sys::ChatSendResultEvent& event) { + const chat::ChatMessage* message = + event.has_protocol + ? app_context.getChatService().getMessageForProtocol( + event.msg_id, + event.protocol) + : app_context.getChatService().getMessage(event.msg_id); chatDeliveryFeedback().onChatSendResult( event.msg_id, event.success, - app_context.getChatService().getMessage(event.msg_id)); + message); } void tickUiRuntime(app::IAppFacade& app_context) diff --git a/platform/esp/arduino_common/src/app_runtime_support.cpp b/platform/esp/arduino_common/src/app_runtime_support.cpp index fe7f8498..abdfbd87 100644 --- a/platform/esp/arduino_common/src/app_runtime_support.cpp +++ b/platform/esp/arduino_common/src/app_runtime_support.cpp @@ -463,8 +463,22 @@ bool dispatchEvent(app::IAppFacade& app_context, sys::Event* event) case sys::EventType::ChatSendResult: { auto* result_event = static_cast(event); - app_context.getChatService().handleSendResult(result_event->msg_id, - result_event->status); + if (result_event->has_protocol) + { + app_context.getChatService().handleSendResultForProtocol( + result_event->msg_id, + result_event->protocol, + result_event->status, + result_event->timestamp, + result_event->failure); + } + else + { + app_context.getChatService().handleSendResult(result_event->msg_id, + result_event->status, + result_event->timestamp, + result_event->failure); + } return false; } case sys::EventType::NodeInfoUpdate: diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter.cpp index 4a773902..455eee73 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter.cpp @@ -717,12 +717,6 @@ void copyHash(uint8_t* out, const uint8_t* in, size_t len) memcpy(out, in, len); } -::chat::ReticulumPeerIdentity reticulumIdentityForPeer(const runtime::PeerInfo& peer) -{ - return ::chat::makeReticulumPeerIdentity(peer.destination_hash, - peer.identity_hash); -} - MeshPeerSource meshPeerSourceFromDirectorySource(rtdir::EntrySource source) { switch (source) @@ -1011,14 +1005,14 @@ bool computeLinkIdFromLinkRequest(const uint8_t* raw_packet, size_t raw_len, return false; } - uint8_t scratch[kMaxPacketLen] = {}; - if (raw_len > sizeof(scratch)) + if (raw_len > kMaxPacketLen) { return false; } size_t working_len = raw_len; - memcpy(scratch, raw_packet, raw_len); + runtime::RuntimeByteBuffer scratch(raw_len, 0); + memcpy(scratch.data(), raw_packet, raw_len); if (packet.payload_len > 64) { @@ -1030,7 +1024,7 @@ bool computeLinkIdFromLinkRequest(const uint8_t* raw_packet, size_t raw_len, working_len -= trim; } - reticulum::computeTruncatedPacketHash(scratch, working_len, out_hash); + reticulum::computeTruncatedPacketHash(scratch.data(), working_len, out_hash); return true; } @@ -1039,7 +1033,7 @@ bool computeLinkIdFromLinkRequest(const uint8_t* raw_packet, size_t raw_len, LxmfAdapter::LxmfAdapter(LoraBoard& board, IMeshPeerDirectory* peer_directory) : interfaces_(board), - peer_directory_(peer_directory) + peer_directory_service_(peer_directory) { uint8_t seed[sizeof(next_app_packet_id_)] = {}; fillRandomBytes(seed, sizeof(seed)); @@ -1159,17 +1153,23 @@ bool LxmfAdapter::dispatchLxmfPayload(PeerInfo& peer, LocalDestinationKind::Delivery); const bool use_opportunistic = !active_link && peerHasUsableRatchet(peer); const auto& propagation_config = rtnet::active().propagation; - const bool propagation_only = - propagation_config.enabled && + bool propagation_peer_available = false; + if (propagation_config.enabled && propagation_config.delivery == - reticulum::LxmfDeliveryPreference::Propagated; - const bool propagation_automatic = - propagation_config.enabled && - propagation_config.delivery == - reticulum::LxmfDeliveryPreference::Automatic && - !active_link && !use_opportunistic && - selectActivePropagationPeer() != nullptr; - if (propagation_only || propagation_automatic) + chat::reticulum::LxmfDeliveryPreference::Automatic && + !active_link && !use_opportunistic) + { + propagation_peer_available = selectActivePropagationPeer() != nullptr; + } + const runtime::OutboundDeliveryPlan plan = + runtime::ReticulumDeliveryPlanner::plan( + runtime::OutboundDeliveryPlanInput{ + active_link != nullptr, + use_opportunistic, + propagation_config.enabled, + propagation_config.delivery, + propagation_peer_available}); + if (plan.propagation_first) { if (queuePropagationUpload(peer, lxmf_message.data(), @@ -1181,14 +1181,19 @@ bool LxmfAdapter::dispatchLxmfPayload(PeerInfo& peer, { return true; } - if (propagation_only) + if (plan.propagation_only) { out_dispatch->failure = MeshOperationFailure::RadioTxFailed; return false; } } - out_dispatch->path = - active_link ? "link" : (use_opportunistic ? "opportunistic" : "deferred_link"); + const runtime::OutboundDeliveryPath fallback_path = + active_link ? runtime::OutboundDeliveryPath::Link + : (use_opportunistic + ? runtime::OutboundDeliveryPath::Opportunistic + : runtime::OutboundDeliveryPath::DeferredLink); + out_dispatch->path = runtime::ReticulumDeliveryPlanner::pathName( + plan.propagation_first ? fallback_path : plan.path); if (active_link) { runtime::DeferredLinkPayload deferred{}; @@ -1197,7 +1202,7 @@ bool LxmfAdapter::dispatchLxmfPayload(PeerInfo& peer, deferred.message_id = track_user_message ? out_dispatch->message_id : 0; - active_link->deferred_payloads.push_back(std::move(deferred)); + link_manager_.appendDeferredPayload(*active_link, std::move(deferred)); flushDeferredLinkPayloads(*active_link); out_dispatch->ok = true; out_dispatch->result_event_deferred = track_user_message; @@ -1218,7 +1223,7 @@ bool LxmfAdapter::dispatchLxmfPayload(PeerInfo& peer, { uint8_t packet_hash[reticulum::kFullHashSize] = {}; reticulum::computePacketHash(packet.data(), packet_len, packet_hash); - path_manager_.notePendingDeliveryReceipt( + delivery_attempt_ledger_.noteDirectPacketReceipt( packet_hash, peer.destination_hash, peer.sig_pub, @@ -1228,7 +1233,7 @@ bool LxmfAdapter::dispatchLxmfPayload(PeerInfo& peer, } } - if (!active_link && !out_dispatch->ok) + if (plan.may_fallback_to_link && !active_link && !out_dispatch->ok) { bool started = false; LinkSession* session = @@ -1241,7 +1246,7 @@ bool LxmfAdapter::dispatchLxmfPayload(PeerInfo& peer, deferred.message_id = track_user_message ? out_dispatch->message_id : 0; - session->deferred_payloads.push_back(std::move(deferred)); + link_manager_.appendDeferredPayload(*session, std::move(deferred)); if (session->state == LinkState::Active) { flushDeferredLinkPayloads(*session); @@ -1381,17 +1386,10 @@ bool LxmfAdapter::queuePropagationUpload( upload.transient_data.size(), upload.transient_id); upload.created_ms = millis(); - upload.message_id = message_id; - upload.track_user_message = track_user_message; - upload.state = runtime::PropagationUploadState::WaitingNode; if (const PropagationPeerState* node = selectActivePropagationPeer()) { - copyHash(upload.node_hash, - node->propagation_hash, - sizeof(upload.node_hash)); - upload.stamp_cost = node->stamp_cost; - upload.state = runtime::PropagationUploadState::NeedsStamp; + propagation_client_.bindUploadNode(upload, *node); } PendingPropagationUpload* queued_upload = @@ -1401,15 +1399,17 @@ bool LxmfAdapter::queuePropagationUpload( { return false; } + if (track_user_message && message_id != 0) + { + delivery_attempt_ledger_.notePropagationReceipt( + queued_upload->transient_id, + message_id, + millis(), + kMaxPendingDeliveryReceipts); + } out_dispatch->ok = true; out_dispatch->result_event_deferred = track_user_message; out_dispatch->path = "propagation"; - if (track_user_message && message_id != 0) - { - sys::EventBus::publish( - new sys::ChatSendResultEvent(message_id, MessageStatus::Queued), - 0); - } char transient_hash[12] = {}; formatHashPrefix(queued_upload->transient_id, transient_hash, @@ -1425,6 +1425,22 @@ bool LxmfAdapter::queuePropagationUpload( return true; } +MessageId LxmfAdapter::propagationUploadMessageId( + const PendingPropagationUpload& upload) +{ + runtime::DeliveryAttemptReceipt* receipt = + delivery_attempt_ledger_.findPropagationReceipt(upload.transient_id); + return receipt ? receipt->message_id : 0; +} + +MessageId LxmfAdapter::takePropagationUploadMessageId( + const PendingPropagationUpload& upload) +{ + const MessageId message_id = propagationUploadMessageId(upload); + delivery_attempt_ledger_.removePropagationReceipt(upload.transient_id); + return message_id; +} + bool LxmfAdapter::queueReadyPropagationUpload( PendingPropagationUpload& upload, const PropagationPeerState& node) @@ -1445,13 +1461,13 @@ bool LxmfAdapter::queueReadyPropagationUpload( return false; } - std::vector messages; + runtime::RuntimeByteSpanList messages; messages.push_back(ByteSpan{upload.transient_data.data(), upload.transient_data.size()}); runtime::ResourcePayloadBuffer batch(upload.transient_data.size() + 32U, 0); size_t batch_len = batch.size(); if (!encodePropagationBatch(static_cast(currentTimestampSeconds()), - messages, + runtime::viewRuntimeByteSpans(messages), batch.data(), &batch_len)) { @@ -1461,9 +1477,14 @@ bool LxmfAdapter::queueReadyPropagationUpload( runtime::DeferredLinkPayload deferred{}; deferred.payload = std::move(batch); - deferred.message_id = upload.track_user_message ? upload.message_id : 0; - session->deferred_payloads.push_back(std::move(deferred)); - upload.state = runtime::PropagationUploadState::QueuedToLink; + const MessageId message_id = propagationUploadMessageId(upload); + deferred.message_id = message_id; + link_manager_.appendDeferredPayload(*session, std::move(deferred)); + if (message_id != 0) + { + delivery_attempt_ledger_.removePropagationReceipt(upload.transient_id); + } + propagation_client_.markUploadQueuedToLink(upload); if (session->state == LinkState::Active) { (void)sendLinkIdentify(*session); @@ -1477,7 +1498,7 @@ bool LxmfAdapter::queueReadyPropagationUpload( transient_hash, sizeof(transient_hash)); Serial.printf("[LXMF][PropagationTX] link_queue msg=%lu transient=%s node=%s link_state=%u started=%u\n", - static_cast(upload.message_id), + static_cast(message_id), transient_hash, node_hash, static_cast(session->state), @@ -1490,18 +1511,17 @@ void LxmfAdapter::processPropagationClient() const auto& config = rtnet::active().propagation; if (!config.enabled) { - const std::vector disabled_uploads = + const auto disabled_uploads = propagation_client_.takeAllPendingUploads(); for (const auto& upload : disabled_uploads) { - if (upload.track_user_message && upload.message_id != 0) + const MessageId message_id = takePropagationUploadMessageId(upload); + if (message_id != 0) { - sys::EventBus::publish( - new sys::ChatSendResultEvent(upload.message_id, false), - 0); + delivery_notifier_.failed(message_id); } Serial.printf("[LXMF][PropagationTX] disabled msg=%lu\n", - static_cast(upload.message_id)); + static_cast(message_id)); } propagation_client_.resetForDisabled(); link_manager_.forEachSession( @@ -1518,18 +1538,16 @@ void LxmfAdapter::processPropagationClient() const uint32_t now_ms = millis(); propagation_client_.markExpiredUploads(now_ms, kPropagationUploadTtlMs); - const std::vector failed_uploads = - propagation_client_.takeFailedUploads(); + const auto failed_uploads = propagation_client_.takeFailedUploads(); for (const auto& upload : failed_uploads) { - if (upload.track_user_message && upload.message_id != 0) + const MessageId message_id = takePropagationUploadMessageId(upload); + if (message_id != 0) { - sys::EventBus::publish( - new sys::ChatSendResultEvent(upload.message_id, false), - 0); + delivery_notifier_.failed(message_id); } Serial.printf("[LXMF][PropagationTX] failed msg=%lu\n", - static_cast(upload.message_id)); + static_cast(message_id)); } const PropagationPeerState* node = selectActivePropagationPeer(); @@ -1542,47 +1560,23 @@ void LxmfAdapter::processPropagationClient() return; } PendingPropagationUpload& upload = *pending_upload; + const MessageId message_id = propagationUploadMessageId(upload); if (!node) { - upload.state = runtime::PropagationUploadState::WaitingNode; - propagation_client_.stamp().reset(); + propagation_client_.markUploadWaitingForNode(upload); } else { - const bool node_changed = - !hashesEqual(upload.node_hash, - node->propagation_hash, - sizeof(upload.node_hash)) || - upload.stamp_cost != node->stamp_cost; - if (node_changed) - { - propagation_client_.stamp().reset(); - copyHash(upload.node_hash, - node->propagation_hash, - sizeof(upload.node_hash)); - upload.stamp_cost = node->stamp_cost; - upload.state = runtime::PropagationUploadState::NeedsStamp; - } - else if (upload.state == - runtime::PropagationUploadState::WaitingNode) - { - upload.state = runtime::PropagationUploadState::NeedsStamp; - } + propagation_client_.bindUploadNode(upload, *node); if (upload.state == runtime::PropagationUploadState::NeedsStamp) { - if (propagation_client_.stamp().begin(upload.transient_id, - upload.stamp_cost)) + if (propagation_client_.beginUploadStamp(upload)) { - upload.state = runtime::PropagationUploadState::Stamping; Serial.printf("[LXMF][PropagationTX] stamp_begin msg=%lu cost=%u\n", - static_cast(upload.message_id), + static_cast(message_id), static_cast(upload.stamp_cost)); } - else - { - upload.state = runtime::PropagationUploadState::Failed; - } } if (upload.state == runtime::PropagationUploadState::Stamping) @@ -1596,25 +1590,20 @@ void LxmfAdapter::processPropagationClient() propagation_client_.stamp().searchRounds(); if (!propagation_client_.stamp().takeStamp(stamp)) { - upload.state = - runtime::PropagationUploadState::Failed; + propagation_client_.markUploadFailed(upload); } - else + else if (propagation_client_.completeUploadStamp(upload, + stamp)) { - upload.transient_data.insert( - upload.transient_data.end(), - stamp, - stamp + sizeof(stamp)); - upload.state = runtime::PropagationUploadState::Ready; Serial.printf("[LXMF][PropagationTX] stamp_ready msg=%lu rounds=%lu\n", - static_cast(upload.message_id), + static_cast(message_id), static_cast(rounds)); } } else if (stamp_state == runtime::PropagationStampRuntime::State::Failed) { - upload.state = runtime::PropagationUploadState::Failed; + propagation_client_.markUploadFailed(upload); } else if (stamp_state == runtime::PropagationStampRuntime::State::Expanding && @@ -1622,7 +1611,7 @@ void LxmfAdapter::processPropagationClient() (propagation_client_.stamp().expandedRounds() % 100U) == 0U) { Serial.printf("[LXMF][PropagationTX] stamp_progress msg=%lu expand=%u/1000 search=%lu\n", - static_cast(upload.message_id), + static_cast(message_id), static_cast( propagation_client_.stamp().expandedRounds()), static_cast( @@ -1634,7 +1623,7 @@ void LxmfAdapter::processPropagationClient() (propagation_client_.stamp().searchRounds() % 4096U) == 0U) { Serial.printf("[LXMF][PropagationTX] stamp_progress msg=%lu expand=%u/1000 search=%lu\n", - static_cast(upload.message_id), + static_cast(message_id), static_cast( propagation_client_.stamp().expandedRounds()), static_cast( @@ -1747,8 +1736,8 @@ bool LxmfAdapter::sendPropagationSyncRequest( } wire_payload.resize(wire_payload_len); - uint8_t packet[kMaxPacketLen] = {}; - size_t packet_len = sizeof(packet); + std::memset(lxmf_tx_packet_scratch_, 0, sizeof(lxmf_tx_packet_scratch_)); + size_t packet_len = sizeof(lxmf_tx_packet_scratch_); if (!reticulum::buildHeader1Packet(reticulum::PacketType::Data, reticulum::DestinationType::Link, reticulum::PacketContext::Request, @@ -1756,18 +1745,20 @@ bool LxmfAdapter::sendPropagationSyncRequest( session.link_id, wire_payload.data(), wire_payload.size(), - packet, + lxmf_tx_packet_scratch_, &packet_len)) { return false; } - reticulum::computeTruncatedPacketHash(packet, packet_len, request_id); + reticulum::computeTruncatedPacketHash(lxmf_tx_packet_scratch_, + packet_len, + request_id); sent = session.interface_id != reticulum::interfaces::kInvalidInterfaceId ? interfaces_.sendPacketOn(session.interface_id, - packet, + lxmf_tx_packet_scratch_, packet_len) - : interfaces_.sendPacket(packet, packet_len); + : interfaces_.sendPacket(lxmf_tx_packet_scratch_, packet_len); } else { @@ -1786,13 +1777,13 @@ bool LxmfAdapter::sendPropagationSyncRequest( return false; } - LinkPendingRequest pending{}; - pending.request_id.assign(request_id, request_id + sizeof(request_id)); - pending.created_ms = millis(); - pending.awaiting_resource = request_payload.size() > session.mdu; - session.pending_requests.push_back(std::move(pending)); + link_manager_.queuePendingRequest(session, + request_id, + sizeof(request_id), + millis(), + request_payload.size() > session.mdu); propagation_client_.markSyncRequestSent(request_id, next_stage); - session.last_outbound_ms = millis(); + link_manager_.touchOutbound(session, millis()); Serial.printf("[LXMF][PropagationSync] request stage=%u wants=%u haves=%u resource=%u\n", static_cast(next_stage), static_cast(wants ? wants->size() : 0U), @@ -1827,25 +1818,23 @@ void LxmfAdapter::processPropagationSyncResponse(LinkSession& session) return; } - auto pending = std::find_if( - session.pending_requests.begin(), - session.pending_requests.end(), + LinkPendingRequest* pending = link_manager_.findPendingRequestIf( + session, [this](const LinkPendingRequest& request) { return propagation_client_.syncRequestMatches(request); }); if ((propagation_client_.syncStage() == PropagationSyncStage::Listing || propagation_client_.syncStage() == PropagationSyncStage::Downloading || propagation_client_.syncStage() == PropagationSyncStage::Acknowledging) && - pending != session.pending_requests.end() && - pending->created_ms != 0 && + pending && pending->created_ms != 0 && (millis() - pending->created_ms) > kLinkRequestTtlMs) { - session.pending_requests.erase(pending); + link_manager_.erasePendingRequest(session, *pending); propagation_client_.markSyncFailed(); - pending = session.pending_requests.end(); + pending = nullptr; } if (propagation_client_.syncStage() == PropagationSyncStage::Listing && - pending != session.pending_requests.end() && pending->response_ready) + pending && pending->response_ready) { runtime::PropagationIdList remote_ids; const bool decoded = @@ -1853,7 +1842,8 @@ void LxmfAdapter::processPropagationSyncResponse(LinkSession& session) pending->response.size(), runtime::appendRuntimeByteBufferCallback, &remote_ids); - session.pending_requests.erase(pending); + link_manager_.erasePendingRequest(session, *pending); + pending = nullptr; if (!decoded) { propagation_client_.markSyncFailed(); @@ -1879,7 +1869,7 @@ void LxmfAdapter::processPropagationSyncResponse(LinkSession& session) } if (propagation_client_.syncStage() == PropagationSyncStage::Downloading && - pending != session.pending_requests.end() && pending->response_ready) + pending && pending->response_ready) { runtime::PropagationMessageList messages; const bool decoded = @@ -1887,7 +1877,8 @@ void LxmfAdapter::processPropagationSyncResponse(LinkSession& session) pending->response.size(), runtime::appendRuntimeByteBufferCallback, &messages); - session.pending_requests.erase(pending); + link_manager_.erasePendingRequest(session, *pending); + pending = nullptr; if (!decoded) { propagation_client_.markSyncFailed(); @@ -1980,9 +1971,10 @@ void LxmfAdapter::processPropagationSyncResponse(LinkSession& session) } if (propagation_client_.syncStage() == PropagationSyncStage::Acknowledging && - pending != session.pending_requests.end() && pending->response_ready) + pending && pending->response_ready) { - session.pending_requests.erase(pending); + link_manager_.erasePendingRequest(session, *pending); + pending = nullptr; propagation_client_.markAcknowledged(); } @@ -2143,15 +2135,7 @@ MeshSendResult LxmfAdapter::sendTextDetailed(ChannelId channel, MeshSendResult result = ok ? MeshSendResult::success(message_id) : MeshSendResult::fail(dispatch.failure, message_id); - result.reticulum_identity = reticulumIdentityForPeer(*peer_info); - if (!send_result_event_deferred) - { - sys::EventBus::publish( - new sys::ChatSendResultEvent( - message_id, - ok ? MessageStatus::Queued : MessageStatus::Failed), - 0); - } + result.reticulum_identity = runtime::reticulumIdentityForPeer(*peer_info); return result; } @@ -2224,7 +2208,7 @@ MeshSendResult LxmfAdapter::sendTextToReticulumDestination( sendTextDetailed(channel, text, forced_msg_id, peer_info->node_id); if (!hasReticulumDestinationIdentity(result.reticulum_identity)) { - result.reticulum_identity = reticulumIdentityForPeer(*peer_info); + result.reticulum_identity = runtime::reticulumIdentityForPeer(*peer_info); } return result; } @@ -2243,13 +2227,13 @@ MeshSendResult LxmfAdapter::sendTextToReticulumDestination( return MeshSendResult::fail(MeshOperationFailure::EncodeFailed); } - uint8_t packet[kMaxPacketLen] = {}; - size_t packet_len = sizeof(packet); + std::memset(lxmf_tx_packet_scratch_, 0, sizeof(lxmf_tx_packet_scratch_)); + size_t packet_len = sizeof(lxmf_tx_packet_scratch_); uint8_t message_hash[reticulum::kFullHashSize] = {}; if (!buildGroupMessagePacket(destination, packed_payload, packed_payload_len, - packet, + lxmf_tx_packet_scratch_, &packet_len, message_hash)) { @@ -2269,7 +2253,7 @@ MeshSendResult LxmfAdapter::sendTextToReticulumDestination( dest_hash, static_cast(packed_payload_len), static_cast(packet_len)); - const bool ok = routeAndSendPacket(packet, packet_len, true); + const bool ok = routeAndSendPacket(lxmf_tx_packet_scratch_, packet_len, true); const auto& tx_result = interfaces_.lastTxResult(); Serial.printf("[LXMF][GroupTX] raw_send ok=%u msg=%lu dest=%s dest_full=%s bearer=%s complete=%u packet_len=%u text=\"%s\"\n", ok ? 1U : 0U, @@ -2285,11 +2269,6 @@ MeshSendResult LxmfAdapter::sendTextToReticulumDestination( : MeshSendResult::fail(MeshOperationFailure::RadioTxFailed, message_id); result.reticulum_identity = makeReticulumDestinationIdentity(destination.destination_hash); - sys::EventBus::publish( - new sys::ChatSendResultEvent( - message_id, - ok ? MessageStatus::Queued : MessageStatus::Failed), - 0); return result; } @@ -2348,7 +2327,7 @@ bool LxmfAdapter::sendAppData(ChannelId channel, uint32_t portnum, isReady() ? 1U : 0U); if (packet_id != 0) { - sys::EventBus::publish(new sys::ChatSendResultEvent(packet_id, false), 0); + delivery_notifier_.failed(packet_id); } return false; } @@ -2382,7 +2361,7 @@ bool LxmfAdapter::sendAppData(ChannelId channel, uint32_t portnum, packed_payload, &packed_payload_len)) { - sys::EventBus::publish(new sys::ChatSendResultEvent(effective_packet_id, false), 0); + delivery_notifier_.failed(effective_packet_id); return false; } @@ -2396,7 +2375,7 @@ bool LxmfAdapter::sendAppData(ChannelId channel, uint32_t portnum, static_cast(portnum), static_cast(dest), static_cast(effective_packet_id)); - sys::EventBus::publish(new sys::ChatSendResultEvent(effective_packet_id, false), 0); + delivery_notifier_.failed(effective_packet_id); return false; } @@ -2457,16 +2436,16 @@ bool LxmfAdapter::sendAppData(ChannelId channel, uint32_t portnum, } else { - uint8_t packet[kMaxPacketLen] = {}; - size_t packet_len = sizeof(packet); + std::memset(lxmf_tx_packet_scratch_, 0, sizeof(lxmf_tx_packet_scratch_)); + size_t packet_len = sizeof(lxmf_tx_packet_scratch_); if (buildSignedMessagePacket(*peer_info, packed_payload, packed_payload_len, - packet, + lxmf_tx_packet_scratch_, &packet_len, message_hash)) { - ok = routeAndSendPacket(packet, packet_len, true); + ok = routeAndSendPacket(lxmf_tx_packet_scratch_, packet_len, true); } if (!ok && have_link_payload) @@ -2478,7 +2457,7 @@ bool LxmfAdapter::sendAppData(ChannelId channel, uint32_t portnum, { runtime::DeferredLinkPayload deferred{}; deferred.payload.assign(lxmf_message, lxmf_message + lxmf_message_len); - session->deferred_payloads.push_back(std::move(deferred)); + link_manager_.appendDeferredPayload(*session, std::move(deferred)); if (session->state == LinkState::Active) { flushDeferredLinkPayloads(*session); @@ -2511,16 +2490,18 @@ bool LxmfAdapter::sendAppData(ChannelId channel, uint32_t portnum, (void)sendPathRequest(peer_info); } - uint8_t packet[kMaxPacketLen] = {}; - size_t packet_len = sizeof(packet); + std::memset(lxmf_tx_packet_scratch_, 0, sizeof(lxmf_tx_packet_scratch_)); + size_t packet_len = sizeof(lxmf_tx_packet_scratch_); uint8_t message_hash[reticulum::kFullHashSize] = {}; if (!buildSignedMessagePacket(peer_info, packed_payload, packed_payload_len, - packet, + lxmf_tx_packet_scratch_, &packet_len, message_hash) || - !routeAndSendPacket(packet, packet_len, true)) + !routeAndSendPacket(lxmf_tx_packet_scratch_, + packet_len, + true)) { ok = false; } @@ -2538,11 +2519,9 @@ bool LxmfAdapter::sendAppData(ChannelId channel, uint32_t portnum, static_cast(effective_packet_id), static_cast(dest), ok ? 1U : 0U); - sys::EventBus::publish( - new sys::ChatSendResultEvent( - effective_packet_id, - ok ? MessageStatus::Queued : MessageStatus::Failed), - 0); + delivery_notifier_.publish( + effective_packet_id, + ok ? MessageStatus::Queued : MessageStatus::Failed); return ok; } @@ -2663,7 +2642,7 @@ MeshActionResult LxmfAdapter::startReticulumAudioCall( const PeerInfo* peer = findOrLoadPeerByDestinationHash(destination.destination_hash); if (!peer && !isZeroBytes(destination.identity_hash, sizeof(destination.identity_hash))) { - peer = findPeerByIdentityHash(destination.identity_hash); + peer = destination_registry_.findByIdentityHash(destination.identity_hash); } uint8_t remote_identity_hash[reticulum::kTruncatedHashSize] = {}; @@ -2699,13 +2678,14 @@ MeshActionResult LxmfAdapter::startReticulumAudioCall( return MeshActionResult::fail(MeshOperationFailure::Busy); } - const PathEntry* path = findPath(call_destination_hash); + const PathEntry* path = + path_manager_.findPath(call_destination_hash, millis(), kPathTtlMs); bool path_requested = false; bool path_waiting = false; if (!path) { const PendingPathRequest* pending = - findPendingPathRequest(call_destination_hash); + path_manager_.findPendingPathRequest(call_destination_hash); path_waiting = pending && !pending->resolved; if (!path_waiting) { @@ -2715,48 +2695,40 @@ MeshActionResult LxmfAdapter::startReticulumAudioCall( } } - LinkSession* new_session = link_manager_.appendSession(kMaxLinkSessions); + const uint32_t now_ms = millis(); + runtime::LinkSessionSpec session_spec{}; + session_spec.now_ms = now_ms; + session_spec.keepalive_interval_ms = kLinkKeepaliveMaxMs; + session_spec.stale_timeout_ms = kLinkKeepaliveMaxMs * 2U; + session_spec.remote_destination_hash = call_destination_hash; + session_spec.remote_identity_hash = remote_identity_hash; + session_spec.peer_identity_sig_pub = peer ? peer->sig_pub : nullptr; + session_spec.expected_hops = path ? path->hops : 0; + session_spec.destination = LocalDestinationKind::CallAudio; + session_spec.state = LinkState::Pending; + session_spec.initiator = true; + session_spec.remote_identity_known = + !isZeroBytes(remote_identity_hash, sizeof(remote_identity_hash)); + + LinkSession* new_session = + link_manager_.openSession(kMaxLinkSessions, session_spec); if (!new_session) { return MeshActionResult::fail(MeshOperationFailure::Busy); } LinkSession& session = *new_session; - session.created_ms = millis(); - session.request_ms = session.created_ms; - session.last_inbound_ms = session.created_ms; - session.initiator = true; - session.destination = LocalDestinationKind::CallAudio; - session.state = LinkState::Pending; - session.close_reason = LinkCloseReason::None; - session.expected_hops = path ? path->hops : 0; - session.remote_identity_known = - !isZeroBytes(remote_identity_hash, sizeof(remote_identity_hash)); - session.validated = false; - session.call_wire_profile = wire_profile; - session.lxst_call = reticulum::lxst::call::makeCaller( + lxst_telephony_client_.beginCallerSession( + session, + wire_profile, call_profile::kEmbeddedLxstProfile, - session.created_ms); - session.keepalive_interval_ms = kLinkKeepaliveMaxMs; - session.stale_timeout_ms = kLinkKeepaliveMaxMs * 2U; - copyHash(session.remote_destination_hash, - call_destination_hash, - sizeof(session.remote_destination_hash)); - copyHash(session.remote_identity_hash, - remote_identity_hash, - sizeof(session.remote_identity_hash)); - if (peer) - { - memcpy(session.peer_identity_sig_pub, - peer->sig_pub, - sizeof(session.peer_identity_sig_pub)); - } + now_ms); Curve25519::dh1(session.local_enc_pub, session.local_enc_priv); if (isZeroBytes(session.local_enc_priv, sizeof(session.local_enc_priv)) || !generateLinkSigningKey(session.local_sig_pub, session.local_sig_priv) || !prepareLinkRequest(session)) { - link_manager_.discardLastSession(); + link_manager_.discardSession(session); return MeshActionResult::fail(MeshOperationFailure::EncodeFailed); } @@ -2787,18 +2759,18 @@ MeshActionResult LxmfAdapter::startReticulumAudioCall( call_profile::runtimeWireProfile(session.call_wire_profile); call_peer.codec2_mode = call_profile::runtimeCodec2Mode(session.call_wire_profile, - session.lxst_call.profile); + lxst_telephony_client_.profile(session)); if (!::platform::ui::reticulum_call::begin_outgoing(call_peer)) { - link_manager_.discardLastSession(); + link_manager_.discardSession(session); return MeshActionResult::fail(MeshOperationFailure::Busy); } - session.call_runtime_started = true; + lxst_telephony_client_.markRuntimeStarted(session, true); if ((path && !sendLinkRequest(session)) || (!path && !path_waiting)) { ::platform::ui::reticulum_call::notify_link_closed(session.link_id); - link_manager_.discardLastSession(); + link_manager_.discardSession(session); return MeshActionResult::fail(MeshOperationFailure::RadioTxFailed); } @@ -2853,7 +2825,8 @@ MeshActionResult LxmfAdapter::pingReticulumDestination( PeerInfo* peer = findOrLoadPeerByDestinationHash(destination.destination_hash); if (!peer && !isZeroBytes(destination.identity_hash, sizeof(destination.identity_hash))) { - if (const PeerInfo* by_identity = findPeerByIdentityHash(destination.identity_hash)) + if (const PeerInfo* by_identity = + destination_registry_.findByIdentityHash(destination.identity_hash)) { peer = findOrLoadPeerByDestinationHash(by_identity->destination_hash); } @@ -3042,7 +3015,8 @@ MeshActionResult LxmfAdapter::persistReticulumPeer( PeerInfo* peer = findOrLoadPeerByDestinationHash(destination.destination_hash); if (!peer && !isZeroBytes(destination.identity_hash, sizeof(destination.identity_hash))) { - if (const PeerInfo* by_identity = findPeerByIdentityHash(destination.identity_hash)) + if (const PeerInfo* by_identity = + destination_registry_.findByIdentityHash(destination.identity_hash)) { peer = findOrLoadPeerByDestinationHash(by_identity->destination_hash); } @@ -3515,7 +3489,7 @@ bool LxmfAdapter::processOneRadioPacket( uint8_t packet_hash[reticulum::kFullHashSize] = {}; reticulum::computePacketHash(packet, packet_len, packet_hash); - if (isDuplicatePacket(packet_hash)) + if (path_manager_.isDuplicatePacket(packet_hash)) { noteRxSummary(false, true, false); if (!ingress_wifi && !deferred_replay) @@ -3561,7 +3535,7 @@ bool LxmfAdapter::processOneRadioPacket( return false; } - rememberPacket(packet_hash); + path_manager_.rememberPacket(packet_hash, millis(), kMaxPacketFilter); switch (packet_router_.route(parsed)) { @@ -3638,7 +3612,7 @@ bool LxmfAdapter::isPublicDiscoveryPacket(const reticulum::ParsedPacket& packet) return false; } return packet.context != static_cast(reticulum::PacketContext::PathResponse) || - !findPendingPathRequest(packet.destination_hash); + !path_manager_.findPendingPathRequest(packet.destination_hash); } bool LxmfAdapter::enqueueDeferredDiscoveryPacket( @@ -4435,8 +4409,9 @@ bool LxmfAdapter::handleProofPacket( return nullptr; }; - if (runtime::PendingDeliveryReceipt* pending = - path_manager_.findPendingDeliveryReceipt(packet.destination_hash)) + if (runtime::DeliveryAttemptReceipt* pending = + delivery_attempt_ledger_.findReceiptByProofHash( + packet.destination_hash)) { const uint8_t* signature = proof_signature_for_hash(pending->packet_hash); @@ -4461,15 +4436,13 @@ bool LxmfAdapter::handleProofPacket( const MessageId message_id = pending->message_id; const uint32_t elapsed_ms = millis() - pending->created_ms; - path_manager_.removePendingDeliveryReceipt(packet.destination_hash); + delivery_attempt_ledger_.removeReceiptByProofHash( + packet.destination_hash); Serial.printf("[LXMF][DirectTX] proof_ok msg=%lu representation=opportunistic elapsed_ms=%lu hops=%u\n", static_cast(message_id), static_cast(elapsed_ms), static_cast(packet.hops)); - sys::EventBus::publish( - new sys::ChatSendResultEvent(message_id, - MessageStatus::Delivered), - 0); + delivery_notifier_.delivered(message_id); return true; } @@ -4514,7 +4487,8 @@ bool LxmfAdapter::handleProofPacket( return true; } - ReverseEntry* reverse = findReversePath(packet.destination_hash); + ReverseEntry* reverse = + path_manager_.findReversePath(packet.destination_hash); if (!reverse) { return false; @@ -4524,8 +4498,8 @@ bool LxmfAdapter::handleProofPacket( return false; } - uint8_t forward_packet[kMaxPacketLen] = {}; - size_t forward_len = sizeof(forward_packet); + std::memset(forward_packet_scratch_, 0, sizeof(forward_packet_scratch_)); + size_t forward_len = sizeof(forward_packet_scratch_); if (!reticulum::buildHeader1Packet(packet.packet_type, packet.destination_type, static_cast(packet.context), @@ -4533,7 +4507,7 @@ bool LxmfAdapter::handleProofPacket( packet.destination_hash, packet.payload, packet.payload_len, - forward_packet, + forward_packet_scratch_, &forward_len, packet.hops, reticulum::TransportType::Broadcast)) @@ -4543,7 +4517,7 @@ bool LxmfAdapter::handleProofPacket( reverse->created_ms = 0; return interfaces_.sendPacketOn(reverse->interface_id, - forward_packet, + forward_packet_scratch_, forward_len); } @@ -4619,43 +4593,45 @@ bool LxmfAdapter::handleLinkRequestPacket( LinkSession* session = findLinkSession(link_id); if (!session) { - session = link_manager_.appendSessionPreserving( + const uint32_t now_ms = millis(); + const uint16_t link_mtu = + (signalling_len != 0) + ? mtuFromLinkSignalling(packet.payload + kLinkRequestBaseLen, + signalling_len) + : reticulum::kReticulumMtu; + runtime::LinkSessionSpec session_spec{}; + session_spec.now_ms = now_ms; + session_spec.link_id = link_id; + session_spec.local_sig_pub = identity_.signingPublicKey(); + session_spec.peer_enc_pub = packet.payload; + session_spec.peer_link_sig_pub = + packet.payload + LxmfIdentity::kEncPubKeySize; + session_spec.interface_id = active_ingress_interface_id_; + session_spec.destination = local_kind; + session_spec.state = LinkState::Handshake; + session_spec.initiator = false; + session_spec.mtu = link_mtu; + session_spec.mdu = linkMduForMtu(link_mtu); + session = link_manager_.openSessionPreserving( kMaxLinkSessions, + session_spec, reject_busy_call ? current_call_link_id : nullptr); if (!session) { return false; } - copyHash(session->link_id, link_id, sizeof(session->link_id)); - memcpy(session->peer_enc_pub, packet.payload, LxmfIdentity::kEncPubKeySize); - memcpy(session->peer_link_sig_pub, - packet.payload + LxmfIdentity::kEncPubKeySize, - LxmfIdentity::kSigPubKeySize); Curve25519::dh1(session->local_enc_pub, session->local_enc_priv); - memcpy(session->local_sig_pub, identity_.signingPublicKey(), sizeof(session->local_sig_pub)); - session->mtu = (signalling_len != 0) - ? mtuFromLinkSignalling(packet.payload + kLinkRequestBaseLen, signalling_len) - : reticulum::kReticulumMtu; - session->mdu = linkMduForMtu(session->mtu); - session->created_ms = millis(); - session->request_ms = session->created_ms; - session->last_inbound_ms = session->created_ms; - session->interface_id = active_ingress_interface_id_; - session->destination = local_kind; - session->initiator = false; - session->state = LinkState::Handshake; if (local_kind == LocalDestinationKind::CallAudio) { - session->call_wire_profile = - ReticulumCallWireProfile::SidebandLxst; - session->lxst_call = reticulum::lxst::call::makeCallee( + lxst_telephony_client_.beginSidebandCalleeSession( + *session, call_profile::kEmbeddedLxstProfile, - session->created_ms); + now_ms); } if (!deriveLinkKey(*session)) { - link_manager_.discardLastSession(); + link_manager_.discardSession(*session); return false; } @@ -4688,7 +4664,7 @@ bool LxmfAdapter::handleLinkRequestPacket( busy_sent ? 1U : 0U, close_sent ? 1U : 0U, static_cast(ingress_interface)); - link_manager_.discardLastSession(); + link_manager_.discardSession(*session); return proof_sent && busy_sent && close_sent; } @@ -4706,11 +4682,12 @@ bool LxmfAdapter::handleLinkRequestPacket( session->call_wire_profile); call_peer.codec2_mode = call_profile::runtimeCodec2Mode( session->call_wire_profile, - session->lxst_call.profile); + lxst_telephony_client_.profile(*session)); const bool ui_started = ::platform::ui::reticulum_call::begin_incoming_identifying( call_peer); - session->call_runtime_started = ui_started; + lxst_telephony_client_.markRuntimeStarted(*session, + ui_started); Serial.printf("[LXMF][CallRX] link_admitted link=%s wire=sideband_lxst ui=%u await_identify=1 iface=%u\n", link_hash, ui_started ? 1U : 0U, @@ -4719,13 +4696,14 @@ bool LxmfAdapter::handleLinkRequestPacket( } else { - session->last_inbound_ms = millis(); + link_manager_.touchInbound(*session, millis()); } return sendLinkHandshakeProof(*session); } - const PathEntry* path = findPath(packet.destination_hash); + const PathEntry* path = + path_manager_.findPath(packet.destination_hash, millis(), kPathTtlMs); if (!path) { return false; @@ -4734,7 +4712,8 @@ bool LxmfAdapter::handleLinkRequestPacket( uint8_t link_id[reticulum::kTruncatedHashSize] = {}; if (computeLinkIdFromLinkRequest(raw_packet, raw_len, packet, link_id)) { - LinkRelayEntry& relay = upsertLinkRelay(link_id); + LinkRelayEntry& relay = + path_manager_.upsertLinkRelay(link_id, kMaxLinkRelays); relay.initiator_interface_id = active_ingress_interface_id_; relay.responder_interface_id = path->interface_id; relay.initiator_hops = packet.hops; @@ -4742,50 +4721,8 @@ bool LxmfAdapter::handleLinkRequestPacket( relay.last_seen_ms = millis(); } - if (path->hops <= 1 || path->direct) - { - uint8_t forward_packet[kMaxPacketLen] = {}; - size_t forward_len = sizeof(forward_packet); - if (!reticulum::buildHeader1Packet(packet.packet_type, - packet.destination_type, - static_cast(packet.context), - packet.context_flag != 0, - packet.destination_hash, - packet.payload, - packet.payload_len, - forward_packet, - &forward_len, - packet.hops, - reticulum::TransportType::Broadcast)) - { - return false; - } - - return interfaces_.sendPacketOn(path->interface_id, - forward_packet, - forward_len); - } - - uint8_t forward_packet[kMaxPacketLen] = {}; - size_t forward_len = sizeof(forward_packet); - if (!reticulum::buildHeader2Packet(packet.packet_type, - packet.destination_type, - static_cast(packet.context), - packet.context_flag != 0, - path->next_hop_transport, - packet.destination_hash, - packet.payload, - packet.payload_len, - forward_packet, - &forward_len, - packet.hops)) - { - return false; - } - - return interfaces_.sendPacketOn(path->interface_id, - forward_packet, - forward_len); + return sendForwardPlan(packet, + packet_router_.planPathForward(*path, packet.hops)); } bool LxmfAdapter::handlePathRequestPacket(const reticulum::ParsedPacket& packet) @@ -4838,7 +4775,8 @@ bool LxmfAdapter::handlePathRequestPacket(const reticulum::ParsedPacket& packet) return sendAnnounce(local_kind, reticulum::PacketContext::PathResponse); } - const PathEntry* path = findPath(requested_hash); + const PathEntry* path = + path_manager_.findPath(requested_hash, millis(), kPathTtlMs); if (!path || path->cached_announce_len == 0) { return true; @@ -4893,7 +4831,7 @@ bool LxmfAdapter::handleLocalLinkPacket( session->interface_id = active_ingress_interface_id_; } - session->last_inbound_ms = millis(); + link_manager_.touchInbound(*session, millis()); if (packet.packet_type == reticulum::PacketType::Proof) { return handleLinkProofPacket(*session, raw_packet, raw_len, packet); @@ -5049,24 +4987,13 @@ bool LxmfAdapter::handleLinkDataPacket(LinkSession& session, DecodedLinkResponse response{}; if (decodeLinkResponsePayload(payload_ptr, payload_len, &response)) { - for (auto& pending : session.pending_requests) - { - if (pending.request_id.size() == response.request_id.size() && - (pending.request_id.empty() || - std::memcmp(pending.request_id.data(), - response.request_id.data(), - pending.request_id.size()) == 0)) - { - pending.response_ready = true; - if (!response.data_is_nil) - { - pending.response.assign(response.packed_data.begin(), - response.packed_data.end()); - } - handled = true; - break; - } - } + handled = link_manager_.markPendingResponseReady( + session, + response.request_id.data(), + response.request_id.size(), + response.packed_data.data(), + response.packed_data.size(), + response.data_is_nil); } should_prove = handled; } @@ -5076,12 +5003,11 @@ bool LxmfAdapter::handleLinkDataPacket(LinkSession& session, if (!session.initiator && unpackFloat64(payload_ptr, payload_len, &rtt_value)) { - session.rtt_s = static_cast(rtt_value); - session.validated = true; - session.keepalive_interval_ms = keepaliveIntervalForRtt(session.rtt_s); - session.stale_timeout_ms = session.keepalive_interval_ms * 2U; - session.last_keepalive_ms = 0; - session.state = LinkState::Active; + const float rtt_s = static_cast(rtt_value); + link_manager_.markSessionValidatedActive( + session, + rtt_s, + keepaliveIntervalForRtt(rtt_s)); if (session.destination == LocalDestinationKind::CallAudio) { ::platform::ui::reticulum_call::mark_link_active( @@ -5122,10 +5048,7 @@ bool LxmfAdapter::handleLinkDataPacket(LinkSession& session, else { handled = (payload_len == 1 && payload_ptr[0] == 0xFE); - if (handled && session.state == LinkState::Stale) - { - session.state = LinkState::Active; - } + (void)(handled && link_manager_.reactivateSessionIfStale(session)); } } else if (context == static_cast(reticulum::PacketContext::ResourceAdv)) @@ -5230,8 +5153,9 @@ bool LxmfAdapter::handleLinkProofPacket(LinkSession& session, const PeerInfo* peer = session.destination == LocalDestinationKind::CallAudio - ? findPeerByIdentityHash(session.remote_identity_hash) - : findPeerByDestinationHash(session.remote_destination_hash); + ? destination_registry_.findByIdentityHash(session.remote_identity_hash) + : destination_registry_.findByDestinationHash( + session.remote_destination_hash); const uint8_t* peer_sig_pub = peer ? peer->sig_pub : nullptr; uint8_t announce_identity_hash[reticulum::kTruncatedHashSize] = {}; uint8_t announce_sig_pub[LxmfIdentity::kSigPubKeySize] = {}; @@ -5250,7 +5174,8 @@ bool LxmfAdapter::handleLinkProofPacket(LinkSession& session, dest_hash_hex, sizeof(dest_hash_hex)); - const PathEntry* path = findPath(session.remote_destination_hash); + const PathEntry* path = path_manager_.findPath( + session.remote_destination_hash, millis(), kPathTtlMs); if (!path) { logNomadLinkProofEvent(session, "drop", "path_missing"); @@ -5410,12 +5335,12 @@ bool LxmfAdapter::handleLinkProofPacket(LinkSession& session, return false; } - session.rtt_s = static_cast((millis() - session.request_ms) / 1000.0f); - session.validated = true; - session.keepalive_interval_ms = keepaliveIntervalForRtt(session.rtt_s); - session.stale_timeout_ms = session.keepalive_interval_ms * 2U; - session.last_keepalive_ms = 0; - session.state = LinkState::Active; + const float rtt_s = + static_cast((millis() - session.request_ms) / 1000.0f); + link_manager_.markSessionValidatedActive( + session, + rtt_s, + keepaliveIntervalForRtt(rtt_s)); const bool rtt_sent = sendLinkRtt(session); logNomadLinkProofEvent(session, "active", @@ -5484,16 +5409,10 @@ bool LxmfAdapter::handleLinkProofPacket(LinkSession& session, } const uint8_t* proved_hash = packet.payload; - auto receipt = std::find_if( - session.pending_packet_receipts.begin(), - session.pending_packet_receipts.end(), - [proved_hash](const runtime::LinkPacketReceipt& candidate) - { - return hashesEqual(candidate.packet_hash, - proved_hash, - reticulum::kFullHashSize); - }); - if (receipt == session.pending_packet_receipts.end()) + runtime::DeliveryAttemptReceipt* receipt = + delivery_attempt_ledger_.findLinkPacketReceipt(session.link_id, + proved_hash); + if (!receipt) { return false; } @@ -5511,7 +5430,8 @@ bool LxmfAdapter::handleLinkProofPacket(LinkSession& session, } const uint32_t message_id = receipt->message_id; - session.pending_packet_receipts.erase(receipt); + delivery_attempt_ledger_.removeLinkPacketReceipt(session.link_id, + proved_hash); if (message_id != 0) { Serial.printf("[LXMF][%s] proof_ok msg=%lu representation=packet\n", @@ -5523,8 +5443,7 @@ bool LxmfAdapter::handleLinkProofPacket(LinkSession& session, session.destination == LocalDestinationKind::Delivery ? MessageStatus::Delivered : MessageStatus::Queued; - sys::EventBus::publish(new sys::ChatSendResultEvent(message_id, status), - 0); + delivery_notifier_.publish(message_id, status); } return true; } @@ -5818,20 +5737,26 @@ bool LxmfAdapter::handleLinkResourceRequest(LinkSession& session, const size_t remaining_hashes = static_cast(resource->part_count) - next_index; const size_t slice_hashes = std::min(segment_capacity, remaining_hashes); - uint8_t update_payload[kMaxPacketLen] = {}; - size_t update_len = sizeof(update_payload); + std::memset(resource_hashmap_update_scratch_, + 0, + sizeof(resource_hashmap_update_scratch_)); + size_t update_len = sizeof(resource_hashmap_update_scratch_) - + reticulum::kFullHashSize; if (encodeResourceHashmapUpdate(segment, resource->hashmap.data() + slice_offset, slice_hashes * kResourceMapHashLen, - update_payload + reticulum::kFullHashSize, + resource_hashmap_update_scratch_ + + reticulum::kFullHashSize, &update_len)) { - memcpy(update_payload, resource->resource_hash, reticulum::kFullHashSize); + memcpy(resource_hashmap_update_scratch_, + resource->resource_hash, + reticulum::kFullHashSize); const size_t wire_len = reticulum::kFullHashSize + update_len; sent_any = sendLinkPacket(session, reticulum::PacketType::Data, reticulum::PacketContext::ResourceHmu, - update_payload, + resource_hashmap_update_scratch_, wire_len, true) || sent_any; @@ -6226,23 +6151,13 @@ bool LxmfAdapter::handleLinkResourcePart(LinkSession& session, DecodedLinkResponse response{}; if (decodeLinkResponsePayload(payload_data.data(), payload_data.size(), &response)) { - for (auto& pending : session.pending_requests) - { - if (pending.request_id.size() == response.request_id.size() && - (pending.request_id.empty() || - std::memcmp(pending.request_id.data(), - response.request_id.data(), - pending.request_id.size()) == 0)) - { - pending.response_ready = true; - if (!response.data_is_nil) - { - pending.response.assign(response.packed_data.begin(), - response.packed_data.end()); - } - break; - } - } + (void)link_manager_.markPendingResponseReady( + session, + response.request_id.data(), + response.request_id.size(), + response.packed_data.data(), + response.packed_data.size(), + response.data_is_nil); } } else if (session.destination == LocalDestinationKind::Delivery && @@ -6299,8 +6214,15 @@ bool LxmfAdapter::handleLinkResourceProof(LinkSession& session, return false; } - const uint32_t message_id = - link_manager_.takeResourceMessageId(*resource); + runtime::DeliveryAttemptReceipt* receipt = + delivery_attempt_ledger_.findLinkResourceReceipt(session.link_id, + resource_hash); + const uint32_t message_id = receipt ? receipt->message_id : 0; + if (receipt) + { + delivery_attempt_ledger_.removeLinkResourceReceipt(session.link_id, + resource_hash); + } if (message_id != 0) { Serial.printf("[LXMF][%s] proof_ok msg=%lu representation=resource\n", @@ -6312,8 +6234,7 @@ bool LxmfAdapter::handleLinkResourceProof(LinkSession& session, session.destination == LocalDestinationKind::Delivery ? MessageStatus::Delivered : MessageStatus::Queued; - sys::EventBus::publish(new sys::ChatSendResultEvent(message_id, status), - 0); + delivery_notifier_.publish(message_id, status); } return true; } @@ -6505,6 +6426,54 @@ bool LxmfAdapter::acceptPropagatedDelivery(const uint8_t* propagated_payload, out_awaiting_commit); } +bool LxmfAdapter::sendForwardPlan(const reticulum::ParsedPacket& packet, + const runtime::PacketForwardPlan& plan) +{ + if (!plan.forward || !packet.destination_hash || + plan.interface_id == reticulum::interfaces::kInvalidInterfaceId) + { + return false; + } + + std::memset(forward_packet_scratch_, 0, sizeof(forward_packet_scratch_)); + size_t forward_len = sizeof(forward_packet_scratch_); + bool built = false; + if (plan.header == runtime::PacketForwardHeader::Header1Broadcast) + { + built = reticulum::buildHeader1Packet( + packet.packet_type, + packet.destination_type, + static_cast(packet.context), + packet.context_flag != 0, + packet.destination_hash, + packet.payload, + packet.payload_len, + forward_packet_scratch_, + &forward_len, + plan.hops, + reticulum::TransportType::Broadcast); + } + else if (plan.header == runtime::PacketForwardHeader::Header2Transport) + { + built = reticulum::buildHeader2Packet( + packet.packet_type, + packet.destination_type, + static_cast(packet.context), + packet.context_flag != 0, + plan.next_hop_transport, + packet.destination_hash, + packet.payload, + packet.payload_len, + forward_packet_scratch_, + &forward_len, + plan.hops); + } + + return built && interfaces_.sendPacketOn(plan.interface_id, + forward_packet_scratch_, + forward_len); +} + bool LxmfAdapter::maybeForwardTransportPacket(const uint8_t* raw_packet, size_t raw_len, const reticulum::ParsedPacket& packet) { @@ -6524,7 +6493,8 @@ bool LxmfAdapter::maybeForwardTransportPacket(const uint8_t* raw_packet, size_t return false; } - const PathEntry* path = findPath(packet.destination_hash); + const PathEntry* path = + path_manager_.findPath(packet.destination_hash, millis(), kPathTtlMs); if (!path) { return false; @@ -6535,55 +6505,15 @@ bool LxmfAdapter::maybeForwardTransportPacket(const uint8_t* raw_packet, size_t { uint8_t proof_hash[reticulum::kTruncatedHashSize] = {}; reticulum::computeTruncatedPacketHash(raw_packet, raw_len, proof_hash); - rememberReversePath(proof_hash, - active_ingress_interface_id_, - path->hops); + path_manager_.rememberReversePath(proof_hash, + active_ingress_interface_id_, + path->hops, + millis(), + kMaxReverseEntries); } - if (path->hops <= 1 || path->direct) - { - uint8_t forward_packet[kMaxPacketLen] = {}; - size_t forward_len = sizeof(forward_packet); - if (!reticulum::buildHeader1Packet(packet.packet_type, - packet.destination_type, - static_cast(packet.context), - packet.context_flag != 0, - packet.destination_hash, - packet.payload, - packet.payload_len, - forward_packet, - &forward_len, - packet.hops, - reticulum::TransportType::Broadcast)) - { - return false; - } - - return interfaces_.sendPacketOn(path->interface_id, - forward_packet, - forward_len); - } - - uint8_t forward_packet[kMaxPacketLen] = {}; - size_t forward_len = sizeof(forward_packet); - if (!reticulum::buildHeader2Packet(packet.packet_type, - packet.destination_type, - static_cast(packet.context), - packet.context_flag != 0, - path->next_hop_transport, - packet.destination_hash, - packet.payload, - packet.payload_len, - forward_packet, - &forward_len, - packet.hops)) - { - return false; - } - - return interfaces_.sendPacketOn(path->interface_id, - forward_packet, - forward_len); + return sendForwardPlan(packet, + packet_router_.planPathForward(*path, packet.hops)); } bool LxmfAdapter::maybeForwardLinkPacket(const uint8_t* raw_packet, size_t raw_len, @@ -6602,51 +6532,23 @@ bool LxmfAdapter::maybeForwardLinkPacket(const uint8_t* raw_packet, size_t raw_l return false; } - LinkRelayEntry* relay = findLinkRelay(packet.destination_hash); + LinkRelayEntry* relay = path_manager_.findLinkRelay(packet.destination_hash); if (!relay) { return false; } - const bool from_initiator = - packet.hops == relay->initiator_hops && - (active_ingress_interface_id_ == - reticulum::interfaces::kInvalidInterfaceId || - active_ingress_interface_id_ == relay->initiator_interface_id); - const bool from_responder = - packet.hops == relay->responder_hops && - (active_ingress_interface_id_ == - reticulum::interfaces::kInvalidInterfaceId || - active_ingress_interface_id_ == relay->responder_interface_id); - if (!from_initiator && !from_responder) - { - return false; - } - - uint8_t forward_packet[kMaxPacketLen] = {}; - size_t forward_len = sizeof(forward_packet); - if (!reticulum::buildHeader1Packet(packet.packet_type, - packet.destination_type, - static_cast(packet.context), - packet.context_flag != 0, - packet.destination_hash, - packet.payload, - packet.payload_len, - forward_packet, - &forward_len, - packet.hops, - reticulum::TransportType::Broadcast)) + const runtime::PacketForwardPlan plan = + packet_router_.planLinkRelayForward(*relay, + active_ingress_interface_id_, + packet.hops); + if (!plan.forward) { return false; } relay->last_seen_ms = millis(); - const auto outbound_interface = from_initiator - ? relay->responder_interface_id - : relay->initiator_interface_id; - return interfaces_.sendPacketOn(outbound_interface, - forward_packet, - forward_len); + return sendForwardPlan(packet, plan); } bool LxmfAdapter::sendProofForPacket(const uint8_t* raw_packet, size_t raw_len) @@ -6668,8 +6570,8 @@ bool LxmfAdapter::sendProofForPacket(const uint8_t* raw_packet, size_t raw_len) uint8_t destination_hash[reticulum::kTruncatedHashSize] = {}; memcpy(destination_hash, packet_hash, sizeof(destination_hash)); - uint8_t proof_packet[kMaxPacketLen] = {}; - size_t proof_len = sizeof(proof_packet); + std::memset(proof_packet_scratch_, 0, sizeof(proof_packet_scratch_)); + size_t proof_len = sizeof(proof_packet_scratch_); if (!reticulum::buildHeader1Packet(reticulum::PacketType::Proof, reticulum::DestinationType::Single, reticulum::PacketContext::None, @@ -6677,7 +6579,7 @@ bool LxmfAdapter::sendProofForPacket(const uint8_t* raw_packet, size_t raw_len) destination_hash, signature, sizeof(signature), - proof_packet, + proof_packet_scratch_, &proof_len)) { return false; @@ -6686,9 +6588,9 @@ bool LxmfAdapter::sendProofForPacket(const uint8_t* raw_packet, size_t raw_len) return active_ingress_interface_id_ != reticulum::interfaces::kInvalidInterfaceId ? interfaces_.sendPacketOn(active_ingress_interface_id_, - proof_packet, + proof_packet_scratch_, proof_len) - : interfaces_.sendPacket(proof_packet, proof_len); + : interfaces_.sendPacket(proof_packet_scratch_, proof_len); } bool LxmfAdapter::sendPathRequest(PeerInfo& peer) @@ -6699,22 +6601,14 @@ bool LxmfAdapter::sendPathRequest(PeerInfo& peer) } const uint32_t now_ms = millis(); - if (peer.last_path_request_ms != 0 && - (now_ms - peer.last_path_request_ms) < kPathRequestMinIntervalMs) + if (path_manager_.pendingPathRequestCoolingDown( + peer.destination_hash, + now_ms, + kPathRequestMinIntervalMs)) { return false; } - if (const PendingPathRequest* pending = findPendingPathRequest(peer.destination_hash)) - { - if (!pending->resolved && - pending->last_attempt_ms != 0 && - (now_ms - pending->last_attempt_ms) < kPathRequestMinIntervalMs) - { - return false; - } - } - uint8_t request_payload[reticulum::kTruncatedHashSize + kPathRequestTagSize] = {}; memcpy(request_payload, peer.destination_hash, reticulum::kTruncatedHashSize); fillRandomBytes(request_payload + reticulum::kTruncatedHashSize, kPathRequestTagSize); @@ -6722,8 +6616,10 @@ bool LxmfAdapter::sendPathRequest(PeerInfo& peer) uint8_t control_hash[reticulum::kTruncatedHashSize] = {}; pathRequestDestinationHash(control_hash); - uint8_t packet[kMaxPacketLen] = {}; - size_t packet_len = sizeof(packet); + std::memset(path_request_packet_scratch_, + 0, + sizeof(path_request_packet_scratch_)); + size_t packet_len = sizeof(path_request_packet_scratch_); if (!reticulum::buildHeader1Packet(reticulum::PacketType::Data, reticulum::DestinationType::Plain, reticulum::PacketContext::None, @@ -6731,19 +6627,20 @@ bool LxmfAdapter::sendPathRequest(PeerInfo& peer) control_hash, request_payload, sizeof(request_payload), - packet, + path_request_packet_scratch_, &packet_len)) { return false; } - if (!routeAndSendPacket(packet, packet_len, false)) + if (!routeAndSendPacket(path_request_packet_scratch_, packet_len, false)) { return false; } - notePendingPathRequest(peer.destination_hash, now_ms); - peer.last_path_request_ms = now_ms; + path_manager_.notePendingPathRequest( + peer.destination_hash, now_ms, kMaxPendingPathRequests); + path_manager_.notePeerPathRequest(peer, now_ms); return true; } @@ -6758,14 +6655,11 @@ bool LxmfAdapter::sendPathRequestForDestination( } const uint32_t now_ms = millis(); - if (const PendingPathRequest* pending = findPendingPathRequest(destination_hash)) + if (path_manager_.pendingPathRequestCoolingDown(destination_hash, + now_ms, + kPathRequestMinIntervalMs)) { - if (!pending->resolved && - pending->last_attempt_ms != 0 && - (now_ms - pending->last_attempt_ms) < kPathRequestMinIntervalMs) - { - return false; - } + return false; } uint8_t request_payload[reticulum::kTruncatedHashSize + kPathRequestTagSize] = {}; @@ -6775,8 +6669,10 @@ bool LxmfAdapter::sendPathRequestForDestination( uint8_t control_hash[reticulum::kTruncatedHashSize] = {}; pathRequestDestinationHash(control_hash); - uint8_t packet[kMaxPacketLen] = {}; - size_t packet_len = sizeof(packet); + std::memset(path_request_packet_scratch_, + 0, + sizeof(path_request_packet_scratch_)); + size_t packet_len = sizeof(path_request_packet_scratch_); if (!reticulum::buildHeader1Packet(reticulum::PacketType::Data, reticulum::DestinationType::Plain, reticulum::PacketContext::None, @@ -6784,18 +6680,22 @@ bool LxmfAdapter::sendPathRequestForDestination( control_hash, request_payload, sizeof(request_payload), - packet, + path_request_packet_scratch_, &packet_len)) { return false; } - if (!routeAndSendPacket(packet, packet_len, false, true)) + if (!routeAndSendPacket(path_request_packet_scratch_, + packet_len, + false, + true)) { return false; } - notePendingPathRequest(destination_hash, now_ms); + path_manager_.notePendingPathRequest( + destination_hash, now_ms, kMaxPendingPathRequests); return true; } @@ -6806,36 +6706,13 @@ bool LxmfAdapter::shouldRequestPath(const PeerInfo& peer) const return false; } - const uint32_t now_ms = millis(); - if (const PendingPathRequest* pending = findPendingPathRequest(peer.destination_hash)) - { - if (!pending->resolved && - pending->created_ms != 0 && - (now_ms - pending->created_ms) < kPendingPathRequestTtlMs) - { - return false; - } - } - - if (peer.last_path_request_ms != 0 && - (now_ms - peer.last_path_request_ms) < kPathRequestMinIntervalMs) - { - return false; - } - - const PathEntry* path = findPath(peer.destination_hash); - if (!path || path->last_seen_s == 0) - { - return true; - } - - const uint32_t now_s = currentTimestampSeconds(); - if (now_s < path->last_seen_s) - { - return true; - } - - return (now_s - path->last_seen_s) >= kPathRefreshAgeS; + return path_manager_.shouldRequestPeerPath(peer, + millis(), + currentTimestampSeconds(), + kPendingPathRequestTtlMs, + kPathRequestMinIntervalMs, + kPathTtlMs, + kPathRefreshAgeS); } LxmfAdapter::LinkSession* LxmfAdapter::ensureOutboundLinkSession(PeerInfo& peer, @@ -6858,39 +6735,36 @@ LxmfAdapter::LinkSession* LxmfAdapter::ensureOutboundLinkSession(PeerInfo& peer, return session; } - const PathEntry* path = findPath(peer.destination_hash); + const PathEntry* path = + path_manager_.findPath(peer.destination_hash, millis(), kPathTtlMs); bool path_requested = false; if (!path && shouldRequestPath(peer)) { path_requested = sendPathRequest(peer); } - LinkSession* new_session = link_manager_.appendSession(kMaxLinkSessions); + const uint32_t now_ms = millis(); + runtime::LinkSessionSpec session_spec{}; + session_spec.now_ms = now_ms; + session_spec.keepalive_interval_ms = kLinkKeepaliveMaxMs; + session_spec.stale_timeout_ms = kLinkKeepaliveMaxMs * 2U; + session_spec.remote_destination_hash = peer.destination_hash; + session_spec.remote_identity_hash = peer.identity_hash; + session_spec.peer_identity_sig_pub = peer.sig_pub; + session_spec.expected_hops = path ? path->hops : 0; + session_spec.destination = kind; + session_spec.state = LinkState::Pending; + session_spec.initiator = true; + session_spec.remote_identity_known = + !isZeroBytes(peer.identity_hash, sizeof(peer.identity_hash)); + + LinkSession* new_session = + link_manager_.openSession(kMaxLinkSessions, session_spec); if (!new_session) { return nullptr; } LinkSession& session = *new_session; - session.created_ms = millis(); - session.request_ms = session.created_ms; - session.last_inbound_ms = session.created_ms; - session.last_outbound_ms = 0; - session.initiator = true; - session.destination = kind; - session.state = LinkState::Pending; - session.close_reason = LinkCloseReason::None; - session.expected_hops = path ? path->hops : 0; - session.remote_identity_known = !isZeroBytes(peer.identity_hash, sizeof(peer.identity_hash)); - session.validated = false; - session.keepalive_interval_ms = kLinkKeepaliveMaxMs; - session.stale_timeout_ms = kLinkKeepaliveMaxMs * 2U; - copyHash(session.remote_destination_hash, - peer.destination_hash, - sizeof(session.remote_destination_hash)); - copyHash(session.remote_identity_hash, - peer.identity_hash, - sizeof(session.remote_identity_hash)); - memcpy(session.peer_identity_sig_pub, peer.sig_pub, sizeof(session.peer_identity_sig_pub)); Curve25519::dh1(session.local_enc_pub, session.local_enc_priv); char dest_hash[12] = {}; @@ -6901,7 +6775,7 @@ LxmfAdapter::LinkSession* LxmfAdapter::ensureOutboundLinkSession(PeerInfo& peer, static_cast(peer.node_id), dest_hash, static_cast(kind)); - link_manager_.discardLastSession(); + link_manager_.discardSession(session); return nullptr; } @@ -6911,7 +6785,7 @@ LxmfAdapter::LinkSession* LxmfAdapter::ensureOutboundLinkSession(PeerInfo& peer, static_cast(peer.node_id), dest_hash, static_cast(kind)); - link_manager_.discardLastSession(); + link_manager_.discardSession(session); return nullptr; } @@ -6935,7 +6809,7 @@ LxmfAdapter::LinkSession* LxmfAdapter::ensureOutboundLinkSession(PeerInfo& peer, static_cast(peer.node_id), dest_hash, static_cast(kind)); - link_manager_.discardLastSession(); + link_manager_.discardSession(session); return nullptr; } @@ -7039,7 +6913,8 @@ bool LxmfAdapter::sendLinkRequest(LinkSession& session) const uint8_t* tx_packet = link_request_packet_scratch_; size_t tx_packet_len = link_request_packet_len_; bool routed = false; - const PathEntry* tx_path = findPath(parsed.destination_hash); + const PathEntry* tx_path = + path_manager_.findPath(parsed.destination_hash, millis(), kPathTtlMs); if (tx_path && tx_path->hops > 1 && !tx_path->direct) { tx_packet_len = sizeof(link_request_routed_scratch_); @@ -7087,7 +6962,7 @@ bool LxmfAdapter::sendLinkRequest(LinkSession& session) const bool wifi_only = session.destination == LocalDestinationKind::CallAudio; - if (wifi_only && !session.call_runtime_started) + if (wifi_only && !lxst_telephony_client_.runtimeStarted(session)) { Serial.printf("[LXMF][LinkTX] request_fail dest=%s kind=%u reason=call_runtime_not_started\n", dest_hash, @@ -7111,7 +6986,7 @@ bool LxmfAdapter::sendLinkRequest(LinkSession& session) const auto& tx_result = interfaces_.lastTxResult(); if (sent) { - session.last_outbound_ms = session.request_ms; + link_manager_.touchOutbound(session, session.request_ms); session.mtu = reticulum::kReticulumMtu; session.mdu = linkMduForMtu(session.mtu); char link_hash[12] = {}; @@ -7354,12 +7229,12 @@ bool LxmfAdapter::buildEncryptedPacketForPeer(const PeerInfo& peer, return false; } - uint8_t payload[kMaxPacketLen] = {}; - size_t payload_len = sizeof(payload); + std::memset(encrypted_payload_scratch_, 0, sizeof(encrypted_payload_scratch_)); + size_t payload_len = sizeof(encrypted_payload_scratch_); if (!encryptForPeer(peer, plaintext, plaintext_len, - payload, + encrypted_payload_scratch_, &payload_len)) { return false; @@ -7370,7 +7245,7 @@ bool LxmfAdapter::buildEncryptedPacketForPeer(const PeerInfo& peer, reticulum::PacketContext::None, false, peer.destination_hash, - payload, + encrypted_payload_scratch_, payload_len, out_packet, inout_len); @@ -7417,7 +7292,8 @@ bool LxmfAdapter::routeAndSendPacket(const uint8_t* raw_packet, size_t raw_len, return send_packet(raw_packet, raw_len); } - const PathEntry* path = findPath(parsed.destination_hash); + const PathEntry* path = + path_manager_.findPath(parsed.destination_hash, millis(), kPathTtlMs); if (!path) { return send_packet(raw_packet, raw_len); @@ -7427,8 +7303,8 @@ bool LxmfAdapter::routeAndSendPacket(const uint8_t* raw_packet, size_t raw_len, return send_packet(raw_packet, raw_len, path->interface_id); } - uint8_t routed_packet[kMaxPacketLen] = {}; - size_t routed_len = sizeof(routed_packet); + std::memset(routed_packet_scratch_, 0, sizeof(routed_packet_scratch_)); + size_t routed_len = sizeof(routed_packet_scratch_); if (!reticulum::buildHeader2Packet(parsed.packet_type, parsed.destination_type, static_cast(parsed.context), @@ -7437,14 +7313,14 @@ bool LxmfAdapter::routeAndSendPacket(const uint8_t* raw_packet, size_t raw_len, parsed.destination_hash, parsed.payload, parsed.payload_len, - routed_packet, + routed_packet_scratch_, &routed_len, raw_packet[1])) { return false; } - return send_packet(routed_packet, routed_len, path->interface_id); + return send_packet(routed_packet_scratch_, routed_len, path->interface_id); } bool LxmfAdapter::sendCachedAnnounceResponse(const PathEntry& path, @@ -7462,8 +7338,8 @@ bool LxmfAdapter::sendCachedAnnounceResponse(const PathEntry& path, return false; } - uint8_t packet[kMaxPacketLen] = {}; - size_t packet_len = sizeof(packet); + std::memset(routed_packet_scratch_, 0, sizeof(routed_packet_scratch_)); + size_t packet_len = sizeof(routed_packet_scratch_); if (!reticulum::buildHeader2Packet(reticulum::PacketType::Announce, reticulum::DestinationType::Single, context, @@ -7472,7 +7348,7 @@ bool LxmfAdapter::sendCachedAnnounceResponse(const PathEntry& path, parsed.destination_hash, parsed.payload, parsed.payload_len, - packet, + routed_packet_scratch_, &packet_len, path.hops)) { @@ -7482,9 +7358,9 @@ bool LxmfAdapter::sendCachedAnnounceResponse(const PathEntry& path, return active_ingress_interface_id_ != reticulum::interfaces::kInvalidInterfaceId ? interfaces_.sendPacketOn(active_ingress_interface_id_, - packet, + routed_packet_scratch_, packet_len) - : interfaces_.sendPacket(packet, packet_len); + : interfaces_.sendPacket(routed_packet_scratch_, packet_len); } bool LxmfAdapter::sendCachedPacketReplay(const uint8_t packet_hash[reticulum::kFullHashSize]) @@ -7552,7 +7428,7 @@ bool LxmfAdapter::shouldProcessWifiIngressPacket(const reticulum::ParsedPacket& } if (packet.packet_type == reticulum::PacketType::Proof && - (findReversePath(packet.destination_hash) || + (path_manager_.findReversePath(packet.destination_hash) || path_manager_.findPendingPingReceipt(packet.destination_hash))) { return true; @@ -7575,7 +7451,7 @@ bool LxmfAdapter::shouldProcessWifiIngressPacket(const reticulum::ParsedPacket& if (packet.packet_type == reticulum::PacketType::Announce) { if (packet.context == static_cast(reticulum::PacketContext::PathResponse) && - findPendingPathRequest(packet.destination_hash)) + path_manager_.findPendingPathRequest(packet.destination_hash)) { return true; } @@ -7815,8 +7691,8 @@ bool LxmfAdapter::rebroadcastAnnounce(const PathEntry& path, const reticulum::Pa } last_announce_rebroadcast_ms_ = now_ms; - uint8_t rebroadcast[kMaxPacketLen] = {}; - size_t rebroadcast_len = sizeof(rebroadcast); + std::memset(forward_packet_scratch_, 0, sizeof(forward_packet_scratch_)); + size_t rebroadcast_len = sizeof(forward_packet_scratch_); if (!reticulum::buildHeader2Packet(reticulum::PacketType::Announce, reticulum::DestinationType::Single, reticulum::PacketContext::None, @@ -7825,66 +7701,14 @@ bool LxmfAdapter::rebroadcastAnnounce(const PathEntry& path, const reticulum::Pa packet.destination_hash, packet.payload, packet.payload_len, - rebroadcast, + forward_packet_scratch_, &rebroadcast_len, packet.hops)) { return false; } - return interfaces_.sendPacket(rebroadcast, rebroadcast_len); -} - -bool LxmfAdapter::isDuplicatePacket(const uint8_t packet_hash[reticulum::kFullHashSize]) -{ - return path_manager_.isDuplicatePacket(packet_hash); -} - -void LxmfAdapter::rememberPacket(const uint8_t packet_hash[reticulum::kFullHashSize]) -{ - path_manager_.rememberPacket(packet_hash, millis(), kMaxPacketFilter); -} - -void LxmfAdapter::rememberReversePath(const uint8_t proof_hash[reticulum::kTruncatedHashSize], - reticulum::interfaces::InterfaceId interface_id, - uint8_t expected_hops) -{ - path_manager_.rememberReversePath(proof_hash, - interface_id, - expected_hops, - millis(), - kMaxReverseEntries); -} - -LxmfAdapter::ReverseEntry* LxmfAdapter::findReversePath( - const uint8_t proof_hash[reticulum::kTruncatedHashSize]) -{ - return path_manager_.findReversePath(proof_hash); -} - -LxmfAdapter::PendingPathRequest* LxmfAdapter::findPendingPathRequest( - const uint8_t destination_hash[reticulum::kTruncatedHashSize]) -{ - return path_manager_.findPendingPathRequest(destination_hash); -} - -const LxmfAdapter::PendingPathRequest* LxmfAdapter::findPendingPathRequest( - const uint8_t destination_hash[reticulum::kTruncatedHashSize]) const -{ - return path_manager_.findPendingPathRequest(destination_hash); -} - -void LxmfAdapter::notePendingPathRequest( - const uint8_t destination_hash[reticulum::kTruncatedHashSize], - uint32_t now_ms) -{ - path_manager_.notePendingPathRequest(destination_hash, now_ms, kMaxPendingPathRequests); -} - -void LxmfAdapter::resolvePendingPathRequest( - const uint8_t destination_hash[reticulum::kTruncatedHashSize]) -{ - path_manager_.resolvePendingPathRequest(destination_hash); + return interfaces_.sendPacket(forward_packet_scratch_, rebroadcast_len); } void LxmfAdapter::cullTransportState() @@ -7913,9 +7737,13 @@ void LxmfAdapter::cullTransportState() 100); }); - path_manager_.forEachPendingDeliveryReceipt( - [now_ms](const runtime::PendingDeliveryReceipt& receipt) + delivery_attempt_ledger_.forEachReceipt( + [now_ms](const runtime::DeliveryAttemptReceipt& receipt) { + if (receipt.kind != runtime::DeliveryAttemptKind::DirectPacket) + { + return; + } if (receipt.created_ms == 0 || (now_ms - receipt.created_ms) <= kPendingDeliveryReceiptTtlMs) @@ -7932,6 +7760,10 @@ void LxmfAdapter::cullTransportState() static_cast(now_ms - receipt.created_ms)); }); + delivery_attempt_ledger_.cull(runtime::DeliveryAttemptKind::DirectPacket, + now_ms, + kPendingDeliveryReceiptTtlMs, + kMaxPendingDeliveryReceipts); const runtime::TransportRuntimeLimits limits{ kMaxPaths, @@ -7945,37 +7777,11 @@ void LxmfAdapter::cullTransportState() kLinkRelayTtlMs, kMaxPendingPingReceipts, kPathTtlMs, - kPendingPingReceiptTtlMs, - kMaxPendingDeliveryReceipts, - kPendingDeliveryReceiptTtlMs}; + kPendingPingReceiptTtlMs}; path_manager_.cull(now_ms, limits); cullLinkSessions(); } -LxmfAdapter::PathEntry& LxmfAdapter::upsertPath( - const uint8_t destination_hash[reticulum::kTruncatedHashSize]) -{ - return path_manager_.upsertPath(destination_hash, kMaxPaths); -} - -const LxmfAdapter::PathEntry* LxmfAdapter::findPath( - const uint8_t destination_hash[reticulum::kTruncatedHashSize]) const -{ - return path_manager_.findPath(destination_hash, millis(), kPathTtlMs); -} - -LxmfAdapter::LinkRelayEntry& LxmfAdapter::upsertLinkRelay( - const uint8_t link_id[reticulum::kTruncatedHashSize]) -{ - return path_manager_.upsertLinkRelay(link_id, kMaxLinkRelays); -} - -LxmfAdapter::LinkRelayEntry* LxmfAdapter::findLinkRelay( - const uint8_t link_id[reticulum::kTruncatedHashSize]) -{ - return path_manager_.findLinkRelay(link_id); -} - void LxmfAdapter::localDestinationHash(LocalDestinationKind kind, uint8_t out_hash[reticulum::kTruncatedHashSize]) const { @@ -8176,21 +7982,27 @@ bool LxmfAdapter::sendLinkPacket(LinkSession& session, return false; } - uint8_t wire_payload[kMaxPacketLen] = {}; const uint8_t* effective_payload = payload; size_t effective_payload_len = payload_len; if (encrypt_payload) { - effective_payload = wire_payload; - effective_payload_len = sizeof(wire_payload); - if (!encryptLinkPayload(session, payload, payload_len, wire_payload, &effective_payload_len)) + std::memset(link_wire_payload_scratch_, + 0, + sizeof(link_wire_payload_scratch_)); + effective_payload = link_wire_payload_scratch_; + effective_payload_len = sizeof(link_wire_payload_scratch_); + if (!encryptLinkPayload(session, + payload, + payload_len, + link_wire_payload_scratch_, + &effective_payload_len)) { return false; } } - uint8_t packet[kMaxPacketLen] = {}; - size_t packet_len = sizeof(packet); + std::memset(link_packet_scratch_, 0, sizeof(link_packet_scratch_)); + size_t packet_len = sizeof(link_packet_scratch_); if (!reticulum::buildHeader1Packet(packet_type, reticulum::DestinationType::Link, context, @@ -8198,7 +8010,7 @@ bool LxmfAdapter::sendLinkPacket(LinkSession& session, session.link_id, effective_payload, effective_payload_len, - packet, + link_packet_scratch_, &packet_len)) { return false; @@ -8208,7 +8020,7 @@ bool LxmfAdapter::sendLinkPacket(LinkSession& session, session.interface_id != reticulum::interfaces::kInvalidInterfaceId; const bool ok = has_bound_interface ? interfaces_.sendPacketOn(session.interface_id, - packet, + link_packet_scratch_, packet_len, session.destination == LocalDestinationKind::CallAudio @@ -8216,17 +8028,20 @@ bool LxmfAdapter::sendLinkPacket(LinkSession& session, : nullptr, call_admission_control) : (session.destination == LocalDestinationKind::CallAudio - ? interfaces_.sendPacketWifiOnly(packet, + ? interfaces_.sendPacketWifiOnly(link_packet_scratch_, packet_len, session.link_id, call_admission_control) - : interfaces_.sendPacket(packet, packet_len)); + : interfaces_.sendPacket(link_packet_scratch_, + packet_len)); if (ok) { - session.last_outbound_ms = millis(); + link_manager_.touchOutbound(session, millis()); if (out_packet_hash) { - reticulum::computePacketHash(packet, packet_len, out_packet_hash); + reticulum::computePacketHash(link_packet_scratch_, + packet_len, + out_packet_hash); } } return ok; @@ -8292,9 +8107,10 @@ bool LxmfAdapter::sendNomadPageRequestPacket( return false; } + uint8_t request_id[reticulum::kTruncatedHashSize] = {}; reticulum::computeTruncatedPacketHash(nomad_page_packet_scratch_, packet_len, - request.request_id); + request_id); const bool ok = session.interface_id != reticulum::interfaces::kInvalidInterfaceId ? interfaces_.sendPacketOn(session.interface_id, @@ -8306,14 +8122,16 @@ bool LxmfAdapter::sendNomadPageRequestPacket( return false; } - LinkPendingRequest pending{}; - pending.request_id.assign(request.request_id, - request.request_id + sizeof(request.request_id)); - pending.created_ms = millis(); - session.pending_requests.push_back(std::move(pending)); - session.last_outbound_ms = millis(); - request.request_sent = true; - return true; + link_manager_.queuePendingRequest(session, + request_id, + sizeof(request_id), + millis(), + false); + link_manager_.touchOutbound(session, millis()); + return network_page_client_.noteRequestPacketSent(request, + request_id, + sizeof(request_id), + millis()); } LxmfAdapter::PendingNomadPageRequest* @@ -8491,9 +8309,8 @@ void LxmfAdapter::pumpNomadPageRequests() timeout_link, static_cast(open_link ? open_link->expected_hops : 0U), static_cast( - request.last_attempt_ms != 0 - ? (now_ms - request.last_attempt_ms) - : 0U)); + network_page_client_.lastAttemptAge(request, + now_ms))); updateNomadPageProgress(request, 100, "Nomad page request timed out", @@ -8526,21 +8343,15 @@ void LxmfAdapter::pumpNomadPageRequests() findActiveLinkSessionByDestination(request.destination_hash, LocalDestinationKind::NomadPage)) { - for (auto pending = session->pending_requests.begin(); - pending != session->pending_requests.end(); - ++pending) + LinkPendingRequest* pending = + link_manager_.findPendingRequest(*session, + request.request_id, + sizeof(request.request_id)); + if (pending && pending->response_ready) { - if (pending->request_id.size() == sizeof(request.request_id) && - std::memcmp(pending->request_id.data(), - request.request_id, - sizeof(request.request_id)) == 0 && - pending->response_ready) - { - completeNomadPageRequest(request, pending->response); - session->pending_requests.erase(pending); - completed = true; - break; - } + completeNomadPageRequest(request, pending->response); + link_manager_.erasePendingRequest(*session, *pending); + completed = true; } } } @@ -8555,10 +8366,11 @@ void LxmfAdapter::pumpNomadPageRequests() findActiveLinkSessionByDestination(request.destination_hash, LocalDestinationKind::NomadPage); if (active_link && !request.request_sent && - (request.last_attempt_ms == 0 || - (now_ms - request.last_attempt_ms) >= kNomadPageSendRetryMs)) + network_page_client_.attemptDue(request, + now_ms, + kNomadPageSendRetryMs)) { - request.last_attempt_ms = now_ms; + network_page_client_.noteAttempt(request, now_ms); const bool sent = sendNomadPageRequestPacket(*active_link, request); updateNomadPageProgress(request, sent ? 40 : 35, @@ -8581,21 +8393,26 @@ void LxmfAdapter::pumpNomadPageRequests() destination_text, request.path, sent ? 1U : 0U, - static_cast(active_link->pending_requests.size())); + static_cast( + link_manager_.pendingRequestCount(*active_link))); } } else if (!active_link) { - const PathEntry* path = findPath(request.destination_hash); + const PathEntry* path = path_manager_.findPath( + request.destination_hash, millis(), kPathTtlMs); if (!path) { - if (request.last_path_request_ms == 0 || - (now_ms - request.last_path_request_ms) >= kPathRequestMinIntervalMs) + if (network_page_client_.pathRequestDue( + request, + now_ms, + kPathRequestMinIntervalMs)) { - request.last_path_request_ms = now_ms; const bool path_sent = sendPathRequestForDestination(request.destination_hash); - request.path_requested = request.path_requested || path_sent; + network_page_client_.notePathRequest(request, + path_sent, + now_ms); updateNomadPageProgress(request, path_sent ? 10 : 5, path_sent ? "Resolving Nomad page path" @@ -8630,13 +8447,15 @@ void LxmfAdapter::pumpNomadPageRequests() if (open_link && (open_link->state == LinkState::Pending || open_link->state == LinkState::Handshake) && - (request.last_attempt_ms == 0 || - (now_ms - request.last_attempt_ms) >= - kNomadPageLinkRetryMs)) + network_page_client_.attemptDue(request, + now_ms, + kNomadPageLinkRetryMs)) { - request.last_attempt_ms = now_ms; const bool link_sent = sendLinkRequest(*open_link); - request.link_started = request.link_started || link_sent; + network_page_client_.noteLinkStart(request, + link_sent, + now_ms, + true); updateNomadPageProgress(request, link_sent ? 25 : 10, link_sent @@ -8667,13 +8486,24 @@ void LxmfAdapter::pumpNomadPageRequests() now_ms - open_link->created_ms)); } else if (!open_link && - (request.last_attempt_ms == 0 || - (now_ms - request.last_attempt_ms) >= - kNomadPageSendRetryMs)) + network_page_client_.attemptDue( + request, + now_ms, + kNomadPageSendRetryMs)) { - request.last_attempt_ms = now_ms; + runtime::LinkSessionSpec session_spec{}; + session_spec.now_ms = now_ms; + session_spec.keepalive_interval_ms = kLinkKeepaliveMaxMs; + session_spec.stale_timeout_ms = kLinkKeepaliveMaxMs * 2U; + session_spec.remote_destination_hash = + request.destination_hash; + session_spec.expected_hops = path->hops; + session_spec.destination = LocalDestinationKind::NomadPage; + session_spec.state = LinkState::Pending; + session_spec.initiator = true; LinkSession* new_session = - link_manager_.appendSession(kMaxLinkSessions); + link_manager_.openSession(kMaxLinkSessions, + session_spec); if (!new_session) { updateNomadPageProgress(request, @@ -8687,21 +8517,6 @@ void LxmfAdapter::pumpNomadPageRequests() continue; } LinkSession& session = *new_session; - session.created_ms = now_ms; - session.request_ms = now_ms; - session.last_inbound_ms = now_ms; - session.initiator = true; - session.destination = LocalDestinationKind::NomadPage; - session.state = LinkState::Pending; - session.close_reason = LinkCloseReason::None; - session.expected_hops = path->hops; - session.remote_identity_known = false; - session.validated = false; - session.keepalive_interval_ms = kLinkKeepaliveMaxMs; - session.stale_timeout_ms = kLinkKeepaliveMaxMs * 2U; - copyHash(session.remote_destination_hash, - request.destination_hash, - sizeof(session.remote_destination_hash)); Curve25519::dh1(session.local_enc_pub, session.local_enc_priv); @@ -8713,9 +8528,12 @@ void LxmfAdapter::pumpNomadPageRequests() sendLinkRequest(session); if (!link_sent) { - link_manager_.discardLastSession(); + link_manager_.discardSession(session); } - request.link_started = link_sent; + network_page_client_.noteLinkStart(request, + link_sent, + now_ms, + false); updateNomadPageProgress(request, link_sent ? 25 : 10, link_sent ? "Opening Nomad page link" @@ -8818,7 +8636,7 @@ bool LxmfAdapter::sendLinkKeepalive(LinkSession& session) false); if (ok) { - session.last_keepalive_ms = millis(); + link_manager_.noteKeepaliveSent(session, millis()); } return ok; } @@ -8973,8 +8791,10 @@ bool LxmfAdapter::advertiseLinkResource(LinkSession& session, const size_t slice_offset = start_hash * kResourceMapHashLen; const size_t slice_len = slice_hashes * kResourceMapHashLen; - uint8_t advertisement[kMaxPacketLen] = {}; - size_t advertisement_len = sizeof(advertisement); + std::memset(resource_advertisement_scratch_, + 0, + sizeof(resource_advertisement_scratch_)); + size_t advertisement_len = sizeof(resource_advertisement_scratch_); if (encodeResourceAdvertisement(resource.transfer_size, resource.data_size, resource.part_count, @@ -8988,7 +8808,7 @@ bool LxmfAdapter::advertiseLinkResource(LinkSession& session, resource.flags, resource.hashmap.data() + slice_offset, slice_len, - advertisement, + resource_advertisement_scratch_, &advertisement_len) && advertisement_len <= session.mdu) { @@ -8996,7 +8816,7 @@ bool LxmfAdapter::advertiseLinkResource(LinkSession& session, return sendLinkPacket(session, reticulum::PacketType::Data, reticulum::PacketContext::ResourceAdv, - advertisement, + resource_advertisement_scratch_, advertisement_len, true); } @@ -9066,8 +8886,6 @@ bool LxmfAdapter::queueOutgoingResource(LinkSession& session, { return false; } - resource.message_id = message_id; - bool mapped = false; for (size_t attempt = 0; attempt < 8 && !mapped; ++attempt) { @@ -9093,7 +8911,7 @@ bool LxmfAdapter::queueOutgoingResource(LinkSession& session, } resource.hashmap.clear(); - std::vector> recent_hashes; + runtime::RuntimeMapHashList recent_hashes; recent_hashes.reserve(collision_guard); bool collision = false; @@ -9148,44 +8966,46 @@ bool LxmfAdapter::queueOutgoingResource(LinkSession& session, (void)link_manager_.discardLastOutgoingResource(session); return false; } + if (message_id != 0) + { + delivery_attempt_ledger_.noteLinkResourceReceipt( + queued_resource->resource_hash, + session.link_id, + message_id, + millis(), + kMaxPendingDeliveryReceipts); + } return true; } void LxmfAdapter::closeLinkSession(LinkSession& session, LinkCloseReason reason) { - for (const auto& deferred : session.deferred_payloads) - { - if (deferred.message_id != 0) - { - Serial.printf("[LXMF][%s] deferred_failed msg=%lu reason=link_close close_reason=%u\n", - session.destination == - LocalDestinationKind::Propagation - ? "PropagationTX" - : "DirectTX", - static_cast(deferred.message_id), - static_cast(reason)); - sys::EventBus::publish( - new sys::ChatSendResultEvent(deferred.message_id, false), 0); - } - } - - for (const auto& receipt : session.pending_packet_receipts) - { - if (receipt.message_id != 0) - { - sys::EventBus::publish( - new sys::ChatSendResultEvent(receipt.message_id, false), 0); - } - } - session.pending_packet_receipts.clear(); - - link_manager_.takeTrackedOutgoingResourceMessageIds( + link_manager_.forEachDeferredPayload( session, - [](uint32_t message_id) + [this, &session, reason](const runtime::DeferredLinkPayload& deferred) { - sys::EventBus::publish( - new sys::ChatSendResultEvent(message_id, false), 0); + if (deferred.message_id != 0) + { + Serial.printf("[LXMF][%s] deferred_failed msg=%lu reason=link_close close_reason=%u\n", + session.destination == + LocalDestinationKind::Propagation + ? "PropagationTX" + : "DirectTX", + static_cast(deferred.message_id), + static_cast(reason)); + delivery_notifier_.failed(deferred.message_id); + } + }); + + delivery_attempt_ledger_.takeReceiptsForLink( + session.link_id, + [this](const runtime::DeliveryAttemptReceipt& receipt) + { + if (receipt.message_id != 0) + { + delivery_notifier_.failed(receipt.message_id); + } }); const bool transitioned = @@ -9197,14 +9017,11 @@ void LxmfAdapter::closeLinkSession(LinkSession& session, LinkCloseReason reason) if (session.destination == LocalDestinationKind::CallAudio) { - if (session.call_wire_profile == - ReticulumCallWireProfile::SidebandLxst) - { - (void)reticulum::lxst::call::dispatch( - &session.lxst_call, - {reticulum::lxst::call::EventType::LinkClosed}, - millis()); - } + (void)lxst_telephony_client_.dispatch( + session, + {reticulum::lxst::call::EventType::LinkClosed}, + millis(), + nullptr); ::platform::ui::reticulum_call::notify_link_closed(session.link_id); } @@ -9213,7 +9030,7 @@ void LxmfAdapter::closeLinkSession(LinkSession& session, LinkCloseReason reason) if ((reason == LinkCloseReason::Timeout || reason == LinkCloseReason::Error) && !isZeroBytes(session.remote_destination_hash, sizeof(session.remote_destination_hash))) { - expirePath(session.remote_destination_hash); + path_manager_.expirePath(session.remote_destination_hash); bool requested = false; destination_registry_.forEach( [&](PeerInfo& peer) @@ -9225,7 +9042,7 @@ void LxmfAdapter::closeLinkSession(LinkSession& session, LinkCloseReason reason) { return; } - peer.last_path_request_ms = 0; + path_manager_.resetPeerPathRequest(peer); (void)sendPathRequest(peer); requested = true; }); @@ -9239,42 +9056,43 @@ void LxmfAdapter::flushDeferredLinkPayloads(LinkSession& session) return; } - while (!session.deferred_payloads.empty()) + while (const runtime::DeferredLinkPayload* deferred = + link_manager_.firstDeferredPayload(session)) { - const runtime::DeferredLinkPayload& deferred = session.deferred_payloads.front(); bool sent = false; - if (deferred.payload.size() <= session.mdu) + if (deferred->payload.size() <= session.mdu) { uint8_t packet_hash[reticulum::kFullHashSize] = {}; sent = sendLinkPacket(session, reticulum::PacketType::Data, reticulum::PacketContext::None, - deferred.payload.data(), - deferred.payload.size(), + deferred->payload.data(), + deferred->payload.size(), true, false, - deferred.message_id != 0 ? packet_hash - : nullptr); - if (sent && deferred.message_id != 0) + deferred->message_id != 0 ? packet_hash + : nullptr); + if (sent && deferred->message_id != 0) { - runtime::LinkPacketReceipt receipt{}; - copyHash(receipt.packet_hash, - packet_hash, - sizeof(receipt.packet_hash)); - receipt.message_id = deferred.message_id; - receipt.created_ms = millis(); - session.pending_packet_receipts.push_back(receipt); + delivery_attempt_ledger_.noteLinkPacketReceipt( + packet_hash, + session.link_id, + deferred->message_id, + millis(), + kMaxPendingDeliveryReceipts); } } else { sent = queueOutgoingResource(session, - deferred.payload.data(), - deferred.payload.size(), - deferred.resource_flags, - deferred.request_id.empty() ? nullptr : deferred.request_id.data(), - deferred.request_id.size(), - deferred.message_id); + deferred->payload.data(), + deferred->payload.size(), + deferred->resource_flags, + deferred->request_id.empty() + ? nullptr + : deferred->request_id.data(), + deferred->request_id.size(), + deferred->message_id); } if (!sent) @@ -9282,48 +9100,29 @@ void LxmfAdapter::flushDeferredLinkPayloads(LinkSession& session) break; } - if (deferred.message_id != 0) - { - sys::EventBus::publish( - new sys::ChatSendResultEvent(deferred.message_id, - MessageStatus::Queued), - 0); - } - if (session.destination == LocalDestinationKind::Delivery) { (void)sendLinkIdentify(session); } - if (deferred.message_id != 0) + if (deferred->message_id != 0) { Serial.printf("[LXMF][%s] awaiting_proof msg=%lu path=link payload_len=%u representation=%s\n", session.destination == LocalDestinationKind::Propagation ? "PropagationTX" : "DirectTX", - static_cast(deferred.message_id), - static_cast(deferred.payload.size()), - deferred.payload.size() <= session.mdu + static_cast(deferred->message_id), + static_cast(deferred->payload.size()), + deferred->payload.size() <= session.mdu ? "packet" : "resource"); } - session.deferred_payloads.erase(session.deferred_payloads.begin()); + link_manager_.popFirstDeferredPayload(session); } } -void LxmfAdapter::expirePath( - const uint8_t destination_hash[reticulum::kTruncatedHashSize]) -{ - if (!destination_hash) - { - return; - } - - path_manager_.expirePath(destination_hash); -} - LxmfAdapter::LinkSession* LxmfAdapter::findLinkSession( const uint8_t link_id[reticulum::kTruncatedHashSize]) { @@ -9352,6 +9151,17 @@ void LxmfAdapter::cullLinkSessions() kLinkKeepaliveTimeoutFactor, 5000}; const runtime::ResourceRuntimeLimits resource_limits{kResourceTransferTtlMs}; + delivery_attempt_ledger_.takeExpiredReceipts( + runtime::DeliveryAttemptKind::LinkPacket, + now_ms, + kLinkPacketReceiptTtlMs, + [this](const runtime::DeliveryAttemptReceipt& receipt) + { + if (receipt.message_id != 0) + { + delivery_notifier_.failed(receipt.message_id); + } + }); link_manager_.forEachSession( [this, now_ms, &call_snapshot, &limits, &resource_limits](LinkSession& session) @@ -9359,12 +9169,10 @@ void LxmfAdapter::cullLinkSessions() if (session.destination == LocalDestinationKind::CallAudio && session.state == LinkState::Active) { - if (session.call_wire_profile == - ReticulumCallWireProfile::SidebandLxst && - reticulum::lxst::call::phaseTimedOut( - session.lxst_call, - now_ms)) + if (lxst_telephony_client_.phaseTimedOut(session, now_ms)) { + const auto& call_state = + lxst_telephony_client_.state(session); char link_hash[12] = {}; formatHashPrefix(session.link_id, link_hash, @@ -9372,14 +9180,14 @@ void LxmfAdapter::cullLinkSessions() Serial.printf("[LXMF][Call] phase_timeout link=%s phase=%s local=%u remote=%u elapsed_ms=%lu\n", link_hash, reticulum::lxst::call::phaseName( - session.lxst_call.phase), + call_state.phase), static_cast( - session.lxst_call.local_status), + call_state.local_status), static_cast( - session.lxst_call.remote_status), + call_state.remote_status), static_cast( now_ms - - session.lxst_call.phase_started_ms)); + call_state.phase_started_ms)); (void)dispatchLxstCallEvent( session, {reticulum::lxst::call::EventType::Timeout}); @@ -9411,35 +9219,29 @@ void LxmfAdapter::cullLinkSessions() } link_manager_.cullSessionTables(session, now_ms, limits); - session.pending_packet_receipts.erase( - std::remove_if( - session.pending_packet_receipts.begin(), - session.pending_packet_receipts.end(), - [now_ms](const runtime::LinkPacketReceipt& receipt) - { - if (now_ms - receipt.created_ms <= - kLinkPacketReceiptTtlMs) - { - return false; - } - if (receipt.message_id != 0) - { - sys::EventBus::publish( - new sys::ChatSendResultEvent(receipt.message_id, - false), - 0); - } - return true; - }), - session.pending_packet_receipts.end()); - link_manager_.takeExpiredOutgoingResourceMessageIds( + link_manager_.forEachExpiredOutgoingResource( session, now_ms, kResourceTransferTtlMs, - [](uint32_t message_id) + [this, &session]( + const runtime::LinkResourceTransfer& resource) { - sys::EventBus::publish( - new sys::ChatSendResultEvent(message_id, false), 0); + runtime::DeliveryAttemptReceipt* receipt = + delivery_attempt_ledger_.findLinkResourceReceipt( + session.link_id, + resource.resource_hash); + if (!receipt) + { + return; + } + const uint32_t message_id = receipt->message_id; + delivery_attempt_ledger_.removeLinkResourceReceipt( + session.link_id, + resource.resource_hash); + if (message_id != 0) + { + delivery_notifier_.failed(message_id); + } }); link_manager_.cullResources(session, now_ms, resource_limits); const runtime::LinkRuntimeMaintenance maintenance = @@ -9485,7 +9287,7 @@ LxmfAdapter::PeerInfo* LxmfAdapter::rememberPeerIdentity( reticulum::computeNameHash("lxmf", "delivery", name_hash); reticulum::computeDestinationHash(name_hash, identity_hash, delivery_hash); - PeerInfo& peer = upsertPeer(delivery_hash); + PeerInfo& peer = destination_registry_.upsertDestination(delivery_hash); copyHash(peer.identity_hash, identity_hash, sizeof(peer.identity_hash)); memcpy(peer.enc_pub, combined_pub, LxmfIdentity::kEncPubKeySize); memcpy(peer.sig_pub, @@ -9729,7 +9531,7 @@ bool LxmfAdapter::acceptVerifiedEnvelopeForDestination( } delivery_context.peer_node_id = peer->node_id; - delivery_context.peer_identity = reticulumIdentityForPeer(*peer); + delivery_context.peer_identity = runtime::reticulumIdentityForPeer(*peer); delivery_context.conversation_identity = hasReticulumDestinationIdentity(conversation_identity) ? conversation_identity @@ -9897,23 +9699,6 @@ bool LxmfAdapter::acceptVerifiedEnvelopeForDestination( return false; } -LxmfAdapter::PeerInfo* LxmfAdapter::findPeerByNodeId(NodeId node_id) -{ - return destination_registry_.findByNodeId(node_id); -} - -const LxmfAdapter::PeerInfo* LxmfAdapter::findPeerByDestinationHash( - const uint8_t hash[reticulum::kTruncatedHashSize]) const -{ - return destination_registry_.findByDestinationHash(hash); -} - -const LxmfAdapter::PeerInfo* LxmfAdapter::findPeerByIdentityHash( - const uint8_t hash[reticulum::kTruncatedHashSize]) const -{ - return destination_registry_.findByIdentityHash(hash); -} - const ReticulumGroupDestinationConfig* LxmfAdapter::findConfiguredGroupDestination( const uint8_t hash[reticulum::kTruncatedHashSize]) const { @@ -9945,167 +9730,77 @@ bool LxmfAdapter::isConfiguredGroupDestination( findConfiguredGroupDestination(destination.destination_hash) != nullptr; } -LxmfAdapter::PeerInfo& LxmfAdapter::upsertPeer( - const uint8_t destination_hash[reticulum::kTruncatedHashSize]) -{ - return destination_registry_.upsertDestination(destination_hash); -} - -LxmfAdapter::PeerInfo* LxmfAdapter::upsertPeerFromDirectoryRecord( - const MeshPeerRecord& record, - bool queue_update) -{ - if (!meshPeerRecordIsValid(record) || record.flags.ignored || - !meshPeerSameProtocol(record.identity.protocol, MeshProtocol::Reticulum) || - record.identity.kind != MeshPeerIdentityKind::ReticulumDestination || - !record.reticulum.has_public_keys) - { - return nullptr; - } - - const ReticulumPeerIdentity& identity = record.reticulum.identity.valid - ? record.reticulum.identity - : record.identity.reticulum; - if (!identity.valid || - isZeroBytes(identity.destination_hash, sizeof(identity.destination_hash)) || - isZeroBytes(identity.identity_hash, sizeof(identity.identity_hash)) || - isZeroBytes(record.reticulum.enc_pub, sizeof(record.reticulum.enc_pub)) || - isZeroBytes(record.reticulum.sig_pub, sizeof(record.reticulum.sig_pub))) - { - return nullptr; - } - - PeerInfo& peer = upsertPeer(identity.destination_hash); - copyHash(peer.identity_hash, identity.identity_hash, sizeof(peer.identity_hash)); - memcpy(peer.enc_pub, record.reticulum.enc_pub, sizeof(peer.enc_pub)); - memcpy(peer.sig_pub, record.reticulum.sig_pub, sizeof(peer.sig_pub)); - if (record.reticulum.has_ratchet && - !isZeroBytes(record.reticulum.ratchet_pub, - sizeof(record.reticulum.ratchet_pub))) - { - memcpy(peer.ratchet_pub, - record.reticulum.ratchet_pub, - sizeof(peer.ratchet_pub)); - peer.has_ratchet = true; - peer.ratchet_seen_s = record.reticulum.ratchet_seen_s; - } - else - { - memset(peer.ratchet_pub, 0, sizeof(peer.ratchet_pub)); - peer.has_ratchet = false; - peer.ratchet_seen_s = 0; - } - peer.last_seen_s = record.last_seen_s != 0 - ? record.last_seen_s - : currentTimestampSeconds(); - peer.last_path_request_ms = 0; - copyCString(peer.display_name, sizeof(peer.display_name), record.display_name); - if (queue_update) - { - queuePeerUpdate(peer); - } - return &peer; -} - LxmfAdapter::PeerInfo* LxmfAdapter::findOrLoadPeerByNodeId(NodeId node_id) { - if (node_id == 0) - { - return nullptr; - } - if (PeerInfo* peer = findPeerByNodeId(node_id)) - { - return peer; - } - if (!peer_directory_) + const runtime::PeerDirectoryLoadResult result = + peer_directory_service_.findOrLoadByNodeId(destination_registry_, + node_id, + currentTimestampSeconds()); + if (!result.status.succeeded()) { + if (result.status.code != MeshPeerDirectoryStatusCode::StorageUnavailable && + result.status.code != MeshPeerDirectoryStatusCode::InvalidArgument) + { + Serial.printf("[LXMF][Directory] peer_lookup miss node=%08lX status=%u\n", + static_cast(node_id), + static_cast(result.status.code)); + } return nullptr; } - MeshPeerRecord record{}; - const MeshPeerDirectoryStatus status = - peer_directory_->findByNodeId(MeshProtocol::Reticulum, node_id, record); - if (!status.succeeded()) - { - Serial.printf("[LXMF][Directory] peer_lookup miss node=%08lX status=%u\n", - static_cast(node_id), - static_cast(status.code)); - return nullptr; - } - PeerInfo* peer = upsertPeerFromDirectoryRecord(record, true); - if (peer) + if (result.loaded_from_directory && result.peer) { + queuePeerUpdate(*result.peer); Serial.printf("[LXMF][Directory] peer_lookup loaded node=%08lX name=%s\n", static_cast(node_id), - peer->display_name[0] != '\0' ? peer->display_name : ""); + result.peer->display_name[0] != '\0' + ? result.peer->display_name + : ""); } - return peer; + return result.peer; } LxmfAdapter::PeerInfo* LxmfAdapter::findOrLoadPeerByDestinationHash( const uint8_t destination_hash[reticulum::kTruncatedHashSize]) { - if (!destination_hash) - { - return nullptr; - } - if (PeerInfo* peer = - destination_registry_.findByDestinationHash(destination_hash)) - { - return peer; - } - if (!peer_directory_) + const runtime::PeerDirectoryLoadResult result = + peer_directory_service_.findOrLoadByDestinationHash(destination_registry_, + destination_hash, + currentTimestampSeconds()); + if (!result.status.succeeded()) { + if (result.status.code != MeshPeerDirectoryStatusCode::StorageUnavailable && + result.status.code != MeshPeerDirectoryStatusCode::InvalidArgument) + { + char dest[12] = {}; + formatHashPrefix(destination_hash, dest, sizeof(dest)); + Serial.printf("[LXMF][Directory] peer_lookup miss dest=%s status=%u\n", + dest, + static_cast(result.status.code)); + } return nullptr; } - MeshPeerIdentity identity{}; - identity.protocol = MeshProtocol::Reticulum; - identity.kind = MeshPeerIdentityKind::ReticulumDestination; - identity.reticulum = makeReticulumDestinationIdentity(destination_hash); - MeshPeerRecord record{}; - const MeshPeerDirectoryStatus status = peer_directory_->find(identity, record); - if (!status.succeeded()) + if (result.loaded_from_directory && result.peer) { + queuePeerUpdate(*result.peer); char dest[12] = {}; - formatHashPrefix(destination_hash, dest, sizeof(dest)); - Serial.printf("[LXMF][Directory] peer_lookup miss dest=%s status=%u\n", - dest, - static_cast(status.code)); - return nullptr; - } - PeerInfo* peer = upsertPeerFromDirectoryRecord(record, true); - if (peer) - { - char dest[12] = {}; - formatHashPrefix(peer->destination_hash, dest, sizeof(dest)); + formatHashPrefix(result.peer->destination_hash, dest, sizeof(dest)); Serial.printf("[LXMF][Directory] peer_lookup loaded dest=%s name=%s\n", dest, - peer->display_name[0] != '\0' ? peer->display_name : ""); + result.peer->display_name[0] != '\0' + ? result.peer->display_name + : ""); } - return peer; + return result.peer; } MeshActionResult LxmfAdapter::persistPeerAddressNow(const PeerInfo& peer, bool favorite) const { - if (isZeroBytes(peer.destination_hash, sizeof(peer.destination_hash)) || - isZeroBytes(peer.identity_hash, sizeof(peer.identity_hash)) || - isZeroBytes(peer.enc_pub, sizeof(peer.enc_pub)) || - isZeroBytes(peer.sig_pub, sizeof(peer.sig_pub))) - { - return MeshActionResult::fail(MeshOperationFailure::PeerKeyMissing); - } - if (!peer_directory_) - { - return MeshActionResult::fail(MeshOperationFailure::NotReady); - } - - if (!recordPeerInDirectory(peer, MeshPeerSource::Manual, true, favorite)) - { - return MeshActionResult::fail(MeshOperationFailure::Unknown); - } - return MeshActionResult::success(); + return peer_directory_service_.persistPeerAddressNow(peer, + favorite, + currentTimestampSeconds()); } bool LxmfAdapter::recordPeerInDirectory(const PeerInfo& peer, @@ -10113,72 +9808,32 @@ bool LxmfAdapter::recordPeerInDirectory(const PeerInfo& peer, bool update_favorite, bool favorite) const { - if (!peer_directory_ || - isZeroBytes(peer.destination_hash, sizeof(peer.destination_hash)) || - isZeroBytes(peer.identity_hash, sizeof(peer.identity_hash)) || - isZeroBytes(peer.enc_pub, sizeof(peer.enc_pub)) || - isZeroBytes(peer.sig_pub, sizeof(peer.sig_pub))) + const runtime::PeerDirectoryWriteResult result = + peer_directory_service_.recordPeer(peer, + source, + update_favorite, + favorite, + currentTimestampSeconds()); + if (!result.record_status.succeeded()) { - return false; - } - - MeshPeerRecord record{}; - record.valid = true; - record.identity = makeMeshPeerReticulumIdentity(reticulumIdentityForPeer(peer)); - record.source = source; - const uint32_t now_s = currentTimestampSeconds(); - record.first_seen_s = peer.last_seen_s != 0 ? peer.last_seen_s : now_s; - record.last_seen_s = peer.last_seen_s != 0 ? peer.last_seen_s : now_s; - copyMeshPeerText(record.display_name, - sizeof(record.display_name), - peer.display_name); - record.reticulum.identity = record.identity.reticulum; - record.reticulum.has_public_keys = true; - memcpy(record.reticulum.enc_pub, - peer.enc_pub, - sizeof(record.reticulum.enc_pub)); - memcpy(record.reticulum.sig_pub, - peer.sig_pub, - sizeof(record.reticulum.sig_pub)); - if (peerHasUsableRatchet(peer)) - { - record.reticulum.has_ratchet = true; - memcpy(record.reticulum.ratchet_pub, - peer.ratchet_pub, - sizeof(record.reticulum.ratchet_pub)); - record.reticulum.ratchet_seen_s = peer.ratchet_seen_s; - } - record.reticulum.delivery = true; - - MeshPeerUserFlags flags{}; - if (update_favorite) - { - MeshPeerRecord existing{}; - if (peer_directory_->find(record.identity, existing).succeeded()) + if (result.record_status.code != MeshPeerDirectoryStatusCode::StorageUnavailable && + result.record_status.code != MeshPeerDirectoryStatusCode::InvalidArgument) { - flags = existing.flags; + Serial.printf("[LXMF][Directory] address_save failed status=%u\n", + static_cast(result.record_status.code)); } - flags.favorite = favorite; - } - - const MeshPeerDirectoryStatus record_status = peer_directory_->record(record); - if (!record_status.succeeded()) - { - Serial.printf("[LXMF][Directory] address_save failed status=%u\n", - static_cast(record_status.code)); return false; } - if (update_favorite) + if (!result.flags_status.succeeded()) { - const MeshPeerDirectoryStatus flags_status = - peer_directory_->setUserFlags(record.identity, flags); - if (!flags_status.succeeded()) + if (result.flags_status.code != MeshPeerDirectoryStatusCode::StorageUnavailable && + result.flags_status.code != MeshPeerDirectoryStatusCode::InvalidArgument) { Serial.printf("[LXMF][Directory] flag_save failed status=%u\n", - static_cast(flags_status.code)); - return false; + static_cast(result.flags_status.code)); } + return false; } return true; } @@ -10234,7 +9889,7 @@ void LxmfAdapter::pumpPendingPeerUpdates() --pending_peer_projection_count_; last_peer_projection_ms_ = now_ms; - const PeerInfo* peer = findPeerByNodeId(node_id); + const PeerInfo* peer = destination_registry_.findByNodeId(node_id); if (peer) { publishPeerUpdate(*peer); @@ -10265,44 +9920,48 @@ void LxmfAdapter::publishPeerUpdate(const PeerInfo& peer) const 0, 0, 0xFF); - node_event->reticulum_identity = reticulumIdentityForPeer(peer); + node_event->reticulum_identity = runtime::reticulumIdentityForPeer(peer); sys::EventBus::publish(node_event, 0); } void LxmfAdapter::loadDirectoryPeers() { - if (!peer_directory_) + if (!peer_directory_service_.hasDirectory()) { Serial.printf("[LXMF][Directory] load skipped reason=no_mesh_peer_directory\n"); return; } - std::size_t count = 0; - const MeshPeerDirectoryStatus status = - peer_directory_->loadRecent(MeshProtocol::Reticulum, - peer_directory_load_entries_.data(), - peer_directory_load_entries_.size(), - &count); - if (!status.succeeded()) + std::array loaded_nodes = {}; + const runtime::PeerDirectoryLoadRecentResult result = + peer_directory_service_.loadRecent(destination_registry_, + peer_directory_load_entries_.data(), + peer_directory_load_entries_.size(), + loaded_nodes.data(), + loaded_nodes.size(), + currentTimestampSeconds()); + if (!result.status.succeeded()) { Serial.printf("[LXMF][Directory] load failed status=%u\n", - static_cast(status.code)); + static_cast(result.status.code)); return; } - std::size_t loaded = 0; - for (std::size_t index = 0; index < count; ++index) + const std::size_t queued_count = + result.loaded < loaded_nodes.size() ? result.loaded : loaded_nodes.size(); + for (std::size_t index = 0; index < queued_count; ++index) { - if (upsertPeerFromDirectoryRecord(peer_directory_load_entries_[index], true)) + const PeerInfo* peer = destination_registry_.findByNodeId(loaded_nodes[index]); + if (peer) { - ++loaded; + queuePeerUpdate(*peer); } } - if (loaded > 0) + if (result.loaded > 0) { Serial.printf("[LXMF][Directory] loaded addresses=%u directory=mesh_peer_directory\n", - static_cast(loaded)); + static_cast(result.loaded)); } } diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter_call.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter_call.cpp index 951b2c70..24ad9bf8 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter_call.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter_call.cpp @@ -86,7 +86,7 @@ void LxmfAdapter::updateCallRuntimePeer(LinkSession& session, call_profile::runtimeWireProfile(session.call_wire_profile); call_peer.codec2_mode = call_profile::runtimeCodec2Mode( session.call_wire_profile, - session.lxst_call.profile); + lxst_telephony_client_.profile(session)); ::platform::ui::reticulum_call::update_peer(call_peer); } @@ -114,10 +114,11 @@ bool LxmfAdapter::beginIncomingCallRuntime(LinkSession& session, call_profile::runtimeWireProfile(session.call_wire_profile); call_peer.codec2_mode = call_profile::runtimeCodec2Mode( session.call_wire_profile, - session.lxst_call.profile); - session.call_runtime_started = + lxst_telephony_client_.profile(session)); + const bool runtime_started = ::platform::ui::reticulum_call::begin_incoming(call_peer); - if (session.call_runtime_started && session.state == LinkState::Active) + lxst_telephony_client_.markRuntimeStarted(session, runtime_started); + if (runtime_started && session.state == LinkState::Active) { ::platform::ui::reticulum_call::mark_link_active(session.link_id); } @@ -127,26 +128,23 @@ bool LxmfAdapter::beginIncomingCallRuntime(LinkSession& session, Serial.printf("[LXMF][CallRX] identified link=%s wire=%u runtime=%u peer=%s\n", link_hash, static_cast(session.call_wire_profile), - session.call_runtime_started ? 1U : 0U, + runtime_started ? 1U : 0U, call_peer.display_name ? call_peer.display_name : ""); - return session.call_runtime_started; + return runtime_started; } bool LxmfAdapter::sendLxstSignal(LinkSession& session, uint16_t signal, bool call_admission_control) { - if (session.destination != LocalDestinationKind::CallAudio || - session.call_wire_profile != ReticulumCallWireProfile::SidebandLxst) + if (!lxst_telephony_client_.isSidebandSession(session)) { return false; } - uint8_t* scratch = lxst_telephony_client_.scratch(); - size_t payload_len = lxst_telephony_client_.scratchCapacity(); - if (!reticulum::lxst::encodeSignalling(signal, - scratch, - &payload_len)) + uint8_t* scratch = nullptr; + size_t payload_len = 0; + if (!lxst_telephony_client_.encodeSignal(signal, &scratch, &payload_len)) { return false; } @@ -171,17 +169,18 @@ bool LxmfAdapter::dispatchLxstCallEvent( LinkSession& session, const reticulum::lxst::call::Event& event) { - if (session.destination != LocalDestinationKind::CallAudio || - session.call_wire_profile != ReticulumCallWireProfile::SidebandLxst) + if (!lxst_telephony_client_.isSidebandSession(session)) { return false; } - const auto previous_phase = session.lxst_call.phase; - const auto transition = reticulum::lxst::call::dispatch( - &session.lxst_call, + reticulum::lxst::call::Phase previous_phase = + reticulum::lxst::call::Phase::Idle; + const auto transition = lxst_telephony_client_.dispatch( + session, event, - millis()); + millis(), + &previous_phase); if (!transition.accepted) { return false; @@ -193,7 +192,8 @@ bool LxmfAdapter::dispatchLxstCallEvent( link_hash, static_cast(event.type), reticulum::lxst::call::phaseName(previous_phase), - reticulum::lxst::call::phaseName(session.lxst_call.phase), + reticulum::lxst::call::phaseName( + lxst_telephony_client_.phase(session)), static_cast(transition.action_count)); bool closes_link = false; @@ -225,7 +225,7 @@ bool LxmfAdapter::dispatchLxstCallEvent( case reticulum::lxst::call::Action::BeginRinging: { const PeerInfo* peer = - findPeerByIdentityHash(session.remote_identity_hash); + destination_registry_.findByIdentityHash(session.remote_identity_hash); const bool admitted = peer && beginIncomingCallRuntime(session, *peer); return dispatchLxstCallEvent( @@ -244,7 +244,7 @@ bool LxmfAdapter::dispatchLxstCallEvent( action_ok = sendLxstSignal( session, reticulum::lxst::kPreferredProfile + - session.lxst_call.profile, + lxst_telephony_client_.profile(session), true); break; case reticulum::lxst::call::Action::SendConnecting: @@ -332,16 +332,16 @@ bool LxmfAdapter::handleLxstPacket(LinkSession& session, { updateCallRuntimePeer( session, - findPeerByIdentityHash(session.remote_identity_hash)); + destination_registry_.findByIdentityHash(session.remote_identity_hash)); } } const reticulum::audio_call::Codec2Mode expected_mode = - call_profile::audioCodec2Mode(session.lxst_call.profile); + call_profile::audioCodec2Mode(lxst_telephony_client_.profile(session)); for (size_t index = 0; index < decoded.frame_count; ++index) { const auto& frame = decoded.frames[index]; - if (session.lxst_call.phase != + if (lxst_telephony_client_.phase(session) != reticulum::lxst::call::Phase::Active || frame.codec != reticulum::lxst::kCodec2 || !frame.codec2_mode_valid || @@ -391,7 +391,8 @@ bool LxmfAdapter::sendCallAudioPacket(LinkSession& session, payload_len, &decoded) || decoded.mode != - call_profile::audioCodec2Mode(session.lxst_call.profile)) + call_profile::audioCodec2Mode( + lxst_telephony_client_.profile(session))) { return false; } @@ -495,7 +496,7 @@ void LxmfAdapter::pumpReticulumAudioCall() #endif !call_session->initiator && call_snapshot.accepted && - call_session->lxst_call.phase == + lxst_telephony_client_.phase(*call_session) == reticulum::lxst::call::Phase::CalleeRinging) { (void)dispatchLxstCallEvent( diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_announce_ingestor.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_announce_ingestor.cpp index 6b761635..01def8cc 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_announce_ingestor.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_announce_ingestor.cpp @@ -403,35 +403,20 @@ bool AnnounceIngestor::ingest(const uint8_t* raw_packet, return true; } - result.path = &path_manager.upsertPath(packet.destination_hash, - options.max_paths); - applyPathAnnounce(*result.path, - packet.hops, - result.announce.random_hash, - options.now_ms, - options.now_s); - result.path->interface_id = options.ingress_interface_id; - result.path->direct = (packet.transport_id == nullptr); - path_manager.resolvePendingPathRequest(packet.destination_hash); - if (packet.transport_id) + result.path = path_manager.observeAnnouncePath(packet.destination_hash, + packet.hops, + result.announce.random_hash, + options.now_ms, + options.now_s, + options.ingress_interface_id, + packet.transport_id, + raw_packet, + raw_len, + options.max_paths); + if (!result.path) { - copyHash(result.path->next_hop_transport, - packet.transport_id, - sizeof(result.path->next_hop_transport)); - } - else - { - copyHash(result.path->next_hop_transport, - packet.destination_hash, - sizeof(result.path->next_hop_transport)); - } - if (raw_len <= sizeof(result.path->cached_announce)) - { - std::memcpy(result.path->cached_announce, raw_packet, raw_len); - result.path->cached_announce_len = raw_len; - reticulum::computePacketHash(raw_packet, - raw_len, - result.path->cached_packet_hash); + result.reason = "path_observe_failed"; + return false; } result.delivery_announce = isLxmfDeliveryAnnounce(result.announce); diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_attempt_ledger.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_attempt_ledger.cpp new file mode 100644 index 00000000..8c6958ce --- /dev/null +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_attempt_ledger.cpp @@ -0,0 +1,376 @@ +/** + * @file lxmf_delivery_attempt_ledger.cpp + * @brief Reticulum/LXMF outbound delivery attempt and proof receipt owner. + */ + +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_attempt_ledger.h" + +#include +#include + +namespace chat::lxmf::runtime +{ +namespace +{ + +bool hashesEqual(const uint8_t* a, const uint8_t* b, std::size_t len) +{ + return a && b && std::memcmp(a, b, len) == 0; +} + +void copyHash(uint8_t* out, const uint8_t* in, std::size_t len) +{ + if (!out || !in) + { + return; + } + std::memcpy(out, in, len); +} + +} // namespace + +void DeliveryAttemptLedger::noteDirectPacketReceipt( + const uint8_t packet_hash[reticulum::kFullHashSize], + const uint8_t destination_hash[reticulum::kTruncatedHashSize], + const uint8_t peer_sig_pub[LxmfIdentity::kSigPubKeySize], + MessageId message_id, + uint32_t now_ms, + std::size_t max_receipts) +{ + if (!packet_hash || !destination_hash || !peer_sig_pub || message_id == 0) + { + return; + } + + uint8_t proof_hash[reticulum::kTruncatedHashSize] = {}; + copyHash(proof_hash, packet_hash, sizeof(proof_hash)); + removeReceiptByProofHash(proof_hash); + + DeliveryAttemptReceipt receipt{}; + copyHash(receipt.packet_hash, packet_hash, sizeof(receipt.packet_hash)); + copyHash(receipt.proof_hash, proof_hash, sizeof(receipt.proof_hash)); + copyHash(receipt.destination_hash, + destination_hash, + sizeof(receipt.destination_hash)); + copyHash(receipt.peer_sig_pub, + peer_sig_pub, + sizeof(receipt.peer_sig_pub)); + receipt.message_id = message_id; + receipt.created_ms = now_ms == 0 ? 1U : now_ms; + receipt.kind = DeliveryAttemptKind::DirectPacket; + trimOldestReceipts(receipt.kind, max_receipts, true); + receipts_.push_back(receipt); +} + +void DeliveryAttemptLedger::noteLinkPacketReceipt( + const uint8_t packet_hash[reticulum::kFullHashSize], + const uint8_t link_id[reticulum::kTruncatedHashSize], + MessageId message_id, + uint32_t now_ms, + std::size_t max_receipts) +{ + if (!packet_hash || !link_id || message_id == 0) + { + return; + } + + removeLinkPacketReceipt(link_id, packet_hash); + DeliveryAttemptReceipt receipt{}; + copyHash(receipt.packet_hash, packet_hash, sizeof(receipt.packet_hash)); + copyHash(receipt.link_id, link_id, sizeof(receipt.link_id)); + receipt.message_id = message_id; + receipt.created_ms = now_ms == 0 ? 1U : now_ms; + receipt.kind = DeliveryAttemptKind::LinkPacket; + trimOldestReceipts(receipt.kind, max_receipts, true); + receipts_.push_back(receipt); +} + +void DeliveryAttemptLedger::noteLinkResourceReceipt( + const uint8_t resource_hash[reticulum::kFullHashSize], + const uint8_t link_id[reticulum::kTruncatedHashSize], + MessageId message_id, + uint32_t now_ms, + std::size_t max_receipts) +{ + if (!resource_hash || !link_id || message_id == 0) + { + return; + } + + removeLinkResourceReceipt(link_id, resource_hash); + DeliveryAttemptReceipt receipt{}; + copyHash(receipt.packet_hash, resource_hash, sizeof(receipt.packet_hash)); + copyHash(receipt.link_id, link_id, sizeof(receipt.link_id)); + receipt.message_id = message_id; + receipt.created_ms = now_ms == 0 ? 1U : now_ms; + receipt.kind = DeliveryAttemptKind::LinkResource; + trimOldestReceipts(receipt.kind, max_receipts, true); + receipts_.push_back(receipt); +} + +void DeliveryAttemptLedger::notePropagationReceipt( + const uint8_t transient_id[reticulum::kFullHashSize], + MessageId message_id, + uint32_t now_ms, + std::size_t max_receipts) +{ + if (!transient_id || message_id == 0) + { + return; + } + + removePropagationReceipt(transient_id); + DeliveryAttemptReceipt receipt{}; + copyHash(receipt.packet_hash, transient_id, sizeof(receipt.packet_hash)); + receipt.message_id = message_id; + receipt.created_ms = now_ms == 0 ? 1U : now_ms; + receipt.kind = DeliveryAttemptKind::Propagation; + trimOldestReceipts(receipt.kind, max_receipts, true); + receipts_.push_back(receipt); +} + +DeliveryAttemptReceipt* DeliveryAttemptLedger::findReceiptByProofHash( + const uint8_t proof_hash[reticulum::kTruncatedHashSize]) +{ + if (!proof_hash) + { + return nullptr; + } + for (auto& receipt : receipts_) + { + if (receipt.created_ms != 0 && + hashesEqual(receipt.proof_hash, + proof_hash, + sizeof(receipt.proof_hash))) + { + return &receipt; + } + } + return nullptr; +} + +void DeliveryAttemptLedger::removeReceiptByProofHash( + const uint8_t proof_hash[reticulum::kTruncatedHashSize]) +{ + if (!proof_hash) + { + return; + } + receipts_.erase( + std::remove_if( + receipts_.begin(), + receipts_.end(), + [proof_hash](const DeliveryAttemptReceipt& receipt) + { + return hashesEqual(receipt.proof_hash, + proof_hash, + sizeof(receipt.proof_hash)); + }), + receipts_.end()); +} + +DeliveryAttemptReceipt* DeliveryAttemptLedger::findLinkPacketReceipt( + const uint8_t link_id[reticulum::kTruncatedHashSize], + const uint8_t packet_hash[reticulum::kFullHashSize]) +{ + if (!link_id || !packet_hash) + { + return nullptr; + } + for (auto& receipt : receipts_) + { + if (receipt.created_ms != 0 && + receipt.kind == DeliveryAttemptKind::LinkPacket && + hashesEqual(receipt.link_id, link_id, sizeof(receipt.link_id)) && + hashesEqual(receipt.packet_hash, + packet_hash, + sizeof(receipt.packet_hash))) + { + return &receipt; + } + } + return nullptr; +} + +void DeliveryAttemptLedger::removeLinkPacketReceipt( + const uint8_t link_id[reticulum::kTruncatedHashSize], + const uint8_t packet_hash[reticulum::kFullHashSize]) +{ + if (!link_id || !packet_hash) + { + return; + } + receipts_.erase( + std::remove_if( + receipts_.begin(), + receipts_.end(), + [link_id, packet_hash](const DeliveryAttemptReceipt& receipt) + { + return receipt.kind == DeliveryAttemptKind::LinkPacket && + hashesEqual(receipt.link_id, + link_id, + sizeof(receipt.link_id)) && + hashesEqual(receipt.packet_hash, + packet_hash, + sizeof(receipt.packet_hash)); + }), + receipts_.end()); +} + +DeliveryAttemptReceipt* DeliveryAttemptLedger::findLinkResourceReceipt( + const uint8_t link_id[reticulum::kTruncatedHashSize], + const uint8_t resource_hash[reticulum::kFullHashSize]) +{ + if (!link_id || !resource_hash) + { + return nullptr; + } + for (auto& receipt : receipts_) + { + if (receipt.created_ms != 0 && + receipt.kind == DeliveryAttemptKind::LinkResource && + hashesEqual(receipt.link_id, link_id, sizeof(receipt.link_id)) && + hashesEqual(receipt.packet_hash, + resource_hash, + sizeof(receipt.packet_hash))) + { + return &receipt; + } + } + return nullptr; +} + +void DeliveryAttemptLedger::removeLinkResourceReceipt( + const uint8_t link_id[reticulum::kTruncatedHashSize], + const uint8_t resource_hash[reticulum::kFullHashSize]) +{ + if (!link_id || !resource_hash) + { + return; + } + receipts_.erase( + std::remove_if( + receipts_.begin(), + receipts_.end(), + [link_id, resource_hash](const DeliveryAttemptReceipt& receipt) + { + return receipt.kind == DeliveryAttemptKind::LinkResource && + hashesEqual(receipt.link_id, + link_id, + sizeof(receipt.link_id)) && + hashesEqual(receipt.packet_hash, + resource_hash, + sizeof(receipt.packet_hash)); + }), + receipts_.end()); +} + +DeliveryAttemptReceipt* DeliveryAttemptLedger::findPropagationReceipt( + const uint8_t transient_id[reticulum::kFullHashSize]) +{ + if (!transient_id) + { + return nullptr; + } + for (auto& receipt : receipts_) + { + if (receipt.created_ms != 0 && + receipt.kind == DeliveryAttemptKind::Propagation && + hashesEqual(receipt.packet_hash, + transient_id, + sizeof(receipt.packet_hash))) + { + return &receipt; + } + } + return nullptr; +} + +void DeliveryAttemptLedger::removePropagationReceipt( + const uint8_t transient_id[reticulum::kFullHashSize]) +{ + if (!transient_id) + { + return; + } + receipts_.erase( + std::remove_if( + receipts_.begin(), + receipts_.end(), + [transient_id](const DeliveryAttemptReceipt& receipt) + { + return receipt.kind == DeliveryAttemptKind::Propagation && + hashesEqual(receipt.packet_hash, + transient_id, + sizeof(receipt.packet_hash)); + }), + receipts_.end()); +} + +void DeliveryAttemptLedger::cull(DeliveryAttemptKind kind, + uint32_t now_ms, + uint32_t receipt_ttl_ms, + std::size_t max_receipts) +{ + if (receipt_ttl_ms != 0) + { + receipts_.erase( + std::remove_if( + receipts_.begin(), + receipts_.end(), + [kind, now_ms, receipt_ttl_ms]( + const DeliveryAttemptReceipt& receipt) + { + return receipt.kind == kind && + receipt.created_ms != 0 && + now_ms - receipt.created_ms > receipt_ttl_ms; + }), + receipts_.end()); + } + trimOldestReceipts(kind, max_receipts, false); +} + +void DeliveryAttemptLedger::clear() +{ + receipts_.clear(); +} + +std::size_t DeliveryAttemptLedger::size() const +{ + return receipts_.size(); +} + +void DeliveryAttemptLedger::trimOldestReceipts(DeliveryAttemptKind kind, + std::size_t max_receipts, + bool reserve_slot) +{ + if (max_receipts == 0) + { + return; + } + + std::size_t matching = 0; + for (const auto& receipt : receipts_) + { + if (receipt.kind == kind) + { + ++matching; + } + } + + while (matching != 0 && + matching + (reserve_slot ? 1U : 0U) > max_receipts) + { + for (auto it = receipts_.begin(); it != receipts_.end(); ++it) + { + if (it->kind == kind) + { + receipts_.erase(it); + --matching; + break; + } + } + } +} + +} // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_notifier.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_notifier.cpp new file mode 100644 index 00000000..ac894b83 --- /dev/null +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_notifier.cpp @@ -0,0 +1,59 @@ +/** + * @file lxmf_delivery_notifier.cpp + * @brief Reticulum/LXMF outbound delivery status publisher. + */ + +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_notifier.h" + +#if !defined(TRAIL_MATE_RETICULUM_PARSE_ONLY) +#include "sys/event_bus.h" +#endif + +namespace chat::lxmf::runtime +{ + +void LxmfDeliveryNotifier::publish(MessageId message_id, + MessageStatus status) const +{ + if (message_id == 0) + { + return; + } +#if !defined(TRAIL_MATE_RETICULUM_PARSE_ONLY) + sys::EventBus::publish( + new sys::ChatSendResultEvent(message_id, + status, + MeshProtocol::Reticulum), + 0); +#else + (void)status; +#endif +} + +void LxmfDeliveryNotifier::publish(MessageId message_id, bool success) const +{ + publish(message_id, + success ? MessageStatus::Sent : MessageStatus::Failed); +} + +void LxmfDeliveryNotifier::queued(MessageId message_id) const +{ + publish(message_id, MessageStatus::Queued); +} + +void LxmfDeliveryNotifier::sent(MessageId message_id) const +{ + publish(message_id, MessageStatus::Sent); +} + +void LxmfDeliveryNotifier::delivered(MessageId message_id) const +{ + publish(message_id, MessageStatus::Delivered); +} + +void LxmfDeliveryNotifier::failed(MessageId message_id) const +{ + publish(message_id, MessageStatus::Failed); +} + +} // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_planner.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_planner.cpp new file mode 100644 index 00000000..849d82ff --- /dev/null +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_planner.cpp @@ -0,0 +1,68 @@ +/** + * @file lxmf_delivery_planner.cpp + * @brief Reticulum/LXMF outbound delivery route decision owner. + */ + +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_planner.h" + +namespace chat::lxmf::runtime +{ + +OutboundDeliveryPlan ReticulumDeliveryPlanner::plan( + const OutboundDeliveryPlanInput& input) +{ + OutboundDeliveryPlan result{}; + const bool propagation_only = + input.propagation_enabled && + input.propagation_preference == + chat::reticulum::LxmfDeliveryPreference::Propagated; + const bool propagation_automatic = + input.propagation_enabled && + input.propagation_preference == + chat::reticulum::LxmfDeliveryPreference::Automatic && + !input.has_active_link && !input.peer_has_usable_ratchet && + input.propagation_peer_available; + + result.propagation_only = propagation_only; + result.propagation_first = propagation_only || propagation_automatic; + result.may_fallback_to_link = !propagation_only; + + if (result.propagation_first) + { + result.path = OutboundDeliveryPath::Propagation; + } + else if (input.has_active_link) + { + result.path = OutboundDeliveryPath::Link; + } + else if (input.peer_has_usable_ratchet) + { + result.path = OutboundDeliveryPath::Opportunistic; + } + else + { + result.path = OutboundDeliveryPath::DeferredLink; + } + + return result; +} + +const char* ReticulumDeliveryPlanner::pathName(OutboundDeliveryPath path) +{ + switch (path) + { + case OutboundDeliveryPath::Link: + return "link"; + case OutboundDeliveryPath::Opportunistic: + return "opportunistic"; + case OutboundDeliveryPath::DeferredLink: + return "deferred_link"; + case OutboundDeliveryPath::Propagation: + return "propagation"; + case OutboundDeliveryPath::None: + return "none"; + } + return "none"; +} + +} // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_runtime.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_runtime.cpp index 638af690..778ccbdf 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_runtime.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_delivery_runtime.cpp @@ -64,7 +64,7 @@ bool materialiseLxmfAppDataDelivery(const DecodedAppData& payload, delivery.incoming.channel = ChannelId::PRIMARY; delivery.incoming.want_response = payload.want_response; delivery.incoming.rx_meta = context.rx_meta; - delivery.payload = payload.payload; + delivery.payload.assign(payload.payload.begin(), payload.payload.end()); *out_delivery = std::move(delivery); return true; diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_link_manager.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_link_manager.cpp index 2e098fe0..cb8e6570 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_link_manager.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_link_manager.cpp @@ -30,6 +30,15 @@ bool hashesEqual(const uint8_t* a, const uint8_t* b, std::size_t len) return true; } +void copyBytes(uint8_t* out, const uint8_t* in, std::size_t len) +{ + if (!out || !in || len == 0) + { + return; + } + std::memcpy(out, in, len); +} + } // namespace std::size_t LinkManager::size() const @@ -113,12 +122,86 @@ LinkSession* LinkManager::appendSessionPreserving( return &links_.sessions.back(); } -void LinkManager::discardLastSession() +void LinkManager::initialiseSession(LinkSession& session, + const LinkSessionSpec& spec) { - if (!links_.sessions.empty()) + session = LinkSession{}; + copyBytes(session.link_id, + spec.link_id, + sizeof(session.link_id)); + copyBytes(session.remote_destination_hash, + spec.remote_destination_hash, + sizeof(session.remote_destination_hash)); + copyBytes(session.remote_identity_hash, + spec.remote_identity_hash, + sizeof(session.remote_identity_hash)); + copyBytes(session.local_sig_pub, + spec.local_sig_pub, + sizeof(session.local_sig_pub)); + copyBytes(session.peer_enc_pub, + spec.peer_enc_pub, + sizeof(session.peer_enc_pub)); + copyBytes(session.peer_link_sig_pub, + spec.peer_link_sig_pub, + sizeof(session.peer_link_sig_pub)); + copyBytes(session.peer_identity_sig_pub, + spec.peer_identity_sig_pub, + sizeof(session.peer_identity_sig_pub)); + + session.created_ms = spec.now_ms; + session.request_ms = spec.now_ms; + session.last_inbound_ms = spec.now_ms; + session.last_outbound_ms = 0; + session.keepalive_interval_ms = spec.keepalive_interval_ms; + session.stale_timeout_ms = spec.stale_timeout_ms; + session.mtu = spec.mtu; + session.mdu = spec.mdu; + session.interface_id = spec.interface_id; + session.expected_hops = spec.expected_hops; + session.destination = spec.destination; + session.state = spec.state; + session.close_reason = LinkCloseReason::None; + session.initiator = spec.initiator; + session.remote_identity_known = spec.remote_identity_known; + session.validated = spec.validated; +} + +LinkSession* LinkManager::openSession(std::size_t max_link_sessions, + const LinkSessionSpec& spec) +{ + return openSessionPreserving(max_link_sessions, spec, nullptr); +} + +LinkSession* LinkManager::openSessionPreserving( + std::size_t max_link_sessions, + const LinkSessionSpec& spec, + const uint8_t preserve_link_id[reticulum::kTruncatedHashSize]) +{ + LinkSession* session = + appendSessionPreserving(max_link_sessions, preserve_link_id); + if (!session) { - links_.sessions.pop_back(); + return nullptr; } + initialiseSession(*session, spec); + return session; +} + +bool LinkManager::discardSession(LinkSession& session) +{ + const auto it = + std::find_if(links_.sessions.begin(), + links_.sessions.end(), + [&session](LinkSession& candidate) + { + return &candidate == &session; + }); + if (it == links_.sessions.end()) + { + return false; + } + links_.sessions.erase(it); + return true; } bool LinkManager::closeSession(LinkSession& session, @@ -128,6 +211,184 @@ bool LinkManager::closeSession(LinkSession& session, return runtime::closeLinkSession(session, reason, now_ms); } +LinkPendingRequest* LinkManager::queuePendingRequest( + LinkSession& session, + const uint8_t* request_id, + std::size_t request_id_len, + uint32_t created_ms, + bool awaiting_resource) +{ + if (!request_id && request_id_len != 0) + { + return nullptr; + } + + LinkPendingRequest request{}; + if (request_id_len != 0) + { + request.request_id.assign(request_id, request_id + request_id_len); + } + request.created_ms = created_ms; + request.awaiting_resource = awaiting_resource; + session.pending_requests.push_back(std::move(request)); + return &session.pending_requests.back(); +} + +LinkPendingRequest* LinkManager::findPendingRequest( + LinkSession& session, + const uint8_t* request_id, + std::size_t request_id_len) +{ + if (!request_id && request_id_len != 0) + { + return nullptr; + } + return findPendingRequestIf( + session, + [request_id, request_id_len](const LinkPendingRequest& request) + { + return request.request_id.size() == request_id_len && + (request_id_len == 0 || + std::memcmp(request.request_id.data(), + request_id, + request_id_len) == 0); + }); +} + +const LinkPendingRequest* LinkManager::findPendingRequest( + const LinkSession& session, + const uint8_t* request_id, + std::size_t request_id_len) const +{ + if (!request_id && request_id_len != 0) + { + return nullptr; + } + return findPendingRequestIf( + session, + [request_id, request_id_len](const LinkPendingRequest& request) + { + return request.request_id.size() == request_id_len && + (request_id_len == 0 || + std::memcmp(request.request_id.data(), + request_id, + request_id_len) == 0); + }); +} + +bool LinkManager::markPendingResponseReady(LinkSession& session, + const uint8_t* request_id, + std::size_t request_id_len, + const uint8_t* response_data, + std::size_t response_len, + bool data_is_nil) +{ + LinkPendingRequest* request = + findPendingRequest(session, request_id, request_id_len); + if (!request) + { + return false; + } + + request->response_ready = true; + request->response.clear(); + if (!data_is_nil && response_data && response_len != 0) + { + request->response.assign(response_data, response_data + response_len); + } + return true; +} + +bool LinkManager::erasePendingRequest(LinkSession& session, + const LinkPendingRequest& request) +{ + auto it = std::find_if( + session.pending_requests.begin(), + session.pending_requests.end(), + [&request](const LinkPendingRequest& candidate) + { + return &candidate == &request; + }); + if (it == session.pending_requests.end()) + { + return false; + } + session.pending_requests.erase(it); + return true; +} + +std::size_t LinkManager::pendingRequestCount(const LinkSession& session) const +{ + return session.pending_requests.size(); +} + +DeferredLinkPayload* LinkManager::appendDeferredPayload( + LinkSession& session, + DeferredLinkPayload&& payload) +{ + session.deferred_payloads.push_back(std::move(payload)); + return &session.deferred_payloads.back(); +} + +const DeferredLinkPayload* LinkManager::firstDeferredPayload( + const LinkSession& session) const +{ + return session.deferred_payloads.empty() ? nullptr + : &session.deferred_payloads.front(); +} + +bool LinkManager::popFirstDeferredPayload(LinkSession& session) +{ + if (session.deferred_payloads.empty()) + { + return false; + } + session.deferred_payloads.erase(session.deferred_payloads.begin()); + return true; +} + +std::size_t LinkManager::deferredPayloadCount(const LinkSession& session) const +{ + return session.deferred_payloads.size(); +} + +void LinkManager::touchInbound(LinkSession& session, uint32_t now_ms) +{ + session.last_inbound_ms = now_ms; +} + +void LinkManager::touchOutbound(LinkSession& session, uint32_t now_ms) +{ + session.last_outbound_ms = now_ms; +} + +void LinkManager::noteKeepaliveSent(LinkSession& session, uint32_t now_ms) +{ + session.last_keepalive_ms = now_ms; +} + +void LinkManager::markSessionValidatedActive(LinkSession& session, + float rtt_s, + uint32_t keepalive_interval_ms) +{ + session.rtt_s = rtt_s; + session.validated = true; + session.keepalive_interval_ms = keepalive_interval_ms; + session.stale_timeout_ms = keepalive_interval_ms * 2U; + session.last_keepalive_ms = 0; + session.state = LinkState::Active; +} + +bool LinkManager::reactivateSessionIfStale(LinkSession& session) +{ + if (session.state != LinkState::Stale) + { + return false; + } + session.state = LinkState::Active; + return true; +} + void LinkManager::cullSessionTables(LinkSession& session, uint32_t now_ms, const LinkRuntimeLimits& limits) @@ -365,13 +626,6 @@ bool LinkManager::markOutgoingResourceProofReceived( return runtime::markResourceProofReceived(resource, expected_proof, now_ms); } -uint32_t LinkManager::takeResourceMessageId(LinkResourceTransfer& resource) -{ - const uint32_t message_id = resource.message_id; - resource.message_id = 0; - return message_id; -} - void LinkManager::touchResource(LinkResourceTransfer& resource, uint32_t now_ms) { resource.last_activity_ms = now_ms; diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_lxst_telephony_client.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_lxst_telephony_client.cpp index 4a72183e..cb033b20 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_lxst_telephony_client.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_lxst_telephony_client.cpp @@ -5,6 +5,8 @@ #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_lxst_telephony_client.h" +#include "chat/infra/reticulum/lxst_telephony_wire.h" + namespace chat::lxmf::runtime { @@ -23,4 +25,102 @@ std::size_t LxstTelephonyClient::scratchCapacity() const return sizeof(scratch_); } +bool LxstTelephonyClient::isCallSession(const LinkSession& session) const +{ + return session.destination == LocalDestinationKind::CallAudio; +} + +bool LxstTelephonyClient::isSidebandSession(const LinkSession& session) const +{ + return isCallSession(session) && + session.call_wire_profile == ReticulumCallWireProfile::SidebandLxst; +} + +bool LxstTelephonyClient::runtimeStarted(const LinkSession& session) const +{ + return session.call_runtime_started; +} + +void LxstTelephonyClient::markRuntimeStarted(LinkSession& session, bool started) +{ + if (isCallSession(session)) + { + session.call_runtime_started = started; + } +} + +void LxstTelephonyClient::beginCallerSession( + LinkSession& session, + ReticulumCallWireProfile wire_profile, + uint16_t profile, + uint32_t now_ms) +{ + session.call_wire_profile = wire_profile; + session.call_runtime_started = false; + session.lxst_call = reticulum::lxst::call::makeCaller(profile, now_ms); +} + +void LxstTelephonyClient::beginSidebandCalleeSession(LinkSession& session, + uint16_t profile, + uint32_t now_ms) +{ + session.call_wire_profile = ReticulumCallWireProfile::SidebandLxst; + session.call_runtime_started = false; + session.lxst_call = reticulum::lxst::call::makeCallee(profile, now_ms); +} + +uint16_t LxstTelephonyClient::profile(const LinkSession& session) const +{ + return session.lxst_call.profile; +} + +reticulum::lxst::call::Phase LxstTelephonyClient::phase( + const LinkSession& session) const +{ + return session.lxst_call.phase; +} + +const reticulum::lxst::call::State& LxstTelephonyClient::state( + const LinkSession& session) const +{ + return session.lxst_call; +} + +bool LxstTelephonyClient::phaseTimedOut(const LinkSession& session, + uint32_t now_ms) const +{ + return isSidebandSession(session) && + reticulum::lxst::call::phaseTimedOut(session.lxst_call, now_ms); +} + +reticulum::lxst::call::Transition LxstTelephonyClient::dispatch( + LinkSession& session, + const reticulum::lxst::call::Event& event, + uint32_t now_ms, + reticulum::lxst::call::Phase* out_previous_phase) +{ + if (out_previous_phase) + { + *out_previous_phase = session.lxst_call.phase; + } + if (!isSidebandSession(session)) + { + return {}; + } + return reticulum::lxst::call::dispatch(&session.lxst_call, event, now_ms); +} + +bool LxstTelephonyClient::encodeSignal(uint16_t signal, + uint8_t** out_payload, + std::size_t* out_len) +{ + if (!out_payload || !out_len) + { + return false; + } + *out_payload = scratch_; + *out_len = sizeof(scratch_); + return reticulum::lxst::encodeSignalling(signal, scratch_, out_len); +} + } // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_network_page_client.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_network_page_client.cpp index 7f0d0a66..6bceb483 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_network_page_client.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_network_page_client.cpp @@ -169,4 +169,72 @@ PendingNomadPageRequest* NetworkPageClient::findByRequestId( return nullptr; } +bool NetworkPageClient::attemptDue(const PendingNomadPageRequest& request, + uint32_t now_ms, + uint32_t retry_interval_ms) const +{ + return request.last_attempt_ms == 0 || + (now_ms - request.last_attempt_ms) >= retry_interval_ms; +} + +bool NetworkPageClient::pathRequestDue( + const PendingNomadPageRequest& request, + uint32_t now_ms, + uint32_t retry_interval_ms) const +{ + return request.last_path_request_ms == 0 || + (now_ms - request.last_path_request_ms) >= retry_interval_ms; +} + +uint32_t NetworkPageClient::lastAttemptAge( + const PendingNomadPageRequest& request, + uint32_t now_ms) const +{ + return request.last_attempt_ms != 0 ? (now_ms - request.last_attempt_ms) + : 0U; +} + +void NetworkPageClient::noteAttempt(PendingNomadPageRequest& request, + uint32_t now_ms) +{ + request.last_attempt_ms = now_ms; +} + +void NetworkPageClient::notePathRequest(PendingNomadPageRequest& request, + bool sent, + uint32_t now_ms) +{ + request.last_path_request_ms = now_ms; + if (sent) + { + request.path_requested = true; + } +} + +void NetworkPageClient::noteLinkStart(PendingNomadPageRequest& request, + bool sent, + uint32_t now_ms, + bool accumulate_success) +{ + request.last_attempt_ms = now_ms; + request.link_started = + accumulate_success ? (request.link_started || sent) : sent; +} + +bool NetworkPageClient::noteRequestPacketSent( + PendingNomadPageRequest& request, + const uint8_t* request_id, + std::size_t request_id_len, + uint32_t now_ms) +{ + if (!request_id || request_id_len != sizeof(request.request_id)) + { + return false; + } + copyHash(request.request_id, request_id, sizeof(request.request_id)); + request.last_attempt_ms = now_ms; + request.request_sent = true; + return true; +} + } // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_packet_router.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_packet_router.cpp index 44bd308e..235162ad 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_packet_router.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_packet_router.cpp @@ -5,6 +5,8 @@ #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_packet_router.h" +#include + namespace chat::lxmf::runtime { @@ -25,4 +27,52 @@ PacketRoute ReticulumPacketRouter::route(const reticulum::ParsedPacket& packet) } } +PacketForwardPlan ReticulumPacketRouter::planPathForward( + const PathEntry& path, + uint8_t packet_hops) const +{ + PacketForwardPlan plan{}; + plan.forward = true; + plan.interface_id = path.interface_id; + plan.hops = packet_hops; + if (path.hops <= 1 || path.direct) + { + plan.header = PacketForwardHeader::Header1Broadcast; + return plan; + } + + plan.header = PacketForwardHeader::Header2Transport; + std::memcpy(plan.next_hop_transport, + path.next_hop_transport, + sizeof(plan.next_hop_transport)); + return plan; +} + +PacketForwardPlan ReticulumPacketRouter::planLinkRelayForward( + const LinkRelayEntry& relay, + uint8_t ingress_interface_id, + uint8_t packet_hops) const +{ + const bool from_initiator = + packet_hops == relay.initiator_hops && + (ingress_interface_id == 0 || + ingress_interface_id == relay.initiator_interface_id); + const bool from_responder = + packet_hops == relay.responder_hops && + (ingress_interface_id == 0 || + ingress_interface_id == relay.responder_interface_id); + if (!from_initiator && !from_responder) + { + return {}; + } + + PacketForwardPlan plan{}; + plan.forward = true; + plan.header = PacketForwardHeader::Header1Broadcast; + plan.interface_id = from_initiator ? relay.responder_interface_id + : relay.initiator_interface_id; + plan.hops = packet_hops; + return plan; +} + } // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_path_manager.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_path_manager.cpp index 117d91c5..e99ddfd2 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_path_manager.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_path_manager.cpp @@ -101,6 +101,71 @@ void PathManager::resolvePendingPathRequest( runtime::resolvePendingPathRequest(transport_, destination_hash); } +bool PathManager::pendingPathRequestCoolingDown( + const uint8_t destination_hash[reticulum::kTruncatedHashSize], + uint32_t now_ms, + uint32_t retry_interval_ms) const +{ + const PendingPathRequest* pending = + runtime::findPendingPathRequest(transport_, destination_hash); + return pending && !pending->resolved && pending->last_attempt_ms != 0 && + (now_ms - pending->last_attempt_ms) < retry_interval_ms; +} + +bool PathManager::shouldRequestPeerPath( + const PeerInfo& peer, + uint32_t now_ms, + uint32_t now_s, + uint32_t pending_request_ttl_ms, + uint32_t min_request_interval_ms, + uint32_t path_ttl_ms, + uint32_t refresh_age_s) const +{ + if (pendingPathRequestCoolingDown(peer.destination_hash, + now_ms, + min_request_interval_ms)) + { + return false; + } + + const PendingPathRequest* pending = + runtime::findPendingPathRequest(transport_, peer.destination_hash); + if (pending && !pending->resolved && pending->created_ms != 0 && + (now_ms - pending->created_ms) < pending_request_ttl_ms) + { + return false; + } + + if (peer.last_path_request_ms != 0 && + (now_ms - peer.last_path_request_ms) < min_request_interval_ms) + { + return false; + } + + const PathEntry* path = findPath(peer.destination_hash, now_ms, path_ttl_ms); + if (!path || path->last_seen_s == 0) + { + return true; + } + + if (now_s < path->last_seen_s) + { + return true; + } + + return (now_s - path->last_seen_s) >= refresh_age_s; +} + +void PathManager::notePeerPathRequest(PeerInfo& peer, uint32_t now_ms) const +{ + peer.last_path_request_ms = now_ms; +} + +void PathManager::resetPeerPathRequest(PeerInfo& peer) const +{ + peer.last_path_request_ms = 0; +} + void PathManager::notePendingPingReceipt( const uint8_t packet_hash[reticulum::kFullHashSize], const uint8_t destination_hash[reticulum::kTruncatedHashSize], @@ -128,35 +193,6 @@ void PathManager::removePendingPingReceipt( runtime::removePendingPingReceipt(transport_, proof_hash); } -void PathManager::notePendingDeliveryReceipt( - const uint8_t packet_hash[reticulum::kFullHashSize], - const uint8_t destination_hash[reticulum::kTruncatedHashSize], - const uint8_t peer_sig_pub[LxmfIdentity::kSigPubKeySize], - MessageId message_id, - uint32_t now_ms, - std::size_t max_pending_delivery_receipts) -{ - runtime::notePendingDeliveryReceipt(transport_, - packet_hash, - destination_hash, - peer_sig_pub, - message_id, - now_ms, - max_pending_delivery_receipts); -} - -PendingDeliveryReceipt* PathManager::findPendingDeliveryReceipt( - const uint8_t proof_hash[reticulum::kTruncatedHashSize]) -{ - return runtime::findPendingDeliveryReceipt(transport_, proof_hash); -} - -void PathManager::removePendingDeliveryReceipt( - const uint8_t proof_hash[reticulum::kTruncatedHashSize]) -{ - runtime::removePendingDeliveryReceipt(transport_, proof_hash); -} - PathEntry& PathManager::upsertPath( const uint8_t destination_hash[reticulum::kTruncatedHashSize], std::size_t max_paths) @@ -164,6 +200,51 @@ PathEntry& PathManager::upsertPath( return runtime::upsertPath(transport_, destination_hash, max_paths); } +PathEntry* PathManager::observeAnnouncePath( + const uint8_t destination_hash[reticulum::kTruncatedHashSize], + uint8_t hops, + const uint8_t random_hash[10], + uint32_t now_ms, + uint32_t now_s, + uint8_t ingress_interface_id, + const uint8_t* next_hop_transport, + const uint8_t* raw_packet, + std::size_t raw_len, + std::size_t max_paths) +{ + if (!destination_hash || !random_hash) + { + return nullptr; + } + + PathEntry& path = runtime::upsertPath(transport_, destination_hash, max_paths); + runtime::applyPathAnnounce(path, hops, random_hash, now_ms, now_s); + path.interface_id = ingress_interface_id; + path.direct = next_hop_transport == nullptr; + runtime::resolvePendingPathRequest(transport_, destination_hash); + if (next_hop_transport) + { + std::memcpy(path.next_hop_transport, + next_hop_transport, + sizeof(path.next_hop_transport)); + } + else + { + std::memcpy(path.next_hop_transport, + destination_hash, + sizeof(path.next_hop_transport)); + } + if (raw_packet && raw_len <= sizeof(path.cached_announce)) + { + std::memcpy(path.cached_announce, raw_packet, raw_len); + path.cached_announce_len = raw_len; + reticulum::computePacketHash(raw_packet, + raw_len, + path.cached_packet_hash); + } + return &path; +} + const PathEntry* PathManager::findPath( const uint8_t destination_hash[reticulum::kTruncatedHashSize], uint32_t now_ms, diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_peer_directory.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_peer_directory.cpp new file mode 100644 index 00000000..6dbaf844 --- /dev/null +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_peer_directory.cpp @@ -0,0 +1,358 @@ +/** + * @file lxmf_peer_directory.cpp + * @brief Reticulum peer directory application service. + */ + +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_peer_directory.h" + +#include + +namespace chat::lxmf::runtime +{ +namespace +{ + +bool isZeroBytes(const uint8_t* data, std::size_t len) +{ + if (!data || len == 0) + { + return true; + } + for (std::size_t index = 0; index < len; ++index) + { + if (data[index] != 0) + { + return false; + } + } + return true; +} + +bool hasDirectoryKeys(const PeerInfo& peer) +{ + return !isZeroBytes(peer.destination_hash, sizeof(peer.destination_hash)) && + !isZeroBytes(peer.identity_hash, sizeof(peer.identity_hash)) && + !isZeroBytes(peer.enc_pub, sizeof(peer.enc_pub)) && + !isZeroBytes(peer.sig_pub, sizeof(peer.sig_pub)); +} + +bool hasUsableRatchet(const PeerInfo& peer) +{ + return peer.has_ratchet && + !isZeroBytes(peer.ratchet_pub, sizeof(peer.ratchet_pub)); +} + +MeshPeerRecord makeRecordForPeer(const PeerInfo& peer, + MeshPeerSource source, + uint32_t now_s) +{ + MeshPeerRecord record{}; + record.valid = true; + record.identity = makeMeshPeerReticulumIdentity(reticulumIdentityForPeer(peer)); + record.source = source; + record.first_seen_s = peer.last_seen_s != 0 ? peer.last_seen_s : now_s; + record.last_seen_s = peer.last_seen_s != 0 ? peer.last_seen_s : now_s; + copyMeshPeerText(record.display_name, + sizeof(record.display_name), + peer.display_name); + record.reticulum.identity = record.identity.reticulum; + record.reticulum.has_public_keys = true; + std::memcpy(record.reticulum.enc_pub, + peer.enc_pub, + sizeof(record.reticulum.enc_pub)); + std::memcpy(record.reticulum.sig_pub, + peer.sig_pub, + sizeof(record.reticulum.sig_pub)); + if (hasUsableRatchet(peer)) + { + record.reticulum.has_ratchet = true; + std::memcpy(record.reticulum.ratchet_pub, + peer.ratchet_pub, + sizeof(record.reticulum.ratchet_pub)); + record.reticulum.ratchet_seen_s = peer.ratchet_seen_s; + } + record.reticulum.delivery = true; + return record; +} + +} // namespace + +ReticulumPeerIdentity reticulumIdentityForPeer(const PeerInfo& peer) +{ + return makeReticulumPeerIdentity(peer.destination_hash, peer.identity_hash); +} + +PeerDirectoryService::PeerDirectoryService(IMeshPeerDirectory* directory) + : directory_(directory) +{ +} + +void PeerDirectoryService::setDirectory(IMeshPeerDirectory* directory) +{ + directory_ = directory; +} + +bool PeerDirectoryService::hasDirectory() const +{ + return directory_ != nullptr; +} + +PeerInfo* PeerDirectoryService::applyRecord(DestinationRegistry& registry, + const MeshPeerRecord& record, + uint32_t now_s) const +{ + if (!meshPeerRecordIsValid(record) || record.flags.ignored || + !meshPeerSameProtocol(record.identity.protocol, MeshProtocol::Reticulum) || + record.identity.kind != MeshPeerIdentityKind::ReticulumDestination || + !record.reticulum.has_public_keys) + { + return nullptr; + } + + const ReticulumPeerIdentity& identity = record.reticulum.identity.valid + ? record.reticulum.identity + : record.identity.reticulum; + if (!identity.valid || + isZeroBytes(identity.destination_hash, sizeof(identity.destination_hash)) || + isZeroBytes(identity.identity_hash, sizeof(identity.identity_hash)) || + isZeroBytes(record.reticulum.enc_pub, sizeof(record.reticulum.enc_pub)) || + isZeroBytes(record.reticulum.sig_pub, sizeof(record.reticulum.sig_pub))) + { + return nullptr; + } + + PeerInfo& peer = registry.upsertDestination(identity.destination_hash); + std::memcpy(peer.identity_hash, identity.identity_hash, sizeof(peer.identity_hash)); + std::memcpy(peer.enc_pub, record.reticulum.enc_pub, sizeof(peer.enc_pub)); + std::memcpy(peer.sig_pub, record.reticulum.sig_pub, sizeof(peer.sig_pub)); + if (record.reticulum.has_ratchet && + !isZeroBytes(record.reticulum.ratchet_pub, + sizeof(record.reticulum.ratchet_pub))) + { + std::memcpy(peer.ratchet_pub, + record.reticulum.ratchet_pub, + sizeof(peer.ratchet_pub)); + peer.has_ratchet = true; + peer.ratchet_seen_s = record.reticulum.ratchet_seen_s; + } + else + { + std::memset(peer.ratchet_pub, 0, sizeof(peer.ratchet_pub)); + peer.has_ratchet = false; + peer.ratchet_seen_s = 0; + } + peer.last_seen_s = record.last_seen_s != 0 ? record.last_seen_s : now_s; + peer.last_path_request_ms = 0; + copyMeshPeerText(peer.display_name, + sizeof(peer.display_name), + record.display_name); + return &peer; +} + +PeerDirectoryLoadResult PeerDirectoryService::findOrLoadByNodeId( + DestinationRegistry& registry, + NodeId node_id, + uint32_t now_s) const +{ + PeerDirectoryLoadResult result{}; + if (node_id == 0) + { + result.status = + MeshPeerDirectoryStatus::fail(MeshPeerDirectoryStatusCode::InvalidArgument); + return result; + } + + if (PeerInfo* peer = registry.findByNodeId(node_id)) + { + result.peer = peer; + return result; + } + + if (!directory_) + { + result.status = + MeshPeerDirectoryStatus::fail(MeshPeerDirectoryStatusCode::StorageUnavailable); + return result; + } + + MeshPeerRecord record{}; + result.status = + directory_->findByNodeId(MeshProtocol::Reticulum, node_id, record); + if (!result.status.succeeded()) + { + return result; + } + + result.peer = applyRecord(registry, record, now_s); + result.loaded_from_directory = result.peer != nullptr; + if (!result.peer) + { + result.status = + MeshPeerDirectoryStatus::fail(MeshPeerDirectoryStatusCode::InvalidArgument); + } + return result; +} + +PeerDirectoryLoadResult PeerDirectoryService::findOrLoadByDestinationHash( + DestinationRegistry& registry, + const uint8_t destination_hash[reticulum::kTruncatedHashSize], + uint32_t now_s) const +{ + PeerDirectoryLoadResult result{}; + if (!destination_hash) + { + result.status = + MeshPeerDirectoryStatus::fail(MeshPeerDirectoryStatusCode::InvalidArgument); + return result; + } + + if (PeerInfo* peer = registry.findByDestinationHash(destination_hash)) + { + result.peer = peer; + return result; + } + + if (!directory_) + { + result.status = + MeshPeerDirectoryStatus::fail(MeshPeerDirectoryStatusCode::StorageUnavailable); + return result; + } + + const MeshPeerIdentity identity = + makeMeshPeerReticulumIdentity(makeReticulumDestinationIdentity(destination_hash)); + MeshPeerRecord record{}; + result.status = directory_->find(identity, record); + if (!result.status.succeeded()) + { + return result; + } + + result.peer = applyRecord(registry, record, now_s); + result.loaded_from_directory = result.peer != nullptr; + if (!result.peer) + { + result.status = + MeshPeerDirectoryStatus::fail(MeshPeerDirectoryStatusCode::InvalidArgument); + } + return result; +} + +MeshActionResult PeerDirectoryService::persistPeerAddressNow( + const PeerInfo& peer, + bool favorite, + uint32_t now_s) const +{ + if (!hasDirectoryKeys(peer)) + { + return MeshActionResult::fail(MeshOperationFailure::PeerKeyMissing); + } + if (!directory_) + { + return MeshActionResult::fail(MeshOperationFailure::NotReady); + } + + const PeerDirectoryWriteResult write = + recordPeer(peer, MeshPeerSource::Manual, true, favorite, now_s); + if (!write.succeeded()) + { + return MeshActionResult::fail(MeshOperationFailure::Unknown); + } + return MeshActionResult::success(); +} + +PeerDirectoryWriteResult PeerDirectoryService::recordPeer(const PeerInfo& peer, + MeshPeerSource source, + bool update_favorite, + bool favorite, + uint32_t now_s) const +{ + PeerDirectoryWriteResult result{}; + if (!directory_) + { + result.record_status = + MeshPeerDirectoryStatus::fail(MeshPeerDirectoryStatusCode::StorageUnavailable); + return result; + } + if (!hasDirectoryKeys(peer)) + { + result.record_status = + MeshPeerDirectoryStatus::fail(MeshPeerDirectoryStatusCode::InvalidArgument); + return result; + } + + MeshPeerRecord record = makeRecordForPeer(peer, source, now_s); + + MeshPeerUserFlags flags{}; + if (update_favorite) + { + MeshPeerRecord existing{}; + if (directory_->find(record.identity, existing).succeeded()) + { + flags = existing.flags; + } + flags.favorite = favorite; + } + + result.record_status = directory_->record(record); + if (!result.record_status.succeeded()) + { + return result; + } + + if (update_favorite) + { + result.flags_attempted = true; + result.flags_status = directory_->setUserFlags(record.identity, flags); + } + return result; +} + +PeerDirectoryLoadRecentResult PeerDirectoryService::loadRecent( + DestinationRegistry& registry, + MeshPeerRecord* scratch, + std::size_t scratch_count, + NodeId* out_loaded_nodes, + std::size_t max_loaded_nodes, + uint32_t now_s) const +{ + PeerDirectoryLoadRecentResult result{}; + if (!directory_) + { + result.status = + MeshPeerDirectoryStatus::fail(MeshPeerDirectoryStatusCode::StorageUnavailable); + return result; + } + if (!scratch || scratch_count == 0) + { + result.status = + MeshPeerDirectoryStatus::fail(MeshPeerDirectoryStatusCode::InvalidArgument); + return result; + } + + std::size_t count = 0; + result.status = + directory_->loadRecent(MeshProtocol::Reticulum, scratch, scratch_count, &count); + if (!result.status.succeeded()) + { + return result; + } + + result.scanned = count; + for (std::size_t index = 0; index < count; ++index) + { + PeerInfo* peer = applyRecord(registry, scratch[index], now_s); + if (!peer) + { + continue; + } + if (out_loaded_nodes && result.loaded < max_loaded_nodes) + { + out_loaded_nodes[result.loaded] = peer->node_id; + } + ++result.loaded; + } + return result; +} + +} // namespace chat::lxmf::runtime diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_propagation_client.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_propagation_client.cpp index 62f9af44..4eabbe9e 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_propagation_client.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_propagation_client.cpp @@ -133,6 +133,84 @@ bool PropagationClient::removeFirstPendingUpload() return true; } +void PropagationClient::markUploadWaitingForNode( + PendingPropagationUpload& upload) +{ + upload.state = PropagationUploadState::WaitingNode; +#if !defined(TRAIL_MATE_RETICULUM_PARSE_ONLY) + stamp_.reset(); +#endif +} + +bool PropagationClient::bindUploadNode(PendingPropagationUpload& upload, + const PropagationPeerState& node) +{ + const bool node_changed = + !bytesEqual(upload.node_hash, + node.propagation_hash, + sizeof(upload.node_hash)) || + upload.stamp_cost != node.stamp_cost; + if (node_changed) + { +#if !defined(TRAIL_MATE_RETICULUM_PARSE_ONLY) + stamp_.reset(); +#endif + copyBytes(upload.node_hash, + node.propagation_hash, + sizeof(upload.node_hash)); + upload.stamp_cost = node.stamp_cost; + } + if (node_changed || upload.state == PropagationUploadState::WaitingNode) + { + upload.state = PropagationUploadState::NeedsStamp; + } + return node_changed; +} + +bool PropagationClient::beginUploadStamp(PendingPropagationUpload& upload) +{ + if (upload.state != PropagationUploadState::NeedsStamp) + { + return false; + } +#if !defined(TRAIL_MATE_RETICULUM_PARSE_ONLY) + if (stamp_.begin(upload.transient_id, upload.stamp_cost)) + { + upload.state = PropagationUploadState::Stamping; + return true; + } +#endif + upload.state = PropagationUploadState::Failed; + return false; +} + +bool PropagationClient::completeUploadStamp( + PendingPropagationUpload& upload, + const uint8_t stamp[reticulum::kFullHashSize]) +{ + if (!stamp || upload.state != PropagationUploadState::Stamping) + { + upload.state = PropagationUploadState::Failed; + return false; + } + upload.transient_data.insert(upload.transient_data.end(), + stamp, + stamp + reticulum::kFullHashSize); + upload.state = PropagationUploadState::Ready; + return true; +} + +void PropagationClient::markUploadFailed(PendingPropagationUpload& upload) +{ + upload.state = PropagationUploadState::Failed; +} + +void PropagationClient::markUploadQueuedToLink( + PendingPropagationUpload& upload) +{ + upload.state = PropagationUploadState::QueuedToLink; +} + void PropagationClient::markExpiredUploads(uint32_t now_ms, uint32_t ttl_ms) { for (auto& upload : state_.pending_uploads) @@ -145,9 +223,9 @@ void PropagationClient::markExpiredUploads(uint32_t now_ms, uint32_t ttl_ms) } } -std::vector PropagationClient::takeFailedUploads() +PendingPropagationUploadList PropagationClient::takeFailedUploads() { - std::vector failed; + PendingPropagationUploadList failed; auto& uploads = state_.pending_uploads; for (auto it = uploads.begin(); it != uploads.end();) { @@ -162,9 +240,9 @@ std::vector PropagationClient::takeFailedUploads() return failed; } -std::vector PropagationClient::takeAllPendingUploads() +PendingPropagationUploadList PropagationClient::takeAllPendingUploads() { - std::vector uploads; + PendingPropagationUploadList uploads; uploads.swap(state_.pending_uploads); return uploads; } diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_resource_runtime.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_resource_runtime.cpp index b71799f1..745fb0c3 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_resource_runtime.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_resource_runtime.cpp @@ -74,7 +74,7 @@ bool resourceIsComplete(const LinkResourceTransfer& resource) } // namespace LinkResourceTransfer* findLinkResource( - std::vector& resources, + LinkResourceTransferList& resources, const uint8_t resource_hash[reticulum::kFullHashSize]) { if (!resource_hash) @@ -93,7 +93,7 @@ LinkResourceTransfer* findLinkResource( } const LinkResourceTransfer* findLinkResource( - const std::vector& resources, + const LinkResourceTransferList& resources, const uint8_t resource_hash[reticulum::kFullHashSize]) { if (!resource_hash) @@ -111,7 +111,7 @@ const LinkResourceTransfer* findLinkResource( return nullptr; } -bool eraseLinkResourceByHash(std::vector& resources, +bool eraseLinkResourceByHash(LinkResourceTransferList& resources, const uint8_t resource_hash[reticulum::kFullHashSize]) { if (!resource_hash) @@ -518,7 +518,7 @@ void cullLinkResources(LinkSession& session, uint32_t now_ms, const ResourceRuntimeLimits& limits) { - auto cull_resources = [now_ms, &limits](std::vector& resources) + auto cull_resources = [now_ms, &limits](LinkResourceTransferList& resources) { resources.erase( std::remove_if(resources.begin(), diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_transport_runtime.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_transport_runtime.cpp index b6de11f8..f85709c6 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_transport_runtime.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_transport_runtime.cpp @@ -431,86 +431,6 @@ void removePendingPingReceipt( transport.pending_ping_receipts.end()); } -void notePendingDeliveryReceipt( - TransportRuntime& transport, - const uint8_t packet_hash[reticulum::kFullHashSize], - const uint8_t destination_hash[reticulum::kTruncatedHashSize], - const uint8_t peer_sig_pub[LxmfIdentity::kSigPubKeySize], - MessageId message_id, - uint32_t now_ms, - std::size_t max_pending_delivery_receipts) -{ - if (!packet_hash || !destination_hash || !peer_sig_pub || message_id == 0) - { - return; - } - - uint8_t proof_hash[reticulum::kTruncatedHashSize] = {}; - copyHash(proof_hash, packet_hash, sizeof(proof_hash)); - removePendingDeliveryReceipt(transport, proof_hash); - if (max_pending_delivery_receipts != 0 && - transport.pending_delivery_receipts.size() >= - max_pending_delivery_receipts) - { - transport.pending_delivery_receipts.erase( - transport.pending_delivery_receipts.begin()); - } - - transport.pending_delivery_receipts.push_back(PendingDeliveryReceipt{}); - PendingDeliveryReceipt& receipt = - transport.pending_delivery_receipts.back(); - copyHash(receipt.packet_hash, packet_hash, sizeof(receipt.packet_hash)); - copyHash(receipt.proof_hash, proof_hash, sizeof(receipt.proof_hash)); - copyHash(receipt.destination_hash, - destination_hash, - sizeof(receipt.destination_hash)); - copyHash(receipt.peer_sig_pub, peer_sig_pub, sizeof(receipt.peer_sig_pub)); - receipt.message_id = message_id; - receipt.created_ms = now_ms == 0 ? 1U : now_ms; -} - -PendingDeliveryReceipt* findPendingDeliveryReceipt( - TransportRuntime& transport, - const uint8_t proof_hash[reticulum::kTruncatedHashSize]) -{ - if (!proof_hash) - { - return nullptr; - } - for (auto& receipt : transport.pending_delivery_receipts) - { - if (receipt.created_ms != 0 && - hashesEqual(receipt.proof_hash, - proof_hash, - sizeof(receipt.proof_hash))) - { - return &receipt; - } - } - return nullptr; -} - -void removePendingDeliveryReceipt( - TransportRuntime& transport, - const uint8_t proof_hash[reticulum::kTruncatedHashSize]) -{ - if (!proof_hash) - { - return; - } - transport.pending_delivery_receipts.erase( - std::remove_if( - transport.pending_delivery_receipts.begin(), - transport.pending_delivery_receipts.end(), - [proof_hash](const PendingDeliveryReceipt& receipt) - { - return hashesEqual(receipt.proof_hash, - proof_hash, - sizeof(receipt.proof_hash)); - }), - transport.pending_delivery_receipts.end()); -} - PathEntry& upsertPath(TransportRuntime& transport, const uint8_t destination_hash[reticulum::kTruncatedHashSize], std::size_t max_paths) @@ -674,32 +594,6 @@ void cullTransportRuntime(TransportRuntime& transport, transport.pending_ping_receipts.begin(), transport.pending_ping_receipts.begin() + excess); } - - if (limits.pending_delivery_receipt_ttl_ms != 0) - { - transport.pending_delivery_receipts.erase( - std::remove_if( - transport.pending_delivery_receipts.begin(), - transport.pending_delivery_receipts.end(), - [now_ms, &limits](const PendingDeliveryReceipt& receipt) - { - return receipt.created_ms == 0 || - (now_ms - receipt.created_ms) > - limits.pending_delivery_receipt_ttl_ms; - }), - transport.pending_delivery_receipts.end()); - } - if (limits.max_pending_delivery_receipts != 0 && - transport.pending_delivery_receipts.size() > - limits.max_pending_delivery_receipts) - { - const std::size_t excess = - transport.pending_delivery_receipts.size() - - limits.max_pending_delivery_receipts; - transport.pending_delivery_receipts.erase( - transport.pending_delivery_receipts.begin(), - transport.pending_delivery_receipts.begin() + excess); - } } } // namespace chat::lxmf::runtime 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 84b9870f..3d5e342d 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 @@ -433,7 +433,11 @@ class PlainMqttRuntime { return; } - sys::EventBus::publish(new sys::ChatSendResultEvent(msg_id, true), 0); + sys::EventBus::publish( + new sys::ChatSendResultEvent(msg_id, + chat::MessageStatus::Sent, + chat::MeshProtocol::Meshtastic), + 0); std::printf("[MT][MQTT] publish ack msg=%08lX\n", static_cast(msg_id)); } diff --git a/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp b/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp index cb0e67e8..b57ea66e 100644 --- a/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp +++ b/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp @@ -2618,7 +2618,10 @@ bool MeshCoreAdapter::executeProtocolEffect(const runtime::ProtocolEffect& effec if (item.message_id != 0) { sys::EventBus::publish( - new sys::ChatSendResultEvent(item.message_id, true), + new sys::ChatSendResultEvent( + item.message_id, + chat::MessageStatus::Delivered, + chat::MeshProtocol::MeshCore), 0); } } @@ -2635,7 +2638,11 @@ bool MeshCoreAdapter::executeProtocolEffect(const runtime::ProtocolEffect& effec if (item.message_id != 0) { sys::EventBus::publish( - new sys::ChatSendResultEvent(item.message_id, false), + new sys::ChatSendResultEvent( + item.message_id, + chat::MessageStatus::Failed, + chat::MeshProtocol::MeshCore, + chat::delivery::SendFailureKind::AckTimeout), 0); } } @@ -3403,7 +3410,11 @@ MeshSendResult MeshCoreAdapter::sendTextDetailed(ChannelId channel, const std::s } else { - sys::EventBus::publish(new sys::ChatSendResultEvent(msg_id, true), 0); + sys::EventBus::publish( + new sys::ChatSendResultEvent(msg_id, + chat::MessageStatus::Sent, + chat::MeshProtocol::MeshCore), + 0); } return MeshSendResult::success(msg_id); } @@ -3489,7 +3500,11 @@ MeshSendResult MeshCoreAdapter::sendTextDetailed(ChannelId channel, const std::s } const MessageId msg_id = (forced_msg_id != 0) ? forced_msg_id : next_msg_id_++; - sys::EventBus::publish(new sys::ChatSendResultEvent(msg_id, true), 0); + sys::EventBus::publish( + new sys::ChatSendResultEvent(msg_id, + chat::MessageStatus::Sent, + chat::MeshProtocol::MeshCore), + 0); return MeshSendResult::success(msg_id); } 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 48e1b352..fd6dbfa5 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 @@ -122,6 +122,38 @@ bool shouldRequireDirectPki(uint8_t encrypt_mode, uint32_t dest_node, uint32_t p allowPkiForPortnum(portnum); } +chat::delivery::SendFailureKind failureKindFromRoutingError( + meshtastic_Routing_Error reason) +{ + switch (reason) + { + case meshtastic_Routing_Error_NONE: + return chat::delivery::SendFailureKind::None; + case meshtastic_Routing_Error_TIMEOUT: + case meshtastic_Routing_Error_MAX_RETRANSMIT: + case meshtastic_Routing_Error_NO_RESPONSE: + case meshtastic_Routing_Error_NO_ROUTE: + return chat::delivery::SendFailureKind::AckTimeout; + case meshtastic_Routing_Error_NO_INTERFACE: + case meshtastic_Routing_Error_DUTY_CYCLE_LIMIT: + case meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED: + return chat::delivery::SendFailureKind::RadioSendFailed; + case meshtastic_Routing_Error_NO_CHANNEL: + return chat::delivery::SendFailureKind::ChannelKeyMissing; + case meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY: + case meshtastic_Routing_Error_PKI_FAILED: + return chat::delivery::SendFailureKind::PeerKeyMissing; + case meshtastic_Routing_Error_GOT_NAK: + case meshtastic_Routing_Error_BAD_REQUEST: + case meshtastic_Routing_Error_NOT_AUTHORIZED: + case meshtastic_Routing_Error_ADMIN_BAD_SESSION_KEY: + case meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED: + case meshtastic_Routing_Error_TOO_LARGE: + return chat::delivery::SendFailureKind::Rejected; + } + return chat::delivery::SendFailureKind::Unknown; +} + int16_t coreRadioRssi(float rssi) { if (!std::isfinite(rssi)) @@ -1398,7 +1430,7 @@ bool MtAdapter::queueMqttProxyPublishFromWire(const uint8_t* wire_data, return false; } - if (mqtt_proxy_settings_.encryption_enabled) + if (mqtt_proxy_settings_.encryption_enabled || is_pki) { std::memset(&scratch.packet, 0, sizeof(scratch.packet)); if (!makeEncryptedPacketFromWire(wire_data, wire_size, &scratch.packet)) @@ -2155,10 +2187,13 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) if (decoded.request_id != 0 && header.to == node_id_) { bool ok = true; + meshtastic_Routing_Error routing_reason = + meshtastic_Routing_Error_NONE; if (routing.which_variant == meshtastic_Routing_error_reason_tag && routing.error_reason != meshtastic_Routing_Error_NONE) { ok = false; + routing_reason = routing.error_reason; } if (routing.which_variant == meshtastic_Routing_error_reason_tag && (routing.error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY || @@ -2190,7 +2225,13 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) (unsigned long)decoded.request_id, ok ? 1 : 0); sys::EventBus::publish( - new sys::ChatSendResultEvent(decoded.request_id, ok), 0); + new sys::ChatSendResultEvent( + decoded.request_id, + ok ? chat::MessageStatus::Delivered + : chat::MessageStatus::Failed, + chat::MeshProtocol::Meshtastic, + failureKindFromRoutingError(routing_reason)), + 0); } } else @@ -2267,7 +2308,10 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) ChannelId channel_id = decoded_channel_id; if (header.channel != 0 && header.from != node_id_) { - rememberNodeLastChannel(header.from, channel_id, millis()); + rememberNodeRuntimeRx(header.from, + channel_id, + rx_meta.origin == chat::RxOrigin::External, + millis()); } if (node_metadata_decoded) { @@ -2646,7 +2690,9 @@ bool MtAdapter::sendPacket(const PendingSend& pending) (channel == ChannelId::SECONDARY) ? secondary_channel_hash_ : primary_channel_hash_; uint8_t hop_limit = config_.hop_limit; uint32_t dest = (pending.dest != 0) ? pending.dest : kBroadcastNodeId; - bool track_ack = true; + const bool dest_last_seen_via_mqtt = + dest != kBroadcastNodeId && nodeLastSeenViaMqtt(dest); + bool track_ack = !dest_last_seen_via_mqtt; bool air_want_ack = shouldSetAirWantAck(dest, track_ack); // Upstream Meshtastic requires PKI for direct unicast traffic on @@ -2686,8 +2732,8 @@ bool MtAdapter::sendPacket(const PendingSend& pending) payload = pki_buffer.data(); payload_len = pki_len; channel_hash = 0; // PKI channel - track_ack = true; - air_want_ack = true; + track_ack = !dest_last_seen_via_mqtt; + air_want_ack = shouldSetAirWantAck(dest, track_ack); use_pki = true; } @@ -2735,12 +2781,27 @@ bool MtAdapter::sendPacket(const PendingSend& pending) (unsigned)psk_len, (unsigned)wire_size, (unsigned long)dest); - if (!board_.isRadioOnline()) + bool tx_ok = false; + if (board_.isRadioOnline()) + { + tx_ok = transmitWirePacket(wire_buffer.data(), wire_size); + } + else if (!dest_last_seen_via_mqtt) { return false; } - - bool ok = transmitWirePacket(wire_buffer.data(), wire_size); + bool mqtt_ok = false; + if (tx_ok || dest_last_seen_via_mqtt) + { + mqtt_ok = queueMqttProxyPublishFromWire(wire_buffer.data(), + wire_size, + use_pki + ? nullptr + : (decoded_ok ? &decoded + : nullptr), + channel); + } + const bool ok = dest_last_seen_via_mqtt ? (tx_ok || mqtt_ok) : tx_ok; LORA_LOG("[LORA] TX text id=%08lX ch=%u len=%u ok=%d\n", (unsigned long)pending.msg_id, static_cast(channel), @@ -2750,11 +2811,13 @@ bool MtAdapter::sendPacket(const PendingSend& pending) { trackPendingAck(pending.msg_id, dest, channel, channel_hash, wire_buffer.data(), wire_size); } - if (ok) + else if (ok) { - queueMqttProxyPublishFromWire(wire_buffer.data(), wire_size, - decoded_ok ? &decoded : nullptr, - channel); + sys::EventBus::publish( + new sys::ChatSendResultEvent(pending.msg_id, + chat::MessageStatus::Sent, + chat::MeshProtocol::Meshtastic), + 0); } return ok; } @@ -3705,6 +3768,25 @@ void MtAdapter::rememberNodeLastChannel(uint32_t node_id, ChannelId channel, uin } } +void MtAdapter::rememberNodeRuntimeRx(uint32_t node_id, + ChannelId channel, + bool via_mqtt, + uint32_t now_ms) +{ + if (auto* entry = upsertNodeRuntime(node_id, now_ms)) + { + entry->last_channel = channel; + entry->has_last_channel = true; + entry->last_seen_via_mqtt = via_mqtt; + } +} + +bool MtAdapter::nodeLastSeenViaMqtt(uint32_t node_id) const +{ + const auto* entry = findNodeRuntime(node_id); + return entry && entry->last_seen_via_mqtt; +} + uint32_t MtAdapter::getNodeInfoReplyMs(uint32_t node_id) const { const auto* entry = findNodeRuntime(node_id); @@ -4699,8 +4781,18 @@ void MtAdapter::emitRoutingResultToPhone(uint32_t request_id, static_cast(app_receive_queue_.size())); } + const bool own_self_echo = from == node_id_ && to == node_id_; + const chat::MessageStatus status = + reason != meshtastic_Routing_Error_NONE + ? chat::MessageStatus::Failed + : (own_self_echo ? chat::MessageStatus::Sent + : chat::MessageStatus::Delivered); sys::EventBus::publish( - new sys::ChatSendResultEvent(request_id, reason == meshtastic_Routing_Error_NONE), 0); + new sys::ChatSendResultEvent(request_id, + status, + chat::MeshProtocol::Meshtastic, + failureKindFromRoutingError(reason)), + 0); } } // namespace meshtastic diff --git a/platform/esp/arduino_common/src/chat/infra/store/sd_store.cpp b/platform/esp/arduino_common/src/chat/infra/store/sd_store.cpp index 51345879..88fed97e 100644 --- a/platform/esp/arduino_common/src/chat/infra/store/sd_store.cpp +++ b/platform/esp/arduino_common/src/chat/infra/store/sd_store.cpp @@ -587,6 +587,93 @@ bool SdStore::updateMessageStatus(MessageId msg_id, MessageStatus status) return updated; } +bool SdStore::updateMessageStatusForProtocol(MessageId msg_id, + MeshProtocol protocol, + MessageStatus status) +{ + if (!ready_ || msg_id == 0) + { + return false; + } + + std::vector entries; + if (!readIndex(entries)) + { + return false; + } + + bool updated = false; + for (auto& entry : entries) + { + if (static_cast(entry.protocol) != protocol) + { + continue; + } + const ConversationId conv = conversationFromIndexEntry(entry); + char path[96]{}; + buildConversationPath(conv, path, sizeof(path)); + if (!storage::sd_exists(path)) + { + continue; + } + + storage::SdRuntimeFile file; + if (!file.open(path, "r+")) + { + continue; + } + + FileHeader header{}; + if (!loadFileHeader(file, header) || + !upgradeConversationFile(file, path, header)) + { + file.close(); + continue; + } + + for (uint16_t index = 0; index < header.count; ++index) + { + const uint16_t slot = + static_cast((header.head + kMaxMessagesPerConv - header.count + index) % + kMaxMessagesPerConv); + Record rec{}; + if (!readRecord(file, header, slot, rec)) + { + continue; + } + if (rec.msg_id != msg_id || rec.from != 0 || + static_cast(rec.protocol) != protocol) + { + continue; + } + + rec.status = static_cast(status); + updated = writeRecord(file, slot, rec); + if (updated) + { + file.flush(); + if (entry.last_msg_id == msg_id) + { + entry.status = static_cast(status); + } + } + break; + } + + file.close(); + if (updated) + { + break; + } + } + + if (updated) + { + (void)writeIndex(entries); + } + return updated; +} + bool SdStore::getMessage(MessageId msg_id, ChatMessage* out) const { if (!ready_ || msg_id == 0) @@ -645,6 +732,72 @@ bool SdStore::getMessage(MessageId msg_id, ChatMessage* out) const return false; } +bool SdStore::getMessageForProtocol(MessageId msg_id, + MeshProtocol protocol, + ChatMessage* out) const +{ + if (!ready_ || msg_id == 0) + { + return false; + } + + std::vector entries; + if (!readIndex(entries)) + { + return false; + } + + for (const auto& entry : entries) + { + if (static_cast(entry.protocol) != protocol) + { + continue; + } + const ConversationId conv = conversationFromIndexEntry(entry); + char path[96]{}; + buildConversationPath(conv, path, sizeof(path)); + if (!storage::sd_exists(path)) + { + continue; + } + + storage::SdRuntimeFile file; + if (!file.open(path, "r")) + { + continue; + } + + FileHeader header{}; + if (!loadFileHeader(file, header)) + { + file.close(); + continue; + } + + for (uint16_t index = 0; index < header.count; ++index) + { + const uint16_t slot = + static_cast((header.head + kMaxMessagesPerConv - header.count + index) % + kMaxMessagesPerConv); + Record rec{}; + if (!readRecord(file, header, slot, rec) || rec.text_len == 0 || + rec.msg_id != msg_id || + static_cast(rec.protocol) != protocol) + { + continue; + } + if (out) + { + *out = messageFromRecord(rec); + } + file.close(); + return true; + } + file.close(); + } + return false; +} + bool SdStore::hasReticulumLxmfMessageHash(const uint8_t* lxmf_hash) const { if (!ready_ || !validLxmfMessageHash(lxmf_hash)) diff --git a/platform/esp/radio/meshtastic_radio_adapter.cpp b/platform/esp/radio/meshtastic_radio_adapter.cpp index 48ce793b..84f7396c 100644 --- a/platform/esp/radio/meshtastic_radio_adapter.cpp +++ b/platform/esp/radio/meshtastic_radio_adapter.cpp @@ -300,12 +300,22 @@ bool MeshtasticRadioAdapter::sendEncodedPayload(chat::ChannelId channel, (void)radio_pump_.restartReceive(); if (publish_send_result) { - sys::EventBus::publish(new sys::ChatSendResultEvent(msg_id, true), 0); + sys::EventBus::publish( + new sys::ChatSendResultEvent(msg_id, + chat::MessageStatus::Sent, + chat::MeshProtocol::Meshtastic), + 0); } } else if (publish_send_result) { - sys::EventBus::publish(new sys::ChatSendResultEvent(msg_id, false), 0); + sys::EventBus::publish( + new sys::ChatSendResultEvent( + msg_id, + chat::MessageStatus::Failed, + chat::MeshProtocol::Meshtastic, + chat::delivery::SendFailureKind::RadioSendFailed), + 0); } ESP_LOGI(kTag, diff --git a/platform/linux/common/include/chat/linux_sqlite_chat_store.h b/platform/linux/common/include/chat/linux_sqlite_chat_store.h index ef76d8ad..8795c67c 100644 --- a/platform/linux/common/include/chat/linux_sqlite_chat_store.h +++ b/platform/linux/common/include/chat/linux_sqlite_chat_store.h @@ -33,8 +33,14 @@ class LinuxSqliteChatStore final : public ::chat::IChatStore void clearAll() override; bool updateMessageStatus(::chat::MessageId msg_id, ::chat::MessageStatus status) override; + bool updateMessageStatusForProtocol(::chat::MessageId msg_id, + ::chat::MeshProtocol protocol, + ::chat::MessageStatus status) override; bool getMessage(::chat::MessageId msg_id, ::chat::ChatMessage* out) const override; + bool getMessageForProtocol(::chat::MessageId msg_id, + ::chat::MeshProtocol protocol, + ::chat::ChatMessage* out) const override; void flush() override; private: diff --git a/platform/linux/common/include/sys/event_bus.h b/platform/linux/common/include/sys/event_bus.h index 9fd6a891..d2e320da 100644 --- a/platform/linux/common/include/sys/event_bus.h +++ b/platform/linux/common/include/sys/event_bus.h @@ -7,6 +7,7 @@ #include #include +#include "chat/delivery/chat_delivery_types.h" #include "chat/domain/chat_types.h" #include "chat/domain/contact_types.h" #include "sys/clock.h" @@ -84,15 +85,24 @@ struct ChatSendResultEvent : public Event uint32_t msg_id; bool success; chat::MessageStatus status; + chat::delivery::SendFailureKind failure = + chat::delivery::SendFailureKind::None; + bool has_protocol = false; + chat::MeshProtocol protocol = chat::MeshProtocol::Meshtastic; - ChatSendResultEvent(uint32_t id, bool ok) - : Event(EventType::ChatSendResult), msg_id(id), success(ok), - status(ok ? chat::MessageStatus::Sent : chat::MessageStatus::Failed) {} - - ChatSendResultEvent(uint32_t id, chat::MessageStatus result_status) + ChatSendResultEvent(uint32_t id, + chat::MessageStatus result_status, + chat::MeshProtocol source_protocol, + chat::delivery::SendFailureKind failure_kind = + chat::delivery::SendFailureKind::Unknown) : Event(EventType::ChatSendResult), msg_id(id), success(result_status != chat::MessageStatus::Failed), - status(result_status) {} + status(result_status), + failure(result_status == chat::MessageStatus::Failed + ? failure_kind + : chat::delivery::SendFailureKind::None), + has_protocol(true), + protocol(source_protocol) {} }; struct ChatUnreadChangedEvent : public Event diff --git a/platform/linux/common/src/app/linux_app_services.cpp b/platform/linux/common/src/app/linux_app_services.cpp index 1a5c8664..90afc74e 100644 --- a/platform/linux/common/src/app/linux_app_services.cpp +++ b/platform/linux/common/src/app/linux_app_services.cpp @@ -2173,8 +2173,21 @@ void LinuxAppServices::dispatchPendingEvents(std::size_t max_events) { break; } - impl_->chat_service.handleSendResult(msg_id, ok); - ::sys::EventBus::publish(new ::sys::ChatSendResultEvent(msg_id, ok), 0); + const auto protocol = impl_->chat_service.getActiveProtocol(); + const auto status = ok ? ::chat::MessageStatus::Sent + : ::chat::MessageStatus::Failed; + const auto failure = + ok ? ::chat::delivery::SendFailureKind::None + : ::chat::delivery::SendFailureKind::RadioSendFailed; + impl_->chat_service.handleSendResultForProtocol( + msg_id, + protocol, + status, + 0, + failure); + ::sys::EventBus::publish( + new ::sys::ChatSendResultEvent(msg_id, status, protocol, failure), + 0); } std::size_t processed = 0; diff --git a/platform/linux/common/src/chat/linux_sqlite_chat_store.cpp b/platform/linux/common/src/chat/linux_sqlite_chat_store.cpp index f838386b..1824f47c 100644 --- a/platform/linux/common/src/chat/linux_sqlite_chat_store.cpp +++ b/platform/linux/common/src/chat/linux_sqlite_chat_store.cpp @@ -1097,6 +1097,47 @@ bool LinuxSqliteChatStore::updateMessageStatus( return ok; } +bool LinuxSqliteChatStore::updateMessageStatusForProtocol( + ::chat::MessageId msg_id, + ::chat::MeshProtocol protocol, + ::chat::MessageStatus status) +{ + if (msg_id == 0) + { + return false; + } + + std::lock_guard lock(mutex_); + DatabaseHandle handle; + if (!handle) + { + return false; + } + + sqlite3_stmt* stmt = nullptr; + constexpr const char* kSql = + "UPDATE chat_messages SET status=?1 " + "WHERE sequence=(" + "SELECT sequence FROM chat_messages " + "WHERE msg_id=?2 AND protocol=?3 AND from_node=0 " + "ORDER BY sequence DESC LIMIT 1" + ");"; + bool ok = false; + if (sqlite3_prepare_v2(handle.db, kSql, -1, &stmt, nullptr) == SQLITE_OK) + { + ok = sqlite3_bind_int(stmt, 1, statusValue(status)) == SQLITE_OK && + sqlite3_bind_int64(stmt, + 2, + static_cast(msg_id)) == + SQLITE_OK && + sqlite3_bind_int(stmt, 3, protocolValue(protocol)) == SQLITE_OK && + sqlite3_step(stmt) == SQLITE_DONE && + sqlite3_changes(handle.db) > 0; + } + sqlite3_finalize(stmt); + return ok; +} + bool LinuxSqliteChatStore::getMessage(::chat::MessageId msg_id, ::chat::ChatMessage* out) const { @@ -1136,6 +1177,49 @@ bool LinuxSqliteChatStore::getMessage(::chat::MessageId msg_id, return found; } +bool LinuxSqliteChatStore::getMessageForProtocol( + ::chat::MessageId msg_id, + ::chat::MeshProtocol protocol, + ::chat::ChatMessage* out) const +{ + if (msg_id == 0) + { + return false; + } + + std::lock_guard lock(mutex_); + DatabaseHandle handle; + if (!handle) + { + return false; + } + + sqlite3_stmt* stmt = nullptr; + constexpr const char* kSql = + "SELECT protocol, channel, peer, from_node, msg_id, timestamp, text, " + "team_location_icon, has_geo, geo_lat_e7, geo_lon_e7, status, " + "reticulum_identity_valid, reticulum_destination_hash, " + "reticulum_identity_hash " + "FROM chat_messages " + "WHERE msg_id=?1 AND protocol=?2 " + "ORDER BY sequence DESC LIMIT 1;"; + bool found = false; + if (sqlite3_prepare_v2(handle.db, kSql, -1, &stmt, nullptr) == SQLITE_OK && + sqlite3_bind_int64(stmt, 1, static_cast(msg_id)) == + SQLITE_OK && + sqlite3_bind_int(stmt, 2, protocolValue(protocol)) == SQLITE_OK && + sqlite3_step(stmt) == SQLITE_ROW) + { + if (out != nullptr) + { + *out = readMessage(stmt, 0); + } + found = true; + } + sqlite3_finalize(stmt); + return found; +} + void LinuxSqliteChatStore::flush() { } diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h index 383a8978..d9ff1776 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h @@ -30,7 +30,13 @@ class InternalFsStore final : public ::chat::IChatStore void clearConversation(const ::chat::ConversationId& conv) override; void clearAll() override; bool updateMessageStatus(::chat::MessageId msg_id, ::chat::MessageStatus status) override; + bool updateMessageStatusForProtocol(::chat::MessageId msg_id, + ::chat::MeshProtocol protocol, + ::chat::MessageStatus status) override; bool getMessage(::chat::MessageId msg_id, ::chat::ChatMessage* out) const override; + bool getMessageForProtocol(::chat::MessageId msg_id, + ::chat::MeshProtocol protocol, + ::chat::ChatMessage* out) const override; void flush() override; private: diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/sys/event_bus.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/sys/event_bus.h index 88c090fd..25acfa16 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/sys/event_bus.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/sys/event_bus.h @@ -1,5 +1,6 @@ #pragma once +#include "chat/delivery/chat_delivery_types.h" #include "chat/domain/chat_types.h" #include "sys/clock.h" @@ -32,15 +33,24 @@ struct ChatSendResultEvent : public Event chat::MessageId msg_id; bool success; chat::MessageStatus status; + chat::delivery::SendFailureKind failure = + chat::delivery::SendFailureKind::None; + bool has_protocol = false; + chat::MeshProtocol protocol = chat::MeshProtocol::Meshtastic; - ChatSendResultEvent(chat::MessageId id, bool ok) - : Event(EventType::ChatSendResult), msg_id(id), success(ok), - status(ok ? chat::MessageStatus::Sent : chat::MessageStatus::Failed) {} - - ChatSendResultEvent(chat::MessageId id, chat::MessageStatus result_status) + ChatSendResultEvent(chat::MessageId id, + chat::MessageStatus result_status, + chat::MeshProtocol source_protocol, + chat::delivery::SendFailureKind failure_kind = + chat::delivery::SendFailureKind::Unknown) : Event(EventType::ChatSendResult), msg_id(id), success(result_status != chat::MessageStatus::Failed), - status(result_status) {} + status(result_status), + failure(result_status == chat::MessageStatus::Failed + ? failure_kind + : chat::delivery::SendFailureKind::None), + has_protocol(true), + protocol(source_protocol) {} }; struct KeyVerificationNumberRequestEvent : public Event diff --git a/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp b/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp index dcd839c6..871b5eb2 100644 --- a/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp +++ b/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp @@ -542,7 +542,10 @@ bool MeshCoreRadioAdapter::executeProtocolEffect(const ::chat::runtime::Protocol static_cast(item.detail)); forgetLocalTextAck(item.request_id); sys::EventBus::publish( - new sys::ChatSendResultEvent(item.message_id, true), + new sys::ChatSendResultEvent( + item.message_id, + ::chat::MessageStatus::Delivered, + ::chat::MeshProtocol::MeshCore), 0); } else if (is_text_ack && @@ -557,7 +560,11 @@ bool MeshCoreRadioAdapter::executeProtocolEffect(const ::chat::runtime::Protocol static_cast(item.detail)); forgetLocalTextAck(item.request_id); sys::EventBus::publish( - new sys::ChatSendResultEvent(item.message_id, false), + new sys::ChatSendResultEvent( + item.message_id, + ::chat::MessageStatus::Failed, + ::chat::MeshProtocol::MeshCore, + ::chat::delivery::SendFailureKind::AckTimeout), 0); } ok = item.state != ::chat::runtime::ProtocolActionState::Failed && 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 e2e9c3d1..b75008a3 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 @@ -74,6 +74,38 @@ bool shouldRequireDirectPki(uint8_t encrypt_mode, ::chat::NodeId dest_node, uint allowPkiForPortnum(portnum); } +::chat::delivery::SendFailureKind failureKindFromRoutingError( + meshtastic_Routing_Error reason) +{ + switch (reason) + { + case meshtastic_Routing_Error_NONE: + return ::chat::delivery::SendFailureKind::None; + case meshtastic_Routing_Error_TIMEOUT: + case meshtastic_Routing_Error_MAX_RETRANSMIT: + case meshtastic_Routing_Error_NO_RESPONSE: + case meshtastic_Routing_Error_NO_ROUTE: + return ::chat::delivery::SendFailureKind::AckTimeout; + case meshtastic_Routing_Error_NO_INTERFACE: + case meshtastic_Routing_Error_DUTY_CYCLE_LIMIT: + case meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED: + return ::chat::delivery::SendFailureKind::RadioSendFailed; + case meshtastic_Routing_Error_NO_CHANNEL: + return ::chat::delivery::SendFailureKind::ChannelKeyMissing; + case meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY: + case meshtastic_Routing_Error_PKI_FAILED: + return ::chat::delivery::SendFailureKind::PeerKeyMissing; + case meshtastic_Routing_Error_GOT_NAK: + case meshtastic_Routing_Error_BAD_REQUEST: + case meshtastic_Routing_Error_NOT_AUTHORIZED: + case meshtastic_Routing_Error_ADMIN_BAD_SESSION_KEY: + case meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED: + case meshtastic_Routing_Error_TOO_LARGE: + return ::chat::delivery::SendFailureKind::Rejected; + } + return ::chat::delivery::SendFailureKind::Unknown; +} + void logMeshtasticRx(const char* format, ...) { char buffer[192] = {}; @@ -2087,8 +2119,17 @@ void MeshtasticRadioAdapter::emitRoutingResult(uint32_t request_id, meshtastic_R return; } + const bool own_self_echo = from == node_id_ && to == node_id_; + const ::chat::MessageStatus status = + reason != meshtastic_Routing_Error_NONE + ? ::chat::MessageStatus::Failed + : (own_self_echo ? ::chat::MessageStatus::Sent + : ::chat::MessageStatus::Delivered); sys::EventBus::publish( - new sys::ChatSendResultEvent(request_id, reason == meshtastic_Routing_Error_NONE), + new sys::ChatSendResultEvent(request_id, + status, + ::chat::MeshProtocol::Meshtastic, + failureKindFromRoutingError(reason)), 0); meshtastic_Routing routing = meshtastic_Routing_init_default; @@ -3824,7 +3865,7 @@ bool MeshtasticRadioAdapter::queueMqttProxyPublishFromWire(const uint8_t* wire_d return false; } - if (mqtt_proxy_settings_.encryption_enabled) + if (mqtt_proxy_settings_.encryption_enabled || is_pki) { std::memset(&scratch.packet, 0, sizeof(scratch.packet)); if (!makeEncryptedPacketFromWire(wire_data, wire_size, &scratch.packet)) diff --git a/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp b/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp index a5b8635e..7a26c991 100644 --- a/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp +++ b/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp @@ -241,6 +241,38 @@ bool InternalFsStore::updateMessageStatus(::chat::MessageId msg_id, ::chat::Mess return false; } +bool InternalFsStore::updateMessageStatusForProtocol( + ::chat::MessageId msg_id, + ::chat::MeshProtocol protocol, + ::chat::MessageStatus status) +{ + if (msg_id == 0) + { + return false; + } + + for (auto& pair : conversations_) + { + auto& storage = pair.second; + const size_t count = storage.messages.size(); + for (size_t i = 0; i < count; ++i) + { + ::chat::ChatMessage* msg = &storage.messages[i].message; + if (!msg || msg->msg_id != msg_id || msg->protocol != protocol || + msg->from != 0) + { + continue; + } + msg->status = status; + markDirty(); + maybeSave(); + return true; + } + } + + return false; +} + bool InternalFsStore::getMessage(::chat::MessageId msg_id, ::chat::ChatMessage* out) const { if (msg_id == 0) @@ -268,6 +300,37 @@ bool InternalFsStore::getMessage(::chat::MessageId msg_id, ::chat::ChatMessage* return false; } +bool InternalFsStore::getMessageForProtocol( + ::chat::MessageId msg_id, + ::chat::MeshProtocol protocol, + ::chat::ChatMessage* out) const +{ + if (msg_id == 0) + { + return false; + } + + for (const auto& pair : conversations_) + { + const auto& storage = pair.second; + for (const auto& entry : storage.messages) + { + if (entry.message.msg_id != msg_id || + entry.message.protocol != protocol) + { + continue; + } + if (out) + { + *out = entry.message; + } + return true; + } + } + + return false; +} + bool InternalFsStore::ensureFs() const { return path_ && InternalFS.begin(); diff --git a/tests/reticulum_conformance/test_reticulum_runtime_state_contract.cpp b/tests/reticulum_conformance/test_reticulum_runtime_state_contract.cpp index b3887054..81957a81 100644 --- a/tests/reticulum_conformance/test_reticulum_runtime_state_contract.cpp +++ b/tests/reticulum_conformance/test_reticulum_runtime_state_contract.cpp @@ -1,5 +1,9 @@ #include "chat/domain/reticulum_identity.h" #include "chat/infra/mesh_incoming_queue.h" +#include "chat/infra/reticulum/lxst_telephony_wire.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_attempt_ledger.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_notifier.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_planner.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_delivery_runtime.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_destination_registry.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_link_manager.h" @@ -8,6 +12,7 @@ #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_network_page_client.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_packet_router.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_path_manager.h" +#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_peer_directory.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_ping_service.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_client.h" #include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_propagation_runtime.h" @@ -86,6 +91,8 @@ int main() { using namespace chat::lxmf::runtime; namespace reticulum = chat::reticulum; + constexpr uint16_t embedded_lxst_profile = + reticulum::lxst::kProfileBandwidthLow; static_assert(!std::is_copy_constructible::value, "DestinationRegistry owns peer state and must not be copied"); @@ -99,6 +106,20 @@ int main() "LinkManager owns link sessions and must not be copied"); static_assert(!std::is_move_constructible::value, "LinkManager ownership must stay in place"); + static_assert(!std::is_copy_constructible::value, + "PeerDirectoryService owns directory IO boundaries and must not be copied"); + static_assert(!std::is_move_constructible::value, + "PeerDirectoryService ownership must stay in place"); + static_assert(!std::is_copy_constructible::value, + "LxmfDeliveryNotifier is the Reticulum delivery status boundary"); + static_assert(!std::is_move_constructible::value, + "LxmfDeliveryNotifier ownership must stay in place"); + static_assert(!std::is_copy_constructible::value, + "ReticulumDeliveryPlanner is a stateless policy boundary"); + static_assert(!std::is_copy_constructible::value, + "DeliveryAttemptLedger owns outbound attempt receipts and must not be copied"); + static_assert(!std::is_move_constructible::value, + "DeliveryAttemptLedger ownership must stay in place"); static_assert(!std::is_copy_constructible::value, "PingService owns pending ping state and must not be copied"); static_assert(!std::is_copy_constructible::value, @@ -107,6 +128,18 @@ int main() "PropagationClient owns propagation state and must not be copied"); static_assert(!std::is_copy_constructible::value, "LxstTelephonyClient owns call scratch state and must not be copied"); + static_assert( + std::is_same>::value, + "Pending ping queue ownership must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Pending Nomad page queue ownership must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Pending propagation upload queue ownership must stay on PSRAM allocator"); static_assert( std::is_same>::value, @@ -123,6 +156,50 @@ int main() std::is_same>::value, "Resource map hash lists must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Link pending request tables must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Deferred link payload tables must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Link resource transfer tables must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Link resource assembly tables must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Link session ownership must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Transport path table ownership must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Packet filter table ownership must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Reverse path table ownership must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Pending path requests must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Pending ping receipts must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Link relay table ownership must stay on PSRAM allocator"); static_assert( std::is_same>::value, @@ -131,6 +208,34 @@ int main() std::is_same>::value, "Propagation message lists must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Propagation entries must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Propagation transient table ownership must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Propagation peer table ownership must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Pending propagation delivery table must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Propagation batch acceptance messages must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Materialised LXMF app data payload must stay on PSRAM allocator"); + static_assert( + std::is_same>::value, + "Resource window requests must use PSRAM-backed hash lists"); const auto registry_destination = filled_hash(0x04); @@ -149,6 +254,277 @@ int main() registry.clear(); assert(registry.size() == 0); + const auto directory_destination = + filled_hash(0xA0); + const auto directory_identity = + filled_hash(0xB0); + const auto directory_enc_pub = + filled_hash(0xC0); + const auto directory_sig_pub = + filled_hash(0xD0); + const auto directory_ratchet = + filled_hash(0xE0); + PeerDirectoryService peer_directory{}; + chat::MeshPeerRecord directory_record{}; + directory_record.valid = true; + directory_record.identity = chat::makeMeshPeerReticulumIdentity( + chat::makeReticulumPeerIdentity(directory_destination.data(), + directory_identity.data())); + directory_record.last_seen_s = 12345; + chat::copyMeshPeerText(directory_record.display_name, + sizeof(directory_record.display_name), + "Directory Peer"); + directory_record.reticulum.identity = directory_record.identity.reticulum; + directory_record.reticulum.has_public_keys = true; + copy_hash(directory_record.reticulum.enc_pub, directory_enc_pub); + copy_hash(directory_record.reticulum.sig_pub, directory_sig_pub); + directory_record.reticulum.has_ratchet = true; + copy_hash(directory_record.reticulum.ratchet_pub, directory_ratchet); + directory_record.reticulum.ratchet_seen_s = 12344; + PeerInfo* directory_peer = + peer_directory.applyRecord(registry, directory_record, 10000); + assert(directory_peer != nullptr); + assert(registry.size() == 1); + assert(same_hash(directory_peer->destination_hash, directory_destination)); + assert(same_hash(directory_peer->identity_hash, directory_identity)); + assert(same_hash(directory_peer->enc_pub, directory_enc_pub)); + assert(same_hash(directory_peer->sig_pub, directory_sig_pub)); + assert(same_hash(directory_peer->ratchet_pub, directory_ratchet)); + assert(directory_peer->has_ratchet); + assert(directory_peer->ratchet_seen_s == 12344); + assert(directory_peer->last_seen_s == 12345); + assert(std::strcmp(directory_peer->display_name, "Directory Peer") == 0); + assert(peer_directory.persistPeerAddressNow(*directory_peer, true, 10000) + .failure == chat::MeshOperationFailure::NotReady); + registry.clear(); + + static_assert(!std::is_copy_constructible::value, + "LxmfDeliveryNotifier is a runtime event bridge and must not be copied"); + static_assert(!std::is_move_constructible::value, + "LxmfDeliveryNotifier is bound to the runtime event bus"); + + const OutboundDeliveryPlan active_link_plan = + ReticulumDeliveryPlanner::plan(OutboundDeliveryPlanInput{ + true, + false, + true, + reticulum::LxmfDeliveryPreference::Automatic, + true}); + assert(active_link_plan.path == OutboundDeliveryPath::Link); + assert(!active_link_plan.propagation_first); + assert(active_link_plan.may_fallback_to_link); + assert(std::strcmp(ReticulumDeliveryPlanner::pathName( + OutboundDeliveryPath::Link), + "link") == 0); + + const OutboundDeliveryPlan opportunistic_plan = + ReticulumDeliveryPlanner::plan(OutboundDeliveryPlanInput{ + false, + true, + true, + reticulum::LxmfDeliveryPreference::Automatic, + true}); + assert(opportunistic_plan.path == OutboundDeliveryPath::Opportunistic); + assert(!opportunistic_plan.propagation_first); + + const OutboundDeliveryPlan automatic_propagation_plan = + ReticulumDeliveryPlanner::plan(OutboundDeliveryPlanInput{ + false, + false, + true, + reticulum::LxmfDeliveryPreference::Automatic, + true}); + assert(automatic_propagation_plan.path == + OutboundDeliveryPath::Propagation); + assert(automatic_propagation_plan.propagation_first); + assert(automatic_propagation_plan.may_fallback_to_link); + + const OutboundDeliveryPlan propagated_only_plan = + ReticulumDeliveryPlanner::plan(OutboundDeliveryPlanInput{ + false, + false, + true, + reticulum::LxmfDeliveryPreference::Propagated, + false}); + assert(propagated_only_plan.path == OutboundDeliveryPath::Propagation); + assert(propagated_only_plan.propagation_only); + assert(!propagated_only_plan.may_fallback_to_link); + + const OutboundDeliveryPlan deferred_plan = + ReticulumDeliveryPlanner::plan(OutboundDeliveryPlanInput{ + false, + false, + false, + reticulum::LxmfDeliveryPreference::Direct, + false}); + assert(deferred_plan.path == OutboundDeliveryPath::DeferredLink); + assert(std::strcmp(ReticulumDeliveryPlanner::pathName( + OutboundDeliveryPath::DeferredLink), + "deferred_link") == 0); + + DeliveryAttemptLedger attempt_ledger{}; + const auto attempt_packet_hash = + filled_hash(0x31); + const auto attempt_destination_hash = + filled_hash(0x51); + const auto attempt_peer_sig_pub = + filled_hash(0x71); + attempt_ledger.noteDirectPacketReceipt(attempt_packet_hash.data(), + attempt_destination_hash.data(), + attempt_peer_sig_pub.data(), + 123, + 1000, + 2); + assert(attempt_ledger.size() == 1); + uint8_t proof_hash[reticulum::kTruncatedHashSize] = {}; + std::memcpy(proof_hash, attempt_packet_hash.data(), sizeof(proof_hash)); + DeliveryAttemptReceipt* receipt = + attempt_ledger.findReceiptByProofHash(proof_hash); + assert(receipt != nullptr); + assert(receipt->message_id == 123); + assert(receipt->kind == DeliveryAttemptKind::DirectPacket); + assert(same_hash(receipt->destination_hash, attempt_destination_hash)); + assert(std::memcmp(receipt->peer_sig_pub, + attempt_peer_sig_pub.data(), + attempt_peer_sig_pub.size()) == 0); + attempt_ledger.removeReceiptByProofHash(proof_hash); + assert(attempt_ledger.size() == 0); + attempt_ledger.noteDirectPacketReceipt(attempt_packet_hash.data(), + attempt_destination_hash.data(), + attempt_peer_sig_pub.data(), + 124, + 1000, + 2); + attempt_ledger.cull(DeliveryAttemptKind::DirectPacket, 1101, 100, 2); + assert(attempt_ledger.size() == 0); + const auto attempt_link_id = + filled_hash(0x91); + attempt_ledger.noteLinkPacketReceipt(attempt_packet_hash.data(), + attempt_link_id.data(), + 125, + 2000, + 4); + DeliveryAttemptReceipt* link_receipt = + attempt_ledger.findLinkPacketReceipt(attempt_link_id.data(), + attempt_packet_hash.data()); + assert(link_receipt != nullptr); + assert(link_receipt->message_id == 125); + assert(link_receipt->kind == DeliveryAttemptKind::LinkPacket); + assert(same_hash(link_receipt->link_id, attempt_link_id)); + std::size_t expired_link_attempts = 0; + chat::MessageId expired_link_message_id = 0; + attempt_ledger.takeExpiredReceipts( + DeliveryAttemptKind::LinkPacket, + 2061, + 60, + [&expired_link_attempts, &expired_link_message_id]( + const DeliveryAttemptReceipt& expired) + { + ++expired_link_attempts; + expired_link_message_id = expired.message_id; + }); + assert(expired_link_attempts == 1); + assert(expired_link_message_id == 125); + assert(attempt_ledger.size() == 0); + attempt_ledger.noteLinkPacketReceipt(attempt_packet_hash.data(), + attempt_link_id.data(), + 126, + 3000, + 4); + std::size_t taken_for_link = 0; + attempt_ledger.takeReceiptsForLink( + attempt_link_id.data(), + [&taken_for_link](const DeliveryAttemptReceipt& taken) + { + assert(taken.kind == DeliveryAttemptKind::LinkPacket); + ++taken_for_link; + }); + assert(taken_for_link == 1); + assert(attempt_ledger.size() == 0); + attempt_ledger.noteLinkResourceReceipt(attempt_packet_hash.data(), + attempt_link_id.data(), + 127, + 4000, + 4); + DeliveryAttemptReceipt* resource_receipt = + attempt_ledger.findLinkResourceReceipt(attempt_link_id.data(), + attempt_packet_hash.data()); + assert(resource_receipt != nullptr); + assert(resource_receipt->message_id == 127); + assert(resource_receipt->kind == DeliveryAttemptKind::LinkResource); + attempt_ledger.removeLinkResourceReceipt(attempt_link_id.data(), + attempt_packet_hash.data()); + assert(attempt_ledger.size() == 0); + attempt_ledger.noteLinkResourceReceipt(attempt_packet_hash.data(), + attempt_link_id.data(), + 128, + 5000, + 4); + std::size_t expired_resource_attempts = 0; + attempt_ledger.takeExpiredReceipts( + DeliveryAttemptKind::LinkResource, + 5061, + 60, + [&expired_resource_attempts](const DeliveryAttemptReceipt& expired) + { + assert(expired.message_id == 128); + assert(expired.kind == DeliveryAttemptKind::LinkResource); + ++expired_resource_attempts; + }); + assert(expired_resource_attempts == 1); + assert(attempt_ledger.size() == 0); + const auto attempt_transient_id = + filled_hash(0xA1); + attempt_ledger.notePropagationReceipt(attempt_transient_id.data(), + 131, + 6000, + 4); + DeliveryAttemptReceipt* propagation_receipt = + attempt_ledger.findPropagationReceipt(attempt_transient_id.data()); + assert(propagation_receipt != nullptr); + assert(propagation_receipt->message_id == 131); + assert(propagation_receipt->kind == DeliveryAttemptKind::Propagation); + assert(std::memcmp(propagation_receipt->packet_hash, + attempt_transient_id.data(), + attempt_transient_id.size()) == 0); + attempt_ledger.removePropagationReceipt(attempt_transient_id.data()); + assert(attempt_ledger.size() == 0); + attempt_ledger.notePropagationReceipt(attempt_transient_id.data(), + 132, + 7000, + 4); + std::size_t expired_propagation_attempts = 0; + attempt_ledger.takeExpiredReceipts( + DeliveryAttemptKind::Propagation, + 7061, + 60, + [&expired_propagation_attempts]( + const DeliveryAttemptReceipt& expired) + { + assert(expired.message_id == 132); + assert(expired.kind == DeliveryAttemptKind::Propagation); + ++expired_propagation_attempts; + }); + assert(expired_propagation_attempts == 1); + assert(attempt_ledger.size() == 0); + attempt_ledger.noteDirectPacketReceipt(attempt_packet_hash.data(), + attempt_destination_hash.data(), + attempt_peer_sig_pub.data(), + 129, + 6000, + 1); + attempt_ledger.noteLinkResourceReceipt(attempt_packet_hash.data(), + attempt_link_id.data(), + 130, + 6001, + 1); + assert(attempt_ledger.size() == 2); + assert(attempt_ledger.findReceiptByProofHash(proof_hash) != nullptr); + assert(attempt_ledger.findLinkResourceReceipt(attempt_link_id.data(), + attempt_packet_hash.data()) != + nullptr); + attempt_ledger.clear(); + ReticulumPacketRouter router{}; reticulum::ParsedPacket route_packet{}; route_packet.packet_type = reticulum::PacketType::Announce; @@ -161,6 +537,43 @@ int main() assert(router.route(route_packet) == PacketRoute::Data); route_packet.packet_type = static_cast(0x7F); assert(router.route(route_packet) == PacketRoute::LinkOrTransport); + PathEntry direct_forward_path{}; + direct_forward_path.interface_id = 3; + direct_forward_path.hops = 1; + PacketForwardPlan direct_forward = + router.planPathForward(direct_forward_path, 4); + assert(direct_forward.forward); + assert(direct_forward.header == PacketForwardHeader::Header1Broadcast); + assert(direct_forward.interface_id == 3); + assert(direct_forward.hops == 4); + PathEntry routed_forward_path{}; + routed_forward_path.interface_id = 5; + routed_forward_path.hops = 3; + std::memset(routed_forward_path.next_hop_transport, + 0x9A, + sizeof(routed_forward_path.next_hop_transport)); + PacketForwardPlan routed_forward = + router.planPathForward(routed_forward_path, 6); + assert(routed_forward.forward); + assert(routed_forward.header == PacketForwardHeader::Header2Transport); + assert(routed_forward.interface_id == 5); + assert(routed_forward.next_hop_transport[0] == 0x9A); + LinkRelayEntry relay_forward{}; + relay_forward.initiator_interface_id = 7; + relay_forward.responder_interface_id = 8; + relay_forward.initiator_hops = 1; + relay_forward.responder_hops = 2; + PacketForwardPlan relay_from_initiator = + router.planLinkRelayForward(relay_forward, 7, 1); + assert(relay_from_initiator.forward); + assert(relay_from_initiator.header == + PacketForwardHeader::Header1Broadcast); + assert(relay_from_initiator.interface_id == 8); + PacketForwardPlan relay_from_responder = + router.planLinkRelayForward(relay_forward, 8, 2); + assert(relay_from_responder.forward); + assert(relay_from_responder.interface_id == 7); + assert(!router.planLinkRelayForward(relay_forward, 9, 1).forward); const auto manager_destination = filled_hash(0x14); @@ -181,14 +594,51 @@ int main() assert(path_manager.findPendingPathRequest(manager_destination.data()) != nullptr); path_manager.resolvePendingPathRequest(manager_destination.data()); assert(path_manager.findPendingPathRequest(manager_destination.data()) == nullptr); + path_manager.notePendingPathRequest(manager_destination.data(), 710, 4); + const auto manager_next_hop = + filled_hash(0x24); + const uint8_t announce_random[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + const uint8_t cached_announce[] = {0x82, 0x01, 0x02}; + PathEntry* observed_path = + path_manager.observeAnnouncePath(manager_destination.data(), + 3, + announce_random, + 900, + 901, + 7, + manager_next_hop.data(), + cached_announce, + sizeof(cached_announce), + 4); + assert(observed_path != nullptr); + assert(observed_path->hops == 3); + assert(observed_path->interface_id == 7); + assert(!observed_path->direct); + assert(same_hash(observed_path->next_hop_transport, manager_next_hop)); + assert(observed_path->cached_announce_len == sizeof(cached_announce)); + assert(path_manager.findPendingPathRequest(manager_destination.data()) == nullptr); LinkManager link_manager{}; - LinkSession* managed_session = link_manager.appendSession(2); + LinkSessionSpec managed_session_spec{}; + managed_session_spec.now_ms = 800; + managed_session_spec.link_id = manager_destination.data(); + managed_session_spec.remote_destination_hash = manager_destination.data(); + managed_session_spec.destination = LocalDestinationKind::Delivery; + managed_session_spec.state = LinkState::Active; + managed_session_spec.initiator = true; + managed_session_spec.expected_hops = 2; + managed_session_spec.keepalive_interval_ms = 1234; + managed_session_spec.stale_timeout_ms = 5678; + LinkSession* managed_session = + link_manager.openSession(2, managed_session_spec); assert(managed_session != nullptr); - copy_hash(managed_session->link_id, manager_destination); - copy_hash(managed_session->remote_destination_hash, manager_destination); - managed_session->destination = LocalDestinationKind::Delivery; - managed_session->state = LinkState::Active; + assert(managed_session->created_ms == 800); + assert(managed_session->request_ms == 800); + assert(managed_session->last_inbound_ms == 800); + assert(managed_session->initiator); + assert(managed_session->expected_hops == 2); + assert(managed_session->keepalive_interval_ms == 1234); + assert(managed_session->stale_timeout_ms == 5678); assert(link_manager.size() == 1); assert(link_manager.findSession(manager_destination.data()) == managed_session); assert(link_manager.findOpenSessionByDestination(manager_destination.data(), @@ -198,8 +648,81 @@ int main() assert(managed_session->state == LinkState::Closed); link_manager.clear(); assert(link_manager.size() == 0); - managed_session = link_manager.appendSession(2); + managed_session = link_manager.openSession(2, LinkSessionSpec{}); assert(managed_session != nullptr); + const uint8_t managed_request_id[] = {0x41, 0x42, 0x43}; + LinkPendingRequest* managed_request = + link_manager.queuePendingRequest(*managed_session, + managed_request_id, + sizeof(managed_request_id), + 910, + true); + assert(managed_request != nullptr); + assert(link_manager.pendingRequestCount(*managed_session) == 1); + assert(managed_request->created_ms == 910); + assert(managed_request->awaiting_resource); + assert(link_manager.findPendingRequest(*managed_session, + managed_request_id, + sizeof(managed_request_id)) == + managed_request); + const uint8_t managed_response[] = {0x51, 0x52}; + assert(link_manager.markPendingResponseReady(*managed_session, + managed_request_id, + sizeof(managed_request_id), + managed_response, + sizeof(managed_response), + false)); + assert(managed_request->response_ready); + assert(managed_request->response.size() == sizeof(managed_response)); + assert(managed_request->response[0] == managed_response[0]); + assert(link_manager.erasePendingRequest(*managed_session, + *managed_request)); + assert(link_manager.pendingRequestCount(*managed_session) == 0); + assert(link_manager.findPendingRequest(*managed_session, + managed_request_id, + sizeof(managed_request_id)) == + nullptr); + DeferredLinkPayload managed_deferred{}; + managed_deferred.payload = {0x61, 0x62, 0x63}; + managed_deferred.message_id = 0x1234; + assert(link_manager.appendDeferredPayload(*managed_session, + std::move(managed_deferred)) != + nullptr); + assert(link_manager.deferredPayloadCount(*managed_session) == 1); + const DeferredLinkPayload* first_deferred = + link_manager.firstDeferredPayload(*managed_session); + assert(first_deferred != nullptr); + assert(first_deferred->message_id == 0x1234); + assert(first_deferred->payload.size() == 3); + std::size_t deferred_visit_count = 0; + link_manager.forEachDeferredPayload( + *managed_session, + [&deferred_visit_count](const DeferredLinkPayload& deferred_payload) + { + assert(deferred_payload.message_id == 0x1234); + ++deferred_visit_count; + }); + assert(deferred_visit_count == 1); + assert(link_manager.popFirstDeferredPayload(*managed_session)); + assert(link_manager.deferredPayloadCount(*managed_session) == 0); + assert(link_manager.firstDeferredPayload(*managed_session) == nullptr); + link_manager.touchInbound(*managed_session, 920); + link_manager.touchOutbound(*managed_session, 930); + assert(managed_session->last_inbound_ms == 920); + assert(managed_session->last_outbound_ms == 930); + link_manager.markSessionValidatedActive(*managed_session, 1.5f, 1500); + assert(managed_session->state == LinkState::Active); + assert(managed_session->validated); + assert(managed_session->rtt_s == 1.5f); + assert(managed_session->keepalive_interval_ms == 1500); + assert(managed_session->stale_timeout_ms == 3000); + assert(managed_session->last_keepalive_ms == 0); + link_manager.noteKeepaliveSent(*managed_session, 940); + assert(managed_session->last_keepalive_ms == 940); + managed_session->state = LinkState::Stale; + assert(link_manager.reactivateSessionIfStale(*managed_session)); + assert(managed_session->state == LinkState::Active); + assert(!link_manager.reactivateSessionIfStale(*managed_session)); const auto resource_hash = filled_hash(0x44); const auto resource_original_hash = filled_hash(0x64); @@ -252,22 +775,24 @@ int main() 1010, 4)); copy_hash(managed_outgoing_resource.resource_hash, resource_hash); - managed_outgoing_resource.message_id = 77; assert(link_manager.appendOutgoingResource(*managed_session, std::move(managed_outgoing_resource)) != nullptr); LinkResourceTransfer* queued_outgoing_resource = link_manager.findOutgoingResource(*managed_session, resource_hash.data()); assert(queued_outgoing_resource != nullptr); - bool saw_resource_message_id = false; - link_manager.takeTrackedOutgoingResourceMessageIds( + bool saw_expired_resource = false; + link_manager.forEachExpiredOutgoingResource( *managed_session, - [&saw_resource_message_id](uint32_t message_id) + 1010 + 60001, + 60000, + [&saw_expired_resource, + &resource_hash](const LinkResourceTransfer& resource) { - saw_resource_message_id = message_id == 77; + saw_expired_resource = + same_hash(resource.resource_hash, resource_hash); }); - assert(saw_resource_message_id); - assert(queued_outgoing_resource->message_id == 0); + assert(saw_expired_resource); assert(link_manager.eraseOutgoingResource(*managed_session, resource_hash.data())); link_manager.clear(); @@ -333,6 +858,33 @@ int main() assert(page_client.findByRequestId(manager_destination.data(), manager_destination.data(), reticulum::kTruncatedHashSize) == page_request); + assert(page_client.attemptDue(*page_request, 100, 50)); + page_client.noteAttempt(*page_request, 100); + assert(!page_client.attemptDue(*page_request, 120, 50)); + assert(page_client.attemptDue(*page_request, 151, 50)); + assert(page_client.lastAttemptAge(*page_request, 125) == 25); + assert(page_client.pathRequestDue(*page_request, 100, 30)); + page_client.notePathRequest(*page_request, true, 100); + assert(page_request->path_requested); + assert(!page_client.pathRequestDue(*page_request, 120, 30)); + page_client.noteLinkStart(*page_request, false, 130, false); + assert(!page_request->link_started); + page_client.noteLinkStart(*page_request, true, 140, true); + assert(page_request->link_started); + uint8_t nomad_request_id[reticulum::kTruncatedHashSize] = {}; + std::memset(nomad_request_id, 0x33, sizeof(nomad_request_id)); + assert(page_client.noteRequestPacketSent(*page_request, + nomad_request_id, + sizeof(nomad_request_id), + 150)); + assert(page_request->request_sent); + assert(std::memcmp(page_request->request_id, + nomad_request_id, + sizeof(nomad_request_id)) == 0); + assert(page_client.findByRequestId(manager_destination.data(), + nomad_request_id, + reticulum::kTruncatedHashSize) == + page_request); assert(page_client.queue(manager_destination.data(), "/", 200, @@ -348,6 +900,7 @@ int main() copy_hash(active_propagation_peer.propagation_hash, manager_destination); active_propagation_peer.node_active = true; active_propagation_peer.last_seen_s = 100; + active_propagation_peer.stamp_cost = 3; propagation_client.state().peers.push_back(active_propagation_peer); PropagationActivePeerSelection selected_peer = propagation_client.selectActivePeer(false, @@ -404,7 +957,6 @@ int main() assert(propagation_client.state().sync_stage == PropagationSyncStage::Idle); assert(!propagation_client.state().initial_sync_pending); PendingPropagationUpload upload_a{}; - upload_a.message_id = 101; upload_a.created_ms = 1000; upload_a.state = PropagationUploadState::WaitingNode; PendingPropagationUpload* queued_upload = @@ -412,38 +964,44 @@ int main() assert(queued_upload != nullptr); assert(propagation_client.hasPendingUploads()); assert(propagation_client.firstPendingUpload() == queued_upload); - assert(propagation_client.firstPendingUpload()->message_id == 101); + assert(propagation_client.firstPendingUpload()->created_ms == 1000); + assert(propagation_client.firstPendingUpload()->state == + PropagationUploadState::WaitingNode); + assert(propagation_client.bindUploadNode(*queued_upload, + active_propagation_peer)); + assert(propagation_client.firstPendingUpload()->state == + PropagationUploadState::NeedsStamp); + assert(propagation_client.firstPendingUpload()->stamp_cost == 3); + assert(!propagation_client.bindUploadNode(*queued_upload, + active_propagation_peer)); PendingPropagationUpload upload_b{}; - upload_b.message_id = 102; upload_b.created_ms = 1005; upload_b.state = PropagationUploadState::WaitingNode; assert(propagation_client.queueUpload(std::move(upload_b), 2) != nullptr); PendingPropagationUpload upload_c{}; - upload_c.message_id = 103; upload_c.created_ms = 1010; upload_c.state = PropagationUploadState::WaitingNode; assert(propagation_client.queueUpload(std::move(upload_c), 2) == nullptr); propagation_client.markExpiredUploads(1101, 100); - std::vector failed_uploads = - propagation_client.takeFailedUploads(); + auto failed_uploads = propagation_client.takeFailedUploads(); assert(failed_uploads.size() == 1); - assert(failed_uploads[0].message_id == 101); + assert(failed_uploads[0].created_ms == 1000); assert(propagation_client.hasPendingUploads()); - assert(propagation_client.firstPendingUpload()->message_id == 102); + assert(propagation_client.firstPendingUpload()->created_ms == 1005); assert(propagation_client.removeFirstPendingUpload()); assert(!propagation_client.removeFirstPendingUpload()); assert(!propagation_client.hasPendingUploads()); PendingPropagationUpload upload_d{}; - upload_d.message_id = 104; + upload_d.created_ms = 1040; upload_d.state = PropagationUploadState::NeedsStamp; assert(propagation_client.queueUpload(std::move(upload_d), 2) != nullptr); - std::vector all_uploads = - propagation_client.takeAllPendingUploads(); + auto all_uploads = propagation_client.takeAllPendingUploads(); assert(all_uploads.size() == 1); - assert(all_uploads[0].message_id == 104); + assert(all_uploads[0].created_ms == 1040); + assert(all_uploads[0].state == PropagationUploadState::NeedsStamp); assert(!propagation_client.hasPendingUploads()); #if !defined(TRAIL_MATE_RETICULUM_PARSE_ONLY) propagation_client.stamp().reset(); @@ -454,6 +1012,44 @@ int main() assert(telephony_client.scratchCapacity() == reticulum::kReticulumMtu); telephony_client.scratch()[0] = 0xA5; assert(telephony_client.scratch()[0] == 0xA5); + LinkSession telephony_session{}; + telephony_session.destination = LocalDestinationKind::CallAudio; + telephony_client.beginCallerSession(telephony_session, + chat::ReticulumCallWireProfile::SidebandLxst, + embedded_lxst_profile, + 1000); + assert(telephony_client.isCallSession(telephony_session)); + assert(telephony_client.isSidebandSession(telephony_session)); + assert(telephony_client.profile(telephony_session) == + embedded_lxst_profile); + assert(telephony_client.phase(telephony_session) == + reticulum::lxst::call::Phase::CallerAwaitingLink); + assert(!telephony_client.runtimeStarted(telephony_session)); + telephony_client.markRuntimeStarted(telephony_session, true); + assert(telephony_client.runtimeStarted(telephony_session)); + reticulum::lxst::call::Phase previous_phase = + reticulum::lxst::call::Phase::Idle; + const auto link_transition = telephony_client.dispatch( + telephony_session, + {reticulum::lxst::call::EventType::LinkActive}, + 1010, + &previous_phase); + assert(link_transition.accepted); + assert(previous_phase == + reticulum::lxst::call::Phase::CallerAwaitingLink); + uint8_t* encoded_signal = nullptr; + std::size_t encoded_signal_len = 0; + assert(telephony_client.encodeSignal(reticulum::lxst::kStatusAvailable, + &encoded_signal, + &encoded_signal_len)); + assert(encoded_signal == telephony_client.scratch()); + assert(encoded_signal_len != 0); + telephony_client.beginSidebandCalleeSession( + telephony_session, + embedded_lxst_profile, + 2000); + assert(telephony_client.phase(telephony_session) == + reticulum::lxst::call::Phase::CalleeAwaitingLink); TransportRuntime transport{}; assert(transport.paths.empty()); @@ -461,7 +1057,6 @@ int main() assert(transport.reverse_table.empty()); assert(transport.pending_path_requests.empty()); assert(transport.pending_ping_receipts.empty()); - assert(transport.pending_delivery_receipts.empty()); assert(transport.link_relays.empty()); const TransportRuntimeLimits limits{ @@ -476,9 +1071,7 @@ int main() 300000, 4, 1000, - 500, - 4, - 750}; + 500}; const auto destination = filled_hash(0x10); PathEntry& path = upsertPath(transport, destination.data(), limits.max_paths); @@ -534,6 +1127,66 @@ int main() limits.path_ttl_ms) == PathAnnounceDecision::AcceptExpired); + PathManager path_policy_manager{}; + PeerInfo path_peer{}; + copy_hash(path_peer.destination_hash, destination); + assert(path_policy_manager.shouldRequestPeerPath(path_peer, + 100, + 10, + 30000, + 5000, + 1000, + 60)); + path_policy_manager.notePendingPathRequest(destination.data(), + 100, + limits.max_pending_path_requests); + assert(path_policy_manager.pendingPathRequestCoolingDown( + destination.data(), + 120, + 5000)); + assert(!path_policy_manager.shouldRequestPeerPath(path_peer, + 120, + 10, + 30000, + 5000, + 1000, + 60)); + path_policy_manager.resolvePendingPathRequest(destination.data()); + path_policy_manager.notePeerPathRequest(path_peer, 140); + assert(!path_policy_manager.shouldRequestPeerPath(path_peer, + 150, + 10, + 30000, + 5000, + 1000, + 60)); + path_policy_manager.resetPeerPathRequest(path_peer); + const auto path_random = announce_blob(0xD0, 400); + assert(path_policy_manager.observeAnnouncePath(destination.data(), + 1, + path_random.data(), + 200, + 100, + 2, + nullptr, + nullptr, + 0, + limits.max_paths) != nullptr); + assert(!path_policy_manager.shouldRequestPeerPath(path_peer, + 250, + 120, + 30000, + 5000, + 1000, + 60)); + assert(path_policy_manager.shouldRequestPeerPath(path_peer, + 250, + 161, + 30000, + 5000, + 1000, + 60)); + const auto packet_hash = filled_hash(0x20); rememberPacket(transport, packet_hash.data(), 100, limits.max_packet_filter); assert(same_hash(transport.packet_filter.front().packet_hash, packet_hash)); @@ -579,21 +1232,6 @@ int main() assert(same_hash(ping_receipt->destination_hash, destination)); assert(same_hash(ping_receipt->peer_sig_pub, peer_sig_pub)); - notePendingDeliveryReceipt(transport, - packet_hash.data(), - destination.data(), - peer_sig_pub.data(), - 1234, - 350, - limits.max_pending_delivery_receipts); - PendingDeliveryReceipt* delivery_receipt = - findPendingDeliveryReceipt(transport, packet_hash.data()); - assert(delivery_receipt != nullptr); - assert(delivery_receipt->message_id == 1234); - assert(same_hash(delivery_receipt->packet_hash, packet_hash)); - assert(same_hash(delivery_receipt->destination_hash, destination)); - assert(same_hash(delivery_receipt->peer_sig_pub, peer_sig_pub)); - LinkRelayEntry& relay = upsertLinkRelay(transport, destination.data(), limits.max_link_relays); relay.initiator_hops = 1; relay.responder_hops = 2; @@ -606,7 +1244,6 @@ int main() assert(transport.reverse_table.empty()); assert(transport.pending_path_requests.empty()); assert(transport.pending_ping_receipts.empty()); - assert(transport.pending_delivery_receipts.empty()); assert(transport.link_relays.empty()); assert(transport.paths.empty()); @@ -1587,7 +2224,10 @@ int main() assert(app_delivery.incoming.channel == ::chat::ChannelId::PRIMARY); assert(app_delivery.incoming.want_response); assert(app_delivery.incoming.rx_meta.rssi_dbm_x10 == -710); - assert(app_delivery.payload == app_payload.payload); + assert(app_delivery.payload.size() == app_payload.payload.size()); + assert(std::memcmp(app_delivery.payload.data(), + app_payload.payload.data(), + app_payload.payload.size()) == 0); const uint32_t team_location_ports[] = { team::proto::TEAM_POSITION_APP, @@ -1623,7 +2263,10 @@ int main() assert(team_delivery.incoming.packet_id == team_payload.packet_id); assert(team_delivery.incoming.channel == ::chat::ChannelId::PRIMARY); assert(!team_delivery.incoming.want_response); - assert(team_delivery.payload == team_payload.payload); + assert(team_delivery.payload.size() == team_payload.payload.size()); + assert(std::memcmp(team_delivery.payload.data(), + team_payload.payload.data(), + team_payload.payload.size()) == 0); } const uint8_t invalid_delivery_payload[] = {0x01, 0x02, 0x03};