From e4c428cf3a0b7a9ca7124a44e929cefabe59c8be Mon Sep 17 00:00:00 2001 From: OpenMouse Bot Date: Fri, 30 Oct 2026 21:34:33 -0600 Subject: [PATCH] fix(arbiter): move Polar 65+ 0xA009 to the live 0xFF02 collection #11 shipped the 0xA009 row on 0xFF01, but the board's 0xFF01 collection answers nothing: after the release the reporter could claim the board but getFirmware timed out. The live vendor app (app2.arbiterstudio.com) pairs product 40969 strictly with usagePage 65282 and only claims devices exposing it, and a USB capture shows all vendor traffic on interface 3 (endpoints 0x04/0x84) while interface 2 is silent. Move the row to 0xFF02 and pin it in tests. Also notes the second Polar 65+ variant 0xA010 found in the vendor bundle (no capture yet, no row). --- docs/arbiter-support.md | 20 ++++++++++++++------ src/arbiter/index.ts | 12 +++++++++--- src/arbiter/protocol.test.ts | 4 ++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/arbiter-support.md b/docs/arbiter-support.md index 12cd7e6..f192294 100644 --- a/docs/arbiter-support.md +++ b/docs/arbiter-support.md @@ -37,14 +37,22 @@ differs between models (see [Protocol variants](#protocol-variants)). | Polar 65 - Gaming Keyboard | `0x371C` | `0xA005` | `0xFF02` | Main | 1 | yes | `yl` | | Arbiter Polar 65 - Gaming Keyboard | `0x371C` | `0xA006` | `0xFF01` | Main | 1 | yes | `s2` | | Polar 65 PRO - Gaming Keyboard | `0x371C` | `0xA003` | `0xFF02` | Main, FN | 3 | yes | `i2` | -| Polar 65+ - Gaming Keyboard | `0x371C` | `0xA009` | `0xFF01` | Main, FN | 3 | yes | `i2` | +| Polar 65+ - Gaming Keyboard | `0x371C` | `0xA009` | `0xFF02` | Main, FN | 3 | yes | `i2` | | Polar 75 PRO - Gaming Keyboard | `0x371C` | `0xA001` | `0xFF02` | Main, FN | 3 | yes | `u2` | -`0x371C:0xA009` is not in the vendor bundle. Its row comes from a USB -capture of the vendor app driving the board (travel writes total 3 with -29+29+10 entries, key-mapping writes total 5 with 29x4+20 = 136 entries, -`SetBoardProfile` values 0-2, `Get/SetUserMode` traffic), matching the `i2` -class. It is `verified: false` like every other entry. +`0x371C:0xA009` is not in the vendor bundle. Its row comes from the live +vendor app (app2.arbiterstudio.com, bundle retrieved 2026-10-31), which +pairs product 40969 strictly with usage page 65282 (`0xFF02`) and only +claims devices whose collections expose that page — plus a USB capture of +that app driving the board (travel writes total 3 with 29+29+10 entries, +key-mapping writes total 5 with 29x4+20 = 136 entries, `SetBoardProfile` +values 0-2, `Get/SetUserMode` traffic), matching the `i2` class. The +board also exposes an `0xFF01` collection, but it answers nothing, so the +row lists `0xFF02` only. It is `verified: false` like every other entry. +The same bundle lists a second Polar 65+ variant, product 40976 +(`0x371C:0xA010`), also on `0xFF02` with Main+FN, 3 profiles and SuperTap +but a different protocol class — no capture exists for it yet, so it has +no row. Only `0x1A81:0x2072` is defined on two usage pages. Whether the other PIDs also expose a second page is unknown. Discovery filters must pair each PID diff --git a/src/arbiter/index.ts b/src/arbiter/index.ts index 70646b4..b8f7e49 100644 --- a/src/arbiter/index.ts +++ b/src/arbiter/index.ts @@ -110,12 +110,18 @@ export const ARBITER_PRODUCTS: readonly ArbiterProduct[] = [ product("Polar 65", ARBITER_VENDOR_ID_ALT, 0xa005, 0xff02, "yl", 68, 68, 1, 1, true), product("Arbiter Polar 65", ARBITER_VENDOR_ID_ALT, 0xa006, 0xff01, "s2", 69, 69, 1, 1, true), product("Polar 65 PRO", ARBITER_VENDOR_ID_ALT, 0xa003, 0xff02, "i2", 68, 68, 2, 3, true), - // 0xA009 ("Polar 65+") is not in the vendor bundle analysed 2026-10-30. - // Row inferred from a USB capture of the vendor app driving the board: + // 0xA009 ("Polar 65+") is not in the vendor bundle analysed 2026-10-30, + // but the current vendor app (app2.arbiterstudio.com) pairs product + // 40969 strictly with usagePage 65282 (0xFF02) and only claims devices + // whose collections expose that page. The board also exposes an 0xFF01 + // collection, but driving it gets no reply (see #9); the live + // configuration interface is the 0xFF02 one, on USB interface 3 + // (all vendor-app traffic flows over endpoints 0x04/0x84, interface 2 + // is silent). Wire sizes come from a USB capture of the vendor app: // travel writes total 3 (29+29+10 = 68), key-mapping writes total 5 // (29x4+20 = 136 = 2 layers x 68), SetBoardProfile values 0-2, and // Get/SetUserMode traffic, matching the i2 (65 PRO) class. - product("Polar 65+", ARBITER_VENDOR_ID_ALT, 0xa009, 0xff01, "i2", 68, 68, 2, 3, true), + product("Polar 65+", ARBITER_VENDOR_ID_ALT, 0xa009, 0xff02, "i2", 68, 68, 2, 3, true), product("Polar 75 PRO", ARBITER_VENDOR_ID_ALT, 0xa001, 0xff02, "u2", 82, 81, 2, 3, true), ]; diff --git a/src/arbiter/protocol.test.ts b/src/arbiter/protocol.test.ts index c0d49e2..ad7c561 100644 --- a/src/arbiter/protocol.test.ts +++ b/src/arbiter/protocol.test.ts @@ -162,6 +162,10 @@ test("the catalog matches the vendor bundle and is entirely unverified", () => { assert.equal(arbiterProduct(0x1a81, 0x2072, 0xff02)?.superTap, true); assert.equal(arbiterProduct(0x371c, 0xa006, 0xff01)?.protocol, "s2"); assert.equal(arbiterProduct(0x371c, 0xa003, 0xff01), undefined); + // 0xA009 lives on 0xFF02 only: that is the page the vendor app claims, + // and the board's 0xFF01 collection answers nothing. + assert.equal(arbiterProduct(0x371c, 0xa009, 0xff02)?.protocol, "i2"); + assert.equal(arbiterProduct(0x371c, 0xa009, 0xff01), undefined); assert.deepEqual(ARBITER_PRODUCTS.map((entry) => [entry.name, entry.travelKeys, entry.bindingKeys, arbiterTravelPackets(entry), arbiterKeyMappingPackets(entry)]), [ ["Arbiter Polar 65", 68, 68, 3, 3], ["Arbiter Polar 65", 68, 68, 3, 3], ["Polar 65", 68, 68, 3, 3], ["Nimbus 65", 68, 68, 3, 3], ["Polar 65", 68, 68, 3, 3], ["Arbiter Polar 65", 69, 69, 3, 3], ["Polar 65 PRO", 68, 68, 3, 5], ["Polar 65+", 68, 68, 3, 5], ["Polar 75 PRO", 82, 81, 3, 6],