Linux driver support for Corsair M65 RGB Ultra — contributed upstream to ckb-next
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.
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.
{0, 2, 1} handles the translation. Zone count cross-checked against OpenRGB's CorsairPeripheralV2 table.Five files, each handling a different layer of device support. Click to expand.
// 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
// 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";
// 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;
// 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);
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.
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.