From 8f2c742ed730ac38bca06e4f0fd3a1a619c4f2e6 Mon Sep 17 00:00:00 2001 From: snekxs Date: Sat, 17 Oct 2026 15:13:17 -0600 Subject: [PATCH] fix(endgame): reopen the device and retry once when a feature-report write fails A handle left stale by sleep/resume, a USB hiccup or an earlier page rejects every write with "Failed to write the feature report" until the mouse is replugged, so reconnecting kept failing. The OP1 command and config writes and the OP1we command write now close the device, reopen it and retry once before surfacing the error. Co-Authored-By: Claude Sonnet 5.5 --- src/drivers/endgame/egg-op1-hid.test.ts | 30 +++++++++++++++++++++ src/drivers/endgame/egg-op1-hid.ts | 19 +++++++++++-- src/drivers/endgame/egg-we-hid.ts | 19 ++++++++++++- src/drivers/endgame/egg-we-protocol.test.ts | 29 ++++++++++++++++++++ 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/drivers/endgame/egg-op1-hid.test.ts b/src/drivers/endgame/egg-op1-hid.test.ts index 3745a9fc..bdc01c0a 100644 --- a/src/drivers/endgame/egg-op1-hid.test.ts +++ b/src/drivers/endgame/egg-op1-hid.test.ts @@ -237,3 +237,33 @@ test("writes over the shared dongle wait until the paired mouse identifies itsel sleepy.state.pairedPid = 0x1984; assert.equal(await client.setDpi(800), 800); }); + +test("a stale handle that rejects the first write is closed, reopened and retried once", async () => { + const mouse = fake4kV2(); + const device = mouse.device as unknown as { + opened: boolean; + open(): Promise; + close(): Promise; + sendFeatureReport(reportId: number, data: BufferSource): Promise; + }; + const send = device.sendFeatureReport.bind(device); + let stale = true; + const calls: string[] = []; + device.open = async () => { calls.push("open"); device.opened = true; stale = false; }; + device.close = async () => { calls.push("close"); device.opened = false; }; + device.sendFeatureReport = async (reportId, data) => { + if (stale) throw new DOMException("Failed to write the feature report.", "NetworkError"); + return send(reportId, data); + }; + + const status = await new EggOp1HidClient(mouse.device).readStatus(); + assert.equal(status.firmware[0], "Firmware V1.07"); + assert.deepEqual(calls.slice(0, 2), ["close", "open"]); +}); + +test("a write that still fails after the reopen surfaces the error", async () => { + const mouse = fake4kV2(); + const device = mouse.device as unknown as { sendFeatureReport(): Promise }; + device.sendFeatureReport = async () => { throw new Error("Failed to write the feature report."); }; + await assert.rejects(new EggOp1HidClient(mouse.device).readStatus(), /Failed to write the feature report/); +}); diff --git a/src/drivers/endgame/egg-op1-hid.ts b/src/drivers/endgame/egg-op1-hid.ts index 6655bbca..ba8e35c8 100644 --- a/src/drivers/endgame/egg-op1-hid.ts +++ b/src/drivers/endgame/egg-op1-hid.ts @@ -637,7 +637,7 @@ export class EggOp1HidClient { let lastError: unknown; for (let attempt = 0; attempt < 4; attempt += 1) { try { - await this.device.sendFeatureReport(EGG_REPORT.config, payload); + await this.sendFeatureRecovering(EGG_REPORT.config, payload); lastError = undefined; break; } catch (error) { @@ -715,7 +715,22 @@ export class EggOp1HidClient { command.set([operation, ...header]); // Payload sits at wire offset 16, i.e. 15 once the report ID is stripped. if (payload) command.set(payload, 15); - await this.device.sendFeatureReport(EGG_REPORT.command, command); + await this.sendFeatureRecovering(EGG_REPORT.command, command); + } + + /** + * A handle left stale (sleep/resume, USB hiccup, an earlier page) rejects + * every write with "Failed to write the feature report" until the device is + * re-enumerated. Close and reopen it once and retry before giving up. + */ + private async sendFeatureRecovering(reportId: number, data: BufferSource): Promise { + try { + await this.device.sendFeatureReport(reportId, data); + } catch { + await this.device.close().catch(() => undefined); + await this.open(); + await this.device.sendFeatureReport(reportId, data); + } } private async pollCommandOk(attempts: number): Promise { diff --git a/src/drivers/endgame/egg-we-hid.ts b/src/drivers/endgame/egg-we-hid.ts index 85dede14..f44d48f2 100644 --- a/src/drivers/endgame/egg-we-hid.ts +++ b/src/drivers/endgame/egg-we-hid.ts @@ -505,6 +505,23 @@ export class EggWeHidClient { waiter.reject(error); } + /** + * A stale handle (sleep/resume, USB hiccup) rejects every write until the + * device is re-enumerated. Close the command channel, reopen and retry once. + */ + private async sendReportRecovering(channels: WeChannels, data: ArrayBuffer): Promise { + try { + await channels.cmd.sendFeatureReport(WE_REPORT_ID, data); + } catch { + await channels.cmd.close().catch(() => undefined); + await this.open(); + const fresh = this.resolvedChannels() ?? channels; + fresh.notify.removeEventListener("inputreport", this.onInputReport); + fresh.notify.addEventListener("inputreport", this.onInputReport); + await fresh.cmd.sendFeatureReport(WE_REPORT_ID, data); + } + } + private async transact( channels: WeChannels, command: number, @@ -529,7 +546,7 @@ export class EggWeHidClient { // Ensure ArrayBuffer-backed view for WebHID typings. const copy = new Uint8Array(payload); try { - await channels.cmd.sendFeatureReport(WE_REPORT_ID, copy.buffer); + await this.sendReportRecovering(channels, copy.buffer); } catch (error) { this.failWaiter(error instanceof Error ? error : new Error(String(error))); } diff --git a/src/drivers/endgame/egg-we-protocol.test.ts b/src/drivers/endgame/egg-we-protocol.test.ts index af7ac1ec..f8b4ce78 100644 --- a/src/drivers/endgame/egg-we-protocol.test.ts +++ b/src/drivers/endgame/egg-we-protocol.test.ts @@ -150,3 +150,32 @@ test("DPI patch refuses corrupt source stages", () => { const profile = new Uint8Array(WE_OFF.dpiStages + 4); assert.throws(() => wePatchActiveDpi(profile, 800, 0), /checksum is invalid/); }); + +test("a stale command channel is closed, reopened and the OP1we write retried once", async () => { + let stale = true; + const calls: string[] = []; + const device = { + vendorId: 0x3367, + productId: 0x1962, + productName: "OP1we", + opened: true, + collections: [ + { usagePage: 0xff02, usage: 0, featureReports: [{ reportId: WE_REPORT_ID, items: [] }], inputReports: [], outputReports: [], children: [] }, + { usagePage: 0xff01, usage: 0, featureReports: [], inputReports: [{ reportId: 0x09, items: [] }], outputReports: [], children: [] }, + ], + async open() { calls.push("open"); this.opened = true; stale = false; }, + async close() { calls.push("close"); this.opened = false; }, + addEventListener() {}, + removeEventListener() {}, + async sendFeatureReport() { + if (stale) throw new DOMException("Failed to write the feature report.", "NetworkError"); + calls.push("sent"); + }, + } as unknown as HIDDevice; + const client = new EggWeHidClient(device); + const send = (client as unknown as { + sendReportRecovering(channels: { cmd: HIDDevice; notify: HIDDevice }, data: ArrayBuffer): Promise; + }).sendReportRecovering.bind(client); + await send({ cmd: device, notify: device }, new ArrayBuffer(16)); + assert.deepEqual(calls, ["close", "open", "sent"]); +});