diff --git a/.gitignore b/.gitignore
index 08c27c0..9e04545 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+/edl/
+/qdl.js/
+/provisioning/
diff --git a/bun.lock b/bun.lock
index d76cbb6..55714d5 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,7 +5,7 @@
"": {
"name": "@commaai/flash",
"dependencies": {
- "@commaai/qdl": "git+https://github.com/commaai/qdl.js.git#21d7be79fa5178f253d32a0879bd8bdd4fa37e30",
+ "@commaai/qdl": "git+https://github.com/commaai/qdl.js.git#7177fde779bc6764727130969af1841d71a4c43c",
"@fontsource-variable/inter": "^5.2.5",
"@fontsource-variable/jetbrains-mono": "^5.2.5",
"react": "^18.3.1",
@@ -81,7 +81,7 @@
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
- "@commaai/qdl": ["@commaai/qdl@github:commaai/qdl.js#21d7be7", { "dependencies": { "@incognitojam/tiny-struct": "npm:@jsr/incognitojam__tiny-struct@^0.1.2", "arg": "^5.0.2", "crc-32": "^1.2.2", "fast-xml-parser": "^5.0.8", "usb": "^2.15.0" }, "peerDependencies": { "typescript": "^5.7.3" }, "bin": { "simg2img.js": "dist/bin/simg2img.js", "qdl.js": "dist/bin/qdl.js" } }, "commaai-qdl.js-21d7be7"],
+ "@commaai/qdl": ["@commaai/qdl@github:commaai/qdl.js#7177fde", { "dependencies": { "@incognitojam/tiny-struct": "npm:@jsr/incognitojam__tiny-struct@^0.1.2", "arg": "^5.0.2", "crc-32": "^1.2.2", "fast-xml-parser": "^5.0.8", "usb": "^2.15.0" }, "peerDependencies": { "typescript": "^5.7.3" }, "bin": { "simg2img.js": "dist/bin/simg2img.js", "qdl.js": "dist/bin/qdl.js" } }, "commaai-qdl.js-7177fde"],
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
diff --git a/deploy-preview.sh b/deploy-preview.sh
index fc035f6..9716b66 100755
--- a/deploy-preview.sh
+++ b/deploy-preview.sh
@@ -3,6 +3,7 @@ set -e
BRANCH="${1:-$(git branch --show-current)}"
+bun install
bun run build
bunx wrangler pages deploy dist --project-name=connect --branch="$BRANCH"
diff --git a/package.json b/package.json
index 73aed3a..adaa696 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"node": ">=20.11.0"
},
"dependencies": {
- "@commaai/qdl": "git+https://github.com/commaai/qdl.js.git#21d7be79fa5178f253d32a0879bd8bdd4fa37e30",
+ "@commaai/qdl": "git+https://github.com/commaai/qdl.js.git#7177fde779bc6764727130969af1841d71a4c43c",
"@fontsource-variable/inter": "^5.2.5",
"@fontsource-variable/jetbrains-mono": "^5.2.5",
"react": "^18.3.1",
diff --git a/src/app/Flash.jsx b/src/app/Flash.jsx
index 0732474..fa836b1 100644
--- a/src/app/Flash.jsx
+++ b/src/app/Flash.jsx
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
-import { FlashManager, StepCode, ErrorCode, DeviceType } from '../utils/manager'
+import { FlashManager, StepCode, ErrorCode, DeviceType, DUMP_GPT_MODE } from '../utils/manager'
import { useImageManager } from '../utils/image'
import { isLinux, isWindows } from '../utils/platform'
import config from '../config'
@@ -517,6 +517,91 @@ function LinuxUnbind({ onNext }) {
)
}
+// GPT Dump diagnostic mode
+function GptDumpMode({ qdlManager, imageManager }) {
+ const [gptDump, setGptDump] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [copied, setCopied] = useState(false)
+ const [ready, setReady] = useState(false)
+
+ useEffect(() => {
+ if (!imageManager.current) return
+ fetch(config.loader.url)
+ .then((res) => res.arrayBuffer())
+ .then((programmer) => {
+ qdlManager.current = new FlashManager(programmer, {})
+ qdlManager.current.initialize(imageManager.current).then(() => setReady(true))
+ })
+ }, [imageManager.current])
+
+ const handleDump = async () => {
+ setLoading(true)
+ const result = await qdlManager.current.dumpGpt()
+ setGptDump(result)
+ setLoading(false)
+ }
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(gptDump)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ return (
+
+
+
GPT Diagnostic Mode
+
+ Connect your device to dump partition table info
+
+
+
+ {!gptDump ? (
+
+ ) : (
+ <>
+
+
+
+
+
+
+ Send this to{' '}
+
+ Discord
+
+ {' '}for debugging
+
+ >
+ )}
+
+ )
+}
+
// WebUSB connection screen - shows while waiting for user to select device
function WebUSBConnect({ onConnect }) {
return (
@@ -722,6 +807,11 @@ export default function Flash() {
// Handle retry on error
const handleRetry = () => window.location.reload()
+ // Render GPT dump diagnostic mode
+ if (DUMP_GPT_MODE) {
+ return
+ }
+
// Render landing page
if (wizardScreen === 'landing' && !error) {
return (
diff --git a/src/utils/manager.js b/src/utils/manager.js
index 618ff8d..496c973 100644
--- a/src/utils/manager.js
+++ b/src/utils/manager.js
@@ -5,6 +5,9 @@ import { getManifest } from './manifest'
import config from '../config'
import { createSteps, withProgress } from './progress'
+// Diagnostic mode - dump GPT partition info
+export const DUMP_GPT_MODE = new URLSearchParams(window.location.search).has('dump_gpt')
+
// Fast mode for development - skips flashing system partition (the slowest)
// Enable with ?fast=1 in URL
const FAST_MODE = new URLSearchParams(window.location.search).has('fast')
@@ -392,17 +395,17 @@ export class FlashManager {
try {
for await (const image of systemImages) {
- const [onDownload, onFlash] = createSteps([1, image.hasAB ? 2 : 1], this.#setProgress.bind(this))
+ // Flash system to slot A only (large, slow), other A/B partitions to both slots
+ const flashBothSlots = image.hasAB && image.name !== 'system'
+ const [onDownload, onFlash] = createSteps([1, flashBothSlots ? 2 : 1], this.#setProgress.bind(this))
this.#setMessage(`Downloading ${image.name}`)
await this.imageManager.downloadImage(image, onDownload)
const blob = await this.imageManager.getImage(image)
onDownload(1.0)
- // Flash image to each slot
- const slots = image.hasAB ? ['_a', '_b'] : ['']
+ const slots = flashBothSlots ? ['_a', '_b'] : (image.hasAB ? ['_a'] : [''])
for (const [slot, onSlotProgress] of withProgress(slots, onFlash)) {
- // NOTE: userdata image name does not match partition name
const partitionName = `${image.name.startsWith('userdata_') ? 'userdata' : image.name}${slot}`
this.#setMessage(`Flashing ${partitionName}`)
@@ -424,10 +427,14 @@ export class FlashManager {
this.#setProgress(-1)
this.#setMessage('Finalizing...')
- // Set bootable LUN and update active partitions
- if (!await this.device.setActiveSlot('a')) {
- console.error('[Flash] Failed to update slot')
+ // Set bootable LUN to slot A (LUN 1)
+ // GPT images already have correct A/B flags, no need to manipulate them
+ try {
+ await this.device.setBootableLun(1)
+ } catch (err) {
+ console.error('[Flash] Failed to set bootable LUN', err)
this.#setError(ErrorCode.FINALIZING_FAILED)
+ return
}
// Reboot the device
@@ -459,4 +466,85 @@ export class FlashManager {
await this.#finalize()
console.info(`Finalized in ${((performance.now() - start) / 1000).toFixed(2)}s`)
}
+
+ /**
+ * Diagnostic mode: connect and dump GPT from all LUNs
+ * @returns {Promise} Formatted GPT dump or null on error
+ */
+ async dumpGpt() {
+ this.#setStep(StepCode.CONNECTING)
+ this.#setProgress(-1)
+
+ let usb
+ try {
+ usb = new usbClass()
+ } catch (err) {
+ console.error('[Flash] Connection error', err)
+ this.#setError(ErrorCode.LOST_CONNECTION)
+ return null
+ }
+
+ try {
+ await this.device.connect(usb)
+ } catch (err) {
+ if (err.name === 'NotFoundError') {
+ console.info('[Flash] No device selected')
+ this.#setStep(StepCode.READY)
+ return null
+ }
+ console.error('[Flash] Connection error', err)
+ this.#setError(ErrorCode.LOST_CONNECTION)
+ return null
+ }
+
+ this.#setConnected(true)
+ this.#setMessage('Reading GPT...')
+
+ try {
+ const lines = []
+ lines.push('=== GPT Dump ===')
+ lines.push(`Time: ${new Date().toISOString()}`)
+ lines.push('')
+
+ // Get storage info
+ const storageInfo = await this.device.getStorageInfo()
+ lines.push('Storage Info:')
+ lines.push(JSON.stringify(storageInfo, null, 2))
+ lines.push('')
+
+ // Dump GPT from each LUN
+ for (let lun = 0; lun < 6; lun++) {
+ lines.push(`=== LUN ${lun} ===`)
+ try {
+ const gpt = await this.device.getGpt(lun)
+ const partitions = gpt.getPartitions()
+
+ for (const part of partitions) {
+ const isAB = part.name.endsWith('_a') || part.name.endsWith('_b')
+ let line = `${part.name}: ${part.attributes}`
+
+ if (isAB) {
+ // Parse A/B flags from attributes
+ const attrs = BigInt(part.attributes)
+ // Flags are at bits 48-55 (correct) or 54-61 (buggy)
+ const flags48 = (attrs >> 48n) & 0xFFFFn
+ const flags54 = (attrs >> 54n) & 0xFFFFn
+ line += ` [bit48: 0x${flags48.toString(16)}, bit54: 0x${flags54.toString(16)}]`
+ }
+ lines.push(line)
+ }
+ } catch (err) {
+ lines.push(`Error reading LUN ${lun}: ${err.message}`)
+ }
+ lines.push('')
+ }
+
+ this.#setStep(StepCode.DONE)
+ return lines.join('\n')
+ } catch (err) {
+ console.error('[Flash] Error dumping GPT', err)
+ this.#setError(ErrorCode.UNKNOWN)
+ return null
+ }
+ }
}