LenovoLegionToolkit
919 строк · 34.8 Кб
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Linq;
5using System.Runtime.InteropServices;
6using System.Threading;
7using System.Threading.Tasks;
8using LenovoLegionToolkit.Lib.Extensions;
9using LenovoLegionToolkit.Lib.Listeners;
10using LenovoLegionToolkit.Lib.SoftwareDisabler;
11using LenovoLegionToolkit.Lib.System;
12using LenovoLegionToolkit.Lib.Utils;
13using Microsoft.Win32.SafeHandles;
14using NeoSmart.AsyncLock;
15using Newtonsoft.Json;
16using Newtonsoft.Json.Converters;
17using Windows.Win32;
18
19namespace LenovoLegionToolkit.Lib.Controllers;
20
21public class SpectrumKeyboardBacklightController
22{
23public interface IScreenCapture
24{
25void CaptureScreen(ref RGBColor[,] buffer, int width, int height, CancellationToken token);
26}
27
28private readonly struct KeyMap
29{
30public static readonly KeyMap Empty = new(0, 0, new ushort[0, 0], Array.Empty<ushort>());
31
32public readonly int Width;
33public readonly int Height;
34public readonly ushort[,] KeyCodes;
35public readonly ushort[] AdditionalKeyCodes;
36
37public KeyMap(int width, int height, ushort[,] keyCodes, ushort[] additionalKeyCodes)
38{
39Width = width;
40Height = height;
41KeyCodes = keyCodes;
42AdditionalKeyCodes = additionalKeyCodes;
43}
44}
45
46private static readonly AsyncLock GetDeviceHandleLock = new();
47private static readonly object IoLock = new();
48
49private readonly TimeSpan _auroraRefreshInterval = TimeSpan.FromMilliseconds(60);
50
51private readonly SpecialKeyListener _listener;
52private readonly VantageDisabler _vantageDisabler;
53private readonly IScreenCapture _screenCapture;
54
55private SafeFileHandle? _deviceHandle;
56
57private CancellationTokenSource? _auroraRefreshCancellationTokenSource;
58private Task? _auroraRefreshTask;
59
60private readonly JsonSerializerSettings _jsonSerializerSettings;
61
62public bool ForceDisable { get; set; }
63
64public SpectrumKeyboardBacklightController(SpecialKeyListener listener, VantageDisabler vantageDisabler, IScreenCapture screenCapture)
65{
66_listener = listener ?? throw new ArgumentNullException(nameof(listener));
67_vantageDisabler = vantageDisabler ?? throw new ArgumentNullException(nameof(vantageDisabler));
68_screenCapture = screenCapture ?? throw new ArgumentNullException(nameof(screenCapture));
69
70_jsonSerializerSettings = new()
71{
72Formatting = Formatting.Indented,
73TypeNameHandling = TypeNameHandling.Auto,
74ObjectCreationHandling = ObjectCreationHandling.Replace,
75Converters =
76{
77new StringEnumConverter(),
78}
79};
80
81_listener.Changed += Listener_Changed;
82}
83
84private async void Listener_Changed(object? sender, SpecialKey e)
85{
86if (!await IsSupportedAsync().ConfigureAwait(false))
87return;
88
89if (await _vantageDisabler.GetStatusAsync().ConfigureAwait(false) == SoftwareStatus.Enabled)
90return;
91
92switch (e)
93{
94case SpecialKey.SpectrumPreset1
95or SpecialKey.SpectrumPreset2
96or SpecialKey.SpectrumPreset3
97or SpecialKey.SpectrumPreset4
98or SpecialKey.SpectrumPreset5
99or SpecialKey.SpectrumPreset6:
100{
101await StartAuroraIfNeededAsync().ConfigureAwait(false);
102break;
103}
104}
105}
106
107public async Task<bool> IsSupportedAsync() => await GetDeviceHandleAsync().ConfigureAwait(false) is not null;
108
109public async Task<(SpectrumLayout, KeyboardLayout, HashSet<ushort>)> GetKeyboardLayoutAsync()
110{
111if (Log.Instance.IsTraceEnabled)
112Log.Instance.Trace($"Checking keyboard layout...");
113
114var (width, height, keys) = await ReadAllKeyCodesAsync().ConfigureAwait(false);
115
116var spectrumLayout = (width, height) switch
117{
118(22, 9) => SpectrumLayout.Full,
119(20, 8) => SpectrumLayout.KeyboardAndFront,
120_ => SpectrumLayout.KeyboardOnly // (20, 7)
121};
122
123var keyboardLayout = keys.Contains(0xA8) ? KeyboardLayout.Iso : KeyboardLayout.Ansi;
124
125if (Log.Instance.IsTraceEnabled)
126Log.Instance.Trace($"Layout is {spectrumLayout}, {keyboardLayout}.");
127
128return (spectrumLayout, keyboardLayout, keys);
129}
130
131public async Task<int> GetBrightnessAsync()
132{
133await ThrowIfVantageEnabled().ConfigureAwait(false);
134
135var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
136if (handle is null)
137throw new InvalidOperationException(nameof(handle));
138
139if (Log.Instance.IsTraceEnabled)
140Log.Instance.Trace($"Getting keyboard brightness...");
141
142var input = new LENOVO_SPECTRUM_GET_BRIGHTNESS_REQUEST();
143SetAndGetFeature(handle, input, out LENOVO_SPECTRUM_GET_BRIGHTNESS_RESPONSE output);
144var result = output.Brightness;
145
146if (Log.Instance.IsTraceEnabled)
147Log.Instance.Trace($"Keyboard brightness is {result}.");
148
149return result;
150}
151
152public async Task SetBrightnessAsync(int brightness)
153{
154await ThrowIfVantageEnabled().ConfigureAwait(false);
155
156var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
157if (handle is null)
158throw new InvalidOperationException(nameof(handle));
159
160if (Log.Instance.IsTraceEnabled)
161Log.Instance.Trace($"Setting keyboard brightness to: {brightness}.");
162
163var input = new LENOVO_SPECTRUM_SET_BRIGHTNESS_REQUEST((byte)brightness);
164SetFeature(handle, input);
165
166if (Log.Instance.IsTraceEnabled)
167Log.Instance.Trace($"Keyboard brightness set.");
168}
169
170public async Task<bool> GetLogoStatusAsync()
171{
172await ThrowIfVantageEnabled().ConfigureAwait(false);
173
174var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
175if (handle is null)
176throw new InvalidOperationException(nameof(handle));
177
178if (Log.Instance.IsTraceEnabled)
179Log.Instance.Trace($"Getting logo status...");
180
181var input = new LENOVO_SPECTRUM_GET_LOGO_STATUS();
182SetAndGetFeature(handle, input, out LENOVO_SPECTRUM_GET_LOGO_STATUS_RESPONSE output);
183var result = output.IsOn;
184
185if (Log.Instance.IsTraceEnabled)
186Log.Instance.Trace($"Logo status is {result}.");
187
188return result;
189}
190
191public async Task SetLogoStatusAsync(bool isOn)
192{
193await ThrowIfVantageEnabled().ConfigureAwait(false);
194
195var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
196if (handle is null)
197throw new InvalidOperationException(nameof(handle));
198
199if (Log.Instance.IsTraceEnabled)
200Log.Instance.Trace($"Setting logo status to: {isOn}.");
201
202var input = new LENOVO_SPECTRUM_SET_LOGO_STATUS_REQUEST(isOn);
203SetFeature(handle, input);
204
205if (Log.Instance.IsTraceEnabled)
206Log.Instance.Trace($"Logo status set.");
207}
208
209public async Task<int> GetProfileAsync()
210{
211await ThrowIfVantageEnabled().ConfigureAwait(false);
212
213var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
214if (handle is null)
215throw new InvalidOperationException(nameof(handle));
216
217if (Log.Instance.IsTraceEnabled)
218Log.Instance.Trace($"Getting keyboard profile...");
219
220var input = new LENOVO_SPECTRUM_GET_PROFILE_REQUEST();
221SetAndGetFeature(handle, input, out LENOVO_SPECTRUM_GET_PROFILE_RESPONSE output);
222var result = output.Profile;
223
224if (Log.Instance.IsTraceEnabled)
225Log.Instance.Trace($"Keyboard profile is {result}.");
226
227return result;
228}
229
230public async Task SetProfileAsync(int profile)
231{
232await ThrowIfVantageEnabled().ConfigureAwait(false);
233
234var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
235if (handle is null)
236throw new InvalidOperationException(nameof(handle));
237
238await StopAuroraIfNeededAsync().ConfigureAwait(false);
239
240if (Log.Instance.IsTraceEnabled)
241Log.Instance.Trace($"Setting keyboard profile to {profile}...");
242
243var input = new LENOVO_SPECTRUM_SET_PROFILE_REQUEST((byte)profile);
244SetFeature(handle, input);
245
246await Task.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(false);
247
248if (Log.Instance.IsTraceEnabled)
249Log.Instance.Trace($"Keyboard profile set to {profile}.");
250
251await StartAuroraIfNeededAsync(profile).ConfigureAwait(false);
252}
253
254public async Task SetProfileDefaultAsync(int profile)
255{
256await ThrowIfVantageEnabled().ConfigureAwait(false);
257
258var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
259if (handle is null)
260throw new InvalidOperationException(nameof(handle));
261
262if (Log.Instance.IsTraceEnabled)
263Log.Instance.Trace($"Setting keyboard profile {profile} to default...");
264
265if (Log.Instance.IsTraceEnabled)
266Log.Instance.Trace($"Keyboard profile {profile} set to default.");
267
268var input = new LENOVO_SPECTRUM_SET_PROFILE_DEFAULT_REQUEST((byte)profile);
269SetFeature(handle, input);
270}
271
272public async Task SetProfileDescriptionAsync(int profile, SpectrumKeyboardBacklightEffect[] effects)
273{
274await ThrowIfVantageEnabled().ConfigureAwait(false);
275
276var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
277if (handle is null)
278throw new InvalidOperationException(nameof(handle));
279
280if (Log.Instance.IsTraceEnabled)
281Log.Instance.Trace($"Setting {effects.Length} effect to keyboard profile {profile}...");
282
283effects = Compress(effects);
284var bytes = Convert(profile, effects).ToBytes();
285SetFeature(handle, bytes);
286
287if (Log.Instance.IsTraceEnabled)
288Log.Instance.Trace($"Set {effects.Length} effect to keyboard profile {profile}.");
289
290await StartAuroraIfNeededAsync(profile).ConfigureAwait(false);
291}
292
293public async Task<(int Profile, SpectrumKeyboardBacklightEffect[] Effects)> GetProfileDescriptionAsync(int profile)
294{
295await ThrowIfVantageEnabled().ConfigureAwait(false);
296
297var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
298if (handle is null)
299throw new InvalidOperationException(nameof(handle));
300
301if (Log.Instance.IsTraceEnabled)
302Log.Instance.Trace($"Getting effects for keyboard profile {profile}...");
303
304var input = new LENOVO_SPECTRUM_GET_EFFECT_REQUEST((byte)profile);
305SetAndGetFeature(handle, input, out var buffer, 960);
306
307var description = LENOVO_SPECTRUM_EFFECT_DESCRIPTION.FromBytes(buffer);
308var result = Convert(description);
309
310if (Log.Instance.IsTraceEnabled)
311Log.Instance.Trace($"Retrieved {result.Effects.Length} effects for keyboard profile {profile}...");
312
313return result;
314}
315
316public async Task ImportProfileDescription(int profile, string jsonPath)
317{
318var json = await File.ReadAllTextAsync(jsonPath).ConfigureAwait(false);
319var effects = JsonConvert.DeserializeObject<SpectrumKeyboardBacklightEffect[]>(json)
320?? throw new InvalidOperationException("Couldn't deserialize effects");
321
322await SetProfileDescriptionAsync(profile, effects).ConfigureAwait(false);
323}
324
325public async Task ExportProfileDescriptionAsync(int profile, string jsonPath)
326{
327var (_, effects) = await GetProfileDescriptionAsync(profile).ConfigureAwait(false);
328var json = JsonConvert.SerializeObject(effects, _jsonSerializerSettings);
329await File.WriteAllTextAsync(jsonPath, json).ConfigureAwait(false);
330}
331
332public async Task<bool> StartAuroraIfNeededAsync(int? profile = null)
333{
334await ThrowIfVantageEnabled().ConfigureAwait(false);
335
336await StopAuroraIfNeededAsync().ConfigureAwait(false);
337
338if (Log.Instance.IsTraceEnabled)
339Log.Instance.Trace($"Starting Aurora... [profile={profile}]");
340
341profile ??= await GetProfileAsync().ConfigureAwait(false);
342var (_, effects) = await GetProfileDescriptionAsync(profile.Value).ConfigureAwait(false);
343
344if (!effects.Any(e => e.Type == SpectrumKeyboardBacklightEffectType.AuroraSync))
345{
346if (Log.Instance.IsTraceEnabled)
347Log.Instance.Trace($"Aurora not needed. [profile={profile}]");
348
349return false;
350}
351
352_auroraRefreshCancellationTokenSource = new();
353var token = _auroraRefreshCancellationTokenSource.Token;
354_auroraRefreshTask = Task.Run(() => AuroraRefreshAsync(profile.Value, token), token);
355
356if (Log.Instance.IsTraceEnabled)
357Log.Instance.Trace($"Aurora started. [profile={profile}]");
358
359return true;
360}
361
362public async Task StopAuroraIfNeededAsync()
363{
364await ThrowIfVantageEnabled().ConfigureAwait(false);
365
366if (Log.Instance.IsTraceEnabled)
367Log.Instance.Trace($"Stopping Aurora...");
368
369_auroraRefreshCancellationTokenSource?.Cancel();
370if (_auroraRefreshTask is not null)
371await _auroraRefreshTask.ConfigureAwait(false);
372_auroraRefreshTask = null;
373
374if (Log.Instance.IsTraceEnabled)
375Log.Instance.Trace($"Aurora stopped.");
376}
377
378public async Task<Dictionary<ushort, RGBColor>> GetStateAsync()
379{
380await ThrowIfVantageEnabled().ConfigureAwait(false);
381
382var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
383if (handle is null)
384throw new InvalidOperationException(nameof(handle));
385
386GetFeature(handle, out LENOVO_SPECTRUM_STATE_RESPONSE state);
387
388var dict = new Dictionary<ushort, RGBColor>();
389
390foreach (var key in state.Data.Where(k => k.KeyCode > 0))
391{
392var rgb = new RGBColor(key.Color.R, key.Color.G, key.Color.B);
393dict.TryAdd(key.KeyCode, rgb);
394}
395
396return dict;
397}
398
399private async Task ThrowIfVantageEnabled()
400{
401var vantageStatus = await _vantageDisabler.GetStatusAsync().ConfigureAwait(false);
402if (vantageStatus == SoftwareStatus.Enabled)
403throw new InvalidOperationException("Can't manage Spectrum keyboard with Vantage enabled.");
404}
405
406private async Task<(int Width, int Height, HashSet<ushort> Keys)> ReadAllKeyCodesAsync()
407{
408var keyMap = await GetKeyMapAsync().ConfigureAwait(false);
409var keyCodes = new HashSet<ushort>(keyMap.Width * keyMap.Height);
410
411foreach (var keyCode in keyMap.KeyCodes)
412if (keyCode > 0)
413keyCodes.Add(keyCode);
414
415foreach (var keyCode in keyMap.AdditionalKeyCodes)
416if (keyCode > 0)
417keyCodes.Add(keyCode);
418
419return (keyMap.Width, keyMap.Height, keyCodes);
420}
421
422private async Task<KeyMap> GetKeyMapAsync()
423{
424try
425{
426var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
427if (handle is null)
428return KeyMap.Empty;
429
430SetAndGetFeature(handle,
431new LENOVO_SPECTRUM_GET_KEY_COUNT_REQUEST(),
432out LENOVO_SPECTRUM_GET_KEY_COUNT_RESPONSE keyCountResponse);
433
434var width = keyCountResponse.KeysPerIndex;
435var height = keyCountResponse.Indexes;
436
437var keyCodes = new ushort[width, height];
438var additionalKeyCodes = new ushort[width];
439
440for (var y = 0; y < height; y++)
441{
442SetAndGetFeature(handle,
443new LENOVO_SPECTRUM_GET_KEY_PAGE_REQUEST((byte)y),
444out LENOVO_SPECTRUM_GET_KEY_PAGE_RESPONSE keyPageResponse);
445
446for (var x = 0; x < width; x++)
447keyCodes[x, y] = keyPageResponse.Items[x].KeyCode;
448}
449
450SetAndGetFeature(handle,
451new LENOVO_SPECTRUM_GET_KEY_PAGE_REQUEST(0, true),
452out LENOVO_SPECTRUM_GET_KEY_PAGE_RESPONSE secondaryKeyPageResponse);
453
454for (var x = 0; x < width; x++)
455additionalKeyCodes[x] = secondaryKeyPageResponse.Items[x].KeyCode;
456
457return new(width, height, keyCodes, additionalKeyCodes);
458}
459catch
460{
461return KeyMap.Empty;
462}
463}
464
465private async Task AuroraRefreshAsync(int profile, CancellationToken token)
466{
467try
468{
469await ThrowIfVantageEnabled().ConfigureAwait(false);
470
471var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
472if (handle is null)
473throw new InvalidOperationException(nameof(handle));
474
475if (Log.Instance.IsTraceEnabled)
476Log.Instance.Trace($"Aurora refresh starting...");
477
478var keyMap = await GetKeyMapAsync().ConfigureAwait(false);
479var width = keyMap.Width;
480var height = keyMap.Height;
481var colorBuffer = new RGBColor[width, height];
482
483SetFeature(handle, new LENOVO_SPECTRUM_AURORA_START_STOP_REQUEST(true, (byte)profile));
484
485while (!token.IsCancellationRequested)
486{
487var delay = Task.Delay(_auroraRefreshInterval, token);
488
489try
490{
491_screenCapture.CaptureScreen(ref colorBuffer, width, height, token);
492}
493catch (Exception ex)
494{
495if (Log.Instance.IsTraceEnabled)
496Log.Instance.Trace($"Screen capture failed. Delaying before next refresh...", ex);
497
498await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
499}
500
501token.ThrowIfCancellationRequested();
502
503var items = new List<LENOVO_SPECTRUM_AURORA_ITEM>(width * height);
504
505var avgR = 0;
506var avgG = 0;
507var avgB = 0;
508
509for (var x = 0; x < width; x++)
510{
511for (var y = 0; y < height; y++)
512{
513var keyCode = keyMap.KeyCodes[x, y];
514if (keyCode < 1)
515continue;
516
517var color = colorBuffer[x, y];
518avgR += color.R;
519avgG += color.G;
520avgB += color.B;
521items.Add(new(keyCode, new(color.R, color.G, color.B)));
522}
523}
524
525avgR /= items.Count;
526avgG /= items.Count;
527avgB /= items.Count;
528
529for (var x = 0; x < width; x++)
530{
531var keyCode = keyMap.AdditionalKeyCodes[x];
532if (keyCode < 1)
533continue;
534
535items.Add(new(keyCode, new((byte)avgR, (byte)avgB, (byte)avgG)));
536}
537
538token.ThrowIfCancellationRequested();
539
540SetFeature(handle, new LENOVO_SPECTRUM_AURORA_SEND_BITMAP_REQUEST(items.ToArray()).ToBytes());
541
542await delay.ConfigureAwait(false);
543}
544}
545catch (OperationCanceledException) { }
546catch (Exception ex)
547{
548if (Log.Instance.IsTraceEnabled)
549Log.Instance.Trace($"Unexpected exception while refreshing Aurora.", ex);
550}
551finally
552{
553var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
554if (handle is not null)
555{
556var currentProfile = await GetProfileAsync().ConfigureAwait(false);
557SetFeature(handle, new LENOVO_SPECTRUM_AURORA_START_STOP_REQUEST(false, (byte)currentProfile));
558}
559
560if (Log.Instance.IsTraceEnabled)
561Log.Instance.Trace($"Aurora refresh stopped.");
562}
563}
564
565private async Task<SafeFileHandle?> GetDeviceHandleAsync()
566{
567if (ForceDisable)
568return null;
569
570try
571{
572using (await GetDeviceHandleLock.LockAsync().ConfigureAwait(false))
573{
574if (_deviceHandle is not null && IsReady(_deviceHandle))
575return _deviceHandle;
576
577SafeFileHandle? newDeviceHandle = null;
578
579const int retries = 3;
580const int delay = 50;
581
582for (var i = 0; i < retries; i++)
583{
584if (Log.Instance.IsTraceEnabled)
585Log.Instance.Trace($"Refreshing handle... [retry={i + 1}]");
586
587var tempDeviceHandle = Devices.GetSpectrumRGBKeyboard(true);
588if (tempDeviceHandle is not null && IsReady(tempDeviceHandle))
589{
590newDeviceHandle = tempDeviceHandle;
591break;
592}
593
594await Task.Delay(delay).ConfigureAwait(false);
595}
596
597if (newDeviceHandle is null)
598{
599if (Log.Instance.IsTraceEnabled)
600Log.Instance.Trace($"Handle couldn't be refreshed.");
601
602return null;
603}
604
605SetAndGetFeature(newDeviceHandle,
606new LENOVO_SPECTRUM_GET_COMPATIBILITY_REQUEST(),
607out LENOVO_SPECTRUM_GET_COMPATIBILITY_RESPONSE res);
608
609if (!res.IsCompatible)
610{
611if (Log.Instance.IsTraceEnabled)
612Log.Instance.Trace($"Handle not compatible.");
613
614return null;
615}
616
617if (Log.Instance.IsTraceEnabled)
618Log.Instance.Trace($"Handle refreshed.");
619
620_deviceHandle = newDeviceHandle;
621return newDeviceHandle;
622}
623}
624catch
625{
626return null;
627}
628}
629
630private static bool IsReady(SafeHandle handle)
631{
632try
633{
634var b = new byte[960];
635b[0] = 7;
636SetFeature(handle, b);
637return true;
638}
639catch
640{
641if (Log.Instance.IsTraceEnabled)
642Log.Instance.Trace($"Keyboard not ready.");
643
644return false;
645}
646}
647
648private static void SetAndGetFeature<TIn, TOut>(SafeHandle handle, TIn input, out TOut output) where TIn : notnull where TOut : struct
649{
650lock (IoLock)
651{
652SetFeature(handle, input);
653GetFeature(handle, out output);
654}
655}
656
657private static void SetAndGetFeature<TIn>(SafeHandle handle, TIn input, out byte[] output, int size) where TIn : notnull
658{
659lock (IoLock)
660{
661SetFeature(handle, input);
662GetFeature(handle, out output, size);
663}
664}
665
666private static unsafe void SetFeature<T>(SafeHandle handle, T str) where T : notnull
667{
668lock (IoLock)
669{
670var ptr = IntPtr.Zero;
671try
672{
673int size;
674if (str is byte[] bytes)
675{
676size = bytes.Length;
677ptr = Marshal.AllocHGlobal(size);
678Marshal.Copy(bytes, 0, ptr, size);
679}
680else
681{
682size = Marshal.SizeOf<T>();
683ptr = Marshal.AllocHGlobal(size);
684Marshal.StructureToPtr(str, ptr, false);
685}
686
687var result = PInvoke.HidD_SetFeature(handle, ptr.ToPointer(), (uint)size);
688if (!result)
689PInvokeExtensions.ThrowIfWin32Error(typeof(T).Name);
690}
691finally
692{
693Marshal.FreeHGlobal(ptr);
694}
695}
696}
697
698private static unsafe void GetFeature<T>(SafeHandle handle, out T str) where T : struct
699{
700lock (IoLock)
701{
702var ptr = IntPtr.Zero;
703try
704{
705var size = Marshal.SizeOf<T>();
706ptr = Marshal.AllocHGlobal(size);
707Marshal.Copy(new byte[] { 7 }, 0, ptr, 1);
708
709var result = PInvoke.HidD_GetFeature(handle, ptr.ToPointer(), (uint)size);
710if (!result)
711PInvokeExtensions.ThrowIfWin32Error(typeof(T).Name);
712
713str = Marshal.PtrToStructure<T>(ptr);
714}
715finally
716{
717Marshal.FreeHGlobal(ptr);
718}
719}
720}
721
722private static unsafe void GetFeature(SafeHandle handle, out byte[] bytes, int size)
723{
724lock (IoLock)
725{
726var ptr = IntPtr.Zero;
727try
728{
729ptr = Marshal.AllocHGlobal(size);
730Marshal.Copy(new byte[] { 7 }, 0, ptr, 1);
731
732var result = PInvoke.HidD_GetFeature(handle, ptr.ToPointer(), (uint)size);
733if (!result)
734PInvokeExtensions.ThrowIfWin32Error("bytes");
735
736bytes = new byte[size];
737Marshal.Copy(ptr, bytes, 0, size);
738}
739finally
740{
741Marshal.FreeHGlobal(ptr);
742}
743}
744}
745
746private static SpectrumKeyboardBacklightEffect[] Compress(SpectrumKeyboardBacklightEffect[] effects)
747{
748if (effects.Any(e => e.Type.IsAllLightsEffect()))
749return new[] { effects.Last(e => e.Type.IsAllLightsEffect()) };
750
751var usedKeyCodes = new HashSet<ushort>();
752var newEffects = new List<SpectrumKeyboardBacklightEffect>();
753
754foreach (var effect in effects.Reverse())
755{
756if (effect.Type.IsWholeKeyboardEffect() && usedKeyCodes.Intersect(effect.Keys).Any())
757continue;
758
759var newKeyCodes = effect.Keys.Except(usedKeyCodes).ToArray();
760
761foreach (var keyCode in newKeyCodes)
762usedKeyCodes.Add(keyCode);
763
764if (newKeyCodes.IsEmpty())
765continue;
766
767var newEffect = new SpectrumKeyboardBacklightEffect(effect.Type,
768effect.Speed,
769effect.Direction,
770effect.ClockwiseDirection,
771effect.Colors,
772newKeyCodes);
773
774newEffects.Add(newEffect);
775}
776
777newEffects.Reverse();
778return newEffects.ToArray();
779}
780
781private static (int Profile, SpectrumKeyboardBacklightEffect[] Effects) Convert(LENOVO_SPECTRUM_EFFECT_DESCRIPTION description)
782{
783var profile = description.Profile;
784var effects = description.Effects.Select(Convert).ToArray();
785return (profile, effects);
786}
787
788private static SpectrumKeyboardBacklightEffect Convert(LENOVO_SPECTRUM_EFFECT effect)
789{
790var effectType = effect.EffectHeader.EffectType switch
791{
792LENOVO_SPECTRUM_EFFECT_TYPE.Always => SpectrumKeyboardBacklightEffectType.Always,
793LENOVO_SPECTRUM_EFFECT_TYPE.LegionAuraSync => SpectrumKeyboardBacklightEffectType.AuroraSync,
794LENOVO_SPECTRUM_EFFECT_TYPE.AudioBounceLighting => SpectrumKeyboardBacklightEffectType.AudioBounce,
795LENOVO_SPECTRUM_EFFECT_TYPE.AudioRippleLighting => SpectrumKeyboardBacklightEffectType.AudioRipple,
796LENOVO_SPECTRUM_EFFECT_TYPE.ColorChange => SpectrumKeyboardBacklightEffectType.ColorChange,
797LENOVO_SPECTRUM_EFFECT_TYPE.ColorPulse => SpectrumKeyboardBacklightEffectType.ColorPulse,
798LENOVO_SPECTRUM_EFFECT_TYPE.ColorWave => SpectrumKeyboardBacklightEffectType.ColorWave,
799LENOVO_SPECTRUM_EFFECT_TYPE.Rain => SpectrumKeyboardBacklightEffectType.Rain,
800LENOVO_SPECTRUM_EFFECT_TYPE.ScrewRainbow => SpectrumKeyboardBacklightEffectType.RainbowScrew,
801LENOVO_SPECTRUM_EFFECT_TYPE.RainbowWave => SpectrumKeyboardBacklightEffectType.RainbowWave,
802LENOVO_SPECTRUM_EFFECT_TYPE.Ripple => SpectrumKeyboardBacklightEffectType.Ripple,
803LENOVO_SPECTRUM_EFFECT_TYPE.Smooth => SpectrumKeyboardBacklightEffectType.Smooth,
804LENOVO_SPECTRUM_EFFECT_TYPE.TypeLighting => SpectrumKeyboardBacklightEffectType.Type,
805_ => throw new ArgumentException(nameof(effect.EffectHeader.EffectType))
806};
807
808var speed = effect.EffectHeader.Speed switch
809{
810LENOVO_SPECTRUM_SPEED.Speed1 => SpectrumKeyboardBacklightSpeed.Speed1,
811LENOVO_SPECTRUM_SPEED.Speed2 => SpectrumKeyboardBacklightSpeed.Speed2,
812LENOVO_SPECTRUM_SPEED.Speed3 => SpectrumKeyboardBacklightSpeed.Speed3,
813_ => SpectrumKeyboardBacklightSpeed.None
814};
815
816var direction = effect.EffectHeader.Direction switch
817{
818LENOVO_SPECTRUM_DIRECTION.LeftToRight => SpectrumKeyboardBacklightDirection.LeftToRight,
819LENOVO_SPECTRUM_DIRECTION.RightToLeft => SpectrumKeyboardBacklightDirection.RightToLeft,
820LENOVO_SPECTRUM_DIRECTION.BottomToTop => SpectrumKeyboardBacklightDirection.BottomToTop,
821LENOVO_SPECTRUM_DIRECTION.TopToBottom => SpectrumKeyboardBacklightDirection.TopToBottom,
822_ => SpectrumKeyboardBacklightDirection.None
823};
824
825var clockwiseDirection = effect.EffectHeader.ClockwiseDirection switch
826{
827LENOVO_SPECTRUM_CLOCKWISE_DIRECTION.Clockwise => SpectrumKeyboardBacklightClockwiseDirection.Clockwise,
828LENOVO_SPECTRUM_CLOCKWISE_DIRECTION.CounterClockwise => SpectrumKeyboardBacklightClockwiseDirection.CounterClockwise,
829_ => SpectrumKeyboardBacklightClockwiseDirection.None
830};
831
832var colors = effect.Colors.Select(c => new RGBColor(c.R, c.G, c.B)).ToArray();
833
834var keys = effect.KeyCodes;
835if (effect.KeyCodes.Length == 1 && effect.KeyCodes[0] == 0x65)
836keys = Array.Empty<ushort>();
837
838return new(effectType, speed, direction, clockwiseDirection, colors, keys);
839}
840
841private static LENOVO_SPECTRUM_EFFECT_DESCRIPTION Convert(int profile, SpectrumKeyboardBacklightEffect[] effects)
842{
843var header = new LENOVO_SPECTRUM_HEADER(LENOVO_SPECTRUM_OPERATION_TYPE.EffectChange, 0); // Size will be set on serialization
844var str = effects.Select((e, i) => Convert(i, e)).ToArray();
845var result = new LENOVO_SPECTRUM_EFFECT_DESCRIPTION(header, (byte)profile, str);
846return result;
847}
848
849private static LENOVO_SPECTRUM_EFFECT Convert(int index, SpectrumKeyboardBacklightEffect effect)
850{
851var effectType = effect.Type switch
852{
853SpectrumKeyboardBacklightEffectType.Always => LENOVO_SPECTRUM_EFFECT_TYPE.Always,
854SpectrumKeyboardBacklightEffectType.AuroraSync => LENOVO_SPECTRUM_EFFECT_TYPE.LegionAuraSync,
855SpectrumKeyboardBacklightEffectType.AudioBounce => LENOVO_SPECTRUM_EFFECT_TYPE.AudioBounceLighting,
856SpectrumKeyboardBacklightEffectType.AudioRipple => LENOVO_SPECTRUM_EFFECT_TYPE.AudioRippleLighting,
857SpectrumKeyboardBacklightEffectType.ColorChange => LENOVO_SPECTRUM_EFFECT_TYPE.ColorChange,
858SpectrumKeyboardBacklightEffectType.ColorPulse => LENOVO_SPECTRUM_EFFECT_TYPE.ColorPulse,
859SpectrumKeyboardBacklightEffectType.ColorWave => LENOVO_SPECTRUM_EFFECT_TYPE.ColorWave,
860SpectrumKeyboardBacklightEffectType.Rain => LENOVO_SPECTRUM_EFFECT_TYPE.Rain,
861SpectrumKeyboardBacklightEffectType.RainbowScrew => LENOVO_SPECTRUM_EFFECT_TYPE.ScrewRainbow,
862SpectrumKeyboardBacklightEffectType.RainbowWave => LENOVO_SPECTRUM_EFFECT_TYPE.RainbowWave,
863SpectrumKeyboardBacklightEffectType.Ripple => LENOVO_SPECTRUM_EFFECT_TYPE.Ripple,
864SpectrumKeyboardBacklightEffectType.Smooth => LENOVO_SPECTRUM_EFFECT_TYPE.Smooth,
865SpectrumKeyboardBacklightEffectType.Type => LENOVO_SPECTRUM_EFFECT_TYPE.TypeLighting,
866_ => throw new ArgumentException(nameof(effect.Type))
867};
868
869var speed = effect.Speed switch
870{
871SpectrumKeyboardBacklightSpeed.Speed1 => LENOVO_SPECTRUM_SPEED.Speed1,
872SpectrumKeyboardBacklightSpeed.Speed2 => LENOVO_SPECTRUM_SPEED.Speed2,
873SpectrumKeyboardBacklightSpeed.Speed3 => LENOVO_SPECTRUM_SPEED.Speed3,
874_ => LENOVO_SPECTRUM_SPEED.None
875};
876
877var direction = effect.Direction switch
878{
879SpectrumKeyboardBacklightDirection.LeftToRight => LENOVO_SPECTRUM_DIRECTION.LeftToRight,
880SpectrumKeyboardBacklightDirection.RightToLeft => LENOVO_SPECTRUM_DIRECTION.RightToLeft,
881SpectrumKeyboardBacklightDirection.BottomToTop => LENOVO_SPECTRUM_DIRECTION.BottomToTop,
882SpectrumKeyboardBacklightDirection.TopToBottom => LENOVO_SPECTRUM_DIRECTION.TopToBottom,
883_ => LENOVO_SPECTRUM_DIRECTION.None
884};
885
886var clockwiseDirection = effect.ClockwiseDirection switch
887{
888SpectrumKeyboardBacklightClockwiseDirection.Clockwise => LENOVO_SPECTRUM_CLOCKWISE_DIRECTION.Clockwise,
889SpectrumKeyboardBacklightClockwiseDirection.CounterClockwise => LENOVO_SPECTRUM_CLOCKWISE_DIRECTION.CounterClockwise,
890_ => LENOVO_SPECTRUM_CLOCKWISE_DIRECTION.None
891};
892
893var colorMode = effect.Type switch
894{
895SpectrumKeyboardBacklightEffectType.Always => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
896SpectrumKeyboardBacklightEffectType.ColorChange when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
897SpectrumKeyboardBacklightEffectType.ColorPulse when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
898SpectrumKeyboardBacklightEffectType.ColorWave when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
899SpectrumKeyboardBacklightEffectType.Rain when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
900SpectrumKeyboardBacklightEffectType.Smooth when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
901SpectrumKeyboardBacklightEffectType.Ripple when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
902SpectrumKeyboardBacklightEffectType.Type when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
903SpectrumKeyboardBacklightEffectType.ColorChange => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
904SpectrumKeyboardBacklightEffectType.ColorPulse => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
905SpectrumKeyboardBacklightEffectType.ColorWave => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
906SpectrumKeyboardBacklightEffectType.Rain => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
907SpectrumKeyboardBacklightEffectType.Smooth => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
908SpectrumKeyboardBacklightEffectType.Ripple => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
909SpectrumKeyboardBacklightEffectType.Type => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
910_ => LENOVO_SPECTRUM_COLOR_MODE.None
911};
912
913var header = new LENOVO_SPECTRUM_EFFECT_HEADER(effectType, speed, direction, clockwiseDirection, colorMode);
914var colors = effect.Colors.Select(c => new LENOVO_SPECTRUM_COLOR(c.R, c.G, c.B)).ToArray();
915var keys = effect.Type.IsAllLightsEffect() ? new ushort[] { 0x65 } : effect.Keys;
916var result = new LENOVO_SPECTRUM_EFFECT(header, index + 1, colors, keys);
917return result;
918}
919}
920