michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* michael@0: * LinuxGamepadService: A Linux backend for the GamepadService. michael@0: * Derived from the kernel documentation at michael@0: * http://www.kernel.org/doc/Documentation/input/joystick-api.txt michael@0: */ michael@0: #include michael@0: #include michael@0: michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: michael@0: #include "nscore.h" michael@0: #include "mozilla/dom/GamepadService.h" michael@0: #include "udev.h" michael@0: michael@0: namespace { michael@0: michael@0: using mozilla::dom::GamepadService; michael@0: using mozilla::udev_lib; michael@0: using mozilla::udev_device; michael@0: using mozilla::udev_list_entry; michael@0: using mozilla::udev_enumerate; michael@0: using mozilla::udev_monitor; michael@0: michael@0: static const float kMaxAxisValue = 32767.0; michael@0: static const char kJoystickPath[] = "/dev/input/js"; michael@0: michael@0: //TODO: should find a USB identifier for each device so we can michael@0: // provide something that persists across connect/disconnect cycles. michael@0: typedef struct { michael@0: int index; michael@0: guint source_id; michael@0: int numAxes; michael@0: int numButtons; michael@0: char idstring[128]; michael@0: char devpath[PATH_MAX]; michael@0: } Gamepad; michael@0: michael@0: class LinuxGamepadService { michael@0: public: michael@0: LinuxGamepadService() : mMonitor(nullptr), michael@0: mMonitorSourceID(0) { michael@0: } michael@0: michael@0: void Startup(); michael@0: void Shutdown(); michael@0: michael@0: private: michael@0: void AddDevice(struct udev_device* dev); michael@0: void RemoveDevice(struct udev_device* dev); michael@0: void ScanForDevices(); michael@0: void AddMonitor(); michael@0: void RemoveMonitor(); michael@0: bool is_gamepad(struct udev_device* dev); michael@0: void ReadUdevChange(); michael@0: michael@0: // handler for data from /dev/input/jsN michael@0: static gboolean OnGamepadData(GIOChannel *source, michael@0: GIOCondition condition, michael@0: gpointer data); michael@0: michael@0: // handler for data from udev monitor michael@0: static gboolean OnUdevMonitor(GIOChannel *source, michael@0: GIOCondition condition, michael@0: gpointer data); michael@0: michael@0: udev_lib mUdev; michael@0: struct udev_monitor* mMonitor; michael@0: guint mMonitorSourceID; michael@0: // Information about currently connected gamepads. michael@0: nsAutoTArray mGamepads; michael@0: }; michael@0: michael@0: // singleton instance michael@0: LinuxGamepadService* gService = nullptr; michael@0: michael@0: void michael@0: LinuxGamepadService::AddDevice(struct udev_device* dev) michael@0: { michael@0: const char* devpath = mUdev.udev_device_get_devnode(dev); michael@0: if (!devpath) { michael@0: return; michael@0: } michael@0: michael@0: // Ensure that this device hasn't already been added. michael@0: for (unsigned int i = 0; i < mGamepads.Length(); i++) { michael@0: if (strcmp(mGamepads[i].devpath, devpath) == 0) { michael@0: return; michael@0: } michael@0: } michael@0: michael@0: Gamepad gamepad; michael@0: snprintf(gamepad.devpath, sizeof(gamepad.devpath), "%s", devpath); michael@0: GIOChannel* channel = g_io_channel_new_file(devpath, "r", nullptr); michael@0: if (!channel) { michael@0: return; michael@0: } michael@0: michael@0: g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, nullptr); michael@0: g_io_channel_set_encoding(channel, nullptr, nullptr); michael@0: g_io_channel_set_buffered(channel, FALSE); michael@0: int fd = g_io_channel_unix_get_fd(channel); michael@0: char name[128]; michael@0: if (ioctl(fd, JSIOCGNAME(sizeof(name)), &name) == -1) { michael@0: strcpy(name, "unknown"); michael@0: } michael@0: const char* vendor_id = michael@0: mUdev.udev_device_get_property_value(dev, "ID_VENDOR_ID"); michael@0: const char* model_id = michael@0: mUdev.udev_device_get_property_value(dev, "ID_MODEL_ID"); michael@0: if (!vendor_id || !model_id) { michael@0: struct udev_device* parent = michael@0: mUdev.udev_device_get_parent_with_subsystem_devtype(dev, michael@0: "input", michael@0: nullptr); michael@0: if (parent) { michael@0: vendor_id = mUdev.udev_device_get_sysattr_value(parent, "id/vendor"); michael@0: model_id = mUdev.udev_device_get_sysattr_value(parent, "id/product"); michael@0: } michael@0: } michael@0: snprintf(gamepad.idstring, sizeof(gamepad.idstring), michael@0: "%s-%s-%s", michael@0: vendor_id ? vendor_id : "unknown", michael@0: model_id ? model_id : "unknown", michael@0: name); michael@0: michael@0: char numAxes = 0, numButtons = 0; michael@0: ioctl(fd, JSIOCGAXES, &numAxes); michael@0: gamepad.numAxes = numAxes; michael@0: ioctl(fd, JSIOCGBUTTONS, &numButtons); michael@0: gamepad.numButtons = numButtons; michael@0: michael@0: nsRefPtr service(GamepadService::GetService()); michael@0: gamepad.index = service->AddGamepad(gamepad.idstring, michael@0: mozilla::dom::NoMapping, michael@0: gamepad.numButtons, michael@0: gamepad.numAxes); michael@0: michael@0: gamepad.source_id = michael@0: g_io_add_watch(channel, michael@0: GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), michael@0: OnGamepadData, michael@0: GINT_TO_POINTER(gamepad.index)); michael@0: g_io_channel_unref(channel); michael@0: michael@0: mGamepads.AppendElement(gamepad); michael@0: } michael@0: michael@0: void michael@0: LinuxGamepadService::RemoveDevice(struct udev_device* dev) michael@0: { michael@0: const char* devpath = mUdev.udev_device_get_devnode(dev); michael@0: if (!devpath) { michael@0: return; michael@0: } michael@0: michael@0: nsRefPtr service(GamepadService::GetService()); michael@0: for (unsigned int i = 0; i < mGamepads.Length(); i++) { michael@0: if (strcmp(mGamepads[i].devpath, devpath) == 0) { michael@0: g_source_remove(mGamepads[i].source_id); michael@0: service->RemoveGamepad(mGamepads[i].index); michael@0: mGamepads.RemoveElementAt(i); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: LinuxGamepadService::ScanForDevices() michael@0: { michael@0: struct udev_enumerate* en = mUdev.udev_enumerate_new(mUdev.udev); michael@0: mUdev.udev_enumerate_add_match_subsystem(en, "input"); michael@0: mUdev.udev_enumerate_scan_devices(en); michael@0: michael@0: struct udev_list_entry* dev_list_entry; michael@0: for (dev_list_entry = mUdev.udev_enumerate_get_list_entry(en); michael@0: dev_list_entry != nullptr; michael@0: dev_list_entry = mUdev.udev_list_entry_get_next(dev_list_entry)) { michael@0: const char* path = mUdev.udev_list_entry_get_name(dev_list_entry); michael@0: struct udev_device* dev = mUdev.udev_device_new_from_syspath(mUdev.udev, michael@0: path); michael@0: if (is_gamepad(dev)) { michael@0: AddDevice(dev); michael@0: } michael@0: michael@0: mUdev.udev_device_unref(dev); michael@0: } michael@0: michael@0: mUdev.udev_enumerate_unref(en); michael@0: } michael@0: michael@0: void michael@0: LinuxGamepadService::AddMonitor() michael@0: { michael@0: // Add a monitor to watch for device changes michael@0: mMonitor = michael@0: mUdev.udev_monitor_new_from_netlink(mUdev.udev, "udev"); michael@0: if (!mMonitor) { michael@0: // Not much we can do here. michael@0: return; michael@0: } michael@0: mUdev.udev_monitor_filter_add_match_subsystem_devtype(mMonitor, michael@0: "input", michael@0: nullptr); michael@0: michael@0: int monitor_fd = mUdev.udev_monitor_get_fd(mMonitor); michael@0: GIOChannel* monitor_channel = g_io_channel_unix_new(monitor_fd); michael@0: mMonitorSourceID = michael@0: g_io_add_watch(monitor_channel, michael@0: GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), michael@0: OnUdevMonitor, michael@0: nullptr); michael@0: g_io_channel_unref(monitor_channel); michael@0: michael@0: mUdev.udev_monitor_enable_receiving(mMonitor); michael@0: } michael@0: michael@0: void michael@0: LinuxGamepadService::RemoveMonitor() michael@0: { michael@0: if (mMonitorSourceID) { michael@0: g_source_remove(mMonitorSourceID); michael@0: mMonitorSourceID = 0; michael@0: } michael@0: if (mMonitor) { michael@0: mUdev.udev_monitor_unref(mMonitor); michael@0: mMonitor = nullptr; michael@0: } michael@0: } michael@0: michael@0: void michael@0: LinuxGamepadService::Startup() michael@0: { michael@0: // Don't bother starting up if libudev couldn't be loaded or initialized. michael@0: if (!mUdev) michael@0: return; michael@0: michael@0: AddMonitor(); michael@0: ScanForDevices(); michael@0: } michael@0: michael@0: void michael@0: LinuxGamepadService::Shutdown() michael@0: { michael@0: for (unsigned int i = 0; i < mGamepads.Length(); i++) { michael@0: g_source_remove(mGamepads[i].source_id); michael@0: } michael@0: mGamepads.Clear(); michael@0: RemoveMonitor(); michael@0: } michael@0: michael@0: bool michael@0: LinuxGamepadService::is_gamepad(struct udev_device* dev) michael@0: { michael@0: if (!mUdev.udev_device_get_property_value(dev, "ID_INPUT_JOYSTICK")) michael@0: return false; michael@0: michael@0: const char* devpath = mUdev.udev_device_get_devnode(dev); michael@0: if (!devpath) { michael@0: return false; michael@0: } michael@0: if (strncmp(kJoystickPath, devpath, sizeof(kJoystickPath) - 1) != 0) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: void michael@0: LinuxGamepadService::ReadUdevChange() michael@0: { michael@0: struct udev_device* dev = michael@0: mUdev.udev_monitor_receive_device(mMonitor); michael@0: const char* action = mUdev.udev_device_get_action(dev); michael@0: if (is_gamepad(dev)) { michael@0: if (strcmp(action, "add") == 0) { michael@0: AddDevice(dev); michael@0: } else if (strcmp(action, "remove") == 0) { michael@0: RemoveDevice(dev); michael@0: } michael@0: } michael@0: mUdev.udev_device_unref(dev); michael@0: } michael@0: michael@0: // static michael@0: gboolean michael@0: LinuxGamepadService::OnGamepadData(GIOChannel* source, michael@0: GIOCondition condition, michael@0: gpointer data) michael@0: { michael@0: int index = GPOINTER_TO_INT(data); michael@0: //TODO: remove gamepad? michael@0: if (condition & G_IO_ERR || condition & G_IO_HUP) michael@0: return FALSE; michael@0: michael@0: while (true) { michael@0: struct js_event event; michael@0: gsize count; michael@0: GError* err = nullptr; michael@0: if (g_io_channel_read_chars(source, michael@0: (gchar*)&event, michael@0: sizeof(event), michael@0: &count, michael@0: &err) != G_IO_STATUS_NORMAL || michael@0: count == 0) { michael@0: break; michael@0: } michael@0: michael@0: //TODO: store device state? michael@0: if (event.type & JS_EVENT_INIT) { michael@0: continue; michael@0: } michael@0: michael@0: nsRefPtr service(GamepadService::GetService()); michael@0: switch (event.type) { michael@0: case JS_EVENT_BUTTON: michael@0: service->NewButtonEvent(index, event.number, !!event.value); michael@0: break; michael@0: case JS_EVENT_AXIS: michael@0: service->NewAxisMoveEvent(index, event.number, michael@0: ((float)event.value) / kMaxAxisValue); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: return TRUE; michael@0: } michael@0: michael@0: // static michael@0: gboolean michael@0: LinuxGamepadService::OnUdevMonitor(GIOChannel* source, michael@0: GIOCondition condition, michael@0: gpointer data) michael@0: { michael@0: if (condition & G_IO_ERR || condition & G_IO_HUP) michael@0: return FALSE; michael@0: michael@0: gService->ReadUdevChange(); michael@0: return TRUE; michael@0: } michael@0: michael@0: } // namespace michael@0: michael@0: namespace mozilla { michael@0: namespace hal_impl { michael@0: michael@0: void StartMonitoringGamepadStatus() michael@0: { michael@0: if (!gService) { michael@0: gService = new LinuxGamepadService(); michael@0: gService->Startup(); michael@0: } michael@0: } michael@0: michael@0: void StopMonitoringGamepadStatus() michael@0: { michael@0: if (gService) { michael@0: gService->Shutdown(); michael@0: delete gService; michael@0: gService = nullptr; michael@0: } michael@0: } michael@0: michael@0: } // namespace hal_impl michael@0: } // namespace mozilla