Made everything needed to update firmware. Added the bundle to upload both front and backendin a bundle. Added magic number for more safety

This commit is contained in:
2026-03-03 22:45:41 -05:00
parent fdb13d62d4
commit 85dc698a8d
17 changed files with 467 additions and 44 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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": {

View File

@@ -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);
}

View File

@@ -108,7 +108,7 @@
<div class="w-full max-w-6xl space-y-8">
<!-- Header -->
<div class="text-center">
<h1 class="text-2xl font-bold text-accent">Calendink Provider 🚀🚀🚀</h1>
<h1 class="text-2xl font-bold text-accent">Calendink Provider 🚀🚀👑</h1>
<p class="text-text-secondary text-sm">ESP32-S3 System Dashboard v{__APP_VERSION__}</p>
<!-- Status Badge -->
@@ -197,13 +197,16 @@
</div>
<div class="text-right flex flex-col items-end">
<div class="text-[11px] font-mono text-text-primary">{formatBytes(part.size)}</div>
{#if part.app_version}
<div class="text-[9px] text-accent font-bold">v{part.app_version}</div>
{:else if part.free !== undefined}
<div class="text-[9px] {part.free > 0 ? 'text-success' : 'text-text-secondary'} font-bold">
{formatBytes(part.free)} free
</div>
{/if}
<div class="flex items-center gap-1.5 mt-0.5">
{#if part.app_version}
<span class="text-[9px] text-accent font-bold">v{part.app_version}</span>
{/if}
{#if part.free !== undefined}
<span class="text-[9px] {part.free > 1024 ? 'text-success' : 'text-text-secondary'} font-bold">
{formatBytes(part.free)} free
</span>
{/if}
</div>
</div>
</div>
{/each}

View File

@@ -1,6 +1,6 @@
<script>
let { onReboot = null } = $props();
import { getOTAStatus, uploadOTAFrontend, uploadOTAFirmware, getSystemInfo } from "./api.js";
import { getOTAStatus, uploadOTAFrontend, uploadOTAFirmware, uploadOTABundle, getSystemInfo } from "./api.js";
const IS_DEV = import.meta.env.DEV;
@@ -23,7 +23,7 @@
let selectedFile = $state(null);
let showAdvanced = $state(false);
/** @type {'frontend' | 'firmware'} */
/** @type {'frontend' | 'firmware' | 'bundle'} */
let updateMode = $state("frontend");
let isDragging = $state(false);
@@ -55,7 +55,15 @@
}
function processFile(file) {
if (file.name.endsWith('.bin')) {
if (updateMode === 'bundle') {
if (file.name.endsWith('.bundle')) {
selectedFile = file;
errorMsg = "";
} else {
selectedFile = null;
errorMsg = "Please select a valid .bundle file";
}
} else if (file.name.endsWith('.bin')) {
selectedFile = file;
errorMsg = "";
} else {
@@ -78,8 +86,10 @@
try {
if (updateMode === "frontend") {
await uploadOTAFrontend(selectedFile);
} else {
} else if (updateMode === "firmware") {
await uploadOTAFirmware(selectedFile);
} else {
await uploadOTABundle(selectedFile);
}
clearInterval(progressInterval);
uploadProgress = 100;
@@ -105,6 +115,7 @@
}
const currentTarget = $derived(() => {
if (updateMode === 'bundle') return 'FW + UI';
if (updateMode === 'frontend') return otaInfo.target_partition;
// For firmware, target is the slot that is NOT the running one
const runningLabel = otaInfo.running_firmware_label || 'ota_0';
@@ -131,7 +142,14 @@
class="text-[10px] font-bold uppercase tracking-tight px-2 py-1 rounded transition-colors
{showAdvanced && updateMode === 'firmware' ? 'bg-accent text-white' : 'bg-border text-text-secondary hover:text-text-primary'}"
>
Firmware OTA
Firmware
</button>
<button
onclick={() => toggleMode('bundle')}
class="text-[10px] font-bold uppercase tracking-tight px-2 py-1 rounded transition-colors
{showAdvanced && updateMode === 'bundle' ? 'bg-accent text-white' : 'bg-border text-text-secondary hover:text-text-primary'}"
>
Universal Bundle
</button>
</div>
</div>
@@ -190,13 +208,13 @@
ondragleave={() => isDragging = false}
ondrop={handleDrop}
>
<input type="file" accept=".bin" onchange={handleFileChange} class="absolute inset-0 opacity-0 cursor-pointer" />
<input type="file" accept="{updateMode === 'bundle' ? '.bundle' : '.bin'}" onchange={handleFileChange} class="absolute inset-0 opacity-0 cursor-pointer" />
<div class="text-2xl">{updateMode === 'frontend' ? '🎨' : '⚙️'}</div>
{#if selectedFile}
<div class="text-xs font-medium text-text-primary">{selectedFile.name}</div>
<div class="text-[10px] text-text-secondary">{(selectedFile.size / 1024).toFixed(1)} KB</div>
{:else}
<div class="text-xs text-text-primary">Drop {updateMode} .bin here</div>
<div class="text-xs text-text-primary">Drop {updateMode === 'bundle' ? 'Universal .bundle' : updateMode === 'frontend' ? 'UI .bin' : 'Firmware .bin'} here</div>
<div class="text-[10px] text-text-secondary">or click to browse</div>
{/if}
</div>
@@ -207,7 +225,7 @@
disabled={status === "uploading"}
class="w-full py-2 text-xs font-bold rounded-lg transition-colors bg-accent text-white hover:brightness-110 disabled:opacity-40"
>
{status === "uploading" ? 'Flashing...' : `Update ${updateMode === 'frontend' ? 'UI' : 'Firmware'}`}
{status === "uploading" ? 'Flashing...' : `Update ${updateMode === 'bundle' ? 'Everything' : updateMode === 'frontend' ? 'UI' : 'Firmware'}`}
</button>
{/if}

View File

@@ -94,3 +94,25 @@ export async function uploadOTAFirmware(file) {
return res.json();
}
/**
* Upload a universal .bundle image (FW + WWW).
* @param {File} file The bundle binary file to upload.
* @returns {Promise<{status: string, message: string}>}
*/
export async function uploadOTABundle(file) {
const res = await fetch(`${API_BASE}/api/ota/bundle`, {
method: 'POST',
body: file,
headers: {
'Content-Type': 'application/octet-stream'
}
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Upload failed (${res.status}): ${errorText || res.statusText}`);
}
return res.json();
}

View File

@@ -1,5 +1,5 @@
{
"major": 0,
"minor": 1,
"revision": 12
"revision": 14
}