<?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 - [Site] Projects - Photonamus.com]]></title>
		<link>https://forum.photonamus.com/</link>
		<description><![CDATA[Photonamus Industries Forums - https://forum.photonamus.com]]></description>
		<pubDate>Wed, 30 Sep 2026 02:52:54 +0000</pubDate>
		<generator>MyBB</generator>
		<item>
			<title><![CDATA[ckb-next Corsair M65 Ultra Support]]></title>
			<link>https://forum.photonamus.com/showthread.php?tid=42</link>
			<pubDate>Sat, 22 Aug 2026 22:58:15 +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=42</guid>
			<description><![CDATA[<div style="text-align: center;" class="mycode_align"><span style="font-size: xx-large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">CKB-next M65 RGB Ultra</span></span><br />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-style: italic;" class="mycode_i">Full Linux Driver Support for the Corsair M65 RGB Ultra<br />
PR #1332 — Photonamus Industries</span></span><br />
<br />
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</div>
<br />
<span style="font-size: x-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-weight: bold;" class="mycode_b">1b1c:1b9e</span>) that had zero Linux support. Every other M65 variant uses Corsair's legacy NXP protocol. The Ultra doesn't — it uses <span style="font-weight: bold;" class="mycode_b">Bragi</span>, 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 />
<ul class="mycode_list"><li><span style="font-weight: bold;" class="mycode_b">Files Changed:</span> 4<br />
</li>
<li><span style="font-weight: bold;" class="mycode_b">Lines:</span> +72 / -12<br />
</li>
<li><span style="font-weight: bold;" class="mycode_b">USB Product ID:</span> 1b9e<br />
</li>
<li><span style="font-weight: bold;" class="mycode_b">Issue Opened:</span> 2022<br />
</li>
</ul>
<br />
<div style="text-align: center;" class="mycode_align">─── ◆ ───</div>
<br />
<span style="font-size: x-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, 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 />
<div style="text-align: center;" class="mycode_align">─── ◆ ───</div>
<br />
<span style="font-size: x-large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">Device Specifics</span></span><br />
<br />
<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 {0, 2, 1} 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.<br />
<br />
<div style="text-align: center;" class="mycode_align">─── ◆ ───</div>
<br />
<span style="font-size: x-large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">The Code</span></span><br />
<br />
Four 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
 #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 },

// 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";</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 />
<div style="text-align: center;" class="mycode_align">─── ◆ ───</div>
<br />
<span style="font-size: x-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 />
<div style="text-align: center;" class="mycode_align">─── ◆ ───</div>
<br />
<span style="font-size: x-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 />
<div style="text-align: center;" class="mycode_align"><a href="https://github.com/ckb-next/ckb-next/pull/1332" target="_blank" rel="noopener" class="mycode_url"><span style="font-weight: bold;" class="mycode_b">VIEW PR #1332 ON GITHUB ↗</span></a></div>
<br />
<div style="text-align: center;" class="mycode_align">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</div>]]></description>
			<content:encoded><![CDATA[<div style="text-align: center;" class="mycode_align"><span style="font-size: xx-large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">CKB-next M65 RGB Ultra</span></span><br />
<br />
<span style="font-size: large;" class="mycode_size"><span style="font-style: italic;" class="mycode_i">Full Linux Driver Support for the Corsair M65 RGB Ultra<br />
PR #1332 — Photonamus Industries</span></span><br />
<br />
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</div>
<br />
<span style="font-size: x-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-weight: bold;" class="mycode_b">1b1c:1b9e</span>) that had zero Linux support. Every other M65 variant uses Corsair's legacy NXP protocol. The Ultra doesn't — it uses <span style="font-weight: bold;" class="mycode_b">Bragi</span>, 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 />
<ul class="mycode_list"><li><span style="font-weight: bold;" class="mycode_b">Files Changed:</span> 4<br />
</li>
<li><span style="font-weight: bold;" class="mycode_b">Lines:</span> +72 / -12<br />
</li>
<li><span style="font-weight: bold;" class="mycode_b">USB Product ID:</span> 1b9e<br />
</li>
<li><span style="font-weight: bold;" class="mycode_b">Issue Opened:</span> 2022<br />
</li>
</ul>
<br />
<div style="text-align: center;" class="mycode_align">─── ◆ ───</div>
<br />
<span style="font-size: x-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, 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 />
<div style="text-align: center;" class="mycode_align">─── ◆ ───</div>
<br />
<span style="font-size: x-large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">Device Specifics</span></span><br />
<br />
<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 {0, 2, 1} 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.<br />
<br />
<div style="text-align: center;" class="mycode_align">─── ◆ ───</div>
<br />
<span style="font-size: x-large;" class="mycode_size"><span style="font-weight: bold;" class="mycode_b">The Code</span></span><br />
<br />
Four 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
 #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 },

// 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";</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 />
<div style="text-align: center;" class="mycode_align">─── ◆ ───</div>
<br />
<span style="font-size: x-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 />
<div style="text-align: center;" class="mycode_align">─── ◆ ───</div>
<br />
<span style="font-size: x-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 />
<div style="text-align: center;" class="mycode_align"><a href="https://github.com/ckb-next/ckb-next/pull/1332" target="_blank" rel="noopener" class="mycode_url"><span style="font-weight: bold;" class="mycode_b">VIEW PR #1332 ON GITHUB ↗</span></a></div>
<br />
<div style="text-align: center;" class="mycode_align">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</div>]]></content:encoded>
		</item>
	</channel>
</rss>