diff --git a/src/com/limelight/Limelight.java b/src/com/limelight/Limelight.java index b574b1a..c7e5271 100644 --- a/src/com/limelight/Limelight.java +++ b/src/com/limelight/Limelight.java @@ -4,6 +4,7 @@ import com.limelight.binding.PlatformBinding; import com.limelight.binding.audio.FakeAudioRenderer; import com.limelight.binding.video.FakeVideoRenderer; import com.limelight.input.EvdevLoader; +import com.limelight.input.GamepadMapper; import com.limelight.input.GamepadMapping; import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.NvConnectionListener; @@ -183,6 +184,7 @@ public class Limelight implements NvConnectionListener { String audio = "sysdefault"; String video = null; String action = null; + String out= null; Level debug = Level.SEVERE; for (int i = 0; i < args.length; i++) { @@ -288,11 +290,13 @@ public class Limelight implements NvConnectionListener { parse = false; } else if (action == null) { action = args[i].toLowerCase(); - if (!action.equals("stream") && !action.equals("pair") && !action.equals("fake") && !action.equals("help") && !action.equals("discover") && !action.equals("list")) { + if (!action.equals("stream") && !action.equals("pair") && !action.equals("fake") && !action.equals("help") && !action.equals("discover") && !action.equals("list") && !action.equals("map")) { System.out.println("Syntax error: invalid action specified"); System.exit(3); } - } else if (host == null) { + } else if (action.equals("map") && out == null) { + out = args[i]; + } else if (!action.equals("map") && host == null) { try { host = InetAddress.getByName(args[i]); } catch (UnknownHostException ex) { @@ -308,20 +312,44 @@ public class Limelight implements NvConnectionListener { if (action == null) { System.out.println("Syntax Error: Missing required action argument"); parse = false; + } else if (action.equals("map")) { + if (inputs.size() != 1) { + System.out.println("Syntax error: specify one -input"); + parse = false; + } else if (out == null) { + System.out.println("Syntax error: specify the output file"); + parse = false; + } + + if (parse) { + try { + GamepadMapper mapper = new GamepadMapper(inputs.get(0)); + mapper.setup(); + mapper.save(new File(out)); + } catch (IOException | InterruptedException ex) { + System.err.println(ex.getMessage()); + } + return; + } } else if (action.equals("help")) parse = false; if (args.length == 0 || !parse) { - System.out.println("Usage: java -jar limelight-pi.jar [options] host"); + System.out.println("Usage: java -jar limelight-pi.jar action [options] host/file"); System.out.println(); System.out.println(" Actions:"); System.out.println(); + System.out.println("\tmap\t\t\tCreate mapping file for gamepad"); System.out.println("\tpair\t\t\tPair device with computer"); System.out.println("\tstream\t\t\tStream computer to device"); System.out.println("\tdiscover\t\tList available computers"); System.out.println("\tlist\t\t\tList available games and applications"); System.out.println("\thelp\t\t\tShow this help"); System.out.println(); + System.out.println(" Mapping options:"); + System.out.println(); + System.out.println("\t-input \t\tUse as input"); + System.out.println(); System.out.println(" Streaming options:"); System.out.println(); System.out.println("\t-720\t\t\tUse 1280x720 resolution [default]"); diff --git a/src/com/limelight/input/EvdevHandler.java b/src/com/limelight/input/EvdevHandler.java index c4ff9d4..3b3cc8f 100644 --- a/src/com/limelight/input/EvdevHandler.java +++ b/src/com/limelight/input/EvdevHandler.java @@ -1,23 +1,18 @@ package com.limelight.input; -import com.limelight.LimeLog; import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.input.ControllerPacket; import com.limelight.nvstream.input.KeyboardPacket; import com.limelight.nvstream.input.MouseButtonPacket; -import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.channels.FileChannel; /** * Class that handles keyboard input using the Evdev interface * @author Iwan Timmer */ -public class EvdevHandler implements Runnable { +public class EvdevHandler extends EvdevReader { private static KeyboardTranslator translator; @@ -32,24 +27,12 @@ public class EvdevHandler implements Runnable { private EvdevAbsolute absLX, absLY, absRX, absRY, absLT, absRT, absDX, absDY; private NvConnection conn; - private FileChannel deviceInput; - private ByteBuffer inputBuffer; - private GamepadMapping mapping; public EvdevHandler(NvConnection conn, String device, GamepadMapping mapping) throws FileNotFoundException, IOException { + super(device); this.conn = conn; this.mapping = mapping; - File file = new File(device); - if (!file.exists()) - throw new FileNotFoundException("File " + device + " not found"); - if (!file.canRead()) - throw new IOException("Can't read from " + device); - - FileInputStream in = new FileInputStream(file); - deviceInput = in.getChannel(); - inputBuffer = ByteBuffer.allocate(EvdevConstants.MAX_STRUCT_SIZE_BYTES); - inputBuffer.order(ByteOrder.nativeOrder()); absLX = new EvdevAbsolute(device, mapping.abs_x, mapping.reverse_x); absLY = new EvdevAbsolute(device, mapping.abs_y, !mapping.reverse_y); @@ -62,15 +45,9 @@ public class EvdevHandler implements Runnable { translator = new KeyboardTranslator(conn); } - - public void start() { - Thread thread = new Thread(this); - thread.setDaemon(true); - thread.setName("Input - Receiver"); - thread.start(); - } - - private void parseEvent(ByteBuffer buffer) { + + @Override + protected void parseEvent(ByteBuffer buffer) { if (buffer.limit()==EvdevConstants.MAX_STRUCT_SIZE_BYTES) { long time_sec = buffer.getLong(); long time_usec = buffer.getLong(); @@ -214,21 +191,5 @@ public class EvdevHandler implements Runnable { return 0; } } - - @Override - public void run() { - try { - while (true) { - while(inputBuffer.remaining()==EvdevConstants.MAX_STRUCT_SIZE_BYTES) - deviceInput.read(inputBuffer); - - inputBuffer.flip(); - parseEvent(inputBuffer); - inputBuffer.clear(); - } - } catch (IOException e) { - LimeLog.warning("Input device removed"); - } - } } diff --git a/src/com/limelight/input/EvdevReader.java b/src/com/limelight/input/EvdevReader.java new file mode 100644 index 0000000..33cdc84 --- /dev/null +++ b/src/com/limelight/input/EvdevReader.java @@ -0,0 +1,55 @@ +package com.limelight.input; + +import com.limelight.LimeLog; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; + +public abstract class EvdevReader implements Runnable { + + private FileChannel deviceInput; + private ByteBuffer inputBuffer; + + public EvdevReader(String device) throws FileNotFoundException, IOException { + File file = new File(device); + if (!file.exists()) + throw new FileNotFoundException("File " + device + " not found"); + if (!file.canRead()) + throw new IOException("Can't read from " + device); + + FileInputStream in = new FileInputStream(file); + deviceInput = in.getChannel(); + inputBuffer = ByteBuffer.allocate(EvdevConstants.MAX_STRUCT_SIZE_BYTES); + inputBuffer.order(ByteOrder.nativeOrder()); + } + + public void start() { + Thread thread = new Thread(this); + thread.setDaemon(true); + thread.setName("Input - Receiver"); + thread.start(); + } + + protected abstract void parseEvent(ByteBuffer buffer); + + @Override + public void run() { + try { + while (true) { + while(inputBuffer.remaining()==EvdevConstants.MAX_STRUCT_SIZE_BYTES) + deviceInput.read(inputBuffer); + + inputBuffer.flip(); + parseEvent(inputBuffer); + inputBuffer.clear(); + } + } catch (IOException e) { + LimeLog.warning("Input device removed"); + } + } + +} diff --git a/src/com/limelight/input/GamepadMapper.java b/src/com/limelight/input/GamepadMapper.java new file mode 100644 index 0000000..976f252 --- /dev/null +++ b/src/com/limelight/input/GamepadMapper.java @@ -0,0 +1,137 @@ +package com.limelight.input; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class GamepadMapper extends EvdevReader { + + private Properties props; + private Map absolutes; + private String device; + + private String key; + private String abs; + private boolean dir; + + private short current; + private boolean reverse; + private boolean lastKey; + + public GamepadMapper(String device) throws FileNotFoundException, IOException { + super(device); + this.device = device; + props = new Properties(); + absolutes = new HashMap<>(); + current = -1; + } + + @Override + protected synchronized void parseEvent(ByteBuffer buffer) { + if (buffer.limit()==EvdevConstants.MAX_STRUCT_SIZE_BYTES) { + long time_sec = buffer.getLong(); + long time_usec = buffer.getLong(); + } else { + int time_sec = buffer.getInt(); + int time_usec = buffer.getInt(); + } + short type = buffer.getShort(); + short code = buffer.getShort(); + int value = buffer.getInt(); + + boolean set = false; + if (key != null && type == EvdevConstants.EV_KEY && value == EvdevConstants.KEY_RELEASED) { + props.put("btn_"+key, Short.toString(code)); + lastKey = true; + set = true; + } else if (abs != null && type == EvdevConstants.EV_ABS) { + if (!absolutes.containsKey(code)) + absolutes.put(code, new EvdevAbsolute(device, code, false)); + + int val = absolutes.get(code).getShort(value); + if (val > Short.MAX_VALUE * 0.75) { + current = code; + reverse = false; + } else if (dir && val < Short.MIN_VALUE * 0.75) { + current = code; + reverse = true; + } else if (current != -1 && code == current && val < Short.MAX_VALUE/4) { + props.put("abs_"+abs, Short.toString(code)); + props.put("revers_"+abs, Boolean.toString(reverse)); + lastKey = false; + set = true; + } + } + + if (set) { + key = null; + abs = null; + current = -1; + notify(); + } + } + + public synchronized void readKey(String key, String name) throws InterruptedException { + System.out.println(name); + this.key = key; + wait(); + } + + public synchronized void readAbs(String abs, String name) throws InterruptedException { + System.out.println(name); + this.abs = abs; + dir = true; + wait(); + } + + public synchronized void readAbsKey(String abs, String key, String name, boolean dir) throws InterruptedException { + System.out.println("Read " + name); + this.key = key; + this.abs = abs; + this.dir = dir; + wait(); + } + + public void setup() throws InterruptedException { + start(); + readAbs("x", "Left Stick Right"); + readAbs("y", "Left Stick Down"); + readKey("thumbl", "Left Stick Button"); + + readAbs("rx", "Right Stick Right"); + readAbs("ry", "Right Stick Down"); + readKey("thumbr", "Right Stick Button"); + + readAbsKey("dpad_x", "dpad_right", "D-Pad Right", true); + if (lastKey) + readKey("dpad_left", "D-Pad Left"); + + readAbsKey("dpad_y", "dpad_down", "D-Pad Down", true); + if (lastKey) + readKey("dpad_up", "D-Pad Up"); + + readKey("south", "Button 1 (A)"); + readKey("easth", "Button 2 (X)"); + readKey("north", "Button 3 (Y)"); + readKey("west", "Button 3 (B)"); + readKey("select", "Back Button"); + readKey("start", "Start Button"); + readKey("mode", "Special Button"); + + readAbsKey("z", "tl", "Left Trigger", false); + readAbsKey("rz", "tr", "Right Trigger", false); + + readKey("tl2", "Left Bumper"); + readKey("tr2", "Right Bumper"); + + } + + public void save(File file) throws FileNotFoundException, IOException { + props.store(new FileOutputStream(file), "Gamepad"); + } +}