diff --git a/Python3/src/basicExample.py b/Python3/src/basicExample.py new file mode 100644 index 0000000..f381e5d --- /dev/null +++ b/Python3/src/basicExample.py @@ -0,0 +1,72 @@ +from time import sleep +import xpc + +def ex(): + print "X-Plane Connect example script" + print "Setting up simulation" + with xpc.XPlaneConnect() as client: + # Verify connection + try: + # If X-Plane does not respond to the request, a timeout error + # will be raised. + client.getDREF("sim/test/test_float") + except: + print "Error establishing connection to X-Plane." + print "Exiting..." + return + + # Set position of the player aircraft + print "Setting position" + # Lat Lon Alt Pitch Roll Yaw Gear + posi = [37.524, -122.06899, 2500, 0, 0, 0, 1] + client.sendPOSI(posi) + + # Set position of a non-player aircraft + print "Setting NPC position" + # Lat Lon Alt Pitch Roll Yaw Gear + posi = [37.52465, -122.06899, 2500, 0, 20, 0, 1] + client.sendPOSI(posi, 1) + + # Set angle of attack, velocity, and orientation using the DATA command + print "Setting orientation" + data = [\ + [18, 0, -998, 0, -998, -998, -998, -998, -998],\ + [ 3, 130, 130, 130, 130, -998, -998, -998, -998],\ + [16, 0, 0, 0, -998, -998, -998, -998, -998]\ + ] + client.sendDATA(data) + + # Set control surfaces and throttle of the player aircraft using sendCTRL + print "Setting controls" + ctrl = [0.0, 0.0, 0.0, 0.8] + client.sendCTRL(ctrl) + + # Pause the sim + print "Pausing" + client.pauseSim(True) + sleep(2) + + # Toggle pause state to resume + print "Resuming" + client.pauseSim(False) + + # Stow landing gear using a dataref + print "Stowing gear" + gear_dref = "sim/cockpit/switches/gear_handle_status" + client.sendDREF(gear_dref, 0) + + # Let the sim run for a bit. + sleep(4) + + # Make sure gear was stowed successfully + gear_status = client.getDREF(gear_dref) + if gear_status[0] == 0: + print "Gear stowed" + else: + print "Error stowing gear" + + print "End of Python client example" + raw_input("Press any key to exit...") + +if __name__ == "__main__": + ex() \ No newline at end of file diff --git a/Python3/src/monitorExample.py b/Python3/src/monitorExample.py new file mode 100644 index 0000000..c4cdb48 --- /dev/null +++ b/Python3/src/monitorExample.py @@ -0,0 +1,16 @@ +import sys + +import xpc + +def monitor(): + with xpc.XPlaneConnect() as client: + while True: + posi = client.getPOSI(); + ctrl = client.getCTRL(); + + print "Loc: (%4f, %4f, %4f) Aileron:%2f Elevator:%2f Rudder:%2f\n"\ + % (posi[0], posi[1], posi[2], ctrl[1], ctrl[0], ctrl[2]) + + +if __name__ == "__main__": + monitor() \ No newline at end of file diff --git a/Python3/src/playbackExample.py b/Python3/src/playbackExample.py new file mode 100644 index 0000000..dfa4f52 --- /dev/null +++ b/Python3/src/playbackExample.py @@ -0,0 +1,82 @@ +from time import sleep +import xpc + +def record(path, interval = 0.1, duration = 60): + try: + fd = open(path, "w") + except: + print "Unable to open file." + return + + count = int(duration / interval) + if count < 1: + print "duration is less than a single frame." + return + + with xpc.XPlaneConnect("localhost", 49009, 0, 1000) as client: + print "Recording..." + for i in range(0, count): + try: + posi = client.getPOSI() + fd.write("{0}, {1}, {2}, {3}, {4}, {5}, {6}\n".format(*posi)) + except: + print "Error reading position" + continue + sleep(interval); + print "Recording Complete" + fd.close() + +def playback(path, interval): + try: + fd = open(path, "r") + except: + print "Unable to open file." + return + + with xpc.XPlaneConnect("localhost", 49009, 0, 1000) as client: + print "Starting Playback..." + for line in fd: + try: + posi = [ float(x) for x in line.split(',') ] + posi = client.sendPOSI(posi) + except: + print "Error sending position" + continue + sleep(interval); + print "Playback Complete" + fd.close() + +def printMenu(title, opts): + print "\n+---------------------------------------------- +" + print "| {0:42} |\n".format(title) + print "+---------------------------------------------- +" + for i in range(0,len(opts)): + print "| {0:2}. {1:40} |".format(i + 1, opts[i]) + print "+---------------------------------------------- +" + return int(raw_input("Please select and option: ")) + +def ex(): + print "X-Plane Connect Playback Example [Version 1.2.0]" + print "(c) 2013-2015 United States Government as represented by the Administrator" + print "of the National Aeronautics and Space Administration. All Rights Reserved." + + mainOpts = [ "Record X-Plane", "Playback File", "Exit" ] + + while True: + opt = printMenu("What would you like to do?", mainOpts) + if opt == 1: + path = raw_input("Enter save file path: ") + interval = float(raw_input("Enter interval between frames (seconds): ")) + duration = float(raw_input("Enter duration to record for (seconds): ")) + record(path, interval, duration) + elif opt == 2: + path = raw_input("Enter save file path: ") + interval = float(raw_input("Enter interval between frames (seconds): ")) + playback(path, interval) + elif opt == 3: + return; + else: + print "Unrecognized option." + +if __name__ == "__main__": + ex() \ No newline at end of file diff --git a/Python3/src/xpc.py b/Python3/src/xpc.py new file mode 100644 index 0000000..ddf2be9 --- /dev/null +++ b/Python3/src/xpc.py @@ -0,0 +1,437 @@ +import socket +import struct + + +class XPlaneConnect(object): + """XPlaneConnect (XPC) facilitates communication to and from the XPCPlugin.""" + socket = None + + # Basic Functions + def __init__(self, xpHost='localhost', xpPort=49009, port=0, timeout=100): + """Sets up a new connection to an X-Plane Connect plugin running in X-Plane. + + Args: + xpHost: The hostname of the machine running X-Plane. + xpPort: The port on which the XPC plugin is listening. Usually 49007. + port: The port which will be used to send and receive data. + timeout: The period (in milliseconds) after which read attempts will fail. + """ + + # Validate parameters + xpIP = None + try: + xpIP = socket.gethostbyname(xpHost) + except: + raise ValueError("Unable to resolve xpHost.") + + if xpPort < 0 or xpPort > 65535: + raise ValueError("The specified X-Plane port is not a valid port number.") + if port < 0 or port > 65535: + raise ValueError("The specified port is not a valid port number.") + if timeout < 0: + raise ValueError("timeout must be non-negative.") + + # Setup XPlane IP and port + self.xpDst = (xpIP, xpPort) + + # Create and bind socket + clientAddr = ("0.0.0.0", port) + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + self.socket.bind(clientAddr) + timeout /= 1000.0 + self.socket.settimeout(timeout) + + def __del__(self): + self.close() + + # Define __enter__ and __exit__ to support the `with` construct. + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def close(self): + """Closes the specified connection and releases resources associated with it.""" + if self.socket is not None: + self.socket.close() + self.socket = None + + def sendUDP(self, buffer): + """Sends a message over the underlying UDP socket.""" + # Preconditions + if(len(buffer) == 0): + raise ValueError("sendUDP: buffer is empty.") + + self.socket.sendto(buffer, 0, self.xpDst) + + def readUDP(self): + """Reads a message from the underlying UDP socket.""" + return self.socket.recv(16384) + + # Configuration + def setCONN(self, port): + """Sets the port on which the client sends and receives data. + + Args: + port: The new port to use. + """ + + #Validate parameters + if port < 0 or port > 65535: + raise ValueError("The specified port is not a valid port number.") + + #Send command + buffer = struct.pack(b"<4sxH", b"CONN", port) + self.sendUDP(buffer) + + #Rebind socket + clientAddr = ("0.0.0.0", port) + timeout = self.socket.gettimeout() + self.socket.close() + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + self.socket.bind(clientAddr) + self.socket.settimeout(timeout) + + #Read response + buffer = self.socket.recv(1024) + + def pauseSim(self, pause): + """Pauses or un-pauses the physics simulation engine in X-Plane. + + Args: + pause: True to pause the simulation; False to resume. + """ + pause = int(pause) + if pause < 0 or pause > 2: + raise ValueError("Invalid argument for pause command.") + + buffer = struct.pack(b"<4sxB", b"SIMU", pause) + self.sendUDP(buffer) + + # X-Plane UDP Data + def readDATA(self): + """Reads X-Plane data. + + Returns: A 2 dimensional array containing 0 or more rows of data. Each array + in the result will have 9 elements, the first of which is the row number which + that array represents data for, and the rest of which are the data elements in + that row. + """ + buffer = self.readUDP() + if len(buffer) < 6: + return None + rows = (len(buffer) - 5) / 36 + data = [] + for i in range(rows): + data.append(struct.unpack_from(b"9f", buffer, 5 + 36*i)) + return data + + def sendDATA(self, data): + """Sends X-Plane data over the underlying UDP socket. + + Args: + data: An array of values representing data rows to be set. Each array in `data` + should have 9 elements, the first of which is a row number in the range (0-134), + and the rest of which are the values to set for that data row. + """ + if len(data) > 134: + raise ValueError("Too many rows in data.") + + buffer = struct.pack(b"<4sx", b"DATA") + for row in data: + if len(row) != 9: + raise ValueError("Row does not contain exactly 9 values. <" + str(row) + ">") + buffer += struct.pack(b" 7: + raise ValueError("Must have between 0 and 7 items in values.") + if ac < 0 or ac > 20: + raise ValueError("Aircraft number must be between 0 and 20.") + + # Pack message + buffer = struct.pack(b"<4sxB", b"POSI", ac) + for i in range(7): + val = -998 + if i < len(values): + val = values[i] + buffer += struct.pack(b" 7: + raise ValueError("Must have between 0 and 6 items in values.") + if ac < 0 or ac > 20: + raise ValueError("Aircraft number must be between 0 and 20.") + + # Pack message + buffer = struct.pack(b"<4sx", b"CTRL") + for i in range(6): + val = -998 + if i < len(values): + val = values[i] + if i == 4: + val = -1 if (abs(val + 998) < 1e-4) else val + buffer += struct.pack(b"b", val) + else: + buffer += struct.pack(b" 255: + raise ValueError("dref must be a non-empty string less than 256 characters.") + + if value is None: + raise ValueError("value must be a scalar or sequence of floats.") + + # Pack message + if hasattr(value, "__len__"): + if len(value) > 255: + raise ValueError("value must have less than 256 items.") + fmt = " ViewType.FullscreenNoHud: + raise ValueError("Unknown view command.") + + # Pack buffer + buffer = struct.pack(b"<4sxi", b"VIEW", view) + + # Send message + self.sendUDP(buffer) + + def sendWYPT(self, op, points): + """Adds, removes, or clears waypoints. Waypoints are three dimensional points on or + above the Earth's surface that are represented visually in the simulator. Each + point consists of a latitude and longitude expressed in fractional degrees and + an altitude expressed as meters above sea level. + + Args: + op: The operation to perform. Pass `1` to add waypoints, + `2` to remove waypoints, and `3` to clear all waypoints. + points: A sequence of floating point values representing latitude, longitude, and + altitude triples. The length of this array should always be divisible by 3. + """ + if op < 1 or op > 3: + raise ValueError("Invalid operation specified.") + if len(points) % 3 != 0: + raise ValueError("Invalid points. Points should be divisible by 3.") + if len(points) / 3 > 255: + raise ValueError("Too many points. You can only send 255 points at a time.") + + if op == 3: + buffer = struct.pack(b"<4sxBB", b"WYPT", 3, 0) + else: + buffer = struct.pack(("<4sxBB" + str(len(points)) + "f").encode(), b"WYPT", op, len(points), *points) + self.sendUDP(buffer) + + +class ViewType(object): + 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 diff --git a/Python3/xplaneConnect.pyproj b/Python3/xplaneConnect.pyproj new file mode 100644 index 0000000..446a9ff --- /dev/null +++ b/Python3/xplaneConnect.pyproj @@ -0,0 +1,58 @@ + + + + Debug + 2.0 + 3c7a940d-17c8-4e91-882f-9bc8b1d2f54b + . + src\basicExample.py + + + . + . + XPlaneConnect + XPlaneConnect + {9a7a9026-48c1-4688-9d5d-e5699d47d074} + 2.7 + + + true + false + + + true + false + + + + Code + + + Code + + + + Code + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets + + + + + + + + + + \ No newline at end of file diff --git a/Python3/xplaneConnect.sln b/Python3/xplaneConnect.sln new file mode 100644 index 0000000..862da8d --- /dev/null +++ b/Python3/xplaneConnect.sln @@ -0,0 +1,24 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.31101.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "XPlaneConnect", "XPlaneConnect.pyproj", "{3C7A940D-17C8-4E91-882F-9BC8B1D2F54B}" +EndProject +Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Tests", "..\TestScripts\Python Tests\Tests.pyproj", "{6931EBB2-4E01-4C5A-86B6-668C0E75051B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3C7A940D-17C8-4E91-882F-9BC8B1D2F54B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C7A940D-17C8-4E91-882F-9BC8B1D2F54B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6931EBB2-4E01-4C5A-86B6-668C0E75051B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6931EBB2-4E01-4C5A-86B6-668C0E75051B}.Release|Any CPU.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal