From c018d6c3bef7ce8ec77694aab77a9a28e9c8e821 Mon Sep 17 00:00:00 2001 From: Jan Chaloupecky Date: Sat, 1 Jun 2019 20:52:13 +0100 Subject: [PATCH] Java client BECN implementation (#155) --- Java/.idea/misc.xml | 2 +- Java/.idea/modules.xml | 1 + Java/Examples/BasicOperation/src/Main.java | 1 - .../DiscoveryExample/DiscoveryExample.iml | 12 +++ .../src/DiscoveryConnectionExample.java | 35 +++++++ .../src/SimpleDiscoveryExample.java | 23 +++++ Java/XPlaneConnect.iml | 4 +- Java/src/XPlaneConnect.java | 15 +++ Java/src/discovery/Beacon.java | 45 +++++++++ Java/src/discovery/BeaconParser.java | 54 +++++++++++ .../src/discovery/BeaconReceivedListener.java | 10 ++ .../DiscoveryConnectionCallback.java | 14 +++ .../src/discovery/XPlaneConnectDiscovery.java | 94 +++++++++++++++++++ .../Java Tests/XPlaneDiscoveryTests.java | 73 ++++++++++++++ 14 files changed, 379 insertions(+), 4 deletions(-) create mode 100644 Java/Examples/DiscoveryExample/DiscoveryExample.iml create mode 100644 Java/Examples/DiscoveryExample/src/DiscoveryConnectionExample.java create mode 100644 Java/Examples/DiscoveryExample/src/SimpleDiscoveryExample.java create mode 100644 Java/src/discovery/Beacon.java create mode 100644 Java/src/discovery/BeaconParser.java create mode 100644 Java/src/discovery/BeaconReceivedListener.java create mode 100644 Java/src/discovery/DiscoveryConnectionCallback.java create mode 100644 Java/src/discovery/XPlaneConnectDiscovery.java create mode 100644 TestScripts/Java Tests/XPlaneDiscoveryTests.java 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; + } +}