313 lines
12 KiB
C++
313 lines
12 KiB
C++
// Copyright 2020 The ChromiumOS Authors
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "sommelier.h" // NOLINT(build/include_directory)
|
|
#include "sommelier-tracing.h" // NOLINT(build/include_directory)
|
|
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <libevdev/libevdev.h>
|
|
#include <libevdev/libevdev-uinput.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "gaming-input-unstable-v2-client-protocol.h" // NOLINT(build/include_directory)
|
|
|
|
// Overview of state management via gaming events, in order:
|
|
// 1) Acquire gaming seats (in sommelier.cc)
|
|
// 2) Add listeners to gaming seats
|
|
// 3) Listen for zcr_gaming_seat_v2.gamepad_added to construct a 'default'
|
|
// game controller (not currently implemented)
|
|
// Calls libevdev_new, libevdev_enable_event_type,
|
|
// libevdev_uinput_create_from_device
|
|
// 4) Listen for zcr_gaming_seat_v2.gamepad_added_with_device_info to construct
|
|
// a custom game controller
|
|
// Calls libevdev_new
|
|
// 5) Listen for zcr_gamepad_v2.axis_added to fill in a custom game controller
|
|
// Calls libevdev_enable_event_type
|
|
// 6) Listen for zcr_gamepad_v2.activated to finalize a custom game controller
|
|
// Calls libevdev_uinput_create_from_device
|
|
// 7) Listen for zcr_gamepad_v2.axis to set frame state for game controller
|
|
// Calls libevdev_uinput_write_event
|
|
// 8) Listen for zcr_gamepad_v2.button to set frame state for game controller
|
|
// Calls libevdev_uinput_write_event
|
|
// 9) Listen for zcr_gamepad_v2.frame to emit collected frame
|
|
// Calls libevdev_uinput_write_event(EV_SYN)
|
|
// 10) Listen for zcr_gamepad_v2.removed to destroy gamepad
|
|
// Must handle gamepads in all states of construction or error
|
|
|
|
enum GamepadActivationState {
|
|
kStateUnknown = 0, // Should not happen
|
|
kStatePending = 1, // Constructed, pending axis definition
|
|
kStateActivated = 2, // Fully activated
|
|
kStateError = 3 // Error occurred during construction; ignore gracefully
|
|
};
|
|
|
|
const char kXboxName[] = "Microsoft X-Box One S pad";
|
|
const uint32_t kUsbBus = 0x03;
|
|
const uint32_t kXboxVendor = 0x45e;
|
|
const uint32_t kXboxProduct = 0x2ea;
|
|
const uint32_t kXboxVersion = 0x301;
|
|
|
|
// Note: the BT vendor ID for SteelSeries is due to a chipset bug
|
|
// and is not an actual claimed Vendor ID.
|
|
const uint32_t kSteelSeriesVendorBt = 0x111;
|
|
const uint32_t kSteelSeriesProductStratusDuoBt = 0x1431;
|
|
const uint32_t kSteelSeriesProductStratusPlusBt = 0x1434;
|
|
|
|
const uint32_t kStadiaVendor = 0x18d1;
|
|
const uint32_t kStadiaProduct = 0x9400;
|
|
const uint32_t kStadiaVersion = 0x111;
|
|
|
|
// Note: the majority of protocol errors are treated as non-fatal, and
|
|
// are intended to be handled gracefully, as is removal at any
|
|
// state of construction or operation. We should expect that
|
|
// 'sudden removal' can happen at any time, due to hotplugging
|
|
// or unexpected state changes from the wayland server.
|
|
|
|
static void sl_internal_gamepad_removed(void* data,
|
|
struct zcr_gamepad_v2* gamepad) {
|
|
TRACE_EVENT("gaming", "sl_internal_gamepad_removed");
|
|
struct sl_host_gamepad* host_gamepad = (struct sl_host_gamepad*)data;
|
|
|
|
assert(host_gamepad->state == kStatePending ||
|
|
host_gamepad->state == kStateActivated ||
|
|
host_gamepad->state == kStateError);
|
|
|
|
if (host_gamepad->uinput_dev != NULL)
|
|
libevdev_uinput_destroy(host_gamepad->uinput_dev);
|
|
if (host_gamepad->ev_dev != NULL)
|
|
libevdev_free(host_gamepad->ev_dev);
|
|
|
|
zcr_gamepad_v2_destroy(gamepad);
|
|
|
|
wl_list_remove(&host_gamepad->link);
|
|
delete host_gamepad;
|
|
}
|
|
|
|
static uint32_t remap_axis(struct sl_host_gamepad* host_gamepad,
|
|
uint32_t axis) {
|
|
if (host_gamepad->axes_quirk) {
|
|
if (axis == ABS_Z)
|
|
axis = ABS_RX;
|
|
else if (axis == ABS_RZ)
|
|
axis = ABS_RY;
|
|
else if (axis == ABS_BRAKE)
|
|
axis = ABS_Z;
|
|
else if (axis == ABS_GAS)
|
|
axis = ABS_RZ;
|
|
}
|
|
return axis;
|
|
}
|
|
|
|
static void sl_internal_gamepad_axis(void* data,
|
|
struct zcr_gamepad_v2* gamepad,
|
|
uint32_t time,
|
|
uint32_t axis,
|
|
wl_fixed_t value) {
|
|
TRACE_EVENT("gaming", "sl_internal_gamepad_axis");
|
|
struct sl_host_gamepad* host_gamepad = (struct sl_host_gamepad*)data;
|
|
|
|
if (host_gamepad->state != kStateActivated)
|
|
return;
|
|
|
|
axis = remap_axis(host_gamepad, axis);
|
|
|
|
// Note: incoming time is ignored, it will be regenerated from current time.
|
|
libevdev_uinput_write_event(host_gamepad->uinput_dev, EV_ABS, axis,
|
|
wl_fixed_to_double(value));
|
|
}
|
|
|
|
static void sl_internal_gamepad_button(void* data,
|
|
struct zcr_gamepad_v2* gamepad,
|
|
uint32_t time,
|
|
uint32_t button,
|
|
uint32_t state,
|
|
wl_fixed_t analog) {
|
|
TRACE_EVENT("gaming", "sl_internal_gamepad_button");
|
|
struct sl_host_gamepad* host_gamepad = (struct sl_host_gamepad*)data;
|
|
|
|
if (host_gamepad->state != kStateActivated)
|
|
return;
|
|
|
|
// Note: Exo wayland server always sends analog==0, only pay attention
|
|
// to state.
|
|
int value = (state == ZCR_GAMEPAD_V2_BUTTON_STATE_PRESSED) ? 1 : 0;
|
|
|
|
// Note: incoming time is ignored, it will be regenerated from current time.
|
|
libevdev_uinput_write_event(host_gamepad->uinput_dev, EV_KEY, button, value);
|
|
}
|
|
|
|
static void sl_internal_gamepad_frame(void* data,
|
|
struct zcr_gamepad_v2* gamepad,
|
|
uint32_t time) {
|
|
TRACE_EVENT("gaming", "sl_internal_gamepad_frame");
|
|
struct sl_host_gamepad* host_gamepad = (struct sl_host_gamepad*)data;
|
|
|
|
if (host_gamepad->state != kStateActivated)
|
|
return;
|
|
|
|
// Note: incoming time is ignored, it will be regenerated from current time.
|
|
libevdev_uinput_write_event(host_gamepad->uinput_dev, EV_SYN, SYN_REPORT, 0);
|
|
}
|
|
|
|
static void sl_internal_gamepad_axis_added(void* data,
|
|
struct zcr_gamepad_v2* gamepad,
|
|
uint32_t index,
|
|
int32_t min_value,
|
|
int32_t max_value,
|
|
int32_t flat,
|
|
int32_t fuzz,
|
|
int32_t resolution) {
|
|
TRACE_EVENT("gaming", "sl_internal_gamepad_axis_added");
|
|
struct sl_host_gamepad* host_gamepad = (struct sl_host_gamepad*)data;
|
|
struct input_absinfo info = {.value = 0, // Does this matter?
|
|
.minimum = min_value,
|
|
.maximum = max_value,
|
|
.fuzz = fuzz,
|
|
.flat = flat,
|
|
.resolution = resolution};
|
|
|
|
if (host_gamepad->state != kStatePending) {
|
|
fprintf(stderr, "error: %s invoked in unexpected state %d\n", __func__,
|
|
host_gamepad->state);
|
|
host_gamepad->state = kStateError;
|
|
return;
|
|
}
|
|
|
|
index = remap_axis(host_gamepad, index);
|
|
|
|
libevdev_enable_event_code(host_gamepad->ev_dev, EV_ABS, index, &info);
|
|
}
|
|
|
|
static void sl_internal_gamepad_activated(void* data,
|
|
struct zcr_gamepad_v2* gamepad) {
|
|
TRACE_EVENT("gaming", "sl_internal_gamepad_activated");
|
|
struct sl_host_gamepad* host_gamepad = (struct sl_host_gamepad*)data;
|
|
|
|
if (host_gamepad->state != kStatePending) {
|
|
fprintf(stderr, "error: %s invoked in unexpected state %d\n", __func__,
|
|
host_gamepad->state);
|
|
host_gamepad->state = kStateError;
|
|
return;
|
|
}
|
|
|
|
int err = libevdev_uinput_create_from_device(host_gamepad->ev_dev,
|
|
LIBEVDEV_UINPUT_OPEN_MANAGED,
|
|
&host_gamepad->uinput_dev);
|
|
if (err == 0) {
|
|
// TODO(kenalba): can we destroy and clean up the ev_dev now?
|
|
host_gamepad->state = kStateActivated;
|
|
} else {
|
|
fprintf(stderr,
|
|
"error: libevdev_uinput_create_from_device failed with error %d\n",
|
|
err);
|
|
host_gamepad->state = kStateError;
|
|
}
|
|
}
|
|
|
|
static void sl_internal_gamepad_vibrator_added(
|
|
void* data,
|
|
struct zcr_gamepad_v2* gamepad,
|
|
struct zcr_gamepad_vibrator_v2* vibrator) {
|
|
TRACE_EVENT("gaming", "sl_internal_gamepad_vibrator_added");
|
|
// TODO(kenalba): add vibration logic
|
|
}
|
|
|
|
static const struct zcr_gamepad_v2_listener sl_internal_gamepad_listener = {
|
|
sl_internal_gamepad_removed, sl_internal_gamepad_axis,
|
|
sl_internal_gamepad_button, sl_internal_gamepad_frame,
|
|
sl_internal_gamepad_axis_added, sl_internal_gamepad_activated,
|
|
sl_internal_gamepad_vibrator_added};
|
|
|
|
static void sl_internal_gaming_seat_gamepad_added_with_device_info(
|
|
void* data,
|
|
struct zcr_gaming_seat_v2* gaming_seat,
|
|
struct zcr_gamepad_v2* gamepad,
|
|
const char* name,
|
|
uint32_t bus,
|
|
uint32_t vendor_id,
|
|
uint32_t product_id,
|
|
uint32_t version) {
|
|
TRACE_EVENT("gaming",
|
|
"sl_internal_gaming_seat_gamepad_added_with_device_info");
|
|
struct sl_context* ctx = (struct sl_context*)data;
|
|
struct sl_host_gamepad* host_gamepad = new sl_host_gamepad();
|
|
wl_list_insert(&ctx->gamepads, &host_gamepad->link);
|
|
zcr_gamepad_v2_add_listener(gamepad, &sl_internal_gamepad_listener,
|
|
host_gamepad);
|
|
|
|
host_gamepad->ctx = ctx;
|
|
host_gamepad->state = kStatePending;
|
|
host_gamepad->ev_dev = libevdev_new();
|
|
host_gamepad->uinput_dev = NULL;
|
|
host_gamepad->axes_quirk = false;
|
|
|
|
if (host_gamepad->ev_dev == NULL) {
|
|
fprintf(stderr, "error: libevdev_new failed\n");
|
|
host_gamepad->state = kStateError;
|
|
return;
|
|
}
|
|
|
|
// We provide limited remapping at this time. Only moderately XBox360
|
|
// HID compatible controllers are likely to work well.
|
|
|
|
if (product_id == kStadiaProduct && vendor_id == kStadiaVendor &&
|
|
version == kStadiaVersion) {
|
|
host_gamepad->axes_quirk = true;
|
|
} else if (bus == ZCR_GAMING_SEAT_V2_BUS_TYPE_BLUETOOTH &&
|
|
vendor_id == kSteelSeriesVendorBt &&
|
|
(product_id == kSteelSeriesProductStratusDuoBt ||
|
|
product_id == kSteelSeriesProductStratusPlusBt)) {
|
|
host_gamepad->axes_quirk = true;
|
|
}
|
|
|
|
// Describe a common controller
|
|
libevdev_set_name(host_gamepad->ev_dev, kXboxName);
|
|
libevdev_set_id_bustype(host_gamepad->ev_dev, kUsbBus);
|
|
libevdev_set_id_vendor(host_gamepad->ev_dev, kXboxVendor);
|
|
libevdev_set_id_product(host_gamepad->ev_dev, kXboxProduct);
|
|
libevdev_set_id_version(host_gamepad->ev_dev, kXboxVersion);
|
|
|
|
// Enable common set of buttons
|
|
|
|
// Note: Do not enable BTN_TL2 or BTN_TR2, as they will significantly
|
|
// change the Linux joydev interpretation of the triggers on ABS_Z/ABS_RZ.
|
|
int buttons[] = {BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST,
|
|
BTN_TL, BTN_TR, BTN_THUMBL, BTN_THUMBR,
|
|
BTN_SELECT, BTN_START, BTN_MODE};
|
|
|
|
for (unsigned int i = 0; i < ARRAY_SIZE(buttons); i++)
|
|
libevdev_enable_event_code(host_gamepad->ev_dev, EV_KEY, buttons[i], NULL);
|
|
} // NOLINT(whitespace/indent), lint bug b/173143790
|
|
|
|
// Note: not currently implemented by Exo.
|
|
static void sl_internal_gaming_seat_gamepad_added(
|
|
void* data,
|
|
struct zcr_gaming_seat_v2* gaming_seat,
|
|
struct zcr_gamepad_v2* gamepad) {
|
|
TRACE_EVENT("gaming", "sl_internal_gaming_seat_gamepad_added");
|
|
fprintf(stderr,
|
|
"error: sl_internal_gaming_seat_gamepad_added unimplemented\n");
|
|
}
|
|
|
|
static const struct zcr_gaming_seat_v2_listener
|
|
sl_internal_gaming_seat_listener = {
|
|
sl_internal_gaming_seat_gamepad_added,
|
|
sl_internal_gaming_seat_gamepad_added_with_device_info};
|
|
|
|
void sl_gaming_seat_add_listener(struct sl_context* ctx) {
|
|
if (ctx->gaming_input_manager && ctx->gaming_input_manager->internal) {
|
|
TRACE_EVENT("gaming", "sl_gaming_seat_add_listener");
|
|
// TODO(kenalba): does gaming_seat need to persist in ctx?
|
|
struct zcr_gaming_seat_v2* gaming_seat =
|
|
zcr_gaming_input_v2_get_gaming_seat(ctx->gaming_input_manager->internal,
|
|
ctx->default_seat->proxy);
|
|
zcr_gaming_seat_v2_add_listener(gaming_seat,
|
|
&sl_internal_gaming_seat_listener, ctx);
|
|
}
|
|
}
|