Add integration with QMK/VIA keyboard on Framework Laptop 16

This commit is contained in:
ivan tkachenko 2026-03-07 00:31:30 +02:00
parent 65784e726e
commit f77e41bd17
10 changed files with 1022 additions and 1 deletions

View File

@ -3,6 +3,7 @@
## MuzikaGromche 1337.9001.69
- Show real Artist & Song info in the config.
- Integrated visual effects with QMK/VIA RGB keyboard (specifically input modules for Framework Laptop 16).
## MuzikaGromche 1337.9001.68 - LocalHost hotfix

View File

@ -47,6 +47,7 @@
<PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.1" PrivateAssets="all" Private="false" />
<PackageReference Include="AinaVT-LethalConfig" Version="1.4.6" PrivateAssets="all" Private="false" />
<PackageReference Include="TeamBMX.LobbyCompatibility" Version="1.*" PrivateAssets="all" Private="false" />
<PackageReference Include="HidSharp" Version="2.6.4" PrivateAssets="all" Private="false" GeneratePathProperty="true" />
</ItemGroup>
<ItemGroup>
@ -91,6 +92,7 @@
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_discoball" />
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_poweredlightsanimators" />
<PackagedResources Include="$(TargetDir)$(AssemblyName).dll" />
<PackagedResources Include="$(PkgHidSharp)\lib\netstandard2.0\*.dll" />
</ItemGroup>
<ItemGroup>

View File

@ -170,6 +170,7 @@ namespace MuzikaGromche
Harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
Harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
Harmony.PatchAll(typeof(ClearAudioClipCachePatch));
Harmony.PatchAll(typeof(Via.ViaFlickerLightsPatch));
NetcodePatcher();
Compatibility.Register(this);
}
@ -1328,6 +1329,7 @@ namespace MuzikaGromche
{
// Calculate final color, substituting null with initialColor if needed.
public abstract Color GetColor(Color initialColor);
public abstract Color? GetNullableColor();
protected string NullableColorToString(Color? color)
{
@ -1344,6 +1346,11 @@ namespace MuzikaGromche
return Color ?? initialColor;
}
public override Color? GetNullableColor()
{
return Color;
}
public override string ToString()
{
return $"Color(#{NullableColorToString(Color)})";
@ -1365,7 +1372,7 @@ namespace MuzikaGromche
return Color.Lerp(from, to, Mathf.Clamp(Easing.Eval(T), 0f, 1f));
}
private Color? GetNullableColor()
public override Color? GetNullableColor()
{
return From is { } from && To is { } to ? Color.Lerp(from, to, Mathf.Clamp(Easing.Eval(T), 0f, 1f)) : null;
}
@ -2340,6 +2347,7 @@ namespace MuzikaGromche
internal void Stop()
{
PoweredLightsBehaviour.Instance.ResetLightColor();
Via.ViaBehaviour.Instance.Restore();
DiscoBallManager.Disable();
ScreenFiltersManager.Clear();
@ -2549,9 +2557,18 @@ namespace MuzikaGromche
case SetLightsColorEvent e:
PoweredLightsBehaviour.Instance.SetLightColor(e);
if (localPlayerCanHearMusic && e.GetNullableColor() is { } color)
{
Via.ViaBehaviour.Instance.SetColor(color);
}
else
{
Via.ViaBehaviour.Instance.Restore();
}
break;
case FlickerLightsEvent:
// VIA is handled by a Harmony patch to integrate with all flickering events, not just from this mod.
RoundManager.Instance.FlickerLights(true);
break;

View File

@ -1,5 +1,6 @@
using DunGen;
using HarmonyLib;
using MuzikaGromche.Via;
using System;
using System.Collections.Generic;
using System.IO;

View File

@ -0,0 +1,83 @@
using System.Threading.Tasks;
namespace MuzikaGromche.Via;
public sealed class AsyncEventProcessor<T> where T : struct
{
private readonly object stateLock = new();
private T latestData;
private T lastProcessedData;
private bool isShutdownRequested;
private TaskCompletionSource<bool> signal = new(TaskCreationOptions.RunContinuationsAsynchronously);
public delegate ValueTask ProcessEventAsync(T oldData, T newData, bool shutdown);
private readonly ProcessEventAsync processEventAsync;
public AsyncEventProcessor(T initialData, ProcessEventAsync processEventAsync)
{
latestData = initialData;
lastProcessedData = initialData;
this.processEventAsync = processEventAsync;
_ = Task.Run(ProcessLoopAsync);
}
/// <summary>
/// Signals the processor that new data is available.
/// If <c>requestShutdown</c> is set, the processor will perform one last pass before exiting.
/// </summary>
public void Notify(T data, bool requestShutdown = false)
{
lock (stateLock)
{
latestData = data;
if (requestShutdown)
{
isShutdownRequested = true;
}
// Trigger the task to wake up
signal.TrySetResult(true);
}
}
private async Task ProcessLoopAsync()
{
bool running = true;
while (running)
{
Task<bool> nextSignal;
lock (stateLock)
{
nextSignal = signal.Task;
}
// Wait for a notification or shutdown signal
//
// VSTHRD003 fix: We are awaiting a task we didn't "start",
// but by using RunContinuationsAsynchronously in the TCS constructor,
// we guarantee the 'await' won't hijack the signaler's thread.
await nextSignal.ConfigureAwait(false);
T newData;
T oldData;
// Reset the signal for the next round
lock (stateLock)
{
signal = new(TaskCreationOptions.RunContinuationsAsynchronously);
if (isShutdownRequested)
{
running = false;
}
newData = latestData;
oldData = lastProcessedData;
lastProcessedData = newData;
}
await processEventAsync(oldData, newData, !running);
}
}
}

View File

@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HidSharp;
namespace MuzikaGromche.Via;
class DevicePool<T> : IDisposable
where T : IDevicePoolDelegate
{
private readonly IDevicePoolFactory<T> sidecarFactory;
// Async synchronization
private readonly SemaphoreSlim semaphore;
// Async access, use semaphore!
private readonly Dictionary<string, (HidDevice, T)> existing = [];
private bool updating = false;
public DevicePool(IDevicePoolFactory<T> sidecarFactory)
{
semaphore = new SemaphoreSlim(1, 1);
this.sidecarFactory = sidecarFactory;
DeviceList.Local.Changed += (_sender, _args) =>
{
Console.WriteLine($"Pool Changed");
_ = Task.Run(async () =>
{
await Task.Delay(300);
if (!updating)
{
updating = true;
UpdateConnections();
updating = false;
return;
}
});
};
UpdateConnections();
}
private void WithSemaphore(Action action)
{
semaphore.Wait();
try
{
action.Invoke();
}
finally
{
semaphore.Release();
}
}
public void Dispose()
{
Console.WriteLine($"Pool Dispose");
Clear();
}
private void Clear()
{
WithSemaphore(ClearUnsafe);
}
private void ClearUnsafe()
{
Console.WriteLine($"Pool Clear");
ForEachUnsafe((sidecar) => sidecar.Dispose());
existing.Clear();
}
private void ClearUnsafe(string path)
{
if (existing.Remove(path, out var data))
{
var (_, sidecar) = data;
sidecar.Dispose();
}
}
public void Restore()
{
Console.WriteLine($"Pool Restore");
ForEach((sidecar) => sidecar.Restore());
}
private void UpdateConnections()
{
WithSemaphore(() =>
{
// set of removed devices to be cleaned up
var removed = existing.Keys.ToHashSet();
foreach (var hidDevice in DeviceList.Local.GetHidDevices())
{
try
{
// surely path is enough to uniquely differentiate
var path = hidDevice.DevicePath;
if (existing.ContainsKey(path))
{
// not gone anywhere
removed.Remove(path);
continue;
}
var sidecar = sidecarFactory.Create(hidDevice);
if (sidecar != null)
{
Console.WriteLine($"Pool Added {path}");
existing[path] = (hidDevice, sidecar);
}
}
catch (Exception)
{
}
}
foreach (var path in removed)
{
try
{
ClearUnsafe(path);
}
catch (Exception)
{
}
}
});
}
public void ForEach(Action<T> action)
{
WithSemaphore(() => ForEachUnsafe(action));
}
private void ForEachUnsafe(Action<T> action)
{
foreach (var (_, sidecar) in existing.Values)
{
try
{
action(sidecar);
}
// ignore. the faulty device will be removed soon.
catch (IOException)
{
}
catch (TimeoutException)
{
}
}
}
}
/// Dispose should call Restore
interface IDevicePoolDelegate : IDisposable
{
void Restore();
}
interface IDevicePoolFactory<T>
{
T? Create(HidDevice hidDevice);
}

View File

@ -0,0 +1,231 @@
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);

View File

@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace MuzikaGromche.Via;
interface ILightshow
{
/// <summary>
/// Override or reset brightness (value) for flickering animation.
/// </summary>
void SetBrightnessOverride(byte? brightness);
void SetColor(byte hue, byte saturation, byte value);
}
class Animations
{
private static CancellationTokenSource? cts;
public static void Flicker(ILightshow lightshow)
{
if (cts != null)
{
cts.Cancel();
}
cts = new();
_ = Task.Run(async () => await FlickerAsync(lightshow, cts.Token), cts.Token);
}
static async ValueTask FlickerAsync(ILightshow lightshow, CancellationToken cancellationToken)
{
await foreach (var on in FlickerAnimationAsync())
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
byte brightness = on ? (byte)0xFF : (byte)0x00;
lightshow.SetBrightnessOverride(brightness);
}
lightshow.SetBrightnessOverride(null);
}
// Timestamps (in frames of 60 fps) of state switches, starting with 0 => ON.
private static readonly int[] MansionWallLampFlicker = [
0, 4, 8, 26, 28, 32, 37, 41, 42, 58, 60, 71,
];
public static async IAsyncEnumerable<bool> FlickerAnimationAsync()
{
bool lastState = false;
int lastFrame = 0;
const int initialMillisecondsDelay = 4 * 1000 / 60;
await Task.Delay(initialMillisecondsDelay);
foreach (int frame in MansionWallLampFlicker)
{
// convert difference in frames into milliseconds
int millisecondsDelay = (int)((frame - lastFrame) / 60f * 1000f);
lastState = !lastState;
lastFrame = frame;
await Task.Delay(millisecondsDelay);
yield return lastState;
}
}
}

View File

@ -0,0 +1,75 @@
using HarmonyLib;
using UnityEngine;
namespace MuzikaGromche.Via;
class ViaBehaviour : MonoBehaviour
{
static ViaBehaviour? instance = null;
public static ViaBehaviour Instance
{
get
{
if (instance == null)
{
var go = new GameObject("MuzikaGromche_ViaBehaviour", [
typeof(ViaBehaviour),
])
{
hideFlags = HideFlags.HideAndDontSave
};
DontDestroyOnLoad(go);
instance = go.GetComponent<ViaBehaviour>();
}
return instance;
}
}
DevicePool<ViaDeviceDelegate> devicePool = null!;
DevicePoolLightshow<ViaDeviceDelegate> lightshow = null!;
void Awake()
{
devicePool = new DevicePool<ViaDeviceDelegate>(new FrameworkDeviceFactory());
lightshow = new DevicePoolLightshow<ViaDeviceDelegate>(devicePool);
}
void OnDestroy()
{
devicePool.Dispose();
}
public void SetColor(Color color)
{
Color.RGBToHSV(color, out var hue, out var saturation, out var value);
var h = (byte)Mathf.RoundToInt(hue * 255);
var s = (byte)Mathf.RoundToInt(saturation * 255);
var v = (byte)Mathf.RoundToInt(value * 255);
lightshow.SetColor(h, s, v);
}
public void Restore()
{
devicePool.Restore();
}
public void Flicker()
{
Animations.Flicker(lightshow);
}
}
[HarmonyPatch(typeof(RoundManager))]
static class ViaFlickerLightsPatch
{
[HarmonyPatch(nameof(RoundManager.FlickerLights))]
[HarmonyPrefix]
static void OnFlickerLights(RoundManager __instance)
{
var _ = __instance;
ViaBehaviour.Instance.Flicker();
}
}

374
MuzikaGromche/Via/Via.cs Normal file
View File

@ -0,0 +1,374 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using HidSharp;
namespace MuzikaGromche.Via;
enum ViaCommandId : byte
{
GetProtocolVersion = 0x01,
GetKeyboardValue = 0x02,
SetKeyboardValue = 0x03,
DynamicKeymapGetKeycode = 0x04,
DynamicKeymapSetKeycode = 0x05,
DynamicKeymapReset = 0x06,
CustomSetValue = 0x07,
CustomGetValue = 0x08,
CustomSave = 0x09,
EepromReset = 0x0A,
BootloaderJump = 0x0B,
DynamicKeymapMacroGetCount = 0x0C,
DynamicKeymapMacroGetBufferSize = 0x0D,
DynamicKeymapMacroGetBuffer = 0x0E,
DynamicKeymapMacroSetBuffer = 0x0F,
DynamicKeymapMacroReset = 0x10,
DynamicKeymapGetLayerCount = 0x11,
DynamicKeymapGetBuffer = 0x12,
DynamicKeymapSetBuffer = 0x13,
DynamicKeymapGetEncoder = 0x14,
DynamicKeymapSetEncoder = 0x15,
Unhandled = 0xFF,
}
enum ViaChannelId : byte
{
CustomChannel = 0,
QmkBacklightChannel = 1,
QmkRgblightChannel = 2,
QmkRgbMatrixChannel = 3,
QmkAudioChannel = 4,
QmkLedMatrixChannel = 5,
}
enum ViaQmkRgbMatrixValue : byte
{
Brightness = 1,
Effect = 2,
EffectSpeed = 3,
Color = 4,
};
enum ViaEffectMode : byte
{
Off = 0x00,
Static = 0x01,
Breathing = 0x02,
}
class ViaKeyboardApi
{
private const uint RAW_EPSIZE = 32;
private const byte COMMAND_START = 0x00;
private readonly HidStream stream;
public ViaKeyboardApi(HidStream stream)
{
this.stream = stream;
}
/// <summary>
/// Sends a raw HID command prefixed with the command byte and returns the response if successful.
/// </summary>
public byte[]? HidCommand(ViaCommandId command, byte[] input)
{
byte[] commandBytes = [(byte)command, .. input];
if (!HidSend(commandBytes))
{
// Console.WriteLine($"no send");
return null;
}
var buffer = HidRead();
if (buffer == null)
{
// Console.WriteLine($"no read");
return null;
}
// Console.WriteLine($"write command {commandBytes.BytesToHex()}");
// Console.WriteLine($"read buffer {buffer.BytesToHex()}");
if (!buffer.AsSpan(1).StartsWith(commandBytes))
{
return null;
}
return buffer.AsSpan(1).ToArray();
}
/// <summary>
/// Sends a raw HID command prefixed with the command byte and returns the response if successful.
/// </summary>
public async ValueTask<byte[]?> HidCommandAsync(ViaCommandId command, byte[] input, CancellationToken cancellationToken)
{
byte[] commandBytes = [(byte)command, .. input];
if (!await HidSendAsync(commandBytes, cancellationToken))
{
return null;
// Console.WriteLine($"no send");
}
var buffer = await HidReadAsync(cancellationToken);
if (buffer == null)
{
// Console.WriteLine($"no read");
return null;
}
// Console.WriteLine($"write command {commandBytes.BytesToHex()}");
// Console.WriteLine($"read buffer {buffer.BytesToHex()}");
if (!buffer.AsSpan(1).StartsWith(commandBytes))
{
return null;
}
return buffer.AsSpan(1).ToArray();
}
/// <summary>
/// Reads from the HID device. Returns null if the read fails.
/// </summary>
public byte[]? HidRead()
{
byte[] buffer = new byte[RAW_EPSIZE + 1];
// Console.WriteLine($"{buffer.BytesToHex()}");
int count = stream.Read(buffer);
if (count != RAW_EPSIZE + 1)
{
return null;
}
return buffer;
}
/// <summary>
/// Reads from the HID device. Returns null if the read fails.
/// </summary>
public async ValueTask<byte[]?> HidReadAsync(CancellationToken cancellationToken)
{
byte[] buffer = new byte[RAW_EPSIZE + 1];
// Console.WriteLine($"{buffer.BytesToHex()}");
int count = await stream.ReadAsync(buffer, cancellationToken);
if (count != RAW_EPSIZE + 1)
{
return null;
}
return buffer;
}
/// <summary>
/// Sends a raw HID command prefixed with the command byte. Returns false if the send fails.
/// </summary>
public bool HidSend(byte[] bytes)
{
if (bytes.Length > RAW_EPSIZE)
{
return false;
}
byte[] commandBytes = [COMMAND_START, .. bytes];
byte[] paddedArray = new byte[RAW_EPSIZE + 1];
commandBytes.AsSpan().CopyTo(paddedArray);
// Console.WriteLine($"Send {paddedArray.BytesToHex()}");
stream.Write(paddedArray);
return true;
}
/// <summary>
/// Sends a raw HID command prefixed with the command byte. Returns false if the send fails.
/// </summary>
public async ValueTask<bool> HidSendAsync(byte[] bytes, CancellationToken cancellationToken)
{
if (bytes.Length > RAW_EPSIZE)
{
return false;
}
byte[] commandBytes = [COMMAND_START, .. bytes];
byte[] paddedArray = new byte[RAW_EPSIZE + 1];
commandBytes.AsSpan().CopyTo(paddedArray);
// Console.WriteLine($"Send {paddedArray.BytesToHex()}");
await stream.WriteAsync(paddedArray, cancellationToken);
return true;
}
/// <summary>
/// Returns the protocol version of the keyboard.
/// </summary>
public ushort GetProtocolVersion()
{
var output = HidCommand(ViaCommandId.GetProtocolVersion, []);
if (output == null)
{
return 0;
}
return (ushort)((output[1] << 8) | output[2]);
}
/// <summary>
/// Returns the protocol version of the keyboard.
/// </summary>
public async ValueTask<ushort> GetProtocolVersionAsync(CancellationToken cancellationToken)
{
var output = await HidCommandAsync(ViaCommandId.GetProtocolVersion, [], cancellationToken);
if (output == null)
{
return 0;
}
return (ushort)((output[1] << 8) | output[2]);
}
public byte? GetRgbMatrixBrightness()
{
var output = HidCommand(ViaCommandId.CustomGetValue,
[
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Brightness,
]);
if (output == null)
{
return null;
}
return output[3];
}
public async ValueTask<byte?> GetRgbMatrixBrightnessAsync(CancellationToken cancellationToken)
{
var output = await HidCommandAsync(ViaCommandId.CustomGetValue,
[
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Brightness,
], cancellationToken);
if (output == null)
{
return null;
}
return output[3];
}
public bool SetRgbMatrixBrightness(byte brightness)
{
return HidCommand(ViaCommandId.CustomSetValue,
[
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Brightness,
brightness,
]) != null;
}
public async ValueTask<bool> SetRgbMatrixBrightnessAsync(byte brightness, CancellationToken cancellationToken)
{
return await HidCommandAsync(ViaCommandId.CustomSetValue,
[
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Brightness,
brightness,
], cancellationToken) != null;
}
public byte? GetRgbMatrixEffect()
{
var output = HidCommand(ViaCommandId.CustomGetValue,
[
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Effect,
]);
if (output == null)
{
return null;
}
return output[3];
}
public async ValueTask<byte?> GetRgbMatrixEffectAsync(CancellationToken cancellationToken)
{
var output = await HidCommandAsync(ViaCommandId.CustomGetValue,
[
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Effect,
], cancellationToken);
if (output == null)
{
return null;
}
return output[3];
}
public bool SetRgbMatrixEffect(ViaEffectMode effect)
{
return SetRgbMatrixEffect((byte)effect);
}
public bool SetRgbMatrixEffect(byte effect)
{
return HidCommand(ViaCommandId.CustomSetValue, [
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Effect,
effect,
]) != null;
}
public ValueTask<bool> SetRgbMatrixEffectAsync(ViaEffectMode effect, CancellationToken cancellationToken)
{
return SetRgbMatrixEffectAsync((byte)effect, cancellationToken);
}
public async ValueTask<bool> SetRgbMatrixEffectAsync(byte effect, CancellationToken cancellationToken)
{
return await HidCommandAsync(ViaCommandId.CustomSetValue, [
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Effect,
effect,
], cancellationToken) != null;
}
public (byte Hue, byte Saturation)? GetRgbMatrixColor()
{
var output = HidCommand(ViaCommandId.CustomGetValue,
[
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Color,
]);
if (output == null)
{
return null;
}
byte hue = output[3];
byte saturation = output[4];
return (hue, saturation);
}
public async ValueTask<(byte Hue, byte Saturation)?> GetRgbMatrixColorAsync(CancellationToken cancellationToken)
{
var output = await HidCommandAsync(ViaCommandId.CustomGetValue,
[
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Color,
], cancellationToken);
if (output == null)
{
return null;
}
byte hue = output[3];
byte saturation = output[4];
return (hue, saturation);
}
public bool SetRgbMatrixColor(byte hue, byte saturation)
{
return HidCommand(ViaCommandId.CustomSetValue, [
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Color,
hue, saturation,
]) != null;
}
public async ValueTask<bool> SetRgbMatrixColorAsync(byte hue, byte saturation, CancellationToken cancellationToken)
{
return await HidCommandAsync(ViaCommandId.CustomSetValue, [
(byte)ViaChannelId.QmkRgbMatrixChannel,
(byte)ViaQmkRgbMatrixValue.Color,
hue, saturation,
], cancellationToken) != null;
}
}
public record struct HueSaturationColor(byte Hue, byte Saturation);