From baedfb0ff1cba9bd9c48c044ee1d10f6238d413f Mon Sep 17 00:00:00 2001 From: cs-powell <142438185+cs-powell@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:10:46 -0400 Subject: [PATCH] switch between takeoff and cruise control modes --- Java/Examples/Model-1/.idea/compiler.xml | 22 + .../.idea/copyright/profiles_settings.xml | 3 + .../Examples/Model-1/.idea/libraries/Java.xml | 9 + Java/Examples/Model-1/.idea/misc.xml | 12 + Java/Examples/Model-1/.idea/modules.xml | 8 + Java/Examples/Model-1/.idea/vcs.xml | 6 + Java/Examples/Model-1/BasicOperation.iml | 12 + Java/Examples/Model-1/src/Main.java | 302 ++++++ Java/Examples/Model-1/src/XPlaneConnect.java | 954 ++++++++++++++++++ 9 files changed, 1328 insertions(+) create mode 100644 Java/Examples/Model-1/.idea/compiler.xml create mode 100644 Java/Examples/Model-1/.idea/copyright/profiles_settings.xml create mode 100644 Java/Examples/Model-1/.idea/libraries/Java.xml create mode 100644 Java/Examples/Model-1/.idea/misc.xml create mode 100644 Java/Examples/Model-1/.idea/modules.xml create mode 100644 Java/Examples/Model-1/.idea/vcs.xml create mode 100644 Java/Examples/Model-1/BasicOperation.iml create mode 100644 Java/Examples/Model-1/src/Main.java create mode 100644 Java/Examples/Model-1/src/XPlaneConnect.java diff --git a/Java/Examples/Model-1/.idea/compiler.xml b/Java/Examples/Model-1/.idea/compiler.xml new file mode 100644 index 0000000..96cc43e --- /dev/null +++ b/Java/Examples/Model-1/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Java/Examples/Model-1/.idea/copyright/profiles_settings.xml b/Java/Examples/Model-1/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/Java/Examples/Model-1/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Java/Examples/Model-1/.idea/libraries/Java.xml b/Java/Examples/Model-1/.idea/libraries/Java.xml new file mode 100644 index 0000000..154988b --- /dev/null +++ b/Java/Examples/Model-1/.idea/libraries/Java.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Java/Examples/Model-1/.idea/misc.xml b/Java/Examples/Model-1/.idea/misc.xml new file mode 100644 index 0000000..5a65e9a --- /dev/null +++ b/Java/Examples/Model-1/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Java/Examples/Model-1/.idea/modules.xml b/Java/Examples/Model-1/.idea/modules.xml new file mode 100644 index 0000000..7843b1c --- /dev/null +++ b/Java/Examples/Model-1/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Java/Examples/Model-1/.idea/vcs.xml b/Java/Examples/Model-1/.idea/vcs.xml new file mode 100644 index 0000000..6564d52 --- /dev/null +++ b/Java/Examples/Model-1/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Java/Examples/Model-1/BasicOperation.iml b/Java/Examples/Model-1/BasicOperation.iml new file mode 100644 index 0000000..ab60ab0 --- /dev/null +++ b/Java/Examples/Model-1/BasicOperation.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Java/Examples/Model-1/src/Main.java b/Java/Examples/Model-1/src/Main.java new file mode 100644 index 0000000..628bf7e --- /dev/null +++ b/Java/Examples/Model-1/src/Main.java @@ -0,0 +1,302 @@ +package gov.nasa.xpc.ex; + +import gov.nasa.xpc.XPlaneConnect; +import java.io.IOException; +import java.net.SocketException; +import java.util.Arrays; + +/** + * An example program demonstrating the basic features of the X-Plane Connect toolbox. + * + * @author Jason Watkins + * @version 1.0 + * @since 2015-04-06 + */ +public class Main +{ + public static void main(String[] args) + { + System.out.println("X-Plane Connect example program"); + System.out.println("Setting up simulation..."); + try(XPlaneConnect xpc = new XPlaneConnect()) + { + // Ensure connection established. + xpc.getDREF("sim/test/test_float"); + + // System.out.println("Setting player aircraft position"); + // double[] posi = new double[] {37.524, -122.06899, 2500, 0, 0, 0, 1}; + // xpc.sendPOSI(posi); + + // System.out.println("Setting another aircraft position"); + // posi[0] = 37.52465; + // posi[4] = 20; + // xpc.sendPOSI(posi, 1); + + System.out.println("Setting rates"); + float[][] data = new float[3][9]; + for(float[] row : data) + { + Arrays.fill(row, -998); + } + data[0][0] = 0; //Alpha + data[0][1] = 0; + data[0][3] = 0; + + data[1][0] = 0; //Velocity + data[1][1] = 0; + data[1][2] = 0; + data[1][3] = 0; + data[1][4] = 0; + + data[2][0] = 0; //PQR + data[2][1] = 0; + data[2][2] = 0; + data[2][3] = 0; + + xpc.sendDATA(data); + + + + System.out.println("Trying something new!!"); + int aircraft = 0; + + + + // JFrame container = new JFrame(); + // Dimension size = new Dimension(500, 500); + // container.setPreferredSize(size); + // container.setVisible(true); + + + // JLabel text = new JLabel("Hello"); + // JLabel text2 = new JLabel("Hello"); + // JPanel display = new JPanel(); + // display.setLayout(new GridLayout()); + // display.setPreferredSize(size); + // display.add(text); + // display.add(text2); + + // container.add(display); + // container.pack(); + + boolean takeoff = true; + boolean climb = false; + boolean cruise = false; + boolean throttleFull = false; + boolean brakeOff = false; + boolean switchTrack = false; + + while(true) { + + //THE GETTERS + double[] posi1 = xpc.getPOSI(aircraft); // FIXME: change this to 64-bit double + float[] ctrl1 = xpc.getCTRL(aircraft); + + // String dref = "sim/cockpit2/gauges/indicators/airspeed_kts_copilot"; + String dref = "sim/cockpit2/gauges/indicators/airspeed_kts_pilot"; + String dref2 = "sim/flightmodel/position/true_phi"; + String drefHDG = "sim/cockpit2/gauges/indicators/heading_AHARS_deg_mag_pilot"; + String drefHDGBug = "sim/cockpit/autopilot/heading_mag"; + String drefAltitude = "sim/cockpit2/gauges/indicators/altitude_ft_pilot"; + + + + + + float[] value = xpc.getDREF(dref); + float[] value2 = xpc.getDREF(dref2); + float[] valueHDG = xpc.getDREF(drefHDG); + float[] valueHDGBug = xpc.getDREF(drefHDGBug); + float[] valueAltitude = xpc.getDREF(drefAltitude); + float bugged = 170; + float rwyHDG = valueHDGBug[0]; + // float[] value2 = xpc.getDREF(dref2); + // System.out.println(String.valueOf(value[0])); + + // System.out.format("Loc: (%4f, %4f, %4f) Aileron:%2f Elevator:%2f Rudder:%2f\n", + // posi1[0], posi1[1], posi1[2], ctrl1[1], ctrl1[0], ctrl1[2]); + + + // System.out.f ormat("\rControl Surfaces: [Elevator: %2f] [Roll: %2f] [Yaw: %2f] ||| Power: [Throttle:%2f] ||| Secondary Control Surfaces: [Flaps: %2f]", + // ctrl1[0], ctrl1[1], ctrl1[2], ctrl1[3], ctrl1[5]); + + System.out.format("\r[Elevator: %2f] [Roll: %2f] [Yaw: %2f] ---- [Throttle:%2f] ---- [Flaps: %2f] -- [Data Ref: %2f] -- [T/O: %b ][Cruise: %b ]", + ctrl1[0], ctrl1[1], ctrl1[2], ctrl1[3], ctrl1[5], valueAltitude[0], takeoff, cruise); + + // //Basic Autopilot For Roll (based on yoke position) + // float[] rollRight = {-998.0f, ctrl1[1]-(ctrl1[1]/2)}; + // float[] rollLeft = {-998.0f, ctrl1[1]-(ctrl1[1]/2)}; + // if(ctrl1[1] < 0) { + // xpc.sendCTRL(rollRight); + // } else if(ctrl1[1] > 0) { + // xpc.sendCTRL(rollLeft); + // } + + //Basic Autopilot For Pitch (based on VSI) + // float[] pitchUp = {ctrl1[0] + 0.01f}; + // float[] pitchDown = {ctrl1[0]- 0.01f}; + // if(value[0] < 0) { + // xpc.sendCTRL(pitchUp); + // } else if(value[0] > 0) { + // xpc.sendCTRL(pitchDown); + // } + if(valueAltitude[0] > 1000f && !switchTrack){ + System.out.println("In switch"); + takeoff = false; + cruise = true; + switchTrack = true; + } + + if(takeoff) { + float[] fullThrottle = {-998.0f, -998.0f, -998.0f, 1f}; + if(!throttleFull) { + xpc.sendCTRL(fullThrottle); + throttleFull = true; + } + + if(throttleFull && !brakeOff) { + String parkingBrake = "sim/cockpit2/controls/parking_brake_ratio"; + xpc.sendDREF(parkingBrake, 0f); + brakeOff = true; + } + + //Takeoff Pitch Control + float[] pitchUp = {ctrl1[0] + 0.01f}; + if(value[0] > bugged) { + if(ctrl1[0] < 0.1f){ + xpc.sendCTRL(pitchUp); + } + } + + //Takeoff Roll Control + float[] rollRight = {-998.0f, ctrl1[1] + 0.01f}; + float[] rollLeft = {-998.0f, ctrl1[1] - 0.01f}; + if(value2[0] < 0 && value[0] > bugged) { + if(ctrl1[1] < 0.15f) { + xpc.sendCTRL(rollRight); + } + + } else if(value2[0] > 0 && value[0] > bugged) { + if(ctrl1[1] > -0.15f){ + xpc.sendCTRL(rollLeft); + } + } + + //Takeoff Rudder ControlĀ  + float[] yawRight = {-998.0f, -998.0f, ctrl1[2] + 0.03f}; + float[] yawLeft = {-998.0f, -998.0f, ctrl1[2] - 0.03f}; + if(valueHDG[0] < rwyHDG && value[0] > 0) { + + if(ctrl1[2] < 0.5f) { + xpc.sendCTRL(yawRight); // YAW RIGHT + } + + } else if(valueHDG[0] > rwyHDG && value[0] > 0) { + if(ctrl1[2] > -0.5f){ + xpc.sendCTRL(yawLeft); // YAW LEFT + } + + } + } + + + if(cruise) { + //Basic Autopilot For Roll (based on bank angle) + float[] rollRight = {-998.0f, ctrl1[1] + 0.01f}; + float[] rollLeft = {-998.0f, ctrl1[1] - 0.01f}; + if(value2[0] < 0) { + + if(ctrl1[1] < 0.15f) { + xpc.sendCTRL(rollRight); + } + + } else if(value2[0] > 0) { + if(ctrl1[1] > -0.15f){ + xpc.sendCTRL(rollLeft); + } + } + + //Basic Autopilot For Pitch (based on speed) + float[] pitchUp = {ctrl1[0] + 0.01f}; + float[] pitchDown = {ctrl1[0]- 0.01f}; + + if(value[0] > bugged+25) { + if(ctrl1[0] < 0.2f) { + xpc.sendCTRL(pitchUp); + } + } else if(value[0] < bugged+25) { + if(ctrl1[0] > -0.2f) { + xpc.sendCTRL(pitchDown); + } + } + + } + + + + // text.setText(String.valueOf(value[0])); + // display.add(elevator); + // System.out.println(""); + try { + Thread.sleep(0); + } + catch (InterruptedException ex) {} + + if (System.in.available() > 0) { + break; + } + } + + + + + // System.out.println("Setting controls"); + // float[] ctrl = new float[4]; + // ctrl[3] = 0.8F; + // xpc.sendCTRL(ctrl); + + // System.out.println("Pausing sim"); + // xpc.pauseSim(true); + // try { Thread.sleep(5000); } catch (InterruptedException ex) {} + // System.out.println("Un-pausing"); + // xpc.pauseSim(false); + + + //Let sim run for 10 seconds + try { Thread.sleep(10000); } catch (InterruptedException ex) {} + + System.out.println("Stowing landing gear"); + xpc.sendDREF("sim/cockpit/switches/gear_handle_status", 1); + + //Let sim run for 10 seconds + try { Thread.sleep(10000); } catch (InterruptedException ex) {} + + System.out.println("Checking gear and sim status"); + String[] drefs = new String[] + { + "sim/cockpit/switches/gear_handle_status", + "sim/operation/override/override_planepath" + }; + float[][] results = xpc.getDREFs(drefs); + + if(results[0][0] == 1) + { + System.out.println("Gear stowed."); + } + else + { + System.out.println("Error stowing gear"); + } + System.out.println("Example complete"); + } + catch (SocketException ex) + { + System.out.println("Unable to set up the connection. (Error message was '" + ex.getMessage() + "'.)"); + } + catch (IOException ex) + { + System.out.println("Something went wrong with one of the commands. (Error message was '" + ex.getMessage() + "'.)"); + } + System.out.println("Exiting"); + } +} diff --git a/Java/Examples/Model-1/src/XPlaneConnect.java b/Java/Examples/Model-1/src/XPlaneConnect.java new file mode 100644 index 0000000..df32cff --- /dev/null +++ b/Java/Examples/Model-1/src/XPlaneConnect.java @@ -0,0 +1,954 @@ +//NOTICES: +// Copyright (c) 2013-2018 United States Government as represented by the Administrator of the +// National Aeronautics and Space Administration. All Rights Reserved. +// +// DISCLAIMERS +// No Warranty: THE SUBJECT SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY OF ANY KIND, +// EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE +// SUBJECT SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE, OR FREEDOM FROM INFRINGEMENT, ANY WARRANTY THAT THE SUBJECT +// SOFTWARE WILL BE ERROR FREE, OR ANY WARRANTY THAT DOCUMENTATION, IF PROVIDED, WILL CONFORM TO +// THE SUBJECT SOFTWARE. THIS AGREEMENT DOES NOT, IN ANY MANNER, CONSTITUTE AN ENDORSEMENT BY +// GOVERNMENT AGENCY OR ANY PRIOR RECIPIENT OF ANY RESULTS, RESULTING DESIGNS, HARDWARE, +// SOFTWARE PRODUCTS OR ANY OTHER APPLICATIONS RESULTING FROM USE OF THE SUBJECT SOFTWARE. +// FURTHER, GOVERNMENT AGENCY DISCLAIMS ALL WARRANTIES AND LIABILITIES REGARDING THIRD-PARTY +// SOFTWARE, IF PRESENT IN THE ORIGINAL SOFTWARE, AND DISTRIBUTES IT "AS IS." +// +// Waiver and Indemnity: RECIPIENT AGREES TO WAIVE ANY AND ALL CLAIMS AGAINST THE UNITED STATES +// GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT. IF +// RECIPIENT'S USE OF THE SUBJECT SOFTWARE RESULTS IN ANY LIABILITIES, DEMANDS, DAMAGES, +// EXPENSES OR LOSSES ARISING FROM SUCH USE, INCLUDING ANY DAMAGES FROM PRODUCTS BASED ON, OR +// RESULTING FROM, RECIPIENT'S USE OF THE SUBJECT SOFTWARE, RECIPIENT SHALL INDEMNIFY AND HOLD +// HARMLESS THE UNITED STATES GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY +// PRIOR RECIPIENT, TO THE EXTENT PERMITTED BY LAW. RECIPIENT'S SOLE REMEDY FOR ANY SUCH MATTER +// SHALL BE THE IMMEDIATE, UNILATERAL TERMINATION OF THIS AGREEMENT. + +package gov.nasa.xpc; + +import gov.nasa.xpc.discovery.Beacon; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.AutoCloseable; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Represents a client that can connect to and interact with the X-Plane Connect plugin. + * + * @author Jason Watkins + * @version 0.1 + * @since 2015-03-31 + */ +public class XPlaneConnect implements AutoCloseable +{ + private static int clientNum; + private DatagramSocket socket; + private InetAddress xplaneAddr; + private int xplanePort; + + /** + * Gets the port on which the client receives data from the plugin. + * + * @return The incoming port number. + */ + public int getRecvPort() { return socket.getLocalPort(); } + + /** + * Gets the port on which the client sends data to X-Plane. + * + * @return The outgoing port number. + */ + public int getXPlanePort() { return xplanePort; } + + /** + * Sets the port on which the client sends data to X-Plane + * + * @param port The new outgoing port number. + * @throws IllegalArgumentException If {@code port} is not a valid port number. + */ + public void setXPlanePort(int port) + { + if(port < 0 || port >= 0xFFFF) + { + throw new IllegalArgumentException("Invalid port (must be non-negative and less than 65536)."); + } + xplanePort = port; + } + + /** + * Gets the hostname of the X-Plane host. + * + * @return The hostname of the X-Plane host. + */ + public String getXPlaneAddr() { return xplaneAddr.getHostAddress(); } + + /** + * Sets the hostname of the X-Plane host. + * + * @param host The new hostname of the X-Plane host machine. + * @throws UnknownHostException {@code host} is not valid. + */ + public void setXplaneAddr(String host) throws UnknownHostException + { + xplaneAddr = InetAddress.getByName(host); + } + + /** + * Initializes a new instance of the {@code XPlaneConnect} class using default ports and assuming X-Plane is running on the + * local machine. + * + * @throws SocketException If this instance is unable to bind to the default receive port. + */ + public XPlaneConnect() throws SocketException + { + this(100); + } + + /** + * Initializes a new instance of the {@code XPlaneConnect} class with the specified timeout using default ports and + * assuming X-Plane is running on the local machine. + * + * @param timeout The time, in milliseconds, after which read attempts will timeout. + * @throws SocketException If this instance is unable to bind to the default receive port. + */ + public XPlaneConnect(int timeout) throws SocketException + { + this.socket = new DatagramSocket(0); + this.xplaneAddr = InetAddress.getLoopbackAddress(); + this.xplanePort = 49009; + this.socket.setSoTimeout(timeout); + } + + /** + * Initializes a new instance of the {@code XPlaneConnect} class using the specified ports and X-Plane host. + * + * @param xpHost The network host on which X-Plane is running. + * @param xpPort The port on which the X-Plane Connect plugin is listening. + * @param port The local port to use when sending and receiving data from XPC. + * @throws java.net.SocketException If this instance is unable to bind to the specified port. + * @throws java.net.UnknownHostException If the specified hostname can not be resolved. + */ + public XPlaneConnect(String xpHost, int xpPort, int port) + throws java.net.SocketException, java.net.UnknownHostException + { + this(xpHost, xpPort, port, 100); + } + + + /** + * Initializes a new instance of the {@code XPlaneConnect} class from a received discovery Beacon + * @param beacon The beacon received from {@code XPlaneConnectDiscovery} + * @throws SocketException If this instance is unable to bind to the specified port. + */ + public XPlaneConnect(Beacon beacon) throws SocketException { + this.socket = new DatagramSocket(0); + this.xplaneAddr = beacon.getXplaneAddress(); + this.xplanePort = beacon.getPluginPort(); + this.socket.setSoTimeout(100); + } + + /** + * Initializes a new instance of the {@code XPlaneConnect} class using the specified ports, hostname, and timeout. + * + * @param xpHost The network host on which X-Plane is running. + * @param xpPort The port on which the X-Plane Connect plugin is listening. + * @param port The port on which the X-Plane Connect plugin is sending data. + * @param timeout The time, in milliseconds, after which read attempts will timeout. + * @throws java.net.SocketException If this instance is unable to bind to the specified port. + * @throws java.net.UnknownHostException If the specified hostname can not be resolved. + */ + public XPlaneConnect(String xpHost, int xpPort, int port, int timeout) + throws java.net.SocketException, java.net.UnknownHostException + { + this.socket = new DatagramSocket(port); + this.xplaneAddr = InetAddress.getByName(xpHost); + this.xplanePort = xpPort; + this.socket.setSoTimeout(timeout); + } + + /** + * Closes the underlying socket. + */ + public void close() + { + if(socket != null) + { + socket.close(); + socket = null; + } + } + + /** + * Read data from the X-Plane plugin. This method will read whatever data is available and return it. + * + * @return The data send from X-Plane. + * @throws IOException If the read operation fails + */ + private byte[] readUDP() throws IOException + { + byte[] buffer = new byte[65536]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + try + { + socket.receive(packet); + return Arrays.copyOf(buffer, packet.getLength()); + } + catch (SocketTimeoutException ex) + { + return new byte[0]; + } + } + + /** + * Send the given data to the X-Plane plugin. + * + * @param buffer The data to send. + * @throws IOException If the send operation fails. + */ + private void sendUDP(byte[] buffer) throws IOException + { + DatagramPacket packet = new DatagramPacket(buffer, buffer.length, xplaneAddr, xplanePort); + socket.send(packet); + } + + /** + * Pauses or unpauses X-Plane. + * + * @param pause {@code true} to pause the simulator; {@code false} to un-pause. + * @throws IOException If the command cannot be sent. + */ + public void pauseSim(boolean pause) throws IOException + { + // S I M U LEN VAL + byte[] msg = {0x53, 0x49, 0x4D, 0x55, 0x00, 0x00}; + msg[5] = (byte)(pause ? 0x01 : 0x00); + sendUDP(msg); + } + + /** + * Pauses, unpauses, or switches the pause state of X-Plane. + * + * @param pause {@code 1} to pause the simulator, {@code 0} to unpause, or {@code 2} to switch. + * @throws IllegalArgumentException If the values of {@code pause} is not a valid command. + * @throws IOException If the command cannot be sent. + */ + public void pauseSim(int pause) throws IOException + { + if(pause < 0 || (pause > 2 && pause < 100) || (pause > 119 && pause < 200) || pause > 219) + { + throw new IllegalArgumentException("pause must be a value in the range [0, 2], [100, 119], or [200, 219]."); + } + + // S I M U LEN VAL + byte[] msg = {0x53, 0x49, 0x4D, 0x55, 0x00, 0x00}; + msg[5] = (byte)pause; + sendUDP(msg); + } + + /** + * Requests a single dref value from X-Plane. + * + * @param dref The name of the dref requested. + * @return A byte array representing data dependent on the dref requested. + * @throws IOException If either the request or the response fails. + */ + public float[] getDREF(String dref) throws IOException + { + return getDREFs(new String[] {dref})[0]; + } + + /** + * Requests several dref values from X-Plane. + * + * @param drefs An array of dref names to request. + * @return A multidimensional array representing the data for each requested dref. + * @throws IOException If either the request or the response fails. + */ + public float[][] getDREFs(String[] drefs) throws IOException + { + //Preconditions + if(drefs == null || drefs.length == 0) + { + throw new IllegalArgumentException("drefs must be a valid array with at least one dref."); + } + if(drefs.length > 255) + { + throw new IllegalArgumentException("Can not request more than 255 DREFs at once."); + } + + //Convert drefs to bytes. + byte[][] drefBytes = new byte[drefs.length][]; + for(int i = 0; i < drefs.length; ++i) + { + drefBytes[i] = drefs[i].getBytes(StandardCharsets.UTF_8); + if(drefBytes[i].length == 0) + { + throw new IllegalArgumentException("DREF " + i + " is an empty string!"); + } + if(drefBytes[i].length > 255) + { + throw new IllegalArgumentException("DREF " + i + " is too long (must be less than 255 bytes in UTF-8). Are you sure this is a valid DREF?"); + } + } + + //Build and send message + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("GETD".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write(drefs.length); + for(byte[] dref : drefBytes) + { + os.write(dref.length); + os.write(dref, 0, dref.length); + } + sendUDP(os.toByteArray()); + + //Read response + byte[] data = readUDP(); + if(data.length == 0) + { + throw new IOException("No response received."); + } + if(data.length < 6) + { + throw new IOException("Response too short"); + } + float[][] result = new float[drefs.length][]; + ByteBuffer bb = ByteBuffer.wrap(data); + bb.order(ByteOrder.LITTLE_ENDIAN); + int cur = 6; + for(int j = 0; j < result.length; ++j) + { + result[j] = new float[data[cur++]]; + for(int k = 0; k < result[j].length; ++k) //TODO: There must be a better way to do this + { + result[j][k] = bb.getFloat(cur); + cur += 4; + } + } + return result; + } + + public void sendDREF(String dref, float value) throws IOException + { + sendDREF(dref, new float[] {value}); + } + + /** + * Sends a command to X-Plane that sets the given DREF. + * + * @param dref The name of the X-Plane dataref to set. + * @param value An array of floating point values whose structure depends on the dref specified. + * @throws IOException If the command cannot be sent. + */ + public void sendDREF(String dref, float[] value) throws IOException + { + sendDREFs(new String[] {dref}, new float[][] {value}); + } + + /** + * Sends a command to X-Plane that sets the given DREF. + * + * @param drefs The names of the X-Plane datarefs to set. + * @param values A sequence of arrays of floating point values whose structure depends on the drefs specified. + * @throws IOException If the command cannot be sent. + */ + public void sendDREFs(String[] drefs, float[][] values) throws IOException + { + //Preconditions + if(drefs == null || drefs.length == 0) + { + throw new IllegalArgumentException(("drefs must be non-empty.")); + } + if(values == null || values.length != drefs.length) + { + throw new IllegalArgumentException("values must be of the same size as drefs."); + } + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("DREF".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + for(int i = 0; i < drefs.length; ++i) + { + String dref = drefs[i]; + float[] value = values[i]; + + if (dref == null) + { + throw new IllegalArgumentException("dref must be a valid string."); + } + if (value == null || value.length == 0) + { + throw new IllegalArgumentException("value must be non-null and should contain at least one value."); + } + + //Convert drefs to bytes. + byte[] drefBytes = dref.getBytes(StandardCharsets.UTF_8); + if (drefBytes.length == 0) + { + throw new IllegalArgumentException("DREF is an empty string!"); + } + if (drefBytes.length > 255) + { + throw new IllegalArgumentException("dref must be less than 255 bytes in UTF-8. Are you sure this is a valid dref?"); + } + + ByteBuffer bb = ByteBuffer.allocate(4 * value.length); + bb.order(ByteOrder.LITTLE_ENDIAN); + for (int j = 0; j < value.length; ++j) + { + bb.putFloat(j * 4, value[j]); + } + + //Build and send message + os.write(drefBytes.length); + os.write(drefBytes, 0, drefBytes.length); + os.write(value.length); + os.write(bb.array()); + } + sendUDP(os.toByteArray()); + } + + /** + * Gets the control surface information for the specified airplane. + * + * @param ac The aircraft to get control surface information for. + * @return An array containing control surface data in the same format as {@code sendCTRL}. + * @throws IOException If the command cannot be sent or a response cannot be read. + */ + public float[] getCTRL(int ac) throws IOException + { + // Send request + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("GETC".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write(ac); + sendUDP(os.toByteArray()); + + // Read response + byte[] data = readUDP(); + if(data.length == 0) + { + throw new IOException("No response received."); + } + if(data.length < 31) + { + throw new IOException("Response too short"); + } + + // Parse response + float[] result = new float[7]; + ByteBuffer bb = ByteBuffer.wrap(data); + bb.order(ByteOrder.LITTLE_ENDIAN); + result[0] = bb.getFloat(5); + result[1] = bb.getFloat(9); + result[2] = bb.getFloat(13); + result[3] = bb.getFloat(17); + result[4] = bb.get(21); + result[5] = bb.getFloat(22); + result[6] = bb.getFloat(27); + return result; + } + + /** + * Sends command to X-Plane setting control surfaces on the player ac. + * + * @param values

An array containing zero to six values representing control surface data as follows:

+ *
    + *
  1. Latitudinal Stick [-1,1]
  2. + *
  3. Longitudinal Stick [-1,1]
  4. + *
  5. Rudder Pedals [-1, 1]
  6. + *
  7. Throttle [-1, 1]
  8. + *
  9. Gear (0=up, 1=down)
  10. + *
  11. Flaps [0, 1]
  12. + *
  13. Speedbrakes [-0.5, 1.5]
  14. + *
+ *

+ * If @{code ctrl} is less than 6 elements long, The missing elements will not be changed. To + * change values in the middle of the array without affecting the preceding values, set the + * preceding values to -998. + *

+ * @throws IOException If the command cannot be sent. + */ + public void sendCTRL(float[] values) throws IOException + { + sendCTRL(values, 0); + } + + /** + * Sends command to X-Plane setting control surfaces on the specified ac. + * + * @param values

An array containing zero to six values representing control surface data as follows:

+ *
    + *
  1. Latitudinal Stick [-1,1]
  2. + *
  3. Longitudinal Stick [-1,1]
  4. + *
  5. Rudder Pedals [-1, 1]
  6. + *
  7. Throttle [-1, 1]
  8. + *
  9. Gear (0=up, 1=down)
  10. + *
  11. Flaps [0, 1]
  12. + *
  13. Speedbrakes [-0.5, 1.5]
  14. + *
+ *

+ * If @{code ctrl} is less than 6 elements long, The missing elements will not be changed. To + * change values in the middle of the array without affecting the preceding values, set the + * preceding values to -998. + *

+ * @param ac The ac to set. 0 for the player's ac. + * @throws IOException If the command cannot be sent. + */ + public void sendCTRL(float[] values, int ac) throws IOException + { + //Preconditions + if(values == null) + { + throw new IllegalArgumentException("ctrl must no be null."); + } + if(values.length > 7) + { + throw new IllegalArgumentException("ctrl must have 7 or fewer elements."); + } + if(ac < 0 || ac > 9) + { + throw new IllegalArgumentException("ac must be non-negative and less than 9."); + } + + //Pad command values and convert to bytes + int i; + int cur = 0; + ByteBuffer bb = ByteBuffer.allocate(26); + bb.order(ByteOrder.LITTLE_ENDIAN); + for(i = 0; i < 6; ++i) + { + if(i == 4) + { + if(i >= values.length) + { + bb.put(cur, (byte)-1); + } + else + { + bb.put(cur, (byte)values[i]); + } + cur += 1; + } + else if (i >= values.length) + { + bb.putFloat(cur, -998); + cur+= 4; + } + else + { + bb.putFloat(cur, values[i]); + cur += 4; + } + } + bb.put(cur++, (byte) ac); + bb.putFloat(cur, values.length == 7 ? values[6] : -998); + + //Build and send message + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("CTRL".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write(bb.array()); + sendUDP(os.toByteArray()); + } + + /** + * Gets position information for the specified airplane. + * + * @param ac The aircraft to get position information for. + * @return An array containing control surface data in the same format as {@code sendPOSI}. + * @throws IOException If the command cannot be sent or a response cannot be read. + */ + public double[] getPOSI(int ac) throws IOException + { + // Send request + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("GETP".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write(ac); + sendUDP(os.toByteArray()); + + // Read response + byte[] data = readUDP(); + if(data.length == 0) + { + throw new IOException("No response received."); + } + if(data.length < 34) + { + throw new IOException("Response too short"); + } + + // Parse response + double[] result = new double[7]; + ByteBuffer bb = ByteBuffer.wrap(data); + bb.order(ByteOrder.LITTLE_ENDIAN); + for(int i = 0; i < 7; ++i) + { + result[i] = bb.getFloat(6 + 4 * i); + } + return result; + } + + /** + * Sets the position of the player ac. + * + * @param values

An array containing position elements as follows:

+ *
    + *
  1. Latitude (deg)
  2. + *
  3. Longitude (deg)
  4. + *
  5. Altitude (m above MSL)
  6. + *
  7. Roll (deg)
  8. + *
  9. Pitch (deg)
  10. + *
  11. True Heading (deg)
  12. + *
  13. Gear (0=up, 1=down)
  14. + *
+ *

+ * If @{code ctrl} is less than 6 elements long, The missing elements will not be changed. To + * change values in the middle of the array without affecting the preceding values, set the + * preceding values to -998. + *

+ * @throws IOException If the command can not be sent. + */ + public void sendPOSI(double[] values) throws IOException + { + sendPOSI(values, 0); + } + + /** + * Sets the position of the specified ac with double precision coordinates. + * + * @param values

An array containing position elements as follows:

+ *
    + *
  1. Latitude (deg)
  2. + *
  3. Longitude (deg)
  4. + *
  5. Altitude (m above MSL)
  6. + *
  7. Roll (deg)
  8. + *
  9. Pitch (deg)
  10. + *
  11. True Heading (deg)
  12. + *
  13. Gear (0=up, 1=down)
  14. + *
+ *

+ * If @{code ctrl} is less than 6 elements long, The missing elements will not be changed. To + * change values in the middle of the array without affecting the preceding values, set the + * preceding values to -998. + *

+ * @param ac The ac to set. 0 for the player ac. + * @throws IOException If the command can not be sent. + */ + public void sendPOSI(double[] values, int ac) throws IOException + { + //Preconditions + if(values == null) + { + throw new IllegalArgumentException("posi must no be null."); + } + if(values.length > 7) + { + throw new IllegalArgumentException("posi must have 7 or fewer elements."); + } + if(ac < 0 || ac > 255) + { + throw new IllegalArgumentException("ac must be between 0 and 255."); + } + + //Pad command values and convert to bytes + int i; + ByteBuffer bb = ByteBuffer.allocate(40); + bb.order(ByteOrder.LITTLE_ENDIAN); + for(i = 0; i < values.length; ++i) + { + if(i<3) /* lat/lon/height as double */ + { + bb.putDouble(values[i]); + } + else + { + bb.putFloat((float)values[i]); + } + } + for(; i < 7; ++i) + { + bb.putFloat(-998); + } + + //Build and send message + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("POSI".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write(ac); + os.write(bb.array()); + sendUDP(os.toByteArray()); + } + + /** + * Reads X-Plane data + * + * @return The data read. + * @throws IOException If the read operation fails. + */ + public float[][] readData() throws IOException + { + byte[] buffer = readUDP(); + ByteBuffer bb = ByteBuffer.wrap(buffer); + int cur = 5; + int len = bb.get(cur++); + float[][] result = new float[bb.get(len)][9]; + for(int i = 0; i < len; ++i) + { + for(int j = 0; j < 9; ++j) + { + result[i][j] = bb.getFloat(cur); + cur += 4; + } + } + return result; + } + + /** + * Sends data to X-Plane + * + * @param data The data to send. + * @throws IOException If the command cannot be sent. + */ + public void sendDATA(float[][] data) throws IOException + { + //Preconditions + if(data == null || data.length == 0) + { + throw new IllegalArgumentException("data must be a non-null, non-empty array."); + } + + //Convert data to bytes + ByteBuffer bb = ByteBuffer.allocate(4 * 9 * data.length); + bb.order(ByteOrder.LITTLE_ENDIAN); + for(int i = 0; i < data.length; ++i) + { + int rowStart = 9 * 4 * i; + float[] row = data[i]; + if(row.length != 9) + { + throw new IllegalArgumentException("Rows must contain exactly 9 items. (Row " + i + ")"); + } + + bb.putInt(rowStart, (int) row[0]); + for(int j = 1; j < row.length; ++j) + { + bb.putFloat(rowStart + 4 * j, row[j]); + } + } + + //Build and send message + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("DATA".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write(bb.array()); + sendUDP(os.toByteArray()); + } + + /** + * Selects what data X-Plane will export over UDP. + * + * @param rows The row numbers to select. + * @throws IOException If the command cannot be sent. + */ + public void selectDATA(int[] rows) throws IOException + { + //Preconditions + if(rows == null || rows.length == 0) + { + throw new IllegalArgumentException("rows must be a non-null, non-empty array."); + } + + //Convert data to bytes + ByteBuffer bb = ByteBuffer.allocate(4 * rows.length); + bb.order(ByteOrder.LITTLE_ENDIAN); + for(int i = 0; i < rows.length; ++i) + { + bb.putInt(i * 4, rows[i]); + } + + //Build and send message + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("DSEL".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write(bb.array()); + sendUDP(os.toByteArray()); + } + + /** + * Sets a message to be displayed on the screen in X-Plane at the default screen location. + * + * @param msg The message to display. Should not contain any newline characters. + * @throws IOException If the command cannot be sent. + */ + public void sendTEXT(String msg) throws IOException + { + sendTEXT(msg, -1, -1); + } + + /** + * Sets a message to be displayed on the screen in X-Plane at the specified coordinates. + * + * @param msg The message to display. Should not contain any newline characters. + * @param x The number of pixels from the right edge of the screen to display the text. + * @param y The number of pixels from the bottom edge of the screen to display the text. + * @throws IOException If the command cannot be sent. + */ + public void sendTEXT(String msg, int x, int y) throws IOException + { + //Preconditions + if(msg == null) + { + msg = ""; + } + + //Convert drefs to bytes. + byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8); + if(msgBytes.length > 255) + { + throw new IllegalArgumentException("msg must be less than 255 bytes in UTF-8."); + } + + ByteBuffer bb = ByteBuffer.allocate(8); + bb.order(ByteOrder.LITTLE_ENDIAN); + bb.putInt(0, x); + bb.putInt(4, y); + + //Build and send message + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("TEXT".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write(bb.array()); + os.write(msgBytes.length); + os.write(msgBytes); + sendUDP(os.toByteArray()); + } + + /** + * Sets the camera view in X-Plane. + * + * @param view The view to use. + * @throws IOException If the command cannot be sent. + */ + public void sendVIEW(ViewType view) throws IOException + { + ByteBuffer bb = ByteBuffer.allocate(4); + bb.order(ByteOrder.LITTLE_ENDIAN); + bb.putInt(view.getValue()); + + //Build and send message + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("VIEW".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write(bb.array()); + sendUDP(os.toByteArray()); + } + + /** + * Adds, removes, or clears a set of waypoints. If the command is clear, the points are ignored + * and all points are removed. + * + * @param op The operation to perform. + * @param points An array of values representing points. Each triplet in the array will be + * interpreted as a (Lat, Lon, Alt) point. + * @throws IOException If the command cannot be sent. + */ + public void sendWYPT(WaypointOp op, float[] points) throws IOException + { + //Preconditions + if(points.length % 3 != 0) + { + throw new IllegalArgumentException("points.length should be divisible by 3."); + } + if(points.length / 3 > 255) + { + throw new IllegalArgumentException("Too many points. Must be less than 256."); + } + + //Convert points to bytes + ByteBuffer bb = ByteBuffer.allocate(4 * points.length); + bb.order(ByteOrder.LITTLE_ENDIAN); + for(float f : points) + { + bb.putFloat(f); + } + + //Build and send message + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("WYPT".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write(op.getValue()); + os.write(points.length / 3); + os.write(bb.array()); + sendUDP(os.toByteArray()); + } + + /** + * Send a command to X-Plane. + * + * @param comm The name of the X-Plane command to send. + * @throws IOException If the command cannot be sent. + */ + public void sendCOMM(String comm) throws IOException + { + //Preconditions + if(comm == null || comm.length() == 0) + { + throw new IllegalArgumentException(("comm must be non-empty.")); + } + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("COMM".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + + //Convert comm to bytes. + byte[] commBytes = comm.getBytes(StandardCharsets.UTF_8); + if (commBytes.length == 0) + { + throw new IllegalArgumentException("COMM is an empty string!"); + } + if (commBytes.length > 255) + { + throw new IllegalArgumentException("comm must be less than 255 bytes in UTF-8. Are you sure this is a valid comm?"); + } + + //Build and send message + os.write(commBytes.length); + os.write(commBytes); + sendUDP(os.toByteArray()); + } + + + /** + * Sets the port on which the client will receive data from X-Plane. + * + * @param port The new incoming port number. + * @throws IOException If the command cannot be sent. + */ + public void setCONN(int port) throws IOException + { + if(port < 0 || port >= 0xFFFF) + { + throw new IllegalArgumentException("Invalid port (must be non-negative and less than 65536)."); + } + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("CONN".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write((byte) port); + os.write((byte) (port >> 8)); + sendUDP(os.toByteArray()); + + int soTimeout = socket.getSoTimeout(); + socket.close(); + socket = new DatagramSocket(port); + socket.setSoTimeout(soTimeout); + readUDP(); // Try to read response + } +}