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
20 changes: 14 additions & 6 deletions docs/arbiter-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions src/arbiter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
];

Expand Down
4 changes: 4 additions & 0 deletions src/arbiter/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
Loading