Added Xbox360 Wireless support
This commit is contained in:
parent
c2d807735e
commit
b88ef37406
15 changed files with 312 additions and 215 deletions
10
SConstruct
10
SConstruct
|
@ -1,14 +1,18 @@
|
||||||
# -*- python -*-
|
# -*- python -*-
|
||||||
|
|
||||||
env = Environment(CPPFLAGS=["-g", "-O0", "-Wall"], LIBS=["usb"])
|
env = Environment(CPPFLAGS=["-g", "-O0", "-Wall"], LIBS=["usb"])
|
||||||
env.Program("xboxdrv", ["xboxdrv.cpp", "xboxmsg.cpp", "uinput.cpp"])
|
env.Program("xboxdrv", ["xboxdrv.cpp",
|
||||||
|
"xboxmsg.cpp",
|
||||||
|
"uinput.cpp",
|
||||||
|
"xbox_controller.cpp",
|
||||||
|
"xbox360_controller.cpp",
|
||||||
|
"xbox360_wireless_controller.cpp",
|
||||||
|
])
|
||||||
env.Program("inputdrv",
|
env.Program("inputdrv",
|
||||||
["inputdrv.cpp",
|
["inputdrv.cpp",
|
||||||
"xbox360_driver.cpp",
|
"xbox360_driver.cpp",
|
||||||
"evdev_driver.cpp",
|
"evdev_driver.cpp",
|
||||||
"xbox360_usb_thread.cpp",
|
"xbox360_usb_thread.cpp",
|
||||||
"xbox360_controller.cpp",
|
|
||||||
"xbox360_wireless_controller.cpp",
|
|
||||||
"control.cpp",
|
"control.cpp",
|
||||||
"abs_to_rel.cpp",
|
"abs_to_rel.cpp",
|
||||||
"abs_to_btn.cpp",
|
"abs_to_btn.cpp",
|
||||||
|
|
8
TODO
8
TODO
|
@ -1,3 +1,11 @@
|
||||||
|
Test:
|
||||||
|
=====
|
||||||
|
--wid
|
||||||
|
controller type enforcment is broken
|
||||||
|
deadzone is broken
|
||||||
|
Rumble
|
||||||
|
LED
|
||||||
|
|
||||||
Easy interface:
|
Easy interface:
|
||||||
===============
|
===============
|
||||||
--trigger-as-zaxis
|
--trigger-as-zaxis
|
||||||
|
|
18
uinput.cpp
18
uinput.cpp
|
@ -16,6 +16,7 @@
|
||||||
** along with this program. If not, see <http://www.gnu.org/licenses/>.
|
** along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
@ -261,6 +262,23 @@ uInput::send_axis(uint16_t code, int32_t value)
|
||||||
write(fd, &ev, sizeof(ev));
|
write(fd, &ev, sizeof(ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
uInput::send(XboxGenericMsg& msg)
|
||||||
|
{
|
||||||
|
if (msg.type == GAMEPAD_XBOX)
|
||||||
|
send(msg.xbox);
|
||||||
|
else if (msg.type == GAMEPAD_XBOX_MAT)
|
||||||
|
send(msg.xbox);
|
||||||
|
else if (msg.type == GAMEPAD_XBOX360)
|
||||||
|
send(msg.xbox360);
|
||||||
|
else if (msg.type == GAMEPAD_XBOX360_WIRELESS)
|
||||||
|
send(msg.xbox360);
|
||||||
|
else if (msg.type == GAMEPAD_XBOX360_GUITAR)
|
||||||
|
send(msg.guitar);
|
||||||
|
else
|
||||||
|
assert(!"Unknown XboxGenericMsg type");
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
uInput::send(Xbox360Msg& msg)
|
uInput::send(Xbox360Msg& msg)
|
||||||
{
|
{
|
||||||
|
|
|
@ -51,7 +51,8 @@ public:
|
||||||
|
|
||||||
void setup_xbox360_gamepad(GamepadType type);
|
void setup_xbox360_gamepad(GamepadType type);
|
||||||
void setup_xbox360_guitar();
|
void setup_xbox360_guitar();
|
||||||
|
|
||||||
|
void send(XboxGenericMsg& msg);
|
||||||
void send(Xbox360Msg& msg);
|
void send(Xbox360Msg& msg);
|
||||||
void send(Xbox360GuitarMsg& msg);
|
void send(Xbox360GuitarMsg& msg);
|
||||||
void send(XboxMsg& msg);
|
void send(XboxMsg& msg);
|
||||||
|
|
|
@ -18,13 +18,15 @@
|
||||||
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <boost/format.hpp>
|
||||||
#include "xboxmsg.hpp"
|
#include "xboxmsg.hpp"
|
||||||
#include "xbox360_controller.hpp"
|
#include "xbox360_controller.hpp"
|
||||||
|
|
||||||
Xbox360Controller::Xbox360Controller(struct usb_device* dev,
|
Xbox360Controller::Xbox360Controller(struct usb_device* dev,
|
||||||
XPadDevice* dev_type)
|
XPadDevice* dev_type)
|
||||||
{
|
{
|
||||||
struct usb_dev_handle* handle = usb_open(dev);
|
handle = usb_open(dev);
|
||||||
if (!handle)
|
if (!handle)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Error opening Xbox360 controller");
|
throw std::runtime_error("Error opening Xbox360 controller");
|
||||||
|
@ -38,11 +40,17 @@ Xbox360Controller::Xbox360Controller(struct usb_device* dev,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Xbox360Controller::~Xbox360Controller()
|
||||||
|
{
|
||||||
|
usb_release_interface(handle, 0);
|
||||||
|
usb_close(handle);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Xbox360Controller::set_rumble(uint8_t left, uint8_t right)
|
Xbox360Controller::set_rumble(uint8_t left, uint8_t right)
|
||||||
{
|
{
|
||||||
char rumblecmd[] = { 0x00, 0x08, 0x00, left, right, 0x00, 0x00, 0x00 };
|
char rumblecmd[] = { 0x00, 0x08, 0x00, left, right, 0x00, 0x00, 0x00 };
|
||||||
usb_interrupt_write(handle, 2, rumblecmd, 8, 0);
|
usb_interrupt_write(handle, 2, rumblecmd, sizeof(rumblecmd), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -53,7 +61,7 @@ Xbox360Controller::set_led(uint8_t status)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Xbox360Controller::read(Xbox360Msg& msg)
|
Xbox360Controller::read(XboxGenericMsg& msg)
|
||||||
{
|
{
|
||||||
uint8_t data[32];
|
uint8_t data[32];
|
||||||
int ret = usb_interrupt_read(handle, 1 /*EndPoint*/, (char*)data, sizeof(data), 0 /*Timeout*/);
|
int ret = usb_interrupt_read(handle, 1 /*EndPoint*/, (char*)data, sizeof(data), 0 /*Timeout*/);
|
||||||
|
@ -71,7 +79,18 @@ Xbox360Controller::read(Xbox360Msg& msg)
|
||||||
}
|
}
|
||||||
else if (ret == 20 && data[0] == 0x00 && data[1] == 0x14)
|
else if (ret == 20 && data[0] == 0x00 && data[1] == 0x14)
|
||||||
{
|
{
|
||||||
msg = (Xbox360Msg&)data;
|
msg.type = GAMEPAD_XBOX360;
|
||||||
|
msg.xbox360 = *reinterpret_cast<Xbox360Msg*>(data);
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,11 +20,12 @@
|
||||||
#define HEADER_XBOX360_CONTROLLER_HPP
|
#define HEADER_XBOX360_CONTROLLER_HPP
|
||||||
|
|
||||||
#include <usb.h>
|
#include <usb.h>
|
||||||
|
#include "xbox_generic_controller.hpp"
|
||||||
|
|
||||||
struct XPadDevice;
|
struct XPadDevice;
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
class Xbox360Controller
|
class Xbox360Controller : public XboxGenericController
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
struct usb_device* dev;
|
struct usb_device* dev;
|
||||||
|
@ -34,10 +35,11 @@ private:
|
||||||
public:
|
public:
|
||||||
Xbox360Controller(struct usb_device* dev,
|
Xbox360Controller(struct usb_device* dev,
|
||||||
XPadDevice* dev_type);
|
XPadDevice* dev_type);
|
||||||
|
~Xbox360Controller();
|
||||||
|
|
||||||
void set_rumble(uint8_t left, uint8_t right);
|
void set_rumble(uint8_t left, uint8_t right);
|
||||||
void set_led(uint8_t status);
|
void set_led(uint8_t status);
|
||||||
void read(Xbox360Msg& msg);
|
void read(XboxGenericMsg& msg);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Xbox360Controller (const Xbox360Controller&);
|
Xbox360Controller (const Xbox360Controller&);
|
||||||
|
|
|
@ -34,7 +34,7 @@ Xbox360WirelessController::Xbox360WirelessController(struct usb_device* dev,
|
||||||
endpoint = controller_id*2 + 1;
|
endpoint = controller_id*2 + 1;
|
||||||
interface = controller_id*2;
|
interface = controller_id*2;
|
||||||
|
|
||||||
struct usb_dev_handle* handle = usb_open(dev);
|
handle = usb_open(dev);
|
||||||
if (!handle)
|
if (!handle)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Xbox360WirelessController: Error opening Xbox360 controller");
|
throw std::runtime_error("Xbox360WirelessController: Error opening Xbox360 controller");
|
||||||
|
@ -48,6 +48,12 @@ Xbox360WirelessController::Xbox360WirelessController(struct usb_device* dev,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Xbox360WirelessController::~Xbox360WirelessController()
|
||||||
|
{
|
||||||
|
usb_release_interface(handle, interface);
|
||||||
|
usb_close(handle);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Xbox360WirelessController::set_rumble(uint8_t left, uint8_t right)
|
Xbox360WirelessController::set_rumble(uint8_t left, uint8_t right)
|
||||||
{
|
{
|
||||||
|
@ -67,7 +73,7 @@ Xbox360WirelessController::set_led(uint8_t status)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Xbox360WirelessController::read(Xbox360Msg& msg)
|
Xbox360WirelessController::read(XboxGenericMsg& msg)
|
||||||
{
|
{
|
||||||
uint8_t data[32];
|
uint8_t data[32];
|
||||||
int ret = usb_interrupt_read(handle, endpoint, (char*)data, sizeof(data), 0 /*Timeout*/);
|
int ret = usb_interrupt_read(handle, endpoint, (char*)data, sizeof(data), 0 /*Timeout*/);
|
||||||
|
@ -112,7 +118,8 @@ Xbox360WirelessController::read(Xbox360Msg& msg)
|
||||||
}
|
}
|
||||||
else if (data[0] == 0x00 && data[1] == 0x01 && data[2] == 0x00 && data[3] == 0xf0 && data[4] == 0x00 && data[5] == 0x13)
|
else if (data[0] == 0x00 && data[1] == 0x01 && data[2] == 0x00 && data[3] == 0xf0 && data[4] == 0x00 && data[5] == 0x13)
|
||||||
{
|
{
|
||||||
msg = *reinterpret_cast<Xbox360Msg*>(&data[6]);
|
msg.type = GAMEPAD_XBOX360_WIRELESS;
|
||||||
|
msg.xbox360 = *reinterpret_cast<Xbox360Msg*>(&data[6]);
|
||||||
}
|
}
|
||||||
else if (data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x13)
|
else if (data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x13)
|
||||||
{ // Battery status
|
{ // Battery status
|
||||||
|
|
|
@ -19,10 +19,12 @@
|
||||||
#ifndef HEADER_XBOX360_WIRELESS_CONTROLLER_HPP
|
#ifndef HEADER_XBOX360_WIRELESS_CONTROLLER_HPP
|
||||||
#define HEADER_XBOX360_WIRELESS_CONTROLLER_HPP
|
#define HEADER_XBOX360_WIRELESS_CONTROLLER_HPP
|
||||||
|
|
||||||
|
#include "xbox_generic_controller.hpp"
|
||||||
|
|
||||||
struct XPadDevice;
|
struct XPadDevice;
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
class Xbox360WirelessController
|
class Xbox360WirelessController : public XboxGenericController
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
struct usb_device* dev;
|
struct usb_device* dev;
|
||||||
|
@ -37,10 +39,11 @@ public:
|
||||||
Xbox360WirelessController(struct usb_device* dev,
|
Xbox360WirelessController(struct usb_device* dev,
|
||||||
XPadDevice* dev_type,
|
XPadDevice* dev_type,
|
||||||
int controller_id);
|
int controller_id);
|
||||||
|
virtual ~Xbox360WirelessController();
|
||||||
|
|
||||||
void set_rumble(uint8_t left, uint8_t right);
|
void set_rumble(uint8_t left, uint8_t right);
|
||||||
void set_led(uint8_t status);
|
void set_led(uint8_t status);
|
||||||
void read(Xbox360Msg& msg);
|
void read(XboxGenericMsg& msg);
|
||||||
uint8_t get_battery_status() const;
|
uint8_t get_battery_status() const;
|
||||||
private:
|
private:
|
||||||
Xbox360WirelessController (const Xbox360WirelessController&);
|
Xbox360WirelessController (const Xbox360WirelessController&);
|
||||||
|
|
|
@ -19,12 +19,12 @@
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include "xboxmsg.hpp"
|
#include "xboxmsg.hpp"
|
||||||
#include "xbox360_controller.hpp"
|
#include "xbox_controller.hpp"
|
||||||
|
|
||||||
XboxController::XboxController(struct usb_device* dev,
|
XboxController::XboxController(struct usb_device* dev,
|
||||||
XPadDevice* dev_type)
|
XPadDevice* dev_type)
|
||||||
{
|
{
|
||||||
struct usb_dev_handle* handle = usb_open(dev);
|
handle = usb_open(dev);
|
||||||
if (!handle)
|
if (!handle)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Error opening Xbox360 controller");
|
throw std::runtime_error("Error opening Xbox360 controller");
|
||||||
|
@ -38,10 +38,16 @@ XboxController::XboxController(struct usb_device* dev,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
XboxController::~XboxController()
|
||||||
|
{
|
||||||
|
usb_release_interface(handle, 0);
|
||||||
|
usb_close(handle);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
XboxController::set_rumble(uint8_t left, uint8_t right)
|
XboxController::set_rumble(uint8_t left, uint8_t right)
|
||||||
{
|
{
|
||||||
char rumblecmd[] = { 0x00, 0x06, 0x00, r, 0x00, l };
|
char rumblecmd[] = { 0x00, 0x06, 0x00, left, 0x00, right };
|
||||||
usb_interrupt_write(handle, 2, rumblecmd, sizeof(rumblecmd), 0);
|
usb_interrupt_write(handle, 2, rumblecmd, sizeof(rumblecmd), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,8 +58,9 @@ XboxController::set_led(uint8_t status)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
XboxController::read(XboxGenericMsg& msg);
|
XboxController::read(XboxGenericMsg& msg)
|
||||||
{
|
{
|
||||||
|
// FIXME: Add tracking for duplicate data packages (send by logitech controller)
|
||||||
uint8_t data[32];
|
uint8_t data[32];
|
||||||
int ret = usb_interrupt_read(handle, 1 /*EndPoint*/, (char*)data, sizeof(data), 0 /*Timeout*/);
|
int ret = usb_interrupt_read(handle, 1 /*EndPoint*/, (char*)data, sizeof(data), 0 /*Timeout*/);
|
||||||
|
|
||||||
|
|
|
@ -20,11 +20,12 @@
|
||||||
#define HEADER_XBOX_CONTROLLER_HPP
|
#define HEADER_XBOX_CONTROLLER_HPP
|
||||||
|
|
||||||
#include <usb.h>
|
#include <usb.h>
|
||||||
|
#include "xbox_generic_controller.hpp"
|
||||||
|
|
||||||
struct XPadDevice;
|
struct XPadDevice;
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
class XboxController
|
class XboxController : public XboxGenericController
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
struct usb_device* dev;
|
struct usb_device* dev;
|
||||||
|
@ -33,7 +34,8 @@ private:
|
||||||
|
|
||||||
public:
|
public:
|
||||||
XboxController(struct usb_device* dev,
|
XboxController(struct usb_device* dev,
|
||||||
XPadDevice* dev_type);
|
XPadDevice* dev_type);
|
||||||
|
virtual ~XboxController();
|
||||||
|
|
||||||
void set_rumble(uint8_t left, uint8_t right);
|
void set_rumble(uint8_t left, uint8_t right);
|
||||||
void set_led(uint8_t status);
|
void set_led(uint8_t status);
|
||||||
|
|
41
xbox_generic_controller.hpp
Normal file
41
xbox_generic_controller.hpp
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
** 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_XBOX_GENERIC_CONTROLLER_HPP
|
||||||
|
#define HEADER_XBOX_GENERIC_CONTROLLER_HPP
|
||||||
|
|
||||||
|
/** */
|
||||||
|
class XboxGenericController
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
public:
|
||||||
|
XboxGenericController() {}
|
||||||
|
virtual ~XboxGenericController() {}
|
||||||
|
|
||||||
|
virtual void set_rumble(uint8_t left, uint8_t right) =0;
|
||||||
|
virtual void set_led(uint8_t status) =0;
|
||||||
|
virtual void read(XboxGenericMsg& msg) =0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
XboxGenericController (const XboxGenericController&);
|
||||||
|
XboxGenericController& operator= (const XboxGenericController&);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* EOF */
|
323
xboxdrv.cpp
323
xboxdrv.cpp
|
@ -23,6 +23,11 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include "uinput.hpp"
|
#include "uinput.hpp"
|
||||||
#include "xboxmsg.hpp"
|
#include "xboxmsg.hpp"
|
||||||
|
#include "xbox_controller.hpp"
|
||||||
|
#include "xbox360_controller.hpp"
|
||||||
|
#include "xbox360_wireless_controller.hpp"
|
||||||
|
#include "xbox_generic_controller.hpp"
|
||||||
|
|
||||||
#include "xboxdrv.hpp"
|
#include "xboxdrv.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
@ -106,8 +111,8 @@ void list_controller()
|
||||||
struct usb_bus* busses = usb_get_busses();
|
struct usb_bus* busses = usb_get_busses();
|
||||||
|
|
||||||
int id = 0;
|
int id = 0;
|
||||||
std::cout << " id | idVendor | idProduct | Name" << std::endl;
|
std::cout << " id | wid | idVendor | idProduct | Name" << std::endl;
|
||||||
std::cout << "----+----------+-----------+---------------------------------" << std::endl;
|
std::cout << "----+-----+----------+-----------+--------------------------------------" << std::endl;
|
||||||
for (struct usb_bus* bus = busses; bus; bus = bus->next)
|
for (struct usb_bus* bus = busses; bus; bus = bus->next)
|
||||||
{
|
{
|
||||||
for (struct usb_device* dev = bus->devices; dev; dev = dev->next)
|
for (struct usb_device* dev = bus->devices; dev; dev = dev->next)
|
||||||
|
@ -117,12 +122,30 @@ void list_controller()
|
||||||
if (dev->descriptor.idVendor == xpad_devices[i].idVendor &&
|
if (dev->descriptor.idVendor == xpad_devices[i].idVendor &&
|
||||||
dev->descriptor.idProduct == xpad_devices[i].idProduct)
|
dev->descriptor.idProduct == xpad_devices[i].idProduct)
|
||||||
{
|
{
|
||||||
std::cout << boost::format(" %2d | 0x%04x | 0x%04x | %s")
|
if (xpad_devices[i].type == GAMEPAD_XBOX360_WIRELESS)
|
||||||
% id
|
{
|
||||||
% int(xpad_devices[i].idVendor)
|
for(int wid = 0; wid < 3; ++wid)
|
||||||
% int(xpad_devices[i].idProduct)
|
{
|
||||||
% xpad_devices[i].name
|
std::cout << boost::format(" %2d | %2d | 0x%04x | 0x%04x | %s (Port: %s)")
|
||||||
<< std::endl;
|
% id
|
||||||
|
% wid
|
||||||
|
% int(xpad_devices[i].idVendor)
|
||||||
|
% int(xpad_devices[i].idProduct)
|
||||||
|
% xpad_devices[i].name
|
||||||
|
% wid
|
||||||
|
<< std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cout << boost::format(" %2d | %2d | 0x%04x | 0x%04x | %s")
|
||||||
|
% id
|
||||||
|
% 0
|
||||||
|
% int(xpad_devices[i].idVendor)
|
||||||
|
% int(xpad_devices[i].idProduct)
|
||||||
|
% xpad_devices[i].name
|
||||||
|
<< std::endl;
|
||||||
|
}
|
||||||
id += 1;
|
id += 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -202,6 +225,7 @@ struct CommandLineOptions
|
||||||
int rumble_l;
|
int rumble_l;
|
||||||
int rumble_r;
|
int rumble_r;
|
||||||
int controller_id;
|
int controller_id;
|
||||||
|
int wireless_id;
|
||||||
bool instant_exit;
|
bool instant_exit;
|
||||||
bool no_uinput;
|
bool no_uinput;
|
||||||
GamepadType gamepad_type;
|
GamepadType gamepad_type;
|
||||||
|
@ -217,6 +241,7 @@ struct CommandLineOptions
|
||||||
rumble_l = 0;
|
rumble_l = 0;
|
||||||
rumble_r = 0;
|
rumble_r = 0;
|
||||||
controller_id = 0;
|
controller_id = 0;
|
||||||
|
wireless_id = 0;
|
||||||
instant_exit = false;
|
instant_exit = false;
|
||||||
no_uinput = false;
|
no_uinput = false;
|
||||||
gamepad_type = GAMEPAD_UNKNOWN;
|
gamepad_type = GAMEPAD_UNKNOWN;
|
||||||
|
@ -236,7 +261,8 @@ void print_command_line_help(int argc, char** argv)
|
||||||
std::cout << " --help-led list possible values for the led" << std::endl;
|
std::cout << " --help-led list possible values for the led" << std::endl;
|
||||||
std::cout << " --help-devices list supported devices" << std::endl;
|
std::cout << " --help-devices list supported devices" << std::endl;
|
||||||
std::cout << " -v, --verbose display controller events" << std::endl;
|
std::cout << " -v, --verbose display controller events" << std::endl;
|
||||||
std::cout << " -i, --id N use controller number (default: 0)" << std::endl;
|
std::cout << " -i, --id N use controller with id N (default: 0)" << std::endl;
|
||||||
|
std::cout << " -w, --wid N use wireless controller with wid N (default: 0)" << std::endl;
|
||||||
std::cout << " -L, --list-controller list available controllers" << std::endl;
|
std::cout << " -L, --list-controller list available controllers" << std::endl;
|
||||||
std::cout << " -R, --test-rumble map rumbling to LT and RT (for testing only)" << std::endl;
|
std::cout << " -R, --test-rumble map rumbling to LT and RT (for testing only)" << std::endl;
|
||||||
std::cout << " --no-uinput do not try to start uinput event dispatching" << std::endl;
|
std::cout << " --no-uinput do not try to start uinput event dispatching" << std::endl;
|
||||||
|
@ -262,25 +288,25 @@ void print_command_line_help(int argc, char** argv)
|
||||||
|
|
||||||
void print_led_help()
|
void print_led_help()
|
||||||
{
|
{
|
||||||
std::cout <<
|
std::cout <<
|
||||||
"Possible values for '--led VALUE' are:\n\n"
|
"Possible values for '--led VALUE' are:\n\n"
|
||||||
" 0: off\n"
|
" 0: off\n"
|
||||||
" 1: all blinking\n"
|
" 1: all blinking\n"
|
||||||
" 2: 1/top-left blink, then on\n"
|
" 2: 1/top-left blink, then on\n"
|
||||||
" 3: 2/top-right blink, then on\n"
|
" 3: 2/top-right blink, then on\n"
|
||||||
" 4: 3/bottom-left blink, then on\n"
|
" 4: 3/bottom-left blink, then on\n"
|
||||||
" 5: 4/bottom-right blink, then on\n"
|
" 5: 4/bottom-right blink, then on\n"
|
||||||
" 6: 1/top-left on\n"
|
" 6: 1/top-left on\n"
|
||||||
" 7: 2/top-right on\n"
|
" 7: 2/top-right on\n"
|
||||||
" 8: 3/bottom-left on\n"
|
" 8: 3/bottom-left on\n"
|
||||||
" 9: 4/bottom-right on\n"
|
" 9: 4/bottom-right on\n"
|
||||||
" 10: rotate\n"
|
" 10: rotate\n"
|
||||||
" 11: blink\n"
|
" 11: blink\n"
|
||||||
" 12: blink slower\n"
|
" 12: blink slower\n"
|
||||||
" 13: rotate with two lights\n"
|
" 13: rotate with two lights\n"
|
||||||
" 14: blink\n"
|
" 14: blink\n"
|
||||||
" 15: blink once\n"
|
" 15: blink once\n"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void parse_command_line(int argc, char** argv, CommandLineOptions& opts)
|
void parse_command_line(int argc, char** argv, CommandLineOptions& opts)
|
||||||
|
@ -394,6 +420,20 @@ void parse_command_line(int argc, char** argv, CommandLineOptions& opts)
|
||||||
exit(EXIT_FAILURE);
|
exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (strcmp(argv[i], "-w") == 0 ||
|
||||||
|
strcmp(argv[i], "--wid") == 0)
|
||||||
|
{
|
||||||
|
++i;
|
||||||
|
if (i < argc)
|
||||||
|
{
|
||||||
|
opts.wireless_id = atoi(argv[i]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cout << "Error: " << argv[i-1] << " expected a argument" << std::endl;
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (strcmp(argv[i], "-l") == 0 ||
|
else if (strcmp(argv[i], "-l") == 0 ||
|
||||||
strcmp(argv[i], "--led") == 0)
|
strcmp(argv[i], "--led") == 0)
|
||||||
{
|
{
|
||||||
|
@ -533,6 +573,8 @@ void print_info(struct usb_device* dev,
|
||||||
std::cout << "USB Device: " << dev->bus->dirname << ":" << dev->filename << std::endl;
|
std::cout << "USB Device: " << dev->bus->dirname << ":" << dev->filename << std::endl;
|
||||||
std::cout << "Controller: " << boost::format("\"%s\" (idVendor: 0x%04x, idProduct: 0x%04x)")
|
std::cout << "Controller: " << boost::format("\"%s\" (idVendor: 0x%04x, idProduct: 0x%04x)")
|
||||||
% (dev_type ? dev_type->name : "unknown") % uint16_t(dev->descriptor.idVendor) % uint16_t(dev->descriptor.idProduct) << std::endl;
|
% (dev_type ? dev_type->name : "unknown") % uint16_t(dev->descriptor.idVendor) % uint16_t(dev->descriptor.idProduct) << std::endl;
|
||||||
|
if (dev_type->type == GAMEPAD_XBOX360_WIRELESS)
|
||||||
|
std::cout << "Wireless Port: " << opts.wireless_id << std::endl;
|
||||||
std::cout << "Controller Type: " << opts.gamepad_type << std::endl;
|
std::cout << "Controller Type: " << opts.gamepad_type << std::endl;
|
||||||
std::cout << "Deadzone: " << opts.deadzone << std::endl;
|
std::cout << "Deadzone: " << opts.deadzone << std::endl;
|
||||||
std::cout << "Rumble Debug: " << (opts.rumble ? "on" : "off") << std::endl;
|
std::cout << "Rumble Debug: " << (opts.rumble ? "on" : "off") << std::endl;
|
||||||
|
@ -540,6 +582,45 @@ void print_info(struct usb_device* dev,
|
||||||
std::cout << "LED Status: " << int(opts.led) << std::endl;
|
std::cout << "LED Status: " << int(opts.led) << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void controller_loop(XboxGenericController* controller, CommandLineOptions& opts)
|
||||||
|
{
|
||||||
|
uInput* uinput = 0;
|
||||||
|
if (!opts.no_uinput)
|
||||||
|
{
|
||||||
|
std::cout << "Starting uinput" << std::endl;
|
||||||
|
uinput = new uInput(opts.gamepad_type, opts.uinput_config);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cout << "Starting without uinput" << std::endl;
|
||||||
|
}
|
||||||
|
std::cout << "\nYour Xbox360 controller should now be available as /dev/input/jsX and /dev/input/eventX" << std::endl;
|
||||||
|
std::cout << "Press Ctrl-c to quit" << std::endl;
|
||||||
|
|
||||||
|
bool quit = false;
|
||||||
|
while(!quit)
|
||||||
|
{
|
||||||
|
XboxGenericMsg msg;
|
||||||
|
controller->read(msg);
|
||||||
|
if (opts.verbose)
|
||||||
|
std::cout << msg << std::endl;
|
||||||
|
if (uinput) uinput->send(msg);
|
||||||
|
|
||||||
|
if (opts.rumble)
|
||||||
|
{
|
||||||
|
if (opts.gamepad_type == GAMEPAD_XBOX)
|
||||||
|
{
|
||||||
|
controller->set_rumble(msg.xbox.lt, msg.xbox.rt);
|
||||||
|
}
|
||||||
|
else if (opts.gamepad_type == GAMEPAD_XBOX360 ||
|
||||||
|
opts.gamepad_type == GAMEPAD_XBOX360_WIRELESS)
|
||||||
|
{
|
||||||
|
controller->set_rumble(msg.xbox360.lt, msg.xbox360.rt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
srand(time(0));
|
srand(time(0));
|
||||||
|
@ -595,167 +676,41 @@ int main(int argc, char** argv)
|
||||||
|
|
||||||
print_info(dev, dev_type, opts);
|
print_info(dev, dev_type, opts);
|
||||||
|
|
||||||
struct usb_dev_handle* handle = usb_open(dev);
|
XboxGenericController* controller = 0;
|
||||||
if (!handle)
|
|
||||||
|
switch (dev_type->type)
|
||||||
{
|
{
|
||||||
std::cout << "Error opening Xbox360 controller" << std::endl;
|
case GAMEPAD_XBOX:
|
||||||
|
case GAMEPAD_XBOX_MAT:
|
||||||
|
controller = new XboxController(dev, dev_type);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GAMEPAD_XBOX360_GUITAR:
|
||||||
|
case GAMEPAD_XBOX360:
|
||||||
|
controller = new Xbox360Controller(dev, dev_type);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GAMEPAD_XBOX360_WIRELESS:
|
||||||
|
controller = new Xbox360WirelessController(dev, dev_type, opts.wireless_id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
assert(!"Unknown gamepad type");
|
||||||
|
}
|
||||||
|
|
||||||
|
controller->set_led(opts.led);
|
||||||
|
controller->set_rumble(opts.rumble_l, opts.rumble_r);
|
||||||
|
|
||||||
|
if (opts.instant_exit)
|
||||||
|
{
|
||||||
|
usleep(1000);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (usb_claim_interface(handle, 0) != 0) // FIXME: bInterfaceNumber shouldn't be hardcoded
|
controller_loop(controller, opts);
|
||||||
std::cout << "Error claiming the interface: " << usb_strerror() << std::endl;
|
delete controller;
|
||||||
|
|
||||||
// Handle LED on Xbox360 Controller
|
|
||||||
if (opts.gamepad_type == GAMEPAD_XBOX360 ||
|
|
||||||
opts.gamepad_type == GAMEPAD_XBOX360_GUITAR)
|
|
||||||
{
|
|
||||||
char ledcmd[] = { 1, 3, opts.led };
|
|
||||||
usb_interrupt_write(handle, 2, ledcmd, 3, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch of Rumble
|
|
||||||
if (opts.gamepad_type == GAMEPAD_XBOX360)
|
|
||||||
{
|
|
||||||
char l = opts.rumble_r; // light weight
|
|
||||||
char b = opts.rumble_l; // big weight
|
|
||||||
char rumblecmd[] = { 0x00, 0x08, 0x00, b, l, 0x00, 0x00, 0x00 };
|
|
||||||
usb_interrupt_write(handle, 2, rumblecmd, 8, 0);
|
|
||||||
}
|
|
||||||
else if (opts.gamepad_type == GAMEPAD_XBOX)
|
|
||||||
{
|
|
||||||
char l = opts.rumble_l;
|
|
||||||
char b = opts.rumble_r;
|
|
||||||
char rumblecmd[] = { 0x00, 0x06, 0x00, l, 0x00, b };
|
|
||||||
usb_interrupt_write(handle, 2, rumblecmd, 6, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.instant_exit)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
uInput* uinput = 0;
|
|
||||||
if (!opts.no_uinput)
|
|
||||||
{
|
|
||||||
std::cout << "Starting uinput" << std::endl;
|
|
||||||
uinput = new uInput(opts.gamepad_type, opts.uinput_config);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
std::cout << "Starting without uinput" << std::endl;
|
|
||||||
}
|
|
||||||
std::cout << "\nYour Xbox360 controller should now be available as /dev/input/jsX and /dev/input/eventX" << std::endl;
|
|
||||||
std::cout << "Press Ctrl-c to quit" << std::endl;
|
|
||||||
|
|
||||||
bool quit = false;
|
|
||||||
uint8_t old_data[20];
|
|
||||||
memset(old_data, 0, 20);
|
|
||||||
while(!quit)
|
|
||||||
{
|
|
||||||
uint8_t data[32];
|
|
||||||
int ret = usb_interrupt_read(handle, 1 /*EndPoint*/, (char*)data, sizeof(data), 0 /*Timeout*/);
|
|
||||||
|
|
||||||
if (ret < 0)
|
|
||||||
{ // Error
|
|
||||||
std::cout << "USBError: " << ret << "\n" << usb_strerror() << std::endl;
|
|
||||||
std::cout << "Shutting down" << std::endl;
|
|
||||||
quit = true;
|
|
||||||
}
|
|
||||||
else if (ret == 0)
|
|
||||||
{
|
|
||||||
// happen with the Xbox360 controller every now
|
|
||||||
// and then, just ignore, seems harmless
|
|
||||||
}
|
|
||||||
else if (ret == 20 && data[0] == 0x00 && data[1] == 0x14)
|
|
||||||
{
|
|
||||||
if (memcmp(data, old_data, 20) == 0)
|
|
||||||
{
|
|
||||||
// Ignore the data, since nothing has changed
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
memcpy(old_data, data, 20);
|
|
||||||
|
|
||||||
if (opts.gamepad_type == GAMEPAD_XBOX360_GUITAR)
|
|
||||||
{
|
|
||||||
Xbox360GuitarMsg& msg = (Xbox360GuitarMsg&)data;
|
|
||||||
if (opts.verbose)
|
|
||||||
std::cout << msg << std::endl;
|
|
||||||
|
|
||||||
uinput->send(msg);
|
|
||||||
}
|
|
||||||
else if (opts.gamepad_type == GAMEPAD_XBOX360)
|
|
||||||
{
|
|
||||||
Xbox360Msg& msg = (Xbox360Msg&)data;
|
|
||||||
|
|
||||||
if (abs(msg.x1) < opts.deadzone)
|
|
||||||
msg.x1 = 0;
|
|
||||||
|
|
||||||
if (abs(msg.y1) < opts.deadzone)
|
|
||||||
msg.y1 = 0;
|
|
||||||
|
|
||||||
if (abs(msg.x2) < opts.deadzone)
|
|
||||||
msg.x2 = 0;
|
|
||||||
|
|
||||||
if (abs(msg.y2) < opts.deadzone)
|
|
||||||
msg.y2 = 0;
|
|
||||||
|
|
||||||
if (opts.verbose)
|
|
||||||
std::cout << msg << std::endl;
|
|
||||||
|
|
||||||
if (uinput) uinput->send(msg);
|
|
||||||
|
|
||||||
if (opts.rumble)
|
|
||||||
{
|
|
||||||
char l = msg.rt;
|
|
||||||
char b = msg.lt;
|
|
||||||
char rumblecmd[] = { 0x00, 0x08, 0x00, b, l, 0x00, 0x00, 0x00 };
|
|
||||||
usb_interrupt_write(handle, 2, rumblecmd, 8, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (opts.gamepad_type == GAMEPAD_XBOX)
|
|
||||||
{
|
|
||||||
XboxMsg& msg = (XboxMsg&)data;
|
|
||||||
|
|
||||||
if (opts.verbose)
|
|
||||||
std::cout << msg << std::endl;
|
|
||||||
|
|
||||||
if (uinput) uinput->send(msg);
|
|
||||||
|
|
||||||
if (opts.rumble)
|
|
||||||
{
|
|
||||||
char l = msg.lt;
|
|
||||||
char b = msg.rt;
|
|
||||||
char rumblecmd[] = { 0x00, 0x06, 0x00, l, 0x00, b };
|
|
||||||
usb_interrupt_write(handle, 2, rumblecmd, 6, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 << "\r" << std::flush;
|
|
||||||
std::cout << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
usb_release_interface(handle, 0); // FIXME: bInterfaceNumber shouldn't be hardcoded
|
|
||||||
|
|
||||||
// Almost never reached since the user will Ctrl-c and we
|
|
||||||
// can't use sigint since we block in usb_interrupt_read()
|
|
||||||
usb_close(handle);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* EOF */
|
/* EOF */
|
||||||
|
|
11
xboxdrv.hpp
11
xboxdrv.hpp
|
@ -18,15 +18,8 @@
|
||||||
|
|
||||||
#ifndef HEADER_XBOX360_HPP
|
#ifndef HEADER_XBOX360_HPP
|
||||||
#define HEADER_XBOX360_HPP
|
#define HEADER_XBOX360_HPP
|
||||||
|
|
||||||
enum GamepadType {
|
#include "xboxmsg.hpp"
|
||||||
GAMEPAD_UNKNOWN,
|
|
||||||
GAMEPAD_XBOX,
|
|
||||||
GAMEPAD_XBOX_MAT,
|
|
||||||
GAMEPAD_XBOX360,
|
|
||||||
GAMEPAD_XBOX360_WIRELESS,
|
|
||||||
GAMEPAD_XBOX360_GUITAR
|
|
||||||
};
|
|
||||||
|
|
||||||
struct XPadDevice {
|
struct XPadDevice {
|
||||||
GamepadType type;
|
GamepadType type;
|
||||||
|
|
16
xboxmsg.cpp
16
xboxmsg.cpp
|
@ -20,6 +20,22 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include "xboxmsg.hpp"
|
#include "xboxmsg.hpp"
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& out, const XboxGenericMsg& msg)
|
||||||
|
{
|
||||||
|
if (msg.type == GAMEPAD_XBOX)
|
||||||
|
return out << msg.xbox;
|
||||||
|
else if (msg.type == GAMEPAD_XBOX_MAT)
|
||||||
|
return out << msg.xbox;
|
||||||
|
else if (msg.type == GAMEPAD_XBOX360)
|
||||||
|
return out << msg.xbox360;
|
||||||
|
else if (msg.type == GAMEPAD_XBOX360_WIRELESS)
|
||||||
|
return out << msg.xbox360;
|
||||||
|
else if (msg.type == GAMEPAD_XBOX360_GUITAR)
|
||||||
|
return out << msg.guitar;
|
||||||
|
else
|
||||||
|
return out << "Error: Unhandled XboxGenericMsg type: " << msg.type;
|
||||||
|
}
|
||||||
|
|
||||||
std::ostream& operator<<(std::ostream& out, const Xbox360GuitarMsg& msg)
|
std::ostream& operator<<(std::ostream& out, const Xbox360GuitarMsg& msg)
|
||||||
{
|
{
|
||||||
out << boost::format(" whammy:%6d tilt:%6d | up:%d down:%d left:%d right:%d | back:%d guide:%d start:%d | green:%d red:%d yellow:%d blue:%d orange:%d ")
|
out << boost::format(" whammy:%6d tilt:%6d | up:%d down:%d left:%d right:%d | back:%d guide:%d start:%d | green:%d red:%d yellow:%d blue:%d orange:%d ")
|
||||||
|
|
21
xboxmsg.hpp
21
xboxmsg.hpp
|
@ -21,6 +21,15 @@
|
||||||
|
|
||||||
#include <iosfwd>
|
#include <iosfwd>
|
||||||
|
|
||||||
|
enum GamepadType {
|
||||||
|
GAMEPAD_UNKNOWN,
|
||||||
|
GAMEPAD_XBOX,
|
||||||
|
GAMEPAD_XBOX_MAT,
|
||||||
|
GAMEPAD_XBOX360,
|
||||||
|
GAMEPAD_XBOX360_WIRELESS,
|
||||||
|
GAMEPAD_XBOX360_GUITAR
|
||||||
|
};
|
||||||
|
|
||||||
struct Xbox360Msg
|
struct Xbox360Msg
|
||||||
{
|
{
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
@ -150,10 +159,22 @@ struct XboxMsg
|
||||||
int x2 :16;
|
int x2 :16;
|
||||||
int y2 :16;
|
int y2 :16;
|
||||||
} __attribute__((__packed__));
|
} __attribute__((__packed__));
|
||||||
|
|
||||||
|
|
||||||
|
struct XboxGenericMsg
|
||||||
|
{
|
||||||
|
GamepadType type;
|
||||||
|
union {
|
||||||
|
struct Xbox360GuitarMsg guitar;
|
||||||
|
struct Xbox360Msg xbox360;
|
||||||
|
struct XboxMsg xbox;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
std::ostream& operator<<(std::ostream& out, const Xbox360GuitarMsg& msg);
|
std::ostream& operator<<(std::ostream& out, const Xbox360GuitarMsg& msg);
|
||||||
std::ostream& operator<<(std::ostream& out, const Xbox360Msg& msg);
|
std::ostream& operator<<(std::ostream& out, const Xbox360Msg& msg);
|
||||||
std::ostream& operator<<(std::ostream& out, const XboxMsg& msg);
|
std::ostream& operator<<(std::ostream& out, const XboxMsg& msg);
|
||||||
|
std::ostream& operator<<(std::ostream& out, const XboxGenericMsg& msg);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue