From b783b4007320bda6b7f14f38d27490398f785397 Mon Sep 17 00:00:00 2001 From: Jason Watkins Date: Tue, 31 Mar 2015 12:05:04 -0700 Subject: [PATCH] Basic implementation of the Java client with integration tests. --- Java/.idea/.name | 2 +- Java/.idea/artifacts/XPlaneConnect_jar.xml | 8 + Java/.idea/libraries/junit_4_11.xml | 10 + Java/.idea/misc.xml | 2 +- Java/.idea/vcs.xml | 2 +- Java/XPlaneConnect.iml | 1 + Java/src/XPlaneConnect.java | 585 ++++++++++++++++++++- Java/src/test/XPlaneConnectTest.java | 495 +++++++++++++++++ 8 files changed, 1101 insertions(+), 4 deletions(-) create mode 100644 Java/.idea/artifacts/XPlaneConnect_jar.xml create mode 100644 Java/.idea/libraries/junit_4_11.xml create mode 100644 Java/src/test/XPlaneConnectTest.java diff --git a/Java/.idea/.name b/Java/.idea/.name index a80fd1e..885cc23 100644 --- a/Java/.idea/.name +++ b/Java/.idea/.name @@ -1 +1 @@ -Java \ No newline at end of file +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/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 index 9793229..a61f7bc 100644 --- a/Java/.idea/misc.xml +++ b/Java/.idea/misc.xml @@ -13,7 +13,7 @@ - + \ No newline at end of file diff --git a/Java/.idea/vcs.xml b/Java/.idea/vcs.xml index 6564d52..6c0b863 100644 --- a/Java/.idea/vcs.xml +++ b/Java/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/Java/XPlaneConnect.iml b/Java/XPlaneConnect.iml index 36b8032..6315e70 100644 --- a/Java/XPlaneConnect.iml +++ b/Java/XPlaneConnect.iml @@ -7,5 +7,6 @@ + \ No newline at end of file diff --git a/Java/src/XPlaneConnect.java b/Java/src/XPlaneConnect.java index 7711fee..5ed32b6 100644 --- a/Java/src/XPlaneConnect.java +++ b/Java/src/XPlaneConnect.java @@ -1,5 +1,588 @@ package gov.nasa.xpc; -public class XPlaneConnect +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 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(49008); + 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 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.socket = new DatagramSocket(port); + this.xplaneAddr = InetAddress.getByName(xplaneHost); + this.xplanePort = xplanePort; + 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 //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 + { + socket.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); + 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); + } + + /** + * 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(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()); + } + + /** + * + * + * @param recvPort + * @throws IOException + */ + 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()); + + socket.close(); + socket = new DatagramSocket(recvPort); + } } diff --git a/Java/src/test/XPlaneConnectTest.java b/Java/src/test/XPlaneConnectTest.java new file mode 100644 index 0000000..0069254 --- /dev/null +++ b/Java/src/test/XPlaneConnectTest.java @@ -0,0 +1,495 @@ +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(49007, 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", + "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); + try + { + //TODO: Without this, The rudder dref seems to consistently report -2.00802304E8 + Thread.sleep(200); + } + catch (InterruptedException ex){} + float[][] result = xpc.requestDREFs(drefs); + if(result.length < ctrl.length) + { + fail(); + } + for(int i = 0; i < 6; ++i) + { + assertEquals(ctrl[i], Math.abs(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.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. + float[][] result = xpc.requestDREFs(drefs); + 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], 1e-4); + 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()); + } + } + + @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