From: Denis Benato <denis.benato@linux.dev>
To: linux-kernel@vger.kernel.org
Cc: platform-driver-x86@vger.kernel.org, linux-input@vger.kernel.org,
kernel@lists.opengamingcollective.org,
"Benjamin Tissoires" <bentiss@kernel.org>,
"Jiri Kosina" <jikos@kernel.org>,
"Luke D . Jones" <luke@ljones.dev>,
"Mateusz Schwartz" <matthew.schwartz@linux.dev>,
"Denis Benato" <benato.denis96@gmail.com>,
"Jonathan LoBue" <jlobue10@gmail.com>,
"Khamunetri Clark" <khamunetriclark@gmail.com>,
"Derek J. Clark" <derekjohn.clark@gmail.com>,
"Joshua Leivenzon" <hacker1024@users.sourceforge.net>,
"Nathan Chancellor" <nathan@kernel.org>,
"Rebecca Mara Müller" <rebecca.mara@posteo.de>,
"Ivan Levchenko" <me@livan.pro>,
"Corentin Chary" <corentin.chary@gmail.com>,
"Hans de Goede" <hansg@kernel.org>,
"Ilpo Järvinen" <ilpo.jarvinen@linux.intel.com>,
"Denis Benato" <denis.benato@linux.dev>
Subject: [PATCH v12 02/17] HID: asus: add support for ROG Ally handhelds
Date: Sat, 31 Oct 2026 17:41:15 +0000 [thread overview]
Message-ID: <20261031174130.208240-3-denis.benato@linux.dev> (raw)
In-Reply-To: <20261031174130.208240-1-denis.benato@linux.dev>
Implement the core functionality for ASUS handhelds:
- extend functionality of hid-asus if driver is enabled
- initialise the device to a "ready" state
- implement Ally X input mapping
- emit a single proper event for AC button long press
Each interface keeps its own state and reports on its own evdev.
Signed-off-by: Khamunetri Clark <khamunetriclark@gmail.com>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Matthew Schwartz <matthew.schwartz@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Jonathan LoBue <jlobue10@gmail.com>
---
drivers/hid/hid-asus.c | 969 ++++++++++++++++++++++++++++++++++++-----
1 file changed, 870 insertions(+), 99 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 327bb4ab1a0d..552d3b25a1d1 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -22,16 +22,24 @@
#include <linux/acpi.h>
#include <linux/cleanup.h>
+#include <linux/compiler_attributes.h>
#include <linux/container_of.h>
#include <linux/dmi.h>
+#include <linux/err.h>
#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/list.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/platform_data/x86/asus-wmi.h>
+#include <linux/types.h>
#include <linux/input/mt.h>
#include <linux/usb.h> /* For to_usb_interface for T100 touchpad intf check */
#include <linux/power_supply.h>
+#include <linux/stddef.h>
#include <linux/leds.h>
#include <linux/types.h>
+#include <linux/unaligned.h>
#include "hid-ids.h"
@@ -39,6 +47,9 @@ MODULE_AUTHOR("Yusuke Fujimaki <usk.fujimaki@gmail.com>");
MODULE_AUTHOR("Brendan McGrath <redmcg@redmandi.dyndns.org>");
MODULE_AUTHOR("Victor Vlasenko <victor.vlasenko@sysgears.com>");
MODULE_AUTHOR("Frederik Wenigwieser <frederik.wenigwieser@gmail.com>");
+MODULE_AUTHOR("Denis Benato <denis.benato@linux.dev>");
+MODULE_AUTHOR("Luke Jones <luke@ljones.dev>");
+MODULE_AUTHOR("Khamunetri Clark <khamunetriclark@gmail.com>");
MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define T100_TPAD_INTF 2
@@ -56,6 +67,30 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ROG_ALLY_X_MIN_MCU 313
#define ROG_ALLY_MIN_MCU 319
+#define HID_ALLY_INTF_KEYBOARD_IN 0x81
+#define HID_ALLY_INTF_CFG_IN 0x83
+#define HID_ALLY_X_INTF_IN 0x87
+
+#define HID_ALLY_GET_REPORT_ID 0x0D
+#define HID_ALLY_FEATURE_CODE_PAGE 0xD1
+
+#define HID_ALLY_X_INPUT_REPORT 0x0B
+
+#define HID_ALLY_READY_MAX_TRIES 6
+
+#define HID_INPUT_KEY_DELETE 0x4c
+#define HID_INPUT_KEY_F20 0x6f
+#define HID_INPUT_LCTRL_MODIFIER 0x01
+#define HID_INPUT_LALT_MODIFIER 0x04
+#define HID_INPUT_LCTRL_LALT (HID_INPUT_LCTRL_MODIFIER | HID_INPUT_LALT_MODIFIER)
+
+#define HID_INPUT_REPORT_ID_OFFSET 0
+#define HID_INPUT_MODIFIER_OFFSET 1
+#define HID_INPUT_RESERVED_OFFSET 2
+#define HID_INPUT_KEY_OFFSET 3
+
+#define ASUS_HID_INPUT_KBD_REPORT_ID 0x01
+
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
#define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -160,6 +195,17 @@ struct asus_touchpad_info {
int report_size;
};
+struct ally_intf {
+ struct hid_device *hdev;
+ /* HID-core evdev of this interface. Reports for it land here. */
+ struct input_dev *input;
+ /* Private gamepad node for report 0x0B. NULL on the other interfaces. */
+ struct input_dev *gamepad;
+ u8 cad_state;
+ unsigned long cad_last_event_time;
+ struct delayed_work resume_work;
+};
+
struct asus_drvdata {
unsigned long quirks;
struct hid_device *hdev;
@@ -167,6 +213,7 @@ struct asus_drvdata {
struct input_dev *tp_kbd_input;
struct asus_worker *worker;
unsigned int kbd_backlight_brightness;
+ struct ally_intf *ally;
const struct asus_touchpad_info *tp;
struct power_supply *battery;
struct power_supply_desc battery_desc;
@@ -239,12 +286,761 @@ static const struct asus_touchpad_info medion_e1239t_tp = {
.report_size = 32 /* 2 byte header + 5 * 5 + 5 byte footer */,
};
+enum ally_command_codes {
+ CMD_SET_GAMEPAD_MODE = 0x01,
+ CMD_SET_MAPPING = 0x02,
+ CMD_SET_JOYSTICK_MAPPING = 0x03,
+ CMD_SET_JOYSTICK_DEADZONE = 0x04,
+ CMD_SET_TRIGGER_RANGE = 0x05,
+ CMD_SET_VIBRATION_INTENSITY = 0x06,
+ CMD_LED_CONTROL = 0x08,
+ CMD_CHECK_READY = 0x0A,
+ CMD_SET_XBOX_CONTROLLER = 0x0B,
+ CMD_CHECK_XBOX_SUPPORT = 0x0C,
+ CMD_USER_CAL_DATA = 0x0D,
+ CMD_CHECK_USER_CAL_SUPPORT = 0x0E,
+ CMD_SET_TURBO_PARAMS = 0x0F,
+ CMD_CHECK_TURBO_SUPPORT = 0x10,
+ CMD_CHECK_RESP_CURVE_SUPPORT = 0x12,
+ CMD_SET_RESP_CURVE = 0x13,
+ CMD_CHECK_DIR_TO_BTN_SUPPORT = 0x14,
+ CMD_SET_GYRO_PARAMS = 0x15,
+ CMD_CHECK_GYRO_TO_JOYSTICK = 0x16,
+ CMD_CHECK_ANTI_DEADZONE = 0x17,
+ CMD_SET_ANTI_DEADZONE = 0x18,
+};
+
+static const u8 ally_force_feedback_off[] = {
+ 0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB
+};
+
+/*
+ * Each Ally interface owns its own ally_intf. A report is delivered on the
+ * evdev of the interface that received it. Nothing here points at another
+ * interface.
+ */
+static void ally_release_vendor_keys(struct input_dev *input)
+{
+ if (!input)
+ return;
+
+ input_report_key(input, KEY_F16, 0);
+ input_report_key(input, KEY_F17, 0);
+ input_report_key(input, KEY_F18, 0);
+ input_report_key(input, KEY_F19, 0);
+ input_report_key(input, KEY_PROG1, 0);
+ input_sync(input);
+}
+
+static void ally_resume_work_fn(struct work_struct *work)
+{
+ struct ally_intf *intf = container_of(work, struct ally_intf,
+ resume_work.work);
+
+ /*
+ * Force release vendor buttons to prevent stuck keys on resume
+ * (workaround for Ally X USB re-probing during suspend/resume).
+ */
+ ally_release_vendor_keys(intf->input);
+}
+
+static void ally_quiesce(struct ally_intf *intf)
+{
+ if (!intf)
+ return;
+
+ cancel_delayed_work_sync(&intf->resume_work);
+}
+
+static void ally_free(struct ally_intf *intf)
+{
+ if (!intf)
+ return;
+
+ if (intf->gamepad)
+ input_unregister_device(intf->gamepad);
+ kfree(intf);
+}
+
+/**
+ * handle_ctrl_alt_del() - detect a left button long press
+ * @intf: ally interface holding the sequence state
+ * @data: raw report buffer, rewritten in place when the sequence matches
+ * @size: length of @data in bytes
+ *
+ * The Ally left button emits a sequence of ctrl+alt+del events. Capture that
+ * and emit only a single code for that single event by rewriting @data in
+ * place. The return value tells the caller whether ally processing of this
+ * report should stop.
+ *
+ * Return: true when this step was recognized and @data was rewritten, so the
+ * caller stops and the generic parser consumes the rewritten report. false
+ * when @data was left unchanged and ally processing should continue.
+ */
+static bool handle_ctrl_alt_del(struct ally_intf *intf, u8 *data, int size)
+{
+ bool time_is_past = time_after(jiffies,
+ intf->cad_last_event_time + msecs_to_jiffies(100));
+
+ if (size < 16 || data[HID_INPUT_REPORT_ID_OFFSET] != ASUS_HID_INPUT_KBD_REPORT_ID)
+ return false;
+
+ if (intf->cad_state > 0 && time_is_past)
+ intf->cad_state = 0;
+
+ intf->cad_last_event_time = jiffies;
+ switch (intf->cad_state) {
+ case 0:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_MODIFIER) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == 0x00)) {
+ intf->cad_state = 1;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ case 1:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_LALT) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == 0x00)) {
+ intf->cad_state = 2;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ case 2:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_LALT) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) {
+ intf->cad_state = 3;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ data[HID_INPUT_KEY_OFFSET] = HID_INPUT_KEY_F20;
+ return true;
+ }
+ break;
+ case 3:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LALT_MODIFIER) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) {
+ intf->cad_state = 4;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ data[HID_INPUT_KEY_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ case 4:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == 0x00) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) {
+ intf->cad_state = 5;
+ data[HID_INPUT_KEY_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ }
+ intf->cad_state = 0;
+ return false;
+}
+
+/**
+ * ally_vendor_keys() - Report a vendor button on this interface's evdev
+ * @input: HID-core input device of the interface that received the report
+ * @data: raw report buffer
+ * @size: length of @data in bytes
+ *
+ * The key is down while @data[1] matches that code. The same decode is used
+ * for every Ally interface: 0x38 is the ROG key (KEY_PROG1 and KEY_F19).
+ *
+ * Return: true if the report was a known vendor button.
+ */
+static bool ally_vendor_keys(struct input_dev *input, const u8 *data, int size)
+{
+ if (size < 2 || data[0] != FEATURE_KBD_REPORT_ID)
+ return false;
+
+ if (data[1] != 0x38 && data[1] != 0xA6 && data[1] != 0xA7 &&
+ data[1] != 0xA8)
+ return false;
+
+ if (!input)
+ return true;
+
+ input_report_key(input, KEY_PROG1, data[1] == 0x38);
+ input_report_key(input, KEY_F16, data[1] == 0xA6);
+ input_report_key(input, KEY_F17, data[1] == 0xA7);
+ input_report_key(input, KEY_F18, data[1] == 0xA8);
+ input_report_key(input, KEY_F19, data[1] == 0x38);
+ input_sync(input);
+ return true;
+}
+
+/**
+ * asus_find_report() - Find a report of the given type by its ID
+ * @hdev: HID device
+ * @type: report type (HID_INPUT_REPORT, HID_OUTPUT_REPORT or
+ * HID_FEATURE_REPORT)
+ * @report_id: report ID to look for
+ *
+ * Return: the report with the given ID, or NULL if the device does not
+ * declare such a report.
+ */
+static struct hid_report *asus_find_report(struct hid_device *hdev,
+ unsigned int type, u8 report_id)
+{
+ return hdev->report_enum[type].report_id_hash[report_id];
+}
+
+/**
+ * asus_report_len() - Length of a feature report for the device
+ * @hdev: HID device
+ * @report_id: report ID of the feature report
+ *
+ * The same ASUS report ID can have different sizes depending on the device:
+ * report 0x5E is a 64-byte report on laptops but a 300-byte report on XG
+ * Mobile stations. Always size transfers from the report descriptor of the
+ * device at hand instead of hardcoding lengths.
+ *
+ * Return: the length of the feature report in bytes, or 0 if the device
+ * does not declare the report.
+ */
+static size_t asus_report_len(struct hid_device *hdev, u8 report_id)
+{
+ struct hid_report *report;
+
+ report = asus_find_report(hdev, HID_FEATURE_REPORT, report_id);
+
+ return report ? hid_report_len(report) : 0;
+}
+
+static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size)
+{
+ size_t hw_len = asus_report_len(hdev, buf[0]);
+
+ /* The payload (including the report ID) must fit the device report. */
+ if (!hw_len || buf_size > hw_len)
+ return -EINVAL;
+
+ u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ memcpy(dmabuf, buf, buf_size);
+
+ /*
+ * The report ID should be set from the incoming buffer due to LED and
+ * key interfaces having different pages. Short payloads are zero-padded
+ * to the exact report size the hardware expects for this report ID.
+ */
+ return hid_hw_raw_request(hdev, buf[0], dmabuf, hw_len, HID_FEATURE_REPORT,
+ HID_REQ_SET_REPORT);
+}
+
+/**
+ * asus_kbd_get_report() - Read a report sized from the descriptor
+ * @hdev: HID device
+ * @report_id: report ID to read
+ * @buf: buffer to store the report in
+ * @buf_size: size of @buf
+ *
+ * The transfer is performed at the exact report size the device declares
+ * for @report_id. The device may answer with a short read, and only the
+ * bytes that fit in @buf are copied into it.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int asus_kbd_get_report(struct hid_device *hdev, u8 report_id,
+ u8 *buf, size_t buf_size)
+{
+ size_t hw_len = asus_report_len(hdev, report_id);
+ int ret;
+
+ if (!hw_len)
+ return -EINVAL;
+
+ u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ ret = hid_hw_raw_request(hdev, report_id, dmabuf, hw_len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+ if (ret < 0)
+ return ret;
+
+ /* The device may return less than requested (short read). */
+ memcpy(buf, dmabuf, min(buf_size, hw_len));
+
+ return 0;
+}
+
static const u8 asus_report_id_init[] = {
FEATURE_KBD_REPORT_ID,
FEATURE_KBD_LED_REPORT_ID1,
FEATURE_KBD_LED_REPORT_ID2
};
+static int ally_dev_set_report(struct hid_device *hdev, const u8 *buf, size_t len)
+{
+ return asus_kbd_set_report(hdev, buf, len);
+}
+
+static int ally_dev_get_report(struct hid_device *hdev, u8 *out, size_t len)
+{
+ return hid_hw_raw_request(hdev, HID_ALLY_GET_REPORT_ID, out, len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+}
+
+/**
+ * ally_gamepad_send_packet() - Send a raw packet to the gamepad device
+ * @hdev: HID device
+ * @buf: buffer containing the packet data
+ * @len: length of data to send
+ *
+ * Return: count of data transferred, negative if error.
+ */
+static int ally_gamepad_send_packet(struct hid_device *hdev, const u8 *buf, size_t len)
+{
+ return ally_dev_set_report(hdev, buf, len);
+}
+
+/**
+ * ally_gamepad_send_receive_packet() - Send a packet and receive the response
+ * @hdev: HID device
+ * @buf: buffer containing the packet data to send and receive response in
+ * @len: length of buffer
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_gamepad_send_receive_packet(struct hid_device *hdev,
+ u8 *buf, size_t len)
+{
+ int ret;
+
+ ret = ally_dev_set_report(hdev, buf, len);
+ if (ret >= 0) {
+ memset(buf, 0, len);
+ ret = ally_dev_get_report(hdev, buf, len);
+ }
+
+ return ret;
+}
+
+/**
+ * ally_alloc_cmd() - Construct a command buffer for the gamepad
+ * @hdev: HID device the command is built for
+ * @cmd: Command code to send
+ * @payload: Optional payload data to include in the command
+ * @payload_size: Size of the payload data
+ * @size: Where to store the length of the allocated buffer
+ *
+ * The buffer is sized after the feature report the device declares for
+ * the gamepad commands, and it is the caller responsibility to free the
+ * buffer using kfree().
+ *
+ * Return: the newly allocated buffer containing the command, or NULL on
+ * allocation failure or if the payload would not fit in the report.
+ */
+static u8 *ally_alloc_cmd(struct hid_device *hdev, u8 cmd, const u8 *payload,
+ u8 payload_size, size_t *size)
+{
+ size_t hw_len = asus_report_len(hdev, FEATURE_KBD_REPORT_ID);
+ u8 *hidbuf;
+
+ /*
+ * The payload is written after the 4-byte command header:
+ * reject one that would spill past the end of the report.
+ */
+ if (hw_len < 4 || payload_size > hw_len - 4)
+ return NULL;
+
+ hidbuf = kzalloc(hw_len, GFP_KERNEL);
+ if (!hidbuf)
+ return NULL;
+
+ hidbuf[0] = FEATURE_KBD_REPORT_ID;
+ hidbuf[1] = HID_ALLY_FEATURE_CODE_PAGE;
+ hidbuf[2] = cmd;
+ hidbuf[3] = payload_size;
+
+ if (payload_size > 0 && payload)
+ memcpy(&hidbuf[4], payload, payload_size);
+
+ *size = hw_len;
+
+ return hidbuf;
+}
+
+/**
+ * ally_gamepad_check_ready() - Wait for the gamepad MCU to report ready
+ * @hdev: HID device
+ *
+ * This should be called before any remapping attempts, and on driver
+ * init/resume, after the asus handshake has been performed on the
+ * configuration endpoint.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_gamepad_check_ready(struct hid_device *hdev)
+{
+ const u8 payload[] = { 0x00 };
+ size_t buf_len;
+ int ret;
+
+ for (int i = 0; i < HID_ALLY_READY_MAX_TRIES; i++) {
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_CHECK_READY,
+ payload, sizeof(payload),
+ &buf_len);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_receive_packet(hdev, buf, buf_len);
+ if (ret < 0)
+ hid_dbg(hdev, "ROG Ally check %d/%d failed: %d\n", i,
+ HID_ALLY_READY_MAX_TRIES, ret);
+ else if (buf[2] == CMD_CHECK_READY)
+ return 0;
+
+ /* Back off on a bus error as well as on a not-ready answer. */
+ usleep_range(1000, 2000);
+ }
+
+ hid_err(hdev, "ROG Ally never responded with a ready\n");
+ return -ENODEV;
+}
+
+static int ally_get_endpoint_address(struct hid_device *hdev)
+{
+ struct usb_host_endpoint *ep;
+ struct usb_interface *intf;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ intf = to_usb_interface(hdev->dev.parent);
+ if (!intf || !intf->cur_altsetting)
+ return -ENODEV;
+
+ if (intf->cur_altsetting->desc.bNumEndpoints == 0)
+ return -ENODEV;
+
+ ep = intf->cur_altsetting->endpoint;
+ if (!ep)
+ return -ENODEV;
+
+ return ep->desc.bEndpointAddress;
+}
+
+struct ally_x_input_report {
+ __le16 x, y;
+ __le16 rx, ry;
+ __le16 z, rz;
+ u8 buttons[3];
+} __packed;
+
+/* The hatswitch outputs integers, we use them to index this X|Y pair */
+static const int hat_values[][2] = {
+ { 0, 0 }, { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 },
+ { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 },
+};
+
+/**
+ * ally_x_raw_event() - Parse and report an Ally X gamepad input report
+ * @input: input device to report the events on
+ * @data: raw report buffer
+ * @size: length of @data in bytes
+ *
+ * Return: true if the event was handled, otherwise false.
+ */
+static bool ally_x_raw_event(struct input_dev *input, u8 *data, int size)
+{
+ struct ally_x_input_report *in_report;
+ u16 x, y, rx, ry, z, rz;
+ u8 byte;
+
+ if (!input)
+ return false;
+
+ if (size < 1)
+ return false;
+
+ if (data[0] != HID_ALLY_X_INPUT_REPORT)
+ return false;
+
+ /*
+ * hid-core only guarantees size >= 1 and does not zero-pad short
+ * reports before ->raw_event, so a truncated transfer would leave the
+ * payload below pointing at stale DMA buffer contents.
+ */
+ if (size < 1 + sizeof(*in_report))
+ return false;
+
+ in_report = (struct ally_x_input_report *)&data[1];
+
+ /* USB HID payloads are little-endian: convert them explicitly. */
+ x = get_unaligned_le16(&in_report->x);
+ y = get_unaligned_le16(&in_report->y);
+ rx = get_unaligned_le16(&in_report->rx);
+ ry = get_unaligned_le16(&in_report->ry);
+ z = get_unaligned_le16(&in_report->z);
+ rz = get_unaligned_le16(&in_report->rz);
+
+ input_report_abs(input, ABS_X, x - 32768);
+ input_report_abs(input, ABS_Y, y - 32768);
+ input_report_abs(input, ABS_RX, rx - 32768);
+ input_report_abs(input, ABS_RY, ry - 32768);
+ input_report_abs(input, ABS_Z, z);
+ input_report_abs(input, ABS_RZ, rz);
+
+ byte = in_report->buttons[0];
+ input_report_key(input, BTN_A, byte & BIT(0));
+ input_report_key(input, BTN_B, byte & BIT(1));
+ input_report_key(input, BTN_X, byte & BIT(2));
+ input_report_key(input, BTN_Y, byte & BIT(3));
+ input_report_key(input, BTN_TL, byte & BIT(4));
+ input_report_key(input, BTN_TR, byte & BIT(5));
+ input_report_key(input, BTN_SELECT, byte & BIT(6));
+ input_report_key(input, BTN_START, byte & BIT(7));
+
+ byte = in_report->buttons[1];
+ input_report_key(input, BTN_THUMBL, byte & BIT(0));
+ input_report_key(input, BTN_THUMBR, byte & BIT(1));
+ input_report_key(input, BTN_MODE, byte & BIT(2));
+
+ /*
+ * The hatswitch byte is device-controlled; treat anything the table
+ * does not cover as centred rather than indexing out of bounds.
+ */
+ byte = in_report->buttons[2];
+ if (byte >= ARRAY_SIZE(hat_values))
+ byte = 0;
+ input_report_abs(input, ABS_HAT0X, hat_values[byte][0]);
+ input_report_abs(input, ABS_HAT0Y, hat_values[byte][1]);
+
+ input_sync(input);
+
+ return true;
+}
+
+static int ally_x_input_open(struct input_dev *dev)
+{
+ struct ally_intf *intf = input_get_drvdata(dev);
+
+ return hid_hw_open(intf->hdev);
+}
+
+static void ally_x_input_close(struct input_dev *dev)
+{
+ struct ally_intf *intf = input_get_drvdata(dev);
+
+ hid_hw_close(intf->hdev);
+}
+
+static struct input_dev *ally_x_alloc_input_dev(struct hid_device *hdev,
+ struct ally_intf *intf)
+{
+ struct input_dev *input_dev = input_allocate_device();
+
+ if (!input_dev)
+ return ERR_PTR(-ENOMEM);
+
+ input_dev->dev.parent = &hdev->dev;
+ input_dev->id.bustype = hdev->bus;
+ input_dev->id.vendor = hdev->vendor;
+ input_dev->id.product = hdev->product;
+ input_dev->id.version = hdev->version;
+ input_dev->uniq = hdev->uniq;
+ input_dev->name = "ASUS ROG Ally X Gamepad";
+
+ input_set_drvdata(input_dev, intf);
+ /*
+ * Let the input core hold the hardware open while the device node
+ * is in use: without these the interface could be runtime-suspended
+ * and drop events.
+ */
+ input_dev->open = ally_x_input_open;
+ input_dev->close = ally_x_input_close;
+
+ return input_dev;
+}
+
+static int ally_x_setup_input(struct hid_device *hdev, struct ally_intf *intf)
+{
+ struct input_dev *input = ally_x_alloc_input_dev(hdev, intf);
+ int ret;
+
+ if (IS_ERR(input))
+ return PTR_ERR(input);
+
+ input_set_abs_params(input, ABS_X, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_Y, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_RX, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_RY, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_Z, 0, 1023, 0, 0);
+ input_set_abs_params(input, ABS_RZ, 0, 1023, 0, 0);
+ input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+ input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+ input_set_capability(input, EV_KEY, BTN_A);
+ input_set_capability(input, EV_KEY, BTN_B);
+ input_set_capability(input, EV_KEY, BTN_X);
+ input_set_capability(input, EV_KEY, BTN_Y);
+ input_set_capability(input, EV_KEY, BTN_TL);
+ input_set_capability(input, EV_KEY, BTN_TR);
+ input_set_capability(input, EV_KEY, BTN_SELECT);
+ input_set_capability(input, EV_KEY, BTN_START);
+ input_set_capability(input, EV_KEY, BTN_MODE);
+ input_set_capability(input, EV_KEY, BTN_THUMBL);
+ input_set_capability(input, EV_KEY, BTN_THUMBR);
+
+ ret = input_register_device(input);
+ if (ret) {
+ hid_err(hdev, "Failed to register Ally X gamepad device: %d\n", ret);
+ input_free_device(input);
+ return ret;
+ }
+
+ intf->gamepad = input;
+ return 0;
+}
+
+static int hid_asus_ally_init(struct hid_device *hdev)
+{
+ int ret;
+
+ /* Failure at this point is non-critical. The packet goes to this interface. */
+ ret = ally_gamepad_send_packet(hdev, ally_force_feedback_off,
+ sizeof(ally_force_feedback_off));
+ if (ret < 0)
+ hid_err(hdev, "Ally failed to init force-feedback off: %d\n", ret);
+
+ return 0;
+}
+
+/**
+ * hid_asus_ally_raw_event() - Route raw reports from one Ally interface
+ * @hdev: HID device the report arrived on
+ * @intf: that interface's ally state
+ * @data: raw report buffer
+ * @size: length of @data in bytes
+ *
+ * Called from the raw_event callback, which may run in atomic context.
+ * Only this interface's own input devices are used.
+ *
+ * Return: true if the event was handled, otherwise false.
+ */
+static bool hid_asus_ally_raw_event(struct hid_device *hdev, struct ally_intf *intf,
+ u8 *data, int size)
+{
+ if (!intf)
+ return false;
+
+ if (ally_vendor_keys(intf->input, data, size))
+ return true;
+
+ switch (ally_get_endpoint_address(hdev)) {
+ case HID_ALLY_X_INTF_IN:
+ return ally_x_raw_event(intf->gamepad, data, size);
+ case HID_ALLY_INTF_KEYBOARD_IN:
+ if (handle_ctrl_alt_del(intf, data, size))
+ return false;
+ break;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+static struct input_dev *ally_interface_input(struct hid_device *hdev)
+{
+ struct hid_input *hidinput;
+
+ if (list_empty(&hdev->inputs))
+ return NULL;
+
+ hidinput = list_first_entry(&hdev->inputs, struct hid_input, list);
+ return hidinput->input;
+}
+
+/**
+ * hid_asus_ally_probe() - Initialize the ROG Ally HID extension
+ * @hdev: HID device
+ *
+ * The Ally code is a plugin of the Asus HID driver: one ally_intf per
+ * bound interface, freed by the caller after hid_hw_stop(). Interfaces
+ * do not share device pointers, so a second interface cannot evict the
+ * first.
+ *
+ * Return: an ally_intf pointer on success, NULL when this endpoint has no
+ * Ally role, or an ERR_PTR on failure. On failure the function frees
+ * anything it allocated.
+ */
+static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
+{
+ int ret, ep = ally_get_endpoint_address(hdev);
+
+ if (ep < 0)
+ return ERR_PTR(ep);
+
+ if (ep != HID_ALLY_INTF_CFG_IN && ep != HID_ALLY_X_INTF_IN &&
+ ep != HID_ALLY_INTF_KEYBOARD_IN)
+ return NULL;
+
+ struct ally_intf *intf __free(kfree) = kzalloc_obj(*intf);
+
+ if (!intf)
+ return ERR_PTR(-ENOMEM);
+
+ intf->hdev = hdev;
+ intf->input = ally_interface_input(hdev);
+ INIT_DELAYED_WORK(&intf->resume_work, ally_resume_work_fn);
+
+ switch (ep) {
+ case HID_ALLY_INTF_CFG_IN:
+ /*
+ * This function assumes the asus-specific initialization
+ * to have been performed already at this point.
+ */
+ ret = ally_gamepad_check_ready(hdev);
+ if (ret < 0) {
+ hid_err(hdev, "ROG Ally device is not ready: %d\n", ret);
+ return ERR_PTR(ret);
+ }
+
+ hid_asus_ally_init(hdev);
+ break;
+ case HID_ALLY_X_INTF_IN:
+ ret = ally_x_setup_input(hdev, intf);
+ if (ret) {
+ hid_err(hdev, "Failed to create Ally X gamepad device.\n");
+ return ERR_PTR(ret);
+ }
+ break;
+ case HID_ALLY_INTF_KEYBOARD_IN:
+ break;
+ default:
+ return NULL;
+ }
+
+ return no_free_ptr(intf);
+}
+
+static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf *intf)
+{
+ int ep;
+
+ /*
+ * The extensions failed to probe and the device is operating as a
+ * generic HID device: do not fail the resume because of that.
+ */
+ if (!intf)
+ return 0;
+
+ ep = ally_get_endpoint_address(hdev);
+ if (ep != HID_ALLY_INTF_CFG_IN)
+ return 0;
+
+ return hid_asus_ally_init(hdev);
+}
+
/*
* Send events to asus-wmi driver for handling special keys
*/
@@ -512,6 +1308,21 @@ static int asus_raw_event(struct hid_device *hdev,
if (drvdata->quirks & QUIRK_MEDION_E1239T)
return asus_e1239t_event(drvdata, data, size);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ struct ally_intf *intf;
+
+ /*
+ * Return -1 to suppress further processing by the generic HID
+ * input parser for reports we fully handle. The interface is
+ * published with smp_store_release() only after its fields
+ * are set. Letting report 0x0B reach the default parser
+ * creates a generic gamepad and Steam Input overlaps.
+ */
+ intf = smp_load_acquire(&drvdata->ally); /* fields are published */
+ if (hid_asus_ally_raw_event(hdev, intf, data, size))
+ return -1;
+ }
+
/*
* Skip these report ID, the device emits a continuous stream associated
* with the AURA mode it is in which looks like an 'echo'.
@@ -581,104 +1392,6 @@ static int asus_raw_event(struct hid_device *hdev,
return 0;
}
-/**
- * asus_find_report() - Find a report of the given type by its ID
- * @hdev: HID device
- * @type: report type (HID_INPUT_REPORT, HID_OUTPUT_REPORT or
- * HID_FEATURE_REPORT)
- * @report_id: report ID to look for
- *
- * Return: the report with the given ID, or NULL if the device does not
- * declare such a report.
- */
-static struct hid_report *asus_find_report(struct hid_device *hdev,
- unsigned int type, u8 report_id)
-{
- return hdev->report_enum[type].report_id_hash[report_id];
-}
-
-/**
- * asus_report_len() - Length of a feature report for the device
- * @hdev: HID device
- * @report_id: report ID of the feature report
- *
- * The same ASUS report ID can have different sizes depending on the device:
- * report 0x5E is a 64-byte report on laptops but a 300-byte report on XG
- * Mobile stations. Always size transfers from the report descriptor of the
- * device at hand instead of hardcoding lengths.
- *
- * Return: the length of the feature report in bytes, or 0 if the device
- * does not declare the report.
- */
-static size_t asus_report_len(struct hid_device *hdev, u8 report_id)
-{
- struct hid_report *report;
-
- report = asus_find_report(hdev, HID_FEATURE_REPORT, report_id);
-
- return report ? hid_report_len(report) : 0;
-}
-
-static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size)
-{
- size_t hw_len = asus_report_len(hdev, buf[0]);
-
- /* The payload (including the report ID) must fit the device report. */
- if (!hw_len || buf_size > hw_len)
- return -EINVAL;
-
- u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
- if (!dmabuf)
- return -ENOMEM;
-
- memcpy(dmabuf, buf, buf_size);
-
- /*
- * The report ID should be set from the incoming buffer due to LED and
- * key interfaces having different pages. Short payloads are zero-padded
- * to the exact report size the hardware expects for this report ID.
- */
- return hid_hw_raw_request(hdev, buf[0], dmabuf, hw_len, HID_FEATURE_REPORT,
- HID_REQ_SET_REPORT);
-}
-
-/**
- * asus_kbd_get_report() - Read a report sized from the descriptor
- * @hdev: HID device
- * @report_id: report ID to read
- * @buf: buffer to store the report in
- * @buf_size: size of @buf
- *
- * The transfer is performed at the exact report size the device declares
- * for @report_id. The device may answer with a short read, and only the
- * bytes that fit in @buf are copied into it.
- *
- * Return: 0 on success, negative errno on failure.
- */
-static int asus_kbd_get_report(struct hid_device *hdev, u8 report_id,
- u8 *buf, size_t buf_size)
-{
- size_t hw_len = asus_report_len(hdev, report_id);
- int ret;
-
- if (!hw_len)
- return -EINVAL;
-
- u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
- if (!dmabuf)
- return -ENOMEM;
-
- ret = hid_hw_raw_request(hdev, report_id, dmabuf, hw_len,
- HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
- if (ret < 0)
- return ret;
-
- /* The device may return less than requested (short read). */
- memcpy(buf, dmabuf, min(buf_size, hw_len));
-
- return 0;
-}
-
static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
{
/*
@@ -1395,6 +2108,7 @@ static int asus_input_mapping(struct hid_device *hdev,
case 0x5f: asus_map_key_clear(KEY_PROG2); break; /* S-shaped programmable key */
case 0x6b: asus_map_key_clear(KEY_F21); break; /* ASUS touchpad toggle */
case 0x38: asus_map_key_clear(KEY_PROG1); break; /* ROG key */
+ case 0x93: asus_map_key_clear(KEY_PROG1); break; /* ROG Ally X AC button */
case 0xba: asus_map_key_clear(KEY_PROG2); break; /* Fn+C ASUS Splendid */
case 0x5c: asus_map_key_clear(KEY_PROG3); break; /* Fn+Space Power4Gear */
case 0x99: asus_map_key_clear(KEY_PROG4); break; /* Fn+F5 "fan" symbol */
@@ -1415,6 +2129,25 @@ static int asus_input_mapping(struct hid_device *hdev,
return -1;
}
+ /*
+ * Declare the Ally vendor keys before this input is registered.
+ * KEY_F19 is the second event of the ROG key (usage 0x38).
+ */
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ switch (usage->hid & HID_USAGE) {
+ case 0x38:
+ case 0xa6:
+ case 0xa7:
+ case 0xa8:
+ input_set_capability(hi->input, EV_KEY, KEY_PROG1);
+ input_set_capability(hi->input, EV_KEY, KEY_F16);
+ input_set_capability(hi->input, EV_KEY, KEY_F17);
+ input_set_capability(hi->input, EV_KEY, KEY_F18);
+ input_set_capability(hi->input, EV_KEY, KEY_F19);
+ break;
+ }
+ }
+
set_bit(EV_REP, hi->input->evbit);
return 1;
}
@@ -1569,6 +2302,8 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
static int __maybe_unused asus_resume(struct hid_device *hdev)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+ /* Published with smp_store_release() after the fields are set. */
+ struct ally_intf *intf = smp_load_acquire(&drvdata->ally);
/*
* If we have a backlight listener registered, restore the previous state,
@@ -1578,12 +2313,16 @@ static int __maybe_unused asus_resume(struct hid_device *hdev)
if (drvdata->listener.brightness_set)
asus_kbd_backlight_set(&drvdata->listener, drvdata->kbd_backlight_brightness);
+ if (intf)
+ schedule_delayed_work(&intf->resume_work, msecs_to_jiffies(500));
+
return 0;
}
static int __maybe_unused asus_reset_resume(struct hid_device *hdev)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
if (!drvdata->tp)
asus_initialize_reports(hdev);
@@ -1591,6 +2330,15 @@ static int __maybe_unused asus_reset_resume(struct hid_device *hdev)
if (drvdata->tp)
return asus_start_multitouch(hdev);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ /* Published with smp_store_release() after the fields are set. */
+ ret = hid_asus_ally_reset_resume(hdev, smp_load_acquire(&drvdata->ally));
+ if (ret) {
+ hid_err(hdev, "Failed to resume ROG Ally HID extensions: %d\n", ret);
+ return ret;
+ }
+ }
+
return 0;
}
@@ -1598,6 +2346,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
struct hid_report_enum *rep_enum;
struct asus_drvdata *drvdata;
+ struct ally_intf *ally;
struct hid_report *rep;
bool is_vendor = false;
int ret;
@@ -1715,12 +2464,22 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
}
}
- /* Laptops keyboard backlight is always at 0x5a */
+ /* Laptops keyboard backlight is always FEATURE_KBD_REPORT_ID */
if (is_vendor && (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT) &&
(asus_has_report_id(hdev, FEATURE_KBD_REPORT_ID)) &&
(asus_kbd_register_leds(hdev)))
hid_warn(hdev, "Failed to initialize backlight.\n");
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ ally = hid_asus_ally_probe(hdev);
+ if (IS_ERR(ally))
+ hid_err(hdev, "Failed to initialize ROG Ally HID extensions: %ld\n",
+ PTR_ERR(ally));
+ else
+ /* Readers take this with smp_load_acquire(). */
+ smp_store_release(&drvdata->ally, ally);
+ }
+
/*
* For ROG keyboards, skip rename for consistency and ->input check as
* some devices do not have inputs.
@@ -1755,8 +2514,14 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ ally_quiesce(drvdata->ally);
+
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
+
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ ally_free(drvdata->ally);
return ret;
}
@@ -1770,8 +2535,14 @@ static void asus_remove(struct hid_device *hdev)
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ ally_quiesce(drvdata->ally);
+
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
+
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ ally_free(drvdata->ally);
}
static const __u8 asus_g752_fixed_rdesc[] = {
--
2.47.3
next prev parent reply other threads:[~2026-10-31 17:41 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-31 17:41 [PATCH v12 00/17] " Denis Benato
2026-10-31 17:41 ` [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor Denis Benato
2026-10-31 18:30 ` Rebecca Mara Müller
2026-10-31 17:41 ` Denis Benato [this message]
2026-10-31 17:41 ` [PATCH v12 03/17] HID: asus: add gamepad configuration Denis Benato
2026-10-31 17:41 ` [PATCH v12 04/17] HID: asus: add vibration strength configuration Denis Benato
2026-10-31 17:41 ` [PATCH v12 05/17] HID: asus: add joysticks inner and outer range configuration Denis Benato
2026-10-31 17:41 ` [PATCH v12 06/17] HID: asus: add triggers " Denis Benato
2026-10-31 17:41 ` [PATCH v12 07/17] HID: asus: add joysticks anti-deadzone configuration Denis Benato
2026-10-31 17:41 ` [PATCH v12 08/17] HID: asus: add support for response curve Denis Benato
2026-10-31 17:41 ` [PATCH v12 09/17] HID: asus: add support to force feedback Denis Benato
2026-10-31 17:41 ` [PATCH v12 10/17] HID: asus: add support for gamepad mode Denis Benato
2026-10-31 17:41 ` [PATCH v12 11/17] HID: asus: add support for turbo buttons Denis Benato
2026-10-31 17:41 ` [PATCH v12 12/17] HID: asus: add support for btn remapping Denis Benato
2026-10-31 17:41 ` [PATCH v12 13/17] HID: asus: identify XG Mobile stations and bind the LED to the reported model Denis Benato
2026-10-31 17:41 ` [PATCH v12 14/17] HID: asus: expose XG Mobile identity and firmware information Denis Benato
2026-10-31 17:41 ` [PATCH v12 15/17] HID: asus: document ROG XG Mobile sysfs attributes Denis Benato
2026-10-31 17:41 ` [PATCH v12 16/17] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU Denis Benato
2026-10-31 17:41 ` [PATCH v12 17/17] MAINTAINERS: add maintainer for the ASUS HID driver Denis Benato
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261031174130.208240-3-denis.benato@linux.dev \
--to=denis.benato@linux.dev \
--cc=benato.denis96@gmail.com \
--cc=bentiss@kernel.org \
--cc=corentin.chary@gmail.com \
--cc=derekjohn.clark@gmail.com \
--cc=hacker1024@users.sourceforge.net \
--cc=hansg@kernel.org \
--cc=ilpo.jarvinen@linux.intel.com \
--cc=jikos@kernel.org \
--cc=jlobue10@gmail.com \
--cc=kernel@lists.opengamingcollective.org \
--cc=khamunetriclark@gmail.com \
--cc=linux-input@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=luke@ljones.dev \
--cc=matthew.schwartz@linux.dev \
--cc=me@livan.pro \
--cc=nathan@kernel.org \
--cc=platform-driver-x86@vger.kernel.org \
--cc=rebecca.mara@posteo.de \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®