From 849b5ce132ff6f27dc7e9de1a69cbb694f1ee213 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Thu, 13 Feb 2025 16:23:01 +0000 Subject: [PATCH 1/6] add @types/w3c-web-serial --- bun.lockb | Bin 8924 -> 9316 bytes package.json | 1 + 2 files changed, 1 insertion(+) diff --git a/bun.lockb b/bun.lockb index d1962834f638c3b38fdc3317e75e78829245d0fd..3f16a9097f70b6cbdc11498a7b5a05175e80cd71 100755 GIT binary patch delta 1659 zcmb_ceQZ-z6u(GLJ|t!;0%A>AuAjzJNKM(c40d4g6cmv{>j;256x=S-n^2zz358+y%owwx4m}psUKNrpmT zI=MF|y?s&lq`+9mBdgT*PR3f+wghUb8CwoT5o4*lfTh4YfmUEIXDlE1aTbSx1(4$s zV>aMy*v$Z@Aq%^Rfw4m1EkNNkY2{_q;c#nYH}ob5$(ZV zOjog&P;+6Dm(qUhWyGCpcqer@PtecMG`ej3>!BsCaU)%@gsggxAst)YlqhRJGqURw zlO%G49%Aoj^B~K#vn;Y0#mGvqTv+H)-9*$1`kobIIk7}di6<5{h5prR5dE#!UIP6p zXs>WrHDBBy%!C>(!q7T;8H$obQ|NBQmBO7ULs7=Uy(r^S>T-LnqgjWlhX&kL;(I#k zUhNihDA)HEzAU5$)mls{3duE|++2F{ht}iki!N76Jfv2uL3}z)eX`=o&8t_Bz4XMc z_S0<=f4WW$tD)eeUW!ZfnJmxCjje~BYFG2I$Y}}J%u{RCYL**ddv87Wd{>iyF*LOZ z05?PoRH4Wf-t2(-{%Xx*X`ewdB7%B#E+PmUX{jQIbCay_mu)Y4V<&b`8RTYNJh0!= z{?$!CO=)^*hoX3LvvK(KM7$zdw`)p&fKDk2AEC>NV(q~g74)u$9OaF)v^-eef*d2Y zspsm#pYk5Z^-Eb3^3f~h&*XKkT-&yM71=z8W_Q=L1iDwQ2uvQ$3C#KDK|oLT&>l~3 Tp7o?}@}4t3JhzV$m0SM;K&U;F delta 1462 zcmb_cU1(HC6rP#An`C!$H=7ufy?2#_#2*yB-K?(ey2)-3BdMDh1Iksnr$Mb? z5X`Du=u6Rhtc`+=kosWJfaJy62jhzb6g7$6pYE*MyN|HFWo!>%wlTJL>)M&KB8;`cP{CMsKF|v^Ib$B+R|Q-J^g+H0ECo)6-$Y|!e`bs6yf&BTDDFOfE^EB9dmsSV&A3mq{7LR*2#%k|^~kvhqU_6@7{&&^R|a z2KJ~A#xRVjm&*S)VOyOeuV0bG&t$CpKAZ5M;hDtgCvTd*_v-Y*o91qc`)rE&bUNxw z^J01e=`p1C(lqzb;ZmD=OLck&$wy!6Hnr+Hozl}>r+;*t`>E4!=6#3=5iJ1}8YP;(7nC;iK(u`vuR;vcguFF+BR!W-#i^fRneXqUZl!JfiUiD~>3P zIa8wS)z61&l2?w6S_GXkjgT`{M&=C8KX>_MX4D>`KTU(D>0i^Bb1ngA)x&x#X`+^r?ev!p6WuyP}poP7QMuG_}>o|ynJ Date: Thu, 13 Feb 2025 16:41:17 +0000 Subject: [PATCH 2/6] implement seriallib --- package.json | 4 ++ src/seriallib.js | 112 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/seriallib.js diff --git a/package.json b/package.json index a13cd29..3820350 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,10 @@ "types": "./dist/qdl.d.ts", "import": "./dist/qdl.js" }, + "./seriallib": { + "types": "./dist/seriallib.d.ts", + "import": "./dist/seriallib.js" + }, "./usblib": { "types": "./dist/usblib.d.ts", "import": "./dist/usblib.js" diff --git a/src/seriallib.js b/src/seriallib.js new file mode 100644 index 0000000..a7e6444 --- /dev/null +++ b/src/seriallib.js @@ -0,0 +1,112 @@ +import * as constants from "./constants"; +import { concatUint8Array } from "./utils"; + + +/** + * @type {SerialOptions} + */ +const SERIAL_OPTIONS = { + baudRate: 115_200, + dataBits: 8, + stopBits: 1, + parity: "none", + bufferSize: 16_384, + flowControl: "hardware", // RTS and CTS +}; + + +export class serialClass { + constructor() { + /** @type {SerialPort|null} */ + this.port = null; + this.opened = false; + } + + get connected() { + return this.port?.connected && this.opened; + } + + async connect() { + const port = await navigator.serial.requestPort({ + filters: [{ + usbVendorId: constants.VENDOR_ID, + usbProductId: constants.PRODUCT_ID, + }], + }); + console.debug("[seriallib] Using serial port:", port); + this.port = port; + try { + await this.port.open(SERIAL_OPTIONS); + } catch (e) { + throw new Error("Failed to connect to serial port", { cause: e }); + } + this.opened = true; + console.debug("[seriallib] Connected"); + } + + /** + * @param {number} [length=0] + * @param {number} [timeout=0] + * @returns {Promise} + */ + async read(length = 0, timeout = 0) { + if (!this.connected) throw new Error("Not connected"); + let canceled = false; + if (timeout) setTimeout(() => { + canceled = true; + }, timeout); + /** @type {Uint8Array[]} */ + const chunks = []; + let received = 0; + while (this.port.readable && !canceled) { + const reader = this.port.readable.getReader(); + try { + do { + const { value, done } = await reader.read(); + if (done) { + canceled = true; + break; + } + chunks.push(value); + received += value.byteLength; + } while (length && received < length); + } catch (error) { + // Handle error + } finally { + reader.releaseLock(); + } + } + return concatUint8Array(chunks); + } + + /** + * @param {Uint8Array} data + * @returns {Promise} + */ + async #write(data) { + if (!this.port.writable) throw new Error("Not writable"); + const writer = this.port.writable.getWriter(); + try { + let pos = 0; + while (pos < data.length) { + await writer.ready; + const chunk = data.slice(pos, pos + Math.max(1, Math.min(constants.BULK_TRANSFER_SIZE, writer.desiredSize))); + await writer.write(chunk); + pos += chunk.length; + } + } finally { + writer.releaseLock(); + } + } + + /** + * @param {Uint8Array} data + * @param {boolean} [wait=true] + * @returns {Promise} + */ + async write(data, wait = true) { + if (!this.connected) throw new Error("Not connected"); + const promise = this.#write(data); + if (wait) await promise; + } +} From e26fa4bcdbcf3870f4d067ecd9467a87b88a0e1f Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Thu, 13 Feb 2025 16:44:10 +0000 Subject: [PATCH 3/6] use serial in demo --- demo/src/app.ts | 18 ++++++++++++++---- src/firehose.js | 2 +- src/qdl.js | 2 +- src/sahara.js | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/demo/src/app.ts b/demo/src/app.ts index 63d7814..2dc9bdb 100644 --- a/demo/src/app.ts +++ b/demo/src/app.ts @@ -1,4 +1,5 @@ import { qdlDevice } from "@commaai/qdl"; +import { serialClass } from "@commaai/qdl/seriallib"; import { usbClass } from "@commaai/qdl/usblib"; interface PartitionInfo { @@ -56,7 +57,7 @@ function createObjectTable(element: HTMLElement, data: Record) { return table; } -window.connectDevice = async () => { +window.connectDevice = async (serial = true) => { const programmerSelect = document.getElementById("programmer") as HTMLSelectElement; const status = document.getElementById("status"); const deviceDiv = document.getElementById("device"); @@ -72,15 +73,24 @@ window.connectDevice = async () => { status.className = ""; status.textContent = "Connecting..."; - if (!("usb" in navigator)) { - throw new Error("Browser missing WebUSB support"); + let cdc: serialClass | usbClass; + if (serial) { + if (!("serial" in navigator)) { + throw new Error("Browser missing Web Serial support"); + } + cdc = new serialClass(); + } else { + if (!("usb" in navigator)) { + throw new Error("Browser missing WebUSB support"); + } + cdc = new usbClass(); } // Initialize QDL device with programmer URL const qdl = new qdlDevice(programmerSelect.value); // Start the connection - await qdl.connect(new usbClass()); + await qdl.connect(cdc); status.className = "success"; status.textContent = "Connected! Reading device info..."; diff --git a/src/firehose.js b/src/firehose.js index b152c64..667ab91 100644 --- a/src/firehose.js +++ b/src/firehose.js @@ -45,7 +45,7 @@ class cfg { export class Firehose { /** - * @param {usbClass} cdc + * @param {serialClass|usbClass} cdc */ constructor(cdc) { this.cdc = cdc; diff --git a/src/qdl.js b/src/qdl.js index 8b126fb..577af20 100644 --- a/src/qdl.js +++ b/src/qdl.js @@ -34,7 +34,7 @@ export class qdlDevice { } /** - * @param {usbClass} cdc + * @param {serialClass|usbClass} cdc * @returns {Promise} */ async connect(cdc) { diff --git a/src/sahara.js b/src/sahara.js index a3dfb23..637494f 100644 --- a/src/sahara.js +++ b/src/sahara.js @@ -4,7 +4,7 @@ import { concatUint8Array, packGenerator, readBlobAsBuffer } from "./utils"; export class Sahara { /** - * @param {usbClass} cdc + * @param {serialClass|usbClass} cdc * @param {string} programmerUrl */ constructor(cdc, programmerUrl) { From 0bb36497e669a514f2608825842440414993b8e2 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Thu, 13 Feb 2025 16:47:26 +0000 Subject: [PATCH 4/6] side by side --- demo/src/app.ts | 4 ++-- demo/src/index.html | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/demo/src/app.ts b/demo/src/app.ts index 2dc9bdb..e11a33e 100644 --- a/demo/src/app.ts +++ b/demo/src/app.ts @@ -32,7 +32,7 @@ interface LunInfo { declare global { interface Window { - connectDevice: () => Promise + connectDevice: (serial: boolean) => Promise } } @@ -57,7 +57,7 @@ function createObjectTable(element: HTMLElement, data: Record) { return table; } -window.connectDevice = async (serial = true) => { +window.connectDevice = async (serial: boolean) => { const programmerSelect = document.getElementById("programmer") as HTMLSelectElement; const status = document.getElementById("status"); const deviceDiv = document.getElementById("device"); diff --git a/demo/src/index.html b/demo/src/index.html index 26dd7bd..bc50400 100644 --- a/demo/src/index.html +++ b/demo/src/index.html @@ -110,7 +110,8 @@

Note on Linux

- + +
From 821d68bcb05704fb60573145192c210faab10319 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Thu, 13 Feb 2025 17:02:41 +0000 Subject: [PATCH 5/6] move navigator check to lib --- demo/src/app.ts | 15 +-------------- src/seriallib.js | 3 +++ src/usblib.js | 3 +++ 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/demo/src/app.ts b/demo/src/app.ts index e11a33e..31e3d05 100644 --- a/demo/src/app.ts +++ b/demo/src/app.ts @@ -73,24 +73,11 @@ window.connectDevice = async (serial: boolean) => { status.className = ""; status.textContent = "Connecting..."; - let cdc: serialClass | usbClass; - if (serial) { - if (!("serial" in navigator)) { - throw new Error("Browser missing Web Serial support"); - } - cdc = new serialClass(); - } else { - if (!("usb" in navigator)) { - throw new Error("Browser missing WebUSB support"); - } - cdc = new usbClass(); - } - // Initialize QDL device with programmer URL const qdl = new qdlDevice(programmerSelect.value); // Start the connection - await qdl.connect(cdc); + await qdl.connect(serial ? new serialClass() : new usbClass()); status.className = "success"; status.textContent = "Connected! Reading device info..."; diff --git a/src/seriallib.js b/src/seriallib.js index a7e6444..88e88df 100644 --- a/src/seriallib.js +++ b/src/seriallib.js @@ -27,6 +27,9 @@ export class serialClass { } async connect() { + if (!("serial" in navigator)) { + throw new Error("Browser missing Web Serial support"); + } const port = await navigator.serial.requestPort({ filters: [{ usbVendorId: constants.VENDOR_ID, diff --git a/src/usblib.js b/src/usblib.js index 2ec19a1..28c64ca 100644 --- a/src/usblib.js +++ b/src/usblib.js @@ -74,6 +74,9 @@ export class usbClass { } async connect() { + if (!("usb" in navigator)) { + throw new Error("Browser missing WebUSB support"); + } const device = await navigator.usb.requestDevice({ filters: [{ vendorId: constants.VENDOR_ID, From 4fdfe02fd45e2b2701252c7b2f09df6be92c5c3c Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Thu, 13 Feb 2025 23:21:24 +0000 Subject: [PATCH 6/6] debug --- demo/src/app.ts | 4 +++- src/sahara.js | 7 +++++++ src/seriallib.js | 11 +++++++++-- src/usblib.js | 20 ++++++++++++++++---- src/utils.js | 6 ++++++ 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/demo/src/app.ts b/demo/src/app.ts index 31e3d05..1a9cefc 100644 --- a/demo/src/app.ts +++ b/demo/src/app.ts @@ -80,6 +80,7 @@ window.connectDevice = async (serial: boolean) => { await qdl.connect(serial ? new serialClass() : new usbClass()); status.className = "success"; status.textContent = "Connected! Reading device info..."; + return; // Device information const activeSlot = await qdl.getActiveSlot(); @@ -87,9 +88,10 @@ window.connectDevice = async (serial: boolean) => { createObjectTable(deviceDiv, { "Active Slot": activeSlot, "SOC Serial Number": qdl.sahara!.serial, - "UFS Serial Number": "0x"+storageInfo.serial_num.toString(16).padStart(8, "0"), + "UFS Serial Number": "0x" + storageInfo.serial_num.toString(16).padStart(8, "0"), }); createObjectTable(storageDiv, storageInfo); + return; // Get GPT info for each LUN const lunInfos: LunInfo[] = []; diff --git a/src/sahara.js b/src/sahara.js index 637494f..c848b65 100644 --- a/src/sahara.js +++ b/src/sahara.js @@ -23,6 +23,7 @@ export class Sahara { * @returns {Promise} */ async connect() { + console.debug("[sahara] connect"); const resp = await this.cdc.read(0xC * 0x4); if (resp.length > 1 && resp[0] === 0x01) { const pkt = this.ch.pkt_cmd_hdr(resp); @@ -111,18 +112,23 @@ export class Sahara { async enterCommandMode() { if (!await this.cmdHello(sahara_mode_t.SAHARA_MODE_COMMAND)) { + console.log("no hello") return false; } let res = await this.getResponse(); + console.log("got response", res); if ("cmd" in res) { if (res.cmd === cmd_t.SAHARA_END_TRANSFER) { + console.debug("sahara end transfer", res); if ("data" in res) { return false; } } else if (res.cmd === cmd_t.SAHARA_CMD_READY) { + console.debug("sahara ready"); return true; } } + console.debug("something else", res); return false; } @@ -179,6 +185,7 @@ export class Sahara { await this.cmdModeSwitch(sahara_mode_t.SAHARA_MODE_COMMAND); await this.connect(); + throw "Done"; console.debug("[sahara] Uploading loader..."); await this.downloadLoader(); const loaderBlob = await this.getLoader(); diff --git a/src/seriallib.js b/src/seriallib.js index 88e88df..2b96194 100644 --- a/src/seriallib.js +++ b/src/seriallib.js @@ -53,9 +53,11 @@ export class serialClass { * @returns {Promise} */ async read(length = 0, timeout = 0) { + console.debug("[seriallib] read", { length, timeout }); if (!this.connected) throw new Error("Not connected"); let canceled = false; if (timeout) setTimeout(() => { + console.debug("cancel read"); canceled = true; }, timeout); /** @type {Uint8Array[]} */ @@ -65,8 +67,10 @@ export class serialClass { const reader = this.port.readable.getReader(); try { do { - const { value, done } = await reader.read(); + const readTimeout = new Promise((resolve) => setTimeout(() => resolve({ done: true })), timeout || 2000); + const { value, done } = await Promise.race([reader.read(), readTimeout]); if (done) { + console.debug(" read done"); canceled = true; break; } @@ -79,7 +83,9 @@ export class serialClass { reader.releaseLock(); } } - return concatUint8Array(chunks); + const result = concatUint8Array(chunks); + console.log(" result:", result.toHexString()); + return result; } /** @@ -109,6 +115,7 @@ export class serialClass { */ async write(data, wait = true) { if (!this.connected) throw new Error("Not connected"); + console.debug("[seriallib] write", data.toHexString()); const promise = this.#write(data); if (wait) await promise; } diff --git a/src/usblib.js b/src/usblib.js index 28c64ca..d7336bc 100644 --- a/src/usblib.js +++ b/src/usblib.js @@ -93,27 +93,38 @@ export class usbClass { await this.#connectDevice(device); } + async #read() { + const result = await this.device?.transferIn(this.epIn?.endpointNumber, this.maxSize); + return new Uint8Array(result.data?.buffer); + } + /** * @param {number} [length=0] * @returns {Promise} */ async read(length = 0) { + console.debug("[usblib] read", { length }); + let result; if (length) { /** @type {Uint8Array[]} */ const chunks = []; let received = 0; do { - const chunk = await this.read(); + const chunk = await this.#read(); if (chunk.byteLength) { chunks.push(chunk); received += chunk.byteLength; + } else { + console.warn(" read empty"); + break; } } while (received < length); - return concatUint8Array(chunks); + result = concatUint8Array(chunks); } else { - const result = await this.device?.transferIn(this.epIn?.endpointNumber, this.maxSize); - return new Uint8Array(result.data?.buffer); + result = await this.#read(); } + console.debug(" result:", result.toHexString()); + return result; } /** @@ -122,6 +133,7 @@ export class usbClass { * @returns {Promise} */ async write(data, wait = true) { + console.debug("[usblib] write", data.toHexString()); if (data.byteLength === 0) { try { await this.device?.transferOut(this.epOut?.endpointNumber, data); diff --git a/src/utils.js b/src/utils.js index 62c4fff..e9b332b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -163,3 +163,9 @@ export function runWithTimeout(promise, timeout) { }); }); } + +Uint8Array.prototype.toHexString = function() { + return Array.from(this) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(' '); +};