* [PATCH v3 1/7] HID: hid-lenovo-go: use the correct calibration commands
2026-09-01 6:36 [PATCH v3 0/7] HID: hid-lenovo-go: fix controller configuration Aditya Dash
@ 2026-09-01 6:36 ` Aditya Dash
2026-09-01 6:36 ` [PATCH v3 2/7] HID: hid-lenovo-go: use the right controller selectors Aditya Dash
` (5 subsequent siblings)
6 siblings, 0 replies; 12+ messages in thread
From: Aditya Dash @ 2026-09-01 6:36 UTC (permalink / raw)
To: Benjamin Tissoires, Derek J. Clark, Jiri Kosina, Mark Pearson
Cc: linux-input, linux-kernel
The calibration attributes pass the calibration operation as the command
and the configuration command as the sub-command. This sends each request
with the two fields reversed.
Swap the command and sub-command values for all six calibration attributes.
For example, the malformed left joystick Start request is:
05 00 04 0c 03 01 ...
The corrected request is:
05 00 0c 04 03 01 ...
The bytes identify the output report, configuration group, command,
sub-command, left controller, and Start action, in that order.
Fixes: 995887a10da1 ("HID: hid-lenovo-go: Add Calibration Settings")
Assisted-by: Pi:gpt-5.6-sol
Signed-off-by: Aditya Dash <mradityadash@gmail.com>
Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 318b1152ff8b..b8248bf8965c 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -1911,18 +1911,18 @@ LEGO_DEVICE_ATTR_RW(rumble_notification_left, "rumble_notification",
static DEVICE_ATTR_RO_NAMED(rumble_notification_left_index,
"rumble_notification_index");
-static struct go_cfg_attr cal_trigg_left = { TRIGGER_CALIBRATE };
-LEGO_CAL_DEVICE_ATTR(cal_trigg_left, "calibrate_trigger", SET_TRIGGER_CFG,
+static struct go_cfg_attr cal_trigg_left = { SET_TRIGGER_CFG };
+LEGO_CAL_DEVICE_ATTR(cal_trigg_left, "calibrate_trigger", TRIGGER_CALIBRATE,
LEFT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_trigg_left_index, "calibrate_trigger_index");
-static struct go_cfg_attr cal_joy_left = { JOYSTICK_CALIBRATE };
-LEGO_CAL_DEVICE_ATTR(cal_joy_left, "calibrate_joystick", SET_JOYSTICK_CFG,
+static struct go_cfg_attr cal_joy_left = { SET_JOYSTICK_CFG };
+LEGO_CAL_DEVICE_ATTR(cal_joy_left, "calibrate_joystick", JOYSTICK_CALIBRATE,
LEFT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_joy_left_index, "calibrate_joystick_index");
-static struct go_cfg_attr cal_gyro_left = { GYRO_CALIBRATE };
-LEGO_CAL_DEVICE_ATTR(cal_gyro_left, "calibrate_gyro", SET_GYRO_CFG,
+static struct go_cfg_attr cal_gyro_left = { SET_GYRO_CFG };
+LEGO_CAL_DEVICE_ATTR(cal_gyro_left, "calibrate_gyro", GYRO_CALIBRATE,
LEFT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_gyro_left_index, "calibrate_gyro_index");
@@ -2018,18 +2018,18 @@ LEGO_DEVICE_ATTR_RW(rumble_notification_right, "rumble_notification",
static DEVICE_ATTR_RO_NAMED(rumble_notification_right_index,
"rumble_notification_index");
-static struct go_cfg_attr cal_trigg_right = { TRIGGER_CALIBRATE };
-LEGO_CAL_DEVICE_ATTR(cal_trigg_right, "calibrate_trigger", SET_TRIGGER_CFG,
+static struct go_cfg_attr cal_trigg_right = { SET_TRIGGER_CFG };
+LEGO_CAL_DEVICE_ATTR(cal_trigg_right, "calibrate_trigger", TRIGGER_CALIBRATE,
RIGHT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_trigg_right_index, "calibrate_trigger_index");
-static struct go_cfg_attr cal_joy_right = { JOYSTICK_CALIBRATE };
-LEGO_CAL_DEVICE_ATTR(cal_joy_right, "calibrate_joystick", SET_JOYSTICK_CFG,
+static struct go_cfg_attr cal_joy_right = { SET_JOYSTICK_CFG };
+LEGO_CAL_DEVICE_ATTR(cal_joy_right, "calibrate_joystick", JOYSTICK_CALIBRATE,
RIGHT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_joy_right_index, "calibrate_joystick_index");
-static struct go_cfg_attr cal_gyro_right = { GYRO_CALIBRATE };
-LEGO_CAL_DEVICE_ATTR(cal_gyro_right, "calibrate_gyro", SET_GYRO_CFG,
+static struct go_cfg_attr cal_gyro_right = { SET_GYRO_CFG };
+LEGO_CAL_DEVICE_ATTR(cal_gyro_right, "calibrate_gyro", GYRO_CALIBRATE,
RIGHT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_gyro_right_index, "calibrate_gyro_index");
--
2.55.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH v3 2/7] HID: hid-lenovo-go: use the right controller selectors
2026-09-01 6:36 [PATCH v3 0/7] HID: hid-lenovo-go: fix controller configuration Aditya Dash
2026-09-01 6:36 ` [PATCH v3 1/7] HID: hid-lenovo-go: use the correct calibration commands Aditya Dash
@ 2026-09-01 6:36 ` Aditya Dash
2026-09-01 6:36 ` [PATCH v3 3/7] HID: hid-lenovo-go: initialize command completion before hardware Aditya Dash
` (4 subsequent siblings)
6 siblings, 0 replies; 12+ messages in thread
From: Aditya Dash @ 2026-09-01 6:36 UTC (permalink / raw)
To: Benjamin Tissoires, Derek J. Clark, Jiri Kosina, Mark Pearson
Cc: linux-input, linux-kernel
The right imu_enabled attribute selects FEATURE_IMU_BYPASS, and the right
reset attribute targets the left controller.
Select FEATURE_IMU_ENABLE for the right imu_enabled attribute. Target the
right controller for a right reset.
Fixes: 82cd9bc866e1 ("HID: hid-lenovo-go: Add Feature Status Attributes")
Assisted-by: Pi:gpt-5.6-sol
Signed-off-by: Aditya Dash <mradityadash@gmail.com>
Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index b8248bf8965c..b937d11dd6f6 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -1999,13 +1999,13 @@ LEGO_DEVICE_ATTR_RW(imu_bypass_right, "imu_bypass_enabled", RIGHT_CONTROLLER,
index, feature_status);
static DEVICE_ATTR_RO_NAMED(imu_bypass_right_index, "imu_bypass_enabled_index");
-static struct go_cfg_attr imu_enabled_right = { FEATURE_IMU_BYPASS };
+static struct go_cfg_attr imu_enabled_right = { FEATURE_IMU_ENABLE };
LEGO_DEVICE_ATTR_RW(imu_enabled_right, "imu_enabled", RIGHT_CONTROLLER, index,
feature_status);
static DEVICE_ATTR_RO_NAMED(imu_enabled_right_index, "imu_enabled_index");
static struct go_cfg_attr reset_right = { FEATURE_RESET_GAMEPAD };
-LEGO_DEVICE_ATTR_WO(reset_right, "reset", LEFT_CONTROLLER, feature_status);
+LEGO_DEVICE_ATTR_WO(reset_right, "reset", RIGHT_CONTROLLER, feature_status);
static struct go_cfg_attr rumble_mode_right = { RUMBLE_MODE };
LEGO_DEVICE_ATTR_RW(rumble_mode_right, "rumble_mode", RIGHT_CONTROLLER, index,
--
2.55.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH v3 3/7] HID: hid-lenovo-go: initialize command completion before hardware
2026-09-01 6:36 [PATCH v3 0/7] HID: hid-lenovo-go: fix controller configuration Aditya Dash
2026-09-01 6:36 ` [PATCH v3 1/7] HID: hid-lenovo-go: use the correct calibration commands Aditya Dash
2026-09-01 6:36 ` [PATCH v3 2/7] HID: hid-lenovo-go: use the right controller selectors Aditya Dash
@ 2026-09-01 6:36 ` Aditya Dash
2026-09-01 6:36 ` [PATCH v3 4/7] HID: hid-lenovo-go: return configuration request errors Aditya Dash
` (3 subsequent siblings)
6 siblings, 0 replies; 12+ messages in thread
From: Aditya Dash @ 2026-09-01 6:36 UTC (permalink / raw)
To: Benjamin Tissoires, Derek J. Clark, Jiri Kosina, Mark Pearson
Cc: linux-input, linux-kernel
The configuration interface initializes its command completion in
cfg_probe(). The hardware starts and opens before this call. Keep the
completion ready before hardware I/O becomes available.
Identify the interface after parsing it. Initialize the completion for the
configuration interface before starting and opening HID hardware. Keep the
remaining configuration setup in cfg_probe().
Fixes: d69ccfcbc955 ("HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver")
Assisted-by: Pi:gpt-5.6-sol
Signed-off-by: Aditya Dash <mradityadash@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index b937d11dd6f6..51e3112780e1 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -2388,8 +2388,6 @@ static int hid_go_cfg_probe(struct hid_device *hdev,
drvdata.led_cdev = &go_cdev_rgb.led_cdev;
- init_completion(&drvdata.send_cmd_complete);
-
/* Executing calls prior to returning from probe will lock the MCU. Schedule
* initial data call after probe has completed and MCU can accept calls.
*/
@@ -2436,6 +2434,10 @@ static int hid_go_probe(struct hid_device *hdev, const struct hid_device_id *id)
return ret;
}
+ ep = get_endpoint_address(hdev);
+ if (ep == GO_GP_INTF_IN)
+ init_completion(&drvdata.send_cmd_complete);
+
ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
if (ret) {
hid_err(hdev, "Failed to start HID device\n");
@@ -2449,7 +2451,6 @@ static int hid_go_probe(struct hid_device *hdev, const struct hid_device_id *id)
return ret;
}
- ep = get_endpoint_address(hdev);
if (ep != GO_GP_INTF_IN) {
dev_dbg(&hdev->dev, "Started interface %x as generic HID device\n", ep);
return 0;
--
2.55.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH v3 4/7] HID: hid-lenovo-go: return configuration request errors
2026-09-01 6:36 [PATCH v3 0/7] HID: hid-lenovo-go: fix controller configuration Aditya Dash
` (2 preceding siblings ...)
2026-09-01 6:36 ` [PATCH v3 3/7] HID: hid-lenovo-go: initialize command completion before hardware Aditya Dash
@ 2026-09-01 6:36 ` Aditya Dash
2026-09-01 6:36 ` [PATCH v3 5/7] HID: hid-lenovo-go: normalize calibration failure status Aditya Dash
` (2 subsequent siblings)
6 siblings, 0 replies; 12+ messages in thread
From: Aditya Dash @ 2026-09-01 6:36 UTC (permalink / raw)
To: Benjamin Tissoires, Derek J. Clark, Jiri Kosina, Mark Pearson
Cc: linux-input, linux-kernel
Any decoded configuration reply completes the shared completion. The driver
also ignores the result of its interruptible wait. An unrelated reply, a
firmware error, an interrupted wait, or a timeout can therefore be reported
as success.
Add pending command state to the driver data. Match replies by command ID,
command, sub-command, and device, and return request errors to the caller.
Clear the pending request after an interruption or timeout so a late reply
cannot replace that result.
Keep the existing 50 ms wait for every configuration request. Corrected
calibration requests on an Original Legion Go received their matching SET
replies within 8 ms. The final calibration result remains asynchronous.
The firmware has no sequence number. A late reply with the same tuple can
still match a newer request.
Fixes: d69ccfcbc955 ("HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver")
Assisted-by: Pi:gpt-5.6-sol
Signed-off-by: Aditya Dash <mradityadash@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 109 +++++++++++++++++++++++++++++-------
1 file changed, 88 insertions(+), 21 deletions(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 51e3112780e1..deed7acd2dc4 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -21,6 +21,7 @@
#include <linux/led-class-multicolor.h>
#include <linux/mutex.h>
#include <linux/printk.h>
+#include <linux/spinlock.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/unaligned.h>
@@ -31,13 +32,27 @@
#include "hid-ids.h"
#define GO_GP_INTF_IN 0x83
+#define GO_INPUT_REPORT_ID 0x04
#define GO_OUTPUT_REPORT_ID 0x05
#define GO_GP_RESET_SUCCESS 0x01
#define GO_PACKET_SIZE 64
+#define GO_COMMAND_TIMEOUT_MS 50
+
+/* Lenovo replies identify a command and a sub-command, but have no sequence. */
+struct hid_go_cmd {
+ struct completion done;
+ spinlock_t lock; /* protects fields below */
+ bool pending;
+ u8 id;
+ u8 command;
+ u8 sub_command;
+ u8 device;
+ int result;
+};
static struct hid_go_cfg {
struct delayed_work go_cfg_setup;
- struct completion send_cmd_complete;
+ struct hid_go_cmd cmd;
struct led_classdev *led_cdev;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
@@ -331,6 +346,62 @@ static const char *const os_mode_text[] = {
[LINUX] = "linux",
};
+static void hid_go_cmd_arm(u8 id, u8 command, u8 sub_command, u8 device)
+{
+ guard(spinlock_irqsave)(&drvdata.cmd.lock);
+
+ reinit_completion(&drvdata.cmd.done);
+ drvdata.cmd.pending = true;
+ drvdata.cmd.id = id;
+ drvdata.cmd.command = command;
+ drvdata.cmd.sub_command = sub_command;
+ drvdata.cmd.device = device;
+}
+
+static void hid_go_cmd_consume(const struct command_report *cmd_rep, int result)
+{
+ guard(spinlock_irqsave)(&drvdata.cmd.lock);
+
+ if (drvdata.cmd.pending && cmd_rep->id == drvdata.cmd.id &&
+ cmd_rep->cmd == drvdata.cmd.command &&
+ cmd_rep->sub_cmd == drvdata.cmd.sub_command &&
+ cmd_rep->device_type == drvdata.cmd.device) {
+ drvdata.cmd.pending = false;
+ drvdata.cmd.result = result;
+ complete(&drvdata.cmd.done);
+ }
+}
+
+static int hid_go_cmd_finish(long wait_result)
+{
+ guard(spinlock_irqsave)(&drvdata.cmd.lock);
+
+ if (wait_result <= 0) {
+ drvdata.cmd.pending = false;
+ return wait_result < 0 ? wait_result : -ETIMEDOUT;
+ }
+
+ return drvdata.cmd.result;
+}
+
+static int hid_go_cmd_cancel(int result)
+{
+ guard(spinlock_irqsave)(&drvdata.cmd.lock);
+
+ drvdata.cmd.pending = false;
+ return result;
+}
+
+static int hid_go_send_output_report(struct hid_device *hdev, u8 *packet)
+{
+ int ret;
+
+ ret = hid_hw_output_report(hdev, packet, GO_PACKET_SIZE);
+ if (ret < 0)
+ return ret;
+ return ret == GO_PACKET_SIZE ? 0 : -EINVAL;
+}
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -654,7 +725,7 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
struct command_report *cmd_rep;
int ep, ret;
- if (size != GO_PACKET_SIZE)
+ if (size != GO_PACKET_SIZE || data[0] != GO_INPUT_REPORT_ID)
goto passthrough;
ep = get_endpoint_address(hdev);
@@ -707,7 +778,7 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n",
GO_PACKET_SIZE, data);
- complete(&drvdata.send_cmd_complete);
+ hid_go_cmd_consume(cmd_rep, ret);
return ret;
passthrough:
@@ -722,7 +793,8 @@ static int mcu_property_out(struct hid_device *hdev, u8 id, u8 command,
unsigned char *dmabuf __free(kfree) = NULL;
u8 header[] = { GO_OUTPUT_REPORT_ID, id, command, index, device };
size_t header_size = ARRAY_SIZE(header);
- int timeout = 50;
+ unsigned long timeout = msecs_to_jiffies(GO_COMMAND_TIMEOUT_MS);
+ long wait_result;
int ret;
if (header_size + len > GO_PACKET_SIZE)
@@ -740,22 +812,14 @@ static int mcu_property_out(struct hid_device *hdev, u8 id, u8 command,
dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
GO_PACKET_SIZE, dmabuf);
- ret = hid_hw_output_report(hdev, dmabuf, GO_PACKET_SIZE);
- if (ret < 0)
- return ret;
-
- ret = ret == GO_PACKET_SIZE ? 0 : -EINVAL;
+ hid_go_cmd_arm(id, command, index, device);
+ ret = hid_go_send_output_report(hdev, dmabuf);
if (ret)
- return ret;
-
- ret = wait_for_completion_interruptible_timeout(&drvdata.send_cmd_complete,
- msecs_to_jiffies(timeout));
-
- if (ret == 0) /* timeout occurred */
- ret = -EBUSY;
+ return hid_go_cmd_cancel(ret);
- reinit_completion(&drvdata.send_cmd_complete);
- return 0;
+ wait_result = wait_for_completion_interruptible_timeout(&drvdata.cmd.done,
+ timeout);
+ return hid_go_cmd_finish(wait_result);
}
static ssize_t version_show(struct device *dev, struct device_attribute *attr,
@@ -2362,9 +2426,9 @@ static int hid_go_cfg_probe(struct hid_device *hdev,
if (!buf)
return -ENOMEM;
+ mutex_init(&drvdata.cfg_mutex);
hid_set_drvdata(hdev, &drvdata);
drvdata.hdev = hdev;
- mutex_init(&drvdata.cfg_mutex);
ret = sysfs_create_groups(&hdev->dev.kobj, top_level_attr_groups);
if (ret) {
@@ -2435,8 +2499,11 @@ static int hid_go_probe(struct hid_device *hdev, const struct hid_device_id *id)
}
ep = get_endpoint_address(hdev);
- if (ep == GO_GP_INTF_IN)
- init_completion(&drvdata.send_cmd_complete);
+ if (ep == GO_GP_INTF_IN) {
+ init_completion(&drvdata.cmd.done);
+ spin_lock_init(&drvdata.cmd.lock);
+ drvdata.cmd.pending = false;
+ }
ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
if (ret) {
--
2.55.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH v3 5/7] HID: hid-lenovo-go: normalize calibration failure status
2026-09-01 6:36 [PATCH v3 0/7] HID: hid-lenovo-go: fix controller configuration Aditya Dash
` (3 preceding siblings ...)
2026-09-01 6:36 ` [PATCH v3 4/7] HID: hid-lenovo-go: return configuration request errors Aditya Dash
@ 2026-09-01 6:36 ` Aditya Dash
2026-09-01 15:57 ` Derek John Clark
2026-09-01 6:36 ` [PATCH v3 6/7] HID: hid-lenovo-go: reject unknown calibration action Aditya Dash
2026-09-01 6:36 ` [PATCH v3 7/7] HID: hid-lenovo-go: clear calibration status on start Aditya Dash
6 siblings, 1 reply; 12+ messages in thread
From: Aditya Dash @ 2026-09-01 6:36 UTC (permalink / raw)
To: Benjamin Tissoires, Derek J. Clark, Jiri Kosina, Mark Pearson
Cc: linux-input, linux-kernel
The driver stores the firmware result byte as an index into the
calibration status text. A Legion Go 1 returned 0x08 after an idle Stop.
The status table has only three entries, so a later read returns -EINVAL.
Keep the defined values 0x00 through 0x02. Treat larger result values as
failure before storing them.
Fixes: 995887a10da1 ("HID: hid-lenovo-go: Add Calibration Settings")
Assisted-by: Pi:gpt-5.6-sol
Signed-off-by: Aditya Dash <mradityadash@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index deed7acd2dc4..59b457683357 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -648,17 +648,22 @@ static int hid_go_light_event(struct command_report *cmd_rep)
static int hid_go_device_status_event(struct command_report *cmd_rep)
{
+ u8 status = cmd_rep->data[1];
+
+ if (status > CAL_STAT_FAILURE)
+ status = CAL_STAT_FAILURE;
+
switch (cmd_rep->device_type) {
case LEFT_CONTROLLER:
switch (cmd_rep->data[0]) {
case CALDEV_GYROSCOPE:
- drvdata.gp_left_gyro_cal_status = cmd_rep->data[1];
+ drvdata.gp_left_gyro_cal_status = status;
return 0;
case CALDEV_JOYSTICK:
- drvdata.gp_left_joy_cal_status = cmd_rep->data[1];
+ drvdata.gp_left_joy_cal_status = status;
return 0;
case CALDEV_TRIGGER:
- drvdata.gp_left_trigg_cal_status = cmd_rep->data[1];
+ drvdata.gp_left_trigg_cal_status = status;
return 0;
default:
return -EINVAL;
@@ -667,13 +672,13 @@ static int hid_go_device_status_event(struct command_report *cmd_rep)
case RIGHT_CONTROLLER:
switch (cmd_rep->data[0]) {
case CALDEV_GYROSCOPE:
- drvdata.gp_right_gyro_cal_status = cmd_rep->data[1];
+ drvdata.gp_right_gyro_cal_status = status;
return 0;
case CALDEV_JOYSTICK:
- drvdata.gp_right_joy_cal_status = cmd_rep->data[1];
+ drvdata.gp_right_joy_cal_status = status;
return 0;
case CALDEV_TRIGGER:
- drvdata.gp_right_trigg_cal_status = cmd_rep->data[1];
+ drvdata.gp_right_trigg_cal_status = status;
return 0;
default:
return -EINVAL;
--
2.55.0
^ permalink raw reply [flat|nested] 12+ messages in thread* Re: [PATCH v3 5/7] HID: hid-lenovo-go: normalize calibration failure status
2026-09-01 6:36 ` [PATCH v3 5/7] HID: hid-lenovo-go: normalize calibration failure status Aditya Dash
@ 2026-09-01 15:57 ` Derek John Clark
2026-09-01 17:06 ` Aditya
0 siblings, 1 reply; 12+ messages in thread
From: Derek John Clark @ 2026-09-01 15:57 UTC (permalink / raw)
To: Aditya Dash
Cc: Benjamin Tissoires, Jiri Kosina, Mark Pearson, linux-input, linux-kernel
On Mon, Aug 31, 2026 at 11:36 PM Aditya Dash <mradityadash@gmail.com> wrote:
>
> The driver stores the firmware result byte as an index into the
> calibration status text. A Legion Go 1 returned 0x08 after an idle Stop.
> The status table has only three entries, so a later read returns -EINVAL.
>
> Keep the defined values 0x00 through 0x02. Treat larger result values as
> failure before storing them.
Hi Aditya,
The 0x08 thing seemed odd to me and I meant to dig into the docs sooner.
I think the real reason for the bug you were seeing is a mixup of what data
bytet means what. There is an off by 1 error with using data[1] since
device_type is really the 0 byte for the information payload. data[1] is the
reason code for the failure.
From the docs:
Byte 0: Device Type:
0x01: Gyroscope
0x02: Joystick
0x03: Trigger
0x04: Joystick Trigger (Currently Unused)
Byte 1: Calibration Status:
0x01: Calibration Successful
0x02: Calibration Failed
Byte 2-3: Failure Reason:
Gyro:
0x0001 - Not stationary, timeout
0x0002 - Connection status changed
Joystick:
0x0100 - Joystick not fully deflected
0x0200 - Joystick not centered
0x0400 - Joystick not rotated two turns
0x0800 - Connection status changed
Trigger:
0x0001 - Trigger not fully depressed
0x0002 - Trigger not returned to starting position
0x0004 - Trigger not pressed twice
0x0008 - Connection status changed
Since we're matching on cmd_rep->device_type(byte 0 corollary) I think
switching to data[0] is sufficient to resolve the bug. If my hunch is
correct a trigger and gyro calibration will always succeed in the
driver as it stands. I won't be able to validate this myself for a
couple of hours on actual hardware, but I'll respond after I do.
Thanks,
Derek
> Fixes: 995887a10da1 ("HID: hid-lenovo-go: Add Calibration Settings")
> Assisted-by: Pi:gpt-5.6-sol
> Signed-off-by: Aditya Dash <mradityadash@gmail.com>
> ---
> drivers/hid/hid-lenovo-go.c | 17 +++++++++++------
> 1 file changed, 11 insertions(+), 6 deletions(-)
>
> diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
> index deed7acd2dc4..59b457683357 100644
> --- a/drivers/hid/hid-lenovo-go.c
> +++ b/drivers/hid/hid-lenovo-go.c
> @@ -648,17 +648,22 @@ static int hid_go_light_event(struct command_report *cmd_rep)
>
> static int hid_go_device_status_event(struct command_report *cmd_rep)
> {
> + u8 status = cmd_rep->data[1];
> +
> + if (status > CAL_STAT_FAILURE)
> + status = CAL_STAT_FAILURE;
> +
> switch (cmd_rep->device_type) {
> case LEFT_CONTROLLER:
> switch (cmd_rep->data[0]) {
> case CALDEV_GYROSCOPE:
> - drvdata.gp_left_gyro_cal_status = cmd_rep->data[1];
> + drvdata.gp_left_gyro_cal_status = status;
> return 0;
> case CALDEV_JOYSTICK:
> - drvdata.gp_left_joy_cal_status = cmd_rep->data[1];
> + drvdata.gp_left_joy_cal_status = status;
> return 0;
> case CALDEV_TRIGGER:
> - drvdata.gp_left_trigg_cal_status = cmd_rep->data[1];
> + drvdata.gp_left_trigg_cal_status = status;
> return 0;
> default:
> return -EINVAL;
> @@ -667,13 +672,13 @@ static int hid_go_device_status_event(struct command_report *cmd_rep)
> case RIGHT_CONTROLLER:
> switch (cmd_rep->data[0]) {
> case CALDEV_GYROSCOPE:
> - drvdata.gp_right_gyro_cal_status = cmd_rep->data[1];
> + drvdata.gp_right_gyro_cal_status = status;
> return 0;
> case CALDEV_JOYSTICK:
> - drvdata.gp_right_joy_cal_status = cmd_rep->data[1];
> + drvdata.gp_right_joy_cal_status = status;
> return 0;
> case CALDEV_TRIGGER:
> - drvdata.gp_right_trigg_cal_status = cmd_rep->data[1];
> + drvdata.gp_right_trigg_cal_status = status;
> return 0;
> default:
> return -EINVAL;
> --
> 2.55.0
>
^ permalink raw reply [flat|nested] 12+ messages in thread* Re: [PATCH v3 5/7] HID: hid-lenovo-go: normalize calibration failure status
2026-09-01 15:57 ` Derek John Clark
@ 2026-09-01 17:06 ` Aditya
2026-09-02 12:40 ` Derek J. Clark
0 siblings, 1 reply; 12+ messages in thread
From: Aditya @ 2026-09-01 17:06 UTC (permalink / raw)
To: Derek John Clark
Cc: Benjamin Tissoires, Jiri Kosina, Mark Pearson, linux-input, linux-kernel
Hi Derek,
Just to clarify, this behaviour was seen when sending a left
calibration joystick stop command when no calibration was
being performed.
Sending a respective start command returns after 10 seconds
with the error codes you have provided.
Here are the exact requests and responses. Consider ellipsis
to mean that the rest of it is zeros:
Left joystick calibration start:
Request:
05 00 0c 04 03 01 ...
Response (after 10s, no movement):
04 00 a0 02 03 02 02 07 ...
Left joystick calibration stop (when no calibration is running):
Request:
05 00 0c 04 03 02 ...
Response (this is near immediate):
04 00 a0 02 03 02 08 00 ...
Thanks,
Aditya
^ permalink raw reply [flat|nested] 12+ messages in thread* Re: [PATCH v3 5/7] HID: hid-lenovo-go: normalize calibration failure status
2026-09-01 17:06 ` Aditya
@ 2026-09-02 12:40 ` Derek J. Clark
2026-09-05 6:10 ` Aditya Dash
0 siblings, 1 reply; 12+ messages in thread
From: Derek J. Clark @ 2026-09-02 12:40 UTC (permalink / raw)
To: Aditya
Cc: Benjamin Tissoires, Jiri Kosina, Mark Pearson, linux-input, linux-kernel
On September 1, 2026 10:06:18 AM PDT, Aditya <mradityadash@gmail.com> wrote:
>Hi Derek,
>
>Just to clarify, this behaviour was seen when sending a left
>calibration joystick stop command when no calibration was
>being performed.
>
>Sending a respective start command returns after 10 seconds
>with the error codes you have provided.
>
>Here are the exact requests and responses. Consider ellipsis
>to mean that the rest of it is zeros:
>
>Left joystick calibration start:
>Request:
> 05 00 0c 04 03 01 ...
>Response (after 10s, no movement):
> 04 00 a0 02 03 02 02 07 ...
This makes it look like the reason code is a bitmask rather than a specific value. Probably more difficult to present in sysfs than the value it provides.
> Left joystick calibration stop (when no calibration is running):
>Request:
> 05 00 0c 04 03 02 ...
>Response (this is near immediate):
> 04 00 a0 02 03 02 08 00 ...
Interesting. This wouldn't be the first time documentation was incomplete or inaccurate from Lenovo so I'm not super surprised. We can then consider a 0x08 response on the status byte as stopped with no call running, vice failed, if you think that information is relevant to the end user. I think it might be. If you stop it with a cal running is there a meaningful difference in the response?
Thanks,
Derek
>Thanks,
>Aditya
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v3 5/7] HID: hid-lenovo-go: normalize calibration failure status
2026-09-02 12:40 ` Derek J. Clark
@ 2026-09-05 6:10 ` Aditya Dash
0 siblings, 0 replies; 12+ messages in thread
From: Aditya Dash @ 2026-09-05 6:10 UTC (permalink / raw)
To: Derek J . Clark
Cc: Benjamin Tissoires, Jiri Kosina, Mark Pearson, linux-input, linux-kernel
Hi Derek,
The Stop reply changes based on calibration state. Its status appears to be
the current failure_mask + 1. However, this mask does not always match the
failure mask returned when calibration times out.
Here are some requests and responses from the experiments done:
Joystick calibration:
Left Start:
05 00 0c 04 03 01 ...
Left Stop:
05 00 0c 04 03 02 ...
No movement:
Without Stop:
04 00 a0 02 03 02 02 07 00 ...
With Stop:
04 00 a0 02 03 02 06 00 00 ...
The timeout error contains deflection 0x01, center 0x02, and turns 0x04.
Stop does not set the center bit, giving mask 0x05 and status 0x06.
One full outer turn, returned to center:
Without Stop:
04 00 a0 02 03 02 02 06 00 ...
With Stop:
04 00 a0 02 03 02 05 00 00 ...
The timeout error contains center 0x02 and turns 0x04. Stop does not set the
center bit, giving mask 0x04 and status 0x05.
One full outer turn, then held away from center (right controller):
Without Stop:
04 00 a0 02 04 02 02 06 00 ...
With Stop:
04 00 a0 02 04 02 07 00 00 ...
Two full outer turns, then held away from center:
Without Stop:
04 00 a0 02 03 02 02 02 00 ...
With Stop:
04 00 a0 02 03 02 03 00 00 ...
Stop with no calibration running:
04 00 a0 02 03 02 08 00 00 ...
Trigger calibration:
Start:
05 00 0a 04 03 01 ...
Stop:
05 00 0a 04 03 02 ...
One full press and release:
Without Stop:
04 00 a0 02 03 03 02 00 06 ...
With Stop:
04 00 a0 02 03 03 05 00 00 ...
The nonzero timeout reason byte is 0x06: return 0x02 and press count 0x04.
Stop does not set the return bit, giving mask 0x04 and status 0x05.
Gyroscope calibration:
Start:
05 00 0e 06 03 01 ...
Stop:
05 00 0e 06 03 02 ...
Continuous movement:
Without Stop:
04 00 a0 02 03 01 02 01 00 ...
With Stop:
04 00 a0 02 03 01 02 02 00 ...
The reason changes from 0x0001 to 0x0002 although no connection change was
made. The cause of this discrepancy is not known.
Successful calibration responses were as expected. The corresponding cases
produced the same outcomes on the other controller where repeated.
Connection-change cases are not yet tested.
Thanks,
Aditya
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v3 6/7] HID: hid-lenovo-go: reject unknown calibration action
2026-09-01 6:36 [PATCH v3 0/7] HID: hid-lenovo-go: fix controller configuration Aditya Dash
` (4 preceding siblings ...)
2026-09-01 6:36 ` [PATCH v3 5/7] HID: hid-lenovo-go: normalize calibration failure status Aditya Dash
@ 2026-09-01 6:36 ` Aditya Dash
2026-09-01 6:36 ` [PATCH v3 7/7] HID: hid-lenovo-go: clear calibration status on start Aditya Dash
6 siblings, 0 replies; 12+ messages in thread
From: Aditya Dash @ 2026-09-01 6:36 UTC (permalink / raw)
To: Benjamin Tissoires, Derek J. Clark, Jiri Kosina, Mark Pearson
Cc: linux-input, linux-kernel
The calibration action table includes 'unknown' at index zero. The options
attribute skips that entry and lists only 'start' and 'stop', but the store
searches the full table. Writing 'unknown' sends a request without an
action byte.
Reject CAL_UNKNOWN before building the request. Start and Stop both use a
one-byte action, so pass that size directly.
Fixes: 995887a10da1 ("HID: hid-lenovo-go: Add Calibration Settings")
Assisted-by: Pi:gpt-5.6-sol
Signed-off-by: Aditya Dash <mradityadash@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 59b457683357..a6dec5490d17 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -1412,20 +1412,18 @@ static ssize_t calibrate_config_store(struct device *dev,
const char *buf, u8 cmd, u8 sub_cmd,
size_t count, enum dev_type device_type)
{
- size_t size = 1;
- u8 val = 0;
+ u8 val;
int ret;
ret = sysfs_match_string(cal_enabled_text, buf);
if (ret < 0)
return ret;
+ if (ret == CAL_UNKNOWN)
+ return -EINVAL;
val = ret;
- if (!val)
- size = 0;
-
ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, cmd, sub_cmd,
- device_type, &val, size);
+ device_type, &val, sizeof(val));
if (ret < 0)
return ret;
--
2.55.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH v3 7/7] HID: hid-lenovo-go: clear calibration status on start
2026-09-01 6:36 [PATCH v3 0/7] HID: hid-lenovo-go: fix controller configuration Aditya Dash
` (5 preceding siblings ...)
2026-09-01 6:36 ` [PATCH v3 6/7] HID: hid-lenovo-go: reject unknown calibration action Aditya Dash
@ 2026-09-01 6:36 ` Aditya Dash
6 siblings, 0 replies; 12+ messages in thread
From: Aditya Dash @ 2026-09-01 6:36 UTC (permalink / raw)
To: Benjamin Tissoires, Derek J. Clark, Jiri Kosina, Mark Pearson
Cc: linux-input, linux-kernel
A completed calibration leaves success or failure in its status file. If
the next calibration has the same result, userspace sees no change and
cannot tell when the new attempt starts or finishes.
Link each calibration action attribute to its status field. Set only that
field to unknown before submitting Start. Stop leaves the status unchanged,
and the next firmware result replaces unknown with success or failure.
Fixes: 995887a10da1 ("HID: hid-lenovo-go: Add Calibration Settings")
Assisted-by: Pi:gpt-5.6-sol
Signed-off-by: Aditya Dash <mradityadash@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 38 +++++++++++++++++++++++++++++--------
1 file changed, 30 insertions(+), 8 deletions(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index a6dec5490d17..749536e19d2d 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -111,6 +111,11 @@ struct go_cfg_attr {
u8 index;
};
+struct go_cal_cfg_attr {
+ u8 index;
+ u8 *status;
+};
+
struct command_report {
u8 report_id;
u8 id;
@@ -1410,7 +1415,8 @@ static ssize_t device_status_show(struct device *dev,
static ssize_t calibrate_config_store(struct device *dev,
struct device_attribute *attr,
const char *buf, u8 cmd, u8 sub_cmd,
- size_t count, enum dev_type device_type)
+ size_t count, enum dev_type device_type,
+ u8 *cal_status)
{
u8 val;
int ret;
@@ -1422,6 +1428,9 @@ static ssize_t calibrate_config_store(struct device *dev,
return -EINVAL;
val = ret;
+ if (val == CAL_START)
+ *cal_status = CAL_STAT_UNKNOWN;
+
ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, cmd, sub_cmd,
device_type, &val, sizeof(val));
if (ret < 0)
@@ -1818,7 +1827,8 @@ static void hid_go_brightness_set(struct led_classdev *led_cdev,
const char *buf, size_t count) \
{ \
return calibrate_config_store(dev, attr, buf, _name.index, \
- _scmd, count, _dtype); \
+ _scmd, count, _dtype, \
+ _name.status); \
} \
static ssize_t _name##_##_rtype##_show( \
struct device *dev, struct device_attribute *attr, char *buf) \
@@ -1978,17 +1988,23 @@ LEGO_DEVICE_ATTR_RW(rumble_notification_left, "rumble_notification",
static DEVICE_ATTR_RO_NAMED(rumble_notification_left_index,
"rumble_notification_index");
-static struct go_cfg_attr cal_trigg_left = { SET_TRIGGER_CFG };
+static struct go_cal_cfg_attr cal_trigg_left = {
+ SET_TRIGGER_CFG, &drvdata.gp_left_trigg_cal_status
+};
LEGO_CAL_DEVICE_ATTR(cal_trigg_left, "calibrate_trigger", TRIGGER_CALIBRATE,
LEFT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_trigg_left_index, "calibrate_trigger_index");
-static struct go_cfg_attr cal_joy_left = { SET_JOYSTICK_CFG };
+static struct go_cal_cfg_attr cal_joy_left = {
+ SET_JOYSTICK_CFG, &drvdata.gp_left_joy_cal_status
+};
LEGO_CAL_DEVICE_ATTR(cal_joy_left, "calibrate_joystick", JOYSTICK_CALIBRATE,
LEFT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_joy_left_index, "calibrate_joystick_index");
-static struct go_cfg_attr cal_gyro_left = { SET_GYRO_CFG };
+static struct go_cal_cfg_attr cal_gyro_left = {
+ SET_GYRO_CFG, &drvdata.gp_left_gyro_cal_status
+};
LEGO_CAL_DEVICE_ATTR(cal_gyro_left, "calibrate_gyro", GYRO_CALIBRATE,
LEFT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_gyro_left_index, "calibrate_gyro_index");
@@ -2085,17 +2101,23 @@ LEGO_DEVICE_ATTR_RW(rumble_notification_right, "rumble_notification",
static DEVICE_ATTR_RO_NAMED(rumble_notification_right_index,
"rumble_notification_index");
-static struct go_cfg_attr cal_trigg_right = { SET_TRIGGER_CFG };
+static struct go_cal_cfg_attr cal_trigg_right = {
+ SET_TRIGGER_CFG, &drvdata.gp_right_trigg_cal_status
+};
LEGO_CAL_DEVICE_ATTR(cal_trigg_right, "calibrate_trigger", TRIGGER_CALIBRATE,
RIGHT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_trigg_right_index, "calibrate_trigger_index");
-static struct go_cfg_attr cal_joy_right = { SET_JOYSTICK_CFG };
+static struct go_cal_cfg_attr cal_joy_right = {
+ SET_JOYSTICK_CFG, &drvdata.gp_right_joy_cal_status
+};
LEGO_CAL_DEVICE_ATTR(cal_joy_right, "calibrate_joystick", JOYSTICK_CALIBRATE,
RIGHT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_joy_right_index, "calibrate_joystick_index");
-static struct go_cfg_attr cal_gyro_right = { SET_GYRO_CFG };
+static struct go_cal_cfg_attr cal_gyro_right = {
+ SET_GYRO_CFG, &drvdata.gp_right_gyro_cal_status
+};
LEGO_CAL_DEVICE_ATTR(cal_gyro_right, "calibrate_gyro", GYRO_CALIBRATE,
RIGHT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_gyro_right_index, "calibrate_gyro_index");
--
2.55.0
^ permalink raw reply [flat|nested] 12+ messages in thread