source: src/mlx/fsuipc.py@ 401:15ad3c16ee11

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

The exception strings are converted from UTF-8 to unicode for proper logging (re #170)

File size: 78.7 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 ("nav1", 0x0350, "H"),
1184 ("nav1_obs", 0x0c4e, "H"),
1185 ("nav2", 0x0352, "H"),
1186 ("nav2_obs", 0x0c5e, "H"),
1187 ("adf1_main", 0x034c, "H"),
1188 ("adf1_ext", 0x0356, "H"),
1189 ("adf2_main", 0x02d4, "H"),
1190 ("adf2_ext", 0x02d6, "H"),
1191 ("squawk", 0x0354, "H"),
1192 ("windSpeed", 0x0e90, "H"),
1193 ("windDirection", 0x0e92, "H"),
1194 ("visibility", 0x0e8a, "H"),
1195 ("cog", 0x2ef8, "f"),
1196 ("xpdrC", 0x7b91, "b"),
1197 ("apMaster", 0x07bc, "d"),
1198 ("apHeadingHold", 0x07c8, "d"),
1199 ("apHeading", 0x07cc, "H"),
1200 ("apAltitudeHold", 0x07d0, "d"),
1201 ("apAltitude", 0x07d4, "u"),
1202 ("elevatorTrim", 0x2ea0, "f"),
1203 ("eng1DeIce", 0x08b2, "H"),
1204 ("eng2DeIce", 0x094a, "H"),
1205 ("propDeIce", 0x337c, "b"),
1206 ("structDeIce", 0x337d, "b")]
1207
1208 specialModels = []
1209
1210 @staticmethod
1211 def registerSpecial(clazz):
1212 """Register the given class as a special model."""
1213 AircraftModel.specialModels.append(clazz)
1214
1215 @staticmethod
1216 def findSpecial(aircraft, aircraftName):
1217 for specialModel in AircraftModel.specialModels:
1218 if specialModel.doesHandle(aircraft, aircraftName):
1219 return specialModel
1220 return None
1221
1222 @staticmethod
1223 def create(aircraft, aircraftName):
1224 """Create the model for the given aircraft name, and notify the
1225 aircraft about it."""
1226 specialModel = AircraftModel.findSpecial(aircraft, aircraftName)
1227 if specialModel is not None:
1228 return specialModel()
1229 if aircraft.type in _genericModels:
1230 return _genericModels[aircraft.type]()
1231 else:
1232 return GenericModel()
1233
1234 @staticmethod
1235 def convertBCD(data, length):
1236 """Convert a data item encoded as BCD into a string of the given number
1237 of digits."""
1238 bcd = ""
1239 for i in range(0, length):
1240 digit = chr(ord('0') + (data&0x0f))
1241 data >>= 4
1242 bcd = digit + bcd
1243 return bcd
1244
1245 @staticmethod
1246 def convertFrequency(data):
1247 """Convert the given frequency data to a string."""
1248 bcd = AircraftModel.convertBCD(data, 4)
1249 return "1" + bcd[0:2] + "." + bcd[2:4]
1250
1251 @staticmethod
1252 def convertADFFrequency(main, ext):
1253 """Convert the given ADF frequency data to a string."""
1254 mainBCD = AircraftModel.convertBCD(main, 4)
1255 extBCD = AircraftModel.convertBCD(ext, 4)
1256
1257 return (extBCD[1] if extBCD[1]!="0" else "") + \
1258 mainBCD[1:] + "." + extBCD[3]
1259
1260 def __init__(self, flapsNotches):
1261 """Construct the aircraft model.
1262
1263 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1264 self._flapsNotches = flapsNotches
1265
1266 @property
1267 def name(self):
1268 """Get the name for this aircraft model."""
1269 return "FSUIPC/Generic"
1270
1271 def doesHandle(self, aircraft, aircraftName):
1272 """Determine if the model handles the given aircraft name.
1273
1274 This default implementation returns False."""
1275 return False
1276
1277 def _addOffsetWithIndexMember(self, dest, offset, type, attrName = None):
1278 """Add the given FSUIPC offset and type to the given array and a member
1279 attribute with the given name."""
1280 dest.append((offset, type))
1281 if attrName is not None:
1282 setattr(self, attrName, len(dest)-1)
1283
1284 def _addDataWithIndexMembers(self, dest, prefix, data):
1285 """Add FSUIPC data to the given array and also corresponding index
1286 member variables with the given prefix.
1287
1288 data is a list of triplets of the following items:
1289 - the name of the data item. The index member variable will have a name
1290 created by prepending the given prefix to this name.
1291 - the FSUIPC offset
1292 - the FSUIPC type
1293
1294 The latter two items will be appended to dest."""
1295 for (name, offset, type) in data:
1296 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
1297
1298 def addMonitoringData(self, data, fsType):
1299 """Add the model-specific monitoring data to the given array."""
1300 self._addDataWithIndexMembers(data, "_monidx_",
1301 AircraftModel.monitoringData)
1302
1303 def getAircraftState(self, aircraft, timestamp, data):
1304 """Get an aircraft state object for the given monitoring data."""
1305 state = fs.AircraftState()
1306
1307 state.timestamp = timestamp
1308
1309 state.latitude = data[self._monidx_latitude] * \
1310 90.0 / 10001750.0 / 65536.0 / 65536.0
1311
1312 state.longitude = data[self._monidx_longitude] * \
1313 360.0 / 65536.0 / 65536.0 / 65536.0 / 65536.0
1314 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1315
1316 state.paused = data[self._monidx_paused]!=0 or \
1317 data[self._monidx_frozen]!=0 or \
1318 data[self._monidx_replay]!=0
1319 state.trickMode = data[self._monidx_slew]!=0
1320
1321 state.overspeed = data[self._monidx_overspeed]!=0
1322 state.stalled = data[self._monidx_stalled]!=0
1323 state.onTheGround = data[self._monidx_onTheGround]!=0
1324
1325 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
1326 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
1327
1328 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
1329
1330 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
1331 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
1332
1333 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
1334 state.mach = data[self._monidx_mach] / 20480.0
1335 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
1336 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
1337
1338 state.radioAltitude = \
1339 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
1340 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
1341
1342 state.gLoad = data[self._monidx_gLoad] / 625.0
1343
1344 numNotchesM1 = len(self._flapsNotches) - 1
1345 flapsIncrement = 16383 / numNotchesM1
1346 flapsControl = data[self._monidx_flapsControl]
1347 flapsIndex = flapsControl / flapsIncrement
1348 if flapsIndex < numNotchesM1:
1349 if (flapsControl - (flapsIndex*flapsIncrement) >
1350 (flapsIndex+1)*flapsIncrement - flapsControl):
1351 flapsIndex += 1
1352 state.flapsSet = self._flapsNotches[flapsIndex]
1353
1354 flapsLeft = data[self._monidx_flapsLeft]
1355 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
1356
1357 lights = data[self._monidx_lights]
1358
1359 state.navLightsOn = (lights&0x01) != 0
1360 state.antiCollisionLightsOn = (lights&0x02) != 0
1361 state.landingLightsOn = (lights&0x04) != 0
1362 state.strobeLightsOn = (lights&0x10) != 0
1363
1364 state.pitotHeatOn = data[self._monidx_pitot]!=0
1365
1366 state.parking = data[self._monidx_parking]!=0
1367
1368 state.gearControlDown = data[self._monidx_gearControl]==16383
1369 state.gearsDown = data[self._monidx_noseGear]==16383
1370
1371 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
1372
1373 spoilers = data[self._monidx_spoilers]
1374 if spoilers<=4800:
1375 state.spoilersExtension = 0.0
1376 else:
1377 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
1378
1379 state.altimeter = data[self._monidx_altimeter] / 16.0
1380 state.altimeterReliable = True
1381
1382 state.ils = None
1383 state.ils_obs = None
1384 state.ils_manual = False
1385 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
1386 state.nav1_obs = data[self._monidx_nav1_obs]
1387 state.nav1_manual = True
1388 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
1389 state.nav2_obs = data[self._monidx_nav2_obs]
1390 state.nav2_manual = True
1391 state.adf1 = \
1392 AircraftModel.convertADFFrequency(data[self._monidx_adf1_main],
1393 data[self._monidx_adf1_ext])
1394 state.adf2 = \
1395 AircraftModel.convertADFFrequency(data[self._monidx_adf2_main],
1396 data[self._monidx_adf2_ext])
1397
1398 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
1399
1400 state.windSpeed = data[self._monidx_windSpeed]
1401 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
1402 if state.windDirection<0.0: state.windDirection += 360.0
1403
1404 state.visibility = data[self._monidx_visibility]*1609.344/100.0
1405
1406 state.cog = data[self._monidx_cog]
1407
1408 state.xpdrC = data[self._monidx_xpdrC]!=1
1409 state.autoXPDR = False
1410
1411 state.apMaster = data[self._monidx_apMaster]!=0
1412 state.apHeadingHold = data[self._monidx_apHeadingHold]!=0
1413 state.apHeading = data[self._monidx_apHeading] * 360.0 / 65536.0
1414 state.apAltitudeHold = data[self._monidx_apAltitudeHold]!=0
1415 state.apAltitude = data[self._monidx_apAltitude] / \
1416 const.FEETTOMETRES / 65536.0
1417
1418
1419 state.elevatorTrim = data[self._monidx_elevatorTrim] * 180.0 / math.pi
1420
1421 state.antiIceOn = data[self._monidx_eng1DeIce]!=0 or \
1422 data[self._monidx_eng2DeIce]!=0 or \
1423 data[self._monidx_propDeIce]!=0 or \
1424 data[self._monidx_structDeIce]!=0
1425
1426 return state
1427
1428#------------------------------------------------------------------------------
1429
1430class GenericAircraftModel(AircraftModel):
1431 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1432 values and some other common parameters in a generic way."""
1433
1434 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1435 """Construct the generic aircraft model with the given data.
1436
1437 flapsNotches is an array of how much degrees the individual flaps
1438 notches mean.
1439
1440 fuelTanks is an array of const.FUELTANK_XXX constants about the
1441 aircraft's fuel tanks. They will be converted to offsets.
1442
1443 numEngines is the number of engines the aircraft has.
1444
1445 isN1 determines if the engines have an N1 value or an RPM value
1446 (e.g. pistons)."""
1447 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1448
1449 self._fuelTanks = fuelTanks
1450 self._fuelStartIndex = None
1451 self._numEngines = numEngines
1452 self._engineStartIndex = None
1453 self._isN1 = isN1
1454
1455 def doesHandle(self, aircraft, aircraftName):
1456 """Determine if the model handles the given aircraft name.
1457
1458 This implementation returns True."""
1459 return True
1460
1461 def addMonitoringData(self, data, fsType):
1462 """Add the model-specific monitoring data to the given array."""
1463 super(GenericAircraftModel, self).addMonitoringData(data, fsType)
1464
1465 self._fuelStartIndex = self._addFuelOffsets(data, "_monidx_fuelWeight")
1466
1467 self._engineStartIndex = len(data)
1468 for i in range(0, self._numEngines):
1469 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
1470 if self._isN1:
1471 self._addOffsetWithIndexMember(data, 0x2000 + i * 0x100, "f") # N1
1472 else:
1473 self._addOffsetWithIndexMember(data, 0x0898 + i * 0x98, "H") # RPM
1474 self._addOffsetWithIndexMember(data, 0x08c8 + i * 0x98, "H") # RPM scaler
1475
1476 def getAircraftState(self, aircraft, timestamp, data):
1477 """Get the aircraft state.
1478
1479 Get it from the parent, and then add the data about the fuel levels and
1480 the engine parameters."""
1481 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1482 timestamp,
1483 data)
1484
1485 (state.fuel, state.totalFuel) = \
1486 self._convertFuelData(data, index = self._monidx_fuelWeight)
1487
1488 state.n1 = [] if self._isN1 else None
1489 state.rpm = None if self._isN1 else []
1490 itemsPerEngine = 2 if self._isN1 else 3
1491
1492 state.reverser = []
1493 for i in range(self._engineStartIndex,
1494 self._engineStartIndex +
1495 itemsPerEngine*self._numEngines,
1496 itemsPerEngine):
1497 state.reverser.append(data[i]<0)
1498 if self._isN1:
1499 state.n1.append(data[i+1])
1500 else:
1501 state.rpm.append(data[i+1] * data[i+2]/65536.0)
1502
1503 return state
1504
1505 def getFuel(self, handler, callback):
1506 """Get the fuel information for this model.
1507
1508 See Simulator.getFuel for more information. This
1509 implementation simply queries the fuel tanks given to the
1510 constructor."""
1511 data = []
1512 self._addFuelOffsets(data)
1513
1514 handler.requestRead(data, self._handleFuelRetrieved,
1515 extra = callback)
1516
1517 def setFuelLevel(self, handler, levels):
1518 """Set the fuel level.
1519
1520 See the description of Simulator.setFuelLevel. This
1521 implementation simply sets the fuel tanks as given."""
1522 data = []
1523 for (tank, level) in levels:
1524 offset = _tank2offset[tank]
1525 value = long(level * 128.0 * 65536.0)
1526 data.append( (offset, "u", value) )
1527
1528 handler.requestWrite(data, self._handleFuelWritten)
1529
1530 def _addFuelOffsets(self, data, weightIndexName = None):
1531 """Add the fuel offsets to the given data array.
1532
1533 If weightIndexName is not None, it will be the name of the
1534 fuel weight index.
1535
1536 Returns the index of the first fuel tank's data."""
1537 self._addOffsetWithIndexMember(data, 0x0af4, "H", weightIndexName)
1538
1539 fuelStartIndex = len(data)
1540 for tank in self._fuelTanks:
1541 offset = _tank2offset[tank]
1542 self._addOffsetWithIndexMember(data, offset, "u") # tank level
1543 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
1544
1545 return fuelStartIndex
1546
1547 def _convertFuelData(self, data, index = 0, addCapacities = False):
1548 """Convert the given data into a fuel info list.
1549
1550 The list consists of two or three-tuples of the following
1551 items:
1552 - the fuel tank ID,
1553 - the amount of the fuel in kg,
1554 - if addCapacities is True, the total capacity of the tank."""
1555 fuelWeight = data[index] / 256.0
1556 index += 1
1557
1558 result = []
1559 totalFuel = 0
1560 for fuelTank in self._fuelTanks:
1561 capacity = data[index+1] * fuelWeight * const.LBSTOKG
1562 amount = data[index] * capacity / 128.0 / 65536.0
1563 index += 2
1564
1565 result.append( (fuelTank, amount, capacity) if addCapacities
1566 else (fuelTank, amount))
1567 totalFuel += amount
1568
1569 return (result, totalFuel)
1570
1571 def _handleFuelRetrieved(self, data, callback):
1572 """Callback for a fuel retrieval request."""
1573 (fuelData, _totalFuel) = self._convertFuelData(data,
1574 addCapacities = True)
1575 callback(fuelData)
1576
1577 def _handleFuelWritten(self, success, extra):
1578 """Callback for a fuel setting request."""
1579 pass
1580
1581#------------------------------------------------------------------------------
1582
1583class GenericModel(GenericAircraftModel):
1584 """Generic aircraft model for an unknown type."""
1585 def __init__(self):
1586 """Construct the model."""
1587 super(GenericModel, self). \
1588 __init__(flapsNotches = [0, 10, 20, 30],
1589 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1590 numEngines = 2)
1591
1592 @property
1593 def name(self):
1594 """Get the name for this aircraft model."""
1595 return "FSUIPC/Generic"
1596
1597#------------------------------------------------------------------------------
1598
1599class B737Model(GenericAircraftModel):
1600 """Generic model for the Boeing 737 Classing and NG aircraft."""
1601 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1602
1603 def __init__(self):
1604 """Construct the model."""
1605 super(B737Model, self). \
1606 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1607 fuelTanks = B737Model.fuelTanks,
1608 numEngines = 2)
1609
1610 @property
1611 def name(self):
1612 """Get the name for this aircraft model."""
1613 return "FSUIPC/Generic Boeing 737"
1614
1615#------------------------------------------------------------------------------
1616
1617class PMDGBoeing737NGModel(B737Model):
1618 """A model handler for the PMDG Boeing 737NG model."""
1619 @staticmethod
1620 def doesHandle(aircraft, (name, airPath)):
1621 """Determine if this model handler handles the aircraft with the given
1622 name."""
1623 return aircraft.type in [const.AIRCRAFT_B736,
1624 const.AIRCRAFT_B737,
1625 const.AIRCRAFT_B738,
1626 const.AIRCRAFT_B738C] and \
1627 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
1628 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
1629 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
1630 name.find("700")!=-1 or airPath.find("700")!=-1 or \
1631 name.find("800")!=-1 or airPath.find("800")!=-1 or \
1632 name.find("900")!=-1 or airPath.find("900")!=-1)
1633
1634 @property
1635 def name(self):
1636 """Get the name for this aircraft model."""
1637 return "FSUIPC/PMDG Boeing 737NG(X)"
1638
1639 def addMonitoringData(self, data, fsType):
1640 """Add the model-specific monitoring data to the given array."""
1641 self._fsType = fsType
1642
1643 super(PMDGBoeing737NGModel, self).addMonitoringData(data, fsType)
1644
1645 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
1646 self._addOffsetWithIndexMember(data, 0x6216, "b", "_pmdgidx_xpdr")
1647 self._addOffsetWithIndexMember(data, 0x6227, "b", "_pmdgidx_ap")
1648 self._addOffsetWithIndexMember(data, 0x6228, "b", "_pmdgidx_aphdgsel")
1649 self._addOffsetWithIndexMember(data, 0x622a, "b", "_pmdgidx_apalthold")
1650 self._addOffsetWithIndexMember(data, 0x622c, "H", "_pmdgidx_aphdg")
1651 self._addOffsetWithIndexMember(data, 0x622e, "H", "_pmdgidx_apalt")
1652
1653 if fsType==const.SIM_MSFSX:
1654 print "FSX detected, adding position lights switch offset"
1655 self._addOffsetWithIndexMember(data, 0x6500, "b",
1656 "_pmdgidx_lts_positionsw")
1657
1658 def getAircraftState(self, aircraft, timestamp, data):
1659 """Get the aircraft state.
1660
1661 Get it from the parent, and then check some PMDG-specific stuff."""
1662 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
1663 timestamp,
1664 data)
1665 if data[self._pmdgidx_switches]&0x01==0x01:
1666 state.altimeter = 1013.25
1667
1668 state.xpdrC = data[self._pmdgidx_xpdr]==4
1669
1670 state.apMaster = data[self._pmdgidx_ap]&0x02==0x02
1671
1672 state.apHeadingHold = data[self._pmdgidx_aphdgsel]==2
1673 state.apHeading = data[self._pmdgidx_aphdg]
1674
1675 apalthold = data[self._pmdgidx_apalthold]
1676 state.apAltitudeHold = apalthold>=3 and apalthold<=6
1677 state.apAltitude = data[self._pmdgidx_apalt]
1678
1679 if self._fsType==const.SIM_MSFSX:
1680 state.strobeLightsOn = data[self._pmdgidx_lts_positionsw]==0x02
1681
1682 return state
1683
1684#------------------------------------------------------------------------------
1685
1686class B767Model(GenericAircraftModel):
1687 """Generic model for the Boeing 767 aircraft."""
1688 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1689
1690 def __init__(self):
1691 """Construct the model."""
1692 super(B767Model, self). \
1693 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1694 fuelTanks = B767Model.fuelTanks,
1695 numEngines = 2)
1696
1697 @property
1698 def name(self):
1699 """Get the name for this aircraft model."""
1700 return "FSUIPC/Generic Boeing 767"
1701
1702#------------------------------------------------------------------------------
1703
1704class DH8DModel(GenericAircraftModel):
1705 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1706 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1707
1708 def __init__(self):
1709 """Construct the model."""
1710 super(DH8DModel, self). \
1711 __init__(flapsNotches = [0, 5, 10, 15, 35],
1712 fuelTanks = DH8DModel.fuelTanks,
1713 numEngines = 2)
1714
1715 @property
1716 def name(self):
1717 """Get the name for this aircraft model."""
1718 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1719
1720#------------------------------------------------------------------------------
1721
1722class DreamwingsDH8DModel(DH8DModel):
1723 """Model handler for the Dreamwings Dash 8-Q400."""
1724 @staticmethod
1725 def doesHandle(aircraft, (name, airPath)):
1726 """Determine if this model handler handles the aircraft with the given
1727 name."""
1728 return aircraft.type==const.AIRCRAFT_DH8D and \
1729 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1730 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1731 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1732 airPath.find("Dash8Q400")!=-1
1733
1734 @property
1735 def name(self):
1736 """Get the name for this aircraft model."""
1737 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1738
1739 def getAircraftState(self, aircraft, timestamp, data):
1740 """Get the aircraft state.
1741
1742 Get it from the parent, and then invert the pitot heat state."""
1743 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1744 timestamp,
1745 data)
1746 state.pitotHeatOn = not state.pitotHeatOn
1747
1748 return state
1749
1750#------------------------------------------------------------------------------
1751
1752class CRJ2Model(GenericAircraftModel):
1753 """Generic model for the Bombardier CRJ-200 aircraft."""
1754 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1755
1756 def __init__(self):
1757 """Construct the model."""
1758 super(CRJ2Model, self). \
1759 __init__(flapsNotches = [0, 8, 20, 30, 45],
1760 fuelTanks = CRJ2Model.fuelTanks,
1761 numEngines = 2)
1762
1763 @property
1764 def name(self):
1765 """Get the name for this aircraft model."""
1766 return "FSUIPC/Generic Bombardier CRJ-200"
1767
1768#------------------------------------------------------------------------------
1769
1770class F70Model(GenericAircraftModel):
1771 """Generic model for the Fokker F70 aircraft."""
1772 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1773
1774 def __init__(self):
1775 """Construct the model."""
1776 super(F70Model, self). \
1777 __init__(flapsNotches = [0, 8, 15, 25, 42],
1778 fuelTanks = F70Model.fuelTanks,
1779 numEngines = 2)
1780
1781 @property
1782 def name(self):
1783 """Get the name for this aircraft model."""
1784 return "FSUIPC/Generic Fokker 70"
1785
1786#------------------------------------------------------------------------------
1787
1788class DAF70Model(F70Model):
1789 """Model for the Digital Aviation F70 implementation on FS9."""
1790 @staticmethod
1791 def doesHandle(aircraft, (name, airPath)):
1792 """Determine if this model handler handles the aircraft with the given
1793 name."""
1794 return aircraft.type == const.AIRCRAFT_F70 and \
1795 (airPath.endswith("fokker70_2k4_v4.1.air") or
1796 airPath.endswith("fokker70_2k4_v4.3.air"))
1797
1798 @property
1799 def name(self):
1800 """Get the name for this aircraft model."""
1801 return "FSUIPC/Digital Aviation Fokker 70"
1802
1803 def getAircraftState(self, aircraft, timestamp, data):
1804 """Get the aircraft state.
1805
1806 Get it from the parent, and then invert the pitot heat state."""
1807 state = super(DAF70Model, self).getAircraftState(aircraft,
1808 timestamp,
1809 data)
1810 state.navLightsOn = None
1811 state.landingLightsOn = None
1812
1813 state.altimeterReliable = False
1814
1815 state.ils = state.nav1
1816 state.ils_obs = state.nav1_obs
1817 state.ils_manual = state.nav1_manual
1818
1819 state.nav1 = state.nav2
1820 state.nav1_obs = state.nav2_obs
1821 state.nav1_manual = aircraft.flight.stage!=const.STAGE_CRUISE
1822
1823 state.nav2 = None
1824 state.nav2_obs = None
1825 state.nav2_manual = False
1826
1827 state.autoXPDR = True
1828
1829 return state
1830
1831#------------------------------------------------------------------------------
1832
1833class DC3Model(GenericAircraftModel):
1834 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1835 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
1836 const.FUELTANK_RIGHT]
1837 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1838 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1839
1840 def __init__(self):
1841 """Construct the model."""
1842 super(DC3Model, self). \
1843 __init__(flapsNotches = [0, 15, 30, 45],
1844 fuelTanks = DC3Model.fuelTanks,
1845 numEngines = 2, isN1 = False)
1846 self._leftLevel = 0.0
1847 self._rightLevel = 0.0
1848
1849 @property
1850 def name(self):
1851 """Get the name for this aircraft model."""
1852 return "FSUIPC/Generic Lisunov Li-2 (DC-3)"
1853
1854 def _convertFuelData(self, data, index = 0, addCapacities = False):
1855 """Convert the given data into a fuel info list.
1856
1857 It assumes to receive the 3 fuel tanks as seen above (left,
1858 centre and right) and converts it to left aux, left, right,
1859 and right aux. The amount in the left tank goes into left aux,
1860 the amount of the right tank goes into right aux and the
1861 amount of the centre tank goes into the left and right tanks
1862 evenly distributed."""
1863 (rawFuelData, totalFuel) = \
1864 super(DC3Model, self)._convertFuelData(data, index, addCapacities)
1865
1866 centreAmount = rawFuelData[1][1]
1867 if addCapacities:
1868 centreCapacity = rawFuelData[1][2]
1869 self._leftLevel = self._rightLevel = \
1870 centreAmount / centreCapacity / 2.0
1871 fuelData = [(const.FUELTANK_LEFT_AUX,
1872 rawFuelData[0][1], rawFuelData[0][2]),
1873 (const.FUELTANK_LEFT,
1874 centreAmount/2.0, centreCapacity/2.0),
1875 (const.FUELTANK_RIGHT,
1876 centreAmount/2.0, centreCapacity/2.0),
1877 (const.FUELTANK_RIGHT_AUX,
1878 rawFuelData[2][1], rawFuelData[2][2])]
1879 else:
1880 fuelData = [(const.FUELTANK_LEFT_AUX, rawFuelData[0][1]),
1881 (const.FUELTANK_LEFT, centreAmount/2.0),
1882 (const.FUELTANK_RIGHT, centreAmount/2.0),
1883 (const.FUELTANK_RIGHT_AUX, rawFuelData[2][1])]
1884
1885 return (fuelData, totalFuel)
1886
1887 def setFuelLevel(self, handler, levels):
1888 """Set the fuel level.
1889
1890 See the description of Simulator.setFuelLevel. This
1891 implementation assumes to get the four-tank representation,
1892 as returned by getFuel()."""
1893 leftLevel = None
1894 centreLevel = None
1895 rightLevel = None
1896
1897 for (tank, level) in levels:
1898 if tank==const.FUELTANK_LEFT_AUX:
1899 leftLevel = level if leftLevel is None else (leftLevel + level)
1900 elif tank==const.FUELTANK_LEFT:
1901 level /= 2.0
1902 centreLevel = (self._rightLevel + level) \
1903 if centreLevel is None else (centreLevel + level)
1904 self._leftLevel = level
1905 elif tank==const.FUELTANK_RIGHT:
1906 level /= 2.0
1907 centreLevel = (self._leftLevel + level) \
1908 if centreLevel is None else (centreLevel + level)
1909 self._rightLevel = level
1910 elif tank==const.FUELTANK_RIGHT_AUX:
1911 rightLevel = level if rightLevel is None \
1912 else (rightLevel + level)
1913
1914 levels = []
1915 if leftLevel is not None: levels.append((const.FUELTANK_LEFT,
1916 leftLevel))
1917 if centreLevel is not None: levels.append((const.FUELTANK_CENTRE,
1918 centreLevel))
1919 if rightLevel is not None: levels.append((const.FUELTANK_RIGHT,
1920 rightLevel))
1921
1922 super(DC3Model, self).setFuelLevel(handler, levels)
1923
1924#------------------------------------------------------------------------------
1925
1926class T134Model(GenericAircraftModel):
1927 """Generic model for the Tupolev Tu-134 aircraft."""
1928 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
1929 const.FUELTANK_LEFT_AUX,
1930 const.FUELTANK_CENTRE,
1931 const.FUELTANK_RIGHT_AUX,
1932 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
1933
1934 def __init__(self):
1935 """Construct the model."""
1936 super(T134Model, self). \
1937 __init__(flapsNotches = [0, 10, 20, 30],
1938 fuelTanks = T134Model.fuelTanks,
1939 numEngines = 2)
1940
1941 @property
1942 def name(self):
1943 """Get the name for this aircraft model."""
1944 return "FSUIPC/Generic Tupolev Tu-134"
1945
1946#------------------------------------------------------------------------------
1947
1948class T154Model(GenericAircraftModel):
1949 """Generic model for the Tupolev Tu-134 aircraft."""
1950 fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1951 const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
1952 const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1953
1954 def __init__(self):
1955 """Construct the model."""
1956 super(T154Model, self). \
1957 __init__(flapsNotches = [0, 15, 28, 45],
1958 fuelTanks = T154Model.fuelTanks,
1959 numEngines = 3)
1960
1961 @property
1962 def name(self):
1963 """Get the name for this aircraft model."""
1964 return "FSUIPC/Generic Tupolev Tu-154"
1965
1966 def getAircraftState(self, aircraft, timestamp, data):
1967 """Get an aircraft state object for the given monitoring data.
1968
1969 This removes the reverser value for the middle engine."""
1970 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1971 del state.reverser[1]
1972 return state
1973
1974#------------------------------------------------------------------------------
1975
1976class PTT154Model(T154Model):
1977 """Project Tupolev Tu-154."""
1978 @staticmethod
1979 def doesHandle(aircraft, (name, airPath)):
1980 """Determine if this model handler handles the aircraft with the given
1981 name."""
1982 print "PTT154Model.doesHandle", aircraft.type, name, airPath
1983 return aircraft.type==const.AIRCRAFT_T154 and \
1984 name.find("Tu-154")!=-1 and \
1985 os.path.basename(airPath).startswith("154b_")
1986
1987 def __init__(self):
1988 """Construct the model."""
1989 super(PTT154Model, self).__init__()
1990 self._adf1 = None
1991 self._adf2 = None
1992 self._lastValue = None
1993
1994 @property
1995 def name(self):
1996 """Get the name for this aircraft model."""
1997 return "FSUIPC/Project Tupolev Tu-154"
1998
1999 def getAircraftState(self, aircraft, timestamp, data):
2000 """Get an aircraft state object for the given monitoring data.
2001
2002 This removes the reverser value for the middle engine."""
2003 state = super(PTT154Model, self).getAircraftState(aircraft, timestamp, data)
2004
2005 adf1 = state.adf1
2006 if self._adf1 is None:
2007 self._adf1 = self._adf2 = adf1
2008 elif adf1 != self._lastValue and adf1 != self._adf1 and \
2009 adf1 != self._adf2:
2010 if self._lastValue==self._adf2:
2011 self._adf1 = adf1
2012 else:
2013 self._adf2 = adf1
2014
2015 self._lastValue = adf1
2016 state.adf1 = self._adf1
2017 state.adf2 = self._adf2
2018
2019 return state
2020
2021
2022#------------------------------------------------------------------------------
2023
2024class YK40Model(GenericAircraftModel):
2025 """Generic model for the Yakovlev Yak-40 aircraft."""
2026 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
2027
2028 def __init__(self):
2029 """Construct the model."""
2030 super(YK40Model, self). \
2031 __init__(flapsNotches = [0, 20, 35],
2032 fuelTanks = YK40Model.fuelTanks,
2033 numEngines = 2)
2034
2035 @property
2036 def name(self):
2037 """Get the name for this aircraft model."""
2038 return "FSUIPC/Generic Yakovlev Yak-40"
2039
2040#------------------------------------------------------------------------------
2041
2042_genericModels = { const.AIRCRAFT_B736 : B737Model,
2043 const.AIRCRAFT_B737 : B737Model,
2044 const.AIRCRAFT_B738 : B737Model,
2045 const.AIRCRAFT_B738C : B737Model,
2046 const.AIRCRAFT_B733 : B737Model,
2047 const.AIRCRAFT_B734 : B737Model,
2048 const.AIRCRAFT_B735 : B737Model,
2049 const.AIRCRAFT_DH8D : DH8DModel,
2050 const.AIRCRAFT_B762 : B767Model,
2051 const.AIRCRAFT_B763 : B767Model,
2052 const.AIRCRAFT_CRJ2 : CRJ2Model,
2053 const.AIRCRAFT_F70 : F70Model,
2054 const.AIRCRAFT_DC3 : DC3Model,
2055 const.AIRCRAFT_T134 : T134Model,
2056 const.AIRCRAFT_T154 : T154Model,
2057 const.AIRCRAFT_YK40 : YK40Model }
2058
2059#------------------------------------------------------------------------------
2060
2061AircraftModel.registerSpecial(PMDGBoeing737NGModel)
2062AircraftModel.registerSpecial(DreamwingsDH8DModel)
2063AircraftModel.registerSpecial(DAF70Model)
2064AircraftModel.registerSpecial(PTT154Model)
2065
2066#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.