source: src/mlx/fsuipc.py@ 793:fea44dadc477

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

Added support for marking requests as unimportant, and if marked so, to ignore TypeError exceptions (re #303).

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