Skip to content

Commit

Permalink
Add UHID gamepad support
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
rom1v committed Sep 7, 2024
1 parent 330d844 commit b0b0590
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 6 deletions.
1 change: 1 addition & 0 deletions app/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 6 additions & 1 deletion app/scrcpy.1
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,18 @@ Start in fullscreen.
.B \-\-force\-adb\-forward
Do not attempt to use "adb reverse" to connect to the device.

.TP
.B \-K
Same as \fB\-\-gamepad=uhid\fR.

.TP
.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.
Expand Down
18 changes: 17 additions & 1 deletion app/src/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2797,6 +2811,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
if (opts->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AUTO) {
// UHID does not work on all devices (with old Android
// versions), so it cannot be enabled by default
opts->gamepad_input_mode = otg ? SC_GAMEPAD_INPUT_MODE_AOA
: SC_GAMEPAD_INPUT_MODE_DISABLED;
}
Expand Down
1 change: 1 addition & 0 deletions app/src/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
11 changes: 10 additions & 1 deletion app/src/scrcpy.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -749,6 +753,11 @@ scrcpy(struct scrcpy_options *options) {
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)) {
Expand Down
122 changes: 122 additions & 0 deletions app/src/uhid/gamepad_uhid.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#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;
if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input,
event)) {
return;
}

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;
if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input,
event)) {
return;
}

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;
}
23 changes: 23 additions & 0 deletions app/src/uhid/gamepad_uhid.h
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ private boolean handleEvent() throws IOException {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ private void registerUhidListener(int id, FileDescriptor fd) {
}
}

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
Expand Down Expand Up @@ -199,9 +205,15 @@ private static byte[] buildUhidInput2Req(byte[] data) {
}

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() {
Expand Down

0 comments on commit b0b0590

Please sign in to comment.