source: src/mlx/fsuipc.py@ 150:0a6ffbd72d7e

Last change on this file since 150:0a6ffbd72d7e was 148:453ebaea9090, checked in by István Váradi <ivaradi@…>, 13 years ago

Implemented FS time synchronization

File size: 55.1 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 TIME_SYNC_INTERVAL = 3.0
474
475 @staticmethod
476 def _getTimestamp(data):
477 """Convert the given data into a timestamp."""
478 timestamp = calendar.timegm(time.struct_time([data[0],
479 1, 1, 0, 0, 0, -1, 1, 0]))
480 timestamp += data[1] * 24 * 3600
481 timestamp += data[2] * 3600
482 timestamp += data[3] * 60
483 timestamp += data[4]
484
485 return timestamp
486
487 def __init__(self, connectionListener, connectAttempts = -1,
488 connectInterval = 0.2):
489 """Construct the simulator.
490
491 The aircraft object passed must provide the following members:
492 - type: one of the AIRCRAFT_XXX constants from const.py
493 - modelChanged(aircraftName, modelName): called when the model handling
494 the aircraft has changed.
495 - handleState(aircraftState): handle the given state.
496 - flareStarted(windSpeed, windDirection, visibility, flareStart,
497 flareStartFS): called when the flare has
498 started. windSpeed is in knots, windDirection is in degrees and
499 visibility is in metres. flareStart and flareStartFS are two time
500 values expressed in seconds that can be used to calculate the flare
501 time.
502 - flareFinished(flareEnd, flareEndFS, tdRate, tdRateCalculatedByFS,
503 ias, pitch, bank, heading): called when the flare has
504 finished, i.e. the aircraft is on the ground. flareEnd and flareEndFS
505 are the two time values corresponding to the touchdown time. tdRate is
506 the touch-down rate, tdRateCalculatedBySim indicates if the data comes
507 from the simulator or was calculated by the adapter. The other data
508 are self-explanatory and expressed in their 'natural' units."""
509 self._aircraft = None
510
511 self._handler = Handler(connectionListener,
512 connectAttempts = connectAttempts,
513 connectInterval = connectInterval)
514 self._handler.start()
515
516 self._scroll = False
517
518 self._syncTime = False
519 self._nextSyncTime = -1
520
521 self._normalRequestID = None
522
523 self._monitoringRequested = False
524 self._monitoring = False
525
526 self._aircraftName = None
527 self._aircraftModel = None
528
529 self._flareRequestID = None
530 self._flareRates = []
531 self._flareStart = None
532 self._flareStartFS = None
533
534 self._latin1decoder = codecs.getdecoder("iso-8859-1")
535
536 def connect(self, aircraft):
537 """Initiate a connection to the simulator."""
538 self._aircraft = aircraft
539 self._aircraftName = None
540 self._aircraftModel = None
541 self._handler.connect()
542 if self._normalRequestID is None:
543 self._nextSyncTime = -1
544 self._startDefaultNormal()
545
546 def reconnect(self):
547 """Initiate a reconnection to the simulator.
548
549 It does not reset already set up data, just calls connect() on the
550 handler."""
551 self._handler.connect()
552
553 def requestZFW(self, callback):
554 """Send a request for the ZFW."""
555 self._handler.requestRead([(0x3bfc, "d")], self._handleZFW, extra = callback)
556
557 def requestWeights(self, callback):
558 """Request the following weights: DOW, ZFW, payload.
559
560 These values will be passed to the callback function in this order, as
561 separate arguments."""
562 self._handler.requestRead([(0x13fc, "d")], self._handlePayloadCount,
563 extra = callback)
564
565 def requestTime(self, callback):
566 """Request the time from the simulator."""
567 self._handler.requestRead(Simulator.timeData, self._handleTime,
568 extra = callback)
569
570 def startMonitoring(self):
571 """Start the periodic monitoring of the aircraft and pass the resulting
572 state to the aircraft object periodically."""
573 assert not self._monitoringRequested
574 self._monitoringRequested = True
575
576 def stopMonitoring(self):
577 """Stop the periodic monitoring of the aircraft."""
578 assert self._monitoringRequested
579 self._monitoringRequested = False
580
581 def startFlare(self):
582 """Start monitoring the flare time.
583
584 At present it is assumed to be called from the FSUIPC thread, hence no
585 protection."""
586 #self._aircraft.logger.debug("startFlare")
587 if self._flareRequestID is None:
588 self._flareRates = []
589 self._flareRequestID = self._handler.requestPeriodicRead(0.1,
590 Simulator.flareData1,
591 self._handleFlare1)
592
593 def cancelFlare(self):
594 """Cancel monitoring the flare time.
595
596 At present it is assumed to be called from the FSUIPC thread, hence no
597 protection."""
598 if self._flareRequestID is not None:
599 self._handler.clearPeriodic(self._flareRequestID)
600 self._flareRequestID = None
601
602 def sendMessage(self, message, duration = 3):
603 """Send a message to the pilot via the simulator.
604
605 duration is the number of seconds to keep the message displayed."""
606
607 if self._scroll:
608 if duration==0: duration = -1
609 elif duration == 1: duration = -2
610 else: duration = -duration
611
612 data = [(0x3380, -1 - len(message), message),
613 (0x32fa, 'h', duration)]
614
615 self._handler.requestWrite(data, self._handleMessageSent)
616
617 def getFuel(self, tanks, callback):
618 """Get the fuel from the given tanks.
619
620 The callback will be called with a list of two-tuples, where the tuples
621 have the following items:
622 - the current weight of the fuel in the tank (in kgs)
623 - the current total capacity of the tank (in kgs)."""
624 data = [(0x0af4, "H")] # Fuel weight
625 for tank in tanks:
626 offset = _tank2offset[tank]
627 data.append( (offset, "u") ) # tank level
628 data.append( (offset+4, "u") ) # tank capacity
629
630 self._handler.requestRead(data, self._handleFuelRetrieved,
631 extra = callback)
632
633 def setFuelLevel(self, levels):
634 """Set the fuel level to the given ones.
635
636 levels is an array of two-tuples, where each tuple consists of the
637 following:
638 - the const.FUELTANK_XXX constant denoting the tank that must be set,
639 - the requested level of the fuel as a floating-point value between 0.0
640 and 1.0."""
641 data = []
642 for (tank, level) in levels:
643 offset = _tank2offset[tank]
644 data.append( (offset, "u", long(level * 128.8 * 65536.0)) )
645 self._handler.requestWrite(data, self._handleFuelWritten)
646
647 def enableTimeSync(self):
648 """Enable the time synchronization."""
649 self._nextSyncTime = -1
650 self._syncTime = True
651
652 def disableTimeSync(self):
653 """Enable the time synchronization."""
654 self._syncTime = False
655 self._nextSyncTime = -1
656
657 def disconnect(self):
658 """Disconnect from the simulator."""
659 assert not self._monitoringRequested
660
661 self._stopNormal()
662 self._handler.disconnect()
663
664 def _startDefaultNormal(self):
665 """Start the default normal periodic request."""
666 assert self._normalRequestID is None
667 self._normalRequestID = \
668 self._handler.requestPeriodicRead(1.0,
669 Simulator.normalData,
670 self._handleNormal,
671 validator = self._validateNormal)
672
673 def _stopNormal(self):
674 """Stop the normal period request."""
675 assert self._normalRequestID is not None
676 self._handler.clearPeriodic(self._normalRequestID)
677 self._normalRequestID = None
678 self._monitoring = False
679
680 def _validateNormal(self, data, extra):
681 """Validate the normal data."""
682 return data[0]!=0 and data[1]!=0 and len(data[5])>0 and len(data[6])>0
683
684 def _handleNormal(self, data, extra):
685 """Handle the reply to the normal request.
686
687 At the beginning the result consists the data for normalData. When
688 monitoring is started, it contains the result also for the
689 aircraft-specific values.
690 """
691 timestamp = Simulator._getTimestamp(data)
692
693 createdNewModel = self._setAircraftName(timestamp, data[5], data[6])
694
695 self._scroll = data[7]!=0
696
697 self._checkTimeSync()
698
699 if self._monitoringRequested and not self._monitoring:
700 self._stopNormal()
701 self._startMonitoring()
702 elif self._monitoring and not self._monitoringRequested:
703 self._stopNormal()
704 self._startDefaultNormal()
705 elif self._monitoring and self._aircraftModel is not None and \
706 not createdNewModel:
707 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
708 timestamp, data)
709 self._aircraft.handleState(aircraftState)
710
711 def _checkTimeSync(self):
712 """Check if we need to synchronize the FS time."""
713 if not self._syncTime: return
714
715 now = time.time()
716 seconds = time.gmtime(now).tm_sec
717
718 if seconds>30 and seconds<59:
719 if self._nextSyncTime > (now - 0.49):
720 return
721
722 self._handler.requestWrite([(0x023a, "b", int(seconds))],
723 self._handleTimeSynced)
724
725 #print "Set the seconds to ", seconds
726
727 if self._nextSyncTime<0:
728 self._nextSyncTime = now
729
730 self._nextSyncTime += Simulator.TIME_SYNC_INTERVAL
731 else:
732 self._nextSyncTime = -1
733
734 def _handleTimeSynced(self, success, extra):
735 """Callback for the time sync result."""
736 pass
737
738 def _setAircraftName(self, timestamp, name, airPath):
739 """Set the name of the aicraft and if it is different from the
740 previous, create a new model for it.
741
742 If so, also notifty the aircraft about the change.
743
744 Return if a new model was created."""
745 aircraftName = (name, airPath)
746 if aircraftName==self._aircraftName:
747 return False
748
749 self._aircraftName = aircraftName
750 needNew = self._aircraftModel is None
751 needNew = needNew or\
752 not self._aircraftModel.doesHandle(self._aircraft, aircraftName)
753 if not needNew:
754 specialModel = AircraftModel.findSpecial(self._aircraft, aircraftName)
755 needNew = specialModel is not None and \
756 specialModel is not self._aircraftModel.__class__
757
758 if needNew:
759 self._setAircraftModel(AircraftModel.create(self._aircraft, aircraftName))
760
761
762 self._aircraft.modelChanged(timestamp, self._latin1decoder(name)[0],
763 self._aircraftModel.name)
764
765 return needNew
766
767 def _setAircraftModel(self, model):
768 """Set a new aircraft model.
769
770 It will be queried for the data to monitor and the monitoring request
771 will be replaced by a new one."""
772 self._aircraftModel = model
773
774 if self._monitoring:
775 self._stopNormal()
776 self._startMonitoring()
777
778 def _startMonitoring(self):
779 """Start monitoring with the current aircraft model."""
780 data = Simulator.normalData[:]
781 self._aircraftModel.addMonitoringData(data)
782
783 self._normalRequestID = \
784 self._handler.requestPeriodicRead(1.0, data,
785 self._handleNormal,
786 validator = self._validateNormal)
787 self._monitoring = True
788
789 def _addFlareRate(self, data):
790 """Append a flare rate to the list of last rates."""
791 if len(self._flareRates)>=3:
792 del self._flareRates[0]
793 self._flareRates.append(Handler.fsuipc2VS(data))
794
795 def _handleFlare1(self, data, normal):
796 """Handle the first stage of flare monitoring."""
797 #self._aircraft.logger.debug("handleFlare1: " + str(data))
798 if Handler.fsuipc2radioAltitude(data[1])<=50.0:
799 self._flareStart = time.time()
800 self._flareStartFS = data[0]
801 self._handler.clearPeriodic(self._flareRequestID)
802 self._flareRequestID = \
803 self._handler.requestPeriodicRead(0.1,
804 Simulator.flareData2,
805 self._handleFlare2)
806 self._handler.requestRead(Simulator.flareStartData,
807 self._handleFlareStart)
808
809 self._addFlareRate(data[2])
810
811 def _handleFlareStart(self, data, extra):
812 """Handle the data need to notify the aircraft about the starting of
813 the flare."""
814 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
815 if data is not None:
816 windDirection = data[1]*360.0/65536.0
817 if windDirection<0.0: windDirection += 360.0
818 self._aircraft.flareStarted(data[0], windDirection,
819 data[2]*1609.344/100.0,
820 self._flareStart, self._flareStartFS)
821
822 def _handleFlare2(self, data, normal):
823 """Handle the first stage of flare monitoring."""
824 #self._aircraft.logger.debug("handleFlare2: " + str(data))
825 if data[1]!=0:
826 flareEnd = time.time()
827 self._handler.clearPeriodic(self._flareRequestID)
828 self._flareRequestID = None
829
830 flareEndFS = data[0]
831 if flareEndFS<self._flareStartFS:
832 flareEndFS += 60
833
834 tdRate = Handler.fsuipc2VS(data[3])
835 tdRateCalculatedByFS = True
836 if tdRate==0 or tdRate>1000.0 or tdRate<-1000.0:
837 tdRate = min(self._flareRates)
838 tdRateCalculatedByFS = False
839
840 self._aircraft.flareFinished(flareEnd, flareEndFS,
841 tdRate, tdRateCalculatedByFS,
842 Handler.fsuipc2IAS(data[4]),
843 Handler.fsuipc2Degrees(data[5]),
844 Handler.fsuipc2Degrees(data[6]),
845 Handler.fsuipc2PositiveDegrees(data[7]))
846 else:
847 self._addFlareRate(data[2])
848
849 def _handleZFW(self, data, callback):
850 """Callback for a ZFW retrieval request."""
851 zfw = data[0] * const.LBSTOKG / 256.0
852 callback(zfw)
853
854 def _handleTime(self, data, callback):
855 """Callback for a time retrieval request."""
856 callback(Simulator._getTimestamp(data))
857
858 def _handlePayloadCount(self, data, callback):
859 """Callback for the payload count retrieval request."""
860 payloadCount = data[0]
861 data = [(0x3bfc, "d"), (0x30c0, "f")]
862 for i in range(0, payloadCount):
863 data.append((0x1400 + i*48, "f"))
864
865 self._handler.requestRead(data, self._handleWeights,
866 extra = callback)
867
868 def _handleWeights(self, data, callback):
869 """Callback for the weights retrieval request."""
870 zfw = data[0] * const.LBSTOKG / 256.0
871 grossWeight = data[1] * const.LBSTOKG
872 payload = sum(data[2:]) * const.LBSTOKG
873 dow = zfw - payload
874 callback(dow, payload, zfw, grossWeight)
875
876 def _handleMessageSent(self, success, extra):
877 """Callback for a message sending request."""
878 pass
879
880 def _handleFuelRetrieved(self, data, callback):
881 """Callback for a fuel retrieval request."""
882 fuelWeight = data[0] / 256.0
883 result = []
884 for i in range(1, len(data), 2):
885 capacity = data[i+1] * fuelWeight * const.LBSTOKG
886 amount = data[i] * capacity / 128.0 / 65536.0
887 result.append( (amount, capacity) )
888
889 callback(result)
890
891 def _handleFuelWritten(self, success, extra):
892 """Callback for a fuel setting request."""
893 pass
894
895#------------------------------------------------------------------------------
896
897class AircraftModel(object):
898 """Base class for the aircraft models.
899
900 Aircraft models handle the data arriving from FSUIPC and turn it into an
901 object describing the aircraft's state."""
902 monitoringData = [("paused", 0x0264, "H"),
903 ("latitude", 0x0560, "l"),
904 ("longitude", 0x0568, "l"),
905 ("frozen", 0x3364, "H"),
906 ("replay", 0x0628, "d"),
907 ("slew", 0x05dc, "H"),
908 ("overspeed", 0x036d, "b"),
909 ("stalled", 0x036c, "b"),
910 ("onTheGround", 0x0366, "H"),
911 ("zfw", 0x3bfc, "d"),
912 ("grossWeight", 0x30c0, "f"),
913 ("heading", 0x0580, "d"),
914 ("pitch", 0x0578, "d"),
915 ("bank", 0x057c, "d"),
916 ("ias", 0x02bc, "d"),
917 ("mach", 0x11c6, "H"),
918 ("groundSpeed", 0x02b4, "d"),
919 ("vs", 0x02c8, "d"),
920 ("radioAltitude", 0x31e4, "d"),
921 ("altitude", 0x0570, "l"),
922 ("gLoad", 0x11ba, "H"),
923 ("flapsControl", 0x0bdc, "d"),
924 ("flapsLeft", 0x0be0, "d"),
925 ("flapsRight", 0x0be4, "d"),
926 ("lights", 0x0d0c, "H"),
927 ("pitot", 0x029c, "b"),
928 ("parking", 0x0bc8, "H"),
929 ("noseGear", 0x0bec, "d"),
930 ("spoilersArmed", 0x0bcc, "d"),
931 ("spoilers", 0x0bd0, "d"),
932 ("altimeter", 0x0330, "H"),
933 ("nav1", 0x0350, "H"),
934 ("nav2", 0x0352, "H"),
935 ("squawk", 0x0354, "H"),
936 ("windSpeed", 0x0e90, "H"),
937 ("windDirection", 0x0e92, "H"),
938 ("visibility", 0x0e8a, "H")]
939
940
941 specialModels = []
942
943 @staticmethod
944 def registerSpecial(clazz):
945 """Register the given class as a special model."""
946 AircraftModel.specialModels.append(clazz)
947
948 @staticmethod
949 def findSpecial(aircraft, aircraftName):
950 for specialModel in AircraftModel.specialModels:
951 if specialModel.doesHandle(aircraft, aircraftName):
952 return specialModel
953 return None
954
955 @staticmethod
956 def create(aircraft, aircraftName):
957 """Create the model for the given aircraft name, and notify the
958 aircraft about it."""
959 specialModel = AircraftModel.findSpecial(aircraft, aircraftName)
960 if specialModel is not None:
961 return specialModel()
962 if aircraft.type in _genericModels:
963 return _genericModels[aircraft.type]()
964 else:
965 return GenericModel()
966
967 @staticmethod
968 def convertBCD(data, length):
969 """Convert a data item encoded as BCD into a string of the given number
970 of digits."""
971 bcd = ""
972 for i in range(0, length):
973 digit = chr(ord('0') + (data&0x0f))
974 data >>= 4
975 bcd = digit + bcd
976 return bcd
977
978 @staticmethod
979 def convertFrequency(data):
980 """Convert the given frequency data to a string."""
981 bcd = AircraftModel.convertBCD(data, 4)
982 return "1" + bcd[0:2] + "." + bcd[2:4]
983
984 def __init__(self, flapsNotches):
985 """Construct the aircraft model.
986
987 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
988 self._flapsNotches = flapsNotches
989
990 @property
991 def name(self):
992 """Get the name for this aircraft model."""
993 return "FSUIPC/Generic"
994
995 def doesHandle(self, aircraft, aircraftName):
996 """Determine if the model handles the given aircraft name.
997
998 This default implementation returns False."""
999 return False
1000
1001 def _addOffsetWithIndexMember(self, dest, offset, type, attrName = None):
1002 """Add the given FSUIPC offset and type to the given array and a member
1003 attribute with the given name."""
1004 dest.append((offset, type))
1005 if attrName is not None:
1006 setattr(self, attrName, len(dest)-1)
1007
1008 def _addDataWithIndexMembers(self, dest, prefix, data):
1009 """Add FSUIPC data to the given array and also corresponding index
1010 member variables with the given prefix.
1011
1012 data is a list of triplets of the following items:
1013 - the name of the data item. The index member variable will have a name
1014 created by prepending the given prefix to this name.
1015 - the FSUIPC offset
1016 - the FSUIPC type
1017
1018 The latter two items will be appended to dest."""
1019 for (name, offset, type) in data:
1020 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
1021
1022 def addMonitoringData(self, data):
1023 """Add the model-specific monitoring data to the given array."""
1024 self._addDataWithIndexMembers(data, "_monidx_",
1025 AircraftModel.monitoringData)
1026
1027 def getAircraftState(self, aircraft, timestamp, data):
1028 """Get an aircraft state object for the given monitoring data."""
1029 state = fs.AircraftState()
1030
1031 state.timestamp = timestamp
1032
1033 state.latitude = data[self._monidx_latitude] * \
1034 90.0 / 10001750.0 / 65536.0 / 65536.0
1035
1036 state.longitude = data[self._monidx_longitude] * \
1037 360.0 / 65536.0 / 65536.0 / 65536.0 / 65536.0
1038 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1039
1040 state.paused = data[self._monidx_paused]!=0 or \
1041 data[self._monidx_frozen]!=0 or \
1042 data[self._monidx_replay]!=0
1043 state.trickMode = data[self._monidx_slew]!=0
1044
1045 state.overspeed = data[self._monidx_overspeed]!=0
1046 state.stalled = data[self._monidx_stalled]!=0
1047 state.onTheGround = data[self._monidx_onTheGround]!=0
1048
1049 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
1050 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
1051
1052 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
1053
1054 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
1055 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
1056
1057 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
1058 state.mach = data[self._monidx_mach] / 20480.0
1059 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
1060 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
1061
1062 state.radioAltitude = \
1063 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
1064 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
1065
1066 state.gLoad = data[self._monidx_gLoad] / 625.0
1067
1068 numNotchesM1 = len(self._flapsNotches) - 1
1069 flapsIncrement = 16383 / numNotchesM1
1070 flapsControl = data[self._monidx_flapsControl]
1071 flapsIndex = flapsControl / flapsIncrement
1072 if flapsIndex < numNotchesM1:
1073 if (flapsControl - (flapsIndex*flapsIncrement) >
1074 (flapsIndex+1)*flapsIncrement - flapsControl):
1075 flapsIndex += 1
1076 state.flapsSet = self._flapsNotches[flapsIndex]
1077
1078 flapsLeft = data[self._monidx_flapsLeft]
1079 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
1080
1081 lights = data[self._monidx_lights]
1082
1083 state.navLightsOn = (lights&0x01) != 0
1084 state.antiCollisionLightsOn = (lights&0x02) != 0
1085 state.landingLightsOn = (lights&0x04) != 0
1086 state.strobeLightsOn = (lights&0x10) != 0
1087
1088 state.pitotHeatOn = data[self._monidx_pitot]!=0
1089
1090 state.parking = data[self._monidx_parking]!=0
1091
1092 state.gearsDown = data[self._monidx_noseGear]==16383
1093
1094 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
1095
1096 spoilers = data[self._monidx_spoilers]
1097 if spoilers<=4800:
1098 state.spoilersExtension = 0.0
1099 else:
1100 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
1101
1102 state.altimeter = data[self._monidx_altimeter] / 16.0
1103
1104 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
1105 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
1106 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
1107
1108 state.windSpeed = data[self._monidx_windSpeed]
1109 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
1110 if state.windDirection<0.0: state.windDirection += 360.0
1111
1112 state.visibility = data[self._monidx_visibility]*1609.344/100.0
1113
1114 return state
1115
1116#------------------------------------------------------------------------------
1117
1118class GenericAircraftModel(AircraftModel):
1119 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1120 values and some other common parameters in a generic way."""
1121
1122 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1123 """Construct the generic aircraft model with the given data.
1124
1125 flapsNotches is an array of how much degrees the individual flaps
1126 notches mean.
1127
1128 fuelTanks is an array of const.FUELTANK_XXX constants about the
1129 aircraft's fuel tanks. They will be converted to offsets.
1130
1131 numEngines is the number of engines the aircraft has.
1132
1133 isN1 determines if the engines have an N1 value or an RPM value
1134 (e.g. pistons)."""
1135 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1136
1137 self._fuelTanks = fuelTanks
1138 self._fuelStartIndex = None
1139 self._numEngines = numEngines
1140 self._engineStartIndex = None
1141 self._isN1 = isN1
1142
1143 def doesHandle(self, aircraft, aircraftName):
1144 """Determine if the model handles the given aircraft name.
1145
1146 This implementation returns True."""
1147 return True
1148
1149 def addMonitoringData(self, data):
1150 """Add the model-specific monitoring data to the given array."""
1151 super(GenericAircraftModel, self).addMonitoringData(data)
1152
1153 self._addOffsetWithIndexMember(data, 0x0af4, "H", "_monidx_fuelWeight")
1154
1155 self._fuelStartIndex = len(data)
1156 for tank in self._fuelTanks:
1157 offset = _tank2offset[tank]
1158 self._addOffsetWithIndexMember(data, offset, "u") # tank level
1159 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
1160
1161 if self._isN1:
1162 self._engineStartIndex = len(data)
1163 for i in range(0, self._numEngines):
1164 self._addOffsetWithIndexMember(data, 0x2000 + i * 0x100, "f") # N1
1165 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
1166
1167 def getAircraftState(self, aircraft, timestamp, data):
1168 """Get the aircraft state.
1169
1170 Get it from the parent, and then add the data about the fuel levels and
1171 the engine parameters."""
1172 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1173 timestamp,
1174 data)
1175
1176 fuelWeight = data[self._monidx_fuelWeight]/256.0
1177 state.fuel = []
1178 for i in range(self._fuelStartIndex,
1179 self._fuelStartIndex + 2*len(self._fuelTanks), 2):
1180 fuel = data[i+1]*data[i]*fuelWeight*const.LBSTOKG/128.0/65536.0
1181 state.fuel.append(fuel)
1182
1183 state.n1 = []
1184 state.reverser = []
1185 for i in range(self._engineStartIndex,
1186 self._engineStartIndex + 2*self._numEngines, 2):
1187 state.n1.append(data[i])
1188 state.reverser.append(data[i+1]<0)
1189
1190 return state
1191
1192#------------------------------------------------------------------------------
1193
1194class GenericModel(GenericAircraftModel):
1195 """Generic aircraft model for an unknown type."""
1196 def __init__(self):
1197 """Construct the model."""
1198 super(GenericModel, self). \
1199 __init__(flapsNotches = [0, 10, 20, 30],
1200 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1201 numEngines = 2)
1202
1203 @property
1204 def name(self):
1205 """Get the name for this aircraft model."""
1206 return "FSUIPC/Generic"
1207
1208#------------------------------------------------------------------------------
1209
1210class B737Model(GenericAircraftModel):
1211 """Generic model for the Boeing 737 Classing and NG aircraft."""
1212 def __init__(self):
1213 """Construct the model."""
1214 super(B737Model, self). \
1215 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1216 fuelTanks = acft.Boeing737.fuelTanks,
1217 numEngines = 2)
1218
1219 @property
1220 def name(self):
1221 """Get the name for this aircraft model."""
1222 return "FSUIPC/Generic Boeing 737"
1223
1224#------------------------------------------------------------------------------
1225
1226class PMDGBoeing737NGModel(B737Model):
1227 """A model handler for the PMDG Boeing 737NG model."""
1228 @staticmethod
1229 def doesHandle(aircraft, (name, airPath)):
1230 """Determine if this model handler handles the aircraft with the given
1231 name."""
1232 return aircraft.type in [const.AIRCRAFT_B736,
1233 const.AIRCRAFT_B737,
1234 const.AIRCRAFT_B738] and \
1235 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
1236 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
1237 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
1238 name.find("700")!=-1 or airPath.find("700")!=-1 or \
1239 name.find("800")!=-1 or airPath.find("800")!=-1 or \
1240 name.find("900")!=-1 or airPath.find("900")!=-1)
1241
1242 @property
1243 def name(self):
1244 """Get the name for this aircraft model."""
1245 return "FSUIPC/PMDG Boeing 737NG"
1246
1247 def addMonitoringData(self, data):
1248 """Add the model-specific monitoring data to the given array."""
1249 super(PMDGBoeing737NGModel, self).addMonitoringData(data)
1250
1251 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
1252
1253 def getAircraftState(self, aircraft, timestamp, data):
1254 """Get the aircraft state.
1255
1256 Get it from the parent, and then check some PMDG-specific stuff."""
1257 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
1258 timestamp,
1259 data)
1260 if data[self._pmdgidx_switches]&0x01==0x01:
1261 state.altimeter = 1013.25
1262
1263 return state
1264
1265#------------------------------------------------------------------------------
1266
1267class B767Model(GenericAircraftModel):
1268 """Generic model for the Boeing 767 aircraft."""
1269 def __init__(self):
1270 """Construct the model."""
1271 super(B767Model, self). \
1272 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1273 fuelTanks = acft.Boeing767.fuelTanks,
1274 numEngines = 2)
1275
1276 @property
1277 def name(self):
1278 """Get the name for this aircraft model."""
1279 return "FSUIPC/Generic Boeing 767"
1280
1281#------------------------------------------------------------------------------
1282
1283class DH8DModel(GenericAircraftModel):
1284 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1285 def __init__(self):
1286 """Construct the model."""
1287 super(DH8DModel, self). \
1288 __init__(flapsNotches = [0, 5, 10, 15, 35],
1289 fuelTanks = acft.DH8D.fuelTanks,
1290 numEngines = 2)
1291
1292 @property
1293 def name(self):
1294 """Get the name for this aircraft model."""
1295 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1296
1297#------------------------------------------------------------------------------
1298
1299class DreamwingsDH8DModel(DH8DModel):
1300 """Model handler for the Dreamwings Dash 8-Q400."""
1301 @staticmethod
1302 def doesHandle(aircraft, (name, airPath)):
1303 """Determine if this model handler handles the aircraft with the given
1304 name."""
1305 return aircraft.type==const.AIRCRAFT_DH8D and \
1306 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1307 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1308 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1309 airPath.find("Dash8Q400")!=-1
1310
1311 @property
1312 def name(self):
1313 """Get the name for this aircraft model."""
1314 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1315
1316 def getAircraftState(self, aircraft, timestamp, data):
1317 """Get the aircraft state.
1318
1319 Get it from the parent, and then invert the pitot heat state."""
1320 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1321 timestamp,
1322 data)
1323 state.pitotHeatOn = not state.pitotHeatOn
1324
1325 return state
1326#------------------------------------------------------------------------------
1327
1328class CRJ2Model(GenericAircraftModel):
1329 """Generic model for the Bombardier CRJ-200 aircraft."""
1330 def __init__(self):
1331 """Construct the model."""
1332 super(CRJ2Model, self). \
1333 __init__(flapsNotches = [0, 8, 20, 30, 45],
1334 fuelTanks = acft.CRJ2.fuelTanks,
1335 numEngines = 2)
1336
1337 @property
1338 def name(self):
1339 """Get the name for this aircraft model."""
1340 return "FSUIPC/Generic Bombardier CRJ-200"
1341
1342#------------------------------------------------------------------------------
1343
1344class F70Model(GenericAircraftModel):
1345 """Generic model for the Fokker F70 aircraft."""
1346 def __init__(self):
1347 """Construct the model."""
1348 super(F70Model, self). \
1349 __init__(flapsNotches = [0, 8, 15, 25, 42],
1350 fuelTanks = acft.F70.fuelTanks,
1351 numEngines = 2)
1352
1353 @property
1354 def name(self):
1355 """Get the name for this aircraft model."""
1356 return "FSUIPC/Generic Fokker 70"
1357
1358#------------------------------------------------------------------------------
1359
1360class DC3Model(GenericAircraftModel):
1361 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1362 def __init__(self):
1363 """Construct the model."""
1364 super(DC3Model, self). \
1365 __init__(flapsNotches = [0, 15, 30, 45],
1366 fuelTanks = acft.DC3.fuelTanks,
1367 numEngines = 2)
1368
1369 @property
1370 def name(self):
1371 """Get the name for this aircraft model."""
1372 return "FSUIPC/Generic Lisunov Li-2"
1373
1374#------------------------------------------------------------------------------
1375
1376class T134Model(GenericAircraftModel):
1377 """Generic model for the Tupolev Tu-134 aircraft."""
1378 def __init__(self):
1379 """Construct the model."""
1380 super(T134Model, self). \
1381 __init__(flapsNotches = [0, 10, 20, 30],
1382 fuelTanks = acft.T134.fuelTanks,
1383 numEngines = 2)
1384
1385 @property
1386 def name(self):
1387 """Get the name for this aircraft model."""
1388 return "FSUIPC/Generic Tupolev Tu-134"
1389
1390#------------------------------------------------------------------------------
1391
1392class T154Model(GenericAircraftModel):
1393 """Generic model for the Tupolev Tu-134 aircraft."""
1394 def __init__(self):
1395 """Construct the model."""
1396 super(T154Model, self). \
1397 __init__(flapsNotches = [0, 15, 28, 45],
1398 fuelTanks = acft.T154.fuelTanks,
1399 numEngines = 3)
1400
1401 @property
1402 def name(self):
1403 """Get the name for this aircraft model."""
1404 return "FSUIPC/Generic Tupolev Tu-154"
1405
1406 def getAircraftState(self, aircraft, timestamp, data):
1407 """Get an aircraft state object for the given monitoring data.
1408
1409 This removes the reverser value for the middle engine."""
1410 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1411 del state.reverser[1]
1412 return state
1413
1414#------------------------------------------------------------------------------
1415
1416class YK40Model(GenericAircraftModel):
1417 """Generic model for the Yakovlev Yak-40 aircraft."""
1418 def __init__(self):
1419 """Construct the model."""
1420 super(YK40Model, self). \
1421 __init__(flapsNotches = [0, 20, 35],
1422 fuelTanks = acft.YK40.fuelTanks,
1423 numEngines = 2)
1424
1425 @property
1426 def name(self):
1427 """Get the name for this aircraft model."""
1428 return "FSUIPC/Generic Yakovlev Yak-40"
1429
1430#------------------------------------------------------------------------------
1431
1432_genericModels = { const.AIRCRAFT_B736 : B737Model,
1433 const.AIRCRAFT_B737 : B737Model,
1434 const.AIRCRAFT_B738 : B737Model,
1435 const.AIRCRAFT_B733 : B737Model,
1436 const.AIRCRAFT_B734 : B737Model,
1437 const.AIRCRAFT_B735 : B737Model,
1438 const.AIRCRAFT_DH8D : DH8DModel,
1439 const.AIRCRAFT_B762 : B767Model,
1440 const.AIRCRAFT_B763 : B767Model,
1441 const.AIRCRAFT_CRJ2 : B767Model,
1442 const.AIRCRAFT_F70 : F70Model,
1443 const.AIRCRAFT_DC3 : DC3Model,
1444 const.AIRCRAFT_T134 : T134Model,
1445 const.AIRCRAFT_T154 : T154Model,
1446 const.AIRCRAFT_YK40 : YK40Model }
1447
1448#------------------------------------------------------------------------------
1449
1450AircraftModel.registerSpecial(PMDGBoeing737NGModel)
1451AircraftModel.registerSpecial(DreamwingsDH8DModel)
1452
1453#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.