source: src/mlx/fsuipc.py@ 435:54d318811885

Last change on this file since 435:54d318811885 was 412:01b3f7c87ceb, checked in by István Váradi <ivaradi@…>, 11 years ago

The index is always stepped, not only if the capacity is correct (re #178)

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