1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/hal/linux/LinuxGamepad.cpp Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,373 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/* 1.9 + * LinuxGamepadService: A Linux backend for the GamepadService. 1.10 + * Derived from the kernel documentation at 1.11 + * http://www.kernel.org/doc/Documentation/input/joystick-api.txt 1.12 + */ 1.13 +#include <algorithm> 1.14 +#include <cstddef> 1.15 + 1.16 +#include <glib.h> 1.17 +#include <linux/joystick.h> 1.18 +#include <stdio.h> 1.19 +#include <stdint.h> 1.20 +#include <sys/ioctl.h> 1.21 +#include <unistd.h> 1.22 + 1.23 +#include "nscore.h" 1.24 +#include "mozilla/dom/GamepadService.h" 1.25 +#include "udev.h" 1.26 + 1.27 +namespace { 1.28 + 1.29 +using mozilla::dom::GamepadService; 1.30 +using mozilla::udev_lib; 1.31 +using mozilla::udev_device; 1.32 +using mozilla::udev_list_entry; 1.33 +using mozilla::udev_enumerate; 1.34 +using mozilla::udev_monitor; 1.35 + 1.36 +static const float kMaxAxisValue = 32767.0; 1.37 +static const char kJoystickPath[] = "/dev/input/js"; 1.38 + 1.39 +//TODO: should find a USB identifier for each device so we can 1.40 +// provide something that persists across connect/disconnect cycles. 1.41 +typedef struct { 1.42 + int index; 1.43 + guint source_id; 1.44 + int numAxes; 1.45 + int numButtons; 1.46 + char idstring[128]; 1.47 + char devpath[PATH_MAX]; 1.48 +} Gamepad; 1.49 + 1.50 +class LinuxGamepadService { 1.51 +public: 1.52 + LinuxGamepadService() : mMonitor(nullptr), 1.53 + mMonitorSourceID(0) { 1.54 + } 1.55 + 1.56 + void Startup(); 1.57 + void Shutdown(); 1.58 + 1.59 +private: 1.60 + void AddDevice(struct udev_device* dev); 1.61 + void RemoveDevice(struct udev_device* dev); 1.62 + void ScanForDevices(); 1.63 + void AddMonitor(); 1.64 + void RemoveMonitor(); 1.65 + bool is_gamepad(struct udev_device* dev); 1.66 + void ReadUdevChange(); 1.67 + 1.68 + // handler for data from /dev/input/jsN 1.69 + static gboolean OnGamepadData(GIOChannel *source, 1.70 + GIOCondition condition, 1.71 + gpointer data); 1.72 + 1.73 + // handler for data from udev monitor 1.74 + static gboolean OnUdevMonitor(GIOChannel *source, 1.75 + GIOCondition condition, 1.76 + gpointer data); 1.77 + 1.78 + udev_lib mUdev; 1.79 + struct udev_monitor* mMonitor; 1.80 + guint mMonitorSourceID; 1.81 + // Information about currently connected gamepads. 1.82 + nsAutoTArray<Gamepad,4> mGamepads; 1.83 +}; 1.84 + 1.85 +// singleton instance 1.86 +LinuxGamepadService* gService = nullptr; 1.87 + 1.88 +void 1.89 +LinuxGamepadService::AddDevice(struct udev_device* dev) 1.90 +{ 1.91 + const char* devpath = mUdev.udev_device_get_devnode(dev); 1.92 + if (!devpath) { 1.93 + return; 1.94 + } 1.95 + 1.96 + // Ensure that this device hasn't already been added. 1.97 + for (unsigned int i = 0; i < mGamepads.Length(); i++) { 1.98 + if (strcmp(mGamepads[i].devpath, devpath) == 0) { 1.99 + return; 1.100 + } 1.101 + } 1.102 + 1.103 + Gamepad gamepad; 1.104 + snprintf(gamepad.devpath, sizeof(gamepad.devpath), "%s", devpath); 1.105 + GIOChannel* channel = g_io_channel_new_file(devpath, "r", nullptr); 1.106 + if (!channel) { 1.107 + return; 1.108 + } 1.109 + 1.110 + g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, nullptr); 1.111 + g_io_channel_set_encoding(channel, nullptr, nullptr); 1.112 + g_io_channel_set_buffered(channel, FALSE); 1.113 + int fd = g_io_channel_unix_get_fd(channel); 1.114 + char name[128]; 1.115 + if (ioctl(fd, JSIOCGNAME(sizeof(name)), &name) == -1) { 1.116 + strcpy(name, "unknown"); 1.117 + } 1.118 + const char* vendor_id = 1.119 + mUdev.udev_device_get_property_value(dev, "ID_VENDOR_ID"); 1.120 + const char* model_id = 1.121 + mUdev.udev_device_get_property_value(dev, "ID_MODEL_ID"); 1.122 + if (!vendor_id || !model_id) { 1.123 + struct udev_device* parent = 1.124 + mUdev.udev_device_get_parent_with_subsystem_devtype(dev, 1.125 + "input", 1.126 + nullptr); 1.127 + if (parent) { 1.128 + vendor_id = mUdev.udev_device_get_sysattr_value(parent, "id/vendor"); 1.129 + model_id = mUdev.udev_device_get_sysattr_value(parent, "id/product"); 1.130 + } 1.131 + } 1.132 + snprintf(gamepad.idstring, sizeof(gamepad.idstring), 1.133 + "%s-%s-%s", 1.134 + vendor_id ? vendor_id : "unknown", 1.135 + model_id ? model_id : "unknown", 1.136 + name); 1.137 + 1.138 + char numAxes = 0, numButtons = 0; 1.139 + ioctl(fd, JSIOCGAXES, &numAxes); 1.140 + gamepad.numAxes = numAxes; 1.141 + ioctl(fd, JSIOCGBUTTONS, &numButtons); 1.142 + gamepad.numButtons = numButtons; 1.143 + 1.144 + nsRefPtr<GamepadService> service(GamepadService::GetService()); 1.145 + gamepad.index = service->AddGamepad(gamepad.idstring, 1.146 + mozilla::dom::NoMapping, 1.147 + gamepad.numButtons, 1.148 + gamepad.numAxes); 1.149 + 1.150 + gamepad.source_id = 1.151 + g_io_add_watch(channel, 1.152 + GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), 1.153 + OnGamepadData, 1.154 + GINT_TO_POINTER(gamepad.index)); 1.155 + g_io_channel_unref(channel); 1.156 + 1.157 + mGamepads.AppendElement(gamepad); 1.158 +} 1.159 + 1.160 +void 1.161 +LinuxGamepadService::RemoveDevice(struct udev_device* dev) 1.162 +{ 1.163 + const char* devpath = mUdev.udev_device_get_devnode(dev); 1.164 + if (!devpath) { 1.165 + return; 1.166 + } 1.167 + 1.168 + nsRefPtr<GamepadService> service(GamepadService::GetService()); 1.169 + for (unsigned int i = 0; i < mGamepads.Length(); i++) { 1.170 + if (strcmp(mGamepads[i].devpath, devpath) == 0) { 1.171 + g_source_remove(mGamepads[i].source_id); 1.172 + service->RemoveGamepad(mGamepads[i].index); 1.173 + mGamepads.RemoveElementAt(i); 1.174 + break; 1.175 + } 1.176 + } 1.177 +} 1.178 + 1.179 +void 1.180 +LinuxGamepadService::ScanForDevices() 1.181 +{ 1.182 + struct udev_enumerate* en = mUdev.udev_enumerate_new(mUdev.udev); 1.183 + mUdev.udev_enumerate_add_match_subsystem(en, "input"); 1.184 + mUdev.udev_enumerate_scan_devices(en); 1.185 + 1.186 + struct udev_list_entry* dev_list_entry; 1.187 + for (dev_list_entry = mUdev.udev_enumerate_get_list_entry(en); 1.188 + dev_list_entry != nullptr; 1.189 + dev_list_entry = mUdev.udev_list_entry_get_next(dev_list_entry)) { 1.190 + const char* path = mUdev.udev_list_entry_get_name(dev_list_entry); 1.191 + struct udev_device* dev = mUdev.udev_device_new_from_syspath(mUdev.udev, 1.192 + path); 1.193 + if (is_gamepad(dev)) { 1.194 + AddDevice(dev); 1.195 + } 1.196 + 1.197 + mUdev.udev_device_unref(dev); 1.198 + } 1.199 + 1.200 + mUdev.udev_enumerate_unref(en); 1.201 +} 1.202 + 1.203 +void 1.204 +LinuxGamepadService::AddMonitor() 1.205 +{ 1.206 + // Add a monitor to watch for device changes 1.207 + mMonitor = 1.208 + mUdev.udev_monitor_new_from_netlink(mUdev.udev, "udev"); 1.209 + if (!mMonitor) { 1.210 + // Not much we can do here. 1.211 + return; 1.212 + } 1.213 + mUdev.udev_monitor_filter_add_match_subsystem_devtype(mMonitor, 1.214 + "input", 1.215 + nullptr); 1.216 + 1.217 + int monitor_fd = mUdev.udev_monitor_get_fd(mMonitor); 1.218 + GIOChannel* monitor_channel = g_io_channel_unix_new(monitor_fd); 1.219 + mMonitorSourceID = 1.220 + g_io_add_watch(monitor_channel, 1.221 + GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), 1.222 + OnUdevMonitor, 1.223 + nullptr); 1.224 + g_io_channel_unref(monitor_channel); 1.225 + 1.226 + mUdev.udev_monitor_enable_receiving(mMonitor); 1.227 +} 1.228 + 1.229 +void 1.230 +LinuxGamepadService::RemoveMonitor() 1.231 +{ 1.232 + if (mMonitorSourceID) { 1.233 + g_source_remove(mMonitorSourceID); 1.234 + mMonitorSourceID = 0; 1.235 + } 1.236 + if (mMonitor) { 1.237 + mUdev.udev_monitor_unref(mMonitor); 1.238 + mMonitor = nullptr; 1.239 + } 1.240 +} 1.241 + 1.242 +void 1.243 +LinuxGamepadService::Startup() 1.244 +{ 1.245 + // Don't bother starting up if libudev couldn't be loaded or initialized. 1.246 + if (!mUdev) 1.247 + return; 1.248 + 1.249 + AddMonitor(); 1.250 + ScanForDevices(); 1.251 +} 1.252 + 1.253 +void 1.254 +LinuxGamepadService::Shutdown() 1.255 +{ 1.256 + for (unsigned int i = 0; i < mGamepads.Length(); i++) { 1.257 + g_source_remove(mGamepads[i].source_id); 1.258 + } 1.259 + mGamepads.Clear(); 1.260 + RemoveMonitor(); 1.261 +} 1.262 + 1.263 +bool 1.264 +LinuxGamepadService::is_gamepad(struct udev_device* dev) 1.265 +{ 1.266 + if (!mUdev.udev_device_get_property_value(dev, "ID_INPUT_JOYSTICK")) 1.267 + return false; 1.268 + 1.269 + const char* devpath = mUdev.udev_device_get_devnode(dev); 1.270 + if (!devpath) { 1.271 + return false; 1.272 + } 1.273 + if (strncmp(kJoystickPath, devpath, sizeof(kJoystickPath) - 1) != 0) { 1.274 + return false; 1.275 + } 1.276 + 1.277 + return true; 1.278 +} 1.279 + 1.280 +void 1.281 +LinuxGamepadService::ReadUdevChange() 1.282 +{ 1.283 + struct udev_device* dev = 1.284 + mUdev.udev_monitor_receive_device(mMonitor); 1.285 + const char* action = mUdev.udev_device_get_action(dev); 1.286 + if (is_gamepad(dev)) { 1.287 + if (strcmp(action, "add") == 0) { 1.288 + AddDevice(dev); 1.289 + } else if (strcmp(action, "remove") == 0) { 1.290 + RemoveDevice(dev); 1.291 + } 1.292 + } 1.293 + mUdev.udev_device_unref(dev); 1.294 +} 1.295 + 1.296 +// static 1.297 +gboolean 1.298 +LinuxGamepadService::OnGamepadData(GIOChannel* source, 1.299 + GIOCondition condition, 1.300 + gpointer data) 1.301 +{ 1.302 + int index = GPOINTER_TO_INT(data); 1.303 + //TODO: remove gamepad? 1.304 + if (condition & G_IO_ERR || condition & G_IO_HUP) 1.305 + return FALSE; 1.306 + 1.307 + while (true) { 1.308 + struct js_event event; 1.309 + gsize count; 1.310 + GError* err = nullptr; 1.311 + if (g_io_channel_read_chars(source, 1.312 + (gchar*)&event, 1.313 + sizeof(event), 1.314 + &count, 1.315 + &err) != G_IO_STATUS_NORMAL || 1.316 + count == 0) { 1.317 + break; 1.318 + } 1.319 + 1.320 + //TODO: store device state? 1.321 + if (event.type & JS_EVENT_INIT) { 1.322 + continue; 1.323 + } 1.324 + 1.325 + nsRefPtr<GamepadService> service(GamepadService::GetService()); 1.326 + switch (event.type) { 1.327 + case JS_EVENT_BUTTON: 1.328 + service->NewButtonEvent(index, event.number, !!event.value); 1.329 + break; 1.330 + case JS_EVENT_AXIS: 1.331 + service->NewAxisMoveEvent(index, event.number, 1.332 + ((float)event.value) / kMaxAxisValue); 1.333 + break; 1.334 + } 1.335 + } 1.336 + 1.337 + return TRUE; 1.338 +} 1.339 + 1.340 +// static 1.341 +gboolean 1.342 +LinuxGamepadService::OnUdevMonitor(GIOChannel* source, 1.343 + GIOCondition condition, 1.344 + gpointer data) 1.345 +{ 1.346 + if (condition & G_IO_ERR || condition & G_IO_HUP) 1.347 + return FALSE; 1.348 + 1.349 + gService->ReadUdevChange(); 1.350 + return TRUE; 1.351 +} 1.352 + 1.353 +} // namespace 1.354 + 1.355 +namespace mozilla { 1.356 +namespace hal_impl { 1.357 + 1.358 +void StartMonitoringGamepadStatus() 1.359 +{ 1.360 + if (!gService) { 1.361 + gService = new LinuxGamepadService(); 1.362 + gService->Startup(); 1.363 + } 1.364 +} 1.365 + 1.366 +void StopMonitoringGamepadStatus() 1.367 +{ 1.368 + if (gService) { 1.369 + gService->Shutdown(); 1.370 + delete gService; 1.371 + gService = nullptr; 1.372 + } 1.373 +} 1.374 + 1.375 +} // namespace hal_impl 1.376 +} // namespace mozilla