add linuxJoystick input module (#10970)
* add linuxJoystick input module * close epollfd to avoid leaks
This commit is contained in:
co-authored by
GitHub
parent
91043faced
commit
9060ab4418
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user