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
7 changes: 7 additions & 0 deletions docs/arbiter-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,15 @@ 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 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.

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
with its own usage page, as `src/drivers/vendors.ts` does for Wuque.
Expand Down
6 changes: 6 additions & 0 deletions src/arbiter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ 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:
// 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 75 PRO", ARBITER_VENDOR_ID_ALT, 0xa001, 0xff02, "u2", 82, 81, 2, 3, true),
];

Expand Down
8 changes: 4 additions & 4 deletions src/arbiter/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,18 @@ test("write acknowledgements read the flag and tolerate a short body", () => {
});

test("the catalog matches the vendor bundle and is entirely unverified", () => {
assert.equal(ARBITER_PRODUCTS.length, 8);
assert.equal(ARBITER_PRODUCTS.length, 9);
assert.ok(ARBITER_PRODUCTS.every((entry) => !entry.verified));
assert.equal(arbiterProduct(0x1a81, 0x2072, 0xff01)?.superTap, false);
assert.equal(arbiterProduct(0x1a81, 0x2072, 0xff02)?.superTap, true);
assert.equal(arbiterProduct(0x371c, 0xa006, 0xff01)?.protocol, "s2");
assert.equal(arbiterProduct(0x371c, 0xa003, 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 75 PRO", 82, 81, 3, 6],
["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],
]);
assert.deepEqual(ARBITER_PRODUCTS.filter(arbiterIsPro).map((entry) => [entry.name, entry.layers, entry.profiles]),
[["Polar 65 PRO", 2, 3], ["Polar 75 PRO", 2, 3]]);
[["Polar 65 PRO", 2, 3], ["Polar 65+", 2, 3], ["Polar 75 PRO", 2, 3]]);
});

test("layouts match the catalog and the factory main layer is the matrix", () => {
Expand All @@ -177,7 +177,7 @@ test("layouts match the catalog and the factory main layer is the matrix", () =>
for (const layer of layout.layers) assert.equal(layer.length, layout.matrix.length);
assert.deepEqual(layout.layers[0], layout.matrix.map((key) => key.keyCode));
}
assert.deepEqual(ARBITER_PRODUCTS.map((entry) => arbiterLayout(entry).matrix.length), [68, 68, 68, 68, 68, 70, 68, 82]);
assert.deepEqual(ARBITER_PRODUCTS.map((entry) => arbiterLayout(entry).matrix.length), [68, 68, 68, 68, 68, 70, 68, 68, 82]);
});

test("wire entries map to matrix keys without landing on a placeholder", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/drivers/arbiter/hid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ test("registry and filters claim only catalogued vendor, product and usage page
const device = new FakeArbiterKeyboard();
assert.ok(createSupportedClient(device.hid) instanceof ArbiterKeyboardHidClient);
assert.equal(deviceBrand(new ArbiterKeyboardHidClient(device.hid)), "Arbiter Studio");
assert.equal(ARBITER_HID_FILTERS.length, 8);
assert.equal(ARBITER_HID_FILTERS.length, 9);
assert.deepEqual(ARBITER_HID_FILTERS[0], { vendorId: 0x1a81, productId: 0x2072, usagePage: 0xff01, usage: 1 });
assert.equal(VENDOR_ID.arbiter, 0x1a81);
assert.equal(VENDOR_ID.arbiterAlt, 0x371c);
Expand Down
Loading