diff --git a/Provider/Documentation/build_bundle.md b/Provider/Documentation/build_bundle.md new file mode 100644 index 0000000..a699f89 --- /dev/null +++ b/Provider/Documentation/build_bundle.md @@ -0,0 +1,59 @@ +# Universal OTA Bundle + +The Universal OTA Bundle allows you to update both the **Firmware** and the **Frontend** of the Calendink Provider in a single operation. This ensures that your UI and backend logic are always in sync. + +## 1. How it Works + +The bundle is a custom `.bundle` file that contains: +1. A **12-byte header** (Magic `BNDL`, FW size, UI size). +2. The **Firmware binary** (`Provider.bin`). +3. The **Frontend LittleFS binary** (`www_v*.bin`). + +The ESP32 backend streams this file, writing the firmware to the next OTA slot and the frontend to the inactive `www` partition. It only commits the update if both parts are written successfully. + +## 2. Prerequisites + +- You have a working [Frontend Build Environment](build_frontend.md). +- You have the [ESP-IDF SDK](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/index.html) installed for firmware compilation. + +## 3. Creating a Bundle + +To create a new bundle, follow these steps in order: + +### Step A: Build the Frontend +Inside the `frontend/` directory: +```bash +npm run build:esp32 +``` + +### Step B: Build the Firmware +From the project **root** directory: +```bash +idf.py build +``` + +### Step C: Generate the Bundle +Inside the `frontend/` directory: +```bash +npm run ota:bundle +``` +> [!NOTE] +> `npm run ota:bundle` now automatically runs `npm run ota:package` first to ensure the latest Svelte build is turned into a LittleFS image before bundling. + +The output will be saved in `frontend/bundles/` with a name like `universal_v0.1.11.bundle`. + +## 4. Flashing the Bundle + +1. Open the Calendink Provider Dashboard in your browser. +2. Navigate to the **System Updates** section. +3. Click the **Universal Bundle** button. +4. Drag and drop your `.bundle` file into the upload area. +5. Click **Update**. + +The device will reboot once the upload is complete. You can verify the update by checking the version numbers and the UI changes (like the number of rockets in the header!). + +## 5. Troubleshooting + +- **"Invalid bundle magic"**: Ensure you are uploading a `.bundle` file, not a `.bin`. +- **"Firmware part is corrupted"**: The bundle was likely created while the firmware build was incomplete or failed. +- **Old UI appearing**: Ensure you ran `npm run build:esp32` *before* `npm run ota:bundle`. diff --git a/Provider/Documentation/build_frontend.md b/Provider/Documentation/build_frontend.md index b29b728..665c450 100644 --- a/Provider/Documentation/build_frontend.md +++ b/Provider/Documentation/build_frontend.md @@ -80,3 +80,9 @@ Once the backend supports it (Phase 2+), you can update the frontend without usi - Go to the **Frontend Update** section. - Select the `www.bin` file and click **Flash**. - The device will automatically write to the inactive partition and reboot. + +## 6. Universal OTA Bundle + +For a safer and more convenient update experience, you can bundle both the Firmware and Frontend into a single file. + +See the [Universal OTA Bundle Guide](build_bundle.md) for details. diff --git a/Provider/frontend/bundles/universal_v0.1.12.bundle b/Provider/frontend/bundles/universal_v0.1.12.bundle new file mode 100644 index 0000000..6c86e7f Binary files /dev/null and b/Provider/frontend/bundles/universal_v0.1.12.bundle differ diff --git a/Provider/frontend/bundles/universal_v0.1.13.bundle b/Provider/frontend/bundles/universal_v0.1.13.bundle new file mode 100644 index 0000000..0b8e1de Binary files /dev/null and b/Provider/frontend/bundles/universal_v0.1.13.bundle differ diff --git a/Provider/frontend/bundles/universal_v0.1.9.bundle b/Provider/frontend/bundles/universal_v0.1.9.bundle new file mode 100644 index 0000000..4ee4940 Binary files /dev/null and b/Provider/frontend/bundles/universal_v0.1.9.bundle differ diff --git a/Provider/frontend/package.json b/Provider/frontend/package.json index c4265e3..ec29239 100644 --- a/Provider/frontend/package.json +++ b/Provider/frontend/package.json @@ -8,6 +8,7 @@ "build": "vite build", "build:esp32": "vite build && node scripts/gzip.js", "ota:package": "node scripts/package.js", + "ota:bundle": "npm run ota:package && node scripts/bundle.js", "preview": "vite preview" }, "devDependencies": { diff --git a/Provider/frontend/scripts/bundle.js b/Provider/frontend/scripts/bundle.js new file mode 100644 index 0000000..4ca8d20 --- /dev/null +++ b/Provider/frontend/scripts/bundle.js @@ -0,0 +1,79 @@ +/** + * Universal OTA Bundle Creator + * Packs FW (Provider.bin) and WWW (www_v*.bin) into a single .bundle file. + */ + +import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const projectRoot = resolve(__dirname, '..'); +const providerRoot = resolve(projectRoot, '..'); +const binDir = resolve(projectRoot, 'bin'); + +// Paths +const fwFile = resolve(providerRoot, 'build', 'Provider.bin'); +const bundleDir = resolve(projectRoot, 'bundles'); + +if (!existsSync(bundleDir)) { + mkdirSync(bundleDir, { recursive: true }); +} + +console.log('--- Universal Bundle Packaging ---'); + +// 1. Find the latest www.bin with proper semantic version sorting +const binFiles = readdirSync(binDir) + .filter(f => f.startsWith('www_v') && f.endsWith('.bin')) + .sort((a, b) => { + const getParts = (s) => { + const m = s.match(/v(\d+)\.(\d+)\.(\d+)/); + return m ? m.slice(1).map(Number) : [0, 0, 0]; + }; + const [aMajor, aMinor, aRev] = getParts(a); + const [bMajor, bMinor, bRev] = getParts(b); + return (bMajor - aMajor) || (bMinor - aMinor) || (bRev - aRev); + }); + +if (binFiles.length === 0) { + console.error('Error: No www_v*.bin found in frontend/bin/. Run "npm run ota:package" first.'); + process.exit(1); +} + +const wwwFile = resolve(binDir, binFiles[0]); + +if (!existsSync(fwFile)) { + console.error(`Error: Firmware binary not found at ${fwFile}. Run "idf.py build" first.`); + process.exit(1); +} + +try { + console.log(`Packing Firmware: ${fwFile}`); + console.log(`Packing Frontend: ${wwwFile}`); + + const fwBuf = readFileSync(fwFile); + const wwwBuf = readFileSync(wwwFile); + + // Create 12-byte header + // Magic: BNDL (4 bytes) + // FW Size: uint32 (4 bytes) + // WWW Size: uint32 (4 bytes) + const header = Buffer.alloc(12); + header.write('BNDL', 0); + header.writeUInt32LE(fwBuf.length, 4); + header.writeUInt32LE(wwwBuf.length, 8); + + const bundleBuf = Buffer.concat([header, fwBuf, wwwBuf]); + const outputFile = resolve(bundleDir, `universal_v${binFiles[0].replace('www_v', '').replace('.bin', '')}.bundle`); + + writeFileSync(outputFile, bundleBuf); + + console.log('-------------------------------'); + console.log(`Success: Bundle created at ${outputFile}`); + console.log(`Total size: ${(bundleBuf.length / 1024 / 1024).toFixed(2)} MB`); + console.log('-------------------------------'); + +} catch (e) { + console.error('Error creating bundle:', e.message); + process.exit(1); +} diff --git a/Provider/frontend/src/App.svelte b/Provider/frontend/src/App.svelte index 2579f88..6544020 100644 --- a/Provider/frontend/src/App.svelte +++ b/Provider/frontend/src/App.svelte @@ -108,7 +108,7 @@
-

Calendink Provider 🚀🚀🚀

+

Calendink Provider 🚀🚀👑

ESP32-S3 System Dashboard v{__APP_VERSION__}

@@ -197,13 +197,16 @@
{formatBytes(part.size)}
- {#if part.app_version} -
v{part.app_version}
- {:else if part.free !== undefined} -
- {formatBytes(part.free)} free -
- {/if} +
+ {#if part.app_version} + v{part.app_version} + {/if} + {#if part.free !== undefined} + + {formatBytes(part.free)} free + + {/if} +
{/each} diff --git a/Provider/frontend/src/lib/OTAUpdate.svelte b/Provider/frontend/src/lib/OTAUpdate.svelte index f3495b1..1b3aa58 100644 --- a/Provider/frontend/src/lib/OTAUpdate.svelte +++ b/Provider/frontend/src/lib/OTAUpdate.svelte @@ -1,6 +1,6 @@