using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Security.Principal; namespace DS4Windows { public class VidPidInfo { public readonly int vid; public readonly int pid; public readonly string name; internal VidPidInfo(int vid, int pid, string name = "Generic DS4") { this.vid = vid; this.pid = pid; this.name = name; } } public class RequestElevationArgs : EventArgs { public const int STATUS_SUCCESS = 0; public const int STATUS_INIT_FAILURE = -1; private int statusCode = STATUS_INIT_FAILURE; private string instanceId; public int StatusCode { get => statusCode; set => statusCode = value; } public string InstanceId { get => instanceId; } public RequestElevationArgs(string instanceId) { this.instanceId = instanceId; } } public delegate void RequestElevationDelegate(RequestElevationArgs args); public class DS4Devices { // (HID device path, DS4Device) private static Dictionary Devices = new Dictionary(); private static HashSet deviceSerials = new HashSet(); private static HashSet DevicePaths = new HashSet(); // Keep instance of opened exclusive mode devices not in use (Charging while using BT connection) private static List DisabledDevices = new List(); private static Stopwatch sw = new Stopwatch(); public static event RequestElevationDelegate RequestElevation; public static bool isExclusiveMode = false; internal const int SONY_VID = 0x054C; internal const int RAZER_VID = 0x1532; internal const int NACON_VID = 0x146B; internal const int HORI_VID = 0x0F0D; // https://support.steampowered.com/kb_article.php?ref=5199-TOKV-4426&l=english web site has a list of other PS4 compatible device VID/PID values and brand names. // However, not all those are guaranteed to work with DS4Windows app so support is added case by case when users of DS4Windows app tests non-official DS4 gamepads. private static VidPidInfo[] knownDevices = { new VidPidInfo(SONY_VID, 0xBA0, "Sony WA"), new VidPidInfo(SONY_VID, 0x5C4, "DS4 v.1"), new VidPidInfo(SONY_VID, 0x09CC, "DS4 v.2"), new VidPidInfo(RAZER_VID, 0x1000, "Razer Raiju PS4"), new VidPidInfo(NACON_VID, 0x0D01, "Nacon Revol Pro v.1"), new VidPidInfo(NACON_VID, 0x0D02, "Nacon Revol Pro v.2"), new VidPidInfo(HORI_VID, 0x00EE, "Hori PS4 Mini"), // Hori PS4 Mini Wired Gamepad new VidPidInfo(0x7545, 0x0104, "Armor 3 LU Cobra"), // Armor 3 Level Up Cobra new VidPidInfo(0x2E95, 0x7725, "Scuf Vantage"), // Scuf Vantage gamepad new VidPidInfo(0x11C0, 0x4001, "PS4 Fun"), // PS4 Fun Controller new VidPidInfo(RAZER_VID, 0x1007, "Razer Raiju TE"), // Razer Raiju Tournament Edition new VidPidInfo(RAZER_VID, 0x1004, "Razer Raiju UE USB"), // Razer Raiju Ultimate Edition (wired) new VidPidInfo(RAZER_VID, 0x1009, "Razer Raiju UE BT"), // Razer Raiju Ultimate Edition (BT). Doesn't work yet for some reason even when non-steam Razer driver lists the BT Razer Ultimate with this ID. new VidPidInfo(SONY_VID, 0x05C5, "CronusMax (PS4 Mode)"), // CronusMax (PS4 Output Mode) new VidPidInfo(0x0C12, 0x57AB, "Warrior Joypad JS083"), // Warrior Joypad JS083 (wired). Custom lightbar color doesn't work, but everything else works OK (except touchpad and gyro because the gamepad doesnt have those). new VidPidInfo(0x0C12, 0x0E16, "Steel Play MetalTech"), // Steel Play Metaltech P4 (wired) new VidPidInfo(NACON_VID, 0x0D08, "Nacon Revol U Pro"), // Nacon Revolution Unlimited Pro new VidPidInfo(NACON_VID, 0x0D10, "Nacon Revol Infinite"), // Nacon Revolution Infinite (sometimes known as Revol Unlimited Pro v2?). Touchpad, gyro, rumble, "led indicator" lightbar. new VidPidInfo(HORI_VID, 0x0084, "Hori Fighting Cmd"), // Hori Fighting Commander (special kind of gamepad without touchpad or sticks. There is a hardware switch to alter d-pad type between dpad and LS/RS) new VidPidInfo(NACON_VID, 0x0D13, "Nacon Revol Pro v.3"), }; private static string devicePathToInstanceId(string devicePath) { string deviceInstanceId = devicePath; deviceInstanceId = deviceInstanceId.Remove(0, deviceInstanceId.LastIndexOf('\\') + 1); deviceInstanceId = deviceInstanceId.Remove(deviceInstanceId.LastIndexOf('{')); deviceInstanceId = deviceInstanceId.Replace('#', '\\'); if (deviceInstanceId.EndsWith("\\")) { deviceInstanceId = deviceInstanceId.Remove(deviceInstanceId.Length - 1); } return deviceInstanceId; } private static bool IsRealDS4(HidDevice hDevice) { string deviceInstanceId = devicePathToInstanceId(hDevice.DevicePath); string temp = Global.GetDeviceProperty(deviceInstanceId, NativeMethods.DEVPKEY_Device_UINumber); return string.IsNullOrEmpty(temp); } // Enumerates ds4 controllers in the system public static void findControllers() { lock (Devices) { IEnumerable hDevices = HidDevices.EnumerateDS4(knownDevices); hDevices = hDevices.Where(dev => IsRealDS4(dev)).Select(dev => dev); //hDevices = from dev in hDevices where IsRealDS4(dev) select dev; // Sort Bluetooth first in case USB is also connected on the same controller. hDevices = hDevices.OrderBy((HidDevice d) => { return DS4Device.HidConnectionType(d); }); List tempList = hDevices.ToList(); purgeHiddenExclusiveDevices(); tempList.AddRange(DisabledDevices); int devCount = tempList.Count(); string devicePlural = "device" + (devCount == 0 || devCount > 1 ? "s" : ""); //Log.LogToGui("Found " + devCount + " possible " + devicePlural + ". Examining " + devicePlural + ".", false); for (int i = 0; i < devCount; i++) //foreach (HidDevice hDevice in hDevices) { HidDevice hDevice = tempList[i]; if (hDevice.Description == "HID-compliant vendor-defined device") continue; // ignore the Nacon Revolution Pro programming interface else if (DevicePaths.Contains(hDevice.DevicePath)) continue; // BT/USB endpoint already open once VidPidInfo metainfo = knownDevices.Single(x => x.vid == hDevice.Attributes.VendorId && x.pid == hDevice.Attributes.ProductId); if (!hDevice.IsOpen) { hDevice.OpenDevice(isExclusiveMode); if (!hDevice.IsOpen && isExclusiveMode) { try { // Check if running with elevated permissions WindowsIdentity identity = WindowsIdentity.GetCurrent(); WindowsPrincipal principal = new WindowsPrincipal(identity); bool elevated = principal.IsInRole(WindowsBuiltInRole.Administrator); if (!elevated) { // Tell the client to launch routine to re-enable a device RequestElevationArgs eleArgs = new RequestElevationArgs(devicePathToInstanceId(hDevice.DevicePath)); RequestElevation?.Invoke(eleArgs); if (eleArgs.StatusCode == RequestElevationArgs.STATUS_SUCCESS) { hDevice.OpenDevice(isExclusiveMode); } } else { reEnableDevice(devicePathToInstanceId(hDevice.DevicePath)); hDevice.OpenDevice(isExclusiveMode); } } catch (Exception) { } } // TODO in exclusive mode, try to hold both open when both are connected if (isExclusiveMode && !hDevice.IsOpen) hDevice.OpenDevice(false); } if (hDevice.IsOpen) { string serial = hDevice.readSerial(); bool validSerial = !serial.Equals(DS4Device.blankSerial); if (validSerial && deviceSerials.Contains(serial)) { // happens when the BT endpoint already is open and the USB is plugged into the same host if (isExclusiveMode && hDevice.IsExclusive && !DisabledDevices.Contains(hDevice)) { // Grab reference to exclusively opened HidDevice so device // stays hidden to other processes DisabledDevices.Add(hDevice); //DevicePaths.Add(hDevice.DevicePath); } continue; } else { DS4Device ds4Device = new DS4Device(hDevice, metainfo.name); //ds4Device.Removal += On_Removal; if (!ds4Device.ExitOutputThread) { Devices.Add(hDevice.DevicePath, ds4Device); DevicePaths.Add(hDevice.DevicePath); deviceSerials.Add(serial); } } } } } } // Returns DS4 controllers that were found and are running public static IEnumerable getDS4Controllers() { lock (Devices) { DS4Device[] controllers = new DS4Device[Devices.Count]; Devices.Values.CopyTo(controllers, 0); return controllers; } } public static void stopControllers() { lock (Devices) { IEnumerable devices = getDS4Controllers(); //foreach (DS4Device device in devices) //for (int i = 0, devCount = devices.Count(); i < devCount; i++) for (var devEnum = devices.GetEnumerator(); devEnum.MoveNext();) { DS4Device device = devEnum.Current; //DS4Device device = devices.ElementAt(i); device.StopUpdate(); //device.runRemoval(); device.HidDevice.CloseDevice(); } Devices.Clear(); DevicePaths.Clear(); deviceSerials.Clear(); DisabledDevices.Clear(); } } // Called when devices is diconnected, timed out or has input reading failure public static void On_Removal(object sender, EventArgs e) { DS4Device device = (DS4Device)sender; RemoveDevice(device); } public static void RemoveDevice(DS4Device device) { lock (Devices) { if (device != null) { device.HidDevice.CloseDevice(); Devices.Remove(device.HidDevice.DevicePath); DevicePaths.Remove(device.HidDevice.DevicePath); deviceSerials.Remove(device.MacAddress); //purgeHiddenExclusiveDevices(); } } } public static void UpdateSerial(object sender, EventArgs e) { lock (Devices) { DS4Device device = (DS4Device)sender; if (device != null) { string devPath = device.HidDevice.DevicePath; string serial = device.getMacAddress(); if (Devices.ContainsKey(devPath)) { deviceSerials.Remove(serial); device.updateSerial(); serial = device.getMacAddress(); if (DS4Device.isValidSerial(serial)) { deviceSerials.Add(serial); } if (device.ShouldRunCalib()) device.RefreshCalibration(); } } } } private static void purgeHiddenExclusiveDevices() { int disabledDevCount = DisabledDevices.Count; if (disabledDevCount > 0) { List disabledDevList = new List(); for (var devEnum = DisabledDevices.GetEnumerator(); devEnum.MoveNext();) //for (int i = 0, arlen = disabledDevCount; i < arlen; i++) { //HidDevice tempDev = DisabledDevices.ElementAt(i); HidDevice tempDev = devEnum.Current; if (tempDev != null) { if (tempDev.IsOpen && tempDev.IsConnected) { disabledDevList.Add(tempDev); } else if (tempDev.IsOpen) { if (!tempDev.IsConnected) { try { tempDev.CloseDevice(); } catch { } } if (DevicePaths.Contains(tempDev.DevicePath)) { DevicePaths.Remove(tempDev.DevicePath); } } } } DisabledDevices.Clear(); DisabledDevices.AddRange(disabledDevList); } } public static void reEnableDevice(string deviceInstanceId) { bool success; Guid hidGuid = new Guid(); NativeMethods.HidD_GetHidGuid(ref hidGuid); IntPtr deviceInfoSet = NativeMethods.SetupDiGetClassDevs(ref hidGuid, deviceInstanceId, 0, NativeMethods.DIGCF_PRESENT | NativeMethods.DIGCF_DEVICEINTERFACE); NativeMethods.SP_DEVINFO_DATA deviceInfoData = new NativeMethods.SP_DEVINFO_DATA(); deviceInfoData.cbSize = Marshal.SizeOf(deviceInfoData); success = NativeMethods.SetupDiEnumDeviceInfo(deviceInfoSet, 0, ref deviceInfoData); if (!success) { throw new Exception("Error getting device info data, error code = " + Marshal.GetLastWin32Error()); } success = NativeMethods.SetupDiEnumDeviceInfo(deviceInfoSet, 1, ref deviceInfoData); // Checks that we have a unique device if (success) { throw new Exception("Can't find unique device"); } NativeMethods.SP_PROPCHANGE_PARAMS propChangeParams = new NativeMethods.SP_PROPCHANGE_PARAMS(); propChangeParams.classInstallHeader.cbSize = Marshal.SizeOf(propChangeParams.classInstallHeader); propChangeParams.classInstallHeader.installFunction = NativeMethods.DIF_PROPERTYCHANGE; propChangeParams.stateChange = NativeMethods.DICS_DISABLE; propChangeParams.scope = NativeMethods.DICS_FLAG_GLOBAL; propChangeParams.hwProfile = 0; success = NativeMethods.SetupDiSetClassInstallParams(deviceInfoSet, ref deviceInfoData, ref propChangeParams, Marshal.SizeOf(propChangeParams)); if (!success) { throw new Exception("Error setting class install params, error code = " + Marshal.GetLastWin32Error()); } success = NativeMethods.SetupDiCallClassInstaller(NativeMethods.DIF_PROPERTYCHANGE, deviceInfoSet, ref deviceInfoData); // TEST: If previous SetupDiCallClassInstaller fails, just continue // otherwise device will likely get permanently disabled. /*if (!success) { throw new Exception("Error disabling device, error code = " + Marshal.GetLastWin32Error()); } */ //System.Threading.Thread.Sleep(50); sw.Restart(); while (sw.ElapsedMilliseconds < 50) { // Use SpinWait to keep control of current thread. Using Sleep could potentially // cause other events to get run out of order System.Threading.Thread.SpinWait(100); } sw.Stop(); propChangeParams.stateChange = NativeMethods.DICS_ENABLE; success = NativeMethods.SetupDiSetClassInstallParams(deviceInfoSet, ref deviceInfoData, ref propChangeParams, Marshal.SizeOf(propChangeParams)); if (!success) { throw new Exception("Error setting class install params, error code = " + Marshal.GetLastWin32Error()); } success = NativeMethods.SetupDiCallClassInstaller(NativeMethods.DIF_PROPERTYCHANGE, deviceInfoSet, ref deviceInfoData); if (!success) { throw new Exception("Error enabling device, error code = " + Marshal.GetLastWin32Error()); } //System.Threading.Thread.Sleep(50); sw.Restart(); while (sw.ElapsedMilliseconds < 50) { // Use SpinWait to keep control of current thread. Using Sleep could potentially // cause other events to get run out of order System.Threading.Thread.SpinWait(100); } sw.Stop(); NativeMethods.SetupDiDestroyDeviceInfoList(deviceInfoSet); } } }