Added Python3 compatible xpc client. (#156)

This commit is contained in:
Roman Geber
2019-06-13 20:45:38 +02:00
committed by Jason Watkins
parent 3120e32e47
commit b98ad22262
6 changed files with 689 additions and 0 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

437
Python3/src/xpc.py Normal file
View File

@@ -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"<I8f", *row)
self.sendUDP(buffer)
# Position
def getPOSI(self, ac=0):
"""Gets position information for the specified aircraft.
Args:
ac: The aircraft to get the position of. 0 is the main/player aircraft.
"""
# Send request
buffer = struct.pack(b"<4sxB", b"GETP", ac)
self.sendUDP(buffer)
# Read response
resultBuf = self.readUDP()
if len(resultBuf) != 34:
raise ValueError("Unexpected response length.")
result = struct.unpack(b"<4sxBfffffff", resultBuf)
if result[0] != b"POSI":
raise ValueError("Unexpected header: " + result[0])
# Drop the header & ac from the return value
return result[2:]
def sendPOSI(self, values, ac=0):
"""Sets position information on the specified aircraft.
Args:
values: The position values to set. `values` is a array containing up to
7 elements. If less than 7 elements are specified or any elment is set to `-998`,
those values will not be changed. The elements in `values` corespond to the
following:
* Latitude (deg)
* Longitude (deg)
* Altitude (m above MSL)
* Pitch (deg)
* Roll (deg)
* True Heading (deg)
* Gear (0=up, 1=down)
ac: The aircraft to set the position of. 0 is the main/player aircraft.
"""
# Preconditions
if len(values) < 1 or len(values) > 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"<f", val)
# Send
self.sendUDP(buffer)
# Controls
def getCTRL(self, ac=0):
"""Gets the control surface information for the specified aircraft.
Args:
ac: The aircraft to get the control surfaces of. 0 is the main/player aircraft.
"""
# Send request
buffer = struct.pack(b"<4sxB", b"GETC", ac)
self.sendUDP(buffer)
# Read response
resultBuf = self.readUDP()
if len(resultBuf) != 31:
raise ValueError("Unexpected response length.")
result = struct.unpack(b"<4sxffffbfBf", resultBuf)
if result[0] != b"CTRL":
raise ValueError("Unexpected header: " + result[0])
# Drop the header from the return value
result =result[1:7] + result[8:]
return result
def sendCTRL(self, values, ac=0):
"""Sets control surface information on the specified aircraft.
Args:
values: The control surface values to set. `values` is a array containing up to
6 elements. If less than 6 elements are specified or any elment is set to `-998`,
those values will not be changed. The elements in `values` corespond to the
following:
* Latitudinal Stick [-1,1]
* Longitudinal Stick [-1,1]
* Rudder Pedals [-1, 1]
* Throttle [-1, 1]
* Gear (0=up, 1=down)
* Flaps [0, 1]
* Speedbrakes [-0.5, 1.5]
ac: The aircraft to set the control surfaces of. 0 is the main/player aircraft.
"""
# Preconditions
if len(values) < 1 or len(values) > 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"<f", val)
buffer += struct.pack(b"B", ac)
if len(values) == 7:
buffer += struct.pack(b"<f", values[6])
# Send
self.sendUDP(buffer)
# DREF Manipulation
def sendDREF(self, dref, values):
"""Sets the specified dataref to the specified value.
Args:
dref: The name of the datarefs to set.
values: Either a scalar value or a sequence of values.
"""
self.sendDREFs([dref], [values])
def sendDREFs(self, drefs, values):
"""Sets the specified datarefs to the specified values.
Args:
drefs: A list of names of the datarefs to set.
values: A list of scalar or vector values to set.
"""
if len(drefs) != len(values):
raise ValueError("drefs and values must have the same number of elements.")
buffer = struct.pack(b"<4sx", b"DREF")
for i in range(len(drefs)):
dref = drefs[i]
value = values[i]
# Preconditions
if len(dref) == 0 or len(dref) > 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 = "<B{0:d}sB{1:d}f".format(len(dref), len(value))
buffer += struct.pack(fmt.encode(), len(dref), dref.encode(), len(value), value)
else:
fmt = "<B{0:d}sBf".format(len(dref))
buffer += struct.pack(fmt.encode(), len(dref), dref.encode(), 1, value)
# Send
self.sendUDP(buffer)
def getDREF(self, dref):
"""Gets the value of an X-Plane dataref.
Args:
dref: The name of the dataref to get.
Returns: A sequence of data representing the values of the requested dataref.
"""
return self.getDREFs([dref])[0]
def getDREFs(self, drefs):
"""Gets the value of one or more X-Plane datarefs.
Args:
drefs: The names of the datarefs to get.
Returns: A multidimensional sequence of data representing the values of the requested
datarefs.
"""
# Send request
buffer = struct.pack(b"<4sxB", b"GETD", len(drefs))
for dref in drefs:
fmt = "<B{0:d}s".format(len(dref))
buffer += struct.pack(fmt.encode(), len(dref), dref.encode())
self.sendUDP(buffer)
# Read and parse response
buffer = self.readUDP()
resultCount = struct.unpack_from(b"B", buffer, 5)[0]
offset = 6
result = []
for i in range(resultCount):
rowLen = struct.unpack_from(b"B", buffer, offset)[0]
offset += 1
fmt = "<{0:d}f".format(rowLen)
row = struct.unpack_from(fmt.encode(), buffer, offset)
result.append(row)
offset += rowLen * 4
return result
# Drawing
def sendTEXT(self, msg, x=-1, y=-1):
"""Sets a message that X-Plane will display on the screen.
Args:
msg: The string to display on the screen
x: The distance in pixels from the left edge of the screen to display the
message. A value of -1 indicates that the default horizontal position should
be used.
y: The distance in pixels from the bottom edge of the screen to display the
message. A value of -1 indicates that the default vertical position should be
used.
"""
if y < -1:
raise ValueError("y must be greater than or equal to -1.")
if msg == None:
msg = ""
msgLen = len(msg)
# TODO: Multiple byte conversions
buffer = struct.pack(b"<4sxiiB" + (str(msgLen) + "s").encode(), b"TEXT", x, y, msgLen, msg.encode())
self.sendUDP(buffer)
def sendVIEW(self, view):
"""Sets the camera view in X-Plane
Args:
view: The view to use. The ViewType class provides named constants
for known views.
"""
# Preconditions
if view < ViewType.Forwards or view > 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