Commit 2d2b87e5 authored by Matt Reynolds's avatar Matt Reynolds Committed by Commit Bot

Add support for Switch Pro Controller on Linux

BUG=749295

Change-Id: I939cc7ab58cc9a8196fe728c926341f4ccebea20
Reviewed-on: https://chromium-review.googlesource.com/887253Reviewed-by: default avatarBrandon Jones <bajones@chromium.org>
Commit-Queue: Matt Reynolds <mattreynolds@chromium.org>
Cr-Commit-Position: refs/heads/master@{#534563}
parent 9fe6e9f8
......@@ -68,6 +68,8 @@ component("gamepad") {
"raw_input_gamepad_device_win.h",
"switch_pro_controller_base.cc",
"switch_pro_controller_base.h",
"switch_pro_controller_linux.cc",
"switch_pro_controller_linux.h",
"udev_gamepad_linux.cc",
"udev_gamepad_linux.h",
"xbox_controller_mac.h",
......
......@@ -6,6 +6,7 @@
#include <fcntl.h>
#include <limits.h>
#include <linux/hidraw.h>
#include <linux/input.h>
#include <linux/joystick.h>
#include <sys/ioctl.h>
......@@ -33,6 +34,17 @@ static inline bool test_bit(int bit, const unsigned long* data) {
return data[bit / LONG_BITS] & (1UL << (bit % LONG_BITS));
}
GamepadBusType GetEvdevBusType(int fd) {
struct input_id input_info;
if (HANDLE_EINTR(ioctl(fd, EVIOCGID, &input_info)) >= 0) {
if (input_info.bustype == BUS_USB)
return GAMEPAD_BUS_USB;
if (input_info.bustype == BUS_BLUETOOTH)
return GAMEPAD_BUS_BLUETOOTH;
}
return GAMEPAD_BUS_UNKNOWN;
}
bool HasRumbleCapability(int fd) {
unsigned long evbit[BITS_TO_LONGS(EV_MAX)];
unsigned long ffbit[BITS_TO_LONGS(FF_MAX)];
......@@ -49,6 +61,28 @@ bool HasRumbleCapability(int fd) {
return test_bit(FF_RUMBLE, ffbit);
}
bool GetHidrawDevinfo(int fd,
GamepadBusType* bus_type,
uint16_t* vendor_id,
uint16_t* product_id) {
struct hidraw_devinfo info;
if (HANDLE_EINTR(ioctl(fd, HIDIOCGRAWINFO, &info)) < 0)
return false;
if (bus_type) {
if (info.bustype == BUS_USB)
*bus_type = GAMEPAD_BUS_USB;
else if (info.bustype == BUS_BLUETOOTH)
*bus_type = GAMEPAD_BUS_BLUETOOTH;
else
*bus_type = GAMEPAD_BUS_UNKNOWN;
}
if (vendor_id)
*vendor_id = static_cast<uint16_t>(info.vendor);
if (product_id)
*product_id = static_cast<uint16_t>(info.product);
return true;
}
int StoreRumbleEffect(int fd,
int effect_id,
uint16_t duration,
......@@ -107,14 +141,26 @@ bool GamepadDeviceLinux::IsEmpty() const {
}
bool GamepadDeviceLinux::SupportsVibration() const {
// Dualshock4 vibration is supported through the hidraw node.
if (is_dualshock4_)
return hidraw_fd_ >= 0 && dualshock4_ != nullptr;
if (dualshock4_)
return true;
// Vibration is only supported over USB.
// TODO(mattreynolds): add support for Switch Pro vibration over Bluetooth.
if (switch_pro_)
return bus_type_ == GAMEPAD_BUS_USB;
return supports_force_feedback_ && evdev_fd_ >= 0;
}
void GamepadDeviceLinux::ReadPadState(Gamepad* pad) const {
if (switch_pro_ && bus_type_ == GAMEPAD_BUS_USB) {
// When connected over USB, the Switch Pro controller does not correctly
// report its state over USB HID. Instead, fetch the state using the
// device's vendor-specific USB protocol.
switch_pro_->ReadUsbPadState(pad);
return;
}
DCHECK_GE(joydev_fd_, 0);
js_event event;
......@@ -142,6 +188,12 @@ void GamepadDeviceLinux::ReadPadState(Gamepad* pad) const {
}
}
GamepadStandardMappingFunction GamepadDeviceLinux::GetMappingFunction() const {
return GetGamepadStandardMappingFunction(vendor_id_.c_str(),
product_id_.c_str(),
version_number_.c_str(), bus_type_);
}
bool GamepadDeviceLinux::IsSameDevice(const UdevGamepadLinux& pad_info) {
return pad_info.syspath_prefix == syspath_prefix_;
}
......@@ -205,8 +257,6 @@ bool GamepadDeviceLinux::OpenJoydevNode(const UdevGamepadLinux& pad_info,
product_id_ = product_id ? product_id : "";
version_number_ = version_number ? version_number : "";
name_ = name_string;
is_dualshock4_ =
Dualshock4ControllerBase::IsDualshock4(vendor_id_int, product_id_int);
return true;
}
......@@ -233,6 +283,7 @@ bool GamepadDeviceLinux::OpenEvdevNode(const UdevGamepadLinux& pad_info) {
return false;
supports_force_feedback_ = HasRumbleCapability(evdev_fd_);
bus_type_ = GetEvdevBusType(evdev_fd_);
return true;
}
......@@ -258,7 +309,27 @@ bool GamepadDeviceLinux::OpenHidrawNode(const UdevGamepadLinux& pad_info) {
if (hidraw_fd_ < 0)
return false;
dualshock4_ = std::make_unique<Dualshock4ControllerLinux>(hidraw_fd_);
uint16_t vendor_id;
uint16_t product_id;
bool is_dualshock4 = false;
bool is_switch_pro = false;
if (GetHidrawDevinfo(hidraw_fd_, &bus_type_, &vendor_id, &product_id)) {
is_dualshock4 =
Dualshock4ControllerLinux::IsDualshock4(vendor_id, product_id);
is_switch_pro =
SwitchProControllerLinux::IsSwitchPro(vendor_id, product_id);
DCHECK(!is_dualshock4 || !is_switch_pro);
}
if (is_dualshock4 && !dualshock4_)
dualshock4_ = std::make_unique<Dualshock4ControllerLinux>(hidraw_fd_);
if (is_switch_pro && !switch_pro_) {
switch_pro_ = std::make_unique<SwitchProControllerLinux>(hidraw_fd_);
if (bus_type_ == GAMEPAD_BUS_USB)
switch_pro_->SendConnectionStatusQuery();
}
return true;
}
......@@ -267,6 +338,9 @@ void GamepadDeviceLinux::CloseHidrawNode() {
if (dualshock4_)
dualshock4_->Shutdown();
dualshock4_.reset();
if (switch_pro_)
switch_pro_->Shutdown();
switch_pro_.reset();
if (hidraw_fd_ >= 0) {
close(hidraw_fd_);
hidraw_fd_ = -1;
......@@ -275,9 +349,13 @@ void GamepadDeviceLinux::CloseHidrawNode() {
void GamepadDeviceLinux::SetVibration(double strong_magnitude,
double weak_magnitude) {
if (is_dualshock4_) {
if (dualshock4_)
dualshock4_->SetVibration(strong_magnitude, weak_magnitude);
if (dualshock4_) {
dualshock4_->SetVibration(strong_magnitude, weak_magnitude);
return;
}
if (switch_pro_) {
switch_pro_->SetVibration(strong_magnitude, weak_magnitude);
return;
}
......@@ -303,9 +381,13 @@ void GamepadDeviceLinux::SetVibration(double strong_magnitude,
}
void GamepadDeviceLinux::SetZeroVibration() {
if (is_dualshock4_) {
if (dualshock4_)
dualshock4_->SetZeroVibration();
if (dualshock4_) {
dualshock4_->SetZeroVibration();
return;
}
if (switch_pro_) {
switch_pro_->SetZeroVibration();
return;
}
......
......@@ -7,6 +7,8 @@
#include "device/gamepad/abstract_haptic_gamepad.h"
#include "device/gamepad/dualshock4_controller_linux.h"
#include "device/gamepad/gamepad_standard_mappings.h"
#include "device/gamepad/switch_pro_controller_linux.h"
#include "device/gamepad/udev_gamepad_linux.h"
extern "C" {
......@@ -40,6 +42,8 @@ class GamepadDeviceLinux : public AbstractHapticGamepad {
std::string GetVersionNumber() const { return version_number_; }
std::string GetName() const { return name_; }
std::string GetSyspathPrefix() const { return syspath_prefix_; }
GamepadBusType GetBusType() const { return bus_type_; }
GamepadStandardMappingFunction GetMappingFunction() const;
bool SupportsVibration() const;
......@@ -120,11 +124,15 @@ class GamepadDeviceLinux : public AbstractHapticGamepad {
// is associated with this device.
int hidraw_fd_;
// True if the vendor and product IDs match any model of Dualshock4.
bool is_dualshock4_;
// The type of the bus through which the device is connected, or
// GAMEPAD_BUS_UNKNOWN if the bus type could not be determined.
GamepadBusType bus_type_ = GAMEPAD_BUS_UNKNOWN;
// Dualshock4 functionality, if available.
std::unique_ptr<Dualshock4ControllerLinux> dualshock4_;
// Nintendo Switch Pro controller functionality, if available.
std::unique_ptr<SwitchProControllerLinux> switch_pro_;
};
} // namespace device
......
......@@ -60,6 +60,38 @@ void GamepadPlatformDataFetcherLinux::GetGamepadData(bool) {
ReadDeviceData(i);
}
// static
void GamepadPlatformDataFetcherLinux::UpdateGamepadStrings(
const std::string& name,
const std::string& vendor_id,
const std::string& product_id,
bool has_standard_mapping,
Gamepad* pad) {
// Set the ID string. The ID contains the device name, vendor and product IDs,
// and an indication of whether the standard mapping is in use.
std::string id =
base::StringPrintf("%s (%sVendor: %s Product: %s)", name.c_str(),
has_standard_mapping ? "STANDARD GAMEPAD " : "",
vendor_id.c_str(), product_id.c_str());
base::TruncateUTF8ToByteSize(id, Gamepad::kIdLengthCap - 1, &id);
base::string16 tmp16 = base::UTF8ToUTF16(id);
memset(pad->id, 0, sizeof(pad->id));
tmp16.copy(pad->id, arraysize(pad->id) - 1);
// Set the mapper string to "standard" if the gamepad has a standard mapping,
// or the empty string otherwise.
if (has_standard_mapping) {
std::string mapping = "standard";
base::TruncateUTF8ToByteSize(mapping, Gamepad::kMappingLengthCap - 1,
&mapping);
tmp16 = base::UTF8ToUTF16(mapping);
memset(pad->mapping, 0, sizeof(pad->mapping));
tmp16.copy(pad->mapping, arraysize(pad->mapping) - 1);
} else {
pad->mapping[0] = 0;
}
}
// Used during enumeration, and monitor notifications.
void GamepadPlatformDataFetcherLinux::RefreshDevice(udev_device* dev) {
std::unique_ptr<UdevGamepadLinux> udev_gamepad =
......@@ -148,39 +180,11 @@ void GamepadPlatformDataFetcherLinux::RefreshJoydevDevice(
return;
}
std::string vendor_id = device->GetVendorId();
std::string product_id = device->GetProductId();
std::string version_number = device->GetVersionNumber();
std::string name = device->GetName();
GamepadStandardMappingFunction& mapper = state->mapper;
mapper = GetGamepadStandardMappingFunction(
vendor_id.c_str(), product_id.c_str(), version_number.c_str(),
GAMEPAD_BUS_UNKNOWN);
state->mapper = device->GetMappingFunction();
Gamepad& pad = state->data;
// Append the vendor and product information then convert the utf-8
// id string to WebUChar.
std::string id =
name + base::StringPrintf(" (%sVendor: %s Product: %s)",
mapper ? "STANDARD GAMEPAD " : "",
vendor_id.c_str(), product_id.c_str());
base::TruncateUTF8ToByteSize(id, Gamepad::kIdLengthCap - 1, &id);
base::string16 tmp16 = base::UTF8ToUTF16(id);
memset(pad.id, 0, sizeof(pad.id));
tmp16.copy(pad.id, arraysize(pad.id) - 1);
if (mapper) {
std::string mapping = "standard";
base::TruncateUTF8ToByteSize(mapping, Gamepad::kMappingLengthCap - 1,
&mapping);
tmp16 = base::UTF8ToUTF16(mapping);
memset(pad.mapping, 0, sizeof(pad.mapping));
tmp16.copy(pad.mapping, arraysize(pad.mapping) - 1);
} else {
pad.mapping[0] = 0;
}
UpdateGamepadStrings(device->GetName(), device->GetVendorId(),
device->GetProductId(), state->mapper != nullptr, &pad);
pad.vibration_actuator.type = GamepadHapticActuatorType::kDualRumble;
pad.vibration_actuator.not_null = device->SupportsVibration();
......@@ -207,6 +211,24 @@ void GamepadPlatformDataFetcherLinux::RefreshEvdevDevice(
DCHECK(state);
if (state) {
Gamepad& pad = state->data;
// To select the correct mapper for an arbitrary gamepad we may need info
// from both the joydev and evdev nodes. For instance, a gamepad that
// connects over USB and Bluetooth may need to select a mapper based on
// the connection type, but this information is only available through
// evdev. To ensure that gamepads are usable when evdev is unavailable, a
// preliminary mapping is assigned when the joydev node is enumerated.
//
// Here we check if associating the evdev node changed the mapping
// function that should be used for this gamepad. If so, assign the new
// mapper and rebuild the gamepad strings.
GamepadStandardMappingFunction mapper = device->GetMappingFunction();
if (mapper != state->mapper) {
state->mapper = mapper;
UpdateGamepadStrings(device->GetName(), device->GetVendorId(),
device->GetProductId(), mapper != nullptr, &pad);
}
pad.vibration_actuator.not_null = device->SupportsVibration();
}
}
......
......@@ -52,6 +52,13 @@ class DEVICE_GAMEPAD_EXPORT GamepadPlatformDataFetcherLinux
mojom::GamepadHapticsManager::ResetVibrationActuatorCallback) override;
private:
// Updates the ID and mapper strings in |pad| with new device info.
static void UpdateGamepadStrings(const std::string& name,
const std::string& vendor_id,
const std::string& product_id,
bool has_standard_mapping,
Gamepad* pad);
void OnAddedToProvider() override;
void RefreshDevice(udev_device* dev);
......
......@@ -67,4 +67,9 @@ void DpadFromAxis(Gamepad* mapped, float dir) {
mapped->buttons[BUTTON_INDEX_DPAD_LEFT].value = left ? 1.f : 0.f;
}
float RenormalizeAndClampAxis(float value, float min, float max) {
value = (2.f * (value - min) / (max - min)) - 1.f;
return value < -1.f ? -1.f : (value > 1.f ? 1.f : value);
}
} // namespace device
......@@ -80,6 +80,7 @@ GamepadButton AxisPositiveAsButton(float input);
GamepadButton ButtonFromButtonAndAxis(GamepadButton button, float axis);
GamepadButton NullButton();
void DpadFromAxis(Gamepad* mapped, float dir);
float RenormalizeAndClampAxis(float value, float min, float max);
} // namespace device
......
......@@ -11,6 +11,20 @@ namespace device {
namespace {
enum SwitchProButtons {
SWITCH_PRO_BUTTON_CAPTURE = BUTTON_INDEX_COUNT,
SWITCH_PRO_BUTTON_COUNT
};
// The Switch Pro controller reports a larger logical range than the analog
// axes are capable of, and as a result the received axis values only use about
// 70% of the total range. We renormalize the axis values to cover the full
// range. The axis extents were determined experimentally.
const static float kSwitchProAxisXMin = -0.7f;
const static float kSwitchProAxisXMax = 0.7f;
const static float kSwitchProAxisYMin = -0.65f;
const static float kSwitchProAxisYMax = 0.75f;
void MapperXInputStyleGamepad(const Gamepad& input, Gamepad* mapped) {
*mapped = input;
mapped->buttons[BUTTON_INDEX_LEFT_TRIGGER] = AxisToButton(input.axes[2]);
......@@ -440,6 +454,43 @@ void MapperSteelSeries(const Gamepad& input, Gamepad* mapped) {
mapped->axes_length = AXIS_INDEX_COUNT;
}
void MapperSwitchProUsb(const Gamepad& input, Gamepad* mapped) {
*mapped = input;
mapped->axes[AXIS_INDEX_LEFT_STICK_X] = RenormalizeAndClampAxis(
input.axes[0], kSwitchProAxisXMin, kSwitchProAxisXMax);
mapped->axes[AXIS_INDEX_LEFT_STICK_Y] = RenormalizeAndClampAxis(
input.axes[1], kSwitchProAxisYMin, kSwitchProAxisYMax);
mapped->axes[AXIS_INDEX_RIGHT_STICK_X] = RenormalizeAndClampAxis(
input.axes[2], kSwitchProAxisXMin, kSwitchProAxisXMax);
mapped->axes[AXIS_INDEX_RIGHT_STICK_Y] = RenormalizeAndClampAxis(
input.axes[3], kSwitchProAxisYMin, kSwitchProAxisYMax);
mapped->buttons_length = SWITCH_PRO_BUTTON_COUNT;
mapped->axes_length = AXIS_INDEX_COUNT;
}
void MapperSwitchProBluetooth(const Gamepad& input, Gamepad* mapped) {
*mapped = input;
mapped->buttons[BUTTON_INDEX_META] = input.buttons[12];
mapped->buttons[SWITCH_PRO_BUTTON_CAPTURE] = input.buttons[13];
mapped->buttons[BUTTON_INDEX_DPAD_UP] = AxisNegativeAsButton(input.axes[5]);
mapped->buttons[BUTTON_INDEX_DPAD_DOWN] = AxisPositiveAsButton(input.axes[5]);
mapped->buttons[BUTTON_INDEX_DPAD_LEFT] = AxisNegativeAsButton(input.axes[4]);
mapped->buttons[BUTTON_INDEX_DPAD_RIGHT] =
AxisPositiveAsButton(input.axes[4]);
mapped->axes[AXIS_INDEX_LEFT_STICK_X] = RenormalizeAndClampAxis(
input.axes[0], kSwitchProAxisXMin, kSwitchProAxisXMax);
mapped->axes[AXIS_INDEX_LEFT_STICK_Y] = RenormalizeAndClampAxis(
input.axes[1], kSwitchProAxisYMin, kSwitchProAxisYMax);
mapped->axes[AXIS_INDEX_RIGHT_STICK_X] = RenormalizeAndClampAxis(
input.axes[2], kSwitchProAxisXMin, kSwitchProAxisXMax);
mapped->axes[AXIS_INDEX_RIGHT_STICK_Y] = RenormalizeAndClampAxis(
input.axes[3], kSwitchProAxisYMin, kSwitchProAxisYMax);
mapped->buttons_length = SWITCH_PRO_BUTTON_COUNT;
mapped->axes_length = AXIS_INDEX_COUNT;
}
struct MappingData {
const char* const vendor_id;
const char* const product_id;
......@@ -466,6 +517,7 @@ struct MappingData {
{"054c", "05c4", MapperDualshock4}, // Playstation Dualshock 4
{"054c", "09cc", MapperDualshock4}, // Dualshock 4 (PS4 Slim)
{"054c", "0ba0", MapperDualshock4}, // Dualshock 4 USB receiver
{"057e", "2009", MapperSwitchProUsb}, // Switch Pro Controller
{"0583", "2060", MapperIBuffalo}, // iBuffalo Classic
{"0925", "0005", MapperLakeviewResearch}, // SmartJoy PLUS Adapter
{"0925", "8866", MapperLakeviewResearch}, // WiseGroup MP-8866
......@@ -507,6 +559,13 @@ GamepadStandardMappingFunction GetGamepadStandardMappingFunction(
mapper = MapperDualshock3SixAxisNew;
}
// The Nintendo Switch Pro controller exposes the same product ID when
// connected over USB or Bluetooth but communicates using different protocols.
// In Bluetooth mode it uses standard HID, but in USB mode it uses a
// vendor-specific protocol. Select a mapper depending on the connection type.
if (mapper == MapperSwitchProUsb && bus_type == GAMEPAD_BUS_BLUETOOTH)
mapper = MapperSwitchProBluetooth;
return mapper;
}
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "device/gamepad/switch_pro_controller_linux.h"
#include "base/posix/eintr_wrapper.h"
#include "device/gamepad/gamepad_standard_mappings.h"
namespace device {
SwitchProControllerLinux::SwitchProControllerLinux(int fd) : fd_(fd) {}
SwitchProControllerLinux::~SwitchProControllerLinux() = default;
void SwitchProControllerLinux::DoShutdown() {
if (force_usb_hid_)
SendForceUsbHid(false);
force_usb_hid_ = false;
}
void SwitchProControllerLinux::ReadUsbPadState(Gamepad* pad) {
DCHECK_GE(fd_, 0);
uint8_t report_bytes[kReportSize];
ssize_t len = 0;
while ((len = HANDLE_EINTR(read(fd_, report_bytes, kReportSize))) > 0) {
uint8_t packet_type = report_bytes[0];
switch (packet_type) {
case kPacketTypeStatus:
if (len >= 2) {
uint8_t status_type = report_bytes[1];
switch (status_type) {
case kStatusTypeSerial:
if (!sent_handshake_) {
sent_handshake_ = true;
SendHandshake();
}
break;
case kStatusTypeInit:
SendForceUsbHid(true);
force_usb_hid_ = true;
break;
}
}
break;
case kPacketTypeControllerData: {
ControllerDataReport* report =
reinterpret_cast<ControllerDataReport*>(report_bytes);
UpdatePadStateFromControllerData(*report, pad);
pad->timestamp = ++report_id_;
break;
}
default:
break;
}
}
}
void SwitchProControllerLinux::WriteOutputReport(void* report,
size_t report_length) {
write(fd_, report, report_length);
}
} // namespace device
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef DEVICE_GAMEPAD_SWITCH_PRO_CONTROLLER_LINUX_
#define DEVICE_GAMEPAD_SWITCH_PRO_CONTROLLER_LINUX_
#include <memory>
#include "device/gamepad/switch_pro_controller_base.h"
namespace device {
class SwitchProControllerLinux : public SwitchProControllerBase {
public:
SwitchProControllerLinux(int fd);
~SwitchProControllerLinux() override;
void ReadUsbPadState(Gamepad* pad);
void DoShutdown() override;
void WriteOutputReport(void* report, size_t report_length) override;
private:
int fd_ = -1;
int report_id_ = 0;
bool force_usb_hid_ = false;
bool sent_handshake_ = false;
};
} // namespace device
#endif // DEVICE_GAMEPAD_SWITCH_PRO_CONTROLLER_LINUX_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment