source: src/mlx/fsuipc.py@ 339:ede634649e12

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

Added the special PMDG offsets for the transponder and AP data

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