source: src/mlx/fsuipc.py@ 877:adf18182fbbf

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

Considering the 64-bit versions of FSX

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