forked from nikita/muzika-gromche
232 lines
6.2 KiB
C#
232 lines
6.2 KiB
C#
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<ViaDeviceDelegate>
|
|
{
|
|
// 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<ViaDeviceState> 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<ViaDeviceState>(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<T> : ILightshow
|
|
where T: IDevicePoolDelegate, ILightshow
|
|
{
|
|
private readonly DevicePool<T> devicePool;
|
|
|
|
public DevicePoolLightshow(DevicePool<T> 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);
|