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};