diff --git a/.gitignore b/.gitignore index 247528f..1f8fc83 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,13 @@ xcuserdata *.moved-aside *.xccheckout +# IntelliJ Files # +################## +*/.idea/workspace.xml +*/.idea/tasks.xml +*/.idea/dictionaries/ +out/ + # MATLAB backup files # ####################### *.m~ diff --git a/Java/.idea/.name b/Java/.idea/.name new file mode 100644 index 0000000..885cc23 --- /dev/null +++ b/Java/.idea/.name @@ -0,0 +1 @@ +XPlaneConnect \ No newline at end of file diff --git a/Java/.idea/artifacts/XPlaneConnect_jar.xml b/Java/.idea/artifacts/XPlaneConnect_jar.xml new file mode 100644 index 0000000..89ac298 --- /dev/null +++ b/Java/.idea/artifacts/XPlaneConnect_jar.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/out/artifacts/XPlaneConnect_jar + + + + + \ No newline at end of file diff --git a/Java/.idea/compiler.xml b/Java/.idea/compiler.xml new file mode 100644 index 0000000..96cc43e --- /dev/null +++ b/Java/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Java/.idea/copyright/profiles_settings.xml b/Java/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/Java/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Java/.idea/libraries/junit_4_11.xml b/Java/.idea/libraries/junit_4_11.xml new file mode 100644 index 0000000..0a24ba1 --- /dev/null +++ b/Java/.idea/libraries/junit_4_11.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Java/.idea/misc.xml b/Java/.idea/misc.xml new file mode 100644 index 0000000..a61f7bc --- /dev/null +++ b/Java/.idea/misc.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Java/.idea/modules.xml b/Java/.idea/modules.xml new file mode 100644 index 0000000..3132258 --- /dev/null +++ b/Java/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Java/.idea/vcs.xml b/Java/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/Java/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Java/XPlaneConnect.iml b/Java/XPlaneConnect.iml new file mode 100644 index 0000000..6315e70 --- /dev/null +++ b/Java/XPlaneConnect.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Java/src/XPlaneConnect.java b/Java/src/XPlaneConnect.java new file mode 100644 index 0000000..25dc70c --- /dev/null +++ b/Java/src/XPlaneConnect.java @@ -0,0 +1,600 @@ +package gov.nasa.xpc; + +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 outSocket; + private DatagramSocket inSocket; + 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 inSocket.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.inSocket = new DatagramSocket(49008); + this.outSocket = new DatagramSocket(50000 + ++clientNum); + this.xplaneAddr = InetAddress.getLoopbackAddress(); + this.xplanePort = 49009; + this.inSocket.setSoTimeout(timeout); + } + + /** + * Initializes a new instance of the {@code XPlaneConnect} class using the specified ports and X-Plane host. + * + * @param port The port on which the X-Plane Connect plugin is sending data. + * @param xplaneHost The network host on which X-Plane is running. + * @param xplanePort The port on which the X-Plane Connect plugin is listening. + * @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(int port, String xplaneHost, int xplanePort) + throws java.net.SocketException, java.net.UnknownHostException + { + this(port, xplaneHost, xplanePort, 100); + } + + /** + * Initializes a new instance of the {@code XPlaneConnect} class using the specified ports, hostname, and timeout. + * + * @param port The port on which the X-Plane Connect plugin is sending data. + * @param xplaneHost The network host on which X-Plane is running. + * @param xplanePort The port on which the X-Plane Connect plugin is listening. + * @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(int port, String xplaneHost, int xplanePort, int timeout) + throws java.net.SocketException, java.net.UnknownHostException + { + this.inSocket = new DatagramSocket(port); + this.outSocket = new DatagramSocket(50000 + ++clientNum); + this.xplaneAddr = InetAddress.getByName(xplaneHost); + this.xplanePort = xplanePort; + this.inSocket.setSoTimeout(timeout); + } + + /** + * Closes the underlying inSocket. + */ + public void close() + { + if(inSocket != null) + { + inSocket.close(); + inSocket = null; + } + if(outSocket != null) + { + outSocket.close(); + outSocket = 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 //TODO: Store data in a class level buffer to account for partial messages + { + byte[] buffer = new byte[2048]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + try + { + inSocket.receive(packet); + return Arrays.copyOf(buffer, buffer[4]); + } + catch (SocketTimeoutException ex) + { + return new byte[0]; + } + } + + /** + * Send the given data to the X-Plane plugin. This method automatically sets the length byte before sending, + * overwriting any value previously stored in @{code buffer[4]}. + * + * @param buffer The data to send. + * @throws IOException If the send operation fails. + */ + private void sendUDP(byte[] buffer) throws IOException + { + if(buffer.length < 5 || buffer.length > 255) + { + throw new IllegalArgumentException("buffer must be between 5 and 255 bytes long."); + } + buffer[4] = (byte)buffer.length; + + DatagramPacket packet = new DatagramPacket(buffer, buffer.length, xplaneAddr, xplanePort); + outSocket.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); + } + + /** + * 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[] requestDREF(String dref) throws IOException + { + return requestDREFs(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[][] requestDREFs(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 + for(int i = 0; i < 40; ++i) + { + byte[] data = readUDP(); + if(data.length == 0) + { + continue; + } + if(data.length < 6) + { + throw new Error("Response too short"); //TODO: Make custom error type + } + if(data[5] != drefs.length) + { + throw new Error("Unexpected response length"); //TODO: Make custom error type + } + 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; + } + throw new IOException("No response received."); + } + + 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 DREF 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 + { + //Preconditions + 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 i = 0; i < value.length; ++i) + { + bb.putFloat(i * 4, value[i]); + } + + //Build and send message + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write("DREF".getBytes(StandardCharsets.UTF_8)); + os.write(0xFF); //Placeholder for message length + os.write(drefBytes.length); + os.write(drefBytes, 0, drefBytes.length); + os.write(value.length); + os.write(bb.array()); + sendUDP(os.toByteArray()); + } + + /** + * Sends command to X-Plane setting control surfaces on the player aircraft. + * + * @param ctrl

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. + *
+ *

+ * 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[] ctrl) throws IOException + { + sendCTRL(ctrl, 0); + } + + /** + * Sends command to X-Plane setting control surfaces on the specified aircraft. + * + * @param ctrl

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. + *
+ *

+ * 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 aircraft The aircraft to set. 0 for the player's aircraft. + * @throws IOException If the command cannot be sent. + */ + private void sendCTRL(float[] ctrl, int aircraft) throws IOException + { + //Preconditions + if(ctrl == null) + { + throw new IllegalArgumentException("ctrl must no be null."); + } + if(ctrl.length > 6) + { + throw new IllegalArgumentException("ctrl must have 6 or fewer elements."); + } + if(aircraft < 0) + { + throw new IllegalArgumentException("aircraft must be non-negative."); + } + if(aircraft != 0) //TODO: Implement support for non-player aircraft on plugin side. + { + throw new Error("Non-player aircraft not supported yet."); + } + + //Pad command values and convert to bytes + int i; + int cur = 0; + ByteBuffer bb = ByteBuffer.allocate(22); + bb.order(ByteOrder.LITTLE_ENDIAN); + for(i = 0; i < ctrl.length; ++i) + { + if(i == 4) + { + bb.put(cur, (byte) ctrl[i]); + cur += 1; + } + else + { + bb.putFloat(cur, ctrl[i]); + cur += 4; + } + } + for(; i < 6; ++i) + { + if(i == 4) + { + bb.put(cur, (byte) 0); + cur += 1; + } + else + { + bb.putFloat(cur, -998); + cur += 4; + } + } + + //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()); + } + + /** + * Sets the position of the player aircraft. + * + * @param posi

An array containing position elements as follows:

+ *
    + *
  1. Latitiude (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(float[] posi) throws IOException + { + sendPOSI(posi, 0); + } + + /** + * Sets the position of the specified aircraft. + * + * @param posi

An array containing position elements as follows:

+ *
    + *
  1. Latitiude (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 aircraft The aircraft to set. 0 for the player aircraft. + * @throws IOException If the command can not be sent. + */ + public void sendPOSI(float[] posi, int aircraft) throws IOException + { + //Preconditions + if(posi == null) + { + throw new IllegalArgumentException("posi must no be null."); + } + if(posi.length > 7) + { + throw new IllegalArgumentException("posi must have 7 or fewer elements."); + } + if(aircraft < 0 || aircraft > 255) + { + throw new IllegalArgumentException("aircraft must be between 0 and 255."); + } + + //Pad command values and convert to bytes + int i; + ByteBuffer bb = ByteBuffer.allocate(28); + bb.order(ByteOrder.LITTLE_ENDIAN); + for(i = 0; i < posi.length; ++i) + { + bb.putFloat(i * 4, posi[i]); + } + for(; i < 7; ++i) + { + bb.putFloat(i * 4, -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(aircraft); + os.write(bb.array()); + sendUDP(os.toByteArray()); + } + + 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()); + } + + /** + * Sets the port on which the client will receive data from X-Plane. + * + * @param recvPort The new incoming port number. + * @throws IOException If the command cannnot be sent. + */ + public void setCONN(int recvPort) throws IOException + { + if(recvPort < 0 || recvPort >= 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) recvPort); + os.write((byte) (recvPort >> 8)); + sendUDP(os.toByteArray()); + + int soTimeout = inSocket.getSoTimeout(); + inSocket.close(); + inSocket = new DatagramSocket(recvPort); + inSocket.setSoTimeout(soTimeout); + } +} diff --git a/Java/src/java Tests/java Tests.xcodeproj/project.pbxproj b/Java/src/java Tests/java Tests.xcodeproj/project.pbxproj new file mode 100644 index 0000000..aa3288a --- /dev/null +++ b/Java/src/java Tests/java Tests.xcodeproj/project.pbxproj @@ -0,0 +1,170 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXGroup section */ + BE9440A11ACF1B7A009068A6 = { + isa = PBXGroup; + children = ( + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXLegacyTarget section */ + BE9440A61ACF1B7A009068A6 /* java Tests */ = { + isa = PBXLegacyTarget; + buildArgumentsString = "$(ACTION)"; + buildConfigurationList = BE9440A91ACF1B7A009068A6 /* Build configuration list for PBXLegacyTarget "java Tests" */; + buildPhases = ( + ); + buildToolPath = /usr/bin/javac; + dependencies = ( + ); + name = "java Tests"; + passBuildSettingsInEnvironment = 1; + productName = "java Tests"; + }; +/* End PBXLegacyTarget section */ + +/* Begin PBXProject section */ + BE9440A21ACF1B7A009068A6 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0500; + ORGANIZATIONNAME = "NASA Diagnostics and Prognostics Group"; + }; + buildConfigurationList = BE9440A51ACF1B7A009068A6 /* Build configuration list for PBXProject "java Tests" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = BE9440A11ACF1B7A009068A6; + projectDirPath = ""; + projectRoot = ""; + targets = ( + BE9440A61ACF1B7A009068A6 /* java Tests */, + ); + }; +/* End PBXProject section */ + +/* Begin XCBuildConfiguration section */ + BE9440A71ACF1B7A009068A6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.9; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + BE9440A81ACF1B7A009068A6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.9; + SDKROOT = macosx; + }; + name = Release; + }; + BE9440AA1ACF1B7A009068A6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUGGING_SYMBOLS = YES; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + BE9440AB1ACF1B7A009068A6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BE9440A51ACF1B7A009068A6 /* Build configuration list for PBXProject "java Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BE9440A71ACF1B7A009068A6 /* Debug */, + BE9440A81ACF1B7A009068A6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BE9440A91ACF1B7A009068A6 /* Build configuration list for PBXLegacyTarget "java Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BE9440AA1ACF1B7A009068A6 /* Debug */, + BE9440AB1ACF1B7A009068A6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; +/* End XCConfigurationList section */ + }; + rootObject = BE9440A21ACF1B7A009068A6 /* Project object */; +} diff --git a/Java/src/java Tests/java Tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Java/src/java Tests/java Tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..c5f49e9 --- /dev/null +++ b/Java/src/java Tests/java Tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Java/src/test/XPlaneConnectTest.java b/Java/src/test/XPlaneConnectTest.java new file mode 100644 index 0000000..defc80c --- /dev/null +++ b/Java/src/test/XPlaneConnectTest.java @@ -0,0 +1,494 @@ +package gov.nasa.xpc.test; + +import gov.nasa.xpc.XPlaneConnect; + +import static org.junit.Assert.*; +import org.junit.Test; + +import java.io.IOException; +import java.net.SocketException; +import java.net.UnknownHostException; + +public class XPlaneConnectTest +{ + @Test + public void constructorTest() + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + assertNotNull(xpc); + } + catch (SocketException ex) + { + fail(); + } + try(XPlaneConnect xpc = new XPlaneConnect(200)) + { + assertNotNull(xpc); + } + catch (SocketException ex) + { + fail(); + } + try(XPlaneConnect xpc = new XPlaneConnect(49007, "127.0.0.1", 49009)) + { + assertNotNull(xpc); + } + catch (SocketException | UnknownHostException ex) + { + fail(); + } + try(XPlaneConnect xpc = new XPlaneConnect(49007, "127.0.0.1", 49009, 200)) + { + assertNotNull(xpc); + } + catch (SocketException | UnknownHostException ex) + { + fail(); + } + } + + @Test + public void testGetRecvPort() throws SocketException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + assertEquals(49008, xpc.getRecvPort()); + } + } + + @Test + public void testGetXPlanePort() throws SocketException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + assertEquals(49009, xpc.getXPlanePort()); + } + } + + @Test + public void testGetXPlaneAddr() throws SocketException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + assertEquals("127.0.0.1", xpc.getXPlaneAddr()); + } + } + + @Test + public void testSetXPlaneAddr() throws SocketException, UnknownHostException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.setXplaneAddr("10.10.0.100"); + assertEquals("10.10.0.100", xpc.getXPlaneAddr()); + } + } + + @Test + public void constructorTest_SocketAlreadyBound() throws SocketException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + try(XPlaneConnect xpc2 = new XPlaneConnect()) + { + fail(); + } + catch (SocketException ex) {} + } + catch (SocketException ex) + { + fail(); + } + } + + @Test(expected = UnknownHostException.class) + public void constructorTest_InvalidHost() throws UnknownHostException + { + try(XPlaneConnect xpc = new XPlaneConnect(49007, "notarealhost", 49009)) + { + + } + catch (SocketException ex) + { + fail(); + } + fail(); + } + + @Test + public void testRequestDREF() throws IOException + { + String dref = "sim/cockpit/switches/gear_handle_status"; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + float[] result = xpc.requestDREF(dref); + assertEquals(1, result.length); + } + } + + @Test + public void testRequestDREFs() throws IOException + { + String[] drefs = + { + "sim/cockpit/switches/gear_handle_status", + "sim/cockpit2/switches/panel_brightness_ratio" + }; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + float[][] result = xpc.requestDREFs(drefs); + assertEquals(2, result.length); + assertEquals(1, result[0].length); + assertEquals(4, result[1].length); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testRequestDREFs_NullArgument() throws IOException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.requestDREFs(null); + fail(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testRequestDREFs_EmptyArgument() throws IOException + { + String[] drefs = new String[0]; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.requestDREFs(drefs); + fail(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testRequestDREFs_TooManyDREFs() throws IOException + { + String[] drefs = new String[300]; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.requestDREFs(drefs); + fail(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testRequestDREFs_DREFTooLong() throws IOException + { + String longDREF = "sim/cockpit/switches/i/am/a/very/long/fake/dref/that/is/over/255/characters/./which/means/that/my/length/cant/be/encoded/in/the/single/byte/allocated/by/the/message/format/,/so/i/should/cause/an/exception/instead/./i/am/still/not/long/enough/./almost/there"; + String[] drefs = new String[]{longDREF}; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.requestDREFs(drefs); + fail(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testRequestDREF_DREFEmpty() throws IOException + { + String dref = ""; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.requestDREF(dref); + fail(); + } + } + + @Test + public void testPauseSim() throws IOException + { + String dref = "sim/operation/override/override_planepath"; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.pauseSim(true); + float[] result = xpc.requestDREF(dref); + //assertEquals(1, result.length); //TODO: Why is this result 20 elements long in Java? (It's only one in MATLAB) + assertEquals(1, result[0], 1e-4); + + xpc.pauseSim(false); + result = xpc.requestDREF(dref); + //assertEquals(1, result.length); + assertEquals(0, result[0], 1e-4); + } + } + + @Test + public void testSendDREF() throws IOException + { + String dref = "sim/cockpit/switches/gear_handle_status"; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + float gearHandle = xpc.requestDREF(dref)[0]; + + float value = gearHandle > 0.5 ? 0 : 1; + xpc.sendDREF(dref, value); + + float result = xpc.requestDREF(dref)[0]; + assertEquals(value, result, 1e-4); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendDREF_NullDREF() throws IOException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendDREF(null, 0); + fail(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendDREF_NullValue() throws IOException + { + String dref = "sim/cockpit/switches/gear_handle_status"; + + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendDREF(dref, null); + fail(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendDREF_EmptyValue() throws IOException + { + String dref = "sim/cockpit/switches/gear_handle_status"; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendDREF(dref, new float[0]); + fail(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendDREF_MessageTooLong() throws IOException + { + String dref = "sim/cockpit/switches/gear_handle_status"; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendDREF(dref, new float[200]); + fail(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendDREF_DREFTooLong() throws IOException + { + String dref = "sim/cockpit/switches/i/am/a/very/long/fake/dref/that/is/over/255/characters/./which/means/that/my/length/cant/be/encoded/in/the/single/byte/allocated/by/the/message/format/,/so/i/should/cause/an/exception/instead/./i/am/still/not/long/enough/./almost/there"; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendDREF(dref, 0); + fail(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendDREF_DREFEmpty() throws IOException + { + String dref = ""; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendDREF(dref, 0); + fail(); + } + } + + @Test + public void testSendCTRL() throws IOException + { + String[] drefs = { + "sim/cockpit2/controls/yoke_pitch_ratio", + "sim/cockpit2/controls/yoke_roll_ratio", + "sim/cockpit2/controls/yoke_heading_ratio", + "sim/flightmodel/engine/ENGN_thro", + "sim/cockpit/switches/gear_handle_status", + "sim/flightmodel/controls/flaprqst" + }; + float[] ctrl = new float[] {0, 0, 1, 0.8F, 0, 1}; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendCTRL(ctrl); + float[][] result = xpc.requestDREFs(drefs); + if(result.length < ctrl.length) + { + fail(); + } + for(int i = 0; i < 6; ++i) + { + assertEquals(ctrl[i], result[i][0], 1e-2); + } + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendCTRL_NullCtrl() throws IOException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendCTRL(null); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendCTRL_LongCtrl() throws IOException + { + float[] ctrl = new float[] {0, 0, 1, 0.8F, 0, 1, -998}; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendCTRL(ctrl); + } + } + + @Test + public void testSendPOSI() throws IOException + { + String[] drefs = { + "sim/flightmodel/position/latitude", + "sim/flightmodel/position/longitude", + "sim/flightmodel/position/y_agl", + "sim/flightmodel/position/phi", + "sim/flightmodel/position/theta", + "sim/flightmodel/position/psi", + "sim/cockpit/switches/gear_handle_status" + }; + float[] posi = new float[] {37.524F, -122.06899F, 2500, 0, 0, 0, 1}; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.pauseSim(true); + xpc.sendPOSI(posi); + //TODO: It seems that these calls are a bit too fast. The dref request often gets stale data, causing the test to fail incorrectly. + try {Thread.sleep(100);}catch(InterruptedException ex){} + float[][] result = xpc.requestDREFs(drefs); + xpc.pauseSim(false); + if(result.length < posi.length) + { + fail(); + } + assertEquals(posi[0], result[0][0], 1e-4); + assertEquals(posi[1], result[1][0], 1e-4); + assertEquals(posi[2], result[2][0], 10); + assertEquals(posi[3], result[3][0], 1e-4); + assertEquals(posi[4], result[4][0], 1e-4); + assertEquals(posi[5], result[5][0], 1e-4); + assertEquals(posi[6], result[6][0], 1e-4); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendPOSI_NullCtrl() throws IOException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendPOSI(null); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendPOSI_LongCtrl() throws IOException + { + float[] posi = new float[] {37.524F, -122.06899F, 2500, 0, 0, 0, 1, -998}; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendPOSI(posi); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendPOSI_NegativeAircraftNum() throws IOException + { + float[] posi = new float[] {37.524F, -122.06899F, 2500, 0, 0, 0, 1, -998}; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendPOSI(posi, -1); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendPOSI_LargeAircraftNum() throws IOException + { + float[] posi = new float[] {37.524F, -122.06899F, 2500, 0, 0, 0, 1, -998}; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendPOSI(posi, 300); + } + } + + @Test + public void testSendDATA() throws IOException + { + float[][] data = {{25,0.8F,-988,-988,-988,-988,-988,-988,-988}}; + String dref = "sim/flightmodel/engine/ENGN_thro"; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendDATA(data); + float[] result = xpc.requestDREF(dref); + assertEquals(data[0][1], result[0], 1e-4); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSendData_NullData() throws IOException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendDATA(null); + } + fail(); + } + + @Test(expected = IllegalArgumentException.class) + public void testSendData_InvalidData() throws IOException + { + float[][] data = {{25,0.8F,-988,-988,-988,-988,-988,-988,-988,-988}}; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.sendDATA(data); + } + fail(); + } + + @Test + public void testSetCONN() throws IOException + { + String dref = "sim/cockpit/switches/gear_handle_status"; + try(XPlaneConnect xpc = new XPlaneConnect()) + { + int p = xpc.getRecvPort(); + xpc.setCONN(49055); + assertEquals(49055, xpc.getRecvPort()); + float[] result = xpc.requestDREF(dref); + assertEquals(1, result.length); + + xpc.setCONN(p); + assertEquals(p, xpc.getRecvPort()); + result = xpc.requestDREF(dref); + assertEquals(1, result.length); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSetCONN_NegativePort() throws IOException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.setCONN(-1); + fail(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testSetCONN_LargePort() throws IOException + { + try(XPlaneConnect xpc = new XPlaneConnect()) + { + xpc.setCONN(65536); + fail(); + } + } +} \ No newline at end of file