diff --git a/Provider/Documentation/build_frontend.md b/Provider/Documentation/build_frontend.md index 33ab010..b29b728 100644 --- a/Provider/Documentation/build_frontend.md +++ b/Provider/Documentation/build_frontend.md @@ -69,7 +69,7 @@ Once the backend supports it (Phase 2+), you can update the frontend without usi 1. **Build the assets**: `npm run build:esp32` 2. **Package the image**: `npm run ota:package` - - This generates a `frontend/bin/www.bin` file. + - This generates a versioned binary in `frontend/bin/` (e.g., `www_v1.0.5.bin`). - **Configuration**: If the tool is not in your PATH, add its path to `frontend/.env`: ```env MKLITTLEFS_PATH=C:\path\to\mklittlefs.exe diff --git a/Provider/frontend/scripts/package.js b/Provider/frontend/scripts/package.js index 6024c88..48ed395 100644 --- a/Provider/frontend/scripts/package.js +++ b/Provider/frontend/scripts/package.js @@ -6,13 +6,13 @@ import { execSync } from 'child_process'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; -import { existsSync, readFileSync, mkdirSync } from 'fs'; +import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const projectRoot = resolve(__dirname, '..'); const distDir = resolve(projectRoot, 'dist'); const binDir = resolve(projectRoot, 'bin'); -const outputFile = resolve(binDir, 'www.bin'); +const versionFile = resolve(projectRoot, 'version.json'); // Ensure bin directory exists if (!existsSync(binDir)) { @@ -26,6 +26,33 @@ const PAGE_SIZE = 256; console.log('--- OTA Packaging ---'); +/** + * Handle versioning: Read current version + */ +function getVersion() { + if (existsSync(versionFile)) { + try { + return JSON.parse(readFileSync(versionFile, 'utf8')); + } catch (e) { + console.warn('Warning: Could not read version.json:', e.message); + } + } + return { major: 0, minor: 0, revision: 0 }; +} + +/** + * Increment and save revision + */ +function incrementVersion(version) { + try { + version.revision = (version.revision || 0) + 1; + writeFileSync(versionFile, JSON.stringify(version, null, 2)); + console.log(`Version incremented to: ${version.major}.${version.minor}.${version.revision} for next build.`); + } catch (e) { + console.warn('Warning: Could not update version.json:', e.message); + } +} + /** * Simple .env parser to load MKLITTLEFS_PATH without external dependencies */ @@ -51,6 +78,9 @@ function loadEnv() { } loadEnv(); +const version = getVersion(); +const versionStr = `${version.major}.${version.minor}.${version.revision}`; +const outputFile = resolve(binDir, `www_v${versionStr}.bin`); if (!existsSync(distDir)) { console.error('Error: dist/ directory not found. Run "npm run build:esp32" first.'); @@ -107,6 +137,9 @@ try { console.log(`Running: ${cmd}`); execSync(cmd, { stdio: 'inherit' }); console.log('Success: www.bin created.'); + + // Auto-increment for the next build + incrementVersion(version); } catch (e) { console.error('Error during packaging:', e.message); process.exit(1); diff --git a/Provider/frontend/src/App.svelte b/Provider/frontend/src/App.svelte index d807e55..7ba8160 100644 --- a/Provider/frontend/src/App.svelte +++ b/Provider/frontend/src/App.svelte @@ -57,12 +57,8 @@ } } - // Poll every 5 seconds - let pollInterval; $effect(() => { fetchInfo(); - pollInterval = setInterval(fetchInfo, 5000); - return () => clearInterval(pollInterval); }); const infoItems = $derived([ @@ -78,8 +74,8 @@
-

Calendink Provider

-

ESP32-S3 System Dashboard

+

Calendink Provider 🚀

+

ESP32-S3 System Dashboard v{__APP_VERSION__}

@@ -147,11 +143,11 @@
- +
-
+
-

+

Device Control

@@ -169,11 +165,11 @@ Reboot

- - -
+ + + {#if showRebootConfirm}
{/if} - -

- Auto-refreshes every 5s -

diff --git a/Provider/frontend/src/lib/OTAUpdate.svelte b/Provider/frontend/src/lib/OTAUpdate.svelte index de8e932..2d47572 100644 --- a/Provider/frontend/src/lib/OTAUpdate.svelte +++ b/Provider/frontend/src/lib/OTAUpdate.svelte @@ -15,6 +15,8 @@ }); let selectedFile = $state(null); + let showAdvanced = $state(false); + let isDragging = $state(false); async function fetchStatus() { status = "loading_status"; @@ -35,10 +37,26 @@ function handleFileChange(event) { const files = event.target.files; if (files && files.length > 0) { - selectedFile = files[0]; + processFile(files[0]); + } + } + + function handleDrop(event) { + event.preventDefault(); + isDragging = false; + const files = event.dataTransfer.files; + if (files && files.length > 0) { + processFile(files[0]); + } + } + + function processFile(file) { + if (file.name.endsWith('.bin')) { + selectedFile = file; errorMsg = ""; } else { selectedFile = null; + errorMsg = "Please select a valid .bin file"; } } @@ -47,19 +65,17 @@ status = "uploading"; errorMsg = ""; - uploadProgress = 0; // The fetch API doesn't natively support upload progress well without XMLHttpRequest, so we emulate it visually. + uploadProgress = 0; - // Emulate progress since doing it cleanly with fetch requires more boilerplate. const progressInterval = setInterval(() => { if (uploadProgress < 90) uploadProgress += 5; }, 500); try { - const result = await uploadOTAFrontend(selectedFile); + await uploadOTAFrontend(selectedFile); clearInterval(progressInterval); uploadProgress = 100; status = "success"; - // The backend triggers a reboot after success. The main App.svelte polling will handle the reconnect. } catch (e) { clearInterval(progressInterval); uploadProgress = 0; @@ -70,85 +86,116 @@ {#if !IS_DEV} -
-
-

- Frontend Update (OTA) +
+
+

+ Frontend Info

-

- Update the dashboard UI without flashing the entire firmware. -

+
- - {#if status === "loading_status"} -
Loading status...
- {:else} - -
-
- Active Partition: - {otaInfo.active_partition} -
-
- Next Target: - {otaInfo.target_partition} -
-
- - -
- {#if status === "success"} -
- - Update successful! The device is rebooting... -
- {:else} -
- - -
- - - {#if status === "uploading"} -
-
+ +
+ +
+
+
Version
+
v{__APP_VERSION__}
+
+
+
Active Slot
+
+ {otaInfo.active_partition} + {#if otaInfo.partitions} + + ({(otaInfo.partitions.find(p => p.label === otaInfo.active_partition)?.free / 1024 / 1024).toFixed(2)} MB free) + + {/if} +
+
+
+ + {#if showAdvanced} +
+
+

OTA Upgrade

+
+ Target: {otaInfo.target_partition} + {#if otaInfo.partitions} + + ({(otaInfo.partitions.find(p => p.label === otaInfo.target_partition)?.size / 1024 / 1024).toFixed(2)} MB capacity) + + {/if} +
- {:else if status === "error"} -

- {errorMsg} -

- {/if} - {/if} -
- {/if} + + {#if status === "success"} +
+ + Update successful! The device is rebooting... +
+ {:else} + +
{ e.preventDefault(); isDragging = true; }} + ondragleave={() => isDragging = false} + ondrop={handleDrop} + > + + +
📦
+ {#if selectedFile} +
{selectedFile.name}
+
{(selectedFile.size / 1024).toFixed(1)} KB
+ {:else} +
Drag & Drop .bin here
+
or click to browse
+ {/if} +
+ + {#if selectedFile} + + {/if} + + {#if status === "uploading"} +
+
+
+ {:else if status === "error"} +

+ {errorMsg} +

+ {/if} + {/if} +
+ {/if} +
{/if} diff --git a/Provider/frontend/version.json b/Provider/frontend/version.json new file mode 100644 index 0000000..daebab9 --- /dev/null +++ b/Provider/frontend/version.json @@ -0,0 +1,5 @@ +{ + "major": 0, + "minor": 1, + "revision": 4 +} \ No newline at end of file diff --git a/Provider/frontend/vite.config.js b/Provider/frontend/vite.config.js index b53b297..a46b566 100644 --- a/Provider/frontend/vite.config.js +++ b/Provider/frontend/vite.config.js @@ -2,9 +2,17 @@ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' import tailwindcss from '@tailwindcss/vite' import { viteSingleFile } from 'vite-plugin-singlefile' +import { readFileSync } from 'fs' +import { resolve } from 'path' + +const version = JSON.parse(readFileSync(resolve(__dirname, 'version.json'), 'utf8')); +const versionString = `${version.major}.${version.minor}.${version.revision}`; // https://vite.dev/config/ export default defineConfig({ + define: { + __APP_VERSION__: JSON.stringify(versionString), + }, plugins: [ svelte(), tailwindcss(), diff --git a/Provider/main/api/ota/status.cpp b/Provider/main/api/ota/status.cpp index 8d03118..8da18b0 100644 --- a/Provider/main/api/ota/status.cpp +++ b/Provider/main/api/ota/status.cpp @@ -1,6 +1,8 @@ // SDK #include "cJSON.h" #include "esp_http_server.h" +#include "esp_littlefs.h" +#include "esp_partition.h" // Project #include "appstate.hpp" @@ -14,6 +16,37 @@ internal esp_err_t api_ota_status_handler(httpd_req_t *req) cJSON *root = cJSON_CreateObject(); cJSON_AddNumberToObject(root, "active_slot", g_Active_WWW_Partition); + + const char *partitions[] = {"www_0", "www_1"}; + cJSON *parts_arr = cJSON_AddArrayToObject(root, "partitions"); + + for (int i = 0; i < 2; i++) + { + cJSON *p_obj = cJSON_CreateObject(); + cJSON_AddStringToObject(p_obj, "label", partitions[i]); + + const esp_partition_t *p = esp_partition_find_first( + ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, partitions[i]); + if (p) + { + cJSON_AddNumberToObject(p_obj, "size", p->size); + + size_t total = 0, used = 0; + if (esp_littlefs_info(partitions[i], &total, &used) == ESP_OK) + { + cJSON_AddNumberToObject(p_obj, "used", used); + cJSON_AddNumberToObject(p_obj, "free", total - used); + } + else + { + // Not mounted or not LFS + cJSON_AddNumberToObject(p_obj, "used", 0); + cJSON_AddNumberToObject(p_obj, "free", p->size); + } + } + cJSON_AddItemToArray(parts_arr, p_obj); + } + cJSON_AddStringToObject(root, "active_partition", g_Active_WWW_Partition == 0 ? "www_0" : "www_1"); cJSON_AddStringToObject(root, "target_partition",