add linuxJoystick input module (#10970)

* add linuxJoystick input module

* close epollfd to avoid leaks
This commit is contained in:
Jonathan Bennett
2026-07-10 00:29:23 -05:00
committed by GitHub
co-authored by GitHub
parent 91043faced
commit 9060ab4418
9 changed files with 268 additions and 5 deletions
+3
View File
@@ -37,6 +37,7 @@
#if defined(ARCH_PORTDUINO)
#include "api/WiFiServerAPI.h"
#include "input/LinuxInputImpl.h"
#include "input/LinuxJoystick.h"
#endif
// Working USB detection for powered/charging states on the RAK platform
@@ -860,6 +861,8 @@ void Power::reboot()
#ifdef __linux__
if (aLinuxInputImpl)
aLinuxInputImpl->deInit();
if (aLinuxJoystick)
aLinuxJoystick->deInit();
#endif
SPI.end();
Wire.end();
+4
View File
@@ -10,6 +10,7 @@
#if ARCH_PORTDUINO
#include "input/LinuxInputImpl.h"
#include "input/LinuxJoystick.h"
#include "input/SeesawRotary.h"
#include "platform/portduino/PortduinoGlue.h"
#endif
@@ -430,6 +431,9 @@ void InputBroker::Init()
// Linux evdev keyboard input only - macOS has no <linux/input.h>.
aLinuxInputImpl = new LinuxInputImpl();
aLinuxInputImpl->init();
// Linux evdev gamepad/joystick input (D-pad + confirm/cancel buttons).
aLinuxJoystick = new LinuxJoystick("LinuxJoystick");
aLinuxJoystick->init();
#endif
}
#endif
+8 -2
View File
@@ -25,8 +25,14 @@ LinuxInput::LinuxInput(const char *name) : concurrency::OSThread(name)
void LinuxInput::deInit()
{
// reboot() on native does execv() in-place, which only closes O_CLOEXEC fds.
// Close both descriptors here so we don't leak one per restart.
if (fd >= 0)
close(fd);
if (epollfd >= 0)
close(epollfd);
fd = -1;
epollfd = -1;
}
int32_t LinuxInput::runOnce()
@@ -35,14 +41,14 @@ int32_t LinuxInput::runOnce()
if (firstTime) {
if (portduino_config.keyboardDevice == "")
return disable();
fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR);
fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR | O_CLOEXEC);
if (fd < 0)
return disable();
ret = ioctl(fd, EVIOCGRAB, (void *)1);
if (ret != 0)
return disable();
epollfd = epoll_create1(0);
epollfd = epoll_create1(EPOLL_CLOEXEC);
assert(epollfd >= 0);
ev.events = EPOLLIN;
+1 -1
View File
@@ -44,7 +44,7 @@ class LinuxInput : public Observable<const InputEvent *>, public concurrency::OS
int fd = -1;
int ret;
uint8_t report[8];
int epollfd;
int epollfd = -1;
struct epoll_event ev;
uint8_t modifiers = 0;
std::map<int, char> keymap{
+159
View File
@@ -0,0 +1,159 @@
#include "configuration.h"
#if ARCH_PORTDUINO && defined(__linux__)
#include "InputBroker.h"
#include "LinuxJoystick.h"
#include "platform/portduino/PortduinoGlue.h"
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
LinuxJoystick *aLinuxJoystick;
LinuxJoystick::LinuxJoystick(const char *name) : concurrency::OSThread(name)
{
this->_originName = name;
}
// Translate a config.yaml action name into a broker event. Returns INPUT_BROKER_NONE
// for unknown names so a typo simply leaves that button unmapped.
static input_broker_event joystickActionToEvent(const std::string &action)
{
if (action == "select")
return INPUT_BROKER_SELECT;
if (action == "cancel")
return INPUT_BROKER_CANCEL;
if (action == "back")
return INPUT_BROKER_BACK;
if (action == "up")
return INPUT_BROKER_UP;
if (action == "down")
return INPUT_BROKER_DOWN;
if (action == "left")
return INPUT_BROKER_LEFT;
if (action == "right")
return INPUT_BROKER_RIGHT;
if (action == "user" || action == "userpress")
return INPUT_BROKER_USER_PRESS;
return INPUT_BROKER_NONE;
}
void LinuxJoystick::init()
{
if (portduino_config.joystickButtons.empty()) {
// No config override: fall back to the built-in defaults.
buttonMap[JOY_BTN_A] = INPUT_BROKER_SELECT;
buttonMap[JOY_BTN_B] = INPUT_BROKER_CANCEL;
} else {
for (const auto &button : portduino_config.joystickButtons) {
input_broker_event event = joystickActionToEvent(button.second);
if (event != INPUT_BROKER_NONE)
buttonMap[button.first] = event;
}
}
inputBroker->registerSource(this);
}
void LinuxJoystick::deInit()
{
// reboot() on native does execv() in-place, which only closes O_CLOEXEC fds.
// Close both descriptors here so we don't leak one per restart.
if (fd >= 0)
close(fd);
if (epollfd >= 0)
close(epollfd);
fd = -1;
epollfd = -1;
}
// Bucket a raw axis value into a direction zone: -1 (min edge), 0 (center), +1 (max edge).
static int axisZone(int value)
{
if (value <= JOY_AXIS_LOW)
return -1;
if (value >= JOY_AXIS_HIGH)
return 1;
return 0;
}
int32_t LinuxJoystick::runOnce()
{
if (firstTime) {
if (portduino_config.joystickDevice == "")
return disable();
fd = open(portduino_config.joystickDevice.c_str(), O_RDWR | O_CLOEXEC);
if (fd < 0)
return disable();
if (ioctl(fd, EVIOCGRAB, (void *)1) != 0)
return disable();
epollfd = epoll_create1(EPOLL_CLOEXEC);
assert(epollfd >= 0);
ev.events = EPOLLIN;
ev.data.fd = fd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev)) {
perror("joystick: unable to epoll add");
return disable();
}
// This is the first time the OSThread library has called this function, so do port setup
firstTime = false;
}
int nfds = epoll_wait(epollfd, events, JOY_MAX_EVENTS, 1);
if (nfds < 0) {
perror("joystick: epoll_wait failed");
return disable();
} else if (nfds == 0) {
return 50;
}
for (int i = 0; i < nfds; i++) {
struct input_event evs[64];
int rd = read(events[i].data.fd, evs, sizeof(evs));
if (rd < (signed int)sizeof(struct input_event))
continue;
for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) {
InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
e.kbchar = 0;
unsigned int type = evs[j].type;
unsigned int code = evs[j].code;
int value = evs[j].value;
if (type == EV_ABS) {
// D-pad reports as ABS_X / ABS_Y with digital 0 / 127 / 255 values.
// Emit exactly once when an axis crosses from center to an edge.
if (code == ABS_X) {
int zone = axisZone(value);
if (zone != axisZone(lastX) && zone != 0)
e.inputEvent = (zone < 0) ? INPUT_BROKER_LEFT : INPUT_BROKER_RIGHT;
lastX = value;
} else if (code == ABS_Y) {
int zone = axisZone(value);
if (zone != axisZone(lastY) && zone != 0)
e.inputEvent = (zone < 0) ? INPUT_BROKER_UP : INPUT_BROKER_DOWN;
lastY = value;
}
} else if (type == EV_KEY && value == 1) {
// Look up the configured action for this button; unmapped buttons are ignored.
auto mapped = buttonMap.find(code);
if (mapped != buttonMap.end())
e.inputEvent = mapped->second;
}
if (e.inputEvent != INPUT_BROKER_NONE) {
LOG_DEBUG("joystick: %s event %d", this->_originName, e.inputEvent);
this->notifyObservers(&e);
}
}
}
return 50; // Poll every 50msec
}
#endif
+51
View File
@@ -0,0 +1,51 @@
#pragma once
// Linux evdev gamepad/joystick input. Only compiled on Linux portduino targets;
// macOS / non-Linux builds have no <linux/input.h> or epoll. Unlike LinuxInput
// (keyboard, EV_KEY only) this decodes the D-pad from EV_ABS axes as well.
#if ARCH_PORTDUINO && defined(__linux__)
#include "InputBroker.h"
#include "concurrency/OSThread.h"
#include <linux/input.h>
#include <map>
#include <stdint.h>
#include <sys/epoll.h>
#define JOY_MAX_EVENTS 10
// Default button map when config.yaml has no [Input] JoystickButtons override.
// Codes confirmed by evdev capture on a 0079:0011 "USB Gamepad".
#define JOY_BTN_A BTN_THUMB // 0x121 -> INPUT_BROKER_SELECT
#define JOY_BTN_B BTN_THUMB2 // 0x122 -> INPUT_BROKER_CANCEL
#define JOY_AXIS_CENTER 127 // D-pad resting value
#define JOY_AXIS_LOW 64 // below this -> "min" edge (0)
#define JOY_AXIS_HIGH 192 // above this -> "max" edge (255)
class LinuxJoystick : public Observable<const InputEvent *>, public concurrency::OSThread
{
public:
explicit LinuxJoystick(const char *name);
void init(); // Registers this source with the InputBroker
void deInit(); // Strictly for cleanly "rebooting" the binary on native
protected:
virtual int32_t runOnce() override;
private:
const char *_originName;
bool firstTime = true;
// evdev button code -> broker event, built from config (or defaults) in init().
std::map<int, input_broker_event> buttonMap;
struct epoll_event events[JOY_MAX_EVENTS];
struct epoll_event ev;
int fd = -1;
int epollfd = -1;
// Latch the last emitted axis position so a held direction fires exactly
// once (on the transition away from center), not a continuous stream.
int lastX = JOY_AXIS_CENTER;
int lastY = JOY_AXIS_CENTER;
};
extern LinuxJoystick *aLinuxJoystick;
#endif
+19
View File
@@ -1038,6 +1038,25 @@ bool loadConfig(const char *configPath)
if (yamlConfig["Input"]) {
portduino_config.keyboardDevice = (yamlConfig["Input"]["KeyboardDevice"]).as<std::string>("");
portduino_config.pointerDevice = (yamlConfig["Input"]["PointerDevice"]).as<std::string>("");
portduino_config.joystickDevice = (yamlConfig["Input"]["JoystickDevice"]).as<std::string>("");
if (yamlConfig["Input"]["JoystickButtons"]) {
// action name -> evdev button code (hex like 0x122 or decimal); stored inverted
// as code -> lowercase action name for the driver to look up per keypress.
for (const auto &button : yamlConfig["Input"]["JoystickButtons"]) {
std::string action = button.first.as<std::string>("");
for (auto &c : action)
c = tolower(c);
int code = 0;
try {
// base 0 accepts hex (0x122) or decimal; a malformed value just skips this entry.
code = std::stoi(button.second.as<std::string>(""), nullptr, 0);
} catch (const std::exception &) {
code = 0;
}
if (code != 0 && action != "")
portduino_config.joystickButtons[code] = action;
}
}
readGPIOFromYaml(yamlConfig["Input"]["User"], portduino_config.userButtonPin);
readGPIOFromYaml(yamlConfig["Input"]["TrackballUp"], portduino_config.tbUpPin);
+13
View File
@@ -160,6 +160,11 @@ extern struct portduino_config_struct {
// Input
std::string keyboardDevice = "";
std::string pointerDevice = "";
std::string joystickDevice = "";
// Joystick/gamepad button map: evdev button code -> lowercase action name
// ("select", "cancel", "back", "up", "down", "left", "right", "user").
// Empty means the LinuxJoystick driver uses its built-in defaults.
std::map<int, std::string> joystickButtons;
int tbDirection;
pinMapping userButtonPin = {"Input", "User"};
pinMapping tbUpPin = {"Input", "TrackballUp"};
@@ -458,6 +463,14 @@ extern struct portduino_config_struct {
out << YAML::Key << "KeyboardDevice" << YAML::Value << keyboardDevice;
if (pointerDevice != "")
out << YAML::Key << "PointerDevice" << YAML::Value << pointerDevice;
if (joystickDevice != "")
out << YAML::Key << "JoystickDevice" << YAML::Value << joystickDevice;
if (!joystickButtons.empty()) {
out << YAML::Key << "JoystickButtons" << YAML::Value << YAML::BeginMap;
for (const auto &button : joystickButtons)
out << YAML::Key << button.second << YAML::Value << button.first;
out << YAML::EndMap;
}
for (const auto *input_pin : all_pins) {
if (input_pin->config_section == "Input" && input_pin->enabled) {