source: src/mlx/fsuipc.py@ 406:00bd37a9869f

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

Tanks with a capacity of less than 1.0 kg are not returned (re #168)

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