using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using HidSharp; namespace MuzikaGromche.Via; class FrameworkDeviceFactory : IDevicePoolFactory { // Framework Vendor ID private const int VID = 0x32AC; internal static readonly int[] PIDs = [ 0x0012, // Framework Laptop 16 Keyboard Module - ANSI 0x0013, // Framework Laptop 16 RGB macropad module ]; // VIA/QMK Raw HID identifiers private const ushort UsagePage = 0xFF60; private const byte Usage = 0x61; // In case of Framework Laptop 16 RGB Keyboard and Macropad, // Usage Page & Usage are encoded at the start of the report descriptor. private static readonly byte[] UsagePageAndUsageEncoded = [0x06, 0x60, 0xFF, 0x09, 0x61]; private const byte ExpectedVersion = 12; private static bool DevicePredicate(HidDevice device) { if (VID != device.VendorID) { return false; } if (!PIDs.Contains(device.ProductID)) { return false; } var report = device.GetRawReportDescriptor(); if (report == null) { return false; } if (!report.AsSpan().StartsWith(UsagePageAndUsageEncoded)) { return false; } return true; } public ViaDeviceDelegate? Create(HidDevice hidDevice) { if (!DevicePredicate(hidDevice)) { return null; } if (!hidDevice.TryOpen(out var stream)) { return null; } var via = new ViaKeyboardApi(stream); var version = via.GetProtocolVersion(); if (version != ExpectedVersion) { return null; } var brightness = via.GetRgbMatrixBrightness(); if (brightness == null) { return null; } var effect = via.GetRgbMatrixEffect(); if (effect == null) { return null; } var color = via.GetRgbMatrixColor(); if (color == null) { return null; } return new ViaDeviceDelegate(hidDevice, stream, via, brightness.Value, effect.Value, color.Value.Hue, color.Value.Saturation); } } class ViaDeviceDelegate : IDevicePoolDelegate, ILightshow, IDisposable { // Objects public readonly HidDevice device; public readonly HidStream stream; public readonly ViaKeyboardApi via; private readonly AsyncEventProcessor asyncEventProcessor; private readonly ViaDeviceState initialState; private byte? brightnessOverride = null; private const ViaEffectMode EFFECT = ViaEffectMode.Static; private byte? effectOverride = null; private ColorHSV? color = null; public ViaDeviceDelegate(HidDevice device, HidStream stream, ViaKeyboardApi via, byte brightness, byte effect, byte hue, byte saturation) { // Objects this.device = device; this.stream = stream; this.via = via; // Initial values initialState = new() { Brightness = brightness, Effect = effect, Hue = hue, Saturation = saturation, }; asyncEventProcessor = new AsyncEventProcessor(initialState, ProcessDataAsync); Notify(); } public void Restore() { brightnessOverride = null; effectOverride = null; color = null; Notify(); } public void Dispose() { // Simplified version of Restore() brightnessOverride = null; effectOverride = null; color = null; asyncEventProcessor.Notify(initialState, requestShutdown: true); } public void SetBrightnessOverride(byte? brightness) { brightnessOverride = brightness; effectOverride = (byte)EFFECT; Notify(); } public void SetColor(byte hue, byte saturation, byte value) { color = new ColorHSV(hue, saturation, value); effectOverride = (byte)EFFECT; Notify(); } private void Notify() { asyncEventProcessor.Notify(new() { Brightness = brightnessOverride ?? color?.Value ?? initialState.Brightness, Effect = effectOverride ?? initialState.Effect, Hue = color?.Hue ?? initialState.Hue, Saturation = color?.Saturation ?? initialState.Saturation, }); } private async ValueTask ProcessDataAsync(ViaDeviceState oldData, ViaDeviceState newData, bool shutdown) { var cancellationToken = CancellationToken.None; try { if (oldData.Brightness != newData.Brightness) { await via.SetRgbMatrixBrightnessAsync(newData.Brightness, cancellationToken); } if (oldData.Effect != newData.Effect) { await via.SetRgbMatrixEffectAsync(newData.Effect, cancellationToken); } if (oldData.Hue != newData.Hue || oldData.Saturation != newData.Saturation) { await via.SetRgbMatrixColorAsync(newData.Hue, newData.Saturation, cancellationToken); } } catch (IOException) { } catch (TimeoutException) { } finally { if (shutdown) { stream.Close(); } } } } class DevicePoolLightshow : ILightshow where T: IDevicePoolDelegate, ILightshow { private readonly DevicePool devicePool; public DevicePoolLightshow(DevicePool devicePool) { this.devicePool = devicePool; } public void SetBrightnessOverride(byte? brightness) { devicePool.ForEach(d => { d.SetBrightnessOverride(brightness); }); } public void SetColor(byte hue, byte saturation, byte value) { devicePool.ForEach(d => { d.SetColor(hue, saturation, value); }); } } struct ViaDeviceState { public byte Brightness; public byte Effect; public byte Hue; public byte Saturation; } record struct ColorHSV(byte Hue, byte Saturation, byte Value);