Skip to content
Open
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
138 changes: 138 additions & 0 deletions src/drivers/pulsar/model-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Source: https://bbb.pulsar.gg/cMouse/devicename.json (2026-11-12).
// Source SHA-256: b0470ebf70f0ffbb2eed597f92cc381fc5eb5c2d7924560c2969d0fc6debe8e9
// Keys combine CID and MID: (cid << 8) | mid. Vendor arrays use MID - 1.
// Generic "Pulsar device" placeholders are omitted. Names do not grant support.
export const PULSAR_MODEL_NAMES: ReadonlyMap<number, string> = new Map([
[0x5701, "Pulsar X2 CrazyLight Mini"],
[0x5702, "Pulsar X2 CrazyLight Mini"],
[0x5703, "Pulsar X2 CrazyLight Mini"],
[0x5704, "Pulsar X2 CrazyLight Mini"],
[0x5705, "Pulsar X2 CrazyLight Mini"],
[0x5706, "Pulsar X2 CrazyLight Mini"],
[0x5707, "Pulsar TenZ Signature Edition"],
[0x5708, "Pulsar TenZ"],
[0x5709, "Pulsar X2 CrazyLight Mini"],
[0x570a, "Pulsar X2 CrazyLight Mini"],
[0x570b, "Pulsar X VSPO! Aizawa Ema"],
[0x570c, "Pulsar X2 CrazyLight Mini T1 Edition"],
[0x570d, "Pulsar X2 CrazyLight Mini T1 Edition"],
[0x570e, "Pulsar X2 CrazyLight Mini PRX Edition"],
[0x570f, "Pulsar X2 CrazyLight Mini Boardzy Edition"],
[0x5710, "Pulsar X2 CrazyLight Mini Randomfrankp Edition"],
[0x5711, "Pulsar Xlite CrazyLight Mini"],
[0x5712, "Pulsar Xlite CrazyLight Mini"],
[0x5713, "Pulsar X3 CrazyLight Mini"],
[0x5714, "Pulsar X3 CrazyLight Mini"],
[0x5715, "Pulsar X3 LHD CrazyLight Mini"],
[0x5716, "Pulsar X3 LHD CrazyLight Mini"],
[0x5717, "Pulsar X2H CrazyLight Mini"],
[0x5718, "Pulsar X2H CrazyLight Mini"],
[0x5719, "Pulsar X2N CrazyLight Mini"],
[0x571a, "Pulsar X2N CrazyLight Mini"],
[0x571b, "Pulsar X2 CrazyLight Medium"],
[0x571c, "Pulsar X2 Mini Pulsar By You XS-1"],
[0x571d, "Pulsar X2H CrazyLight Medium"],
[0x571e, "Pulsar X2H Medium Pulsar By You XS-1"],
[0x571f, "Pulsar X2A Medium Pulsar By You XS-1"],
[0x5720, "Pulsar X2A Mini Pulsar By You XS-1"],
[0x5721, "Pulsar Xlite Mini Pulsar By You XS-1"],
[0x5722, "Pulsar Xlite CrazyLight Medium"],
[0x5723, "Pulsar Xlite Large Pulsar By You XS-1"],
[0x5724, "Pulsar Lab. X2F"],
[0x5725, "Pulsar TenZ Signature RED"],
[0x5726, "Pulsar JV-X Pro Series"],
[0x5727, "Pulsar Susanto-X Pro Series"],
[0x5728, "Pulsar TenZ Signature Edition"],
[0x5729, "Pulsar X2 CrazyLight 5th Anniversary Edition"],
[0x572a, "Pulsar X2 CrazyLight WildScape Desert Edition"],
[0x572b, "Pulsar X2 CrazyLight Mini Forest"],
[0x572c, "Pulsar X2 CrazyLight Mini WildScape Ocean Edition"],
[0x572d, "Pulsar X VSPO! Asumi Sena"],
[0x572e, "Pulsar X VSPO! Hanabusa Lisa"],
[0x572f, "Pulsar X VSPO! Ichinose Uruha"],
[0x5730, "Pulsar X VSPO! Kaga Sumire"],
[0x5731, "Pulsar X VSPO! Kisaragi Ren"],
[0x5732, "Pulsar X VSPO! Kogara Toto"],
[0x5733, "Pulsar X VSPO! Komori Met"],
[0x5734, "Pulsar X VSPO! Kurumi Noah"],
[0x5735, "Pulsar X VSPO! Kaga Nazuna"],
[0x5736, "Pulsar X VSPO! Nekota Tsuna"],
[0x5737, "Pulsar X VSPO! Kaminari Qpi"],
[0x5738, "Pulsar X VSPO! Sendo Yuuhi"],
[0x5739, "Pulsar X VSPO! Shinomiya Runa"],
[0x573a, "Pulsar X VSPO! Shiranami Ramune"],
[0x573b, "Pulsar X VSPO! Tachibana Hinano"],
[0x573c, "Pulsar X VSPO! Tosaki Mimi"],
[0x573d, "Pulsar X VSPO! Tsumugi Kokage"],
[0x573e, "Pulsar X VSPO! Yakumo Beni"],
[0x573f, "Pulsar X VSPO! Yano Kuromu"],
[0x5740, "Pulsar X VSPO! Yumeno Akari"],
[0x5741, "Pulsar X VSPO! Amayui Moka"],
[0x5742, "Pulsar X VSPO! Choya Hanabi"],
[0x5743, "Pulsar Lab. X2F 5th Anniversary Edition"],
[0x5744, "Pulsar ZywOo The Chosen Mouse Gen.2"],
[0x5745, "Pulsar ZywOo The Chosen Mouse Gen.2"],
[0x5746, "Pulsar X2 CrazyLight Medium"],
[0x5747, "Pulsar Xlite CrazyLight Medium"],
[0x5748, "Pulsar X3 CrazyLight Medium"],
[0x5749, "Pulsar X3 CrazyLight Medium"],
[0x574a, "Pulsar X3 LHD CrazyLight Medium"],
[0x574b, "Pulsar X3 LHD CrazyLight Medium"],
[0x574c, "Pulsar X2H CrazyLight Medium"],
[0x574d, "Pulsar X2N CrazyLight Medium"],
[0x574e, "Pulsar X2N CrazyLight Medium"],
[0x574f, "Pulsar X2 CrazyLight Medium The Finals Edition"],
[0x5750, "Pulsar X2 CrazyLight Medium THE GRAND MAJOR 2025 Edition"],
[0x5751, "Pulsar X2 CrazyLight Medium Bruce Lee 85th Edition"],
[0x5752, "Pulsar X2 CrazyLight Mini Bruce Lee 85th Edition"],
[0x5753, "Pulsar X2 CrazyLight Medium PRX Pacific Gold Edition"],
[0x5754, "Pulsar X2 CrazyLight Medium Pokemon Pikachu Edition"],
[0x5755, "Pulsar X2 CrazyLight Mini Pokemon Pikachu Edition"],
[0x5756, "Pulsar X2 CrazyLight Mini Pokemon Mew-Two FF Edition"],
[0x5757, "Pulsar X2 CrazyLight Medium BlueArchive Hoshino Edition"],
[0x5758, "Pulsar X2 CrazyLight Medium BlueArchive Shiroko Edition"],
[0x5759, "Pulsar X2 CrazyLight Medium BlueArchive Nonomi Edition"],
[0x575a, "Pulsar X2 CrazyLight Medium BlueArchive Hoshino FF Edition"],
[0x575b, "Pulsar eS FS-1 Medium. Wireless Gaming mouse"],
[0x575c, "Pulsar BUZZ-X Pro Series - MOON GRAY"],
[0x575d, "Pulsar STA-X Pro Series - STEALTH BLACK"],
[0x575e, "Pulsar CARPE-X Pro Series - CARP ORANGE"],
[0x575f, "Pulsar X2 CrazyLight Mini Pokemon Mew-Two FF Edition"],
[0x5760, "Pulsar X2 CrazyLight Mini PRX Pacific Gold Edition"],
[0x5761, "Pulsar X2H CrazyLight Mini BadSeedTech Edition"],
[0x5762, "Pulsar X2H CrazyLight Medium BadSeedTech Edition"],
[0x5763, "Pulsar Jinggg-X Pro Series"],
[0x5764, "Pulsar ZywOo The Chosen Mouse Gen.2"],
[0x5765, "Pulsar Feinmann F01 Noctua Edition"],
[0x5766, "Pulsar X3 CrazyLight Desert"],
[0x5767, "Pulsar X3 CrazyLight Medium Desert"],
[0x5768, "Pulsar X3 LHD CrazyLight Medium Desert"],
[0x5769, "Pulsar X2N CrazyLight Mini Ocean"],
[0x576a, "Pulsar X2N CrazyLight Medium Ocean"],
[0x576b, "Pulsar TenZ 2.0 Mini Misty Blue"],
[0x576d, "Pulsar X2 CrazyLight Medium Forest"],
[0x576e, "Pulsar TenZ Signature Edition 2.0 Mini"],
[0x5770, "Pulsar Susanto-X 2.0 Large Black"],
[0x5774, "Pulsar Xlite CrazyLight Rock"],
[0x5775, "Pulsar X2H CrazyLight Volcano"],
[0x5776, "Pulsar Xlite CrazyLight Medium Rock"],
[0x5777, "Pulsar X2H CrazyLight Medium Volcano"],
[0x5778, "Pulsar Susanto-X 2.0 Large Phantom Jungle"],
[0x577a, "Pulsar TenZ Signature Edition 2.0 Medium"],
[0x577b, "Pulsar TenZ 2.0 Medium Misty Blue"],
[0x577c, "Pulsar Susanto-X 2.0 Medium Phantom Jungle"],
[0x577d, "Pulsar Susanto-X 2.0 Medium Black"],
[0x577e, "Pulsar X2F CrazyLight Black"],
[0x577f, "Pulsar eS FS-1 GT Silver"],
[0x5780, "Pulsar eS FS-1 Tournament Red"],
[0x5781, "Pulsar eS FS-1 Tournament Blue"],
[0x5782, "Pulsar X2 CrazyLight Mini Shibuki"],
[0x5783, "Pulsar X2 CrazyLight Mini Yuni"],
[0x5784, "Pulsar X2 CrazyLight Mini Mashiro"],
[0x5785, "Pulsar ZywOo The Chosen Mouse Gen.3 Mini"],
[0x5786, "Pulsar ZywOo The Chosen Mouse Gen.3 Mini"],
[0x5787, "Pulsar ZywOo The Chosen Mouse Gen.3 Medium"],
[0x5788, "Pulsar ZywOo The Chosen Mouse Gen.3 Medium"],
[0x5792, "Pulsar X2 CrazyLight 2 Mini 2026 PRX Edition"],
[0x5793, "Pulsar X2 CrazyLight 2 Medium 2026 PRX Edition"],
]);
69 changes: 67 additions & 2 deletions src/drivers/pulsar/pulsar-hid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ test("rejects devices without the report-8 control collection", () => {
});

/** Answers report 8 from a flash image, enough for a write and its read-back. */
function flashDevice(): HIDDevice & { flash: Uint8Array } {
function flashDevice({
vendorId = 0x3710, productId = 0x0001, productName = "Pulsar 8K Dongle", cid = 0, mid = 0,
} = {}): HIDDevice & { flash: Uint8Array } {
const flash = new Uint8Array(256);
let listener: ((event: HIDInputReportEvent) => void) | null = null;
const fake = {
...device(0x3710, 0x0001),
...device(vendorId, productId),
productName,
flash,
opened: true,
addEventListener: (_: string, fn: (event: HIDInputReportEvent) => void) => { listener = fn; },
Expand All @@ -58,6 +61,11 @@ function flashDevice(): HIDDevice & { flash: Uint8Array } {
const packet = new Uint8Array(data as Uint8Array);
const reply = new Uint8Array(16);
reply.set(packet.subarray(0, 5));
reply[1] = 0;
if (packet[0] === PULSAR_COMMAND.encryptionData) {
reply[9] = cid;
reply[10] = mid;
}
const address = (packet[2]! << 8) | packet[3]!;
if (packet[0] === PULSAR_COMMAND.deviceOnline) reply[5] = 1;
if (packet[0] === PULSAR_COMMAND.readFlashData) reply.set(flash.subarray(address, address + packet[4]!), 5);
Expand All @@ -68,6 +76,63 @@ function flashDevice(): HIDDevice & { flash: Uint8Array } {
return fake as unknown as HIDDevice & { flash: Uint8Array };
}

test("resolves model names and preserves product-name and generic fallbacks", async () => {
for (const { options, expected } of [
{ options: { productId: 0x5406, cid: 0x57, mid: 0x14 }, expected: "Pulsar X3 CrazyLight Mini" },
{ options: { productId: 0x5406, cid: 0x57, mid: 0x11 }, expected: "Pulsar Xlite CrazyLight Mini" },
{ options: { productId: 0x5406, cid: 0x57, mid: 0x12 }, expected: "Pulsar Xlite CrazyLight Mini" },
{ options: { productId: 0x5406, cid: 0x57, mid: 0x22 }, expected: "Pulsar Xlite CrazyLight Medium" },
{ options: { productId: 0x5406, cid: 0x57, mid: 0x47 }, expected: "Pulsar Xlite CrazyLight Medium" },
{ options: { productId: 0x5406, cid: 0x57, mid: 0x48 }, expected: "Pulsar X3 CrazyLight Medium" },
{ options: { productId: 0x5406, cid: 0x57, mid: 0x15 }, expected: "Pulsar X3 LHD CrazyLight Mini" },
{ options: { productId: 0x5406 }, expected: "Pulsar CrazyLight 8K Dongle" },
{ options: { productId: 0x5406, cid: 0x58, mid: 0x14 }, expected: "Pulsar CrazyLight 8K Dongle" },
{ options: { productId: 0x5406, cid: 0x57, mid: 0xff }, expected: "Pulsar CrazyLight 8K Dongle" },
{ options: { productId: 0x5403, cid: 0x57, mid: 0x04 }, expected: "Pulsar X2 CrazyLight Mini" },
{ options: { productId: 0x3414, cid: 0x57, mid: 0x14 }, expected: "Pulsar X3 CrazyLight Mini" },
{ options: { productId: 0x5403 }, expected: "Pulsar 8K Dongle" },
{ options: { vendorId: 0x3554, productId: 0x5406 }, expected: "Pulsar 8K Dongle" },
{ options: { vendorId: 0x3554, cid: 0x57, mid: 0x04 }, expected: "Pulsar 8K Dongle" },
{ options: { cid: 0x57, mid: 0xff }, expected: "Pulsar 8K Dongle" },
{ options: { cid: 0x58, mid: 0x04 }, expected: "Pulsar 8K Dongle" },
{ options: { productName: "Pulsar X2 Wireless" }, expected: "Pulsar X2 Wireless" },
{ options: { productName: "" }, expected: "Pulsar Mouse" },
]) {
const status = await new PulsarHidClient(flashDevice(options)).readStatus();
assert.equal(status.name, expected, JSON.stringify(options));
}
});

test("names other Pulsar families and editions from the handshake on wired and receiver PIDs", async () => {
for (const productId of [0x3506, 0x5406]) {
for (const [mid, name] of [
[0x07, "Pulsar TenZ Signature Edition"],
[0x17, "Pulsar X2H CrazyLight Mini"],
[0x19, "Pulsar X2N CrazyLight Mini"],
[0x1f, "Pulsar X2A Medium Pulsar By You XS-1"],
[0x24, "Pulsar Lab. X2F"],
[0x26, "Pulsar JV-X Pro Series"],
[0x2d, "Pulsar X VSPO! Asumi Sena"],
[0x44, "Pulsar ZywOo The Chosen Mouse Gen.2"],
[0x65, "Pulsar Feinmann F01 Noctua Edition"],
[0x6b, "Pulsar TenZ 2.0 Mini Misty Blue"],
[0x6d, "Pulsar X2 CrazyLight Medium Forest"],
[0x93, "Pulsar X2 CrazyLight 2 Medium 2026 PRX Edition"],
] satisfies [number, string][]) {
const fake = flashDevice({ productId, cid: 0x57, mid });
assert.equal(PulsarHidClient.isSupported(fake), true);
assert.equal((await new PulsarHidClient(fake).readStatus()).name, name);
}
}
});

test("unnamed vendor placeholders and out-of-range MIDs retain the fallback", async () => {
for (const mid of [0x00, 0x6c, 0x6f, 0x89, 0x95, 0x96, 0xff]) {
const fake = flashDevice({ productId: 0x5406, cid: 0x57, mid });
assert.equal((await new PulsarHidClient(fake).readStatus()).name, "Pulsar CrazyLight 8K Dongle");
}
});

test("setButtonMapping writes one key record at 96 + 4 * slot", async () => {
const fake = flashDevice();
[[KEY.mouse, 0x0100], [KEY.mouse, 0x0200], [KEY.mouse, 0x0400], [KEY.mouse, 0x0800], [KEY.mouse, 0x1000], [KEY.dpi, 0x0100]]
Expand Down
8 changes: 7 additions & 1 deletion src/drivers/pulsar/pulsar-hid.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { MouseStatus } from "../mouse-types.ts";
import { PULSAR_MODEL_NAMES } from "./model-names.ts";
import {
PULSAR_COMMAND as COMMAND,
PULSAR_CONFIG_PACKET_LENGTH as CONFIG_PACKET_LENGTH,
Expand Down Expand Up @@ -154,9 +155,14 @@ export class PulsarHidClient {
const currentDpi = Math.min(flash[FLASH.currentDpi] ?? 0, 7);
const dpi = this.decodeDpi(flash.slice(FLASH.dpiValues + currentDpi * 4, FLASH.dpiValues + currentDpi * 4 + 4)) ?? 800;
const lodValue = flash[FLASH.liftOffDistance];
const modelName = this.device.vendorId === PULSAR_VENDOR_ID
? PULSAR_MODEL_NAMES.get((info.cid << 8) | info.mid)
?? (this.device.productId === 0x5406 ? "Pulsar CrazyLight 8K Dongle" : null)
: null;

return {
brand: "Pulsar",
name: info.cid === 0x57 && info.mid === 0x04 ? "Pulsar X2 CrazyLight" : (this.device.productName || "Pulsar Mouse"),
name: modelName ?? (this.device.productName || "Pulsar Mouse"),
ui: { family: "pulsar", hideUnsupportedPollingRates: true },
batteryPercent: battery[5] <= 100 ? battery[5] : null,
batteryState: battery[6] === 1 ? "Charging" : "Discharging",
Expand Down
Loading
Loading