using HidSharp; using RGB.NET.Core; using RGB.NET.HID; using System; using System.Collections; using System.Collections.Generic; using System.Linq; namespace RGB.NET.Devices.Logitech.HID; /// /// Represents a loaded for logitech HID-devices. /// /// The type of the identifier leds are mapped to. /// The type of the custom data added to the HID-device. public class LightspeedHIDLoader : IEnumerable> where TLed : notnull { #region Constants private const int LOGITECH_PROTOCOL_TIMEOUT = 300; private const int VENDOR_ID = 0x046D; // ReSharper disable once StaticMemberInGenericType - This is used like a const private static readonly List RECEIVER_PIDS = new() { 0xC539, 0xC53A, 0xC541, 0xC545 }; #endregion #region Properties & Fields private readonly Dictionary> _deviceDefinitions = new(); /// /// Gets the vendor id used for this loader. /// public int VendorId => VENDOR_ID; /// /// Gets or sets the filter used to determine which devices should be loaded. /// public RGBDeviceType LoadFilter { get; set; } = RGBDeviceType.All; #endregion #region Methods /// /// Adds a new to this loader. /// /// The virtual product id of the HID-device. /// The type of the device. /// The name of the device. /// The mapping of the leds of the device. /// Some custom data to attach to the device. public void Add(int virtualPid, RGBDeviceType deviceType, string name, LedMapping ledMapping, TData customData) => _deviceDefinitions.Add(virtualPid, new HIDDeviceDefinition(virtualPid, deviceType, name, ledMapping, customData)); /// /// Gets a enumerable containing all devices from the definition-list that are connected and match the . /// /// The enumerable containing the connected devices. public IEnumerable> GetConnectedDevices() { foreach (int device in Detect()) { if (_deviceDefinitions.TryGetValue(device, out HIDDeviceDefinition? definition)) if (LoadFilter.HasFlag(definition.DeviceType)) yield return definition; } } /// /// Gets a enumerable containing all the first device of each group of devices from the definition-list that are connected and match the . /// The grouping is done by the specified function. /// /// The type of the key used to group the devices. /// The function grouping the devices. /// The enumerable containing the selected devices. public IEnumerable> GetConnectedDevices(Func, TKey> groupBy) => GetConnectedDevices().GroupBy(groupBy) .Select(group => group.First()); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// public IEnumerator> GetEnumerator() => _deviceDefinitions.Values.GetEnumerator(); #endregion #region Private Methods private IEnumerable Detect() => RECEIVER_PIDS.SelectMany(Detect); private IEnumerable Detect(int pid) { Dictionary deviceUsages = DeviceList.Local .GetHidDevices(VendorId, pid) .Where(d => d.DevicePath.Contains("mi_02")) .ToDictionary(x => (byte)x.GetUsage(), x => x); foreach ((int wirelessPid, byte _) in GetWirelessDevices(deviceUsages)) yield return wirelessPid; } private Dictionary GetWirelessDevices(IReadOnlyDictionary deviceUsages) { const byte LOGITECH_RECEIVER_ADDRESS = 0xFF; const byte LOGITECH_SET_REGISTER_REQUEST = 0x80; const byte LOGITECH_GET_REGISTER_REQUEST = 0x81; Dictionary map = new(); if (!deviceUsages.TryGetValue(1, out HidDevice? device) || !device.TryOpen(out HidStream stream)) return map; int tries = 0; const int MAX_TRIES = 5; while (tries < MAX_TRIES) { try { stream.ReadTimeout = LOGITECH_PROTOCOL_TIMEOUT; stream.WriteTimeout = LOGITECH_PROTOCOL_TIMEOUT; FapResponse response = new(); FapShortRequest getConnectedDevices = new(); getConnectedDevices.Init(LOGITECH_RECEIVER_ADDRESS, LOGITECH_GET_REGISTER_REQUEST); stream.Write(getConnectedDevices.AsSpan()); stream.Read(response.AsSpan()); bool wirelessNotifications = (response.Data01 & 1) == 1; if (!wirelessNotifications) { response = new FapResponse(); getConnectedDevices.Init(LOGITECH_RECEIVER_ADDRESS, LOGITECH_SET_REGISTER_REQUEST); getConnectedDevices.Data1 = 1; stream.Write(getConnectedDevices.AsSpan()); stream.Read(response.AsSpan()); if (getConnectedDevices.FeatureIndex == 0x8f) { //error?? } } response = new FapResponse(); getConnectedDevices.Init(LOGITECH_RECEIVER_ADDRESS, LOGITECH_GET_REGISTER_REQUEST); getConnectedDevices.FeatureCommand = 0x02; stream.Write(getConnectedDevices.AsSpan()); stream.Read(response.AsSpan()); int deviceCount = response.Data01; if (deviceCount <= 0) return map; //Add 1 to the device_count to include the receiver deviceCount++; getConnectedDevices.Init(LOGITECH_RECEIVER_ADDRESS, LOGITECH_SET_REGISTER_REQUEST); getConnectedDevices.FeatureCommand = 0x02; getConnectedDevices.Data0 = 0x02; stream.Write(getConnectedDevices.AsSpan()); for (int i = 0; i < deviceCount; i++) { FapResponse devices = new(); stream.Read(devices.AsSpan()); int wirelessPid = (devices.Data02 << 8) | devices.Data01; if (devices.DeviceIndex != 0xff) map.Add(wirelessPid, devices.DeviceIndex); } break; } catch { tries++; //This might timeout if LGS or GHUB interfere. //Retry. } } return map; } #endregion }