Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@
3. 为每个轴配置输入源(陀螺仪或拖动条)、峰值、死区等参数
4. 保存并重启服务器

**配置优先级:**
- 摇杆配置优先从 `config/buttons.json` 读取(通过 Web 界面保存的用户配置)
- 如果 `buttons.json` 中没有配置,则使用 `config/config.py` 中的默认值作为后备
- 修改摇杆类型或轴数量后,需要重启服务器才能生效

详细配置说明请参考 [CUSTOM_JOYSTICK_GUIDE.md](CUSTOM_JOYSTICK_GUIDE.md)

## 技术细节
Expand Down
16 changes: 15 additions & 1 deletion server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] ✅ 虚拟摇杆已成功初始化")
Expand Down
77 changes: 42 additions & 35 deletions server/joystick_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
class CustomVirtualJoystick:
"""自定义多轴虚拟摇杆,支持可配置的轴数量。"""

# 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"):
"""
初始化自定义虚拟摇杆。
Expand Down Expand Up @@ -68,21 +72,25 @@ def _init_gamepad(self):
elif self.system == 'Linux':
try:
import uinput
# Initialize class constant on first use (thread-safe)
if CustomVirtualJoystick.UINPUT_AXIS_CODES is None:
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 = []
# 添加轴(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
Expand Down Expand Up @@ -135,18 +143,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}")
Expand All @@ -169,16 +167,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()

Expand All @@ -191,13 +202,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
Expand Down
51 changes: 38 additions & 13 deletions static/js/main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
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;
Expand Down Expand Up @@ -79,6 +88,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();
}, { immediate: true });

// Canvas state
let ctx = null;
let canvasWidth = 0;
Expand Down Expand Up @@ -162,19 +192,12 @@ 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,保证表格编辑能同步回 customAxisMapping
cfg.axisIndex = i;
list.push(cfg);
}
}
return list;
});
Expand Down Expand Up @@ -279,6 +302,8 @@ const app = createApp({
if (data.custom_joystick.axis_mapping) {
Object.assign(customAxisMapping, data.custom_joystick.axis_mapping);
}
// 初始化任何缺失的轴配置
initializeCustomAxisMapping();
}

// 加载驾驶模式配置
Expand Down