diff --git a/src/components/EspFlasher.tsx b/src/components/EspFlasher.tsx
index 450734a..16c5830 100644
--- a/src/components/EspFlasher.tsx
+++ b/src/components/EspFlasher.tsx
@@ -318,8 +318,8 @@ export default function EspFlasher({
{!hasFactorySection ? (
- Full device reset is not available for this bundle—only an update. Typical reasons: an older download, a
- build that did not include a factory image, or firmware not produced by this app’s usual ESP32 flow.
+ Full device reset is not available for this bundle—only an update. Typical reasons: an older download, or a
+ build that did not include a factory image.
) : null}
diff --git a/src/lib/nrfFlashRun.ts b/src/lib/nrfFlashRun.ts
index d3b0bba..f1187b2 100644
--- a/src/lib/nrfFlashRun.ts
+++ b/src/lib/nrfFlashRun.ts
@@ -32,6 +32,9 @@ function pickDirectory(): Promise {
* Build an nRF52 flash plan from the bundle tarball + parsed manifest.
* Reads the `update` section for a normal flash, or `factory` when factoryInstall is true.
* Images with role "uf2" are the firmware; role "nuke" is the erase-all UF2.
+ *
+ * Falls back to scanning the bundle directly for *.uf2 when no manifest is present
+ * (covers pre-manifest bundles and builds where emit-flash-manifest.py did not run).
*/
export function buildNrfPlan(
files: Map,
@@ -39,26 +42,46 @@ export function buildNrfPlan(
factoryInstall: boolean
): NrfFlashPlan | null {
const section = factoryInstall ? (manifest?.factory ?? manifest?.update) : manifest?.update
- if (!section?.images?.length) return null
- let firmwareFile: Uint8Array | undefined
- let firmwareName: string | undefined
- let nukeFile: Uint8Array | undefined
+ if (section?.images?.length) {
+ let firmwareFile: Uint8Array | undefined
+ let firmwareName: string | undefined
+ let nukeFile: Uint8Array | undefined
- for (const img of section.images) {
- const role = img.role?.toLowerCase()
- if (role === 'nuke') {
- nukeFile = findInTar(files, img.file)
- } else if (role === 'uf2' || img.file.toLowerCase().endsWith('.uf2')) {
- if (!firmwareFile) {
- firmwareFile = findInTar(files, img.file)
- firmwareName = img.file
+ for (const img of section.images) {
+ const role = img.role?.toLowerCase()
+ if (role === 'nuke') {
+ nukeFile = findInTar(files, img.file)
+ } else if (role === 'uf2' || img.file.toLowerCase().endsWith('.uf2')) {
+ if (!firmwareFile) {
+ firmwareFile = findInTar(files, img.file)
+ firmwareName = img.file
+ }
}
}
+
+ if (firmwareFile && firmwareName) {
+ return { firmwareFile, firmwareName, nukeFile }
+ }
}
- if (!firmwareFile || !firmwareName) return null
- return { firmwareFile, firmwareName, nukeFile }
+ // Fallback: scan bundle files directly for *.uf2 (no manifest or manifest had no UF2 images).
+ // Prefer names starting with "firmware", exclude nuke.uf2.
+ let fallbackName: string | undefined
+ let fallbackData: Uint8Array | undefined
+ for (const [path, data] of files) {
+ const base = path.replace(/^.*\//, '')
+ const baseLower = base.toLowerCase()
+ if (!baseLower.endsWith('.uf2') || baseLower === 'nuke.uf2') continue
+ if (!fallbackName || baseLower.startsWith('firmware')) {
+ fallbackName = base
+ fallbackData = data
+ if (baseLower.startsWith('firmware')) break
+ }
+ }
+
+ if (!fallbackData || !fallbackName) return null
+ return { firmwareFile: fallbackData, firmwareName: fallbackName }
}
/**