diff --git a/app/src/main/java/com/limelight/binding/input/driver/AbstractController.java b/app/src/main/java/com/limelight/binding/input/driver/AbstractController.java new file mode 100644 index 00000000..12fffdc3 --- /dev/null +++ b/app/src/main/java/com/limelight/binding/input/driver/AbstractController.java @@ -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); + } +} diff --git a/app/src/main/java/com/limelight/binding/input/driver/AbstractXboxController.java b/app/src/main/java/com/limelight/binding/input/driver/AbstractXboxController.java new file mode 100644 index 00000000..c6c880d3 --- /dev/null +++ b/app/src/main/java/com/limelight/binding/input/driver/AbstractXboxController.java @@ -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(); +} diff --git a/app/src/main/java/com/limelight/binding/input/driver/UsbDriverService.java b/app/src/main/java/com/limelight/binding/input/driver/UsbDriverService.java index 4c020aed..dc8c071b 100644 --- a/app/src/main/java/com/limelight/binding/input/driver/UsbDriverService.java +++ b/app/src/main/java/com/limelight/binding/input/driver/UsbDriverService.java @@ -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 controllers = new ArrayList<>(); + private final ArrayList 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); } diff --git a/app/src/main/java/com/limelight/binding/input/driver/Xbox360Controller.java b/app/src/main/java/com/limelight/binding/input/driver/Xbox360Controller.java new file mode 100644 index 00000000..98dfb8e7 --- /dev/null +++ b/app/src/main/java/com/limelight/binding/input/driver/Xbox360Controller.java @@ -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; + } + } +} diff --git a/app/src/main/java/com/limelight/binding/input/driver/XboxOneController.java b/app/src/main/java/com/limelight/binding/input/driver/XboxOneController.java index 96ea0c0b..284e2e8a 100644 --- a/app/src/main/java/com/limelight/binding/input/driver/XboxOneController.java +++ b/app/src/main/java/com/limelight/binding/input/driver/XboxOneController.java @@ -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; + } }