/* * Copyright 2015-2016 Cel Skeggs * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * The CCRE 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 Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.frc; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Properties; import java.util.WeakHashMap; import ccre.channel.EventInput; import ccre.channel.EventOutput; import ccre.ctrl.AbstractJoystick; import ccre.ctrl.Joystick; import ccre.log.Logger; import net.java.games.input.Component; import net.java.games.input.Controller; import net.java.games.input.ControllerEnvironment; /** * Uses JInput to allow the Emulator to work with physical joysticks. * * @author skeggsc */ public class JoystickHandler { static { File f; try { f = File.createTempFile("joystick-binaries", ""); if (!f.delete()) { Logger.warning("Could not delete JInput temporary file."); } else if (!f.mkdir()) { Logger.warning("Could not create JInput temporary directory."); } else { f.deleteOnExit(); Logger.info("Putting binaries in: " + f); Properties props = new Properties(); try (InputStream resource = JoystickHandler.class.getResourceAsStream("/natives.properties")) { props.load(resource); } Object str = props.get("natives"); if (!(str instanceof String)) { throw new IOException("Bad type for natives field: " + str); } String[] natives = ((String) str).split(";"); for (String s : natives) { File out = new File(f, s); out.deleteOnExit(); InputStream inp = JoystickHandler.class.getResourceAsStream("/" + s); if (inp == null) { throw new IOException("Could not find resource: /" + s); } try { OutputStream outp = new FileOutputStream(out); try { byte[] data = new byte[4096]; while (true) { int len = inp.read(data); if (len == -1) { break; } outp.write(data, 0, len); } } finally { outp.close(); } } finally { inp.close(); } } System.setProperty("net.java.games.input.librarypath", f.getAbsolutePath()); } } catch (IOException e) { Logger.warning("Could not unpack binaries for JInput", e); } ControllerEnvironment.getDefaultEnvironment().getControllers(); } /** * A holder for physical Joysticks that translates the active Joystick into * a single interface. * * @author skeggsc */ public static class ExternalJoystickHolder { private JoystickWrapper ctrl; /** * Check if a Joystick is currently associated. * * @return if any Joystick is associated. */ public boolean hasJoystick() { return ctrl != null; } /** * Associate a new Joystick. * * @param wrapper the Joystick to associate. */ public void setJoystick(JoystickWrapper wrapper) { this.ctrl = wrapper; } /** * Create an interface to allow access to the currently associated * Joystick. * * This method does NOT need to be called again when the associated * Joystick changes. * * @param check when to update the Joystick. * @return the Joystick interface. */ public Joystick getJoystick(EventInput check) { check.send(new EventOutput() { @Override public void event() { if (ctrl != null) { ctrl.ctrl.poll(); } } }); return new AbstractJoystick(check, 12, 32) { @Override protected boolean getButton(int btn) { if (ctrl == null) { return false; } ArrayList<Component> buttons = ctrl.buttons; if (btn < 1 || btn > buttons.size()) { return false; } return buttons.get(btn - 1).getPollData() > 0.5f; } @Override protected float getAxis(int axis) { if (ctrl == null) { return 0.0f; } ArrayList<Component> axes = ctrl.axes; if (ctrl.isXBox()) { // Split axis 3 into axes 3 and 4. if (axis >= 5 && axis <= 6) { return axes.get(axis - 2).getPollData(); } else if (axis == 3) { float raw = axes.get(2).getPollData(); return raw > 0 ? raw : 0; } else if (axis == 4) { float raw = axes.get(2).getPollData(); return raw < 0 ? -raw : 0; } } if (axis < 1 || axis > axes.size()) { return 0.0f; } return axes.get(axis - 1).getPollData(); } @Override protected boolean getPOV(int direction) { if (ctrl == null || ctrl.pov == null) { return false; } int angle = (int) ((ctrl.pov.getPollData() * 360 + 270) % 360); return direction == angle; } @Override protected void setRumble(float left, float right) { // TODO: rumble in emulator } }; } } /** * A wrapped physical Joystick that has organized buttons and axes. * * @author skeggsc */ public static class JoystickWrapper { private final Controller ctrl; private final ArrayList<Component> buttons = new ArrayList<Component>(); private final ArrayList<Component> axes = new ArrayList<Component>(); private Component pov; private JoystickWrapper(Controller ctrl) { this.ctrl = ctrl; } private boolean isXBox() { return ctrl.getName().contains("XBOX 360 For Windows") && axes.size() == 5; } @Override public String toString() { return ctrl + " on " + ctrl.getPortType() + ":" + ctrl.getPortNumber(); } private void start() { Logger.info("Started: " + ctrl + ": " + ctrl.getType()); axes.clear(); buttons.clear(); axes.add(null); axes.add(null); axes.add(null); axes.add(null); axes.add(null); for (Component comp : ctrl.getComponents()) { Logger.info("Component: " + comp); if (comp.getIdentifier() instanceof Component.Identifier.Button) { buttons.add(comp); } else if (comp.getIdentifier() instanceof Component.Identifier.Axis) { if (comp.getIdentifier() == Component.Identifier.Axis.X) { axes.set(0, comp); } else if (comp.getIdentifier() == Component.Identifier.Axis.Y) { axes.set(1, comp); } else if (comp.getIdentifier() == Component.Identifier.Axis.Z) { axes.set(2, comp); } else if (comp.getIdentifier() == Component.Identifier.Axis.RX) { axes.set(3, comp); } else if (comp.getIdentifier() == Component.Identifier.Axis.RY) { axes.set(4, comp); } else if (comp.getIdentifier() == Component.Identifier.Axis.POV) { pov = comp; } else { axes.add(comp); } } } while (axes.contains(null)) { axes.remove(null); } Logger.info("B/A/P: " + buttons + "/" + axes + "/" + pov); if (isXBox()) { Logger.info("This is a 5-axis XBOX controller, which means that it's not going to show up the same as on the real robot."); Logger.info("To resolve this, the emulator will remap the trigger axis into the two separate axes - but it won't work exactly the same."); Logger.info("Notably, unlike the real robot, we don't know the difference between pressing both axes and pressing neither axes."); Logger.info("So, it will think that neither are pressed in this scenario."); } } } private final WeakHashMap<Controller, JoystickWrapper> wrappings = new WeakHashMap<Controller, JoystickWrapper>(); private boolean isIgnored(Controller ctrl) { Controller.Type t = ctrl.getType(); return t == Controller.Type.MOUSE || t == Controller.Type.KEYBOARD || t == Controller.Type.UNKNOWN; } /** * Get a Joystick that currently has a button held down. * * @return the detected Joystick, or null if none are found. */ public JoystickWrapper getActivelyPressedJoystick() { for (Controller ctrl : ControllerEnvironment.getDefaultEnvironment().getControllers()) { if (isIgnored(ctrl)) { continue; } ctrl.poll(); for (Component comp : ctrl.getComponents()) { if (comp.getIdentifier() instanceof Component.Identifier.Button) { if (comp.getPollData() > 0.5f) { return getOrWrap(ctrl); } } } } return null; } private JoystickWrapper getOrWrap(Controller ctrl) { JoystickWrapper w = wrappings.get(ctrl); if (w == null) { w = new JoystickWrapper(ctrl); wrappings.put(ctrl, w); w.start(); } return w; } }