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);