From 561330ea4e1db5e7aadee7261cdef754a9d2a49d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 07:04:25 +0000 Subject: [PATCH 1/6] Initial plan From 3445ffbd5c67cc93d1c681a58974d21031990f66 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 07:08:58 +0000 Subject: [PATCH 2/6] fix: address PR review comments - extract axis_codes, fix Vue computed mutation, use buttons.json config priority Co-authored-by: w1010tdev <246258262+w1010tdev@users.noreply.github.com> --- server/app.py | 16 ++++++++- server/joystick_manager.py | 73 ++++++++++++++++++++------------------ static/js/main.js | 44 ++++++++++++++++------- 3 files changed, 84 insertions(+), 49 deletions(-) diff --git a/server/app.py b/server/app.py index e3a3e43..9bfdc89 100644 --- a/server/app.py +++ b/server/app.py @@ -508,7 +508,21 @@ def init_virtual_joystick(): if config.MODE == 'driving': try: from joystick_manager import VirtualJoystick - virtual_joystick = VirtualJoystick() + # 加载配置(优先使用 buttons.json,否则使用 config.py) + button_config = load_config() + joystick_config = None + + # 从 buttons.json 构建摇杆配置 + if 'joystick_type' in button_config: + joystick_config = { + 'type': button_config['joystick_type'], + 'name': button_config.get('joystick_name', 'wtxrc Custom Joystick') + } + if button_config['joystick_type'] == 'custom' and 'custom_joystick' in button_config: + joystick_config['custom'] = button_config['custom_joystick'] + + # 传递配置到 VirtualJoystick(会自动回退到 config.py 如果 joystick_config 为 None) + virtual_joystick = VirtualJoystick(joystick_config=joystick_config) if virtual_joystick.initialized: if config.DEBUG: print("[INIT] ✅ 虚拟摇杆已成功初始化") diff --git a/server/joystick_manager.py b/server/joystick_manager.py index 56b0772..e27fa48 100644 --- a/server/joystick_manager.py +++ b/server/joystick_manager.py @@ -33,6 +33,9 @@ class CustomVirtualJoystick: """自定义多轴虚拟摇杆,支持可配置的轴数量。""" + # Linux uinput axis codes mapping (class constant to avoid duplication) + UINPUT_AXIS_CODES = None # Will be initialized when uinput is available + def __init__(self, axis_count=8, name="Custom Virtual Joystick"): """ 初始化自定义虚拟摇杆。 @@ -68,21 +71,22 @@ def _init_gamepad(self): elif self.system == 'Linux': try: import uinput + # Initialize class constant on first use + if CustomVirtualJoystick.UINPUT_AXIS_CODES is None: + CustomVirtualJoystick.UINPUT_AXIS_CODES = [ + uinput.ABS_X, uinput.ABS_Y, uinput.ABS_Z, + uinput.ABS_RX, uinput.ABS_RY, uinput.ABS_RZ, + uinput.ABS_THROTTLE, uinput.ABS_RUDDER, + uinput.ABS_WHEEL, uinput.ABS_GAS, uinput.ABS_BRAKE, + uinput.ABS_HAT0X, uinput.ABS_HAT0Y, uinput.ABS_HAT1X, + uinput.ABS_HAT1Y, uinput.ABS_HAT2X, uinput.ABS_HAT2Y, + uinput.ABS_HAT3X, uinput.ABS_HAT3Y, uinput.ABS_PRESSURE, + ] + # 创建包含指定数量轴的 uinput 设备 events = [] - # 添加轴(ABS_X, ABS_Y, ABS_Z, ABS_RX, ABS_RY, ABS_RZ, ABS_THROTTLE, ABS_RUDDER等) - axis_codes = [ - uinput.ABS_X, uinput.ABS_Y, uinput.ABS_Z, - uinput.ABS_RX, uinput.ABS_RY, uinput.ABS_RZ, - uinput.ABS_THROTTLE, uinput.ABS_RUDDER, - uinput.ABS_WHEEL, uinput.ABS_GAS, uinput.ABS_BRAKE, - uinput.ABS_HAT0X, uinput.ABS_HAT0Y, uinput.ABS_HAT1X, - uinput.ABS_HAT1Y, uinput.ABS_HAT2X, uinput.ABS_HAT2Y, - uinput.ABS_HAT3X, uinput.ABS_HAT3Y, uinput.ABS_PRESSURE, - ] - - for i in range(min(self.axis_count, len(axis_codes))): - events.append(axis_codes[i] + (-32767, 32767, 0, 0)) + for i in range(min(self.axis_count, len(CustomVirtualJoystick.UINPUT_AXIS_CODES))): + events.append(CustomVirtualJoystick.UINPUT_AXIS_CODES[i] + (-32767, 32767, 0, 0)) self.gamepad = uinput.Device(events, name=self.name) self.initialized = True @@ -135,18 +139,8 @@ def set_axis(self, axis_index, value): # 将 -1.0~1.0 映射到 -32767~32767 int_value = int(value * 32767) - axis_codes = [ - uinput.ABS_X, uinput.ABS_Y, uinput.ABS_Z, - uinput.ABS_RX, uinput.ABS_RY, uinput.ABS_RZ, - uinput.ABS_THROTTLE, uinput.ABS_RUDDER, - uinput.ABS_WHEEL, uinput.ABS_GAS, uinput.ABS_BRAKE, - uinput.ABS_HAT0X, uinput.ABS_HAT0Y, uinput.ABS_HAT1X, - uinput.ABS_HAT1Y, uinput.ABS_HAT2X, uinput.ABS_HAT2Y, - uinput.ABS_HAT3X, uinput.ABS_HAT3Y, uinput.ABS_PRESSURE, - ] - - if axis_index < len(axis_codes): - self.gamepad.emit(axis_codes[axis_index], int_value, syn=True) + if axis_index < len(CustomVirtualJoystick.UINPUT_AXIS_CODES): + self.gamepad.emit(CustomVirtualJoystick.UINPUT_AXIS_CODES[axis_index], int_value, syn=True) except Exception as e: if config and config.DEBUG: print(f"设置 Linux 自定义摇杆轴失败:{e}") @@ -169,16 +163,29 @@ def close(self): class VirtualJoystick: """Virtual joystick abstraction layer. 支持 Xbox 360 和自定义多轴模式。""" - def __init__(self): + def __init__(self, joystick_config=None): + """ + 初始化虚拟摇杆。 + + Args: + joystick_config: 摇杆配置字典(来自 buttons.json),如果为 None 则使用 config.py 的默认值 + 格式: {'type': 'xbox360'|'custom', 'custom': {'axis_count': 8, ...}} + """ self.system = platform.system() self.gamepad = None self.initialized = False self.joystick_type = "xbox360" # 默认使用 Xbox 360 self.custom_joystick = None # 自定义摇杆实例 - # 从配置中读取摇杆类型 - if HAS_CONFIG and hasattr(config, 'JOYSTICK_CONFIG'): + # 优先使用传入的配置(来自 buttons.json),否则使用 config.py 作为后备 + if joystick_config: + self.joystick_type = joystick_config.get('type', 'xbox360') + self._joystick_config = joystick_config + elif HAS_CONFIG and hasattr(config, 'JOYSTICK_CONFIG'): self.joystick_type = config.JOYSTICK_CONFIG.get('type', 'xbox360') + self._joystick_config = config.JOYSTICK_CONFIG + else: + self._joystick_config = {'type': 'xbox360'} self._init_gamepad() @@ -191,13 +198,9 @@ def _init_gamepad(self): """Initialize the virtual gamepad based on the platform and configuration.""" if self.joystick_type == "custom": # 使用自定义多轴摇杆 - if HAS_CONFIG and hasattr(config, 'JOYSTICK_CONFIG'): - custom_config = config.JOYSTICK_CONFIG.get('custom', {}) - axis_count = custom_config.get('axis_count', 8) - name = config.JOYSTICK_CONFIG.get('name', 'wtxrc Custom Joystick') - else: - axis_count = 8 - name = 'wtxrc Custom Joystick' + custom_config = self._joystick_config.get('custom', {}) + axis_count = custom_config.get('axis_count', 8) + name = self._joystick_config.get('name', 'wtxrc Custom Joystick') self.custom_joystick = CustomVirtualJoystick(axis_count=axis_count, name=name) self.initialized = self.custom_joystick.initialized diff --git a/static/js/main.js b/static/js/main.js index 0935d55..5f25bdd 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,4 +1,4 @@ -const { createApp, ref, reactive, computed, onMounted, onUnmounted, nextTick } = Vue; +const { createApp, ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } = Vue; // Constants const MIN_BUTTON_SIZE = 50; @@ -79,6 +79,27 @@ const app = createApp({ const customJoystickAxisCount = ref(8); // 自定义摇杆的轴数量 const customAxisMapping = reactive({}); // 自定义摇杆的轴映射 + // 初始化自定义摇杆轴映射(确保所有轴都有配置) + const initializeCustomAxisMapping = () => { + for (let i = 0; i < customJoystickAxisCount.value; i++) { + if (!customAxisMapping[i]) { + customAxisMapping[i] = { + source_type: 'none', + source_id: null, + peak_value: 1.0, + deadzone: 0.05, + gyro_range: 90.0, + invert: false + }; + } + } + }; + + // 监听轴数量变化,自动初始化新的轴配置 + watch(customJoystickAxisCount, () => { + initializeCustomAxisMapping(); + }); + // Canvas state let ctx = null; let canvasWidth = 0; @@ -162,19 +183,14 @@ const app = createApp({ const customAxisConfigList = computed(() => { const list = []; for (let i = 0; i < customJoystickAxisCount.value; i++) { - if (!customAxisMapping[i]) { - customAxisMapping[i] = { - source_type: 'none', - source_id: null, - peak_value: 1.0, - deadzone: 0.05, - gyro_range: 90.0, - invert: false - }; - } const cfg = customAxisMapping[i]; - cfg.axisIndex = i; - list.push(cfg); + if (cfg) { + // 创建一个新对象,包含 axisIndex,而不是直接修改 cfg + list.push({ + ...cfg, + axisIndex: i + }); + } } return list; }); @@ -279,6 +295,8 @@ const app = createApp({ if (data.custom_joystick.axis_mapping) { Object.assign(customAxisMapping, data.custom_joystick.axis_mapping); } + // 初始化任何缺失的轴配置 + initializeCustomAxisMapping(); } // 加载驾驶模式配置 From 44541277768e10cb9efb255795f5a7cc99b1877c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 07:09:25 +0000 Subject: [PATCH 3/6] docs: document configuration priority (buttons.json over config.py) Co-authored-by: w1010tdev <246258262+w1010tdev@users.noreply.github.com> --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 4264278..4939d68 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,11 @@ 3. 为每个轴配置输入源(陀螺仪或拖动条)、峰值、死区等参数 4. 保存并重启服务器 +**配置优先级:** +- 摇杆配置优先从 `config/buttons.json` 读取(通过 Web 界面保存的用户配置) +- 如果 `buttons.json` 中没有配置,则使用 `config/config.py` 中的默认值作为后备 +- 修改摇杆类型或轴数量后,需要重启服务器才能生效 + 详细配置说明请参考 [CUSTOM_JOYSTICK_GUIDE.md](CUSTOM_JOYSTICK_GUIDE.md) ## 技术细节 From 8744df13887a97df4aaf4aa30acd7b8e78e81d80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 07:11:00 +0000 Subject: [PATCH 4/6] fix: improve thread-safety and code readability Co-authored-by: w1010tdev <246258262+w1010tdev@users.noreply.github.com> --- server/joystick_manager.py | 28 ++++++++++++++++------------ static/js/main.js | 11 ++++++++++- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/server/joystick_manager.py b/server/joystick_manager.py index e27fa48..43cd670 100644 --- a/server/joystick_manager.py +++ b/server/joystick_manager.py @@ -33,8 +33,9 @@ class CustomVirtualJoystick: """自定义多轴虚拟摇杆,支持可配置的轴数量。""" - # Linux uinput axis codes mapping (class constant to avoid duplication) - UINPUT_AXIS_CODES = None # Will be initialized when uinput is available + # Linux uinput axis codes mapping (class constant, lazily initialized when uinput is available) + UINPUT_AXIS_CODES = None + _axis_codes_lock = threading.Lock() # Thread-safe initialization def __init__(self, axis_count=8, name="Custom Virtual Joystick"): """ @@ -71,17 +72,20 @@ def _init_gamepad(self): elif self.system == 'Linux': try: import uinput - # Initialize class constant on first use + # Initialize class constant on first use (thread-safe) if CustomVirtualJoystick.UINPUT_AXIS_CODES is None: - CustomVirtualJoystick.UINPUT_AXIS_CODES = [ - uinput.ABS_X, uinput.ABS_Y, uinput.ABS_Z, - uinput.ABS_RX, uinput.ABS_RY, uinput.ABS_RZ, - uinput.ABS_THROTTLE, uinput.ABS_RUDDER, - uinput.ABS_WHEEL, uinput.ABS_GAS, uinput.ABS_BRAKE, - uinput.ABS_HAT0X, uinput.ABS_HAT0Y, uinput.ABS_HAT1X, - uinput.ABS_HAT1Y, uinput.ABS_HAT2X, uinput.ABS_HAT2Y, - uinput.ABS_HAT3X, uinput.ABS_HAT3Y, uinput.ABS_PRESSURE, - ] + with CustomVirtualJoystick._axis_codes_lock: + # Double-check after acquiring lock + if CustomVirtualJoystick.UINPUT_AXIS_CODES is None: + CustomVirtualJoystick.UINPUT_AXIS_CODES = [ + uinput.ABS_X, uinput.ABS_Y, uinput.ABS_Z, + uinput.ABS_RX, uinput.ABS_RY, uinput.ABS_RZ, + uinput.ABS_THROTTLE, uinput.ABS_RUDDER, + uinput.ABS_WHEEL, uinput.ABS_GAS, uinput.ABS_BRAKE, + uinput.ABS_HAT0X, uinput.ABS_HAT0Y, uinput.ABS_HAT1X, + uinput.ABS_HAT1Y, uinput.ABS_HAT2X, uinput.ABS_HAT2Y, + uinput.ABS_HAT3X, uinput.ABS_HAT3Y, uinput.ABS_PRESSURE, + ] # 创建包含指定数量轴的 uinput 设备 events = [] diff --git a/static/js/main.js b/static/js/main.js index 5f25bdd..2b8c48c 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,4 +1,13 @@ -const { createApp, ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } = Vue; +const { + createApp, + ref, + reactive, + computed, + onMounted, + onUnmounted, + nextTick, + watch +} = Vue; // Constants const MIN_BUTTON_SIZE = 50; From 8d35924b9afbb1017525d636a5cacdada70d1bd5 Mon Sep 17 00:00:00 2001 From: Wang Taixi Date: Sat, 24 Jan 2026 09:23:16 +0800 Subject: [PATCH 5/6] Update static/js/main.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- static/js/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/main.js b/static/js/main.js index 2b8c48c..0b4df76 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -107,7 +107,7 @@ const app = createApp({ // 监听轴数量变化,自动初始化新的轴配置 watch(customJoystickAxisCount, () => { initializeCustomAxisMapping(); - }); + }, { immediate: true }); // Canvas state let ctx = null; From 2d61f9759028a3641ac27e49ed5e793238f4a954 Mon Sep 17 00:00:00 2001 From: Wang Taixi Date: Sat, 24 Jan 2026 09:24:20 +0800 Subject: [PATCH 6/6] Update static/js/main.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- static/js/main.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/static/js/main.js b/static/js/main.js index 0b4df76..3883d61 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -194,11 +194,9 @@ const app = createApp({ for (let i = 0; i < customJoystickAxisCount.value; i++) { const cfg = customAxisMapping[i]; if (cfg) { - // 创建一个新对象,包含 axisIndex,而不是直接修改 cfg - list.push({ - ...cfg, - axisIndex: i - }); + // 直接使用原始配置对象,并附加 axisIndex,保证表格编辑能同步回 customAxisMapping + cfg.axisIndex = i; + list.push(cfg); } } return list;