Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support WebSerial communication #37

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
15 changes: 7 additions & 8 deletions demo/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { qdlDevice } from "@commaai/qdl";
import { serialClass } from "@commaai/qdl/seriallib";
import { usbClass } from "@commaai/qdl/usblib";

interface PartitionInfo {
Expand Down Expand Up @@ -31,7 +32,7 @@ interface LunInfo {

declare global {
interface Window {
connectDevice: () => Promise<void>
connectDevice: (serial: boolean) => Promise<void>
}
}

Expand All @@ -56,7 +57,7 @@ function createObjectTable(element: HTMLElement, data: Record<string, any>) {
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");
Expand All @@ -72,27 +73,25 @@ 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();
const storageInfo = await qdl.getStorageInfo();
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[] = [];
Expand Down
3 changes: 2 additions & 1 deletion demo/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ <h3>Note on Linux</h3>
<option value="https://raw.githubusercontent.com/commaai/flash/master/src/QDL/programmer.bin">comma 3/3X</option>
<option value="https://raw.githubusercontent.com/bkerler/Loaders/master/oneplus/0008b0e10051459b_dd7c5f2e53176bee_fhprg_op6t.bin">OnePlus 6T</option>
</select>
<button onclick="connectDevice()">Connect & Read Info</button>
<button onclick="connectDevice(false)">Connect with WebUSB</button>
<button onclick="connectDevice(true)">Connect with Web Serial</button>
</section>

<div id="status"></div>
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion src/firehose.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class cfg {

export class Firehose {
/**
* @param {usbClass} cdc
* @param {serialClass|usbClass} cdc
*/
constructor(cdc) {
this.cdc = cdc;
Expand Down
2 changes: 1 addition & 1 deletion src/qdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class qdlDevice {
}

/**
* @param {usbClass} cdc
* @param {serialClass|usbClass} cdc
* @returns {Promise<void>}
*/
async connect(cdc) {
Expand Down
9 changes: 8 additions & 1 deletion src/sahara.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -23,6 +23,7 @@ export class Sahara {
* @returns {Promise<boolean>}
*/
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);
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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();
Expand Down
122 changes: 122 additions & 0 deletions src/seriallib.js
Original file line number Diff line number Diff line change
@@ -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<Uint8Array>}
*/
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<void>}
*/
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<void>}
*/
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;
}
}
23 changes: 19 additions & 4 deletions src/usblib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Uint8Array>}
*/
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;
}

/**
Expand All @@ -119,6 +133,7 @@ export class usbClass {
* @returns {Promise<void>}
*/
async write(data, wait = true) {
console.debug("[usblib] write", data.toHexString());
if (data.byteLength === 0) {
try {
await this.device?.transferOut(this.epOut?.endpointNumber, data);
Expand Down
6 changes: 6 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(' ');
};
Loading