Home | History | Annotate | Download | only in gamepad
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "content/browser/gamepad/xbox_data_fetcher_mac.h"
      6 
      7 #include <algorithm>
      8 #include <cmath>
      9 #include <limits>
     10 
     11 #include <CoreFoundation/CoreFoundation.h>
     12 #include <IOKit/IOCFPlugIn.h>
     13 #include <IOKit/IOKitLib.h>
     14 #include <IOKit/usb/IOUSBLib.h>
     15 #include <IOKit/usb/USB.h>
     16 
     17 #include "base/logging.h"
     18 #include "base/mac/foundation_util.h"
     19 
     20 namespace {
     21 const int kVendorMicrosoft = 0x045e;
     22 const int kProduct360Controller = 0x028e;
     23 
     24 const int kReadEndpoint = 1;
     25 const int kControlEndpoint = 2;
     26 
     27 enum {
     28   STATUS_MESSAGE_BUTTONS = 0,
     29   STATUS_MESSAGE_LED = 1,
     30 
     31   // Apparently this message tells you if the rumble pack is disabled in the
     32   // controller. If the rumble pack is disabled, vibration control messages
     33   // have no effect.
     34   STATUS_MESSAGE_RUMBLE = 3,
     35 };
     36 
     37 enum {
     38   CONTROL_MESSAGE_SET_RUMBLE = 0,
     39   CONTROL_MESSAGE_SET_LED = 1,
     40 };
     41 
     42 #pragma pack(push, 1)
     43 struct ButtonData {
     44   bool dpad_up    : 1;
     45   bool dpad_down  : 1;
     46   bool dpad_left  : 1;
     47   bool dpad_right : 1;
     48 
     49   bool start             : 1;
     50   bool back              : 1;
     51   bool stick_left_click  : 1;
     52   bool stick_right_click : 1;
     53 
     54   bool bumper_left  : 1;
     55   bool bumper_right : 1;
     56   bool guide        : 1;
     57   bool dummy1       : 1;  // Always 0.
     58 
     59   bool a : 1;
     60   bool b : 1;
     61   bool x : 1;
     62   bool y : 1;
     63 
     64   uint8 trigger_left;
     65   uint8 trigger_right;
     66 
     67   int16 stick_left_x;
     68   int16 stick_left_y;
     69   int16 stick_right_x;
     70   int16 stick_right_y;
     71 
     72   // Always 0.
     73   uint32 dummy2;
     74   uint16 dummy3;
     75 };
     76 #pragma pack(pop)
     77 
     78 COMPILE_ASSERT(sizeof(ButtonData) == 0x12, xbox_button_data_wrong_size);
     79 
     80 // From MSDN:
     81 // http://msdn.microsoft.com/en-us/library/windows/desktop/ee417001(v=vs.85).aspx#dead_zone
     82 const int16 kLeftThumbDeadzone = 7849;
     83 const int16 kRightThumbDeadzone = 8689;
     84 const uint8 kTriggerDeadzone = 30;
     85 
     86 void NormalizeAxis(int16 x,
     87                        int16 y,
     88                        int16 deadzone,
     89                        float* x_out,
     90                        float* y_out) {
     91   float x_val = x;
     92   float y_val = y;
     93 
     94   // Determine how far the stick is pushed.
     95   float real_magnitude = std::sqrt(x_val * x_val + y_val * y_val);
     96 
     97   // Check if the controller is outside a circular dead zone.
     98   if (real_magnitude > deadzone) {
     99     // Clip the magnitude at its expected maximum value.
    100     float magnitude = std::min(32767.0f, real_magnitude);
    101 
    102     // Adjust magnitude relative to the end of the dead zone.
    103     magnitude -= deadzone;
    104 
    105     // Normalize the magnitude with respect to its expected range giving a
    106     // magnitude value of 0.0 to 1.0
    107     float ratio = (magnitude / (32767 - deadzone)) / real_magnitude;
    108 
    109     // Y is negated because xbox controllers have an opposite sign from
    110     // the 'standard controller' recommendations.
    111     *x_out = x_val * ratio;
    112     *y_out = -y_val * ratio;
    113   } else {
    114     // If the controller is in the deadzone zero out the magnitude.
    115     *x_out = *y_out = 0.0f;
    116   }
    117 }
    118 
    119 float NormalizeTrigger(uint8 value) {
    120   return value < kTriggerDeadzone ? 0 :
    121       static_cast<float>(value - kTriggerDeadzone) /
    122           (std::numeric_limits<uint8>::max() - kTriggerDeadzone);
    123 }
    124 
    125 void NormalizeButtonData(const ButtonData& data,
    126                          XboxController::Data* normalized_data) {
    127   normalized_data->buttons[0] = data.a;
    128   normalized_data->buttons[1] = data.b;
    129   normalized_data->buttons[2] = data.x;
    130   normalized_data->buttons[3] = data.y;
    131   normalized_data->buttons[4] = data.bumper_left;
    132   normalized_data->buttons[5] = data.bumper_right;
    133   normalized_data->buttons[6] = data.back;
    134   normalized_data->buttons[7] = data.start;
    135   normalized_data->buttons[8] = data.stick_left_click;
    136   normalized_data->buttons[9] = data.stick_right_click;
    137   normalized_data->buttons[10] = data.dpad_up;
    138   normalized_data->buttons[11] = data.dpad_down;
    139   normalized_data->buttons[12] = data.dpad_left;
    140   normalized_data->buttons[13] = data.dpad_right;
    141   normalized_data->buttons[14] = data.guide;
    142   normalized_data->triggers[0] = NormalizeTrigger(data.trigger_left);
    143   normalized_data->triggers[1] = NormalizeTrigger(data.trigger_right);
    144   NormalizeAxis(data.stick_left_x,
    145                 data.stick_left_y,
    146                 kLeftThumbDeadzone,
    147                 &normalized_data->axes[0],
    148                 &normalized_data->axes[1]);
    149   NormalizeAxis(data.stick_right_x,
    150                 data.stick_right_y,
    151                 kRightThumbDeadzone,
    152                 &normalized_data->axes[2],
    153                 &normalized_data->axes[3]);
    154 }
    155 
    156 }  // namespace
    157 
    158 XboxController::XboxController(Delegate* delegate)
    159     : device_(NULL),
    160       interface_(NULL),
    161       device_is_open_(false),
    162       interface_is_open_(false),
    163       read_buffer_size_(0),
    164       led_pattern_(LED_NUM_PATTERNS),
    165       location_id_(0),
    166       delegate_(delegate) {
    167 }
    168 
    169 XboxController::~XboxController() {
    170   if (source_)
    171     CFRunLoopSourceInvalidate(source_);
    172   if (interface_ && interface_is_open_)
    173     (*interface_)->USBInterfaceClose(interface_);
    174   if (device_ && device_is_open_)
    175     (*device_)->USBDeviceClose(device_);
    176 }
    177 
    178 bool XboxController::OpenDevice(io_service_t service) {
    179   IOCFPlugInInterface **plugin;
    180   SInt32 score;  // Unused, but required for IOCreatePlugInInterfaceForService.
    181   kern_return_t kr =
    182       IOCreatePlugInInterfaceForService(service,
    183                                         kIOUSBDeviceUserClientTypeID,
    184                                         kIOCFPlugInInterfaceID,
    185                                         &plugin,
    186                                         &score);
    187   if (kr != KERN_SUCCESS)
    188     return false;
    189   base::mac::ScopedIOPluginInterface<IOCFPlugInInterface> plugin_ref(plugin);
    190 
    191   HRESULT res =
    192       (*plugin)->QueryInterface(plugin,
    193                                 CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID320),
    194                                 (LPVOID *)&device_);
    195   if (!SUCCEEDED(res) || !device_)
    196     return false;
    197 
    198   UInt16 vendor_id;
    199   kr = (*device_)->GetDeviceVendor(device_, &vendor_id);
    200   if (kr != KERN_SUCCESS)
    201     return false;
    202   UInt16 product_id;
    203   kr = (*device_)->GetDeviceProduct(device_, &product_id);
    204   if (kr != KERN_SUCCESS)
    205     return false;
    206   if (vendor_id != kVendorMicrosoft || product_id != kProduct360Controller)
    207     return false;
    208 
    209   // Open the device and configure it.
    210   kr = (*device_)->USBDeviceOpen(device_);
    211   if (kr != KERN_SUCCESS)
    212     return false;
    213   device_is_open_ = true;
    214 
    215   // Xbox controllers have one configuration option which has configuration
    216   // value 1. Try to set it and fail if it couldn't be configured.
    217   IOUSBConfigurationDescriptorPtr config_desc;
    218   kr = (*device_)->GetConfigurationDescriptorPtr(device_, 0, &config_desc);
    219   if (kr != KERN_SUCCESS)
    220     return false;
    221   kr = (*device_)->SetConfiguration(device_, config_desc->bConfigurationValue);
    222   if (kr != KERN_SUCCESS)
    223     return false;
    224 
    225   // The device has 4 interfaces. They are as follows:
    226   // Protocol 1:
    227   //  - Endpoint 1 (in) : Controller events, including button presses.
    228   //  - Endpoint 2 (out): Rumble pack and LED control
    229   // Protocol 2 has a single endpoint to read from a connected ChatPad device.
    230   // Protocol 3 is used by a connected headset device.
    231   // The device also has an interface on subclass 253, protocol 10 with no
    232   // endpoints.  It is unused.
    233   //
    234   // We don't currently support the ChatPad or headset, so protocol 1 is the
    235   // only protocol we care about.
    236   //
    237   // For more detail, see
    238   // https://github.com/Grumbel/xboxdrv/blob/master/PROTOCOL
    239   IOUSBFindInterfaceRequest request;
    240   request.bInterfaceClass = 255;
    241   request.bInterfaceSubClass = 93;
    242   request.bInterfaceProtocol = 1;
    243   request.bAlternateSetting = kIOUSBFindInterfaceDontCare;
    244   io_iterator_t iter;
    245   kr = (*device_)->CreateInterfaceIterator(device_, &request, &iter);
    246   if (kr != KERN_SUCCESS)
    247     return false;
    248   base::mac::ScopedIOObject<io_iterator_t> iter_ref(iter);
    249 
    250   // There should be exactly one USB interface which matches the requested
    251   // settings.
    252   io_service_t usb_interface = IOIteratorNext(iter);
    253   if (!usb_interface)
    254     return false;
    255 
    256   // We need to make an InterfaceInterface to communicate with the device
    257   // endpoint. This is the same process as earlier: first make a
    258   // PluginInterface from the io_service then make the InterfaceInterface from
    259   // that.
    260   IOCFPlugInInterface **plugin_interface;
    261   kr = IOCreatePlugInInterfaceForService(usb_interface,
    262                                          kIOUSBInterfaceUserClientTypeID,
    263                                          kIOCFPlugInInterfaceID,
    264                                          &plugin_interface,
    265                                          &score);
    266   if (kr != KERN_SUCCESS || !plugin_interface)
    267     return false;
    268   base::mac::ScopedIOPluginInterface<IOCFPlugInInterface> interface_ref(
    269       plugin_interface);
    270 
    271   // Release the USB interface, and any subsequent interfaces returned by the
    272   // iterator. (There shouldn't be any, but in case a future device does
    273   // contain more interfaces, this will serve to avoid memory leaks.)
    274   do {
    275     IOObjectRelease(usb_interface);
    276   } while ((usb_interface = IOIteratorNext(iter)));
    277 
    278   // Actually create the interface.
    279   res = (*plugin_interface)->QueryInterface(
    280       plugin_interface,
    281       CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID300),
    282       (LPVOID *)&interface_);
    283 
    284   if (!SUCCEEDED(res) || !interface_)
    285     return false;
    286 
    287   // Actually open the interface.
    288   kr = (*interface_)->USBInterfaceOpen(interface_);
    289   if (kr != KERN_SUCCESS)
    290     return false;
    291   interface_is_open_ = true;
    292 
    293   CFRunLoopSourceRef source_ref;
    294   kr = (*interface_)->CreateInterfaceAsyncEventSource(interface_, &source_ref);
    295   if (kr != KERN_SUCCESS || !source_ref)
    296     return false;
    297   source_.reset(source_ref);
    298   CFRunLoopAddSource(CFRunLoopGetCurrent(), source_, kCFRunLoopDefaultMode);
    299 
    300   // The interface should have two pipes. Pipe 1 with direction kUSBIn and pipe
    301   // 2 with direction kUSBOut. Both pipes should have type kUSBInterrupt.
    302   uint8 num_endpoints;
    303   kr = (*interface_)->GetNumEndpoints(interface_, &num_endpoints);
    304   if (kr != KERN_SUCCESS || num_endpoints < 2)
    305     return false;
    306 
    307   for (int i = 1; i <= 2; i++) {
    308     uint8 direction;
    309     uint8 number;
    310     uint8 transfer_type;
    311     uint16 max_packet_size;
    312     uint8 interval;
    313 
    314     kr = (*interface_)->GetPipeProperties(interface_,
    315                                           i,
    316                                           &direction,
    317                                           &number,
    318                                           &transfer_type,
    319                                           &max_packet_size,
    320                                           &interval);
    321     if (kr != KERN_SUCCESS || transfer_type != kUSBInterrupt)
    322       return false;
    323     if (i == kReadEndpoint) {
    324       if (direction != kUSBIn)
    325         return false;
    326       if (max_packet_size > 32)
    327         return false;
    328       read_buffer_.reset(new uint8[max_packet_size]);
    329       read_buffer_size_ = max_packet_size;
    330       QueueRead();
    331     } else if (i == kControlEndpoint) {
    332       if (direction != kUSBOut)
    333         return false;
    334     }
    335   }
    336 
    337   // The location ID is unique per controller, and can be used to track
    338   // controllers through reconnections (though if a controller is detached from
    339   // one USB hub and attached to another, the location ID will change).
    340   kr = (*device_)->GetLocationID(device_, &location_id_);
    341   if (kr != KERN_SUCCESS)
    342     return false;
    343 
    344   return true;
    345 }
    346 
    347 void XboxController::SetLEDPattern(LEDPattern pattern) {
    348   led_pattern_ = pattern;
    349   const UInt8 length = 3;
    350 
    351   // This buffer will be released in WriteComplete when WritePipeAsync
    352   // finishes.
    353   UInt8* buffer = new UInt8[length];
    354   buffer[0] = static_cast<UInt8>(CONTROL_MESSAGE_SET_LED);
    355   buffer[1] = length;
    356   buffer[2] = static_cast<UInt8>(pattern);
    357   kern_return_t kr = (*interface_)->WritePipeAsync(interface_,
    358                                                    kControlEndpoint,
    359                                                    buffer,
    360                                                    (UInt32)length,
    361                                                    WriteComplete,
    362                                                    buffer);
    363   if (kr != KERN_SUCCESS) {
    364     delete[] buffer;
    365     IOError();
    366     return;
    367   }
    368 }
    369 
    370 int XboxController::GetVendorId() const {
    371   return kVendorMicrosoft;
    372 }
    373 
    374 int XboxController::GetProductId() const {
    375   return kProduct360Controller;
    376 }
    377 
    378 void XboxController::WriteComplete(void* context, IOReturn result, void* arg0) {
    379   UInt8* buffer = static_cast<UInt8*>(context);
    380   delete[] buffer;
    381 
    382   // Ignoring any errors sending data, because they will usually only occur
    383   // when the device is disconnected, in which case it really doesn't matter if
    384   // the data got to the controller or not.
    385   if (result != kIOReturnSuccess)
    386     return;
    387 }
    388 
    389 void XboxController::GotData(void* context, IOReturn result, void* arg0) {
    390   size_t bytes_read = reinterpret_cast<size_t>(arg0);
    391   XboxController* controller = static_cast<XboxController*>(context);
    392 
    393   if (result != kIOReturnSuccess) {
    394     // This will happen if the device was disconnected. The gamepad has
    395     // probably been destroyed by a meteorite.
    396     controller->IOError();
    397     return;
    398   }
    399 
    400   controller->ProcessPacket(bytes_read);
    401 
    402   // Queue up another read.
    403   controller->QueueRead();
    404 }
    405 
    406 void XboxController::ProcessPacket(size_t length) {
    407   if (length < 2) return;
    408   DCHECK(length <= read_buffer_size_);
    409   if (length > read_buffer_size_) {
    410     IOError();
    411     return;
    412   }
    413   uint8* buffer = read_buffer_.get();
    414 
    415   if (buffer[1] != length)
    416     // Length in packet doesn't match length reported by USB.
    417     return;
    418 
    419   uint8 type = buffer[0];
    420   buffer += 2;
    421   length -= 2;
    422   switch (type) {
    423     case STATUS_MESSAGE_BUTTONS: {
    424       if (length != sizeof(ButtonData))
    425         return;
    426       ButtonData* data = reinterpret_cast<ButtonData*>(buffer);
    427       Data normalized_data;
    428       NormalizeButtonData(*data, &normalized_data);
    429       delegate_->XboxControllerGotData(this, normalized_data);
    430       break;
    431     }
    432     case STATUS_MESSAGE_LED:
    433       if (length != 3)
    434         return;
    435       // The controller sends one of these messages every time the LED pattern
    436       // is set, as well as once when it is plugged in.
    437       if (led_pattern_ == LED_NUM_PATTERNS && buffer[0] < LED_NUM_PATTERNS)
    438         led_pattern_ = static_cast<LEDPattern>(buffer[0]);
    439       break;
    440     default:
    441       // Unknown packet: ignore!
    442       break;
    443   }
    444 }
    445 
    446 void XboxController::QueueRead() {
    447   kern_return_t kr = (*interface_)->ReadPipeAsync(interface_,
    448                                                   kReadEndpoint,
    449                                                   read_buffer_.get(),
    450                                                   read_buffer_size_,
    451                                                   GotData,
    452                                                   this);
    453   if (kr != KERN_SUCCESS)
    454     IOError();
    455 }
    456 
    457 void XboxController::IOError() {
    458   delegate_->XboxControllerError(this);
    459 }
    460 
    461 //-----------------------------------------------------------------------------
    462 
    463 XboxDataFetcher::XboxDataFetcher(Delegate* delegate)
    464     : delegate_(delegate),
    465       listening_(false),
    466       source_(NULL),
    467       port_(NULL) {
    468 }
    469 
    470 XboxDataFetcher::~XboxDataFetcher() {
    471   while (!controllers_.empty()) {
    472     RemoveController(*controllers_.begin());
    473   }
    474   UnregisterFromNotifications();
    475 }
    476 
    477 void XboxDataFetcher::DeviceAdded(void* context, io_iterator_t iterator) {
    478   DCHECK(context);
    479   XboxDataFetcher* fetcher = static_cast<XboxDataFetcher*>(context);
    480   io_service_t ref;
    481   while ((ref = IOIteratorNext(iterator))) {
    482     base::mac::ScopedIOObject<io_service_t> scoped_ref(ref);
    483     XboxController* controller = new XboxController(fetcher);
    484     if (controller->OpenDevice(ref)) {
    485       fetcher->AddController(controller);
    486     } else {
    487       delete controller;
    488     }
    489   }
    490 }
    491 
    492 void XboxDataFetcher::DeviceRemoved(void* context, io_iterator_t iterator) {
    493   DCHECK(context);
    494   XboxDataFetcher* fetcher = static_cast<XboxDataFetcher*>(context);
    495   io_service_t ref;
    496   while ((ref = IOIteratorNext(iterator))) {
    497     base::mac::ScopedIOObject<io_service_t> scoped_ref(ref);
    498     base::ScopedCFTypeRef<CFNumberRef> number(
    499         base::mac::CFCastStrict<CFNumberRef>(
    500             IORegistryEntryCreateCFProperty(ref,
    501                                             CFSTR(kUSBDevicePropertyLocationID),
    502                                             kCFAllocatorDefault,
    503                                             kNilOptions)));
    504     UInt32 location_id = 0;
    505     CFNumberGetValue(number, kCFNumberSInt32Type, &location_id);
    506     fetcher->RemoveControllerByLocationID(location_id);
    507   }
    508 }
    509 
    510 bool XboxDataFetcher::RegisterForNotifications() {
    511   if (listening_)
    512     return true;
    513   base::ScopedCFTypeRef<CFNumberRef> vendor_cf(CFNumberCreate(
    514       kCFAllocatorDefault, kCFNumberSInt32Type, &kVendorMicrosoft));
    515   base::ScopedCFTypeRef<CFNumberRef> product_cf(CFNumberCreate(
    516       kCFAllocatorDefault, kCFNumberSInt32Type, &kProduct360Controller));
    517   base::ScopedCFTypeRef<CFMutableDictionaryRef> matching_dict(
    518       IOServiceMatching(kIOUSBDeviceClassName));
    519   if (!matching_dict)
    520     return false;
    521   CFDictionarySetValue(matching_dict, CFSTR(kUSBVendorID), vendor_cf);
    522   CFDictionarySetValue(matching_dict, CFSTR(kUSBProductID), product_cf);
    523   port_ = IONotificationPortCreate(kIOMasterPortDefault);
    524   if (!port_)
    525     return false;
    526   source_ = IONotificationPortGetRunLoopSource(port_);
    527   if (!source_)
    528     return false;
    529   CFRunLoopAddSource(CFRunLoopGetCurrent(), source_, kCFRunLoopDefaultMode);
    530 
    531   listening_ = true;
    532 
    533   // IOServiceAddMatchingNotification() releases the dictionary when it's done.
    534   // Retain it before each call to IOServiceAddMatchingNotification to keep
    535   // things balanced.
    536   CFRetain(matching_dict);
    537   io_iterator_t device_added_iter;
    538   IOReturn ret;
    539   ret = IOServiceAddMatchingNotification(port_,
    540                                          kIOFirstMatchNotification,
    541                                          matching_dict,
    542                                          DeviceAdded,
    543                                          this,
    544                                          &device_added_iter);
    545   device_added_iter_.reset(device_added_iter);
    546   if (ret != kIOReturnSuccess) {
    547     LOG(ERROR) << "Error listening for Xbox controller add events: " << ret;
    548     return false;
    549   }
    550   DeviceAdded(this, device_added_iter_.get());
    551 
    552   CFRetain(matching_dict);
    553   io_iterator_t device_removed_iter;
    554   ret = IOServiceAddMatchingNotification(port_,
    555                                          kIOTerminatedNotification,
    556                                          matching_dict,
    557                                          DeviceRemoved,
    558                                          this,
    559                                          &device_removed_iter);
    560   device_removed_iter_.reset(device_removed_iter);
    561   if (ret != kIOReturnSuccess) {
    562     LOG(ERROR) << "Error listening for Xbox controller remove events: " << ret;
    563     return false;
    564   }
    565   DeviceRemoved(this, device_removed_iter_.get());
    566   return true;
    567 }
    568 
    569 void XboxDataFetcher::UnregisterFromNotifications() {
    570   if (!listening_)
    571     return;
    572   listening_ = false;
    573   if (source_)
    574     CFRunLoopSourceInvalidate(source_);
    575   if (port_)
    576     IONotificationPortDestroy(port_);
    577   port_ = NULL;
    578 }
    579 
    580 XboxController* XboxDataFetcher::ControllerForLocation(UInt32 location_id) {
    581   for (std::set<XboxController*>::iterator i = controllers_.begin();
    582        i != controllers_.end();
    583        ++i) {
    584     if ((*i)->location_id() == location_id)
    585       return *i;
    586   }
    587   return NULL;
    588 }
    589 
    590 void XboxDataFetcher::AddController(XboxController* controller) {
    591   DCHECK(!ControllerForLocation(controller->location_id()))
    592       << "Controller with location ID " << controller->location_id()
    593       << " already exists in the set of controllers.";
    594   controllers_.insert(controller);
    595   delegate_->XboxDeviceAdd(controller);
    596 }
    597 
    598 void XboxDataFetcher::RemoveController(XboxController* controller) {
    599   delegate_->XboxDeviceRemove(controller);
    600   controllers_.erase(controller);
    601   delete controller;
    602 }
    603 
    604 void XboxDataFetcher::RemoveControllerByLocationID(uint32 location_id) {
    605   XboxController* controller = NULL;
    606   for (std::set<XboxController*>::iterator i = controllers_.begin();
    607        i != controllers_.end();
    608        ++i) {
    609     if ((*i)->location_id() == location_id) {
    610       controller = *i;
    611       break;
    612     }
    613   }
    614   if (controller)
    615     RemoveController(controller);
    616 }
    617 
    618 void XboxDataFetcher::XboxControllerGotData(XboxController* controller,
    619                                             const XboxController::Data& data) {
    620   delegate_->XboxValueChanged(controller, data);
    621 }
    622 
    623 void XboxDataFetcher::XboxControllerError(XboxController* controller) {
    624   RemoveController(controller);
    625 }
    626