Skip to content

Commit b6cdf43

Browse files
committed
v0.5.1: Lighting tab, presets, keyboard effects, cooling pad
- New Lighting tab; all lighting controls moved off the main page - Keyboard effects: Static, Breathing, Spectrum, Wave Right/Left, Off - Lighting presets: 10 slots each for keyboard, cooling pad, and both - Restore lighting at startup (opt-in) - Razer Laptop Cooling Pad card: fan control, 5% speed dropdown, 3 saved presets - New README screenshots
1 parent 9859cc5 commit b6cdf43

19 files changed

Lines changed: 2374 additions & 16 deletions

‎CHANGELOG.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
# Changelog
22

3+
## 0.5.1 - 2026-10-17
4+
5+
### Added
6+
- **Lighting tab**: all lighting controls moved off the main page onto their own tab (Main /
7+
Lighting). The Cooling Pad fan card stays on Main.
8+
- **Lighting presets**: 10 named slots for the keyboard, 10 for the cooling pad and 10 for both
9+
together (a "both" preset can hold either part or both). Save / Apply / Clear per slot, stored in
10+
`config.txt`.
11+
- **Restore lighting at startup (opt-in, off by default)**: puts the last applied keyboard effect and
12+
pad lighting back at launch and when the pad is plugged in again. Requires "Toggle App Lighting
13+
Control" for that device. Without it nothing is applied at launch, as before.
14+
- **Keyboard effects (opt-in)** in the Lighting card: Static, Breathing, Spectrum, Wave (left /
15+
right) and Off with a color picker, behind a **Toggle App Lighting Control** checkbox that is off
16+
by default. Effects are only sent when you click one (or restored at startup if you opt in).
17+
Brightness keeps using the existing Keyboard Brightness slider. Uses the Razer extended matrix
18+
effect command (0x0F02) aimed at the keyboard backlight LED with transaction id 0x1F; Options >
19+
Advanced > Keyboard effect tuning can switch to the older standard command (0x030A), a random
20+
transaction id, or an "Off" before Static / Breathing. Static is painted as a custom frame
21+
(every key set to the color), the way razer-laptop-control does it, because the firmware's own
22+
Static effect ignores the color on some laptops (switchable in the same place). Not yet verified on every Blade model.
23+
- **Cooling Pad** card for the Razer Laptop Cooling Pad (USB 1532:0F43), shown only while a pad is
24+
connected. Fan: Manual / Off, a speed dropdown in 5% steps, three saveable speed presets and an
25+
exact RPM slider. Lighting (Static / Breathing / Off, color, brightness) is opt-in via "Toggle App
26+
Lighting Control", off by default, and is not applied at launch unless you opt in to restoring it, so Razer
27+
Chroma / Synapse keep control of the pad's lights unless you ask otherwise. Runs on its own
28+
background thread, so the window never waits on the pad, and re-applies the last fan setting when
29+
the pad is plugged back in or after sleep. Based on the cooling pad work in
30+
[Robak08/r-helper](https://github.com/Robak08/r-helper) (MIT). Fan "Auto" (temperature-driven) is
31+
not included yet.
32+
333
## 0.5.0 - first CPUnity:R-Helper release
434

535
Fork of [R-Helper](https://github.com/Fatalution/r-helper) by Fatalution (MIT), which is built on

‎Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "r-helper"
3-
version = "0.5.0"
3+
version = "0.5.1"
44
edition = "2021"
55
description = "CPUnity:R-Helper - Razer Blade control interface (fork of R-Helper)"
66
authors = ["Fatalution", "CPUnity"]

‎NOTICE‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Original project: https://github.com/blauzim/razer-ctl
44
Attribution:
55
- Original authors: tdakhran, blauzim and contributors.
66
- Changes in this fork (Fatalution): Windows GUI, resource embedding, UI tweaks, polling, and device state UX improvements.
7+
- Cooling pad support (fan and lighting protocol) is based on Robak08/r-helper (MIT), https://github.com/Robak08/r-helper
78
- Changes in CPUnity:R-Helper: accent colors, options window, battery charge limit, CPU Overclock handling, built-in Synapse standby (original script), custom CPUnity app icon and logo (replacing the upstream icon).
89

910
Licenses:

‎README.md‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ Fork of [R-Helper](https://github.com/Fatalution/r-helper) by Fatalution (MIT),
1212
[razer-ctl](https://github.com/blauzim/razer-ctl)'s `librazer` (MIT).
1313

1414
<p align="center">
15-
<img src="docs/screenshot.png" alt="CPUnity:R-Helper window" width="360">
15+
<img src="docs/screenshot.png" alt="CPUnity:R-Helper, Main tab" width="300">
16+
&nbsp;&nbsp;
17+
<img src="docs/screenshot-lighting.png" alt="CPUnity:R-Helper, Lighting tab" width="300">
1618
</p>
19+
<p align="center"><sub>Main tab (left) and Lighting tab (right)</sub></p>
1720

1821
## Features
1922

@@ -31,6 +34,18 @@ Fork of [R-Helper](https://github.com/Fatalution/r-helper) by Fatalution (MIT),
3134

3235
**Lighting and battery**
3336
- Lid logo mode (Static, Breathing, Off), keyboard brightness, "backlight always on".
37+
- All lighting lives on its own **Lighting** tab (click it next to Main), away from the main page.
38+
- **Keyboard effects (opt-in).** Tick **Toggle App Lighting Control** to get Static, Breathing,
39+
Spectrum, Wave (both directions) and Off, plus a color picker. It is off by default: R-Helper
40+
only sends an effect when you click one, so Razer Chroma / Synapse stay in charge until you ask
41+
otherwise.
42+
- **Lighting presets: 10 slots each for the keyboard, the cooling pad, and both together.** Set the
43+
lights up, optionally type a name, press **Save** on a slot; **Apply** brings it back. A "both"
44+
preset can hold the keyboard part, the pad part, or both. Presets live in `config.txt`, so they
45+
work with Synapse parked.
46+
- **Restore lighting at startup (opt-in).** Off by default. When on, the last keyboard effect and
47+
pad lighting you applied are put back when the app starts (and when the pad is plugged in again),
48+
provided "Toggle App Lighting Control" is on for that device.
3449
- **Battery Health Optimizer** with a charge limit from 50% to 80% in 5% steps.
3550

3651
**Razer Synapse Standby** (built in, so you no longer need a separate Synapse killer tool)
@@ -42,6 +57,18 @@ Fork of [R-Helper](https://github.com/Fatalution/r-helper) by Fatalution (MIT),
4257
Synapse on every boot.
4358
- **Resume Synapse** undoes everything Standby changed and removes the boot task.
4459

60+
**Cooling pad** (new, shown only when a Razer Laptop Cooling Pad is plugged in)
61+
- Fan: Manual or Off. In Manual, pick a speed from a dropdown in 5% steps (15-100% of the pad's
62+
maximum, shown with the RPM), or use one of **three saved presets** (click to apply, right-click to
63+
save the current speed into that slot). An "Exact RPM" slider (500-3200 RPM) is tucked away for
64+
fine tuning.
65+
- Your last fan setting is re-applied when the pad is plugged back in or the PC wakes from sleep.
66+
- **Lighting is opt-in.** "Toggle App Lighting Control" is off by default: while it is off,
67+
R-Helper never opens or touches the pad's lights, so Razer Chroma / Synapse stay in charge. When
68+
you switch it on (Lighting tab) you get Static / Breathing / Off, color and brightness, and the
69+
lights only change when you click something there, or when you chose "Restore lighting at
70+
startup".
71+
4572
**Look**
4673
- Dark card layout, resizable window that fits its content, selectable UI font.
4774
- Accent color with presets, RGB boxes, hex, a visual picker and 10 saved colors.
@@ -156,6 +183,12 @@ Synapse** and untick "launch at startup" so the boot task and registry entry are
156183
simplest), then lower it to the value you want; charging continues up to that value. Staying
157184
just under the limit is also normal: with an 80% limit the battery cycles between roughly
158185
70 and 80%.
186+
- Keyboard effects use Razer's standard extended-matrix-effect command and are new; they are not
187+
yet confirmed on every Blade model. Wave and Spectrum ignore the picked color. If an effect
188+
ignores the color or looks choppy, try the "Keyboard effect tuning" switches in Options > Advanced
189+
and click the effect again; `log.txt` records which variant was sent.
190+
- Cooling pad support is new and has only been tried with one pad (USB 1532:0F43). Fan "Auto"
191+
(temperature-driven) is not included yet; use Manual or Off.
159192
- Balanced and Performance can have the same CPU wattage on some laptops; the difference may be
160193
only fans or GPU behaviour, set by the firmware, not by this app.
161194

@@ -172,6 +205,8 @@ Huge thanks to everyone who made this possible:
172205
Standby (keep your Razer settings without Synapse running in the background). Please support
173206
their project too: if Synapse Standby saves you time, give
174207
[SynapseKiller](https://github.com/NxRoot/SynapseKiller) a star on GitHub.
208+
- **[Robak08](https://github.com/Robak08/r-helper)** - Razer Laptop Cooling Pad support in their
209+
R-Helper fork (MIT), the groundwork for the Cooling Pad card.
175210
- **egui / eframe, image-rs, hidapi-rs, windows-rs, ab_glyph, strum, serde, anyhow, rand, clap,
176211
embed-resource** - the Rust crates that carry the app.
177212
- **The Rust project and community.**

‎docs/screenshot-lighting.png‎

156 KB
Loading

‎docs/screenshot.png‎

4.29 KB
Loading

‎librazer/src/command.rs‎

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,230 @@ pub fn set_battery_care(device: &Device, mode: BatteryCare) -> Result<()> {
241241
let pct = get_battery_limit(device).map(|(_, p)| p).unwrap_or(BATTERY_LIMIT_MAX);
242242
set_battery_limit(device, matches!(mode, BatteryCare::Enable), pct)
243243
}
244+
245+
/// Laptop keyboard lighting effects (Razer "extended matrix effect", class 0x0F id 0x02).
246+
///
247+
/// Opt-in only: the app sends one of these when the user clicks an effect, never at launch.
248+
/// The argument layout follows the Razer extended-matrix-effect format used by openrazer and
249+
/// razer-laptop-control; it has not been verified on every Blade model.
250+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251+
pub enum KeyboardEffect {
252+
Off,
253+
Static,
254+
Breathing,
255+
Spectrum,
256+
WaveRight,
257+
WaveLeft,
258+
}
259+
260+
impl KeyboardEffect {
261+
pub const ALL: [KeyboardEffect; 6] = [
262+
KeyboardEffect::Static,
263+
KeyboardEffect::Breathing,
264+
KeyboardEffect::Spectrum,
265+
KeyboardEffect::WaveRight,
266+
KeyboardEffect::WaveLeft,
267+
KeyboardEffect::Off,
268+
];
269+
270+
pub fn name(self) -> &'static str {
271+
match self {
272+
KeyboardEffect::Off => "Off",
273+
KeyboardEffect::Static => "Static",
274+
KeyboardEffect::Breathing => "Breathing",
275+
KeyboardEffect::Spectrum => "Spectrum",
276+
KeyboardEffect::WaveRight => "Wave Right",
277+
KeyboardEffect::WaveLeft => "Wave Left",
278+
}
279+
}
280+
281+
pub fn from_name(name: &str) -> Option<KeyboardEffect> {
282+
Self::ALL.into_iter().find(|e| e.name() == name)
283+
}
284+
285+
/// Plain ASCII identifier used when saving to the config file.
286+
pub fn key(self) -> &'static str {
287+
match self {
288+
KeyboardEffect::Off => "off",
289+
KeyboardEffect::Static => "static",
290+
KeyboardEffect::Breathing => "breathing",
291+
KeyboardEffect::Spectrum => "spectrum",
292+
KeyboardEffect::WaveRight => "wave_right",
293+
KeyboardEffect::WaveLeft => "wave_left",
294+
}
295+
}
296+
297+
pub fn from_key(key: &str) -> Option<KeyboardEffect> {
298+
Self::ALL.into_iter().find(|e| e.key() == key)
299+
}
300+
301+
/// Whether the effect uses the picked color (Spectrum and Wave cycle through colors).
302+
pub fn uses_color(self) -> bool {
303+
matches!(self, KeyboardEffect::Static | KeyboardEffect::Breathing)
304+
}
305+
}
306+
307+
const KBD_VARSTORE: u8 = 0x01;
308+
/// The keyboard backlight LED, the same id `set_keyboard_brightness` uses ([1, 5, level]).
309+
const KBD_LED_BACKLIGHT: u8 = 0x05;
310+
const KBD_WAVE_SPEED: u8 = 0x28;
311+
312+
/// Arguments of the extended matrix effect command (class 0x0F, id 0x02).
313+
fn keyboard_effect_args(effect: KeyboardEffect, rgb: [u8; 3]) -> Vec<u8> {
314+
let [r, g, b] = rgb;
315+
let (vs, led) = (KBD_VARSTORE, KBD_LED_BACKLIGHT);
316+
match effect {
317+
KeyboardEffect::Off => vec![vs, led, 0x00, 0, 0, 0],
318+
KeyboardEffect::Static => vec![vs, led, 0x01, 0, 0, 0x01, r, g, b],
319+
KeyboardEffect::Breathing => vec![vs, led, 0x02, 0x01, 0, 0x01, r, g, b],
320+
KeyboardEffect::Spectrum => vec![vs, led, 0x03, 0, 0, 0],
321+
KeyboardEffect::WaveRight => vec![vs, led, 0x04, 0x01, KBD_WAVE_SPEED, 0],
322+
KeyboardEffect::WaveLeft => vec![vs, led, 0x04, 0x02, KBD_WAVE_SPEED, 0],
323+
}
324+
}
325+
326+
/// Arguments of the older "standard matrix effect" command (class 0x03, id 0x0A).
327+
fn keyboard_effect_args_standard(effect: KeyboardEffect, rgb: [u8; 3]) -> Vec<u8> {
328+
let [r, g, b] = rgb;
329+
match effect {
330+
KeyboardEffect::Off => vec![0x00],
331+
KeyboardEffect::Static => vec![0x06, r, g, b],
332+
KeyboardEffect::Breathing => vec![0x03, 0x01, r, g, b],
333+
KeyboardEffect::Spectrum => vec![0x04],
334+
KeyboardEffect::WaveRight => vec![0x01, 0x01],
335+
KeyboardEffect::WaveLeft => vec![0x01, 0x02],
336+
}
337+
}
338+
339+
/// Transaction id that newer Razer firmware expects on lighting commands.
340+
const KBD_TRANSACTION_ID: u8 = 0x1F;
341+
342+
/// How a keyboard effect is sent. The defaults are the most likely to work; the other
343+
/// combinations exist so a model that ignores them can be tried from Options > Advanced.
344+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
345+
pub struct KeyboardEffectOptions {
346+
/// Use the older standard matrix effect command (0x030A) instead of the extended one (0x0F02).
347+
pub legacy: bool,
348+
/// Use a random transaction id (like the other commands) instead of the fixed 0x1F.
349+
pub random_id: bool,
350+
/// Send an "Off" effect first for Static / Breathing, for firmware that ignores a new color
351+
/// while the same effect is already showing.
352+
pub clear_first: bool,
353+
/// Paint Static as a custom frame (every key set to the color) instead of the firmware's
354+
/// Static effect, which ignores the color on some laptops.
355+
pub static_frame: bool,
356+
}
357+
358+
/// Custom frame size used for a plain color: 6 rows of 15 keys (what razer-laptop-control uses).
359+
const KBD_FRAME_ROWS: u8 = 6;
360+
const KBD_FRAME_KEYS: usize = 15;
361+
362+
fn keyboard_frame_row_args(row: u8, rgb: [u8; 3]) -> Vec<u8> {
363+
let mut args = vec![0u8; 7 + 3 * KBD_FRAME_KEYS];
364+
args[0] = 0xff;
365+
args[1] = row;
366+
args[2] = 0x00; // first column
367+
args[3] = 0x0f; // last column
368+
for key in 0..KBD_FRAME_KEYS {
369+
args[7 + 3 * key..10 + 3 * key].copy_from_slice(&rgb);
370+
}
371+
args
372+
}
373+
374+
/// Paints the whole keyboard one color with a custom frame (class 0x03 ids 0x0B and 0x0A).
375+
fn set_keyboard_static_frame(device: &Device, rgb: [u8; 3]) -> Result<()> {
376+
for row in 0..KBD_FRAME_ROWS {
377+
device.send(
378+
Packet::new(0x030b, &keyboard_frame_row_args(row, rgb)).with_id(KBD_TRANSACTION_ID),
379+
)?;
380+
}
381+
// effect 0x05 = custom frame, 0x00 = do not store
382+
device.send(Packet::new(0x030a, &[0x05, 0x00]).with_id(KBD_TRANSACTION_ID))?;
383+
Ok(())
384+
}
385+
386+
fn keyboard_effect_packet(
387+
effect: KeyboardEffect,
388+
rgb: [u8; 3],
389+
opts: KeyboardEffectOptions,
390+
) -> Packet {
391+
let packet = if opts.legacy {
392+
Packet::new(0x030a, &keyboard_effect_args_standard(effect, rgb))
393+
} else {
394+
Packet::new(0x0f02, &keyboard_effect_args(effect, rgb))
395+
};
396+
if opts.random_id {
397+
packet
398+
} else {
399+
packet.with_id(KBD_TRANSACTION_ID)
400+
}
401+
}
402+
403+
/// Sets the laptop keyboard's lighting effect. Brightness is separate (`set_keyboard_brightness`).
404+
pub fn set_keyboard_effect(
405+
device: &Device,
406+
effect: KeyboardEffect,
407+
rgb: [u8; 3],
408+
opts: KeyboardEffectOptions,
409+
) -> Result<()> {
410+
if effect == KeyboardEffect::Static
411+
&& opts.static_frame
412+
&& set_keyboard_static_frame(device, rgb).is_ok()
413+
{
414+
return Ok(());
415+
}
416+
if opts.clear_first && effect.uses_color() {
417+
device.send(keyboard_effect_packet(KeyboardEffect::Off, rgb, opts))?;
418+
std::thread::sleep(std::time::Duration::from_millis(50));
419+
}
420+
device.send(keyboard_effect_packet(effect, rgb, opts))?;
421+
Ok(())
422+
}
423+
424+
#[cfg(test)]
425+
mod keyboard_effect_tests {
426+
use super::*;
427+
428+
#[test]
429+
fn static_effect_layout() {
430+
let args = keyboard_effect_args(KeyboardEffect::Static, [1, 2, 3]);
431+
assert_eq!(args, vec![1, 5, 1, 0, 0, 1, 1, 2, 3]);
432+
}
433+
434+
#[test]
435+
fn breathing_effect_layout() {
436+
let args = keyboard_effect_args(KeyboardEffect::Breathing, [9, 8, 7]);
437+
assert_eq!(args, vec![1, 5, 2, 1, 0, 1, 9, 8, 7]);
438+
}
439+
440+
#[test]
441+
fn frame_row_layout() {
442+
let args = keyboard_frame_row_args(3, [1, 2, 3]);
443+
assert_eq!(args.len(), 52);
444+
assert_eq!(&args[..8], &[0xff, 3, 0, 0x0f, 0, 0, 0, 1]);
445+
assert_eq!(&args[49..], &[1, 2, 3]);
446+
}
447+
448+
#[test]
449+
fn standard_effect_layout() {
450+
assert_eq!(
451+
keyboard_effect_args_standard(KeyboardEffect::Static, [1, 2, 3]),
452+
vec![6, 1, 2, 3]
453+
);
454+
assert_eq!(
455+
keyboard_effect_args_standard(KeyboardEffect::Breathing, [1, 2, 3]),
456+
vec![3, 1, 1, 2, 3]
457+
);
458+
assert_eq!(keyboard_effect_args_standard(KeyboardEffect::Spectrum, [0; 3]), vec![4]);
459+
assert_eq!(keyboard_effect_args_standard(KeyboardEffect::WaveLeft, [0; 3]), vec![1, 2]);
460+
}
461+
462+
#[test]
463+
fn names_round_trip() {
464+
for e in KeyboardEffect::ALL {
465+
assert_eq!(KeyboardEffect::from_name(e.name()), Some(e));
466+
assert_eq!(KeyboardEffect::from_key(e.key()), Some(e));
467+
}
468+
assert!(KeyboardEffect::from_name("nope").is_none());
469+
}
470+
}

0 commit comments

Comments
 (0)