Uh oh!
There was an error while loading. Please reload this page.
feat(razer): add Viper V3 Pro button mapping - #22
Merged
Conversation
Reverse-engineered and hardware-verified against the Viper V3 Pro receiver
(firmware 1.12), class 0x02.
Read 0x02/0x8c, dataSize 0x0a, args [0x01, controlIndex, layer]
Write 0x02/0x0c, dataSize 0x0a,
[0x01, controlIndex, layer, actionType, actionLen, actionValue, 0,0,0,0]
The write silently no-ops under the shared transaction id 0x1f: status 0x02
(ok), the read-back even changes, but the physical button keeps its old
function. Confirmed by writing a plain mouse-button action, pressing the
button, and finding it unchanged with 0x1f and correct with any other id
(0x02 and 0x10 both round-tripped and worked physically). Hence
RAZER_BUTTON_TRANSACTION_ID = 0x10, pinned on the command itself;
encodeRazerRequest already accepted a per-call id, so exchange() now prefers
command.transactionId over the per-product one.
Two distinct control types rather than one:
RazerButtonControl (leftClick, rightClick, mouse4, mouse5) is cross-
assignable between each other's actions or Disabled -- confirmed cross-
control on hardware, not just identity: Mouse Button 4 written with Right
Click's index physically right-clicked. Left Click is locked on the
Standard layer, matching Synapse's own restriction.
RazerToggleControl (scrollUp, scrollDown, sensitivityButton) only switches
between its own captured factory action and Disabled. Kept a separate type
and out of RAZER_BUTTON_MAPPINGS because cross-assigning these has never
been attempted on hardware, and because the sensitivity button's default
("Cycle Up Sensitivity Stages") is actionType 0x06 -- not a plain-button
action and not decoded generally.
Control indices, each settled by isolated single-variable capture or physical
cross-remap test: 0x01 Left Click, 0x02 Right Click, 0x03 Scroll Click,
0x04 Mouse Button 4, 0x05 Mouse Button 5, 0x09 Scroll Up, 0x0a Scroll Down,
0x60 sensitivity button. 0x06-0x08 were probed and never answered; the space
is sparse, so neighbours are not guessed.
Gated per product by RazerProduct.buttonMapping, set only on the receiver
(0x00c1) -- class 0x02 has only ever been exercised over that connection
path. The cable (0x00c0) is the same mouse and very likely answers the
same way, but a shared protocol family is not evidence for a specific
product id, so it is left off until someone checks it. Mappings are read once
per connection and refreshed from each write's own confirmed read-back,
mirroring asymmetric/asymmetricKnown, rather than polled on the status timer.
readButtonMappings returns null rather than an empty record when no control
answers, so a transport that does not support the class hides the control
instead of reporting an empty set.
Not shipped: the Hypershift layer (the read does not honour a requested layer
at all, which breaks the read-back verification every write here depends on)
and Scroll Click (index known, but this driver's own write to it has only
been physically checked against the wrong action).
Adds 20 tests built from the hardware captures.Pochiiko
commented
Aug 16, 2026
ContributorAuthor
Application changesThe consuming UI is OpenMouse-Project/openmouse#101, which adds the |
Answers a review question on the pull request. Confirmed on a Viper V3 Pro (0x00c1): Mouse Button 4 disabled, host application closed, mouse powered off at its switch for 10 seconds, powered back on. The button was still dead on reconnect and the read reported Disabled, with Synapse never running -- so both the physical behaviour and the device's own read survived, ruling out host-side state. Also records the leading argument byte as an explicit unknown. It is always 0x01, which is RAZER_STORAGE on this device, so a storage selector is the obvious reading -- but the byte has never been varied on this command, so it is documented rather than assigned that meaning.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Base:
OpenMouse-Project/mouse-protocolmainHead:
Pochiiko/mouse-protocolvv3p-button-mappingfeat(razer): add Viper V3 Pro button mapping
Adds configurable button mapping for the Razer Viper V3 Pro, reverse-engineered
and verified on hardware (firmware 1.12, HyperSpeed receiver).
Protocol
Class
0x02:0x02/0x8c0x0a[0x01, controlIndex, layer]0x02/0x0c0x0a[0x01, controlIndex, layer, actionType, actionLen, actionValue, 0,0,0,0]The write silently no-ops under the shared transaction id
0x1f— status0x02(ok), the read-back even changes, but the physical button keeps its oldfunction. Confirmed by writing a plain mouse-button action, pressing the button,
and finding it unchanged with
0x1fand correct with any other id (0x02and0x10both round-tripped and worked physically). HenceRAZER_BUTTON_TRANSACTION_ID = 0x10, pinned on the command itself.encodeRazerRequestalready accepted a per-call id, soexchange()now preferscommand.transactionIdover the per-product one — no signature change.Two control types, deliberately not one
RazerButtonControl(leftClick,rightClick,mouse4,mouse5) —cross-assignable between each other's actions or Disabled. Cross-control was
confirmed on hardware, not just identity: Mouse Button 4 written with Right
Click's index physically right-clicked. Left Click is locked on the Standard
layer, matching Synapse's own restriction.
RazerToggleControl(scrollUp,scrollDown,sensitivityButton) — onlyswitches between its own captured factory action and Disabled. Kept a separate
type and out of
RAZER_BUTTON_MAPPINGSbecause cross-assigning these has neverbeen attempted on hardware, and because the sensitivity button's default
("Cycle Up Sensitivity Stages") is
actionType 0x06— not a plain-button actionand not decoded generally. Its enabled payload is stored verbatim from the
capture rather than constructed.
Control indices
Each settled by isolated single-variable capture or physical cross-remap test:
0x010x020x030x040x050x090x0a0x600x06–0x08were probed and never answered. The space is sparse, soneighbours are not guessed at.
Scoping and safety
RazerProduct.buttonMapping, set only on the Viper V3Pro pair (
0x00c0/0x00c1) — the class has only ever been exercised there.confirmed read-back, mirroring
asymmetric/asymmetricKnown, rather thanpolled on the status timer.
readButtonMappingsreturnsnullrather than an empty record when nocontrol answers, so a transport that does not support the class hides the
control instead of reporting an empty set. (The cable,
0x00c0, carries theflag but has only ever been tested on the receiver.)
under the wrong transaction id the write reports success and does nothing.
Not shipped
0x00Standard,0x01Hypershift, seen in a real Synapse write), but the read does not honour a
requested layer at all — every attempt to read a Hypershift-side mapping
returned the Standard value. That breaks the read-back verification every
write here depends on.
been physically checked against the wrong action, so it has not cleared the
verification bar.
Hardware verification
Razer Viper V3 Pro, product id
0x00c1(HyperSpeed receiver), firmware 1.12,with Synapse fully closed. Each write was applied through this driver and then
confirmed by physically using the control:
The Right Click case is the one that matters: it confirms the cross-control
action reaches the device with the correct transaction id. A read-back alone
cannot establish that, because this write's read-back also changes under the
transaction id that silently no-ops.
The cable (
0x00c0) was not tested and does not carry thebuttonMappingflag.Tests
20 new tests built from the hardware captures, including one asserting the two
control families share no control names and no option labels — the thing that
would let one family's renderer read the other's state.
429/429 pass.