source: src/mlx/fsuipc.py@ 1161:38b1f077d211

python3
Last change on this file since 1161:38b1f077d211 was 1137:87a29e5e4b2d, checked in by István Váradi <ivaradi@…>, 8 months ago

The G-load is measured during flare and the value at touchdown is logged (re #385)

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