Received: by 10.223.185.116 with SMTP id b49csp2995598wrg; Sun, 25 Feb 2018 11:08:43 -0800 (PST) X-Google-Smtp-Source: AH8x225PMIztnop8tnXIY/wFozKG7hZ80jB42YREqi10K+lSQZE5c0CQkY30yVSemM9ca3Fk2HcD X-Received: by 10.98.83.6 with SMTP id h6mr8269590pfb.174.1519585723553; Sun, 25 Feb 2018 11:08:43 -0800 (PST) ARC-Seal: i=1; a=rsa-sha256; t=1519585723; cv=none; d=google.com; s=arc-20160816; b=s34mLtKtzdaJiTU1LI2WQ+ftu/tAO1wUILJ6ncXGEVRpcZ6Ai+V9udUToFd0Lo/B4I YaZr/9Z4rkPsYEjXx1PMMjL4DBY5OMgFDa1+gRvXzes7ryLnJqmiyZUgcPoaa+JUnXZa OTic7OleDP0jGh+nISw7hwRHNYxblMuHvyWartVHa0eiZELuXPaB5V4TS1f6AbBI76S1 w7n9J0rNFkhpSlIaTBzAJ2TaJ3Phl9k5nQXY+3xujSFJS9UBsGdVyowcSrBr1D8u0hS6 jTNN8PC9qQtfkBIjaAuU8keu1peze1lzKYQxD/MVYQytaqMk0H6mvsGqIwFhJoCJDwk0 0V5w== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; h=list-id:precedence:sender:references:in-reply-to:message-id:date :subject:cc:to:from:dkim-signature:arc-authentication-results; bh=QsduGeo/Z1yCLYoSLniKodhOxi3KKSV+bHiNZy4YevE=; b=eluLspROaEqc+ufGDvrJPzGEgADFvufVZVkNgzRFNQos93CO+bWFJqJesT7XC1yVt/ Xa6s/35DAh52ZvRhAo4ZqEw8zd8VijfE4Yamt+8+iXreWjv/x3bSvKImEZtK8380wfRA 0TdCkACNOnnqjXyKwkgjiX5KPnmVg6eK/N/JMfYG6VauF906a4ozxbkoLVBghVfacg5y Fvy7qjCLOXTr+rctbGxxsEv0RxRMw7zACEeBV0X032/xKeF5tsj+EUHPa+vhsYNfEJDV RxTppUEbnjYMJUSKivM5l2BW610GL63ANf1T6dZ8awjVoGWjA0Hoqk3n0IodgYIlUerX 3vUw== ARC-Authentication-Results: i=1; mx.google.com; dkim=pass header.i=@gmail.com header.s=20161025 header.b=r5ICngzf; spf=pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) smtp.mailfrom=linux-kernel-owner@vger.kernel.org; dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com Return-Path: Received: from vger.kernel.org (vger.kernel.org. [209.132.180.67]) by mx.google.com with ESMTP id 38-v6si5490534pln.397.2018.02.25.11.08.28; Sun, 25 Feb 2018 11:08:43 -0800 (PST) Received-SPF: pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) client-ip=209.132.180.67; Authentication-Results: mx.google.com; dkim=pass header.i=@gmail.com header.s=20161025 header.b=r5ICngzf; spf=pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) smtp.mailfrom=linux-kernel-owner@vger.kernel.org; dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1751899AbeBYSxP (ORCPT + 99 others); Sun, 25 Feb 2018 13:53:15 -0500 Received: from mail-wm0-f65.google.com ([74.125.82.65]:38511 "EHLO mail-wm0-f65.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1751872AbeBYSxI (ORCPT ); Sun, 25 Feb 2018 13:53:08 -0500 Received: by mail-wm0-f65.google.com with SMTP id z9so13216188wmb.3; Sun, 25 Feb 2018 10:53:07 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:cc:subject:date:message-id:in-reply-to:references; bh=QsduGeo/Z1yCLYoSLniKodhOxi3KKSV+bHiNZy4YevE=; b=r5ICngzf6UTc35gVpknkFHY6wk90mhhbJCNrSCEXdxwU+YZ3KPJqmhrOxDBcHY6fVi 2hz7msXd2q7kpYy6eLtBLHACWbmBq5Lop6aVgK/xrB1HzdaOHbiLo2yUIHt6yCTXKHjB 94CGQXmO2KACqJDWSMIidr+00Kn06PrPVLntQQvB8OJX5cPm3vMr10CFruwNLR8DO/Le SXbtroQyxkAFP2hNvfBvktETR25yidtZKuYwp+xnprIFTQxyuHUy6L5RB452BeZciOuZ DeUYtPa5o0is2NyBLQO0fzjz9XAcDvcVS32Abqoesw+Q6acz/jhE3pr+k1ZkiKQPu81e n69Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references; bh=QsduGeo/Z1yCLYoSLniKodhOxi3KKSV+bHiNZy4YevE=; b=ijmcH5buaZGY65oK9yhCJsx8fapK3NT6yZBssRkdVovzDbaznVbKQDNnGJFwB8e35y KVKBombDhsHxELtQGUG30MH/6r+AOwP8OxOllHU3ALPQhYXpv1zci5n4iEt8Jkn8XeW4 BIa3y+5eWo5QAkKC1kBomKDrQKalVLqoNKf112NFfX3qc57oV2qcANxTXyUpq/iPVk/5 1V8fudyASevYqTBQiQyoZx2IjAJSp6SumYBSmdkYcf5OsDz/M5JMLfGvLAdLQo5lv4Dt DThOfjnIRihzSIT5HwOOGbMitkFPfCtOjSBpMEOGN9/2f/OnPiwAKyvnPpewH1HVkXYm dhZA== X-Gm-Message-State: APf1xPBA0kBfD6hI9uhM8F8Zj0R3fPcRIZbMYntFKPFKTgsf+XVp1MeJ dKoCU+L3SQ9JwMJngvdv838= X-Received: by 10.28.128.137 with SMTP id b131mr6946086wmd.121.1519584786475; Sun, 25 Feb 2018 10:53:06 -0800 (PST) Received: from localhost.localdomain ([2a01:c50e:5126:7a00:50d8:4f2:c65c:e8e9]) by smtp.gmail.com with ESMTPSA id 72sm6332850wmh.44.2018.02.25.10.53.05 (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Sun, 25 Feb 2018 10:53:05 -0800 (PST) From: Rodrigo Rivas Costa To: Jiri Kosina , Benjamin Tissoires , "Pierre-Loup A. Griffais" , Cameron Gutman , =?UTF-8?q?Cl=C3=A9ment=20VUCHENER?= , linux-kernel@vger.kernel.org, linux-input@vger.kernel.org Cc: Rodrigo Rivas Costa Subject: [PATCH v3 1/3] HID: add driver for Valve Steam Controller Date: Sun, 25 Feb 2018 19:52:48 +0100 Message-Id: <20180225185250.10759-2-rodrigorivascosta@gmail.com> X-Mailer: git-send-email 2.16.2 In-Reply-To: <20180225185250.10759-1-rodrigorivascosta@gmail.com> References: <20180225185250.10759-1-rodrigorivascosta@gmail.com> Sender: linux-kernel-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org There are two ways to connect the Steam Controller: directly to the USB or with the USB wireless adapter. Both methods are similar, but the wireless adapter can connect up to 4 devices at the same time. The wired device will appear as 3 interfaces: a virtual mouse, a virtual keyboard and a custom HID device. The wireless device will appear as 5 interfaces: a virtual keyboard and 4 custom HID devices, that will remain silent until a device is actually connected. The custom HID device has a report descriptor with all vendor specific usages, so the hid-generic is not very useful. In a PC/SteamBox Valve Steam Client provices a software translation by using direct USB access and a creates a uinput virtual gamepad. This driver was reverse engineered to provide direct kernel support in case you cannot, or do not want to, use Valve Steam Client. It disables the virtual keyboard and mouse, as they are not so useful when you have a working gamepad. Working: buttons, axes, pads, wireless connect/disconnect. TO-DO: Battery, force-feedback, accelerometer/gyro, led, beeper... Signed-off-by: Rodrigo Rivas Costa --- drivers/hid/Kconfig | 8 + drivers/hid/Makefile | 1 + drivers/hid/hid-ids.h | 4 + drivers/hid/hid-steam.c | 539 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 552 insertions(+) create mode 100644 drivers/hid/hid-steam.c diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index 19c499f5623d..6e80fbf04e03 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -823,6 +823,14 @@ config HID_SPEEDLINK ---help--- Support for Speedlink Vicious and Divine Cezanne mouse. +config HID_STEAM + tristate "Steam Controller support" + depends on HID + ---help--- + Say Y here if you have a Steam Controller if you want to use it + without running the Steam Client. It supports both the wired and + the wireless adaptor. + config HID_STEELSERIES tristate "Steelseries SRW-S1 steering wheel support" depends on HID diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index eb13b9e92d85..60a8abf84682 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -95,6 +95,7 @@ obj-$(CONFIG_HID_SAMSUNG) += hid-samsung.o obj-$(CONFIG_HID_SMARTJOYPLUS) += hid-sjoy.o obj-$(CONFIG_HID_SONY) += hid-sony.o obj-$(CONFIG_HID_SPEEDLINK) += hid-speedlink.o +obj-$(CONFIG_HID_STEAM) += hid-steam.o obj-$(CONFIG_HID_STEELSERIES) += hid-steelseries.o obj-$(CONFIG_HID_SUNPLUS) += hid-sunplus.o obj-$(CONFIG_HID_GREENASIA) += hid-gaff.o diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 43ddcdfbd0da..be31a3c20818 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -988,6 +988,10 @@ #define USB_VENDOR_ID_STANTUM_SITRONIX 0x1403 #define USB_DEVICE_ID_MTP_SITRONIX 0x5001 +#define USB_VENDOR_ID_VALVE 0x28de +#define USB_DEVICE_ID_STEAM_CONTROLLER 0x1102 +#define USB_DEVICE_ID_STEAM_CONTROLLER_WIRELESS 0x1142 + #define USB_VENDOR_ID_STEELSERIES 0x1038 #define USB_DEVICE_ID_STEELSERIES_SRWS1 0x1410 diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c new file mode 100644 index 000000000000..59dbcb95f349 --- /dev/null +++ b/drivers/hid/hid-steam.c @@ -0,0 +1,539 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * HID driver for Valve Steam Controller + * + * Copyright (c) 2018 Rodrigo Rivas Costa + * + * Supports both the wired and wireless interfaces. + * + * For additional functions, such as disabling the virtual mouse/keyboard or + * changing the right-pad margin you can use the user-space tool at: + * + * https://github.com/rodrigorc/steamctrl + */ + +#include +#include +#include +#include +#include +#include "hid-ids.h" + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Rodrigo Rivas Costa "); + +#define STEAM_QUIRK_WIRELESS BIT(0) + +/* Touch pads are 40 mm in diameter and 65535 units */ +#define STEAM_PAD_RESOLUTION 1638 +/* Trigger runs are about 5 mm and 256 units */ +#define STEAM_TRIGGER_RESOLUTION 51 +/* Joystick runs are about 5 mm and 256 units */ +#define STEAM_JOYSTICK_RESOLUTION 51 + +#define STEAM_PAD_FUZZ 128 + +struct steam_device { + spinlock_t lock; + struct hid_device *hdev; + struct input_dev __rcu *input; + unsigned long quirks; + struct work_struct work_connect; + bool connected; +}; + +static int steam_input_open(struct input_dev *dev) +{ + struct steam_device *steam = input_get_drvdata(dev); + + return hid_hw_open(steam->hdev); +} + +static void steam_input_close(struct input_dev *dev) +{ + struct steam_device *steam = input_get_drvdata(dev); + + hid_hw_close(steam->hdev); +} + +static int steam_register(struct steam_device *steam) +{ + struct hid_device *hdev = steam->hdev; + struct input_dev *input; + int ret; + + rcu_read_lock(); + input = rcu_dereference(steam->input); + rcu_read_unlock(); + if (input) { + dbg_hid("%s: already connected\n", __func__); + return -ENODEV; + } + + hid_info(hdev, "Steam Controller connected"); + + input = input_allocate_device(); + if (!input) + return -ENOMEM; + + input_set_drvdata(input, steam); + input->dev.parent = &hdev->dev; + input->open = steam_input_open; + input->close = steam_input_close; + + input->name = (steam->quirks & STEAM_QUIRK_WIRELESS) ? + "Wireless Steam Controller" : + "Steam Controller"; + input->phys = hdev->phys; + input->uniq = hdev->uniq; + input->id.bustype = hdev->bus; + input->id.vendor = hdev->vendor; + input->id.product = hdev->product; + input->id.version = hdev->version; + + input_set_capability(input, EV_KEY, BTN_TR2); + input_set_capability(input, EV_KEY, BTN_TL2); + input_set_capability(input, EV_KEY, BTN_TR); + input_set_capability(input, EV_KEY, BTN_TL); + input_set_capability(input, EV_KEY, BTN_Y); + input_set_capability(input, EV_KEY, BTN_B); + input_set_capability(input, EV_KEY, BTN_X); + input_set_capability(input, EV_KEY, BTN_A); + input_set_capability(input, EV_KEY, BTN_SELECT); + input_set_capability(input, EV_KEY, BTN_MODE); + input_set_capability(input, EV_KEY, BTN_START); + input_set_capability(input, EV_KEY, BTN_GEAR_DOWN); + input_set_capability(input, EV_KEY, BTN_GEAR_UP); + input_set_capability(input, EV_KEY, BTN_THUMBR); + input_set_capability(input, EV_KEY, BTN_THUMBL); + + input_set_abs_params(input, ABS_Z, 0, 255, 0, 0); + input_set_abs_params(input, ABS_RZ, 0, 255, 0, 0); + input_set_abs_params(input, ABS_X, -32767, 32767, 0, 0); + input_set_abs_params(input, ABS_Y, -32767, 32767, 0, 0); + input_set_abs_params(input, ABS_RX, -32767, 32767, + STEAM_PAD_FUZZ, 0); + input_set_abs_params(input, ABS_RY, -32767, 32767, + STEAM_PAD_FUZZ, 0); + input_set_abs_params(input, ABS_HAT1X, -32767, 32767, + STEAM_PAD_FUZZ, 0); + input_set_abs_params(input, ABS_HAT1Y, -32767, 32767, + STEAM_PAD_FUZZ, 0); + input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0); + input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0); + input_abs_set_res(input, ABS_X, STEAM_JOYSTICK_RESOLUTION); + input_abs_set_res(input, ABS_Y, STEAM_JOYSTICK_RESOLUTION); + input_abs_set_res(input, ABS_RX, STEAM_PAD_RESOLUTION); + input_abs_set_res(input, ABS_RY, STEAM_PAD_RESOLUTION); + input_abs_set_res(input, ABS_HAT1X, STEAM_PAD_RESOLUTION); + input_abs_set_res(input, ABS_HAT1Y, STEAM_PAD_RESOLUTION); + input_abs_set_res(input, ABS_Z, STEAM_TRIGGER_RESOLUTION); + input_abs_set_res(input, ABS_RZ, STEAM_TRIGGER_RESOLUTION); + + ret = input_register_device(input); + if (ret) + goto input_register_fail; + + rcu_assign_pointer(steam->input, input); + + return 0; + +input_register_fail: + input_free_device(input); + return ret; +} + +static void steam_unregister(struct steam_device *steam) +{ + struct input_dev *input; + + rcu_read_lock(); + input = rcu_dereference(steam->input); + rcu_read_unlock(); + + if (input) { + RCU_INIT_POINTER(steam->input, NULL); + synchronize_rcu(); + hid_info(steam->hdev, "Steam Controller disconnected"); + input_unregister_device(input); + } +} + +static void steam_work_connect_cb(struct work_struct *work) +{ + struct steam_device *steam = container_of(work, struct steam_device, + work_connect); + unsigned long flags; + bool connected; + int ret; + + spin_lock_irqsave(&steam->lock, flags); + connected = steam->connected; + spin_unlock_irqrestore(&steam->lock, flags); + + if (connected) { + ret = steam_register(steam); + if (ret) { + hid_err(steam->hdev, + "%s:steam_register failed with error %d\n", + __func__, ret); + return; + } + } else { + steam_unregister(steam); + } +} + +static bool steam_is_valve_interface(struct hid_device *hdev) +{ + struct hid_report_enum *rep_enum; + struct hid_report *hreport; + + /* + * The wired device creates 3 interfaces: + * 0: emulated mouse. + * 1: emulated keyboard. + * 2: the real game pad. + * The wireless device creates 5 interfaces: + * 0: emulated keyboard. + * 1-4: slots where up to 4 real game pads will be connected to. + * We know which one is the real gamepad interface because they are the + * only ones with a feature report. + */ + rep_enum = &hdev->report_enum[HID_FEATURE_REPORT]; + list_for_each_entry(hreport, &rep_enum->report_list, list) { + /* should we check hreport->id == 0? */ + return true; + } + return false; +} + +static int steam_probe(struct hid_device *hdev, + const struct hid_device_id *id) +{ + struct steam_device *steam; + int ret; + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, + "%s:parse of hid interface failed\n", __func__); + return ret; + } + + /* + * Connect the HID device so that Steam Controller keeps on working + * without changes. + */ + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) { + hid_err(hdev, + "%s:hid_hw_start failed with error %d\n", + __func__, ret); + return ret; + } + + if (!steam_is_valve_interface(hdev)) + return 0; + + /* + * From this point on, if anything fails return 0 and ignores + * the error, so that the default HID devices are still bound. + */ + steam = devm_kzalloc(&hdev->dev, + sizeof(struct steam_device), GFP_KERNEL); + if (!steam) { + ret = -ENOMEM; + goto mem_fail; + } + + spin_lock_init(&steam->lock); + steam->hdev = hdev; + hid_set_drvdata(hdev, steam); + steam->quirks = id->driver_data; + INIT_WORK(&steam->work_connect, steam_work_connect_cb); + + if (steam->quirks & STEAM_QUIRK_WIRELESS) { + ret = hid_hw_open(hdev); + if (ret) { + hid_err(hdev, + "%s:hid_hw_open for wireless\n", + __func__); + goto hid_hw_open_fail; + } + hid_info(hdev, "Steam wireless receiver connected"); + } else { + ret = steam_register(steam); + if (ret) { + hid_err(hdev, + "%s:steam_register failed with error %d\n", + __func__, ret); + goto input_register_fail; + } + } + + return 0; + +input_register_fail: +hid_hw_open_fail: + cancel_work_sync(&steam->work_connect); + hid_set_drvdata(hdev, NULL); +mem_fail: + hid_err(hdev, "%s: failed with error %d\n", + __func__, ret); + return 0; +} + +static void steam_remove(struct hid_device *hdev) +{ + struct steam_device *steam = hid_get_drvdata(hdev); + + if (steam && (steam->quirks & STEAM_QUIRK_WIRELESS)) { + hid_info(hdev, "Steam wireless receiver disconnected"); + hid_hw_close(hdev); + } + + hid_hw_stop(hdev); + + if (steam) { + cancel_work_sync(&steam->work_connect); + steam_unregister(steam); + hid_set_drvdata(hdev, NULL); + } +} + +static void steam_do_connect_event(struct steam_device *steam, bool connected) +{ + unsigned long flags; + + spin_lock_irqsave(&steam->lock, flags); + steam->connected = connected; + spin_unlock_irqrestore(&steam->lock, flags); + + if (schedule_work(&steam->work_connect) == 0) + dbg_hid("%s: connected=%d event already queued\n", + __func__, connected); +} + +/* + * The size for this message payload is 60. + * The known values are: + * (* values are not sent through wireless) + * (* accelerator/gyro is disabled by default) + * Offset| Type | Mapped to |Meaning + * -------+-------+-----------+-------------------------- + * 4-7 | u32 | -- | sequence number + * 8-10 | 24bit | see below | buttons + * 11 | u8 | ABS_Z | left trigger + * 12 | u8 | ABS_RZ | right trigger + * 13-15 | -- | -- | always 0 + * 16-17 | s16 | ABS_X | X value + * 18-19 | s16 | ABS_Y | Y value + * 20-21 | s16 | ABS_RX | right-pad X value + * 22-23 | s16 | ABS_RY | right-pad Y value + * 24-25 | s16 | -- | * left trigger + * 26-27 | s16 | -- | * right trigger + * 28-29 | s16 | -- | * accelerometer X value + * 30-31 | s16 | -- | * accelerometer Y value + * 32-33 | s16 | -- | * accelerometer Z value + * 34-35 | s16 | -- | gyro X value + * 36-36 | s16 | -- | gyro Y value + * 38-39 | s16 | -- | gyro Z value + * 40-41 | s16 | -- | quaternion W value + * 42-43 | s16 | -- | quaternion X value + * 44-45 | s16 | -- | quaternion Y value + * 46-47 | s16 | -- | quaternion Z value + * 48-49 | -- | -- | always 0 + * 50-51 | s16 | -- | * left trigger (uncalibrated) + * 52-53 | s16 | -- | * right trigger (uncalibrated) + * 54-55 | s16 | -- | * joystick X value (uncalibrated) + * 56-57 | s16 | -- | * joystick Y value (uncalibrated) + * 58-59 | s16 | -- | * left-pad X value + * 60-61 | s16 | -- | * left-pad Y value + * 62-63 | u16 | -- | * battery voltage + * + * The buttons are: + * Bit | Mapped to | Description + * ------+------------+-------------------------------- + * 8.0 | BTN_TR2 | right trigger fully pressed + * 8.1 | BTN_TL2 | left trigger fully pressed + * 8.2 | BTN_TR | right shoulder + * 8.3 | BTN_TL | left shoulder + * 8.4 | BTN_Y | button Y + * 8.5 | BTN_B | button B + * 8.6 | BTN_X | button X + * 8.7 | BTN_A | button A + * 9.0 | -ABS_HAT0Y | lef-pad up + * 9.1 | +ABS_HAT0X | lef-pad right + * 9.2 | -ABS_HAT0X | lef-pad left + * 9.3 | +ABS_HAT0Y | lef-pad down + * 9.4 | BTN_SELECT | menu left + * 9.5 | BTN_MODE | steam logo + * 9.6 | BTN_START | menu right + * 9.7 | BTN_GEAR_DOWN | left back lever + * 10.0 | BTN_GEAR_UP | right back lever + * 10.1 | -- | left-pad clicked + * 10.2 | BTN_THUMBR | right-pad clicked + * 10.3 | -- | left-pad touched (but see explanation below) + * 10.4 | -- | right-pad touched + * 10.5 | -- | unknown + * 10.6 | BTN_THUMBL | joystick clicked + * 10.7 | -- | lpad_and_joy + */ + +static void steam_do_input_event(struct steam_device *steam, + struct input_dev *input, u8 *data) +{ + /* 24 bits of buttons */ + u8 b8, b9, b10; + bool lpad_touched, lpad_and_joy; + + b8 = data[8]; + b9 = data[9]; + b10 = data[10]; + + input_report_abs(input, ABS_Z, data[11]); + input_report_abs(input, ABS_RZ, data[12]); + + /* + * These two bits tells how to interpret the values X and Y. + * lpad_and_joy tells that the joystick and the lpad are used at the + * same time. + * lpad_touched tells whether X/Y are to be read as lpad coord or + * joystick values. + * (lpad_touched || lpad_and_joy) tells if the lpad is really touched. + */ + lpad_touched = b10 & 0x08; + lpad_and_joy = b10 & 0x80; + input_report_abs(input, lpad_touched ? ABS_HAT1X : ABS_X, + (s16) le16_to_cpup((__le16 *)(data + 16))); + input_report_abs(input, lpad_touched ? ABS_HAT1Y : ABS_Y, + -(s16) le16_to_cpup((__le16 *)(data + 18))); + /* Check if joystick is centered */ + if (lpad_touched && !lpad_and_joy) { + input_report_abs(input, ABS_X, 0); + input_report_abs(input, ABS_Y, 0); + } + /* Check if lpad is untouched */ + if (!(lpad_touched || lpad_and_joy)) { + input_report_abs(input, ABS_HAT1X, 0); + input_report_abs(input, ABS_HAT1Y, 0); + } + + input_report_abs(input, ABS_RX, + (s16) le16_to_cpup((__le16 *)(data + 20))); + input_report_abs(input, ABS_RY, + -(s16) le16_to_cpup((__le16 *)(data + 22))); + + input_event(input, EV_KEY, BTN_TR2, !!(b8 & 0x01)); + input_event(input, EV_KEY, BTN_TL2, !!(b8 & 0x02)); + input_event(input, EV_KEY, BTN_TR, !!(b8 & 0x04)); + input_event(input, EV_KEY, BTN_TL, !!(b8 & 0x08)); + input_event(input, EV_KEY, BTN_Y, !!(b8 & 0x10)); + input_event(input, EV_KEY, BTN_B, !!(b8 & 0x20)); + input_event(input, EV_KEY, BTN_X, !!(b8 & 0x40)); + input_event(input, EV_KEY, BTN_A, !!(b8 & 0x80)); + input_event(input, EV_KEY, BTN_SELECT, !!(b9 & 0x10)); + input_event(input, EV_KEY, BTN_MODE, !!(b9 & 0x20)); + input_event(input, EV_KEY, BTN_START, !!(b9 & 0x40)); + input_event(input, EV_KEY, BTN_GEAR_DOWN, !!(b9 & 0x80)); + input_event(input, EV_KEY, BTN_GEAR_UP, !!(b10 & 0x01)); + input_event(input, EV_KEY, BTN_THUMBR, !!(b10 & 0x04)); + input_event(input, EV_KEY, BTN_THUMBL, !!(b10 & 0x40)); + + input_report_abs(input, ABS_HAT0X, + !!(b9 & 0x02) - !!(b9 & 0x04)); + input_report_abs(input, ABS_HAT0Y, + !!(b9 & 0x08) - !!(b9 & 0x01)); + + input_sync(input); +} + +static int steam_raw_event(struct hid_device *hdev, + struct hid_report *report, u8 *data, + int size) +{ + struct steam_device *steam = hid_get_drvdata(hdev); + struct input_dev *input; + + if (!steam) + return 0; + + /* + * All messages are size=64, all values little-endian. + * The format is: + * Offset| Meaning + * -------+-------------------------------------------- + * 0-1 | always 0x01, 0x00, maybe protocol version? + * 2 | type of message + * 3 | length of the real payload (not checked) + * 4-n | payload data, depends on the type + * + * There are these known types of message: + * 0x01: input data (60 bytes) + * 0x03: wireless connect/disconnect (1 byte) + * 0x04: battery status (11 bytes) + */ + + if (size != 64 || data[0] != 1 || data[1] != 0) + return 0; + + switch (data[2]) { + case 0x01: + rcu_read_lock(); + input = rcu_dereference(steam->input); + if (likely(input)) { + steam_do_input_event(steam, input, data); + } else { + dbg_hid("%s: input data without connect event\n", + __func__); + steam_do_connect_event(steam, true); + } + rcu_read_unlock(); + break; + case 0x03: + /* + * The payload of this event is a single byte: + * 0x01: disconnected. + * 0x02: connected. + */ + switch (data[4]) { + case 0x01: + steam_do_connect_event(steam, false); + break; + case 0x02: + steam_do_connect_event(steam, true); + break; + } + break; + case 0x04: + /* TODO battery status */ + break; + } + return 0; +} + +static const struct hid_device_id steam_controllers[] = { + { /* Wired Steam Controller */ + HID_USB_DEVICE(USB_VENDOR_ID_VALVE, + USB_DEVICE_ID_STEAM_CONTROLLER) + }, + { /* Wireless Steam Controller */ + HID_USB_DEVICE(USB_VENDOR_ID_VALVE, + USB_DEVICE_ID_STEAM_CONTROLLER_WIRELESS), + .driver_data = STEAM_QUIRK_WIRELESS + }, + {} +}; + +MODULE_DEVICE_TABLE(hid, steam_controllers); + +static struct hid_driver steam_controller_driver = { + .name = "hid-steam", + .id_table = steam_controllers, + .probe = steam_probe, + .remove = steam_remove, + .raw_event = steam_raw_event, +}; + +module_hid_driver(steam_controller_driver); -- 2.16.2