From c4c2f210a06cce5a974f0c67175cbd987e0cde29 Mon Sep 17 00:00:00 2001 From: snekxs Date: Sat, 17 Oct 2026 01:03:36 -0600 Subject: [PATCH 1/2] feat(endgame): trace raw OT packets so failed handshakes show what came back exchange() records tx/rx packets (collapsing repeats), logs them to the console, and queryBlock() appends the trace to its error. Refs #237 Co-Authored-By: Claude Sonnet 5.5 --- src/drivers/endgame/egg-ot-hid.test.ts | 22 ++++++++++++++ src/drivers/endgame/egg-ot-hid.ts | 41 +++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/drivers/endgame/egg-ot-hid.test.ts b/src/drivers/endgame/egg-ot-hid.test.ts index 93147cc9..f3354756 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 .*| rx 00 14 .*| rx 00 00 .* xd+/, + ); +}); diff --git a/src/drivers/endgame/egg-ot-hid.ts b/src/drivers/endgame/egg-ot-hid.ts index 16041564..475a0125 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,31 @@ 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(" "); + if (this.trace.length >= 24) this.trace.shift(); + this.trace.push(`${direction} ${text}`); + 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 +465,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) { From 7983f9e53a3413da3189204fec67089a464f0867 Mon Sep 17 00:00:00 2001 From: snekxs Date: Sat, 17 Oct 2026 01:06:15 -0600 Subject: [PATCH 2/2] fix(endgame): collapse repeated OT trace entries and tighten trace test Addresses CodeRabbit review on #239. Co-Authored-By: Claude Sonnet 5.5 --- src/drivers/endgame/egg-ot-hid.test.ts | 2 +- src/drivers/endgame/egg-ot-hid.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/drivers/endgame/egg-ot-hid.test.ts b/src/drivers/endgame/egg-ot-hid.test.ts index f3354756..b2f9a7b4 100644 --- a/src/drivers/endgame/egg-ot-hid.test.ts +++ b/src/drivers/endgame/egg-ot-hid.test.ts @@ -306,6 +306,6 @@ test("a mouse that never sends the response packet reports the raw tx/rx trace", const client = new EggOtHidClient(fake.device); await assert.rejects( () => client.readStatus(), - /did not answer OT profile block 0. Trace: tx 33 .*| rx 00 14 .*| rx 00 00 .* xd+/, + /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 475a0125..5c10bbd4 100644 --- a/src/drivers/endgame/egg-ot-hid.ts +++ b/src/drivers/endgame/egg-ot-hid.ts @@ -396,8 +396,17 @@ export class EggOtHidClient { const text = typeof detail === "string" ? detail : Array.from(detail.subarray(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join(" "); - if (this.trace.length >= 24) this.trace.shift(); - this.trace.push(`${direction} ${text}`); + 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}`); }