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 @@
+
An array containing zero to six values representing control surface data as follows:
+ *+ * 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 ctrlAn array containing zero to six values representing control surface data as follows:
+ *+ * 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 posiAn array containing position elements as follows:
+ *+ * 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 posiAn array containing position elements as follows:
+ *+ * 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