diff options
Diffstat (limited to 'dom/gamepad/linux')
-rw-r--r-- | dom/gamepad/linux/LinuxGamepad.cpp | 363 | ||||
-rw-r--r-- | dom/gamepad/linux/udev.h | 150 |
2 files changed, 513 insertions, 0 deletions
diff --git a/dom/gamepad/linux/LinuxGamepad.cpp b/dom/gamepad/linux/LinuxGamepad.cpp new file mode 100644 index 0000000000..512ac76502 --- /dev/null +++ b/dom/gamepad/linux/LinuxGamepad.cpp @@ -0,0 +1,363 @@ +/* -*- 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/GamepadHandle.h" +#include "mozilla/dom/GamepadPlatformService.h" +#include "mozilla/Tainting.h" +#include "mozilla/UniquePtr.h" +#include "udev.h" + +namespace { + +using namespace mozilla::dom; +using mozilla::MakeUnique; +using mozilla::udev_device; +using mozilla::udev_enumerate; +using mozilla::udev_lib; +using mozilla::udev_list_entry; +using mozilla::udev_monitor; +using mozilla::UniquePtr; + +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 { + GamepadHandle handle; + guint source_id; + int numAxes; + int numButtons; + char idstring[256]; + 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<UniquePtr<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; + } + } + + auto gamepad = MakeUnique<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->handle = service->AddGamepad( + gamepad->idstring, mozilla::dom::GamepadMappingType::_empty, + mozilla::dom::GamepadHand::_empty, gamepad->numButtons, gamepad->numAxes, + 0, 0, 0); // TODO: Bug 680289, implement gamepad haptics for Linux. + // TODO: Bug 1523355, implement gamepad lighindicator and touch for Linux. + + gamepad->source_id = + g_io_add_watch(channel, GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), + OnGamepadData, gamepad.get()); + g_io_channel_unref(channel); + + mGamepads.AppendElement(std::move(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) { + auto gamepad = std::move(mGamepads[i]); + mGamepads.RemoveElementAt(i); + + g_source_remove(gamepad->source_id); + service->RemoveGamepad(gamepad->handle); + + 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; + } + auto* gamepad = static_cast<Gamepad*>(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(gamepad->handle, event.number, !!event.value); + break; + case JS_EVENT_AXIS: + service->NewAxisMoveEvent(gamepad->handle, 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::dom { + +void StartGamepadMonitoring() { + if (gService) { + return; + } + gService = new LinuxGamepadService(); + gService->Startup(); +} + +void StopGamepadMonitoring() { + if (!gService) { + return; + } + gService->Shutdown(); + delete gService; + gService = nullptr; +} + +void SetGamepadLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle, + const Tainted<uint32_t>& aLightColorIndex, + const Tainted<uint8_t>& aRed, + const Tainted<uint8_t>& aGreen, + const Tainted<uint8_t>& aBlue) { + // TODO: Bug 1523355. + NS_WARNING("Linux doesn't support gamepad light indicator."); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/linux/udev.h b/dom/gamepad/linux/udev.h new file mode 100644 index 0000000000..4e3f0b7865 --- /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: +#define DLSYM(s) \ + do { \ + (s) = (decltype(s))dlsym(lib, #s); \ + if (!(s)) return false; \ + } while (0) + + bool LoadSymbols() { + 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); + + return true; + } + +#undef DLSYM + + 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_ |