Skip to content
Merged
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
22 changes: 22 additions & 0 deletions src/drivers/endgame/egg-ot-hid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>;
receiveFeatureReport: () => Promise<DataView>;
};
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+/,
);
});
50 changes: 46 additions & 4 deletions src/drivers/endgame/egg-ot-hid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export class EggOtHidClient {
private chain: Promise<unknown> = Promise.resolve();
private firmwareRaw: number[] | null | undefined;
private packetPayloadLength = OT_PACKET_SIZE - 1;
private trace: string[] = [];

readonly device: HIDDevice;
readonly profile: OtDeviceProfile;
Expand Down Expand Up @@ -375,11 +376,40 @@ export class EggOtHidClient {
}

private async queryBlock(block: number, profileIndex: number): Promise<Uint8Array> {
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<OtEffect> {
return this.run(async () => {
const { effect } = await this.loadProfile(profileIndex);
Expand Down Expand Up @@ -444,16 +474,28 @@ export class EggOtHidClient {
private async exchange(packet: Uint8Array, expectResponse: boolean): Promise<Uint8Array | null> {
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) {
Expand Down
Loading