diff --git a/src/drivers/endgame/egg-ot-hid.test.ts b/src/drivers/endgame/egg-ot-hid.test.ts index 93147cc9..b2f9a7b4 100644 --- a/src/drivers/endgame/egg-ot-hid.test.ts +++ b/src/drivers/endgame/egg-ot-hid.test.ts @@ -287,3 +287,25 @@ test("unacknowledged writes reject instead of silently succeeding", async () => const client = new EggOtHidClient(fake.device); await assert.rejects(() => client.setDpi(1600), /did not acknowledge/); }); + +test("a mouse that never sends the response packet reports the raw tx/rx trace", async () => { + const fake = fakeOtMouse(); + const device = fake.device as unknown as { + sendFeatureReport: (reportId: number, data: BufferSource) => Promise; + receiveFeatureReport: () => Promise; + }; + const send = device.sendFeatureReport.bind(device); + const receive = device.receiveFeatureReport.bind(device); + let reads = 0; + // Ack only: the vendor response never arrives, like the first OP1 RGB hardware report. + device.sendFeatureReport = async (reportId, data) => { + reads = 0; + return send(reportId, data); + }; + device.receiveFeatureReport = async () => (reads++ === 0 ? receive() : new DataView(new ArrayBuffer(64))); + const client = new EggOtHidClient(fake.device); + await assert.rejects( + () => client.readStatus(), + /did not answer OT profile block 0\. Trace: tx 33 [0-9a-f ]+ \| rx 00 14 [0-9a-f ]+ \| rx 00 00 [0-9a-f ]+ x\d+/, + ); +}); diff --git a/src/drivers/endgame/egg-ot-hid.ts b/src/drivers/endgame/egg-ot-hid.ts index 16041564..5c10bbd4 100644 --- a/src/drivers/endgame/egg-ot-hid.ts +++ b/src/drivers/endgame/egg-ot-hid.ts @@ -85,6 +85,7 @@ export class EggOtHidClient { private chain: Promise = Promise.resolve(); private firmwareRaw: number[] | null | undefined; private packetPayloadLength = OT_PACKET_SIZE - 1; + private trace: string[] = []; readonly device: HIDDevice; readonly profile: OtDeviceProfile; @@ -375,11 +376,40 @@ export class EggOtHidClient { } private async queryBlock(block: number, profileIndex: number): Promise { - const reply = await this.exchange(otEncodeQueryPacket(block, profileIndex), true); - if (!reply) throw new Error(`The mouse did not answer OT profile block ${block}.`); + let reply: Uint8Array | null; + try { + reply = await this.exchange(otEncodeQueryPacket(block, profileIndex), true); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`OT profile block ${block} failed: ${message} Trace: ${this.describeTrace()}`); + } + if (!reply) throw new Error(`The mouse did not answer OT profile block ${block}. Trace: ${this.describeTrace()}`); return reply.subarray(OT_PAYLOAD_INDEX); } + /** Packets seen by the last exchange (tx/rx hex, newest last) so a failed handshake reports what actually came back. */ + private describeTrace(): string { + return this.trace.length > 0 ? this.trace.join(" | ") : "no packets"; + } + + private record(direction: "tx" | "rx" | "err", detail: Uint8Array | string): void { + const text = typeof detail === "string" + ? detail + : Array.from(detail.subarray(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join(" "); + const entry = `${direction} ${text}`; + const last = this.trace[this.trace.length - 1]; + const repeat = last?.match(/^(.*) x(\d+)$/); + if (last === entry) { + this.trace[this.trace.length - 1] = `${entry} x2`; + } else if (repeat?.[1] === entry) { + this.trace[this.trace.length - 1] = `${entry} x${Number(repeat[2]) + 1}`; + } else { + if (this.trace.length >= 24) this.trace.shift(); + this.trace.push(entry); + } + console.log(`[egg-ot] ${direction} ${text}`); + } + private async updateProfile(profileIndex: number, change: (effect: OtEffect) => void): Promise { return this.run(async () => { const { effect } = await this.loadProfile(profileIndex); @@ -444,16 +474,28 @@ export class EggOtHidClient { private async exchange(packet: Uint8Array, expectResponse: boolean): Promise { await this.open(); const payload = Uint8Array.from(packet.subarray(1, 1 + this.packetPayloadLength)); + this.trace = []; let writeCounter = 0; while (writeCounter++ < OT_WRITE_RETRIES) { + this.record("tx", payload); const sent = (await this.device.sendFeatureReport(OT_REPORT_ID, payload)) as unknown as | number | undefined; - if (typeof sent === "number" && sent <= 0) continue; + if (typeof sent === "number" && sent <= 0) { + this.record("err", `write returned ${sent}`); + continue; + } let readCounter = 0; let receivedAck = false; while (readCounter++ < OT_READ_RETRIES) { - const reply = await this.receivePacket(); + let reply: Uint8Array; + try { + reply = await this.receivePacket(); + } catch (error) { + this.record("err", error instanceof Error ? error.message : String(error)); + throw error; + } + this.record("rx", reply); if (reply[1] === 0x00) continue; if (expectResponse) { if (reply[1] === OT_COMTYPE.ack) {