Skip to content

Commit 684e751

Browse files
Dronakurlclaude
andcommitted
Add TUXEDO keyboard LED interface helper for OpenRGB profiles
This helper script converts OpenRGB .orp profile files to the Linux kernel LED interface format used by the tuxedo_keyboard driver. Purpose: On TUXEDO/Clevo laptops, the Embedded Controller (EC) can turn off the keyboard backlight after idle periods. When it wakes, it uses firmware defaults instead of OpenRGB's USB HID settings. The kernel LED interface may provide better persistence through EC wake cycles. Features: - Parses OpenRGB .orp profile binary format - Uses the same hardware LED mapping as RGBController_ClevoKeyboard - Applies colors to /sys/class/leds/rgb:kbd_backlight_*/multi_intensity - Works with any OpenRGB profile (specify profile name as argument) Usage: python3 tuxedo_keyboard_interface_helper.py [profile_name] Hardware Compatibility: - TUXEDO laptops with Clevo per-key RGB keyboards (ITE 8291 controller) - Linux with tuxedo_keyboard kernel driver - NOT part of OpenRGB itself - external helper utility Related: - Addresses EC timeout persistence issue - Complements Clevo keyboard support (PR #76) - See: https://gitlab.com/tuxedocomputers/development/packages/tuxedo-keyboard Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent df95d56 commit 684e751

File tree

1 file changed

+341
-0
lines changed

1 file changed

+341
-0
lines changed
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
#!/usr/bin/env python3
2+
"""
3+
TUXEDO Keyboard LED Interface Helper for OpenRGB Profiles
4+
5+
============================================================================
6+
IMPORTANT: This is NOT part of OpenRGB itself!
7+
============================================================================
8+
9+
This script is a helper utility specifically for TUXEDO/Clevo laptops
10+
with per-key RGB keyboards using the tuxedo_keyboard kernel driver.
11+
12+
Purpose:
13+
--------
14+
Convert OpenRGB .orp profile files to the Linux kernel LED interface format
15+
used by the tuxedo_keyboard driver. This allows applying OpenRGB profiles
16+
through the kernel's /sys/class/leds interface instead of USB HID.
17+
18+
Why This Exists:
19+
---------------
20+
On TUXEDO laptops, the Embedded Controller (EC) can turn off the keyboard
21+
backlight after idle periods to save power. When it wakes up, it uses firmware
22+
default colors, not OpenRGB's USB HID settings.
23+
24+
The kernel LED interface has better integration with the EC and may provide
25+
more persistent color storage. This script bridges the gap between OpenRGB
26+
profiles and the kernel interface.
27+
28+
Hardware Compatibility:
29+
----------------------
30+
- TUXEDO laptops with Clevo per-key RGB keyboards (ITE 8291 controller)
31+
- Linux with tuxedo_keyboard driver (kernel module)
32+
- NOT compatible with other RGB hardware
33+
34+
Usage:
35+
------
36+
python3 tuxedo_keyboard_interface_helper.py
37+
38+
The script will:
39+
1. Read ~/.config/OpenRGB/<profile>.orp (default: ddd.orp)
40+
2. Convert colors using the hardware LED mapping from OpenRGB's ClevoKeyboard controller
41+
3. Apply colors to /sys/class/leds/rgb:kbd_backlight_*/multi_intensity
42+
43+
File Location:
44+
--------------
45+
Place this script in your OpenRGB repository root or in a tools/ directory.
46+
It is NOT meant to be integrated into OpenRGB's build system.
47+
48+
Notes:
49+
------
50+
- This uses the SAME LED mapping as OpenRGB's RGBController_ClevoKeyboard
51+
- Hardware LED indices (0-125) map directly to kernel zone numbers
52+
- The kernel interface is: /sys/class/leds/rgb:kbd_backlight_<zone>/multi_intensity
53+
- Each zone accepts "R G B" format (e.g., "255 128 0")
54+
55+
For more information on the EC timeout issue, see:
56+
https://gitlab.com/tuxedocomputers/development/packages/tuxedo-keyboard
57+
58+
Author: Helper script for TUXEDO hardware community
59+
License: GPL-2.0-or-later (same as OpenRGB)
60+
============================================================================
61+
"""
62+
63+
import sys
64+
import os
65+
66+
# Hardware LED mapping from OpenRGB's RGBController_ClevoKeyboard
67+
# Source: Controllers/ClevoKeyboardController/RGBController_ClevoKeyboard.cpp
68+
# This mapping translates key names to hardware LED indices (0-125)
69+
# The kernel LED zones use the SAME numbering as hardware LEDs
70+
HW_LED_MAP = {
71+
# F-row
72+
"Key: Escape": 105,
73+
"Key: F1": 106, "Key: F2": 107, "Key: F3": 108, "Key: F4": 109,
74+
"Key: F5": 110, "Key: F6": 111, "Key: F7": 112, "Key: F8": 113,
75+
"Key: F9": 114, "Key: F10": 115, "Key: F11": 116, "Key: F12": 117,
76+
"Key: Print Screen": 118,
77+
# Number row
78+
"Key: `": 84,
79+
"Key: 1": 85, "Key: 2": 86, "Key: 3": 87, "Key: 4": 88,
80+
"Key: 5": 89, "Key: 6": 90, "Key: 7": 91, "Key: 8": 92,
81+
"Key: 9": 93, "Key: 0": 94,
82+
"Key: -": 95, "Key: =": 96, "Key: Backspace": 98,
83+
"Key: Insert": 119, "Key: Home": 121, "Key: Page Up": 123,
84+
"Key: Delete": 120, "Key: End": 122, "Key: Page Down": 124,
85+
# QWERTY row
86+
"Key: Tab": 63,
87+
"Key: Q": 65, "Key: W": 66, "Key: E": 67, "Key: R": 68,
88+
"Key: T": 69, "Key: Y": 70, "Key: U": 71, "Key: I": 72,
89+
"Key: O": 73, "Key: P": 74,
90+
"Key: [": 75, "Key: ]": 76,
91+
# ASDF row
92+
"Key: Caps Lock": 42,
93+
"Key: A": 44, "Key: S": 45, "Key: D": 46, "Key: F": 47,
94+
"Key: G": 48, "Key: H": 49, "Key: J": 50, "Key: K": 51,
95+
"Key: L": 52, "Key: ;": 53, "Key: '": 54, "Key: #": 55,
96+
"Key: Enter": 77,
97+
# ZXCV row
98+
"Key: Left Shift": 22,
99+
"Key: \\ (ISO)": 23,
100+
"Key: Z": 24, "Key: X": 25, "Key: C": 26, "Key: V": 27,
101+
"Key: B": 28, "Key: N": 29, "Key: M": 30,
102+
"Key: ,": 31, "Key: .": 32, "Key: /": 33,
103+
"Key: Right Shift": 35,
104+
# Modifiers
105+
"Key: Left Control": 0, "Key: Left Fn": 2, "Key: Left Windows": 3,
106+
"Key: Left Alt": 4, "Key: Space": 7, "Key: Right Alt": 10,
107+
"Key: Right Control": 12,
108+
# Arrows
109+
"Key: Up Arrow": 14, "Key: Left Arrow": 13,
110+
"Key: Down Arrow": 18, "Key: Right Arrow": 15,
111+
# Numpad
112+
"Key: Num Lock": 99,
113+
"Key: Number Pad /": 100, "Key: Number Pad *": 101, "Key: Number Pad -": 102,
114+
"Key: Number Pad +": 81,
115+
"Key: Number Pad 7": 78, "Key: Number Pad 8": 79, "Key: Number Pad 9": 80,
116+
"Key: Number Pad 6": 59,
117+
"Key: Number Pad 4": 57, "Key: Number Pad 5": 58,
118+
"Key: Number Pad 1": 36, "Key: Number Pad 2": 37, "Key: Number Pad 3": 38,
119+
"Key: Number Pad Enter": 39,
120+
"Key: Number Pad 0": 16, "Key: Number Pad .": 17
121+
}
122+
123+
# OpenRGB LED list order from RGBController_ClevoKeyboard
124+
# This is the order LEDs appear in OpenRGB's GUI and .orp files
125+
OPENRGB_LED_NAMES = [
126+
"Key: Escape", "Key: F1", "Key: F2", "Key: F3", "Key: F4", "Key: F5", "Key: F6",
127+
"Key: F7", "Key: F8", "Key: F9", "Key: F10", "Key: F11", "Key: F12", "Key: Print Screen",
128+
"Key: `", "Key: 1", "Key: 2", "Key: 3", "Key: 4", "Key: 5", "Key: 6", "Key: 7", "Key: 8",
129+
"Key: 9", "Key: 0", "Key: -", "Key: =", "Key: Backspace",
130+
"Key: Insert", "Key: Num Lock", "Key: Number Pad /", "Key: Number Pad *", "Key: Number Pad -",
131+
"Key: Home", "Key: Page Up",
132+
"Key: Tab", "Key: Q", "Key: W", "Key: E", "Key: R", "Key: T", "Key: Y", "Key: U",
133+
"Key: I", "Key: O", "Key: P", "Key: [", "Key: ]", "Key: Delete",
134+
"Key: Number Pad 7", "Key: Number Pad 8", "Key: Number Pad 9", "Key: Number Pad +",
135+
"Key: End", "Key: Page Down",
136+
"Key: Caps Lock", "Key: A", "Key: S", "Key: D", "Key: F", "Key: G", "Key: H", "Key: J",
137+
"Key: K", "Key: L", "Key: ;", "Key: '", "Key: #", "Key: Enter",
138+
"Key: Number Pad 4", "Key: Number Pad 5", "Key: Number Pad 6",
139+
"Key: Left Shift", "Key: \\ (ISO)", "Key: Z", "Key: X", "Key: C", "Key: V", "Key: B",
140+
"Key: N", "Key: M", "Key: ,", "Key: .", "Key: /", "Key: Right Shift",
141+
"Key: Number Pad 1", "Key: Number Pad 2", "Key: Number Pad 3", "Key: Number Pad Enter", "Key: Up Arrow",
142+
"Key: Left Control", "Key: Left Fn", "Key: Left Windows", "Key: Left Alt", "Key: Space",
143+
"Key: Right Alt", "Key: Right Control", "Key: Left Arrow", "Key: Down Arrow",
144+
"Key: Number Pad 0", "Key: Number Pad .", "Key: Right Arrow"
145+
]
146+
147+
def parse_orp_file(filepath):
148+
"""
149+
Parse OpenRGB .orp profile file and extract colors
150+
151+
The .orp file format:
152+
- Color data starts at offset 0x0e44
153+
- Format per LED: [0x00, R, G, B] (4 bytes)
154+
- LEDs are in OpenRGB's display order (OPENRGB_LED_NAMES)
155+
156+
Returns: list of (R, G, B) tuples for each LED
157+
"""
158+
with open(filepath, "rb") as f:
159+
data = f.read()
160+
161+
# Color data offset determined from binary analysis
162+
color_offset = 0x0e44
163+
164+
colors = []
165+
for i in range(color_offset, min(len(data), color_offset + 126 * 4), 4):
166+
if i + 3 >= len(data):
167+
break
168+
169+
marker = data[i]
170+
r = data[i + 1]
171+
g = data[i + 2]
172+
b = data[i + 3]
173+
174+
if marker != 0x00:
175+
break
176+
177+
colors.append((r, g, b))
178+
179+
return colors
180+
181+
def convert_openrgb_to_kernel(orp_file, profile_name="ddd"):
182+
"""
183+
Convert OpenRGB profile to kernel LED interface format
184+
185+
Args:
186+
orp_file: Path to OpenRGB .orp profile file
187+
profile_name: Name of the profile (used for finding the file)
188+
189+
Returns:
190+
dict mapping kernel_zone -> (R, G, B)
191+
"""
192+
if not os.path.exists(orp_file):
193+
print(f"❌ Profile not found: {orp_file}")
194+
print(f" Looking for: {profile_name}.orp in ~/.config/OpenRGB/")
195+
return None
196+
197+
print(f"📂 Reading profile: {orp_file}")
198+
199+
# Parse OpenRGB profile
200+
openrgb_colors = parse_orp_file(orp_file)
201+
print(f"✅ Extracted {len(openrgb_colors)} colors from profile")
202+
print()
203+
204+
# Build mapping: OpenRGB LED index -> Kernel zone
205+
openrgb_to_kernel = {}
206+
207+
for openrgb_idx, key_name in enumerate(OPENRGB_LED_NAMES):
208+
if openrgb_idx >= len(openrgb_colors):
209+
break
210+
211+
if key_name in HW_LED_MAP:
212+
kernel_zone = HW_LED_MAP[key_name]
213+
openrgb_to_kernel[openrgb_idx] = kernel_zone
214+
215+
print(f"✅ Mapped {len(openrgb_to_kernel)} LEDs to kernel zones")
216+
print()
217+
218+
# Convert colors: kernel_zone -> (R, G, B)
219+
kernel_colors = {}
220+
221+
for openrgb_idx, kernel_zone in openrgb_to_kernel.items():
222+
if openrgb_idx < len(openrgb_colors):
223+
kernel_colors[kernel_zone] = openrgb_colors[openrgb_idx]
224+
225+
return kernel_colors
226+
227+
def apply_to_kernel_interface(kernel_colors, brightness=50):
228+
"""
229+
Apply colors to the kernel LED interface
230+
231+
Args:
232+
kernel_colors: dict mapping kernel_zone -> (R, G, B)
233+
brightness: Brightness value (0-50, default 50)
234+
235+
Returns:
236+
Number of zones successfully applied
237+
"""
238+
leds_path = "/sys/class/leds"
239+
applied = 0
240+
errors = 0
241+
242+
print(f"💡 Applying {len(kernel_colors)} colors with brightness {brightness}...")
243+
print()
244+
245+
for kernel_zone, (r, g, b) in sorted(kernel_colors.items()):
246+
if kernel_zone == 0:
247+
zone = "rgb:kbd_backlight"
248+
else:
249+
zone = f"rgb:kbd_backlight_{kernel_zone}"
250+
251+
intensity_file = os.path.join(leds_path, zone, "multi_intensity")
252+
brightness_file = os.path.join(leds_path, zone, "brightness")
253+
254+
try:
255+
with open(intensity_file, 'w') as f:
256+
f.write(f"{r} {g} {b}")
257+
with open(brightness_file, 'w') as f:
258+
f.write(str(brightness))
259+
applied += 1
260+
except FileNotFoundError:
261+
errors += 1
262+
except Exception as e:
263+
print(f" ⚠️ Zone {kernel_zone}: {e}")
264+
errors += 1
265+
266+
if errors > 0:
267+
print(f"⚠️ {errors} zones had errors (might not exist on this hardware)")
268+
269+
return applied
270+
271+
def main():
272+
print("=" * 70)
273+
print(" TUXEDO Keyboard LED Interface Helper for OpenRGB Profiles")
274+
print("=" * 70)
275+
print()
276+
print("⚠️ This is a helper utility for TUXEDO/Clevo laptops only!")
277+
print(" It converts OpenRGB profiles to the kernel LED interface.")
278+
print()
279+
280+
# Default profile location
281+
config_dir = os.path.expanduser("~/.config/OpenRGB")
282+
default_profile = "ddd"
283+
284+
# Allow command-line profile name override
285+
if len(sys.argv) > 1:
286+
default_profile = sys.argv[1]
287+
288+
orp_file = os.path.join(config_dir, f"{default_profile}.orp")
289+
290+
# Convert profile
291+
kernel_colors = convert_openrgb_to_kernel(orp_file, default_profile)
292+
293+
if kernel_colors is None:
294+
sys.exit(1)
295+
296+
if len(kernel_colors) == 0:
297+
print("❌ No colors to apply!")
298+
sys.exit(1)
299+
300+
# Show sample conversions for verification
301+
print("🔍 Sample conversions (WASD + hjkl):")
302+
test_keys = {
303+
"Key: W": "W", "Key: A": "A", "Key: S": "S", "Key: D": "D",
304+
"Key: H": "H", "Key: J": "J", "Key: K": "K", "Key: L": "L"
305+
}
306+
307+
for key_name, short_name in test_keys.items():
308+
openrgb_idx = OPENRGB_LED_NAMES.index(key_name)
309+
if key_name in HW_LED_MAP and openrgb_idx < len(orp_file):
310+
kernel_zone = HW_LED_MAP[key_name]
311+
if kernel_zone in kernel_colors:
312+
r, g, b = kernel_colors[kernel_zone]
313+
is_lit = (r > 0 or g > 0 or b > 0)
314+
if is_lit:
315+
print(f" {short_name}: OpenRGB {openrgb_idx} → Kernel zone {kernel_zone} → #{r:02x}{g:02x}{b:02x}")
316+
317+
print()
318+
print("=" * 70)
319+
320+
# Apply to kernel interface
321+
applied = apply_to_kernel_interface(kernel_colors)
322+
323+
print()
324+
print(f"✅ Successfully applied {applied} LED colors!")
325+
print()
326+
print("📋 Test Summary:")
327+
print(" Your OpenRGB profile is now active via the kernel LED interface.")
328+
print(" Check if the keyboard shows your expected colors.")
329+
print()
330+
print("⏳ EC Timeout Test:")
331+
print(" 1. Don't touch the keyboard for 5-10 minutes")
332+
print(" 2. Wait for the EC to turn off the backlight")
333+
print(" 3. Press any key to wake it")
334+
print(" 4. Verify: Do your profile colors return?")
335+
print(" - If YES → Success! Kernel interface persists through EC wake")
336+
print(" - If NO → Need periodic reapplication (systemd timer)")
337+
print()
338+
print("=" * 70)
339+
340+
if __name__ == "__main__":
341+
main()

0 commit comments

Comments
 (0)