forked from nikita/muzika-gromche
375 lines
11 KiB
C#
375 lines
11 KiB
C#
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);
|