From 6d85f480c73cd0dc23b8c38887e61470a8ce8fc9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 2 Dec 2024 14:29:17 +0100 Subject: [PATCH] Some initial work towards generic HID gamepad support --- CLI/cli.cpp | 7 +-- soup/HidReportDescriptor.cpp | 32 ++++++++++++-- soup/hwGamepad.cpp | 86 ++++++++++++++++++++++++++++++------ soup/hwGamepad.hpp | 2 +- soup/hwHid.cpp | 5 ++- 5 files changed, 110 insertions(+), 22 deletions(-) diff --git a/CLI/cli.cpp b/CLI/cli.cpp index 5c678226..a722fa3b 100644 --- a/CLI/cli.cpp +++ b/CLI/cli.cpp @@ -174,14 +174,15 @@ int main(int argc, const char** argv) for (auto& gp : hwGamepad::getAll()) { console.init(false); - std::cout << gp.name << " detected, awaiting input.\n"; + const std::string name = gp.name ? gp.name : gp.hid.getProductName(); + std::cout << name << " detected, awaiting input.\n"; hwGamepad::Status prev_status{}; while (true) { auto status = gp.receiveStatus(); if (gp.disconnected) { - std::cout << gp.name << " disconnected.\n"; + std::cout << name << " disconnected.\n"; return 0; } if (memcmp(&status, &prev_status, sizeof(hwGamepad::Status)) != 0) @@ -189,7 +190,7 @@ int main(int argc, const char** argv) prev_status = status; console.clearScreen(); console.setCursorPos(0, 0); - std::cout << gp.name << "\n"; + std::cout << name << "\n"; std::cout << "Left Stick: " << status.left_stick_x << ", " << status.left_stick_y << "\n"; std::cout << "Right Stick: " << status.right_stick_x << ", " << status.right_stick_y << "\n"; if (gp.hasAnalogueTriggers()) diff --git a/soup/HidReportDescriptor.cpp b/soup/HidReportDescriptor.cpp index e7561ee9..76b7cccc 100644 --- a/soup/HidReportDescriptor.cpp +++ b/soup/HidReportDescriptor.cpp @@ -168,6 +168,7 @@ NAMESPACE_SOUP const auto flags = get_hid_report_bytes(rawdesc, size, data_len, pos); const bool is_variable = ((flags >> 1) & 1) != 0; + const bool has_null = ((flags >> 6) & 1) != 0; if (!is_variable) { usage_ids.clear(); @@ -175,7 +176,7 @@ NAMESPACE_SOUP parsed.input_report_fields.emplace_back(ReportField{ report_size, report_count, - logical_max, + logical_max + has_null, is_variable, usage_page, std::move(usage_ids) @@ -235,6 +236,10 @@ NAMESPACE_SOUP HidParsedReport parsed; MemoryRefReader mr(report, size); + if (!report_ids.empty()) + { + mr.skip(1); // Ignore report id + } BitReader br(&mr); for (const auto& f : input_report_fields) { @@ -258,16 +263,35 @@ NAMESPACE_SOUP } continue; } - else if (f.size == 8 && f.logical_max != 0) + else if (f.size <= 8 && f.logical_max != 0) { auto pUsageId = f.usage_ids.cbegin(); for (uint32_t i = 0; i != f.count; ++i) { uint8_t value = 0; - SOUP_UNUSED(br.u8(8, value)); + SOUP_UNUSED(br.u8(f.size, value)); + if (pUsageId != f.usage_ids.end()) + { + parsed.dynamic_values.emplace(HidUsage(f.usage_page, *pUsageId), static_cast(value) / static_cast(f.logical_max)); + ++pUsageId; + } + } + br.finishByte(); // It seems like in the case of a "Hat Switch", the high nibble is unused. + continue; + } + else if (f.size == 16 && f.logical_max != 0) + { + auto pUsageId = f.usage_ids.cbegin(); + for (uint32_t i = 0; i != f.count; ++i) + { + uint8_t hi = 0; + uint8_t lo = 0; + SOUP_UNUSED(br.u8(8, lo)); + SOUP_UNUSED(br.u8(8, hi)); + uint16_t value = ((static_cast(hi) << 8) | lo); if (pUsageId != f.usage_ids.end()) { - parsed.dynamic_values.emplace(HidUsage(f.usage_page, *pUsageId), static_cast(value) / f.logical_max); + parsed.dynamic_values.emplace(HidUsage(f.usage_page, *pUsageId), static_cast(value) / static_cast(f.logical_max)); ++pUsageId; } } diff --git a/soup/hwGamepad.cpp b/soup/hwGamepad.cpp index 6e697cb4..f0d007f0 100644 --- a/soup/hwGamepad.cpp +++ b/soup/hwGamepad.cpp @@ -41,7 +41,7 @@ NAMESPACE_SOUP std::vector res{}; for (auto& hid : hwHid::getAll()) { - if (hid.usage == 0x05) + if (hid.usage_page == 1 && hid.usage == 5) // Gamepad { if (hid.vendor_id == 0x54c) // Sony { @@ -72,6 +72,12 @@ NAMESPACE_SOUP bool is_bluetooth = !hid.sendReport(std::move(buf)); res.emplace_back("Stadia Controller", std::move(hid)).stadia.is_bluetooth = is_bluetooth; } +#if false // Generic HID Gamepad support is pretty bad right now. + else + { + res.emplace_back(nullptr, std::move(hid)); + } +#endif } } return res; @@ -218,6 +224,14 @@ NAMESPACE_SOUP } const Buffer& report_data = hid.receiveReport(); + + // Ensure 'report_data' is the latest the device has to offer. + // This works because the buffer ref returned is always the same. + while (hid.hasReport()) + { + SOUP_UNUSED(hid.receiveReport()); + } + SOUP_IF_UNLIKELY (report_data.empty()) { if (hid.vendor_id == 0x54c && hid.isBluetooth() && isDs4StillAlive(hid)) @@ -232,6 +246,7 @@ NAMESPACE_SOUP //std::cout << string::bin2hex(report_data.toString(), true) << std::endl; +#if true if (hid.vendor_id == 0x54c) // Sony, Y Down { MemoryRefReader r(report_data); @@ -322,7 +337,7 @@ NAMESPACE_SOUP status.num_fingers_on_touchpad++; } } - else if (hid.vendor_id == 0x57e) // Nintendo Switch Pro Controller, Y Up + else if (hid.vendor_id == 0x57e && hid.product_id == 0x2009) // Nintendo Switch Pro Controller, Y Up { SOUP_IF_UNLIKELY (!switch_pro.has_calibration_data) { @@ -395,7 +410,7 @@ NAMESPACE_SOUP status.left_trigger = status.buttons[BTN_LTRIGGER] ? 1.0f : 0.0f; status.right_trigger = status.buttons[BTN_RTRIGGER] ? 1.0f : 0.0f; } - else // Stadia Controller, Y Down + else if (hid.vendor_id == 0x18d1 && hid.product_id == 0x9400) // Stadia Controller, Y Down { MemoryRefReader r(report_data); @@ -432,6 +447,50 @@ NAMESPACE_SOUP status.buttons[BTN_ACT_RIGHT] = (report.buttons_2 & 0x20); status.buttons[BTN_ACT_DOWN] = (report.buttons_2 & 0x40); } +#else + if (false) { } +#endif +#if false + else + { + //std::cout << string::bin2hex(report_data.toString()) << std::endl; + const auto report_desc = hid.getReportDescriptor(); + const auto report = report_desc.parseInputReport(report_data.data(), report_data.size()); + + /*for (const auto& sel : report.active_selectors) + { + std::cout << std::hex << sel.value << std::endl; + } + for (const auto& val : report.dynamic_values) + { + std::cout << std::hex << val.first.value << ": " << val.second << std::endl; + }*/ + + status.left_stick_x = report.dynamic_values.get_or_default({ 1, 0x30 }, 0.5f); + status.left_stick_y = report.dynamic_values.get_or_default({ 1, 0x31 }, 0.5f); + + const float Z = report.dynamic_values.get_or_default({ 1, 0x32 }, 0.5f); + if (fabsf(Z - 0.5f) > 0.00001f) + { + if (Z < 0.5f) + { + status.right_trigger = 1.0f - (Z * 2.0f); + } + else + { + status.left_trigger = (Z - 0.5f) * 2.0f; + } + } + + status.setDpad(static_cast(report.dynamic_values.get_or_default({ 1, 0x39 }, 1.0f) * 8.0f)); + + // Captured from Stadia Controller. Mappings are slightly different on the DS4. + status.buttons[BTN_ACT_DOWN] = report.active_selectors.count({ 9, 1 }) != 0; + status.buttons[BTN_ACT_RIGHT] = report.active_selectors.count({ 9, 2 }) != 0; + status.buttons[BTN_ACT_LEFT] = report.active_selectors.count({ 9, 4 }) != 0; + status.buttons[BTN_ACT_UP] = report.active_selectors.count({ 9, 5 }) != 0; + } +#endif } return status; @@ -445,7 +504,7 @@ NAMESPACE_SOUP bool hwGamepad::hasInvertedActionButtons() const noexcept { - return hid.vendor_id == 0x57e; // Nintendo Switch Pro Controller + return hid.vendor_id == 0x57e; // Nintendo } bool hwGamepad::hasTouchpad() const noexcept @@ -484,19 +543,20 @@ NAMESPACE_SOUP bool hwGamepad::canRumble() const noexcept { - if (hid.vendor_id == 0x54c && hid.product_id == 0xce6) // DualSense 5 - { - return false; // TODO: Look into this. - } - if (hid.vendor_id == 0x57e) // Nintendo Switch Pro Controller + if (hid.vendor_id == 0x54c) // Sony { - return false; // This thing *can* rumble, but I just can't be arsed with their format right now -- let's say it's a TODO. :) + if (hid.product_id == 0xce6) // DualSense 5 + { + return false; // TODO: Look into this. + } + return true; } - if (hid.vendor_id == 0x18d1) // Stadia Controller; rumble only works via USB + // TODO: Look into Nintendo Switch Pro Controller's rumble format. + if (hid.vendor_id == 0x18d1 && hid.product_id == 0x9400) // Stadia Controller; rumble only works via USB { return !stadia.is_bluetooth; } - return true; + return false; } void hwGamepad::rumbleWeak(uint8_t intensity, time_t ms) @@ -559,7 +619,7 @@ NAMESPACE_SOUP buf.push_back(0x00); sendReportDs4(std::move(buf)); } - else // Stadia Controller + else if (hid.vendor_id == 0x18d1 && hid.product_id == 0x9400) // Stadia Controller { Buffer buf(5); buf.push_back(0x05); diff --git a/soup/hwGamepad.hpp b/soup/hwGamepad.hpp index 44edbc77..8846c040 100644 --- a/soup/hwGamepad.hpp +++ b/soup/hwGamepad.hpp @@ -34,7 +34,7 @@ NAMESPACE_SOUP void setDpad(uint8_t dpad); }; - const char* name; + const char* name; // can be nullptr for generic devices hwHid hid; bool disconnected = false; union diff --git a/soup/hwHid.cpp b/soup/hwHid.cpp index 43c2e8b9..c55011f6 100644 --- a/soup/hwHid.cpp +++ b/soup/hwHid.cpp @@ -757,6 +757,9 @@ NAMESPACE_SOUP for (uint32_t i = 0; i != pp_data->header.input_item_count; ++i) { + //std::cout << "report_id: " << (int)pp_data->items[i].report_id << std::endl; + //std::cout << (int)pp_data->items[i].byte_index << ", " << (int)pp_data->items[i].bit_index << std::endl; + if (pp_data->items[i].report_id != 0) { result.report_ids.emplace(pp_data->items[i].report_id); @@ -770,7 +773,7 @@ NAMESPACE_SOUP result.input_report_fields.emplace_back(HidReportDescriptor::ReportField{ pp_data->items[i].bit_size, pp_data->items[i].report_count, - static_cast(pp_data->items[i].logical_maximum), + static_cast(pp_data->items[i].logical_maximum) + (((pp_data->items[i].bit_field >> 6) & 1) != 0), ((pp_data->items[i].bit_field >> 1) & 1) != 0, pp_data->items[i].usage_page, std::move(usage_ids)