CKB-NEXT: M65 RGB ULTRA

Linux driver support for Corsair M65 RGB Ultra — contributed upstream to ckb-next

C Linux Bragi Protocol Reverse Engineering Open Source

WHAT THIS IS

The Corsair M65 RGB Ultra is a wired gaming mouse (USB ID 1b1c:1b9e) that had zero Linux support. Every other M65 variant uses Corsair's legacy NXP protocol. The Ultra doesn't — it uses Bragi, a newer protocol that shares nothing with NXP at the wire level. Nobody knew this. The existing device request sat open on GitHub since 2022.

This contribution adds full support: cursor, all 8 buttons including sniper, scroll, DPI control, and RGB lighting across all three zones. It was submitted as PR #1332 to the ckb-next project.

4
FILES CHANGED
+72 / -12
LINES
1b9e
USB PRODUCT ID
2022
ISSUE OPENED

THE DISCOVERY

The M65 RGB Ultra's firmware does something deceptive: it ACKs legacy NXP commands without applying them. You send an NXP lighting command, the device responds with a proper acknowledgment on the command endpoint (07 22 00 00 ...), and nothing happens. No error, no rejection — just silence on the hardware. This is why treating it as an NXP clone of the M65 RGB Elite never worked, and why the issue sat open for years.

The answer came from USB traffic captures with usbmon. NXP writes completed successfully with zero hardware effect. The Bragi handshake returned real data — firmware version, poll rate, brightness — where NXP returned all zeros or failures. The device is Bragi. Once that was established, the actual code changes were surgical.

DEVICE SPECIFICS

No Report ID prefix — HID mouse reports arrive raw: byte 0 is buttons, bytes 1–4 are X/Y as int16 little-endian, byte 5 is wheel. Every other mouse in the codebase has a Report ID prefix byte. New translation function required.

3 lighting zones — Logo, scroll wheel, DPI indicator. The device sends them in that order, but the shared M65E GUI layout expects a different mapping. Zone reorder array {0, 2, 1} handles the translation. Zone count cross-checked against OpenRGB's CorsairPeripheralV2 table.

Button mapping — All 8 buttons arrive in one byte in straight bit order. Calibrated by capture: 0x08 back, 0x10 forward, 0x20 DPI up, 0x40 DPI down, 0x80 sniper.

Wheel data — Only exists in the HID packet, not in the Bragi software packet. Device added to SW_PKT_HAS_NO_WHEEL macro.

THE CODE

Five files, each handling a different layer of device support. Click to expand.

src/daemon/usb.h — device definition, macros, protocol registration
// Product ID definition
 #define P_M65_RGB_ELITE      0x1b5a
 #define P_M65_RGB_ULTRA      0x1b9e
 #define IS_M65(kb)           ((kb)->vendor == V_CORSAIR && ...)

// Added to IS_MOUSE macro
 (product) == (P_M65_RGB_ULTRA) ||

// Added to SUPPORTS_ADJRATE macro
 (kb)->product == P_M65_RGB_ULTRA ||

// New macro — devices with no HID Report ID prefix
 #define HAS_HID_NO_REPORTID(kb)  ((kb)->product == P_M65_RGB_ULTRA)

// Added to USES_BRAGI macro
 (product) == (P_M65_RGB_ULTRA)

// Added to SW_PKT_HAS_NO_WHEEL macro (wheel only in HID packet)
 (kb)->product == P_M65_RGB_ULTRA
src/daemon/usb.c — device table, DPI range, product string
// DPI list — Ultra supports up to 26000
     { P_M65_RGB_ELITE, 18000 },
     { P_M65_RGB_ULTRA, 26000 },

// Device model table
     { V_CORSAIR, P_M65_RGB_ELITE, },
     { V_CORSAIR, P_M65_RGB_ULTRA, },

// Product string — shares "m65e" layout with the Elite
-    if(product == P_M65_RGB_ELITE)
+    if(product == P_M65_RGB_ELITE || product == P_M65_RGB_ULTRA)
         return "m65e";
src/daemon/keymap.c — input translation, button LUT, HID parsing
// New function declaration
 static void hid_mouse_translate_noreportid(usbinput* input, int length,
                                             const unsigned char* urbinput);

// Bragi input routing — added no-reportid branch
 if(buffer[1] == BRAGI_INPUT_HID && urblen == 64) {
     corsair_bragi_mousecopy(targetkb, &targetkb->input, buffer);
 } else if(HAS_HID_NO_REPORTID(targetkb)) {
     // No Report ID prefix: buttons at byte 0, XY at bytes 1-4
     hid_mouse_translate_noreportid(&targetkb->input, urblen, buffer);
 } else {

// New translation function — raw HID with no Report ID
 static void hid_mouse_translate_noreportid(usbinput* input,
                          int length, const unsigned char* urbinput){
     if(length < 6){
         ckb_err("Invalid length %d", length);
         return;
     }
     for(int bit = 0; bit < BUTTON_HID_COUNT; bit++){
         if(urbinput[0] & (1 << bit))
             SET_KEYBIT(input->keys, MOUSE_BUTTON_FIRST + bit);
         else
             CLEAR_KEYBIT(input->keys, MOUSE_BUTTON_FIRST + bit);
     }
     input->rel_x += (urbinput[2] << 8) | urbinput[1];
     input->rel_y += (urbinput[4] << 8) | urbinput[3];
     input->whl_rel_y = (signed char)urbinput[5];
 }

// Button lookup table — all 8 in straight bit order
 const unsigned char m65_ultra_lut[BRAGI_ONE_BYTE_MOUSE_BUTTONS] = {
     0x00,
     0x01,
     0x02,
     0x03, // back
     0x04, // forward
     0x05, // dpi up
     0x06, // dpi down
     0x07, // sniper
 };

// Added M65 Ultra to one-byte button device list + LUT routing
 || kb->product == P_M65_RGB_ULTRA))
 else if(kb->vendor == V_CORSAIR && kb->product == P_M65_RGB_ULTRA)
     lut = m65_ultra_lut;
src/daemon/led_bragi.c — RGB lighting, zone mapping, LED offset
// LED zone count — 3 zones: logo, scroll wheel, DPI indicator
     LED_CASE_M(P_M65_RGB_ULTRA, 3);

// Zone reorder — device order doesn't match GUI layout
     if(kb->product == P_M65_RGB_ULTRA){
         // Device zone order: logo, scroll wheel, dpi indicator
         // GUI order: back, dpi, wheel — swap last two
         static const uchar zmap[3] = {0, 2, 1};
         bytes *= 3;
         for(size_t i = 0; i < zones; i++){
             pkt[7 + i]             = newlight->r[led_offset + zmap[i]];
             pkt[7 + zones + i]     = newlight->g[led_offset + zmap[i]];
             pkt[7 + zones * 2 + i] = newlight->b[led_offset + zmap[i]];
         }
     }

// LED offset — M65E layout has no "front" zone, start at back
-    return updatergb_bragi(kb, force, LED_MOUSE);
+    const size_t led_offset = LED_MOUSE +
+        (kb->product == P_M65_RGB_ULTRA ? 1 : 0);
+    return updatergb_bragi(kb, force, led_offset);

TESTING

Developed and tested with an M65 RGB Elite and the Ultra connected simultaneously. Verified on hardware: cursor tracking, all 8 buttons including sniper hold with DPI shift and light change, scroll wheel, DPI stage switching, all three lighting zones with per-zone independent colors, GUI key rebinding — across daemon restarts and USB replugs. The NXP code path is completely untouched, so existing devices are unaffected by construction.

TRANSPARENCY

This was a human-driven hardware debugging effort done in collaboration with an AI assistant (Anthropic Claude). Every change was motivated by wire-level evidence — usbmon captures, daemon debug logs, button-by-button calibration — and verified on the physical hardware before the next step. The AI disclosure is included in the commit and PR description. This is how AI-assisted open source contribution should work: full transparency, real engineering, no black boxes.


VIEW PR #1332 ON GITHUB ↗