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;
}
///
/// Sends a raw HID command prefixed with the command byte and returns the response if successful.
///
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();
}
///
/// Sends a raw HID command prefixed with the command byte and returns the response if successful.
///
public async ValueTask 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();
}
///
/// Reads from the HID device. Returns null if the read fails.
///
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;
}
///
/// Reads from the HID device. Returns null if the read fails.
///
public async ValueTask 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;
}
///
/// Sends a raw HID command prefixed with the command byte. Returns false if the send fails.
///
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;
}
///
/// Sends a raw HID command prefixed with the command byte. Returns false if the send fails.
///
public async ValueTask 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;
}
///
/// Returns the protocol version of the keyboard.
///
public ushort GetProtocolVersion()
{
var output = HidCommand(ViaCommandId.GetProtocolVersion, []);
if (output == null)
{
return 0;
}
return (ushort)((output[1] << 8) | output[2]);
}
///
/// Returns the protocol version of the keyboard.
///
public async ValueTask 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 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 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 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 SetRgbMatrixEffectAsync(ViaEffectMode effect, CancellationToken cancellationToken)
{
return SetRgbMatrixEffectAsync((byte)effect, cancellationToken);
}
public async ValueTask 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 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);