Skip to content

Razer BlackWidow V3 Pro: the battery over its 2.4 GHz receiver (#56), unit tests - #112

Merged
HeyOkay merged 4 commits into
HeyOkay:mainfrom
casparas123:razer-blackwidow-v3-pro
Sep 29, 2026
Merged

HeyOkay merged 4 commits into
HeyOkay:mainfrom
casparas123:razer-blackwidow-v3-pro

Conversation

@casparas123

@casparas123 casparas123 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds the Razer BlackWidow V3 Pro - the last Razer device of #56 - over its 2.4 GHz receiver. It is one row in razer.py's table plus tests: the keyboard answers the commands this provider already sends.

Base and other PRs

1. Why

#56's diagnostics dump shows the keyboard twice:

VID=1532 PID=025a ... 'Razer BlackWidow V3 Pro'   (on the cable)
VID=1532 PID=025c ... 'Razer BlackWidow V3 Pro'   (the receiver)

025a is not in the table and never gets a request, so nothing is read; 025c is not there either, and the name alone does not pass the wireless filter.

2. Source

OpenRazer, which the maintainer's comment on #56 names:

  • RazerBlackWidowV3ProWireless lists get_battery and is_charging, and the Wired class (USB_PID 0x025A) lists them too - the wireless model inherits that class. (The first draft claimed the Wired class lists neither; thanks @ahmedkhursheed23 for catching it.)
  • The commands are the ones this provider already sends: class 0x07, id 0x80 for the level and id 0x84 for charging, with the level scaled /255*100, exactly as OpenRazer's driver/daemon pair does it for the mice.

The transaction ids come from razer_attr_read_charge_level(): 0x9F for the wireless id, 0x3F for the wired one, which a second commit adds to the table.

3. Which collection gets the request

The keyboard answers on a control collection, not a ff00 vendor page. OpenRazer sends its commands with wIndex = report_index = 0x02 (razer_get_report_params()), i.e. USB interface 2, for both ids; the probe order's ranking is a preference, not a filter, so it reaches whichever collection answers - a test polls a non-vendor collection and reads 71% from 0xB5. An earlier draft of this section named a specific 0059:0001 collection; that could not be re-derived from any source and is dropped.

4. Verification

Unverified on hardware: there is no Razer keyboard here. The level, the charging flag and the transaction id come from the reference; the reporter of #56 offered to test, and the diagnostics the provider prints carry every request and its reply, which settles the transaction id and the answering collection if the table is off.

Tests: python -m unittest discover -s tests - 245 pass; the three new ones fail if the table row is removed (checked by mutation).

The wireless id (1532:025C) answers the same 0x07:0x80 battery and 0x07:0x84
charging commands this provider already sends, so it is one row in the table
plus tests: OpenRazer's RazerBlackWidowV3ProWireless class lists
get_battery/is_charging, and its driver reads them with
razer_chroma_misc_get_battery_level() (class 0x07, id 0x80).

The wired twin (0x025A) is deliberately left out: its OpenRazer class has no
get_battery, and a wired keyboard's answer to the battery command is a
meaningless value (the Huntsman V2 note above WIRELESS_WORDS).

OpenRazer's keyboard driver does not pin a transaction id for this model, so
0x3f is tried first - the one its wireless devices of this generation use -
and the provider's fallback list covers the rest. The keyboard's control
collection is 0059:0001 rather than a vendor page; the probe order already
reaches it, and a test polls exactly that shape.

Reported in HeyOkay#56 by @KKiruano, whose diagnostics pinned both pids. Unverified on
hardware: no Razer keyboard here, so the reporter can confirm the level, the
charging flag and the transaction id in one Diagnostics report.
@ahmedkhursheed23

Copy link
Copy Markdown
Contributor

Thanks @casparas123. I checked this against OpenRazer's keyboard driver while I worked on the DeathStalker V2 Pro for #106 (#122). There are three points. Links are to OpenRazer 6820f9d.

1. Transaction id for 025C: OpenRazer uses 0x9F, not 0x3F. driver/razerkbd_driver.c, razer_attr_read_charge_level() and razer_attr_read_charge_status():

case USB_DEVICE_ID_RAZER_BLACKWIDOW_V3_PRO_WIRELESS:
...
case USB_DEVICE_ID_RAZER_DEATHSTALKER_V2_PRO_TKL_WIRELESS:
case USB_DEVICE_ID_RAZER_BLACKWIDOW_V4_TENKEYLESS_HYPERSPEED_WIRELESS:
    request.transaction_id.id = 0x9f;
    break;

case USB_DEVICE_ID_RAZER_BLACKWIDOW_V3_PRO_WIRED:
    request.transaction_id.id = 0x3f;
    break;

So 0x3F is the wired id (025A). With 0x3F first, the receiver will probably answer "not supported" before the fallback reaches 0x9F. That works, but it costs one request more on each first probe.

2. The wired 025A does have a battery in OpenRazer. In daemon/openrazer_daemon/hardware/keyboards.py, RazerBlackWidowV3ProWired (USB_PID = 0x025A) lists 'get_battery', 'is_charging' in METHODS. The driver has a charge_level case for it (0x3F, above). The mouse part of KNOWN includes the wired ids that OpenRazer reads. If you want the same rule here, 0x025A: ("Razer BlackWidow V3 Pro", 0x3F) fits.

3. Interface. razer_get_report_params() gives report_index = 0x02 for both 025A and 025C. It is sent as wIndex with USB_RECIP_INTERFACE (razercommon.c), so the commands go to interface 2. The test puts the control collection on interface 3 (0059:0001). If that comes from the #56 dump, it is real data and I am wrong. Otherwise interface 2 is the one OpenRazer uses. In #122 I added KEYBOARD_INTERFACE so the provider asks that interface first; 025A/025C can go in it (2 and 2) after both PRs are merged.

#122 does not add 025A or 025C, and its table block is above the mice, so the two PRs only meet in CHANGELOG.md.

… twin

razer_attr_read_charge_level() reads the wireless keyboards of this generation
with transaction id 0x9f and their wired twins with 0x3f, and OpenRazer lists
get_battery/is_charging on the wired class (0x025A) as well - the wireless model
inherits it. So 0x025C moves from 0x3f to 0x9f and 0x025A joins the table.
The 0059:0001 line could not be re-derived from any source - it is not in the HeyOkay#56
dump, libratbag has no BlackWidow V3 Pro file and OpenRazer records no such usage.
The README now states what is verified: commands, transaction ids 0x9F/0x3F and USB
interface 2 from OpenRazer's keyboard driver, with the probe order as a preference,
not a filter. Thanks @ahmedkhursheed23 for the review.
@casparas123

Copy link
Copy Markdown
Contributor Author

Thanks - and you're right on all three. Points 1 and 2 landed here just after you wrote this, as the second commit (e9738b8): 025C carries 0x9F and 025A (0x3F) is in the table - your point 2's rule exactly. Good to have it independently checked.

Point 3 is the valuable one: re-deriving the 0059:0001 line for this reply, I can't. It is not in the #56 dump (that dump only has the two VID/PID lines), libratbag has no BlackWidow V3 Pro file, and OpenRazer records no such usage (its only 0x0059 is the Lancehead's pid). So that specific was wrong to state as fact, and it is now fixed here: the README row and the test comment say what is verified, and the PR body was corrected in the same pass. Your report_index = 2 is verified: BLACKWIDOW_V3_PRO_WIRED and _WIRELESS both hit the 0x02 case.

And on KEYBOARD_INTERFACE after both merge: yes please, 2 for both 025A/025C - it makes explicit what the rank only implies (the rank sorts by interface within a class, so 2 already precedes 3 there).

@HeyOkay
HeyOkay merged commit 542ebb9 into HeyOkay:main Sep 29, 2026
ahmedkhursheed23 added a commit to ahmedkhursheed23/HaloBattery that referenced this pull request Sep 29, 2026
…as it is

The merge had added 0x025A / 0x025C to KEYBOARD_INTERFACE, which changes
the interface order and pictogram of a device this PR does not cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QmGsnjgXs8VbKmmxLkPsf6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants