Skip to content

Implementing the Razer viper v3 Pro LOD System - #37

Merged
snekxs merged 4 commits into
OpenMouse-Project:devfrom
Pochiiko:feat/razer-lift-off-distance
Aug 8, 2026
Merged

Implementing the Razer viper v3 Pro LOD System#37
snekxs merged 4 commits into
OpenMouse-Project:devfrom
Pochiiko:feat/razer-lift-off-distance

Conversation

@Pochiiko

Copy link
Copy Markdown

Implements the lift-off distance system Synapse exposes for the Viper V3 Pro
(1532:00c0 wired, 1532:00c1 receiver). Nothing here touches the V4 Pro driver.

Class 0x0b was previously unmapped in any open-source project — OpenRazer has
had it open as issue #783, tagged help wanted, since 2019.

Class / IDdataSizePayload
Read0x0b/0x850x05echo, 00, tracking, liftOff-1, landing-1
Write pair0x0b/0x050x0a00, 04, liftOff-1, landing-1, 00 × 6
Write setting0x0b/0x0b0x0400, 04, setting, value
01 = tracking level · 04 = asymmetric unlock

UI

A Single / Asymmetric switch matching the vendor software: one control or
the other, never both. Single reuses the existing three-stop liftOffDistance
control unchanged. Asymmetric shows lift-off (2–26) and landing (1–25) as
sliders, where landing is bounded by lift-off so an invalid pair cannot be
expressed.

Three things worth knowing

The pair write is refused in symmetric mode and still moves what the read
reports.
Omitting the unlock gives a driver that throws, then reads back
exactly the values the user asked for while the sensor holds the old ones.
setLiftOff always sends the unlock first.

There is no mode bit and nothing readable reports one. The mouse honours
whichever store was written last, so writing a tracking level is what leaves
asymmetric mode. probeAsymmetric recovers the mode from the pair write's own
status, re-sending the stored values so it cannot disturb anything, and runs
once per connection because it is still a write.

The firmware stores an inverted pair without complaint — testing left the
mouse holding lift-off 2 with landing 26, which Synapse cannot express.
razerMaxLanding is the single source of that rule, enforced at the slider,
the driver, and the command builder.

Verification

Every protocol claim was confirmed by lifting the mouse, not by read-back —
on this device a rejected write still changes what the read reports, which
produced three wrong conclusions before that rule was adopted.
npm run check is 117/117 green.

Notes for review

  • MouseStatus.asymmetricLiftOff is a new optional field. Additive:
    liftOffDistance is unchanged and no other driver is touched.
  • Cable untested. Class 0x0b has only been exercised on the receiver.
    Expected wired behaviour is no lift-off card rather than an error —
    readLiftOff degrades to null so an unsupported transport cannot fail the
    whole status read.
imageimageimage

OpenRazer has had this open as issue #783, tagged help wanted, since 2019.
Class 0x0b is now fully mapped, and every claim below was verified at the
sensor by lifting the mouse rather than at a status byte.
read 0x0b/0x85 dataSize 0x05 echo, 00, tracking, liftOff-1, landing-1
pair 0x0b/0x05 dataSize 0x0a 00, 04, liftOff-1, landing-1, 00 x 6
setting 0x0b/0x0b dataSize 0x04 00, 04, setting, value
setting 01 = tracking level 00/01/02
setting 04 = asymmetric unlock, value 01
Three things are easy to get wrong here, so they are enforced rather than
merely documented.
The pair write is refused in symmetric mode and still moves what the read
reports. Omitting the unlock therefore produces a driver that throws, shows an
error, and then reads back exactly the values the user asked for while the
sensor holds the old ones. setLiftOff always sends the unlock first.
There is no mode bit, and nothing readable reports one. The mouse honours
whichever store was written last, so writing a tracking level is what leaves
asymmetric mode. probeAsymmetric recovers the current mode from the pair
write's own status, re-sending the values already stored so it cannot disturb
anything, and runs once per connection because it is still a write.
The firmware stores an inverted pair without complaint. One session left the
mouse holding lift-off 2 with landing 26, which the vendor software cannot
express. razerMaxLanding is the single source of that rule.
readLiftOff degrades to null rather than throwing. Class 0x0b has only been
exercised on the receiver, and an uncaught read would fail the whole status
read instead of one control.
MouseStatus gains an optional asymmetricLiftOff for the pair. liftOffDistance
is unchanged and no other driver is touched.
Mice that report asymmetricLiftOff get a Single / Asymmetric switch in the
shape the vendor software uses: one control or the other, never both. Drivers
that do not report the pair never see the switch and keep the plain three-stop
control, so no existing device changes.
Choosing a mode is itself the write, because the mouse honours whichever store
was written last. That means there is no mode flag to drift out of sync, and
no state the panel can claim while the device disagrees.
Landing is a slider bounded by lift-off rather than a number field, so an
invalid pair is not expressible. The value is clamped and the range is left
alone: a range input positions its thumb relative to its own bounds, so
narrowing max to the ceiling slid the thumb across the track whenever lift-off
moved even though the number under it had not changed. Keeping both at the
device's full range also lines the two tracks up, since they are the same 24
steps offset by one. Staging happens on release, so a drag is one change.
Two fixes to existing CSS that this uncovered:
- `.segmented.three` was never defined, so the three lift-off buttons had
been laid out on a four-column grid.
- Author-level `display` beats the browser's `[hidden]` rule, so a `hidden`
property set from script silently did nothing on anything laid out with
grid or flex. There was already a one-off patch for `.pending-bar`; this
replaces the need for more of them.
@snekxs

Copy link
Copy Markdown
Member

Closing because this conflicts with the current Razer implementation and overlaps newer Viper V3 Pro work. Please rebase the LOD portion onto current dev and submit it separately.

@snekxssnekxs closed this Aug 8, 2026
@snekxssnekxs reopened this Aug 8, 2026
@snekxssnekxs closed this Aug 8, 2026
@snekxssnekxs reopened this Aug 8, 2026
@snekxs
snekxs merged commit 6907975 into OpenMouse-Project:devAug 8, 2026
1 check passed
@Pochiiko

Copy link
Copy Markdown
Author

im assuming that this issue has been fixed if it was merged already yeah?

dwei30 pushed a commit to dwei30/openmouse that referenced this pull request Aug 30, 2026
Picks up Keychron M6 (OpenMouse-Project#33), Nape Pro (OpenMouse-Project#37), Glorious Model O 2/I 2 (OpenMouse-Project#38),
and SteelSeries Rival 3 (OpenMouse-Project#39) support from mouse-protocol main.
The bump retires the generic KeychronHidClient in favor of
KeychronNapeHidClient (a strict superset), which this app's controller.ts
still imported by its old path. Also widens the Glorious lighting call site
since SupportedClient now includes GloriousHidClient's stricter setLighting
signature.
Verified with npm ci, npm run build, and the full test suite (84/84).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for freeto 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.

2 participants

@Pochiiko@snekxs