1 // Copyright (c) 2012 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.
5 #include "content/browser/gamepad/gamepad_platform_data_fetcher_mac.h"
7 #include "base/mac/foundation_util.h"
8 #include "base/mac/scoped_nsobject.h"
9 #include "base/strings/string16.h"
10 #include "base/strings/string_util.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "base/time/time.h"
14 #import <Foundation/Foundation.h>
15 #include <IOKit/hid/IOHIDKeys.h>
17 using blink::WebGamepad;
18 using blink::WebGamepads;
24 void CopyNSStringAsUTF16LittleEndian(
25 NSString* src, blink::WebUChar* dest, size_t dest_len) {
26 NSData* as16 = [src dataUsingEncoding:NSUTF16LittleEndianStringEncoding];
27 memset(dest, 0, dest_len);
28 [as16 getBytes:dest length:dest_len - sizeof(blink::WebUChar)];
31 NSDictionary* DeviceMatching(uint32_t usage_page, uint32_t usage) {
32 return [NSDictionary dictionaryWithObjectsAndKeys:
33 [NSNumber numberWithUnsignedInt:usage_page],
34 base::mac::CFToNSCast(CFSTR(kIOHIDDeviceUsagePageKey)),
35 [NSNumber numberWithUnsignedInt:usage],
36 base::mac::CFToNSCast(CFSTR(kIOHIDDeviceUsageKey)),
40 float NormalizeAxis(CFIndex value, CFIndex min, CFIndex max) {
41 return (2.f * (value - min) / static_cast<float>(max - min)) - 1.f;
44 // http://www.usb.org/developers/hidpage
45 const uint32_t kGenericDesktopUsagePage = 0x01;
46 const uint32_t kGameControlsUsagePage = 0x05;
47 const uint32_t kButtonUsagePage = 0x09;
48 const uint32_t kJoystickUsageNumber = 0x04;
49 const uint32_t kGameUsageNumber = 0x05;
50 const uint32_t kMultiAxisUsageNumber = 0x08;
51 const uint32_t kAxisMinimumUsageNumber = 0x30;
55 GamepadPlatformDataFetcherMac::GamepadPlatformDataFetcherMac()
56 : enabled_(true), paused_(false) {
57 memset(associated_, 0, sizeof(associated_));
59 xbox_fetcher_.reset(new XboxDataFetcher(this));
60 if (!xbox_fetcher_->RegisterForNotifications())
61 xbox_fetcher_.reset();
63 hid_manager_ref_.reset(IOHIDManagerCreate(kCFAllocatorDefault,
64 kIOHIDOptionsTypeNone));
65 if (CFGetTypeID(hid_manager_ref_) != IOHIDManagerGetTypeID()) {
70 base::scoped_nsobject<NSArray> criteria([[NSArray alloc] initWithObjects:
71 DeviceMatching(kGenericDesktopUsagePage, kJoystickUsageNumber),
72 DeviceMatching(kGenericDesktopUsagePage, kGameUsageNumber),
73 DeviceMatching(kGenericDesktopUsagePage, kMultiAxisUsageNumber),
75 IOHIDManagerSetDeviceMatchingMultiple(
77 base::mac::NSToCFCast(criteria));
79 RegisterForNotifications();
82 void GamepadPlatformDataFetcherMac::RegisterForNotifications() {
83 // Register for plug/unplug notifications.
84 IOHIDManagerRegisterDeviceMatchingCallback(
88 IOHIDManagerRegisterDeviceRemovalCallback(
93 // Register for value change notifications.
94 IOHIDManagerRegisterInputValueCallback(
99 IOHIDManagerScheduleWithRunLoop(
102 kCFRunLoopDefaultMode);
104 enabled_ = IOHIDManagerOpen(hid_manager_ref_,
105 kIOHIDOptionsTypeNone) == kIOReturnSuccess;
108 xbox_fetcher_->RegisterForNotifications();
111 void GamepadPlatformDataFetcherMac::UnregisterFromNotifications() {
112 IOHIDManagerUnscheduleFromRunLoop(
114 CFRunLoopGetCurrent(),
115 kCFRunLoopDefaultMode);
116 IOHIDManagerClose(hid_manager_ref_, kIOHIDOptionsTypeNone);
118 xbox_fetcher_->UnregisterFromNotifications();
121 void GamepadPlatformDataFetcherMac::PauseHint(bool pause) {
125 GamepadPlatformDataFetcherMac::~GamepadPlatformDataFetcherMac() {
126 UnregisterFromNotifications();
129 GamepadPlatformDataFetcherMac*
130 GamepadPlatformDataFetcherMac::InstanceFromContext(void* context) {
131 return reinterpret_cast<GamepadPlatformDataFetcherMac*>(context);
134 void GamepadPlatformDataFetcherMac::DeviceAddCallback(void* context,
137 IOHIDDeviceRef ref) {
138 InstanceFromContext(context)->DeviceAdd(ref);
141 void GamepadPlatformDataFetcherMac::DeviceRemoveCallback(void* context,
144 IOHIDDeviceRef ref) {
145 InstanceFromContext(context)->DeviceRemove(ref);
148 void GamepadPlatformDataFetcherMac::ValueChangedCallback(void* context,
152 InstanceFromContext(context)->ValueChanged(ref);
155 bool GamepadPlatformDataFetcherMac::AddButtonsAndAxes(NSArray* elements,
157 WebGamepad& pad = data_.items[slot];
158 AssociatedData& associated = associated_[slot];
159 CHECK(!associated.is_xbox);
162 pad.buttonsLength = 0;
164 memset(pad.axes, 0, sizeof(pad.axes));
165 memset(pad.buttons, 0, sizeof(pad.buttons));
167 bool mapped_all_axes = true;
169 for (id elem in elements) {
170 IOHIDElementRef element = reinterpret_cast<IOHIDElementRef>(elem);
171 uint32_t usage_page = IOHIDElementGetUsagePage(element);
172 uint32_t usage = IOHIDElementGetUsage(element);
173 if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Button &&
174 usage_page == kButtonUsagePage) {
175 uint32_t button_index = usage - 1;
176 if (button_index < WebGamepad::buttonsLengthCap) {
177 associated.hid.button_elements[button_index] = element;
178 pad.buttonsLength = std::max(pad.buttonsLength, button_index + 1);
181 else if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Misc) {
182 uint32_t axis_index = usage - kAxisMinimumUsageNumber;
183 if (axis_index < WebGamepad::axesLengthCap) {
184 associated.hid.axis_minimums[axis_index] =
185 IOHIDElementGetLogicalMin(element);
186 associated.hid.axis_maximums[axis_index] =
187 IOHIDElementGetLogicalMax(element);
188 associated.hid.axis_elements[axis_index] = element;
189 pad.axesLength = std::max(pad.axesLength, axis_index + 1);
191 mapped_all_axes = false;
196 if (!mapped_all_axes) {
197 // For axes who's usage puts them outside the standard axesLengthCap range.
198 uint32_t next_index = 0;
199 for (id elem in elements) {
200 IOHIDElementRef element = reinterpret_cast<IOHIDElementRef>(elem);
201 uint32_t usage_page = IOHIDElementGetUsagePage(element);
202 uint32_t usage = IOHIDElementGetUsage(element);
203 if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Misc &&
204 usage - kAxisMinimumUsageNumber >= WebGamepad::axesLengthCap &&
205 usage_page <= kGameControlsUsagePage) {
206 for (; next_index < WebGamepad::axesLengthCap; ++next_index) {
207 if (associated.hid.axis_elements[next_index] == NULL)
210 if (next_index < WebGamepad::axesLengthCap) {
211 associated.hid.axis_minimums[next_index] =
212 IOHIDElementGetLogicalMin(element);
213 associated.hid.axis_maximums[next_index] =
214 IOHIDElementGetLogicalMax(element);
215 associated.hid.axis_elements[next_index] = element;
216 pad.axesLength = std::max(pad.axesLength, next_index + 1);
220 if (next_index >= WebGamepad::axesLengthCap)
225 return (pad.axesLength > 0 || pad.buttonsLength > 0);
228 size_t GamepadPlatformDataFetcherMac::GetEmptySlot() {
229 // Find a free slot for this device.
230 for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
231 if (!data_.items[slot].connected)
234 return WebGamepads::itemsLengthCap;
237 size_t GamepadPlatformDataFetcherMac::GetSlotForDevice(IOHIDDeviceRef device) {
238 for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
239 // If we already have this device, and it's already connected, don't do
241 if (data_.items[slot].connected &&
242 !associated_[slot].is_xbox &&
243 associated_[slot].hid.device_ref == device)
244 return WebGamepads::itemsLengthCap;
246 return GetEmptySlot();
249 size_t GamepadPlatformDataFetcherMac::GetSlotForXboxDevice(
250 XboxController* device) {
251 for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
252 if (associated_[slot].is_xbox &&
253 associated_[slot].xbox.location_id == device->location_id()) {
254 if (data_.items[slot].connected) {
255 // The device is already connected. No idea why we got a second "device
256 // added" call, but let's not add it twice.
257 DCHECK_EQ(associated_[slot].xbox.device, device);
258 return WebGamepads::itemsLengthCap;
260 // A device with the same location ID was previously connected, so put
261 // it in the same slot.
266 return GetEmptySlot();
269 void GamepadPlatformDataFetcherMac::DeviceAdd(IOHIDDeviceRef device) {
270 using base::mac::CFToNSCast;
271 using base::mac::CFCastStrict;
276 // Find an index for this device.
277 size_t slot = GetSlotForDevice(device);
279 // We can't handle this many connected devices.
280 if (slot == WebGamepads::itemsLengthCap)
283 // Clear some state that may have been left behind by previous gamepads
284 memset(&associated_[slot], 0, sizeof(AssociatedData));
286 NSNumber* vendor_id = CFToNSCast(CFCastStrict<CFNumberRef>(
287 IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey))));
288 NSNumber* product_id = CFToNSCast(CFCastStrict<CFNumberRef>(
289 IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey))));
290 NSString* product = CFToNSCast(CFCastStrict<CFStringRef>(
291 IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey))));
292 int vendor_int = [vendor_id intValue];
293 int product_int = [product_id intValue];
295 char vendor_as_str[5], product_as_str[5];
296 snprintf(vendor_as_str, sizeof(vendor_as_str), "%04x", vendor_int);
297 snprintf(product_as_str, sizeof(product_as_str), "%04x", product_int);
298 associated_[slot].hid.mapper =
299 GetGamepadStandardMappingFunction(vendor_as_str, product_as_str);
301 NSString* ident = [NSString stringWithFormat:
302 @"%@ (%sVendor: %04x Product: %04x)",
304 associated_[slot].hid.mapper ? "STANDARD GAMEPAD " : "",
307 CopyNSStringAsUTF16LittleEndian(
309 data_.items[slot].id,
310 sizeof(data_.items[slot].id));
312 if (associated_[slot].hid.mapper) {
313 CopyNSStringAsUTF16LittleEndian(
315 data_.items[slot].mapping,
316 sizeof(data_.items[slot].mapping));
318 data_.items[slot].mapping[0] = 0;
321 base::ScopedCFTypeRef<CFArrayRef> elements(
322 IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone));
324 if (!AddButtonsAndAxes(CFToNSCast(elements), slot))
327 associated_[slot].hid.device_ref = device;
328 data_.items[slot].connected = true;
329 if (slot >= data_.length)
330 data_.length = slot + 1;
333 void GamepadPlatformDataFetcherMac::DeviceRemove(IOHIDDeviceRef device) {
337 // Find the index for this device.
339 for (slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
340 if (data_.items[slot].connected &&
341 !associated_[slot].is_xbox &&
342 associated_[slot].hid.device_ref == device)
345 DCHECK(slot < WebGamepads::itemsLengthCap);
346 // Leave associated device_ref so that it will be reconnected in the same
347 // location. Simply mark it as disconnected.
348 data_.items[slot].connected = false;
351 void GamepadPlatformDataFetcherMac::ValueChanged(IOHIDValueRef value) {
352 if (!enabled_ || paused_)
355 IOHIDElementRef element = IOHIDValueGetElement(value);
356 IOHIDDeviceRef device = IOHIDElementGetDevice(element);
360 for (slot = 0; slot < data_.length; ++slot) {
361 if (data_.items[slot].connected &&
362 !associated_[slot].is_xbox &&
363 associated_[slot].hid.device_ref == device)
366 if (slot == data_.length)
369 WebGamepad& pad = data_.items[slot];
370 AssociatedData& associated = associated_[slot];
372 uint32_t value_length = IOHIDValueGetLength(value);
373 if (value_length > 4) {
374 // Workaround for bizarre issue with PS3 controllers that try to return
375 // massive (30+ byte) values and crash IOHIDValueGetIntegerValue
379 // Find and fill in the associated button event, if any.
380 for (size_t i = 0; i < pad.buttonsLength; ++i) {
381 if (associated.hid.button_elements[i] == element) {
382 pad.buttons[i].pressed = IOHIDValueGetIntegerValue(value);
383 pad.buttons[i].value = pad.buttons[i].pressed ? 1.f : 0.f;
384 pad.timestamp = std::max(pad.timestamp, IOHIDValueGetTimeStamp(value));
389 // Find and fill in the associated axis event, if any.
390 for (size_t i = 0; i < pad.axesLength; ++i) {
391 if (associated.hid.axis_elements[i] == element) {
392 pad.axes[i] = NormalizeAxis(IOHIDValueGetIntegerValue(value),
393 associated.hid.axis_minimums[i],
394 associated.hid.axis_maximums[i]);
395 pad.timestamp = std::max(pad.timestamp, IOHIDValueGetTimeStamp(value));
401 void GamepadPlatformDataFetcherMac::XboxDeviceAdd(XboxController* device) {
405 size_t slot = GetSlotForXboxDevice(device);
407 // We can't handle this many connected devices.
408 if (slot == WebGamepads::itemsLengthCap)
411 device->SetLEDPattern(
412 (XboxController::LEDPattern)(XboxController::LED_FLASH_TOP_LEFT + slot));
415 [NSString stringWithFormat:
416 @"%@ (STANDARD GAMEPAD Vendor: %04x Product: %04x)",
417 device->GetControllerType() == XboxController::XBOX_360_CONTROLLER
418 ? @"Xbox 360 Controller"
419 : @"Xbox One Controller",
420 device->GetProductId(), device->GetVendorId()];
421 CopyNSStringAsUTF16LittleEndian(
423 data_.items[slot].id,
424 sizeof(data_.items[slot].id));
426 CopyNSStringAsUTF16LittleEndian(
428 data_.items[slot].mapping,
429 sizeof(data_.items[slot].mapping));
431 associated_[slot].is_xbox = true;
432 associated_[slot].xbox.device = device;
433 associated_[slot].xbox.location_id = device->location_id();
434 data_.items[slot].connected = true;
435 data_.items[slot].axesLength = 4;
436 data_.items[slot].buttonsLength = 17;
437 data_.items[slot].timestamp = 0;
438 if (slot >= data_.length)
439 data_.length = slot + 1;
442 void GamepadPlatformDataFetcherMac::XboxDeviceRemove(XboxController* device) {
446 // Find the index for this device.
448 for (slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
449 if (data_.items[slot].connected &&
450 associated_[slot].is_xbox &&
451 associated_[slot].xbox.device == device)
454 DCHECK(slot < WebGamepads::itemsLengthCap);
455 // Leave associated location id so that the controller will be reconnected in
456 // the same slot if it is plugged in again. Simply mark it as disconnected.
457 data_.items[slot].connected = false;
460 void GamepadPlatformDataFetcherMac::XboxValueChanged(
461 XboxController* device, const XboxController::Data& data) {
464 for (slot = 0; slot < data_.length; ++slot) {
465 if (data_.items[slot].connected &&
466 associated_[slot].is_xbox &&
467 associated_[slot].xbox.device == device)
470 if (slot == data_.length)
473 WebGamepad& pad = data_.items[slot];
475 for (size_t i = 0; i < 6; i++) {
476 pad.buttons[i].pressed = data.buttons[i];
477 pad.buttons[i].value = data.buttons[i] ? 1.0f : 0.0f;
479 pad.buttons[6].pressed = data.triggers[0] > kDefaultButtonPressedThreshold;
480 pad.buttons[6].value = data.triggers[0];
481 pad.buttons[7].pressed = data.triggers[1] > kDefaultButtonPressedThreshold;
482 pad.buttons[7].value = data.triggers[1];
483 for (size_t i = 8; i < 17; i++) {
484 pad.buttons[i].pressed = data.buttons[i - 2];
485 pad.buttons[i].value = data.buttons[i - 2] ? 1.0f : 0.0f;
487 for (size_t i = 0; i < arraysize(data.axes); i++) {
488 pad.axes[i] = data.axes[i];
491 pad.timestamp = base::TimeTicks::Now().ToInternalValue();
494 void GamepadPlatformDataFetcherMac::GetGamepadData(WebGamepads* pads, bool) {
495 if (!enabled_ && !xbox_fetcher_) {
500 // Copy to the current state to the output buffer, using the mapping
501 // function, if there is one available.
502 pads->length = WebGamepads::itemsLengthCap;
503 for (size_t i = 0; i < WebGamepads::itemsLengthCap; ++i) {
504 if (!associated_[i].is_xbox && associated_[i].hid.mapper)
505 associated_[i].hid.mapper(data_.items[i], &pads->items[i]);
507 pads->items[i] = data_.items[i];
511 } // namespace content