Add UHID gamepad support

Similar to UHID keyboard and mouse, but for gamepads.

Can be enabled with --gamepad=uhid or -G.

It is not enabled by default because not all devices support UHID
(there is a permission error on old Android versions).
This commit is contained in:
Romain Vimont 2024-09-06 23:08:08 +02:00
parent b266fdb229
commit b32406681b
9 changed files with 188 additions and 6 deletions

View file

@ -37,6 +37,7 @@ src = [
'src/hid/hid_mouse.c',
'src/trait/frame_source.c',
'src/trait/packet_source.c',
'src/uhid/gamepad_uhid.c',
'src/uhid/keyboard_uhid.c',
'src/uhid/mouse_uhid.c',
'src/uhid/uhid_output.c',

View file

@ -179,9 +179,10 @@ Do not attempt to use "adb reverse" to connect to the device.
.BI "\-\-gamepad " mode
Select how to send gamepad inputs to the device.
Possible values are "disabled" and "aoa":
Possible values are "disabled", "uhid" and "aoa":
- "disabled" does not send keyboard inputs to the device.
- "uhid" simulates a physical HID gamepad using the Linux HID kernel module on the device.
- "aoa" simulates a physical HID gamepad using the AOAv2 protocol. It may only work over USB.
Also see \fB\-\-keyboard\f and R\fB\-\-mouse\fR.

View file

@ -373,13 +373,19 @@ static const struct sc_option options[] = {
.longopt_id = OPT_FORWARD_ALL_CLICKS,
.longopt = "forward-all-clicks",
},
{
.shortopt = 'G',
.text = "Same as --gamepad=uhid.",
},
{
.longopt_id = OPT_GAMEPAD,
.longopt = "gamepad",
.argdesc = "mode",
.text = "Select how to send gamepad inputs to the device.\n"
"Possible values are \"disabled\" and \"aoa\".\n"
"Possible values are \"disabled\", \"uhid\" and \"aoa\".\n"
"\"disabled\" does not send gamepad inputs to the device.\n"
"\"uhid\" simulates a physical HID gamepad using the Linux "
"UHID kernel module on the device.\n"
"\"aoa\" simulates a physical gamepad using the AOAv2 "
"protocol. It may only work over USB.\n"
"Also see --keyboard and --mouse.",
@ -2077,6 +2083,11 @@ parse_gamepad(const char *optarg, enum sc_gamepad_input_mode *mode) {
return true;
}
if (!strcmp(optarg, "uhid")) {
*mode = SC_GAMEPAD_INPUT_MODE_UHID;
return true;
}
if (!strcmp(optarg, "aoa")) {
#ifdef HAVE_USB
*mode = SC_GAMEPAD_INPUT_MODE_AOA;
@ -2659,6 +2670,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_AUDIO_DUP:
opts->audio_dup = true;
break;
case 'G':
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID;
break;
case OPT_GAMEPAD:
if (!parse_gamepad(optarg, &opts->gamepad_input_mode)) {
return false;
@ -2800,6 +2814,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (otg) {
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_AOA;
} else {
// UHID does not work on all devices (with old Android
// versions), so it cannot be enabled by default
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_DISABLED;
}
}

View file

@ -159,6 +159,7 @@ enum sc_mouse_input_mode {
enum sc_gamepad_input_mode {
SC_GAMEPAD_INPUT_MODE_AUTO,
SC_GAMEPAD_INPUT_MODE_DISABLED,
SC_GAMEPAD_INPUT_MODE_UHID,
SC_GAMEPAD_INPUT_MODE_AOA,
};

View file

@ -25,6 +25,7 @@
#include "recorder.h"
#include "screen.h"
#include "server.h"
#include "uhid/gamepad_uhid.h"
#include "uhid/keyboard_uhid.h"
#include "uhid/mouse_uhid.h"
#ifdef HAVE_USB
@ -80,9 +81,12 @@ struct scrcpy {
struct sc_mouse_aoa mouse_aoa;
#endif
};
union {
struct sc_gamepad_uhid gamepad_uhid;
#ifdef HAVE_USB
struct sc_gamepad_aoa gamepad_aoa;
struct sc_gamepad_aoa gamepad_aoa;
#endif
};
struct sc_timeout timeout;
};
@ -734,6 +738,11 @@ aoa_complete:
mp = &s->mouse_uhid.mouse_processor;
}
if (options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID) {
sc_gamepad_uhid_init(&s->gamepad_uhid, &s->controller);
gp = &s->gamepad_uhid.gamepad_processor;
}
sc_controller_configure(&s->controller, acksync, uhid_devices);
if (!sc_controller_start(&s->controller)) {

116
app/src/uhid/gamepad_uhid.c Normal file
View file

@ -0,0 +1,116 @@
#include "gamepad_uhid.h"
#include "hid/hid_gamepad.h"
#include "input_events.h"
#include "util/log.h"
/** Downcast gamepad processor to sc_gamepad_uhid */
#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor)
static void
sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad,
const struct sc_hid_input *hid_input,
const char *name) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT;
msg.uhid_input.id = hid_input->hid_id;
assert(hid_input->size <= SC_HID_MAX_SIZE);
memcpy(msg.uhid_input.data, hid_input->data, hid_input->size);
msg.uhid_input.size = hid_input->size;
if (!sc_controller_push_msg(gamepad->controller, &msg)) {
LOGE("Could not push UHID_INPUT message (%s)", name);
}
}
static void
sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad,
const struct sc_hid_open *hid_open) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
msg.uhid_create.id = hid_open->hid_id;
msg.uhid_create.report_desc = hid_open->report_desc;
msg.uhid_create.report_desc_size = hid_open->report_desc_size;
if (!sc_controller_push_msg(gamepad->controller, &msg)) {
LOGE("Could not push UHID_CREATE message (gamepad)");
}
}
static void
sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad,
const struct sc_hid_close *hid_close) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_DESTROY;
msg.uhid_create.id = hid_close->hid_id;
if (!sc_controller_push_msg(gamepad->controller, &msg)) {
LOGE("Could not push UHID_DESTROY message (gamepad)");
}
}
static void
sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp,
const struct sc_gamepad_device_event *event) {
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
if (event->type == SC_GAMEPAD_DEVICE_ADDED) {
struct sc_hid_open hid_open;
if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open,
event->gamepad_id)) {
return;
}
sc_gamepad_uhid_send_open(gamepad, &hid_open);
} else {
assert(event->type == SC_GAMEPAD_DEVICE_REMOVED);
struct sc_hid_close hid_close;
if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close,
event->gamepad_id)) {
return;
}
sc_gamepad_uhid_send_close(gamepad, &hid_close);
}
}
static void
sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp,
const struct sc_gamepad_axis_event *event) {
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
struct sc_hid_input hid_input;
sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input, event);
sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad axis");
}
static void
sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp,
const struct sc_gamepad_button_event *event) {
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
struct sc_hid_input hid_input;
sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input, event);
sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad button");
}
void
sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad,
struct sc_controller *controller) {
sc_hid_gamepad_init(&gamepad->hid);
gamepad->controller = controller;
static const struct sc_gamepad_processor_ops ops = {
.process_gamepad_device = sc_gamepad_processor_process_gamepad_device,
.process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis,
.process_gamepad_button = sc_gamepad_processor_process_gamepad_button,
};
gamepad->gamepad_processor.ops = &ops;
}

View file

@ -0,0 +1,23 @@
#ifndef SC_GAMEPAD_UHID_H
#define SC_GAMEPAD_UHID_H
#include "common.h"
#include <stdbool.h>
#include "controller.h"
#include "hid/hid_gamepad.h"
#include "trait/gamepad_processor.h"
struct sc_gamepad_uhid {
struct sc_gamepad_processor gamepad_processor; // gamepad processor trait
struct sc_hid_gamepad hid;
struct sc_controller *controller;
};
void
sc_gamepad_uhid_init(struct sc_gamepad_uhid *mouse,
struct sc_controller *controller);
#endif

View file

@ -215,6 +215,9 @@ public class Controller implements AsyncProcessor {
case ControlMessage.TYPE_UHID_INPUT:
getUhidManager().writeInput(msg.getId(), msg.getData());
break;
case ControlMessage.TYPE_UHID_DESTROY:
getUhidManager().close(msg.getId());
break;
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
openHardKeyboardSettings();
break;

View file

@ -95,6 +95,12 @@ public final class UhidManager {
}
}
private void unregisterUhidListener(FileDescriptor fd) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
queue.removeOnFileDescriptorEventListener(fd);
}
}
private static byte[] extractHidOutputData(ByteBuffer buffer) {
/*
* #define UHID_DATA_MAX 4096
@ -199,9 +205,15 @@ public final class UhidManager {
}
public void close(int id) {
FileDescriptor fd = fds.get(id);
assert fd != null;
close(fd);
// Linux: Documentation/hid/uhid.rst
// If you close() the fd, the device is automatically unregistered and destroyed internally.
FileDescriptor fd = fds.remove(id);
if (fd != null) {
unregisterUhidListener(fd);
close(fd);
} else {
Ln.w("Closing unknown UHID device: " + id);
}
}
public void closeAll() {