From 20842126c2d935b4bd808c11d77019b9daa31714 Mon Sep 17 00:00:00 2001 From: Matthew Schwartz Date: Sat, 20 Dec 2025 19:57:41 -0800 Subject: [PATCH 01/73] [FROM-ML] mmc: rtsx_pci_sdmmc: drop MMC_CAP_AGGRESSIVE_PM for RTS525A Using MMC_CAP_AGGRESSIVE_PM on RTS525A card readers causes game performance issues when the card reader comes back from idle into active use. This can be observed in Hades II when loading new sections of the game or menu after the card reader puts itself into idle, and presents as a 1-2 second hang. Disabling this capability for RTS525A eliminates the performance issues. Signed-off-by: Matthew Schwartz --- drivers/mmc/host/rtsx_pci_sdmmc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/mmc/host/rtsx_pci_sdmmc.c b/drivers/mmc/host/rtsx_pci_sdmmc.c index 8dfbc62f165bc..ab7dbc7c7a185 100644 --- a/drivers/mmc/host/rtsx_pci_sdmmc.c +++ b/drivers/mmc/host/rtsx_pci_sdmmc.c @@ -1421,7 +1421,7 @@ static void realtek_init_host(struct realtek_pci_sdmmc *host) mmc->caps = MMC_CAP_4_BIT_DATA | MMC_CAP_SD_HIGHSPEED | MMC_CAP_MMC_HIGHSPEED | MMC_CAP_BUS_WIDTH_TEST | MMC_CAP_UHS_SDR12 | MMC_CAP_UHS_SDR25; - if (pcr->rtd3_en) + if (pcr->rtd3_en && PCI_PID(pcr) != PID_525A) mmc->caps = mmc->caps | MMC_CAP_AGGRESSIVE_PM; mmc->caps2 = MMC_CAP2_NO_PRESCAN_POWERUP | MMC_CAP2_FULL_PWR_CYCLE | MMC_CAP2_NO_SDIO; From aeebc0741da6961773ea75f4acc589a5779a070b Mon Sep 17 00:00:00 2001 From: Matthew Schwartz Date: Sat, 20 Dec 2025 19:59:53 -0800 Subject: [PATCH 02/73] [FROM-ML] misc: rtsx_pcr: prevent pm_schedule_suspend for RTS525A The chip's pm_schedule_suspend sequence can sometimes cause SD card reinitialization failures with "__mmc_poll_for_busy" after reaching the end of its operations. As a workaround, skip pm_schedule_suspend for RTS525A card readers. Signed-off-by: Matthew Schwartz --- drivers/misc/cardreader/rtsx_pcr.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/misc/cardreader/rtsx_pcr.c b/drivers/misc/cardreader/rtsx_pcr.c index 2b6a994c6fe1f..f718e4b0ee4e5 100644 --- a/drivers/misc/cardreader/rtsx_pcr.c +++ b/drivers/misc/cardreader/rtsx_pcr.c @@ -1760,7 +1760,7 @@ static int rtsx_pci_runtime_idle(struct device *device) mutex_unlock(&pcr->pcr_mutex); - if (pcr->rtd3_en) + if (pcr->rtd3_en && PCI_PID(pcr) != PID_525A) pm_schedule_suspend(device, 10000); return -EBUSY; From 467fe0bc1d8bf649bfdfdf0365c19833c6e909cb Mon Sep 17 00:00:00 2001 From: Matthew Schwartz Date: Sat, 20 Dec 2025 19:49:24 -0800 Subject: [PATCH 03/73] [FROM-ML] mmc: rtsx_pci_sdmmc: increase delay after power on for SD cards An mdelay of 1 leads to unreliable SD card initialization on some cards, such as SanDisk Extreme PRO UHS-I microSD models, where they will be in a "card stuck busy" state after power on. Increasing the delay to 10 ms improves reliability from a 20% failure rate to 0% on two devices with RTS525A card readers after testing 30 s2idle/resume cycles. Signed-off-by: Matthew Schwartz --- drivers/mmc/host/rtsx_pci_sdmmc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/mmc/host/rtsx_pci_sdmmc.c b/drivers/mmc/host/rtsx_pci_sdmmc.c index ab7dbc7c7a185..dd3c0f2cad387 100644 --- a/drivers/mmc/host/rtsx_pci_sdmmc.c +++ b/drivers/mmc/host/rtsx_pci_sdmmc.c @@ -937,7 +937,7 @@ static int sd_power_on(struct realtek_pci_sdmmc *host, unsigned char power_mode) if (err < 0) return err; - mdelay(1); + mdelay(10); err = rtsx_pci_write_register(pcr, CARD_OE, SD_OUTPUT_EN, SD_OUTPUT_EN); if (err < 0) From 0cee22c4697d345f53efee97f90e9c2361577480 Mon Sep 17 00:00:00 2001 From: Natalie Vock Date: Sun, 31 Aug 2025 17:17:20 +0200 Subject: [PATCH 04/73] [FROM-ML] cgroup/dmem: Add queries for protection values Callers can use this feedback to be more aggressive in making space for allocations of a cgroup if they know it is protected. These are counterparts to memcg's mem_cgroup_below_{min,low}. Signed-off-by: Natalie Vock --- include/linux/cgroup_dmem.h | 16 ++++++++++ kernel/cgroup/dmem.c | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/include/linux/cgroup_dmem.h b/include/linux/cgroup_dmem.h index dd4869f1d736e..1a88cd0c9eb00 100644 --- a/include/linux/cgroup_dmem.h +++ b/include/linux/cgroup_dmem.h @@ -24,6 +24,10 @@ void dmem_cgroup_uncharge(struct dmem_cgroup_pool_state *pool, u64 size); bool dmem_cgroup_state_evict_valuable(struct dmem_cgroup_pool_state *limit_pool, struct dmem_cgroup_pool_state *test_pool, bool ignore_low, bool *ret_hit_low); +bool dmem_cgroup_below_min(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test); +bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test); void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool); #else @@ -59,6 +63,18 @@ bool dmem_cgroup_state_evict_valuable(struct dmem_cgroup_pool_state *limit_pool, return true; } +static inline bool dmem_cgroup_below_min(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test) +{ + return false; +} + +static inline bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test) +{ + return false; +} + static inline void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool) { } diff --git a/kernel/cgroup/dmem.c b/kernel/cgroup/dmem.c index 4753a67d0f0f2..be8499a5b04ab 100644 --- a/kernel/cgroup/dmem.c +++ b/kernel/cgroup/dmem.c @@ -695,6 +695,68 @@ int dmem_cgroup_try_charge(struct dmem_cgroup_region *region, u64 size, } EXPORT_SYMBOL_GPL(dmem_cgroup_try_charge); +/** + * dmem_cgroup_below_min() - Tests whether current usage is within min limit. + * + * @root: Root of the subtree to calculate protection for, or NULL to calculate global protection. + * @test: The pool to test the usage/min limit of. + * + * Return: true if usage is below min and the cgroup is protected, false otherwise. + */ +bool dmem_cgroup_below_min(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test) +{ + if (root == test || !pool_parent(test)) + return false; + + if (!root) { + for (root = test; pool_parent(root); root = pool_parent(root)) + {} + } + + /* + * In mem_cgroup_below_min(), the memcg pendant, this call is missing. + * mem_cgroup_below_min() gets called during traversal of the cgroup tree, where + * protection is already calculated as part of the traversal. dmem cgroup eviction + * does not traverse the cgroup tree, so we need to recalculate effective protection + * here. + */ + dmem_cgroup_calculate_protection(root, test); + return page_counter_read(&test->cnt) <= READ_ONCE(test->cnt.emin); +} +EXPORT_SYMBOL_GPL(dmem_cgroup_below_min); + +/** + * dmem_cgroup_below_low() - Tests whether current usage is within low limit. + * + * @root: Root of the subtree to calculate protection for, or NULL to calculate global protection. + * @test: The pool to test the usage/low limit of. + * + * Return: true if usage is below low and the cgroup is protected, false otherwise. + */ +bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test) +{ + if (root == test || !pool_parent(test)) + return false; + + if (!root) { + for (root = test; pool_parent(root); root = pool_parent(root)) + {} + } + + /* + * In mem_cgroup_below_low(), the memcg pendant, this call is missing. + * mem_cgroup_below_low() gets called during traversal of the cgroup tree, where + * protection is already calculated as part of the traversal. dmem cgroup eviction + * does not traverse the cgroup tree, so we need to recalculate effective protection + * here. + */ + dmem_cgroup_calculate_protection(root, test); + return page_counter_read(&test->cnt) <= READ_ONCE(test->cnt.elow); +} +EXPORT_SYMBOL_GPL(dmem_cgroup_below_low); + static int dmem_cgroup_region_capacity_show(struct seq_file *sf, void *v) { struct dmem_cgroup_region *region; From 97e1b67e02ad4e91762cfc028def63210262bdc9 Mon Sep 17 00:00:00 2001 From: Natalie Vock Date: Sun, 14 Sep 2025 18:35:45 +0200 Subject: [PATCH 05/73] [FROM-ML] cgroup,cgroup/dmem: Add (dmem_)cgroup_common_ancestor helper This helps to find a common subtree of two resources, which is important when determining whether it's helpful to evict one resource in favor of another. To facilitate this, add a common helper to find the ancestor of two cgroups using each cgroup's ancestor array. Signed-off-by: Natalie Vock --- include/linux/cgroup.h | 21 +++++++++++++++++++++ include/linux/cgroup_dmem.h | 9 +++++++++ kernel/cgroup/dmem.c | 29 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/include/linux/cgroup.h b/include/linux/cgroup.h index c5648fcf74e2e..c0b7c857ae126 100644 --- a/include/linux/cgroup.h +++ b/include/linux/cgroup.h @@ -623,6 +623,27 @@ static inline struct cgroup *cgroup_ancestor(struct cgroup *cgrp, return cgrp->ancestors[ancestor_level]; } +/** + * cgroup_common_ancestor - find common ancestor of two cgroups + * @a: first cgroup to find common ancestor of + * @b: second cgroup to find common ancestor of + * + * Find the first cgroup that is an ancestor of both @a and @b, if it exists + * and return a pointer to it. If such a cgroup doesn't exist, return NULL. + * + * This function is safe to call as long as both @a and @b are accessible. + */ +static inline struct cgroup *cgroup_common_ancestor(struct cgroup *a, + struct cgroup *b) +{ + int level; + + for (level = min(a->level, b->level); level >= 0; level--) + if (a->ancestors[level] == b->ancestors[level]) + return a->ancestors[level]; + return NULL; +} + /** * task_under_cgroup_hierarchy - test task's membership of cgroup ancestry * @task: the task to be tested diff --git a/include/linux/cgroup_dmem.h b/include/linux/cgroup_dmem.h index 1a88cd0c9eb00..9d72457c4cb9d 100644 --- a/include/linux/cgroup_dmem.h +++ b/include/linux/cgroup_dmem.h @@ -28,6 +28,8 @@ bool dmem_cgroup_below_min(struct dmem_cgroup_pool_state *root, struct dmem_cgroup_pool_state *test); bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, struct dmem_cgroup_pool_state *test); +struct dmem_cgroup_pool_state *dmem_cgroup_get_common_ancestor(struct dmem_cgroup_pool_state *a, + struct dmem_cgroup_pool_state *b); void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool); #else @@ -75,6 +77,13 @@ static inline bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, return false; } +static inline +struct dmem_cgroup_pool_state *dmem_cgroup_get_common_ancestor(struct dmem_cgroup_pool_state *a, + struct dmem_cgroup_pool_state *b) +{ + return NULL; +} + static inline void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool) { } diff --git a/kernel/cgroup/dmem.c b/kernel/cgroup/dmem.c index be8499a5b04ab..7d22536be6d3b 100644 --- a/kernel/cgroup/dmem.c +++ b/kernel/cgroup/dmem.c @@ -757,6 +757,35 @@ bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, } EXPORT_SYMBOL_GPL(dmem_cgroup_below_low); +/** + * dmem_cgroup_get_common_ancestor(): Find the first common ancestor of two pools. + * @a: First pool to find the common ancestor of. + * @b: First pool to find the common ancestor of. + * + * Return: The first pool that is a parent of both @a and @b, or NULL if either @a or @b are NULL, + * or if such a pool does not exist. A reference to the returned pool is grabbed and must be + * released by the caller when it is done using the pool. + */ +struct dmem_cgroup_pool_state *dmem_cgroup_get_common_ancestor(struct dmem_cgroup_pool_state *a, + struct dmem_cgroup_pool_state *b) +{ + struct cgroup *ancestor_cgroup; + struct cgroup_subsys_state *ancestor_css; + + if (!a || !b) + return NULL; + + ancestor_cgroup = cgroup_common_ancestor(a->cs->css.cgroup, b->cs->css.cgroup); + if (!ancestor_cgroup) + return NULL; + + ancestor_css = cgroup_e_css(ancestor_cgroup, &dmem_cgrp_subsys); + css_get(ancestor_css); + + return get_cg_pool_unlocked(css_to_dmemcs(ancestor_css), a->region); +} +EXPORT_SYMBOL_GPL(dmem_cgroup_get_common_ancestor); + static int dmem_cgroup_region_capacity_show(struct seq_file *sf, void *v) { struct dmem_cgroup_region *region; From de30da41f622e7e498fd8061c7eaac2a1e612ef5 Mon Sep 17 00:00:00 2001 From: Natalie Vock Date: Wed, 15 Oct 2025 14:43:26 +0200 Subject: [PATCH 06/73] [FROM-ML] drm/ttm: Extract code for attempting allocation in a place Move all code for attempting allocation for a specific place to ttm_bo_alloc_place. With subsequent patches, this logic is going to get more complicated, so it helps readability to have this separate. ttm_bo_alloc_at_place takes a pointer to a struct ttm_bo_alloc_state. This struct holds various state produced by the allocation (e.g. cgroup resource associated with the allocation) that the caller needs to keep track of (and potentially dispose of). This is just the limiting cgroup pool for now, but future patches will add more state needing to be tracked. ttm_bo_alloc_at_place also communicates via return codes if eviction using ttm_bo_evict_alloc should be attempted. This is preparation for attempting eviction in more cases than just force_space being set. No functional change intended. Signed-off-by: Natalie Vock Reviewed-by: Tvrtko Ursulin --- drivers/gpu/drm/ttm/ttm_bo.c | 103 +++++++++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 23 deletions(-) diff --git a/drivers/gpu/drm/ttm/ttm_bo.c b/drivers/gpu/drm/ttm/ttm_bo.c index bcd76f6bb7f02..69b3cff2ab171 100644 --- a/drivers/gpu/drm/ttm/ttm_bo.c +++ b/drivers/gpu/drm/ttm/ttm_bo.c @@ -488,6 +488,62 @@ int ttm_bo_evict_first(struct ttm_device *bdev, struct ttm_resource_manager *man return ret; } +struct ttm_bo_alloc_state { + /** @limit_pool: Which pool limit we should test against */ + struct dmem_cgroup_pool_state *limit_pool; +}; + +/** + * ttm_bo_alloc_at_place - Attempt allocating a BO's backing store in a place + * + * @bo: The buffer to allocate the backing store of + * @place: The place to attempt allocation in + * @ctx: ttm_operation_ctx associated with this allocation + * @force_space: If we should evict buffers to force space + * @res: On allocation success, the resulting struct ttm_resource. + * @alloc_state: Object holding allocation state such as charged cgroups. + * + * Returns: + * -EBUSY: No space available, but allocation should be retried with ttm_bo_evict_alloc. + * -ENOSPC: No space available, allocation should not be retried. + * -ERESTARTSYS: An interruptible sleep was interrupted by a signal. + * + */ +static int ttm_bo_alloc_at_place(struct ttm_buffer_object *bo, + const struct ttm_place *place, + struct ttm_operation_ctx *ctx, + bool force_space, + struct ttm_resource **res, + struct ttm_bo_alloc_state *alloc_state) +{ + bool may_evict; + int ret; + + may_evict = force_space && place->mem_type != TTM_PL_SYSTEM; + + ret = ttm_resource_alloc(bo, place, res, + force_space ? &alloc_state->limit_pool : NULL); + + if (ret) { + /* + * -EAGAIN means the charge failed, which we treat like an + * allocation failure. Therefore, return an error code indicating + * the allocation failed - either -EBUSY if the allocation should + * be retried with eviction, or -ENOSPC if there should be no second + * attempt. + */ + if (ret == -EAGAIN) + return may_evict ? -EBUSY : -ENOSPC; + + if (ret == -ENOSPC && may_evict) + return -EBUSY; + + return ret; + } + + return 0; +} + /** * struct ttm_bo_evict_walk - Parameters for the evict walk. */ @@ -503,12 +559,13 @@ struct ttm_bo_evict_walk { /** @evicted: Number of successful evictions. */ unsigned long evicted; - /** @limit_pool: Which pool limit we should test against */ - struct dmem_cgroup_pool_state *limit_pool; /** @try_low: Whether we should attempt to evict BO's with low watermark threshold */ bool try_low; /** @hit_low: If we cannot evict a bo when @try_low is false (first pass) */ bool hit_low; + + /** @alloc_state: State associated with the allocation attempt. */ + struct ttm_bo_alloc_state *alloc_state; }; static s64 ttm_bo_evict_cb(struct ttm_lru_walk *walk, struct ttm_buffer_object *bo) @@ -517,8 +574,9 @@ static s64 ttm_bo_evict_cb(struct ttm_lru_walk *walk, struct ttm_buffer_object * container_of(walk, typeof(*evict_walk), walk); s64 lret; - if (!dmem_cgroup_state_evict_valuable(evict_walk->limit_pool, bo->resource->css, - evict_walk->try_low, &evict_walk->hit_low)) + if (!dmem_cgroup_state_evict_valuable(evict_walk->alloc_state->limit_pool, + bo->resource->css, evict_walk->try_low, + &evict_walk->hit_low)) return 0; if (bo->pin_count || !bo->bdev->funcs->eviction_valuable(bo, evict_walk->place)) @@ -560,7 +618,7 @@ static int ttm_bo_evict_alloc(struct ttm_device *bdev, struct ttm_operation_ctx *ctx, struct ww_acquire_ctx *ticket, struct ttm_resource **res, - struct dmem_cgroup_pool_state *limit_pool) + struct ttm_bo_alloc_state *state) { struct ttm_bo_evict_walk evict_walk = { .walk = { @@ -573,7 +631,7 @@ static int ttm_bo_evict_alloc(struct ttm_device *bdev, .place = place, .evictor = evictor, .res = res, - .limit_pool = limit_pool, + .alloc_state = state, }; s64 lret; @@ -724,9 +782,8 @@ static int ttm_bo_alloc_resource(struct ttm_buffer_object *bo, for (i = 0; i < placement->num_placement; ++i) { const struct ttm_place *place = &placement->placement[i]; - struct dmem_cgroup_pool_state *limit_pool = NULL; + struct ttm_bo_alloc_state alloc_state = {}; struct ttm_resource_manager *man; - bool may_evict; man = ttm_manager_type(bdev, place->mem_type); if (!man || !ttm_resource_manager_used(man)) @@ -736,25 +793,25 @@ static int ttm_bo_alloc_resource(struct ttm_buffer_object *bo, TTM_PL_FLAG_FALLBACK)) continue; - may_evict = (force_space && place->mem_type != TTM_PL_SYSTEM); - ret = ttm_resource_alloc(bo, place, res, force_space ? &limit_pool : NULL); - if (ret) { - if (ret != -ENOSPC) { - dmem_cgroup_pool_state_put(limit_pool); - return ret; - } - if (!may_evict) { - dmem_cgroup_pool_state_put(limit_pool); - continue; - } - + ret = ttm_bo_alloc_at_place(bo, place, ctx, force_space, + res, &alloc_state); + + if (ret == -ENOSPC) { + dmem_cgroup_pool_state_put(alloc_state.limit_pool); + continue; + } else if (ret == -EBUSY) { ret = ttm_bo_evict_alloc(bdev, man, place, bo, ctx, - ticket, res, limit_pool); - dmem_cgroup_pool_state_put(limit_pool); + ticket, res, &alloc_state); + + dmem_cgroup_pool_state_put(alloc_state.limit_pool); + if (ret == -EBUSY) continue; - if (ret) + else if (ret) return ret; + } else if (ret) { + dmem_cgroup_pool_state_put(alloc_state.limit_pool); + return ret; } ret = ttm_bo_add_pipelined_eviction_fences(bo, man, ctx->no_wait_gpu); From ed22ce3ce7274265315cf9867d294cfeae0a1549 Mon Sep 17 00:00:00 2001 From: Natalie Vock Date: Wed, 25 Feb 2026 11:29:40 +0100 Subject: [PATCH 07/73] [FROM-ML] drm/ttm: Split cgroup charge and resource allocation Coupling resource allocation and cgroup charging is racy when charging succeeds, but subsequent resource allocation fails. Certain eviction decisions are made on the basis of whether the allocating cgroup is protected, i.e. within its min/low limits, but with the charge being tied to resource allocation (and uncharged when the resource allocation fails), this check is done at a point where the allocation is not actually charged to the cgroup. This is subtly wrong if the allocation were to cause the cgroup to exceed the min/low protection, but it's even more wrong if the same cgroup tries allocating multiple buffers concurrently: In this case, the min/low protection may pass for all allocation attempts when the real min/low protection covers only some, or potentially none of the allocated buffers. Instead, charge the allocation to the cgroup once and keep the charge for as long as we try to allocate a ttm_resource, and only undo the charge if allocating the resource is ultimately unsuccessful and we move on to a different ttm_place. Signed-off-by: Natalie Vock --- drivers/gpu/drm/ttm/ttm_bo.c | 66 ++++++++++++++++++++---------- drivers/gpu/drm/ttm/ttm_resource.c | 51 +++++++++++++++-------- include/drm/ttm/ttm_resource.h | 6 ++- 3 files changed, 85 insertions(+), 38 deletions(-) diff --git a/drivers/gpu/drm/ttm/ttm_bo.c b/drivers/gpu/drm/ttm/ttm_bo.c index 69b3cff2ab171..824dad2935ef4 100644 --- a/drivers/gpu/drm/ttm/ttm_bo.c +++ b/drivers/gpu/drm/ttm/ttm_bo.c @@ -489,8 +489,12 @@ int ttm_bo_evict_first(struct ttm_device *bdev, struct ttm_resource_manager *man } struct ttm_bo_alloc_state { + /** @charge_pool: The memory pool the resource is charged to */ + struct dmem_cgroup_pool_state *charge_pool; /** @limit_pool: Which pool limit we should test against */ struct dmem_cgroup_pool_state *limit_pool; + /** @in_evict: Whether we are currently evicting buffers */ + bool in_evict; }; /** @@ -519,28 +523,39 @@ static int ttm_bo_alloc_at_place(struct ttm_buffer_object *bo, bool may_evict; int ret; - may_evict = force_space && place->mem_type != TTM_PL_SYSTEM; - - ret = ttm_resource_alloc(bo, place, res, - force_space ? &alloc_state->limit_pool : NULL); + may_evict = !alloc_state->in_evict && force_space && + place->mem_type != TTM_PL_SYSTEM; + if (!alloc_state->charge_pool) { + ret = ttm_resource_try_charge(bo, place, &alloc_state->charge_pool, + force_space ? &alloc_state->limit_pool + : NULL); + if (ret) { + /* + * -EAGAIN means the charge failed, which we treat + * like an allocation failure. Therefore, return an + * error code indicating the allocation failed - + * either -EBUSY if the allocation should be + * retried with eviction, or -ENOSPC if there should + * be no second attempt. + */ + if (ret == -EAGAIN) + ret = may_evict ? -EBUSY : -ENOSPC; + return ret; + } + } + ret = ttm_resource_alloc(bo, place, res, alloc_state->charge_pool); if (ret) { - /* - * -EAGAIN means the charge failed, which we treat like an - * allocation failure. Therefore, return an error code indicating - * the allocation failed - either -EBUSY if the allocation should - * be retried with eviction, or -ENOSPC if there should be no second - * attempt. - */ - if (ret == -EAGAIN) - return may_evict ? -EBUSY : -ENOSPC; - if (ret == -ENOSPC && may_evict) - return -EBUSY; - + ret = -EBUSY; return ret; } + /* + * Ownership of charge_pool has been transferred to the TTM resource, + * don't make the caller think we still hold a reference to it. + */ + alloc_state->charge_pool = NULL; return 0; } @@ -595,8 +610,9 @@ static s64 ttm_bo_evict_cb(struct ttm_lru_walk *walk, struct ttm_buffer_object * evict_walk->evicted++; if (evict_walk->res) - lret = ttm_resource_alloc(evict_walk->evictor, evict_walk->place, - evict_walk->res, NULL); + lret = ttm_bo_alloc_at_place(evict_walk->evictor, evict_walk->place, + walk->arg.ctx, false, evict_walk->res, + evict_walk->alloc_state); if (lret == 0) return 1; out: @@ -635,6 +651,8 @@ static int ttm_bo_evict_alloc(struct ttm_device *bdev, }; s64 lret; + state->in_evict = true; + evict_walk.walk.arg.trylock_only = true; lret = ttm_lru_walk_for_evict(&evict_walk.walk, bdev, man, 1); @@ -665,6 +683,7 @@ static int ttm_bo_evict_alloc(struct ttm_device *bdev, goto retry; } out: + state->in_evict = false; if (lret < 0) return lret; if (lret == 0) @@ -797,6 +816,7 @@ static int ttm_bo_alloc_resource(struct ttm_buffer_object *bo, res, &alloc_state); if (ret == -ENOSPC) { + dmem_cgroup_uncharge(alloc_state.charge_pool, bo->base.size); dmem_cgroup_pool_state_put(alloc_state.limit_pool); continue; } else if (ret == -EBUSY) { @@ -805,11 +825,15 @@ static int ttm_bo_alloc_resource(struct ttm_buffer_object *bo, dmem_cgroup_pool_state_put(alloc_state.limit_pool); - if (ret == -EBUSY) - continue; - else if (ret) + if (ret) { + dmem_cgroup_uncharge(alloc_state.charge_pool, + bo->base.size); + if (ret == -EBUSY) + continue; return ret; + } } else if (ret) { + dmem_cgroup_uncharge(alloc_state.charge_pool, bo->base.size); dmem_cgroup_pool_state_put(alloc_state.limit_pool); return ret; } diff --git a/drivers/gpu/drm/ttm/ttm_resource.c b/drivers/gpu/drm/ttm/ttm_resource.c index 154d6739256f8..02eca679cb68c 100644 --- a/drivers/gpu/drm/ttm/ttm_resource.c +++ b/drivers/gpu/drm/ttm/ttm_resource.c @@ -386,33 +386,52 @@ void ttm_resource_fini(struct ttm_resource_manager *man, } EXPORT_SYMBOL(ttm_resource_fini); +/** + * ttm_resource_try_charge - charge a resource manager's cgroup pool + * @bo: buffer for which an allocation should be charged + * @place: where the allocation is attempted to be placed + * @ret_pool: on charge success, the pool that was charged + * @ret_limit_pool: on charge failure, the pool responsible for the failure + * + * Should be used to charge cgroups before attempting resource allocation. + * When charging succeeds, the value of ret_pool should be passed to + * ttm_resource_alloc. + * + * Returns: 0 on charge success, negative errno on failure. + */ +int ttm_resource_try_charge(struct ttm_buffer_object *bo, + const struct ttm_place *place, + struct dmem_cgroup_pool_state **ret_pool, + struct dmem_cgroup_pool_state **ret_limit_pool) +{ + struct ttm_resource_manager *man = + ttm_manager_type(bo->bdev, place->mem_type); + + if (!man->cg) { + *ret_pool = NULL; + if (ret_limit_pool) + *ret_limit_pool = NULL; + return 0; + } + + return dmem_cgroup_try_charge(man->cg, bo->base.size, ret_pool, + ret_limit_pool); +} + int ttm_resource_alloc(struct ttm_buffer_object *bo, const struct ttm_place *place, struct ttm_resource **res_ptr, - struct dmem_cgroup_pool_state **ret_limit_pool) + struct dmem_cgroup_pool_state *charge_pool) { struct ttm_resource_manager *man = ttm_manager_type(bo->bdev, place->mem_type); - struct dmem_cgroup_pool_state *pool = NULL; int ret; - if (man->cg) { - ret = dmem_cgroup_try_charge(man->cg, bo->base.size, &pool, ret_limit_pool); - if (ret) { - if (ret == -EAGAIN) - ret = -ENOSPC; - return ret; - } - } - ret = man->func->alloc(man, bo, place, res_ptr); - if (ret) { - if (pool) - dmem_cgroup_uncharge(pool, bo->base.size); + if (ret) return ret; - } - (*res_ptr)->css = pool; + (*res_ptr)->css = charge_pool; spin_lock(&bo->bdev->lru_lock); ttm_resource_add_bulk_move(*res_ptr, bo); diff --git a/include/drm/ttm/ttm_resource.h b/include/drm/ttm/ttm_resource.h index a5d386583fb6e..e567b7ec8218f 100644 --- a/include/drm/ttm/ttm_resource.h +++ b/include/drm/ttm/ttm_resource.h @@ -458,10 +458,14 @@ void ttm_resource_init(struct ttm_buffer_object *bo, void ttm_resource_fini(struct ttm_resource_manager *man, struct ttm_resource *res); +int ttm_resource_try_charge(struct ttm_buffer_object *bo, + const struct ttm_place *place, + struct dmem_cgroup_pool_state **ret_pool, + struct dmem_cgroup_pool_state **ret_limit_pool); int ttm_resource_alloc(struct ttm_buffer_object *bo, const struct ttm_place *place, struct ttm_resource **res, - struct dmem_cgroup_pool_state **ret_limit_pool); + struct dmem_cgroup_pool_state *charge_pool); void ttm_resource_free(struct ttm_buffer_object *bo, struct ttm_resource **res); bool ttm_resource_intersects(struct ttm_device *bdev, struct ttm_resource *res, From a0e35dbd13afe98e094837b7b99e24cf1fef3cd1 Mon Sep 17 00:00:00 2001 From: Natalie Vock Date: Thu, 11 Sep 2025 11:30:59 +0200 Subject: [PATCH 08/73] [FROM-ML] drm/ttm: Be more aggressive when allocating below protection limit When the cgroup's memory usage is below the low/min limit and allocation fails, try evicting some unprotected buffers to make space. Otherwise, application buffers may be forced to go into GTT even though usage is below the corresponding low/min limit, if other applications filled VRAM with their allocations first. Signed-off-by: Natalie Vock --- drivers/gpu/drm/ttm/ttm_bo.c | 51 +++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/ttm/ttm_bo.c b/drivers/gpu/drm/ttm/ttm_bo.c index 824dad2935ef4..4b56016ab6a9f 100644 --- a/drivers/gpu/drm/ttm/ttm_bo.c +++ b/drivers/gpu/drm/ttm/ttm_bo.c @@ -495,6 +495,10 @@ struct ttm_bo_alloc_state { struct dmem_cgroup_pool_state *limit_pool; /** @in_evict: Whether we are currently evicting buffers */ bool in_evict; + /** @may_try_low: If only unprotected BOs, i.e. BOs whose cgroup + * is exceeding its dmem low/min protection, should be considered for eviction + */ + bool may_try_low; }; /** @@ -544,6 +548,42 @@ static int ttm_bo_alloc_at_place(struct ttm_buffer_object *bo, } } + /* + * cgroup protection plays a special role in eviction. + * Conceptually, protection of memory via the dmem cgroup controller + * entitles the protected cgroup to use a certain amount of memory. + * There are two types of protection - the 'low' limit is a + * "best-effort" protection, whereas the 'min' limit provides a hard + * guarantee that memory within the cgroup's allowance will not be + * evicted under any circumstance. + * + * To faithfully model this concept in TTM, we also need to take cgroup + * protection into account when allocating. When allocation in one + * place fails, TTM will default to trying other places first before + * evicting. + * If the allocation is covered by dmem cgroup protection, however, + * this prevents the allocation from using the memory it is "entitled" + * to. To make sure unprotected allocations cannot push new protected + * allocations out of places they are "entitled" to use, we should + * evict buffers not covered by any cgroup protection, if this + * allocation is covered by cgroup protection. + * + * Buffers covered by 'min' protection are a special case - the 'min' + * limit is a stronger guarantee than 'low', and thus buffers protected + * by 'low' but not 'min' should also be considered for eviction. + * Buffers protected by 'min' will never be considered for eviction + * anyway, so the regular eviction path should be triggered here. + * Buffers protected by 'low' but not 'min' will take a special + * eviction path that only evicts buffers covered by neither 'low' or + * 'min' protections. + */ + if (!alloc_state->in_evict) { + may_evict |= dmem_cgroup_below_min(NULL, alloc_state->charge_pool); + alloc_state->may_try_low = may_evict; + + may_evict |= dmem_cgroup_below_low(NULL, alloc_state->charge_pool); + } + ret = ttm_resource_alloc(bo, place, res, alloc_state->charge_pool); if (ret) { if (ret == -ENOSPC && may_evict) @@ -656,8 +696,12 @@ static int ttm_bo_evict_alloc(struct ttm_device *bdev, evict_walk.walk.arg.trylock_only = true; lret = ttm_lru_walk_for_evict(&evict_walk.walk, bdev, man, 1); - /* One more attempt if we hit low limit? */ - if (!lret && evict_walk.hit_low) { + /* If we failed to find enough BOs to evict, but we skipped over + * some BOs because they were covered by dmem low protection, retry + * evicting these protected BOs too, except if we're told not to + * consider protected BOs at all. + */ + if (!lret && evict_walk.hit_low && state->may_try_low) { evict_walk.try_low = true; lret = ttm_lru_walk_for_evict(&evict_walk.walk, bdev, man, 1); } @@ -678,7 +722,8 @@ static int ttm_bo_evict_alloc(struct ttm_device *bdev, } while (!lret && evict_walk.evicted); /* We hit the low limit? Try once more */ - if (!lret && evict_walk.hit_low && !evict_walk.try_low) { + if (!lret && evict_walk.hit_low && !evict_walk.try_low && + state->may_try_low) { evict_walk.try_low = true; goto retry; } From 8a167ef5eeba91a55487579b374842948c933de3 Mon Sep 17 00:00:00 2001 From: Natalie Vock Date: Sun, 14 Sep 2025 18:45:08 +0200 Subject: [PATCH 09/73] [FROM-ML] drm/ttm: Use common ancestor of evictor and evictee as limit pool When checking whether to skip certain buffers because they're protected by dmem.low, we're checking the effective protection of the evictee's cgroup, but depending on how the evictor's cgroup relates to the evictee's, the semantics of effective protection values change. When testing against cgroups from different subtrees, page_counter's recursive protection propagates memory protection afforded to a parent down to the child cgroups, even if the children were not explicitly protected. This prevents cgroups whose parents were afforded no protection from stealing memory from cgroups whose parents were afforded more protection, without users having to explicitly propagate this protection. However, if we always calculate protection from the root cgroup, this breaks prioritization of sibling cgroups: If one cgroup was explicitly protected and its siblings were not, the protected cgroup should get higher priority, i.e. the protected cgroup should be able to steal from unprotected siblings. This only works if we restrict the protection calculation to the subtree shared by evictor and evictee. Signed-off-by: Natalie Vock --- drivers/gpu/drm/ttm/ttm_bo.c | 43 +++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/ttm/ttm_bo.c b/drivers/gpu/drm/ttm/ttm_bo.c index 4b56016ab6a9f..277d3af23e30e 100644 --- a/drivers/gpu/drm/ttm/ttm_bo.c +++ b/drivers/gpu/drm/ttm/ttm_bo.c @@ -627,11 +627,48 @@ static s64 ttm_bo_evict_cb(struct ttm_lru_walk *walk, struct ttm_buffer_object * { struct ttm_bo_evict_walk *evict_walk = container_of(walk, typeof(*evict_walk), walk); + struct dmem_cgroup_pool_state *limit_pool, *ancestor = NULL; + bool evict_valuable; s64 lret; - if (!dmem_cgroup_state_evict_valuable(evict_walk->alloc_state->limit_pool, - bo->resource->css, evict_walk->try_low, - &evict_walk->hit_low)) + /* + * If may_try_low is not set, then we're trying to evict unprotected + * buffers in favor of a protected allocation for charge_pool. Explicitly skip + * buffers belonging to the same cgroup here - that cgroup is definitely protected, + * even though dmem_cgroup_state_evict_valuable would allow the eviction because a + * cgroup is always allowed to evict from itself even if it is protected. + */ + if (!evict_walk->alloc_state->may_try_low && + bo->resource->css == evict_walk->alloc_state->charge_pool) + return 0; + + limit_pool = evict_walk->alloc_state->limit_pool; + /* + * If there is no explicit limit pool, find the root of the shared subtree between + * evictor and evictee. This is important so that recursive protection rules can + * apply properly: Recursive protection distributes cgroup protection afforded + * to a parent cgroup but not used explicitly by a child cgroup between all child + * cgroups (see docs of effective_protection in mm/page_counter.c). However, when + * direct siblings compete for memory, siblings that were explicitly protected + * should get prioritized over siblings that weren't. This only happens correctly + * when the root of the shared subtree is passed to + * dmem_cgroup_state_evict_valuable. Otherwise, the effective-protection + * calculation cannot distinguish direct siblings from unrelated subtrees and the + * calculated protection ends up wrong. + */ + if (!limit_pool) { + ancestor = dmem_cgroup_get_common_ancestor(bo->resource->css, + evict_walk->alloc_state->charge_pool); + limit_pool = ancestor; + } + + evict_valuable = dmem_cgroup_state_evict_valuable(limit_pool, bo->resource->css, + evict_walk->try_low, + &evict_walk->hit_low); + if (ancestor) + dmem_cgroup_pool_state_put(ancestor); + + if (!evict_valuable) return 0; if (bo->pin_count || !bo->bdev->funcs->eviction_valuable(bo, evict_walk->place)) From 41378b47097f4b38765edc7ab2e691236e6f3661 Mon Sep 17 00:00:00 2001 From: Natalie Vock Date: Mon, 4 May 2026 20:37:15 -0700 Subject: [PATCH 10/73] [FROM-ML] drm/nouveau: Userspace can now make use of memory protection via dmem cgroups. Let nouveau benefit from this as well by registering the vram region with the dmem cgroup controller. This patch adapts the approach amdgpu and Xe have taken for enabling dmem cgroups. Signed-off-by: Natalie Vock --- drivers/gpu/drm/nouveau/nouveau_ttm.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/gpu/drm/nouveau/nouveau_ttm.c b/drivers/gpu/drm/nouveau/nouveau_ttm.c index ad01f922aa86a..860bca7e3ce28 100644 --- a/drivers/gpu/drm/nouveau/nouveau_ttm.c +++ b/drivers/gpu/drm/nouveau/nouveau_ttm.c @@ -188,6 +188,11 @@ nouveau_ttm_init_vram(struct nouveau_drm *drm) man->func = &nouveau_vram_manager; + man->cg = drmm_cgroup_register_region(drm->dev, "vram", + drm->gem.vram_available); + if (IS_ERR(man->cg)) + return PTR_ERR(man->cg); + ttm_resource_manager_init(man, &drm->ttm.bdev, drm->gem.vram_available >> PAGE_SHIFT); ttm_set_driver_manager(&drm->ttm.bdev, TTM_PL_VRAM, man); From 9d87f81dc375896cc68f94e07758d90e8ff52d5a Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Wed, 13 May 2026 23:14:42 +0000 Subject: [PATCH 11/73] [FROM-ML] HID: hid-msi: Add MSI Claw configuration driver Adds configuration HID driver for the MSI Claw series of handheld PC's. In this initial patch add the initial driver outline and attributes for changing the gamepad mode, M-key behavior, and add a WO reset function. Sending the SWITCH_MODE and RESET commands causes a USB disconnect in the device. The completion will therefore never get hit and would trigger an -EIO. To avoid showing the user an error for every write to these attrs a bypass for the completion handling is introduced when timeout == 0. The initial version of this patch was written by Denis Benato, which contained the initial reverse-engineering and implementation for the gamepad mode switching. This work was later expanded by Zhouwang Huang to include more gamepad modes. Finally, I refactored the drivers data in/out flow and overall format to conform to kernel driver best practices and style guides. Claude was used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Denis Benato Signed-off-by: Denis Benato Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- MAINTAINERS | 6 + drivers/hid/Kconfig | 12 + drivers/hid/Makefile | 1 + drivers/hid/hid-ids.h | 6 + drivers/hid/hid-msi.c | 582 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 607 insertions(+) create mode 100644 drivers/hid/hid-msi.c diff --git a/MAINTAINERS b/MAINTAINERS index c8d4b913f26c1..52cbf34646fc3 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -18129,6 +18129,12 @@ S: Odd Fixes F: Documentation/devicetree/bindings/net/ieee802154/mrf24j40.txt F: drivers/net/ieee802154/mrf24j40.c +MSI HID DRIVER +M: Derek J. Clark +L: linux-input@vger.kernel.org +S: Maintained +F: drivers/hid/hid-msi.c + MSI EC DRIVER M: Nikita Kravets L: platform-driver-x86@vger.kernel.org diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index ff2f580b660ba..ec412bba1e1ed 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -485,6 +485,18 @@ config HID_GT683R Currently the following devices are know to be supported: - MSI GT683R +config HID_MSI + tristate "MSI Claw Gamepad Support" + depends on USB_HID + select LEDS_CLASS + select LEDS_CLASS_MULTICOLOR + help + Support for the MSI Claw RGB and controller configuration + + Say Y here to include configuration interface support for the MSI Claw Line + of Handheld Console Controllers. Say M here to compile this driver as a + module. The module will be called hid-msi. + config HID_KEYTOUCH tristate "Keytouch HID devices" help diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 0597fd6a4ffd3..c2cb312d8247c 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -92,6 +92,7 @@ obj-$(CONFIG_HID_MAYFLASH) += hid-mf.o obj-$(CONFIG_HID_MEGAWORLD_FF) += hid-megaworld.o obj-$(CONFIG_HID_MICROSOFT) += hid-microsoft.o obj-$(CONFIG_HID_MONTEREY) += hid-monterey.o +obj-$(CONFIG_HID_MSI) += hid-msi.o obj-$(CONFIG_HID_MULTITOUCH) += hid-multitouch.o obj-$(CONFIG_HID_NINTENDO) += hid-nintendo.o obj-$(CONFIG_HID_NTI) += hid-nti.o diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 426ff78c1c033..f39408c76aa42 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1066,7 +1066,13 @@ #define USB_DEVICE_ID_MOZA_R16_R21_2 0x0010 #define USB_VENDOR_ID_MSI 0x1770 +#define USB_VENDOR_ID_MSI_2 0x0db0 #define USB_DEVICE_ID_MSI_GT683R_LED_PANEL 0xff00 +#define USB_DEVICE_ID_MSI_CLAW_XINPUT 0x1901 +#define USB_DEVICE_ID_MSI_CLAW_DINPUT 0x1902 +#define USB_DEVICE_ID_MSI_CLAW_DESKTOP 0x1903 +#define USB_DEVICE_ID_MSI_CLAW_BIOS 0x1904 + #define USB_VENDOR_ID_NATIONAL_SEMICONDUCTOR 0x0400 #define USB_DEVICE_ID_N_S_HARMONY 0xc359 diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c new file mode 100644 index 0000000000000..8915942af15e6 --- /dev/null +++ b/drivers/hid/hid-msi.c @@ -0,0 +1,582 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for MSI Claw Handheld PC gamepads. + * + * Provides configuration support for the MSI Claw series of handheld PC + * gamepads. Multiple iterations of the device firmware has led to some + * quirks for how certain attributes are handled. The original firmware + * did not support remapping of the M1 (right) and M2 (left) rear paddles. + * Additionally, the MCU RAM address for writing configuration data has + * changed twice. Checks are done during probe to enumerate these variances. + * + * Copyright (c) 2026 Zhouwang Huang + * Copyright (c) 2026 Denis Benato + * Copyright (c) 2026 Valve Corporation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hid-ids.h" + +#define CLAW_OUTPUT_REPORT_ID 0x0f +#define CLAW_INPUT_REPORT_ID 0x10 + +#define CLAW_PACKET_SIZE 64 + +#define CLAW_DINPUT_CFG_INTF_IN 0x82 +#define CLAW_XINPUT_CFG_INTF_IN 0x83 + +enum claw_command_index { + CLAW_COMMAND_TYPE_READ_PROFILE = 0x04, + CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05, + CLAW_COMMAND_TYPE_ACK = 0x06, + CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA = 0x21, + CLAW_COMMAND_TYPE_SYNC_TO_ROM = 0x22, + CLAW_COMMAND_TYPE_SWITCH_MODE = 0x24, + CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE = 0x26, + CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK = 0x27, + CLAW_COMMAND_TYPE_RESET_DEVICE = 0x28, +}; + +enum claw_gamepad_mode_index { + CLAW_GAMEPAD_MODE_XINPUT = 0x01, + CLAW_GAMEPAD_MODE_DINPUT = 0x02, + CLAW_GAMEPAD_MODE_DESKTOP = 0x04, +}; + +static const char * const claw_gamepad_mode_text[] = { + [CLAW_GAMEPAD_MODE_XINPUT] = "xinput", + [CLAW_GAMEPAD_MODE_DINPUT] = "dinput", + [CLAW_GAMEPAD_MODE_DESKTOP] = "desktop", +}; + +enum claw_mkeys_function_index { + CLAW_MKEY_FUNCTION_MACRO, + CLAW_MKEY_FUNCTION_COMBO, + CLAW_MKEY_FUNCTION_DISABLED, +}; + +static const char * const claw_mkeys_function_text[] = { + [CLAW_MKEY_FUNCTION_MACRO] = "macro", + [CLAW_MKEY_FUNCTION_COMBO] = "combination", + [CLAW_MKEY_FUNCTION_DISABLED] = "disabled", +}; + +struct claw_command_report { + u8 report_id; + u8 padding[2]; + u8 header_tail; + u8 cmd; + u8 data[59]; +} __packed; + +struct claw_drvdata { + /* MCU General Variables */ + struct completion send_cmd_complete; + struct delayed_work cfg_resume; + struct delayed_work cfg_setup; + struct hid_device *hdev; + struct mutex cfg_mutex; /* mutex for synchronous data */ + u8 ep; + + /* Gamepad Variables */ + enum claw_mkeys_function_index mkeys_function; + enum claw_gamepad_mode_index gamepad_mode; +}; + +static int get_endpoint_address(struct hid_device *hdev) +{ + struct usb_host_endpoint *ep; + struct usb_interface *intf; + + intf = to_usb_interface(hdev->dev.parent); + ep = intf->cur_altsetting->endpoint; + if (ep) + return ep->desc.bEndpointAddress; + + return -ENODEV; +} + +static int claw_gamepad_mode_event(struct claw_drvdata *drvdata, + struct claw_command_report *cmd_rep) +{ + if (cmd_rep->data[0] >= ARRAY_SIZE(claw_gamepad_mode_text) || + !claw_gamepad_mode_text[cmd_rep->data[0]] || + cmd_rep->data[1] >= ARRAY_SIZE(claw_mkeys_function_text)) + return -EINVAL; + + drvdata->gamepad_mode = cmd_rep->data[0]; + drvdata->mkeys_function = cmd_rep->data[1]; + + return 0; +} + +static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report, + u8 *data, int size) +{ + struct claw_command_report *cmd_rep; + int ret = 0; + + if (size != CLAW_PACKET_SIZE) + return 0; + + cmd_rep = (struct claw_command_report *)data; + + if (cmd_rep->report_id != CLAW_INPUT_REPORT_ID || cmd_rep->header_tail != 0x3c) + return 0; + + dev_dbg(&drvdata->hdev->dev, "Rx data as raw input report: [%*ph]\n", + CLAW_PACKET_SIZE, data); + + switch (cmd_rep->cmd) { + case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK: + ret = claw_gamepad_mode_event(drvdata, cmd_rep); + break; + case CLAW_COMMAND_TYPE_ACK: + break; + default: + dev_dbg(&drvdata->hdev->dev, "Unknown command: %x\n", cmd_rep->cmd); + return 0; + } + + complete(&drvdata->send_cmd_complete); + + return ret; +} + +static int msi_raw_event(struct hid_device *hdev, struct hid_report *report, + u8 *data, int size) +{ + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + + if (!drvdata || (drvdata->ep != CLAW_XINPUT_CFG_INTF_IN && + drvdata->ep != CLAW_DINPUT_CFG_INTF_IN)) + return 0; + + return claw_raw_event(drvdata, report, data, size); +} + +static int claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data, + size_t len, unsigned int timeout) +{ + unsigned char *dmabuf __free(kfree) = NULL; + u8 header[] = { CLAW_OUTPUT_REPORT_ID, 0, 0, 0x3c, index }; + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + size_t header_size = ARRAY_SIZE(header); + int ret; + + if (header_size + len > CLAW_PACKET_SIZE) + return -EINVAL; + + /* We can't use a devm_alloc reusable buffer without side effects during suspend */ + dmabuf = kzalloc(CLAW_PACKET_SIZE, GFP_KERNEL); + if (!dmabuf) + return -ENOMEM; + + memcpy(dmabuf, header, header_size); + if (data && len) + memcpy(dmabuf + header_size, data, len); + + /* Don't hold a mutex when timeout=0, those commands cause USB disconnect */ + if (timeout) { + guard(mutex)(&drvdata->cfg_mutex); + reinit_completion(&drvdata->send_cmd_complete); + } + + dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n", + CLAW_PACKET_SIZE, dmabuf); + + ret = hid_hw_output_report(hdev, dmabuf, CLAW_PACKET_SIZE); + if (ret < 0) + return ret; + + ret = ret == CLAW_PACKET_SIZE ? 0 : -EIO; + if (ret) + return ret; + + if (timeout) { + ret = wait_for_completion_interruptible_timeout(&drvdata->send_cmd_complete, + msecs_to_jiffies(timeout)); + + dev_dbg(&hdev->dev, "Remaining timeout: %u\n", ret); + if (ret >= 0) /* preserve errors */ + ret = ret == 0 ? -EBUSY : 0; /* timeout occurred : time remained */ + } + + return ret; +} + +static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u8 data[2] = { 0x00, drvdata->mkeys_function }; + int i, ret = -EINVAL; + + for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) { + if (claw_gamepad_mode_text[i] && sysfs_streq(buf, claw_gamepad_mode_text[i])) { + ret = i; + break; + } + } + if (ret < 0) + return ret; + + data[0] = ret; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0); + if (ret) + return ret; + + return count; +} + +static ssize_t gamepad_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + int ret, i; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8); + if (ret) + return ret; + + i = drvdata->gamepad_mode; + + if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0') + return sysfs_emit(buf, "unsupported\n"); + + return sysfs_emit(buf, "%s\n", claw_gamepad_mode_text[i]); +} +static DEVICE_ATTR_RW(gamepad_mode); + +static ssize_t gamepad_mode_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t count = 0; + int i; + + for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) { + if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0') + continue; + count += sysfs_emit_at(buf, count, "%s ", claw_gamepad_mode_text[i]); + } + + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(gamepad_mode_index); + +static ssize_t mkeys_function_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u8 data[2] = { drvdata->gamepad_mode, 0x00 }; + int i, ret = -EINVAL; + + for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) { + if (claw_mkeys_function_text[i] && sysfs_streq(buf, claw_mkeys_function_text[i])) { + ret = i; + break; + } + } + if (ret < 0) + return ret; + + data[1] = ret; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0); + if (ret) + return ret; + + return count; +} + +static ssize_t mkeys_function_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + int ret, i; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8); + if (ret) + return ret; + + i = drvdata->mkeys_function; + + if (i >= ARRAY_SIZE(claw_mkeys_function_text)) + return sysfs_emit(buf, "unsupported\n"); + + return sysfs_emit(buf, "%s\n", claw_mkeys_function_text[i]); +} +static DEVICE_ATTR_RW(mkeys_function); + +static ssize_t mkeys_function_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int i, count = 0; + + for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) + count += sysfs_emit_at(buf, count, "%s ", claw_mkeys_function_text[i]); + + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(mkeys_function_index); + +static ssize_t reset_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev = to_hid_device(dev); + bool val; + int ret; + + ret = kstrtobool(buf, &val); + if (ret) + return ret; + + if (!val) + return -EINVAL; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_RESET_DEVICE, NULL, 0, 0); + if (ret < 0) + return ret; + + return count; +} +static DEVICE_ATTR_WO(reset); + +static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr, + int n) +{ + struct hid_device *hdev = to_hid_device(kobj_to_dev(kobj)); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + + if (!drvdata) { + dev_warn(&hdev->dev, + "Failed to get drvdata from kobj. Gamepad attributes are not available.\n"); + return 0; + } + + return attr->mode; +} + +static struct attribute *claw_gamepad_attrs[] = { + &dev_attr_gamepad_mode.attr, + &dev_attr_gamepad_mode_index.attr, + &dev_attr_mkeys_function.attr, + &dev_attr_mkeys_function_index.attr, + &dev_attr_reset.attr, + NULL, +}; + +static const struct attribute_group claw_gamepad_attr_group = { + .attrs = claw_gamepad_attrs, + .is_visible = claw_gamepad_attr_is_visible, +}; + +static void cfg_setup_fn(struct work_struct *work) +{ + struct delayed_work *dwork = container_of(work, struct delayed_work, work); + struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_setup); + int ret; + + ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to setup device, can't read gamepad mode: %d\n", ret); + return; + } + + /* Add sysfs attributes after we get the device state */ + ret = devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to setup device, can't create gamepad attrs: %d\n", ret); + return; + } + + kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE); +} + +static void cfg_resume_fn(struct work_struct *work) +{ + struct delayed_work *dwork = container_of(work, struct delayed_work, work); + struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_resume); + u8 data[2] = { drvdata->gamepad_mode, drvdata->mkeys_function }; + int ret; + + ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, + ARRAY_SIZE(data), 0); + if (ret) + dev_err(&drvdata->hdev->dev, "Failed to set gamepad mode settings: %d\n", ret); +} + +static int claw_probe(struct hid_device *hdev, u8 ep) +{ + struct claw_drvdata *drvdata; + int ret; + + drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL); + if (!drvdata) + return -ENOMEM; + + hid_set_drvdata(hdev, drvdata); + drvdata->hdev = hdev; + drvdata->ep = ep; + + mutex_init(&drvdata->cfg_mutex); + init_completion(&drvdata->send_cmd_complete); + + /* For control interface: open the HID transport for sending commands. */ + ret = hid_hw_open(hdev); + if (ret) + return ret; + + INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn); + INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn); + schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500)); + + return 0; +} + +static int msi_probe(struct hid_device *hdev, const struct hid_device_id *id) +{ + int ret; + u8 ep; + + if (!hid_is_usb(hdev)) { + ret = -ENODEV; + goto err_probe; + } + + ret = hid_parse(hdev); + if (ret) + goto err_probe; + + /* Set quirk to create separate input devices per HID application */ + hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT; + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) + goto err_probe; + + /* For non-control interfaces (keyboard/mouse), allow userspace to grab the devices. */ + ret = get_endpoint_address(hdev); + if (ret < 0) + goto err_stop_hw; + + ep = ret; + if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) { + ret = claw_probe(hdev, ep); + if (ret) + goto err_stop_hw; + } + + return 0; + +err_stop_hw: + hid_hw_stop(hdev); +err_probe: + return dev_err_probe(&hdev->dev, ret, "Failed to init device\n"); +} + +static void claw_remove(struct hid_device *hdev) +{ + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + + if (!drvdata) { + hid_hw_stop(hdev); + return; + } + + cancel_delayed_work_sync(&drvdata->cfg_setup); + cancel_delayed_work_sync(&drvdata->cfg_resume); + hid_hw_close(hdev); +} + +static void msi_remove(struct hid_device *hdev) +{ + int ret; + u8 ep; + + ret = get_endpoint_address(hdev); + if (ret <= 0) + goto hw_stop; + + ep = ret; + if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) + claw_remove(hdev); + +hw_stop: + hid_hw_stop(hdev); +} + +static int claw_resume(struct hid_device *hdev) +{ + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + + /* MCU can take up to 500ms to be ready after resume */ + schedule_delayed_work(&drvdata->cfg_resume, msecs_to_jiffies(500)); + return 0; +} + +static int msi_resume(struct hid_device *hdev) +{ + int ret; + u8 ep; + + ret = get_endpoint_address(hdev); + if (ret <= 0) + return 0; + + ep = ret; + if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) + return claw_resume(hdev); + + return 0; +} + +static const struct hid_device_id msi_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_XINPUT) }, + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DINPUT) }, + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DESKTOP) }, + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_BIOS) }, + { } +}; +MODULE_DEVICE_TABLE(hid, msi_devices); + +static struct hid_driver msi_driver = { + .name = "hid-msi", + .id_table = msi_devices, + .raw_event = msi_raw_event, + .probe = msi_probe, + .remove = msi_remove, + .resume = msi_resume, +}; +module_hid_driver(msi_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Denis Benato "); +MODULE_AUTHOR("Zhouwang Huang "); +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("HID driver for MSI Claw Handheld PC gamepads"); From 29eb0c66813662bdc3389026d4bf05b1cef1ef5d Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Wed, 13 May 2026 23:14:43 +0000 Subject: [PATCH 12/73] [FROM-ML] HID: hid-msi: Add M-key mapping attributes Adds attributes that allow for remapping the M-keys with up to 5 values when in macro mode. There are 2 mappable buttons on the rear of the device, M1 on the right and M2 on the left. When mapped, the events will fire from one of three event devices: gamepad buttons will fire from the device handled by xpad, while keyboard and mouse events will fire from respectively typed evdevs provided by the input core. Names of each mapping have been kept as close to the event that will fire from the evdev as possible, with context added to the ABS_ events on the direction of the movement. Initial reverse-engineering and implementation of this feature was done by Zhouwang Huang. I refactored the overall format to conform to kernel driver best practices and style guides. Claude was used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- drivers/hid/hid-msi.c | 398 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 397 insertions(+), 1 deletion(-) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index 8915942af15e6..13ba2747fdb66 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -40,6 +40,8 @@ #define CLAW_DINPUT_CFG_INTF_IN 0x82 #define CLAW_XINPUT_CFG_INTF_IN 0x83 +#define CLAW_KEYS_MAX 5 + enum claw_command_index { CLAW_COMMAND_TYPE_READ_PROFILE = 0x04, CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05, @@ -64,6 +66,17 @@ static const char * const claw_gamepad_mode_text[] = { [CLAW_GAMEPAD_MODE_DESKTOP] = "desktop", }; +enum claw_profile_ack_pending { + CLAW_NO_PENDING, + CLAW_M1_PENDING, + CLAW_M2_PENDING, +}; + +enum claw_key_index { + CLAW_KEY_M1, + CLAW_KEY_M2, +}; + enum claw_mkeys_function_index { CLAW_MKEY_FUNCTION_MACRO, CLAW_MKEY_FUNCTION_COMBO, @@ -76,6 +89,154 @@ static const char * const claw_mkeys_function_text[] = { [CLAW_MKEY_FUNCTION_DISABLED] = "disabled", }; +static const struct { + u8 code; + const char *name; +} claw_button_mapping_key_map[] = { + /* Gamepad buttons */ + { 0x01, "ABS_HAT0Y_UP" }, + { 0x02, "ABS_HAT0Y_DOWN" }, + { 0x03, "ABS_HAT0X_LEFT" }, + { 0x04, "ABS_HAT0X_RIGHT" }, + { 0x05, "BTN_TL" }, + { 0x06, "BTN_TR" }, + { 0x07, "BTN_THUMBL" }, + { 0x08, "BTN_THUMBR" }, + { 0x09, "BTN_SOUTH" }, + { 0x0a, "BTN_EAST" }, + { 0x0b, "BTN_NORTH" }, + { 0x0c, "BTN_WEST" }, + { 0x0d, "BTN_MODE" }, + { 0x0e, "BTN_SELECT" }, + { 0x0f, "BTN_START" }, + { 0x13, "BTN_TL2"}, + { 0x14, "BTN_TR2"}, + { 0x15, "ABS_Y_UP"}, + { 0x16, "ABS_Y_DOWN"}, + { 0x17, "ABS_X_LEFT"}, + { 0x18, "ABS_X_LEFT_RIGHT"}, + { 0x19, "ABS_RY_UP"}, + { 0x1a, "ABS_RY_DOWN"}, + { 0x1b, "ABS_RX_LEFT"}, + { 0x1c, "ABS_RX_RIGHT"}, + /* Keyboard keys */ + { 0x32, "KEY_ESC" }, + { 0x33, "KEY_F1" }, + { 0x34, "KEY_F2" }, + { 0x35, "KEY_F3" }, + { 0x36, "KEY_F4" }, + { 0x37, "KEY_F5" }, + { 0x38, "KEY_F6" }, + { 0x39, "KEY_F7" }, + { 0x3a, "KEY_F8" }, + { 0x3b, "KEY_F9" }, + { 0x3c, "KEY_F10" }, + { 0x3d, "KEY_F11" }, + { 0x3e, "KEY_F12" }, + { 0x3f, "KEY_GRAVE" }, + { 0x40, "KEY_1" }, + { 0x41, "KEY_2" }, + { 0x42, "KEY_3" }, + { 0x43, "KEY_4" }, + { 0x44, "KEY_5" }, + { 0x45, "KEY_6" }, + { 0x46, "KEY_7" }, + { 0x47, "KEY_8" }, + { 0x48, "KEY_9" }, + { 0x49, "KEY_0" }, + { 0x4a, "KEY_MINUS" }, + { 0x4b, "KEY_EQUAL" }, + { 0x4c, "KEY_BACKSPACE" }, + { 0x4d, "KEY_TAB" }, + { 0x4e, "KEY_Q" }, + { 0x4f, "KEY_W" }, + { 0x50, "KEY_E" }, + { 0x51, "KEY_R" }, + { 0x52, "KEY_T" }, + { 0x53, "KEY_Y" }, + { 0x54, "KEY_U" }, + { 0x55, "KEY_I" }, + { 0x56, "KEY_O" }, + { 0x57, "KEY_P" }, + { 0x58, "KEY_LEFTBRACE" }, + { 0x59, "KEY_RIGHTBRACE" }, + { 0x5a, "KEY_BACKSLASH" }, + { 0x5b, "KEY_CAPSLOCK" }, + { 0x5c, "KEY_A" }, + { 0x5d, "KEY_S" }, + { 0x5e, "KEY_D" }, + { 0x5f, "KEY_F" }, + { 0x60, "KEY_G" }, + { 0x61, "KEY_H" }, + { 0x62, "KEY_J" }, + { 0x63, "KEY_K" }, + { 0x64, "KEY_L" }, + { 0x65, "KEY_SEMICOLON" }, + { 0x66, "KEY_APOSTROPHE" }, + { 0x67, "KEY_ENTER" }, + { 0x68, "KEY_LEFTSHIFT" }, + { 0x69, "KEY_Z" }, + { 0x6a, "KEY_X" }, + { 0x6b, "KEY_C" }, + { 0x6c, "KEY_V" }, + { 0x6d, "KEY_B" }, + { 0x6e, "KEY_N" }, + { 0x6f, "KEY_M" }, + { 0x70, "KEY_COMMA" }, + { 0x71, "KEY_DOT" }, + { 0x72, "KEY_SLASH" }, + { 0x73, "KEY_RIGHTSHIFT" }, + { 0x74, "KEY_LEFTCTRL" }, + { 0x75, "KEY_LEFTMETA" }, + { 0x76, "KEY_LEFTALT" }, + { 0x77, "KEY_SPACE" }, + { 0x78, "KEY_RIGHTALT" }, + { 0x79, "KEY_RIGHTCTRL" }, + { 0x7a, "KEY_INSERT" }, + { 0x7b, "KEY_HOME" }, + { 0x7c, "KEY_PAGEUP" }, + { 0x7d, "KEY_DELETE" }, + { 0x7e, "KEY_END" }, + { 0x7f, "KEY_PAGEDOWN" }, + { 0x8a, "KEY_KPENTER" }, + { 0x8b, "KEY_KP0" }, + { 0x8c, "KEY_KP1" }, + { 0x8d, "KEY_KP2" }, + { 0x8e, "KEY_KP3" }, + { 0x8f, "KEY_KP4" }, + { 0x90, "KEY_KP5" }, + { 0x91, "KEY_KP6" }, + { 0x92, "KEY_KP7" }, + { 0x93, "KEY_KP8" }, + { 0x94, "KEY_KP9" }, + { 0x95, "MD_PLAY" }, + { 0x96, "MD_STOP" }, + { 0x97, "MD_NEXT" }, + { 0x98, "MD_PREV" }, + { 0x99, "MD_VOL_UP" }, + { 0x9a, "MD_VOL_DOWN" }, + { 0x9b, "MD_VOL_MUTE" }, + { 0x9c, "KEY_F23" }, + /* Mouse events */ + { 0xc8, "BTN_LEFT" }, + { 0xc9, "BTN_MIDDLE" }, + { 0xca, "BTN_RIGHT" }, + { 0xcb, "BTN_SIDE" }, + { 0xcc, "BTN_EXTRA" }, + { 0xcd, "REL_WHEEL_UP" }, + { 0xce, "REL_WHEEL_DOWN" }, +}; + +static const u16 button_mapping_addr_old[] = { + 0x007a, /* M1 */ + 0x011f, /* M2 */ +}; + +static const u16 button_mapping_addr_new[] = { + 0x00bb, /* M1 */ + 0x0164, /* M2 */ +}; + struct claw_command_report { u8 report_id; u8 padding[2]; @@ -86,16 +247,24 @@ struct claw_command_report { struct claw_drvdata { /* MCU General Variables */ + enum claw_profile_ack_pending profile_pending; struct completion send_cmd_complete; struct delayed_work cfg_resume; struct delayed_work cfg_setup; + struct mutex profile_mutex; /* mutex for profile_pending calls */ struct hid_device *hdev; struct mutex cfg_mutex; /* mutex for synchronous data */ + struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */ + u16 bcd_device; u8 ep; /* Gamepad Variables */ enum claw_mkeys_function_index mkeys_function; enum claw_gamepad_mode_index gamepad_mode; + u8 m1_codes[CLAW_KEYS_MAX]; + u8 m2_codes[CLAW_KEYS_MAX]; + const u16 *bmap_addr; + bool bmap_support; }; static int get_endpoint_address(struct hid_device *hdev) @@ -125,6 +294,31 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata, return 0; } +static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep) +{ + u8 *codes; + int i; + + switch (drvdata->profile_pending) { + case CLAW_M1_PENDING: + case CLAW_M2_PENDING: + codes = (drvdata->profile_pending == CLAW_M1_PENDING) ? + drvdata->m1_codes : drvdata->m2_codes; + /* Extract key codes; replace disabled (0xff) with 0x00, which is (null) in _show */ + for (i = 0; i < CLAW_KEYS_MAX; i++) + codes[i] = (cmd_rep->data[6 + i] != 0xff) ? cmd_rep->data[6 + i] : 0x00; + break; + default: + dev_warn(&drvdata->hdev->dev, + "Got profile event without changes pending from command: %x\n", + cmd_rep->cmd); + return -EINVAL; + } + drvdata->profile_pending = CLAW_NO_PENDING; + + return 0; +} + static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report, u8 *data, int size) { @@ -146,6 +340,9 @@ static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *repor case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK: ret = claw_gamepad_mode_event(drvdata, cmd_rep); break; + case CLAW_COMMAND_TYPE_READ_PROFILE_ACK: + ret = claw_profile_event(drvdata, cmd_rep); + break; case CLAW_COMMAND_TYPE_ACK: break; default: @@ -366,6 +563,161 @@ static ssize_t reset_store(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_WO(reset); +static int button_mapping_name_to_code(const char *name) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) { + if (!strcmp(name, claw_button_mapping_key_map[i].name)) + return claw_button_mapping_key_map[i].code; + } + + return -EINVAL; +} + +static const char *button_mapping_code_to_name(u8 code) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) { + if (claw_button_mapping_key_map[i].code == code) + return claw_button_mapping_key_map[i].name; + } + + return NULL; +} + +static int claw_buttons_store(struct device *dev, const char *buf, u8 mkey_idx) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u8 data[] = { 0x01, (drvdata->bmap_addr[mkey_idx] >> 8) & 0xff, + drvdata->bmap_addr[mkey_idx] & 0xff, 0x07, + 0x04, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff }; + size_t len = ARRAY_SIZE(data); + int ret, key_count, i; + char **raw_keys; + + raw_keys = argv_split(GFP_KERNEL, buf, &key_count); + if (!raw_keys) + return -ENOMEM; + + guard(mutex)(&drvdata->rom_mutex); /* all err_free paths must be in scope */ + if (key_count > CLAW_KEYS_MAX) { + ret = -EINVAL; + goto err_free; + } + + if (key_count == 0) + goto set_buttons; + + for (i = 0; i < key_count; i++) { + ret = button_mapping_name_to_code(raw_keys[i]); + if (ret < 0) + goto err_free; + + data[6 + i] = ret; + } + +set_buttons: + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, data, len, 8); + if (ret < 0) + goto err_free; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); + +err_free: + argv_free(raw_keys); + return ret; +} + +static int claw_buttons_show(struct device *dev, char *buf, enum claw_key_index m_key) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u8 data[] = { 0x01, (drvdata->bmap_addr[m_key] >> 8) & 0xff, + drvdata->bmap_addr[m_key] & 0xff, 0x07 }; + size_t len = ARRAY_SIZE(data); + int i, ret, count = 0; + const char *name; + u8 *codes; + + codes = (m_key == CLAW_KEY_M1) ? drvdata->m1_codes : drvdata->m2_codes; + + guard(mutex)(&drvdata->profile_mutex); + drvdata->profile_pending = (m_key == CLAW_KEY_M1) ? CLAW_M1_PENDING : CLAW_M2_PENDING; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 8); + if (ret) { + drvdata->profile_pending = CLAW_NO_PENDING; + return ret; + } + for (i = 0; i < CLAW_KEYS_MAX; i++) { + name = button_mapping_code_to_name(codes[i]); + if (name) + count += sysfs_emit_at(buf, count, "%s ", name); + } + + if (!count) + return sysfs_emit(buf, "(not set)\n"); + + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t button_m1_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + + ret = claw_buttons_store(dev, buf, CLAW_KEY_M1); + if (ret) + return ret; + + return count; +} + +static ssize_t button_m1_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return claw_buttons_show(dev, buf, CLAW_KEY_M1); +} +static DEVICE_ATTR_RW(button_m1); + +static ssize_t button_m2_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + + ret = claw_buttons_store(dev, buf, CLAW_KEY_M2); + if (ret) + return ret; + + return count; +} + +static ssize_t button_m2_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return claw_buttons_show(dev, buf, CLAW_KEY_M2); +} +static DEVICE_ATTR_RW(button_m2); + +static ssize_t button_mapping_options_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int i, count = 0; + + for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) + count += sysfs_emit_at(buf, count, "%s ", claw_button_mapping_key_map[i].name); + + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(button_mapping_options); + static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n) { @@ -378,10 +730,22 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu return 0; } - return attr->mode; + /* Always show attrs available on all firmware */ + if (attr == &dev_attr_gamepad_mode.attr || + attr == &dev_attr_gamepad_mode_index.attr || + attr == &dev_attr_mkeys_function.attr || + attr == &dev_attr_mkeys_function_index.attr || + attr == &dev_attr_reset.attr) + return attr->mode; + + /* Hide button mapping attrs if it isn't supported */ + return drvdata->bmap_support ? attr->mode : 0; } static struct attribute *claw_gamepad_attrs[] = { + &dev_attr_button_m1.attr, + &dev_attr_button_m2.attr, + &dev_attr_button_mapping_options.attr, &dev_attr_gamepad_mode.attr, &dev_attr_gamepad_mode_index.attr, &dev_attr_mkeys_function.attr, @@ -432,8 +796,31 @@ static void cfg_resume_fn(struct work_struct *work) dev_err(&drvdata->hdev->dev, "Failed to set gamepad mode settings: %d\n", ret); } +static void claw_features_supported(struct claw_drvdata *drvdata) +{ + u8 major = (drvdata->bcd_device >> 8) & 0xff; + u8 minor = drvdata->bcd_device & 0xff; + + if (major == 0x01) { + drvdata->bmap_support = true; + if (minor >= 0x66) + drvdata->bmap_addr = button_mapping_addr_new; + else + drvdata->bmap_addr = button_mapping_addr_old; + return; + } + + if ((major == 0x02 && minor >= 0x17) || major >= 0x03) { + drvdata->bmap_support = true; + drvdata->bmap_addr = button_mapping_addr_new; + return; + } +} + static int claw_probe(struct hid_device *hdev, u8 ep) { + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_device *udev = interface_to_usbdev(intf); struct claw_drvdata *drvdata; int ret; @@ -446,8 +833,17 @@ static int claw_probe(struct hid_device *hdev, u8 ep) drvdata->ep = ep; mutex_init(&drvdata->cfg_mutex); + mutex_init(&drvdata->profile_mutex); + mutex_init(&drvdata->rom_mutex); init_completion(&drvdata->send_cmd_complete); + /* Determine feature level from firmware version */ + drvdata->bcd_device = le16_to_cpu(udev->descriptor.bcdDevice); + claw_features_supported(drvdata); + + if (!drvdata->bmap_support) + dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n"); + /* For control interface: open the HID transport for sending commands. */ ret = hid_hw_open(hdev); if (ret) From 13755fb2312a487e28a6a5ce5f32e46e305b1de4 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Wed, 13 May 2026 23:14:44 +0000 Subject: [PATCH 13/73] [FROM-ML] HID: hid-msi: Add RGB control interface Adds RGB control interface for MSI Claw devices. The MSI Claw uses a fairly unique RGB interface. It has 9 total zones (4 per joystick ring and 1 for the ABXY buttons), and supports up to 8 sequential frames of RGB zone data. Each frame is written to a specific area of MCU memory by the profile command, the value of which changes based on the firmware of the device. Unlike other devices (such as the Legion Go or the OneXPlayer devices), there are no hard coded effects built into the MCU. Instead, the basic effects are provided as a series of frame data. I have mirrored the effects available in Windows in this driver, while keeping the effect names consistent with the Lenovo drivers for the effects that are similar. Initial reverse-engineering and implementation of this feature was done by Zhouwang Huang. I refactored the overall format to conform to kernel driver best practices and style guides. Claude was used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- drivers/hid/hid-msi.c | 548 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 542 insertions(+), 6 deletions(-) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index 13ba2747fdb66..a628b77bfb7b5 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,10 @@ #define CLAW_KEYS_MAX 5 +#define CLAW_RGB_ZONES 9 +#define CLAW_RGB_MAX_FRAMES 8 +#define CLAW_RGB_FRAME_OFFSET 0x24 + enum claw_command_index { CLAW_COMMAND_TYPE_READ_PROFILE = 0x04, CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05, @@ -70,6 +75,7 @@ enum claw_profile_ack_pending { CLAW_NO_PENDING, CLAW_M1_PENDING, CLAW_M2_PENDING, + CLAW_RGB_PENDING, }; enum claw_key_index { @@ -227,6 +233,22 @@ static const struct { { 0xce, "REL_WHEEL_DOWN" }, }; +enum claw_rgb_effect_index { + CLAW_RGB_EFFECT_MONOCOLOR, + CLAW_RGB_EFFECT_BREATHE, + CLAW_RGB_EFFECT_CHROMA, + CLAW_RGB_EFFECT_RAINBOW, + CLAW_RGB_EFFECT_FROSTFIRE, +}; + +static const char * const claw_rgb_effect_text[] = { + [CLAW_RGB_EFFECT_MONOCOLOR] = "monocolor", + [CLAW_RGB_EFFECT_BREATHE] = "breathe", + [CLAW_RGB_EFFECT_CHROMA] = "chroma", + [CLAW_RGB_EFFECT_RAINBOW] = "rainbow", + [CLAW_RGB_EFFECT_FROSTFIRE] = "frostfire", +}; + static const u16 button_mapping_addr_old[] = { 0x007a, /* M1 */ 0x011f, /* M2 */ @@ -237,6 +259,9 @@ static const u16 button_mapping_addr_new[] = { 0x0164, /* M2 */ }; +static const u16 rgb_addr_old = 0x01fa; +static const u16 rgb_addr_new = 0x024a; + struct claw_command_report { u8 report_id; u8 padding[2]; @@ -245,6 +270,28 @@ struct claw_command_report { u8 data[59]; } __packed; +struct rgb_zone { + u8 red; + u8 green; + u8 blue; +}; + +struct rgb_frame { + struct rgb_zone zone[CLAW_RGB_ZONES]; +}; + +struct rgb_report { + u8 profile; + __be16 read_addr; + u8 frame_bytes; + u8 padding; + u8 frame_count; + u8 state; /* Always 0x09 */ + u8 speed; + u8 brightness; + struct rgb_frame zone_data; +} __packed; + struct claw_drvdata { /* MCU General Variables */ enum claw_profile_ack_pending profile_pending; @@ -265,6 +312,16 @@ struct claw_drvdata { u8 m2_codes[CLAW_KEYS_MAX]; const u16 *bmap_addr; bool bmap_support; + + /* RGB Variables */ + struct rgb_frame rgb_frames[CLAW_RGB_MAX_FRAMES]; + enum claw_rgb_effect_index rgb_effect; + struct led_classdev_mc led_mc; + struct delayed_work rgb_queue; + u8 rgb_frame_count; + bool rgb_enabled; + u8 rgb_speed; + u16 rgb_addr; }; static int get_endpoint_address(struct hid_device *hdev) @@ -296,8 +353,11 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata, static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep) { - u8 *codes; - int i; + struct rgb_report *frame; + u16 rgb_addr, read_addr; + u8 *codes, f_idx; + u16 frame_calc; + int i, ret = 0; switch (drvdata->profile_pending) { case CLAW_M1_PENDING: @@ -308,15 +368,52 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_ for (i = 0; i < CLAW_KEYS_MAX; i++) codes[i] = (cmd_rep->data[6 + i] != 0xff) ? cmd_rep->data[6 + i] : 0x00; break; + case CLAW_RGB_PENDING: + frame = (struct rgb_report *)cmd_rep->data; + rgb_addr = drvdata->rgb_addr; + read_addr = be16_to_cpu(frame->read_addr); + frame_calc = (read_addr - rgb_addr) / CLAW_RGB_FRAME_OFFSET; + if (frame_calc > U8_MAX) { + dev_err(drvdata->led_mc.led_cdev.dev, "Got unsupported frame index: %x\n", + frame_calc); + ret = -EINVAL; + goto err_pending; + } + f_idx = frame_calc; + + if (f_idx >= CLAW_RGB_MAX_FRAMES) { + dev_err(drvdata->led_mc.led_cdev.dev, "Got illegal frame index: %x\n", + f_idx); + ret = -EINVAL; + goto err_pending; + } + + /* Always treat the first frame as the truth for these constants */ + if (f_idx == 0) { + drvdata->rgb_frame_count = frame->frame_count; + /* Invert device speed (20-0) to sysfs speed (0-20) */ + drvdata->rgb_speed = frame->speed; + drvdata->led_mc.led_cdev.brightness = frame->brightness; + drvdata->led_mc.subled_info[0].intensity = frame->zone_data.zone[0].red; + drvdata->led_mc.subled_info[1].intensity = frame->zone_data.zone[0].green; + drvdata->led_mc.subled_info[2].intensity = frame->zone_data.zone[0].blue; + } + + memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data, + sizeof(struct rgb_frame)); + + break; default: dev_warn(&drvdata->hdev->dev, "Got profile event without changes pending from command: %x\n", cmd_rep->cmd); - return -EINVAL; + ret = -EINVAL; } + +err_pending: drvdata->profile_pending = CLAW_NO_PENDING; - return 0; + return ret; } static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report, @@ -759,6 +856,397 @@ static const struct attribute_group claw_gamepad_attr_group = { .is_visible = claw_gamepad_attr_is_visible, }; +/* Read RGB config from device */ +static int claw_read_rgb_config(struct hid_device *hdev) +{ + u8 data[4] = { 0x01, 0x00, 0x00, CLAW_RGB_FRAME_OFFSET }; + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u16 read_addr = drvdata->rgb_addr; + size_t len = ARRAY_SIZE(data); + int ret, i; + + if (!drvdata->rgb_addr) + return -ENODEV; + + /* Loop through all 8 pages of RGB data */ + guard(mutex)(&drvdata->profile_mutex); + for (i = 0; i < 8; i++) { + drvdata->profile_pending = CLAW_RGB_PENDING; + data[1] = (read_addr >> 8) & 0xff; + data[2] = read_addr & 0x00ff; + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 8); + if (ret) { + drvdata->profile_pending = CLAW_NO_PENDING; + return ret; + } + read_addr += CLAW_RGB_FRAME_OFFSET; + } + + return 0; +} + +/* Send RGB configuration to device */ +static int claw_write_rgb_state(struct claw_drvdata *drvdata) +{ + struct rgb_report report = { 0x01, 0x0000, CLAW_RGB_FRAME_OFFSET, 0x00, + drvdata->rgb_frame_count, 0x09, drvdata->rgb_speed, + drvdata->led_mc.led_cdev.brightness }; + u16 write_addr = drvdata->rgb_addr; + size_t len = sizeof(report); + int f, ret; + + if (!drvdata->rgb_addr) + return -ENODEV; + + if (!drvdata->rgb_frame_count) + return -EINVAL; + + guard(mutex)(&drvdata->rom_mutex); + /* Loop through (up to) 8 pages of RGB data */ + for (f = 0; f < drvdata->rgb_frame_count; f++) { + report.zone_data = drvdata->rgb_frames[f]; + + /* Set the MCU address to write the frame data to */ + report.read_addr = cpu_to_be16(write_addr); + + /* Serialize the rgb_report and write it to MCU */ + ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + (u8 *)&report, len, 8); + if (ret) + return ret; + + /* Increment the write addr by the offset for the next frame */ + write_addr += CLAW_RGB_FRAME_OFFSET; + } + + ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); + + return ret; +} + +/* Fill all zones with the same color */ +static void claw_frame_fill_solid(struct rgb_frame *frame, struct rgb_zone zone) +{ + int z; + + for (z = 0; z < CLAW_RGB_ZONES; z++) + frame->zone[z] = zone; +} + +/* Apply solid effect (1 frame, all zones same color) */ +static int claw_apply_monocolor(struct claw_drvdata *drvdata) +{ + struct mc_subled *subleds = drvdata->led_mc.subled_info; + struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity, + subleds[2].intensity }; + + drvdata->rgb_frame_count = 1; + claw_frame_fill_solid(&drvdata->rgb_frames[0], zone); + + return claw_write_rgb_state(drvdata); +} + +/* Apply breathe effect (2 frames: color -> off) */ +static int claw_apply_breathe(struct claw_drvdata *drvdata) +{ + struct mc_subled *subleds = drvdata->led_mc.subled_info; + struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity, + subleds[2].intensity }; + static const struct rgb_zone off = { 0, 0, 0 }; + + drvdata->rgb_frame_count = 2; + claw_frame_fill_solid(&drvdata->rgb_frames[0], zone); + claw_frame_fill_solid(&drvdata->rgb_frames[1], off); + + return claw_write_rgb_state(drvdata); +} + +/* Apply chroma effect (6 frames: rainbow cycle, all zones sync) */ +static int claw_apply_chroma(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone colors[] = { + {255, 0, 0}, /* red */ + {255, 255, 0}, /* yellow */ + { 0, 255, 0}, /* green */ + { 0, 255, 255}, /* cyan */ + { 0, 0, 255}, /* blue */ + {255, 0, 255}, /* magenta */ + }; + u8 frame_count = ARRAY_SIZE(colors); + int frame; + + drvdata->rgb_frame_count = frame_count; + + for (frame = 0; frame < frame_count; frame++) + claw_frame_fill_solid(&drvdata->rgb_frames[frame], colors[frame]); + + return claw_write_rgb_state(drvdata); +} + +/* Apply rainbow effect (4 frames: rotating colors around joysticks) */ +static int claw_apply_rainbow(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone colors[] = { + {255, 0, 0}, /* red */ + { 0, 255, 0}, /* green */ + { 0, 255, 255}, /* cyan */ + { 0, 0, 255}, /* blue */ + }; + u8 frame_count = ARRAY_SIZE(colors); + int frame, zone; + + drvdata->rgb_frame_count = frame_count; + + for (frame = 0; frame < frame_count; frame++) { + for (zone = 0; zone < 4; zone++) { + drvdata->rgb_frames[frame].zone[zone] = colors[(zone + frame) % 4]; + drvdata->rgb_frames[frame].zone[zone + 4] = colors[(zone + frame) % 4]; + } + drvdata->rgb_frames[frame].zone[8] = colors[frame]; + } + + return claw_write_rgb_state(drvdata); +} + +/* + * Apply frostfire effect (4 frames: fire vs ice rotating) + * Right joystick: fire red -> dark -> ice blue -> dark (clockwise) + * Left joystick: ice blue -> dark -> fire red -> dark (counter-clockwise) + * ABXY: fire red -> dark -> ice blue -> dark + */ +static int claw_apply_frostfire(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone colors[] = { + {255, 0, 0}, /* fire red */ + { 0, 0, 0}, /* dark */ + { 0, 0, 255}, /* ice blue */ + { 0, 0, 0}, /* dark */ + }; + u8 frame_count = ARRAY_SIZE(colors); + int frame, zone; + + drvdata->rgb_frame_count = frame_count; + + for (frame = 0; frame < frame_count; frame++) { + for (zone = 0; zone < 4; zone++) { + drvdata->rgb_frames[frame].zone[zone] = colors[(zone + frame) % 4]; + drvdata->rgb_frames[frame].zone[zone + 4] = colors[(zone - frame + 6) % 4]; + } + drvdata->rgb_frames[frame].zone[8] = colors[frame]; + } + + return claw_write_rgb_state(drvdata); +} + +/* Apply current state to device */ +static int claw_apply_rgb_state(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone off = { 0, 0, 0 }; + + if (!drvdata->rgb_enabled) { + drvdata->rgb_frame_count = 1; + claw_frame_fill_solid(&drvdata->rgb_frames[0], off); + return claw_write_rgb_state(drvdata); + } + + switch (drvdata->rgb_effect) { + case CLAW_RGB_EFFECT_MONOCOLOR: + return claw_apply_monocolor(drvdata); + case CLAW_RGB_EFFECT_BREATHE: + return claw_apply_breathe(drvdata); + case CLAW_RGB_EFFECT_CHROMA: + return claw_apply_chroma(drvdata); + case CLAW_RGB_EFFECT_RAINBOW: + return claw_apply_rainbow(drvdata); + case CLAW_RGB_EFFECT_FROSTFIRE: + return claw_apply_frostfire(drvdata); + default: + dev_err(drvdata->led_mc.led_cdev.dev, + "No supported rgb_effect selected\n"); + return -EINVAL; + } +} + +static void claw_rgb_queue_fn(struct work_struct *work) +{ + struct delayed_work *dwork = container_of(work, struct delayed_work, work); + struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, rgb_queue); + int ret; + + ret = claw_apply_rgb_state(drvdata); + if (ret) + dev_err(drvdata->led_mc.led_cdev.dev, + "Failed to apply RGB state: %d\n", ret); +} + +static ssize_t effect_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + int ret; + + ret = sysfs_match_string(claw_rgb_effect_text, buf); + if (ret < 0) + return ret; + + drvdata->rgb_effect = ret; + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); + + return count; +} + +static ssize_t effect_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + + if (drvdata->rgb_effect >= ARRAY_SIZE(claw_rgb_effect_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", claw_rgb_effect_text[drvdata->rgb_effect]); +} + +static DEVICE_ATTR_RW(effect); + +static ssize_t effect_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int i, count = 0; + + for (i = 0; i < ARRAY_SIZE(claw_rgb_effect_text); i++) + count += sysfs_emit_at(buf, count, "%s ", claw_rgb_effect_text[i]); + + if (count) + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(effect_index); + +static ssize_t enabled_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + bool val; + int ret; + + ret = kstrtobool(buf, &val); + if (ret) + return ret; + + drvdata->rgb_enabled = val; + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); + + return count; +} + +static ssize_t enabled_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + + return sysfs_emit(buf, "%s\n", drvdata->rgb_enabled ? "true" : "false"); +} +static DEVICE_ATTR_RW(enabled); + +static ssize_t enabled_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "true false\n"); +} +static DEVICE_ATTR_RO(enabled_index); + +static ssize_t speed_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + unsigned int val, speed; + int ret; + + ret = kstrtouint(buf, 10, &val); + if (ret) + return ret; + + if (val > 20) + return -EINVAL; + + /* 0 is fastest, invert value for intuitive userspace speed */ + speed = 20 - val; + + drvdata->rgb_speed = speed; + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); + + return count; +} + +static ssize_t speed_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u8 speed = 20 - drvdata->rgb_speed; + + return sysfs_emit(buf, "%u\n", speed); +} +static DEVICE_ATTR_RW(speed); + +static ssize_t speed_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0-20\n"); +} +static DEVICE_ATTR_RO(speed_range); + +static void claw_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness _brightness) +{ + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); +} + +static struct attribute *claw_rgb_attrs[] = { + &dev_attr_effect.attr, + &dev_attr_effect_index.attr, + &dev_attr_enabled.attr, + &dev_attr_enabled_index.attr, + &dev_attr_speed.attr, + &dev_attr_speed_range.attr, + NULL, +}; + +static const struct attribute_group rgb_attr_group = { + .attrs = claw_rgb_attrs, +}; + +static struct mc_subled claw_rgb_subled_info[] = { + { + .color_index = LED_COLOR_ID_RED, + .channel = 0x1, + }, + { + .color_index = LED_COLOR_ID_GREEN, + .channel = 0x2, + }, + { + .color_index = LED_COLOR_ID_BLUE, + .channel = 0x3, + }, +}; + static void cfg_setup_fn(struct work_struct *work) { struct delayed_work *dwork = container_of(work, struct delayed_work, work); @@ -772,6 +1260,13 @@ static void cfg_setup_fn(struct work_struct *work) return; } + ret = claw_read_rgb_config(drvdata->hdev); + if (ret) { + dev_err(drvdata->led_mc.led_cdev.dev, + "Failed to setup device, can't read RGB config: %d\n", ret); + return; + } + /* Add sysfs attributes after we get the device state */ ret = devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group); if (ret) { @@ -780,7 +1275,15 @@ static void cfg_setup_fn(struct work_struct *work) return; } + ret = devm_device_add_group(drvdata->led_mc.led_cdev.dev, &rgb_attr_group); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to setup device, can't create led attributes: %d\n", ret); + return; + } + kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE); + kobject_uevent(&drvdata->led_mc.led_cdev.dev->kobj, KOBJ_CHANGE); } static void cfg_resume_fn(struct work_struct *work) @@ -790,6 +1293,10 @@ static void cfg_resume_fn(struct work_struct *work) u8 data[2] = { drvdata->gamepad_mode, drvdata->mkeys_function }; int ret; + ret = claw_read_rgb_config(drvdata->hdev); + if (ret) + dev_err(drvdata->led_mc.led_cdev.dev, "Failed to read RGB config: %d\n", ret); + ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0); if (ret) @@ -803,18 +1310,24 @@ static void claw_features_supported(struct claw_drvdata *drvdata) if (major == 0x01) { drvdata->bmap_support = true; - if (minor >= 0x66) + if (minor >= 0x66) { drvdata->bmap_addr = button_mapping_addr_new; - else + drvdata->rgb_addr = rgb_addr_new; + } else { drvdata->bmap_addr = button_mapping_addr_old; + drvdata->rgb_addr = rgb_addr_old; + } return; } if ((major == 0x02 && minor >= 0x17) || major >= 0x03) { drvdata->bmap_support = true; drvdata->bmap_addr = button_mapping_addr_new; + drvdata->rgb_addr = rgb_addr_new; return; } + + drvdata->rgb_addr = rgb_addr_old; } static int claw_probe(struct hid_device *hdev, u8 ep) @@ -844,6 +1357,26 @@ static int claw_probe(struct hid_device *hdev, u8 ep) if (!drvdata->bmap_support) dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n"); + /* Initialize RGB LED */ + INIT_DELAYED_WORK(&drvdata->rgb_queue, &claw_rgb_queue_fn); + + drvdata->led_mc.led_cdev.name = "msi_claw:rgb:joystick_rings"; + drvdata->led_mc.led_cdev.brightness = 0x50; + drvdata->led_mc.led_cdev.max_brightness = 0x64; + drvdata->led_mc.led_cdev.color = LED_COLOR_ID_RGB; + drvdata->led_mc.led_cdev.brightness_set = claw_led_brightness_set; + drvdata->led_mc.num_colors = 3; + drvdata->led_mc.subled_info = devm_kmemdup(&hdev->dev, claw_rgb_subled_info, + sizeof(claw_rgb_subled_info), GFP_KERNEL); + if (!drvdata->led_mc.subled_info) + return -ENOMEM; + + drvdata->rgb_enabled = true; + + ret = devm_led_classdev_multicolor_register(&hdev->dev, &drvdata->led_mc); + if (ret) + return ret; + /* For control interface: open the HID transport for sending commands. */ ret = hid_hw_open(hdev); if (ret) @@ -905,6 +1438,9 @@ static void claw_remove(struct hid_device *hdev) return; } + /* Block writes to brightness/multi_intensity during teardown */ + drvdata->led_mc.led_cdev.brightness_set = NULL; + cancel_delayed_work_sync(&drvdata->rgb_queue); cancel_delayed_work_sync(&drvdata->cfg_setup); cancel_delayed_work_sync(&drvdata->cfg_resume); hid_hw_close(hdev); From 4a7c57dd76407ee3e596747774b42691a57e5b09 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Wed, 13 May 2026 23:14:45 +0000 Subject: [PATCH 14/73] [FROM-ML] HID: hid-msi: Add Rumble Intensity Attributes Adds intensity adjustment for the left and right rumble motors. Claude was used during the reverse-engineering data gathering for this feature done by Zhouwang Huang. As the code had already been affected, I used Claude to create the initial framing for the feature, then did manual cleanup of the _show and _store functions afterwards to fix bugs and keep the coding style consistent. Claude was also used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- drivers/hid/hid-msi.c | 147 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index a628b77bfb7b5..cffd6ed253ec9 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -76,6 +76,8 @@ enum claw_profile_ack_pending { CLAW_M1_PENDING, CLAW_M2_PENDING, CLAW_RGB_PENDING, + CLAW_RUMBLE_LEFT_PENDING, + CLAW_RUMBLE_RIGHT_PENDING, }; enum claw_key_index { @@ -262,6 +264,11 @@ static const u16 button_mapping_addr_new[] = { static const u16 rgb_addr_old = 0x01fa; static const u16 rgb_addr_new = 0x024a; +static const u16 rumble_addr[] = { + 0x0022, /* left */ + 0x0023, /* right */ +}; + struct claw_command_report { u8 report_id; u8 padding[2]; @@ -310,7 +317,10 @@ struct claw_drvdata { enum claw_gamepad_mode_index gamepad_mode; u8 m1_codes[CLAW_KEYS_MAX]; u8 m2_codes[CLAW_KEYS_MAX]; + u8 rumble_intensity_right; + u8 rumble_intensity_left; const u16 *bmap_addr; + bool rumble_support; bool bmap_support; /* RGB Variables */ @@ -402,6 +412,12 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_ memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data, sizeof(struct rgb_frame)); + break; + case CLAW_RUMBLE_LEFT_PENDING: + drvdata->rumble_intensity_left = cmd_rep->data[4]; + break; + case CLAW_RUMBLE_RIGHT_PENDING: + drvdata->rumble_intensity_right = cmd_rep->data[4]; break; default: dev_warn(&drvdata->hdev->dev, @@ -815,6 +831,126 @@ static ssize_t button_mapping_options_show(struct device *dev, } static DEVICE_ATTR_RO(button_mapping_options); +static ssize_t rumble_intensity_left_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u8 data[] = { 0x01, (rumble_addr[0] >> 8) & 0xff, rumble_addr[0] & 0xff, 0x01, 0x00 }; + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u8 val; + int ret; + + ret = kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val > 100) + return -EINVAL; + + data[4] = val; + + guard(mutex)(&drvdata->rom_mutex); + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + data, ARRAY_SIZE(data), 8); + if (ret) + return ret; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); + if (ret) + return ret; + + drvdata->rumble_intensity_left = val; + + return count; +} + +static ssize_t rumble_intensity_left_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + u8 data[4] = { 0x01, (rumble_addr[0] >> 8) & 0xff, rumble_addr[0] & 0xff, 0x01 }; + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + int ret; + + guard(mutex)(&drvdata->profile_mutex); + drvdata->profile_pending = CLAW_RUMBLE_LEFT_PENDING; + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, + ARRAY_SIZE(data), 8); + if (ret) { + drvdata->profile_pending = CLAW_NO_PENDING; + return ret; + } + + return sysfs_emit(buf, "%u\n", drvdata->rumble_intensity_left); +} +static DEVICE_ATTR_RW(rumble_intensity_left); + +static ssize_t rumble_intensity_right_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u8 data[] = { 0x01, (rumble_addr[1] >> 8) & 0xff, rumble_addr[1] & 0xff, 0x01, 0x00 }; + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u8 val; + int ret; + + ret = kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val > 100) + return -EINVAL; + + data[4] = val; + + guard(mutex)(&drvdata->rom_mutex); + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + data, ARRAY_SIZE(data), 8); + if (ret) + return ret; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); + if (ret) + return ret; + + drvdata->rumble_intensity_right = val; + + return count; +} + +static ssize_t rumble_intensity_right_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + u8 data[4] = { 0x01, (rumble_addr[1] >> 8) & 0xff, rumble_addr[1] & 0xff, 0x01 }; + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + int ret; + + guard(mutex)(&drvdata->profile_mutex); + drvdata->profile_pending = CLAW_RUMBLE_RIGHT_PENDING; + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, + ARRAY_SIZE(data), 8); + if (ret) { + drvdata->profile_pending = CLAW_NO_PENDING; + return ret; + } + + return sysfs_emit(buf, "%u\n", drvdata->rumble_intensity_right); +} +static DEVICE_ATTR_RW(rumble_intensity_right); + +static ssize_t rumble_intensity_range_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0-100\n"); +} +static DEVICE_ATTR_RO(rumble_intensity_range); + static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n) { @@ -835,6 +971,12 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu attr == &dev_attr_reset.attr) return attr->mode; + /* Hide rumble attrs if not supported */ + if (attr == &dev_attr_rumble_intensity_left.attr || + attr == &dev_attr_rumble_intensity_right.attr || + attr == &dev_attr_rumble_intensity_range.attr) + return drvdata->rumble_support ? attr->mode : 0; + /* Hide button mapping attrs if it isn't supported */ return drvdata->bmap_support ? attr->mode : 0; } @@ -848,6 +990,9 @@ static struct attribute *claw_gamepad_attrs[] = { &dev_attr_mkeys_function.attr, &dev_attr_mkeys_function_index.attr, &dev_attr_reset.attr, + &dev_attr_rumble_intensity_left.attr, + &dev_attr_rumble_intensity_right.attr, + &dev_attr_rumble_intensity_range.attr, NULL, }; @@ -1312,6 +1457,7 @@ static void claw_features_supported(struct claw_drvdata *drvdata) drvdata->bmap_support = true; if (minor >= 0x66) { drvdata->bmap_addr = button_mapping_addr_new; + drvdata->rumble_support = true; drvdata->rgb_addr = rgb_addr_new; } else { drvdata->bmap_addr = button_mapping_addr_old; @@ -1323,6 +1469,7 @@ static void claw_features_supported(struct claw_drvdata *drvdata) if ((major == 0x02 && minor >= 0x17) || major >= 0x03) { drvdata->bmap_support = true; drvdata->bmap_addr = button_mapping_addr_new; + drvdata->rumble_support = true; drvdata->rgb_addr = rgb_addr_new; return; } From 66708554841ada5d9e26654c26f79709a7dabd19 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 18 Apr 2026 21:26:20 -0700 Subject: [PATCH 15/73] [FROM-ML] HID: hid-oxp: Add OneXPlayer configuration driver Adds OneXPlayer HID configuration driver. In this initial driver patch, add the RGB interface for the first generation of HID based RGB control. This interface provides the following attributes: - brightness: provided by the LED core, this works in a fairly unique way on this device. The hardware accepts 5 brightness values (0-4), which affects the brightness of the multicolor and animated effects built into the MCU firmware. For monocolor settings, the device expects the hardware brightness value to be pushed to maximum, then we apply brightness adjustments mathematically based on % (0-100). This leads to some odd conversion as we need the brightness slider to reach the full range, but it has no affect when incrementing between the division points for other effects. - multi-intensity: provided by the LED core for red, green, and blue. - effect: Allows the MCU to set 19 individual effects. - effect_index: Lists the 19 valid effect names for the interface. - enabled: Allows the MCU to toggle the RGB interface on/off. - enabled_index: Lists the valid states for enabled. - speed: Allows the MCU to set the animation rate for the various effects. - speed_range: Lists the valid range of speed (0-9). The MCU also has a few odd quirks that make sending multiple synchronous events challenging. It will essentially freeze if it receives another message before it has finished processing the last command. It also will not reply if you wait on it using a completion. To get around this, we do a 200ms sleep inside a work queue thread and debounce all but the most recent message using a 50ms mod_delayed_work. This will cache the last write, queue the work, then return so userspace can release its write thread. The work queue is only used for brightness/multi-intensity as that is the path likely to receive rapid successive writes. Reviewed-by: Zhouwang Huang Tested-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- MAINTAINERS | 6 + drivers/hid/Kconfig | 12 + drivers/hid/Makefile | 1 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-oxp.c | 652 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 674 insertions(+) create mode 100644 drivers/hid/hid-oxp.c diff --git a/MAINTAINERS b/MAINTAINERS index 52cbf34646fc3..df840f5fa4427 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19937,6 +19937,12 @@ S: Maintained F: drivers/mtd/nand/onenand/ F: include/linux/mtd/onenand*.h +ONEXPLAYER HID DRIVER +M: Derek J. Clark +L: linux-input@vger.kernel.org +S: Maintained +F: drivers/hid/hid-oxp.c + ONEXPLAYER PLATFORM EC DRIVER M: Antheas Kapenekakis M: Derek John Clark diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index ec412bba1e1ed..3eaa45e59cb8e 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -908,6 +908,18 @@ config HID_ORTEK - Ortek WKB-2000 - Skycable wireless presenter +config HID_OXP + tristate "OneXPlayer handheld controller configuration support" + depends on USB_HID + depends on LEDS_CLASS + depends on LEDS_CLASS_MULTICOLOR + help + Say Y here if you would like to enable support for OneXPlayer handheld + devices that come with RGB LED rings around the joysticks and macro buttons. + + To compile this driver as a module, choose M here: the module will + be called hid-oxp. + config HID_PANTHERLORD tristate "Pantherlord/GreenAsia game controller" help diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index c2cb312d8247c..69a97d77a9889 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -99,6 +99,7 @@ obj-$(CONFIG_HID_NTI) += hid-nti.o obj-$(CONFIG_HID_NTRIG) += hid-ntrig.o obj-$(CONFIG_HID_NVIDIA_SHIELD) += hid-nvidia-shield.o obj-$(CONFIG_HID_ORTEK) += hid-ortek.o +obj-$(CONFIG_HID_OXP) += hid-oxp.o obj-$(CONFIG_HID_PRODIKEYS) += hid-prodikeys.o obj-$(CONFIG_HID_PANTHERLORD) += hid-pl.o obj-$(CONFIG_HID_PENMOUNT) += hid-penmount.o diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index f39408c76aa42..291670f784b48 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1137,6 +1137,9 @@ #define USB_VENDOR_ID_NVIDIA 0x0955 #define USB_DEVICE_ID_NVIDIA_THUNDERSTRIKE_CONTROLLER 0x7214 +#define USB_VENDOR_ID_CRSC 0x1a2c +#define USB_DEVICE_ID_ONEXPLAYER_GEN1 0xb001 + #define USB_VENDOR_ID_ONTRAK 0x0a07 #define USB_DEVICE_ID_ONTRAK_ADU100 0x0064 diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c new file mode 100644 index 0000000000000..f72bc74a7e6e1 --- /dev/null +++ b/drivers/hid/hid-oxp.c @@ -0,0 +1,652 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for OneXPlayer gamepad configuration devices. + * + * Copyright (c) 2026 Valve Corporation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hid-ids.h" + +#define OXP_PACKET_SIZE 64 + +#define GEN1_MESSAGE_ID 0xff + +#define GEN1_USAGE_PAGE 0xff01 + +enum oxp_function_index { + OXP_FID_GEN1_RGB_SET = 0x07, + OXP_FID_GEN1_RGB_REPLY = 0x0f, +}; + +static struct oxp_hid_cfg { + struct delayed_work oxp_rgb_queue; + struct led_classdev_mc *led_mc; + struct hid_device *hdev; + struct mutex cfg_mutex; /*ensure single synchronous output report*/ + u8 rgb_brightness; + u8 rgb_effect; + u8 rgb_speed; + u8 rgb_en; +} drvdata; + +enum oxp_feature_en_index { + OXP_FEAT_DISABLED, + OXP_FEAT_ENABLED, +}; + +static const char *const oxp_feature_en_text[] = { + [OXP_FEAT_DISABLED] = "false", + [OXP_FEAT_ENABLED] = "true", +}; + +enum oxp_rgb_effect_index { + OXP_UNKNOWN, + OXP_EFFECT_AURORA, + OXP_EFFECT_BIRTHDAY, + OXP_EFFECT_FLOWING, + OXP_EFFECT_CHROMA_1, + OXP_EFFECT_NEON, + OXP_EFFECT_CHROMA_2, + OXP_EFFECT_DREAMY, + OXP_EFFECT_WARM, + OXP_EFFECT_CYBERPUNK, + OXP_EFFECT_SEA, + OXP_EFFECT_SUNSET, + OXP_EFFECT_COLORFUL, + OXP_EFFECT_MONSTER, + OXP_EFFECT_GREEN, + OXP_EFFECT_BLUE, + OXP_EFFECT_YELLOW, + OXP_EFFECT_TEAL, + OXP_EFFECT_PURPLE, + OXP_EFFECT_FOGGY, + OXP_EFFECT_MONO_LIST, /* placeholder for effect_index_show */ +}; + +/* These belong to rgb_effect_index, but we want to hide them from + * rgb_effect_text + */ + +#define OXP_GET_PROPERTY 0xfc +#define OXP_SET_PROPERTY 0xfd +#define OXP_EFFECT_MONO_TRUE 0xfe /* actual index for monocolor */ + +static const char *const oxp_rgb_effect_text[] = { + [OXP_UNKNOWN] = "unknown", + [OXP_EFFECT_AURORA] = "aurora", + [OXP_EFFECT_BIRTHDAY] = "birthday_cake", + [OXP_EFFECT_FLOWING] = "flowing_light", + [OXP_EFFECT_CHROMA_1] = "chroma_popping", + [OXP_EFFECT_NEON] = "neon", + [OXP_EFFECT_CHROMA_2] = "chroma_breathing", + [OXP_EFFECT_DREAMY] = "dreamy", + [OXP_EFFECT_WARM] = "warm_sun", + [OXP_EFFECT_CYBERPUNK] = "cyberpunk", + [OXP_EFFECT_SEA] = "sea_foam", + [OXP_EFFECT_SUNSET] = "sunset_afterglow", + [OXP_EFFECT_COLORFUL] = "colorful", + [OXP_EFFECT_MONSTER] = "monster_woke", + [OXP_EFFECT_GREEN] = "green_breathing", + [OXP_EFFECT_BLUE] = "blue_breathing", + [OXP_EFFECT_YELLOW] = "yellow_breathing", + [OXP_EFFECT_TEAL] = "teal_breathing", + [OXP_EFFECT_PURPLE] = "purple_breathing", + [OXP_EFFECT_FOGGY] = "foggy_haze", + [OXP_EFFECT_MONO_LIST] = "monocolor", +}; + +struct oxp_gen_1_rgb_report { + u8 report_id; + u8 message_id; + u8 padding_2[2]; + u8 effect; + u8 enabled; + u8 speed; + u8 brightness; + u8 red; + u8 green; + u8 blue; +} __packed; + +static u16 get_usage_page(struct hid_device *hdev) +{ + return hdev->collection[0].usage >> 16; +} + +static int oxp_hid_raw_event_gen_1(struct hid_device *hdev, + struct hid_report *report, u8 *data, + int size) +{ + struct led_classdev_mc *led_mc = drvdata.led_mc; + struct oxp_gen_1_rgb_report *rgb_rep; + + if (data[1] != OXP_FID_GEN1_RGB_REPLY) + return 0; + + rgb_rep = (struct oxp_gen_1_rgb_report *)data; + /* Ensure we save monocolor as the list value */ + drvdata.rgb_effect = rgb_rep->effect == OXP_EFFECT_MONO_TRUE ? + OXP_EFFECT_MONO_LIST : + rgb_rep->effect; + drvdata.rgb_speed = rgb_rep->speed; + drvdata.rgb_en = rgb_rep->enabled == 0 ? OXP_FEAT_DISABLED : + OXP_FEAT_ENABLED; + drvdata.rgb_brightness = rgb_rep->brightness; + led_mc->led_cdev.brightness = rgb_rep->brightness / 4 * + led_mc->led_cdev.max_brightness; + /* If monocolor had less than 100% brightness on the previous boot, + * there will be no reliable way to determine the real intensity. + * Since intensity scaling is used with a hardware brightness set at max, + * our brightness will always look like 100%. Use the last set value to + * prevent successive boots from lowering the brightness further. + * Brightness will be "wrong" but the effect will remain the same visually. + */ + led_mc->subled_info[0].intensity = rgb_rep->red; + led_mc->subled_info[1].intensity = rgb_rep->green; + led_mc->subled_info[2].intensity = rgb_rep->blue; + + return 0; +} + +static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report, + u8 *data, int size) +{ + u16 up = get_usage_page(hdev); + + dev_dbg(&hdev->dev, "raw event data: [%*ph]\n", OXP_PACKET_SIZE, data); + + switch (up) { + case GEN1_USAGE_PAGE: + return oxp_hid_raw_event_gen_1(hdev, report, data, size); + default: + break; + } + + return 0; +} + +static int mcu_property_out(u8 *header, size_t header_size, u8 *data, + size_t data_size, u8 *footer, size_t footer_size) +{ + unsigned char *dmabuf __free(kfree) = kzalloc(OXP_PACKET_SIZE, GFP_KERNEL); + int ret; + + if (!dmabuf) + return -ENOMEM; + + if (header_size + data_size + footer_size > OXP_PACKET_SIZE) + return -EINVAL; + + guard(mutex)(&drvdata.cfg_mutex); + memcpy(dmabuf, header, header_size); + memcpy(dmabuf + header_size, data, data_size); + if (footer_size) + memcpy(dmabuf + OXP_PACKET_SIZE - footer_size, footer, footer_size); + + dev_dbg(&drvdata.hdev->dev, "raw data: [%*ph]\n", OXP_PACKET_SIZE, dmabuf); + + ret = hid_hw_output_report(drvdata.hdev, dmabuf, OXP_PACKET_SIZE); + if (ret < 0) + return ret; + + /* MCU takes 200ms to be ready for another command. */ + msleep(200); + return ret == OXP_PACKET_SIZE ? 0 : -EIO; +} + +static int oxp_gen_1_property_out(enum oxp_function_index fid, u8 *data, + u8 data_size) +{ + u8 header[] = { fid, GEN1_MESSAGE_ID }; + size_t header_size = ARRAY_SIZE(header); + + return mcu_property_out(header, header_size, data, data_size, NULL, 0); +} + +static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness) +{ + u16 up = get_usage_page(drvdata.hdev); + u8 *data; + + /* Always default to max brightness and use intensity scaling when in + * monocolor mode. + */ + switch (up) { + case GEN1_USAGE_PAGE: + data = (u8[4]) { OXP_SET_PROPERTY, enabled, speed, brightness }; + if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST) + data[3] = 0x04; + return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 4); + default: + return -ENODEV; + } +} + +static ssize_t oxp_rgb_status_show(void) +{ + u16 up = get_usage_page(drvdata.hdev); + u8 *data; + + switch (up) { + case GEN1_USAGE_PAGE: + data = (u8[1]) { OXP_GET_PROPERTY }; + return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1); + default: + return -ENODEV; + } +} + +static int oxp_rgb_color_set(void) +{ + u8 max_br = drvdata.led_mc->led_cdev.max_brightness; + u8 br = drvdata.led_mc->led_cdev.brightness; + u16 up = get_usage_page(drvdata.hdev); + u8 green, red, blue; + size_t size; + u8 *data; + int i; + + red = br * drvdata.led_mc->subled_info[0].intensity / max_br; + green = br * drvdata.led_mc->subled_info[1].intensity / max_br; + blue = br * drvdata.led_mc->subled_info[2].intensity / max_br; + + switch (up) { + case GEN1_USAGE_PAGE: + size = 55; + data = (u8[55]) { OXP_EFFECT_MONO_TRUE }; + + for (i = 0; i < (size - 1) / 3; i++) { + data[3 * i + 1] = red; + data[3 * i + 2] = green; + data[3 * i + 3] = blue; + } + return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, size); + default: + return -ENODEV; + } +} + +static int oxp_rgb_effect_set(u8 effect) +{ + u16 up = get_usage_page(drvdata.hdev); + u8 *data; + int ret; + + switch (effect) { + case OXP_EFFECT_AURORA: + case OXP_EFFECT_BIRTHDAY: + case OXP_EFFECT_FLOWING: + case OXP_EFFECT_CHROMA_1: + case OXP_EFFECT_NEON: + case OXP_EFFECT_CHROMA_2: + case OXP_EFFECT_DREAMY: + case OXP_EFFECT_WARM: + case OXP_EFFECT_CYBERPUNK: + case OXP_EFFECT_SEA: + case OXP_EFFECT_SUNSET: + case OXP_EFFECT_COLORFUL: + case OXP_EFFECT_MONSTER: + case OXP_EFFECT_GREEN: + case OXP_EFFECT_BLUE: + case OXP_EFFECT_YELLOW: + case OXP_EFFECT_TEAL: + case OXP_EFFECT_PURPLE: + case OXP_EFFECT_FOGGY: + switch (up) { + case GEN1_USAGE_PAGE: + data = (u8[1]) { effect }; + ret = oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1); + break; + default: + ret = -ENODEV; + } + break; + case OXP_EFFECT_MONO_LIST: + ret = oxp_rgb_color_set(); + break; + default: + return -EINVAL; + } + + if (ret) + return ret; + + drvdata.rgb_effect = effect; + + return 0; +} + +static ssize_t enabled_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + u8 val; + + ret = sysfs_match_string(oxp_feature_en_text, buf); + if (ret < 0) + return ret; + val = ret; + + ret = oxp_rgb_status_store(val, drvdata.rgb_speed, + drvdata.rgb_brightness); + if (ret) + return ret; + + drvdata.rgb_en = val; + return count; +} + +static ssize_t enabled_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int ret; + + ret = oxp_rgb_status_show(); + if (ret) + return ret; + + if (drvdata.rgb_en >= ARRAY_SIZE(oxp_feature_en_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", oxp_feature_en_text[drvdata.rgb_en]); +} +static DEVICE_ATTR_RW(enabled); + +static ssize_t enabled_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + size_t count = 0; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(oxp_feature_en_text); i++) + count += sysfs_emit_at(buf, count, "%s ", oxp_feature_en_text[i]); + + if (count) + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(enabled_index); + +static ssize_t effect_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + u8 val; + + ret = sysfs_match_string(oxp_rgb_effect_text, buf); + if (ret < 0) + return ret; + + val = ret; + + ret = oxp_rgb_status_store(drvdata.rgb_en, drvdata.rgb_speed, + drvdata.rgb_brightness); + if (ret) + return ret; + + ret = oxp_rgb_effect_set(val); + if (ret) + return ret; + + return count; +} + +static ssize_t effect_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int ret; + + ret = oxp_rgb_status_show(); + if (ret) + return ret; + + if (drvdata.rgb_effect >= ARRAY_SIZE(oxp_rgb_effect_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", oxp_rgb_effect_text[drvdata.rgb_effect]); +} + +static DEVICE_ATTR_RW(effect); + +static ssize_t effect_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + size_t count = 0; + unsigned int i; + + for (i = 1; i < ARRAY_SIZE(oxp_rgb_effect_text); i++) + count += sysfs_emit_at(buf, count, "%s ", oxp_rgb_effect_text[i]); + + if (count) + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(effect_index); + +static ssize_t speed_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + u8 val; + + ret = kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val > 9) + return -EINVAL; + + ret = oxp_rgb_status_store(drvdata.rgb_en, val, drvdata.rgb_brightness); + if (ret) + return ret; + + drvdata.rgb_speed = val; + return count; +} + +static ssize_t speed_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int ret; + + ret = oxp_rgb_status_show(); + if (ret) + return ret; + + if (drvdata.rgb_speed > 9) + return -EINVAL; + + return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed); +} +static DEVICE_ATTR_RW(speed); + +static ssize_t speed_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0-9\n"); +} +static DEVICE_ATTR_RO(speed_range); + +static void oxp_rgb_queue_fn(struct work_struct *work) +{ + unsigned int max_brightness = drvdata.led_mc->led_cdev.max_brightness; + unsigned int brightness = drvdata.led_mc->led_cdev.brightness; + u8 val = 4 * brightness / max_brightness; + int ret; + + if (drvdata.rgb_brightness != val) { + ret = oxp_rgb_status_store(drvdata.rgb_en, drvdata.rgb_speed, val); + if (ret) + dev_err(drvdata.led_mc->led_cdev.dev, + "Error: Failed to write RGB Status: %i\n", ret); + + drvdata.rgb_brightness = val; + } + + if (drvdata.rgb_effect != OXP_EFFECT_MONO_LIST) + return; + + ret = oxp_rgb_effect_set(drvdata.rgb_effect); + if (ret) + dev_err(drvdata.led_mc->led_cdev.dev, "Error: Failed to write RGB color: %i\n", + ret); +} + +static void oxp_rgb_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + led_cdev->brightness = brightness; + mod_delayed_work(system_wq, &drvdata.oxp_rgb_queue, msecs_to_jiffies(50)); +} + +static struct attribute *oxp_rgb_attrs[] = { + &dev_attr_effect.attr, + &dev_attr_effect_index.attr, + &dev_attr_enabled.attr, + &dev_attr_enabled_index.attr, + &dev_attr_speed.attr, + &dev_attr_speed_range.attr, + NULL, +}; + +static const struct attribute_group oxp_rgb_attr_group = { + .attrs = oxp_rgb_attrs, +}; + +static struct mc_subled oxp_rgb_subled_info[] = { + { + .color_index = LED_COLOR_ID_RED, + .intensity = 0x24, + .channel = 0x1, + }, + { + .color_index = LED_COLOR_ID_GREEN, + .intensity = 0x22, + .channel = 0x2, + }, + { + .color_index = LED_COLOR_ID_BLUE, + .intensity = 0x99, + .channel = 0x3, + }, +}; + +static struct led_classdev_mc oxp_cdev_rgb = { + .led_cdev = { + .name = "oxp:rgb:joystick_rings", + .color = LED_COLOR_ID_RGB, + .brightness = 0x64, + .max_brightness = 0x64, + .brightness_set = oxp_rgb_brightness_set, + }, + .num_colors = ARRAY_SIZE(oxp_rgb_subled_info), + .subled_info = oxp_rgb_subled_info, +}; + +static int oxp_cfg_probe(struct hid_device *hdev, u16 up) +{ + int ret; + + hid_set_drvdata(hdev, &drvdata); + mutex_init(&drvdata.cfg_mutex); + drvdata.hdev = hdev; + drvdata.led_mc = &oxp_cdev_rgb; + + INIT_DELAYED_WORK(&drvdata.oxp_rgb_queue, oxp_rgb_queue_fn); + ret = devm_led_classdev_multicolor_register(&hdev->dev, &oxp_cdev_rgb); + if (ret) + return dev_err_probe(&hdev->dev, ret, + "Failed to create RGB device\n"); + + ret = devm_device_add_group(drvdata.led_mc->led_cdev.dev, + &oxp_rgb_attr_group); + if (ret) + return dev_err_probe(drvdata.led_mc->led_cdev.dev, ret, + "Failed to create RGB configuration attributes\n"); + + ret = oxp_rgb_status_show(); + if (ret) + dev_warn(drvdata.led_mc->led_cdev.dev, + "Failed to query RGB initial state: %i\n", ret); + + return 0; +} + +static int oxp_hid_probe(struct hid_device *hdev, + const struct hid_device_id *id) +{ + int ret; + u16 up; + + ret = hid_parse(hdev); + if (ret) + return dev_err_probe(&hdev->dev, ret, "Failed to parse HID device\n"); + + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) + return dev_err_probe(&hdev->dev, ret, "Failed to start HID device\n"); + + ret = hid_hw_open(hdev); + if (ret) { + hid_hw_stop(hdev); + return dev_err_probe(&hdev->dev, ret, "Failed to open HID device\n"); + } + + up = get_usage_page(hdev); + dev_dbg(&hdev->dev, "Got usage page %04x\n", up); + + switch (up) { + case GEN1_USAGE_PAGE: + ret = oxp_cfg_probe(hdev, up); + if (ret) { + hid_hw_close(hdev); + hid_hw_stop(hdev); + } + + return ret; + default: + return 0; + } +} + +static void oxp_hid_remove(struct hid_device *hdev) +{ + cancel_delayed_work(&drvdata.oxp_rgb_queue); + hid_hw_close(hdev); + hid_hw_stop(hdev); +} + +static const struct hid_device_id oxp_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_CRSC, USB_DEVICE_ID_ONEXPLAYER_GEN1) }, + {} +}; + +MODULE_DEVICE_TABLE(hid, oxp_devices); +static struct hid_driver hid_oxp = { + .name = "hid-oxp", + .id_table = oxp_devices, + .probe = oxp_hid_probe, + .remove = oxp_hid_remove, + .raw_event = oxp_hid_raw_event, +}; +module_hid_driver(hid_oxp); + +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("Driver for OneXPlayer HID Interfaces"); +MODULE_LICENSE("GPL"); From 7b609427b293d3f5933fcd9848344a4b3983fcf9 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 18 Apr 2026 21:26:21 -0700 Subject: [PATCH 16/73] [FROM-ML] HID: hid-oxp: Add Second Generation RGB Control Adds support for the second generation of RGB Control for OneXPlayer devices. The interface mirrors the first generation, with some differences to how messages are formatted. Some devices have both a GEN1 MCU for RGB control and a GEN2 MCU for button mapping. To avoid conflicts, quirk these devices to skip RGB setup for the GEN2_USAGE_PAGE. Reviewed-by: Zhouwang Huang Tested-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- drivers/hid/Kconfig | 1 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-oxp.c | 151 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+) diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index 3eaa45e59cb8e..fb380ff6081a3 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -913,6 +913,7 @@ config HID_OXP depends on USB_HID depends on LEDS_CLASS depends on LEDS_CLASS_MULTICOLOR + depends on DMI help Say Y here if you would like to enable support for OneXPlayer handheld devices that come with RGB LED rings around the joysticks and macro buttons. diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 291670f784b48..c196d57931995 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1140,6 +1140,9 @@ #define USB_VENDOR_ID_CRSC 0x1a2c #define USB_DEVICE_ID_ONEXPLAYER_GEN1 0xb001 +#define USB_VENDOR_ID_WCH 0x1a86 +#define USB_DEVICE_ID_ONEXPLAYER_GEN2 0xfe00 + #define USB_VENDOR_ID_ONTRAK 0x0a07 #define USB_DEVICE_ID_ONTRAK_ADU100 0x0064 diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c index f72bc74a7e6e1..835de2118e3c5 100644 --- a/drivers/hid/hid-oxp.c +++ b/drivers/hid/hid-oxp.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -24,12 +25,15 @@ #define OXP_PACKET_SIZE 64 #define GEN1_MESSAGE_ID 0xff +#define GEN2_MESSAGE_ID 0x3f #define GEN1_USAGE_PAGE 0xff01 +#define GEN2_USAGE_PAGE 0xff00 enum oxp_function_index { OXP_FID_GEN1_RGB_SET = 0x07, OXP_FID_GEN1_RGB_REPLY = 0x0f, + OXP_FID_GEN2_STATUS_EVENT = 0xb8, }; static struct oxp_hid_cfg { @@ -122,6 +126,22 @@ struct oxp_gen_1_rgb_report { u8 blue; } __packed; +struct oxp_gen_2_rgb_report { + u8 report_id; + u8 header_id; + u8 padding_2; + u8 message_id; + u8 padding_4[2]; + u8 enabled; + u8 speed; + u8 brightness; + u8 red; + u8 green; + u8 blue; + u8 padding_12[3]; + u8 effect; +} __packed; + static u16 get_usage_page(struct hid_device *hdev) { return hdev->collection[0].usage >> 16; @@ -162,6 +182,44 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev, return 0; } +static int oxp_hid_raw_event_gen_2(struct hid_device *hdev, + struct hid_report *report, u8 *data, + int size) +{ + struct led_classdev_mc *led_mc = drvdata.led_mc; + struct oxp_gen_2_rgb_report *rgb_rep; + + if (data[0] != OXP_FID_GEN2_STATUS_EVENT) + return 0; + + if (data[3] != OXP_GET_PROPERTY) + return 0; + + rgb_rep = (struct oxp_gen_2_rgb_report *)data; + /* Ensure we save monocolor as the list value */ + drvdata.rgb_effect = rgb_rep->effect == OXP_EFFECT_MONO_TRUE ? + OXP_EFFECT_MONO_LIST : + rgb_rep->effect; + drvdata.rgb_speed = rgb_rep->speed; + drvdata.rgb_en = rgb_rep->enabled == 0 ? OXP_FEAT_DISABLED : + OXP_FEAT_ENABLED; + drvdata.rgb_brightness = rgb_rep->brightness; + led_mc->led_cdev.brightness = rgb_rep->brightness / 4 * + led_mc->led_cdev.max_brightness; + /* If monocolor had less than 100% brightness on the previous boot, + * there will be no reliable way to determine the real intensity. + * Since intensity scaling is used with a hardware brightness set at max, + * our brightness will always look like 100%. Use the last set value to + * prevent successive boots from lowering the brightness further. + * Brightness will be "wrong" but the effect will remain the same visually. + */ + led_mc->subled_info[0].intensity = rgb_rep->red; + led_mc->subled_info[1].intensity = rgb_rep->green; + led_mc->subled_info[2].intensity = rgb_rep->blue; + + return 0; +} + static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) { @@ -172,6 +230,8 @@ static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report, switch (up) { case GEN1_USAGE_PAGE: return oxp_hid_raw_event_gen_1(hdev, report, data, size); + case GEN2_USAGE_PAGE: + return oxp_hid_raw_event_gen_2(hdev, report, data, size); default: break; } @@ -217,6 +277,18 @@ static int oxp_gen_1_property_out(enum oxp_function_index fid, u8 *data, return mcu_property_out(header, header_size, data, data_size, NULL, 0); } +static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, + u8 data_size) +{ + u8 header[] = { fid, GEN2_MESSAGE_ID, 0x01 }; + u8 footer[] = { GEN2_MESSAGE_ID, fid }; + size_t header_size = ARRAY_SIZE(header); + size_t footer_size = ARRAY_SIZE(footer); + + return mcu_property_out(header, header_size, data, data_size, footer, + footer_size); +} + static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness) { u16 up = get_usage_page(drvdata.hdev); @@ -231,6 +303,11 @@ static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness) if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST) data[3] = 0x04; return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 4); + case GEN2_USAGE_PAGE: + data = (u8[6]) { OXP_SET_PROPERTY, 0x00, 0x02, enabled, speed, brightness }; + if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST) + data[5] = 0x04; + return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 6); default: return -ENODEV; } @@ -245,6 +322,9 @@ static ssize_t oxp_rgb_status_show(void) case GEN1_USAGE_PAGE: data = (u8[1]) { OXP_GET_PROPERTY }; return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1); + case GEN2_USAGE_PAGE: + data = (u8[3]) { OXP_GET_PROPERTY, 0x00, 0x02 }; + return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 3); default: return -ENODEV; } @@ -275,6 +355,16 @@ static int oxp_rgb_color_set(void) data[3 * i + 3] = blue; } return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, size); + case GEN2_USAGE_PAGE: + size = 57; + data = (u8[57]) { OXP_EFFECT_MONO_TRUE, 0x00, 0x02 }; + + for (i = 1; i < size / 3; i++) { + data[3 * i] = red; + data[3 * i + 1] = green; + data[3 * i + 2] = blue; + } + return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, size); default: return -ENODEV; } @@ -311,6 +401,10 @@ static int oxp_rgb_effect_set(u8 effect) data = (u8[1]) { effect }; ret = oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1); break; + case GEN2_USAGE_PAGE: + data = (u8[3]) { effect, 0x00, 0x02 }; + ret = oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 3); + break; default: ret = -ENODEV; } @@ -559,6 +653,56 @@ static struct led_classdev_mc oxp_cdev_rgb = { .subled_info = oxp_rgb_subled_info, }; +struct quirk_entry { + bool hybrid_mcu; +}; + +static struct quirk_entry quirk_hybrid_mcu = { + .hybrid_mcu = true, +}; + +static const struct dmi_system_id oxp_hybrid_mcu_list[] = { + { + .ident = "OneXPlayer Apex", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"), + DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER APEX"), + }, + .driver_data = &quirk_hybrid_mcu, + }, + { + .ident = "OneXPlayer G1 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"), + DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER G1 A"), + }, + .driver_data = &quirk_hybrid_mcu, + }, + { + .ident = "OneXPlayer G1 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"), + DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER G1 i"), + }, + .driver_data = &quirk_hybrid_mcu, + }, + {}, +}; + +static bool oxp_hybrid_mcu_device(void) +{ + const struct dmi_system_id *dmi_id; + struct quirk_entry *quirks; + + dmi_id = dmi_first_match(oxp_hybrid_mcu_list); + if (!dmi_id) + return false; + + quirks = dmi_id->driver_data; + + return quirks->hybrid_mcu; +} + static int oxp_cfg_probe(struct hid_device *hdev, u16 up) { int ret; @@ -566,6 +710,10 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up) hid_set_drvdata(hdev, &drvdata); mutex_init(&drvdata.cfg_mutex); drvdata.hdev = hdev; + + if (up == GEN2_USAGE_PAGE && oxp_hybrid_mcu_device()) + goto skip_rgb; + drvdata.led_mc = &oxp_cdev_rgb; INIT_DELAYED_WORK(&drvdata.oxp_rgb_queue, oxp_rgb_queue_fn); @@ -585,6 +733,7 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up) dev_warn(drvdata.led_mc->led_cdev.dev, "Failed to query RGB initial state: %i\n", ret); +skip_rgb: return 0; } @@ -613,6 +762,7 @@ static int oxp_hid_probe(struct hid_device *hdev, switch (up) { case GEN1_USAGE_PAGE: + case GEN2_USAGE_PAGE: ret = oxp_cfg_probe(hdev, up); if (ret) { hid_hw_close(hdev); @@ -634,6 +784,7 @@ static void oxp_hid_remove(struct hid_device *hdev) static const struct hid_device_id oxp_devices[] = { { HID_USB_DEVICE(USB_VENDOR_ID_CRSC, USB_DEVICE_ID_ONEXPLAYER_GEN1) }, + { HID_USB_DEVICE(USB_VENDOR_ID_WCH, USB_DEVICE_ID_ONEXPLAYER_GEN2) }, {} }; From 57c16fc3072ac0faac3607d724c83f37265b7959 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 18 Apr 2026 21:26:22 -0700 Subject: [PATCH 17/73] [FROM-ML] HID: hid-oxp: Add Second Generation Gamepad Mode Switch Adds "gamepad_mode" attribute to second generation OneXPlayer configuration HID devices. This attribute initiates a mode shift in the device MCU that puts it into a state where all events are routed to an hidraw interface instead of the xpad evdev interface. This allows for debugging the hardware input mapping added in the next patch. Reviewed-by: Zhouwang Huang Tested-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- drivers/hid/hid-oxp.c | 131 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c index 835de2118e3c5..2504b56b8f8a8 100644 --- a/drivers/hid/hid-oxp.c +++ b/drivers/hid/hid-oxp.c @@ -33,20 +33,33 @@ enum oxp_function_index { OXP_FID_GEN1_RGB_SET = 0x07, OXP_FID_GEN1_RGB_REPLY = 0x0f, + OXP_FID_GEN2_TOGGLE_MODE = 0xb2, OXP_FID_GEN2_STATUS_EVENT = 0xb8, }; static struct oxp_hid_cfg { struct delayed_work oxp_rgb_queue; + struct delayed_work oxp_mcu_init; struct led_classdev_mc *led_mc; struct hid_device *hdev; struct mutex cfg_mutex; /*ensure single synchronous output report*/ u8 rgb_brightness; + u8 gamepad_mode; u8 rgb_effect; u8 rgb_speed; u8 rgb_en; } drvdata; +enum oxp_gamepad_mode_index { + OXP_GP_MODE_XINPUT = 0x00, + OXP_GP_MODE_DEBUG = 0x03, +}; + +static const char *const oxp_gamepad_mode_text[] = { + [OXP_GP_MODE_XINPUT] = "xinput", + [OXP_GP_MODE_DEBUG] = "debug", +}; + enum oxp_feature_en_index { OXP_FEAT_DISABLED, OXP_FEAT_ENABLED, @@ -182,6 +195,30 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev, return 0; } +static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size); + +static void oxp_mcu_init_fn(struct work_struct *work) +{ + u8 gp_mode_data[3] = { OXP_GP_MODE_DEBUG, 0x01, 0x02 }; + int ret; + + /* Cycle the gamepad mode */ + ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3); + if (ret) + dev_err(&drvdata.hdev->dev, + "Error: Failed to set gamepad mode: %i\n", ret); + + /* Remainder only applies for xinput mode */ + if (drvdata.gamepad_mode == OXP_GP_MODE_DEBUG) + return; + + gp_mode_data[0] = OXP_GP_MODE_XINPUT; + ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3); + if (ret) + dev_err(&drvdata.hdev->dev, + "Error: Failed to set gamepad mode: %i\n", ret); +} + static int oxp_hid_raw_event_gen_2(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) @@ -192,6 +229,14 @@ static int oxp_hid_raw_event_gen_2(struct hid_device *hdev, if (data[0] != OXP_FID_GEN2_STATUS_EVENT) return 0; + /* Sent ~6s after resume event, indicating the MCU has fully reset. + * Re-apply our settings after this has been received. + */ + if (data[3] == OXP_EFFECT_MONO_TRUE) { + mod_delayed_work(system_wq, &drvdata.oxp_mcu_init, msecs_to_jiffies(50)); + return 0; + } + if (data[3] != OXP_GET_PROPERTY) return 0; @@ -289,6 +334,77 @@ static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, footer_size); } +static ssize_t gamepad_mode_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + u16 up = get_usage_page(drvdata.hdev); + u8 data[3] = { 0x00, 0x01, 0x02 }; + int ret = -EINVAL; + int i; + + if (up != GEN2_USAGE_PAGE) + return ret; + + for (i = 0; i < ARRAY_SIZE(oxp_gamepad_mode_text); i++) { + if (oxp_gamepad_mode_text[i] && sysfs_streq(buf, oxp_gamepad_mode_text[i])) { + ret = i; + break; + } + } + if (ret < 0) + return ret; + + data[0] = ret; + + ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, data, 3); + if (ret) + return ret; + + drvdata.gamepad_mode = data[0]; + + return count; +} + +static ssize_t gamepad_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%s\n", oxp_gamepad_mode_text[drvdata.gamepad_mode]); +} +static DEVICE_ATTR_RW(gamepad_mode); + +static ssize_t gamepad_mode_index_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + ssize_t count = 0; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(oxp_gamepad_mode_text); i++) { + if (!oxp_gamepad_mode_text[i] || + oxp_gamepad_mode_text[i][0] == '\0') + continue; + + count += sysfs_emit_at(buf, count, "%s ", oxp_gamepad_mode_text[i]); + } + + if (count) + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(gamepad_mode_index); + +static struct attribute *oxp_cfg_attrs[] = { + &dev_attr_gamepad_mode.attr, + &dev_attr_gamepad_mode_index.attr, + NULL, +}; + +static const struct attribute_group oxp_cfg_attrs_group = { + .attrs = oxp_cfg_attrs, +}; + static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness) { u16 up = get_usage_page(drvdata.hdev); @@ -733,7 +849,21 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up) dev_warn(drvdata.led_mc->led_cdev.dev, "Failed to query RGB initial state: %i\n", ret); + /* Below features are only implemented in gen 2 */ + if (up != GEN2_USAGE_PAGE) + return 0; + skip_rgb: + drvdata.gamepad_mode = OXP_GP_MODE_XINPUT; + + INIT_DELAYED_WORK(&drvdata.oxp_mcu_init, oxp_mcu_init_fn); + mod_delayed_work(system_wq, &drvdata.oxp_mcu_init, msecs_to_jiffies(50)); + + ret = devm_device_add_group(&hdev->dev, &oxp_cfg_attrs_group); + if (ret) + return dev_err_probe(&hdev->dev, ret, + "Failed to attach configuration attributes\n"); + return 0; } @@ -778,6 +908,7 @@ static int oxp_hid_probe(struct hid_device *hdev, static void oxp_hid_remove(struct hid_device *hdev) { cancel_delayed_work(&drvdata.oxp_rgb_queue); + cancel_delayed_work(&drvdata.oxp_mcu_init); hid_hw_close(hdev); hid_hw_stop(hdev); } From df286120901319ade71099dbc359186aefa77496 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 18 Apr 2026 21:26:23 -0700 Subject: [PATCH 18/73] [FROM-ML] HID: hid-oxp: Add Button Mapping Interface Adds button mapping interface for second generation OneXPlayer configuration HID interfaces. This interface allows the MCU to swap button mappings at the hardware level. The current state cannot be retrieved, and the mappings may have been modified in Windows prior, so we reset the button mapping at init and expose an attribute to allow userspace to do this again at any time. The interface requires two pages of button mapping data to be sent before the settings will take place. Since the MCU requires a 200ms delay after each message (total 400ms for these attributes) use the same debounce work queue method we used for RGB. This will allow for userspace or udev rules to rapidly map all buttons. The values will be cached before the final write is finally sent to the device. Reviewed-by: Zhouwang Huang Tested-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- drivers/hid/hid-oxp.c | 568 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 568 insertions(+) diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c index 2504b56b8f8a8..52002d4cbd0b2 100644 --- a/drivers/hid/hid-oxp.c +++ b/drivers/hid/hid-oxp.c @@ -34,11 +34,147 @@ enum oxp_function_index { OXP_FID_GEN1_RGB_SET = 0x07, OXP_FID_GEN1_RGB_REPLY = 0x0f, OXP_FID_GEN2_TOGGLE_MODE = 0xb2, + OXP_FID_GEN2_KEY_STATE = 0xb4, OXP_FID_GEN2_STATUS_EVENT = 0xb8, }; +#define OXP_MAPPING_GAMEPAD 0x01 +#define OXP_MAPPING_KEYBOARD 0x02 + +struct oxp_button_data { + u8 mode; + u8 index; + u8 key_id; + u8 padding[2]; +} __packed; + +struct oxp_button_entry { + struct oxp_button_data data; + const char *name; +}; + +static const struct oxp_button_entry oxp_button_table[] = { + /* Gamepad Buttons */ + { { OXP_MAPPING_GAMEPAD, 0x01 }, "BTN_A" }, + { { OXP_MAPPING_GAMEPAD, 0x02 }, "BTN_B" }, + { { OXP_MAPPING_GAMEPAD, 0x03 }, "BTN_X" }, + { { OXP_MAPPING_GAMEPAD, 0x04 }, "BTN_Y" }, + { { OXP_MAPPING_GAMEPAD, 0x05 }, "BTN_LB" }, + { { OXP_MAPPING_GAMEPAD, 0x06 }, "BTN_RB" }, + { { OXP_MAPPING_GAMEPAD, 0x07 }, "BTN_LT" }, + { { OXP_MAPPING_GAMEPAD, 0x08 }, "BTN_RT" }, + { { OXP_MAPPING_GAMEPAD, 0x09 }, "BTN_START" }, + { { OXP_MAPPING_GAMEPAD, 0x0a }, "BTN_SELECT" }, + { { OXP_MAPPING_GAMEPAD, 0x0b }, "BTN_L3" }, + { { OXP_MAPPING_GAMEPAD, 0x0c }, "BTN_R3" }, + { { OXP_MAPPING_GAMEPAD, 0x0d }, "DPAD_UP" }, + { { OXP_MAPPING_GAMEPAD, 0x0e }, "DPAD_DOWN" }, + { { OXP_MAPPING_GAMEPAD, 0x0f }, "DPAD_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x10 }, "DPAD_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x11 }, "JOY_L_UP" }, + { { OXP_MAPPING_GAMEPAD, 0x12 }, "JOY_L_UP_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x13 }, "JOY_L_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x14 }, "JOY_L_DOWN_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x15 }, "JOY_L_DOWN" }, + { { OXP_MAPPING_GAMEPAD, 0x16 }, "JOY_L_DOWN_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x17 }, "JOY_L_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x18 }, "JOY_L_UP_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x19 }, "JOY_R_UP" }, + { { OXP_MAPPING_GAMEPAD, 0x1a }, "JOY_R_UP_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x1b }, "JOY_R_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x1c }, "JOY_R_DOWN_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x1d }, "JOY_R_DOWN" }, + { { OXP_MAPPING_GAMEPAD, 0x1e }, "JOY_R_DOWN_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x1f }, "JOY_R_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x20 }, "JOY_R_UP_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x22 }, "BTN_GUIDE" }, + /* Keyboard Keys */ + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5a }, "KEY_F1" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5b }, "KEY_F2" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5c }, "KEY_F3" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5d }, "KEY_F4" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5e }, "KEY_F5" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5f }, "KEY_F6" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x60 }, "KEY_F7" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x61 }, "KEY_F8" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x62 }, "KEY_F9" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x63 }, "KEY_F10" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x64 }, "KEY_F11" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x65 }, "KEY_F12" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x66 }, "KEY_F13" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x67 }, "KEY_F14" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x68 }, "KEY_F15" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x69 }, "KEY_F16" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6a }, "KEY_F17" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6b }, "KEY_F18" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6c }, "KEY_F19" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6d }, "KEY_F20" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6e }, "KEY_F21" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6f }, "KEY_F22" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x70 }, "KEY_F23" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x71 }, "KEY_F24" }, +}; + +enum oxp_joybutton_index { + BUTTON_A = 0x01, + BUTTON_B, + BUTTON_X, + BUTTON_Y, + BUTTON_LB, + BUTTON_RB, + BUTTON_LT, + BUTTON_RT, + BUTTON_START, + BUTTON_SELECT, + BUTTON_L3, + BUTTON_R3, + BUTTON_DUP, + BUTTON_DDOWN, + BUTTON_DLEFT, + BUTTON_DRIGHT, + BUTTON_M1 = 0x22, + BUTTON_M2, + /* These are unused currently, reserved for future devices */ + BUTTON_M3, + BUTTON_M4, + BUTTON_M5, + BUTTON_M6, +}; + +struct oxp_button_idx { + enum oxp_joybutton_index button_idx; + u8 mapping_idx; +} __packed; + +struct oxp_bmap_page_1 { + struct oxp_button_idx btn_a; + struct oxp_button_idx btn_b; + struct oxp_button_idx btn_x; + struct oxp_button_idx btn_y; + struct oxp_button_idx btn_lb; + struct oxp_button_idx btn_rb; + struct oxp_button_idx btn_lt; + struct oxp_button_idx btn_rt; + struct oxp_button_idx btn_start; +} __packed; + +struct oxp_bmap_page_2 { + struct oxp_button_idx btn_select; + struct oxp_button_idx btn_l3; + struct oxp_button_idx btn_r3; + struct oxp_button_idx btn_dup; + struct oxp_button_idx btn_ddown; + struct oxp_button_idx btn_dleft; + struct oxp_button_idx btn_dright; + struct oxp_button_idx btn_m1; + struct oxp_button_idx btn_m2; +} __packed; + static struct oxp_hid_cfg { struct delayed_work oxp_rgb_queue; + struct delayed_work oxp_btn_queue; + struct oxp_bmap_page_1 *bmap_1; + struct oxp_bmap_page_2 *bmap_2; struct delayed_work oxp_mcu_init; struct led_classdev_mc *led_mc; struct hid_device *hdev; @@ -50,6 +186,10 @@ static struct oxp_hid_cfg { u8 rgb_en; } drvdata; +#define OXP_FILL_PAGE_SLOT(page, btn) \ + { .button_idx = (page)->btn.button_idx, \ + .mapping_idx = (page)->btn.mapping_idx } + enum oxp_gamepad_mode_index { OXP_GP_MODE_XINPUT = 0x00, OXP_GP_MODE_DEBUG = 0x03, @@ -155,6 +295,10 @@ struct oxp_gen_2_rgb_report { u8 effect; } __packed; +struct oxp_attr { + u8 index; +}; + static u16 get_usage_page(struct hid_device *hdev) { return hdev->collection[0].usage >> 16; @@ -196,12 +340,19 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev, } static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size); +static int oxp_set_buttons(void); static void oxp_mcu_init_fn(struct work_struct *work) { u8 gp_mode_data[3] = { OXP_GP_MODE_DEBUG, 0x01, 0x02 }; int ret; + /* Re-apply the button mapping */ + ret = oxp_set_buttons(); + if (ret) + dev_err(&drvdata.hdev->dev, + "Error: Failed to set button mapping: %i\n", ret); + /* Cycle the gamepad mode */ ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3); if (ret) @@ -395,9 +546,408 @@ static ssize_t gamepad_mode_index_show(struct device *dev, } static DEVICE_ATTR_RO(gamepad_mode_index); +static void oxp_set_defaults_bmap_1(struct oxp_bmap_page_1 *bmap) +{ + bmap->btn_a.button_idx = BUTTON_A; + bmap->btn_a.mapping_idx = 0; + bmap->btn_b.button_idx = BUTTON_B; + bmap->btn_b.mapping_idx = 1; + bmap->btn_x.button_idx = BUTTON_X; + bmap->btn_x.mapping_idx = 2; + bmap->btn_y.button_idx = BUTTON_Y; + bmap->btn_y.mapping_idx = 3; + bmap->btn_lb.button_idx = BUTTON_LB; + bmap->btn_lb.mapping_idx = 4; + bmap->btn_rb.button_idx = BUTTON_RB; + bmap->btn_rb.mapping_idx = 5; + bmap->btn_lt.button_idx = BUTTON_LT; + bmap->btn_lt.mapping_idx = 6; + bmap->btn_rt.button_idx = BUTTON_RT; + bmap->btn_rt.mapping_idx = 7; + bmap->btn_start.button_idx = BUTTON_START; + bmap->btn_start.mapping_idx = 8; +} + +static void oxp_set_defaults_bmap_2(struct oxp_bmap_page_2 *bmap) +{ + bmap->btn_select.button_idx = BUTTON_SELECT; + bmap->btn_select.mapping_idx = 9; + bmap->btn_l3.button_idx = BUTTON_L3; + bmap->btn_l3.mapping_idx = 10; + bmap->btn_r3.button_idx = BUTTON_R3; + bmap->btn_r3.mapping_idx = 11; + bmap->btn_dup.button_idx = BUTTON_DUP; + bmap->btn_dup.mapping_idx = 12; + bmap->btn_ddown.button_idx = BUTTON_DDOWN; + bmap->btn_ddown.mapping_idx = 13; + bmap->btn_dleft.button_idx = BUTTON_DLEFT; + bmap->btn_dleft.mapping_idx = 14; + bmap->btn_dright.button_idx = BUTTON_DRIGHT; + bmap->btn_dright.mapping_idx = 15; + bmap->btn_m1.button_idx = BUTTON_M1; + bmap->btn_m1.mapping_idx = 48; /* KEY_F15 */ + bmap->btn_m2.button_idx = BUTTON_M2; + bmap->btn_m2.mapping_idx = 49; /* KEY_F16 */ +} + +static void oxp_page_fill_data(char *buf, const struct oxp_button_idx *buttons, + size_t len) +{ + size_t offset_increment = sizeof(u8) + sizeof(struct oxp_button_idx); + size_t offset = 5; + unsigned int i; + + for (i = 0; i < len; i++, offset += offset_increment) { + buf[offset] = (u8)buttons[i].button_idx; + memcpy(buf + offset + 1, + &oxp_button_table[buttons[i].mapping_idx].data, + sizeof(struct oxp_button_data)); + } +} + +static int oxp_set_buttons(void) +{ + u8 page_1[59] = { 0x02, 0x38, 0x20, 0x01, 0x01 }; + u8 page_2[59] = { 0x02, 0x38, 0x20, 0x02, 0x01 }; + u16 up = get_usage_page(drvdata.hdev); + int ret; + + if (up != GEN2_USAGE_PAGE) + return -EINVAL; + + const struct oxp_button_idx p1[] = { + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_a), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_b), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_x), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_y), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_lb), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_rb), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_lt), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_rt), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_start), + }; + + const struct oxp_button_idx p2[] = { + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_select), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_l3), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_r3), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dup), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_ddown), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dleft), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dright), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_m1), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_m2), + }; + + oxp_page_fill_data(page_1, p1, ARRAY_SIZE(p1)); + oxp_page_fill_data(page_2, p2, ARRAY_SIZE(p2)); + + ret = oxp_gen_2_property_out(OXP_FID_GEN2_KEY_STATE, page_1, ARRAY_SIZE(page_1)); + if (ret) + return ret; + + return oxp_gen_2_property_out(OXP_FID_GEN2_KEY_STATE, page_2, ARRAY_SIZE(page_2)); +} + +static void oxp_reset_buttons(void) +{ + oxp_set_defaults_bmap_1(drvdata.bmap_1); + oxp_set_defaults_bmap_2(drvdata.bmap_2); +} + +static ssize_t reset_buttons_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int val, ret; + + ret = kstrtoint(buf, 10, &val); + if (ret) + return ret; + + if (val != 1) + return -EINVAL; + + oxp_reset_buttons(); + ret = oxp_set_buttons(); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_WO(reset_buttons); + +static void oxp_btn_queue_fn(struct work_struct *work) +{ + int ret; + + ret = oxp_set_buttons(); + if (ret) + dev_err(&drvdata.hdev->dev, + "Error: Failed to write button mapping: %i\n", ret); +} + +static int oxp_button_idx_from_str(const char *buf) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(oxp_button_table); i++) + if (sysfs_streq(buf, oxp_button_table[i].name)) + return i; + + return -EINVAL; +} + +static ssize_t map_button_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count, u8 index) +{ + int idx; + + idx = oxp_button_idx_from_str(buf); + if (idx < 0) + return idx; + + switch (index) { + case BUTTON_A: + drvdata.bmap_1->btn_a.mapping_idx = idx; + break; + case BUTTON_B: + drvdata.bmap_1->btn_b.mapping_idx = idx; + break; + case BUTTON_X: + drvdata.bmap_1->btn_x.mapping_idx = idx; + break; + case BUTTON_Y: + drvdata.bmap_1->btn_y.mapping_idx = idx; + break; + case BUTTON_LB: + drvdata.bmap_1->btn_lb.mapping_idx = idx; + break; + case BUTTON_RB: + drvdata.bmap_1->btn_rb.mapping_idx = idx; + break; + case BUTTON_LT: + drvdata.bmap_1->btn_lt.mapping_idx = idx; + break; + case BUTTON_RT: + drvdata.bmap_1->btn_rt.mapping_idx = idx; + break; + case BUTTON_START: + drvdata.bmap_1->btn_start.mapping_idx = idx; + break; + case BUTTON_SELECT: + drvdata.bmap_2->btn_select.mapping_idx = idx; + break; + case BUTTON_L3: + drvdata.bmap_2->btn_l3.mapping_idx = idx; + break; + case BUTTON_R3: + drvdata.bmap_2->btn_r3.mapping_idx = idx; + break; + case BUTTON_DUP: + drvdata.bmap_2->btn_dup.mapping_idx = idx; + break; + case BUTTON_DDOWN: + drvdata.bmap_2->btn_ddown.mapping_idx = idx; + break; + case BUTTON_DLEFT: + drvdata.bmap_2->btn_dleft.mapping_idx = idx; + break; + case BUTTON_DRIGHT: + drvdata.bmap_2->btn_dright.mapping_idx = idx; + break; + case BUTTON_M1: + drvdata.bmap_2->btn_m1.mapping_idx = idx; + break; + case BUTTON_M2: + drvdata.bmap_2->btn_m2.mapping_idx = idx; + break; + default: + return -EINVAL; + } + mod_delayed_work(system_wq, &drvdata.oxp_btn_queue, msecs_to_jiffies(50)); + return count; +} + +static ssize_t map_button_show(struct device *dev, + struct device_attribute *attr, char *buf, + u8 index) +{ + u8 i; + + switch (index) { + case BUTTON_A: + i = drvdata.bmap_1->btn_a.mapping_idx; + break; + case BUTTON_B: + i = drvdata.bmap_1->btn_b.mapping_idx; + break; + case BUTTON_X: + i = drvdata.bmap_1->btn_x.mapping_idx; + break; + case BUTTON_Y: + i = drvdata.bmap_1->btn_y.mapping_idx; + break; + case BUTTON_LB: + i = drvdata.bmap_1->btn_lb.mapping_idx; + break; + case BUTTON_RB: + i = drvdata.bmap_1->btn_rb.mapping_idx; + break; + case BUTTON_LT: + i = drvdata.bmap_1->btn_lt.mapping_idx; + break; + case BUTTON_RT: + i = drvdata.bmap_1->btn_rt.mapping_idx; + break; + case BUTTON_START: + i = drvdata.bmap_1->btn_start.mapping_idx; + break; + case BUTTON_SELECT: + i = drvdata.bmap_2->btn_select.mapping_idx; + break; + case BUTTON_L3: + i = drvdata.bmap_2->btn_l3.mapping_idx; + break; + case BUTTON_R3: + i = drvdata.bmap_2->btn_r3.mapping_idx; + break; + case BUTTON_DUP: + i = drvdata.bmap_2->btn_dup.mapping_idx; + break; + case BUTTON_DDOWN: + i = drvdata.bmap_2->btn_ddown.mapping_idx; + break; + case BUTTON_DLEFT: + i = drvdata.bmap_2->btn_dleft.mapping_idx; + break; + case BUTTON_DRIGHT: + i = drvdata.bmap_2->btn_dright.mapping_idx; + break; + case BUTTON_M1: + i = drvdata.bmap_2->btn_m1.mapping_idx; + break; + case BUTTON_M2: + i = drvdata.bmap_2->btn_m2.mapping_idx; + break; + default: + return -EINVAL; + } + + if (i >= ARRAY_SIZE(oxp_button_table)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", oxp_button_table[i].name); +} + +static ssize_t button_mapping_options_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t count = 0; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(oxp_button_table); i++) + count += sysfs_emit_at(buf, count, "%s ", oxp_button_table[i].name); + + if (count) + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(button_mapping_options); + +#define OXP_DEVICE_ATTR_RW(_name, _group) \ + static ssize_t _name##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return _group##_store(dev, attr, buf, count, _name.index); \ + } \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + return _group##_show(dev, attr, buf, _name.index); \ + } \ + static DEVICE_ATTR_RW(_name) + +static struct oxp_attr button_a = { BUTTON_A }; +OXP_DEVICE_ATTR_RW(button_a, map_button); + +static struct oxp_attr button_b = { BUTTON_B }; +OXP_DEVICE_ATTR_RW(button_b, map_button); + +static struct oxp_attr button_x = { BUTTON_X }; +OXP_DEVICE_ATTR_RW(button_x, map_button); + +static struct oxp_attr button_y = { BUTTON_Y }; +OXP_DEVICE_ATTR_RW(button_y, map_button); + +static struct oxp_attr button_lb = { BUTTON_LB }; +OXP_DEVICE_ATTR_RW(button_lb, map_button); + +static struct oxp_attr button_rb = { BUTTON_RB }; +OXP_DEVICE_ATTR_RW(button_rb, map_button); + +static struct oxp_attr button_lt = { BUTTON_LT }; +OXP_DEVICE_ATTR_RW(button_lt, map_button); + +static struct oxp_attr button_rt = { BUTTON_RT }; +OXP_DEVICE_ATTR_RW(button_rt, map_button); + +static struct oxp_attr button_start = { BUTTON_START }; +OXP_DEVICE_ATTR_RW(button_start, map_button); + +static struct oxp_attr button_select = { BUTTON_SELECT }; +OXP_DEVICE_ATTR_RW(button_select, map_button); + +static struct oxp_attr button_l3 = { BUTTON_L3 }; +OXP_DEVICE_ATTR_RW(button_l3, map_button); + +static struct oxp_attr button_r3 = { BUTTON_R3 }; +OXP_DEVICE_ATTR_RW(button_r3, map_button); + +static struct oxp_attr button_d_up = { BUTTON_DUP }; +OXP_DEVICE_ATTR_RW(button_d_up, map_button); + +static struct oxp_attr button_d_down = { BUTTON_DDOWN }; +OXP_DEVICE_ATTR_RW(button_d_down, map_button); + +static struct oxp_attr button_d_left = { BUTTON_DLEFT }; +OXP_DEVICE_ATTR_RW(button_d_left, map_button); + +static struct oxp_attr button_d_right = { BUTTON_DRIGHT }; +OXP_DEVICE_ATTR_RW(button_d_right, map_button); + +static struct oxp_attr button_m1 = { BUTTON_M1 }; +OXP_DEVICE_ATTR_RW(button_m1, map_button); + +static struct oxp_attr button_m2 = { BUTTON_M2 }; +OXP_DEVICE_ATTR_RW(button_m2, map_button); + static struct attribute *oxp_cfg_attrs[] = { + &dev_attr_button_a.attr, + &dev_attr_button_b.attr, + &dev_attr_button_d_down.attr, + &dev_attr_button_d_left.attr, + &dev_attr_button_d_right.attr, + &dev_attr_button_d_up.attr, + &dev_attr_button_l3.attr, + &dev_attr_button_lb.attr, + &dev_attr_button_lt.attr, + &dev_attr_button_m1.attr, + &dev_attr_button_m2.attr, + &dev_attr_button_mapping_options.attr, + &dev_attr_button_r3.attr, + &dev_attr_button_rb.attr, + &dev_attr_button_rt.attr, + &dev_attr_button_select.attr, + &dev_attr_button_start.attr, + &dev_attr_button_x.attr, + &dev_attr_button_y.attr, &dev_attr_gamepad_mode.attr, &dev_attr_gamepad_mode_index.attr, + &dev_attr_reset_buttons.attr, NULL, }; @@ -821,6 +1371,8 @@ static bool oxp_hybrid_mcu_device(void) static int oxp_cfg_probe(struct hid_device *hdev, u16 up) { + struct oxp_bmap_page_1 *bmap_1; + struct oxp_bmap_page_2 *bmap_2; int ret; hid_set_drvdata(hdev, &drvdata); @@ -854,6 +1406,21 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up) return 0; skip_rgb: + bmap_1 = devm_kzalloc(&hdev->dev, sizeof(struct oxp_bmap_page_1), GFP_KERNEL); + if (!bmap_1) + return dev_err_probe(&hdev->dev, -ENOMEM, + "Unable to allocate button map page 1\n"); + + bmap_2 = devm_kzalloc(&hdev->dev, sizeof(struct oxp_bmap_page_2), GFP_KERNEL); + if (!bmap_2) + return dev_err_probe(&hdev->dev, -ENOMEM, + "Unable to allocate button map page 2\n"); + + drvdata.bmap_1 = bmap_1; + drvdata.bmap_2 = bmap_2; + oxp_reset_buttons(); + INIT_DELAYED_WORK(&drvdata.oxp_btn_queue, oxp_btn_queue_fn); + drvdata.gamepad_mode = OXP_GP_MODE_XINPUT; INIT_DELAYED_WORK(&drvdata.oxp_mcu_init, oxp_mcu_init_fn); @@ -908,6 +1475,7 @@ static int oxp_hid_probe(struct hid_device *hdev, static void oxp_hid_remove(struct hid_device *hdev) { cancel_delayed_work(&drvdata.oxp_rgb_queue); + cancel_delayed_work(&drvdata.oxp_btn_queue); cancel_delayed_work(&drvdata.oxp_mcu_init); hid_hw_close(hdev); hid_hw_stop(hdev); From b3b29c25741a19c09c37511b12933eb153f674d1 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 18 Apr 2026 21:26:24 -0700 Subject: [PATCH 19/73] [FROM-ML] HID: hid-oxp: Add Vibration Intensity Attribute Adds attribute for setting the rumble intensity level. This setting must be re-applied after the gamepad mode is set as doing so resets this to the default value. Reviewed-by: Zhouwang Huang Tested-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- drivers/hid/hid-oxp.c | 78 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c index 52002d4cbd0b2..20a54f337220d 100644 --- a/drivers/hid/hid-oxp.c +++ b/drivers/hid/hid-oxp.c @@ -34,6 +34,7 @@ enum oxp_function_index { OXP_FID_GEN1_RGB_SET = 0x07, OXP_FID_GEN1_RGB_REPLY = 0x0f, OXP_FID_GEN2_TOGGLE_MODE = 0xb2, + OXP_FID_GEN2_RUMBLE_SET = 0xb3, OXP_FID_GEN2_KEY_STATE = 0xb4, OXP_FID_GEN2_STATUS_EVENT = 0xb8, }; @@ -181,6 +182,7 @@ static struct oxp_hid_cfg { struct mutex cfg_mutex; /*ensure single synchronous output report*/ u8 rgb_brightness; u8 gamepad_mode; + u8 rumble_intensity; u8 rgb_effect; u8 rgb_speed; u8 rgb_en; @@ -266,6 +268,11 @@ static const char *const oxp_rgb_effect_text[] = { [OXP_EFFECT_MONO_LIST] = "monocolor", }; +enum oxp_rumble_side_index { + OXP_RUMBLE_LEFT = 0x00, + OXP_RUMBLE_RIGHT, +}; + struct oxp_gen_1_rgb_report { u8 report_id; u8 message_id; @@ -341,6 +348,7 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev, static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size); static int oxp_set_buttons(void); +static int oxp_rumble_intensity_set(u8 intensity); static void oxp_mcu_init_fn(struct work_struct *work) { @@ -368,6 +376,12 @@ static void oxp_mcu_init_fn(struct work_struct *work) if (ret) dev_err(&drvdata.hdev->dev, "Error: Failed to set gamepad mode: %i\n", ret); + + /* Set vibration level */ + ret = oxp_rumble_intensity_set(drvdata.rumble_intensity); + if (ret) + dev_err(&drvdata.hdev->dev, + "Error: Failed to set rumble intensity: %i\n", ret); } static int oxp_hid_raw_event_gen_2(struct hid_device *hdev, @@ -514,6 +528,14 @@ static ssize_t gamepad_mode_store(struct device *dev, drvdata.gamepad_mode = data[0]; + if (drvdata.gamepad_mode == OXP_GP_MODE_DEBUG) + return count; + + /* Re-apply rumble settings as switching gamepad mode will override */ + ret = oxp_rumble_intensity_set(drvdata.rumble_intensity); + if (ret) + return ret; + return count; } @@ -857,6 +879,59 @@ static ssize_t button_mapping_options_show(struct device *dev, } static DEVICE_ATTR_RO(button_mapping_options); +static int oxp_rumble_intensity_set(u8 intensity) +{ + u8 header[15] = { 0x02, 0x38, 0x02, 0xe3, 0x39, 0xe3, 0x39, 0xe3, + 0x39, 0x01, intensity, 0x05, 0xe3, 0x39, 0xe3 }; + u8 footer[9] = { 0x39, 0xe3, 0x39, 0xe3, 0xe3, 0x02, 0x04, 0x39, 0x39 }; + size_t footer_size = ARRAY_SIZE(footer); + size_t header_size = ARRAY_SIZE(header); + u8 data[59] = { 0x0 }; + size_t data_size = ARRAY_SIZE(data); + + memcpy(data, header, header_size); + memcpy(data + data_size - footer_size, footer, footer_size); + + return oxp_gen_2_property_out(OXP_FID_GEN2_RUMBLE_SET, data, data_size); +} + +static ssize_t rumble_intensity_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int ret; + u8 val; + + ret = kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val < 0 || val > 5) + return -EINVAL; + + ret = oxp_rumble_intensity_set(val); + if (ret) + return ret; + + drvdata.rumble_intensity = val; + + return count; +} + +static ssize_t rumble_intensity_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%i\n", drvdata.rumble_intensity); +} +static DEVICE_ATTR_RW(rumble_intensity); + +static ssize_t rumble_intensity_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0-5\n"); +} +static DEVICE_ATTR_RO(rumble_intensity_range); + #define OXP_DEVICE_ATTR_RW(_name, _group) \ static ssize_t _name##_store(struct device *dev, \ struct device_attribute *attr, \ @@ -948,6 +1023,8 @@ static struct attribute *oxp_cfg_attrs[] = { &dev_attr_gamepad_mode.attr, &dev_attr_gamepad_mode_index.attr, &dev_attr_reset_buttons.attr, + &dev_attr_rumble_intensity.attr, + &dev_attr_rumble_intensity_range.attr, NULL, }; @@ -1422,6 +1499,7 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up) INIT_DELAYED_WORK(&drvdata.oxp_btn_queue, oxp_btn_queue_fn); drvdata.gamepad_mode = OXP_GP_MODE_XINPUT; + drvdata.rumble_intensity = 5; INIT_DELAYED_WORK(&drvdata.oxp_mcu_init, oxp_mcu_init_fn); mod_delayed_work(system_wq, &drvdata.oxp_mcu_init, msecs_to_jiffies(50)); From 68fb636b35d91efc8c3e19aa94364398058535bd Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 26 Jul 2025 13:40:38 -0700 Subject: [PATCH 20/73] [FROM-ML] platform/x86: (ayn-ec) Add PWM Fan HWMON Interface Adds platform driver for AYN Loki and Tectoy Zeenix lines of handheld devices. This patch implements a hwmon interface for EC provided manual PWM fan control and user defined fan curves. A global ACPI lock is used when reading or writing from the EC. There are 4 fan modes implemented in this patch. Modes 0-3 act in accordance with the standard hwmon logic where 0 is 100% fan speed, 1 is manual control, and 2 is automatic control. As the EC only provides 3 modes by default, mode 0 is implemented by setting the device to manual and then setting fan speed to 100% directly. In mode 1 the PWM duty cycle is set in sysfs with values [0-255], which are then scaled to the EC max of 128. Mode 4 is an automatic mode where the fan curve is user defined. There are 5 total set points and each set point takes a temperature in Celsius [0-100] and a PWM duty cycle [0-255]. When the CPU temperature reaches a given set point, the corresponding duty cycle is automatically set by the EC. Signed-off-by: Derek J. Clark --- MAINTAINERS | 6 + drivers/platform/x86/Kconfig | 12 + drivers/platform/x86/Makefile | 3 + drivers/platform/x86/ayn-ec.c | 520 ++++++++++++++++++++++++++++++++++ 4 files changed, 541 insertions(+) create mode 100644 drivers/platform/x86/ayn-ec.c diff --git a/MAINTAINERS b/MAINTAINERS index df840f5fa4427..a9dde8924fd03 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4373,6 +4373,12 @@ F: Documentation/devicetree/bindings/spi/axiado,ax3000-spi.yaml F: drivers/spi/spi-axiado.c F: drivers/spi/spi-axiado.h +AYN PLATFORM EC DRIVER +M: Derek J. Clark +L: platform-driver-x86@vger.kernel.org +S: Maintained +F: drivers/platform/x86/ayn-ec.c + AYANEO PLATFORM EC DRIVER M: Antheas Kapenekakis L: platform-driver-x86@vger.kernel.org diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index 7a4956088300c..d06f47aea7752 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -330,6 +330,18 @@ config ASUS_TF103C_DOCK If you have an Asus TF103C tablet say Y or M here, for a generic x86 distro config say M here. +config AYN_EC + tristate "AYN x86 devices EC platform control" + depends on ACPI + depends on HWMON + help + This is a driver for AYN and Tectoy x86 handheld devices. It provides + temperature monitoring, manual fan speed control, fan curve control, + and chassis RGB settings. + + If you have an x86 AYN or Tectoy handheld device say M here. The module + will be called ayn-platform. + config AYANEO_EC tristate "Ayaneo EC platform control" depends on DMI diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile index 872ac3842391f..31b4fd9083948 100644 --- a/drivers/platform/x86/Makefile +++ b/drivers/platform/x86/Makefile @@ -40,6 +40,9 @@ obj-$(CONFIG_ASUS_TF103C_DOCK) += asus-tf103c-dock.o obj-$(CONFIG_EEEPC_LAPTOP) += eeepc-laptop.o obj-$(CONFIG_EEEPC_WMI) += eeepc-wmi.o +# Ayn +obj-$(CONFIG_AYN_EC) += ayn-ec.o + # Ayaneo obj-$(CONFIG_AYANEO_EC) += ayaneo-ec.o diff --git a/drivers/platform/x86/ayn-ec.c b/drivers/platform/x86/ayn-ec.c new file mode 100644 index 0000000000000..8bd3ed1c69ebb --- /dev/null +++ b/drivers/platform/x86/ayn-ec.c @@ -0,0 +1,520 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Platform driver for AYN x86 Handhelds. + * + * Implements multiple attributes provided by the EC. Fan reading and control, + * as well as temperature sensor readings are exposed via hwmon sysfs. EC RGB + * control is exposed via an led-class-multicolor interface. + * + * Fan control is provided via a pwm interface in the range [0-255]. AYN use + * [0-128] as the range in the EC, the written value is scaled to accommodate. + * The EC also provides a configurable fan curve with five set points that + * associate a temperature in Celcius [0-100] with a fan speed [0-128]. The + * auto_point fan speeds are also scaled from the range [0-255]. Temperature + * readings are scaled from degrees to millidegrees when read. + * + * RGB control is provided using 4 registers. One each for the colors red, + * green, and blue are [0-255]. There is also a effect register that takes + * switches between an EC controlled breathing that cycles through all colors + * and fades in/out, and manual, which enables setting a user defined color. + * + * Copyright (C) 2025 Derek J. Clark + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Fan speed and PWM registers */ +#define AYN_SENSOR_PWM_FAN_ENABLE_REG 0x10 /* PWM operating mode */ +#define AYN_SENSOR_PWM_FAN_SET_REG 0x11 /* PWM duty cycle */ +#define AYN_SENSOR_PWM_FAN_SPEED_REG 0x20 /* Fan speed */ + +/* EC controlled fan curve registers */ +#define AYN_SENSOR_PWM_FAN_SPEED_1_REG 0x12 +#define AYN_SENSOR_PWM_FAN_SPEED_2_REG 0x14 +#define AYN_SENSOR_PWM_FAN_SPEED_3_REG 0x16 +#define AYN_SENSOR_PWM_FAN_SPEED_4_REG 0x18 +#define AYN_SENSOR_PWM_FAN_SPEED_5_REG 0x1A +#define AYN_SENSOR_PWM_FAN_TEMP_1_REG 0x13 +#define AYN_SENSOR_PWM_FAN_TEMP_2_REG 0x15 +#define AYN_SENSOR_PWM_FAN_TEMP_3_REG 0x17 +#define AYN_SENSOR_PWM_FAN_TEMP_4_REG 0x19 +#define AYN_SENSOR_PWM_FAN_TEMP_5_REG 0x1B + +/* AYN EC PWM Fan modes */ +#define AYN_PWM_FAN_MODE_MANUAL 0x00 +#define AYN_PWM_FAN_MODE_AUTO 0x01 +#define AYN_PWM_FAN_MODE_EC_CURVE 0x02 + +/* hwmon fan modes */ +#define HWMON_PWM_FAN_MODE_FULL 0x00 +#define HWMON_PWM_FAN_MODE_MANUAL 0x01 +#define HWMON_PWM_FAN_MODE_AUTO 0x02 +#define HWMON_PWM_FAN_MODE_EC_CURVE 0x03 + +/* Handle ACPI lock mechanism */ +#define ACPI_LOCK_DELAY_MS 500 + +int ayn_pwm_curve_registers[10] = { + AYN_SENSOR_PWM_FAN_SPEED_1_REG, + AYN_SENSOR_PWM_FAN_SPEED_2_REG, + AYN_SENSOR_PWM_FAN_SPEED_3_REG, + AYN_SENSOR_PWM_FAN_SPEED_4_REG, + AYN_SENSOR_PWM_FAN_SPEED_5_REG, + AYN_SENSOR_PWM_FAN_TEMP_1_REG, + AYN_SENSOR_PWM_FAN_TEMP_2_REG, + AYN_SENSOR_PWM_FAN_TEMP_3_REG, + AYN_SENSOR_PWM_FAN_TEMP_4_REG, + AYN_SENSOR_PWM_FAN_TEMP_5_REG, +}; + +struct ayn_device { + u32 ayn_lock; /* ACPI EC Lock */ +} drvdata; + +/* Handle ACPI lock mechanism */ +#define ACPI_LOCK_DELAY_MS 500 + +static bool lock_global_acpi_lock(void) +{ + return ACPI_SUCCESS(acpi_acquire_global_lock(ACPI_LOCK_DELAY_MS, + &drvdata.ayn_lock)); +} + +static bool unlock_global_acpi_lock(void) +{ + return ACPI_SUCCESS(acpi_release_global_lock(drvdata.ayn_lock)); +} + +/** + * read_from_ec() - Reads a value from the embedded controller. + * + * @reg: The register to start the read from. + * @size: The number of sequential registers the data is contained in. + * @val: Pointer to return the data with. + * + * Return: 0, or an error. + */ +static int read_from_ec(u8 reg, int size, long *val) +{ + int ret, i; + u8 buf; + + if (!lock_global_acpi_lock()) + return -EBUSY; + + *val = 0; + for (i = 0; i < size; i++) { + ret = ec_read(reg + i, &buf); + if (ret) + return ret; + *val <<= i * 8; + *val += buf; + } + + if (!unlock_global_acpi_lock()) + return -EBUSY; + + return 0; +} + +/** + * write_to_ec() - Writes a value to the embedded controller. + * + * @reg: The register to write to. + * @val: Value to write + * + * Return: 0, or an error. + */ +static int write_to_ec(u8 reg, u8 val) +{ + int ret; + + if (!lock_global_acpi_lock()) + return -EBUSY; + + pr_info("Writing EC value %d to register %u\n", val, reg); + ret = ec_write(reg, val); + + if (!unlock_global_acpi_lock()) + return -EBUSY; + + return ret; +} + +/** + * ayn_pwm_manual() - Enable manual control of the fan. + */ +static int ayn_pwm_manual(void) +{ + return write_to_ec(AYN_SENSOR_PWM_FAN_ENABLE_REG, 0x00); +} + +/** + * ayn_pwm_full() - Set fan to 100% speed. + */ +static int ayn_pwm_full(void) +{ + int ret; + + ret = write_to_ec(AYN_SENSOR_PWM_FAN_ENABLE_REG, 0x00); + if (ret) + return ret; + + return write_to_ec(AYN_SENSOR_PWM_FAN_SET_REG, 128); +} + +/** + * ayn_pwm_auto() - Enable automatic EC control of the fan. + */ +static int ayn_pwm_auto(void) +{ + return write_to_ec(AYN_SENSOR_PWM_FAN_ENABLE_REG, 0x01); +} + +/** + * ayn_pwm_ec_curve() - Enable manually setting the fan curve for automatic + * EC control of the fan. + */ +static int ayn_pwm_ec_curve(void) +{ + return write_to_ec(AYN_SENSOR_PWM_FAN_ENABLE_REG, 0x02); +} + +/** + * ayn_ec_hwmon_is_visible() - Determines RO or RW for hwmon attribute sysfs. + * + * @drvdata: Unused void pointer to context data. + * @type: The hwmon_sensor_types type. + * @attr: The attribute to set RO/RW on. + * @channel: HWMON subsystem usage flags for the attribute. + * + * Return: Permission level. + */ +static umode_t ayn_ec_hwmon_is_visible(const void *drvdata, + enum hwmon_sensor_types type, u32 attr, + int channel) +{ + switch (type) { + case hwmon_fan: + return 0444; + case hwmon_pwm: + return 0644; + default: + return 0; + } +} + +/** + * ayn_pwm_fan_read() - Read from a hwmon pwm or fan attribute. + * + * @dev: parent device of the given attribute. + * @type: The hwmon_sensor_types type. + * @attr: The attribute to read from. + * @channel: HWMON subsystem usage flags for the attribute. + * @val: Pointer to return the read value from. + * + * Return: 0, or an error. + */ +static int ayn_pwm_fan_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + int ret; + + switch (type) { + case hwmon_fan: + switch (attr) { + case hwmon_fan_input: + return read_from_ec(AYN_SENSOR_PWM_FAN_SPEED_REG, 2, + val); + default: + break; + } + break; + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_enable: + ret = read_from_ec(AYN_SENSOR_PWM_FAN_ENABLE_REG, 1, + val); + if (ret) + return ret; + + /* EC uses 0 for manual, 1 for automatic, 2 for user + * fan curve. Reflect hwmon usage instead. + */ + if (*val == 1) { + *val = 2; + return 0; + } + + if (*val == 2) { + *val = 3; + return 0; + } + + /* Return 0 when fan at max, otherwise 1 for manual. */ + ret = read_from_ec(AYN_SENSOR_PWM_FAN_SET_REG, 1, val); + if (ret) + return ret; + + if (*val == 128) + *val = 0; + else + *val = 1; + + return ret; + case hwmon_pwm_input: + ret = read_from_ec(AYN_SENSOR_PWM_FAN_SET_REG, 1, val); + if (ret) + return ret; + + *val = *val << 1; /* Max value is 128, scale to 255 */ + + return 0; + default: + break; + } + break; + default: + break; + } + return -EOPNOTSUPP; +} + +/** + * ayn_pwm_fan_write() - Write to a hwmon pwm attribute. + * + * @dev: parent device of the given attribute. + * @type: The hwmon_sensor_types type. + * @attr: The attribute to write to. + * @channel: HWMON subsystem usage flags for the attribute. + * @val: Value to write. + * + * Return: 0, or an error. + */ +static int ayn_pwm_fan_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + if (type != hwmon_pwm) + return -EOPNOTSUPP; + switch (attr) { + case hwmon_pwm_enable: + switch (val) { + case HWMON_PWM_FAN_MODE_FULL: + return ayn_pwm_full(); + case HWMON_PWM_FAN_MODE_MANUAL: + return ayn_pwm_manual(); + case HWMON_PWM_FAN_MODE_AUTO: + return ayn_pwm_auto(); + case HWMON_PWM_FAN_MODE_EC_CURVE: + return ayn_pwm_ec_curve(); + default: + return -EINVAL; + } + case hwmon_pwm_input: + if (val < 0 || val > 255) + return -EINVAL; + + val = val >> 1; /* Max value is 128, scale from 255 */ + + return write_to_ec(AYN_SENSOR_PWM_FAN_SET_REG, val); + default: + return -EOPNOTSUPP; + } +} + +static const struct hwmon_channel_info *ayn_ec_sensors[] = { + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT), + HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT | HWMON_PWM_ENABLE), + NULL, +}; + +static const struct hwmon_ops ayn_ec_hwmon_ops = { + .is_visible = ayn_ec_hwmon_is_visible, + .read = ayn_pwm_fan_read, + .write = ayn_pwm_fan_write, +}; + +static const struct hwmon_chip_info ayn_ec_chip_info = { + .ops = &ayn_ec_hwmon_ops, + .info = ayn_ec_sensors, +}; + +/** + * pwm_curve_store() - Write a fan curve speed or temperature value. + * + * @dev: The attribute's parent device. + * @attr: The attribute to read. + * @buf: Input value string from sysfs write. + * + * Return: Number of bytes read, or an error. + */ +static ssize_t pwm_curve_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int i = to_sensor_dev_attr(attr)->index; + int ret, val; + u8 reg; + + ret = kstrtoint(buf, 0, &val); + if (ret) + return ret; + + if (i < 5) { + if (val < 0 || val > 255) + return -EINVAL; + val = val >> 1; /* Max EC value is 128, scale from 255 */ + } else + if (val < 0 || val > 100) + return -EINVAL; + + reg = ayn_pwm_curve_registers[i]; + + ret = write_to_ec(reg, val); + if (ret) + return ret; + + return count; +} + +/** + * pwm_curve_show() - Read a fan curve speed or temperature value. + * + * @dev: The attribute's parent device. + * @attr: The attribute to read. + * @buf: Output buffer. + * + * Return: Number of bytes read, or an error. + */ +static ssize_t pwm_curve_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int i = to_sensor_dev_attr(attr)->index; + long val; + int ret; + u8 reg; + + reg = ayn_pwm_curve_registers[i]; + + ret = read_from_ec(reg, 1, &val); + if (ret) + return ret; + + if (i < 5) + val = val << 1; /* Max EC value is 128, scale to 255 */ + + return sysfs_emit(buf, "%ld\n", val); +} + +/* Fan curve attributes */ +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point1_pwm, pwm_curve, 0); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point2_pwm, pwm_curve, 1); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point3_pwm, pwm_curve, 2); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point4_pwm, pwm_curve, 3); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point5_pwm, pwm_curve, 4); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point1_temp, pwm_curve, 5); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point2_temp, pwm_curve, 6); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point3_temp, pwm_curve, 7); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point4_temp, pwm_curve, 8); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point5_temp, pwm_curve, 9); + +static struct attribute *ayn_sensors_attrs[] = { + &sensor_dev_attr_pwm1_auto_point1_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point1_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point2_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point2_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point3_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point3_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point4_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point4_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point5_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point5_temp.dev_attr.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(ayn_sensors); + +static int ayn_ec_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device *hwdev; + + hwdev = devm_hwmon_device_register_with_info(dev, "aynec", NULL, + &ayn_ec_chip_info, + ayn_sensors_groups); + return PTR_ERR_OR_ZERO(hwdev); +} + +static struct platform_driver ayn_ec_driver = { + .driver = { + .name = "ayn-ec", + }, + .probe = ayn_ec_probe, +}; + +static struct platform_device *ayn_ec_device; + +static int __init ayn_ec_init(void) +{ + ayn_ec_device = platform_create_bundle(&ayn_ec_driver, ayn_ec_probe, + NULL, 0, NULL, 0); + + return PTR_ERR_OR_ZERO(ayn_ec_device); +} + +static void __exit ayn_ec_exit(void) +{ + platform_device_unregister(ayn_ec_device); + platform_driver_unregister(&ayn_ec_driver); +} + +static const struct dmi_system_id ayn_dmi_table[] = { + { + .ident = "AYN Loki Max", + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "ayn"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "Loki Max"), + }, + }, + { + .ident = "AYN Loki MiniPro", + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "ayn"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "Loki MiniPro"), + }, + }, + { + .ident = "AYN Loki Zero", + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "ayn"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "Loki Zero"), + }, + }, + { + .ident = "Tectoy Zeenix Lite", + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "Tectoy"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "Zeenix Lite"), + }, + }, + {}, +}; + +MODULE_DEVICE_TABLE(dmi, ayn_dmi_table); + +module_init(ayn_ec_init); +module_exit(ayn_ec_exit); + +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("Platform driver that handles EC sensors of AYN x86 devices"); +MODULE_LICENSE("GPL"); From af44039e3ef80803ff5037e66799c4eda406c9f7 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 26 Jul 2025 13:40:39 -0700 Subject: [PATCH 21/73] [FROM-ML] platform/x86: (ayn-ec) Add Temperature Sensors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds temperature sensors to the ayn-ec hwmon interface. These read-only values include Battery, Motherboard, Charger IC, vCore, and CPU Core, as well as labels for each entry. The temperature values provided by the EC are whole numbers in degrees Celsius. As hwmon expects millidegrees, we scale the raw value up. `sensors` output after this patch is applied: aynec-isa-0000 Adapter: ISA adapter fan1: 1876 RPM Battery: +29.0°C Motherboard: +30.0°C Charger IC: +30.0°C vCore: +36.0°C CPU Core: +48.0°C Signed-off-by: Derek J. Clark --- drivers/platform/x86/ayn-ec.c | 88 ++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/drivers/platform/x86/ayn-ec.c b/drivers/platform/x86/ayn-ec.c index 8bd3ed1c69ebb..466cc33adcb08 100644 --- a/drivers/platform/x86/ayn-ec.c +++ b/drivers/platform/x86/ayn-ec.c @@ -61,6 +61,14 @@ #define HWMON_PWM_FAN_MODE_AUTO 0x02 #define HWMON_PWM_FAN_MODE_EC_CURVE 0x03 +/* EC Temperature Sensors */ +#define AYN_SENSOR_BAT_TEMP_REG 0x04 /* Battery */ +#define AYN_SENSOR_CHARGE_TEMP_REG 0x07 /* Charger IC */ +#define AYN_SENSOR_MB_TEMP_REG 0x05 /* Motherboard */ +#define AYN_SENSOR_PROC_TEMP_REG 0x09 /* CPU Core */ +#define AYN_SENSOR_VCORE_TEMP_REG 0x08 /* vCore */ + + /* Handle ACPI lock mechanism */ #define ACPI_LOCK_DELAY_MS 500 @@ -81,8 +89,19 @@ struct ayn_device { u32 ayn_lock; /* ACPI EC Lock */ } drvdata; -/* Handle ACPI lock mechanism */ -#define ACPI_LOCK_DELAY_MS 500 +struct thermal_sensor { + char *name; + int reg; +}; + +static struct thermal_sensor thermal_sensors[] = { + { "Battery", AYN_SENSOR_BAT_TEMP_REG }, + { "Motherboard", AYN_SENSOR_MB_TEMP_REG }, + { "Charger IC", AYN_SENSOR_CHARGE_TEMP_REG }, + { "vCore", AYN_SENSOR_VCORE_TEMP_REG }, + { "CPU Core", AYN_SENSOR_PROC_TEMP_REG }, + {} +}; static bool lock_global_acpi_lock(void) { @@ -428,6 +447,61 @@ static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point3_temp, pwm_curve, 7); static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point4_temp, pwm_curve, 8); static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point5_temp, pwm_curve, 9); +/** + * thermal_sensor_show() - Read a thermal sensor attribute value. + * + * @dev: The attribute's parent device. + * @attr: The attribute to read. + * @buf: Buffer to write the result into. + * + * Return: Number of bytes read, or an error. + */ +static ssize_t thermal_sensor_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + long ret, val; + int i; + + i = to_sensor_dev_attr(attr)->index; + + ret = read_from_ec(thermal_sensors[i].reg, 1, &val); + if (ret) + return ret; + + val = val * 1000L; + + return sysfs_emit(buf, "%ld\n", val); +} + +/** + * thermal_sensor_label_show() - Read a thermal sensor attribute label. + * + * @dev: The attribute's parent device. + * @attr: The attribute to read. + * @buf: Buffer to read to. + * + * Return: Number of bytes read, or an error. + */ +static ssize_t thermal_sensor_label_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int i = to_sensor_dev_attr(attr)->index; + + return sysfs_emit(buf, "%s\n", thermal_sensors[i].name); +} + +static SENSOR_DEVICE_ATTR_RO(temp1_input, thermal_sensor, 0); +static SENSOR_DEVICE_ATTR_RO(temp2_input, thermal_sensor, 1); +static SENSOR_DEVICE_ATTR_RO(temp3_input, thermal_sensor, 2); +static SENSOR_DEVICE_ATTR_RO(temp4_input, thermal_sensor, 3); +static SENSOR_DEVICE_ATTR_RO(temp5_input, thermal_sensor, 4); +static SENSOR_DEVICE_ATTR_RO(temp1_label, thermal_sensor_label, 0); +static SENSOR_DEVICE_ATTR_RO(temp2_label, thermal_sensor_label, 1); +static SENSOR_DEVICE_ATTR_RO(temp3_label, thermal_sensor_label, 2); +static SENSOR_DEVICE_ATTR_RO(temp4_label, thermal_sensor_label, 3); +static SENSOR_DEVICE_ATTR_RO(temp5_label, thermal_sensor_label, 4); + static struct attribute *ayn_sensors_attrs[] = { &sensor_dev_attr_pwm1_auto_point1_pwm.dev_attr.attr, &sensor_dev_attr_pwm1_auto_point1_temp.dev_attr.attr, @@ -439,6 +513,16 @@ static struct attribute *ayn_sensors_attrs[] = { &sensor_dev_attr_pwm1_auto_point4_temp.dev_attr.attr, &sensor_dev_attr_pwm1_auto_point5_pwm.dev_attr.attr, &sensor_dev_attr_pwm1_auto_point5_temp.dev_attr.attr, + &sensor_dev_attr_temp1_input.dev_attr.attr, + &sensor_dev_attr_temp1_label.dev_attr.attr, + &sensor_dev_attr_temp2_input.dev_attr.attr, + &sensor_dev_attr_temp2_label.dev_attr.attr, + &sensor_dev_attr_temp3_input.dev_attr.attr, + &sensor_dev_attr_temp3_label.dev_attr.attr, + &sensor_dev_attr_temp4_input.dev_attr.attr, + &sensor_dev_attr_temp4_label.dev_attr.attr, + &sensor_dev_attr_temp5_input.dev_attr.attr, + &sensor_dev_attr_temp5_label.dev_attr.attr, NULL, }; From db70b4c7d27ed083574aeb3cfee68a1669df749b Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 26 Jul 2025 13:40:40 -0700 Subject: [PATCH 22/73] [FROM-ML] platform/x86: (ayn-ec) Add RGB Interface Adds an EC controlled LED Multicolor Class Device for controlling the RGB rings around the joysticks. The EC provides a single register for each of the colors red, green, and blue, as well as a mode switching register. The EC accepts values [0-255] for all colors. There are two available effects: breathe, which is the default when the device is started, and monocolor. When resuming from sleep the user selected effect will be overwritten by the EC, so the driver retains the last setting and resets on resume. When setting a color, each color register is set before a final "write" code is sent to the device. The EC may briefly reflect the "write" code when writing, but quickly changes to the "monocolor" value once complete. The driver interprets both of these values as "monocolor" in _show to simplify the sysfs exposed to the user. Two custom attributes are added to the standard LED parent device: effect, a RW file descriptor used to set the effect, and effect_index, which enumerates the available valid options. Signed-off-by: Derek J. Clark --- drivers/platform/x86/Kconfig | 3 + drivers/platform/x86/ayn-ec.c | 285 ++++++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index d06f47aea7752..cb97aac80248d 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -334,6 +334,9 @@ config AYN_EC tristate "AYN x86 devices EC platform control" depends on ACPI depends on HWMON + depends on NEW_LEDS + select LEDS_CLASS + select LEDS_CLASS_MULTICOLOR help This is a driver for AYN and Tectoy x86 handheld devices. It provides temperature monitoring, manual fan speed control, fan curve control, diff --git a/drivers/platform/x86/ayn-ec.c b/drivers/platform/x86/ayn-ec.c index 466cc33adcb08..25f748d7db18e 100644 --- a/drivers/platform/x86/ayn-ec.c +++ b/drivers/platform/x86/ayn-ec.c @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include #include @@ -68,6 +70,16 @@ #define AYN_SENSOR_PROC_TEMP_REG 0x09 /* CPU Core */ #define AYN_SENSOR_VCORE_TEMP_REG 0x08 /* vCore */ +/* EC Controlled RGB registers */ +#define AYN_LED_MC_RED_REG 0xB0 /* Range 0x00-0xFF */ +#define AYN_LED_MC_GREEN_REG 0xB1 /* Range 0x00-0xFF */ +#define AYN_LED_MC_BLUE_REG 0xB2 /* Range 0x00-0xFF */ +#define AYN_RGB_EFFECT_REG 0xB3 + +/* RGB effect modes */ +#define AYN_RGB_EFFECT_BREATHE 0x00 +#define AYN_RGB_EFFECT_MONOCOLOR 0x55 +#define AYN_RGB_EFFECT_WRITE 0xAA /* Handle ACPI lock mechanism */ #define ACPI_LOCK_DELAY_MS 500 @@ -86,7 +98,9 @@ int ayn_pwm_curve_registers[10] = { }; struct ayn_device { + struct led_classdev *led_cdev; u32 ayn_lock; /* ACPI EC Lock */ + u8 rgb_effect; } drvdata; struct thermal_sensor { @@ -103,6 +117,33 @@ static struct thermal_sensor thermal_sensors[] = { {} }; +#define DEVICE_ATTR_RW_NAMED(_name, _attrname) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attrname, .mode = 0644 }, \ + .show = _name##_show, \ + .store = _name##_store, \ + } + +#define DEVICE_ATTR_RO_NAMED(_name, _attrname) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attrname, .mode = 0444 }, \ + .show = _name##_show, \ + } + +/* Handle ACPI lock mechanism */ +#define ACPI_LOCK_DELAY_MS 500 + +/* RGB effect values */ +enum RGB_EFFECT_OPTION { + BREATHE, + MONOCOLOR, +}; + +static const char *const RGB_EFFECT_TEXT[] = { + [BREATHE] = "breathe", + [MONOCOLOR] = "monocolor", +}; + static bool lock_global_acpi_lock(void) { return ACPI_SUCCESS(acpi_acquire_global_lock(ACPI_LOCK_DELAY_MS, @@ -528,10 +569,253 @@ static struct attribute *ayn_sensors_attrs[] = { ATTRIBUTE_GROUPS(ayn_sensors); +/** + * rgb_effect_write() - Set the RGB effect stored in drvdata.rgb_effect. + */ +static int rgb_effect_write(void) +{ + return write_to_ec(AYN_RGB_EFFECT_REG, drvdata.rgb_effect); +}; + +/** + * rgb_effect_read() - Read the RGB effect and store it in drvdata.rgb_effect. + */ +static int rgb_effect_read(void) +{ + int ret; + long effect; + + ret = read_from_ec(AYN_RGB_EFFECT_REG, 1, &effect); + if (ret) + return ret; + + switch (effect) { + case AYN_RGB_EFFECT_WRITE: + case AYN_RGB_EFFECT_MONOCOLOR: + drvdata.rgb_effect = AYN_RGB_EFFECT_WRITE; + break; + default: + drvdata.rgb_effect = AYN_RGB_EFFECT_BREATHE; + } + + return 0; +} + +/** + * rgb_effect_store() - Store the given RGB effect and set it. + * + * @dev: parent device of the given attribute. + * @attr: The attribute to write to. + * @buf: Input value string from sysfs write. + * @count: The number of bytes written. + * + * Return: The number of bytes written, or an error. + */ +static ssize_t rgb_effect_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int ret; + + ret = sysfs_match_string(RGB_EFFECT_TEXT, buf); + if (ret < 0) + return ret; + + if (ret) + drvdata.rgb_effect = AYN_RGB_EFFECT_WRITE; + else + drvdata.rgb_effect = AYN_RGB_EFFECT_BREATHE; + + ret = rgb_effect_write(); + if (ret) + return ret; + + return count; +}; + +/** + * rgb_effect_show() - Read the current RGB effect. + * + * @dev: parent device of the given attribute. + * @attr: The attribute to read. + * @buf: Buffer to read to. + * + * Return: The number of bytes read, or an error. + */ +static ssize_t rgb_effect_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret, i; + + ret = rgb_effect_read(); + if (ret) + return ret; + + switch (drvdata.rgb_effect) { + case AYN_RGB_EFFECT_WRITE: + case AYN_RGB_EFFECT_MONOCOLOR: + i = MONOCOLOR; + break; + default: + i = BREATHE; + break; + } + + return sysfs_emit(buf, "%s\n", RGB_EFFECT_TEXT[i]); +}; + +static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect"); + +/** + * rgb_effect_show() - Display the RGB effects available. + * + * @dev: parent device of the given attribute. + * @attr: The attribute to read. + * @buf: Buffer to read to. + * + * Return: The number of bytes read, or an error. + */ +static ssize_t rgb_effect_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + size_t count = 0; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(RGB_EFFECT_TEXT); i++) + count += sysfs_emit_at(buf, count, "%s ", RGB_EFFECT_TEXT[i]); + + buf[count - 1] = '\n'; + + return count; +} + +static DEVICE_ATTR_RO_NAMED(rgb_effect_index, "effect_index"); + +/** + * ayn_led_mc_brightness_set() - Write the brightness for the RGB LED. + * + * @led_cdev: Parent LED device for the led_classdev_mc. + * @brightness: Brightness value to write [0-255]. + */ +static void ayn_led_mc_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *led_cdev_mc = lcdev_to_mccdev(led_cdev); + struct mc_subled s_led; + int i, ret, val; + + switch (drvdata.rgb_effect) { + case AYN_RGB_EFFECT_WRITE: + case AYN_RGB_EFFECT_MONOCOLOR: + break; + case AYN_RGB_EFFECT_BREATHE: + return; + } + + led_cdev->brightness = brightness; + for (i = 0; i < led_cdev_mc->num_colors; i++) { + s_led = led_cdev_mc->subled_info[i]; + val = brightness * s_led.intensity / led_cdev->max_brightness; + ret = write_to_ec(s_led.channel, val); + if (ret) { + dev_err(led_cdev->dev, + "Error setting brightness: %d\n", ret); + return; + } + } + + /* Must write mode again to change to set color */ + write_to_ec(AYN_RGB_EFFECT_REG, AYN_RGB_EFFECT_WRITE); +}; + +/** + * ayn_led_mc_brightness_get() - Get the brightness for the RGB LED. + * + * @led_cdev: Parent LED device for the led_classdev_mc. + * + * Return: Current brightness. + */ +static enum led_brightness ayn_led_mc_brightness_get(struct led_classdev *led_cdev) +{ + return led_cdev->brightness; +}; + +static struct attribute *ayn_led_mc_attrs[] = { + &dev_attr_rgb_effect.attr, + &dev_attr_rgb_effect_index.attr, + NULL, +}; + +static struct attribute_group ayn_led_mc_group = { + .attrs = ayn_led_mc_attrs, +}; + +struct mc_subled ayn_led_mc_subled_info[] = { + { + .color_index = LED_COLOR_ID_RED, + .brightness = 0, + .intensity = 0, + .channel = AYN_LED_MC_RED_REG, + }, + { + .color_index = LED_COLOR_ID_GREEN, + .brightness = 0, + .intensity = 0, + .channel = AYN_LED_MC_GREEN_REG, + }, + { + .color_index = LED_COLOR_ID_BLUE, + .brightness = 0, + .intensity = 0, + .channel = AYN_LED_MC_BLUE_REG, + }, +}; + +struct led_classdev_mc ayn_led_mc = { + .led_cdev = { + .name = "ayn:rgb:joystick_rings", + .brightness = 0, + .max_brightness = 255, + .brightness_set = ayn_led_mc_brightness_set, + .brightness_get = ayn_led_mc_brightness_get, + .color = LED_COLOR_ID_RGB, + }, + .num_colors = ARRAY_SIZE(ayn_led_mc_subled_info), + .subled_info = ayn_led_mc_subled_info, +}; + +static int ayn_ec_resume(struct platform_device *pdev) +{ + struct led_classdev *led_cdev = drvdata.led_cdev; + int ret; + + ret = rgb_effect_write(); + if (ret) + return ret; + + ayn_led_mc_brightness_set(led_cdev, led_cdev->brightness); + + return 0; +} + static int ayn_ec_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct device *hwdev; + int ret; + + ret = devm_led_classdev_multicolor_register(dev, &ayn_led_mc); + if (ret) + return ret; + + ret = devm_device_add_group(ayn_led_mc.led_cdev.dev, &ayn_led_mc_group); + if (ret) + return ret; + + drvdata.led_cdev = &ayn_led_mc.led_cdev; + ret = rgb_effect_read(); + if (ret) + return ret; hwdev = devm_hwmon_device_register_with_info(dev, "aynec", NULL, &ayn_ec_chip_info, @@ -544,6 +828,7 @@ static struct platform_driver ayn_ec_driver = { .name = "ayn-ec", }, .probe = ayn_ec_probe, + .resume = ayn_ec_resume, }; static struct platform_device *ayn_ec_device; From 17de0c4898df514bae642a934ba550db9962bb58 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 26 Jul 2025 13:40:41 -0700 Subject: [PATCH 23/73] [FROM-ML] platform/x86: (ayn-ec) Add AYN EC Platform Documentation Adds ABI documentation for the ayn-ec platform driver Signed-off-by: Derek J. Clark --- .../ABI/testing/sysfs-platform-ayn-ec | 59 +++++++++++++++++++ MAINTAINERS | 1 + 2 files changed, 60 insertions(+) create mode 100644 Documentation/ABI/testing/sysfs-platform-ayn-ec diff --git a/Documentation/ABI/testing/sysfs-platform-ayn-ec b/Documentation/ABI/testing/sysfs-platform-ayn-ec new file mode 100644 index 0000000000000..32cb6f7ca2fc5 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-platform-ayn-ec @@ -0,0 +1,59 @@ +What: /sys/class/hwmon/hwmon[0-9]/pwm1_enable +Date: July 2025 +KernelVersion: 6.17 +Contact: "Derek J. Clark" +Description: + This sets the PWM fan mode of operation. Valid values are [0-3]. + Values [0-2] conform with standard hwmon operating modes. Value 3 + enables user defined fan curve settings. + + Applies to AYN Loki and Tectoy Zeenix lines of handheld devices. + +What: /sys/class/hwmon/hwmon[0-9]/pwm1_auto_point[1-5]_pwm +Date: July 2025 +KernelVersion: 6.17 +Contact: "Derek J. Clark" +Description: + This sets the PWM fan duty cycle for the given index of the fan curve. + When the temperature reaches the corresponding pwm1_auto_point[1-5]_temp, + the EC will automatically increase the fan duty cycle to the given value. + + Values are [0-255] + + Applies to AYN Loki and Tectoy Zeenix lines of handheld devices. + +What: /sys/class/hwmon/hwmon[0-9]/pwm1_auto_point[1-5]_temp +Date: July 2025 +KernelVersion: 6.17 +Contact: "Derek J. Clark" +Description: + This sets the activation temperature for the given index of the fan curve. + When the temperature reaches the given value, the EC will automatically + increase the fan duty cycle to the corresponding pwm1_auto_point[1-5]_pwm + value. + + Values are [0-100] + + Applies to AYN Loki and Tectoy Zeenix lines of handheld devices. + +What: /sys/class/leds/ayn:rgb:joystick_rings/effect +Date: July 2025 +KernelVersion: 6.17 +Contact: "Derek J. Clark" +Description: + This controls the display effect of the RGB interface. + + Values are monocolor or breathe. + + Applies to AYN Loki and Tectoy Zeenix lines of handheld devices. + +What: /sys/class/leds/ayn:rgb:joystick_rings/effect_index +Date: July 2025 +KernelVersion: 6.17 +Contact: "Derek J. Clark" +Description: + This displays the available options for the effect attribute. + + Values are monocolor or breathe. + + Applies to AYN Loki and Tectoy Zeenix lines of handheld devices. diff --git a/MAINTAINERS b/MAINTAINERS index a9dde8924fd03..5339fe8f1fc10 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4377,6 +4377,7 @@ AYN PLATFORM EC DRIVER M: Derek J. Clark L: platform-driver-x86@vger.kernel.org S: Maintained +F: Documentation/ABI/testing/sysfs-platform-ayn-ec F: drivers/platform/x86/ayn-ec.c AYANEO PLATFORM EC DRIVER From 85f1a6f3df53d09aa3e863aef9384273d972d318 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sun, 10 May 2026 04:25:40 +0000 Subject: [PATCH 24/73] [FROM-ML] platform/x86: lenovo-wmi-other: Add missing CPU tunable attributes Use an enum for all device ID's and CPU attribute feature ID's, add missing CPU attributes. Reviewed-by: Rong Zhang Reviewed-by: Mark Pearson Signed-off-by: Derek J. Clark --- .../wmi/devices/lenovo-wmi-other.rst | 9 ++ drivers/platform/x86/lenovo/wmi-capdata.h | 5 +- drivers/platform/x86/lenovo/wmi-other.c | 99 ++++++++++++++++++- 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst index 01d4711567380..189dd4d31926d 100644 --- a/Documentation/wmi/devices/lenovo-wmi-other.rst +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst @@ -68,9 +68,18 @@ Each attribute has the following properties: - type The following firmware-attributes are implemented: + - cpu_temp: CPU Thermal Load Limit + - ppt_cpu_cl: CPU Cross Loading Power Limit + - ppt_pl1_apu_spl: Platform Profile Tracking APU Sustained Power Limit - ppt_pl1_spl: Platform Profile Tracking Sustained Power Limit + - ppt_pl1_spl_cl: Platform Profile Tracking Cross Loading Sustained Power Limit + - ppt_pl1_tau: Exceed Duration for Platform Profile Tracking Sustained Power Limit - ppt_pl2_sppt: Platform Profile Tracking Slow Package Power Tracking + - ppt_pl2_sppt_cl: Platform Profile Tracking Cross Loading Slow Package Tracking - ppt_pl3_fppt: Platform Profile Tracking Fast Package Power Tracking + - ppt_pl3_fppt_cl: Platform Profile Tracking Cross Loading Fast Package Power Tracking + - ppt_pl4_ipl: Platform Profile Tracking Instantaneous Power Limit + - ppt_pl4_ipl_cl: Platform Profile Tracking Cross Loading Instantaneous Power Limit LENOVO_FAN_TEST_DATA ------------------------- diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h index c3e760b8c3c3d..c74a0e5294e78 100644 --- a/drivers/platform/x86/lenovo/wmi-capdata.h +++ b/drivers/platform/x86/lenovo/wmi-capdata.h @@ -18,7 +18,10 @@ #define LWMI_ATTR_MODE_ID_MASK GENMASK(15, 8) #define LWMI_ATTR_TYPE_ID_MASK GENMASK(7, 0) -#define LWMI_DEVICE_ID_FAN 0x04 +enum lwmi_device_id { + LWMI_DEVICE_ID_CPU = 0x01, + LWMI_DEVICE_ID_FAN = 0x04, +}; #define LWMI_TYPE_ID_NONE 0x00 diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c index d318ba432fdcc..fe4cd7da017eb 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -51,14 +51,21 @@ #define LENOVO_OTHER_MODE_GUID "DC2A8805-3A8C-41BA-A6F7-092E0089CD3B" -#define LWMI_DEVICE_ID_CPU 0x01 - -#define LWMI_FEATURE_ID_CPU_SPPT 0x01 -#define LWMI_FEATURE_ID_CPU_SPL 0x02 -#define LWMI_FEATURE_ID_CPU_FPPT 0x03 +enum lwmi_feature_id_cpu { + LWMI_FEATURE_ID_CPU_SPPT = 0x01, + LWMI_FEATURE_ID_CPU_SPL = 0x02, + LWMI_FEATURE_ID_CPU_FPPT = 0x03, + LWMI_FEATURE_ID_CPU_TEMP = 0x04, + LWMI_FEATURE_ID_CPU_APU = 0x05, + LWMI_FEATURE_ID_CPU_CL = 0x06, + LWMI_FEATURE_ID_CPU_TAU = 0x07, + LWMI_FEATURE_ID_CPU_IPL = 0x09, +}; #define LWMI_FEATURE_ID_FAN_RPM 0x03 +#define LWMI_TYPE_ID_CROSSLOAD 0x01 + #define LWMI_FEATURE_VALUE_GET 17 #define LWMI_FEATURE_VALUE_SET 18 @@ -566,18 +573,72 @@ static struct tunable_attr_01 ppt_pl1_spl = { .type_id = LWMI_TYPE_ID_NONE, }; +static struct tunable_attr_01 ppt_pl1_spl_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_SPL, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + static struct tunable_attr_01 ppt_pl2_sppt = { .device_id = LWMI_DEVICE_ID_CPU, .feature_id = LWMI_FEATURE_ID_CPU_SPPT, .type_id = LWMI_TYPE_ID_NONE, }; +static struct tunable_attr_01 ppt_pl2_sppt_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_SPPT, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + static struct tunable_attr_01 ppt_pl3_fppt = { .device_id = LWMI_DEVICE_ID_CPU, .feature_id = LWMI_FEATURE_ID_CPU_FPPT, .type_id = LWMI_TYPE_ID_NONE, }; +static struct tunable_attr_01 ppt_pl3_fppt_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_FPPT, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + +static struct tunable_attr_01 cpu_temp = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_TEMP, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl1_apu_spl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_APU, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_cpu_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_CL, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl1_tau = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_TAU, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl4_ipl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_IPL, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl4_ipl_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_IPL, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + struct capdata01_attr_group { const struct attribute_group *attr_group; struct tunable_attr_01 *tunable_attr; @@ -913,17 +974,45 @@ static bool lwmi_attr_01_is_supported(struct tunable_attr_01 *tunable_attr) .name = _fsname, .attrs = _attrname##_attrs \ } +LWMI_ATTR_GROUP_TUNABLE_CAP01(cpu_temp, "cpu_temp", + "Set the CPU thermal load limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_cpu_cl, "ppt_cpu_cl", + "Set the CPU cross loading power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_apu_spl, "ppt_pl1_apu_spl", + "Set the APU sustained power limit"); LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_spl, "ppt_pl1_spl", "Set the CPU sustained power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_spl_cl, "ppt_pl1_spl_cl", + "Set the CPU cross loading sustained power limit"); LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl2_sppt, "ppt_pl2_sppt", "Set the CPU slow package power tracking limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl2_sppt_cl, "ppt_pl2_sppt_cl", + "Set the CPU cross loading slow package power tracking limit"); LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl3_fppt, "ppt_pl3_fppt", "Set the CPU fast package power tracking limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl3_fppt_cl, "ppt_pl3_fppt_cl", + "Set the CPU cross loading fast package power tracking limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_tau, "ppt_pl1_tau", + "Set the CPU sustained power limit exceed duration"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl4_ipl, "ppt_pl4_ipl", + "Set the CPU instantaneous power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl4_ipl_cl, "ppt_pl4_ipl_cl", + "Set the CPU cross loading instantaneous power limit"); + static struct capdata01_attr_group cd01_attr_groups[] = { + { &cpu_temp_attr_group, &cpu_temp }, + { &ppt_cpu_cl_attr_group, &ppt_cpu_cl }, + { &ppt_pl1_apu_spl_attr_group, &ppt_pl1_apu_spl }, { &ppt_pl1_spl_attr_group, &ppt_pl1_spl }, + { &ppt_pl1_spl_cl_attr_group, &ppt_pl1_spl_cl }, + { &ppt_pl1_tau_attr_group, &ppt_pl1_tau }, { &ppt_pl2_sppt_attr_group, &ppt_pl2_sppt }, + { &ppt_pl2_sppt_cl_attr_group, &ppt_pl2_sppt_cl }, { &ppt_pl3_fppt_attr_group, &ppt_pl3_fppt }, + { &ppt_pl3_fppt_cl_attr_group, &ppt_pl3_fppt_cl }, + { &ppt_pl4_ipl_attr_group, &ppt_pl4_ipl }, + { &ppt_pl4_ipl_cl_attr_group, &ppt_pl4_ipl_cl }, {}, }; From a635ae28ab1815b280470ba67624323fac563fe7 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sun, 10 May 2026 04:25:41 +0000 Subject: [PATCH 25/73] [FROM-ML] platform/x86: lenovo-wmi-other: Add GPU tunable attributes Use an enum for all GPU attribute feature ID's and add GPU attributes. Reviewed-by: Rong Zhang Reviewed-by: Mark Pearson Signed-off-by: Derek J. Clark --- .../wmi/devices/lenovo-wmi-other.rst | 10 ++ drivers/platform/x86/lenovo/wmi-capdata.h | 1 + drivers/platform/x86/lenovo/wmi-other.c | 105 ++++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst index 189dd4d31926d..011054d64eac5 100644 --- a/Documentation/wmi/devices/lenovo-wmi-other.rst +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst @@ -69,6 +69,16 @@ Each attribute has the following properties: The following firmware-attributes are implemented: - cpu_temp: CPU Thermal Load Limit + - dgpu_boost_clk: Dedicated GPU Boost Clock + - dgpu_didvid: Dedicated GPU Device Identifier and Vendor Identifier + - dgpu_enable: Dedicated GPU Enabled Status + - gpu_mode: GPU Mode by Power Limit + - gpu_nv_ac_offset: Nvidia GPU AC Total Processing Power Baseline Offset + - gpu_nv_bpl: Nvidia GPU Base Power Limit + - gpu_nv_cpu_boost: Nvidia GPU to CPU Dynamic Boost Limit + - gpu_nv_ctgp: Nvidia GPU Configurable Total Graphics Power + - gpu_nv_ppab: Nvidia GPU Power Performance Aware Boost Limit + - gpu_temp: GPU Thermal Load Limit - ppt_cpu_cl: CPU Cross Loading Power Limit - ppt_pl1_apu_spl: Platform Profile Tracking APU Sustained Power Limit - ppt_pl1_spl: Platform Profile Tracking Sustained Power Limit diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h index c74a0e5294e78..a7cfdeaa58f77 100644 --- a/drivers/platform/x86/lenovo/wmi-capdata.h +++ b/drivers/platform/x86/lenovo/wmi-capdata.h @@ -20,6 +20,7 @@ enum lwmi_device_id { LWMI_DEVICE_ID_CPU = 0x01, + LWMI_DEVICE_ID_GPU = 0x02, LWMI_DEVICE_ID_FAN = 0x04, }; diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c index fe4cd7da017eb..9a6183bbcc920 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -62,6 +62,19 @@ enum lwmi_feature_id_cpu { LWMI_FEATURE_ID_CPU_IPL = 0x09, }; +enum lwmi_feature_id_gpu { + LWMI_FEATURE_ID_GPU_NV_PPAB = 0x01, + LWMI_FEATURE_ID_GPU_NV_CTGP = 0x02, + LWMI_FEATURE_ID_GPU_TEMP = 0x03, + LWMI_FEATURE_ID_GPU_AC_OFFSET = 0x04, + LWMI_FEATURE_ID_DGPU_BOOST_CLK = 0x06, + LWMI_FEATURE_ID_DGPU_EN = 0x07, + LWMI_FEATURE_ID_GPU_MODE = 0x08, + LWMI_FEATURE_ID_DGPU_DIDVID = 0x09, + LWMI_FEATURE_ID_GPU_NV_BPL = 0x0a, + LWMI_FEATURE_ID_GPU_NV_CPU_BOOST = 0x0b, +}; + #define LWMI_FEATURE_ID_FAN_RPM 0x03 #define LWMI_TYPE_ID_CROSSLOAD 0x01 @@ -639,6 +652,66 @@ static struct tunable_attr_01 ppt_pl4_ipl_cl = { .type_id = LWMI_TYPE_ID_CROSSLOAD, }; +static struct tunable_attr_01 gpu_nv_ppab = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_PPAB, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_ctgp = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_CTGP, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_temp = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_TEMP, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_ac_offset = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_AC_OFFSET, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 dgpu_boost_clk = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_DGPU_BOOST_CLK, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 dgpu_enable = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_DGPU_EN, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_mode = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_MODE, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 dgpu_didvid = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_DGPU_DIDVID, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_bpl = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_BPL, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_cpu_boost = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_CPU_BOOST, + .type_id = LWMI_TYPE_ID_NONE, +}; + struct capdata01_attr_group { const struct attribute_group *attr_group; struct tunable_attr_01 *tunable_attr; @@ -974,6 +1047,7 @@ static bool lwmi_attr_01_is_supported(struct tunable_attr_01 *tunable_attr) .name = _fsname, .attrs = _attrname##_attrs \ } +/* CPU tunable attributes */ LWMI_ATTR_GROUP_TUNABLE_CAP01(cpu_temp, "cpu_temp", "Set the CPU thermal load limit"); LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_cpu_cl, "ppt_cpu_cl", @@ -999,9 +1073,40 @@ LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl4_ipl, "ppt_pl4_ipl", LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl4_ipl_cl, "ppt_pl4_ipl_cl", "Set the CPU cross loading instantaneous power limit"); +/* GPU tunable attributes */ +LWMI_ATTR_GROUP_TUNABLE_CAP01(dgpu_boost_clk, "dgpu_boost_clk", + "Set the dedicated GPU boost clock"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(dgpu_didvid, "dgpu_didvid", + "Get the GPU device identifier and vendor identifier"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(dgpu_enable, "dgpu_enable", + "Set the dedicated Nvidia GPU enabled status"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_mode, "gpu_mode", + "Set the GPU mode by power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_ac_offset, "gpu_nv_ac_offset", + "Set the Nvidia GPU AC total processing power baseline offset"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_bpl, "gpu_nv_bpl", + "Set the Nvidia GPU base power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_cpu_boost, "gpu_nv_cpu_boost", + "Set the Nvidia GPU to CPU dynamic boost limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_ctgp, "gpu_nv_ctgp", + "Set the GPU configurable total graphics power"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_ppab, "gpu_nv_ppab", + "Set the Nvidia GPU power performance aware boost limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_temp, "gpu_temp", + "Set the GPU thermal load limit"); static struct capdata01_attr_group cd01_attr_groups[] = { { &cpu_temp_attr_group, &cpu_temp }, + { &dgpu_boost_clk_attr_group, &dgpu_boost_clk }, + { &dgpu_didvid_attr_group, &dgpu_didvid }, + { &dgpu_enable_attr_group, &dgpu_enable }, + { &gpu_mode_attr_group, &gpu_mode }, + { &gpu_nv_ac_offset_attr_group, &gpu_nv_ac_offset }, + { &gpu_nv_bpl_attr_group, &gpu_nv_bpl }, + { &gpu_nv_cpu_boost_attr_group, &gpu_nv_cpu_boost }, + { &gpu_nv_ctgp_attr_group, &gpu_nv_ctgp }, + { &gpu_nv_ppab_attr_group, &gpu_nv_ppab }, + { &gpu_temp_attr_group, &gpu_temp }, { &ppt_cpu_cl_attr_group, &ppt_cpu_cl }, { &ppt_pl1_apu_spl_attr_group, &ppt_pl1_apu_spl }, { &ppt_pl1_spl_attr_group, &ppt_pl1_spl }, From 8002b33f9080ca60f6cec46826169c6ee747fd23 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sun, 10 May 2026 04:25:42 +0000 Subject: [PATCH 26/73] [FROM-ML] platform/x86: lenovo-wmi-other: Rename LWMI_OM_FW_ATTR_BASE_PATH In the next patch a power supply extension is added which requires a name attribute. Instead of creating another const macro with the same information, rename LWMI_OM_FW_ATTR_BASE_PATH to LWMI_OM_SYSFS_NAME. Reviewed-by: Rong Zhang Tested-by: Rong Zhang Reviewed-by: Mark Pearson Signed-off-by: Derek J. Clark --- drivers/platform/x86/lenovo/wmi-other.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c index 9a6183bbcc920..beb92c7cff257 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -92,7 +92,7 @@ enum lwmi_feature_id_gpu { lwmi_attr_id(LWMI_DEVICE_ID_FAN, LWMI_FEATURE_ID_FAN_RPM, \ LWMI_GZ_THERMAL_MODE_NONE, LWMI_FAN_ID(x)) -#define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other" +#define LWMI_OM_SYSFS_NAME "lenovo-wmi-other" #define LWMI_OM_HWMON_NAME "lenovo_wmi_other" static DEFINE_IDA(lwmi_om_ida); @@ -1138,8 +1138,7 @@ static void lwmi_om_fw_attr_add(struct lwmi_om_priv *priv) priv->fw_attr_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), NULL, "%s-%u", - LWMI_OM_FW_ATTR_BASE_PATH, - priv->ida_id); + LWMI_OM_SYSFS_NAME, priv->ida_id); if (IS_ERR(priv->fw_attr_dev)) { err = PTR_ERR(priv->fw_attr_dev); goto err_free_ida; From 5f902257e7f1e09a4f7f75ce06a32d3f17773315 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sun, 10 May 2026 04:25:43 +0000 Subject: [PATCH 27/73] [FROM-ML] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting Add charge_behaviour and charge_control_end_threshold attributes through a power supply extension for devices that support WMI based charge enable & disable. Lenovo Legion devices that implement WMI function and capdata ID 0x03010001 in their BIOS are able to enable or disable charging at 80% through the lenovo-wmi-other interface. Add a charge_control_end_threshold attribute for BATX devices to expose this capability. The GET method for this attribute is bugged. After analyzing the DSDT and some testing it appears the method grabs bit(3) instead of bit(4) from the EC register that stores the current status, and will only report if charging has been inhibited or not. To work around this, store and report the last setting written to the attribute. Additionally, devices that support WMI function and capdata ID 0x03020000 are able to force discharge of the battery. Expose this capability with a charge_behaviour attribute in the power supply extension, with the AUTO and FORCE_DISCHARGE behaviors enabled. As some devices only expose one attribute or the other, a bitmask is added with a lookup table and some helper macros to select the correct configuration for the hardware at runtime. The ideapad_laptop driver provides the charge_type attribute to provide similar functionality. When the WMI method is set this can corrupt the ACPI method return and cause hardware and driver errors. To avoid conflicts between the drivers, we get the acpi_handle and do the same check that ideapad_laptop does when it enables the feature. If the feature is supported in ideapad_laptop, abort adding the extension from lenovo-wmi-other. The ACPI method is more reliable when both are present, from my testing, so we can prefer that implementation and do not need to worry about de-conflicting from inside that driver. A new module parameter, force_load_psy_ext, is provided to bypass this ACPI check, as well as feature supported checks, if desired. Signed-off-by: Derek J. Clark --- drivers/platform/x86/lenovo/Kconfig | 1 + drivers/platform/x86/lenovo/wmi-capdata.h | 1 + drivers/platform/x86/lenovo/wmi-other.c | 388 ++++++++++++++++++++++ 3 files changed, 390 insertions(+) diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig index 09b1b055d2e01..b9a5d18caa1e3 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -262,6 +262,7 @@ config LENOVO_WMI_GAMEZONE config LENOVO_WMI_TUNING tristate "Lenovo Other Mode WMI Driver" depends on ACPI_WMI + depends on ACPI_BATTERY select HWMON select FW_ATTR_CLASS select LENOVO_WMI_CAPDATA diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h index a7cfdeaa58f77..a49229cec2451 100644 --- a/drivers/platform/x86/lenovo/wmi-capdata.h +++ b/drivers/platform/x86/lenovo/wmi-capdata.h @@ -21,6 +21,7 @@ enum lwmi_device_id { LWMI_DEVICE_ID_CPU = 0x01, LWMI_DEVICE_ID_GPU = 0x02, + LWMI_DEVICE_ID_PSU = 0x03, LWMI_DEVICE_ID_FAN = 0x04, }; diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c index beb92c7cff257..b7f49f5e0e53e 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -41,9 +41,12 @@ #include #include #include +#include #include #include +#include + #include "wmi-capdata.h" #include "wmi-events.h" #include "wmi-helpers.h" @@ -75,9 +78,15 @@ enum lwmi_feature_id_gpu { LWMI_FEATURE_ID_GPU_NV_CPU_BOOST = 0x0b, }; +enum lwmi_feature_id_psu { + LWMI_FEATURE_ID_PSU_CHARGE_END_THRESHOLD = 0x01, + LWMI_FEATURE_ID_PSU_CHARGE_BEHAVIOR = 0x02, +}; + #define LWMI_FEATURE_ID_FAN_RPM 0x03 #define LWMI_TYPE_ID_CROSSLOAD 0x01 +#define LWMI_TYPE_ID_PSU_AC 0x01 #define LWMI_FEATURE_VALUE_GET 17 #define LWMI_FEATURE_VALUE_SET 18 @@ -88,10 +97,19 @@ enum lwmi_feature_id_gpu { #define LWMI_FAN_DIV 100 +#define LWMI_CHARGE_BEHAVIOR_DISCHARGE 0x00 +#define LWMI_CHARGE_BEHAVIOR_AUTO 0x01 +#define LWMI_CHARGE_END_100 0x00 +#define LWMI_CHARGE_END_80 0x01 + #define LWMI_ATTR_ID_FAN_RPM(x) \ lwmi_attr_id(LWMI_DEVICE_ID_FAN, LWMI_FEATURE_ID_FAN_RPM, \ LWMI_GZ_THERMAL_MODE_NONE, LWMI_FAN_ID(x)) +#define LWMI_ATTR_ID_PSU(feat, type) \ + lwmi_attr_id(LWMI_DEVICE_ID_PSU, feat, \ + LWMI_GZ_THERMAL_MODE_NONE, type) + #define LWMI_OM_SYSFS_NAME "lenovo-wmi-other" #define LWMI_OM_HWMON_NAME "lenovo_wmi_other" @@ -131,6 +149,11 @@ struct lwmi_om_priv { bool capdata00_collected : 1; bool capdata_fan_collected : 1; } fan_flags; + + enum power_supply_charge_behaviour charge_behaviour; + const struct power_supply_ext *battery_ext; + struct acpi_battery_hook battery_hook; + bool bh_registered; }; /* @@ -557,6 +580,368 @@ static void lwmi_om_fan_info_collect_cd_fan(struct device *dev, struct cd_list * lwmi_om_hwmon_add(priv); } +/* ======== Power Supply Extension (component: lenovo-wmi-capdata 00) ======== */ + +/** + * lwmi_psy_ext_get_prop() - Get a power_supply_ext property + * @ps: The battery that was extended + * @ext: The extension + * @ext_data: Pointer to the lwmi_om_priv drvdata + * @prop: The property to read + * @val: The value to return + * + * Writes the given value to the power_supply_ext property + * + * Return: 0 on success, or an error + */ +static int lwmi_psy_ext_get_prop(struct power_supply *ps, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property prop, + union power_supply_propval *val) +{ + struct lwmi_om_priv *priv = ext_data; + struct wmi_method_args_32 args = {}; + u32 retval; + int ret; + + switch (prop) { + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + /* Reading from BIOS reads the wrong bit. Use cached value */ + val->intval = priv->charge_behaviour; + return 0; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + args.arg0 = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_END_THRESHOLD, + LWMI_TYPE_ID_PSU_AC); + break; + default: + return -EINVAL; + } + + ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_GET, + (u8 *)&args, sizeof(args), + &retval); + if (ret) + return ret; + + dev_dbg(&priv->wdev->dev, "Got return value %#x for property %#x\n", retval, prop); + + switch (retval) { + case LWMI_CHARGE_END_80: + val->intval = 80; + break; + case LWMI_CHARGE_END_100: + val->intval = 100; + break; + default: + dev_err(&priv->wdev->dev, "Got invalid charge limit value: %#x\n", retval); + return -EINVAL; + } + + return 0; +} + +/** + * lwmi_psy_ext_set_prop() - Set a power_supply_ext property + * @ps: The battery that was extended + * @ext: The extension + * @ext_data: Pointer to the lwmi_om_priv drvdata + * @prop: The property to write + * @val: The value to write + * + * Writes the given value to the power_supply_ext property + * + * Return: 0 on success, or an error + */ +static int lwmi_psy_ext_set_prop(struct power_supply *ps, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property prop, + const union power_supply_propval *val) +{ + struct lwmi_om_priv *priv = ext_data; + struct wmi_method_args_32 args = {}; + + switch (prop) { + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + args.arg0 = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_BEHAVIOR, + LWMI_TYPE_ID_NONE); + switch (val->intval) { + case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO: + args.arg1 = LWMI_CHARGE_BEHAVIOR_AUTO; + break; + case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE: + args.arg1 = LWMI_CHARGE_BEHAVIOR_DISCHARGE; + break; + default: + dev_err(&priv->wdev->dev, "Got invalid charge behavior value: %#x\n", + val->intval); + return -EINVAL; + } + priv->charge_behaviour = val->intval; + break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + args.arg0 = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_END_THRESHOLD, + LWMI_TYPE_ID_PSU_AC); + switch (val->intval) { + case 0 ... 80: + args.arg1 = LWMI_CHARGE_END_80; + break; + case 81 ... 100: + args.arg1 = LWMI_CHARGE_END_100; + break; + default: + dev_err(&priv->wdev->dev, "Got invalid charge limit value: %#x\n", + val->intval); + return -EINVAL; + } + break; + default: + return false; + } + + dev_dbg(&priv->wdev->dev, "Attempting to set %#010x for property %#x to %#x\n", + args.arg0, prop, args.arg1); + + return lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_SET, + (u8 *)&args, sizeof(args), NULL); +} + +/** + * lwmi_psy_prop_is_suppported() - Determine if the property is supported + * @priv: Pointer to the lwmi_om_priv drvdata + * @prop: The power supply property to be evaluated + * + * Checks capdata 00 to determine if the property is supported. + * + * Return: true if readable, or false + */ +static bool lwmi_psy_prop_is_supported(struct lwmi_om_priv *priv, enum power_supply_property prop) +{ + struct capdata00 capdata; + u32 attribute_id; + int ret; + + switch (prop) { + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + attribute_id = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_BEHAVIOR, + LWMI_TYPE_ID_NONE); + break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + attribute_id = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_END_THRESHOLD, + LWMI_TYPE_ID_PSU_AC); + break; + default: + return false; + } + + ret = lwmi_cd00_get_data(priv->cd00_list, attribute_id, &capdata); + if (ret) + return false; + + dev_dbg(&priv->wdev->dev, "Battery charge feature (%#010x) support level: %#x\n", + attribute_id, capdata.supported); + + return (capdata.supported & LWMI_SUPP_VALID) && (capdata.supported & LWMI_SUPP_GET); +} + +/** + * lwmi_psy_prop_is_writeable() - Determine if the property is writeable + * @ps: The battery that was extended + * @ext: The extension + * @ext_data: Pointer the lwmi_om_priv drvdata + * @prop: The property to check + * + * Checks capdata 00 to determine if the property is writable. + * + * Return: true if writable, or false + */ +static int lwmi_psy_prop_is_writeable(struct power_supply *ps, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property prop) +{ + struct lwmi_om_priv *priv = ext_data; + struct capdata00 capdata; + u32 attribute_id; + int ret; + + switch (prop) { + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + attribute_id = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_BEHAVIOR, + LWMI_TYPE_ID_NONE); + break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + attribute_id = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_END_THRESHOLD, + LWMI_TYPE_ID_PSU_AC); + break; + default: + return false; + } + + ret = lwmi_cd00_get_data(priv->cd00_list, attribute_id, &capdata); + if (ret) + return false; + + return !!(capdata.supported & LWMI_SUPP_SET); +} + +static const enum power_supply_property lwmi_psy_ext_props_all[] = { + POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR, + POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD, +}; + +static const enum power_supply_property lwmi_psy_ext_props_threshold[] = { + POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD, +}; + +static const enum power_supply_property lwmi_psy_ext_props_behaviour[] = { + POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR, +}; + +#define DEFINE_LWMI_POWER_SUPPLY_EXTENSION(_name, _props, _behaviours) \ + static const struct power_supply_ext _name = { \ + .name = LWMI_OM_SYSFS_NAME, \ + .properties = _props, \ + .num_properties = ARRAY_SIZE(_props), \ + .charge_behaviours = _behaviours, \ + .get_property = lwmi_psy_ext_get_prop, \ + .set_property = lwmi_psy_ext_set_prop, \ + .property_is_writeable = lwmi_psy_prop_is_writeable, \ + } + +#define LWMI_CHARGE_BEHAVIOURS (BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) | \ + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)) + +DEFINE_LWMI_POWER_SUPPLY_EXTENSION(lwmi_psy_ext_all, lwmi_psy_ext_props_all, + LWMI_CHARGE_BEHAVIOURS); +DEFINE_LWMI_POWER_SUPPLY_EXTENSION(lwmi_psy_ext_threshold, + lwmi_psy_ext_props_threshold, 0); +DEFINE_LWMI_POWER_SUPPLY_EXTENSION(lwmi_psy_ext_behaviour, + lwmi_psy_ext_props_behaviour, + LWMI_CHARGE_BEHAVIOURS); + +#define LWMI_PSY_PROP_BEHAVIOUR BIT(0) +#define LWMI_PSY_PROP_THRESHOLD BIT(1) + +static const struct power_supply_ext *lwmi_psy_exts[] = { + [LWMI_PSY_PROP_BEHAVIOUR] = &lwmi_psy_ext_behaviour, + [LWMI_PSY_PROP_THRESHOLD] = &lwmi_psy_ext_threshold, + [LWMI_PSY_PROP_BEHAVIOUR | LWMI_PSY_PROP_THRESHOLD] = &lwmi_psy_ext_all, +}; + +/** + * lwmi_add_battery() - Connect the power_supply_ext + * @battery: The battery to extend + * @hook: The driver hook used to extend the battery + * + * Return: 0 on success, or an error. + */ +static int lwmi_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct lwmi_om_priv *priv = container_of(hook, struct lwmi_om_priv, battery_hook); + + return power_supply_register_extension(battery, priv->battery_ext, &priv->wdev->dev, priv); +} + +/** + * lwmi_remove_battery() - Disconnect the power_supply_ext + * @battery: The battery that was extended + * @hook: The driver hook used to extend the battery + * + * Return: 0 on success, or an error. + */ +static int lwmi_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct lwmi_om_priv *priv = container_of(hook, struct lwmi_om_priv, battery_hook); + + power_supply_unregister_extension(battery, priv->battery_ext); + return 0; +} + +/** + * lwmi_acpi_match() - Attempts to return the ideapad acpi handle + * @handle: The ACPI handle that manages battery charging + * @lvl: Unused + * @context: Void pointer to the acpi_handle object to return + * @retval: Unused + * + * Checks if the ideapad_laptop driver is going to manage charge_type first, + * then if not, hooks the battery to our WMI methods. + * + * Return: AE_CTRL_TERMINATE if found, AE_OK if not found. + */ +static acpi_status lwmi_acpi_match(acpi_handle handle, u32 lvl, + void *context, void **retval) +{ + acpi_handle *ahand = context; + + if (!handle) + return AE_OK; + + *ahand = handle; + + return AE_CTRL_TERMINATE; +} + +/** + * lwmi_om_psy_ext_init() - Hooks power supply extension to device battery + * @priv: Pointer to the lwmi_om_priv drvdata. + * + * Checks if the ideapad_laptop driver is going to manage charge attributes first, + * then if not, hooks the battery to our WMI methods if they are supported. + */ +static void lwmi_om_psy_ext_init(struct lwmi_om_priv *priv) +{ + static const char * const ideapad_hid = "VPC2004"; + acpi_handle handle = NULL; + unsigned int props = 0; + int ret; + + priv->bh_registered = false; + + /* Deconflict ideapad_laptop driver */ + ret = acpi_get_devices(ideapad_hid, lwmi_acpi_match, &handle, NULL); + if (ret) + return; + + if (handle && acpi_has_method(handle, "GBMD") && acpi_has_method(handle, "SBMC")) { + dev_dbg(&priv->wdev->dev, "ideapad_laptop driver manages battery for device\n"); + return; + } + + if (lwmi_psy_prop_is_supported(priv, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR)) + props |= LWMI_PSY_PROP_BEHAVIOUR; + if (lwmi_psy_prop_is_supported(priv, POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD)) + props |= LWMI_PSY_PROP_THRESHOLD; + if (!props) + return; + + /* Add battery hooks */ + priv->battery_ext = lwmi_psy_exts[props]; + priv->battery_hook.add_battery = lwmi_add_battery; + priv->battery_hook.remove_battery = lwmi_remove_battery; + priv->battery_hook.name = "Lenovo WMI Other Battery Extension"; + priv->bh_registered = true; + + battery_hook_register(&priv->battery_hook); +} + +/** + * lwmi_om_psy_remove() - Unregister battery hook + * @priv: Driver private data + * + * Unregisters the battery hook if applicable. + */ +static void lwmi_om_psy_remove(struct lwmi_om_priv *priv) +{ + if (!priv->bh_registered) + return; + + battery_hook_unregister(&priv->battery_hook); + priv->bh_registered = false; +} + /* ======== fw_attributes (component: lenovo-wmi-capdata 01) ======== */ struct tunable_attr_01 { @@ -1243,6 +1628,7 @@ static int lwmi_om_master_bind(struct device *dev) } lwmi_om_fan_info_collect_cd00(priv); + lwmi_om_psy_ext_init(priv); lwmi_om_fw_attr_add(priv); @@ -1265,6 +1651,8 @@ static void lwmi_om_master_unbind(struct device *dev) lwmi_om_hwmon_remove(priv); + lwmi_om_psy_remove(priv); + component_unbind_all(dev, NULL); } From 43692849ba4ff7b40c669b4a0821ed7f4731234a Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sun, 10 May 2026 04:25:44 +0000 Subject: [PATCH 28/73] [FROM-ML] platform/x86: lenovo-wmi-other: Add force_load_psy_ext module parameter Some Lenovo BIOS have been shown to have incomplete and/or broken capability data and WMI attribute IDs. In some cases the capability data reports that a feature is not supported when the get/set methods are fully implemented. It is also possible that the ACPI methods from the ideapad_laptop driver we defer to could be bugged while the WMI method is fully working. To aid end users in submitting more complete bug reports in these situations, add an override to skip the ACPI and compatibility checks to force load the power supply extension as if it is fully supported and has no conflicts. Signed-off-by: Derek J. Clark --- drivers/platform/x86/lenovo/wmi-other.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c index b7f49f5e0e53e..411f93f9b979c 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -185,6 +185,16 @@ MODULE_PARM_DESC(relax_fan_constraint, "Enabling this may results in HWMON attributes being out-of-sync, " "and setting a too low RPM stops the fan. Use with caution."); +/* Visibility of power supply extensions */ +static bool force_load_psy_ext; +module_param(force_load_psy_ext, bool, 0444); +MODULE_PARM_DESC(force_load_psy_ext, + "This option will skip checking if the ideapad_laptop driver will conflict " + "with adding an extension to set the battery charge behavior and battery charge " + "control end threshold. It will also skip checking if the BIOS reports that " + "those features are fully supported. It is recommended to blacklist the ideapad " + "driver before using this option."); + /* ======== HWMON (component: lenovo-wmi-capdata 00 & fan) ======== */ /** @@ -900,6 +910,11 @@ static void lwmi_om_psy_ext_init(struct lwmi_om_priv *priv) priv->bh_registered = false; + if (force_load_psy_ext) { + props = LWMI_PSY_PROP_BEHAVIOUR | LWMI_PSY_PROP_THRESHOLD; + goto load_psy_ext; + } + /* Deconflict ideapad_laptop driver */ ret = acpi_get_devices(ideapad_hid, lwmi_acpi_match, &handle, NULL); if (ret) @@ -917,6 +932,7 @@ static void lwmi_om_psy_ext_init(struct lwmi_om_priv *priv) if (!props) return; +load_psy_ext: /* Add battery hooks */ priv->battery_ext = lwmi_psy_exts[props]; priv->battery_hook.add_battery = lwmi_add_battery; From 085fdfabcf2a95aed905dd0bd42e984566b7952b Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Sun, 10 May 2026 04:25:45 +0000 Subject: [PATCH 29/73] [FROM-ML] platform/x86: lenovo-wmi-helpers: Add helper for creating per-device debugfs dir We are about to add debugfs support for lenovo-wmi-capdata. Let's setup a debugfs directory called "lenovo_wmi" for tidiness, so that any lenovo-wmi-* device can put its subdirectory under the directory. Subdirectories will be named after the corresponding WMI devices. Signed-off-by: Rong Zhang Signed-off-by: Derek J. Clark --- drivers/platform/x86/lenovo/wmi-helpers.c | 34 +++++++++++++++++++++++ drivers/platform/x86/lenovo/wmi-helpers.h | 2 ++ 2 files changed, 36 insertions(+) diff --git a/drivers/platform/x86/lenovo/wmi-helpers.c b/drivers/platform/x86/lenovo/wmi-helpers.c index 7a198259e3933..82dfd26c9c2b1 100644 --- a/drivers/platform/x86/lenovo/wmi-helpers.c +++ b/drivers/platform/x86/lenovo/wmi-helpers.c @@ -17,6 +17,8 @@ */ #include +#include +#include #include #include #include @@ -185,6 +187,38 @@ int lwmi_tm_notifier_call(enum thermal_mode *mode) } EXPORT_SYMBOL_NS_GPL(lwmi_tm_notifier_call, "LENOVO_WMI_HELPERS"); +static struct dentry *lwmi_debugfs_dir; + +/** + * lwmi_debugfs_create_dir() - Helper function for creating a debugfs directory + * for a device. + * @wdev: Pointer to the WMI device to be called. + * + * Caller must remove the directory with debugfs_remove_recursive() on device + * removal. + * + * Return: Pointer to the created directory. + */ +struct dentry *lwmi_debugfs_create_dir(struct wmi_device *wdev) +{ + return debugfs_create_dir(dev_name(&wdev->dev), lwmi_debugfs_dir); +} +EXPORT_SYMBOL_NS_GPL(lwmi_debugfs_create_dir, "LENOVO_WMI_HELPERS"); + +static int __init lwmi_helpers_init(void) +{ + lwmi_debugfs_dir = debugfs_create_dir("lenovo_wmi", NULL); + + return 0; +} +subsys_initcall(lwmi_helpers_init) + +static void __exit lwmi_helpers_exit(void) +{ + debugfs_remove_recursive(lwmi_debugfs_dir); +} +module_exit(lwmi_helpers_exit) + MODULE_AUTHOR("Derek J. Clark "); MODULE_DESCRIPTION("Lenovo WMI Helpers Driver"); MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-helpers.h b/drivers/platform/x86/lenovo/wmi-helpers.h index ed7db3ebba6cc..039fe61003ceb 100644 --- a/drivers/platform/x86/lenovo/wmi-helpers.h +++ b/drivers/platform/x86/lenovo/wmi-helpers.h @@ -16,6 +16,8 @@ struct wmi_method_args_32 { u32 arg1; }; +struct dentry *lwmi_debugfs_create_dir(struct wmi_device *wdev); + enum lwmi_event_type { LWMI_GZ_GET_THERMAL_MODE = 0x01, }; From dbf5b08fabbaa73e6009874c2a8af4bc8c76b026 Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Sun, 10 May 2026 04:25:46 +0000 Subject: [PATCH 30/73] [FROM-ML] platform/x86: lenovo-wmi-capdata: Add debugfs file for dumping capdata The Lenovo GameZone/Other interfaces have some delicate divergences among different devices. When making a bug report or adding support for new devices/interfaces, capdata is the most important information to cross-check with. Add a debugfs file (lenovo_wmi//capdata), so that users can dump capdata and include it in their reports. Since `struct capdata01' is just an extension to `struct capdata00', also convert the former to include the latter anonymously (-fms-extensions, since v6.19). This is declared as a union in the capdata01 struct, with both the anonymous declaration and as a named member to avoid type casting when passing just the capdata00 struct pointer. Tested-by: Kurt Borja Signed-off-by: Rong Zhang Signed-off-by: Derek J. Clark --- drivers/platform/x86/lenovo/Kconfig | 1 + drivers/platform/x86/lenovo/wmi-capdata.c | 120 ++++++++++++++++++++++ drivers/platform/x86/lenovo/wmi-capdata.h | 7 +- 3 files changed, 125 insertions(+), 3 deletions(-) diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig index b9a5d18caa1e3..4443f40ef8aa7 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -236,6 +236,7 @@ config YT2_1380 config LENOVO_WMI_CAPDATA tristate depends on ACPI_WMI + depends on LENOVO_WMI_HELPERS config LENOVO_WMI_EVENTS tristate diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c index 714aa6fd6f1fc..d5e9615661364 100644 --- a/drivers/platform/x86/lenovo/wmi-capdata.c +++ b/drivers/platform/x86/lenovo/wmi-capdata.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -87,6 +89,7 @@ struct lwmi_cd_priv { struct notifier_block acpi_nb; /* ACPI events */ struct wmi_device *wdev; struct cd_list *list; + struct dentry *debugfs_dir; /* * A capdata device may be a component master of another capdata device. @@ -117,6 +120,8 @@ struct cd_list { static struct wmi_driver lwmi_cd_driver; +/* ======== Device components ======== */ + /** * lwmi_cd_match() - Match rule for the master driver. * @dev: Pointer to the capability data parent device. @@ -470,6 +475,116 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CAPDATA"); DEF_LWMI_CDXX_GET_DATA(cd_fan, LENOVO_FAN_TEST_DATA, struct capdata_fan); EXPORT_SYMBOL_NS_GPL(lwmi_cd_fan_get_data, "LENOVO_WMI_CAPDATA"); +/* ======== debugfs ======== */ + +/** + * lwmi_cd00_show() - Dump capdata00 + * @s: Pointer to seq_file where the capdata00 is dumped. + * @cd00: Pointer to a capdata00 struct to be dumped. + */ +static void lwmi_cd00_show(struct seq_file *s, struct capdata00 *cd00) +{ + u8 dev = FIELD_GET(LWMI_ATTR_DEV_ID_MASK, cd00->id); + u8 feat = FIELD_GET(LWMI_ATTR_FEAT_ID_MASK, cd00->id); + u8 mode = FIELD_GET(LWMI_ATTR_MODE_ID_MASK, cd00->id); + u8 type = FIELD_GET(LWMI_ATTR_TYPE_ID_MASK, cd00->id); + bool extra = cd00->supported & ~(LWMI_SUPP_GET | LWMI_SUPP_SET | LWMI_SUPP_VALID); + bool get = cd00->supported & LWMI_SUPP_GET; + bool set = cd00->supported & LWMI_SUPP_SET; + bool valid = cd00->supported & LWMI_SUPP_VALID; + + seq_printf(s, " id: 0x%08x [dev: %2u, feat: %2u, mode: %2u, type: %2u]\n", + cd00->id, dev, feat, mode, type); + + seq_printf(s, " supported: 0x%08x [%c%c%c%c]\n", cd00->supported, + extra ? '+' : ' ', + get ? 'R' : ' ', + set ? 'W' : ' ', + valid ? 'V' : ' '); + + seq_printf(s, " default_value: %u\n", cd00->default_value); +} + +/** + * lwmi_cd01_show() - Dump capdata01 + * @s: Pointer to seq_file where the capdata01 is dumped. + * @cd01: Pointer to a capdata01 struct to be dumped. + */ +static void lwmi_cd01_show(struct seq_file *s, struct capdata01 *cd01) +{ + /* capdata01 is an extension to capdata00. */ + lwmi_cd00_show(s, &cd01->cd00); + + seq_printf(s, " step: %u\n", cd01->step); + seq_printf(s, " min_value: %u\n", cd01->min_value); + seq_printf(s, " max_value: %u\n", cd01->max_value); +} + +/** + * lwmi_cd_fan_show() - Dump capdata_fan + * @s: Pointer to seq_file where the capdata_fan is dumped. + * @cd_fan: Pointer to a capdata_fan struct to be dumped. + */ +static void lwmi_cd_fan_show(struct seq_file *s, struct capdata_fan *cd_fan) +{ + seq_printf(s, " id: %u\n", cd_fan->id); + seq_printf(s, " min_rpm: %u\n", cd_fan->min_rpm); + seq_printf(s, " max_rpm: %u\n", cd_fan->max_rpm); +} + +/** + * lwmi_cd_debugfs_show() - Dump capability data to debugfs + * @s: Pointer to seq_file where the capability data is dumped. + * @data: unused. + * + * Return: 0 + */ +static int lwmi_cd_debugfs_show(struct seq_file *s, void *data) +{ + struct lwmi_cd_priv *priv = s->private; + u8 idx; + + guard(mutex)(&priv->list->list_mutex); + + /* lwmi_cd_alloc() ensured priv->list->type must be a valid type. */ + for (idx = 0; idx < priv->list->count; idx++) { + seq_printf(s, "%s[%u]:\n", lwmi_cd_table[priv->list->type].name, idx); + + if (priv->list->type == LENOVO_CAPABILITY_DATA_00) + lwmi_cd00_show(s, &priv->list->cd00[idx]); + else if (priv->list->type == LENOVO_CAPABILITY_DATA_01) + lwmi_cd01_show(s, &priv->list->cd01[idx]); + else if (priv->list->type == LENOVO_FAN_TEST_DATA) + lwmi_cd_fan_show(s, &priv->list->cd_fan[idx]); + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(lwmi_cd_debugfs); + +/** + * lwmi_cd_debugfs_add() - Create debugfs directory and files for a device + * @priv: lenovo-wmi-capdata driver data. + */ +static void lwmi_cd_debugfs_add(struct lwmi_cd_priv *priv) +{ + priv->debugfs_dir = lwmi_debugfs_create_dir(priv->wdev); + + debugfs_create_file("capdata", 0444, priv->debugfs_dir, priv, &lwmi_cd_debugfs_fops); +} + +/** + * lwmi_cd_debugfs_remove() - Remove debugfs directory for a device + * @priv: lenovo-wmi-capdata driver data. + */ +static void lwmi_cd_debugfs_remove(struct lwmi_cd_priv *priv) +{ + debugfs_remove_recursive(priv->debugfs_dir); + priv->debugfs_dir = NULL; +} + +/* ======== WMI interface ======== */ + /** * lwmi_cd_cache() - Cache all WMI data block information * @priv: lenovo-wmi-capdata driver data. @@ -772,6 +887,8 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context) dev_err(&wdev->dev, "failed to register %s: %d\n", info->name, ret); } else { + lwmi_cd_debugfs_add(priv); + dev_dbg(&wdev->dev, "registered %s with %u items\n", info->name, priv->list->count); } @@ -782,6 +899,8 @@ static void lwmi_cd_remove(struct wmi_device *wdev) { struct lwmi_cd_priv *priv = dev_get_drvdata(&wdev->dev); + lwmi_cd_debugfs_remove(priv); + switch (priv->list->type) { case LENOVO_CAPABILITY_DATA_00: lwmi_cd_sub_master_del(priv); @@ -821,6 +940,7 @@ static struct wmi_driver lwmi_cd_driver = { module_wmi_driver(lwmi_cd_driver); +MODULE_IMPORT_NS("LENOVO_WMI_HELPERS"); MODULE_DEVICE_TABLE(wmi, lwmi_cd_id_table); MODULE_AUTHOR("Derek J. Clark "); MODULE_AUTHOR("Rong Zhang "); diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h index a49229cec2451..92098aeeee84a 100644 --- a/drivers/platform/x86/lenovo/wmi-capdata.h +++ b/drivers/platform/x86/lenovo/wmi-capdata.h @@ -38,9 +38,10 @@ struct capdata00 { }; struct capdata01 { - u32 id; - u32 supported; - u32 default_value; + union { + struct capdata00; + struct capdata00 cd00; + }; u32 step; u32 min_value; u32 max_value; From 45089f17ddc6f4af132d91ac4a7597f1036976c6 Mon Sep 17 00:00:00 2001 From: Marco Rodolfi Date: Sun, 14 Jun 2026 10:05:29 +0200 Subject: [PATCH 31/73] [EXTERNALLY-MAINTAINED] mfd: Add MFD core driver for Steam Deck Add MFD core driver for Steam Deck. Doesn't really do much so far besides instantiating a number of MFD cells that implement all the interesting functionality. (cherry picked from commit 5f534c2d6ebdefccb9c024eb0f013bc1c0c622d9) [fwd-ported to v6.11 and v7.0] Signed-off-by: Cristian Ciocaltea Signed-off-by: Marco Rodolfi --- drivers/mfd/Kconfig | 12 ++++ drivers/mfd/Makefile | 2 + drivers/mfd/steamdeck.c | 127 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 drivers/mfd/steamdeck.c diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index 7192c9d1d268e..1edfdd34119ec 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -2580,5 +2580,17 @@ config MFD_MAX7360 additional drivers must be enabled in order to use the functionality of the device. +config MFD_STEAMDECK + tristate "Valve Steam Deck" + select MFD_CORE + depends on ACPI + depends on X86_64 || COMPILE_TEST + help + This driver registers various MFD cells that expose aspects + of Steam Deck specific ACPI functionality. + + Say N here, unless you are running on Steam Deck hardware. + + endmenu endif diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile index e75e8045c28af..72adf78840c63 100644 --- a/drivers/mfd/Makefile +++ b/drivers/mfd/Makefile @@ -304,3 +304,5 @@ obj-$(CONFIG_MFD_RSMU_SPI) += rsmu_spi.o rsmu_core.o obj-$(CONFIG_MFD_UPBOARD_FPGA) += upboard-fpga.o obj-$(CONFIG_MFD_LOONGSON_SE) += loongson-se.o + +obj-$(CONFIG_MFD_STEAMDECK) += steamdeck.o diff --git a/drivers/mfd/steamdeck.c b/drivers/mfd/steamdeck.c new file mode 100644 index 0000000000000..0e504b3c2796e --- /dev/null +++ b/drivers/mfd/steamdeck.c @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/* + * Steam Deck EC MFD core driver + * + * Copyright (C) 2021-2022 Valve Corporation + * + */ + +#include +#include +#include + +#define STEAMDECK_STA_OK \ + (ACPI_STA_DEVICE_ENABLED | \ + ACPI_STA_DEVICE_PRESENT | \ + ACPI_STA_DEVICE_FUNCTIONING) + +struct steamdeck { + struct acpi_device *adev; + struct device *dev; +}; + +#define STEAMDECK_ATTR_RO(_name, _method) \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + struct steamdeck *sd = dev_get_drvdata(dev); \ + unsigned long long val; \ + \ + if (ACPI_FAILURE(acpi_evaluate_integer( \ + sd->adev->handle, \ + _method, NULL, &val))) \ + return -EIO; \ + \ + return sysfs_emit(buf, "%llu\n", val); \ + } \ + static DEVICE_ATTR_RO(_name) + +STEAMDECK_ATTR_RO(firmware_version, "PDFW"); +STEAMDECK_ATTR_RO(board_id, "BOID"); + +static struct attribute *steamdeck_attrs[] = { + &dev_attr_firmware_version.attr, + &dev_attr_board_id.attr, + NULL +}; + +ATTRIBUTE_GROUPS(steamdeck); + +static const struct mfd_cell steamdeck_cells[] = { + { .name = "steamdeck-hwmon" }, + { .name = "steamdeck-leds" }, + { .name = "steamdeck-extcon" }, +}; + +static void steamdeck_remove_sysfs_groups(void *data) +{ + struct steamdeck *sd = data; + + sysfs_remove_groups(&sd->dev->kobj, steamdeck_groups); +} + +static int steamdeck_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + unsigned long long sta; + struct steamdeck *sd; + acpi_status status; + int ret; + + sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL); + if (!sd) + return -ENOMEM; + sd->adev = ACPI_COMPANION(dev); + sd->dev = dev; + platform_set_drvdata(pdev, sd); + + status = acpi_evaluate_integer(sd->adev->handle, "_STA", + NULL, &sta); + if (ACPI_FAILURE(status)) { + dev_err(dev, "Status check failed (0x%x)\n", status); + return -EINVAL; + } + + if ((sta & STEAMDECK_STA_OK) != STEAMDECK_STA_OK) { + dev_err(dev, "Device is not ready\n"); + return -EINVAL; + } + + ret = sysfs_create_groups(&dev->kobj, steamdeck_groups); + if (ret) { + dev_err(dev, "Failed to create sysfs group\n"); + return ret; + } + + ret = devm_add_action_or_reset(dev, steamdeck_remove_sysfs_groups, + sd); + if (ret) { + dev_err(dev, "Failed to register devres action\n"); + return ret; + } + + return devm_mfd_add_devices(dev, PLATFORM_DEVID_NONE, + steamdeck_cells, ARRAY_SIZE(steamdeck_cells), + NULL, 0, NULL); +} + +static const struct acpi_device_id steamdeck_device_ids[] = { + { "VLV0100", 0 }, + { "", 0 }, +}; +MODULE_DEVICE_TABLE(acpi, steamdeck_device_ids); + +static struct platform_driver steamdeck_driver = { + .probe = steamdeck_probe, + .driver = { + .name = "steamdeck", + .acpi_match_table = steamdeck_device_ids, + }, +}; +module_platform_driver(steamdeck_driver); + +MODULE_AUTHOR("Andrey Smirnov "); +MODULE_DESCRIPTION("Steam Deck EC MFD core driver"); +MODULE_LICENSE("GPL"); From 8e9229d65343a59d1410877235b6a94748bb13fe Mon Sep 17 00:00:00 2001 From: Marco Rodolfi Date: Sun, 14 Jun 2026 10:05:54 +0200 Subject: [PATCH 32/73] [EXTERNALLY-MAINTAINED] mfd: steamdeck: Expose controller board power in sysfs As of version 118 Deck's BIOS implements "SCBP" method that allows gating power of the controller board (VBUS). Add a basic WO method to our root MFD device to allow toggling that. Signed-off-by: Andrey Smirnov (cherry picked from commit f97f32718acc10cbb51fef925842392e80904d74) Signed-off-by: Cristian Ciocaltea --- drivers/mfd/steamdeck.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/drivers/mfd/steamdeck.c b/drivers/mfd/steamdeck.c index 0e504b3c2796e..a60fa7db91415 100644 --- a/drivers/mfd/steamdeck.c +++ b/drivers/mfd/steamdeck.c @@ -41,9 +41,29 @@ struct steamdeck { STEAMDECK_ATTR_RO(firmware_version, "PDFW"); STEAMDECK_ATTR_RO(board_id, "BOID"); +static ssize_t controller_board_power_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct steamdeck *sd = dev_get_drvdata(dev); + bool enabled; + ssize_t ret = kstrtobool(buf, &enabled); + + if (ret) + return ret; + + if (ACPI_FAILURE(acpi_execute_simple_method(sd->adev->handle, + "SCBP", enabled))) + return -EIO; + + return count; +} +static DEVICE_ATTR_WO(controller_board_power); + static struct attribute *steamdeck_attrs[] = { &dev_attr_firmware_version.attr, &dev_attr_board_id.attr, + &dev_attr_controller_board_power.attr, NULL }; From ecd34d51a55e8ffb9988e306d88303446d4cf7bb Mon Sep 17 00:00:00 2001 From: Marco Rodolfi Date: Sun, 14 Jun 2026 10:06:12 +0200 Subject: [PATCH 33/73] [EXTERNALLY-MAINTAINED] hwmon: Add driver for Steam Deck's EC sensors Add driver for sensors exposed by EC firmware on Steam Deck hardware. (cherry picked from commit 6917aac77bee6185ae3920b936cdbe7876118c0b) [fwd-ported to v7.0] Signed-off-by: Cristian Ciocaltea Signed-off-by: Marco Rodolfi --- drivers/hwmon/Kconfig | 11 ++ drivers/hwmon/Makefile | 1 + drivers/hwmon/steamdeck-hwmon.c | 224 ++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 drivers/hwmon/steamdeck-hwmon.c diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 14e4cea48acc4..c149b47bd4dbb 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -2160,6 +2160,17 @@ config SENSORS_SCH5636 This driver can also be built as a module. If so, the module will be called sch5636. +config SENSORS_STEAMDECK + tristate "Steam Deck EC sensors" + depends on MFD_STEAMDECK + help + If you say yes here you get support for the hardware + monitoring features exposed by EC firmware on Steam Deck + devices + + This driver can also be built as a module. If so, the module + will be called steamdeck-hwmon. + config SENSORS_STTS751 tristate "ST Microelectronics STTS751" depends on I2C diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index 982ee2c6f9deb..5fcb26e0d84c2 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -219,6 +219,7 @@ obj-$(CONFIG_SENSORS_SMSC47M1) += smsc47m1.o obj-$(CONFIG_SENSORS_SMSC47M192)+= smsc47m192.o obj-$(CONFIG_SENSORS_SPARX5) += sparx5-temp.o obj-$(CONFIG_SENSORS_SPD5118) += spd5118.o +obj-$(CONFIG_SENSORS_STEAMDECK) += steamdeck-hwmon.o obj-$(CONFIG_SENSORS_STTS751) += stts751.o obj-$(CONFIG_SENSORS_SURFACE_FAN)+= surface_fan.o obj-$(CONFIG_SENSORS_SURFACE_TEMP)+= surface_temp.o diff --git a/drivers/hwmon/steamdeck-hwmon.c b/drivers/hwmon/steamdeck-hwmon.c new file mode 100644 index 0000000000000..fab9e9460bd40 --- /dev/null +++ b/drivers/hwmon/steamdeck-hwmon.c @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Steam Deck EC sensors driver + * + * Copyright (C) 2021-2022 Valve Corporation + */ + +#include +#include +#include + +#define STEAMDECK_HWMON_NAME "steamdeck-hwmon" + +struct steamdeck_hwmon { + struct acpi_device *adev; +}; + +static long +steamdeck_hwmon_get(struct steamdeck_hwmon *sd, const char *method) +{ + unsigned long long val; + if (ACPI_FAILURE(acpi_evaluate_integer(sd->adev->handle, + (char *)method, NULL, &val))) + return -EIO; + + return val; +} + +static int +steamdeck_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *out) +{ + struct steamdeck_hwmon *sd = dev_get_drvdata(dev); + + switch (type) { + case hwmon_curr: + if (attr != hwmon_curr_input) + return -EOPNOTSUPP; + + *out = steamdeck_hwmon_get(sd, "PDAM"); + if (*out < 0) + return *out; + break; + case hwmon_in: + if (attr != hwmon_in_input) + return -EOPNOTSUPP; + + *out = steamdeck_hwmon_get(sd, "PDVL"); + if (*out < 0) + return *out; + break; + case hwmon_temp: + if (attr != hwmon_temp_input) + return -EOPNOTSUPP; + + *out = steamdeck_hwmon_get(sd, "BATT"); + if (*out < 0) + return *out; + /* + * Assuming BATT returns deg C we need to mutiply it + * by 1000 to convert to mC + */ + *out *= 1000; + break; + case hwmon_fan: + switch (attr) { + case hwmon_fan_input: + *out = steamdeck_hwmon_get(sd, "FANR"); + if (*out < 0) + return *out; + break; + case hwmon_fan_target: + *out = steamdeck_hwmon_get(sd, "FSSR"); + if (*out < 0) + return *out; + break; + case hwmon_fan_fault: + *out = steamdeck_hwmon_get(sd, "FANC"); + if (*out < 0) + return *out; + /* + * FANC (Fan check): + * 0: Abnormal + * 1: Normal + */ + *out = !*out; + break; + default: + return -EOPNOTSUPP; + } + break; + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static int +steamdeck_hwmon_read_string(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) +{ + switch (type) { + /* + * These two aren't, strictly speaking, measured. EC + * firmware just reports what PD negotiation resulted + * in. + */ + case hwmon_curr: + *str = "PD Contract Current"; + break; + case hwmon_in: + *str = "PD Contract Voltage"; + break; + case hwmon_temp: + *str = "Battery Temp"; + break; + case hwmon_fan: + *str = "System Fan"; + break; + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static int +steamdeck_hwmon_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + struct steamdeck_hwmon *sd = dev_get_drvdata(dev); + + if (type != hwmon_fan || + attr != hwmon_fan_target) + return -EOPNOTSUPP; + + val = clamp_val(val, 0, 7300); + + if (ACPI_FAILURE(acpi_execute_simple_method(sd->adev->handle, + "FANS", val))) + return -EIO; + + return 0; +} + +static umode_t +steamdeck_hwmon_is_visible(const void *data, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + if (type == hwmon_fan && + attr == hwmon_fan_target) + return 0644; + + return 0444; +} + +static const struct hwmon_channel_info *steamdeck_hwmon_info[] = { + HWMON_CHANNEL_INFO(in, + HWMON_I_INPUT | HWMON_I_LABEL), + HWMON_CHANNEL_INFO(curr, + HWMON_C_INPUT | HWMON_C_LABEL), + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT | HWMON_T_LABEL), + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_LABEL | + HWMON_F_TARGET | HWMON_F_FAULT), + NULL +}; + +static const struct hwmon_ops steamdeck_hwmon_ops = { + .is_visible = steamdeck_hwmon_is_visible, + .read = steamdeck_hwmon_read, + .read_string = steamdeck_hwmon_read_string, + .write = steamdeck_hwmon_write, +}; + +static const struct hwmon_chip_info steamdeck_hwmon_chip_info = { + .ops = &steamdeck_hwmon_ops, + .info = steamdeck_hwmon_info, +}; + +static int steamdeck_hwmon_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct steamdeck_hwmon *sd; + struct device *hwmon; + + sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL); + if (!sd) + return -ENOMEM; + + sd->adev = ACPI_COMPANION(dev->parent); + hwmon = devm_hwmon_device_register_with_info(dev, + "steamdeck_hwmon", + sd, + &steamdeck_hwmon_chip_info, + NULL); + if (IS_ERR(hwmon)) { + dev_err(dev, "Failed to register HWMON device"); + return PTR_ERR(hwmon); + } + + return 0; +} + +static const struct platform_device_id steamdeck_hwmon_id_table[] = { + { .name = STEAMDECK_HWMON_NAME }, + {} +}; +MODULE_DEVICE_TABLE(platform, steamdeck_hwmon_id_table); + +static struct platform_driver steamdeck_hwmon_driver = { + .probe = steamdeck_hwmon_probe, + .driver = { + .name = STEAMDECK_HWMON_NAME, + }, + .id_table = steamdeck_hwmon_id_table, +}; +module_platform_driver(steamdeck_hwmon_driver); + +MODULE_AUTHOR("Andrey Smirnov "); +MODULE_DESCRIPTION("Steam Deck EC sensors driver"); +MODULE_LICENSE("GPL"); From d81d7701e53eb2abf3a6d21b7a29c6ca7d2c1eb1 Mon Sep 17 00:00:00 2001 From: Marco Rodolfi Date: Sun, 14 Jun 2026 10:06:30 +0200 Subject: [PATCH 34/73] [EXTERNALLY-MAINTAINED] hwmon: steamdeck-hwmon: Add support for max battery level/rate Add support for max battery level/charge rate attributes. (cherry picked from commit 50af83e8fd75dc52221edd3fb6fd7a7f70c4d8a4) Signed-off-by: Andrey Smirnov --- drivers/hwmon/steamdeck-hwmon.c | 72 ++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/drivers/hwmon/steamdeck-hwmon.c b/drivers/hwmon/steamdeck-hwmon.c index fab9e9460bd40..9d0a5471b1811 100644 --- a/drivers/hwmon/steamdeck-hwmon.c +++ b/drivers/hwmon/steamdeck-hwmon.c @@ -180,6 +180,76 @@ static const struct hwmon_chip_info steamdeck_hwmon_chip_info = { .info = steamdeck_hwmon_info, }; + +static ssize_t +steamdeck_hwmon_simple_store(struct device *dev, const char *buf, size_t count, + const char *method, + unsigned long upper_limit) +{ + struct steamdeck_hwmon *sd = dev_get_drvdata(dev); + unsigned long value; + + if (kstrtoul(buf, 10, &value) || value >= upper_limit) + return -EINVAL; + + if (ACPI_FAILURE(acpi_execute_simple_method(sd->adev->handle, + (char *)method, value))) + return -EIO; + + return count; +} + +static ssize_t +steamdeck_hwmon_simple_show(struct device *dev, char *buf, + const char *method) +{ + struct steamdeck_hwmon *sd = dev_get_drvdata(dev); + unsigned long value; + + value = steamdeck_hwmon_get(sd, method); + if (value < 0) + return value; + + return sprintf(buf, "%ld\n", value); +} + +#define STEAMDECK_HWMON_ATTR_RW(_name, _set_method, _get_method, \ + _upper_limit) \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + return steamdeck_hwmon_simple_show(dev, buf, \ + _get_method); \ + } \ + static ssize_t _name##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return steamdeck_hwmon_simple_store(dev, buf, count, \ + _set_method, \ + _upper_limit); \ + } \ + static DEVICE_ATTR_RW(_name) + +STEAMDECK_HWMON_ATTR_RW(max_battery_charge_level, "FCBL", "SFBL", 101); +STEAMDECK_HWMON_ATTR_RW(max_battery_charge_rate, "CHGR", "GCHR", 101); + +static struct attribute *steamdeck_hwmon_attributes[] = { + &dev_attr_max_battery_charge_level.attr, + &dev_attr_max_battery_charge_rate.attr, + NULL +}; + +static const struct attribute_group steamdeck_hwmon_group = { + .attrs = steamdeck_hwmon_attributes, +}; + +static const struct attribute_group *steamdeck_hwmon_groups[] = { + &steamdeck_hwmon_group, + NULL +}; + static int steamdeck_hwmon_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; @@ -195,7 +265,7 @@ static int steamdeck_hwmon_probe(struct platform_device *pdev) "steamdeck_hwmon", sd, &steamdeck_hwmon_chip_info, - NULL); + steamdeck_hwmon_groups); if (IS_ERR(hwmon)) { dev_err(dev, "Failed to register HWMON device"); return PTR_ERR(hwmon); From 9dc5f1c22c104da0499dbaef30c3740f5ee24e8e Mon Sep 17 00:00:00 2001 From: Ahmed Yaseen Date: Mon, 11 May 2026 00:12:13 +0500 Subject: [PATCH 35/73] [FROM-ML] platform/x86: asus-armoury: gate PPT writes behind active fan curve On models flagged with requires_fan_curve in the DMI power_data table (30 entries), the BIOS ACPI method SPLX only writes PPT values to the EC when the fan mode is set to Manual (FANM=4). FANM is set to 4 by the DEFC method when a custom fan curve is written. Without an active custom fan curve, the WMI DEVS call returns success but the firmware silently ignores the PPT value, so userspace observes no effect from its write. Gate writes to ASUS_WMI_DEVID_PPT_{PL1_SPL,PL2_SPPT,PL3_FPPT,APU_SPPT, PLAT_SPPT} on a check of asus_wmi_custom_fan_curve_is_enabled(), and return -EBUSY with a pr_warn_once() when no fan curve is active on an affected model. Export the helper from asus-wmi so asus-armoury can call it across module boundaries. Signed-off-by: Ahmed Yaseen --- drivers/platform/x86/asus-armoury.c | 20 ++++++++++++++++++ drivers/platform/x86/asus-wmi.c | 24 ++++++++++++++++++++++ include/linux/platform_data/x86/asus-wmi.h | 5 +++++ 3 files changed, 49 insertions(+) diff --git a/drivers/platform/x86/asus-armoury.c b/drivers/platform/x86/asus-armoury.c index 495dc1e31d40e..f2a880eb0cdf5 100644 --- a/drivers/platform/x86/asus-armoury.c +++ b/drivers/platform/x86/asus-armoury.c @@ -93,6 +93,8 @@ struct asus_armoury_priv { u32 mini_led_dev_id; u32 gpu_mux_dev_id; + + bool requires_fan_curve; }; static struct asus_armoury_priv asus_armoury = { @@ -216,6 +218,22 @@ static int armoury_set_devstate(struct kobj_attribute *attr, u32 result; int err; + /* On some models, PPT changes require an active fan curve */ + if (asus_armoury.requires_fan_curve) { + switch (dev_id) { + case ASUS_WMI_DEVID_PPT_PL1_SPL: + case ASUS_WMI_DEVID_PPT_PL2_SPPT: + case ASUS_WMI_DEVID_PPT_PL3_FPPT: + case ASUS_WMI_DEVID_PPT_APU_SPPT: + case ASUS_WMI_DEVID_PPT_PLAT_SPPT: + if (!asus_wmi_custom_fan_curve_is_enabled()) { + pr_warn_once("PPT change requires an active fan curve on this model. Enable a custom fan curve first.\n"); + return -EBUSY; + } + break; + } + } + /* * Prevent developers from bricking devices or issuing dangerous * commands that can be difficult or impossible to recover from. @@ -1010,6 +1028,8 @@ static void init_rog_tunables(void) return; } + asus_armoury.requires_fan_curve = power_data->requires_fan_curve; + /* Initialize AC power tunables */ ac_limits = power_data->ac_data; if (ac_limits) { diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c index 80144c412b902..e4c1716d9b308 100644 --- a/drivers/platform/x86/asus-wmi.c +++ b/drivers/platform/x86/asus-wmi.c @@ -4001,6 +4001,30 @@ static int asus_wmi_custom_fan_curve_init(struct asus_wmi *asus) return 0; } +/* + * Returns true if at least one custom fan curve is active + * + * Used by asus-armoury to check if PPT writes will be accepted by the BIOS + * on models that require an active fan curve for TDP changes. + */ +bool asus_wmi_custom_fan_curve_is_enabled(void) +{ + struct fan_curve_data *curves; + struct asus_wmi *asus; + + guard(spinlock_irqsave)(&asus_ref.lock); + asus = asus_ref.asus; + if (!asus) + return false; + + curves = asus->custom_fan_curves; + + return (asus->cpu_fan_curve_available && curves[FAN_CURVE_DEV_CPU].enabled) || + (asus->gpu_fan_curve_available && curves[FAN_CURVE_DEV_GPU].enabled) || + (asus->mid_fan_curve_available && curves[FAN_CURVE_DEV_MID].enabled); +} +EXPORT_SYMBOL_NS_GPL(asus_wmi_custom_fan_curve_is_enabled, "ASUS_WMI"); + /* Throttle thermal policy ****************************************************/ static int throttle_thermal_policy_write(struct asus_wmi *asus) { diff --git a/include/linux/platform_data/x86/asus-wmi.h b/include/linux/platform_data/x86/asus-wmi.h index 554f41b827e18..5d57293ced6cc 100644 --- a/include/linux/platform_data/x86/asus-wmi.h +++ b/include/linux/platform_data/x86/asus-wmi.h @@ -196,6 +196,7 @@ int asus_wmi_evaluate_method(u32 method_id, u32 arg0, u32 arg1, u32 *retval); int asus_hid_register_listener(struct asus_hid_listener *cdev); void asus_hid_unregister_listener(struct asus_hid_listener *cdev); int asus_hid_event(enum asus_hid_event event); +bool asus_wmi_custom_fan_curve_is_enabled(void); #else static inline void set_ally_mcu_hack(enum asus_ally_mcu_hack status) { @@ -227,6 +228,10 @@ static inline int asus_hid_event(enum asus_hid_event event) { return -ENODEV; } +static inline bool asus_wmi_custom_fan_curve_is_enabled(void) +{ + return false; +} #endif #endif /* __PLATFORM_DATA_X86_ASUS_WMI_H */ From 61b00d98dadb3b31b4406d0421d4c05980469c84 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Fri, 12 Jun 2026 19:16:53 -0700 Subject: [PATCH 36/73] [FROM-ML] platform/x86: msi-wmi: Reformat msi_wmi_notify() Reformats msi_wmi_notify() to use a switch statement that reduces nesting and prepares the function to support additional ACPI types. Signed-off-by: Derek J. Clark --- drivers/platform/x86/msi-wmi.c | 71 +++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/drivers/platform/x86/msi-wmi.c b/drivers/platform/x86/msi-wmi.c index 4a7ac85c4db48..d00ced756581f 100644 --- a/drivers/platform/x86/msi-wmi.c +++ b/drivers/platform/x86/msi-wmi.c @@ -172,44 +172,51 @@ static const struct backlight_ops msi_backlight_ops = { static void msi_wmi_notify(union acpi_object *obj, void *context) { - struct key_entry *key; + struct key_entry *key = NULL; + int eventcode = 0; - if (obj && obj->type == ACPI_TYPE_INTEGER) { - int eventcode = obj->integer.value; + if (!obj) + return; + + switch (obj->type) { + case ACPI_TYPE_INTEGER: + eventcode = obj->integer.value; pr_debug("Eventcode: 0x%x\n", eventcode); - key = sparse_keymap_entry_from_scancode(msi_wmi_input_dev, - eventcode); - if (!key) { - pr_info("Unknown key pressed - %x\n", eventcode); + break; + default: + pr_info("Unknown event received\n"); + return; + } + + key = sparse_keymap_entry_from_scancode(msi_wmi_input_dev, eventcode); + + if (!key) { + pr_info("Unknown key pressed - 0x%x\n", eventcode); + return; + } + + if (event_wmi->quirk_last_pressed) { + ktime_t cur = ktime_get_real(); + ktime_t diff = ktime_sub(cur, last_pressed); + /* Ignore event if any event happened in a 50 ms + * timeframe -> Key press may result in 10-20 GPEs + */ + if (ktime_to_us(diff) < 1000 * 50) { + pr_debug("Suppressed key event 0x%X - Last press was %lld us ago\n", + key->code, ktime_to_us(diff)); return; } + last_pressed = cur; + } - if (event_wmi->quirk_last_pressed) { - ktime_t cur = ktime_get_real(); - ktime_t diff = ktime_sub(cur, last_pressed); - /* Ignore event if any event happened in a 50 ms - timeframe -> Key press may result in 10-20 GPEs */ - if (ktime_to_us(diff) < 1000 * 50) { - pr_debug("Suppressed key event 0x%X - " - "Last press was %lld us ago\n", - key->code, ktime_to_us(diff)); - return; - } - last_pressed = cur; - } + /* Brightness is served via acpi video driver */ + if (key->type == KE_KEY && + (backlight || (key->code == MSI_KEY_BRIGHTNESSUP || + key->code == MSI_KEY_BRIGHTNESSDOWN))) + return; - if (key->type == KE_KEY && - /* Brightness is served via acpi video driver */ - (backlight || - (key->code != MSI_KEY_BRIGHTNESSUP && - key->code != MSI_KEY_BRIGHTNESSDOWN))) { - pr_debug("Send key: 0x%X - Input layer keycode: %d\n", - key->code, key->keycode); - sparse_keymap_report_entry(msi_wmi_input_dev, key, 1, - true); - } - } else - pr_info("Unknown event received\n"); + pr_debug("Send key: 0x%X - Input layer keycode: %d\n", key->code, key->keycode); + sparse_keymap_report_entry(msi_wmi_input_dev, key, 1, true); } static int __init msi_wmi_backlight_setup(void) From dd4b52b575325c247ccdb3108dcec33ea3c3c9fa Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Fri, 12 Jun 2026 19:16:54 -0700 Subject: [PATCH 37/73] [FROM-ML] platform/x86: msi-wmi: Add MSI Claw M-Center keys MSI Claw devices produce WMI events through the MSI WMI hotkeys GUID for some of their buttons. When pressed, these cause spam in the kernel. For the majority of devices these events can be safely ignored as they are duplicated by the AT Translated Set 2 Keyboard device exposed as an evdev. For the MSI Claw A8 BZ2EM model's M-Center Menu button (left of the screen) there is no associated keyboard event, so this event must be exposed. Map this button to the same scancode produced by the AT Keyboard device on other models. This does cause double F15 events on the A1M, 7 AI+ A2VM, and 8 AI+ A2VM, but it appears to be harmless in my testing. Signed-off-by: Derek J. Clark --- drivers/platform/x86/msi-wmi.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/drivers/platform/x86/msi-wmi.c b/drivers/platform/x86/msi-wmi.c index d00ced756581f..c9db750fe5ae6 100644 --- a/drivers/platform/x86/msi-wmi.c +++ b/drivers/platform/x86/msi-wmi.c @@ -46,6 +46,12 @@ enum msi_scancodes { WIND_KEY_WLAN = 0x5f, /* Fn+F11 Wi-Fi toggle */ WIND_KEY_TURBO, /* Fn+F10 turbo mode toggle */ WIND_KEY_ECO = 0x69, /* Fn+F10 ECO mode toggle */ + /* MSI Claw keys */ + CLAW_KEY_VOLUMEDOWN = 0x21, + CLAW_KEY_CENTER = 0x29, /* MSI M-Center main menu */ + CLAW_KEY_QUICK_LONG = 0x2a, /* MSI M-Center quick access long hold */ + CLAW_KEY_VOLUMEUP = 0x32, + CLAW_KEY_QUICK_SHORT = 0x58, /* MSI M-Center quick access short press */ }; static struct key_entry msi_wmi_keymap[] = { { KE_KEY, MSI_KEY_BRIGHTNESSUP, {KEY_BRIGHTNESSUP} }, @@ -69,6 +75,15 @@ static struct key_entry msi_wmi_keymap[] = { { KE_KEY, WIND_KEY_TURBO, {KEY_PROG1} }, { KE_KEY, WIND_KEY_ECO, {KEY_PROG2} }, + /* These are MSI Claw keys, used for MSI M-Center in Windows */ + { KE_KEY, CLAW_KEY_CENTER, {KEY_F15} }, + + /* These MSI Claw keys work without WMI. Ignore them to avoid double keycodes */ + { KE_IGNORE, CLAW_KEY_QUICK_SHORT }, + { KE_IGNORE, CLAW_KEY_QUICK_LONG }, + { KE_IGNORE, CLAW_KEY_VOLUMEUP }, + { KE_IGNORE, CLAW_KEY_VOLUMEDOWN }, + { KE_END, 0 } }; @@ -183,6 +198,17 @@ static void msi_wmi_notify(union acpi_object *obj, void *context) eventcode = obj->integer.value; pr_debug("Eventcode: 0x%x\n", eventcode); break; + case ACPI_TYPE_BUFFER: + /* Field returns u8[2] here, but is u32 by spec. Allow "oversized" buffers. */ + if (obj->buffer.length < 2) + return; + + /* pointer[0] is key ID, pointer[1] is active state. We don't get release + * events, so ignore the active state and treat as autorelease. + */ + eventcode = obj->buffer.pointer[0]; + pr_debug("Eventcode: 0x%x\n", eventcode); + break; default: pr_info("Unknown event received\n"); return; From 1354996d52bffe0ff43d487c272d60c9c45da78f Mon Sep 17 00:00:00 2001 From: Armin Wolf Date: Sun, 11 May 2025 22:44:18 +0200 Subject: [PATCH 38/73] [FROM-ML] platform/x86: msi-wmi-platform: Use input buffer for returning result Modify msi_wmi_platform_query() to reuse the input buffer for returning the result of a WMI method call. Using a separate output buffer to return the result is unnecessary because the WMI interface requires both buffers to have the same length anyway. Co-developed-by: Antheas Kapenekakis Signed-off-by: Antheas Kapenekakis Signed-off-by: Armin Wolf --- drivers/platform/x86/msi-wmi-platform.c | 53 ++++++++++++------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c index e912fcc12d124..8d0f9328c95f7 100644 --- a/drivers/platform/x86/msi-wmi-platform.c +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -141,19 +142,19 @@ static int msi_wmi_platform_parse_buffer(union acpi_object *obj, u8 *output, siz } static int msi_wmi_platform_query(struct msi_wmi_platform_data *data, - enum msi_wmi_platform_method method, u8 *input, - size_t input_length, u8 *output, size_t output_length) + enum msi_wmi_platform_method method, u8 *buffer, + size_t length) { struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; struct acpi_buffer in = { - .length = input_length, - .pointer = input + .length = length, + .pointer = buffer }; union acpi_object *obj; acpi_status status; int ret; - if (!input_length || !output_length) + if (!length) return -EINVAL; /* @@ -170,7 +171,7 @@ static int msi_wmi_platform_query(struct msi_wmi_platform_data *data, if (!obj) return -ENODATA; - ret = msi_wmi_platform_parse_buffer(obj, output, output_length); + ret = msi_wmi_platform_parse_buffer(obj, buffer, length); kfree(obj); return ret; @@ -186,17 +187,15 @@ static int msi_wmi_platform_read(struct device *dev, enum hwmon_sensor_types typ int channel, long *val) { struct msi_wmi_platform_data *data = dev_get_drvdata(dev); - u8 input[32] = { 0 }; - u8 output[32]; + u8 buffer[32] = { 0 }; u16 value; int ret; - ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, input, sizeof(input), output, - sizeof(output)); + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, buf, sizeof(buf)); if (ret < 0) return ret; - value = get_unaligned_be16(&output[channel * 2 + 1]); + value = get_unaligned_be16(&buffer[channel * 2 + 1]); if (!value) *val = 0; else @@ -246,13 +245,17 @@ static ssize_t msi_wmi_platform_write(struct file *fp, const char __user *input, return ret; down_write(&data->buffer_lock); - ret = msi_wmi_platform_query(data->data, data->method, payload, data->length, data->buffer, + ret = msi_wmi_platform_query(data->data, data->method, data->buffer, data->length); up_write(&data->buffer_lock); if (ret < 0) return ret; + down_write(&data->buffer_lock); + memcpy(data->buffer, payload, data->length); + up_write(&data->buffer_lock); + return length; } @@ -349,23 +352,21 @@ static int msi_wmi_platform_hwmon_init(struct msi_wmi_platform_data *data) static int msi_wmi_platform_ec_init(struct msi_wmi_platform_data *data) { - u8 input[32] = { 0 }; - u8 output[32]; + u8 buffer[32] = { 0 }; u8 flags; int ret; - ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_EC, input, sizeof(input), output, - sizeof(output)); + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_EC, buffer, sizeof(buffer)); if (ret < 0) return ret; - flags = output[MSI_PLATFORM_EC_FLAGS_OFFSET]; + flags = buffer[MSI_PLATFORM_EC_FLAGS_OFFSET]; dev_dbg(&data->wdev->dev, "EC RAM version %lu.%lu\n", FIELD_GET(MSI_PLATFORM_EC_MAJOR_MASK, flags), FIELD_GET(MSI_PLATFORM_EC_MINOR_MASK, flags)); dev_dbg(&data->wdev->dev, "EC firmware version %.28s\n", - &output[MSI_PLATFORM_EC_VERSION_OFFSET]); + &buffer[MSI_PLATFORM_EC_VERSION_OFFSET]); if (!(flags & MSI_PLATFORM_EC_IS_TIGERLAKE)) { if (!force) @@ -379,27 +380,25 @@ static int msi_wmi_platform_ec_init(struct msi_wmi_platform_data *data) static int msi_wmi_platform_init(struct msi_wmi_platform_data *data) { - u8 input[32] = { 0 }; - u8 output[32]; + u8 buffer[32] = { 0 }; int ret; - ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_WMI, input, sizeof(input), output, - sizeof(output)); + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_WMI, buffer, sizeof(buffer)); if (ret < 0) return ret; dev_dbg(&data->wdev->dev, "WMI interface version %u.%u\n", - output[MSI_PLATFORM_WMI_MAJOR_OFFSET], - output[MSI_PLATFORM_WMI_MINOR_OFFSET]); + buffer[MSI_PLATFORM_WMI_MAJOR_OFFSET], + buffer[MSI_PLATFORM_WMI_MINOR_OFFSET]); - if (output[MSI_PLATFORM_WMI_MAJOR_OFFSET] != MSI_WMI_PLATFORM_INTERFACE_VERSION) { + if (buffer[MSI_PLATFORM_WMI_MAJOR_OFFSET] != MSI_WMI_PLATFORM_INTERFACE_VERSION) { if (!force) return -ENODEV; dev_warn(&data->wdev->dev, "Loading despite unsupported WMI interface version (%u.%u)\n", - output[MSI_PLATFORM_WMI_MAJOR_OFFSET], - output[MSI_PLATFORM_WMI_MINOR_OFFSET]); + buffer[MSI_PLATFORM_WMI_MAJOR_OFFSET], + buffer[MSI_PLATFORM_WMI_MINOR_OFFSET]); } return 0; From 7a0cc5d4715c098a8e3862d7b389f85058dd7ddd Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Sun, 11 May 2025 22:44:19 +0200 Subject: [PATCH 39/73] [FROM-ML] platform/x86: msi-wmi-platform: Add unlocked msi_wmi_platform_query This driver requires to be able to handle transactions that perform multiple WMI actions at a time. Therefore, it needs to be able to lock the wmi_lock mutex for multiple operations. Add msi_wmi_platform_query_unlocked() to allow the caller to perform the WMI query without locking the wmi_lock mutex, by renaming the existing function and adding a new one that only locks the mutex. Signed-off-by: Antheas Kapenekakis --- drivers/platform/x86/msi-wmi-platform.c | 27 ++++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c index 8d0f9328c95f7..94236214c3c06 100644 --- a/drivers/platform/x86/msi-wmi-platform.c +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -141,7 +141,7 @@ static int msi_wmi_platform_parse_buffer(union acpi_object *obj, u8 *output, siz return 0; } -static int msi_wmi_platform_query(struct msi_wmi_platform_data *data, +static int msi_wmi_platform_query_unlocked(struct msi_wmi_platform_data *data, enum msi_wmi_platform_method method, u8 *buffer, size_t length) { @@ -157,15 +157,9 @@ static int msi_wmi_platform_query(struct msi_wmi_platform_data *data, if (!length) return -EINVAL; - /* - * The ACPI control method responsible for handling the WMI method calls - * is not thread-safe. Because of this we have to do the locking ourself. - */ - scoped_guard(mutex, &data->wmi_lock) { - status = wmidev_evaluate_method(data->wdev, 0x0, method, &in, &out); - if (ACPI_FAILURE(status)) - return -EIO; - } + status = wmidev_evaluate_method(data->wdev, 0x0, method, &in, &out); + if (ACPI_FAILURE(status)) + return -EIO; obj = out.pointer; if (!obj) @@ -177,6 +171,19 @@ static int msi_wmi_platform_query(struct msi_wmi_platform_data *data, return ret; } +static int msi_wmi_platform_query(struct msi_wmi_platform_data *data, + enum msi_wmi_platform_method method, u8 *buffer, + size_t length) +{ + /* + * The ACPI control method responsible for handling the WMI method calls + * is not thread-safe. Because of this we have to do the locking ourself. + */ + scoped_guard(mutex, &data->wmi_lock) { + return msi_wmi_platform_query_unlocked(data, method, buffer, length); + } +} + static umode_t msi_wmi_platform_is_visible(const void *drvdata, enum hwmon_sensor_types type, u32 attr, int channel) { From 53280f938bea2b201f3cd1ce32bc356b5dcedc83 Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Sun, 11 May 2025 22:44:20 +0200 Subject: [PATCH 40/73] [FROM-ML] platform/x86: msi-wmi-platform: Add quirk system MSI uses the WMI interface as a passthrough for writes to the EC and uses a board name match and a quirk table from userspace on Windows. Therefore, there is no auto-detection functionality and we have to fallback to a quirk table. Introduce it here, prior to starting to add features to it. Signed-off-by: Antheas Kapenekakis --- drivers/platform/x86/msi-wmi-platform.c | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c index 94236214c3c06..1125a41ee40f3 100644 --- a/drivers/platform/x86/msi-wmi-platform.c +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -80,8 +80,12 @@ enum msi_wmi_platform_method { MSI_PLATFORM_GET_WMI = 0x1d, }; +struct msi_wmi_platform_quirk { +}; + struct msi_wmi_platform_data { struct wmi_device *wdev; + struct msi_wmi_platform_quirk *quirks; struct mutex wmi_lock; /* Necessary when calling WMI methods */ }; @@ -125,6 +129,39 @@ static const char * const msi_wmi_platform_debugfs_names[] = { "get_wmi" }; +static struct msi_wmi_platform_quirk quirk_default = {}; +static struct msi_wmi_platform_quirk quirk_gen1 = { +}; +static struct msi_wmi_platform_quirk quirk_gen2 = { +}; + +static const struct dmi_system_id msi_quirks[] = { + { + .ident = "MSI Claw (gen 1)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."), + DMI_MATCH(DMI_BOARD_NAME, "MS-1T41"), + }, + .driver_data = &quirk_gen1, + }, + { + .ident = "MSI Claw AI+ 7", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."), + DMI_MATCH(DMI_BOARD_NAME, "MS-1T42"), + }, + .driver_data = &quirk_gen2, + }, + { + .ident = "MSI Claw AI+ 8", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."), + DMI_MATCH(DMI_BOARD_NAME, "MS-1T52"), + }, + .driver_data = &quirk_gen2, + }, +}; + static int msi_wmi_platform_parse_buffer(union acpi_object *obj, u8 *output, size_t length) { if (obj->type != ACPI_TYPE_BUFFER) @@ -414,6 +451,7 @@ static int msi_wmi_platform_init(struct msi_wmi_platform_data *data) static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) { struct msi_wmi_platform_data *data; + const struct dmi_system_id *dmi_id; int ret; data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL); @@ -423,6 +461,12 @@ static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) data->wdev = wdev; dev_set_drvdata(&wdev->dev, data); + dmi_id = dmi_first_match(msi_quirks); + if (dmi_id) + data->quirks = dmi_id->driver_data; + else + data->quirks = &quirk_default; + ret = devm_mutex_init(&wdev->dev, &data->wmi_lock); if (ret < 0) return ret; From 0337bc8152b7675d346a75dff7fac442c19bfe9a Mon Sep 17 00:00:00 2001 From: Armin Wolf Date: Sun, 11 May 2025 22:44:21 +0200 Subject: [PATCH 41/73] [FROM-ML] platform/x86: msi-wmi-platform: Add support for fan control Adds fan curve support for the MSI platform. These devices contain support for two fans, where they are named CPU and GPU but in the case of the Claw series just map to left and right fan. Co-developed-by: Antheas Kapenekakis Signed-off-by: Antheas Kapenekakis Signed-off-by: Armin Wolf --- .../wmi/devices/msi-wmi-platform.rst | 26 ++ drivers/platform/x86/msi-wmi-platform.c | 328 +++++++++++++++++- 2 files changed, 337 insertions(+), 17 deletions(-) diff --git a/Documentation/wmi/devices/msi-wmi-platform.rst b/Documentation/wmi/devices/msi-wmi-platform.rst index 73197b31926a5..704bfdac5203e 100644 --- a/Documentation/wmi/devices/msi-wmi-platform.rst +++ b/Documentation/wmi/devices/msi-wmi-platform.rst @@ -169,6 +169,32 @@ The fan RPM readings can be calculated with the following formula: If the fan speed reading is zero, then the fan RPM is zero too. +The subfeature ``0x01`` is used to retrieve the fan speed table for the CPU fan. The output +data contains the fan speed table and two bytes with unknown data. The fan speed table +consists of six 8-bit entries, each containing a fan speed value in percent. + +The subfeature ``0x02`` is used tho retrieve the same data for the GPU fan. + +WMI method Set_Fan() +-------------------- + +The fan speed tables can be accessed using subfeature ``0x01`` (CPU fan) and subfeature ``0x02`` +(GPU fan). The input data has the same format as the output data of the ``Get_Fan`` WMI method. + +WMI method Get_AP() +------------------- + +The current fan mode can be accessed using subfeature ``0x01``. The output data contains a flag +byte and two bytes of unknown data. If the 7th bit inside the flag byte is cleared then all fans +are operating in automatic mode, otherwise the fans operate based on the fan speed tables +accessible thru the ``Get_Fan``/``Set_Fan`` WMI methods. + +WMI method Set_AP() +------------------- + +The current fan mode can be changed using subfeature ``0x01``. The input data has the same format +as the output data of the ``Get_AP`` WMI method. + WMI method Get_WMI() -------------------- diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c index 1125a41ee40f3..98f233bd91efe 100644 --- a/drivers/platform/x86/msi-wmi-platform.c +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -16,13 +16,18 @@ #include #include #include +#include #include +#include #include +#include +#include #include #include #include #include #include +#include #include #include @@ -34,9 +39,11 @@ #define MSI_WMI_PLATFORM_INTERFACE_VERSION 2 +/* Get_WMI() WMI method */ #define MSI_PLATFORM_WMI_MAJOR_OFFSET 1 #define MSI_PLATFORM_WMI_MINOR_OFFSET 2 +/* Get_EC() and Set_EC() WMI methods */ #define MSI_PLATFORM_EC_FLAGS_OFFSET 1 #define MSI_PLATFORM_EC_MINOR_MASK GENMASK(3, 0) #define MSI_PLATFORM_EC_MAJOR_MASK GENMASK(5, 4) @@ -44,6 +51,18 @@ #define MSI_PLATFORM_EC_IS_TIGERLAKE BIT(7) #define MSI_PLATFORM_EC_VERSION_OFFSET 2 +/* Get_Fan() and Set_Fan() WMI methods */ +#define MSI_PLATFORM_FAN_SUBFEATURE_FAN_SPEED 0x0 +#define MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE 0x1 +#define MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE 0x2 +#define MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE 0x1 +#define MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE 0x2 + +/* Get_AP() and Set_AP() WMI methods */ +#define MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE 0x1 +#define MSI_PLATFORM_AP_FAN_FLAGS_OFFSET 1 +#define MSI_PLATFORM_AP_ENABLE_FAN_TABLES BIT(7) + static bool force; module_param_unsafe(force, bool, 0); MODULE_PARM_DESC(force, "Force loading without checking for supported WMI interface versions"); @@ -221,9 +240,201 @@ static int msi_wmi_platform_query(struct msi_wmi_platform_data *data, } } +static ssize_t msi_wmi_platform_fan_table_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { sattr->nr }; + u8 fan_percent; + int ret; + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + fan_percent = buffer[sattr->index + 1]; + if (fan_percent > 100) + return -EIO; + + return sysfs_emit(buf, "%d\n", fixp_linear_interpolate(0, 0, 100, 255, fan_percent)); +} + +static ssize_t msi_wmi_platform_fan_table_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { sattr->nr }; + long speed; + int ret; + + ret = kstrtol(buf, 10, &speed); + if (ret < 0) + return ret; + + speed = clamp_val(speed, 0, 255); + + guard(mutex)(&data->wmi_lock); + + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_GET_FAN, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + buffer[0] = sattr->nr; + buffer[sattr->index + 1] = fixp_linear_interpolate(0, 0, 255, 100, speed); + + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_SET_FAN, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t msi_wmi_platform_temp_table_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { sattr->nr }; + u8 temp_c; + int ret; + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_TEMPERATURE, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + temp_c = buffer[sattr->index + 1]; + + return sysfs_emit(buf, "%d\n", temp_c); +} + +static ssize_t msi_wmi_platform_temp_table_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { sattr->nr }; + long temp_c; + int ret; + + ret = kstrtol(buf, 10, &temp_c); + if (ret < 0) + return ret; + + temp_c = clamp_val(temp_c, 0, 255); + + guard(mutex)(&data->wmi_lock); + + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_GET_TEMPERATURE, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + buffer[0] = sattr->nr; + buffer[sattr->index + 1] = temp_c; + + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_SET_TEMPERATURE, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + return count; +} + +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x0); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x3); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x4); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x5); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x6); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x7); + +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x1); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x2); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x3); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x4); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x5); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x6); + +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x0); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x3); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point3_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x4); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point4_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x5); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point5_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x6); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point6_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x7); + +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x1); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x2); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point3_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x3); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point4_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x4); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point5_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x5); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point6_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x6); + +static struct attribute *msi_wmi_platform_hwmon_attrs[] = { + &sensor_dev_attr_pwm1_auto_point1_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point2_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point3_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point4_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point5_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point6_temp.dev_attr.attr, + + &sensor_dev_attr_pwm1_auto_point1_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point2_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point3_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point4_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point5_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point6_pwm.dev_attr.attr, + + &sensor_dev_attr_pwm2_auto_point1_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point2_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point3_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point4_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point5_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point6_temp.dev_attr.attr, + + &sensor_dev_attr_pwm2_auto_point1_pwm.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point2_pwm.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point3_pwm.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point4_pwm.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point5_pwm.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point6_pwm.dev_attr.attr, + NULL +}; +ATTRIBUTE_GROUPS(msi_wmi_platform_hwmon); + static umode_t msi_wmi_platform_is_visible(const void *drvdata, enum hwmon_sensor_types type, u32 attr, int channel) { + if (type == hwmon_pwm && attr == hwmon_pwm_enable) + return 0644; + return 0444; } @@ -233,24 +444,102 @@ static int msi_wmi_platform_read(struct device *dev, enum hwmon_sensor_types typ struct msi_wmi_platform_data *data = dev_get_drvdata(dev); u8 buffer[32] = { 0 }; u16 value; + u8 flags; int ret; - ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, buf, sizeof(buf)); - if (ret < 0) - return ret; + switch (type) { + case hwmon_fan: + switch (attr) { + case hwmon_fan_input: + buffer[0] = MSI_PLATFORM_FAN_SUBFEATURE_FAN_SPEED; + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, buffer, + sizeof(buffer)); + if (ret < 0) + return ret; + + value = get_unaligned_be16(&buffer[channel * 2 + 1]); + if (!value) + *val = 0; + else + *val = 480000 / value; + + return 0; + default: + return -EOPNOTSUPP; + } + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_enable: + buffer[0] = MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE; + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_AP, buffer, + sizeof(buffer)); + if (ret < 0) + return ret; + + flags = buffer[MSI_PLATFORM_AP_FAN_FLAGS_OFFSET]; + if (flags & MSI_PLATFORM_AP_ENABLE_FAN_TABLES) + *val = 1; + else + *val = 2; + + return 0; + default: + return -EOPNOTSUPP; + } + default: + return -EOPNOTSUPP; + } +} - value = get_unaligned_be16(&buffer[channel * 2 + 1]); - if (!value) - *val = 0; - else - *val = 480000 / value; +static int msi_wmi_platform_write(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, long val) +{ + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { }; + int ret; - return 0; + switch (type) { + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_enable: + guard(mutex)(&data->wmi_lock); + + buffer[0] = MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE; + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_GET_AP, buffer, + sizeof(buffer)); + if (ret < 0) + return ret; + + buffer[0] = MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE; + switch (val) { + case 1: + buffer[MSI_PLATFORM_AP_FAN_FLAGS_OFFSET] |= + MSI_PLATFORM_AP_ENABLE_FAN_TABLES; + break; + case 2: + buffer[MSI_PLATFORM_AP_FAN_FLAGS_OFFSET] &= + ~MSI_PLATFORM_AP_ENABLE_FAN_TABLES; + break; + default: + return -EINVAL; + } + + return msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_SET_AP, buffer, + sizeof(buffer)); + default: + return -EOPNOTSUPP; + } + default: + return -EOPNOTSUPP; + } } static const struct hwmon_ops msi_wmi_platform_ops = { .is_visible = msi_wmi_platform_is_visible, .read = msi_wmi_platform_read, + .write = msi_wmi_platform_write, }; static const struct hwmon_channel_info * const msi_wmi_platform_info[] = { @@ -260,6 +549,10 @@ static const struct hwmon_channel_info * const msi_wmi_platform_info[] = { HWMON_F_INPUT, HWMON_F_INPUT ), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_ENABLE, + HWMON_PWM_ENABLE + ), NULL }; @@ -268,8 +561,8 @@ static const struct hwmon_chip_info msi_wmi_platform_chip_info = { .info = msi_wmi_platform_info, }; -static ssize_t msi_wmi_platform_write(struct file *fp, const char __user *input, size_t length, - loff_t *offset) +static ssize_t msi_wmi_platform_debugfs_write(struct file *fp, const char __user *input, + size_t length, loff_t *offset) { struct seq_file *seq = fp->private_data; struct msi_wmi_platform_debugfs_data *data = seq->private; @@ -303,7 +596,7 @@ static ssize_t msi_wmi_platform_write(struct file *fp, const char __user *input, return length; } -static int msi_wmi_platform_show(struct seq_file *seq, void *p) +static int msi_wmi_platform_debugfs_show(struct seq_file *seq, void *p) { struct msi_wmi_platform_debugfs_data *data = seq->private; int ret; @@ -315,19 +608,19 @@ static int msi_wmi_platform_show(struct seq_file *seq, void *p) return ret; } -static int msi_wmi_platform_open(struct inode *inode, struct file *fp) +static int msi_wmi_platform_debugfs_open(struct inode *inode, struct file *fp) { struct msi_wmi_platform_debugfs_data *data = inode->i_private; /* The seq_file uses the last byte of the buffer for detecting buffer overflows */ - return single_open_size(fp, msi_wmi_platform_show, data, data->length + 1); + return single_open_size(fp, msi_wmi_platform_debugfs_show, data, data->length + 1); } static const struct file_operations msi_wmi_platform_debugfs_fops = { .owner = THIS_MODULE, - .open = msi_wmi_platform_open, + .open = msi_wmi_platform_debugfs_open, .read = seq_read, - .write = msi_wmi_platform_write, + .write = msi_wmi_platform_debugfs_write, .llseek = seq_lseek, .release = single_release, }; @@ -389,7 +682,8 @@ static int msi_wmi_platform_hwmon_init(struct msi_wmi_platform_data *data) struct device *hdev; hdev = devm_hwmon_device_register_with_info(&data->wdev->dev, "msi_wmi_platform", data, - &msi_wmi_platform_chip_info, NULL); + &msi_wmi_platform_chip_info, + msi_wmi_platform_hwmon_groups); return PTR_ERR_OR_ZERO(hdev); } From fe7871881a4978828a1dedf0a1a37878e6e27547 Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Sun, 11 May 2025 22:44:22 +0200 Subject: [PATCH 42/73] [FROM-ML] platform/x86: msi-wmi-platform: Add platform profile through shift mode MSI's version of platform profile in Windows is called shift mode. Introduce it here, and add a profile handler to it. It has 5 modes: sport, comfort, green, eco, and user. Confusingly, for the Claw, MSI only uses sport, green, and eco, where they correspond to performance, balanced, and low-power. Therefore, comfort is mapped to balanced-performance, and user to custom. Signed-off-by: Antheas Kapenekakis --- drivers/platform/x86/Kconfig | 1 + drivers/platform/x86/msi-wmi-platform.c | 117 ++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index cb97aac80248d..c24d21eda8847 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -589,6 +589,7 @@ config MSI_WMI_PLATFORM depends on ACPI_WMI depends on DMI depends on HWMON + select ACPI_PLATFORM_PROFILE help Say Y here if you want to have support for WMI-based platform features like fan sensor access on MSI machines. diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c index 98f233bd91efe..48a68f939cf89 100644 --- a/drivers/platform/x86/msi-wmi-platform.c +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -63,6 +64,16 @@ #define MSI_PLATFORM_AP_FAN_FLAGS_OFFSET 1 #define MSI_PLATFORM_AP_ENABLE_FAN_TABLES BIT(7) +/* Get_Data() and Set_Data() Shift Mode Register */ +#define MSI_PLATFORM_SHIFT_ADDR 0xd2 +#define MSI_PLATFORM_SHIFT_DISABLE BIT(7) +#define MSI_PLATFORM_SHIFT_ENABLE (BIT(7) | BIT(6)) +#define MSI_PLATFORM_SHIFT_SPORT (MSI_PLATFORM_SHIFT_ENABLE + 4) +#define MSI_PLATFORM_SHIFT_COMFORT (MSI_PLATFORM_SHIFT_ENABLE + 0) +#define MSI_PLATFORM_SHIFT_GREEN (MSI_PLATFORM_SHIFT_ENABLE + 1) +#define MSI_PLATFORM_SHIFT_ECO (MSI_PLATFORM_SHIFT_ENABLE + 2) +#define MSI_PLATFORM_SHIFT_USER (MSI_PLATFORM_SHIFT_ENABLE + 3) + static bool force; module_param_unsafe(force, bool, 0); MODULE_PARM_DESC(force, "Force loading without checking for supported WMI interface versions"); @@ -100,12 +111,14 @@ enum msi_wmi_platform_method { }; struct msi_wmi_platform_quirk { + bool shift_mode; /* Shift mode is supported */ }; struct msi_wmi_platform_data { struct wmi_device *wdev; struct msi_wmi_platform_quirk *quirks; struct mutex wmi_lock; /* Necessary when calling WMI methods */ + struct device *ppdev; }; struct msi_wmi_platform_debugfs_data { @@ -150,8 +163,10 @@ static const char * const msi_wmi_platform_debugfs_names[] = { static struct msi_wmi_platform_quirk quirk_default = {}; static struct msi_wmi_platform_quirk quirk_gen1 = { + .shift_mode = true }; static struct msi_wmi_platform_quirk quirk_gen2 = { + .shift_mode = true }; static const struct dmi_system_id msi_quirks[] = { @@ -561,6 +576,90 @@ static const struct hwmon_chip_info msi_wmi_platform_chip_info = { .info = msi_wmi_platform_info, }; +static int msi_wmi_platform_profile_probe(void *drvdata, unsigned long *choices) +{ + set_bit(PLATFORM_PROFILE_LOW_POWER, choices); + set_bit(PLATFORM_PROFILE_BALANCED, choices); + set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + return 0; +} + +static int msi_wmi_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + int ret; + + u8 buffer[32] = { }; + + buffer[0] = MSI_PLATFORM_SHIFT_ADDR; + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_DATA, buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + if (buffer[0] != 1) + return -EINVAL; + + switch (buffer[1]) { + case MSI_PLATFORM_SHIFT_SPORT: + *profile = PLATFORM_PROFILE_PERFORMANCE; + return 0; + case MSI_PLATFORM_SHIFT_COMFORT: + *profile = PLATFORM_PROFILE_BALANCED_PERFORMANCE; + return 0; + case MSI_PLATFORM_SHIFT_GREEN: + *profile = PLATFORM_PROFILE_BALANCED; + return 0; + case MSI_PLATFORM_SHIFT_ECO: + *profile = PLATFORM_PROFILE_LOW_POWER; + return 0; + case MSI_PLATFORM_SHIFT_USER: + *profile = PLATFORM_PROFILE_CUSTOM; + return 0; + default: + return -EINVAL; + } +} + +static int msi_wmi_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { }; + + buffer[0] = MSI_PLATFORM_SHIFT_ADDR; + + switch (profile) { + case PLATFORM_PROFILE_PERFORMANCE: + buffer[1] = MSI_PLATFORM_SHIFT_SPORT; + break; + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + buffer[1] = MSI_PLATFORM_SHIFT_COMFORT; + break; + case PLATFORM_PROFILE_BALANCED: + buffer[1] = MSI_PLATFORM_SHIFT_GREEN; + break; + case PLATFORM_PROFILE_LOW_POWER: + buffer[1] = MSI_PLATFORM_SHIFT_ECO; + break; + case PLATFORM_PROFILE_CUSTOM: + buffer[1] = MSI_PLATFORM_SHIFT_USER; + break; + default: + return -EINVAL; + } + + return msi_wmi_platform_query(data, MSI_PLATFORM_SET_DATA, buffer, sizeof(buffer)); +} + +static const struct platform_profile_ops msi_wmi_platform_profile_ops = { + .probe = msi_wmi_platform_profile_probe, + .profile_get = msi_wmi_platform_profile_get, + .profile_set = msi_wmi_platform_profile_set, +}; + static ssize_t msi_wmi_platform_debugfs_write(struct file *fp, const char __user *input, size_t length, loff_t *offset) { @@ -742,6 +841,22 @@ static int msi_wmi_platform_init(struct msi_wmi_platform_data *data) return 0; } +static int msi_wmi_platform_profile_setup(struct msi_wmi_platform_data *data) +{ + int err; + + if (!data->quirks->shift_mode) + return 0; + + data->ppdev = devm_platform_profile_register( + &data->wdev->dev, "msi-wmi-platform", data, + &msi_wmi_platform_profile_ops); + if (err) + return err; + + return PTR_ERR_OR_ZERO(data->ppdev); +} + static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) { struct msi_wmi_platform_data *data; @@ -775,6 +890,8 @@ static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) msi_wmi_platform_debugfs_init(data); + msi_wmi_platform_profile_setup(data); + return msi_wmi_platform_hwmon_init(data); } From 700a41bef43c004fdf4f091168b7a277b8029993 Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Sun, 11 May 2025 22:44:23 +0200 Subject: [PATCH 43/73] [FROM-ML] platform/x86: msi-wmi-platform: Add PL1/PL2 support via firmware attributes Adds PL1, and PL2 support through the firmware attributes interface. The min and max values are quirked, and the attributes are only defined if they are set to a non-zero value. These values are meant to be set in conjunction with shift mode, where shift mode automatically sets an upper bound on PL1/PL2 (e.g., low-power would be used with 8W). Signed-off-by: Antheas Kapenekakis --- drivers/platform/x86/Kconfig | 1 + drivers/platform/x86/msi-wmi-platform.c | 361 +++++++++++++++++++++++- 2 files changed, 360 insertions(+), 2 deletions(-) diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index c24d21eda8847..e3a95060c24d4 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -590,6 +590,7 @@ config MSI_WMI_PLATFORM depends on DMI depends on HWMON select ACPI_PLATFORM_PROFILE + select FW_ATTR_CLASS help Say Y here if you want to have support for WMI-based platform features like fan sensor access on MSI machines. diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c index 48a68f939cf89..16a44723d6195 100644 --- a/drivers/platform/x86/msi-wmi-platform.c +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -34,6 +34,8 @@ #include +#include "firmware_attributes_class.h" + #define DRIVER_NAME "msi-wmi-platform" #define MSI_PLATFORM_GUID "ABBC0F6E-8EA1-11D1-00A0-C90629100000" @@ -74,6 +76,10 @@ #define MSI_PLATFORM_SHIFT_ECO (MSI_PLATFORM_SHIFT_ENABLE + 2) #define MSI_PLATFORM_SHIFT_USER (MSI_PLATFORM_SHIFT_ENABLE + 3) +/* Get_Data() and Set_Data() Params */ +#define MSI_PLATFORM_PL1_ADDR 0x50 +#define MSI_PLATFORM_PL2_ADDR 0x51 + static bool force; module_param_unsafe(force, bool, 0); MODULE_PARM_DESC(force, "Force loading without checking for supported WMI interface versions"); @@ -112,6 +118,9 @@ enum msi_wmi_platform_method { struct msi_wmi_platform_quirk { bool shift_mode; /* Shift mode is supported */ + int pl_min; /* Minimum PLx value */ + int pl1_max; /* Maximum PL1 value */ + int pl2_max; /* Maximum PL2 value */ }; struct msi_wmi_platform_data { @@ -119,6 +128,44 @@ struct msi_wmi_platform_data { struct msi_wmi_platform_quirk *quirks; struct mutex wmi_lock; /* Necessary when calling WMI methods */ struct device *ppdev; + struct device *fw_attrs_dev; + struct kset *fw_attrs_kset; +}; + +enum msi_fw_attr_id { + MSI_ATTR_PPT_PL1_SPL, + MSI_ATTR_PPT_PL2_SPPT, +}; + +static const char *const msi_fw_attr_name[] = { + [MSI_ATTR_PPT_PL1_SPL] = "ppt_pl1_spl", + [MSI_ATTR_PPT_PL2_SPPT] = "ppt_pl2_sppt", +}; + +static const char *const msi_fw_attr_desc[] = { + [MSI_ATTR_PPT_PL1_SPL] = "CPU Steady package limit (PL1/SPL)", + [MSI_ATTR_PPT_PL2_SPPT] = "CPU Boost slow package limit (PL2/SPPT)", +}; + +#define MSI_ATTR_LANGUAGE_CODE "en_US.UTF-8" + +struct msi_fw_attr { + struct msi_wmi_platform_data *data; + enum msi_fw_attr_id fw_attr_id; + struct attribute_group attr_group; + struct kobj_attribute display_name; + struct kobj_attribute current_value; + struct kobj_attribute min_value; + struct kobj_attribute max_value; + + u32 min; + u32 max; + + int (*get_value)(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, char *buf); + ssize_t (*set_value)(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, const char *buf, + size_t count); }; struct msi_wmi_platform_debugfs_data { @@ -163,10 +210,16 @@ static const char * const msi_wmi_platform_debugfs_names[] = { static struct msi_wmi_platform_quirk quirk_default = {}; static struct msi_wmi_platform_quirk quirk_gen1 = { - .shift_mode = true + .shift_mode = true, + .pl_min = 8, + .pl1_max = 43, + .pl2_max = 45 }; static struct msi_wmi_platform_quirk quirk_gen2 = { - .shift_mode = true + .shift_mode = true, + .pl_min = 8, + .pl1_max = 30, + .pl2_max = 37 }; static const struct dmi_system_id msi_quirks[] = { @@ -660,6 +713,306 @@ static const struct platform_profile_ops msi_wmi_platform_profile_ops = { .profile_set = msi_wmi_platform_profile_set, }; +/* Firmware Attributes setup */ +static int data_get_addr(struct msi_wmi_platform_data *data, + const enum msi_fw_attr_id id) +{ + switch (id) { + case MSI_ATTR_PPT_PL1_SPL: + return MSI_PLATFORM_PL1_ADDR; + case MSI_ATTR_PPT_PL2_SPPT: + return MSI_PLATFORM_PL2_ADDR; + default: + pr_warn("Invalid attribute id %d\n", id); + return -EINVAL; + } +} + +static ssize_t data_set_value(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, const char *buf, + size_t count) +{ + u8 buffer[32] = { 0 }; + int ret, fwid; + u32 value; + + fwid = data_get_addr(data, fw_attr->fw_attr_id); + if (fwid < 0) + return fwid; + + ret = kstrtou32(buf, 10, &value); + if (ret) + return ret; + + if (fw_attr->min >= 0 && value < fw_attr->min) + return -EINVAL; + if (fw_attr->max >= 0 && value > fw_attr->max) + return -EINVAL; + + buffer[0] = fwid; + put_unaligned_le32(value, &buffer[1]); + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_SET_DATA, buffer, sizeof(buffer)); + if (ret) { + pr_warn("Failed to set_data with id %d: %d\n", + fw_attr->fw_attr_id, ret); + return ret; + } + + return count; +} + +static int data_get_value(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, char *buf) +{ + u8 buffer[32] = { 0 }; + u32 value; + int ret, addr; + + addr = data_get_addr(data, fw_attr->fw_attr_id); + if (addr < 0) + return addr; + + buffer[0] = addr; + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_DATA, buffer, sizeof(buffer)); + if (ret) { + pr_warn("Failed to show set_data for id %d: %d\n", + fw_attr->fw_attr_id, ret); + return ret; + } + + value = get_unaligned_le32(&buffer[1]); + + return sysfs_emit(buf, "%d\n", value); +} + +static ssize_t display_name_language_code_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%s\n", MSI_ATTR_LANGUAGE_CODE); +} + +static struct kobj_attribute fw_attr_display_name_language_code = + __ATTR_RO(display_name_language_code); + +static ssize_t scalar_increment_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "1\n"); +} + +static struct kobj_attribute fw_attr_scalar_increment = + __ATTR_RO(scalar_increment); + +static ssize_t pending_reboot_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0\n"); +} + +static struct kobj_attribute fw_attr_pending_reboot = __ATTR_RO(pending_reboot); + +static ssize_t display_name_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct msi_fw_attr *fw_attr = + container_of(attr, struct msi_fw_attr, display_name); + + return sysfs_emit(buf, "%s\n", msi_fw_attr_desc[fw_attr->fw_attr_id]); +} + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct msi_fw_attr *fw_attr = + container_of(attr, struct msi_fw_attr, current_value); + + return fw_attr->get_value(fw_attr->data, fw_attr, buf); +} + +static ssize_t current_value_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct msi_fw_attr *fw_attr = + container_of(attr, struct msi_fw_attr, current_value); + + return fw_attr->set_value(fw_attr->data, fw_attr, buf, count); +} + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "integer\n"); +} + +static struct kobj_attribute fw_attr_type_int = { + .attr = { .name = "type", .mode = 0444 }, + .show = type_show, +}; + +static ssize_t min_value_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct msi_fw_attr *fw_attr = + container_of(attr, struct msi_fw_attr, min_value); + + return sysfs_emit(buf, "%d\n", fw_attr->min); +} + +static ssize_t max_value_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct msi_fw_attr *fw_attr = + container_of(attr, struct msi_fw_attr, max_value); + + return sysfs_emit(buf, "%d\n", fw_attr->max); +} + +#define FW_ATTR_ENUM_MAX_ATTRS 7 + +static int +msi_fw_attr_init(struct msi_wmi_platform_data *data, + const enum msi_fw_attr_id fw_attr_id, + struct kobj_attribute *fw_attr_type, const s32 min, + const s32 max, + int (*get_value)(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, char *buf), + ssize_t (*set_value)(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, + const char *buf, size_t count)) +{ + struct msi_fw_attr *fw_attr; + struct attribute **attrs; + int idx = 0; + + fw_attr = devm_kzalloc(&data->wdev->dev, sizeof(*fw_attr), GFP_KERNEL); + if (!fw_attr) + return -ENOMEM; + + attrs = devm_kcalloc(&data->wdev->dev, FW_ATTR_ENUM_MAX_ATTRS + 1, + sizeof(*attrs), GFP_KERNEL); + if (!attrs) + return -ENOMEM; + + fw_attr->data = data; + fw_attr->fw_attr_id = fw_attr_id; + fw_attr->attr_group.name = msi_fw_attr_name[fw_attr_id]; + fw_attr->attr_group.attrs = attrs; + fw_attr->get_value = get_value; + fw_attr->set_value = set_value; + + attrs[idx++] = &fw_attr_type->attr; + if (fw_attr_type == &fw_attr_type_int) + attrs[idx++] = &fw_attr_scalar_increment.attr; + attrs[idx++] = &fw_attr_display_name_language_code.attr; + + sysfs_attr_init(&fw_attr->display_name.attr); + fw_attr->display_name.attr.name = "display_name"; + fw_attr->display_name.attr.mode = 0444; + fw_attr->display_name.show = display_name_show; + attrs[idx++] = &fw_attr->display_name.attr; + + sysfs_attr_init(&fw_attr->current_value.attr); + fw_attr->current_value.attr.name = "current_value"; + fw_attr->current_value.attr.mode = 0644; + fw_attr->current_value.show = current_value_show; + fw_attr->current_value.store = current_value_store; + attrs[idx++] = &fw_attr->current_value.attr; + + if (min >= 0) { + fw_attr->min = min; + sysfs_attr_init(&fw_attr->min_value.attr); + fw_attr->min_value.attr.name = "min_value"; + fw_attr->min_value.attr.mode = 0444; + fw_attr->min_value.show = min_value_show; + attrs[idx++] = &fw_attr->min_value.attr; + } else { + fw_attr->min = -1; + } + + if (max >= 0) { + fw_attr->max = max; + sysfs_attr_init(&fw_attr->max_value.attr); + fw_attr->max_value.attr.name = "max_value"; + fw_attr->max_value.attr.mode = 0444; + fw_attr->max_value.show = max_value_show; + attrs[idx++] = &fw_attr->max_value.attr; + } else { + fw_attr->max = -1; + } + + attrs[idx] = NULL; + return sysfs_create_group(&data->fw_attrs_kset->kobj, &fw_attr->attr_group); +} + +static void msi_kset_unregister(void *data) +{ + struct kset *kset = data; + + sysfs_remove_file(&kset->kobj, &fw_attr_pending_reboot.attr); + kset_unregister(kset); +} + +static void msi_fw_attrs_dev_unregister(void *data) +{ + struct device *fw_attrs_dev = data; + + device_unregister(fw_attrs_dev); +} + +static int msi_wmi_fw_attrs_init(struct msi_wmi_platform_data *data) +{ + int err; + + data->fw_attrs_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), + NULL, "%s", DRIVER_NAME); + if (IS_ERR(data->fw_attrs_dev)) + return PTR_ERR(data->fw_attrs_dev); + + err = devm_add_action_or_reset(&data->wdev->dev, + msi_fw_attrs_dev_unregister, + data->fw_attrs_dev); + if (err) + return err; + + data->fw_attrs_kset = kset_create_and_add("attributes", NULL, + &data->fw_attrs_dev->kobj); + if (!data->fw_attrs_kset) + return -ENOMEM; + + err = sysfs_create_file(&data->fw_attrs_kset->kobj, + &fw_attr_pending_reboot.attr); + if (err) { + kset_unregister(data->fw_attrs_kset); + return err; + } + + err = devm_add_action_or_reset(&data->wdev->dev, msi_kset_unregister, + data->fw_attrs_kset); + if (err) + return err; + + if (data->quirks->pl1_max) { + err = msi_fw_attr_init(data, MSI_ATTR_PPT_PL1_SPL, + &fw_attr_type_int, data->quirks->pl_min, + data->quirks->pl1_max, &data_get_value, + &data_set_value); + if (err) + return err; + } + + if (data->quirks->pl2_max) { + err = msi_fw_attr_init(data, MSI_ATTR_PPT_PL2_SPPT, + &fw_attr_type_int, data->quirks->pl_min, + data->quirks->pl2_max, &data_get_value, + &data_set_value); + if (err) + return err; + } + + return 0; +} + static ssize_t msi_wmi_platform_debugfs_write(struct file *fp, const char __user *input, size_t length, loff_t *offset) { @@ -888,6 +1241,10 @@ static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) if (ret < 0) return ret; + ret = msi_wmi_fw_attrs_init(data); + if (ret < 0) + return ret; + msi_wmi_platform_debugfs_init(data); msi_wmi_platform_profile_setup(data); From d04f6fe2fcc541c69e0500adc5f35bf4d8aaeefa Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Sun, 11 May 2025 22:44:24 +0200 Subject: [PATCH 44/73] [FROM-ML] platform/x86: msi-wmi-platform: Add charge_threshold support The battery of MSI laptops supports charge threshold. Add support for it. Signed-off-by: Antheas Kapenekakis --- drivers/platform/x86/Kconfig | 1 + drivers/platform/x86/msi-wmi-platform.c | 110 ++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index e3a95060c24d4..70e014856e376 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -586,6 +586,7 @@ config MSI_WMI config MSI_WMI_PLATFORM tristate "MSI WMI Platform features" + depends on ACPI_BATTERY depends on ACPI_WMI depends on DMI depends on HWMON diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c index 16a44723d6195..5f8260be85cc3 100644 --- a/drivers/platform/x86/msi-wmi-platform.c +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -31,6 +31,7 @@ #include #include #include +#include #include @@ -79,6 +80,7 @@ /* Get_Data() and Set_Data() Params */ #define MSI_PLATFORM_PL1_ADDR 0x50 #define MSI_PLATFORM_PL2_ADDR 0x51 +#define MSI_PLATFORM_BAT_ADDR 0xd7 static bool force; module_param_unsafe(force, bool, 0); @@ -118,6 +120,7 @@ enum msi_wmi_platform_method { struct msi_wmi_platform_quirk { bool shift_mode; /* Shift mode is supported */ + bool charge_threshold; /* Charge threshold is supported */ int pl_min; /* Minimum PLx value */ int pl1_max; /* Maximum PL1 value */ int pl2_max; /* Maximum PL2 value */ @@ -128,6 +131,7 @@ struct msi_wmi_platform_data { struct msi_wmi_platform_quirk *quirks; struct mutex wmi_lock; /* Necessary when calling WMI methods */ struct device *ppdev; + struct acpi_battery_hook battery_hook; struct device *fw_attrs_dev; struct kset *fw_attrs_kset; }; @@ -211,12 +215,14 @@ static const char * const msi_wmi_platform_debugfs_names[] = { static struct msi_wmi_platform_quirk quirk_default = {}; static struct msi_wmi_platform_quirk quirk_gen1 = { .shift_mode = true, + .charge_threshold = true, .pl_min = 8, .pl1_max = 43, .pl2_max = 45 }; static struct msi_wmi_platform_quirk quirk_gen2 = { .shift_mode = true, + .charge_threshold = true, .pl_min = 8, .pl1_max = 30, .pl2_max = 37 @@ -1013,6 +1019,94 @@ static int msi_wmi_fw_attrs_init(struct msi_wmi_platform_data *data) return 0; } +static int msi_platform_psy_ext_get_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *data, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct msi_wmi_platform_data *msi = data; + u8 buffer[32] = { 0 }; + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + buffer[0] = MSI_PLATFORM_BAT_ADDR; + ret = msi_wmi_platform_query(msi, MSI_PLATFORM_GET_DATA, + buffer, sizeof(buffer)); + if (ret) + return ret; + + val->intval = buffer[1] & ~BIT(7); + if (val->intval > 100) + return -EINVAL; + + return 0; + default: + return -EINVAL; + } +} + +static int msi_platform_psy_ext_set_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *data, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct msi_wmi_platform_data *msi = data; + u8 buffer[32] = { 0 }; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + if (val->intval > 100) + return -EINVAL; + buffer[0] = MSI_PLATFORM_BAT_ADDR; + buffer[1] = val->intval | BIT(7); + return msi_wmi_platform_query(msi, MSI_PLATFORM_SET_DATA, + buffer, sizeof(buffer)); + default: + return -EINVAL; + } +} + +static int +msi_platform_psy_prop_is_writeable(struct power_supply *psy, + const struct power_supply_ext *ext, + void *data, enum power_supply_property psp) +{ + return true; +} + +static const enum power_supply_property oxp_psy_ext_props[] = { + POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD, +}; + +static const struct power_supply_ext msi_platform_psy_ext = { + .name = "msi-platform-charge-control", + .properties = oxp_psy_ext_props, + .num_properties = ARRAY_SIZE(oxp_psy_ext_props), + .get_property = msi_platform_psy_ext_get_prop, + .set_property = msi_platform_psy_ext_set_prop, + .property_is_writeable = msi_platform_psy_prop_is_writeable, +}; + +static int msi_wmi_platform_battery_add(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + struct msi_wmi_platform_data *data = + container_of(hook, struct msi_wmi_platform_data, battery_hook); + + return power_supply_register_extension(battery, &msi_platform_psy_ext, + &data->wdev->dev, data); +} + +static int msi_wmi_platform_battery_remove(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + power_supply_unregister_extension(battery, &msi_platform_psy_ext); + return 0; +} + static ssize_t msi_wmi_platform_debugfs_write(struct file *fp, const char __user *input, size_t length, loff_t *offset) { @@ -1245,6 +1339,13 @@ static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) if (ret < 0) return ret; + if (data->quirks->charge_threshold) { + data->battery_hook.name = "MSI Battery"; + data->battery_hook.add_battery = msi_wmi_platform_battery_add; + data->battery_hook.remove_battery = msi_wmi_platform_battery_remove; + battery_hook_register(&data->battery_hook); + } + msi_wmi_platform_debugfs_init(data); msi_wmi_platform_profile_setup(data); @@ -1252,6 +1353,14 @@ static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) return msi_wmi_platform_hwmon_init(data); } +static void msi_wmi_platform_remove(struct wmi_device *wdev) +{ + struct msi_wmi_platform_data *data = dev_get_drvdata(&wdev->dev); + + if (data->quirks->charge_threshold) + battery_hook_unregister(&data->battery_hook); +} + static const struct wmi_device_id msi_wmi_platform_id_table[] = { { MSI_PLATFORM_GUID, NULL }, { } @@ -1265,6 +1374,7 @@ static struct wmi_driver msi_wmi_platform_driver = { }, .id_table = msi_wmi_platform_id_table, .probe = msi_wmi_platform_probe, + .remove = msi_wmi_platform_remove, .no_singleton = true, }; From 47fffe58f0477c96e2f82dbd941e8e3afc038803 Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Sun, 11 May 2025 22:44:25 +0200 Subject: [PATCH 45/73] [FROM-ML] platform/x86: msi-wmi-platform: Drop excess fans in dual fan devices Currently, the platform driver always exposes 4 fans, since the underlying WMI interface reads 4 values from the EC. However, most devices only have two fans. Therefore, at least in the case of the Claw series, quirk the driver to only show two hwmon fans. Signed-off-by: Antheas Kapenekakis --- drivers/platform/x86/msi-wmi-platform.c | 28 ++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c index 5f8260be85cc3..8cae145202d38 100644 --- a/drivers/platform/x86/msi-wmi-platform.c +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -121,6 +121,7 @@ enum msi_wmi_platform_method { struct msi_wmi_platform_quirk { bool shift_mode; /* Shift mode is supported */ bool charge_threshold; /* Charge threshold is supported */ + bool dual_fans; /* For devices with two hwmon fans */ int pl_min; /* Minimum PLx value */ int pl1_max; /* Maximum PL1 value */ int pl2_max; /* Maximum PL2 value */ @@ -216,6 +217,7 @@ static struct msi_wmi_platform_quirk quirk_default = {}; static struct msi_wmi_platform_quirk quirk_gen1 = { .shift_mode = true, .charge_threshold = true, + .dual_fans = true, .pl_min = 8, .pl1_max = 43, .pl2_max = 45 @@ -223,6 +225,7 @@ static struct msi_wmi_platform_quirk quirk_gen1 = { static struct msi_wmi_platform_quirk quirk_gen2 = { .shift_mode = true, .charge_threshold = true, + .dual_fans = true, .pl_min = 8, .pl1_max = 30, .pl2_max = 37 @@ -635,6 +638,23 @@ static const struct hwmon_chip_info msi_wmi_platform_chip_info = { .info = msi_wmi_platform_info, }; +static const struct hwmon_channel_info * const msi_wmi_platform_info_dual[] = { + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT, + HWMON_F_INPUT + ), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_ENABLE, + HWMON_PWM_ENABLE + ), + NULL +}; + +static const struct hwmon_chip_info msi_wmi_platform_chip_info_dual = { + .ops = &msi_wmi_platform_ops, + .info = msi_wmi_platform_info_dual, +}; + static int msi_wmi_platform_profile_probe(void *drvdata, unsigned long *choices) { set_bit(PLATFORM_PROFILE_LOW_POWER, choices); @@ -1227,9 +1247,11 @@ static int msi_wmi_platform_hwmon_init(struct msi_wmi_platform_data *data) { struct device *hdev; - hdev = devm_hwmon_device_register_with_info(&data->wdev->dev, "msi_wmi_platform", data, - &msi_wmi_platform_chip_info, - msi_wmi_platform_hwmon_groups); + hdev = devm_hwmon_device_register_with_info( + &data->wdev->dev, "msi_wmi_platform", data, + data->quirks->dual_fans ? &msi_wmi_platform_chip_info_dual : + &msi_wmi_platform_chip_info, + msi_wmi_platform_hwmon_groups); return PTR_ERR_OR_ZERO(hdev); } From 8a35b5f3477ad06e4a7146344ffdcd4b337f8657 Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Sun, 11 May 2025 22:44:26 +0200 Subject: [PATCH 46/73] [FROM-ML] platform/x86: msi-wmi-platform: Update header text Update copyright information in the header and specify that this driver also applies to handhelds. Signed-off-by: Antheas Kapenekakis --- drivers/platform/x86/msi-wmi-platform.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c index 8cae145202d38..347510f671342 100644 --- a/drivers/platform/x86/msi-wmi-platform.c +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Linux driver for WMI platform features on MSI notebooks. + * Linux driver for WMI platform features on MSI notebooks and handhelds. * - * Copyright (C) 2024 Armin Wolf + * Copyright (C) 2024-2025 Armin Wolf + * Copyright (C) 2025 Antheas Kapenekakis */ #define pr_format(fmt) KBUILD_MODNAME ": " fmt From a9034a5487597f4692a67de7dde60b1f3ab5a6d8 Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Sun, 11 May 2025 22:44:27 +0200 Subject: [PATCH 47/73] [FROM-ML] platform/x86: msi-wmi-platform: Restore fan curves on PWM disable and unload MSI software is a bit weird in that even when the manual fan curve is disabled, the fan speed is still somewhat affected by the curve. So we have to restore the fan curves on unload and PWM disable, as it is done in Windows. Suggested-by: Armin Wolf Signed-off-by: Antheas Kapenekakis --- drivers/platform/x86/msi-wmi-platform.c | 123 +++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c index 347510f671342..dfb65ac8fbf6c 100644 --- a/drivers/platform/x86/msi-wmi-platform.c +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -123,16 +123,25 @@ struct msi_wmi_platform_quirk { bool shift_mode; /* Shift mode is supported */ bool charge_threshold; /* Charge threshold is supported */ bool dual_fans; /* For devices with two hwmon fans */ + bool restore_curves; /* Restore factory curves on unload */ int pl_min; /* Minimum PLx value */ int pl1_max; /* Maximum PL1 value */ int pl2_max; /* Maximum PL2 value */ }; +struct msi_wmi_platform_factory_curves { + u8 cpu_fan_table[32]; + u8 gpu_fan_table[32]; + u8 cpu_temp_table[32]; + u8 gpu_temp_table[32]; +}; + struct msi_wmi_platform_data { struct wmi_device *wdev; struct msi_wmi_platform_quirk *quirks; struct mutex wmi_lock; /* Necessary when calling WMI methods */ struct device *ppdev; + struct msi_wmi_platform_factory_curves factory_curves; struct acpi_battery_hook battery_hook; struct device *fw_attrs_dev; struct kset *fw_attrs_kset; @@ -219,6 +228,7 @@ static struct msi_wmi_platform_quirk quirk_gen1 = { .shift_mode = true, .charge_threshold = true, .dual_fans = true, + .restore_curves = true, .pl_min = 8, .pl1_max = 43, .pl2_max = 45 @@ -227,6 +237,7 @@ static struct msi_wmi_platform_quirk quirk_gen2 = { .shift_mode = true, .charge_threshold = true, .dual_fans = true, + .restore_curves = true, .pl_min = 8, .pl1_max = 30, .pl2_max = 37 @@ -507,6 +518,94 @@ static struct attribute *msi_wmi_platform_hwmon_attrs[] = { }; ATTRIBUTE_GROUPS(msi_wmi_platform_hwmon); +static int msi_wmi_platform_curves_save(struct msi_wmi_platform_data *data) +{ + int ret; + + data->factory_curves.cpu_fan_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE; + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_GET_FAN, + data->factory_curves.cpu_fan_table, + sizeof(data->factory_curves.cpu_fan_table)); + if (ret < 0) + return ret; + data->factory_curves.cpu_fan_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE; + + data->factory_curves.gpu_fan_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE; + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_GET_FAN, + data->factory_curves.gpu_fan_table, + sizeof(data->factory_curves.gpu_fan_table)); + if (ret < 0) + return ret; + data->factory_curves.gpu_fan_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE; + + data->factory_curves.cpu_temp_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE; + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_GET_TEMPERATURE, + data->factory_curves.cpu_temp_table, + sizeof(data->factory_curves.cpu_temp_table)); + if (ret < 0) + return ret; + data->factory_curves.cpu_temp_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE; + + data->factory_curves.gpu_temp_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE; + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_GET_TEMPERATURE, + data->factory_curves.gpu_temp_table, + sizeof(data->factory_curves.gpu_temp_table)); + if (ret < 0) + return ret; + data->factory_curves.gpu_temp_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE; + + return 0; +} + +static int msi_wmi_platform_curves_load(struct msi_wmi_platform_data *data) +{ + u8 buffer[32] = { }; + int ret; + + memcpy(buffer, data->factory_curves.cpu_fan_table, + sizeof(data->factory_curves.cpu_fan_table)); + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_SET_FAN, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + memcpy(buffer, data->factory_curves.gpu_fan_table, + sizeof(data->factory_curves.gpu_fan_table)); + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_SET_FAN, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + memcpy(buffer, data->factory_curves.cpu_temp_table, + sizeof(data->factory_curves.cpu_temp_table)); + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_SET_TEMPERATURE, buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + memcpy(buffer, data->factory_curves.gpu_temp_table, + sizeof(data->factory_curves.gpu_temp_table)); + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_SET_TEMPERATURE, buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + return 0; +} + + static umode_t msi_wmi_platform_is_visible(const void *drvdata, enum hwmon_sensor_types type, u32 attr, int channel) { @@ -603,9 +702,19 @@ static int msi_wmi_platform_write(struct device *dev, enum hwmon_sensor_types ty return -EINVAL; } - return msi_wmi_platform_query_unlocked( + ret = msi_wmi_platform_query_unlocked( data, MSI_PLATFORM_SET_AP, buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + if (val == 2 && data->quirks->restore_curves) { + ret = msi_wmi_platform_curves_load(data); + if (ret < 0) + return ret; + } + + return 0; default: return -EOPNOTSUPP; } @@ -1373,6 +1482,13 @@ static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) msi_wmi_platform_profile_setup(data); + if (data->quirks->restore_curves) { + guard(mutex)(&data->wmi_lock); + ret = msi_wmi_platform_curves_save(data); + if (ret < 0) + return ret; + } + return msi_wmi_platform_hwmon_init(data); } @@ -1382,6 +1498,11 @@ static void msi_wmi_platform_remove(struct wmi_device *wdev) if (data->quirks->charge_threshold) battery_hook_unregister(&data->battery_hook); + + if (data->quirks->restore_curves) { + guard(mutex)(&data->wmi_lock); + msi_wmi_platform_curves_load(data); + } } static const struct wmi_device_id msi_wmi_platform_id_table[] = { From ed3f668ba07e5c631f136e51fd070b31ae586f93 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Fri, 15 May 2026 22:24:07 -0700 Subject: [PATCH 48/73] Revert "[FROM-ML] HID: hid-msi: Add Rumble Intensity Attributes" This reverts commit 10a2fcd0228cf20433b21bf57e4488c0a35d3918. --- drivers/hid/hid-msi.c | 147 ------------------------------------------ 1 file changed, 147 deletions(-) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index cffd6ed253ec9..a628b77bfb7b5 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -76,8 +76,6 @@ enum claw_profile_ack_pending { CLAW_M1_PENDING, CLAW_M2_PENDING, CLAW_RGB_PENDING, - CLAW_RUMBLE_LEFT_PENDING, - CLAW_RUMBLE_RIGHT_PENDING, }; enum claw_key_index { @@ -264,11 +262,6 @@ static const u16 button_mapping_addr_new[] = { static const u16 rgb_addr_old = 0x01fa; static const u16 rgb_addr_new = 0x024a; -static const u16 rumble_addr[] = { - 0x0022, /* left */ - 0x0023, /* right */ -}; - struct claw_command_report { u8 report_id; u8 padding[2]; @@ -317,10 +310,7 @@ struct claw_drvdata { enum claw_gamepad_mode_index gamepad_mode; u8 m1_codes[CLAW_KEYS_MAX]; u8 m2_codes[CLAW_KEYS_MAX]; - u8 rumble_intensity_right; - u8 rumble_intensity_left; const u16 *bmap_addr; - bool rumble_support; bool bmap_support; /* RGB Variables */ @@ -412,12 +402,6 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_ memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data, sizeof(struct rgb_frame)); - break; - case CLAW_RUMBLE_LEFT_PENDING: - drvdata->rumble_intensity_left = cmd_rep->data[4]; - break; - case CLAW_RUMBLE_RIGHT_PENDING: - drvdata->rumble_intensity_right = cmd_rep->data[4]; break; default: dev_warn(&drvdata->hdev->dev, @@ -831,126 +815,6 @@ static ssize_t button_mapping_options_show(struct device *dev, } static DEVICE_ATTR_RO(button_mapping_options); -static ssize_t rumble_intensity_left_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - u8 data[] = { 0x01, (rumble_addr[0] >> 8) & 0xff, rumble_addr[0] & 0xff, 0x01, 0x00 }; - struct hid_device *hdev = to_hid_device(dev); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - u8 val; - int ret; - - ret = kstrtou8(buf, 10, &val); - if (ret) - return ret; - - if (val > 100) - return -EINVAL; - - data[4] = val; - - guard(mutex)(&drvdata->rom_mutex); - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, - data, ARRAY_SIZE(data), 8); - if (ret) - return ret; - - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); - if (ret) - return ret; - - drvdata->rumble_intensity_left = val; - - return count; -} - -static ssize_t rumble_intensity_left_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - u8 data[4] = { 0x01, (rumble_addr[0] >> 8) & 0xff, rumble_addr[0] & 0xff, 0x01 }; - struct hid_device *hdev = to_hid_device(dev); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - int ret; - - guard(mutex)(&drvdata->profile_mutex); - drvdata->profile_pending = CLAW_RUMBLE_LEFT_PENDING; - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, - ARRAY_SIZE(data), 8); - if (ret) { - drvdata->profile_pending = CLAW_NO_PENDING; - return ret; - } - - return sysfs_emit(buf, "%u\n", drvdata->rumble_intensity_left); -} -static DEVICE_ATTR_RW(rumble_intensity_left); - -static ssize_t rumble_intensity_right_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - u8 data[] = { 0x01, (rumble_addr[1] >> 8) & 0xff, rumble_addr[1] & 0xff, 0x01, 0x00 }; - struct hid_device *hdev = to_hid_device(dev); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - u8 val; - int ret; - - ret = kstrtou8(buf, 10, &val); - if (ret) - return ret; - - if (val > 100) - return -EINVAL; - - data[4] = val; - - guard(mutex)(&drvdata->rom_mutex); - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, - data, ARRAY_SIZE(data), 8); - if (ret) - return ret; - - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); - if (ret) - return ret; - - drvdata->rumble_intensity_right = val; - - return count; -} - -static ssize_t rumble_intensity_right_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - u8 data[4] = { 0x01, (rumble_addr[1] >> 8) & 0xff, rumble_addr[1] & 0xff, 0x01 }; - struct hid_device *hdev = to_hid_device(dev); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - int ret; - - guard(mutex)(&drvdata->profile_mutex); - drvdata->profile_pending = CLAW_RUMBLE_RIGHT_PENDING; - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, - ARRAY_SIZE(data), 8); - if (ret) { - drvdata->profile_pending = CLAW_NO_PENDING; - return ret; - } - - return sysfs_emit(buf, "%u\n", drvdata->rumble_intensity_right); -} -static DEVICE_ATTR_RW(rumble_intensity_right); - -static ssize_t rumble_intensity_range_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return sysfs_emit(buf, "0-100\n"); -} -static DEVICE_ATTR_RO(rumble_intensity_range); - static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n) { @@ -971,12 +835,6 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu attr == &dev_attr_reset.attr) return attr->mode; - /* Hide rumble attrs if not supported */ - if (attr == &dev_attr_rumble_intensity_left.attr || - attr == &dev_attr_rumble_intensity_right.attr || - attr == &dev_attr_rumble_intensity_range.attr) - return drvdata->rumble_support ? attr->mode : 0; - /* Hide button mapping attrs if it isn't supported */ return drvdata->bmap_support ? attr->mode : 0; } @@ -990,9 +848,6 @@ static struct attribute *claw_gamepad_attrs[] = { &dev_attr_mkeys_function.attr, &dev_attr_mkeys_function_index.attr, &dev_attr_reset.attr, - &dev_attr_rumble_intensity_left.attr, - &dev_attr_rumble_intensity_right.attr, - &dev_attr_rumble_intensity_range.attr, NULL, }; @@ -1457,7 +1312,6 @@ static void claw_features_supported(struct claw_drvdata *drvdata) drvdata->bmap_support = true; if (minor >= 0x66) { drvdata->bmap_addr = button_mapping_addr_new; - drvdata->rumble_support = true; drvdata->rgb_addr = rgb_addr_new; } else { drvdata->bmap_addr = button_mapping_addr_old; @@ -1469,7 +1323,6 @@ static void claw_features_supported(struct claw_drvdata *drvdata) if ((major == 0x02 && minor >= 0x17) || major >= 0x03) { drvdata->bmap_support = true; drvdata->bmap_addr = button_mapping_addr_new; - drvdata->rumble_support = true; drvdata->rgb_addr = rgb_addr_new; return; } From f0037789aa47cbb25013e8d34981b33694f45ec0 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Fri, 15 May 2026 22:24:07 -0700 Subject: [PATCH 49/73] Revert "[FROM-ML] HID: hid-msi: Add RGB control interface" This reverts commit be5174ecf2844a2a2eff74827eb34d47132d907e. --- drivers/hid/hid-msi.c | 548 +----------------------------------------- 1 file changed, 6 insertions(+), 542 deletions(-) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index a628b77bfb7b5..13ba2747fdb66 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -43,10 +42,6 @@ #define CLAW_KEYS_MAX 5 -#define CLAW_RGB_ZONES 9 -#define CLAW_RGB_MAX_FRAMES 8 -#define CLAW_RGB_FRAME_OFFSET 0x24 - enum claw_command_index { CLAW_COMMAND_TYPE_READ_PROFILE = 0x04, CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05, @@ -75,7 +70,6 @@ enum claw_profile_ack_pending { CLAW_NO_PENDING, CLAW_M1_PENDING, CLAW_M2_PENDING, - CLAW_RGB_PENDING, }; enum claw_key_index { @@ -233,22 +227,6 @@ static const struct { { 0xce, "REL_WHEEL_DOWN" }, }; -enum claw_rgb_effect_index { - CLAW_RGB_EFFECT_MONOCOLOR, - CLAW_RGB_EFFECT_BREATHE, - CLAW_RGB_EFFECT_CHROMA, - CLAW_RGB_EFFECT_RAINBOW, - CLAW_RGB_EFFECT_FROSTFIRE, -}; - -static const char * const claw_rgb_effect_text[] = { - [CLAW_RGB_EFFECT_MONOCOLOR] = "monocolor", - [CLAW_RGB_EFFECT_BREATHE] = "breathe", - [CLAW_RGB_EFFECT_CHROMA] = "chroma", - [CLAW_RGB_EFFECT_RAINBOW] = "rainbow", - [CLAW_RGB_EFFECT_FROSTFIRE] = "frostfire", -}; - static const u16 button_mapping_addr_old[] = { 0x007a, /* M1 */ 0x011f, /* M2 */ @@ -259,9 +237,6 @@ static const u16 button_mapping_addr_new[] = { 0x0164, /* M2 */ }; -static const u16 rgb_addr_old = 0x01fa; -static const u16 rgb_addr_new = 0x024a; - struct claw_command_report { u8 report_id; u8 padding[2]; @@ -270,28 +245,6 @@ struct claw_command_report { u8 data[59]; } __packed; -struct rgb_zone { - u8 red; - u8 green; - u8 blue; -}; - -struct rgb_frame { - struct rgb_zone zone[CLAW_RGB_ZONES]; -}; - -struct rgb_report { - u8 profile; - __be16 read_addr; - u8 frame_bytes; - u8 padding; - u8 frame_count; - u8 state; /* Always 0x09 */ - u8 speed; - u8 brightness; - struct rgb_frame zone_data; -} __packed; - struct claw_drvdata { /* MCU General Variables */ enum claw_profile_ack_pending profile_pending; @@ -312,16 +265,6 @@ struct claw_drvdata { u8 m2_codes[CLAW_KEYS_MAX]; const u16 *bmap_addr; bool bmap_support; - - /* RGB Variables */ - struct rgb_frame rgb_frames[CLAW_RGB_MAX_FRAMES]; - enum claw_rgb_effect_index rgb_effect; - struct led_classdev_mc led_mc; - struct delayed_work rgb_queue; - u8 rgb_frame_count; - bool rgb_enabled; - u8 rgb_speed; - u16 rgb_addr; }; static int get_endpoint_address(struct hid_device *hdev) @@ -353,11 +296,8 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata, static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep) { - struct rgb_report *frame; - u16 rgb_addr, read_addr; - u8 *codes, f_idx; - u16 frame_calc; - int i, ret = 0; + u8 *codes; + int i; switch (drvdata->profile_pending) { case CLAW_M1_PENDING: @@ -368,52 +308,15 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_ for (i = 0; i < CLAW_KEYS_MAX; i++) codes[i] = (cmd_rep->data[6 + i] != 0xff) ? cmd_rep->data[6 + i] : 0x00; break; - case CLAW_RGB_PENDING: - frame = (struct rgb_report *)cmd_rep->data; - rgb_addr = drvdata->rgb_addr; - read_addr = be16_to_cpu(frame->read_addr); - frame_calc = (read_addr - rgb_addr) / CLAW_RGB_FRAME_OFFSET; - if (frame_calc > U8_MAX) { - dev_err(drvdata->led_mc.led_cdev.dev, "Got unsupported frame index: %x\n", - frame_calc); - ret = -EINVAL; - goto err_pending; - } - f_idx = frame_calc; - - if (f_idx >= CLAW_RGB_MAX_FRAMES) { - dev_err(drvdata->led_mc.led_cdev.dev, "Got illegal frame index: %x\n", - f_idx); - ret = -EINVAL; - goto err_pending; - } - - /* Always treat the first frame as the truth for these constants */ - if (f_idx == 0) { - drvdata->rgb_frame_count = frame->frame_count; - /* Invert device speed (20-0) to sysfs speed (0-20) */ - drvdata->rgb_speed = frame->speed; - drvdata->led_mc.led_cdev.brightness = frame->brightness; - drvdata->led_mc.subled_info[0].intensity = frame->zone_data.zone[0].red; - drvdata->led_mc.subled_info[1].intensity = frame->zone_data.zone[0].green; - drvdata->led_mc.subled_info[2].intensity = frame->zone_data.zone[0].blue; - } - - memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data, - sizeof(struct rgb_frame)); - - break; default: dev_warn(&drvdata->hdev->dev, "Got profile event without changes pending from command: %x\n", cmd_rep->cmd); - ret = -EINVAL; + return -EINVAL; } - -err_pending: drvdata->profile_pending = CLAW_NO_PENDING; - return ret; + return 0; } static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report, @@ -856,397 +759,6 @@ static const struct attribute_group claw_gamepad_attr_group = { .is_visible = claw_gamepad_attr_is_visible, }; -/* Read RGB config from device */ -static int claw_read_rgb_config(struct hid_device *hdev) -{ - u8 data[4] = { 0x01, 0x00, 0x00, CLAW_RGB_FRAME_OFFSET }; - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - u16 read_addr = drvdata->rgb_addr; - size_t len = ARRAY_SIZE(data); - int ret, i; - - if (!drvdata->rgb_addr) - return -ENODEV; - - /* Loop through all 8 pages of RGB data */ - guard(mutex)(&drvdata->profile_mutex); - for (i = 0; i < 8; i++) { - drvdata->profile_pending = CLAW_RGB_PENDING; - data[1] = (read_addr >> 8) & 0xff; - data[2] = read_addr & 0x00ff; - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 8); - if (ret) { - drvdata->profile_pending = CLAW_NO_PENDING; - return ret; - } - read_addr += CLAW_RGB_FRAME_OFFSET; - } - - return 0; -} - -/* Send RGB configuration to device */ -static int claw_write_rgb_state(struct claw_drvdata *drvdata) -{ - struct rgb_report report = { 0x01, 0x0000, CLAW_RGB_FRAME_OFFSET, 0x00, - drvdata->rgb_frame_count, 0x09, drvdata->rgb_speed, - drvdata->led_mc.led_cdev.brightness }; - u16 write_addr = drvdata->rgb_addr; - size_t len = sizeof(report); - int f, ret; - - if (!drvdata->rgb_addr) - return -ENODEV; - - if (!drvdata->rgb_frame_count) - return -EINVAL; - - guard(mutex)(&drvdata->rom_mutex); - /* Loop through (up to) 8 pages of RGB data */ - for (f = 0; f < drvdata->rgb_frame_count; f++) { - report.zone_data = drvdata->rgb_frames[f]; - - /* Set the MCU address to write the frame data to */ - report.read_addr = cpu_to_be16(write_addr); - - /* Serialize the rgb_report and write it to MCU */ - ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, - (u8 *)&report, len, 8); - if (ret) - return ret; - - /* Increment the write addr by the offset for the next frame */ - write_addr += CLAW_RGB_FRAME_OFFSET; - } - - ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); - - return ret; -} - -/* Fill all zones with the same color */ -static void claw_frame_fill_solid(struct rgb_frame *frame, struct rgb_zone zone) -{ - int z; - - for (z = 0; z < CLAW_RGB_ZONES; z++) - frame->zone[z] = zone; -} - -/* Apply solid effect (1 frame, all zones same color) */ -static int claw_apply_monocolor(struct claw_drvdata *drvdata) -{ - struct mc_subled *subleds = drvdata->led_mc.subled_info; - struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity, - subleds[2].intensity }; - - drvdata->rgb_frame_count = 1; - claw_frame_fill_solid(&drvdata->rgb_frames[0], zone); - - return claw_write_rgb_state(drvdata); -} - -/* Apply breathe effect (2 frames: color -> off) */ -static int claw_apply_breathe(struct claw_drvdata *drvdata) -{ - struct mc_subled *subleds = drvdata->led_mc.subled_info; - struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity, - subleds[2].intensity }; - static const struct rgb_zone off = { 0, 0, 0 }; - - drvdata->rgb_frame_count = 2; - claw_frame_fill_solid(&drvdata->rgb_frames[0], zone); - claw_frame_fill_solid(&drvdata->rgb_frames[1], off); - - return claw_write_rgb_state(drvdata); -} - -/* Apply chroma effect (6 frames: rainbow cycle, all zones sync) */ -static int claw_apply_chroma(struct claw_drvdata *drvdata) -{ - static const struct rgb_zone colors[] = { - {255, 0, 0}, /* red */ - {255, 255, 0}, /* yellow */ - { 0, 255, 0}, /* green */ - { 0, 255, 255}, /* cyan */ - { 0, 0, 255}, /* blue */ - {255, 0, 255}, /* magenta */ - }; - u8 frame_count = ARRAY_SIZE(colors); - int frame; - - drvdata->rgb_frame_count = frame_count; - - for (frame = 0; frame < frame_count; frame++) - claw_frame_fill_solid(&drvdata->rgb_frames[frame], colors[frame]); - - return claw_write_rgb_state(drvdata); -} - -/* Apply rainbow effect (4 frames: rotating colors around joysticks) */ -static int claw_apply_rainbow(struct claw_drvdata *drvdata) -{ - static const struct rgb_zone colors[] = { - {255, 0, 0}, /* red */ - { 0, 255, 0}, /* green */ - { 0, 255, 255}, /* cyan */ - { 0, 0, 255}, /* blue */ - }; - u8 frame_count = ARRAY_SIZE(colors); - int frame, zone; - - drvdata->rgb_frame_count = frame_count; - - for (frame = 0; frame < frame_count; frame++) { - for (zone = 0; zone < 4; zone++) { - drvdata->rgb_frames[frame].zone[zone] = colors[(zone + frame) % 4]; - drvdata->rgb_frames[frame].zone[zone + 4] = colors[(zone + frame) % 4]; - } - drvdata->rgb_frames[frame].zone[8] = colors[frame]; - } - - return claw_write_rgb_state(drvdata); -} - -/* - * Apply frostfire effect (4 frames: fire vs ice rotating) - * Right joystick: fire red -> dark -> ice blue -> dark (clockwise) - * Left joystick: ice blue -> dark -> fire red -> dark (counter-clockwise) - * ABXY: fire red -> dark -> ice blue -> dark - */ -static int claw_apply_frostfire(struct claw_drvdata *drvdata) -{ - static const struct rgb_zone colors[] = { - {255, 0, 0}, /* fire red */ - { 0, 0, 0}, /* dark */ - { 0, 0, 255}, /* ice blue */ - { 0, 0, 0}, /* dark */ - }; - u8 frame_count = ARRAY_SIZE(colors); - int frame, zone; - - drvdata->rgb_frame_count = frame_count; - - for (frame = 0; frame < frame_count; frame++) { - for (zone = 0; zone < 4; zone++) { - drvdata->rgb_frames[frame].zone[zone] = colors[(zone + frame) % 4]; - drvdata->rgb_frames[frame].zone[zone + 4] = colors[(zone - frame + 6) % 4]; - } - drvdata->rgb_frames[frame].zone[8] = colors[frame]; - } - - return claw_write_rgb_state(drvdata); -} - -/* Apply current state to device */ -static int claw_apply_rgb_state(struct claw_drvdata *drvdata) -{ - static const struct rgb_zone off = { 0, 0, 0 }; - - if (!drvdata->rgb_enabled) { - drvdata->rgb_frame_count = 1; - claw_frame_fill_solid(&drvdata->rgb_frames[0], off); - return claw_write_rgb_state(drvdata); - } - - switch (drvdata->rgb_effect) { - case CLAW_RGB_EFFECT_MONOCOLOR: - return claw_apply_monocolor(drvdata); - case CLAW_RGB_EFFECT_BREATHE: - return claw_apply_breathe(drvdata); - case CLAW_RGB_EFFECT_CHROMA: - return claw_apply_chroma(drvdata); - case CLAW_RGB_EFFECT_RAINBOW: - return claw_apply_rainbow(drvdata); - case CLAW_RGB_EFFECT_FROSTFIRE: - return claw_apply_frostfire(drvdata); - default: - dev_err(drvdata->led_mc.led_cdev.dev, - "No supported rgb_effect selected\n"); - return -EINVAL; - } -} - -static void claw_rgb_queue_fn(struct work_struct *work) -{ - struct delayed_work *dwork = container_of(work, struct delayed_work, work); - struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, rgb_queue); - int ret; - - ret = claw_apply_rgb_state(drvdata); - if (ret) - dev_err(drvdata->led_mc.led_cdev.dev, - "Failed to apply RGB state: %d\n", ret); -} - -static ssize_t effect_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - struct led_classdev *led_cdev = dev_get_drvdata(dev); - struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); - struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); - int ret; - - ret = sysfs_match_string(claw_rgb_effect_text, buf); - if (ret < 0) - return ret; - - drvdata->rgb_effect = ret; - mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); - - return count; -} - -static ssize_t effect_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - struct led_classdev *led_cdev = dev_get_drvdata(dev); - struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); - struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); - - if (drvdata->rgb_effect >= ARRAY_SIZE(claw_rgb_effect_text)) - return -EINVAL; - - return sysfs_emit(buf, "%s\n", claw_rgb_effect_text[drvdata->rgb_effect]); -} - -static DEVICE_ATTR_RW(effect); - -static ssize_t effect_index_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - int i, count = 0; - - for (i = 0; i < ARRAY_SIZE(claw_rgb_effect_text); i++) - count += sysfs_emit_at(buf, count, "%s ", claw_rgb_effect_text[i]); - - if (count) - buf[count - 1] = '\n'; - - return count; -} -static DEVICE_ATTR_RO(effect_index); - -static ssize_t enabled_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - struct led_classdev *led_cdev = dev_get_drvdata(dev); - struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); - struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); - bool val; - int ret; - - ret = kstrtobool(buf, &val); - if (ret) - return ret; - - drvdata->rgb_enabled = val; - mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); - - return count; -} - -static ssize_t enabled_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - struct led_classdev *led_cdev = dev_get_drvdata(dev); - struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); - struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); - - return sysfs_emit(buf, "%s\n", drvdata->rgb_enabled ? "true" : "false"); -} -static DEVICE_ATTR_RW(enabled); - -static ssize_t enabled_index_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - return sysfs_emit(buf, "true false\n"); -} -static DEVICE_ATTR_RO(enabled_index); - -static ssize_t speed_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) -{ - struct led_classdev *led_cdev = dev_get_drvdata(dev); - struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); - struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); - unsigned int val, speed; - int ret; - - ret = kstrtouint(buf, 10, &val); - if (ret) - return ret; - - if (val > 20) - return -EINVAL; - - /* 0 is fastest, invert value for intuitive userspace speed */ - speed = 20 - val; - - drvdata->rgb_speed = speed; - mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); - - return count; -} - -static ssize_t speed_show(struct device *dev, struct device_attribute *attr, - char *buf) -{ - struct hid_device *hdev = to_hid_device(dev); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - u8 speed = 20 - drvdata->rgb_speed; - - return sysfs_emit(buf, "%u\n", speed); -} -static DEVICE_ATTR_RW(speed); - -static ssize_t speed_range_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - return sysfs_emit(buf, "0-20\n"); -} -static DEVICE_ATTR_RO(speed_range); - -static void claw_led_brightness_set(struct led_classdev *led_cdev, - enum led_brightness _brightness) -{ - struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); - struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); - - mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); -} - -static struct attribute *claw_rgb_attrs[] = { - &dev_attr_effect.attr, - &dev_attr_effect_index.attr, - &dev_attr_enabled.attr, - &dev_attr_enabled_index.attr, - &dev_attr_speed.attr, - &dev_attr_speed_range.attr, - NULL, -}; - -static const struct attribute_group rgb_attr_group = { - .attrs = claw_rgb_attrs, -}; - -static struct mc_subled claw_rgb_subled_info[] = { - { - .color_index = LED_COLOR_ID_RED, - .channel = 0x1, - }, - { - .color_index = LED_COLOR_ID_GREEN, - .channel = 0x2, - }, - { - .color_index = LED_COLOR_ID_BLUE, - .channel = 0x3, - }, -}; - static void cfg_setup_fn(struct work_struct *work) { struct delayed_work *dwork = container_of(work, struct delayed_work, work); @@ -1260,13 +772,6 @@ static void cfg_setup_fn(struct work_struct *work) return; } - ret = claw_read_rgb_config(drvdata->hdev); - if (ret) { - dev_err(drvdata->led_mc.led_cdev.dev, - "Failed to setup device, can't read RGB config: %d\n", ret); - return; - } - /* Add sysfs attributes after we get the device state */ ret = devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group); if (ret) { @@ -1275,15 +780,7 @@ static void cfg_setup_fn(struct work_struct *work) return; } - ret = devm_device_add_group(drvdata->led_mc.led_cdev.dev, &rgb_attr_group); - if (ret) { - dev_err(&drvdata->hdev->dev, - "Failed to setup device, can't create led attributes: %d\n", ret); - return; - } - kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE); - kobject_uevent(&drvdata->led_mc.led_cdev.dev->kobj, KOBJ_CHANGE); } static void cfg_resume_fn(struct work_struct *work) @@ -1293,10 +790,6 @@ static void cfg_resume_fn(struct work_struct *work) u8 data[2] = { drvdata->gamepad_mode, drvdata->mkeys_function }; int ret; - ret = claw_read_rgb_config(drvdata->hdev); - if (ret) - dev_err(drvdata->led_mc.led_cdev.dev, "Failed to read RGB config: %d\n", ret); - ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0); if (ret) @@ -1310,24 +803,18 @@ static void claw_features_supported(struct claw_drvdata *drvdata) if (major == 0x01) { drvdata->bmap_support = true; - if (minor >= 0x66) { + if (minor >= 0x66) drvdata->bmap_addr = button_mapping_addr_new; - drvdata->rgb_addr = rgb_addr_new; - } else { + else drvdata->bmap_addr = button_mapping_addr_old; - drvdata->rgb_addr = rgb_addr_old; - } return; } if ((major == 0x02 && minor >= 0x17) || major >= 0x03) { drvdata->bmap_support = true; drvdata->bmap_addr = button_mapping_addr_new; - drvdata->rgb_addr = rgb_addr_new; return; } - - drvdata->rgb_addr = rgb_addr_old; } static int claw_probe(struct hid_device *hdev, u8 ep) @@ -1357,26 +844,6 @@ static int claw_probe(struct hid_device *hdev, u8 ep) if (!drvdata->bmap_support) dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n"); - /* Initialize RGB LED */ - INIT_DELAYED_WORK(&drvdata->rgb_queue, &claw_rgb_queue_fn); - - drvdata->led_mc.led_cdev.name = "msi_claw:rgb:joystick_rings"; - drvdata->led_mc.led_cdev.brightness = 0x50; - drvdata->led_mc.led_cdev.max_brightness = 0x64; - drvdata->led_mc.led_cdev.color = LED_COLOR_ID_RGB; - drvdata->led_mc.led_cdev.brightness_set = claw_led_brightness_set; - drvdata->led_mc.num_colors = 3; - drvdata->led_mc.subled_info = devm_kmemdup(&hdev->dev, claw_rgb_subled_info, - sizeof(claw_rgb_subled_info), GFP_KERNEL); - if (!drvdata->led_mc.subled_info) - return -ENOMEM; - - drvdata->rgb_enabled = true; - - ret = devm_led_classdev_multicolor_register(&hdev->dev, &drvdata->led_mc); - if (ret) - return ret; - /* For control interface: open the HID transport for sending commands. */ ret = hid_hw_open(hdev); if (ret) @@ -1438,9 +905,6 @@ static void claw_remove(struct hid_device *hdev) return; } - /* Block writes to brightness/multi_intensity during teardown */ - drvdata->led_mc.led_cdev.brightness_set = NULL; - cancel_delayed_work_sync(&drvdata->rgb_queue); cancel_delayed_work_sync(&drvdata->cfg_setup); cancel_delayed_work_sync(&drvdata->cfg_resume); hid_hw_close(hdev); From 2510d2c37a9e478aff87a35c7cceae2dbed357c5 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Fri, 15 May 2026 22:24:07 -0700 Subject: [PATCH 50/73] Revert "[FROM-ML] HID: hid-msi: Add M-key mapping attributes" This reverts commit b3fcf4866457bc838d2e3309e86189a8b000f2ed. --- drivers/hid/hid-msi.c | 398 +----------------------------------------- 1 file changed, 1 insertion(+), 397 deletions(-) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index 13ba2747fdb66..8915942af15e6 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -40,8 +40,6 @@ #define CLAW_DINPUT_CFG_INTF_IN 0x82 #define CLAW_XINPUT_CFG_INTF_IN 0x83 -#define CLAW_KEYS_MAX 5 - enum claw_command_index { CLAW_COMMAND_TYPE_READ_PROFILE = 0x04, CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05, @@ -66,17 +64,6 @@ static const char * const claw_gamepad_mode_text[] = { [CLAW_GAMEPAD_MODE_DESKTOP] = "desktop", }; -enum claw_profile_ack_pending { - CLAW_NO_PENDING, - CLAW_M1_PENDING, - CLAW_M2_PENDING, -}; - -enum claw_key_index { - CLAW_KEY_M1, - CLAW_KEY_M2, -}; - enum claw_mkeys_function_index { CLAW_MKEY_FUNCTION_MACRO, CLAW_MKEY_FUNCTION_COMBO, @@ -89,154 +76,6 @@ static const char * const claw_mkeys_function_text[] = { [CLAW_MKEY_FUNCTION_DISABLED] = "disabled", }; -static const struct { - u8 code; - const char *name; -} claw_button_mapping_key_map[] = { - /* Gamepad buttons */ - { 0x01, "ABS_HAT0Y_UP" }, - { 0x02, "ABS_HAT0Y_DOWN" }, - { 0x03, "ABS_HAT0X_LEFT" }, - { 0x04, "ABS_HAT0X_RIGHT" }, - { 0x05, "BTN_TL" }, - { 0x06, "BTN_TR" }, - { 0x07, "BTN_THUMBL" }, - { 0x08, "BTN_THUMBR" }, - { 0x09, "BTN_SOUTH" }, - { 0x0a, "BTN_EAST" }, - { 0x0b, "BTN_NORTH" }, - { 0x0c, "BTN_WEST" }, - { 0x0d, "BTN_MODE" }, - { 0x0e, "BTN_SELECT" }, - { 0x0f, "BTN_START" }, - { 0x13, "BTN_TL2"}, - { 0x14, "BTN_TR2"}, - { 0x15, "ABS_Y_UP"}, - { 0x16, "ABS_Y_DOWN"}, - { 0x17, "ABS_X_LEFT"}, - { 0x18, "ABS_X_LEFT_RIGHT"}, - { 0x19, "ABS_RY_UP"}, - { 0x1a, "ABS_RY_DOWN"}, - { 0x1b, "ABS_RX_LEFT"}, - { 0x1c, "ABS_RX_RIGHT"}, - /* Keyboard keys */ - { 0x32, "KEY_ESC" }, - { 0x33, "KEY_F1" }, - { 0x34, "KEY_F2" }, - { 0x35, "KEY_F3" }, - { 0x36, "KEY_F4" }, - { 0x37, "KEY_F5" }, - { 0x38, "KEY_F6" }, - { 0x39, "KEY_F7" }, - { 0x3a, "KEY_F8" }, - { 0x3b, "KEY_F9" }, - { 0x3c, "KEY_F10" }, - { 0x3d, "KEY_F11" }, - { 0x3e, "KEY_F12" }, - { 0x3f, "KEY_GRAVE" }, - { 0x40, "KEY_1" }, - { 0x41, "KEY_2" }, - { 0x42, "KEY_3" }, - { 0x43, "KEY_4" }, - { 0x44, "KEY_5" }, - { 0x45, "KEY_6" }, - { 0x46, "KEY_7" }, - { 0x47, "KEY_8" }, - { 0x48, "KEY_9" }, - { 0x49, "KEY_0" }, - { 0x4a, "KEY_MINUS" }, - { 0x4b, "KEY_EQUAL" }, - { 0x4c, "KEY_BACKSPACE" }, - { 0x4d, "KEY_TAB" }, - { 0x4e, "KEY_Q" }, - { 0x4f, "KEY_W" }, - { 0x50, "KEY_E" }, - { 0x51, "KEY_R" }, - { 0x52, "KEY_T" }, - { 0x53, "KEY_Y" }, - { 0x54, "KEY_U" }, - { 0x55, "KEY_I" }, - { 0x56, "KEY_O" }, - { 0x57, "KEY_P" }, - { 0x58, "KEY_LEFTBRACE" }, - { 0x59, "KEY_RIGHTBRACE" }, - { 0x5a, "KEY_BACKSLASH" }, - { 0x5b, "KEY_CAPSLOCK" }, - { 0x5c, "KEY_A" }, - { 0x5d, "KEY_S" }, - { 0x5e, "KEY_D" }, - { 0x5f, "KEY_F" }, - { 0x60, "KEY_G" }, - { 0x61, "KEY_H" }, - { 0x62, "KEY_J" }, - { 0x63, "KEY_K" }, - { 0x64, "KEY_L" }, - { 0x65, "KEY_SEMICOLON" }, - { 0x66, "KEY_APOSTROPHE" }, - { 0x67, "KEY_ENTER" }, - { 0x68, "KEY_LEFTSHIFT" }, - { 0x69, "KEY_Z" }, - { 0x6a, "KEY_X" }, - { 0x6b, "KEY_C" }, - { 0x6c, "KEY_V" }, - { 0x6d, "KEY_B" }, - { 0x6e, "KEY_N" }, - { 0x6f, "KEY_M" }, - { 0x70, "KEY_COMMA" }, - { 0x71, "KEY_DOT" }, - { 0x72, "KEY_SLASH" }, - { 0x73, "KEY_RIGHTSHIFT" }, - { 0x74, "KEY_LEFTCTRL" }, - { 0x75, "KEY_LEFTMETA" }, - { 0x76, "KEY_LEFTALT" }, - { 0x77, "KEY_SPACE" }, - { 0x78, "KEY_RIGHTALT" }, - { 0x79, "KEY_RIGHTCTRL" }, - { 0x7a, "KEY_INSERT" }, - { 0x7b, "KEY_HOME" }, - { 0x7c, "KEY_PAGEUP" }, - { 0x7d, "KEY_DELETE" }, - { 0x7e, "KEY_END" }, - { 0x7f, "KEY_PAGEDOWN" }, - { 0x8a, "KEY_KPENTER" }, - { 0x8b, "KEY_KP0" }, - { 0x8c, "KEY_KP1" }, - { 0x8d, "KEY_KP2" }, - { 0x8e, "KEY_KP3" }, - { 0x8f, "KEY_KP4" }, - { 0x90, "KEY_KP5" }, - { 0x91, "KEY_KP6" }, - { 0x92, "KEY_KP7" }, - { 0x93, "KEY_KP8" }, - { 0x94, "KEY_KP9" }, - { 0x95, "MD_PLAY" }, - { 0x96, "MD_STOP" }, - { 0x97, "MD_NEXT" }, - { 0x98, "MD_PREV" }, - { 0x99, "MD_VOL_UP" }, - { 0x9a, "MD_VOL_DOWN" }, - { 0x9b, "MD_VOL_MUTE" }, - { 0x9c, "KEY_F23" }, - /* Mouse events */ - { 0xc8, "BTN_LEFT" }, - { 0xc9, "BTN_MIDDLE" }, - { 0xca, "BTN_RIGHT" }, - { 0xcb, "BTN_SIDE" }, - { 0xcc, "BTN_EXTRA" }, - { 0xcd, "REL_WHEEL_UP" }, - { 0xce, "REL_WHEEL_DOWN" }, -}; - -static const u16 button_mapping_addr_old[] = { - 0x007a, /* M1 */ - 0x011f, /* M2 */ -}; - -static const u16 button_mapping_addr_new[] = { - 0x00bb, /* M1 */ - 0x0164, /* M2 */ -}; - struct claw_command_report { u8 report_id; u8 padding[2]; @@ -247,24 +86,16 @@ struct claw_command_report { struct claw_drvdata { /* MCU General Variables */ - enum claw_profile_ack_pending profile_pending; struct completion send_cmd_complete; struct delayed_work cfg_resume; struct delayed_work cfg_setup; - struct mutex profile_mutex; /* mutex for profile_pending calls */ struct hid_device *hdev; struct mutex cfg_mutex; /* mutex for synchronous data */ - struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */ - u16 bcd_device; u8 ep; /* Gamepad Variables */ enum claw_mkeys_function_index mkeys_function; enum claw_gamepad_mode_index gamepad_mode; - u8 m1_codes[CLAW_KEYS_MAX]; - u8 m2_codes[CLAW_KEYS_MAX]; - const u16 *bmap_addr; - bool bmap_support; }; static int get_endpoint_address(struct hid_device *hdev) @@ -294,31 +125,6 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata, return 0; } -static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep) -{ - u8 *codes; - int i; - - switch (drvdata->profile_pending) { - case CLAW_M1_PENDING: - case CLAW_M2_PENDING: - codes = (drvdata->profile_pending == CLAW_M1_PENDING) ? - drvdata->m1_codes : drvdata->m2_codes; - /* Extract key codes; replace disabled (0xff) with 0x00, which is (null) in _show */ - for (i = 0; i < CLAW_KEYS_MAX; i++) - codes[i] = (cmd_rep->data[6 + i] != 0xff) ? cmd_rep->data[6 + i] : 0x00; - break; - default: - dev_warn(&drvdata->hdev->dev, - "Got profile event without changes pending from command: %x\n", - cmd_rep->cmd); - return -EINVAL; - } - drvdata->profile_pending = CLAW_NO_PENDING; - - return 0; -} - static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report, u8 *data, int size) { @@ -340,9 +146,6 @@ static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *repor case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK: ret = claw_gamepad_mode_event(drvdata, cmd_rep); break; - case CLAW_COMMAND_TYPE_READ_PROFILE_ACK: - ret = claw_profile_event(drvdata, cmd_rep); - break; case CLAW_COMMAND_TYPE_ACK: break; default: @@ -563,161 +366,6 @@ static ssize_t reset_store(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_WO(reset); -static int button_mapping_name_to_code(const char *name) -{ - int i; - - for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) { - if (!strcmp(name, claw_button_mapping_key_map[i].name)) - return claw_button_mapping_key_map[i].code; - } - - return -EINVAL; -} - -static const char *button_mapping_code_to_name(u8 code) -{ - int i; - - for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) { - if (claw_button_mapping_key_map[i].code == code) - return claw_button_mapping_key_map[i].name; - } - - return NULL; -} - -static int claw_buttons_store(struct device *dev, const char *buf, u8 mkey_idx) -{ - struct hid_device *hdev = to_hid_device(dev); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - u8 data[] = { 0x01, (drvdata->bmap_addr[mkey_idx] >> 8) & 0xff, - drvdata->bmap_addr[mkey_idx] & 0xff, 0x07, - 0x04, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff }; - size_t len = ARRAY_SIZE(data); - int ret, key_count, i; - char **raw_keys; - - raw_keys = argv_split(GFP_KERNEL, buf, &key_count); - if (!raw_keys) - return -ENOMEM; - - guard(mutex)(&drvdata->rom_mutex); /* all err_free paths must be in scope */ - if (key_count > CLAW_KEYS_MAX) { - ret = -EINVAL; - goto err_free; - } - - if (key_count == 0) - goto set_buttons; - - for (i = 0; i < key_count; i++) { - ret = button_mapping_name_to_code(raw_keys[i]); - if (ret < 0) - goto err_free; - - data[6 + i] = ret; - } - -set_buttons: - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, data, len, 8); - if (ret < 0) - goto err_free; - - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); - -err_free: - argv_free(raw_keys); - return ret; -} - -static int claw_buttons_show(struct device *dev, char *buf, enum claw_key_index m_key) -{ - struct hid_device *hdev = to_hid_device(dev); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - u8 data[] = { 0x01, (drvdata->bmap_addr[m_key] >> 8) & 0xff, - drvdata->bmap_addr[m_key] & 0xff, 0x07 }; - size_t len = ARRAY_SIZE(data); - int i, ret, count = 0; - const char *name; - u8 *codes; - - codes = (m_key == CLAW_KEY_M1) ? drvdata->m1_codes : drvdata->m2_codes; - - guard(mutex)(&drvdata->profile_mutex); - drvdata->profile_pending = (m_key == CLAW_KEY_M1) ? CLAW_M1_PENDING : CLAW_M2_PENDING; - - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 8); - if (ret) { - drvdata->profile_pending = CLAW_NO_PENDING; - return ret; - } - for (i = 0; i < CLAW_KEYS_MAX; i++) { - name = button_mapping_code_to_name(codes[i]); - if (name) - count += sysfs_emit_at(buf, count, "%s ", name); - } - - if (!count) - return sysfs_emit(buf, "(not set)\n"); - - buf[count - 1] = '\n'; - - return count; -} - -static ssize_t button_m1_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) -{ - int ret; - - ret = claw_buttons_store(dev, buf, CLAW_KEY_M1); - if (ret) - return ret; - - return count; -} - -static ssize_t button_m1_show(struct device *dev, struct device_attribute *attr, - char *buf) -{ - return claw_buttons_show(dev, buf, CLAW_KEY_M1); -} -static DEVICE_ATTR_RW(button_m1); - -static ssize_t button_m2_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) -{ - int ret; - - ret = claw_buttons_store(dev, buf, CLAW_KEY_M2); - if (ret) - return ret; - - return count; -} - -static ssize_t button_m2_show(struct device *dev, struct device_attribute *attr, - char *buf) -{ - return claw_buttons_show(dev, buf, CLAW_KEY_M2); -} -static DEVICE_ATTR_RW(button_m2); - -static ssize_t button_mapping_options_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - int i, count = 0; - - for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) - count += sysfs_emit_at(buf, count, "%s ", claw_button_mapping_key_map[i].name); - - buf[count - 1] = '\n'; - - return count; -} -static DEVICE_ATTR_RO(button_mapping_options); - static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n) { @@ -730,22 +378,10 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu return 0; } - /* Always show attrs available on all firmware */ - if (attr == &dev_attr_gamepad_mode.attr || - attr == &dev_attr_gamepad_mode_index.attr || - attr == &dev_attr_mkeys_function.attr || - attr == &dev_attr_mkeys_function_index.attr || - attr == &dev_attr_reset.attr) - return attr->mode; - - /* Hide button mapping attrs if it isn't supported */ - return drvdata->bmap_support ? attr->mode : 0; + return attr->mode; } static struct attribute *claw_gamepad_attrs[] = { - &dev_attr_button_m1.attr, - &dev_attr_button_m2.attr, - &dev_attr_button_mapping_options.attr, &dev_attr_gamepad_mode.attr, &dev_attr_gamepad_mode_index.attr, &dev_attr_mkeys_function.attr, @@ -796,31 +432,8 @@ static void cfg_resume_fn(struct work_struct *work) dev_err(&drvdata->hdev->dev, "Failed to set gamepad mode settings: %d\n", ret); } -static void claw_features_supported(struct claw_drvdata *drvdata) -{ - u8 major = (drvdata->bcd_device >> 8) & 0xff; - u8 minor = drvdata->bcd_device & 0xff; - - if (major == 0x01) { - drvdata->bmap_support = true; - if (minor >= 0x66) - drvdata->bmap_addr = button_mapping_addr_new; - else - drvdata->bmap_addr = button_mapping_addr_old; - return; - } - - if ((major == 0x02 && minor >= 0x17) || major >= 0x03) { - drvdata->bmap_support = true; - drvdata->bmap_addr = button_mapping_addr_new; - return; - } -} - static int claw_probe(struct hid_device *hdev, u8 ep) { - struct usb_interface *intf = to_usb_interface(hdev->dev.parent); - struct usb_device *udev = interface_to_usbdev(intf); struct claw_drvdata *drvdata; int ret; @@ -833,17 +446,8 @@ static int claw_probe(struct hid_device *hdev, u8 ep) drvdata->ep = ep; mutex_init(&drvdata->cfg_mutex); - mutex_init(&drvdata->profile_mutex); - mutex_init(&drvdata->rom_mutex); init_completion(&drvdata->send_cmd_complete); - /* Determine feature level from firmware version */ - drvdata->bcd_device = le16_to_cpu(udev->descriptor.bcdDevice); - claw_features_supported(drvdata); - - if (!drvdata->bmap_support) - dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n"); - /* For control interface: open the HID transport for sending commands. */ ret = hid_hw_open(hdev); if (ret) From 3d7ea84926dbab91310772052b1d4aa90b4ae58b Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Fri, 15 May 2026 22:24:08 -0700 Subject: [PATCH 51/73] Revert "[FROM-ML] HID: hid-msi: Add MSI Claw configuration driver" This reverts commit dc7aa161b6d5190a31e1ef6314c79d89b7b0e564. --- MAINTAINERS | 6 - drivers/hid/Kconfig | 12 - drivers/hid/Makefile | 1 - drivers/hid/hid-ids.h | 6 - drivers/hid/hid-msi.c | 582 ------------------------------------------ 5 files changed, 607 deletions(-) delete mode 100644 drivers/hid/hid-msi.c diff --git a/MAINTAINERS b/MAINTAINERS index 5339fe8f1fc10..e0f73ae6afbed 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -18136,12 +18136,6 @@ S: Odd Fixes F: Documentation/devicetree/bindings/net/ieee802154/mrf24j40.txt F: drivers/net/ieee802154/mrf24j40.c -MSI HID DRIVER -M: Derek J. Clark -L: linux-input@vger.kernel.org -S: Maintained -F: drivers/hid/hid-msi.c - MSI EC DRIVER M: Nikita Kravets L: platform-driver-x86@vger.kernel.org diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index fb380ff6081a3..f60837e4ed525 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -485,18 +485,6 @@ config HID_GT683R Currently the following devices are know to be supported: - MSI GT683R -config HID_MSI - tristate "MSI Claw Gamepad Support" - depends on USB_HID - select LEDS_CLASS - select LEDS_CLASS_MULTICOLOR - help - Support for the MSI Claw RGB and controller configuration - - Say Y here to include configuration interface support for the MSI Claw Line - of Handheld Console Controllers. Say M here to compile this driver as a - module. The module will be called hid-msi. - config HID_KEYTOUCH tristate "Keytouch HID devices" help diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 69a97d77a9889..70c0e02b9383a 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -92,7 +92,6 @@ obj-$(CONFIG_HID_MAYFLASH) += hid-mf.o obj-$(CONFIG_HID_MEGAWORLD_FF) += hid-megaworld.o obj-$(CONFIG_HID_MICROSOFT) += hid-microsoft.o obj-$(CONFIG_HID_MONTEREY) += hid-monterey.o -obj-$(CONFIG_HID_MSI) += hid-msi.o obj-$(CONFIG_HID_MULTITOUCH) += hid-multitouch.o obj-$(CONFIG_HID_NINTENDO) += hid-nintendo.o obj-$(CONFIG_HID_NTI) += hid-nti.o diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index c196d57931995..e3b085f6597d2 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1066,13 +1066,7 @@ #define USB_DEVICE_ID_MOZA_R16_R21_2 0x0010 #define USB_VENDOR_ID_MSI 0x1770 -#define USB_VENDOR_ID_MSI_2 0x0db0 #define USB_DEVICE_ID_MSI_GT683R_LED_PANEL 0xff00 -#define USB_DEVICE_ID_MSI_CLAW_XINPUT 0x1901 -#define USB_DEVICE_ID_MSI_CLAW_DINPUT 0x1902 -#define USB_DEVICE_ID_MSI_CLAW_DESKTOP 0x1903 -#define USB_DEVICE_ID_MSI_CLAW_BIOS 0x1904 - #define USB_VENDOR_ID_NATIONAL_SEMICONDUCTOR 0x0400 #define USB_DEVICE_ID_N_S_HARMONY 0xc359 diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c deleted file mode 100644 index 8915942af15e6..0000000000000 --- a/drivers/hid/hid-msi.c +++ /dev/null @@ -1,582 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * HID driver for MSI Claw Handheld PC gamepads. - * - * Provides configuration support for the MSI Claw series of handheld PC - * gamepads. Multiple iterations of the device firmware has led to some - * quirks for how certain attributes are handled. The original firmware - * did not support remapping of the M1 (right) and M2 (left) rear paddles. - * Additionally, the MCU RAM address for writing configuration data has - * changed twice. Checks are done during probe to enumerate these variances. - * - * Copyright (c) 2026 Zhouwang Huang - * Copyright (c) 2026 Denis Benato - * Copyright (c) 2026 Valve Corporation - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "hid-ids.h" - -#define CLAW_OUTPUT_REPORT_ID 0x0f -#define CLAW_INPUT_REPORT_ID 0x10 - -#define CLAW_PACKET_SIZE 64 - -#define CLAW_DINPUT_CFG_INTF_IN 0x82 -#define CLAW_XINPUT_CFG_INTF_IN 0x83 - -enum claw_command_index { - CLAW_COMMAND_TYPE_READ_PROFILE = 0x04, - CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05, - CLAW_COMMAND_TYPE_ACK = 0x06, - CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA = 0x21, - CLAW_COMMAND_TYPE_SYNC_TO_ROM = 0x22, - CLAW_COMMAND_TYPE_SWITCH_MODE = 0x24, - CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE = 0x26, - CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK = 0x27, - CLAW_COMMAND_TYPE_RESET_DEVICE = 0x28, -}; - -enum claw_gamepad_mode_index { - CLAW_GAMEPAD_MODE_XINPUT = 0x01, - CLAW_GAMEPAD_MODE_DINPUT = 0x02, - CLAW_GAMEPAD_MODE_DESKTOP = 0x04, -}; - -static const char * const claw_gamepad_mode_text[] = { - [CLAW_GAMEPAD_MODE_XINPUT] = "xinput", - [CLAW_GAMEPAD_MODE_DINPUT] = "dinput", - [CLAW_GAMEPAD_MODE_DESKTOP] = "desktop", -}; - -enum claw_mkeys_function_index { - CLAW_MKEY_FUNCTION_MACRO, - CLAW_MKEY_FUNCTION_COMBO, - CLAW_MKEY_FUNCTION_DISABLED, -}; - -static const char * const claw_mkeys_function_text[] = { - [CLAW_MKEY_FUNCTION_MACRO] = "macro", - [CLAW_MKEY_FUNCTION_COMBO] = "combination", - [CLAW_MKEY_FUNCTION_DISABLED] = "disabled", -}; - -struct claw_command_report { - u8 report_id; - u8 padding[2]; - u8 header_tail; - u8 cmd; - u8 data[59]; -} __packed; - -struct claw_drvdata { - /* MCU General Variables */ - struct completion send_cmd_complete; - struct delayed_work cfg_resume; - struct delayed_work cfg_setup; - struct hid_device *hdev; - struct mutex cfg_mutex; /* mutex for synchronous data */ - u8 ep; - - /* Gamepad Variables */ - enum claw_mkeys_function_index mkeys_function; - enum claw_gamepad_mode_index gamepad_mode; -}; - -static int get_endpoint_address(struct hid_device *hdev) -{ - struct usb_host_endpoint *ep; - struct usb_interface *intf; - - intf = to_usb_interface(hdev->dev.parent); - ep = intf->cur_altsetting->endpoint; - if (ep) - return ep->desc.bEndpointAddress; - - return -ENODEV; -} - -static int claw_gamepad_mode_event(struct claw_drvdata *drvdata, - struct claw_command_report *cmd_rep) -{ - if (cmd_rep->data[0] >= ARRAY_SIZE(claw_gamepad_mode_text) || - !claw_gamepad_mode_text[cmd_rep->data[0]] || - cmd_rep->data[1] >= ARRAY_SIZE(claw_mkeys_function_text)) - return -EINVAL; - - drvdata->gamepad_mode = cmd_rep->data[0]; - drvdata->mkeys_function = cmd_rep->data[1]; - - return 0; -} - -static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report, - u8 *data, int size) -{ - struct claw_command_report *cmd_rep; - int ret = 0; - - if (size != CLAW_PACKET_SIZE) - return 0; - - cmd_rep = (struct claw_command_report *)data; - - if (cmd_rep->report_id != CLAW_INPUT_REPORT_ID || cmd_rep->header_tail != 0x3c) - return 0; - - dev_dbg(&drvdata->hdev->dev, "Rx data as raw input report: [%*ph]\n", - CLAW_PACKET_SIZE, data); - - switch (cmd_rep->cmd) { - case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK: - ret = claw_gamepad_mode_event(drvdata, cmd_rep); - break; - case CLAW_COMMAND_TYPE_ACK: - break; - default: - dev_dbg(&drvdata->hdev->dev, "Unknown command: %x\n", cmd_rep->cmd); - return 0; - } - - complete(&drvdata->send_cmd_complete); - - return ret; -} - -static int msi_raw_event(struct hid_device *hdev, struct hid_report *report, - u8 *data, int size) -{ - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - - if (!drvdata || (drvdata->ep != CLAW_XINPUT_CFG_INTF_IN && - drvdata->ep != CLAW_DINPUT_CFG_INTF_IN)) - return 0; - - return claw_raw_event(drvdata, report, data, size); -} - -static int claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data, - size_t len, unsigned int timeout) -{ - unsigned char *dmabuf __free(kfree) = NULL; - u8 header[] = { CLAW_OUTPUT_REPORT_ID, 0, 0, 0x3c, index }; - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - size_t header_size = ARRAY_SIZE(header); - int ret; - - if (header_size + len > CLAW_PACKET_SIZE) - return -EINVAL; - - /* We can't use a devm_alloc reusable buffer without side effects during suspend */ - dmabuf = kzalloc(CLAW_PACKET_SIZE, GFP_KERNEL); - if (!dmabuf) - return -ENOMEM; - - memcpy(dmabuf, header, header_size); - if (data && len) - memcpy(dmabuf + header_size, data, len); - - /* Don't hold a mutex when timeout=0, those commands cause USB disconnect */ - if (timeout) { - guard(mutex)(&drvdata->cfg_mutex); - reinit_completion(&drvdata->send_cmd_complete); - } - - dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n", - CLAW_PACKET_SIZE, dmabuf); - - ret = hid_hw_output_report(hdev, dmabuf, CLAW_PACKET_SIZE); - if (ret < 0) - return ret; - - ret = ret == CLAW_PACKET_SIZE ? 0 : -EIO; - if (ret) - return ret; - - if (timeout) { - ret = wait_for_completion_interruptible_timeout(&drvdata->send_cmd_complete, - msecs_to_jiffies(timeout)); - - dev_dbg(&hdev->dev, "Remaining timeout: %u\n", ret); - if (ret >= 0) /* preserve errors */ - ret = ret == 0 ? -EBUSY : 0; /* timeout occurred : time remained */ - } - - return ret; -} - -static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) -{ - struct hid_device *hdev = to_hid_device(dev); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - u8 data[2] = { 0x00, drvdata->mkeys_function }; - int i, ret = -EINVAL; - - for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) { - if (claw_gamepad_mode_text[i] && sysfs_streq(buf, claw_gamepad_mode_text[i])) { - ret = i; - break; - } - } - if (ret < 0) - return ret; - - data[0] = ret; - - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0); - if (ret) - return ret; - - return count; -} - -static ssize_t gamepad_mode_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - struct hid_device *hdev = to_hid_device(dev); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - int ret, i; - - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8); - if (ret) - return ret; - - i = drvdata->gamepad_mode; - - if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0') - return sysfs_emit(buf, "unsupported\n"); - - return sysfs_emit(buf, "%s\n", claw_gamepad_mode_text[i]); -} -static DEVICE_ATTR_RW(gamepad_mode); - -static ssize_t gamepad_mode_index_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - ssize_t count = 0; - int i; - - for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) { - if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0') - continue; - count += sysfs_emit_at(buf, count, "%s ", claw_gamepad_mode_text[i]); - } - - buf[count - 1] = '\n'; - - return count; -} -static DEVICE_ATTR_RO(gamepad_mode_index); - -static ssize_t mkeys_function_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) -{ - struct hid_device *hdev = to_hid_device(dev); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - u8 data[2] = { drvdata->gamepad_mode, 0x00 }; - int i, ret = -EINVAL; - - for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) { - if (claw_mkeys_function_text[i] && sysfs_streq(buf, claw_mkeys_function_text[i])) { - ret = i; - break; - } - } - if (ret < 0) - return ret; - - data[1] = ret; - - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0); - if (ret) - return ret; - - return count; -} - -static ssize_t mkeys_function_show(struct device *dev, struct device_attribute *attr, - char *buf) -{ - struct hid_device *hdev = to_hid_device(dev); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - int ret, i; - - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8); - if (ret) - return ret; - - i = drvdata->mkeys_function; - - if (i >= ARRAY_SIZE(claw_mkeys_function_text)) - return sysfs_emit(buf, "unsupported\n"); - - return sysfs_emit(buf, "%s\n", claw_mkeys_function_text[i]); -} -static DEVICE_ATTR_RW(mkeys_function); - -static ssize_t mkeys_function_index_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - int i, count = 0; - - for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) - count += sysfs_emit_at(buf, count, "%s ", claw_mkeys_function_text[i]); - - buf[count - 1] = '\n'; - - return count; -} -static DEVICE_ATTR_RO(mkeys_function_index); - -static ssize_t reset_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) -{ - struct hid_device *hdev = to_hid_device(dev); - bool val; - int ret; - - ret = kstrtobool(buf, &val); - if (ret) - return ret; - - if (!val) - return -EINVAL; - - ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_RESET_DEVICE, NULL, 0, 0); - if (ret < 0) - return ret; - - return count; -} -static DEVICE_ATTR_WO(reset); - -static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr, - int n) -{ - struct hid_device *hdev = to_hid_device(kobj_to_dev(kobj)); - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - - if (!drvdata) { - dev_warn(&hdev->dev, - "Failed to get drvdata from kobj. Gamepad attributes are not available.\n"); - return 0; - } - - return attr->mode; -} - -static struct attribute *claw_gamepad_attrs[] = { - &dev_attr_gamepad_mode.attr, - &dev_attr_gamepad_mode_index.attr, - &dev_attr_mkeys_function.attr, - &dev_attr_mkeys_function_index.attr, - &dev_attr_reset.attr, - NULL, -}; - -static const struct attribute_group claw_gamepad_attr_group = { - .attrs = claw_gamepad_attrs, - .is_visible = claw_gamepad_attr_is_visible, -}; - -static void cfg_setup_fn(struct work_struct *work) -{ - struct delayed_work *dwork = container_of(work, struct delayed_work, work); - struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_setup); - int ret; - - ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8); - if (ret) { - dev_err(&drvdata->hdev->dev, - "Failed to setup device, can't read gamepad mode: %d\n", ret); - return; - } - - /* Add sysfs attributes after we get the device state */ - ret = devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group); - if (ret) { - dev_err(&drvdata->hdev->dev, - "Failed to setup device, can't create gamepad attrs: %d\n", ret); - return; - } - - kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE); -} - -static void cfg_resume_fn(struct work_struct *work) -{ - struct delayed_work *dwork = container_of(work, struct delayed_work, work); - struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_resume); - u8 data[2] = { drvdata->gamepad_mode, drvdata->mkeys_function }; - int ret; - - ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, - ARRAY_SIZE(data), 0); - if (ret) - dev_err(&drvdata->hdev->dev, "Failed to set gamepad mode settings: %d\n", ret); -} - -static int claw_probe(struct hid_device *hdev, u8 ep) -{ - struct claw_drvdata *drvdata; - int ret; - - drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL); - if (!drvdata) - return -ENOMEM; - - hid_set_drvdata(hdev, drvdata); - drvdata->hdev = hdev; - drvdata->ep = ep; - - mutex_init(&drvdata->cfg_mutex); - init_completion(&drvdata->send_cmd_complete); - - /* For control interface: open the HID transport for sending commands. */ - ret = hid_hw_open(hdev); - if (ret) - return ret; - - INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn); - INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn); - schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500)); - - return 0; -} - -static int msi_probe(struct hid_device *hdev, const struct hid_device_id *id) -{ - int ret; - u8 ep; - - if (!hid_is_usb(hdev)) { - ret = -ENODEV; - goto err_probe; - } - - ret = hid_parse(hdev); - if (ret) - goto err_probe; - - /* Set quirk to create separate input devices per HID application */ - hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT; - ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); - if (ret) - goto err_probe; - - /* For non-control interfaces (keyboard/mouse), allow userspace to grab the devices. */ - ret = get_endpoint_address(hdev); - if (ret < 0) - goto err_stop_hw; - - ep = ret; - if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) { - ret = claw_probe(hdev, ep); - if (ret) - goto err_stop_hw; - } - - return 0; - -err_stop_hw: - hid_hw_stop(hdev); -err_probe: - return dev_err_probe(&hdev->dev, ret, "Failed to init device\n"); -} - -static void claw_remove(struct hid_device *hdev) -{ - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - - if (!drvdata) { - hid_hw_stop(hdev); - return; - } - - cancel_delayed_work_sync(&drvdata->cfg_setup); - cancel_delayed_work_sync(&drvdata->cfg_resume); - hid_hw_close(hdev); -} - -static void msi_remove(struct hid_device *hdev) -{ - int ret; - u8 ep; - - ret = get_endpoint_address(hdev); - if (ret <= 0) - goto hw_stop; - - ep = ret; - if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) - claw_remove(hdev); - -hw_stop: - hid_hw_stop(hdev); -} - -static int claw_resume(struct hid_device *hdev) -{ - struct claw_drvdata *drvdata = hid_get_drvdata(hdev); - - /* MCU can take up to 500ms to be ready after resume */ - schedule_delayed_work(&drvdata->cfg_resume, msecs_to_jiffies(500)); - return 0; -} - -static int msi_resume(struct hid_device *hdev) -{ - int ret; - u8 ep; - - ret = get_endpoint_address(hdev); - if (ret <= 0) - return 0; - - ep = ret; - if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) - return claw_resume(hdev); - - return 0; -} - -static const struct hid_device_id msi_devices[] = { - { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_XINPUT) }, - { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DINPUT) }, - { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DESKTOP) }, - { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_BIOS) }, - { } -}; -MODULE_DEVICE_TABLE(hid, msi_devices); - -static struct hid_driver msi_driver = { - .name = "hid-msi", - .id_table = msi_devices, - .raw_event = msi_raw_event, - .probe = msi_probe, - .remove = msi_remove, - .resume = msi_resume, -}; -module_hid_driver(msi_driver); - -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("Denis Benato "); -MODULE_AUTHOR("Zhouwang Huang "); -MODULE_AUTHOR("Derek J. Clark "); -MODULE_DESCRIPTION("HID driver for MSI Claw Handheld PC gamepads"); From 23ee498bc4e73233100415213c787eed6800f782 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 16 May 2026 04:28:38 +0000 Subject: [PATCH 52/73] [FROM-ML] HID: hid-msi: Add MSI Claw configuration driver Adds configuration HID driver for the MSI Claw series of handheld PC's. In this initial patch add the initial driver outline and attributes for changing the gamepad mode, M-key behavior, and add a WO reset function. Sending the SWITCH_MODE and RESET commands causes a USB disconnect in the device. The completion will therefore never get hit and would trigger an -EIO. To avoid showing the user an error for every write to these attrs a bypass for the completion handling is introduced when timeout == 0. The initial version of this patch was written by Denis Benato, which contained the initial reverse-engineering and implementation for the gamepad mode switching. This work was later expanded by Zhouwang Huang to include more gamepad modes. Finally, I refactored the drivers data in/out flow and overall format to conform to kernel driver best practices and style guides. Claude was used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Denis Benato Signed-off-by: Denis Benato Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Link: https://lore.kernel.org/r/20260516042841.500299-2-derekjohn.clark@gmail.com Signed-off-by: Derek J. Clark --- MAINTAINERS | 6 + drivers/hid/Kconfig | 12 + drivers/hid/Makefile | 1 + drivers/hid/hid-ids.h | 6 + drivers/hid/hid-msi.c | 625 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 650 insertions(+) create mode 100644 drivers/hid/hid-msi.c diff --git a/MAINTAINERS b/MAINTAINERS index e0f73ae6afbed..5339fe8f1fc10 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -18136,6 +18136,12 @@ S: Odd Fixes F: Documentation/devicetree/bindings/net/ieee802154/mrf24j40.txt F: drivers/net/ieee802154/mrf24j40.c +MSI HID DRIVER +M: Derek J. Clark +L: linux-input@vger.kernel.org +S: Maintained +F: drivers/hid/hid-msi.c + MSI EC DRIVER M: Nikita Kravets L: platform-driver-x86@vger.kernel.org diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index f60837e4ed525..fb380ff6081a3 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -485,6 +485,18 @@ config HID_GT683R Currently the following devices are know to be supported: - MSI GT683R +config HID_MSI + tristate "MSI Claw Gamepad Support" + depends on USB_HID + select LEDS_CLASS + select LEDS_CLASS_MULTICOLOR + help + Support for the MSI Claw RGB and controller configuration + + Say Y here to include configuration interface support for the MSI Claw Line + of Handheld Console Controllers. Say M here to compile this driver as a + module. The module will be called hid-msi. + config HID_KEYTOUCH tristate "Keytouch HID devices" help diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 70c0e02b9383a..69a97d77a9889 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -92,6 +92,7 @@ obj-$(CONFIG_HID_MAYFLASH) += hid-mf.o obj-$(CONFIG_HID_MEGAWORLD_FF) += hid-megaworld.o obj-$(CONFIG_HID_MICROSOFT) += hid-microsoft.o obj-$(CONFIG_HID_MONTEREY) += hid-monterey.o +obj-$(CONFIG_HID_MSI) += hid-msi.o obj-$(CONFIG_HID_MULTITOUCH) += hid-multitouch.o obj-$(CONFIG_HID_NINTENDO) += hid-nintendo.o obj-$(CONFIG_HID_NTI) += hid-nti.o diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index e3b085f6597d2..c196d57931995 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1066,7 +1066,13 @@ #define USB_DEVICE_ID_MOZA_R16_R21_2 0x0010 #define USB_VENDOR_ID_MSI 0x1770 +#define USB_VENDOR_ID_MSI_2 0x0db0 #define USB_DEVICE_ID_MSI_GT683R_LED_PANEL 0xff00 +#define USB_DEVICE_ID_MSI_CLAW_XINPUT 0x1901 +#define USB_DEVICE_ID_MSI_CLAW_DINPUT 0x1902 +#define USB_DEVICE_ID_MSI_CLAW_DESKTOP 0x1903 +#define USB_DEVICE_ID_MSI_CLAW_BIOS 0x1904 + #define USB_VENDOR_ID_NATIONAL_SEMICONDUCTOR 0x0400 #define USB_DEVICE_ID_N_S_HARMONY 0xc359 diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c new file mode 100644 index 0000000000000..1bf1d6150f4fd --- /dev/null +++ b/drivers/hid/hid-msi.c @@ -0,0 +1,625 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for MSI Claw Handheld PC gamepads. + * + * Provides configuration support for the MSI Claw series of handheld PC + * gamepads. Multiple iterations of the device firmware has led to some + * quirks for how certain attributes are handled. The original firmware + * did not support remapping of the M1 (right) and M2 (left) rear paddles. + * Additionally, the MCU RAM address for writing configuration data has + * changed twice. Checks are done during probe to enumerate these variances. + * + * Copyright (c) 2026 Zhouwang Huang + * Copyright (c) 2026 Denis Benato + * Copyright (c) 2026 Valve Corporation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hid-ids.h" + +#define CLAW_OUTPUT_REPORT_ID 0x0f +#define CLAW_INPUT_REPORT_ID 0x10 + +#define CLAW_PACKET_SIZE 64 + +#define CLAW_DINPUT_CFG_INTF_IN 0x82 +#define CLAW_XINPUT_CFG_INTF_IN 0x83 + +enum claw_command_index { + CLAW_COMMAND_TYPE_READ_PROFILE = 0x04, + CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05, + CLAW_COMMAND_TYPE_ACK = 0x06, + CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA = 0x21, + CLAW_COMMAND_TYPE_SYNC_TO_ROM = 0x22, + CLAW_COMMAND_TYPE_SWITCH_MODE = 0x24, + CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE = 0x26, + CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK = 0x27, + CLAW_COMMAND_TYPE_RESET_DEVICE = 0x28, +}; + +enum claw_gamepad_mode_index { + CLAW_GAMEPAD_MODE_XINPUT = 0x01, + CLAW_GAMEPAD_MODE_DINPUT = 0x02, + CLAW_GAMEPAD_MODE_DESKTOP = 0x04, +}; + +static const char * const claw_gamepad_mode_text[] = { + [CLAW_GAMEPAD_MODE_XINPUT] = "xinput", + [CLAW_GAMEPAD_MODE_DINPUT] = "dinput", + [CLAW_GAMEPAD_MODE_DESKTOP] = "desktop", +}; + +enum claw_mkeys_function_index { + CLAW_MKEY_FUNCTION_MACRO, + CLAW_MKEY_FUNCTION_COMBO, + CLAW_MKEY_FUNCTION_DISABLED, +}; + +static const char * const claw_mkeys_function_text[] = { + [CLAW_MKEY_FUNCTION_MACRO] = "macro", + [CLAW_MKEY_FUNCTION_COMBO] = "combination", + [CLAW_MKEY_FUNCTION_DISABLED] = "disabled", +}; + +struct claw_command_report { + u8 report_id; + u8 padding[2]; + u8 header_tail; + u8 cmd; + u8 data[59]; +} __packed; + +struct claw_drvdata { + /* MCU General Variables */ + struct completion send_cmd_complete; + struct delayed_work cfg_resume; + struct delayed_work cfg_setup; + struct hid_device *hdev; + struct mutex mode_mutex; /* mutex for mode calls */ + struct mutex cfg_mutex; /* mutex for synchronous data */ + u8 ep; + + /* Gamepad Variables */ + enum claw_mkeys_function_index mkeys_function; + enum claw_gamepad_mode_index gamepad_mode; +}; + +static int get_endpoint_address(struct hid_device *hdev) +{ + struct usb_host_endpoint *ep; + struct usb_interface *intf; + + intf = to_usb_interface(hdev->dev.parent); + ep = intf->cur_altsetting->endpoint; + if (ep) + return ep->desc.bEndpointAddress; + + return -ENODEV; +} + +static int claw_gamepad_mode_event(struct claw_drvdata *drvdata, + struct claw_command_report *cmd_rep) +{ + if (cmd_rep->data[0] >= ARRAY_SIZE(claw_gamepad_mode_text) || + !claw_gamepad_mode_text[cmd_rep->data[0]] || + cmd_rep->data[1] >= ARRAY_SIZE(claw_mkeys_function_text)) + return -EINVAL; + + drvdata->gamepad_mode = cmd_rep->data[0]; + drvdata->mkeys_function = cmd_rep->data[1]; + + return 0; +} + +static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report, + u8 *data, int size) +{ + struct claw_command_report *cmd_rep; + int ret = 0; + + if (size != CLAW_PACKET_SIZE) + return 0; + + cmd_rep = (struct claw_command_report *)data; + + if (cmd_rep->report_id != CLAW_INPUT_REPORT_ID || cmd_rep->header_tail != 0x3c) + return 0; + + dev_dbg(&drvdata->hdev->dev, "Rx data as raw input report: [%*ph]\n", + CLAW_PACKET_SIZE, data); + + switch (cmd_rep->cmd) { + case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK: + ret = claw_gamepad_mode_event(drvdata, cmd_rep); + break; + case CLAW_COMMAND_TYPE_ACK: + break; + default: + dev_dbg(&drvdata->hdev->dev, "Unknown command: %x\n", cmd_rep->cmd); + return 0; + } + + complete(&drvdata->send_cmd_complete); + + return ret; +} + +static int msi_raw_event(struct hid_device *hdev, struct hid_report *report, + u8 *data, int size) +{ + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + + if (!drvdata || (drvdata->ep != CLAW_XINPUT_CFG_INTF_IN && + drvdata->ep != CLAW_DINPUT_CFG_INTF_IN)) + return 0; + + return claw_raw_event(drvdata, report, data, size); +} + +static int claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data, + size_t len, unsigned int timeout) +{ + unsigned char *dmabuf __free(kfree) = NULL; + u8 header[] = { CLAW_OUTPUT_REPORT_ID, 0, 0, 0x3c, index }; + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + size_t header_size = ARRAY_SIZE(header); + int ret; + + if (header_size + len > CLAW_PACKET_SIZE) + return -EINVAL; + + /* We can't use a devm_alloc reusable buffer without side effects during suspend */ + dmabuf = kzalloc(CLAW_PACKET_SIZE, GFP_KERNEL); + if (!dmabuf) + return -ENOMEM; + + memcpy(dmabuf, header, header_size); + if (data && len) + memcpy(dmabuf + header_size, data, len); + + guard(mutex)(&drvdata->cfg_mutex); + if (timeout) + reinit_completion(&drvdata->send_cmd_complete); + + dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n", + CLAW_PACKET_SIZE, dmabuf); + + ret = hid_hw_output_report(hdev, dmabuf, CLAW_PACKET_SIZE); + if (ret < 0) + return ret; + + ret = ret == CLAW_PACKET_SIZE ? 0 : -EIO; + if (ret) + return ret; + + if (timeout) { + ret = wait_for_completion_interruptible_timeout(&drvdata->send_cmd_complete, + msecs_to_jiffies(timeout)); + + dev_dbg(&hdev->dev, "Remaining timeout: %u\n", ret); + if (ret >= 0) /* preserve errors */ + ret = ret == 0 ? -EBUSY : 0; /* timeout occurred : time remained */ + } + + return ret; +} + +static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + int i, ret = -EINVAL; + u8 data[2]; + + for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) { + if (claw_gamepad_mode_text[i] && sysfs_streq(buf, claw_gamepad_mode_text[i])) { + ret = i; + break; + } + } + if (ret < 0) + return ret; + + guard(mutex)(&drvdata->mode_mutex); + data[0] = ret; + data[1] = drvdata->mkeys_function; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0); + if (ret) + return ret; + + return count; +} + +static ssize_t gamepad_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + int ret, i; + + guard(mutex)(&drvdata->mode_mutex); + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8); + if (ret) + return ret; + + i = drvdata->gamepad_mode; + + if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0') + return sysfs_emit(buf, "unsupported\n"); + + return sysfs_emit(buf, "%s\n", claw_gamepad_mode_text[i]); +} +static DEVICE_ATTR_RW(gamepad_mode); + +static ssize_t gamepad_mode_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t count = 0; + int i; + + for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) { + if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0') + continue; + count += sysfs_emit_at(buf, count, "%s ", claw_gamepad_mode_text[i]); + } + + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(gamepad_mode_index); + +static ssize_t mkeys_function_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + int i, ret = -EINVAL; + u8 data[2]; + + for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) { + if (claw_mkeys_function_text[i] && sysfs_streq(buf, claw_mkeys_function_text[i])) { + ret = i; + break; + } + } + if (ret < 0) + return ret; + + guard(mutex)(&drvdata->mode_mutex); + data[0] = drvdata->gamepad_mode; + data[1] = ret; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0); + if (ret) + return ret; + + return count; +} + +static ssize_t mkeys_function_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + int ret, i; + + guard(mutex)(&drvdata->mode_mutex); + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8); + if (ret) + return ret; + + i = drvdata->mkeys_function; + + if (i >= ARRAY_SIZE(claw_mkeys_function_text)) + return sysfs_emit(buf, "unsupported\n"); + + return sysfs_emit(buf, "%s\n", claw_mkeys_function_text[i]); +} +static DEVICE_ATTR_RW(mkeys_function); + +static ssize_t mkeys_function_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int i, count = 0; + + for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) + count += sysfs_emit_at(buf, count, "%s ", claw_mkeys_function_text[i]); + + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(mkeys_function_index); + +static ssize_t reset_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev = to_hid_device(dev); + bool val; + int ret; + + ret = kstrtobool(buf, &val); + if (ret) + return ret; + + if (!val) + return -EINVAL; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_RESET_DEVICE, NULL, 0, 0); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_WO(reset); + +static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr, + int n) +{ + struct hid_device *hdev = to_hid_device(kobj_to_dev(kobj)); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + + if (!drvdata) { + dev_warn(&hdev->dev, + "Failed to get drvdata from kobj. Gamepad attributes are not available.\n"); + return 0; + } + + return attr->mode; +} + +static struct attribute *claw_gamepad_attrs[] = { + &dev_attr_gamepad_mode.attr, + &dev_attr_gamepad_mode_index.attr, + &dev_attr_mkeys_function.attr, + &dev_attr_mkeys_function_index.attr, + &dev_attr_reset.attr, + NULL, +}; + +static const struct attribute_group claw_gamepad_attr_group = { + .attrs = claw_gamepad_attrs, + .is_visible = claw_gamepad_attr_is_visible, +}; + +static void cfg_setup_fn(struct work_struct *work) +{ + struct delayed_work *dwork = container_of(work, struct delayed_work, work); + struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_setup); + int ret; + + ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to setup device, can't read gamepad mode: %d\n", ret); + return; + } + + /* Add sysfs attributes after we get the device state */ + ret = device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to setup device, can't create gamepad attrs: %d\n", ret); + return; + } + + kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE); +} + +static void cfg_resume_fn(struct work_struct *work) +{ + struct delayed_work *dwork = container_of(work, struct delayed_work, work); + struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_resume); + u8 data[2] = { drvdata->gamepad_mode, drvdata->mkeys_function }; + int ret; + + ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, + ARRAY_SIZE(data), 0); + if (ret) + dev_err(&drvdata->hdev->dev, "Failed to set gamepad mode settings: %d\n", ret); +} + +static int claw_probe(struct hid_device *hdev, u8 ep) +{ + struct claw_drvdata *drvdata; + int ret; + + drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL); + if (!drvdata) + return -ENOMEM; + + drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT; + drvdata->hdev = hdev; + drvdata->ep = ep; + + mutex_init(&drvdata->cfg_mutex); + init_completion(&drvdata->send_cmd_complete); + INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn); + INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn); + + /* For control interface: open the HID transport for sending commands. */ + ret = hid_hw_open(hdev); + if (ret) + return ret; + + hid_set_drvdata(hdev, drvdata); + schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500)); + + return 0; +} + +static int msi_probe(struct hid_device *hdev, const struct hid_device_id *id) +{ + int ret; + u8 ep; + + if (!hid_is_usb(hdev)) { + ret = -ENODEV; + goto err_probe; + } + + ret = hid_parse(hdev); + if (ret) + goto err_probe; + + /* Set quirk to create separate input devices per HID application */ + hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT; + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) + goto err_probe; + + /* For non-control interfaces (keyboard/mouse), allow userspace to grab the devices. */ + ret = get_endpoint_address(hdev); + if (ret < 0) + goto err_stop_hw; + + ep = ret; + if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) { + ret = claw_probe(hdev, ep); + if (ret) + goto err_stop_hw; + } + + return 0; + +err_stop_hw: + hid_hw_stop(hdev); +err_probe: + return dev_err_probe(&hdev->dev, ret, "Failed to init device\n"); +} + +static void claw_remove(struct hid_device *hdev) +{ + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + + if (!drvdata) { + hid_hw_stop(hdev); + return; + } + + cancel_delayed_work_sync(&drvdata->cfg_setup); + cancel_delayed_work_sync(&drvdata->cfg_resume); + + guard(mutex)(&drvdata->cfg_mutex); + device_remove_group(&hdev->dev, &claw_gamepad_attr_group); + hid_hw_close(hdev); +} + +static void msi_remove(struct hid_device *hdev) +{ + int ret; + u8 ep; + + ret = get_endpoint_address(hdev); + if (ret <= 0) + goto hw_stop; + + ep = ret; + if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) + claw_remove(hdev); + +hw_stop: + hid_hw_stop(hdev); +} + +static int claw_resume(struct hid_device *hdev) +{ + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + + if (!drvdata) + return -ENODEV; + + /* MCU can take up to 500ms to be ready after resume */ + schedule_delayed_work(&drvdata->cfg_resume, msecs_to_jiffies(500)); + return 0; +} + +static int msi_resume(struct hid_device *hdev) +{ + int ret; + u8 ep; + + ret = get_endpoint_address(hdev); + if (ret <= 0) + return 0; + + ep = ret; + if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) + return claw_resume(hdev); + + return 0; +} + +static int claw_suspend(struct hid_device *hdev) +{ + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + + if (!drvdata) + return -ENODEV; + + cancel_delayed_work_sync(&drvdata->cfg_setup); + cancel_delayed_work_sync(&drvdata->cfg_resume); + + return 0; +} + +static int msi_suspend(struct hid_device *hdev, pm_message_t msg) +{ + int ret; + u8 ep; + + ret = get_endpoint_address(hdev); + if (ret <= 0) + return 0; + + ep = ret; + if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) + return claw_suspend(hdev); + + return 0; +} + +static const struct hid_device_id msi_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_XINPUT) }, + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DINPUT) }, + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DESKTOP) }, + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_BIOS) }, + { } +}; +MODULE_DEVICE_TABLE(hid, msi_devices); + +static struct hid_driver msi_driver = { + .name = "hid-msi", + .id_table = msi_devices, + .raw_event = msi_raw_event, + .probe = msi_probe, + .remove = msi_remove, + .resume = msi_resume, + .suspend = pm_ptr(msi_suspend), +}; +module_hid_driver(msi_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Denis Benato "); +MODULE_AUTHOR("Zhouwang Huang "); +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("HID driver for MSI Claw Handheld PC gamepads"); From 72d944bfff8ddb2cf174257c6cb59b631bfaac84 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 16 May 2026 04:28:39 +0000 Subject: [PATCH 53/73] [FROM-ML] HID: hid-msi: Add M-key mapping attributes Adds attributes that allow for remapping the M-keys with up to 5 values when in macro mode. There are 2 mappable buttons on the rear of the device, M1 on the right and M2 on the left. When mapped, the events will fire from one of three event devices: gamepad buttons will fire from the device handled by xpad, while keyboard and mouse events will fire from respectively typed evdevs provided by the input core. Names of each mapping have been kept as close to the event that will fire from the evdev as possible, with context added to the ABS_ events on the direction of the movement. Initial reverse-engineering and implementation of this feature was done by Zhouwang Huang. I refactored the overall format to conform to kernel driver best practices and style guides. Claude was used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Link: https://lore.kernel.org/r/20260516042841.500299-3-derekjohn.clark@gmail.com Signed-off-by: Derek J. Clark --- drivers/hid/hid-msi.c | 396 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 395 insertions(+), 1 deletion(-) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index 1bf1d6150f4fd..bc0169c992ae8 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -41,6 +41,8 @@ #define CLAW_DINPUT_CFG_INTF_IN 0x82 #define CLAW_XINPUT_CFG_INTF_IN 0x83 +#define CLAW_KEYS_MAX 5 + enum claw_command_index { CLAW_COMMAND_TYPE_READ_PROFILE = 0x04, CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05, @@ -65,6 +67,17 @@ static const char * const claw_gamepad_mode_text[] = { [CLAW_GAMEPAD_MODE_DESKTOP] = "desktop", }; +enum claw_profile_ack_pending { + CLAW_NO_PENDING, + CLAW_M1_PENDING, + CLAW_M2_PENDING, +}; + +enum claw_key_index { + CLAW_KEY_M1, + CLAW_KEY_M2, +}; + enum claw_mkeys_function_index { CLAW_MKEY_FUNCTION_MACRO, CLAW_MKEY_FUNCTION_COMBO, @@ -77,6 +90,154 @@ static const char * const claw_mkeys_function_text[] = { [CLAW_MKEY_FUNCTION_DISABLED] = "disabled", }; +static const struct { + u8 code; + const char *name; +} claw_button_mapping_key_map[] = { + /* Gamepad buttons */ + { 0x01, "ABS_HAT0Y_UP" }, + { 0x02, "ABS_HAT0Y_DOWN" }, + { 0x03, "ABS_HAT0X_LEFT" }, + { 0x04, "ABS_HAT0X_RIGHT" }, + { 0x05, "BTN_TL" }, + { 0x06, "BTN_TR" }, + { 0x07, "BTN_THUMBL" }, + { 0x08, "BTN_THUMBR" }, + { 0x09, "BTN_SOUTH" }, + { 0x0a, "BTN_EAST" }, + { 0x0b, "BTN_NORTH" }, + { 0x0c, "BTN_WEST" }, + { 0x0d, "BTN_MODE" }, + { 0x0e, "BTN_SELECT" }, + { 0x0f, "BTN_START" }, + { 0x13, "BTN_TL2"}, + { 0x14, "BTN_TR2"}, + { 0x15, "ABS_Y_UP"}, + { 0x16, "ABS_Y_DOWN"}, + { 0x17, "ABS_X_LEFT"}, + { 0x18, "ABS_X_LEFT_RIGHT"}, + { 0x19, "ABS_RY_UP"}, + { 0x1a, "ABS_RY_DOWN"}, + { 0x1b, "ABS_RX_LEFT"}, + { 0x1c, "ABS_RX_RIGHT"}, + /* Keyboard keys */ + { 0x32, "KEY_ESC" }, + { 0x33, "KEY_F1" }, + { 0x34, "KEY_F2" }, + { 0x35, "KEY_F3" }, + { 0x36, "KEY_F4" }, + { 0x37, "KEY_F5" }, + { 0x38, "KEY_F6" }, + { 0x39, "KEY_F7" }, + { 0x3a, "KEY_F8" }, + { 0x3b, "KEY_F9" }, + { 0x3c, "KEY_F10" }, + { 0x3d, "KEY_F11" }, + { 0x3e, "KEY_F12" }, + { 0x3f, "KEY_GRAVE" }, + { 0x40, "KEY_1" }, + { 0x41, "KEY_2" }, + { 0x42, "KEY_3" }, + { 0x43, "KEY_4" }, + { 0x44, "KEY_5" }, + { 0x45, "KEY_6" }, + { 0x46, "KEY_7" }, + { 0x47, "KEY_8" }, + { 0x48, "KEY_9" }, + { 0x49, "KEY_0" }, + { 0x4a, "KEY_MINUS" }, + { 0x4b, "KEY_EQUAL" }, + { 0x4c, "KEY_BACKSPACE" }, + { 0x4d, "KEY_TAB" }, + { 0x4e, "KEY_Q" }, + { 0x4f, "KEY_W" }, + { 0x50, "KEY_E" }, + { 0x51, "KEY_R" }, + { 0x52, "KEY_T" }, + { 0x53, "KEY_Y" }, + { 0x54, "KEY_U" }, + { 0x55, "KEY_I" }, + { 0x56, "KEY_O" }, + { 0x57, "KEY_P" }, + { 0x58, "KEY_LEFTBRACE" }, + { 0x59, "KEY_RIGHTBRACE" }, + { 0x5a, "KEY_BACKSLASH" }, + { 0x5b, "KEY_CAPSLOCK" }, + { 0x5c, "KEY_A" }, + { 0x5d, "KEY_S" }, + { 0x5e, "KEY_D" }, + { 0x5f, "KEY_F" }, + { 0x60, "KEY_G" }, + { 0x61, "KEY_H" }, + { 0x62, "KEY_J" }, + { 0x63, "KEY_K" }, + { 0x64, "KEY_L" }, + { 0x65, "KEY_SEMICOLON" }, + { 0x66, "KEY_APOSTROPHE" }, + { 0x67, "KEY_ENTER" }, + { 0x68, "KEY_LEFTSHIFT" }, + { 0x69, "KEY_Z" }, + { 0x6a, "KEY_X" }, + { 0x6b, "KEY_C" }, + { 0x6c, "KEY_V" }, + { 0x6d, "KEY_B" }, + { 0x6e, "KEY_N" }, + { 0x6f, "KEY_M" }, + { 0x70, "KEY_COMMA" }, + { 0x71, "KEY_DOT" }, + { 0x72, "KEY_SLASH" }, + { 0x73, "KEY_RIGHTSHIFT" }, + { 0x74, "KEY_LEFTCTRL" }, + { 0x75, "KEY_LEFTMETA" }, + { 0x76, "KEY_LEFTALT" }, + { 0x77, "KEY_SPACE" }, + { 0x78, "KEY_RIGHTALT" }, + { 0x79, "KEY_RIGHTCTRL" }, + { 0x7a, "KEY_INSERT" }, + { 0x7b, "KEY_HOME" }, + { 0x7c, "KEY_PAGEUP" }, + { 0x7d, "KEY_DELETE" }, + { 0x7e, "KEY_END" }, + { 0x7f, "KEY_PAGEDOWN" }, + { 0x8a, "KEY_KPENTER" }, + { 0x8b, "KEY_KP0" }, + { 0x8c, "KEY_KP1" }, + { 0x8d, "KEY_KP2" }, + { 0x8e, "KEY_KP3" }, + { 0x8f, "KEY_KP4" }, + { 0x90, "KEY_KP5" }, + { 0x91, "KEY_KP6" }, + { 0x92, "KEY_KP7" }, + { 0x93, "KEY_KP8" }, + { 0x94, "KEY_KP9" }, + { 0x95, "MD_PLAY" }, + { 0x96, "MD_STOP" }, + { 0x97, "MD_NEXT" }, + { 0x98, "MD_PREV" }, + { 0x99, "MD_VOL_UP" }, + { 0x9a, "MD_VOL_DOWN" }, + { 0x9b, "MD_VOL_MUTE" }, + { 0x9c, "KEY_F23" }, + /* Mouse events */ + { 0xc8, "BTN_LEFT" }, + { 0xc9, "BTN_MIDDLE" }, + { 0xca, "BTN_RIGHT" }, + { 0xcb, "BTN_SIDE" }, + { 0xcc, "BTN_EXTRA" }, + { 0xcd, "REL_WHEEL_UP" }, + { 0xce, "REL_WHEEL_DOWN" }, +}; + +static const u16 button_mapping_addr_old[] = { + 0x007a, /* M1 */ + 0x011f, /* M2 */ +}; + +static const u16 button_mapping_addr_new[] = { + 0x00bb, /* M1 */ + 0x0164, /* M2 */ +}; + struct claw_command_report { u8 report_id; u8 padding[2]; @@ -87,17 +248,25 @@ struct claw_command_report { struct claw_drvdata { /* MCU General Variables */ + enum claw_profile_ack_pending profile_pending; struct completion send_cmd_complete; struct delayed_work cfg_resume; struct delayed_work cfg_setup; + struct mutex profile_mutex; /* mutex for profile_pending calls */ struct hid_device *hdev; struct mutex mode_mutex; /* mutex for mode calls */ struct mutex cfg_mutex; /* mutex for synchronous data */ + struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */ + u16 bcd_device; u8 ep; /* Gamepad Variables */ enum claw_mkeys_function_index mkeys_function; enum claw_gamepad_mode_index gamepad_mode; + u8 m1_codes[CLAW_KEYS_MAX]; + u8 m2_codes[CLAW_KEYS_MAX]; + const u16 *bmap_addr; + bool bmap_support; }; static int get_endpoint_address(struct hid_device *hdev) @@ -127,6 +296,30 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata, return 0; } +static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep) +{ + u8 *codes; + int i; + + switch (drvdata->profile_pending) { + case CLAW_M1_PENDING: + case CLAW_M2_PENDING: + codes = (drvdata->profile_pending == CLAW_M1_PENDING) ? + drvdata->m1_codes : drvdata->m2_codes; + for (i = 0; i < CLAW_KEYS_MAX; i++) + codes[i] = (cmd_rep->data[6 + i]); + break; + default: + dev_dbg(&drvdata->hdev->dev, + "Got profile event without changes pending from command: %x\n", + cmd_rep->cmd); + return -EINVAL; + } + drvdata->profile_pending = CLAW_NO_PENDING; + + return 0; +} + static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report, u8 *data, int size) { @@ -148,6 +341,9 @@ static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *repor case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK: ret = claw_gamepad_mode_event(drvdata, cmd_rep); break; + case CLAW_COMMAND_TYPE_READ_PROFILE_ACK: + ret = claw_profile_event(drvdata, cmd_rep); + break; case CLAW_COMMAND_TYPE_ACK: break; default: @@ -372,6 +568,160 @@ static ssize_t reset_store(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_WO(reset); +static int button_mapping_name_to_code(const char *name) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) { + if (!strcmp(name, claw_button_mapping_key_map[i].name)) + return claw_button_mapping_key_map[i].code; + } + + return -EINVAL; +} + +static const char *button_mapping_code_to_name(u8 code) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) { + if (claw_button_mapping_key_map[i].code == code) + return claw_button_mapping_key_map[i].name; + } + + return NULL; +} + +DEFINE_FREE(argv, char **, if (_T) argv_free(_T)) + +static int claw_buttons_store(struct device *dev, const char *buf, u8 mkey_idx) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u8 data[] = { 0x01, (drvdata->bmap_addr[mkey_idx] >> 8) & 0xff, + drvdata->bmap_addr[mkey_idx] & 0xff, 0x07, + 0x04, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff }; + char **raw_keys __free(argv) = NULL; + size_t len = ARRAY_SIZE(data); + int ret, key_count, i; + + raw_keys = argv_split(GFP_KERNEL, buf, &key_count); + if (!raw_keys) + return -ENOMEM; + + if (key_count > CLAW_KEYS_MAX) + return -EINVAL; + + if (key_count == 0) + return 0; + + for (i = 0; i < key_count; i++) { + ret = button_mapping_name_to_code(raw_keys[i]); + if (ret) + return ret; + + data[6 + i] = ret; + } + + scoped_guard(mutex, &drvdata->rom_mutex) { + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + data, len, 8); + if (ret) + return ret; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); + } + + return ret; +} + +static int claw_buttons_show(struct device *dev, char *buf, enum claw_key_index m_key) +{ + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u8 data[] = { 0x01, (drvdata->bmap_addr[m_key] >> 8) & 0xff, + drvdata->bmap_addr[m_key] & 0xff, 0x07 }; + size_t len = ARRAY_SIZE(data); + int i, ret, count = 0; + const char *name; + u8 *codes; + + codes = (m_key == CLAW_KEY_M1) ? drvdata->m1_codes : drvdata->m2_codes; + + guard(mutex)(&drvdata->profile_mutex); + drvdata->profile_pending = (m_key == CLAW_KEY_M1) ? CLAW_M1_PENDING : CLAW_M2_PENDING; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 8); + if (ret) { + drvdata->profile_pending = CLAW_NO_PENDING; + return ret; + } + for (i = 0; i < CLAW_KEYS_MAX; i++) { + name = button_mapping_code_to_name(codes[i]); + if (name) + count += sysfs_emit_at(buf, count, "%s ", name); + } + + if (!count) + return sysfs_emit(buf, "(not set)\n"); + + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t button_m1_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + + ret = claw_buttons_store(dev, buf, CLAW_KEY_M1); + if (ret) + return ret; + + return count; +} + +static ssize_t button_m1_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return claw_buttons_show(dev, buf, CLAW_KEY_M1); +} +static DEVICE_ATTR_RW(button_m1); + +static ssize_t button_m2_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + + ret = claw_buttons_store(dev, buf, CLAW_KEY_M2); + if (ret) + return ret; + + return count; +} + +static ssize_t button_m2_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return claw_buttons_show(dev, buf, CLAW_KEY_M2); +} +static DEVICE_ATTR_RW(button_m2); + +static ssize_t button_mapping_options_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int i, count = 0; + + for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) + count += sysfs_emit_at(buf, count, "%s ", claw_button_mapping_key_map[i].name); + + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(button_mapping_options); + static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n) { @@ -384,10 +734,22 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu return 0; } - return attr->mode; + /* Always show attrs available on all firmware */ + if (attr == &dev_attr_gamepad_mode.attr || + attr == &dev_attr_gamepad_mode_index.attr || + attr == &dev_attr_mkeys_function.attr || + attr == &dev_attr_mkeys_function_index.attr || + attr == &dev_attr_reset.attr) + return attr->mode; + + /* Hide button mapping attrs if it isn't supported */ + return drvdata->bmap_support ? attr->mode : 0; } static struct attribute *claw_gamepad_attrs[] = { + &dev_attr_button_m1.attr, + &dev_attr_button_m2.attr, + &dev_attr_button_mapping_options.attr, &dev_attr_gamepad_mode.attr, &dev_attr_gamepad_mode_index.attr, &dev_attr_mkeys_function.attr, @@ -438,8 +800,31 @@ static void cfg_resume_fn(struct work_struct *work) dev_err(&drvdata->hdev->dev, "Failed to set gamepad mode settings: %d\n", ret); } +static void claw_features_supported(struct claw_drvdata *drvdata) +{ + u8 major = (drvdata->bcd_device >> 8) & 0xff; + u8 minor = drvdata->bcd_device & 0xff; + + if (major == 0x01) { + drvdata->bmap_support = true; + if (minor >= 0x66) + drvdata->bmap_addr = button_mapping_addr_new; + else + drvdata->bmap_addr = button_mapping_addr_old; + return; + } + + if ((major == 0x02 && minor >= 0x17) || major >= 0x03) { + drvdata->bmap_support = true; + drvdata->bmap_addr = button_mapping_addr_new; + return; + } +} + static int claw_probe(struct hid_device *hdev, u8 ep) { + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_device *udev = interface_to_usbdev(intf); struct claw_drvdata *drvdata; int ret; @@ -451,7 +836,16 @@ static int claw_probe(struct hid_device *hdev, u8 ep) drvdata->hdev = hdev; drvdata->ep = ep; + /* Determine feature level from firmware version */ + drvdata->bcd_device = le16_to_cpu(udev->descriptor.bcdDevice); + claw_features_supported(drvdata); + + if (!drvdata->bmap_support) + dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n"); + mutex_init(&drvdata->cfg_mutex); + mutex_init(&drvdata->profile_mutex); + mutex_init(&drvdata->rom_mutex); init_completion(&drvdata->send_cmd_complete); INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn); INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn); From 5b796afd6c02badbd76635e2afd1d9ad9e2d3b97 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 16 May 2026 04:28:40 +0000 Subject: [PATCH 54/73] [FROM-ML] HID: hid-msi: Add RGB control interface Adds RGB control interface for MSI Claw devices. The MSI Claw uses a fairly unique RGB interface. It has 9 total zones (4 per joystick ring and 1 for the ABXY buttons), and supports up to 8 sequential frames of RGB zone data. Each frame is written to a specific area of MCU memory by the profile command, the value of which changes based on the firmware of the device. Unlike other devices (such as the Legion Go or the OneXPlayer devices), there are no hard coded effects built into the MCU. Instead, the basic effects are provided as a series of frame data. I have mirrored the effects available in Windows in this driver, while keeping the effect names consistent with the Lenovo drivers for the effects that are similar. Initial reverse-engineering and implementation of this feature was done by Zhouwang Huang. I refactored the overall format to conform to kernel driver best practices and style guides. Claude was used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Link: https://lore.kernel.org/r/20260516042841.500299-4-derekjohn.clark@gmail.com Signed-off-by: Derek J. Clark --- drivers/hid/hid-msi.c | 549 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 544 insertions(+), 5 deletions(-) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index bc0169c992ae8..8ea8f551b8e43 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -21,10 +21,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include #include @@ -43,6 +46,10 @@ #define CLAW_KEYS_MAX 5 +#define CLAW_RGB_ZONES 9 +#define CLAW_RGB_MAX_FRAMES 8 +#define CLAW_RGB_FRAME_OFFSET 0x24 + enum claw_command_index { CLAW_COMMAND_TYPE_READ_PROFILE = 0x04, CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05, @@ -71,6 +78,7 @@ enum claw_profile_ack_pending { CLAW_NO_PENDING, CLAW_M1_PENDING, CLAW_M2_PENDING, + CLAW_RGB_PENDING, }; enum claw_key_index { @@ -228,6 +236,22 @@ static const struct { { 0xce, "REL_WHEEL_DOWN" }, }; +enum claw_rgb_effect_index { + CLAW_RGB_EFFECT_MONOCOLOR, + CLAW_RGB_EFFECT_BREATHE, + CLAW_RGB_EFFECT_CHROMA, + CLAW_RGB_EFFECT_RAINBOW, + CLAW_RGB_EFFECT_FROSTFIRE, +}; + +static const char * const claw_rgb_effect_text[] = { + [CLAW_RGB_EFFECT_MONOCOLOR] = "monocolor", + [CLAW_RGB_EFFECT_BREATHE] = "breathe", + [CLAW_RGB_EFFECT_CHROMA] = "chroma", + [CLAW_RGB_EFFECT_RAINBOW] = "rainbow", + [CLAW_RGB_EFFECT_FROSTFIRE] = "frostfire", +}; + static const u16 button_mapping_addr_old[] = { 0x007a, /* M1 */ 0x011f, /* M2 */ @@ -238,6 +262,9 @@ static const u16 button_mapping_addr_new[] = { 0x0164, /* M2 */ }; +static const u16 rgb_addr_old = 0x01fa; +static const u16 rgb_addr_new = 0x024a; + struct claw_command_report { u8 report_id; u8 padding[2]; @@ -246,6 +273,28 @@ struct claw_command_report { u8 data[59]; } __packed; +struct rgb_zone { + u8 red; + u8 green; + u8 blue; +}; + +struct rgb_frame { + struct rgb_zone zone[CLAW_RGB_ZONES]; +}; + +struct rgb_report { + u8 profile; + __be16 read_addr; + u8 frame_bytes; + u8 padding; + u8 frame_count; + u8 state; /* Always 0x09 */ + u8 speed; + u8 brightness; + struct rgb_frame zone_data; +} __packed; + struct claw_drvdata { /* MCU General Variables */ enum claw_profile_ack_pending profile_pending; @@ -257,6 +306,7 @@ struct claw_drvdata { struct mutex mode_mutex; /* mutex for mode calls */ struct mutex cfg_mutex; /* mutex for synchronous data */ struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */ + spinlock_t frame_lock; /* lock for read/write rgb_frames */ u16 bcd_device; u8 ep; @@ -267,6 +317,16 @@ struct claw_drvdata { u8 m2_codes[CLAW_KEYS_MAX]; const u16 *bmap_addr; bool bmap_support; + + /* RGB Variables */ + struct rgb_frame rgb_frames[CLAW_RGB_MAX_FRAMES]; + enum claw_rgb_effect_index rgb_effect; + struct led_classdev_mc led_mc; + struct delayed_work rgb_queue; + u8 rgb_frame_count; + bool rgb_enabled; + u8 rgb_speed; + u16 rgb_addr; }; static int get_endpoint_address(struct hid_device *hdev) @@ -298,8 +358,11 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata, static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep) { - u8 *codes; - int i; + struct rgb_report *frame; + u16 rgb_addr, read_addr; + u8 *codes, f_idx; + u16 frame_calc; + int i, ret = 0; switch (drvdata->profile_pending) { case CLAW_M1_PENDING: @@ -309,15 +372,46 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_ for (i = 0; i < CLAW_KEYS_MAX; i++) codes[i] = (cmd_rep->data[6 + i]); break; + case CLAW_RGB_PENDING: + frame = (struct rgb_report *)cmd_rep->data; + rgb_addr = drvdata->rgb_addr; + read_addr = be16_to_cpu(frame->read_addr); + frame_calc = (read_addr - rgb_addr) / CLAW_RGB_FRAME_OFFSET; + if (frame_calc >= CLAW_RGB_MAX_FRAMES) { + dev_err(drvdata->led_mc.led_cdev.dev, "Got unsupported frame index: %x\n", + frame_calc); + ret = -EINVAL; + goto err_pending; + } + f_idx = frame_calc; + + scoped_guard(spinlock, &drvdata->frame_lock) { + memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data, + sizeof(struct rgb_frame)); + + /* Only use frame 0 for remaining variable assignment */ + if (f_idx != 0) + break; + + drvdata->rgb_speed = frame->speed; + drvdata->led_mc.led_cdev.brightness = frame->brightness; + drvdata->led_mc.subled_info[0].intensity = frame->zone_data.zone[0].red; + drvdata->led_mc.subled_info[1].intensity = frame->zone_data.zone[0].green; + drvdata->led_mc.subled_info[2].intensity = frame->zone_data.zone[0].blue; + } + + break; default: dev_dbg(&drvdata->hdev->dev, "Got profile event without changes pending from command: %x\n", cmd_rep->cmd); return -EINVAL; } + +err_pending: drvdata->profile_pending = CLAW_NO_PENDING; - return 0; + return ret; } static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report, @@ -763,6 +857,404 @@ static const struct attribute_group claw_gamepad_attr_group = { .is_visible = claw_gamepad_attr_is_visible, }; +/* Read RGB config from device */ +static int claw_read_rgb_config(struct hid_device *hdev) +{ + u8 data[4] = { 0x01, 0x00, 0x00, CLAW_RGB_FRAME_OFFSET }; + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u16 read_addr = drvdata->rgb_addr; + size_t len = ARRAY_SIZE(data); + int ret, i; + + if (!drvdata->rgb_addr) + return -ENODEV; + + /* Loop through all 8 pages of RGB data */ + guard(mutex)(&drvdata->profile_mutex); + for (i = 0; i < 8; i++) { + drvdata->profile_pending = CLAW_RGB_PENDING; + data[1] = (read_addr >> 8) & 0xff; + data[2] = read_addr & 0x00ff; + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 8); + if (ret) { + drvdata->profile_pending = CLAW_NO_PENDING; + return ret; + } + read_addr += CLAW_RGB_FRAME_OFFSET; + } + + return 0; +} + +/* Send RGB configuration to device */ +static int claw_write_rgb_state(struct claw_drvdata *drvdata) +{ + struct rgb_report report = { 0x01, 0x0000, CLAW_RGB_FRAME_OFFSET, 0x00, + drvdata->rgb_frame_count, 0x09, drvdata->rgb_speed, + drvdata->led_mc.led_cdev.brightness }; + u16 write_addr = drvdata->rgb_addr; + size_t len = sizeof(report); + int f, ret; + + if (!drvdata->rgb_addr) + return -ENODEV; + + if (!drvdata->rgb_frame_count) + return -EINVAL; + + guard(mutex)(&drvdata->rom_mutex); + /* Loop through (up to) 8 pages of RGB data */ + for (f = 0; f < drvdata->rgb_frame_count; f++) { + report.zone_data = drvdata->rgb_frames[f]; + + /* Set the MCU address to write the frame data to */ + report.read_addr = cpu_to_be16(write_addr); + + /* Serialize the rgb_report and write it to MCU */ + ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + (u8 *)&report, len, 8); + if (ret) + return ret; + + /* Increment the write addr by the offset for the next frame */ + write_addr += CLAW_RGB_FRAME_OFFSET; + } + + ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); + + return ret; +} + +/* Fill all zones with the same color */ +static void claw_frame_fill_solid(struct rgb_frame *frame, struct rgb_zone zone) +{ + int z; + + for (z = 0; z < CLAW_RGB_ZONES; z++) + frame->zone[z] = zone; +} + +/* Apply solid effect (1 frame, all zones same color) */ +static int claw_apply_monocolor(struct claw_drvdata *drvdata) +{ + struct mc_subled *subleds = drvdata->led_mc.subled_info; + struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity, + subleds[2].intensity }; + + guard(spinlock)(&drvdata->frame_lock); + drvdata->rgb_frame_count = 1; + claw_frame_fill_solid(&drvdata->rgb_frames[0], zone); + + return claw_write_rgb_state(drvdata); +} + +/* Apply breathe effect (2 frames: color -> off) */ +static int claw_apply_breathe(struct claw_drvdata *drvdata) +{ + struct mc_subled *subleds = drvdata->led_mc.subled_info; + struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity, + subleds[2].intensity }; + static const struct rgb_zone off = { 0, 0, 0 }; + + guard(spinlock)(&drvdata->frame_lock); + drvdata->rgb_frame_count = 2; + claw_frame_fill_solid(&drvdata->rgb_frames[0], zone); + claw_frame_fill_solid(&drvdata->rgb_frames[1], off); + + return claw_write_rgb_state(drvdata); +} + +/* Apply chroma effect (6 frames: rainbow cycle, all zones sync) */ +static int claw_apply_chroma(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone colors[] = { + {255, 0, 0}, /* red */ + {255, 255, 0}, /* yellow */ + { 0, 255, 0}, /* green */ + { 0, 255, 255}, /* cyan */ + { 0, 0, 255}, /* blue */ + {255, 0, 255}, /* magenta */ + }; + u8 frame_count = ARRAY_SIZE(colors); + int frame; + + guard(spinlock)(&drvdata->frame_lock); + drvdata->rgb_frame_count = frame_count; + + for (frame = 0; frame < frame_count; frame++) + claw_frame_fill_solid(&drvdata->rgb_frames[frame], colors[frame]); + + return claw_write_rgb_state(drvdata); +} + +/* Apply rainbow effect (4 frames: rotating colors around joysticks) */ +static int claw_apply_rainbow(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone colors[] = { + {255, 0, 0}, /* red */ + { 0, 255, 0}, /* green */ + { 0, 255, 255}, /* cyan */ + { 0, 0, 255}, /* blue */ + }; + u8 frame_count = ARRAY_SIZE(colors); + int frame, zone; + + guard(spinlock)(&drvdata->frame_lock); + drvdata->rgb_frame_count = frame_count; + + for (frame = 0; frame < frame_count; frame++) { + for (zone = 0; zone < 4; zone++) { + drvdata->rgb_frames[frame].zone[zone] = colors[(zone + frame) % 4]; + drvdata->rgb_frames[frame].zone[zone + 4] = colors[(zone + frame) % 4]; + } + drvdata->rgb_frames[frame].zone[8] = colors[frame]; + } + + return claw_write_rgb_state(drvdata); +} + +/* + * Apply frostfire effect (4 frames: fire vs ice rotating) + * Right joystick: fire red -> dark -> ice blue -> dark (clockwise) + * Left joystick: ice blue -> dark -> fire red -> dark (counter-clockwise) + * ABXY: fire red -> dark -> ice blue -> dark + */ +static int claw_apply_frostfire(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone colors[] = { + {255, 0, 0}, /* fire red */ + { 0, 0, 0}, /* dark */ + { 0, 0, 255}, /* ice blue */ + { 0, 0, 0}, /* dark */ + }; + u8 frame_count = ARRAY_SIZE(colors); + int frame, zone; + + guard(spinlock)(&drvdata->frame_lock); + drvdata->rgb_frame_count = frame_count; + + for (frame = 0; frame < frame_count; frame++) { + for (zone = 0; zone < 4; zone++) { + drvdata->rgb_frames[frame].zone[zone] = colors[(zone + frame) % 4]; + drvdata->rgb_frames[frame].zone[zone + 4] = colors[(zone - frame + 6) % 4]; + } + drvdata->rgb_frames[frame].zone[8] = colors[frame]; + } + + return claw_write_rgb_state(drvdata); +} + +/* Apply current state to device */ +static int claw_apply_rgb_state(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone off = { 0, 0, 0 }; + + if (!drvdata->rgb_enabled) { + guard(spinlock)(&drvdata->frame_lock); + drvdata->rgb_frame_count = 1; + claw_frame_fill_solid(&drvdata->rgb_frames[0], off); + return claw_write_rgb_state(drvdata); + } + + switch (drvdata->rgb_effect) { + case CLAW_RGB_EFFECT_MONOCOLOR: + return claw_apply_monocolor(drvdata); + case CLAW_RGB_EFFECT_BREATHE: + return claw_apply_breathe(drvdata); + case CLAW_RGB_EFFECT_CHROMA: + return claw_apply_chroma(drvdata); + case CLAW_RGB_EFFECT_RAINBOW: + return claw_apply_rainbow(drvdata); + case CLAW_RGB_EFFECT_FROSTFIRE: + return claw_apply_frostfire(drvdata); + default: + dev_err(drvdata->led_mc.led_cdev.dev, + "No supported rgb_effect selected\n"); + return -EINVAL; + } +} + +static void claw_rgb_queue_fn(struct work_struct *work) +{ + struct delayed_work *dwork = container_of(work, struct delayed_work, work); + struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, rgb_queue); + int ret; + + ret = claw_apply_rgb_state(drvdata); + if (ret) + dev_err(drvdata->led_mc.led_cdev.dev, + "Failed to apply RGB state: %d\n", ret); +} + +static ssize_t effect_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + int ret; + + ret = sysfs_match_string(claw_rgb_effect_text, buf); + if (ret < 0) + return ret; + + drvdata->rgb_effect = ret; + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); + + return count; +} + +static ssize_t effect_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + + if (drvdata->rgb_effect >= ARRAY_SIZE(claw_rgb_effect_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", claw_rgb_effect_text[drvdata->rgb_effect]); +} + +static DEVICE_ATTR_RW(effect); + +static ssize_t effect_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int i, count = 0; + + for (i = 0; i < ARRAY_SIZE(claw_rgb_effect_text); i++) + count += sysfs_emit_at(buf, count, "%s ", claw_rgb_effect_text[i]); + + if (count) + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(effect_index); + +static ssize_t enabled_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + bool val; + int ret; + + ret = kstrtobool(buf, &val); + if (ret) + return ret; + + drvdata->rgb_enabled = val; + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); + + return count; +} + +static ssize_t enabled_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + + return sysfs_emit(buf, "%s\n", drvdata->rgb_enabled ? "true" : "false"); +} +static DEVICE_ATTR_RW(enabled); + +static ssize_t enabled_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "true false\n"); +} +static DEVICE_ATTR_RO(enabled_index); + +static ssize_t speed_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + unsigned int val, speed; + int ret; + + ret = kstrtouint(buf, 10, &val); + if (ret) + return ret; + + if (val > 20) + return -EINVAL; + + /* 0 is fastest, invert value for intuitive userspace speed */ + speed = 20 - val; + + drvdata->rgb_speed = speed; + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); + + return count; +} + +static ssize_t speed_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + u8 speed = 20 - drvdata->rgb_speed; + + return sysfs_emit(buf, "%u\n", speed); +} +static DEVICE_ATTR_RW(speed); + +static ssize_t speed_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0-20\n"); +} +static DEVICE_ATTR_RO(speed_range); + +static void claw_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness _brightness) +{ + struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev); + struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc); + + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); +} + +static struct attribute *claw_rgb_attrs[] = { + &dev_attr_effect.attr, + &dev_attr_effect_index.attr, + &dev_attr_enabled.attr, + &dev_attr_enabled_index.attr, + &dev_attr_speed.attr, + &dev_attr_speed_range.attr, + NULL, +}; + +static const struct attribute_group claw_rgb_attr_group = { + .attrs = claw_rgb_attrs, +}; + +static struct mc_subled claw_rgb_subled_info[] = { + { + .color_index = LED_COLOR_ID_RED, + .channel = 0x1, + }, + { + .color_index = LED_COLOR_ID_GREEN, + .channel = 0x2, + }, + { + .color_index = LED_COLOR_ID_BLUE, + .channel = 0x3, + }, +}; + static void cfg_setup_fn(struct work_struct *work) { struct delayed_work *dwork = container_of(work, struct delayed_work, work); @@ -776,6 +1268,13 @@ static void cfg_setup_fn(struct work_struct *work) return; } + ret = claw_read_rgb_config(drvdata->hdev); + if (ret) { + dev_err(drvdata->led_mc.led_cdev.dev, + "Failed to setup device, can't read RGB config: %d\n", ret); + return; + } + /* Add sysfs attributes after we get the device state */ ret = device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group); if (ret) { @@ -784,7 +1283,15 @@ static void cfg_setup_fn(struct work_struct *work) return; } + ret = device_add_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to setup device, can't create led attributes: %d\n", ret); + return; + } + kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE); + kobject_uevent(&drvdata->led_mc.led_cdev.dev->kobj, KOBJ_CHANGE); } static void cfg_resume_fn(struct work_struct *work) @@ -794,6 +1301,10 @@ static void cfg_resume_fn(struct work_struct *work) u8 data[2] = { drvdata->gamepad_mode, drvdata->mkeys_function }; int ret; + ret = claw_read_rgb_config(drvdata->hdev); + if (ret) + dev_err(drvdata->led_mc.led_cdev.dev, "Failed to read RGB config: %d\n", ret); + ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0); if (ret) @@ -807,18 +1318,24 @@ static void claw_features_supported(struct claw_drvdata *drvdata) if (major == 0x01) { drvdata->bmap_support = true; - if (minor >= 0x66) + if (minor >= 0x66) { drvdata->bmap_addr = button_mapping_addr_new; - else + drvdata->rgb_addr = rgb_addr_new; + } else { drvdata->bmap_addr = button_mapping_addr_old; + drvdata->rgb_addr = rgb_addr_old; + } return; } if ((major == 0x02 && minor >= 0x17) || major >= 0x03) { drvdata->bmap_support = true; drvdata->bmap_addr = button_mapping_addr_new; + drvdata->rgb_addr = rgb_addr_new; return; } + + drvdata->rgb_addr = rgb_addr_old; } static int claw_probe(struct hid_device *hdev, u8 ep) @@ -833,6 +1350,7 @@ static int claw_probe(struct hid_device *hdev, u8 ep) return -ENOMEM; drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT; + drvdata->rgb_enabled = true; drvdata->hdev = hdev; drvdata->ep = ep; @@ -843,12 +1361,28 @@ static int claw_probe(struct hid_device *hdev, u8 ep) if (!drvdata->bmap_support) dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n"); + drvdata->led_mc.led_cdev.name = "msi_claw:rgb:joystick_rings"; + drvdata->led_mc.led_cdev.brightness = 0x50; + drvdata->led_mc.led_cdev.max_brightness = 0x64; + drvdata->led_mc.led_cdev.color = LED_COLOR_ID_RGB; + drvdata->led_mc.led_cdev.brightness_set = claw_led_brightness_set; + drvdata->led_mc.num_colors = 3; + drvdata->led_mc.subled_info = devm_kmemdup(&hdev->dev, claw_rgb_subled_info, + sizeof(claw_rgb_subled_info), GFP_KERNEL); + if (!drvdata->led_mc.subled_info) + return -ENOMEM; + mutex_init(&drvdata->cfg_mutex); mutex_init(&drvdata->profile_mutex); mutex_init(&drvdata->rom_mutex); init_completion(&drvdata->send_cmd_complete); INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn); INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn); + INIT_DELAYED_WORK(&drvdata->rgb_queue, &claw_rgb_queue_fn); + + ret = devm_led_classdev_multicolor_register(&hdev->dev, &drvdata->led_mc); + if (ret) + return ret; /* For control interface: open the HID transport for sending commands. */ ret = hid_hw_open(hdev); @@ -910,10 +1444,14 @@ static void claw_remove(struct hid_device *hdev) return; } + /* Block writes to brightness/multi_intensity during teardown */ + drvdata->led_mc.led_cdev.brightness_set = NULL; cancel_delayed_work_sync(&drvdata->cfg_setup); cancel_delayed_work_sync(&drvdata->cfg_resume); + cancel_delayed_work_sync(&drvdata->rgb_queue); guard(mutex)(&drvdata->cfg_mutex); + device_remove_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group); device_remove_group(&hdev->dev, &claw_gamepad_attr_group); hid_hw_close(hdev); } @@ -972,6 +1510,7 @@ static int claw_suspend(struct hid_device *hdev) cancel_delayed_work_sync(&drvdata->cfg_setup); cancel_delayed_work_sync(&drvdata->cfg_resume); + cancel_delayed_work_sync(&drvdata->rgb_queue); return 0; } From 7a40a184d6d22fb9468ad912c848151bb5b0286f Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 16 May 2026 04:28:41 +0000 Subject: [PATCH 55/73] [FROM-ML] HID: hid-msi: Add Rumble Intensity Attributes Adds intensity adjustment for the left and right rumble motors. Claude was used during the reverse-engineering data gathering for this feature done by Zhouwang Huang. As the code had already been affected, I used Claude to create the initial framing for the feature, then did manual cleanup of the _show and _store functions afterwards to fix bugs and keep the coding style consistent. Claude was also used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Link: https://lore.kernel.org/r/20260516042841.500299-5-derekjohn.clark@gmail.com Signed-off-by: Derek J. Clark --- drivers/hid/hid-msi.c | 151 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index 8ea8f551b8e43..d4688caef296a 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -79,6 +79,8 @@ enum claw_profile_ack_pending { CLAW_M1_PENDING, CLAW_M2_PENDING, CLAW_RGB_PENDING, + CLAW_RUMBLE_LEFT_PENDING, + CLAW_RUMBLE_RIGHT_PENDING, }; enum claw_key_index { @@ -234,6 +236,7 @@ static const struct { { 0xcc, "BTN_EXTRA" }, { 0xcd, "REL_WHEEL_UP" }, { 0xce, "REL_WHEEL_DOWN" }, + { 0xff, "DISABLED" }, }; enum claw_rgb_effect_index { @@ -265,6 +268,11 @@ static const u16 button_mapping_addr_new[] = { static const u16 rgb_addr_old = 0x01fa; static const u16 rgb_addr_new = 0x024a; +static const u16 rumble_addr[] = { + 0x0022, /* left */ + 0x0023, /* right */ +}; + struct claw_command_report { u8 report_id; u8 padding[2]; @@ -315,7 +323,10 @@ struct claw_drvdata { enum claw_gamepad_mode_index gamepad_mode; u8 m1_codes[CLAW_KEYS_MAX]; u8 m2_codes[CLAW_KEYS_MAX]; + u8 rumble_intensity_right; + u8 rumble_intensity_left; const u16 *bmap_addr; + bool rumble_support; bool bmap_support; /* RGB Variables */ @@ -400,6 +411,12 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_ drvdata->led_mc.subled_info[2].intensity = frame->zone_data.zone[0].blue; } + break; + case CLAW_RUMBLE_LEFT_PENDING: + drvdata->rumble_intensity_left = cmd_rep->data[4]; + break; + case CLAW_RUMBLE_RIGHT_PENDING: + drvdata->rumble_intensity_right = cmd_rep->data[4]; break; default: dev_dbg(&drvdata->hdev->dev, @@ -678,6 +695,9 @@ static const char *button_mapping_code_to_name(u8 code) { int i; + if (code == 0xff) + return NULL; + for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) { if (claw_button_mapping_key_map[i].code == code) return claw_button_mapping_key_map[i].name; @@ -816,6 +836,126 @@ static ssize_t button_mapping_options_show(struct device *dev, } static DEVICE_ATTR_RO(button_mapping_options); +static ssize_t rumble_intensity_left_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u8 data[] = { 0x01, (rumble_addr[0] >> 8) & 0xff, rumble_addr[0] & 0xff, 0x01, 0x00 }; + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u8 val; + int ret; + + ret = kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val > 100) + return -EINVAL; + + data[4] = val; + + guard(mutex)(&drvdata->rom_mutex); + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + data, ARRAY_SIZE(data), 8); + if (ret) + return ret; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); + if (ret) + return ret; + + drvdata->rumble_intensity_left = val; + + return count; +} + +static ssize_t rumble_intensity_left_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + u8 data[4] = { 0x01, (rumble_addr[0] >> 8) & 0xff, rumble_addr[0] & 0xff, 0x01 }; + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + int ret; + + guard(mutex)(&drvdata->profile_mutex); + drvdata->profile_pending = CLAW_RUMBLE_LEFT_PENDING; + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, + ARRAY_SIZE(data), 8); + if (ret) { + drvdata->profile_pending = CLAW_NO_PENDING; + return ret; + } + + return sysfs_emit(buf, "%u\n", drvdata->rumble_intensity_left); +} +static DEVICE_ATTR_RW(rumble_intensity_left); + +static ssize_t rumble_intensity_right_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u8 data[] = { 0x01, (rumble_addr[1] >> 8) & 0xff, rumble_addr[1] & 0xff, 0x01, 0x00 }; + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + u8 val; + int ret; + + ret = kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val > 100) + return -EINVAL; + + data[4] = val; + + guard(mutex)(&drvdata->rom_mutex); + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + data, ARRAY_SIZE(data), 8); + if (ret) + return ret; + + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8); + if (ret) + return ret; + + drvdata->rumble_intensity_right = val; + + return count; +} + +static ssize_t rumble_intensity_right_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + u8 data[4] = { 0x01, (rumble_addr[1] >> 8) & 0xff, rumble_addr[1] & 0xff, 0x01 }; + struct hid_device *hdev = to_hid_device(dev); + struct claw_drvdata *drvdata = hid_get_drvdata(hdev); + int ret; + + guard(mutex)(&drvdata->profile_mutex); + drvdata->profile_pending = CLAW_RUMBLE_RIGHT_PENDING; + ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, + ARRAY_SIZE(data), 8); + if (ret) { + drvdata->profile_pending = CLAW_NO_PENDING; + return ret; + } + + return sysfs_emit(buf, "%u\n", drvdata->rumble_intensity_right); +} +static DEVICE_ATTR_RW(rumble_intensity_right); + +static ssize_t rumble_intensity_range_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0-100\n"); +} +static DEVICE_ATTR_RO(rumble_intensity_range); + static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n) { @@ -836,6 +976,12 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu attr == &dev_attr_reset.attr) return attr->mode; + /* Hide rumble attrs if not supported */ + if (attr == &dev_attr_rumble_intensity_left.attr || + attr == &dev_attr_rumble_intensity_right.attr || + attr == &dev_attr_rumble_intensity_range.attr) + return drvdata->rumble_support ? attr->mode : 0; + /* Hide button mapping attrs if it isn't supported */ return drvdata->bmap_support ? attr->mode : 0; } @@ -849,6 +995,9 @@ static struct attribute *claw_gamepad_attrs[] = { &dev_attr_mkeys_function.attr, &dev_attr_mkeys_function_index.attr, &dev_attr_reset.attr, + &dev_attr_rumble_intensity_left.attr, + &dev_attr_rumble_intensity_right.attr, + &dev_attr_rumble_intensity_range.attr, NULL, }; @@ -1320,6 +1469,7 @@ static void claw_features_supported(struct claw_drvdata *drvdata) drvdata->bmap_support = true; if (minor >= 0x66) { drvdata->bmap_addr = button_mapping_addr_new; + drvdata->rumble_support = true; drvdata->rgb_addr = rgb_addr_new; } else { drvdata->bmap_addr = button_mapping_addr_old; @@ -1331,6 +1481,7 @@ static void claw_features_supported(struct claw_drvdata *drvdata) if ((major == 0x02 && minor >= 0x17) || major >= 0x03) { drvdata->bmap_support = true; drvdata->bmap_addr = button_mapping_addr_new; + drvdata->rumble_support = true; drvdata->rgb_addr = rgb_addr_new; return; } From e26cff900f03185c355b708265539c277b50961a Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Mon, 26 Aug 2024 12:49:35 +1200 Subject: [PATCH 56/73] [FOR-UPSTREAM] hid-asus-ally: Add joystick LED ring support Adds basic support for the joystick RGB LED rings as a multicolour LED device with 4 LEDs. As this uses the software-mode for setting the LED colours it also sets the MCU-mode for LED's to static + the first RGB colour on suspend/reboot/shutdown. This is done to prevent user confusion if the LED's were to not match what was set, and to maintain consistency. Signed-off-by: Luke D. Jones --- drivers/hid/Kconfig | 9 + drivers/hid/Makefile | 1 + drivers/hid/hid-asus-ally.c | 518 ++++++++++++++++++++++++++++++++++++ drivers/hid/hid-asus-ally.h | 38 +++ drivers/hid/hid-asus.c | 15 ++ drivers/hid/hid-asus.h | 11 + 6 files changed, 592 insertions(+) create mode 100644 drivers/hid/hid-asus-ally.c create mode 100644 drivers/hid/hid-asus-ally.h create mode 100644 drivers/hid/hid-asus.h diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index fb380ff6081a3..3766d55d1a9e4 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -200,6 +200,15 @@ config HID_ASUS - GL553V series - GL753V series +config HID_ASUS_ALLY + tristate "Asus Ally gamepad configuration support" + depends on USB_HID + depends on LEDS_CLASS + depends on LEDS_CLASS_MULTICOLOR + select POWER_SUPPLY + help + Support for configuring the Asus ROG Ally gamepad using attributes. + config HID_AUREAL tristate "Aureal" help diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 69a97d77a9889..83df7c8661f58 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -34,6 +34,7 @@ obj-$(CONFIG_HID_APPLETB_BL) += hid-appletb-bl.o obj-$(CONFIG_HID_APPLETB_KBD) += hid-appletb-kbd.o obj-$(CONFIG_HID_CREATIVE_SB0540) += hid-creative-sb0540.o obj-$(CONFIG_HID_ASUS) += hid-asus.o +obj-$(CONFIG_HID_ASUS_ALLY) += hid-asus-ally.o obj-$(CONFIG_HID_AUREAL) += hid-aureal.o obj-$(CONFIG_HID_BELKIN) += hid-belkin.o obj-$(CONFIG_HID_BETOP_FF) += hid-betopff.o diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c new file mode 100644 index 0000000000000..db5d256093504 --- /dev/null +++ b/drivers/hid/hid-asus-ally.c @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for Asus ROG laptops and Ally + * + * Copyright (c) 2023 Luke Jones + */ + +#include "linux/device.h" +#include +#include +#include "linux/pm.h" +#include "linux/slab.h" +#include "linux/stddef.h" +#include +#include +#include +#include +#include + +#include "hid-ids.h" +#include "hid-asus.h" +#include "hid-asus-ally.h" + +#define READY_MAX_TRIES 3 +#define FEATURE_REPORT_ID 0x0d +#define FEATURE_ROG_ALLY_REPORT_ID 0x5a +#define FEATURE_ROG_ALLY_CODE_PAGE 0xD1 +#define FEATURE_ROG_ALLY_REPORT_SIZE 64 +#define ALLY_X_INPUT_REPORT_USB 0x0B +#define ALLY_X_INPUT_REPORT_USB_SIZE 16 + +#define ROG_ALLY_REPORT_SIZE 64 +#define ROG_ALLY_X_MIN_MCU 313 +#define ROG_ALLY_MIN_MCU 319 + +#define FEATURE_KBD_LED_REPORT_ID1 0x5d +#define FEATURE_KBD_LED_REPORT_ID2 0x5e + +static const u8 EC_INIT_STRING[] = { 0x5A, 'A', 'S', 'U', 'S', ' ', 'T', 'e','c', 'h', '.', 'I', 'n', 'c', '.', '\0' }; +static const u8 EC_MODE_LED_APPLY[] = { 0x5A, 0xB4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +static const u8 EC_MODE_LED_SET[] = { 0x5A, 0xB5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +static const u8 FORCE_FEEDBACK_OFF[] = { 0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB }; + +static const struct hid_device_id rog_ally_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK, USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY) }, + { HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK, USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X) }, + {} +}; + +struct ally_rgb_dev { + struct hid_device *hdev; + struct led_classdev_mc led_rgb_dev; + struct work_struct work; + bool output_worker_initialized; + spinlock_t lock; + + bool removed; + bool update_rgb; + uint8_t red[4]; + uint8_t green[4]; + uint8_t blue[4]; +}; + +struct ally_rgb_data { + uint8_t brightness; + uint8_t red[4]; + uint8_t green[4]; + uint8_t blue[4]; + bool initialized; +}; + +static struct ally_drvdata { + struct hid_device *hdev; + struct ally_rgb_dev *led_rgb_dev; + struct ally_rgb_data led_rgb_data; +} drvdata; + +/** + * asus_dev_set_report - send set report request to device. + * + * @hdev: hid device + * @buf: in/out data to transfer + * @len: length of buf + * + * Return: count of data transferred, negative if error + * + * Same behavior as hid_hw_raw_request. Note that the input buffer is duplicated. + */ +static int asus_dev_set_report(struct hid_device *hdev, const u8 *buf, size_t len) +{ + unsigned char *dmabuf; + int ret; + + dmabuf = kmemdup(buf, len, GFP_KERNEL); + if (!dmabuf) + return -ENOMEM; + + ret = hid_hw_raw_request(hdev, buf[0], dmabuf, len, HID_FEATURE_REPORT, + HID_REQ_SET_REPORT); + kfree(dmabuf); + + return ret; +} + +static u8 get_endpoint_address(struct hid_device *hdev) +{ + struct usb_interface *intf; + struct usb_host_endpoint *ep; + + intf = to_usb_interface(hdev->dev.parent); + + if (intf) { + ep = intf->cur_altsetting->endpoint; + if (ep) { + return ep->desc.bEndpointAddress; + } + } + + return -ENODEV; +} + +/**************************************************************************************************/ +/* ROG Ally LED control */ +/**************************************************************************************************/ +static void ally_rgb_schedule_work(struct ally_rgb_dev *led) +{ + unsigned long flags; + + spin_lock_irqsave(&led->lock, flags); + if (!led->removed) + schedule_work(&led->work); + spin_unlock_irqrestore(&led->lock, flags); +} + +/* + * The RGB still has the basic 0-3 level brightness. Since the multicolour + * brightness is being used in place, set this to max + */ +static int ally_rgb_set_bright_base_max(struct hid_device *hdev) +{ + u8 buf[] = { FEATURE_KBD_LED_REPORT_ID1, 0xba, 0xc5, 0xc4, 0x02 }; + + return asus_dev_set_report(hdev, buf, sizeof(buf)); +} + +static void ally_rgb_do_work(struct work_struct *work) +{ + struct ally_rgb_dev *led = container_of(work, struct ally_rgb_dev, work); + int ret; + unsigned long flags; + + u8 buf[16] = { [0] = FEATURE_ROG_ALLY_REPORT_ID, + [1] = FEATURE_ROG_ALLY_CODE_PAGE, + [2] = xpad_cmd_set_leds, + [3] = xpad_cmd_len_leds }; + + spin_lock_irqsave(&led->lock, flags); + if (!led->update_rgb) { + spin_unlock_irqrestore(&led->lock, flags); + return; + } + + for (int i = 0; i < 4; i++) { + buf[5 + i * 3] = drvdata.led_rgb_dev->green[i]; + buf[6 + i * 3] = drvdata.led_rgb_dev->blue[i]; + buf[4 + i * 3] = drvdata.led_rgb_dev->red[i]; + } + led->update_rgb = false; + + spin_unlock_irqrestore(&led->lock, flags); + + ret = asus_dev_set_report(led->hdev, buf, sizeof(buf)); + if (ret < 0) + hid_err(led->hdev, "Ally failed to set gamepad backlight: %d\n", ret); +} + +static void ally_rgb_set(struct led_classdev *cdev, enum led_brightness brightness) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev); + struct ally_rgb_dev *led = container_of(mc_cdev, struct ally_rgb_dev, led_rgb_dev); + int intensity, bright; + unsigned long flags; + + led_mc_calc_color_components(mc_cdev, brightness); + spin_lock_irqsave(&led->lock, flags); + led->update_rgb = true; + bright = mc_cdev->led_cdev.brightness; + for (int i = 0; i < 4; i++) { + intensity = mc_cdev->subled_info[i].intensity; + drvdata.led_rgb_dev->red[i] = (((intensity >> 16) & 0xFF) * bright) / 255; + drvdata.led_rgb_dev->green[i] = (((intensity >> 8) & 0xFF) * bright) / 255; + drvdata.led_rgb_dev->blue[i] = ((intensity & 0xFF) * bright) / 255; + } + spin_unlock_irqrestore(&led->lock, flags); + drvdata.led_rgb_data.initialized = true; + + ally_rgb_schedule_work(led); +} + +static int ally_rgb_set_static_from_multi(struct hid_device *hdev) +{ + u8 buf[17] = {FEATURE_KBD_LED_REPORT_ID1, 0xb3}; + int ret; + + /* + * Set single zone single colour based on the first LED of EC software mode. + * buf[2] = zone, buf[3] = mode + */ + buf[4] = drvdata.led_rgb_data.red[0]; + buf[5] = drvdata.led_rgb_data.green[0]; + buf[6] = drvdata.led_rgb_data.blue[0]; + + ret = asus_dev_set_report(hdev, buf, sizeof(buf)); + if (ret < 0) + return ret; + + ret = asus_dev_set_report(hdev, EC_MODE_LED_APPLY, sizeof(EC_MODE_LED_APPLY)); + if (ret < 0) + return ret; + + return asus_dev_set_report(hdev, EC_MODE_LED_SET, sizeof(EC_MODE_LED_SET)); +} + +/* + * Store the RGB values for restoring on resume, and set the static mode to the first LED colour +*/ +static void ally_rgb_store_settings(void) +{ + int arr_size = sizeof(drvdata.led_rgb_data.red); + + struct ally_rgb_dev *led_rgb = drvdata.led_rgb_dev; + + drvdata.led_rgb_data.brightness = led_rgb->led_rgb_dev.led_cdev.brightness; + + memcpy(drvdata.led_rgb_data.red, led_rgb->red, arr_size); + memcpy(drvdata.led_rgb_data.green, led_rgb->green, arr_size); + memcpy(drvdata.led_rgb_data.blue, led_rgb->blue, arr_size); + + ally_rgb_set_static_from_multi(led_rgb->hdev); +} + +static void ally_rgb_restore_settings(struct ally_rgb_dev *led_rgb, struct led_classdev *led_cdev, + struct mc_subled *mc_led_info) +{ + int arr_size = sizeof(drvdata.led_rgb_data.red); + + memcpy(led_rgb->red, drvdata.led_rgb_data.red, arr_size); + memcpy(led_rgb->green, drvdata.led_rgb_data.green, arr_size); + memcpy(led_rgb->blue, drvdata.led_rgb_data.blue, arr_size); + for (int i = 0; i < 4; i++) { + mc_led_info[i].intensity = (drvdata.led_rgb_data.red[i] << 16) | + (drvdata.led_rgb_data.green[i] << 8) | + drvdata.led_rgb_data.blue[i]; + } + led_cdev->brightness = drvdata.led_rgb_data.brightness; +} + +/* Set LEDs. Call after any setup. */ +static void ally_rgb_resume(void) +{ + struct ally_rgb_dev *led_rgb = drvdata.led_rgb_dev; + struct led_classdev *led_cdev; + struct mc_subled *mc_led_info; + + if (!led_rgb) + return; + + led_cdev = &led_rgb->led_rgb_dev.led_cdev; + mc_led_info = led_rgb->led_rgb_dev.subled_info; + + if (drvdata.led_rgb_data.initialized) { + ally_rgb_restore_settings(led_rgb, led_cdev, mc_led_info); + led_rgb->update_rgb = true; + ally_rgb_schedule_work(led_rgb); + ally_rgb_set_bright_base_max(led_rgb->hdev); + } +} + +static int ally_rgb_register(struct hid_device *hdev, struct ally_rgb_dev *led_rgb) +{ + struct mc_subled *mc_led_info; + struct led_classdev *led_cdev; + + mc_led_info = + devm_kmalloc_array(&hdev->dev, 12, sizeof(*mc_led_info), GFP_KERNEL | __GFP_ZERO); + if (!mc_led_info) + return -ENOMEM; + + mc_led_info[0].color_index = LED_COLOR_ID_RGB; + mc_led_info[1].color_index = LED_COLOR_ID_RGB; + mc_led_info[2].color_index = LED_COLOR_ID_RGB; + mc_led_info[3].color_index = LED_COLOR_ID_RGB; + + led_rgb->led_rgb_dev.subled_info = mc_led_info; + led_rgb->led_rgb_dev.num_colors = 4; + + led_cdev = &led_rgb->led_rgb_dev.led_cdev; + led_cdev->brightness = 128; + led_cdev->name = "ally:rgb:joystick_rings"; + led_cdev->max_brightness = 255; + led_cdev->brightness_set = ally_rgb_set; + + if (drvdata.led_rgb_data.initialized) { + ally_rgb_restore_settings(led_rgb, led_cdev, mc_led_info); + } + + return devm_led_classdev_multicolor_register(&hdev->dev, &led_rgb->led_rgb_dev); +} + +static struct ally_rgb_dev *ally_rgb_create(struct hid_device *hdev) +{ + struct ally_rgb_dev *led_rgb; + int ret; + + led_rgb = devm_kzalloc(&hdev->dev, sizeof(struct ally_rgb_dev), GFP_KERNEL); + if (!led_rgb) + return ERR_PTR(-ENOMEM); + + ret = ally_rgb_register(hdev, led_rgb); + if (ret < 0) { + cancel_work_sync(&led_rgb->work); + devm_kfree(&hdev->dev, led_rgb); + return ERR_PTR(ret); + } + + led_rgb->hdev = hdev; + led_rgb->removed = false; + + INIT_WORK(&led_rgb->work, ally_rgb_do_work); + led_rgb->output_worker_initialized = true; + spin_lock_init(&led_rgb->lock); + + ally_rgb_set_bright_base_max(hdev); + + /* Not marked as initialized unless ally_rgb_set() is called */ + if (drvdata.led_rgb_data.initialized) { + msleep(1500); + led_rgb->update_rgb = true; + ally_rgb_schedule_work(led_rgb); + } + + return led_rgb; +} + +static void ally_rgb_remove(struct hid_device *hdev) +{ + struct ally_rgb_dev *led_rgb = drvdata.led_rgb_dev; + unsigned long flags; + int ep; + + ep = get_endpoint_address(hdev); + if (ep != ROG_ALLY_CFG_INTF_IN) + return; + + if (!drvdata.led_rgb_dev || led_rgb->removed) + return; + + spin_lock_irqsave(&led_rgb->lock, flags); + led_rgb->removed = true; + led_rgb->output_worker_initialized = false; + spin_unlock_irqrestore(&led_rgb->lock, flags); + cancel_work_sync(&led_rgb->work); + devm_led_classdev_multicolor_unregister(&hdev->dev, &led_rgb->led_rgb_dev); + + hid_info(hdev, "Removed Ally RGB interface"); +} + +/**************************************************************************************************/ +/* ROG Ally driver init */ +/**************************************************************************************************/ + +static int ally_hid_init(struct hid_device *hdev) +{ + int ret; + + ret = asus_dev_set_report(hdev, EC_INIT_STRING, sizeof(EC_INIT_STRING)); + if (ret < 0) { + hid_err(hdev, "Ally failed to send init command: %d\n", ret); + return ret; + } + + ret = asus_dev_set_report(hdev, FORCE_FEEDBACK_OFF, sizeof(FORCE_FEEDBACK_OFF)); + if (ret < 0) + hid_err(hdev, "Ally failed to send init command: %d\n", ret); + + return ret; +} + +static int ally_hid_probe(struct hid_device *hdev, const struct hid_device_id *_id) +{ + int ret, ep; + + ep = get_endpoint_address(hdev); + if (ep < 0) + return ep; + + if (ep != ROG_ALLY_CFG_INTF_IN) + return -ENODEV; + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "Parse failed\n"); + return ret; + } + + ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); + if (ret) { + hid_err(hdev, "Failed to start HID device\n"); + return ret; + } + + ret = hid_hw_open(hdev); + if (ret) { + hid_err(hdev, "Failed to open HID device\n"); + goto err_stop; + } + + /* Initialize MCU even before alloc */ + ret = ally_hid_init(hdev); + if (ret < 0) + return ret; + + drvdata.hdev = hdev; + hid_set_drvdata(hdev, &drvdata); + + /* This should almost always exist */ + if (ep == ROG_ALLY_CFG_INTF_IN) { + drvdata.led_rgb_dev = ally_rgb_create(hdev); + if (IS_ERR(drvdata.led_rgb_dev)) + hid_err(hdev, "Failed to create Ally gamepad LEDs.\n"); + else + hid_info(hdev, "Created Ally RGB LED controls.\n"); + + if (IS_ERR(drvdata.led_rgb_dev)) + goto err_close; + } + + return 0; + +err_close: + hid_hw_close(hdev); +err_stop: + hid_hw_stop(hdev); + return ret; +} + +static void ally_hid_remove(struct hid_device *hdev) +{ + if (drvdata.led_rgb_dev) + ally_rgb_remove(hdev); + + hid_hw_close(hdev); + hid_hw_stop(hdev); +} + +static int ally_hid_reset_resume(struct hid_device *hdev) +{ + int ep = get_endpoint_address(hdev); + if (ep != ROG_ALLY_CFG_INTF_IN) + return 0; + + ally_hid_init(hdev); + ally_rgb_resume(); + + return 0; +} + +static int ally_pm_thaw(struct device *dev) +{ + struct hid_device *hdev = to_hid_device(dev); + + return ally_hid_reset_resume(hdev); +} + +static int ally_pm_suspend(struct device *dev) +{ + if (drvdata.led_rgb_dev) { + ally_rgb_store_settings(); + } + + return 0; +} + +static const struct dev_pm_ops ally_pm_ops = { + .thaw = ally_pm_thaw, + .suspend = ally_pm_suspend, + .poweroff = ally_pm_suspend, +}; + +MODULE_DEVICE_TABLE(hid, rog_ally_devices); + +static struct hid_driver rog_ally_cfg = { .name = "asus_rog_ally", + .id_table = rog_ally_devices, + .probe = ally_hid_probe, + .remove = ally_hid_remove, + /* ALLy 1 requires this to reset device state correctly */ + .reset_resume = ally_hid_reset_resume, + .driver = { + .pm = &ally_pm_ops, + } +}; + +static int __init rog_ally_init(void) +{ + return hid_register_driver(&rog_ally_cfg); +} + +static void __exit rog_ally_exit(void) +{ + hid_unregister_driver(&rog_ally_cfg); +} + +module_init(rog_ally_init); +module_exit(rog_ally_exit); + +MODULE_AUTHOR("Luke D. Jones"); +MODULE_DESCRIPTION("HID Driver for ASUS ROG Ally gamepad configuration."); +MODULE_LICENSE("GPL"); diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h new file mode 100644 index 0000000000000..eb8617c80c2ab --- /dev/null +++ b/drivers/hid/hid-asus-ally.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * + * HID driver for Asus ROG laptops and Ally + * + * Copyright (c) 2023 Luke Jones + */ + +#include +#include + +/* the xpad_cmd determines which feature is set or queried */ +enum xpad_cmd { + xpad_cmd_set_mode = 0x01, + xpad_cmd_set_mapping = 0x02, + xpad_cmd_set_js_dz = 0x04, /* deadzones */ + xpad_cmd_set_tr_dz = 0x05, /* deadzones */ + xpad_cmd_set_vibe_intensity = 0x06, + xpad_cmd_set_leds = 0x08, + xpad_cmd_check_ready = 0x0A, + xpad_cmd_set_calibration = 0x0D, + xpad_cmd_set_turbo = 0x0F, + xpad_cmd_set_response_curve = 0x13, + xpad_cmd_set_adz = 0x18, +}; + +/* the xpad_cmd determines which feature is set or queried */ +enum xpad_cmd_len { + xpad_cmd_len_mode = 0x01, + xpad_cmd_len_mapping = 0x2c, + xpad_cmd_len_deadzone = 0x04, + xpad_cmd_len_vibe_intensity = 0x02, + xpad_cmd_len_leds = 0x0C, + xpad_cmd_len_calibration2 = 0x01, + xpad_cmd_len_calibration3 = 0x01, + xpad_cmd_len_turbo = 0x20, + xpad_cmd_len_response_curve = 0x09, + xpad_cmd_len_adz = 0x02, +}; diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 3f5e96900b67a..6baa5011d0466 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -32,6 +32,7 @@ #include #include "hid-ids.h" +#include "hid-asus.h" MODULE_AUTHOR("Yusuke Fujimaki "); MODULE_AUTHOR("Brendan McGrath "); @@ -1195,6 +1196,8 @@ 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 usb_host_endpoint *ep; + struct usb_interface *intf; struct hid_report *rep; bool is_vendor = false; int ret; @@ -1209,6 +1212,18 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) drvdata->quirks = id->driver_data; + /* Ignore these endpoints as they are used by hid-asus-ally */ + #if IS_REACHABLE(CONFIG_HID_ASUS_ALLY) + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { + intf = to_usb_interface(hdev->dev.parent); + ep = intf->cur_altsetting->endpoint; + if (ep->desc.bEndpointAddress == ROG_ALLY_X_INTF_IN || + ep->desc.bEndpointAddress == ROG_ALLY_CFG_INTF_IN || + ep->desc.bEndpointAddress == ROG_ALLY_CFG_INTF_OUT) + return -ENODEV; + } + #endif /* IS_REACHABLE(CONFIG_HID_ASUS_ALLY) */ + /* * T90CHI's keyboard dock returns same ID values as T100CHI's dock. * Thus, identify T90CHI dock with product name string. diff --git a/drivers/hid/hid-asus.h b/drivers/hid/hid-asus.h new file mode 100644 index 0000000000000..789a4ff3662cf --- /dev/null +++ b/drivers/hid/hid-asus.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __HID_ASUS_H +#define __HID_ASUS_H + +#include + +#define ROG_ALLY_CFG_INTF_IN 0x83 +#define ROG_ALLY_CFG_INTF_OUT 0x04 +#define ROG_ALLY_X_INTF_IN 0x87 + +#endif /* __HID_ASUS_H */ From 2f05d82aa7440b798461a7beb0ea43e800dffcde Mon Sep 17 00:00:00 2001 From: Luke Jones Date: Mon, 24 Mar 2025 15:41:05 +1300 Subject: [PATCH 57/73] [FOR-UPSTREAM] hid-asus-ally: do MCY FW validation in hid-asus-ally Export the validate_mcu_fw_version() symbol in namespace and use in hid-asus-ally if the driver is enabled. Signed-off-by: Luke Jones --- drivers/hid/hid-asus-ally.c | 7 +++++++ drivers/hid/hid-asus.c | 25 +++++++++++++++---------- drivers/hid/hid-asus.h | 2 ++ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index db5d256093504..8c6eeedb35c0a 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -388,6 +388,9 @@ static int ally_hid_init(struct hid_device *hdev) static int ally_hid_probe(struct hid_device *hdev, const struct hid_device_id *_id) { + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_device *udev = interface_to_usbdev(intf); + u16 idProduct = le16_to_cpu(udev->descriptor.idProduct); int ret, ep; ep = get_endpoint_address(hdev); @@ -425,6 +428,8 @@ static int ally_hid_probe(struct hid_device *hdev, const struct hid_device_id *_ /* This should almost always exist */ if (ep == ROG_ALLY_CFG_INTF_IN) { + validate_mcu_fw_version(hdev, idProduct); + drvdata.led_rgb_dev = ally_rgb_create(hdev); if (IS_ERR(drvdata.led_rgb_dev)) hid_err(hdev, "Failed to create Ally gamepad LEDs.\n"); @@ -513,6 +518,8 @@ static void __exit rog_ally_exit(void) module_init(rog_ally_init); module_exit(rog_ally_exit); +MODULE_IMPORT_NS("ASUS_WMI"); +MODULE_IMPORT_NS("HID_ASUS"); MODULE_AUTHOR("Luke D. Jones"); MODULE_DESCRIPTION("HID Driver for ASUS ROG Ally gamepad configuration."); MODULE_LICENSE("GPL"); diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 6baa5011d0466..48fb1ebcee170 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -20,6 +20,10 @@ * Copyright (c) 2016 Frederik Wenigwieser */ +/* + */ + +#include "linux/export.h" #include #include #include @@ -687,7 +691,7 @@ static int mcu_request_version(struct hid_device *hdev) return ret; } -static void validate_mcu_fw_version(struct hid_device *hdev, int idProduct) +void validate_mcu_fw_version(struct hid_device *hdev, int idProduct) { int min_version, version; @@ -715,6 +719,7 @@ static void validate_mcu_fw_version(struct hid_device *hdev, int idProduct) set_ally_mcu_powersave(true); } } +EXPORT_SYMBOL_NS(validate_mcu_fw_version, "HID_ASUS"); static bool asus_has_report_id(struct hid_device *hdev, u16 report_id) { @@ -734,8 +739,6 @@ static bool asus_has_report_id(struct hid_device *hdev, u16 report_id) static int asus_kbd_register_leds(struct hid_device *hdev) { struct asus_drvdata *drvdata = hid_get_drvdata(hdev); - struct usb_interface *intf; - struct usb_device *udev; unsigned char kbd_func; int ret; @@ -748,19 +751,21 @@ static int asus_kbd_register_leds(struct hid_device *hdev) if (!(kbd_func & SUPPORT_KBD_BACKLIGHT)) return -ENODEV; + #if !IS_REACHABLE(CONFIG_HID_ASUS_ALLY) + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_device *udev = interface_to_usbdev(intf); + validate_mcu_fw_version(hdev, + le16_to_cpu(udev->descriptor.idProduct)); + } + #endif /* !IS_REACHABLE(CONFIG_HID_ASUS_ALLY) */ + if (dmi_match(DMI_PRODUCT_FAMILY, "ProArt P16")) { ret = asus_kbd_disable_oobe(hdev); if (ret < 0) return ret; } - if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { - intf = to_usb_interface(hdev->dev.parent); - udev = interface_to_usbdev(intf); - validate_mcu_fw_version(hdev, - le16_to_cpu(udev->descriptor.idProduct)); - } - drvdata->kbd_backlight = devm_kzalloc(&hdev->dev, sizeof(struct asus_kbd_leds), GFP_KERNEL); diff --git a/drivers/hid/hid-asus.h b/drivers/hid/hid-asus.h index 789a4ff3662cf..f67dd5a3a1bc3 100644 --- a/drivers/hid/hid-asus.h +++ b/drivers/hid/hid-asus.h @@ -8,4 +8,6 @@ #define ROG_ALLY_CFG_INTF_OUT 0x04 #define ROG_ALLY_X_INTF_IN 0x87 +void validate_mcu_fw_version(struct hid_device *hdev, int idProduct); + #endif /* __HID_ASUS_H */ From 895c8f17d3862a459b60ef7bfe54c778f67cd580 Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Wed, 2 Oct 2024 23:32:46 +1300 Subject: [PATCH 58/73] [FOR-UPSTREAM] hid-asus-ally: initial Ally-X gamepad bringup Enable use of the new gamepad device created by the MCU. - Triggers - Buttons - Sticks - Vibration Signed-off-by: Luke D. Jones --- drivers/hid/hid-asus-ally.c | 385 +++++++++++++++++++++++++++++++++++- drivers/hid/hid-asus-ally.h | 5 + 2 files changed, 389 insertions(+), 1 deletion(-) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index 8c6eeedb35c0a..20947218b436d 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -47,6 +47,51 @@ static const struct hid_device_id rog_ally_devices[] = { {} }; +/* 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 }, +}; + +/* rumble packet structure */ +struct ff_data { + u8 enable; + u8 magnitude_left; + u8 magnitude_right; + u8 magnitude_strong; + u8 magnitude_weak; + u8 pulse_sustain_10ms; + u8 pulse_release_10ms; + u8 loop_count; +} __packed; + +struct ff_report { + u8 report_id; + struct ff_data ff; +} __packed; + +struct ally_x_input_report { + uint16_t x, y; + uint16_t rx, ry; + uint16_t z, rz; + uint8_t buttons[4]; +} __packed; + +struct ally_x_device { + struct input_dev *input; + struct hid_device *hdev; + spinlock_t lock; + + struct ff_report *ff_packet; + struct work_struct output_worker; + bool output_worker_initialized; + /* Prevent multiple queued event due to the enforced delay in worker */ + bool update_qam_btn; + /* Set if the QAM and AC buttons emit Xbox and Xbox+A */ + bool qam_btns_steam_mode; + bool update_ff; +}; + struct ally_rgb_dev { struct hid_device *hdev; struct led_classdev_mc led_rgb_dev; @@ -71,6 +116,7 @@ struct ally_rgb_data { static struct ally_drvdata { struct hid_device *hdev; + struct ally_x_device *ally_x; struct ally_rgb_dev *led_rgb_dev; struct ally_rgb_data led_rgb_data; } drvdata; @@ -119,6 +165,309 @@ static u8 get_endpoint_address(struct hid_device *hdev) return -ENODEV; } +/**************************************************************************************************/ +/* ROG Ally gamepad i/o and force-feedback */ +/**************************************************************************************************/ +static int ally_x_raw_event(struct ally_x_device *ally_x, struct hid_report *report, u8 *data, + int size) +{ + struct ally_x_input_report *in_report; + unsigned long flags; + u8 byte; + + if (data[0] == 0x0B) { + in_report = (struct ally_x_input_report *)&data[1]; + + input_report_abs(ally_x->input, ABS_X, in_report->x); + input_report_abs(ally_x->input, ABS_Y, in_report->y); + input_report_abs(ally_x->input, ABS_RX, in_report->rx); + input_report_abs(ally_x->input, ABS_RY, in_report->ry); + input_report_abs(ally_x->input, ABS_Z, in_report->z); + input_report_abs(ally_x->input, ABS_RZ, in_report->rz); + + byte = in_report->buttons[0]; + input_report_key(ally_x->input, BTN_A, byte & BIT(0)); + input_report_key(ally_x->input, BTN_B, byte & BIT(1)); + input_report_key(ally_x->input, BTN_X, byte & BIT(2)); + input_report_key(ally_x->input, BTN_Y, byte & BIT(3)); + input_report_key(ally_x->input, BTN_TL, byte & BIT(4)); + input_report_key(ally_x->input, BTN_TR, byte & BIT(5)); + input_report_key(ally_x->input, BTN_SELECT, byte & BIT(6)); + input_report_key(ally_x->input, BTN_START, byte & BIT(7)); + + byte = in_report->buttons[1]; + input_report_key(ally_x->input, BTN_THUMBL, byte & BIT(0)); + input_report_key(ally_x->input, BTN_THUMBR, byte & BIT(1)); + input_report_key(ally_x->input, BTN_MODE, byte & BIT(2)); + + byte = in_report->buttons[2]; + input_report_abs(ally_x->input, ABS_HAT0X, hat_values[byte][0]); + input_report_abs(ally_x->input, ABS_HAT0Y, hat_values[byte][1]); + } + /* + * The MCU used on Ally provides many devices: gamepad, keyboord, mouse, other. + * The AC and QAM buttons route through another interface making it difficult to + * use the events unless we grab those and use them here. Only works for Ally X. + */ + else if (data[0] == 0x5A) { + if (ally_x->qam_btns_steam_mode) { + spin_lock_irqsave(&ally_x->lock, flags); + if (data[1] == 0x38 && !ally_x->update_qam_btn) { + ally_x->update_qam_btn = true; + if (ally_x->output_worker_initialized) + schedule_work(&ally_x->output_worker); + } + spin_unlock_irqrestore(&ally_x->lock, flags); + /* Left/XBox button. Long press does ctrl+alt+del which we can't catch */ + input_report_key(ally_x->input, BTN_MODE, data[1] == 0xA6); + } else { + input_report_key(ally_x->input, KEY_F16, data[1] == 0xA6); + input_report_key(ally_x->input, KEY_PROG1, data[1] == 0x38); + } + /* QAM long press */ + input_report_key(ally_x->input, KEY_F17, data[1] == 0xA7); + /* QAM long press released */ + input_report_key(ally_x->input, KEY_F18, data[1] == 0xA8); + } + + input_sync(ally_x->input); + + return 0; +} + +static struct input_dev *ally_x_alloc_input_dev(struct hid_device *hdev, + const char *name_suffix) +{ + struct input_dev *input_dev; + + input_dev = devm_input_allocate_device(&hdev->dev); + if (!input_dev) + return ERR_PTR(-ENOMEM); + + 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, hdev); + + return input_dev; +} + +static int ally_x_play_effect(struct input_dev *idev, void *data, struct ff_effect *effect) +{ + struct ally_x_device *ally_x = drvdata.ally_x; + unsigned long flags; + + if (effect->type != FF_RUMBLE) + return 0; + + spin_lock_irqsave(&ally_x->lock, flags); + ally_x->ff_packet->ff.magnitude_strong = effect->u.rumble.strong_magnitude / 512; + ally_x->ff_packet->ff.magnitude_weak = effect->u.rumble.weak_magnitude / 512; + ally_x->update_ff = true; + spin_unlock_irqrestore(&ally_x->lock, flags); + + if (ally_x->output_worker_initialized) + schedule_work(&ally_x->output_worker); + + return 0; +} + +static void ally_x_work(struct work_struct *work) +{ + struct ally_x_device *ally_x = container_of(work, struct ally_x_device, output_worker); + struct ff_report *ff_report = NULL; + bool update_qam = false; + bool update_ff = false; + unsigned long flags; + + spin_lock_irqsave(&ally_x->lock, flags); + update_ff = ally_x->update_ff; + if (ally_x->update_ff) { + ff_report = kmemdup(ally_x->ff_packet, sizeof(*ally_x->ff_packet), GFP_KERNEL); + ally_x->update_ff = false; + } + update_qam = ally_x->update_qam_btn; + spin_unlock_irqrestore(&ally_x->lock, flags); + + if (update_ff && ff_report) { + ff_report->ff.magnitude_left = ff_report->ff.magnitude_strong; + ff_report->ff.magnitude_right = ff_report->ff.magnitude_weak; + asus_dev_set_report(ally_x->hdev, (u8 *)ff_report, sizeof(*ff_report)); + } + kfree(ff_report); + + if (update_qam) { + /* + * The sleeps here are required to allow steam to register the button combo. + */ + usleep_range(1000, 2000); + input_report_key(ally_x->input, BTN_MODE, 1); + input_sync(ally_x->input); + + msleep(80); + input_report_key(ally_x->input, BTN_A, 1); + input_sync(ally_x->input); + + msleep(80); + input_report_key(ally_x->input, BTN_A, 0); + input_sync(ally_x->input); + + msleep(80); + input_report_key(ally_x->input, BTN_MODE, 0); + input_sync(ally_x->input); + + spin_lock_irqsave(&ally_x->lock, flags); + ally_x->update_qam_btn = false; + spin_unlock_irqrestore(&ally_x->lock, flags); + } +} + +static struct input_dev *ally_x_setup_input(struct hid_device *hdev) +{ + int ret, abs_min = 0, js_abs_max = 65535, tr_abs_max = 1023; + struct input_dev *input; + + input = ally_x_alloc_input_dev(hdev, NULL); + if (IS_ERR(input)) + return ERR_CAST(input); + + input_set_abs_params(input, ABS_X, abs_min, js_abs_max, 0, 0); + input_set_abs_params(input, ABS_Y, abs_min, js_abs_max, 0, 0); + input_set_abs_params(input, ABS_RX, abs_min, js_abs_max, 0, 0); + input_set_abs_params(input, ABS_RY, abs_min, js_abs_max, 0, 0); + input_set_abs_params(input, ABS_Z, abs_min, tr_abs_max, 0, 0); + input_set_abs_params(input, ABS_RZ, abs_min, tr_abs_max, 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); + + input_set_capability(input, EV_KEY, KEY_PROG1); + input_set_capability(input, EV_KEY, KEY_F16); + input_set_capability(input, EV_KEY, KEY_F17); + input_set_capability(input, EV_KEY, KEY_F18); + + input_set_capability(input, EV_FF, FF_RUMBLE); + input_ff_create_memless(input, NULL, ally_x_play_effect); + + ret = input_register_device(input); + if (ret) + return ERR_PTR(ret); + + return input; +} + +static ssize_t ally_x_qam_mode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct ally_x_device *ally_x = drvdata.ally_x; + + return sysfs_emit(buf, "%d\n", ally_x->qam_btns_steam_mode); +} + +static ssize_t ally_x_qam_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ally_x_device *ally_x = drvdata.ally_x; + bool val; + int ret; + + ret = kstrtobool(buf, &val); + if (ret < 0) + return ret; + + ally_x->qam_btns_steam_mode = val; + + return count; +} +ALLY_DEVICE_ATTR_RW(ally_x_qam_mode, qam_mode); + +static struct ally_x_device *ally_x_create(struct hid_device *hdev) +{ + uint8_t max_output_report_size; + struct ally_x_device *ally_x; + struct ff_report *report; + int ret; + + ally_x = devm_kzalloc(&hdev->dev, sizeof(*ally_x), GFP_KERNEL); + if (!ally_x) + return ERR_PTR(-ENOMEM); + + ally_x->hdev = hdev; + INIT_WORK(&ally_x->output_worker, ally_x_work); + spin_lock_init(&ally_x->lock); + ally_x->output_worker_initialized = true; + ally_x->qam_btns_steam_mode = + true; /* Always default to steam mode, it can be changed by userspace attr */ + + max_output_report_size = sizeof(struct ally_x_input_report); + report = devm_kzalloc(&hdev->dev, sizeof(*report), GFP_KERNEL); + if (!report) { + ret = -ENOMEM; + goto free_ally_x; + } + + /* None of these bytes will change for the FF command for now */ + report->report_id = 0x0D; + report->ff.enable = 0x0F; /* Enable all by default */ + report->ff.pulse_sustain_10ms = 0xFF; /* Duration */ + report->ff.pulse_release_10ms = 0x00; /* Start Delay */ + report->ff.loop_count = 0xEB; /* Loop Count */ + ally_x->ff_packet = report; + + ally_x->input = ally_x_setup_input(hdev); + if (IS_ERR(ally_x->input)) { + ret = PTR_ERR(ally_x->input); + goto free_ff_packet; + } + + if (sysfs_create_file(&hdev->dev.kobj, &dev_attr_ally_x_qam_mode.attr)) { + ret = -ENODEV; + goto unregister_input; + } + + ally_x->update_ff = true; + if (ally_x->output_worker_initialized) + schedule_work(&ally_x->output_worker); + + hid_info(hdev, "Registered Ally X controller using %s\n", + dev_name(&ally_x->input->dev)); + return ally_x; + +unregister_input: + input_unregister_device(ally_x->input); +free_ff_packet: + kfree(ally_x->ff_packet); +free_ally_x: + kfree(ally_x); + return ERR_PTR(ret); +} + +static void ally_x_remove(struct hid_device *hdev) +{ + struct ally_x_device *ally_x = drvdata.ally_x; + unsigned long flags; + + spin_lock_irqsave(&ally_x->lock, flags); + ally_x->output_worker_initialized = false; + spin_unlock_irqrestore(&ally_x->lock, flags); + cancel_work_sync(&ally_x->output_worker); + sysfs_remove_file(&hdev->dev.kobj, &dev_attr_ally_x_qam_mode.attr); +} + /**************************************************************************************************/ /* ROG Ally LED control */ /**************************************************************************************************/ @@ -369,6 +718,24 @@ static void ally_rgb_remove(struct hid_device *hdev) /* ROG Ally driver init */ /**************************************************************************************************/ +static int ally_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, + int size) +{ + struct ally_x_device *ally_x = drvdata.ally_x; + + if (ally_x) { + if ((hdev->bus == BUS_USB && report->id == ALLY_X_INPUT_REPORT_USB && + size == ALLY_X_INPUT_REPORT_USB_SIZE) || + (data[0] == 0x5A)) { + ally_x_raw_event(ally_x, report, data, size); + } else { + return -1; + } + } + + return 0; +} + static int ally_hid_init(struct hid_device *hdev) { int ret; @@ -397,7 +764,8 @@ static int ally_hid_probe(struct hid_device *hdev, const struct hid_device_id *_ if (ep < 0) return ep; - if (ep != ROG_ALLY_CFG_INTF_IN) + if (ep != ROG_ALLY_CFG_INTF_IN && + ep != ROG_ALLY_X_INTF_IN) return -ENODEV; ret = hid_parse(hdev); @@ -440,6 +808,17 @@ static int ally_hid_probe(struct hid_device *hdev, const struct hid_device_id *_ goto err_close; } + /* May or may not exist */ + if (ep == ROG_ALLY_X_INTF_IN) { + drvdata.ally_x = ally_x_create(hdev); + if (IS_ERR(drvdata.ally_x)) { + hid_err(hdev, "Failed to create Ally X gamepad.\n"); + drvdata.ally_x = NULL; + goto err_close; + } + hid_info(hdev, "Created Ally X controller.\n"); + } + return 0; err_close: @@ -454,6 +833,9 @@ static void ally_hid_remove(struct hid_device *hdev) if (drvdata.led_rgb_dev) ally_rgb_remove(hdev); + if (drvdata.ally_x) + ally_x_remove(hdev); + hid_hw_close(hdev); hid_hw_stop(hdev); } @@ -498,6 +880,7 @@ static struct hid_driver rog_ally_cfg = { .name = "asus_rog_ally", .id_table = rog_ally_devices, .probe = ally_hid_probe, .remove = ally_hid_remove, + .raw_event = ally_raw_event, /* ALLy 1 requires this to reset device state correctly */ .reset_resume = ally_hid_reset_resume, .driver = { diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h index eb8617c80c2ab..458d02996bca3 100644 --- a/drivers/hid/hid-asus-ally.h +++ b/drivers/hid/hid-asus-ally.h @@ -36,3 +36,8 @@ enum xpad_cmd_len { xpad_cmd_len_response_curve = 0x09, xpad_cmd_len_adz = 0x02, }; + +/* required so we can have nested attributes with same name but different functions */ +#define ALLY_DEVICE_ATTR_RW(_name, _sysfs_name) \ + struct device_attribute dev_attr_##_name = \ + __ATTR(_sysfs_name, 0644, _name##_show, _name##_store) From 85fcb422c08104c3835e41eba24f28472c6e7284 Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Wed, 2 Oct 2024 23:51:36 +1300 Subject: [PATCH 59/73] [FOR-UPSTREAM] hid-asus-ally: initial gamepad configuration Add the basics of the gamepad configuration options. Makes the gamepad usable. Signed-off-by: Luke D. Jones --- drivers/hid/hid-asus-ally.c | 282 +++++++++++++++++++++++++++++++++++- drivers/hid/hid-asus-ally.h | 38 +++-- 2 files changed, 301 insertions(+), 19 deletions(-) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index 20947218b436d..d2679f5f124a7 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -10,7 +10,6 @@ #include #include "linux/pm.h" #include "linux/slab.h" -#include "linux/stddef.h" #include #include #include @@ -36,6 +35,9 @@ #define FEATURE_KBD_LED_REPORT_ID1 0x5d #define FEATURE_KBD_LED_REPORT_ID2 0x5e +#define BTN_DATA_LEN 11; +#define BTN_CODE_BYTES_LEN 8 + static const u8 EC_INIT_STRING[] = { 0x5A, 'A', 'S', 'U', 'S', ' ', 'T', 'e','c', 'h', '.', 'I', 'n', 'c', '.', '\0' }; static const u8 EC_MODE_LED_APPLY[] = { 0x5A, 0xB4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; static const u8 EC_MODE_LED_SET[] = { 0x5A, 0xB5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; @@ -47,6 +49,58 @@ static const struct hid_device_id rog_ally_devices[] = { {} }; +struct btn_code_map { + u64 code; + const char *name; +}; + +/* byte_array must be >= 8 in length */ +static void btn_code_to_byte_array(u64 keycode, u8 *byte_array) +{ + /* Convert the u64 to bytes[8] */ + for (int i = 0; i < 8; ++i) { + byte_array[i] = (keycode >> (56 - 8 * i)) & 0xFF; + } +} + +struct btn_data { + u64 button; + u64 macro; +}; + +struct btn_mapping { + struct btn_data btn_a; + struct btn_data btn_b; + struct btn_data btn_x; + struct btn_data btn_y; + struct btn_data btn_lb; + struct btn_data btn_rb; + struct btn_data btn_ls; + struct btn_data btn_rs; + struct btn_data btn_lt; + struct btn_data btn_rt; + struct btn_data dpad_up; + struct btn_data dpad_down; + struct btn_data dpad_left; + struct btn_data dpad_right; + struct btn_data btn_view; + struct btn_data btn_menu; + struct btn_data btn_m1; + struct btn_data btn_m2; +}; + +/* ROG Ally has many settings related to the gamepad, all using the same n-key endpoint */ +struct ally_gamepad_cfg { + struct hid_device *hdev; + struct input_dev *input; + + enum xpad_mode mode; + /* + * index: [mode] + */ + struct btn_mapping *key_mapping[xpad_mode_mouse]; +}; + /* 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 }, @@ -117,6 +171,7 @@ struct ally_rgb_data { static struct ally_drvdata { struct hid_device *hdev; struct ally_x_device *ally_x; + struct ally_gamepad_cfg *gamepad_cfg; struct ally_rgb_dev *led_rgb_dev; struct ally_rgb_data led_rgb_data; } drvdata; @@ -148,6 +203,23 @@ static int asus_dev_set_report(struct hid_device *hdev, const u8 *buf, size_t le return ret; } +/** + * asus_dev_get_report - send get report request to device. + * + * @hdev: hid device + * @out: buffer to write output data in to + * @len: length the output buffer provided + * + * Return: count of data transferred, negative if error + * + * Same behavior as hid_hw_raw_request. + */ +static int asus_dev_get_report(struct hid_device *hdev, u8 *out, size_t len) +{ + return hid_hw_raw_request(hdev, FEATURE_REPORT_ID, out, len, + HID_FEATURE_REPORT, HID_REQ_GET_REPORT); +} + static u8 get_endpoint_address(struct hid_device *hdev) { struct usb_interface *intf; @@ -165,6 +237,172 @@ static u8 get_endpoint_address(struct hid_device *hdev) return -ENODEV; } +/**************************************************************************************************/ +/* ROG Ally gamepad configuration */ +/**************************************************************************************************/ + +/* This should be called before any attempts to set device functions */ +static int ally_gamepad_check_ready(struct hid_device *hdev) +{ + int ret, count; + u8 *hidbuf; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + ret = 0; + for (count = 0; count < READY_MAX_TRIES; count++) { + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_check_ready; + hidbuf[3] = 01; + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + hid_dbg(hdev, "ROG Ally check failed set report: %d\n", ret); + + hidbuf[0] = hidbuf[1] = hidbuf[2] = hidbuf[3] = 0; + ret = asus_dev_get_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + hid_dbg(hdev, "ROG Ally check failed get report: %d\n", ret); + + ret = hidbuf[2] == xpad_cmd_check_ready; + if (ret) + break; + usleep_range( + 1000, + 2000); /* don't spam the entire loop in less than USB response time */ + } + + if (count == READY_MAX_TRIES) + hid_warn(hdev, "ROG Ally never responded with a ready\n"); + + kfree(hidbuf); + return ret; +} + +/* A HID packet conatins mappings for two buttons: btn1, btn1_macro, btn2, btn2_macro */ +static void _btn_pair_to_hid_pkt(struct ally_gamepad_cfg *ally_cfg, + enum btn_pair_index pair, + struct btn_data *btn1, struct btn_data *btn2, + u8 *out, int out_len) +{ + int start = 5; + + out[0] = FEATURE_ROG_ALLY_REPORT_ID; + out[1] = FEATURE_ROG_ALLY_CODE_PAGE; + out[2] = xpad_cmd_set_mapping; + out[3] = pair; + out[4] = xpad_cmd_len_mapping; + + btn_code_to_byte_array(btn1->button, &out[start]); + start += BTN_DATA_LEN; + btn_code_to_byte_array(btn1->macro, &out[start]); + start += BTN_DATA_LEN; + btn_code_to_byte_array(btn2->button, &out[start]); + start += BTN_DATA_LEN; + btn_code_to_byte_array(btn2->macro, &out[start]); + //print_hex_dump(KERN_DEBUG, "byte_array: ", DUMP_PREFIX_OFFSET, 64, 1, out, 64, false); +} + +/* Apply the mapping pair to the device */ +static int _gamepad_apply_btn_pair(struct hid_device *hdev, struct ally_gamepad_cfg *ally_cfg, + enum btn_pair_index btn_pair) +{ + u8 mode = ally_cfg->mode - 1; + struct btn_data *btn1, *btn2; + u8 *hidbuf; + int ret; + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + return ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + switch (btn_pair) { + case btn_pair_m1_m2: + btn1 = &ally_cfg->key_mapping[mode]->btn_m1; + btn2 = &ally_cfg->key_mapping[mode]->btn_m2; + break; + default: + break; + } + + _btn_pair_to_hid_pkt(ally_cfg, btn_pair, btn1, btn2, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + + kfree(hidbuf); + + return ret; +} + +static struct ally_gamepad_cfg *ally_gamepad_cfg_create(struct hid_device *hdev) +{ + struct ally_gamepad_cfg *ally_cfg; + struct input_dev *input_dev; + int err; + + ally_cfg = devm_kzalloc(&hdev->dev, sizeof(*ally_cfg), GFP_KERNEL); + if (!ally_cfg) + return ERR_PTR(-ENOMEM); + ally_cfg->hdev = hdev; + // Allocate memory for each mode's `btn_mapping` + ally_cfg->mode = xpad_mode_game; + for (int i = 0; i < xpad_mode_mouse; i++) { + ally_cfg->key_mapping[i] = devm_kzalloc(&hdev->dev, sizeof(struct btn_mapping), GFP_KERNEL); + if (!ally_cfg->key_mapping[i]) { + err = -ENOMEM; + goto free_key_mappings; + } + } + + input_dev = devm_input_allocate_device(&hdev->dev); + if (!input_dev) { + err = -ENOMEM; + goto free_ally_cfg; + } + + 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 Config"; + input_set_capability(input_dev, EV_KEY, KEY_PROG1); + input_set_capability(input_dev, EV_KEY, KEY_F16); + input_set_capability(input_dev, EV_KEY, KEY_F17); + input_set_capability(input_dev, EV_KEY, KEY_F18); + input_set_drvdata(input_dev, hdev); + + err = input_register_device(input_dev); + if (err) + goto free_input_dev; + ally_cfg->input = input_dev; + + /* ignore all errors for this as they are related to USB HID I/O */ + ally_cfg->key_mapping[ally_cfg->mode - 1]->btn_m1.button = BTN_KB_M1; + ally_cfg->key_mapping[ally_cfg->mode - 1]->btn_m2.button = BTN_KB_M2; + _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_m1_m2); + + return ally_cfg; + +free_input_dev: + devm_kfree(&hdev->dev, input_dev); + +free_key_mappings: + for (int i = 0; i < xpad_mode_mouse; i++) { + if (ally_cfg->key_mapping[i]) + devm_kfree(&hdev->dev, ally_cfg->key_mapping[i]); + } + +free_ally_cfg: + devm_kfree(&hdev->dev, ally_cfg); + return ERR_PTR(err); +} + /**************************************************************************************************/ /* ROG Ally gamepad i/o and force-feedback */ /**************************************************************************************************/ @@ -721,6 +959,7 @@ static void ally_rgb_remove(struct hid_device *hdev) static int ally_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) { + struct ally_gamepad_cfg *cfg = drvdata.gamepad_cfg; struct ally_x_device *ally_x = drvdata.ally_x; if (ally_x) { @@ -733,6 +972,14 @@ static int ally_raw_event(struct hid_device *hdev, struct hid_report *report, u8 } } + if (cfg && !ally_x) { + input_report_key(cfg->input, KEY_PROG1, data[1] == 0x38); + input_report_key(cfg->input, KEY_F16, data[1] == 0xA6); + input_report_key(cfg->input, KEY_F17, data[1] == 0xA7); + input_report_key(cfg->input, KEY_F18, data[1] == 0xA8); + input_sync(cfg->input); + } + return 0; } @@ -804,7 +1051,13 @@ static int ally_hid_probe(struct hid_device *hdev, const struct hid_device_id *_ else hid_info(hdev, "Created Ally RGB LED controls.\n"); - if (IS_ERR(drvdata.led_rgb_dev)) + drvdata.gamepad_cfg = ally_gamepad_cfg_create(hdev); + if (IS_ERR(drvdata.gamepad_cfg)) + hid_err(hdev, "Failed to create Ally gamepad attributes.\n"); + else + hid_info(hdev, "Created Ally gamepad attributes.\n"); + + if (IS_ERR(drvdata.led_rgb_dev) && IS_ERR(drvdata.gamepad_cfg)) goto err_close; } @@ -817,6 +1070,12 @@ static int ally_hid_probe(struct hid_device *hdev, const struct hid_device_id *_ goto err_close; } hid_info(hdev, "Created Ally X controller.\n"); + + // Not required since we send this inputs ep through the gamepad input dev + if (drvdata.gamepad_cfg && drvdata.gamepad_cfg->input) { + input_unregister_device(drvdata.gamepad_cfg->input); + hid_info(hdev, "Ally X removed unrequired input dev.\n"); + } } return 0; @@ -840,6 +1099,21 @@ static void ally_hid_remove(struct hid_device *hdev) hid_hw_stop(hdev); } +static int ally_hid_resume(struct hid_device *hdev) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + int err; + + if (!ally_cfg) + return 0; + + err = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_m1_m2); + if (err) + return err; + + return 0; +} + static int ally_hid_reset_resume(struct hid_device *hdev) { int ep = get_endpoint_address(hdev); @@ -849,7 +1123,7 @@ static int ally_hid_reset_resume(struct hid_device *hdev) ally_hid_init(hdev); ally_rgb_resume(); - return 0; + return ally_hid_resume(hdev); } static int ally_pm_thaw(struct device *dev) @@ -881,6 +1155,8 @@ static struct hid_driver rog_ally_cfg = { .name = "asus_rog_ally", .probe = ally_hid_probe, .remove = ally_hid_remove, .raw_event = ally_raw_event, + /* HID is the better place for resume functions, not pm_ops */ + .resume = ally_hid_resume, /* ALLy 1 requires this to reset device state correctly */ .reset_resume = ally_hid_reset_resume, .driver = { diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h index 458d02996bca3..2b298ad4da0e8 100644 --- a/drivers/hid/hid-asus-ally.h +++ b/drivers/hid/hid-asus-ally.h @@ -8,35 +8,41 @@ #include #include +/* + * the xpad_mode is used inside the mode setting packet and is used + * for indexing (xpad_mode - 1) + */ +enum xpad_mode { + xpad_mode_game = 0x01, + xpad_mode_wasd = 0x02, + xpad_mode_mouse = 0x03, +}; + /* the xpad_cmd determines which feature is set or queried */ enum xpad_cmd { - xpad_cmd_set_mode = 0x01, xpad_cmd_set_mapping = 0x02, - xpad_cmd_set_js_dz = 0x04, /* deadzones */ - xpad_cmd_set_tr_dz = 0x05, /* deadzones */ - xpad_cmd_set_vibe_intensity = 0x06, xpad_cmd_set_leds = 0x08, xpad_cmd_check_ready = 0x0A, - xpad_cmd_set_calibration = 0x0D, - xpad_cmd_set_turbo = 0x0F, - xpad_cmd_set_response_curve = 0x13, - xpad_cmd_set_adz = 0x18, }; /* the xpad_cmd determines which feature is set or queried */ enum xpad_cmd_len { - xpad_cmd_len_mode = 0x01, xpad_cmd_len_mapping = 0x2c, - xpad_cmd_len_deadzone = 0x04, - xpad_cmd_len_vibe_intensity = 0x02, xpad_cmd_len_leds = 0x0C, - xpad_cmd_len_calibration2 = 0x01, - xpad_cmd_len_calibration3 = 0x01, - xpad_cmd_len_turbo = 0x20, - xpad_cmd_len_response_curve = 0x09, - xpad_cmd_len_adz = 0x02, }; +/* Values correspond to the actual HID byte value required */ +enum btn_pair_index { + btn_pair_m1_m2 = 0x08, +}; + +#define BTN_KB_M2 0x02008E0000000000 +#define BTN_KB_M1 0x02008F0000000000 + +#define ALLY_DEVICE_ATTR_WO(_name, _sysfs_name) \ + struct device_attribute dev_attr_##_name = \ + __ATTR(_sysfs_name, 0200, NULL, _name##_store) + /* required so we can have nested attributes with same name but different functions */ #define ALLY_DEVICE_ATTR_RW(_name, _sysfs_name) \ struct device_attribute dev_attr_##_name = \ From d512078754c7e5701277247e3eff4edb3ac9bad0 Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Sat, 5 Oct 2024 14:58:33 +1300 Subject: [PATCH 60/73] [FOR-UPSTREAM] hid-asus-ally: add button remap attributes Add the full set of button remapping abilities for plain remap and macro remap (hold one button, press another for macro mapped action). Signed-off-by: Luke D. Jones --- drivers/hid/hid-asus-ally.c | 395 ++++++++++++++++++++++++++++++++++-- drivers/hid/hid-asus-ally.h | 211 +++++++++++++++++++ 2 files changed, 587 insertions(+), 19 deletions(-) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index d2679f5f124a7..d2234d72312c5 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -54,6 +54,152 @@ struct btn_code_map { const char *name; }; +static const struct btn_code_map ally_btn_codes[] = { + { 0, "NONE" }, + /* Gamepad button codes */ + { BTN_PAD_A, "PAD_A" }, + { BTN_PAD_B, "PAD_B" }, + { BTN_PAD_X, "PAD_X" }, + { BTN_PAD_Y, "PAD_Y" }, + { BTN_PAD_LB, "PAD_LB" }, + { BTN_PAD_RB, "PAD_RB" }, + { BTN_PAD_LS, "PAD_LS" }, + { BTN_PAD_RS, "PAD_RS" }, + { BTN_PAD_DPAD_UP, "PAD_DPAD_UP" }, + { BTN_PAD_DPAD_DOWN, "PAD_DPAD_DOWN" }, + { BTN_PAD_DPAD_LEFT, "PAD_DPAD_LEFT" }, + { BTN_PAD_DPAD_RIGHT, "PAD_DPAD_RIGHT" }, + { BTN_PAD_VIEW, "PAD_VIEW" }, + { BTN_PAD_MENU, "PAD_MENU" }, + { BTN_PAD_XBOX, "PAD_XBOX" }, + + /* Triggers mapped to keyboard codes */ + { BTN_KB_M2, "KB_M2" }, + { BTN_KB_M1, "KB_M1" }, + { BTN_KB_ESC, "KB_ESC" }, + { BTN_KB_F1, "KB_F1" }, + { BTN_KB_F2, "KB_F2" }, + { BTN_KB_F3, "KB_F3" }, + { BTN_KB_F4, "KB_F4" }, + { BTN_KB_F5, "KB_F5" }, + { BTN_KB_F6, "KB_F6" }, + { BTN_KB_F7, "KB_F7" }, + { BTN_KB_F8, "KB_F8" }, + { BTN_KB_F9, "KB_F9" }, + { BTN_KB_F10, "KB_F10" }, + { BTN_KB_F11, "KB_F11" }, + { BTN_KB_F12, "KB_F12" }, + { BTN_KB_F14, "KB_F14" }, + { BTN_KB_F15, "KB_F15" }, + { BTN_KB_BACKTICK, "KB_BACKTICK" }, + { BTN_KB_1, "KB_1" }, + { BTN_KB_2, "KB_2" }, + { BTN_KB_3, "KB_3" }, + { BTN_KB_4, "KB_4" }, + { BTN_KB_5, "KB_5" }, + { BTN_KB_6, "KB_6" }, + { BTN_KB_7, "KB_7" }, + { BTN_KB_8, "KB_8" }, + { BTN_KB_9, "KB_9" }, + { BTN_KB_0, "KB_0" }, + { BTN_KB_HYPHEN, "KB_HYPHEN" }, + { BTN_KB_EQUALS, "KB_EQUALS" }, + { BTN_KB_BACKSPACE, "KB_BACKSPACE" }, + { BTN_KB_TAB, "KB_TAB" }, + { BTN_KB_Q, "KB_Q" }, + { BTN_KB_W, "KB_W" }, + { BTN_KB_E, "KB_E" }, + { BTN_KB_R, "KB_R" }, + { BTN_KB_T, "KB_T" }, + { BTN_KB_Y, "KB_Y" }, + { BTN_KB_U, "KB_U" }, + { BTN_KB_O, "KB_O" }, + { BTN_KB_P, "KB_P" }, + { BTN_KB_LBRACKET, "KB_LBRACKET" }, + { BTN_KB_RBRACKET, "KB_RBRACKET" }, + { BTN_KB_BACKSLASH, "KB_BACKSLASH" }, + { BTN_KB_CAPS, "KB_CAPS" }, + { BTN_KB_A, "KB_A" }, + { BTN_KB_S, "KB_S" }, + { BTN_KB_D, "KB_D" }, + { BTN_KB_F, "KB_F" }, + { BTN_KB_G, "KB_G" }, + { BTN_KB_H, "KB_H" }, + { BTN_KB_J, "KB_J" }, + { BTN_KB_K, "KB_K" }, + { BTN_KB_L, "KB_L" }, + { BTN_KB_SEMI, "KB_SEMI" }, + { BTN_KB_QUOTE, "KB_QUOTE" }, + { BTN_KB_RET, "KB_RET" }, + { BTN_KB_LSHIFT, "KB_LSHIFT" }, + { BTN_KB_Z, "KB_Z" }, + { BTN_KB_X, "KB_X" }, + { BTN_KB_C, "KB_C" }, + { BTN_KB_V, "KB_V" }, + { BTN_KB_B, "KB_B" }, + { BTN_KB_N, "KB_N" }, + { BTN_KB_M, "KB_M" }, + { BTN_KB_COMMA, "KB_COMMA" }, + { BTN_KB_PERIOD, "KB_PERIOD" }, + { BTN_KB_RSHIFT, "KB_RSHIFT" }, + { BTN_KB_LCTL, "KB_LCTL" }, + { BTN_KB_META, "KB_META" }, + { BTN_KB_LALT, "KB_LALT" }, + { BTN_KB_SPACE, "KB_SPACE" }, + { BTN_KB_RALT, "KB_RALT" }, + { BTN_KB_MENU, "KB_MENU" }, + { BTN_KB_RCTL, "KB_RCTL" }, + { BTN_KB_PRNTSCN, "KB_PRNTSCN" }, + { BTN_KB_SCRLCK, "KB_SCRLCK" }, + { BTN_KB_PAUSE, "KB_PAUSE" }, + { BTN_KB_INS, "KB_INS" }, + { BTN_KB_HOME, "KB_HOME" }, + { BTN_KB_PGUP, "KB_PGUP" }, + { BTN_KB_DEL, "KB_DEL" }, + { BTN_KB_END, "KB_END" }, + { BTN_KB_PGDWN, "KB_PGDWN" }, + { BTN_KB_UP_ARROW, "KB_UP_ARROW" }, + { BTN_KB_DOWN_ARROW, "KB_DOWN_ARROW" }, + { BTN_KB_LEFT_ARROW, "KB_LEFT_ARROW" }, + { BTN_KB_RIGHT_ARROW, "KB_RIGHT_ARROW" }, + + /* Numpad mappings */ + { BTN_NUMPAD_LOCK, "NUMPAD_LOCK" }, + { BTN_NUMPAD_FWDSLASH, "NUMPAD_FWDSLASH" }, + { BTN_NUMPAD_ASTERISK, "NUMPAD_ASTERISK" }, + { BTN_NUMPAD_HYPHEN, "NUMPAD_HYPHEN" }, + { BTN_NUMPAD_0, "NUMPAD_0" }, + { BTN_NUMPAD_1, "NUMPAD_1" }, + { BTN_NUMPAD_2, "NUMPAD_2" }, + { BTN_NUMPAD_3, "NUMPAD_3" }, + { BTN_NUMPAD_4, "NUMPAD_4" }, + { BTN_NUMPAD_5, "NUMPAD_5" }, + { BTN_NUMPAD_6, "NUMPAD_6" }, + { BTN_NUMPAD_7, "NUMPAD_7" }, + { BTN_NUMPAD_8, "NUMPAD_8" }, + { BTN_NUMPAD_9, "NUMPAD_9" }, + { BTN_NUMPAD_PLUS, "NUMPAD_PLUS" }, + { BTN_NUMPAD_ENTER, "NUMPAD_ENTER" }, + { BTN_NUMPAD_PERIOD, "NUMPAD_PERIOD" }, + + /* Mouse mappings */ + { BTN_MOUSE_LCLICK, "MOUSE_LCLICK" }, + { BTN_MOUSE_RCLICK, "MOUSE_RCLICK" }, + { BTN_MOUSE_MCLICK, "MOUSE_MCLICK" }, + { BTN_MOUSE_WHEEL_UP, "MOUSE_WHEEL_UP" }, + { BTN_MOUSE_WHEEL_DOWN, "MOUSE_WHEEL_DOWN" }, + + /* Media mappings */ + { BTN_MEDIA_SCREENSHOT, "MEDIA_SCREENSHOT" }, + { BTN_MEDIA_SHOW_KEYBOARD, "MEDIA_SHOW_KEYBOARD" }, + { BTN_MEDIA_SHOW_DESKTOP, "MEDIA_SHOW_DESKTOP" }, + { BTN_MEDIA_START_RECORDING, "MEDIA_START_RECORDING" }, + { BTN_MEDIA_MIC_OFF, "MEDIA_MIC_OFF" }, + { BTN_MEDIA_VOL_DOWN, "MEDIA_VOL_DOWN" }, + { BTN_MEDIA_VOL_UP, "MEDIA_VOL_UP" }, +}; +static const size_t keymap_len = ARRAY_SIZE(ally_btn_codes); + /* byte_array must be >= 8 in length */ static void btn_code_to_byte_array(u64 keycode, u8 *byte_array) { @@ -63,6 +209,27 @@ static void btn_code_to_byte_array(u64 keycode, u8 *byte_array) } } +static u64 name_to_btn(const char *name) +{ + int len = strcspn(name, "\n"); + for (size_t i = 0; i < keymap_len; ++i) { + if (strncmp(ally_btn_codes[i].name, name, len) == 0) { + return ally_btn_codes[i].code; + } + } + return -EINVAL; +} + +static const char* btn_to_name(u64 key) +{ + for (size_t i = 0; i < keymap_len; ++i) { + if (ally_btn_codes[i].code == key) { + return ally_btn_codes[i].name; + } + } + return NULL; +} + struct btn_data { u64 button; u64 macro; @@ -98,7 +265,7 @@ struct ally_gamepad_cfg { /* * index: [mode] */ - struct btn_mapping *key_mapping[xpad_mode_mouse]; + struct btn_mapping key_mapping[xpad_mode_mouse]; }; /* The hatswitch outputs integers, we use them to index this X|Y pair */ @@ -323,9 +490,41 @@ static int _gamepad_apply_btn_pair(struct hid_device *hdev, struct ally_gamepad_ return -ENOMEM; switch (btn_pair) { + case btn_pair_dpad_u_d: + btn1 = &ally_cfg->key_mapping[mode].dpad_up; + btn2 = &ally_cfg->key_mapping[mode].dpad_down; + break; + case btn_pair_dpad_l_r: + btn1 = &ally_cfg->key_mapping[mode].dpad_left; + btn2 = &ally_cfg->key_mapping[mode].dpad_right; + break; + case btn_pair_ls_rs: + btn1 = &ally_cfg->key_mapping[mode].btn_ls; + btn2 = &ally_cfg->key_mapping[mode].btn_rs; + break; + case btn_pair_lb_rb: + btn1 = &ally_cfg->key_mapping[mode].btn_lb; + btn2 = &ally_cfg->key_mapping[mode].btn_rb; + break; + case btn_pair_lt_rt: + btn1 = &ally_cfg->key_mapping[mode].btn_lt; + btn2 = &ally_cfg->key_mapping[mode].btn_rt; + break; + case btn_pair_a_b: + btn1 = &ally_cfg->key_mapping[mode].btn_a; + btn2 = &ally_cfg->key_mapping[mode].btn_b; + break; + case btn_pair_x_y: + btn1 = &ally_cfg->key_mapping[mode].btn_x; + btn2 = &ally_cfg->key_mapping[mode].btn_y; + break; + case btn_pair_view_menu: + btn1 = &ally_cfg->key_mapping[mode].btn_view; + btn2 = &ally_cfg->key_mapping[mode].btn_menu; + break; case btn_pair_m1_m2: - btn1 = &ally_cfg->key_mapping[mode]->btn_m1; - btn2 = &ally_cfg->key_mapping[mode]->btn_m2; + btn1 = &ally_cfg->key_mapping[mode].btn_m1; + btn2 = &ally_cfg->key_mapping[mode].btn_m2; break; default: break; @@ -339,6 +538,157 @@ static int _gamepad_apply_btn_pair(struct hid_device *hdev, struct ally_gamepad_ return ret; } +static ssize_t _gamepad_apply_all(struct hid_device *hdev, struct ally_gamepad_cfg *ally_cfg) +{ + int ret; + + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_dpad_u_d); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_dpad_l_r); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_ls_rs); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_lb_rb); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_a_b); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_x_y); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_view_menu); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_m1_m2); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_lt_rt); + if (ret < 0) + return ret; + + return 0; +} + +static ssize_t gamepad_apply_all_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + struct hid_device *hdev = to_hid_device(dev); + int ret; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + ret = _gamepad_apply_all(hdev, ally_cfg); + if (ret < 0) + return ret; + + return count; +} +ALLY_DEVICE_ATTR_WO(gamepad_apply_all, apply_all); + +/* button map attributes, regular and macro*/ +ALLY_BTN_MAPPING(m1, btn_m1); +ALLY_BTN_MAPPING(m2, btn_m2); +ALLY_BTN_MAPPING(a, btn_a); +ALLY_BTN_MAPPING(b, btn_b); +ALLY_BTN_MAPPING(x, btn_x); +ALLY_BTN_MAPPING(y, btn_y); +ALLY_BTN_MAPPING(lb, btn_lb); +ALLY_BTN_MAPPING(rb, btn_rb); +ALLY_BTN_MAPPING(ls, btn_ls); +ALLY_BTN_MAPPING(rs, btn_rs); +ALLY_BTN_MAPPING(lt, btn_lt); +ALLY_BTN_MAPPING(rt, btn_rt); +ALLY_BTN_MAPPING(dpad_u, dpad_up); +ALLY_BTN_MAPPING(dpad_d, dpad_down); +ALLY_BTN_MAPPING(dpad_l, dpad_left); +ALLY_BTN_MAPPING(dpad_r, dpad_right); +ALLY_BTN_MAPPING(view, btn_view); +ALLY_BTN_MAPPING(menu, btn_menu); + +static void _gamepad_set_xpad_default(struct ally_gamepad_cfg *ally_cfg) +{ + struct btn_mapping *map = &ally_cfg->key_mapping[ally_cfg->mode - 1]; + map->btn_m1.button = BTN_KB_M1; + map->btn_m2.button = BTN_KB_M2; + map->btn_a.button = BTN_PAD_A; + map->btn_b.button = BTN_PAD_B; + map->btn_x.button = BTN_PAD_X; + map->btn_y.button = BTN_PAD_Y; + map->btn_lb.button = BTN_PAD_LB; + map->btn_rb.button = BTN_PAD_RB; + map->btn_lt.button = BTN_PAD_LT; + map->btn_rt.button = BTN_PAD_RT; + map->btn_ls.button = BTN_PAD_LS; + map->btn_rs.button = BTN_PAD_RS; + map->dpad_up.button = BTN_PAD_DPAD_UP; + map->dpad_down.button = BTN_PAD_DPAD_DOWN; + map->dpad_left.button = BTN_PAD_DPAD_LEFT; + map->dpad_right.button = BTN_PAD_DPAD_RIGHT; + map->btn_view.button = BTN_PAD_VIEW; + map->btn_menu.button = BTN_PAD_MENU; +} + +static ssize_t btn_mapping_reset_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + switch (ally_cfg->mode) { + case xpad_mode_game: + _gamepad_set_xpad_default(ally_cfg); + break; + default: + _gamepad_set_xpad_default(ally_cfg); + break; + } + + return count; +} +ALLY_DEVICE_ATTR_WO(btn_mapping_reset, reset_btn_mapping); + +/* ROOT LEVEL ATTRS *******************************************************************************/ +static struct attribute *gamepad_device_attrs[] = { + &dev_attr_btn_mapping_reset.attr, + &dev_attr_gamepad_apply_all.attr, + NULL +}; + +static const struct attribute_group ally_controller_attr_group = { + .attrs = gamepad_device_attrs, +}; + +static const struct attribute_group *gamepad_device_attr_groups[] = { + &ally_controller_attr_group, + &btn_mapping_m1_attr_group, + &btn_mapping_m2_attr_group, + &btn_mapping_a_attr_group, + &btn_mapping_b_attr_group, + &btn_mapping_x_attr_group, + &btn_mapping_y_attr_group, + &btn_mapping_lb_attr_group, + &btn_mapping_rb_attr_group, + &btn_mapping_ls_attr_group, + &btn_mapping_rs_attr_group, + &btn_mapping_lt_attr_group, + &btn_mapping_rt_attr_group, + &btn_mapping_dpad_u_attr_group, + &btn_mapping_dpad_d_attr_group, + &btn_mapping_dpad_l_attr_group, + &btn_mapping_dpad_r_attr_group, + &btn_mapping_view_attr_group, + &btn_mapping_menu_attr_group, + NULL, +}; + static struct ally_gamepad_cfg *ally_gamepad_cfg_create(struct hid_device *hdev) { struct ally_gamepad_cfg *ally_cfg; @@ -351,13 +701,6 @@ static struct ally_gamepad_cfg *ally_gamepad_cfg_create(struct hid_device *hdev) ally_cfg->hdev = hdev; // Allocate memory for each mode's `btn_mapping` ally_cfg->mode = xpad_mode_game; - for (int i = 0; i < xpad_mode_mouse; i++) { - ally_cfg->key_mapping[i] = devm_kzalloc(&hdev->dev, sizeof(struct btn_mapping), GFP_KERNEL); - if (!ally_cfg->key_mapping[i]) { - err = -ENOMEM; - goto free_key_mappings; - } - } input_dev = devm_input_allocate_device(&hdev->dev); if (!input_dev) { @@ -383,26 +726,37 @@ static struct ally_gamepad_cfg *ally_gamepad_cfg_create(struct hid_device *hdev) ally_cfg->input = input_dev; /* ignore all errors for this as they are related to USB HID I/O */ - ally_cfg->key_mapping[ally_cfg->mode - 1]->btn_m1.button = BTN_KB_M1; - ally_cfg->key_mapping[ally_cfg->mode - 1]->btn_m2.button = BTN_KB_M2; + _gamepad_set_xpad_default(ally_cfg); + ally_cfg->key_mapping[ally_cfg->mode - 1].btn_m1.button = BTN_KB_M1; + ally_cfg->key_mapping[ally_cfg->mode - 1].btn_m2.button = BTN_KB_M2; _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_m1_m2); + drvdata.gamepad_cfg = ally_cfg; // Must asign before attr group setup + if (sysfs_create_groups(&hdev->dev.kobj, gamepad_device_attr_groups)) { + err = -ENODEV; + goto unregister_input_dev; + } + return ally_cfg; +unregister_input_dev: + input_unregister_device(input_dev); + ally_cfg->input = NULL; // Prevent double free when kfree(ally_cfg) happens + free_input_dev: devm_kfree(&hdev->dev, input_dev); -free_key_mappings: - for (int i = 0; i < xpad_mode_mouse; i++) { - if (ally_cfg->key_mapping[i]) - devm_kfree(&hdev->dev, ally_cfg->key_mapping[i]); - } - free_ally_cfg: devm_kfree(&hdev->dev, ally_cfg); return ERR_PTR(err); } +static void ally_cfg_remove(struct hid_device *hdev) +{ + // __gamepad_set_mode(hdev, drvdata.gamepad_cfg, xpad_mode_mouse); + sysfs_remove_groups(&hdev->dev.kobj, gamepad_device_attr_groups); +} + /**************************************************************************************************/ /* ROG Ally gamepad i/o and force-feedback */ /**************************************************************************************************/ @@ -1095,6 +1449,9 @@ static void ally_hid_remove(struct hid_device *hdev) if (drvdata.ally_x) ally_x_remove(hdev); + if (drvdata.gamepad_cfg) + ally_cfg_remove(hdev); + hid_hw_close(hdev); hid_hw_stop(hdev); } @@ -1107,7 +1464,7 @@ static int ally_hid_resume(struct hid_device *hdev) if (!ally_cfg) return 0; - err = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_m1_m2); + err = _gamepad_apply_all(hdev, ally_cfg); if (err) return err; diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h index 2b298ad4da0e8..f985cbd698c33 100644 --- a/drivers/hid/hid-asus-ally.h +++ b/drivers/hid/hid-asus-ally.h @@ -33,11 +33,155 @@ enum xpad_cmd_len { /* Values correspond to the actual HID byte value required */ enum btn_pair_index { + btn_pair_dpad_u_d = 0x01, + btn_pair_dpad_l_r = 0x02, + btn_pair_ls_rs = 0x03, + btn_pair_lb_rb = 0x04, + btn_pair_a_b = 0x05, + btn_pair_x_y = 0x06, + btn_pair_view_menu = 0x07, btn_pair_m1_m2 = 0x08, + btn_pair_lt_rt = 0x09, }; +#define BTN_PAD_A 0x0101000000000000 +#define BTN_PAD_B 0x0102000000000000 +#define BTN_PAD_X 0x0103000000000000 +#define BTN_PAD_Y 0x0104000000000000 +#define BTN_PAD_LB 0x0105000000000000 +#define BTN_PAD_RB 0x0106000000000000 +#define BTN_PAD_LS 0x0107000000000000 +#define BTN_PAD_RS 0x0108000000000000 +#define BTN_PAD_DPAD_UP 0x0109000000000000 +#define BTN_PAD_DPAD_DOWN 0x010A000000000000 +#define BTN_PAD_DPAD_LEFT 0x010B000000000000 +#define BTN_PAD_DPAD_RIGHT 0x010C000000000000 +#define BTN_PAD_LT 0x010D000000000000 +#define BTN_PAD_RT 0x010E000000000000 +#define BTN_PAD_VIEW 0x0111000000000000 +#define BTN_PAD_MENU 0x0112000000000000 +#define BTN_PAD_XBOX 0x0113000000000000 + #define BTN_KB_M2 0x02008E0000000000 #define BTN_KB_M1 0x02008F0000000000 +#define BTN_KB_ESC 0x0200760000000000 +#define BTN_KB_F1 0x0200500000000000 +#define BTN_KB_F2 0x0200600000000000 +#define BTN_KB_F3 0x0200400000000000 +#define BTN_KB_F4 0x02000C0000000000 +#define BTN_KB_F5 0x0200030000000000 +#define BTN_KB_F6 0x02000B0000000000 +#define BTN_KB_F7 0x0200800000000000 +#define BTN_KB_F8 0x02000A0000000000 +#define BTN_KB_F9 0x0200010000000000 +#define BTN_KB_F10 0x0200090000000000 +#define BTN_KB_F11 0x0200780000000000 +#define BTN_KB_F12 0x0200070000000000 +#define BTN_KB_F14 0x0200180000000000 +#define BTN_KB_F15 0x0200100000000000 +#define BTN_KB_BACKTICK 0x02000E0000000000 +#define BTN_KB_1 0x0200160000000000 +#define BTN_KB_2 0x02001E0000000000 +#define BTN_KB_3 0x0200260000000000 +#define BTN_KB_4 0x0200250000000000 +#define BTN_KB_5 0x02002E0000000000 +#define BTN_KB_6 0x0200360000000000 +#define BTN_KB_7 0x02003D0000000000 +#define BTN_KB_8 0x02003E0000000000 +#define BTN_KB_9 0x0200460000000000 +#define BTN_KB_0 0x0200450000000000 +#define BTN_KB_HYPHEN 0x02004E0000000000 +#define BTN_KB_EQUALS 0x0200550000000000 +#define BTN_KB_BACKSPACE 0x0200660000000000 +#define BTN_KB_TAB 0x02000D0000000000 +#define BTN_KB_Q 0x0200150000000000 +#define BTN_KB_W 0x02001D0000000000 +#define BTN_KB_E 0x0200240000000000 +#define BTN_KB_R 0x02002D0000000000 +#define BTN_KB_T 0x02002C0000000000 +#define BTN_KB_Y 0x0200350000000000 +#define BTN_KB_U 0x02003C0000000000 +#define BTN_KB_O 0x0200440000000000 +#define BTN_KB_P 0x02004D0000000000 +#define BTN_KB_LBRACKET 0x0200540000000000 +#define BTN_KB_RBRACKET 0x02005B0000000000 +#define BTN_KB_BACKSLASH 0x02005D0000000000 +#define BTN_KB_CAPS 0x0200580000000000 +#define BTN_KB_A 0x02001C0000000000 +#define BTN_KB_S 0x02001B0000000000 +#define BTN_KB_D 0x0200230000000000 +#define BTN_KB_F 0x02002B0000000000 +#define BTN_KB_G 0x0200340000000000 +#define BTN_KB_H 0x0200330000000000 +#define BTN_KB_J 0x02003B0000000000 +#define BTN_KB_K 0x0200420000000000 +#define BTN_KB_L 0x02004B0000000000 +#define BTN_KB_SEMI 0x02004C0000000000 +#define BTN_KB_QUOTE 0x0200520000000000 +#define BTN_KB_RET 0x02005A0000000000 +#define BTN_KB_LSHIFT 0x0200880000000000 +#define BTN_KB_Z 0x02001A0000000000 +#define BTN_KB_X 0x0200220000000000 +#define BTN_KB_C 0x0200210000000000 +#define BTN_KB_V 0x02002A0000000000 +#define BTN_KB_B 0x0200320000000000 +#define BTN_KB_N 0x0200310000000000 +#define BTN_KB_M 0x02003A0000000000 +#define BTN_KB_COMMA 0x0200410000000000 +#define BTN_KB_PERIOD 0x0200490000000000 +#define BTN_KB_RSHIFT 0x0200890000000000 +#define BTN_KB_LCTL 0x02008C0000000000 +#define BTN_KB_META 0x0200820000000000 +#define BTN_KB_LALT 0x02008A0000000000 +#define BTN_KB_SPACE 0x0200290000000000 +#define BTN_KB_RALT 0x02008B0000000000 +#define BTN_KB_MENU 0x0200840000000000 +#define BTN_KB_RCTL 0x02008D0000000000 +#define BTN_KB_PRNTSCN 0x0200C30000000000 +#define BTN_KB_SCRLCK 0x02007E0000000000 +#define BTN_KB_PAUSE 0x0200910000000000 +#define BTN_KB_INS 0x0200C20000000000 +#define BTN_KB_HOME 0x0200940000000000 +#define BTN_KB_PGUP 0x0200960000000000 +#define BTN_KB_DEL 0x0200C00000000000 +#define BTN_KB_END 0x0200950000000000 +#define BTN_KB_PGDWN 0x0200970000000000 +#define BTN_KB_UP_ARROW 0x0200980000000000 +#define BTN_KB_DOWN_ARROW 0x0200990000000000 +#define BTN_KB_LEFT_ARROW 0x0200910000000000 +#define BTN_KB_RIGHT_ARROW 0x02009B0000000000 + +#define BTN_NUMPAD_LOCK 0x0200770000000000 +#define BTN_NUMPAD_FWDSLASH 0x0200900000000000 +#define BTN_NUMPAD_ASTERISK 0x02007C0000000000 +#define BTN_NUMPAD_HYPHEN 0x02007B0000000000 +#define BTN_NUMPAD_0 0x0200700000000000 +#define BTN_NUMPAD_1 0x0200690000000000 +#define BTN_NUMPAD_2 0x0200720000000000 +#define BTN_NUMPAD_3 0x02007A0000000000 +#define BTN_NUMPAD_4 0x02006B0000000000 +#define BTN_NUMPAD_5 0x0200730000000000 +#define BTN_NUMPAD_6 0x0200740000000000 +#define BTN_NUMPAD_7 0x02006C0000000000 +#define BTN_NUMPAD_8 0x0200750000000000 +#define BTN_NUMPAD_9 0x02007D0000000000 +#define BTN_NUMPAD_PLUS 0x0200790000000000 +#define BTN_NUMPAD_ENTER 0x0200810000000000 +#define BTN_NUMPAD_PERIOD 0x0200710000000000 + +#define BTN_MOUSE_LCLICK 0x0300000001000000 +#define BTN_MOUSE_RCLICK 0x0300000002000000 +#define BTN_MOUSE_MCLICK 0x0300000003000000 +#define BTN_MOUSE_WHEEL_UP 0x0300000004000000 +#define BTN_MOUSE_WHEEL_DOWN 0x0300000005000000 + +#define BTN_MEDIA_SCREENSHOT 0x0500001600000000 +#define BTN_MEDIA_SHOW_KEYBOARD 0x0500001900000000 +#define BTN_MEDIA_SHOW_DESKTOP 0x0500001C00000000 +#define BTN_MEDIA_START_RECORDING 0x0500001E00000000 +#define BTN_MEDIA_MIC_OFF 0x0500000100000000 +#define BTN_MEDIA_VOL_DOWN 0x0500000200000000 +#define BTN_MEDIA_VOL_UP 0x0500000300000000 #define ALLY_DEVICE_ATTR_WO(_name, _sysfs_name) \ struct device_attribute dev_attr_##_name = \ @@ -47,3 +191,70 @@ enum btn_pair_index { #define ALLY_DEVICE_ATTR_RW(_name, _sysfs_name) \ struct device_attribute dev_attr_##_name = \ __ATTR(_sysfs_name, 0644, _name##_show, _name##_store) + +/* button specific macros */ +#define ALLY_BTN_SHOW(_fname, _btn_name, _secondary) \ + static ssize_t _fname##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct btn_data *btn; \ + const char* name; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + btn = &ally_cfg->key_mapping[ally_cfg->mode - 1]._btn_name; \ + name = btn_to_name(_secondary ? btn->macro : btn->button); \ + return sysfs_emit(buf, "%s\n", name); \ + } + +#define ALLY_BTN_STORE(_fname, _btn_name, _secondary) \ + static ssize_t _fname##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct btn_data *btn; \ + u64 code; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + btn = &ally_cfg->key_mapping[ally_cfg->mode - 1]._btn_name; \ + code = name_to_btn(buf); \ + if (_secondary) \ + btn->macro = code; \ + else \ + btn->button = code; \ + return count; \ + } + +#define ALLY_BTN_ATTRS_GROUP(_name, _fname) \ + static struct attribute *_fname##_attrs[] = { \ + &dev_attr_##_fname.attr, \ + &dev_attr_##_fname##_macro.attr, \ + }; \ + static const struct attribute_group _fname##_attr_group = { \ + .name = __stringify(_name), \ + .attrs = _fname##_attrs, \ + } + +#define _ALLY_BTN_REMAP(_fname, _btn_name) \ + ALLY_BTN_SHOW(btn_mapping_##_fname##_remap, _btn_name, false); \ + ALLY_BTN_STORE(btn_mapping_##_fname##_remap, _btn_name, false); \ + ALLY_DEVICE_ATTR_RW(btn_mapping_##_fname##_remap, remap); + +#define _ALLY_BTN_MACRO(_fname, _btn_name) \ + ALLY_BTN_SHOW(btn_mapping_##_fname##_macro, _btn_name, true); \ + ALLY_BTN_STORE(btn_mapping_##_fname##_macro, _btn_name, true); \ + ALLY_DEVICE_ATTR_RW(btn_mapping_##_fname##_macro, macro_remap); + +#define ALLY_BTN_MAPPING(_fname, _btn_name) \ + _ALLY_BTN_REMAP(_fname, _btn_name) \ + _ALLY_BTN_MACRO(_fname, _btn_name) \ + static struct attribute *_fname##_attrs[] = { \ + &dev_attr_btn_mapping_##_fname##_remap.attr, \ + &dev_attr_btn_mapping_##_fname##_macro.attr, \ + NULL, \ + }; \ + static const struct attribute_group btn_mapping_##_fname##_attr_group = { \ + .name = __stringify(btn_##_fname), \ + .attrs = _fname##_attrs, \ + } From 8bae185157a2c2c28c8c39f02e24012066541a85 Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Fri, 25 Oct 2024 08:56:54 +0200 Subject: [PATCH 61/73] [FOR-UPSTREAM] hid-asus-ally: add gamepad mode selection --- drivers/hid/hid-asus-ally.c | 73 +++++++++++++++++++++++++++++++++++++ drivers/hid/hid-asus-ally.h | 2 + 2 files changed, 75 insertions(+) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index d2234d72312c5..fbebe1d65080c 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -655,9 +655,82 @@ static ssize_t btn_mapping_reset_store(struct device *dev, struct device_attribu } ALLY_DEVICE_ATTR_WO(btn_mapping_reset, reset_btn_mapping); +/* GAMEPAD MODE */ +static ssize_t _gamepad_set_mode(struct hid_device *hdev, struct ally_gamepad_cfg *ally_cfg, + int val) +{ + u8 *hidbuf; + int ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_set_mode; + hidbuf[3] = xpad_cmd_len_mode; + hidbuf[4] = val; + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + goto report_fail; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto report_fail; + + ret = _gamepad_apply_all(hdev, ally_cfg); + if (ret < 0) + goto report_fail; + +report_fail: + kfree(hidbuf); + return ret; +} + +static ssize_t gamepad_mode_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + return sysfs_emit(buf, "%d\n", ally_cfg->mode); +} + +static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev = to_hid_device(dev); + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + int ret, val; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + ret = kstrtoint(buf, 0, &val); + if (ret) + return ret; + + if (val < xpad_mode_game || val > xpad_mode_mouse) + return -EINVAL; + + ally_cfg->mode = val; + + ret = _gamepad_set_mode(hdev, ally_cfg, val); + if (ret < 0) + return ret; + + return count; +} + +DEVICE_ATTR_RW(gamepad_mode); + /* ROOT LEVEL ATTRS *******************************************************************************/ static struct attribute *gamepad_device_attrs[] = { &dev_attr_btn_mapping_reset.attr, + &dev_attr_gamepad_mode.attr, &dev_attr_gamepad_apply_all.attr, NULL }; diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h index f985cbd698c33..f7e21be50d8e6 100644 --- a/drivers/hid/hid-asus-ally.h +++ b/drivers/hid/hid-asus-ally.h @@ -20,6 +20,7 @@ enum xpad_mode { /* the xpad_cmd determines which feature is set or queried */ enum xpad_cmd { + xpad_cmd_set_mode = 0x01, xpad_cmd_set_mapping = 0x02, xpad_cmd_set_leds = 0x08, xpad_cmd_check_ready = 0x0A, @@ -27,6 +28,7 @@ enum xpad_cmd { /* the xpad_cmd determines which feature is set or queried */ enum xpad_cmd_len { + xpad_cmd_len_mode = 0x01, xpad_cmd_len_mapping = 0x2c, xpad_cmd_len_leds = 0x0C, }; From c7e0f8e337825c7cd5bd5740a787abeb5b8c43ef Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Sat, 5 Oct 2024 15:40:09 +1300 Subject: [PATCH 62/73] [FOR-UPSTREAM] hid-asus-ally: Turbo settings for buttons Signed-off-by: Luke D. Jones --- drivers/hid/hid-asus-ally.c | 72 +++++++++++++++++++++++++++++-------- drivers/hid/hid-asus-ally.h | 50 ++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index fbebe1d65080c..eb9fb2bdcaeb5 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -233,6 +233,7 @@ static const char* btn_to_name(u64 key) struct btn_data { u64 button; u64 macro; + bool turbo; }; struct btn_mapping { @@ -538,6 +539,46 @@ static int _gamepad_apply_btn_pair(struct hid_device *hdev, struct ally_gamepad_ return ret; } +static int _gamepad_apply_turbo(struct hid_device *hdev, struct ally_gamepad_cfg *ally_cfg) +{ + struct btn_mapping *map = &ally_cfg->key_mapping[ally_cfg->mode - 1]; + u8 *hidbuf; + int ret; + + /* set turbo */ + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_set_turbo; + hidbuf[3] = xpad_cmd_len_turbo; + + hidbuf[4] = map->dpad_up.turbo; + hidbuf[6] = map->dpad_down.turbo; + hidbuf[8] = map->dpad_left.turbo; + hidbuf[10] = map->dpad_right.turbo; + + hidbuf[12] = map->btn_ls.turbo; + hidbuf[14] = map->btn_rs.turbo; + hidbuf[16] = map->btn_lb.turbo; + hidbuf[18] = map->btn_rb.turbo; + + hidbuf[20] = map->btn_a.turbo; + hidbuf[22] = map->btn_b.turbo; + hidbuf[24] = map->btn_x.turbo; + hidbuf[26] = map->btn_y.turbo; + + hidbuf[28] = map->btn_lt.turbo; + hidbuf[30] = map->btn_rt.turbo; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + + kfree(hidbuf); + + return ret; +} + static ssize_t _gamepad_apply_all(struct hid_device *hdev, struct ally_gamepad_cfg *ally_cfg) { int ret; @@ -567,6 +608,9 @@ static ssize_t _gamepad_apply_all(struct hid_device *hdev, struct ally_gamepad_c if (ret < 0) return ret; ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_lt_rt); + if (ret < 0) + return ret; + ret = _gamepad_apply_turbo(hdev, ally_cfg); if (ret < 0) return ret; @@ -594,22 +638,22 @@ ALLY_DEVICE_ATTR_WO(gamepad_apply_all, apply_all); /* button map attributes, regular and macro*/ ALLY_BTN_MAPPING(m1, btn_m1); ALLY_BTN_MAPPING(m2, btn_m2); -ALLY_BTN_MAPPING(a, btn_a); -ALLY_BTN_MAPPING(b, btn_b); -ALLY_BTN_MAPPING(x, btn_x); -ALLY_BTN_MAPPING(y, btn_y); -ALLY_BTN_MAPPING(lb, btn_lb); -ALLY_BTN_MAPPING(rb, btn_rb); -ALLY_BTN_MAPPING(ls, btn_ls); -ALLY_BTN_MAPPING(rs, btn_rs); -ALLY_BTN_MAPPING(lt, btn_lt); -ALLY_BTN_MAPPING(rt, btn_rt); -ALLY_BTN_MAPPING(dpad_u, dpad_up); -ALLY_BTN_MAPPING(dpad_d, dpad_down); -ALLY_BTN_MAPPING(dpad_l, dpad_left); -ALLY_BTN_MAPPING(dpad_r, dpad_right); ALLY_BTN_MAPPING(view, btn_view); ALLY_BTN_MAPPING(menu, btn_menu); +ALLY_TURBO_BTN_MAPPING(a, btn_a); +ALLY_TURBO_BTN_MAPPING(b, btn_b); +ALLY_TURBO_BTN_MAPPING(x, btn_x); +ALLY_TURBO_BTN_MAPPING(y, btn_y); +ALLY_TURBO_BTN_MAPPING(lb, btn_lb); +ALLY_TURBO_BTN_MAPPING(rb, btn_rb); +ALLY_TURBO_BTN_MAPPING(ls, btn_ls); +ALLY_TURBO_BTN_MAPPING(rs, btn_rs); +ALLY_TURBO_BTN_MAPPING(lt, btn_lt); +ALLY_TURBO_BTN_MAPPING(rt, btn_rt); +ALLY_TURBO_BTN_MAPPING(dpad_u, dpad_up); +ALLY_TURBO_BTN_MAPPING(dpad_d, dpad_down); +ALLY_TURBO_BTN_MAPPING(dpad_l, dpad_left); +ALLY_TURBO_BTN_MAPPING(dpad_r, dpad_right); static void _gamepad_set_xpad_default(struct ally_gamepad_cfg *ally_cfg) { diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h index f7e21be50d8e6..63a3b5caa71ce 100644 --- a/drivers/hid/hid-asus-ally.h +++ b/drivers/hid/hid-asus-ally.h @@ -24,6 +24,7 @@ enum xpad_cmd { xpad_cmd_set_mapping = 0x02, xpad_cmd_set_leds = 0x08, xpad_cmd_check_ready = 0x0A, + xpad_cmd_set_turbo = 0x0F, }; /* the xpad_cmd determines which feature is set or queried */ @@ -31,6 +32,7 @@ enum xpad_cmd_len { xpad_cmd_len_mode = 0x01, xpad_cmd_len_mapping = 0x2c, xpad_cmd_len_leds = 0x0C, + xpad_cmd_len_turbo = 0x20, }; /* Values correspond to the actual HID byte value required */ @@ -228,6 +230,37 @@ enum btn_pair_index { return count; \ } +#define ALLY_TURBO_SHOW(_fname, _btn_name) \ + static ssize_t _fname##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct btn_data *btn; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + btn = &ally_cfg->key_mapping[ally_cfg->mode - 1]._btn_name; \ + return sysfs_emit(buf, "%d\n", btn->turbo); \ + } + +#define ALLY_TURBO_STORE(_fname, _btn_name) \ + static ssize_t _fname##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct btn_data *btn; \ + bool turbo; \ + int ret; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + btn = &ally_cfg->key_mapping[ally_cfg->mode - 1]._btn_name; \ + ret = kstrtobool(buf, &turbo); \ + if (ret) \ + return ret; \ + btn->turbo = turbo; \ + return count; \ + } + #define ALLY_BTN_ATTRS_GROUP(_name, _fname) \ static struct attribute *_fname##_attrs[] = { \ &dev_attr_##_fname.attr, \ @@ -260,3 +293,20 @@ enum btn_pair_index { .name = __stringify(btn_##_fname), \ .attrs = _fname##_attrs, \ } + +#define ALLY_TURBO_BTN_MAPPING(_fname, _btn_name) \ + _ALLY_BTN_REMAP(_fname, _btn_name) \ + _ALLY_BTN_MACRO(_fname, _btn_name) \ + ALLY_TURBO_SHOW(btn_mapping_##_fname##_turbo, _btn_name); \ + ALLY_TURBO_STORE(btn_mapping_##_fname##_turbo, _btn_name); \ + ALLY_DEVICE_ATTR_RW(btn_mapping_##_fname##_turbo, turbo); \ + static struct attribute *_fname##_turbo_attrs[] = { \ + &dev_attr_btn_mapping_##_fname##_remap.attr, \ + &dev_attr_btn_mapping_##_fname##_macro.attr, \ + &dev_attr_btn_mapping_##_fname##_turbo.attr, \ + NULL, \ + }; \ + static const struct attribute_group btn_mapping_##_fname##_attr_group = { \ + .name = __stringify(btn_##_fname), \ + .attrs = _fname##_turbo_attrs, \ + } From d878c0e7d316ecfd277544d6a5d9a00d06770d7b Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Sat, 5 Oct 2024 20:46:00 +1300 Subject: [PATCH 63/73] [FOR-UPSTREAM] hid-asus-ally: add vibration intensity settings Signed-off-by: Luke D. Jones --- drivers/hid/hid-asus-ally.c | 93 +++++++++++++++++++++++++++++++++++++ drivers/hid/hid-asus-ally.h | 6 +++ 2 files changed, 99 insertions(+) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index eb9fb2bdcaeb5..35480dea92046 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -267,6 +267,11 @@ struct ally_gamepad_cfg { * index: [mode] */ struct btn_mapping key_mapping[xpad_mode_mouse]; + /* + * index: left, right + * max: 64 + */ + u8 vibration_intensity[2]; }; /* The hatswitch outputs integers, we use them to index this X|Y pair */ @@ -449,6 +454,89 @@ static int ally_gamepad_check_ready(struct hid_device *hdev) return ret; } +/* VIBRATION INTENSITY ****************************************************************************/ +static ssize_t gamepad_vibration_intensity_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "left right\n"); +} + +ALLY_DEVICE_ATTR_RO(gamepad_vibration_intensity_index, vibration_intensity_index); + +static ssize_t _gamepad_apply_intensity(struct hid_device *hdev, + struct ally_gamepad_cfg *ally_cfg) +{ + u8 *hidbuf; + int ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_set_vibe_intensity; + hidbuf[3] = xpad_cmd_len_vibe_intensity; + hidbuf[4] = ally_cfg->vibration_intensity[0]; + hidbuf[5] = ally_cfg->vibration_intensity[1]; + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + goto report_fail; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto report_fail; + +report_fail: + kfree(hidbuf); + return ret; +} + +static ssize_t gamepad_vibration_intensity_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + return sysfs_emit( + buf, "%d %d\n", + ally_cfg->vibration_intensity[0], + ally_cfg->vibration_intensity[1]); +} + +static ssize_t gamepad_vibration_intensity_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct hid_device *hdev = to_hid_device(dev); + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + u32 left, right; + int ret; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + if (sscanf(buf, "%d %d", &left, &right) != 2) + return -EINVAL; + + if (left > 64 || right > 64) + return -EINVAL; + + ally_cfg->vibration_intensity[0] = left; + ally_cfg->vibration_intensity[1] = right; + + ret = _gamepad_apply_intensity(hdev, ally_cfg); + if (ret < 0) + return ret; + + return count; +} + +ALLY_DEVICE_ATTR_RW(gamepad_vibration_intensity, vibration_intensity); + /* A HID packet conatins mappings for two buttons: btn1, btn1_macro, btn2, btn2_macro */ static void _btn_pair_to_hid_pkt(struct ally_gamepad_cfg *ally_cfg, enum btn_pair_index pair, @@ -776,6 +864,8 @@ static struct attribute *gamepad_device_attrs[] = { &dev_attr_btn_mapping_reset.attr, &dev_attr_gamepad_mode.attr, &dev_attr_gamepad_apply_all.attr, + &dev_attr_gamepad_vibration_intensity.attr, + &dev_attr_gamepad_vibration_intensity_index.attr, NULL }; @@ -848,6 +938,9 @@ static struct ally_gamepad_cfg *ally_gamepad_cfg_create(struct hid_device *hdev) ally_cfg->key_mapping[ally_cfg->mode - 1].btn_m2.button = BTN_KB_M2; _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_m1_m2); + ally_cfg->vibration_intensity[0] = 0x64; + ally_cfg->vibration_intensity[1] = 0x64; + drvdata.gamepad_cfg = ally_cfg; // Must asign before attr group setup if (sysfs_create_groups(&hdev->dev.kobj, gamepad_device_attr_groups)) { err = -ENODEV; diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h index 63a3b5caa71ce..6ac79ad3c5f26 100644 --- a/drivers/hid/hid-asus-ally.h +++ b/drivers/hid/hid-asus-ally.h @@ -22,6 +22,7 @@ enum xpad_mode { enum xpad_cmd { xpad_cmd_set_mode = 0x01, xpad_cmd_set_mapping = 0x02, + xpad_cmd_set_vibe_intensity = 0x06, xpad_cmd_set_leds = 0x08, xpad_cmd_check_ready = 0x0A, xpad_cmd_set_turbo = 0x0F, @@ -31,6 +32,7 @@ enum xpad_cmd { enum xpad_cmd_len { xpad_cmd_len_mode = 0x01, xpad_cmd_len_mapping = 0x2c, + xpad_cmd_len_vibe_intensity = 0x02, xpad_cmd_len_leds = 0x0C, xpad_cmd_len_turbo = 0x20, }; @@ -196,6 +198,10 @@ enum btn_pair_index { struct device_attribute dev_attr_##_name = \ __ATTR(_sysfs_name, 0644, _name##_show, _name##_store) +#define ALLY_DEVICE_ATTR_RO(_name, _sysfs_name) \ + struct device_attribute dev_attr_##_name = \ + __ATTR(_sysfs_name, 0444, _name##_show, NULL) + /* button specific macros */ #define ALLY_BTN_SHOW(_fname, _btn_name, _secondary) \ static ssize_t _fname##_show(struct device *dev, \ From 00f1793c9d77cd9a634cb8dce7a71b5c966e196e Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Sat, 5 Oct 2024 21:32:41 +1300 Subject: [PATCH 64/73] [FOR-UPSTREAM] hid-asus-ally: add JS deadzones Signed-off-by: Luke D. Jones --- drivers/hid/hid-asus-ally.c | 84 +++++++++++++++++++++++++++++++++++++ drivers/hid/hid-asus-ally.h | 39 +++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index 35480dea92046..1ea164d73da63 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -257,6 +257,11 @@ struct btn_mapping { struct btn_data btn_m2; }; +struct deadzone { + u8 inner; + u8 outer; +}; + /* ROG Ally has many settings related to the gamepad, all using the same n-key endpoint */ struct ally_gamepad_cfg { struct hid_device *hdev; @@ -272,6 +277,10 @@ struct ally_gamepad_cfg { * max: 64 */ u8 vibration_intensity[2]; + + /* deadzones */ + struct deadzone ls_dz; // left stick + struct deadzone rs_dz; // right stick }; /* The hatswitch outputs integers, we use them to index this X|Y pair */ @@ -537,6 +546,75 @@ static ssize_t gamepad_vibration_intensity_store(struct device *dev, ALLY_DEVICE_ATTR_RW(gamepad_vibration_intensity, vibration_intensity); +/* ANALOGUE DEADZONES *****************************************************************************/ +static ssize_t _gamepad_apply_deadzones(struct hid_device *hdev, + struct ally_gamepad_cfg *ally_cfg) +{ + u8 *hidbuf; + int ret; + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + return ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_set_js_dz; + hidbuf[3] = xpad_cmd_len_deadzone; + hidbuf[4] = ally_cfg->ls_dz.inner; + hidbuf[5] = ally_cfg->ls_dz.outer; + hidbuf[6] = ally_cfg->rs_dz.inner; + hidbuf[7] = ally_cfg->rs_dz.outer; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + + kfree(hidbuf); + return ret; +} + +static void _gamepad_set_deadzones_default(struct ally_gamepad_cfg *ally_cfg) +{ + ally_cfg->ls_dz.inner = 0x00; + ally_cfg->ls_dz.outer = 0x64; + ally_cfg->rs_dz.inner = 0x00; + ally_cfg->rs_dz.outer = 0x64; +} + +static ssize_t axis_xyz_deadzone_index_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "inner outer\n"); +} + +ALLY_DEVICE_ATTR_RO(axis_xyz_deadzone_index, deadzone_index); + +ALLY_DEADZONES(axis_xy_left, ls_dz); +ALLY_DEADZONES(axis_xy_right, rs_dz); + +static struct attribute *axis_xy_left_attrs[] = { + &dev_attr_axis_xy_left_deadzone.attr, + &dev_attr_axis_xyz_deadzone_index.attr, + NULL +}; +static const struct attribute_group axis_xy_left_attr_group = { + .name = "axis_xy_left", + .attrs = axis_xy_left_attrs, +}; + +static struct attribute *axis_xy_right_attrs[] = { + &dev_attr_axis_xy_right_deadzone.attr, + &dev_attr_axis_xyz_deadzone_index.attr, + NULL +}; +static const struct attribute_group axis_xy_right_attr_group = { + .name = "axis_xy_right", + .attrs = axis_xy_right_attrs, +}; + /* A HID packet conatins mappings for two buttons: btn1, btn1_macro, btn2, btn2_macro */ static void _btn_pair_to_hid_pkt(struct ally_gamepad_cfg *ally_cfg, enum btn_pair_index pair, @@ -699,6 +777,9 @@ static ssize_t _gamepad_apply_all(struct hid_device *hdev, struct ally_gamepad_c if (ret < 0) return ret; ret = _gamepad_apply_turbo(hdev, ally_cfg); + if (ret < 0) + return ret; + ret = _gamepad_apply_deadzones(hdev, ally_cfg); if (ret < 0) return ret; @@ -875,6 +956,8 @@ static const struct attribute_group ally_controller_attr_group = { static const struct attribute_group *gamepad_device_attr_groups[] = { &ally_controller_attr_group, + &axis_xy_left_attr_group, + &axis_xy_right_attr_group, &btn_mapping_m1_attr_group, &btn_mapping_m2_attr_group, &btn_mapping_a_attr_group, @@ -940,6 +1023,7 @@ static struct ally_gamepad_cfg *ally_gamepad_cfg_create(struct hid_device *hdev) ally_cfg->vibration_intensity[0] = 0x64; ally_cfg->vibration_intensity[1] = 0x64; + _gamepad_set_deadzones_default(ally_cfg); drvdata.gamepad_cfg = ally_cfg; // Must asign before attr group setup if (sysfs_create_groups(&hdev->dev.kobj, gamepad_device_attr_groups)) { diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h index 6ac79ad3c5f26..3dc14a5226f30 100644 --- a/drivers/hid/hid-asus-ally.h +++ b/drivers/hid/hid-asus-ally.h @@ -22,6 +22,7 @@ enum xpad_mode { enum xpad_cmd { xpad_cmd_set_mode = 0x01, xpad_cmd_set_mapping = 0x02, + xpad_cmd_set_js_dz = 0x04, /* deadzones */ xpad_cmd_set_vibe_intensity = 0x06, xpad_cmd_set_leds = 0x08, xpad_cmd_check_ready = 0x0A, @@ -32,6 +33,7 @@ enum xpad_cmd { enum xpad_cmd_len { xpad_cmd_len_mode = 0x01, xpad_cmd_len_mapping = 0x2c, + xpad_cmd_len_deadzone = 0x04, xpad_cmd_len_vibe_intensity = 0x02, xpad_cmd_len_leds = 0x0C, xpad_cmd_len_turbo = 0x20, @@ -267,6 +269,43 @@ enum btn_pair_index { return count; \ } +#define ALLY_DEADZONE_SHOW(_fname, _axis_name) \ + static ssize_t _fname##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct deadzone *dz; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + dz = &ally_cfg->_axis_name; \ + return sysfs_emit(buf, "%d %d\n", dz->inner, dz->outer); \ + } + +#define ALLY_DEADZONE_STORE(_fname, _axis_name) \ + static ssize_t _fname##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct hid_device *hdev = to_hid_device(dev); \ + u32 inner, outer; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + if (sscanf(buf, "%d %d", &inner, &outer) != 2) \ + return -EINVAL; \ + if (inner > 64 || outer > 64 || inner > outer) \ + return -EINVAL; \ + ally_cfg->_axis_name.inner = inner; \ + ally_cfg->_axis_name.outer = outer; \ + _gamepad_apply_deadzones(hdev, ally_cfg); \ + return count; \ + } + +#define ALLY_DEADZONES(_fname, _mname) \ + ALLY_DEADZONE_SHOW(_fname##_deadzone, _mname); \ + ALLY_DEADZONE_STORE(_fname##_deadzone, _mname); \ + ALLY_DEVICE_ATTR_RW(_fname##_deadzone, deadzone) + #define ALLY_BTN_ATTRS_GROUP(_name, _fname) \ static struct attribute *_fname##_attrs[] = { \ &dev_attr_##_fname.attr, \ From 4a07f8d29572de576c647c6a1b12b3aef5c49f64 Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Sat, 5 Oct 2024 21:37:27 +1300 Subject: [PATCH 65/73] [FOR-UPSTREAM] hid-asus-ally: add trigger deadzones Signed-off-by: Luke D. Jones --- drivers/hid/hid-asus-ally.c | 43 +++++++++++++++++++++++++++++++++++++ drivers/hid/hid-asus-ally.h | 1 + 2 files changed, 44 insertions(+) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index 1ea164d73da63..255e5f7f856db 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -281,6 +281,8 @@ struct ally_gamepad_cfg { /* deadzones */ struct deadzone ls_dz; // left stick struct deadzone rs_dz; // right stick + struct deadzone lt_dz; // left trigger + struct deadzone rt_dz; // right trigger }; /* The hatswitch outputs integers, we use them to index this X|Y pair */ @@ -571,7 +573,20 @@ static ssize_t _gamepad_apply_deadzones(struct hid_device *hdev, hidbuf[7] = ally_cfg->rs_dz.outer; ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto end; + + hidbuf[2] = xpad_cmd_set_tr_dz; + hidbuf[4] = ally_cfg->lt_dz.inner; + hidbuf[5] = ally_cfg->lt_dz.outer; + hidbuf[6] = ally_cfg->rt_dz.inner; + hidbuf[7] = ally_cfg->rt_dz.outer; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto end; +end: kfree(hidbuf); return ret; } @@ -582,6 +597,10 @@ static void _gamepad_set_deadzones_default(struct ally_gamepad_cfg *ally_cfg) ally_cfg->ls_dz.outer = 0x64; ally_cfg->rs_dz.inner = 0x00; ally_cfg->rs_dz.outer = 0x64; + ally_cfg->lt_dz.inner = 0x00; + ally_cfg->lt_dz.outer = 0x64; + ally_cfg->rt_dz.inner = 0x00; + ally_cfg->rt_dz.outer = 0x64; } static ssize_t axis_xyz_deadzone_index_show(struct device *dev, struct device_attribute *attr, @@ -594,6 +613,8 @@ ALLY_DEVICE_ATTR_RO(axis_xyz_deadzone_index, deadzone_index); ALLY_DEADZONES(axis_xy_left, ls_dz); ALLY_DEADZONES(axis_xy_right, rs_dz); +ALLY_DEADZONES(axis_z_left, lt_dz); +ALLY_DEADZONES(axis_z_right, rt_dz); static struct attribute *axis_xy_left_attrs[] = { &dev_attr_axis_xy_left_deadzone.attr, @@ -615,6 +636,26 @@ static const struct attribute_group axis_xy_right_attr_group = { .attrs = axis_xy_right_attrs, }; +static struct attribute *axis_z_left_attrs[] = { + &dev_attr_axis_z_left_deadzone.attr, + &dev_attr_axis_xyz_deadzone_index.attr, + NULL, +}; +static const struct attribute_group axis_z_left_attr_group = { + .name = "axis_z_left", + .attrs = axis_z_left_attrs, +}; + +static struct attribute *axis_z_right_attrs[] = { + &dev_attr_axis_z_right_deadzone.attr, + &dev_attr_axis_xyz_deadzone_index.attr, + NULL, +}; +static const struct attribute_group axis_z_right_attr_group = { + .name = "axis_z_right", + .attrs = axis_z_right_attrs, +}; + /* A HID packet conatins mappings for two buttons: btn1, btn1_macro, btn2, btn2_macro */ static void _btn_pair_to_hid_pkt(struct ally_gamepad_cfg *ally_cfg, enum btn_pair_index pair, @@ -958,6 +999,8 @@ static const struct attribute_group *gamepad_device_attr_groups[] = { &ally_controller_attr_group, &axis_xy_left_attr_group, &axis_xy_right_attr_group, + &axis_z_left_attr_group, + &axis_z_right_attr_group, &btn_mapping_m1_attr_group, &btn_mapping_m2_attr_group, &btn_mapping_a_attr_group, diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h index 3dc14a5226f30..32ed5caa37599 100644 --- a/drivers/hid/hid-asus-ally.h +++ b/drivers/hid/hid-asus-ally.h @@ -23,6 +23,7 @@ enum xpad_cmd { xpad_cmd_set_mode = 0x01, xpad_cmd_set_mapping = 0x02, xpad_cmd_set_js_dz = 0x04, /* deadzones */ + xpad_cmd_set_tr_dz = 0x05, /* deadzones */ xpad_cmd_set_vibe_intensity = 0x06, xpad_cmd_set_leds = 0x08, xpad_cmd_check_ready = 0x0A, From 026cd7fb56b59fdcc2182d4b851fa74e29e59993 Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Sun, 6 Oct 2024 19:49:24 +1300 Subject: [PATCH 66/73] [FOR-UPSTREAM] hid-asus-ally: add anti-deadzones Signed-off-by: Luke D. Jones --- drivers/hid/hid-asus-ally.c | 110 ++++++++++++++++++++++++++++++++++++ drivers/hid/hid-asus-ally.h | 2 + 2 files changed, 112 insertions(+) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index 255e5f7f856db..b4131b5991cff 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -283,6 +283,9 @@ struct ally_gamepad_cfg { struct deadzone rs_dz; // right stick struct deadzone lt_dz; // left trigger struct deadzone rt_dz; // right trigger + /* anti-deadzones */ + u8 ls_adz; // left stick + u8 rs_adz; // right stick }; /* The hatswitch outputs integers, we use them to index this X|Y pair */ @@ -616,7 +619,109 @@ ALLY_DEADZONES(axis_xy_right, rs_dz); ALLY_DEADZONES(axis_z_left, lt_dz); ALLY_DEADZONES(axis_z_right, rt_dz); +/* ANTI-DEADZONES *********************************************************************************/ +static ssize_t _gamepad_apply_js_ADZ(struct hid_device *hdev, + struct ally_gamepad_cfg *ally_cfg) +{ + u8 *hidbuf; + int ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_set_adz; + hidbuf[3] = xpad_cmd_len_adz; + hidbuf[4] = ally_cfg->ls_adz; + hidbuf[5] = ally_cfg->rs_adz; + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + goto report_fail; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto report_fail; + +report_fail: + kfree(hidbuf); + return ret; +} + +static void _gamepad_set_anti_deadzones_default(struct ally_gamepad_cfg *ally_cfg) +{ + ally_cfg->ls_adz = 0x00; + ally_cfg->rs_adz = 0x00; +} + +static ssize_t _gamepad_js_ADZ_store(struct device *dev, const char *buf, u8 *adz) +{ + int ret, val; + + ret = kstrtoint(buf, 0, &val); + if (ret) + return ret; + + if (val < 0 || val > 32) + return -EINVAL; + + *adz = val; + + return ret; +} + +static ssize_t axis_xy_left_anti_deadzone_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + + return sysfs_emit(buf, "%d\n", ally_cfg->ls_adz); +} + +static ssize_t axis_xy_left_anti_deadzone_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + int ret; + + ret = _gamepad_js_ADZ_store(dev, buf, &ally_cfg->ls_adz); + if (ret) + return ret; + + return count; +} +ALLY_DEVICE_ATTR_RW(axis_xy_left_anti_deadzone, anti_deadzone); + +static ssize_t axis_xy_right_anti_deadzone_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + + return sysfs_emit(buf, "%d\n", ally_cfg->rs_adz); +} + +static ssize_t axis_xy_right_anti_deadzone_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + int ret; + + ret = _gamepad_js_ADZ_store(dev, buf, &ally_cfg->rs_adz); + if (ret) + return ret; + + return count; +} +ALLY_DEVICE_ATTR_RW(axis_xy_right_anti_deadzone, anti_deadzone); + static struct attribute *axis_xy_left_attrs[] = { + &dev_attr_axis_xy_left_anti_deadzone.attr, &dev_attr_axis_xy_left_deadzone.attr, &dev_attr_axis_xyz_deadzone_index.attr, NULL @@ -627,6 +732,7 @@ static const struct attribute_group axis_xy_left_attr_group = { }; static struct attribute *axis_xy_right_attrs[] = { + &dev_attr_axis_xy_right_anti_deadzone.attr, &dev_attr_axis_xy_right_deadzone.attr, &dev_attr_axis_xyz_deadzone_index.attr, NULL @@ -821,6 +927,9 @@ static ssize_t _gamepad_apply_all(struct hid_device *hdev, struct ally_gamepad_c if (ret < 0) return ret; ret = _gamepad_apply_deadzones(hdev, ally_cfg); + if (ret < 0) + return ret; + ret = _gamepad_apply_js_ADZ(hdev, ally_cfg); if (ret < 0) return ret; @@ -1067,6 +1176,7 @@ static struct ally_gamepad_cfg *ally_gamepad_cfg_create(struct hid_device *hdev) ally_cfg->vibration_intensity[0] = 0x64; ally_cfg->vibration_intensity[1] = 0x64; _gamepad_set_deadzones_default(ally_cfg); + _gamepad_set_anti_deadzones_default(ally_cfg); drvdata.gamepad_cfg = ally_cfg; // Must asign before attr group setup if (sysfs_create_groups(&hdev->dev.kobj, gamepad_device_attr_groups)) { diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h index 32ed5caa37599..69f59592dd504 100644 --- a/drivers/hid/hid-asus-ally.h +++ b/drivers/hid/hid-asus-ally.h @@ -28,6 +28,7 @@ enum xpad_cmd { xpad_cmd_set_leds = 0x08, xpad_cmd_check_ready = 0x0A, xpad_cmd_set_turbo = 0x0F, + xpad_cmd_set_adz = 0x18, }; /* the xpad_cmd determines which feature is set or queried */ @@ -38,6 +39,7 @@ enum xpad_cmd_len { xpad_cmd_len_vibe_intensity = 0x02, xpad_cmd_len_leds = 0x0C, xpad_cmd_len_turbo = 0x20, + xpad_cmd_len_adz = 0x02, }; /* Values correspond to the actual HID byte value required */ From 5b52ef7b2b50a409f63b4d908be5d3b0317fa267 Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Sun, 6 Oct 2024 21:22:40 +1300 Subject: [PATCH 67/73] [FOR-UPSTREAM] hid-asus-ally: add JS response curves Signed-off-by: Luke D. Jones --- drivers/hid/hid-asus-ally.c | 103 ++++++++++++++++++++++++++++++++++++ drivers/hid/hid-asus-ally.h | 38 +++++++++++++ 2 files changed, 141 insertions(+) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index b4131b5991cff..ba36bd1fb877a 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -5,10 +5,12 @@ * Copyright (c) 2023 Luke Jones */ +#include "linux/compiler_attributes.h" #include "linux/device.h" #include #include #include "linux/pm.h" +#include "linux/printk.h" #include "linux/slab.h" #include #include @@ -262,6 +264,17 @@ struct deadzone { u8 outer; }; +struct response_curve { + uint8_t move_pct_1; + uint8_t response_pct_1; + uint8_t move_pct_2; + uint8_t response_pct_2; + uint8_t move_pct_3; + uint8_t response_pct_3; + uint8_t move_pct_4; + uint8_t response_pct_4; +} __packed; + /* ROG Ally has many settings related to the gamepad, all using the same n-key endpoint */ struct ally_gamepad_cfg { struct hid_device *hdev; @@ -286,6 +299,9 @@ struct ally_gamepad_cfg { /* anti-deadzones */ u8 ls_adz; // left stick u8 rs_adz; // right stick + /* joystick response curves */ + struct response_curve ls_rc; + struct response_curve rs_rc; }; /* The hatswitch outputs integers, we use them to index this X|Y pair */ @@ -720,10 +736,85 @@ static ssize_t axis_xy_right_anti_deadzone_store(struct device *dev, } ALLY_DEVICE_ATTR_RW(axis_xy_right_anti_deadzone, anti_deadzone); +/* JS RESPONSE CURVES *****************************************************************************/ +static void _gamepad_set_js_response_curves_default(struct ally_gamepad_cfg *ally_cfg) +{ + struct response_curve *js1_rc = &ally_cfg->ls_rc; + struct response_curve *js2_rc = &ally_cfg->rs_rc; + js1_rc->move_pct_1 = js2_rc->move_pct_1 = 0x16; // 25% + js1_rc->move_pct_2 = js2_rc->move_pct_2 = 0x32; // 50% + js1_rc->move_pct_3 = js2_rc->move_pct_3 = 0x48; // 75% + js1_rc->move_pct_4 = js2_rc->move_pct_4 = 0x64; // 100% + js1_rc->response_pct_1 = js2_rc->response_pct_1 = 0x16; + js1_rc->response_pct_2 = js2_rc->response_pct_2 = 0x32; + js1_rc->response_pct_3 = js2_rc->response_pct_3 = 0x48; + js1_rc->response_pct_4 = js2_rc->response_pct_4 = 0x64; +} + +static ssize_t _gamepad_apply_response_curves(struct hid_device *hdev, + struct ally_gamepad_cfg *ally_cfg) +{ + u8 *hidbuf; + int ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + memcpy(&hidbuf[2], &ally_cfg->ls_rc, sizeof(ally_cfg->ls_rc)); + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + goto report_fail; + + hidbuf[4] = 0x02; + memcpy(&hidbuf[5], &ally_cfg->rs_rc, sizeof(ally_cfg->rs_rc)); + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + goto report_fail; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto report_fail; + +report_fail: + kfree(hidbuf); + return ret; +} + +ALLY_JS_RC_POINT(axis_xy_left, move, 1); +ALLY_JS_RC_POINT(axis_xy_left, move, 2); +ALLY_JS_RC_POINT(axis_xy_left, move, 3); +ALLY_JS_RC_POINT(axis_xy_left, move, 4); +ALLY_JS_RC_POINT(axis_xy_left, response, 1); +ALLY_JS_RC_POINT(axis_xy_left, response, 2); +ALLY_JS_RC_POINT(axis_xy_left, response, 3); +ALLY_JS_RC_POINT(axis_xy_left, response, 4); + +ALLY_JS_RC_POINT(axis_xy_right, move, 1); +ALLY_JS_RC_POINT(axis_xy_right, move, 2); +ALLY_JS_RC_POINT(axis_xy_right, move, 3); +ALLY_JS_RC_POINT(axis_xy_right, move, 4); +ALLY_JS_RC_POINT(axis_xy_right, response, 1); +ALLY_JS_RC_POINT(axis_xy_right, response, 2); +ALLY_JS_RC_POINT(axis_xy_right, response, 3); +ALLY_JS_RC_POINT(axis_xy_right, response, 4); + static struct attribute *axis_xy_left_attrs[] = { &dev_attr_axis_xy_left_anti_deadzone.attr, &dev_attr_axis_xy_left_deadzone.attr, &dev_attr_axis_xyz_deadzone_index.attr, + &dev_attr_axis_xy_left_move_1.attr, + &dev_attr_axis_xy_left_move_2.attr, + &dev_attr_axis_xy_left_move_3.attr, + &dev_attr_axis_xy_left_move_4.attr, + &dev_attr_axis_xy_left_response_1.attr, + &dev_attr_axis_xy_left_response_2.attr, + &dev_attr_axis_xy_left_response_3.attr, + &dev_attr_axis_xy_left_response_4.attr, NULL }; static const struct attribute_group axis_xy_left_attr_group = { @@ -735,6 +826,14 @@ static struct attribute *axis_xy_right_attrs[] = { &dev_attr_axis_xy_right_anti_deadzone.attr, &dev_attr_axis_xy_right_deadzone.attr, &dev_attr_axis_xyz_deadzone_index.attr, + &dev_attr_axis_xy_right_move_1.attr, + &dev_attr_axis_xy_right_move_2.attr, + &dev_attr_axis_xy_right_move_3.attr, + &dev_attr_axis_xy_right_move_4.attr, + &dev_attr_axis_xy_right_response_1.attr, + &dev_attr_axis_xy_right_response_2.attr, + &dev_attr_axis_xy_right_response_3.attr, + &dev_attr_axis_xy_right_response_4.attr, NULL }; static const struct attribute_group axis_xy_right_attr_group = { @@ -930,6 +1029,9 @@ static ssize_t _gamepad_apply_all(struct hid_device *hdev, struct ally_gamepad_c if (ret < 0) return ret; ret = _gamepad_apply_js_ADZ(hdev, ally_cfg); + if (ret < 0) + return ret; + ret =_gamepad_apply_response_curves(hdev, ally_cfg); if (ret < 0) return ret; @@ -1177,6 +1279,7 @@ static struct ally_gamepad_cfg *ally_gamepad_cfg_create(struct hid_device *hdev) ally_cfg->vibration_intensity[1] = 0x64; _gamepad_set_deadzones_default(ally_cfg); _gamepad_set_anti_deadzones_default(ally_cfg); + _gamepad_set_js_response_curves_default(ally_cfg); drvdata.gamepad_cfg = ally_cfg; // Must asign before attr group setup if (sysfs_create_groups(&hdev->dev.kobj, gamepad_device_attr_groups)) { diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h index 69f59592dd504..c83817589082f 100644 --- a/drivers/hid/hid-asus-ally.h +++ b/drivers/hid/hid-asus-ally.h @@ -28,6 +28,7 @@ enum xpad_cmd { xpad_cmd_set_leds = 0x08, xpad_cmd_check_ready = 0x0A, xpad_cmd_set_turbo = 0x0F, + xpad_cmd_set_response_curve = 0x13, xpad_cmd_set_adz = 0x18, }; @@ -39,6 +40,7 @@ enum xpad_cmd_len { xpad_cmd_len_vibe_intensity = 0x02, xpad_cmd_len_leds = 0x0C, xpad_cmd_len_turbo = 0x20, + xpad_cmd_len_response_curve = 0x09, xpad_cmd_len_adz = 0x02, }; @@ -309,6 +311,42 @@ enum btn_pair_index { ALLY_DEADZONE_STORE(_fname##_deadzone, _mname); \ ALLY_DEVICE_ATTR_RW(_fname##_deadzone, deadzone) +/* response curve macros */ +#define ALLY_RESP_CURVE_SHOW(_fname, _mname) \ +static ssize_t _fname##_show(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + return sysfs_emit(buf, "%d\n", ally_cfg->ls_rc._mname); \ + } + +#define ALLY_RESP_CURVE_STORE(_fname, _mname) \ +static ssize_t _fname##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + int ret, val; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + ret = kstrtoint(buf, 0, &val); \ + if (ret) \ + return ret; \ + if (val < 0 || val > 100) \ + return -EINVAL; \ + ally_cfg->ls_rc._mname = val; \ + return count; \ + } + +/* _point_n must start at 1 */ +#define ALLY_JS_RC_POINT(_fname, _mname, _num) \ + ALLY_RESP_CURVE_SHOW(_fname##_##_mname##_##_num, _mname##_pct_##_num); \ + ALLY_RESP_CURVE_STORE(_fname##_##_mname##_##_num, _mname##_pct_##_num); \ + ALLY_DEVICE_ATTR_RW(_fname##_##_mname##_##_num, curve_##_mname##_pct_##_num) + #define ALLY_BTN_ATTRS_GROUP(_name, _fname) \ static struct attribute *_fname##_attrs[] = { \ &dev_attr_##_fname.attr, \ From b63276c9a8980730e65b6e68edab7bebe35bc9ec Mon Sep 17 00:00:00 2001 From: Luke Jones Date: Mon, 10 Feb 2025 17:15:01 +1300 Subject: [PATCH 68/73] [FOR-UPSTREAM] hid-asus-ally: mcu_version attribute Add the mcu_version sysfs attribute so that userspace has a way to check the MCU FW version. Signed-off-by: Luke Jones --- drivers/hid/hid-asus-ally.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index ba36bd1fb877a..5ca93f8342b15 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -377,6 +377,7 @@ static struct ally_drvdata { struct ally_gamepad_cfg *gamepad_cfg; struct ally_rgb_dev *led_rgb_dev; struct ally_rgb_data led_rgb_data; + uint mcu_version; } drvdata; /** @@ -1192,6 +1193,13 @@ static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *a DEVICE_ATTR_RW(gamepad_mode); +static ssize_t mcu_version_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%d\n", drvdata.mcu_version); +} + +DEVICE_ATTR_RO(mcu_version); + /* ROOT LEVEL ATTRS *******************************************************************************/ static struct attribute *gamepad_device_attrs[] = { &dev_attr_btn_mapping_reset.attr, @@ -1199,6 +1207,7 @@ static struct attribute *gamepad_device_attrs[] = { &dev_attr_gamepad_apply_all.attr, &dev_attr_gamepad_vibration_intensity.attr, &dev_attr_gamepad_vibration_intensity_index.attr, + &dev_attr_mcu_version.attr, NULL }; From fc2b4d18e07a946f74ce6d3af0dc74196019f0a4 Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Thu, 10 Oct 2024 11:15:36 +1300 Subject: [PATCH 69/73] [FOR-UPSTREAM] hid-asus-ally: add calibrations (wip) Signed-off-by: Luke D. Jones --- drivers/hid/hid-asus-ally.c | 95 +++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index 5ca93f8342b15..e6ff4ba08a5cd 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -275,6 +275,28 @@ struct response_curve { uint8_t response_pct_4; } __packed; +struct js_axis_calibrations { + uint16_t left_y_stable; + uint16_t left_y_min; + uint16_t left_y_max; + uint16_t left_x_stable; + uint16_t left_x_min; + uint16_t left_x_max; + uint16_t right_y_stable; + uint16_t right_y_min; + uint16_t right_y_max; + uint16_t right_x_stable; + uint16_t right_x_min; + uint16_t right_x_max; +} __packed; + +struct tr_axis_calibrations { + uint16_t left_stable; + uint16_t left_max; + uint16_t right_stable; + uint16_t right_max; +} __packed; + /* ROG Ally has many settings related to the gamepad, all using the same n-key endpoint */ struct ally_gamepad_cfg { struct hid_device *hdev; @@ -302,6 +324,9 @@ struct ally_gamepad_cfg { /* joystick response curves */ struct response_curve ls_rc; struct response_curve rs_rc; + + struct js_axis_calibrations js_cal; + struct tr_axis_calibrations tr_cal; }; /* The hatswitch outputs integers, we use them to index this X|Y pair */ @@ -380,6 +405,18 @@ static struct ally_drvdata { uint mcu_version; } drvdata; +static void reverse_bytes_in_pairs(u8 *buf, size_t size) { + uint16_t *word_ptr; + size_t i; + + for (i = 0; i < size; i += 2) { + if (i + 1 < size) { + word_ptr = (uint16_t *)&buf[i]; + *word_ptr = cpu_to_be16(*word_ptr); + } + } +} + /** * asus_dev_set_report - send set report request to device. * @@ -804,6 +841,63 @@ ALLY_JS_RC_POINT(axis_xy_right, response, 2); ALLY_JS_RC_POINT(axis_xy_right, response, 3); ALLY_JS_RC_POINT(axis_xy_right, response, 4); +/* CALIBRATIONS ***********************************************************************************/ +static int gamepad_get_calibration(struct hid_device *hdev) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + u8 *hidbuf; + int ret, i; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + for (i = 0; i < 2; i++) { + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = 0xD0; + hidbuf[2] = 0x03; + hidbuf[3] = i + 1; // 0x01 JS, 0x02 TR + hidbuf[4] = 0x20; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) { + hid_warn(hdev, "ROG Ally check failed set report: %d\n", ret); + goto cleanup; + } + + memset(hidbuf, 0, FEATURE_ROG_ALLY_REPORT_SIZE); + ret = asus_dev_get_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0 || hidbuf[5] != 1) { + hid_warn(hdev, "ROG Ally check failed get report: %d\n", ret); + goto cleanup; + } + + if (i == 0) { + /* Joystick calibration */ + reverse_bytes_in_pairs(&hidbuf[6], sizeof(struct js_axis_calibrations)); + ally_cfg->js_cal = *(struct js_axis_calibrations *)&hidbuf[6]; + print_hex_dump(KERN_INFO, "HID Buffer JS: ", DUMP_PREFIX_OFFSET, 16, 1, hidbuf, 32, true); + struct js_axis_calibrations *cal = &drvdata.gamepad_cfg->js_cal; + pr_err("LS_CAL: X: %d, Min: %d, Max: %d", cal->left_x_stable, cal->left_x_min, cal->left_x_max); + pr_err("LS_CAL: Y: %d, Min: %d, Max: %d", cal->left_y_stable, cal->left_y_min, cal->left_y_max); + pr_err("RS_CAL: X: %d, Min: %d, Max: %d", cal->right_x_stable, cal->right_x_min, cal->right_x_max); + pr_err("RS_CAL: Y: %d, Min: %d, Max: %d", cal->right_y_stable, cal->right_y_min, cal->right_y_max); + } else { + /* Trigger calibration */ + reverse_bytes_in_pairs(&hidbuf[6], sizeof(struct tr_axis_calibrations)); + ally_cfg->tr_cal = *(struct tr_axis_calibrations *)&hidbuf[6]; + print_hex_dump(KERN_INFO, "HID Buffer TR: ", DUMP_PREFIX_OFFSET, 16, 1, hidbuf, 32, true); + } + } + +cleanup: + kfree(hidbuf); + return ret; +} + static struct attribute *axis_xy_left_attrs[] = { &dev_attr_axis_xy_left_anti_deadzone.attr, &dev_attr_axis_xy_left_deadzone.attr, @@ -1283,6 +1377,7 @@ static struct ally_gamepad_cfg *ally_gamepad_cfg_create(struct hid_device *hdev) ally_cfg->key_mapping[ally_cfg->mode - 1].btn_m1.button = BTN_KB_M1; ally_cfg->key_mapping[ally_cfg->mode - 1].btn_m2.button = BTN_KB_M2; _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_m1_m2); + gamepad_get_calibration(hdev); ally_cfg->vibration_intensity[0] = 0x64; ally_cfg->vibration_intensity[1] = 0x64; From b48a4cf96fb5166af41ca5a69ffd1a01607f1df5 Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Wed, 6 Nov 2024 00:27:03 +0300 Subject: [PATCH 70/73] [FOR-UPSTREAM] debug by default --- drivers/hid/hid-asus-ally.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index e6ff4ba08a5cd..e78625f70c44e 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -22,6 +22,8 @@ #include "hid-asus.h" #include "hid-asus-ally.h" +#define DEBUG + #define READY_MAX_TRIES 3 #define FEATURE_REPORT_ID 0x0d #define FEATURE_ROG_ALLY_REPORT_ID 0x5a From ff0b14ac3a3a281d88059ee70863c93dc979e9d5 Mon Sep 17 00:00:00 2001 From: Matthew Schwartz Date: Sun, 26 Oct 2025 22:03:22 -0700 Subject: [PATCH 71/73] [FOR-UPSTREAM] hid-asus-ally: grab short press QAM on ROG Xbox Ally X The hid report for the QAM button changed on the ASUS ROG Xbox Ally X, so add the new mapping for the short press event. Otherwise, all we get is a long press event on "Asus Keyboard". neptune: We're carrying this as [NOT-FOR-UPSTREAM] because the last version of hid-asus-ally on lkml was in August of 2024, and we have some divergence from the last submission upstream. This should be included upstream when there is a new submission for hid-asus-ally. Reviewed-by: Derek J. Clark Signed-off-by: Matthew Schwartz --- drivers/hid/hid-asus-ally.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index e78625f70c44e..ae3f257c79736 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -1460,7 +1460,7 @@ static int ally_x_raw_event(struct ally_x_device *ally_x, struct hid_report *rep else if (data[0] == 0x5A) { if (ally_x->qam_btns_steam_mode) { spin_lock_irqsave(&ally_x->lock, flags); - if (data[1] == 0x38 && !ally_x->update_qam_btn) { + if ((data[1] == 0x38 || data[1] == 0x93) && !ally_x->update_qam_btn) { ally_x->update_qam_btn = true; if (ally_x->output_worker_initialized) schedule_work(&ally_x->output_worker); @@ -1470,7 +1470,7 @@ static int ally_x_raw_event(struct ally_x_device *ally_x, struct hid_report *rep input_report_key(ally_x->input, BTN_MODE, data[1] == 0xA6); } else { input_report_key(ally_x->input, KEY_F16, data[1] == 0xA6); - input_report_key(ally_x->input, KEY_PROG1, data[1] == 0x38); + input_report_key(ally_x->input, KEY_PROG1, data[1] == 0x38 || data[1] == 0x93); } /* QAM long press */ input_report_key(ally_x->input, KEY_F17, data[1] == 0xA7); From 70e7f084332cdf89b64d1532cd42309d33200de2 Mon Sep 17 00:00:00 2001 From: Matthew Schwartz Date: Sat, 6 Sep 2025 21:02:30 -0700 Subject: [PATCH 72/73] [FOR-UPSTREAM] hid-asus-ally: disable wakeup attribute on N-Key device With the current ROG Ally patchset, the MCU will send spurious events when the ROG Ally is in s2idle while plugged in, which can cause the system to wake unexpectedly. There's really no reason the N-Key needs wakeup enabled, so just disable the capability for now. This can be dropped when the issue is root caused and fixed within the standard ROG Ally patchset. Closes: https://github.com/ValveSoftware/SteamOS/issues/2119 Signed-off-by: Matthew Schwartz --- drivers/hid/hid-asus-ally.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c index ae3f257c79736..8b51560fdbcfb 100644 --- a/drivers/hid/hid-asus-ally.c +++ b/drivers/hid/hid-asus-ally.c @@ -2010,6 +2010,23 @@ static int ally_hid_init(struct hid_device *hdev) return ret; } +static void ally_disable_nkey_wakeup(struct hid_device *hdev) +{ + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_device *udev; + + if (!intf) + return; + + udev = interface_to_usbdev(intf); + if (!udev) + return; + + /* HACK: Mark Ally N-Key as incapable of wakeup */ + device_set_wakeup_enable(&udev->dev, false); + hid_info(hdev, "Disabled wakeup capability on %s\n", dev_name(&udev->dev)); +} + static int ally_hid_probe(struct hid_device *hdev, const struct hid_device_id *_id) { struct usb_interface *intf = to_usb_interface(hdev->dev.parent); @@ -2054,6 +2071,7 @@ static int ally_hid_probe(struct hid_device *hdev, const struct hid_device_id *_ /* This should almost always exist */ if (ep == ROG_ALLY_CFG_INTF_IN) { validate_mcu_fw_version(hdev, idProduct); + ally_disable_nkey_wakeup(hdev); drvdata.led_rgb_dev = ally_rgb_create(hdev); if (IS_ERR(drvdata.led_rgb_dev)) From 46381fa56ad5311df8f329538b0cf75077cf5d23 Mon Sep 17 00:00:00 2001 From: Ahmed Yaseen Date: Wed, 3 Jun 2026 22:09:45 +0500 Subject: [PATCH 73/73] [FROM-ML] HID: usbhid: skip interrupt IN polling for devices with no input reports usbhid starts polling a device's interrupt IN endpoint on open (usbhid_open() -> hid_start_in()). If the report descriptor declares no input reports there is nothing to read there, so the poll is useless, and on some composite devices it is also harmful. The ASUS ROG N-Key keyboards expose a second, input-less interface used only for RGB control via feature reports. Opening its hidraw node (any hidraw reader does, including SDL/Steam Input or a plain cat) starts the pointless IN poll and keypress reports on the keyboard interface get dropped for as long as the node stays open: a lost key-down drops a letter, a lost key-up leaves the key stuck. usbmon shows the dropped reports never reach the URB layer. The useless poll itself is long-standing; commit 4ac74ea68f64 ("HID: asus: early return for ROG devices") is what exposes it on these devices by keeping the input-less interface alive instead of ejecting it, so its hidraw node can be opened and the poll started. Skip the poll in usbhid_open() when the device has no input reports. Feature reports and hidraw output keep working over the control and OUT endpoints, so the interface is otherwise unaffected. Fixes: 4ac74ea68f64 ("HID: asus: early return for ROG devices") Tested-by: Kerim Kabirov Tested-by: GameBurrow Signed-off-by: Ahmed Yaseen --- drivers/hid/usbhid/hid-core.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/hid/usbhid/hid-core.c b/drivers/hid/usbhid/hid-core.c index 5af93b9b1fb56..8bbcec63fc766 100644 --- a/drivers/hid/usbhid/hid-core.c +++ b/drivers/hid/usbhid/hid-core.c @@ -687,7 +687,8 @@ static int usbhid_open(struct hid_device *hid) set_bit(HID_OPENED, &usbhid->iofl); - if (hid->quirks & HID_QUIRK_ALWAYS_POLL) { + if ((hid->quirks & HID_QUIRK_ALWAYS_POLL) || + list_empty(&hid->report_enum[HID_INPUT_REPORT].report_list)) { res = 0; goto Done; }