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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ and the project follows [Semantic Versioning](https://semver.org/).
level is shown as a gauge ("about 50%") and no charging state is reported.
**Unverified** here: no Corsair mouse was on hand, so the collection and the
offsets are the reference's until the reporter of #56 confirms.
- Razer BlackWidow V3 Pro: the battery over its 2.4 GHz receiver (1532:025C), through
the same class 0x07 commands the mice use (OpenRazer's RazerBlackWidowV3ProWireless
class lists them). The wired id 1532:025A has no battery and is left out.
**Unverified** here: no Razer keyboard was on hand, so the level arrives as the
reference describes it until the reporter of #56 confirms.

### Changed
- New tray menu in the Windows 11 style: Segoe UI Variable text, Fluent icons, an acrylic
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Tested on real hardware:
|---|---|---|
| Razer Barracuda Pro (2.4 GHz) | 2.4 GHz dongle (1532:053a) | Its receiver publishes two collections and neither answers the standard Razer mouse request this app sends. The headset speaks the "PA" protocol instead: 64-byte vendor frames, `P`,`A` out and `P`,`I` back, battery command `0x21` with the level in the reply's data byte, charging `0x2A`. Decoded from a USBPcap capture of Razer Synapse on a real unit (it read 34%), then confirmed on hardware with @phl23's own Barracuda Pro: the level tracks (27% at the test), charging follows the charger, and a switched-off headset reports "no link" rather than a stale value. Over Bluetooth Windows reports the level itself. |
| Razer BlackShark V2 Pro (2023) | 2.4 GHz receiver (1532:0555) | The headset's own "PA" protocol: output reports 0x02 on the vendor interface 0xFF00, remote mode 0xE1, commands 0x21 (battery) and 0x2A (charging) |
| Razer BlackWidow V3 Pro | 2.4 GHz receiver (1532:025C), cable (1532:025A) | The standard Razer commands: class `0x07` id `0x80` for the level (0-255, shown as a percentage) and id `0x84` for charging. The keyboard answers on its own control collection, not a `ff00` vendor page - the probe order's ranking is a preference, not a filter, so it reaches any collection that answers. From OpenRazer's keyboard driver: `razer_attr_read_charge_level()` reads the wireless id with transaction id `0x9F` and the wired id with `0x3F`, and `razer_get_report_params()` puts both on USB interface 2. **Unverified** - no Razer keyboard was on hand; the diagnostics name every interface/usage they try, so a dump from the reporter of #56 settles it |
| WLmouse Beast X Max | 8K receiver (36A7:A880) and USB cable | Feature request `02 02 00 83`; if there is no reply, the mouse heartbeat is used. Receiver and cable share one icon |
| G-Wolves WARG, HTS Plus (Pro), HTXU, Lycan, Fenrir Pro / Asym, HTX Mini | 8K receiver (33E4:3854) or USB cable | The same feature report exchange as the WLmouse mice (`00 00 02 02 00 83` out, `a1 ... 83 <charging> <battery%>` back), which G-Wolves' web driver mouse.xyz uses for every mouse on this receiver. The request goes only to the collection that Windows reports with a 64-byte feature report (HidP_GetCaps), as the web driver chooses it. A sleeping mouse keeps its last value on a greyed icon. **Unverified** - no G-Wolves mouse was on hand |
| LAMZU Maya X | 8K dongle (373E:001E) or USB cable (373E:001C) | The same feature report exchange as the WLmouse and G-Wolves mice (`00 00 02 02 00 83` out, `a1 00 02 02 00 83 <charging> <battery %>` back), on the vendor collection `ffff:0000` of interface 2 only; protocol from Sheroune/lamzu-battery-monitory (MIT). Confirmed on a Maya X on its 8K dongle |
Expand Down
10 changes: 10 additions & 0 deletions providers/razer.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@
0x00D4: ("Razer Basilisk Mobile", 0x1F),
0x00D6: ("Razer Basilisk V3 Pro 35K Phantom Green", 0x1F),
0x00D7: ("Razer Basilisk V3 Pro 35K Phantom Green", 0x1F),

# Keyboards with a battery. OpenRazer's RazerBlackWidowV3ProWired (0x025A) lists
# get_battery/is_charging, and the wireless model (0x025C) inherits that class.
# razer_attr_read_charge_level() reads both with razer_chroma_misc_get_battery_level()
# - the same 0x07:0x80 / 0x07:0x84 commands this provider already sends - and sets
# the transaction id to 0x9f for the wireless keyboards of this generation and 0x3f
# for their wired twins. 0x3f is not a value this keyboard answers on: the wired id
# goes only to 0x025A.
0x025A: ("Razer BlackWidow V3 Pro", 0x3F),
0x025C: ("Razer BlackWidow V3 Pro", 0x9F),
}

TRANSACTION_IDS = (0x1F, 0x3F, 0xFF, 0x9F, 0x08)
Expand Down
24 changes: 24 additions & 0 deletions tests/test_razer.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ def poll(self, entries, mice):
R.hidlist = types.SimpleNamespace(enumerate=lambda vid=0: list(entries))
return R.RazerProvider().poll()

def test_the_blackwidow_answers_through_its_own_collection(self):
# The keyboard answers on a control collection that is not a vendor page, so
# the probe order (which ranks 0001/ff00 first) must still reach it. The fake's
# page 0059 is a stand-in: the unit test only pins that ranking does not filter.
e = entry(0x025C, b"kbd", "Razer BlackWidow V3 Pro", iface=3, page=0x0059)
mouse = FakeMouse(tid=0x9F, raw_level=0xB5, charging=1) # 0xB5 -> 71%
out = self.poll([e], {b"kbd": mouse})
self.assertEqual(len(out), 1)
self.assertEqual((out[0].level, out[0].charging), (round(0xB5 / 255 * 100), True))
self.assertEqual(mouse.tids[0], 0x9F)


# ------------------------------------------------------------------ tests
class TableTest(unittest.TestCase):
Expand Down Expand Up @@ -195,13 +206,26 @@ def test_naga_pro_and_naga_v2_pro_are_different_mice(self):
self.assertEqual(R.KNOWN[0x00A7][0], "Razer Naga V2 Pro")
self.assertEqual(R.KNOWN[0x00A8][0], "Razer Naga V2 Pro")

def test_the_blackwidow_pro_pair_is_known(self):
# OpenRazer lists get_battery/is_charging on both the wired class (0x025A) and,
# through inheritance, the wireless model (0x025C); the driver reads them with
# transaction id 0x3f and 0x9f respectively.
self.assertEqual(R.KNOWN[0x025A], ("Razer BlackWidow V3 Pro", 0x3F))
self.assertEqual(R.KNOWN[0x025C], ("Razer BlackWidow V3 Pro", 0x9F))

def test_wired_viper_is_not_in_the_table(self):
# 0078 is OpenRazer's USB_DEVICE_ID_RAZER_VIPER, a wired mouse with no battery.
# The Viper Ultimate is 007A / 007B.
self.assertNotIn(0x0078, R.KNOWN)
self.assertFalse(R.maybe_wireless(0x0078, "Razer Viper"))
self.assertTrue(R.maybe_wireless(0x007B, "Razer Viper Ultimate"))

def test_the_wireless_blackwidow_is_in_the_table(self):
# OpenRazer's RazerBlackWidowV3ProWireless (USB_PID 0x025C) lists get_battery
# and is_charging in METHODS; the Wired class (0x025A) lists neither.
self.assertEqual(R.KNOWN[0x025C][0], "Razer BlackWidow V3 Pro")
self.assertIn(R.KNOWN[0x025C][1], R.TRANSACTION_IDS)


class PollRazerTest(PollTest):
def test_pro_click_v2_vertical_issue_58(self):
Expand Down