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:
BIN
Provider/frontend/bundles/universal_v0.1.12.bundle
Normal file
BIN
Provider/frontend/bundles/universal_v0.1.12.bundle
Normal file
Binary file not shown.
BIN
Provider/frontend/bundles/universal_v0.1.13.bundle
Normal file
BIN
Provider/frontend/bundles/universal_v0.1.13.bundle
Normal file
Binary file not shown.
BIN
Provider/frontend/bundles/universal_v0.1.9.bundle
Normal file
BIN
Provider/frontend/bundles/universal_v0.1.9.bundle
Normal file
Binary file not shown.
@@ -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": {
|
||||
|
||||
79
Provider/frontend/scripts/bundle.js
Normal file
79
Provider/frontend/scripts/bundle.js
Normal 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);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"major": 0,
|
||||
"minor": 1,
|
||||
"revision": 12
|
||||
"revision": 14
|
||||
}
|
||||
Reference in New Issue
Block a user