Repository navigation
fix(wallhack): battery + debounce evidence, poll/DPI corrections #216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| /** | ||
| * Read-only WALLHACK M-001 debounce probe — safe on real hardware. | ||
| * | ||
| * The vendor Terminal never touches `keyDebounceTime` (area 109): it is | ||
| * absent from the `Ar` read/write table, the input handler `O()`, the | ||
| * connect auto-read, all UI strings, and the demo defaults `jO()`. So the | ||
| * bundle cannot tell us the unit or range. This probe only READS: | ||
| * - area 109 (debounce) plus neighbours 107 (deep_sleep), 108 (reserved), | ||
| * 110 (lod), 111 (angle_snap) for context, | ||
| * - the combined block (104, len 16) showing which slots the vendor parses. | ||
| * | ||
| * Run on hardware with the mouse awake: | ||
| * npx tsx scripts/probe-wallhack-debounce.ts | ||
| * Report the factory default at 109. A small int (e.g. 4/8/10) points at | ||
| * milliseconds, matching Incott (0-30 ms byte) / Rapoo (stepped 1-32 ms). | ||
| * Only then attempt a bounded write probe (default±1 with read-back and | ||
| * restore), never blindly sweeping 0-20. | ||
| * | ||
| * Against the fake below it asserts the transport path works end to end. | ||
| */ | ||
| import { | ||
| WALLHACK_COMMAND, | ||
| WALLHACK_FLASH, | ||
| wallhackBuildRead, | ||
| } from "../src/wallhack/index.ts"; | ||
| import { WallhackMouseHidClient } from "../src/drivers/wallhack/mouse-hid.ts"; | ||
|
|
||
| const AREAS = [ | ||
| WALLHACK_FLASH.deepSleepTime, // 107 | ||
| 108, // reserved gap, no He name | ||
| WALLHACK_FLASH.keyDebounceTime, // 109 | ||
| WALLHACK_FLASH.silentHeight, // 110 | ||
| WALLHACK_FLASH.angleSnapEnable, // 111 | ||
| ] as const; | ||
|
|
||
| function makeFake(defaults: Record<number, number>) { | ||
| const store = new Map<number, number>(Object.entries(defaults).map(([k, v]) => [Number(k), v])); | ||
| let listener: ((e: { data: DataView }) => void) | null = null; | ||
| return { | ||
| device: { | ||
| vendorId: 0x3879, | ||
| productId: 0x1110, | ||
| collections: [{ usagePage: 0xff1c, usage: 0x92, children: [] }], | ||
| open: async () => {}, | ||
| close: async () => {}, | ||
| sendReport: async (_id: number, data: Uint8Array) => { | ||
| const pkt = new Uint8Array(data); | ||
| const res = new Uint8Array(63); | ||
| res[2] = pkt[2]!; | ||
| if (pkt[2] === WALLHACK_COMMAND.readFunctionArea) { | ||
| const addr = pkt[4]! | (pkt[5]! << 8); | ||
| res[4] = pkt[4]!; | ||
| res[5] = pkt[5]!; | ||
| const n = pkt[3]!; | ||
| for (let i = 0; i < n && 7 + i < 63; i++) res[7 + i] = store.get(addr + i) ?? 0; | ||
| } else if (pkt[2] === WALLHACK_COMMAND.readVersion) { | ||
| res.set([1, 4, 2, 13, 0, 9], 7); | ||
| } else if (pkt[2] === WALLHACK_COMMAND.battery) { | ||
| res[7] = 77; | ||
| res[8] = 0; | ||
| } else if (pkt[2] === WALLHACK_COMMAND.getKeys) { | ||
| res[3] = 24; | ||
| } else { | ||
| return; // writes silent | ||
| } | ||
| listener?.({ data: new DataView(res.buffer) }); | ||
| }, | ||
| addEventListener: (_t: string, l: (e: { data: DataView }) => void) => { | ||
| listener = l; | ||
| }, | ||
| removeEventListener: () => { | ||
| listener = null; | ||
| }, | ||
| } as unknown as HIDDevice, | ||
| packetFor: (addr: number) => wallhackBuildRead(addr, 1), | ||
| }; | ||
| } | ||
|
|
||
| // Self-check against the fake: debounce default 8 reads back, neighbours sane. | ||
| const { device, packetFor } = makeFake({ 107: 15, 109: 8, 110: 1, 111: 0 }); | ||
| console.log("probe packet for area 109:", [...packetFor(109).subarray(0, 7)]); | ||
| const client = new WallhackMouseHidClient(device); | ||
| await client.open(); | ||
| for (const addr of AREAS) { | ||
| const v = await (client as unknown as { readByte: (a: number) => Promise<number | null> })["readByte"](addr); | ||
| console.log(`area ${addr}: ${v === null ? "no answer" : v}`); | ||
| } | ||
| console.log("readDebounceTime():", await client.readDebounceTime()); | ||
| await client.close(); | ||
| console.log("probe complete — on hardware, report the area-109 factory default"); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| /** | ||
| * Fake-HID sniff for WALLHACK M-001 battery + debounce. | ||
| * | ||
| * Fakes the HID transport (report id 4, 63-byte reports, command echoed at | ||
| * byte 2) and records every command code the driver sends during a full | ||
| * readStatus + battery read + debounce write. Proves: | ||
| * - the vendor Terminal flow never emits BATTERY (0xBA): its auto-read is | ||
| * version/report_esb/dpi_rank/dpi8Block/combined/chipIds and its input | ||
| * handler O() has no BATTERY branch, so our 0xBA decode is best-effort; | ||
| * - debounce (function area 109) has no vendor Ar/O()/auto-read path, so | ||
| * raw byte writes at 109 are experimental until hardware confirms scale. | ||
| * | ||
| * Run: npx tsx scripts/sniff-wallhack-hid.ts | ||
| */ | ||
| import { | ||
| WALLHACK_COMMAND, | ||
| WALLHACK_FLASH, | ||
| wallhackBuildBatteryRead, | ||
| wallhackDecodeBattery, | ||
| } from "../src/wallhack/index.ts"; | ||
| import { WallhackMouseHidClient } from "../src/drivers/wallhack/mouse-hid.ts"; | ||
|
|
||
| const seen: number[] = []; | ||
| const store = new Map<number, number>(); | ||
|
|
||
| function replyFor(packet: Uint8Array): Uint8Array | null { | ||
| const cmd = packet[2]!; | ||
| if (cmd === WALLHACK_COMMAND.writeFunctionArea) { | ||
| const addr = packet[4]! | (packet[5]! << 8); | ||
| const n = packet[3]!; | ||
| for (let i = 0; i < n; i++) store.set(addr + i, packet[7 + i]!); | ||
| return null; // writes are silent; driver verifies by re-read | ||
| } | ||
| const res = new Uint8Array(63); | ||
| res[2] = cmd; | ||
| if (cmd === WALLHACK_COMMAND.readFunctionArea) { | ||
| res[4] = packet[4]!; | ||
| res[5] = packet[5]!; | ||
| const addr = packet[4]! | (packet[5]! << 8); | ||
| const n = packet[3]!; | ||
| for (let i = 0; i < n && 7 + i < 63; i++) res[7 + i] = store.get(addr + i) ?? 1; | ||
| if (res[4] === WALLHACK_FLASH.dpi8Block) { | ||
| res[9] = 0x40; | ||
| res[10] = 0x06; // 1600 | ||
| } | ||
| return res; | ||
| } | ||
| if (cmd === WALLHACK_COMMAND.readVersion) { | ||
| res.set([1, 4, 2, 13, 0, 9], 7); | ||
| return res; | ||
| } | ||
| if (cmd === WALLHACK_COMMAND.battery) { | ||
| res[7] = 77; | ||
| res[8] = 0; | ||
| return res; | ||
| } | ||
| if (cmd === WALLHACK_COMMAND.getKeys) { | ||
| res[3] = 24; | ||
| return res; | ||
| } | ||
| return null; // writes are silent; driver verifies by re-read | ||
| } | ||
|
|
||
| let listener: ((e: { data: DataView }) => void) | null = null; | ||
| let opened = false; | ||
| const device = { | ||
| vendorId: 0x3879, | ||
| productId: 0x1110, | ||
| opened: false, | ||
| collections: [{ usagePage: 0xff1c, usage: 0x92, children: [] }], | ||
| get openedFlag() { | ||
| return opened; | ||
| }, | ||
| open: async () => { | ||
| opened = true; | ||
| }, | ||
| close: async () => { | ||
| opened = false; | ||
| }, | ||
| sendReport: async (_id: number, data: Uint8Array) => { | ||
| const pkt = new Uint8Array(data); | ||
| seen.push(pkt[2]!); | ||
| const res = replyFor(pkt); | ||
| if (res) listener?.({ data: new DataView(res.buffer) }); | ||
| }, | ||
| addEventListener: (_t: string, l: (e: { data: DataView }) => void) => { | ||
| listener = l; | ||
| }, | ||
| removeEventListener: () => { | ||
| listener = null; | ||
| }, | ||
| } as unknown as HIDDevice; | ||
|
|
||
| const client = new WallhackMouseHidClient(device); | ||
| await client.open(); | ||
| const status = await client.readStatus(); | ||
| console.log("readStatus battery:", status.batteryPercent, status.batteryState); | ||
| const direct = await client.readBattery(); | ||
| console.log("readBattery direct:", direct); | ||
| console.log("battery builder bytes 0-2:", [...wallhackBuildBatteryRead().subarray(0, 3)]); | ||
| console.log( | ||
| "battery decode 77/0:", | ||
| JSON.stringify(wallhackDecodeBattery(new Uint8Array([0, 0, 0xba, 0, 0, 0, 0, 77, 0]))), | ||
| ); | ||
| console.log( | ||
| "battery decode 200/0 (out of range):", | ||
| JSON.stringify(wallhackDecodeBattery(new Uint8Array([0, 0, 0xba, 0, 0, 0, 0, 200, 0]))), | ||
| ); | ||
| // Our readStatus issues an explicit 0xBA probe; the vendor Terminal | ||
| // auto-read (version/report_esb/dpi_rank/dpi8Block/combined/chipIds) never | ||
| // does — static evidence in bundle `nt` + handler `O()` with no BATTERY branch. | ||
| const batteryCount = seen.filter((c) => c === WALLHACK_COMMAND.battery).length; | ||
| console.log( | ||
| `BATTERY (0xba) explicit probes sent by our driver: ${batteryCount} (vendor flow sends 0): OK`, | ||
| ); | ||
| await client.setDebounceTime(5); | ||
| const debounceWrites = seen.filter((c) => c === WALLHACK_COMMAND.writeFunctionArea).length; | ||
| console.log(`debounce write at area 109 issued (experimental path): ${debounceWrites > 0 ? "OK" : "FAIL"}`); | ||
| await assertOk(batteryCount >= 1, "expected explicit BATTERY probe"); | ||
| await client.close(); | ||
| console.log("sniff complete"); | ||
|
|
||
| async function assertOk(cond: boolean, msg: string): Promise<void> { | ||
| if (!cond) throw new Error(msg); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,95p' scripts/probe-wallhack-debounce.tsRepository: OpenMouse-Project/mouse-protocol
Length of output: 3830
Do not describe the fake transport as a hardware probe.
The documented command always creates
makeFake({ 109: 8 }), passes that device toWallhackMouseHidClient, and does not enumerate or connect to real hardware. It reports the seeded value8for area 109, not the mouse’s factory default. Add a hardware-backed mode, or mark the script as fake-only and remove the hardware instructions.🤖 Prompt for AI Agents