mega changes -- transition to Python3

This commit is contained in:
cs-powell
2025-01-24 09:33:57 -05:00
parent 3be0e2323b
commit e15c534c07
65 changed files with 273097 additions and 16 deletions

View File

@@ -0,0 +1,35 @@
package CognitiveModel.ModelFiles;
public abstract class Action {
Integer priority; // Priority of the Task
Integer delay; // Potential Delay before starting the task
String targetDREF;
Integer taskTime; // How long should the task take
boolean taskComplete; // Is the task completed?
ActionType actionType;
public Action(ActionType actionType, int delay) {
this.actionType = actionType;
this.delay = delay;
this.targetDREF = null;
}
public Action(ActionType actionType, int delay, String target) {
this.actionType = actionType;
this.delay = delay;
targetDREF = target;
}
public ActionType getType() {
return actionType;
}
public Integer getDelay() {
return delay;
}
public String getTarget() {
return targetDREF;
}
}

View File

@@ -0,0 +1,7 @@
package CognitiveModel.ModelFiles;
public enum ActionType {
VISION,
MOTOR,
DELAY,
}

View File

@@ -0,0 +1,14 @@
package CognitiveModel.ModelFiles;
// package ProjectModels.CognitiveModel;
public class Delay extends Action {
public Delay() {
super(ActionType.DELAY, 1000);
}
public Delay(int delay) {
super(ActionType.DELAY, delay);
}
}

View File

@@ -0,0 +1,62 @@
package CognitiveModel.ModelFiles;
// package ProjectModels.CognitiveModel;
import java.util.LinkedList;
public class MindQueue {
/*
Recommended Changes/Improvements
See Google Doc Notes 12/11/24
*/
LinkedList<Action> q;
public MindQueue() {
q = new LinkedList<Action>();
}
public void push(Action e) {
q.add(e);
}
public Action removeEvent(Action e) {
Action temp = e;
q.remove(e);
return temp;
}
public Action pop() {
Vision v = new Vision();
if (q.isEmpty()) {
return v;
}
return q.remove();
}
public boolean isEmpty() {
return q.isEmpty();
}
public String queueToString() {
String queueTrace = "Next to Execute ==> ";
for (Action action : q) {
if (action.getType() == ActionType.VISION) {
queueTrace += "[V]";
}
if (action.getType() == ActionType.MOTOR) {
queueTrace += "[M]";
}
if (action.getType() == ActionType.DELAY) {
queueTrace += "[D]";
}
}
queueTrace += "\n";
return queueTrace;
}
public int queueLength() {
return q.size();
}
}

View File

@@ -0,0 +1,282 @@
package CognitiveModel.ModelFiles;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
// import ModelFiles.ActionType;
// import ModelFiles.MotorType;
public class Model {
private MindQueue q; // Queue for actions
boolean modelActive; // On/Off Switch for the model execution
XPlaneConnect xpc; // Allows connection to the simulator
String storedVisionTarget;
float[] storedVisionResult;
boolean logCreated;
boolean logAllowed;
File dataLogFile = null;
// Constructors
public Model() {
q = new MindQueue();
modelActive = false;
logCreated = false;
}
public Model(XPlaneConnect xpc) {
q = new MindQueue();
modelActive = false;
this.xpc = xpc;
logCreated = false;
}
/* Getters */
public MindQueue getQueue() {
return q;
}
/* Setters */
/*
* Setup Methods
* Printing, Empty check, Activation/Deactivation of Model, etc.
*/
public void activateModel() {
modelActive = true;
}
public void establishConnection(XPlaneConnect newXPC) {
this.xpc = newXPC;
}
public void deactivateModel() {
modelActive = false;
}
public boolean isEmpty() {
return q.isEmpty();
}
public void push(Action a) {
q.push(a);
}
public boolean isActive() {
return modelActive;
}
public void printModelQueue() {
System.out.println(q.queueToString());
}
public int getModelQueueLength() {
return q.queueLength();
}
public void createAction(ActionType actionType, MotorType motorType, int delay, String target) {
Action newAction = null;
switch (actionType) {
case VISION:
newAction = new Vision(delay, target);
this.push(newAction);
break;
case MOTOR:
newAction = new Motor(motorType, delay, target);
this.push(newAction);
break;
case DELAY:
newAction = new Delay(delay);
this.push(newAction);
break;
}
}
/*
* Processing Methods
*
*/
/*
* Process the next event in the model queue
*/
public float[] next() {
float[] returnArray = null;
Action temp = q.pop();
// System.out.println("Type of Action: " + temp.getType());
if (temp.getType() == ActionType.VISION) { // Vision Action (Get Data)
handelVisionAction(temp, returnArray);
} else if (temp.getType() == ActionType.MOTOR) { // Motor Action (Act Upon Data)
handleMotorAction(temp);
} else if (temp.getType() == ActionType.DELAY) {// Pure Delays (Do nothing)
handleDelayAction(temp);
}
// q.push(temp);
return returnArray;
}
/* Next Helpers */
private void handelVisionAction(Action temp, float[] returnArray) {
Vision tempV = (Vision) temp;
initiateDelay(tempV.getDelay());
String dref = tempV.getTarget();
try {
returnArray = xpc.getDREF(dref);
storedVisionTarget = dref;
storedVisionResult = returnArray.clone();
// System.out.println(returnArray[0]);
} catch (IOException e) {
}
}
private void handleMotorAction(Action temp) {
/*
TODO: GoogleDoc Notes 12/11/24
*/
Motor tempM = (Motor) temp;
MotorType motorType = tempM.getMotorType();
float[] currentControls = null;
try {
currentControls = xpc.getCTRL(0);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
switch (motorType) {
case PITCHUP:
float[] pitchUp = { currentControls[0] + 0.01f };
if (storedVisionResult[0] > 80) {
if (currentControls[0] < 0.2f) {
System.out.println("Pitching Up");
xpc.sendCTRL(pitchUp);
}
}
break;
case PITCHDOWN:
float[] pitchDown = { currentControls[0] - 0.01f };
if (storedVisionResult[0] < 80) {
if (currentControls[0] > -0.2f) {
// System.out.println("Sending Pitch Down");
System.out.println("Pitching Down");
xpc.sendCTRL(pitchDown);
}
}
break;
case THROTTLEUP:
break;
case THROTTLEDOWN:
break;
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void handleDelayAction(Action temp) {
Delay tempD = (Delay) temp;
initiateDelay(tempD.getDelay());
storedVisionTarget = "INTERRUPTION: DELAY";
}
public static void initiateDelay(int delay) {
try {
Thread.sleep(delay); // Using the action's Built in Delay
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public float[] getCurrentControls() throws IOException{
float [] returnArray = null;
try {
returnArray = xpc.getCTRL(0);
} catch (IOException e) {
// TODO Auto-generated catch block
while(returnArray == null) {
System.out.println("Waiting for reconnect");
returnArray = xpc.getCTRL(0);
}
}
return returnArray;
}
public void startLog() {
logAllowed = true;
}
public void stopLog() {
logAllowed = false;
logCreated = false;
}
public boolean logPermission() {
return logAllowed;
}
public void logData() {
if(logAllowed) {
logProcess();
}
}
private void logProcess(){
if(!logCreated) {
dataLogFile = new File("./dataLog/" + java.time.LocalDateTime.now() + ".csv");
logCreated = true;
try {
float[] ctrl1 = this.getCurrentControls();
String setup = "Elevator, Roll, Yaw, Throttle, Flaps\n";
FileWriter fw = new FileWriter(dataLogFile, true);
BufferedWriter br = new BufferedWriter(fw);
br.write(setup);
br.flush();
} catch (IOException e) {
}
}
String log = null;
try {
float[] ctrl1 = this.getCurrentControls();
//Labled Format
log = String.format("[Elevator: %2f] [Roll: %2f] [Yaw: %2f] [Throttle:%2f] [Flaps: %2f] \n",
ctrl1[0], ctrl1[1], ctrl1[2], ctrl1[3], ctrl1[5]);
//CSV Format
log = String.format("%2f, %2f, %2f, %2f, %2f\n",
ctrl1[0], ctrl1[1], ctrl1[2], ctrl1[3], ctrl1[5]);
FileWriter fw = new FileWriter(dataLogFile, true);
BufferedWriter br = new BufferedWriter(fw);
br.write(log);
br.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println("No data to log");
}
}
public float[] getStoredVisionResult() {
return storedVisionResult;
}
public String getStoredVisionTarget() {
return storedVisionTarget;
}
}

View File

@@ -0,0 +1,44 @@
package CognitiveModel.ModelFiles;
import java.io.IOException;
public class Motor extends Action {
float[] control;
MotorType motorType;
public Motor(MotorType motorType) {
super(ActionType.MOTOR, 1000);
this.motorType = motorType;
}
// public Motor(int delay) {
// super(ActionType.MOTOR,delay);
// }
public Motor(MotorType motorType, int delay, String target) {
super(ActionType.MOTOR, delay, target);
this.motorType = motorType;
}
public void createMotorControl() { // TODO: Maybe takes in a formatted set of vision inputs?
control = null;
}
public MotorType getMotorType() {
return motorType;
}
public void sendMotorControl(XPlaneConnect xpc) {
if (control != null) {
try {
xpc.sendCTRL(control);
} catch (IOException e) {
// TODO Auto-generated catch block
System.out.println("Control failed to send");
}
}
}
}

View File

@@ -0,0 +1,8 @@
package CognitiveModel.ModelFiles;
public enum MotorType {
PITCHUP,
PITCHDOWN,
THROTTLEUP,
THROTTLEDOWN
}

View File

@@ -0,0 +1,9 @@
package CognitiveModel.ModelFiles;
public class Process {
public Process() {
}
}

View File

@@ -0,0 +1,19 @@
package CognitiveModel.ModelFiles;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class Tests {
MindQueue m = new MindQueue();
@Test
public void test1() {
Integer i = 1;
Action a = new Vision();
m.push(a);
assertEquals(i, m.pop());
}
}

View File

@@ -0,0 +1,21 @@
package CognitiveModel.ModelFiles;
public class Vision extends Action {
public Vision() {
super(ActionType.VISION, 1000);
}
public Vision(int delay) {
super(ActionType.VISION, delay);
}
public Vision(String dref) {
super(ActionType.VISION, 1000, dref);
}
public Vision(int delay, String dref) {
super(ActionType.VISION, delay, dref);
}
}

View File

@@ -0,0 +1,45 @@
package CognitiveModel.ModelFiles.XPCdependencies;
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();
}
}

View File

@@ -0,0 +1,54 @@
package CognitiveModel.ModelFiles.XPCdependencies;
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);
}
}

View File

@@ -0,0 +1,10 @@
package CognitiveModel.ModelFiles.XPCdependencies;
public interface BeaconReceivedListener {
/**
* Called every time a BECN packet is received
* @param beacon The parsed beacon
*/
void onBeaconReceived(Beacon beacon);
}

View File

@@ -0,0 +1,14 @@
package CognitiveModel.ModelFiles.XPCdependencies;
import CognitiveModel.ModelFiles.*;
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);
}

View File

@@ -0,0 +1,60 @@
//NOTICES:
// Copyright (c) 2013-2018 United States Government as represented by the Administrator of the
// National Aeronautics and Space Administration. All Rights Reserved.
//
// DISCLAIMERS
// No Warranty: THE SUBJECT SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY OF ANY KIND,
// EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE
// SUBJECT SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE, OR FREEDOM FROM INFRINGEMENT, ANY WARRANTY THAT THE SUBJECT
// SOFTWARE WILL BE ERROR FREE, OR ANY WARRANTY THAT DOCUMENTATION, IF PROVIDED, WILL CONFORM TO
// THE SUBJECT SOFTWARE. THIS AGREEMENT DOES NOT, IN ANY MANNER, CONSTITUTE AN ENDORSEMENT BY
// GOVERNMENT AGENCY OR ANY PRIOR RECIPIENT OF ANY RESULTS, RESULTING DESIGNS, HARDWARE,
// SOFTWARE PRODUCTS OR ANY OTHER APPLICATIONS RESULTING FROM USE OF THE SUBJECT SOFTWARE.
// FURTHER, GOVERNMENT AGENCY DISCLAIMS ALL WARRANTIES AND LIABILITIES REGARDING THIRD-PARTY
// SOFTWARE, IF PRESENT IN THE ORIGINAL SOFTWARE, AND DISTRIBUTES IT "AS IS."
//
// Waiver and Indemnity: RECIPIENT AGREES TO WAIVE ANY AND ALL CLAIMS AGAINST THE UNITED STATES
// GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT. IF
// RECIPIENT'S USE OF THE SUBJECT SOFTWARE RESULTS IN ANY LIABILITIES, DEMANDS, DAMAGES,
// EXPENSES OR LOSSES ARISING FROM SUCH USE, INCLUDING ANY DAMAGES FROM PRODUCTS BASED ON, OR
// RESULTING FROM, RECIPIENT'S USE OF THE SUBJECT SOFTWARE, RECIPIENT SHALL INDEMNIFY AND HOLD
// HARMLESS THE UNITED STATES GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY
// PRIOR RECIPIENT, TO THE EXTENT PERMITTED BY LAW. RECIPIENT'S SOLE REMEDY FOR ANY SUCH MATTER
// SHALL BE THE IMMEDIATE, UNILATERAL TERMINATION OF THIS AGREEMENT.
package CognitiveModel.ModelFiles.XPCdependencies;
/**
* Represents a camera view in X-Plane
*
* @author Jason Watkins
* @version 1.1
* @since 2015-05-08
*/
public enum ViewType
{
Forwards(73),
Down(74),
Left(75),
Right(76),
Back(77),
Tower(78),
Runway(79),
Chase(80),
Follow(81),
FollowWithPanel(82),
Spot(83),
FullscreenWithHud(84),
FullscreenNoHud(85);
private final int value;
private ViewType(int value)
{
this.value = value;
}
public int getValue()
{
return value;
}
}

View File

@@ -0,0 +1,50 @@
//NOTICES:
// Copyright (c) 2013-2018 United States Government as represented by the Administrator of the
// National Aeronautics and Space Administration. All Rights Reserved.
//
// DISCLAIMERS
// No Warranty: THE SUBJECT SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY OF ANY KIND,
// EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE
// SUBJECT SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE, OR FREEDOM FROM INFRINGEMENT, ANY WARRANTY THAT THE SUBJECT
// SOFTWARE WILL BE ERROR FREE, OR ANY WARRANTY THAT DOCUMENTATION, IF PROVIDED, WILL CONFORM TO
// THE SUBJECT SOFTWARE. THIS AGREEMENT DOES NOT, IN ANY MANNER, CONSTITUTE AN ENDORSEMENT BY
// GOVERNMENT AGENCY OR ANY PRIOR RECIPIENT OF ANY RESULTS, RESULTING DESIGNS, HARDWARE,
// SOFTWARE PRODUCTS OR ANY OTHER APPLICATIONS RESULTING FROM USE OF THE SUBJECT SOFTWARE.
// FURTHER, GOVERNMENT AGENCY DISCLAIMS ALL WARRANTIES AND LIABILITIES REGARDING THIRD-PARTY
// SOFTWARE, IF PRESENT IN THE ORIGINAL SOFTWARE, AND DISTRIBUTES IT "AS IS."
//
// Waiver and Indemnity: RECIPIENT AGREES TO WAIVE ANY AND ALL CLAIMS AGAINST THE UNITED STATES
// GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT. IF
// RECIPIENT'S USE OF THE SUBJECT SOFTWARE RESULTS IN ANY LIABILITIES, DEMANDS, DAMAGES,
// EXPENSES OR LOSSES ARISING FROM SUCH USE, INCLUDING ANY DAMAGES FROM PRODUCTS BASED ON, OR
// RESULTING FROM, RECIPIENT'S USE OF THE SUBJECT SOFTWARE, RECIPIENT SHALL INDEMNIFY AND HOLD
// HARMLESS THE UNITED STATES GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY
// PRIOR RECIPIENT, TO THE EXTENT PERMITTED BY LAW. RECIPIENT'S SOLE REMEDY FOR ANY SUCH MATTER
// SHALL BE THE IMMEDIATE, UNILATERAL TERMINATION OF THIS AGREEMENT.
package CognitiveModel.ModelFiles.XPCdependencies;
/**
* Represents operations that can be performed by the WYPT command.
*
* @author Jason Watkins
* @version 1.0
* @since 2015-04-09
*/
public enum WaypointOp
{
Add(1),
Del(2),
Clr(3);
private final int value;
private WaypointOp(int value)
{
this.value = value;
}
public int getValue()
{
return value;
}
}

View File

@@ -0,0 +1,94 @@
package CognitiveModel.ModelFiles.XPCdependencies;
import CognitiveModel.ModelFiles.*;
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();
}
@SuppressWarnings("deprecation") // Added custom
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();
}
}

View File

@@ -0,0 +1,919 @@
//NOTICES:
// Copyright (c) 2013-2018 United States Government as represented by the Administrator of the
// National Aeronautics and Space Administration. All Rights Reserved.
//
// DISCLAIMERS
// No Warranty: THE SUBJECT SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY OF ANY KIND,
// EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE
// SUBJECT SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE, OR FREEDOM FROM INFRINGEMENT, ANY WARRANTY THAT THE SUBJECT
// SOFTWARE WILL BE ERROR FREE, OR ANY WARRANTY THAT DOCUMENTATION, IF PROVIDED, WILL CONFORM TO
// THE SUBJECT SOFTWARE. THIS AGREEMENT DOES NOT, IN ANY MANNER, CONSTITUTE AN ENDORSEMENT BY
// GOVERNMENT AGENCY OR ANY PRIOR RECIPIENT OF ANY RESULTS, RESULTING DESIGNS, HARDWARE,
// SOFTWARE PRODUCTS OR ANY OTHER APPLICATIONS RESULTING FROM USE OF THE SUBJECT SOFTWARE.
// FURTHER, GOVERNMENT AGENCY DISCLAIMS ALL WARRANTIES AND LIABILITIES REGARDING THIRD-PARTY
// SOFTWARE, IF PRESENT IN THE ORIGINAL SOFTWARE, AND DISTRIBUTES IT "AS IS."
//
// Waiver and Indemnity: RECIPIENT AGREES TO WAIVE ANY AND ALL CLAIMS AGAINST THE UNITED STATES
// GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT. IF
// RECIPIENT'S USE OF THE SUBJECT SOFTWARE RESULTS IN ANY LIABILITIES, DEMANDS, DAMAGES,
// EXPENSES OR LOSSES ARISING FROM SUCH USE, INCLUDING ANY DAMAGES FROM PRODUCTS BASED ON, OR
// RESULTING FROM, RECIPIENT'S USE OF THE SUBJECT SOFTWARE, RECIPIENT SHALL INDEMNIFY AND HOLD
// HARMLESS THE UNITED STATES GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY
// PRIOR RECIPIENT, TO THE EXTENT PERMITTED BY LAW. RECIPIENT'S SOLE REMEDY FOR ANY SUCH MATTER
// SHALL BE THE IMMEDIATE, UNILATERAL TERMINATION OF THIS AGREEMENT.
package CognitiveModel.ModelFiles;
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;
import CognitiveModel.ModelFiles.XPCdependencies.*;
/**
* 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 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(0);
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 xpHost The network host on which X-Plane is running.
* @param xpPort The port on which the X-Plane Connect plugin is listening.
* @param port The local port to use when sending and receiving data from XPC.
* @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(String xpHost, int xpPort, int port)
throws java.net.SocketException, java.net.UnknownHostException {
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.
*
* @param xpHost The network host on which X-Plane is running.
* @param xpPort The port on which the X-Plane Connect plugin is listening.
* @param port The port on which the X-Plane Connect plugin is sending data.
* @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(String xpHost, int xpPort, int port, int timeout)
throws java.net.SocketException, java.net.UnknownHostException {
this.socket = new DatagramSocket(port);
this.xplaneAddr = InetAddress.getByName(xpHost);
this.xplanePort = xpPort;
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 {
byte[] buffer = new byte[65536];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
socket.receive(packet);
return Arrays.copyOf(buffer, packet.getLength());
} catch (SocketTimeoutException ex) {
return new byte[0];
}
}
/**
* Send the given data to the X-Plane plugin.
*
* @param buffer The data to send.
* @throws IOException If the send operation fails.
*/
private void sendUDP(byte[] buffer) throws IOException {
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);
}
/**
* Pauses, unpauses, or switches the pause state of X-Plane.
*
* @param pause {@code 1} to pause the simulator, {@code 0} to unpause, or
* {@code 2} to switch.
* @throws IllegalArgumentException If the values of {@code pause} is not a
* valid command.
* @throws IOException If the command cannot be sent.
*/
public void pauseSim(int pause) throws IOException {
if (pause < 0 || (pause > 2 && pause < 100) || (pause > 119 && pause < 200) || pause > 219) {
throw new IllegalArgumentException("pause must be a value in the range [0, 2], [100, 119], or [200, 219].");
}
// S I M U LEN VAL
byte[] msg = { 0x53, 0x49, 0x4D, 0x55, 0x00, 0x00 };
msg[5] = (byte) pause;
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[] getDREF(String dref) throws IOException {
return getDREFs(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[][] getDREFs(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
byte[] data = readUDP();
if (data.length == 0) {
throw new IOException("No response received.");
}
if (data.length < 6) {
throw new IOException("Response too short");
}
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;
}
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 X-Plane dataref 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 {
sendDREFs(new String[] { dref }, new float[][] { value });
}
/**
* Sends a command to X-Plane that sets the given DREF.
*
* @param drefs The names of the X-Plane datarefs to set.
* @param values A sequence of arrays of floating point values whose structure
* depends on the drefs specified.
* @throws IOException If the command cannot be sent.
*/
public void sendDREFs(String[] drefs, float[][] values) throws IOException {
// Preconditions
if (drefs == null || drefs.length == 0) {
throw new IllegalArgumentException(("drefs must be non-empty."));
}
if (values == null || values.length != drefs.length) {
throw new IllegalArgumentException("values must be of the same size as drefs.");
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write("DREF".getBytes(StandardCharsets.UTF_8));
os.write(0xFF); // Placeholder for message length
for (int i = 0; i < drefs.length; ++i) {
String dref = drefs[i];
float[] value = values[i];
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 j = 0; j < value.length; ++j) {
bb.putFloat(j * 4, value[j]);
}
// Build and send message
os.write(drefBytes.length);
os.write(drefBytes, 0, drefBytes.length);
os.write(value.length);
os.write(bb.array());
}
sendUDP(os.toByteArray());
}
/**
* Gets the control surface information for the specified airplane.
*
* @param ac The aircraft to get control surface information for.
* @return An array containing control surface data in the same format as
* {@code sendCTRL}.
* @throws IOException If the command cannot be sent or a response cannot be
* read.
*/
public float[] getCTRL(int ac) throws IOException {
// Send request
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write("GETC".getBytes(StandardCharsets.UTF_8));
os.write(0xFF); // Placeholder for message length
os.write(ac);
sendUDP(os.toByteArray());
// Read response
byte[] data = readUDP();
if (data.length == 0) {
throw new IOException("No response received.");
}
if (data.length < 31) {
throw new IOException("Response too short");
}
// Parse response
float[] result = new float[7];
ByteBuffer bb = ByteBuffer.wrap(data);
bb.order(ByteOrder.LITTLE_ENDIAN);
result[0] = bb.getFloat(5);
result[1] = bb.getFloat(9);
result[2] = bb.getFloat(13);
result[3] = bb.getFloat(17);
result[4] = bb.get(21);
result[5] = bb.getFloat(22);
result[6] = bb.getFloat(27);
return result;
}
/**
* Sends command to X-Plane setting control surfaces on the player ac.
*
* @param values
* <p>
* An array containing zero to six values representing control
* surface data as follows:
* </p>
* <ol>
* <li>Latitudinal Stick [-1,1]</li>
* <li>Longitudinal Stick [-1,1]</li>
* <li>Rudder Pedals [-1, 1]</li>
* <li>Throttle [-1, 1]</li>
* <li>Gear (0=up, 1=down)</li>
* <li>Flaps [0, 1]</li>
* <li>Speedbrakes [-0.5, 1.5]</li>
* </ol>
* <p>
* 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.
* </p>
* @throws IOException If the command cannot be sent.
*/
public void sendCTRL(float[] values) throws IOException {
sendCTRL(values, 0);
}
/**
* Sends command to X-Plane setting control surfaces on the specified ac.
*
* @param values
* <p>
* An array containing zero to six values representing control
* surface data as follows:
* </p>
* <ol>
* <li>Latitudinal Stick [-1,1]</li>
* <li>Longitudinal Stick [-1,1]</li>
* <li>Rudder Pedals [-1, 1]</li>
* <li>Throttle [-1, 1]</li>
* <li>Gear (0=up, 1=down)</li>
* <li>Flaps [0, 1]</li>
* <li>Speedbrakes [-0.5, 1.5]</li>
* </ol>
* <p>
* 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.
* </p>
* @param ac The ac to set. 0 for the player's ac.
* @throws IOException If the command cannot be sent.
*/
public void sendCTRL(float[] values, int ac) throws IOException {
// Preconditions
if (values == null) {
throw new IllegalArgumentException("ctrl must no be null.");
}
if (values.length > 7) {
throw new IllegalArgumentException("ctrl must have 7 or fewer elements.");
}
if (ac < 0 || ac > 9) {
throw new IllegalArgumentException("ac must be non-negative and less than 9.");
}
// Pad command values and convert to bytes
int i;
int cur = 0;
ByteBuffer bb = ByteBuffer.allocate(26);
bb.order(ByteOrder.LITTLE_ENDIAN);
for (i = 0; i < 6; ++i) {
if (i == 4) {
if (i >= values.length) {
bb.put(cur, (byte) -1);
} else {
bb.put(cur, (byte) values[i]);
}
cur += 1;
} else if (i >= values.length) {
bb.putFloat(cur, -998);
cur += 4;
} else {
bb.putFloat(cur, values[i]);
cur += 4;
}
}
bb.put(cur++, (byte) ac);
bb.putFloat(cur, values.length == 7 ? values[6] : -998);
// 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());
}
/**
* Gets position information for the specified airplane.
*
* @param ac The aircraft to get position information for.
* @return An array containing control surface data in the same format as
* {@code sendPOSI}.
* @throws IOException If the command cannot be sent or a response cannot be
* read.
*/
public double[] getPOSI(int ac) throws IOException {
// Send request
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write("GETP".getBytes(StandardCharsets.UTF_8));
os.write(0xFF); // Placeholder for message length
os.write(ac);
sendUDP(os.toByteArray());
// Read response
byte[] data = readUDP();
if (data.length == 0) {
throw new IOException("No response received.");
}
if (data.length < 34) {
throw new IOException("Response too short");
}
// Parse response
double[] result = new double[7];
ByteBuffer bb = ByteBuffer.wrap(data);
bb.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < 7; ++i) {
result[i] = bb.getFloat(6 + 4 * i);
}
return result;
}
/**
* Sets the position of the player ac.
*
* @param values
* <p>
* An array containing position elements as follows:
* </p>
* <ol>
* <li>Latitude (deg)</li>
* <li>Longitude (deg)</li>
* <li>Altitude (m above MSL)</li>
* <li>Roll (deg)</li>
* <li>Pitch (deg)</li>
* <li>True Heading (deg)</li>
* <li>Gear (0=up, 1=down)</li>
* </ol>
* <p>
* 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.
* </p>
* @throws IOException If the command can not be sent.
*/
public void sendPOSI(double[] values) throws IOException {
sendPOSI(values, 0);
}
/**
* Sets the position of the specified ac with double precision coordinates.
*
* @param values
* <p>
* An array containing position elements as follows:
* </p>
* <ol>
* <li>Latitude (deg)</li>
* <li>Longitude (deg)</li>
* <li>Altitude (m above MSL)</li>
* <li>Roll (deg)</li>
* <li>Pitch (deg)</li>
* <li>True Heading (deg)</li>
* <li>Gear (0=up, 1=down)</li>
* </ol>
* <p>
* 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.
* </p>
* @param ac The ac to set. 0 for the player ac.
* @throws IOException If the command can not be sent.
*/
public void sendPOSI(double[] values, int ac) throws IOException {
// Preconditions
if (values == null) {
throw new IllegalArgumentException("posi must no be null.");
}
if (values.length > 7) {
throw new IllegalArgumentException("posi must have 7 or fewer elements.");
}
if (ac < 0 || ac > 255) {
throw new IllegalArgumentException("ac must be between 0 and 255.");
}
// Pad command values and convert to bytes
int i;
ByteBuffer bb = ByteBuffer.allocate(40);
bb.order(ByteOrder.LITTLE_ENDIAN);
for (i = 0; i < values.length; ++i) {
if (i < 3) /* lat/lon/height as double */
{
bb.putDouble(values[i]);
} else {
bb.putFloat((float) values[i]);
}
}
for (; i < 7; ++i) {
bb.putFloat(-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(ac);
os.write(bb.array());
sendUDP(os.toByteArray());
}
/**
* Reads X-Plane data
*
* @return The data read.
* @throws IOException If the read operation fails.
*/
public float[][] readData() throws IOException {
byte[] buffer = readUDP();
ByteBuffer bb = ByteBuffer.wrap(buffer);
int cur = 5;
int len = bb.get(cur++);
float[][] result = new float[bb.get(len)][9];
for (int i = 0; i < len; ++i) {
for (int j = 0; j < 9; ++j) {
result[i][j] = bb.getFloat(cur);
cur += 4;
}
}
return result;
}
/**
* Sends data to X-Plane
*
* @param data The data to send.
* @throws IOException If the command cannot be sent.
*/
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());
}
/**
* Selects what data X-Plane will export over UDP.
*
* @param rows The row numbers to select.
* @throws IOException If the command cannot be sent.
*/
public void selectDATA(int[] rows) throws IOException {
// Preconditions
if (rows == null || rows.length == 0) {
throw new IllegalArgumentException("rows must be a non-null, non-empty array.");
}
// Convert data to bytes
ByteBuffer bb = ByteBuffer.allocate(4 * rows.length);
bb.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < rows.length; ++i) {
bb.putInt(i * 4, rows[i]);
}
// Build and send message
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write("DSEL".getBytes(StandardCharsets.UTF_8));
os.write(0xFF); // Placeholder for message length
os.write(bb.array());
sendUDP(os.toByteArray());
}
/**
* Sets a message to be displayed on the screen in X-Plane at the default screen
* location.
*
* @param msg The message to display. Should not contain any newline characters.
* @throws IOException If the command cannot be sent.
*/
public void sendTEXT(String msg) throws IOException {
sendTEXT(msg, -1, -1);
}
/**
* Sets a message to be displayed on the screen in X-Plane at the specified
* coordinates.
*
* @param msg The message to display. Should not contain any newline characters.
* @param x The number of pixels from the right edge of the screen to display
* the text.
* @param y The number of pixels from the bottom edge of the screen to display
* the text.
* @throws IOException If the command cannot be sent.
*/
public void sendTEXT(String msg, int x, int y) throws IOException {
// Preconditions
if (msg == null) {
msg = "";
}
// Convert drefs to bytes.
byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8);
if (msgBytes.length > 255) {
throw new IllegalArgumentException("msg must be less than 255 bytes in UTF-8.");
}
ByteBuffer bb = ByteBuffer.allocate(8);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.putInt(0, x);
bb.putInt(4, y);
// Build and send message
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write("TEXT".getBytes(StandardCharsets.UTF_8));
os.write(0xFF); // Placeholder for message length
os.write(bb.array());
os.write(msgBytes.length);
os.write(msgBytes);
sendUDP(os.toByteArray());
}
/**
* Sets the camera view in X-Plane.
*
* @param view The view to use.
* @throws IOException If the command cannot be sent.
*/
public void sendVIEW(ViewType view) throws IOException {
ByteBuffer bb = ByteBuffer.allocate(4);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.putInt(view.getValue());
// Build and send message
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write("VIEW".getBytes(StandardCharsets.UTF_8));
os.write(0xFF); // Placeholder for message length
os.write(bb.array());
sendUDP(os.toByteArray());
}
/**
* Adds, removes, or clears a set of waypoints. If the command is clear, the
* points are ignored
* and all points are removed.
*
* @param op The operation to perform.
* @param points An array of values representing points. Each triplet in the
* array will be
* interpreted as a (Lat, Lon, Alt) point.
* @throws IOException If the command cannot be sent.
*/
public void sendWYPT(WaypointOp op, float[] points) throws IOException {
// Preconditions
if (points.length % 3 != 0) {
throw new IllegalArgumentException("points.length should be divisible by 3.");
}
if (points.length / 3 > 255) {
throw new IllegalArgumentException("Too many points. Must be less than 256.");
}
// Convert points to bytes
ByteBuffer bb = ByteBuffer.allocate(4 * points.length);
bb.order(ByteOrder.LITTLE_ENDIAN);
for (float f : points) {
bb.putFloat(f);
}
// Build and send message
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write("WYPT".getBytes(StandardCharsets.UTF_8));
os.write(0xFF); // Placeholder for message length
os.write(op.getValue());
os.write(points.length / 3);
os.write(bb.array());
sendUDP(os.toByteArray());
}
/**
* Send a command to X-Plane.
*
* @param comm The name of the X-Plane command to send.
* @throws IOException If the command cannot be sent.
*/
public void sendCOMM(String comm) throws IOException {
// Preconditions
if (comm == null || comm.length() == 0) {
throw new IllegalArgumentException(("comm must be non-empty."));
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write("COMM".getBytes(StandardCharsets.UTF_8));
os.write(0xFF); // Placeholder for message length
// Convert comm to bytes.
byte[] commBytes = comm.getBytes(StandardCharsets.UTF_8);
if (commBytes.length == 0) {
throw new IllegalArgumentException("COMM is an empty string!");
}
if (commBytes.length > 255) {
throw new IllegalArgumentException(
"comm must be less than 255 bytes in UTF-8. Are you sure this is a valid comm?");
}
// Build and send message
os.write(commBytes.length);
os.write(commBytes);
sendUDP(os.toByteArray());
}
/**
* Sets the port on which the client will receive data from X-Plane.
*
* @param port The new incoming port number.
* @throws IOException If the command cannot be sent.
*/
public void setCONN(int port) throws IOException {
if (port < 0 || port >= 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) port);
os.write((byte) (port >> 8));
sendUDP(os.toByteArray());
int soTimeout = socket.getSoTimeout();
socket.close();
socket = new DatagramSocket(port);
socket.setSoTimeout(soTimeout);
readUDP(); // Try to read response
}
}