diff --git a/src/badge-api.ts b/src/badge-api.ts index 3303f32..df504dc 100644 --- a/src/badge-api.ts +++ b/src/badge-api.ts @@ -18,6 +18,7 @@ export class BadgeAPI { async connect() { this.badge = await BadgeUSB.connect(); + this.badge.onConnectionLost = () => delete this.badge; } async disconnect(reset = false) { diff --git a/src/badge-usb.ts b/src/badge-usb.ts index f05d33d..e8fc4dd 100644 --- a/src/badge-usb.ts +++ b/src/badge-usb.ts @@ -6,6 +6,11 @@ import { crc32FromArrayBuffer } from "./lib/crc32"; import { concatBuffers } from "./lib/buffers"; +export enum BadgeUSBState { + CDC = 0x00, + WebUSB = 0x01, +} + export class BadgeUSB { static filters: USBDeviceFilter[] = [ { vendorId: 0x16d0, productId: 0x0f9a } // MCH2022 badge @@ -80,9 +85,11 @@ export class BadgeUSB { throw new Error("Browser does not support WebUSB"); } + console.debug('Requesting device from user agent...'); const usbDevice = await navigator.usb.requestDevice({ filters: this.filters }); + console.log('Selected device:', usbDevice); await usbDevice.open(); await usbDevice.selectConfiguration(this.defaultConfiguration); @@ -100,7 +107,8 @@ export class BadgeUSB { const badge = new BadgeUSB(usbDevice, interfaceIndex, endpoints); - await badge.controlSetState(true); + console.debug('Connecting: requesting device to enter WebUSB mode...'); + await badge.controlSetState(BadgeUSBState.WebUSB); await badge.controlSetBaudrate(921600); let currentMode = await badge.controlGetMode(); @@ -110,17 +118,31 @@ export class BadgeUSB { } badge._listen(); + console.debug('Connecting: started listening for incoming data'); + + console.time('Connecting: bus synchronized'); let protocolVersion: number | undefined; - while (protocolVersion == undefined) { - await badge.sync().then(v => protocolVersion = v).catch(); - } + let n = 0; + do { + if (++n > 100) { + throw new Error(`Sync failed after ${n} tries`); + } + console.debug('Connecting: syncing bus: attempt', n); + await badge.sync().then(v => protocolVersion = v).catch(() => {}); + + } while (protocolVersion == undefined) + + console.timeEnd('Connecting: bus synchronized'); + console.debug(`Connecting: bus synchronized in ${n} attempts`); + console.debug(`Protocol version: ${protocolVersion}`); if (protocolVersion < 2) { throw new Error("Protocol version not supported"); } badge.connected = true; + console.log('Connected to badge! 🎉'); return badge; } @@ -139,19 +161,26 @@ export class BadgeUSB { this.connected = false; try { this._stopListening(); - await this.controlSetMode(BadgeUSB.MODE_NORMAL); - if (reset) await this.controlReset(false); - await this.controlSetState(false); + + if (reset) { + console.debug('Disconnecting: requesting device to reset...'); + await this.controlReset(false); + } else { + console.debug('Disconnecting: requesting device to exit WebUSB mode...'); + await this.controlSetMode(BadgeUSB.MODE_NORMAL); + } + + console.debug('Disconnecting: resetting and releasing device USB interface...'); + await this.controlSetState(BadgeUSBState.CDC); await this.device.releaseInterface(this.interfaceIndex); } catch (error) { // Ignore errors } await this.device.close(); - this.nextTransactionID = 0; + console.log('Disconnecting: done'); + console.log('Session stats:', this.connectionStats); - if (this._onDisconnect) { - this._onDisconnect(); - } + if (this._onDisconnect) this._onDisconnect(); } set onConnectionLost(callback: () => void) { @@ -162,6 +191,16 @@ export class BadgeUSB { this._onDisconnect = callback; } + get connectionStats() { + return { + rxPackets: this.rxPacketCount, + txPackets: this.txPacketCount, + timesOutOfSync: this.resyncCount, + transactions: this.nextTransactionID, + pendingTransactions: Object.keys(this.transactionPromises).length, + }; + } + get manufacturerName() { this.assertConnected(); return this.device.manufacturerName; @@ -177,8 +216,11 @@ export class BadgeUSB { return this.device.serialNumber; } - async controlSetState(state: boolean) { - await this._controlTransferOut(BadgeUSB.REQUEST_STATE, state ? 0x0001 : 0x0000); + async controlSetState(state: BadgeUSBState) { + await this._controlTransferOut( + BadgeUSB.REQUEST_STATE, + state == BadgeUSBState.WebUSB ? BadgeUSBState.WebUSB : BadgeUSBState.CDC, + ); } async controlReset(bootloaderMode = false) { @@ -207,17 +249,24 @@ export class BadgeUSB { * @returns the protocol version number * @throws an error if sync fails **/ - async sync(): Promise { + async sync(): Promise { this.dataBuffer = new ArrayBuffer(0); // reset buffer - let result = await this.transaction(BadgeUSB.PROTOCOL_COMMAND_SYNC, new ArrayBuffer(0), 100); + let result = await this.transaction(BadgeUSB.PROTOCOL_COMMAND_SYNC, new ArrayBuffer(0), 100) + .catch(e => { if (e?.message != 'timeout') throw e }); + + if (result === undefined) return; + this.inSync = true; return new DataView(result).getUint16(0, true); } + private resyncCount = 0; async syncIfNeeded(): Promise { + if (!this.inSync) this.resyncCount++; + while (!this.inSync) { - await this.sync().catch(); + await this.sync().catch(() => {}); } } @@ -285,11 +334,11 @@ export class BadgeUSB { await this._handleData(result.buffer); } } catch (error) { - console.error(error); + console.error('FATAL Error while listening for data:', error); + console.warn('Connection lost. If this was not intentional, try reconnecting.'); + this.listening = false; - if (this._onConnectionLost) { - this._onConnectionLost(); - } + if (this._onConnectionLost) this._onConnectionLost(); } } @@ -336,6 +385,7 @@ export class BadgeUSB { return result.data; } + private txPacketCount = 0; private async _sendPacket(identifier: number, command: number, payload: ArrayBuffer | null = null) { if (payload === null) payload = new ArrayBuffer(0); @@ -346,8 +396,11 @@ export class BadgeUSB { dataView.setUint32(8, command, true); dataView.setUint32(12, payload.byteLength, true); dataView.setUint32(16, payload.byteLength > 0 ? crc32FromArrayBuffer(payload) : 0, true); + let packet = concatBuffers([header, payload]); await this._dataTransferOut(packet); + + this.txPacketCount++; } private async _handleData(buffer: ArrayBuffer) { @@ -370,6 +423,8 @@ export class BadgeUSB { } } + private rxPacketCount = 0; + private crcMismatchCount = 0; private async _handlePacket(buffer: ArrayBuffer) { let dataView = new DataView(buffer); let magic = dataView.getUint32(0, true); @@ -382,6 +437,8 @@ export class BadgeUSB { if (payloadLength > 0) { payload = buffer.slice(20); if (crc32FromArrayBuffer(payload) !== payloadCRC) { + console.debug('CRC mismatch; mismatches so far:', ++this.crcMismatchCount); + if (identifier in this.transactionPromises) { if (this.transactionPromises[identifier].timeout !== null) { clearTimeout(this.transactionPromises[identifier].timeout); @@ -421,6 +478,7 @@ export class BadgeUSB { responseText: BadgeUSB.textDecoder.decode(new Uint8Array(buffer.slice(8,12))), }); delete this.transactionPromises[identifier]; + this.rxPacketCount++; } else { console.error("Found no transaction for", identifier, responseType); } @@ -433,14 +491,16 @@ class TransactionPromise extends Promise { timeout?: number; - constructor() { + constructor(executor: ConstructorParameters>[0] = () => {}) { let resolver: (value: TransactionResponse | PromiseLike) => void; let rejector: (reason: TransactionResponse | Error) => void; super((resolve, reject) => { resolver = resolve; rejector = reject; + return executor(resolve, reject); // Promise magic: this line is essential but idk why }); + this.resolve = resolver!; this.reject = rejector!; }