source: xplra/src/client/python/xplra.py@ 52:50bbffd99071

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

Implemented the Python client for hotkey support

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