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
17 changes: 17 additions & 0 deletions captures/razer-diamondback-chroma/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Razer Diamondback Chroma (`1532:004c`) hardware report, 2026-09-30

Reporter's own mouse, firmware "Mouse 1.0", wired, Windows. Connected through
OpenMouse Bridge with an unmodified `RazerHidClient`; the Bridge listed seven
HID paths for the device and opened all of them.

- `hardware-test-2026-09-30.json`: the app's hardware test export. Identity,
firmware, DPI (1800) and polling (500 Hz) read back; 800 DPI and 1000 Hz each
wrote, read back and were restored. Verdict: pass.

What it does not settle:

- Polling was read back but not measured: the sampler averaged 305 Hz against
the reported 500 Hz and flagged dropouts, so it skipped rather than failed.
- The DPI stage read (`0x04`/`0x86`) gave nothing usable, so no stage editor
is offered on this model.
- No raw feature reports were captured in this session.
132 changes: 132 additions & 0 deletions captures/razer-diamondback-chroma/hardware-test-2026-09-30.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
{
"device": {
"present": true,
"brand": "Razer",
"name": "Razer Diamondback Chroma",
"vendorId": 5426,
"productId": 76,
"productName": "Razer Diamondback Chroma",
"transport": "bridge",
"connectionType": "Wired",
"pollingRateHz": 500,
"supportedPollingRates": [
125,
500,
1000
],
"dpi": 1800,
"dpiStages": null,
"activeDpiStage": null,
"batteryPercent": null,
"batteryState": "Unknown",
"firmware": [
"Mouse 1.0"
],
"liftOffDistance": null,
"driverFamily": "razer",
"deviceMode": null,
"collectionsSummary": "interfaces 0x0001:0x0002, 0x000C:0x0001, 0x0001:0x0006, 0x0001:0x0080, 0x0001:0x0000",
"signalStrength": null,
"receiverOnline": null,
"receiverRfId": null,
"pairingInProgress": false
},
"results": [
{
"key": "connection",
"label": "Device connected",
"status": "pass",
"detail": "Connected through OpenMouse Bridge"
},
{
"key": "interface",
"label": "Control interface",
"status": "pass",
"detail": "interfaces 0x0001:0x0002, 0x000C:0x0001, 0x0001:0x0006, 0x0001:0x0080, 0x0001:0x0000"
},
{
"key": "identity",
"label": "Device identity",
"status": "pass",
"detail": "Razer Razer Diamondback Chroma"
},
{
"key": "driver",
"label": "Driver identification",
"status": "pass",
"detail": "Razer razer"
},
{
"key": "dpi",
"label": "DPI read-back",
"status": "pass",
"detail": "1,800 DPI"
},
{
"key": "pollingRead",
"label": "Polling rate read-back",
"status": "pass",
"detail": "500 Hz"
},
{
"key": "battery",
"label": "Battery read-back",
"status": "skip",
"detail": "Not reported by this device."
},
{
"key": "firmware",
"label": "Firmware read-back",
"status": "pass",
"detail": "Mouse 1.0"
},
{
"key": "liftOff",
"label": "Lift-off read-back",
"status": "skip",
"detail": "Not reported by this device."
},
{
"key": "link",
"label": "Wireless link (receiver)",
"status": "skip",
"detail": null
},
{
"key": "flashRead",
"label": "Flash / EEPROM read-back",
"status": "pass",
"detail": "decoded fields are in range (dpi, polling)"
},
{
"key": "brandWirelessBattery",
"label": "Razer wireless battery",
"status": "skip",
"detail": null
},
{
"key": "sampling",
"label": "Polling rate sampling",
"status": "skip",
"detail": "avg 305 Hz vs reported 500 Hz — dropout or rate mismatch detected, so the sample is not counted as a failure."
},
{
"key": "flashWrite",
"label": "Flash write round-trip",
"status": "pass",
"detail": "wrote 800 DPI → read back → restored 1,800 DPI · wrote 1,000 Hz → read back → restored 500 Hz · Lift-off skipped"
}
],
"verdict": "pass",
"durationMs": 17802.09999999404,
"runAt": "2026-09-30T21:58:33.335Z",
"build": "BETA · v2.0.58d",
"supportedPage": {
"listed": true,
"status": "likely",
"label": "Test Needed",
"matchedBy": "pid",
"pageModel": "Diamondback Chroma",
"detail": "listed as Test Needed on the supported-devices page — this passing verification supports moving it to Supported."
}
}
52 changes: 49 additions & 3 deletions docs/razer-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Identifiers verified on hardware:
- `1532:00a3` — Cobra, wired (separate driver)
- `1532:00b6` — DeathAdder V3 Pro, wired (firmware 2.1)
- `1532:00b7` — DeathAdder V3 Pro, stock HyperSpeed receiver (firmware 2.1)
- `1532:004c`: Diamondback Chroma, wired (firmware 1.0, through OpenMouse Bridge)

Mouse Dock Pro uses the same 90-byte protocol as the paired mouse. It has no
fixed polling list: if the paired mouse answers the extended polling command it
Expand Down Expand Up @@ -375,9 +376,11 @@ What is deliberately **not** attempted on an untested model:
`asymmetricLiftOff` is set, which only the four Viper V2/V3 Pro ids have. An
untested mouse that answers class `0x0b` still gets the plain three-stop
tracking control, which costs reads only.
- Lighting, button mapping and macros. The generic driver implements none of
them for any model — the only lighting controls anywhere in this project are
the dedicated Cobra and Viper Mini drivers.
- Lighting, button mapping and macros. Macros are not implemented for any
Razer model. Lighting and button mapping are per-product allowlists
(`standardMatrixLighting`, `buttonMapping`) that only list models someone
has connected; beyond them, the only lighting controls are the dedicated
Cobra and Viper Mini drivers.

To promote a model to verified:

Expand Down Expand Up @@ -868,3 +871,46 @@ other effects also use `0x3f`, but every other Cobra effect answers on `0x1f`
and breathing does too, so the single `0x1f` choice holds.

Brightness is not implemented: this driver covers effects and colour only.

## Diamondback Chroma (`1532:004c`)

Driven by the generic `RazerHidClient` on transaction id `0xff`. Verified on
firmware 1.0 over the cable, through OpenMouse Bridge on Windows: identity,
DPI and legacy polling read back, and an 800 DPI and a 1000 Hz write each
round-tripped and were restored. The report is in
`captures/razer-diamondback-chroma/`.

It is the one model this driver lights, through openrazer's older
standard-matrix family, and **the lighting has not been tried on hardware
yet**:

| Write | Class / ID | Notes |
| --- | --- | --- |
| Off / Spectrum / Wave / Static / Reactive / Breathing | `0x03` / `0x0a` | effect id first, no storage byte or led; one effect drives every LED |
| Backlight brightness | `0x03` / `0x03` | storage byte, backlight led (`0x05`), level on a 0-255 scale |

| Read | Class / ID | Notes |
| --- | --- | --- |
| Backlight brightness | `0x03` / `0x83` | level in the third byte |

The effect write has no read-back, so the panel shows the last effect it wrote
and marks the card write-only. Brightness is read on every refresh and
confirmed after each write. Wave always runs in direction `0x01`.

openrazer sends all of these on `0xff` except breathing, which it lists on
`0x3f` in the same block where it lists the Cobra. The Cobra's breathing
answered on its usual id, so breathing goes out on `0xff` here too. **If
breathing alone fails, that is the first thing to change.**

To test the lighting, with Synapse quit:

1. Open the Lighting tab and note the brightness it reads.
2. Pick each effect and watch the mouse: Off, Spectrum, Wave, Static, Reactive
(click to trigger it), and the three breathing modes.
3. Change the Static colour, the Reactive speed and both Breathing dual
colours.
4. Change the brightness, reload, and confirm the new level reads back.
5. Unplug the mouse and plug it back in, and note whether the effect and the
brightness survived. Nothing reads the effect back, so this is the only way
to learn whether it is stored.
6. Report any effect that errors or leaves the LEDs unchanged.
13 changes: 12 additions & 1 deletion src/drivers/razer/devices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ const VERIFIED_SINCE: ReadonlyArray<[number, VerifiedProfile]> = [
// Same mouse over the cable, same session: 13/13 writes persisted, 126 Hz
// peak measured at 125, battery reports "Charging".
[0x00b6, { model: "DeathAdder V3 Pro (Wired)", wireless: false, maxDpi: 30000, transactionId: RAZER_TRANSACTION_ID, rates: RATES_1K, highRate: false }],
// Diamondback Chroma (firmware 1.0) over the cable through OpenMouse Bridge:
// answers on `0xff`, and DPI and legacy polling each round-tripped.
[0x004c, { model: "Diamondback Chroma", wireless: false, maxDpi: 16000, transactionId: RAZER_TRANSACTION_ID_FF, rates: RATES_1K, highRate: false }],
];

const VERIFIED = [...REFACTOR_BASELINE, ...VERIFIED_SINCE];
Expand All @@ -77,7 +80,7 @@ const VERIFIED = [...REFACTOR_BASELINE, ...VERIFIED_SINCE];
* hardware-tested", so it is pinned like the others without claiming to be
* verified.
*/
const HARDWARE_VERIFIED: readonly number[] = [0x00a4, 0x00a5, 0x00a6, 0x00a7, 0x00a8, 0x00c0, 0x00c1, 0x00b8, 0x00b7, 0x00b6];
const HARDWARE_VERIFIED: readonly number[] = [0x00a4, 0x00a5, 0x00a6, 0x00a7, 0x00a8, 0x00c0, 0x00c1, 0x00b8, 0x00b7, 0x00b6, 0x004c];

test("every pinned product keeps exactly the profile it was given", () => {
// A silent change to any of these would only show up on hardware, which is
Expand Down Expand Up @@ -269,6 +272,14 @@ test("button mapping is only offered on connections where class 0x02 answered",
}
});

test("standard-matrix lighting is only offered on a model someone is testing it on", () => {
// Effect writes have no read-back, so a mouse that ignores them looks the
// same as one that obeys. Every entry needs its owner to watch the LEDs.
const offered = RAZER_PRODUCT_IDS.filter((id) => RAZER_PRODUCTS.get(id)?.standardMatrixLighting === true);
assert.deepEqual(offered, [0x004c]);
for (const id of offered) assert.equal(RAZER_PRODUCTS.get(id)?.verified, true);
});

test("no product is claimed by both this registry and a dedicated Razer driver", () => {
// `driverFor` returns the first match in DEVICE_DRIVERS, so an overlap would
// silently kill whichever driver is registered later.
Expand Down
107 changes: 107 additions & 0 deletions src/drivers/razer/hid.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
import type { MouseLighting } from "../mouse-types.ts";

// `hid.ts` schedules its inter-exchange delay through `window`, which node does
// not provide. The global carries the same `setTimeout`.
Expand Down Expand Up @@ -914,3 +915,109 @@ test("a Chrome-refused feature-report write surfaces troubleshooting, not the ba
return true;
});
});

/**
* A Chroma-era wired mouse: firmware, DPI and legacy polling, plus the
* standard-matrix effect write and the backlight brightness pair (`0x03`).
* Brightness is held on the mouse's 0-255 scale; `ignoreWrites` acknowledges
* a brightness write and keeps the old level.
*/
function fakeChromaMouse(options: { productId?: number; brightness?: number; ignoreWrites?: boolean } = {}) {
const sent: Uint8Array[] = [];
let level = options.brightness ?? 0xff;
let pending = new Uint8Array(RAZER_PACKET_LENGTH);
const device = {
vendorId: 0x1532,
productId: options.productId ?? 0x004c,
productName: "Razer Diamondback Chroma",
opened: true,
collections: [{ usagePage: 0x01, usage: 0x02, children: [], featureReports: [], inputReports: [], outputReports: [] }],
open: async () => {},
close: async () => {},
sendFeatureReport: async (_reportId: number, data: Uint8Array) => {
sent.push(data);
const [commandClass, commandId] = [data[6], data[7]];
const answer = (dataSize: number, args: number[]) =>
replyPacket(commandClass, commandId, dataSize, args, RAZER_STATUS.ok);
if (commandClass === 0x00 && commandId === 0x81) pending = answer(0x02, [1, 0]);
else if (commandClass === 0x04 && commandId === 0x85) pending = answer(0x07, [0x01, 0x07, 0x08, 0x07, 0x08]);
else if (commandClass === 0x00 && commandId === 0x85) pending = answer(0x01, [2]);
else if (commandClass === 0x03 && commandId === 0x0a) pending = answer(data[5], [...data.slice(8, 8 + data[5])]);
else if (commandClass === 0x03 && commandId === 0x03) {
if (!options.ignoreWrites) level = data[10];
pending = answer(0x03, [data[8], data[9], data[10]]);
} else if (commandClass === 0x03 && commandId === 0x83) pending = answer(0x03, [data[8], data[9], level]);
else pending = replyPacket(commandClass, commandId, data[5], [], RAZER_STATUS.unsupported);
},
receiveFeatureReport: async () => new DataView(pending.buffer.slice(0)),
} as unknown as HIDDevice;
return { client: new RazerHidClient(device), sent };
}

test("the Diamondback Chroma offers its lighting with the brightness it reports", async () => {
const { client } = fakeChromaMouse({ brightness: 0x80 });

const { lighting } = await client.readStatus();

assert.equal(lighting?.zone, "Mouse");
assert.ok(lighting?.modes.includes("Wave"));
assert.equal(lighting?.brightness, 50);
// The effect has no read, so nothing claims to know the running one.
assert.equal(lighting?.mode, null);
assert.equal(lighting?.writeOnly, true);
});

test("a Razer model off the lighting allowlist is never sent a lighting command", async () => {
// Arrange: 0x0043 is the DeathAdder Chroma, same generation, not listed.
const { client, sent } = fakeChromaMouse({ productId: 0x0043 });

const status = await client.readStatus();

assert.equal(status.lighting, undefined);
assert.equal(sent.some((packet) => packet[6] === 0x03), false);
await assert.rejects(
client.setLighting({ zone: "Mouse", mode: "Static", color: "#ffffff" } as MouseLighting),
/does not support changing the lighting/,
);
assert.equal(sent.some((packet) => packet[6] === 0x03), false);
});

test("an effect change sends one standard-matrix write and leaves an unchanged brightness alone", async () => {
const { client, sent } = fakeChromaMouse();
const { lighting } = await client.readStatus();
sent.length = 0;

await client.setLighting({ ...lighting!, mode: "Static", color: "#ff8000" });

// On the product's own 0xff id, not the driver default.
assert.deepEqual(sent.map((packet) => [packet[1], packet[6], packet[7]]), [[0xff, 0x03, 0x0a]]);
assert.deepEqual([...sliceArgs(sent[0], 0, 4)], [0x06, 0xff, 0x80, 0x00]);
// Write-only: the next status read reports the cached effect.
assert.equal((await client.readStatus()).lighting?.mode, "Static");
});

test("a brightness change is written to the backlight and confirmed by reading it back", async () => {
const { client, sent } = fakeChromaMouse();
const { lighting } = await client.readStatus();
sent.length = 0;

const result = await client.setLighting({ ...lighting!, mode: "Spectrum", brightness: 50 });

assert.deepEqual(sent.map((packet) => [packet[6], packet[7]]), [[0x03, 0x0a], [0x03, 0x03], [0x03, 0x83]]);
assert.deepEqual([...sliceArgs(sent[1], 0, 3)], [0x01, 0x05, 0x80]);
assert.equal(result.brightness, 50);
});

test("a brightness the mouse does not keep is reported, and the effect it did take stays cached", async () => {
const { client } = fakeChromaMouse({ ignoreWrites: true });
const { lighting } = await client.readStatus();

await assert.rejects(
client.setLighting({ ...lighting!, mode: "Spectrum", brightness: 50 }),
/kept 100% brightness instead of 50%/,
);

const after = (await client.readStatus()).lighting;
assert.equal(after?.mode, "Spectrum");
assert.equal(after?.brightness, 100);
});
Loading
Loading