@@ -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