source: src/mlx/fsuipc.py@ 1088:2f55d163f603

python3
Last change on this file since 1088:2f55d163f603 was 1088:2f55d163f603, checked in by István Váradi <ivaradi@…>, 14 months ago

The flight simulator type is set for all FSUIPC-based aircraft models.

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
[1088]992 model.setFSType(self._fsType)
[330]993
[5]994 if self._monitoring:
[8]995 self._stopNormal()
[5]996 self._startMonitoring()
[330]997
[5]998 def _startMonitoring(self):
999 """Start monitoring with the current aircraft model."""
1000 data = Simulator.normalData[:]
[212]1001 self._aircraftModel.addMonitoringData(data, self._fsType)
[330]1002
[5]1003 self._normalRequestID = \
[330]1004 self._handler.requestPeriodicRead(1.0, data,
[21]1005 self._handleNormal,
1006 validator = self._validateNormal)
[16]1007 self._monitoring = True
[4]1008
[9]1009 def _addFlareRate(self, data):
1010 """Append a flare rate to the list of last rates."""
1011 if len(self._flareRates)>=3:
1012 del self._flareRates[0]
1013 self._flareRates.append(Handler.fsuipc2VS(data))
1014
1015 def _handleFlare1(self, data, normal):
1016 """Handle the first stage of flare monitoring."""
1017 #self._aircraft.logger.debug("handleFlare1: " + str(data))
1018 if Handler.fsuipc2radioAltitude(data[1])<=50.0:
1019 self._flareStart = time.time()
1020 self._flareStartFS = data[0]
1021 self._handler.clearPeriodic(self._flareRequestID)
1022 self._flareRequestID = \
[330]1023 self._handler.requestPeriodicRead(0.1,
[9]1024 Simulator.flareData2,
1025 self._handleFlare2)
1026 self._handler.requestRead(Simulator.flareStartData,
1027 self._handleFlareStart)
[330]1028
[9]1029 self._addFlareRate(data[2])
1030
1031 def _handleFlareStart(self, data, extra):
1032 """Handle the data need to notify the aircraft about the starting of
1033 the flare."""
1034 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
1035 if data is not None:
1036 windDirection = data[1]*360.0/65536.0
1037 if windDirection<0.0: windDirection += 360.0
1038 self._aircraft.flareStarted(data[0], windDirection,
1039 data[2]*1609.344/100.0,
1040 self._flareStart, self._flareStartFS)
1041
1042 def _handleFlare2(self, data, normal):
1043 """Handle the first stage of flare monitoring."""
1044 #self._aircraft.logger.debug("handleFlare2: " + str(data))
1045 if data[1]!=0:
1046 flareEnd = time.time()
1047 self._handler.clearPeriodic(self._flareRequestID)
1048 self._flareRequestID = None
1049
1050 flareEndFS = data[0]
1051 if flareEndFS<self._flareStartFS:
1052 flareEndFS += 60
1053
1054 tdRate = Handler.fsuipc2VS(data[3])
1055 tdRateCalculatedByFS = True
1056 if tdRate==0 or tdRate>1000.0 or tdRate<-1000.0:
1057 tdRate = min(self._flareRates)
1058 tdRateCalculatedByFS = False
1059
1060 self._aircraft.flareFinished(flareEnd, flareEndFS,
1061 tdRate, tdRateCalculatedByFS,
1062 Handler.fsuipc2IAS(data[4]),
1063 Handler.fsuipc2Degrees(data[5]),
1064 Handler.fsuipc2Degrees(data[6]),
1065 Handler.fsuipc2PositiveDegrees(data[7]))
1066 else:
1067 self._addFlareRate(data[2])
[59]1068
1069 def _handleZFW(self, data, callback):
1070 """Callback for a ZFW retrieval request."""
1071 zfw = data[0] * const.LBSTOKG / 256.0
1072 callback(zfw)
[330]1073
[61]1074 def _handleTime(self, data, callback):
1075 """Callback for a time retrieval request."""
1076 callback(Simulator._getTimestamp(data))
[117]1077
1078 def _handlePayloadCount(self, data, callback):
1079 """Callback for the payload count retrieval request."""
1080 payloadCount = data[0]
1081 data = [(0x3bfc, "d"), (0x30c0, "f")]
1082 for i in range(0, payloadCount):
1083 data.append((0x1400 + i*48, "f"))
[330]1084
[117]1085 self._handler.requestRead(data, self._handleWeights,
1086 extra = callback)
1087
1088 def _handleWeights(self, data, callback):
1089 """Callback for the weights retrieval request."""
1090 zfw = data[0] * const.LBSTOKG / 256.0
1091 grossWeight = data[1] * const.LBSTOKG
1092 payload = sum(data[2:]) * const.LBSTOKG
1093 dow = zfw - payload
1094 callback(dow, payload, zfw, grossWeight)
[133]1095
[152]1096 def _handleMessageSent(self, success, disconnect):
[133]1097 """Callback for a message sending request."""
[152]1098 #print "fsuipc.Simulator._handleMessageSent", disconnect
1099 if disconnect:
1100 self._handler.disconnect()
[141]1101
[1004]1102 def _handleNumHotkeys(self, data, hotkeySet):
[168]1103 """Handle the result of the query of the number of hotkeys"""
[1004]1104 (id, generation) = hotkeySet
[168]1105 with self._hotkeyLock:
1106 if id==self._hotkeySetID and generation==self._hotkeySetGeneration:
1107 numHotkeys = data[0]
[919]1108 print("fsuipc.Simulator._handleNumHotkeys: numHotkeys:", numHotkeys)
[330]1109 data = [(0x3210 + i*4, "d") for i in range(0, numHotkeys)]
[168]1110 self._handler.requestRead(data, self._handleHotkeyTable,
[1004]1111 hotkeySet)
[168]1112
1113 def _setupHotkeys(self, data):
1114 """Setup the hiven hotkeys and return the data to be written.
1115
1116 If there were hotkeys set previously, they are reused as much as
1117 possible. Any of them not reused will be cleared."""
1118 hotkeys = self._hotkeys
1119 numHotkeys = len(hotkeys)
1120
1121 oldHotkeyOffsets = set([] if self._hotkeyOffets is None else
1122 self._hotkeyOffets)
1123
1124 self._hotkeyOffets = []
1125 numOffsets = 0
1126
1127 while oldHotkeyOffsets:
1128 offset = oldHotkeyOffsets.pop()
1129 self._hotkeyOffets.append(offset)
1130 numOffsets += 1
1131
1132 if numOffsets>=numHotkeys:
1133 break
1134
1135 for i in range(0, len(data)):
1136 if numOffsets>=numHotkeys:
1137 break
1138
1139 if data[i]==0:
1140 self._hotkeyOffets.append(0x3210 + i*4)
1141 numOffsets += 1
1142
1143 writeData = []
1144 for i in range(0, numOffsets):
1145 Simulator._appendHotkeyData(writeData,
1146 self._hotkeyOffets[i],
1147 hotkeys[i])
1148
1149 for offset in oldHotkeyOffsets:
[919]1150 writeData.append((offset, "u", int(0)))
[168]1151
1152 return writeData
1153
[1004]1154 def _handleHotkeyTable(self, data, hotkeySet):
[168]1155 """Handle the result of the query of the hotkey table."""
[1004]1156 (id, generation) = hotkeySet
[168]1157 with self._hotkeyLock:
1158 if id==self._hotkeySetID and generation==self._hotkeySetGeneration:
1159 writeData = self._setupHotkeys(data)
1160 self._handler.requestWrite(writeData,
1161 self._handleHotkeysWritten,
[1004]1162 hotkeySet)
[330]1163
[1004]1164 def _handleHotkeysWritten(self, success, hotkeySet):
[168]1165 """Handle the result of the hotkeys having been written."""
[1004]1166 (id, generation) = hotkeySet
[330]1167 with self._hotkeyLock:
[168]1168 if success and id==self._hotkeySetID and \
1169 generation==self._hotkeySetGeneration:
1170 data = [(offset + 3, "b") for offset in self._hotkeyOffets]
[330]1171
[168]1172 self._hotkeyRequestID = \
1173 self._handler.requestPeriodicRead(0.5, data,
1174 self._handleHotkeys,
[1004]1175 hotkeySet)
[168]1176
[1004]1177 def _handleHotkeys(self, data, hotkeySet):
[330]1178 """Handle the hotkeys."""
[1004]1179 (id, generation) = hotkeySet
[168]1180 with self._hotkeyLock:
1181 if id!=self._hotkeySetID or generation!=self._hotkeySetGeneration:
1182 return
1183
1184 callback = self._hotkeyCallback
1185 offsets = self._hotkeyOffets
1186
1187 hotkeysPressed = []
1188 for i in range(0, len(data)):
1189 if data[i]!=0:
1190 hotkeysPressed.append(i)
1191
1192 if hotkeysPressed:
1193 data = []
1194 for index in hotkeysPressed:
1195 data.append((offsets[index]+3, "b", int(0)))
1196 self._handler.requestWrite(data, self._handleHotkeysCleared)
1197
1198 callback(id, hotkeysPressed)
1199
1200 def _handleHotkeysCleared(self, sucess, extra):
[330]1201 """Callback for the hotkey-clearing write request."""
[168]1202
1203 def _clearHotkeyRequest(self):
1204 """Clear the hotkey request in the handler if there is any."""
1205 if self._hotkeyRequestID is not None:
1206 self._handler.clearPeriodic(self._hotkeyRequestID)
1207 self._hotkeyRequestID = None
[330]1208
[4]1209#------------------------------------------------------------------------------
1210
1211class AircraftModel(object):
1212 """Base class for the aircraft models.
1213
1214 Aircraft models handle the data arriving from FSUIPC and turn it into an
1215 object describing the aircraft's state."""
[8]1216 monitoringData = [("paused", 0x0264, "H"),
[89]1217 ("latitude", 0x0560, "l"),
1218 ("longitude", 0x0568, "l"),
[4]1219 ("frozen", 0x3364, "H"),
[5]1220 ("replay", 0x0628, "d"),
[4]1221 ("slew", 0x05dc, "H"),
1222 ("overspeed", 0x036d, "b"),
1223 ("stalled", 0x036c, "b"),
1224 ("onTheGround", 0x0366, "H"),
[9]1225 ("zfw", 0x3bfc, "d"),
[4]1226 ("grossWeight", 0x30c0, "f"),
1227 ("heading", 0x0580, "d"),
1228 ("pitch", 0x0578, "d"),
1229 ("bank", 0x057c, "d"),
1230 ("ias", 0x02bc, "d"),
[9]1231 ("mach", 0x11c6, "H"),
[5]1232 ("groundSpeed", 0x02b4, "d"),
[4]1233 ("vs", 0x02c8, "d"),
[8]1234 ("radioAltitude", 0x31e4, "d"),
[4]1235 ("altitude", 0x0570, "l"),
[665]1236 ("gLoad", 0x11ba, "h"),
[4]1237 ("flapsControl", 0x0bdc, "d"),
1238 ("flapsLeft", 0x0be0, "d"),
1239 ("flapsRight", 0x0be4, "d"),
[559]1240 ("flapsAxis", 0x3414, "H"),
1241 ("flapsIncrement", 0x3bfa, "H"),
[4]1242 ("lights", 0x0d0c, "H"),
1243 ("pitot", 0x029c, "b"),
[8]1244 ("parking", 0x0bc8, "H"),
[209]1245 ("gearControl", 0x0be8, "d"),
[4]1246 ("noseGear", 0x0bec, "d"),
1247 ("spoilersArmed", 0x0bcc, "d"),
[5]1248 ("spoilers", 0x0bd0, "d"),
1249 ("altimeter", 0x0330, "H"),
[408]1250 ("qnh", 0x0ec6, "H"),
[5]1251 ("nav1", 0x0350, "H"),
[320]1252 ("nav1_obs", 0x0c4e, "H"),
[8]1253 ("nav2", 0x0352, "H"),
[321]1254 ("nav2_obs", 0x0c5e, "H"),
[317]1255 ("adf1_main", 0x034c, "H"),
1256 ("adf1_ext", 0x0356, "H"),
1257 ("adf2_main", 0x02d4, "H"),
1258 ("adf2_ext", 0x02d6, "H"),
[9]1259 ("squawk", 0x0354, "H"),
1260 ("windSpeed", 0x0e90, "H"),
[134]1261 ("windDirection", 0x0e92, "H"),
[243]1262 ("visibility", 0x0e8a, "H"),
[334]1263 ("cog", 0x2ef8, "f"),
[337]1264 ("xpdrC", 0x7b91, "b"),
1265 ("apMaster", 0x07bc, "d"),
1266 ("apHeadingHold", 0x07c8, "d"),
1267 ("apHeading", 0x07cc, "H"),
1268 ("apAltitudeHold", 0x07d0, "d"),
[340]1269 ("apAltitude", 0x07d4, "u"),
[390]1270 ("elevatorTrim", 0x2ea0, "f"),
1271 ("eng1DeIce", 0x08b2, "H"),
1272 ("eng2DeIce", 0x094a, "H"),
1273 ("propDeIce", 0x337c, "b"),
1274 ("structDeIce", 0x337d, "b")]
[7]1275
1276 specialModels = []
1277
1278 @staticmethod
1279 def registerSpecial(clazz):
1280 """Register the given class as a special model."""
1281 AircraftModel.specialModels.append(clazz)
1282
[4]1283 @staticmethod
[8]1284 def findSpecial(aircraft, aircraftName):
1285 for specialModel in AircraftModel.specialModels:
1286 if specialModel.doesHandle(aircraft, aircraftName):
1287 return specialModel
1288 return None
1289
1290 @staticmethod
[4]1291 def create(aircraft, aircraftName):
1292 """Create the model for the given aircraft name, and notify the
[7]1293 aircraft about it."""
[8]1294 specialModel = AircraftModel.findSpecial(aircraft, aircraftName)
1295 if specialModel is not None:
1296 return specialModel()
1297 if aircraft.type in _genericModels:
1298 return _genericModels[aircraft.type]()
[7]1299 else:
1300 return GenericModel()
[4]1301
[5]1302 @staticmethod
[8]1303 def convertBCD(data, length):
1304 """Convert a data item encoded as BCD into a string of the given number
1305 of digits."""
1306 bcd = ""
1307 for i in range(0, length):
1308 digit = chr(ord('0') + (data&0x0f))
1309 data >>= 4
1310 bcd = digit + bcd
1311 return bcd
1312
1313 @staticmethod
[5]1314 def convertFrequency(data):
1315 """Convert the given frequency data to a string."""
[8]1316 bcd = AircraftModel.convertBCD(data, 4)
1317 return "1" + bcd[0:2] + "." + bcd[2:4]
[5]1318
[317]1319 @staticmethod
1320 def convertADFFrequency(main, ext):
1321 """Convert the given ADF frequency data to a string."""
1322 mainBCD = AircraftModel.convertBCD(main, 4)
1323 extBCD = AircraftModel.convertBCD(ext, 4)
1324
1325 return (extBCD[1] if extBCD[1]!="0" else "") + \
1326 mainBCD[1:] + "." + extBCD[3]
[330]1327
[4]1328 def __init__(self, flapsNotches):
1329 """Construct the aircraft model.
[330]1330
[4]1331 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1332 self._flapsNotches = flapsNotches
[507]1333 self._xpdrReliable = False
[559]1334 self._flapsSet = -1
[1088]1335 self._fsType = None
[330]1336
[4]1337 @property
1338 def name(self):
1339 """Get the name for this aircraft model."""
1340 return "FSUIPC/Generic"
[330]1341
[1088]1342 def setFSType(self, fsType):
1343 """Set the flight simulator type."""
1344 self._fsType = fsType
1345
[8]1346 def doesHandle(self, aircraft, aircraftName):
[4]1347 """Determine if the model handles the given aircraft name.
[330]1348
[7]1349 This default implementation returns False."""
1350 return False
[4]1351
[7]1352 def _addOffsetWithIndexMember(self, dest, offset, type, attrName = None):
1353 """Add the given FSUIPC offset and type to the given array and a member
[330]1354 attribute with the given name."""
[7]1355 dest.append((offset, type))
1356 if attrName is not None:
[8]1357 setattr(self, attrName, len(dest)-1)
[7]1358
1359 def _addDataWithIndexMembers(self, dest, prefix, data):
[4]1360 """Add FSUIPC data to the given array and also corresponding index
1361 member variables with the given prefix.
1362
1363 data is a list of triplets of the following items:
1364 - the name of the data item. The index member variable will have a name
1365 created by prepending the given prefix to this name.
1366 - the FSUIPC offset
1367 - the FSUIPC type
[330]1368
[4]1369 The latter two items will be appended to dest."""
1370 for (name, offset, type) in data:
[7]1371 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
[330]1372
[212]1373 def addMonitoringData(self, data, fsType):
[7]1374 """Add the model-specific monitoring data to the given array."""
[8]1375 self._addDataWithIndexMembers(data, "_monidx_",
1376 AircraftModel.monitoringData)
[330]1377
[8]1378 def getAircraftState(self, aircraft, timestamp, data):
[4]1379 """Get an aircraft state object for the given monitoring data."""
1380 state = fs.AircraftState()
[330]1381
[4]1382 state.timestamp = timestamp
[89]1383
1384 state.latitude = data[self._monidx_latitude] * \
1385 90.0 / 10001750.0 / 65536.0 / 65536.0
1386
1387 state.longitude = data[self._monidx_longitude] * \
1388 360.0 / 65536.0 / 65536.0 / 65536.0 / 65536.0
1389 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
[330]1390
[4]1391 state.paused = data[self._monidx_paused]!=0 or \
[5]1392 data[self._monidx_frozen]!=0 or \
1393 data[self._monidx_replay]!=0
[4]1394 state.trickMode = data[self._monidx_slew]!=0
1395
1396 state.overspeed = data[self._monidx_overspeed]!=0
1397 state.stalled = data[self._monidx_stalled]!=0
1398 state.onTheGround = data[self._monidx_onTheGround]!=0
1399
[9]1400 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
[6]1401 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
[330]1402
[9]1403 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
[4]1404
[9]1405 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
1406 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
[4]1407
[9]1408 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
1409 state.mach = data[self._monidx_mach] / 20480.0
[5]1410 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
[9]1411 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
[4]1412
[9]1413 state.radioAltitude = \
1414 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
[6]1415 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
[5]1416
1417 state.gLoad = data[self._monidx_gLoad] / 625.0
[330]1418
[4]1419 numNotchesM1 = len(self._flapsNotches) - 1
[922]1420 flapsIncrement = 16383 // numNotchesM1
[4]1421 flapsControl = data[self._monidx_flapsControl]
[922]1422 flapsIndex = flapsControl // flapsIncrement
[4]1423 if flapsIndex < numNotchesM1:
1424 if (flapsControl - (flapsIndex*flapsIncrement) >
1425 (flapsIndex+1)*flapsIncrement - flapsControl):
1426 flapsIndex += 1
1427 state.flapsSet = self._flapsNotches[flapsIndex]
[559]1428 if state.flapsSet != self._flapsSet:
[919]1429 print("flapsControl: %d, flapsLeft: %d, flapsRight: %d, flapsAxis: %d, flapsIncrement: %d, flapsSet: %d, numNotchesM1: %d" % \
[559]1430 (flapsControl, data[self._monidx_flapsLeft],
1431 data[self._monidx_flapsRight], data[self._monidx_flapsAxis],
[919]1432 data[self._monidx_flapsIncrement], state.flapsSet, numNotchesM1))
[559]1433 self._flapsSet = state.flapsSet
[330]1434
[4]1435 flapsLeft = data[self._monidx_flapsLeft]
[16]1436 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
[330]1437
[4]1438 lights = data[self._monidx_lights]
[330]1439
[5]1440 state.navLightsOn = (lights&0x01) != 0
1441 state.antiCollisionLightsOn = (lights&0x02) != 0
1442 state.landingLightsOn = (lights&0x04) != 0
1443 state.strobeLightsOn = (lights&0x10) != 0
[330]1444
[4]1445 state.pitotHeatOn = data[self._monidx_pitot]!=0
1446
[8]1447 state.parking = data[self._monidx_parking]!=0
1448
[209]1449 state.gearControlDown = data[self._monidx_gearControl]==16383
[4]1450 state.gearsDown = data[self._monidx_noseGear]==16383
1451
1452 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
[330]1453
[4]1454 spoilers = data[self._monidx_spoilers]
1455 if spoilers<=4800:
1456 state.spoilersExtension = 0.0
1457 else:
1458 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
[5]1459
1460 state.altimeter = data[self._monidx_altimeter] / 16.0
[394]1461 state.altimeterReliable = True
[408]1462 state.qnh = data[self._monidx_qnh] / 16.0
[330]1463
[366]1464 state.ils = None
1465 state.ils_obs = None
1466 state.ils_manual = False
[5]1467 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
[320]1468 state.nav1_obs = data[self._monidx_nav1_obs]
[321]1469 state.nav1_manual = True
[5]1470 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
[320]1471 state.nav2_obs = data[self._monidx_nav2_obs]
[321]1472 state.nav2_manual = True
[317]1473 state.adf1 = \
1474 AircraftModel.convertADFFrequency(data[self._monidx_adf1_main],
1475 data[self._monidx_adf1_ext])
1476 state.adf2 = \
1477 AircraftModel.convertADFFrequency(data[self._monidx_adf2_main],
1478 data[self._monidx_adf2_ext])
[366]1479
[8]1480 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
[9]1481
1482 state.windSpeed = data[self._monidx_windSpeed]
1483 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
1484 if state.windDirection<0.0: state.windDirection += 360.0
[134]1485
1486 state.visibility = data[self._monidx_visibility]*1609.344/100.0
[330]1487
[243]1488 state.cog = data[self._monidx_cog]
[330]1489
[507]1490 if not self._xpdrReliable:
1491 self._xpdrReliable = data[self._monidx_xpdrC]!=0
1492
1493 state.xpdrC = data[self._monidx_xpdrC]!=1 \
1494 if self._xpdrReliable else None
[361]1495 state.autoXPDR = False
[334]1496
[337]1497 state.apMaster = data[self._monidx_apMaster]!=0
1498 state.apHeadingHold = data[self._monidx_apHeadingHold]!=0
1499 state.apHeading = data[self._monidx_apHeading] * 360.0 / 65536.0
1500 state.apAltitudeHold = data[self._monidx_apAltitudeHold]!=0
1501 state.apAltitude = data[self._monidx_apAltitude] / \
1502 const.FEETTOMETRES / 65536.0
1503
[340]1504
1505 state.elevatorTrim = data[self._monidx_elevatorTrim] * 180.0 / math.pi
1506
[390]1507 state.antiIceOn = data[self._monidx_eng1DeIce]!=0 or \
1508 data[self._monidx_eng2DeIce]!=0 or \
1509 data[self._monidx_propDeIce]!=0 or \
1510 data[self._monidx_structDeIce]!=0
1511
[4]1512 return state
[5]1513
1514#------------------------------------------------------------------------------
1515
[7]1516class GenericAircraftModel(AircraftModel):
1517 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1518 values and some other common parameters in a generic way."""
[330]1519
[140]1520 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
[7]1521 """Construct the generic aircraft model with the given data.
1522
1523 flapsNotches is an array of how much degrees the individual flaps
1524 notches mean.
1525
[140]1526 fuelTanks is an array of const.FUELTANK_XXX constants about the
1527 aircraft's fuel tanks. They will be converted to offsets.
[7]1528
1529 numEngines is the number of engines the aircraft has.
1530
1531 isN1 determines if the engines have an N1 value or an RPM value
1532 (e.g. pistons)."""
1533 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1534
[140]1535 self._fuelTanks = fuelTanks
[7]1536 self._fuelStartIndex = None
1537 self._numEngines = numEngines
1538 self._engineStartIndex = None
1539 self._isN1 = isN1
1540
[8]1541 def doesHandle(self, aircraft, aircraftName):
1542 """Determine if the model handles the given aircraft name.
[330]1543
[8]1544 This implementation returns True."""
1545 return True
1546
[212]1547 def addMonitoringData(self, data, fsType):
[7]1548 """Add the model-specific monitoring data to the given array."""
[212]1549 super(GenericAircraftModel, self).addMonitoringData(data, fsType)
[330]1550
[274]1551 self._fuelStartIndex = self._addFuelOffsets(data, "_monidx_fuelWeight")
[7]1552
[263]1553 self._engineStartIndex = len(data)
1554 for i in range(0, self._numEngines):
1555 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
1556 if self._isN1:
[23]1557 self._addOffsetWithIndexMember(data, 0x2000 + i * 0x100, "f") # N1
[263]1558 else:
1559 self._addOffsetWithIndexMember(data, 0x0898 + i * 0x98, "H") # RPM
1560 self._addOffsetWithIndexMember(data, 0x08c8 + i * 0x98, "H") # RPM scaler
[330]1561
[8]1562 def getAircraftState(self, aircraft, timestamp, data):
[7]1563 """Get the aircraft state.
1564
[330]1565 Get it from the parent, and then add the data about the fuel levels and
[7]1566 the engine parameters."""
1567 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
[8]1568 timestamp,
[7]1569 data)
1570
[274]1571 (state.fuel, state.totalFuel) = \
1572 self._convertFuelData(data, index = self._monidx_fuelWeight)
[263]1573
1574 state.n1 = [] if self._isN1 else None
1575 state.rpm = None if self._isN1 else []
1576 itemsPerEngine = 2 if self._isN1 else 3
[330]1577
[7]1578 state.reverser = []
1579 for i in range(self._engineStartIndex,
[263]1580 self._engineStartIndex +
1581 itemsPerEngine*self._numEngines,
1582 itemsPerEngine):
1583 state.reverser.append(data[i]<0)
1584 if self._isN1:
1585 state.n1.append(data[i+1])
1586 else:
1587 state.rpm.append(data[i+1] * data[i+2]/65536.0)
[7]1588
1589 return state
1590
[274]1591 def getFuel(self, handler, callback):
1592 """Get the fuel information for this model.
1593
1594 See Simulator.getFuel for more information. This
1595 implementation simply queries the fuel tanks given to the
1596 constructor."""
1597 data = []
1598 self._addFuelOffsets(data)
1599
1600 handler.requestRead(data, self._handleFuelRetrieved,
1601 extra = callback)
1602
1603 def setFuelLevel(self, handler, levels):
1604 """Set the fuel level.
1605
1606 See the description of Simulator.setFuelLevel. This
1607 implementation simply sets the fuel tanks as given."""
1608 data = []
1609 for (tank, level) in levels:
1610 offset = _tank2offset[tank]
[919]1611 value = int(level * 128.0 * 65536.0)
[274]1612 data.append( (offset, "u", value) )
1613
1614 handler.requestWrite(data, self._handleFuelWritten)
1615
1616 def _addFuelOffsets(self, data, weightIndexName = None):
1617 """Add the fuel offsets to the given data array.
1618
1619 If weightIndexName is not None, it will be the name of the
1620 fuel weight index.
1621
1622 Returns the index of the first fuel tank's data."""
1623 self._addOffsetWithIndexMember(data, 0x0af4, "H", weightIndexName)
1624
1625 fuelStartIndex = len(data)
1626 for tank in self._fuelTanks:
1627 offset = _tank2offset[tank]
1628 self._addOffsetWithIndexMember(data, offset, "u") # tank level
1629 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
[330]1630
[274]1631 return fuelStartIndex
1632
1633 def _convertFuelData(self, data, index = 0, addCapacities = False):
1634 """Convert the given data into a fuel info list.
1635
1636 The list consists of two or three-tuples of the following
1637 items:
1638 - the fuel tank ID,
1639 - the amount of the fuel in kg,
1640 - if addCapacities is True, the total capacity of the tank."""
1641 fuelWeight = data[index] / 256.0
1642 index += 1
1643
1644 result = []
1645 totalFuel = 0
1646 for fuelTank in self._fuelTanks:
1647 capacity = data[index+1] * fuelWeight * const.LBSTOKG
[406]1648 if capacity>=1.0:
1649 amount = data[index] * capacity / 128.0 / 65536.0
[330]1650
[406]1651 result.append( (fuelTank, amount, capacity) if addCapacities
1652 else (fuelTank, amount))
1653 totalFuel += amount
[412]1654 index += 2
[274]1655
[330]1656 return (result, totalFuel)
[274]1657
1658 def _handleFuelRetrieved(self, data, callback):
1659 """Callback for a fuel retrieval request."""
[330]1660 (fuelData, _totalFuel) = self._convertFuelData(data,
[274]1661 addCapacities = True)
1662 callback(fuelData)
[330]1663
[274]1664 def _handleFuelWritten(self, success, extra):
1665 """Callback for a fuel setting request."""
1666 pass
1667
[7]1668#------------------------------------------------------------------------------
1669
1670class GenericModel(GenericAircraftModel):
1671 """Generic aircraft model for an unknown type."""
1672 def __init__(self):
1673 """Construct the model."""
[8]1674 super(GenericModel, self). \
[7]1675 __init__(flapsNotches = [0, 10, 20, 30],
[140]1676 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
[7]1677 numEngines = 2)
1678
1679 @property
1680 def name(self):
1681 """Get the name for this aircraft model."""
[330]1682 return "FSUIPC/Generic"
[7]1683
1684#------------------------------------------------------------------------------
1685
1686class B737Model(GenericAircraftModel):
1687 """Generic model for the Boeing 737 Classing and NG aircraft."""
[330]1688 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1689
[7]1690 def __init__(self):
1691 """Construct the model."""
[8]1692 super(B737Model, self). \
[7]1693 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
[274]1694 fuelTanks = B737Model.fuelTanks,
[7]1695 numEngines = 2)
1696
1697 @property
1698 def name(self):
1699 """Get the name for this aircraft model."""
1700 return "FSUIPC/Generic Boeing 737"
1701
[539]1702 # Note: the function below should be enabled if testing the speed-based
1703 # takeoff on Linux
1704 # def getAircraftState(self, aircraft, timestamp, data):
1705 # """Get the aircraft state.
1706
1707 # Get it from the parent, and then check some PMDG-specific stuff."""
1708 # state = super(B737Model, self).getAircraftState(aircraft,
1709 # timestamp,
1710 # data)
1711 # state.strobeLightsOn = None
1712 # state.xpdrC = None
1713
1714 # return state
1715
[7]1716#------------------------------------------------------------------------------
1717
[8]1718class PMDGBoeing737NGModel(B737Model):
1719 """A model handler for the PMDG Boeing 737NG model."""
1720 @staticmethod
[968]1721 def doesHandle(aircraft, aircraftName):
[8]1722 """Determine if this model handler handles the aircraft with the given
1723 name."""
[968]1724 (name, airPath) = aircraftName
[8]1725 return aircraft.type in [const.AIRCRAFT_B736,
1726 const.AIRCRAFT_B737,
[191]1727 const.AIRCRAFT_B738,
1728 const.AIRCRAFT_B738C] and \
[8]1729 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
1730 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
1731 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
1732 name.find("700")!=-1 or airPath.find("700")!=-1 or \
1733 name.find("800")!=-1 or airPath.find("800")!=-1 or \
1734 name.find("900")!=-1 or airPath.find("900")!=-1)
1735
[789]1736 def __init__(self):
1737 """Construct the model."""
1738 super(PMDGBoeing737NGModel, self).__init__()
1739 self._lastGearControl = None
1740 self._lastNoseGear = None
1741
[8]1742 @property
1743 def name(self):
1744 """Get the name for this aircraft model."""
[212]1745 return "FSUIPC/PMDG Boeing 737NG(X)"
[8]1746
[212]1747 def addMonitoringData(self, data, fsType):
[8]1748 """Add the model-specific monitoring data to the given array."""
[212]1749 super(PMDGBoeing737NGModel, self).addMonitoringData(data, fsType)
[330]1750
[1078]1751 if fsType==const.SIM_MSFSX or fsType==const.SIM_P3D or fsType==const.SIM_MSFS2020:
[919]1752 print("%s detected, adding PMDG 737 NGX-specific offsets" % \
[1078]1753 ("FSX" if fsType==const.SIM_MSFSX else
1754 "P3D" if fsType==const.SIM_P3D else "MSFS 2020",))
[212]1755 self._addOffsetWithIndexMember(data, 0x6500, "b",
1756 "_pmdgidx_lts_positionsw")
[476]1757 self._addOffsetWithIndexMember(data, 0x6545, "b", "_pmdgidx_cmda")
1758 self._addOffsetWithIndexMember(data, 0x653f, "b", "_pmdgidx_aphdgsel")
1759 self._addOffsetWithIndexMember(data, 0x6543, "b", "_pmdgidx_apalthold")
1760 self._addOffsetWithIndexMember(data, 0x652c, "H", "_pmdgidx_aphdg")
1761 self._addOffsetWithIndexMember(data, 0x652e, "H", "_pmdgidx_apalt")
[1078]1762 if fsType==const.SIM_MSFS2020:
1763 self._addOffsetWithIndexMember(data, 0x0b46, "b", "_pmdgidx_xpdr")
1764 else:
1765 self._addOffsetWithIndexMember(data, 0x65cd, "b", "_pmdgidx_xpdr")
[476]1766 else:
[919]1767 print("FS9 detected, adding PMDG 737 NG-specific offsets")
[476]1768 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
1769 self._addOffsetWithIndexMember(data, 0x6216, "b", "_pmdgidx_xpdr")
1770 self._addOffsetWithIndexMember(data, 0x6227, "b", "_pmdgidx_ap")
1771 self._addOffsetWithIndexMember(data, 0x6228, "b", "_pmdgidx_aphdgsel")
1772 self._addOffsetWithIndexMember(data, 0x622a, "b", "_pmdgidx_apalthold")
1773 self._addOffsetWithIndexMember(data, 0x622c, "H", "_pmdgidx_aphdg")
1774 self._addOffsetWithIndexMember(data, 0x622e, "H", "_pmdgidx_apalt")
[212]1775
[8]1776 def getAircraftState(self, aircraft, timestamp, data):
1777 """Get the aircraft state.
1778
1779 Get it from the parent, and then check some PMDG-specific stuff."""
1780 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
1781 timestamp,
1782 data)
[1078]1783
1784 fsType = self._fsType
1785 if fsType==const.SIM_MSFSX or fsType==const.SIM_P3D or \
1786 fsType==const.SIM_MSFS2020:
1787 state.apMaster = data[self._pmdgidx_cmda]!=0
1788 state.apHeadingHold = data[self._pmdgidx_aphdgsel]!=0
1789 state.apAltitudeHold = data[self._pmdgidx_apalthold]!=0
1790
1791 # state.strobeLightsOn = data[self._pmdgidx_lts_positionsw]==0x02
1792 # state.xpdrC = data[self._pmdgidx_xpdr]==4
1793 if fsType==const.SIM_MSFS2020:
1794 state.xpdrC = data[self._pmdgidx_xpdr]==4
1795 else:
1796 state.strobeLightsOn = None
1797 state.xpdrC = None
1798 else:
[476]1799 if data[self._pmdgidx_switches]&0x01==0x01:
1800 state.altimeter = 1013.25
1801 state.apMaster = data[self._pmdgidx_ap]&0x02==0x02
1802 state.apHeadingHold = data[self._pmdgidx_aphdgsel]==2
1803 apalthold = data[self._pmdgidx_apalthold]
1804 state.apAltitudeHold = apalthold>=3 and apalthold<=6
[539]1805 state.xpdrC = data[self._pmdgidx_xpdr]==4
1806
1807 # Uncomment the following to test the speed-based takeoff
1808 # state.strobeLightsOn = None
1809 # state.xpdrC = None
[8]1810
[339]1811 state.apHeading = data[self._pmdgidx_aphdg]
1812 state.apAltitude = data[self._pmdgidx_apalt]
1813
[789]1814 gearControl = data[self._monidx_gearControl]
1815 noseGear = data[self._monidx_noseGear]
1816
1817 if gearControl!=self._lastGearControl or noseGear!=self._lastNoseGear:
[919]1818 print("gearControl:", gearControl, " noseGear:", noseGear)
[789]1819 self._lastGearControl = gearControl
1820 self._lastNoseGear = noseGear
1821
[8]1822 return state
1823
1824#------------------------------------------------------------------------------
1825
[7]1826class B767Model(GenericAircraftModel):
1827 """Generic model for the Boeing 767 aircraft."""
[330]1828 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
[274]1829
[7]1830 def __init__(self):
1831 """Construct the model."""
[8]1832 super(B767Model, self). \
[7]1833 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
[381]1834 fuelTanks = B767Model.fuelTanks,
[7]1835 numEngines = 2)
1836
1837 @property
1838 def name(self):
1839 """Get the name for this aircraft model."""
1840 return "FSUIPC/Generic Boeing 767"
1841
1842#------------------------------------------------------------------------------
1843
1844class DH8DModel(GenericAircraftModel):
[16]1845 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
[330]1846 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1847
[7]1848 def __init__(self):
1849 """Construct the model."""
[8]1850 super(DH8DModel, self). \
[7]1851 __init__(flapsNotches = [0, 5, 10, 15, 35],
[274]1852 fuelTanks = DH8DModel.fuelTanks,
[7]1853 numEngines = 2)
1854
1855 @property
1856 def name(self):
1857 """Get the name for this aircraft model."""
[16]1858 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1859
1860#------------------------------------------------------------------------------
1861
1862class DreamwingsDH8DModel(DH8DModel):
1863 """Model handler for the Dreamwings Dash 8-Q400."""
1864 @staticmethod
[968]1865 def doesHandle(aircraft, aircraftName):
[16]1866 """Determine if this model handler handles the aircraft with the given
1867 name."""
[968]1868 (name, airPath) = aircraftName
[16]1869 return aircraft.type==const.AIRCRAFT_DH8D and \
1870 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1871 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1872 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1873 airPath.find("Dash8Q400")!=-1
[7]1874
[16]1875 @property
1876 def name(self):
1877 """Get the name for this aircraft model."""
1878 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1879
[456]1880 def addMonitoringData(self, data, fsType):
1881 """Add the model-specific monitoring data to the given array."""
1882 super(DreamwingsDH8DModel, self).addMonitoringData(data, fsType)
1883
1884 self._addOffsetWithIndexMember(data, 0x132c, "d", "_dwdh8d_navgps")
1885
[16]1886 def getAircraftState(self, aircraft, timestamp, data):
1887 """Get the aircraft state.
1888
1889 Get it from the parent, and then invert the pitot heat state."""
1890 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1891 timestamp,
1892 data)
[456]1893 if data[self._dwdh8d_navgps]==1:
1894 state.apHeading = None
1895
[16]1896 return state
[314]1897
[7]1898#------------------------------------------------------------------------------
1899
[662]1900class MajesticDH8DModel(DH8DModel):
1901 """Model handler for the Majestic Dash 8-Q400."""
1902 @staticmethod
[968]1903 def doesHandle(aircraft, aircraftName):
[662]1904 """Determine if this model handler handles the aircraft with the given
1905 name."""
[968]1906 (name, airPath) = aircraftName
[662]1907 return aircraft.type==const.AIRCRAFT_DH8D and \
1908 (name.find("MJC8Q400")!=-1 or \
1909 airPath.lower().find("mjc8q400") or \
1910 airPath.lower().find("mjc8q4.air"))
1911
1912 @property
1913 def name(self):
1914 """Get the name for this aircraft model."""
1915 return "FSUIPC/Majestic Bombardier Dash 8-Q400"
1916
1917 def getAircraftState(self, aircraft, timestamp, data):
1918 """Get the aircraft state.
1919
1920 Get it from the parent, and then clear the anti-collision and landing
1921 lights."""
1922 state = super(MajesticDH8DModel, self).getAircraftState(aircraft,
1923 timestamp,
1924 data)
1925 state.antiCollisionLightsOn = None
1926 state.strobeLightsOn = None
[667]1927 state.pitotHeatOn = None
[662]1928
[668]1929 # G-load seems to be offset by -1.0 (i.e a value of 0 seem to mean
1930 # a G-load of 1.0)
1931 state.gLoad += 1.0
1932
[669]1933 # None of the gear values seem to work correctly
1934 state.gearsDown = state.gearControlDown
1935
[766]1936 # Th N1 values cannot be read either
1937 state.n1 = [None, None]
1938
[662]1939 return state
1940
1941#------------------------------------------------------------------------------
1942
[7]1943class CRJ2Model(GenericAircraftModel):
1944 """Generic model for the Bombardier CRJ-200 aircraft."""
[330]1945 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
[274]1946
[7]1947 def __init__(self):
1948 """Construct the model."""
[8]1949 super(CRJ2Model, self). \
[7]1950 __init__(flapsNotches = [0, 8, 20, 30, 45],
[274]1951 fuelTanks = CRJ2Model.fuelTanks,
[7]1952 numEngines = 2)
1953
1954 @property
1955 def name(self):
1956 """Get the name for this aircraft model."""
1957 return "FSUIPC/Generic Bombardier CRJ-200"
1958
1959#------------------------------------------------------------------------------
1960
1961class F70Model(GenericAircraftModel):
1962 """Generic model for the Fokker F70 aircraft."""
[330]1963 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
[274]1964
[7]1965 def __init__(self):
1966 """Construct the model."""
[8]1967 super(F70Model, self). \
[7]1968 __init__(flapsNotches = [0, 8, 15, 25, 42],
[274]1969 fuelTanks = F70Model.fuelTanks,
[7]1970 numEngines = 2)
1971
1972 @property
1973 def name(self):
1974 """Get the name for this aircraft model."""
1975 return "FSUIPC/Generic Fokker 70"
1976
1977#------------------------------------------------------------------------------
1978
[314]1979class DAF70Model(F70Model):
1980 """Model for the Digital Aviation F70 implementation on FS9."""
1981 @staticmethod
[968]1982 def doesHandle(aircraft, aircraftName):
[314]1983 """Determine if this model handler handles the aircraft with the given
1984 name."""
[968]1985 (name, airPath) = aircraftName
[314]1986 return aircraft.type == const.AIRCRAFT_F70 and \
1987 (airPath.endswith("fokker70_2k4_v4.1.air") or
[780]1988 airPath.endswith("fokker70_2k4_v4.3.air") or
1989 airPath.lower().endswith("fokker70_fsx_v4.3.air"))
[314]1990
1991 @property
1992 def name(self):
1993 """Get the name for this aircraft model."""
1994 return "FSUIPC/Digital Aviation Fokker 70"
1995
1996 def getAircraftState(self, aircraft, timestamp, data):
1997 """Get the aircraft state.
1998
1999 Get it from the parent, and then invert the pitot heat state."""
2000 state = super(DAF70Model, self).getAircraftState(aircraft,
2001 timestamp,
2002 data)
[341]2003 state.navLightsOn = None
[314]2004 state.landingLightsOn = None
[366]2005
[394]2006 state.altimeterReliable = False
2007
[366]2008 state.ils = state.nav1
2009 state.ils_obs = state.nav1_obs
2010 state.ils_manual = state.nav1_manual
2011
2012 state.nav1 = state.nav2
2013 state.nav1_obs = state.nav2_obs
2014 state.nav1_manual = aircraft.flight.stage!=const.STAGE_CRUISE
2015
2016 state.nav2 = None
2017 state.nav2_obs = None
2018 state.nav2_manual = False
2019
2020 state.autoXPDR = True
[314]2021
2022 return state
2023
2024#------------------------------------------------------------------------------
2025
[7]2026class DC3Model(GenericAircraftModel):
2027 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
[289]2028 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
[330]2029 const.FUELTANK_RIGHT]
[274]2030 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
2031 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
2032
[7]2033 def __init__(self):
2034 """Construct the model."""
[8]2035 super(DC3Model, self). \
[7]2036 __init__(flapsNotches = [0, 15, 30, 45],
[274]2037 fuelTanks = DC3Model.fuelTanks,
[263]2038 numEngines = 2, isN1 = False)
[289]2039 self._leftLevel = 0.0
2040 self._rightLevel = 0.0
[7]2041
2042 @property
2043 def name(self):
2044 """Get the name for this aircraft model."""
[263]2045 return "FSUIPC/Generic Lisunov Li-2 (DC-3)"
[7]2046
[274]2047 def _convertFuelData(self, data, index = 0, addCapacities = False):
2048 """Convert the given data into a fuel info list.
2049
2050 It assumes to receive the 3 fuel tanks as seen above (left,
2051 centre and right) and converts it to left aux, left, right,
2052 and right aux. The amount in the left tank goes into left aux,
2053 the amount of the right tank goes into right aux and the
2054 amount of the centre tank goes into the left and right tanks
2055 evenly distributed."""
2056 (rawFuelData, totalFuel) = \
2057 super(DC3Model, self)._convertFuelData(data, index, addCapacities)
2058
2059 centreAmount = rawFuelData[1][1]
2060 if addCapacities:
2061 centreCapacity = rawFuelData[1][2]
[289]2062 self._leftLevel = self._rightLevel = \
2063 centreAmount / centreCapacity / 2.0
[330]2064 fuelData = [(const.FUELTANK_LEFT_AUX,
[274]2065 rawFuelData[0][1], rawFuelData[0][2]),
2066 (const.FUELTANK_LEFT,
2067 centreAmount/2.0, centreCapacity/2.0),
2068 (const.FUELTANK_RIGHT,
2069 centreAmount/2.0, centreCapacity/2.0),
2070 (const.FUELTANK_RIGHT_AUX,
2071 rawFuelData[2][1], rawFuelData[2][2])]
2072 else:
2073 fuelData = [(const.FUELTANK_LEFT_AUX, rawFuelData[0][1]),
2074 (const.FUELTANK_LEFT, centreAmount/2.0),
2075 (const.FUELTANK_RIGHT, centreAmount/2.0),
2076 (const.FUELTANK_RIGHT_AUX, rawFuelData[2][1])]
2077
2078 return (fuelData, totalFuel)
2079
2080 def setFuelLevel(self, handler, levels):
2081 """Set the fuel level.
2082
2083 See the description of Simulator.setFuelLevel. This
2084 implementation assumes to get the four-tank representation,
2085 as returned by getFuel()."""
[289]2086 leftLevel = None
2087 centreLevel = None
2088 rightLevel = None
[330]2089
[274]2090 for (tank, level) in levels:
[289]2091 if tank==const.FUELTANK_LEFT_AUX:
2092 leftLevel = level if leftLevel is None else (leftLevel + level)
2093 elif tank==const.FUELTANK_LEFT:
2094 level /= 2.0
2095 centreLevel = (self._rightLevel + level) \
2096 if centreLevel is None else (centreLevel + level)
2097 self._leftLevel = level
2098 elif tank==const.FUELTANK_RIGHT:
2099 level /= 2.0
2100 centreLevel = (self._leftLevel + level) \
2101 if centreLevel is None else (centreLevel + level)
2102 self._rightLevel = level
2103 elif tank==const.FUELTANK_RIGHT_AUX:
2104 rightLevel = level if rightLevel is None \
2105 else (rightLevel + level)
[274]2106
[289]2107 levels = []
2108 if leftLevel is not None: levels.append((const.FUELTANK_LEFT,
2109 leftLevel))
2110 if centreLevel is not None: levels.append((const.FUELTANK_CENTRE,
2111 centreLevel))
2112 if rightLevel is not None: levels.append((const.FUELTANK_RIGHT,
2113 rightLevel))
2114
2115 super(DC3Model, self).setFuelLevel(handler, levels)
[274]2116
[7]2117#------------------------------------------------------------------------------
2118
2119class T134Model(GenericAircraftModel):
2120 """Generic model for the Tupolev Tu-134 aircraft."""
[274]2121 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
2122 const.FUELTANK_LEFT_AUX,
2123 const.FUELTANK_CENTRE,
2124 const.FUELTANK_RIGHT_AUX,
2125 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
2126
[7]2127 def __init__(self):
2128 """Construct the model."""
[8]2129 super(T134Model, self). \
[7]2130 __init__(flapsNotches = [0, 10, 20, 30],
[274]2131 fuelTanks = T134Model.fuelTanks,
[7]2132 numEngines = 2)
2133
2134 @property
2135 def name(self):
2136 """Get the name for this aircraft model."""
2137 return "FSUIPC/Generic Tupolev Tu-134"
2138
2139#------------------------------------------------------------------------------
2140
2141class T154Model(GenericAircraftModel):
2142 """Generic model for the Tupolev Tu-134 aircraft."""
[274]2143 fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
2144 const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
2145 const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
2146
[7]2147 def __init__(self):
2148 """Construct the model."""
[8]2149 super(T154Model, self). \
[7]2150 __init__(flapsNotches = [0, 15, 28, 45],
[274]2151 fuelTanks = T154Model.fuelTanks,
[7]2152 numEngines = 3)
2153
2154 @property
2155 def name(self):
2156 """Get the name for this aircraft model."""
2157 return "FSUIPC/Generic Tupolev Tu-154"
2158
[8]2159 def getAircraftState(self, aircraft, timestamp, data):
[7]2160 """Get an aircraft state object for the given monitoring data.
2161
2162 This removes the reverser value for the middle engine."""
[8]2163 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
[7]2164 del state.reverser[1]
2165 return state
2166
2167#------------------------------------------------------------------------------
2168
[368]2169class PTT154Model(T154Model):
2170 """Project Tupolev Tu-154."""
2171 @staticmethod
[968]2172 def doesHandle(aircraft, aircraftName):
[368]2173 """Determine if this model handler handles the aircraft with the given
2174 name."""
[968]2175 (name, airPath) = aircraftName
[919]2176 print("PTT154Model.doesHandle", aircraft.type, name, airPath)
[368]2177 return aircraft.type==const.AIRCRAFT_T154 and \
[530]2178 (name.find("Tu-154")!=-1 or name.find("Tu154B")!=-1) and \
[368]2179 os.path.basename(airPath).startswith("154b_")
2180
2181 def __init__(self):
2182 """Construct the model."""
2183 super(PTT154Model, self).__init__()
2184
2185 @property
2186 def name(self):
2187 """Get the name for this aircraft model."""
2188 return "FSUIPC/Project Tupolev Tu-154"
2189
[529]2190 def addMonitoringData(self, data, fsType):
2191 """Add the model-specific monitoring data to the given array.
2192
2193 It only stores the flight simulator type."""
[530]2194 super(PTT154Model, self).addMonitoringData(data, fsType)
[529]2195
[368]2196 def getAircraftState(self, aircraft, timestamp, data):
2197 """Get an aircraft state object for the given monitoring data.
2198
2199 This removes the reverser value for the middle engine."""
2200 state = super(PTT154Model, self).getAircraftState(aircraft, timestamp, data)
2201
[660]2202 if self._fsType==const.SIM_MSFSX or self._fsType==const.SIM_P3D:
[529]2203 state.xpdrC = None
2204
[368]2205 return state
2206
2207
2208#------------------------------------------------------------------------------
2209
[7]2210class YK40Model(GenericAircraftModel):
2211 """Generic model for the Yakovlev Yak-40 aircraft."""
[274]2212 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
[330]2213
[7]2214 def __init__(self):
2215 """Construct the model."""
[8]2216 super(YK40Model, self). \
[7]2217 __init__(flapsNotches = [0, 20, 35],
[274]2218 fuelTanks = YK40Model.fuelTanks,
[7]2219 numEngines = 2)
2220
2221 @property
2222 def name(self):
2223 """Get the name for this aircraft model."""
2224 return "FSUIPC/Generic Yakovlev Yak-40"
2225
2226#------------------------------------------------------------------------------
2227
[443]2228class B462Model(GenericAircraftModel):
2229 """Generic model for the British Aerospace BAe 146-200 aircraft."""
2230 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
2231 const.FUELTANK_RIGHT]
2232
2233 def __init__(self):
2234 """Construct the model."""
2235 super(B462Model, self). \
2236 __init__(flapsNotches = [0, 18, 24, 30, 33],
2237 fuelTanks = B462Model.fuelTanks,
2238 numEngines = 4)
2239
2240 @property
2241 def name(self):
2242 """Get the name for this aircraft model."""
2243 return "FSUIPC/Generic British Aerospace 146"
2244
2245 def getAircraftState(self, aircraft, timestamp, data):
2246 """Get an aircraft state object for the given monitoring data.
2247
2248 This removes the reverser value for the middle engine."""
2249 state = super(B462Model, self).getAircraftState(aircraft, timestamp, data)
2250 state.reverser = []
2251 return state
2252
2253#------------------------------------------------------------------------------
2254
[191]2255_genericModels = { const.AIRCRAFT_B736 : B737Model,
2256 const.AIRCRAFT_B737 : B737Model,
2257 const.AIRCRAFT_B738 : B737Model,
2258 const.AIRCRAFT_B738C : B737Model,
[790]2259 const.AIRCRAFT_B732 : B737Model,
[191]2260 const.AIRCRAFT_B733 : B737Model,
2261 const.AIRCRAFT_B734 : B737Model,
2262 const.AIRCRAFT_B735 : B737Model,
2263 const.AIRCRAFT_DH8D : DH8DModel,
2264 const.AIRCRAFT_B762 : B767Model,
2265 const.AIRCRAFT_B763 : B767Model,
[381]2266 const.AIRCRAFT_CRJ2 : CRJ2Model,
[191]2267 const.AIRCRAFT_F70 : F70Model,
2268 const.AIRCRAFT_DC3 : DC3Model,
2269 const.AIRCRAFT_T134 : T134Model,
2270 const.AIRCRAFT_T154 : T154Model,
[443]2271 const.AIRCRAFT_YK40 : YK40Model,
2272 const.AIRCRAFT_B462 : B462Model }
[8]2273
2274#------------------------------------------------------------------------------
2275
2276AircraftModel.registerSpecial(PMDGBoeing737NGModel)
[16]2277AircraftModel.registerSpecial(DreamwingsDH8DModel)
[662]2278AircraftModel.registerSpecial(MajesticDH8DModel)
[314]2279AircraftModel.registerSpecial(DAF70Model)
[368]2280AircraftModel.registerSpecial(PTT154Model)
[8]2281
2282#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.