diff --git a/README.md b/README.md index e20a3134..2bed670a 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,10 @@ bun dev Open [http://localhost:5173](http://localhost:5173) with your browser to see the result. You can start editing the page by modifying `src/app/index.jsx`. The page auto-updates as you edit the file. + + +**Helpful for debugging** +* [chrome://usb-internals/](chrome://usb-internals/) +* [chrome://device-log/](chrome://device-log/) +* Add `?fast=1` to the URL to skip flashing the system partition (the slowest). Useful for testing the full flow quickly. +* Add `?windows=1` to the URL to force Windows mode (shows Zadig driver instructions). Useful for testing the Windows flow on other platforms. \ No newline at end of file diff --git a/deploy-preview.sh b/deploy-preview.sh new file mode 100755 index 00000000..fc035f67 --- /dev/null +++ b/deploy-preview.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +BRANCH="${1:-$(git branch --show-current)}" + +bun run build +bunx wrangler pages deploy dist --project-name=connect --branch="$BRANCH" + +echo "" +echo "Preview URL: https://${BRANCH}.connect-d5y.pages.dev" diff --git a/src/app.html b/src/app.html index c6072191..6bc08c2d 100644 --- a/src/app.html +++ b/src/app.html @@ -6,7 +6,7 @@ flash.comma.ai %sveltekit.head% diff --git a/src/lib/components/CopyText.svelte b/src/lib/components/CopyText.svelte deleted file mode 100644 index 8c54b919..00000000 --- a/src/lib/components/CopyText.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
-
{text}
- -
diff --git a/src/lib/components/DeviceState.svelte b/src/lib/components/DeviceState.svelte deleted file mode 100644 index baf20b51..00000000 --- a/src/lib/components/DeviceState.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - -
-
- - - - Device connected -
- | -
- - Serial: - {serial || "unknown"} - -
-
diff --git a/src/lib/components/Flash.svelte b/src/lib/components/Flash.svelte deleted file mode 100644 index 28af1bfd..00000000 --- a/src/lib/components/Flash.svelte +++ /dev/null @@ -1,253 +0,0 @@ - - -
- -
- -
- {title} - {description} - {#if error} - - {/if} - - {#if connected} - - {/if} -
diff --git a/src/lib/config.js b/src/lib/config.js index 7c0f49e5..fa2bad87 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -3,7 +3,6 @@ const config = { release_mici: 'https://raw.githubusercontent.com/commaai/openpilot/ccf7361798a2b7ff36f5065dffb602eb40c22302/system/hardware/tici/all-partitions.json', release_tizi: 'https://raw.githubusercontent.com/commaai/openpilot/927548621be1be0c2c9063868b93d1f5020904de/system/hardware/tici/all-partitions.json', release_tici: 'https://raw.githubusercontent.com/commaai/openpilot/927548621be1be0c2c9063868b93d1f5020904de/system/hardware/tici/all-partitions.json', - master: 'https://raw.githubusercontent.com/commaai/openpilot/master/system/hardware/tici/all-partitions.json', }, loader: { url: 'https://raw.githubusercontent.com/commaai/flash/master/src/QDL/programmer.bin', diff --git a/src/lib/icons/CommaIcon.svelte b/src/lib/icons/CommaIcon.svelte new file mode 100644 index 00000000..d81ae20b --- /dev/null +++ b/src/lib/icons/CommaIcon.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/src/lib/icons/DiscordIcon.svelte b/src/lib/icons/DiscordIcon.svelte new file mode 100644 index 00000000..8133457d --- /dev/null +++ b/src/lib/icons/DiscordIcon.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/src/lib/icons/GitHubIcon.svelte b/src/lib/icons/GitHubIcon.svelte new file mode 100644 index 00000000..befcc868 --- /dev/null +++ b/src/lib/icons/GitHubIcon.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/src/lib/images/comma3X.webp b/src/lib/images/comma3X.webp new file mode 100644 index 00000000..fc0424fc Binary files /dev/null and b/src/lib/images/comma3X.webp differ diff --git a/src/lib/images/four_screen_on.webp b/src/lib/images/four_screen_on.webp new file mode 100644 index 00000000..674add95 Binary files /dev/null and b/src/lib/images/four_screen_on.webp differ diff --git a/src/lib/images/qdl-ports-four.svg b/src/lib/images/qdl-ports-four.svg index bed828b2..d2da770a 100644 --- a/src/lib/images/qdl-ports-four.svg +++ b/src/lib/images/qdl-ports-four.svg @@ -1,3 +1,3 @@ -2 +2 1 \ No newline at end of file diff --git a/src/lib/images/qdl-ports-three.svg b/src/lib/images/qdl-ports-three.svg index e5e8f9c1..5ae6e200 100644 --- a/src/lib/images/qdl-ports-three.svg +++ b/src/lib/images/qdl-ports-three.svg @@ -13,6 +13,6 @@ - + diff --git a/src/lib/utils/image.js b/src/lib/utils/image.js index 1bd6aa7d..f3cf420e 100644 --- a/src/lib/utils/image.js +++ b/src/lib/utils/image.js @@ -9,7 +9,7 @@ import { fetchStream } from "./stream"; * @returns {void} */ -const MIN_QUOTA_MB = 5250; +const MIN_QUOTA_GB = 5.25; export class ImageManager { /** @type {FileSystemDirectoryHandle} */ @@ -18,14 +18,22 @@ export class ImageManager { async init() { if (!this.root) { this.root = await globalThis.navigator.storage.getDirectory() - await this.root.remove({ recursive: true }) + // Clean up any leftover files from previous sessions + try { + await this.root.remove({ recursive: true }) + } catch (e) { + // Ignore errors - directory might not exist or be empty + console.debug('[ImageManager] Could not remove old directory:', e) + } + // Re-get the directory after removal + this.root = await navigator.storage.getDirectory() console.info('[ImageManager] Initialized') } const estimate = await globalThis.navigator.storage.estimate() - const quotaMB = (estimate.quota || 0) / (1024 ** 2) - if (quotaMB < MIN_QUOTA_MB) { - throw new Error(`Not enough storage: ${quotaMB.toFixed(0)}MB free, need ${MIN_QUOTA_MB.toFixed(0)}MB`) + const quotaGB = (estimate.quota || 0) / (1024 ** 3) + if (quotaGB < MIN_QUOTA_GB) { + throw new Error(`Not enough storage: ${quotaGB.toFixed(1)}GB free, need ${MIN_QUOTA_GB.toFixed(1)}GB`) } } diff --git a/src/lib/utils/manager.js b/src/lib/utils/manager.js index 9b67084b..44c6e79c 100644 --- a/src/lib/utils/manager.js +++ b/src/lib/utils/manager.js @@ -5,15 +5,31 @@ import { getManifest } from './manifest' import config from '../config' import { createSteps, withProgress } from './progress' +// Fast mode for development - skips flashing system partition (the slowest) +// Enable with ?fast=1 in URL +const fastModeEnabled = () => { + const FAST_MODE = new URLSearchParams(window.location.search).has('fast') + if (FAST_MODE()) { + console.warn('[Flash] FAST MODE ENABLED - skipping system partition') + } +} + export const StepCode = { INITIALIZING: 0, READY: 1, - CONNECTING: 2, - REPAIR_PARTITION_TABLES: 3, - ERASE_DEVICE: 4, - FLASH_SYSTEM: 5, - FINALIZING: 6, - DONE: 7, + DEVICE_PICKER: 2, + CONNECTING: 3, + REPAIR_PARTITION_TABLES: 4, + ERASE_DEVICE: 5, + FLASH_SYSTEM: 6, + FINALIZING: 7, + DONE: 8, +} + +// Device types for the picker +export const DeviceType = { + COMMA_3: 'comma3', // comma 3 or 3X + COMMA_4: 'comma4', // comma four } export const ErrorCode = { @@ -181,9 +197,10 @@ export class FlashManager { } catch (err) { console.error('[Flash] Failed to initialize image worker') console.error(err) - if (err instanceof String && err.startsWith('Not enough storage')) { + const message = err?.message || String(err) + if (message.startsWith('Not enough storage')) { this.#setError(ErrorCode.STORAGE_SPACE) - this.#setMessage(err) + this.#setMessage(message) } else { this.#setError(ErrorCode.UNKNOWN) } @@ -210,6 +227,12 @@ export class FlashManager { try { await this.device.connect(usb) } catch (err) { + // User cancelled the WebUSB dialog or no device was selected - not an error + if (err.name === 'NotFoundError') { + console.info('[Flash] No device selected') + this.#setStep(StepCode.READY) + return + } console.error('[Flash] Connection error', err) this.#setError(ErrorCode.LOST_CONNECTION) this.#setConnected(false) @@ -353,10 +376,16 @@ export class FlashManager { this.#setProgress(0) // Exclude GPT images and persist image, and pick correct userdata image to flash - const systemImages = this.manifest + let systemImages = this.manifest .filter((image) => !image.gpt && image.name !== 'persist') .filter((image) => !image.name.startsWith('userdata_') || image.name === this.#userdataImage) + // In fast mode, skip the system partition (slowest to flash) + if (fastModeEnabled()) { + systemImages = systemImages.filter((image) => image.name !== 'system') + console.info('[Flash] Fast mode: skipping system partition') + } + if (!systemImages.find((image) => image.name === this.#userdataImage)) { console.error(`[Flash] Did not find userdata image "${this.#userdataImage}"`) this.#setError(ErrorCode.UNKNOWN) @@ -414,7 +443,8 @@ export class FlashManager { async start() { if (this.step !== StepCode.READY) return await this.#connect() - if (this.error !== ErrorCode.NONE) return + // Check if connection was cancelled (step went back to READY) or failed + if (this.step === StepCode.READY || this.error !== ErrorCode.NONE) return let start = performance.now() await this.#repairPartitionTables() console.info(`Repaired partition tables in ${((performance.now() - start) / 1000).toFixed(2)}s`) diff --git a/src/lib/utils/platform.js b/src/lib/utils/platform.js new file mode 100644 index 00000000..a1e34333 --- /dev/null +++ b/src/lib/utils/platform.js @@ -0,0 +1,15 @@ +import { browser } from "$app/environment" + +export const isWindows = () => { + if (!browser) return false + const FORCE_WINDOWS = new URLSearchParams(globalThis.window.location?.search).has('windows') ?? false + if (FORCE_WINDOWS) { + console.warn('[Platform] FORCE WINDOWS MODE ENABLED') + } + return FORCE_WINDOWS || (globalThis.navigator.userAgent.toLowerCase().includes("windows") ?? false) +} + +export const isLinux = () => { + if (!browser) return false; + return globalThis.navigator.userAgent.toLowerCase().includes("linux") ?? false +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 589eefd1..162f4200 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,9 +1,38 @@ - + + + {#each preloadImages as src} + + {/each} + + {@render children()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index aa0f332f..5f802e7f 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,214 +1,29 @@ -
-
-
- comma -

flash.comma.ai

-

- This tool allows you to flash AGNOS onto your comma device. AGNOS is the Ubuntu-based operating system for your - comma 3/3X/4. -

-
-
- -
-

Requirements

-
    -
  • - A web browser which supports WebUSB - {" "}(such as Google Chrome, Microsoft Edge, Opera), running on Windows, macOS, Linux, or Android. -
  • -
  • - A good quality USB-C cable to connect the device to your computer. USB 3 - {" "}is recommended for faster flashing speed. -
  • -
  • - Another USB-C cable and a charger, to power the device outside your car. -
  • -
- {#if browser} - {#if isWindows()} -

USB Driver

-

- You need additional driver software for Windows before you connect your device. -

-
    -
  1. - Download and run Zadig. -
  2. -
  3. - Under Device in the menu bar, select{" "} - Create New Device. - Zadig Create New Device -
  4. -
  5. - Fill in three fields. The first field is just a description and you can fill in anything. The next two - fields are very important. Fill them in with{" "} - {VENDOR_ID} and {PRODUCT_ID} - respectively. Press "Install Driver" and give it a few minutes to install. - Zadig Form -
  6. -
-

No additional software is required for macOS, Linux or Android.

- {/if} - {/if} -
-
- -
-

Flashing

-

Follow these steps to put your device into QDL mode:

-
    -
  1. Unplug the device and wait for the LED to switch off.
  2. -
  3. - First, connect the device to your computer using the lower USB-C port - (port 1). -
  4. -
  5. - Second, connect power to the upper - OBD-C - port (port 2). -
  6. -
- comma four and two ports. the right port is labeled 1. the left port is labeled 2. - comma three and two ports. the lower port is labeled 1. the upper port is labeled 2. -

- Your device's screen will remain blank for the entire flashing process. This is normal. -

- {#if browser} - {#if isLinux()} - Note for Linux users -

- On Linux systems, devices in QDL mode are automatically bound to the kernel's qcserial driver, and need - to be unbound before we can access the device. Copy the script below into your terminal and run it after - plugging in your device. -

- - {/if} - {/if} -

- Next, click the button to start flashing. From the prompt select the device which starts with - “QUSB_BULK”. -

-

- The process can take 30+ minutes depending on your internet connection and system performance. Do not unplug the - device until all steps are complete. -

-
-
- -
-

Troubleshooting

-

Lost connection

-

- Try using high quality USB 3 cables. You should also try different USB ports on the front or back of your - computer. If you're using a USB hub, try connecting directly to your computer instead. -

-

My device's screen is blank

-

- This is normal in QDL mode. You can verify that the “QUSB_BULK” device shows up when you press the - Flash button to know that it is working correctly. -

-

My device says “fastboot mode”

-

- You may have followed outdated instructions for flashing. Please read the instructions above for putting your - device into QDL mode. -

-

General Tips

-
    -
  • Try another computer or OS
  • -
  • Try different USB ports on your computer
  • -
  • - Try different USB-C cables; low quality cables are often the source of problems. Note that the included OBD-C - cable will not work. -
  • -
-

Other questions

-

- If you need help, join our Discord server - and go to the #hw-three-3x channel. -

-
- - -
- -
- {#if browser} - - {/if} +
+ + - -
- flash.comma.ai version: {version} +
diff --git a/src/routes/Flash.svelte b/src/routes/Flash.svelte new file mode 100644 index 00000000..60196926 --- /dev/null +++ b/src/routes/Flash.svelte @@ -0,0 +1,419 @@ + + + +{#if wizardScreen === 'landing' && !error} + + + +{:else if wizardScreen === 'device' && !error} +
+ + +
+ + +{:else if wizardScreen === 'zadig' && !error} +
+ + +
+ + +{:else if wizardScreen === 'connect' && !error} +
+ + +
+ + +{:else if wizardScreen === 'unbind' && !error} +
+ + +
+ + +{:else if wizardScreen === 'webusb' && !error} +
+ + +
+ +{:else} +
+ {#if wizardStep >= 0} + {}} + /> + {/if} +
+ status +
+
+ +
+ {title} + + {description} + {#if showDiscordHelp} + If the problem persists, join #{discordChannel} on Discord for help. + {/if} + {#if error !== ErrorCode.NONE && hideRetry && !showDebugInfo} + + {/if} + + {#if error !== ErrorCode.NONE && !hideRetry} + + {/if} + {#if connected} + + {/if} + {#if error !== ErrorCode.NONE && (!hideRetry || showDebugInfo)} + setShowDebugInfo(false) : undefined} + /> + {/if} +
+{/if} diff --git a/src/routes/Stepper.svelte b/src/routes/Stepper.svelte new file mode 100644 index 00000000..565f4ea9 --- /dev/null +++ b/src/routes/Stepper.svelte @@ -0,0 +1,35 @@ + + +
+ {#each steps as stepName, index (stepName)} + {@const isCompleted = index < currentStep} + {@const isCurrent = index === currentStep} + {@const isClickable = index < currentStep} +
+ {#if index > 0} +
+ {/if} + +
+ {/each} +
\ No newline at end of file diff --git a/src/routes/_components/ConnectInstructions.svelte b/src/routes/_components/ConnectInstructions.svelte new file mode 100644 index 00000000..a4decbf6 --- /dev/null +++ b/src/routes/_components/ConnectInstructions.svelte @@ -0,0 +1,59 @@ + + +
+
+

Connect your device

+

Follow these steps to prepare your device for flashing

+
+ +
+ {isCommaFour + +
    +
  1. + A + Unplug the device +
  2. + {#if !isCommaFour} +
  3. + B + Wait for the light on the back to fully turn off +
  4. + {/if} +
  5. + {isCommaFour ? 'B' : 'C'} + Connect port 1 to your computer +
  6. +
  7. + {isCommaFour ? 'C' : 'D'} + Connect port 2 to your computer or a power brick +
  8. +
+
+ +

+ Your device's screen will remain blank. This is normal. +

+ + +
\ No newline at end of file diff --git a/src/routes/_components/DebugInfo.svelte b/src/routes/_components/DebugInfo.svelte new file mode 100644 index 00000000..7fa60571 --- /dev/null +++ b/src/routes/_components/DebugInfo.svelte @@ -0,0 +1,114 @@ + + +
+
+

+ Copy this debug info and paste it in{' '} + Discord + {' '}or{' '} + GitHub Issues. +

+ {#if onClose} + + {/if} +
+
+    {getDebugReport()}
+  
+ +
\ No newline at end of file diff --git a/src/routes/_components/DevicePicker.svelte b/src/routes/_components/DevicePicker.svelte new file mode 100644 index 00000000..13d2f757 --- /dev/null +++ b/src/routes/_components/DevicePicker.svelte @@ -0,0 +1,57 @@ + + +
+
+

Which device are you flashing?

+

Select your comma device

+
+ +
+ + + +
+ + +
\ No newline at end of file diff --git a/src/routes/_components/DeviceState.svelte b/src/routes/_components/DeviceState.svelte new file mode 100644 index 00000000..2751b663 --- /dev/null +++ b/src/routes/_components/DeviceState.svelte @@ -0,0 +1,19 @@ + + +
+ + + + Device serial: {serial || 'unknown'} +
diff --git a/src/routes/_components/LandingPage.svelte b/src/routes/_components/LandingPage.svelte new file mode 100644 index 00000000..63e8d6c8 --- /dev/null +++ b/src/routes/_components/LandingPage.svelte @@ -0,0 +1,21 @@ + + +
+ comma +
+

flash.comma.ai

+

+ Restore your comma device to a fresh factory state +

+
+ +
\ No newline at end of file diff --git a/src/lib/components/LinearProgress.svelte b/src/routes/_components/LinearProgress.svelte similarity index 100% rename from src/lib/components/LinearProgress.svelte rename to src/routes/_components/LinearProgress.svelte diff --git a/src/routes/_components/LinuxUnbind.svelte b/src/routes/_components/LinuxUnbind.svelte new file mode 100644 index 00000000..d19cb58d --- /dev/null +++ b/src/routes/_components/LinuxUnbind.svelte @@ -0,0 +1,43 @@ + + +
+
+

Unbind from qcserial

+

+ On Linux, devices in QDL mode are bound to the kernel's qcserial driver. + Run this command in a terminal to unbind it: +

+
+ +
+
+      {DETACH_SCRIPT}
+    
+ +
+ + +
\ No newline at end of file diff --git a/src/routes/_components/WebUSBConnect.svelte b/src/routes/_components/WebUSBConnect.svelte new file mode 100644 index 00000000..31e8c99f --- /dev/null +++ b/src/routes/_components/WebUSBConnect.svelte @@ -0,0 +1,23 @@ + + +
+
+ connect +
+
+

Select your device

+

+ Click the button below to open the device selector, then choose QUSB_BULK_CID from the list. +

+
+ +
\ No newline at end of file diff --git a/src/routes/_components/WindowsZadig.svelte b/src/routes/_components/WindowsZadig.svelte new file mode 100644 index 00000000..b8757078 --- /dev/null +++ b/src/routes/_components/WindowsZadig.svelte @@ -0,0 +1,61 @@ + + +
+
+

Install USB Driver

+

Windows requires a driver to communicate with your device

+
+ +
+
+ 1 +
+

Download and run Zadig

+
+
+ +
+ 2 +
+

Under Device in the menu bar, select Create New Device

+ Zadig Create New Device +
+
+ +
+ 3 +
+

Fill in the form:

+
    +
  • a.Name: {deviceType === DeviceType.COMMA_4 ? 'comma four' : 'comma 3/3X'}
  • +
  • b.USB ID: {vendorId} and {PRODUCT_ID}
  • +
+ Zadig Form +
+
+ +
+ 4 +
+

Click Install Driver

+
+
+
+ + +
\ No newline at end of file diff --git a/src/routes/layout.css b/src/routes/layout.css index ce91050e..5b9b7b87 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -7,3 +7,32 @@ --bg-gradient-radial: radial-gradient(var(--tw-gradient-stops)); --bg-gradient-conic: conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops)); } + + +::selection { + background-color: #51ff00; + color: black; +} + +/* Custom scrollbar for debug info */ +.debug-scrollbar::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.debug-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.debug-scrollbar::-webkit-scrollbar-thumb { + background: #4b5563; + border-radius: 4px; +} + +.debug-scrollbar::-webkit-scrollbar-thumb:hover { + background: #6b7280; +} + +.debug-scrollbar::-webkit-scrollbar-corner { + background: transparent; +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index d5c23d9c..a2d9d9db 100644 --- a/vite.config.js +++ b/vite.config.js @@ -34,6 +34,9 @@ export default defineConfig({ }, ], }, + resolve: { + extensions: ['.svelte', '.js', '.ts', '.json', '.mjs',] // You can add or remove extensions + }, ssr: { noExternal: [ "@commaai/qdl",