From 588719df91acd7d6f3d5fa619911a908eb2864dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Sat, 23 Nov 2024 19:02:35 +0100 Subject: [PATCH] feat: support factory flashing --- icons.config.js | 3 + package.json | 1 + pnpm-lock.yaml | 22 +++ .../ccos/[device]/[version]/+page.svelte | 161 +++++++++++++----- .../(app)/ccos/[device]/[version]/+page.ts | 45 ++++- .../(app)/ccos/[device]/[version]/meta.ts | 26 +++ 6 files changed, 207 insertions(+), 51 deletions(-) create mode 100644 src/routes/(app)/ccos/[device]/[version]/meta.ts diff --git a/icons.config.js b/icons.config.js index 14b62b14..831cd7c4 100644 --- a/icons.config.js +++ b/icons.config.js @@ -110,6 +110,9 @@ const config = { "experiment", "code", "dictionary", + "developer_board", + "developer_board_off", + "memory", ], codePoints: { speed: "e9e4", diff --git a/package.json b/package.json index b05a893a..0582e092 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "codemirror": "^6.0.1", "cypress": "^13.13.2", "d3": "^7.9.0", + "esptool-js": "^0.4.7", "flexsearch": "^0.7.43", "fontkit": "^2.0.4", "glob": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd52f44f..228444fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: d3: specifier: ^7.9.0 version: 7.9.0 + esptool-js: + specifier: ^0.4.7 + version: 0.4.7 flexsearch: specifier: ^0.7.43 version: 0.7.43 @@ -1648,6 +1651,9 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} + atob-lite@2.0.0: + resolution: {integrity: sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==} + autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -2253,6 +2259,9 @@ packages: esm-env@1.0.0: resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==} + esptool-js@0.4.7: + resolution: {integrity: sha512-xVwtSVDRsvjXSEvNFrorgJfB71RFFkZkL+hs7O7gW5hgPrKGywZxo2U5LJddzkJ6eE31QinNVyywc0OaSntZCw==} + esrap@1.2.2: resolution: {integrity: sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==} @@ -3087,6 +3096,9 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -5802,6 +5814,8 @@ snapshots: at-least-node@1.0.0: {} + atob-lite@2.0.0: {} + autoprefixer@10.4.20(postcss@8.4.39): dependencies: browserslist: 4.24.2 @@ -6525,6 +6539,12 @@ snapshots: esm-env@1.0.0: {} + esptool-js@0.4.7: + dependencies: + atob-lite: 2.0.0 + pako: 2.1.0 + tslib: 2.6.3 + esrap@1.2.2: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -7342,6 +7362,8 @@ snapshots: pako@0.2.9: {} + pako@2.1.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 diff --git a/src/routes/(app)/ccos/[device]/[version]/+page.svelte b/src/routes/(app)/ccos/[device]/[version]/+page.svelte index b64c2a4b..7182c2c4 100644 --- a/src/routes/(app)/ccos/[device]/[version]/+page.svelte +++ b/src/routes/(app)/ccos/[device]/[version]/+page.svelte @@ -2,6 +2,7 @@ import { downloadBackup } from "$lib/backup/backup"; import { initSerial, serialPort } from "$lib/serial/connection"; import { fade, slide } from "svelte/transition"; + import type { LoaderOptions, ESPLoader } from "esptool-js"; let { data } = $props(); @@ -9,7 +10,12 @@ let success = $state(false); let error = $state(undefined); + let terminalOutput = $state(""); + let step = $state(0); + let eraseAll = $state(false); + + let espLoader; async function update() { working = true; @@ -18,7 +24,9 @@ const port = $serialPort!; $serialPort = undefined; try { - const file = await fetch(otaUrl!).then((it) => it.blob()); + const file = await fetch( + `${data.meta.path}/${data.meta.update.ota!}`, + ).then((it) => it.blob()); await port.updateFirmware(file); @@ -36,18 +44,7 @@ : undefined, ); let isCorrectDevice = $derived( - currentDevice ? currentDevice === data.device : undefined, - ); - - let uf2Url = $derived( - data.uf2 - ? `${import.meta.env.VITE_FIRMWARE_URL}${data.device}/${data.version}/${data.uf2.name}` - : undefined, - ); - let otaUrl = $derived( - data.ota - ? `${import.meta.env.VITE_FIRMWARE_URL}${data.device}/${data.version}/${data.ota.name}` - : undefined, + currentDevice ? currentDevice === data.meta.target : undefined, ); /** @@ -84,10 +81,12 @@ } async function getFileSystem() { - if (!uf2Url) return; - const uf2Promise = fetch(uf2Url).then((it) => it.blob()); + if (!data.meta.update.uf2) return; + const uf2Promise = fetch( + `${data.meta.path}/${data.meta.update.uf2.name}`, + ).then((it) => it.blob()); const handle = await window.showSaveFilePicker({ - id: `${data.device}-update`, + id: `${data.meta.target}-update`, suggestedName: "CURRENT.UF2", excludeAcceptAllOption: true, types: [ @@ -102,21 +101,82 @@ await uf2.stream().pipeTo(writable); step = 4; } + + async function espBootloader() { + $serialPort?.forget(); + const port = await navigator.serial.requestPort(); + port.open({ baudRate: 1200 }); + } + + async function connectEsp(port: SerialPort): Promise { + const esptool = data.meta.update.esptool!; + const { Transport, ESPLoader } = await import("esptool-js"); + const espLoader = new ESPLoader({ + transport: new Transport(port), + baudrate: Number(esptool.baud), + romBaudrate: Number(esptool.baud), + terminal: { + clean: () => { + terminalOutput = ""; + }, + writeLine: (data) => { + terminalOutput += data + "\n"; + }, + write: (data) => { + terminalOutput += data; + }, + }, + } satisfies LoaderOptions); + await espLoader.connect(esptool.before); + await espLoader.runStub(); + + return espLoader; + } + + async function flashImages() { + const port = await navigator.serial.requestPort(); + try { + const esptool = data.meta.update.esptool!; + espLoader = await connectEsp(port); + const fileArray = await Promise.all( + Object.entries(esptool.files).map(([offset, name]) => + fetch(`${data.meta.path}/${name}`) + .then((it) => it.blob()) + .then((it) => it.text()) + .then((it) => ({ + address: Number(offset), + data: it, + })), + ), + ); + + await espLoader.writeFlash({ + flashSize: esptool.flash_size, + flashMode: esptool.flash_mode, + flashFreq: esptool.flash_freq, + compress: true, + eraseAll, + fileArray, + }); + } finally { + port.close(); + } + }

CCOS / {data.device}{data.meta.target} - / {data.version} + / {data.meta.version}

- {#if data.ota && !data.device.endsWith("m0")} + {#if data.meta.update.ota && !data.meta.target.endsWith("m0")} {@const buttonError = error || (!success && isCorrectDevice === false)}
{:else if $serialPort && isCorrectDevice === false}
@@ -158,27 +218,6 @@

Manual Update

{/if} - - {#if isCorrectDevice === false}
These files are incompatible with your device @@ -186,7 +225,6 @@ {/if}
-

UF2 Instructions

+ + {#if data.meta.update.esptool} +
+

Factory Flash

+

+ If everything else fails, you can go through the same process that is + being used in the factory. +

+

+ This will temporarily brick your device if the process is not done + completely or incorrectly. +

+ +
+ + + +
+ +
{terminalOutput}
+
+ {/if}
diff --git a/src/routes/(app)/ccos/[device]/[version]/+page.ts b/src/routes/(app)/ccos/[device]/[version]/+page.ts index 7c5075de..a41bd7e9 100644 --- a/src/routes/(app)/ccos/[device]/[version]/+page.ts +++ b/src/routes/(app)/ccos/[device]/[version]/+page.ts @@ -1,20 +1,49 @@ import type { PageLoad } from "./$types"; import type { FileListing, Listing } from "../../listing"; +import type { VersionMeta } from "./meta"; export const load = (async ({ fetch, params }) => { const result = await fetch( `${import.meta.env.VITE_FIRMWARE_URL}/${params.device}/${params.version}/`, ); const data: Listing[] = await result.json(); + const meta: VersionMeta | undefined = data.some( + (entry) => entry.type === "file" && entry.name === "meta.json", + ) + ? await fetch( + `${import.meta.env.VITE_FIRMWARE_URL}/${params.device}/${params.version}/meta.json`, + ).then((res) => res.json()) + : undefined; return { - uf2: data.find( - (entry) => entry.type === "file" && entry.name === "CURRENT.UF2", - ) as FileListing, - ota: data.find( - (entry) => entry.type === "file" && entry.name === "firmware.bin", - ), - version: params.version, - device: params.device, + meta: { + version: meta?.version ?? params.version, + target: meta?.target ?? params.device, + path: `${import.meta.env.VITE_FIRMWARE_URL}${params.device}/${params.version}`, + git_commit: meta?.git_commit ?? "", + git_is_dirty: meta?.git_is_dirty ?? false, + git_date: meta?.git_date ?? data[0]?.mtime ?? "", + public_build: meta?.public_build ?? !params.version.startsWith("."), + development_mode: meta?.development_mode ?? 0, + update: { + uf2: + (data.find( + (entry) => + entry.type === "file" && + entry.name === (meta?.update?.uf2 ?? "CURRENT.UF2"), + ) as FileListing) ?? undefined, + ota: + data.find( + (entry) => + entry.type === "file" && + entry.name === (meta?.update?.ota ?? "firmware.bin"), + ) ?? undefined, + esptool: meta?.update?.esptool ?? undefined, + }, + files: data.filter( + (entry) => + entry.type === "file" && (!meta?.files || entry.name in meta.files), + ) as FileListing[], + }, }; }) satisfies PageLoad; diff --git a/src/routes/(app)/ccos/[device]/[version]/meta.ts b/src/routes/(app)/ccos/[device]/[version]/meta.ts new file mode 100644 index 00000000..f1f062e3 --- /dev/null +++ b/src/routes/(app)/ccos/[device]/[version]/meta.ts @@ -0,0 +1,26 @@ +export interface VersionMeta { + version: string; + target: string; + git_commit: string; + git_is_dirty: boolean; + git_date: string; + public_build: boolean; + development_mode: number; + update: { + ota: string | null; + uf2: string | null; + esptool: EspToolData | null; + }; + files: string[]; +} + +export interface EspToolData { + chip: string; + baud: string; + before: string; + after: string; + flash_mode: string; + flash_freq: string; + flash_size: string; + files: Record; +}