source: src/mlx/xplane.py@ 424:3512d665bdfe

xplane
Last change on this file since 424:3512d665bdfe was 424:3512d665bdfe, checked in by István Váradi <ivaradi@…>, 12 years ago

Weight query for the help window works too

File size: 71.0 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 data = [ ("sim/aircraft/weight/acf_m_empty", TYPE_FLOAT),
749 ("sim/flightmodel/weight/m_fixed", TYPE_FLOAT),
750 ("sim/flightmodel/weight/m_total", TYPE_FLOAT) ]
751 self._handler.requestRead(data, self._handleWeights,
752 extra = callback)
753
754 def requestTime(self, callback):
755 """Request the time from the simulator."""
756 self._handler.requestRead(Simulator.timeData, self._handleTime,
757 extra = callback)
758
759 def startMonitoring(self):
760 """Start the periodic monitoring of the aircraft and pass the resulting
761 state to the aircraft object periodically."""
762 assert not self._monitoringRequested
763 self._monitoringRequested = True
764
765 def stopMonitoring(self):
766 """Stop the periodic monitoring of the aircraft."""
767 assert self._monitoringRequested
768 self._monitoringRequested = False
769
770 def startFlare(self):
771 """Start monitoring the flare time.
772
773 At present it is assumed to be called from the handler thread, hence no
774 protection."""
775 #self._aircraft.logger.debug("startFlare")
776 if self._flareRequestID is None:
777 self._flareRates = []
778 self._flareRequestID = \
779 self._handler.requestPeriodicRead(0.1,
780 Simulator.flareData1,
781 self._handleFlare1)
782
783 def cancelFlare(self):
784 """Cancel monitoring the flare time.
785
786 At present it is assumed to be called from the handler thread, hence no
787 protection."""
788 if self._flareRequestID is not None:
789 self._handler.clearPeriodic(self._flareRequestID)
790 self._flareRequestID = None
791
792 def sendMessage(self, message, duration = 3,
793 _disconnect = False):
794 """Send a message to the pilot via the simulator.
795
796 duration is the number of seconds to keep the message displayed."""
797 print "xplra.Simulator.sendMessage:", message
798 self._handler.requestShowMessage(message, duration,
799 self._handleMessageSent,
800 extra = _disconnect)
801
802 def getFuel(self, callback):
803 """Get the fuel information for the current model.
804
805 The callback will be called with a list of triplets with the following
806 items:
807 - the fuel tank identifier
808 - the current weight of the fuel in the tank (in kgs)
809 - the current total capacity of the tank (in kgs)."""
810 if self._aircraftModel is None:
811 self._fuelCallback = callback
812 else:
813 self._aircraftModel.getFuel(self._handler, callback)
814
815 def setFuelLevel(self, levels):
816 """Set the fuel level to the given ones.
817
818 levels is an array of two-tuples, where each tuple consists of the
819 following:
820 - the const.FUELTANK_XXX constant denoting the tank that must be set,
821 - the requested level of the fuel as a floating-point value between 0.0
822 and 1.0."""
823 if self._aircraftModel is not None:
824 self._aircraftModel.setFuelLevel(self._handler, levels)
825
826 def enableTimeSync(self):
827 """Enable the time synchronization."""
828 self._nextSyncTime = -1
829 self._syncTime = True
830
831 def disableTimeSync(self):
832 """Enable the time synchronization."""
833 self._syncTime = False
834 self._nextSyncTime = -1
835
836 def listenHotkeys(self, hotkeys, callback):
837 """Start listening to the given hotkeys.
838
839 callback is function expecting two arguments:
840 - the ID of the hotkey set as returned by this function,
841 - the list of the indexes of the hotkeys that were pressed."""
842 with self._hotkeyLock:
843 assert self._hotkeyCodes is None
844
845 self._hotkeyCodes = \
846 [self._getHotkeyCode(hotkey) for hotkey in hotkeys]
847 self._hotkeySetID += 1
848 self._hotkeySetGeneration = 0
849 self._hotkeyCallback = callback
850
851 self._handler.registerHotkeys(self._hotkeyCodes,
852 self._handleHotkeysRegistered,
853 (self._hotkeySetID,
854 self._hotkeySetGeneration))
855
856 return self._hotkeySetID
857
858 def clearHotkeys(self):
859 """Clear the current hotkey set.
860
861 Note that it is possible, that the callback function set either
862 previously or after calling this function by listenHotkeys() will be
863 called with data from the previous hotkey set.
864
865 Therefore it is recommended to store the hotkey set ID somewhere and
866 check that in the callback function. Right before calling
867 clearHotkeys(), this stored ID should be cleared so that the check
868 fails for sure."""
869 with self._hotkeyLock:
870 if self._hotkeyCodes is not None:
871 self._hotkeyCodes = None
872 self._hotkeySetID += 1
873 self._hotkeyCallback = None
874 self._clearHotkeyRequest()
875
876 def disconnect(self, closingMessage = None, duration = 3):
877 """Disconnect from the simulator."""
878 assert not self._monitoringRequested
879
880 print "xplra.Simulator.disconnect", closingMessage, duration
881
882 self._stopNormal()
883 self.clearHotkeys()
884 if closingMessage is None:
885 self._handler.disconnect()
886 else:
887 self.sendMessage(closingMessage, duration = duration,
888 _disconnect = True)
889
890 def connected(self, fsType, descriptor):
891 """Called when a connection has been established to the flight
892 simulator of the given type."""
893 self._fsType = fsType
894 with self._hotkeyLock:
895 if self._hotkeyCodes is not None:
896 self._hotkeySetGeneration += 1
897
898 self._handler.registerHotkeys(self._hotkeyCodes,
899 self._handleHotkeysRegistered,
900 (self._hotkeySetID,
901 self._hotkeySetGeneration))
902
903 self._connectionListener.connected(fsType, descriptor)
904
905 def connectionFailed(self):
906 """Called when the connection could not be established."""
907 with self._hotkeyLock:
908 self._clearHotkeyRequest()
909 self._connectionListener.connectionFailed()
910
911 def disconnected(self):
912 """Called when a connection to the flight simulator has been broken."""
913 with self._hotkeyLock:
914 self._clearHotkeyRequest()
915 self._connectionListener.disconnected()
916
917 def _startDefaultNormal(self):
918 """Start the default normal periodic request."""
919 assert self._normalRequestID is None
920 self._normalRequestID = \
921 self._handler.requestPeriodicRead(1.0,
922 Simulator.normalData,
923 self._handleNormal)
924
925 def _stopNormal(self):
926 """Stop the normal period request."""
927 assert self._normalRequestID is not None
928 self._handler.clearPeriodic(self._normalRequestID)
929 self._normalRequestID = None
930 self._monitoring = False
931
932 def _handleNormal(self, data, extra):
933 """Handle the reply to the normal request.
934
935 At the beginning the result consists the data for normalData. When
936 monitoring is started, it contains the result also for the
937 aircraft-specific values.
938 """
939 timestamp = Simulator._getTimestamp(data)
940
941 createdNewModel = self._setAircraftName(timestamp,
942 data.getString(2),
943 data.getString(3),
944 data.getString(4),
945 data.getString(5),
946 data.getString(6),
947 data.getString(7))
948 if self._fuelCallback is not None:
949 self._aircraftModel.getFuel(self._handler, self._fuelCallback)
950 self._fuelCallback = None
951
952 if self._monitoringRequested and not self._monitoring:
953 self._stopNormal()
954 self._startMonitoring()
955 elif self._monitoring and not self._monitoringRequested:
956 self._stopNormal()
957 self._startDefaultNormal()
958 elif self._monitoring and self._aircraftModel is not None and \
959 not createdNewModel:
960 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
961 timestamp, data)
962
963 self._aircraft.handleState(aircraftState)
964
965 def _setAircraftName(self, timestamp, tailnum, author, description,
966 notes, icao, liveryPath):
967 """Set the name of the aicraft and if it is different from the
968 previous, create a new model for it.
969
970 If so, also notifty the aircraft about the change.
971
972 Return if a new model was created."""
973 author = self._latin1decoder(author)[0]
974 description = self._latin1decoder(description)[0]
975 notes = self._latin1decoder(notes)[0]
976 liveryPath = self._latin1decoder(liveryPath)[0]
977
978 aircraftInfo = (tailnum, author, description, notes, icao, liveryPath)
979 if aircraftInfo==self._aircraftInfo:
980 return False
981
982 print "xplane.Simulator: new data: %s, %s, %s, %s, %s, %s" % \
983 (tailnum, author, description, notes, icao, liveryPath)
984
985 self._aircraftInfo = aircraftInfo
986 needNew = self._aircraftModel is None
987 needNew = needNew or\
988 not self._aircraftModel.doesHandle(self._aircraft, aircraftInfo)
989 if not needNew:
990 specialModel = AircraftModel.findSpecial(self._aircraft,
991 aircraftInfo)
992 needNew = specialModel is not None and \
993 specialModel is not self._aircraftModel.__class__
994
995 if needNew:
996 self._setAircraftModel(AircraftModel.create(self._aircraft,
997 aircraftInfo))
998
999 self._aircraft.modelChanged(timestamp, description,
1000 self._aircraftModel.name)
1001
1002 return needNew
1003
1004 def _setAircraftModel(self, model):
1005 """Set a new aircraft model.
1006
1007 It will be queried for the data to monitor and the monitoring request
1008 will be replaced by a new one."""
1009 self._aircraftModel = model
1010
1011 if self._monitoring:
1012 self._stopNormal()
1013 self._startMonitoring()
1014
1015 def _startMonitoring(self):
1016 """Start monitoring with the current aircraft model."""
1017 data = Simulator.normalData[:]
1018 self._aircraftModel.addMonitoringData(data, self._fsType)
1019
1020 self._normalRequestID = \
1021 self._handler.requestPeriodicRead(1.0, data,
1022 self._handleNormal)
1023 self._monitoring = True
1024
1025 def _addFlareRate(self, data):
1026 """Append a flare rate to the list of last rates."""
1027 if len(self._flareRates)>=3:
1028 del self._flareRates[0]
1029 self._flareRates.append(data)
1030
1031 def _handleFlare1(self, data, normal):
1032 """Handle the first stage of flare monitoring."""
1033 #self._aircraft.logger.debug("handleFlare1: " + str(data))
1034 if data[1]<=50.0*0.3048:
1035 self._flareStart = time.time()
1036 self._flareStartFS = data[0]
1037 self._handler.clearPeriodic(self._flareRequestID)
1038 self._handler.requestRead(Simulator.flareStartData,
1039 self._handleFlareStart)
1040
1041 self._addFlareRate(data[2])
1042
1043 def _handleFlareStart(self, data, extra):
1044 """Handle the data need to notify the aircraft about the starting of
1045 the flare."""
1046 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
1047 if data is not None:
1048 windDirection = data[1]
1049 if windDirection<0.0: windDirection += 360.0
1050 self._aircraft.flareStarted(data[0], windDirection, data[2],
1051 self._flareStart, self._flareStartFS)
1052
1053 self._flareRequestID = \
1054 self._handler.requestPeriodicRead(0.1,
1055 Simulator.flareData2,
1056 self._handleFlare2)
1057
1058 def _handleFlare2(self, data, normal):
1059 """Handle the first stage of flare monitoring."""
1060 #self._aircraft.logger.debug("handleFlare2: " + str(data))
1061 if data[1]!=0:
1062 flareEnd = time.time()
1063 self._handler.clearPeriodic(self._flareRequestID)
1064 self._flareRequestID = None
1065
1066 flareEndFS = data[0]
1067 if flareEndFS<self._flareStartFS:
1068 flareEndFS += 86400.0
1069
1070 tdRate = min(self._flareRates)
1071 tdRateCalculatedByFS = False
1072
1073 heading = data[6]
1074 if heading<0.0: heading += 360.0
1075
1076 self._aircraft.flareFinished(flareEnd, flareEndFS,
1077 tdRate, tdRateCalculatedByFS,
1078 data[3], data[4], data[5], heading)
1079 else:
1080 self._addFlareRate(data[2])
1081
1082 def _handleZFW(self, data, callback):
1083 """Callback for a ZFW retrieval request."""
1084 zfw = data[0] + data[1]
1085 callback(zfw)
1086
1087 def _handleTime(self, data, callback):
1088 """Callback for a time retrieval request."""
1089 callback(Simulator._getTimestamp(data))
1090
1091 def _handleWeights(self, data, callback):
1092 """Callback for the weights retrieval request."""
1093 dow = data[0]
1094 payload = data[1]
1095 zfw = dow + payload
1096 grossWeight = data[2]
1097 callback(dow, payload, zfw, grossWeight)
1098
1099 def _handleMessageSent(self, success, disconnect):
1100 """Callback for a message sending request."""
1101 #print "xplra.Simulator._handleMessageSent", disconnect
1102 if disconnect:
1103 self._handler.disconnect()
1104
1105 def _handleHotkeysRegistered(self, success, (id, generation)):
1106 """Handle the result of the hotkeys having been written."""
1107 with self._hotkeyLock:
1108 if success and id==self._hotkeySetID and \
1109 generation==self._hotkeySetGeneration:
1110 self._hotkeyRequestID = \
1111 self._handler.requestHotkeysState(0.5,
1112 self._handleHotkeys,
1113 (id, generation))
1114
1115 def _handleHotkeys(self, data, (id, generation)):
1116 """Handle the hotkeys."""
1117 with self._hotkeyLock:
1118 if id!=self._hotkeySetID or generation!=self._hotkeySetGeneration:
1119 return
1120
1121 callback = self._hotkeyCallback
1122 offsets = self._hotkeyOffets
1123
1124 hotkeysPressed = []
1125 for i in range(0, len(data)):
1126 if data[i]!=0:
1127 hotkeysPressed.append(i)
1128
1129 if hotkeysPressed:
1130 callback(id, hotkeysPressed)
1131
1132 def _clearHotkeyRequest(self):
1133 """Clear the hotkey request in the handler if there is any."""
1134 if self._hotkeyRequestID is not None:
1135 self._handler.unregisterHotkeys(self._hotkeysUnregistered)
1136 self._handler.clearPeriodic(self._hotkeyRequestID)
1137 self._hotkeyRequestID = None
1138
1139 def _hotkeysUnregistered(self):
1140 """Called when the hotkeys have been unregistered."""
1141 pass
1142
1143#------------------------------------------------------------------------------
1144
1145class AircraftModel(object):
1146 """Base class for the aircraft models.
1147
1148 Aircraft models handle the data arriving from X-Plane and turn it into an
1149 object describing the aircraft's state."""
1150 monitoringData = [ ("paused",
1151 "sim/time/paused", TYPE_INT),
1152 ("latitude",
1153 "sim/flightmodel/position/latitude", TYPE_DOUBLE),
1154 ("longitude",
1155 "sim/flightmodel/position/longitude", TYPE_DOUBLE),
1156 ("replay",
1157 "sim/operation/prefs/replay_mode", TYPE_INT),
1158 ("overspeed",
1159 "sim/flightmodel/failures/over_vne", TYPE_INT),
1160 ("stalled",
1161 "sim/flightmodel/failures/stallwarning", TYPE_INT),
1162 ("onTheGround",
1163 "sim/flightmodel/failures/onground_any", TYPE_INT),
1164 ("emptyWeight",
1165 "sim/aircraft/weight/acf_m_empty", TYPE_FLOAT),
1166 ("payloadWeight",
1167 "sim/flightmodel/weight/m_fixed", TYPE_FLOAT),
1168 ("grossWeight",
1169 "sim/flightmodel/weight/m_total", TYPE_FLOAT),
1170 ("heading",
1171 "sim/flightmodel/position/psi", TYPE_FLOAT),
1172 ("pitch",
1173 "sim/flightmodel/position/theta", TYPE_FLOAT),
1174 ("bank",
1175 "sim/flightmodel/position/phi", TYPE_FLOAT),
1176 ("ias",
1177 "sim/flightmodel/position/indicated_airspeed2",
1178 TYPE_FLOAT),
1179 ("mach",
1180 "sim/flightmodel/misc/machno", TYPE_FLOAT),
1181 ("groundSpeed",
1182 "sim/flightmodel/position/groundspeed", TYPE_FLOAT),
1183 ("vs",
1184 "sim/flightmodel/position/vh_ind_fpm2", TYPE_FLOAT),
1185 ("radioAltitude",
1186 "sim/flightmodel/position/y_agl", TYPE_FLOAT),
1187 ("altitude",
1188 "sim/flightmodel/position/elevation", TYPE_FLOAT),
1189 ("gLoad",
1190 "sim/flightmodel/forces/g_nrml", TYPE_FLOAT),
1191 ("flapsControl",
1192 "sim/flightmodel/controls/flaprqst", TYPE_FLOAT),
1193 ("flapsLeft",
1194 "sim/flightmodel/controls/flaprat", TYPE_FLOAT),
1195 ("flapsRight",
1196 "sim/flightmodel/controls/flap2rat", TYPE_FLOAT),
1197 ("navLights",
1198 "sim/cockpit/electrical/nav_lights_on", TYPE_INT),
1199 ("beaconLights",
1200 "sim/cockpit/electrical/beacon_lights_on", TYPE_INT),
1201 ("strobeLights",
1202 "sim/cockpit/electrical/strobe_lights_on", TYPE_INT),
1203 ("landingLights",
1204 "sim/cockpit/electrical/landing_lights_on", TYPE_INT),
1205 ("pitot",
1206 "sim/cockpit/switches/pitot_heat_on", TYPE_INT),
1207 ("parking",
1208 "sim/flightmodel/controls/parkbrake", TYPE_FLOAT),
1209 ("gearControl",
1210 "sim/cockpit2/controls/gear_handle_down", TYPE_INT),
1211 ("noseGear",
1212 "sim/flightmodel2/gear/deploy_ratio",
1213 (TYPE_FLOAT_ARRAY, 1)),
1214 ("spoilers",
1215 "sim/flightmodel/controls/lsplrdef", TYPE_FLOAT),
1216 ("altimeter",
1217 "sim/cockpit/misc/barometer_setting", TYPE_FLOAT),
1218 ("qnh",
1219 "sim/physics/earth_pressure_p", TYPE_FLOAT),
1220 ("nav1",
1221 "sim/cockpit/radios/nav1_freq_hz", TYPE_INT),
1222 ("nav1_obs",
1223 "sim/cockpit/radios/nav1_obs_degm", TYPE_FLOAT),
1224 ("nav2",
1225 "sim/cockpit/radios/nav2_freq_hz", TYPE_INT),
1226 ("nav2_obs",
1227 "sim/cockpit/radios/nav2_obs_degm", TYPE_FLOAT),
1228 ("adf1",
1229 "sim/cockpit/radios/adf1_freq_hz", TYPE_INT),
1230 ("adf2",
1231 "sim/cockpit/radios/adf2_freq_hz", TYPE_INT),
1232 ("squawk",
1233 "sim/cockpit/radios/transponder_code", TYPE_INT),
1234 ("windSpeed",
1235 "sim/weather/wind_speed_kt[0]", TYPE_FLOAT),
1236 ("windDirection",
1237 "sim/weather/wind_direction_degt[0]", TYPE_FLOAT),
1238 ("visibility",
1239 "sim/weather/visibility_reported_m", TYPE_FLOAT),
1240 ("cog",
1241 "sim/flightmodel/misc/cgz_ref_to_default", TYPE_FLOAT),
1242 ("xpdrC",
1243 "sim/cockpit/radios/transponder_mode", TYPE_INT),
1244 ("apMaster",
1245 "sim/cockpit/autopilot/autopilot_mode", TYPE_INT),
1246 ("apState",
1247 "sim/cockpit/autopilot/autopilot_state", TYPE_INT),
1248 ("apHeading",
1249 "sim/cockpit/autopilot/heading_mag", TYPE_FLOAT),
1250 ("apAltitude",
1251 "sim/cockpit/autopilot/altitude", TYPE_FLOAT),
1252 ("elevatorTrim",
1253 "sim/flightmodel/controls/elv_trim", TYPE_FLOAT),
1254 ("antiIceOn",
1255 "sim/cockpit/switches/anti_ice_on", TYPE_INT),
1256 ("surfaceHeat",
1257 "sim/cockpit/switches/anti_ice_surf_heat", TYPE_INT),
1258 ("propHeat",
1259 "sim/cockpit/switches/anti_ice_prop_heat", TYPE_INT) ]
1260
1261 specialModels = []
1262
1263 @staticmethod
1264 def registerSpecial(clazz):
1265 """Register the given class as a special model."""
1266 AircraftModel.specialModels.append(clazz)
1267
1268 @staticmethod
1269 def findSpecial(aircraft, aircraftInfo):
1270 for specialModel in AircraftModel.specialModels:
1271 if specialModel.doesHandle(aircraft, aircraftInfo):
1272 return specialModel
1273 return None
1274
1275 @staticmethod
1276 def create(aircraft, aircraftInfo):
1277 """Create the model for the given aircraft name, and notify the
1278 aircraft about it."""
1279 specialModel = AircraftModel.findSpecial(aircraft, aircraftInfo)
1280 if specialModel is not None:
1281 return specialModel()
1282 if aircraft.type in _genericModels:
1283 return _genericModels[aircraft.type]()
1284 else:
1285 return GenericModel()
1286
1287 @staticmethod
1288 def _convertFrequency(value):
1289 """Convert the given frequency value into a string."""
1290 return "%.2f" % (value/100.0,)
1291
1292 @staticmethod
1293 def _convertOBS(value):
1294 """Convert the given OBS value into an integer."""
1295 while value<0.0:
1296 value += 360.0
1297 return int(round(value))
1298
1299 def __init__(self, flapsNotches):
1300 """Construct the aircraft model.
1301
1302 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1303 self._flapsNotches = flapsNotches
1304
1305 @property
1306 def name(self):
1307 """Get the name for this aircraft model."""
1308 return "X-Plane/Generic"
1309
1310 def doesHandle(self, aircraft, aircraftInfo):
1311 """Determine if the model handles the given aircraft name.
1312
1313 This default implementation returns False."""
1314 return False
1315
1316 def _addDatarefWithIndexMember(self, dest, name, type, attrName = None):
1317 """Add the given X-Plane dataref name and type to the given array and a
1318 member attribute with the given name."""
1319 dest.append((name, type))
1320 if attrName is not None:
1321 setattr(self, attrName, len(dest)-1)
1322
1323 def _addDataWithIndexMembers(self, dest, prefix, data):
1324 """Add X-Plane dataref data to the given array and also corresponding
1325 index member variables with the given prefix.
1326
1327 data is a list of triplets of the following items:
1328 - the name of the data item. The index member variable will have a name
1329 created by prepending the given prefix to this name.
1330 - the X-Plane dataref name
1331 - the dataref type
1332
1333 The latter two items will be appended to dest."""
1334 for (name, datarefName, type) in data:
1335 self._addDatarefWithIndexMember(dest, datarefName, type,
1336 prefix + name)
1337
1338 def addMonitoringData(self, data, fsType):
1339 """Add the model-specific monitoring data to the given array."""
1340 self._addDataWithIndexMembers(data, "_monidx_",
1341 AircraftModel.monitoringData)
1342
1343 def getAircraftState(self, aircraft, timestamp, data):
1344 """Get an aircraft state object for the given monitoring data."""
1345 state = fs.AircraftState()
1346
1347 state.timestamp = timestamp
1348
1349 state.latitude = data[self._monidx_latitude]
1350 state.longitude = data[self._monidx_longitude]
1351 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1352
1353 state.paused = data[self._monidx_paused]!=0 or \
1354 data[self._monidx_replay]!=0
1355 state.trickMode = data[self._monidx_replay]!=0
1356
1357 state.overspeed = data[self._monidx_overspeed]!=0
1358 state.stalled = data[self._monidx_stalled]!=0
1359 state.onTheGround = data[self._monidx_onTheGround]!=0
1360
1361 state.zfw = data[self._monidx_emptyWeight] + \
1362 data[self._monidx_payloadWeight]
1363 state.grossWeight = data[self._monidx_grossWeight]
1364
1365 state.heading = data[self._monidx_heading]
1366
1367 state.pitch = data[self._monidx_pitch]
1368 state.bank = data[self._monidx_bank]
1369
1370 state.ias = data[self._monidx_ias]
1371 state.mach = data[self._monidx_mach]
1372 state.groundSpeed = data[self._monidx_groundSpeed] * _mps2knots
1373 state.vs = data[self._monidx_vs]
1374
1375 state.radioAltitude = data[self._monidx_radioAltitude]/.3048
1376 state.altitude = data[self._monidx_altitude]/.3048
1377
1378 state.gLoad = data[self._monidx_gLoad]
1379
1380 flapsControl = data[self._monidx_flapsControl]
1381 flapsIndex = int(flapsControl * (len(self._flapsNotches)-1))
1382 state.flapsSet = 0 if flapsIndex<1 else self._flapsNotches[flapsIndex]
1383
1384 state.flaps = self._flapsNotches[-1]*data[self._monidx_flapsLeft]
1385
1386 state.navLightsOn = data[self._monidx_navLights] != 0
1387 state.antiCollisionLightsOn = data[self._monidx_beaconLights] != 0
1388 state.landingLightsOn = data[self._monidx_landingLights] != 0
1389 state.strobeLightsOn = data[self._monidx_strobeLights] != 0
1390
1391 state.pitotHeatOn = data[self._monidx_pitot]!=0
1392
1393 state.parking = data[self._monidx_parking]>=0.5
1394
1395 state.gearControlDown = data[self._monidx_gearControl]!=0
1396 state.gearsDown = data[self._monidx_noseGear][0]>0.99
1397
1398 state.spoilersArmed = None
1399
1400 state.spoilersExtension = data[self._monidx_spoilers]*100.0
1401
1402 state.altimeter = data[self._monidx_altimeter]* _hgin2hpa
1403 state.altimeterReliable = True
1404 state.qnh = data[self._monidx_qnh]/100.0
1405
1406 state.ils = None
1407 state.ils_obs = None
1408 state.ils_manual = False
1409 state.nav1 = self._convertFrequency(data[self._monidx_nav1])
1410 state.nav1_obs = self._convertOBS(data[self._monidx_nav1_obs])
1411 state.nav1_manual = True
1412 state.nav2 = self._convertFrequency(data[self._monidx_nav2])
1413 state.nav2_obs = self._convertOBS(data[self._monidx_nav2_obs])
1414 state.nav2_manual = True
1415 state.adf1 = str(data[self._monidx_adf1])
1416 state.adf2 = str(data[self._monidx_adf2])
1417
1418 state.squawk = "%04d" % (data[self._monidx_squawk],)
1419
1420 state.windSpeed = data[self._monidx_windSpeed]
1421 state.windDirection = data[self._monidx_windDirection]
1422 if state.windDirection<0.0: state.windDirection += 360.0
1423
1424 state.visibility = data[self._monidx_visibility]
1425
1426 state.cog = data[self._monidx_cog]
1427
1428 state.xpdrC = data[self._monidx_xpdrC]==2
1429 state.autoXPDR = False
1430
1431 state.apMaster = data[self._monidx_apMaster]==2
1432 apState = data[self._monidx_apState]
1433 state.apHeadingHold = (apState&0x00002)!=0
1434 state.apHeading = data[self._monidx_apHeading]
1435 state.apAltitudeHold = (apState&0x04000)!=0
1436 state.apAltitude = data[self._monidx_apAltitude]
1437
1438 state.elevatorTrim = data[self._monidx_elevatorTrim] * 180.0 / math.pi
1439
1440 state.antiIceOn = data[self._monidx_antiIceOn]!=0 or \
1441 data[self._monidx_surfaceHeat]!=0 or \
1442 data[self._monidx_propHeat]!=0
1443
1444 return state
1445
1446#------------------------------------------------------------------------------
1447
1448class GenericAircraftModel(AircraftModel):
1449 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1450 values and some other common parameters in a generic way."""
1451
1452 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1453 """Construct the generic aircraft model with the given data.
1454
1455 flapsNotches is an array of how much degrees the individual flaps
1456 notches mean.
1457
1458 fuelTanks is an array of const.FUELTANK_XXX constants about the
1459 aircraft's fuel tanks. They will be converted to offsets.
1460
1461 numEngines is the number of engines the aircraft has.
1462
1463 isN1 determines if the engines have an N1 value or an RPM value
1464 (e.g. pistons)."""
1465 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1466
1467 self._fuelTanks = fuelTanks
1468 self._fuelTankCapacities = [1.0] * len(fuelTanks)
1469 self._fuelIndex = None
1470 self._numEngines = numEngines
1471 self._engineStartIndex = None
1472 self._isN1 = isN1
1473
1474 def doesHandle(self, aircraft, aircraftInfo):
1475 """Determine if the model handles the given aircraft name.
1476
1477 This implementation returns True."""
1478 return True
1479
1480 def addMonitoringData(self, data, fsType):
1481 """Add the model-specific monitoring data to the given array."""
1482 super(GenericAircraftModel, self).addMonitoringData(data, fsType)
1483
1484 self._fuelIndex = self._addFuelData(data)
1485
1486 self._engineStartIndex = len(data)
1487 if self._isN1:
1488 self._addDatarefWithIndexMember(data,
1489 "sim/flightmodel/engine/ENGN_N1_",
1490 (TYPE_FLOAT_ARRAY,
1491 self._numEngines))
1492 else:
1493 self._addDatarefWithIndexMember(data,
1494 "sim/flightmodel/engine/POINT_tacrad",
1495 (TYPE_FLOAT_ARRAY,
1496 self._numEngines))
1497
1498 self._addDatarefWithIndexMember(data,
1499 "sim/flightmodel/engine/ENGN_propmode",
1500 (TYPE_INT_ARRAY, self._numEngines))
1501
1502 def getAircraftState(self, aircraft, timestamp, data):
1503 """Get the aircraft state.
1504
1505 Get it from the parent, and then add the data about the fuel levels and
1506 the engine parameters."""
1507 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1508 timestamp,
1509 data)
1510
1511 state.fuel = []
1512 state.totalFuel = 0.0
1513
1514 fuelAmounts = data[self._fuelIndex]
1515 for i in range(0, len(self._fuelTanks)):
1516 amount = fuelAmounts[i]
1517 state.fuel.append((self._fuelTanks[i], amount))
1518 state.totalFuel += amount
1519
1520 power = data[self._engineStartIndex]
1521
1522 state.n1 = power[:] if self._isN1 else None
1523 state.rpm = None if self._isN1 else power[:]
1524
1525 propMode = data[self._engineStartIndex+1]
1526 state.reverser = [mode == 3 for mode in propMode]
1527
1528 return state
1529
1530 def getFuel(self, handler, callback):
1531 """Get the fuel information for this model.
1532
1533 See Simulator.getFuel for more information. This
1534 implementation simply queries the fuel tanks given to the
1535 constructor."""
1536 data = []
1537 self._addFuelData(data)
1538 data.append( ("sim/aircraft/weight/acf_m_fuel_tot", TYPE_FLOAT) )
1539 data.append( ("sim/aircraft/overflow/acf_tank_rat",
1540 (TYPE_FLOAT_ARRAY, len(self._fuelTanks)) ) )
1541
1542 handler.requestRead(data, self._handleFuelRetrieved,
1543 extra = callback)
1544
1545 def setFuelLevel(self, handler, levels):
1546 """Set the fuel level.
1547
1548 See the description of Simulator.setFuelLevel. This
1549 implementation simply sets the fuel tanks as given."""
1550 data = []
1551 for (tank, level) in levels:
1552 try:
1553 index = self._fuelTanks.index(tank)
1554 data.append( ("sim/flightmodel/weight/m_fuel",
1555 (TYPE_FLOAT_ARRAY, 1, index),
1556 [level * self._fuelTankCapacities[index]]) )
1557 except:
1558 print "xplane.Simulator.setFuelLevel: invalid tank constant: %d" % \
1559 (tank,)
1560
1561 handler.requestWrite(data, self._handleFuelWritten)
1562
1563 def _addFuelData(self, data):
1564 """Add the fuel offsets to the given data array.
1565
1566 Returns the index of the first fuel tank's data."""
1567 fuelStartIndex = len(data)
1568 data.append( ("sim/flightmodel/weight/m_fuel",
1569 (TYPE_FLOAT_ARRAY, len(self._fuelTanks) ) ) )
1570
1571 return fuelStartIndex
1572
1573 def _convertFuelData(self, data, index = 0, addCapacities = False):
1574 """Convert the given data into a fuel info list.
1575
1576 The list consists of two or three-tuples of the following
1577 items:
1578 - the fuel tank ID,
1579 - the amount of the fuel in kg,
1580 - if addCapacities is True, the total capacity of the tank."""
1581 fuelWeight = data[index] / 256.0
1582 index += 1
1583
1584 result = []
1585 totalFuel = 0
1586 for fuelTank in self._fuelTanks:
1587 capacity = data[index+1] * fuelWeight * const.LBSTOKG
1588 if capacity>=1.0:
1589 amount = data[index] * capacity / 128.0 / 65536.0
1590
1591 result.append( (fuelTank, amount, capacity) if addCapacities
1592 else (fuelTank, amount))
1593 totalFuel += amount
1594 index += 2
1595
1596 return (result, totalFuel)
1597
1598 def _handleFuelRetrieved(self, data, callback):
1599 """Callback for a fuel retrieval request."""
1600 result = []
1601 totalCapacity = data[1]
1602 for index in range(0, len(self._fuelTanks)):
1603 amount = data[0][index]
1604 capacity = data[2][index] * totalCapacity
1605 self._fuelTankCapacities[index] = capacity
1606 result.append( (self._fuelTanks[index], amount, capacity) )
1607
1608 callback(result)
1609
1610 def _handleFuelWritten(self, success, extra):
1611 """Callback for a fuel setting request."""
1612 pass
1613
1614#------------------------------------------------------------------------------
1615
1616class GenericModel(GenericAircraftModel):
1617 """Generic aircraft model for an unknown type."""
1618 def __init__(self):
1619 """Construct the model."""
1620 super(GenericModel, self). \
1621 __init__(flapsNotches = [0, 10, 20, 30],
1622 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1623 numEngines = 2)
1624
1625 @property
1626 def name(self):
1627 """Get the name for this aircraft model."""
1628 return "X-Plane/Generic"
1629
1630#------------------------------------------------------------------------------
1631
1632class B737Model(GenericAircraftModel):
1633 """Generic model for the Boeing 737 Classing and NG aircraft."""
1634 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1635
1636 def __init__(self):
1637 """Construct the model."""
1638 super(B737Model, self). \
1639 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1640 fuelTanks = B737Model.fuelTanks,
1641 numEngines = 2)
1642
1643 @property
1644 def name(self):
1645 """Get the name for this aircraft model."""
1646 return "X-Plane/Generic Boeing 737"
1647
1648#------------------------------------------------------------------------------
1649
1650class B767Model(GenericAircraftModel):
1651 """Generic model for the Boeing 767 aircraft."""
1652 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1653
1654 def __init__(self):
1655 """Construct the model."""
1656 super(B767Model, self). \
1657 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1658 fuelTanks = B767Model.fuelTanks,
1659 numEngines = 2)
1660
1661 @property
1662 def name(self):
1663 """Get the name for this aircraft model."""
1664 return "X-Plane/Generic Boeing 767"
1665
1666#------------------------------------------------------------------------------
1667
1668class DH8DModel(GenericAircraftModel):
1669 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1670 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1671
1672 def __init__(self):
1673 """Construct the model."""
1674 super(DH8DModel, self). \
1675 __init__(flapsNotches = [0, 5, 10, 15, 35],
1676 fuelTanks = DH8DModel.fuelTanks,
1677 numEngines = 2)
1678
1679 @property
1680 def name(self):
1681 """Get the name for this aircraft model."""
1682 return "X-Plane/Generic Bombardier Dash 8-Q400"
1683
1684#------------------------------------------------------------------------------
1685
1686class FJSDH8DModel(DH8DModel):
1687 """Model handler for the FlyJSim Dash 8-Q400."""
1688 @staticmethod
1689 def doesHandle(aircraft, (tailnum, author, description, notes,
1690 icao, liveryPath)):
1691 """Determine if this model handler handles the aircraft with the given
1692 name."""
1693 return aircraft.type==const.AIRCRAFT_DH8D and \
1694 description.find("Dash 8 Q400")!=-1 and \
1695 liveryPath.startswith("Aircraft/Heavy Metal/Dash 8 Q400")
1696
1697 @property
1698 def name(self):
1699 """Get the name for this aircraft model."""
1700 return "X-Plane/FlyJSim Bombardier Dash 8-Q400"
1701
1702 def getAircraftState(self, aircraft, timestamp, data):
1703 """Get the aircraft state.
1704
1705 Get it from the parent, and then invert the pitot heat state."""
1706 state = super(FJSDH8DModel, self).getAircraftState(aircraft,
1707 timestamp,
1708 data)
1709 state.antiCollisionLightsOn = \
1710 state.antiCollisionLightsOn or state.strobeLightsOn
1711
1712 return state
1713
1714#------------------------------------------------------------------------------
1715
1716class CRJ2Model(GenericAircraftModel):
1717 """Generic model for the Bombardier CRJ-200 aircraft."""
1718 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1719
1720 def __init__(self):
1721 """Construct the model."""
1722 super(CRJ2Model, self). \
1723 __init__(flapsNotches = [0, 8, 20, 30, 45],
1724 fuelTanks = CRJ2Model.fuelTanks,
1725 numEngines = 2)
1726
1727 @property
1728 def name(self):
1729 """Get the name for this aircraft model."""
1730 return "X-Plane/Generic Bombardier CRJ-200"
1731
1732#------------------------------------------------------------------------------
1733
1734class F70Model(GenericAircraftModel):
1735 """Generic model for the Fokker F70 aircraft."""
1736 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1737
1738 def __init__(self):
1739 """Construct the model."""
1740 super(F70Model, self). \
1741 __init__(flapsNotches = [0, 8, 15, 25, 42],
1742 fuelTanks = F70Model.fuelTanks,
1743 numEngines = 2)
1744
1745 @property
1746 def name(self):
1747 """Get the name for this aircraft model."""
1748 return "X-Plane/Generic Fokker 70"
1749
1750#------------------------------------------------------------------------------
1751
1752class DC3Model(GenericAircraftModel):
1753 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1754 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
1755 const.FUELTANK_RIGHT]
1756 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1757 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1758
1759 def __init__(self):
1760 """Construct the model."""
1761 super(DC3Model, self). \
1762 __init__(flapsNotches = [0, 15, 30, 45],
1763 fuelTanks = DC3Model.fuelTanks,
1764 numEngines = 2, isN1 = False)
1765 self._leftLevel = 0.0
1766 self._rightLevel = 0.0
1767
1768 @property
1769 def name(self):
1770 """Get the name for this aircraft model."""
1771 return "X-Plane/Generic Lisunov Li-2 (DC-3)"
1772
1773#------------------------------------------------------------------------------
1774
1775class T134Model(GenericAircraftModel):
1776 """Generic model for the Tupolev Tu-134 aircraft."""
1777 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
1778 const.FUELTANK_LEFT_AUX,
1779 const.FUELTANK_CENTRE,
1780 const.FUELTANK_RIGHT_AUX,
1781 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
1782
1783 def __init__(self):
1784 """Construct the model."""
1785 super(T134Model, self). \
1786 __init__(flapsNotches = [0, 10, 20, 30],
1787 fuelTanks = T134Model.fuelTanks,
1788 numEngines = 2)
1789
1790 @property
1791 def name(self):
1792 """Get the name for this aircraft model."""
1793 return "X-Plane/Generic Tupolev Tu-134"
1794
1795#------------------------------------------------------------------------------
1796
1797class T154Model(GenericAircraftModel):
1798 """Generic model for the Tupolev Tu-134 aircraft."""
1799 fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1800 const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
1801 const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1802
1803 def __init__(self):
1804 """Construct the model."""
1805 super(T154Model, self). \
1806 __init__(flapsNotches = [0, 15, 28, 45],
1807 fuelTanks = T154Model.fuelTanks,
1808 numEngines = 3)
1809
1810 @property
1811 def name(self):
1812 """Get the name for this aircraft model."""
1813 return "X-Plane/Generic Tupolev Tu-154"
1814
1815 def getAircraftState(self, aircraft, timestamp, data):
1816 """Get an aircraft state object for the given monitoring data.
1817
1818 This removes the reverser value for the middle engine."""
1819 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1820 del state.reverser[1]
1821 return state
1822
1823#------------------------------------------------------------------------------
1824
1825class YK40Model(GenericAircraftModel):
1826 """Generic model for the Yakovlev Yak-40 aircraft."""
1827 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1828
1829 def __init__(self):
1830 """Construct the model."""
1831 super(YK40Model, self). \
1832 __init__(flapsNotches = [0, 20, 35],
1833 fuelTanks = YK40Model.fuelTanks,
1834 numEngines = 2)
1835
1836 @property
1837 def name(self):
1838 """Get the name for this aircraft model."""
1839 return "X-Plane/Generic Yakovlev Yak-40"
1840
1841#------------------------------------------------------------------------------
1842
1843_genericModels = { const.AIRCRAFT_B736 : B737Model,
1844 const.AIRCRAFT_B737 : B737Model,
1845 const.AIRCRAFT_B738 : B737Model,
1846 const.AIRCRAFT_B738C : B737Model,
1847 const.AIRCRAFT_B733 : B737Model,
1848 const.AIRCRAFT_B734 : B737Model,
1849 const.AIRCRAFT_B735 : B737Model,
1850 const.AIRCRAFT_DH8D : DH8DModel,
1851 const.AIRCRAFT_B762 : B767Model,
1852 const.AIRCRAFT_B763 : B767Model,
1853 const.AIRCRAFT_CRJ2 : CRJ2Model,
1854 const.AIRCRAFT_F70 : F70Model,
1855 const.AIRCRAFT_DC3 : DC3Model,
1856 const.AIRCRAFT_T134 : T134Model,
1857 const.AIRCRAFT_T154 : T154Model,
1858 const.AIRCRAFT_YK40 : YK40Model }
1859
1860#------------------------------------------------------------------------------
1861
1862AircraftModel.registerSpecial(FJSDH8DModel)
Note: See TracBrowser for help on using the repository browser.