source: src/mlx/xplane.py@ 423:959722a16e8d

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

Implemented hotkey support for X-Plane

File size: 71.3 KB
Line 
1
2import fs
3import const
4import util
5
6import threading
7import time
8import calendar
9import datetime
10import sys
11import codecs
12import math
13
14from xplra import XPlane, MultiGetter, MultiSetter
15from xplra import TYPE_INT, TYPE_FLOAT, TYPE_DOUBLE
16from xplra import TYPE_FLOAT_ARRAY, TYPE_INT_ARRAY, TYPE_BYTE_ARRAY
17from xplra import HOTKEY_MODIFIER_SHIFT, HOTKEY_MODIFIER_CONTROL
18
19#------------------------------------------------------------------------------
20
21## @package mlx.xplane
22#
23# The module towards X-Plane
24#
25# This module implements the simulator interface to X-Plane via the
26# X-Plane Remote Access (xplra) plugin.
27
28#------------------------------------------------------------------------------
29
30_hgin2hpa = 1013.25 / 29.92
31
32_mps2knots = 3600.0 / 1852
33
34#------------------------------------------------------------------------------
35
36class Request(object):
37 """Base class for one-shot requests."""
38 def __init__(self, handler, callback, extra):
39 """Construct the request."""
40 self._handler = handler
41 self._callback = callback
42 self._extra = extra
43 self._result = None
44
45 def process(self, time):
46 """Process the request.
47
48 Return True if the request has succeeded, False if data validation
49 has failed for a reading request. An exception may also be thrown
50 if there is some lower-level communication problem."""
51 if self._process(time):
52 Handler._callSafe(lambda: self._callback(self._result,
53 self._extra))
54 return True
55 else:
56 return False
57
58 def fail(self):
59 """Handle the failure of this request."""
60 Handler._callSafe(lambda: self._callback(False, self._extra))
61
62class DataRequest(Request):
63 """A simple, one-shot data read or write request."""
64 def __init__(self, handler, forWrite, data, callback, extra,
65 validator = None):
66 """Construct the request."""
67 super(DataRequest, self).__init__(handler, callback, extra)
68
69 self._forWrite = forWrite
70 self._validator = validator
71
72 xplane = handler._xplane
73 self._multiBuffer = xplane.createMultiSetter() if forWrite \
74 else xplane.createMultiGetter()
75
76 Handler._setupMultiBuffer(self._multiBuffer,
77 [(d[0], d[1]) for d in data])
78
79 if forWrite:
80 index = 0
81 for (_, _, value) in data:
82 self._multiBuffer[index] = value
83 index += 1
84
85
86 def fail(self):
87 """Handle the failure of this request."""
88 if self._forWrite:
89 super(DataRequest, self).fail()
90 else:
91 Handler._callSafe(lambda: self._callback(None, self._extra))
92
93 def _process(self, time):
94 """Process the request."""
95 if self._forWrite:
96 self._multiBuffer.execute()
97 self._result = True
98 return True
99 elif Handler._performRead(self._multiBuffer,
100 self._extra, self._validator):
101 self._result = self._multiBuffer
102 return True
103 else:
104 return False
105
106class ShowMessageRequest(Request):
107 """Request to show a message in the simulator window."""
108 def __init__(self, handler, message, duration, callback, extra):
109 """Construct the request."""
110 super(ShowMessageRequest, self).__init__(handler,
111 callback, extra)
112 self._message = message
113 self._duration = duration
114
115 def _process(self, time):
116 """Process the request."""
117 self._handler._xplane.showMessage(self._message, self._duration)
118 self._result = True
119 return True
120
121class RegisterHotkeysRequest(Request):
122 """Request to register hotkeys with the simulator."""
123 def __init__(self, handler, hotkeyCodes, callback, extra):
124 """Construct the request."""
125 super(RegisterHotkeysRequest, self).__init__(handler,
126 callback,
127 extra)
128 self._hotkeyCodes = hotkeyCodes
129
130 def _process(self, time):
131 """Process the request."""
132 self._handler._xplane.registerHotkeys(self._hotkeyCodes)
133 self._result = True
134 return True
135
136class UnregisterHotkeysRequest(Request):
137 """Request to register hotkeys with the simulator."""
138 def _process(self, time):
139 """Process the request."""
140 self._handler._xplane.unregisterHotkeys()
141 self._result = True
142 return True
143
144class PeriodicRequest(object):
145 """A periodic request."""
146 def __init__(self, handler, id, period, callback, extra):
147 """Construct the periodic request."""
148 self._handler = handler
149 self._id = id
150 self._period = period
151 self._nextFire = time.time()
152 self._callback = callback
153 self._extra = extra
154 self._result = None
155
156 @property
157 def id(self):
158 """Get the ID of this periodic request."""
159 return self._id
160
161 @property
162 def nextFire(self):
163 """Get the next firing time."""
164 return self._nextFire
165
166 def process(self, now):
167 """Check if this request should be executed, and if so, do so.
168
169 now is the time at which the request is being executed. If this
170 function is called too early, nothing is done, and True is
171 returned.
172
173 Return True if the request has succeeded, False if data validation
174 has failed. An exception may also be thrown if there is some
175 lower-level communication problem."""
176 if now<self._nextFire:
177 return True
178
179 isOK = self._process(time)
180
181 if isOK:
182 Handler._callSafe(lambda: self._callback(self._result,
183 self._extra))
184 now = time.time()
185 while self._nextFire <= now:
186 self._nextFire += self._period
187
188 return isOK
189
190 def fail(self):
191 """Handle the failure of this request."""
192 pass
193
194 def __cmp__(self, other):
195 """Compare two periodic requests. They are ordered by their next
196 firing times."""
197 return cmp(self._nextFire, other._nextFire)
198
199class PeriodicDataRequest(PeriodicRequest):
200 """A periodic request."""
201 def __init__(self, handler, id, period, data, callback, extra,
202 validator):
203 """Construct the periodic request."""
204 super(PeriodicDataRequest, self).__init__(handler, id, period,
205 callback, extra)
206 self._validator = validator
207 self._multiGetter = handler._xplane.createMultiGetter()
208 Handler._setupMultiBuffer(self._multiGetter, data)
209
210 def _process(self, now):
211 """Process the request."""
212 if Handler._performRead(self._multiGetter,
213 self._extra, self._validator):
214 self._result = self._multiGetter
215 return True
216 else:
217 return False
218
219#------------------------------------------------------------------------------
220
221class HotkeysStateRequest(PeriodicRequest):
222 """Periodic hotkey query request."""
223 def _process(self, now):
224 """Process the request."""
225 self._result = self._handler._xplane.queryHotkeys()
226 return True
227
228#------------------------------------------------------------------------------
229
230class Handler(threading.Thread):
231 """The thread to handle the requests towards X-Plane."""
232 @staticmethod
233 def _callSafe(fun):
234 """Call the given function and swallow any exceptions."""
235 try:
236 return fun()
237 except Exception, e:
238 print >> sys.stderr, util.utf2unicode(str(e))
239 return None
240
241 # The number of times a read is attempted
242 NUM_READATTEMPTS = 3
243
244 # The number of connection attempts
245 NUM_CONNECTATTEMPTS = 3
246
247 # The interval between successive connect attempts
248 CONNECT_INTERVAL = 0.25
249
250 @staticmethod
251 def _setupMultiBuffer(buffer, dataSpec):
252 """Setup the given multi-dataref buffer for the given data
253 specification.
254
255 The specification is a list of tuples of two items:
256 - the name of the dataref
257 - the type of the dataref. It can be one of the following:
258 - an integer denoting the type. If it denotes an array type, the
259 length will be -1 (i.e. as many as returned when reading the
260 value), and the offset will be 0
261 - a tuple of two or three items:
262 - the first item is the type constant
263 - the second item is the length
264 - the third item is the offset, which defaults to 0."""
265 for (name, typeInfo) in dataSpec:
266 length = -1
267 offset = 0
268 type = 0
269 if isinstance(typeInfo, tuple):
270 type = typeInfo[0]
271 length = typeInfo[1]
272 offset = 0 if len(typeInfo)<3 else typeInfo[2]
273 else:
274 type = typeInfo
275
276 if type==TYPE_INT:
277 buffer.addInt(name)
278 elif type==TYPE_FLOAT:
279 buffer.addFloat(name)
280 elif type==TYPE_DOUBLE:
281 buffer.addDouble(name)
282 elif type==TYPE_FLOAT_ARRAY:
283 buffer.addFloatArray(name, length = length, offset = offset)
284 elif type==TYPE_INT_ARRAY:
285 buffer.addIntArray(name, length = length, offset = offset)
286 elif type==TYPE_BYTE_ARRAY:
287 buffer.addByteArray(name, length = length, offset = offset)
288 else:
289 raise TypeError("xplane.Handler._setupMultiBuffer: invalid type info: %s for dataref '%s'" % (typeInfo, name))
290
291
292
293 @staticmethod
294 def _performRead(multiGetter, extra, validator):
295 """Perform a read request.
296
297 If there is a validator, that will be called with the return values,
298 and if the values are wrong, the request is retried at most a certain
299 number of times.
300
301 Return True if the request has succeeded, False if validation has
302 failed during all attempts. An exception may also be thrown if there is
303 some lower-level communication problem."""
304 attemptsLeft = Handler.NUM_READATTEMPTS
305 while attemptsLeft>0:
306 multiGetter.execute()
307 if validator is None or \
308 Handler._callSafe(lambda: validator(multiGetter, extra)):
309 return True
310 else:
311 attemptsLeft -= 1
312 return False
313
314 def __init__(self, connectionListener,
315 connectAttempts = -1, connectInterval = 0.2):
316 """Construct the handler with the given connection listener."""
317 threading.Thread.__init__(self)
318
319 self._connectionListener = connectionListener
320 self._connectAttempts = connectAttempts
321 self._connectInterval = connectInterval
322
323 self._xplane = XPlane()
324
325 self._requestCondition = threading.Condition()
326 self._connectionRequested = False
327 self._connected = False
328
329 self._requests = []
330 self._nextPeriodicID = 1
331 self._periodicRequests = []
332
333 self.daemon = True
334
335 def requestRead(self, data, callback, extra = None, validator = None):
336 """Request the reading of some data.
337
338 data is a list of tuples of the following items:
339 - the offset of the data as an integer
340 - the type letter of the data as a string
341
342 callback is a function that receives two pieces of data:
343 - the values retrieved or None on error
344 - the extra parameter
345
346 It will be called in the handler's thread!
347 """
348 with self._requestCondition:
349 self._requests.append(DataRequest(self, False, data,
350 callback, extra,
351 validator))
352 self._requestCondition.notify()
353
354 def requestWrite(self, data, callback, extra = None):
355 """Request the writing of some data.
356
357 data is a list of tuples of the following items:
358 - the offset of the data as an integer
359 - the type letter of the data as a string
360 - the data to write
361
362 callback is a function that receives two pieces of data:
363 - a boolean indicating if writing was successful
364 - the extra data
365 It will be called in the handler's thread!
366 """
367 with self._requestCondition:
368 request = DataRequest(self, True, data, callback, extra)
369 #print "xplane.Handler.requestWrite", request
370 self._requests.append(request)
371 self._requestCondition.notify()
372
373 def requestPeriodicRead(self, period, data, callback, extra = None,
374 validator = None):
375 """Request a periodic read of data.
376
377 period is a floating point number with the period in seconds.
378
379 This function returns an identifier which can be used to cancel the
380 request."""
381 with self._requestCondition:
382 id = self._nextPeriodicID
383 self._nextPeriodicID += 1
384 request = PeriodicDataRequest(self, id, period,
385 data, callback,
386 extra, validator)
387 self._periodicRequests.append(request)
388 self._requestCondition.notify()
389 return id
390
391 def clearPeriodic(self, id):
392 """Clear the periodic request with the given ID."""
393 with self._requestCondition:
394 for i in range(0, len(self._periodicRequests)):
395 if self._periodicRequests[i].id==id:
396 del self._periodicRequests[i]
397 return True
398 return False
399
400 def requestShowMessage(self, message, duration, callback, extra = None):
401 """Request showing a message in the simulator."""
402 with self._requestCondition:
403 self._requests.append(ShowMessageRequest(self,
404 message, duration,
405 callback, extra))
406 self._requestCondition.notify()
407
408 def registerHotkeys(self, hotkeys, callback, extra = None):
409 """Request registering the given hotkeys."""
410 with self._requestCondition:
411 self._requests.append(RegisterHotkeysRequest(self, hotkeys,
412 callback, extra))
413 self._requestCondition.notify()
414
415 def requestHotkeysState(self, period, callback, extra = None):
416 """Request a periodic query of the hotkey status."""
417 with self._requestCondition:
418 id = self._nextPeriodicID
419 self._nextPeriodicID += 1
420 request = HotkeysStateRequest(self, id, period, callback, extra)
421 self._periodicRequests.append(request)
422 self._requestCondition.notify()
423 return id
424
425 def unregisterHotkeys(self, callback, extra = None):
426 """Request unregistering the hotkeys."""
427 with self._requestCondition:
428 self._requests.append(UnregisterHotkeysRequest(self,
429 callback, extra))
430 self._requestCondition.notify()
431
432 def connect(self):
433 """Initiate the connection to the flight simulator."""
434 with self._requestCondition:
435 if not self._connectionRequested:
436 self._connectionRequested = True
437 self._requestCondition.notify()
438
439 def disconnect(self):
440 """Disconnect from the flight simulator."""
441 with self._requestCondition:
442 self._requests = []
443 if self._connectionRequested:
444 self._connectionRequested = False
445 self._requestCondition.notify()
446
447 def clearRequests(self):
448 """Clear the outstanding one-shot requests."""
449 with self._requestCondition:
450 self._requests = []
451
452 def run(self):
453 """Perform the operation of the thread."""
454 while True:
455 self._waitConnectionRequest()
456
457 if self._connect()>0:
458 self._handleConnection()
459
460 self._disconnect()
461
462 def _waitConnectionRequest(self):
463 """Wait for a connection request to arrive."""
464 with self._requestCondition:
465 while not self._connectionRequested:
466 self._requestCondition.wait()
467
468 def _connect(self, autoReconnection = False, attempts = 0):
469 """Try to connect to the flight simulator via XPLRA
470
471 Returns True if the connection has been established, False if it was
472 not due to no longer requested.
473 """
474 while self._connectionRequested:
475 if attempts>=self.NUM_CONNECTATTEMPTS:
476 self._connectionRequested = False
477 if autoReconnection:
478 Handler._callSafe(lambda:
479 self._connectionListener.disconnected())
480 else:
481 Handler._callSafe(lambda:
482 self._connectionListener.connectionFailed())
483 return 0
484
485 try:
486 attempts += 1
487 self._xplane.connect()
488
489 (xplaneVersion, xplmVersion, xplraVersion) = \
490 self._xplane.getVersions()
491
492 description = "(X-Plane version: %d, XPLM version: %d, XPLRA version: %03d)" % \
493 (xplaneVersion, xplmVersion, xplraVersion)
494 if not autoReconnection:
495 fsType = const.SIM_XPLANE10 if xplraVersion>=10000 else const.SIM_XPLANE9
496
497 Handler._callSafe(lambda:
498 self._connectionListener.connected(fsType,
499 description))
500 self._connected = True
501 return attempts
502 except Exception, e:
503 print "xplane.Handler._connect: connection failed: " + \
504 util.utf2unicode(str(e)) + \
505 " (attempts: %d)" % (attempts,)
506 if attempts<self.NUM_CONNECTATTEMPTS:
507 time.sleep(self.CONNECT_INTERVAL)
508 self._xplane.disconnect()
509
510 def _handleConnection(self):
511 """Handle a living connection."""
512 with self._requestCondition:
513 while self._connectionRequested:
514 self._processRequests()
515 self._waitRequest()
516
517 def _waitRequest(self):
518 """Wait for the time of the next request.
519
520 Returns also, if the connection is no longer requested.
521
522 Should be called with the request condition lock held."""
523 while self._connectionRequested:
524 timeout = None
525 if self._periodicRequests:
526 self._periodicRequests.sort()
527 timeout = self._periodicRequests[0].nextFire - time.time()
528
529 if self._requests or \
530 (timeout is not None and timeout <= 0.0):
531 return
532
533 self._requestCondition.wait(timeout)
534
535 def _disconnect(self):
536 """Disconnect from the flight simulator."""
537 print "xplane.Handler._disconnect"
538 if self._connected:
539 try:
540 self._xplane.disconnect()
541 except:
542 pass
543
544 self._connected = False
545
546 def _processRequest(self, request, time, attempts):
547 """Process the given request.
548
549 If an exception occurs or invalid data is read too many times, we try
550 to reconnect.
551
552 This function returns only if the request has succeeded, or if a
553 connection is no longer requested.
554
555 This function is called with the request lock held, but is relased
556 whole processing the request and reconnecting."""
557 self._requestCondition.release()
558
559 #print "xplane.Handler._processRequest", request
560
561 needReconnect = False
562 try:
563 try:
564 if not request.process(time):
565 print "xplane.Handler._processRequest: X-Plane returned invalid data too many times, reconnecting"
566 needReconnect = True
567 except Exception as e:
568 print "xplane.Handler._processRequest: X-Plane connection failed (" + \
569 util.utf2unicode(str(e)) + \
570 "), reconnecting (attempts=%d)." % (attempts,)
571 needReconnect = True
572
573 if needReconnect:
574 with self._requestCondition:
575 self._requests.insert(0, request)
576 self._disconnect()
577 return self._connect(autoReconnection = True, attempts = attempts)
578 else:
579 return 0
580 finally:
581 self._requestCondition.acquire()
582
583 def _processRequests(self):
584 """Process any pending requests.
585
586 Will be called with the request lock held."""
587 attempts = 0
588 while self._connectionRequested and self._periodicRequests:
589 self._periodicRequests.sort()
590 request = self._periodicRequests[0]
591
592 t = time.time()
593
594 if request.nextFire>t:
595 break
596
597 attempts = self._processRequest(request, t, attempts)
598
599 while self._connectionRequested and self._requests:
600 request = self._requests[0]
601 del self._requests[0]
602
603 attempts = self._processRequest(request, None, attempts)
604
605 return self._connectionRequested
606
607#------------------------------------------------------------------------------
608
609class Simulator(object):
610 """The simulator class representing the interface to the flight simulator
611 via XPLRA."""
612 # The basic data that should be queried all the time once we are connected
613 timeData = [ ("sim/time/local_date_days", TYPE_INT),
614 ("sim/time/zulu_time_sec", TYPE_FLOAT) ]
615
616 normalData = timeData + \
617 [ ("sim/aircraft/view/acf_tailnum", TYPE_BYTE_ARRAY),
618 ("sim/aircraft/view/acf_author", TYPE_BYTE_ARRAY),
619 ("sim/aircraft/view/acf_descrip", TYPE_BYTE_ARRAY),
620 ("sim/aircraft/view/acf_notes", TYPE_BYTE_ARRAY),
621 ("sim/aircraft/view/acf_ICAO", TYPE_BYTE_ARRAY),
622 ("sim/aircraft/view/acf_livery_path", TYPE_BYTE_ARRAY) ]
623
624 flareData1 = [ ("sim/time/zulu_time_sec", TYPE_FLOAT),
625 ("sim/flightmodel/position/y_agl", TYPE_FLOAT),
626 ("sim/flightmodel/position/vh_ind_fpm2", TYPE_FLOAT) ]
627
628 flareStartData = [ ("sim/weather/wind_speed_kt[0]", TYPE_FLOAT),
629 ("sim/weather/wind_direction_degt[0]", TYPE_FLOAT),
630 ("sim/weather/visibility_reported_m", TYPE_FLOAT) ]
631
632 flareData2 = [ ("sim/time/zulu_time_sec", TYPE_FLOAT),
633 ("sim/flightmodel/failures/onground_any", TYPE_INT),
634 ("sim/flightmodel/position/vh_ind_fpm2", TYPE_FLOAT),
635 ("sim/flightmodel/position/indicated_airspeed2",
636 TYPE_FLOAT),
637 ("sim/flightmodel/position/theta", TYPE_FLOAT),
638 ("sim/flightmodel/position/phi", TYPE_FLOAT),
639 ("sim/flightmodel/position/psi", TYPE_FLOAT) ]
640
641 TIME_SYNC_INTERVAL = 3.0
642
643 @staticmethod
644 def _getTimestamp(data):
645 """Convert the given data into a timestamp."""
646 year = datetime.date.today().year
647 timestamp = calendar.timegm(time.struct_time([year,
648 1, 1, 0, 0, 0, -1, 1, 0]))
649 timestamp += data[0] * 24 * 3600
650 timestamp += data[1]
651
652 return timestamp
653
654 @staticmethod
655 def _getHotkeyCode(hotkey):
656 """Get the hotkey code for the given hot key."""
657 code = ord(hotkey.key)
658 if hotkey.shift: code |= HOTKEY_MODIFIER_SHIFT
659 if hotkey.ctrl: code |= HOTKEY_MODIFIER_CONTROL
660 return code
661
662 def __init__(self, connectionListener, connectAttempts = -1,
663 connectInterval = 0.2):
664 """Construct the simulator.
665
666 The aircraft object passed must provide the following members:
667 - type: one of the AIRCRAFT_XXX constants from const.py
668 - modelChanged(aircraftName, modelName): called when the model handling
669 the aircraft has changed.
670 - handleState(aircraftState): handle the given state.
671 - flareStarted(windSpeed, windDirection, visibility, flareStart,
672 flareStartFS): called when the flare has
673 started. windSpeed is in knots, windDirection is in degrees and
674 visibility is in metres. flareStart and flareStartFS are two time
675 values expressed in seconds that can be used to calculate the flare
676 time.
677 - flareFinished(flareEnd, flareEndFS, tdRate, tdRateCalculatedByFS,
678 ias, pitch, bank, heading): called when the flare has
679 finished, i.e. the aircraft is on the ground. flareEnd and flareEndFS
680 are the two time values corresponding to the touchdown time. tdRate is
681 the touch-down rate, tdRateCalculatedBySim indicates if the data comes
682 from the simulator or was calculated by the adapter. The other data
683 are self-explanatory and expressed in their 'natural' units."""
684 self._fsType = None
685 self._aircraft = None
686
687 self._handler = Handler(self,
688 connectAttempts = connectAttempts,
689 connectInterval = connectInterval)
690 self._connectionListener = connectionListener
691 self._handler.start()
692
693 self._syncTime = False
694 self._nextSyncTime = -1
695
696 self._normalRequestID = None
697
698 self._monitoringRequested = False
699 self._monitoring = False
700
701 self._aircraftInfo = None
702 self._aircraftModel = None
703
704 self._flareRequestID = None
705 self._flareRates = []
706 self._flareStart = None
707 self._flareStartFS = None
708
709 self._hotkeyLock = threading.Lock()
710 self._hotkeyCodes = None
711 self._hotkeySetID = 0
712 self._hotkeySetGeneration = 0
713 self._hotkeyOffets = None
714 self._hotkeyRequestID = None
715 self._hotkeyCallback = None
716
717 self._latin1decoder = codecs.getdecoder("iso-8859-1")
718 self._fuelCallback = None
719
720 def connect(self, aircraft):
721 """Initiate a connection to the simulator."""
722 self._aircraft = aircraft
723 self._aircraftInfo = None
724 self._aircraftModel = None
725 self._handler.connect()
726 if self._normalRequestID is None:
727 self._nextSyncTime = -1
728 self._startDefaultNormal()
729
730 def reconnect(self):
731 """Initiate a reconnection to the simulator.
732
733 It does not reset already set up data, just calls connect() on the
734 handler."""
735 self._handler.connect()
736
737 def requestZFW(self, callback):
738 """Send a request for the ZFW."""
739 data = [ ("sim/aircraft/weight/acf_m_empty", TYPE_FLOAT),
740 ("sim/flightmodel/weight/m_fixed", TYPE_FLOAT) ]
741 self._handler.requestRead(data, self._handleZFW, extra = callback)
742
743 def requestWeights(self, callback):
744 """Request the following weights: DOW, ZFW, payload.
745
746 These values will be passed to the callback function in this order, as
747 separate arguments."""
748 # TODO: implement this for X-Plane
749 self._handler.requestRead([(0x13fc, "d")], self._handlePayloadCount,
750 extra = callback)
751
752 def requestTime(self, callback):
753 """Request the time from the simulator."""
754 self._handler.requestRead(Simulator.timeData, self._handleTime,
755 extra = callback)
756
757 def startMonitoring(self):
758 """Start the periodic monitoring of the aircraft and pass the resulting
759 state to the aircraft object periodically."""
760 assert not self._monitoringRequested
761 self._monitoringRequested = True
762
763 def stopMonitoring(self):
764 """Stop the periodic monitoring of the aircraft."""
765 assert self._monitoringRequested
766 self._monitoringRequested = False
767
768 def startFlare(self):
769 """Start monitoring the flare time.
770
771 At present it is assumed to be called from the handler thread, hence no
772 protection."""
773 #self._aircraft.logger.debug("startFlare")
774 if self._flareRequestID is None:
775 self._flareRates = []
776 self._flareRequestID = \
777 self._handler.requestPeriodicRead(0.1,
778 Simulator.flareData1,
779 self._handleFlare1)
780
781 def cancelFlare(self):
782 """Cancel monitoring the flare time.
783
784 At present it is assumed to be called from the handler thread, hence no
785 protection."""
786 if self._flareRequestID is not None:
787 self._handler.clearPeriodic(self._flareRequestID)
788 self._flareRequestID = None
789
790 def sendMessage(self, message, duration = 3,
791 _disconnect = False):
792 """Send a message to the pilot via the simulator.
793
794 duration is the number of seconds to keep the message displayed."""
795 print "xplra.Simulator.sendMessage:", message
796 self._handler.requestShowMessage(message, duration,
797 self._handleMessageSent,
798 extra = _disconnect)
799
800 def getFuel(self, callback):
801 """Get the fuel information for the current model.
802
803 The callback will be called with a list of triplets with the following
804 items:
805 - the fuel tank identifier
806 - the current weight of the fuel in the tank (in kgs)
807 - the current total capacity of the tank (in kgs)."""
808 if self._aircraftModel is None:
809 self._fuelCallback = callback
810 else:
811 self._aircraftModel.getFuel(self._handler, callback)
812
813 def setFuelLevel(self, levels):
814 """Set the fuel level to the given ones.
815
816 levels is an array of two-tuples, where each tuple consists of the
817 following:
818 - the const.FUELTANK_XXX constant denoting the tank that must be set,
819 - the requested level of the fuel as a floating-point value between 0.0
820 and 1.0."""
821 if self._aircraftModel is not None:
822 self._aircraftModel.setFuelLevel(self._handler, levels)
823
824 def enableTimeSync(self):
825 """Enable the time synchronization."""
826 self._nextSyncTime = -1
827 self._syncTime = True
828
829 def disableTimeSync(self):
830 """Enable the time synchronization."""
831 self._syncTime = False
832 self._nextSyncTime = -1
833
834 def listenHotkeys(self, hotkeys, callback):
835 """Start listening to the given hotkeys.
836
837 callback is function expecting two arguments:
838 - the ID of the hotkey set as returned by this function,
839 - the list of the indexes of the hotkeys that were pressed."""
840 with self._hotkeyLock:
841 assert self._hotkeyCodes is None
842
843 self._hotkeyCodes = \
844 [self._getHotkeyCode(hotkey) for hotkey in hotkeys]
845 self._hotkeySetID += 1
846 self._hotkeySetGeneration = 0
847 self._hotkeyCallback = callback
848
849 self._handler.registerHotkeys(self._hotkeyCodes,
850 self._handleHotkeysRegistered,
851 (self._hotkeySetID,
852 self._hotkeySetGeneration))
853
854 return self._hotkeySetID
855
856 def clearHotkeys(self):
857 """Clear the current hotkey set.
858
859 Note that it is possible, that the callback function set either
860 previously or after calling this function by listenHotkeys() will be
861 called with data from the previous hotkey set.
862
863 Therefore it is recommended to store the hotkey set ID somewhere and
864 check that in the callback function. Right before calling
865 clearHotkeys(), this stored ID should be cleared so that the check
866 fails for sure."""
867 with self._hotkeyLock:
868 if self._hotkeyCodes is not None:
869 self._hotkeyCodes = None
870 self._hotkeySetID += 1
871 self._hotkeyCallback = None
872 self._clearHotkeyRequest()
873
874 def disconnect(self, closingMessage = None, duration = 3):
875 """Disconnect from the simulator."""
876 assert not self._monitoringRequested
877
878 print "xplra.Simulator.disconnect", closingMessage, duration
879
880 self._stopNormal()
881 self.clearHotkeys()
882 if closingMessage is None:
883 self._handler.disconnect()
884 else:
885 self.sendMessage(closingMessage, duration = duration,
886 _disconnect = True)
887
888 def connected(self, fsType, descriptor):
889 """Called when a connection has been established to the flight
890 simulator of the given type."""
891 self._fsType = fsType
892 with self._hotkeyLock:
893 if self._hotkeyCodes is not None:
894 self._hotkeySetGeneration += 1
895
896 self._handler.registerHotkeys(self._hotkeyCodes,
897 self._handleHotkeysRegistered,
898 (self._hotkeySetID,
899 self._hotkeySetGeneration))
900
901 self._connectionListener.connected(fsType, descriptor)
902
903 def connectionFailed(self):
904 """Called when the connection could not be established."""
905 with self._hotkeyLock:
906 self._clearHotkeyRequest()
907 self._connectionListener.connectionFailed()
908
909 def disconnected(self):
910 """Called when a connection to the flight simulator has been broken."""
911 with self._hotkeyLock:
912 self._clearHotkeyRequest()
913 self._connectionListener.disconnected()
914
915 def _startDefaultNormal(self):
916 """Start the default normal periodic request."""
917 assert self._normalRequestID is None
918 self._normalRequestID = \
919 self._handler.requestPeriodicRead(1.0,
920 Simulator.normalData,
921 self._handleNormal)
922
923 def _stopNormal(self):
924 """Stop the normal period request."""
925 assert self._normalRequestID is not None
926 self._handler.clearPeriodic(self._normalRequestID)
927 self._normalRequestID = None
928 self._monitoring = False
929
930 def _handleNormal(self, data, extra):
931 """Handle the reply to the normal request.
932
933 At the beginning the result consists the data for normalData. When
934 monitoring is started, it contains the result also for the
935 aircraft-specific values.
936 """
937 timestamp = Simulator._getTimestamp(data)
938
939 createdNewModel = self._setAircraftName(timestamp,
940 data.getString(2),
941 data.getString(3),
942 data.getString(4),
943 data.getString(5),
944 data.getString(6),
945 data.getString(7))
946 if self._fuelCallback is not None:
947 self._aircraftModel.getFuel(self._handler, self._fuelCallback)
948 self._fuelCallback = None
949
950 if self._monitoringRequested and not self._monitoring:
951 self._stopNormal()
952 self._startMonitoring()
953 elif self._monitoring and not self._monitoringRequested:
954 self._stopNormal()
955 self._startDefaultNormal()
956 elif self._monitoring and self._aircraftModel is not None and \
957 not createdNewModel:
958 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
959 timestamp, data)
960
961 self._aircraft.handleState(aircraftState)
962
963 def _setAircraftName(self, timestamp, tailnum, author, description,
964 notes, icao, liveryPath):
965 """Set the name of the aicraft and if it is different from the
966 previous, create a new model for it.
967
968 If so, also notifty the aircraft about the change.
969
970 Return if a new model was created."""
971 author = self._latin1decoder(author)[0]
972 description = self._latin1decoder(description)[0]
973 notes = self._latin1decoder(notes)[0]
974 liveryPath = self._latin1decoder(liveryPath)[0]
975
976 aircraftInfo = (tailnum, author, description, notes, icao, liveryPath)
977 if aircraftInfo==self._aircraftInfo:
978 return False
979
980 print "xplane.Simulator: new data: %s, %s, %s, %s, %s, %s" % \
981 (tailnum, author, description, notes, icao, liveryPath)
982
983 self._aircraftInfo = aircraftInfo
984 needNew = self._aircraftModel is None
985 needNew = needNew or\
986 not self._aircraftModel.doesHandle(self._aircraft, aircraftInfo)
987 if not needNew:
988 specialModel = AircraftModel.findSpecial(self._aircraft,
989 aircraftInfo)
990 needNew = specialModel is not None and \
991 specialModel is not self._aircraftModel.__class__
992
993 if needNew:
994 self._setAircraftModel(AircraftModel.create(self._aircraft,
995 aircraftInfo))
996
997 self._aircraft.modelChanged(timestamp, description,
998 self._aircraftModel.name)
999
1000 return needNew
1001
1002 def _setAircraftModel(self, model):
1003 """Set a new aircraft model.
1004
1005 It will be queried for the data to monitor and the monitoring request
1006 will be replaced by a new one."""
1007 self._aircraftModel = model
1008
1009 if self._monitoring:
1010 self._stopNormal()
1011 self._startMonitoring()
1012
1013 def _startMonitoring(self):
1014 """Start monitoring with the current aircraft model."""
1015 data = Simulator.normalData[:]
1016 self._aircraftModel.addMonitoringData(data, self._fsType)
1017
1018 self._normalRequestID = \
1019 self._handler.requestPeriodicRead(1.0, data,
1020 self._handleNormal)
1021 self._monitoring = True
1022
1023 def _addFlareRate(self, data):
1024 """Append a flare rate to the list of last rates."""
1025 if len(self._flareRates)>=3:
1026 del self._flareRates[0]
1027 self._flareRates.append(data)
1028
1029 def _handleFlare1(self, data, normal):
1030 """Handle the first stage of flare monitoring."""
1031 #self._aircraft.logger.debug("handleFlare1: " + str(data))
1032 if data[1]<=50.0*0.3048:
1033 self._flareStart = time.time()
1034 self._flareStartFS = data[0]
1035 self._handler.clearPeriodic(self._flareRequestID)
1036 self._handler.requestRead(Simulator.flareStartData,
1037 self._handleFlareStart)
1038
1039 self._addFlareRate(data[2])
1040
1041 def _handleFlareStart(self, data, extra):
1042 """Handle the data need to notify the aircraft about the starting of
1043 the flare."""
1044 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
1045 if data is not None:
1046 windDirection = data[1]
1047 if windDirection<0.0: windDirection += 360.0
1048 self._aircraft.flareStarted(data[0], windDirection, data[2],
1049 self._flareStart, self._flareStartFS)
1050
1051 self._flareRequestID = \
1052 self._handler.requestPeriodicRead(0.1,
1053 Simulator.flareData2,
1054 self._handleFlare2)
1055
1056 def _handleFlare2(self, data, normal):
1057 """Handle the first stage of flare monitoring."""
1058 #self._aircraft.logger.debug("handleFlare2: " + str(data))
1059 if data[1]!=0:
1060 flareEnd = time.time()
1061 self._handler.clearPeriodic(self._flareRequestID)
1062 self._flareRequestID = None
1063
1064 flareEndFS = data[0]
1065 if flareEndFS<self._flareStartFS:
1066 flareEndFS += 86400.0
1067
1068 tdRate = min(self._flareRates)
1069 tdRateCalculatedByFS = False
1070
1071 heading = data[6]
1072 if heading<0.0: heading += 360.0
1073
1074 self._aircraft.flareFinished(flareEnd, flareEndFS,
1075 tdRate, tdRateCalculatedByFS,
1076 data[3], data[4], data[5], heading)
1077 else:
1078 self._addFlareRate(data[2])
1079
1080 def _handleZFW(self, data, callback):
1081 """Callback for a ZFW retrieval request."""
1082 zfw = data[0] + data[1]
1083 callback(zfw)
1084
1085 def _handleTime(self, data, callback):
1086 """Callback for a time retrieval request."""
1087 callback(Simulator._getTimestamp(data))
1088
1089 def _handlePayloadCount(self, data, callback):
1090 """Callback for the payload count retrieval request."""
1091 payloadCount = data[0]
1092 data = [(0x3bfc, "d"), (0x30c0, "f")]
1093 for i in range(0, payloadCount):
1094 data.append((0x1400 + i*48, "f"))
1095
1096 self._handler.requestRead(data, self._handleWeights,
1097 extra = callback)
1098
1099 def _handleWeights(self, data, callback):
1100 """Callback for the weights retrieval request."""
1101 zfw = data[0] * const.LBSTOKG / 256.0
1102 grossWeight = data[1] * const.LBSTOKG
1103 payload = sum(data[2:]) * const.LBSTOKG
1104 dow = zfw - payload
1105 callback(dow, payload, zfw, grossWeight)
1106
1107 def _handleMessageSent(self, success, disconnect):
1108 """Callback for a message sending request."""
1109 #print "xplra.Simulator._handleMessageSent", disconnect
1110 if disconnect:
1111 self._handler.disconnect()
1112
1113 def _handleHotkeysRegistered(self, success, (id, generation)):
1114 """Handle the result of the hotkeys having been written."""
1115 with self._hotkeyLock:
1116 if success and id==self._hotkeySetID and \
1117 generation==self._hotkeySetGeneration:
1118 self._hotkeyRequestID = \
1119 self._handler.requestHotkeysState(0.5,
1120 self._handleHotkeys,
1121 (id, generation))
1122
1123 def _handleHotkeys(self, data, (id, generation)):
1124 """Handle the hotkeys."""
1125 with self._hotkeyLock:
1126 if id!=self._hotkeySetID or generation!=self._hotkeySetGeneration:
1127 return
1128
1129 callback = self._hotkeyCallback
1130 offsets = self._hotkeyOffets
1131
1132 hotkeysPressed = []
1133 for i in range(0, len(data)):
1134 if data[i]!=0:
1135 hotkeysPressed.append(i)
1136
1137 if hotkeysPressed:
1138 callback(id, hotkeysPressed)
1139
1140 def _clearHotkeyRequest(self):
1141 """Clear the hotkey request in the handler if there is any."""
1142 if self._hotkeyRequestID is not None:
1143 self._handler.unregisterHotkeys(self._hotkeysUnregistered)
1144 self._handler.clearPeriodic(self._hotkeyRequestID)
1145 self._hotkeyRequestID = None
1146
1147 def _hotkeysUnregistered(self):
1148 """Called when the hotkeys have been unregistered."""
1149 pass
1150
1151#------------------------------------------------------------------------------
1152
1153class AircraftModel(object):
1154 """Base class for the aircraft models.
1155
1156 Aircraft models handle the data arriving from X-Plane and turn it into an
1157 object describing the aircraft's state."""
1158 monitoringData = [ ("paused",
1159 "sim/time/paused", TYPE_INT),
1160 ("latitude",
1161 "sim/flightmodel/position/latitude", TYPE_DOUBLE),
1162 ("longitude",
1163 "sim/flightmodel/position/longitude", TYPE_DOUBLE),
1164 ("replay",
1165 "sim/operation/prefs/replay_mode", TYPE_INT),
1166 ("overspeed",
1167 "sim/flightmodel/failures/over_vne", TYPE_INT),
1168 ("stalled",
1169 "sim/flightmodel/failures/stallwarning", TYPE_INT),
1170 ("onTheGround",
1171 "sim/flightmodel/failures/onground_any", TYPE_INT),
1172 ("emptyWeight",
1173 "sim/aircraft/weight/acf_m_empty", TYPE_FLOAT),
1174 ("payloadWeight",
1175 "sim/flightmodel/weight/m_fixed", TYPE_FLOAT),
1176 ("grossWeight",
1177 "sim/flightmodel/weight/m_total", TYPE_FLOAT),
1178 ("heading",
1179 "sim/flightmodel/position/psi", TYPE_FLOAT),
1180 ("pitch",
1181 "sim/flightmodel/position/theta", TYPE_FLOAT),
1182 ("bank",
1183 "sim/flightmodel/position/phi", TYPE_FLOAT),
1184 ("ias",
1185 "sim/flightmodel/position/indicated_airspeed2",
1186 TYPE_FLOAT),
1187 ("mach",
1188 "sim/flightmodel/misc/machno", TYPE_FLOAT),
1189 ("groundSpeed",
1190 "sim/flightmodel/position/groundspeed", TYPE_FLOAT),
1191 ("vs",
1192 "sim/flightmodel/position/vh_ind_fpm2", TYPE_FLOAT),
1193 ("radioAltitude",
1194 "sim/flightmodel/position/y_agl", TYPE_FLOAT),
1195 ("altitude",
1196 "sim/flightmodel/position/elevation", TYPE_FLOAT),
1197 ("gLoad",
1198 "sim/flightmodel/forces/g_nrml", TYPE_FLOAT),
1199 ("flapsControl",
1200 "sim/flightmodel/controls/flaprqst", TYPE_FLOAT),
1201 ("flapsLeft",
1202 "sim/flightmodel/controls/flaprat", TYPE_FLOAT),
1203 ("flapsRight",
1204 "sim/flightmodel/controls/flap2rat", TYPE_FLOAT),
1205 ("navLights",
1206 "sim/cockpit/electrical/nav_lights_on", TYPE_INT),
1207 ("beaconLights",
1208 "sim/cockpit/electrical/beacon_lights_on", TYPE_INT),
1209 ("strobeLights",
1210 "sim/cockpit/electrical/strobe_lights_on", TYPE_INT),
1211 ("landingLights",
1212 "sim/cockpit/electrical/landing_lights_on", TYPE_INT),
1213 ("pitot",
1214 "sim/cockpit/switches/pitot_heat_on", TYPE_INT),
1215 ("parking",
1216 "sim/flightmodel/controls/parkbrake", TYPE_FLOAT),
1217 ("gearControl",
1218 "sim/cockpit2/controls/gear_handle_down", TYPE_INT),
1219 ("noseGear",
1220 "sim/flightmodel2/gear/deploy_ratio",
1221 (TYPE_FLOAT_ARRAY, 1)),
1222 ("spoilers",
1223 "sim/flightmodel/controls/lsplrdef", TYPE_FLOAT),
1224 ("altimeter",
1225 "sim/cockpit/misc/barometer_setting", TYPE_FLOAT),
1226 ("qnh",
1227 "sim/physics/earth_pressure_p", TYPE_FLOAT),
1228 ("nav1",
1229 "sim/cockpit/radios/nav1_freq_hz", TYPE_INT),
1230 ("nav1_obs",
1231 "sim/cockpit/radios/nav1_obs_degm", TYPE_FLOAT),
1232 ("nav2",
1233 "sim/cockpit/radios/nav2_freq_hz", TYPE_INT),
1234 ("nav2_obs",
1235 "sim/cockpit/radios/nav2_obs_degm", TYPE_FLOAT),
1236 ("adf1",
1237 "sim/cockpit/radios/adf1_freq_hz", TYPE_INT),
1238 ("adf2",
1239 "sim/cockpit/radios/adf2_freq_hz", TYPE_INT),
1240 ("squawk",
1241 "sim/cockpit/radios/transponder_code", TYPE_INT),
1242 ("windSpeed",
1243 "sim/weather/wind_speed_kt[0]", TYPE_FLOAT),
1244 ("windDirection",
1245 "sim/weather/wind_direction_degt[0]", TYPE_FLOAT),
1246 ("visibility",
1247 "sim/weather/visibility_reported_m", TYPE_FLOAT),
1248 ("cog",
1249 "sim/flightmodel/misc/cgz_ref_to_default", TYPE_FLOAT),
1250 ("xpdrC",
1251 "sim/cockpit/radios/transponder_mode", TYPE_INT),
1252 ("apMaster",
1253 "sim/cockpit/autopilot/autopilot_mode", TYPE_INT),
1254 ("apState",
1255 "sim/cockpit/autopilot/autopilot_state", TYPE_INT),
1256 ("apHeading",
1257 "sim/cockpit/autopilot/heading_mag", TYPE_FLOAT),
1258 ("apAltitude",
1259 "sim/cockpit/autopilot/altitude", TYPE_FLOAT),
1260 ("elevatorTrim",
1261 "sim/flightmodel/controls/elv_trim", TYPE_FLOAT),
1262 ("antiIceOn",
1263 "sim/cockpit/switches/anti_ice_on", TYPE_INT),
1264 ("surfaceHeat",
1265 "sim/cockpit/switches/anti_ice_surf_heat", TYPE_INT),
1266 ("propHeat",
1267 "sim/cockpit/switches/anti_ice_prop_heat", TYPE_INT) ]
1268
1269 specialModels = []
1270
1271 @staticmethod
1272 def registerSpecial(clazz):
1273 """Register the given class as a special model."""
1274 AircraftModel.specialModels.append(clazz)
1275
1276 @staticmethod
1277 def findSpecial(aircraft, aircraftInfo):
1278 for specialModel in AircraftModel.specialModels:
1279 if specialModel.doesHandle(aircraft, aircraftInfo):
1280 return specialModel
1281 return None
1282
1283 @staticmethod
1284 def create(aircraft, aircraftInfo):
1285 """Create the model for the given aircraft name, and notify the
1286 aircraft about it."""
1287 specialModel = AircraftModel.findSpecial(aircraft, aircraftInfo)
1288 if specialModel is not None:
1289 return specialModel()
1290 if aircraft.type in _genericModels:
1291 return _genericModels[aircraft.type]()
1292 else:
1293 return GenericModel()
1294
1295 @staticmethod
1296 def _convertFrequency(value):
1297 """Convert the given frequency value into a string."""
1298 return "%.2f" % (value/100.0,)
1299
1300 @staticmethod
1301 def _convertOBS(value):
1302 """Convert the given OBS value into an integer."""
1303 while value<0.0:
1304 value += 360.0
1305 return int(round(value))
1306
1307 def __init__(self, flapsNotches):
1308 """Construct the aircraft model.
1309
1310 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1311 self._flapsNotches = flapsNotches
1312
1313 @property
1314 def name(self):
1315 """Get the name for this aircraft model."""
1316 return "X-Plane/Generic"
1317
1318 def doesHandle(self, aircraft, aircraftInfo):
1319 """Determine if the model handles the given aircraft name.
1320
1321 This default implementation returns False."""
1322 return False
1323
1324 def _addDatarefWithIndexMember(self, dest, name, type, attrName = None):
1325 """Add the given X-Plane dataref name and type to the given array and a
1326 member attribute with the given name."""
1327 dest.append((name, type))
1328 if attrName is not None:
1329 setattr(self, attrName, len(dest)-1)
1330
1331 def _addDataWithIndexMembers(self, dest, prefix, data):
1332 """Add X-Plane dataref data to the given array and also corresponding
1333 index member variables with the given prefix.
1334
1335 data is a list of triplets of the following items:
1336 - the name of the data item. The index member variable will have a name
1337 created by prepending the given prefix to this name.
1338 - the X-Plane dataref name
1339 - the dataref type
1340
1341 The latter two items will be appended to dest."""
1342 for (name, datarefName, type) in data:
1343 self._addDatarefWithIndexMember(dest, datarefName, type,
1344 prefix + name)
1345
1346 def addMonitoringData(self, data, fsType):
1347 """Add the model-specific monitoring data to the given array."""
1348 self._addDataWithIndexMembers(data, "_monidx_",
1349 AircraftModel.monitoringData)
1350
1351 def getAircraftState(self, aircraft, timestamp, data):
1352 """Get an aircraft state object for the given monitoring data."""
1353 state = fs.AircraftState()
1354
1355 state.timestamp = timestamp
1356
1357 state.latitude = data[self._monidx_latitude]
1358 state.longitude = data[self._monidx_longitude]
1359 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1360
1361 state.paused = data[self._monidx_paused]!=0 or \
1362 data[self._monidx_replay]!=0
1363 state.trickMode = data[self._monidx_replay]!=0
1364
1365 state.overspeed = data[self._monidx_overspeed]!=0
1366 state.stalled = data[self._monidx_stalled]!=0
1367 state.onTheGround = data[self._monidx_onTheGround]!=0
1368
1369 state.zfw = data[self._monidx_emptyWeight] + \
1370 data[self._monidx_payloadWeight]
1371 state.grossWeight = data[self._monidx_grossWeight]
1372
1373 state.heading = data[self._monidx_heading]
1374
1375 state.pitch = data[self._monidx_pitch]
1376 state.bank = data[self._monidx_bank]
1377
1378 state.ias = data[self._monidx_ias]
1379 state.mach = data[self._monidx_mach]
1380 state.groundSpeed = data[self._monidx_groundSpeed] * _mps2knots
1381 state.vs = data[self._monidx_vs]
1382
1383 state.radioAltitude = data[self._monidx_radioAltitude]/.3048
1384 state.altitude = data[self._monidx_altitude]/.3048
1385
1386 state.gLoad = data[self._monidx_gLoad]
1387
1388 flapsControl = data[self._monidx_flapsControl]
1389 flapsIndex = int(flapsControl * (len(self._flapsNotches)-1))
1390 state.flapsSet = 0 if flapsIndex<1 else self._flapsNotches[flapsIndex]
1391
1392 state.flaps = self._flapsNotches[-1]*data[self._monidx_flapsLeft]
1393
1394 state.navLightsOn = data[self._monidx_navLights] != 0
1395 state.antiCollisionLightsOn = data[self._monidx_beaconLights] != 0
1396 state.landingLightsOn = data[self._monidx_landingLights] != 0
1397 state.strobeLightsOn = data[self._monidx_strobeLights] != 0
1398
1399 state.pitotHeatOn = data[self._monidx_pitot]!=0
1400
1401 state.parking = data[self._monidx_parking]>=0.5
1402
1403 state.gearControlDown = data[self._monidx_gearControl]!=0
1404 state.gearsDown = data[self._monidx_noseGear][0]>0.99
1405
1406 state.spoilersArmed = None
1407
1408 state.spoilersExtension = data[self._monidx_spoilers]*100.0
1409
1410 state.altimeter = data[self._monidx_altimeter]* _hgin2hpa
1411 state.altimeterReliable = True
1412 state.qnh = data[self._monidx_qnh]/100.0
1413
1414 state.ils = None
1415 state.ils_obs = None
1416 state.ils_manual = False
1417 state.nav1 = self._convertFrequency(data[self._monidx_nav1])
1418 state.nav1_obs = self._convertOBS(data[self._monidx_nav1_obs])
1419 state.nav1_manual = True
1420 state.nav2 = self._convertFrequency(data[self._monidx_nav2])
1421 state.nav2_obs = self._convertOBS(data[self._monidx_nav2_obs])
1422 state.nav2_manual = True
1423 state.adf1 = str(data[self._monidx_adf1])
1424 state.adf2 = str(data[self._monidx_adf2])
1425
1426 state.squawk = "%04d" % (data[self._monidx_squawk],)
1427
1428 state.windSpeed = data[self._monidx_windSpeed]
1429 state.windDirection = data[self._monidx_windDirection]
1430 if state.windDirection<0.0: state.windDirection += 360.0
1431
1432 state.visibility = data[self._monidx_visibility]
1433
1434 state.cog = data[self._monidx_cog]
1435
1436 state.xpdrC = data[self._monidx_xpdrC]==2
1437 state.autoXPDR = False
1438
1439 state.apMaster = data[self._monidx_apMaster]==2
1440 apState = data[self._monidx_apState]
1441 state.apHeadingHold = (apState&0x00002)!=0
1442 state.apHeading = data[self._monidx_apHeading]
1443 state.apAltitudeHold = (apState&0x04000)!=0
1444 state.apAltitude = data[self._monidx_apAltitude]
1445
1446 state.elevatorTrim = data[self._monidx_elevatorTrim] * 180.0 / math.pi
1447
1448 state.antiIceOn = data[self._monidx_antiIceOn]!=0 or \
1449 data[self._monidx_surfaceHeat]!=0 or \
1450 data[self._monidx_propHeat]!=0
1451
1452 return state
1453
1454#------------------------------------------------------------------------------
1455
1456class GenericAircraftModel(AircraftModel):
1457 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1458 values and some other common parameters in a generic way."""
1459
1460 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1461 """Construct the generic aircraft model with the given data.
1462
1463 flapsNotches is an array of how much degrees the individual flaps
1464 notches mean.
1465
1466 fuelTanks is an array of const.FUELTANK_XXX constants about the
1467 aircraft's fuel tanks. They will be converted to offsets.
1468
1469 numEngines is the number of engines the aircraft has.
1470
1471 isN1 determines if the engines have an N1 value or an RPM value
1472 (e.g. pistons)."""
1473 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1474
1475 self._fuelTanks = fuelTanks
1476 self._fuelTankCapacities = [1.0] * len(fuelTanks)
1477 self._fuelIndex = None
1478 self._numEngines = numEngines
1479 self._engineStartIndex = None
1480 self._isN1 = isN1
1481
1482 def doesHandle(self, aircraft, aircraftInfo):
1483 """Determine if the model handles the given aircraft name.
1484
1485 This implementation returns True."""
1486 return True
1487
1488 def addMonitoringData(self, data, fsType):
1489 """Add the model-specific monitoring data to the given array."""
1490 super(GenericAircraftModel, self).addMonitoringData(data, fsType)
1491
1492 self._fuelIndex = self._addFuelData(data)
1493
1494 self._engineStartIndex = len(data)
1495 if self._isN1:
1496 self._addDatarefWithIndexMember(data,
1497 "sim/flightmodel/engine/ENGN_N1_",
1498 (TYPE_FLOAT_ARRAY,
1499 self._numEngines))
1500 else:
1501 self._addDatarefWithIndexMember(data,
1502 "sim/flightmodel/engine/POINT_tacrad",
1503 (TYPE_FLOAT_ARRAY,
1504 self._numEngines))
1505
1506 self._addDatarefWithIndexMember(data,
1507 "sim/flightmodel/engine/ENGN_propmode",
1508 (TYPE_INT_ARRAY, self._numEngines))
1509
1510 def getAircraftState(self, aircraft, timestamp, data):
1511 """Get the aircraft state.
1512
1513 Get it from the parent, and then add the data about the fuel levels and
1514 the engine parameters."""
1515 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1516 timestamp,
1517 data)
1518
1519 state.fuel = []
1520 state.totalFuel = 0.0
1521
1522 fuelAmounts = data[self._fuelIndex]
1523 for i in range(0, len(self._fuelTanks)):
1524 amount = fuelAmounts[i]
1525 state.fuel.append((self._fuelTanks[i], amount))
1526 state.totalFuel += amount
1527
1528 power = data[self._engineStartIndex]
1529
1530 state.n1 = power[:] if self._isN1 else None
1531 state.rpm = None if self._isN1 else power[:]
1532
1533 propMode = data[self._engineStartIndex+1]
1534 state.reverser = [mode == 3 for mode in propMode]
1535
1536 return state
1537
1538 def getFuel(self, handler, callback):
1539 """Get the fuel information for this model.
1540
1541 See Simulator.getFuel for more information. This
1542 implementation simply queries the fuel tanks given to the
1543 constructor."""
1544 data = []
1545 self._addFuelData(data)
1546 data.append( ("sim/aircraft/weight/acf_m_fuel_tot", TYPE_FLOAT) )
1547 data.append( ("sim/aircraft/overflow/acf_tank_rat",
1548 (TYPE_FLOAT_ARRAY, len(self._fuelTanks)) ) )
1549
1550 handler.requestRead(data, self._handleFuelRetrieved,
1551 extra = callback)
1552
1553 def setFuelLevel(self, handler, levels):
1554 """Set the fuel level.
1555
1556 See the description of Simulator.setFuelLevel. This
1557 implementation simply sets the fuel tanks as given."""
1558 data = []
1559 for (tank, level) in levels:
1560 try:
1561 index = self._fuelTanks.index(tank)
1562 data.append( ("sim/flightmodel/weight/m_fuel",
1563 (TYPE_FLOAT_ARRAY, 1, index),
1564 [level * self._fuelTankCapacities[index]]) )
1565 except:
1566 print "xplane.Simulator.setFuelLevel: invalid tank constant: %d" % \
1567 (tank,)
1568
1569 handler.requestWrite(data, self._handleFuelWritten)
1570
1571 def _addFuelData(self, data):
1572 """Add the fuel offsets to the given data array.
1573
1574 Returns the index of the first fuel tank's data."""
1575 fuelStartIndex = len(data)
1576 data.append( ("sim/flightmodel/weight/m_fuel",
1577 (TYPE_FLOAT_ARRAY, len(self._fuelTanks) ) ) )
1578
1579 return fuelStartIndex
1580
1581 def _convertFuelData(self, data, index = 0, addCapacities = False):
1582 """Convert the given data into a fuel info list.
1583
1584 The list consists of two or three-tuples of the following
1585 items:
1586 - the fuel tank ID,
1587 - the amount of the fuel in kg,
1588 - if addCapacities is True, the total capacity of the tank."""
1589 fuelWeight = data[index] / 256.0
1590 index += 1
1591
1592 result = []
1593 totalFuel = 0
1594 for fuelTank in self._fuelTanks:
1595 capacity = data[index+1] * fuelWeight * const.LBSTOKG
1596 if capacity>=1.0:
1597 amount = data[index] * capacity / 128.0 / 65536.0
1598
1599 result.append( (fuelTank, amount, capacity) if addCapacities
1600 else (fuelTank, amount))
1601 totalFuel += amount
1602 index += 2
1603
1604 return (result, totalFuel)
1605
1606 def _handleFuelRetrieved(self, data, callback):
1607 """Callback for a fuel retrieval request."""
1608 result = []
1609 totalCapacity = data[1]
1610 for index in range(0, len(self._fuelTanks)):
1611 amount = data[0][index]
1612 capacity = data[2][index] * totalCapacity
1613 self._fuelTankCapacities[index] = capacity
1614 result.append( (self._fuelTanks[index], amount, capacity) )
1615
1616 callback(result)
1617
1618 def _handleFuelWritten(self, success, extra):
1619 """Callback for a fuel setting request."""
1620 pass
1621
1622#------------------------------------------------------------------------------
1623
1624class GenericModel(GenericAircraftModel):
1625 """Generic aircraft model for an unknown type."""
1626 def __init__(self):
1627 """Construct the model."""
1628 super(GenericModel, self). \
1629 __init__(flapsNotches = [0, 10, 20, 30],
1630 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1631 numEngines = 2)
1632
1633 @property
1634 def name(self):
1635 """Get the name for this aircraft model."""
1636 return "X-Plane/Generic"
1637
1638#------------------------------------------------------------------------------
1639
1640class B737Model(GenericAircraftModel):
1641 """Generic model for the Boeing 737 Classing and NG aircraft."""
1642 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1643
1644 def __init__(self):
1645 """Construct the model."""
1646 super(B737Model, self). \
1647 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1648 fuelTanks = B737Model.fuelTanks,
1649 numEngines = 2)
1650
1651 @property
1652 def name(self):
1653 """Get the name for this aircraft model."""
1654 return "X-Plane/Generic Boeing 737"
1655
1656#------------------------------------------------------------------------------
1657
1658class B767Model(GenericAircraftModel):
1659 """Generic model for the Boeing 767 aircraft."""
1660 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1661
1662 def __init__(self):
1663 """Construct the model."""
1664 super(B767Model, self). \
1665 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1666 fuelTanks = B767Model.fuelTanks,
1667 numEngines = 2)
1668
1669 @property
1670 def name(self):
1671 """Get the name for this aircraft model."""
1672 return "X-Plane/Generic Boeing 767"
1673
1674#------------------------------------------------------------------------------
1675
1676class DH8DModel(GenericAircraftModel):
1677 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1678 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1679
1680 def __init__(self):
1681 """Construct the model."""
1682 super(DH8DModel, self). \
1683 __init__(flapsNotches = [0, 5, 10, 15, 35],
1684 fuelTanks = DH8DModel.fuelTanks,
1685 numEngines = 2)
1686
1687 @property
1688 def name(self):
1689 """Get the name for this aircraft model."""
1690 return "X-Plane/Generic Bombardier Dash 8-Q400"
1691
1692#------------------------------------------------------------------------------
1693
1694class FJSDH8DModel(DH8DModel):
1695 """Model handler for the FlyJSim Dash 8-Q400."""
1696 @staticmethod
1697 def doesHandle(aircraft, (tailnum, author, description, notes,
1698 icao, liveryPath)):
1699 """Determine if this model handler handles the aircraft with the given
1700 name."""
1701 return aircraft.type==const.AIRCRAFT_DH8D and \
1702 description.find("Dash 8 Q400")!=-1 and \
1703 liveryPath.startswith("Aircraft/Heavy Metal/Dash 8 Q400")
1704
1705 @property
1706 def name(self):
1707 """Get the name for this aircraft model."""
1708 return "X-Plane/FlyJSim Bombardier Dash 8-Q400"
1709
1710 def getAircraftState(self, aircraft, timestamp, data):
1711 """Get the aircraft state.
1712
1713 Get it from the parent, and then invert the pitot heat state."""
1714 state = super(FJSDH8DModel, self).getAircraftState(aircraft,
1715 timestamp,
1716 data)
1717 state.antiCollisionLightsOn = \
1718 state.antiCollisionLightsOn or state.strobeLightsOn
1719
1720 return state
1721
1722#------------------------------------------------------------------------------
1723
1724class CRJ2Model(GenericAircraftModel):
1725 """Generic model for the Bombardier CRJ-200 aircraft."""
1726 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1727
1728 def __init__(self):
1729 """Construct the model."""
1730 super(CRJ2Model, self). \
1731 __init__(flapsNotches = [0, 8, 20, 30, 45],
1732 fuelTanks = CRJ2Model.fuelTanks,
1733 numEngines = 2)
1734
1735 @property
1736 def name(self):
1737 """Get the name for this aircraft model."""
1738 return "X-Plane/Generic Bombardier CRJ-200"
1739
1740#------------------------------------------------------------------------------
1741
1742class F70Model(GenericAircraftModel):
1743 """Generic model for the Fokker F70 aircraft."""
1744 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1745
1746 def __init__(self):
1747 """Construct the model."""
1748 super(F70Model, self). \
1749 __init__(flapsNotches = [0, 8, 15, 25, 42],
1750 fuelTanks = F70Model.fuelTanks,
1751 numEngines = 2)
1752
1753 @property
1754 def name(self):
1755 """Get the name for this aircraft model."""
1756 return "X-Plane/Generic Fokker 70"
1757
1758#------------------------------------------------------------------------------
1759
1760class DC3Model(GenericAircraftModel):
1761 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1762 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
1763 const.FUELTANK_RIGHT]
1764 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1765 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1766
1767 def __init__(self):
1768 """Construct the model."""
1769 super(DC3Model, self). \
1770 __init__(flapsNotches = [0, 15, 30, 45],
1771 fuelTanks = DC3Model.fuelTanks,
1772 numEngines = 2, isN1 = False)
1773 self._leftLevel = 0.0
1774 self._rightLevel = 0.0
1775
1776 @property
1777 def name(self):
1778 """Get the name for this aircraft model."""
1779 return "X-Plane/Generic Lisunov Li-2 (DC-3)"
1780
1781#------------------------------------------------------------------------------
1782
1783class T134Model(GenericAircraftModel):
1784 """Generic model for the Tupolev Tu-134 aircraft."""
1785 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
1786 const.FUELTANK_LEFT_AUX,
1787 const.FUELTANK_CENTRE,
1788 const.FUELTANK_RIGHT_AUX,
1789 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
1790
1791 def __init__(self):
1792 """Construct the model."""
1793 super(T134Model, self). \
1794 __init__(flapsNotches = [0, 10, 20, 30],
1795 fuelTanks = T134Model.fuelTanks,
1796 numEngines = 2)
1797
1798 @property
1799 def name(self):
1800 """Get the name for this aircraft model."""
1801 return "X-Plane/Generic Tupolev Tu-134"
1802
1803#------------------------------------------------------------------------------
1804
1805class T154Model(GenericAircraftModel):
1806 """Generic model for the Tupolev Tu-134 aircraft."""
1807 fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1808 const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
1809 const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1810
1811 def __init__(self):
1812 """Construct the model."""
1813 super(T154Model, self). \
1814 __init__(flapsNotches = [0, 15, 28, 45],
1815 fuelTanks = T154Model.fuelTanks,
1816 numEngines = 3)
1817
1818 @property
1819 def name(self):
1820 """Get the name for this aircraft model."""
1821 return "X-Plane/Generic Tupolev Tu-154"
1822
1823 def getAircraftState(self, aircraft, timestamp, data):
1824 """Get an aircraft state object for the given monitoring data.
1825
1826 This removes the reverser value for the middle engine."""
1827 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1828 del state.reverser[1]
1829 return state
1830
1831#------------------------------------------------------------------------------
1832
1833class YK40Model(GenericAircraftModel):
1834 """Generic model for the Yakovlev Yak-40 aircraft."""
1835 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1836
1837 def __init__(self):
1838 """Construct the model."""
1839 super(YK40Model, self). \
1840 __init__(flapsNotches = [0, 20, 35],
1841 fuelTanks = YK40Model.fuelTanks,
1842 numEngines = 2)
1843
1844 @property
1845 def name(self):
1846 """Get the name for this aircraft model."""
1847 return "X-Plane/Generic Yakovlev Yak-40"
1848
1849#------------------------------------------------------------------------------
1850
1851_genericModels = { const.AIRCRAFT_B736 : B737Model,
1852 const.AIRCRAFT_B737 : B737Model,
1853 const.AIRCRAFT_B738 : B737Model,
1854 const.AIRCRAFT_B738C : B737Model,
1855 const.AIRCRAFT_B733 : B737Model,
1856 const.AIRCRAFT_B734 : B737Model,
1857 const.AIRCRAFT_B735 : B737Model,
1858 const.AIRCRAFT_DH8D : DH8DModel,
1859 const.AIRCRAFT_B762 : B767Model,
1860 const.AIRCRAFT_B763 : B767Model,
1861 const.AIRCRAFT_CRJ2 : CRJ2Model,
1862 const.AIRCRAFT_F70 : F70Model,
1863 const.AIRCRAFT_DC3 : DC3Model,
1864 const.AIRCRAFT_T134 : T134Model,
1865 const.AIRCRAFT_T154 : T154Model,
1866 const.AIRCRAFT_YK40 : YK40Model }
1867
1868#------------------------------------------------------------------------------
1869
1870AircraftModel.registerSpecial(FJSDH8DModel)
Note: See TracBrowser for help on using the repository browser.