summaryrefslogtreecommitdiffstats
path: root/dom/gamepad/linux
diff options
context:
space:
mode:
Diffstat (limited to 'dom/gamepad/linux')
-rw-r--r--dom/gamepad/linux/LinuxGamepad.cpp390
-rw-r--r--dom/gamepad/linux/udev.h150
2 files changed, 540 insertions, 0 deletions
diff --git a/dom/gamepad/linux/LinuxGamepad.cpp b/dom/gamepad/linux/LinuxGamepad.cpp
new file mode 100644
index 000000000..c45f4174a
--- /dev/null
+++ b/dom/gamepad/linux/LinuxGamepad.cpp
@@ -0,0 +1,390 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * LinuxGamepadService: A Linux backend for the GamepadService.
+ * Derived from the kernel documentation at
+ * http://www.kernel.org/doc/Documentation/input/joystick-api.txt
+ */
+#include <algorithm>
+#include <cstddef>
+
+#include <glib.h>
+#include <linux/joystick.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+#include "nscore.h"
+#include "mozilla/dom/GamepadPlatformService.h"
+#include "udev.h"
+
+namespace {
+
+using namespace mozilla::dom;
+using mozilla::udev_lib;
+using mozilla::udev_device;
+using mozilla::udev_list_entry;
+using mozilla::udev_enumerate;
+using mozilla::udev_monitor;
+
+static const float kMaxAxisValue = 32767.0;
+static const char kJoystickPath[] = "/dev/input/js";
+
+//TODO: should find a USB identifier for each device so we can
+// provide something that persists across connect/disconnect cycles.
+typedef struct {
+ int index;
+ guint source_id;
+ int numAxes;
+ int numButtons;
+ char idstring[128];
+ char devpath[PATH_MAX];
+} Gamepad;
+
+class LinuxGamepadService {
+public:
+ LinuxGamepadService() : mMonitor(nullptr),
+ mMonitorSourceID(0) {
+ }
+
+ void Startup();
+ void Shutdown();
+
+private:
+ void AddDevice(struct udev_device* dev);
+ void RemoveDevice(struct udev_device* dev);
+ void ScanForDevices();
+ void AddMonitor();
+ void RemoveMonitor();
+ bool is_gamepad(struct udev_device* dev);
+ void ReadUdevChange();
+
+ // handler for data from /dev/input/jsN
+ static gboolean OnGamepadData(GIOChannel *source,
+ GIOCondition condition,
+ gpointer data);
+
+ // handler for data from udev monitor
+ static gboolean OnUdevMonitor(GIOChannel *source,
+ GIOCondition condition,
+ gpointer data);
+
+ udev_lib mUdev;
+ struct udev_monitor* mMonitor;
+ guint mMonitorSourceID;
+ // Information about currently connected gamepads.
+ AutoTArray<Gamepad,4> mGamepads;
+};
+
+// singleton instance
+LinuxGamepadService* gService = nullptr;
+
+void
+LinuxGamepadService::AddDevice(struct udev_device* dev)
+{
+ RefPtr<GamepadPlatformService> service =
+ GamepadPlatformService::GetParentService();
+ if (!service) {
+ return;
+ }
+
+ const char* devpath = mUdev.udev_device_get_devnode(dev);
+ if (!devpath) {
+ return;
+ }
+
+ // Ensure that this device hasn't already been added.
+ for (unsigned int i = 0; i < mGamepads.Length(); i++) {
+ if (strcmp(mGamepads[i].devpath, devpath) == 0) {
+ return;
+ }
+ }
+
+ Gamepad gamepad;
+ snprintf(gamepad.devpath, sizeof(gamepad.devpath), "%s", devpath);
+ GIOChannel* channel = g_io_channel_new_file(devpath, "r", nullptr);
+ if (!channel) {
+ return;
+ }
+
+ g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, nullptr);
+ g_io_channel_set_encoding(channel, nullptr, nullptr);
+ g_io_channel_set_buffered(channel, FALSE);
+ int fd = g_io_channel_unix_get_fd(channel);
+ char name[128];
+ if (ioctl(fd, JSIOCGNAME(sizeof(name)), &name) == -1) {
+ strcpy(name, "unknown");
+ }
+ const char* vendor_id =
+ mUdev.udev_device_get_property_value(dev, "ID_VENDOR_ID");
+ const char* model_id =
+ mUdev.udev_device_get_property_value(dev, "ID_MODEL_ID");
+ if (!vendor_id || !model_id) {
+ struct udev_device* parent =
+ mUdev.udev_device_get_parent_with_subsystem_devtype(dev,
+ "input",
+ nullptr);
+ if (parent) {
+ vendor_id = mUdev.udev_device_get_sysattr_value(parent, "id/vendor");
+ model_id = mUdev.udev_device_get_sysattr_value(parent, "id/product");
+ }
+ }
+ snprintf(gamepad.idstring, sizeof(gamepad.idstring),
+ "%s-%s-%s",
+ vendor_id ? vendor_id : "unknown",
+ model_id ? model_id : "unknown",
+ name);
+
+ char numAxes = 0, numButtons = 0;
+ ioctl(fd, JSIOCGAXES, &numAxes);
+ gamepad.numAxes = numAxes;
+ ioctl(fd, JSIOCGBUTTONS, &numButtons);
+ gamepad.numButtons = numButtons;
+
+ gamepad.index = service->AddGamepad(gamepad.idstring,
+ mozilla::dom::GamepadMappingType::_empty,
+ gamepad.numButtons,
+ gamepad.numAxes);
+
+ gamepad.source_id =
+ g_io_add_watch(channel,
+ GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP),
+ OnGamepadData,
+ GINT_TO_POINTER(gamepad.index));
+ g_io_channel_unref(channel);
+
+ mGamepads.AppendElement(gamepad);
+}
+
+void
+LinuxGamepadService::RemoveDevice(struct udev_device* dev)
+{
+ RefPtr<GamepadPlatformService> service =
+ GamepadPlatformService::GetParentService();
+ if (!service) {
+ return;
+ }
+
+ const char* devpath = mUdev.udev_device_get_devnode(dev);
+ if (!devpath) {
+ return;
+ }
+
+ for (unsigned int i = 0; i < mGamepads.Length(); i++) {
+ if (strcmp(mGamepads[i].devpath, devpath) == 0) {
+ g_source_remove(mGamepads[i].source_id);
+ service->RemoveGamepad(mGamepads[i].index);
+ mGamepads.RemoveElementAt(i);
+ break;
+ }
+ }
+}
+
+void
+LinuxGamepadService::ScanForDevices()
+{
+ struct udev_enumerate* en = mUdev.udev_enumerate_new(mUdev.udev);
+ mUdev.udev_enumerate_add_match_subsystem(en, "input");
+ mUdev.udev_enumerate_scan_devices(en);
+
+ struct udev_list_entry* dev_list_entry;
+ for (dev_list_entry = mUdev.udev_enumerate_get_list_entry(en);
+ dev_list_entry != nullptr;
+ dev_list_entry = mUdev.udev_list_entry_get_next(dev_list_entry)) {
+ const char* path = mUdev.udev_list_entry_get_name(dev_list_entry);
+ struct udev_device* dev = mUdev.udev_device_new_from_syspath(mUdev.udev,
+ path);
+ if (is_gamepad(dev)) {
+ AddDevice(dev);
+ }
+
+ mUdev.udev_device_unref(dev);
+ }
+
+ mUdev.udev_enumerate_unref(en);
+}
+
+void
+LinuxGamepadService::AddMonitor()
+{
+ // Add a monitor to watch for device changes
+ mMonitor =
+ mUdev.udev_monitor_new_from_netlink(mUdev.udev, "udev");
+ if (!mMonitor) {
+ // Not much we can do here.
+ return;
+ }
+ mUdev.udev_monitor_filter_add_match_subsystem_devtype(mMonitor,
+ "input",
+ nullptr);
+
+ int monitor_fd = mUdev.udev_monitor_get_fd(mMonitor);
+ GIOChannel* monitor_channel = g_io_channel_unix_new(monitor_fd);
+ mMonitorSourceID =
+ g_io_add_watch(monitor_channel,
+ GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP),
+ OnUdevMonitor,
+ nullptr);
+ g_io_channel_unref(monitor_channel);
+
+ mUdev.udev_monitor_enable_receiving(mMonitor);
+}
+
+void
+LinuxGamepadService::RemoveMonitor()
+{
+ if (mMonitorSourceID) {
+ g_source_remove(mMonitorSourceID);
+ mMonitorSourceID = 0;
+ }
+ if (mMonitor) {
+ mUdev.udev_monitor_unref(mMonitor);
+ mMonitor = nullptr;
+ }
+}
+
+void
+LinuxGamepadService::Startup()
+{
+ // Don't bother starting up if libudev couldn't be loaded or initialized.
+ if (!mUdev)
+ return;
+
+ AddMonitor();
+ ScanForDevices();
+}
+
+void
+LinuxGamepadService::Shutdown()
+{
+ for (unsigned int i = 0; i < mGamepads.Length(); i++) {
+ g_source_remove(mGamepads[i].source_id);
+ }
+ mGamepads.Clear();
+ RemoveMonitor();
+}
+
+bool
+LinuxGamepadService::is_gamepad(struct udev_device* dev)
+{
+ if (!mUdev.udev_device_get_property_value(dev, "ID_INPUT_JOYSTICK"))
+ return false;
+
+ const char* devpath = mUdev.udev_device_get_devnode(dev);
+ if (!devpath) {
+ return false;
+ }
+ if (strncmp(kJoystickPath, devpath, sizeof(kJoystickPath) - 1) != 0) {
+ return false;
+ }
+
+ return true;
+}
+
+void
+LinuxGamepadService::ReadUdevChange()
+{
+ struct udev_device* dev =
+ mUdev.udev_monitor_receive_device(mMonitor);
+ const char* action = mUdev.udev_device_get_action(dev);
+ if (is_gamepad(dev)) {
+ if (strcmp(action, "add") == 0) {
+ AddDevice(dev);
+ } else if (strcmp(action, "remove") == 0) {
+ RemoveDevice(dev);
+ }
+ }
+ mUdev.udev_device_unref(dev);
+}
+
+// static
+gboolean
+LinuxGamepadService::OnGamepadData(GIOChannel* source,
+ GIOCondition condition,
+ gpointer data)
+{
+ RefPtr<GamepadPlatformService> service =
+ GamepadPlatformService::GetParentService();
+ if (!service) {
+ return TRUE;
+ }
+ int index = GPOINTER_TO_INT(data);
+ //TODO: remove gamepad?
+ if (condition & G_IO_ERR || condition & G_IO_HUP)
+ return FALSE;
+
+ while (true) {
+ struct js_event event;
+ gsize count;
+ GError* err = nullptr;
+ if (g_io_channel_read_chars(source,
+ (gchar*)&event,
+ sizeof(event),
+ &count,
+ &err) != G_IO_STATUS_NORMAL ||
+ count == 0) {
+ break;
+ }
+
+ //TODO: store device state?
+ if (event.type & JS_EVENT_INIT) {
+ continue;
+ }
+
+ switch (event.type) {
+ case JS_EVENT_BUTTON:
+ service->NewButtonEvent(index, event.number, !!event.value);
+ break;
+ case JS_EVENT_AXIS:
+ service->NewAxisMoveEvent(index, event.number,
+ ((float)event.value) / kMaxAxisValue);
+ break;
+ }
+ }
+
+ return TRUE;
+}
+
+// static
+gboolean
+LinuxGamepadService::OnUdevMonitor(GIOChannel* source,
+ GIOCondition condition,
+ gpointer data)
+{
+ if (condition & G_IO_ERR || condition & G_IO_HUP)
+ return FALSE;
+
+ gService->ReadUdevChange();
+ return TRUE;
+}
+
+} // namespace
+
+namespace mozilla {
+namespace dom {
+
+void StartGamepadMonitoring()
+{
+ if (gService) {
+ return;
+ }
+ gService = new LinuxGamepadService();
+ gService->Startup();
+}
+
+void StopGamepadMonitoring()
+{
+ if (!gService) {
+ return;
+ }
+ gService->Shutdown();
+ delete gService;
+ gService = nullptr;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/gamepad/linux/udev.h b/dom/gamepad/linux/udev.h
new file mode 100644
index 000000000..18da9f7dc
--- /dev/null
+++ b/dom/gamepad/linux/udev.h
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This file defines a wrapper around libudev so we can avoid
+ * linking directly to it and use dlopen instead.
+ */
+
+#ifndef HAL_LINUX_UDEV_H_
+#define HAL_LINUX_UDEV_H_
+
+#include <dlfcn.h>
+
+#include "mozilla/ArrayUtils.h"
+
+namespace mozilla {
+
+struct udev;
+struct udev_device;
+struct udev_enumerate;
+struct udev_list_entry;
+struct udev_monitor;
+
+class udev_lib {
+ public:
+ udev_lib() : lib(nullptr),
+ udev(nullptr) {
+ // Be careful about ABI compat! 0 -> 1 didn't change any
+ // symbols this code relies on, per:
+ // https://lists.fedoraproject.org/pipermail/devel/2012-June/168227.html
+ const char* lib_names[] = {"libudev.so.0", "libudev.so.1"};
+ // Check whether a library is already loaded so we don't load two
+ // conflicting libs.
+ for (unsigned i = 0; i < ArrayLength(lib_names); i++) {
+ lib = dlopen(lib_names[i], RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
+ if (lib) {
+ break;
+ }
+ }
+ // If nothing loads the first time through, it means no version of libudev
+ // was already loaded.
+ if (!lib) {
+ for (unsigned i = 0; i < ArrayLength(lib_names); i++) {
+ lib = dlopen(lib_names[i], RTLD_LAZY | RTLD_GLOBAL);
+ if (lib) {
+ break;
+ }
+ }
+ }
+ if (lib && LoadSymbols()) {
+ udev = udev_new();
+ }
+ }
+
+ ~udev_lib() {
+ if (udev) {
+ udev_unref(udev);
+ }
+
+ if (lib) {
+ dlclose(lib);
+ }
+ }
+
+ explicit operator bool() {
+ return udev;
+ }
+
+ private:
+ bool LoadSymbols() {
+#define DLSYM(s) \
+ do { \
+ s = (typeof(s))dlsym(lib, #s); \
+ if (!s) return false; \
+ } while (0)
+
+ DLSYM(udev_new);
+ DLSYM(udev_unref);
+ DLSYM(udev_device_unref);
+ DLSYM(udev_device_new_from_syspath);
+ DLSYM(udev_device_get_devnode);
+ DLSYM(udev_device_get_parent_with_subsystem_devtype);
+ DLSYM(udev_device_get_property_value);
+ DLSYM(udev_device_get_action);
+ DLSYM(udev_device_get_sysattr_value);
+ DLSYM(udev_enumerate_new);
+ DLSYM(udev_enumerate_unref);
+ DLSYM(udev_enumerate_add_match_subsystem);
+ DLSYM(udev_enumerate_scan_devices);
+ DLSYM(udev_enumerate_get_list_entry);
+ DLSYM(udev_list_entry_get_next);
+ DLSYM(udev_list_entry_get_name);
+ DLSYM(udev_monitor_new_from_netlink);
+ DLSYM(udev_monitor_filter_add_match_subsystem_devtype);
+ DLSYM(udev_monitor_enable_receiving);
+ DLSYM(udev_monitor_get_fd);
+ DLSYM(udev_monitor_receive_device);
+ DLSYM(udev_monitor_unref);
+#undef DLSYM
+ return true;
+ }
+
+ void* lib;
+
+ public:
+ struct udev* udev;
+
+ // Function pointers returned from dlsym.
+ struct udev* (*udev_new)(void);
+ void (*udev_unref)(struct udev*);
+
+ void (*udev_device_unref)(struct udev_device*);
+ struct udev_device* (*udev_device_new_from_syspath)(struct udev*,
+ const char*);
+ const char* (*udev_device_get_devnode)(struct udev_device*);
+ struct udev_device* (*udev_device_get_parent_with_subsystem_devtype)
+ (struct udev_device*, const char*, const char*);
+ const char* (*udev_device_get_property_value)(struct udev_device*,
+ const char*);
+ const char* (*udev_device_get_action)(struct udev_device*);
+ const char* (*udev_device_get_sysattr_value)(struct udev_device*,
+ const char*);
+
+ struct udev_enumerate* (*udev_enumerate_new)(struct udev*);
+ void (*udev_enumerate_unref)(struct udev_enumerate*);
+ int (*udev_enumerate_add_match_subsystem)(struct udev_enumerate*,
+ const char*);
+ int (*udev_enumerate_scan_devices)(struct udev_enumerate*);
+ struct udev_list_entry* (*udev_enumerate_get_list_entry)
+ (struct udev_enumerate*);
+
+ struct udev_list_entry* (*udev_list_entry_get_next)(struct udev_list_entry *);
+ const char* (*udev_list_entry_get_name)(struct udev_list_entry*);
+
+ struct udev_monitor* (*udev_monitor_new_from_netlink)(struct udev*,
+ const char*);
+ int (*udev_monitor_filter_add_match_subsystem_devtype)
+ (struct udev_monitor*, const char*, const char*);
+ int (*udev_monitor_enable_receiving)(struct udev_monitor*);
+ int (*udev_monitor_get_fd)(struct udev_monitor*);
+ struct udev_device* (*udev_monitor_receive_device)(struct udev_monitor*);
+ void (*udev_monitor_unref)(struct udev_monitor*);
+};
+
+} // namespace mozilla
+
+#endif // HAL_LINUX_UDEV_H_