source: xplra/src/client/python/xplra.py@ 40:ec5dde8a6ff6

Last change on this file since 40:ec5dde8a6ff6 was 40:ec5dde8a6ff6, checked in by István Váradi <ivaradi@…>, 11 years ago

Implemented the client support for the new commands and updated the basic test programs with tests showing messages

File size: 22.7 KB
Line 
1# X-Plane Remote Access client module
2#-------------------------------------------------------------------------------
3
4import os
5import struct
6import array
7
8#-------------------------------------------------------------------------------
9
10COMMAND_GET_SINGLE = 0x01
11
12COMMAND_SET_SINGLE = 0x02
13
14COMMAND_GET_MULTI = 0x03
15
16COMMAND_SET_MULTI = 0x04
17
18COMMAND_REGISTER_GET_MULTI = 0x11
19
20COMMAND_UNREGISTER_GET_MULTI = 0x12
21
22COMMAND_EXECUTE_GET_MULTI = 0x13
23
24COMMAND_REGISTER_SET_MULTI = 0x21
25
26COMMAND_UNREGISTER_SET_MULTI = 0x22
27
28COMMAND_EXECUTE_SET_MULTI = 0x23
29
30COMMAND_GET_VERSIONS = 0x31
31
32COMMAND_RELOAD_PLUGINS = 0x32
33
34COMMAND_SHOW_MESSAGE = 0x41
35
36TYPE_INT = 0x01
37
38TYPE_FLOAT = 0x02
39
40TYPE_DOUBLE = 0x03
41
42TYPE_FLOAT_ARRAY = 0x11
43
44TYPE_INT_ARRAY = 0x12
45
46TYPE_BYTE_ARRAY = 0x13
47
48RESULT_OK = 0x00
49
50RESULT_INVALID_COMMAND = 0x01
51
52RESULT_UNKNOWN_DATAREF = 0x02
53
54RESULT_INVALID_TYPE = 0x03
55
56RESULT_INVALID_LENGTH = 0x04
57
58RESULT_INVALID_OFFSET = 0x05
59
60RESULT_INVALID_COUNT = 0x06
61
62RESULT_INVALID_ID = 0x07
63
64RESULT_INVALID_DURATION = 0x08
65
66RESULT_OTHER_ERROR = 0xff
67
68#-------------------------------------------------------------------------------
69
70class ProtocolException(Exception):
71 """Exception to signify protocol errors."""
72 _message = { RESULT_INVALID_COMMAND : "invalid command",
73 RESULT_UNKNOWN_DATAREF : "unknown dataref",
74 RESULT_INVALID_TYPE : "invalid type",
75 RESULT_INVALID_LENGTH : "invalid length",
76 RESULT_INVALID_OFFSET : "invalid offset",
77 RESULT_INVALID_COUNT : "invalid count",
78 RESULT_INVALID_ID : "invalid ID",
79 RESULT_INVALID_DURATION : "invalid duration",
80 RESULT_OTHER_ERROR : "other error" }
81
82 @staticmethod
83 def getMessage(resultCode):
84 """Get the message for the given result code."""
85 if resultCode in ProtocolException._message:
86 return ProtocolException._message[resultCode]
87 else:
88 return "unknown error code " + `resultCode`
89
90 def __init__(self, resultCode, parameter = None):
91 message = "xplra.ProtocolException: " + self.getMessage(resultCode)
92 if parameter is not None:
93 if resultCode==RESULT_UNKNOWN_DATAREF:
94 message += " (# %d)" % (parameter,)
95
96 super(ProtocolException, self).__init__(message)
97 self.resultCode = resultCode
98 self.parameter = parameter
99
100#-------------------------------------------------------------------------------
101
102class XPlane(object):
103 """The main class representing the connection to X-Plane."""
104
105 @staticmethod
106 def _packLength(x):
107 """Pack the given integer as a variable-length value."""
108 s = ""
109 while True:
110 y = x&0x7f
111 x >>= 7
112 if x>0: y |= 0x80
113 s += struct.pack("B", y)
114 if x==0:
115 return s
116
117 @staticmethod
118 def _packString(s):
119 """Pack the given string."""
120 return XPlane._packLength(len(s)) + s
121
122 def __init__(self):
123 """Construct the object."""
124 self._stream = None
125
126 def connect(self):
127 """Try to connect to X-Plane."""
128 if self._stream is not None:
129 return
130
131 if os.name=="nt":
132 self._stream = open(r'\\.\pipe\\xplra', "b")
133 else:
134 import socket
135 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
136 s.connect("/tmp/xplra-" + os.environ["LOGNAME"])
137 self._stream = s.makefile()
138
139 @property
140 def isConnected(self):
141 """Determine if we are connected to the simulator."""
142 return self._stream is not None
143
144 def createMultiGetter(self):
145 """Create a new multi-dataref getter for this X-Plane object."""
146 return MultiGetter(self)
147
148 def createMultiSetter(self):
149 """Create a new multi-dataref setter for this X-Plane object."""
150 return MultiSetter(self)
151
152 def getVersions(self):
153 """Get the versions of X-Plane, XPLM and XPLRA as a tuple."""
154 self._writeU8(COMMAND_GET_VERSIONS)
155 self._flush()
156 self._checkResult()
157 return (self._readS32(), self._readS32(), self._readS32())
158
159 def reloadPlugins(self):
160 """Reload the plugins in X-Plane.
161
162 After this, this connection becomes invalid."""
163 self._writeU8(COMMAND_RELOAD_PLUGINS)
164 self._flush()
165 self._checkResult();
166
167 def getInt(self, name):
168 """Get the value of the integer dataref with the given name."""
169 return self._getSingle(name, TYPE_INT)
170
171 def getFloat(self, name):
172 """Get the value of the float dataref with the given name."""
173 return self._getSingle(name, TYPE_FLOAT)
174
175 def getDouble(self, name):
176 """Get the value of the double dataref with the given name."""
177 return self._getSingle(name, TYPE_DOUBLE)
178
179 def getFloatArray(self, name, length = -1, offset = 0):
180 """Get the value of the float array dataref with the given name."""
181 return self._getSingle(name, TYPE_FLOAT_ARRAY, length, offset)
182
183 def getIntArray(self, name, length = -1, offset = 0):
184 """Get the value of the integer array dataref with the given name."""
185 return self._getSingle(name, TYPE_INT_ARRAY, length, offset)
186
187 def getByteArray(self, name, length = -1, offset = 0):
188 """Get the value of the byte array dataref with the given name."""
189 return self._getSingle(name, TYPE_BYTE_ARRAY, length, offset)
190
191 def getString(self, name, offset = 0):
192 """Get the value of the byte array dataref with the given name as a
193 string."""
194 s = ""
195 for b in self.getByteArray(name, offset = offset):
196 if b==0: break
197 s += chr(b)
198 return s
199
200 def setInt(self, name, value):
201 """Set the value of the integer dataref with the given name."""
202 self._setSingle(name, TYPE_INT, value)
203
204 def setFloat(self, name, value):
205 """Set the value of the float dataref with the given name."""
206 self._setSingle(name, TYPE_FLOAT, value)
207
208 def setDouble(self, name, value):
209 """Set the value of the double dataref with the given name."""
210 self._setSingle(name, TYPE_DOUBLE, value)
211
212 def setFloatArray(self, name, value, offset = 0):
213 """Set the value of the float array dataref with the given name."""
214 self._setSingle(name, TYPE_FLOAT_ARRAY, value, offset = offset)
215
216 def setIntArray(self, name, value, offset = 0):
217 """Set the value of the integer array dataref with the given name."""
218 self._setSingle(name, TYPE_INT_ARRAY, value, offset = offset)
219
220 def setByteArray(self, name, value, offset = 0):
221 """Set the value of the byte array dataref with the given name."""
222 self._setSingle(name, TYPE_BYTE_ARRAY, value, offset = offset)
223
224 def setString(self, name, value, length, offset = 0):
225 """Set the value of the byte array dataref with the given name from a
226 string.
227
228 The string will be padded with 0's so that its length is the given
229 one."""
230 value = [ord(c) for c in value[:length]]
231 value += [0] * (length - len(value))
232 self.setByteArray(name, value, offset)
233
234 def showMessage(self, message, duration):
235 """Show a message in the simulator window for the given duration.
236
237 The duration is a floating-point number denoting seconds."""
238 self._writeU8(COMMAND_SHOW_MESSAGE)
239 self._writeString(message)
240 self._writeFloat(duration)
241 self._flush()
242 self._checkResult()
243
244 def disconnect(self):
245 """Disconnect from X-Plane."""
246 if self._stream is not None:
247 self._stream.close()
248 self._stream = None
249
250 def _checkResult(self, resultCode = None, parameter = None, multi = False):
251 """Check the given result code.
252
253 If it is None, it will be read first.
254
255 If it is not RESULT_OK, a ProtocolException is thrown."""
256
257 if resultCode is None:
258 resultCode = self._readU8()
259 if multi and resultCode==RESULT_UNKNOWN_DATAREF:
260 parameter = self._readU32()
261
262 if resultCode!=RESULT_OK:
263 raise ProtocolException(resultCode, parameter)
264
265 def _getSingle(self, name, type, length = None, offset = None):
266 """Get the single value of the given name and type."""
267 self._writeU8(COMMAND_GET_SINGLE)
268 self._writeString(name)
269 self._writeU8(type)
270 if length is not None and offset is not None:
271 self._writeS32(length)
272 self._writeS32(offset)
273
274 self._flush()
275 self._checkResult()
276 return self._readValue(type)
277
278 def _readValue(self, type):
279 """Read the value of the given type from the stream."""
280 if type==TYPE_INT:
281 return self._readS32()
282 elif type==TYPE_FLOAT:
283 return self._readFloat()
284 elif type==TYPE_DOUBLE:
285 return self._readDouble()
286 else:
287 length = self._readS32()
288 arr = None
289 elementSize = 4
290 if type==TYPE_FLOAT_ARRAY:
291 arr = array.array("f")
292 elif type==TYPE_INT_ARRAY:
293 arr = array.array("i")
294 elif type==TYPE_BYTE_ARRAY:
295 arr = array.array("B")
296 elementSize = 1
297 if arr is not None and length>0:
298 arr.fromstring(self._stream.read(length*elementSize))
299 return None if arr is None else arr.tolist()
300
301 def _setSingle(self, name, type, value, offset = None):
302 """Set the single value of the given name and type."""
303 self._writeU8(COMMAND_SET_SINGLE)
304 self._writeString(name)
305 self._writeU8(type)
306 if offset is not None:
307 self._writeS32(len(value))
308 self._writeS32(offset)
309
310 self._writeValue(type, value)
311 self._flush()
312 self._checkResult()
313
314 def _writeValue(self, type, value):
315 """Write a value of the given type."""
316 if type==TYPE_INT:
317 self._writeS32(int(value))
318 elif type==TYPE_FLOAT:
319 self._writeFloat(float(value))
320 elif type==TYPE_DOUBLE:
321 self._writeDouble(float(value))
322 else:
323 arr = None
324 if type==TYPE_FLOAT_ARRAY:
325 arr = array.array("f")
326 elif type==TYPE_INT_ARRAY:
327 arr = array.array("i")
328 elif type==TYPE_BYTE_ARRAY:
329 arr = array.array("B")
330 arr.fromlist(value)
331 self._stream.write(arr.tostring())
332
333 def _writeU8(self, x):
334 """Write the given value as an unsigned, 8-bit value."""
335 self._stream.write(struct.pack("B", x))
336
337 def _writeS32(self, x):
338 """Write the given value as a signed, 32-bit value."""
339 self._stream.write(struct.pack("i", x))
340
341 def _writeU32(self, x):
342 """Write the given value as an unsigned, 32-bit value."""
343 self._stream.write(struct.pack("I", x))
344
345 def _writeFloat(self, x):
346 """Write the given value as a single-precision floating point."""
347 self._stream.write(struct.pack("f", x))
348
349 def _writeDouble(self, x):
350 """Write the given value as a double-precision floating point."""
351 self._stream.write(struct.pack("d", x))
352
353 def _writeLength(self, length):
354 """Write the given value is a variable-length value into our stream."""
355 self._stream.write(self._packLength(length))
356
357 def _writeString(self, str):
358 """Write the given string into the stream."""
359 self._stream.write(self._packString(str))
360
361 def _flush(self):
362 """Flush our stream."""
363 self._stream.flush()
364
365 def _readU8(self):
366 """Read an unsigned, 8-bit value from the stream."""
367 (value,) = struct.unpack("B", self._stream.read(1))
368 return value
369
370 def _readS32(self):
371 """Read a signed, 32-bit value from the stream."""
372 (value,) = struct.unpack("i", self._stream.read(4))
373 return value
374
375 def _readU32(self):
376 """Read an unsigned, 32-bit value from the stream."""
377 (value,) = struct.unpack("I", self._stream.read(4))
378 return value
379
380 def _readFloat(self):
381 """Read a single-precision floating point value from the stream."""
382 (value,) = struct.unpack("f", self._stream.read(4))
383 return value
384
385 def _readDouble(self):
386 """Read a double-precision floating point value from the stream."""
387 (value,) = struct.unpack("d", self._stream.read(8))
388 return value
389
390 def _readLength(self):
391 """Read a variable-length value from our stream."""
392 length = 0
393 while True:
394 (x,) = struct.unpack("B", self._stream.read(1))
395 length <<= 7
396 length |= x&0x7f
397 if x&0x80==0:
398 return length
399
400 def _readString(self):
401 """Read a string from our stream."""
402 length = self._readLength()
403 return self._stream.read(length)
404
405#-------------------------------------------------------------------------------
406
407class MultiBuffer(object):
408 """Buffer for querying or setting multi-dataref values."""
409 @staticmethod
410 def _getDefault(type, length):
411 """Get the default value for the given type."""
412 if type==TYPE_INT:
413 return 0
414 elif type==TYPE_FLOAT:
415 return float(0.0)
416 elif type==TYPE_DOUBLE:
417 return 0.0
418 elif length<=0:
419 return []
420 elif type==TYPE_FLOAT_ARRAY:
421 return [float(0.0)] * length
422 elif type==TYPE_INT_ARRAY or type==TYPE_BYTE_ARRAY:
423 return [0] * length
424
425 def __init__(self, xplane, registerCommand, unregisterCommand):
426 """Construct the buffer for the given XPlane instance and with the
427 given register/unregister command values."""
428 self._xplane = xplane
429 self._registerCommand = registerCommand
430 self._unregisterCommand = unregisterCommand
431
432 self._dataRefs = []
433 self._values = None
434
435 self._registeredID = None
436
437 @property
438 def values(self):
439 """Query the values as a list."""
440 if self._values is None:
441 self.finalize()
442 return self._values
443
444 def addInt(self, name):
445 """Add an integer to the buffer with the given name
446
447 Returns an ID (or index) of the dataref."""
448 return self._add(name, TYPE_INT)
449
450 def addFloat(self, name):
451 """Add a float to the buffer with the given name
452
453 Returns an ID (or index) of the dataref."""
454 return self._add(name, TYPE_FLOAT)
455
456 def addDouble(self, name):
457 """Add a double to the buffer with the given name
458
459 Returns an ID (or index) of the dataref."""
460 return self._add(name, TYPE_DOUBLE)
461
462 def addFloatArray(self, name, length = -1, offset = 0):
463 """Add a floating point array to the buffer with the given name.
464
465 Returns an ID (or index) of the dataref."""
466 return self._add(name, TYPE_FLOAT_ARRAY, length, offset)
467
468 def addIntArray(self, name, length = -1, offset = 0):
469 """Add an integer array to the buffer with the given name.
470
471 Returns an ID (or index) of the dataref."""
472 return self._add(name, TYPE_INT_ARRAY, length, offset)
473
474 def addByteArray(self, name, length = -1, offset = 0):
475 """Add a byte array to the buffer with the given name.
476
477 Returns an ID (or index) of the dataref."""
478 return self._add(name, TYPE_BYTE_ARRAY, length, offset)
479
480 def finalize(self):
481 """Finalize the buffer, if not finalized yet.
482
483 It initializes the array of values with some defaults.
484
485 Returns whether there is any data in the buffer."""
486 if self._values is None:
487 self._values = [self._getDefault(type, length)
488 for (_, type, length, _) in self._dataRefs]
489 return self._values!=[]
490
491 def register(self):
492 """Register the buffer in X-Plane."""
493 if self.finalize() and self._registeredID is None:
494 self._writeSpec(self._registerCommand)
495 self._registeredID = self._xplane._readU32()
496
497 def unregister(self):
498 """Unregister the buffer from X-Plane."""
499 if self._registeredID is not None:
500 self._xplane._writeU8(self._unregisterCommand)
501 self._xplane._writeU32(self._registeredID)
502 self._xplane._flush()
503 self._xplane._checkResult()
504 self._registeredID = None
505
506 def execute(self):
507 """Perform the querying or the setting of the values.
508
509 It first checks if the buffer is finalized. If not, it will be
510 finalized. However, if it is not finalized but also registered, it will
511 first be unregistered and the re-registered after finalizing."""
512 if self._values is None and self._registeredID is not None:
513 self.unregister()
514 self.register()
515 else:
516 self.finalize()
517
518 if self._registeredID is None:
519 self._executeUnregistered()
520 else:
521 self._executeRegistered()
522
523 def getString(self, id):
524 """Get the value of the dataref with the given ID as a string.
525
526 The dataref should be of type byte array."""
527 if self._dataRefs[id][1]!=TYPE_BYTE_ARRAY:
528 raise TypeError("xplra.MultiBuffer.getString: only byte arrays can be converted to strings")
529 if self._values is None:
530 self.finalize()
531 s = ""
532 for c in self._values[id]:
533 if c==0: break
534 s += chr(c)
535 return s
536
537 def _add(self, name, type, length = None, offset = None):
538 """Add a scalar to the buffer with the given name and type"""
539 index = len(self._dataRefs)
540 self._values = None
541 self._dataRefs.append( (name, type, length, offset) )
542 return index
543
544 def _writeSpec(self, command):
545 """Write the specification preceded by the given command and check for
546 the result.
547
548 The specification is basically the list of the datarefs."""
549 self._xplane._writeU8(command)
550 self._xplane._writeU32(len(self._dataRefs))
551
552 for (name, type, length, offset) in self._dataRefs:
553 self._xplane._writeString(name)
554 self._xplane._writeU8(type)
555 if length is not None and offset is not None:
556 self._xplane._writeS32(length)
557 self._xplane._writeS32(offset)
558
559 self._xplane._flush()
560 self._xplane._checkResult(multi = True)
561
562 def __len__(self):
563 """Get the number of value items in the buffer."""
564 if self._values is None:
565 self.finalize()
566 return len(self._values)
567
568 def __getitem__(self, id):
569 """Get the item with the given ID."""
570 if self._values is None:
571 self.finalize()
572 return self._values[id]
573
574 def __setitem__(self, id, value):
575 """Set the item with the given ID."""
576 if self._values is None:
577 self.finalize()
578 type = self._dataRefs[id][1]
579 length = self._dataRefs[id][2]
580 if type==TYPE_INT:
581 self._values[id] = int(value)
582 elif type==TYPE_FLOAT or type==TYPE_DOUBLE:
583 self._values[id] = float(value)
584 elif type==TYPE_FLOAT_ARRAY:
585 if len(value)!=length:
586 raise ValueError("xplra.MultiBuffer: expected a list of length %d" % (length,))
587 self._values[id] = [float(x) for x in value]
588 elif type==TYPE_INT_ARRAY:
589 if len(value)!=length:
590 raise ValueError("xplra.MultiBuffer: expected a list of length %d" % (length,))
591 self._values[id] = [int(x) for x in value]
592 elif type==TYPE_BYTE_ARRAY:
593 if isinstance(value, str):
594 lst = [ord(x) for x in value[:length]]
595 if len(lst)<length:
596 lst += [0] * (length-len(lst))
597 self._values[id] = lst
598 else:
599 if len(value)!=length:
600 raise ValueError("xplra.MultiBuffer: expected a list of length %d" % (length,))
601 self._values[id] = [int(x) for x in value]
602
603 def __iter__(self):
604 """Get an iterator over the values of this buffer."""
605 if self._values is None:
606 self.finalize()
607 return iter(self._values)
608
609
610#-------------------------------------------------------------------------------
611
612class MultiGetter(MultiBuffer):
613 """Multi-dataref buffer for querying."""
614 def __init__(self, xplane):
615 """Construct the getter."""
616 super(MultiGetter, self).__init__(xplane,
617 COMMAND_REGISTER_GET_MULTI,
618 COMMAND_UNREGISTER_GET_MULTI)
619
620 def _executeRegistered(self):
621 """Execute the query if the buffer is registered."""
622 self._xplane._writeU8(COMMAND_EXECUTE_GET_MULTI)
623 self._xplane._writeU32(self._registeredID)
624 self._xplane._flush()
625
626 self._xplane._checkResult(multi = True)
627
628 self._readValues()
629
630 def _executeUnregistered(self):
631 """Execute the query if the buffer is not registered."""
632 self._writeSpec(COMMAND_GET_MULTI)
633 self._readValues()
634
635 def _readValues(self):
636 """Read the values into the values array."""
637 for i in range(0, len(self._dataRefs)):
638 self._values[i] = self._xplane._readValue(self._dataRefs[i][1])
639
640#-------------------------------------------------------------------------------
641
642class MultiSetter(MultiBuffer):
643 """Multi-dataref buffer for setting."""
644 def __init__(self, xplane):
645 """Construct the getter."""
646 super(MultiSetter, self).__init__(xplane,
647 COMMAND_REGISTER_SET_MULTI,
648 COMMAND_UNREGISTER_SET_MULTI)
649
650 def _executeRegistered(self):
651 """Execute the query if the buffer is registered."""
652 self._xplane._writeU8(COMMAND_EXECUTE_SET_MULTI)
653 self._xplane._writeU32(self._registeredID)
654 for i in range(0, len(self._dataRefs)):
655 self._xplane._writeValue(self._dataRefs[i][1], self._values[i])
656 self._xplane._flush()
657
658 self._xplane._checkResult(multi = True)
659
660 def _executeUnregistered(self):
661 """Execute the query if the buffer is not registered."""
662 self._xplane._writeU8(COMMAND_SET_MULTI)
663
664 numDataRefs = len(self._dataRefs)
665 self._xplane._writeU32(numDataRefs)
666
667 for i in range(0, numDataRefs):
668 (name, type, length, offset) = self._dataRefs[i]
669 value = self._values[i]
670
671 self._xplane._writeString(name)
672 self._xplane._writeU8(type)
673 if length is not None and offset is not None:
674 self._xplane._writeS32(len(value))
675 self._xplane._writeS32(offset)
676 self._xplane._writeValue(type, value)
677
678 self._xplane._flush()
679 self._xplane._checkResult(multi = True)
680
681#-------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.