summaryrefslogtreecommitdiffstats
path: root/src/detection/gamepad
diff options
context:
space:
mode:
Diffstat (limited to 'src/detection/gamepad')
-rw-r--r--src/detection/gamepad/gamepad.h11
-rw-r--r--src/detection/gamepad/gamepad_apple.c48
-rw-r--r--src/detection/gamepad/gamepad_bsd.c65
-rw-r--r--src/detection/gamepad/gamepad_haiku.cpp18
-rw-r--r--src/detection/gamepad/gamepad_linux.c97
-rw-r--r--src/detection/gamepad/gamepad_nosupport.c5
-rw-r--r--src/detection/gamepad/gamepad_windows.c201
7 files changed, 445 insertions, 0 deletions
diff --git a/src/detection/gamepad/gamepad.h b/src/detection/gamepad/gamepad.h
new file mode 100644
index 0000000..f26def9
--- /dev/null
+++ b/src/detection/gamepad/gamepad.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "fastfetch.h"
+
+typedef struct FFGamepadDevice {
+ FFstrbuf serial;
+ FFstrbuf name;
+ uint8_t battery; // 0-100%
+} FFGamepadDevice;
+
+const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */);
diff --git a/src/detection/gamepad/gamepad_apple.c b/src/detection/gamepad/gamepad_apple.c
new file mode 100644
index 0000000..c262c86
--- /dev/null
+++ b/src/detection/gamepad/gamepad_apple.c
@@ -0,0 +1,48 @@
+#include "gamepad.h"
+#include "common/apple/cf_helpers.h"
+#include "common/mallocHelper.h"
+
+#include <IOKit/IOKitLib.h>
+#include <IOKit/hid/IOHIDLib.h>
+
+static void enumSet(IOHIDDeviceRef value, FFlist* results) {
+ FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *results);
+ ffStrbufInit(&device->serial);
+ ffStrbufInit(&device->name);
+ device->battery = 0;
+
+ CFStringRef manufacturer = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDManufacturerKey));
+ ffCfStrGetString(manufacturer, &device->name);
+
+ CFStringRef product = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDProductKey));
+ if (device->name.length) {
+ ffCfStrGetString(product, &device->serial);
+ ffStrbufAppendC(&device->name, ' ');
+ ffStrbufAppend(&device->name, &device->serial);
+ } else {
+ ffCfStrGetString(product, &device->name);
+ }
+
+ CFStringRef serialNumber = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDSerialNumberKey));
+ ffCfStrGetString(serialNumber, &device->serial);
+}
+
+const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
+ IOHIDManagerRef FF_CFTYPE_AUTO_RELEASE manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
+ if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {
+ return "IOHIDManagerOpen() failed";
+ }
+
+ CFDictionaryRef FF_CFTYPE_AUTO_RELEASE matching1 = CFDictionaryCreate(kCFAllocatorDefault, (const void**) (CFStringRef[]) { CFSTR(kIOHIDDeviceUsagePageKey), CFSTR(kIOHIDDeviceUsageKey) }, (const void**) (CFNumberRef[]) { ffCfCreateInt(kHIDPage_GenericDesktop), ffCfCreateInt(kHIDUsage_GD_Joystick) }, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ CFDictionaryRef FF_CFTYPE_AUTO_RELEASE matching2 = CFDictionaryCreate(kCFAllocatorDefault, (const void**) (CFStringRef[]) { CFSTR(kIOHIDDeviceUsagePageKey), CFSTR(kIOHIDDeviceUsageKey) }, (const void**) (CFNumberRef[]) { ffCfCreateInt(kHIDPage_GenericDesktop), ffCfCreateInt(kHIDUsage_GD_GamePad) }, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ CFArrayRef FF_CFTYPE_AUTO_RELEASE matchings = CFArrayCreate(kCFAllocatorDefault, (const void**) (CFTypeRef[]) { matching1, matching2 }, 2, &kCFTypeArrayCallBacks);
+ IOHIDManagerSetDeviceMatchingMultiple(manager, matchings);
+
+ CFSetRef FF_CFTYPE_AUTO_RELEASE set = IOHIDManagerCopyDevices(manager);
+ if (set) {
+ CFSetApplyFunction(set, (CFSetApplierFunction) &enumSet, devices);
+ }
+ IOHIDManagerClose(manager, kIOHIDOptionsTypeNone);
+
+ return NULL;
+}
diff --git a/src/detection/gamepad/gamepad_bsd.c b/src/detection/gamepad/gamepad_bsd.c
new file mode 100644
index 0000000..b67b8fd
--- /dev/null
+++ b/src/detection/gamepad/gamepad_bsd.c
@@ -0,0 +1,65 @@
+#include "gamepad.h"
+#include "common/io.h"
+
+#include <stdio.h>
+#include <fcntl.h>
+#include <usbhid.h>
+
+#if __has_include(<dev/usb/usb_ioctl.h>)
+ #include <dev/usb/usb_ioctl.h> // FreeBSD
+#else
+ #include <bus/u4b/usb_ioctl.h> // DragonFly
+#endif
+
+#define MAX_UHID_JOYS 64
+
+const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
+ char path[16];
+ for (int i = 0; i < MAX_UHID_JOYS; i++) {
+ snprintf(path, ARRAY_SIZE(path), "/dev/uhid%d", i);
+ FF_AUTO_CLOSE_FD int fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ if (errno == ENOENT) {
+ break; // No more devices
+ }
+ continue; // Device not found
+ }
+ report_desc_t repDesc = hid_get_report_desc(fd);
+ if (!repDesc) {
+ continue;
+ }
+
+ int reportId = hid_get_report_id(fd);
+
+ struct hid_data* hData = hid_start_parse(repDesc, 0, reportId);
+ if (hData) {
+ struct hid_item hItem;
+ while (hid_get_item(hData, &hItem) > 0) {
+ if (HID_PAGE(hItem.usage) != 1) {
+ continue;
+ }
+ switch (HID_USAGE(hItem.usage)) {
+ case 1: // Pointer. FreeBSD returns 1 for my Pro Controller for some reason
+ case 4: // Joystick
+ case 5: // Gamepad
+ break;
+ default:
+ continue;
+ }
+
+ struct usb_device_info di;
+ if (ioctl(fd, USB_GET_DEVICEINFO, &di) != -1) {
+ FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *devices);
+ ffStrbufInitS(&device->serial, di.udi_serial);
+ ffStrbufInitF(&device->name, "%s %s", di.udi_vendor, di.udi_product);
+ device->battery = 0;
+ }
+ }
+ hid_end_parse(hData);
+ }
+
+ hid_dispose_report_desc(repDesc);
+ }
+
+ return NULL;
+}
diff --git a/src/detection/gamepad/gamepad_haiku.cpp b/src/detection/gamepad/gamepad_haiku.cpp
new file mode 100644
index 0000000..50b43a0
--- /dev/null
+++ b/src/detection/gamepad/gamepad_haiku.cpp
@@ -0,0 +1,18 @@
+extern "C" {
+#include "gamepad.h"
+}
+#include <Joystick.h>
+
+const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
+ BJoystick js;
+ for (int32 i = 0, n = js.CountDevices(); i < n; ++i) {
+ char name[B_OS_NAME_LENGTH];
+ if (js.GetDeviceName(i, name) == B_OK) {
+ FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *devices);
+ ffStrbufInit(&device->serial);
+ ffStrbufInitS(&device->name, name);
+ device->battery = 0;
+ }
+ }
+ return NULL;
+}
diff --git a/src/detection/gamepad/gamepad_linux.c b/src/detection/gamepad/gamepad_linux.c
new file mode 100644
index 0000000..6bb4ee2
--- /dev/null
+++ b/src/detection/gamepad/gamepad_linux.c
@@ -0,0 +1,97 @@
+#include "gamepad.h"
+#include "common/io.h"
+#include "common/strutil.h"
+
+static void detectGamepad(FFlist* devices, FFstrbuf* name, FFstrbuf* path) {
+ uint32_t baseLen = path->length;
+ FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *devices);
+ ffStrbufInit(&device->serial);
+ ffStrbufInitMove(&device->name, name);
+ device->battery = 0;
+
+ ffStrbufAppendS(path, "uniq");
+ if (ffAppendFileBuffer(path->chars, &device->serial)) {
+ ffStrbufTrimRightSpace(&device->serial);
+ }
+
+ ffStrbufSubstrBefore(path, baseLen);
+ ffStrbufAppendS(path, "device/power_supply/"); // /sys/class/input/jsX/device/device/power_supply
+
+ FF_AUTO_CLOSE_DIR DIR* dirp = opendir(path->chars);
+ if (dirp) {
+ struct dirent* entry;
+ while ((entry = readdir(dirp)) != NULL) {
+ if (entry->d_name[0] == '.' || (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN)) {
+ continue;
+ }
+ ffStrbufAppendS(path, entry->d_name);
+ ffStrbufAppendS(path, "/capacity"); // /sys/class/input/jsX/device/device/power_supply/XXX/capacity
+ char capacity[32];
+ ssize_t nRead = ffReadFileData(path->chars, ARRAY_SIZE(capacity) - 1, capacity);
+ if (nRead > 0) // Tested with a PS4 controller
+ {
+ capacity[nRead] = '\0';
+ device->battery = (uint8_t) strtoul(capacity, NULL, 10);
+ break;
+ }
+
+ ffStrbufAppendS(path, "_level");
+ nRead = ffReadFileData(path->chars, ARRAY_SIZE(capacity) - 1, capacity);
+ if (nRead > 0) // Tested with a NS Pro controller
+ {
+ // https://github.com/torvalds/linux/blob/52b1853b080a082ec3749c3a9577f6c71b1d4a90/drivers/power/supply/power_supply_sysfs.c#L124
+ switch (capacity[0]) {
+ case 'C':
+ device->battery = 1;
+ break; // Critical
+ case 'L':
+ device->battery = 25;
+ break; // Low
+ case 'N':
+ device->battery = 50;
+ break; // Normal
+ case 'H':
+ device->battery = 75;
+ break; // High
+ case 'F':
+ device->battery = 100;
+ break; // Full
+ }
+ }
+ }
+ }
+}
+
+const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
+ FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/input/");
+ if (dirp == NULL) {
+ return "opendir(\"/sys/class/input/\") == NULL";
+ }
+
+ FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateS("/sys/class/input/");
+ uint32_t baseLen = path.length;
+
+ struct dirent* entry;
+ while ((entry = readdir(dirp)) != NULL) {
+ if (!ffStrStartsWith(entry->d_name, "js")) {
+ continue;
+ }
+ if (!ffCharIsDigit(entry->d_name[strlen("js")])) {
+ continue;
+ }
+
+ ffStrbufAppendS(&path, entry->d_name);
+ ffStrbufAppendS(&path, "/device/name");
+
+ FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate();
+ if (ffAppendFileBuffer(path.chars, &name)) {
+ ffStrbufTrimRightSpace(&name);
+ ffStrbufSubstrBefore(&path, path.length - 4);
+ detectGamepad(devices, &name, &path);
+ }
+
+ ffStrbufSubstrBefore(&path, baseLen);
+ }
+
+ return NULL;
+}
diff --git a/src/detection/gamepad/gamepad_nosupport.c b/src/detection/gamepad/gamepad_nosupport.c
new file mode 100644
index 0000000..c0d791e
--- /dev/null
+++ b/src/detection/gamepad/gamepad_nosupport.c
@@ -0,0 +1,5 @@
+#include "gamepad.h"
+
+const char* ffDetectGamepad(FF_A_UNUSED FFlist* devices /* List of FFGamepadDevice */) {
+ return "Not supported on this platform";
+}
diff --git a/src/detection/gamepad/gamepad_windows.c b/src/detection/gamepad/gamepad_windows.c
new file mode 100644
index 0000000..9cbb561
--- /dev/null
+++ b/src/detection/gamepad/gamepad_windows.c
@@ -0,0 +1,201 @@
+#include "gamepad.h"
+#include "common/io.h"
+#include "common/mallocHelper.h"
+#include "common/windows/unicode.h"
+
+#include <windows.h>
+#include <hidsdi.h>
+
+static const char* detectKnownDeviceName(uint32_t vendorId, uint32_t productId) {
+ switch (vendorId) {
+ // Nintendo
+ case 0x057E: {
+ switch (productId) {
+ case 0x2006:
+ return "Nintendo Switch Joycon L";
+ case 0x2007:
+ return "Nintendo Switch Joycon R";
+ case 0x2009:
+ return "Nintendo Switch Pro Controller";
+ case 0x200E:
+ return "Nintendo Switch Charging Grip";
+ case 0x2017:
+ return "Nintendo Switch SNES Controller";
+
+ default:
+ return NULL;
+ }
+ }
+
+ // Sony
+ case 0x054C: {
+ switch (productId) {
+ case 0x0268:
+ return "Sony DualShock 3 / Six Axis";
+
+ case 0x05C4:
+ return "Sony DualShock 4 Gen1";
+ case 0x09CC:
+ return "Sony DualShock 4 Gen2";
+ case 0x0BA0:
+ return "Sony DualShock 4 USB receiver";
+
+ case 0x0CE6:
+ return "Sony DualSense";
+ case 0x0DF2:
+ return "Sony DualSense Edge";
+
+ default:
+ return NULL;
+ }
+ }
+
+ // Logitech
+ case 0x046D: {
+ switch (productId) {
+ case 0xC216:
+ return "Logitech F310, DirectInput";
+ case 0xC218:
+ return "Logitech F510, DirectInput";
+ case 0xC219:
+ return "Logitech F710, DirectInput";
+ case 0xC21D:
+ return "Logitech F310";
+ case 0xC21E:
+ return "Logitech F510";
+ case 0xC21F:
+ return "Logitech F710";
+
+ default:
+ return NULL;
+ }
+ }
+
+ case 0x045E: // Microsoft Xbox compatible controllers should be handled by Windows without problems
+ default:
+ return NULL;
+ }
+}
+
+const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
+ UINT nDevices = 0;
+ if (GetRawInputDeviceList(NULL, &nDevices, sizeof(RAWINPUTDEVICELIST))) {
+ return "GetRawInputDeviceList(NULL) failed";
+ }
+ if (nDevices == 0) {
+ return "No HID devices found";
+ }
+ RAWINPUTDEVICELIST* FF_AUTO_FREE pRawInputDeviceList = (RAWINPUTDEVICELIST*) malloc(sizeof(RAWINPUTDEVICELIST) * nDevices);
+ if ((nDevices = GetRawInputDeviceList(pRawInputDeviceList, &nDevices, sizeof(RAWINPUTDEVICELIST))) == (UINT) -1) {
+ return "GetRawInputDeviceList(pRawInputDeviceList) failed";
+ }
+
+ for (UINT i = 0; i < nDevices; ++i) {
+ if (pRawInputDeviceList[i].dwType != RIM_TYPEHID) {
+ continue;
+ }
+
+ HANDLE hDevice = pRawInputDeviceList[i].hDevice;
+
+ RID_DEVICE_INFO rdi;
+ UINT rdiSize = sizeof(rdi);
+ if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize) == (UINT) -1) {
+ continue;
+ }
+
+ if (rdi.hid.usUsagePage != 1 || (rdi.hid.usUsage != 4 /*Joystick*/ && rdi.hid.usUsage != 5 /*Gamepad*/)) {
+ continue;
+ }
+
+ WCHAR devName[MAX_PATH] = L"";
+ UINT nameSize = MAX_PATH;
+ if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, devName, &nameSize) == (UINT) -1) {
+ continue;
+ }
+
+ FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *devices);
+ ffStrbufInit(&device->serial);
+ ffStrbufInit(&device->name);
+ device->battery = 0;
+
+ const char* knownGamepad = detectKnownDeviceName(rdi.hid.dwVendorId, rdi.hid.dwProductId);
+ if (knownGamepad) {
+ ffStrbufSetS(&device->name, knownGamepad);
+ }
+ HANDLE FF_AUTO_CLOSE_FD hHidFile = CreateFileW(devName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
+ if (hHidFile == INVALID_HANDLE_VALUE) {
+ if (!knownGamepad) {
+ ffStrbufSetF(&device->name, "Unknown gamepad %04X-%04X", (unsigned) rdi.hid.dwVendorId, (unsigned) rdi.hid.dwProductId);
+ }
+ continue;
+ }
+
+ if (!knownGamepad) {
+ wchar_t displayName[126];
+ if (HidD_GetProductString(hHidFile, displayName, sizeof(displayName) /*in bytes*/)) {
+ wchar_t manufacturer[126];
+ if (HidD_GetManufacturerString(hHidFile, manufacturer, sizeof(manufacturer) /*in bytes*/)) {
+ ffStrbufSetWS(&device->name, manufacturer);
+ FF_STRBUF_AUTO_DESTROY displayNameStr = ffStrbufCreateWS(displayName);
+ ffStrbufAppendC(&device->name, ' ');
+ ffStrbufAppend(&device->name, &displayNameStr);
+ } else {
+ ffStrbufSetWS(&device->name, displayName);
+ }
+ }
+ }
+
+ wchar_t serialNumber[127] = L"";
+ if (HidD_GetSerialNumberString(hHidFile, serialNumber, sizeof(serialNumber) /*in bytes*/)) {
+ ffStrbufSetWS(&device->serial, serialNumber);
+ }
+
+ PHIDP_PREPARSED_DATA preparsedData = NULL;
+ if (HidD_GetPreparsedData(hHidFile, &preparsedData)) {
+ HIDP_CAPS caps;
+ NTSTATUS capsResult = HidP_GetCaps(preparsedData, &caps);
+ HidD_FreePreparsedData(preparsedData);
+ if (!NT_SUCCESS(capsResult)) {
+ continue;
+ }
+
+ if (
+ (rdi.hid.dwVendorId == 0x054C && (rdi.hid.dwProductId == 0x05C4 || // PS4 Gen1
+ rdi.hid.dwProductId == 0x09CC // PS4 Gen2
+ )) ||
+ (rdi.hid.dwVendorId == 0x057E && (rdi.hid.dwProductId == 0x2009 // NS Pro
+ ))) {
+ // Controller must be connected by other programs
+ FF_AUTO_FREE uint8_t* reportBuffer = malloc(caps.InputReportByteLength);
+ OVERLAPPED overlapped = {};
+ DWORD nBytes;
+ if (ReadFile(hHidFile, reportBuffer, caps.InputReportByteLength, &nBytes, &overlapped) ||
+ GetOverlappedResultEx(hHidFile, &overlapped, &nBytes, FF_IO_TERM_RESP_WAIT_MS, TRUE)) {
+ if (rdi.hid.dwVendorId == 0x054C) {
+ if (nBytes > 31) {
+ uint8_t batteryInfo = reportBuffer[caps.InputReportByteLength == 64 /*USB?*/ ? 30 : 32];
+ device->battery = (uint8_t) ((batteryInfo & 0x0f) * 100 / (batteryInfo & 0x10 /*charging?*/ ? 11 /*BATTERY_MAX_USB*/ : 8 /*BATTERY_MAX*/));
+ if (device->battery > 100) {
+ device->battery = 100;
+ }
+ }
+ } else {
+ if (nBytes > 3 && reportBuffer[0] == 0x30) {
+ uint8_t batteryInfo = reportBuffer[2];
+ device->battery = (uint8_t) (((batteryInfo & 0xE0) >> 4) * 100 / 8);
+ if (device->battery == 0) {
+ device->battery = 1;
+ } else if (device->battery > 100) {
+ device->battery = 100;
+ }
+ }
+ }
+ } else {
+ CancelIo(hHidFile);
+ }
+ }
+ }
+ }
+
+ return NULL;
+}