source: src/mlx/xplane.py@ 531:f8ffb59d8112

xplane
Last change on this file since 531:f8ffb59d8112 was 505:3444589a54b4, checked in by István Váradi <ivaradi@…>, 12 years ago

The latest version of the FlyJSim Dash-8 Q400 is detected properly

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