Skip to content

fix(firmware): Keep bundled release discovery available#6082

Merged
jamesarich merged 7 commits into
meshtastic:mainfrom
jeremiah-k:bugfix/firmware-release-discovery
Jul 8, 2026
Merged

fix(firmware): Keep bundled release discovery available#6082
jamesarich merged 7 commits into
meshtastic:mainfrom
jeremiah-k:bugfix/firmware-release-discovery

Conversation

@jeremiah-k

@jeremiah-k jeremiah-k commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR fixes firmware release discovery when the app needs bundled release metadata.

F-Droid builds do not use the Meshtastic firmware release API, so bundled release metadata needs to remain available for the firmware update screen. The app also uses per-device databases, which means a bundled release snapshot seeded into one active database is not enough for later selected-device databases.

This change keeps bundled release discovery available for the current active database while still preventing unknown hardware from becoming flashable.

Key Changes

  • Cached the decoded bundled release snapshot for the current process.
  • Re-evaluated bundled release seeding against the current active database.
  • Seeded newly active or empty device databases from the bundled release snapshot.
  • Preserved newer cached release data instead of regressing it with older bundled data.
  • Kept partial bundled snapshots from deleting release types they do not contain.
  • Marked bundled release data unavailable only when the bundled asset cannot be read or decoded.
  • Applied bundled snapshots as a best-effort path so apply failures do not poison release discovery.
  • Avoided holding the bundled decode mutex while waiting on the refresh/apply mutex.
  • Preserved selected release state when hardware lookup fails.
  • Cleared stale device metadata when hardware lookup fails.
  • Kept the Ready state gated on resolved device hardware.
  • Added focused coverage for active database switching, bundled seeding behavior, error handling, and selected-release preservation.

Testing

  • Added coverage proving an empty cache seeds from the bundled snapshot.
  • Added coverage proving a newly active database is seeded after a previous database was already seeded.
  • Added coverage proving newer bundled data replaces older cached release data.
  • Added coverage proving older bundled data does not regress newer cached release data.
  • Added coverage proving partial bundled snapshots leave missing release types untouched.
  • Added coverage proving selected release state can populate when hardware lookup fails.
  • Added coverage proving unknown hardware remains non-flashable.
  • Verified git diff --check passes.

Migration Notes

  • No user data migration is required.
  • Unknown hardware remains blocked from flashing.
  • This only changes firmware release discovery and update-screen state initialization.

Summary by CodeRabbit

  • Bug Fixes
    • Firmware releases now re-seed/refresh correctly when the active local database changes, rather than relying on a single one-time seed.
    • Bundled firmware data issues (missing/unreadable/invalid content) are treated as permanent, preventing repeated retry attempts.
    • Firmware update checks now clear stale device-related info when hardware lookup fails, ensuring the correct error state is shown.
    • Cache reads in stale-while-revalidate are more robust, avoiding unexpected failures when local data can’t be loaded.
  • Tests
    • Added coverage for permanent bundled decode failure, reseeding on database switches, and stale-metadata clearing.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dd653f7c-e6a1-437c-b60a-4b491448f74b

📥 Commits

Reviewing files that changed from the base of the PR and between 272f328 and d019ae0.

📒 Files selected for processing (6)
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepositoryImpl.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/util/StaleWhileRevalidateFlow.kt
  • core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepositoryImplTest.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeDatabaseProvider.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
  • feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/util/StaleWhileRevalidateFlow.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeDatabaseProvider.kt
  • core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepositoryImplTest.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepositoryImpl.kt

📝 Walkthrough

Walkthrough

Firmware seeding now caches decoded bundled releases and stops retrying decode failures. Cache reads in stale-while-revalidate are guarded, and firmware update handling now clears device metadata on hardware lookup errors while keeping the update flow aligned with the current device state.

Changes

Firmware data and update flow

Layer / File(s) Summary
Bundled seeding cache and reapply logic
core/data/.../FirmwareReleaseRepositoryImpl.kt
Adds cached bundled snapshot state, permanently records decode failures, and re-evaluates on each call which bundled release types should be applied to the active database.
Cache read error handling
core/data/.../StaleWhileRevalidateFlow.kt
Wraps cache reads in stale-while-revalidate flow execution before and after fetch so cache read failures log and continue with a null cache result.
Repository test support and database switching
core/data/.../FirmwareReleaseRepositoryImplTest.kt, core/testing/.../FakeDatabaseProvider.kt
Extends the fake bundled asset reader with controlled failures, adds active-database switching support to the fake provider, and covers decode-failure suppression and re-seeding after switching databases.
Firmware update release and metadata handling
feature/firmware/.../FirmwareUpdateViewModel.kt, feature/firmware/.../FirmwareUpdateViewModelTest.kt
Collects releases when device hardware is available, returns early when it is not, clears device metadata on hardware lookup errors, and updates tests to assert cleared metadata and error states.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the main firmware change: keeping bundled release discovery available.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bugfix PR tag label Jul 5, 2026
@jeremiah-k
jeremiah-k force-pushed the bugfix/firmware-release-discovery branch from e3c4275 to 419355c Compare July 5, 2026 14:17
@jeremiah-k jeremiah-k changed the title fix(firmware): Keep release discovery retryable fix(firmware): Keep bundled release discovery available Jul 5, 2026
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 5, 2026 15:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeDatabaseProvider.kt (1)

27-42: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Minor: db isn't volatile, and the previous DB is closed immediately on switch.

db is a plain var read by withDb() and written by switchToNewDatabase(); without @Volatile a reader on a different dispatcher/thread than the writer isn't guaranteed to see the update. Also, switchToNewDatabase() closes previous synchronously — if any in-flight withDb() call captured the old instance concurrently, it would keep using (or get errors from) a closed database.

Not exercised by the tests in this diff (usage is sequential), but worth hardening if this fake is reused with real dispatchers/concurrent collectors in future tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeDatabaseProvider.kt`
around lines 27 - 42, The FakeDatabaseProvider’s shared db state is not safe
across threads, and switchToNewDatabase() closes the old database immediately
while withDb() may still be using it. Harden FakeDatabaseProvider by making the
db field visibly safe for cross-dispatcher reads (or otherwise synchronizing
access) and avoid closing the previous database synchronously until you know no
in-flight withDb() calls can still reference it. Keep the fix centered around
db, withDb(), and switchToNewDatabase() so the fake remains reliable under
concurrent test usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepositoryImpl.kt`:
- Around line 105-134: The bundle seeding logic in ensureSeeded() can still
overwrite newer data because it compares with isBundleNewerFor() outside the
same lock used by singleFlightRefresh(). Update the compare-and-write path so
replaceFirmwareReleases() is guarded by refreshMutex as well, or recheck
freshness under that lock immediately before writing. Keep the fix localized to
FirmwareReleaseRepositoryImpl and use the existing mutexes and
localDataSource.replaceFirmwareReleases() path.

---

Nitpick comments:
In
`@core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeDatabaseProvider.kt`:
- Around line 27-42: The FakeDatabaseProvider’s shared db state is not safe
across threads, and switchToNewDatabase() closes the old database immediately
while withDb() may still be using it. Harden FakeDatabaseProvider by making the
db field visibly safe for cross-dispatcher reads (or otherwise synchronizing
access) and avoid closing the previous database synchronously until you know no
in-flight withDb() calls can still reference it. Keep the fix centered around
db, withDb(), and switchToNewDatabase() so the fake remains reliable under
concurrent test usage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7bc7b5e1-ad73-4ab4-b7ec-54fd016231fc

📥 Commits

Reviewing files that changed from the base of the PR and between d8afe84 and 419355c.

📒 Files selected for processing (5)
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepositoryImpl.kt
  • core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepositoryImplTest.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeDatabaseProvider.kt
  • feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
  • feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt

@jeremiah-k
jeremiah-k marked this pull request as draft July 5, 2026 15:17
@jeremiah-k
jeremiah-k force-pushed the bugfix/firmware-release-discovery branch from 419355c to c9a1ef8 Compare July 5, 2026 17:46
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 5, 2026 18:36
@jeremiah-k
jeremiah-k force-pushed the bugfix/firmware-release-discovery branch from c9a1ef8 to faa5edb Compare July 5, 2026 20:20

@jamesarich jamesarich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after the force-push. The re-seed fix is solid — bundled releases now re-seed when the active device DB switches (good test), and the bundleDecodeFailed latch still prevents per-collection retry. Three carryovers inline; none are hard blockers, but the first two are worth tightening before merge.

} else {
FirmwareUpdateMethod.Usb
}
releaseFlow.collectLatest { release ->

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description says release collection now starts before hardware resolution, but releaseFlow is a cold flow still collected here — after getDeviceHardware() returns at line 180, in the same coroutine. During a slow api.meshtastic.org hardware lookup the release state is still not populated any earlier. The real fix is the null-guard restructure below; the "collect before resolution" framing (and the construction reorder) is inert — worth correcting the PR body so it doesn't imply a latency win that isn't there.

@jeremiah-k
jeremiah-k marked this pull request as draft July 5, 2026 20:29

@jamesarich jamesarich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 4f9ccd6 — two of the three are fixed:

  • Recovery path clears stale metadata (clearDeviceMetadata() in both branches of enterRecoveryModeOrError), with recovery hardware lookup failure clears stale device metadata.
  • Specific unknown-hardware Error no longer clobbered — on hardware-lookup failure the connected path now return@launches before collecting the release flow, so a throwing cache read can't overwrite it (the new test stubs the cache to throw and asserts the specific error survives).

Two follow-ups:

  1. PR body now contradicts the code. The harden release discovery errors commit stops collecting the release flow when hardware resolution fails (getDeviceHardware(ourNode) ?: return@launch) — the opposite of the Key Changes line "Ensured release state is still collected when hardware lookup fails." And neither head ever started collection before hardware resolution (it's a cold flow collected in the same coroutine). Please update the description to match what the code does.
  2. Same clobber survives on the recovery Ready path. The fix short-circuited only the connected hardware-failure path. After a successful recovery lookup, getReleaseFlow(type).collectLatest still runs inside the outer safeCatching, and loadFromCache() in StaleWhileRevalidateFlow is still unguarded (only fetch is), so a throwing cache read there overwrites the recovery Ready state with a generic DynamicString(e.message). Guarding loadFromCache at the source would cover both paths.

@jeremiah-k
jeremiah-k force-pushed the bugfix/firmware-release-discovery branch 4 times, most recently from 5f32e79 to 272f328 Compare July 8, 2026 19:38
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 8, 2026 20:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/data/src/commonMain/kotlin/org/meshtastic/core/data/util/StaleWhileRevalidateFlow.kt`:
- Around line 84-90: The reload path in StaleWhileRevalidateFlow should not emit
null when loadFromCache() fails after a successful fetch. Update the logic
around safeCatching { loadFromCache() } and the subsequent fresh/cached
comparison so that a reload failure falls back to the previous cached value
instead of getOrNull() producing null. Keep the existing logging in the cache
reload block, and make sure the emit call only publishes a real updated value or
preserves the prior cached state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a32c272-5c79-48b7-8f1c-e22be8fec774

📥 Commits

Reviewing files that changed from the base of the PR and between faa5edb and 272f328.

📒 Files selected for processing (4)
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepositoryImpl.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/util/StaleWhileRevalidateFlow.kt
  • core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepositoryImplTest.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeDatabaseProvider.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeDatabaseProvider.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepositoryImpl.kt
  • core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepositoryImplTest.kt

@jeremiah-k
jeremiah-k marked this pull request as draft July 8, 2026 20:30
Mark the bundled firmware release seed as checked only after the bundled snapshot path completes successfully. A failed asset decode or local cache update now leaves the seed eligible for a later collection retry instead of suppressing bundled fallback for the rest of the process.

Start collecting the selected release flow before resolving device hardware so release cache seeding and selected-release state are not skipped by an unknown hardware lookup. FirmwareUpdateState.Ready remains gated on resolved hardware so update actions are still blocked when the device context is not known.

Add focused coverage for retrying a failed bundled seed and for preserving selected release state when hardware lookup fails.
Resolve device hardware once per update refresh before collecting release emissions, keeping release discovery independent from hardware lookup while avoiding repeated repository lookups for each release emission.

Clear cached device hardware and firmware version when hardware resolution fails so the firmware screen does not keep displaying stale device metadata beside an error state.

Extend the hardware-failure coverage to start from a successful Ready state, then verify selected release preservation, metadata clearing, and the final error state after a failed refresh.
The process-wide seedChecked flag assumed one successful seed meant
every Room database was populated. But the active database switches
per selected device, so the first seed could populate the default DB
and skip the device-specific DB entirely.

Replace seedChecked with a cached decoded snapshot (bundledSnapshot)
that avoids re-reading the asset file, but re-evaluates the apply/skip
decision against the current active database on every collection. When
the device DB switches and is empty, the bundle re-applies.

Also removes temporary diagnostic logging.
Guard bundled release compare-and-write behind the refresh mutex so an older bundled snapshot cannot race and replace fresher API data.

Stop collecting release flows after hardware resolution fails, clear stale firmware metadata on connected and recovery errors, and cover both paths in view-model tests.
Wrap both loadFromCache calls in staleWhileRevalidateFlow with
safeCatching so a cache-read failure logs and returns null instead
of throwing into the caller. This prevents a DB error during release
cache reads from clobbering a recovery Ready state with a generic
error on the recovery path.
…ailure

A transient loadFromCache() failure after a successful fetch was
producing null via getOrNull(), which then emitted null downstream and
wiped valid cached state for consumers. Use getOrElse { cached } so a
reload failure preserves the prior cached value instead.
@jeremiah-k
jeremiah-k force-pushed the bugfix/firmware-release-discovery branch from 272f328 to d019ae0 Compare July 8, 2026 20:41
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 8, 2026 20:56
@jeremiah-k

Copy link
Copy Markdown
Contributor Author

@jamesarich I pushed a refreshed branch with the release-state cleanup tightened.

The description no longer claims release collection starts before hardware resolution. The code now keeps bundled seeding DB-aware, clears stale metadata on hardware lookup failures, and guards cache reads in the stale-while-revalidate flow so a transient cache/DB read failure does not overwrite the more specific firmware update state.

@jamesarich jamesarich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Traced the core logic (no deadlock / correct temp-file lifecycle & validation respectively) — LGTM. Details in review pass.

@jamesarich
jamesarich added this pull request to the merge queue Jul 8, 2026
Merged via the queue into meshtastic:main with commit 8662a27 Jul 8, 2026
17 checks passed
@jeremiah-k
jeremiah-k deleted the bugfix/firmware-release-discovery branch July 8, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants