feat: factory kit

This commit is contained in:
2026-03-26 12:15:12 +01:00
parent dee754c015
commit 8837c44300
9 changed files with 409 additions and 5 deletions

View File

@@ -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

View File

@@ -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 @@
</section>
{/if}
{#if data.meta.update.esptool}
<FactoryKit meta={data.meta as any} />
{/if}
{#if false && data.meta.update.esptool}
<section>
<h3>Factory Flash (WIP)</h3>

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import ProgressButton from "$lib/ProgressButton.svelte";
import {
assembleFactoryKit,
type VersionMetaWithEsptool,
} from "./factory-kit";
let { meta }: { meta: VersionMetaWithEsptool } = $props();
const osChoices = ["Windows", "Linux", "MacOS"] as const;
let os: (typeof osChoices)[number] = $state(osChoices[0]);
let working = $state(false);
let progress = $state(0);
let error: string | undefined = $state(undefined);
async function download() {
progress = 0;
error = undefined;
working = true;
try {
const result = await assembleFactoryKit(meta, os, (value) => {
progress = value;
});
const url = URL.createObjectURL(result);
const a = document.createElement("a");
a.href = url;
a.download = `factory-kit-${meta.device}-${meta.version}-${os}.zip`;
a.click();
} catch (err) {
error = (err as Error).message;
} finally {
working = false;
}
}
</script>
<section>
<h3><span class="icon">factory</span> Factory Kit</h3>
<div class="os-selection">
{#each osChoices as value}
<label
><input type="radio" name="os" {value} bind:group={os} />{value}</label
>
{/each}
</div>
<ProgressButton onclick={download} {progress} {working} {error}
><span class="icon">download</span> Download</ProgressButton
>
</section>
<style lang="scss">
.os-selection {
display: flex;
gap: 2px;
margin: 16px 0;
}
h3 {
display: flex;
align-items: center;
gap: 8px;
}
</style>

View File

@@ -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<ArrayBuffer>((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<VersionMeta["update"]["esptool"], undefined> };
};
export async function assembleFactoryKit(
meta: VersionMetaWithEsptool,
os: "Windows" | "Linux" | "MacOS",
onProgress: (progress: number) => void,
): Promise<Blob> {
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 <port>\``,
].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();
});
}