blob: 8ed98ee729624ed5160baab4173d721672429d66 [file] [log] [blame]
// Copyright 2019 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/nintendo_controller.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/numerics/ranges.h"
#include "device/gamepad/gamepad_data_fetcher.h"
#include "device/gamepad/gamepad_id_list.h"
namespace device {
namespace {
// Device IDs for the Switch Charging Grip, also used for composite devices.
const uint16_t kVendorNintendo = 0x057e;
const uint16_t kProductSwitchChargingGrip = 0x200e;
// Maximum output report sizes, used to distinguish USB and Bluetooth.
const size_t kSwitchProMaxOutputReportSizeBytesUsb = 63;
const size_t kSwitchProMaxOutputReportSizeBytesBluetooth = 48;
// Input report size.
const size_t kMaxInputReportSizeBytes = 64;
// Device name for a composite Joy-Con device.
const char kProductNameSwitchCompositeDevice[] = "Joy-Con L+R";
// Report IDs.
const uint8_t kReportIdOutput01 = 0x01;
const uint8_t kReportIdOutput10 = 0x10;
const uint8_t kReportIdInput21 = 0x21;
const uint8_t kReportIdInput30 = 0x30;
const uint8_t kUsbReportIdOutput80 = 0x80;
const uint8_t kUsbReportIdInput81 = 0x81;
// Sub-types of the 0x80 output report, used for initialization.
const uint8_t kSubTypeRequestMac = 0x01;
const uint8_t kSubTypeHandshake = 0x02;
const uint8_t kSubTypeBaudRate = 0x03;
const uint8_t kSubTypeDisableUsbTimeout = 0x04;
const uint8_t kSubTypeEnableUsbTimeout = 0x05;
// UART subcommands.
const uint8_t kSubCommandSetInputReportMode = 0x03;
const uint8_t kSubCommandReadSpi = 0x10;
const uint8_t kSubCommandSetPlayerLights = 0x30;
const uint8_t kSubCommand33 = 0x33;
const uint8_t kSubCommandSetHomeLight = 0x38;
const uint8_t kSubCommandEnableImu = 0x40;
const uint8_t kSubCommandSetImuSensitivity = 0x41;
const uint8_t kSubCommandEnableVibration = 0x48;
// SPI memory regions.
const uint16_t kSpiImuCalibrationAddress = 0x6020;
const size_t kSpiImuCalibrationSize = 24;
const uint16_t kSpiAnalogStickCalibrationAddress = 0x603d;
const size_t kSpiAnalogStickCalibrationSize = 18;
const uint16_t kSpiImuHorizontalOffsetsAddress = 0x6080;
const size_t kSpiImuHorizontalOffsetsSize = 6;
const uint16_t kSpiAnalogStickParametersAddress = 0x6086;
const size_t kSpiAnalogStickParametersSize = 18;
// Byte index for the first byte of subcommand data in 0x80 output reports.
const size_t kSubCommandDataOffset = 11;
// Byte index for the first byte of SPI data in SPI read responses.
const size_t kSpiDataOffset = 20;
// Values for the |device_type| field reported in the MAC reply.
const uint8_t kUsbDeviceTypeChargingGripNoDevice = 0x00;
const uint8_t kUsbDeviceTypeChargingGripJoyConL = 0x01;
const uint8_t kUsbDeviceTypeChargingGripJoyConR = 0x02;
const uint8_t kUsbDeviceTypeProController = 0x03;
// During initialization, the current initialization step will be retried if
// the client does not respond within |kTimeoutDuration|. After |kMaxRetryCount|
// retries, initialization is restarted from the first step.
//
// The timeout duration was chosen through experimentation. A shorter duration
// (~1 second) works for Pro controllers, but Joy-Cons sometimes fail to
// initialize correctly.
const base::TimeDelta kTimeoutDuration =
base::TimeDelta::FromMilliseconds(3000);
const size_t kMaxRetryCount = 3;
const size_t kMaxVibrationEffectDurationMillis = 100;
// Initialization parameters.
const uint8_t kGyroSensitivity2000Dps = 0x03;
const uint8_t kAccelerometerSensitivity8G = 0x00;
const uint8_t kGyroPerformance208Hz = 0x01;
const uint8_t kAccelerometerFilterBandwidth100Hz = 0x01;
const uint8_t kPlayerLightPattern1 = 0x01;
// Bogus calibration value that should be ignored.
const uint16_t kCalBogusValue = 0xfff;
// Default calibration values to use if the controller returns bogus values.
const uint16_t kCalDefaultDeadzone = 160;
const uint16_t kCalDefaultMin = 550;
const uint16_t kCalDefaultCenter = 2050;
const uint16_t kCalDefaultMax = 3550;
// Parameters for the "strong" and "weak" components of the dual-rumble effect.
const double kVibrationFrequencyStrongRumble = 141.0;
const double kVibrationFrequencyWeakRumble = 182.0;
const double kVibrationAmplitudeStrongRumbleMax = 0.9;
const double kVibrationAmplitudeWeakRumbleMax = 0.1;
const int kVibrationFrequencyHzMin = 41;
const int kVibrationFrequencyHzMax = 1253;
const int kVibrationAmplitudeMax = 1000;
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/rumble_data_table.md
struct VibrationFrequency {
uint16_t hf;
uint8_t lf;
int freq_hz; // rounded
} kVibrationFrequency[] = {
// The linear resonant actuators (LRAs) on Switch devices are capable of
// producing vibration effects at a wide range of frequencies, but the
// Gamepad API assumes "dual-rumble" style vibration which is typically
// implemented by a pair of eccentric rotating mass (ERM) actuators. To
// simulate "dual-rumble" with Switch LRAs, the strong and weak vibration
// magnitudes are translated into low and high frequency vibration effects.
// Only the frequencies used for this translation are included; unused
// frequencies have been removed.
//
// This list must be kept sorted.
{0x0068, 0x3a, 141},
{0x0098, 0x46, 182}};
const size_t kVibrationFrequencySize = base::size(kVibrationFrequency);
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/rumble_data_table.md
struct VibrationAmplitude {
uint8_t hfa;
uint16_t lfa;
int amp; // rounded, max 1000 (kVibrationAmplitudeMax)
} kVibrationAmplitude[]{
// Only include safe amplitudes.
{0x00, 0x0040, 0}, {0x02, 0x8040, 10}, {0x04, 0x0041, 12},
{0x06, 0x8041, 14}, {0x08, 0x0042, 17}, {0x0a, 0x8042, 20},
{0x0c, 0x0043, 24}, {0x0e, 0x8043, 28}, {0x10, 0x0044, 33},
{0x12, 0x8044, 40}, {0x14, 0x0045, 47}, {0x16, 0x8045, 56},
{0x18, 0x0046, 67}, {0x1a, 0x8046, 80}, {0x1c, 0x0047, 95},
{0x1e, 0x8047, 112}, {0x20, 0x0048, 117}, {0x22, 0x8048, 123},
{0x24, 0x0049, 128}, {0x26, 0x8049, 134}, {0x28, 0x004a, 140},
{0x2a, 0x804a, 146}, {0x2c, 0x004b, 152}, {0x2e, 0x804b, 159},
{0x30, 0x004c, 166}, {0x32, 0x804c, 173}, {0x34, 0x004d, 181},
{0x36, 0x804d, 189}, {0x38, 0x004e, 198}, {0x3a, 0x804e, 206},
{0x3c, 0x004f, 215}, {0x3e, 0x804f, 225}, {0x40, 0x0050, 230},
{0x42, 0x8050, 235}, {0x44, 0x0051, 240}, {0x46, 0x8051, 245},
{0x48, 0x0052, 251}, {0x4a, 0x8052, 256}, {0x4c, 0x0053, 262},
{0x4e, 0x8053, 268}, {0x50, 0x0054, 273}, {0x52, 0x8054, 279},
{0x54, 0x0055, 286}, {0x56, 0x8055, 292}, {0x58, 0x0056, 298},
{0x5a, 0x8056, 305}, {0x5c, 0x0057, 311}, {0x5e, 0x8057, 318},
{0x60, 0x0058, 325}, {0x62, 0x8058, 332}, {0x64, 0x0059, 340},
{0x66, 0x8059, 347}, {0x68, 0x005a, 355}, {0x6a, 0x805a, 362},
{0x6c, 0x005b, 370}, {0x6e, 0x805b, 378}, {0x70, 0x005c, 387},
{0x72, 0x805c, 395}, {0x74, 0x005d, 404}, {0x76, 0x805d, 413},
{0x78, 0x005e, 422}, {0x7a, 0x805e, 431}, {0x7c, 0x005f, 440},
{0x7e, 0x805f, 450}, {0x80, 0x0060, 460}, {0x82, 0x8060, 470},
{0x84, 0x0061, 480}, {0x86, 0x8061, 491}, {0x88, 0x0062, 501},
{0x8a, 0x8062, 512}, {0x8c, 0x0063, 524}, {0x8e, 0x8063, 535},
{0x90, 0x0064, 547}, {0x92, 0x8064, 559}, {0x94, 0x0065, 571},
{0x96, 0x8065, 584}, {0x98, 0x0066, 596}, {0x9a, 0x8066, 609},
{0x9c, 0x0067, 623}, {0x9e, 0x8067, 636}, {0xa0, 0x0068, 650},
{0xa2, 0x8068, 665}, {0xa4, 0x0069, 679}, {0xa6, 0x8069, 694},
{0xa8, 0x006a, 709}, {0xaa, 0x806a, 725}, {0xac, 0x006b, 741},
{0xae, 0x806b, 757}, {0xb0, 0x006c, 773}, {0xb2, 0x806c, 790},
{0xb4, 0x006d, 808}, {0xb6, 0x806d, 825}, {0xb8, 0x006e, 843},
{0xba, 0x806e, 862}, {0xbc, 0x006f, 881}, {0xbe, 0x806f, 900},
{0xc0, 0x0070, 920}, {0xc2, 0x8070, 940}, {0xc4, 0x0071, 960},
{0xc6, 0x8071, 981}, {0xc8, 0x0072, 1000},
};
const size_t kVibrationAmplitudeSize = base::size(kVibrationAmplitude);
// Define indices for the additional buttons on Switch controllers.
enum SWITCH_BUTTON_INDICES {
SWITCH_BUTTON_INDEX_CAPTURE = BUTTON_INDEX_META + 1,
SWITCH_BUTTON_INDEX_LEFT_SL,
SWITCH_BUTTON_INDEX_LEFT_SR,
SWITCH_BUTTON_INDEX_RIGHT_SL,
SWITCH_BUTTON_INDEX_RIGHT_SR,
SWITCH_BUTTON_INDEX_COUNT
};
// Input reports with ID 0x81 are replies to commands sent during the
// initialization sequence.
#pragma pack(push, 1)
struct UsbInputReport81 {
uint8_t subtype;
uint8_t data[kMaxInputReportSizeBytes - 2];
};
#pragma pack(pop)
static_assert(sizeof(UsbInputReport81) == kMaxInputReportSizeBytes - 1,
"UsbInputReport81 has incorrect size");
// When connected over USB, the initialization sequence includes a step to
// request the MAC address. The MAC is returned in an input report with ID 0x81
// and subtype 0x01.
#pragma pack(push, 1)
struct MacAddressReport {
uint8_t subtype; // 0x01
uint8_t padding;
uint8_t device_type;
uint8_t mac_data[6];
uint8_t padding2[kMaxInputReportSizeBytes - 10];
};
#pragma pack(pop)
static_assert(sizeof(MacAddressReport) == kMaxInputReportSizeBytes - 1,
"MacAddressReport has incorrect size");
// When configured for standard full input report mode, controller data is
// reported at regular intervals. The data format is the same for all Switch
// devices, although some buttons are not present on all devices.
#pragma pack(push, 1)
struct ControllerData {
uint8_t timestamp;
uint8_t battery_level : 4;
uint8_t connection_info : 4;
bool button_y : 1;
bool button_x : 1;
bool button_b : 1;
bool button_a : 1;
bool button_right_sr : 1;
bool button_right_sl : 1;
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;
uint8_t dummy : 1;
bool charging_grip : 1;
bool dpad_down : 1;
bool dpad_up : 1;
bool dpad_right : 1;
bool dpad_left : 1;
bool button_left_sr : 1;
bool button_left_sl : 1;
bool button_l : 1;
bool button_zl : 1;
uint8_t analog[6];
uint8_t vibrator_input_report;
};
#pragma pack(pop)
static_assert(sizeof(ControllerData) == 12,
"ControllerData has incorrect size");
// In standard full input report mode, controller data is reported with IMU data
// in reports with ID 0x30.
#pragma pack(push, 1)
struct ControllerDataReport {
ControllerData controller_data; // 12 bytes
uint8_t imu_data[36];
uint8_t padding[kMaxInputReportSizeBytes - 49];
};
#pragma pack(pop)
static_assert(sizeof(ControllerDataReport) == kMaxInputReportSizeBytes - 1,
"ControllerDataReport has incorrect size");
// Responses to SPI read requests are sent in reports with ID 0x21. These
// reports also include controller data.
#pragma pack(push, 1)
struct SpiReadReport {
ControllerData controller_data; // 12 bytes
uint8_t subcommand_ack; // 0x90
uint8_t subcommand; // 0x10
uint8_t addrl;
uint8_t addrh;
uint8_t padding[2]; // 0x00 0x00
uint8_t length;
uint8_t spi_data[kMaxInputReportSizeBytes - kSpiDataOffset];
};
#pragma pack(pop)
static_assert(sizeof(SpiReadReport) == kMaxInputReportSizeBytes - 1,
"SpiReadReport has incorrect size");
// Unpack two packed 12-bit values.
void UnpackShorts(uint8_t byte0,
uint8_t byte1,
uint8_t byte2,
uint16_t* short1,
uint16_t* short2) {
DCHECK(short1);
DCHECK(short2);
*short1 = ((byte1 << 8) & 0x0f00) | byte0;
*short2 = (byte2 << 4) | (byte1 >> 4);
}
// Unpack a 6-byte MAC address.
uint64_t UnpackSwitchMacAddress(const uint8_t* data) {
DCHECK(data);
uint64_t acc = data[5];
acc = (acc << 8) | data[4];
acc = (acc << 8) | data[3];
acc = (acc << 8) | data[2];
acc = (acc << 8) | data[1];
acc = (acc << 8) | data[0];
return acc;
}
// Unpack the analog stick parameters into |cal|.
void UnpackSwitchAnalogStickParameters(
const uint8_t* data,
NintendoController::SwitchCalibrationData& cal) {
DCHECK(data);
// Only fetch the dead zone and range ratio. The other parameters are unknown.
UnpackShorts(data[3], data[4], data[5], &cal.dead_zone, &cal.range_ratio);
if (cal.dead_zone == kCalBogusValue) {
// If the controller reports an invalid dead zone, default to something
// reasonable.
cal.dead_zone = kCalDefaultDeadzone;
}
}
// Unpack the IMU calibration data into |cal|
void UnpackSwitchImuCalibration(
const uint8_t* data,
NintendoController::SwitchCalibrationData& cal) {
DCHECK(data);
// 24 bytes, as 4 groups of 3 16-bit little-endian values.
cal.accelerometer_origin_x = (data[1] << 8) | data[0];
cal.accelerometer_origin_y = (data[3] << 8) | data[2];
cal.accelerometer_origin_z = (data[5] << 8) | data[4];
cal.accelerometer_sensitivity_x = (data[7] << 8) | data[6];
cal.accelerometer_sensitivity_y = (data[9] << 8) | data[8];
cal.accelerometer_sensitivity_z = (data[11] << 8) | data[10];
cal.gyro_origin_x = (data[13] << 8) | data[12];
cal.gyro_origin_y = (data[15] << 8) | data[14];
cal.gyro_origin_z = (data[17] << 8) | data[16];
cal.gyro_sensitivity_x = (data[19] << 8) | data[18];
cal.gyro_sensitivity_y = (data[21] << 8) | data[20];
cal.gyro_sensitivity_z = (data[23] << 8) | data[22];
}
// Unpack the IMU horizontal offsets into |cal|.
void UnpackSwitchImuHorizontalOffsets(
const uint8_t* data,
NintendoController::SwitchCalibrationData& cal) {
DCHECK(data);
// 6 bytes, as 3 16-bit little-endian values.
cal.horizontal_offset_x = (data[1] << 8) | data[0];
cal.horizontal_offset_y = (data[3] << 8) | data[2];
cal.horizontal_offset_z = (data[5] << 8) | data[4];
}
// Unpack the analog stick calibration data into |cal|.
void UnpackSwitchAnalogStickCalibration(
const uint8_t* data,
NintendoController::SwitchCalibrationData& cal) {
DCHECK(data);
// 18 bytes, as 2 groups of 6 packed 12-bit values.
UnpackShorts(data[0], data[1], data[2], &cal.lx_max, &cal.ly_max);
UnpackShorts(data[3], data[4], data[5], &cal.lx_center, &cal.ly_center);
UnpackShorts(data[6], data[7], data[8], &cal.lx_min, &cal.ly_min);
UnpackShorts(data[9], data[10], data[11], &cal.rx_center, &cal.ry_center);
UnpackShorts(data[12], data[13], data[14], &cal.rx_min, &cal.ry_min);
UnpackShorts(data[15], data[16], data[17], &cal.rx_max, &cal.ry_max);
if (cal.lx_min == kCalBogusValue && cal.ly_max == kCalBogusValue) {
// No valid data for the left stick, use reasonable defaults.
cal.lx_min = kCalDefaultMin;
cal.lx_center = kCalDefaultCenter;
cal.lx_max = kCalDefaultMax;
cal.ly_min = kCalDefaultMin;
cal.ly_center = kCalDefaultCenter;
cal.ly_max = kCalDefaultMax;
} else {
cal.lx_min = cal.lx_center - cal.lx_min;
cal.lx_max = cal.lx_center + cal.lx_max;
cal.ly_min = cal.ly_center - cal.ly_min;
cal.ly_max = cal.ly_center + cal.ly_max;
}
if (cal.rx_min == kCalBogusValue && cal.ry_max == kCalBogusValue) {
// No valid data for the right stick, use reasonable defaults.
cal.rx_min = kCalDefaultMin;
cal.rx_center = kCalDefaultCenter;
cal.rx_max = kCalDefaultMax;
cal.ry_min = kCalDefaultMin;
cal.ry_center = kCalDefaultCenter;
cal.ry_max = kCalDefaultMax;
} else {
cal.rx_min = cal.rx_center - cal.rx_min;
cal.rx_max = cal.rx_center + cal.rx_max;
cal.ry_min = cal.ry_center - cal.ry_min;
cal.ry_max = cal.ry_center + cal.ry_max;
}
}
// Unpack one frame of IMU data into |imu_data|.
void UnpackSwitchImuData(const uint8_t* data,
NintendoController::SwitchImuData* imu_data) {
DCHECK(data);
DCHECK(imu_data);
// 12 bytes of IMU data containing 6 16-bit little-endian values.
imu_data->accelerometer_x = (data[1] << 8) | data[0];
imu_data->accelerometer_y = (data[3] << 8) | data[2];
imu_data->accelerometer_z = (data[5] << 8) | data[4];
imu_data->gyro_x = (data[7] << 8) | data[6];
imu_data->gyro_y = (data[9] << 8) | data[8];
imu_data->gyro_z = (data[11] << 8) | data[10];
}
// Given joystick input |x|,|y|, apply a radial deadzone with radius
// |dead_zone| centered at |x_center|,|y_center|. If the input is within the
// dead zone region, the value is snapped to the center of the dead zone.
bool ApplyDeadZone(uint16_t& x,
uint16_t& y,
uint16_t x_center,
uint16_t y_center,
uint16_t dead_zone) {
int dx = x - x_center;
int dy = y - y_center;
if (dx * dx + dy * dy < dead_zone * dead_zone) {
x = x_center;
y = y_center;
return true;
}
return false;
}
// Normalize |value| to the range [|min|,|max|]. If |value| is outside this
// range, clamp it.
double NormalizeAndClampAxis(int value, int min, int max) {
if (value <= min)
return -1.0;
if (value >= max)
return 1.0;
return (2.0 * (value - min) / static_cast<double>(max - min)) - 1.0;
}
// Update the button and axis state in |pad| with the new controller data in
// |data|, using the calibration data |cal|. Returns true if the new data
// differs from the previous data.
bool UpdateGamepadFromControllerData(
const ControllerData& data,
const NintendoController::SwitchCalibrationData& cal,
Gamepad& pad) {
bool buttons_changed =
pad.buttons_length != SWITCH_BUTTON_INDEX_COUNT ||
pad.buttons[BUTTON_INDEX_PRIMARY].pressed != data.button_b ||
pad.buttons[BUTTON_INDEX_SECONDARY].pressed != data.button_a ||
pad.buttons[BUTTON_INDEX_TERTIARY].pressed != data.button_y ||
pad.buttons[BUTTON_INDEX_QUATERNARY].pressed != data.button_x ||
pad.buttons[BUTTON_INDEX_LEFT_SHOULDER].pressed != data.button_l ||
pad.buttons[BUTTON_INDEX_RIGHT_SHOULDER].pressed != data.button_r ||
pad.buttons[BUTTON_INDEX_LEFT_TRIGGER].pressed != data.button_zl ||
pad.buttons[BUTTON_INDEX_RIGHT_TRIGGER].pressed != data.button_zr ||
pad.buttons[BUTTON_INDEX_BACK_SELECT].pressed != data.button_minus ||
pad.buttons[BUTTON_INDEX_START].pressed != data.button_plus ||
pad.buttons[BUTTON_INDEX_LEFT_THUMBSTICK].pressed !=
data.button_thumb_l ||
pad.buttons[BUTTON_INDEX_RIGHT_THUMBSTICK].pressed !=
data.button_thumb_r ||
pad.buttons[BUTTON_INDEX_DPAD_UP].pressed != data.dpad_up ||
pad.buttons[BUTTON_INDEX_DPAD_DOWN].pressed != data.dpad_down ||
pad.buttons[BUTTON_INDEX_DPAD_LEFT].pressed != data.dpad_left ||
pad.buttons[BUTTON_INDEX_DPAD_RIGHT].pressed != data.dpad_right ||
pad.buttons[BUTTON_INDEX_META].pressed != data.button_home ||
pad.buttons[SWITCH_BUTTON_INDEX_CAPTURE].pressed != data.button_capture ||
pad.buttons[SWITCH_BUTTON_INDEX_LEFT_SL].pressed != data.button_left_sl ||
pad.buttons[SWITCH_BUTTON_INDEX_LEFT_SR].pressed != data.button_left_sr ||
pad.buttons[SWITCH_BUTTON_INDEX_RIGHT_SL].pressed !=
data.button_right_sl ||
pad.buttons[SWITCH_BUTTON_INDEX_RIGHT_SR].pressed != data.button_right_sr;
if (buttons_changed) {
pad.buttons_length = SWITCH_BUTTON_INDEX_COUNT;
pad.buttons[BUTTON_INDEX_PRIMARY].pressed = data.button_b;
pad.buttons[BUTTON_INDEX_PRIMARY].value = data.button_b ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_SECONDARY].pressed = data.button_a;
pad.buttons[BUTTON_INDEX_SECONDARY].value = data.button_a ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_TERTIARY].pressed = data.button_y;
pad.buttons[BUTTON_INDEX_TERTIARY].value = data.button_y ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_QUATERNARY].pressed = data.button_x;
pad.buttons[BUTTON_INDEX_QUATERNARY].value = data.button_x ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_LEFT_SHOULDER].pressed = data.button_l;
pad.buttons[BUTTON_INDEX_LEFT_SHOULDER].value = data.button_l ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_RIGHT_SHOULDER].pressed = data.button_r;
pad.buttons[BUTTON_INDEX_RIGHT_SHOULDER].value = data.button_r ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_LEFT_TRIGGER].pressed = data.button_zl;
pad.buttons[BUTTON_INDEX_LEFT_TRIGGER].value = data.button_zl ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_RIGHT_TRIGGER].pressed = data.button_zr;
pad.buttons[BUTTON_INDEX_RIGHT_TRIGGER].value = data.button_zr ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_BACK_SELECT].pressed = data.button_minus;
pad.buttons[BUTTON_INDEX_BACK_SELECT].value = data.button_minus ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_START].pressed = data.button_plus;
pad.buttons[BUTTON_INDEX_START].value = data.button_plus ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_LEFT_THUMBSTICK].pressed = data.button_thumb_l;
pad.buttons[BUTTON_INDEX_LEFT_THUMBSTICK].value =
data.button_thumb_l ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_RIGHT_THUMBSTICK].pressed = data.button_thumb_r;
pad.buttons[BUTTON_INDEX_RIGHT_THUMBSTICK].value =
data.button_thumb_r ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_DPAD_UP].pressed = data.dpad_up;
pad.buttons[BUTTON_INDEX_DPAD_UP].value = data.dpad_up ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_DPAD_DOWN].pressed = data.dpad_down;
pad.buttons[BUTTON_INDEX_DPAD_DOWN].value = data.dpad_down ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_DPAD_LEFT].pressed = data.dpad_left;
pad.buttons[BUTTON_INDEX_DPAD_LEFT].value = data.dpad_left ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_DPAD_RIGHT].pressed = data.dpad_right;
pad.buttons[BUTTON_INDEX_DPAD_RIGHT].value = data.dpad_right ? 1.0 : 0.0;
pad.buttons[BUTTON_INDEX_META].pressed = data.button_home;
pad.buttons[BUTTON_INDEX_META].value = data.button_home ? 1.0 : 0.0;
pad.buttons[SWITCH_BUTTON_INDEX_CAPTURE].pressed = data.button_capture;
pad.buttons[SWITCH_BUTTON_INDEX_CAPTURE].value =
data.button_capture ? 1.0 : 0.0;
pad.buttons[SWITCH_BUTTON_INDEX_LEFT_SL].pressed = data.button_left_sl;
pad.buttons[SWITCH_BUTTON_INDEX_LEFT_SL].value =
data.button_left_sl ? 1.0 : 0.0;
pad.buttons[SWITCH_BUTTON_INDEX_LEFT_SR].pressed = data.button_left_sr;
pad.buttons[SWITCH_BUTTON_INDEX_LEFT_SR].value =
data.button_left_sr ? 1.0 : 0.0;
pad.buttons[SWITCH_BUTTON_INDEX_RIGHT_SL].pressed = data.button_right_sl;
pad.buttons[SWITCH_BUTTON_INDEX_RIGHT_SL].value =
data.button_right_sl ? 1.0 : 0.0;
pad.buttons[SWITCH_BUTTON_INDEX_RIGHT_SR].pressed = data.button_right_sr;
pad.buttons[SWITCH_BUTTON_INDEX_RIGHT_SR].value =
data.button_right_sr ? 1.0 : 0.0;
}
uint16_t axis_lx;
uint16_t axis_ly;
uint16_t axis_rx;
uint16_t axis_ry;
UnpackShorts(data.analog[0], data.analog[1], data.analog[2], &axis_lx,
&axis_ly);
UnpackShorts(data.analog[3], data.analog[4], data.analog[5], &axis_rx,
&axis_ry);
// Apply a radial dead zone to both sticks.
bool ldead = ApplyDeadZone(axis_lx, axis_ly, cal.lx_center, cal.ly_center,
cal.dead_zone);
bool rdead = ApplyDeadZone(axis_rx, axis_ry, cal.rx_center, cal.ry_center,
cal.dead_zone);
// Normalize using calibration data.
double lx =
ldead ? 0.0 : NormalizeAndClampAxis(axis_lx, cal.lx_min, cal.lx_max);
double ly =
ldead ? 0.0 : -NormalizeAndClampAxis(axis_ly, cal.ly_min, cal.ly_max);
double rx =
rdead ? 0.0 : NormalizeAndClampAxis(axis_rx, cal.rx_min, cal.rx_max);
double ry =
rdead ? 0.0 : -NormalizeAndClampAxis(axis_ry, cal.ry_min, cal.ry_max);
bool axes_changed = pad.axes_length != AXIS_INDEX_COUNT ||
pad.axes[device::AXIS_INDEX_LEFT_STICK_X] != lx ||
pad.axes[device::AXIS_INDEX_LEFT_STICK_Y] != ly ||
pad.axes[device::AXIS_INDEX_RIGHT_STICK_X] != rx ||
pad.axes[device::AXIS_INDEX_RIGHT_STICK_Y] != ry;
if (axes_changed) {
pad.axes_length = AXIS_INDEX_COUNT;
pad.axes[device::AXIS_INDEX_LEFT_STICK_X] = lx;
pad.axes[device::AXIS_INDEX_LEFT_STICK_Y] = ly;
pad.axes[device::AXIS_INDEX_RIGHT_STICK_X] = rx;
pad.axes[device::AXIS_INDEX_RIGHT_STICK_Y] = ry;
}
return buttons_changed || axes_changed;
}
// Update the state for a single button. The button state is taken from
// the button at index |button_index| in |src_pad|. If this is a composite
// device, |src_pad| holds the state for the left component. If |horizontal| is
// true, the button index is remapped for horizontal orientation before updating
// the state in |dst_pad|.
void UpdateButtonForLeftSide(const Gamepad& src_pad,
Gamepad& dst_pad,
size_t button_index,
bool horizontal) {
size_t remapped_index = button_index;
// The internal button mapping assumes a docked orientation for Joy-Cons. If
// a Joy-Con is used by itself, remap the buttons so they match the Standard
// Gamepad spec when held horizontally.
if (horizontal) {
switch (button_index) {
// Map the D-pad buttons to action buttons.
case BUTTON_INDEX_DPAD_LEFT:
remapped_index = BUTTON_INDEX_PRIMARY;
break;
case BUTTON_INDEX_DPAD_DOWN:
remapped_index = BUTTON_INDEX_SECONDARY;
break;
case BUTTON_INDEX_DPAD_UP:
remapped_index = BUTTON_INDEX_TERTIARY;
break;
case BUTTON_INDEX_DPAD_RIGHT:
remapped_index = BUTTON_INDEX_QUATERNARY;
break;
// Map L to Select.
case BUTTON_INDEX_LEFT_SHOULDER:
remapped_index = BUTTON_INDEX_BACK_SELECT;
break;
// Map Minus to Start.
case BUTTON_INDEX_BACK_SELECT:
remapped_index = BUTTON_INDEX_START;
break;
// Map Capture to Meta.
case SWITCH_BUTTON_INDEX_CAPTURE:
remapped_index = BUTTON_INDEX_META;
break;
// Map SL and SR to the left and right shoulders.
case SWITCH_BUTTON_INDEX_LEFT_SL:
remapped_index = BUTTON_INDEX_LEFT_SHOULDER;
break;
case SWITCH_BUTTON_INDEX_LEFT_SR:
remapped_index = BUTTON_INDEX_RIGHT_SHOULDER;
break;
// ZL and the left thumbstick are unmodified.
case BUTTON_INDEX_LEFT_TRIGGER:
case BUTTON_INDEX_LEFT_THUMBSTICK:
break;
default:
NOTREACHED();
break;
}
}
dst_pad.buttons[remapped_index] = src_pad.buttons[button_index];
}
// Update the state for a single button. The button state is taken from
// the button at index |button_index| in |src_pad|. If this is a composite
// device, |src_pad| holds the state for the right component. If |horizontal| is
// true, the button index is remapped for horizontal orientation before updating
// the state in |dst_pad|.
void UpdateButtonForRightSide(const Gamepad& src_pad,
Gamepad& dst_pad,
size_t button_index,
bool horizontal) {
size_t remapped_index = button_index;
// The internal button mapping assumes a docked orientation for Joy-Cons. If
// a Joy-Con is used by itself, remap the buttons so they match the Standard
// Gamepad spec when held horizontally.
if (horizontal) {
switch (button_index) {
// Re-map the action buttons to rotate them.
case BUTTON_INDEX_PRIMARY:
remapped_index = BUTTON_INDEX_TERTIARY;
break;
case BUTTON_INDEX_TERTIARY:
remapped_index = BUTTON_INDEX_QUATERNARY;
break;
case BUTTON_INDEX_QUATERNARY:
remapped_index = BUTTON_INDEX_SECONDARY;
break;
case BUTTON_INDEX_SECONDARY:
remapped_index = BUTTON_INDEX_PRIMARY;
break;
// Map R to Select.
case BUTTON_INDEX_RIGHT_SHOULDER:
remapped_index = BUTTON_INDEX_BACK_SELECT;
break;
// Map SL and SR to the left and right shoulders.
case SWITCH_BUTTON_INDEX_RIGHT_SL:
remapped_index = BUTTON_INDEX_LEFT_SHOULDER;
break;
case SWITCH_BUTTON_INDEX_RIGHT_SR:
remapped_index = BUTTON_INDEX_RIGHT_SHOULDER;
break;
// Map right thumbstick button to left thumbstick button.
case BUTTON_INDEX_RIGHT_THUMBSTICK:
remapped_index = BUTTON_INDEX_LEFT_THUMBSTICK;
break;
// The Plus, Home, and ZR buttons are unmodified.
case BUTTON_INDEX_START:
case BUTTON_INDEX_META:
case BUTTON_INDEX_RIGHT_TRIGGER:
break;
default:
NOTREACHED();
break;
}
}
dst_pad.buttons[remapped_index] = src_pad.buttons[button_index];
}
// Update the state for a single axis. The axis state is taken from the axis at
// index |axis_index| in |src_pad|. If this is a composite device, |src_pad|
// holds the state for the left component. If |horizontal| is true, the axis
// index and value are remapped for horizontal orientation before updating the
// state in |dst_pad|.
void UpdateAxisForLeftSide(const Gamepad& src_pad,
Gamepad& dst_pad,
size_t axis_index,
bool horizontal) {
size_t remapped_index = axis_index;
double axis_value = src_pad.axes[axis_index];
// The internal axis values assume a docked orientation for Joy-Cons. If a
// Joy-Con is used by itself, remap the axis indices and adjust the sign on
// the axis value for a horizontal orientation.
if (horizontal) {
switch (axis_index) {
case AXIS_INDEX_LEFT_STICK_X:
// Map +X to -Y.
axis_value = -axis_value;
remapped_index = AXIS_INDEX_LEFT_STICK_Y;
break;
case AXIS_INDEX_LEFT_STICK_Y:
// Map +Y to +X.
remapped_index = AXIS_INDEX_LEFT_STICK_X;
break;
default:
NOTREACHED();
break;
}
}
dst_pad.axes[remapped_index] = axis_value;
}
// Update the state for a single axis. The axis state is taken from the axis at
// index |axis_index| in |src_pad|. If this is a composite device, |src_pad|
// holds the state for the right component. If |horizontal| is true, the axis
// index and value are remapped for horizontal orientation before updating the
// state in |dst_pad|.
void UpdateAxisForRightSide(const Gamepad& src_pad,
Gamepad& dst_pad,
size_t axis_index,
bool horizontal) {
size_t remapped_index = axis_index;
double axis_value = src_pad.axes[axis_index];
// The internal axis values assume a docked orientation for Joy-Cons. If a
// Joy-Con is used by itself, remap the axis indices and adjust the sign on
// the axis value for a horizontal orientation.
if (horizontal) {
switch (axis_index) {
case AXIS_INDEX_RIGHT_STICK_X:
// Map +X to +Y.
remapped_index = AXIS_INDEX_LEFT_STICK_Y;
break;
case AXIS_INDEX_RIGHT_STICK_Y:
// Map +Y to -X.
axis_value = -axis_value;
remapped_index = AXIS_INDEX_LEFT_STICK_X;
break;
default:
NOTREACHED();
break;
}
}
dst_pad.axes[remapped_index] = axis_value;
}
// Convert the vibration parameters |frequency| and |amplitude| into a set of
// parameters that can be sent to the vibration actuator.
void FrequencyToHex(float frequency,
float amplitude,
uint16_t* hf,
uint8_t* lf,
uint8_t* hf_amp,
uint16_t* lf_amp) {
int freq = static_cast<int>(frequency);
int amp = static_cast<int>(amplitude * kVibrationAmplitudeMax);
// Clamp the target frequency and amplitude to a safe range.
freq = base::ClampToRange(freq, kVibrationFrequencyHzMin,
kVibrationFrequencyHzMax);
amp = base::ClampToRange(amp, 0, kVibrationAmplitudeMax);
const auto* best_vf = &kVibrationFrequency[0];
for (size_t i = 1; i < kVibrationFrequencySize; ++i) {
const auto* vf = &kVibrationFrequency[i];
if (vf->freq_hz < freq) {
best_vf = vf;
} else {
// The candidate frequency is higher than the target frequency. Check if
// it is closer than the current best.
int vf_error_above = vf->freq_hz - freq;
int best_vf_error_below = freq - best_vf->freq_hz;
if (vf_error_above < best_vf_error_below)
best_vf = vf;
break;
}
}
const auto* best_va = &kVibrationAmplitude[0];
for (size_t i = 0; i < kVibrationAmplitudeSize; ++i) {
const auto* va = &kVibrationAmplitude[i];
if (va->amp < amp) {
best_va = va;
} else {
// The candidate amplitude is higher than the target amplitude. Check if
// it is closer than the current best.
int va_error_above = va->amp - amp;
int best_va_error_below = amp - best_va->amp;
if (va_error_above < best_va_error_below)
best_va = va;
break;
}
}
DCHECK(best_vf);
DCHECK(best_va);
*hf = best_vf->hf;
*lf = best_vf->lf;
*hf_amp = best_va->hfa;
*lf_amp = best_va->lfa;
}
// Return the bus type of the Switch device described by |device_info|. This is
// needed for Windows which does not report the bus type in the HID API.
GamepadBusType BusTypeFromDeviceInfo(const mojom::HidDeviceInfo* device_info) {
DCHECK(device_info);
// If the |device_info| indicates the device is connected over Bluetooth, it's
// probably right. On some platforms the bus type is reported as USB
// regardless of the actual connection.
if (device_info->bus_type == mojom::HidBusType::kHIDBusTypeBluetooth)
return GAMEPAD_BUS_BLUETOOTH;
auto gamepad_id = GamepadIdList::Get().GetGamepadId(device_info->product_name,
device_info->vendor_id,
device_info->product_id);
switch (gamepad_id) {
case GamepadId::kNintendoProduct2009:
// The Switch Pro Controller may be connected over USB or Bluetooth.
// Determine which connection is in use by comparing the max output report
// size against known values.
switch (device_info->max_output_report_size) {
case kSwitchProMaxOutputReportSizeBytesUsb:
return GAMEPAD_BUS_USB;
case kSwitchProMaxOutputReportSizeBytesBluetooth:
return GAMEPAD_BUS_BLUETOOTH;
default:
break;
}
break;
case GamepadId::kNintendoProduct200e:
// The Charging Grip can only be connected over USB.
return GAMEPAD_BUS_USB;
case GamepadId::kNintendoProduct2006:
case GamepadId::kNintendoProduct2007:
// Joy Cons can only be connected over Bluetooth. When connected through
// a Charging Grip, the grip's ID is reported instead.
return GAMEPAD_BUS_BLUETOOTH;
case GamepadId::kPowerALicPro:
// The PowerA controller can only be connected over Bluetooth.
return GAMEPAD_BUS_BLUETOOTH;
default:
break;
}
NOTREACHED();
return GAMEPAD_BUS_UNKNOWN;
}
} // namespace
NintendoController::SwitchCalibrationData::SwitchCalibrationData() = default;
NintendoController::SwitchCalibrationData::~SwitchCalibrationData() = default;
NintendoController::SwitchImuData::SwitchImuData() = default;
NintendoController::SwitchImuData::~SwitchImuData() = default;
NintendoController::NintendoController(int source_id,
mojom::HidDeviceInfoPtr device_info,
mojom::HidManager* hid_manager)
: source_id_(source_id),
is_composite_(false),
bus_type_(GAMEPAD_BUS_UNKNOWN),
output_report_size_bytes_(0),
device_info_(std::move(device_info)),
hid_manager_(hid_manager) {
if (device_info_) {
bus_type_ = BusTypeFromDeviceInfo(device_info_.get());
output_report_size_bytes_ = device_info_->max_output_report_size;
gamepad_id_ = GamepadIdList::Get().GetGamepadId(device_info_->product_name,
device_info_->vendor_id,
device_info_->product_id);
} else {
gamepad_id_ = GamepadId::kUnknownGamepad;
}
}
NintendoController::NintendoController(
int source_id,
std::unique_ptr<NintendoController> composite1,
std::unique_ptr<NintendoController> composite2,
mojom::HidManager* hid_manager)
: source_id_(source_id), is_composite_(true), hid_manager_(hid_manager) {
// Require exactly one left component and one right component, but allow them
// to be provided in either order.
DCHECK(composite1);
DCHECK(composite2);
composite_left_ = std::move(composite1);
composite_right_ = std::move(composite2);
if (composite_left_->GetGamepadHand() != GamepadHand::kLeft)
composite_left_.swap(composite_right_);
DCHECK_EQ(composite_left_->GetGamepadHand(), GamepadHand::kLeft);
DCHECK_EQ(composite_right_->GetGamepadHand(), GamepadHand::kRight);
DCHECK_EQ(composite_left_->GetBusType(), composite_right_->GetBusType());
bus_type_ = composite_left_->GetBusType();
}
NintendoController::~NintendoController() = default;
// static
std::unique_ptr<NintendoController> NintendoController::Create(
int source_id,
mojom::HidDeviceInfoPtr device_info,
mojom::HidManager* hid_manager) {
return std::make_unique<NintendoController>(source_id, std::move(device_info),
hid_manager);
}
// static
std::unique_ptr<NintendoController> NintendoController::CreateComposite(
int source_id,
std::unique_ptr<NintendoController> composite1,
std::unique_ptr<NintendoController> composite2,
mojom::HidManager* hid_manager) {
return std::make_unique<NintendoController>(
source_id, std::move(composite1), std::move(composite2), hid_manager);
}
// static
bool NintendoController::IsNintendoController(GamepadId gamepad_id) {
switch (gamepad_id) {
case GamepadId::kNintendoProduct2006:
case GamepadId::kNintendoProduct2007:
case GamepadId::kNintendoProduct2009:
case GamepadId::kNintendoProduct200e:
case GamepadId::kPowerALicPro:
return true;
default:
break;
}
return false;
}
std::vector<std::unique_ptr<NintendoController>>
NintendoController::Decompose() {
// Stop any ongoing vibration effects before decomposing the device.
SetZeroVibration();
std::vector<std::unique_ptr<NintendoController>> decomposed_devices;
if (composite_left_)
decomposed_devices.push_back(std::move(composite_left_));
if (composite_right_)
decomposed_devices.push_back(std::move(composite_right_));
return decomposed_devices;
}
void NintendoController::Open(base::OnceClosure device_ready_closure) {
device_ready_closure_ = std::move(device_ready_closure);
if (is_composite_) {
StartInitSequence();
} else {
GamepadId gamepad_id = GamepadIdList::Get().GetGamepadId(
device_info_->product_name, device_info_->vendor_id,
device_info_->product_id);
if (IsNintendoController(gamepad_id)) {
Connect(base::BindOnce(&NintendoController::OnConnect,
weak_factory_.GetWeakPtr()));
}
}
}
GamepadHand NintendoController::GetGamepadHand() const {
if (is_composite_)
return GamepadHand::kNone;
switch (gamepad_id_) {
case GamepadId::kNintendoProduct2009:
case GamepadId::kPowerALicPro:
// Switch Pro and PowerA are held in both hands.
return GamepadHand::kNone;
case GamepadId::kNintendoProduct2006:
// Joy-Con L is held in the left hand.
return GamepadHand::kLeft;
case GamepadId::kNintendoProduct2007:
// Joy-Con R is held in the right hand.
return GamepadHand::kRight;
case GamepadId::kNintendoProduct200e:
// Refer to |usb_device_type_| to determine the handedness of Joy-Cons
// connected to a Charging Grip.
if (state_ == kInitialized) {
switch (usb_device_type_) {
case kUsbDeviceTypeChargingGripJoyConL:
return GamepadHand::kLeft;
case kUsbDeviceTypeChargingGripJoyConR:
return GamepadHand::kRight;
case kUsbDeviceTypeChargingGripNoDevice:
case kUsbDeviceTypeProController:
return GamepadHand::kNone;
default:
break;
}
} else {
return GamepadHand::kNone;
}
break;
default:
break;
}
NOTREACHED();
return GamepadHand::kNone;
}
bool NintendoController::IsUsable() const {
if (state_ != kInitialized)
return false;
if (is_composite_)
return composite_left_ && composite_right_;
switch (gamepad_id_) {
case GamepadId::kNintendoProduct2009:
case GamepadId::kNintendoProduct2006:
case GamepadId::kNintendoProduct2007:
case GamepadId::kPowerALicPro:
return true;
case GamepadId::kNintendoProduct200e:
// Only usable as a composite device.
return false;
default:
break;
}
NOTREACHED();
return false;
}
bool NintendoController::HasGuid(const std::string& guid) const {
if (is_composite_) {
DCHECK(composite_left_);
DCHECK(composite_right_);
return composite_left_->HasGuid(guid) || composite_right_->HasGuid(guid);
}
return device_info_->guid == guid;
}
GamepadStandardMappingFunction NintendoController::GetMappingFunction() const {
if (is_composite_) {
// In composite mode, we use the same mapping as the Charging Grip.
return GetGamepadStandardMappingFunction(
kProductNameSwitchCompositeDevice, kVendorNintendo,
kProductSwitchChargingGrip,
/*hid_specification_version=*/0, /*version_number=*/0, bus_type_);
} else {
return GetGamepadStandardMappingFunction(
device_info_->product_name, device_info_->vendor_id,
device_info_->product_id,
/*hid_specification_version=*/0, /*version_number=*/0, bus_type_);
}
}
void NintendoController::InitializeGamepadState(bool has_standard_mapping,
Gamepad& pad) const {
pad.buttons_length = SWITCH_BUTTON_INDEX_COUNT;
pad.axes_length = device::AXIS_INDEX_COUNT;
if (gamepad_id_ == GamepadId::kPowerALicPro) {
pad.vibration_actuator.not_null = false;
} else {
pad.vibration_actuator.type = GamepadHapticActuatorType::kDualRumble;
pad.vibration_actuator.not_null = true;
}
pad.timestamp = GamepadDataFetcher::CurrentTimeInMicroseconds();
if (is_composite_) {
// Composite devices use the same product ID as the Switch Charging Grip.
GamepadDataFetcher::UpdateGamepadStrings(
kProductNameSwitchCompositeDevice, kVendorNintendo,
kProductSwitchChargingGrip, has_standard_mapping, pad);
} else {
GamepadDataFetcher::UpdateGamepadStrings(
device_info_->product_name, device_info_->vendor_id,
device_info_->product_id, has_standard_mapping, pad);
}
}
void NintendoController::UpdatePadConnected() {
if (is_composite_) {
// Composite devices are always connected.
pad_.connected = true;
return;
}
if (gamepad_id_ == GamepadId::kNintendoProduct200e &&
usb_device_type_ == kUsbDeviceTypeChargingGripNoDevice) {
// If the Charging Grip reports that no Joy-Con is docked, mark the gamepad
// disconnected.
pad_.connected = false;
return;
}
// All other devices are considered connected after completing initialization.
pad_.connected = (state_ == kInitialized);
}
void NintendoController::UpdateGamepadState(Gamepad& pad) const {
if (is_composite_) {
DCHECK(composite_left_);
DCHECK(composite_right_);
// If this is a composite device, update the gamepad state using the state
// of the subcomponents.
pad.connected = true;
composite_left_->UpdateLeftGamepadState(pad, false);
composite_right_->UpdateRightGamepadState(pad, false);
} else {
switch (GetGamepadHand()) {
case GamepadHand::kLeft:
// Update state for a Joy-Con L, remapping buttons and axes to match the
// Standard Gamepad when the device is held horizontally.
UpdateLeftGamepadState(pad, true);
break;
case GamepadHand::kRight:
// Update state for a Joy-Con R, remapping buttons and axes to match the
// Standard Gamepad when the device is held horizontally.
UpdateRightGamepadState(pad, true);
break;
case GamepadHand::kNone:
// Update state for a Pro Controller.
UpdateLeftGamepadState(pad, false);
UpdateRightGamepadState(pad, false);
break;
default:
NOTREACHED();
break;
}
pad.connected = pad_.connected;
}
}
void NintendoController::UpdateLeftGamepadState(Gamepad& pad,
bool horizontal) const {
// Buttons associated with the left Joy-Con.
const size_t kLeftButtonIndices[] = {
BUTTON_INDEX_LEFT_SHOULDER, // ZL button
BUTTON_INDEX_LEFT_TRIGGER, // L button
BUTTON_INDEX_BACK_SELECT, // - button
BUTTON_INDEX_LEFT_THUMBSTICK,
BUTTON_INDEX_DPAD_UP, // D-pad directions for the composite gamepad
BUTTON_INDEX_DPAD_DOWN, // assume the Joy-Con is held in the vertical
BUTTON_INDEX_DPAD_LEFT, // orientation or is attached to a grip.
BUTTON_INDEX_DPAD_RIGHT, SWITCH_BUTTON_INDEX_CAPTURE,
SWITCH_BUTTON_INDEX_LEFT_SL, SWITCH_BUTTON_INDEX_LEFT_SR,
};
const size_t kLeftButtonIndicesSize = base::size(kLeftButtonIndices);
// Axes associated with the left Joy-Con thumbstick.
const size_t kLeftAxisIndices[] = {
AXIS_INDEX_LEFT_STICK_X, // Axes assume the Joy-Con is held vertically
AXIS_INDEX_LEFT_STICK_Y, // or is attached to a grip.
};
const size_t kLeftAxisIndicesSize = base::size(kLeftAxisIndices);
if (pad_.buttons_length == SWITCH_BUTTON_INDEX_COUNT) {
for (size_t i = 0; i < kLeftButtonIndicesSize; ++i)
UpdateButtonForLeftSide(pad_, pad, kLeftButtonIndices[i], horizontal);
}
if (pad_.axes_length == AXIS_INDEX_COUNT) {
for (size_t i = 0; i < kLeftAxisIndicesSize; ++i)
UpdateAxisForLeftSide(pad_, pad, kLeftAxisIndices[i], horizontal);
}
pad.timestamp = std::max(pad.timestamp, pad_.timestamp);
if (!pad_.connected)
pad.connected = false;
}
void NintendoController::UpdateRightGamepadState(Gamepad& pad,
bool horizontal) const {
// Buttons associated with the right Joy-Con.
const size_t kRightButtonIndices[]{
BUTTON_INDEX_PRIMARY, // B button
BUTTON_INDEX_SECONDARY, // A button
BUTTON_INDEX_TERTIARY, // Y button
BUTTON_INDEX_QUATERNARY, // X button
BUTTON_INDEX_RIGHT_SHOULDER, // R button
BUTTON_INDEX_RIGHT_TRIGGER, // ZR button
BUTTON_INDEX_START, // + button
BUTTON_INDEX_RIGHT_THUMBSTICK,
BUTTON_INDEX_META, // Home button
SWITCH_BUTTON_INDEX_RIGHT_SL,
SWITCH_BUTTON_INDEX_RIGHT_SR,
};
const size_t kRightButtonIndicesSize = base::size(kRightButtonIndices);
// Axes associated with the right Joy-Con thumbstick.
const size_t kRightAxisIndices[] = {
AXIS_INDEX_RIGHT_STICK_X, // Axes assume the Joy-Con is held vertically
AXIS_INDEX_RIGHT_STICK_Y, // or is attached to a grip.
};
const size_t kRightAxisIndicesSize = base::size(kRightAxisIndices);
if (pad_.buttons_length == SWITCH_BUTTON_INDEX_COUNT) {
for (size_t i = 0; i < kRightButtonIndicesSize; ++i)
UpdateButtonForRightSide(pad_, pad, kRightButtonIndices[i], horizontal);
}
if (pad_.axes_length == AXIS_INDEX_COUNT) {
for (size_t i = 0; i < kRightAxisIndicesSize; ++i)
UpdateAxisForRightSide(pad_, pad, kRightAxisIndices[i], horizontal);
}
pad.timestamp = std::max(pad.timestamp, pad_.timestamp);
if (!pad_.connected)
pad.connected = false;
}
void NintendoController::Connect(mojom::HidManager::ConnectCallback callback) {
DCHECK(!is_composite_);
DCHECK(hid_manager_);
hid_manager_->Connect(device_info_->guid,
/*connection_client=*/mojo::NullRemote(),
/*watcher=*/mojo::NullRemote(),
/*allow_protected_reports=*/false, std::move(callback));
}
void NintendoController::OnConnect(
mojo::PendingRemote<mojom::HidConnection> connection) {
if (connection) {
connection_.Bind(std::move(connection));
ReadInputReport();
StartInitSequence();
}
}
void NintendoController::StartInitSequence() {
if (is_composite_) {
if (composite_left_ && composite_left_->IsOpen() && composite_right_ &&
composite_right_->IsOpen()) {
DCHECK_EQ(composite_left_->GetGamepadHand(), GamepadHand::kLeft);
DCHECK_EQ(composite_right_->GetGamepadHand(), GamepadHand::kRight);
FinishInitSequence();
} else {
FailInitSequence();
}
return;
}
switch (bus_type_) {
case GAMEPAD_BUS_USB:
DCHECK(timeout_callback_.IsCancelled());
MakeInitSequenceRequests(kPendingMacAddress);
break;
case GAMEPAD_BUS_BLUETOOTH:
DCHECK(timeout_callback_.IsCancelled());
MakeInitSequenceRequests(kPendingSetPlayerLights);
break;
default:
NOTREACHED();
break;
}
}
void NintendoController::FinishInitSequence() {
state_ = kInitialized;
UpdatePadConnected();
if (device_ready_closure_)
std::move(device_ready_closure_).Run();
}
void NintendoController::FailInitSequence() {
state_ = kUninitialized;
UpdatePadConnected();
}
void NintendoController::HandleInputReport(
uint8_t report_id,
const std::vector<uint8_t>& report_bytes) {
// Register to receive the next input report.
ReadInputReport();
// Listen for reports related to the initialization sequence or gamepad state.
// Other reports are ignored.
if (bus_type_ == GAMEPAD_BUS_USB && report_id == kUsbReportIdInput81)
HandleUsbInputReport81(report_bytes);
else if (report_id == kReportIdInput21)
HandleInputReport21(report_bytes);
else if (report_id == kReportIdInput30)
HandleInputReport30(report_bytes);
// Check whether the input report should cause us to transition to the next
// initialization step.
if (state_ != kInitialized && state_ != kUninitialized)
ContinueInitSequence(report_id, report_bytes);
}
void NintendoController::HandleUsbInputReport81(
const std::vector<uint8_t>& report_bytes) {
const auto* ack_report =
reinterpret_cast<const UsbInputReport81*>(report_bytes.data());
switch (ack_report->subtype) {
case kSubTypeRequestMac: {
const auto* mac_report =
reinterpret_cast<const MacAddressReport*>(report_bytes.data());
mac_address_ = UnpackSwitchMacAddress(mac_report->mac_data);
if (usb_device_type_ != mac_report->device_type) {
usb_device_type_ = mac_report->device_type;
switch (usb_device_type_) {
case kUsbDeviceTypeChargingGripNoDevice:
UpdatePadConnected();
// If this was received from an initialized gamepad it means one of
// the Joy-Cons was disconnected from the charging grip. The HID
// device does not disconnect; de-initialize the device so the
// composite device will be hidden.
if (state_ == kInitialized)
FailInitSequence();
break;
case kUsbDeviceTypeChargingGripJoyConL:
case kUsbDeviceTypeChargingGripJoyConR:
UpdatePadConnected();
// A Joy-Con was connected to a de-initialized device. Restart the
// initialization sequence.
if (state_ == kUninitialized)
StartInitSequence();
break;
default:
break;
}
}
break;
}
default:
break;
}
}
void NintendoController::HandleInputReport21(
const std::vector<uint8_t>& report_bytes) {
const auto* spi_report =
reinterpret_cast<const SpiReadReport*>(report_bytes.data());
if (UpdateGamepadFromControllerData(spi_report->controller_data, cal_data_,
pad_)) {
pad_.timestamp = GamepadDataFetcher::CurrentTimeInMicroseconds();
}
// The input report includes the parameters for the SPI read request along
// with the data that was read. Use the read address to determine how to
// unpack the data.
if (spi_report->subcommand == kSubCommandReadSpi) {
uint16_t address = (spi_report->addrh << 8) | spi_report->addrl;
switch (address) {
case kSpiImuCalibrationAddress:
UnpackSwitchImuCalibration(spi_report->spi_data, cal_data_);
break;
case kSpiImuHorizontalOffsetsAddress:
UnpackSwitchImuHorizontalOffsets(spi_report->spi_data, cal_data_);
break;
case kSpiAnalogStickCalibrationAddress:
UnpackSwitchAnalogStickCalibration(spi_report->spi_data, cal_data_);
break;
case kSpiAnalogStickParametersAddress:
UnpackSwitchAnalogStickParameters(spi_report->spi_data, cal_data_);
break;
default:
break;
}
}
}
void NintendoController::HandleInputReport30(
const std::vector<uint8_t>& report_bytes) {
const auto* controller_report =
reinterpret_cast<const ControllerDataReport*>(report_bytes.data());
// Each input report contains three frames of IMU data.
UnpackSwitchImuData(&controller_report->imu_data[0], &imu_data_[0]);
UnpackSwitchImuData(&controller_report->imu_data[12], &imu_data_[1]);
UnpackSwitchImuData(&controller_report->imu_data[24], &imu_data_[2]);
if (UpdateGamepadFromControllerData(controller_report->controller_data,
cal_data_, pad_)) {
pad_.timestamp = GamepadDataFetcher::CurrentTimeInMicroseconds();
}
}
void NintendoController::ContinueInitSequence(
uint8_t report_id,
const std::vector<uint8_t>& report_bytes) {
const auto* ack_report =
reinterpret_cast<const UsbInputReport81*>(report_bytes.data());
const auto* spi_report =
reinterpret_cast<const SpiReadReport*>(report_bytes.data());
const uint8_t ack_subtype =
(report_id == kUsbReportIdInput81) ? ack_report->subtype : 0;
const uint8_t spi_subcommand =
(report_id == kReportIdInput21) ? spi_report->subcommand : 0;
const bool is_spi_read =
(report_id == kReportIdInput21 && spi_subcommand == kSubCommandReadSpi);
const uint16_t spi_read_address =
is_spi_read ? ((spi_report->addrh << 8) | spi_report->addrl) : 0;
const uint16_t spi_read_length = is_spi_read ? spi_report->length : 0;
switch (state_) {
case kPendingMacAddress:
if (ack_subtype == kSubTypeRequestMac) {
CancelTimeout();
if (mac_address_)
MakeInitSequenceRequests(kPendingHandshake1);
else
FailInitSequence();
}
break;
case kPendingHandshake1:
if (ack_subtype == kSubTypeHandshake) {
CancelTimeout();
MakeInitSequenceRequests(kPendingBaudRate);
}
break;
case kPendingBaudRate:
if (ack_subtype == kSubTypeBaudRate) {
CancelTimeout();
MakeInitSequenceRequests(kPendingHandshake2);
}
break;
case kPendingHandshake2:
if (ack_subtype == kSubTypeHandshake) {
CancelTimeout();
MakeInitSequenceRequests(kPendingDisableUsbTimeout);
}
break;
case kPendingDisableUsbTimeout:
if (spi_subcommand == kSubCommand33) {
CancelTimeout();
MakeInitSequenceRequests(kPendingSetPlayerLights);
}
break;
case kPendingSetPlayerLights:
if (spi_subcommand == kSubCommandSetPlayerLights) {
CancelTimeout();
MakeInitSequenceRequests(kPendingEnableImu);
}
break;
case kPendingEnableImu:
if (spi_subcommand == kSubCommandEnableImu) {
CancelTimeout();
MakeInitSequenceRequests(kPendingSetImuSensitivity);
}
break;
case kPendingSetImuSensitivity:
if (spi_subcommand == kSubCommandSetImuSensitivity) {
CancelTimeout();
MakeInitSequenceRequests(kPendingReadImuCalibration);
}
break;
case kPendingReadImuCalibration:
if (spi_read_address == kSpiImuCalibrationAddress &&
spi_read_length == kSpiImuCalibrationSize) {
CancelTimeout();
MakeInitSequenceRequests(kPendingReadHorizontalOffsets);
}
break;
case kPendingReadHorizontalOffsets:
if (spi_read_address == kSpiImuHorizontalOffsetsAddress &&
spi_read_length == kSpiImuHorizontalOffsetsSize) {
CancelTimeout();
MakeInitSequenceRequests(kPendingReadAnalogStickCalibration);
}
break;
case kPendingReadAnalogStickCalibration:
if (spi_read_address == kSpiAnalogStickCalibrationAddress &&
spi_read_length == kSpiAnalogStickCalibrationSize) {
CancelTimeout();
MakeInitSequenceRequests(kPendingReadAnalogStickParameters);
}
break;
case kPendingReadAnalogStickParameters:
if (spi_read_address == kSpiAnalogStickParametersAddress &&
spi_read_length == kSpiAnalogStickParametersSize) {
CancelTimeout();
MakeInitSequenceRequests(kPendingEnableVibration);
}
break;
case kPendingEnableVibration:
if (spi_subcommand == kSubCommandEnableVibration) {
CancelTimeout();
// PowerA controller doesn't have a home light and trying to set it will
// fail, so skip this step.
if (gamepad_id_ == GamepadId::kPowerALicPro) {
MakeInitSequenceRequests(kPendingSetInputReportMode);
} else {
MakeInitSequenceRequests(kPendingSetHomeLight);
}
}
break;
case kPendingSetHomeLight:
if (spi_subcommand == kSubCommandSetHomeLight) {
CancelTimeout();
MakeInitSequenceRequests(kPendingSetInputReportMode);
}
break;
case kPendingSetInputReportMode:
if (spi_subcommand == kSubCommandSetInputReportMode) {
CancelTimeout();
MakeInitSequenceRequests(kPendingControllerData);
}
break;
case kPendingControllerData:
if (report_id == kReportIdInput30) {
CancelTimeout();
FinishInitSequence();
}
break;
case kInitialized:
case kUninitialized:
NOTREACHED();
break;
default:
break;
}
}
void NintendoController::MakeInitSequenceRequests(InitializationState state) {
DCHECK(timeout_callback_.IsCancelled());
state_ = state;
switch (state_) {
case kPendingMacAddress:
RequestMacAddress();
break;
case kPendingHandshake1:
case kPendingHandshake2:
RequestHandshake();
break;
case kPendingBaudRate:
RequestBaudRate();
break;
case kPendingDisableUsbTimeout:
RequestEnableUsbTimeout(false);
break;
case kPendingSetPlayerLights:
RequestSetPlayerLights(kPlayerLightPattern1); // Player 1 indicator on.
break;
case kPendingEnableImu:
RequestEnableImu(false); // IMU disabled.
break;
case kPendingSetImuSensitivity:
RequestSetImuSensitivity(
kGyroSensitivity2000Dps, kAccelerometerSensitivity8G,
kGyroPerformance208Hz, kAccelerometerFilterBandwidth100Hz);
break;
case kPendingReadImuCalibration:
RequestImuCalibration();
break;
case kPendingReadHorizontalOffsets:
RequestHorizontalOffsets();
break;
case kPendingReadAnalogStickCalibration:
RequestAnalogCalibration();
break;
case kPendingReadAnalogStickParameters:
RequestAnalogParameters();
break;
case kPendingEnableVibration:
RequestEnableVibration(true);
break;
case kPendingSetHomeLight:
RequestSetHomeLightIntensity(1.0); // 100% intensity.
break;
case kPendingSetInputReportMode:
RequestSetInputReportMode(0x30); // Standard full mode reported at 60Hz.
break;
case kPendingControllerData:
ArmTimeout();
break;
case kInitialized:
case kUninitialized:
default:
NOTREACHED();
break;
}
}
void NintendoController::SubCommand(uint8_t sub_command,
const std::vector<uint8_t>& bytes) {
std::vector<uint8_t> report_bytes(output_report_size_bytes_ - 1);
// Serial subcommands also carry vibration data. Configure the vibration
// portion of the report for a neutral vibration effect (zero amplitude).
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/bluetooth_hid_notes.md#output-0x12
report_bytes[0] = uint8_t{output_report_counter_++ & 0xff};
report_bytes[1] = 0x00;
report_bytes[2] = 0x01;
report_bytes[3] = 0x40;
report_bytes[4] = 0x40;
report_bytes[5] = 0x00;
report_bytes[6] = 0x01;
report_bytes[7] = 0x40;
report_bytes[8] = 0x40;
report_bytes[9] = sub_command;
DCHECK_LT(bytes.size() + kSubCommandDataOffset, output_report_size_bytes_);
std::copy(bytes.begin(), bytes.end(),
&report_bytes[kSubCommandDataOffset - 1]);
WriteOutputReport(kReportIdOutput01, report_bytes, true);
}
void NintendoController::RequestMacAddress() {
std::vector<uint8_t> report_bytes(output_report_size_bytes_ - 1);
report_bytes[0] = kSubTypeRequestMac;
WriteOutputReport(kUsbReportIdOutput80, report_bytes, true);
}
void NintendoController::RequestHandshake() {
std::vector<uint8_t> report_bytes(output_report_size_bytes_ - 1);
report_bytes[0] = kSubTypeHandshake;
WriteOutputReport(kUsbReportIdOutput80, report_bytes, true);
}
void NintendoController::RequestBaudRate() {
std::vector<uint8_t> report_bytes(output_report_size_bytes_ - 1);
report_bytes[0] = kSubTypeBaudRate;
WriteOutputReport(kUsbReportIdOutput80, report_bytes, true);
}
void NintendoController::RequestSubCommand33() {
// Unrecognized commands do nothing, but still generate a reply.
SubCommand(kSubCommand33, {});
}
void NintendoController::RequestVibration(double left_frequency,
double left_magnitude,
double right_frequency,
double right_magnitude) {
uint16_t lhf;
uint8_t llf;
uint8_t lhfa;
uint16_t llfa;
uint16_t rhf;
uint8_t rlf;
uint8_t rhfa;
uint16_t rlfa;
FrequencyToHex(left_frequency, left_magnitude, &lhf, &llf, &lhfa, &llfa);
FrequencyToHex(right_frequency, right_magnitude, &rhf, &rlf, &rhfa, &rlfa);
std::vector<uint8_t> report_bytes(output_report_size_bytes_ - 1);
uint8_t counter = uint8_t{output_report_counter_++ & 0x0f};
report_bytes[0] = counter;
report_bytes[1] = lhf & 0xff;
report_bytes[2] = lhfa + ((lhf >> 8) & 0xff);
report_bytes[3] = llf + ((llfa >> 8) & 0xff);
report_bytes[4] = llfa & 0xff;
report_bytes[5] = rhf & 0xff;
report_bytes[6] = rhfa + ((rhf >> 8) & 0xff);
report_bytes[7] = rlf + ((rlfa >> 8) & 0xff);
report_bytes[8] = rlfa & 0xff;
WriteOutputReport(kReportIdOutput10, report_bytes, false);
}
void NintendoController::RequestEnableUsbTimeout(bool enable) {
// By default, Switch Pro will revert to Bluetooth mode if it does not
// receive any USB HID commands within a timeout window. Disabling the
// timeout keeps the device in USB mode.
std::vector<uint8_t> report_bytes(output_report_size_bytes_ - 1);
report_bytes[0] =
enable ? kSubTypeEnableUsbTimeout : kSubTypeDisableUsbTimeout;
// This report may not be acked due to a software bug on the device.
WriteOutputReport(kUsbReportIdOutput80, report_bytes, false);
// Send an unused subcommand (0x33) which is acked.
RequestSubCommand33();
}
void NintendoController::RequestEnableImu(bool enable) {
SubCommand(kSubCommandEnableImu, {enable ? 0x01 : 0x00});
}
void NintendoController::RequestEnableVibration(bool enable) {
SubCommand(kSubCommandEnableVibration, {enable ? 0x01 : 0x00});
}
void NintendoController::RequestSetPlayerLights(uint8_t light_pattern) {
SubCommand(kSubCommandSetPlayerLights, {light_pattern});
}
void NintendoController::RequestSetHomeLight(
uint8_t minicycle_count,
uint8_t minicycle_duration,
uint8_t start_intensity,
uint8_t cycle_count,
const std::vector<uint8_t>& minicycle_data) {
DCHECK_LE(minicycle_count, 0xf);
DCHECK_LE(minicycle_duration, 0xf);
DCHECK_LE(start_intensity, 0xf);
DCHECK_LE(cycle_count, 0xf);
if ((cycle_count > 0 && minicycle_count == 1) || minicycle_duration == 0)
minicycle_count = 0;
std::vector<uint8_t> bytes = {(minicycle_count << 4) | minicycle_duration,
(start_intensity << 4) | cycle_count};
bytes.insert(bytes.end(), minicycle_data.begin(), minicycle_data.end());
SubCommand(kSubCommandSetHomeLight, bytes);
}
void NintendoController::RequestSetHomeLightIntensity(double intensity) {
intensity = base::ClampToRange(intensity, 0.0, 1.0);
uint8_t led_intensity = std::round(intensity * 0x0f);
// Each pair of bytes in the minicycle data describes two minicyles.
// The first byte holds two 4-bit values encoding minicycle intensities.
// The second byte holds two 4-bit multipliers for the duration of each
// transition.
//
// This command encodes one minicycle that transitions to 100% intensity after
// 1x minicycle duration. Because |minicycle_count| and |cycle_count| are
// both zero, the device will transition to the 1st minicycle and then stay at
// |led_intensity|.
RequestSetHomeLight(0, 1, led_intensity, 0, {led_intensity << 4, 0x00});
}
void NintendoController::RequestSetImuSensitivity(
uint8_t gyro_sensitivity,
uint8_t accelerometer_sensitivity,
uint8_t gyro_performance_rate,
uint8_t accelerometer_filter_bandwidth) {
SubCommand(kSubCommandSetImuSensitivity,
{gyro_sensitivity, accelerometer_sensitivity,
gyro_performance_rate, accelerometer_filter_bandwidth});
}
void NintendoController::RequestSetInputReportMode(uint8_t mode) {
SubCommand(kSubCommandSetInputReportMode, {mode});
}
void NintendoController::ReadSpi(uint16_t address, size_t length) {
DCHECK_LE(length + kSpiDataOffset, output_report_size_bytes_);
length = std::min(length, output_report_size_bytes_ - kSpiDataOffset);
uint8_t address_high = (address >> 8) & 0xff;
uint8_t address_low = address & 0xff;
SubCommand(kSubCommandReadSpi,
{address_low, address_high, 0x00, 0x00, uint8_t{length}});
}
void NintendoController::RequestImuCalibration() {
ReadSpi(kSpiImuCalibrationAddress, kSpiImuCalibrationSize);
}
void NintendoController::RequestHorizontalOffsets() {
ReadSpi(kSpiImuHorizontalOffsetsAddress, kSpiImuHorizontalOffsetsSize);
}
void NintendoController::RequestAnalogCalibration() {
ReadSpi(kSpiAnalogStickCalibrationAddress, kSpiAnalogStickCalibrationSize);
}
void NintendoController::RequestAnalogParameters() {
ReadSpi(kSpiAnalogStickParametersAddress, kSpiAnalogStickParametersSize);
}
void NintendoController::ReadInputReport() {
DCHECK(connection_);
connection_->Read(base::BindOnce(&NintendoController::OnReadInputReport,
weak_factory_.GetWeakPtr()));
}
void NintendoController::OnReadInputReport(
bool success,
uint8_t report_id,
const base::Optional<std::vector<uint8_t>>& report_bytes) {
if (success) {
DCHECK(report_bytes);
HandleInputReport(report_id, *report_bytes);
} else {
CancelTimeout();
FailInitSequence();
}
}
void NintendoController::WriteOutputReport(
uint8_t report_id,
const std::vector<uint8_t>& report_bytes,
bool expect_reply) {
DCHECK(connection_);
DCHECK(timeout_callback_.IsCancelled());
connection_->Write(report_id, report_bytes,
base::BindOnce(&NintendoController::OnWriteOutputReport,
weak_factory_.GetWeakPtr()));
if (expect_reply)
ArmTimeout();
}
void NintendoController::OnWriteOutputReport(bool success) {
if (!success) {
CancelTimeout();
FailInitSequence();
}
}
void NintendoController::DoShutdown() {
if (composite_left_)
composite_left_->Shutdown();
composite_left_.reset();
if (composite_right_)
composite_right_->Shutdown();
composite_right_.reset();
connection_.reset();
device_info_.reset();
}
void NintendoController::SetVibration(double strong_magnitude,
double weak_magnitude) {
if (is_composite_) {
// Split the vibration effect between the left and right subdevices.
if (composite_left_ && composite_right_) {
composite_left_->SetVibration(strong_magnitude, 0);
composite_right_->SetVibration(0, weak_magnitude);
}
} else {
RequestVibration(kVibrationFrequencyStrongRumble,
kVibrationAmplitudeStrongRumbleMax * strong_magnitude,
kVibrationFrequencyWeakRumble,
kVibrationAmplitudeWeakRumbleMax * weak_magnitude);
}
}
double NintendoController::GetMaxEffectDurationMillis() {
return kMaxVibrationEffectDurationMillis;
}
void NintendoController::ArmTimeout() {
DCHECK(timeout_callback_.IsCancelled());
timeout_callback_.Reset(base::BindOnce(&NintendoController::OnTimeout,
weak_factory_.GetWeakPtr()));
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, timeout_callback_.callback(), kTimeoutDuration);
}
void NintendoController::CancelTimeout() {
timeout_callback_.Cancel();
retry_count_ = 0;
}
void NintendoController::OnTimeout() {
++retry_count_;
if (retry_count_ <= kMaxRetryCount)
MakeInitSequenceRequests(state_);
else {
retry_count_ = 0;
StartInitSequence();
}
}
base::WeakPtr<AbstractHapticGamepad> NintendoController::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
} // namespace device