forked from nikita/muzika-gromche
Add integration with QMK/VIA keyboard on Framework Laptop 16
This commit is contained in:
parent
65784e726e
commit
f77e41bd17
|
|
@ -3,6 +3,7 @@
|
||||||
## MuzikaGromche 1337.9001.69
|
## MuzikaGromche 1337.9001.69
|
||||||
|
|
||||||
- Show real Artist & Song info in the config.
|
- 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
|
## MuzikaGromche 1337.9001.68 - LocalHost hotfix
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@
|
||||||
<PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.1" PrivateAssets="all" Private="false" />
|
<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="AinaVT-LethalConfig" Version="1.4.6" PrivateAssets="all" Private="false" />
|
||||||
<PackageReference Include="TeamBMX.LobbyCompatibility" Version="1.*" 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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -91,6 +92,7 @@
|
||||||
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_discoball" />
|
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_discoball" />
|
||||||
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_poweredlightsanimators" />
|
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_poweredlightsanimators" />
|
||||||
<PackagedResources Include="$(TargetDir)$(AssemblyName).dll" />
|
<PackagedResources Include="$(TargetDir)$(AssemblyName).dll" />
|
||||||
|
<PackagedResources Include="$(PkgHidSharp)\lib\netstandard2.0\*.dll" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ namespace MuzikaGromche
|
||||||
Harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
|
Harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
|
||||||
Harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
|
Harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
|
||||||
Harmony.PatchAll(typeof(ClearAudioClipCachePatch));
|
Harmony.PatchAll(typeof(ClearAudioClipCachePatch));
|
||||||
|
Harmony.PatchAll(typeof(Via.ViaFlickerLightsPatch));
|
||||||
NetcodePatcher();
|
NetcodePatcher();
|
||||||
Compatibility.Register(this);
|
Compatibility.Register(this);
|
||||||
}
|
}
|
||||||
|
|
@ -1328,6 +1329,7 @@ namespace MuzikaGromche
|
||||||
{
|
{
|
||||||
// Calculate final color, substituting null with initialColor if needed.
|
// Calculate final color, substituting null with initialColor if needed.
|
||||||
public abstract Color GetColor(Color initialColor);
|
public abstract Color GetColor(Color initialColor);
|
||||||
|
public abstract Color? GetNullableColor();
|
||||||
|
|
||||||
protected string NullableColorToString(Color? color)
|
protected string NullableColorToString(Color? color)
|
||||||
{
|
{
|
||||||
|
|
@ -1344,6 +1346,11 @@ namespace MuzikaGromche
|
||||||
return Color ?? initialColor;
|
return Color ?? initialColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Color? GetNullableColor()
|
||||||
|
{
|
||||||
|
return Color;
|
||||||
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"Color(#{NullableColorToString(Color)})";
|
return $"Color(#{NullableColorToString(Color)})";
|
||||||
|
|
@ -1365,7 +1372,7 @@ namespace MuzikaGromche
|
||||||
return Color.Lerp(from, to, Mathf.Clamp(Easing.Eval(T), 0f, 1f));
|
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;
|
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()
|
internal void Stop()
|
||||||
{
|
{
|
||||||
PoweredLightsBehaviour.Instance.ResetLightColor();
|
PoweredLightsBehaviour.Instance.ResetLightColor();
|
||||||
|
Via.ViaBehaviour.Instance.Restore();
|
||||||
DiscoBallManager.Disable();
|
DiscoBallManager.Disable();
|
||||||
ScreenFiltersManager.Clear();
|
ScreenFiltersManager.Clear();
|
||||||
|
|
||||||
|
|
@ -2549,9 +2557,18 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
case SetLightsColorEvent e:
|
case SetLightsColorEvent e:
|
||||||
PoweredLightsBehaviour.Instance.SetLightColor(e);
|
PoweredLightsBehaviour.Instance.SetLightColor(e);
|
||||||
|
if (localPlayerCanHearMusic && e.GetNullableColor() is { } color)
|
||||||
|
{
|
||||||
|
Via.ViaBehaviour.Instance.SetColor(color);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Via.ViaBehaviour.Instance.Restore();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FlickerLightsEvent:
|
case FlickerLightsEvent:
|
||||||
|
// VIA is handled by a Harmony patch to integrate with all flickering events, not just from this mod.
|
||||||
RoundManager.Instance.FlickerLights(true);
|
RoundManager.Instance.FlickerLights(true);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using DunGen;
|
using DunGen;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
using MuzikaGromche.Via;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
Loading…
Reference in New Issue