Problem
I have the first generation Lexip Np93 ALPHA mouse. It has an LED, and Lexip makes an app to set its colour, but only for Windows. On Linux, OpenRGB finds the mouse as "Np93 ALPHA - Gaming Mouse". You pick a colour, click Apply, and nothing happens.
Looking inside the Windows installer
I wanted to know what the official app sends to the mouse, so I unpacked the installer
(lexip_control_software_4.exe) without running it. The installer is made with Advanced Installer, and 7z only
gets the wrapper out of it. The real MSI and CAB sit inside the exe. A small table at the end of the file
lists each file's offset and size. The first 0x200 bytes of each file are XORed with 0xFF, so searching for
the normal MSI or CAB signature finds nothing. With that undone, cabextract unpacks the CAB.
The app inside is built with Flutter, and its logic is compiled into app.so, which is not much fun to read.
The USB part is in hiddriverx64_wired.dll, and its exports show it only moves bytes:
Hid_Write, SetFeature, GetFeature, Set_VIDPID. So the installer
didn't give me the protocol. It did tell me the app uses plain HID writes, nothing special.
What OpenRGB does wrong
OpenRGB already has a Lexip driver, and its colour packet looks reasonable:
00 24 01 RR GG BB 00 64 80 00 ... 00 (64 bytes)
So I looked at where it sends it. The mouse shows up as five HID interfaces:
lsusb -v -d 04d8:fd0a | grep -E "bInterfaceNumber|bEndpointAddress"
Interfaces 0 to 3 have only IN endpoints (data from the mouse). Interface 4 is the only one with an OUT endpoint,
64 bytes, so it is the one that can receive commands. The OpenRGB log showed it opened /dev/hidraw7,
which is interface 0, the normal boot mouse. The packet goes there and the mouse ignores it.
Quite simple fix
Same packet, interface 4 (/dev/hidraw11 on my machine):
import os
fd = os.open('/dev/hidraw11', os.O_RDWR)
os.write(fd, bytes([0x00, 0x24, 0x01, 0xff, 0x00, 0x00, 0x00, 0x64, 0x80]) + bytes(55))
The mouse turned red. The leading 00 is the report number. This device has no numbered reports,
so the kernel strips it. I also expected 0x64 (100) to be brightness, but changing it does nothing.
For a dimmer LED, use a darker colour.
Making it usable
A one-off script is fine for a test. I wanted the mouse to just work, so I ended up with three things:
lexip_led.py, a small Python script without dependencies. It finds the right hidraw node in sysfs by USB ID and interface number, so it doesn't break when the number changes after a replug. Run./lexip_led.py ff0000for red or000000for off.- A udev rule that runs the script with a saved colour every time the mouse is plugged in.
sudo ./install.sh ff0080installs it. - An OpenRGB plugin that adds a second, working "Lexip Np93 ALPHA" device. You then disable the old entry in Settings > Supported Devices.
The plugin was the fiddly part. It must match the installed OpenRGB exactly: version 0.9 is built with Qt5 and plugin
API 3, so the plugin is built against the release_0.9 source. The openrgb binary exports
only 10 dynamic symbols, so the plugin can't use the RGBController base class from it. The plugin compiles
OpenRGB's RGBController.cpp in, and that is also why the repo is GPL-2.0.
sudo apt install qtbase5-dev
./openrgb-plugin/build.sh
Everything is on GitHub: ra100/lexip-np93-linux. No Lexip software or firmware is included.
Conclusion
When OpenRGB detects a device but can't control it, check which interface it opens. The packet can be right and still
go to the wrong place. Here lsusb -v and the OpenRGB log were enough. I never had to read the Flutter binary.
I did this together with Claude Code. It unpacked the installer, found the interface mismatch and wrote the plugin. I checked the colours on the mouse, from the script and from OpenRGB.