From aa00779b71e36fd859eafe277c026b63893fc9ad Mon Sep 17 00:00:00 2001 From: liu weikai Date: Thu, 23 Jul 2026 19:23:51 +0800 Subject: [PATCH] refactor: unify peer directory storage Retire the legacy NodeInfo, INodeStore, and IContactStore directory model. Route contact projections, protocol observations, persistence, and platform composition through IMeshPeerDirectory across ESP, Linux, and nRF52. --- .../src/esp32_lvgl_idf_app_facade_runtime.cpp | 295 +----- apps/linux_sim_shell/CMakeLists.txt | 23 +- .../src/nrf52_node_app_facade_runtime.cpp | 86 +- .../src/nrf52_node_app_facade_runtime.h | 11 +- .../src/platform_ui_bindings.cpp | 2 +- .../t_echo_lite/src/platform_ui_bindings.cpp | 2 +- .../esp_idf/ESP_IDF_COMPONENT_SOURCES.cmake | 2 - cmake/TrailMateLinuxSources.cmake | 3 - .../include/chat/domain/contact_types.h | 23 +- .../include/chat/domain/mesh_peer_directory.h | 48 + .../include/chat/infra/contact_store_core.h | 50 -- .../chat/infra/mesh_peer_directory_core.h | 13 +- .../chat/infra/node_store_blob_format.h | 65 -- .../include/chat/infra/node_store_core.h | 66 -- .../include/chat/ports/i_contact_blob_store.h | 19 - .../include/chat/ports/i_contact_store.h | 80 -- .../chat/ports/i_mesh_peer_directory.h | 35 +- .../include/chat/ports/i_node_blob_store.h | 23 - .../include/chat/ports/i_node_store.h | 129 --- .../chat/ports/i_protocol_peer_repository.h | 8 +- .../include/chat/usecase/contact_service.h | 33 +- .../src/infra/contact_store_core.cpp | 212 ----- .../src/infra/mesh_peer_directory_core.cpp | 180 +++- .../src/infra/node_store_blob_format.cpp | 128 --- .../core_chat/src/infra/node_store_core.cpp | 791 ----------------- .../core_chat/src/usecase/contact_service.cpp | 834 ++++++++++------- .../test_mesh_peer_directory_contract.cpp | 103 +++ .../core_chat/tests/test_node_store_core.cpp | 385 -------- .../include/mesh/ports/i_node_store.h | 25 - .../app/app_context_platform_bindings.h | 2 - modules/core_sys/include/app/app_facades.h | 3 - .../platform/ui/reticulum_directory_runtime.h | 1 + ...test_key_verification_runtime_adapters.cpp | 129 +-- modules/ui_mono/include/ui/mono/runtime.h | 4 +- modules/ui_mono/src/runtime.cpp | 14 +- .../screens/contacts/contacts_page_layout.h | 4 +- .../ui/screens/contacts/contacts_state.h | 10 +- .../node_info/node_info_page_components.h | 2 +- .../dashboard/dashboard_compass_widget.cpp | 4 +- .../chat_presentation_source.cpp | 4 +- .../ui/screens/chat/chat_ui_controller.cpp | 8 +- .../contacts/contacts_page_components.cpp | 70 +- .../screens/contacts/contacts_page_layout.cpp | 4 +- .../contacts/contacts_page_runtime.cpp | 32 +- .../node_info/node_info_page_components.cpp | 22 +- .../tests/test_chat_presentation_source.cpp | 155 +--- .../arduino_common/include/app/app_context.h | 24 - .../arduino_common/include/ble/meshcore_ble.h | 1 - .../infra/store/sd_protocol_peer_repository.h | 84 +- .../esp/arduino_common/src/app_context.cpp | 6 +- .../src/app_context_platform_bindings.cpp | 9 +- .../src/app_runtime_support.cpp | 4 +- .../src/ble/app_phone_facade.cpp | 46 +- .../arduino_common/src/ble/meshcore_ble.cpp | 18 +- .../src/ble/meshcore_ble_owner_hooks.cpp | 20 +- .../src/chat/infra/meshtastic/mt_adapter.cpp | 1 - .../store/sd_protocol_peer_repository.cpp | 837 +++--------------- .../common/src/app/linux_app_services.cpp | 74 +- .../ui/reticulum_directory_runtime.cpp | 5 + .../src/uconsole_chat_workspace_model.cpp | 28 +- .../uconsole/src/uconsole_dashboard_model.cpp | 10 +- .../src/uconsole_map_workspace_model.cpp | 4 +- .../arduino_common/include/ble/meshcore_ble.h | 1 - .../chat/infra/blob_file_store.h | 37 +- .../arduino_common/chat/infra/contact_store.h | 29 - .../infra/meshcore/meshcore_radio_adapter.h | 2 + .../meshtastic/meshtastic_radio_adapter.h | 4 +- .../chat/infra/meshtastic/node_store.h | 37 - .../nrf52/arduino_common/device_identity.h | 4 +- .../src/ble/app_phone_facade.cpp | 46 +- .../src/chat/infra/blob_file_store.cpp | 59 +- .../src/chat/infra/contact_store.cpp | 57 -- .../infra/meshcore/meshcore_radio_adapter.cpp | 51 ++ .../meshtastic/meshtastic_radio_adapter.cpp | 62 +- .../src/chat/infra/meshtastic/node_store.cpp | 76 -- .../arduino_common/src/device_identity.cpp | 23 +- .../nrf52/protocol/nrf52_protocol_factory.cpp | 2 - .../nrf52/protocol/nrf52_protocol_factory.h | 2 - 78 files changed, 1408 insertions(+), 4397 deletions(-) delete mode 100644 modules/core_chat/include/chat/infra/contact_store_core.h delete mode 100644 modules/core_chat/include/chat/infra/node_store_blob_format.h delete mode 100644 modules/core_chat/include/chat/infra/node_store_core.h delete mode 100644 modules/core_chat/include/chat/ports/i_contact_blob_store.h delete mode 100644 modules/core_chat/include/chat/ports/i_contact_store.h delete mode 100644 modules/core_chat/include/chat/ports/i_node_blob_store.h delete mode 100644 modules/core_chat/include/chat/ports/i_node_store.h delete mode 100644 modules/core_chat/src/infra/contact_store_core.cpp delete mode 100644 modules/core_chat/src/infra/node_store_blob_format.cpp delete mode 100644 modules/core_chat/src/infra/node_store_core.cpp delete mode 100644 modules/core_chat/tests/test_node_store_core.cpp delete mode 100644 modules/core_mesh/include/mesh/ports/i_node_store.h delete mode 100644 platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/contact_store.h delete mode 100644 platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h delete mode 100644 platform/nrf52/arduino_common/src/chat/infra/contact_store.cpp delete mode 100644 platform/nrf52/arduino_common/src/chat/infra/meshtastic/node_store.cpp diff --git a/apps/esp32_lvgl/src/esp32_lvgl_idf_app_facade_runtime.cpp b/apps/esp32_lvgl/src/esp32_lvgl_idf_app_facade_runtime.cpp index 42ead3bf..587315a5 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 @@ -8,18 +8,13 @@ #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" #include "chat/infra/meshcore/mc_region_presets.h" #include "chat/infra/meshtastic/mt_region.h" -#include "chat/infra/node_store_blob_format.h" -#include "chat/infra/node_store_core.h" #include "chat/infra/store/ram_store.h" -#include "chat/ports/i_contact_blob_store.h" #include "chat/ports/i_mesh_adapter.h" #include "chat/ports/i_mesh_peer_directory_blob_store.h" -#include "chat/ports/i_node_blob_store.h" #include "chat/usecase/chat_service.h" #include "chat/usecase/contact_service.h" #include "esp_log.h" @@ -81,23 +76,12 @@ constexpr const char* kIdfStoreTag = "idf-app-store"; constexpr const char* kIdfConfigTag = "idf-app-cfg"; constexpr const char* kIdfSettingsNs = "idf_app"; constexpr const char* kIdfConfigKey = "app_cfg"; -constexpr const char* kIdfNodesNvsKey = "nodes_blob"; -constexpr const char* kIdfContactsNvsKey = "contacts"; -constexpr const char* kIdfContactsFile = "/contacts.dat"; -constexpr const char* kIdfNodesFile = "/nodes.bin"; constexpr const char* kIdfMeshPeersDir = "/mesh"; constexpr const char* kIdfMeshPeersFile = "/mesh/peers.bin"; constexpr size_t kIdfReadChunkBytes = 256; constexpr uint32_t kIdfAppConfigMagic = 0x50344346UL; // P4CF constexpr uint16_t kIdfAppConfigVersion = 1; -constexpr uint32_t kIdfNodeStoreFlushIntervalMs = 5000UL; -constexpr size_t kIdfMaxContactBlobBytes = - chat::contacts::ContactStoreCore::kMaxContacts * - chat::contacts::ContactStoreCore::kSerializedEntrySize; -constexpr size_t kIdfMaxNodeFileBytes = - sizeof(chat::contacts::NodeStoreSdHeader) + - chat::contacts::NodeStoreCore::kMaxNodes * - chat::contacts::NodeStoreCore::kSerializedEntrySize; +constexpr uint32_t kIdfPeerDirectoryFlushIntervalMs = 5000UL; constexpr size_t kIdfMaxMeshPeerBlobBytes = 768U * 1024U; constexpr const char* kIdfTeamTag = "idf-team"; constexpr size_t kTeamAeadTagBytes = 16; @@ -282,62 +266,6 @@ bool saveIdfAppConfig(const app::AppConfig& config) return ok; } -bool loadNvsBlob(const char* key, const char* label, std::vector& out, size_t max_len) -{ - std::vector blob; - if (!platform::ui::settings_store::get_blob(kIdfSettingsNs, key, blob)) - { - return false; - } - if (blob.empty() || blob.size() > max_len) - { - ESP_LOGW(kIdfStoreTag, - "%s nvs load rejected len=%u max=%u", - label, - static_cast(blob.size()), - static_cast(max_len)); - out.clear(); - return false; - } - out = blob; - ESP_LOGI(kIdfStoreTag, - "%s load source=nvs key=%s len=%u", - label, - key, - static_cast(out.size())); - return true; -} - -bool saveNvsBlob(const char* key, const char* label, const uint8_t* data, size_t len, size_t max_len) -{ - if (len == 0) - { - return platform::ui::settings_store::put_blob(kIdfSettingsNs, key, nullptr, 0); - } - if (!data || len > max_len) - { - return false; - } - - const bool ok = platform::ui::settings_store::put_blob(kIdfSettingsNs, key, data, len); - ESP_LOGI(kIdfStoreTag, - "%s save target=nvs key=%s len=%u ok=%u", - label, - key, - static_cast(len), - ok ? 1U : 0U); - return ok; -} - -bool isValidContactBlobSize(size_t len) -{ - return len != 0 && - len <= kIdfMaxContactBlobBytes && - (len % chat::contacts::ContactStoreCore::kSerializedEntrySize) == 0 && - (len / chat::contacts::ContactStoreCore::kSerializedEntrySize) <= - chat::contacts::ContactStoreCore::kMaxContacts; -} - std::string makeSdPath(const char* relative) { if (!relative || !relative[0]) @@ -477,197 +405,6 @@ bool writeSdFileAtomic(const char* relative, const uint8_t* data, size_t len) return true; } -class IdfSdNodeBlobStore final : public chat::contacts::INodeBlobStore -{ - public: - bool loadBlob(std::vector& out) override - { - std::vector file; - if (!readSdFile(kIdfNodesFile, file, kIdfMaxNodeFileBytes) || - file.size() <= sizeof(chat::contacts::NodeStoreSdHeader)) - { - return loadBlobFromNvs(out); - } - - chat::contacts::NodeStoreSdHeader header{}; - std::memcpy(&header, file.data(), sizeof(header)); - const uint8_t* payload = file.data() + sizeof(header); - const size_t payload_len = file.size() - sizeof(header); - if (chat::contacts::validateNodeStoreSdBlob(header, payload, payload_len) != - chat::contacts::NodeBlobValidation::Ok) - { - out.clear(); - ESP_LOGW(kIdfStoreTag, - "node load invalid path=%s ver=%u count=%u len=%u", - kIdfNodesFile, - static_cast(header.ver), - static_cast(header.count), - static_cast(payload_len)); - return loadBlobFromNvs(out); - } - - std::vector entries; - if (!chat::contacts::NodeStoreCore::decodeBlob(entries, payload, payload_len, header.ver)) - { - out.clear(); - ESP_LOGW(kIdfStoreTag, "node load decode failed path=%s ver=%u", - kIdfNodesFile, - static_cast(header.ver)); - return loadBlobFromNvs(out); - } - - chat::contacts::NodeStoreCore::encodeBlob(out, entries); - ESP_LOGI(kIdfStoreTag, - "node load source=sd path=%s count=%u len=%u", - kIdfNodesFile, - static_cast(entries.size()), - static_cast(out.size())); - return !out.empty(); - } - - bool saveBlob(const uint8_t* data, size_t len) override - { - if (len == 0) - { - (void)removeSdFile(kIdfNodesFile); - (void)saveNvsBlob(kIdfNodesNvsKey, "node", nullptr, 0, kIdfMaxNodeFileBytes); - return true; - } - if (!chat::contacts::isValidNodeBlobSize(len) || - chat::contacts::nodeBlobEntryCount(len) > chat::contacts::NodeStoreCore::kMaxNodes) - { - return false; - } - - chat::contacts::NodeStoreSdHeader header = - chat::contacts::makeNodeStoreSdHeader(data, len); - std::vector file(sizeof(header) + len); - std::memcpy(file.data(), &header, sizeof(header)); - std::memcpy(file.data() + sizeof(header), data, len); - const bool sd_ok = writeSdFileAtomic(kIdfNodesFile, file.data(), file.size()); - const bool nvs_attempted = !sd_ok; - const bool nvs_ok = - nvs_attempted - ? saveNvsBlob(kIdfNodesNvsKey, "node", data, len, kIdfMaxNodeFileBytes) - : false; - ESP_LOGI(kIdfStoreTag, - "node save path=%s count=%u len=%u sd=%u nvs=%s", - kIdfNodesFile, - static_cast(header.count), - static_cast(len), - sd_ok ? 1U : 0U, - nvs_attempted ? (nvs_ok ? "ok" : "fail") : "skipped"); - return sd_ok || nvs_ok; - } - - void clearBlob() override - { - (void)removeSdFile(kIdfNodesFile); - (void)saveNvsBlob(kIdfNodesNvsKey, "node", nullptr, 0, kIdfMaxNodeFileBytes); - } - - private: - bool loadBlobFromNvs(std::vector& out) - { - if (!loadNvsBlob(kIdfNodesNvsKey, "node", out, kIdfMaxNodeFileBytes)) - { - out.clear(); - return false; - } - if (!chat::contacts::isValidNodeBlobSize(out.size()) || - chat::contacts::nodeBlobEntryCount(out.size()) > - chat::contacts::NodeStoreCore::kMaxNodes) - { - ESP_LOGW(kIdfStoreTag, - "node nvs load invalid len=%u", - static_cast(out.size())); - out.clear(); - return false; - } - - std::vector entries; - if (!chat::contacts::NodeStoreCore::decodeBlob(entries, out.data(), out.size())) - { - ESP_LOGW(kIdfStoreTag, - "node nvs decode failed len=%u", - static_cast(out.size())); - out.clear(); - return false; - } - - chat::contacts::NodeStoreCore::encodeBlob(out, entries); - ESP_LOGI(kIdfStoreTag, - "node load source=nvs count=%u len=%u", - static_cast(entries.size()), - static_cast(out.size())); - return !out.empty(); - } -}; - -class IdfSdContactBlobStore final : public chat::IContactBlobStore -{ - public: - bool loadBlob(std::vector& out) override - { - const bool ok = readSdFile(kIdfContactsFile, out, kIdfMaxContactBlobBytes); - if (ok) - { - ESP_LOGI(kIdfStoreTag, - "contacts load source=sd path=%s len=%u", - kIdfContactsFile, - static_cast(out.size())); - if (isValidContactBlobSize(out.size())) - { - return true; - } - ESP_LOGW(kIdfStoreTag, - "contacts sd load invalid len=%u", - static_cast(out.size())); - out.clear(); - } - - if (!loadNvsBlob(kIdfContactsNvsKey, "contacts", out, kIdfMaxContactBlobBytes)) - { - out.clear(); - return false; - } - if (!isValidContactBlobSize(out.size())) - { - ESP_LOGW(kIdfStoreTag, - "contacts nvs load invalid len=%u", - static_cast(out.size())); - out.clear(); - return false; - } - return true; - } - - bool saveBlob(const uint8_t* data, size_t len) override - { - if (len == 0) - { - (void)removeSdFile(kIdfContactsFile); - (void)saveNvsBlob(kIdfContactsNvsKey, "contacts", nullptr, 0, kIdfMaxContactBlobBytes); - return true; - } - if (!isValidContactBlobSize(len)) - { - return false; - } - - const bool sd_ok = writeSdFileAtomic(kIdfContactsFile, data, len); - const bool nvs_ok = - saveNvsBlob(kIdfContactsNvsKey, "contacts", data, len, kIdfMaxContactBlobBytes); - ESP_LOGI(kIdfStoreTag, - "contacts save path=%s len=%u sd=%u nvs=%u", - kIdfContactsFile, - static_cast(len), - sd_ok ? 1U : 0U, - nvs_ok ? 1U : 0U); - return sd_ok || nvs_ok; - } -}; - class IdfSdMeshPeerDirectoryBlobStore final : public chat::IMeshPeerDirectoryBlobStore { @@ -1223,12 +960,6 @@ class IdfAppFacadeRuntime final : public app::IAppFacade applyNetworkLimits(); applyPrivacyConfig(); - node_store_.setProtectedNodeChecker( - [this](uint32_t node_id) - { - return !contact_store_.getNickname(node_id).empty(); - }); - node_store_.setAutoSaveEnabled(false); contact_service_.begin(); chat_store_ = createIdfChatStore(); if (!chat_store_) @@ -1442,8 +1173,7 @@ class IdfAppFacadeRuntime final : public app::IAppFacade void clearNodeDb() override { - node_store_.clear(); - node_blob_store_.clearBlob(); + (void)mesh_peer_directory_.clearProtocol(config_.mesh_protocol); } void clearMessageDb() override @@ -1482,7 +1212,7 @@ class IdfAppFacadeRuntime final : public app::IAppFacade platform::ui::tracker::poll(); chat_service_->processIncoming(); chat_service_->flushStore(); - flushNodeStoreIfDue(); + flushPeerDirectoryIfDue(); if (team_service_) { team_service_->processIncoming(); @@ -1709,18 +1439,15 @@ class IdfAppFacadeRuntime final : public app::IAppFacade return null_mesh_adapter_; } - void flushNodeStoreIfDue() + void flushPeerDirectoryIfDue() { const uint32_t now_ms = static_cast(esp_timer_get_time() / 1000ULL); - if ((now_ms - last_node_store_flush_ms_) < kIdfNodeStoreFlushIntervalMs) + if ((now_ms - last_peer_directory_flush_ms_) < + kIdfPeerDirectoryFlushIntervalMs) { return; } - last_node_store_flush_ms_ = now_ms; - if (!node_store_.flush()) - { - ESP_LOGW(kIdfStoreTag, "node flush failed"); - } + last_peer_directory_flush_ms_ = now_ms; if (mesh_peer_directory_ready_ && !mesh_peer_directory_.flush().succeeded()) { @@ -1872,13 +1599,9 @@ class IdfAppFacadeRuntime final : public app::IAppFacade BoardBase* board_ = nullptr; LoraBoard* lora_board_ = nullptr; app::AppConfig config_{}; - IdfSdNodeBlobStore node_blob_store_{}; - IdfSdContactBlobStore contact_blob_store_{}; IdfSdMeshPeerDirectoryBlobStore mesh_peer_directory_blob_store_{}; - chat::contacts::NodeStoreCore node_store_{node_blob_store_}; - chat::contacts::ContactStoreCore contact_store_{contact_blob_store_}; chat::MeshPeerDirectoryCore mesh_peer_directory_{mesh_peer_directory_blob_store_}; - chat::contacts::ContactService contact_service_{node_store_, contact_store_}; + chat::contacts::ContactService contact_service_{mesh_peer_directory_}; chat::ChatModel chat_model_{}; std::unique_ptr chat_store_{}; IdfNullMeshAdapter null_mesh_adapter_{}; @@ -1902,7 +1625,7 @@ class IdfAppFacadeRuntime final : public app::IAppFacade chat::ui::IChatUiRuntime* chat_ui_runtime_ = nullptr; bool mesh_peer_directory_ready_ = false; bool background_tasks_started_ = false; - uint32_t last_node_store_flush_ms_ = 0; + uint32_t last_peer_directory_flush_ms_ = 0; }; IdfAppFacadeRuntime s_runtime{}; diff --git a/apps/linux_sim_shell/CMakeLists.txt b/apps/linux_sim_shell/CMakeLists.txt index cdd3799c..6cfe2c8f 100644 --- a/apps/linux_sim_shell/CMakeLists.txt +++ b/apps/linux_sim_shell/CMakeLists.txt @@ -109,6 +109,7 @@ if(BUILD_TESTING) "${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/mesh_peer_directory_core.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/usecase/contact_service.cpp" @@ -386,28 +387,15 @@ if(BUILD_TESTING) add_test(NAME trailmate_chat_delivery_event_projection_adapter_smoke COMMAND trailmate_chat_delivery_event_projection_adapter_smoke) - add_executable(trailmate_node_store_core_smoke - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/tests/test_node_store_core.cpp" - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/contact_store_core.cpp" - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/node_store_blob_format.cpp" - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/node_store_core.cpp" + add_executable(trailmate_mesh_peer_directory_contract_smoke + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/tests/test_mesh_peer_directory_contract.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/mesh_peer_directory_core.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/usecase/contact_service.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_sys/src/sys/clock.cpp") - target_include_directories(trailmate_node_store_core_smoke + target_include_directories(trailmate_mesh_peer_directory_contract_smoke PRIVATE "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/include" "${TRAIL_MATE_REPO_ROOT}/modules/core_sys/include") - target_compile_features(trailmate_node_store_core_smoke - PRIVATE cxx_std_17) - add_test(NAME trailmate_node_store_core_smoke - COMMAND trailmate_node_store_core_smoke) - - add_executable(trailmate_mesh_peer_directory_contract_smoke - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/tests/test_mesh_peer_directory_contract.cpp" - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/mesh_peer_directory_core.cpp") - target_include_directories(trailmate_mesh_peer_directory_contract_smoke - PRIVATE - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/include") target_compile_features(trailmate_mesh_peer_directory_contract_smoke PRIVATE cxx_std_17) add_test(NAME trailmate_mesh_peer_directory_contract_smoke @@ -765,6 +753,7 @@ if(BUILD_TESTING) "${TRAIL_MATE_REPO_ROOT}/modules/ui_key_verification_runtime/tests/test_key_verification_runtime_adapters.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/ui_key_verification_runtime/src/key_verification_action_sink.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/ui_key_verification_runtime/src/key_verification_presentation_source.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/mesh_peer_directory_core.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/usecase/contact_service.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_sys/src/sys/clock.cpp") target_include_directories(trailmate_key_verification_runtime_adapters_smoke 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 a9218663..fc181f41 100644 --- a/apps/nrf52_node/src/nrf52_node_app_facade_runtime.cpp +++ b/apps/nrf52_node/src/nrf52_node_app_facade_runtime.cpp @@ -1,16 +1,16 @@ -#include "nrf52_node_app_facade_runtime.h" +#include "nrf52_node_app_facade_runtime.h" #include "app/app_facade_access.h" #include "chat/domain/chat_model.h" #include "chat/infra/mesh_adapter_router_core.h" +#include "chat/infra/mesh_peer_directory_core.h" #include "chat/infra/mesh_protocol_utils.h" #include "chat/infra/store/ram_store.h" #include "chat/runtime/self_identity_provider.h" #include "chat/usecase/chat_service.h" #include "chat/usecase/contact_service.h" -#include "platform/nrf52/arduino_common/chat/infra/contact_store.h" +#include "platform/nrf52/arduino_common/chat/infra/blob_file_store.h" #include "platform/nrf52/arduino_common/chat/infra/meshtastic/meshtastic_radio_adapter.h" -#include "platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h" #include "platform/nrf52/arduino_common/chat/infra/radio_packet_io.h" #include "platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h" #include "platform/nrf52/arduino_common/device_identity.h" @@ -213,31 +213,28 @@ bool AppFacadeRuntime::installMeshBackend(chat::MeshProtocol protocol, void AppFacadeRuntime::initializeStores() { - if (node_store_ && contact_store_ && contact_service_) + if (mesh_peer_directory_ && contact_service_) { return; } - auto node_store = std::unique_ptr( - new platform::nrf52::arduino_common::chat::meshtastic::NodeStore()); - auto contact_store = std::unique_ptr( - new platform::nrf52::arduino_common::chat::infra::ContactStore()); - platform::nrf52::arduino_common::chat::infra::ContactStore* contact_store_ptr = contact_store.get(); - node_store->setProtectedNodeChecker([contact_store_ptr](uint32_t node_id) - { return contact_store_ptr && contact_store_ptr->hasContactNode(node_id); }); - node_store_ = std::move(node_store); - contact_store_ = std::move(contact_store); - contact_service_ = std::unique_ptr( - new chat::contacts::ContactService(*node_store_, *contact_store_)); + using PeerBlobStore = + platform::nrf52::arduino_common::chat::infra:: + MeshPeerDirectoryBlobFileStore; + mesh_peer_directory_blob_store_ = + std::unique_ptr( + new PeerBlobStore("/mesh_peers.bin")); - if (node_store_) - { - node_store_->begin(); - } - if (contact_store_) - { - contact_store_->begin(); - } + chat::MeshPeerDirectoryCore::Options options{}; + options.meshtastic_capacity = {200, 16}; + options.meshcore_capacity = {64, 32}; + options.reticulum_capacity = {32, 16}; + options.auto_save = false; + mesh_peer_directory_ = std::unique_ptr( + new chat::MeshPeerDirectoryCore(*mesh_peer_directory_blob_store_, + options)); + contact_service_ = std::unique_ptr( + new chat::contacts::ContactService(*mesh_peer_directory_)); if (contact_service_) { contact_service_->begin(); @@ -261,10 +258,12 @@ void AppFacadeRuntime::initializeChatRuntime() (void)installMeshBackend(chat::MeshProtocol::Meshtastic, platform::nrf52::protocol::createProtocolAdapter(chat::MeshProtocol::Meshtastic, identityProvider(), - static_cast(node_store_.get()), contact_service_.get())); (void)installMeshBackend(chat::MeshProtocol::MeshCore, - platform::nrf52::protocol::createProtocolAdapter(chat::MeshProtocol::MeshCore, identityProvider())); + platform::nrf52::protocol::createProtocolAdapter( + chat::MeshProtocol::MeshCore, + identityProvider(), + contact_service_.get())); applyMeshConfig(); applyUserInfo(); @@ -407,8 +406,7 @@ chat::NodeId AppFacadeRuntime::resolveSelfNodeId() const NRF_FICR->DEVICEADDR[0], NRF_FICR->DEVICEADDR[1], NRF_FICR->DEVICEID[0], - NRF_FICR->DEVICEID[1], - node_store_.get()); + NRF_FICR->DEVICEID[1]); } void AppFacadeRuntime::applyNetworkLimits() @@ -582,9 +580,9 @@ void AppFacadeRuntime::broadcastNodeInfo() void AppFacadeRuntime::clearNodeDb() { - if (node_store_) + if (mesh_peer_directory_) { - node_store_->clear(); + (void)mesh_peer_directory_->clearProtocol(config_.mesh_protocol); } if (contact_service_) { @@ -604,22 +602,10 @@ bool AppFacadeRuntime::clearVolatileStoragePreserveSettings() { clearNodeDb(); - bool contact_ok = true; - if (contact_store_) - { - auto* nrf_contact_store = - static_cast(contact_store_.get()); - contact_ok = nrf_contact_store->clear(); - } - if (contact_service_) - { - contact_service_->clearCache(); - } - clearMessageDb(); const bool fs_ok = platform::nrf52::arduino_common::internal_fs::removeVolatileArtifactsPreserveSettings( "[nrf52][storage]"); - return contact_ok && fs_ok; + return fs_ok; } ble::BleManager* AppFacadeRuntime::getBleManager() @@ -688,16 +674,6 @@ void AppFacadeRuntime::restartDevice() NVIC_SystemReset(); } -chat::contacts::INodeStore* AppFacadeRuntime::getNodeStore() -{ - return node_store_.get(); -} - -const chat::contacts::INodeStore* AppFacadeRuntime::getNodeStore() const -{ - return node_store_.get(); -} - bool AppFacadeRuntime::getDeviceMacAddress(uint8_t out_mac[6]) const { if (!out_mac) @@ -768,9 +744,9 @@ void AppFacadeRuntime::updateCoreServices() if ((now_ms - last_chat_store_flush_ms_) >= kChatStoreFlushIntervalMs) { chat_service_->flushStore(); - if (node_store_) + if (mesh_peer_directory_) { - (void)node_store_->flush(); + (void)mesh_peer_directory_->flush(); } if (auto* mt = getMeshtasticBackend(getMeshAdapter())) { @@ -824,7 +800,7 @@ void AppFacadeRuntime::syncSelfPositionFromGps() return; } - const ::chat::contacts::NodeInfo* existing = contact_service_->getNodeInfo(effective_identity_.node_id); + const ::chat::contacts::PeerDirectoryItem* existing = contact_service_->getPeerByNodeId(effective_identity_.node_id); if (existing && existing->position.valid && existing->position.latitude_i == position.latitude_i && existing->position.longitude_i == position.longitude_i && diff --git a/apps/nrf52_node/src/nrf52_node_app_facade_runtime.h b/apps/nrf52_node/src/nrf52_node_app_facade_runtime.h index d237ccd9..7b8c051e 100644 --- a/apps/nrf52_node/src/nrf52_node_app_facade_runtime.h +++ b/apps/nrf52_node/src/nrf52_node_app_facade_runtime.h @@ -23,10 +23,10 @@ class ChatModel; class ChatService; class IChatStore; class IMeshAdapter; +class IMeshPeerDirectory; +class IMeshPeerDirectoryBlobStore; namespace contacts { -class INodeStore; -class IContactStore; class ContactService; } // namespace contacts } // namespace chat @@ -92,8 +92,6 @@ class AppFacadeRuntime final : public app::IAppBleFacade bool isBleEnabled() const override; void setBleEnabled(bool enabled) override; void restartDevice() override; - chat::contacts::INodeStore* getNodeStore() override; - const chat::contacts::INodeStore* getNodeStore() const override; bool getDeviceMacAddress(uint8_t out_mac[6]) const override; bool syncCurrentEpochSeconds(uint32_t epoch_seconds) override; void resetMeshConfig() override; @@ -136,8 +134,9 @@ class AppFacadeRuntime final : public app::IAppBleFacade std::unique_ptr identity_bridge_; mutable chat::runtime::EffectiveSelfIdentity effective_identity_{}; - std::unique_ptr node_store_; - std::unique_ptr contact_store_; + std::unique_ptr + mesh_peer_directory_blob_store_; + std::unique_ptr mesh_peer_directory_; std::unique_ptr contact_service_; std::unique_ptr chat_model_; std::unique_ptr chat_store_; diff --git a/boards/gat562_mesh_evb_pro/src/platform_ui_bindings.cpp b/boards/gat562_mesh_evb_pro/src/platform_ui_bindings.cpp index 75b7a1dc..6a28940b 100644 --- a/boards/gat562_mesh_evb_pro/src/platform_ui_bindings.cpp +++ b/boards/gat562_mesh_evb_pro/src/platform_ui_bindings.cpp @@ -290,7 +290,7 @@ GpsState get_data() return gps; } - const ::chat::contacts::NodeInfo* self = facade.getContactService().getNodeInfo(self_id); + const ::chat::contacts::PeerDirectoryItem* self = facade.getContactService().getPeerByNodeId(self_id); if (!self || !self->position.valid) { return gps; diff --git a/boards/t_echo_lite/src/platform_ui_bindings.cpp b/boards/t_echo_lite/src/platform_ui_bindings.cpp index 2727b359..6b1c9e28 100644 --- a/boards/t_echo_lite/src/platform_ui_bindings.cpp +++ b/boards/t_echo_lite/src/platform_ui_bindings.cpp @@ -301,7 +301,7 @@ GpsState get_data() return gps; } - const ::chat::contacts::NodeInfo* self = facade.getContactService().getNodeInfo(self_id); + const ::chat::contacts::PeerDirectoryItem* self = facade.getContactService().getPeerByNodeId(self_id); if (!self || !self->position.valid) { return gps; diff --git a/builds/esp_idf/ESP_IDF_COMPONENT_SOURCES.cmake b/builds/esp_idf/ESP_IDF_COMPONENT_SOURCES.cmake index a13f5f5c..507c240c 100644 --- a/builds/esp_idf/ESP_IDF_COMPONENT_SOURCES.cmake +++ b/builds/esp_idf/ESP_IDF_COMPONENT_SOURCES.cmake @@ -63,7 +63,6 @@ set(TRAILMATE_ESP_IDF_CORE_CHAT_SOURCES "${TRAILMATE_ROOT}/modules/core_chat/src/delivery/chat_delivery_send_result_projection.cpp" "${TRAILMATE_ROOT}/modules/core_chat/src/delivery/chat_message_ledger.cpp" "${TRAILMATE_ROOT}/modules/core_chat/src/delivery/chat_outbox_service.cpp" - "${TRAILMATE_ROOT}/modules/core_chat/src/infra/contact_store_core.cpp" "${TRAILMATE_ROOT}/modules/core_chat/src/infra/mesh_adapter_router_core.cpp" "${TRAILMATE_ROOT}/modules/core_chat/src/infra/mesh_protocol_utils.cpp" "${TRAILMATE_ROOT}/modules/core_chat/src/infra/meshcore/mc_region_presets.cpp" @@ -88,7 +87,6 @@ set(TRAILMATE_ESP_IDF_CORE_CHAT_SOURCES "${TRAILMATE_ROOT}/modules/core_chat/src/infra/meshtastic/mt_radio_config.cpp" "${TRAILMATE_ROOT}/modules/core_chat/src/infra/meshtastic/mt_region.cpp" "${TRAILMATE_ROOT}/modules/core_chat/src/infra/node_store_blob_format.cpp" - "${TRAILMATE_ROOT}/modules/core_chat/src/infra/node_store_core.cpp" "${TRAILMATE_ROOT}/modules/core_chat/src/infra/mesh_peer_directory_core.cpp" "${TRAILMATE_ROOT}/modules/core_chat/src/infra/lxmf/lxmf_wire.cpp" "${TRAILMATE_ROOT}/modules/core_chat/src/infra/reticulum/audio_call_wire.cpp" diff --git a/cmake/TrailMateLinuxSources.cmake b/cmake/TrailMateLinuxSources.cmake index 66672470..2c5eec7f 100644 --- a/cmake/TrailMateLinuxSources.cmake +++ b/cmake/TrailMateLinuxSources.cmake @@ -184,7 +184,6 @@ set(TRAIL_MATE_LINUX_COMMON_SOURCES "${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_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/infra/contact_store_core.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/mesh_protocol_utils.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/meshcore/mc_region_presets.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/meshcore/meshcore_identity_crypto.cpp" @@ -206,8 +205,6 @@ set(TRAIL_MATE_LINUX_COMMON_SOURCES "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/meshtastic/mt_protocol_helpers.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/meshtastic/mt_radio_config.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/meshtastic/mt_region.cpp" - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/node_store_blob_format.cpp" - "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/node_store_core.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/store/ram_store.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/usecase/chat_service.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/usecase/contact_service.cpp" diff --git a/modules/core_chat/include/chat/domain/contact_types.h b/modules/core_chat/include/chat/domain/contact_types.h index 8ccd9e8f..5c0602fd 100644 --- a/modules/core_chat/include/chat/domain/contact_types.h +++ b/modules/core_chat/include/chat/domain/contact_types.h @@ -142,9 +142,9 @@ struct NodeUpdate }; /** - * @brief Base node information + * @brief UI-facing projection of one unified peer-directory record. */ -struct NodeInfoBase +struct PeerDirectoryItem { uint32_t node_id; char short_name[10]; @@ -172,24 +172,7 @@ struct NodeInfoBase NodePosition position; }; -/** - * @brief Meshtastic-specific node info (reserved for future extensions) - */ -struct MeshtasticNodeInfo : public NodeInfoBase -{ -}; - -/** - * @brief MeshCore-specific node info (reserved for future extensions) - */ -struct MeshCoreNodeInfo : public NodeInfoBase -{ -}; - -/** - * @brief Node information (from mesh network) - */ -using NodeInfo = NodeInfoBase; +static constexpr uint8_t kNodeRoleUnknown = 0xFF; } // namespace contacts } // namespace chat diff --git a/modules/core_chat/include/chat/domain/mesh_peer_directory.h b/modules/core_chat/include/chat/domain/mesh_peer_directory.h index f2e3a88d..4c5c7231 100644 --- a/modules/core_chat/include/chat/domain/mesh_peer_directory.h +++ b/modules/core_chat/include/chat/domain/mesh_peer_directory.h @@ -132,6 +132,9 @@ struct MeshPeerRecord ReticulumPeerFacts reticulum{}; }; +MeshPeerRecord mergeMeshPeerRecordFacts(const MeshPeerRecord& existing, + const MeshPeerRecord& incoming); + inline bool meshPeerIsReticulumProtocol(MeshProtocol protocol) { return protocol == MeshProtocol::Reticulum || protocol == MeshProtocol::RNode; @@ -242,6 +245,51 @@ inline bool meshPeerRecordIsValid(const MeshPeerRecord& record) return record.valid && meshPeerIdentityIsValid(record.identity); } +inline bool meshPeerIsStableContactIdentity(const MeshPeerIdentity& identity) +{ + if (!meshPeerIdentityIsValid(identity)) + { + return false; + } + if (identity.protocol == MeshProtocol::Meshtastic) + { + return identity.kind == MeshPeerIdentityKind::NodeId; + } + if (identity.protocol == MeshProtocol::MeshCore) + { + return identity.kind == MeshPeerIdentityKind::PublicKey; + } + return meshPeerIsReticulumProtocol(identity.protocol) && + identity.kind == MeshPeerIdentityKind::ReticulumDestination; +} + +inline NodeId meshPeerProjectedNodeId(const MeshPeerRecord& record) +{ + if (record.identity.kind == MeshPeerIdentityKind::NodeId) + { + return record.identity.node_id; + } + if (record.identity.protocol == MeshProtocol::MeshCore) + { + return record.meshcore.node_id_hint; + } + if (meshPeerIsReticulumProtocol(record.identity.protocol) && + record.identity.reticulum.valid) + { + const uint8_t* hash = record.identity.reticulum.destination_hash; + return (static_cast(hash[12]) << 24) | + (static_cast(hash[13]) << 16) | + (static_cast(hash[14]) << 8) | + static_cast(hash[15]); + } + return 0; +} + +inline bool meshPeerIsContact(const MeshPeerRecord& record) +{ + return record.flags.favorite || record.user_alias[0] != '\0'; +} + inline void copyMeshPeerText(char* out, std::size_t out_len, const char* text) { if (!out || out_len == 0) diff --git a/modules/core_chat/include/chat/infra/contact_store_core.h b/modules/core_chat/include/chat/infra/contact_store_core.h deleted file mode 100644 index 8e39bc1b..00000000 --- a/modules/core_chat/include/chat/infra/contact_store_core.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include "../ports/i_contact_blob_store.h" -#include "../ports/i_contact_store.h" -#include "chat/infra/node_store_core.h" -#include -#include -#include -#include - -namespace chat -{ -namespace contacts -{ - -class ContactStoreCore : public IContactStore -{ - public: - struct Entry - { - uint32_t node_id = 0; - char nickname[13] = {0}; - }; - - static constexpr size_t kMaxContacts = NodeStoreCore::kMaxNodes; - static constexpr size_t kSerializedEntrySize = sizeof(Entry); - - explicit ContactStoreCore(IContactBlobStore& blob_store); - - void begin() override; - std::string getNickname(uint32_t node_id) const override; - bool setNickname(uint32_t node_id, const char* nickname) override; - bool removeNickname(uint32_t node_id) override; - bool hasNickname(const char* nickname) const override; - std::vector getAllContactIds() const override; - size_t getCount() const override; - bool clear(); - - private: - bool loadEntries(); - bool saveEntries(); - bool decodeEntries(const uint8_t* data, size_t len); - void encodeEntries(std::vector& out) const; - - IContactBlobStore& blob_store_; - std::vector entries_; -}; - -} // namespace contacts -} // namespace chat diff --git a/modules/core_chat/include/chat/infra/mesh_peer_directory_core.h b/modules/core_chat/include/chat/infra/mesh_peer_directory_core.h index c522df49..142c1df2 100644 --- a/modules/core_chat/include/chat/infra/mesh_peer_directory_core.h +++ b/modules/core_chat/include/chat/infra/mesh_peer_directory_core.h @@ -10,9 +10,6 @@ namespace chat { -MeshPeerRecord mergeMeshPeerRecordFacts(const MeshPeerRecord& existing, - const MeshPeerRecord& incoming); - class MeshPeerDirectoryCore final : public IMeshPeerDirectory { public: @@ -48,9 +45,19 @@ class MeshPeerDirectoryCore final : public IMeshPeerDirectory MeshPeerRecord* out_records, std::size_t max_records, std::size_t* out_count) override; + MeshPeerDirectoryStatus visit( + MeshProtocol protocol, + MeshPeerDirectoryView view, + IMeshPeerDirectoryVisitor& visitor) override; + MeshPeerDirectoryStatus setUserAlias( + const MeshPeerIdentity& identity, + const char* alias) override; MeshPeerDirectoryStatus setUserFlags( const MeshPeerIdentity& identity, const MeshPeerUserFlags& flags) override; + MeshPeerDirectoryStatus setKeyManuallyVerified( + const MeshPeerIdentity& identity, + bool verified) override; MeshPeerDirectoryStatus remove(const MeshPeerIdentity& identity) override; MeshPeerDirectoryStatus clearProtocol(MeshProtocol protocol) override; MeshPeerDirectoryCapacity capacityFor(MeshProtocol protocol) const override; diff --git a/modules/core_chat/include/chat/infra/node_store_blob_format.h b/modules/core_chat/include/chat/infra/node_store_blob_format.h deleted file mode 100644 index 22287e18..00000000 --- a/modules/core_chat/include/chat/infra/node_store_blob_format.h +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @file node_store_blob_format.h - * @brief Shared node-store blob format helpers - */ - -#pragma once - -#include -#include - -#if defined(_MSC_VER) -#define TRAILMATE_PACK_PUSH __pragma(pack(push, 1)) -#define TRAILMATE_PACK_POP __pragma(pack(pop)) -#define TRAILMATE_PACKED -#else -#define TRAILMATE_PACK_PUSH -#define TRAILMATE_PACK_POP -#define TRAILMATE_PACKED __attribute__((packed)) -#endif - -namespace chat -{ -namespace contacts -{ - -enum class NodeBlobValidation -{ - Ok, - Empty, - StaleMetadata, - InvalidLength, - MissingCrc, - VersionMismatch, - CrcMismatch, - TooManyEntries, -}; - -TRAILMATE_PACK_PUSH -struct NodeStoreSdHeader -{ - uint8_t ver; - uint8_t reserved[3]; - uint32_t crc; - uint32_t count; -} TRAILMATE_PACKED; -TRAILMATE_PACK_POP - -bool isValidNodeBlobSize(size_t len); -size_t nodeBlobEntryCount(size_t len); -size_t nodeBlobByteSize(size_t count); -size_t nodeBlobEntrySizeForVersion(uint8_t version); -NodeStoreSdHeader makeNodeStoreSdHeader(const uint8_t* data, size_t len); -NodeBlobValidation validateNodeBlobMetadata(size_t len, uint8_t version, - bool has_crc, uint32_t stored_crc, - const uint8_t* data); -NodeBlobValidation validateNodeStoreSdHeader(const NodeStoreSdHeader& header); -NodeBlobValidation validateNodeStoreSdBlob(const NodeStoreSdHeader& header, - const uint8_t* data, size_t len); - -} // namespace contacts -} // namespace chat - -#undef TRAILMATE_PACK_PUSH -#undef TRAILMATE_PACK_POP -#undef TRAILMATE_PACKED diff --git a/modules/core_chat/include/chat/infra/node_store_core.h b/modules/core_chat/include/chat/infra/node_store_core.h deleted file mode 100644 index fe4f1c9c..00000000 --- a/modules/core_chat/include/chat/infra/node_store_core.h +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include "../ports/i_node_blob_store.h" -#include "../ports/i_node_store.h" -#include -#include -#include -#include - -namespace chat -{ -namespace contacts -{ - -class NodeStoreCore : public INodeStore -{ - public: - static constexpr size_t kMaxNodes = 200; - static constexpr size_t kSerializedEntrySizeV8 = 144; - static constexpr size_t kSerializedEntrySizeV9 = 180; - static constexpr size_t kSerializedEntrySize = kSerializedEntrySizeV9; - static constexpr uint8_t kPersistVersionV8 = 8; - static constexpr uint8_t kPersistVersion = 9; - static constexpr uint32_t kSaveIntervalMs = 5000; - - explicit NodeStoreCore(INodeBlobStore& blob_store); - void setProtectedNodeChecker(std::function checker); - void setAutoSaveEnabled(bool enabled); - - void begin() override; - void applyUpdate(uint32_t node_id, const NodeUpdate& update) override; - void upsert(uint32_t node_id, const char* short_name, const char* long_name, - uint32_t now_secs, float snr = 0.0f, float rssi = 0.0f, uint8_t protocol = 0, - uint8_t role = kNodeRoleUnknown, uint8_t hops_away = 0xFF, - uint8_t hw_model = 0, uint8_t channel = 0xFF) override; - void updateProtocol(uint32_t node_id, uint8_t protocol, uint32_t now_secs) override; - void updatePosition(uint32_t node_id, const NodePosition& position) override; - bool setNextHop(uint32_t node_id, uint8_t next_hop, uint32_t now_secs); - uint8_t getNextHop(uint32_t node_id) const; - bool remove(uint32_t node_id) override; - const std::vector& getEntries() const override; - void clear() override; - bool flush() override; - - static uint32_t computeBlobCrc(const uint8_t* data, size_t len); - static bool decodeBlob(std::vector& out, const uint8_t* data, size_t len, uint8_t persist_version = kPersistVersion); - static void encodeBlob(std::vector& out, const std::vector& entries); - - private: - bool loadEntries(); - bool saveEntries(); - bool decodeEntries(const uint8_t* data, size_t len); - void encodeEntries(std::vector& out) const; - void maybeSave(); - size_t selectEvictionIndex() const; - - INodeBlobStore& blob_store_; - std::vector entries_; - uint32_t last_save_ms_ = 0; - bool dirty_ = false; - bool auto_save_enabled_ = true; - std::function protected_node_checker_; -}; - -} // namespace contacts -} // namespace chat diff --git a/modules/core_chat/include/chat/ports/i_contact_blob_store.h b/modules/core_chat/include/chat/ports/i_contact_blob_store.h deleted file mode 100644 index 3be47240..00000000 --- a/modules/core_chat/include/chat/ports/i_contact_blob_store.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace chat -{ - -class IContactBlobStore -{ - public: - virtual ~IContactBlobStore() = default; - - virtual bool loadBlob(std::vector& out) = 0; - virtual bool saveBlob(const uint8_t* data, size_t len) = 0; -}; - -} // namespace chat diff --git a/modules/core_chat/include/chat/ports/i_contact_store.h b/modules/core_chat/include/chat/ports/i_contact_store.h deleted file mode 100644 index e2eb301a..00000000 --- a/modules/core_chat/include/chat/ports/i_contact_store.h +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @file i_contact_store.h - * @brief Contact store interface - */ - -#pragma once - -#include "chat/domain/chat_types.h" - -#include -#include -#include - -namespace chat -{ -namespace contacts -{ - -/** - * @brief Contact store interface - * Abstracts contact nickname storage implementation - */ -class IContactStore -{ - public: - virtual ~IContactStore() = default; - - virtual void setActiveProtocol(MeshProtocol protocol) - { - (void)protocol; - } - - /** - * @brief Initialize store (load from persistent storage) - */ - virtual void begin() = 0; - - /** - * @brief Get nickname for a node_id - * @param node_id Node ID - * @return Nickname if found, empty string otherwise - */ - virtual std::string getNickname(uint32_t node_id) const = 0; - - /** - * @brief Set nickname for a node_id - * @param node_id Node ID - * @param nickname Nickname (max 12 bytes) - * @return true if successful, false if duplicate name or storage full - */ - virtual bool setNickname(uint32_t node_id, const char* nickname) = 0; - - /** - * @brief Remove nickname for a node_id - * @param node_id Node ID - * @return true if removed, false if not found - */ - virtual bool removeNickname(uint32_t node_id) = 0; - - /** - * @brief Check if a nickname already exists - * @param nickname Nickname to check - * @return true if duplicate - */ - virtual bool hasNickname(const char* nickname) const = 0; - - /** - * @brief Get all contact node IDs - * @return Vector of node IDs - */ - virtual std::vector getAllContactIds() const = 0; - - /** - * @brief Get number of contacts - */ - virtual size_t getCount() const = 0; -}; - -} // namespace contacts -} // namespace chat diff --git a/modules/core_chat/include/chat/ports/i_mesh_peer_directory.h b/modules/core_chat/include/chat/ports/i_mesh_peer_directory.h index a2feab55..7bcc2e82 100644 --- a/modules/core_chat/include/chat/ports/i_mesh_peer_directory.h +++ b/modules/core_chat/include/chat/ports/i_mesh_peer_directory.h @@ -45,8 +45,29 @@ struct MeshPeerDirectoryStatus struct MeshPeerDirectoryCapacity { - std::size_t persisted_records = 0; - std::size_t hot_cache_records = 0; + constexpr MeshPeerDirectoryCapacity(std::size_t persisted = 0, + std::size_t hot_cache = 0) + : persisted_records(persisted), hot_cache_records(hot_cache) + { + } + + std::size_t persisted_records; + std::size_t hot_cache_records; +}; + +enum class MeshPeerDirectoryView : uint8_t +{ + All = 0, + Contacts = 1, + Nearby = 2, + Ignored = 3, +}; + +class IMeshPeerDirectoryVisitor +{ + public: + virtual ~IMeshPeerDirectoryVisitor() = default; + virtual bool visit(const MeshPeerRecord& record) = 0; }; class IMeshPeerDirectory @@ -70,9 +91,19 @@ class IMeshPeerDirectory MeshPeerRecord* out_records, std::size_t max_records, std::size_t* out_count) = 0; + virtual MeshPeerDirectoryStatus visit( + MeshProtocol protocol, + MeshPeerDirectoryView view, + IMeshPeerDirectoryVisitor& visitor) = 0; + virtual MeshPeerDirectoryStatus setUserAlias( + const MeshPeerIdentity& identity, + const char* alias) = 0; virtual MeshPeerDirectoryStatus setUserFlags( const MeshPeerIdentity& identity, const MeshPeerUserFlags& flags) = 0; + virtual MeshPeerDirectoryStatus setKeyManuallyVerified( + const MeshPeerIdentity& identity, + bool verified) = 0; virtual MeshPeerDirectoryStatus remove(const MeshPeerIdentity& identity) = 0; virtual MeshPeerDirectoryStatus clearProtocol(MeshProtocol protocol) = 0; virtual MeshPeerDirectoryCapacity capacityFor(MeshProtocol protocol) const = 0; diff --git a/modules/core_chat/include/chat/ports/i_node_blob_store.h b/modules/core_chat/include/chat/ports/i_node_blob_store.h deleted file mode 100644 index 92a0304c..00000000 --- a/modules/core_chat/include/chat/ports/i_node_blob_store.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace chat -{ -namespace contacts -{ - -class INodeBlobStore -{ - public: - virtual ~INodeBlobStore() = default; - - virtual bool loadBlob(std::vector& out) = 0; - virtual bool saveBlob(const uint8_t* data, size_t len) = 0; - virtual void clearBlob() = 0; -}; - -} // namespace contacts -} // namespace chat diff --git a/modules/core_chat/include/chat/ports/i_node_store.h b/modules/core_chat/include/chat/ports/i_node_store.h deleted file mode 100644 index 3424fa77..00000000 --- a/modules/core_chat/include/chat/ports/i_node_store.h +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @file i_node_store.h - * @brief Node store interface - */ - -#pragma once - -#include "chat/domain/chat_types.h" -#include "chat/domain/contact_types.h" -#include -#include - -namespace chat -{ -namespace contacts -{ - -/** - * @brief Node entry structure - */ -struct NodeEntry -{ - uint32_t node_id; - char short_name[10]; - char long_name[32]; - uint32_t last_seen; // Unix timestamp (seconds) - float snr; // Signal-to-Noise Ratio - float rssi; // RSSI in dBm - uint8_t hops_away = 0xFF; - uint8_t channel = 0xFF; // Meshtastic channel index; 0xFF = unknown - uint8_t next_hop = 0; // Meshtastic learned next-hop relay hint; 0 = unknown/flooding - uint8_t protocol; // NodeProtocolType - uint8_t role; // NodeRoleType (Meshtastic roles) - uint8_t hw_model; // Meshtastic_HardwareModel (0 = UNSET) - bool has_macaddr = false; - uint8_t macaddr[6] = {}; - bool via_mqtt = false; - bool is_ignored = false; - bool has_public_key = false; - bool key_manually_verified = false; - ReticulumPeerIdentity reticulum_identity{}; - bool has_device_metrics = false; - NodeDeviceMetrics device_metrics{}; - bool position_valid = false; - int32_t position_latitude_i = 0; - int32_t position_longitude_i = 0; - bool position_has_altitude = false; - int32_t position_altitude = 0; - uint32_t position_timestamp = 0; - uint32_t position_precision_bits = 0; - uint32_t position_pdop = 0; - uint32_t position_hdop = 0; - uint32_t position_vdop = 0; - uint32_t position_gps_accuracy_mm = 0; -}; - -static constexpr uint8_t kNodeRoleUnknown = 0xFF; - -/** - * @brief Node store interface - * Abstracts node information storage implementation - */ -class INodeStore -{ - public: - virtual ~INodeStore() = default; - - virtual void setActiveProtocol(MeshProtocol protocol) - { - (void)protocol; - } - - /** - * @brief Initialize store (load from persistent storage) - */ - virtual void begin() = 0; - - /** - * @brief Update or insert a node entry - */ - virtual void applyUpdate(uint32_t node_id, const NodeUpdate& update) = 0; - - virtual void upsert(uint32_t node_id, const char* short_name, const char* long_name, - uint32_t now_secs, float snr = 0.0f, float rssi = 0.0f, uint8_t protocol = 0, - uint8_t role = kNodeRoleUnknown, uint8_t hops_away = 0xFF, - uint8_t hw_model = 0, uint8_t channel = 0xFF) = 0; - - /** - * @brief Update node protocol (without changing names) - * @param node_id Node ID - * @param protocol Protocol type - * @param now_secs Current timestamp (seconds) - */ - virtual void updateProtocol(uint32_t node_id, uint8_t protocol, uint32_t now_secs) = 0; - - /** - * @brief Update node position info - * @param node_id Node ID - * @param position Latest known position - */ - virtual void updatePosition(uint32_t node_id, const NodePosition& position) = 0; - - /** - * @brief Remove a node entry by node ID - * @param node_id Node ID - * @return true if an entry was removed - */ - virtual bool remove(uint32_t node_id) = 0; - - /** - * @brief Get all entries (for iteration) - * @return Reference to entries vector - */ - virtual const std::vector& getEntries() const = 0; - - /** - * @brief Clear all stored node entries - */ - virtual void clear() = 0; - - /** - * @brief Flush any pending dirty state to persistent storage immediately - * @return true if storage is synced or there was nothing pending - */ - virtual bool flush() = 0; -}; - -} // namespace contacts -} // namespace chat diff --git a/modules/core_chat/include/chat/ports/i_protocol_peer_repository.h b/modules/core_chat/include/chat/ports/i_protocol_peer_repository.h index 95bbd19e..2545d8c0 100644 --- a/modules/core_chat/include/chat/ports/i_protocol_peer_repository.h +++ b/modules/core_chat/include/chat/ports/i_protocol_peer_repository.h @@ -1,8 +1,6 @@ #pragma once -#include "chat/ports/i_contact_store.h" #include "chat/ports/i_mesh_peer_directory.h" -#include "chat/ports/i_node_store.h" namespace chat { @@ -10,16 +8,12 @@ namespace chat /** * Canonical owner for protocol peer facts and user contact facts. * - * The three inherited ports are separate read/write views of one repository; - * they must never be backed by independent stores in the product runtime. + * Protocol observations and user-owned contact facts share one durable owner. */ class IProtocolPeerRepository : public IMeshPeerDirectory { public: ~IProtocolPeerRepository() override = default; - - virtual contacts::INodeStore& nodeStoreView() = 0; - virtual contacts::IContactStore& contactStoreView() = 0; }; } // namespace chat diff --git a/modules/core_chat/include/chat/usecase/contact_service.h b/modules/core_chat/include/chat/usecase/contact_service.h index 821b528f..f048602e 100644 --- a/modules/core_chat/include/chat/usecase/contact_service.h +++ b/modules/core_chat/include/chat/usecase/contact_service.h @@ -10,8 +10,7 @@ #pragma once #include "../domain/contact_types.h" -#include "../ports/i_contact_store.h" -#include "../ports/i_node_store.h" +#include "../ports/i_mesh_peer_directory.h" #include #include @@ -29,10 +28,9 @@ class ContactService public: /** * @brief Constructor with dependency injection - * @param node_store Node store implementation - * @param contact_store Contact store implementation + * @param directory Unified durable peer directory */ - ContactService(INodeStore& node_store, IContactStore& contact_store); + explicit ContactService(IMeshPeerDirectory& directory); ~ContactService() = default; /** @@ -44,9 +42,10 @@ class ContactService void setActiveProtocol(MeshProtocol protocol); void applyNodeUpdate(uint32_t node_id, const NodeUpdate& update); + bool recordPeer(const MeshPeerRecord& record); /** - * @brief Update node info from NodeInfo packet + * @brief Update node facts decoded from a protocol metadata packet * @param node_id Node ID * @param short_name Short name * @param long_name Long name @@ -67,6 +66,8 @@ class ContactService * @brief Update node position info */ void updateNodePosition(uint32_t node_id, const NodePosition& pos); + bool setNextHop(uint32_t node_id, uint8_t next_hop); + uint8_t getNextHop(uint32_t node_id) const; /** * @brief Get display name for a node (nickname if contact, short_name otherwise) @@ -85,17 +86,20 @@ class ContactService /** * @brief Get all contacts (nodes with nicknames) */ - std::vector getContacts() const; + std::vector getContacts() const; /** * @brief Get all nearby nodes (nodes without nicknames, visible within 6 days) */ - std::vector getNearby() const; + std::vector getNearby() const; /** * @brief Get ignored non-contact nodes so local admin UIs can still manage them */ - std::vector getIgnoredNodes() const; + std::vector getIgnoredNodes() const; + + /** Get the active protocol's complete peer-directory projection. */ + std::vector getAllPeers() const; /** * @brief Add contact (set nickname) @@ -146,9 +150,9 @@ class ContactService /** * @brief Get node info by node_id * @param node_id Node ID - * @return NodeInfo if found, nullptr otherwise + * @return Peer projection if found, nullptr otherwise */ - const NodeInfo* getNodeInfo(uint32_t node_id) const; + const PeerDirectoryItem* getPeerByNodeId(uint32_t node_id) const; bool findNodeIdByReticulumDestinationHash( const uint8_t destination_hash[kReticulumPeerHashSize], @@ -160,11 +164,12 @@ class ContactService void clearCache(); private: - INodeStore& node_store_; - IContactStore& contact_store_; + IMeshPeerDirectory& directory_; MeshProtocol active_protocol_ = MeshProtocol::Meshtastic; - mutable std::vector cached_nodes_; // Cache for getContacts/getNearby + mutable std::vector cached_nodes_; mutable uint32_t cache_timestamp_; + mutable MeshPeerRecord update_scratch_{}; + mutable MeshPeerRecord lookup_scratch_{}; static constexpr uint32_t kCacheTimeoutMs = 1000; // 1 second cache void invalidateCache() const; diff --git a/modules/core_chat/src/infra/contact_store_core.cpp b/modules/core_chat/src/infra/contact_store_core.cpp deleted file mode 100644 index 48b79978..00000000 --- a/modules/core_chat/src/infra/contact_store_core.cpp +++ /dev/null @@ -1,212 +0,0 @@ -#include "chat/infra/contact_store_core.h" - -#include - -namespace chat -{ -namespace contacts -{ - -namespace -{ - -bool isEmptyNickname(const char* nickname) -{ - return nickname == nullptr || nickname[0] == '\0'; -} - -} // namespace - -ContactStoreCore::ContactStoreCore(IContactBlobStore& blob_store) - : blob_store_(blob_store) -{ -} - -void ContactStoreCore::begin() -{ - if (!loadEntries()) - { - entries_.clear(); - } -} - -std::string ContactStoreCore::getNickname(uint32_t node_id) const -{ - for (const auto& entry : entries_) - { - if (entry.node_id == node_id) - { - return std::string(entry.nickname); - } - } - return std::string(); -} - -bool ContactStoreCore::setNickname(uint32_t node_id, const char* nickname) -{ - if (isEmptyNickname(nickname)) - { - return false; - } - - if (strlen(nickname) > 12) - { - return false; - } - - for (auto& entry : entries_) - { - if (entry.node_id == node_id) - { - if (strcmp(entry.nickname, nickname) == 0) - { - return true; - } - char previous[sizeof(entry.nickname)] = {}; - memcpy(previous, entry.nickname, sizeof(previous)); - previous[sizeof(previous) - 1] = '\0'; - const std::size_t nickname_len = strlen(nickname); - memcpy(entry.nickname, nickname, nickname_len + 1U); - if (saveEntries()) - { - return true; - } - memcpy(entry.nickname, previous, sizeof(entry.nickname)); - return false; - } - } - - if (entries_.size() >= kMaxContacts) - { - return false; - } - - Entry entry{}; - entry.node_id = node_id; - strncpy(entry.nickname, nickname, sizeof(entry.nickname) - 1); - entry.nickname[sizeof(entry.nickname) - 1] = '\0'; - entries_.push_back(entry); - if (saveEntries()) - { - return true; - } - entries_.pop_back(); - return false; -} - -bool ContactStoreCore::removeNickname(uint32_t node_id) -{ - for (auto it = entries_.begin(); it != entries_.end(); ++it) - { - if (it->node_id == node_id) - { - entries_.erase(it); - return saveEntries(); - } - } - return false; -} - -bool ContactStoreCore::hasNickname(const char* nickname) const -{ - if (isEmptyNickname(nickname)) - { - return false; - } - - for (const auto& entry : entries_) - { - if (strcmp(entry.nickname, nickname) == 0) - { - return true; - } - } - return false; -} - -std::vector ContactStoreCore::getAllContactIds() const -{ - std::vector ids; - ids.reserve(entries_.size()); - for (const auto& entry : entries_) - { - ids.push_back(entry.node_id); - } - return ids; -} - -size_t ContactStoreCore::getCount() const -{ - return entries_.size(); -} - -bool ContactStoreCore::clear() -{ - entries_.clear(); - return saveEntries(); -} - -bool ContactStoreCore::loadEntries() -{ - std::vector blob; - if (!blob_store_.loadBlob(blob)) - { - return false; - } - return decodeEntries(blob.data(), blob.size()); -} - -bool ContactStoreCore::saveEntries() -{ - std::vector blob; - encodeEntries(blob); - return blob_store_.saveBlob(blob.data(), blob.size()); -} - -bool ContactStoreCore::decodeEntries(const uint8_t* data, size_t len) -{ - if (!data) - { - return len == 0; - } - - if (len == 0) - { - entries_.clear(); - return true; - } - - if ((len % kSerializedEntrySize) != 0) - { - return false; - } - - size_t count = len / kSerializedEntrySize; - if (count > kMaxContacts) - { - count = kMaxContacts; - } - - entries_.resize(count); - memcpy(entries_.data(), data, count * kSerializedEntrySize); - for (auto& entry : entries_) - { - entry.nickname[sizeof(entry.nickname) - 1] = '\0'; - } - return true; -} - -void ContactStoreCore::encodeEntries(std::vector& out) const -{ - out.clear(); - if (entries_.empty()) - { - return; - } - - out.resize(entries_.size() * kSerializedEntrySize); - memcpy(out.data(), entries_.data(), out.size()); -} - -} // namespace contacts -} // namespace chat diff --git a/modules/core_chat/src/infra/mesh_peer_directory_core.cpp b/modules/core_chat/src/infra/mesh_peer_directory_core.cpp index d9ce0d51..1e934888 100644 --- a/modules/core_chat/src/infra/mesh_peer_directory_core.cpp +++ b/modules/core_chat/src/infra/mesh_peer_directory_core.cpp @@ -20,7 +20,8 @@ namespace { constexpr uint32_t kMeshPeerDirectoryMagic = 0x5244504DUL; // MPDR -constexpr uint8_t kMeshPeerDirectoryPersistVersion = 1; +constexpr uint8_t kMeshPeerDirectoryPersistVersionV1 = 1; +constexpr uint8_t kMeshPeerDirectoryPersistVersion = 2; TRAILMATE_PACK_PUSH struct PersistedMeshPeerDirectoryHeaderV1 @@ -104,6 +105,14 @@ struct PersistedMeshPeerEntryV1 } TRAILMATE_PACKED; TRAILMATE_PACK_POP +TRAILMATE_PACK_PUSH +struct PersistedMeshPeerEntryV2 +{ + PersistedMeshPeerEntryV1 peer{}; + char user_alias[kMeshPeerUserAliasMaxLen + 1] = {}; +} TRAILMATE_PACKED; +TRAILMATE_PACK_POP + void copyIntoPersisted(PersistedMeshPeerNodeFactsV1& dst, const MeshPeerNodeFacts& src) { @@ -289,6 +298,27 @@ bool copyFromPersisted(MeshPeerRecord& dst, return meshPeerRecordIsValid(dst); } +void copyIntoPersisted(PersistedMeshPeerEntryV2& dst, + const MeshPeerRecord& src) +{ + dst = {}; + copyIntoPersisted(dst.peer, src); + copyMeshPeerText(dst.user_alias, sizeof(dst.user_alias), src.user_alias); +} + +bool copyFromPersisted(MeshPeerRecord& dst, + const PersistedMeshPeerEntryV2& src) +{ + if (!copyFromPersisted(dst, src.peer)) + { + return false; + } + copyMeshPeerText(dst.user_alias, + sizeof(dst.user_alias), + src.user_alias); + return true; +} + bool textContains(const char* haystack, const char* needle) { return haystack && needle && std::strstr(haystack, needle) != nullptr; @@ -299,6 +329,24 @@ bool hasMeshPeerText(const char* text) return text && text[0] != '\0'; } +bool matchesDirectoryView(const MeshPeerRecord& record, + MeshPeerDirectoryView view) +{ + const bool contact = meshPeerIsContact(record); + switch (view) + { + case MeshPeerDirectoryView::Contacts: + return contact; + case MeshPeerDirectoryView::Nearby: + return !contact && !record.flags.ignored; + case MeshPeerDirectoryView::Ignored: + return !contact && record.flags.ignored; + case MeshPeerDirectoryView::All: + default: + return true; + } +} + void copyNonEmptyMeshPeerText(char* out, std::size_t out_len, const char* text) { if (hasMeshPeerText(text)) @@ -500,9 +548,6 @@ MeshPeerRecord mergePeerRecordFactsImpl(const MeshPeerRecord& existing, copyNonEmptyMeshPeerText(next.display_name, sizeof(next.display_name), incoming.display_name); - copyNonEmptyMeshPeerText(next.user_alias, - sizeof(next.user_alias), - incoming.user_alias); next.flags = existing.flags; if (incoming.observations.has_snr) { @@ -628,6 +673,8 @@ MeshPeerDirectoryStatus MeshPeerDirectoryCore::record( } MeshPeerRecord next = record; + next.user_alias[0] = '\0'; + next.flags = {}; if (next.last_seen_s < next.first_seen_s) { next.last_seen_s = next.first_seen_s; @@ -677,6 +724,13 @@ MeshPeerDirectoryStatus MeshPeerDirectoryCore::findByNodeId( return MeshPeerDirectoryStatus::success(); } + if (protocol == MeshProtocol::MeshCore && + record.meshcore.node_id_hint == node_id) + { + out_record = record; + return MeshPeerDirectoryStatus::success(); + } + if (meshPeerIsReticulumProtocol(protocol) && record.identity.kind == MeshPeerIdentityKind::ReticulumDestination && record.identity.reticulum.valid && @@ -765,10 +819,56 @@ MeshPeerDirectoryStatus MeshPeerDirectoryCore::search( return MeshPeerDirectoryStatus::success(); } +MeshPeerDirectoryStatus MeshPeerDirectoryCore::visit( + MeshProtocol protocol, + MeshPeerDirectoryView view, + IMeshPeerDirectoryVisitor& visitor) +{ + for (const MeshPeerRecord& record : records_) + { + if (meshPeerSameProtocol(record.identity.protocol, protocol) && + matchesDirectoryView(record, view) && !visitor.visit(record)) + { + break; + } + } + return MeshPeerDirectoryStatus::success(); +} + +MeshPeerDirectoryStatus MeshPeerDirectoryCore::setUserAlias( + const MeshPeerIdentity& identity, + const char* alias) +{ + if (!alias || std::strlen(alias) > kMeshPeerUserAliasMaxLen || + !meshPeerIsStableContactIdentity(identity)) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::InvalidArgument); + } + const std::size_t index = findIndex(identity); + if (index >= records_.size()) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::NotFound); + } + copyMeshPeerText(records_[index].user_alias, + sizeof(records_[index].user_alias), + alias); + records_[index].flags.favorite = alias[0] != '\0'; + dirty_ = true; + maybeSave(); + return MeshPeerDirectoryStatus::success(); +} + MeshPeerDirectoryStatus MeshPeerDirectoryCore::setUserFlags( const MeshPeerIdentity& identity, const MeshPeerUserFlags& flags) { + if (!meshPeerIsStableContactIdentity(identity)) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::Unsupported); + } const std::size_t index = findIndex(identity); if (index >= records_.size()) { @@ -781,6 +881,38 @@ MeshPeerDirectoryStatus MeshPeerDirectoryCore::setUserFlags( return MeshPeerDirectoryStatus::success(); } +MeshPeerDirectoryStatus MeshPeerDirectoryCore::setKeyManuallyVerified( + const MeshPeerIdentity& identity, + bool verified) +{ + const std::size_t index = findIndex(identity); + if (index >= records_.size()) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::NotFound); + } + MeshPeerRecord& record = records_[index]; + if (record.identity.protocol == MeshProtocol::Meshtastic && + record.meshtastic.has_public_key) + { + record.meshtastic.key_manually_verified = verified; + } + else if (record.identity.protocol == MeshProtocol::MeshCore && + (record.meshcore.has_public_key || + record.identity.kind == MeshPeerIdentityKind::PublicKey)) + { + record.meshcore.public_key_verified = verified; + } + else + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::Unsupported); + } + dirty_ = true; + maybeSave(); + return MeshPeerDirectoryStatus::success(); +} + MeshPeerDirectoryStatus MeshPeerDirectoryCore::remove( const MeshPeerIdentity& identity) { @@ -881,14 +1013,19 @@ bool MeshPeerDirectoryCore::decodeBlob(std::vector& out, PersistedMeshPeerDirectoryHeaderV1 header{}; std::memcpy(&header, data, sizeof(header)); if (header.magic != kMeshPeerDirectoryMagic || - header.version != kMeshPeerDirectoryPersistVersion) + (header.version != kMeshPeerDirectoryPersistVersionV1 && + header.version != kMeshPeerDirectoryPersistVersion)) { return false; } const std::size_t entries_len = len - sizeof(header); - if ((entries_len % sizeof(PersistedMeshPeerEntryV1)) != 0 || - (entries_len / sizeof(PersistedMeshPeerEntryV1)) != header.count) + const std::size_t entry_size = + header.version == kMeshPeerDirectoryPersistVersionV1 + ? sizeof(PersistedMeshPeerEntryV1) + : sizeof(PersistedMeshPeerEntryV2); + if ((entries_len % entry_size) != 0 || + (entries_len / entry_size) != header.count) { return false; } @@ -902,12 +1039,25 @@ bool MeshPeerDirectoryCore::decodeBlob(std::vector& out, out.reserve(header.count); for (std::size_t index = 0; index < header.count; ++index) { - PersistedMeshPeerEntryV1 persisted{}; - std::memcpy(&persisted, - entries_data + index * sizeof(PersistedMeshPeerEntryV1), - sizeof(persisted)); MeshPeerRecord record{}; - if (copyFromPersisted(record, persisted)) + bool valid = false; + if (header.version == kMeshPeerDirectoryPersistVersionV1) + { + PersistedMeshPeerEntryV1 persisted{}; + std::memcpy(&persisted, + entries_data + index * entry_size, + sizeof(persisted)); + valid = copyFromPersisted(record, persisted); + } + else + { + PersistedMeshPeerEntryV2 persisted{}; + std::memcpy(&persisted, + entries_data + index * entry_size, + sizeof(persisted)); + valid = copyFromPersisted(record, persisted); + } + if (valid) { out.push_back(record); } @@ -920,15 +1070,15 @@ void MeshPeerDirectoryCore::encodeBlob( const std::vector& records) { const std::size_t entries_len = - records.size() * sizeof(PersistedMeshPeerEntryV1); + records.size() * sizeof(PersistedMeshPeerEntryV2); out.assign(sizeof(PersistedMeshPeerDirectoryHeaderV1) + entries_len, 0); auto* entries_data = out.data() + sizeof(PersistedMeshPeerDirectoryHeaderV1); for (std::size_t index = 0; index < records.size(); ++index) { - PersistedMeshPeerEntryV1 persisted{}; + PersistedMeshPeerEntryV2 persisted{}; copyIntoPersisted(persisted, records[index]); - std::memcpy(entries_data + index * sizeof(PersistedMeshPeerEntryV1), + std::memcpy(entries_data + index * sizeof(PersistedMeshPeerEntryV2), &persisted, sizeof(persisted)); } diff --git a/modules/core_chat/src/infra/node_store_blob_format.cpp b/modules/core_chat/src/infra/node_store_blob_format.cpp deleted file mode 100644 index af39a97c..00000000 --- a/modules/core_chat/src/infra/node_store_blob_format.cpp +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @file node_store_blob_format.cpp - * @brief Shared node-store blob format helpers - */ - -#include "chat/infra/node_store_blob_format.h" - -#include "chat/infra/node_store_core.h" - -namespace chat -{ -namespace contacts -{ - -size_t nodeBlobEntrySizeForVersion(uint8_t version) -{ - if (version == NodeStoreCore::kPersistVersion) - { - return NodeStoreCore::kSerializedEntrySize; - } - if (version == NodeStoreCore::kPersistVersionV8) - { - return NodeStoreCore::kSerializedEntrySizeV8; - } - return 0; -} - -bool isValidNodeBlobSize(size_t len) -{ - return len != 0 && (len % NodeStoreCore::kSerializedEntrySize) == 0; -} - -size_t nodeBlobEntryCount(size_t len) -{ - return isValidNodeBlobSize(len) ? (len / NodeStoreCore::kSerializedEntrySize) : 0; -} - -size_t nodeBlobByteSize(size_t count) -{ - return count * NodeStoreCore::kSerializedEntrySize; -} - -NodeStoreSdHeader makeNodeStoreSdHeader(const uint8_t* data, size_t len) -{ - NodeStoreSdHeader header{}; - header.ver = NodeStoreCore::kPersistVersion; - header.reserved[0] = 0; - header.reserved[1] = 0; - header.reserved[2] = 0; - header.count = static_cast(nodeBlobEntryCount(len)); - header.crc = NodeStoreCore::computeBlobCrc(data, len); - return header; -} - -NodeBlobValidation validateNodeBlobMetadata(size_t len, uint8_t version, - bool has_crc, uint32_t stored_crc, - const uint8_t* data) -{ - if (len == 0) - { - return has_crc ? NodeBlobValidation::StaleMetadata : NodeBlobValidation::Empty; - } - if (version == 0) - { - return NodeBlobValidation::VersionMismatch; - } - const size_t entry_size = nodeBlobEntrySizeForVersion(version); - if (entry_size == 0) - { - return NodeBlobValidation::VersionMismatch; - } - if ((len % entry_size) != 0) - { - return NodeBlobValidation::InvalidLength; - } - if ((len / entry_size) > NodeStoreCore::kMaxNodes) - { - return NodeBlobValidation::TooManyEntries; - } - if (!has_crc) - { - return NodeBlobValidation::MissingCrc; - } - if (!data) - { - return NodeBlobValidation::InvalidLength; - } - const uint32_t calc_crc = NodeStoreCore::computeBlobCrc(data, len); - return (calc_crc == stored_crc) ? NodeBlobValidation::Ok : NodeBlobValidation::CrcMismatch; -} - -NodeBlobValidation validateNodeStoreSdHeader(const NodeStoreSdHeader& header) -{ - if (nodeBlobEntrySizeForVersion(header.ver) == 0) - { - return NodeBlobValidation::VersionMismatch; - } - if (header.count > NodeStoreCore::kMaxNodes) - { - return NodeBlobValidation::TooManyEntries; - } - return NodeBlobValidation::Ok; -} - -NodeBlobValidation validateNodeStoreSdBlob(const NodeStoreSdHeader& header, - const uint8_t* data, size_t len) -{ - const NodeBlobValidation header_status = validateNodeStoreSdHeader(header); - if (header_status != NodeBlobValidation::Ok) - { - return header_status; - } - const size_t entry_size = nodeBlobEntrySizeForVersion(header.ver); - if (entry_size == 0) - { - return NodeBlobValidation::VersionMismatch; - } - const size_t expected_bytes = header.count * entry_size; - if (expected_bytes != len) - { - return NodeBlobValidation::InvalidLength; - } - const uint32_t calc_crc = NodeStoreCore::computeBlobCrc(data, len); - return (calc_crc == header.crc) ? NodeBlobValidation::Ok : NodeBlobValidation::CrcMismatch; -} - -} // namespace contacts -} // namespace chat diff --git a/modules/core_chat/src/infra/node_store_core.cpp b/modules/core_chat/src/infra/node_store_core.cpp deleted file mode 100644 index 925e35ab..00000000 --- a/modules/core_chat/src/infra/node_store_core.cpp +++ /dev/null @@ -1,791 +0,0 @@ -#include "chat/infra/node_store_core.h" - -#include "chat/domain/contact_types.h" -#include "chat/infra/node_store_blob_format.h" -#include "sys/clock.h" -#include -#include -#include - -#if defined(_MSC_VER) -#define TRAILMATE_PACK_PUSH __pragma(pack(push, 1)) -#define TRAILMATE_PACK_POP __pragma(pack(pop)) -#define TRAILMATE_PACKED -#else -#define TRAILMATE_PACK_PUSH -#define TRAILMATE_PACK_POP -#define TRAILMATE_PACKED __attribute__((packed)) -#endif - -namespace chat -{ -namespace contacts -{ - -namespace -{ -bool differentString(const char* lhs, const char* rhs, size_t rhs_size) -{ - if (!rhs || rhs[0] == '\0') - { - return false; - } - return std::strncmp(lhs, rhs, rhs_size) != 0; -} - -bool differentBytes(const uint8_t* lhs, const uint8_t* rhs, size_t len) -{ - return std::memcmp(lhs, rhs, len) != 0; -} - -bool differentPosition(const NodeEntry& lhs, const NodePosition& rhs) -{ - return lhs.position_valid != rhs.valid || - lhs.position_latitude_i != rhs.latitude_i || - lhs.position_longitude_i != rhs.longitude_i || - lhs.position_has_altitude != rhs.has_altitude || - lhs.position_altitude != rhs.altitude || - lhs.position_precision_bits != rhs.precision_bits || - lhs.position_pdop != rhs.pdop || - lhs.position_hdop != rhs.hdop || - lhs.position_vdop != rhs.vdop || - lhs.position_gps_accuracy_mm != rhs.gps_accuracy_mm; -} - -bool differentMetrics(const NodeDeviceMetrics& lhs, const NodeDeviceMetrics& rhs) -{ - return lhs.has_battery_level != rhs.has_battery_level || - lhs.battery_level != rhs.battery_level || - lhs.has_voltage != rhs.has_voltage || - lhs.voltage != rhs.voltage || - lhs.has_channel_utilization != rhs.has_channel_utilization || - lhs.channel_utilization != rhs.channel_utilization || - lhs.has_air_util_tx != rhs.has_air_util_tx || - lhs.air_util_tx != rhs.air_util_tx || - lhs.has_uptime_seconds != rhs.has_uptime_seconds || - lhs.uptime_seconds != rhs.uptime_seconds; -} - -TRAILMATE_PACK_PUSH -struct PersistedNodeEntryV8 -{ - uint32_t node_id; - char short_name[10]; - char long_name[32]; - uint32_t last_seen; - float snr; - float rssi; - uint8_t hops_away; - uint8_t channel; - uint8_t next_hop; - uint8_t protocol; - uint8_t role; - uint8_t hw_model; - uint8_t has_macaddr; - uint8_t via_mqtt; - uint8_t is_ignored; - uint8_t has_public_key; - uint8_t key_manually_verified; - uint8_t has_device_metrics; - uint8_t position_valid; - uint8_t position_has_altitude; - uint8_t metrics_has_battery_level; - uint8_t metrics_has_voltage; - uint8_t metrics_has_channel_utilization; - uint8_t metrics_has_air_util_tx; - uint8_t metrics_has_uptime_seconds; - uint8_t reserved[3]; - uint8_t macaddr[6]; - uint8_t reserved_mac[2]; - int32_t position_latitude_i; - int32_t position_longitude_i; - int32_t position_altitude; - uint32_t position_timestamp; - uint32_t position_precision_bits; - uint32_t position_pdop; - uint32_t position_hdop; - uint32_t position_vdop; - uint32_t position_gps_accuracy_mm; - uint32_t metrics_battery_level; - float metrics_voltage; - float metrics_channel_utilization; - float metrics_air_util_tx; - uint32_t metrics_uptime_seconds; -} TRAILMATE_PACKED; -TRAILMATE_PACK_POP - -static_assert(sizeof(PersistedNodeEntryV8) == NodeStoreCore::kSerializedEntrySizeV8, - "PersistedNodeEntryV8 size changed"); - -TRAILMATE_PACK_PUSH -struct PersistedNodeEntryV9 -{ - PersistedNodeEntryV8 base; - uint8_t reticulum_identity_valid; - uint8_t reticulum_destination_hash[kReticulumPeerHashSize]; - uint8_t reticulum_identity_hash[kReticulumPeerHashSize]; - uint8_t reserved_v9[3]; -} TRAILMATE_PACKED; -TRAILMATE_PACK_POP - -static_assert(sizeof(PersistedNodeEntryV9) == NodeStoreCore::kSerializedEntrySizeV9, - "PersistedNodeEntryV9 size changed"); - -void copyCommonFields(NodeEntry& dst, - uint32_t node_id, - const char short_name[10], - const char long_name[32], - uint32_t last_seen, - float snr, - float rssi, - uint8_t hops_away, - uint8_t channel, - uint8_t next_hop, - uint8_t protocol, - uint8_t role, - uint8_t hw_model) -{ - dst = {}; - dst.node_id = node_id; - memcpy(dst.short_name, short_name, sizeof(dst.short_name)); - dst.short_name[sizeof(dst.short_name) - 1] = '\0'; - memcpy(dst.long_name, long_name, sizeof(dst.long_name)); - dst.long_name[sizeof(dst.long_name) - 1] = '\0'; - dst.last_seen = last_seen; - dst.snr = snr; - dst.rssi = rssi; - dst.hops_away = hops_away; - dst.channel = channel; - dst.next_hop = next_hop; - dst.protocol = protocol; - dst.role = role; - dst.hw_model = hw_model; -} - -void copyIntoPersisted(PersistedNodeEntryV8& dst, const NodeEntry& src) -{ - memset(&dst, 0, sizeof(dst)); - dst.node_id = src.node_id; - memcpy(dst.short_name, src.short_name, sizeof(dst.short_name)); - dst.short_name[sizeof(dst.short_name) - 1] = '\0'; - memcpy(dst.long_name, src.long_name, sizeof(dst.long_name)); - dst.long_name[sizeof(dst.long_name) - 1] = '\0'; - dst.last_seen = src.last_seen; - dst.snr = src.snr; - dst.rssi = src.rssi; - dst.hops_away = src.hops_away; - dst.channel = src.channel; - dst.next_hop = src.next_hop; - dst.protocol = src.protocol; - dst.role = src.role; - dst.hw_model = src.hw_model; - dst.has_macaddr = src.has_macaddr ? 1U : 0U; - memcpy(dst.macaddr, src.macaddr, sizeof(dst.macaddr)); - dst.via_mqtt = src.via_mqtt ? 1U : 0U; - dst.is_ignored = src.is_ignored ? 1U : 0U; - dst.has_public_key = src.has_public_key ? 1U : 0U; - dst.key_manually_verified = src.key_manually_verified ? 1U : 0U; - dst.has_device_metrics = src.has_device_metrics ? 1U : 0U; - dst.position_valid = src.position_valid ? 1U : 0U; - dst.position_has_altitude = src.position_has_altitude ? 1U : 0U; - dst.position_latitude_i = src.position_latitude_i; - dst.position_longitude_i = src.position_longitude_i; - dst.position_altitude = src.position_altitude; - dst.position_timestamp = src.position_timestamp; - dst.position_precision_bits = src.position_precision_bits; - dst.position_pdop = src.position_pdop; - dst.position_hdop = src.position_hdop; - dst.position_vdop = src.position_vdop; - dst.position_gps_accuracy_mm = src.position_gps_accuracy_mm; - dst.metrics_has_battery_level = src.device_metrics.has_battery_level ? 1U : 0U; - dst.metrics_has_voltage = src.device_metrics.has_voltage ? 1U : 0U; - dst.metrics_has_channel_utilization = src.device_metrics.has_channel_utilization ? 1U : 0U; - dst.metrics_has_air_util_tx = src.device_metrics.has_air_util_tx ? 1U : 0U; - dst.metrics_has_uptime_seconds = src.device_metrics.has_uptime_seconds ? 1U : 0U; - dst.metrics_battery_level = src.device_metrics.battery_level; - dst.metrics_voltage = src.device_metrics.voltage; - dst.metrics_channel_utilization = src.device_metrics.channel_utilization; - dst.metrics_air_util_tx = src.device_metrics.air_util_tx; - dst.metrics_uptime_seconds = src.device_metrics.uptime_seconds; -} - -void copyIntoPersisted(PersistedNodeEntryV9& dst, const NodeEntry& src) -{ - memset(&dst, 0, sizeof(dst)); - copyIntoPersisted(dst.base, src); - dst.reticulum_identity_valid = src.reticulum_identity.valid ? 1U : 0U; - (void)copyReticulumIdentityHashes(dst.reticulum_destination_hash, - dst.reticulum_identity_hash, - src.reticulum_identity); -} - -void copyFromPersisted(NodeEntry& dst, const PersistedNodeEntryV8& src) -{ - copyCommonFields(dst, - src.node_id, - src.short_name, - src.long_name, - src.last_seen, - src.snr, - src.rssi, - src.hops_away, - src.channel, - src.next_hop, - src.protocol, - src.role, - src.hw_model); - dst.has_macaddr = src.has_macaddr != 0; - memcpy(dst.macaddr, src.macaddr, sizeof(dst.macaddr)); - dst.via_mqtt = src.via_mqtt != 0; - dst.is_ignored = src.is_ignored != 0; - dst.has_public_key = src.has_public_key != 0; - dst.key_manually_verified = src.key_manually_verified != 0; - dst.has_device_metrics = src.has_device_metrics != 0; - dst.position_valid = src.position_valid != 0; - dst.position_has_altitude = src.position_has_altitude != 0; - dst.position_latitude_i = src.position_latitude_i; - dst.position_longitude_i = src.position_longitude_i; - dst.position_altitude = src.position_altitude; - dst.position_timestamp = src.position_timestamp; - dst.position_precision_bits = src.position_precision_bits; - dst.position_pdop = src.position_pdop; - dst.position_hdop = src.position_hdop; - dst.position_vdop = src.position_vdop; - dst.position_gps_accuracy_mm = src.position_gps_accuracy_mm; - dst.device_metrics.has_battery_level = src.metrics_has_battery_level != 0; - dst.device_metrics.battery_level = src.metrics_battery_level; - dst.device_metrics.has_voltage = src.metrics_has_voltage != 0; - dst.device_metrics.voltage = src.metrics_voltage; - dst.device_metrics.has_channel_utilization = src.metrics_has_channel_utilization != 0; - dst.device_metrics.channel_utilization = src.metrics_channel_utilization; - dst.device_metrics.has_air_util_tx = src.metrics_has_air_util_tx != 0; - dst.device_metrics.air_util_tx = src.metrics_air_util_tx; - dst.device_metrics.has_uptime_seconds = src.metrics_has_uptime_seconds != 0; - dst.device_metrics.uptime_seconds = src.metrics_uptime_seconds; -} - -void copyFromPersisted(NodeEntry& dst, const PersistedNodeEntryV9& src) -{ - copyFromPersisted(dst, src.base); - if (src.reticulum_identity_valid != 0) - { - dst.reticulum_identity = makeReticulumPeerIdentity( - src.reticulum_destination_hash, - src.reticulum_identity_hash); - } -} - -} // namespace - -NodeStoreCore::NodeStoreCore(INodeBlobStore& blob_store) - : blob_store_(blob_store) -{ -} - -void NodeStoreCore::setProtectedNodeChecker(std::function checker) -{ - protected_node_checker_ = std::move(checker); -} - -void NodeStoreCore::setAutoSaveEnabled(bool enabled) -{ - auto_save_enabled_ = enabled; -} - -void NodeStoreCore::begin() -{ - if (!loadEntries()) - { - entries_.clear(); - dirty_ = false; - last_save_ms_ = 0; - } -} - -void NodeStoreCore::applyUpdate(uint32_t node_id, const NodeUpdate& update) -{ - if (node_id == 0) - { - return; - } - - auto apply_to_entry = [&](NodeEntry& entry) -> bool - { - bool persistent_changed = false; - if (update.short_name && update.short_name[0] != '\0') - { - persistent_changed = - differentString(entry.short_name, update.short_name, sizeof(entry.short_name)) || - persistent_changed; - strncpy(entry.short_name, update.short_name, sizeof(entry.short_name) - 1); - entry.short_name[sizeof(entry.short_name) - 1] = '\0'; - } - if (update.long_name && update.long_name[0] != '\0') - { - persistent_changed = - differentString(entry.long_name, update.long_name, sizeof(entry.long_name)) || - persistent_changed; - strncpy(entry.long_name, update.long_name, sizeof(entry.long_name) - 1); - entry.long_name[sizeof(entry.long_name) - 1] = '\0'; - } - if (update.has_last_seen) - { - entry.last_seen = update.last_seen; - } - if (update.has_snr) - { - entry.snr = update.snr; - } - if (update.has_rssi) - { - entry.rssi = update.rssi; - } - if (update.has_hops_away) - { - entry.hops_away = update.hops_away; - } - if (update.has_channel) - { - entry.channel = update.channel; - } - if (update.has_next_hop) - { - persistent_changed = (entry.next_hop != update.next_hop) || persistent_changed; - entry.next_hop = update.next_hop; - } - if (update.has_protocol) - { - persistent_changed = (entry.protocol != update.protocol) || persistent_changed; - entry.protocol = update.protocol; - } - if (update.has_role) - { - persistent_changed = (entry.role != update.role) || persistent_changed; - entry.role = update.role; - } - if (update.has_hw_model) - { - persistent_changed = (entry.hw_model != update.hw_model) || persistent_changed; - entry.hw_model = update.hw_model; - } - if (update.has_macaddr) - { - persistent_changed = - !entry.has_macaddr || - differentBytes(entry.macaddr, update.macaddr, sizeof(entry.macaddr)) || - persistent_changed; - entry.has_macaddr = true; - memcpy(entry.macaddr, update.macaddr, sizeof(entry.macaddr)); - } - if (update.has_via_mqtt) - { - persistent_changed = (entry.via_mqtt != update.via_mqtt) || persistent_changed; - entry.via_mqtt = update.via_mqtt; - } - if (update.has_is_ignored) - { - persistent_changed = (entry.is_ignored != update.is_ignored) || persistent_changed; - entry.is_ignored = update.is_ignored; - } - if (update.has_public_key) - { - persistent_changed = - (entry.has_public_key != update.public_key_present) || persistent_changed; - entry.has_public_key = update.public_key_present; - } - if (update.has_key_manually_verified) - { - persistent_changed = - (entry.key_manually_verified != update.key_manually_verified) || - persistent_changed; - entry.key_manually_verified = update.key_manually_verified; - } - if (update.reticulum_identity.valid) - { - persistent_changed = - !sameReticulumPeerIdentity(entry.reticulum_identity, - update.reticulum_identity) || - persistent_changed; - entry.reticulum_identity = update.reticulum_identity; - } - if (update.has_device_metrics) - { - persistent_changed = - !entry.has_device_metrics || - differentMetrics(entry.device_metrics, update.device_metrics) || - persistent_changed; - entry.has_device_metrics = true; - entry.device_metrics = update.device_metrics; - } - if (update.has_position) - { - persistent_changed = - differentPosition(entry, update.position) || - persistent_changed; - entry.position_valid = update.position.valid; - entry.position_latitude_i = update.position.latitude_i; - entry.position_longitude_i = update.position.longitude_i; - entry.position_has_altitude = update.position.has_altitude; - entry.position_altitude = update.position.altitude; - entry.position_timestamp = update.position.timestamp; - entry.position_precision_bits = update.position.precision_bits; - entry.position_pdop = update.position.pdop; - entry.position_hdop = update.position.hdop; - entry.position_vdop = update.position.vdop; - entry.position_gps_accuracy_mm = update.position.gps_accuracy_mm; - } - return persistent_changed; - }; - - for (auto& entry : entries_) - { - if (entry.node_id != node_id) - { - continue; - } - if (apply_to_entry(entry)) - { - dirty_ = true; - maybeSave(); - } - return; - } - - if (entries_.size() >= kMaxNodes) - { - const size_t eviction_index = selectEvictionIndex(); - if (eviction_index < entries_.size()) - { - entries_.erase(entries_.begin() + static_cast(eviction_index)); - } - } - - NodeEntry entry{}; - entry.node_id = node_id; - entry.snr = std::numeric_limits::quiet_NaN(); - entry.rssi = std::numeric_limits::quiet_NaN(); - entry.hops_away = 0xFF; - entry.channel = 0xFF; - entry.role = kNodeRoleUnknown; - (void)apply_to_entry(entry); - entries_.push_back(entry); - dirty_ = true; - maybeSave(); -} - -void NodeStoreCore::upsert(uint32_t node_id, const char* short_name, const char* long_name, - uint32_t now_secs, float snr, float rssi, uint8_t protocol, - uint8_t role, uint8_t hops_away, uint8_t hw_model, uint8_t channel) -{ - NodeUpdate update{}; - update.short_name = short_name; - update.long_name = long_name; - update.has_last_seen = true; - update.last_seen = now_secs; - update.has_snr = !std::isnan(snr); - update.snr = snr; - update.has_rssi = !std::isnan(rssi); - update.rssi = rssi; - update.has_protocol = (protocol != 0); - update.protocol = protocol; - update.has_role = (role != kNodeRoleUnknown); - update.role = role; - update.has_hops_away = (hops_away != 0xFF); - update.hops_away = hops_away; - update.has_hw_model = (hw_model != 0); - update.hw_model = hw_model; - update.has_channel = (channel != 0xFF); - update.channel = channel; - applyUpdate(node_id, update); -} - -void NodeStoreCore::updateProtocol(uint32_t node_id, uint8_t protocol, uint32_t now_secs) -{ - if (protocol == 0) - { - return; - } - NodeUpdate update{}; - update.has_protocol = true; - update.protocol = protocol; - update.has_last_seen = true; - update.last_seen = now_secs; - applyUpdate(node_id, update); -} - -void NodeStoreCore::updatePosition(uint32_t node_id, const NodePosition& position) -{ - NodeUpdate update{}; - update.has_position = true; - update.position = position; - applyUpdate(node_id, update); -} - -bool NodeStoreCore::setNextHop(uint32_t node_id, uint8_t next_hop, uint32_t now_secs) -{ - if (node_id == 0) - { - return false; - } - - if (getNextHop(node_id) == next_hop) - { - return true; - } - - NodeUpdate update{}; - update.has_next_hop = true; - update.next_hop = next_hop; - if (now_secs != 0) - { - update.has_last_seen = true; - update.last_seen = now_secs; - } - applyUpdate(node_id, update); - return true; -} - -uint8_t NodeStoreCore::getNextHop(uint32_t node_id) const -{ - for (const auto& entry : entries_) - { - if (entry.node_id == node_id) - { - return entry.next_hop; - } - } - return 0; -} - -bool NodeStoreCore::remove(uint32_t node_id) -{ - for (auto it = entries_.begin(); it != entries_.end(); ++it) - { - if (it->node_id == node_id) - { - entries_.erase(it); - dirty_ = true; - return saveEntries(); - } - } - return false; -} - -const std::vector& NodeStoreCore::getEntries() const -{ - return entries_; -} - -void NodeStoreCore::clear() -{ - entries_.clear(); - dirty_ = false; - last_save_ms_ = 0; - blob_store_.clearBlob(); -} - -bool NodeStoreCore::flush() -{ - if (!dirty_) - { - return true; - } - return saveEntries(); -} - -uint32_t NodeStoreCore::computeBlobCrc(const uint8_t* data, size_t len) -{ - uint32_t crc = 0xFFFFFFFF; - for (size_t i = 0; i < len; ++i) - { - crc ^= data[i]; - for (int bit = 0; bit < 8; ++bit) - { - if (crc & 1U) - { - crc = (crc >> 1) ^ 0xEDB88320U; - } - else - { - crc >>= 1; - } - } - } - return ~crc; -} - -bool NodeStoreCore::decodeBlob(std::vector& out, const uint8_t* data, size_t len, uint8_t persist_version) -{ - if (!data) - { - out.clear(); - return len == 0; - } - - if (len == 0) - { - out.clear(); - return true; - } - - const size_t entry_size = nodeBlobEntrySizeForVersion(persist_version); - if (entry_size == 0 || (len % entry_size) != 0) - { - out.clear(); - return false; - } - - size_t count = len / entry_size; - if (count > kMaxNodes) - { - count = kMaxNodes; - } - - out.clear(); - out.reserve(count); - if (persist_version == kPersistVersion) - { - auto* persisted = reinterpret_cast(data); - for (size_t index = 0; index < count; ++index) - { - NodeEntry entry{}; - copyFromPersisted(entry, persisted[index]); - out.push_back(entry); - } - return true; - } - - if (persist_version == kPersistVersionV8) - { - auto* persisted = reinterpret_cast(data); - for (size_t index = 0; index < count; ++index) - { - NodeEntry entry{}; - copyFromPersisted(entry, persisted[index]); - out.push_back(entry); - } - return true; - } - - out.clear(); - return false; -} - -void NodeStoreCore::encodeBlob(std::vector& out, const std::vector& entries) -{ - out.clear(); - if (entries.empty()) - { - return; - } - - std::vector persisted(entries.size()); - for (size_t index = 0; index < entries.size(); ++index) - { - copyIntoPersisted(persisted[index], entries[index]); - } - - out.resize(persisted.size() * sizeof(PersistedNodeEntryV9)); - memcpy(out.data(), persisted.data(), out.size()); -} - -bool NodeStoreCore::loadEntries() -{ - std::vector blob; - if (!blob_store_.loadBlob(blob)) - { - return false; - } - const bool ok = decodeEntries(blob.data(), blob.size()); - if (ok) - { - dirty_ = false; - last_save_ms_ = 0; - } - return ok; -} - -bool NodeStoreCore::saveEntries() -{ - std::vector blob; - encodeEntries(blob); - if (!blob_store_.saveBlob(blob.data(), blob.size())) - { - return false; - } - last_save_ms_ = sys::millis_now(); - dirty_ = false; - return true; -} - -bool NodeStoreCore::decodeEntries(const uint8_t* data, size_t len) -{ - return decodeBlob(entries_, data, len, kPersistVersion); -} - -void NodeStoreCore::encodeEntries(std::vector& out) const -{ - encodeBlob(out, entries_); -} - -void NodeStoreCore::maybeSave() -{ - if (!dirty_ || !auto_save_enabled_) - { - return; - } - - const uint32_t now_ms = sys::millis_now(); - if (last_save_ms_ == 0 || (now_ms - last_save_ms_) >= kSaveIntervalMs) - { - saveEntries(); - } -} - -size_t NodeStoreCore::selectEvictionIndex() const -{ - if (entries_.empty()) - { - return 0; - } - - auto is_protected = [this](uint32_t node_id) -> bool - { - return protected_node_checker_ ? protected_node_checker_(node_id) : false; - }; - - size_t oldest_unprotected_index = entries_.size(); - uint32_t oldest_unprotected_seen = std::numeric_limits::max(); - size_t oldest_any_index = 0; - uint32_t oldest_any_seen = std::numeric_limits::max(); - - for (size_t index = 0; index < entries_.size(); ++index) - { - const NodeEntry& entry = entries_[index]; - if (entry.last_seen < oldest_any_seen) - { - oldest_any_seen = entry.last_seen; - oldest_any_index = index; - } - - if (is_protected(entry.node_id)) - { - continue; - } - - if (entry.last_seen < oldest_unprotected_seen) - { - oldest_unprotected_seen = entry.last_seen; - oldest_unprotected_index = index; - } - } - - return (oldest_unprotected_index < entries_.size()) ? oldest_unprotected_index : oldest_any_index; -} - -} // namespace contacts -} // namespace chat - -#undef TRAILMATE_PACK_PUSH -#undef TRAILMATE_PACK_POP -#undef TRAILMATE_PACKED diff --git a/modules/core_chat/src/usecase/contact_service.cpp b/modules/core_chat/src/usecase/contact_service.cpp index 164c4f5c..03b7903d 100644 --- a/modules/core_chat/src/usecase/contact_service.cpp +++ b/modules/core_chat/src/usecase/contact_service.cpp @@ -1,15 +1,15 @@ /** * @file contact_service.cpp - * @brief Contact service implementation + * @brief Contact and peer-directory use cases. */ #include "chat/usecase/contact_service.h" #include "sys/clock.h" + #include #include #include #include -#include #include namespace chat @@ -44,7 +44,8 @@ bool same_text(const char* lhs, const char* rhs) return std::strcmp(lhs, rhs) == 0; } -std::string preferred_node_name(const char* short_name, const char* long_name) +std::string preferred_node_name(const char* short_name, + const char* long_name) { if (has_text(long_name) && !same_text(long_name, short_name)) { @@ -54,83 +55,394 @@ std::string preferred_node_name(const char* short_name, const char* long_name) { return std::string(short_name); } - if (has_text(long_name)) + return has_text(long_name) ? std::string(long_name) : std::string(); +} + +std::string preferred_node_name(const PeerDirectoryItem& peer) +{ + if (!peer.display_name.empty() && + !same_text(peer.display_name.c_str(), peer.short_name)) { - return std::string(long_name); + return peer.display_name; } - return std::string(); + return preferred_node_name(peer.short_name, peer.long_name); } -std::string preferred_node_name(const NodeInfo& node) +MeshProtocol normalize_protocol(MeshProtocol protocol) { - if (!node.display_name.empty() && - !same_text(node.display_name.c_str(), node.short_name)) + return protocol == MeshProtocol::RNode ? MeshProtocol::Reticulum + : protocol; +} + +const MeshPeerNodeFacts* node_facts(const MeshPeerRecord& record) +{ + if (record.identity.protocol == MeshProtocol::Meshtastic) { - return node.display_name; + return &record.meshtastic.node; } - return preferred_node_name(node.short_name, node.long_name); + if (record.identity.protocol == MeshProtocol::MeshCore) + { + return &record.meshcore.node; + } + return nullptr; } -bool is_reticulum_node(const NodeInfo& node) +MeshPeerNodeFacts* mutable_node_facts(MeshPeerRecord& record) { - return node.protocol == NodeProtocolType::Reticulum || - hasReticulumDestinationIdentity(node.reticulum_identity); + if (record.identity.protocol == MeshProtocol::Meshtastic) + { + return &record.meshtastic.node; + } + if (record.identity.protocol == MeshProtocol::MeshCore) + { + return &record.meshcore.node; + } + return nullptr; } -bool is_reticulum_entry(const NodeEntry& entry) +void copy_peer_item(PeerDirectoryItem& out, const MeshPeerRecord& record) { - return static_cast(entry.protocol) == NodeProtocolType::Reticulum || - hasReticulumDestinationIdentity(entry.reticulum_identity); + out = {}; + out.node_id = meshPeerProjectedNodeId(record); + out.last_seen = record.last_seen_s; + const MeshProtocol protocol = normalize_protocol(record.identity.protocol); + out.protocol = static_cast(protocol); + + const MeshPeerNodeFacts* facts = node_facts(record); + if (facts) + { + copyMeshPeerText(out.short_name, sizeof(out.short_name), + facts->short_name); + copyMeshPeerText(out.long_name, sizeof(out.long_name), + facts->long_name); + out.role = static_cast(facts->role); + out.hw_model = facts->hw_model; + out.channel = facts->channel; + out.hops_away = facts->hops_away; + out.has_macaddr = facts->has_macaddr; + std::memcpy(out.macaddr, facts->macaddr, sizeof(out.macaddr)); + out.via_mqtt = facts->via_mqtt; + } + else + { + copyMeshPeerText(out.long_name, sizeof(out.long_name), + record.display_name); + out.role = NodeRoleType::Client; + out.hops_away = 0; + } + + if (out.long_name[0] == '\0') + { + copyMeshPeerText(out.long_name, sizeof(out.long_name), + record.display_name); + } + if (out.short_name[0] == '\0') + { + std::snprintf(out.short_name, + sizeof(out.short_name), + "%04X", + static_cast(out.node_id & 0xFFFFU)); + } + + out.snr = record.observations.has_snr ? record.observations.snr : 0.0F; + out.rssi = record.observations.has_rssi ? record.observations.rssi : 0.0F; + out.is_contact = meshPeerIsContact(record); + out.is_ignored = record.flags.ignored; + out.has_device_metrics = record.observations.has_device_metrics; + if (out.has_device_metrics) + { + out.device_metrics = record.observations.device_metrics; + } + if (record.observations.has_position) + { + out.position = record.observations.position; + } + + if (protocol == MeshProtocol::Meshtastic) + { + out.next_hop = record.meshtastic.has_next_hop + ? record.meshtastic.next_hop + : 0; + out.has_public_key = record.meshtastic.has_public_key; + out.key_manually_verified = + record.meshtastic.key_manually_verified; + } + else if (protocol == MeshProtocol::MeshCore) + { + out.next_hop = record.meshcore.has_next_hop + ? record.meshcore.next_hop + : 0; + out.has_public_key = + record.meshcore.has_public_key || + record.identity.kind == MeshPeerIdentityKind::PublicKey; + out.key_manually_verified = record.meshcore.public_key_verified; + } + else if (protocol == MeshProtocol::Reticulum) + { + out.reticulum_identity = record.identity.reticulum; + out.has_public_key = record.reticulum.has_public_keys; + } + + const std::string protocol_name = + preferred_node_name(out.short_name, out.long_name); + out.display_name = record.user_alias[0] != '\0' + ? std::string(record.user_alias) + : protocol_name; } +class ProjectionVisitor final : public IMeshPeerDirectoryVisitor +{ + public: + explicit ProjectionVisitor(std::vector& out) + : out_(out) + { + } + + bool visit(const MeshPeerRecord& record) override + { + if (meshPeerProjectedNodeId(record) == 0) + { + return true; + } + out_.emplace_back(); + copy_peer_item(out_.back(), record); + return true; + } + + private: + std::vector& out_; +}; + +class ReticulumLookupVisitor final : public IMeshPeerDirectoryVisitor +{ + public: + ReticulumLookupVisitor( + const uint8_t destination_hash[kReticulumPeerHashSize], + uint32_t* out_node_id) + : destination_hash_(destination_hash), out_node_id_(out_node_id) + { + } + + bool visit(const MeshPeerRecord& record) override + { + if (sameReticulumDestinationHash(record.identity.reticulum, + destination_hash_)) + { + *out_node_id_ = meshPeerProjectedNodeId(record); + found_ = *out_node_id_ != 0; + return false; + } + return true; + } + + bool found() const { return found_; } + + private: + const uint8_t* destination_hash_ = nullptr; + uint32_t* out_node_id_ = nullptr; + bool found_ = false; +}; + } // namespace -ContactService::ContactService(INodeStore& node_store, IContactStore& contact_store) - : node_store_(node_store), contact_store_(contact_store), cache_timestamp_(0) +ContactService::ContactService(IMeshPeerDirectory& directory) + : directory_(directory), cache_timestamp_(0) { } void ContactService::begin() { - node_store_.setActiveProtocol(active_protocol_); - contact_store_.setActiveProtocol(active_protocol_); - node_store_.begin(); - contact_store_.begin(); - - const std::vector contact_ids = contact_store_.getAllContactIds(); - for (size_t i = 0; i < contact_ids.size(); ++i) - { - (void)ensureNodeExistsForContact(contact_ids[i]); - } - + (void)directory_.begin(); invalidateCache(); } void ContactService::setActiveProtocol(MeshProtocol protocol) { - active_protocol_ = protocol == MeshProtocol::RNode - ? MeshProtocol::Reticulum - : protocol; - node_store_.setActiveProtocol(active_protocol_); - contact_store_.setActiveProtocol(active_protocol_); + active_protocol_ = normalize_protocol(protocol); invalidateCache(); } -void ContactService::applyNodeUpdate(uint32_t node_id, const NodeUpdate& update) +void ContactService::applyNodeUpdate(uint32_t node_id, + const NodeUpdate& update) { - node_store_.applyUpdate(node_id, update); + if (node_id == 0) + { + return; + } + + MeshProtocol protocol = active_protocol_; + if (update.has_protocol) + { + const MeshProtocol requested = + normalize_protocol(static_cast(update.protocol)); + if (requested == MeshProtocol::Meshtastic || + requested == MeshProtocol::MeshCore || + requested == MeshProtocol::Reticulum) + { + protocol = requested; + } + } + + const bool found = + directory_.findByNodeId(protocol, node_id, lookup_scratch_).succeeded(); + update_scratch_ = {}; + update_scratch_.valid = true; + update_scratch_.source = MeshPeerSource::RuntimeRx; + if (protocol == MeshProtocol::Reticulum && + hasReticulumDestinationIdentity(update.reticulum_identity)) + { + update_scratch_.identity = + makeMeshPeerReticulumIdentity(update.reticulum_identity); + update_scratch_.reticulum.identity = update.reticulum_identity; + } + else if (found) + { + update_scratch_.identity = lookup_scratch_.identity; + } + else + { + update_scratch_.identity = makeMeshPeerNodeIdentity(protocol, node_id); + } + update_scratch_.identity.protocol = protocol; + if (update.has_last_seen) + { + update_scratch_.first_seen_s = update.last_seen; + update_scratch_.last_seen_s = update.last_seen; + } + if (has_text(update.long_name)) + { + copyMeshPeerText(update_scratch_.display_name, + sizeof(update_scratch_.display_name), + update.long_name); + } + else if (has_text(update.short_name)) + { + copyMeshPeerText(update_scratch_.display_name, + sizeof(update_scratch_.display_name), + update.short_name); + } + if (update.has_snr) + { + update_scratch_.observations.has_snr = true; + update_scratch_.observations.snr = update.snr; + } + if (update.has_rssi) + { + update_scratch_.observations.has_rssi = true; + update_scratch_.observations.rssi = update.rssi; + } + if (update.has_device_metrics) + { + update_scratch_.observations.has_device_metrics = true; + update_scratch_.observations.device_metrics = update.device_metrics; + } + if (update.has_position) + { + update_scratch_.observations.has_position = true; + update_scratch_.observations.position = update.position; + } + + MeshPeerNodeFacts* facts = mutable_node_facts(update_scratch_); + if (facts) + { + copyMeshPeerText(facts->short_name, + sizeof(facts->short_name), + update.short_name); + copyMeshPeerText(facts->long_name, + sizeof(facts->long_name), + update.long_name); + if (update.has_role) + { + facts->role = update.role; + } + if (update.has_hw_model) + { + facts->hw_model = update.hw_model; + } + if (update.has_channel) + { + facts->channel = update.channel; + } + if (update.has_hops_away) + { + facts->hops_away = update.hops_away; + } + if (update.has_macaddr) + { + facts->has_macaddr = true; + std::memcpy(facts->macaddr, + update.macaddr, + sizeof(facts->macaddr)); + } + if (update.has_via_mqtt) + { + facts->via_mqtt = update.via_mqtt; + } + if (update.has_next_hop) + { + if (protocol == MeshProtocol::Meshtastic) + { + update_scratch_.meshtastic.has_next_hop = true; + update_scratch_.meshtastic.next_hop = update.next_hop; + } + else + { + update_scratch_.meshcore.has_next_hop = true; + update_scratch_.meshcore.next_hop = update.next_hop; + } + } + if (protocol == MeshProtocol::MeshCore) + { + update_scratch_.meshcore.node_id_hint = node_id; + } + } + + if (!directory_.record(update_scratch_).succeeded()) + { + return; + } + if (directory_.findByNodeId(protocol, node_id, lookup_scratch_).succeeded()) + { + if (update.has_is_ignored) + { + MeshPeerUserFlags flags = lookup_scratch_.flags; + flags.ignored = update.is_ignored; + (void)directory_.setUserFlags(lookup_scratch_.identity, flags); + } + if (update.has_key_manually_verified) + { + (void)directory_.setKeyManuallyVerified( + lookup_scratch_.identity, + update.key_manually_verified); + } + } invalidateCache(); } -void ContactService::updateNodeInfo(uint32_t node_id, const char* short_name, const char* long_name, - float snr, float rssi, uint32_t now_secs, uint8_t protocol, uint8_t role, - uint8_t hops_away, uint8_t hw_model, uint8_t channel) +bool ContactService::recordPeer(const MeshPeerRecord& record) { - CONTACT_SERVICE_LOG("[ContactService] updateNodeInfo node=%08lX snr=%.1f rssi=%.1f ts=%lu\n", - (unsigned long)node_id, - snr, - rssi, - (unsigned long)now_secs); + const bool recorded = directory_.record(record).succeeded(); + if (recorded) + { + invalidateCache(); + } + return recorded; +} + +void ContactService::updateNodeInfo(uint32_t node_id, + const char* short_name, + const char* long_name, + float snr, + float rssi, + uint32_t now_secs, + uint8_t protocol, + uint8_t role, + uint8_t hops_away, + uint8_t hw_model, + uint8_t channel) +{ + CONTACT_SERVICE_LOG( + "[ContactService] updateNodeInfo node=%08" PRIX32 "\n", node_id); NodeUpdate update{}; update.short_name = short_name; update.long_name = long_name; @@ -140,64 +452,61 @@ void ContactService::updateNodeInfo(uint32_t node_id, const char* short_name, co update.snr = snr; update.has_rssi = !std::isnan(rssi); update.rssi = rssi; - update.has_protocol = (protocol != 0); + update.has_protocol = protocol != 0; update.protocol = protocol; - update.has_role = (role != kNodeRoleUnknown); + update.has_role = role != kNodeRoleUnknown; update.role = role; - update.has_hops_away = (hops_away != 0xFF); + update.has_hops_away = hops_away != 0xFF; update.hops_away = hops_away; - update.has_hw_model = (hw_model != 0); + update.has_hw_model = hw_model != 0; update.hw_model = hw_model; - update.has_channel = (channel != 0xFF); + update.has_channel = channel != 0xFF; update.channel = channel; applyNodeUpdate(node_id, update); } -void ContactService::updateNodeProtocol(uint32_t node_id, uint8_t protocol, uint32_t now_secs) +void ContactService::updateNodeProtocol(uint32_t node_id, + uint8_t protocol, + uint32_t now_secs) { NodeUpdate update{}; - update.has_protocol = (protocol != 0); + update.has_protocol = protocol != 0; update.protocol = protocol; update.has_last_seen = true; update.last_seen = now_secs; applyNodeUpdate(node_id, update); } -void ContactService::updateNodePosition(uint32_t node_id, const NodePosition& pos) +void ContactService::updateNodePosition(uint32_t node_id, + const NodePosition& position) { NodeUpdate update{}; update.has_position = true; - update.position = pos; + update.position = position; applyNodeUpdate(node_id, update); } +bool ContactService::setNextHop(uint32_t node_id, uint8_t next_hop) +{ + NodeUpdate update{}; + update.has_next_hop = true; + update.next_hop = next_hop; + update.has_last_seen = true; + update.last_seen = ::sys::epoch_seconds_now(); + applyNodeUpdate(node_id, update); + return getNextHop(node_id) == next_hop; +} + +uint8_t ContactService::getNextHop(uint32_t node_id) const +{ + const PeerDirectoryItem* peer = getPeerByNodeId(node_id); + return peer ? peer->next_hop : 0; +} + std::string ContactService::getContactName(uint32_t node_id) const { - std::string nickname = contact_store_.getNickname(node_id); - if (!nickname.empty()) - { - return nickname; - } - - buildCache(); - for (const auto& node : cached_nodes_) - { - if (node.node_id == node_id) - { - return preferred_node_name(node); - } - } - - const auto& entries = node_store_.getEntries(); - for (const auto& entry : entries) - { - if (entry.node_id == node_id) - { - return preferred_node_name(entry.short_name, entry.long_name); - } - } - - return std::string(); + const PeerDirectoryItem* peer = getPeerByNodeId(node_id); + return peer ? preferred_node_name(*peer) : std::string(); } std::string ContactService::getReticulumContactName( @@ -205,152 +514,107 @@ std::string ContactService::getReticulumContactName( { if (!hasReticulumDestinationIdentity(identity)) { - return std::string(); + return {}; } - buildCache(); - for (const auto& node : cached_nodes_) + for (const PeerDirectoryItem& peer : cached_nodes_) { - if (!sameReticulumDestinationHash(node.reticulum_identity, identity)) + if (sameReticulumDestinationHash(peer.reticulum_identity, identity)) { - continue; + return preferred_node_name(peer); } - - const std::string nickname = contact_store_.getNickname(node.node_id); - if (is_reticulum_node(node)) - { - const std::string name = preferred_node_name(node); - if (!name.empty()) - { - return name; - } - } - if (!nickname.empty()) - { - return nickname; - } - return preferred_node_name(node); } - - const auto& entries = node_store_.getEntries(); - for (const auto& entry : entries) - { - if (!sameReticulumDestinationHash(entry.reticulum_identity, identity)) - { - continue; - } - - const std::string nickname = contact_store_.getNickname(entry.node_id); - if (is_reticulum_entry(entry)) - { - const std::string name = - preferred_node_name(entry.short_name, entry.long_name); - if (!name.empty()) - { - return name; - } - } - if (!nickname.empty()) - { - return nickname; - } - return preferred_node_name(entry.short_name, entry.long_name); - } - - return std::string(); + return {}; } -std::vector ContactService::getContacts() const +std::vector ContactService::getContacts() const { buildCache(); - std::vector contacts; - for (const auto& node : cached_nodes_) + std::vector result; + for (const PeerDirectoryItem& peer : cached_nodes_) { - if (node.is_contact) + if (peer.is_contact) { - contacts.push_back(node); + result.push_back(peer); } } - return contacts; + return result; } -std::vector ContactService::getNearby() const +std::vector ContactService::getNearby() const { buildCache(); - std::vector nearby; - for (const auto& node : cached_nodes_) + std::vector result; + for (const PeerDirectoryItem& peer : cached_nodes_) { - if (!node.is_contact && !node.is_ignored && isNodeVisible(node.last_seen)) + if (!peer.is_contact && !peer.is_ignored && + isNodeVisible(peer.last_seen)) { - nearby.push_back(node); + result.push_back(peer); } } - return nearby; + return result; } -std::vector ContactService::getIgnoredNodes() const +std::vector ContactService::getIgnoredNodes() const { buildCache(); - std::vector ignored; - for (const auto& node : cached_nodes_) + std::vector result; + for (const PeerDirectoryItem& peer : cached_nodes_) { - if (!node.is_contact && node.is_ignored && isNodeVisible(node.last_seen)) + if (!peer.is_contact && peer.is_ignored && + isNodeVisible(peer.last_seen)) { - ignored.push_back(node); + result.push_back(peer); } } - return ignored; + return result; +} + +std::vector ContactService::getAllPeers() const +{ + buildCache(); + return cached_nodes_; } bool ContactService::addContact(uint32_t node_id, const char* nickname) { - if (!ensureNodeExistsForContact(node_id)) - { - return false; - } - if (contact_store_.setNickname(node_id, nickname)) - { - invalidateCache(); - return true; - } - return false; + return editContact(node_id, nickname); } bool ContactService::editContact(uint32_t node_id, const char* nickname) { - if (!ensureNodeExistsForContact(node_id)) + if (!nickname || nickname[0] == '\0' || + !ensureNodeExistsForContact(node_id) || + !directory_.setUserAlias(lookup_scratch_.identity, nickname).succeeded()) { return false; } - if (contact_store_.setNickname(node_id, nickname)) - { - invalidateCache(); - return true; - } - return false; + invalidateCache(); + return true; } bool ContactService::removeContact(uint32_t node_id) { - if (contact_store_.removeNickname(node_id)) + if (!hasNodeEntry(node_id) || + !directory_.setUserAlias(lookup_scratch_.identity, "").succeeded()) { - invalidateCache(); - return true; + return false; } - return false; + invalidateCache(); + return true; } bool ContactService::removeNode(uint32_t node_id) { - bool removed = false; - if (contact_store_.removeNickname(node_id)) + if (!hasNodeEntry(node_id)) { - removed = true; - } - if (node_store_.remove(node_id)) - { - removed = true; + return false; } + const MeshPeerIdentity identity = lookup_scratch_.identity; + (void)directory_.setUserAlias(identity, ""); + (void)directory_.setUserFlags(identity, MeshPeerUserFlags{}); + const bool removed = directory_.remove(identity).succeeded(); if (removed) { invalidateCache(); @@ -364,36 +628,44 @@ bool ContactService::setNodeIgnored(uint32_t node_id, bool ignored) { return false; } - - NodeUpdate update{}; - update.has_is_ignored = true; - update.is_ignored = ignored; - applyNodeUpdate(node_id, update); - return true; + MeshPeerUserFlags flags = lookup_scratch_.flags; + flags.ignored = ignored; + const bool updated = + directory_.setUserFlags(lookup_scratch_.identity, flags).succeeded(); + if (updated) + { + invalidateCache(); + } + return updated; } -bool ContactService::setNodeKeyManuallyVerified(uint32_t node_id, bool verified) +bool ContactService::setNodeKeyManuallyVerified(uint32_t node_id, + bool verified) { if (!hasNodeEntry(node_id)) { return false; } - - NodeUpdate update{}; - update.has_key_manually_verified = true; - update.key_manually_verified = verified; - applyNodeUpdate(node_id, update); - return true; + const bool updated = directory_.setKeyManuallyVerified( + lookup_scratch_.identity, + verified) + .succeeded(); + if (updated) + { + invalidateCache(); + } + return updated; } -const NodeInfo* ContactService::getNodeInfo(uint32_t node_id) const +const PeerDirectoryItem* ContactService::getPeerByNodeId( + uint32_t node_id) const { buildCache(); - for (const auto& node : cached_nodes_) + for (const PeerDirectoryItem& peer : cached_nodes_) { - if (node.node_id == node_id) + if (peer.node_id == node_id) { - return &node; + return &peer; } } return nullptr; @@ -407,18 +679,11 @@ bool ContactService::findNodeIdByReticulumDestinationHash( { return false; } - - const auto& entries = node_store_.getEntries(); - for (const auto& entry : entries) - { - if (sameReticulumDestinationHash(entry.reticulum_identity, - destination_hash)) - { - *out_node_id = entry.node_id; - return true; - } - } - return false; + ReticulumLookupVisitor visitor(destination_hash, out_node_id); + (void)directory_.visit(MeshProtocol::Reticulum, + MeshPeerDirectoryView::All, + visitor); + return visitor.found(); } void ContactService::clearCache() @@ -434,132 +699,32 @@ void ContactService::invalidateCache() const void ContactService::buildCache() const { - uint32_t now_ms = sys::millis_now(); - if (cache_timestamp_ != 0 && (now_ms - cache_timestamp_) < kCacheTimeoutMs) + const uint32_t now_ms = sys::millis_now(); + if (cache_timestamp_ != 0 && + (now_ms - cache_timestamp_) < kCacheTimeoutMs) { return; } - cached_nodes_.clear(); - - std::vector contact_ids = contact_store_.getAllContactIds(); - - const auto& node_entries = node_store_.getEntries(); - for (const auto& entry : node_entries) - { - const MeshProtocol entry_protocol = - entry.protocol == static_cast(MeshProtocol::RNode) - ? MeshProtocol::Reticulum - : static_cast(entry.protocol); - if (entry_protocol != active_protocol_) - { - continue; - } - if (!isNodeVisible(entry.last_seen)) - { - continue; - } - - NodeInfo info{}; - info.node_id = entry.node_id; - strncpy(info.short_name, entry.short_name, sizeof(info.short_name) - 1); - info.short_name[sizeof(info.short_name) - 1] = '\0'; - strncpy(info.long_name, entry.long_name, sizeof(info.long_name) - 1); - info.long_name[sizeof(info.long_name) - 1] = '\0'; - info.last_seen = entry.last_seen; - info.snr = entry.snr; - info.rssi = entry.rssi; - info.hops_away = entry.hops_away; - info.channel = entry.channel; - info.protocol = static_cast(entry.protocol); - info.role = static_cast(entry.role); - info.hw_model = entry.hw_model; - info.next_hop = entry.next_hop; - info.has_macaddr = entry.has_macaddr; - std::memcpy(info.macaddr, entry.macaddr, sizeof(info.macaddr)); - info.via_mqtt = entry.via_mqtt; - info.is_ignored = entry.is_ignored; - info.has_public_key = entry.has_public_key; - info.key_manually_verified = entry.key_manually_verified; - info.reticulum_identity = entry.reticulum_identity; - info.has_device_metrics = entry.has_device_metrics; - info.device_metrics = entry.device_metrics; - info.position.valid = entry.position_valid; - info.position.latitude_i = entry.position_latitude_i; - info.position.longitude_i = entry.position_longitude_i; - info.position.has_altitude = entry.position_has_altitude; - info.position.altitude = entry.position_altitude; - info.position.timestamp = entry.position_timestamp; - info.position.precision_bits = entry.position_precision_bits; - info.position.pdop = entry.position_pdop; - info.position.hdop = entry.position_hdop; - info.position.vdop = entry.position_vdop; - info.position.gps_accuracy_mm = entry.position_gps_accuracy_mm; - - if (info.short_name[0] == '\0') - { - std::snprintf(info.short_name, sizeof(info.short_name), "%04X", - static_cast(info.node_id & 0xFFFF)); - } - - info.is_contact = std::find(contact_ids.begin(), contact_ids.end(), entry.node_id) != contact_ids.end(); - - const std::string nickname = - info.is_contact ? contact_store_.getNickname(entry.node_id) : std::string(); - const bool reticulum_node = is_reticulum_node(info); - if (reticulum_node) - { - info.display_name = preferred_node_name(info.short_name, info.long_name); - if (info.display_name.empty() && !nickname.empty()) - { - info.display_name = nickname; - } - } - else if (!nickname.empty()) - { - info.display_name = nickname; - } - else - { - info.display_name = preferred_node_name(info.short_name, info.long_name); - } - - cached_nodes_.push_back(info); - } - + ProjectionVisitor visitor(cached_nodes_); + (void)directory_.visit(active_protocol_, + MeshPeerDirectoryView::All, + visitor); cache_timestamp_ = now_ms; } bool ContactService::ensureNodeExistsForContact(uint32_t node_id) { - if (node_id == 0) - { - return false; - } - - if (hasNodeEntry(node_id)) - { - return true; - } - - NodeUpdate update{}; - update.has_last_seen = true; - update.last_seen = 0; - applyNodeUpdate(node_id, update); - return hasNodeEntry(node_id); + return hasNodeEntry(node_id) && + meshPeerIsStableContactIdentity(lookup_scratch_.identity); } bool ContactService::hasNodeEntry(uint32_t node_id) const { - const auto& entries = node_store_.getEntries(); - for (size_t i = 0; i < entries.size(); ++i) - { - if (entries[i].node_id == node_id) - { - return true; - } - } - return false; + return node_id != 0 && + directory_ + .findByNodeId(active_protocol_, node_id, lookup_scratch_) + .succeeded(); } bool ContactService::isNodeVisible(uint32_t last_seen) const @@ -570,44 +735,39 @@ bool ContactService::isNodeVisible(uint32_t last_seen) const std::string ContactService::formatTimeStatus(uint32_t last_seen) const { - uint32_t now_secs = sys::epoch_seconds_now(); + const uint32_t now_secs = sys::epoch_seconds_now(); if (now_secs < last_seen) { return "Offline"; } - - uint32_t age_secs = now_secs - last_seen; - + const uint32_t age_secs = now_secs - last_seen; if (age_secs <= 120) { return "Online"; } - + char buffer[24] = {}; if (age_secs < 3600) { - uint32_t minutes = age_secs / 60; - char buf[16]; - std::snprintf(buf, sizeof(buf), "Seen %lum", static_cast(minutes)); - return std::string(buf); + std::snprintf(buffer, + sizeof(buffer), + "Seen %lum", + static_cast(age_secs / 60)); } - - if (age_secs < 86400) + else if (age_secs < 86400) { - uint32_t hours = age_secs / 3600; - char buf[16]; - std::snprintf(buf, sizeof(buf), "Seen %luh", static_cast(hours)); - return std::string(buf); + std::snprintf(buffer, + sizeof(buffer), + "Seen %luh", + static_cast(age_secs / 3600)); } - - if (age_secs < 6 * 86400) + else { - uint32_t days = age_secs / 86400; - char buf[16]; - std::snprintf(buf, sizeof(buf), "Seen %lud", static_cast(days)); - return std::string(buf); + std::snprintf(buffer, + sizeof(buffer), + "Seen %lud", + static_cast(age_secs / 86400)); } - - return "Offline"; + return buffer; } } // namespace contacts diff --git a/modules/core_chat/tests/test_mesh_peer_directory_contract.cpp b/modules/core_chat/tests/test_mesh_peer_directory_contract.cpp index e527f88e..6d6c6596 100644 --- a/modules/core_chat/tests/test_mesh_peer_directory_contract.cpp +++ b/modules/core_chat/tests/test_mesh_peer_directory_contract.cpp @@ -1,4 +1,5 @@ #include "chat/infra/mesh_peer_directory_core.h" +#include "chat/usecase/contact_service.h" #include #include @@ -215,6 +216,78 @@ class MemoryMeshPeerDirectory final : public chat::IMeshPeerDirectory chat::MeshPeerDirectoryStatusCode::NotFound); } + chat::MeshPeerDirectoryStatus visit( + chat::MeshProtocol protocol, + chat::MeshPeerDirectoryView view, + chat::IMeshPeerDirectoryVisitor& visitor) override + { + for (const auto& record : records_) + { + const bool contact = chat::meshPeerIsContact(record); + const bool matches = + view == chat::MeshPeerDirectoryView::All || + (view == chat::MeshPeerDirectoryView::Contacts && contact) || + (view == chat::MeshPeerDirectoryView::Nearby && !contact && + !record.flags.ignored) || + (view == chat::MeshPeerDirectoryView::Ignored && !contact && + record.flags.ignored); + if (chat::meshPeerSameProtocol(record.identity.protocol, protocol) && + matches && !visitor.visit(record)) + { + break; + } + } + return chat::MeshPeerDirectoryStatus::success(); + } + + chat::MeshPeerDirectoryStatus setUserAlias( + const chat::MeshPeerIdentity& identity, + const char* alias) override + { + for (auto& record : records_) + { + if (chat::sameMeshPeerIdentity(record.identity, identity)) + { + chat::copyMeshPeerText(record.user_alias, + sizeof(record.user_alias), + alias); + record.flags.favorite = alias && alias[0] != '\0'; + return chat::MeshPeerDirectoryStatus::success(); + } + } + return chat::MeshPeerDirectoryStatus::fail( + chat::MeshPeerDirectoryStatusCode::NotFound); + } + + chat::MeshPeerDirectoryStatus setKeyManuallyVerified( + const chat::MeshPeerIdentity& identity, + bool verified) override + { + for (auto& record : records_) + { + if (!chat::sameMeshPeerIdentity(record.identity, identity)) + { + continue; + } + if (record.identity.protocol == chat::MeshProtocol::Meshtastic) + { + record.meshtastic.key_manually_verified = verified; + } + else if (record.identity.protocol == chat::MeshProtocol::MeshCore) + { + record.meshcore.public_key_verified = verified; + } + else + { + return chat::MeshPeerDirectoryStatus::fail( + chat::MeshPeerDirectoryStatusCode::Unsupported); + } + return chat::MeshPeerDirectoryStatus::success(); + } + return chat::MeshPeerDirectoryStatus::fail( + chat::MeshPeerDirectoryStatusCode::NotFound); + } + chat::MeshPeerDirectoryStatus remove( const chat::MeshPeerIdentity& identity) override { @@ -728,6 +801,7 @@ void core_persists_records_and_preserves_user_flags() flags.favorite = true; flags.trusted = true; assert(directory.setUserFlags(alpha_identity, flags).succeeded()); + assert(directory.setUserAlias(alpha_identity, "Alpha").succeeded()); auto updated = makeMeshtasticPeer(0x0C16AAEC, "alpha fresh", 30); assert(directory.record(updated).succeeded()); @@ -746,6 +820,7 @@ void core_persists_records_and_preserves_user_flags() assert(std::strcmp(loaded.display_name, "alpha fresh") == 0); assert(loaded.flags.favorite); assert(loaded.flags.trusted); + assert(std::strcmp(loaded.user_alias, "Alpha") == 0); } } @@ -871,6 +946,33 @@ void remove_and_clear_protocol_are_directory_behaviors() assert(!directory.find(meshcore.identity, loaded).succeeded()); } +void contact_service_projects_one_directory_without_legacy_stores() +{ + CountingMeshPeerDirectoryBlobStore blob; + chat::MeshPeerDirectoryCore directory(blob); + chat::contacts::ContactService contacts(directory); + contacts.begin(); + + constexpr chat::NodeId node_id = 0x10203040U; + contacts.updateNodeInfo(node_id, + "PEER", + "Unified Peer", + 8.5F, + -79.0F, + 123U, + static_cast( + chat::contacts::NodeProtocolType::Meshtastic)); + assert(contacts.addContact(node_id, "Alias")); + assert(contacts.getContacts().size() == 1U); + assert(contacts.getContactName(node_id) == "Alias"); + + assert(contacts.setNextHop(node_id, 0x42U)); + assert(contacts.getNextHop(node_id) == 0x42U); + const auto all = contacts.getAllPeers(); + assert(all.size() == 1U); + assert(all.front().display_name == "Alias"); +} + } // namespace int main() @@ -885,5 +987,6 @@ int main() core_merges_sparse_meshtastic_records_without_losing_key_or_node_facts(); verified_keys_cannot_be_replaced_by_runtime_observations(); remove_and_clear_protocol_are_directory_behaviors(); + contact_service_projects_one_directory_without_legacy_stores(); return 0; } diff --git a/modules/core_chat/tests/test_node_store_core.cpp b/modules/core_chat/tests/test_node_store_core.cpp deleted file mode 100644 index ea4c922c..00000000 --- a/modules/core_chat/tests/test_node_store_core.cpp +++ /dev/null @@ -1,385 +0,0 @@ -#include "chat/infra/contact_store_core.h" -#include "chat/infra/node_store_core.h" -#include "chat/usecase/contact_service.h" -#include "sys/clock.h" - -#include -#include -#include -#include -#include - -namespace -{ - -uint32_t test_millis_now() -{ - return 0; -} - -class CountingBlobStore final : public chat::contacts::INodeBlobStore -{ - public: - bool loadBlob(std::vector& out) override - { - out = blob; - return load_ok; - } - - bool saveBlob(const uint8_t* data, size_t len) override - { - blob.assign(data, data + len); - ++save_count; - return save_ok; - } - - void clearBlob() override - { - blob.clear(); - ++clear_count; - } - - std::vector blob; - bool load_ok = false; - bool save_ok = true; - int save_count = 0; - int clear_count = 0; -}; - -class CountingContactBlobStore final : public chat::IContactBlobStore -{ - public: - bool loadBlob(std::vector& out) override - { - out = blob; - return load_ok; - } - - bool saveBlob(const uint8_t* data, size_t len) override - { - blob.assign(data, data + len); - ++save_count; - return save_ok; - } - - std::vector blob; - bool load_ok = false; - bool save_ok = true; - int save_count = 0; -}; - -class EmptyContactStore final : public chat::contacts::IContactStore -{ - public: - void begin() override {} - std::string getNickname(uint32_t node_id) const override - { - (void)node_id; - return std::string(); - } - bool setNickname(uint32_t node_id, const char* nickname) override - { - (void)node_id; - (void)nickname; - return false; - } - bool removeNickname(uint32_t node_id) override - { - (void)node_id; - return false; - } - bool hasNickname(const char* nickname) const override - { - (void)nickname; - return false; - } - std::vector getAllContactIds() const override - { - return {}; - } - size_t getCount() const override - { - return 0; - } -}; - -void volatile_node_updates_do_not_force_persist() -{ - CountingBlobStore blob; - chat::contacts::NodeStoreCore store(blob); - store.setAutoSaveEnabled(true); - store.begin(); - - store.upsert(0xAABBCCDD, - "CCDD", - "node-ccdd", - 10, - 1.0f, - -80.0f, - static_cast(chat::contacts::NodeProtocolType::Meshtastic), - static_cast(chat::contacts::NodeRoleType::Client), - 1, - 42, - 0); - assert(blob.save_count == 1); - - chat::contacts::NodeUpdate update{}; - update.has_last_seen = true; - update.last_seen = 20; - update.has_snr = true; - update.snr = 5.0f; - update.has_rssi = true; - update.rssi = -60.0f; - update.has_hops_away = true; - update.hops_away = 2; - update.has_channel = true; - update.channel = 1; - store.applyUpdate(0xAABBCCDD, update); - - assert(blob.save_count == 1); - const auto& entry = store.getEntries().front(); - assert(entry.last_seen == 20); - assert(entry.snr == 5.0f); - assert(entry.rssi == -60.0f); - assert(entry.hops_away == 2); - assert(entry.channel == 1); -} - -void persistent_node_updates_still_flush() -{ - CountingBlobStore blob; - chat::contacts::NodeStoreCore store(blob); - store.setAutoSaveEnabled(true); - store.begin(); - - store.upsert(0x01020304, - "0304", - "node-0304", - 10, - 1.0f, - -80.0f, - static_cast(chat::contacts::NodeProtocolType::Meshtastic), - static_cast(chat::contacts::NodeRoleType::Client), - 1, - 42, - 0); - assert(blob.save_count == 1); - - chat::contacts::NodeUpdate name_update{}; - name_update.long_name = "node-renamed"; - store.applyUpdate(0x01020304, name_update); - assert(blob.save_count == 2); - - chat::contacts::NodeUpdate position_update{}; - position_update.has_position = true; - position_update.position.valid = true; - position_update.position.latitude_i = 266777300; - position_update.position.longitude_i = 1072822500; - position_update.position.has_altitude = true; - position_update.position.altitude = 1880; - position_update.position.timestamp = 1234; - store.applyUpdate(0x01020304, position_update); - assert(blob.save_count == 3); - - position_update.position.timestamp = 5678; - store.applyUpdate(0x01020304, position_update); - assert(blob.save_count == 3); -} - -void reticulum_identity_updates_are_persisted_and_lookupable() -{ - CountingBlobStore blob; - chat::contacts::NodeStoreCore store(blob); - store.setAutoSaveEnabled(true); - store.begin(); - - store.upsert(0xC001D00D, - "D00D", - "reticulum-peer", - 10, - 1.0f, - -80.0f, - static_cast(chat::contacts::NodeProtocolType::Reticulum), - static_cast(chat::contacts::NodeRoleType::Client), - 1, - 0, - 0xFF); - assert(blob.save_count == 1); - - chat::contacts::NodeUpdate update{}; - uint8_t destination_hash[chat::contacts::kReticulumPeerHashSize] = {}; - uint8_t identity_hash[chat::contacts::kReticulumPeerHashSize] = {}; - for (std::size_t index = 0; index < chat::contacts::kReticulumPeerHashSize; ++index) - { - destination_hash[index] = static_cast(0x10U + index); - identity_hash[index] = static_cast(0x40U + index); - } - update.reticulum_identity = - chat::makeReticulumPeerIdentity(destination_hash, identity_hash); - - store.applyUpdate(0xC001D00D, update); - - assert(blob.save_count == 2); - assert(blob.blob.size() == chat::contacts::NodeStoreCore::kSerializedEntrySize); - const auto& entry = store.getEntries().front(); - assert(entry.reticulum_identity.valid); - assert(std::memcmp(entry.reticulum_identity.destination_hash, - update.reticulum_identity.destination_hash, - chat::contacts::kReticulumPeerHashSize) == 0); - assert(std::memcmp(entry.reticulum_identity.identity_hash, - update.reticulum_identity.identity_hash, - chat::contacts::kReticulumPeerHashSize) == 0); - - CountingBlobStore persisted_blob; - persisted_blob.load_ok = true; - persisted_blob.blob = blob.blob; - chat::contacts::NodeStoreCore restored_store(persisted_blob); - restored_store.begin(); - const auto& restored_entry = restored_store.getEntries().front(); - assert(restored_entry.reticulum_identity.valid); - assert(std::memcmp(restored_entry.reticulum_identity.destination_hash, - update.reticulum_identity.destination_hash, - chat::contacts::kReticulumPeerHashSize) == 0); - assert(std::memcmp(restored_entry.reticulum_identity.identity_hash, - update.reticulum_identity.identity_hash, - chat::contacts::kReticulumPeerHashSize) == 0); - - EmptyContactStore contact_store; - chat::contacts::ContactService contact_service(restored_store, contact_store); - uint32_t projected_node_id = 0; - assert(contact_service.findNodeIdByReticulumDestinationHash( - update.reticulum_identity.destination_hash, - &projected_node_id)); - assert(projected_node_id == 0xC001D00D); -} - -void v8_node_blobs_decode_without_reticulum_identity() -{ - CountingBlobStore blob; - chat::contacts::NodeStoreCore store(blob); - store.setAutoSaveEnabled(true); - store.begin(); - - store.upsert(0x0102A0B0, - "A0B0", - "legacy-node", - 10, - 1.0f, - -80.0f, - static_cast(chat::contacts::NodeProtocolType::Meshtastic), - static_cast(chat::contacts::NodeRoleType::Client), - 1, - 42, - 0); - assert(blob.blob.size() == chat::contacts::NodeStoreCore::kSerializedEntrySize); - - std::vector legacy_blob(blob.blob.begin(), - blob.blob.begin() + - chat::contacts::NodeStoreCore::kSerializedEntrySizeV8); - std::vector decoded; - assert(chat::contacts::NodeStoreCore::decodeBlob(decoded, - legacy_blob.data(), - legacy_blob.size(), - chat::contacts::NodeStoreCore::kPersistVersionV8)); - assert(decoded.size() == 1); - assert(decoded.front().node_id == 0x0102A0B0); - assert(!decoded.front().reticulum_identity.valid); -} - -void node_store_capacity_is_limited_to_200_entries() -{ - static_assert(chat::contacts::NodeStoreCore::kMaxNodes == 200, - "Node persistence budget is sized for 200 nodes"); - - CountingBlobStore blob; - chat::contacts::NodeStoreCore store(blob); - store.setAutoSaveEnabled(false); - store.begin(); - - for (uint32_t index = 0; index < chat::contacts::NodeStoreCore::kMaxNodes + 1; ++index) - { - const uint32_t node_id = 0x1000U + index; - char short_name[10] = {}; - char long_name[32] = {}; - std::snprintf(short_name, sizeof(short_name), "%04lX", static_cast(node_id & 0xFFFFU)); - std::snprintf(long_name, sizeof(long_name), "node-%02lu", static_cast(index)); - - store.upsert(node_id, - short_name, - long_name, - index + 1, - 1.0f, - -80.0f, - static_cast(chat::contacts::NodeProtocolType::Meshtastic), - static_cast(chat::contacts::NodeRoleType::Client), - 1, - 42, - 0); - } - - assert(store.getEntries().size() == chat::contacts::NodeStoreCore::kMaxNodes); - - bool oldest_evicted = true; - for (const auto& entry : store.getEntries()) - { - if (entry.node_id == 0x1000U) - { - oldest_evicted = false; - } - } - assert(oldest_evicted); -} - -void contact_store_allows_reapplying_and_duplicate_display_names() -{ - CountingContactBlobStore blob; - chat::contacts::ContactStoreCore store(blob); - store.begin(); - - assert(store.setNickname(0x01020304U, "Unknown Peer")); - assert(blob.save_count == 1); - assert(store.setNickname(0x01020304U, "Unknown Peer")); - assert(blob.save_count == 1); - assert(store.setNickname(0x05060708U, "Unknown Peer")); - assert(blob.save_count == 2); - assert(store.getCount() == 2); - assert(store.getNickname(0x01020304U) == "Unknown Peer"); - assert(store.getNickname(0x05060708U) == "Unknown Peer"); -} - -void contact_store_rolls_back_failed_saves() -{ - CountingContactBlobStore blob; - chat::contacts::ContactStoreCore store(blob); - store.begin(); - - blob.save_ok = false; - assert(!store.setNickname(0x01020304U, "RT-01020304")); - assert(store.getCount() == 0); - assert(store.getNickname(0x01020304U).empty()); - - blob.save_ok = true; - assert(store.setNickname(0x01020304U, "RT-01020304")); - assert(store.getCount() == 1); - assert(store.getNickname(0x01020304U) == "RT-01020304"); - - blob.save_ok = false; - assert(!store.setNickname(0x01020304U, "RT-05060708")); - assert(store.getCount() == 1); - assert(store.getNickname(0x01020304U) == "RT-01020304"); -} - -} // namespace - -int main() -{ - sys::set_millis_provider(test_millis_now); - volatile_node_updates_do_not_force_persist(); - persistent_node_updates_still_flush(); - reticulum_identity_updates_are_persisted_and_lookupable(); - v8_node_blobs_decode_without_reticulum_identity(); - node_store_capacity_is_limited_to_200_entries(); - contact_store_allows_reapplying_and_duplicate_display_names(); - contact_store_rolls_back_failed_saves(); - return 0; -} diff --git a/modules/core_mesh/include/mesh/ports/i_node_store.h b/modules/core_mesh/include/mesh/ports/i_node_store.h deleted file mode 100644 index 499accae..00000000 --- a/modules/core_mesh/include/mesh/ports/i_node_store.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "mesh/domain/mesh_result.h" -#include "mesh/domain/node_id.h" - -namespace mesh -{ - -struct NodeInfoView -{ - NodeId node_id; - uint32_t last_seen_ms = 0; - bool direct_reachable = false; -}; - -class INodeStore -{ - public: - virtual ~INodeStore() = default; - - virtual StoreResult get(NodeId node_id, NodeInfoView& out) = 0; - virtual StoreResult put(const NodeInfoView& node) = 0; -}; - -} // namespace mesh diff --git a/modules/core_sys/include/app/app_context_platform_bindings.h b/modules/core_sys/include/app/app_context_platform_bindings.h index 109b76d0..de4d9380 100644 --- a/modules/core_sys/include/app/app_context_platform_bindings.h +++ b/modules/core_sys/include/app/app_context_platform_bindings.h @@ -9,10 +9,8 @@ #include "chat/domain/chat_model.h" #include "chat/domain/chat_types.h" #include "chat/ports/i_chat_store.h" -#include "chat/ports/i_contact_store.h" #include "chat/ports/i_mesh_adapter.h" #include "chat/ports/i_mesh_peer_directory.h" -#include "chat/ports/i_node_store.h" #include "chat/ports/i_protocol_peer_repository.h" #include "chat/usecase/chat_service.h" #include "chat/usecase/contact_service.h" diff --git a/modules/core_sys/include/app/app_facades.h b/modules/core_sys/include/app/app_facades.h index e22b0e8f..205207c0 100644 --- a/modules/core_sys/include/app/app_facades.h +++ b/modules/core_sys/include/app/app_facades.h @@ -30,7 +30,6 @@ class IChatDeliveryEventPort; namespace contacts { class ContactService; -class INodeStore; } // namespace contacts namespace ui { @@ -152,8 +151,6 @@ class IAppBleFacade : public IAppFacade { public: ~IAppBleFacade() override = default; - virtual chat::contacts::INodeStore* getNodeStore() = 0; - virtual const chat::contacts::INodeStore* getNodeStore() const = 0; virtual bool getDeviceMacAddress(uint8_t out_mac[6]) const = 0; virtual bool syncCurrentEpochSeconds(uint32_t epoch_seconds) = 0; virtual void resetMeshConfig() = 0; diff --git a/modules/core_sys/include/platform/ui/reticulum_directory_runtime.h b/modules/core_sys/include/platform/ui/reticulum_directory_runtime.h index 40352445..98ca00ee 100644 --- a/modules/core_sys/include/platform/ui/reticulum_directory_runtime.h +++ b/modules/core_sys/include/platform/ui/reticulum_directory_runtime.h @@ -84,6 +84,7 @@ struct LxmfAddressRecord const char* announces_path(); const char* lxmf_addresses_path(); +chat::IMeshPeerDirectory& mesh_peer_directory(); void bind_mesh_peer_directory(chat::IMeshPeerDirectory* directory); Status record_announce(const AnnounceRecord& record); diff --git a/modules/ui_key_verification_runtime/tests/test_key_verification_runtime_adapters.cpp b/modules/ui_key_verification_runtime/tests/test_key_verification_runtime_adapters.cpp index cee53e62..f02c9be5 100644 --- a/modules/ui_key_verification_runtime/tests/test_key_verification_runtime_adapters.cpp +++ b/modules/ui_key_verification_runtime/tests/test_key_verification_runtime_adapters.cpp @@ -1,6 +1,5 @@ -#include "chat/ports/i_contact_store.h" +#include "chat/infra/mesh_peer_directory_core.h" #include "chat/ports/i_mesh_adapter.h" -#include "chat/ports/i_node_store.h" #include "chat/usecase/contact_service.h" #include "ui_key_verification_runtime/key_verification_action_sink.h" #include "ui_key_verification_runtime/key_verification_presentation_source.h" @@ -77,106 +76,29 @@ class FakeMeshAdapter final : public ::chat::IMeshAdapter ::chat::MeshProtocol protocol = ::chat::MeshProtocol::MeshCore; }; -class FakeNodeStore final : public ::chat::contacts::INodeStore +class MemoryPeerDirectoryBlobStore final + : public ::chat::IMeshPeerDirectoryBlobStore { public: - void begin() override {} - - void applyUpdate(uint32_t node_id, - const ::chat::contacts::NodeUpdate& update) override + ::chat::MeshPeerDirectoryBlobLoadResult loadBlob( + std::vector& out) override { - for (auto& entry : entries_) - { - if (entry.node_id == node_id) - { - if (update.has_key_manually_verified) - { - entry.key_manually_verified = - update.key_manually_verified; - } - return; - } - } + out = bytes_; + return bytes_.empty() + ? ::chat::MeshPeerDirectoryBlobLoadResult::Missing + : ::chat::MeshPeerDirectoryBlobLoadResult::Loaded; } - void upsert(uint32_t node_id, - const char* short_name, - const char* long_name, - uint32_t now_secs, - float = 0.0f, - float = 0.0f, - uint8_t protocol = 0, - uint8_t = ::chat::contacts::kNodeRoleUnknown, - uint8_t = 0xFF, - uint8_t = 0, - uint8_t = 0xFF) override + bool saveBlob(const uint8_t* data, std::size_t len) override { - ::chat::contacts::NodeEntry entry{}; - entry.node_id = node_id; - std::strncpy(entry.short_name, short_name ? short_name : "", - sizeof(entry.short_name) - 1U); - std::strncpy(entry.long_name, long_name ? long_name : "", - sizeof(entry.long_name) - 1U); - entry.last_seen = now_secs; - entry.protocol = protocol; - entries_.push_back(entry); - } - - void updateProtocol(uint32_t, uint8_t, uint32_t) override {} - void updatePosition(uint32_t, - const ::chat::contacts::NodePosition&) override {} - bool remove(uint32_t) override { return false; } - const std::vector<::chat::contacts::NodeEntry>& getEntries() const override - { - return entries_; - } - void clear() override { entries_.clear(); } - bool flush() override { return true; } - - std::vector<::chat::contacts::NodeEntry> entries_; -}; - -class FakeContactStore final : public ::chat::contacts::IContactStore -{ - public: - void begin() override {} - - std::string getNickname(uint32_t node_id) const override - { - return node_id == nickname_node ? nickname : std::string(); - } - - bool setNickname(uint32_t node_id, const char* value) override - { - nickname_node = node_id; - nickname = value ? value : ""; + bytes_.assign(data, data + len); return true; } - bool removeNickname(uint32_t node_id) override - { - if (node_id == nickname_node) - { - nickname_node = 0; - nickname.clear(); - return true; - } - return false; - } + void clearBlob() override { bytes_.clear(); } - bool hasNickname(const char*) const override { return false; } - std::vector getAllContactIds() const override - { - if (nickname_node == 0) - { - return {}; - } - return {nickname_node}; - } - size_t getCount() const override { return nickname_node == 0 ? 0U : 1U; } - - uint32_t nickname_node = 0; - std::string nickname; + private: + std::vector bytes_{}; }; } // namespace @@ -185,11 +107,21 @@ int main() { using namespace ui::key_verification; - FakeNodeStore node_store; - node_store.upsert(0x1234, "ADA", "Ada Lovelace", 1); - FakeContactStore contact_store; - assert(contact_store.setNickname(0x1234, "Ada")); - ::chat::contacts::ContactService contacts(node_store, contact_store); + MemoryPeerDirectoryBlobStore blob_store; + ::chat::MeshPeerDirectoryCore directory(blob_store); + ::chat::contacts::ContactService contacts(directory); + contacts.begin(); + contacts.updateNodeInfo(0x1234, "ADA", "Ada Lovelace", 0.0F, 0.0F, 1); + ::chat::MeshPeerRecord keyed_peer{}; + keyed_peer.valid = true; + keyed_peer.identity = ::chat::makeMeshPeerNodeIdentity( + ::chat::MeshProtocol::Meshtastic, + 0x1234); + keyed_peer.last_seen_s = 1; + keyed_peer.meshtastic.has_public_key = true; + keyed_peer.meshtastic.public_key[0] = 0x42; + assert(directory.record(keyed_peer).succeeded()); + assert(contacts.addContact(0x1234, "Ada")); FakeMeshAdapter mesh; ui_key_verification_runtime::KeyVerificationSessionAdapter session{}; @@ -245,7 +177,8 @@ int main() result = sink.accept(0x1234); assert(result.ok); assert(session.state == VerificationState::Verified); - assert(node_store.entries_[0].key_manually_verified); + const auto* verified_peer = contacts.getPeerByNodeId(0x1234); + assert(verified_peer && verified_peer->key_manually_verified); source.onFinal(0x1234, 102, true, "WXYZ-9876"); result = sink.copyCode(0x1234); diff --git a/modules/ui_mono/include/ui/mono/runtime.h b/modules/ui_mono/include/ui/mono/runtime.h index 2d12494b..e0666690 100644 --- a/modules/ui_mono/include/ui/mono/runtime.h +++ b/modules/ui_mono/include/ui/mono/runtime.h @@ -293,7 +293,7 @@ class Runtime : public chat::ChatService::IncomingTextObserver, uint32_t nowMs() const; app::IAppFacade* app() const; const chat::ChatMessage* selectedMessage() const; - const chat::contacts::NodeInfo* selectedNode() const; + const chat::contacts::PeerDirectoryItem* selectedNode() const; void executeNodeAction(); void requestNodePositionExchange(); chat::MessageId nextMeshtasticActionRequestId(chat::NodeId peer); @@ -362,7 +362,7 @@ class Runtime : public chat::ChatService::IncomingTextObserver, size_t conversation_total_ = 0; static constexpr size_t kMaxNodeItems = 16; - chat::contacts::NodeInfo nodes_[kMaxNodeItems]{}; + chat::contacts::PeerDirectoryItem nodes_[kMaxNodeItems]{}; size_t node_count_ = 0; static constexpr size_t kNodeInfoLines = 24; static constexpr size_t kNodeInfoWidth = 40; diff --git a/modules/ui_mono/src/runtime.cpp b/modules/ui_mono/src/runtime.cpp index a3708c6d..5f00fbe3 100644 --- a/modules/ui_mono/src/runtime.cpp +++ b/modules/ui_mono/src/runtime.cpp @@ -3837,7 +3837,7 @@ void Runtime::renderNodeInfo() void Runtime::renderNodeCompass() { - const chat::contacts::NodeInfo* node = selectedNode(); + const chat::contacts::PeerDirectoryItem* node = selectedNode(); char right[12] = {}; if (node != nullptr) { @@ -5480,7 +5480,7 @@ void Runtime::rebuildNodeList() contacts.insert(contacts.end(), nearby.begin(), nearby.end()); contacts.insert(contacts.end(), ignored.begin(), ignored.end()); std::sort(contacts.begin(), contacts.end(), - [](const chat::contacts::NodeInfo& a, const chat::contacts::NodeInfo& b) + [](const chat::contacts::PeerDirectoryItem& a, const chat::contacts::PeerDirectoryItem& b) { if (a.last_seen != b.last_seen) { @@ -5937,7 +5937,7 @@ void Runtime::buildMessageInfo() if (msg->from != 0 && app()) { - if (const auto* node = app()->getContactService().getNodeInfo(msg->from)) + if (const auto* node = app()->getContactService().getPeerByNodeId(msg->from)) { push_section("NODE"); push_kv("NM", node->display_name.empty() ? "-" : node->display_name.c_str()); @@ -8067,7 +8067,7 @@ const chat::ChatMessage* Runtime::selectedMessage() const return &messages_[index]; } -const chat::contacts::NodeInfo* Runtime::selectedNode() const +const chat::contacts::PeerDirectoryItem* Runtime::selectedNode() const { if (node_count_ == 0) { @@ -8203,7 +8203,7 @@ void Runtime::handleMeshtasticActionResult( const char* Runtime::nodeActionLabel(size_t index) const { - const chat::contacts::NodeInfo* node = selectedNode(); + const chat::contacts::PeerDirectoryItem* node = selectedNode(); const bool meshtastic_mode = app() && app()->getConfig().mesh_protocol != chat::MeshProtocol::MeshCore; const bool can_reply = node && app() && chat::infra::meshProtocolFromRaw( @@ -8235,7 +8235,7 @@ void Runtime::executeNodeAction() { auto* mesh = app() ? app()->getMeshAdapter() : nullptr; auto* contacts = app() ? &app()->getContactService() : nullptr; - const chat::contacts::NodeInfo* node = selectedNode(); + const chat::contacts::PeerDirectoryItem* node = selectedNode(); const bool meshtastic_mode = app() && app()->getConfig().mesh_protocol != chat::MeshProtocol::MeshCore; if (!node) { @@ -8413,7 +8413,7 @@ void Runtime::executeNodeAction() void Runtime::requestNodePositionExchange() { auto* mesh = app() ? app()->getMeshAdapter() : nullptr; - const chat::contacts::NodeInfo* node = selectedNode(); + const chat::contacts::PeerDirectoryItem* node = selectedNode(); if (!node) { appendBootLog("pos req node na"); diff --git a/modules/ui_shared/include/ui/screens/contacts/contacts_page_layout.h b/modules/ui_shared/include/ui/screens/contacts/contacts_page_layout.h index 7e59ddad..92cc3ac7 100644 --- a/modules/ui_shared/include/ui/screens/contacts/contacts_page_layout.h +++ b/modules/ui_shared/include/ui/screens/contacts/contacts_page_layout.h @@ -38,10 +38,10 @@ void create_list_panel(lv_obj_t* parent); */ void ensure_list_subcontainers(); -std::string preferred_node_display_name(const chat::contacts::NodeInfo& node); +std::string preferred_node_display_name(const chat::contacts::PeerDirectoryItem& node); lv_obj_t* create_list_item(lv_obj_t* parent, - const chat::contacts::NodeInfo& node, + const chat::contacts::PeerDirectoryItem& node, ContactsMode mode, const char* status_text); diff --git a/modules/ui_shared/include/ui/screens/contacts/contacts_state.h b/modules/ui_shared/include/ui/screens/contacts/contacts_state.h index 789e14bd..933d07da 100644 --- a/modules/ui_shared/include/ui/screens/contacts/contacts_state.h +++ b/modules/ui_shared/include/ui/screens/contacts/contacts_state.h @@ -98,11 +98,11 @@ struct ContactsPageState bool focused_filter_mode_valid = false; // Data (using forward declaration, full type in .cpp) - std::vector contacts_list; - std::vector nearby_list; - std::vector reticulum_group_list; - std::vector ignored_list; - std::vector display_list; + std::vector contacts_list; + std::vector nearby_list; + std::vector reticulum_group_list; + std::vector ignored_list; + std::vector display_list; char search_query[32] = {}; // Timers diff --git a/modules/ui_shared/include/ui/screens/node_info/node_info_page_components.h b/modules/ui_shared/include/ui/screens/node_info/node_info_page_components.h index be296240..115ad85d 100644 --- a/modules/ui_shared/include/ui/screens/node_info/node_info_page_components.h +++ b/modules/ui_shared/include/ui/screens/node_info/node_info_page_components.h @@ -78,7 +78,7 @@ const NodeInfoWidgets& widgets(); /** * @brief Update UI widgets with NodeInfo data. */ -void set_node_info(const chat::contacts::NodeInfo& node); +void set_node_info(const chat::contacts::PeerDirectoryItem& node); } // namespace ui } // namespace node_info diff --git a/modules/ui_shared/src/ui/menu/dashboard/dashboard_compass_widget.cpp b/modules/ui_shared/src/ui/menu/dashboard/dashboard_compass_widget.cpp index 1e89e8d2..e7529933 100644 --- a/modules/ui_shared/src/ui/menu/dashboard/dashboard_compass_widget.cpp +++ b/modules/ui_shared/src/ui/menu/dashboard/dashboard_compass_widget.cpp @@ -49,7 +49,7 @@ void set_label_text_if_changed(lv_obj_t* label, const char* english) } } -std::string best_name_for(const chat::contacts::NodeInfo& info) +std::string best_name_for(const chat::contacts::PeerDirectoryItem& info) { if (!info.display_name.empty()) { @@ -185,7 +185,7 @@ void refresh_compass_widget() continue; } - const chat::contacts::NodeInfo* info = contacts.getNodeInfo(member.node_id); + const chat::contacts::PeerDirectoryItem* info = contacts.getPeerByNodeId(member.node_id); if (info == nullptr || !info->position.valid) { continue; diff --git a/modules/ui_shared/src/ui/presentation_sources/chat_presentation_source.cpp b/modules/ui_shared/src/ui/presentation_sources/chat_presentation_source.cpp index f480a6ca..f831ecab 100644 --- a/modules/ui_shared/src/ui/presentation_sources/chat_presentation_source.cpp +++ b/modules/ui_shared/src/ui/presentation_sources/chat_presentation_source.cpp @@ -67,7 +67,7 @@ std::string contactDisplayName(const ::chat::contacts::ContactService* contacts, if (contacts != nullptr) { - const auto* node = contacts->getNodeInfo(node_id); + const auto* node = contacts->getPeerByNodeId(node_id); if (node != nullptr) { if (!node->display_name.empty()) @@ -163,7 +163,7 @@ bool nodePositionToLocation(const ::chat::contacts::ContactService* contacts, return false; } - const auto* node = contacts->getNodeInfo(node_id); + const auto* node = contacts->getPeerByNodeId(node_id); if (node == nullptr || !node->position.valid) { return false; 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 965ec033..fb40a328 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 @@ -235,7 +235,7 @@ void format_reticulum_hash(const uint8_t* hash, char* out, size_t out_len) const chat::ReticulumPeerIdentity& conversation_reticulum_identity( const chat::ConversationId& conv, - const chat::contacts::NodeInfo* node) + const chat::contacts::PeerDirectoryItem* node) { if (chat::hasReticulumDestinationIdentity(conv.reticulum_identity)) { @@ -249,7 +249,7 @@ const chat::ReticulumPeerIdentity& conversation_reticulum_identity( } std::string node_display_name_for_info(const chat::ConversationId& conv, - const chat::contacts::NodeInfo* node) + const chat::contacts::PeerDirectoryItem* node) { if (node) { @@ -1416,7 +1416,7 @@ void UiController::openConversationInfoModal(const chat::ConversationId& conv) auto& contact_service = app::messagingFacade().getContactService(); uint32_t node_id = conv.peer; - const chat::contacts::NodeInfo* node = nullptr; + const chat::contacts::PeerDirectoryItem* node = nullptr; if (has_reticulum_destination(conv)) { uint32_t resolved_node_id = 0; @@ -1429,7 +1429,7 @@ void UiController::openConversationInfoModal(const chat::ConversationId& conv) } if (node_id != 0) { - node = contact_service.getNodeInfo(node_id); + node = contact_service.getPeerByNodeId(node_id); } const std::string display_name = node_display_name_for_info(conv, node); diff --git a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp index 4f6cf03e..da55a56e 100644 --- a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp @@ -190,13 +190,13 @@ static void format_reticulum_hash_text(const uint8_t* hash, char* out, size_t ou out[chat::kReticulumPeerHashSize * 2U] = '\0'; } -static bool is_reticulum_node(const chat::contacts::NodeInfo& node) +static bool is_reticulum_node(const chat::contacts::PeerDirectoryItem& node) { return node.protocol == chat::contacts::NodeProtocolType::Reticulum || chat::hasReticulumDestinationIdentity(node.reticulum_identity); } -static std::string node_display_name_for_contacts(const chat::contacts::NodeInfo& node) +static std::string node_display_name_for_contacts(const chat::contacts::PeerDirectoryItem& node) { return contacts::ui::layout::preferred_node_display_name(node); } @@ -281,9 +281,9 @@ static void on_lxmf_address_cancel(void* user_data); static void apply_filter_panel_visibility(); static void toggle_filter_panel_visibility(); static void contacts_handle_page_shortcut(lv_event_t* event); -static const chat::contacts::NodeInfo* get_selected_node(); -static const chat::contacts::NodeInfo* get_selected_reticulum_group(); -static const chat::contacts::NodeInfo* find_node_by_id(uint32_t node_id); +static const chat::contacts::PeerDirectoryItem* get_selected_node(); +static const chat::contacts::PeerDirectoryItem* get_selected_reticulum_group(); +static const chat::contacts::PeerDirectoryItem* find_node_by_id(uint32_t node_id); struct BroadcastTargetSpec; static bool get_selected_broadcast_target(BroadcastTargetSpec* out_spec, std::string* out_title); @@ -291,7 +291,7 @@ static void open_add_edit_modal(bool is_edit); static void open_reticulum_group_config_modal(); static void open_delete_confirm_modal(); static void open_node_info_screen_for_node(uint32_t node_id); -static void open_reticulum_node_info_screen(const chat::contacts::NodeInfo& node, +static void open_reticulum_node_info_screen(const chat::contacts::PeerDirectoryItem& node, lv_obj_t* parent); static void close_node_info_screen(); static void modal_close(lv_obj_t*& modal_obj); @@ -1105,7 +1105,7 @@ static bool contains_ci(const char* text, const char* query) return false; } -static bool node_matches_search(const chat::contacts::NodeInfo& node) +static bool node_matches_search(const chat::contacts::PeerDirectoryItem& node) { if (!search_active()) { @@ -1133,7 +1133,7 @@ static bool node_matches_search(const chat::contacts::NodeInfo& node) contains_ci(identity_hash, query); } -static void build_display_list(const std::vector& source) +static void build_display_list(const std::vector& source) { g_contacts_state.display_list.clear(); g_contacts_state.display_list.reserve(source.size()); @@ -1151,7 +1151,7 @@ static bool use_search_display_list_for_mode(ContactsMode mode) return search_active() && is_searchable_contacts_mode(mode); } -static const std::vector* raw_list_for_mode(ContactsMode mode) +static const std::vector* raw_list_for_mode(ContactsMode mode) { switch (mode) { @@ -1169,7 +1169,7 @@ static const std::vector* raw_list_for_mode(ContactsMo return nullptr; } -static const std::vector* selectable_list_for_mode( +static const std::vector* selectable_list_for_mode( ContactsMode mode) { if (use_search_display_list_for_mode(mode)) @@ -1491,7 +1491,7 @@ static void on_back_clicked(lv_event_t* /*e*/) contacts_focus_to_filter(); } -static const chat::contacts::NodeInfo* get_selected_node() +static const chat::contacts::PeerDirectoryItem* get_selected_node() { if (g_contacts_state.current_mode == ContactsMode::Broadcast || g_contacts_state.current_mode == ContactsMode::Team || @@ -1514,7 +1514,7 @@ static const chat::contacts::NodeInfo* get_selected_node() return &(*list)[g_contacts_state.selected_index]; } -static const chat::contacts::NodeInfo* get_selected_reticulum_group() +static const chat::contacts::PeerDirectoryItem* get_selected_reticulum_group() { if (g_contacts_state.current_mode != ContactsMode::Groups || g_contacts_state.selected_index < 0) @@ -1530,7 +1530,7 @@ static const chat::contacts::NodeInfo* get_selected_reticulum_group() return &(*list)[g_contacts_state.selected_index]; } -static const chat::contacts::NodeInfo* find_node_by_id(uint32_t node_id) +static const chat::contacts::PeerDirectoryItem* find_node_by_id(uint32_t node_id) { for (const auto& node : g_contacts_state.contacts_list) { @@ -1574,8 +1574,8 @@ static bool reticulum_identity_hash_present( } static void merge_reticulum_projection_for_details( - chat::contacts::NodeInfo& target, - const chat::contacts::NodeInfo& projection) + chat::contacts::PeerDirectoryItem& target, + const chat::contacts::PeerDirectoryItem& projection) { if (!is_reticulum_node(projection)) { @@ -1820,7 +1820,7 @@ static void on_lxmf_address_apply(const char* text, void* /*user_data*/) node_id = chat::reticulum::nodeIdFromDestinationHash(identity.destination_hash); } - const chat::contacts::NodeInfo* existing_node = find_node_by_id(node_id); + const chat::contacts::PeerDirectoryItem* existing_node = find_node_by_id(node_id); char short_name[10] = {}; char nickname[13] = {}; char generated_nickname[13] = {}; @@ -2367,7 +2367,7 @@ static void add_reticulum_detail_row(lv_obj_t* parent, lv_label_set_long_mode(value, LV_LABEL_LONG_WRAP); } -static const char* reticulum_node_status_text(const chat::contacts::NodeInfo& node, +static const char* reticulum_node_status_text(const chat::contacts::PeerDirectoryItem& node, char* out, size_t out_len) { @@ -2385,7 +2385,7 @@ static const char* reticulum_node_status_text(const chat::contacts::NodeInfo& no return out; } -static const char* reticulum_link_text(const chat::contacts::NodeInfo& node, +static const char* reticulum_link_text(const chat::contacts::PeerDirectoryItem& node, char* out, size_t out_len) { @@ -2422,7 +2422,7 @@ static void reticulum_node_info_back_requested(void*) close_node_info_screen(); } -static void open_reticulum_node_info_screen(const chat::contacts::NodeInfo& node, +static void open_reticulum_node_info_screen(const chat::contacts::PeerDirectoryItem& node, lv_obj_t* parent) { if (!parent) @@ -2554,10 +2554,10 @@ static void open_node_info_screen_for_node(uint32_t node_id) return; } - const chat::contacts::NodeInfo* node = find_node_by_id(node_id); + const chat::contacts::PeerDirectoryItem* node = find_node_by_id(node_id); if (!node && g_contacts_state.contact_service) { - node = g_contacts_state.contact_service->getNodeInfo(node_id); + node = g_contacts_state.contact_service->getPeerByNodeId(node_id); } if (!node) { @@ -2587,10 +2587,10 @@ static void open_node_info_screen_for_node(uint32_t node_id) static_cast(lv_obj_get_height(parent)), lv_obj_has_flag(parent, LV_OBJ_FLAG_HIDDEN) ? 1 : 0); - chat::contacts::NodeInfo detail_info = *node; + chat::contacts::PeerDirectoryItem detail_info = *node; if (g_contacts_state.contact_service) { - const auto* latest = g_contacts_state.contact_service->getNodeInfo(node->node_id); + const auto* latest = g_contacts_state.contact_service->getPeerByNodeId(node->node_id); if (latest) { detail_info = *latest; @@ -3911,7 +3911,7 @@ static const char* reticulum_call_failure_message(const chat::MeshActionResult& return "Call failed"; } -static bool selected_node_supports_reticulum_call(const chat::contacts::NodeInfo* node) +static bool selected_node_supports_reticulum_call(const chat::contacts::PeerDirectoryItem* node) { if (!node || chat_support::active_mesh_protocol() != chat::MeshProtocol::Reticulum) { @@ -3927,7 +3927,7 @@ static bool selected_node_supports_reticulum_call(const chat::contacts::NodeInfo chat::hasReticulumDestinationIdentity(node->reticulum_identity); } -static bool selected_node_supports_reticulum_ping(const chat::contacts::NodeInfo* node) +static bool selected_node_supports_reticulum_ping(const chat::contacts::PeerDirectoryItem* node) { if (!node || chat_support::active_mesh_protocol() != chat::MeshProtocol::Reticulum) { @@ -4125,7 +4125,7 @@ static void open_action_menu_modal() return; } - const chat::contacts::NodeInfo* node = get_selected_node(); + const chat::contacts::PeerDirectoryItem* node = get_selected_node(); const bool show_ignore = (node != nullptr) && (g_contacts_state.current_mode == ContactsMode::Contacts || g_contacts_state.current_mode == ContactsMode::Nearby); @@ -4445,10 +4445,10 @@ void refresh_ui() } g_contacts_state.list_items.clear(); // Choose list by mode (unchanged) - std::vector broadcast_list; - std::vector team_list; - std::vector discover_list; - const std::vector* current_list = nullptr; + std::vector broadcast_list; + std::vector team_list; + std::vector discover_list; + const std::vector* current_list = nullptr; if (g_contacts_state.current_mode == ContactsMode::Contacts) { current_list = &g_contacts_state.contacts_list; @@ -4469,7 +4469,7 @@ void refresh_ui() { contacts::ui::ContactsTeamSnapshot team_snapshot; (void)load_contacts_team_snapshot(team_snapshot); - chat::contacts::NodeInfo team_node{}; + chat::contacts::PeerDirectoryItem team_node{}; team_node.node_id = 0; team_node.last_seen = 0; team_node.snr = 0.0f; @@ -4485,7 +4485,7 @@ void refresh_ui() { for (size_t i = 0; i < (sizeof(kDiscoveryActionSpecs) / sizeof(kDiscoveryActionSpecs[0])); ++i) { - chat::contacts::NodeInfo item{}; + chat::contacts::PeerDirectoryItem item{}; item.node_id = static_cast(i + 1); item.display_name = ::ui::i18n::tr(kDiscoveryActionSpecs[i].label); item.protocol = chat::contacts::NodeProtocolType::MeshCore; @@ -4503,7 +4503,7 @@ void refresh_ui() { continue; } - chat::contacts::NodeInfo target{}; + chat::contacts::PeerDirectoryItem target{}; target.display_name = format_broadcast_target_label(spec); target.protocol = (spec.protocol == chat::MeshProtocol::MeshCore) ? chat::contacts::NodeProtocolType::MeshCore @@ -4578,7 +4578,7 @@ void refresh_ui() } if (show_reticulum_group_add_item) { - chat::contacts::NodeInfo add_node{}; + chat::contacts::PeerDirectoryItem add_node{}; add_node.protocol = chat::contacts::NodeProtocolType::Reticulum; std::snprintf(add_node.long_name, sizeof(add_node.long_name), "%s", "Add Group"); add_node.display_name = add_node.long_name; @@ -4688,7 +4688,7 @@ void refresh_ui() if (append_back_item) { - chat::contacts::NodeInfo back_node{}; + chat::contacts::PeerDirectoryItem back_node{}; back_node.display_name = ::ui::i18n::tr("Back"); lv_obj_t* back_item = contacts::ui::layout::create_list_item( g_contacts_state.sub_container, diff --git a/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp b/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp index f55b4215..07ad8ca3 100644 --- a/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp +++ b/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp @@ -66,7 +66,7 @@ static bool same_text(const std::string& lhs, const char* rhs) return rhs && lhs == rhs; } -std::string preferred_node_display_name(const chat::contacts::NodeInfo& node) +std::string preferred_node_display_name(const chat::contacts::PeerDirectoryItem& node) { const bool reticulum_node = node.protocol == chat::contacts::NodeProtocolType::Reticulum || @@ -275,7 +275,7 @@ void ensure_list_subcontainers() } lv_obj_t* create_list_item(lv_obj_t* parent, - const chat::contacts::NodeInfo& node, + const chat::contacts::PeerDirectoryItem& node, ContactsMode mode, const char* status_text) { diff --git a/modules/ui_shared/src/ui/screens/contacts/contacts_page_runtime.cpp b/modules/ui_shared/src/ui/screens/contacts/contacts_page_runtime.cpp index 44790faf..2fb560e4 100644 --- a/modules/ui_shared/src/ui/screens/contacts/contacts_page_runtime.cpp +++ b/modules/ui_shared/src/ui/screens/contacts/contacts_page_runtime.cpp @@ -107,7 +107,7 @@ static uint32_t hash_reticulum_identity(uint32_t hash, return hash; } -static uint32_t hash_node(uint32_t hash, const chat::contacts::NodeInfo& node) +static uint32_t hash_node(uint32_t hash, const chat::contacts::PeerDirectoryItem& node) { hash = hash_u32(hash, node.node_id); hash = hash_text(hash, node.short_name); @@ -125,7 +125,7 @@ static uint32_t hash_node(uint32_t hash, const chat::contacts::NodeInfo& node) } static uint32_t hash_node_list(uint32_t hash, - const std::vector& nodes) + const std::vector& nodes) { hash = hash_u32(hash, static_cast(nodes.size())); for (const auto& node : nodes) @@ -300,7 +300,7 @@ void refresh_reticulum_group_storage_state(const platform::ui::reticulum_groups: status.detail); } -bool node_matches_active_protocol(const chat::contacts::NodeInfo& node) +bool node_matches_active_protocol(const chat::contacts::PeerDirectoryItem& node) { const chat::MeshProtocol active_protocol = chat::infra::normalizeMeshProtocol( @@ -321,11 +321,11 @@ bool node_matches_active_protocol(const chat::contacts::NodeInfo& node) active_protocol) == active_protocol; } -void filter_to_active_protocol(std::vector& nodes) +void filter_to_active_protocol(std::vector& nodes) { nodes.erase(std::remove_if(nodes.begin(), nodes.end(), - [](const chat::contacts::NodeInfo& node) + [](const chat::contacts::PeerDirectoryItem& node) { return !node_matches_active_protocol(node); }), @@ -354,11 +354,11 @@ std::string reticulum_record_fallback_name( return "Anonymous Peer"; } -chat::contacts::NodeInfo node_from_lxmf_address( +chat::contacts::PeerDirectoryItem node_from_lxmf_address( const rtdir::LxmfAddressRecord& record, bool as_contact) { - chat::contacts::NodeInfo item{}; + chat::contacts::PeerDirectoryItem item{}; item.node_id = reticulum_node_id_from_destination_hash(record.destination_hash); item.last_seen = record.last_seen_s; item.snr = std::numeric_limits::quiet_NaN(); @@ -388,8 +388,8 @@ chat::contacts::NodeInfo node_from_lxmf_address( return item; } -bool same_reticulum_node(const chat::contacts::NodeInfo& lhs, - const chat::contacts::NodeInfo& rhs) +bool same_reticulum_node(const chat::contacts::PeerDirectoryItem& lhs, + const chat::contacts::PeerDirectoryItem& rhs) { if (chat::hasReticulumDestinationIdentity(lhs.reticulum_identity) && chat::hasReticulumDestinationIdentity(rhs.reticulum_identity)) @@ -400,8 +400,8 @@ bool same_reticulum_node(const chat::contacts::NodeInfo& lhs, return lhs.node_id != 0 && lhs.node_id == rhs.node_id; } -void upsert_reticulum_projection(std::vector& nodes, - const chat::contacts::NodeInfo& projection, +void upsert_reticulum_projection(std::vector& nodes, + const chat::contacts::PeerDirectoryItem& projection, bool force_contact) { for (auto& existing : nodes) @@ -429,7 +429,7 @@ void upsert_reticulum_projection(std::vector& nodes, return; } - chat::contacts::NodeInfo inserted = projection; + chat::contacts::PeerDirectoryItem inserted = projection; inserted.is_contact = inserted.is_contact || force_contact; nodes.push_back(inserted); } @@ -485,7 +485,7 @@ void merge_reticulum_directory_projection() continue; } - const chat::contacts::NodeInfo item = + const chat::contacts::PeerDirectoryItem item = node_from_lxmf_address( record, bucket == rtcontacts::ProjectionBucket::Contact); @@ -548,7 +548,7 @@ void refresh_reticulum_groups_data() continue; } - chat::contacts::NodeInfo item{}; + chat::contacts::PeerDirectoryItem item{}; item.node_id = 0; item.protocol = chat::contacts::NodeProtocolType::Reticulum; item.role = chat::contacts::NodeRoleType::Client; @@ -601,8 +601,8 @@ void refresh_contacts_data_impl_internal() std::stable_sort( g_contacts_state.nearby_list.begin(), g_contacts_state.nearby_list.end(), - [](const chat::contacts::NodeInfo& lhs, - const chat::contacts::NodeInfo& rhs) + [](const chat::contacts::PeerDirectoryItem& lhs, + const chat::contacts::PeerDirectoryItem& rhs) { return lhs.last_seen > rhs.last_seen; }); diff --git a/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp b/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp index b6ff5e78..525c02d5 100644 --- a/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp @@ -57,7 +57,7 @@ struct NodeInfoRuntimeState { bool has_node = false; bool map_ready = false; - chat::contacts::NodeInfo node{}; + chat::contacts::PeerDirectoryItem node{}; map_viewport::GeoPoint self_point{}; int zoom = map_viewport::kDefaultZoom; int pan_x = 0; @@ -246,7 +246,7 @@ void log_scene_widgets(const char* stage) viewport_status.has_visible_map_data ? 1 : 0); } -void log_node_summary(const char* stage, const chat::contacts::NodeInfo& node) +void log_node_summary(const char* stage, const chat::contacts::PeerDirectoryItem& node) { NODE_INFO_LOG( "%s node id=%08" PRIX32 " protocol=%d channel=%u last_seen=%" PRIu32 " rssi=%.1f snr=%.1f hops=%u next_hop=%u via_mqtt=%d ignored=%d display='%s' short='%s' long='%s'\n", @@ -994,7 +994,7 @@ map_viewport::Model build_map_model(const map_viewport::GeoPoint& node_point, return model; } -map_viewport::GeoPoint node_point_from_info(const chat::contacts::NodeInfo& node) +map_viewport::GeoPoint node_point_from_info(const chat::contacts::PeerDirectoryItem& node) { map_viewport::GeoPoint point{}; if (!node.position.valid) @@ -1017,7 +1017,7 @@ map_viewport::GeoPoint resolve_self_position() const chat::NodeId self_node_id = app::messagingFacade().getSelfNodeId(); if (self_node_id != 0) { - const auto* self_info = app::messagingFacade().getContactService().getNodeInfo(self_node_id); + const auto* self_info = app::messagingFacade().getContactService().getPeerByNodeId(self_node_id); if (self_info && self_info->position.valid) { point.valid = true; @@ -1217,7 +1217,7 @@ void format_age_short(uint32_t ts, char* out, size_t out_len) return chat::infra::nodeProtocolName(protocol); } -std::string preferred_node_title(const chat::contacts::NodeInfo& node) +std::string preferred_node_title(const chat::contacts::PeerDirectoryItem& node) { if (!node.display_name.empty()) { @@ -1234,7 +1234,7 @@ std::string preferred_node_title(const chat::contacts::NodeInfo& node) return ::ui::i18n::tr("NODE INFO"); } -void set_top_bar_title(const chat::contacts::NodeInfo& node) +void set_top_bar_title(const chat::contacts::PeerDirectoryItem& node) { if (!valid_obj(s_top_bar.container)) { @@ -1288,12 +1288,12 @@ void hide_unused_info_lines(std::size_t visible_count) } } -void build_protocol_line(const chat::contacts::NodeInfo& node, char* out, size_t out_len) +void build_protocol_line(const chat::contacts::PeerDirectoryItem& node, char* out, size_t out_len) { std::snprintf(out, out_len, "%s", protocol_name(node.protocol)); } -bool build_rssi_line(const chat::contacts::NodeInfo& node, char* out, size_t out_len) +bool build_rssi_line(const chat::contacts::PeerDirectoryItem& node, char* out, size_t out_len) { if (std::isnan(node.rssi)) { @@ -1303,7 +1303,7 @@ bool build_rssi_line(const chat::contacts::NodeInfo& node, char* out, size_t out return true; } -bool build_snr_line(const chat::contacts::NodeInfo& node, char* out, size_t out_len) +bool build_snr_line(const chat::contacts::PeerDirectoryItem& node, char* out, size_t out_len) { if (std::isnan(node.snr)) { @@ -1313,7 +1313,7 @@ bool build_snr_line(const chat::contacts::NodeInfo& node, char* out, size_t out_ return true; } -bool build_seen_line(const chat::contacts::NodeInfo& node, char* out, size_t out_len) +bool build_seen_line(const chat::contacts::PeerDirectoryItem& node, char* out, size_t out_len) { if (node.last_seen == 0) { @@ -2143,7 +2143,7 @@ const NodeInfoWidgets& widgets() return s_widgets; } -void set_node_info(const chat::contacts::NodeInfo& node) +void set_node_info(const chat::contacts::PeerDirectoryItem& node) { NODE_INFO_LOG("set_node_info begin\n"); log_node_summary("set_node_info", node); diff --git a/modules/ui_shared/tests/test_chat_presentation_source.cpp b/modules/ui_shared/tests/test_chat_presentation_source.cpp index dce72296..5f3bf856 100644 --- a/modules/ui_shared/tests/test_chat_presentation_source.cpp +++ b/modules/ui_shared/tests/test_chat_presentation_source.cpp @@ -1,6 +1,7 @@ #include "chat/delivery/chat_delivery_message_projection.h" #include "chat/delivery/chat_delivery_read_model.h" #include "chat/domain/chat_model.h" +#include "chat/infra/mesh_peer_directory_core.h" #include "chat/infra/store/ram_store.h" #include "chat/ports/i_mesh_adapter.h" #include "chat/usecase/chat_service.h" @@ -92,142 +93,29 @@ class FakeMeshAdapter final : public ::chat::IMeshAdapter std::deque<::chat::MeshIncomingText> incoming; }; -class FakeNodeStore final : public ::chat::contacts::INodeStore +class MemoryPeerDirectoryBlobStore final + : public ::chat::IMeshPeerDirectoryBlobStore { public: - void begin() override {} - - void applyUpdate(uint32_t node_id, - const ::chat::contacts::NodeUpdate& update) override + ::chat::MeshPeerDirectoryBlobLoadResult loadBlob( + std::vector& out) override { - auto* entry = findOrCreate(node_id); - if (update.short_name != nullptr) - { - std::snprintf(entry->short_name, - sizeof(entry->short_name), - "%s", - update.short_name); - } - if (update.long_name != nullptr) - { - std::snprintf(entry->long_name, - sizeof(entry->long_name), - "%s", - update.long_name); - } - if (update.has_last_seen) - { - entry->last_seen = update.last_seen; - } - if (update.has_protocol) - { - entry->protocol = update.protocol; - } - if (update.reticulum_identity.valid) - { - entry->reticulum_identity = update.reticulum_identity; - } - if (update.has_position) - { - updatePosition(node_id, update.position); - } + out = bytes_; + return bytes_.empty() + ? ::chat::MeshPeerDirectoryBlobLoadResult::Missing + : ::chat::MeshPeerDirectoryBlobLoadResult::Loaded; } - void upsert(uint32_t node_id, - const char* short_name, - const char* long_name, - uint32_t now_secs, - float snr = 0.0f, - float rssi = 0.0f, - uint8_t protocol = 0, - uint8_t role = ::chat::contacts::kNodeRoleUnknown, - uint8_t hops_away = 0xFF, - uint8_t hw_model = 0, - uint8_t channel = 0xFF) override + bool saveBlob(const uint8_t* data, std::size_t len) override { - (void)snr; - (void)rssi; - (void)protocol; - (void)role; - (void)hops_away; - (void)hw_model; - (void)channel; - auto* entry = findOrCreate(node_id); - std::snprintf(entry->short_name, - sizeof(entry->short_name), - "%s", - short_name ? short_name : ""); - std::snprintf(entry->long_name, - sizeof(entry->long_name), - "%s", - long_name ? long_name : ""); - entry->last_seen = now_secs; + bytes_.assign(data, data + len); + return true; } - void updateProtocol(uint32_t, uint8_t, uint32_t) override {} - - void updatePosition(uint32_t node_id, - const ::chat::contacts::NodePosition& position) override - { - auto* entry = findOrCreate(node_id); - entry->position_valid = position.valid; - entry->position_latitude_i = position.latitude_i; - entry->position_longitude_i = position.longitude_i; - entry->position_has_altitude = position.has_altitude; - entry->position_altitude = position.altitude; - entry->position_timestamp = position.timestamp; - } - - bool remove(uint32_t node_id) override - { - for (auto it = entries.begin(); it != entries.end(); ++it) - { - if (it->node_id == node_id) - { - entries.erase(it); - return true; - } - } - return false; - } - - const std::vector<::chat::contacts::NodeEntry>& getEntries() const override - { - return entries; - } - - void clear() override { entries.clear(); } - bool flush() override { return true; } + void clearBlob() override { bytes_.clear(); } private: - ::chat::contacts::NodeEntry* findOrCreate(uint32_t node_id) - { - for (auto& entry : entries) - { - if (entry.node_id == node_id) - { - return &entry; - } - } - ::chat::contacts::NodeEntry entry{}; - entry.node_id = node_id; - entries.push_back(entry); - return &entries.back(); - } - - std::vector<::chat::contacts::NodeEntry> entries; -}; - -class FakeContactStore final : public ::chat::contacts::IContactStore -{ - public: - void begin() override {} - std::string getNickname(uint32_t) const override { return {}; } - bool setNickname(uint32_t, const char*) override { return false; } - bool removeNickname(uint32_t) override { return false; } - bool hasNickname(const char*) const override { return false; } - std::vector getAllContactIds() const override { return {}; } - size_t getCount() const override { return 0; } + std::vector bytes_{}; }; class PagingStore final : public ::chat::IChatStore @@ -515,9 +403,10 @@ int main() ::chat::RamStore store; ::chat::ChatService service(model, mesh, store); ::chat::delivery::ChatDeliveryReadModel delivery_read_model; - FakeNodeStore node_store; - FakeContactStore contact_store; - ::chat::contacts::ContactService contacts(node_store, contact_store); + MemoryPeerDirectoryBlobStore peer_blob_store; + ::chat::MeshPeerDirectoryCore peer_directory(peer_blob_store); + ::chat::contacts::ContactService contacts(peer_directory); + contacts.begin(); setNodePosition(contacts, mesh.self_node_id, 312345678, 1219876543); setNodePosition(contacts, 1234, 313000000, 1220000000); contacts.updateNodeInfo(1234, @@ -732,10 +621,12 @@ int main() assert(snapshot.message_total_count == 25); assert(!snapshot.has_newer_messages); assert(snapshot.has_older_messages); - assert(std::strcmp(snapshot.messages[0].text.c_str(), "page-6") == 0); - assert(std::strcmp(snapshot.messages[19].text.c_str(), "page-25") == 0); + assert(std::strcmp(snapshot.messages[0].text.c_str(), "page-16") == 0); + assert(std::strcmp(snapshot.messages[snapshot.message_count - 1].text.c_str(), + "page-25") == 0); - paging_request.message_offset = ui::chat::ChatWorkspaceSnapshot::kMaxMessages; + paging_request.message_offset = + 2U * ui::chat::ChatWorkspaceSnapshot::kMaxMessages; assert(paging_source.buildChatWorkspaceSnapshot(paging_request, snapshot)); assert(snapshot.message_count == 5); assert(snapshot.message_total_count == 25); diff --git a/platform/esp/arduino_common/include/app/app_context.h b/platform/esp/arduino_common/include/app/app_context.h index c3248169..0081b771 100644 --- a/platform/esp/arduino_common/include/app/app_context.h +++ b/platform/esp/arduino_common/include/app/app_context.h @@ -38,8 +38,6 @@ class IMeshAdapter; class IMeshPeerDirectory; namespace contacts { -class INodeStore; -class IContactStore; class ContactService; } // namespace contacts namespace ui @@ -159,30 +157,10 @@ class AppContext final : public IAppBleFacade return board_; } - chat::contacts::INodeStore* getNodeStore() override - { - return node_store_; - } - - const chat::contacts::INodeStore* getNodeStore() const override - { - return node_store_; - } - bool getDeviceMacAddress(uint8_t out_mac[6]) const override; bool syncCurrentEpochSeconds(uint32_t epoch_seconds) override; void restartDevice() override; - chat::contacts::IContactStore* getContactStore() - { - return contact_store_; - } - - const chat::contacts::IContactStore* getContactStore() const - { - return contact_store_; - } - void saveConfig() override; void requestSaveConfig() override; @@ -274,8 +252,6 @@ class AppContext final : public IAppBleFacade std::unique_ptr chat_store_; std::unique_ptr mesh_peer_directory_; std::unique_ptr mesh_router_; - chat::contacts::INodeStore* node_store_ = nullptr; - chat::contacts::IContactStore* contact_store_ = nullptr; std::unique_ptr chat_service_; std::unique_ptr contact_service_; diff --git a/platform/esp/arduino_common/include/ble/meshcore_ble.h b/platform/esp/arduino_common/include/ble/meshcore_ble.h index eb0a607e..85a725ba 100644 --- a/platform/esp/arduino_common/include/ble/meshcore_ble.h +++ b/platform/esp/arduino_common/include/ble/meshcore_ble.h @@ -3,7 +3,6 @@ #include "app/app_facades.h" #include "ble/app_phone_facade.h" #include "ble/ble_manager.h" -#include "chat/ports/i_node_store.h" #include "chat/usecase/chat_service.h" #include "phone/meshcore/meshcore_phone_core.h" #include "phone/meshtastic/meshtastic_phone_core.h" diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/sd_protocol_peer_repository.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/sd_protocol_peer_repository.h index d94c1369..c1bb7d3c 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/sd_protocol_peer_repository.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/sd_protocol_peer_repository.h @@ -1,6 +1,5 @@ #pragma once -#include "chat/infra/mesh_peer_directory_core.h" #include "chat/ports/i_chat_store.h" #include "chat/ports/i_protocol_peer_repository.h" #include "platform/esp/arduino_common/chat/infra/store/fixed_slot_journal.h" @@ -12,7 +11,6 @@ #include #include -#include #include namespace chat @@ -43,17 +41,24 @@ class SdProtocolPeerRepository final : public IProtocolPeerRepository MeshPeerRecord* out_records, std::size_t max_records, std::size_t* out_count) override; + MeshPeerDirectoryStatus visit( + MeshProtocol protocol, + MeshPeerDirectoryView view, + IMeshPeerDirectoryVisitor& visitor) override; + MeshPeerDirectoryStatus setUserAlias( + const MeshPeerIdentity& identity, + const char* alias) override; MeshPeerDirectoryStatus setUserFlags( const MeshPeerIdentity& identity, const MeshPeerUserFlags& flags) override; + MeshPeerDirectoryStatus setKeyManuallyVerified( + const MeshPeerIdentity& identity, + bool verified) override; MeshPeerDirectoryStatus remove(const MeshPeerIdentity& identity) override; MeshPeerDirectoryStatus clearProtocol(MeshProtocol protocol) override; MeshPeerDirectoryCapacity capacityFor(MeshProtocol protocol) const override; MeshPeerDirectoryStatus flush() override; - contacts::INodeStore& nodeStoreView() override; - contacts::IContactStore& contactStoreView() override; - private: using PeerVector = std::vector< MeshPeerRecord, @@ -76,56 +81,6 @@ class SdProtocolPeerRepository final : public IProtocolPeerRepository uint32_t contact_delta_count = 0; }; - class NodeStoreView final : public contacts::INodeStore - { - public: - explicit NodeStoreView(SdProtocolPeerRepository& owner); - void setActiveProtocol(MeshProtocol protocol) override; - void begin() override; - void applyUpdate(uint32_t node_id, - const contacts::NodeUpdate& update) override; - void upsert(uint32_t node_id, - const char* short_name, - const char* long_name, - uint32_t now_secs, - float snr, - float rssi, - uint8_t protocol, - uint8_t role, - uint8_t hops_away, - uint8_t hw_model, - uint8_t channel) override; - void updateProtocol(uint32_t node_id, - uint8_t protocol, - uint32_t now_secs) override; - void updatePosition(uint32_t node_id, - const contacts::NodePosition& position) override; - bool remove(uint32_t node_id) override; - const std::vector& getEntries() const override; - void clear() override; - bool flush() override; - - private: - SdProtocolPeerRepository& owner_; - }; - - class ContactStoreView final : public contacts::IContactStore - { - public: - explicit ContactStoreView(SdProtocolPeerRepository& owner); - void setActiveProtocol(MeshProtocol protocol) override; - void begin() override; - std::string getNickname(uint32_t node_id) const override; - bool setNickname(uint32_t node_id, const char* nickname) override; - bool removeNickname(uint32_t node_id) override; - bool hasNickname(const char* nickname) const override; - std::vector getAllContactIds() const override; - size_t getCount() const override; - - private: - SdProtocolPeerRepository& owner_; - }; - bool ensureLayout(); bool ensureProtocolLayout(MeshProtocol protocol); bool loadProtocol(MeshProtocol protocol); @@ -163,21 +118,6 @@ class SdProtocolPeerRepository final : public IProtocolPeerRepository std::size_t ephemeralCount(MeshProtocol protocol) const; bool evictOldestEphemeral(MeshProtocol protocol); - void applyNodeUpdate(uint32_t node_id, - const contacts::NodeUpdate& update); - bool removeNode(uint32_t node_id); - void clearActiveNodes(); - const std::vector& activeNodeEntries() const; - void rebuildActiveNodeProjection() const; - bool toNodeEntry(const MeshPeerRecord& peer, - contacts::NodeEntry& out) const; - - std::string nicknameForNode(uint32_t node_id) const; - bool setNicknameForNode(uint32_t node_id, const char* nickname); - bool removeNicknameForNode(uint32_t node_id); - bool hasNicknameForActiveProtocol(const char* nickname) const; - std::vector activeContactIds() const; - size_t activeContactCount() const; bool persistContactFacts(const MeshPeerIdentity& identity, const MeshPeerUserFlags& flags, const char* alias, @@ -204,13 +144,9 @@ class SdProtocolPeerRepository final : public IProtocolPeerRepository std::size_t pending_peer_head_ = 0U; PsramByteVector slot_scratch_{}; PartitionState partitions_[3]{}; - mutable std::vector active_node_projection_{}; - mutable bool active_node_projection_dirty_ = true; MeshProtocol active_protocol_ = MeshProtocol::Meshtastic; bool begun_ = false; mutable SemaphoreHandle_t mutex_ = nullptr; - NodeStoreView node_store_view_; - ContactStoreView contact_store_view_; }; } // namespace chat diff --git a/platform/esp/arduino_common/src/app_context.cpp b/platform/esp/arduino_common/src/app_context.cpp index 76622d9b..eef76f00 100644 --- a/platform/esp/arduino_common/src/app_context.cpp +++ b/platform/esp/arduino_common/src/app_context.cpp @@ -237,8 +237,6 @@ void AppContext::initContactServices() return; } - node_store_ = &mesh_peer_directory_->nodeStoreView(); - contact_store_ = &mesh_peer_directory_->contactStoreView(); contact_service_ = std::move(contact_services.service); if (contact_service_) { @@ -726,9 +724,9 @@ chat::NodeId AppContext::getSelfNodeId() const void AppContext::clearNodeDb() { - if (node_store_) + if (mesh_peer_directory_) { - node_store_->clear(); + (void)mesh_peer_directory_->clearProtocol(config_.mesh_protocol); } if (contact_service_) { diff --git a/platform/esp/arduino_common/src/app_context_platform_bindings.cpp b/platform/esp/arduino_common/src/app_context_platform_bindings.cpp index 2ed44502..1e358c18 100644 --- a/platform/esp/arduino_common/src/app_context_platform_bindings.cpp +++ b/platform/esp/arduino_common/src/app_context_platform_bindings.cpp @@ -159,16 +159,11 @@ app::ContactServicesBundle create_contact_services( { app::ContactServicesBundle bundle; bundle.service = std::unique_ptr( - new chat::contacts::ContactService(repository.nodeStoreView(), - repository.contactStoreView())); + new chat::contacts::ContactService(repository)); if (bundle.service) { bundle.service->begin(); - Serial.printf("[ContactService] startup nodes=%u nicknames=%u\n", - static_cast( - repository.nodeStoreView().getEntries().size()), - static_cast( - repository.contactStoreView().getCount())); + Serial.printf("[ContactService] unified peer directory ready\n"); } return bundle; } diff --git a/platform/esp/arduino_common/src/app_runtime_support.cpp b/platform/esp/arduino_common/src/app_runtime_support.cpp index 8d108252..3cfd441c 100644 --- a/platform/esp/arduino_common/src/app_runtime_support.cpp +++ b/platform/esp/arduino_common/src/app_runtime_support.cpp @@ -344,8 +344,8 @@ void notifyNodeInfoUpdate(app::IAppFacade& app_context, const sys::NodeInfoUpdat return; } - const chat::contacts::NodeInfo* node_info = - app_context.getContactService().getNodeInfo(node_event.node_id); + const chat::contacts::PeerDirectoryItem* node_info = + app_context.getContactService().getPeerByNodeId(node_event.node_id); const bool is_contact = node_info && node_info->is_contact; if (alert_mode == kContactAlertsContacts && !is_contact) { diff --git a/platform/esp/arduino_common/src/ble/app_phone_facade.cpp b/platform/esp/arduino_common/src/ble/app_phone_facade.cpp index 7b992f66..b023acee 100644 --- a/platform/esp/arduino_common/src/ble/app_phone_facade.cpp +++ b/platform/esp/arduino_common/src/ble/app_phone_facade.cpp @@ -5,7 +5,6 @@ #include "chat/infra/meshcore/meshcore_ble_backend.h" #include "chat/infra/meshtastic/mt_radio_config.h" #include "chat/ports/i_mesh_adapter.h" -#include "chat/ports/i_node_store.h" #include "chat/usecase/chat_service.h" #include "chat/usecase/contact_service.h" #include "platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h" @@ -96,7 +95,8 @@ chat::meshtastic::MtAdapter* getMeshtasticBackend(app::IAppBleFacade& app) return static_cast(backend); } -void copyNodeView(const chat::contacts::NodeEntry& entry, phone::PhoneNodeView& out) +void copyNodeView(const chat::contacts::PeerDirectoryItem& entry, + phone::PhoneNodeView& out) { out = {}; out.node_id = entry.node_id; @@ -108,8 +108,8 @@ void copyNodeView(const chat::contacts::NodeEntry& entry, phone::PhoneNodeView& out.hops_away = entry.hops_away; out.channel = entry.channel; out.next_hop = entry.next_hop; - out.protocol = entry.protocol; - out.role = entry.role; + out.protocol = static_cast(entry.protocol); + out.role = static_cast(entry.role); out.hw_model = entry.hw_model; out.has_macaddr = entry.has_macaddr; std::memcpy(out.macaddr, entry.macaddr, sizeof(out.macaddr)); @@ -119,17 +119,7 @@ void copyNodeView(const chat::contacts::NodeEntry& entry, phone::PhoneNodeView& out.key_manually_verified = entry.key_manually_verified; out.has_device_metrics = entry.has_device_metrics; out.device_metrics = entry.device_metrics; - out.position.valid = entry.position_valid; - out.position.latitude_i = entry.position_latitude_i; - out.position.longitude_i = entry.position_longitude_i; - out.position.has_altitude = entry.position_has_altitude; - out.position.altitude = entry.position_altitude; - out.position.timestamp = entry.position_timestamp; - out.position.precision_bits = entry.position_precision_bits; - out.position.pdop = entry.position_pdop; - out.position.hdop = entry.position_hdop; - out.position.vdop = entry.position_vdop; - out.position.gps_accuracy_mm = entry.position_gps_accuracy_mm; + out.position = entry.position; } } // namespace @@ -247,46 +237,28 @@ bool AppPhoneFacade::pollIncomingPhoneData(chat::MeshIncomingData& out) std::size_t AppPhoneFacade::phoneNodeCount() const { - const auto* store = app_.getNodeStore(); - return store ? store->getEntries().size() : 0; + return app_.getContactService().getAllPeers().size(); } bool AppPhoneFacade::getPhoneNodeByIndex(std::size_t index, phone::PhoneNodeView& out) const { - const auto* store = app_.getNodeStore(); - if (!store) - { - return false; - } - const auto& entries = store->getEntries(); + const auto entries = app_.getContactService().getAllPeers(); if (index >= entries.size()) { return false; } copyNodeView(entries[index], out); - if (const auto* node = app_.getContactService().getNodeInfo(out.node_id)) - { - out.position = node->position; - } return true; } bool AppPhoneFacade::findPhoneNode(chat::NodeId node_id, phone::PhoneNodeView& out) const { - const auto* store = app_.getNodeStore(); - if (!store) - { - return false; - } - for (const auto& entry : store->getEntries()) + const auto entries = app_.getContactService().getAllPeers(); + for (const auto& entry : entries) { if (entry.node_id == node_id) { copyNodeView(entry, out); - if (const auto* node = app_.getContactService().getNodeInfo(out.node_id)) - { - out.position = node->position; - } return true; } } diff --git a/platform/esp/arduino_common/src/ble/meshcore_ble.cpp b/platform/esp/arduino_common/src/ble/meshcore_ble.cpp index cad01d4a..a683fb0e 100644 --- a/platform/esp/arduino_common/src/ble/meshcore_ble.cpp +++ b/platform/esp/arduino_common/src/ble/meshcore_ble.cpp @@ -2028,17 +2028,15 @@ bool MeshCoreBleService::buildContactFrame(const chat::meshcore::MeshCoreAdapter i += kMaxPathSize; char name[32] = {}; - const auto* store = ctx_.getNodeStore(); - if (store) + const auto* directory_peer = + ctx_.getContactService().getPeerByNodeId(peer.node_id); + if (directory_peer) { - for (const auto& entry : store->getEntries()) - { - if (entry.node_id == peer.node_id) - { - copyBounded(name, sizeof(name), entry.long_name[0] != '\0' ? entry.long_name : entry.short_name); - break; - } - } + copyBounded(name, + sizeof(name), + directory_peer->long_name[0] != '\0' + ? directory_peer->long_name + : directory_peer->short_name); } if (name[0] == '\0') { diff --git a/platform/esp/arduino_common/src/ble/meshcore_ble_owner_hooks.cpp b/platform/esp/arduino_common/src/ble/meshcore_ble_owner_hooks.cpp index 36bd2181..36a0a0cb 100644 --- a/platform/esp/arduino_common/src/ble/meshcore_ble_owner_hooks.cpp +++ b/platform/esp/arduino_common/src/ble/meshcore_ble_owner_hooks.cpp @@ -219,19 +219,15 @@ bool MeshCoreBleService::getMeshCoreContactByIndex(std::size_t index, const size_t copy_len = std::min(static_cast(out->out_path_len), sizeof(out->out_path)); std::memcpy(out->out_path, peer.out_path, copy_len); } - const auto* store = ctx_.getNodeStore(); - if (store) + const auto* directory_peer = + ctx_.getContactService().getPeerByNodeId(peer.node_id); + if (directory_peer) { - for (const auto& entry : store->getEntries()) - { - if (entry.node_id == peer.node_id) - { - copyBounded(out->name, - sizeof(out->name), - entry.long_name[0] != '\0' ? entry.long_name : entry.short_name); - break; - } - } + copyBounded(out->name, + sizeof(out->name), + directory_peer->long_name[0] != '\0' + ? directory_peer->long_name + : directory_peer->short_name); } if (out->name[0] == '\0') { 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 9855285c..1fc69627 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 @@ -7,7 +7,6 @@ #include "app/app_config.h" #include "app/app_facade_access.h" #include "chat/domain/contact_types.h" -#include "chat/ports/i_node_store.h" #include "chat/time_utils.h" #include "platform/esp/arduino_common/app_tasks.h" #include "platform/esp/arduino_common/gps/gps_service_api.h" diff --git a/platform/esp/arduino_common/src/chat/infra/store/sd_protocol_peer_repository.cpp b/platform/esp/arduino_common/src/chat/infra/store/sd_protocol_peer_repository.cpp index 6ff2f27c..90409fd9 100644 --- a/platform/esp/arduino_common/src/chat/infra/store/sd_protocol_peer_repository.cpp +++ b/platform/esp/arduino_common/src/chat/infra/store/sd_protocol_peer_repository.cpp @@ -40,18 +40,6 @@ bool textContains(const char* text, const char* query) return text && query && std::strstr(text, query) != nullptr; } -void copyNodeText(char* out, std::size_t out_len, const char* text) -{ - copyMeshPeerText(out, out_len, text ? text : ""); -} - -MeshPeerNodeFacts& mutableNodeFacts(MeshPeerRecord& record) -{ - return record.identity.protocol == MeshProtocol::MeshCore - ? record.meshcore.node - : record.meshtastic.node; -} - const MeshPeerNodeFacts* nodeFacts(const MeshPeerRecord& record) { if (record.identity.protocol == MeshProtocol::Meshtastic) @@ -69,9 +57,7 @@ const MeshPeerNodeFacts* nodeFacts(const MeshPeerRecord& record) SdProtocolPeerRepository::SdProtocolPeerRepository(IChatStore& chat_store) : chat_store_(chat_store), - mutex_(xSemaphoreCreateRecursiveMutex()), - node_store_view_(*this), - contact_store_view_(*this) + mutex_(xSemaphoreCreateRecursiveMutex()) { peers_.reserve(256U); contacts_.reserve(64U); @@ -138,7 +124,6 @@ MeshPeerDirectoryStatus SdProtocolPeerRepository::begin() } } begun_ = true; - active_node_projection_dirty_ = true; Serial.printf("[PeerStoreV2] ready=1 peers=%u contacts=%u root=%s\n", static_cast(peers_.size()), static_cast(contacts_.size()), @@ -746,6 +731,8 @@ MeshPeerDirectoryStatus SdProtocolPeerRepository::record( } MeshPeerRecord incoming = input; + incoming.user_alias[0] = '\0'; + incoming.flags = {}; incoming.identity.protocol = normalizeProtocol(incoming.identity.protocol); if (incoming.identity.protocol == MeshProtocol::Reticulum && incoming.identity.kind == MeshPeerIdentityKind::ReticulumDestination) @@ -816,7 +803,6 @@ MeshPeerDirectoryStatus SdProtocolPeerRepository::record( { overlayContactFactsForPeer(peers_[merge_index]); } - active_node_projection_dirty_ = true; if (!immediately_durable) { Serial.printf("[PeerStoreV2] peer queued protocol=%s pending=%u\n", @@ -992,6 +978,119 @@ MeshPeerDirectoryStatus SdProtocolPeerRepository::setUserFlags( return MeshPeerDirectoryStatus::success(); } +MeshPeerDirectoryStatus SdProtocolPeerRepository::visit( + MeshProtocol protocol, + MeshPeerDirectoryView view, + IMeshPeerDirectoryVisitor& visitor) +{ + ScopedRepositoryLock lock(mutex_); + if (!lock.locked()) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::StorageUnavailable); + } + const MeshProtocol normalized = normalizeProtocol(protocol); + for (const MeshPeerRecord& peer : peers_) + { + if (!meshPeerSameProtocol(peer.identity.protocol, normalized)) + { + continue; + } + const bool contact = meshPeerIsContact(peer); + const bool matches = + view == MeshPeerDirectoryView::All || + (view == MeshPeerDirectoryView::Contacts && contact) || + (view == MeshPeerDirectoryView::Nearby && !contact && + !peer.flags.ignored) || + (view == MeshPeerDirectoryView::Ignored && !contact && + peer.flags.ignored); + if (matches && !visitor.visit(peer)) + { + break; + } + } + return MeshPeerDirectoryStatus::success(); +} + +MeshPeerDirectoryStatus SdProtocolPeerRepository::setUserAlias( + const MeshPeerIdentity& identity, + const char* alias) +{ + if (!alias || std::strlen(alias) > kMeshPeerUserAliasMaxLen) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::InvalidArgument); + } + ScopedRepositoryLock lock(mutex_); + if (!lock.locked()) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::StorageUnavailable); + } + const std::size_t peer_index = findPeerIndex(identity); + if (peer_index >= peers_.size()) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::NotFound); + } + MeshPeerIdentity stable{}; + if (!stableContactIdentity(peers_[peer_index], stable)) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::Unsupported); + } + MeshPeerUserFlags flags = peers_[peer_index].flags; + flags.favorite = alias[0] != '\0'; + const bool remove_projection = alias[0] == '\0' && !flags.ignored && + !flags.trusted; + return persistContactFacts(stable, flags, alias, remove_projection) + ? MeshPeerDirectoryStatus::success() + : MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::IoError); +} + +MeshPeerDirectoryStatus SdProtocolPeerRepository::setKeyManuallyVerified( + const MeshPeerIdentity& identity, + bool verified) +{ + ScopedRepositoryLock lock(mutex_); + if (!lock.locked()) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::StorageUnavailable); + } + const std::size_t index = findPeerIndex(identity); + if (index >= peers_.size()) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::NotFound); + } + MeshPeerRecord next = peers_[index]; + if (next.identity.protocol == MeshProtocol::Meshtastic && + next.meshtastic.has_public_key) + { + next.meshtastic.key_manually_verified = verified; + } + else if (next.identity.protocol == MeshProtocol::MeshCore && + (next.meshcore.has_public_key || + next.identity.kind == MeshPeerIdentityKind::PublicKey)) + { + next.meshcore.public_key_verified = verified; + } + else + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::Unsupported); + } + if (!appendPeerDelta(storage_v2::PeerProjection{next, false})) + { + return MeshPeerDirectoryStatus::fail( + MeshPeerDirectoryStatusCode::StorageUnavailable); + } + peers_[index] = next; + return MeshPeerDirectoryStatus::success(); +} + MeshPeerDirectoryStatus SdProtocolPeerRepository::remove( const MeshPeerIdentity& identity) { @@ -1017,7 +1116,6 @@ MeshPeerDirectoryStatus SdProtocolPeerRepository::remove( tombstone.deleted = true; (void)queueOrAppendPeerDelta(tombstone); peers_.erase(peers_.begin() + static_cast(index)); - active_node_projection_dirty_ = true; return MeshPeerDirectoryStatus::success(); } @@ -1064,7 +1162,6 @@ MeshPeerDirectoryStatus SdProtocolPeerRepository::clearProtocol( { return MeshPeerDirectoryStatus::fail(MeshPeerDirectoryStatusCode::IoError); } - active_node_projection_dirty_ = true; return MeshPeerDirectoryStatus::success(); } @@ -1282,710 +1379,9 @@ bool SdProtocolPeerRepository::evictOldestEphemeral(MeshProtocol protocol) tombstone.deleted = true; (void)queueOrAppendPeerDelta(tombstone); peers_.erase(peers_.begin() + static_cast(candidate)); - active_node_projection_dirty_ = true; return true; } -contacts::INodeStore& SdProtocolPeerRepository::nodeStoreView() -{ - return node_store_view_; -} - -contacts::IContactStore& SdProtocolPeerRepository::contactStoreView() -{ - return contact_store_view_; -} - -SdProtocolPeerRepository::NodeStoreView::NodeStoreView( - SdProtocolPeerRepository& owner) - : owner_(owner) -{ -} - -void SdProtocolPeerRepository::NodeStoreView::setActiveProtocol( - MeshProtocol protocol) -{ - ScopedRepositoryLock lock(owner_.mutex_); - if (!lock.locked()) - { - return; - } - owner_.active_protocol_ = normalizeProtocol(protocol); - owner_.active_node_projection_dirty_ = true; -} - -void SdProtocolPeerRepository::NodeStoreView::begin() -{ - (void)owner_.begin(); -} - -void SdProtocolPeerRepository::NodeStoreView::applyUpdate( - uint32_t node_id, - const contacts::NodeUpdate& update) -{ - owner_.applyNodeUpdate(node_id, update); -} - -void SdProtocolPeerRepository::NodeStoreView::upsert( - uint32_t node_id, - const char* short_name, - const char* long_name, - uint32_t now_secs, - float snr, - float rssi, - uint8_t protocol, - uint8_t role, - uint8_t hops_away, - uint8_t hw_model, - uint8_t channel) -{ - contacts::NodeUpdate update{}; - update.short_name = short_name; - update.long_name = long_name; - update.has_last_seen = true; - update.last_seen = now_secs; - update.has_snr = !std::isnan(snr); - update.snr = snr; - update.has_rssi = !std::isnan(rssi); - update.rssi = rssi; - update.has_protocol = protocol != 0U; - update.protocol = protocol; - update.has_role = role != contacts::kNodeRoleUnknown; - update.role = role; - update.has_hops_away = hops_away != 0xFFU; - update.hops_away = hops_away; - update.has_hw_model = hw_model != 0U; - update.hw_model = hw_model; - update.has_channel = channel != 0xFFU; - update.channel = channel; - owner_.applyNodeUpdate(node_id, update); -} - -void SdProtocolPeerRepository::NodeStoreView::updateProtocol( - uint32_t node_id, - uint8_t protocol, - uint32_t now_secs) -{ - contacts::NodeUpdate update{}; - update.has_protocol = protocol != 0U; - update.protocol = protocol; - update.has_last_seen = true; - update.last_seen = now_secs; - owner_.applyNodeUpdate(node_id, update); -} - -void SdProtocolPeerRepository::NodeStoreView::updatePosition( - uint32_t node_id, - const contacts::NodePosition& position) -{ - contacts::NodeUpdate update{}; - update.has_position = true; - update.position = position; - owner_.applyNodeUpdate(node_id, update); -} - -bool SdProtocolPeerRepository::NodeStoreView::remove(uint32_t node_id) -{ - return owner_.removeNode(node_id); -} - -const std::vector& -SdProtocolPeerRepository::NodeStoreView::getEntries() const -{ - return owner_.activeNodeEntries(); -} - -void SdProtocolPeerRepository::NodeStoreView::clear() -{ - owner_.clearActiveNodes(); -} - -bool SdProtocolPeerRepository::NodeStoreView::flush() -{ - return owner_.flush().succeeded(); -} - -void SdProtocolPeerRepository::applyNodeUpdate( - uint32_t node_id, - const contacts::NodeUpdate& update) -{ - if (node_id == 0U) - { - return; - } - ScopedRepositoryLock lock(mutex_); - if (!lock.locked() || !begun_) - { - return; - } - MeshProtocol protocol = active_protocol_; - if (update.has_protocol) - { - const MeshProtocol requested = - static_cast(update.protocol); - if (requested == MeshProtocol::Meshtastic || - requested == MeshProtocol::MeshCore || - requested == MeshProtocol::Reticulum || - requested == MeshProtocol::RNode) - { - protocol = normalizeProtocol(requested); - } - } - - std::size_t existing_index = findPeerIndexByNodeId(protocol, node_id); - MeshPeerRecord incoming{}; - incoming.valid = true; - incoming.source = MeshPeerSource::RuntimeRx; - if (protocol == MeshProtocol::Reticulum && - hasReticulumDestinationIdentity(update.reticulum_identity)) - { - incoming.identity = makeMeshPeerReticulumIdentity( - update.reticulum_identity); - } - else if (existing_index < peers_.size()) - { - incoming.identity = peers_[existing_index].identity; - } - else - { - incoming.identity = makeMeshPeerNodeIdentity(protocol, node_id); - } - incoming.identity.protocol = protocol; - incoming.first_seen_s = update.has_last_seen ? update.last_seen : 0U; - incoming.last_seen_s = update.has_last_seen ? update.last_seen : 0U; - if (hasText(update.long_name)) - { - copyMeshPeerText(incoming.display_name, - sizeof(incoming.display_name), - update.long_name); - } - else if (hasText(update.short_name)) - { - copyMeshPeerText(incoming.display_name, - sizeof(incoming.display_name), - update.short_name); - } - if (update.has_snr) - { - incoming.observations.has_snr = true; - incoming.observations.snr = update.snr; - } - if (update.has_rssi) - { - incoming.observations.has_rssi = true; - incoming.observations.rssi = update.rssi; - } - if (update.has_device_metrics) - { - incoming.observations.has_device_metrics = true; - incoming.observations.device_metrics = update.device_metrics; - } - if (update.has_position) - { - incoming.observations.has_position = true; - incoming.observations.position = update.position; - } - - if (protocol == MeshProtocol::Reticulum) - { - incoming.reticulum.identity = incoming.identity.reticulum; - } - else - { - MeshPeerNodeFacts& facts = mutableNodeFacts(incoming); - copyNodeText(facts.short_name, - sizeof(facts.short_name), - update.short_name); - copyNodeText(facts.long_name, - sizeof(facts.long_name), - update.long_name); - if (update.has_role) - { - facts.role = update.role; - } - if (update.has_hw_model) - { - facts.hw_model = update.hw_model; - } - if (update.has_channel) - { - facts.channel = update.channel; - } - if (update.has_hops_away) - { - facts.hops_away = update.hops_away; - } - if (update.has_macaddr) - { - facts.has_macaddr = true; - std::memcpy(facts.macaddr, - update.macaddr, - sizeof(facts.macaddr)); - } - if (update.has_via_mqtt) - { - facts.via_mqtt = update.via_mqtt; - } - if (update.has_next_hop) - { - if (protocol == MeshProtocol::Meshtastic) - { - incoming.meshtastic.has_next_hop = true; - incoming.meshtastic.next_hop = update.next_hop; - } - else - { - incoming.meshcore.has_next_hop = true; - incoming.meshcore.next_hop = update.next_hop; - } - } - if (protocol == MeshProtocol::MeshCore) - { - incoming.meshcore.node_id_hint = node_id; - } - } - - MeshPeerRecord next = incoming; - bool identity_upgrade = false; - if (existing_index < peers_.size()) - { - next = mergeMeshPeerRecordFacts(peers_[existing_index], incoming); - if (!sameMeshPeerIdentity(peers_[existing_index].identity, - incoming.identity) && - incoming.identity.kind != MeshPeerIdentityKind::NodeId) - { - next.identity = incoming.identity; - identity_upgrade = true; - } - if (update.has_key_manually_verified) - { - if (protocol == MeshProtocol::Meshtastic && - next.meshtastic.has_public_key) - { - next.meshtastic.key_manually_verified = - update.key_manually_verified; - } - else if (protocol == MeshProtocol::MeshCore && - next.meshcore.has_public_key) - { - next.meshcore.public_key_verified = - update.key_manually_verified; - } - } - } - const storage_v2::PeerProjection projection{next, false}; - (void)queueOrAppendPeerDelta(projection); - if (identity_upgrade) - { - storage_v2::PeerProjection tombstone{}; - tombstone.record = peers_[existing_index]; - tombstone.deleted = true; - (void)queueOrAppendPeerDelta(tombstone); - } - if (existing_index < peers_.size()) - { - peers_[existing_index] = next; - } - else - { - peers_.push_back(next); - existing_index = peers_.size() - 1U; - } - - if (update.has_is_ignored && - (update.is_ignored || - findContactIndex(peers_[existing_index].identity) < contacts_.size())) - { - MeshPeerIdentity stable{}; - if (stableContactIdentity(peers_[existing_index], stable)) - { - MeshPeerUserFlags flags = peers_[existing_index].flags; - flags.ignored = update.is_ignored; - const bool remove_projection = !flags.favorite && !flags.ignored && - !flags.trusted && - peers_[existing_index] - .user_alias[0] == '\0'; - (void)persistContactFacts( - stable, - flags, - peers_[existing_index].user_alias, - remove_projection); - } - } - overlayContactFactsForPeer(peers_[existing_index]); - active_node_projection_dirty_ = true; -} - -bool SdProtocolPeerRepository::removeNode(uint32_t node_id) -{ - ScopedRepositoryLock lock(mutex_); - if (!lock.locked() || node_id == 0U) - { - return false; - } - const std::size_t index = findPeerIndexByNodeId(active_protocol_, node_id); - if (index >= peers_.size() || peerIsProtected(peers_[index])) - { - return false; - } - storage_v2::PeerProjection tombstone{}; - tombstone.record = peers_[index]; - tombstone.deleted = true; - (void)queueOrAppendPeerDelta(tombstone); - peers_.erase(peers_.begin() + static_cast(index)); - active_node_projection_dirty_ = true; - return true; -} - -void SdProtocolPeerRepository::clearActiveNodes() -{ - (void)clearProtocol(active_protocol_); -} - -const std::vector& -SdProtocolPeerRepository::activeNodeEntries() const -{ - ScopedRepositoryLock lock(mutex_); - if (lock.locked() && active_node_projection_dirty_) - { - rebuildActiveNodeProjection(); - } - return active_node_projection_; -} - -void SdProtocolPeerRepository::rebuildActiveNodeProjection() const -{ - active_node_projection_.clear(); - active_node_projection_.reserve( - std::count_if(peers_.begin(), - peers_.end(), - [this](const MeshPeerRecord& peer) - { - return meshPeerSameProtocol( - peer.identity.protocol, - active_protocol_); - })); - for (const MeshPeerRecord& peer : peers_) - { - contacts::NodeEntry entry{}; - if (meshPeerSameProtocol(peer.identity.protocol, active_protocol_) && - toNodeEntry(peer, entry)) - { - active_node_projection_.push_back(entry); - } - } - active_node_projection_dirty_ = false; -} - -bool SdProtocolPeerRepository::toNodeEntry( - const MeshPeerRecord& peer, - contacts::NodeEntry& out) const -{ - const NodeId node_id = projectedNodeId(peer); - if (node_id == 0U) - { - return false; - } - out = contacts::NodeEntry{}; - out.node_id = node_id; - out.last_seen = peer.last_seen_s; - out.protocol = static_cast(normalizeProtocol( - peer.identity.protocol)); - const MeshPeerNodeFacts* facts = nodeFacts(peer); - if (facts) - { - copyNodeText(out.short_name, - sizeof(out.short_name), - facts->short_name); - copyNodeText(out.long_name, - sizeof(out.long_name), - facts->long_name); - out.role = facts->role; - out.hw_model = facts->hw_model; - out.channel = facts->channel; - out.hops_away = facts->hops_away; - out.has_macaddr = facts->has_macaddr; - std::memcpy(out.macaddr, facts->macaddr, sizeof(out.macaddr)); - out.via_mqtt = facts->via_mqtt; - } - else - { - copyNodeText(out.long_name, - sizeof(out.long_name), - peer.display_name); - out.role = static_cast(contacts::NodeRoleType::Client); - out.hops_away = 0U; - } - if (out.long_name[0] == '\0') - { - copyNodeText(out.long_name, - sizeof(out.long_name), - peer.display_name); - } - out.snr = peer.observations.has_snr ? peer.observations.snr : 0.0F; - out.rssi = peer.observations.has_rssi ? peer.observations.rssi : 0.0F; - out.is_ignored = peer.flags.ignored; - if (peer.identity.protocol == MeshProtocol::Meshtastic) - { - out.next_hop = peer.meshtastic.has_next_hop - ? peer.meshtastic.next_hop - : 0U; - out.has_public_key = peer.meshtastic.has_public_key; - out.key_manually_verified = - peer.meshtastic.key_manually_verified; - } - else if (peer.identity.protocol == MeshProtocol::MeshCore) - { - out.next_hop = peer.meshcore.has_next_hop - ? peer.meshcore.next_hop - : 0U; - out.has_public_key = peer.meshcore.has_public_key || - peer.identity.kind == - MeshPeerIdentityKind::PublicKey; - out.key_manually_verified = peer.meshcore.public_key_verified; - } - else if (peer.identity.protocol == MeshProtocol::Reticulum) - { - out.has_public_key = peer.reticulum.has_public_keys; - out.reticulum_identity = peer.identity.reticulum; - } - if (peer.observations.has_device_metrics) - { - out.has_device_metrics = true; - out.device_metrics = peer.observations.device_metrics; - } - if (peer.observations.has_position) - { - const contacts::NodePosition& position = - peer.observations.position; - out.position_valid = position.valid; - out.position_latitude_i = position.latitude_i; - out.position_longitude_i = position.longitude_i; - out.position_has_altitude = position.has_altitude; - out.position_altitude = position.altitude; - out.position_timestamp = position.timestamp; - out.position_precision_bits = position.precision_bits; - out.position_pdop = position.pdop; - out.position_hdop = position.hdop; - out.position_vdop = position.vdop; - out.position_gps_accuracy_mm = position.gps_accuracy_mm; - } - return true; -} - -SdProtocolPeerRepository::ContactStoreView::ContactStoreView( - SdProtocolPeerRepository& owner) - : owner_(owner) -{ -} - -void SdProtocolPeerRepository::ContactStoreView::setActiveProtocol( - MeshProtocol protocol) -{ - ScopedRepositoryLock lock(owner_.mutex_); - if (!lock.locked()) - { - return; - } - owner_.active_protocol_ = normalizeProtocol(protocol); - owner_.active_node_projection_dirty_ = true; -} - -void SdProtocolPeerRepository::ContactStoreView::begin() -{ - (void)owner_.begin(); -} - -std::string SdProtocolPeerRepository::ContactStoreView::getNickname( - uint32_t node_id) const -{ - return owner_.nicknameForNode(node_id); -} - -bool SdProtocolPeerRepository::ContactStoreView::setNickname( - uint32_t node_id, - const char* nickname) -{ - return owner_.setNicknameForNode(node_id, nickname); -} - -bool SdProtocolPeerRepository::ContactStoreView::removeNickname( - uint32_t node_id) -{ - return owner_.removeNicknameForNode(node_id); -} - -bool SdProtocolPeerRepository::ContactStoreView::hasNickname( - const char* nickname) const -{ - return owner_.hasNicknameForActiveProtocol(nickname); -} - -std::vector -SdProtocolPeerRepository::ContactStoreView::getAllContactIds() const -{ - return owner_.activeContactIds(); -} - -size_t SdProtocolPeerRepository::ContactStoreView::getCount() const -{ - return owner_.activeContactCount(); -} - -std::string SdProtocolPeerRepository::nicknameForNode(uint32_t node_id) const -{ - ScopedRepositoryLock lock(mutex_); - if (!lock.locked()) - { - return {}; - } - const std::size_t peer_index = findPeerIndexByNodeId(active_protocol_, - node_id); - if (peer_index >= peers_.size()) - { - return {}; - } - MeshPeerIdentity stable{}; - if (!stableContactIdentity(peers_[peer_index], stable)) - { - return {}; - } - const std::size_t contact_index = findContactIndex(stable); - return contact_index < contacts_.size() - ? std::string(contacts_[contact_index].alias) - : std::string(); -} - -bool SdProtocolPeerRepository::setNicknameForNode(uint32_t node_id, - const char* nickname) -{ - if (!nickname || nickname[0] == '\0' || - std::strlen(nickname) > kMeshPeerUserAliasMaxLen) - { - return false; - } - ScopedRepositoryLock lock(mutex_); - if (!lock.locked()) - { - return false; - } - const std::size_t peer_index = findPeerIndexByNodeId(active_protocol_, - node_id); - if (peer_index >= peers_.size()) - { - return false; - } - MeshPeerIdentity stable{}; - if (!stableContactIdentity(peers_[peer_index], stable)) - { - return false; - } - MeshPeerUserFlags flags = peers_[peer_index].flags; - flags.favorite = true; - return persistContactFacts(stable, flags, nickname, false); -} - -bool SdProtocolPeerRepository::removeNicknameForNode(uint32_t node_id) -{ - ScopedRepositoryLock lock(mutex_); - if (!lock.locked()) - { - return false; - } - const std::size_t peer_index = findPeerIndexByNodeId(active_protocol_, - node_id); - if (peer_index >= peers_.size()) - { - return false; - } - MeshPeerIdentity stable{}; - if (!stableContactIdentity(peers_[peer_index], stable)) - { - return false; - } - const std::size_t contact_index = findContactIndex(stable); - if (contact_index >= contacts_.size()) - { - return false; - } - MeshPeerUserFlags flags = contacts_[contact_index].flags; - flags.favorite = false; - const bool deleted = !flags.ignored && !flags.trusted; - return persistContactFacts(stable, flags, "", deleted); -} - -bool SdProtocolPeerRepository::hasNicknameForActiveProtocol( - const char* nickname) const -{ - if (!nickname || nickname[0] == '\0') - { - return false; - } - ScopedRepositoryLock lock(mutex_); - if (!lock.locked()) - { - return false; - } - for (const storage_v2::ContactProjection& contact : contacts_) - { - if (meshPeerSameProtocol(contact.identity.protocol, - active_protocol_) && - std::strcmp(contact.alias, nickname) == 0) - { - return true; - } - } - return false; -} - -std::vector SdProtocolPeerRepository::activeContactIds() const -{ - ScopedRepositoryLock lock(mutex_); - std::vector ids; - if (!lock.locked()) - { - return ids; - } - ids.reserve(contacts_.size()); - for (const storage_v2::ContactProjection& contact : contacts_) - { - if (!meshPeerSameProtocol(contact.identity.protocol, - active_protocol_) || - (!contact.flags.favorite && contact.alias[0] == '\0')) - { - continue; - } - const std::size_t peer_index = findPeerIndex(contact.identity); - if (peer_index < peers_.size()) - { - const NodeId node_id = projectedNodeId(peers_[peer_index]); - if (node_id != 0U) - { - ids.push_back(node_id); - } - } - } - return ids; -} - -size_t SdProtocolPeerRepository::activeContactCount() const -{ - ScopedRepositoryLock lock(mutex_); - if (!lock.locked()) - { - return 0U; - } - return static_cast(std::count_if( - contacts_.begin(), - contacts_.end(), - [this](const storage_v2::ContactProjection& contact) - { - return meshPeerSameProtocol(contact.identity.protocol, - active_protocol_) && - (contact.flags.favorite || contact.alias[0] != '\0'); - })); -} - bool SdProtocolPeerRepository::persistContactFacts( const MeshPeerIdentity& identity, const MeshPeerUserFlags& flags, @@ -2021,7 +1417,6 @@ bool SdProtocolPeerRepository::persistContactFacts( { overlayContactFactsForPeer(peers_[peer_index]); } - active_node_projection_dirty_ = true; return true; } diff --git a/platform/linux/common/src/app/linux_app_services.cpp b/platform/linux/common/src/app/linux_app_services.cpp index 90afc74e..1abbbfbd 100644 --- a/platform/linux/common/src/app/linux_app_services.cpp +++ b/platform/linux/common/src/app/linux_app_services.cpp @@ -24,17 +24,14 @@ extern char** environ; #endif #include "chat/domain/chat_model.h" -#include "chat/infra/contact_store_core.h" -#include "chat/infra/node_store_core.h" #include "chat/linux_noop_mesh_adapter.h" #include "chat/linux_raw_lora_mesh_adapter.h" #include "chat/linux_sqlite_chat_store.h" -#include "chat/ports/i_contact_blob_store.h" -#include "chat/ports/i_node_blob_store.h" #include "chat/usecase/chat_service.h" #include "chat/usecase/contact_service.h" #include "platform/ui/device_runtime.h" #include "platform/ui/gps_runtime.h" +#include "platform/ui/reticulum_directory_runtime.h" #include "platform/ui/reticulum_group_config_runtime.h" #include "platform/ui/settings_store.h" #include "platform/ui/team_ui_store_runtime.h" @@ -65,11 +62,6 @@ constexpr const char* kConfigBlobKey = "app_config_v1"; constexpr uint32_t kConfigBlobMagic = 0x544D4346U; // TMCF constexpr uint32_t kConfigBlobVersion = 2U; -constexpr const char* kNodeStoreNamespace = "linux_contact_nodes"; -constexpr const char* kNodeStoreKey = "nodes_v1"; -constexpr const char* kContactStoreNamespace = "linux_contact_names"; -constexpr const char* kContactStoreKey = "contacts_v1"; - constexpr ::chat::NodeId kDemoAlphaNodeId = 0x435A1001U; constexpr ::chat::NodeId kDemoBravoNodeId = 0x435A1002U; constexpr ::chat::NodeId kDemoScoutNodeId = 0x435A1003U; @@ -353,44 +345,6 @@ void sync_reticulum_group_config(::app::AppConfig& config) status.detail); } -class LinuxNodeBlobStore final : public ::chat::contacts::INodeBlobStore -{ - public: - bool loadBlob(std::vector& out) override - { - return ::platform::ui::settings_store::get_blob(kNodeStoreNamespace, kNodeStoreKey, out); - } - - bool saveBlob(const uint8_t* data, size_t len) override - { - return ::platform::ui::settings_store::put_blob(kNodeStoreNamespace, kNodeStoreKey, data, len); - } - - void clearBlob() override - { - ::platform::ui::settings_store::clear_namespace(kNodeStoreNamespace); - } -}; - -class LinuxContactBlobStore final : public ::chat::IContactBlobStore -{ - public: - bool loadBlob(std::vector& out) override - { - return ::platform::ui::settings_store::get_blob(kContactStoreNamespace, kContactStoreKey, out); - } - - bool saveBlob(const uint8_t* data, size_t len) override - { - return ::platform::ui::settings_store::put_blob(kContactStoreNamespace, kContactStoreKey, data, len); - } - - void clear() - { - ::platform::ui::settings_store::clear_namespace(kContactStoreNamespace); - } -}; - class LinuxLoopbackMeshAdapter final : public ::chat::IMeshAdapter { public: @@ -1569,11 +1523,9 @@ struct LinuxAppServices::Implementation : options(options_in), demo_world_enabled( ::platform::linux_runtime::demo_world_enabled(options.runtime_mode)), - node_blob_store(), - contact_blob_store(), - node_store(node_blob_store), - contact_store(contact_blob_store), - contact_service(node_store, contact_store), + mesh_peer_directory( + ::platform::ui::reticulum_directory::mesh_peer_directory()), + contact_service(mesh_peer_directory), chat_model(), chat_store(), raw_lora_enabled(raw_lora_enabled_for_mode(options.runtime_mode)), @@ -1616,12 +1568,9 @@ struct LinuxAppServices::Implementation noop_mesh_adapter.setSelfNodeId(self_node_id); raw_lora_mesh_adapter.setSelfNodeId(self_node_id); loopback_mesh_adapter.setSelfNodeId(self_node_id); - node_store.setProtectedNodeChecker( - [self_node_id](uint32_t node_id) - { - return node_id == self_node_id; - }); contact_service.begin(); + ::platform::ui::reticulum_directory::bind_mesh_peer_directory( + &mesh_peer_directory); if (demo_world_enabled) { seedDemoWorld(self_node_id); @@ -1631,8 +1580,10 @@ struct LinuxAppServices::Implementation void clearContactAndNodeData() { - contact_blob_store.clear(); - node_store.clear(); + (void)mesh_peer_directory.clearProtocol(::chat::MeshProtocol::Meshtastic); + (void)mesh_peer_directory.clearProtocol(::chat::MeshProtocol::MeshCore); + (void)mesh_peer_directory.clearProtocol(::chat::MeshProtocol::RNode); + (void)mesh_peer_directory.clearProtocol(::chat::MeshProtocol::Reticulum); contact_service.begin(); demo_seeded = false; } @@ -1749,10 +1700,7 @@ struct LinuxAppServices::Implementation LinuxAppServicesOptions options{}; bool demo_world_enabled = false; - LinuxNodeBlobStore node_blob_store; - LinuxContactBlobStore contact_blob_store; - ::chat::contacts::NodeStoreCore node_store; - ::chat::contacts::ContactStoreCore contact_store; + ::chat::IMeshPeerDirectory& mesh_peer_directory; ::chat::contacts::ContactService contact_service; ::chat::ChatModel chat_model; LinuxSqliteChatStore chat_store; diff --git a/platform/linux/common/src/platform/ui/reticulum_directory_runtime.cpp b/platform/linux/common/src/platform/ui/reticulum_directory_runtime.cpp index eef0022e..918965fd 100644 --- a/platform/linux/common/src/platform/ui/reticulum_directory_runtime.cpp +++ b/platform/linux/common/src/platform/ui/reticulum_directory_runtime.cpp @@ -975,6 +975,11 @@ void bind_mesh_peer_directory(chat::IMeshPeerDirectory* directory) s_bound_mesh_peer_directory = directory; } +chat::IMeshPeerDirectory& mesh_peer_directory() +{ + return local_mesh_peer_directory(); +} + Status record_announce(const AnnounceRecord& record) { Status out{}; diff --git a/platform/linux/uconsole/src/uconsole_chat_workspace_model.cpp b/platform/linux/uconsole/src/uconsole_chat_workspace_model.cpp index dee5ccf2..7457efb4 100644 --- a/platform/linux/uconsole/src/uconsole_chat_workspace_model.cpp +++ b/platform/linux/uconsole/src/uconsole_chat_workspace_model.cpp @@ -71,7 +71,7 @@ namespace { return {}; } - if (const auto* node = contacts.getNodeInfo(node_id)) + if (const auto* node = contacts.getPeerByNodeId(node_id)) { if (!node->display_name.empty()) { @@ -217,7 +217,7 @@ namespace } [[nodiscard]] ::chat::ChannelId channelForNode( - const ::chat::contacts::NodeInfo& node) noexcept + const ::chat::contacts::PeerDirectoryItem& node) noexcept { return node.channel == 1U ? ::chat::ChannelId::SECONDARY : ::chat::ChannelId::PRIMARY; @@ -357,7 +357,7 @@ void appendDetailSection(ChatNodeDetailSnapshot& out, } [[nodiscard]] ChatNodeInfoItem makeNodeInfoItem( - const ::chat::contacts::NodeInfo& node) + const ::chat::contacts::PeerDirectoryItem& node) { ChatNodeInfoItem item{}; item.node_id = node.node_id; @@ -487,7 +487,7 @@ void appendDetailSection(ChatNodeDetailSnapshot& out, { meta += " / "; meta += formatNodeLabel(conversation.id.peer); - if (const auto* node = contacts.getNodeInfo(conversation.id.peer)) + if (const auto* node = contacts.getPeerByNodeId(conversation.id.peer)) { meta += node->via_mqtt ? " / MQTT" : " / LoRa"; } @@ -504,7 +504,7 @@ void appendDetailSection(ChatNodeDetailSnapshot& out, return meta; } -[[nodiscard]] const ::chat::contacts::NodeInfo* nodeForConversation( +[[nodiscard]] const ::chat::contacts::PeerDirectoryItem* nodeForConversation( const ::chat::ConversationId& id, const ::chat::contacts::ContactService& contacts) { @@ -512,12 +512,12 @@ void appendDetailSection(ChatNodeDetailSnapshot& out, { return nullptr; } - return contacts.getNodeInfo(id.peer); + return contacts.getPeerByNodeId(id.peer); } [[nodiscard]] std::string groupForConversation( const ::chat::ConversationMeta& conversation, - const ::chat::contacts::NodeInfo* node) + const ::chat::contacts::PeerDirectoryItem* node) { if (conversation.id.peer == 0) { @@ -537,7 +537,7 @@ void appendDetailSection(ChatNodeDetailSnapshot& out, [[nodiscard]] std::string factsForConversation( const ::chat::ConversationMeta& conversation, - const ::chat::contacts::NodeInfo* node, + const ::chat::contacts::PeerDirectoryItem* node, bool has_local_gps, double local_lat, double local_lon) @@ -813,7 +813,7 @@ void sortConversations(std::vector<::chat::ConversationMeta>& conversations, void appendNodeConversationIfMissing( std::vector<::chat::ConversationMeta>& conversations, - const ::chat::contacts::NodeInfo& node, + const ::chat::contacts::PeerDirectoryItem& node, ::chat::MeshProtocol fallback_protocol) { if (node.node_id == 0 || @@ -962,7 +962,7 @@ ChatWorkspaceSnapshot UConsoleChatWorkspaceModel::snapshot( }); if (!already_listed && out.nodes.size() < 5U) { - if (const auto* node = contacts.getNodeInfo(sender)) + if (const auto* node = contacts.getPeerByNodeId(sender)) { out.nodes.push_back(makeNodeInfoItem(*node)); } @@ -977,7 +977,7 @@ ChatWorkspaceSnapshot UConsoleChatWorkspaceModel::snapshot( return node.node_id == active_conversation_.peer; })) { - if (const auto* node = contacts.getNodeInfo(active_conversation_.peer)) + if (const auto* node = contacts.getPeerByNodeId(active_conversation_.peer)) { out.nodes.insert(out.nodes.begin(), makeNodeInfoItem(*node)); } @@ -999,7 +999,7 @@ ChatNodeDetailSnapshot UConsoleChatWorkspaceModel::nodeDetails( return out; } - const auto* node = services_.contacts().getNodeInfo(node_id); + const auto* node = services_.contacts().getPeerByNodeId(node_id); if (node == nullptr) { out.subtitle = "No NodeInfo record is stored locally yet."; @@ -1019,7 +1019,7 @@ ChatNodeDetailSnapshot UConsoleChatWorkspaceModel::nodeDetails( out.lon = static_cast(node->position.longitude_i) / 10000000.0; if (const auto* self_info = - services_.contacts().getNodeInfo(services_.selfNodeId()); + services_.contacts().getPeerByNodeId(services_.selfNodeId()); self_info != nullptr && self_info->position.valid) { out.has_self_position = true; @@ -1562,7 +1562,7 @@ bool UConsoleChatWorkspaceModel::toggleNodeIgnored(::chat::NodeId node_id) action_status_ = "Node is unavailable."; return false; } - const auto* node = services_.contacts().getNodeInfo(node_id); + const auto* node = services_.contacts().getPeerByNodeId(node_id); if (node == nullptr) { action_status_ = "Node record is not stored yet."; diff --git a/platform/linux/uconsole/src/uconsole_dashboard_model.cpp b/platform/linux/uconsole/src/uconsole_dashboard_model.cpp index 586e583c..7016fa78 100644 --- a/platform/linux/uconsole/src/uconsole_dashboard_model.cpp +++ b/platform/linux/uconsole/src/uconsole_dashboard_model.cpp @@ -189,7 +189,7 @@ void pushOverviewTimeline(std::vector& out, } [[nodiscard]] ContactPreview makeContactPreview( - const ::chat::contacts::NodeInfo& node) + const ::chat::contacts::PeerDirectoryItem& node) { ContactPreview preview{}; preview.name = node.display_name.empty() ? node.short_name : node.display_name; @@ -216,7 +216,7 @@ void pushOverviewTimeline(std::vector& out, ? "Secondary broadcast" : "Primary broadcast"; } - if (const auto* node = contacts.getNodeInfo(id.peer)) + if (const auto* node = contacts.getPeerByNodeId(id.peer)) { if (!node->display_name.empty()) { @@ -250,7 +250,7 @@ void pushOverviewTimeline(std::vector& out, formatLastSeen(conversation.last_timestamp); if (conversation.id.peer != 0) { - if (const auto* node = contacts.getNodeInfo(conversation.id.peer)) + if (const auto* node = contacts.getPeerByNodeId(conversation.id.peer)) { preview.detail = std::string("hops ") + @@ -461,7 +461,7 @@ void appendChatTimeline(std::vector& out, } void appendNodeTimeline(std::vector& out, - const std::vector<::chat::contacts::NodeInfo>& nodes) + const std::vector<::chat::contacts::PeerDirectoryItem>& nodes) { for (const auto& node : nodes) { @@ -762,7 +762,7 @@ UConsoleDashboardSnapshot UConsoleDashboardModel::snapshot() const out.nearby_count = nearby.size(); out.ignored_count = ignored.size(); - std::vector<::chat::contacts::NodeInfo> visible_nodes{}; + std::vector<::chat::contacts::PeerDirectoryItem> visible_nodes{}; visible_nodes.reserve(contacts.size() + nearby.size()); visible_nodes.insert(visible_nodes.end(), contacts.begin(), contacts.end()); visible_nodes.insert(visible_nodes.end(), nearby.begin(), nearby.end()); diff --git a/platform/linux/uconsole/src/uconsole_map_workspace_model.cpp b/platform/linux/uconsole/src/uconsole_map_workspace_model.cpp index 73251d8f..57170c8f 100644 --- a/platform/linux/uconsole/src/uconsole_map_workspace_model.cpp +++ b/platform/linux/uconsole/src/uconsole_map_workspace_model.cpp @@ -152,7 +152,7 @@ std::string trim_copy(std::string value) return value; } -std::string node_label(const ::chat::contacts::NodeInfo& node) +std::string node_label(const ::chat::contacts::PeerDirectoryItem& node) { if (!node.display_name.empty()) { @@ -170,7 +170,7 @@ std::string node_label(const ::chat::contacts::NodeInfo& node) } void append_projected_node(MapWorkspaceSnapshot& out, - const ::chat::contacts::NodeInfo& node, + const ::chat::contacts::PeerDirectoryItem& node, const ::platform::linux_runtime::MapTileId& top_left) { if (!node.position.valid) diff --git a/platform/nrf52/arduino_common/include/ble/meshcore_ble.h b/platform/nrf52/arduino_common/include/ble/meshcore_ble.h index cde1e021..b3ae8f7f 100644 --- a/platform/nrf52/arduino_common/include/ble/meshcore_ble.h +++ b/platform/nrf52/arduino_common/include/ble/meshcore_ble.h @@ -4,7 +4,6 @@ #include "ble/app_phone_facade.h" #include "ble_manager.h" #include "chat/domain/chat_types.h" -#include "chat/ports/i_node_store.h" #include "chat/usecase/chat_service.h" #include "phone/meshcore/meshcore_phone_core.h" #include "phone/meshtastic/meshtastic_phone_core.h" diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/blob_file_store.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/blob_file_store.h index 891da4da..ba21b966 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/blob_file_store.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/blob_file_store.h @@ -1,7 +1,6 @@ #pragma once -#include "chat/ports/i_contact_blob_store.h" -#include "chat/ports/i_node_blob_store.h" +#include "chat/ports/i_mesh_peer_directory_blob_store.h" #include #include @@ -13,7 +12,10 @@ namespace platform::nrf52::arduino_common::chat::infra class BlobFileStore { public: - BlobFileStore(const char* path, uint32_t magic, uint16_t version); + BlobFileStore(const char* path, + uint32_t magic, + uint16_t version, + std::size_t max_payload_bytes); bool loadBlob(std::vector& out); bool saveBlob(const uint8_t* data, size_t len); @@ -35,38 +37,25 @@ class BlobFileStore const char* path_ = nullptr; uint32_t magic_ = 0; uint16_t version_ = 0; + std::size_t max_payload_bytes_ = 0; }; -class NodeBlobFileStore final : public ::chat::contacts::INodeBlobStore +class MeshPeerDirectoryBlobFileStore final + : public ::chat::IMeshPeerDirectoryBlobStore { public: - explicit NodeBlobFileStore(const char* path) - : store_(path, 0x444F4E43UL, 1) + explicit MeshPeerDirectoryBlobFileStore(const char* path) + : store_(path, 0x5244504DUL, 1, 256U * 1024U) { } - bool loadBlob(std::vector& out) override; - bool saveBlob(const uint8_t* data, size_t len) override { return store_.saveBlob(data, len); } + ::chat::MeshPeerDirectoryBlobLoadResult loadBlob( + std::vector& out) override; + bool saveBlob(const uint8_t* data, size_t len) override; void clearBlob() override { store_.clearBlob(); } private: BlobFileStore store_; }; -class ContactBlobFileStore final : public ::chat::IContactBlobStore -{ - public: - explicit ContactBlobFileStore(const char* path) - : store_(path, 0x544E4F43UL, 1) - { - } - - bool loadBlob(std::vector& out) override; - bool saveBlob(const uint8_t* data, size_t len) override { return store_.saveBlob(data, len); } - void clearBlob() { store_.clearBlob(); } - - private: - BlobFileStore store_; -}; - } // namespace platform::nrf52::arduino_common::chat::infra diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/contact_store.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/contact_store.h deleted file mode 100644 index 7d4efe4d..00000000 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/contact_store.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "chat/infra/contact_store_core.h" -#include "platform/nrf52/arduino_common/chat/infra/blob_file_store.h" - -namespace platform::nrf52::arduino_common::chat::infra -{ - -class ContactStore final : public ::chat::contacts::IContactStore -{ - public: - ContactStore(); - - void begin() override; - std::string getNickname(uint32_t node_id) const override; - bool setNickname(uint32_t node_id, const char* nickname) override; - bool removeNickname(uint32_t node_id) override; - bool hasNickname(const char* nickname) const override; - std::vector getAllContactIds() const override; - size_t getCount() const override; - bool clear(); - bool hasContactNode(uint32_t node_id) const; - - private: - ContactBlobFileStore blob_store_; - ::chat::contacts::ContactStoreCore core_; -}; - -} // namespace platform::nrf52::arduino_common::chat::infra diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h index fc5a50a2..ea50eb1c 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h @@ -1,5 +1,6 @@ #pragma once +#include "chat/domain/mesh_peer_directory.h" #include "chat/infra/mesh_incoming_queue.h" #include "chat/infra/meshcore/meshcore_ble_backend.h" #include "chat/infra/meshcore/meshcore_identity_crypto.h" @@ -129,6 +130,7 @@ class MeshCoreRadioAdapter final : public ::chat::IMeshAdapter, std::string short_name_; const ::chat::runtime::SelfIdentityProvider* identity_provider_ = nullptr; chat_contacts::ContactService* contact_service_ = nullptr; + ::chat::MeshPeerRecord peer_record_scratch_{}; bool keys_ready_ = false; uint8_t public_key_[::chat::meshcore::kMeshCorePubKeySize] = {}; uint8_t private_key_[::chat::meshcore::kMeshCorePrivKeySize] = {}; diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/meshtastic_radio_adapter.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/meshtastic_radio_adapter.h index 6c562680..226e30bb 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/meshtastic_radio_adapter.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/meshtastic_radio_adapter.h @@ -12,7 +12,6 @@ #include "mesh/domain/peer_identity.h" #include "meshtastic/mesh.pb.h" #include "meshtastic/mqtt.pb.h" -#include "platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h" #include "sys/ringbuf.h" #include @@ -29,7 +28,6 @@ class MeshtasticRadioAdapter final : public ::chat::IMeshAdapter using MqttProxySettings = ::chat::meshtastic::MqttProxyRuntimeSettings; explicit MeshtasticRadioAdapter(const ::chat::runtime::SelfIdentityProvider* identity_provider = nullptr, - NodeStore* node_store = nullptr, ::chat::contacts::ContactService* contact_service = nullptr); ::chat::MeshCapabilities getCapabilities() const override; @@ -372,8 +370,8 @@ class MeshtasticRadioAdapter final : public ::chat::IMeshAdapter std::string long_name_; std::string short_name_; const ::chat::runtime::SelfIdentityProvider* identity_provider_ = nullptr; - NodeStore* node_store_ = nullptr; ::chat::contacts::ContactService* contact_service_ = nullptr; + ::chat::MeshPeerRecord peer_record_scratch_{}; float last_rx_rssi_ = std::numeric_limits::quiet_NaN(); float last_rx_snr_ = std::numeric_limits::quiet_NaN(); static constexpr std::size_t kIncomingQueueDepth = 12; diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h deleted file mode 100644 index cafdefb5..00000000 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include "chat/infra/node_store_core.h" -#include "platform/nrf52/arduino_common/chat/infra/blob_file_store.h" - -#include - -namespace platform::nrf52::arduino_common::chat::meshtastic -{ - -class NodeStore final : public ::chat::contacts::INodeStore -{ - public: - NodeStore(); - - void begin() override; - void applyUpdate(uint32_t node_id, const ::chat::contacts::NodeUpdate& update) override; - void upsert(uint32_t node_id, const char* short_name, const char* long_name, - uint32_t now_secs, float snr = 0.0f, float rssi = 0.0f, uint8_t protocol = 0, - uint8_t role = ::chat::contacts::kNodeRoleUnknown, uint8_t hops_away = 0xFF, - uint8_t hw_model = 0, uint8_t channel = 0xFF) override; - void updateProtocol(uint32_t node_id, uint8_t protocol, uint32_t now_secs) override; - void updatePosition(uint32_t node_id, const ::chat::contacts::NodePosition& position) override; - bool setNextHop(uint32_t node_id, uint8_t next_hop, uint32_t now_secs); - uint8_t getNextHop(uint32_t node_id) const; - bool remove(uint32_t node_id) override; - const std::vector<::chat::contacts::NodeEntry>& getEntries() const override; - void clear() override; - bool flush() override; - void setProtectedNodeChecker(std::function checker); - - private: - ::platform::nrf52::arduino_common::chat::infra::NodeBlobFileStore blob_store_; - ::chat::contacts::NodeStoreCore core_; -}; - -} // namespace platform::nrf52::arduino_common::chat::meshtastic diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/device_identity.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/device_identity.h index 60a6e49d..57a6ce2f 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/device_identity.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/device_identity.h @@ -1,7 +1,6 @@ #pragma once #include "chat/domain/chat_types.h" -#include "chat/ports/i_node_store.h" #include #include @@ -12,8 +11,7 @@ namespace platform::nrf52::arduino_common::device_identity ::chat::NodeId deriveNodeIdFromDeviceAddress(uint32_t deviceaddr0, uint32_t deviceaddr1); std::array deriveMacAddressFromDeviceAddress(uint32_t deviceaddr0, uint32_t deviceaddr1); ::chat::NodeId resolveNodeId(uint32_t deviceaddr0, uint32_t deviceaddr1, - uint32_t deviceid0, uint32_t deviceid1, - const ::chat::contacts::INodeStore* node_store); + uint32_t deviceid0, uint32_t deviceid1); void setResolvedSelfNodeId(::chat::NodeId node_id); void clearResolvedSelfNodeId(); ::chat::NodeId getSelfNodeId(); diff --git a/platform/nrf52/arduino_common/src/ble/app_phone_facade.cpp b/platform/nrf52/arduino_common/src/ble/app_phone_facade.cpp index 0f5e9cbc..836d8665 100644 --- a/platform/nrf52/arduino_common/src/ble/app_phone_facade.cpp +++ b/platform/nrf52/arduino_common/src/ble/app_phone_facade.cpp @@ -4,7 +4,6 @@ #include "chat/infra/meshcore/meshcore_ble_backend.h" #include "chat/infra/meshtastic/mt_radio_config.h" #include "chat/ports/i_mesh_adapter.h" -#include "chat/ports/i_node_store.h" #include "chat/usecase/chat_service.h" #include "chat/usecase/contact_service.h" #include "platform/nrf52/arduino_common/chat/infra/meshtastic/meshtastic_radio_adapter.h" @@ -81,7 +80,8 @@ platform::nrf52::arduino_common::chat::meshtastic::MeshtasticRadioAdapter* getMe return static_cast(backend); } -void copyNodeView(const chat::contacts::NodeEntry& entry, phone::PhoneNodeView& out) +void copyNodeView(const chat::contacts::PeerDirectoryItem& entry, + phone::PhoneNodeView& out) { out = {}; out.node_id = entry.node_id; @@ -93,8 +93,8 @@ void copyNodeView(const chat::contacts::NodeEntry& entry, phone::PhoneNodeView& out.hops_away = entry.hops_away; out.channel = entry.channel; out.next_hop = entry.next_hop; - out.protocol = entry.protocol; - out.role = entry.role; + out.protocol = static_cast(entry.protocol); + out.role = static_cast(entry.role); out.hw_model = entry.hw_model; out.has_macaddr = entry.has_macaddr; std::memcpy(out.macaddr, entry.macaddr, sizeof(out.macaddr)); @@ -104,17 +104,7 @@ void copyNodeView(const chat::contacts::NodeEntry& entry, phone::PhoneNodeView& out.key_manually_verified = entry.key_manually_verified; out.has_device_metrics = entry.has_device_metrics; out.device_metrics = entry.device_metrics; - out.position.valid = entry.position_valid; - out.position.latitude_i = entry.position_latitude_i; - out.position.longitude_i = entry.position_longitude_i; - out.position.has_altitude = entry.position_has_altitude; - out.position.altitude = entry.position_altitude; - out.position.timestamp = entry.position_timestamp; - out.position.precision_bits = entry.position_precision_bits; - out.position.pdop = entry.position_pdop; - out.position.hdop = entry.position_hdop; - out.position.vdop = entry.position_vdop; - out.position.gps_accuracy_mm = entry.position_gps_accuracy_mm; + out.position = entry.position; } } // namespace @@ -232,46 +222,28 @@ bool AppPhoneFacade::pollIncomingPhoneData(chat::MeshIncomingData& out) std::size_t AppPhoneFacade::phoneNodeCount() const { - const auto* store = app_.getNodeStore(); - return store ? store->getEntries().size() : 0; + return app_.getContactService().getAllPeers().size(); } bool AppPhoneFacade::getPhoneNodeByIndex(std::size_t index, phone::PhoneNodeView& out) const { - const auto* store = app_.getNodeStore(); - if (!store) - { - return false; - } - const auto& entries = store->getEntries(); + const auto entries = app_.getContactService().getAllPeers(); if (index >= entries.size()) { return false; } copyNodeView(entries[index], out); - if (const auto* node = app_.getContactService().getNodeInfo(out.node_id)) - { - out.position = node->position; - } return true; } bool AppPhoneFacade::findPhoneNode(chat::NodeId node_id, phone::PhoneNodeView& out) const { - const auto* store = app_.getNodeStore(); - if (!store) - { - return false; - } - for (const auto& entry : store->getEntries()) + const auto entries = app_.getContactService().getAllPeers(); + for (const auto& entry : entries) { if (entry.node_id == node_id) { copyNodeView(entry, out); - if (const auto* node = app_.getContactService().getNodeInfo(out.node_id)) - { - out.position = node->position; - } return true; } } diff --git a/platform/nrf52/arduino_common/src/chat/infra/blob_file_store.cpp b/platform/nrf52/arduino_common/src/chat/infra/blob_file_store.cpp index 48385d4e..4a1efffb 100644 --- a/platform/nrf52/arduino_common/src/chat/infra/blob_file_store.cpp +++ b/platform/nrf52/arduino_common/src/chat/infra/blob_file_store.cpp @@ -1,8 +1,5 @@ #include "platform/nrf52/arduino_common/chat/infra/blob_file_store.h" -#include "chat/infra/contact_store_core.h" -#include "chat/infra/node_store_blob_format.h" -#include "chat/infra/node_store_core.h" #include "platform/nrf52/arduino_common/internal_fs_utils.h" #include @@ -20,9 +17,6 @@ using Adafruit_LittleFS_Namespace::FILE_O_READ; constexpr const char* kLogTag = "[nrf52][blob_store]"; constexpr const char* kTempSuffix = ".tmp"; constexpr std::size_t kBlobReadChunkBytes = 128; -constexpr std::size_t kMaxBlobPayloadBytes = - ::chat::contacts::NodeStoreCore::kMaxNodes * - ::chat::contacts::NodeStoreCore::kSerializedEntrySize; bool writeBytes(Adafruit_LittleFS_Namespace::File& file, const uint8_t* data, std::size_t len) { @@ -69,9 +63,10 @@ bool readBytes(Adafruit_LittleFS_Namespace::File& file, uint8_t* data, std::size bool readBlobPayload(Adafruit_LittleFS_Namespace::File& file, std::size_t len, + std::size_t max_len, std::vector& out) { - if (len > kMaxBlobPayloadBytes) + if (len > max_len) { return false; } @@ -81,10 +76,14 @@ bool readBlobPayload(Adafruit_LittleFS_Namespace::File& file, } // namespace -BlobFileStore::BlobFileStore(const char* path, uint32_t magic, uint16_t version) +BlobFileStore::BlobFileStore(const char* path, + uint32_t magic, + uint16_t version, + std::size_t max_payload_bytes) : path_(path), magic_(magic), - version_(version) + version_(version), + max_payload_bytes_(max_payload_bytes) { } @@ -150,7 +149,8 @@ bool BlobFileStore::loadBlob(std::vector& out) return false; } - const bool read_ok = readBlobPayload(file, header.payload_len, out); + const bool read_ok = + readBlobPayload(file, header.payload_len, max_payload_bytes_, out); file.close(); if (!read_ok || computeCrc(out.data(), out.size()) != header.crc) { @@ -164,7 +164,9 @@ bool BlobFileStore::loadBlob(std::vector& out) bool BlobFileStore::saveBlob(const uint8_t* data, size_t len) { - if (!path_ || !::platform::nrf52::arduino_common::internal_fs::ensureMounted(false, kLogTag)) + if (!path_ || len > max_payload_bytes_ || + !::platform::nrf52::arduino_common::internal_fs::ensureMounted(false, + kLogTag)) { return false; } @@ -245,40 +247,21 @@ uint32_t BlobFileStore::computeCrc(const uint8_t* data, size_t len) return ~crc; } -bool NodeBlobFileStore::loadBlob(std::vector& out) +::chat::MeshPeerDirectoryBlobLoadResult +MeshPeerDirectoryBlobFileStore::loadBlob(std::vector& out) { if (!store_.loadBlob(out)) { - return false; + return ::chat::MeshPeerDirectoryBlobLoadResult::Missing; } - - if (!::chat::contacts::isValidNodeBlobSize(out.size()) || - ::chat::contacts::nodeBlobEntryCount(out.size()) > ::chat::contacts::NodeStoreCore::kMaxNodes) - { - out.clear(); - store_.clearBlob(); - return false; - } - - return true; + return ::chat::MeshPeerDirectoryBlobLoadResult::Loaded; } -bool ContactBlobFileStore::loadBlob(std::vector& out) +bool MeshPeerDirectoryBlobFileStore::saveBlob( + const uint8_t* data, + size_t len) { - if (!store_.loadBlob(out)) - { - return false; - } - - if ((out.size() % ::chat::contacts::ContactStoreCore::kSerializedEntrySize) != 0 || - (out.size() / ::chat::contacts::ContactStoreCore::kSerializedEntrySize) > ::chat::contacts::ContactStoreCore::kMaxContacts) - { - out.clear(); - store_.clearBlob(); - return false; - } - - return true; + return store_.saveBlob(data, len); } } // namespace platform::nrf52::arduino_common::chat::infra diff --git a/platform/nrf52/arduino_common/src/chat/infra/contact_store.cpp b/platform/nrf52/arduino_common/src/chat/infra/contact_store.cpp deleted file mode 100644 index d0ce7e63..00000000 --- a/platform/nrf52/arduino_common/src/chat/infra/contact_store.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "platform/nrf52/arduino_common/chat/infra/contact_store.h" - -namespace platform::nrf52::arduino_common::chat::infra -{ - -ContactStore::ContactStore() - : blob_store_("/chat_contacts.bin"), - core_(blob_store_) -{ -} - -void ContactStore::begin() -{ - core_.begin(); -} - -std::string ContactStore::getNickname(uint32_t node_id) const -{ - return core_.getNickname(node_id); -} - -bool ContactStore::setNickname(uint32_t node_id, const char* nickname) -{ - return core_.setNickname(node_id, nickname); -} - -bool ContactStore::removeNickname(uint32_t node_id) -{ - return core_.removeNickname(node_id); -} - -bool ContactStore::hasNickname(const char* nickname) const -{ - return core_.hasNickname(nickname); -} - -std::vector ContactStore::getAllContactIds() const -{ - return core_.getAllContactIds(); -} - -size_t ContactStore::getCount() const -{ - return core_.getCount(); -} - -bool ContactStore::clear() -{ - return core_.clear(); -} - -bool ContactStore::hasContactNode(uint32_t node_id) const -{ - return !core_.getNickname(node_id).empty(); -} - -} // namespace platform::nrf52::arduino_common::chat::infra 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 871b5eb2..1753ff1a 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 @@ -524,6 +524,38 @@ bool MeshCoreRadioAdapter::executeProtocolEffect(const ::chat::runtime::Protocol else if constexpr (std::is_same_v) { ok = item.protocol == ::chat::MeshProtocol::MeshCore; + if (ok && contact_service_ && + item.public_key.size() == + ::chat::kMeshPeerMeshCorePublicKeyLen) + { + peer_record_scratch_ = {}; + peer_record_scratch_.valid = true; + if (::chat::makeMeshPeerPublicKeyIdentity( + ::chat::MeshProtocol::MeshCore, + item.public_key.data(), + item.public_key.size(), + peer_record_scratch_.identity)) + { + peer_record_scratch_.source = + ::chat::MeshPeerSource::RuntimeRx; + peer_record_scratch_.last_seen_s = + ::chat::now_epoch_seconds(); + peer_record_scratch_.meshcore.has_public_key = true; + std::memcpy( + peer_record_scratch_.meshcore.public_key, + item.public_key.data(), + ::chat::kMeshPeerMeshCorePublicKeyLen); + peer_record_scratch_.meshcore.public_key_verified = + item.public_key_verified; + peer_record_scratch_.meshcore.node_id_hint = item.peer; + peer_record_scratch_.meshcore.has_peer_hash = true; + peer_record_scratch_.meshcore.peer_hash = item.peer_hash; + peer_record_scratch_.meshcore.has_next_hop = true; + peer_record_scratch_.meshcore.next_hop = item.next_hop; + ok = contact_service_->recordPeer( + peer_record_scratch_); + } + } } else if constexpr (std::is_same_v) { @@ -975,6 +1007,25 @@ void MeshCoreRadioAdapter::handleRawPacket(const uint8_t* data, size_t size) ::chat::meshcore::kMeshCorePubKeySize); if (advert_node != 0 && advert_node != node_id_ && contact_service_) { + peer_record_scratch_ = {}; + peer_record_scratch_.valid = true; + if (::chat::makeMeshPeerPublicKeyIdentity( + ::chat::MeshProtocol::MeshCore, + pubkey, + ::chat::meshcore::kMeshCorePubKeySize, + peer_record_scratch_.identity)) + { + peer_record_scratch_.source = + ::chat::MeshPeerSource::RuntimeRx; + peer_record_scratch_.last_seen_s = + ::chat::now_epoch_seconds(); + peer_record_scratch_.meshcore.has_public_key = true; + std::memcpy(peer_record_scratch_.meshcore.public_key, + pubkey, + ::chat::meshcore::kMeshCorePubKeySize); + peer_record_scratch_.meshcore.node_id_hint = advert_node; + (void)contact_service_->recordPeer(peer_record_scratch_); + } ::chat::contacts::NodeUpdate update{}; if (advert.has_name) { 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 b75008a3..c5f2562a 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 @@ -453,11 +453,9 @@ uint32_t nowSeconds() } // namespace MeshtasticRadioAdapter::MeshtasticRadioAdapter(const ::chat::runtime::SelfIdentityProvider* identity_provider, - NodeStore* node_store, ::chat::contacts::ContactService* contact_service) : node_id_(device_identity::getSelfNodeId()), identity_provider_(identity_provider), - node_store_(node_store), contact_service_(contact_service) { randomSeed(static_cast(NRF_FICR->DEVICEADDR[0] ^ NRF_FICR->DEVICEADDR[1] ^ micros())); @@ -1205,16 +1203,12 @@ void MeshtasticRadioAdapter::handleRawPacket(const uint8_t* data, size_t size) { contact_service_->applyNodeUpdate(node_id, update); } - else if (node_store_) - { - node_store_->applyUpdate(node_id, update); - } }; if (decoded_ok && ::chat::meshtastic::isNodeMetadataPayload(decoded.portnum) && decoded.payload.size > 0 && - (node_store_ || contact_service_)) + contact_service_) { ::chat::meshtastic::NodePayloadDecodeContext context{}; context.fallback_node_id = header.from; @@ -1240,6 +1234,28 @@ void MeshtasticRadioAdapter::handleRawPacket(const uint8_t* data, size_t size) savePkiNodeKey(node.node_id, node.public_key.data(), node.public_key.size()); + if (contact_service_ && + node.public_key.size() == + ::chat::kMeshPeerMeshtasticPublicKeyLen) + { + peer_record_scratch_ = {}; + peer_record_scratch_.valid = true; + peer_record_scratch_.identity = + ::chat::makeMeshPeerNodeIdentity( + ::chat::MeshProtocol::Meshtastic, + node.node_id); + peer_record_scratch_.source = + ::chat::MeshPeerSource::RuntimeRx; + peer_record_scratch_.last_seen_s = nowSeconds(); + peer_record_scratch_.meshtastic.has_public_key = + true; + std::memcpy( + peer_record_scratch_.meshtastic.public_key, + node.public_key.data(), + ::chat::kMeshPeerMeshtasticPublicKeyLen); + (void)contact_service_->recordPeer( + peer_record_scratch_); + } } } maybeBroadcastNodeInfoAfterPeerAnnouncement(node.node_id, @@ -1575,9 +1591,10 @@ void MeshtasticRadioAdapter::processSendQueue() pending.channel_hash, nullptr); } - if (node_store_ && pending.dest != 0 && pending.dest != kBroadcastNode) + if (contact_service_ && pending.dest != 0 && + pending.dest != kBroadcastNode) { - node_store_->setNextHop(pending.dest, 0, nowSeconds()); + contact_service_->setNextHop(pending.dest, 0); } pending_retransmits_.eraseAt(index); continue; @@ -1593,9 +1610,10 @@ void MeshtasticRadioAdapter::processSendQueue() { header->next_hop = 0; pending.fallback_sent = true; - if (node_store_ && pending.dest != 0 && pending.dest != kBroadcastNode) + if (contact_service_ && pending.dest != 0 && + pending.dest != kBroadcastNode) { - node_store_->setNextHop(pending.dest, 0, nowSeconds()); + contact_service_->setNextHop(pending.dest, 0); } } @@ -2191,12 +2209,12 @@ uint8_t MeshtasticRadioAdapter::ourRelayId() const uint8_t MeshtasticRadioAdapter::getLearnedNextHop(::chat::NodeId dest, uint8_t relay_node) const { - if (!node_store_ || dest == 0 || dest == kBroadcastNode) + if (!contact_service_ || dest == 0 || dest == kBroadcastNode) { return 0; } - const uint8_t next_hop = node_store_->getNextHop(dest); + const uint8_t next_hop = contact_service_->getNextHop(dest); if (next_hop == 0 || next_hop == relay_node) { return 0; @@ -2206,11 +2224,12 @@ uint8_t MeshtasticRadioAdapter::getLearnedNextHop(::chat::NodeId dest, uint8_t r void MeshtasticRadioAdapter::learnNextHop(::chat::NodeId dest, uint8_t next_hop) { - if (!node_store_ || dest == 0 || dest == kBroadcastNode || next_hop == 0 || next_hop == ourRelayId()) + if (!contact_service_ || dest == 0 || dest == kBroadcastNode || + next_hop == 0 || next_hop == ourRelayId()) { return; } - (void)node_store_->setNextHop(dest, next_hop, nowSeconds()); + (void)contact_service_->setNextHop(dest, next_hop); } MeshtasticRadioAdapter::PacketHistoryEntry* MeshtasticRadioAdapter::findHistory(::chat::NodeId sender, ::chat::MessageId packet_id) @@ -2382,7 +2401,7 @@ bool MeshtasticRadioAdapter::maybeRebroadcast(const ::chat::meshtastic::PacketHe void MeshtasticRadioAdapter::updateNodeLastSeen(::chat::NodeId node_id, uint8_t hops_away, ::chat::ChannelId channel) { - if (node_id == 0 || node_id == node_id_ || (!node_store_ && !contact_service_)) + if (node_id == 0 || node_id == node_id_ || !contact_service_) { return; } @@ -2405,12 +2424,7 @@ void MeshtasticRadioAdapter::updateNodeLastSeen(::chat::NodeId node_id, uint8_t update.has_channel = true; update.channel = static_cast(channel); - if (contact_service_) - { - contact_service_->applyNodeUpdate(node_id, update); - return; - } - node_store_->applyUpdate(node_id, update); + contact_service_->applyNodeUpdate(node_id, update); } void MeshtasticRadioAdapter::handleRoutingPacket(const ::chat::meshtastic::PacketHeaderWire& header, @@ -2459,9 +2473,9 @@ void MeshtasticRadioAdapter::handleRoutingPacket(const ::chat::meshtastic::Packe } else if (reason == meshtastic_Routing_Error_GOT_NAK || reason == meshtastic_Routing_Error_NO_ROUTE) { - if (node_store_) + if (contact_service_) { - node_store_->setNextHop(header.from, 0, nowSeconds()); + contact_service_->setNextHop(header.from, 0); } } diff --git a/platform/nrf52/arduino_common/src/chat/infra/meshtastic/node_store.cpp b/platform/nrf52/arduino_common/src/chat/infra/meshtastic/node_store.cpp deleted file mode 100644 index 09190058..00000000 --- a/platform/nrf52/arduino_common/src/chat/infra/meshtastic/node_store.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h" - -namespace platform::nrf52::arduino_common::chat::meshtastic -{ - -NodeStore::NodeStore() - : blob_store_("/chat_nodes.bin"), - core_(blob_store_) -{ - core_.setAutoSaveEnabled(false); -} - -void NodeStore::begin() -{ - core_.begin(); -} - -void NodeStore::applyUpdate(uint32_t node_id, const ::chat::contacts::NodeUpdate& update) -{ - core_.applyUpdate(node_id, update); -} - -void NodeStore::upsert(uint32_t node_id, const char* short_name, const char* long_name, - uint32_t now_secs, float snr, float rssi, uint8_t protocol, - uint8_t role, uint8_t hops_away, uint8_t hw_model, uint8_t channel) -{ - core_.upsert(node_id, short_name, long_name, now_secs, snr, rssi, - protocol, role, hops_away, hw_model, channel); -} - -void NodeStore::updateProtocol(uint32_t node_id, uint8_t protocol, uint32_t now_secs) -{ - core_.updateProtocol(node_id, protocol, now_secs); -} - -void NodeStore::updatePosition(uint32_t node_id, const ::chat::contacts::NodePosition& position) -{ - core_.updatePosition(node_id, position); -} - -bool NodeStore::setNextHop(uint32_t node_id, uint8_t next_hop, uint32_t now_secs) -{ - return core_.setNextHop(node_id, next_hop, now_secs); -} - -uint8_t NodeStore::getNextHop(uint32_t node_id) const -{ - return core_.getNextHop(node_id); -} - -bool NodeStore::remove(uint32_t node_id) -{ - return core_.remove(node_id); -} - -const std::vector<::chat::contacts::NodeEntry>& NodeStore::getEntries() const -{ - return core_.getEntries(); -} - -void NodeStore::clear() -{ - core_.clear(); -} - -bool NodeStore::flush() -{ - return core_.flush(); -} - -void NodeStore::setProtectedNodeChecker(std::function checker) -{ - core_.setProtectedNodeChecker(std::move(checker)); -} - -} // namespace platform::nrf52::arduino_common::chat::meshtastic diff --git a/platform/nrf52/arduino_common/src/device_identity.cpp b/platform/nrf52/arduino_common/src/device_identity.cpp index b02d975b..de2cca2b 100644 --- a/platform/nrf52/arduino_common/src/device_identity.cpp +++ b/platform/nrf52/arduino_common/src/device_identity.cpp @@ -16,24 +16,6 @@ bool isReservedNodeId(::chat::NodeId node_id) return node_id == 0 || node_id == kBroadcastNodeId || node_id < kReservedNodeCount; } -bool nodeIdInUse(::chat::NodeId node_id, const ::chat::contacts::INodeStore* node_store) -{ - if (!node_store) - { - return false; - } - - const auto& entries = node_store->getEntries(); - for (const auto& entry : entries) - { - if (entry.node_id == node_id) - { - return true; - } - } - return false; -} - uint32_t mixSeed(uint32_t value) { value ^= value >> 16; @@ -89,8 +71,7 @@ std::array deriveMacAddressFromDeviceAddress(uint32_t deviceaddr0, u } ::chat::NodeId resolveNodeId(uint32_t deviceaddr0, uint32_t deviceaddr1, - uint32_t deviceid0, uint32_t deviceid1, - const ::chat::contacts::INodeStore* node_store) + uint32_t deviceid0, uint32_t deviceid1) { ::chat::NodeId node_id = deriveNodeIdFromDeviceAddress(deviceaddr0, deviceaddr1); // The node store may contain our own previously persisted NodeInfo. Treating @@ -104,7 +85,7 @@ std::array deriveMacAddressFromDeviceAddress(uint32_t deviceaddr0, u for (uint32_t attempt = 0; attempt < 1024; ++attempt) { const ::chat::NodeId candidate = fallbackCandidate(seed, attempt); - if (!isReservedNodeId(candidate) && !nodeIdInUse(candidate, node_store)) + if (!isReservedNodeId(candidate)) { return candidate; } diff --git a/platform/nrf52/protocol/nrf52_protocol_factory.cpp b/platform/nrf52/protocol/nrf52_protocol_factory.cpp index 0a9f0b99..3537ce0d 100644 --- a/platform/nrf52/protocol/nrf52_protocol_factory.cpp +++ b/platform/nrf52/protocol/nrf52_protocol_factory.cpp @@ -8,7 +8,6 @@ namespace platform::nrf52::protocol std::unique_ptr createProtocolAdapter(chat::MeshProtocol protocol, const chat::runtime::SelfIdentityProvider* identity_provider, - platform::nrf52::arduino_common::chat::meshtastic::NodeStore* meshtastic_node_store, chat::contacts::ContactService* contact_service) { switch (protocol) @@ -21,7 +20,6 @@ std::unique_ptr createProtocolAdapter(chat::MeshProtocol pro default: return std::unique_ptr( new platform::nrf52::arduino_common::chat::meshtastic::MeshtasticRadioAdapter(identity_provider, - meshtastic_node_store, contact_service)); } } diff --git a/platform/nrf52/protocol/nrf52_protocol_factory.h b/platform/nrf52/protocol/nrf52_protocol_factory.h index 7a3eec05..3a7153c8 100644 --- a/platform/nrf52/protocol/nrf52_protocol_factory.h +++ b/platform/nrf52/protocol/nrf52_protocol_factory.h @@ -3,7 +3,6 @@ #include "chat/domain/chat_types.h" #include "chat/runtime/self_identity_provider.h" #include "chat/usecase/contact_service.h" -#include "platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h" #include @@ -17,7 +16,6 @@ namespace platform::nrf52::protocol std::unique_ptr createProtocolAdapter(chat::MeshProtocol protocol, const chat::runtime::SelfIdentityProvider* identity_provider, - platform::nrf52::arduino_common::chat::meshtastic::NodeStore* meshtastic_node_store = nullptr, chat::contacts::ContactService* contact_service = nullptr); } // namespace platform::nrf52::protocol