diff --git a/bun.lockb b/bun.lockb index d196283..3f16a90 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/demo/src/app.ts b/demo/src/app.ts index 63d7814..1a9cefc 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 { @@ -31,7 +32,7 @@ interface LunInfo { declare global { interface Window { - connectDevice: () => Promise + connectDevice: (serial: boolean) => Promise } } @@ -56,7 +57,7 @@ function createObjectTable(element: HTMLElement, data: Record) { return table; } -window.connectDevice = async () => { +window.connectDevice = async (serial: boolean) => { const programmerSelect = document.getElementById("programmer") as HTMLSelectElement; const status = document.getElementById("status"); const deviceDiv = document.getElementById("device"); @@ -72,17 +73,14 @@ window.connectDevice = async () => { status.className = ""; status.textContent = "Connecting..."; - if (!("usb" in navigator)) { - throw new Error("Browser missing WebUSB support"); - } - // Initialize QDL device with programmer URL const qdl = new qdlDevice(programmerSelect.value); // Start the connection - await qdl.connect(new usbClass()); + 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(); @@ -90,9 +88,10 @@ window.connectDevice = async () => { 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/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

- + +
diff --git a/package.json b/package.json index 400d6b2..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" @@ -33,6 +37,7 @@ "@biomejs/biome": "1.9.4", "@happy-dom/global-registrator": "^16.7.2", "@types/bun": "latest", + "@types/w3c-web-serial": "^1.0.7", "@types/w3c-web-usb": "^1.0.10" }, "//dependencies": { 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..c848b65 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) { @@ -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 new file mode 100644 index 0000000..2b96194 --- /dev/null +++ b/src/seriallib.js @@ -0,0 +1,122 @@ +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() { + if (!("serial" in navigator)) { + throw new Error("Browser missing Web Serial support"); + } + 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) { + 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[]} */ + const chunks = []; + let received = 0; + while (this.port.readable && !canceled) { + const reader = this.port.readable.getReader(); + try { + do { + 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; + } + chunks.push(value); + received += value.byteLength; + } while (length && received < length); + } catch (error) { + // Handle error + } finally { + reader.releaseLock(); + } + } + const result = concatUint8Array(chunks); + console.log(" result:", result.toHexString()); + return result; + } + + /** + * @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"); + 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 2ec19a1..d7336bc 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, @@ -90,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; } /** @@ -119,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(' '); +};