diff options
Diffstat (limited to 'dom/gamepad/linux/LinuxGamepad.cpp')
-rw-r--r-- | dom/gamepad/linux/LinuxGamepad.cpp | 362 |
1 files changed, 362 insertions, 0 deletions
diff --git a/dom/gamepad/linux/LinuxGamepad.cpp b/dom/gamepad/linux/LinuxGamepad.cpp new file mode 100644 index 0000000000..deee47b9d2 --- /dev/null +++ b/dom/gamepad/linux/LinuxGamepad.cpp @@ -0,0 +1,362 @@ +/* -*- 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 uint8_t& aRed, const uint8_t& aGreen, + const uint8_t& aBlue) { + // TODO: Bug 1523355. + NS_WARNING("Linux doesn't support gamepad light indicator."); +} + +} // namespace mozilla::dom |