Skip to content

Commit

Permalink
Some initial work towards generic HID gamepad support
Browse files Browse the repository at this point in the history
  • Loading branch information
Sainan committed Dec 2, 2024
1 parent 4f06783 commit 6d85f48
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 22 deletions.
7 changes: 4 additions & 3 deletions CLI/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -174,22 +174,23 @@ 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)
{
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())
Expand Down
32 changes: 28 additions & 4 deletions soup/HidReportDescriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,15 @@ 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();
}
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)
Expand Down Expand Up @@ -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)
{
Expand All @@ -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<float>(value) / static_cast<uint8_t>(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<uint16_t>(hi) << 8) | lo);
if (pUsageId != f.usage_ids.end())
{
parsed.dynamic_values.emplace(HidUsage(f.usage_page, *pUsageId), static_cast<float>(value) / f.logical_max);
parsed.dynamic_values.emplace(HidUsage(f.usage_page, *pUsageId), static_cast<float>(value) / static_cast<uint16_t>(f.logical_max));
++pUsageId;
}
}
Expand Down
86 changes: 73 additions & 13 deletions soup/hwGamepad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ NAMESPACE_SOUP
std::vector<hwGamepad> 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
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand All @@ -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);
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<uint8_t>(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;
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion soup/hwGamepad.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion soup/hwHid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<uint32_t>(pp_data->items[i].logical_maximum),
static_cast<uint32_t>(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)
Expand Down

0 comments on commit 6d85f48

Please sign in to comment.