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 NSDictionary* DeviceMatching(uint32_t usage_page, uint32_t usage) {
25 return [NSDictionary dictionaryWithObjectsAndKeys:
26 [NSNumber numberWithUnsignedInt:usage_page],
27 base::mac::CFToNSCast(CFSTR(kIOHIDDeviceUsagePageKey)),
28 [NSNumber numberWithUnsignedInt:usage],
29 base::mac::CFToNSCast(CFSTR(kIOHIDDeviceUsageKey)),
33 float NormalizeAxis(CFIndex value, CFIndex min, CFIndex max) {
34 return (2.f * (value - min) / static_cast<float>(max - min)) - 1.f;
37 // http://www.usb.org/developers/hidpage
38 const uint32_t kGenericDesktopUsagePage = 0x01;
39 const uint32_t kButtonUsagePage = 0x09;
40 const uint32_t kJoystickUsageNumber = 0x04;
41 const uint32_t kGameUsageNumber = 0x05;
42 const uint32_t kMultiAxisUsageNumber = 0x08;
43 const uint32_t kAxisMinimumUsageNumber = 0x30;
47 GamepadPlatformDataFetcherMac::GamepadPlatformDataFetcherMac()
49 memset(associated_, 0, sizeof(associated_));
51 xbox_fetcher_.reset(new XboxDataFetcher(this));
52 if (!xbox_fetcher_->RegisterForNotifications())
53 xbox_fetcher_.reset();
55 hid_manager_ref_.reset(IOHIDManagerCreate(kCFAllocatorDefault,
56 kIOHIDOptionsTypeNone));
57 if (CFGetTypeID(hid_manager_ref_) != IOHIDManagerGetTypeID()) {
62 base::scoped_nsobject<NSArray> criteria([[NSArray alloc] initWithObjects:
63 DeviceMatching(kGenericDesktopUsagePage, kJoystickUsageNumber),
64 DeviceMatching(kGenericDesktopUsagePage, kGameUsageNumber),
65 DeviceMatching(kGenericDesktopUsagePage, kMultiAxisUsageNumber),
67 IOHIDManagerSetDeviceMatchingMultiple(
69 base::mac::NSToCFCast(criteria));
71 RegisterForNotifications();
74 void GamepadPlatformDataFetcherMac::RegisterForNotifications() {
75 // Register for plug/unplug notifications.
76 IOHIDManagerRegisterDeviceMatchingCallback(
80 IOHIDManagerRegisterDeviceRemovalCallback(
85 // Register for value change notifications.
86 IOHIDManagerRegisterInputValueCallback(
91 IOHIDManagerScheduleWithRunLoop(
94 kCFRunLoopDefaultMode);
96 enabled_ = IOHIDManagerOpen(hid_manager_ref_,
97 kIOHIDOptionsTypeNone) == kIOReturnSuccess;
100 xbox_fetcher_->RegisterForNotifications();
103 void GamepadPlatformDataFetcherMac::UnregisterFromNotifications() {
104 IOHIDManagerUnscheduleFromRunLoop(
106 CFRunLoopGetCurrent(),
107 kCFRunLoopDefaultMode);
108 IOHIDManagerClose(hid_manager_ref_, kIOHIDOptionsTypeNone);
110 xbox_fetcher_->UnregisterFromNotifications();
113 void GamepadPlatformDataFetcherMac::PauseHint(bool pause) {
115 UnregisterFromNotifications();
117 RegisterForNotifications();
120 GamepadPlatformDataFetcherMac::~GamepadPlatformDataFetcherMac() {
121 UnregisterFromNotifications();
124 GamepadPlatformDataFetcherMac*
125 GamepadPlatformDataFetcherMac::InstanceFromContext(void* context) {
126 return reinterpret_cast<GamepadPlatformDataFetcherMac*>(context);
129 void GamepadPlatformDataFetcherMac::DeviceAddCallback(void* context,
132 IOHIDDeviceRef ref) {
133 InstanceFromContext(context)->DeviceAdd(ref);
136 void GamepadPlatformDataFetcherMac::DeviceRemoveCallback(void* context,
139 IOHIDDeviceRef ref) {
140 InstanceFromContext(context)->DeviceRemove(ref);
143 void GamepadPlatformDataFetcherMac::ValueChangedCallback(void* context,
147 InstanceFromContext(context)->ValueChanged(ref);
150 void GamepadPlatformDataFetcherMac::AddButtonsAndAxes(NSArray* elements,
152 WebGamepad& pad = data_.items[slot];
153 AssociatedData& associated = associated_[slot];
154 CHECK(!associated.is_xbox);
157 pad.buttonsLength = 0;
159 memset(pad.axes, 0, sizeof(pad.axes));
160 memset(pad.buttons, 0, sizeof(pad.buttons));
162 for (id elem in elements) {
163 IOHIDElementRef element = reinterpret_cast<IOHIDElementRef>(elem);
164 uint32_t usagePage = IOHIDElementGetUsagePage(element);
165 uint32_t usage = IOHIDElementGetUsage(element);
166 if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Button &&
167 usagePage == kButtonUsagePage) {
168 uint32_t button_index = usage - 1;
169 if (button_index < WebGamepad::buttonsLengthCap) {
170 associated.hid.button_elements[button_index] = element;
171 pad.buttonsLength = std::max(pad.buttonsLength, button_index + 1);
174 else if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Misc) {
175 uint32_t axis_index = usage - kAxisMinimumUsageNumber;
176 if (axis_index < WebGamepad::axesLengthCap) {
177 associated.hid.axis_minimums[axis_index] =
178 IOHIDElementGetLogicalMin(element);
179 associated.hid.axis_maximums[axis_index] =
180 IOHIDElementGetLogicalMax(element);
181 associated.hid.axis_elements[axis_index] = element;
182 pad.axesLength = std::max(pad.axesLength, axis_index + 1);
188 size_t GamepadPlatformDataFetcherMac::GetEmptySlot() {
189 // Find a free slot for this device.
190 for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
191 if (!data_.items[slot].connected)
194 return WebGamepads::itemsLengthCap;
197 size_t GamepadPlatformDataFetcherMac::GetSlotForDevice(IOHIDDeviceRef device) {
198 for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
199 // If we already have this device, and it's already connected, don't do
201 if (data_.items[slot].connected &&
202 !associated_[slot].is_xbox &&
203 associated_[slot].hid.device_ref == device)
204 return WebGamepads::itemsLengthCap;
206 return GetEmptySlot();
209 size_t GamepadPlatformDataFetcherMac::GetSlotForXboxDevice(
210 XboxController* device) {
211 for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
212 if (associated_[slot].is_xbox &&
213 associated_[slot].xbox.location_id == device->location_id()) {
214 if (data_.items[slot].connected) {
215 // The device is already connected. No idea why we got a second "device
216 // added" call, but let's not add it twice.
217 DCHECK_EQ(associated_[slot].xbox.device, device);
218 return WebGamepads::itemsLengthCap;
220 // A device with the same location ID was previously connected, so put
221 // it in the same slot.
226 return GetEmptySlot();
229 void GamepadPlatformDataFetcherMac::DeviceAdd(IOHIDDeviceRef device) {
230 using base::mac::CFToNSCast;
231 using base::mac::CFCastStrict;
236 // Find an index for this device.
237 size_t slot = GetSlotForDevice(device);
239 // We can't handle this many connected devices.
240 if (slot == WebGamepads::itemsLengthCap)
243 NSNumber* vendor_id = CFToNSCast(CFCastStrict<CFNumberRef>(
244 IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey))));
245 NSNumber* product_id = CFToNSCast(CFCastStrict<CFNumberRef>(
246 IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey))));
247 NSString* product = CFToNSCast(CFCastStrict<CFStringRef>(
248 IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey))));
249 int vendor_int = [vendor_id intValue];
250 int product_int = [product_id intValue];
252 char vendor_as_str[5], product_as_str[5];
253 snprintf(vendor_as_str, sizeof(vendor_as_str), "%04x", vendor_int);
254 snprintf(product_as_str, sizeof(product_as_str), "%04x", product_int);
255 associated_[slot].hid.mapper =
256 GetGamepadStandardMappingFunction(vendor_as_str, product_as_str);
258 NSString* ident = [NSString stringWithFormat:
259 @"%@ (%sVendor: %04x Product: %04x)",
261 associated_[slot].hid.mapper ? "STANDARD GAMEPAD " : "",
264 NSData* as16 = [ident dataUsingEncoding:NSUTF16LittleEndianStringEncoding];
266 const size_t kOutputLengthBytes = sizeof(data_.items[slot].id);
267 memset(&data_.items[slot].id, 0, kOutputLengthBytes);
268 [as16 getBytes:data_.items[slot].id
269 length:kOutputLengthBytes - sizeof(blink::WebUChar)];
271 base::ScopedCFTypeRef<CFArrayRef> elements(
272 IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone));
273 AddButtonsAndAxes(CFToNSCast(elements), slot);
275 associated_[slot].hid.device_ref = device;
276 data_.items[slot].connected = true;
277 if (slot >= data_.length)
278 data_.length = slot + 1;
281 void GamepadPlatformDataFetcherMac::DeviceRemove(IOHIDDeviceRef device) {
285 // Find the index for this device.
287 for (slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
288 if (data_.items[slot].connected &&
289 !associated_[slot].is_xbox &&
290 associated_[slot].hid.device_ref == device)
293 DCHECK(slot < WebGamepads::itemsLengthCap);
294 // Leave associated device_ref so that it will be reconnected in the same
295 // location. Simply mark it as disconnected.
296 data_.items[slot].connected = false;
299 void GamepadPlatformDataFetcherMac::ValueChanged(IOHIDValueRef value) {
303 IOHIDElementRef element = IOHIDValueGetElement(value);
304 IOHIDDeviceRef device = IOHIDElementGetDevice(element);
308 for (slot = 0; slot < data_.length; ++slot) {
309 if (data_.items[slot].connected &&
310 !associated_[slot].is_xbox &&
311 associated_[slot].hid.device_ref == device)
314 if (slot == data_.length)
317 WebGamepad& pad = data_.items[slot];
318 AssociatedData& associated = associated_[slot];
320 // Find and fill in the associated button event, if any.
321 for (size_t i = 0; i < pad.buttonsLength; ++i) {
322 if (associated.hid.button_elements[i] == element) {
323 pad.buttons[i] = IOHIDValueGetIntegerValue(value) ? 1.f : 0.f;
324 pad.timestamp = std::max(pad.timestamp, IOHIDValueGetTimeStamp(value));
329 // Find and fill in the associated axis event, if any.
330 for (size_t i = 0; i < pad.axesLength; ++i) {
331 if (associated.hid.axis_elements[i] == element) {
332 pad.axes[i] = NormalizeAxis(IOHIDValueGetIntegerValue(value),
333 associated.hid.axis_minimums[i],
334 associated.hid.axis_maximums[i]);
335 pad.timestamp = std::max(pad.timestamp, IOHIDValueGetTimeStamp(value));
341 void GamepadPlatformDataFetcherMac::XboxDeviceAdd(XboxController* device) {
345 size_t slot = GetSlotForXboxDevice(device);
347 // We can't handle this many connected devices.
348 if (slot == WebGamepads::itemsLengthCap)
351 device->SetLEDPattern(
352 (XboxController::LEDPattern)(XboxController::LED_FLASH_TOP_LEFT + slot));
355 [NSString stringWithFormat:
356 @"Xbox 360 Controller (STANDARD GAMEPAD Vendor: %04x Product: %04x)",
357 device->GetProductId(), device->GetVendorId()];
358 NSData* as16 = [ident dataUsingEncoding:NSUTF16StringEncoding];
359 const size_t kOutputLengthBytes = sizeof(data_.items[slot].id);
360 memset(&data_.items[slot].id, 0, kOutputLengthBytes);
361 [as16 getBytes:data_.items[slot].id
362 length:kOutputLengthBytes - sizeof(blink::WebUChar)];
364 associated_[slot].is_xbox = true;
365 associated_[slot].xbox.device = device;
366 associated_[slot].xbox.location_id = device->location_id();
367 data_.items[slot].connected = true;
368 data_.items[slot].axesLength = 4;
369 data_.items[slot].buttonsLength = 17;
370 data_.items[slot].timestamp = 0;
371 if (slot >= data_.length)
372 data_.length = slot + 1;
375 void GamepadPlatformDataFetcherMac::XboxDeviceRemove(XboxController* device) {
379 // Find the index for this device.
381 for (slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
382 if (data_.items[slot].connected &&
383 associated_[slot].is_xbox &&
384 associated_[slot].xbox.device == device)
387 DCHECK(slot < WebGamepads::itemsLengthCap);
388 // Leave associated location id so that the controller will be reconnected in
389 // the same slot if it is plugged in again. Simply mark it as disconnected.
390 data_.items[slot].connected = false;
393 void GamepadPlatformDataFetcherMac::XboxValueChanged(
394 XboxController* device, const XboxController::Data& data) {
397 for (slot = 0; slot < data_.length; ++slot) {
398 if (data_.items[slot].connected &&
399 associated_[slot].is_xbox &&
400 associated_[slot].xbox.device == device)
403 if (slot == data_.length)
406 WebGamepad& pad = data_.items[slot];
408 for (size_t i = 0; i < 6; i++) {
409 pad.buttons[i] = data.buttons[i] ? 1.0f : 0.0f;
411 pad.buttons[6] = data.triggers[0];
412 pad.buttons[7] = data.triggers[1];
413 for (size_t i = 8; i < 17; i++) {
414 pad.buttons[i] = data.buttons[i - 2] ? 1.0f : 0.0f;
416 for (size_t i = 0; i < arraysize(data.axes); i++) {
417 pad.axes[i] = data.axes[i];
420 pad.timestamp = base::TimeTicks::Now().ToInternalValue();
423 void GamepadPlatformDataFetcherMac::GetGamepadData(WebGamepads* pads, bool) {
424 if (!enabled_ && !xbox_fetcher_) {
429 // Copy to the current state to the output buffer, using the mapping
430 // function, if there is one available.
431 pads->length = WebGamepads::itemsLengthCap;
432 for (size_t i = 0; i < WebGamepads::itemsLengthCap; ++i) {
433 if (!associated_[i].is_xbox && associated_[i].hid.mapper)
434 associated_[i].hid.mapper(data_.items[i], &pads->items[i]);
436 pads->items[i] = data_.items[i];
440 } // namespace content