diff options
| -rw-r--r-- | src/routes/(app)/pos/+page.server.ts | 7 | ||||
| -rw-r--r-- | src/routes/(app)/pos/+page.svelte | 216 |
2 files changed, 173 insertions, 50 deletions
diff --git a/src/routes/(app)/pos/+page.server.ts b/src/routes/(app)/pos/+page.server.ts index 59f2d923..1df1bb33 100644 --- a/src/routes/(app)/pos/+page.server.ts +++ b/src/routes/(app)/pos/+page.server.ts @@ -3,7 +3,12 @@ import { auth, fd, get } from "$lib/utils"; import { hex } from "@scure/base"; export async function load({ cookies }) { - const token = await get("/ro", auth(cookies)); + let token; + + try { + token = await get("/ro", auth(cookies)); + } catch (e) {} + return { token }; } diff --git a/src/routes/(app)/pos/+page.svelte b/src/routes/(app)/pos/+page.svelte index be9a9719..6f3c5d24 100644 --- a/src/routes/(app)/pos/+page.svelte +++ b/src/routes/(app)/pos/+page.svelte @@ -3,102 +3,220 @@ import { ESPLoader, Transport } from "esptool-js"; import { hex } from "@scure/base"; + // Convert Uint8Array → JS "binary string" in safe chunks function bs(uint8Array) { let binaryString = ""; - const chunkSize = 0x8000; // 32,768 bytes (safe limit) - + const chunkSize = 0x8000; for (let i = 0; i < uint8Array.length; i += chunkSize) { const chunk = uint8Array.subarray(i, i + chunkSize); binaryString += String.fromCharCode(...chunk); } - return binaryString; } let { data, form } = $props(); let { token } = $state(data); - const address = 0x3d0000; - const baudrate = 921600; + // --- Defaults --- + const LITTLEFS_ADDRESS_DEFAULT = 0x3d0000; // your existing LittleFS offset + const FW_ADDRESS_DEFAULT = 0x10000; // typical app offset on ESP32-C3 + const CONNECT_BAUD = 115200; // initial + const WORK_BAUD = 921600; // fast flashing - let file = $state(); - let ssid = $state(); - let key = $state(); + // --- Shared state --- let esploader = $state(); + let chip, transport; + let connected = $state(false); + let portInfo = $state(""); + + // --- Existing config path (generated server-side to hex in form.bytes) --- let bytes = $derived(form ? hex.decode(form.bytes) : undefined); + let littlefsAddress = $state(LITTLEFS_ADDRESS_DEFAULT); + let configProgress = $state(0); + let configDone = $state(false); + + // --- New firmware flashing path --- + let fwFile = $state(null); + let fwBytes = $state(); // Uint8Array + let fwAddress = $state(FW_ADDRESS_DEFAULT); + let fwEraseAll = $state(false); + let fwProgress = $state(0); + let fwDone = $state(false); + let fwError = $state(""); + + // Simple tab toggle + let tab = $state("config"); // "config" | "firmware" - let chip, transport; let connect = async () => { try { - let device = await navigator.serial.requestPort({}); + const device = await navigator.serial.requestPort({}); transport = new Transport(device, true); - esploader = new ESPLoader({ transport, - baudrate, + baudrate: CONNECT_BAUD, }); chip = await esploader.main(); + + connected = true; + portInfo = (await device.getInfo?.()) ? JSON.stringify(await device.getInfo()) : "Connected"; } catch (e) { console.log(e); + connected = false; } }; - let progress = $state(); - let flash = async () => { - let fileArray = [{ data: bs(bytes), address }]; + let flashConfig = async () => { + if (!esploader || !bytes) return; + configDone = false; + configProgress = 0; - const flashOptions = { + const fileArray = [{ data: bs(bytes), address: Number(littlefsAddress) }]; + + await esploader.writeFlash({ fileArray, flashSize: "keep", eraseAll: false, compress: true, - reportProgress: (fileIndex, written, total) => { - progress = parseInt((written / total) * 100); + reportProgress: (_fileIndex, written, total) => { + configProgress = Math.floor((written / total) * 100); }, + }); + + configDone = true; + }; + + function onPickFw(e) { + fwError = ""; + fwDone = false; + const f = e.target.files?.[0]; + fwFile = f || null; + if (!fwFile) { + fwBytes = undefined; + return; + } + const reader = new FileReader(); + reader.onload = () => { + const buf = new Uint8Array(reader.result); + fwBytes = buf; + }; + reader.onerror = () => { + fwError = "Failed to read file."; + fwBytes = undefined; }; + reader.readAsArrayBuffer(fwFile); + } + + let flashFirmware = async () => { + if (!esploader) { + fwError = "Not connected."; + return; + } + if (!fwBytes?.length) { + fwError = "No firmware selected."; + return; + } + fwError = ""; + fwDone = false; + fwProgress = 0; - await esploader.writeFlash(flashOptions); + try { + const fileArray = [{ data: bs(fwBytes), address: Number(fwAddress) }]; + + await esploader.writeFlash({ + fileArray, + flashSize: "keep", + eraseAll: Boolean(fwEraseAll), + compress: true, + reportProgress: (_fileIndex, written, total) => { + fwProgress = Math.floor((written / total) * 100); + }, + }); + + fwDone = true; + } catch (e) { + console.error(e); + fwError = e?.message || "Flash failed."; + } }; </script> <div class="container px-4 max-w-lg mx-auto space-y-5 mt-20"> <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> - Flash config + ESP32-C3 Flasher </h1> - {#if esploader} - {#if bytes} - {#if progress} - <div class="text-center text-2xl">Done!</div> + {#if !connected} + <div class="text-center"> + <button class="btn" onclick={connect}>Connect</button> + <div class="text-sm mt-2 opacity-70">{portInfo}</div> + </div> +{/if} + + {#if connected} + <!-- Tabs --> + <div class="flex gap-2 justify-center"> + <button class="btn" class:btn-neutral={tab === "config"} onclick={() => (tab = "config")}>Flash config</button> + <button class="btn" class:btn-neutral={tab === "firmware"} onclick={() => (tab = "firmware")}>Flash firmware</button> + </div> + + {#if tab === "config"} + <!-- Existing CONFIG FLOW --> + {#if bytes} + {#if configDone} + <div class="text-center text-2xl">Config written ✔</div> + {:else} + <div>Generated config file (<b>{bytes.length} bytes</b>)</div> + <label class="label mt-2">LittleFS address (hex)</label> + <input class="input" bind:value={littlefsAddress} onchange={(e) => (littlefsAddress = Number(e.target.value))} /> + <div class="mt-3"> + <button class="btn" onclick={flashConfig}>Flash config</button> + </div> + {#if configProgress > 0}<div class="mt-2">Progress: {configProgress}%</div>{/if} + {/if} {:else} - <div>Generated config file (<b>{bytes.length * 2} bytes</b>)</div> - <button class="btn" onclick={flash}>Flash</button> + <form method="POST" use:enhance class="space-y-2"> + <input name="ssid" placeholder="Wifi SSID" class="input" /> + <input name="key" placeholder="Wifi password" class="input" /> + <input name="token" bind:value={token} placeholder="Coinos API token" class="input" /> + <button type="submit" class="btn">Generate Config</button> + </form> {/if} {:else} - <form method="POST" use:enhance class="space-y-2"> - <input - name="ssid" - bind:value={ssid} - placeholder="Wifi SSID" - class="input" - /> - <input - name="key" - bind:value={key} - placeholder="Wifi password" - class="input" - /> - <input - name="token" - bind:value={token} - placeholder="Coinos API token" - class="input" - /> - <button type="submit" class="btn">Generate Config</button> - </form> + <!-- NEW FIRMWARE FLOW --> + <div class="space-y-3"> + <label class="label">Select .ino.bin (firmware)</label> + <input type="file" accept=".bin,application/octet-stream" class="input" onchange={onPickFw} /> + + <label class="label">Firmware address (hex, default 0x10000)</label> + <input class="input" bind:value={fwAddress} onchange={(e) => (fwAddress = Number(e.target.value))} /> + + <label class="flex items-center gap-2 mt-2"> + <input type="checkbox" bind:checked={fwEraseAll} /> + <span>Erase all flash (usually NOT needed)</span> + </label> + + <div class="mt-3"> + <button class="btn" disabled={!fwBytes} onclick={flashFirmware}>Flash firmware</button> + </div> + + {#if fwProgress > 0 && !fwDone} + <div>Progress: {fwProgress}%</div> + {/if} + {#if fwDone} + <div class="text-center text-2xl">Firmware written ✔</div> + {/if} + {#if fwError} + <div class="text-red-600">{fwError}</div> + {/if} + </div> {/if} - {:else} - <button class="btn" onclick={connect}>Connect</button> {/if} </div> + +<style> + .btn { @apply px-4 py-2 rounded-2xl shadow; } + .btn-neutral { @apply bg-gray-200; } + .input { @apply w-full px-3 py-2 border rounded-xl; } + .label { @apply text-sm opacity-70; } +</style> |
