diff --git a/CUSTOM_JOYSTICK_GUIDE.md b/CUSTOM_JOYSTICK_GUIDE.md new file mode 100644 index 0000000..fc3a41e --- /dev/null +++ b/CUSTOM_JOYSTICK_GUIDE.md @@ -0,0 +1,183 @@ +# 自定义虚拟摇杆功能指南 + +## 概述 + +wtxrc 现在支持两种虚拟摇杆模式: + +1. **Xbox 360 模式**(默认):标准的 6 轴控制器(2个摇杆 + 2个扳机) +2. **自定义多轴摇杆模式**:可配置 1-32 个轴,适用于飞行模拟等需要更多控制轴的场景 + +## 功能特性 + +### 自定义摇杆模式 + +- ✅ 支持 1-32 个可配置的轴 +- ✅ 每个轴可独立绑定到陀螺仪或拖动条 +- ✅ 支持轴反转(正负方向对调) +- ✅ 独立的峰值、死区和陀螺仪范围设置 +- ✅ 适用于飞行模拟、太空模拟等多轴控制场景 + +### 轴映射配置 + +每个轴可以配置: +- **输入源**:未绑定、陀螺仪或拖动条 +- **源选择**:选择具体的陀螺仪轴(gamma/beta/alpha)或拖动条 +- **峰值**:轴的最大输出值(0.1-1.0) +- **死区**:忽略小幅输入变化的阈值(0-0.5) +- **陀螺仪范围**:转动多少度达到满输出(1-180度) +- **反转**:反转轴的方向(仅自定义模式) + +## 系统要求 + +### Windows +- **Xbox 360 模式**: + - ViGEmBus 驱动:https://github.com/ViGEm/ViGEmBus/releases + - Python 包:`pip install vgamepad` + +- **自定义模式**: + - vJoy 驱动:https://sourceforge.net/projects/vjoystick/ + - Python 包:`pip install pyvjoy` + +### Linux +- **Xbox 360 模式**: + - Python 包:`pip install python-uinput` + - 加载内核模块:`sudo modprobe uinput` + +- **自定义模式**: + - Python 包:`pip install python-uinput` + - 加载内核模块:`sudo modprobe uinput` + +## 配置方法 + +### 方法一:通过配置文件(config/config.py) + +```python +JOYSTICK_CONFIG = { + # 摇杆类型: "xbox360" 或 "custom" + "type": "custom", + + # 自定义摇杆配置(仅在 type="custom" 时使用) + "custom": { + # 轴的数量(1-32) + "axis_count": 8, + + # 轴映射配置 + "axis_mapping": { + 0: { + "source_type": "gyro", # "none", "gyro", "slider" + "source_id": "gamma", # 陀螺仪轴或拖动条ID + "peak_value": 1.0, # 峰值 + "deadzone": 0.05, # 死区 + "gyro_range": 90.0, # 陀螺仪范围(度) + "invert": False # 是否反转 + }, + 1: { + "source_type": "gyro", + "source_id": "beta", + "peak_value": 1.0, + "deadzone": 0.05, + "gyro_range": 90.0, + "invert": False + }, + # ... 更多轴配置 + } + } +} +``` + +### 方法二:通过 Web 界面 + +1. 在驾驶模式下,点击 **⚙️ 驾驶配置** 按钮 +2. 在对话框顶部选择 **摇杆类型**: + - 选择 **"自定义多轴摇杆"** +3. 设置 **轴数量**(1-32) +4. 为每个轴配置: + - **输入源**:选择未绑定、陀螺仪或拖动条 + - **源选择**:选择具体的陀螺仪轴或拖动条 + - **峰值**:调整最大输出强度 + - **死区**:设置死区阈值 + - **陀螺仪范围**:设置陀螺仪归一化范围 + - **反转**:根据需要反转轴方向 +5. 点击 **保存** 并 **重启服务器** + +## 使用场景示例 + +### 飞行模拟器 + +配置 6 个轴用于飞行控制: +- 轴 0:副翼(横滚) - 绑定到陀螺仪 gamma +- 轴 1:升降舵(俯仰) - 绑定到陀螺仪 beta +- 轴 2:方向舵(偏航) - 绑定到陀螺仪 alpha +- 轴 3:油门 - 绑定到拖动条 1 +- 轴 4:襟翼 - 绑定到拖动条 2 +- 轴 5:刹车 - 绑定到拖动条 3 + +### 太空模拟器 + +配置 12 个轴用于六自由度控制: +- 轴 0-2:平移(X/Y/Z) +- 轴 3-5:旋转(Roll/Pitch/Yaw) +- 轴 6-11:其他控制(引擎、武器等) + +## 调试和测试 + +### 检查虚拟摇杆状态 + +1. 启用调试模式:在 `config/config.py` 中设置 `DEBUG = True` +2. 查看控制台输出,确认: + - 虚拟摇杆初始化成功 + - 轴映射正确 + - 输入值正确传递到相应轴 + +### Windows 测试工具 +- 使用 Windows 自带的"设置 USB 游戏控制器"查看虚拟摇杆 +- 或使用第三方工具如 JoyTest + +### Linux 测试工具 +```bash +# 查看输入设备 +ls /dev/input/ + +# 使用 evtest 测试 +sudo evtest /dev/input/eventX +``` + +## 常见问题 + +### Q: 如何在 Xbox 360 和自定义模式之间切换? +A: 修改 `config/config.py` 中的 `JOYSTICK_CONFIG.type`,或在 Web 界面的驾驶配置中切换,然后重启服务器。 + +### Q: 自定义摇杆最多支持多少个轴? +A: Windows (vJoy) 支持最多 8 个轴,Linux (uinput) 支持最多 20 个轴(取决于可用的轴代码)。 + +### Q: 为什么虚拟摇杆初始化失败? +A: +- **Windows**: 确保已安装 vJoy 驱动和 pyvjoy 包 +- **Linux**: 确保已加载 uinput 模块并有适当权限 + +### Q: 可以同时使用陀螺仪和拖动条吗? +A: 是的!每个轴可以独立配置不同的输入源。 + +### Q: 轴反转功能有什么用? +A: 某些游戏可能需要反向的轴输入(例如反向俯仰),使用反转功能可以不用修改游戏设置就能调整。 + +## 技术细节 + +### 轴值范围 +- 输入范围:-1.0 到 1.0 +- Windows (vJoy):映射到 1-32768 +- Linux (uinput):映射到 -32767 到 32767 + +### 支持的陀螺仪轴 +- **gamma**:Y轴左右倾斜(设备向左右倾斜) +- **beta**:X轴前后倾斜(设备向前后倾斜) +- **alpha**:Z轴旋转(设备平面内旋转,0-360度) + +## 更新日志 + +### v1.0 - 自定义摇杆功能 +- ✅ 新增自定义多轴摇杆支持 +- ✅ 支持 1-32 个可配置轴 +- ✅ 新增轴反转功能 +- ✅ Web 界面配置支持 +- ✅ 同时支持 Xbox 360 和自定义模式 diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..73664bd --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,175 @@ +# 自定义虚拟摇杆功能实现总结 + +## 概述 + +本次更新为 wtxrc 项目添加了自定义虚拟摇杆功能,允许用户在 Xbox 360 标准模式和自定义多轴摇杆模式之间选择。自定义模式支持 1-32 个可配置轴,适用于飞行模拟、太空模拟等需要更多控制轴的场景。 + +## 主要变更 + +### 1. 后端更改 + +#### config/config.py +- 在 `JOYSTICK_CONFIG` 中添加了 `type` 字段("xbox360" 或 "custom") +- 添加了 `custom` 配置块,包含: + - `axis_count`: 轴数量(1-32) + - `axis_mapping`: 轴映射配置字典 + - 每个轴支持配置:`source_type`, `source_id`, `peak_value`, `deadzone`, `gyro_range`, `invert` + +#### server/joystick_manager.py +- 新增 `CustomVirtualJoystick` 类: + - 支持 Windows (pyvjoy) 和 Linux (uinput) + - 可配置 1-32 个轴 + - 每个轴独立控制 +- 修改 `VirtualJoystick` 类: + - 根据配置自动选择 Xbox 360 或自定义模式 + - `set_axis()` 方法支持轴索引(自定义模式)和轴名称(Xbox 360 模式) + - `reset()` 和 `close()` 方法支持两种模式 + +#### server/app.py +- 修改 `/api/config` 端点: + - 返回摇杆类型和自定义摇杆配置 +- 修改 `/api/update_driving_config` 端点: + - 支持保存摇杆类型和自定义摇杆配置 + - 支持保存 Xbox 360 模式的驾驶配置 +- 修改 `handle_gyro_data()` 函数: + - 根据摇杆类型选择不同的轴映射逻辑 + - 自定义模式使用 `custom.axis_mapping` + - Xbox 360 模式使用原有的 `axis_config` +- 修改 `handle_slider_value()` 函数: + - 支持自定义摇杆的轴映射 + +### 2. 前端更改 + +#### templates/index.html +- 在驾驶配置对话框中添加摇杆类型选择: + - Xbox 360 控制器(默认) + - 自定义多轴摇杆 +- 为 Xbox 360 模式保留原有的轴配置表格 +- 新增自定义摇杆配置界面: + - 轴数量输入框(1-32) + - 自定义轴配置表格 + - 支持配置输入源、源选择、峰值、死区、陀螺仪范围 + - 新增轴反转开关 + +#### static/js/main.js +- 添加状态变量: + - `joystickType`: 摇杆类型('xbox360' 或 'custom') + - `customJoystickAxisCount`: 自定义摇杆轴数量 + - `customAxisMapping`: 自定义摇杆轴映射配置 +- 添加计算属性: + - `customAxisConfigList`: 自定义轴配置列表 +- 添加函数: + - `onJoystickTypeChange()`: 摇杆类型改变处理 + - `onCustomAxisCountChange()`: 轴数量改变处理 + - `onCustomAxisSourceTypeChange()`: 自定义轴源类型改变处理 + - `getAvailableSlidersForCustomAxis()`: 获取可用的拖动条(排除已绑定) +- 修改 `loadConfig()`: + - 加载摇杆类型和自定义摇杆配置 +- 修改 `saveDrivingConfig()`: + - 根据摇杆类型保存不同的配置数据 + +### 3. 文档更新 + +#### 新增文件 +- `CUSTOM_JOYSTICK_GUIDE.md`: 详细的自定义摇杆功能指南 + - 功能特性说明 + - 系统要求(Windows/Linux) + - 配置方法(配置文件和 Web 界面) + - 使用场景示例 + - 调试和测试方法 + - 常见问题解答 + - 技术细节 + +#### 更新文件 +- `README.md`: + - 在功能列表中添加自定义虚拟摇杆说明 + - 更新安装说明,区分 Xbox 360 和自定义模式的依赖 + - 添加虚拟摇杆配置部分 + - 添加到详细指南的链接 + +- `requirements.txt`: + - 添加 pyvjoy 的说明(自定义摇杆模式) + - 区分 Xbox 360 模式和自定义模式的依赖 + +## 功能亮点 + +### 1. 灵活的轴配置 +- 每个轴可以独立配置输入源(陀螺仪或拖动条) +- 支持峰值、死区和陀螺仪范围的精细调整 +- 自定义模式支持轴反转功能 + +### 2. 向后兼容 +- 保留了原有的 Xbox 360 模式作为默认选项 +- 现有配置无需修改即可继续使用 +- 配置文件自动迁移 + +### 3. 友好的用户界面 +- Web 界面提供直观的配置选项 +- 表格形式展示所有轴的配置 +- 实时验证(如防止多个轴绑定同一拖动条) + +### 4. 跨平台支持 +- Windows: 支持 vgamepad (Xbox 360) 和 pyvjoy (自定义) +- Linux: 统一使用 uinput + +## 使用示例 + +### 配置 8 轴飞行摇杆 + +```python +# config/config.py +JOYSTICK_CONFIG = { + "type": "custom", + "custom": { + "axis_count": 8, + "axis_mapping": { + 0: {"source_type": "gyro", "source_id": "gamma", "peak_value": 1.0, "deadzone": 0.05, "gyro_range": 45.0, "invert": False}, # 副翼 + 1: {"source_type": "gyro", "source_id": "beta", "peak_value": 1.0, "deadzone": 0.05, "gyro_range": 45.0, "invert": False}, # 升降舵 + 2: {"source_type": "slider", "source_id": "slider1", "peak_value": 1.0, "deadzone": 0.05, "gyro_range": 90.0, "invert": False}, # 油门 + 3: {"source_type": "slider", "source_id": "slider2", "peak_value": 1.0, "deadzone": 0.05, "gyro_range": 90.0, "invert": False}, # 方向舵 + 4: {"source_type": "slider", "source_id": "slider3", "peak_value": 1.0, "deadzone": 0.05, "gyro_range": 90.0, "invert": False}, # 襟翼 + 5: {"source_type": "none", "source_id": None, "peak_value": 1.0, "deadzone": 0.05, "gyro_range": 90.0, "invert": False}, + 6: {"source_type": "none", "source_id": None, "peak_value": 1.0, "deadzone": 0.05, "gyro_range": 90.0, "invert": False}, + 7: {"source_type": "none", "source_id": None, "peak_value": 1.0, "deadzone": 0.05, "gyro_range": 90.0, "invert": False}, + } + } +} +``` + +## 测试建议 + +### 基本功能测试 +1. ✅ 验证 Xbox 360 模式仍然正常工作 +2. ✅ 验证自定义模式可以创建虚拟摇杆 +3. ✅ 验证轴映射正确(陀螺仪和拖动条) +4. ✅ 验证死区和峰值配置生效 +5. ✅ 验证轴反转功能 + +### 界面测试 +1. ✅ 验证摇杆类型切换 +2. ✅ 验证轴数量调整 +3. ✅ 验证配置保存和加载 +4. ✅ 验证拖动条绑定限制(一个拖动条不能绑定到多个轴) + +### 跨平台测试 +1. Windows + vJoy +2. Linux + uinput + +## 已知限制 + +1. **Windows vJoy 限制**: 最多支持 8 个轴 +2. **Linux uinput 限制**: 取决于可用的轴代码,通常最多 20 个轴 +3. **配置生效**: 修改摇杆类型需要重启服务器 +4. **驱动依赖**: 需要安装相应的虚拟摇杆驱动 + +## 未来改进建议 + +1. 支持更多轴类型(按钮、POV 帽等) +2. 预设配置模板(飞行、赛车、太空等) +3. 热重载配置(无需重启服务器) +4. 轴校准功能 +5. 实时轴状态显示 + +## 总结 + +本次更新成功为 wtxrc 添加了强大的自定义虚拟摇杆功能,使其能够应对更多样化的游戏控制需求。通过灵活的轴配置和友好的 Web 界面,用户可以轻松定制适合自己游戏的控制方案。 diff --git a/QUICK_START_CUSTOM_JOYSTICK.md b/QUICK_START_CUSTOM_JOYSTICK.md new file mode 100644 index 0000000..7b04c49 --- /dev/null +++ b/QUICK_START_CUSTOM_JOYSTICK.md @@ -0,0 +1,220 @@ +# 快速开始:自定义虚拟摇杆 + +## 示例 1:基本的飞行摇杆配置(4 轴) + +### 场景 +使用手机/平板作为飞行模拟器的控制器,配置 4 个轴: +- 轴 0:副翼控制(左右倾斜设备) +- 轴 1:升降舵控制(前后倾斜设备) +- 轴 2:油门(使用拖动条) +- 轴 3:方向舵(使用拖动条) + +### 步骤 + +1. **安装依赖**(Windows): + ```bash + # 下载并安装 vJoy 驱动 + # https://sourceforge.net/projects/vjoystick/ + + # 安装 Python 包 + pip install pyvjoy + ``` + +2. **修改 config/config.py**: + ```python + MODE = "driving" + + JOYSTICK_CONFIG = { + "type": "custom", + "name": "Flight Stick", + "custom": { + "axis_count": 4, + "axis_mapping": { + 0: { + "source_type": "gyro", + "source_id": "gamma", + "peak_value": 1.0, + "deadzone": 0.05, + "gyro_range": 45.0, + "invert": False + }, + 1: { + "source_type": "gyro", + "source_id": "beta", + "peak_value": 1.0, + "deadzone": 0.05, + "gyro_range": 45.0, + "invert": False + }, + 2: { + "source_type": "slider", + "source_id": "slider_throttle", + "peak_value": 1.0, + "deadzone": 0.02, + "gyro_range": 90.0, + "invert": False + }, + 3: { + "source_type": "slider", + "source_id": "slider_rudder", + "peak_value": 1.0, + "deadzone": 0.02, + "gyro_range": 90.0, + "invert": False + } + } + } + } + ``` + +3. **启动服务器**: + ```bash + python server/app.py + ``` + +4. **在 Web 界面添加拖动条**: + - 点击"编辑"按钮 + - 点击"+ 添加拖动条" + - 创建两个拖动条: + - ID: `slider_throttle`,标签: "油门",方向: 竖向 + - ID: `slider_rudder`,标签: "方向舵",方向: 竖向 + +5. **测试**: + - 设为主设备 + - 倾斜设备测试副翼和升降舵 + - 拖动条控制油门和方向舵 + +## 示例 2:使用 Web 界面配置 + +1. **启动服务器**(使用默认配置): + ```bash + python server/app.py + ``` + +2. **在 Web 界面配置**: + - 连接到服务器 + - 点击"⚙️ 驾驶配置" + - 在对话框顶部选择"自定义多轴摇杆" + - 设置轴数量为 4 + - 为每个轴配置: + + | 轴索引 | 输入源 | 源选择 | 峰值 | 死区 | 陀螺仪范围 | 反转 | + |--------|--------|--------|------|------|------------|------| + | 轴 0 | 陀螺仪 | Gamma | 1.0 | 0.05 | 45 | ❌ | + | 轴 1 | 陀螺仪 | Beta | 1.0 | 0.05 | 45 | ❌ | + | 轴 2 | 拖动条 | 油门 | 1.0 | 0.02 | 90 | ❌ | + | 轴 3 | 拖动条 | 方向舵 | 1.0 | 0.02 | 90 | ❌ | + +3. **保存并重启服务器** + +## 示例 3:太空模拟器(6 自由度,12 轴) + +```python +JOYSTICK_CONFIG = { + "type": "custom", + "name": "6DOF Space Controller", + "custom": { + "axis_count": 12, + "axis_mapping": { + # 平移控制 + 0: {"source_type": "slider", "source_id": "translate_x", ...}, + 1: {"source_type": "slider", "source_id": "translate_y", ...}, + 2: {"source_type": "slider", "source_id": "translate_z", ...}, + + # 旋转控制 + 3: {"source_type": "gyro", "source_id": "gamma", ...}, # Roll + 4: {"source_type": "gyro", "source_id": "beta", ...}, # Pitch + 5: {"source_type": "gyro", "source_id": "alpha", ...}, # Yaw + + # 引擎控制 + 6: {"source_type": "slider", "source_id": "main_engine", ...}, + 7: {"source_type": "slider", "source_id": "strafe_left", ...}, + 8: {"source_type": "slider", "source_id": "strafe_right", ...}, + + # 其他控制 + 9: {"source_type": "none", ...}, + 10: {"source_type": "none", ...}, + 11: {"source_type": "none", ...} + } + } +} +``` + +## 常见配置技巧 + +### 1. 轴反转 +如果游戏中的轴方向与你的习惯相反,使用反转功能: +```python +"invert": True # 将正值变为负值,负值变为正值 +``` + +### 2. 调整灵敏度 +通过 `peak_value` 调整轴的灵敏度: +```python +"peak_value": 0.5 # 50% 灵敏度 +"peak_value": 1.0 # 100% 灵敏度(默认) +``` + +### 3. 减少漂移 +增加死区值来减少轴漂移: +```python +"deadzone": 0.1 # 10% 死区,忽略小于此值的输入 +``` + +### 4. 陀螺仪范围 +根据你的控制习惯调整陀螺仪范围: +```python +"gyro_range": 30.0 # 转动 30 度即达到最大值(高灵敏度) +"gyro_range": 90.0 # 转动 90 度即达到最大值(默认) +"gyro_range": 180.0 # 转动 180 度即达到最大值(低灵敏度) +``` + +## 调试提示 + +### 查看虚拟摇杆是否创建成功 +**Windows:** +- 打开"设置" → "设备" → "蓝牙和其他设备" +- 或者运行 `joy.cpl` 查看游戏控制器 + +**Linux:** +```bash +# 查看输入设备 +ls /dev/input/ + +# 实时监控 +sudo evtest /dev/input/eventX +``` + +### 启用调试日志 +在 `config/config.py` 中: +```python +DEBUG = True +``` + +查看控制台输出以了解轴值变化。 + +## 故障排除 + +### 问题:虚拟摇杆未创建 +**检查项:** +- ✅ 是否安装了相应的驱动(vJoy 或 ViGEmBus) +- ✅ 是否安装了 Python 包(pyvjoy 或 vgamepad) +- ✅ 是否重启了服务器 +- ✅ 查看控制台是否有错误消息 + +### 问题:轴没有响应 +**检查项:** +- ✅ 确认配置中的 `source_id` 与拖动条的 ID 匹配 +- ✅ 确认设备已设为主设备(陀螺仪输入) +- ✅ 检查死区设置是否过大 +- ✅ 启用调试模式查看轴值 + +### 问题:轴方向相反 +**解决方案:** +- 在配置中设置 `"invert": True` + +## 更多帮助 + +- 详细文档:[CUSTOM_JOYSTICK_GUIDE.md](CUSTOM_JOYSTICK_GUIDE.md) +- 实现细节:[IMPLEMENTATION_SUMMARY.md](IMPLEMENTATION_SUMMARY.md) +- 问题反馈:提交 GitHub Issue diff --git a/README.md b/README.md index 1dc2c7e..4939d68 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - **悬浮覆盖层** — 在 PC 上显示当前被按下的按键 - **低延迟** — 使用 WebSocket 以尽可能降低延迟 - **驾驶模式** — 使用设备陀螺仪模拟方向盘 +- **🎮 自定义虚拟摇杆** — 支持 Xbox 360 和自定义多轴摇杆(1-32轴),适用于飞行模拟等场景 ## 安装 @@ -18,8 +19,9 @@ ``` 2. (可选)驾驶模式需要虚拟摇杆: - - **Windows**:从 https://github.com/ViGEm/ViGEmBus/releases 安装 ViGEmBus 驱动,然后 `pip install vgamepad` - - **Linux**:`pip install python-uinput`(可能需要 sudo 权限) + - **Windows (Xbox 360 模式)**:从 https://github.com/ViGEm/ViGEmBus/releases 安装 ViGEmBus 驱动,然后 `pip install vgamepad` + - **Windows (自定义摇杆模式)**:从 https://sourceforge.net/projects/vjoystick/ 安装 vJoy 驱动,然后 `pip install pyvjoy` + - **Linux**:`pip install python-uinput`(可能需要 sudo 权限),并加载内核模块 `sudo modprobe uinput` ## 使用方法 @@ -66,6 +68,25 @@ 3. 主设备的陀螺仪数据将用于转向控制 4. 左右倾斜设备即可转向 +#### 虚拟摇杆配置 +驾驶模式支持两种虚拟摇杆: + +- **Xbox 360 模式**(默认):标准 6 轴控制器(2个摇杆 + 2个扳机) +- **自定义多轴摇杆模式**:支持 1-32 个轴,适用于飞行模拟、太空模拟等场景 + +**配置方法:** +1. 点击界面上的 **⚙️ 驾驶配置** 按钮 +2. 在对话框顶部选择摇杆类型 +3. 为每个轴配置输入源(陀螺仪或拖动条)、峰值、死区等参数 +4. 保存并重启服务器 + +**配置优先级:** +- 摇杆配置优先从 `config/buttons.json` 读取(通过 Web 界面保存的用户配置) +- 如果 `buttons.json` 中没有配置,则使用 `config/config.py` 中的默认值作为后备 +- 修改摇杆类型或轴数量后,需要重启服务器才能生效 + +详细配置说明请参考 [CUSTOM_JOYSTICK_GUIDE.md](CUSTOM_JOYSTICK_GUIDE.md) + ## 技术细节 ### 触控/指针处理 diff --git a/config/buttons.json b/config/buttons.json index fb8fd81..54364e6 100644 --- a/config/buttons.json +++ b/config/buttons.json @@ -90,5 +90,51 @@ "gyro_range": 90.0 } } + }, + "joystick_type": "custom", + "custom_joystick": { + "axis_count": 5, + "axis_mapping": { + "0": { + "source_type": "gyro", + "source_id": "gamma", + "peak_value": 1, + "deadzone": 0.05, + "gyro_range": 90, + "invert": false + }, + "1": { + "source_type": "gyro", + "source_id": "beta", + "peak_value": 1, + "deadzone": 0.05, + "gyro_range": 90, + "invert": false + }, + "2": { + "source_type": "gyro", + "source_id": "alpha", + "peak_value": 1, + "deadzone": 0.05, + "gyro_range": 90, + "invert": false + }, + "3": { + "source_type": "slider", + "source_id": "btn6", + "peak_value": 1, + "deadzone": 0.05, + "gyro_range": 90, + "invert": false + }, + "4": { + "source_type": "slider", + "source_id": "btn12", + "peak_value": 1, + "deadzone": 0.05, + "gyro_range": 90, + "invert": false + } + } } } \ No newline at end of file diff --git a/config/config.py b/config/config.py index ece4063..3fdf9ce 100644 --- a/config/config.py +++ b/config/config.py @@ -9,7 +9,7 @@ # 调试选项 DEBUG = True # 是否输出详细日志 -SHOW_JOYSTICK_MONITOR = True # 是否显示虚拟手柄监视器悬浮窗 +SHOW_JOYSTICK_MONITOR = False # 是否显示虚拟手柄监视器悬浮窗 # 服务器配置 SERVER_HOST = "0.0.0.0" @@ -86,11 +86,61 @@ # Joystick Settings (for driving mode) JOYSTICK_CONFIG = { + # 摇杆类型: "xbox360" 或 "custom" + # xbox360: 使用 Xbox 360 控制器(6轴:left_x, left_y, right_x, right_y, left_trigger, right_trigger) + # custom: 使用自定义多轴摇杆(可配置轴数量,适用于飞行模拟等场景) + "type": "xbox360", # "xbox360" or "custom" + # Virtual joystick name "name": "wtxrc 虚拟摇杆", + # Axis range "axis_min": -32767, "axis_max": 32767, + + # 自定义摇杆配置(仅在 type="custom" 时使用) + "custom": { + # 轴的数量(1-32) + "axis_count": 8, + + # 轴映射配置:将 gyro/slider 映射到自定义摇杆的轴 + # 键是轴索引(0-based),值是源配置 + "axis_mapping": { + # 示例: + # 0: { + # "source_type": "gyro", # "none", "gyro", "slider" + # "source_id": "gamma", # 陀螺仪轴名称或拖动条ID + # "peak_value": 1.0, # 峰值(最大输出) + # "deadzone": 0.05, # 死区 + # "gyro_range": 90.0, # 陀螺仪归一化范围(度) + # "invert": False # 是否反转轴 + # }, + 0: { + "source_type": "gyro", + "source_id": "gamma", + "peak_value": 1.0, + "deadzone": 0.05, + "gyro_range": 90.0, + "invert": False + }, + 1: { + "source_type": "gyro", + "source_id": "beta", + "peak_value": 1.0, + "deadzone": 0.05, + "gyro_range": 90.0, + "invert": False + }, + 2: { + "source_type": "none", + "source_id": None, + "peak_value": 1.0, + "deadzone": 0.05, + "gyro_range": 90.0, + "invert": False + } + } + } } # Supported Modifier Keys diff --git a/requirements.txt b/requirements.txt index 8873168..2d4f97f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,11 @@ pynput eventlet # 可选:驾驶模式下用于虚拟摇杆的依赖 -# Windows: pip install vgamepad (also requires ViGEmBus driver) -# Linux: pip install python-uinput + +# Xbox 360 模式(标准 6 轴控制器) +# Windows: pip install vgamepad (also requires ViGEmBus driver from https://github.com/ViGEm/ViGEmBus/releases) +# Linux: pip install python-uinput (may require sudo permissions) + +# 自定义多轴摇杆模式(支持 1-32 轴,适用于飞行模拟等) +# Windows: pip install pyvjoy (also requires vJoy driver from https://sourceforge.net/projects/vjoystick/) +# Linux: pip install python-uinput (may require sudo permissions) diff --git a/server/app.py b/server/app.py index bef5644..9bfdc89 100644 --- a/server/app.py +++ b/server/app.py @@ -60,6 +60,13 @@ def get_config(): button_config['modifier_keys'] = config.MODIFIER_KEYS button_config['special_keys'] = config.SPECIAL_KEYS + # 加载摇杆配置(优先使用 buttons.json 中的用户配置,否则使用 config.py 的默认值) + if 'joystick_type' not in button_config and hasattr(config, 'JOYSTICK_CONFIG'): + button_config['joystick_type'] = config.JOYSTICK_CONFIG.get('type', 'xbox360') + if 'custom_joystick' not in button_config and hasattr(config, 'JOYSTICK_CONFIG'): + if button_config.get('joystick_type') == 'custom': + button_config['custom_joystick'] = config.JOYSTICK_CONFIG.get('custom', {}) + # 优先使用 buttons.json 中的 driving_config,如果没有则使用 config.py 中的默认值 if config.MODE == 'driving': if 'driving_config' not in button_config: @@ -116,7 +123,7 @@ def delete_button(): @app.route('/api/update_driving_config', methods=['POST']) def update_driving_config(): - """更新驾驶模式配置(陀螺仪轴映射和拖动条)""" + """更新驾驶模式配置(陀螺仪轴映射和拖动条,或自定义摇杆配置)""" try: if config.DEBUG: print("[CONFIG] 收到驾驶配置更新请求") @@ -126,18 +133,28 @@ def update_driving_config(): data = request.json if config.DEBUG: print(f"[CONFIG] 请求数据: {data}") - if data: - driving_config = data.get('driving_config', {}) - print(f"[CONFIG] 驾驶配置内容: {driving_config}") - # 保存到config.py中需要重启服务器 - # 这里我们保存到buttons.json中 + # 保存到buttons.json中 current_config = load_config() - driving_config = data.get('driving_config', {}) if data else {} - current_config['driving_config'] = driving_config - if config.DEBUG: - print(f"[CONFIG] 保存驾驶配置: {driving_config}") + # 检查是否包含摇杆类型配置 + if data and 'joystick_type' in data: + current_config['joystick_type'] = data['joystick_type'] + if config.DEBUG: + print(f"[CONFIG] 摇杆类型: {data['joystick_type']}") + + # 如果是自定义摇杆,保存自定义配置 + if data['joystick_type'] == 'custom' and 'custom_joystick' in data: + current_config['custom_joystick'] = data['custom_joystick'] + if config.DEBUG: + print(f"[CONFIG] 自定义摇杆配置: {data['custom_joystick']}") + + # 如果包含 Xbox 360 的驾驶配置 + if data and 'driving_config' in data: + driving_config = data['driving_config'] + current_config['driving_config'] = driving_config + if config.DEBUG: + print(f"[CONFIG] 驾驶配置内容: {driving_config}") save_config(current_config) @@ -148,6 +165,12 @@ def update_driving_config(): if config.DEBUG: print(f"[CONFIG] 返回响应: {response.get_json()}") return response + except Exception as e: + if config.DEBUG: + print(f"[CONFIG] 保存配置时发生错误: {e}") + import traceback + traceback.print_exc() + return jsonify({'status': 'error', 'message': str(e)}), 500 except Exception as e: print(f"[CONFIG] 错误: {e}") @@ -223,43 +246,76 @@ def handle_gyro_data(data): # 应用陀螺仪数据到虚拟手柄(如果已初始化) if virtual_joystick and virtual_joystick.initialized: button_config = load_config() - axis_config = button_config.get('driving_config', {}).get('axis_config', {}) - # 如果没有新的轴配置,回退到旧的 gyro_axis_mapping - if not axis_config: - gyro_mapping = button_config.get('driving_config', {}).get('gyro_axis_mapping', {}) - if not gyro_mapping: - gyro_mapping = config.DRIVING_CONFIG.get('gyro_axis_mapping', {}) + # 检查是否使用自定义摇杆模式(优先使用 buttons.json,否则使用 config.py) + joystick_type = button_config.get('joystick_type', config.JOYSTICK_CONFIG.get('type', 'xbox360')) + + if joystick_type == 'custom': + # 使用自定义摇杆的轴映射(优先使用 buttons.json) + custom_config = button_config.get('custom_joystick', config.JOYSTICK_CONFIG.get('custom', {})) + axis_mapping = custom_config.get('axis_mapping', {}) - # 使用旧的映射方式(使用 LEGACY_GYRO_RANGE 保持向后兼容) - gyro_values = {'alpha': alpha, 'beta': beta, 'gamma': gamma} - for gyro_axis, gamepad_axis in gyro_mapping.items(): - if gamepad_axis and gyro_axis in gyro_values: - value = normalize_gyro_value(gyro_values[gyro_axis], gyro_axis, LEGACY_GYRO_RANGE) - if config.DEBUG: - print(f"[GYRO] 映射 {gyro_axis}({gyro_values[gyro_axis]:.2f}) -> {gamepad_axis}({value:.2f})") - virtual_joystick.set_axis(gamepad_axis, value) - else: - # 使用新的统一轴配置 gyro_values = {'alpha': alpha, 'beta': beta, 'gamma': gamma} - for gamepad_axis, axis_cfg in axis_config.items(): + for axis_index, axis_cfg in axis_mapping.items(): + if isinstance(axis_index, str): + axis_index = int(axis_index) + if axis_cfg.get('source_type') == 'gyro' and axis_cfg.get('source_id'): gyro_axis = axis_cfg['source_id'] if gyro_axis in gyro_values: - gyro_range = axis_cfg.get('gyro_range', 45.0) # 获取陀螺仪范围,默认45度 + gyro_range = axis_cfg.get('gyro_range', 90.0) raw_value = normalize_gyro_value(gyro_values[gyro_axis], gyro_axis, gyro_range) + # 应用死区 value = apply_deadzone(raw_value, axis_cfg.get('deadzone', 0.05)) # 应用峰值限制 value = apply_peak_value(value, axis_cfg.get('peak_value', 1.0)) + # 应用反转 + if axis_cfg.get('invert', False): + value = -value + if config.DEBUG: - print(f"[GYRO] 映射 {gyro_axis}({gyro_values[gyro_axis]:.2f}) -> {gamepad_axis}({value:.2f}) [range={gyro_range}, deadzone={axis_cfg.get('deadzone', 0.05)}, peak={axis_cfg.get('peak_value', 1.0)}]") + print(f"[GYRO] 自定义摇杆: 映射 {gyro_axis}({gyro_values[gyro_axis]:.2f}) -> 轴{axis_index}({value:.2f})") + virtual_joystick.set_axis(axis_index, value) + else: + # 使用 Xbox 360 模式的轴配置 + axis_config = button_config.get('driving_config', {}).get('axis_config', {}) + + # 如果没有新的轴配置,回退到旧的 gyro_axis_mapping + if not axis_config: + gyro_mapping = button_config.get('driving_config', {}).get('gyro_axis_mapping', {}) + if not gyro_mapping: + gyro_mapping = config.DRIVING_CONFIG.get('gyro_axis_mapping', {}) + + # 使用旧的映射方式(使用 LEGACY_GYRO_RANGE 保持向后兼容) + gyro_values = {'alpha': alpha, 'beta': beta, 'gamma': gamma} + for gyro_axis, gamepad_axis in gyro_mapping.items(): + if gamepad_axis and gyro_axis in gyro_values: + value = normalize_gyro_value(gyro_values[gyro_axis], gyro_axis, LEGACY_GYRO_RANGE) + if config.DEBUG: + print(f"[GYRO] 映射 {gyro_axis}({gyro_values[gyro_axis]:.2f}) -> {gamepad_axis}({value:.2f})") virtual_joystick.set_axis(gamepad_axis, value) - elif axis_cfg.get('source_type') == 'none': - # 当轴配置为 none 时,显式将该轴重置为 0,避免保留上一次的陀螺仪值 - if config.DEBUG: - print(f"[GYRO] 轴 {gamepad_axis} 的 source_type=none,重置为 0") - virtual_joystick.set_axis(gamepad_axis, 0.0) + else: + # 使用新的统一轴配置 + gyro_values = {'alpha': alpha, 'beta': beta, 'gamma': gamma} + for gamepad_axis, axis_cfg in axis_config.items(): + if axis_cfg.get('source_type') == 'gyro' and axis_cfg.get('source_id'): + gyro_axis = axis_cfg['source_id'] + if gyro_axis in gyro_values: + gyro_range = axis_cfg.get('gyro_range', 45.0) # 获取陀螺仪范围,默认45度 + raw_value = normalize_gyro_value(gyro_values[gyro_axis], gyro_axis, gyro_range) + # 应用死区 + value = apply_deadzone(raw_value, axis_cfg.get('deadzone', 0.05)) + # 应用峰值限制 + value = apply_peak_value(value, axis_cfg.get('peak_value', 1.0)) + if config.DEBUG: + print(f"[GYRO] 映射 {gyro_axis}({gyro_values[gyro_axis]:.2f}) -> {gamepad_axis}({value:.2f}) [range={gyro_range}, deadzone={axis_cfg.get('deadzone', 0.05)}, peak={axis_cfg.get('peak_value', 1.0)}]") + virtual_joystick.set_axis(gamepad_axis, value) + elif axis_cfg.get('source_type') == 'none': + # 当轴配置为 none 时,显式将该轴重置为 0,避免保留上一次的陀螺仪值 + if config.DEBUG: + print(f"[GYRO] 轴 {gamepad_axis} 的 source_type=none,重置为 0") + virtual_joystick.set_axis(gamepad_axis, 0.0) else: if config.DEBUG: print("[GYRO] 警告: 虚拟摇杆未初始化") @@ -309,51 +365,81 @@ def handle_slider_value(data): if config.DEBUG: print("[SLIDER] 虚拟摇杆已初始化,应用拖动条值") button_config = load_config() - axis_config = button_config.get('driving_config', {}).get('axis_config', {}) buttons = button_config.get('buttons', []) + # 找到滑块的展示标签/autoCenter 信息(如果存在) slider_btn = next((b for b in buttons if b.get('id') == slider_id and b.get('type') == 'slider'), None) slider_label = slider_btn.get('label') if slider_btn else slider_id slider_auto_center = bool(slider_btn.get('autoCenter')) if slider_btn else False + # 显示 overlay(实时显示正在操作的滑块) try: overlay_queue.put({'cmd': 'SHOW', 'text': f"{slider_label}: {value:.2f}"}) except Exception: pass - # 如果没有新的轴配置,回退到旧方式 - if not axis_config: - if config.DEBUG: - print("[SLIDER] 警告: 找不到按钮新版配置") - buttons = button_config.get('buttons', []) - slider = next((b for b in buttons if b.get('id') == slider_id and b.get('type') == 'slider'), None) + # 检查是否使用自定义摇杆模式(优先使用 buttons.json,否则使用 config.py) + joystick_type = button_config.get('joystick_type', config.JOYSTICK_CONFIG.get('type', 'xbox360')) + + if joystick_type == 'custom': + # 使用自定义摇杆的轴映射(优先使用 buttons.json) + custom_config = button_config.get('custom_joystick', config.JOYSTICK_CONFIG.get('custom', {})) + axis_mapping = custom_config.get('axis_mapping', {}) - if slider and slider.get('axis'): - axis = slider['axis'] - if config.DEBUG: - print(f"[SLIDER] 应用到轴: {axis} = {value:.3f}") - virtual_joystick.set_axis(axis, value) - else: - if config.DEBUG: - print(f"[SLIDER] 警告: 找不到拖动条 {slider_id} 的配置或轴映射") - else: - if config.DEBUG: - print("[SLIDER] 使用新版统一轴配置应用拖动条值") - print(f"[SLIDER] axis_config: {axis_config}") - print(f"[SLIDER] 查找 slider_id: {slider_id}") - # 使用新的统一轴配置 - for gamepad_axis, axis_cfg in axis_config.items(): - if config.DEBUG: - print(f"[SLIDER] 检查轴 {gamepad_axis}: {axis_cfg}") + for axis_index, axis_cfg in axis_mapping.items(): + if isinstance(axis_index, str): + axis_index = int(axis_index) + if axis_cfg.get('source_type') == 'slider' and axis_cfg.get('source_id') == slider_id: # 应用死区 processed_value = apply_deadzone(value, axis_cfg.get('deadzone', 0.05)) # 应用峰值限制 processed_value = apply_peak_value(processed_value, axis_cfg.get('peak_value', 1.0)) + # 应用反转 + if axis_cfg.get('invert', False): + processed_value = -processed_value + if config.DEBUG: - print(f"[SLIDER] 应用到轴: {gamepad_axis} = {processed_value:.3f} [原始={value:.3f}, deadzone={axis_cfg.get('deadzone', 0.05)}, peak={axis_cfg.get('peak_value', 1.0)}]") - virtual_joystick.set_axis(gamepad_axis, processed_value) + print(f"[SLIDER] 自定义摇杆: 应用到轴{axis_index} = {processed_value:.3f}") + virtual_joystick.set_axis(axis_index, processed_value) break + else: + # 使用 Xbox 360 模式的轴配置 + axis_config = button_config.get('driving_config', {}).get('axis_config', {}) + + # 如果没有新的轴配置,回退到旧方式 + if not axis_config: + if config.DEBUG: + print("[SLIDER] 警告: 找不到按钮新版配置") + slider = next((b for b in buttons if b.get('id') == slider_id and b.get('type') == 'slider'), None) + + if slider and slider.get('axis'): + axis = slider['axis'] + if config.DEBUG: + print(f"[SLIDER] 应用到轴: {axis} = {value:.3f}") + virtual_joystick.set_axis(axis, value) + else: + if config.DEBUG: + print(f"[SLIDER] 警告: 找不到拖动条 {slider_id} 的配置或轴映射") + else: + if config.DEBUG: + print("[SLIDER] 使用新版统一轴配置应用拖动条值") + print(f"[SLIDER] axis_config: {axis_config}") + print(f"[SLIDER] 查找 slider_id: {slider_id}") + # 使用新的统一轴配置 + for gamepad_axis, axis_cfg in axis_config.items(): + if config.DEBUG: + print(f"[SLIDER] 检查轴 {gamepad_axis}: {axis_cfg}") + if axis_cfg.get('source_type') == 'slider' and axis_cfg.get('source_id') == slider_id: + # 应用死区 + processed_value = apply_deadzone(value, axis_cfg.get('deadzone', 0.05)) + # 应用峰值限制 + processed_value = apply_peak_value(processed_value, axis_cfg.get('peak_value', 1.0)) + if config.DEBUG: + print(f"[SLIDER] 应用到轴: {gamepad_axis} = {processed_value:.3f} [原始={value:.3f}, deadzone={axis_cfg.get('deadzone', 0.05)}, peak={axis_cfg.get('peak_value', 1.0)}]") + virtual_joystick.set_axis(gamepad_axis, processed_value) + break + # 如果滑块设置为自动归中并且回到默认值,则隐藏 overlay try: default_val = 0.5 if (slider_btn and slider_btn.get('rangeMode') == 'unipolar') else 0.0 @@ -370,7 +456,7 @@ def handle_slider_value(data): @socketio.on('save_layout') def handle_save_layout(data): - # Data should be the new list of buttons + """Save the current button layout.""" print("Saving Layout...") current_config = load_config() current_config['buttons'] = data @@ -422,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 e4b86e1..43cd670 100644 --- a/server/joystick_manager.py +++ b/server/joystick_manager.py @@ -3,6 +3,9 @@ 该模块为驾驶模式提供虚拟摇杆功能。 It uses vgamepad on Windows or uinput on Linux to create a virtual game controller. +支持两种模式: +1. Xbox 360 模式:标准 6 轴摇杆 +2. 自定义模式:可配置多轴摇杆(适用于飞行模拟等场景) """ import platform @@ -27,13 +30,167 @@ print("Warning: joystick_monitor not available") +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"): + """ + 初始化自定义虚拟摇杆。 + + Args: + axis_count: 轴的数量(1-32) + name: 摇杆名称 + """ + self.system = platform.system() + self.axis_count = min(32, max(1, axis_count)) # 限制在 1-32 之间 + self.name = name + self.gamepad = None + self.initialized = False + self.axis_values = [0.0] * self.axis_count # 存储所有轴的当前值 + self._init_gamepad() + + def _init_gamepad(self): + """根据平台初始化虚拟摇杆。""" + if self.system == 'Windows': + try: + import pyvjoy + # 使用 vJoy 来创建自定义多轴摇杆 + # 注意:需要安装 vJoy 驱动和 pyvjoy + self.gamepad = pyvjoy.VJoyDevice(1) # 使用第一个 vJoy 设备 + self.initialized = True + print(f"自定义虚拟摇杆已初始化 (Windows, {self.axis_count} 轴)") + except ImportError: + print("pyvjoy 未安装。请安装 vJoy 驱动和 pyvjoy:") + print("1. 从 https://sourceforge.net/projects/vjoystick/ 下载并安装 vJoy") + print("2. pip install pyvjoy") + except Exception as e: + print(f"在 Windows 上初始化自定义虚拟摇杆失败:{e}") + 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 = [] + 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 + print(f"自定义虚拟摇杆已初始化 (Linux, {self.axis_count} 轴)") + except ImportError: + print("python-uinput 未安装。请使用以下命令安装:pip install python-uinput") + except PermissionError: + print("权限被拒绝。请使用 sudo 运行或添加 uinput 权限。") + print("可能需要执行:sudo modprobe uinput") + except Exception as e: + print(f"在 Linux 上初始化自定义虚拟摇杆失败:{e}") + else: + print(f"自定义虚拟摇杆在 {self.system} 上不受支持") + + def set_axis(self, axis_index, value): + """ + 设置指定轴的值。 + + Args: + axis_index: 轴索引(0-based) + value: 轴值,范围 -1.0 到 1.0 + """ + if not self.initialized: + return + + if axis_index < 0 or axis_index >= self.axis_count: + return + + # 限制值范围 + value = max(-1.0, min(1.0, value)) + self.axis_values[axis_index] = value + + if self.system == 'Windows': + try: + import pyvjoy + # pyvjoy 使用 1-32768 的范围 + # 将 -1.0~1.0 映射到 1~32768 + int_value = int((value + 1.0) * 16383.5 + 1) + int_value = max(1, min(32768, int_value)) + + # vJoy 支持最多 8 个轴(从 X 开始顺序映射) + if axis_index < 8: + self.gamepad.set_axis(pyvjoy.HID_USAGE_X + axis_index, int_value) + except Exception as e: + if config and config.DEBUG: + print(f"设置 Windows 自定义摇杆轴失败:{e}") + elif self.system == 'Linux': + try: + import uinput + # 将 -1.0~1.0 映射到 -32767~32767 + int_value = int(value * 32767) + + 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}") + + def reset(self): + """将所有轴重置为中性位置。""" + for i in range(self.axis_count): + self.set_axis(i, 0.0) + + def close(self): + """清理资源。""" + if self.initialized: + self.reset() + if self.system == 'Linux' and self.gamepad: + self.gamepad.destroy() + self.gamepad = None + self.initialized = False + + class VirtualJoystick: - """Virtual joystick abstraction layer.""" + """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 # 自定义摇杆实例 + + # 优先使用传入的配置(来自 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() # 启动监视器(如果配置允许) @@ -42,7 +199,20 @@ def __init__(self): start_monitor() def _init_gamepad(self): - """Initialize the virtual gamepad based on the platform.""" + """Initialize the virtual gamepad based on the platform and configuration.""" + if self.joystick_type == "custom": + # 使用自定义多轴摇杆 + 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 + if self.initialized: + print(f"使用自定义虚拟摇杆模式({axis_count} 轴)") + return + + # 使用标准 Xbox 360 模式 if self.system == 'Windows': try: import vgamepad as vg @@ -113,14 +283,44 @@ def set_axis(self, axis_name, value): Set a specific gamepad axis value. Args: - axis_name: Axis name - "left_x", "left_y", "right_x", "right_y", "left_trigger", "right_trigger" + axis_name: 对于 Xbox 360 模式:"left_x", "left_y", "right_x", "right_y", "left_trigger", "right_trigger" + 对于自定义模式:轴索引(整数)或字符串形式的索引(如 "0", "1", "2" 等) value: Float from -1.0 to 1.0 (for joysticks) or 0.0 to 1.0 (for triggers) """ if not self.initialized: return + # 处理自定义摇杆模式 + if self.joystick_type == "custom" and self.custom_joystick: + # 将轴名称转换为索引 + if isinstance(axis_name, int): + axis_index = axis_name + elif isinstance(axis_name, str) and axis_name.isdigit(): + axis_index = int(axis_name) + else: + # 如果是字符串但不是数字,可能是从 Xbox 映射过来的 + # 尝试映射到索引 + axis_map = { + 'left_x': 0, + 'left_y': 1, + 'right_x': 2, + 'right_y': 3, + 'left_trigger': 4, + 'right_trigger': 5 + } + axis_index = axis_map.get(axis_name, None) + if axis_index is None: + if config and config.DEBUG: + print(f"未知的自定义摇杆轴名称:{axis_name}") + return + + # 设置自定义摇杆的轴 + self.custom_joystick.set_axis(axis_index, value) + return + + # Xbox 360 模式的原有逻辑 # Clamp value - if 'trigger' in axis_name: + if 'trigger' in str(axis_name): value = max(0.0, min(1.0, value)) else: value = max(-1.0, min(1.0, value)) @@ -268,6 +468,10 @@ def reset(self): if not self.initialized: return + if self.joystick_type == "custom" and self.custom_joystick: + self.custom_joystick.reset() + return + if self.system == 'Windows': self.gamepad.reset() self.gamepad.update() @@ -282,7 +486,10 @@ def close(self): """Clean up resources.""" if self.initialized: self.reset() - if self.system == 'Linux' and self.gamepad: + if self.joystick_type == "custom" and self.custom_joystick: + self.custom_joystick.close() + self.custom_joystick = None + elif self.system == 'Linux' and self.gamepad: self.gamepad.destroy() self.gamepad = None self.initialized = False diff --git a/static/js/main.js b/static/js/main.js index 7db45c5..3883d61 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -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; @@ -74,6 +83,32 @@ const app = createApp({ } }); + // 摇杆类型和自定义摇杆配置 + const joystickType = ref('xbox360'); // 'xbox360' 或 'custom' + 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; @@ -153,6 +188,20 @@ const app = createApp({ }); }); + // 自定义摇杆轴配置列表 + const customAxisConfigList = computed(() => { + const list = []; + for (let i = 0; i < customJoystickAxisCount.value; i++) { + const cfg = customAxisMapping[i]; + if (cfg) { + // 直接使用原始配置对象,并附加 axisIndex,保证表格编辑能同步回 customAxisMapping + cfg.axisIndex = i; + list.push(cfg); + } + } + return list; + }); + // 获取轴的显示名称 const getAxisDisplayName = (axis) => { const names = { @@ -173,6 +222,42 @@ const app = createApp({ } }; + // 当自定义轴的源类型改变时 + const onCustomAxisSourceTypeChange = (axisConfig) => { + if (axisConfig.source_type === 'none') { + axisConfig.source_id = null; + } + }; + + // 当摇杆类型改变时 + const onJoystickTypeChange = () => { + console.log('摇杆类型改变为:', joystickType.value); + }; + + // 当自定义摇杆轴数量改变时 + const onCustomAxisCountChange = () => { + console.log('自定义摇杆轴数量改变为:', customJoystickAxisCount.value); + }; + + // 获取自定义轴可用的拖动条(排除已被其他轴绑定的拖动条) + const getAvailableSlidersForCustomAxis = (currentAxisIndex) => { + const sliders = buttonsData.value.filter(b => b.type === 'slider'); + + // 找出已被其他轴绑定的拖动条 + const boundSliders = new Set(); + for (let i = 0; i < customJoystickAxisCount.value; i++) { + if (i !== currentAxisIndex && customAxisMapping[i]?.source_type === 'slider') { + boundSliders.add(customAxisMapping[i].source_id); + } + } + + return sliders.map(slider => ({ + id: slider.id, + displayLabel: slider.label || slider.id, + disabled: boundSliders.has(slider.id) + })); + }; + // Helper function to get default value based on range mode const getSliderDefaultValue = (rangeMode) => { return rangeMode === 'unipolar' ? 0.5 : 0.0; @@ -207,6 +292,20 @@ const app = createApp({ } }); + // 加载摇杆类型和自定义摇杆配置 + if (data.joystick_type) { + joystickType.value = data.joystick_type; + } + + if (data.custom_joystick) { + customJoystickAxisCount.value = data.custom_joystick.axis_count || 8; + if (data.custom_joystick.axis_mapping) { + Object.assign(customAxisMapping, data.custom_joystick.axis_mapping); + } + // 初始化任何缺失的轴配置 + initializeCustomAxisMapping(); + } + // 加载驾驶模式配置 if (data.driving_config) { Object.assign(drivingConfig, data.driving_config); @@ -972,36 +1071,68 @@ const app = createApp({ try { console.log('开始保存驾驶配置...'); console.log('当前drivingConfig:', drivingConfig); - // 更新旧的 gyro_axis_mapping 和 sliders(保持向后兼容) - const newGyroMapping = { gamma: null, beta: null, alpha: null }; - const newSliders = []; + console.log('摇杆类型:', joystickType.value); - // 从统一轴配置中反向生成旧格式 - Object.entries(drivingConfig.axis_config).forEach(([axis, config]) => { - if (config.source_type === 'gyro' && config.source_id) { - newGyroMapping[config.source_id] = axis; - } else if (config.source_type === 'slider' && config.source_id) { - const sliderBtn = buttonsData.value.find(b => b.id === config.source_id && b.type === 'slider'); - if (sliderBtn) { - newSliders.push({ - id: sliderBtn.id, - label: sliderBtn.label, - axis: axis, - orientation: sliderBtn.orientation, - autoCenter: sliderBtn.autoCenter - }); + // 准备要保存的配置数据 + const configToSave = { + joystick_type: joystickType.value + }; + + if (joystickType.value === 'custom') { + // 自定义摇杆模式:保存自定义轴映射 + configToSave.custom_joystick = { + axis_count: customJoystickAxisCount.value, + axis_mapping: {} + }; + + // 转换 customAxisMapping 为普通对象(因为可能是 reactive) + for (let i = 0; i < customJoystickAxisCount.value; i++) { + if (customAxisMapping[i]) { + configToSave.custom_joystick.axis_mapping[i] = { + source_type: customAxisMapping[i].source_type, + source_id: customAxisMapping[i].source_id, + peak_value: customAxisMapping[i].peak_value, + deadzone: customAxisMapping[i].deadzone, + gyro_range: customAxisMapping[i].gyro_range, + invert: customAxisMapping[i].invert || false + }; } } - }); - - drivingConfig.gyro_axis_mapping = newGyroMapping; - drivingConfig.sliders = newSliders; + } else { + // Xbox 360 模式:保存原有的轴配置 + // 更新旧的 gyro_axis_mapping 和 sliders(保持向后兼容) + const newGyroMapping = { gamma: null, beta: null, alpha: null }; + const newSliders = []; + + // 从统一轴配置中反向生成旧格式 + Object.entries(drivingConfig.axis_config).forEach(([axis, config]) => { + if (config.source_type === 'gyro' && config.source_id) { + newGyroMapping[config.source_id] = axis; + } else if (config.source_type === 'slider' && config.source_id) { + const sliderBtn = buttonsData.value.find(b => b.id === config.source_id && b.type === 'slider'); + if (sliderBtn) { + newSliders.push({ + id: sliderBtn.id, + label: sliderBtn.label, + axis: axis, + orientation: sliderBtn.orientation, + autoCenter: sliderBtn.autoCenter + }); + } + } + }); + + drivingConfig.gyro_axis_mapping = newGyroMapping; + drivingConfig.sliders = newSliders; + + configToSave.driving_config = drivingConfig; + } - console.log('准备发送请求,数据:', { driving_config: drivingConfig }); + console.log('准备发送请求,数据:', configToSave); const response = await fetch('/api/update_driving_config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ driving_config: drivingConfig }) + body: JSON.stringify(configToSave) }); console.log('收到响应:', response.status, response.statusText); @@ -1177,9 +1308,16 @@ const app = createApp({ activeButtonsMap, BUTTON_COLORS, axisConfigList, + customAxisConfigList, + joystickType, + customJoystickAxisCount, getAxisDisplayName, getAvailableSlidersForAxis, + getAvailableSlidersForCustomAxis, onAxisSourceTypeChange, + onCustomAxisSourceTypeChange, + onJoystickTypeChange, + onCustomAxisCountChange, toggleEditMode, saveLayout, editButton, diff --git a/templates/index.html b/templates/index.html index ba92cd9..8d54b59 100644 --- a/templates/index.html +++ b/templates/index.html @@ -91,13 +91,28 @@ style="max-width: 900px;" > - Xbox 手柄轴配置 -

- 为每个 Xbox 手柄轴配置输入源(陀螺仪或拖动条)、峰值和死区 -

+ + 摇杆类型 + + + Xbox 360 控制器 + 自定义多轴摇杆 + +

+ Xbox 360: 标准6轴控制器(2个摇杆 + 2个扳机)
+ 自定义: 可配置多轴摇杆(适用于飞行模拟等场景,支持1-32轴) +

+
- - + +