source: src/mlx/fsuipc.py@ 141:3172532920ad

Last change on this file since 141:3172532920ad was 141:3172532920ad, checked in by István Váradi <ivaradi@…>, 12 years ago

Fuel loading works

File size: 53.8 KB
Line 
1# Module handling the connection to FSUIPC
2
3#------------------------------------------------------------------------------
4
5import fs
6import const
7import util
8import acft
9
10import threading
11import os
12import time
13import calendar
14import sys
15import codecs
16
17if os.name == "nt":
18 import pyuipc
19else:
20 import pyuipc_sim as pyuipc
21
22#------------------------------------------------------------------------------
23
24# The mapping of tank types to FSUIPC offsets
25_tank2offset = { const.FUELTANK_CENTRE : 0x0b74,
26 const.FUELTANK_LEFT : 0x0b7c,
27 const.FUELTANK_RIGHT : 0x0b94,
28 const.FUELTANK_LEFT_AUX : 0x0b84,
29 const.FUELTANK_RIGHT_AUX : 0x0b9c,
30 const.FUELTANK_LEFT_TIP : 0x0b8c,
31 const.FUELTANK_RIGHT_TIP : 0x0ba4,
32 const.FUELTANK_EXTERNAL1 : 0x1254,
33 const.FUELTANK_EXTERNAL2 : 0x125c,
34 const.FUELTANK_CENTRE2 : 0x1244 }
35
36#------------------------------------------------------------------------------
37
38class Handler(threading.Thread):
39 """The thread to handle the FSUIPC requests."""
40 @staticmethod
41 def fsuipc2VS(data):
42 """Convert the given vertical speed data read from FSUIPC into feet/min."""
43 return data*60.0/const.FEETTOMETRES/256.0
44
45 @staticmethod
46 def fsuipc2radioAltitude(data):
47 """Convert the given radio altitude data read from FSUIPC into feet."""
48 return data/const.FEETTOMETRES/65536.0
49
50 @staticmethod
51 def fsuipc2Degrees(data):
52 """Convert the given data into degrees."""
53 return data * 360.0 / 65536.0 / 65536.0
54
55 @staticmethod
56 def fsuipc2PositiveDegrees(data):
57 """Convert the given data into positive degrees."""
58 degrees = Handler.fsuipc2Degrees(data)
59 if degrees<0.0: degrees += 360.0
60 return degrees
61
62 @staticmethod
63 def fsuipc2IAS(data):
64 """Convert the given data into indicated airspeed."""
65 return data / 128.0
66
67 @staticmethod
68 def _callSafe(fun):
69 """Call the given function and swallow any exceptions."""
70 try:
71 return fun()
72 except Exception, e:
73 print >> sys.stderr, str(e)
74 return None
75
76 # The number of times a read is attempted
77 NUM_READATTEMPTS = 3
78
79 # The number of connection attempts
80 NUM_CONNECTATTEMPTS = 3
81
82 # The interval between successive connect attempts
83 CONNECT_INTERVAL = 0.25
84
85 @staticmethod
86 def _performRead(data, callback, extra, validator):
87 """Perform a read request.
88
89 If there is a validator, that will be called with the return values,
90 and if the values are wrong, the request is retried at most a certain
91 number of times.
92
93 Return True if the request has succeeded, False if validation has
94 failed during all attempts. An exception may also be thrown if there is
95 some lower-level communication problem."""
96 attemptsLeft = Handler.NUM_READATTEMPTS
97 while attemptsLeft>0:
98 values = pyuipc.read(data)
99 if validator is None or \
100 Handler._callSafe(lambda: validator(values, extra)):
101 Handler._callSafe(lambda: callback(values, extra))
102 return True
103 else:
104 attemptsLeft -= 1
105 return False
106
107 class Request(object):
108 """A simple, one-shot request."""
109 def __init__(self, forWrite, data, callback, extra, validator = None):
110 """Construct the request."""
111 self._forWrite = forWrite
112 self._data = data
113 self._callback = callback
114 self._extra = extra
115 self._validator = validator
116
117 def process(self, time):
118 """Process the request.
119
120 Return True if the request has succeeded, False if data validation
121 has failed for a reading request. An exception may also be thrown
122 if there is some lower-level communication problem."""
123 if self._forWrite:
124 pyuipc.write(self._data)
125 Handler._callSafe(lambda: self._callback(True, self._extra))
126 return True
127 else:
128 return Handler._performRead(self._data, self._callback,
129 self._extra, self._validator)
130
131 def fail(self):
132 """Handle the failure of this request."""
133 if self._forWrite:
134 Handler._callSafe(lambda: self._callback(False, self._extra))
135 else:
136 Handler._callSafe(lambda: self._callback(None, self._extra))
137
138 class PeriodicRequest(object):
139 """A periodic request."""
140 def __init__(self, id, period, data, callback, extra, validator):
141 """Construct the periodic request."""
142 self._id = id
143 self._period = period
144 self._nextFire = time.time() + period
145 self._data = data
146 self._preparedData = None
147 self._callback = callback
148 self._extra = extra
149 self._validator = validator
150
151 @property
152 def id(self):
153 """Get the ID of this periodic request."""
154 return self._id
155
156 @property
157 def nextFire(self):
158 """Get the next firing time."""
159 return self._nextFire
160
161 def process(self, time):
162 """Check if this request should be executed, and if so, do so.
163
164 time is the time at which the request is being executed. If this
165 function is called too early, nothing is done, and True is
166 returned.
167
168 Return True if the request has succeeded, False if data validation
169 has failed. An exception may also be thrown if there is some
170 lower-level communication problem."""
171 if time<self._nextFire:
172 return True
173
174 if self._preparedData is None:
175 self._preparedData = pyuipc.prepare_data(self._data)
176 self._data = None
177
178 isOK = Handler._performRead(self._preparedData, self._callback,
179 self._extra, self._validator)
180
181 if isOK:
182 while self._nextFire <= time:
183 self._nextFire += self._period
184
185 return isOK
186
187 def fail(self):
188 """Handle the failure of this request."""
189 pass
190
191 def __cmp__(self, other):
192 """Compare two periodic requests. They are ordered by their next
193 firing times."""
194 return cmp(self._nextFire, other._nextFire)
195
196 def __init__(self, connectionListener,
197 connectAttempts = -1, connectInterval = 0.2):
198 """Construct the handler with the given connection listener."""
199 threading.Thread.__init__(self)
200
201 self._connectionListener = connectionListener
202 self._connectAttempts = connectAttempts
203 self._connectInterval = connectInterval
204
205 self._requestCondition = threading.Condition()
206 self._connectionRequested = False
207 self._connected = False
208
209 self._requests = []
210 self._nextPeriodicID = 1
211 self._periodicRequests = []
212
213 self.daemon = True
214
215 def requestRead(self, data, callback, extra = None, validator = None):
216 """Request the reading of some data.
217
218 data is a list of tuples of the following items:
219 - the offset of the data as an integer
220 - the type letter of the data as a string
221
222 callback is a function that receives two pieces of data:
223 - the values retrieved or None on error
224 - the extra parameter
225
226 It will be called in the handler's thread!
227 """
228 with self._requestCondition:
229 self._requests.append(Handler.Request(False, data, callback, extra,
230 validator))
231 self._requestCondition.notify()
232
233 def requestWrite(self, data, callback, extra = None):
234 """Request the writing of some data.
235
236 data is a list of tuples of the following items:
237 - the offset of the data as an integer
238 - the type letter of the data as a string
239 - the data to write
240
241 callback is a function that receives two pieces of data:
242 - a boolean indicating if writing was successful
243 - the extra data
244 It will be called in the handler's thread!
245 """
246 with self._requestCondition:
247 self._requests.append(Handler.Request(True, data, callback, extra))
248 self._requestCondition.notify()
249
250 @staticmethod
251 def _readWriteCallback(data, extra):
252 """Callback for the read() and write() calls below."""
253 extra.append(data)
254 with extra[0] as condition:
255 condition.notify()
256
257 def requestPeriodicRead(self, period, data, callback, extra = None,
258 validator = None):
259 """Request a periodic read of data.
260
261 period is a floating point number with the period in seconds.
262
263 This function returns an identifier which can be used to cancel the
264 request."""
265 with self._requestCondition:
266 id = self._nextPeriodicID
267 self._nextPeriodicID += 1
268 request = Handler.PeriodicRequest(id, period, data, callback,
269 extra, validator)
270 self._periodicRequests.append(request)
271 self._requestCondition.notify()
272 return id
273
274 def clearPeriodic(self, id):
275 """Clear the periodic request with the given ID."""
276 with self._requestCondition:
277 for i in range(0, len(self._periodicRequests)):
278 if self._periodicRequests[i].id==id:
279 del self._periodicRequests[i]
280 return True
281 return False
282
283 def connect(self):
284 """Initiate the connection to the flight simulator."""
285 with self._requestCondition:
286 if not self._connectionRequested:
287 self._connectionRequested = True
288 self._requestCondition.notify()
289
290 def disconnect(self):
291 """Disconnect from the flight simulator."""
292 with self._requestCondition:
293 self._requests = []
294 if self._connectionRequested:
295 self._connectionRequested = False
296 self._requestCondition.notify()
297
298 def clearRequests(self):
299 """Clear the outstanding one-shot requests."""
300 with self._requestCondition:
301 self._requests = []
302
303 def run(self):
304 """Perform the operation of the thread."""
305 while True:
306 self._waitConnectionRequest()
307
308 if self._connect():
309 self._handleConnection()
310
311 self._disconnect()
312
313 def _waitConnectionRequest(self):
314 """Wait for a connection request to arrive."""
315 with self._requestCondition:
316 while not self._connectionRequested:
317 self._requestCondition.wait()
318
319 def _connect(self, autoReconnection = False):
320 """Try to connect to the flight simulator via FSUIPC
321
322 Returns True if the connection has been established, False if it was
323 not due to no longer requested.
324 """
325 attempts = 0
326 while self._connectionRequested:
327 try:
328 attempts += 1
329 pyuipc.open(pyuipc.SIM_ANY)
330 description = "(FSUIPC version: 0x%04x, library version: 0x%04x, FS version: %d)" % \
331 (pyuipc.fsuipc_version, pyuipc.lib_version,
332 pyuipc.fs_version)
333 if not autoReconnection:
334 Handler._callSafe(lambda:
335 self._connectionListener.connected(const.SIM_MSFS9,
336 description))
337 self._connected = True
338 return True
339 except Exception, e:
340 print "fsuipc.Handler._connect: connection failed: " + str(e)
341 if attempts<self.NUM_CONNECTATTEMPTS:
342 time.sleep(self.CONNECT_INTERVAL)
343 else:
344 self._connectionRequested = False
345 if autoReconnection:
346 Handler._callSafe(lambda:
347 self._connectionListener.disconnected())
348 else:
349 Handler._callSafe(lambda:
350 self._connectionListener.connectionFailed())
351
352 return False
353
354 def _handleConnection(self):
355 """Handle a living connection."""
356 with self._requestCondition:
357 while self._connectionRequested:
358 self._processRequests()
359 self._waitRequest()
360
361 def _waitRequest(self):
362 """Wait for the time of the next request.
363
364 Returns also, if the connection is no longer requested.
365
366 Should be called with the request condition lock held."""
367 while self._connectionRequested:
368 timeout = None
369 if self._periodicRequests:
370 self._periodicRequests.sort()
371 timeout = self._periodicRequests[0].nextFire - time.time()
372
373 if timeout is not None and timeout <= 0.0:
374 return
375
376 self._requestCondition.wait(timeout)
377
378 def _disconnect(self):
379 """Disconnect from the flight simulator."""
380 if self._connected:
381 pyuipc.close()
382 self._connected = False
383
384 def _processRequest(self, request, time):
385 """Process the given request.
386
387 If an exception occurs or invalid data is read too many times, we try
388 to reconnect.
389
390 This function returns only if the request has succeeded, or if a
391 connection is no longer requested.
392
393 This function is called with the request lock held, but is relased
394 whole processing the request and reconnecting."""
395 self._requestCondition.release()
396
397 needReconnect = False
398 try:
399 try:
400 if not request.process(time):
401 print "fsuipc.Handler._processRequest: FSUIPC returned invalid data too many times, reconnecting"
402 needReconnect = True
403 except Exception as e:
404 print "fsuipc.Handler._processRequest: FSUIPC connection failed (" + \
405 str(e) + "), reconnecting."
406 needReconnect = True
407
408 if needReconnect:
409 with self._requestCondition:
410 self._requests.insert(0, request)
411 self._disconnect()
412 self._connect(autoReconnection = True)
413 finally:
414 self._requestCondition.acquire()
415
416 def _processRequests(self):
417 """Process any pending requests.
418
419 Will be called with the request lock held."""
420 while self._connectionRequested and self._periodicRequests:
421 self._periodicRequests.sort()
422 request = self._periodicRequests[0]
423
424 t = time.time()
425
426 if request.nextFire>t:
427 break
428
429 self._processRequest(request, t)
430
431 while self._connectionRequested and self._requests:
432 request = self._requests[0]
433 del self._requests[0]
434
435 self._processRequest(request, None)
436
437 return self._connectionRequested
438
439#------------------------------------------------------------------------------
440
441class Simulator(object):
442 """The simulator class representing the interface to the flight simulator
443 via FSUIPC."""
444 # The basic data that should be queried all the time once we are connected
445 timeData = [ (0x0240, "H"), # Year
446 (0x023e, "H"), # Number of day in year
447 (0x023b, "b"), # UTC hour
448 (0x023c, "b"), # UTC minute
449 (0x023a, "b") ] # seconds
450
451 normalData = timeData + \
452 [ (0x3d00, -256), # The name of the current aircraft
453 (0x3c00, -256), # The path of the current AIR file
454 (0x1274, "h") ] # Text display mode
455
456 flareData1 = [ (0x023a, "b"), # Seconds of time
457 (0x31e4, "d"), # Radio altitude
458 (0x02c8, "d") ] # Vertical speed
459
460 flareStartData = [ (0x0e90, "H"), # Ambient wind speed
461 (0x0e92, "H"), # Ambient wind direction
462 (0x0e8a, "H") ] # Visibility
463
464 flareData2 = [ (0x023a, "b"), # Seconds of time
465 (0x0366, "H"), # On the ground
466 (0x02c8, "d"), # Vertical speed
467 (0x030c, "d"), # Touch-down rate
468 (0x02bc, "d"), # IAS
469 (0x0578, "d"), # Pitch
470 (0x057c, "d"), # Bank
471 (0x0580, "d") ] # Heading
472
473 @staticmethod
474 def _getTimestamp(data):
475 """Convert the given data into a timestamp."""
476 timestamp = calendar.timegm(time.struct_time([data[0],
477 1, 1, 0, 0, 0, -1, 1, 0]))
478 timestamp += data[1] * 24 * 3600
479 timestamp += data[2] * 3600
480 timestamp += data[3] * 60
481 timestamp += data[4]
482
483 return timestamp
484
485 def __init__(self, connectionListener, connectAttempts = -1,
486 connectInterval = 0.2):
487 """Construct the simulator.
488
489 The aircraft object passed must provide the following members:
490 - type: one of the AIRCRAFT_XXX constants from const.py
491 - modelChanged(aircraftName, modelName): called when the model handling
492 the aircraft has changed.
493 - handleState(aircraftState): handle the given state.
494 - flareStarted(windSpeed, windDirection, visibility, flareStart,
495 flareStartFS): called when the flare has
496 started. windSpeed is in knots, windDirection is in degrees and
497 visibility is in metres. flareStart and flareStartFS are two time
498 values expressed in seconds that can be used to calculate the flare
499 time.
500 - flareFinished(flareEnd, flareEndFS, tdRate, tdRateCalculatedByFS,
501 ias, pitch, bank, heading): called when the flare has
502 finished, i.e. the aircraft is on the ground. flareEnd and flareEndFS
503 are the two time values corresponding to the touchdown time. tdRate is
504 the touch-down rate, tdRateCalculatedBySim indicates if the data comes
505 from the simulator or was calculated by the adapter. The other data
506 are self-explanatory and expressed in their 'natural' units."""
507 self._aircraft = None
508
509 self._handler = Handler(connectionListener,
510 connectAttempts = connectAttempts,
511 connectInterval = connectInterval)
512 self._handler.start()
513
514 self._scroll = False
515
516 self._normalRequestID = None
517
518 self._monitoringRequested = False
519 self._monitoring = False
520
521 self._aircraftName = None
522 self._aircraftModel = None
523
524 self._flareRequestID = None
525 self._flareRates = []
526 self._flareStart = None
527 self._flareStartFS = None
528
529 self._latin1decoder = codecs.getdecoder("iso-8859-1")
530
531 def connect(self, aircraft):
532 """Initiate a connection to the simulator."""
533 self._aircraft = aircraft
534 self._aircraftName = None
535 self._aircraftModel = None
536 self._handler.connect()
537 if self._normalRequestID is None:
538 self._startDefaultNormal()
539
540 def reconnect(self):
541 """Initiate a reconnection to the simulator.
542
543 It does not reset already set up data, just calls connect() on the
544 handler."""
545 self._handler.connect()
546
547 def requestZFW(self, callback):
548 """Send a request for the ZFW."""
549 self._handler.requestRead([(0x3bfc, "d")], self._handleZFW, extra = callback)
550
551 def requestWeights(self, callback):
552 """Request the following weights: DOW, ZFW, payload.
553
554 These values will be passed to the callback function in this order, as
555 separate arguments."""
556 self._handler.requestRead([(0x13fc, "d")], self._handlePayloadCount,
557 extra = callback)
558
559 def requestTime(self, callback):
560 """Request the time from the simulator."""
561 self._handler.requestRead(Simulator.timeData, self._handleTime,
562 extra = callback)
563
564 def startMonitoring(self):
565 """Start the periodic monitoring of the aircraft and pass the resulting
566 state to the aircraft object periodically."""
567 assert not self._monitoringRequested
568 self._monitoringRequested = True
569
570 def stopMonitoring(self):
571 """Stop the periodic monitoring of the aircraft."""
572 assert self._monitoringRequested
573 self._monitoringRequested = False
574
575 def startFlare(self):
576 """Start monitoring the flare time.
577
578 At present it is assumed to be called from the FSUIPC thread, hence no
579 protection."""
580 #self._aircraft.logger.debug("startFlare")
581 if self._flareRequestID is None:
582 self._flareRates = []
583 self._flareRequestID = self._handler.requestPeriodicRead(0.1,
584 Simulator.flareData1,
585 self._handleFlare1)
586
587 def cancelFlare(self):
588 """Cancel monitoring the flare time.
589
590 At present it is assumed to be called from the FSUIPC thread, hence no
591 protection."""
592 if self._flareRequestID is not None:
593 self._handler.clearPeriodic(self._flareRequestID)
594 self._flareRequestID = None
595
596 def sendMessage(self, message, duration = 3):
597 """Send a message to the pilot via the simulator.
598
599 duration is the number of seconds to keep the message displayed."""
600
601 if self._scroll:
602 if duration==0: duration = -1
603 elif duration == 1: duration = -2
604 else: duration = -duration
605
606 data = [(0x3380, -1 - len(message), message),
607 (0x32fa, 'h', duration)]
608
609 self._handler.requestWrite(data, self._handleMessageSent)
610
611 def getFuel(self, tanks, callback):
612 """Get the fuel from the given tanks.
613
614 The callback will be called with a list of two-tuples, where the tuples
615 have the following items:
616 - the current weight of the fuel in the tank (in kgs)
617 - the current total capacity of the tank (in kgs)."""
618 data = [(0x0af4, "H")] # Fuel weight
619 for tank in tanks:
620 offset = _tank2offset[tank]
621 data.append( (offset, "u") ) # tank level
622 data.append( (offset+4, "u") ) # tank capacity
623
624 self._handler.requestRead(data, self._handleFuelRetrieved,
625 extra = callback)
626
627 def setFuelLevel(self, levels):
628 """Set the fuel level to the given ones.
629
630 levels is an array of two-tuples, where each tuple consists of the
631 following:
632 - the const.FUELTANK_XXX constant denoting the tank that must be set,
633 - the requested level of the fuel as a floating-point value between 0.0
634 and 1.0."""
635 data = []
636 for (tank, level) in levels:
637 offset = _tank2offset[tank]
638 data.append( (offset, "u", long(level * 128.8 * 65536.0)) )
639 self._handler.requestWrite(data, self._handleFuelWritten)
640
641 def disconnect(self):
642 """Disconnect from the simulator."""
643 assert not self._monitoringRequested
644
645 self._stopNormal()
646 self._handler.disconnect()
647
648 def _startDefaultNormal(self):
649 """Start the default normal periodic request."""
650 assert self._normalRequestID is None
651 self._normalRequestID = \
652 self._handler.requestPeriodicRead(1.0,
653 Simulator.normalData,
654 self._handleNormal,
655 validator = self._validateNormal)
656
657 def _stopNormal(self):
658 """Stop the normal period request."""
659 assert self._normalRequestID is not None
660 self._handler.clearPeriodic(self._normalRequestID)
661 self._normalRequestID = None
662 self._monitoring = False
663
664 def _validateNormal(self, data, extra):
665 """Validate the normal data."""
666 return data[0]!=0 and data[1]!=0 and len(data[5])>0 and len(data[6])>0
667
668 def _handleNormal(self, data, extra):
669 """Handle the reply to the normal request.
670
671 At the beginning the result consists the data for normalData. When
672 monitoring is started, it contains the result also for the
673 aircraft-specific values.
674 """
675 timestamp = Simulator._getTimestamp(data)
676
677 createdNewModel = self._setAircraftName(timestamp, data[5], data[6])
678
679 self._scroll = data[7]!=0
680
681 if self._monitoringRequested and not self._monitoring:
682 self._stopNormal()
683 self._startMonitoring()
684 elif self._monitoring and not self._monitoringRequested:
685 self._stopNormal()
686 self._startDefaultNormal()
687 elif self._monitoring and self._aircraftModel is not None and \
688 not createdNewModel:
689 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
690 timestamp, data)
691 self._aircraft.handleState(aircraftState)
692
693 def _setAircraftName(self, timestamp, name, airPath):
694 """Set the name of the aicraft and if it is different from the
695 previous, create a new model for it.
696
697 If so, also notifty the aircraft about the change.
698
699 Return if a new model was created."""
700 aircraftName = (name, airPath)
701 if aircraftName==self._aircraftName:
702 return False
703
704 self._aircraftName = aircraftName
705 needNew = self._aircraftModel is None
706 needNew = needNew or\
707 not self._aircraftModel.doesHandle(self._aircraft, aircraftName)
708 if not needNew:
709 specialModel = AircraftModel.findSpecial(self._aircraft, aircraftName)
710 needNew = specialModel is not None and \
711 specialModel is not self._aircraftModel.__class__
712
713 if needNew:
714 self._setAircraftModel(AircraftModel.create(self._aircraft, aircraftName))
715
716
717 self._aircraft.modelChanged(timestamp, self._latin1decoder(name)[0],
718 self._aircraftModel.name)
719
720 return needNew
721
722 def _setAircraftModel(self, model):
723 """Set a new aircraft model.
724
725 It will be queried for the data to monitor and the monitoring request
726 will be replaced by a new one."""
727 self._aircraftModel = model
728
729 if self._monitoring:
730 self._stopNormal()
731 self._startMonitoring()
732
733 def _startMonitoring(self):
734 """Start monitoring with the current aircraft model."""
735 data = Simulator.normalData[:]
736 self._aircraftModel.addMonitoringData(data)
737
738 self._normalRequestID = \
739 self._handler.requestPeriodicRead(1.0, data,
740 self._handleNormal,
741 validator = self._validateNormal)
742 self._monitoring = True
743
744 def _addFlareRate(self, data):
745 """Append a flare rate to the list of last rates."""
746 if len(self._flareRates)>=3:
747 del self._flareRates[0]
748 self._flareRates.append(Handler.fsuipc2VS(data))
749
750 def _handleFlare1(self, data, normal):
751 """Handle the first stage of flare monitoring."""
752 #self._aircraft.logger.debug("handleFlare1: " + str(data))
753 if Handler.fsuipc2radioAltitude(data[1])<=50.0:
754 self._flareStart = time.time()
755 self._flareStartFS = data[0]
756 self._handler.clearPeriodic(self._flareRequestID)
757 self._flareRequestID = \
758 self._handler.requestPeriodicRead(0.1,
759 Simulator.flareData2,
760 self._handleFlare2)
761 self._handler.requestRead(Simulator.flareStartData,
762 self._handleFlareStart)
763
764 self._addFlareRate(data[2])
765
766 def _handleFlareStart(self, data, extra):
767 """Handle the data need to notify the aircraft about the starting of
768 the flare."""
769 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
770 if data is not None:
771 windDirection = data[1]*360.0/65536.0
772 if windDirection<0.0: windDirection += 360.0
773 self._aircraft.flareStarted(data[0], windDirection,
774 data[2]*1609.344/100.0,
775 self._flareStart, self._flareStartFS)
776
777 def _handleFlare2(self, data, normal):
778 """Handle the first stage of flare monitoring."""
779 #self._aircraft.logger.debug("handleFlare2: " + str(data))
780 if data[1]!=0:
781 flareEnd = time.time()
782 self._handler.clearPeriodic(self._flareRequestID)
783 self._flareRequestID = None
784
785 flareEndFS = data[0]
786 if flareEndFS<self._flareStartFS:
787 flareEndFS += 60
788
789 tdRate = Handler.fsuipc2VS(data[3])
790 tdRateCalculatedByFS = True
791 if tdRate==0 or tdRate>1000.0 or tdRate<-1000.0:
792 tdRate = min(self._flareRates)
793 tdRateCalculatedByFS = False
794
795 self._aircraft.flareFinished(flareEnd, flareEndFS,
796 tdRate, tdRateCalculatedByFS,
797 Handler.fsuipc2IAS(data[4]),
798 Handler.fsuipc2Degrees(data[5]),
799 Handler.fsuipc2Degrees(data[6]),
800 Handler.fsuipc2PositiveDegrees(data[7]))
801 else:
802 self._addFlareRate(data[2])
803
804 def _handleZFW(self, data, callback):
805 """Callback for a ZFW retrieval request."""
806 zfw = data[0] * const.LBSTOKG / 256.0
807 callback(zfw)
808
809 def _handleTime(self, data, callback):
810 """Callback for a time retrieval request."""
811 callback(Simulator._getTimestamp(data))
812
813 def _handlePayloadCount(self, data, callback):
814 """Callback for the payload count retrieval request."""
815 payloadCount = data[0]
816 data = [(0x3bfc, "d"), (0x30c0, "f")]
817 for i in range(0, payloadCount):
818 data.append((0x1400 + i*48, "f"))
819
820 self._handler.requestRead(data, self._handleWeights,
821 extra = callback)
822
823 def _handleWeights(self, data, callback):
824 """Callback for the weights retrieval request."""
825 zfw = data[0] * const.LBSTOKG / 256.0
826 grossWeight = data[1] * const.LBSTOKG
827 payload = sum(data[2:]) * const.LBSTOKG
828 dow = zfw - payload
829 callback(dow, payload, zfw, grossWeight)
830
831 def _handleMessageSent(self, success, extra):
832 """Callback for a message sending request."""
833 pass
834
835 def _handleFuelRetrieved(self, data, callback):
836 """Callback for a fuel retrieval request."""
837 fuelWeight = data[0] / 256.0
838 result = []
839 for i in range(1, len(data), 2):
840 capacity = data[i+1] * fuelWeight * const.LBSTOKG
841 amount = data[i] * capacity / 128.0 / 65536.0
842 result.append( (amount, capacity) )
843
844 callback(result)
845
846 def _handleFuelWritten(self, success, extra):
847 """Callback for a fuel setting request."""
848 pass
849
850#------------------------------------------------------------------------------
851
852class AircraftModel(object):
853 """Base class for the aircraft models.
854
855 Aircraft models handle the data arriving from FSUIPC and turn it into an
856 object describing the aircraft's state."""
857 monitoringData = [("paused", 0x0264, "H"),
858 ("latitude", 0x0560, "l"),
859 ("longitude", 0x0568, "l"),
860 ("frozen", 0x3364, "H"),
861 ("replay", 0x0628, "d"),
862 ("slew", 0x05dc, "H"),
863 ("overspeed", 0x036d, "b"),
864 ("stalled", 0x036c, "b"),
865 ("onTheGround", 0x0366, "H"),
866 ("zfw", 0x3bfc, "d"),
867 ("grossWeight", 0x30c0, "f"),
868 ("heading", 0x0580, "d"),
869 ("pitch", 0x0578, "d"),
870 ("bank", 0x057c, "d"),
871 ("ias", 0x02bc, "d"),
872 ("mach", 0x11c6, "H"),
873 ("groundSpeed", 0x02b4, "d"),
874 ("vs", 0x02c8, "d"),
875 ("radioAltitude", 0x31e4, "d"),
876 ("altitude", 0x0570, "l"),
877 ("gLoad", 0x11ba, "H"),
878 ("flapsControl", 0x0bdc, "d"),
879 ("flapsLeft", 0x0be0, "d"),
880 ("flapsRight", 0x0be4, "d"),
881 ("lights", 0x0d0c, "H"),
882 ("pitot", 0x029c, "b"),
883 ("parking", 0x0bc8, "H"),
884 ("noseGear", 0x0bec, "d"),
885 ("spoilersArmed", 0x0bcc, "d"),
886 ("spoilers", 0x0bd0, "d"),
887 ("altimeter", 0x0330, "H"),
888 ("nav1", 0x0350, "H"),
889 ("nav2", 0x0352, "H"),
890 ("squawk", 0x0354, "H"),
891 ("windSpeed", 0x0e90, "H"),
892 ("windDirection", 0x0e92, "H"),
893 ("visibility", 0x0e8a, "H")]
894
895
896 specialModels = []
897
898 @staticmethod
899 def registerSpecial(clazz):
900 """Register the given class as a special model."""
901 AircraftModel.specialModels.append(clazz)
902
903 @staticmethod
904 def findSpecial(aircraft, aircraftName):
905 for specialModel in AircraftModel.specialModels:
906 if specialModel.doesHandle(aircraft, aircraftName):
907 return specialModel
908 return None
909
910 @staticmethod
911 def create(aircraft, aircraftName):
912 """Create the model for the given aircraft name, and notify the
913 aircraft about it."""
914 specialModel = AircraftModel.findSpecial(aircraft, aircraftName)
915 if specialModel is not None:
916 return specialModel()
917 if aircraft.type in _genericModels:
918 return _genericModels[aircraft.type]()
919 else:
920 return GenericModel()
921
922 @staticmethod
923 def convertBCD(data, length):
924 """Convert a data item encoded as BCD into a string of the given number
925 of digits."""
926 bcd = ""
927 for i in range(0, length):
928 digit = chr(ord('0') + (data&0x0f))
929 data >>= 4
930 bcd = digit + bcd
931 return bcd
932
933 @staticmethod
934 def convertFrequency(data):
935 """Convert the given frequency data to a string."""
936 bcd = AircraftModel.convertBCD(data, 4)
937 return "1" + bcd[0:2] + "." + bcd[2:4]
938
939 def __init__(self, flapsNotches):
940 """Construct the aircraft model.
941
942 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
943 self._flapsNotches = flapsNotches
944
945 @property
946 def name(self):
947 """Get the name for this aircraft model."""
948 return "FSUIPC/Generic"
949
950 def doesHandle(self, aircraft, aircraftName):
951 """Determine if the model handles the given aircraft name.
952
953 This default implementation returns False."""
954 return False
955
956 def _addOffsetWithIndexMember(self, dest, offset, type, attrName = None):
957 """Add the given FSUIPC offset and type to the given array and a member
958 attribute with the given name."""
959 dest.append((offset, type))
960 if attrName is not None:
961 setattr(self, attrName, len(dest)-1)
962
963 def _addDataWithIndexMembers(self, dest, prefix, data):
964 """Add FSUIPC data to the given array and also corresponding index
965 member variables with the given prefix.
966
967 data is a list of triplets of the following items:
968 - the name of the data item. The index member variable will have a name
969 created by prepending the given prefix to this name.
970 - the FSUIPC offset
971 - the FSUIPC type
972
973 The latter two items will be appended to dest."""
974 for (name, offset, type) in data:
975 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
976
977 def addMonitoringData(self, data):
978 """Add the model-specific monitoring data to the given array."""
979 self._addDataWithIndexMembers(data, "_monidx_",
980 AircraftModel.monitoringData)
981
982 def getAircraftState(self, aircraft, timestamp, data):
983 """Get an aircraft state object for the given monitoring data."""
984 state = fs.AircraftState()
985
986 state.timestamp = timestamp
987
988 state.latitude = data[self._monidx_latitude] * \
989 90.0 / 10001750.0 / 65536.0 / 65536.0
990
991 state.longitude = data[self._monidx_longitude] * \
992 360.0 / 65536.0 / 65536.0 / 65536.0 / 65536.0
993 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
994
995 state.paused = data[self._monidx_paused]!=0 or \
996 data[self._monidx_frozen]!=0 or \
997 data[self._monidx_replay]!=0
998 state.trickMode = data[self._monidx_slew]!=0
999
1000 state.overspeed = data[self._monidx_overspeed]!=0
1001 state.stalled = data[self._monidx_stalled]!=0
1002 state.onTheGround = data[self._monidx_onTheGround]!=0
1003
1004 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
1005 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
1006
1007 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
1008
1009 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
1010 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
1011
1012 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
1013 state.mach = data[self._monidx_mach] / 20480.0
1014 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
1015 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
1016
1017 state.radioAltitude = \
1018 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
1019 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
1020
1021 state.gLoad = data[self._monidx_gLoad] / 625.0
1022
1023 numNotchesM1 = len(self._flapsNotches) - 1
1024 flapsIncrement = 16383 / numNotchesM1
1025 flapsControl = data[self._monidx_flapsControl]
1026 flapsIndex = flapsControl / flapsIncrement
1027 if flapsIndex < numNotchesM1:
1028 if (flapsControl - (flapsIndex*flapsIncrement) >
1029 (flapsIndex+1)*flapsIncrement - flapsControl):
1030 flapsIndex += 1
1031 state.flapsSet = self._flapsNotches[flapsIndex]
1032
1033 flapsLeft = data[self._monidx_flapsLeft]
1034 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
1035
1036 lights = data[self._monidx_lights]
1037
1038 state.navLightsOn = (lights&0x01) != 0
1039 state.antiCollisionLightsOn = (lights&0x02) != 0
1040 state.landingLightsOn = (lights&0x04) != 0
1041 state.strobeLightsOn = (lights&0x10) != 0
1042
1043 state.pitotHeatOn = data[self._monidx_pitot]!=0
1044
1045 state.parking = data[self._monidx_parking]!=0
1046
1047 state.gearsDown = data[self._monidx_noseGear]==16383
1048
1049 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
1050
1051 spoilers = data[self._monidx_spoilers]
1052 if spoilers<=4800:
1053 state.spoilersExtension = 0.0
1054 else:
1055 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
1056
1057 state.altimeter = data[self._monidx_altimeter] / 16.0
1058
1059 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
1060 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
1061 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
1062
1063 state.windSpeed = data[self._monidx_windSpeed]
1064 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
1065 if state.windDirection<0.0: state.windDirection += 360.0
1066
1067 state.visibility = data[self._monidx_visibility]*1609.344/100.0
1068
1069 return state
1070
1071#------------------------------------------------------------------------------
1072
1073class GenericAircraftModel(AircraftModel):
1074 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1075 values and some other common parameters in a generic way."""
1076
1077 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1078 """Construct the generic aircraft model with the given data.
1079
1080 flapsNotches is an array of how much degrees the individual flaps
1081 notches mean.
1082
1083 fuelTanks is an array of const.FUELTANK_XXX constants about the
1084 aircraft's fuel tanks. They will be converted to offsets.
1085
1086 numEngines is the number of engines the aircraft has.
1087
1088 isN1 determines if the engines have an N1 value or an RPM value
1089 (e.g. pistons)."""
1090 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1091
1092 self._fuelTanks = fuelTanks
1093 self._fuelStartIndex = None
1094 self._numEngines = numEngines
1095 self._engineStartIndex = None
1096 self._isN1 = isN1
1097
1098 def doesHandle(self, aircraft, aircraftName):
1099 """Determine if the model handles the given aircraft name.
1100
1101 This implementation returns True."""
1102 return True
1103
1104 def addMonitoringData(self, data):
1105 """Add the model-specific monitoring data to the given array."""
1106 super(GenericAircraftModel, self).addMonitoringData(data)
1107
1108 self._addOffsetWithIndexMember(data, 0x0af4, "H", "_monidx_fuelWeight")
1109
1110 self._fuelStartIndex = len(data)
1111 for tank in self._fuelTanks:
1112 offset = _tank2offset[tank]
1113 self._addOffsetWithIndexMember(data, offset, "u") # tank level
1114 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
1115
1116 if self._isN1:
1117 self._engineStartIndex = len(data)
1118 for i in range(0, self._numEngines):
1119 self._addOffsetWithIndexMember(data, 0x2000 + i * 0x100, "f") # N1
1120 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
1121
1122 def getAircraftState(self, aircraft, timestamp, data):
1123 """Get the aircraft state.
1124
1125 Get it from the parent, and then add the data about the fuel levels and
1126 the engine parameters."""
1127 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1128 timestamp,
1129 data)
1130
1131 fuelWeight = data[self._monidx_fuelWeight]/256.0
1132 state.fuel = []
1133 for i in range(self._fuelStartIndex,
1134 self._fuelStartIndex + 2*len(self._fuelTanks), 2):
1135 fuel = data[i+1]*data[i]*fuelWeight*const.LBSTOKG/128.0/65536.0
1136 state.fuel.append(fuel)
1137
1138 state.n1 = []
1139 state.reverser = []
1140 for i in range(self._engineStartIndex,
1141 self._engineStartIndex + 2*self._numEngines, 2):
1142 state.n1.append(data[i])
1143 state.reverser.append(data[i+1]<0)
1144
1145 return state
1146
1147#------------------------------------------------------------------------------
1148
1149class GenericModel(GenericAircraftModel):
1150 """Generic aircraft model for an unknown type."""
1151 def __init__(self):
1152 """Construct the model."""
1153 super(GenericModel, self). \
1154 __init__(flapsNotches = [0, 10, 20, 30],
1155 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1156 numEngines = 2)
1157
1158 @property
1159 def name(self):
1160 """Get the name for this aircraft model."""
1161 return "FSUIPC/Generic"
1162
1163#------------------------------------------------------------------------------
1164
1165class B737Model(GenericAircraftModel):
1166 """Generic model for the Boeing 737 Classing and NG aircraft."""
1167 def __init__(self):
1168 """Construct the model."""
1169 super(B737Model, self). \
1170 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1171 fuelTanks = acft.Boeing737.fuelTanks,
1172 numEngines = 2)
1173
1174 @property
1175 def name(self):
1176 """Get the name for this aircraft model."""
1177 return "FSUIPC/Generic Boeing 737"
1178
1179#------------------------------------------------------------------------------
1180
1181class PMDGBoeing737NGModel(B737Model):
1182 """A model handler for the PMDG Boeing 737NG model."""
1183 @staticmethod
1184 def doesHandle(aircraft, (name, airPath)):
1185 """Determine if this model handler handles the aircraft with the given
1186 name."""
1187 return aircraft.type in [const.AIRCRAFT_B736,
1188 const.AIRCRAFT_B737,
1189 const.AIRCRAFT_B738] and \
1190 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
1191 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
1192 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
1193 name.find("700")!=-1 or airPath.find("700")!=-1 or \
1194 name.find("800")!=-1 or airPath.find("800")!=-1 or \
1195 name.find("900")!=-1 or airPath.find("900")!=-1)
1196
1197 @property
1198 def name(self):
1199 """Get the name for this aircraft model."""
1200 return "FSUIPC/PMDG Boeing 737NG"
1201
1202 def addMonitoringData(self, data):
1203 """Add the model-specific monitoring data to the given array."""
1204 super(PMDGBoeing737NGModel, self).addMonitoringData(data)
1205
1206 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
1207
1208 def getAircraftState(self, aircraft, timestamp, data):
1209 """Get the aircraft state.
1210
1211 Get it from the parent, and then check some PMDG-specific stuff."""
1212 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
1213 timestamp,
1214 data)
1215 if data[self._pmdgidx_switches]&0x01==0x01:
1216 state.altimeter = 1013.25
1217
1218 return state
1219
1220#------------------------------------------------------------------------------
1221
1222class B767Model(GenericAircraftModel):
1223 """Generic model for the Boeing 767 aircraft."""
1224 def __init__(self):
1225 """Construct the model."""
1226 super(B767Model, self). \
1227 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1228 fuelTanks = acft.Boeing767.fuelTanks,
1229 numEngines = 2)
1230
1231 @property
1232 def name(self):
1233 """Get the name for this aircraft model."""
1234 return "FSUIPC/Generic Boeing 767"
1235
1236#------------------------------------------------------------------------------
1237
1238class DH8DModel(GenericAircraftModel):
1239 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1240 def __init__(self):
1241 """Construct the model."""
1242 super(DH8DModel, self). \
1243 __init__(flapsNotches = [0, 5, 10, 15, 35],
1244 fuelTanks = acft.DH8D.fuelTanks,
1245 numEngines = 2)
1246
1247 @property
1248 def name(self):
1249 """Get the name for this aircraft model."""
1250 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1251
1252#------------------------------------------------------------------------------
1253
1254class DreamwingsDH8DModel(DH8DModel):
1255 """Model handler for the Dreamwings Dash 8-Q400."""
1256 @staticmethod
1257 def doesHandle(aircraft, (name, airPath)):
1258 """Determine if this model handler handles the aircraft with the given
1259 name."""
1260 return aircraft.type==const.AIRCRAFT_DH8D and \
1261 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1262 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1263 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1264 airPath.find("Dash8Q400")!=-1
1265
1266 @property
1267 def name(self):
1268 """Get the name for this aircraft model."""
1269 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1270
1271 def getAircraftState(self, aircraft, timestamp, data):
1272 """Get the aircraft state.
1273
1274 Get it from the parent, and then invert the pitot heat state."""
1275 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1276 timestamp,
1277 data)
1278 state.pitotHeatOn = not state.pitotHeatOn
1279
1280 return state
1281#------------------------------------------------------------------------------
1282
1283class CRJ2Model(GenericAircraftModel):
1284 """Generic model for the Bombardier CRJ-200 aircraft."""
1285 def __init__(self):
1286 """Construct the model."""
1287 super(CRJ2Model, self). \
1288 __init__(flapsNotches = [0, 8, 20, 30, 45],
1289 fuelTanks = acft.CRJ2.fuelTanks,
1290 numEngines = 2)
1291
1292 @property
1293 def name(self):
1294 """Get the name for this aircraft model."""
1295 return "FSUIPC/Generic Bombardier CRJ-200"
1296
1297#------------------------------------------------------------------------------
1298
1299class F70Model(GenericAircraftModel):
1300 """Generic model for the Fokker F70 aircraft."""
1301 def __init__(self):
1302 """Construct the model."""
1303 super(F70Model, self). \
1304 __init__(flapsNotches = [0, 8, 15, 25, 42],
1305 fuelTanks = acft.F70.fuelTanks,
1306 numEngines = 2)
1307
1308 @property
1309 def name(self):
1310 """Get the name for this aircraft model."""
1311 return "FSUIPC/Generic Fokker 70"
1312
1313#------------------------------------------------------------------------------
1314
1315class DC3Model(GenericAircraftModel):
1316 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1317 def __init__(self):
1318 """Construct the model."""
1319 super(DC3Model, self). \
1320 __init__(flapsNotches = [0, 15, 30, 45],
1321 fuelTanks = acft.DC3.fuelTanks,
1322 numEngines = 2)
1323
1324 @property
1325 def name(self):
1326 """Get the name for this aircraft model."""
1327 return "FSUIPC/Generic Lisunov Li-2"
1328
1329#------------------------------------------------------------------------------
1330
1331class T134Model(GenericAircraftModel):
1332 """Generic model for the Tupolev Tu-134 aircraft."""
1333 def __init__(self):
1334 """Construct the model."""
1335 super(T134Model, self). \
1336 __init__(flapsNotches = [0, 10, 20, 30],
1337 fuelTanks = acft.T134.fuelTanks,
1338 numEngines = 2)
1339
1340 @property
1341 def name(self):
1342 """Get the name for this aircraft model."""
1343 return "FSUIPC/Generic Tupolev Tu-134"
1344
1345#------------------------------------------------------------------------------
1346
1347class T154Model(GenericAircraftModel):
1348 """Generic model for the Tupolev Tu-134 aircraft."""
1349 def __init__(self):
1350 """Construct the model."""
1351 super(T154Model, self). \
1352 __init__(flapsNotches = [0, 15, 28, 45],
1353 fuelTanks = acft.T154.fuelTanks,
1354 numEngines = 3)
1355
1356 @property
1357 def name(self):
1358 """Get the name for this aircraft model."""
1359 return "FSUIPC/Generic Tupolev Tu-154"
1360
1361 def getAircraftState(self, aircraft, timestamp, data):
1362 """Get an aircraft state object for the given monitoring data.
1363
1364 This removes the reverser value for the middle engine."""
1365 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1366 del state.reverser[1]
1367 return state
1368
1369#------------------------------------------------------------------------------
1370
1371class YK40Model(GenericAircraftModel):
1372 """Generic model for the Yakovlev Yak-40 aircraft."""
1373 def __init__(self):
1374 """Construct the model."""
1375 super(YK40Model, self). \
1376 __init__(flapsNotches = [0, 20, 35],
1377 fuelTanks = acft.YK40.fuelTanks,
1378 numEngines = 2)
1379
1380 @property
1381 def name(self):
1382 """Get the name for this aircraft model."""
1383 return "FSUIPC/Generic Yakovlev Yak-40"
1384
1385#------------------------------------------------------------------------------
1386
1387_genericModels = { const.AIRCRAFT_B736 : B737Model,
1388 const.AIRCRAFT_B737 : B737Model,
1389 const.AIRCRAFT_B738 : B737Model,
1390 const.AIRCRAFT_B733 : B737Model,
1391 const.AIRCRAFT_B734 : B737Model,
1392 const.AIRCRAFT_B735 : B737Model,
1393 const.AIRCRAFT_DH8D : DH8DModel,
1394 const.AIRCRAFT_B762 : B767Model,
1395 const.AIRCRAFT_B763 : B767Model,
1396 const.AIRCRAFT_CRJ2 : B767Model,
1397 const.AIRCRAFT_F70 : F70Model,
1398 const.AIRCRAFT_DC3 : DC3Model,
1399 const.AIRCRAFT_T134 : T134Model,
1400 const.AIRCRAFT_T154 : T154Model,
1401 const.AIRCRAFT_YK40 : YK40Model }
1402
1403#------------------------------------------------------------------------------
1404
1405AircraftModel.registerSpecial(PMDGBoeing737NGModel)
1406AircraftModel.registerSpecial(DreamwingsDH8DModel)
1407
1408#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.