From 175c8fca3da08eacc3d281a8f6415d088b1646ea Mon Sep 17 00:00:00 2001 From: liu weikai Date: Mon, 28 Sep 2026 17:41:11 +0800 Subject: [PATCH] Read saved geocaches without online startup or GPX rehashing --- .../tests/test_browse_runtime.cpp | 11 ++- .../tests/test_journal_replay.cpp | 7 +- .../core_geocaching/tests/test_sd_journal.cpp | 6 +- .../geocaching/saved_cache_catalog.h | 75 ++++--------------- .../src/geocaching/browse_runtime.cpp | 38 ++++++---- 5 files changed, 57 insertions(+), 80 deletions(-) diff --git a/modules/core_geocaching/tests/test_browse_runtime.cpp b/modules/core_geocaching/tests/test_browse_runtime.cpp index 78cf836e..dc0dd80a 100644 --- a/modules/core_geocaching/tests/test_browse_runtime.cpp +++ b/modules/core_geocaching/tests/test_browse_runtime.cpp @@ -467,10 +467,13 @@ int main(int argc, char** argv) const auto disk = test::files; const auto disk_directories = test::directories; std::fprintf(stderr, "Runtime: healthy restart\n"); + // Cold offline startup has no dispatch destination or QueryClient yet. + // Saved rows and full details must not wait for either to become available. + router.ready = false; test::source->activate(true); until([&] - { return std::strstr(snapshot(Section::Discover).status.data(), "Finding a public directory"); }, - "restart did not restore indexed state"); + { return std::strstr(snapshot(Section::Discover).status.data(), "Waiting for Reticulum IP connection"); }, + "offline startup did not wait for its online identity"); test::source->requestWindow(Section::Downloaded, 0, 4); until([&] { @@ -478,7 +481,6 @@ int main(int argc, char** argv) return view.count == 1 && test::source->item(Section::Downloaded, 0, view.generation, item) && item.id == saved_id && item.downloaded; }, "restart lost downloaded map row"); require(test::files == disk, "read-only restart changed persisted files"); - router.ready = false; const auto offline_sends = router.sends; test::source->open(item, snapshot(Section::Downloaded).generation); until([&] @@ -490,6 +492,9 @@ int main(int argc, char** argv) test::source->closeDetail(); tick(); router.ready = true; + until([&] + { return std::strstr(snapshot(Section::Discover).status.data(), "Finding a public directory"); }, + "offline session did not resume discovery when transport became ready"); test::source->requestWindow(Section::Published, 0, 4); until([&] { diff --git a/modules/core_geocaching/tests/test_journal_replay.cpp b/modules/core_geocaching/tests/test_journal_replay.cpp index a1657958..89053408 100644 --- a/modules/core_geocaching/tests/test_journal_replay.cpp +++ b/modules/core_geocaching/tests/test_journal_replay.cpp @@ -2492,6 +2492,7 @@ int checkIndexedDownloadReceipt(const char* path) if (read_saved(*indexed, {saved_id.data(), 32}, false, crypto) != DownloadRecoveryRead::End || owner.holder()) return 364; if (read_saved(*indexed, {other_cache.bytes.data(), 32}, true, crypto) != DownloadRecoveryRead::End || owner.holder()) return 365; + const auto gpx_reads_before_listing = read_bytes[target]; SavedCacheCatalog catalog(*indexed, crypto); if (!owner.acquire(&other_owner) || catalog.advance() || !catalog.pending() || !owner.heldBy(&other_owner)) return 366; owner.release(&other_owner); @@ -2525,12 +2526,14 @@ int checkIndexedDownloadReceipt(const char* path) !catalog.checked(other_cache.bytes, record.hash.bytes) || catalog.contains(other_cache.bytes, record.hash.bytes)) return 373; files.at(target)[0] ^= 1; catalog.reset(); - if (!advance_catalog() || catalog.contains(record.id.bytes, record.hash.bytes) || !catalog.checked(record.id.bytes, record.hash.bytes)) return 374; + // Listing describes the installed record, without rereading the GPX. + // External file edits are checked before replacing that file. + if (!advance_catalog() || !catalog.contains(record.id.bytes, record.hash.bytes) || !catalog.checked(record.id.bytes, record.hash.bytes)) return 374; files = disk; catalog.requestWindow(0, 4); if (!advance_catalog()) return 375; catalog.snapshot(snapshot); - if (snapshot.count != 1) return 376; + if (snapshot.count != 1 || read_bytes[target] != gpx_reads_before_listing) return 376; auto make_saved_reader = [&](size_t frame_capacity, size_t verify_capacity, protocol::RecordCrypto& verifier) { diff --git a/modules/core_geocaching/tests/test_sd_journal.cpp b/modules/core_geocaching/tests/test_sd_journal.cpp index 0f243c9e..048248c1 100644 --- a/modules/core_geocaching/tests/test_sd_journal.cpp +++ b/modules/core_geocaching/tests/test_sd_journal.cpp @@ -777,8 +777,8 @@ int checkDownloadController(const char* query_path, const char* response_path) for (unsigned i = 0; saved.pending() && i < 256; ++i) saved.advance(); saved.snapshot(snapshot); if (snapshot.count != 1 || saved.item(1, snapshot.generation, item)) return 342; - // Reset during hashing must be a UI-only notification. The next - // owner step discards the old reader and starts the new window. + // Reset is a UI-only notification. The next owner step starts + // reading metadata for the new window. saved.requestWindow(0, 1); saved.advance(); fixture::step_io_calls = 0; @@ -805,7 +805,7 @@ int checkDownloadController(const char* query_path, const char* response_path) saved.reset(); for (unsigned i = 0; saved.pending() && i < 256; ++i) saved.advance(); saved.snapshot(snapshot); - if (snapshot.count || !std::strstr(snapshot.status.data(), "changed")) return 186; + if (snapshot.count != 1 || !saved.item(0, snapshot.generation, item)) return 186; fixture::files[target] = valid_file; std::array restored_a{}, restored_b{}; storage::LogicalState restored(restored_a.data(), restored_b.data(), restored_a.size()); diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/geocaching/saved_cache_catalog.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/geocaching/saved_cache_catalog.h index 5e7dd518..fd48ccd5 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/geocaching/saved_cache_catalog.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/geocaching/saved_cache_catalog.h @@ -1,16 +1,14 @@ #pragma once #include "platform/esp/arduino_common/geocaching/download_store.h" -#include "platform/esp/arduino_common/geocaching/sd_gpx_hash.h" #include "ui_presentation/geocaching/geocaching_source.h" #include -#include -#include namespace platform::esp::arduino_common::geocaching { // Four owned rows shared by the saved-list window and discovery membership // probes. UI calls never scan storage or close files. The maintenance owner -// advances verification and resets the projection after installs/media handoff. +// reads saved metadata and resets the projection after installs/media handoff. +// GPX integrity belongs to installation/recovery, not list rendering. template class SavedCacheCatalog { @@ -25,7 +23,6 @@ class SavedCacheCatalog { restart_ = true; valid_ = 0; - invalid_ = false; checking_ = true; error_ = DownloadRecoveryRead::End; ++generation_; @@ -36,11 +33,9 @@ class SavedCacheCatalog // Maintenance-owner call before a foreground operation or media handoff. void releaseRead() { - if (!reader_ && !reading_ && !metadata_ready_) return; + if (!reading_) return; if (reading_) store_.releaseRead(); - reading_ = metadata_ready_ = false; - reader_.reset(); - digest_.reset(); + reading_ = false; reset(); } void requestWindow(size_t offset, size_t count) @@ -87,21 +82,11 @@ class SavedCacheCatalog if (restart_) { if (reading_) store_.releaseRead(); - reader_.reset(); - digest_.reset(); - reading_ = metadata_ready_ = has_after_ = false; + reading_ = has_after_ = false; cursor_ = total_ = 0; restart_ = false; } if (!checking_) return false; - if (reader_) - { - if (reader_->step() == GpxHashStep::Reading) return true; - std::array hash; - finish(reader_->result(hash) && hash == current_.file_hash); - return true; - } - if (!metadata_ready_) { if (preview_) { @@ -132,31 +117,13 @@ class SavedCacheCatalog ++cursor_; return true; } - metadata_ready_ = true; - } - char path[112]{}; - std::snprintf(path, sizeof(path), "/trailmate/geocaching/caches/"); - size_t offset = std::strlen(path); - constexpr char hex[] = "0123456789abcdef"; - for (auto byte : current_.id) - { - path[offset++] = hex[byte >> 4]; - path[offset++] = hex[byte & 15]; - } - std::memcpy(path + offset, ".gpx", 5); - digest_.reset(new (std::nothrow) Digest); - if (digest_) reader_.reset(new (std::nothrow) SdGpxHash(*digest_)); - if (!reader_) - { - digest_.reset(); - return readError(DownloadRecoveryRead::Unavailable, true); } if (error_ != DownloadRecoveryRead::End) { error_ = DownloadRecoveryRead::End; ++generation_; } - if (!reader_->open(path)) finish(false); + finish(); return true; } const char* error() const @@ -183,8 +150,7 @@ class SavedCacheCatalog out.generation = generation_; out.can_refresh = !checking_; out.count = count_; - std::snprintf(out.status.data(), out.status.size(), "%s", error() ? error() : checking_ ? "Checking saved GPX files..." - : invalid_ ? "Some GPX files changed or are unavailable" + std::snprintf(out.status.data(), out.status.size(), "%s", error() ? error() : checking_ ? "Loading downloaded caches..." : out.count ? "Saved GPX - available offline" : "No downloaded caches"); } @@ -235,32 +201,23 @@ class SavedCacheCatalog } return false; } - void finish(bool valid) + void finish() { - if (valid) + const auto slot = preview_ ? requestedRow(current_.id, current_.hash) : total_ >= offset_ ? total_ - offset_ + : rows_.size(); + if (slot < window_) { - const auto slot = preview_ ? requestedRow(current_.id, current_.hash) : total_ >= offset_ ? total_ - offset_ - : rows_.size(); - if (slot < window_) - { - rows_[slot] = current_; - valid_ |= uint8_t(1u << slot); - } - ++total_; + rows_[slot] = current_; + valid_ |= uint8_t(1u << slot); } - else invalid_ = true; + ++total_; after_ = current_.id; has_after_ = true; - metadata_ready_ = false; ++cursor_; ++generation_; - reader_.reset(); - digest_.reset(); } DownloadStore& store_; ::geocaching::protocol::RecordCrypto& crypto_; - std::unique_ptr digest_; - std::unique_ptr> reader_; std::array after_{}; std::array<::geocaching::storage::SavedCacheEntry, 4> rows_{}; ::geocaching::storage::SavedCacheRecord current_; @@ -268,7 +225,7 @@ class SavedCacheCatalog size_t cursor_ = 0, total_ = 0, count_ = 0, offset_ = 0, window_ = 4; uint8_t valid_ = 0, requested_ = 0; DownloadRecoveryRead error_ = DownloadRecoveryRead::End; - bool checking_ = true, restart_ = false, invalid_ = false, preview_ = false; - bool reading_ = false, metadata_ready_ = false, has_after_ = false; + bool checking_ = true, restart_ = false, preview_ = false; + bool reading_ = false, has_after_ = false; }; } // namespace platform::esp::arduino_common::geocaching diff --git a/platform/esp/arduino_common/src/geocaching/browse_runtime.cpp b/platform/esp/arduino_common/src/geocaching/browse_runtime.cpp index 5fdfb6e8..9cbccedf 100644 --- a/platform/esp/arduino_common/src/geocaching/browse_runtime.cpp +++ b/platform/esp/arduino_common/src/geocaching/browse_runtime.cpp @@ -1826,7 +1826,7 @@ void step() } advanceBrowse(s); if (s.detail) s.detail->advanceNetwork(*router, s.local, s.crypto, now(nullptr).monotonic_ms); - if (!s.storage_requested || !s.client) + if (!s.storage_requested) { next_step.store(millis() + 100); return; @@ -2126,20 +2126,11 @@ void step() s.download_store.reset(new (std::nothrow) IndexedDownloadStore(s.volume, s.root, s.root_copy, s.roots[0], s.roots[1], s.workspace_owner, s.workspace, s.frame, kFrameCapacity, s.payload, kPayloadCapacity, s.verification, kVerificationCapacity, s.crypto)); - s.dispatch_store.reset(new (std::nothrow) IndexedDispatchStore(s.volume, s.root, s.root_copy, s.roots[0], s.roots[1], - s.workspace_owner, s.workspace, s.frame, kFrameCapacity)); - if (!s.store || !s.download_store || !s.dispatch_store || !s.workspace_owner.setPrepare(Session::prepareWorkspace, &s)) + if (!s.store || !s.download_store || !s.workspace_owner.setPrepare(Session::prepareWorkspace, &s)) { fail("Insufficient indexed storage memory"); return; } - s.dispatcher.reset(new (std::nothrow) RequestDispatcher(*router, *s.dispatch_store, 5000, 120000)); - s.receipts.reset(new (std::nothrow) StoredReplyReceipt(s.volume, s.root, s.local, s.workspace_owner, s.crypto)); - if (!s.dispatcher || !s.receipts) - { - fail("Insufficient storage service memory"); - return; - } s.saved.reset(new (std::nothrow) SavedCacheCatalog(*s.download_store, s.crypto)); if (!s.saved) { @@ -2164,8 +2155,8 @@ void step() return; } // A user-edited installed GPX or missing history remains untouched. - // Its catalogue entry is checked separately; keep browsing other - // caches instead of turning one offline file into a service outage. + // Keep its installed metadata available for browsing instead of + // turning one offline file into a service outage. s.recovery_attention |= result != gc::DownloadOperationResult::Complete || s.download_port->historyPending(); s.download_port.reset(); s.saved->reset(); @@ -2220,6 +2211,27 @@ void step() fail("Storage interrupted - reopen to recover"); return; } + // Local lists, details, drafts and interrupted file installation above + // do not need an online identity or request services. Only resume network + // work once the real local destination is known. + if (!s.client) + { + if (!startCheckpoint(s)) next_step.store(millis() + 100); + return; + } + if (!s.dispatch_store) + s.dispatch_store.reset(new (std::nothrow) IndexedDispatchStore(s.volume, s.root, s.root_copy, s.roots[0], s.roots[1], + s.workspace_owner, s.workspace, s.frame, kFrameCapacity)); + if (s.dispatch_store && !s.dispatcher) + s.dispatcher.reset(new (std::nothrow) RequestDispatcher(*router, *s.dispatch_store, 5000, 120000)); + if (!s.receipts) + s.receipts.reset(new (std::nothrow) StoredReplyReceipt(s.volume, s.root, s.local, s.workspace_owner, s.crypto)); + if (!s.dispatcher || !s.receipts) + { + s.browse_status = "Waiting for network service memory"; + next_step.store(millis() + 1000); + return; + } if (publicationActive()) { auto& job = *s.publication;