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