source: src/mlx/fsuipc.py@ 394:d85637f7b989

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

Added support for unreliable altimeter readings (re #165)

File size: 78.5 KB
Line 
1
2import fs
3import const
4import util
5import acft
6
7import threading
8import os
9import time
10import calendar
11import sys
12import codecs
13import math
14
15if os.name == "nt" and "FORCE_PYUIPC_SIM" not in os.environ:
16 import pyuipc
17else:
18 import pyuipc_sim as pyuipc
19
20#------------------------------------------------------------------------------
21
22## @package mlx.fsuipc
23#
24# The module towards FSUIPC.
25#
26# This module implements the simulator interface to FSUIPC.
27#
28# The \ref Handler class is thread handling the FSUIPC requests. It can be
29# given read, periodic read and write, requests, that are executed
30# asynchronously, and a callback function is called with the result. This class
31# is used internally within the module.
32#
33# The \ref Simulator class is the actual interface to the flight simulator, and
34# an instance of it is returned by \ref mlx.fs.createSimulator. This object can
35# be used to connect to the simulator and disconnect from it, to query various
36# data and to start and stop the monitoring of the data.
37#
38# \ref AircraftModel is the base class of the aircraft models. A "model" is a
39# particular implementation of an aircraft, such as the PMDG Boeing 737NG in
40# Flight Simulator 2004. Since each type and each model has its peculiarities
41# (e.g. the different engine and fuel tank configurations), each aircraft type
42# has a generic model, that should work for most of the time. However, certain
43# models may implement various controls, gauges, etc. differently, and such
44# peculiarites can be handled in a specific subclass of \ref
45# AircraftModel. These subclasses should be registered as special ones, and if
46# the simulator detects that the aircraft's model became known or has changed,
47# it will check these special models before resorting to the generic ones.
48#
49# The models are responsible also for querying certain parameters, such as the
50# fuel tank configuration. While ideally it should be specific to a type only,
51# it is possible that the model contains different tanks, in which case some
52# tricks may be needed. See the \ref DC3Model "DC-3 (Li-2)" aircraft as an
53# example.
54
55#------------------------------------------------------------------------------
56
57## The mapping of tank types to FSUIPC offsets
58_tank2offset = { const.FUELTANK_CENTRE : 0x0b74,
59 const.FUELTANK_LEFT : 0x0b7c,
60 const.FUELTANK_RIGHT : 0x0b94,
61 const.FUELTANK_LEFT_AUX : 0x0b84,
62 const.FUELTANK_RIGHT_AUX : 0x0b9c,
63 const.FUELTANK_LEFT_TIP : 0x0b8c,
64 const.FUELTANK_RIGHT_TIP : 0x0ba4,
65 const.FUELTANK_EXTERNAL1 : 0x1254,
66 const.FUELTANK_EXTERNAL2 : 0x125c,
67 const.FUELTANK_CENTRE2 : 0x1244 }
68
69#------------------------------------------------------------------------------
70
71class Handler(threading.Thread):
72 """The thread to handle the FSUIPC requests."""
73 @staticmethod
74 def fsuipc2VS(data):
75 """Convert the given vertical speed data read from FSUIPC into feet/min."""
76 return data*60.0/const.FEETTOMETRES/256.0
77
78 @staticmethod
79 def fsuipc2radioAltitude(data):
80 """Convert the given radio altitude data read from FSUIPC into feet."""
81 return data/const.FEETTOMETRES/65536.0
82
83 @staticmethod
84 def fsuipc2Degrees(data):
85 """Convert the given data into degrees."""
86 return data * 360.0 / 65536.0 / 65536.0
87
88 @staticmethod
89 def fsuipc2PositiveDegrees(data):
90 """Convert the given data into positive degrees."""
91 degrees = Handler.fsuipc2Degrees(data)
92 if degrees<0.0: degrees += 360.0
93 return degrees
94
95 @staticmethod
96 def fsuipc2IAS(data):
97 """Convert the given data into indicated airspeed."""
98 return data / 128.0
99
100 @staticmethod
101 def _callSafe(fun):
102 """Call the given function and swallow any exceptions."""
103 try:
104 return fun()
105 except Exception, e:
106 print >> sys.stderr, str(e)
107 return None
108
109 # The number of times a read is attempted
110 NUM_READATTEMPTS = 3
111
112 # The number of connection attempts
113 NUM_CONNECTATTEMPTS = 3
114
115 # The interval between successive connect attempts
116 CONNECT_INTERVAL = 0.25
117
118 @staticmethod
119 def _performRead(data, callback, extra, validator):
120 """Perform a read request.
121
122 If there is a validator, that will be called with the return values,
123 and if the values are wrong, the request is retried at most a certain
124 number of times.
125
126 Return True if the request has succeeded, False if validation has
127 failed during all attempts. An exception may also be thrown if there is
128 some lower-level communication problem."""
129 attemptsLeft = Handler.NUM_READATTEMPTS
130 while attemptsLeft>0:
131 values = pyuipc.read(data)
132 if validator is None or \
133 Handler._callSafe(lambda: validator(values, extra)):
134 Handler._callSafe(lambda: callback(values, extra))
135 return True
136 else:
137 attemptsLeft -= 1
138 return False
139
140 class Request(object):
141 """A simple, one-shot request."""
142 def __init__(self, forWrite, data, callback, extra, validator = None):
143 """Construct the request."""
144 self._forWrite = forWrite
145 self._data = data
146 self._callback = callback
147 self._extra = extra
148 self._validator = validator
149
150 def process(self, time):
151 """Process the request.
152
153 Return True if the request has succeeded, False if data validation
154 has failed for a reading request. An exception may also be thrown
155 if there is some lower-level communication problem."""
156 if self._forWrite:
157 pyuipc.write(self._data)
158 Handler._callSafe(lambda: self._callback(True, self._extra))
159 return True
160 else:
161 return Handler._performRead(self._data, self._callback,
162 self._extra, self._validator)
163
164 def fail(self):
165 """Handle the failure of this request."""
166 if self._forWrite:
167 Handler._callSafe(lambda: self._callback(False, self._extra))
168 else:
169 Handler._callSafe(lambda: self._callback(None, self._extra))
170
171 class PeriodicRequest(object):
172 """A periodic request."""
173 def __init__(self, id, period, data, callback, extra, validator):
174 """Construct the periodic request."""
175 self._id = id
176 self._period = period
177 self._nextFire = time.time()
178 self._data = data
179 self._preparedData = None
180 self._callback = callback
181 self._extra = extra
182 self._validator = validator
183
184 @property
185 def id(self):
186 """Get the ID of this periodic request."""
187 return self._id
188
189 @property
190 def nextFire(self):
191 """Get the next firing time."""
192 return self._nextFire
193
194 def process(self, time):
195 """Check if this request should be executed, and if so, do so.
196
197 time is the time at which the request is being executed. If this
198 function is called too early, nothing is done, and True is
199 returned.
200
201 Return True if the request has succeeded, False if data validation
202 has failed. An exception may also be thrown if there is some
203 lower-level communication problem."""
204 if time<self._nextFire:
205 return True
206
207 if self._preparedData is None:
208 self._preparedData = pyuipc.prepare_data(self._data)
209 self._data = None
210
211 isOK = Handler._performRead(self._preparedData, self._callback,
212 self._extra, self._validator)
213
214 if isOK:
215 while self._nextFire <= time:
216 self._nextFire += self._period
217
218 return isOK
219
220 def fail(self):
221 """Handle the failure of this request."""
222 pass
223
224 def __cmp__(self, other):
225 """Compare two periodic requests. They are ordered by their next
226 firing times."""
227 return cmp(self._nextFire, other._nextFire)
228
229 def __init__(self, connectionListener,
230 connectAttempts = -1, connectInterval = 0.2):
231 """Construct the handler with the given connection listener."""
232 threading.Thread.__init__(self)
233
234 self._connectionListener = connectionListener
235 self._connectAttempts = connectAttempts
236 self._connectInterval = connectInterval
237
238 self._requestCondition = threading.Condition()
239 self._connectionRequested = False
240 self._connected = False
241
242 self._requests = []
243 self._nextPeriodicID = 1
244 self._periodicRequests = []
245
246 self.daemon = True
247
248 def requestRead(self, data, callback, extra = None, validator = None):
249 """Request the reading of some data.
250
251 data is a list of tuples of the following items:
252 - the offset of the data as an integer
253 - the type letter of the data as a string
254
255 callback is a function that receives two pieces of data:
256 - the values retrieved or None on error
257 - the extra parameter
258
259 It will be called in the handler's thread!
260 """
261 with self._requestCondition:
262 self._requests.append(Handler.Request(False, data, callback, extra,
263 validator))
264 self._requestCondition.notify()
265
266 def requestWrite(self, data, callback, extra = None):
267 """Request the writing of some data.
268
269 data is a list of tuples of the following items:
270 - the offset of the data as an integer
271 - the type letter of the data as a string
272 - the data to write
273
274 callback is a function that receives two pieces of data:
275 - a boolean indicating if writing was successful
276 - the extra data
277 It will be called in the handler's thread!
278 """
279 with self._requestCondition:
280 request = Handler.Request(True, data, callback, extra)
281 #print "fsuipc.Handler.requestWrite", request
282 self._requests.append(request)
283 self._requestCondition.notify()
284
285 @staticmethod
286 def _readWriteCallback(data, extra):
287 """Callback for the read() and write() calls below."""
288 extra.append(data)
289 with extra[0] as condition:
290 condition.notify()
291
292 def requestPeriodicRead(self, period, data, callback, extra = None,
293 validator = None):
294 """Request a periodic read of data.
295
296 period is a floating point number with the period in seconds.
297
298 This function returns an identifier which can be used to cancel the
299 request."""
300 with self._requestCondition:
301 id = self._nextPeriodicID
302 self._nextPeriodicID += 1
303 request = Handler.PeriodicRequest(id, period, data, callback,
304 extra, validator)
305 self._periodicRequests.append(request)
306 self._requestCondition.notify()
307 return id
308
309 def clearPeriodic(self, id):
310 """Clear the periodic request with the given ID."""
311 with self._requestCondition:
312 for i in range(0, len(self._periodicRequests)):
313 if self._periodicRequests[i].id==id:
314 del self._periodicRequests[i]
315 return True
316 return False
317
318 def connect(self):
319 """Initiate the connection to the flight simulator."""
320 with self._requestCondition:
321 if not self._connectionRequested:
322 self._connectionRequested = True
323 self._requestCondition.notify()
324
325 def disconnect(self):
326 """Disconnect from the flight simulator."""
327 with self._requestCondition:
328 self._requests = []
329 if self._connectionRequested:
330 self._connectionRequested = False
331 self._requestCondition.notify()
332
333 def clearRequests(self):
334 """Clear the outstanding one-shot requests."""
335 with self._requestCondition:
336 self._requests = []
337
338 def run(self):
339 """Perform the operation of the thread."""
340 while True:
341 self._waitConnectionRequest()
342
343 if self._connect()>0:
344 self._handleConnection()
345
346 self._disconnect()
347
348 def _waitConnectionRequest(self):
349 """Wait for a connection request to arrive."""
350 with self._requestCondition:
351 while not self._connectionRequested:
352 self._requestCondition.wait()
353
354 def _connect(self, autoReconnection = False, attempts = 0):
355 """Try to connect to the flight simulator via FSUIPC
356
357 Returns True if the connection has been established, False if it was
358 not due to no longer requested.
359 """
360 while self._connectionRequested:
361 if attempts>=self.NUM_CONNECTATTEMPTS:
362 self._connectionRequested = False
363 if autoReconnection:
364 Handler._callSafe(lambda:
365 self._connectionListener.disconnected())
366 else:
367 Handler._callSafe(lambda:
368 self._connectionListener.connectionFailed())
369 return 0
370
371 try:
372 attempts += 1
373 pyuipc.open(pyuipc.SIM_ANY)
374 description = "(FSUIPC version: 0x%04x, library version: 0x%04x, FS version: %d)" % \
375 (pyuipc.fsuipc_version, pyuipc.lib_version,
376 pyuipc.fs_version)
377 if not autoReconnection:
378 fsType = const.SIM_MSFSX \
379 if pyuipc.fs_version == pyuipc.SIM_FSX \
380 else const.SIM_MSFS9
381
382 Handler._callSafe(lambda:
383 self._connectionListener.connected(fsType,
384 description))
385 self._connected = True
386 return attempts
387 except Exception, e:
388 print "fsuipc.Handler._connect: connection failed: " + str(e) + \
389 " (attempts: %d)" % (attempts,)
390 if attempts<self.NUM_CONNECTATTEMPTS:
391 time.sleep(self.CONNECT_INTERVAL)
392
393 def _handleConnection(self):
394 """Handle a living connection."""
395 with self._requestCondition:
396 while self._connectionRequested:
397 self._processRequests()
398 self._waitRequest()
399
400 def _waitRequest(self):
401 """Wait for the time of the next request.
402
403 Returns also, if the connection is no longer requested.
404
405 Should be called with the request condition lock held."""
406 while self._connectionRequested:
407 timeout = None
408 if self._periodicRequests:
409 self._periodicRequests.sort()
410 timeout = self._periodicRequests[0].nextFire - time.time()
411
412 if self._requests or \
413 (timeout is not None and timeout <= 0.0):
414 return
415
416 self._requestCondition.wait(timeout)
417
418 def _disconnect(self):
419 """Disconnect from the flight simulator."""
420 print "fsuipc.Handler._disconnect"
421 if self._connected:
422 pyuipc.close()
423 self._connected = False
424
425 def _processRequest(self, request, time, attempts):
426 """Process the given request.
427
428 If an exception occurs or invalid data is read too many times, we try
429 to reconnect.
430
431 This function returns only if the request has succeeded, or if a
432 connection is no longer requested.
433
434 This function is called with the request lock held, but is relased
435 whole processing the request and reconnecting."""
436 self._requestCondition.release()
437
438 #print "fsuipc.Handler._processRequest", request
439
440 needReconnect = False
441 try:
442 try:
443 if not request.process(time):
444 print "fsuipc.Handler._processRequest: FSUIPC returned invalid data too many times, reconnecting"
445 needReconnect = True
446 except Exception as e:
447 print "fsuipc.Handler._processRequest: FSUIPC connection failed (" + \
448 str(e) + "), reconnecting (attempts=%d)." % (attempts,)
449 needReconnect = True
450
451 if needReconnect:
452 with self._requestCondition:
453 self._requests.insert(0, request)
454 self._disconnect()
455 return self._connect(autoReconnection = True, attempts = attempts)
456 else:
457 return 0
458 finally:
459 self._requestCondition.acquire()
460
461 def _processRequests(self):
462 """Process any pending requests.
463
464 Will be called with the request lock held."""
465 attempts = 0
466 while self._connectionRequested and self._periodicRequests:
467 self._periodicRequests.sort()
468 request = self._periodicRequests[0]
469
470 t = time.time()
471
472 if request.nextFire>t:
473 break
474
475 attempts = self._processRequest(request, t, attempts)
476
477 while self._connectionRequested and self._requests:
478 request = self._requests[0]
479 del self._requests[0]
480
481 attempts = self._processRequest(request, None, attempts)
482
483 return self._connectionRequested
484
485#------------------------------------------------------------------------------
486
487class Simulator(object):
488 """The simulator class representing the interface to the flight simulator
489 via FSUIPC."""
490 # The basic data that should be queried all the time once we are connected
491 timeData = [ (0x0240, "H"), # Year
492 (0x023e, "H"), # Number of day in year
493 (0x023b, "b"), # UTC hour
494 (0x023c, "b"), # UTC minute
495 (0x023a, "b") ] # seconds
496
497 normalData = timeData + \
498 [ (0x3d00, -256), # The name of the current aircraft
499 (0x3c00, -256), # The path of the current AIR file
500 (0x1274, "h") ] # Text display mode
501
502 flareData1 = [ (0x023a, "b"), # Seconds of time
503 (0x31e4, "d"), # Radio altitude
504 (0x02c8, "d") ] # Vertical speed
505
506 flareStartData = [ (0x0e90, "H"), # Ambient wind speed
507 (0x0e92, "H"), # Ambient wind direction
508 (0x0e8a, "H") ] # Visibility
509
510 flareData2 = [ (0x023a, "b"), # Seconds of time
511 (0x0366, "H"), # On the ground
512 (0x02c8, "d"), # Vertical speed
513 (0x030c, "d"), # Touch-down rate
514 (0x02bc, "d"), # IAS
515 (0x0578, "d"), # Pitch
516 (0x057c, "d"), # Bank
517 (0x0580, "d") ] # Heading
518
519 TIME_SYNC_INTERVAL = 3.0
520
521 @staticmethod
522 def _getTimestamp(data):
523 """Convert the given data into a timestamp."""
524 timestamp = calendar.timegm(time.struct_time([data[0],
525 1, 1, 0, 0, 0, -1, 1, 0]))
526 timestamp += data[1] * 24 * 3600
527 timestamp += data[2] * 3600
528 timestamp += data[3] * 60
529 timestamp += data[4]
530
531 return timestamp
532
533 @staticmethod
534 def _appendHotkeyData(data, offset, hotkey):
535 """Append the data for the given hotkey to the given array, that is
536 intended to be passed to requestWrite call on the handler."""
537 data.append((offset + 0, "b", ord(hotkey.key)))
538
539 modifiers = 0
540 if hotkey.ctrl: modifiers |= 0x02
541 if hotkey.shift: modifiers |= 0x01
542 data.append((offset + 1, "b", modifiers))
543
544 data.append((offset + 2, "b", 0))
545
546 data.append((offset + 3, "b", 0))
547
548 def __init__(self, connectionListener, connectAttempts = -1,
549 connectInterval = 0.2):
550 """Construct the simulator.
551
552 The aircraft object passed must provide the following members:
553 - type: one of the AIRCRAFT_XXX constants from const.py
554 - modelChanged(aircraftName, modelName): called when the model handling
555 the aircraft has changed.
556 - handleState(aircraftState): handle the given state.
557 - flareStarted(windSpeed, windDirection, visibility, flareStart,
558 flareStartFS): called when the flare has
559 started. windSpeed is in knots, windDirection is in degrees and
560 visibility is in metres. flareStart and flareStartFS are two time
561 values expressed in seconds that can be used to calculate the flare
562 time.
563 - flareFinished(flareEnd, flareEndFS, tdRate, tdRateCalculatedByFS,
564 ias, pitch, bank, heading): called when the flare has
565 finished, i.e. the aircraft is on the ground. flareEnd and flareEndFS
566 are the two time values corresponding to the touchdown time. tdRate is
567 the touch-down rate, tdRateCalculatedBySim indicates if the data comes
568 from the simulator or was calculated by the adapter. The other data
569 are self-explanatory and expressed in their 'natural' units."""
570 self._fsType = None
571 self._aircraft = None
572
573 self._handler = Handler(self,
574 connectAttempts = connectAttempts,
575 connectInterval = connectInterval)
576 self._connectionListener = connectionListener
577 self._handler.start()
578
579 self._scroll = False
580
581 self._syncTime = False
582 self._nextSyncTime = -1
583
584 self._normalRequestID = None
585
586 self._monitoringRequested = False
587 self._monitoring = False
588
589 self._aircraftName = None
590 self._aircraftModel = None
591
592 self._flareRequestID = None
593 self._flareRates = []
594 self._flareStart = None
595 self._flareStartFS = None
596
597 self._hotkeyLock = threading.Lock()
598 self._hotkeys = None
599 self._hotkeySetID = 0
600 self._hotkeySetGeneration = 0
601 self._hotkeyOffets = None
602 self._hotkeyRequestID = None
603 self._hotkeyCallback = None
604
605 self._latin1decoder = codecs.getdecoder("iso-8859-1")
606 self._fuelCallback = None
607
608 def connect(self, aircraft):
609 """Initiate a connection to the simulator."""
610 self._aircraft = aircraft
611 self._aircraftName = None
612 self._aircraftModel = None
613 self._handler.connect()
614 if self._normalRequestID is None:
615 self._nextSyncTime = -1
616 self._startDefaultNormal()
617
618 def reconnect(self):
619 """Initiate a reconnection to the simulator.
620
621 It does not reset already set up data, just calls connect() on the
622 handler."""
623 self._handler.connect()
624
625 def requestZFW(self, callback):
626 """Send a request for the ZFW."""
627 self._handler.requestRead([(0x3bfc, "d")], self._handleZFW, extra = callback)
628
629 def requestWeights(self, callback):
630 """Request the following weights: DOW, ZFW, payload.
631
632 These values will be passed to the callback function in this order, as
633 separate arguments."""
634 self._handler.requestRead([(0x13fc, "d")], self._handlePayloadCount,
635 extra = callback)
636
637 def requestTime(self, callback):
638 """Request the time from the simulator."""
639 self._handler.requestRead(Simulator.timeData, self._handleTime,
640 extra = callback)
641
642 def startMonitoring(self):
643 """Start the periodic monitoring of the aircraft and pass the resulting
644 state to the aircraft object periodically."""
645 assert not self._monitoringRequested
646 self._monitoringRequested = True
647
648 def stopMonitoring(self):
649 """Stop the periodic monitoring of the aircraft."""
650 assert self._monitoringRequested
651 self._monitoringRequested = False
652
653 def startFlare(self):
654 """Start monitoring the flare time.
655
656 At present it is assumed to be called from the FSUIPC thread, hence no
657 protection."""
658 #self._aircraft.logger.debug("startFlare")
659 if self._flareRequestID is None:
660 self._flareRates = []
661 self._flareRequestID = self._handler.requestPeriodicRead(0.1,
662 Simulator.flareData1,
663 self._handleFlare1)
664
665 def cancelFlare(self):
666 """Cancel monitoring the flare time.
667
668 At present it is assumed to be called from the FSUIPC thread, hence no
669 protection."""
670 if self._flareRequestID is not None:
671 self._handler.clearPeriodic(self._flareRequestID)
672 self._flareRequestID = None
673
674 def sendMessage(self, message, duration = 3,
675 _disconnect = False):
676 """Send a message to the pilot via the simulator.
677
678 duration is the number of seconds to keep the message displayed."""
679
680 print "fsuipc.Simulator.sendMessage:", message
681
682 if self._scroll:
683 if duration==0: duration = -1
684 elif duration == 1: duration = -2
685 else: duration = -duration
686
687 data = [(0x3380, -1 - len(message), message),
688 (0x32fa, 'h', duration)]
689
690 #if _disconnect:
691 # print "fsuipc.Simulator.sendMessage(disconnect)", message
692
693 self._handler.requestWrite(data, self._handleMessageSent,
694 extra = _disconnect)
695
696 def getFuel(self, callback):
697 """Get the fuel information for the current model.
698
699 The callback will be called with a list of triplets with the following
700 items:
701 - the fuel tank identifier
702 - the current weight of the fuel in the tank (in kgs)
703 - the current total capacity of the tank (in kgs)."""
704 if self._aircraftModel is None:
705 self._fuelCallback = callback
706 else:
707 self._aircraftModel.getFuel(self._handler, callback)
708
709 def setFuelLevel(self, levels):
710 """Set the fuel level to the given ones.
711
712 levels is an array of two-tuples, where each tuple consists of the
713 following:
714 - the const.FUELTANK_XXX constant denoting the tank that must be set,
715 - the requested level of the fuel as a floating-point value between 0.0
716 and 1.0."""
717 if self._aircraftModel is not None:
718 self._aircraftModel.setFuelLevel(self._handler, levels)
719
720 def enableTimeSync(self):
721 """Enable the time synchronization."""
722 self._nextSyncTime = -1
723 self._syncTime = True
724
725 def disableTimeSync(self):
726 """Enable the time synchronization."""
727 self._syncTime = False
728 self._nextSyncTime = -1
729
730 def listenHotkeys(self, hotkeys, callback):
731 """Start listening to the given hotkeys.
732
733 callback is function expecting two arguments:
734 - the ID of the hotkey set as returned by this function,
735 - the list of the indexes of the hotkeys that were pressed."""
736 with self._hotkeyLock:
737 assert self._hotkeys is None
738
739 self._hotkeys = hotkeys
740 self._hotkeySetID += 1
741 self._hotkeySetGeneration = 0
742 self._hotkeyCallback = callback
743
744 self._handler.requestRead([(0x320c, "u")],
745 self._handleNumHotkeys,
746 (self._hotkeySetID,
747 self._hotkeySetGeneration))
748
749 return self._hotkeySetID
750
751 def clearHotkeys(self):
752 """Clear the current hotkey set.
753
754 Note that it is possible, that the callback function set either
755 previously or after calling this function by listenHotkeys() will be
756 called with data from the previous hotkey set.
757
758 Therefore it is recommended to store the hotkey set ID somewhere and
759 check that in the callback function. Right before calling
760 clearHotkeys(), this stored ID should be cleared so that the check
761 fails for sure."""
762 with self._hotkeyLock:
763 if self._hotkeys is not None:
764 self._hotkeys = None
765 self._hotkeySetID += 1
766 self._hotkeyCallback = None
767 self._clearHotkeyRequest()
768
769 def disconnect(self, closingMessage = None, duration = 3):
770 """Disconnect from the simulator."""
771 assert not self._monitoringRequested
772
773 print "fsuipc.Simulator.disconnect", closingMessage, duration
774
775 self._stopNormal()
776 self.clearHotkeys()
777 if closingMessage is None:
778 self._handler.disconnect()
779 else:
780 self.sendMessage(closingMessage, duration = duration,
781 _disconnect = True)
782
783 def connected(self, fsType, descriptor):
784 """Called when a connection has been established to the flight
785 simulator of the given type."""
786 self._fsType = fsType
787 with self._hotkeyLock:
788 if self._hotkeys is not None:
789 self._hotkeySetGeneration += 1
790
791 self._handler.requestRead([(0x320c, "u")],
792 self._handleNumHotkeys,
793 (self._hotkeySetID,
794 self._hotkeySetGeneration))
795 self._connectionListener.connected(fsType, descriptor)
796
797 def connectionFailed(self):
798 """Called when the connection could not be established."""
799 with self._hotkeyLock:
800 self._clearHotkeyRequest()
801 self._connectionListener.connectionFailed()
802
803 def disconnected(self):
804 """Called when a connection to the flight simulator has been broken."""
805 with self._hotkeyLock:
806 self._clearHotkeyRequest()
807 self._connectionListener.disconnected()
808
809 def _startDefaultNormal(self):
810 """Start the default normal periodic request."""
811 assert self._normalRequestID is None
812 self._normalRequestID = \
813 self._handler.requestPeriodicRead(1.0,
814 Simulator.normalData,
815 self._handleNormal,
816 validator = self._validateNormal)
817
818 def _stopNormal(self):
819 """Stop the normal period request."""
820 assert self._normalRequestID is not None
821 self._handler.clearPeriodic(self._normalRequestID)
822 self._normalRequestID = None
823 self._monitoring = False
824
825 def _validateNormal(self, data, extra):
826 """Validate the normal data."""
827 return data[0]!=0 and data[1]!=0 and len(data[5])>0 and len(data[6])>0
828
829 def _handleNormal(self, data, extra):
830 """Handle the reply to the normal request.
831
832 At the beginning the result consists the data for normalData. When
833 monitoring is started, it contains the result also for the
834 aircraft-specific values.
835 """
836 timestamp = Simulator._getTimestamp(data)
837
838 createdNewModel = self._setAircraftName(timestamp, data[5], data[6])
839 if self._fuelCallback is not None:
840 self._aircraftModel.getFuel(self._handler, self._fuelCallback)
841 self._fuelCallback = None
842
843 self._scroll = data[7]!=0
844
845 if self._monitoringRequested and not self._monitoring:
846 self._stopNormal()
847 self._startMonitoring()
848 elif self._monitoring and not self._monitoringRequested:
849 self._stopNormal()
850 self._startDefaultNormal()
851 elif self._monitoring and self._aircraftModel is not None and \
852 not createdNewModel:
853 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
854 timestamp, data)
855
856 self._checkTimeSync(aircraftState)
857
858 self._aircraft.handleState(aircraftState)
859
860 def _checkTimeSync(self, aircraftState):
861 """Check if we need to synchronize the FS time."""
862 if not self._syncTime or aircraftState.paused or \
863 self._flareRequestID is not None:
864 self._nextSyncTime = -1
865 return
866
867 now = time.time()
868 seconds = time.gmtime(now).tm_sec
869
870 if seconds>30 and seconds<59:
871 if self._nextSyncTime > (now - 0.49):
872 return
873
874 self._handler.requestWrite([(0x023a, "b", int(seconds))],
875 self._handleTimeSynced)
876
877 #print "Set the seconds to ", seconds
878
879 if self._nextSyncTime<0:
880 self._nextSyncTime = now
881
882 self._nextSyncTime += Simulator.TIME_SYNC_INTERVAL
883 else:
884 self._nextSyncTime = -1
885
886 def _handleTimeSynced(self, success, extra):
887 """Callback for the time sync result."""
888 pass
889
890 def _setAircraftName(self, timestamp, name, airPath):
891 """Set the name of the aicraft and if it is different from the
892 previous, create a new model for it.
893
894 If so, also notifty the aircraft about the change.
895
896 Return if a new model was created."""
897 aircraftName = (name, airPath)
898 if aircraftName==self._aircraftName:
899 return False
900
901 print "fsuipc.Simulator: new aircraft name and air file path: %s, %s" % \
902 (name, airPath)
903
904 self._aircraftName = aircraftName
905 needNew = self._aircraftModel is None
906 needNew = needNew or\
907 not self._aircraftModel.doesHandle(self._aircraft, aircraftName)
908 if not needNew:
909 specialModel = AircraftModel.findSpecial(self._aircraft, aircraftName)
910 needNew = specialModel is not None and \
911 specialModel is not self._aircraftModel.__class__
912
913 if needNew:
914 self._setAircraftModel(AircraftModel.create(self._aircraft, aircraftName))
915
916
917 self._aircraft.modelChanged(timestamp, self._latin1decoder(name)[0],
918 self._aircraftModel.name)
919
920 return needNew
921
922 def _setAircraftModel(self, model):
923 """Set a new aircraft model.
924
925 It will be queried for the data to monitor and the monitoring request
926 will be replaced by a new one."""
927 self._aircraftModel = model
928
929 if self._monitoring:
930 self._stopNormal()
931 self._startMonitoring()
932
933 def _startMonitoring(self):
934 """Start monitoring with the current aircraft model."""
935 data = Simulator.normalData[:]
936 self._aircraftModel.addMonitoringData(data, self._fsType)
937
938 self._normalRequestID = \
939 self._handler.requestPeriodicRead(1.0, data,
940 self._handleNormal,
941 validator = self._validateNormal)
942 self._monitoring = True
943
944 def _addFlareRate(self, data):
945 """Append a flare rate to the list of last rates."""
946 if len(self._flareRates)>=3:
947 del self._flareRates[0]
948 self._flareRates.append(Handler.fsuipc2VS(data))
949
950 def _handleFlare1(self, data, normal):
951 """Handle the first stage of flare monitoring."""
952 #self._aircraft.logger.debug("handleFlare1: " + str(data))
953 if Handler.fsuipc2radioAltitude(data[1])<=50.0:
954 self._flareStart = time.time()
955 self._flareStartFS = data[0]
956 self._handler.clearPeriodic(self._flareRequestID)
957 self._flareRequestID = \
958 self._handler.requestPeriodicRead(0.1,
959 Simulator.flareData2,
960 self._handleFlare2)
961 self._handler.requestRead(Simulator.flareStartData,
962 self._handleFlareStart)
963
964 self._addFlareRate(data[2])
965
966 def _handleFlareStart(self, data, extra):
967 """Handle the data need to notify the aircraft about the starting of
968 the flare."""
969 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
970 if data is not None:
971 windDirection = data[1]*360.0/65536.0
972 if windDirection<0.0: windDirection += 360.0
973 self._aircraft.flareStarted(data[0], windDirection,
974 data[2]*1609.344/100.0,
975 self._flareStart, self._flareStartFS)
976
977 def _handleFlare2(self, data, normal):
978 """Handle the first stage of flare monitoring."""
979 #self._aircraft.logger.debug("handleFlare2: " + str(data))
980 if data[1]!=0:
981 flareEnd = time.time()
982 self._handler.clearPeriodic(self._flareRequestID)
983 self._flareRequestID = None
984
985 flareEndFS = data[0]
986 if flareEndFS<self._flareStartFS:
987 flareEndFS += 60
988
989 tdRate = Handler.fsuipc2VS(data[3])
990 tdRateCalculatedByFS = True
991 if tdRate==0 or tdRate>1000.0 or tdRate<-1000.0:
992 tdRate = min(self._flareRates)
993 tdRateCalculatedByFS = False
994
995 self._aircraft.flareFinished(flareEnd, flareEndFS,
996 tdRate, tdRateCalculatedByFS,
997 Handler.fsuipc2IAS(data[4]),
998 Handler.fsuipc2Degrees(data[5]),
999 Handler.fsuipc2Degrees(data[6]),
1000 Handler.fsuipc2PositiveDegrees(data[7]))
1001 else:
1002 self._addFlareRate(data[2])
1003
1004 def _handleZFW(self, data, callback):
1005 """Callback for a ZFW retrieval request."""
1006 zfw = data[0] * const.LBSTOKG / 256.0
1007 callback(zfw)
1008
1009 def _handleTime(self, data, callback):
1010 """Callback for a time retrieval request."""
1011 callback(Simulator._getTimestamp(data))
1012
1013 def _handlePayloadCount(self, data, callback):
1014 """Callback for the payload count retrieval request."""
1015 payloadCount = data[0]
1016 data = [(0x3bfc, "d"), (0x30c0, "f")]
1017 for i in range(0, payloadCount):
1018 data.append((0x1400 + i*48, "f"))
1019
1020 self._handler.requestRead(data, self._handleWeights,
1021 extra = callback)
1022
1023 def _handleWeights(self, data, callback):
1024 """Callback for the weights retrieval request."""
1025 zfw = data[0] * const.LBSTOKG / 256.0
1026 grossWeight = data[1] * const.LBSTOKG
1027 payload = sum(data[2:]) * const.LBSTOKG
1028 dow = zfw - payload
1029 callback(dow, payload, zfw, grossWeight)
1030
1031 def _handleMessageSent(self, success, disconnect):
1032 """Callback for a message sending request."""
1033 #print "fsuipc.Simulator._handleMessageSent", disconnect
1034 if disconnect:
1035 self._handler.disconnect()
1036
1037 def _handleNumHotkeys(self, data, (id, generation)):
1038 """Handle the result of the query of the number of hotkeys"""
1039 with self._hotkeyLock:
1040 if id==self._hotkeySetID and generation==self._hotkeySetGeneration:
1041 numHotkeys = data[0]
1042 print "fsuipc.Simulator._handleNumHotkeys: numHotkeys:", numHotkeys
1043 data = [(0x3210 + i*4, "d") for i in range(0, numHotkeys)]
1044 self._handler.requestRead(data, self._handleHotkeyTable,
1045 (id, generation))
1046
1047 def _setupHotkeys(self, data):
1048 """Setup the hiven hotkeys and return the data to be written.
1049
1050 If there were hotkeys set previously, they are reused as much as
1051 possible. Any of them not reused will be cleared."""
1052 hotkeys = self._hotkeys
1053 numHotkeys = len(hotkeys)
1054
1055 oldHotkeyOffsets = set([] if self._hotkeyOffets is None else
1056 self._hotkeyOffets)
1057
1058 self._hotkeyOffets = []
1059 numOffsets = 0
1060
1061 while oldHotkeyOffsets:
1062 offset = oldHotkeyOffsets.pop()
1063 self._hotkeyOffets.append(offset)
1064 numOffsets += 1
1065
1066 if numOffsets>=numHotkeys:
1067 break
1068
1069 for i in range(0, len(data)):
1070 if numOffsets>=numHotkeys:
1071 break
1072
1073 if data[i]==0:
1074 self._hotkeyOffets.append(0x3210 + i*4)
1075 numOffsets += 1
1076
1077 writeData = []
1078 for i in range(0, numOffsets):
1079 Simulator._appendHotkeyData(writeData,
1080 self._hotkeyOffets[i],
1081 hotkeys[i])
1082
1083 for offset in oldHotkeyOffsets:
1084 writeData.append((offset, "u", long(0)))
1085
1086 return writeData
1087
1088 def _handleHotkeyTable(self, data, (id, generation)):
1089 """Handle the result of the query of the hotkey table."""
1090 with self._hotkeyLock:
1091 if id==self._hotkeySetID and generation==self._hotkeySetGeneration:
1092 writeData = self._setupHotkeys(data)
1093 self._handler.requestWrite(writeData,
1094 self._handleHotkeysWritten,
1095 (id, generation))
1096
1097 def _handleHotkeysWritten(self, success, (id, generation)):
1098 """Handle the result of the hotkeys having been written."""
1099 with self._hotkeyLock:
1100 if success and id==self._hotkeySetID and \
1101 generation==self._hotkeySetGeneration:
1102 data = [(offset + 3, "b") for offset in self._hotkeyOffets]
1103
1104 self._hotkeyRequestID = \
1105 self._handler.requestPeriodicRead(0.5, data,
1106 self._handleHotkeys,
1107 (id, generation))
1108
1109 def _handleHotkeys(self, data, (id, generation)):
1110 """Handle the hotkeys."""
1111 with self._hotkeyLock:
1112 if id!=self._hotkeySetID or generation!=self._hotkeySetGeneration:
1113 return
1114
1115 callback = self._hotkeyCallback
1116 offsets = self._hotkeyOffets
1117
1118 hotkeysPressed = []
1119 for i in range(0, len(data)):
1120 if data[i]!=0:
1121 hotkeysPressed.append(i)
1122
1123 if hotkeysPressed:
1124 data = []
1125 for index in hotkeysPressed:
1126 data.append((offsets[index]+3, "b", int(0)))
1127 self._handler.requestWrite(data, self._handleHotkeysCleared)
1128
1129 callback(id, hotkeysPressed)
1130
1131 def _handleHotkeysCleared(self, sucess, extra):
1132 """Callback for the hotkey-clearing write request."""
1133
1134 def _clearHotkeyRequest(self):
1135 """Clear the hotkey request in the handler if there is any."""
1136 if self._hotkeyRequestID is not None:
1137 self._handler.clearPeriodic(self._hotkeyRequestID)
1138 self._hotkeyRequestID = None
1139
1140#------------------------------------------------------------------------------
1141
1142class AircraftModel(object):
1143 """Base class for the aircraft models.
1144
1145 Aircraft models handle the data arriving from FSUIPC and turn it into an
1146 object describing the aircraft's state."""
1147 monitoringData = [("paused", 0x0264, "H"),
1148 ("latitude", 0x0560, "l"),
1149 ("longitude", 0x0568, "l"),
1150 ("frozen", 0x3364, "H"),
1151 ("replay", 0x0628, "d"),
1152 ("slew", 0x05dc, "H"),
1153 ("overspeed", 0x036d, "b"),
1154 ("stalled", 0x036c, "b"),
1155 ("onTheGround", 0x0366, "H"),
1156 ("zfw", 0x3bfc, "d"),
1157 ("grossWeight", 0x30c0, "f"),
1158 ("heading", 0x0580, "d"),
1159 ("pitch", 0x0578, "d"),
1160 ("bank", 0x057c, "d"),
1161 ("ias", 0x02bc, "d"),
1162 ("mach", 0x11c6, "H"),
1163 ("groundSpeed", 0x02b4, "d"),
1164 ("vs", 0x02c8, "d"),
1165 ("radioAltitude", 0x31e4, "d"),
1166 ("altitude", 0x0570, "l"),
1167 ("gLoad", 0x11ba, "H"),
1168 ("flapsControl", 0x0bdc, "d"),
1169 ("flapsLeft", 0x0be0, "d"),
1170 ("flapsRight", 0x0be4, "d"),
1171 ("lights", 0x0d0c, "H"),
1172 ("pitot", 0x029c, "b"),
1173 ("parking", 0x0bc8, "H"),
1174 ("gearControl", 0x0be8, "d"),
1175 ("noseGear", 0x0bec, "d"),
1176 ("spoilersArmed", 0x0bcc, "d"),
1177 ("spoilers", 0x0bd0, "d"),
1178 ("altimeter", 0x0330, "H"),
1179 ("nav1", 0x0350, "H"),
1180 ("nav1_obs", 0x0c4e, "H"),
1181 ("nav2", 0x0352, "H"),
1182 ("nav2_obs", 0x0c5e, "H"),
1183 ("adf1_main", 0x034c, "H"),
1184 ("adf1_ext", 0x0356, "H"),
1185 ("adf2_main", 0x02d4, "H"),
1186 ("adf2_ext", 0x02d6, "H"),
1187 ("squawk", 0x0354, "H"),
1188 ("windSpeed", 0x0e90, "H"),
1189 ("windDirection", 0x0e92, "H"),
1190 ("visibility", 0x0e8a, "H"),
1191 ("cog", 0x2ef8, "f"),
1192 ("xpdrC", 0x7b91, "b"),
1193 ("apMaster", 0x07bc, "d"),
1194 ("apHeadingHold", 0x07c8, "d"),
1195 ("apHeading", 0x07cc, "H"),
1196 ("apAltitudeHold", 0x07d0, "d"),
1197 ("apAltitude", 0x07d4, "u"),
1198 ("elevatorTrim", 0x2ea0, "f"),
1199 ("eng1DeIce", 0x08b2, "H"),
1200 ("eng2DeIce", 0x094a, "H"),
1201 ("propDeIce", 0x337c, "b"),
1202 ("structDeIce", 0x337d, "b")]
1203
1204 specialModels = []
1205
1206 @staticmethod
1207 def registerSpecial(clazz):
1208 """Register the given class as a special model."""
1209 AircraftModel.specialModels.append(clazz)
1210
1211 @staticmethod
1212 def findSpecial(aircraft, aircraftName):
1213 for specialModel in AircraftModel.specialModels:
1214 if specialModel.doesHandle(aircraft, aircraftName):
1215 return specialModel
1216 return None
1217
1218 @staticmethod
1219 def create(aircraft, aircraftName):
1220 """Create the model for the given aircraft name, and notify the
1221 aircraft about it."""
1222 specialModel = AircraftModel.findSpecial(aircraft, aircraftName)
1223 if specialModel is not None:
1224 return specialModel()
1225 if aircraft.type in _genericModels:
1226 return _genericModels[aircraft.type]()
1227 else:
1228 return GenericModel()
1229
1230 @staticmethod
1231 def convertBCD(data, length):
1232 """Convert a data item encoded as BCD into a string of the given number
1233 of digits."""
1234 bcd = ""
1235 for i in range(0, length):
1236 digit = chr(ord('0') + (data&0x0f))
1237 data >>= 4
1238 bcd = digit + bcd
1239 return bcd
1240
1241 @staticmethod
1242 def convertFrequency(data):
1243 """Convert the given frequency data to a string."""
1244 bcd = AircraftModel.convertBCD(data, 4)
1245 return "1" + bcd[0:2] + "." + bcd[2:4]
1246
1247 @staticmethod
1248 def convertADFFrequency(main, ext):
1249 """Convert the given ADF frequency data to a string."""
1250 mainBCD = AircraftModel.convertBCD(main, 4)
1251 extBCD = AircraftModel.convertBCD(ext, 4)
1252
1253 return (extBCD[1] if extBCD[1]!="0" else "") + \
1254 mainBCD[1:] + "." + extBCD[3]
1255
1256 def __init__(self, flapsNotches):
1257 """Construct the aircraft model.
1258
1259 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1260 self._flapsNotches = flapsNotches
1261
1262 @property
1263 def name(self):
1264 """Get the name for this aircraft model."""
1265 return "FSUIPC/Generic"
1266
1267 def doesHandle(self, aircraft, aircraftName):
1268 """Determine if the model handles the given aircraft name.
1269
1270 This default implementation returns False."""
1271 return False
1272
1273 def _addOffsetWithIndexMember(self, dest, offset, type, attrName = None):
1274 """Add the given FSUIPC offset and type to the given array and a member
1275 attribute with the given name."""
1276 dest.append((offset, type))
1277 if attrName is not None:
1278 setattr(self, attrName, len(dest)-1)
1279
1280 def _addDataWithIndexMembers(self, dest, prefix, data):
1281 """Add FSUIPC data to the given array and also corresponding index
1282 member variables with the given prefix.
1283
1284 data is a list of triplets of the following items:
1285 - the name of the data item. The index member variable will have a name
1286 created by prepending the given prefix to this name.
1287 - the FSUIPC offset
1288 - the FSUIPC type
1289
1290 The latter two items will be appended to dest."""
1291 for (name, offset, type) in data:
1292 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
1293
1294 def addMonitoringData(self, data, fsType):
1295 """Add the model-specific monitoring data to the given array."""
1296 self._addDataWithIndexMembers(data, "_monidx_",
1297 AircraftModel.monitoringData)
1298
1299 def getAircraftState(self, aircraft, timestamp, data):
1300 """Get an aircraft state object for the given monitoring data."""
1301 state = fs.AircraftState()
1302
1303 state.timestamp = timestamp
1304
1305 state.latitude = data[self._monidx_latitude] * \
1306 90.0 / 10001750.0 / 65536.0 / 65536.0
1307
1308 state.longitude = data[self._monidx_longitude] * \
1309 360.0 / 65536.0 / 65536.0 / 65536.0 / 65536.0
1310 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1311
1312 state.paused = data[self._monidx_paused]!=0 or \
1313 data[self._monidx_frozen]!=0 or \
1314 data[self._monidx_replay]!=0
1315 state.trickMode = data[self._monidx_slew]!=0
1316
1317 state.overspeed = data[self._monidx_overspeed]!=0
1318 state.stalled = data[self._monidx_stalled]!=0
1319 state.onTheGround = data[self._monidx_onTheGround]!=0
1320
1321 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
1322 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
1323
1324 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
1325
1326 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
1327 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
1328
1329 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
1330 state.mach = data[self._monidx_mach] / 20480.0
1331 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
1332 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
1333
1334 state.radioAltitude = \
1335 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
1336 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
1337
1338 state.gLoad = data[self._monidx_gLoad] / 625.0
1339
1340 numNotchesM1 = len(self._flapsNotches) - 1
1341 flapsIncrement = 16383 / numNotchesM1
1342 flapsControl = data[self._monidx_flapsControl]
1343 flapsIndex = flapsControl / flapsIncrement
1344 if flapsIndex < numNotchesM1:
1345 if (flapsControl - (flapsIndex*flapsIncrement) >
1346 (flapsIndex+1)*flapsIncrement - flapsControl):
1347 flapsIndex += 1
1348 state.flapsSet = self._flapsNotches[flapsIndex]
1349
1350 flapsLeft = data[self._monidx_flapsLeft]
1351 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
1352
1353 lights = data[self._monidx_lights]
1354
1355 state.navLightsOn = (lights&0x01) != 0
1356 state.antiCollisionLightsOn = (lights&0x02) != 0
1357 state.landingLightsOn = (lights&0x04) != 0
1358 state.strobeLightsOn = (lights&0x10) != 0
1359
1360 state.pitotHeatOn = data[self._monidx_pitot]!=0
1361
1362 state.parking = data[self._monidx_parking]!=0
1363
1364 state.gearControlDown = data[self._monidx_gearControl]==16383
1365 state.gearsDown = data[self._monidx_noseGear]==16383
1366
1367 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
1368
1369 spoilers = data[self._monidx_spoilers]
1370 if spoilers<=4800:
1371 state.spoilersExtension = 0.0
1372 else:
1373 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
1374
1375 state.altimeter = data[self._monidx_altimeter] / 16.0
1376 state.altimeterReliable = True
1377
1378 state.ils = None
1379 state.ils_obs = None
1380 state.ils_manual = False
1381 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
1382 state.nav1_obs = data[self._monidx_nav1_obs]
1383 state.nav1_manual = True
1384 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
1385 state.nav2_obs = data[self._monidx_nav2_obs]
1386 state.nav2_manual = True
1387 state.adf1 = \
1388 AircraftModel.convertADFFrequency(data[self._monidx_adf1_main],
1389 data[self._monidx_adf1_ext])
1390 state.adf2 = \
1391 AircraftModel.convertADFFrequency(data[self._monidx_adf2_main],
1392 data[self._monidx_adf2_ext])
1393
1394 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
1395
1396 state.windSpeed = data[self._monidx_windSpeed]
1397 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
1398 if state.windDirection<0.0: state.windDirection += 360.0
1399
1400 state.visibility = data[self._monidx_visibility]*1609.344/100.0
1401
1402 state.cog = data[self._monidx_cog]
1403
1404 state.xpdrC = data[self._monidx_xpdrC]!=1
1405 state.autoXPDR = False
1406
1407 state.apMaster = data[self._monidx_apMaster]!=0
1408 state.apHeadingHold = data[self._monidx_apHeadingHold]!=0
1409 state.apHeading = data[self._monidx_apHeading] * 360.0 / 65536.0
1410 state.apAltitudeHold = data[self._monidx_apAltitudeHold]!=0
1411 state.apAltitude = data[self._monidx_apAltitude] / \
1412 const.FEETTOMETRES / 65536.0
1413
1414
1415 state.elevatorTrim = data[self._monidx_elevatorTrim] * 180.0 / math.pi
1416
1417 state.antiIceOn = data[self._monidx_eng1DeIce]!=0 or \
1418 data[self._monidx_eng2DeIce]!=0 or \
1419 data[self._monidx_propDeIce]!=0 or \
1420 data[self._monidx_structDeIce]!=0
1421
1422 return state
1423
1424#------------------------------------------------------------------------------
1425
1426class GenericAircraftModel(AircraftModel):
1427 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1428 values and some other common parameters in a generic way."""
1429
1430 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1431 """Construct the generic aircraft model with the given data.
1432
1433 flapsNotches is an array of how much degrees the individual flaps
1434 notches mean.
1435
1436 fuelTanks is an array of const.FUELTANK_XXX constants about the
1437 aircraft's fuel tanks. They will be converted to offsets.
1438
1439 numEngines is the number of engines the aircraft has.
1440
1441 isN1 determines if the engines have an N1 value or an RPM value
1442 (e.g. pistons)."""
1443 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1444
1445 self._fuelTanks = fuelTanks
1446 self._fuelStartIndex = None
1447 self._numEngines = numEngines
1448 self._engineStartIndex = None
1449 self._isN1 = isN1
1450
1451 def doesHandle(self, aircraft, aircraftName):
1452 """Determine if the model handles the given aircraft name.
1453
1454 This implementation returns True."""
1455 return True
1456
1457 def addMonitoringData(self, data, fsType):
1458 """Add the model-specific monitoring data to the given array."""
1459 super(GenericAircraftModel, self).addMonitoringData(data, fsType)
1460
1461 self._fuelStartIndex = self._addFuelOffsets(data, "_monidx_fuelWeight")
1462
1463 self._engineStartIndex = len(data)
1464 for i in range(0, self._numEngines):
1465 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
1466 if self._isN1:
1467 self._addOffsetWithIndexMember(data, 0x2000 + i * 0x100, "f") # N1
1468 else:
1469 self._addOffsetWithIndexMember(data, 0x0898 + i * 0x98, "H") # RPM
1470 self._addOffsetWithIndexMember(data, 0x08c8 + i * 0x98, "H") # RPM scaler
1471
1472 def getAircraftState(self, aircraft, timestamp, data):
1473 """Get the aircraft state.
1474
1475 Get it from the parent, and then add the data about the fuel levels and
1476 the engine parameters."""
1477 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1478 timestamp,
1479 data)
1480
1481 (state.fuel, state.totalFuel) = \
1482 self._convertFuelData(data, index = self._monidx_fuelWeight)
1483
1484 state.n1 = [] if self._isN1 else None
1485 state.rpm = None if self._isN1 else []
1486 itemsPerEngine = 2 if self._isN1 else 3
1487
1488 state.reverser = []
1489 for i in range(self._engineStartIndex,
1490 self._engineStartIndex +
1491 itemsPerEngine*self._numEngines,
1492 itemsPerEngine):
1493 state.reverser.append(data[i]<0)
1494 if self._isN1:
1495 state.n1.append(data[i+1])
1496 else:
1497 state.rpm.append(data[i+1] * data[i+2]/65536.0)
1498
1499 return state
1500
1501 def getFuel(self, handler, callback):
1502 """Get the fuel information for this model.
1503
1504 See Simulator.getFuel for more information. This
1505 implementation simply queries the fuel tanks given to the
1506 constructor."""
1507 data = []
1508 self._addFuelOffsets(data)
1509
1510 handler.requestRead(data, self._handleFuelRetrieved,
1511 extra = callback)
1512
1513 def setFuelLevel(self, handler, levels):
1514 """Set the fuel level.
1515
1516 See the description of Simulator.setFuelLevel. This
1517 implementation simply sets the fuel tanks as given."""
1518 data = []
1519 for (tank, level) in levels:
1520 offset = _tank2offset[tank]
1521 value = long(level * 128.0 * 65536.0)
1522 data.append( (offset, "u", value) )
1523
1524 handler.requestWrite(data, self._handleFuelWritten)
1525
1526 def _addFuelOffsets(self, data, weightIndexName = None):
1527 """Add the fuel offsets to the given data array.
1528
1529 If weightIndexName is not None, it will be the name of the
1530 fuel weight index.
1531
1532 Returns the index of the first fuel tank's data."""
1533 self._addOffsetWithIndexMember(data, 0x0af4, "H", weightIndexName)
1534
1535 fuelStartIndex = len(data)
1536 for tank in self._fuelTanks:
1537 offset = _tank2offset[tank]
1538 self._addOffsetWithIndexMember(data, offset, "u") # tank level
1539 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
1540
1541 return fuelStartIndex
1542
1543 def _convertFuelData(self, data, index = 0, addCapacities = False):
1544 """Convert the given data into a fuel info list.
1545
1546 The list consists of two or three-tuples of the following
1547 items:
1548 - the fuel tank ID,
1549 - the amount of the fuel in kg,
1550 - if addCapacities is True, the total capacity of the tank."""
1551 fuelWeight = data[index] / 256.0
1552 index += 1
1553
1554 result = []
1555 totalFuel = 0
1556 for fuelTank in self._fuelTanks:
1557 capacity = data[index+1] * fuelWeight * const.LBSTOKG
1558 amount = data[index] * capacity / 128.0 / 65536.0
1559 index += 2
1560
1561 result.append( (fuelTank, amount, capacity) if addCapacities
1562 else (fuelTank, amount))
1563 totalFuel += amount
1564
1565 return (result, totalFuel)
1566
1567 def _handleFuelRetrieved(self, data, callback):
1568 """Callback for a fuel retrieval request."""
1569 (fuelData, _totalFuel) = self._convertFuelData(data,
1570 addCapacities = True)
1571 callback(fuelData)
1572
1573 def _handleFuelWritten(self, success, extra):
1574 """Callback for a fuel setting request."""
1575 pass
1576
1577#------------------------------------------------------------------------------
1578
1579class GenericModel(GenericAircraftModel):
1580 """Generic aircraft model for an unknown type."""
1581 def __init__(self):
1582 """Construct the model."""
1583 super(GenericModel, self). \
1584 __init__(flapsNotches = [0, 10, 20, 30],
1585 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1586 numEngines = 2)
1587
1588 @property
1589 def name(self):
1590 """Get the name for this aircraft model."""
1591 return "FSUIPC/Generic"
1592
1593#------------------------------------------------------------------------------
1594
1595class B737Model(GenericAircraftModel):
1596 """Generic model for the Boeing 737 Classing and NG aircraft."""
1597 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1598
1599 def __init__(self):
1600 """Construct the model."""
1601 super(B737Model, self). \
1602 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1603 fuelTanks = B737Model.fuelTanks,
1604 numEngines = 2)
1605
1606 @property
1607 def name(self):
1608 """Get the name for this aircraft model."""
1609 return "FSUIPC/Generic Boeing 737"
1610
1611#------------------------------------------------------------------------------
1612
1613class PMDGBoeing737NGModel(B737Model):
1614 """A model handler for the PMDG Boeing 737NG model."""
1615 @staticmethod
1616 def doesHandle(aircraft, (name, airPath)):
1617 """Determine if this model handler handles the aircraft with the given
1618 name."""
1619 return aircraft.type in [const.AIRCRAFT_B736,
1620 const.AIRCRAFT_B737,
1621 const.AIRCRAFT_B738,
1622 const.AIRCRAFT_B738C] and \
1623 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
1624 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
1625 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
1626 name.find("700")!=-1 or airPath.find("700")!=-1 or \
1627 name.find("800")!=-1 or airPath.find("800")!=-1 or \
1628 name.find("900")!=-1 or airPath.find("900")!=-1)
1629
1630 @property
1631 def name(self):
1632 """Get the name for this aircraft model."""
1633 return "FSUIPC/PMDG Boeing 737NG(X)"
1634
1635 def addMonitoringData(self, data, fsType):
1636 """Add the model-specific monitoring data to the given array."""
1637 self._fsType = fsType
1638
1639 super(PMDGBoeing737NGModel, self).addMonitoringData(data, fsType)
1640
1641 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
1642 self._addOffsetWithIndexMember(data, 0x6216, "b", "_pmdgidx_xpdr")
1643 self._addOffsetWithIndexMember(data, 0x6227, "b", "_pmdgidx_ap")
1644 self._addOffsetWithIndexMember(data, 0x6228, "b", "_pmdgidx_aphdgsel")
1645 self._addOffsetWithIndexMember(data, 0x622a, "b", "_pmdgidx_apalthold")
1646 self._addOffsetWithIndexMember(data, 0x622c, "H", "_pmdgidx_aphdg")
1647 self._addOffsetWithIndexMember(data, 0x622e, "H", "_pmdgidx_apalt")
1648
1649 if fsType==const.SIM_MSFSX:
1650 print "FSX detected, adding position lights switch offset"
1651 self._addOffsetWithIndexMember(data, 0x6500, "b",
1652 "_pmdgidx_lts_positionsw")
1653
1654 def getAircraftState(self, aircraft, timestamp, data):
1655 """Get the aircraft state.
1656
1657 Get it from the parent, and then check some PMDG-specific stuff."""
1658 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
1659 timestamp,
1660 data)
1661 if data[self._pmdgidx_switches]&0x01==0x01:
1662 state.altimeter = 1013.25
1663
1664 state.xpdrC = data[self._pmdgidx_xpdr]==4
1665
1666 state.apMaster = data[self._pmdgidx_ap]&0x02==0x02
1667
1668 state.apHeadingHold = data[self._pmdgidx_aphdgsel]==2
1669 state.apHeading = data[self._pmdgidx_aphdg]
1670
1671 apalthold = data[self._pmdgidx_apalthold]
1672 state.apAltitudeHold = apalthold>=3 and apalthold<=6
1673 state.apAltitude = data[self._pmdgidx_apalt]
1674
1675 if self._fsType==const.SIM_MSFSX:
1676 state.strobeLightsOn = data[self._pmdgidx_lts_positionsw]==0x02
1677
1678 return state
1679
1680#------------------------------------------------------------------------------
1681
1682class B767Model(GenericAircraftModel):
1683 """Generic model for the Boeing 767 aircraft."""
1684 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1685
1686 def __init__(self):
1687 """Construct the model."""
1688 super(B767Model, self). \
1689 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1690 fuelTanks = B767Model.fuelTanks,
1691 numEngines = 2)
1692
1693 @property
1694 def name(self):
1695 """Get the name for this aircraft model."""
1696 return "FSUIPC/Generic Boeing 767"
1697
1698#------------------------------------------------------------------------------
1699
1700class DH8DModel(GenericAircraftModel):
1701 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1702 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1703
1704 def __init__(self):
1705 """Construct the model."""
1706 super(DH8DModel, self). \
1707 __init__(flapsNotches = [0, 5, 10, 15, 35],
1708 fuelTanks = DH8DModel.fuelTanks,
1709 numEngines = 2)
1710
1711 @property
1712 def name(self):
1713 """Get the name for this aircraft model."""
1714 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1715
1716#------------------------------------------------------------------------------
1717
1718class DreamwingsDH8DModel(DH8DModel):
1719 """Model handler for the Dreamwings Dash 8-Q400."""
1720 @staticmethod
1721 def doesHandle(aircraft, (name, airPath)):
1722 """Determine if this model handler handles the aircraft with the given
1723 name."""
1724 return aircraft.type==const.AIRCRAFT_DH8D and \
1725 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1726 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1727 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1728 airPath.find("Dash8Q400")!=-1
1729
1730 @property
1731 def name(self):
1732 """Get the name for this aircraft model."""
1733 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1734
1735 def getAircraftState(self, aircraft, timestamp, data):
1736 """Get the aircraft state.
1737
1738 Get it from the parent, and then invert the pitot heat state."""
1739 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1740 timestamp,
1741 data)
1742 state.pitotHeatOn = not state.pitotHeatOn
1743
1744 return state
1745
1746#------------------------------------------------------------------------------
1747
1748class CRJ2Model(GenericAircraftModel):
1749 """Generic model for the Bombardier CRJ-200 aircraft."""
1750 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1751
1752 def __init__(self):
1753 """Construct the model."""
1754 super(CRJ2Model, self). \
1755 __init__(flapsNotches = [0, 8, 20, 30, 45],
1756 fuelTanks = CRJ2Model.fuelTanks,
1757 numEngines = 2)
1758
1759 @property
1760 def name(self):
1761 """Get the name for this aircraft model."""
1762 return "FSUIPC/Generic Bombardier CRJ-200"
1763
1764#------------------------------------------------------------------------------
1765
1766class F70Model(GenericAircraftModel):
1767 """Generic model for the Fokker F70 aircraft."""
1768 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1769
1770 def __init__(self):
1771 """Construct the model."""
1772 super(F70Model, self). \
1773 __init__(flapsNotches = [0, 8, 15, 25, 42],
1774 fuelTanks = F70Model.fuelTanks,
1775 numEngines = 2)
1776
1777 @property
1778 def name(self):
1779 """Get the name for this aircraft model."""
1780 return "FSUIPC/Generic Fokker 70"
1781
1782#------------------------------------------------------------------------------
1783
1784class DAF70Model(F70Model):
1785 """Model for the Digital Aviation F70 implementation on FS9."""
1786 @staticmethod
1787 def doesHandle(aircraft, (name, airPath)):
1788 """Determine if this model handler handles the aircraft with the given
1789 name."""
1790 return aircraft.type == const.AIRCRAFT_F70 and \
1791 (airPath.endswith("fokker70_2k4_v4.1.air") or
1792 airPath.endswith("fokker70_2k4_v4.3.air"))
1793
1794 @property
1795 def name(self):
1796 """Get the name for this aircraft model."""
1797 return "FSUIPC/Digital Aviation Fokker 70"
1798
1799 def getAircraftState(self, aircraft, timestamp, data):
1800 """Get the aircraft state.
1801
1802 Get it from the parent, and then invert the pitot heat state."""
1803 state = super(DAF70Model, self).getAircraftState(aircraft,
1804 timestamp,
1805 data)
1806 state.navLightsOn = None
1807 state.landingLightsOn = None
1808
1809 state.altimeterReliable = False
1810
1811 state.ils = state.nav1
1812 state.ils_obs = state.nav1_obs
1813 state.ils_manual = state.nav1_manual
1814
1815 state.nav1 = state.nav2
1816 state.nav1_obs = state.nav2_obs
1817 state.nav1_manual = aircraft.flight.stage!=const.STAGE_CRUISE
1818
1819 state.nav2 = None
1820 state.nav2_obs = None
1821 state.nav2_manual = False
1822
1823 state.autoXPDR = True
1824
1825 return state
1826
1827#------------------------------------------------------------------------------
1828
1829class DC3Model(GenericAircraftModel):
1830 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1831 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
1832 const.FUELTANK_RIGHT]
1833 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1834 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1835
1836 def __init__(self):
1837 """Construct the model."""
1838 super(DC3Model, self). \
1839 __init__(flapsNotches = [0, 15, 30, 45],
1840 fuelTanks = DC3Model.fuelTanks,
1841 numEngines = 2, isN1 = False)
1842 self._leftLevel = 0.0
1843 self._rightLevel = 0.0
1844
1845 @property
1846 def name(self):
1847 """Get the name for this aircraft model."""
1848 return "FSUIPC/Generic Lisunov Li-2 (DC-3)"
1849
1850 def _convertFuelData(self, data, index = 0, addCapacities = False):
1851 """Convert the given data into a fuel info list.
1852
1853 It assumes to receive the 3 fuel tanks as seen above (left,
1854 centre and right) and converts it to left aux, left, right,
1855 and right aux. The amount in the left tank goes into left aux,
1856 the amount of the right tank goes into right aux and the
1857 amount of the centre tank goes into the left and right tanks
1858 evenly distributed."""
1859 (rawFuelData, totalFuel) = \
1860 super(DC3Model, self)._convertFuelData(data, index, addCapacities)
1861
1862 centreAmount = rawFuelData[1][1]
1863 if addCapacities:
1864 centreCapacity = rawFuelData[1][2]
1865 self._leftLevel = self._rightLevel = \
1866 centreAmount / centreCapacity / 2.0
1867 fuelData = [(const.FUELTANK_LEFT_AUX,
1868 rawFuelData[0][1], rawFuelData[0][2]),
1869 (const.FUELTANK_LEFT,
1870 centreAmount/2.0, centreCapacity/2.0),
1871 (const.FUELTANK_RIGHT,
1872 centreAmount/2.0, centreCapacity/2.0),
1873 (const.FUELTANK_RIGHT_AUX,
1874 rawFuelData[2][1], rawFuelData[2][2])]
1875 else:
1876 fuelData = [(const.FUELTANK_LEFT_AUX, rawFuelData[0][1]),
1877 (const.FUELTANK_LEFT, centreAmount/2.0),
1878 (const.FUELTANK_RIGHT, centreAmount/2.0),
1879 (const.FUELTANK_RIGHT_AUX, rawFuelData[2][1])]
1880
1881 return (fuelData, totalFuel)
1882
1883 def setFuelLevel(self, handler, levels):
1884 """Set the fuel level.
1885
1886 See the description of Simulator.setFuelLevel. This
1887 implementation assumes to get the four-tank representation,
1888 as returned by getFuel()."""
1889 leftLevel = None
1890 centreLevel = None
1891 rightLevel = None
1892
1893 for (tank, level) in levels:
1894 if tank==const.FUELTANK_LEFT_AUX:
1895 leftLevel = level if leftLevel is None else (leftLevel + level)
1896 elif tank==const.FUELTANK_LEFT:
1897 level /= 2.0
1898 centreLevel = (self._rightLevel + level) \
1899 if centreLevel is None else (centreLevel + level)
1900 self._leftLevel = level
1901 elif tank==const.FUELTANK_RIGHT:
1902 level /= 2.0
1903 centreLevel = (self._leftLevel + level) \
1904 if centreLevel is None else (centreLevel + level)
1905 self._rightLevel = level
1906 elif tank==const.FUELTANK_RIGHT_AUX:
1907 rightLevel = level if rightLevel is None \
1908 else (rightLevel + level)
1909
1910 levels = []
1911 if leftLevel is not None: levels.append((const.FUELTANK_LEFT,
1912 leftLevel))
1913 if centreLevel is not None: levels.append((const.FUELTANK_CENTRE,
1914 centreLevel))
1915 if rightLevel is not None: levels.append((const.FUELTANK_RIGHT,
1916 rightLevel))
1917
1918 super(DC3Model, self).setFuelLevel(handler, levels)
1919
1920#------------------------------------------------------------------------------
1921
1922class T134Model(GenericAircraftModel):
1923 """Generic model for the Tupolev Tu-134 aircraft."""
1924 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
1925 const.FUELTANK_LEFT_AUX,
1926 const.FUELTANK_CENTRE,
1927 const.FUELTANK_RIGHT_AUX,
1928 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
1929
1930 def __init__(self):
1931 """Construct the model."""
1932 super(T134Model, self). \
1933 __init__(flapsNotches = [0, 10, 20, 30],
1934 fuelTanks = T134Model.fuelTanks,
1935 numEngines = 2)
1936
1937 @property
1938 def name(self):
1939 """Get the name for this aircraft model."""
1940 return "FSUIPC/Generic Tupolev Tu-134"
1941
1942#------------------------------------------------------------------------------
1943
1944class T154Model(GenericAircraftModel):
1945 """Generic model for the Tupolev Tu-134 aircraft."""
1946 fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1947 const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
1948 const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1949
1950 def __init__(self):
1951 """Construct the model."""
1952 super(T154Model, self). \
1953 __init__(flapsNotches = [0, 15, 28, 45],
1954 fuelTanks = T154Model.fuelTanks,
1955 numEngines = 3)
1956
1957 @property
1958 def name(self):
1959 """Get the name for this aircraft model."""
1960 return "FSUIPC/Generic Tupolev Tu-154"
1961
1962 def getAircraftState(self, aircraft, timestamp, data):
1963 """Get an aircraft state object for the given monitoring data.
1964
1965 This removes the reverser value for the middle engine."""
1966 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1967 del state.reverser[1]
1968 return state
1969
1970#------------------------------------------------------------------------------
1971
1972class PTT154Model(T154Model):
1973 """Project Tupolev Tu-154."""
1974 @staticmethod
1975 def doesHandle(aircraft, (name, airPath)):
1976 """Determine if this model handler handles the aircraft with the given
1977 name."""
1978 print "PTT154Model.doesHandle", aircraft.type, name, airPath
1979 return aircraft.type==const.AIRCRAFT_T154 and \
1980 name.find("Tu-154")!=-1 and \
1981 os.path.basename(airPath).startswith("154b_")
1982
1983 def __init__(self):
1984 """Construct the model."""
1985 super(PTT154Model, self).__init__()
1986 self._adf1 = None
1987 self._adf2 = None
1988 self._lastValue = None
1989
1990 @property
1991 def name(self):
1992 """Get the name for this aircraft model."""
1993 return "FSUIPC/Project Tupolev Tu-154"
1994
1995 def getAircraftState(self, aircraft, timestamp, data):
1996 """Get an aircraft state object for the given monitoring data.
1997
1998 This removes the reverser value for the middle engine."""
1999 state = super(PTT154Model, self).getAircraftState(aircraft, timestamp, data)
2000
2001 adf1 = state.adf1
2002 if self._adf1 is None:
2003 self._adf1 = self._adf2 = adf1
2004 elif adf1 != self._lastValue and adf1 != self._adf1 and \
2005 adf1 != self._adf2:
2006 if self._lastValue==self._adf2:
2007 self._adf1 = adf1
2008 else:
2009 self._adf2 = adf1
2010
2011 self._lastValue = adf1
2012 state.adf1 = self._adf1
2013 state.adf2 = self._adf2
2014
2015 return state
2016
2017
2018#------------------------------------------------------------------------------
2019
2020class YK40Model(GenericAircraftModel):
2021 """Generic model for the Yakovlev Yak-40 aircraft."""
2022 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
2023
2024 def __init__(self):
2025 """Construct the model."""
2026 super(YK40Model, self). \
2027 __init__(flapsNotches = [0, 20, 35],
2028 fuelTanks = YK40Model.fuelTanks,
2029 numEngines = 2)
2030
2031 @property
2032 def name(self):
2033 """Get the name for this aircraft model."""
2034 return "FSUIPC/Generic Yakovlev Yak-40"
2035
2036#------------------------------------------------------------------------------
2037
2038_genericModels = { const.AIRCRAFT_B736 : B737Model,
2039 const.AIRCRAFT_B737 : B737Model,
2040 const.AIRCRAFT_B738 : B737Model,
2041 const.AIRCRAFT_B738C : B737Model,
2042 const.AIRCRAFT_B733 : B737Model,
2043 const.AIRCRAFT_B734 : B737Model,
2044 const.AIRCRAFT_B735 : B737Model,
2045 const.AIRCRAFT_DH8D : DH8DModel,
2046 const.AIRCRAFT_B762 : B767Model,
2047 const.AIRCRAFT_B763 : B767Model,
2048 const.AIRCRAFT_CRJ2 : CRJ2Model,
2049 const.AIRCRAFT_F70 : F70Model,
2050 const.AIRCRAFT_DC3 : DC3Model,
2051 const.AIRCRAFT_T134 : T134Model,
2052 const.AIRCRAFT_T154 : T154Model,
2053 const.AIRCRAFT_YK40 : YK40Model }
2054
2055#------------------------------------------------------------------------------
2056
2057AircraftModel.registerSpecial(PMDGBoeing737NGModel)
2058AircraftModel.registerSpecial(DreamwingsDH8DModel)
2059AircraftModel.registerSpecial(DAF70Model)
2060AircraftModel.registerSpecial(PTT154Model)
2061
2062#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.