source: src/mlx/fsuipc.py@ 1087:73fbd444b7fe

python3
Last change on this file since 1087:73fbd444b7fe was 1078:a1e7d4f63210, checked in by István Váradi <ivaradi@…>, 15 months ago

PMDG on MSFS 2020 support (re #364)

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