Add support for wired Xbox 360 controllers (pending testing)

This commit is contained in:
Cameron Gutman 2016-01-27 13:45:04 -05:00
parent ceef4510fb
commit 23c54f6813
5 changed files with 379 additions and 165 deletions

View File

@ -0,0 +1,55 @@
package com.limelight.binding.input.driver;
import android.hardware.usb.UsbEndpoint;
import com.limelight.LimeLog;
import com.limelight.binding.video.MediaCodecHelper;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public abstract class AbstractController {
private final int deviceId;
private UsbDriverListener listener;
protected short buttonFlags;
protected float leftTrigger, rightTrigger;
protected float rightStickX, rightStickY;
protected float leftStickX, leftStickY;
public int getControllerId() {
return deviceId;
}
protected void setButtonFlag(int buttonFlag, int data) {
if (data != 0) {
buttonFlags |= buttonFlag;
}
else {
buttonFlags &= ~buttonFlag;
}
}
protected void reportInput() {
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
rightStickX, rightStickY, leftTrigger, rightTrigger);
}
public abstract boolean start();
public abstract void stop();
public AbstractController(int deviceId, UsbDriverListener listener) {
this.deviceId = deviceId;
this.listener = listener;
}
protected void notifyDeviceRemoved() {
listener.deviceRemoved(deviceId);
}
protected void notifyDeviceAdded() {
listener.deviceAdded(deviceId);
}
}

View File

@ -0,0 +1,149 @@
package com.limelight.binding.input.driver;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import com.limelight.LimeLog;
import com.limelight.binding.video.MediaCodecHelper;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public abstract class AbstractXboxController extends AbstractController {
protected final UsbDevice device;
protected final UsbDeviceConnection connection;
private Thread inputThread;
private boolean stopped;
protected UsbEndpoint inEndpt, outEndpt;
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(deviceId, listener);
this.device = device;
this.connection = connection;
}
private Thread createInputThread() {
return new Thread() {
public void run() {
while (!isInterrupted() && !stopped) {
byte[] buffer = new byte[64];
int res;
//
// There's no way that I can tell to determine if a device has failed
// or if the timeout has simply expired. We'll check how long the transfer
// took to fail and assume the device failed if it happened before the timeout
// expired.
//
do {
// Read the next input state packet
long lastMillis = MediaCodecHelper.getMonotonicMillis();
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
// If we get a zero length response, treat it as an error
if (res == 0) {
res = -1;
}
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
LimeLog.warning("Detected device I/O error");
AbstractXboxController.this.stop();
break;
}
} while (res == -1 && !isInterrupted() && !stopped);
if (res == -1 || stopped) {
break;
}
if (handleRead(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN))) {
// Report input if handleRead() returns true
reportInput();
}
}
}
};
}
public boolean start() {
// Force claim all interfaces
for (int i = 0; i < device.getInterfaceCount(); i++) {
UsbInterface iface = device.getInterface(i);
if (!connection.claimInterface(iface, true)) {
LimeLog.warning("Failed to claim interfaces");
return false;
}
}
// Find the endpoints
UsbInterface iface = device.getInterface(0);
for (int i = 0; i < iface.getEndpointCount(); i++) {
UsbEndpoint endpt = iface.getEndpoint(i);
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
if (inEndpt != null) {
LimeLog.warning("Found duplicate IN endpoint");
return false;
}
inEndpt = endpt;
}
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
if (outEndpt != null) {
LimeLog.warning("Found duplicate OUT endpoint");
return false;
}
outEndpt = endpt;
}
}
// Make sure the required endpoints were present
if (inEndpt == null || outEndpt == null) {
LimeLog.warning("Missing required endpoint");
return false;
}
// Run the init function
if (!doInit()) {
return false;
}
// Start listening for controller input
inputThread = createInputThread();
inputThread.start();
// Now report we're added
notifyDeviceAdded();
return true;
}
public void stop() {
if (stopped) {
return;
}
stopped = true;
// Stop the input thread
if (inputThread != null) {
inputThread.interrupt();
inputThread = null;
}
// Report the device removed
notifyDeviceRemoved();
// Close the USB connection
connection.close();
}
protected abstract boolean handleRead(ByteBuffer buffer);
protected abstract boolean doInit();
}

View File

@ -26,7 +26,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
private final UsbEventReceiver receiver = new UsbEventReceiver();
private final UsbDriverBinder binder = new UsbDriverBinder();
private final ArrayList<XboxOneController> controllers = new ArrayList<>();
private final ArrayList<AbstractController> controllers = new ArrayList<>();
private UsbDriverListener listener;
private static int nextDeviceId;
@ -42,7 +42,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
@Override
public void deviceRemoved(int controllerId) {
// Remove the the controller from our list (if not removed already)
for (XboxOneController controller : controllers) {
for (AbstractController controller : controllers) {
if (controller.getControllerId() == controllerId) {
controllers.remove(controller);
break;
@ -93,7 +93,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
// Report all controllerMap that already exist
if (listener != null) {
for (XboxOneController controller : controllers) {
for (AbstractController controller : controllers) {
listener.deviceAdded(controller.getControllerId());
}
}
@ -117,8 +117,21 @@ public class UsbDriverService extends Service implements UsbDriverListener {
return;
}
// Try to initialize it
XboxOneController controller = new XboxOneController(device, connection, nextDeviceId++, this);
AbstractController controller;
if (XboxOneController.canClaimDevice(device)) {
controller = new XboxOneController(device, connection, nextDeviceId++, this);
}
else if (Xbox360Controller.canClaimDevice(device)) {
controller = new Xbox360Controller(device, connection, nextDeviceId++, this);
}
else {
// Unreachable
return;
}
// Start the controller
if (!controller.start()) {
connection.close();
return;
@ -141,7 +154,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
// Enumerate existing devices
for (UsbDevice dev : usbManager.getDeviceList().values()) {
if (XboxOneController.canClaimDevice(dev)) {
if (XboxOneController.canClaimDevice(dev) || Xbox360Controller.canClaimDevice(dev)) {
// Start the process of claiming this device
handleUsbDeviceState(dev);
}

View File

@ -0,0 +1,137 @@
package com.limelight.binding.input.driver;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import com.limelight.LimeLog;
import com.limelight.nvstream.input.ControllerPacket;
import java.nio.ByteBuffer;
public class Xbox360Controller extends AbstractXboxController {
// This list is taken from the Xpad driver in the Linux kernel.
// I've excluded the devices that aren't "controllers" in the traditional sense, but
// if people really want to use their dancepads or fight sticks with Moonlight, I can
// put them in.
private static final DeviceIdTuple[] supportedDeviceTuples = {
new DeviceIdTuple(0x045e, 0x028e, "Microsoft X-Box 360 pad"),
new DeviceIdTuple(0x044f, 0xb326, "Thrustmaster Gamepad GP XID"),
new DeviceIdTuple(0x046d, 0xc21d, "Logitech Gamepad F310"),
new DeviceIdTuple(0x046d, 0xc21e, "Logitech Gamepad F510"),
new DeviceIdTuple(0x046d, 0xc21f, "Logitech Gamepad F710"),
new DeviceIdTuple(0x046d, 0xc242, "Logitech Chillstream Controller"),
new DeviceIdTuple(0x0738, 0x4716, "Mad Catz Wired Xbox 360 Controller"),
new DeviceIdTuple(0x0738, 0x4726, "Mad Catz Xbox 360 Controller"),
new DeviceIdTuple(0x0738, 0xb726, "Mad Catz Xbox controller - MW2"),
new DeviceIdTuple(0x0738, 0xbeef, "Mad Catz JOYTECH NEO SE Advanced GamePad"),
new DeviceIdTuple(0x0738, 0xcb02, "Saitek Cyborg Rumble Pad - PC/Xbox 360"),
new DeviceIdTuple(0x0738, 0xcb03, "Saitek P3200 Rumble Pad - PC/Xbox 360"),
new DeviceIdTuple(0x0e6f, 0x0113, "Afterglow AX.1 Gamepad for Xbox 360"),
new DeviceIdTuple(0x0e6f, 0x0201, "Pelican PL-3601 'TSZ' Wired Xbox 360 Controller"),
new DeviceIdTuple(0x0e6f, 0x0213, "Afterglow Gamepad for Xbox 360"),
new DeviceIdTuple(0x0e6f, 0x021f, "Rock Candy Gamepad for Xbox 360"),
new DeviceIdTuple(0x0e6f, 0x0301, "Logic3 Controller"),
new DeviceIdTuple(0x0e6f, 0x0401, "Logic3 Controller"),
new DeviceIdTuple(0x12ab, 0x0301, "PDP AFTERGLOW AX.1"),
new DeviceIdTuple(0x146b, 0x0601, "BigBen Interactive XBOX 360 Controller"),
new DeviceIdTuple(0x1532, 0x0037, "Razer Sabertooth"),
new DeviceIdTuple(0x15e4, 0x3f00, "Power A Mini Pro Elite"),
new DeviceIdTuple(0x15e4, 0x3f0a, "Xbox Airflo wired controller"),
new DeviceIdTuple(0x15e4, 0x3f10, "Batarang Xbox 360 controller"),
new DeviceIdTuple(0x162e, 0xbeef, "Joytech Neo-Se Take2"),
new DeviceIdTuple(0x1689, 0xfd00, "Razer Onza Tournament Edition"),
new DeviceIdTuple(0x1689, 0xfd01, "Razer Onza Classic Edition"),
new DeviceIdTuple(0x24c6, 0x5d04, "Razer Sabertooth"),
new DeviceIdTuple(0x1bad, 0xf016, "Mad Catz Xbox 360 Controller"),
new DeviceIdTuple(0x1bad, 0xf023, "MLG Pro Circuit Controller (Xbox)"),
new DeviceIdTuple(0x1bad, 0xf900, "Harmonix Xbox 360 Controller"),
new DeviceIdTuple(0x1bad, 0xf901, "Gamestop Xbox 360 Controller"),
new DeviceIdTuple(0x1bad, 0xf903, "Tron Xbox 360 controller"),
new DeviceIdTuple(0x24c6, 0x5300, "PowerA MINI PROEX Controller"),
new DeviceIdTuple(0x24c6, 0x5303, "Xbox Airflo wired controller"),
};
public static boolean canClaimDevice(UsbDevice device) {
for (DeviceIdTuple tuple : supportedDeviceTuples) {
if (device.getVendorId() == tuple.vid && device.getProductId() == tuple.pid) {
LimeLog.info("XB360 can claim device: " + tuple.name);
return true;
}
}
return false;
}
public Xbox360Controller(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(device, connection, deviceId, listener);
}
@Override
protected boolean handleRead(ByteBuffer buffer) {
// Skip first byte
buffer.position(buffer.position()+1);
// DPAD
byte b = buffer.get();
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
// Start/Select
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x20);
// LS/RS
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
// ABXY buttons
b = buffer.get();
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
// LB/RB
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x01);
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x02);
// Xbox button
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, b & 0x04);
// Triggers
leftTrigger = buffer.get() / 255.0f;
rightTrigger = buffer.get() / 255.0f;
// Left stick
leftStickX = buffer.getShort() / 32767.0f;
leftStickY = ~buffer.getShort() / 32767.0f;
// Right stick
rightStickX = buffer.getShort() / 32767.0f;
rightStickY = ~buffer.getShort() / 32767.0f;
// Return true to send input
return true;
}
@Override
protected boolean doInit() {
// Xbox 360 wired controller requires no initialization
return true;
}
private static class DeviceIdTuple {
public final int vid;
public final int pid;
public final String name;
public DeviceIdTuple(int vid, int pid, String name) {
this.vid = vid;
this.pid = pid;
this.name = name;
}
}
}

View File

@ -13,19 +13,7 @@ import com.limelight.nvstream.input.ControllerPacket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class XboxOneController {
private final UsbDevice device;
private final UsbDeviceConnection connection;
private final int deviceId;
private Thread inputThread;
private UsbDriverListener listener;
private boolean stopped;
private short buttonFlags;
private float leftTrigger, rightTrigger;
private float rightStickX, rightStickY;
private float leftStickX, leftStickY;
public class XboxOneController extends AbstractXboxController {
private static final int MICROSOFT_VID = 0x045e;
private static final int XB1_IFACE_SUBCLASS = 71;
@ -35,28 +23,7 @@ public class XboxOneController {
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
this.device = device;
this.connection = connection;
this.deviceId = deviceId;
this.listener = listener;
}
public int getControllerId() {
return this.deviceId;
}
private void setButtonFlag(int buttonFlag, int data) {
if (data != 0) {
buttonFlags |= buttonFlag;
}
else {
buttonFlags &= ~buttonFlag;
}
}
private void reportInput() {
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
rightStickX, rightStickY, leftTrigger, rightTrigger);
super(device, connection, deviceId, listener);
}
private void processButtons(ByteBuffer buffer) {
@ -90,143 +57,24 @@ public class XboxOneController {
rightStickX = buffer.getShort() / 32767.0f;
rightStickY = ~buffer.getShort() / 32767.0f;
reportInput();
}
private void processPacket(ByteBuffer buffer) {
@Override
protected boolean handleRead(ByteBuffer buffer) {
switch (buffer.get())
{
case 0x20:
buffer.position(buffer.position()+3);
processButtons(buffer);
break;
return true;
case 0x07:
buffer.position(buffer.position() + 3);
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
reportInput();
break;
}
}
private void startInputThread(final UsbEndpoint inEndpt) {
inputThread = new Thread() {
public void run() {
while (!isInterrupted() && !stopped) {
byte[] buffer = new byte[64];
int res;
//
// There's no way that I can tell to determine if a device has failed
// or if the timeout has simply expired. We'll check how long the transfer
// took to fail and assume the device failed if it happened before the timeout
// expired.
//
do {
// Read the next input state packet
long lastMillis = MediaCodecHelper.getMonotonicMillis();
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
// If we get a zero length response, treat it as an error
if (res == 0) {
res = -1;
}
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
LimeLog.warning("Detected device I/O error");
XboxOneController.this.stop();
break;
}
} while (res == -1 && !isInterrupted() && !stopped);
if (res == -1 || stopped) {
break;
}
processPacket(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN));
}
}
};
inputThread.setName("Xbox One Controller - Input Thread");
inputThread.start();
}
public boolean start() {
// Force claim all interfaces
for (int i = 0; i < device.getInterfaceCount(); i++) {
UsbInterface iface = device.getInterface(i);
if (!connection.claimInterface(iface, true)) {
LimeLog.warning("Failed to claim interfaces");
return false;
}
return true;
}
// Find the endpoints
UsbEndpoint outEndpt = null;
UsbEndpoint inEndpt = null;
UsbInterface iface = device.getInterface(0);
for (int i = 0; i < iface.getEndpointCount(); i++) {
UsbEndpoint endpt = iface.getEndpoint(i);
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
if (inEndpt != null) {
LimeLog.warning("Found duplicate IN endpoint");
return false;
}
inEndpt = endpt;
}
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
if (outEndpt != null) {
LimeLog.warning("Found duplicate OUT endpoint");
return false;
}
outEndpt = endpt;
}
}
// Make sure the required endpoints were present
if (inEndpt == null || outEndpt == null) {
LimeLog.warning("Missing required endpoint");
return false;
}
// Send the initialization packet
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
if (res != XB1_INIT_DATA.length) {
LimeLog.warning("Initialization transfer failed: "+res);
return false;
}
// Start listening for controller input
startInputThread(inEndpt);
// Report this device added via the listener
listener.deviceAdded(deviceId);
return true;
}
public void stop() {
if (stopped) {
return;
}
stopped = true;
// Stop the input thread
if (inputThread != null) {
inputThread.interrupt();
inputThread = null;
}
// Report the device removed
listener.deviceRemoved(deviceId);
// Close the USB connection
connection.close();
return false;
}
public static boolean canClaimDevice(UsbDevice device) {
@ -236,4 +84,16 @@ public class XboxOneController {
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL;
}
@Override
protected boolean doInit() {
// Send the initialization packet
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
if (res != XB1_INIT_DATA.length) {
LimeLog.warning("Initialization transfer failed: "+res);
return false;
}
return true;
}
}