diff --git a/Java/.idea/misc.xml b/Java/.idea/misc.xml
index 7885190..b7148e8 100644
--- a/Java/.idea/misc.xml
+++ b/Java/.idea/misc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/Java/.idea/modules.xml b/Java/.idea/modules.xml
index 3132258..bc73bac 100644
--- a/Java/.idea/modules.xml
+++ b/Java/.idea/modules.xml
@@ -2,6 +2,7 @@
+
diff --git a/Java/Examples/BasicOperation/src/Main.java b/Java/Examples/BasicOperation/src/Main.java
index 92412d0..99c28c9 100644
--- a/Java/Examples/BasicOperation/src/Main.java
+++ b/Java/Examples/BasicOperation/src/Main.java
@@ -1,7 +1,6 @@
package gov.nasa.xpc.ex;
import gov.nasa.xpc.XPlaneConnect;
-
import java.io.IOException;
import java.net.SocketException;
import java.util.Arrays;
diff --git a/Java/Examples/DiscoveryExample/DiscoveryExample.iml b/Java/Examples/DiscoveryExample/DiscoveryExample.iml
new file mode 100644
index 0000000..eeb2a2f
--- /dev/null
+++ b/Java/Examples/DiscoveryExample/DiscoveryExample.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Java/Examples/DiscoveryExample/src/DiscoveryConnectionExample.java b/Java/Examples/DiscoveryExample/src/DiscoveryConnectionExample.java
new file mode 100644
index 0000000..454dd6d
--- /dev/null
+++ b/Java/Examples/DiscoveryExample/src/DiscoveryConnectionExample.java
@@ -0,0 +1,35 @@
+package gov.nasa.xpc.ex;
+
+import gov.nasa.xpc.XPlaneConnect;
+import gov.nasa.xpc.discovery.XPlaneConnectDiscovery;
+
+import java.io.IOException;
+import java.net.SocketException;
+
+
+
+public class DiscoveryConnectionExample {
+
+ public static void main(String[] args) throws IOException {
+
+ XPlaneConnectDiscovery discovery = new XPlaneConnectDiscovery();
+ discovery.start(xpc -> {
+
+ sendDref(xpc);
+ });
+ System.out.println("Example done");
+ }
+
+ static void sendDref(XPlaneConnect xpc) {
+ System.out.println("Sending DREF");
+ try {
+ xpc.getDREF("sim/test/test_float");
+
+ } catch (SocketException e) {
+ System.out.println("Unable to set up the connection. (Error message was '" + e.getMessage() + "'.)");
+ } catch (IOException e) {
+ System.out.println("Something went wrong with one of the commands. (Error message was '" + e.getMessage() + "'.)");
+ }
+ System.out.println("Sending DREF done");
+ }
+}
diff --git a/Java/Examples/DiscoveryExample/src/SimpleDiscoveryExample.java b/Java/Examples/DiscoveryExample/src/SimpleDiscoveryExample.java
new file mode 100644
index 0000000..cfab704
--- /dev/null
+++ b/Java/Examples/DiscoveryExample/src/SimpleDiscoveryExample.java
@@ -0,0 +1,23 @@
+package gov.nasa.xpc.ex;
+
+import gov.nasa.xpc.discovery.XPlaneConnectDiscovery;
+
+import java.io.IOException;
+
+
+/**
+ * XPlaneConnect discovery example that prints continuously each BECN packet
+ */
+public class SimpleDiscoveryExample {
+
+ public static void main(String[] args) throws IOException {
+XPlaneConnectDiscovery discovery = new XPlaneConnectDiscovery();
+discovery.onBeaconReceived(beacon -> {
+ System.out.println("Discovered XPlaneConnect plugin:");
+ System.out.println("Plugin version: " + beacon.getPluginVersion());
+ System.out.println("X-Plane version: " + beacon.getXplaneVersion());
+ System.out.println("Address: " + beacon.getXplaneAddress().getHostAddress() + ":" + beacon.getPluginPort());
+});
+discovery.start();
+ }
+}
diff --git a/Java/XPlaneConnect.iml b/Java/XPlaneConnect.iml
index 33007ca..8acc69f 100644
--- a/Java/XPlaneConnect.iml
+++ b/Java/XPlaneConnect.iml
@@ -1,6 +1,6 @@
-
+
@@ -8,7 +8,7 @@
-
+
diff --git a/Java/src/XPlaneConnect.java b/Java/src/XPlaneConnect.java
index 9e7f4a3..675da9b 100644
--- a/Java/src/XPlaneConnect.java
+++ b/Java/src/XPlaneConnect.java
@@ -25,6 +25,8 @@
package gov.nasa.xpc;
+import gov.nasa.xpc.discovery.Beacon;
+
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.AutoCloseable;
@@ -136,6 +138,19 @@ public class XPlaneConnect implements AutoCloseable
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.
*
diff --git a/Java/src/discovery/Beacon.java b/Java/src/discovery/Beacon.java
new file mode 100644
index 0000000..c5a33c1
--- /dev/null
+++ b/Java/src/discovery/Beacon.java
@@ -0,0 +1,45 @@
+package gov.nasa.xpc.discovery;
+
+import java.net.InetAddress;
+
+
+public class Beacon {
+
+ private InetAddress xplaneAddress;
+ private int pluginPort;
+ private String pluginVersion;
+ private int xPlaneVersion;
+
+
+ public Beacon(InetAddress xplaneAddress, int pluginPort, String pluginVersion, int xPlaneVersion) {
+ this.xplaneAddress = xplaneAddress;
+ this.pluginPort = pluginPort;
+ this.pluginVersion = pluginVersion;
+ this.xPlaneVersion = xPlaneVersion;
+ }
+
+ public String getHost() {
+ return xplaneAddress.getHostAddress();
+ }
+
+ public String getPluginVersion() {
+ return pluginVersion;
+ }
+
+ public String getXplaneVersion() {
+ return String.format("%.2f", xPlaneVersion / 100.0);
+ }
+
+ public int getPluginPort() {
+ return pluginPort;
+ }
+
+ public InetAddress getXplaneAddress() {
+ return xplaneAddress;
+ }
+
+ @Override
+ public String toString() {
+ return "host: " + getHost() + ":" + getPluginPort() +" version: " + getPluginVersion() + " X-Plane Version: " + getXplaneVersion();
+ }
+}
diff --git a/Java/src/discovery/BeaconParser.java b/Java/src/discovery/BeaconParser.java
new file mode 100644
index 0000000..1f7805f
--- /dev/null
+++ b/Java/src/discovery/BeaconParser.java
@@ -0,0 +1,54 @@
+package gov.nasa.xpc.discovery;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Parser class that parses the UDP discovery BECN packet. The format of the packet is
+ * - 4 bytes: BECN
+ * - 1 byte: 0
+ * - 2 bytes: XPlaneConnect server port e.g. '49009'
+ * - 4 bytes: X-Plane version e.g. '11260'
+ * - null terminated string: XPlaneConnect plugin version e.g. '1.3-rc.1'
+ */
+public class BeaconParser {
+
+ private static String BECN = "BECN";
+
+ private static int XPC_PORT_OFFSET = BECN.length() + 1;
+ private static int XPC_PORT_LEN = 2;
+ private static int XPC_VERSION_OFFSET = XPC_PORT_OFFSET + XPC_PORT_LEN;
+ private static int XPC_VERSION_LEN = 4;
+ private static int XPC_PLUGIN_VERSION_OFFSET = XPC_VERSION_OFFSET + XPC_VERSION_LEN;
+
+
+ public Beacon readBCN(DatagramPacket packet) throws IOException {
+ if (packet.getLength() < BECN.length()) {
+ throw new IOException("BECN response too short");
+ }
+
+ byte[] data = packet.getData();
+
+ String command = new String(data, 0, BECN.length());
+ if (!command.equals(BECN)) {
+ throw new IOException("Expected " + BECN + " got '" + command + "'");
+ }
+
+ InetAddress address = packet.getAddress();
+ ByteBuffer bb = ByteBuffer.wrap(data);
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ // 2 bytes: port as short + converted to an int
+ int port = bb.getShort(XPC_PORT_OFFSET) & 0xffff;
+
+ // 4 bytes: x plane version as int
+ int version = bb.getInt(XPC_VERSION_OFFSET);
+
+ // plugin version
+ String pluginVersion = new String(data, XPC_PLUGIN_VERSION_OFFSET, packet.getLength() - XPC_PLUGIN_VERSION_OFFSET);
+
+ return new Beacon(address, port, pluginVersion.trim(), version);
+ }
+}
\ No newline at end of file
diff --git a/Java/src/discovery/BeaconReceivedListener.java b/Java/src/discovery/BeaconReceivedListener.java
new file mode 100644
index 0000000..cd2e0d8
--- /dev/null
+++ b/Java/src/discovery/BeaconReceivedListener.java
@@ -0,0 +1,10 @@
+package gov.nasa.xpc.discovery;
+
+public interface BeaconReceivedListener {
+
+ /**
+ * Called every time a BECN packet is received
+ * @param beacon The parsed beacon
+ */
+ void onBeaconReceived(Beacon beacon);
+}
diff --git a/Java/src/discovery/DiscoveryConnectionCallback.java b/Java/src/discovery/DiscoveryConnectionCallback.java
new file mode 100644
index 0000000..1292826
--- /dev/null
+++ b/Java/src/discovery/DiscoveryConnectionCallback.java
@@ -0,0 +1,14 @@
+package gov.nasa.xpc.discovery;
+
+import gov.nasa.xpc.XPlaneConnect;
+
+public interface DiscoveryConnectionCallback {
+
+ /**
+ * Helper callback called when a 1st XPlanePlugin is discovered. When the 1st packet is received, an XPlaneConnect
+ * instance is created and the discovery is stopped.
+ *
+ * @param xpc The XPlaneConnect instance configured with the discovered
+ */
+ void onConnectionEstablished(XPlaneConnect xpc);
+}
diff --git a/Java/src/discovery/XPlaneConnectDiscovery.java b/Java/src/discovery/XPlaneConnectDiscovery.java
new file mode 100644
index 0000000..0a92dce
--- /dev/null
+++ b/Java/src/discovery/XPlaneConnectDiscovery.java
@@ -0,0 +1,94 @@
+package gov.nasa.xpc.discovery;
+
+import gov.nasa.xpc.XPlaneConnect;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+import java.net.SocketException;
+
+
+/**
+ * The XPlaneConnectDiscovery joins a UDP multi cast group and listens for BECN packets published by
+ * XPlaneConnect server plugin. It allows to clients to discover XPlane instances and be sure that the XPlanePlugin is
+ * installed.
+ */
+public class XPlaneConnectDiscovery implements AutoCloseable {
+
+ private static int DEFAULT_PORT = 49710;
+ private static String DEFAULT_ADDRESS = "239.255.1.1";
+
+ private BeaconReceivedListener mListener;
+
+ private MulticastSocket socket = null;
+ private byte[] buf = new byte[256];
+
+ private int mPort;
+ private String mAddress;
+ private BeaconParser parser = new BeaconParser();
+
+ private XPlaneConnectDiscovery(int port, String address) {
+ mPort = port;
+ mAddress = address;
+ }
+
+ public XPlaneConnectDiscovery() {
+ this(DEFAULT_PORT, DEFAULT_ADDRESS);
+ }
+
+ public void start(DiscoveryConnectionCallback callback) throws IOException {
+ onBeaconReceived(beacon -> {
+ this.close();
+
+ try {
+ XPlaneConnect xpc = new XPlaneConnect(beacon);
+ callback.onConnectionEstablished(xpc);
+ } catch (SocketException e) {
+ System.err.println("Could not connect to a discovered XplaneConnect " +beacon+ ": " + e.getMessage());
+ }
+ });
+ start();
+
+ }
+
+ public void start() throws IOException {
+ System.out.println("Starting XPlane Connect discovery");
+ socket = new MulticastSocket(mPort);
+ InetAddress group = InetAddress.getByName(mAddress);
+ socket.joinGroup(group);
+ while (socket != null) {
+ DatagramPacket packet = new DatagramPacket(buf, buf.length);
+ socket.receive(packet);
+ if (mListener == null) {
+ continue;
+ }
+ try {
+ Beacon beacon = parser.readBCN(packet);
+ mListener.onBeaconReceived(beacon);
+ } catch (IOException ex) {
+ System.err.println("Received packet on discovery group but could not parse it: " + ex);
+ }
+ }
+
+ close();
+ System.out.println("XPlane Connect discovery ended");
+ }
+
+
+ public void onBeaconReceived(BeaconReceivedListener mListener) {
+ this.mListener = mListener;
+ }
+
+ @Override
+ public void close() {
+ if (socket != null) {
+ socket.close();
+ socket = null;
+ }
+ }
+
+ public void stop() {
+ close();
+ }
+}
diff --git a/TestScripts/Java Tests/XPlaneDiscoveryTests.java b/TestScripts/Java Tests/XPlaneDiscoveryTests.java
new file mode 100644
index 0000000..bb954f2
--- /dev/null
+++ b/TestScripts/Java Tests/XPlaneDiscoveryTests.java
@@ -0,0 +1,73 @@
+package gov.nasa.xpc.test;
+
+import static org.junit.Assert.*;
+
+import gov.nasa.xpc.discovery.Beacon;
+import gov.nasa.xpc.discovery.BeaconParser;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+
+
+public class XPlaneDiscoveryTests {
+
+ private BeaconParser parser;
+ private DatagramPacket packet;
+
+ @Before
+ public void setup() {
+ parser = new BeaconParser();
+ byte[] bytes = HexHelper.decodeHexString("4245434E0071BFFC2B0000312E332D72632E310000000000000000");
+ packet = new DatagramPacket(bytes, 0, bytes.length);
+ }
+
+ @Test
+ public void readBeaconParsesPort() throws IOException {
+ Beacon beacon = parser.readBCN(packet);
+ assertEquals(beacon.getPluginPort(), 49009);
+ }
+
+ @Test
+ public void readBeaconParsesXplaneVersion() throws IOException {
+ Beacon beacon = parser.readBCN(packet);
+ assertEquals(beacon.getXplaneVersion(), "112.60");
+
+ }
+
+ @Test
+ public void readBeaconParsesPluginVersion() throws IOException {
+ Beacon beacon = parser.readBCN(packet);
+ assertEquals(beacon.getPluginVersion(), "1.3-rc.1");
+ }
+}
+
+class HexHelper {
+ static byte[] decodeHexString(String hexString) {
+ if (hexString.length() % 2 == 1) {
+ throw new IllegalArgumentException("Invalid hexadecimal String supplied.");
+ }
+
+ byte[] bytes = new byte[hexString.length() / 2];
+ for (int i = 0; i < hexString.length(); i += 2) {
+ bytes[i / 2] = hexToByte(hexString.substring(i, i + 2));
+ }
+ return bytes;
+ }
+
+ private static byte hexToByte(String hexString) {
+ int firstDigit = toDigit(hexString.charAt(0));
+ int secondDigit = toDigit(hexString.charAt(1));
+ return (byte) ((firstDigit << 4) + secondDigit);
+ }
+
+ private static int toDigit(char hexChar) {
+ int digit = Character.digit(hexChar, 16);
+ if (digit == -1) {
+ throw new IllegalArgumentException(
+ "Invalid Hexadecimal Character: " + hexChar);
+ }
+ return digit;
+ }
+}