0

Add support for Switch Pro Controller on Linux

The Nintendo Switch Pro Controller supports Bluetooth and USB modes,
but defaults to Bluetooth and requires a vendor-specific initialization
sequence to read controller inputs over USB.

This CL adds the logic to initialize the Pro controller for either
Bluetooth or USB depending on how it is connected. It also adds mappers
for both modes, and renormalizes axis values so they are centered and
cover the full analog range.

BUG=801717

Change-Id: I3ca031dcffc6a7e6d5cba2e378fe293de78b5309
Reviewed-on: https://chromium-review.googlesource.com/908048
Reviewed-by: Brandon Jones <bajones@chromium.org>
Commit-Queue: Matt Reynolds <mattreynolds@chromium.org>
Cr-Commit-Position: refs/heads/master@{#535863}
This commit is contained in:
Matt Reynolds
2018-02-09 22:47:48 +00:00
committed by Commit Bot
parent 2369af2eca
commit bb1fbd0aed
12 changed files with 508 additions and 172 deletions

@ -70,6 +70,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;
}

@ -4,13 +4,60 @@
#include "device/gamepad/switch_pro_controller_base.h"
#include <limits>
#include "device/gamepad/gamepad_standard_mappings.h"
namespace {
const uint32_t kVendorNintendo = 0x057e;
const uint32_t kProductSwitchProController = 0x2009;
const uint8_t kRumbleMagnitudeMax = 0xff;
// Switch Pro Controller USB packet types.
static const uint8_t kPacketTypeStatus = 0x81;
static const uint8_t kPacketTypeControllerData = 0x30;
// Status packet subtypes.
static const uint8_t kStatusTypeSerial = 0x01;
static const uint8_t kStatusTypeInit = 0x02;
// Axis extents, used for normalization.
static const int8_t kAxisMin = std::numeric_limits<int8_t>::min();
static const int8_t kAxisMax = std::numeric_limits<int8_t>::max();
enum ControllerType { UNKNOWN_CONTROLLER, SWITCH_PRO_CONTROLLER };
#pragma pack(push, 1)
struct ControllerDataReport {
uint8_t type; // must be kPacketTypeControllerData
uint8_t timestamp;
uint8_t dummy1;
bool button_y : 1;
bool button_x : 1;
bool button_b : 1;
bool button_a : 1;
bool dummy2 : 2;
bool button_r : 1;
bool button_zr : 1;
bool button_minus : 1;
bool button_plus : 1;
bool button_thumb_r : 1;
bool button_thumb_l : 1;
bool button_home : 1;
bool button_capture : 1;
bool dummy3 : 2;
bool dpad_down : 1;
bool dpad_up : 1;
bool dpad_right : 1;
bool dpad_left : 1;
bool dummy4 : 2;
bool button_l : 1;
bool button_zl : 1;
uint8_t analog[6];
};
#pragma pack(pop)
ControllerType ControllerTypeFromDeviceIds(int vendor_id, int product_id) {
if (vendor_id == kVendorNintendo) {
switch (product_id) {
@ -27,6 +74,102 @@ double NormalizeAxis(int value, int min, int max) {
return (2.0 * (value - min) / static_cast<double>(max - min)) - 1.0;
}
void UpdatePadStateFromControllerData(const ControllerDataReport& report,
device::Gamepad* pad) {
pad->buttons[device::BUTTON_INDEX_PRIMARY].pressed = report.button_b;
pad->buttons[device::BUTTON_INDEX_PRIMARY].value =
report.button_b ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_SECONDARY].pressed = report.button_a;
pad->buttons[device::BUTTON_INDEX_SECONDARY].value =
report.button_a ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_TERTIARY].pressed = report.button_y;
pad->buttons[device::BUTTON_INDEX_TERTIARY].value =
report.button_y ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_QUATERNARY].pressed = report.button_x;
pad->buttons[device::BUTTON_INDEX_QUATERNARY].value =
report.button_x ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_LEFT_SHOULDER].pressed = report.button_l;
pad->buttons[device::BUTTON_INDEX_LEFT_SHOULDER].value =
report.button_l ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_RIGHT_SHOULDER].pressed = report.button_r;
pad->buttons[device::BUTTON_INDEX_RIGHT_SHOULDER].value =
report.button_r ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_LEFT_TRIGGER].pressed = report.button_zl;
pad->buttons[device::BUTTON_INDEX_LEFT_TRIGGER].value =
report.button_zl ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_RIGHT_TRIGGER].pressed = report.button_zr;
pad->buttons[device::BUTTON_INDEX_RIGHT_TRIGGER].value =
report.button_zr ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_BACK_SELECT].pressed = report.button_minus;
pad->buttons[device::BUTTON_INDEX_BACK_SELECT].value =
report.button_minus ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_START].pressed = report.button_plus;
pad->buttons[device::BUTTON_INDEX_START].value =
report.button_plus ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_LEFT_THUMBSTICK].pressed =
report.button_thumb_l;
pad->buttons[device::BUTTON_INDEX_LEFT_THUMBSTICK].value =
report.button_thumb_l ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_RIGHT_THUMBSTICK].pressed =
report.button_thumb_r;
pad->buttons[device::BUTTON_INDEX_RIGHT_THUMBSTICK].value =
report.button_thumb_r ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_DPAD_UP].pressed = report.dpad_up;
pad->buttons[device::BUTTON_INDEX_DPAD_UP].value = report.dpad_up ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_DPAD_DOWN].pressed = report.dpad_down;
pad->buttons[device::BUTTON_INDEX_DPAD_DOWN].value =
report.dpad_down ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_DPAD_LEFT].pressed = report.dpad_left;
pad->buttons[device::BUTTON_INDEX_DPAD_LEFT].value =
report.dpad_left ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_DPAD_RIGHT].pressed = report.dpad_right;
pad->buttons[device::BUTTON_INDEX_DPAD_RIGHT].value =
report.dpad_right ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_META].pressed = report.button_home;
pad->buttons[device::BUTTON_INDEX_META].value =
report.button_home ? 1.0 : 0.0;
pad->buttons[device::BUTTON_INDEX_META + 1].pressed = report.button_capture;
pad->buttons[device::BUTTON_INDEX_META + 1].value =
report.button_capture ? 1.0 : 0.0;
int8_t axis_lx =
(((report.analog[1] & 0x0F) << 4) | ((report.analog[0] & 0xF0) >> 4)) +
127;
int8_t axis_ly = report.analog[2] + 127;
int8_t axis_rx =
(((report.analog[4] & 0x0F) << 4) | ((report.analog[3] & 0xF0) >> 4)) +
127;
int8_t axis_ry = report.analog[5] + 127;
pad->axes[device::AXIS_INDEX_LEFT_STICK_X] =
NormalizeAxis(axis_lx, kAxisMin, kAxisMax);
pad->axes[device::AXIS_INDEX_LEFT_STICK_Y] =
NormalizeAxis(-axis_ly, kAxisMin, kAxisMax);
pad->axes[device::AXIS_INDEX_RIGHT_STICK_X] =
NormalizeAxis(axis_rx, kAxisMin, kAxisMax);
pad->axes[device::AXIS_INDEX_RIGHT_STICK_Y] =
NormalizeAxis(-axis_ry, kAxisMin, kAxisMax);
pad->buttons_length = device::BUTTON_INDEX_COUNT + 1;
pad->axes_length = device::AXIS_INDEX_COUNT;
}
} // namespace
namespace device {
@ -39,98 +182,75 @@ bool SwitchProControllerBase::IsSwitchPro(int vendor_id, int product_id) {
UNKNOWN_CONTROLLER;
}
// static
void SwitchProControllerBase::UpdatePadStateFromControllerData(
const ControllerDataReport& report,
Gamepad* pad) {
pad->buttons[BUTTON_INDEX_PRIMARY].pressed = report.button_b;
pad->buttons[BUTTON_INDEX_PRIMARY].value = report.button_b ? 1.0 : 0.0;
void SwitchProControllerBase::DoShutdown() {
if (force_usb_hid_)
SendForceUsbHid(false);
force_usb_hid_ = false;
}
pad->buttons[BUTTON_INDEX_SECONDARY].pressed = report.button_a;
pad->buttons[BUTTON_INDEX_SECONDARY].value = report.button_a ? 1.0 : 0.0;
void SwitchProControllerBase::ReadUsbPadState(Gamepad* pad) {
DCHECK(pad);
pad->buttons[BUTTON_INDEX_TERTIARY].pressed = report.button_y;
pad->buttons[BUTTON_INDEX_TERTIARY].value = report.button_y ? 1.0 : 0.0;
// Consume reports until the input pipe is empty.
uint8_t report_bytes[kReportSize];
while (true) {
size_t report_length = ReadInputReport(report_bytes);
if (report_length == 0)
break;
HandleInputReport(report_bytes, report_length, pad);
}
}
pad->buttons[BUTTON_INDEX_QUATERNARY].pressed = report.button_x;
pad->buttons[BUTTON_INDEX_QUATERNARY].value = report.button_x ? 1.0 : 0.0;
void SwitchProControllerBase::HandleInputReport(void* report,
size_t report_length,
Gamepad* pad) {
DCHECK(report);
DCHECK_GE(report_length, 1U);
DCHECK(pad);
pad->buttons[BUTTON_INDEX_LEFT_SHOULDER].pressed = report.button_l;
pad->buttons[BUTTON_INDEX_LEFT_SHOULDER].value = report.button_l ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_RIGHT_SHOULDER].pressed = report.button_r;
pad->buttons[BUTTON_INDEX_RIGHT_SHOULDER].value = report.button_r ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_LEFT_TRIGGER].pressed = report.button_zl;
pad->buttons[BUTTON_INDEX_LEFT_TRIGGER].value = report.button_zl ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_RIGHT_TRIGGER].pressed = report.button_zr;
pad->buttons[BUTTON_INDEX_RIGHT_TRIGGER].value = report.button_zr ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_BACK_SELECT].pressed = report.button_minus;
pad->buttons[BUTTON_INDEX_BACK_SELECT].value =
report.button_minus ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_START].pressed = report.button_plus;
pad->buttons[BUTTON_INDEX_START].value = report.button_plus ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_LEFT_THUMBSTICK].pressed = report.button_thumb_l;
pad->buttons[BUTTON_INDEX_LEFT_THUMBSTICK].value =
report.button_thumb_l ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_RIGHT_THUMBSTICK].pressed = report.button_thumb_r;
pad->buttons[BUTTON_INDEX_RIGHT_THUMBSTICK].value =
report.button_thumb_r ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_DPAD_UP].pressed = report.dpad_up;
pad->buttons[BUTTON_INDEX_DPAD_UP].value = report.dpad_up ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_DPAD_DOWN].pressed = report.dpad_down;
pad->buttons[BUTTON_INDEX_DPAD_DOWN].value = report.dpad_down ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_DPAD_LEFT].pressed = report.dpad_left;
pad->buttons[BUTTON_INDEX_DPAD_LEFT].value = report.dpad_left ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_DPAD_RIGHT].pressed = report.dpad_right;
pad->buttons[BUTTON_INDEX_DPAD_RIGHT].value = report.dpad_right ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_META].pressed = report.button_home;
pad->buttons[BUTTON_INDEX_META].value = report.button_home ? 1.0 : 0.0;
pad->buttons[BUTTON_INDEX_META + 1].pressed = report.button_capture;
pad->buttons[BUTTON_INDEX_META + 1].value = report.button_capture ? 1.0 : 0.0;
int8_t axis_lx =
(((report.analog[1] & 0x0F) << 4) | ((report.analog[0] & 0xF0) >> 4)) +
127;
int8_t axis_ly = report.analog[2] + 127;
int8_t axis_rx =
(((report.analog[4] & 0x0F) << 4) | ((report.analog[3] & 0xF0) >> 4)) +
127;
int8_t axis_ry = report.analog[5] + 127;
pad->axes[AXIS_INDEX_LEFT_STICK_X] =
NormalizeAxis(axis_lx, kAxisMin, kAxisMax);
pad->axes[AXIS_INDEX_LEFT_STICK_Y] =
NormalizeAxis(-axis_ly, kAxisMin, kAxisMax);
pad->axes[AXIS_INDEX_RIGHT_STICK_X] =
NormalizeAxis(axis_rx, kAxisMin, kAxisMax);
pad->axes[AXIS_INDEX_RIGHT_STICK_Y] =
NormalizeAxis(-axis_ry, kAxisMin, kAxisMax);
pad->buttons_length = BUTTON_INDEX_COUNT + 1;
pad->axes_length = AXIS_INDEX_COUNT;
const uint8_t* report_bytes = static_cast<uint8_t*>(report);
const uint8_t type = report_bytes[0];
switch (type) {
case kPacketTypeStatus:
if (report_length >= 2) {
const uint8_t status_type = report_bytes[1];
switch (status_type) {
case kStatusTypeSerial:
if (!sent_handshake_) {
sent_handshake_ = true;
SendHandshake();
}
break;
case kStatusTypeInit:
force_usb_hid_ = true;
SendForceUsbHid(true);
break;
default:
break;
}
}
break;
case kPacketTypeControllerData: {
ControllerDataReport* controller_data =
reinterpret_cast<ControllerDataReport*>(report);
UpdatePadStateFromControllerData(*controller_data, pad);
pad->timestamp = ++report_id_;
break;
}
default:
break;
}
}
void SwitchProControllerBase::SendConnectionStatusQuery() {
// Requests the current connection status and info about the connected
// controller. The controller will respond with a status packet.
const size_t report_length = 2;
uint8_t report[report_length];
memset(report, 0, report_length);
uint8_t report[kReportSize];
memset(report, 0, kReportSize);
report[0] = 0x80;
report[1] = 0x01;
WriteOutputReport(report, report_length);
WriteOutputReport(report, kReportSize);
}
void SwitchProControllerBase::SendHandshake() {
@ -186,4 +306,13 @@ void SwitchProControllerBase::SetVibration(double strong_magnitude,
WriteOutputReport(report, kReportSize);
}
size_t SwitchProControllerBase::ReadInputReport(void* report) {
return 0;
}
size_t SwitchProControllerBase::WriteOutputReport(void* report,
size_t report_length) {
return 0;
}
} // namespace device

@ -6,75 +6,38 @@
#define DEVICE_GAMEPAD_SWITCH_PRO_CONTROLLER_BASE_
#include "device/gamepad/abstract_haptic_gamepad.h"
#include "device/gamepad/gamepad_standard_mappings.h"
#include "device/gamepad/public/cpp/gamepad.h"
namespace device {
class SwitchProControllerBase : public AbstractHapticGamepad {
public:
// Switch Pro Controller USB packet types.
static const uint8_t kPacketTypeStatus = 0x81;
static const uint8_t kPacketTypeControllerData = 0x30;
// Status packet subtypes.
static const uint8_t kStatusTypeSerial = 0x01;
static const uint8_t kStatusTypeInit = 0x02;
// Axis extents, used for normalization.
static const int kAxisMin = -128;
static const int kAxisMax = 127;
// Maximum size of a Switch HID report, in bytes.
static const int kReportSize = 64;
#pragma pack(push, 1)
struct ControllerDataReport {
uint8_t type; // must be kPacketTypeControllerData
uint8_t timestamp;
uint8_t dummy1;
bool button_y : 1;
bool button_x : 1;
bool button_b : 1;
bool button_a : 1;
bool dummy2 : 2;
bool button_r : 1;
bool button_zr : 1;
bool button_minus : 1;
bool button_plus : 1;
bool button_thumb_r : 1;
bool button_thumb_l : 1;
bool button_home : 1;
bool button_capture : 1;
bool dummy3 : 2;
bool dpad_down : 1;
bool dpad_up : 1;
bool dpad_right : 1;
bool dpad_left : 1;
bool dummy4 : 2;
bool button_l : 1;
bool button_zl : 1;
uint8_t analog[6];
};
#pragma pack(pop)
SwitchProControllerBase() = default;
~SwitchProControllerBase() override;
static bool IsSwitchPro(int vendor_id, int product_id);
static void UpdatePadStateFromControllerData(
const ControllerDataReport& report,
Gamepad* pad);
void DoShutdown() override;
void ReadUsbPadState(Gamepad* pad);
void HandleInputReport(void* report, size_t report_length, Gamepad* pad);
void SendConnectionStatusQuery();
void SendHandshake();
void SendForceUsbHid(bool enable);
void SetVibration(double strong_magnitude, double weak_magnitude) override;
virtual void WriteOutputReport(void* report, size_t report_length) {}
virtual size_t ReadInputReport(void* report);
virtual size_t WriteOutputReport(void* report, size_t report_length);
private:
uint32_t counter_ = 0;
uint32_t report_id_ = 0;
bool force_usb_hid_ = false;
bool sent_handshake_ = false;
};
} // namespace device

@ -0,0 +1,30 @@
// 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;
size_t SwitchProControllerLinux::ReadInputReport(void* report) {
DCHECK(report);
ssize_t bytes_read = HANDLE_EINTR(read(fd_, report, kReportSize));
return bytes_read < 0 ? 0 : static_cast<size_t>(bytes_read);
}
size_t SwitchProControllerLinux::WriteOutputReport(void* report,
size_t report_length) {
DCHECK(report);
DCHECK_GE(report_length, 1U);
ssize_t bytes_written = HANDLE_EINTR(write(fd_, report, report_length));
return bytes_written < 0 ? 0 : static_cast<size_t>(bytes_written);
}
} // namespace device

@ -0,0 +1,28 @@
// 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;
size_t ReadInputReport(void* report) override;
size_t WriteOutputReport(void* report, size_t report_length) override;
private:
int fd_ = -1;
};
} // namespace device
#endif // DEVICE_GAMEPAD_SWITCH_PRO_CONTROLLER_LINUX_