<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
	<channel>
		<title><![CDATA[Photonamus Industries Forums - General Projects Discussion]]></title>
		<link>https://forum.photonamus.com/</link>
		<description><![CDATA[Photonamus Industries Forums - https://forum.photonamus.com]]></description>
		<pubDate>Wed, 30 Sep 2026 02:52:55 +0000</pubDate>
		<generator>MyBB</generator>
		<item>
			<title><![CDATA[cbk-next Support for Corsair M65 Ultra]]></title>
			<link>https://forum.photonamus.com/showthread.php?tid=24</link>
			<pubDate>Thu, 20 Aug 2026 06:25:10 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://forum.photonamus.com/member.php?action=profile&uid=1">Photonamus</a>]]></dc:creator>
			<guid isPermaLink="false">https://forum.photonamus.com/showthread.php?tid=24</guid>
			<description><![CDATA[<div style="text-align: center;" class="mycode_align"><span style="font-size: x-large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">CKB-NEXT: M65 RGB ULTRA</span></span><br />
Linux driver support for Corsair M65 RGB Ultra — contributed upstream to <a href="https://github.com/ckb-next/ckb-next" target="_blank" rel="noopener" class="mycode_url">ckb-next</a><br />
<br />
<span style="font-size: small;" class="mycode_size"><span style="color: #A89880;" class="mycode_color">C  •  Linux  •  Bragi Protocol  •  Reverse Engineering  •  Open Source</span></span><br />
<br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">WHAT THIS IS</span></span><br />
<br />
The Corsair M65 RGB Ultra is a wired gaming mouse (USB ID <span style="font-family: 'Courier New';" class="mycode_font">1b1c:1b9e</span>) 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.<br />
<br />
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 <a href="https://github.com/ckb-next/ckb-next/pull/1332" target="_blank" rel="noopener" class="mycode_url">PR #1332</a> to the ckb-next project.<br />
<br />
<span style="font-weight: bold;" class="mycode_b">4</span> files changed  ·  <span style="font-weight: bold;" class="mycode_b">+72 / -12</span> lines  ·  USB ID <span style="font-weight: bold;" class="mycode_b">1b9e</span>  ·  issue open since <span style="font-weight: bold;" class="mycode_b">2022</span><br />
<br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">THE DISCOVERY</span></span><br />
<br />
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 (<span style="font-family: 'Courier New';" class="mycode_font">07 22 00 00 ...</span>), 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.<br />
<br />
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.<br />
<br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">DEVICE SPECIFICS</span></span><br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite><span style="font-weight: bold;" class="mycode_b">No Report ID prefix</span> — 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.<br />
<br />
<span style="font-weight: bold;" class="mycode_b">3 lighting zones</span> — 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 <span style="font-family: 'Courier New';" class="mycode_font">{0, 2, 1}</span> handles the translation. Zone count cross-checked against OpenRGB's CorsairPeripheralV2 table.<br />
<br />
<span style="font-weight: bold;" class="mycode_b">Button mapping</span> — 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.<br />
<br />
<span style="font-weight: bold;" class="mycode_b">Wheel data</span> — Only exists in the HID packet, not in the Bragi software packet. Device added to SW_PKT_HAS_NO_WHEEL macro.</blockquote>
<br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">THE CODE</span></span><br />
<br />
Five files, each handling a different layer of device support.<br />
<br />
<span style="font-weight: bold;" class="mycode_b">src/daemon/usb.h</span> — device definition, macros, protocol registration<br />
<div class="codeblock"><div class="title">Code:</div><div class="body" dir="ltr"><code>// Product ID definition
 #define P_M65_RGB_ELITE      0x1b5a
 #define P_M65_RGB_ULTRA      0x1b9e    // &lt;-- NEW
 #define IS_M65(kb)          ((kb)-&gt;vendor == V_CORSAIR &amp;&amp; ...)

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

// Added to SUPPORTS_ADJRATE macro
 (kb)-&gt;product == P_M65_RGB_ULTRA ||

// New macro — devices with no HID Report ID prefix
 #define HAS_HID_NO_REPORTID(kb)  ((kb)-&gt;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)-&gt;product == P_M65_RGB_ULTRA</code></div></div><br />
<span style="font-weight: bold;" class="mycode_b">src/daemon/usb.c</span> — device table, DPI range, product string<br />
<div class="codeblock"><div class="title">Code:</div><div class="body" dir="ltr"><code>// DPI list — Ultra supports up to 26000
    { P_M65_RGB_ELITE, 18000 },
    { P_M65_RGB_ULTRA, 26000 },    // &lt;-- NEW

// Device model table
    { V_CORSAIR, P_M65_RGB_ELITE, },
    { V_CORSAIR, P_M65_RGB_ULTRA, },    // &lt;-- 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";</code></div></div><br />
<span style="font-weight: bold;" class="mycode_b">src/daemon/keymap.c</span> — input translation, button LUT, HID parsing<br />
<div class="codeblock"><div class="title">Code:</div><div class="body" dir="ltr"><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 &amp;&amp; urblen == 64) {
    corsair_bragi_mousecopy(targetkb, &amp;targetkb-&gt;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(&amp;targetkb-&gt;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 &lt; 6){
        ckb_err("Invalid length %d", length);
        return;
    }
    for(int bit = 0; bit &lt; BUTTON_HID_COUNT; bit++){
        if(urbinput[0] &amp; (1 &lt;&lt; bit))
            SET_KEYBIT(input-&gt;keys, MOUSE_BUTTON_FIRST + bit);
        else
            CLEAR_KEYBIT(input-&gt;keys, MOUSE_BUTTON_FIRST + bit);
    }
    input-&gt;rel_x += (urbinput[2] &lt;&lt; 8) | urbinput[1];
    input-&gt;rel_y += (urbinput[4] &lt;&lt; 8) | urbinput[3];
    input-&gt;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-&gt;product == P_M65_RGB_ULTRA))
 else if(kb-&gt;vendor == V_CORSAIR &amp;&amp; kb-&gt;product == P_M65_RGB_ULTRA)
    lut = m65_ultra_lut;</code></div></div><br />
<span style="font-weight: bold;" class="mycode_b">src/daemon/led_bragi.c</span> — RGB lighting, zone mapping, LED offset<br />
<div class="codeblock"><div class="title">Code:</div><div class="body" dir="ltr"><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-&gt;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 &lt; zones; i++){
            pkt[7 + i]            = newlight-&gt;r[led_offset + zmap[i]];
            pkt[7 + zones + i]    = newlight-&gt;g[led_offset + zmap[i]];
            pkt[7 + zones * 2 + i] = newlight-&gt;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-&gt;product == P_M65_RGB_ULTRA ? 1 : 0);
+    return updatergb_bragi(kb, force, led_offset);</code></div></div><br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">TESTING</span></span><br />
<br />
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.<br />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">TRANSPARENCY</span></span><br />
<br />
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.<br />
<br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><a href="https://github.com/ckb-next/ckb-next/pull/1332" target="_blank" rel="noopener" class="mycode_url">VIEW PR #1332 ON GITHUB ↗</a></span><br />
</div>]]></description>
			<content:encoded><![CDATA[<div style="text-align: center;" class="mycode_align"><span style="font-size: x-large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">CKB-NEXT: M65 RGB ULTRA</span></span><br />
Linux driver support for Corsair M65 RGB Ultra — contributed upstream to <a href="https://github.com/ckb-next/ckb-next" target="_blank" rel="noopener" class="mycode_url">ckb-next</a><br />
<br />
<span style="font-size: small;" class="mycode_size"><span style="color: #A89880;" class="mycode_color">C  •  Linux  •  Bragi Protocol  •  Reverse Engineering  •  Open Source</span></span><br />
<br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">WHAT THIS IS</span></span><br />
<br />
The Corsair M65 RGB Ultra is a wired gaming mouse (USB ID <span style="font-family: 'Courier New';" class="mycode_font">1b1c:1b9e</span>) 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.<br />
<br />
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 <a href="https://github.com/ckb-next/ckb-next/pull/1332" target="_blank" rel="noopener" class="mycode_url">PR #1332</a> to the ckb-next project.<br />
<br />
<span style="font-weight: bold;" class="mycode_b">4</span> files changed  ·  <span style="font-weight: bold;" class="mycode_b">+72 / -12</span> lines  ·  USB ID <span style="font-weight: bold;" class="mycode_b">1b9e</span>  ·  issue open since <span style="font-weight: bold;" class="mycode_b">2022</span><br />
<br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">THE DISCOVERY</span></span><br />
<br />
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 (<span style="font-family: 'Courier New';" class="mycode_font">07 22 00 00 ...</span>), 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.<br />
<br />
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.<br />
<br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">DEVICE SPECIFICS</span></span><br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite><span style="font-weight: bold;" class="mycode_b">No Report ID prefix</span> — 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.<br />
<br />
<span style="font-weight: bold;" class="mycode_b">3 lighting zones</span> — 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 <span style="font-family: 'Courier New';" class="mycode_font">{0, 2, 1}</span> handles the translation. Zone count cross-checked against OpenRGB's CorsairPeripheralV2 table.<br />
<br />
<span style="font-weight: bold;" class="mycode_b">Button mapping</span> — 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.<br />
<br />
<span style="font-weight: bold;" class="mycode_b">Wheel data</span> — Only exists in the HID packet, not in the Bragi software packet. Device added to SW_PKT_HAS_NO_WHEEL macro.</blockquote>
<br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">THE CODE</span></span><br />
<br />
Five files, each handling a different layer of device support.<br />
<br />
<span style="font-weight: bold;" class="mycode_b">src/daemon/usb.h</span> — device definition, macros, protocol registration<br />
<div class="codeblock"><div class="title">Code:</div><div class="body" dir="ltr"><code>// Product ID definition
 #define P_M65_RGB_ELITE      0x1b5a
 #define P_M65_RGB_ULTRA      0x1b9e    // &lt;-- NEW
 #define IS_M65(kb)          ((kb)-&gt;vendor == V_CORSAIR &amp;&amp; ...)

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

// Added to SUPPORTS_ADJRATE macro
 (kb)-&gt;product == P_M65_RGB_ULTRA ||

// New macro — devices with no HID Report ID prefix
 #define HAS_HID_NO_REPORTID(kb)  ((kb)-&gt;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)-&gt;product == P_M65_RGB_ULTRA</code></div></div><br />
<span style="font-weight: bold;" class="mycode_b">src/daemon/usb.c</span> — device table, DPI range, product string<br />
<div class="codeblock"><div class="title">Code:</div><div class="body" dir="ltr"><code>// DPI list — Ultra supports up to 26000
    { P_M65_RGB_ELITE, 18000 },
    { P_M65_RGB_ULTRA, 26000 },    // &lt;-- NEW

// Device model table
    { V_CORSAIR, P_M65_RGB_ELITE, },
    { V_CORSAIR, P_M65_RGB_ULTRA, },    // &lt;-- 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";</code></div></div><br />
<span style="font-weight: bold;" class="mycode_b">src/daemon/keymap.c</span> — input translation, button LUT, HID parsing<br />
<div class="codeblock"><div class="title">Code:</div><div class="body" dir="ltr"><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 &amp;&amp; urblen == 64) {
    corsair_bragi_mousecopy(targetkb, &amp;targetkb-&gt;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(&amp;targetkb-&gt;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 &lt; 6){
        ckb_err("Invalid length %d", length);
        return;
    }
    for(int bit = 0; bit &lt; BUTTON_HID_COUNT; bit++){
        if(urbinput[0] &amp; (1 &lt;&lt; bit))
            SET_KEYBIT(input-&gt;keys, MOUSE_BUTTON_FIRST + bit);
        else
            CLEAR_KEYBIT(input-&gt;keys, MOUSE_BUTTON_FIRST + bit);
    }
    input-&gt;rel_x += (urbinput[2] &lt;&lt; 8) | urbinput[1];
    input-&gt;rel_y += (urbinput[4] &lt;&lt; 8) | urbinput[3];
    input-&gt;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-&gt;product == P_M65_RGB_ULTRA))
 else if(kb-&gt;vendor == V_CORSAIR &amp;&amp; kb-&gt;product == P_M65_RGB_ULTRA)
    lut = m65_ultra_lut;</code></div></div><br />
<span style="font-weight: bold;" class="mycode_b">src/daemon/led_bragi.c</span> — RGB lighting, zone mapping, LED offset<br />
<div class="codeblock"><div class="title">Code:</div><div class="body" dir="ltr"><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-&gt;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 &lt; zones; i++){
            pkt[7 + i]            = newlight-&gt;r[led_offset + zmap[i]];
            pkt[7 + zones + i]    = newlight-&gt;g[led_offset + zmap[i]];
            pkt[7 + zones * 2 + i] = newlight-&gt;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-&gt;product == P_M65_RGB_ULTRA ? 1 : 0);
+    return updatergb_bragi(kb, force, led_offset);</code></div></div><br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">TESTING</span></span><br />
<br />
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.<br />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">TRANSPARENCY</span></span><br />
<br />
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.<br />
<br />
<hr class="mycode_hr" />
<br />
<span style="font-size: large;" class="mycode_size"><a href="https://github.com/ckb-next/ckb-next/pull/1332" target="_blank" rel="noopener" class="mycode_url">VIEW PR #1332 ON GITHUB ↗</a></span><br />
</div>]]></content:encoded>
		</item>
	</channel>
</rss>