LenovoLegionToolkit

Форк
0
/
SpectrumKeyboardBacklightController.cs 
919 строк · 34.8 Кб
1
using System;
2
using System.Collections.Generic;
3
using System.IO;
4
using System.Linq;
5
using System.Runtime.InteropServices;
6
using System.Threading;
7
using System.Threading.Tasks;
8
using LenovoLegionToolkit.Lib.Extensions;
9
using LenovoLegionToolkit.Lib.Listeners;
10
using LenovoLegionToolkit.Lib.SoftwareDisabler;
11
using LenovoLegionToolkit.Lib.System;
12
using LenovoLegionToolkit.Lib.Utils;
13
using Microsoft.Win32.SafeHandles;
14
using NeoSmart.AsyncLock;
15
using Newtonsoft.Json;
16
using Newtonsoft.Json.Converters;
17
using Windows.Win32;
18

19
namespace LenovoLegionToolkit.Lib.Controllers;
20

21
public class SpectrumKeyboardBacklightController
22
{
23
    public interface IScreenCapture
24
    {
25
        void CaptureScreen(ref RGBColor[,] buffer, int width, int height, CancellationToken token);
26
    }
27

28
    private readonly struct KeyMap
29
    {
30
        public static readonly KeyMap Empty = new(0, 0, new ushort[0, 0], Array.Empty<ushort>());
31

32
        public readonly int Width;
33
        public readonly int Height;
34
        public readonly ushort[,] KeyCodes;
35
        public readonly ushort[] AdditionalKeyCodes;
36

37
        public KeyMap(int width, int height, ushort[,] keyCodes, ushort[] additionalKeyCodes)
38
        {
39
            Width = width;
40
            Height = height;
41
            KeyCodes = keyCodes;
42
            AdditionalKeyCodes = additionalKeyCodes;
43
        }
44
    }
45

46
    private static readonly AsyncLock GetDeviceHandleLock = new();
47
    private static readonly object IoLock = new();
48

49
    private readonly TimeSpan _auroraRefreshInterval = TimeSpan.FromMilliseconds(60);
50

51
    private readonly SpecialKeyListener _listener;
52
    private readonly VantageDisabler _vantageDisabler;
53
    private readonly IScreenCapture _screenCapture;
54

55
    private SafeFileHandle? _deviceHandle;
56

57
    private CancellationTokenSource? _auroraRefreshCancellationTokenSource;
58
    private Task? _auroraRefreshTask;
59

60
    private readonly JsonSerializerSettings _jsonSerializerSettings;
61

62
    public bool ForceDisable { get; set; }
63

64
    public 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
        {
72
            Formatting = Formatting.Indented,
73
            TypeNameHandling = TypeNameHandling.Auto,
74
            ObjectCreationHandling = ObjectCreationHandling.Replace,
75
            Converters =
76
            {
77
                new StringEnumConverter(),
78
            }
79
        };
80

81
        _listener.Changed += Listener_Changed;
82
    }
83

84
    private async void Listener_Changed(object? sender, SpecialKey e)
85
    {
86
        if (!await IsSupportedAsync().ConfigureAwait(false))
87
            return;
88

89
        if (await _vantageDisabler.GetStatusAsync().ConfigureAwait(false) == SoftwareStatus.Enabled)
90
            return;
91

92
        switch (e)
93
        {
94
            case SpecialKey.SpectrumPreset1
95
                or SpecialKey.SpectrumPreset2
96
                or SpecialKey.SpectrumPreset3
97
                or SpecialKey.SpectrumPreset4
98
                or SpecialKey.SpectrumPreset5
99
                or SpecialKey.SpectrumPreset6:
100
                {
101
                    await StartAuroraIfNeededAsync().ConfigureAwait(false);
102
                    break;
103
                }
104
        }
105
    }
106

107
    public async Task<bool> IsSupportedAsync() => await GetDeviceHandleAsync().ConfigureAwait(false) is not null;
108

109
    public async Task<(SpectrumLayout, KeyboardLayout, HashSet<ushort>)> GetKeyboardLayoutAsync()
110
    {
111
        if (Log.Instance.IsTraceEnabled)
112
            Log.Instance.Trace($"Checking keyboard layout...");
113

114
        var (width, height, keys) = await ReadAllKeyCodesAsync().ConfigureAwait(false);
115

116
        var spectrumLayout = (width, height) switch
117
        {
118
            (22, 9) => SpectrumLayout.Full,
119
            (20, 8) => SpectrumLayout.KeyboardAndFront,
120
            _ => SpectrumLayout.KeyboardOnly // (20, 7)
121
        };
122

123
        var keyboardLayout = keys.Contains(0xA8) ? KeyboardLayout.Iso : KeyboardLayout.Ansi;
124

125
        if (Log.Instance.IsTraceEnabled)
126
            Log.Instance.Trace($"Layout is {spectrumLayout}, {keyboardLayout}.");
127

128
        return (spectrumLayout, keyboardLayout, keys);
129
    }
130

131
    public async Task<int> GetBrightnessAsync()
132
    {
133
        await ThrowIfVantageEnabled().ConfigureAwait(false);
134

135
        var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
136
        if (handle is null)
137
            throw new InvalidOperationException(nameof(handle));
138

139
        if (Log.Instance.IsTraceEnabled)
140
            Log.Instance.Trace($"Getting keyboard brightness...");
141

142
        var input = new LENOVO_SPECTRUM_GET_BRIGHTNESS_REQUEST();
143
        SetAndGetFeature(handle, input, out LENOVO_SPECTRUM_GET_BRIGHTNESS_RESPONSE output);
144
        var result = output.Brightness;
145

146
        if (Log.Instance.IsTraceEnabled)
147
            Log.Instance.Trace($"Keyboard brightness is {result}.");
148

149
        return result;
150
    }
151

152
    public async Task SetBrightnessAsync(int brightness)
153
    {
154
        await ThrowIfVantageEnabled().ConfigureAwait(false);
155

156
        var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
157
        if (handle is null)
158
            throw new InvalidOperationException(nameof(handle));
159

160
        if (Log.Instance.IsTraceEnabled)
161
            Log.Instance.Trace($"Setting keyboard brightness to: {brightness}.");
162

163
        var input = new LENOVO_SPECTRUM_SET_BRIGHTNESS_REQUEST((byte)brightness);
164
        SetFeature(handle, input);
165

166
        if (Log.Instance.IsTraceEnabled)
167
            Log.Instance.Trace($"Keyboard brightness set.");
168
    }
169

170
    public async Task<bool> GetLogoStatusAsync()
171
    {
172
        await ThrowIfVantageEnabled().ConfigureAwait(false);
173

174
        var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
175
        if (handle is null)
176
            throw new InvalidOperationException(nameof(handle));
177

178
        if (Log.Instance.IsTraceEnabled)
179
            Log.Instance.Trace($"Getting logo status...");
180

181
        var input = new LENOVO_SPECTRUM_GET_LOGO_STATUS();
182
        SetAndGetFeature(handle, input, out LENOVO_SPECTRUM_GET_LOGO_STATUS_RESPONSE output);
183
        var result = output.IsOn;
184

185
        if (Log.Instance.IsTraceEnabled)
186
            Log.Instance.Trace($"Logo status is {result}.");
187

188
        return result;
189
    }
190

191
    public async Task SetLogoStatusAsync(bool isOn)
192
    {
193
        await ThrowIfVantageEnabled().ConfigureAwait(false);
194

195
        var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
196
        if (handle is null)
197
            throw new InvalidOperationException(nameof(handle));
198

199
        if (Log.Instance.IsTraceEnabled)
200
            Log.Instance.Trace($"Setting logo status to: {isOn}.");
201

202
        var input = new LENOVO_SPECTRUM_SET_LOGO_STATUS_REQUEST(isOn);
203
        SetFeature(handle, input);
204

205
        if (Log.Instance.IsTraceEnabled)
206
            Log.Instance.Trace($"Logo status set.");
207
    }
208

209
    public async Task<int> GetProfileAsync()
210
    {
211
        await ThrowIfVantageEnabled().ConfigureAwait(false);
212

213
        var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
214
        if (handle is null)
215
            throw new InvalidOperationException(nameof(handle));
216

217
        if (Log.Instance.IsTraceEnabled)
218
            Log.Instance.Trace($"Getting keyboard profile...");
219

220
        var input = new LENOVO_SPECTRUM_GET_PROFILE_REQUEST();
221
        SetAndGetFeature(handle, input, out LENOVO_SPECTRUM_GET_PROFILE_RESPONSE output);
222
        var result = output.Profile;
223

224
        if (Log.Instance.IsTraceEnabled)
225
            Log.Instance.Trace($"Keyboard profile is {result}.");
226

227
        return result;
228
    }
229

230
    public async Task SetProfileAsync(int profile)
231
    {
232
        await ThrowIfVantageEnabled().ConfigureAwait(false);
233

234
        var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
235
        if (handle is null)
236
            throw new InvalidOperationException(nameof(handle));
237

238
        await StopAuroraIfNeededAsync().ConfigureAwait(false);
239

240
        if (Log.Instance.IsTraceEnabled)
241
            Log.Instance.Trace($"Setting keyboard profile to {profile}...");
242

243
        var input = new LENOVO_SPECTRUM_SET_PROFILE_REQUEST((byte)profile);
244
        SetFeature(handle, input);
245

246
        await Task.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(false);
247

248
        if (Log.Instance.IsTraceEnabled)
249
            Log.Instance.Trace($"Keyboard profile set to {profile}.");
250

251
        await StartAuroraIfNeededAsync(profile).ConfigureAwait(false);
252
    }
253

254
    public async Task SetProfileDefaultAsync(int profile)
255
    {
256
        await ThrowIfVantageEnabled().ConfigureAwait(false);
257

258
        var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
259
        if (handle is null)
260
            throw new InvalidOperationException(nameof(handle));
261

262
        if (Log.Instance.IsTraceEnabled)
263
            Log.Instance.Trace($"Setting keyboard profile {profile} to default...");
264

265
        if (Log.Instance.IsTraceEnabled)
266
            Log.Instance.Trace($"Keyboard profile {profile} set to default.");
267

268
        var input = new LENOVO_SPECTRUM_SET_PROFILE_DEFAULT_REQUEST((byte)profile);
269
        SetFeature(handle, input);
270
    }
271

272
    public async Task SetProfileDescriptionAsync(int profile, SpectrumKeyboardBacklightEffect[] effects)
273
    {
274
        await ThrowIfVantageEnabled().ConfigureAwait(false);
275

276
        var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
277
        if (handle is null)
278
            throw new InvalidOperationException(nameof(handle));
279

280
        if (Log.Instance.IsTraceEnabled)
281
            Log.Instance.Trace($"Setting {effects.Length} effect to keyboard profile {profile}...");
282

283
        effects = Compress(effects);
284
        var bytes = Convert(profile, effects).ToBytes();
285
        SetFeature(handle, bytes);
286

287
        if (Log.Instance.IsTraceEnabled)
288
            Log.Instance.Trace($"Set {effects.Length} effect to keyboard profile {profile}.");
289

290
        await StartAuroraIfNeededAsync(profile).ConfigureAwait(false);
291
    }
292

293
    public async Task<(int Profile, SpectrumKeyboardBacklightEffect[] Effects)> GetProfileDescriptionAsync(int profile)
294
    {
295
        await ThrowIfVantageEnabled().ConfigureAwait(false);
296

297
        var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
298
        if (handle is null)
299
            throw new InvalidOperationException(nameof(handle));
300

301
        if (Log.Instance.IsTraceEnabled)
302
            Log.Instance.Trace($"Getting effects for keyboard profile {profile}...");
303

304
        var input = new LENOVO_SPECTRUM_GET_EFFECT_REQUEST((byte)profile);
305
        SetAndGetFeature(handle, input, out var buffer, 960);
306

307
        var description = LENOVO_SPECTRUM_EFFECT_DESCRIPTION.FromBytes(buffer);
308
        var result = Convert(description);
309

310
        if (Log.Instance.IsTraceEnabled)
311
            Log.Instance.Trace($"Retrieved {result.Effects.Length} effects for keyboard profile {profile}...");
312

313
        return result;
314
    }
315

316
    public async Task ImportProfileDescription(int profile, string jsonPath)
317
    {
318
        var json = await File.ReadAllTextAsync(jsonPath).ConfigureAwait(false);
319
        var effects = JsonConvert.DeserializeObject<SpectrumKeyboardBacklightEffect[]>(json)
320
                      ?? throw new InvalidOperationException("Couldn't deserialize effects");
321

322
        await SetProfileDescriptionAsync(profile, effects).ConfigureAwait(false);
323
    }
324

325
    public async Task ExportProfileDescriptionAsync(int profile, string jsonPath)
326
    {
327
        var (_, effects) = await GetProfileDescriptionAsync(profile).ConfigureAwait(false);
328
        var json = JsonConvert.SerializeObject(effects, _jsonSerializerSettings);
329
        await File.WriteAllTextAsync(jsonPath, json).ConfigureAwait(false);
330
    }
331

332
    public async Task<bool> StartAuroraIfNeededAsync(int? profile = null)
333
    {
334
        await ThrowIfVantageEnabled().ConfigureAwait(false);
335

336
        await StopAuroraIfNeededAsync().ConfigureAwait(false);
337

338
        if (Log.Instance.IsTraceEnabled)
339
            Log.Instance.Trace($"Starting Aurora... [profile={profile}]");
340

341
        profile ??= await GetProfileAsync().ConfigureAwait(false);
342
        var (_, effects) = await GetProfileDescriptionAsync(profile.Value).ConfigureAwait(false);
343

344
        if (!effects.Any(e => e.Type == SpectrumKeyboardBacklightEffectType.AuroraSync))
345
        {
346
            if (Log.Instance.IsTraceEnabled)
347
                Log.Instance.Trace($"Aurora not needed. [profile={profile}]");
348

349
            return false;
350
        }
351

352
        _auroraRefreshCancellationTokenSource = new();
353
        var token = _auroraRefreshCancellationTokenSource.Token;
354
        _auroraRefreshTask = Task.Run(() => AuroraRefreshAsync(profile.Value, token), token);
355

356
        if (Log.Instance.IsTraceEnabled)
357
            Log.Instance.Trace($"Aurora started. [profile={profile}]");
358

359
        return true;
360
    }
361

362
    public async Task StopAuroraIfNeededAsync()
363
    {
364
        await ThrowIfVantageEnabled().ConfigureAwait(false);
365

366
        if (Log.Instance.IsTraceEnabled)
367
            Log.Instance.Trace($"Stopping Aurora...");
368

369
        _auroraRefreshCancellationTokenSource?.Cancel();
370
        if (_auroraRefreshTask is not null)
371
            await _auroraRefreshTask.ConfigureAwait(false);
372
        _auroraRefreshTask = null;
373

374
        if (Log.Instance.IsTraceEnabled)
375
            Log.Instance.Trace($"Aurora stopped.");
376
    }
377

378
    public async Task<Dictionary<ushort, RGBColor>> GetStateAsync()
379
    {
380
        await ThrowIfVantageEnabled().ConfigureAwait(false);
381

382
        var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
383
        if (handle is null)
384
            throw new InvalidOperationException(nameof(handle));
385

386
        GetFeature(handle, out LENOVO_SPECTRUM_STATE_RESPONSE state);
387

388
        var dict = new Dictionary<ushort, RGBColor>();
389

390
        foreach (var key in state.Data.Where(k => k.KeyCode > 0))
391
        {
392
            var rgb = new RGBColor(key.Color.R, key.Color.G, key.Color.B);
393
            dict.TryAdd(key.KeyCode, rgb);
394
        }
395

396
        return dict;
397
    }
398

399
    private async Task ThrowIfVantageEnabled()
400
    {
401
        var vantageStatus = await _vantageDisabler.GetStatusAsync().ConfigureAwait(false);
402
        if (vantageStatus == SoftwareStatus.Enabled)
403
            throw new InvalidOperationException("Can't manage Spectrum keyboard with Vantage enabled.");
404
    }
405

406
    private async Task<(int Width, int Height, HashSet<ushort> Keys)> ReadAllKeyCodesAsync()
407
    {
408
        var keyMap = await GetKeyMapAsync().ConfigureAwait(false);
409
        var keyCodes = new HashSet<ushort>(keyMap.Width * keyMap.Height);
410

411
        foreach (var keyCode in keyMap.KeyCodes)
412
            if (keyCode > 0)
413
                keyCodes.Add(keyCode);
414

415
        foreach (var keyCode in keyMap.AdditionalKeyCodes)
416
            if (keyCode > 0)
417
                keyCodes.Add(keyCode);
418

419
        return (keyMap.Width, keyMap.Height, keyCodes);
420
    }
421

422
    private async Task<KeyMap> GetKeyMapAsync()
423
    {
424
        try
425
        {
426
            var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
427
            if (handle is null)
428
                return KeyMap.Empty;
429

430
            SetAndGetFeature(handle,
431
                new LENOVO_SPECTRUM_GET_KEY_COUNT_REQUEST(),
432
                out LENOVO_SPECTRUM_GET_KEY_COUNT_RESPONSE keyCountResponse);
433

434
            var width = keyCountResponse.KeysPerIndex;
435
            var height = keyCountResponse.Indexes;
436

437
            var keyCodes = new ushort[width, height];
438
            var additionalKeyCodes = new ushort[width];
439

440
            for (var y = 0; y < height; y++)
441
            {
442
                SetAndGetFeature(handle,
443
                    new LENOVO_SPECTRUM_GET_KEY_PAGE_REQUEST((byte)y),
444
                    out LENOVO_SPECTRUM_GET_KEY_PAGE_RESPONSE keyPageResponse);
445

446
                for (var x = 0; x < width; x++)
447
                    keyCodes[x, y] = keyPageResponse.Items[x].KeyCode;
448
            }
449

450
            SetAndGetFeature(handle,
451
                new LENOVO_SPECTRUM_GET_KEY_PAGE_REQUEST(0, true),
452
                out LENOVO_SPECTRUM_GET_KEY_PAGE_RESPONSE secondaryKeyPageResponse);
453

454
            for (var x = 0; x < width; x++)
455
                additionalKeyCodes[x] = secondaryKeyPageResponse.Items[x].KeyCode;
456

457
            return new(width, height, keyCodes, additionalKeyCodes);
458
        }
459
        catch
460
        {
461
            return KeyMap.Empty;
462
        }
463
    }
464

465
    private async Task AuroraRefreshAsync(int profile, CancellationToken token)
466
    {
467
        try
468
        {
469
            await ThrowIfVantageEnabled().ConfigureAwait(false);
470

471
            var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
472
            if (handle is null)
473
                throw new InvalidOperationException(nameof(handle));
474

475
            if (Log.Instance.IsTraceEnabled)
476
                Log.Instance.Trace($"Aurora refresh starting...");
477

478
            var keyMap = await GetKeyMapAsync().ConfigureAwait(false);
479
            var width = keyMap.Width;
480
            var height = keyMap.Height;
481
            var colorBuffer = new RGBColor[width, height];
482

483
            SetFeature(handle, new LENOVO_SPECTRUM_AURORA_START_STOP_REQUEST(true, (byte)profile));
484

485
            while (!token.IsCancellationRequested)
486
            {
487
                var delay = Task.Delay(_auroraRefreshInterval, token);
488

489
                try
490
                {
491
                    _screenCapture.CaptureScreen(ref colorBuffer, width, height, token);
492
                }
493
                catch (Exception ex)
494
                {
495
                    if (Log.Instance.IsTraceEnabled)
496
                        Log.Instance.Trace($"Screen capture failed. Delaying before next refresh...", ex);
497

498
                    await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
499
                }
500

501
                token.ThrowIfCancellationRequested();
502

503
                var items = new List<LENOVO_SPECTRUM_AURORA_ITEM>(width * height);
504

505
                var avgR = 0;
506
                var avgG = 0;
507
                var avgB = 0;
508

509
                for (var x = 0; x < width; x++)
510
                {
511
                    for (var y = 0; y < height; y++)
512
                    {
513
                        var keyCode = keyMap.KeyCodes[x, y];
514
                        if (keyCode < 1)
515
                            continue;
516

517
                        var color = colorBuffer[x, y];
518
                        avgR += color.R;
519
                        avgG += color.G;
520
                        avgB += color.B;
521
                        items.Add(new(keyCode, new(color.R, color.G, color.B)));
522
                    }
523
                }
524

525
                avgR /= items.Count;
526
                avgG /= items.Count;
527
                avgB /= items.Count;
528

529
                for (var x = 0; x < width; x++)
530
                {
531
                    var keyCode = keyMap.AdditionalKeyCodes[x];
532
                    if (keyCode < 1)
533
                        continue;
534

535
                    items.Add(new(keyCode, new((byte)avgR, (byte)avgB, (byte)avgG)));
536
                }
537

538
                token.ThrowIfCancellationRequested();
539

540
                SetFeature(handle, new LENOVO_SPECTRUM_AURORA_SEND_BITMAP_REQUEST(items.ToArray()).ToBytes());
541

542
                await delay.ConfigureAwait(false);
543
            }
544
        }
545
        catch (OperationCanceledException) { }
546
        catch (Exception ex)
547
        {
548
            if (Log.Instance.IsTraceEnabled)
549
                Log.Instance.Trace($"Unexpected exception while refreshing Aurora.", ex);
550
        }
551
        finally
552
        {
553
            var handle = await GetDeviceHandleAsync().ConfigureAwait(false);
554
            if (handle is not null)
555
            {
556
                var currentProfile = await GetProfileAsync().ConfigureAwait(false);
557
                SetFeature(handle, new LENOVO_SPECTRUM_AURORA_START_STOP_REQUEST(false, (byte)currentProfile));
558
            }
559

560
            if (Log.Instance.IsTraceEnabled)
561
                Log.Instance.Trace($"Aurora refresh stopped.");
562
        }
563
    }
564

565
    private async Task<SafeFileHandle?> GetDeviceHandleAsync()
566
    {
567
        if (ForceDisable)
568
            return null;
569

570
        try
571
        {
572
            using (await GetDeviceHandleLock.LockAsync().ConfigureAwait(false))
573
            {
574
                if (_deviceHandle is not null && IsReady(_deviceHandle))
575
                    return _deviceHandle;
576

577
                SafeFileHandle? newDeviceHandle = null;
578

579
                const int retries = 3;
580
                const int delay = 50;
581

582
                for (var i = 0; i < retries; i++)
583
                {
584
                    if (Log.Instance.IsTraceEnabled)
585
                        Log.Instance.Trace($"Refreshing handle... [retry={i + 1}]");
586

587
                    var tempDeviceHandle = Devices.GetSpectrumRGBKeyboard(true);
588
                    if (tempDeviceHandle is not null && IsReady(tempDeviceHandle))
589
                    {
590
                        newDeviceHandle = tempDeviceHandle;
591
                        break;
592
                    }
593

594
                    await Task.Delay(delay).ConfigureAwait(false);
595
                }
596

597
                if (newDeviceHandle is null)
598
                {
599
                    if (Log.Instance.IsTraceEnabled)
600
                        Log.Instance.Trace($"Handle couldn't be refreshed.");
601

602
                    return null;
603
                }
604

605
                SetAndGetFeature(newDeviceHandle,
606
                    new LENOVO_SPECTRUM_GET_COMPATIBILITY_REQUEST(),
607
                    out LENOVO_SPECTRUM_GET_COMPATIBILITY_RESPONSE res);
608

609
                if (!res.IsCompatible)
610
                {
611
                    if (Log.Instance.IsTraceEnabled)
612
                        Log.Instance.Trace($"Handle not compatible.");
613

614
                    return null;
615
                }
616

617
                if (Log.Instance.IsTraceEnabled)
618
                    Log.Instance.Trace($"Handle refreshed.");
619

620
                _deviceHandle = newDeviceHandle;
621
                return newDeviceHandle;
622
            }
623
        }
624
        catch
625
        {
626
            return null;
627
        }
628
    }
629

630
    private static bool IsReady(SafeHandle handle)
631
    {
632
        try
633
        {
634
            var b = new byte[960];
635
            b[0] = 7;
636
            SetFeature(handle, b);
637
            return true;
638
        }
639
        catch
640
        {
641
            if (Log.Instance.IsTraceEnabled)
642
                Log.Instance.Trace($"Keyboard not ready.");
643

644
            return false;
645
        }
646
    }
647

648
    private static void SetAndGetFeature<TIn, TOut>(SafeHandle handle, TIn input, out TOut output) where TIn : notnull where TOut : struct
649
    {
650
        lock (IoLock)
651
        {
652
            SetFeature(handle, input);
653
            GetFeature(handle, out output);
654
        }
655
    }
656

657
    private static void SetAndGetFeature<TIn>(SafeHandle handle, TIn input, out byte[] output, int size) where TIn : notnull
658
    {
659
        lock (IoLock)
660
        {
661
            SetFeature(handle, input);
662
            GetFeature(handle, out output, size);
663
        }
664
    }
665

666
    private static unsafe void SetFeature<T>(SafeHandle handle, T str) where T : notnull
667
    {
668
        lock (IoLock)
669
        {
670
            var ptr = IntPtr.Zero;
671
            try
672
            {
673
                int size;
674
                if (str is byte[] bytes)
675
                {
676
                    size = bytes.Length;
677
                    ptr = Marshal.AllocHGlobal(size);
678
                    Marshal.Copy(bytes, 0, ptr, size);
679
                }
680
                else
681
                {
682
                    size = Marshal.SizeOf<T>();
683
                    ptr = Marshal.AllocHGlobal(size);
684
                    Marshal.StructureToPtr(str, ptr, false);
685
                }
686

687
                var result = PInvoke.HidD_SetFeature(handle, ptr.ToPointer(), (uint)size);
688
                if (!result)
689
                    PInvokeExtensions.ThrowIfWin32Error(typeof(T).Name);
690
            }
691
            finally
692
            {
693
                Marshal.FreeHGlobal(ptr);
694
            }
695
        }
696
    }
697

698
    private static unsafe void GetFeature<T>(SafeHandle handle, out T str) where T : struct
699
    {
700
        lock (IoLock)
701
        {
702
            var ptr = IntPtr.Zero;
703
            try
704
            {
705
                var size = Marshal.SizeOf<T>();
706
                ptr = Marshal.AllocHGlobal(size);
707
                Marshal.Copy(new byte[] { 7 }, 0, ptr, 1);
708

709
                var result = PInvoke.HidD_GetFeature(handle, ptr.ToPointer(), (uint)size);
710
                if (!result)
711
                    PInvokeExtensions.ThrowIfWin32Error(typeof(T).Name);
712

713
                str = Marshal.PtrToStructure<T>(ptr);
714
            }
715
            finally
716
            {
717
                Marshal.FreeHGlobal(ptr);
718
            }
719
        }
720
    }
721

722
    private static unsafe void GetFeature(SafeHandle handle, out byte[] bytes, int size)
723
    {
724
        lock (IoLock)
725
        {
726
            var ptr = IntPtr.Zero;
727
            try
728
            {
729
                ptr = Marshal.AllocHGlobal(size);
730
                Marshal.Copy(new byte[] { 7 }, 0, ptr, 1);
731

732
                var result = PInvoke.HidD_GetFeature(handle, ptr.ToPointer(), (uint)size);
733
                if (!result)
734
                    PInvokeExtensions.ThrowIfWin32Error("bytes");
735

736
                bytes = new byte[size];
737
                Marshal.Copy(ptr, bytes, 0, size);
738
            }
739
            finally
740
            {
741
                Marshal.FreeHGlobal(ptr);
742
            }
743
        }
744
    }
745

746
    private static SpectrumKeyboardBacklightEffect[] Compress(SpectrumKeyboardBacklightEffect[] effects)
747
    {
748
        if (effects.Any(e => e.Type.IsAllLightsEffect()))
749
            return new[] { effects.Last(e => e.Type.IsAllLightsEffect()) };
750

751
        var usedKeyCodes = new HashSet<ushort>();
752
        var newEffects = new List<SpectrumKeyboardBacklightEffect>();
753

754
        foreach (var effect in effects.Reverse())
755
        {
756
            if (effect.Type.IsWholeKeyboardEffect() && usedKeyCodes.Intersect(effect.Keys).Any())
757
                continue;
758

759
            var newKeyCodes = effect.Keys.Except(usedKeyCodes).ToArray();
760

761
            foreach (var keyCode in newKeyCodes)
762
                usedKeyCodes.Add(keyCode);
763

764
            if (newKeyCodes.IsEmpty())
765
                continue;
766

767
            var newEffect = new SpectrumKeyboardBacklightEffect(effect.Type,
768
                effect.Speed,
769
                effect.Direction,
770
                effect.ClockwiseDirection,
771
                effect.Colors,
772
                newKeyCodes);
773

774
            newEffects.Add(newEffect);
775
        }
776

777
        newEffects.Reverse();
778
        return newEffects.ToArray();
779
    }
780

781
    private static (int Profile, SpectrumKeyboardBacklightEffect[] Effects) Convert(LENOVO_SPECTRUM_EFFECT_DESCRIPTION description)
782
    {
783
        var profile = description.Profile;
784
        var effects = description.Effects.Select(Convert).ToArray();
785
        return (profile, effects);
786
    }
787

788
    private static SpectrumKeyboardBacklightEffect Convert(LENOVO_SPECTRUM_EFFECT effect)
789
    {
790
        var effectType = effect.EffectHeader.EffectType switch
791
        {
792
            LENOVO_SPECTRUM_EFFECT_TYPE.Always => SpectrumKeyboardBacklightEffectType.Always,
793
            LENOVO_SPECTRUM_EFFECT_TYPE.LegionAuraSync => SpectrumKeyboardBacklightEffectType.AuroraSync,
794
            LENOVO_SPECTRUM_EFFECT_TYPE.AudioBounceLighting => SpectrumKeyboardBacklightEffectType.AudioBounce,
795
            LENOVO_SPECTRUM_EFFECT_TYPE.AudioRippleLighting => SpectrumKeyboardBacklightEffectType.AudioRipple,
796
            LENOVO_SPECTRUM_EFFECT_TYPE.ColorChange => SpectrumKeyboardBacklightEffectType.ColorChange,
797
            LENOVO_SPECTRUM_EFFECT_TYPE.ColorPulse => SpectrumKeyboardBacklightEffectType.ColorPulse,
798
            LENOVO_SPECTRUM_EFFECT_TYPE.ColorWave => SpectrumKeyboardBacklightEffectType.ColorWave,
799
            LENOVO_SPECTRUM_EFFECT_TYPE.Rain => SpectrumKeyboardBacklightEffectType.Rain,
800
            LENOVO_SPECTRUM_EFFECT_TYPE.ScrewRainbow => SpectrumKeyboardBacklightEffectType.RainbowScrew,
801
            LENOVO_SPECTRUM_EFFECT_TYPE.RainbowWave => SpectrumKeyboardBacklightEffectType.RainbowWave,
802
            LENOVO_SPECTRUM_EFFECT_TYPE.Ripple => SpectrumKeyboardBacklightEffectType.Ripple,
803
            LENOVO_SPECTRUM_EFFECT_TYPE.Smooth => SpectrumKeyboardBacklightEffectType.Smooth,
804
            LENOVO_SPECTRUM_EFFECT_TYPE.TypeLighting => SpectrumKeyboardBacklightEffectType.Type,
805
            _ => throw new ArgumentException(nameof(effect.EffectHeader.EffectType))
806
        };
807

808
        var speed = effect.EffectHeader.Speed switch
809
        {
810
            LENOVO_SPECTRUM_SPEED.Speed1 => SpectrumKeyboardBacklightSpeed.Speed1,
811
            LENOVO_SPECTRUM_SPEED.Speed2 => SpectrumKeyboardBacklightSpeed.Speed2,
812
            LENOVO_SPECTRUM_SPEED.Speed3 => SpectrumKeyboardBacklightSpeed.Speed3,
813
            _ => SpectrumKeyboardBacklightSpeed.None
814
        };
815

816
        var direction = effect.EffectHeader.Direction switch
817
        {
818
            LENOVO_SPECTRUM_DIRECTION.LeftToRight => SpectrumKeyboardBacklightDirection.LeftToRight,
819
            LENOVO_SPECTRUM_DIRECTION.RightToLeft => SpectrumKeyboardBacklightDirection.RightToLeft,
820
            LENOVO_SPECTRUM_DIRECTION.BottomToTop => SpectrumKeyboardBacklightDirection.BottomToTop,
821
            LENOVO_SPECTRUM_DIRECTION.TopToBottom => SpectrumKeyboardBacklightDirection.TopToBottom,
822
            _ => SpectrumKeyboardBacklightDirection.None
823
        };
824

825
        var clockwiseDirection = effect.EffectHeader.ClockwiseDirection switch
826
        {
827
            LENOVO_SPECTRUM_CLOCKWISE_DIRECTION.Clockwise => SpectrumKeyboardBacklightClockwiseDirection.Clockwise,
828
            LENOVO_SPECTRUM_CLOCKWISE_DIRECTION.CounterClockwise => SpectrumKeyboardBacklightClockwiseDirection.CounterClockwise,
829
            _ => SpectrumKeyboardBacklightClockwiseDirection.None
830
        };
831

832
        var colors = effect.Colors.Select(c => new RGBColor(c.R, c.G, c.B)).ToArray();
833

834
        var keys = effect.KeyCodes;
835
        if (effect.KeyCodes.Length == 1 && effect.KeyCodes[0] == 0x65)
836
            keys = Array.Empty<ushort>();
837

838
        return new(effectType, speed, direction, clockwiseDirection, colors, keys);
839
    }
840

841
    private static LENOVO_SPECTRUM_EFFECT_DESCRIPTION Convert(int profile, SpectrumKeyboardBacklightEffect[] effects)
842
    {
843
        var header = new LENOVO_SPECTRUM_HEADER(LENOVO_SPECTRUM_OPERATION_TYPE.EffectChange, 0); // Size will be set on serialization
844
        var str = effects.Select((e, i) => Convert(i, e)).ToArray();
845
        var result = new LENOVO_SPECTRUM_EFFECT_DESCRIPTION(header, (byte)profile, str);
846
        return result;
847
    }
848

849
    private static LENOVO_SPECTRUM_EFFECT Convert(int index, SpectrumKeyboardBacklightEffect effect)
850
    {
851
        var effectType = effect.Type switch
852
        {
853
            SpectrumKeyboardBacklightEffectType.Always => LENOVO_SPECTRUM_EFFECT_TYPE.Always,
854
            SpectrumKeyboardBacklightEffectType.AuroraSync => LENOVO_SPECTRUM_EFFECT_TYPE.LegionAuraSync,
855
            SpectrumKeyboardBacklightEffectType.AudioBounce => LENOVO_SPECTRUM_EFFECT_TYPE.AudioBounceLighting,
856
            SpectrumKeyboardBacklightEffectType.AudioRipple => LENOVO_SPECTRUM_EFFECT_TYPE.AudioRippleLighting,
857
            SpectrumKeyboardBacklightEffectType.ColorChange => LENOVO_SPECTRUM_EFFECT_TYPE.ColorChange,
858
            SpectrumKeyboardBacklightEffectType.ColorPulse => LENOVO_SPECTRUM_EFFECT_TYPE.ColorPulse,
859
            SpectrumKeyboardBacklightEffectType.ColorWave => LENOVO_SPECTRUM_EFFECT_TYPE.ColorWave,
860
            SpectrumKeyboardBacklightEffectType.Rain => LENOVO_SPECTRUM_EFFECT_TYPE.Rain,
861
            SpectrumKeyboardBacklightEffectType.RainbowScrew => LENOVO_SPECTRUM_EFFECT_TYPE.ScrewRainbow,
862
            SpectrumKeyboardBacklightEffectType.RainbowWave => LENOVO_SPECTRUM_EFFECT_TYPE.RainbowWave,
863
            SpectrumKeyboardBacklightEffectType.Ripple => LENOVO_SPECTRUM_EFFECT_TYPE.Ripple,
864
            SpectrumKeyboardBacklightEffectType.Smooth => LENOVO_SPECTRUM_EFFECT_TYPE.Smooth,
865
            SpectrumKeyboardBacklightEffectType.Type => LENOVO_SPECTRUM_EFFECT_TYPE.TypeLighting,
866
            _ => throw new ArgumentException(nameof(effect.Type))
867
        };
868

869
        var speed = effect.Speed switch
870
        {
871
            SpectrumKeyboardBacklightSpeed.Speed1 => LENOVO_SPECTRUM_SPEED.Speed1,
872
            SpectrumKeyboardBacklightSpeed.Speed2 => LENOVO_SPECTRUM_SPEED.Speed2,
873
            SpectrumKeyboardBacklightSpeed.Speed3 => LENOVO_SPECTRUM_SPEED.Speed3,
874
            _ => LENOVO_SPECTRUM_SPEED.None
875
        };
876

877
        var direction = effect.Direction switch
878
        {
879
            SpectrumKeyboardBacklightDirection.LeftToRight => LENOVO_SPECTRUM_DIRECTION.LeftToRight,
880
            SpectrumKeyboardBacklightDirection.RightToLeft => LENOVO_SPECTRUM_DIRECTION.RightToLeft,
881
            SpectrumKeyboardBacklightDirection.BottomToTop => LENOVO_SPECTRUM_DIRECTION.BottomToTop,
882
            SpectrumKeyboardBacklightDirection.TopToBottom => LENOVO_SPECTRUM_DIRECTION.TopToBottom,
883
            _ => LENOVO_SPECTRUM_DIRECTION.None
884
        };
885

886
        var clockwiseDirection = effect.ClockwiseDirection switch
887
        {
888
            SpectrumKeyboardBacklightClockwiseDirection.Clockwise => LENOVO_SPECTRUM_CLOCKWISE_DIRECTION.Clockwise,
889
            SpectrumKeyboardBacklightClockwiseDirection.CounterClockwise => LENOVO_SPECTRUM_CLOCKWISE_DIRECTION.CounterClockwise,
890
            _ => LENOVO_SPECTRUM_CLOCKWISE_DIRECTION.None
891
        };
892

893
        var colorMode = effect.Type switch
894
        {
895
            SpectrumKeyboardBacklightEffectType.Always => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
896
            SpectrumKeyboardBacklightEffectType.ColorChange when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
897
            SpectrumKeyboardBacklightEffectType.ColorPulse when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
898
            SpectrumKeyboardBacklightEffectType.ColorWave when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
899
            SpectrumKeyboardBacklightEffectType.Rain when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
900
            SpectrumKeyboardBacklightEffectType.Smooth when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
901
            SpectrumKeyboardBacklightEffectType.Ripple when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
902
            SpectrumKeyboardBacklightEffectType.Type when effect.Colors.Any() => LENOVO_SPECTRUM_COLOR_MODE.ColorList,
903
            SpectrumKeyboardBacklightEffectType.ColorChange => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
904
            SpectrumKeyboardBacklightEffectType.ColorPulse => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
905
            SpectrumKeyboardBacklightEffectType.ColorWave => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
906
            SpectrumKeyboardBacklightEffectType.Rain => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
907
            SpectrumKeyboardBacklightEffectType.Smooth => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
908
            SpectrumKeyboardBacklightEffectType.Ripple => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
909
            SpectrumKeyboardBacklightEffectType.Type => LENOVO_SPECTRUM_COLOR_MODE.RandomColor,
910
            _ => LENOVO_SPECTRUM_COLOR_MODE.None
911
        };
912

913
        var header = new LENOVO_SPECTRUM_EFFECT_HEADER(effectType, speed, direction, clockwiseDirection, colorMode);
914
        var colors = effect.Colors.Select(c => new LENOVO_SPECTRUM_COLOR(c.R, c.G, c.B)).ToArray();
915
        var keys = effect.Type.IsAllLightsEffect() ? new ushort[] { 0x65 } : effect.Keys;
916
        var result = new LENOVO_SPECTRUM_EFFECT(header, index + 1, colors, keys);
917
        return result;
918
    }
919
}
920

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.