add linuxJoystick input module (#10970)
* add linuxJoystick input module * close epollfd to avoid leaks
This commit is contained in:
+10
-2
@@ -173,6 +173,16 @@ Input:
|
|||||||
|
|
||||||
# KeyboardDevice: /dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd
|
# KeyboardDevice: /dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd
|
||||||
|
|
||||||
|
### Configure device for USB gamepad/joystick input (evdev). The D-pad drives
|
||||||
|
### Up/Down/Left/Right; buttons are mapped below. Find button codes with `evtest`.
|
||||||
|
# JoystickDevice: /dev/input/by-id/usb-0079_USB_Gamepad-event-joystick
|
||||||
|
### Map evdev button codes (hex or decimal) to actions. Omit to use built-in
|
||||||
|
### defaults (select=0x121, cancel=0x122). Actions: select, cancel, back, up,
|
||||||
|
### down, left, right, user.
|
||||||
|
# JoystickButtons:
|
||||||
|
# select: 0x122
|
||||||
|
# cancel: 0x121
|
||||||
|
|
||||||
### Standard User Button Config
|
### Standard User Button Config
|
||||||
# UserButton: 6
|
# UserButton: 6
|
||||||
|
|
||||||
@@ -199,13 +209,11 @@ Webserver:
|
|||||||
# SSLKey: /etc/meshtasticd/ssl/private_key.pem # Path to SSL Key, generated if not present
|
# SSLKey: /etc/meshtasticd/ssl/private_key.pem # Path to SSL Key, generated if not present
|
||||||
# SSLCert: /etc/meshtasticd/ssl/certificate.pem # Path to SSL Certificate, generated if not present
|
# SSLCert: /etc/meshtasticd/ssl/certificate.pem # Path to SSL Certificate, generated if not present
|
||||||
|
|
||||||
|
|
||||||
HostMetrics:
|
HostMetrics:
|
||||||
# ReportInterval: 30 # Interval in minutes between HostMetrics report packets, or 0 for disabled
|
# ReportInterval: 30 # Interval in minutes between HostMetrics report packets, or 0 for disabled
|
||||||
# Channel: 0 # channel to send Host Metrics over. Defaults to the primary channel.
|
# Channel: 0 # channel to send Host Metrics over. Defaults to the primary channel.
|
||||||
# UserStringCommand: cat /sys/firmware/devicetree/base/serial-number # Command to execute, to send the results as the userString
|
# UserStringCommand: cat /sys/firmware/devicetree/base/serial-number # Command to execute, to send the results as the userString
|
||||||
|
|
||||||
|
|
||||||
Config:
|
Config:
|
||||||
# DisplayMode: TWOCOLOR # uncomment to force BaseUI
|
# DisplayMode: TWOCOLOR # uncomment to force BaseUI
|
||||||
# DisplayMode: COLOR # uncomment to force MUI
|
# DisplayMode: COLOR # uncomment to force MUI
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
#if defined(ARCH_PORTDUINO)
|
#if defined(ARCH_PORTDUINO)
|
||||||
#include "api/WiFiServerAPI.h"
|
#include "api/WiFiServerAPI.h"
|
||||||
#include "input/LinuxInputImpl.h"
|
#include "input/LinuxInputImpl.h"
|
||||||
|
#include "input/LinuxJoystick.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Working USB detection for powered/charging states on the RAK platform
|
// Working USB detection for powered/charging states on the RAK platform
|
||||||
@@ -860,6 +861,8 @@ void Power::reboot()
|
|||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
if (aLinuxInputImpl)
|
if (aLinuxInputImpl)
|
||||||
aLinuxInputImpl->deInit();
|
aLinuxInputImpl->deInit();
|
||||||
|
if (aLinuxJoystick)
|
||||||
|
aLinuxJoystick->deInit();
|
||||||
#endif
|
#endif
|
||||||
SPI.end();
|
SPI.end();
|
||||||
Wire.end();
|
Wire.end();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
#include "input/LinuxInputImpl.h"
|
#include "input/LinuxInputImpl.h"
|
||||||
|
#include "input/LinuxJoystick.h"
|
||||||
#include "input/SeesawRotary.h"
|
#include "input/SeesawRotary.h"
|
||||||
#include "platform/portduino/PortduinoGlue.h"
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -430,6 +431,9 @@ void InputBroker::Init()
|
|||||||
// Linux evdev keyboard input only - macOS has no <linux/input.h>.
|
// Linux evdev keyboard input only - macOS has no <linux/input.h>.
|
||||||
aLinuxInputImpl = new LinuxInputImpl();
|
aLinuxInputImpl = new LinuxInputImpl();
|
||||||
aLinuxInputImpl->init();
|
aLinuxInputImpl->init();
|
||||||
|
// Linux evdev gamepad/joystick input (D-pad + confirm/cancel buttons).
|
||||||
|
aLinuxJoystick = new LinuxJoystick("LinuxJoystick");
|
||||||
|
aLinuxJoystick->init();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -25,8 +25,14 @@ LinuxInput::LinuxInput(const char *name) : concurrency::OSThread(name)
|
|||||||
|
|
||||||
void LinuxInput::deInit()
|
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)
|
if (fd >= 0)
|
||||||
close(fd);
|
close(fd);
|
||||||
|
if (epollfd >= 0)
|
||||||
|
close(epollfd);
|
||||||
|
fd = -1;
|
||||||
|
epollfd = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t LinuxInput::runOnce()
|
int32_t LinuxInput::runOnce()
|
||||||
@@ -35,14 +41,14 @@ int32_t LinuxInput::runOnce()
|
|||||||
if (firstTime) {
|
if (firstTime) {
|
||||||
if (portduino_config.keyboardDevice == "")
|
if (portduino_config.keyboardDevice == "")
|
||||||
return disable();
|
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)
|
if (fd < 0)
|
||||||
return disable();
|
return disable();
|
||||||
ret = ioctl(fd, EVIOCGRAB, (void *)1);
|
ret = ioctl(fd, EVIOCGRAB, (void *)1);
|
||||||
if (ret != 0)
|
if (ret != 0)
|
||||||
return disable();
|
return disable();
|
||||||
|
|
||||||
epollfd = epoll_create1(0);
|
epollfd = epoll_create1(EPOLL_CLOEXEC);
|
||||||
assert(epollfd >= 0);
|
assert(epollfd >= 0);
|
||||||
|
|
||||||
ev.events = EPOLLIN;
|
ev.events = EPOLLIN;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class LinuxInput : public Observable<const InputEvent *>, public concurrency::OS
|
|||||||
int fd = -1;
|
int fd = -1;
|
||||||
int ret;
|
int ret;
|
||||||
uint8_t report[8];
|
uint8_t report[8];
|
||||||
int epollfd;
|
int epollfd = -1;
|
||||||
struct epoll_event ev;
|
struct epoll_event ev;
|
||||||
uint8_t modifiers = 0;
|
uint8_t modifiers = 0;
|
||||||
std::map<int, char> keymap{
|
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"]) {
|
if (yamlConfig["Input"]) {
|
||||||
portduino_config.keyboardDevice = (yamlConfig["Input"]["KeyboardDevice"]).as<std::string>("");
|
portduino_config.keyboardDevice = (yamlConfig["Input"]["KeyboardDevice"]).as<std::string>("");
|
||||||
portduino_config.pointerDevice = (yamlConfig["Input"]["PointerDevice"]).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"]["User"], portduino_config.userButtonPin);
|
||||||
readGPIOFromYaml(yamlConfig["Input"]["TrackballUp"], portduino_config.tbUpPin);
|
readGPIOFromYaml(yamlConfig["Input"]["TrackballUp"], portduino_config.tbUpPin);
|
||||||
|
|||||||
@@ -160,6 +160,11 @@ extern struct portduino_config_struct {
|
|||||||
// Input
|
// Input
|
||||||
std::string keyboardDevice = "";
|
std::string keyboardDevice = "";
|
||||||
std::string pointerDevice = "";
|
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;
|
int tbDirection;
|
||||||
pinMapping userButtonPin = {"Input", "User"};
|
pinMapping userButtonPin = {"Input", "User"};
|
||||||
pinMapping tbUpPin = {"Input", "TrackballUp"};
|
pinMapping tbUpPin = {"Input", "TrackballUp"};
|
||||||
@@ -458,6 +463,14 @@ extern struct portduino_config_struct {
|
|||||||
out << YAML::Key << "KeyboardDevice" << YAML::Value << keyboardDevice;
|
out << YAML::Key << "KeyboardDevice" << YAML::Value << keyboardDevice;
|
||||||
if (pointerDevice != "")
|
if (pointerDevice != "")
|
||||||
out << YAML::Key << "PointerDevice" << YAML::Value << 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) {
|
for (const auto *input_pin : all_pins) {
|
||||||
if (input_pin->config_section == "Input" && input_pin->enabled) {
|
if (input_pin->config_section == "Input" && input_pin->enabled) {
|
||||||
|
|||||||
Reference in New Issue
Block a user