From 60c85eaef6e7f399e4d26e002a69dce3e3be855d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Fri, 20 Mar 2026 00:09:33 +0300 Subject: [PATCH] Hall effect interference correction --- src/hhd/device/rog_ally/__init__.py | 14 +- src/hhd/device/rog_ally/base.py | 169 +++++++++++++++++++++++- src/hhd/device/rog_ally/controllers.yml | 2 + src/hhd/plugins/__init__.py | 6 + src/hhd/plugins/hall_interference.yml | 92 +++++++++++++ src/hhd/plugins/outputs.py | 32 +++++ 6 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 src/hhd/plugins/hall_interference.yml diff --git a/src/hhd/device/rog_ally/__init__.py b/src/hhd/device/rog_ally/__init__.py index a9f8a66f..b6b33cd5 100644 --- a/src/hhd/device/rog_ally/__init__.py +++ b/src/hhd/device/rog_ally/__init__.py @@ -7,7 +7,9 @@ Emitter, Event, HHDPlugin, + fix_hall_interference, load_relative_yaml, + get_hall_interference_config, get_outputs_config, get_limits_config, fix_limits, @@ -40,7 +42,7 @@ def open( self.prev = None def settings(self) -> HHDSettings: - from .base import LIMIT_DEFAULTS + from .base import HALL_DEFAULTS, LIMIT_DEFAULTS base = {"controllers": {"rog_ally": load_relative_yaml("controllers.yml")}} base["controllers"]["rog_ally"]["children"]["controller_mode"].update( @@ -51,6 +53,9 @@ def settings(self) -> HHDSettings: base["controllers"]["rog_ally"]["children"]["limits"] = get_limits_config( LIMIT_DEFAULTS(self.ally_x) ) + base["controllers"]["rog_ally"]["children"][ + "hall_interference" + ] = get_hall_interference_config(HALL_DEFAULTS) if not self.xbox: del base["controllers"]["rog_ally"]["children"]["swap_xbox"] @@ -58,9 +63,14 @@ def settings(self) -> HHDSettings: return base def update(self, conf: Config): - from .base import LIMIT_DEFAULTS + from .base import HALL_DEFAULTS, LIMIT_DEFAULTS fix_limits(conf, "controllers.rog_ally.limits", LIMIT_DEFAULTS(self.ally_x)) + fix_hall_interference( + conf, + "controllers.rog_ally.hall_interference", + HALL_DEFAULTS, + ) new_conf = conf["controllers.rog_ally"] if new_conf == self.prev: diff --git a/src/hhd/device/rog_ally/base.py b/src/hhd/device/rog_ally/base.py index 37cab9f0..daf8eea4 100644 --- a/src/hhd/device/rog_ally/base.py +++ b/src/hhd/device/rog_ally/base.py @@ -2,7 +2,7 @@ import select import time from threading import Event as TEvent -from typing import Sequence, Literal +from typing import Sequence from hhd.controller import DEBUG_MODE, Axis, Event, Multiplexer, can_read from hhd.controller.lib.hide import unhide_all @@ -123,6 +123,36 @@ ) +def _clamp_axis(v: float) -> float: + return max(-1.0, min(1.0, v)) + + +def _edge_gain_center(x: float) -> float: + # Full effect near center, smoothly fades near extremes. + g = max(0.0, 1.0 - abs(x)) + return g * g + + +def _remap_deadzone(v: float, d: float) -> float: + av = abs(v) + if av <= d: + return 0.0 + if d >= 1.0: + return 0.0 + out = (av - d) / (1.0 - d) + return -out if v < 0 else out + + +HALL_DEFAULTS = { + "ls_idle_x": -0.15, + "lt_corr_x": 0.28, + "rs_idle_x": 0.10, + "ls_deadzone": 0.15, + "rs_deadzone": 0.05, + "remap_deadzone": True, +} + + class AllyHidraw(GenericGamepadHidraw): def __init__(self, *args, kconf={}, rgb_boot, rgb_charging, **kwargs) -> None: super().__init__(*args, **kwargs) @@ -339,6 +369,40 @@ def controller_loop( swap_armoury = conf.get("swap_armory", False) swap_xbox = conf.get("swap_xbox", False) and xbox + hall_mode = conf.get("hall_interference.mode", "default") + hall_enabled = hall_mode != "disabled" + if hall_mode == "manual": + hall_ls_idle_x = conf.get( + "hall_interference.manual.ls_idle_x", HALL_DEFAULTS["ls_idle_x"] + ) + hall_lt_corr_x = conf.get( + "hall_interference.manual.lt_corr_x", HALL_DEFAULTS["lt_corr_x"] + ) + hall_rs_idle_x = conf.get( + "hall_interference.manual.rs_idle_x", HALL_DEFAULTS["rs_idle_x"] + ) + hall_ls_deadzone = conf.get( + "hall_interference.manual.ls_deadzone", HALL_DEFAULTS["ls_deadzone"] + ) + hall_rs_deadzone = conf.get( + "hall_interference.manual.rs_deadzone", HALL_DEFAULTS["rs_deadzone"] + ) + hall_remap_deadzone = conf.get( + "hall_interference.manual.remap_deadzone", + bool(HALL_DEFAULTS["remap_deadzone"]), + ) + else: + hall_ls_idle_x = HALL_DEFAULTS["ls_idle_x"] + hall_lt_corr_x = HALL_DEFAULTS["lt_corr_x"] + hall_rs_idle_x = HALL_DEFAULTS["rs_idle_x"] + hall_ls_deadzone = HALL_DEFAULTS["ls_deadzone"] + hall_rs_deadzone = HALL_DEFAULTS["rs_deadzone"] + hall_remap_deadzone = bool(HALL_DEFAULTS["remap_deadzone"]) + + rt_now = 0.0 + ls_y_now = 0.0 + rs_y_now = 0.0 + # Imu d_imu = CombinedImu(conf["imu_hz"].to(int), ALLY_MAPPINGS, gyro_scale="0.000266") d_timer = HrtimerTrigger(conf["imu_hz"].to(int), [HrtimerTrigger.IMU_NAMES]) @@ -474,6 +538,109 @@ def prepare(m): evs.extend(d.produce(r)) evs.extend(d_vend.produce(r)) + if hall_enabled: + ls_x_seen = False + ls_y_seen = False + rs_x_seen = False + rs_y_seen = False + rt_seen = False + for ev in evs: + if ev["type"] != "axis": + continue + elif ev["code"] == "rt": + rt_now = max(0.0, float(ev["value"])) + rt_seen = True + elif ev["code"] == "ls_x": + ls_x_now = float(ev["value"]) + ls_x_seen = True + elif ev["code"] == "ls_y": + ls_y_now = float(ev["value"]) + ls_y_seen = True + elif ev["code"] == "rs_x": + rs_x_now = float(ev["value"]) + rs_x_seen = True + elif ev["code"] == "rs_y": + rs_y_now = float(ev["value"]) + rs_y_seen = True + + trigger_corr_x = hall_lt_corr_x * rt_now + ls_idle_gain = _edge_gain_center(ls_x_now) + rs_idle_gain = _edge_gain_center(rs_x_now) + + ls_x_corr = _clamp_axis( + ls_x_now - hall_ls_idle_x * ls_idle_gain + trigger_corr_x + ) + ls_y_corr = _clamp_axis(ls_y_now) + ls_in_deadzone = ( + abs(ls_x_corr) < hall_ls_deadzone + and abs(ls_y_corr) < hall_ls_deadzone + ) + + rs_x_corr = _clamp_axis(rs_x_now - hall_rs_idle_x * rs_idle_gain) + rs_y_corr = _clamp_axis(rs_y_now) + rs_in_deadzone = ( + abs(rs_x_corr) < hall_rs_deadzone + and abs(rs_y_corr) < hall_rs_deadzone + ) + + if hall_remap_deadzone and not ls_in_deadzone: + ls_x_corr = _clamp_axis(_remap_deadzone(ls_x_corr, hall_ls_deadzone)) + ls_y_corr = _clamp_axis(_remap_deadzone(ls_y_corr, hall_ls_deadzone)) + + if hall_remap_deadzone and not rs_in_deadzone: + rs_x_corr = _clamp_axis(_remap_deadzone(rs_x_corr, hall_rs_deadzone)) + rs_y_corr = _clamp_axis(_remap_deadzone(rs_y_corr, hall_rs_deadzone)) + + for ev in evs: + if ev["type"] != "axis": + continue + if ev["code"] == "ls_x": + ev["value"] = 0.0 if ls_in_deadzone else ls_x_corr + elif ev["code"] == "ls_y": + ev["value"] = 0.0 if ls_in_deadzone else ls_y_corr + elif ev["code"] == "rs_x": + ev["value"] = 0.0 if rs_in_deadzone else rs_x_corr + elif ev["code"] == "rs_y": + ev["value"] = 0.0 if rs_in_deadzone else rs_y_corr + + # Keep stick axes consistent when only one axis was reported this frame, + # and propagate trigger-driven LS correction even if sticks were unchanged. + if rt_seen or ls_x_seen or ls_y_seen: + if not ls_x_seen: + evs.append( + { + "type": "axis", + "code": "ls_x", + "value": 0.0 if ls_in_deadzone else ls_x_corr, + } + ) + if not ls_y_seen: + evs.append( + { + "type": "axis", + "code": "ls_y", + "value": 0.0 if ls_in_deadzone else ls_y_corr, + } + ) + + if rs_x_seen or rs_y_seen: + if not rs_x_seen: + evs.append( + { + "type": "axis", + "code": "rs_x", + "value": 0.0 if rs_in_deadzone else rs_x_corr, + } + ) + if not rs_y_seen: + evs.append( + { + "type": "axis", + "code": "rs_y", + "value": 0.0 if rs_in_deadzone else rs_y_corr, + } + ) + evs = multiplexer.process(evs) if evs: if debug: diff --git a/src/hhd/device/rog_ally/controllers.yml b/src/hhd/device/rog_ally/controllers.yml index 1cc28556..401c515f 100644 --- a/src/hhd/device/rog_ally/controllers.yml +++ b/src/hhd/device/rog_ally/controllers.yml @@ -75,3 +75,5 @@ children: tags: [advanced] title: RGB During Charging Asleep default: False + + hall_interference: diff --git a/src/hhd/plugins/__init__.py b/src/hhd/plugins/__init__.py index 917ec71a..c35f1b8d 100644 --- a/src/hhd/plugins/__init__.py +++ b/src/hhd/plugins/__init__.py @@ -1,7 +1,10 @@ from .conf import Config from .inputs import gen_gyro_state, get_gyro_config, get_gyro_state, get_touchpad_config from .outputs import ( + fix_hall_interference, fix_limits, + get_hall_interference, + get_hall_interference_config, get_limits, get_limits_config, get_outputs, @@ -39,7 +42,10 @@ "HHDLocale", "HHDLocaleRegister", "get_limits_config", + "get_hall_interference_config", "get_limits", + "get_hall_interference", "get_gid", "fix_limits", + "fix_hall_interference", ] diff --git a/src/hhd/plugins/hall_interference.yml b/src/hhd/plugins/hall_interference.yml new file mode 100644 index 00000000..f1ecc0f4 --- /dev/null +++ b/src/hhd/plugins/hall_interference.yml @@ -0,0 +1,92 @@ +type: mode +tags: [non-essential] +title: Hall Effect Interference Correction +hint: >- + Compensates left stick Hall trigger interference and stick idle drift. + +default: disabled +modes: + disabled: + type: container + title: Disabled + hint: >- + Disables Hall interference correction. + default: + type: container + title: Default + hint: >- + Uses built-in correction coefficients and deadzone. + manual: + type: container + title: Manual + hint: >- + Set LS/RS idle offsets and LT-to-LS correction manually. + children: + ls_idle_x: + type: float + title: Left Stick Idle X + hint: >- + Constant offset removed from left stick X around center. + min: -0.5 + max: 0.5 + smin: -500 + smax: 500 + step: 0.01 + + lt_corr_x: + type: float + title: LT Correction For LS X + hint: >- + Additional correction applied to left stick X as LT is pressed. + Uses RT axis input internally on devices where LT/RT are reversed. + min: -0.5 + max: 0.5 + smin: -500 + smax: 500 + step: 0.01 + + rs_idle_x: + type: float + title: Right Stick Idle X + hint: >- + Constant offset removed from right stick X around center. + min: -0.5 + max: 0.5 + smin: -500 + smax: 500 + step: 0.01 + + ls_deadzone: + type: float + title: Left Stick Deadzone + hint: >- + Values below this absolute magnitude are clamped to zero on left stick X/Y. + min: 0 + max: 0.3 + smin: 0 + smax: 300 + step: 0.01 + + rs_deadzone: + type: float + title: Right Stick Deadzone + hint: >- + Values below this absolute magnitude are clamped to zero on right stick X. + min: 0 + max: 0.3 + smin: 0 + smax: 300 + step: 0.01 + + remap_deadzone: + type: bool + title: Remove Deadzone From Scale + hint: >- + Rescales outputs outside deadzone to keep full range. + Uses (value - deadzone) / (1 - deadzone) with sign. + + reset: + type: action + title: Reset to Default + hint: >- + Reset manual Hall interference values to default. diff --git a/src/hhd/plugins/outputs.py b/src/hhd/plugins/outputs.py index d38ad635..a36fcf52 100644 --- a/src/hhd/plugins/outputs.py +++ b/src/hhd/plugins/outputs.py @@ -358,6 +358,18 @@ def get_limits_config(defaults: dict[str, int] = {}): return s +def get_hall_interference_config(defaults: dict[str, float | bool] = {}): + s = load_relative_yaml("hall_interference.yml") + hall = s["modes"]["manual"]["children"] + hall["ls_idle_x"]["default"] = defaults["ls_idle_x"] + hall["lt_corr_x"]["default"] = defaults["lt_corr_x"] + hall["rs_idle_x"]["default"] = defaults["rs_idle_x"] + hall["ls_deadzone"]["default"] = defaults["ls_deadzone"] + hall["rs_deadzone"]["default"] = defaults["rs_deadzone"] + hall["remap_deadzone"]["default"] = bool(defaults["remap_deadzone"]) + return s + + def get_limits(conf, defaults={}): if conf["mode"].to(str) != "manual": return defaults @@ -369,6 +381,12 @@ def get_limits(conf, defaults={}): return kconf.to(dict) +def get_hall_interference(conf, defaults={}): + if conf["mode"].to(str) != "manual": + return defaults + return conf["manual"].to(dict) + + def fix_limits(conf, prefix: str, defaults: dict[str, int] = {}): if conf[f"{prefix}.mode"].to(str) != "manual": return {} @@ -393,3 +411,17 @@ def fix_limits(conf, prefix: str, defaults: dict[str, int] = {}): f"{comp}_max", 95 ) conf[f"{prefix}.manual.reset"] = False + + +def fix_hall_interference(conf, prefix: str, defaults: dict[str, float | bool] = {}): + if conf[f"{prefix}.mode"].to(str) != "manual": + return {} + + if conf[f"{prefix}.manual.reset"].to(bool): + conf[f"{prefix}.manual.ls_idle_x"] = defaults["ls_idle_x"] + conf[f"{prefix}.manual.lt_corr_x"] = defaults["lt_corr_x"] + conf[f"{prefix}.manual.rs_idle_x"] = defaults["rs_idle_x"] + conf[f"{prefix}.manual.ls_deadzone"] = defaults["ls_deadzone"] + conf[f"{prefix}.manual.rs_deadzone"] = defaults["rs_deadzone"] + conf[f"{prefix}.manual.remap_deadzone"] = bool(defaults["remap_deadzone"]) + conf[f"{prefix}.manual.reset"] = False