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],