diff --git a/.typesafe-i18n.json b/.typesafe-i18n.json index 3db0e7b1..7e08fafe 100644 --- a/.typesafe-i18n.json +++ b/.typesafe-i18n.json @@ -1,5 +1,5 @@ { - "$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json", - "baseLocale": "en", - "adapter": "svelte" -} + "$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json", + "baseLocale": "en", + "adapter": "svelte" +} \ No newline at end of file diff --git a/icons.config.js b/icons.config.js index 877f4746..e5da12ca 100644 --- a/icons.config.js +++ b/icons.config.js @@ -107,6 +107,7 @@ const config = { "delete_sweep", "print", "restore_from_trash", + "factory", "history", "history_toggle_off", "text_to_speech", diff --git a/package.json b/package.json index d0977647..56365559 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "glob": "^11.0.3", "js-yaml": "^4.1.1", "jsdom": "^26.1.0", + "jszip": "^3.10.1", "npm-run-all": "^4.1.5", "prettier": "^3.7.4", "prettier-plugin-css-order": "^2.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d80b8606..e6c80457 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,6 +125,9 @@ importers: jsdom: specifier: ^26.1.0 version: 26.1.0 + jszip: + specifier: ^3.10.1 + version: 3.10.1 npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -2562,7 +2565,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} @@ -2698,6 +2701,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immutable@5.1.1: resolution: {integrity: sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==} @@ -2936,6 +2942,9 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -3016,6 +3025,9 @@ packages: resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} engines: {'0': node >=0.6.0} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + keyv@5.5.5: resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==} @@ -3044,6 +3056,9 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3269,6 +3284,9 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} @@ -3429,6 +3447,9 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -3464,6 +3485,9 @@ packages: resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} engines: {node: '>=4'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@4.0.2: resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} engines: {node: '>= 14.16.0'} @@ -3573,6 +3597,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -3631,6 +3658,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -3789,6 +3819,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-object@3.3.0: resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} engines: {node: '>=4'} @@ -7280,6 +7313,8 @@ snapshots: ignore@7.0.5: {} + immediate@3.0.6: {} + immutable@5.1.1: {} import-fresh@3.3.0: @@ -7510,6 +7545,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -7596,6 +7633,13 @@ snapshots: json-schema: 0.4.0 verror: 1.10.0 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + keyv@5.5.5: dependencies: '@keyv/serialize': 1.1.1 @@ -7614,6 +7658,10 @@ snapshots: leven@3.1.0: {} + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lines-and-columns@1.2.4: {} listr2@3.14.0(enquirer@2.4.1): @@ -7823,6 +7871,8 @@ snapshots: pako@0.2.9: {} + pako@1.0.11: {} + pako@2.1.0: {} parent-module@1.0.1: @@ -7948,6 +7998,8 @@ snapshots: pretty-bytes@6.1.1: {} + process-nextick-args@2.0.1: {} + process@0.11.10: {} proxy-from-env@1.0.0: {} @@ -7981,6 +8033,16 @@ snapshots: normalize-package-data: 2.5.0 path-type: 3.0.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.2 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@4.0.2: {} reflect.getprototypeof@1.0.10: @@ -8127,6 +8189,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-push-apply@1.0.0: @@ -8194,6 +8258,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setimmediate@1.0.5: {} + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -8410,6 +8476,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + stringify-object@3.3.0: dependencies: get-own-enumerable-property-symbols: 3.0.2 diff --git a/src/lib/assets/factory-flash.ps1 b/src/lib/assets/factory-flash.ps1 new file mode 100644 index 00000000..319442b1 --- /dev/null +++ b/src/lib/assets/factory-flash.ps1 @@ -0,0 +1,121 @@ +Add-Type -AssemblyName System.Windows.Forms + +# Function to create a form for COM port selection +function Show-ComPortSelectionForm { + # Create the form + $form = New-Object System.Windows.Forms.Form + $form.Text = "Select a COM Port" + $form.Size = New-Object System.Drawing.Size(400, 400) # Set the size of the form + $form.StartPosition = "CenterScreen" + + # Create a label to display a message + $infoLabel = New-Object System.Windows.Forms.Label + $infoLabel.Location = New-Object System.Drawing.Point(20, 20) + $infoLabel.Size = New-Object System.Drawing.Size(340, 30) + $infoLabel.Text = "Select a COM port." + + # Create a ListBox to hold the COM port items + $comPortListBox = New-Object System.Windows.Forms.ListBox + $comPortListBox.Location = New-Object System.Drawing.Point(20, 60) + $comPortListBox.Size = New-Object System.Drawing.Size(340, 200) + + # Create a label for status output + $statusLabel = New-Object System.Windows.Forms.Label + $statusLabel.Location = New-Object System.Drawing.Point(20, 280) + $statusLabel.Size = New-Object System.Drawing.Size(340, 60) + $statusLabel.Text = "Status: Ready" + $statusLabel.ForeColor = [System.Drawing.Color]::Black + + # Function to populate COM ports + function PopulateComPorts { + # Clear existing items + $comPortListBox.Items.Clear() + + # Get all COM ports with "OK" status + $comPorts = Get-PnpDevice | Where-Object { $_.Class -eq 'Ports' -and $_.Status -eq 'OK' } + + # Add full friendly name to the ListBox + foreach ($comPort in $comPorts) { + $comPortListBox.Items.Add($comPort.FriendlyName) + } + } + + # Populate COM ports initially + PopulateComPorts + + # Event handler for mouse click + $comPortListBox.Add_MouseClick({ + ProcessSelection + }) + + # Event handler for "Enter" key press + $comPortListBox.Add_KeyDown({ + param($sender, $e) + if ($e.KeyCode -eq 'Enter') { + ProcessSelection + } + }) + + # Function to process selection + function ProcessSelection { + if ($comPortListBox.SelectedItem) { + # Get the selected full friendly name + $selectedFriendlyName = $comPortListBox.SelectedItem + + # Extract the COMx part from the friendly name + $selectedFriendlyName -match '(COM\d+)' | Out-Null + $selectedComPort = $Matches[1] + + if ($selectedComPort) { + # Clear the status label + $statusLabel.Text = "Status: Executing..." + $statusLabel.ForeColor = [System.Drawing.Color]::Black + + # Run the batch file with the extracted COM port (e.g., COM3) + $process = Start-Process cmd.exe -ArgumentList "/c .\flash.bat $selectedComPort" -NoNewWindow -PassThru -Wait + $exitCode = $process.ExitCode + + if ($exitCode -eq 0) { + # Successful execution + $statusLabel.Text = "Status: Execution completed successfully." + $statusLabel.ForeColor = [System.Drawing.Color]::Green + } else { + # Error occurred + $statusLabel.Text = "Status: Error occurred during execution." + $statusLabel.ForeColor = [System.Drawing.Color]::Red + } + } + } + } + + # Create a timer for automatic updates + $timer = New-Object System.Windows.Forms.Timer + $timer.Interval = 1000 # Set interval to 1 second + + # Timer Tick event to refresh the COM port items + $timer.Add_Tick({ + PopulateComPorts # Refresh the COM port items + }) + + # Start the timer when the form is shown + $form.Add_Shown({ + $timer.Start() + }) + + # Stop the timer when the form is closed + $form.Add_FormClosing({ + $timer.Stop() + }) + + # Add controls to the form + $form.Controls.Add($infoLabel) + $form.Controls.Add($comPortListBox) # Add the ListBox to the form + $form.Controls.Add($statusLabel) # Add the status label + + # Show the form + $form.ShowDialog() +} + +# Show the COM port selection dialog +Show-ComPortSelectionForm + diff --git a/src/routes/(app)/ccos/[device]/[version]/+page.svelte b/src/routes/(app)/ccos/[device]/[version]/+page.svelte index ba81b1ed..04f84e69 100644 --- a/src/routes/(app)/ccos/[device]/[version]/+page.svelte +++ b/src/routes/(app)/ccos/[device]/[version]/+page.svelte @@ -5,6 +5,7 @@ import { lt as semverLt } from "semver"; import type { LoaderOptions, ESPLoader } from "esptool-js"; import ProgressButton from "$lib/ProgressButton.svelte"; + import FactoryKit from "./FactoryKit.svelte"; let { data } = $props(); @@ -354,6 +355,10 @@ {/if} + {#if data.meta.update.esptool} + + {/if} + {#if false && data.meta.update.esptool}

Factory Flash (WIP)

diff --git a/src/routes/(app)/ccos/[device]/[version]/FactoryKit.svelte b/src/routes/(app)/ccos/[device]/[version]/FactoryKit.svelte new file mode 100644 index 00000000..090c5abd --- /dev/null +++ b/src/routes/(app)/ccos/[device]/[version]/FactoryKit.svelte @@ -0,0 +1,64 @@ + + +
+

factory Factory Kit

+
+ {#each osChoices as value} + + {/each} +
+ download Download +
+ + diff --git a/src/routes/(app)/ccos/[device]/[version]/factory-kit.ts b/src/routes/(app)/ccos/[device]/[version]/factory-kit.ts new file mode 100644 index 00000000..c6a14180 --- /dev/null +++ b/src/routes/(app)/ccos/[device]/[version]/factory-kit.ts @@ -0,0 +1,142 @@ +import type { VersionMeta } from "$lib/meta/types/meta"; + +async function progressFetch( + url: string, + onProgress: (progress: number) => void, +) { + const request = new XMLHttpRequest(); + request.open("GET", url, true); + request.responseType = "arraybuffer"; + request.onprogress = (event) => { + if (event.total > 0) { + onProgress(event.loaded / event.total); + } + }; + const result = new Promise((resolve, reject) => { + request.onload = () => { + onProgress(1); + if (request.status >= 200 && request.status < 300) { + resolve(request.response); + } else { + reject(new Error(`Failed to fetch ${url}: ${request.statusText}`)); + } + }; + }); + request.send(); + return result; +} +export type VersionMetaWithEsptool = VersionMeta & { + update: { esptool: Exclude }; +}; + +export async function assembleFactoryKit( + meta: VersionMetaWithEsptool, + os: "Windows" | "Linux" | "MacOS", + onProgress: (progress: number) => void, +): Promise { + let otherProgress: number[] = new Array( + Object.keys(meta.update.esptool.files).length, + ).fill(0); + let esptoolProgress = 0; + let compressProgress = 0; + function reportProgress() { + const total = + 0.1 * (otherProgress.reduce((a, b) => a + b) / otherProgress.length) + + 0.5 * esptoolProgress + + 0.4 * compressProgress; + onProgress(total); + } + const esptool = + os === "Windows" + ? progressFetch(`/esptool-v5.2.0-windows-amd64.zip`, (progress) => { + esptoolProgress = progress; + reportProgress(); + }) + : undefined; + const files = Object.values(meta.update.esptool.files).map( + (file, i) => + [ + file, + progressFetch(`${meta.path}/${file}`, (progress) => { + otherProgress[i] = progress; + reportProgress(); + }), + ] as const, + ); + console.log(files); + const JSZip = await import("jszip").then((m) => m.default); + const zip = new JSZip(); + const esptoolZipPromise = esptool + ? esptool.then((it) => JSZip.loadAsync(it)) + : undefined; + for (const [file, dataPromise] of files) { + zip.file(file, dataPromise); + } + + const esptoolScript = ".\\esptool\\esptool.exe"; + const port = "%1"; + const ext = os === "Windows" ? "bat" : "sh"; + zip.file( + `flash.${ext}`, + [ + esptoolScript, + "--chip", + meta.update.esptool.chip, + "--port", + port, + "--baud", + meta.update.esptool.baud, + "--before", + meta.update.esptool.before, + "--after", + meta.update.esptool.after, + "write_flash", + "-z", + "--flash_mode", + meta.update.esptool.flash_mode, + "--flash_freq", + meta.update.esptool.flash_freq, + "--flash_size", + meta.update.esptool.flash_size, + ...Object.entries(meta.update.esptool.files).flatMap( + ([address, file]) => [address, file], + ), + ].join(" "), + { unixPermissions: "755" }, + ); + zip.file(`${meta.device}-${meta.version}`, ""); + if (os === "Windows") { + zip.file( + "factory-flash.ps1", + import("$lib/assets/factory-flash.ps1?raw").then((m) => m.default), + ); + } + zip.file( + "README.txt", + [ + `Factory Kit for ${meta.device} ${meta.version}`, + "", + os !== "Windows" + ? "Requires esptool, please download from https://github.com/espressif/esptool/releases" + : "", + os === "Windows" + ? 'Right click factory-flash.ps1 and select "Run with PowerShell' + : `Run flash.sh with \`./flash.sh \``, + ].join("\n"), + ); + if (esptoolZipPromise) { + const esptoolZip = await esptoolZipPromise; + for (const [path, file] of Object.entries(esptoolZip.files)) { + if (!file.dir) { + zip.file( + path.replace(/^esptool[^\/]*\//, "esptool/"), + file.async("arraybuffer"), + ); + } + } + } + return zip.generateAsync({ type: "blob" }, ({ percent }) => { + compressProgress = percent / 100; + reportProgress(); + }); +} diff --git a/static/esptool-v5.2.0-windows-amd64.zip b/static/esptool-v5.2.0-windows-amd64.zip new file mode 100644 index 00000000..be724946 Binary files /dev/null and b/static/esptool-v5.2.0-windows-amd64.zip differ