diff --git a/docs/arbiter-support.md b/docs/arbiter-support.md index bc28145..12cd7e6 100644 --- a/docs/arbiter-support.md +++ b/docs/arbiter-support.md @@ -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. diff --git a/src/arbiter/index.ts b/src/arbiter/index.ts index 8b968d5..70646b4 100644 --- a/src/arbiter/index.ts +++ b/src/arbiter/index.ts @@ -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), ]; diff --git a/src/arbiter/protocol.test.ts b/src/arbiter/protocol.test.ts index ff4b185..c0d49e2 100644 --- a/src/arbiter/protocol.test.ts +++ b/src/arbiter/protocol.test.ts @@ -156,7 +156,7 @@ 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); @@ -164,10 +164,10 @@ test("the catalog matches the vendor bundle and is entirely unverified", () => { 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", () => { @@ -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", () => { diff --git a/src/drivers/arbiter/hid.test.ts b/src/drivers/arbiter/hid.test.ts index 31cb6a7..ac397d2 100644 --- a/src/drivers/arbiter/hid.test.ts +++ b/src/drivers/arbiter/hid.test.ts @@ -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);