mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-28 15:53:51 +01:00
Gamepad (Windows): fixes battery level detection
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#include "gamepad.h"
|
||||
#include "common/io.h"
|
||||
#include "common/mallocHelper.h"
|
||||
#include "common/time.h"
|
||||
#include "common/windows/unicode.h"
|
||||
|
||||
#include <windows.h>
|
||||
@@ -15,6 +16,8 @@ static const char* detectKnownDeviceName(uint32_t vendorId, uint32_t productId)
|
||||
return "Nintendo Switch Joycon L";
|
||||
case 0x2007:
|
||||
return "Nintendo Switch Joycon R";
|
||||
case 0x2008:
|
||||
return "Nintendo Switch Joycon L+R";
|
||||
case 0x2009:
|
||||
return "Nintendo Switch Pro Controller";
|
||||
case 0x200E:
|
||||
@@ -22,6 +25,15 @@ static const char* detectKnownDeviceName(uint32_t vendorId, uint32_t productId)
|
||||
case 0x2017:
|
||||
return "Nintendo Switch SNES Controller";
|
||||
|
||||
case 0x2066:
|
||||
return "Nintendo Switch 2 Joycon R";
|
||||
case 0x2067:
|
||||
return "Nintendo Switch 2 Joycon L";
|
||||
case 0x2068:
|
||||
return "Nintendo Switch 2 Joycon L+R";
|
||||
case 0x2069:
|
||||
return "Nintendo Switch 2 Pro Controller";
|
||||
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
@@ -44,6 +56,8 @@ static const char* detectKnownDeviceName(uint32_t vendorId, uint32_t productId)
|
||||
return "Sony DualSense";
|
||||
case 0x0DF2:
|
||||
return "Sony DualSense Edge";
|
||||
case 0x0E5F:
|
||||
return "Sony Access Controller";
|
||||
|
||||
default:
|
||||
return nullptr;
|
||||
@@ -77,6 +91,231 @@ static const char* detectKnownDeviceName(uint32_t vendorId, uint32_t productId)
|
||||
}
|
||||
}
|
||||
|
||||
// A HID read on Windows hands back as many bytes as the descriptor declares for the largest report,
|
||||
// so the number of bytes a read returns says nothing about which report actually arrived. The report
|
||||
// id, the first byte of the buffer, is what identifies it.
|
||||
static bool readHidReport(HANDLE hHidFile, uint8_t* buffer, uint32_t length, DWORD timeoutMs, DWORD* nBytes) {
|
||||
OVERLAPPED overlapped = {};
|
||||
DWORD read;
|
||||
if (ReadFile(hHidFile, buffer, length, &read, &overlapped)) {
|
||||
*nBytes = read;
|
||||
return true;
|
||||
}
|
||||
if (GetOverlappedResultEx(hHidFile, &overlapped, &read, timeoutMs, TRUE)) {
|
||||
*nBytes = read;
|
||||
return true;
|
||||
}
|
||||
CancelIo(hHidFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The DualShock 4 sends a battery level in its state packet, but only in the extended report it
|
||||
// switches to when a program asks it to. Until then it sends the plain HID gamepad report, which
|
||||
// ends after the buttons.
|
||||
typedef enum FFDs4ReportId: uint8_t {
|
||||
FF_DS4_REPORT_ID_USB_STATE = 0x01,
|
||||
FF_DS4_REPORT_ID_BLUETOOTH_STATE_FIRST = 0x11,
|
||||
FF_DS4_REPORT_ID_BLUETOOTH_STATE_LAST = 0x19,
|
||||
} FFDs4ReportId;
|
||||
|
||||
// Offset of the battery level in the state packet, which is what the DualShock 4 documents and
|
||||
// what SDL reads: five pad bytes after the six IMU axes.
|
||||
#define FF_DS4_STATE_PACKET_BATTERY_OFFSET 29
|
||||
|
||||
// The plain HID gamepad report is ten bytes long, report id included
|
||||
#define FF_DS4_NON_EXTENDED_REPORT_LENGTH 10
|
||||
|
||||
// Reads the battery level out of one input report of a DualShock 4, and returns 0 when the report
|
||||
// carries none. `report[0]` is the report id and `reportLength` the size of the buffer.
|
||||
static uint8_t parseDs4Battery(const uint8_t* report, uint32_t reportLength) {
|
||||
uint32_t batteryOffset;
|
||||
|
||||
if (report[0] == FF_DS4_REPORT_ID_USB_STATE) {
|
||||
// A non-extended report stops after the buttons, so a non-zero byte past that can only come
|
||||
// from an extended packet. A wired controller always sends the extended one, a Bluetooth one
|
||||
// only once another program has enabled it.
|
||||
bool extended = false;
|
||||
for (uint32_t i = FF_DS4_NON_EXTENDED_REPORT_LENGTH; i < reportLength; ++i) {
|
||||
if (report[i]) {
|
||||
extended = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!extended) {
|
||||
return 0;
|
||||
}
|
||||
// On USB the state packet follows the report id directly
|
||||
batteryOffset = 1 + FF_DS4_STATE_PACKET_BATTERY_OFFSET;
|
||||
} else if (report[0] >= FF_DS4_REPORT_ID_BLUETOOTH_STATE_FIRST && report[0] <= FF_DS4_REPORT_ID_BLUETOOTH_STATE_LAST) {
|
||||
// A Bluetooth state packet has two extra bytes in front, the first of which says whether the
|
||||
// packet carries HID state at all
|
||||
if (reportLength <= 1 || !(report[1] & 0x80)) {
|
||||
return 0;
|
||||
}
|
||||
batteryOffset = 3 + FF_DS4_STATE_PACKET_BATTERY_OFFSET;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (batteryOffset >= reportLength) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t batteryInfo = report[batteryOffset];
|
||||
uint8_t level = batteryInfo & 0x0f;
|
||||
if ((batteryInfo & 0x10 /*charging?*/) && level > 11) {
|
||||
return 0; // A charging controller reports 11 for a full battery; anything above is not a level
|
||||
}
|
||||
|
||||
uint8_t battery = (uint8_t) (level * 10 + 5); // 0..11 maps onto the middle of eleven 10% steps
|
||||
return battery > 100 ? 100 : battery;
|
||||
}
|
||||
|
||||
// The DualSense uses one report id over USB and another over Bluetooth, and the state packet follows
|
||||
// the id directly in the first case and one byte later in the second. The official controller sends
|
||||
// SDL's PS5StatePacket_t; the shorter PS5StatePacketAlt_t belongs to third-party pads and is not read.
|
||||
typedef enum FFDualSenseReportId: uint8_t {
|
||||
FF_DUALSENSE_REPORT_ID_USB_STATE = 0x01,
|
||||
FF_DUALSENSE_REPORT_ID_BLUETOOTH_STATE = 0x31,
|
||||
} FFDualSenseReportId;
|
||||
|
||||
// Offset of the battery level in PS5StatePacket_t, and the length of a whole USB state report
|
||||
#define FF_DUALSENSE_STATE_PACKET_BATTERY_OFFSET 52
|
||||
#define FF_DUALSENSE_USB_REPORT_LENGTH 64
|
||||
|
||||
static uint8_t parseDualSenseBattery(const uint8_t* report, uint32_t reportLength) {
|
||||
uint32_t batteryOffset;
|
||||
|
||||
if (report[0] == FF_DUALSENSE_REPORT_ID_USB_STATE) {
|
||||
// Over Bluetooth the controller answers the same id with a short report that stops before the
|
||||
// battery, so the USB report length is what tells the two apart
|
||||
if (reportLength != FF_DUALSENSE_USB_REPORT_LENGTH) {
|
||||
return 0;
|
||||
}
|
||||
batteryOffset = 1 + FF_DUALSENSE_STATE_PACKET_BATTERY_OFFSET;
|
||||
} else if (report[0] == FF_DUALSENSE_REPORT_ID_BLUETOOTH_STATE) {
|
||||
// The Bluetooth report has one extra byte in front of the state packet
|
||||
batteryOffset = 2 + FF_DUALSENSE_STATE_PACKET_BATTERY_OFFSET;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (batteryOffset >= reportLength) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t batteryInfo = report[batteryOffset];
|
||||
uint32_t status = (batteryInfo >> 4) & 0x0f;
|
||||
if (status == 2) {
|
||||
return 100; // Charged
|
||||
}
|
||||
if (status > 1) {
|
||||
return 0; // Not a status the controller documents
|
||||
}
|
||||
|
||||
uint32_t battery = (batteryInfo & 0x0f) * 10 + 5; // Discharging and charging share one scale
|
||||
return (uint8_t) (battery > 100 ? 100 : battery);
|
||||
}
|
||||
|
||||
// A Switch controller interleaves its full state report with subcommand replies, which carry no
|
||||
// battery at all. SDL skips those replies and waits for the next full report, and so does this,
|
||||
// inside the same one-second budget a single read of any other controller gets.
|
||||
typedef enum FFSwitchReportId: uint8_t {
|
||||
FF_SWITCH_REPORT_ID_FULL_STATE_FIRST = 0x30,
|
||||
FF_SWITCH_REPORT_ID_FULL_STATE_LAST = 0x31,
|
||||
} FFSwitchReportId;
|
||||
|
||||
// The controller state packet starts one byte after the report id, and its second byte carries the
|
||||
// battery level in its top three bits and the charging flag in bit 4. Anything that is not a full
|
||||
// state report carries no battery and answers 0.
|
||||
#define FF_SWITCH_STATE_PACKET_BATTERY_OFFSET 2
|
||||
|
||||
static uint8_t parseSwitchBattery(const uint8_t* report, uint32_t reportLength) {
|
||||
if (report[0] < FF_SWITCH_REPORT_ID_FULL_STATE_FIRST || report[0] > FF_SWITCH_REPORT_ID_FULL_STATE_LAST) {
|
||||
return 0;
|
||||
}
|
||||
if (reportLength <= FF_SWITCH_STATE_PACKET_BATTERY_OFFSET) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t batteryInfo = report[FF_SWITCH_STATE_PACKET_BATTERY_OFFSET];
|
||||
uint32_t battery = ((batteryInfo & 0xE0) >> 4) * 100 / 8; // A level of 8 is a full battery
|
||||
if (battery == 0) {
|
||||
return 1; // Keep a drained controller apart from the "unknown" 0 the module uses
|
||||
}
|
||||
return (uint8_t) (battery > 100 ? 100 : battery);
|
||||
}
|
||||
|
||||
// The longest run of reports without a full state report measured on a Pro Controller was eleven
|
||||
#define FF_SWITCH_BATTERY_MAX_READS 16
|
||||
|
||||
static uint8_t detectSwitchBattery(HANDLE hHidFile, uint8_t* reportBuffer, uint32_t reportLength) {
|
||||
const double deadline = ffTimeGetTick() + (double) FF_IO_TERM_RESP_WAIT_MS;
|
||||
|
||||
for (uint32_t i = 0; i < FF_SWITCH_BATTERY_MAX_READS; ++i) {
|
||||
double remaining = deadline - ffTimeGetTick();
|
||||
if (remaining < 1.) {
|
||||
break;
|
||||
}
|
||||
|
||||
DWORD nBytes;
|
||||
if (!readHidReport(hHidFile, reportBuffer, reportLength, (DWORD) remaining, &nBytes)) {
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t battery = parseSwitchBattery(reportBuffer, reportLength);
|
||||
if (battery) {
|
||||
return battery;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Which of the battery layouts a controller uses, if any. A controller that is not listed here keeps
|
||||
// the module's "unknown" 0 without a single read being issued.
|
||||
typedef enum FFGamepadBatteryKind: uint8_t {
|
||||
FF_GAMEPAD_BATTERY_NONE,
|
||||
FF_GAMEPAD_BATTERY_DS4,
|
||||
FF_GAMEPAD_BATTERY_DUALSENSE,
|
||||
FF_GAMEPAD_BATTERY_SWITCH,
|
||||
} FFGamepadBatteryKind;
|
||||
|
||||
static FFGamepadBatteryKind detectBatteryKind(uint32_t vendorId, uint32_t productId) {
|
||||
switch (vendorId) {
|
||||
case 0x054C: // Sony
|
||||
switch (productId) {
|
||||
case 0x0CE6: // DualSense
|
||||
case 0x0DF2: // DualSense Edge
|
||||
case 0x0E5F: // Access Controller
|
||||
return FF_GAMEPAD_BATTERY_DUALSENSE;
|
||||
|
||||
case 0x05C4: // DualShock 4 Gen1
|
||||
case 0x09CC: // DualShock 4 Gen2
|
||||
case 0x0BA0: // DualShock 4 USB receiver
|
||||
return FF_GAMEPAD_BATTERY_DS4;
|
||||
|
||||
default:
|
||||
return FF_GAMEPAD_BATTERY_NONE;
|
||||
}
|
||||
|
||||
case 0x057E: // Nintendo
|
||||
switch (productId) {
|
||||
case 0x2006: // Joy-Con L
|
||||
case 0x2007: // Joy-Con R
|
||||
case 0x2008: // Joy-Con pair
|
||||
case 0x2009: // Pro Controller
|
||||
return FF_GAMEPAD_BATTERY_SWITCH;
|
||||
|
||||
default:
|
||||
return FF_GAMEPAD_BATTERY_NONE;
|
||||
}
|
||||
|
||||
default:
|
||||
return FF_GAMEPAD_BATTERY_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
|
||||
UINT nDevices = 0;
|
||||
if (GetRawInputDeviceList(nullptr, &nDevices, sizeof(RAWINPUTDEVICELIST))) {
|
||||
@@ -159,39 +398,28 @@ const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
|
||||
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
|
||||
// Sony and Nintendo controllers report a battery level only in a report a program has to
|
||||
// ask the controller for, so a controller that nothing else has opened reports none.
|
||||
FFGamepadBatteryKind batteryKind = detectBatteryKind(rdi.hid.dwVendorId, rdi.hid.dwProductId);
|
||||
if (batteryKind != FF_GAMEPAD_BATTERY_NONE) {
|
||||
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;
|
||||
}
|
||||
switch (batteryKind) {
|
||||
case FF_GAMEPAD_BATTERY_DS4:
|
||||
if (readHidReport(hHidFile, reportBuffer, caps.InputReportByteLength, FF_IO_TERM_RESP_WAIT_MS, &nBytes)) {
|
||||
device->battery = parseDs4Battery(reportBuffer, caps.InputReportByteLength);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
break;
|
||||
case FF_GAMEPAD_BATTERY_DUALSENSE:
|
||||
if (readHidReport(hHidFile, reportBuffer, caps.InputReportByteLength, FF_IO_TERM_RESP_WAIT_MS, &nBytes)) {
|
||||
device->battery = parseDualSenseBattery(reportBuffer, caps.InputReportByteLength);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CancelIo(hHidFile);
|
||||
break;
|
||||
case FF_GAMEPAD_BATTERY_SWITCH:
|
||||
device->battery = detectSwitchBattery(hHidFile, reportBuffer, caps.InputReportByteLength);
|
||||
break;
|
||||
case FF_GAMEPAD_BATTERY_NONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user