Added support for remapping buttons
This commit is contained in:
parent
beb61477f9
commit
133d0982fd
9 changed files with 803 additions and 107 deletions
16
PROTOCOL
16
PROTOCOL
|
@ -229,6 +229,13 @@ Rumble: { 0x00, 0x01, 0x0f, 0xc0, 0x00, large, small, 0x00, 0x00, 0x00, 0x00, 0x
|
|||
LED: { 0x00, 0x00, 0x08, 0x40 + (mode % 0x0e), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }
|
||||
|
||||
|
||||
Connection Status Messages:
|
||||
|
||||
0x08 0x00 - Nothing
|
||||
0x08 0x40 - Headset
|
||||
0x08 0x80 - Controller
|
||||
0x08 0xc0 - Controller and Headset
|
||||
|
||||
On connection:
|
||||
--------------
|
||||
|
||||
|
@ -262,7 +269,7 @@ len: 29 data: 0x00 0x00 0x00 0xf0 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0
|
|||
|
||||
|
||||
Battery Status Msg (maybe):
|
||||
---------------------------
|
||||
------------------------
|
||||
|
||||
len: 29 data: 0x00 0xf8 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
len: 29 data: 0x00 0xf8 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
|
@ -283,6 +290,13 @@ Controller without doing anything
|
|||
29 data: 0x00 0x0f 0x00 0xf0 0xf0 0xcc 0xfd 0x1f 0x9f 0x70 0xc9 0x00 0x63 0xb0 0x00 0x05 0x13 0xe7 0x20 0x1d 0x30 0x03 0x40 0x01 0x50 0x01 0xff 0xff 0xff
|
||||
29 data: 0x00 0xf8 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
|
||||
Unknown Messages:
|
||||
29 data: 0x00 0x00 0x00 0x20 0x1d 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
29 data: 0x00 0x00 0x00 0x40 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
29 data: 0x00 0xf8 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
29 data: 0x00 0xf8 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
|
||||
|
||||
---------------
|
||||
len: 29 data: 0x00 0x01 0x00 0xf0 0x00 0x13 0x00 0x00 0x00 0x00 0x94 0xff 0xc0 0x02 0xa6 0x02 0xf0 0xf6 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
len: 29 data: 0x00 0x01 0x00 0xf0 0x00 0x13 0x00 0x00 0x00 0x00 0x14 0xfd 0xc0 0x02 0xa6 0x02 0xf0 0xf6 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
|
|
|
@ -4,6 +4,7 @@ env = Environment(CPPFLAGS=["-g", "-O0", "-Wall"], LIBS=["usb"])
|
|||
env.Program("xboxdrv", ["xboxdrv.cpp",
|
||||
"xboxmsg.cpp",
|
||||
"uinput.cpp",
|
||||
"helper.cpp",
|
||||
"xbox_controller.cpp",
|
||||
"xbox360_controller.cpp",
|
||||
"xbox360_wireless_controller.cpp",
|
||||
|
|
7
TODO
7
TODO
|
@ -1,3 +1,6 @@
|
|||
--buttonmap a=b,c=d,e=f
|
||||
--axismap x1=x2,x2=
|
||||
|
||||
Pictures of Xbox360 and controller:
|
||||
http://g-prime.net/x360/
|
||||
|
||||
|
@ -5,10 +8,6 @@ FIX:
|
|||
=====
|
||||
filter auto known unknown messages
|
||||
usbcat contains ugly endpoint hack
|
||||
add magic to detect which jsX device we are going to get (opendir, fmmatch, etc.)
|
||||
fix guitar support
|
||||
|
||||
sigint doesn't work properly
|
||||
|
||||
Battery warning: LEDs 1,4 then 2,3 over and over ~10 times rapidly
|
||||
|
||||
|
|
48
helper.cpp
Normal file
48
helper.cpp
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
** Xbox/Xbox360 USB Gamepad Userspace Driver
|
||||
** Copyright (C) 2008 Ingo Ruhnke <grumbel@gmx.de>
|
||||
**
|
||||
** This program is free software: you can redistribute it and/or modify
|
||||
** it under the terms of the GNU General Public License as published by
|
||||
** the Free Software Foundation, either version 3 of the License, or
|
||||
** (at your option) any later version.
|
||||
**
|
||||
** This program is distributed in the hope that it will be useful,
|
||||
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
** GNU General Public License for more details.
|
||||
**
|
||||
** You should have received a copy of the GNU General Public License
|
||||
** along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <boost/format.hpp>
|
||||
#include "helper.hpp"
|
||||
|
||||
void print_raw_data(std::ostream& out, uint8_t* data, int len)
|
||||
{
|
||||
std::cout << "len: " << len
|
||||
<< " data: ";
|
||||
|
||||
for(int i = 0; i < len; ++i)
|
||||
std::cout << boost::format("0x%02x ") % int(data[i]);
|
||||
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
std::string to_lower(const std::string &str)
|
||||
{
|
||||
std::string lower_impl = str;
|
||||
|
||||
for( std::string::iterator i = lower_impl.begin();
|
||||
i != lower_impl.end();
|
||||
++i )
|
||||
{
|
||||
*i = tolower(*i);
|
||||
}
|
||||
|
||||
return lower_impl;
|
||||
}
|
||||
|
||||
/* EOF */
|
29
helper.hpp
Normal file
29
helper.hpp
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
** Xbox/Xbox360 USB Gamepad Userspace Driver
|
||||
** Copyright (C) 2008 Ingo Ruhnke <grumbel@gmx.de>
|
||||
**
|
||||
** This program is free software: you can redistribute it and/or modify
|
||||
** it under the terms of the GNU General Public License as published by
|
||||
** the Free Software Foundation, either version 3 of the License, or
|
||||
** (at your option) any later version.
|
||||
**
|
||||
** This program is distributed in the hope that it will be useful,
|
||||
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
** GNU General Public License for more details.
|
||||
**
|
||||
** You should have received a copy of the GNU General Public License
|
||||
** along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef HEADER_HELPER_HPP
|
||||
#define HEADER_HELPER_HPP
|
||||
|
||||
#include <iosfwd>
|
||||
|
||||
void print_raw_data(std::ostream& out, uint8_t* buffer, int len);
|
||||
std::string to_lower(const std::string &str);
|
||||
|
||||
#endif
|
||||
|
||||
/* EOF */
|
|
@ -21,6 +21,7 @@
|
|||
#include <iostream>
|
||||
#include <boost/format.hpp>
|
||||
#include "xboxmsg.hpp"
|
||||
#include "helper.hpp"
|
||||
#include "xbox360_controller.hpp"
|
||||
|
||||
Xbox360Controller::Xbox360Controller(struct usb_device* dev, bool is_guitar)
|
||||
|
@ -111,13 +112,8 @@ Xbox360Controller::read(XboxGenericMsg& msg)
|
|||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Unknown data: bytes: " << ret
|
||||
<< " Data: ";
|
||||
|
||||
for(int j = 0; j < ret; ++j)
|
||||
std::cout << boost::format("0x%02x ") % int(data[j]);
|
||||
|
||||
std::cout << std::endl;
|
||||
std::cout << "Unknown: ";
|
||||
print_raw_data(std::cout, data, ret);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include <iostream>
|
||||
#include <boost/format.hpp>
|
||||
#include <stdexcept>
|
||||
#include "helper.hpp"
|
||||
#include "xboxmsg.hpp"
|
||||
#include "xbox360_wireless_controller.hpp"
|
||||
|
||||
|
@ -109,7 +110,7 @@ Xbox360WirelessController::read(XboxGenericMsg& msg)
|
|||
std::cout << "Connection status: unknown" << std::endl;
|
||||
}
|
||||
}
|
||||
else if (ret == 29) // Event Message
|
||||
else if (ret == 29)
|
||||
{
|
||||
if (data[0] == 0x00 && data[1] == 0x0f && data[2] == 0x00 && data[3] == 0xf0)
|
||||
{ // Initial Announc Message
|
||||
|
@ -126,7 +127,7 @@ Xbox360WirelessController::read(XboxGenericMsg& msg)
|
|||
std::cout << "Battery Status: " << battery_status << std::endl;
|
||||
}
|
||||
else if (data[0] == 0x00 && data[1] == 0x01 && data[2] == 0x00 && data[3] == 0xf0 && data[4] == 0x00 && data[5] == 0x13)
|
||||
{
|
||||
{ // Event message
|
||||
msg.type = GAMEPAD_XBOX360_WIRELESS;
|
||||
msg.xbox360 = *reinterpret_cast<Xbox360Msg*>(&data[4]);
|
||||
return true;
|
||||
|
@ -136,14 +137,21 @@ Xbox360WirelessController::read(XboxGenericMsg& msg)
|
|||
battery_status = data[4];
|
||||
std::cout << "Battery Status: " << battery_status << std::endl;
|
||||
}
|
||||
else if (data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0xf0)
|
||||
{
|
||||
// 0x00 0x00 0x00 0xf0 0x00 ... is send after each button
|
||||
// press, doesn't seem to contain any information
|
||||
}
|
||||
else
|
||||
{
|
||||
// unknown/junk
|
||||
std::cout << "Unknown: ";
|
||||
print_raw_data(std::cout, data, ret);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// unknown/junk
|
||||
std::cout << "Unknown: ";
|
||||
print_raw_data(std::cout, data, ret);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
733
xboxdrv.cpp
733
xboxdrv.cpp
|
@ -26,6 +26,7 @@
|
|||
#include "xbox_controller.hpp"
|
||||
#include "xbox360_controller.hpp"
|
||||
#include "xbox360_wireless_controller.hpp"
|
||||
#include "helper.hpp"
|
||||
#include "xbox_generic_controller.hpp"
|
||||
|
||||
#include "xboxdrv.hpp"
|
||||
|
@ -85,6 +86,9 @@ XPadDevice xpad_devices[] = {
|
|||
|
||||
const int xpad_devices_count = sizeof(xpad_devices)/sizeof(XPadDevice);
|
||||
|
||||
XboxButton string2btn(const std::string& str_);
|
||||
XboxAxis string2axis(const std::string& str_);
|
||||
|
||||
std::ostream& operator<<(std::ostream& out, const GamepadType& type)
|
||||
{
|
||||
switch (type)
|
||||
|
@ -109,6 +113,482 @@ std::ostream& operator<<(std::ostream& out, const GamepadType& type)
|
|||
}
|
||||
}
|
||||
|
||||
int get_button(XboxGenericMsg& msg, XboxButton button)
|
||||
{
|
||||
switch(msg.type)
|
||||
{
|
||||
case GAMEPAD_XBOX360_GUITAR:
|
||||
case GAMEPAD_XBOX360:
|
||||
case GAMEPAD_XBOX360_WIRELESS:
|
||||
switch(button)
|
||||
{
|
||||
case XBOX_BTN_START:
|
||||
return msg.xbox360.start;
|
||||
case XBOX_BTN_GUIDE:
|
||||
return msg.xbox360.guide;
|
||||
case XBOX_BTN_BACK:
|
||||
return msg.xbox360.back;
|
||||
|
||||
case XBOX_BTN_A:
|
||||
return msg.xbox360.a;
|
||||
case XBOX_BTN_B:
|
||||
return msg.xbox360.b;
|
||||
case XBOX_BTN_X:
|
||||
return msg.xbox360.x;
|
||||
case XBOX_BTN_Y:
|
||||
return msg.xbox360.y;
|
||||
|
||||
case XBOX_BTN_LB:
|
||||
case XBOX_BTN_WHITE:
|
||||
return msg.xbox360.lb;
|
||||
case XBOX_BTN_RB:
|
||||
case XBOX_BTN_BLACK:
|
||||
return msg.xbox360.rb;
|
||||
|
||||
case XBOX_BTN_LT:
|
||||
return msg.xbox360.lt;
|
||||
case XBOX_BTN_RT:
|
||||
return msg.xbox360.rt;
|
||||
|
||||
case XBOX_BTN_THUMB_L:
|
||||
return msg.xbox360.thumb_l;
|
||||
case XBOX_BTN_THUMB_R:
|
||||
return msg.xbox360.thumb_r;
|
||||
|
||||
case XBOX_DPAD_UP:
|
||||
return msg.xbox360.dpad_up;
|
||||
case XBOX_DPAD_DOWN:
|
||||
return msg.xbox360.dpad_down;
|
||||
case XBOX_DPAD_LEFT:
|
||||
return msg.xbox360.dpad_left;
|
||||
case XBOX_DPAD_RIGHT:
|
||||
return msg.xbox360.dpad_right;
|
||||
|
||||
case XBOX_BTN_UNKNOWN:
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case GAMEPAD_XBOX:
|
||||
case GAMEPAD_XBOX_MAT:
|
||||
switch(button)
|
||||
{
|
||||
case XBOX_BTN_START:
|
||||
return msg.xbox.start;
|
||||
case XBOX_BTN_GUIDE:
|
||||
return 0;
|
||||
case XBOX_BTN_BACK:
|
||||
return msg.xbox.back;
|
||||
|
||||
case XBOX_BTN_A:
|
||||
return msg.xbox.a;
|
||||
case XBOX_BTN_B:
|
||||
return msg.xbox.b;
|
||||
case XBOX_BTN_X:
|
||||
return msg.xbox.x;
|
||||
case XBOX_BTN_Y:
|
||||
return msg.xbox.y;
|
||||
|
||||
case XBOX_BTN_LB:
|
||||
case XBOX_BTN_WHITE:
|
||||
return msg.xbox.white;
|
||||
case XBOX_BTN_RB:
|
||||
case XBOX_BTN_BLACK:
|
||||
return msg.xbox.black;
|
||||
|
||||
case XBOX_BTN_LT:
|
||||
return msg.xbox.lt;
|
||||
case XBOX_BTN_RT:
|
||||
return msg.xbox.rt;
|
||||
|
||||
case XBOX_BTN_THUMB_L:
|
||||
return msg.xbox.thumb_l;
|
||||
case XBOX_BTN_THUMB_R:
|
||||
return msg.xbox.thumb_r;
|
||||
|
||||
case XBOX_DPAD_UP:
|
||||
return msg.xbox.dpad_up;
|
||||
case XBOX_DPAD_DOWN:
|
||||
return msg.xbox.dpad_down;
|
||||
case XBOX_DPAD_LEFT:
|
||||
return msg.xbox.dpad_left;
|
||||
case XBOX_DPAD_RIGHT:
|
||||
return msg.xbox.dpad_right;
|
||||
|
||||
case XBOX_BTN_UNKNOWN:
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case GAMEPAD_UNKNOWN:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void set_button(XboxGenericMsg& msg, XboxButton button, int v)
|
||||
{
|
||||
switch(msg.type)
|
||||
{
|
||||
case GAMEPAD_XBOX360_GUITAR:
|
||||
case GAMEPAD_XBOX360:
|
||||
case GAMEPAD_XBOX360_WIRELESS:
|
||||
switch(button)
|
||||
{
|
||||
case XBOX_BTN_START:
|
||||
msg.xbox360.start = v; break;
|
||||
case XBOX_BTN_GUIDE:
|
||||
msg.xbox360.guide = v; break;
|
||||
case XBOX_BTN_BACK:
|
||||
msg.xbox360.back = v; break;
|
||||
|
||||
case XBOX_BTN_A:
|
||||
msg.xbox360.a = v; break;
|
||||
case XBOX_BTN_B:
|
||||
msg.xbox360.b = v; break;
|
||||
case XBOX_BTN_X:
|
||||
msg.xbox360.x = v; break;
|
||||
case XBOX_BTN_Y:
|
||||
msg.xbox360.y = v; break;
|
||||
|
||||
case XBOX_BTN_LB:
|
||||
case XBOX_BTN_WHITE:
|
||||
msg.xbox360.lb = v; break;
|
||||
case XBOX_BTN_RB:
|
||||
case XBOX_BTN_BLACK:
|
||||
msg.xbox360.rb = v; break;
|
||||
|
||||
case XBOX_BTN_LT:
|
||||
msg.xbox360.lt = v; break;
|
||||
case XBOX_BTN_RT:
|
||||
msg.xbox360.rt = v; break;
|
||||
|
||||
case XBOX_BTN_THUMB_L:
|
||||
msg.xbox360.thumb_l = v; break;
|
||||
case XBOX_BTN_THUMB_R:
|
||||
msg.xbox360.thumb_r = v; break;
|
||||
|
||||
case XBOX_DPAD_UP:
|
||||
msg.xbox360.dpad_up = v; break;
|
||||
case XBOX_DPAD_DOWN:
|
||||
msg.xbox360.dpad_down = v; break;
|
||||
case XBOX_DPAD_LEFT:
|
||||
msg.xbox360.dpad_left = v; break;
|
||||
case XBOX_DPAD_RIGHT:
|
||||
msg.xbox360.dpad_right = v; break;
|
||||
|
||||
case XBOX_BTN_UNKNOWN:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case GAMEPAD_XBOX:
|
||||
case GAMEPAD_XBOX_MAT:
|
||||
switch(button)
|
||||
{
|
||||
case XBOX_BTN_START:
|
||||
msg.xbox.start = v; break;
|
||||
case XBOX_BTN_GUIDE:
|
||||
break;
|
||||
case XBOX_BTN_BACK:
|
||||
msg.xbox.back = v; break;
|
||||
|
||||
case XBOX_BTN_A:
|
||||
msg.xbox.a = v; break;
|
||||
case XBOX_BTN_B:
|
||||
msg.xbox.b = v; break;
|
||||
case XBOX_BTN_X:
|
||||
msg.xbox.x = v; break;
|
||||
case XBOX_BTN_Y:
|
||||
msg.xbox.y = v; break;
|
||||
|
||||
case XBOX_BTN_LB:
|
||||
case XBOX_BTN_WHITE:
|
||||
msg.xbox.white = v; break;
|
||||
case XBOX_BTN_RB:
|
||||
case XBOX_BTN_BLACK:
|
||||
msg.xbox.black = v; break;
|
||||
|
||||
case XBOX_BTN_LT:
|
||||
msg.xbox.lt = v; break;
|
||||
case XBOX_BTN_RT:
|
||||
msg.xbox.rt = v; break;
|
||||
|
||||
case XBOX_BTN_THUMB_L:
|
||||
msg.xbox.thumb_l = v; break;
|
||||
case XBOX_BTN_THUMB_R:
|
||||
msg.xbox.thumb_r = v; break;
|
||||
|
||||
case XBOX_DPAD_UP:
|
||||
msg.xbox.dpad_up = v; break;
|
||||
case XBOX_DPAD_DOWN:
|
||||
msg.xbox.dpad_down = v; break;
|
||||
case XBOX_DPAD_LEFT:
|
||||
msg.xbox.dpad_left = v; break;
|
||||
case XBOX_DPAD_RIGHT:
|
||||
msg.xbox.dpad_right = v; break;
|
||||
|
||||
case XBOX_BTN_UNKNOWN:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case GAMEPAD_UNKNOWN:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int get_axis(XboxGenericMsg& msg, XboxAxis axis)
|
||||
{
|
||||
switch(msg.type)
|
||||
{
|
||||
case GAMEPAD_XBOX360_GUITAR:
|
||||
case GAMEPAD_XBOX360:
|
||||
case GAMEPAD_XBOX360_WIRELESS:
|
||||
switch(axis)
|
||||
{
|
||||
case XBOX_AXIS_UNKNOWN:
|
||||
return 0;
|
||||
case XBOX_AXIS_X1:
|
||||
return msg.xbox360.x1;
|
||||
case XBOX_AXIS_Y1:
|
||||
return msg.xbox360.y1;
|
||||
case XBOX_AXIS_X2:
|
||||
return msg.xbox360.x2;
|
||||
case XBOX_AXIS_Y2:
|
||||
return msg.xbox360.y2;
|
||||
case XBOX_AXIS_LT:
|
||||
return msg.xbox360.lt;
|
||||
case XBOX_AXIS_RT:
|
||||
return msg.xbox360.rt;
|
||||
}
|
||||
break;
|
||||
|
||||
case GAMEPAD_XBOX:
|
||||
case GAMEPAD_XBOX_MAT:
|
||||
switch(axis)
|
||||
{
|
||||
case XBOX_AXIS_UNKNOWN:
|
||||
return 0;
|
||||
case XBOX_AXIS_X1:
|
||||
return msg.xbox.x1;
|
||||
case XBOX_AXIS_Y1:
|
||||
return msg.xbox.y1;
|
||||
case XBOX_AXIS_X2:
|
||||
return msg.xbox.x2;
|
||||
case XBOX_AXIS_Y2:
|
||||
return msg.xbox.y2;
|
||||
case XBOX_AXIS_LT:
|
||||
return msg.xbox.lt;
|
||||
case XBOX_AXIS_RT:
|
||||
return msg.xbox.rt;
|
||||
}
|
||||
break;
|
||||
|
||||
case GAMEPAD_UNKNOWN:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void set_axis(XboxGenericMsg& msg, XboxAxis axis, int v)
|
||||
{
|
||||
switch(msg.type)
|
||||
{
|
||||
case GAMEPAD_XBOX360_GUITAR:
|
||||
case GAMEPAD_XBOX360:
|
||||
case GAMEPAD_XBOX360_WIRELESS:
|
||||
switch(axis)
|
||||
{
|
||||
case XBOX_AXIS_UNKNOWN:
|
||||
break;
|
||||
case XBOX_AXIS_X1:
|
||||
msg.xbox360.x1 = v; break;
|
||||
case XBOX_AXIS_Y1:
|
||||
msg.xbox360.y1 = v; break;
|
||||
case XBOX_AXIS_X2:
|
||||
msg.xbox360.x2 = v; break;
|
||||
case XBOX_AXIS_Y2:
|
||||
msg.xbox360.y2 = v; break;
|
||||
case XBOX_AXIS_LT:
|
||||
msg.xbox360.lt = v; break;
|
||||
case XBOX_AXIS_RT:
|
||||
msg.xbox360.rt = v; break;
|
||||
}
|
||||
break;
|
||||
|
||||
case GAMEPAD_XBOX:
|
||||
case GAMEPAD_XBOX_MAT:
|
||||
switch(axis)
|
||||
{
|
||||
case XBOX_AXIS_UNKNOWN:
|
||||
break;
|
||||
case XBOX_AXIS_X1:
|
||||
msg.xbox.x1 = v; break;
|
||||
case XBOX_AXIS_Y1:
|
||||
msg.xbox.y1 = v; break;
|
||||
case XBOX_AXIS_X2:
|
||||
msg.xbox.x2 = v; break;
|
||||
case XBOX_AXIS_Y2:
|
||||
msg.xbox.y2 = v; break;
|
||||
case XBOX_AXIS_LT:
|
||||
msg.xbox.lt = v; break;
|
||||
case XBOX_AXIS_RT:
|
||||
msg.xbox.rt = v; break;
|
||||
}
|
||||
break;
|
||||
|
||||
case GAMEPAD_UNKNOWN:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void apply_button_map(XboxGenericMsg& msg, std::vector<ButtonMapping>& lst)
|
||||
{
|
||||
XboxGenericMsg newmsg = msg;
|
||||
|
||||
for(std::vector<ButtonMapping>::iterator i = lst.begin(); i != lst.end(); ++i)
|
||||
set_button(newmsg, i->lhs, 0);
|
||||
|
||||
for(std::vector<ButtonMapping>::iterator i = lst.begin(); i != lst.end(); ++i)
|
||||
set_button(newmsg, i->rhs, get_button(msg, i->lhs) || get_button(newmsg, i->rhs));
|
||||
|
||||
msg = newmsg;
|
||||
}
|
||||
|
||||
void apply_axis_map(XboxGenericMsg& msg, std::vector<AxisMapping>& lst)
|
||||
{
|
||||
XboxGenericMsg& newmsg = msg;
|
||||
for(std::vector<AxisMapping>::iterator i = lst.begin(); i != lst.end(); ++i)
|
||||
{
|
||||
if (i->invert)
|
||||
set_axis(newmsg, i->lhs, get_axis(msg, i->rhs));
|
||||
else
|
||||
set_axis(newmsg, i->lhs, -get_axis(msg, i->rhs));
|
||||
}
|
||||
msg = newmsg;
|
||||
}
|
||||
|
||||
ButtonMapping string2buttonmapping(const std::string& str)
|
||||
{
|
||||
std::cout << str << std::endl;
|
||||
for(std::string::const_iterator i = str.begin(); i != str.end(); ++i)
|
||||
{
|
||||
if (*i == '=')
|
||||
{
|
||||
ButtonMapping mapping;
|
||||
mapping.lhs = string2btn(std::string(str.begin(), i));
|
||||
mapping.rhs = string2btn(std::string(i+1, str.end()));
|
||||
|
||||
if (mapping.lhs == XBOX_BTN_UNKNOWN ||
|
||||
mapping.rhs == XBOX_BTN_UNKNOWN)
|
||||
throw std::runtime_error("Couldn't convert string \"" + str + "\" to button mapping");
|
||||
|
||||
return mapping;
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("Couldn't convert string \"" + str + "\" to button mapping");
|
||||
}
|
||||
|
||||
void string2buttonmap(const std::string& str, std::vector<ButtonMapping>& lst)
|
||||
{
|
||||
std::string::const_iterator start = str.begin();
|
||||
for(std::string::const_iterator i = str.begin(); i != str.end(); ++i)
|
||||
{
|
||||
if (*i == ',')
|
||||
{
|
||||
if (i != start)
|
||||
{
|
||||
ButtonMapping mapping = string2buttonmapping(std::string(start, i));
|
||||
lst.push_back(mapping);
|
||||
}
|
||||
start = i+1;
|
||||
}
|
||||
}
|
||||
if (start != str.end())
|
||||
{
|
||||
ButtonMapping mapping = string2buttonmapping(std::string(start, str.end()));
|
||||
lst.push_back(mapping);
|
||||
}
|
||||
}
|
||||
|
||||
void string2axismap(const std::string& str, std::vector<AxisMapping>& lst)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
XboxButton string2btn(const std::string& str_)
|
||||
{
|
||||
std::string str = to_lower(str_);
|
||||
|
||||
if (str == "start")
|
||||
return XBOX_BTN_START;
|
||||
else if (str == "guide")
|
||||
return XBOX_BTN_GUIDE;
|
||||
else if (str == "back")
|
||||
return XBOX_BTN_BACK;
|
||||
|
||||
else if (str == "a")
|
||||
return XBOX_BTN_A;
|
||||
else if (str == "b")
|
||||
return XBOX_BTN_B;
|
||||
else if (str == "x")
|
||||
return XBOX_BTN_X;
|
||||
else if (str == "y")
|
||||
return XBOX_BTN_Y;
|
||||
|
||||
else if (str == "black")
|
||||
return XBOX_BTN_BLACK;
|
||||
else if (str == "white")
|
||||
return XBOX_BTN_WHITE;
|
||||
|
||||
else if (str == "lb")
|
||||
return XBOX_BTN_LB;
|
||||
else if (str == "rb")
|
||||
return XBOX_BTN_RB;
|
||||
|
||||
else if (str == "lt")
|
||||
return XBOX_BTN_LT;
|
||||
else if (str == "rt")
|
||||
return XBOX_BTN_RT;
|
||||
|
||||
else if (str == "tl")
|
||||
return XBOX_BTN_THUMB_L;
|
||||
else if (str == "tr")
|
||||
return XBOX_BTN_THUMB_R;
|
||||
|
||||
else if (str == "du")
|
||||
return XBOX_DPAD_UP;
|
||||
else if (str == "dd")
|
||||
return XBOX_DPAD_DOWN;
|
||||
else if (str == "dl")
|
||||
return XBOX_DPAD_LEFT;
|
||||
else if (str == "dr")
|
||||
return XBOX_DPAD_RIGHT;
|
||||
|
||||
else
|
||||
return XBOX_BTN_UNKNOWN;
|
||||
}
|
||||
|
||||
XboxAxis string2axis(const std::string& str_)
|
||||
{
|
||||
std::string str = to_lower(str_);
|
||||
if (str == "x1")
|
||||
return XBOX_AXIS_X1;
|
||||
else if (str == "y1")
|
||||
return XBOX_AXIS_Y2;
|
||||
else if (str == "x2")
|
||||
return XBOX_AXIS_X2;
|
||||
else if (str == "y2")
|
||||
return XBOX_AXIS_Y2;
|
||||
else if (str == "lt")
|
||||
return XBOX_AXIS_LT;
|
||||
else if (str == "rt")
|
||||
return XBOX_AXIS_RT;
|
||||
else
|
||||
return XBOX_AXIS_UNKNOWN;
|
||||
}
|
||||
|
||||
void list_controller()
|
||||
{
|
||||
struct usb_bus* busses = usb_get_busses();
|
||||
|
@ -266,7 +746,9 @@ struct CommandLineOptions
|
|||
char devid[4];
|
||||
uInputCfg uinput_config;
|
||||
int deadzone;
|
||||
|
||||
std::vector<ButtonMapping> button_map;
|
||||
std::vector<AxisMapping> axis_map;
|
||||
|
||||
CommandLineOptions() {
|
||||
silent = false;
|
||||
rumble = false;
|
||||
|
@ -315,6 +797,8 @@ void print_command_line_help(int argc, char** argv)
|
|||
std::cout << " --dpad-as-button DPad sends button instead of axis events" << std::endl;
|
||||
std::cout << " --type TYPE Ignore autodetection and enforce controller type\n"
|
||||
<< " (xbox, xbox-mat, xbox360, xbox360-wireless, xbox360-guitar)" << std::endl;
|
||||
std::cout << " -b, --buttonmap MAP Remap the buttons as specified by MAP" << std::endl;
|
||||
std::cout << " -a, --axismap MAP Remap the axis as specified by MAP" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Report bugs to Ingo Ruhnke <grumbel@gmx.de>" << std::endl;
|
||||
}
|
||||
|
@ -375,13 +859,13 @@ void parse_command_line(int argc, char** argv, CommandLineOptions& opts)
|
|||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: " << argv[i-1] << " expected a argument in form INT,INT" << std::endl;
|
||||
std::cout << "Error: " << argv[i-1] << " expected an argument in form INT,INT" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: " << argv[i-1] << " expected a argument" << std::endl;
|
||||
std::cout << "Error: " << argv[i-1] << " expected an argument" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
@ -434,10 +918,38 @@ void parse_command_line(int argc, char** argv, CommandLineOptions& opts)
|
|||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: " << argv[i-1] << " expected a argument" << std::endl;
|
||||
std::cout << "Error: " << argv[i-1] << " expected an argument" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
else if (strcmp(argv[i], "-b") == 0 ||
|
||||
strcmp(argv[i], "--buttonmap") == 0)
|
||||
{
|
||||
++i;
|
||||
if (i < argc)
|
||||
{
|
||||
string2buttonmap(argv[i], opts.button_map);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: " << argv[i-1] << " expected an argument" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
else if (strcmp(argv[i], "-a") == 0 ||
|
||||
strcmp(argv[i], "--axismap") == 0)
|
||||
{
|
||||
++i;
|
||||
if (i < argc)
|
||||
{
|
||||
string2axismap(argv[i], opts.axis_map);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: " << argv[i-1] << " expected an argument" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
else if (strcmp(argv[i], "-i") == 0 ||
|
||||
strcmp(argv[i], "--id") == 0)
|
||||
{
|
||||
|
@ -448,7 +960,7 @@ void parse_command_line(int argc, char** argv, CommandLineOptions& opts)
|
|||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: " << argv[i-1] << " expected a argument" << std::endl;
|
||||
std::cout << "Error: " << argv[i-1] << " expected an argument" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
@ -462,7 +974,7 @@ void parse_command_line(int argc, char** argv, CommandLineOptions& opts)
|
|||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: " << argv[i-1] << " expected a argument" << std::endl;
|
||||
std::cout << "Error: " << argv[i-1] << " expected an argument" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
@ -484,7 +996,7 @@ void parse_command_line(int argc, char** argv, CommandLineOptions& opts)
|
|||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: " << argv[i-1] << " expected a argument" << std::endl;
|
||||
std::cout << "Error: " << argv[i-1] << " expected an argument" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
@ -556,7 +1068,7 @@ void parse_command_line(int argc, char** argv, CommandLineOptions& opts)
|
|||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: " << argv[i-1] << " expected a argument in form BUS:DEV (i.e. 006:003)" << std::endl;
|
||||
std::cout << "Error: " << argv[i-1] << " expected an argument in form BUS:DEV (i.e. 006:003)" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
@ -615,6 +1127,32 @@ void print_info(struct usb_device* dev,
|
|||
std::cout << "LED Status: " << "auto" << std::endl;
|
||||
else
|
||||
std::cout << "LED Status: " << opts.led << std::endl;
|
||||
std::cout << "ButtonMap: ";
|
||||
if (opts.button_map.empty())
|
||||
{
|
||||
std::cout << "none" << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
for(std::vector<ButtonMapping>::const_iterator i = opts.button_map.begin(); i != opts.button_map.end(); ++i)
|
||||
{
|
||||
std::cout << i->lhs << "->" << i->rhs << " ";
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
if (opts.axis_map.empty())
|
||||
{
|
||||
std::cout << "none" << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
for(std::vector<AxisMapping>::const_iterator i = opts.axis_map.begin(); i != opts.axis_map.end(); ++i)
|
||||
{
|
||||
std::cout << i->lhs << "->" << i->rhs << " ";
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void apply_deadzone(XboxGenericMsg& msg, int deadzone)
|
||||
|
@ -666,6 +1204,12 @@ void controller_loop(uInput* uinput, XboxGenericController* controller, CommandL
|
|||
{
|
||||
apply_deadzone(msg, opts.deadzone);
|
||||
|
||||
if (!opts.button_map.empty())
|
||||
apply_button_map(msg, opts.button_map);
|
||||
|
||||
if (!opts.axis_map.empty())
|
||||
apply_axis_map(msg, opts.axis_map);
|
||||
|
||||
if (memcmp(&msg, &oldmsg, sizeof(XboxGenericMsg)))
|
||||
{ // Only send a new event out if something has changed,
|
||||
// this is useful since some controllers send events
|
||||
|
@ -746,108 +1290,115 @@ void on_sigint(int)
|
|||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
signal(SIGINT, on_sigint);
|
||||
try
|
||||
{
|
||||
signal(SIGINT, on_sigint);
|
||||
|
||||
CommandLineOptions opts;
|
||||
CommandLineOptions opts;
|
||||
|
||||
parse_command_line(argc, argv, opts);
|
||||
parse_command_line(argc, argv, opts);
|
||||
|
||||
usb_init();
|
||||
usb_find_busses();
|
||||
usb_find_devices();
|
||||
usb_init();
|
||||
usb_find_busses();
|
||||
usb_find_devices();
|
||||
|
||||
struct usb_device* dev = 0;
|
||||
XPadDevice dev_type;
|
||||
struct usb_device* dev = 0;
|
||||
XPadDevice dev_type;
|
||||
|
||||
find_controller(dev, dev_type, opts);
|
||||
find_controller(dev, dev_type, opts);
|
||||
|
||||
if (!dev)
|
||||
{
|
||||
std::cout << "No suitable USB device found, abort" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (opts.gamepad_type != GAMEPAD_UNKNOWN)
|
||||
{ // Override the default gamepad type when given
|
||||
dev_type.type = opts.gamepad_type;
|
||||
}
|
||||
else
|
||||
if (!dev)
|
||||
{
|
||||
opts.gamepad_type = dev_type.type;
|
||||
std::cout << "No suitable USB device found, abort" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
print_info(dev, dev_type, opts);
|
||||
|
||||
XboxGenericController* controller = 0;
|
||||
|
||||
switch (dev_type.type)
|
||||
else
|
||||
{
|
||||
case GAMEPAD_XBOX:
|
||||
case GAMEPAD_XBOX_MAT:
|
||||
controller = new XboxController(dev);
|
||||
break;
|
||||
|
||||
case GAMEPAD_XBOX360_GUITAR:
|
||||
controller = new Xbox360Controller(dev, true);
|
||||
break;
|
||||
|
||||
case GAMEPAD_XBOX360:
|
||||
controller = new Xbox360Controller(dev, false);
|
||||
break;
|
||||
|
||||
case GAMEPAD_XBOX360_WIRELESS:
|
||||
controller = new Xbox360WirelessController(dev, opts.wireless_id);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(!"Unknown gamepad type");
|
||||
}
|
||||
|
||||
global_controller = controller;
|
||||
|
||||
int jsdev_number = find_jsdev_number();
|
||||
int evdev_number = find_evdev_number();
|
||||
|
||||
// FIXME: insert /dev/input/jsX detection magic here
|
||||
if (opts.led == -1)
|
||||
controller->set_led(2 + jsdev_number % 4);
|
||||
else
|
||||
controller->set_led(opts.led);
|
||||
|
||||
controller->set_rumble(opts.rumble_l, opts.rumble_r);
|
||||
|
||||
if (opts.instant_exit)
|
||||
{
|
||||
usleep(1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
uInput* uinput = 0;
|
||||
if (!opts.no_uinput)
|
||||
{
|
||||
std::cout << "Starting with uinput" << std::endl;
|
||||
uinput = new uInput(opts.gamepad_type, opts.uinput_config);
|
||||
if (opts.gamepad_type != GAMEPAD_UNKNOWN)
|
||||
{ // Override the default gamepad type when given
|
||||
dev_type.type = opts.gamepad_type;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Starting without uinput" << std::endl;
|
||||
opts.gamepad_type = dev_type.type;
|
||||
}
|
||||
std::cout << "\nYour Xbox/Xbox360 controller should now be available as:" << std::endl
|
||||
<< " /dev/input/js" << jsdev_number << std::endl
|
||||
<< " /dev/input/event" << evdev_number << std::endl;
|
||||
|
||||
print_info(dev, dev_type, opts);
|
||||
|
||||
XboxGenericController* controller = 0;
|
||||
|
||||
switch (dev_type.type)
|
||||
{
|
||||
case GAMEPAD_XBOX:
|
||||
case GAMEPAD_XBOX_MAT:
|
||||
controller = new XboxController(dev);
|
||||
break;
|
||||
|
||||
case GAMEPAD_XBOX360_GUITAR:
|
||||
controller = new Xbox360Controller(dev, true);
|
||||
break;
|
||||
|
||||
case GAMEPAD_XBOX360:
|
||||
controller = new Xbox360Controller(dev, false);
|
||||
break;
|
||||
|
||||
case GAMEPAD_XBOX360_WIRELESS:
|
||||
controller = new Xbox360WirelessController(dev, opts.wireless_id);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(!"Unknown gamepad type");
|
||||
}
|
||||
|
||||
global_controller = controller;
|
||||
|
||||
int jsdev_number = find_jsdev_number();
|
||||
int evdev_number = find_evdev_number();
|
||||
|
||||
// FIXME: insert /dev/input/jsX detection magic here
|
||||
if (opts.led == -1)
|
||||
controller->set_led(2 + jsdev_number % 4);
|
||||
else
|
||||
controller->set_led(opts.led);
|
||||
|
||||
controller->set_rumble(opts.rumble_l, opts.rumble_r);
|
||||
|
||||
if (opts.instant_exit)
|
||||
{
|
||||
usleep(1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
uInput* uinput = 0;
|
||||
if (!opts.no_uinput)
|
||||
{
|
||||
std::cout << "Starting with uinput" << std::endl;
|
||||
uinput = new uInput(opts.gamepad_type, opts.uinput_config);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Starting without uinput" << std::endl;
|
||||
}
|
||||
std::cout << "\nYour Xbox/Xbox360 controller should now be available as:" << std::endl
|
||||
<< " /dev/input/js" << jsdev_number << std::endl
|
||||
<< " /dev/input/event" << evdev_number << std::endl;
|
||||
|
||||
std::cout << "\nPress Ctrl-c to quit\n" << std::endl;
|
||||
std::cout << "\nPress Ctrl-c to quit\n" << std::endl;
|
||||
|
||||
global_exit_xboxdrv = false;
|
||||
controller_loop(uinput, controller, opts);
|
||||
global_exit_xboxdrv = false;
|
||||
controller_loop(uinput, controller, opts);
|
||||
|
||||
delete controller;
|
||||
delete uinput;
|
||||
delete controller;
|
||||
delete uinput;
|
||||
|
||||
std::cout << "Shutdown complete" << std::endl;
|
||||
std::cout << "Shutdown complete" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(std::exception& err)
|
||||
{
|
||||
std::cout << "Exception: " << err.what() << std::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
50
xboxdrv.hpp
50
xboxdrv.hpp
|
@ -27,6 +27,56 @@ struct XPadDevice {
|
|||
uint16_t idProduct;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
enum XboxButton {
|
||||
XBOX_BTN_UNKNOWN,
|
||||
XBOX_BTN_START,
|
||||
XBOX_BTN_GUIDE,
|
||||
XBOX_BTN_BACK,
|
||||
|
||||
XBOX_BTN_A,
|
||||
XBOX_BTN_B,
|
||||
XBOX_BTN_X,
|
||||
XBOX_BTN_Y,
|
||||
|
||||
XBOX_BTN_WHITE,
|
||||
XBOX_BTN_BLACK,
|
||||
|
||||
XBOX_BTN_LB,
|
||||
XBOX_BTN_RB,
|
||||
|
||||
XBOX_BTN_LT,
|
||||
XBOX_BTN_RT,
|
||||
|
||||
XBOX_BTN_THUMB_L,
|
||||
XBOX_BTN_THUMB_R,
|
||||
|
||||
XBOX_DPAD_UP,
|
||||
XBOX_DPAD_DOWN,
|
||||
XBOX_DPAD_LEFT,
|
||||
XBOX_DPAD_RIGHT,
|
||||
};
|
||||
|
||||
enum XboxAxis {
|
||||
XBOX_AXIS_UNKNOWN,
|
||||
XBOX_AXIS_X1,
|
||||
XBOX_AXIS_Y1,
|
||||
XBOX_AXIS_X2,
|
||||
XBOX_AXIS_Y2,
|
||||
XBOX_AXIS_LT,
|
||||
XBOX_AXIS_RT,
|
||||
};
|
||||
|
||||
struct ButtonMapping {
|
||||
XboxButton lhs;
|
||||
XboxButton rhs;
|
||||
};
|
||||
|
||||
struct AxisMapping {
|
||||
XboxAxis lhs;
|
||||
XboxAxis rhs;
|
||||
bool invert;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
|
|
Loading…
Reference in a new issue