![]() |
|
cbk-next Support for Corsair M65 Ultra - Printable Version +- Photonamus Industries Forums (https://forum.photonamus.com) +-- Forum: Main Topics & Discussions (https://forum.photonamus.com/forumdisplay.php?fid=1) +--- Forum: General Projects Discussion (https://forum.photonamus.com/forumdisplay.php?fid=26) +--- Thread: cbk-next Support for Corsair M65 Ultra (/showthread.php?tid=24) |
cbk-next Support for Corsair M65 Ultra - Photonamus - 08-20-2026 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 · USB ID 1b9e · issue open since 2022 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 Quote: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. THE CODE Five files, each handling a different layer of device support. src/daemon/usb.h — device definition, macros, protocol registration Code: // Product ID definition
#define P_M65_RGB_ELITE 0x1b5a
#define P_M65_RGB_ULTRA 0x1b9e // <-- NEW
#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_ULTRAsrc/daemon/usb.c — device table, DPI range, product string Code: // DPI list — Ultra supports up to 26000
{ P_M65_RGB_ELITE, 18000 },
{ P_M65_RGB_ULTRA, 26000 }, // <-- NEW
// Device model table
{ V_CORSAIR, P_M65_RGB_ELITE, },
{ V_CORSAIR, P_M65_RGB_ULTRA, }, // <-- NEW
// 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 Code: // 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 Code: // 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 ↗ |